Admit source evidence snapshots and harden stream completeness

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06e89-93a2-7aa2-82b3-ce5ccd2682e6
This commit is contained in:
tegwick 2026-09-05 00:42:19 +02:00
parent 824fb1b966
commit 31e9963933
34 changed files with 1057 additions and 122 deletions

View file

@ -50,8 +50,16 @@ def observation_from_audit_event(
evidence_class=resolved_class,
event_class=resolved_event_class,
deny_reason=_optional_str(event.get("deny_reason")),
identity_binding=identity_binding,
egress_destination=egress_destination,
identity_binding=(
_optional_str(event.get("identity_binding"))
if "identity_binding" in event
else identity_binding
),
egress_destination=(
_optional_str(event.get("egress_destination"))
if "egress_destination" in event
else egress_destination
),
latency_ms=_optional_int(event.get("latency_ms")),
result_count=_optional_int(event.get("result_count")),
policy_version=_optional_int(event.get("policy_version")),

View file

@ -0,0 +1,125 @@
"""Pure admission of the Engine's non-secret secret-use snapshot envelope.
Callers supply an Engine response and explicit catalog-to-tenant bindings.
A snapshot is not an authorization event or proof of complete observation.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from kings_guard.cadence import parse_interval, parse_timestamp
from kings_guard.contracts import StreamCompleteness
@dataclass(frozen=True, slots=True)
class SecretUseLane:
catalog_id: str
tenant_id: str
subject_id: str
stage: str
kind: str
ready: bool | None
revocation_attempted: bool | None
revocation_succeeded: bool | None
lifecycle_operation: str | None
evidence_kind: str | None
@dataclass(frozen=True, slots=True)
class SecretUseSnapshot:
as_of: str
lanes: tuple[SecretUseLane, ...]
heartbeat_interval: timedelta
findings: tuple[str, ...]
completeness: StreamCompleteness = StreamCompleteness.UNKNOWN
source_system: str = "secrets-engine"
def snapshot_from_engine(
payload: Mapping[str, Any],
*,
lane_bindings: Mapping[str, tuple[str, str]],
now: str,
) -> SecretUseSnapshot:
"""Admit scoped snapshot metadata; freshness never asserts event recency.
Bindings are caller-owned (tenant_id, subject_id), never inferred from paths.
Omitted evidence stays None. The response's decision id, session handle,
mount, path and arbitrary extra fields are deliberately not retained.
"""
if payload.get("surface") != "secret-use-evidence":
raise ValueError("unsupported Engine snapshot surface")
if payload.get("completeness_claimed") is not False:
raise ValueError("secret-use snapshot must explicitly deny completeness")
as_of = _text(payload, "as_of")
age = parse_timestamp(now) - parse_timestamp(as_of)
cadence = payload.get("cadence")
if not isinstance(cadence, Mapping) or cadence.get("form") != "heartbeat":
raise ValueError("expected source-declared heartbeat cadence")
interval = parse_interval(_text(cadence, "interval"))
if interval <= timedelta(0):
raise ValueError("heartbeat interval must be positive")
raw_lanes = payload.get("lanes")
if not isinstance(raw_lanes, list):
raise ValueError("snapshot lanes must be a list")
lanes: list[SecretUseLane] = []
seen: set[str] = set()
for row in raw_lanes:
if not isinstance(row, Mapping):
raise ValueError("snapshot lane must be an object")
catalog_id = _text(row, "catalog_id")
if catalog_id in seen or catalog_id not in lane_bindings:
raise ValueError("duplicate or unbound catalog lane")
seen.add(catalog_id)
binding = lane_bindings[catalog_id]
if (not isinstance(binding, tuple) or len(binding) != 2
or any(not isinstance(value, str) or not value.strip() for value in binding)):
raise ValueError("each lane needs explicit tenant and subject scope")
if parse_timestamp(_text(row, "as_of")) != parse_timestamp(as_of):
raise ValueError("lane timestamp differs from the snapshot envelope")
operation = _optional_text(row, "lifecycle_operation")
if operation not in {None, "suspend", "deactivate", "destroy", "revoke"}:
raise ValueError("unsupported lifecycle operation")
evidence_kind = _optional_text(row, "evidence_kind")
if evidence_kind not in {None, "attributive", "load-bearing", "heartbeat"}:
raise ValueError("unsupported evidence kind")
lanes.append(SecretUseLane(
catalog_id=catalog_id,
tenant_id=binding[0],
subject_id=binding[1],
stage=_text(row, "stage"),
kind=_text(row, "kind"),
ready=_optional_bool(row, "ready"),
revocation_attempted=_optional_bool(row, "revocation_attempted"),
revocation_succeeded=_optional_bool(row, "revocation_succeeded"),
lifecycle_operation=operation,
evidence_kind=evidence_kind,
))
findings: list[str] = []
if age < timedelta(0):
findings.append("snapshot:future_timestamp")
elif age > interval:
findings.append("snapshot:stale")
return SecretUseSnapshot(as_of, tuple(lanes), interval, tuple(findings))
def _text(payload: Mapping[str, Any], key: str) -> str:
value = payload.get(key)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{key} must be a nonempty string")
return value
def _optional_text(payload: Mapping[str, Any], key: str) -> str | None:
return _text(payload, key) if key in payload else None
def _optional_bool(payload: Mapping[str, Any], key: str) -> bool | None:
if key not in payload:
return None
if type(payload[key]) is not bool:
raise ValueError(f"{key} must be a boolean when supplied")
return payload[key]

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import json
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
@ -32,15 +33,10 @@ def parse_interval(value: str | int) -> timedelta:
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]))
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}")
@ -71,10 +67,10 @@ class ReconciliationCadence:
@dataclass(frozen=True, slots=True)
class EmissionCadence:
"""Runtime view of the Taxonomy draft, loaded from the local worked example.
"""Runtime projection of InfoTechCanon standard/emission-cadence 0.1.
This is a consumer of the draft in `specs/EmissionCadenceDeclaration.md`,
not a competing schema. Ownership stays with Taxonomy.
NetKingdom classifications and local provenance live in extensions.
Canonical schema validation belongs to the owner's schema, not a local copy.
"""
schema_version: str
@ -106,12 +102,27 @@ def load_qonto_assistant_cadence() -> EmissionCadence:
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] = []
for item in data.get("sources", ()):
evidence_class = EvidenceClass(str(item["evidence_class"]))
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(
@ -150,11 +161,11 @@ def emission_cadence_from_dict(data: Mapping[str, Any]) -> EmissionCadence:
)
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")),
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(data.get("reference_instance", "GH-WP-0002-T04")),
reference_instance=str(provenance.get("reference_instance", data["declaration_id"])),
rates=tuple(rates),
heartbeats=tuple(heartbeats),
reconciliations=tuple(reconciliations),

