feat(conformance): deterministic conformance coverage model (024) - #375
Merged
Conversation
…ity lane Lands the buildx setup + verification step BEFORE the runner starts depending on it, so the runner change is never the thing that reddens the lane. Declarative cases with a `build` operation need BuildKit. The GitHub runner ships buildx today, so this is mostly an assertion — which is the point. The alternative was an outcome-conditional assertion inside the case data, tolerating "succeeded OR failed" because BuildKit's presence was unknown, and that shape was nearly vacuous: every one of those invocations passes `--output-format json`, and deacon's JSON ERROR document also contains `"outcome"`, so the assertion held either way. Establishing the precondition here — and checking it in the harness for local runs, which follows — dissolves the need for that assertion shape entirely rather than arguing about how to express it. The verify step bootstraps the builder and fails loud, so a missing builder cannot surface later as a `build` case reporting a divergence that is really a missing builder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017UCGzJEFZd85HJqz1Wx2kE
…s (024 T001-T006) Adds empty module stubs for the scenario/obligation/coverage-report/regression machinery, wires the dev-only `coverage generate|check|report|scaffold` command group into the conformance CLI (dispatching to todo!() stubs), and reserves the V26-V30 rows in the RULES.md violation-class index ahead of the sections later tasks add. Confirmed cargo run -p deacon-conformance -- validate/certify both exit 0 on the pre-change baseline (T001). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Mechanical move only: conformance/registry/cases.json becomes conformance/registry/cases/<area>.json, one file per area, where an area is the `area` of each case's first linked behavior — the same per-area layout `behaviors/<area>.json` already uses (024 T007, research Decision 7). No record content changes: the 88 records are byte-identical, and concatenating the ten new files and sorting by id reproduces the original record list exactly. Per-area counts: build 1, exec 4, host-ca 1, observable-state 9, ports 1, profiles 1, read-configuration 65, secrets 1, trust 1, up 4 = 88. The loader change that reads the new layout lands in the next commit (T008), together with the count guard test (T009), so this commit stays a reviewable pure move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
… guard T008: `Registry::load` reads every `conformance/registry/cases/*.json` instead of the single `cases.json`. `load_case_files` walks the directory in sorted-filename order and keeps each record's origin file plus its index WITHIN that file, so the load-time shape and allowed-difference diagnostics still point at a reviewable `cases/<area>.json:records[i]` rather than an index into a concatenation. The concatenated set is then stable-sorted by case id, making the aggregate ordering a property of the data rather than of how the areas happen to be named or split — which is exactly the ordering the single id-sorted `cases.json` had. T009: `every_migrated_case_survives_the_per_area_split` in `registry_valid.rs` asserts the loaded count is exactly 88, that the set is id-sorted across files, and that no two records claim the same id. The constant is a deliberate-edit gate, not a floor: the split's one real risk is silently dropping records, and a smaller denominator is invisible in every downstream count. The 24 fixture registries under `fixtures/conformance/` move to the same layout, and the three test helpers that synthesize or mutate a registry follow. validate and certify both still exit 0; `make test-nextest-fast` is green (3587 passed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
T010: `TestCase` gains `scenarioContext`, an ordered map from each `sdim-` scenario dimension to the value this case exercises. It is a SIBLING of `context`, not a replacement: `context` stays the environment selector answering "where can this evidence be gathered?", while `scenarioContext` answers "what does this case exercise?". They are separate namespaces because `applies_in_profile` treats a condition on an unassigned environment dimension as unsatisfied, so folding scenario dimensions into the environment model would silently shrink the very denominator this feature exists to expose (research Decision 1). The field only has to round-trip here; well-formedness of the assignment is V26/V16 in US1. T011: `scenarioContext` joins the case-hash allow-list, so changing what a case exercises re-records and re-reviews its snapshot rather than silently carrying the old evidence forward. Key order is insignificant — `canonicalize` sorts it — so reformatting an assignment never re-records. The key is omitted when the assignment is empty. That is the one conditional in the input document and it is deliberate: an empty map declares nothing, the field is `skip_serializing_if` empty so "absent" and "present but empty" are the same record, and hashing it would have marked every pre-existing snapshot stale without any input having changed. Staleness must mean the evidence-determining inputs drifted, never that the hash function grew a field. `snapshot check` still reports 1 fresh, 0 stale. validate and certify both still exit 0; 451/451 conformance tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
T012: `behavior_obligation_id` and `combination_obligation_id` derive an obligation's id from what it IS, following the `clu-` clause precedent — `obl-bhv-<hash8>` over `behavior ‖ canonical(context)` and `obl-cmb-<hash8>` over `operation ‖ canonical(sorted assignment)`. Reordering records, renaming a file, or moving a dimension's declaration position leaves an id alone, so a cosmetic edit never orphans a hand-authored disposition; changing what a combination is does change it, which is a new obligation needing its own decision. `hash8` is local rather than the private helpers in `inventory.rs` / `clause.rs`: those hash a schema pointer and a prose excerpt, so coupling to either would tie obligation identity to an unrelated record's field set. Only the truncation convention and the `0x1f` separator are shared. `canonical_context` sorts both the conditions and each condition's value subset — a context is a conjunction and a condition pins a set, so neither order carries meaning. `combination_obligation_id` takes any iterable of pairs and sorts internally, so no caller can opt out of key-order invariance by picking an order-preserving container. T013: `assignment_key_order_does_not_change_the_combination_id` builds its two assignments from `IndexMap`, which preserves insertion order — a function that failed to sort internally forks visibly there, whereas a `BTreeMap` would have sorted on the caller's behalf and made the test vacuous. It also asserts the fixtures genuinely iterate differently before comparing, and re-checks agreement through a `Vec` in a third order and a `BTreeMap`. Five further tests cover substance tracking, separator injectivity (a key/value split and a pair boundary must both be visible to the hash), stability under cosmetic context reordering, and disjointness of the two id spaces. validate and certify both still exit 0; 457/457 conformance tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ation T014, documentation only. Extends the standing hermetic-conformance convention block in .config/nextest.toml with this feature's binaries, forward-referencing files later tasks create — the same style the block already uses — so the classification is decided once rather than re-litigated per binary. Three groups: the five conformance-crate coverage guards carry NO override and run in every profile; `injection_faults` and `prereq_faults` are still hermetic but take an `fs-heavy` override mirroring `observation_faults`, which bounds their fixture-tree I/O without admitting a daemon; and `parity_conformance_docker` is the ONLY live binary, needing per-profile overrides in all six profiles when T020/T021 wire it, or the non-parity lanes stop being truthful by non-selection. No real overrides are added here. Also records that the `coverage` command group is a `deacon-conformance` bin surface, never a shipped `deacon` subcommand. All six profiles still parse; fmt, clippy, validate and certify are green, and `make test-nextest-fast` passes 3593 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…wn off the executor Two defects the resource-group split (024 Block B) would otherwise amplify. **FR-077b — a per-case bound.** `run_case` now runs under a 5-minute `tokio::time::timeout` and reports expiry as `HarnessError::CaseTimeout` naming the case id. nextest's `slow-timeout` is per TEST FUNCTION, so a driver that owns a whole resource group can only report "the group was slow" — indistinguishable from a wedged daemon, and naming nothing to fix. The bound is also strictly wider than the per-invocation bound in `exec`: a case that hangs BETWEEN invocations (a teardown that never returns, a snapshot probe against an unresponsive daemon) is invisible to that one. Abandoning the future drops it, which runs the workspace's RAII guard, so a timed-out case still reclaims its container, network, volume and temp directory. **Principle V — no blocking work on the async executor.** Three call paths reachable from the (about to become concurrent) Docker driver still ran blocking `std::process::Command::output` inline: - `execute_ops` created the isolated temp workspace and recursively copied every fixture tree into it inline; now offloaded, with the guard dropping on the blocking pool when a fixture is missing. - three of the four oracle-type paths (`spec-expectation`, `snapshot`, `invariant-metamorphic`) held the workspace guard to end of scope, so `deacon down` plus several `docker` calls ran on the executor at drop. - `live_differential` offloaded deacon's teardown but not the reference's. All now go through one `runner::release_workspace`, which is the pattern `live_differential` already established for deacon's side. Reclamation happens as soon as the last observer has read what it needs — every Docker observer reads the pre-fetched `container_inspect`, never a live container, so nothing captured afterwards depends on the resources still existing. The error path is deliberately left to `Drop`: a leaked container is worse than a stalled runtime on a path that is already failing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ce group The declarative runner was ONE `#[tokio::test]` iterating every case serially, which makes three requirements unreachable (research Decision 4): `resourceGroup` is declared data on 12 real cases and nextest cannot act on it (grouping is per test binary/function); a per-case bound cannot be attributed; and a tier budget needs concurrency, which needs to know which cases may share a daemon. The case set is now partitioned by `ResourceGroup`, one driver function per variant, across two binaries. `parity_conformance_runner` keeps the groups that need no container runtime — `none` (66 cases) and `fs-heavy`, which per its own model definition is significant *filesystem* work, not Docker. The new `parity_conformance_docker` owns `docker-shared` (12 cases) and `docker-exclusive`. Both call one shared `parity_harness::driver`, so the binaries are thin and there is no second copy of the reporting logic. SC-013 survives: `ResourceGroup` is a closed set and every variant now has a driver, so adding a case with an existing group stays a pure data edit. Only a genuinely new group would need a new function — a deliberate infrastructure change, not case authoring. A hermetic unit test asserts the two binaries partition the enum, so a fifth variant added with no driver fails rather than being silently driven by nobody. **Bounded concurrency (FR-077, Principle V).** `drive_group` runs cases through a `JoinSet` bounded by a semaphore, mirroring the same-named nextest group: `docker-shared` at 4 (each case has its own isolated temp workspace, so its `devcontainer.local_folder` label and therefore its container/network/volume names are unique), `docker-exclusive` at 1. The config-only groups stay at 1 — no requirement asks for concurrency there, and perturbing a green lane as a side effect of a reshape is not a speed-up worth having. Results are collected in completion order and then id-sorted, so the emitted verdict report stays byte-stable. **Tier budget (FR-077a, research Decision 10).** The Docker binary measures its own wall clock and asserts the 30-minute budget EXPLICITLY, reporting the elapsed number, the groups it spans, and the slowest cases. nextest's `slow-timeout` is set above it (35m) as a backstop only: a `slow-timeout` failure says "the binary was slow", which names nothing to fix. The tier spans sibling nextest processes, so each group records its span into a per-run artifact tree keyed by `NEXTEST_RUN_ID` — the fold takes `max(finished) - min(started)`, the true wall clock rather than the sum, and a stale artifact from an earlier run cannot widen it. A process that runs before its siblings sees a partial fold, which can only UNDER-report. **Registry truth (T020).** `registry.json` recorded `parity_conformance_runner` as `docker_required: false` while 12 of its cases drove Docker. The split makes the claim true rather than papering over it, and registers `parity_conformance_docker` with `docker_required: true`. Baseline enumeration gains an explicit `POST_FREEZE_PROGRAMS` allow-list: the baseline records a pre-migration world, so a binary created after the freeze contributes zero units. It is an allow-list, not a fallthrough — a typo'd registry name is still `UnknownProgram`. A per-case timeout is recorded as that case's failure and the group keeps going; every other `HarnessError` aborts the group fail-loud, because those recur for every case and continuing would restate one environmental fault N times. An empty group is not a silent skip: it still writes the fragment that proves the binary ran, and says so on stderr. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…command boundary Two structural checks in `parity_registry_check`, hermetic and selected in every lane, so both facts fail on the PR that breaks them rather than in the parity lane weeks later. **The Docker driver is wired in all three places.** `check_nextest_profiles` already enforces registry/profile agreement generically, so why name one binary? Because this is the binary whose absence is plausible. It was split out of `parity_conformance_runner` precisely so the Docker-backed resource groups could be scheduled and budgeted separately, and a half-landed split — source file present, registry entry or profile filter missing — reads as "the Docker cases are covered" while nothing selects them under `--profile parity`, or while a hermetic lane silently picks up a binary that needs a daemon. The generic check reports that as one line in a list; this reports it as what it is. It also asserts the SIBLING's claim: if `parity_conformance_runner` were still `docker_required`, the split bought nothing and the registry would be lying in exactly the way T020 fixed. **No coverage/regression command reaches the shipped CLI (Constitution II).** `coverage` and the regression tooling are `deacon-conformance` surfaces — contributor machinery about how deacon is tested, not consumer functionality the containers.dev spec describes. The existing check walks the top-level `Commands:` column only, which was sufficient while every dev-only tool was a top-level group; `coverage` has subcommands, and the cheapest way to leak one is to hang it off an existing consumer command rather than add a top-level entry. So this walks one level deeper, and carries a positive control (`up` must be found) so a parse that silently matched nothing cannot pass vacuously. Both functions are authored in `FUNCTION_UNITS`, as 023 US4 did for its seven fault-injection guards: baseline enumeration must describe every `#[test]` it finds, so a guard with no recorded assertion is a hard error rather than an undescribed unit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
… evaluator Introduces the `sdim-` / `rule-` / `hrt-` record shapes and the applicability evaluator in a new `scenario` module (024 T031-T033). Scenario dimensions get their OWN namespace and must never join `dimensions.json`: `applies_in_profile` treats a condition on an unassigned environment dimension as unsatisfied, so folding scenario dimensions into the environment model would silently drop behaviors out of profile and shrink the very denominator this feature exposes (research Decision 1). `Condition` is reused verbatim; only the evaluator is new. A rule constrains only the dimensions it names, a partial combination missing a named dimension is inconclusive rather than excluded, and a conditionless rule excludes nothing — treating it as excluding everything would turn one malformed record into an empty denominator, the failure mode most likely to look like success. Also registers the four new id prefixes on `RecordType` so V2's grammar, prefix-type agreement, and cross-registry uniqueness cover them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ty rules Adds `scenario.json` / `applicability.json` loading to the registry loader and authors both records (024 T034-T036). The six required dimensions carry the FR-005 - FR-009 minimum value sets. Eight exclusion rules prune the space, each carrying a ground that names a mechanism rather than asserting its own exclusion: - no container state for the six operations that never create or attach to one; - no Feature composition for teardown, scaffolding, and diagnostics; - no Feature ordering or lockfile resolution for `exec`, which attaches to a container another operation created; - no image-metadata layer for operations that never resolve an image; - no layered configuration for `doctor`; - output-mode exclusions for the operations that emit only one form (`exec` streams the invoked command and emits no result document; `read-configuration`/`up` emit only a JSON document; `down`/`run-user-commands` emit none). Note the deliberate divergence from data-model.md's illustrative rule, which excludes only the three container-ful states rather than all four: excluding `none` as well would leave `read-configuration`, `build`, and `doctor` with NO valid total assignment, and a case must assign every dimension (data-model.md §3). The dimension therefore survives with one value rather than being pruned outright. `applicability.json` carries two record kinds in one document, so it needs its own file struct (the `MappingFile` precedent) rather than the `Collection` envelope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Implements the contracts/obligation.md 11-step generation algorithm (024 T037-T038): per-operation pairwise enumeration over applicable dimensions, then hand-selected high-risk triples, then one obligation per behavior, all sorted by id. Two decisions worth stating, both about not multiplying the denominator: - The full Cartesian product is never materialized. Pairs come directly from the two-dimension cross product of PERMITTED values, after dimension pruning — which is what makes the space tractable without a covering-array minimizer (research D3). - Each behavior yields EXACTLY ONE obligation. An empty applicability is one universal context (zero obligations would erase the behavior from the denominator); a non-empty one is itself the context, because each condition pins a value SUBSET meaning "any of these", and expanding a subset into one obligation per value would multiply the two obligation kinds against the environment model — the multiplication FR-019 forbids. Generation is refused rather than guessed when the registry declares no spec-kind revision: an inventory with an invented pin would have no provenance at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…n provenance
V26 (registry-only, so it runs on every PR via `validate`): dead dimension values, empty
or duplicated value sets, a missing required dimension, a rule naming an unknown
dimension or value, a rule with fewer than two conditions or no real ground, a malformed
high-risk triple, and a case `scenarioContext` that is partial, undeclared, or itself an
excluded combination.
V27 (needs the committed inventory, so it joins V14/V17 in the sibling-resolved pass):
committed != regenerated, a revision that names no spec-kind record, and an obligation
referencing a value the registry no longer declares. A registry that declares a scenario
model but ships no inventory is a violation, not a skip — an absent file would otherwise
read as "nothing to check".
A registry declaring no scenario dimensions opts out of V26 entirely, so the fixture
registries that predate this feature stay silent rather than reporting six missing
dimensions each.
The ground check reuses V23's filler vocabulary rather than forking a second one, but
deliberately NOT `names_an_exclusion_ground`, whose markers ("constitution",
"principle", "authoring") are tuned for permanent out-of-scope claims: an applicability
ground argues from a mechanism, so requiring those markers would reject every correct
ground.
Both are free functions rather than further `Checker` methods — `validate.rs` is already
3,300 lines and Principle V puts new logic in a new boundary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…he coverage CLI Wires the `coverage generate|check|report` handlers (024 T039-T040, T043-T048) and commits the generated obligation inventory (T051): 745 obligations — 718 combination pairs across the ten operations, plus 27 behavior obligations. Coverage matching (T043) follows contracts/scenario-model.md exactly: a pair is covered iff a case's `scenarioContext` matches both values under the SAME operation and the case is executable — a legacy carrier counts only while an open residual keeps it alive, so coverage decays with the residual rather than outliving it. Behavior obligations reuse the established per-behavior coverage rather than a second, forkable evaluator, so the denominator `certify` measures and the one this feature adds can never disagree. Buckets are the five FR-026 ones plus `undispositioned` — the honest sixth state for an obligation carrying no explicit disposition yet (User Story 2's job). Folding it into `gap` would overstate what is known; folding it into `covered` would understate the hole. Three report families ship now; `coverage-triples` waits for the `hrt-` records it renders, since an empty triples document would report a complete triple set nobody chose. Each .md is rendered from the same in-memory model as its .json, so the human-readable artifact and the machine-readable one cannot disagree about coverage. Two report fields are DERIVED because the record does not carry them yet, and say so: input classes (`boundary`/`unsupported` have no derivable signal and are therefore always reported missing) and per-operation case attribution (from the subcommands a case actually invokes, which keeps the report informative for the 88 cases predating the scenario model). Against the committed record it reports 6 channels below the three-case floor and 7 operations with zero cases — the measured shape of the hole. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…igation determinism guard Eight acceptance tests (024 T023-T030) covering every US1 scenario, plus the hermetic `coverage check` equivalent in `registry_valid` (T049) so committed-vs-regenerated drift fails on every PR. Two of the tests deliberately RE-IMPLEMENT the applicability predicate and the pair enumeration rather than calling the production evaluator: asserting `scenario::is_invalid` against `scenario::is_invalid` would be a tautology, and a second independent reading of the same records is what makes it evidence. The comparison runs in both directions — a dropped valid pair understates the hole and a kept forbidden pair makes the denominator unreachable, and both look like progress from inside the report. The T030 guard fingerprints the whole `conformance/` tree before and after a real `coverage generate` run, so "writes only obligations.json" covers files the test never thought to name. Also fixes an unrelated failure from ccd479c: registering `parity_conformance_docker` took the live-binary count to 7 while `parity_corpus`'s assertion still expected 6. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…nce (V26-V27) Adds the RULES.md section the V26/V27 index rows already link to (024 T050), and marks T023-T051 complete in tasks.md. Records four things that are easy to get wrong later: - why scenario dimensions must never join `dimensions.json`, and that the failure would be INVISIBLE (certify counts only in-profile behaviors, so the denominator would shrink and stay green); - why exclusion must stay attributable — collapse "absent because impossible" into "absent because nobody wrote it" and any missing combination can be explained away, which makes the denominator unfalsifiable; - the deliberate divergence from data-model.md §2's illustrative rule: excluding `none` as well as the three container-ful states would leave read-configuration/build/doctor with no valid total assignment at all, and a case must assign every dimension; - why a hand edit and a stale regeneration are indistinguishable to V27, and both fail — there is no way to edit the generated file that the check treats as legitimate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
T015–T022 marked done with what was verified, in the annotation style T001/T007–T014 already use. Notes the two things that were not obvious going in: `fs-heavy` belongs to the config-only binary (its own model definition says "no Docker"), and registering a seventh live binary required an explicit post-freeze allow-list in baseline enumeration, because the frozen baseline records a pre-migration world a new binary was never part of. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…s explicit resolution Adds the hand-authored `odp-` half of the obligation model (024 US2, T061-T063): `conformance/registry/obligation-dispositions/<area>.json`, loaded exactly like `behaviors/` and `classifications/` (same envelope, same per-file split, so adding an area is a file rather than a code change). The exactly-one-of arity across `cases`/`rationale`/`waiver`/`gap` is enforced at DESERIALIZE time via a raw intermediate + `TryFrom`, following `residual.rs`: a record whose payload disagrees with its disposition word is not a nuance for a validation pass to interpret, it is a half-stated judgement, and the only honest reading is to refuse it at the door. V28 then owns arity ACROSS records (how many dispositions one obligation has) and V29 owns semantics -- questions that genuinely need the rest of the registry to answer. The same deserializer refuses a blank payload and the `UNREVIEWED` scaffold sentinel (T070), so a skeleton committed unedited cannot load. Resolution is explicit only -- no inheritance, no default, and no winner picked from a conflict: silently preferring one of two records would convert a disagreement between reviewers into a decision neither made. `audit_dispositions` is the single join behind V28, `certify`'s blocking set, and `coverage scaffold`'s worklist, for the reason `join_inventory` exists: if the scaffold's idea of "undispositioned" could drift from V28's, the workflow would loop forever. `evaluate_obligations` now consults dispositions with a stated precedence: the DERIVED inactive-environment bucket first (an author must not be able to retire an obligation by declaring its environment inactive), then the single explicit record, then the evidence. The last step is not a disposition default -- V28 fires on an undispositioned obligation whatever bucket the report prints -- it is only what a report can honestly say in the interval between generating an obligation and judging it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Replaces the T003 `todo!()` stub (024 US2, T069). Emits skeleton `odp-` records to STDOUT for every undispositioned APPLICABLE obligation, reading its worklist from the same `audit_dispositions` join V28 will enforce, so filling in everything the scaffold emits is guaranteed to clear the gate. Every skeleton carries the `UNREVIEWED` sentinel twice. `disposition` carries it so the closed enum refuses the record -- which forces the author to pick one of the four words rather than inherit a default; `rationale` carries it because contracts/coverage-cli.md names that field, and because a record still holding it after a disposition was chosen is exactly the shape V29's filler test rejects. Never writes the registry, and exits 0 even when nothing is undispositioned: making "nothing to do" an error would put the workflow's terminal state in the failure channel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Authors the initial `obligation-dispositions/<area>.json` tree (024 US2, T071): one
`odp-` record for each of the 745 committed obligations, so the queue V28 will
enforce starts at zero rather than at 745.
- **27 behavior obligations -> `case`**, pointing at the cases already linked to
each behavior in `cases/<area>.json`. Nothing new is claimed here: the links
predate this feature and back `certify`'s existing behavior-level coverage. All 27
have at least one, so none needed a waiver or a gap.
- **718 combination obligations -> `gap`.** No committed case declares a
`scenarioContext` yet (that is US3-US5), so not one pair can honestly be
dispositioned `case`, and `non-testable` would be an argument standing in for
evidence nobody has. `gap` is the honest word.
The 718 records point at **ten** shared `gap-` records, one per operation, rather
than 718 individual ones: the gap is a single fact per operation ("no case enters
this operation's pair space"), and 718 restatements of it would be noise a reviewer
cannot read. Each names what is missing and links the tracking phase. They carry no
`behaviors`: this is missing coverage in the combination space, not a claim about
any behavior, and linking one would flip that behavior's coverage state and R7
disposition for a reason that has nothing to do with it.
The records were generated mechanically from the committed inventory (obligation ->
gap is determined purely by the obligation's `operation`), never hand-typed, for the
same reason `coverage generate` exists.
**`certify` now exits 1 with ten blocking gaps, and that is the checkpoint, not a
regression**: "undispositioned work blocks the release" is what US2 delivers, and
watering a disposition down to reach green is precisely the move the whole feature
exists to prevent. `validate` stays exit 0. The one test asserting the real registry
certified with an EMPTY blocking set is retargeted at the invariant that still
holds and is the one worth guarding: nothing but a declared coverage gap blocks --
no uncovered behavior, no constraint/clause join violation, nothing hiding behind
them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ispositions Implements the obligation half of the gate (024 US2, T064-T068). **V28 (arity across records)**: an applicable obligation with zero dispositions, or with more than one. Zero is the state nobody chose -- the diagnostic names the obligation, its behavior or operation+assignment, and its context, so it is actionable without opening the machine-owned inventory. More than one is two judgements: resolution never picks a winner, because doing so would turn a disagreement between reviewers into a decision neither made. **V29 (semantics)**: a `non-testable` rationale that names no ground (the same ground-naming test V23 applies to `outOfScopeRationale` -- a bare restatement is indistinguishable from unqueued debt, which is what a gap is for); a high-risk triple argued rather than tested (FR-015 -- triples are selected precisely because interaction defects hide there); a stale record whose obligation no longer resolves; a payload naming a case, waiver, or gap the registry does not declare; and a `waived` record backed by a BLANKET waiver scope. That last one is the FR-023 analogue of the FR-032 rule V19 enforces on an allowed difference. `Scope` is a closed union and only `StateField` can be widened -- its `field` matches exactly or as a trailing-`*` prefix, so a bare `"*"` matches every field of the fixture: a global ignore wearing a waiver's clothes, and one that could never self-invalidate. `CorpusCase` names one case of one corpus and has no wildcard form, so it has no blanket shape to reject; inventing one for symmetry would reject correct records. Both classes are computed from the SAME `audit_dispositions` join `coverage scaffold` emits from, so filling in what the scaffold prints is guaranteed to clear the gate. Both are scoped to registries that declare a scenario model, exactly as V27 is: a pre-024 fixture never opted into the obligation regime, and without the scoping every one of them would suddenly owe a decision per behavior. `certify` gains `BlockingKind::Obligation` carrying the class in `code` -- the shape `Constraint` and `Clause` already use, so the output does not fork into a third -- plus a `today` parameter for the one date-dependent condition: a `waived` disposition whose waiver has expired (V6, SC-009). An expired waiver has never blocked certification on its own, and still does not; what changed is that when a waiver is the ONLY thing standing between an obligation and "undispositioned", its expiry leaves nothing standing there. A `gap` disposition gets no blocker of its own: it blocks through the `gap-` record it names, which V29 already requires to resolve, and listing it twice would double-count one fact. The five FR-026 buckets plus the undispositioned queue are reported ALONGSIDE the behavior-level numbers, never folded into them: the two denominators answer different questions, and collapsing them would let progress on one hide the absence of progress on the other. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…tures Covers every User Story 2 acceptance scenario (024, T052-T060; FR-071, FR-078): undispositioned blocks and is NAMED (with its operation and assignment, or its behavior and context); `gap` blocks; a waiver expiring before `--today` blocks and names the waiver; unexpired `waived` and `non-testable` neither block nor vanish; two dispositions on one obligation fail; a filler rationale fails; adding a behavior without dispositioning its obligation fails; a disposition whose obligation no longer resolves is stale. Plus the triple-argued and blanket-waiver-scope arms of V29, and a scoping test proving a pre-024 registry owes nothing. The fixtures are `base/` plus per-scenario overlay directories rather than nine complete registries: nine copies of a twelve-file registry would bury each scenario's one interesting edit in several hundred identical lines, and the review question is always "what one thing changed, and does the gate notice?". The obligation inventory is generated at test time because it is a pure function of the registry (V27 says so), so a committed copy per variant would be a second thing to keep in sync with no independent authority. Three choices worth naming: - The tests drive the LIBRARY gate, not the binary. The binary validates before certifying and exits 1 on any violation, so every scenario here would "fail certification" whether or not the obligation gate existed. Asserting on the verdict's SHAPE -- which blocker, which class, which record -- is what separates a working gate from a registry that merely fails. - `stale` is driven by renaming a dimension VALUE, not by hand-writing a record that names a nonexistent obligation. Both produce a stale record; only the model edit shows the mechanism the drift workflow actually meets, and it simultaneously proves the other half -- the re-hashed obligations come back undispositioned, because disposition is never inherited by name. - Every "the gate fires" assertion is paired with a control. `base` is clean and certifies (so a check that fired unconditionally would not pass), the filler rationale is contrasted with nine grounded ones of the same disposition kind, and the blanket waiver is contrasted with a specific-scope waiver. One behavior changed to make the conflict test true rather than to make it pass: a conflicting obligation now buckets `undispositioned` instead of falling through to the evidence. The evidence fallback answers "nobody has looked at this yet", and someone plainly has -- twice, incompatibly. Letting the evidence speak would report a conflict as settled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Adds the `## Obligation dispositions (V28 - V29)` section the T004 violation-class index already links to, keeping the `validate.rs` <-> `RULES.md` lockstep the index exists to make checkable (024 US2, T072). Restates gap vs. waiver for obligations, where the vocabulary gains a third option the behavior-level distinction does not have. The three are not interchangeable, and `non-testable` is the only one admitting NO follow-up -- which is why its rationale is held to V23's ground-naming test. The reverse mistake gets equal weight: reaching for `gap` when the difference is already characterized inflates the blocker list with things nobody intends to change, and a blocker list that is mostly noise stops being read. Also records the triple restriction (an argument is the one thing the model does not accept where interaction defects hide), why explicit-only resolution picks no winner from a conflict, why `inactive-environment` outranks even an explicit record, why a stale disposition is reported rather than dropped, and why a `gap` disposition gets no blocker of its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Records what each task landed as, including the three places the implementation differs from the task text and why: the fixtures live at `fixtures/conformance/obligation-gating/` (where all 24 sibling conformance fixture registries live) rather than under `crates/`; the `UNREVIEWED` rejection sits in the `odp-` deserializer rather than `load.rs`, next to the record's other field rules; and V29's "all executable" check does not reuse `executable_case_ids`, whose residual-carrier decay rule would declare six live integration binaries nonexistent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…low (024 US3)
Fills the hole User Story 2 froze in place: 718 combination obligations, every one
dispositioned `gap`, because no committed case declared a `scenarioContext`.
Data
- 14 hand-selected high-risk triples (`hrt-`) in `applicability.json`, each with a
stated reason a reviewer can argue with, and each covered by an executable case —
FR-015 makes the triple set the one place an argument may not stand in for evidence.
- 81 new cases across all ten operations, taking the seven with ZERO cases
(`build`, `run-user-commands`, `down`, `outdated`, `upgrade`, `templates apply`,
`doctor`) to real coverage, and giving every operation a case in each of the five
input classes and each permitted configuration source.
- 77 pre-US3 declarative cases annotated with the `scenarioContext` they always
exercised. Four are deliberately left unannotated: the `bad-config-path` /
`missing-config` families carry no configuration document whose shape could be
recorded, and annotating `case-readconfig-snapshot` would invalidate its committed
snapshot for a record whose inputs did not change.
- 18 behaviors, all from research Decision 9 source (a) — the observable contract of a
previously-uncovered operation — each traced to a source unit and registered in
`POST_BRANCH_BEHAVIORS` with why it is not a variant of an existing claim.
- 4 waivers and 2 extensions for divergences measured against the pinned oracle
0.87.0: the reference does not search the spec's `.devcontainer/<folder>/`
discovery location, accepts a value outside a closed schema enum, reports an empty
`outdated` table for a Feature an extends chain contributes, and fails `upgrade` on
a configuration with no Features.
Result: 315/732 combination obligations covered (from 0), 45/45 behavior obligations,
14/14 triples, 0 undispositioned. `missingInputClasses` and `missingConfigSources` are
empty for all ten operations.
Code
- `TestCase.inputClass`: declared rather than inferred. Inference could only ever
distinguish three of the five classes, so `boundary` and `unsupported` were reported
permanently missing however many such cases existed.
- `coverage-triples` renderer (contracts/coverage-report.md §2), joined to generation
on the substance-anchored obligation id rather than by position.
- `coverage-operations` gains `differentialAvailable` / `differentialSubstitution`, so
an operation with no runnable differential says why instead of looking under-tested.
`reference-lenient` is then not a permitted class there — demanding a case that
cannot be written turns an honest measure into noise.
- V16: a `reference-lenient` case must be a `live-differential`; an operation with no
runnable differential must not declare one. V26: a case's declared operation must be
one it actually invokes. V29: a triple's covering case must be EXECUTABLE.
- `outdated` and `upgrade` join `CONSUMER_SUBCOMMANDS`; `templates-apply` expands to
the two-word command both CLIs actually take.
- `fs-heavy` cases now get an isolated temp workspace with temp-dir-only cleanup: the
group means "significant filesystem operations", and those must not land in the
committed fixture tree every other case reads.
Retired, both for the reason V25 was retired
- V21's reverse-orphan direction ("every declarative case is reached by a mapping
entry"). True only while the case set WAS the migration's output; a case authored
for coverage the migration never had maps from no baseline unit by construction, so
the rule would forbid exactly the growth the migration exists to make room for.
- `post_branch_exceptions` now covers waivers as well as extensions, so the exemption
depends on when a fact was learned rather than on which mechanism an author reached
for.
Also: the report's dead-value liveness is computed from the pair enumeration only, so
a triple naming a value the pair space cannot reach can no longer rescue it — that is
a V26 modelling mistake, and the report must not be the one that hides it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…-review Six hermetic acceptance tests in `crates/conformance/tests/workflow_coverage.rs`, all reading the REAL committed registry so none can pass against a synthetic model: - T073 refuses all three shapes of "skipped" — no driver selects it (the ResourceGroup partition asserted total AND both drivers non-empty, so one binary's selection cannot go dead), nothing can observe it, its evidence cannot be gathered. An `#[ignore]`-style opt-out is deliberately not asserted: the record has no field that could express one. - T074 reads the SAME report a reviewer reads. A test that recomputed the tallies would pass while the report said something else, and the report is where the claim is made. - T075 matches a lenient case's direction-pinning twin on a SHARED BEHAVIOR, not a name suffix — a naming convention can be satisfied by a file rename. - T076 asserts both halves: no differential is claimed where none can run, and the report states why. Without the second, such an operation just reads as under-tested. - T077 asserts per case, not in aggregate: an aggregate failure names the suite, and the suite is not what needs fixing. Repetition is the live tier's job; what is checkable hermetically is what repetition would expose. - T078 catches the failure V29 cannot: a triple dispositioned `gap` — honest, permitted by FR-015, and still leaving SC-003 unmet. It caught exactly that during authoring. `conformance/RULES.md`: records the retirement of V21's reverse orphan-*case* direction and the post-branch exception exemption now covering waivers as well as extensions, and states that a triple's covering case must be executable. T095: of the 156 `non-testable` clause classifications, only the 56 carrying "pending a dedicated conformance behavior" are the reservoir Decision 9 names — the other 100 are descriptive prose whose ground is unchanged. Eight are re-classified `behavior-mapped` where a US3 behavior now covers them: the lifecycle sequence and its per-command exit requirement, the lockfile's name/version/integrity/resolved fields, and `dependsOn` install-before plus the tool's responsibility to calculate an order. The round-priority ALGORITHM clauses are deliberately left: the new cases pin the resulting order, not the intermediate rounds, and mapping them would claim evidence for steps nothing observes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…/T102) Adds the `errorPathTier` marker to the declarative case shape and extends V16 to check that a case claiming it can actually reach a verdict past configuration read. `errorPathTier` is a DECLARED boolean rather than a predicate over `expectFailurePhase`, because a predicate gets it wrong in both directions: `case-down-removes-container` declares `expectFailurePhase: exec` on a verification step and is not an error-path case, while a genuine tier case whose phase was left off would silently leave the tier with nothing to notice. Declaring membership makes each of those a validation failure instead. The phase stays the single record of WHERE the failure occurs (FR-042) — the boolean adds only the claim of membership. V16 gains four rules. A tier case must (1) declare a later-stage `expectFailurePhase` on some operation, (2) declare `config-resolution` on none — a verdict reachable at configuration read is a verdict about the stage the tier exists to look past — and (3) be Docker-backed, since every later stage needs the runtime and a case without the group also gets no isolated workspace and no cleanup guard. Rule (4) governs EVERY declarative case: a declared phase must be one its subcommand can reach, via the closed `phases_reachable_by` mapping. `read-configuration` reaches exactly one phase, so a case declaring `lifecycle:postCreate` on it describes a run that cannot happen; unchecked it would validate cleanly, run, and report a green nothing. `down` deliberately stops at `config-resolution`: teardown owns no phase in the closed set (022 data-model §8, reused rather than extended), so a teardown error-path case declares its failure on the operation that OBSERVES the teardown — the shape `case-down-removes-container` already uses. `errorPathTier` is excluded from `caseHash` for the same reason as `inputClass`: it classifies a case, it does not change a byte the runner feeds the CLI, so joining the tier must never re-record a snapshot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…h side's outcome Two harness defects the error-path tier surfaced by being the first coverage whose cases compare little else but an exit code. **An exit-code divergence could not be tolerated at all.** `diff_paths` emits the BARE channel id when two sides' evidence is a differing scalar — there is no key to descend into — and a bare channel id is exactly what `AllowedDifference::is_global_ignore` rejects (FR-032). So every `observablePath` a case could legally declare was unreachable, and the four committed tolerances spelling `chan-exit-code.exitCode` were inert: three `case-*-reference-lenient-wrong-type` cases were diverging in the live lane against tolerances their authors reasonably believed applied. Fixed by NAMING the observable rather than weakening FR-032. A channel in the new `SCALAR_OBSERVABLE` set carries exactly one value, so `chan-exit-code.exitCode` is fully specific — it is the whole of that channel because that channel is one thing, not because the path matches everything. `chan-stdout`/`chan-stderr` are deliberately absent: they carry arbitrary text, and a tolerance over the whole of one is a wildcard by any reading, so their diverging path stays the bare id that no loadable path can cover. **A verdict recorded a stage nobody declared, and only one side's outcome.** `attach_failure_phase` carried the coarse subcommand inference, which maps EVERY `run-user-commands` failure to `exec` — so a case pinning `lifecycle:postStart` had its own recorded stage contradicted by the report it appeared in. The DECLARED phase now wins (it is the reviewed record), with `inferredFailurePhase` kept alongside when the two differ so nothing is hidden. And a new `sides` block records each side's exit code and whether it failed: a differential verdict said only THAT the two disagree, while FR-042 asks the error-path tier to record what each side did. Also exposes three seams the tier's acceptance tests need: `prereq::probe_docker`, `oracle::verify_binary`, and `DriverConfig::run_config`. A test cannot drive the env-var resolution those two wrap, because `std::env::set_var` is `unsafe` under this edition and `unsafe_code = "deny"` — quite apart from being process-global and hostile to a parallel runner. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
The dev-only program that proves each observable channel is live. For every `reg-` record it runs each candidate case twice — once clean, once with the record's perturbation applied at the evidence-source boundary — and classifies the record `detected` when a case that was clean before is red after, ON THAT RECORD'S CHANNEL. It writes the byte-stable `target/conformance/regressions.json` and exits non-zero on any inert channel (FR-067). This is the only program that can inject: it takes out the process-level capability `inject::activate` requires, which no other program does. Four decisions worth stating: - Baselines are cached PER CASE, not per record. One Docker `up` exercises four channels; re-running it once per record would multiply the tier's cost without adding evidence. - Detection requires a CLEAN baseline. A case already failing on the channel is reported as a note and does not count — its post-injection failure is not evidence the injection caused anything, and counting it would let a pre-existing divergence certify a channel nobody perturbed. - A perturbation that never landed aborts the run naming the record, rather than becoming an `inert` verdict about the channel. - A FILTERED run refuses to write the canonical report. A partial document at the canonical path would read as a complete run that happened to exercise one channel, which is the same category of lie the whole feature exists to prevent. The exit rule lives on `RegressionReport::exit_status`, so the acceptance test asserts the decision this bin makes rather than a restatement of it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Running the injected-regression harness for the first time reported
`chan-process-graph` and `chan-stderr` INERT. In both cases the perturbation was
sound and the CASE was not — which is the finding this harness exists to produce.
`case-up-docker-channels` asserted `chan-process-graph` with `jsonSubset: {}`. An
empty subset matches any graph, so the channel was declared, observed, compared,
and structurally incapable of failing: the mount/network/volume graph could have
been anything at all and the case would still have been green. It now pins the
graph's headline claim — the workspace bind (source, type, read-write) and the
network the container joined.
`case-errors-decl-malformed-json` asserted `chan-stderr` with `contains`. A
`contains` cannot see APPENDED output by construction, so trailing bytes after the
diagnostic — a stray print, a partially written second error, a buffer flushed
late — were invisible on the one channel whose whole job is diagnostics. The
assertion is now anchored at end-of-text: the parse diagnostic must be the LAST
thing deacon writes.
Neither change weakens what the cases already asserted; both add a property that a
plausible defect could violate. Verified against live Docker and the pinned 0.87.0
oracle: `cargo run -p parity-harness --bin coverage-regressions` reports all
eleven channels `detected`, `inertCount: 0`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Adds `make test-parity-regressions` and wires the same run into `parity / live-certification`, after the comparison and the aggregator so a real divergence is still reported on its own terms first. Deliberately without `continue-on-error`. The two steps answer different questions: the comparison asks whether deacon matches the reference, this asks whether the suite that says so is capable of noticing that it does not. An inert channel means every green result resting on that channel was unearned, which is a worse state than a red comparison — so it fails the lane exactly as a divergence does. The report is uploaded as its own artifact for diagnosis on red runs. `make test-parity-regressions` is a separate target from `make test-parity` for the same reason `test-parity-equivalence` is: it is a question you ask about the harness, not a step in comparing against the reference. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
`injection_faults.rs` is the suite that watches the watchman. The injected-regression harness is the only thing standing between a dead observable channel and a trusted green conformance run, so the ways IT can be wrong are exactly the ways a green suite becomes worthless. Six guards, one per failure mode: - an injected regression fails the run and the verdict names the CHANNEL. A run that merely goes red proves something broke; the record's claim is that THIS channel noticed. - an inert channel makes the run exit non-zero (FR-067), asserted through `RegressionReport::exit_status` — the same decision the bin makes, not a restatement of it. Plus: every channel with an observer has a committed record, so an acceptance run can never report `inertCount: 0` about channels it never exercised. - the tree is byte-identical after a run, including after an UNWIND (FR-066). The panic path is the one that matters: `Drop`, not the happy path, is what carries the guarantee. - the same inputs classify identically twice (FR-069). A classification that varies run to run would make `detected` a coin flip and let an `inert` result be re-rolled away. - a DEAD observer — constructed here deliberately, returning a constant whatever the source says — is reported `inert`, not `detected`. The test then asserts the raw source really did change, which is the proof the injection point is upstream: had the harness perturbed the observer's RETURN value, this observer would have reported `detected` while observing nothing (research Decision 5, FR-065b). - an ordinary run cannot apply a regression (FR-070), asserted structurally rather than by convention. Enabling injection requires `RegressionHarness::declare()`, and a source scan asserts that call exists in exactly one program; the drivers and the harness modules they route through are additionally asserted not to name `activate` / `perturb_source` at all. Their single contact point is `inject::intercept`, and a runtime check confirms it leaves captured evidence byte-identical when nothing is armed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ments (T150 batch 1)
Every remaining `non-testable` clause classification carried one of two boilerplate
rationales ("Descriptive/informative prose..." or "...pending a dedicated conformance
behavior"), which is the filler-rationale failure V23/V29 reject elsewhere in this
registry. T150 re-reviews each against the clause's actual excerpt.
This batch covers supporting-tools, lockfile, features-user-env, parallel-lifecycle,
declarative-secrets, secrets-support, gpu-host-requirement, features-legacy-ids and
devcontainer-id-variable (37 clauses):
- 9 flipped to `behavior-mapped` on EXISTING behaviors, so the denominator does not
grow and every flip is evidence-backed: the parallel-lifecycle object form and its
per-command success rule (bhv-up-lifecycle-command-forms,
bhv-run-user-commands-hook-failure), the secrets-file mechanism and its JSON example
(bhv-secrets-dotenv-superset), and the four hostRequirements.gpu property-shape
clauses (bhv-readconfig-basic-parse, matching how their `cst-` schema twins are
already classified).
- 11 re-dispositioned `not-applicable`: the tool-catalogue and editor-owned entries in
supporting-tools, and the whole features-legacy-ids document, whose `legacyIds` /
`deprecated` schema twins are already not-applicable under constitution II.
- 17 keep `non-testable` with a restated ground naming a specific mechanism.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…and reference (T150 batch 2) 27 clauses re-reviewed against their actual excerpts (image-metadata 8, features-lifecycle-scripts 3, devcontainer-reference 16): - 20 flipped to `behavior-mapped` on EXISTING behaviors. The largest group is the one 024 US5 unblocked by de-suppressing named fields: "container mounts, environment variables, and user configuration should be applied at [container creation]" now has three behaviors that observe exactly those three axes, and "per variable, last value wins" is precisely bhv-up-container-env-merge-precedence. The image-metadata merge rows join their already-mapped siblings on bhv-readconfig-merged-configuration, and the metadata structure joins bhv-container-metadata-label-content. - 7 keep `non-testable`, each with a ground naming a mechanism: a goal statement, two block preambles, a delegation clause, an explicit non-requirement, a prohibition scoped to a stage no channel observes, and a validation over an invocation state deacon's argv defaults make unreachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…n-reference (T150 batch 3) All 31 remaining `non-testable` clauses of the devcontainer.json property reference, re-reviewed against their excerpts. - 27 flipped to `behavior-mapped` on EXISTING behaviors, so nothing is added to the coverage denominator and every flip rests on evidence that already runs. The rule applied is the one 020 already established for the schema surface: a prose clause that states a property's shape maps where its `cst-` schema twin maps (bhv-readconfig-basic-parse), and a clause that states a runtime consequence maps to the behavior that observes it — the lifecycle-stage descriptions to bhv-run-user-commands-hook-order, `containerEnv` to bhv-up-container-env-merge-precedence, `remoteEnv`/`remoteUser` to bhv-exec-container-id-metadata, `mounts` to bhv-up-mount-source-and-shape, `containerUser` to bhv-up-effective-user-uid-gid, `userEnvProbe`'s closed enum additionally to bhv-readconfig-unsupported-enum-rejected. - 4 keep `non-testable`: `name`'s UI display text, `requireLocalPort`, `onAutoForward: notify` and the `customizations` delegation — each a UI or other-product affordance deacon has no surface for, with the property's own shape already claimed elsewhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ncies (T150 batch 4) All 16 remaining `non-testable` clauses of the Feature-dependencies proposal. - 10 flipped to `behavior-mapped` on EXISTING behaviors. Every step of the round-based sort that can change WHICH order results (graph construction, roundPriority assignment and defaulting, the max-priority commit rule, the lexicographic tiebreak, the worklist rule, Feature equality, overrideFeatureInstallOrder's n-idx assignment) maps to the two behaviors that observe the resulting order, joining the sibling algorithm clauses of this document that were already mapped there. The no-progress-terminates-with-an-error step maps to bhv-up-feature-install-failure, which 024 US4 authored for exactly "a dependency graph with no resolvable order". - 5 keep `non-testable`: a definition preamble, the sort's initialization line, the OCI media-type fragment, the WHEN-resolution-runs statement, and the MUST-fallback clause for an annotation fast path deacon does not implement at all (so extracting the Feature is its only path and nothing distinguishes falling back from never optimizing). - 1 re-dispositioned `not-applicable`: serializing the Feature metadata into an OCI annotation is publish-side work, out of scope under constitution II. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ocuments (T150 batch 5)
All 36 remaining `non-testable` clauses carried by authoring.json — 30 from the
Features document, 6 from Templates.
A finding worth recording: none of the 36 turned out to be miscategorized
authoring-surface clauses that the document-scope `not-applicable` default already
covers. Every one of them is a CONSUMER obligation living inside an authoring
document — the installer's contract with a Feature, or the applying tool's contract
with a Template — which is exactly why they were given per-clause records rather than
left to the blanket default.
- 24 flipped to `behavior-mapped` on EXISTING behaviors, following the mapping this
file already used for its three consumer clauses (bhv-up-exec-parity as the "up
builds the container correctly from its Features" claim): option resolution and its
environment-variable naming, the per-Feature command sequencing, install ordering and
the dependsOn family, the `containerEnv`-before-install rule (which 024 US5's
bhv-up-path-construction now actually observes), the capAdd/privileged merge rows,
the `_REMOTE_USER`/`_CONTAINER_USER` family, and the three Template option
substitution clauses.
- 12 keep `non-testable`, each with a ground naming a mechanism: two `${devcontainerId}`
computation steps, its scope enumeration and uniqueness claim, the build-layering
note, identifier lowercasing (OCI references are already lowercase at the registry),
the install.sh invocation mechanics and their shell default, a motivation sentence,
and the three `optionalPaths` clauses, whose obligation is an interactive prompt a
non-interactive CLI has no channel for.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
`DiskCache` loads `index.json` into memory at construction and republishes the whole map on every mutation. The publish was already atomic (temp file + rename), but the read-modify-write SEQUENCE was not, so two processes sharing a cache directory silently erased each other's entries — a classic lost update. The host-global state index is the caller that makes this user-visible: `up` for workspace A and `up` for workspace B race on the same file, and the loser's container is no longer recorded. A later `deacon down` for that workspace then resolves nothing and exits 0 having removed nothing. `down --remove --all` was never affected, because it selects by label rather than through the index. Take an advisory `fs2` flock over a sibling `index.lock` across the WHOLE sequence and re-read the published index under it before applying the local mutation, mirroring `port_forward::registry`. `remove`/`clear` degrade to the in-memory snapshot with a warning if the lock cannot be taken, since the `Cache` trait has no fallible form for them — never silently. Both regression tests fail without the fix and pass with it: 170 of 200 entries lost at the cache level, 10 of 16 workspaces at the `StateManager` level. Surfaced by the 024 conformance Docker tier, where four cases run concurrently in `docker-shared` and `case-down-removes-container` diverged because its container was never removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
`down` was the one subcommand that tolerated an unreadable devcontainer.json. On a parse error it logged a warning and fell through to auto-discovery, which re-derives the workspace identity from a DEFAULT configuration — so a `down` over a malformed document scoped the teardown differently from the `up` that created the container, then exited 0 having removed nothing. `up` and `exec` already rejected the same document with exit 1. A configuration that was FOUND but cannot be read is a developer mistake, not an absent configuration, so it now propagates (constitution IV, "strict on mistakes"). The absent case is untouched: `DiscoveryResult::None` still reaches auto-discovery, which is a real capability rather than a fallback — it resolves a container recorded for the workspace PATH, so teardown survives the configuration being deleted, and nine integration tests pin exit 0 there. Fixes the divergence `case-down-malformed-config-rejected` reported on the 024 live Docker tier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…atch 6) The last of the 148 `non-testable` clause classifications, and the only one of them that was neither out of deacon's scope nor already covered by an existing behavior: features-contribute-lifecycle-scripts' "As with all lifecycle hooks, commands are executed from the context (cwd) of the [project workspace folder]". It is the clause inventory's ONLY carrier of the cwd rule — nothing in devcontainer-reference or devcontainerjson-reference states it — so classifying it `non-testable` would have been false and mapping it onto a neighbouring behavior would have claimed evidence that does not exist. So it takes the other branch T150 allows: `behavior-mapped`, on a new behavior that arrives with its evidence rather than with a promise of it. - `bhv-up-lifecycle-command-cwd`, accounted in POST_BRANCH_BEHAVIORS with why it is not a variant of `bhv-up-lifecycle-command-forms`: an implementation can run both declared forms of every hook out of the image's default directory and satisfy that claim while breaking this one for every hook that resolves a relative path. - `case-up-lifecycle-cwd` + `fx-lifecycle-cwd`. The hook reports its OWN `pwd` into a file at an absolute path. Writing to a relative path would have been the shorter fixture and the worse evidence: a file that lands outside the bind mount and a hook that never ran leave the host observer with the same empty allowlist, whereas the recorded value names the directory that was actually used. - Both axes were measured before the record was written — deacon and the pinned oracle 0.87.0 each wrote `/workspace` for this fixture — so `reference: aligned` is an observation, not an assumption (research Decision 11). Verified live: `cargo nextest run --profile parity -E 'binary(=parity_conformance_docker) and test(conformance_group_docker_shared)'` passes with the new case in it. `validate` stays green, `certify`'s blocking set is unchanged (the obligation is dispositioned `case`, not `gap`), and the obligation count moves 787 → 788 with covered 370 → 371. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Closes the T150 deferral in 024's Deferred Work section with the tallies (148 → 45 non-testable, +91 behavior-mapped of which 90 reuse existing behaviors, +12 not-applicable, zero left open), the mapping rule the flips followed, and the two findings that are follow-ups rather than T150 deliverables — `optionalPaths` being unimplemented, and the disproven hypothesis that authoring.json held records the document-scope default already covered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
The `docker-shared` group left eight `diverge` verdicts from User Story 3's originally-authored cases. Each is now either fixed in deacon (separate commits) or evidence-backed here. The tier is green with nothing waived silently. Two are the SAME already-recorded deacon extension surfacing outside `read-configuration`: oracle 0.87.0 has no `extends` support at all, so its `up` fails where deacon builds, and its `run-user-commands` then reports "Dev container not found." Both cases link `bhv-readconfig-extends-merged` and scope an exit-code tolerance to `ext-extends-resolution`. Two are genuine differences newly characterized, each with a behavior carrying all three axes, a self-invalidating waiver, and a filed issue: - `bhv-exec-image-path-preserved` — `exec` drops the PATH entries the image contributed via `ENV PATH`, so a tool the image installed does not resolve. The probe runs a login shell whose `/etc/profile` replaces PATH, `build_effective_env` rebuilds from the probe alone (the sibling `merge_environments` implements the documented containerEnv layer and `exec` does not call it), and `build_exec_args` then forces the degraded value with `-e PATH=`. Not fixed here: the precedence question it raises is a deliberate decision on a path shared by up/exec/run-user-commands, not a reorder (#370). - `bhv-up-changed-config-recreates-container` — deacon provisions a new container when the configuration has changed; the reference reattaches to the stale one (measured directly at 0.87.0). deacon's direction is deliberate and safer. Leaving the superseded container RUNNING is not, and is filed separately (#371). Three cases asserted contracts deacon has never held and are corrected, with the reasoning recorded in each record: - `case-down-unsupported-no-workspace-config` expected a rejection. Teardown is idempotent by design and nine existing tests pin exit 0; the class stays `unsupported` because the input IS unprovidable — it is the outcome column of the class table, a typical consequence rather than a definition, that does not apply to an idempotent operation. - `case-run-user-commands-feature-contributed-hook` counted hooks across BOTH `up` and `run-user-commands`, so it asserted the negation of the already accepted `wvr-ruc-completed-hooks-not-rerun`. Every hook was at 2, the configuration's own included; only this case counted. `up --skip-post-create` isolates the pass under test, and `matches ^1\s*$` replaces a `contains "1"` that would also have passed on 11. - `case-up-stale-config-reentry` asserted `first-create-vs-restart`, which is not deacon's contract and which the closed relationship set cannot express for a recreate. Re-pointed to a spec-expectation. `case-exec-dockerfile-overlay` is split in two. It carried the environment LAYERING (which agrees) and the image PATH (which does not) in one argv, and the PATH difference lands in arbitrary stdout text — where a tolerance would be a wildcard, which is exactly why `chan-stdout` is excluded from `SCALAR_OBSERVABLE`. `case-exec-image-path-overlay` takes the probe alone so the tolerance is scoped to a case whose only exit code IS the probe. It resolves the tool through a SHELL rather than as the exec argv, because `docker exec` resolves a bare argv against the container's own configuration and finds it on both sides — an earlier `command -v` form was worse still, being a builtin and therefore not an executable at all, so both CLIs failed identically and agreed about nothing. Also files #372 (`run-user-commands` writes markers with `config_hash: None`, erasing `up`'s config-drift detection) and #373, the `parity-drift` issue `bhv-container-metadata-label-content` has been asking for in its notes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
T149 asked for a second environment profile to be activated for real, so that FR-004b's promise — activation is a data change, nothing else moves — is proved against the authoritative registry rather than a fixture. The record now exists: `prof-linux-amd64-podman-0870`, identical to the docker profile but for `dim-runtime: podman`. It is deliberately `active: false`, and the reason is empirical. Activation WAS performed, on a copy of the real registry. Its structural half holds: it needed zero changes to `scenario.json`, `applicability.json` or any case. Its substantive half does not. `validate` and `certify` under the podman profile returned results identical to the docker profile's — 787 obligations, 370 covered, 417 gap, `inactive-environment 0`, the same ten blocking gaps — because all 55 behaviors carry an empty `applicability` and therefore apply in every environment. The re-bucketing SC-015 predicts is real and currently vacuous: nothing is runtime-conditioned for a swap to move. That identical output is the argument for leaving the flag off. Activation would move nothing, so it would verify nothing, while asserting that the registry's whole `reference` axis holds under a runtime no case has ever executed against. "Statuses are evidence-backed claims, not aspirations" is the registry's own first principle, and a profile flag is the last place to break it. Genuine activation needs a podman execution lane and runtime-conditioned applicability on the behaviors that actually differ. Modelling the second profile also creates a hazard that did not exist while the file held one record. Coverage and validation both resolve the active profile with `.find(|p| p.active)` — the FIRST match, silently — so a contributor "activating" podman by setting a second `active: true` would get a clean report that means nothing. Activation can only ever be a swap. FR-004a says exactly one profile is active and nothing enforced it, so `exactly_one_environment_profile_is_active` now does. `load_real::seed_registry_loads_cleanly` asserted `profiles.len() == 1` under the label "one active profile", conflating the record count with the active count. It now asserts both separately, which is what it meant all along. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…oor (T142) SC-005 requires no observable channel to rest on fewer than three covering cases. Four were below it: `chan-image` on ONE case, `chan-filesystem`, `chan-injected-process` and `chan-process-graph` on two each. A channel carried by a single case is one authoring mistake away from being unobserved — exactly the failure US6's injected-regression harness exists to catch after the fact, and one worth not authoring in the first place. Four cases, each with a full `scenarioContext`, so they close pairwise gaps as well as the floor: - `case-up-compose-service-graph` — a Compose `up` with no Features. `chan-image`: the service container is created from the image the Compose file DECLARES and still carries deacon's `devcontainer.*` identity alongside Compose's own labels. `chan-process-graph`: both declared mounts with their shape, the workspace bind and the named volume. Neither the volume's daemon path nor the `<project>_default` network is pinned — both are workspace-derived, so pinning them would assert the temp directory rather than the graph. - `case-up-dockerfile-feature-image` — Dockerfile plus one local Feature, a pairwise gap. The Feature layer rebuilds the image, so `chan-image` pins the base's `ENV` and entrypoint on the image the container was finally created from, and `chan-injected-process` pins that the same value reaches the process under the configured non-root `containerUser`. `cwd` is deliberately not asserted: deacon leaves `WorkingDir` empty here and pinning it would record an uncharacterized value. - `case-up-running-reattach-process-context` — `up` against an ALREADY-RUNNING container. The sibling metamorphic case proves re-entry creates no second container; the relationship oracle produces a `chan-temporal` verdict only and never looks at what the reattached container's process context IS. - `case-templates-apply-multifile-tree` — scaffolding as a TREE. `exists` on a file outside `.devcontainer/`, `absent` on the template manifest a naive whole-tree copy leaves behind, `mode` on the scaffolded configuration, and the option substituted into the DOCKERFILE rather than the configuration. Every assertion was run against live Docker and the pinned 0.87.0 oracle, then NEGATIVELY tested: each was perturbed and confirmed to diverge, so none passes vacuously. `channelsBelowFloor` is now 0. Also corrects 22 combination dispositions left reading `gap` while a case already matched them. US3 updated its dispositions in lockstep; US4, US5 and T150 added cases and only registered the new BEHAVIOR dispositions, so the combination records drifted. The direction was pessimistic — over-reporting gaps, never claiming absent coverage — but the report is meant to be truthful in both directions. Covered 374 → 396, gap 417 → 395. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
The 024 entry read `(PLANNED)`. It now records what was built, and the machinery it
describes gets a section of its own rather than being folded into the registry's.
- New "Deterministic Coverage Model" section: the `sdim-` namespace and WHY it must stay
out of `dimensions.json` (a condition on an unassigned dimension is unsatisfied, so a
scenario dim there silently shrinks the denominator); the two obligation kinds and why
they are never multiplied; the mandatory `odp-` disposition and the precedence trap that
bit this branch — explicit records outrank the evidence, so a commit that adds cases and
registers only the new BEHAVIOR dispositions leaves the combination records reading
`gap` while a case matches; the five reporting buckets; reporting-never-gates; V26–V30;
and the injected-regression harness with the two findings it produced (`jsonSubset: {}`
matches anything, `contains` cannot see appended output).
- "Conformance Registry": `cases/<area>.json`, the four new registry files, the `coverage`
command group, and 11 channels rather than the pre-024 nine.
- "Parity & Conformance": `certify` also blocks on V28/V29; `coverage report` is not a
gate; a new case must flip its `odp-cmb-*` records in the same commit; a new channel
needs a `reg-` record and the three-case floor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Three additions, one of them a rule this branch learned the hard way. - **Drift workflow (adding a case)** — new. The pin-bump workflow already existed; adding a case has a different shape (nothing is regenerated, only dispositions move) and its step 4 is the one that gets skipped silently. An explicit disposition takes precedence over the evidence — deliberately, so a reviewer can rule that a mechanical `scenarioContext` match is not real coverage — and the cost is that a commit which adds cases and registers only the new `obl-bhv-` dispositions leaves the combination records reading `gap` while a case matches. Nothing fires: `validate` sees a well-formed record, `certify` sees a blocking-shaped gap, the report under-counts. It happened here, 22 records across three areas, found by hand. - **The five reporting buckets** — a single table defining `covered` / `waived` / `non-testable` / `gap` / `inactive-environment` plus the undispositioned queue, with what each means and whether it blocks. They were previously inferable only from the `certify` outcome table and a closing paragraph; that paragraph now points here rather than half-restating it. - **Reporting never gates** — stated already; made explicit that `coverage report`'s exit code reflects only whether it could WRITE, and that the rule runs both ways (`certify` names what blocks, and does not reproduce the reports' numbers). Also drops the "(User Story 2)" forward references from the drift workflow and the gating note — that story shipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…behind The Docker tier leaked one network and one volume per Compose case, every run, and the leak presented as a FLAKE IN AN UNRELATED CASE: after enough runs the daemon answers the next `docker compose up` with "all predefined address pools have been fully subnetted", so whichever compose case ran next failed with "observed nothing" — a message about observation for a fault in cleanup. 28 leaked networks had accumulated before anything looked. Nothing was watching because nothing could be. `deacon down` computes `deacon_<workspaceHash>_<configHash>` while the reference derives its project from the workspace DIRECTORY (`<basename>_devcontainer`), so `down` never sees the reference's project; and the container label sweep does not reach it either, because only CONTAINERS carry `devcontainer.local_folder` — a Compose network carries just `com.docker.compose.project`. The sweep matches on NAME, which is what the available labels leave: compose names every resource `<project>_<resource>` and the isolated workspace basename is unique per run, so containment selects this workspace's project and no other — the same collision-safety FR-037 already relies on. Matching is case-INSENSITIVE, and that is not a nicety: a compose project name is lowercased and `tempfile`'s suffix is not, so the first version of this fix selected nothing and left the leak exactly where it was, behind a sweep that looked like it worked. The selection is a pure function with its own unit test for that reason. `the_error_path_tier_reclaims_every_resource_it_creates` gains the matching assertion, attributed the same way the container check is: a Compose network or volume named for an isolated workspace that no longer exists is orphaned by definition, and one whose workspace still exists is a sibling driver's in-flight run. Networks are now flat across repeated tier runs (3, the daemon's built-ins) where they previously grew by three. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…d commands (T148) Walked every command block end to end against the shipped binaries. The commands themselves are accurate — `generate` / `check` / `report` / `scaffold` / `validate` all behave and print as documented, `scaffold`'s note really is on stderr so the documented `> skeletons.json` redirect works, and every field the "Find what is missing" table names (`summary.undispositioned`, `bucket`, `excluded`, `deadValues`) exists in the emitted JSON. Four corrections: - **"Add a deterministic case" was missing its load-bearing step.** It said "point the disposition at it" as an aside; the actual requirement is to flip every `odp-cmb-*` the case now covers off `gap`, in the same commit, because an explicit disposition outranks the evidence. Skipping it is silent — validate passes, certify blocks on a gap-shaped gap, and the report under-counts. It happened during this feature's build-out (22 records). Now step 4, with the reports to read at step 3 to find them. - Same section gains the two rules the injected-regression work paid for: perturb each new assertion and confirm it diverges, and keep every channel at three covering cases. - **`--profile parity` does not run "both drivers".** It runs the whole live allow-list, the two declarative drivers plus the four surviving legacy carriers. Added the per-binary filter for the config-only tier alongside the Docker one. - Troubleshooting gains three rows for symptoms seen while finishing this feature: a pair still reading `gap` after a case covers it, `channelsBelowFloor > 0`, and `observed nothing` — which reads like an assertion failure and is not one. Also states the report artifact count as emitted (4 families, 8 artifacts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…148) Records the nine polish and verification tasks, and what each one actually found rather than just that it ran. Two were not the no-ops their titles suggest. **T142** found the channel floor breached four ways — `chan-image` on one covering case — and closed it with four new cases, negatively tested. **T145** found the live tier flaking, and the cause was a Compose resource leak on the reference side that presented as an observation fault three layers away; the fix and its evidence are recorded on the task. Final state: 791 obligations, 0 undispositioned, 396 covered / 395 gap; 14/14 high-risk triples covered; `channelsBelowFloor` 0; `inertCount` 0 across 11/11 channels; `missingConfigSources` and `missingInputClasses` empty for all ten operations; `make test-nextest` 4032/4032; `certify` NOT certified on exactly 10 `gap-pairwise-<operation>` blockers, which is the honest end state this feature was built to produce. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Disk cache index locking (crates/core/src/cache/disk.rs):
- `lock_index` created the lock file unconditionally, so an existing but
non-writable cache dir (read-only mount, another uid's `~/.deacon/state`)
failed `DiskCache::new` and took `up`/`down`/`exec` down with it. Reads are
safe unlocked — `save_index` publishes by rename — so `load_index` now
degrades to an unlocked read and skips only the destructive expiry sweep.
- Replaced the unbounded blocking `flock(LOCK_EX)` with a bounded try-lock
poll. These calls are reached from `async fn` command paths, where an
indefinite wait stalls every other future on the runtime (constitution V).
- `remove`/`clear` skipped the re-read on lock failure but still reached
`save_index`, republishing the construction-time in-memory snapshot over the
live index — the exact lost update the lock exists to prevent, on the failure
path. They now always re-read, locked or not.
- The expiry sweep unlinked data files without republishing the index that
named them, so later readers logged "Cache data file missing" until some
unrelated `set` happened to republish. It is now published under the same
lock that performed it, and no longer runs on every `set` (a whole-map stat
and unlink pass serializing N independent cache writes).
Compose feature env (crates/deacon/src/commands/up/compose.rs):
- The compose path never read `feature_build.combined_env`, so a Feature's
`containerEnv` reached the feature-extended image but never the service's
`environment:` block. `bhv-up-container-env-merge-precedence` is stated over
"the created container", not one container shape. Folded in below the
configuration's and the CLI's values, matching the 024 US5 single-container
fix.
Parity harness:
- `verdict_differential` named the scalar observable only on the value-diff
branch, so a presence mismatch reported a bare channel id — which
`is_global_ignore` rejects at load (FR-032), making it un-tolerable by
construction.
- A per-case wall-clock timeout was reported as `Cause::OracleTimeout`,
blaming the pinned reference for wedged deacon runs, hung `docker inspect`
calls and teardown stalls. Added `Cause::CaseTimeout`.
- The structured-output injector kept visiting outcomes after a failure, so the
reported cause could come from a different operation than the one it
corrupted while earlier outcomes stayed mutated. It now stops at the first
failure and names the operation.
- `authored_properties` was re-read from disk on every structured-output
capture — per operation, per side, per case — and at OBSERVE time, so an
operation that rewrites the configuration changed the authored set out from
under the document being classified. Snapshotted once in `RunContext`,
mirroring `container_inspect`.
- `deacon_binary` shelled out to a synchronous `cargo build` from inside
`runtime.block_on`, blocking a tokio worker for the whole compile.
Conformance:
- `canonical_context` had no arity marker, so `[{a:[b]},{c:[d]}]` and
`[{a:[b,c,d]}]` hashed identically — two behavior obligations colliding on
one `obl-bhv-` id, silently transferring a hand-authored disposition. Values
now join on a distinct separator. No id churn: the registry has no
multi-value conditions today.
- `build_observables` counted every registry case toward the SC-005 three-case
channel floor, so a legacy case whose carrier binary is deleted could satisfy
a floor that exists because an unobserved channel reports green forever. It
now gates on `executable_case_ids`, as `evaluate_obligations` already did.
All 11 channels still clear the floor.
Also corrected the `down` config-load comment, which claimed the branch only
handled "found but unreadable" when it also covers a missing `--config-path`
(symmetric with `up`/`exec`, which pass an explicit `--config` straight to the
loader).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
The Windows `dev-fast` lane failed `committed_obligations_match_a_fresh_regeneration`
with an EMPTY semantic diff:
conformance/obligations/obligations.json does not byte-match a fresh
regeneration (V27): first difference None; +0 added, -0 removed, ~0 changed.
Run `cargo run -p deacon-conformance -- coverage generate`
Identical records, different bytes: git's autocrlf rewrote LF to CRLF on checkout,
which JSON parses away, so the units compared equal while the byte comparison failed.
The suggested remedy could not work — `coverage generate` writes LF and the checkout
rewrites it right back.
Extend `.gitattributes` with `conformance/obligations/** -text`, mirroring the entries
020/021/022/023 each needed for their own byte-exact artifacts (schemas, clause prose,
snapshots, the migration baseline). The committed blob is already LF, so this only pins
the working-tree bytes on every platform.
Also make the failure name its own cause. V27 deliberately cannot distinguish a hand
edit from a stale regeneration; it equally cannot distinguish either from a re-encoded
checkout, and the message sent the reader after drift that did not exist. Both the
validator and the hermetic test now detect the identical-units-but-different-bytes case,
report it as a line-ending difference, and point at `.gitattributes` instead of
`coverage generate`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
specs/024-deterministic-conformance-coveragein full (150 tasks, no deferrals).The registry already said whether each behavior was covered. This says whether the scenario space is — and where it is not, turns the hole into a counted, dispositioned number instead of an unknown.
What this adds
A scenario model in its own namespace.
conformance/registry/scenario.jsondeclares sixsdim-dimensions (operation,config-source,container-state,features,layering,output-mode). They deliberately do not joindimensions.json:applies_in_profiletreats a condition on an unassigned dimension as unsatisfied, so a scenario dim there would silently drop behaviors out of profile and shrink the very denominator this work exposes (research D1).applicability.jsonadds 8rule-exclusions — each with agroundnaming the mechanism, not "out of scope" — and 14 hand-selectedhrt-high-risk triples.Two obligation kinds, never multiplied.
conformance/obligations/obligations.jsonis machine-owned, the sole output ofcoverage generate:obl-bhv-*(one per in-profile behavior) andobl-cmb-*(per-operation pairwise + the selected triples). A behavior is not crossed with the scenario space — that product is the combinatorial explosion the model exists to avoid. Ids are substance-anchored, so a reordered or renamed triple keeps its id and its disposition.A mandatory disposition per obligation. Every applicable obligation carries exactly one hand-authored
odp-record:case,non-testable(+ a rationale naming a ground),waived(+ a scoped waiver withexpires), orgap. A high-risk triple accepts onlycaseorgap— an argument may not stand in for evidence where interaction defects hide.Violation classes V26–V30, all blocking a PR via
registry_valid: scenario-model integrity, obligation provenance (a hand edit and a stale regeneration are indistinguishable and both fail), zero-or-multiple dispositions, malformed dispositions, and injected-regression integrity.The
coverage generate|check|report|scaffoldcommand group, dev-only and hermetic. Reporting never gates:coverage report's exit code reflects whether it could write its artifacts, never what they say.certifyis the gate.An injected-regression harness proving the channels are live.
coverage-regressionsapplies eachreg-record to the evidence source — a process result, an inspect document, file bytes — and requires the case to go from clean to failing on that channel. Injecting into an observer's return value is forbidden and does not compile: every entry point is generic over a sealedEvidenceSourcetrait no observer output can implement.inertCountmust be zero.Also: a Docker-backed error-path tier, de-suppression of previously-hidden normalized fields, and
cases.jsonsplit intocases/<area>.json.Two findings this machinery produced
Both were real, both were in committed cases, and only the injected run found them:
jsonSubset: {}assertion matches any value — the channel was declared, observed, and incapable of failing.containsassertion cannot see appended output by construction.That is also why SC-005 floors every channel at three covering cases: a channel carried by one case is one authoring mistake from unobserved.
Along the way the work surfaced several real deacon defects, fixed here: a
containerEnvprecedence bug (deacon merged the Feature layer over the configuration's value instead of the reverse — measured against oracle 0.87.0), an unlocked read-modify-write in the disk cache's state index causing container leaks under concurrentup/down,downsilently accepting an unreadable config and exiting 0, and a Compose project-name case-sensitivity bug leaking networks.The end state is deliberate
certifyreports NOT certified on 10gap-pairwise-<operation>records. The feature measures and exposes the residual combination hole; it does not claim to close it. Those gaps are the honest output, not an oversight — closing them is follow-up work, andcertifyrefusing to go green is exactly the behavior the model exists to produce.Note this means the release gate is currently red on this branch. That is a conscious hand-off, not a regression: before 024 the same hole existed and was simply invisible.
Deferrals
Both spec-documented deferral tasks were genuinely resolved rather than left open:
Coverage::evaluateand the validator both use a single-profile.find(|p| p.active)lookup). Modeledprof-linux-amd64-podman-0870withactive: falseand added a guard,exactly_one_environment_profile_is_active, rather than forcing a broken state.Review notes
The last commit (
8772e7c) addresses anxhighcode review of the branch: an unboundedflockon tokio worker threads plus a lost-update on its failure path in the disk cache, a compose path that never applied feature-contributedcontainerEnv, a presence-mismatch divergence reported under a bare channel id that is un-tolerable by construction, a per-case timeout mislabelled as an oracle timeout, and an obligation-id encoding that was not injective across condition arity. One review finding was rejected on the merits — erroring on a missing--config-pathindownis symmetric withup/exec, so only the misleading comment was corrected.The compose
containerEnvfix ships without a conformance case behind it, tracked in #374.Verification
cargo fmt --all -- --checkandcargo clippy --all-targets --all-features -- -D warningscleanmake test-nextest(full profile): 4037/4037 passingvalidate,coverage check,certifyall behave as designed (certifyred on the 10 intended gaps)🤖 Generated with Claude Code
https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf