From 04e9573b5aee12496372a7fb48639acfcd926ca7 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 22 Aug 2026 23:21:07 +0200 Subject: [PATCH] T04: deterministic semantic kernel Alice/Bob/Carol runs end to end, deterministically, replayable from seed. 16 tests pass, no third-party dependencies. - src/testdriver: intent, provenance, world, actions, drivers, observers, oracles, evidence, energy, scenario, runner - lab/minimal.py: the SUT, exposing the independent observation channel required by D-07 - evidence is stratified S1/S2/S3; Runner refuses to attribute S2/S3 to an actor; claims are frozen and provenance-checked at construction - missing evidence yields INCONCLUSIVE, which outranks PASS in the run verdict - EnergyEvents captured, no scoring (H-005 dormant) The observation channel records both stored state and an out-of-band enforcement probe; their disagreement is an invariant and is what detects an authorization defect that leaves the audit trail intact. A seeded RevokeIsCosmetic lab fails the run via both the claim and that invariant. Also closes TD-WP-0001-T02 (stack and commands now exist). Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 1629012@bnt-lap001 Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39 --- AGENTS.md | 31 +++ README.md | 40 +++- WORK-RECORDS.md | 4 +- lab/__init__.py | 1 + lab/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 201 bytes lab/__pycache__/minimal.cpython-312.pyc | Bin 0 -> 10313 bytes lab/minimal.py | 191 ++++++++++++++++++ pyproject.toml | 20 ++ research/concepts/fitness-map.md | 31 +-- .../alice_bob_carol.cpython-312.pyc | Bin 0 -> 6248 bytes scenarios/alice_bob_carol.py | 135 +++++++++++++ src/testdriver/__init__.py | 23 +++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1358 bytes .../__pycache__/actions.cpython-312.pyc | Bin 0 -> 3602 bytes .../__pycache__/drivers.cpython-312.pyc | Bin 0 -> 4076 bytes .../__pycache__/energy.cpython-312.pyc | Bin 0 -> 2372 bytes .../__pycache__/evidence.cpython-312.pyc | Bin 0 -> 4542 bytes .../__pycache__/intent.cpython-312.pyc | Bin 0 -> 3078 bytes .../__pycache__/observers.cpython-312.pyc | Bin 0 -> 2724 bytes .../__pycache__/oracles.cpython-312.pyc | Bin 0 -> 4550 bytes .../__pycache__/provenance.cpython-312.pyc | Bin 0 -> 2366 bytes .../__pycache__/runner.cpython-312.pyc | Bin 0 -> 9536 bytes .../__pycache__/scenario.cpython-312.pyc | Bin 0 -> 2269 bytes .../__pycache__/world.cpython-312.pyc | Bin 0 -> 4538 bytes src/testdriver/actions.py | 57 ++++++ src/testdriver/drivers.py | 76 +++++++ src/testdriver/energy.py | 45 +++++ src/testdriver/evidence.py | 86 ++++++++ src/testdriver/intent.py | 63 ++++++ src/testdriver/observers.py | 47 +++++ src/testdriver/oracles.py | 106 ++++++++++ src/testdriver/provenance.py | 46 +++++ src/testdriver/runner.py | 190 +++++++++++++++++ src/testdriver/scenario.py | 47 +++++ src/testdriver/world.py | 79 ++++++++ ...el_guarantees.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 16008 bytes .../test_kernel_guarantees.cpython-312.pyc | Bin 0 -> 9051 bytes ...ence_scenario.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 13455 bytes tests/test_kernel_guarantees.py | 134 ++++++++++++ tests/test_reference_scenario.py | 70 +++++++ workplans/TD-WP-0001-statehub-bootstrap.md | 9 +- ...-WP-0002-vertical-spike-crystallization.md | 22 +- 42 files changed, 1533 insertions(+), 20 deletions(-) create mode 100644 lab/__init__.py create mode 100644 lab/__pycache__/__init__.cpython-312.pyc create mode 100644 lab/__pycache__/minimal.cpython-312.pyc create mode 100644 lab/minimal.py create mode 100644 pyproject.toml create mode 100644 scenarios/__pycache__/alice_bob_carol.cpython-312.pyc create mode 100644 scenarios/alice_bob_carol.py create mode 100644 src/testdriver/__init__.py create mode 100644 src/testdriver/__pycache__/__init__.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/actions.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/drivers.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/energy.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/evidence.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/intent.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/observers.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/oracles.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/provenance.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/runner.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/scenario.cpython-312.pyc create mode 100644 src/testdriver/__pycache__/world.cpython-312.pyc create mode 100644 src/testdriver/actions.py create mode 100644 src/testdriver/drivers.py create mode 100644 src/testdriver/energy.py create mode 100644 src/testdriver/evidence.py create mode 100644 src/testdriver/intent.py create mode 100644 src/testdriver/observers.py create mode 100644 src/testdriver/oracles.py create mode 100644 src/testdriver/provenance.py create mode 100644 src/testdriver/runner.py create mode 100644 src/testdriver/scenario.py create mode 100644 src/testdriver/world.py create mode 100644 tests/__pycache__/test_kernel_guarantees.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_kernel_guarantees.cpython-312.pyc create mode 100644 tests/__pycache__/test_reference_scenario.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/test_kernel_guarantees.py create mode 100644 tests/test_reference_scenario.py diff --git a/AGENTS.md b/AGENTS.md index a3f37ec..f6d4620 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,6 +133,37 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ {CREDENTIAL_ROUTING} + +## Stack and commands + +Python ≥ 3.11, stdlib only; pytest for the suite. No package manager step is +needed — `pyproject.toml` puts `src/` and the repo root on `pythonpath`. + +```bash +python3 -m pytest -q # run everything +python3 -m pytest -q -k oracle # narrow +``` + +Deliberately boring by decision (`docs/TestDriverResearchPrototype.md`): one +process, one database, one browser engine, one application under test. Novelty +belongs in the verification model, never in the infrastructure. Do not add a +dependency without a stated reason in the workplan. + +## Non-negotiables + +These are architectural, not stylistic. Breaking one silently defeats the +framework's purpose — see `docs/TestDriverClassificationDesign.md`. + +- **Claims and invariants are run inputs.** Never add a code path that lets + adaptation, retry, or a learned trajectory modify them (D-02). +- **S2/S3 evidence is never collected by an actor.** `Runner` enforces this; + do not weaken the check (D-01). +- **Model judgment is confined to S1** — locating controls, proposing paths. + Never verdicts, never claim evaluation (Concept Model § 2.3). +- **Missing evidence yields `INCONCLUSIVE`**, never a default pass or fail. +- **Claims require independent provenance.** `agent-from-implementation` output + is an exploratory hypothesis until a human promotes it (D-06). + diff --git a/README.md b/README.md index 4d1e923..8a3d0d8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,41 @@ # test-driver -Agentic framework for integration, end2end, multiuserinteraction, security testing based on usecases. \ No newline at end of file +Agentic framework for integration, end-to-end, multi-user interaction and +security testing, driven by use cases. + +Tests mature alongside the software they protect: fluid and agentic while +behaviour is changing, deterministic once it settles. See `INTENT.md` for the +thesis and `SCOPE.md` for boundaries. + +**Status:** research prototype. The deterministic kernel runs; agentic +realization, adaptation classification and crystallization are not built yet. +Current work: `workplans/TD-WP-0002-vertical-spike-crystallization.md`. + +## Run + +```bash +python3 -m pytest -q # the whole suite +python3 -m pytest -q tests/test_reference_scenario.py +``` + +No third-party dependencies. Python ≥ 3.11, pytest for the suite. + +## Layout + +``` +src/testdriver/ the kernel — intent, world, actions, drivers, + observers, oracles, evidence, runner +lab/ the system under test +scenarios/ reference scenarios +research/ hypotheses, experiments, findings, fitness map +docs/ concept model, improvement loop, milestones, design notes +history/ assessments and completed-work write-ups +``` + +## Reading order + +1. `INTENT.md` — the thesis +2. `docs/TestDriverConceptModel.md` — canonical concept set +3. `docs/TestDriverClassificationDesign.md` — why adaptation cannot normalize a + defect, and where model judgment is and is not permitted +4. `docs/TestDriverInitialMilestones.md` — canonical milestones M0–M10 diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index f0a9173..eeeaba5 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -8,14 +8,14 @@ | Kind | ID | Status | Lane | Source | | --- | --- | --- | --- | --- | -| workplan | TD-WP-0001 | ready | — | workplans/TD-WP-0001-statehub-bootstrap.md | +| workplan | TD-WP-0001 | active | — | workplans/TD-WP-0001-statehub-bootstrap.md | | workplan | TD-WP-0002 | active | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0001-T01 | done | — | workplans/TD-WP-0001-statehub-bootstrap.md | | task | TD-WP-0001-T02 | wait | — | workplans/TD-WP-0001-statehub-bootstrap.md | | task | TD-WP-0001-T03 | done | — | workplans/TD-WP-0001-statehub-bootstrap.md | | task | TD-WP-0002-T01 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T02 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | -| task | TD-WP-0002-T03 | todo | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | +| task | TD-WP-0002-T03 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T04 | todo | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T05 | todo | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T06 | todo | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | diff --git a/lab/__init__.py b/lab/__init__.py new file mode 100644 index 0000000..0e1aa10 --- /dev/null +++ b/lab/__init__.py @@ -0,0 +1 @@ +"""The test-driver lab — the system under test.""" diff --git a/lab/__pycache__/__init__.cpython-312.pyc b/lab/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..930c9db458ae53ec4340c4dcc5f6eb3424634df4 GIT binary patch literal 201 zcmX@j%ge<81V;qBvP^;WV-N=h7@>^M96-i&h7^VPO4oI8&D(2RmC6|ePCu} NWW2>-Qp5t}007aaHcbEk literal 0 HcmV?d00001 diff --git a/lab/__pycache__/minimal.cpython-312.pyc b/lab/__pycache__/minimal.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1dabc8c663410514f250adf8fb1984ff3923982 GIT binary patch literal 10313 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 token + _tokens: dict[str, str] = field(default_factory=dict) # token -> user_id + resources: dict[str, dict[str, Any]] = field(default_factory=dict) + grants: dict[tuple[str, str], Permission] = field(default_factory=dict) + audit: list[AuditRecord] = field(default_factory=list) + _seq: int = 0 + + # -- setup ----------------------------------------------------------- + + def add_user(self, user_id: str) -> str: + token = f"tok-{user_id}" + self.users[user_id] = token + self._tokens[token] = user_id + return token + + def _audit(self, event: str, actor_id: str, **kw: Any) -> None: + self._seq += 1 + self.audit.append(AuditRecord(self._seq, event, actor_id, **kw)) + + def _whoami(self, token: str) -> str: + if token not in self._tokens: + raise Denied("unknown token") + return self._tokens[token] + + # -- enforcement path (what actors use) ------------------------------ + + def request(self, token: str, op: str, **args: Any) -> Any: + """The single entry point actors go through. Authorization is enforced here.""" + user_id = self._whoami(token) + handler = getattr(self, f"_op_{op}", None) + if handler is None: + raise Denied(f"unknown operation {op!r}") + return handler(user_id, **args) + + def _op_create_resource(self, user_id: str, resource_id: str, content: str) -> dict: + self.resources[resource_id] = {"owner": user_id, "content": content} + self._audit("create", user_id, resource_id=resource_id) + return {"resource_id": resource_id} + + def _op_read_resource(self, user_id: str, resource_id: str) -> dict: + resource = self.resources.get(resource_id) + if resource is None: + raise Denied("no such resource") + if not self._may_read(user_id, resource_id): + raise Denied("not authorized to read") + return {"resource_id": resource_id, "content": resource["content"]} + + def _op_grant( + self, user_id: str, resource_id: str, subject_id: str, permission: Permission + ) -> dict: + resource = self.resources.get(resource_id) + if resource is None or resource["owner"] != user_id: + raise Denied("only the owner may grant") + self.grants[(resource_id, subject_id)] = permission + self._audit( + "grant", user_id, resource_id=resource_id, + subject_id=subject_id, permission=permission, + ) + return {"granted": permission} + + def _op_revoke(self, user_id: str, resource_id: str, subject_id: str) -> dict: + resource = self.resources.get(resource_id) + if resource is None or resource["owner"] != user_id: + raise Denied("only the owner may revoke") + self.grants.pop((resource_id, subject_id), None) + self._audit("revoke", user_id, resource_id=resource_id, subject_id=subject_id) + return {"revoked": True} + + def _may_read(self, user_id: str, resource_id: str) -> bool: + """The authorization rule as the system actually enforces it.""" + resource = self.resources.get(resource_id) + if resource is None: + return False + if resource["owner"] == user_id: + return True + return (resource_id, user_id) in self.grants + + +class ObservationChannel: + """Independent read access to lab state — decision D-07. + + Bypasses authorization deliberately. This is the channel test-driver requires + of any system under test, and the main integration burden the framework + imposes on an adopter. + """ + + def __init__(self, lab: MinimalLab) -> None: + self._lab = lab + + @property + def version(self) -> str: + return self._lab.version + + def state_permission(self, user_id: str, resource_id: str) -> str | None: + """What the stored record says, independent of any enforcement decision.""" + resource = self._lab.resources.get(resource_id) + if resource is None: + return None + if resource["owner"] == user_id: + return "OWNER" + return self._lab.grants.get((resource_id, user_id)) + + def probe_read(self, user_id: str, resource_id: str) -> bool: + """Exercise the enforcement path out-of-band and report what it did. + + This uses the subject's own credentials, which can look like a violation + of actor isolation but is not: independence means the *actor's report* is + never the evidence. The observer issues its own request and records the + raw outcome. No actor is ever asked whether it succeeded. + """ + token = self._lab.users.get(user_id) + if token is None: + return False + try: + self._lab.request(token, "read_resource", resource_id=resource_id) + except Denied: + return False + return True + + def audit_events(self, resource_id: str) -> list[dict[str, Any]]: + return [ + { + "sequence": r.sequence, + "event": r.event, + "actor_id": r.actor_id, + "subject_id": r.subject_id, + "permission": r.permission, + } + for r in self._lab.audit + if r.resource_id == resource_id + ] + + +def build_baseline() -> tuple[MinimalLab, dict[str, str]]: + """Known initial state, so that runs replay from the same starting point.""" + lab = MinimalLab() + tokens = {user: lab.add_user(user) for user in ("alice", "bob", "carol")} + return lab, tokens diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c4b9c17 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "test-driver" +version = "0.1.0" +description = "Use-case-driven verification framework whose tests mature alongside the software they protect" +requires-python = ">=3.11" +dependencies = [] + +[project.optional-dependencies] +dev = ["pytest>=7.4"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/testdriver"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src", "."] diff --git a/research/concepts/fitness-map.md b/research/concepts/fitness-map.md index f6c7630..ea72475 100644 --- a/research/concepts/fitness-map.md +++ b/research/concepts/fitness-map.md @@ -1,6 +1,6 @@ # Concept ↔ Implementation Fitness Map -**Updated:** 2026-08-22 (TD-WP-0002-T03) +**Updated:** 2026-08-22 (TD-WP-0002-T04) Traces each important concept to the implementation, experiment and evidence that support it. **Unsupported entries are the point of this map** — a concept with no @@ -13,25 +13,29 @@ Support levels follow `TestDriverImprovementLoop.md` §13: ## Current state -At the time of writing there is **no implementation**, so no concept can honestly -exceed `C1`. The initial classifications in §13 of the Improvement Loop +The deterministic kernel exists (T04) and its guarantees are covered by unit +tests. **Levels have not moved.** A passing unit test is not an experiment: it +shows the code does what its author intended, not that the concept holds under +the mutations it claims to survive. Levels rise when E-001/E-002/E-003 produce +evidence, not before. The implementation column below moves; the level column +does not. The initial classifications in §13 of the Improvement Loop (`Actor Isolation C2`, `Independent Oracles C2`) are corrected downward here: they were aspirational, not evidenced. | Concept | Level | Implementation | Experiment | Evidence | Open question | |---|---|---|---|---|---| -| `C-use-case` | C1 | — (T04) | — | — | Is a use case expressible without leaking mechanics? | -| `C-actor-isolation` | C1 | — (T04) | E-001 | — | Isolation is asserted by construction; unverified. | -| `C-semantic-action` | C1 | — (T04) | E-001 | — | Does identity survive restructuring better than a recorded sequence? (H-001) | -| `C-oracle-independence` | C1 | — (T04) | E-001, E-003 | — | Independence of components ≠ independence of belief. (H-004) | -| `C-evidence-pack` | C1 | — (T04) | — | — | What is the minimum sufficient for replay? | -| `C-observation-channel` | C1 | — (T05) | — | — | **D-07** — required of every system under test. Adoption cost unknown. | +| `C-use-case` | C1 | `intent.py` | — | — | Is a use case expressible without leaking mechanics? | +| `C-actor-isolation` | C1 | `world.py` | E-001 | — | Isolation is asserted by construction; unverified. | +| `C-semantic-action` | C1 | `actions.py` | E-001 | — | Does identity survive restructuring better than a recorded sequence? (H-001) | +| `C-oracle-independence` | C1 | `runner.py`, `oracles.py` | E-001, E-003 | — | Independence of components ≠ independence of belief. (H-004) | +| `C-evidence-pack` | C1 | `evidence.py` | — | — | What is the minimum sufficient for replay? | +| `C-observation-channel` | C1 | `lab/minimal.py` | — | — | **D-07** — required of every system under test. Adoption cost unknown. | | `C-adaptation` | C1 | — (T08) | E-001 | — | (H-002) | | `C-classification` | C1 | — (T08) | E-001, E-003 | — | Decision table is total on paper; unexercised. | | `C-crystallization` | C1 | — (T09) | E-002 | — | (H-003) | -| `C-intent-provenance` | C1 | — (T04) | E-003 | — | Constrains provenance, not quality. Accepted residual. | +| `C-intent-provenance` | C1 | `provenance.py` | E-003 | — | Constrains provenance, not quality. Accepted residual. | | `C-lineage` | C0 | — | — | — | Parent pointer only in the spike. | -| `C-energy` | C0 | events only (T04) | — | — | Dormant by decision. (H-005) | +| `C-energy` | C0 | `energy.py`, capture only | — | — | Dormant by decision. (H-005) | | `C-temperature` | C0 | — | — | — | Deferred. No implementation planned in TD-WP-0002. | | `C-confidence` | C0 | — | — | — | Deferred. | | `C-campaign` | C0 | — | — | — | Deferred. | @@ -49,4 +53,7 @@ most at risk of being built because they are easy and satisfying, and never validated. They are revisited at T10, where the question is not "when do we build these" but "does the evidence justify keeping them in the model at all". -**Implementation orphans** — none yet; there is no implementation. +**Implementation orphans** — none. Every module in `src/testdriver/` traces to a +concept above. `energy.py` is the one to watch: it exists solely to capture +events for a dormant hypothesis, and if T10 finds no use for the history it +should be removed rather than kept out of sentiment. diff --git a/scenarios/__pycache__/alice_bob_carol.cpython-312.pyc b/scenarios/__pycache__/alice_bob_carol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6b11c15875654ea58a26ce5a93134406dff4ed3 GIT binary patch literal 6248 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 bool: + return obs["probe_read:bob:R"] is True + + +def _bob_cannot_read(obs) -> bool: + return obs["probe_read:bob:R"] is False + + +def _carol_cannot_read(obs) -> bool: + return obs["probe_read:carol:R"] is False + + +def _enforcement_matches_record(obs) -> bool: + """Enforcement and stored record must agree about every watched subject. + + This invariant is what catches an authorization defect that leaves the audit + trail looking correct: the grant is recorded as revoked, yet the enforcement + path still allows the read. Neither observation alone would notice. + """ + for key, permitted in obs.items(): + if not key.startswith("probe_read:"): + continue + recorded = obs.get("state_permission:" + key.removeprefix("probe_read:")) + if permitted != (recorded is not None): + return False + return True + + +def _audit_is_append_only(obs) -> bool: + events = obs["audit:R"] + sequences = [e["sequence"] for e in events] + return sequences == sorted(sequences) + + +USE_CASE = UseCase( + id="uc-share-resource", + title="Share a resource and revoke the share", + narrative=( + "Alice owns resource R. Alice grants Bob READ access. Bob can read R. " + "Carol cannot read R. Alice revokes Bob's access. " + "Bob can no longer read R." + ), + provenance=Provenance.HUMAN, + source_ref="INTENT.md#first-reference-scenario", + claims=( + Claim("c-bob-reads", "Bob can read R after the grant", + Provenance.HUMAN, _bob_can_read, after_step="s2-grant"), + Claim("c-carol-denied", "Carol can never read R", + Provenance.HUMAN, _carol_cannot_read, after_step="s2-grant"), + Claim("c-bob-revoked", "Bob cannot read R after revocation", + Provenance.HUMAN, _bob_cannot_read, after_step="s3-revoke"), + ), + invariants=( + Invariant("i-enforcement-matches-record", + "Enforcement and the stored record agree for every subject", + Provenance.HUMAN, _enforcement_matches_record), + Invariant("i-audit-append-only", "The audit trail is append-only", + Provenance.HUMAN, _audit_is_append_only), + ), +) + + +def build(variant: str = "baseline"): + """Assemble world, driver, observer and asset from a known initial state.""" + lab, tokens = build_baseline() + cast = Cast() + for name in ("alice", "bob", "carol"): + cast.add(Actor(id=name, display_name=name.title(), + credentials={"token": tokens[name]})) + + world = World(id="w-baseline", sut=lab, sut_version=lab.version, + seed={"users": ["alice", "bob", "carol"], "resource": RESOURCE}, + cast=cast) + + scenario = Scenario( + id="sc-share-resource", + use_case=USE_CASE, + variant=variant, + watches=(Watch("bob", RESOURCE), Watch("carol", RESOURCE)), + steps=( + Step("s1-create", "alice", SemanticAction( + "create_resource", + {"resource_id": RESOURCE, "content": "the secret"}, + permitted_surfaces=API, + postcondition=lambda obs: "audit:R" in obs, + )), + Step("s2-grant", "alice", SemanticAction( + "grant_access", + {"resource_id": RESOURCE, "subject_id": "bob", "permission": "READ"}, + permitted_surfaces=API, + postcondition=lambda obs: obs["state_permission:bob:R"] == "READ", + )), + Step("s3-revoke", "alice", SemanticAction( + "revoke_access", + {"resource_id": RESOURCE, "subject_id": "bob"}, + permitted_surfaces=API, + postcondition=lambda obs: obs["state_permission:bob:R"] is None, + )), + ), + ) + + driver = DirectDriver(lab, tokens) + observer = StateObserver(ObservationChannel(lab), scenario.watches) + asset = VerificationAsset(id="va-share-resource", scenario=scenario, maturity="T5") + return world, driver, observer, asset, Oracle() diff --git a/src/testdriver/__init__.py b/src/testdriver/__init__.py new file mode 100644 index 0000000..d56c3ca --- /dev/null +++ b/src/testdriver/__init__.py @@ -0,0 +1,23 @@ +"""test-driver — verification assets that mature alongside the software they protect.""" + +from .actions import SemanticAction, Surface, SurfaceNotPermitted +from .drivers import DirectDriver, Realization +from .energy import EnergyEvent, EnergyEventType +from .evidence import EvidencePack, Observation, Stratum +from .intent import Claim, Invariant, UseCase +from .observers import StateObserver, Watch +from .oracles import Judgment, Oracle, Verdict, overall +from .provenance import InadmissibleProvenance, Provenance +from .runner import CollectorIndependenceError, Runner, RunResult +from .scenario import Scenario, Step, VerificationAsset +from .world import Actor, ActorIsolationError, Cast, World + +__all__ = [ + "Actor", "ActorIsolationError", "Cast", "Claim", "CollectorIndependenceError", + "DirectDriver", "EnergyEvent", "EnergyEventType", "EvidencePack", + "InadmissibleProvenance", "Invariant", "Judgment", "Observation", "Oracle", + "Provenance", "Realization", "RunResult", "Runner", "Scenario", + "SemanticAction", "StateObserver", "Step", "Stratum", "Surface", + "SurfaceNotPermitted", "UseCase", "VerificationAsset", "Verdict", "Watch", + "overall", +] diff --git a/src/testdriver/__pycache__/__init__.cpython-312.pyc b/src/testdriver/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..216faa64380a0459d764e2d73536f5c4786725f3 GIT binary patch literal 1358 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 O6#^3Neym~?+5JBZ?aZVA literal 0 HcmV?d00001 diff --git a/src/testdriver/__pycache__/drivers.cpython-312.pyc b/src/testdriver/__pycache__/drivers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6367df1bd014d84b15001e1a25b996d404139e40 GIT binary patch literal 4076 zcmbUkZEO_Bb@uDNeEzn|uO5 zH?$>^Q_%+0O170$CQ_vk!H@jTueKpls{W{5NI9*jO21U)pTVxu(m#D~_6}^H zw=-|%y_xs@?$_1T2?FDO{SSsCF#9tuDnkn=(&EQ3nIbmH5L>e4tdx^8vV?ObtK`&- zD&}fd%SAGgTr?AfwPr`Mv0OY8&m}Smv5sb|a>-0mA~K<>9kXLM<2U5+2ARA}?8GHv zS5+`K%5TV2i@hZ5RnzDVB}8Xx1hxjSwY2szwoYK{09#+-lc^tXNHyjgB|_57IZqka z88x#`f$E&*OxJfT-L!nibGdGwGaZ-vx~cO#8#FE2kw|puwjk3-TyKo)Icg1=u4C~I z{g|)Qoa1x7_c@)OcWmlfRJSR&m^0wG=k%RJ-q=pv0d|&8ZQXS31f!!K1B$UB#~P|! zf=BY4+Pn0DypIc)!ZYEOm9x|zf;E8hL-jF}CoGRKYWez~F8&0zOdy1Z?CtE`hZpGs zH0!zNxbAy`&KuyAAyeE#MjkV*EQt37sxu`8Ri^8DzA4lI%eZa&aKPj|B?n5EJ08SN zFwgfaFZ%^3k^r`*x2(M`p}Zj48>$SbH9&c+4-Q2JnOC50pz*Bdb1Sr62+M|)7Wcz? zicpe~z`f*?;8YpKmcc=k!?0tkFw?{=lF{raIu5SJBdK^$^_sAxcy=Lm3bkNX%w;>E z(2BLpY<9duKapn?jMWh++-E_eY-7W*3pKW5`L6?Zmkz?74dN9--cv{bS}*E7o=bxW zGaay0DiXvD!!>i%FoJ|(&-pHHT@=nw+Y|jF8tzj(FuVJtTkl;hI2#`G5 z+d1UrXy=&6xHZ)2Q||8$J+6~8ORUSw@WVSs$5{;!3Q!Ldz7fD%J;KbzCXG<>x|Gs6DaCG)C9gIJ9eOsIIbO3g{5W`Ln);OssR*1)bPC#>T(gL z3}Yvyu&3Y?D7YO7cUMLpq%ts=Zy1H<<;Lu&pq~YDz7@blQfgX1sa}qk*0;eYd2jvZ zQf<@p3v>HlSyJSd#H9MeqzDBToFuaLYu3IW>%bX!`-C!~7S%L)N3&#ySn}B|fYXW^P$gI1gRAp| zOP3q|+ip=qu0NJ9x2t3#0$j*2)}~dXD6J(0QcgsRN-?tbzAe3{OvK{QEdp0ewX4Q3 zR?$_X2>b>?s$%44(vPHxcrjj#*(w$@A4`B3|GS=xu}T-&3wn~^e?l80?Zn5zBF0w2 z(i4gQkGGEs5Tlr=SacZ6(5mq+vE^doUGh`qrcy~DmV6qz@rlHD620Z`m^4N%D5uGo z1nC44Mcjkn5uH&GH`%#7mKQE6XF)!srzFvXU`+9R)=$+0DmE?9)_kMu#ED})FN^vS zR9i#T8ZpY%UgQ&o=^#*nC1;&5Wmq<_dcTo})+LB{U$CgiNum-5sy`0zz-pYT5{XDW zLDcO)_D03qL?^{r7jjD@H%+7|hRqFo2@wjtV2R?4RiZpZ+aPLsp=m|lDmM-Xfsl7X z%t6|!s(T`Ko8{HMCzl`oZz{kKb`iq+hWwa&*{r~78l%$%7u zW{f+r-S=BsXRBwbuMK`M^8Uzt%ig!EC*{dwrTt(1UG2&1>7O@!w(d8ZezobhU42*7 zD}z%bS4XaGonNY z{~)S9lbDPzB$W7oRBGOKrElu=)zf#Ix6LcAbN!jQ*S|HFG3JhZ`%a_zdD8sXf`Vj^_)bvVFZMO2 zW9l!X4FEsUHm9q}t?GF9M&;JJgWb)_r?nEyKW)|k_M{?g4^S{HcI-I-%e!glozF6? z9f67sG;4=xD!OJ9vlr=)Ab1+Vsy=N8;t(J+z8%0N@<3L#*kV%C5({;t^|(}O=_zeE zQrd9%L0qegT>Sb{O`~=o?BS5IC)L2PUBZriFb^FBbnFbvFqG2D&mGE{ikJwvEp!o~ zP!YsL%T253Hp4DhgPuKWhmEM{$VD@WeSwG`@!VB%Jc=C-e=K~By$0v7B5)G`=w6Z@ z$eN`7y@p8be11Dk?m-YEg@VME^%$fIw0x_t(BHfyAf&pKU2I``nYw|Ge{bKyrm+$<8O5G!wIcT6O|b#=62)edF}OtWO~a<+ z*l-Cpuy14T40xp%_VAn#*Mk+F`t53!&O|b zD>#YO_!S0N&nRdnl;8&Qoap|B#ry!Xri&M;X*A*ZlH~^;i!^hC7pcNexqiT!W&lTR zBcSa4$VTd8!@gp)d~ODKrM^Vr67CghTVNze+AXj&7JjGj`azZpw}YSq@3QHn@)wUm zK?t*k3ztTY^9zYEW%$H0gD^vkz%XsY_rqkvqBbdrp$t44SPl(kj;o$aL4l-#e_&1& z-Im!6Y&1bs`4Bda0^UXxX#ymmNCYiGL`@fH)>|ty%$L;}wN%&3Y8@uE9Bwd0~DI)YR25 zS337`tzMj)t7vfP_Jx{SDuQTPoj(T@H$+y?tEB~?s)fE#)^*o#sIG^KuE#D1I;ZQG znuZ*tW?Fu-9OhwU5&ISg?a<}d!>qwsC~Ax^WAl6xU>7nx-{Aun_8E4bhhtlpY9C5 z-L7^9cC{;=!M*Ld&gjItG}6hBem=`~^;Fd%B~p;qtJDpXzU>A68{toY91LND0lp6Z z0(2do02C771nfkV6j4dwr8vcV2u;w zfo_p71(ghQG znt>T+EZ=MJZX2*MV8)T(4b&_7gl#yB#5lP+w8h7hJ#GPHd@k5V-^)MbzRle~|KP&C z3y<*eC%F6&mmlGocC0QGjlkS|BAnn4ik?3Q#BF4u*ABS4`JlT35w8BPLzXuk0>`_< z67CA{OtH!i0H=^(TqdH!aq$TbhdI0%6NWjD1ON9=0(^mflRo^lf7`V)om~Ff z>CV8ON7A0BZ|(c(=;ODJwP$WrA4$ia_76VYIrB0h=2PttZX`C4m`uHtg`_IP;pWP! zg#mU1X5PyhP5w~^yLfqr+0Cy6^<%ffFAx;j9A#9TOcI4J1mj5@@rEa9APFgpOoDEfxv8;G)(UC~) zUMijp2k?*%NZVlXet=}dfTb9UHb@@2r{!@EecOv2d9y;a=&(NZ&8f2$&C|}gloi>{ zMu3Nh=bU?a@44Uk;olO8s07!0UGL6L0`~>JY+fOskvHxDb5$~>ykyFzQj*I`UXhUx zlmhtxJk^p~4(5a9P(D-+=fgr5ENNveuazVDhvxj!iQA4~(UwQ36+PjO40p z+5i)A6`eU^*u*HfXgFtZQlV_xVP%-?Nlr_&;5v+t5av`}3eSpBD>;VAQ~qoR$!Tho ztaJ}*@mP?98r9y3Lj==OLaf>j)Oc4yMLXmKP1^yI=$cd^sQ>L~n7PL8q zTQpp;o`S;|tRfoC8aNd9S}nu$oEXHkrl%>xp+?m$I?QrwY4VC=(h?_SqekXHLSfHo zGU>QQ%bp=`Rl(MdTDD1HdEhITy!sZW>^!VmSaXs)NE}9ThrnYwN{Q(d_~;bnZiZQ4 zb*yA?4$dx!EoUgVX6$qswt326E*Vcpzr>SE$zIH`ZO0W>d@>dABBtRQ1sv@K;jBug z7c#hM72K5KX`rL7Ri>VXbl$Nk%%EoNY8jq^al2M%LPR{8;E*?V!s}IuN_iQet(Y=E zS;?zrARnx$sh}4Y$f%Y*B{v55LAVc1rb3>k>$Xv*y6#1Fz3iCP67q3f|7q1IH8tVf znbR+hAIo~N)7kNtC-UP{6R%Es+KDrn)*85vhx5);g&Z{ zV6IAT(-V?3+YBxcw=TCCjG%2Mf|d%H%@CW7v`c+{Gaz%)4S6yZWML3{0o$2ln&7H# z!HZbj!9X!whTHHLj0fVWI$Ays?04Yi$Sg{q#(J(CdFRLSValN)NKZ{)=EGvP1nO!XsiYV414JNs{*za2N+2E`W7j7mMFVY(H zAPO?F0BOUfsHYX(GClPi%r)PELHh>iM+gMwSx?2lz9D-_lTI7elB=Uv99Hub!<|fN ztP>o`5ag^Ih!@s%NGT9+NnLMC5xUNTXoF~B`;lx#6&pku5&}2>0gzuv_f$2gt;eNE z{NnLOqPMR0Hj>>JCmV_Wy4v4}CN54iqC4yA&bu8w*E6dfsl^vA1y62U*y(9%cvQLnFapDEbY3O*#Wv_3%BjM zxX^XBbcwb(JIpQ^*DY4tZ}vc8PMSN-UM$e&W9ANccA2}(K6rN5BB=o{o^2M=Q%2#O z5r@#o0!(Xu4Pn$ZEGTURqP0u_A&9*ux)G-p7(>cc?vl4C)VgUX78Vv72o6RoC8$eK zByo&i8XYKO#*FQ73ehqR2!lE$U^-_(O@n%c)==AKEZZoN3UdJaJPp_*KGd=DifUO9 zaPmJ|sW=P+QY;ZB5EF`pLCit4Wq&l0$xa^2>MxE@ynN<#c7#B&g6{){q1X}&r-a4V z0QzN8g+j>)CU-x6*bpGY3&9RBDq_4qZG%~kc&T!#7YramQ+0Ll8#W>ADI5b~1!a<& zI)nxrMoA2IZ1JKHJ?JV~+L%p_tbKLO@{?Ia^zQ3}e8R8Y?LNYVvmL3vaWU6891moIlOD8EuJ z?@+<|f|3I&3vO=9ZrNV=Tq?kh!b9+$>~QoKbawtOqt3M6%(nCWTXyetsB7ctD{_&jynP1CylG$Y~`wcue*5oTa`_wgxF2z58xIy}I}L z#g~>l2J7ncclV9{RbAbeU3_J^XSl9r?+%aLo?0D#VOi~6vg_>$>kZ-nIFi}FTD z4ffuT2ZJYMzsnNKoPyP*dW28#mutET2LQiZ3}R7YlR&&!>+g_qhJg0nCNPOn>;HJD z6#fVq#)lVjYZYh|7>0;A0{;j_`#6X)@!_pKX5dvI>j7Do?_)O}%9}395mkLWy-dh|BW9dzz%gmiAmTSLLCv pm4v)+T>`QZkd!^^_Domae(sCHnbv$P*5r)BV}1GogGm4b!D*8zHK6cAERyl3w&Nk0(c0G9Ucn|VLK znR)Lw@A>O$)zaWu7=GvUd{NVW#zF7NXNtb@D=6nRrr8?Pnc?d#!!~r(bAGO6+GZUJ%7fP9UBV z5eF#hTRLpZ#x2|FW$MvK_gCNW{EXR$c%&4NSPo+%q4UhW_=1Rw?@6$P-C&lM!6oHqk zkP;0n)=3dA@PNX8lM%sRi#@>|%37Y39^hvIQpm!Z2_(8{^hcPzF~uMUgMgr z1F4NuKxI3}3?O0dFj!K}Otn1B6;iDTTB&a>rQpwl!W$@+luW zPaGi=oeBM6Hktyn7341Gp0@>lU~mZ;+WV;E5%}Sh(B~yy0Rzdk@}o z2#vlm4a#{<^=)rxr@QyD2gQG;!@Rk9Cezi3`+?1@oOEf+j;-u^f_zQ zy#F^-&mYw@XXwX;U;lyR;iGSy1o^LdT?RH=z~)NI<|-RV*<2nUOv=ypN?PZ{k-uRy z@{UY!$Xj~?U_6?HngyJPVg}_42oJH%m|hn(*lJ1zF^n2=JEO-o)V70|5qF@9vHY*- zYV{9F1U4x#1|s8VmO{*FXGb5km2Y(f-^jPqm$rYkQ&onaM6K7S>x8ZAGlfShU@Q`Ioic+<>ix* zzngg3TvlJ4{rv3mnU%?-S4zkJ;^lZn?1C2rGEnS6@gRzaP++HkIAh?%L7X^*qF?Q9 zWb;0>qCl9O27zpTCS!ADaA|t2HnKF6RlRS?o@IWS99}%WGO%;iJpAq6eV6#mobUrh_E!`qk>P8Gz94fWIl9*Kb-xgd7>{!_W3aX9Y6rzAL@X^~X zJSkf&9Aa%s3gH}nHznp=82TcuN@)Sd`;{K*hSOdz9nSj1lj1Re;60Hag1DB`b$#6^ z=;rT3n!f87ZSVKm?q9S=ua&gw=(+g|?#GW_IP=XN6Dy++e=@r=dhnZpLyP70f@W0C z?Y_V+>%0CiD*93Vnuc;Cry0Air4#G-Y=&6+ae$!gLQq{F+YA}drvQR7g{&K>rg{Gp DrE&Cu literal 0 HcmV?d00001 diff --git a/src/testdriver/__pycache__/observers.cpython-312.pyc b/src/testdriver/__pycache__/observers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..193b6e8158d3ab4701dff7a3eb5c06d30072392b GIT binary patch literal 2724 zcmah~-D_M$6rcOG-O*fdQxO}1hcX<8~J6~E(0EW+Ayx!IY`-gx)kb>?p4 zE?X$H!YZN_9~vk^1W`*1`X~7AOV(C4UIZ(G;#+DIp--NfyVu5;;(a)GX3m*&&iv*# z=X{gRrVxC$2HstKD~8a|ViJ8uI8Zj;hrwk;P!SP@sE$%qi>e~Vnxj?qqFyzMhMen8 ztQs%ItBGPlK`Np;F^F+Jc1;aEspu(0;ujG~L@U>Nv5Lvi`g-!3vdK0Dw(0-arW%=i z*6M=ci}Reavy^eXVEGlwFgXs8>l8u2JE;FC7D78nf;=Bd9#sSDY9oML~;!#2lNYPq(% zP_8?G8@7Fpy(Jejd!gb_!&I0+A}MY!xRzgMRNyJFs)HA1H^Va$S_VKSAukFR67D*I z%|5eiCrsOOg#seU({9dNWD1LLZpsR&-vkBnN{|NBJYTx!c{NB9%eP97#d%Jd!-v~Z$q9Du=PL{U)zQmdzc%0-Q+Kv(S; zXyiy2qlS@>1;%TZU#c{pK7q%1eST3Ml2Pu}0rWf|ZZmmWUZSqWY!5G4u0Sy47r|E9 zm&By30ar(;o8~5%O!F$*5XZdp=%I>NrH7V)@ukWkpK^bSFdMjjh_jNM_omTH;DuU)rD3ZS z12Et@5O1TO^jB`>c3gOIZD8o8KE#G2lf1%`Fy0>GSAXBm>Zs27usbUqto=YS0~diBjGW<1A%`v@QA++oaJtHvuHuvWfc!CO|B zbQOFgUeyAAOI~BwTPiT9cqoYXUMCQ)A4qJ~9p5a=QqgGkN6dO%lHg1bUlL@XyxGS^ zDpL`7EF)a$;&Mv83I+=Zi5d#DQ#O8q)yv36=M?`ReN_FaGNQeJ zf!;DA4Bi{nQA?Rad1LmDGTltqm^V*N(Kx1?1ClLfjk2oEMb)2f#w<}Dr}HVO2h?YK zAOI71u|6Sofe2D}T?DzU9sCJ>*2E_-8md>eD4m7Xs2g#_qK-rY+ z4YW6CLx$l3GAuu~Q$$QeFQ~cmUr((Ntv>`-{7(?)(b|K!t^F``-$zfjlWW7fE}#DJ z^zSH^r=nspEa^nM((kOUtKLb0eM1{`D9tJo4q~tEanD&vvGs-EK<( zwyX}0tjw%DwwlX#G83>TyYqt=-g#kpw7dIYXZOMHGL!2Akli1H=z)DJ`5SvCe@9C4 zWu^Vx+C<^Y$*YsyiJ8vC%-7G|n0N`oA6VYAoL$Ww_%<`Pt_iza{e$h4tbMbh$uJ6y z&~5e+yX3zP^?VXTBRZ2$F+oix>LwFpV}n8n5;0K;0>iJ@92#Es17dYTh#?_lSrwPS z?vI2Z0Urk;YVb3+vbhV-f4_JCSEp|7J$nABws}K8x|YM|Pqs&H==cx4&)5+<5&LKJ zX&GB|kaAr#ZPR3;{{`uPx+vvLl$9{EnBS;DXO<Y?`-Fbz Wrn2)dby(T4jz9?QeYeF_p674Fal6m} literal 0 HcmV?d00001 diff --git a/src/testdriver/__pycache__/oracles.cpython-312.pyc b/src/testdriver/__pycache__/oracles.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..798077640decdde7464362b4be5c20115f470c71 GIT binary patch literal 4550 zcmcH+TWlN0aqoB!zC=qe#Z>J?F_}*m+wyG!5y|t!j9wsSjHal% zB85BU(;}gg88_n}C04``7f>dFvaN|(2`5NC6U?PxF5R5Vx0@Yi=7N;(sI+H0%Q7%I z$#lb}T%&WkUDiFS8MbZ}xu!d&W;t^@vvkJ`hb88kWrJ#Sl$n;{@zHpE+|k^CfyStA za?N$@issEwSZdM|1>ks^`x>WgPWLR=(YT|R_>Aj~Y7?F|qjSx1HQg{M=bGonaiwl} zE*pkXz{g}3Y&Q(MAZC6>_cUq&8PnV;Bq(aHQQM+(aDo}V1Upg_#CmlicjRR5$ca;v z6Q_^8s#zevHf1sHjlk((E4S1{W9bCrCrIO2V`E1#oF5*F_m|F~_X4KOy5CybqfeJup#WIH_dPU=Ioaw;kU`K6;6t(jklcIp&qof9{3P5lW$2*=%k(9Wp7#QIeYN52_u@dNRkfV!P+#R9Ede2zL%Ly| zg{i=AEB8_u8qNaoF!)X76<;yY7i^;;5XeXfMou`Yu<;IbmPwV&B6dduY$L|CP5`zM z4O_9Zjab}@1IlRYIP3#9i0IpAOyM{3Bp7fmE3&6@X-badGeO{S#Z@GH4 zy4I6j?a4L<^*!HPK6mxporIE!uM;H}4~!~@_5{(U%p5idys}Z&0Uy?hpbLPXE)-gv zx=>&;VkML*I2L;tNzs5qD@CbUUla60hmQbwo80c`z9Dtj<85zE)Z4mlNL}@|o*Pon z&3*f?KmPf?rQ?^`$ee~fJZnUX1jtxAifiu^He z%Ur9%u4X9gW`^FD8SZL&5iAcZVFhmh+9aGAxF^C6nevCs&!J&dMeYGK|AED z$O%ViHj4P-v>_hWcTG0mrmd(cPV$X|DkuE z567JwF&V?G9E)!%Wcab)k9{~+PqtwHx%m*Rze{4V6cBXvf4e~Y9=-n5^=$3%&uV=? zuVp3!(Y`{_{=JzZnZ0+38cY4pQ|%l5$h&@IrLuNteD%iX_kTS3@v&Op@ml7^ z7MAM~;C7Gy5VZEk_Gi=NlXUFplaWt)pFGkP`7|X!|I@A{LTx;8^pVJC2NCtzBQn4o zq>m?H-9md%$3eQ^D&*Lc0lrc1-Ouk~_S0aJWf5TJfF!yj38{sG=^BuHoeK<*3Vru6cZ?i^Z|;k&L7=&9I?l8*lM81~{M-m#g0UYNO%#kMj7V|-9% z3? z2K=@{OXJPxX(dFN!@-jfo_|cEHiu`Yr$c5)0@$`68jMh7E(6UMrV&V zPW4LYlc<1fCp!!ud>c)bp8m|N z82o}f{1?*wSMuN&WN1^7g#8zdE01py=-f#WF>z(+n)$gf__df4AVve&h!AmT9fx1< T|A7#{LjY{%g|yHcM27zW_m&Ug literal 0 HcmV?d00001 diff --git a/src/testdriver/__pycache__/provenance.cpython-312.pyc b/src/testdriver/__pycache__/provenance.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71c71369f92c7cad4b69407cf0dcbc50cb3f73ec GIT binary patch literal 2366 zcmaJ?%}*Og6rZuz#w-v*AWqr92Mxx$;l0nmZX$-th+&rTagZrzu+6AjkRkL6E6x?EU5@pk=z&HE{S2MP(&s^*JnsY(txi<^3g>_mJ%vy7;|pJ zt%=7Ki6e4W?>gd{E=dlntKboCQQWVby+T&2!h@kA{0a49h|svF!cQc(bsr#D=3Wfz zLdC{+O>zJwhWF2NPFUoru?4Q;DJfRC1l*Jo72%oJOmQWa!eYR#kU1)2a-(Sfp&@s0 z+C#%IYP?y&(3%dDz-y$VVVk+Zd+)$_6LD0=knaq`6<0H4~>hCJsaE0byD#13icx?_Ge-CVGnE*3zP%pFJyX|{*|8SX=!ZvdZEBDp9SkP+M(=UyyU zL|ivy2O$7CxD$t_lN75v_NM`hrBVNg6qmj$SmaF*$7zdoL z0j&XHF5zx-{eiDY)=9NqivSD-{%FXESFEjC&;`~)#V zjG%%BN-a?thO4@YdC-o@$w*yW($kZ-rb`R%C-?5ox-+wLccy1yIJq!$x1>3$HKTN{ z$?Ng4Y83D>(4g|FV=?f5lon`wUs2zo;5f}9rMB{kq)UG~B7 z2PBWdpX}8}Lubr!=%^x?4fL}$`l@$ueR`*7V0~)m)W!9?JLJP{>&;iWzP~bs+;zN{ zMfqcYnr_3D_Hw8KG*yN7wA;fKpq_EE>8Y8p4Obh(wU6#mp`Ze*KadL z;U08!QCNomqfHIR&7z0UQ~Vr0XRxpN;p*J`Ji{{ISzAqbzQA?lWJ_zuNfl(&rW;w? z>?rRC@7CqaDBv(IS|zJd;w%?pgizF0#7q-7M<8Y?4-T z^70HgYOwh&=%rcs49kAqvr19O9osNL>#34})<92ECoHv~I0jN@O1`a)DJ9+0Pjo?o zQf(e;qCG;Q*M(~hVSX$xz&V{zH4bJsi!laiVC(N8gwOqsPVVOi@OAub@EO}jy0?D` T?-%meHjnrR^u3z0 literal 0 HcmV?d00001 diff --git a/src/testdriver/__pycache__/runner.cpython-312.pyc b/src/testdriver/__pycache__/runner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09410a69fe4831a4977138b3b8565fa8b881e89e GIT binary patch literal 9536 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 LarX%;7%%=0IQaW> literal 0 HcmV?d00001 diff --git a/src/testdriver/__pycache__/scenario.cpython-312.pyc b/src/testdriver/__pycache__/scenario.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bf7e17b6abee1dcfc90fd3abc90da1d7658406c GIT binary patch literal 2269 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 None: + if self.permitted_surfaces and surface_id not in self.permitted_surfaces: + raise SurfaceNotPermitted( + f"action {self.name!r} was realized through surface " + f"{surface_id!r}, which is not in " + f"{sorted(self.permitted_surfaces)} (D-05)" + ) + + def describe(self) -> str: + rendered = ", ".join(f"{k}={v!r}" for k, v in sorted(self.args.items())) + return f"{self.name}({rendered})" diff --git a/src/testdriver/drivers.py b/src/testdriver/drivers.py new file mode 100644 index 0000000..bb8cdc8 --- /dev/null +++ b/src/testdriver/drivers.py @@ -0,0 +1,76 @@ +"""Drivers realize semantic actions against a surface. + +A driver knows mechanics. It emits S1 evidence describing *how* it acted and +reports which surface it used, but it never reports whether the action was +correct — that is S2/S3 and belongs to the observer and the oracle. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Protocol + +from .actions import SemanticAction, Surface +from .world import Actor + + +@dataclass(frozen=True, slots=True) +class Realization: + """What a driver did, mechanically. Pure S1.""" + + surface_id: str + mechanics: dict[str, Any] + raised: str | None = None + + +class Driver(Protocol): + surface: Surface + + def realize(self, actor: Actor, action: SemanticAction) -> Realization: ... + + +class UnsupportedAction(Exception): + """The driver has no mechanical implementation for this semantic action.""" + + +class DirectDriver: + """Deterministic driver against the lab's enforcement path. + + This is the T5 Deterministic end of the maturity continuum: a fixed mapping + from semantic action to mechanics, with no discovery and no model. + """ + + def __init__(self, lab: Any, tokens: dict[str, str]) -> None: + self._lab = lab + self._tokens = tokens + self.surface = Surface( + id="api", kind="http-like", description="lab enforcement path" + ) + + _MAPPING = { + "create_resource": ("create_resource", ("resource_id", "content")), + "grant_access": ("grant", ("resource_id", "subject_id", "permission")), + "revoke_access": ("revoke", ("resource_id", "subject_id")), + "read_resource": ("read_resource", ("resource_id",)), + } + + def realize(self, actor: Actor, action: SemanticAction) -> Realization: + if action.name not in self._MAPPING: + raise UnsupportedAction(action.name) + action.check_surface(self.surface.id) + + op, arg_names = self._MAPPING[action.name] + args = {name: action.args[name] for name in arg_names if name in action.args} + token = self._tokens[actor.id] + + mechanics: dict[str, Any] = { + "operation": op, + "arguments": args, + "actor": actor.id, + } + try: + result = self._lab.request(token, op, **args) + except Exception as exc: # the SUT refusing is data, not a framework error + return Realization(self.surface.id, mechanics, raised=f"{type(exc).__name__}: {exc}") + mechanics["result"] = result + return Realization(self.surface.id, mechanics) diff --git a/src/testdriver/energy.py b/src/testdriver/energy.py new file mode 100644 index 0000000..23b94f2 --- /dev/null +++ b/src/testdriver/energy.py @@ -0,0 +1,45 @@ +"""Energy events — capture only. + +H-005 is dormant by decision: verification energy is not testable at the current +scale, and a scoring function producing a number nobody can check is worse than +no number. Events are recorded from the first run because history cannot be +reconstructed later; scores always can. + +There is deliberately no score() function in this module. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +from datetime import datetime, timezone +from enum import Enum +from typing import Any + + +class EnergyEventType(str, Enum): + DEFECT_DETECTED = "DEFECT_DETECTED" + REGRESSION_CAUGHT = "REGRESSION_CAUGHT" + MECHANICAL_ADAPTATION = "MECHANICAL_ADAPTATION" + SEMANTIC_ADAPTATION = "SEMANTIC_ADAPTATION" + TEST_DEFECT = "TEST_DEFECT" + FALSE_POSITIVE = "FALSE_POSITIVE" + DUPLICATE = "DUPLICATE" + CRYSTALLIZED = "CRYSTALLIZED" + USECASE_DEPRECATED = "USECASE_DEPRECATED" + EXECUTED = "EXECUTED" + + +@dataclass(frozen=True, slots=True) +class EnergyEvent: + """Immutable. Energy is derived from event history, never stored as state.""" + + asset_id: str + run_id: str + event_type: EnergyEventType + detail: dict[str, Any] = field(default_factory=dict) + at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + + def as_dict(self) -> dict[str, Any]: + return {**asdict(self), "event_type": self.event_type.value} diff --git a/src/testdriver/evidence.py b/src/testdriver/evidence.py new file mode 100644 index 0000000..01a65a8 --- /dev/null +++ b/src/testdriver/evidence.py @@ -0,0 +1,86 @@ +"""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 latest(self, kind: str) -> Observation | None: + for observation in reversed(self.observations): + if observation.kind == kind: + return observation + return None + + 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) diff --git a/src/testdriver/intent.py b/src/testdriver/intent.py new file mode 100644 index 0000000..89fd427 --- /dev/null +++ b/src/testdriver/intent.py @@ -0,0 +1,63 @@ +"""The intent layer: what is supposed to be true. + +Claims and invariants are *inputs* to a run and are frozen — decision D-02. +There is deliberately no code path by which adaptation, retry, or a learned +trajectory can modify them. That absence is what makes False Adaptation Rate = 0 +an architectural property rather than a tuning target. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Callable, Mapping + +from .provenance import Provenance, require_admissible + +# A predicate over the independent observations gathered during a run. +# It receives the observation mapping and returns True when satisfied. +Predicate = Callable[[Mapping[str, object]], bool] + + +@dataclass(frozen=True, slots=True) +class Claim: + """A statement that must hold at a specific point in a scenario.""" + + id: str + text: str + provenance: Provenance + predicate: Predicate + after_step: str + source_ref: str | None = None + + def __post_init__(self) -> None: + require_admissible(self.provenance, f"Claim {self.id!r}") + + +@dataclass(frozen=True, slots=True) +class Invariant: + """A statement that must hold after *every* step, not merely at one point.""" + + id: str + text: str + provenance: Provenance + predicate: Predicate + source_ref: str | None = None + + def __post_init__(self) -> None: + require_admissible(self.provenance, f"Invariant {self.id!r}") + + +@dataclass(frozen=True, slots=True) +class UseCase: + """Purposeful behaviour, described independently of mechanics.""" + + id: str + title: str + narrative: str + provenance: Provenance + claims: tuple[Claim, ...] = field(default_factory=tuple) + invariants: tuple[Invariant, ...] = field(default_factory=tuple) + source_ref: str | None = None + + def __post_init__(self) -> None: + require_admissible(self.provenance, f"UseCase {self.id!r}") diff --git a/src/testdriver/observers.py b/src/testdriver/observers.py new file mode 100644 index 0000000..40de7bb --- /dev/null +++ b/src/testdriver/observers.py @@ -0,0 +1,47 @@ +"""Observers gather evidence independently of the actors. + +An observer never asks an actor what happened. It reads the system directly +through the independent observation channel required by decision D-07, and +records both what the stored record says and what the enforcement path actually +does. Disagreement between those two is meaningful in its own right: it is the +signature of an authorization defect, where the audit trail says one thing and +enforcement does another. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Sequence + + +@dataclass(frozen=True, slots=True) +class Watch: + """A (subject, resource) pair the scenario wants observed after every step.""" + + subject_id: str + resource_id: str + + +@dataclass(slots=True) +class StateObserver: + """Collects the S3 snapshot: what is true of the domain right now.""" + + channel: Any + watches: Sequence[Watch] = field(default_factory=tuple) + name: str = "state-observer" + + def snapshot(self) -> dict[str, Any]: + out: dict[str, Any] = {} + resources = set() + for watch in self.watches: + key = f"{watch.subject_id}:{watch.resource_id}" + out[f"probe_read:{key}"] = self.channel.probe_read( + watch.subject_id, watch.resource_id + ) + out[f"state_permission:{key}"] = self.channel.state_permission( + watch.subject_id, watch.resource_id + ) + resources.add(watch.resource_id) + for resource_id in sorted(resources): + out[f"audit:{resource_id}"] = self.channel.audit_events(resource_id) + return out diff --git a/src/testdriver/oracles.py b/src/testdriver/oracles.py new file mode 100644 index 0000000..705b1ae --- /dev/null +++ b/src/testdriver/oracles.py @@ -0,0 +1,106 @@ +"""Oracles evaluate claims and invariants and produce verdicts. + +An oracle reads only the independent observation snapshot. It has no access to +the actor, to the driver, or to what either of them believes happened. + +`INCONCLUSIVE` is a first-class outcome, not a failure mode of the framework. An +oracle that cannot support a judgment from the retained evidence must say so +rather than defaulting either way — silently defaulting to PASS hides defects, +and silently defaulting to FAIL trains people to ignore results. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Mapping + +from .intent import Claim, Invariant + + +class Verdict(str, Enum): + PASS = "PASS" + FAIL = "FAIL" + SUSPICIOUS = "SUSPICIOUS" + INCONCLUSIVE = "INCONCLUSIVE" + + +@dataclass(frozen=True, slots=True) +class Judgment: + assertion_id: str + text: str + verdict: Verdict + step_id: str | None + detail: dict[str, Any] = field(default_factory=dict) + + def as_dict(self) -> dict[str, Any]: + return { + "assertion_id": self.assertion_id, + "text": self.text, + "verdict": self.verdict.value, + "step_id": self.step_id, + "detail": self.detail, + } + + +class Oracle: + """Deterministic evaluation of one assertion against an observation snapshot.""" + + def judge( + self, + assertion: Claim | Invariant, + snapshot: Mapping[str, Any], + step_id: str | None, + ) -> Judgment: + if not snapshot: + return Judgment( + assertion.id, + assertion.text, + Verdict.INCONCLUSIVE, + step_id, + {"reason": "no observations were collected"}, + ) + try: + satisfied = assertion.predicate(snapshot) + except KeyError as missing: + # The evidence needed to judge this assertion was not collected. + # That is an evidence failure, never a pass and never a fail. + return Judgment( + assertion.id, + assertion.text, + Verdict.INCONCLUSIVE, + step_id, + {"reason": f"required observation {missing} missing from snapshot"}, + ) + except Exception as exc: + return Judgment( + assertion.id, + assertion.text, + Verdict.INCONCLUSIVE, + step_id, + {"reason": f"predicate raised {type(exc).__name__}: {exc}"}, + ) + return Judgment( + assertion.id, + assertion.text, + Verdict.PASS if satisfied else Verdict.FAIL, + step_id, + ) + + +def overall(judgments: list[Judgment]) -> Verdict: + """Aggregate run verdict. + + FAIL dominates; INCONCLUSIVE outranks PASS. A run containing an unjudgeable + assertion has not passed, whatever else it did. + """ + verdicts = {j.verdict for j in judgments} + if not judgments: + return Verdict.INCONCLUSIVE + if Verdict.FAIL in verdicts: + return Verdict.FAIL + if Verdict.INCONCLUSIVE in verdicts: + return Verdict.INCONCLUSIVE + if Verdict.SUSPICIOUS in verdicts: + return Verdict.SUSPICIOUS + return Verdict.PASS diff --git a/src/testdriver/provenance.py b/src/testdriver/provenance.py new file mode 100644 index 0000000..8df49ce --- /dev/null +++ b/src/testdriver/provenance.py @@ -0,0 +1,46 @@ +"""Claim provenance — decision D-06. + +A claim may only be authored by a source causally independent of the +implementation it constrains. Without this rule the framework's guarantee +reduces to "the implementation agrees with itself", which is exactly the failure +the project exists to prevent. + +See docs/TestDriverClassificationDesign.md, Part B. +""" + +from __future__ import annotations + +from enum import Enum + + +class Provenance(str, Enum): + """Where an intent artifact came from.""" + + HUMAN = "human" + SPEC = "spec" + AGENT_FROM_SPEC = "agent-from-spec" + AGENT_FROM_IMPLEMENTATION = "agent-from-implementation" + + @property + def admissible_as_claim(self) -> bool: + """Whether this provenance may back an assertion that can produce FAIL. + + `agent-from-implementation` is not forbidden as an activity — it is + genuinely useful for T0 exploration. It is forbidden as a *claim*. + Such output enters as an exploratory hypothesis and requires an explicit + human acceptance event before it can constrain the system. + """ + return self is not Provenance.AGENT_FROM_IMPLEMENTATION + + +class InadmissibleProvenance(Exception): + """Raised when implementation-derived intent is used as a claim.""" + + +def require_admissible(provenance: Provenance, what: str) -> None: + if not provenance.admissible_as_claim: + raise InadmissibleProvenance( + f"{what} has provenance {provenance.value!r}, which is derived from the " + "implementation it would constrain. Promote it through an explicit human " + "acceptance event before using it as a claim (D-06)." + ) diff --git a/src/testdriver/runner.py b/src/testdriver/runner.py new file mode 100644 index 0000000..08fdff4 --- /dev/null +++ b/src/testdriver/runner.py @@ -0,0 +1,190 @@ +"""The orchestrator: executes a scenario and assembles its evidence. + +The runner is the only component that sees everything. Actors see their own +credentials and memory; drivers see mechanics; observers see the system; oracles +see the observation snapshot. Keeping those views separate is what makes the +independence claim structural rather than procedural. +""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Any + +from .actions import SurfaceNotPermitted +from .drivers import Driver +from .energy import EnergyEvent, EnergyEventType +from .evidence import EvidencePack, Observation, Stratum +from .observers import StateObserver +from .oracles import Judgment, Oracle, Verdict, overall +from .scenario import Scenario, VerificationAsset +from .world import World + + +class CollectorIndependenceError(Exception): + """An actor was about to be recorded as the collector of S2/S3 evidence.""" + + +@dataclass(slots=True) +class RunResult: + run_id: str + verdict: Verdict + judgments: list[Judgment] + evidence: EvidencePack + + def judgment(self, assertion_id: str) -> Judgment: + for j in self.judgments: + if j.assertion_id == assertion_id: + return j + raise KeyError(assertion_id) + + +class Runner: + def __init__( + self, + world: World, + driver: Driver, + observer: StateObserver, + oracle: Oracle | None = None, + ) -> None: + self._world = world + self._driver = driver + self._observer = observer + self._oracle = oracle or Oracle() + + # -- independence guards --------------------------------------------- + + def _assert_collector_independence(self, stratum: Stratum, collector: str) -> None: + """S2 and S3 evidence may never be attributed to an actor. + + This is the check that makes oracle independence falsifiable rather than + merely asserted: wiring an actor in as an observer fails loudly here. + """ + if stratum is Stratum.SURFACE: + return + if collector in self._world.cast.actors: + raise CollectorIndependenceError( + f"{stratum.value} evidence cannot be collected by actor " + f"{collector!r}; actors do not judge their own outcomes" + ) + + def _record( + self, + pack: EvidencePack, + stratum: Stratum, + collector: str, + kind: str, + data: dict[str, Any], + step_id: str | None, + ) -> None: + self._assert_collector_independence(stratum, collector) + pack.record( + Observation( + id=f"obs-{len(pack.observations) + 1:04d}", + stratum=stratum, + collector=collector, + step_id=step_id, + kind=kind, + data=data, + ) + ) + + # -- execution -------------------------------------------------------- + + def run(self, asset: VerificationAsset) -> RunResult: + scenario: Scenario = asset.scenario + run_id = f"run-{uuid.uuid4().hex[:12]}" + pack = EvidencePack( + run_id=run_id, + scenario_id=scenario.id, + use_case_id=scenario.use_case.id, + sut_version=self._world.sut_version, + ) + pack.provenance_index = { + scenario.use_case.id: scenario.use_case.provenance.value, + **{c.id: c.provenance.value for c in scenario.use_case.claims}, + **{i.id: i.provenance.value for i in scenario.use_case.invariants}, + } + pack.energy_events.append( + EnergyEvent(asset.id, run_id, EnergyEventType.EXECUTED).as_dict() + ) + + judgments: list[Judgment] = [] + claims_by_step: dict[str, list] = {} + for claim in scenario.use_case.claims: + claims_by_step.setdefault(claim.after_step, []).append(claim) + + for step in scenario.steps: + actor = self._world.cast[step.actor_id] + + # --- S1: how it was done ------------------------------------- + try: + realization = self._driver.realize(actor, step.action) + except SurfaceNotPermitted as exc: + # D-05: routing around a control is a finding, not a recovery. + self._record( + pack, Stratum.REALIZATION, self._observer.name, + "surface_violation", + {"step": step.id, "action": step.action.describe(), "error": str(exc)}, + step.id, + ) + break + + self._record( + pack, Stratum.SURFACE, actor.id, "realization", + { + "action": step.action.describe(), + "surface": realization.surface_id, + "mechanics": realization.mechanics, + "raised": realization.raised, + }, + step.id, + ) + + # --- S3: what is now true ------------------------------------ + snapshot = self._observer.snapshot() + self._record( + pack, Stratum.JUDGMENT, self._observer.name, + "state_snapshot", dict(snapshot), step.id, + ) + + # --- S2: did the action actually take effect ------------------ + refused = realization.raised is not None + postcondition_met: bool | None = None + if step.action.postcondition is not None: + try: + postcondition_met = step.action.postcondition(snapshot) + except KeyError: + postcondition_met = None + self._record( + pack, Stratum.REALIZATION, self._observer.name, "realization_check", + { + "step": step.id, + "action": step.action.name, + "surface_used": realization.surface_id, + "refused_by_sut": refused, + "refusal_expected": step.expect_refusal, + "postcondition_met": postcondition_met, + }, + step.id, + ) + + # --- invariants after every step ----------------------------- + for invariant in scenario.use_case.invariants: + judgments.append(self._oracle.judge(invariant, snapshot, step.id)) + + # --- claims attached to this step ---------------------------- + for claim in claims_by_step.get(step.id, ()): + judgments.append(self._oracle.judge(claim, snapshot, step.id)) + + pack.verdicts = [j.as_dict() for j in judgments] + pack.finished_at = datetime.now(timezone.utc).isoformat() + + result_verdict = overall(judgments) + if result_verdict is Verdict.FAIL: + pack.energy_events.append( + EnergyEvent(asset.id, run_id, EnergyEventType.DEFECT_DETECTED).as_dict() + ) + return RunResult(run_id, result_verdict, judgments, pack) diff --git a/src/testdriver/scenario.py b/src/testdriver/scenario.py new file mode 100644 index 0000000..a41de6e --- /dev/null +++ b/src/testdriver/scenario.py @@ -0,0 +1,47 @@ +"""A scenario binds a use case to concrete actors, a world and a schedule.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Sequence + +from .actions import SemanticAction +from .intent import UseCase +from .observers import Watch + + +@dataclass(frozen=True, slots=True) +class Step: + """One scheduled semantic action, attributed to one actor.""" + + id: str + actor_id: str + action: SemanticAction + expect_refusal: bool = False + + +@dataclass(frozen=True, slots=True) +class Scenario: + """UseCase + Actors + World + Schedule + Surfaces + Variant.""" + + id: str + use_case: UseCase + steps: tuple[Step, ...] + watches: tuple[Watch, ...] = field(default_factory=tuple) + variant: str = "baseline" + + +@dataclass(slots=True) +class VerificationAsset: + """A test as a durable thing with identity, maturity and lineage. + + Maturity is the T0..T5 continuum. The kernel produces T5 assets: fully + deterministic, no model involvement. Agentic assets (T1) arrive in T07, and + crystallization walks an asset from T1 toward T5 in T09. + """ + + id: str + scenario: Scenario + maturity: str = "T5" + parent_id: str | None = None + adaptation_history: list[dict] = field(default_factory=list) diff --git a/src/testdriver/world.py b/src/testdriver/world.py new file mode 100644 index 0000000..04f2341 --- /dev/null +++ b/src/testdriver/world.py @@ -0,0 +1,79 @@ +"""The situation layer: who is acting, and in what state of the world. + +Actor isolation is structural. An Actor holds its own credentials and private +memory and has no reference to the Cast or to any sibling. The orchestrator may +know the whole world; actors must not learn anything merely because they happen +to be executed by the same process. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Iterator + + +class ActorIsolationError(Exception): + """Raised when one actor is offered another actor's private state.""" + + +@dataclass(slots=True) +class Actor: + """An independent execution entity. + + Deliberately holds no back-reference to the Cast or the World. An actor that + can enumerate its siblings can leak knowledge it was never given, and no + later check can reliably detect that it did. + """ + + id: str + display_name: str + credentials: dict[str, str] = field(default_factory=dict) + session: dict[str, Any] = field(default_factory=dict) + _memory: dict[str, Any] = field(default_factory=dict, repr=False) + + def remember(self, key: str, value: Any) -> None: + self._memory[key] = value + + def recall(self, key: str, default: Any = None) -> Any: + return self._memory.get(key, default) + + def known_keys(self) -> tuple[str, ...]: + return tuple(sorted(self._memory)) + + +@dataclass(slots=True) +class Cast: + """The set of actors in a scenario. Known to the orchestrator, not to actors.""" + + actors: dict[str, Actor] = field(default_factory=dict) + + def add(self, actor: Actor) -> Actor: + if actor.id in self.actors: + raise ValueError(f"duplicate actor id {actor.id!r}") + self.actors[actor.id] = actor + return actor + + def __getitem__(self, actor_id: str) -> Actor: + return self.actors[actor_id] + + def __iter__(self) -> Iterator[Actor]: + return iter(self.actors.values()) + + def __len__(self) -> int: + return len(self.actors) + + +@dataclass(slots=True) +class World: + """Initial state plus the handle to the system under test. + + `seed` is everything needed to rebuild the initial state, so that a run can + be replayed from a known starting point rather than from wherever the + previous run happened to leave things. + """ + + id: str + sut: Any + sut_version: str + seed: dict[str, Any] = field(default_factory=dict) + cast: Cast = field(default_factory=Cast) diff --git a/tests/__pycache__/test_kernel_guarantees.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_kernel_guarantees.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfc3f095cb9bd8b3dad55a3e2c0e3b7835b39829 GIT binary patch literal 16008 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 Ni#Ik%;#&TI{s&262gCpX literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_kernel_guarantees.cpython-312.pyc b/tests/__pycache__/test_kernel_guarantees.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f6df561844b4bd6c551f67f0b7132b31aba8571 GIT binary patch literal 9051 zcmbtZYj6}-cJ7|;ndx~+LIQz6pyuttXb@n8BOJ+NNr12s7zwjGw$pBPw@?o=)AH>e z3C+&fvl}Wac3p*>O@f>_Wt{x5DM$rZTv?|oRWVihK~?_DSZpMB;lzoZO7P$HBLC&KASB9g94 z)TQea_34I01MS0`T9s~0G^U#pP3hH%)#){fH8kIsYEG|BtcA4y0+U#$3J|xb0f^VD zL5Me~A&6VmFvJ_x2*jH_%n?SdyTGXRTGKVo8GWKnZGbXST?O&xDK6S5?_e2bcuW(| zX(Xei#B|oQL@hIF5JeL$$R8)hxJE2pGwJJa$39WjO#N&|R8q2@Hbtl^>Q*cmgkDTd zl!+#0GrHBD(k7rkWlU4fnWB-Qt(}kTG)2Y8m=?(@mTqKXB7T2F8WcfEou`wO$CVtqgS(v zKBA{|Yic*tv$Tx0qfSn$`l zhEdiI$(f8{(E*r)(U9%!#f7k&2Qspn)=g6%NogmEF`;E-7>*qn$V|vYmot{_J3)tQ z2NxClPGvJ0jo5WVS{mx;N}Mi=9T|d8XeUNYjlfFSO+#5SDl6KcVV%@STDL4swf#dD z3@)3t{V!-l)fFqs+rB<6qeEHnxSr9|a_TvG#BOxDp+oK+gW+o_yKW?_r&MV~hQ-!1 z8qCW}a}-BXoLGPdhz|Rp1c}Rx1+wCVq{mIwo)+(>uT|W)?uX1I8x?ZBeeE5cU83yBG6*(-U020XVx+}M*;2;z%vmb9=9Fn^X;GnD zUy)u}&!FJvf=avLY?GSXGC2mj1{=1dRhAGCyVR7Z8k(t87qK$vCR|`#yUWZJd(|^I ze3iM%+8p$0VpZ|clY7RDw6q{@6H=OjyTbS0uCH3eiV_EgGHBP;4KObyG75ZZ*N#6BG@* zC4QuTa9BEg>cnwr;P}bs`j5kFe0bo*pv?hd*xm^_l|^agwDXFyK4d*MbXH;M=e}!# zWGA$0hM$>$sK|U7sK2!F;>O!++vftYyNuTxys`C;5LpPXE3NOo6K#OCusZp+vd3hCs=P}^s;d#t6c;>EE z`h&C4#4}S&lpoCTdb@GX=0^b=zFT4mE+Scr(K?70?Frcc@3|f-dFHsLNPw%-SQcPT z0$i@(BbB-uaDG$nwqnmHd}Jd;QLiH>-lM0F#|LeG=wyGdUB5_ZFoWOS1Bi;uowb|i*X}H@-Fbce-?v?D`{!NrUBl(B;ZpPI`wZ)Snw<{b z5mwFYnB8$@S2@=GE1~BehuL)WD{ubC(C=b!r9fE)*p)VWIZ!T!*<=-Bq1#HZs$iQ8 zyLq*z3z-K1993VQk!q0bSK+S)zf=C@U^?Zmf@vJUbYGWC3y=qpEFffNJZqUTanKND zRUUVcM;w=}F)=MqiA0+usM-N5(lMJ=Y~Iq&Tfd*gJx2F;5vT#4=d{c>Me8wGgPLfO zP59adQ7&o%;96DmUy3T?q>)XjK#a6ejSNPG8hVk(u?LhDvIQbLq{|7gm+iSqpH?2`WFvx*`R-ypj(hTG8r2uvb8+8Z&U)qB!Q6B;x0? z0E_^a3IZ~M?;)ZCAfg9A--BWYk_ft8&cF+0nAR9EvS-IICz}Dr>rgBpo|JYWNd(dd z;;H~}k*Sy+K8t!*k`+ZWO*=$16S%9QNxEvYr)>WSF(!e!!QM7jJCiD%nKmbn>w{4r z!DDF$9Pu;MtZK3Gp+!cfa9L%E@xS^w7Wy{kBilELl%QJ){#01#E$2*u;esMF)8IegCPGXQ4fIX{I5fpu8Zo zls3fg2#xc?x-$H)@4ofKLoXNc75g6wOhfa}&tH1&;%l>7X~&ZvZ7Xf>`FQnQ{o&%# zg;3LcXhS));qA%!w*6)J5A84U`%9ty3!yDn#@;jDHExWRcl4A)JtZFFZ?XzJ4gk6A zgZCi;ehTzaFln~Hq&)?eWjsutg&)v9mA9%eyb@v7?E z(HB-|;T|s_oW4PVn{d@*3&$$EV*3$ked`A_z!rm?sUn2`9bmp8%Wf&w|f)gnLZ;@O%It`t~v7=KE6`B9!pZKp( zw0eJ`Rdb!oU2MibX1Ehu;=YyNd!AVKy?%jwU{!pY6@IDsHIIk<=ER4m~5|{(!p~A)~P{@DN-Qg)QMZY?ZqqAf0 z`{sl_^TPhJu>VH?ZJ`VG`k~K5yH~Pky--BkJKFw(P4T_l&Ar}u*HUd?(dX;~v;{9S z8k1mECgD+8aE@`fF+Paea9CIq}zU(+K=ZeP>DHl;; z*H!9Mk+Z69iEwCP&quNr-O!4^60scokhr$PV?fy8A2wMu$K-KMG)6@k=$o7p<*YSk z5IskIAmH;E1x?xoPD7w|4&_Gy1JWjguaS1<@coowoD*dbE_n)MkkUTT+$Qvy2B1=R z(-AJpcjzK5i_vbBDno{);o(PN`cEP8Q}(AG)YnN!FAvGZtty2Vs{mWz@?4RfV)LBp zL0x~5JqJb&VW+qn+n)AJv!foBzaYRDs*qp%P9NhUi+ehOF?a_L+g>;v8W~=~F>o~d zQN0u)@#mgrfq~=5Up@esgdD5ZZ8fDbIHvO~5rNOMD|t39a+MnOv<9xtvT)IZHU5IV z%z}^NAowWQzvIAz=drz%h7U$L+Y37gKGq;mV02*t5hut%z$NO5b=WMSZ?+%kFo7}? zKO&07B0YqU0#BXQGTQlZ(w%ExE}1b23{^^;7I7T6x&qyp&p`Cs|6_hzWbQLuQ}FYK zRd+ciQg`Xd#UnG5 z_F27*Q$Q{Hb&*+UT6?+s&F=Z8?d7KJSI*5f?VA>Wl$1gn7Mj*UV&+IW)OyALNoXh3 zyi?!!kj{5a%jLwI3E($PK~Q%M;Q8VLL8{r%$f4uh<_?wuCz3fUOENY;Vi+la13{7y zbS23SN>Unp_9;w0F29nTJ8P!=$TJGvAkb~Q6spcNwhnb1+@;rvU2@VufWBJ;wktinP@}q z=rAb6#aDfBMP?97JRBc*&f!kfNw-^^9MdwO+Nv5EZ!V~=#k_M`jM8<0A}?H58N}v$ zW$@{T`*hIOR-Z$Iyl0RVf!9JfJ=8Dt#)tYLT~E1~1g0Kv9b%+%Eh%}Vy(6|a*3o{v z<3Kwq=XOUYLv6I?OyFHnj+}u>+8ne4XBjrMy%vlk1C>Gw(cr$%(&4%yCrs8_^9l?q z7g5@wRl758_1AFHncS9Vj1l)T2Q6aplt`~J7R`7t+G+=D<_7r@v?F!;CGP7I_`Z1J!a;#72luwjisjgjxtdT~%Qkx|Px>@gzUSN+FEERSz}Y=C+?+ zM!{_f@=t;m0!d(W1S7}n!5t(>2yLI^OmuvUt5zDgGdd2OLpBG?Z?jq3M_1D%e}X;! z0J~u&xFf9G4gv{9_tcA6_!35_GZ4g0aATM_Sm0bsgF|j(l-!!&*gz(`n0Ue63=%I1 z$-C)VUF0q3N{64>3nopG`CtCf9DnFDe&1(&$CttNrLEn!gFVIGg9_hRC{IopN zbh-7-)>-)%ZRI+#6zMA-hUn8!{m(m#eG3iE=#hV(o#q#W@Fn4*@aCEsYj*gZ#9N8K zdg&wo$IZ9+o2{jTgSS_m_?2+-E|A~n*}Fbw%l7wL-fj8Y4P|~)FMAyNdmvMp|aK&Q8olb`%fasc*f#anB8IZsQX-GV`6s%bmyP zJ5QH8PtVoAP(1o&q-myqw)4u9*I&5ygSklOe59)!>H4U9E^@4R`18=JOWhZ{-#+us zOK-hY4n<3Rv=oXiKyJ^)p4pzc(BtzVbOd}j`OEwV`EsbY#P^m$y`P7gXobDy(8l@D zmU3uIX~(foLdQz{v2y6x*Z13)x_;I~pX=3@{@wf%#fLP3Dn3NJ#flGxAC-E8y49!XZ2a2qBt4*Wum|@A%W;`iJY($Bx3)`T@*R z4T6{4Q3pN-Z7;118G}<-r z+M009Bsp!ta|L50??8*6U{kb7ip>A;JHD)Mo|%}Pyj{Pu#P2MHc9!aQQo0h|LZ+Y& zl916XP6^lstP?{Je;tc(8dJD_&dnwAY3D!+n4;$sX@zr)(Gf?`h;|NX2o75;?cB=~ zw3G=t73pCojhMQK5wZ$;J4tVzsK=lBZ>gJ!pg)2de-7or14Adcj1a-quS27{$=^T~ zTr`tF1X9kj_dH&f|4kjkZvBF3{DO&m#dECZE5?n$F#)dv!=7O0n8DAO$M5?Dw&#Ac zhkcs8ALQ9Md%w=dZoA(oU`Df#ZM)y-V|yO1YhZ&jxrYoq?(b-3pJgBHb~`i?!B>1TA|9Wf^(-*^j| zwElX9P=XVT(HW+sEPTFcgRGr4+xCSUKH z=@Vt*wf_3R%mDIzte+)bi_Z+Qpk z$Is5|1+LHOTyK>1f>G8RC0=P3>Mg@8@K&S1bW`W`N~2BKr}CvLjqD@m4YDOSviUs0X*swmfc zSgO~GCC@2m&|&nECsB-3EjxzKEhg;bc?>|QVcPKvOIi87(I;?;W}Ft*h!|OH;3pUz~X7g?CSUlpVbI$UEEL-SLz0YuSS<+3~B{ z@%LKSvd8|WwVXY^tRBCCs#2K}@1XA#gS!K1cyO<{CBCU(Xfe2!R7I}$=qRQ;w31Y% zFYY-yF`9su)KM++dbVGYBQpz!Kiv|^KgxbUffy( zJy%0-iKt`6yw+L#lG?`nVeW?}e_wfHOvPI|3BTWoHVUz%6|cym|4H+HuU6Q`7Vo+s6;sofWm~BjMY(Rlbjcd7HpN8wKGpIEE2+KC2kLD1kFH}1zv8}7fM{Wb9k%~#+sFe zVJB)$lrZcanlayO)tD%%y+5)}G;A1F68^VB4B`_V>@h6#APcNN#eezR9 zi6{O`{Y3hoI5Hp0@mIJ0oN}K@?tO9fbGrJp>(<%^S2s3z)?M%Ou9D(4)EV(o|JkS1 zA^*1DjnDiZ-*5{2hf`0qB9iWY1RwE*=!B8Oi2U6{yH6xje=l!A8Z9aQ7DIODk^&f^ zl$CkH2w->z7+>`z2VUzQZz*pKs1n3DN;3J+u@f!Tik;g<$YjNhADASs#8IV5?>Qhj z;*cLPsuReMP@M>w6NvviK<{m#HY3nGz*WeZpvO^f_Ys)T8Yrv?6icuKFfHL@+K~65 zq1K|%c|A;nQsm>ABJk`vzIje( zMfs-k#4!Ni7DBr*;Mo%n@I4MtN)Qg9j6(1DxMB*X`e~fNG*hrkzp)gh)2wxlw1I3~6#drj2 z>=LM<163Eba;;RUYZXItK8-((*N|v3zKx^{F+SY0e`U}3)ji`M81wV2d5VUXf_Y3P|Ph)c z^{88FU}wfz;OIt}7Yf|Pm ztTb_iT8hy5td!T1cXstB75Y=62X8Xd?V-wWsl_VhaA{#dZ?NHJqqg`7fzV|D%P|6$ z0|YEU6GyW`CeW<*x|mahYIV$k9cWZ`tfn{Y)Pl}6!_*gwD&K+j_yi?KDIrzxW*CQ{ zKylIqi@5-Y_-|0rDM|ti>VY2tNdX3>h4vD{pc7R47$y6WgaPOfAEDa^kQg-E4kJYB z=qV!taVia$TpG~YbWJ~BskD)tM~8w zp#PNWj#s?MI+{t5mFmsRxC=s)^h}E%_ijnK!~}@N0Jng5Fqq>O@~^F0=6hX zND!HF00Go70=PP9jC0iejFq-B=OEmL;`g7ScSns72q45rit2>KNIC$sDB9|+ryj!r zfk@90m;<^D2T|kh+oS;6=}@nu%BDT&WSP6xQ{Nb|X;1x<=xJt?o@V@>W?iw7VmX$# zQm)v@nzy@3vR+Hx8DfK0Ng99gPG(V9J3;Yd7d!%vT(Pe`arQ!!ERRNEjyLNC6ZT01 z1hru3Ix8&jCTo@TLItX;utyerVmiW~!DL8-8+0TI8fIKl!r(Jdz+T!iod?Z2t) zCgJofNuYITKbhz%45DebFPa<~*3J68Vs?D4!VMS-4ZYlKFn%B2&QDSzL{q+3G(A9% z0@2hhi(o_$Z|_lh72)gGhdw$fyuia>wX?hdj01CFaPfiICR7_L4&(lxWkweQoOjL?!DK)YwF%F){nDxv;#$3v&G-BL4!XHc5t8-EIA;?5QR^;G;$%=D#U6tV88iUd2luVe~L{&#pE z6q9-@j%ozLbs?;e5Y;H@;iD07D<~Nu87#jfa7)>wtWU3D#J$~~?hLgV(bHh0JOl%! z*s#a9)V1i;!pBHzV~!(DG+k>YLRPM` zR^q>2E3{31h1Uv6*`8yawUYStTA_jZ|GHL8(rCm{ebCDVXP^$T@1DbxBpy z$spHBfmx8Hu;+m3IAy8A5S6yl#$Sf8kW{FigSZ12M4IXZ3?ft~N)q{u2m15vN|&&7 zi;yl|Ok+c-5|kAB`^IH`$<4BtVnRw}g#Mm!_4mXtEi+Q2!(+^otk0Ph*5AouG_(E; z)Iy`V{tOIIojad_P}@;6<+Bbp=(5}#+roydocsOx+y4H%w*tMhD>I$%&n`;Et<&vF z$o752vuGjXTC~>T5zRU1%OWBo(-9Ugn9Us`fs9)fX0eeoFFb!@@|mej2j(zaT_}~$ zaWW4VFb_-;rPj~O--K_ z@8ZwX=Mu*QOB?Zg6E4SFX8=7WCd_yU_6RJ7G-6$z3rre+iH0$2v`h_#I0OEQ_Lf>{ zcKE=^*vNt5*^;5xDhOx!P3p&Ulzfd6vON}u96xPJKTpiGtlODjj>ViI2F97p5PuU? z@5|Ii57o-0=+M$gF8lG)9;odS=c2t|P_yYNCl^VOQqhSJDtDq`{>P zdRS>woDgSdWt5uxH$e|Wobbc!;G0i<_o=1htLna$d;hzx?pswySML4qx;nb5?q9k0 zzw7G$kMe_W&U|;~9re$0Kgj*4?^^!Rr4x7Y<^QPeUsWeo?&P1KQe9z4p*j&3$bgk@QZ8Vmp0gLu zrBDD%$k7yPi8fl#VMVbwl0sepORAV|pPDSVF(OXr9s=})7l!s-0pS$Fj#gj?cI1rG`M;D7>47cb3A``0r5R0a)eEBW@752YV3H zUs>0PRZNB;s@q1z1F)eL;)0FiJlWHYG92bK&J5Rc8@!y)qap#jL2eqDpMqeIU(7xG zzIyW3_3K+^79fi^T)%RZU%^?cR`fGPtYZX4L^E<06VDO!5V;GSr;b~e?CIo|-E0S_ z!s8(7gmkfuLm7M>Gqi4_c1mEeNcbKHI3YtG2S3~|)Kb(A@b&K=UCi@1C{3vAEbSDm*R~oUSz#}?3874ua%~)B4Gv=(EaXr*Kn*E-GWvv`gBu6!Z zF$A;q9ju%BB!cmdu(Jei_d5zQQ9`|rnjPOKu}L|9TsA4^dk!`!7ij2t*N&B^=w&PK zYUq7|13uJJ)T-$7X3RUg8oKZ>03S2>%V8#C)hLTZnr7W&o1Hit==580t6#{DjFmBd z8hU%wtPjEiC9^G5=T~^v`@OMeL$1GQfNf>ltO1{I-R>S=k8k<2+35%8+CGi&>skdG zb_@YccHBhBQiD%pOM(^n5lgOr2A-cDMP2w}xNf$$QCLDjv{WEZh5=z%F*wCqp`qu9 zUcjl->)lz0&p{v|{L5KACg+qJhqYVkkgvyr2n^AYAl<*iIJ(LSK1<2B6PjGitd6?^U+U zb>VG8{OG!Z9pUSGU9Zm~Iv&BUG#VkELlno(1br(GI_@g6r0^;w=O|fEA8=?4`KrP+ zW+HrX#0htG!bZmqbX^_fC2D;Zi9u4Lo5tYE(J>AP9ivn>5xcL=HxVLRF`6|IoDmuT z!D;ZJO}@z6XzXcf^-+h-xX!N0rPhao+uz>x{ax>zygK-mclE!h{0s`GG_4tA|(BM_2CI?|t>rRdx5u-TqxycMEk2|NW8j z*mP0hB%JIRcw=#=ozyheENhyb(X_di*+P(v#$!|<<;60Kg=O#5oapBfX9Iz&k8u40 zub7Br_HWrq_nxzEzeVjNoO*VK=G*z_4Ds(Df~7n0)jPfa%P>X+KxjZ31Xv?t()n)@ zq4|!XG5$0q6sjXUpMu{CB~+{pzCaI%|A-ZKGxbCr&g+`~B>zL)BW1&&s-MPWS^g*K z*?*ND{#cS;ll~z4Gb#VEG bool: + resource = self.resources.get(resource_id) + if resource is None: + return False + if resource["owner"] == user_id: + return True + return any( + r.event == "grant" and r.subject_id == user_id and r.resource_id == resource_id + for r in self.audit + ) + + +def test_seeded_authorization_defect_fails_the_run(): + """The kernel must report FAIL, not adapt, when revocation does not revoke.""" + from testdriver import Actor, Cast, DirectDriver, World + from scenarios.alice_bob_carol import USE_CASE + from scenarios import alice_bob_carol as ref + + lab = RevokeIsCosmetic(version="lab-0.1.0-M05-auth-defect") + tokens = {u: lab.add_user(u) for u in ("alice", "bob", "carol")} + cast = Cast() + for name in tokens: + cast.add(Actor(name, name.title(), credentials={"token": tokens[name]})) + + _, _, _, baseline_asset, _ = ref.build() + world = World("w-defect", lab, lab.version, cast=cast) + driver = DirectDriver(lab, tokens) + observer = StateObserver(ObservationChannel(lab), baseline_asset.scenario.watches) + + result = Runner(world, driver, observer, Oracle()).run(baseline_asset) + + assert result.verdict is Verdict.FAIL + assert result.judgment("c-bob-revoked").verdict is Verdict.FAIL + # The claim set is untouched by the failure — there is no path to adapt it. + assert USE_CASE.claims[2].text == "Bob cannot read R after revocation" + + +def test_defect_run_emits_an_energy_event(): + """Energy events are captured; no score is computed (H-005 is dormant).""" + import testdriver.energy as energy + + assert not hasattr(energy, "score") diff --git a/tests/test_reference_scenario.py b/tests/test_reference_scenario.py new file mode 100644 index 0000000..090e807 --- /dev/null +++ b/tests/test_reference_scenario.py @@ -0,0 +1,70 @@ +"""The reference scenario must run deterministically and be replayable.""" + +from __future__ import annotations + +import json + +import pytest + +from testdriver import Runner, Stratum, Verdict +from scenarios.alice_bob_carol import build + + +def run_once(variant: str = "baseline"): + world, driver, observer, asset, oracle = build(variant) + return Runner(world, driver, observer, oracle).run(asset), world + + +def test_reference_scenario_passes(): + result, _ = run_once() + assert result.verdict is Verdict.PASS, [ + j.as_dict() for j in result.judgments if j.verdict is not Verdict.PASS + ] + + +def test_every_claim_is_judged(): + result, _ = run_once() + judged = {j.assertion_id for j in result.judgments} + assert {"c-bob-reads", "c-carol-denied", "c-bob-revoked"} <= judged + + +def test_invariants_are_evaluated_after_every_step(): + result, _ = run_once() + per_step = [j for j in result.judgments if j.assertion_id == "i-audit-append-only"] + assert len(per_step) == 3 + + +def test_run_is_replayable_from_known_initial_state(): + """Two runs from the same seed produce identical judgments.""" + first, _ = run_once() + second, _ = run_once() + assert [(j.assertion_id, j.verdict) for j in first.judgments] == [ + (j.assertion_id, j.verdict) for j in second.judgments + ] + assert first.run_id != second.run_id + + +def test_evidence_is_stratified_and_serializable(): + result, _ = run_once() + pack = result.evidence + assert pack.of_stratum(Stratum.SURFACE) + assert pack.of_stratum(Stratum.REALIZATION) + assert pack.of_stratum(Stratum.JUDGMENT) + parsed = json.loads(pack.to_json()) + assert parsed["run_id"] == result.run_id + assert parsed["sut_version"] == "lab-0.1.0-baseline" + + +def test_evidence_records_claim_provenance(): + """A verdict must be auditable for the independence of the claim behind it.""" + result, _ = run_once() + assert result.evidence.provenance_index["c-bob-revoked"] == "human" + + +def test_actors_hold_isolated_credentials_and_memory(): + _, world = run_once() + alice, bob = world.cast["alice"], world.cast["bob"] + assert alice.credentials["token"] != bob.credentials["token"] + alice.remember("secret", "only alice knows this") + assert bob.recall("secret") is None + assert bob.known_keys() == () diff --git a/workplans/TD-WP-0001-statehub-bootstrap.md b/workplans/TD-WP-0001-statehub-bootstrap.md index 439e319..7c5bc94 100644 --- a/workplans/TD-WP-0001-statehub-bootstrap.md +++ b/workplans/TD-WP-0001-statehub-bootstrap.md @@ -32,7 +32,7 @@ Replace generated placeholders with repo-specific facts where needed. ```task id: TD-WP-0001-T02 -status: wait +status: done priority: high state_hub_task_id: "39da4237-aaa3-5b4f-93c4-9cbf794f0850" ``` @@ -58,8 +58,9 @@ checkout: statehub fix-consistency ``` -Blocked until the stack exists: no code, no dependency manifest and no test -runner are present yet. Unblocks with TD-WP-0002-T04 (deterministic semantic -kernel), which introduces the first Python package and pytest configuration. +**Done 2026-08-22.** Unblocked by TD-WP-0002-T04. `pyproject.toml` added; +`python3 -m pytest -q` is the whole workflow — stdlib only, no install step. +Commands and the architectural non-negotiables are recorded in `AGENTS.md` +under the repo-extensions marker, and in `README.md`. Seeded workplan: `workplans/TD-WP-0002-vertical-spike-crystallization.md`. diff --git a/workplans/TD-WP-0002-vertical-spike-crystallization.md b/workplans/TD-WP-0002-vertical-spike-crystallization.md index 8ea00e1..c558ae1 100644 --- a/workplans/TD-WP-0002-vertical-spike-crystallization.md +++ b/workplans/TD-WP-0002-vertical-spike-crystallization.md @@ -176,7 +176,7 @@ Three things worth carrying forward: ```task id: TD-WP-0002-T04 -status: todo +status: done priority: high state_hub_task_id: "ffbcc9e8-c1bd-5c50-b62d-495ad9e135a4" ``` @@ -198,6 +198,26 @@ structured Evidence Pack; the scenario replays from known initial state. Emit raw `EnergyEvent` records from this point onward. Implement no scoring. +**Done 2026-08-22.** `src/testdriver/` (11 modules), `lab/minimal.py`, +`scenarios/alice_bob_carol.py`, 16 passing tests. The reference scenario runs +end to end and replays identically from the same seed; evidence comes out +stratified 3/3/3 across S1/S2/S3. + +Three things that came out of building it rather than designing it: + +- **The observation channel needs two probes, not one.** Reading stored state + alone verifies test-driver's reimplementation of the rules rather than the + system's enforcement of them; probing enforcement alone cannot notice that + record and enforcement disagree. The lab exposes both, and their disagreement + is now an invariant (`i-enforcement-matches-record`). That invariant is what + catches an authorization defect which leaves the audit trail looking correct. +- **A seeded `RevokeIsCosmetic` lab already fails the run** — both the claim and + the independent invariant fire, and the claim set is provably untouched. Early + evidence for H-004, though not yet the experiment. +- **Scenarios are Python, not YAML.** Claims are predicates over observations; a + YAML dialect able to express them would be a programming language with worse + tooling. Revisit once we know which predicates actually recur. + ## Test-driver lab with labelled ground truth ```task