View file

@ -140,12 +140,9 @@ class SecurityGenome:
permitted_protocols=frozenset(str(item) for item in data["permitted_protocols"]),
permitted_egress=frozenset(str(item) for item in data["permitted_egress"]),
data_classifications=tuple(str(item) for item in data.get("data_classifications", ())),
tolerances=tuple(
ToleranceRule.from_dict(item) for item in data.get("tolerances", ())
),
tolerances=tuple(ToleranceRule.from_dict(item) for item in data.get("tolerances", ())),
evidence_sources=tuple(
DeclaredEvidenceSource.from_dict(item)
for item in data.get("evidence_sources", ())
DeclaredEvidenceSource.from_dict(item) for item in data.get("evidence_sources", ())
),
)

View file

@ -1,35 +1,31 @@
{
"schema_version": "0.1",
"status": "taxonomy-draft",
"drafter": "kings-guard",
"owner": "Taxonomy",
"source": "qonto-assistant",
"belongs_alongside": "security_genome",
"reference_instance": "GH-WP-0002-T04",
"reference_source_declaration": "approval-engine/cadence.yaml",
"sources": [
{
"source_id": "qonto-assistant.audit.allow",
"source_system": "qonto-assistant",
"event_class": "audit.allow",
"evidence_class": "attributive",
"form": "expected-rate",
"window": "24h",
"window_seconds": 86400,
"expected_min": 1,
"drop_below": "finding",
"note": "Worked example of the volume form. qonto-assistant is called sporadically, so this rate is a SHOULD illustration, not a claim that completeness of allows is currently meaningful."
"extensions": {
"net-kingdom": {
"evidence_class": "attributive"
},
"kings-guard": {
"note": "Worked example of the volume form. qonto-assistant is called sporadically, so this rate is a SHOULD illustration, not a claim that completeness of allows is currently meaningful."
}
}
},
{
"source_id": "qonto-assistant.audit.deny",
"source_system": "qonto-assistant",
"event_class": "audit.deny",
"evidence_class": "load-bearing",
"form": "heartbeat-or-reconciliation",
"rate_monitoring": "forbidden",
"heartbeat": {
"event_class": "audit.heartbeat",
"interval": "24h",
"interval_seconds": 86400,
"assertion": "nothing-to-report",
"missing": "finding"
@ -38,7 +34,24 @@
"compare_local": "source_transition_counts.audit.deny",
"compare_observed": "evidence_counts.audit.deny",
"divergence": "finding"
},
"extensions": {
"net-kingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "forbidden"
}
}
}
]
],
"declaration_id": "kings-guard.qonto-worked-example",
"extensions": {
"kings-guard": {
"status": "taxonomy-draft",
"drafter": "kings-guard",
"owner": "Taxonomy",
"belongs_alongside": "security_genome",
"reference_instance": "GH-WP-0002-T04",
"reference_source_declaration": "approval-engine/cadence.yaml"
}
}
}

