test-driver/src/testdriver/evidence.py

81 lines
2.4 KiB
Python
Raw Normal View History

"""Stratified evidence — decision D-01.
S1 Surface how an action was performed (selectors, routes, payloads)
S2 Realization whether it happened, and through which surface
S3 Judgment whether that was correct
Each stratum has a different authority. Models may write S1. Nothing but an
independent Observer writes S2 or S3.
See docs/TestDriverClassificationDesign.md, Part A.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from enum import Enum
from typing import Any
class Stratum(str, Enum):
SURFACE = "S1"
REALIZATION = "S2"
JUDGMENT = "S3"
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(frozen=True, slots=True)
class Observation:
"""One recorded fact, attributed to a stratum and a collector.
`collector` is never an actor for S2/S3 observations. The runner enforces
this; see runner._assert_collector_independence.
"""
id: str
stratum: Stratum
collector: str
step_id: str | None
kind: str
data: dict[str, Any]
at: str = field(default_factory=_now)
@dataclass(slots=True)
class EvidencePack:
"""Everything retained from one run.
The pack must be sufficient to replay the run and to diagnose a finding
without the original process. An assertion that cannot be supported from the
pack is an EVIDENCE_FAILURE, not a defect in the system under test.
"""
run_id: str
scenario_id: str
use_case_id: str
sut_version: str
started_at: str = field(default_factory=_now)
finished_at: str | None = None
observations: list[Observation] = field(default_factory=list)
verdicts: list[dict[str, Any]] = field(default_factory=list)
energy_events: list[dict[str, Any]] = field(default_factory=list)
provenance_index: dict[str, str] = field(default_factory=dict)
def record(self, observation: Observation) -> None:
self.observations.append(observation)
def of_stratum(self, stratum: Stratum) -> list[Observation]:
return [o for o in self.observations if o.stratum is stratum]
def to_json(self) -> str:
payload = asdict(self)
payload["observations"] = [
{**asdict(o), "stratum": o.stratum.value} for o in self.observations
]
return json.dumps(payload, indent=2, sort_keys=True, default=str)