kings-guard/src/kings_guard/cadence.py
tegwick 31e9963933 Admit source evidence snapshots and harden stream completeness
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06e89-93a2-7aa2-82b3-ce5ccd2682e6
2026-09-05 00:42:19 +02:00

179 lines
6.4 KiB
Python

from __future__ import annotations
import json
import re
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]))
match = re.fullmatch(r"p(?:(\d+)d)?(?:t(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?)?", text)
if match and any(match.groups()):
days, hours, minutes, seconds = (int(value or 0) for value in match.groups())
return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
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 projection of InfoTechCanon standard/emission-cadence 0.1.
NetKingdom classifications and local provenance live in extensions.
Canonical schema validation belongs to the owner's schema, not a local copy.
"""
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 load_qonto_assistant_source_cadence() -> EmissionCadence:
"""Load qonto-assistant's shipped source-owned emission declaration."""
payload = json.loads(
files("kings_guard")
.joinpath("fixtures")
.joinpath("qonto_assistant_source_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] = []
if data.get("schema_version") != "0.1" or not data.get("declaration_id"):
raise ValueError("expected canonical emission-cadence 0.1 declaration")
provenance = data.get("extensions", {}).get("kings-guard", {})
for item in data["sources"]:
profile = item.get("extensions", {}).get("net-kingdom", {})
evidence_class = EvidenceClass(str(profile["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(provenance.get("status", "source-declared")),
drafter=str(provenance.get("drafter", data["source"])),
owner=str(provenance.get("owner", data["source"])),
source_system=str(data.get("source", data.get("source_system", "unknown"))),
reference_instance=str(provenance.get("reference_instance", data["declaration_id"])),
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