View file

@ -0,0 +1,38 @@
{
"schema_version": "0.1",
"source": "qonto-assistant",
"sources": [
{
"source_id": "qonto-assistant.audit.deny",
"source_system": "qonto-assistant",
"event_class": "audit.deny",
"form": "heartbeat-or-reconciliation",
"heartbeat": {
"event_class": "audit.heartbeat",
"interval_seconds": 86400,
"assertion": "nothing-to-report",
"missing": "finding"
},
"reconciliation": {
"compare_local": "source_transition_counts.audit.deny",
"compare_observed": "evidence_counts.audit.deny",
"divergence": "finding"
},
"extensions": {
"net-kingdom": {
"evidence_class": "load-bearing",
"rate_monitoring": "forbidden"
}
}
}
],
"declaration_id": "qonto-assistant.audit-source-cadence",
"extensions": {
"kings-guard": {
"status": "source-declared",
"drafter": "qonto-assistant",
"owner": "qonto-assistant",
"reference_instance": "QONTO-WP-0005"
}
}
}

View file

@ -8,12 +8,15 @@ from pathlib import Path
from typing import Any
from kings_guard.adapters import observation_from_audit_event
from kings_guard.contracts import ImmuneObservation, SecurityGenome
from kings_guard.contracts import (
ImmuneObservation,
ReconciliationView,
SecurityGenome,
StreamHeartbeat,
)
QONTO_ASSISTANT_SRC = Path("/home/worsch/qonto-assistant/src")
QONTO_POLICY = (
Path("/home/worsch/qonto-assistant/src/qonto_assistant/policy/qonto-v1.yaml")
)
QONTO_POLICY = Path("/home/worsch/qonto-assistant/src/qonto_assistant/policy/qonto-v1.yaml")
QONTO_FIXTURES = Path("/home/worsch/qonto-assistant/tests/fixtures/qonto")
@ -21,8 +24,12 @@ QONTO_FIXTURES = Path("/home/worsch/qonto-assistant/tests/fixtures/qonto")
class LiveQontoCapture:
events: tuple[dict[str, Any], ...]
observations: tuple[ImmuneObservation, ...]
heartbeats: tuple[StreamHeartbeat, ...]
reconciliation: ReconciliationView
mapping_notes: tuple[str, ...]
corrections_for_source: tuple[str, ...]
stream_notes: tuple[str, ...]
stream_corrections: tuple[str, ...]
def qonto_assistant_available() -> bool:
@ -36,9 +43,7 @@ def capture_qonto_assistant_events(genome: SecurityGenome) -> LiveQontoCapture:
client is opened; the source publishes its own stream.
"""
if not qonto_assistant_available():
raise FileNotFoundError(
f"qonto-assistant checkout not found at {QONTO_ASSISTANT_SRC}"
)
raise FileNotFoundError(f"qonto-assistant checkout not found at {QONTO_ASSISTANT_SRC}")
src = str(QONTO_ASSISTANT_SRC)
if src not in sys.path:
@ -53,6 +58,8 @@ def capture_qonto_assistant_events(genome: SecurityGenome) -> LiveQontoCapture:
from qonto_assistant.service import CapabilityService
events: list[dict[str, Any]] = []
audit_logger = AuditLogger(sink=events.append)
audit_logger.emit_heartbeat(reason="startup")
service = CapabilityService(
client=FixtureQontoClient(fixture_dir=QONTO_FIXTURES),
policy=PolicyEngine.from_file(
@ -60,7 +67,7 @@ def capture_qonto_assistant_events(genome: SecurityGenome) -> LiveQontoCapture:
required_scope="finance.qonto.read",
enforce_scope=False,
),
audit_logger=AuditLogger(sink=events.append),
audit_logger=audit_logger,
rate_limiter=RateLimiter(limit=100, window_seconds=60),
concurrency_limiter=ConcurrencyLimiter(limit=4),
)
@ -89,8 +96,13 @@ def capture_qonto_assistant_events(genome: SecurityGenome) -> LiveQontoCapture:
else:
raise RuntimeError("expected qonto-assistant to emit a deny for oversized page_size")
if len(events) < 2:
raise RuntimeError(f"expected allow and deny audit events, got {len(events)}")
audit_logger.emit_heartbeat(reason="periodic")
reconciliation_payload = audit_logger.reconciliation_snapshot()
request_events = tuple(
event for event in events if event.get("event_class") in {"audit.allow", "audit.deny"}
)
if len(request_events) < 2:
raise RuntimeError(f"expected allow and deny audit events, got {len(request_events)}")
observations = tuple(
observation_from_audit_event(
@ -101,14 +113,25 @@ def capture_qonto_assistant_events(genome: SecurityGenome) -> LiveQontoCapture:
egress_destination="qonto-thirdparty-api",
genome=genome,
)
for event in events
for event in request_events
)
mapping_notes, corrections = _confirm_mapping(events, observations)
mapping_notes, corrections = _confirm_mapping(request_events, observations)
heartbeats, reconciliation, stream_notes, stream_corrections = _confirm_stream(
events, reconciliation_payload
)
if corrections or stream_corrections:
raise ValueError("source capture failed admission: " + "; ".join(
(*corrections, *stream_corrections)
))
return LiveQontoCapture(
events=tuple(events),
events=request_events,
observations=observations,
heartbeats=heartbeats,
reconciliation=reconciliation,
mapping_notes=mapping_notes,
corrections_for_source=corrections,
stream_notes=stream_notes,
stream_corrections=stream_corrections,
)
@ -166,6 +189,14 @@ def _confirm_mapping(
corrections.append("actor mapping drifted")
if observation.decision.value != str(event["decision"]):
corrections.append("decision mapping drifted")
if "identity_binding" in event and observation.identity_binding != (
event["identity_binding"] or None
):
corrections.append("identity_binding mapping drifted")
if "egress_destination" in event and observation.egress_destination != (
event["egress_destination"] or None
):
corrections.append("egress_destination mapping drifted")
notes.append(
f"{event['request_id']}: decision={event['decision']} "
f"capability={event['capability']} protocol={event['protocol']} "
@ -196,3 +227,95 @@ def _read_jsonl(path: Path) -> list[dict[str, Any]]:
continue
events.append(json.loads(text))
return events
def _confirm_stream(
events: Sequence[Mapping[str, Any]],
snapshot: Mapping[str, Any],
) -> tuple[
tuple[StreamHeartbeat, ...],
ReconciliationView,
tuple[str, ...],
tuple[str, ...],
]:
notes: list[str] = []
corrections: list[str] = []
if not events:
raise ValueError("cannot validate an empty source capture")
instance_ids = {str(event.get("stream_instance_id")) for event in events}
if len(instance_ids) != 1 or "None" in instance_ids:
corrections.append("stream_instance_id is missing or changed within one capture")
if any(
type(event.get("stream_sequence")) is not int or event["stream_sequence"] < 1
for event in events
):
raise ValueError("stream_sequence must be a positive integer")
if snapshot.get("stream_instance_id") not in instance_ids:
corrections.append("reconciliation stream_instance_id does not match the stream")
stream_ids = {event.get("stream_id") for event in events}
if len(stream_ids) != 1 or None in stream_ids or snapshot.get("stream_id") not in stream_ids:
corrections.append("reconciliation stream_id is missing or does not match the stream")
sequences = [event["stream_sequence"] for event in events]
expected_sequences = list(range(1, len(events) + 1))
if sequences != expected_sequences:
corrections.append("stream_sequence is not contiguous from 1 within the instance")
if (type(snapshot.get("last_stream_sequence")) is not int
or snapshot["last_stream_sequence"] != sequences[-1]):
corrections.append("reconciliation last_stream_sequence does not match the stream")
heartbeat_events = [event for event in events if event.get("event_class") == "audit.heartbeat"]
heartbeats = tuple(
StreamHeartbeat(
source_system="qonto-assistant",
timestamp=str(event["timestamp"]),
event_class="audit.heartbeat",
assertion=str(event["assertion"]),
counts=_transition_counts(event),
)
for event in heartbeat_events
)
if not heartbeats:
corrections.append("source emitted no audit.heartbeat record")
running_counts = {"audit.allow": 0, "audit.deny": 0}
for event in events:
event_class = event.get("event_class")
if event_class in running_counts:
running_counts[event_class] += 1
elif event_class == "audit.heartbeat":
if _transition_counts(event) != running_counts:
corrections.append("heartbeat counts do not match preceding request events")
if event.get("assertion") not in {"nothing-to-report", "transitions-reported"}:
corrections.append("heartbeat assertion is not a recognized positive claim")
else:
corrections.append("source emitted an undeclared event class")
source_counts = _transition_counts(snapshot)
evidence_counts = {
event_class: sum(1 for event in events if event.get("event_class") == event_class)
for event_class in ("audit.allow", "audit.deny")
}
reconciliation = ReconciliationView(
source_counts=source_counts,
evidence_counts=evidence_counts,
)
if source_counts != evidence_counts:
corrections.append("reconciliation source counts do not match captured request events")
if events:
notes.append(
f"instance={next(iter(instance_ids))} sequence=1..{sequences[-1]} "
f"heartbeats={len(heartbeats)}"
)
notes.append(f"source_counts={source_counts} evidence_counts={evidence_counts}")
return heartbeats, reconciliation, tuple(notes), tuple(dict.fromkeys(corrections))
def _transition_counts(payload: Mapping[str, Any]) -> dict[str, int]:
counts = payload.get("source_transition_counts")
if not isinstance(counts, Mapping) or any(
type(counts.get(key)) is not int or counts[key] < 0
for key in ("audit.allow", "audit.deny")
):
raise ValueError("source transition counts must include nonnegative allow and deny counts")
return {key: counts[key] for key in ("audit.allow", "audit.deny")}

View file

@ -146,7 +146,9 @@ class PostureEvaluator:
findings.append("unexpected_egress_destination")
if observation.identity_binding and observation.identity_binding != "verified_token":
tolerance = _matching_tolerance(genome, "identity_binding", observation.identity_binding)
tolerance = _matching_tolerance(
genome, "identity_binding", observation.identity_binding
)
if tolerance is None:
findings.append("unverified_identity_binding")
else:
@ -357,9 +359,8 @@ def _build_rationale(
) -> str:
if posture is PostureLevel.HEALTHY:
if tolerated_findings:
content = (
"Healthy posture with tolerated deviations only: "
+ ", ".join(tolerated_findings)
content = "Healthy posture with tolerated deviations only: " + ", ".join(
tolerated_findings
)
else:
content = "Healthy posture: observation is compatible with declared intent."
@ -375,10 +376,7 @@ def _build_rationale(
if completeness is StreamCompleteness.COMPLETE:
stream_text = "Stream completeness is complete."
elif completeness is StreamCompleteness.DEGRADED:
stream_text = (
"This judgment rests on a stream I cannot vouch for "
f"({completeness_reason})."
)
stream_text = f"This judgment rests on a stream I cannot vouch for ({completeness_reason})."
else:
stream_text = (
"Stream completeness is unknown; confidence scores the record, "

View file

@ -31,6 +31,14 @@ def evaluate_stream(
now_dt = _as_datetime(now)
findings: list[str] = []
reasons: list[str] = []
observations = tuple(
item for item in observations
if item.source_system == cadence.source_system and parse_timestamp(item.timestamp) <= now_dt
)
heartbeats = tuple(
item for item in heartbeats
if item.source_system == cadence.source_system and parse_timestamp(item.timestamp) <= now_dt
)
observed_counts = _count_observations(observations)
watch_start = _as_datetime(watching_since) if watching_since is not None else None
@ -49,6 +57,8 @@ def evaluate_stream(
)
observed_counts[rate.event_class] = count
watched_long_enough = (now_dt - watch_start) >= rate.window
if not watched_long_enough:
reasons.append(f"rate window for {rate.event_class} has not been fully observed")
if watched_long_enough and count < rate.expected_min:
findings.append(f"{STREAM_FINDING_PREFIX}cadence_unmet:{rate.event_class}")
reasons.append(
@ -78,16 +88,19 @@ def evaluate_stream(
if cadence.reconciliations:
if reconciliation is None:
reasons.append(
"reconciliation view was not supplied; divergence cannot be ruled out"
)
reasons.append("reconciliation view was not supplied; divergence cannot be ruled out")
else:
for spec in cadence.reconciliations:
source_count = int(reconciliation.source_counts.get(spec.covered_event_class, 0))
evidence_count = int(
reconciliation.evidence_counts.get(spec.covered_event_class, 0)
)
if evidence_count < source_count:
source_count = reconciliation.source_counts.get(spec.covered_event_class)
evidence_count = reconciliation.evidence_counts.get(spec.covered_event_class)
if any(type(value) is not int or value < 0
for value in (source_count, evidence_count)):
reasons.append(
"valid reconciliation counts were not supplied for "
f"{spec.covered_event_class}"
)
continue
if evidence_count != source_count:
findings.append(
f"{STREAM_FINDING_PREFIX}reconciliation_divergence:{spec.covered_event_class}"
)
@ -98,9 +111,7 @@ def evaluate_stream(
completeness = _completeness(findings, reasons, cadence)
reason = (
"; ".join(reasons)
if reasons
else "declared cadence is met and no stream finding is open"
"; ".join(reasons) if reasons else "declared cadence is met and no stream finding is open"
)
return StreamAssessment(
completeness=completeness,
@ -118,9 +129,7 @@ def _completeness(
) -> StreamCompleteness:
if findings:
return StreamCompleteness.DEGRADED
heartbeat_pending = any("not yet due" in item for item in reasons)
reconciliation_unsupplied = any("was not supplied" in item for item in reasons)
if heartbeat_pending or reconciliation_unsupplied:
if reasons:
return StreamCompleteness.UNKNOWN
if not cadence.heartbeats and not cadence.rates and not cadence.reconciliations:
return StreamCompleteness.UNKNOWN