Finish KG-WP-0003: stream completeness and live qonto observation

Classify evidence as load-bearing or attributive, draft the emission-cadence
declaration for Taxonomy, treat silence as a stream finding, keep completeness
separate from record richness, forbid immune memory as a state plane, and make
containment proposals reconstructable to their origin. Observe real
qonto-assistant audit events; deny-class completeness stays unknown until the
source publishes a heartbeat.

Assistant: grok
Assistant-Session: 01a05ef1-9e5a-70f2-b0ff-0b05d6b38ae9
This commit is contained in:
tegwick 2026-09-02 00:11:57 +02:00
parent c85646dc3c
commit 9daea96c43
35 changed files with 2023 additions and 138 deletions

168
src/kings_guard/cadence.py Normal file
View file

@ -0,0 +1,168 @@
from __future__ import annotations
import json
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from importlib.resources import files
from typing import Any
from kings_guard.contracts import CadenceForm, EvidenceClass
def parse_timestamp(value: str) -> datetime:
text = value.strip()
if text.endswith("Z"):
text = text[:-1] + "+00:00"
parsed = datetime.fromisoformat(text)
if parsed.tzinfo is None:
return parsed.replace(tzinfo=UTC)
return parsed.astimezone(UTC)
def parse_interval(value: str | int) -> timedelta:
if isinstance(value, int):
return timedelta(seconds=value)
text = str(value).strip().lower()
if text.endswith("s") and text[:-1].isdigit():
return timedelta(seconds=int(text[:-1]))
if text.endswith("m") and text[:-1].isdigit():
return timedelta(minutes=int(text[:-1]))
if text.endswith("h") and text[:-1].isdigit():
return timedelta(hours=int(text[:-1]))
if text.endswith("d") and text[:-1].isdigit():
return timedelta(days=int(text[:-1]))
if text.startswith("pt"):
# Minimal ISO-8601 duration: PT24H, PT1H, PT30M.
amount = text[2:]
if amount.endswith("h") and amount[:-1].isdigit():
return timedelta(hours=int(amount[:-1]))
if amount.endswith("m") and amount[:-1].isdigit():
return timedelta(minutes=int(amount[:-1]))
if amount.endswith("s") and amount[:-1].isdigit():
return timedelta(seconds=int(amount[:-1]))
raise ValueError(f"unsupported interval: {value!r}")
@dataclass(frozen=True, slots=True)
class RateCadence:
event_class: str
evidence_class: EvidenceClass
window: timedelta
expected_min: int
@dataclass(frozen=True, slots=True)
class HeartbeatCadence:
event_class: str
covered_event_class: str
evidence_class: EvidenceClass
interval: timedelta
assertion: str
@dataclass(frozen=True, slots=True)
class ReconciliationCadence:
covered_event_class: str
evidence_class: EvidenceClass
local_field: str
observed_field: str
@dataclass(frozen=True, slots=True)
class EmissionCadence:
"""Runtime view of the Taxonomy draft, loaded from the local worked example.
This is a consumer of the draft in `specs/EmissionCadenceDeclaration.md`,
not a competing schema. Ownership stays with Taxonomy.
"""
schema_version: str
status: str
drafter: str
owner: str
source_system: str
reference_instance: str
rates: tuple[RateCadence, ...]
heartbeats: tuple[HeartbeatCadence, ...]
reconciliations: tuple[ReconciliationCadence, ...]
def forms(self) -> frozenset[CadenceForm]:
forms: set[CadenceForm] = set()
if self.rates:
forms.add("expected-rate")
if self.heartbeats or self.reconciliations:
forms.add("heartbeat-or-reconciliation")
return frozenset(forms)
def load_qonto_assistant_cadence() -> EmissionCadence:
payload = json.loads(
files("kings_guard")
.joinpath("fixtures")
.joinpath("qonto_assistant_cadence.json")
.read_text(encoding="utf-8")
)
return emission_cadence_from_dict(payload)
def emission_cadence_from_dict(data: Mapping[str, Any]) -> EmissionCadence:
rates: list[RateCadence] = []
heartbeats: list[HeartbeatCadence] = []
reconciliations: list[ReconciliationCadence] = []
for item in data.get("sources", ()):
evidence_class = EvidenceClass(str(item["evidence_class"]))
form = str(item["form"])
if form == "expected-rate":
rates.append(
RateCadence(
event_class=str(item["event_class"]),
evidence_class=evidence_class,
window=parse_interval(item.get("window_seconds", item.get("window"))),
expected_min=int(item["expected_min"]),
)
)
continue
if form != "heartbeat-or-reconciliation":
raise ValueError(f"unknown cadence form: {form}")
heartbeat = item.get("heartbeat") or {}
if heartbeat:
heartbeats.append(
HeartbeatCadence(
event_class=str(heartbeat["event_class"]),
covered_event_class=str(item["event_class"]),
evidence_class=evidence_class,
interval=parse_interval(
heartbeat.get("interval_seconds", heartbeat.get("interval"))
),
assertion=str(heartbeat.get("assertion", "nothing-to-report")),
)
)
reconciliation = item.get("reconciliation") or {}
if reconciliation:
reconciliations.append(
ReconciliationCadence(
covered_event_class=str(item["event_class"]),
evidence_class=evidence_class,
local_field=str(reconciliation.get("compare_local", "source_counts")),
observed_field=str(reconciliation.get("compare_observed", "evidence_counts")),
)
)
return EmissionCadence(
schema_version=str(data.get("schema_version", "0.1")),
status=str(data.get("status", "taxonomy-draft")),
drafter=str(data.get("drafter", "kings-guard")),
owner=str(data.get("owner", "Taxonomy")),
source_system=str(data.get("source", data.get("source_system", "unknown"))),
reference_instance=str(data.get("reference_instance", "GH-WP-0002-T04")),
rates=tuple(rates),
heartbeats=tuple(heartbeats),
reconciliations=tuple(reconciliations),
)
def count_event_classes(event_classes: Sequence[str]) -> dict[str, int]:
counts: dict[str, int] = {}
for event_class in event_classes:
counts[event_class] = counts.get(event_class, 0) + 1
return counts