From 1b9860a8eead8fa93d8f69f42d03bceaa19e5005 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 23 Aug 2026 00:39:36 +0200 Subject: [PATCH] T10: gate review and first compression pass All four gate criteria met. False Adaptation Rate 0/7 with 12 of 13 mechanical mutations absorbed. 178 tests pass. TD-WP-0002 finished. Fitness loop closed via F-0003: actor isolation was a property of scenarios written to expose it, not of runs. Actors now carry an automatic private marker and the runner examines all of them on every scenario, with two permanent regressions behind it. Compression - six abstractions removed, each declared and never used: Verdict.SUSPICIOUS (a verdict no oracle could emit), Step.expect_refusal, ActorIsolationError, World.seed, EvidencePack.latest, Trajectory.method. F-0008: Temperature may be redundant. Crystallization was built without it ever being consulted; measured stability of realization did the work, and is observed rather than declared. Gated for removal alongside energy.py. INTENT_CHANGED and REALIZATION_FAILED had never run. Both now have purpose-built cases and a test that fails if a seventh outcome is added without one. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 1629012@bnt-lap001 Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39 --- INTENT.md | 13 +- README.md | 1 + WORK-RECORDS.md | 2 +- history/2026-08-23-td-wp-0002-gate-review.md | 212 +++++++++++ research/concepts/fitness-map.md | 23 +- .../F-0003-isolation-needs-canaries.md | 31 +- .../F-0008-temperature-may-be-redundant.md | 70 ++++ .../alice_bob_carol.cpython-312.pyc | Bin 7840 -> 7782 bytes scenarios/alice_bob_carol.py | 4 +- src/testdriver/__init__.py | 4 +- .../__pycache__/__init__.cpython-312.pyc | Bin 1358 -> 1323 bytes .../crystallization.cpython-312.pyc | Bin 12997 -> 12833 bytes .../__pycache__/evidence.cpython-312.pyc | Bin 4542 -> 4214 bytes .../__pycache__/oracles.cpython-312.pyc | Bin 4550 -> 4622 bytes .../__pycache__/runner.cpython-312.pyc | Bin 9974 -> 11363 bytes .../__pycache__/scenario.cpython-312.pyc | Bin 2269 -> 2217 bytes .../__pycache__/world.cpython-312.pyc | Bin 4538 -> 5077 bytes src/testdriver/crystallization.py | 4 +- src/testdriver/evidence.py | 6 - src/testdriver/oracles.py | 7 +- src/testdriver/runner.py | 27 +- src/testdriver/scenario.py | 1 - src/testdriver/world.py | 28 +- ...e_e2_use_case.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 12422 bytes ...lassification.cpython-312-pytest-7.4.4.pyc | Bin 22324 -> 30781 bytes ...ence_scenario.cpython-312-pytest-7.4.4.pyc | Bin 13438 -> 15885 bytes .../__pycache__/checks.cpython-312.pyc | Bin 9566 -> 10530 bytes ...ecks_can_fail.cpython-312-pytest-7.4.4.pyc | Bin 17844 -> 20586 bytes ..._verification.cpython-312-pytest-7.4.4.pyc | Bin 14275 -> 15515 bytes tests/selfverification/checks.py | 17 + .../selfverification/test_checks_can_fail.py | 33 ++ .../test_self_verification.py | 9 + tests/test_audit_core_e2_use_case.py | 136 +++++++ tests/test_classification.py | 95 +++++ tests/test_reference_scenario.py | 16 +- usecases/__init__.py | 5 + usecases/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 329 bytes ...it_core_e2_tenant_boundary.cpython-312.pyc | Bin 0 -> 10754 bytes usecases/audit_core_e2_tenant_boundary.py | 331 ++++++++++++++++++ ...-WP-0002-vertical-spike-crystallization.md | 45 ++- 40 files changed, 1074 insertions(+), 46 deletions(-) create mode 100644 history/2026-08-23-td-wp-0002-gate-review.md create mode 100644 research/findings/F-0008-temperature-may-be-redundant.md create mode 100644 tests/__pycache__/test_audit_core_e2_use_case.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/test_audit_core_e2_use_case.py create mode 100644 usecases/__init__.py create mode 100644 usecases/__pycache__/__init__.cpython-312.pyc create mode 100644 usecases/__pycache__/audit_core_e2_tenant_boundary.cpython-312.pyc create mode 100644 usecases/audit_core_e2_tenant_boundary.py diff --git a/INTENT.md b/INTENT.md index 904b763..e357550 100755 --- a/INTENT.md +++ b/INTENT.md @@ -116,6 +116,14 @@ COLD -> deterministic The framework should make it natural for tests to crystallize as software cools. +> **Under review since T10 — see `research/findings/F-0008-temperature-may-be-redundant.md`.** +> Crystallization was built and demonstrated without Temperature ever being +> consulted. What actually triggered freezing was *measured stability of +> realization* — the same path found across several runs — which is observed, +> not declared. Temperature is a label a human would have to maintain and keep +> honest. If measured stability is sufficient, Temperature is a concept the +> framework does not need. + --- ## Energy @@ -248,10 +256,13 @@ Initial verdicts are: ```text PASS FAIL -SUSPICIOUS INCONCLUSIVE ``` +`SUSPICIOUS` was listed here originally and **removed at T10**: no oracle could +produce it, and a verdict nothing can emit is a promise the framework does not +keep. It returns, with an identifier, if a mechanism ever needs it. + --- ## Lineage diff --git a/README.md b/README.md index 8a3d0d8..d5ce8d3 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ No third-party dependencies. Python ≥ 3.11, pytest for the suite. ``` src/testdriver/ the kernel — intent, world, actions, drivers, observers, oracles, evidence, runner +usecases/ durable test intent, including not-yet-runnable use cases lab/ the system under test scenarios/ reference scenarios research/ hypotheses, experiments, findings, fitness map diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index a61b979..c2d070d 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -21,5 +21,5 @@ | task | TD-WP-0002-T06 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T07 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T08 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | -| task | TD-WP-0002-T09 | todo | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | +| task | TD-WP-0002-T09 | done | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | | task | TD-WP-0002-T10 | todo | — | workplans/TD-WP-0002-vertical-spike-crystallization.md | diff --git a/history/2026-08-23-td-wp-0002-gate-review.md b/history/2026-08-23-td-wp-0002-gate-review.md new file mode 100644 index 0000000..e599184 --- /dev/null +++ b/history/2026-08-23-td-wp-0002-gate-review.md @@ -0,0 +1,212 @@ +# TD-WP-0002 — Gate Review and First Compression Pass + +**Date:** 2026-08-23 +**Workplan:** `TD-WP-0002` — Vertical spike: falsify the crystallization thesis +**Tests:** 172 passing · **Lab catalogue:** 24 labelled mutations +**Assessment that set the direction:** `history/2026-08-22-concept-assessment-swot.md` + +--- + +## 1. The gate + +The workplan fixed four criteria in advance, before any code existed. + +### Criterion 1 — recover from M01 and M02 with zero claim or invariant changes + +**Met.** + +| Mutation | Classification | Claim set unchanged | Accepted without a human | +|---|---|---|---| +| M01 sharing control moved into a modal | `MECHANICAL_ADAPTATION` | yes | yes | +| M02 DOM rewritten, test ids dropped | `MECHANICAL_ADAPTATION` | yes | yes | + +The claim set is unchanged *by construction*, not by good behaviour: claims are +run inputs and the adaptation machinery has no write path to them (D-02). + +### Criterion 2 — the authorization defect is reported as a Product Finding + +**Met**, with a naming correction. The workplan inherited "M05" from the original +milestones document; in the catalogue as built, the authorization defect is +**M15** (revocation updates the record but not enforcement). All three +authorization defects behave correctly: + +| Mutation | Classification | Regressions reported | +|---|---|---| +| M15 revoke is cosmetic | `BEHAVIOUR_CHANGED` | `c-bob-revoked`, `i-enforcement-matches-record` | +| M17 anyone can read anything | `BEHAVIOUR_CHANGED` | 5 assertions across 3 steps | +| M20 tenant isolation leaks | `BEHAVIOUR_CHANGED` | 5 assertions across 3 steps | + +None is safe to accept. The classifier collapses `PRODUCT_DEFECT` and +`SEMANTIC_CHANGE` into one escalating outcome — see F-0006 — so "Product Finding" +in the original wording is now "escalated to a human with the regressions named". + +### Criterion 3 — one asset crystallizes into a deterministic test + +**Met.** `crystallized/test_grant_access.py` is generated, runs with **no model +involvement**, preserves its ancestor's oracle set as a superset, agrees with the +ancestor across five lab versions, still catches a seeded defect, and carries its +lineage in its own docstring. + +### Criterion 4 — False Adaptation Rate = 0 + +**Met. 0 / 7.** No `DEFECT`-labelled mutation was classified as safe to accept, +including the three E-003 attacks (surface substitution, concurrent +mechanical+defect, evidence starvation) and provenance laundering. + +The other side of the trade is asserted too: **12 of 13** mechanical mutations are +absorbed without a human. A classifier that escalated everything would score a +perfect FAR and be worthless. + +### Gate verdict + +**Passed, on all four criteria.** With one qualification that belongs in the same +sentence: FAR = 0 follows largely from *architecture* — claims are run inputs, +and `SAFE_TO_ACCEPT` is a closed two-element set — not from classifier accuracy. +The experiment shows the architecture behaves as designed over 24 hand-written +mutations. It says nothing about mutations nobody thought of. + +--- + +## 2. What the evidence changed + +Three of the project's own claims were narrowed or corrected by building them. +This is the part worth more than the green gate. + +**H-001 was narrowed (F-0005).** Semantic actions do not generally outlast +recorded interaction sequences. Where an application keeps stable identifiers, +the two arms tie exactly — 9/9 against 9/9 — and the conventional approach is +cheaper and deterministic. Semantic actions earn their keep only where +identifiers are absent or not carried forward (2/3 against 0/3). The concept +model presents this as a general superiority; it is conditional. + +**The classifier cannot infer intent (F-0006).** The T02 design listed +`SEMANTIC_CHANGE` as an outcome the decision table could produce. It cannot: a +deliberate product decision and a defect are behaviourally identical, as M12 and +M19 have asserted since T05. The two collapse into `BEHAVIOUR_CHANGED` and the +distinction becomes a recorded human adjudication. Erring toward false alarm +costs one review; erring toward false adaptation ends the project. + +**Crystallization's economic case is unmeasured (F-0007).** The descendant is 54% +cheaper than its ancestor, and that number must not be quoted in support of the +thesis. The runtime is token-free by design, so the entire saving is one page +fetch, one parse and a two-candidate scoring pass. The saving the concept +actually claims — model tokens, latency, retry variance — is absent from the +measurement. + +--- + +## 3. The compression pass + +### Which concepts have no supporting evidence? + +`Temperature`, `Confidence`, `Campaign`, `Metabolism`, `Retirement` — declared, +unimplemented, never consulted by any decision. + +`Temperature` is the decidable one and is now **F-0008**. Crystallization was +built and demonstrated without it: what triggered freezing was *measured +stability of realization*, which is observed rather than declared. A declared +temperature is a claim nobody verifies, and the project's own first heuristic +argues against it. Not deleted yet — two unevidenced cases for keeping it are +recorded — but gated: if the next workplan completes without a decision +consulting it, it goes. + +### Which implementation abstractions have no conceptual justification? + +Removed at T10: + +| Removed | Why | +|---|---| +| `Verdict.SUSPICIOUS` | No oracle could produce it. A verdict nothing can emit is a promise not kept. | +| `Step.expect_refusal` | Never set by any scenario. Speculative generality. | +| `ActorIsolationError` | Declared, never raised, never caught. | +| `World.seed` | Written at T04, never read. Replay comes from rebuilding the lab. | +| `EvidencePack.latest()` | Never called. | +| `Trajectory.method` | Computed on every capture, never read. | + +### Which metadata has not informed a decision? + +**`EnergyEvent` history.** Events have been recorded since T04 and have informed +nothing — as designed, since H-005 is dormant. Capture is retained because +history cannot be reconstructed later while scores always can, but this is now on +the same gate as Temperature: if the next workplan ends with no decision having +used it, the module goes. + +### Which agentic behaviour can now be deterministic? + +The `grant_access` realization, and it has been — that is T09. Nothing else in +the spike is agentic. + +### What have the experiments falsified? + +Nothing outright. Two claims were **narrowed** (H-001, and the classifier's +output space), and one criterion was met in a sense **too weak to support the +thesis** (H-003's cost clause). No hypothesis was rejected, and none was promoted +past `EXPERIMENTING`: 24 hand-written mutations is a small, self-chosen sample. + +### Two outcomes that had never run + +`INTENT_CHANGED` and `REALIZATION_FAILED` were declared at T08 and produced by no +test. Code that has never run is code nobody has checked. Rather than delete +meaningful outcomes or trust them untested, both now have purpose-built cases, +and `test_no_classification_is_unreachable` fails if a seventh is ever added +without one. + +Building the `REALIZATION_FAILED` case clarified a boundary worth keeping: a run +that asserts *nothing at all* is `AMBIGUOUS`, not `REALIZATION_FAILED` — a use +case with no claims cannot conclude anything, however well its steps ran. + +--- + +## 4. The fitness loop, closed + +The milestone asks that a framework finding become a permanent guarantee rather +than a note. **F-0003 is resolved.** + +The finding: actor isolation left no trace in ordinary evidence. The self-test +caught a shared memory store only because the harness planted per-actor secrets +first. Isolation was a property of *scenarios written to expose it*, not of runs. + +The fix: every `Actor` is seeded at construction with an automatic private +marker, and the runner examines all actors on **every** scenario, recording the +verdict as S3 evidence. Two regressions now stand behind it — +`test_every_run_records_a_verdict_on_isolation`, and a self-check that catches a +leak with the harness planting nothing at all. + +``` +Framework Finding (F-0003) + → classified FRAMEWORK_LIMITATION + → improvement hypothesis: automatic canaries make isolation observable + → implemented at T10 + → permanent self-verification + deterministic regression + → fitness map updated +``` + +That is the first complete Concept–Implementation Fitness Loop, which the +milestones document calls the point at which the self-improvement system +genuinely exists. + +--- + +## 5. Where this leaves the project + +**The thesis survived contact with its own evidence, in narrowed form.** The +distinctive claim — that verification can adapt to a changing surface without +adapting to a defect — is demonstrated, and the demonstration is architectural +rather than statistical, which is the stronger kind. + +**The single highest-value next step is a bounded live-model experiment.** Two +independent findings converge on it: M22 defeats the heuristic runtime while +remaining solvable by reading a visible label (F-0005), and crystallization's +economic case cannot be measured without token costs (F-0007). One experiment +settles whether a model adds capability *and* whether crystallization has a +product rationale. The `RealizationMetrics` fields were populated from the first +run so that comparison is a subtraction rather than a re-run. + +That should come before any further framework feature. + +**Open findings carried forward:** F-0003 resolved · F-0004 (structural +durability only; visual relayout untested) · F-0005 (H-001 narrowed) · F-0007 +(cost unmeasurable; generated tests not fully standalone) · F-0008 (Temperature +gated). The F-0001 residual — the taxonomy has no class for document-to-document +drift — was considered and deliberately not resolved: adding a finding class to a +taxonomy already larger than its evidence would be the opposite of compression. diff --git a/research/concepts/fitness-map.md b/research/concepts/fitness-map.md index c8c4c75..32a0046 100644 --- a/research/concepts/fitness-map.md +++ b/research/concepts/fitness-map.md @@ -1,6 +1,6 @@ # Concept ↔ Implementation Fitness Map -**Updated:** 2026-08-23 (TD-WP-0002-T09) +**Updated:** 2026-08-23 (TD-WP-0002-T10) 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 @@ -29,7 +29,7 @@ were aspirational, not evidenced. | Concept | Level | Implementation | Experiment | Evidence | Open question | |---|---|---|---|---|---| | `C-use-case` | C1 | `intent.py` | — | — | Is a use case expressible without leaking mechanics? | -| `C-actor-isolation` | C1 | `world.py` | E-001 | `td://self/actor-isolation` | **F-0003** — only observable when the scenario plants canaries. | +| `C-actor-isolation` | **C2** | `world.py`, `runner.py` | E-001 | `td://self/actor-isolation`, examined every run | **F-0003 resolved** — automatic canaries; observable on every scenario. | | `C-semantic-action` | **C2** | `actions.py`, `agentic.py` | E-001 (partial) | T07 arm comparison | **F-0005** — supported only where stable identifiers are absent. Narrower than the concept model claims. | | `C-oracle-independence` | **C2** | `runner.py`, `oracles.py` | E-001, E-003 | — | Independence of components ≠ independence of belief. (H-004) | | `C-evidence-pack` | C1 | `evidence.py` | — | `td://self/evidence-reproducibility` | Verdicts are reproducible from S3 alone, on passing and failing runs. | @@ -39,9 +39,9 @@ were aspirational, not evidenced. | `C-crystallization` | **C2** | `crystallization.py` | E-002 | T09 agreement matrix | Fidelity supported; **F-0007** — the economic case is unmeasurable with a token-free runtime. | | `C-intent-provenance` | C1 | `provenance.py` | E-003 | `td://self/intent-independence` | Constrains provenance, not quality. Accepted residual. | | `C-lineage` | C1 | `scenario.py`, generated headers | E-002 | generated module docstring | Parent pointer plus provenance in the artefact; no graph. | -| `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-energy` | C0 | `energy.py`, capture only | — | none | Dormant. **Gated**: if the next workplan ends with no decision having used the history, remove. | +| `C-temperature` | **C0 — under review** | — | — | none | **F-0008** — crystallization was built without it; measured stability did the work. Gated for removal. | +| `C-confidence` | C0 | — | — | none | Declared, unimplemented, never consulted. Same position as `C-temperature` without a built alternative. | | `C-campaign` | C0 | — | — | — | Deferred. | | `C-metabolism` | C0 | — | — | — | Deferred. Depends on C-energy. | | `C-retirement` | C0 | — | — | — | Deferred. Depends on C-energy. | @@ -58,6 +58,13 @@ validated. They are revisited at T10, where the question is not "when do we buil these" but "does the evidence justify keeping them in the model at all". **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. +concept above. + +**Removed at T10** (compression pass — see the gate review § 3): +`Verdict.SUSPICIOUS`, `Step.expect_refusal`, `ActorIsolationError`, +`World.seed`, `EvidencePack.latest()`, `Trajectory.method`. Each was declared and +never used; `SUSPICIOUS` was additionally a verdict no oracle could emit. + +**Gated for removal**: `C-temperature` (F-0008) and `energy.py`. Both survive the +current review on the strength of being cheap, not of being used. If the next +workplan closes without a decision consulting either, they go. diff --git a/research/findings/F-0003-isolation-needs-canaries.md b/research/findings/F-0003-isolation-needs-canaries.md index 008f659..d0c93b8 100644 --- a/research/findings/F-0003-isolation-needs-canaries.md +++ b/research/findings/F-0003-isolation-needs-canaries.md @@ -2,12 +2,13 @@ id: F-0003 type: framework-finding class: FRAMEWORK_LIMITATION -status: open +status: resolved discovered: "2026-08-22" discovered_by: TD-WP-0002-T06 workplan: TD-WP-0002 task: TD-WP-0002-T06 -carried_to: TD-WP-0002-T10 +resolved: "2026-08-23" +resolved_in: TD-WP-0002-T10 --- # F-0003 — Actor isolation is only observable if the scenario plants canaries @@ -55,9 +56,31 @@ system. Worth stating plainly rather than counting four green self-tests as four equivalent proofs. They are not equivalent. -## Candidate resolutions +## Resolution (T10) -Not resolved now; deciding cheaply here would be guessing. +Candidate 3 — **both**. + +Every `Actor` is seeded at construction with an automatic private marker +(`Actor.canary`), and `Runner._isolation_violations` examines every actor on +**every** scenario, recording the verdict as S3 evidence. An isolation violation +now leaves a trace whether or not anyone thought to look for one. + +Two permanent regressions stand behind it: + +- `tests/test_reference_scenario.py::test_every_run_records_a_verdict_on_isolation` +- `tests/selfverification/test_checks_can_fail.py::test_a_leak_is_caught_without_the_test_planting_anything` + — an actor holding another's marker is caught by the ordinary run, with the + harness planting nothing. + +The second limitation stands as recorded: oracle independence is enforced by +construction, so it is verified once rather than per run. That asymmetry is real +and is now stated in the fitness map rather than hidden behind two equally green +self-tests. + +This finding closed the first Concept–Implementation Fitness Loop — see +`history/2026-08-23-td-wp-0002-gate-review.md` § 4. + +## Candidate resolutions considered 1. **Automatic canaries** — the runtime seeds each actor with a unique private marker at construction and the isolation check runs on every Evidence Pack, diff --git a/research/findings/F-0008-temperature-may-be-redundant.md b/research/findings/F-0008-temperature-may-be-redundant.md new file mode 100644 index 0000000..a867ce3 --- /dev/null +++ b/research/findings/F-0008-temperature-may-be-redundant.md @@ -0,0 +1,70 @@ +--- +id: F-0008 +type: framework-finding +class: UNNECESSARY_COMPLEXITY +status: open +discovered: "2026-08-23" +discovered_by: TD-WP-0002-T10 +workplan: TD-WP-0002 +task: TD-WP-0002-T10 +carried_to: next workplan +--- + +# F-0008 — Temperature may be redundant; measured stability did the work + +## Observation + +`Temperature` (`HOT` / `WARM` / `COOL` / `COLD`) is one of the more prominent +ideas in `INTENT.md` and the Concept Model. It is supposed to govern which +verification mode an asset uses, and it is the mechanism by which "tests +crystallize as software cools". + +The spike built crystallization end to end. **Temperature was never consulted, +never implemented, and never missed.** + +What actually triggered freezing was `assess_stability`: the same realization +path observed across several consecutive runs. Nothing declared the surface cold; +the surface was observed to have stopped moving. + +## The argument for removal + +The two mechanisms answer the same question, and only one of them can be wrong: + +| | Temperature | Measured stability | +|---|---|---| +| source | declared by a human | observed from runs | +| upkeep | must be maintained and kept honest | none | +| failure mode | says `COLD` while the surface churns | none — it is the churn | +| already built | no | yes | + +A declared temperature is a claim about the system that nobody verifies. The +project's own first heuristic — *do not let a declaration substitute for +evidence* — argues against keeping it. + +## The argument for keeping it + +Two cases measured stability cannot cover: + +1. **Forward-looking scope.** A team knows a rewrite lands next month. + Temperature can say `HOT` before any run has observed instability, where + measured stability would happily freeze an asset the day before it breaks. +2. **Cheap prioritisation.** Deciding *where to spend effort* across many + capabilities may want a coarse label without running anything. + +Neither case arose in the spike, and neither is evidenced. + +## Recommendation + +Do not delete yet; do not build either. Carry `Temperature` as **explicitly +unvalidated** in the fitness map, and set a gate: if the next workplan completes +without any decision consulting it, remove it from the concept model. + +Recorded now because the compression review's purpose is to catch concepts that +survive on the strength of being attractive rather than useful, and Temperature +is currently the clearest instance in the corpus. + +## Related + +`Confidence`, `Campaign`, `Metabolism` and `Retirement` are in the same position — +declared, unimplemented, never consulted — but Temperature is the one with a +built alternative already doing its job, which makes it the decidable case. diff --git a/scenarios/__pycache__/alice_bob_carol.cpython-312.pyc b/scenarios/__pycache__/alice_bob_carol.cpython-312.pyc index 9053e9f2bf8653b87f2d25000d3a4ec49519cd5e..619b306826fb6bed53067d6da7da44a86475bce7 100644 GIT binary patch delta 568 zcmZ2r`^<*#G%qg~0}yOg>dM+Hv60VRi1F0qFrn8$H7sk`SF?bm7#M2VN(3P+28I-l z5+NA7hPgx(#!lfZ5reT;GeKBLvRox%la~w2>T{>?lt{zWrSKy3r0}Ki&tWQ&fy)WB zFsu=rY%d~hD1@R%7)cLjib#s+9A+e4VlZ9K;wZW#P;^P6*dztg1M}-@W{3;YnAb>8 zo-2~gE>pvvBD+RzGQX%Mqx@z|(L6>*mdU%tBq!ezJH^;Pd8v3LW9a1n;>pr6K#fI` zAVLaA6f*(|g(8rDiXtW}O3E-!ojgmzlrt105DO$GJBW)-ekUOzod6QSqi^LB>@)+{ouH#CUIVn9%F`bcPh>MT{i^AW0BNVV}cP!?K2B zH7kT)%T^)`VFBehOGIGo8s-vl7(0avp?@_KL=;Ju8&fuosYDX2&KO9h@TBmTNW%@| zLnu$-PZ5~IR3ZbH6Kr8vBQ*KGu(Y8tiXIUpJzOcGDPnV&k#vc}bU90)=#oUyC52*> zG)T{C7KqKOnIUu<^BS4Sog&%nvNh}}a%j$e4_*VlGg{O1WDcL9W66p+U~B zMcg3EI6{M6NCz1c22#S3mzbM+i?uwzC?};Td$NU; zC08|2ycCFw^CwqJm5Sd|v^v9aQ9SU5xbzoRCSKVF*Do9l0uqydN&ROmp8QLCs$c=g zAkMU+{HoNv;?$C&qR9(n^i&FgqD3IHia@bjBnKiuk@t(kCO1E&G$+-rsCDxvnH)wA J14a|DGyokBlzadH diff --git a/scenarios/alice_bob_carol.py b/scenarios/alice_bob_carol.py index 70bcfa7..e9c10a8 100644 --- a/scenarios/alice_bob_carol.py +++ b/scenarios/alice_bob_carol.py @@ -128,9 +128,7 @@ def build(*mutations: str): 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) + world = World(id="w-baseline", sut=lab, sut_version=lab.version, cast=cast) scenario = Scenario( id="sc-share-resource", diff --git a/src/testdriver/__init__.py b/src/testdriver/__init__.py index d56c3ca..31418a5 100644 --- a/src/testdriver/__init__.py +++ b/src/testdriver/__init__.py @@ -10,10 +10,10 @@ 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 +from .world import Actor, Cast, World __all__ = [ - "Actor", "ActorIsolationError", "Cast", "Claim", "CollectorIndependenceError", + "Actor", "Cast", "Claim", "CollectorIndependenceError", "DirectDriver", "EnergyEvent", "EnergyEventType", "EvidencePack", "InadmissibleProvenance", "Invariant", "Judgment", "Observation", "Oracle", "Provenance", "Realization", "RunResult", "Runner", "Scenario", diff --git a/src/testdriver/__pycache__/__init__.cpython-312.pyc b/src/testdriver/__pycache__/__init__.cpython-312.pyc index 216faa64380a0459d764e2d73536f5c4786725f3..833fb3f45533c57342643bb6abf13ddb01edb6e7 100644 GIT binary patch delta 111 zcmX@dwVI3fG%qg~0}z-fb!FvDPHtogW;B|7mBmfK>N11XCl(HFrbhN61E5j>I9)0X diff --git a/src/testdriver/__pycache__/crystallization.cpython-312.pyc b/src/testdriver/__pycache__/crystallization.cpython-312.pyc index 6e999c0c09aec992183c9a5cb12d465ef6ed522d..9eee1fbdc993d28ba969e85eea62a900f869ddc0 100644 GIT binary patch delta 1571 zcmZ8hTWl0n7@o5`JA0YEwCwh>Z7H)$TX%Y+TMdv>No$oR7ApZH0gIdM&S_cbUN|$G z&{AxP28jkJoCl4N2)>#6ATCi~jW-gFk>G=wn8>RSn3^^`V7&bQnF6LK*>At||Nr^t zznuSkLvI+N??a)01ZyJs_VMwXp$|P|FaN|dSm&j(;Tr^np&I@- z-2p!++>%6AKjpWT(?KhQE^y6D{6tf~9>kKRPUR!~9>*2w?yPAWPaGhg6Sz9hfso7c9&_9Kb4!5>Q^YVD)B)u?$^DxdzxBI-wzrTk z<_nbq+r@9ix=51Wi`9D$g2}$>U$GzDV9sJhjZ41~< z7DYG$VEYVe=2(7`I_4V6TzSLxmrJIVnJTe@t~ya2!!yGBeq<$t83b`ZyzAjI(egaV zZF#a}%$h1tjCg&yAabKMMb1_)v@Q}d%EMYyzFnA)mx@$ZnD`D}Z0BP^WN5%}AayU> z&`TUECyohZ6@(34J%loZGQX@PoUR_KexZ#8ljzKrEqcn*U2F_DB=@+vc2*s*C^#8IUgi$cQ+C8IJ! z*=11OueNP#BxDzVwyz;33b+TPI{6(Q47i@`WqzzLv9J#|Yy?4=452P20(Tyyi>SJS zAVL=xN5n6fNJ6khG!cF<9p{2w0yY;qKu=a^F-Kn=F3uix@)IY-_k2k8CZ_Y6bRB8s z9qIf|d;5yF1Ih z)97Mr3x#M9BY2~R_y@kIsWFgEq>m9qNN;#{H@b9DlXo+NR>34Mmjasayg1}3 z`c;J@Y1)(1tyfJtC3X2Agj*2M${qTh^hWEXa4G;nA6-_kyl3_h-7Cidx?pyR8}t%)l09-i6D+Li!Fg#*Ki%ZE00L+OhtO>6BW(U z4eeI;W|j_COis5l?bia{#Qty#Ky?`!nLyys1>p~||GpBMJ+fF8Sr8+Oq44a{#XxYj zcO5n^h#ObM3XjhT0}(QaL3u$*yzmiL#7DxIFoh-s4MFx&k8l|c*O&UeKOlO=8`}_? zTR*8)W!qRfF0DF`v@nhM)$YigAK*zxyk2n!1qlquhxh-eX1#1H9ct0~d$$lit zSeOy+#~Nl1EC$1u`|br>Rs@C42X6N?B(80~n*H3oYJO!bH0+u`^lQVjSDw4Gb1_`^ zdpP<@?KSD+hJ|q3sv<{x{|d6#zpN-8|7uJKR9z0g^Ykxb4c|?%4K&gOp@xdU7g!(WD%gTsR_5x zrRp8%B#npfHS@DjA>IS7lI3k6tU7H#<*b> z@k>nIUwSce*NysUYjjsP^hgzPGyCJI+CezMI-D9!XDkyJ@iAs1A_JhBrSfjH9Zi+y zqc6Krj8?~JEdXRKSM+Du4$N490quWfnA~LB!i6+-Errg zb{U%OEttegjhZ;8`kk!C;S7&H2oeD(06;`rUQZYE;mCnX95J;b@iP%JUSBSj`ShRWLYUxE896A8Rh$X(x}9AQb@~Yx5B3 zIMBZu6Hc3lN|C1CKr;lhMN5C(QeF5cY*6Mwx8c|e^lUPT{-fU}Z%ONfW;>cg4_SN@ zytdF!JN8clc@B+mq0~wYMA?4UKjtNh;$Q<3hh_k6%Cf0t%DqbRwTiECBQv#V?5$4(Fn zC!6-=i^C3~hnZ1DOYAXY^NR=Q?XJZ1akjw+0XXL{VwH`xPZC@%iIsq?9Dup3Q+(^%D zGj{Py2Z`}pKc!~&+7D$mu5e3qM zm7JQAn?pHvGjCSpoyf@OJ$WKM2ZNI7D$my5e3qM zDN-q7Kw7AhQ&W1gFUM}?dM1!|C}0NCK+O0#4#=3!P{Ih4sA0%LRtFVc4V8Cch!tgE zsAZ~Q%3^`(K+rXe*%}NC#f&||HB8A&j10jHEDV(lMN&W&n#{NAIf_!tQj3aHQ;IBs zB1M{D;Uaq&yT}2^Dh4@BK|!HN0!ZIt%SkLrEiS1Nb}h?HNzF@64M001v)y8r+H diff --git a/src/testdriver/__pycache__/oracles.cpython-312.pyc b/src/testdriver/__pycache__/oracles.cpython-312.pyc index 798077640decdde7464362b4be5c20115f470c71..d8a11d5fe1a7445b3028f6c568823873b6ad209c 100644 GIT binary patch delta 1016 zcmZ9LO=uHA6vuaxO;Qs#Nt?FDuXb#Tbu|==RZvQ`jTKUAD#Qw=*0^M+&Di}2GnXd(x9?--?W5F{?&woR z2}|%h)^+*huyQ^60zJ9@{>@^3B7Z2G$&OFtPvCitV@6DSj_6o(@ZrAgd$46=n`tG3 z;F4W6bX;M!UM&$!oix@gh{JP)>9pj8E!!zmYX+Az3loz%m~v1d-lUvhr%doP(@ZjN zvstX$ghOfv56==(>BU(G3OiM1@id;NP8m{%sZK11PE*3tm`+0~la$JuMY)Ly)WjAc zI)^@L572G2KMAAVkcd=3x>WG#sNmOq1zGnO0t>Pl@Ipr#U3&7Np?uzx4-93;yw%xU zW;~Y}o5*L6jO5gyd)L3i{oo(juojZ>X23b=b)@;s{st18&W^FRT2{V`MmK=SHVSkI zV1X1s4YGeFFEFPWRcQB51oC7~jA@R*gIT*f80yAUzaVm}0*YR(uMHSuYj zwdOQNHOpZ;>;8jUr2-o<$sM@DA!}i(aSELvvMmClD>d-%zYKxmK5jYgtvIiD603sWKY?<5bk3+?gD$>N6%R=Tu_~QA16z zJ?^vkv5T#~xK1pTP%V(D7{7YY8Xtd@L{Yuz{Xmnikk7e(P^3Gmsy)7;*qpk#e3egfG$ B`vU*~ delta 888 zcmZ9K?@JUx7{_i&eFH@)#7&=#X!6ylrEG^AcNvxi^~>}Thh=R5QKn0XQX5D`E6 zd~Sw}efjy!cki-T2Cn6ApG)`I*8nu*7TD0M!CTydOXX~SLPuIQW7%+QAd#@)&_Ha& zb|=OYW3j>5=y<~R#^Qsc@xhVtL~P<(T;=g?c!otXUV_(^IugSS5 z;q`#>a0CTQd+;q7ZXpWPO3_B4P(%q-4*l=4UGr(9pdV5I8=AUt*91f!Mj^b#wSp)X z`2KbpHmvtWwTkWlCzHrWxV1!(dis6Qd=TWd32mAtJ9{bBP*NF*@Br%&zbk~7E_(EC_Q5EGI>AAUVZaQgNs53l}TbQb}AxxY| zrJ!l4uEQ0Q1)EQ$w0tI&LcLUTq{vfrjiL)TiWkC8k<`r4ejW@+CRGdrcH<>e1OT6w zWc))2V87%8ZhS|2AL1$(SLp;%JLMxkKJPzS@07^r=XI1e4Af7&N%EPGaM3?C$%|`z zLz!<_xmo6qZAig2NhwRps-SFmYF_p|>sy7PKfeNuzsq!yO-}+>+F@LU=n^rCqLDSN zGDd#e2VNL=^y6{)%o6TzlL%)oMh=i7IP7-Wa z0RVQ`Y5;!+84%fII=(PDbasy+lO2&^JxkFQ?GuRXvm$VBGX%SM MTMh~msCORKKR%_&4*&oF diff --git a/src/testdriver/__pycache__/runner.cpython-312.pyc b/src/testdriver/__pycache__/runner.cpython-312.pyc index 48c6c669a006421d8e63c6ba187ca82fa5bb0802..d9431ba1cac3e0c87fb41738216940518bc2aa92 100644 GIT binary patch delta 3250 zcmbVOT}&L;6~1?7XZHVpb{Cc(Phi)uF~3$^3{GO>fNijiUHlIrWV18ayT-fZ&VU1+ z4NhvQc12~J8-+x68puN($(0(5N=frlrE(=#l^Snx%XU;L50$DuRp7Wy^VHru?0{?W z)Ir>P=G=R}bLQN0?zjIs{MXZ--?`lmK%SRtew2A7cGuHneSU=ny^MRG_MGIMy$S`~Ck9Fcj+CGljoPQrwHao*YjBanJ3J=7}; z0Q}|)h;MRH{H;w8y1;dqV$vYR43z_rhZ@XS@RyvxWOzL64LK_;AkXCCS&(P(M_icN zxVpg5+oipBTyRIJ;sQ}vh_Mst zr++*v=CYzZA)`rANy%h6oof(@0a1@m(zRTcAYMdiRh6@%niEx7QN`)Bk{eB`>0GvH z9jkAOUvoNDW%yM@;#C|&>4~H&ix-mUyo|)$h&bgG$FmZV&&!iy!hv>>MCg`OQc-n& zEkK<~OSQOg1SxkK|NxbX0ewm2_57liBH%jAGP|Ju4Hl0h>L1wYwOpT#nQgB6UlV#3NvJhiBPhS=C3b4_x!d zmdd6Rv)o^r+dq%r3qEY_e&1&3PdqH!cK^9qt{5yU1Y@6sK8=4?f46=q(NPqtmxXvi zh!>;ceCT%BLfO3QK|{~|`2F$~0FS|VFLEPld`e`2;&A**Ub3lW5ffE+LbpMU9Xdsio ztAPs6N~CS1fkUtXU+`D4Ak%_dJ;Czr#P+F&^pI8Efsc5$;Ex3X?!*g`Xtl)MVkB^< z)UXhcTf-*td3ze<9U6m^b^*J+Vf?j^&!fl|Kn?Ic0SRL&7>2V%!^O? zB(GgymKpj3xocc_*dHOTgBHz|*@J)Vk67JyK;qv^BgUr#L0)r{rG4ar3c5?zP2OXE zptHfm&K#vtKsUY{7I?{Sm=FYbnBHs_&6DXjqV!~r)49TvIcZvM%#!0434kB_e&rjFhj3+*+kix->M|Zq1m9H zHblR~yY7KlmX7_2tn}hmQU=DCL^g}nWyBR?P0a13EE(Cd`?Y}NzQt|C&iN4|dGK9d zIX)hWvO&#;Uk^p#t42I2YRd&RUuplPaM0R8D!`GE=?Z=}5RQ0F${<|xLvsF3&5qhO zL2*8T5`s2-BOHQf@oKobMssG)Z6w5$)Ql-*Monise3Cy6@6gsHk9iP;C9YXW`OW;1 zgk~eWDWsUQls5w;96=NCTfWLYH<%g0LfJ#mEW?DP)*khBQ_3Jy+L%?LDoTC>d}vx(klr52G*HI3!c; z2>yGt23J;GgM0B$D{8jV8&tQAEAlX5N!cJw^cvMU#!zm8kam{P;X(Y5ik%$VM-iJU ztGY<2c%5Xfsw^4T_*yUrE4ScWWqB|Cjfd=X zr?bDi^H9$~=kcB+eY#DOl@v<9wno2*CQ^4O<7gzAl84h0qV{(OCdZU>ITf!~ zM*V0f)p40|>8y;piHsYnCK|&gVJnKPB3f)HP6v7==rEnpVN zEwvmexQ;&PKlzwr?T!@(p4)O7_Ajn%c@;(z9ji{>A6W4Ke`wBn!}>VraC=ro;EUi( z&n($aF#XUTSviMK*1YQ;W`Xx0{7(1u{rhnX{H_|G>>2nT xF2p)H9~b>5h6C|FIE9n(I;h}l@mmn#ty>SmpJB9B@$X^bUib(Y6WT&R{tIT-0}=oL delta 2014 zcma)7TWl0n7(QoqW@l!%d+Y9WyWP_5z1_B4W4RTDmKG?NV!5)ojK>b#FKsAFFhU?qE9G#S^uQ^hIf_t!Yg!n6=`S)t7wRmk?NGu^FLKoxr@S( zLzPs=MSh5f3t|XnMV|hJpsey#Ca5k|pl|0{9Ct5V7uVp0kjKSUSbQp(Gc0E^Wn@T> zL})Zim`t9W(4gIy!5s_l`&MJO#f__EJJpeHc<7Jf9{9t*Rw)RWbT=b=80m$sz)IW; z*+4qrr2+>+_!qN;)nH<{CWICorQdTUV<<1Gd|nzyc^NXLVR)oe2rg6B<8WIDI$U}S zG?c9ky5=#(pbthft9fK{dK>fyLnX7^@DB*(-6qn#5C{fgpge@#(5ZwGc1P|4AKz~GWZ?WMi(RVRB_^F~y5N+GLWsUHWEODj>0#%B` zqwlp-^XPrh?+Oai9B;be+i(rsEDLgaKf``Ykpy26T3&{RrHFzN+$alidKYu{$;RPg zT+=sC`x32$Tjg#V#0H0l<-F`yQ9e+}npQCYI1+Z)HPF={OT|Y$3n`nJ&9Lm#)I?XwW@clRx5xdcw!4x-! zQrvJUjU66#;GEwr&$!7h0~RlFkdO-iK6Qo>d;X% zGtL${Q(PWb$P5|J(mMx#Ri45oB%>8@X+@Lo{IZ9`4AEYEx}q*fn_PJ65y|Jh|7YC7 zKgRuZFIN~li#VpYn2qY3bJ%05>N|qyp7Xxu#b_4K94+uucnY0y96?jK4jGA}J59rh znRVa@n&oDm6m3!*wtF^(;(mz5Hp67B11I2SY`f$ngVdWO;LliXz)!T%iCpsJh;}Bc zr3_7ls_M&l8+=$@+qjFGS<*yK8=*&&Mp*9&`&=wnmSiTibTXaNNEa+sZwRz{q|k z#VHGq!`<4I4Xk^XV4u^3eUg?#JCh>Qj1z5qO(iL)sjKG|Dudm1t!Zco0mLdp!AU+1(d7e74fm+_IQ_Skw5&d@gTPD>)1ND z(|AuB>}sCgF@#ZMQ|?6?>$9!R10m<@qs%Ws^CYGe}!?XZq|gy_{~xl`S?B zPu^f4WsKPPb(D*Xq=E^ej8rnhsv@% Z2GF_(UxRO2bAfRVcj2Fr{UR~y{x@z`-V^`; diff --git a/src/testdriver/__pycache__/scenario.cpython-312.pyc b/src/testdriver/__pycache__/scenario.cpython-312.pyc index 8bf7e17b6abee1dcfc90fd3abc90da1d7658406c..3ef1d6055aa3a5728eb0d32862d6b11d40afbed4 100644 GIT binary patch delta 367 zcmcaBxKfb!G%qg~0}#Y0b!A=L$a|WR(O|MRtK8)8jKVCH?3x^tWtifb{WMu8Pi3;3 ze1pkbK@_OCmEj?a;{y)vX7?L>g3Z1+c%-hdNEJx}Ws3MF3$jXXPGgp3^biK|ML+~d zPmvgi5C;*gK;jl_Nohe&YLOyHKp8|xfC#V|QXrNLj1ZfAn#Gb0EFrU*i}f%gqs-)U zY$lBIlbPAod~jN#3DU0xBEZ(MAwlbfGXnv-f*WIQ>L!$gFYQThV|koe5Zz@+kl3CQ{=FnK120}IGd E0ONc{zW@LL delta 416 zcmZ1}cvq13G%qg~0}y-{?8^GIk@qwsquu12jD|uf>`|;K9H}fRoHjt&N-jEK)b56&m~|xLjdTD3S)MDdL-amql{35wk3#hbV|I1|q~kgan9? z1QD!2;udR3X+chEkupd?6+}pZ2(TG4AeJ1A5T87q#gYvyA-DM&%V9=FxyiHGOc)g> zpJ!9^!D)#$NWTt<09(hBlUZC+qzmGsS)>6H0})D-9oZfEz!rhHewuPO4pz>10_B6A?B>#SaWX;xjV?lgbAsAnT*hAhkxr@?c6;t!Xe%deE~IQ=gCUDuda(->j8M_PJKC%85| zqh`R@^vqVKmT6fvOVKl4wq@7sR!^-5{(G7AtXa$H8qcgOPLa&YD+vEA6XVUV1R=BX zl74X102}aU?J{;dB>E9>zbupgs~ju&&fAlyYK z;xr0q;B)FUqCiw=>|cy|7&>zvkbDOJ?yr%V3)%?$FcNVias$7b%Tah{uDW3q(lDS= z(}}1X1|84AeOS`H!Cc72BC6V3rRUtJ8N|R7QHP2cIDC4g={A~_FLKdV>XZ*OY&wF6 z%BdD_1)@zua19*}SloaezUVUUH#n$ZVBZCoT@XT-=Q|>3QAcTO1VZpeq!j&k`9*M% z(~Geq96#cmHUp2L5wDG&gbnUH!VN-tMYvJKeGCh~x)6k5C8{WWskd-2Ke$Z~9NY0c zy65D+>FMcv%9B;ayOeD>ps>BKY$RqJyDZ5d9!iWRzbcOzhku-8>nV%qg;e#y8}0yD z*4Fy8($I@%uh&*;^8ANGyFM7&bz|`E8~NQg%-y0W|7c8-m!wnLAwM#vYRrD4=XE{J zbjz4y z-CG`c>j`s)oH<$6#R$kGrr--=sl?ny^b}M^Qks-pY=3(Alz{Sb|5f@Dp3fMxC9y#p^Cx_)Hk$j#od8}^tw)%(*~# zEdBzv&dSeA6P*fO@ZwOl>IS3@rxLVxXw%SzyEEr;zTi1+m_{ua1@qYM1)qyuk0B%W zc+Z)GuL$CcP5O-5sRQYm3iRzL6>w>x%RJNPt#< zcKQ(L!HES{9ad2@Qoxj21aT))D$gF6PKQ967*&*bu@WPUqa+Vd2l)`Dq!N6k$kF`A?Yjnf1$)5FLP0znDxJrh<)JMtGng+08X!^(+{;uEO X-Ic%VA0Nq+eQO$k+F#dUrH=9~6Wm@C delta 1958 zcmZ8iO>7fa5Z<>x&ida@0wK^Oi&F^O7(+t~O`%XADFlhAiYiq#1+!$IvCDebdb?jX1WW!rGjHDa z=FPkr|2Fi)VCI)}Iw8U5%lzk`+>S3~w!Qx=Xp539l_i_lvO^kjStckej?z%eYC|h) zLRXzwqodr>(91fJWa*S-Yx9yFqw#wRGD=c8Axl(UXbux~;SR#jCdq#Lm=R9pkvS4M zk+mEMixQQ}1P+yLQdZissf1m}rvr~o_?}}0bY%40&Z|mjmGT6{p39vRQQzRVl{=U zcHU0UtL22@UT8$u536UV!~q;vbu0zY=Oe ztLe}%=6fulwxGL91e1^H!IS@q?<9M8E-^5Ni8N#42-b~<6%b+oVT{qB#oSPxgkmy5 z{-WdV4M($@L^~T4@x;6j>e~OPJhhJarAH9C+#)Tzy^iludgR$>h%l+e#VSG&`TH#5IXeET< zh^UM6e~|h+wgveP@Uir+ZsZRY%eH6rla?QpKpNrK(i4#0pVCKlkpTj}>C7u6 z$6w17$XonuW;-m`GS!po*;nfUW$XY_#iD|>>#kc znLX1d*yK=gs4K`ek?=d&?Q=(wH4kuI`aRou^Nq#B3x}7o0}u59aY^VVb`VyhgozSi z#=ts?R#EHc){VkVKwK7d?K#ol=hq7wDs?w7Q=5u70VG&OhE2)#BjBakK7>OEZzEuo z>^Qrr(`orez)ShUTq%)&_A@T}l-d_@I?)-*+Ci=EUN&5?!JG;_ zgV9T^x&va38ub^ciiYoj)d~#DU@g~}wp_t?iNcS;cP! zVG^JMggByD59Eez2f$GTti=cOX|T#`K@0C%lRUxuP&>_s0DGyq?w5ZL84NrwqVxK zN`)!>ac6NZjE{dhO`G`V68#6@+l_({2W}4HseYv4*)4}2WoN+;Tg|@)u%Zw`R^=E` z|IJBc@Goi4pOOJ{fSe(>%_kE6uIiGUyxF}>w*Dt?A;T*Yz?zbf` RWbcXuu%^Jmstl62#s9rp%)I~r diff --git a/src/testdriver/crystallization.py b/src/testdriver/crystallization.py index 7897e2c..6e90483 100644 --- a/src/testdriver/crystallization.py +++ b/src/testdriver/crystallization.py @@ -35,7 +35,6 @@ class Trajectory: step_id: str action_name: str surface_id: str - method: str target: str fields: tuple[str, ...] @@ -43,7 +42,7 @@ class Trajectory: return json.dumps( { "step": self.step_id, "action": self.action_name, - "surface": self.surface_id, "method": self.method, + "surface": self.surface_id, "target": self.target, "fields": sorted(self.fields), }, sort_keys=True, @@ -63,7 +62,6 @@ def capture(pack: Mapping[str, Any]) -> tuple[Trajectory, ...]: step_id=obs["step_id"], action_name=str(mechanics.get("action", "")).split("(")[0], surface_id=obs["data"].get("surface", ""), - method=("POST" if obs["data"].get("surface") == "browser" else "CALL"), target=target, fields=tuple(sorted(fields)), )) diff --git a/src/testdriver/evidence.py b/src/testdriver/evidence.py index 01a65a8..bdbc78d 100644 --- a/src/testdriver/evidence.py +++ b/src/testdriver/evidence.py @@ -72,12 +72,6 @@ class EvidencePack: 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"] = [ diff --git a/src/testdriver/oracles.py b/src/testdriver/oracles.py index 705b1ae..17f1c20 100644 --- a/src/testdriver/oracles.py +++ b/src/testdriver/oracles.py @@ -3,6 +3,10 @@ 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. +`SUSPICIOUS` was removed at T10: no oracle could produce it, and a verdict +nothing can emit is a promise the framework does not keep. It returns, with an +identifier, if a mechanism ever needs it. + `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, @@ -21,7 +25,6 @@ from .intent import Claim, Invariant class Verdict(str, Enum): PASS = "PASS" FAIL = "FAIL" - SUSPICIOUS = "SUSPICIOUS" INCONCLUSIVE = "INCONCLUSIVE" @@ -101,6 +104,4 @@ def overall(judgments: list[Judgment]) -> Verdict: 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/runner.py b/src/testdriver/runner.py index 833eab0..9aeb9e1 100644 --- a/src/testdriver/runner.py +++ b/src/testdriver/runner.py @@ -56,6 +56,24 @@ class Runner: # -- independence guards --------------------------------------------- + def _isolation_violations(self) -> list[str]: + """Does any actor hold another's canary? + + Run on every scenario, not only on ones written to test isolation. + """ + canaries = {actor.canary: actor.id for actor in self._world.cast} + violations: list[str] = [] + for actor in self._world.cast: + for key in actor.known_keys(): + value = actor.recall(key) + owner = canaries.get(value) if isinstance(value, str) else None + if owner is not None and owner != actor.id: + violations.append( + f"actor {actor.id!r} holds the private marker of {owner!r} " + f"under key {key!r}" + ) + return violations + def _assert_collector_independence(self, stratum: Stratum, collector: str) -> None: """S2 and S3 evidence may never be attributed to an actor. @@ -111,6 +129,12 @@ class Runner: EnergyEvent(asset.id, run_id, EnergyEventType.EXECUTED).as_dict() ) + isolation = self._isolation_violations() + self._record( + pack, Stratum.JUDGMENT, self._observer.name, "actor_isolation", + {"violations": isolation, "actors": sorted(self._world.cast.actors)}, None, + ) + judgments: list[Judgment] = [] scenario_sound = True claims_by_step: dict[str, list] = {} @@ -166,7 +190,6 @@ class Runner: "action": step.action.name, "surface_used": realization.surface_id, "refused_by_sut": refused, - "refusal_expected": step.expect_refusal, "postcondition_met": postcondition_met, }, step.id, @@ -176,7 +199,7 @@ class Runner: # An action that was accepted but did not take effect did happen — # and that is a statement about the system, judged below, not a # reason to stop judging. - if refused and not step.expect_refusal: + if refused: scenario_sound = False # --- invariants after every step ----------------------------- diff --git a/src/testdriver/scenario.py b/src/testdriver/scenario.py index a41de6e..c4ac795 100644 --- a/src/testdriver/scenario.py +++ b/src/testdriver/scenario.py @@ -17,7 +17,6 @@ class Step: id: str actor_id: str action: SemanticAction - expect_refusal: bool = False @dataclass(frozen=True, slots=True) diff --git a/src/testdriver/world.py b/src/testdriver/world.py index 04f2341..55289c8 100644 --- a/src/testdriver/world.py +++ b/src/testdriver/world.py @@ -8,12 +8,20 @@ to be executed by the same process. from __future__ import annotations +import uuid from dataclasses import dataclass, field from typing import Any, Iterator -class ActorIsolationError(Exception): - """Raised when one actor is offered another actor's private state.""" +def _canary() -> str: + """A value private to one actor, unguessable and unique per construction. + + Exists so that isolation is *observable*. Without it a run in which every + actor shares one memory store produces evidence indistinguishable from a + correct one — the guarantee holds only in scenarios written to expose it, + which is no guarantee at all (F-0003). + """ + return f"canary-{uuid.uuid4().hex}" @dataclass(slots=True) @@ -28,8 +36,13 @@ class Actor: 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) + canary: str = field(default_factory=_canary) + + def __post_init__(self) -> None: + # Seeded automatically, on every actor, in every scenario. An isolation + # violation now leaves a trace whether or not anyone thought to look. + self._memory.setdefault("__canary__", self.canary) def remember(self, key: str, value: Any) -> None: self._memory[key] = value @@ -65,15 +78,14 @@ class Cast: @dataclass(slots=True) class World: - """Initial state plus the handle to the system under test. + """The handle to the system under test, plus the cast acting on it. - `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. + Replay comes from rebuilding the lab through `build_lab`, not from a seed + dict carried here — the dict was written at T04, never read, and removed at + T10. """ 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_audit_core_e2_use_case.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_audit_core_e2_use_case.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78d416ea218bd30d7830d673a1d442cc71038b57 GIT binary patch literal 12422 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_classification.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_classification.cpython-312-pytest-7.4.4.pyc index ba46d325053a6a63237e6b30467872aa6d37473a..e875d95186040a9f31036466157b8237bde4510c 100644 GIT binary patch delta 6672 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 Gr~D5_<~o!B delta 359 zcmdn{fpN<^M!wU$yj%=G;HA)&bxLL;p9JHjiR$YZ^(J1^X4K!z#Q1}WrIK0GV6ymJRxZL_5ZCmZ9w$|`TqUW+B`HOjWvNAz`%6s3Z;2uV^a_gd%Tn_a z^O931pD0n{zr~xAn536mT9R0jnV(lY`F%;ez!8w4=^$bjh?qY)u2h=i7F%(CQAujb zhyZ!*7l%!5eoARhs$J17AQ$B4V%yCxO0Ai=J}a;? IvVm0s0JdRpSpWb4 diff --git a/tests/__pycache__/test_reference_scenario.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_reference_scenario.cpython-312-pytest-7.4.4.pyc index 9f5843bd06ba1c99d657062ef3969ed054c1a200..d39b6dda8a6b89dac65c973b457d601ea8f016d4 100644 GIT binary patch delta 1754 zcmb_dO>7fK6yCAd#$G$N6FYY7ILU_mF0m7*X;l>k#E%Fhl+cs}l~Q4hcLUCk7tJmZ zN4AnkNR5zsaGwN+LIsyf2pTntsvg=(+=|*mL1{(gLxJ8}sS>4Ce>k*#&j~bCRog>H zyWhNd@0+)8-prf*?Zg{-&u0}Cl7Owb>D-vZRrJ)b^ToEK%v+pKNUG|apxjg+9k~#h$Tocg~HkHAgzXP_#>f!Bx6OP9kv3D~DpKodaIUqqGb{Rem zNTJY0aoR2j1&3OrhVzaLD~~E+UtD38a3vCIbWX8?tsvzDB{EpDr6w|ItzA1m2R zeO$}(mtd|dVqZ(km97Pk!b(yynNDR>JeN$uP(rOcgdt6PQih>%Q*LQLnR`Jo_(@bM zYV;d0BekV9s#3L@ZWq}n2qvLh@nil*y88oZ zQm6z81fvQ;hU?w^QURrHqoAF5o9;vUCVPY-WGt%Za=fb{kR`KaR-?bzH+xQ7t95su(2Y{yl}fJ3O=y{b;a7K zY&y_Gxm{zcJrA3d*K!*RKx5R;9FVH_+ia@cd_3J{tv1O)tB0eoj?UVnzAQuLdgWYw zoooHLSXBp?JT)%bKHL{iBoaNF<&2?Eq|A(-MM>7CQj?jirph?fPHQ|T z8)*%TBcsRUtZt%uB1bkos~M7v?F`SDrk0gW9p|3Z4NcCNoh5H7ZR$LknR)E{8`>7G>2Ah(cNI-OaI%;0}>7vV*=dKD4ZtsldBJzZXhQ4+^9S(Uy4 z3tnY=NmNs2N^$>r@0F}b=X3PYEd()w0fJ5fDns5&5GNRckAsn!xTzUtvYeA7Kb1{# zEv<9aNT!mQCN+~blX^D!B$xbkocSWY#@7h5!jE>LG7QpO|7_p<_Isha3p2%;JE5N2 zp`M?t7N32^=YP-mU}|yd*3RMO=ZDW(ACPu-&jSHFs{49a^zDKhzIt{S?zcSC`lZq93R4f?UX6WHA#ekdX+7EX$i)iAjdf!>}Y>?pj|b8zck)bfJ_qXc6Fbib4Xy(*Lo nllxHd$k98gtEVQktsHPj`(_M6{Mp^!hi)fp{h`Le5@q%uuO`P| delta 614 zcmeCJ`Io_WnwOW00SH<|y0X4;OyrYboG?*6fst=whXG3^vnKz>L-~A+nv;$Aoq%L1 zzYU}7WM*sa$#?mM7~Lj+;Ll+6+#D%zgO$;JvXNLIcL30&A}tURIJr-(fiYw=zjzD} zW6)?+xxGQMJ|Mz(@;~Jk#UKz<0Z3@_6^Vh^VC`WbmNtm+n7mlUiZOWd zJr#e(l+AjoTbX5%i~y;S1`>Xn97Td4PUIv_FQ95~O(({T$$gr+Oa-iy?{Ua)meC4i zm8xXW6!W{qnwXQBoO+8nDL?5JYe{}~YF?4wuD{Qsc%9nlDvM%~5YP%urXo;0-(oH2KczG$)vl-&$YliLV$aFjZQZ#(^RO{0ffWJ(oerOh diff --git a/tests/selfverification/__pycache__/checks.cpython-312.pyc b/tests/selfverification/__pycache__/checks.cpython-312.pyc index 3c615c3a8e84ea49b9a4ffa70e4658a2dd07b4aa..b46525bbbaa393365f54924bab446e850737bfe8 100644 GIT binary patch delta 1594 zcmZWp-D@0G6u)=&BfFdZ%EzWjn#5~Tt4lY@qM#ugQ&O`5V?T_osBM?+WbWN$%TFvKr?N?|Lsb@+Ka@F&rd^f4rFo z5s&Z|9>OvD=q_*Njflk#VF?~aEROyW9GiF>kK)*j>lltLOP=JB@>^a)&Xw(cv(W$5 zuQwuXROYq+_;K_QLLc04PBt86x#tL3r7M)v4&30%9Ik<03nSu@h%@zATlMxn!6LO3 zN@mEavZ9QW4?UVPLo_AVfD|6?Tk(8KK2z3}Ye6rftGG+adEzD9KSR$WLS1x$Bpb@# zAYry5V2MH5V5PEUGff3tsti%CYc?|trfRkcOt-3LQMGjh?p^(eP4uVJ>1R?5gk=^h zf)9j344+e?V1i)wtZp%}s9Uz+DLh&~xu9CeSX^S}v?ZXTP8S8!4Q6S=P(e2>wg9?q z3xnCFOD7iRO$%M@VN9tilSJ=@SzViD!p(axH)~b}!I4?8k|>#Y0Si+WY#vNr)&$?< zs_0x8nheM>G2NIc>v&M6Pyn-p0W=dpxOWxiY2Z>UswLeJmc&X@$Gwc}cTF+Nblp{y0@HFC&BC5^`|`MIqmsI9 ziDIFODHc?{SOziRdP4#`qZV^D$RCId?TEs0G|Qd!($)2^=yE3bFk2pUYa}J*I>&3; z^@I=VT|cDSOf43ZN)?(Va}-U|3voG@t?7oU(TauLPGk>26HaPf+N{v#{!J^7wa@Qs z!Xb?=D!$ennyci`W)knde&J56_dfCZCg_@Hr!l%#c`viod1AZq#7?+%C((ZPdfWP? zkMryK&oAFeJiVP5+L&Go?F^6q9LsJ@Z8fKEPThQAJD%Q-Ww!&_-Js;z4Ux#`R^#ZF zcl59O9ul9RmJb7d9Ucq%H{XeVKz&2FEN`~NsqZ)p;=C5X7BGPeUJI%gHmR%uWN-(L zNHgLLB`*6War*Y=CyA0LJ}lAarE#4`owkl&Xx90=qnnO6M?1%SFXP^=O|?@fU60Tp zcoxEFlS)k)NW-9Wuj@4Z!5Qq%&|zn;dyIbXeA%6+XPijS1bxl9(9`F;i0F$X z>J7>KpP;Qy_}IBub8^S+yG}VvNh6yFWZ^*80i?hnwSgr10yB;6DW#N b&~ZuewKkmF$F#tM(=c^nOgM!bD8`>6lEMR|1yV#)c!9Jakk1FCg({gf#Y!7l7#RFZOLX(o zbdwVEQWVNki#Gn4!n@fmdyevT4X+XhN?AdERcC*@(tB@Y;hpL_{nF~WEkTnzf^Ooj|U3; z;>^h`E{QHKDT>u(D-r-H0jo0tvZ{DJOHzvxlX6nQ@|c=9oI|9~ivj1Y*B0ueeO0u&QPps+3i1!57%|3x4_ sf&;Ed7{n3;62CZXa`RJ4b5iY!(tun>ATC}#IZGp%`9l!nS*WNd(S=h-gEAG+%xZ-Imog6%0DLyfhTXe>g$Csa}_(dX8GFZZMDz7@j zrIE71vPk)0IrLr6bAuIX8epaB2K1>Oz$(=XSgoc5)~FePwdyLsIyDopUd;k*P_qFW z)f~VkH5ag1%>!INY4PQc=U6yyyKHFTp;}WjOnGL2zi#1e)FlnBDGUU)z^FfD=wZbS z=~4fLV)(T~N+cB3)EQN3ckn&*`HK$Xew|#m#*@~^e^dqcpt6KsFeJ`T{H9bj60HbH8e>9-Ic2(a8)vh z3rVC1{*sol$wpE?-)hG0hCW{!$wP`5(8nf|c2(2HutnJ9bQmF>2weyr2;B%f5q2Sb z8Ng>F`>>6Bk*5$e09u(>1M=<7+aN`u+dFeAFJJH!K(h}fc#*A_{&e-)x)x}Hi2X?U z8o~g=Zh)j!CrTi!84`44e29+<1wE1p{by$S3j{GwK|Y&d62%;kB!z)yf?JRSbadU8 zL>4wtn50z+m^v{oi|i=K5Dwr`kOqK0pNpLW2_aP+iNoQS)TQG{=B*?{|@ zY1jam{7i!uxl=j}lmI;{|60_cMSoS0Gqb;_$YMo9kz#tRw9Cq5Y@mNCEp(xY=7pmV zN=r&}u>S<Do3n=}(O`fYc9Dn#g{7zN`o= z`OC6a=@d?)|0#Qq*Js`Z!gBN_Ree=>8ozDcm6tCR21%BFpVW!Nl7E&%|&3JFyJQ@iQu=B@NrQ)c2Ej;8_q zllyda#zhDZ!0NF>~c-vAJXnJ9Ja( zvPd}Dyu4r~fn2Ea1YY0j4F)A{S6Ul6$>9& zIjLz*Z2W)yX2r(O?%QB}@+RyafOqk+arBP`wl*x6Ud8H62y+oW&P9Yc&#E;`*b))r zU?zc!2^c!~JPXptEpag}#jSB$%tABPWLZ=(pL$p4imJ3gu;$aT+FbbxCP8@E8hp$W z6Jt`$c1284M>1&t+CnjIH#4b!;~L&hFIHv=Bl+~6eRa(hKE)Jwm<6d8$Hg3rRA=YxXR9r^M+Y+2$f~q?BH>X3~F^IEtgZ>Im^uV8^)2 ztXP<}jI|P3c^AFbnpx(Ir^TEqpAT$tcPuUDKFGyAF^>^m#$QE0v}G2#VxA#hO+%g` zVL{DhTugxMv;{358d2<78y5yt_j!Q{=T$wbH|CueHT?@jEpimnpS8LY>4*4u8oZvk zdotaZG4=kQpa#J*OfzqzZ8GSlb%mJdA2K;L( zgx-Nkh(}F^(l!dyYasAsBcMeU64K=f5;9E<0#=u`Lu0z3u~1B~lDdwqw>>JeK$L@e zSPeyoWd#B^sF4aoR>n;ouGmmOjwoalVm)NYsVG_4QztM*A(NOCQp*u^B%eSq@Q4lD ze5z7K-$*)iNEtzGR3R!^jU3YmXm?gjO_iV!22*E@gbpgEwy?E6Bv&ZmP(Z67N09w7 ziM-F3lXT*TgbGtNGA{yc>|B@d;uIO-O8`l0KtZVq*3D!KLMy^ngf@hg<`{MZWGpG0 zQneqKSYC%iA|nP8pGII66#==U}3`G;ekn$!?@S`5h z;We6WevubmejZ~*rZ9;~9_uD0y^tE_ze{X84r^hs7a-@&sr{|k6? BoAUqw delta 2225 zcma)-Yiv|i5Xbl2-Fw?^+igps+rnaZ!*1!e&@C-ZBlJNj;3IWgDM4ADOYbFZX?M%H z3tECM;VrLF#-L)1HNj}(D`wTiC?U8VWaUQ2K^v0zW89A|7oSQkkC!~>zp}r z=FFLybMB3C@!lEX`LeLk?cg7kogRGLH|8l9Wn)XP?hp=BnC_t^!@*c^I1~%G4*JArjT2e2O406YLZ$Oz}i&5+?+ zMgfz0%Of1j(efsJm=#@_tK}71Y$1zLwl0EA7b9zojHWC*l{I91z_x6ehnXDUeg+5H z1Nl*4Bhbyr8c7-N8?tPj2Y&oyHPtVAShsl`%cK`+uEo1xmt&`7Jhgu~tMxWY>@DMr zTTOJ+Q$_E13+Ss#SGp8>6Xr&|FO`(`giB>dT9`m~WP}k7=SwdJ{lEaAR?a;;Dlt!D zA$F64z&6JM=5`is99kVl9jD*FPCv++-Sn>iQ<2EnfmKe!&k7l!OmMxSf{V~UL0|ri zs3;U#5@e6}EE)}kD%Pu-O_qz7WZiMwwq$A`nXqL5K4Q@3uNfKLIXP3$;r?IKAh**O zp#T^9mr#p-o@e*b@~V$H-Wyee&h%68@-%P=3tc@Oy}j}Yj8%*(4W}_2Wkkb8vlDy* za~|L%a0-~q>DUe}iLy<#BP11w+78FKThg`KCjBDM9io=nANUYsoohAPR#z%^(Ma7R zCgPoN&S;K3P~6cLCGb9nwlF3y&2#}oQl6zi#K-Aui>z?J$bJy6{R(Yu4$$SuQDJB9 zt8dVZUD)AS+T8G%F9)2{zYaUTbJcz0s{32k zz_J#=<06+caQPum_qaNYnxDgoa%Y`Lz5r(d)l6Fhg02-+xuZ-r#{Xq%7~TU zYVDU;lxl0$BhO*@JYzziy~9~dsq^V9DUa-KL34}#Dvw{H&CN%+kk^_|YUv9wRoyqA zrL4YZK;q%Ct-fT!R4o|Jn>;?|=Tbpkz6MKG!Pg;8pKnGq*vy}@=*Lx+`UDTIkaP7# z4(ihCotn5nf3&X2Ph+tk2vA2`P5LmzBfwGM7;v0%XMN^j>fWW1&tSECcJ^*xb9n}S zRQQ}x-{kF(=U8dG8UIyEJxxY?gZ?Hf-=fy`%OXKzofY)?%C1c7nij3^4fs*zQ<2>+ z-(8;1xrTlPQOw9EeZRIwf18ybP=4oeBf(O*fqc=B=%lsL#>x(apgMRJq!C8ejgO2e z2M$=0&P6v-ZkLa)M>kMIcYV4HR;ums*_8NPNgFr_oCokINZdy`4xqIpN-2>CiCAP1 zKrHG8-pXwmPMZ6NtTy=(>jeMVcnyDb3L$Q49^v}k?+{C_I~>R8=kCSs9HIT`0E)@m N)8_tdo*^R2+rRqk)0_YR diff --git a/tests/selfverification/__pycache__/test_self_verification.cpython-312-pytest-7.4.4.pyc b/tests/selfverification/__pycache__/test_self_verification.cpython-312-pytest-7.4.4.pyc index fee743c0a0ab273411dc5d95a745be441e0f53c4..f9e0cff4be238c5c0ec86fc087f802b378741586 100644 GIT binary patch delta 2242 zcmcIlTWl0n7@jjbyWMNsZM*H#3rs0(Gqh}XS+1&f2-udjDfPK-(ng4r9T8t@YD|sjspHACX^g5?+Q=j) zRWqrdQpeI6UCUh&4@#tp)t7y44?`}^x$n!qv5{`}splH$Vwb!vmHR-AOJ)XOR#;-U zyt~-4cU>;#J7*(N_HpG?9UZVl0|+4i#X&ct{1`yqHlE!`dsd_v-NrWin>^@`IuQcw z_i%gQ2@n-0|Uczb+`fOfD04!53sZHa4@K>f3Cw!pnZ>S`AMZs}8rY#i)z<0*cQ* zFHJcFVcMZNv@&+SrEkW0icH(!wogl0r{bD=ZTE(7I2@I!4qcqkwI2C?J3*2&#-yyu z!-#IQ zdOu3L5&97N5%wVL1u$_em?vJe3;yJrF>q3!&}lXah20~MS>zNw1a=tToCcT`es&5~ zEpP8x^oLIGotfQruKC>lB`JFS|NkvYQHtb9!M$>^A7xMOi>fb0;Ocx}XQLPNg|4jy zX~ACPDowE80<8|-Ixn_PDdi>a8orYE^p0nfs%gToqx4yH*T<~dPGIRfwO#gMQ1r2D zwU=bhQi&c!i{}71O(PLSq|c*jF!y@hP1|NZ&m*WPN5FaJ-39NkTTS``YE^`z012hM zFxzJKUPBWp$21@3X}aj&CY;81un{XcqNE|{0HzCo&$uvW7H-^W9|he2n`r!q4CF>b zs@PHF8k#?hYxk(;!bIYj$U<4c?%!EfRt!(OSd-jiH$XGU4$EgC_>%0BlwxCiu#~qC zr>SY>9j2Z#GFoX{n7?(s0}mUTWIJ1Hd|N=gmzNLo(s_ft^aR@QyeLYK>P_i@!+fI@ zG{XiRQ>_@^D<;zDe*Zv#Dt>D1v`>Qi2wT&3gAA}Gc~!2a{TWFa#ApweQ^>zMHr)CrNARtOab#iTICA4G>Uf43jPf7{x+AOYuYfuO z9H?i5u@MOHZRDodTkIY`^&S-^LFX|3Lp=YnDQ-WD0a;J%)hfO#MZQ_`Hq)X?JG&Zd zCB5v|Sk$o{UDUHk_uLHMjr^O&4}^WVf?uG#G^WzydC#~?)iK?o=_#Ebe-t-9#VtgU z$`qNvyG5}oiU|~s0!)Cn^saO3n4z5<)wk0(Ku6#_5klDdcw>c&h`$H`cMz6? z>_A*rmfeErJlk-(;W`O^MS^cOTy5L_HQ8}Xl*pz#HbLwq%P8KiVUzKG34{FwwLcE! delta 1568 zcmbVMO>7%Q6yDkO+Kyv8waH(TrdiXt>A1nM6YMlj(jOt3v{4d3EmUs7u{thI+Qi|l zL2{t{v{nz?(h&!Kq)3&JuzK_bByONy;D!*@A`bMzfeYnQ2`;=h>kwD87cBYPH*em& z_r0H)PpjXQ!{3EM0S$kv-5;((cq`n?0$an^hL|RN!Y|rH;4>rND|$q*=E?6Bp_-QO z6Jf-D(Xn-Rd}_~ac1Nf4nkFJO?T#yI`9TpyBTGDm7!#d{Pm3xpr6qW~Dc}pPiEN=%AG(U1z0aR|>_8l`<4o?DExOWu?3hXK*VkMf58+-^lm; zru)ZGQD>WzGLasXi;>`A9=K-B%qSKU+=h1CcO%8f(%ek~7UXtcyj`(VR*G1Bhu8~r zkj^;m$2Y14H@R_miPR@$U;hY}ZMJ{ZETH13e7paX!FiOpr>=447z^o)`$=;hSE)1< z8<~CgjU&VtUM2_;Pz{`b>m2j5;T6*63C<%d@~~SzODOZskm`f$d(SvZ0bVy zGFs!1=*g7B-m>YcTNak+i_{`3$cL%red^jRkWb2$H!sw`X$`;l4ygnGC$%&Hp0xuA z&F0_-ga4jRoA09Xyo`-K`0sR({o|&i-kc7#PbD9Yu9E*poBvB@zxe^GOSiJu(f5t) zef{`u$<+8gDZnDUPoW><^z+FH^9Js%%3mjMM%7i+)eUhBy8=8etGPHkCbx6r-WhVS zSKiCjy3~WDUVzi2q}K%s@{OsU6LdF#Mii*MK*aze4HN{H2`C8ATlJTJdaW$pyjnU6 mTexKSw?CFYObvDTnf_2ictG$+xAab1`k#B`()65%PX7ao$5CYf diff --git a/tests/selfverification/checks.py b/tests/selfverification/checks.py index 47d321e..2329882 100644 --- a/tests/selfverification/checks.py +++ b/tests/selfverification/checks.py @@ -94,6 +94,23 @@ def check_actor_isolation( return violations +def check_isolation_was_examined(pack: Mapping[str, Any]) -> list[str]: + """Every run must carry a verdict on actor isolation — F-0003, resolved. + + Before this existed, isolation was only observable in scenarios written to + expose it: a run in which every actor shared one memory store produced + evidence indistinguishable from a correct one. Actors now carry an automatic + private marker and the runner examines them on every scenario, so the absence + of this observation is itself a failure. + """ + for obs in _observations(pack): + if obs["kind"] != "actor_isolation": + continue + violations = obs["data"].get("violations") or [] + return [f"actor isolation violated: {v}" for v in violations] + return ["this run did not examine actor isolation at all"] + + # --- td://self/oracle-independence --------------------------------------- diff --git a/tests/selfverification/test_checks_can_fail.py b/tests/selfverification/test_checks_can_fail.py index c727c0d..b131778 100644 --- a/tests/selfverification/test_checks_can_fail.py +++ b/tests/selfverification/test_checks_can_fail.py @@ -30,6 +30,7 @@ from tests.selfverification.checks import ( check_actor_isolation, check_evidence_supports_every_verdict, check_intent_independence, + check_isolation_was_examined, check_no_actor_collected_judgment, check_runs_agree, check_verdicts_follow_from_judgment_evidence, @@ -186,3 +187,35 @@ def test_unrecorded_provenance_is_caught(): tampered = copy.deepcopy(pack) tampered["provenance_index"] = {} assert check_intent_independence(tampered) + + +# --- F-0003 resolution: isolation observed on every run ------------------- + + +def test_an_unexamined_run_is_caught(): + """A run that never looked at isolation must not read as isolated.""" + pack, _, _ = run_and_serialize() + tampered = copy.deepcopy(pack) + tampered["observations"] = [ + o for o in tampered["observations"] if o["kind"] != "actor_isolation" + ] + assert check_isolation_was_examined(tampered) + + +def test_a_leak_is_caught_without_the_test_planting_anything(): + """The regression F-0003 leaves behind. + + No secrets seeded by the harness, no scenario written to expose isolation. + An actor holding another's automatic marker is caught by the ordinary run. + """ + from testdriver import Oracle, Runner + from scenarios.alice_bob_carol import build + + world, driver, observer, asset, oracle = build() + world.cast["bob"].remember("overheard", world.cast["alice"].canary) + result = Runner(world, driver, observer, oracle).run(asset) + pack = json.loads(result.evidence.to_json()) + + violations = check_isolation_was_examined(pack) + assert violations + assert "holds the private marker of 'alice'" in violations[0] diff --git a/tests/selfverification/test_self_verification.py b/tests/selfverification/test_self_verification.py index 506eb79..3b06899 100644 --- a/tests/selfverification/test_self_verification.py +++ b/tests/selfverification/test_self_verification.py @@ -17,6 +17,7 @@ from tests.selfverification.checks import ( check_actor_isolation, check_evidence_supports_every_verdict, check_intent_independence, + check_isolation_was_examined, check_no_actor_collected_judgment, check_runs_agree, check_verdicts_follow_from_judgment_evidence, @@ -56,6 +57,14 @@ def test_actor_isolation(): assert check_actor_isolation(pack, SECRETS, memories) == [] +@pytest.mark.parametrize("mutations", [(), ("M15",), ("M01",)]) +def test_every_run_examines_isolation(mutations): + """F-0003 resolved: isolation is now a property of runs, not of scenarios + written to expose it. No canaries need be planted by the test.""" + pack, _, _ = run_and_serialize(*mutations) + assert check_isolation_was_examined(pack) == [] + + # --- td://self/oracle-independence --------------------------------------- diff --git a/tests/test_audit_core_e2_use_case.py b/tests/test_audit_core_e2_use_case.py new file mode 100644 index 0000000..7079f4c --- /dev/null +++ b/tests/test_audit_core_e2_use_case.py @@ -0,0 +1,136 @@ +"""The audit-core use case is durable intent, even before drivers can run it.""" + +from __future__ import annotations + +from copy import deepcopy + +from testdriver import Oracle, Provenance, Verdict +from usecases.audit_core_e2_tenant_boundary import ( + PHASE_CONTRACTS, + PRECEDENT_EVIDENCE_REF, + ROLE_CONTRACTS, + TEST_USE_CASE, +) + + +def passing_observations() -> dict[str, object]: + absent = { + "status": 404, + "schema": ("$", "$.error:str"), + "digest": "absent-surface", + "fixture_match_count": 0, + } + return { + "event_by_id": { + "owner": {"status": 200, "fixture_match_count": 2}, + "attacker": dict(absent), + "absent": dict(absent), + }, + "correlation_slice": { + "owner": {"status": 200, "fixture_match_count": 2}, + "attacker": {"status": 200, "fixture_match_count": 0}, + }, + "append_as_b": { + "attacker": {"status": 400}, + "state_after": dict(absent), + "absent": dict(absent), + }, + "run": { + "attempted_operations": 10, + "maximum_operations": 30, + "max_concurrency": 1, + "rate_per_minute": 10, + "target_revision_matches": True, + "attribution_complete": True, + }, + "cleanup": { + "completed_before_expiry": True, + "runner_absent": True, + "temporary_identities": [], + "kv_paths": [], + "projection_resources": [], + "mounted_secrets": [], + "target_ready": True, + "secret_values_observed": False, + }, + "report": { + "bound_to_projection_receipt": True, + "bound_to_cleanup_receipt": True, + "bounded_assurance_statement": True, + "delivered_to_risk_nexus": True, + }, + } + + +def judgments(observations): + oracle = Oracle() + assertions = (*TEST_USE_CASE.claims, *TEST_USE_CASE.invariants) + return { + assertion.id: oracle.judge( + assertion, + observations, + getattr(assertion, "after_step", None), + ) + for assertion in assertions + } + + +def test_precedent_calibration_satisfies_every_assertion(): + result = judgments(passing_observations()) + assert {judgment.verdict for judgment in result.values()} == {Verdict.PASS} + assert PRECEDENT_EVIDENCE_REF.endswith( + "AUDIT-WP-0008-T05-whitehat-e2-03-pass-2026-08-22.md" + ) + + +def test_cross_tenant_event_disclosure_fails_the_claim(): + observations = passing_observations() + observations["event_by_id"]["attacker"] = { + "status": 200, + "schema": ("$", "$.event_id:str"), + "digest": "tenant-b-event", + "fixture_match_count": 2, + } + result = judgments(observations) + assert result["c-event-id-tenant-scoped"].verdict is Verdict.FAIL + + +def test_missing_cleanup_evidence_is_inconclusive(): + observations = passing_observations() + del observations["cleanup"] + result = judgments(observations) + assert result["c-receipt-bound-cleanup"].verdict is Verdict.INCONCLUSIVE + assert result["i-no-secret-evidence"].verdict is Verdict.INCONCLUSIVE + + +def test_cleanup_residue_fails_closed(): + observations = deepcopy(passing_observations()) + observations["cleanup"]["temporary_identities"] = ["unexpected-residue"] + result = judgments(observations) + assert result["c-receipt-bound-cleanup"].verdict is Verdict.FAIL + + +def test_intent_has_independent_provenance_and_separate_roles(): + assert TEST_USE_CASE.provenance is Provenance.SPEC + assert all( + assertion.provenance is Provenance.SPEC + for assertion in (*TEST_USE_CASE.claims, *TEST_USE_CASE.invariants) + ) + roles = {role.id for role in ROLE_CONTRACTS} + assert { + "authorizer", + "target-owner", + "credential-custodian", + "security-coordinator", + "cluster-executor", + "tenant-a-attacker", + "tenant-b-control", + "independent-observer", + } == roles + + +def test_schedule_requires_cleanup_before_finalization(): + phases = {phase.id: phase for phase in PHASE_CONTRACTS} + assert phases["finalize-and-deliver"].requires == ("cleanup-custody",) + assert phases["cleanup-custody"].requires == ("delete-runner",) + assert phases["run-probes"].requires == ("ready-runner",) diff --git a/tests/test_classification.py b/tests/test_classification.py index 37d5d2f..ccf6c39 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -194,3 +194,98 @@ def test_safe_to_accept_is_a_closed_set(): assert SAFE_TO_ACCEPT == { Classification.UNCHANGED, Classification.MECHANICAL_ADAPTATION, } + + +# --- classifications that no lab mutation happens to produce --------------- +# +# Two outcomes were declared at T08 and exercised by nothing. Left that way they +# are decoration: code that has never run is code nobody has checked. Rather than +# delete meaningful outcomes or trust them untested, both are given a case. + + +def test_intent_change_is_detected_when_the_claim_set_moves(baseline): + """`INTENT_CHANGED` is a fact about the recorded use case, not an inference. + + It fires because a human edited what is being asserted — which is why it is + detectable at all, where `SEMANTIC_CHANGE` was not (F-0006). + """ + import copy + + altered = copy.deepcopy(baseline) + altered["provenance_index"]["c-newly-added-claim"] = "human" + outcome = classify(baseline, altered) + assert outcome.classification is Classification.INTENT_CHANGED + assert not outcome.safe_to_accept + + +def test_realization_failure_is_distinguishable_from_ambiguity(): + """`REALIZATION_FAILED` says "we could not act"; `AMBIGUOUS` says "we do not know". + + Every lab mutation that breaks realization also strands a claim, so the + catalogue only ever produces `AMBIGUOUS`. This builds the case the catalogue + cannot: a step that fails while every assertion in the run still holds and + none of them depended on it. + + Note that a run asserting *nothing at all* is `AMBIGUOUS`, not + `REALIZATION_FAILED` — a use case with no claims cannot conclude anything, + however well its steps ran. + """ + from testdriver import ( + Actor, Cast, Invariant, Oracle, Runner, Scenario, SemanticAction, + StateObserver, Step, UseCase, VerificationAsset, World, + ) + from testdriver.agentic import DiscoveryRuntime + from testdriver.browser import BrowserDriver + from testdriver.observers import Watch + from testdriver.provenance import Provenance + from lab.mutations import ObservationChannel + + use_case = UseCase( + "uc-audit-only", "Sharing leaves an ordered audit trail", + "Alice shares R with Bob; the audit trail stays ordered.", + Provenance.HUMAN, + invariants=(Invariant( + "i-audit-ordered", "The audit trail is append-only", Provenance.HUMAN, + lambda obs: [e["sequence"] for e in obs["audit:R"]] + == sorted(e["sequence"] for e in obs["audit:R"]), + ),), + ) + + def run(*mutations): + with journey_lab_server(*mutations) as (app, tokens, base_url): + app.request(tokens["alice"], "create_resource", + resource_id="R", content="x") + cast = Cast() + cast.add(Actor("alice", "Alice", credentials={"token": tokens["alice"]})) + scenario = Scenario( + "sc-audit-only", use_case, + watches=(Watch("bob", "R"),), + steps=(Step("s1", "alice", SemanticAction( + "grant_access", {"subject_id": "bob", "permission": "READ"}, + permitted_surfaces=frozenset({"browser"}), + )),), + ) + driver = BrowserDriver(base_url, tokens, DiscoveryRuntime(), "R") + observer = StateObserver(ObservationChannel(app), scenario.watches) + world = World("w-audit", app, app.version, cast=cast) + return json.loads( + Runner(world, driver, observer, Oracle()) + .run(VerificationAsset("va-audit-only", scenario)) + .evidence.to_json() + ) + + outcome = classify(run(), run("M23")) # the control is gone from the UI + assert outcome.classification is Classification.REALIZATION_FAILED + assert not outcome.safe_to_accept + + +def test_no_classification_is_unreachable(): + """Every declared outcome must be produced somewhere in this suite. + + An outcome nothing can emit is the same kind of dead promise `SUSPICIOUS` + was before T10 removed it. + """ + exercised = set(EXPECTED.values()) | { + Classification.INTENT_CHANGED, Classification.REALIZATION_FAILED, + } + assert exercised == set(Classification) diff --git a/tests/test_reference_scenario.py b/tests/test_reference_scenario.py index 5739969..70554d5 100644 --- a/tests/test_reference_scenario.py +++ b/tests/test_reference_scenario.py @@ -67,4 +67,18 @@ def test_actors_hold_isolated_credentials_and_memory(): assert alice.credentials["token"] != bob.credentials["token"] alice.remember("secret", "only alice knows this") assert bob.recall("secret") is None - assert bob.known_keys() == () + # Every actor carries its own automatic private marker (F-0003) and nothing + # else it was not given. + assert bob.known_keys() == ("__canary__",) + assert alice.canary != bob.canary + + +def test_every_run_records_a_verdict_on_isolation(): + """F-0003: isolation is examined on every scenario, not only on ones + written to expose it.""" + result, _ = run_once() + examined = [ + obs for obs in result.evidence.observations if obs.kind == "actor_isolation" + ] + assert len(examined) == 1 + assert examined[0].data["violations"] == [] diff --git a/usecases/__init__.py b/usecases/__init__.py new file mode 100644 index 0000000..1e646be --- /dev/null +++ b/usecases/__init__.py @@ -0,0 +1,5 @@ +"""Durable test intent, kept separate from concrete scenario realizations.""" + +from .audit_core_e2_tenant_boundary import TEST_USE_CASE + +__all__ = ["TEST_USE_CASE"] diff --git a/usecases/__pycache__/__init__.cpython-312.pyc b/usecases/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df4655cd3be8d46e5217330b4d3752e17aade148 GIT binary patch literal 329 zcmXv~u}T9$5Z%3tBt%l#MT9ME0wxDm79xlSD{T^+G{a_hG^=;JVRlbRI@|n!Utr<4 z*w~s%5DPmYT`KoPr?@W?b9G0k<7^CB4~ zoaywMkJ4GKvoSwAElG}Z6ajq@#s;)uPu$`(2#-@qGN8C3R|w^-B8h{$L1~gFc)b5BikG?nS4ztOIDL0G2zl)Fyy(gn;`H<)jTTcBR#l zX20{*u*`FSXSS;DSa-9DD=@ov(CLT@@-Hx3J@%N&oW edQj!dZ^iVTa?an_`6oMg-)z4&w;$@%i2Wa$a$qe0 literal 0 HcmV?d00001 diff --git a/usecases/__pycache__/audit_core_e2_tenant_boundary.cpython-312.pyc b/usecases/__pycache__/audit_core_e2_tenant_boundary.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c790cb41edff0aefd19b70b65b5d19387db472c GIT binary patch literal 10754 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 bool: + """Compare only the sanitized non-disclosure surface.""" + + return ( + left["status"] == right["status"] + and left["schema"] == right["schema"] + and left["digest"] == right["digest"] + and left["fixture_match_count"] == 0 + and right["fixture_match_count"] == 0 + ) + + +def _event_id_is_tenant_scoped(obs: Mapping[str, object]) -> bool: + probe = obs["event_by_id"] + owner = probe["owner"] + attacker = probe["attacker"] + absent = probe["absent"] + return ( + owner["status"] == 200 + and owner["fixture_match_count"] > 0 + and _same_absence_surface(attacker, absent) + ) + + +def _correlation_slice_is_tenant_scoped(obs: Mapping[str, object]) -> bool: + probe = obs["correlation_slice"] + return ( + probe["owner"]["status"] == 200 + and probe["owner"]["fixture_match_count"] > 0 + and probe["attacker"]["status"] == 200 + and probe["attacker"]["fixture_match_count"] == 0 + ) + + +def _cross_tenant_append_is_refused(obs: Mapping[str, object]) -> bool: + probe = obs["append_as_b"] + return ( + probe["attacker"]["status"] in {400, 403, 404} + and _same_absence_surface(probe["state_after"], probe["absent"]) + ) + + +def _cleanup_is_complete(obs: Mapping[str, object]) -> bool: + cleanup = obs["cleanup"] + return ( + cleanup["completed_before_expiry"] is True + and cleanup["runner_absent"] is True + and cleanup["temporary_identities"] == [] + and cleanup["kv_paths"] == [] + and cleanup["projection_resources"] == [] + and cleanup["mounted_secrets"] == [] + and cleanup["target_ready"] is True + ) + + +def _execution_remains_bounded(obs: Mapping[str, object]) -> bool: + run = obs["run"] + return ( + run["attempted_operations"] <= run["maximum_operations"] + and run["max_concurrency"] == 1 + and run["rate_per_minute"] <= 10 + and run["target_revision_matches"] is True + and run["attribution_complete"] is True + ) + + +def _secret_values_do_not_enter_evidence(obs: Mapping[str, object]) -> bool: + return obs["cleanup"]["secret_values_observed"] is False + + +def _report_preserves_assurance_limit(obs: Mapping[str, object]) -> bool: + report = obs["report"] + return ( + report["bound_to_projection_receipt"] is True + and report["bound_to_cleanup_receipt"] is True + and report["bounded_assurance_statement"] is True + and report["delivered_to_risk_nexus"] is True + ) + + +TEST_USE_CASE = UseCase( + id="uc-audit-core-e2-tenant-boundary", + title="Demonstrate audit-core's E2 tenant boundary with separate drivers", + narrative=( + "Within one explicitly authorized production window, independent target, " + "custody, security, cluster, attacker, control and observer roles establish " + "that an ordinary tenant-A identity cannot read or create tenant-B audit " + "fixtures. The run must remain bounded, preserve non-disclosure semantics, " + "clean every temporary resource before expiry, and deliver a cleanup-bound " + "sanitized report without retaining credential values." + ), + provenance=Provenance.SPEC, + source_ref=SPEC_REF, + claims=( + Claim( + "c-event-id-tenant-scoped", + "Tenant A cannot distinguish tenant B's event id from an absent event", + Provenance.SPEC, + _event_id_is_tenant_scoped, + after_step="run-probes", + source_ref=ENGAGEMENT_REF, + ), + Claim( + "c-correlation-slice-tenant-scoped", + "Tenant A's correlation slice contains no tenant-B fixture", + Provenance.SPEC, + _correlation_slice_is_tenant_scoped, + after_step="run-probes", + source_ref=ENGAGEMENT_REF, + ), + Claim( + "c-cross-tenant-append-refused", + "Tenant A cannot append an audit event attributed to tenant B", + Provenance.SPEC, + _cross_tenant_append_is_refused, + after_step="run-probes", + source_ref=ENGAGEMENT_REF, + ), + Claim( + "c-receipt-bound-cleanup", + "Every engagement-specific runtime and custody resource is absent " + "before expiry while the target remains ready", + Provenance.SPEC, + _cleanup_is_complete, + after_step="cleanup-custody", + source_ref=SPEC_REF, + ), + ), + invariants=( + Invariant( + "i-bounded-execution", + "The run stays within its operation, rate, concurrency, revision and " + "attribution boundaries", + Provenance.SPEC, + _execution_remains_bounded, + source_ref=ENGAGEMENT_REF, + ), + Invariant( + "i-no-secret-evidence", + "Credential values never enter retained observations or reports", + Provenance.SPEC, + _secret_values_do_not_enter_evidence, + source_ref=SPEC_REF, + ), + Invariant( + "i-bounded-assurance-report", + "Delivery is receipt-bound and says only that attempted attacks failed", + Provenance.SPEC, + _report_preserves_assurance_limit, + source_ref=SPEC_REF, + ), + ), +) + +# Alias retained for the existing scenario-module convention. +USE_CASE = TEST_USE_CASE + diff --git a/workplans/TD-WP-0002-vertical-spike-crystallization.md b/workplans/TD-WP-0002-vertical-spike-crystallization.md index dece354..36317cd 100644 --- a/workplans/TD-WP-0002-vertical-spike-crystallization.md +++ b/workplans/TD-WP-0002-vertical-spike-crystallization.md @@ -4,11 +4,11 @@ type: workplan title: "Vertical spike: falsify the crystallization thesis" domain: infotech repo: test-driver -status: active +status: finished owner: codex topic_slug: custodian created: "2026-08-22" -updated: "2026-08-22" +updated: "2026-08-23" state_hub_workstream_id: "68b252b8-03e7-55c7-8dec-ac13f6ca0aa1" --- @@ -461,7 +461,7 @@ to execute than the agentic ancestor. ```task id: TD-WP-0002-T10 -status: todo +status: done priority: medium state_hub_task_id: "884c2e91-b56d-5b5e-a2d3-52d2a2768cae" ``` @@ -478,3 +478,42 @@ retaining them, and update the Concept ↔ Implementation Fitness Map. Convert at least one framework finding discovered during T04–T09 into a permanent self-verification or deterministic regression — that closes the first genuine Concept–Implementation Fitness Loop. + +**Done 2026-08-23.** Full write-up: +`history/2026-08-23-td-wp-0002-gate-review.md`. 178 tests pass. + +**All four gate criteria met.** M01/M02 absorbed with the claim set unchanged; +the authorization defects escalated with regressions named; one asset +crystallized and running with no model; **False Adaptation Rate 0/7**, with 12 of +13 mechanical mutations absorbed so the safety result is not bought by escalating +everything. + +**Fitness loop closed via F-0003.** Actor isolation left no trace in ordinary +evidence — it was a property of scenarios written to expose it. Every actor is +now seeded with an automatic private marker and the runner examines all actors on +every scenario, with two permanent regressions behind it, one of which catches a +leak with the harness planting nothing. + +**Compression — six abstractions removed**, each declared and never used: +`Verdict.SUSPICIOUS` (a verdict no oracle could emit), `Step.expect_refusal`, +`ActorIsolationError`, `World.seed`, `EvidencePack.latest()`, +`Trajectory.method`. + +**F-0008 — Temperature may be redundant.** Crystallization was built end to end +without it ever being consulted; what triggered freezing was *measured stability +of realization*, which is observed rather than declared. A declared temperature +is a claim nobody verifies. Not deleted — two unevidenced cases for keeping it +are recorded — but gated, along with `energy.py`: if the next workplan closes +without a decision consulting either, they go. + +**Two classifications had never run.** `INTENT_CHANGED` and `REALIZATION_FAILED` +were declared at T08 and produced by no test. Both now have purpose-built cases, +and `test_no_classification_is_unreachable` fails if a seventh is added without +one. + +**What this leaves.** The thesis survived contact with its own evidence in +narrowed form, and the demonstration is architectural rather than statistical. +The highest-value next step is a **bounded live-model experiment** — F-0005 and +F-0007 converge on it independently, and one experiment settles both whether a +model adds capability and whether crystallization has a product rationale. That +should come before any further framework feature.