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