Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06e89-93a2-7aa2-82b3-ce5ccd2682e6
179 lines
6.4 KiB
Python
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
|