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:
parent
c85646dc3c
commit
9daea96c43
35 changed files with 2023 additions and 138 deletions
168
src/kings_guard/cadence.py
Normal file
168
src/kings_guard/cadence.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue