diff --git a/conformance/fixtures/fx-state-default-mount/.devcontainer/devcontainer.json b/conformance/fixtures/fx-state-default-mount/.devcontainer/devcontainer.json new file mode 100644 index 00000000..32f1e247 --- /dev/null +++ b/conformance/fixtures/fx-state-default-mount/.devcontainer/devcontainer.json @@ -0,0 +1,6 @@ +{ + "name": "StateDiffDefaultMount", + "image": "debian:bookworm-slim", + "workspaceFolder": "/workspace", + "containerEnv": { "DM_ENV": "yes" } +} diff --git a/conformance/migration/mapping.json b/conformance/migration/mapping.json index 7552ad9e..6fbbc07f 100644 --- a/conformance/migration/mapping.json +++ b/conformance/migration/mapping.json @@ -1116,17 +1116,26 @@ { "unit": "parity_state_diff::compose-parity-with-feature-mount-gap", "disposition": "residual", - "residualId": "res-state-diff-cross-cli-snapshot" + "residualId": "res-state-diff-compose-command-override" }, { "unit": "parity_state_diff::compose-sidecar-and-named-volume", "disposition": "residual", - "residualId": "res-state-diff-cross-cli-snapshot" + "residualId": "res-state-diff-compose-command-override" }, { "unit": "parity_state_diff::default-workspace-mount-target-parity", - "disposition": "residual", - "residualId": "res-state-diff-cross-cli-snapshot" + "disposition": "migrated", + "caseIds": [ + "case-state-default-mount", + "case-state-default-mount-target" + ], + "fixtureMapping": [ + { + "from": "inline:state_diff_default_workspace_mount_target_parity", + "to": "conformance/fixtures/fx-state-default-mount" + } + ] }, { "unit": "parity_state_diff::dockerfile-build-and-nonroot-user", diff --git a/conformance/registry/behaviors/observable-state.json b/conformance/registry/behaviors/observable-state.json index f79b0f14..68546ace 100644 --- a/conformance/registry/behaviors/observable-state.json +++ b/conformance/registry/behaviors/observable-state.json @@ -51,6 +51,16 @@ "decision": "follow-spec", "notes": "Backed by case-observable-state (parity_observable_state)." }, + { + "id": "bhv-state-default-workspace-mount-target", + "area": "observable-state", + "statement": "With `workspaceFolder` set and no explicit `workspaceMount`, the workspace is bound at the spec default `/workspaces/` — never at `workspaceFolder`, which is the working directory rather than a mount target.", + "applicability": [], + "spec": "conformant", + "reference": "aligned", + "decision": "follow-spec", + "notes": "deacon mounted the workspace at `workspaceFolder` until #273 aligned it. This is an ABSOLUTE claim about each side, not an agreement claim: a live differential would pass if deacon and the reference regressed together, which is why it is evidenced by the spec-expectation case case-state-default-mount-target while its differential twin case-state-default-mount carries the agreement half. Both sides were measured at the pinned oracle 0.87.0. Asserted through the observer's DERIVED `workspaceBindTargets` field, so no quantified assertion predicate was needed." + }, { "id": "bhv-state-diff-parity", "area": "observable-state", diff --git a/conformance/registry/cases.json b/conformance/registry/cases.json index 973b72f5..9a78741d 100644 --- a/conformance/registry/cases.json +++ b/conformance/registry/cases.json @@ -1881,6 +1881,139 @@ }, "notes": "Migrated from parity_state_diff::appport-published-ports (024 Phase 5). `appPort: [3000]` publishes the same container port in both CLIs. This case was unmigratable before 024 Phase 3: with a FIXED host port both sides collided, because deacon's container was still running while the oracle's `up` ran. The side sequencing fix (capture deacon's evidence, reclaim, then run the oracle) is what makes a fixed-host-port differential expressible at all. Image pinned to debian:bookworm-slim (V18)." }, + { + "id": "case-state-default-mount", + "behaviors": [ + "bhv-container-identity-labels", + "bhv-container-keepalive-command", + "bhv-container-metadata-label-authored", + "bhv-state-diff-parity" + ], + "context": [], + "oracleType": "live-differential", + "resourceGroup": "docker-shared", + "operations": [ + { + "id": "op-up", + "subcommand": "up", + "argv": [ + "--workspace-folder", + "${WORKSPACE}" + ], + "fixtures": [ + "fx-state-default-mount" + ] + } + ], + "expected": [ + { + "channel": "chan-container-state", + "operation": "op-up" + } + ], + "allowedDifferences": [ + { + "behavior": "bhv-container-identity-labels", + "context": [], + "observablePath": "chan-container-state.labels.devcontainer.configHash", + "rationale": "deacon sets `devcontainer.configHash`; the reference sets no such label (measured against the pinned oracle 0.87.0). One named key, never the `devcontainer.*` namespace.", + "divergenceId": "ext-container-identity-labels" + }, + { + "behavior": "bhv-container-identity-labels", + "context": [], + "observablePath": "chan-container-state.labels.devcontainer.config_name", + "rationale": "deacon sets `devcontainer.config_name`; the reference sets no such label (measured against the pinned oracle 0.87.0). One named key, never the `devcontainer.*` namespace.", + "divergenceId": "ext-container-identity-labels" + }, + { + "behavior": "bhv-container-identity-labels", + "context": [], + "observablePath": "chan-container-state.labels.devcontainer.name", + "rationale": "deacon sets `devcontainer.name`; the reference sets no such label (measured against the pinned oracle 0.87.0). One named key, never the `devcontainer.*` namespace.", + "divergenceId": "ext-container-identity-labels" + }, + { + "behavior": "bhv-container-identity-labels", + "context": [], + "observablePath": "chan-container-state.labels.devcontainer.source", + "rationale": "deacon sets `devcontainer.source`; the reference sets no such label (measured against the pinned oracle 0.87.0). One named key, never the `devcontainer.*` namespace.", + "divergenceId": "ext-container-identity-labels" + }, + { + "behavior": "bhv-container-identity-labels", + "context": [], + "observablePath": "chan-container-state.labels.devcontainer.workspaceHash", + "rationale": "deacon sets `devcontainer.workspaceHash`; the reference sets no such label (measured against the pinned oracle 0.87.0). One named key, never the `devcontainer.*` namespace.", + "divergenceId": "ext-container-identity-labels" + }, + { + "behavior": "bhv-container-keepalive-command", + "context": [], + "observablePath": "chan-container-state.cmd", + "rationale": "The keep-alive command strings differ; the observable behavior is measured equal (`docker stop` 245 ms / exit 0 vs 215 ms / exit 0). Scoped to `cmd`; `entrypoint` and every other field stay compared.", + "divergenceId": "bhv-container-keepalive-command" + } + ], + "cleanup": { + "containers": true, + "images": false, + "networks": true, + "volumes": true, + "tempdir": true + }, + "notes": "Migrated from parity_state_diff::default-workspace-mount-target-parity (024 Phase 5b). A config with `workspaceFolder` but NO explicit `workspaceMount`: both CLIs must produce equivalent container state, including where the workspace is bound. The legacy unit wrote NO report fragment at all (its `assert_parity` call was absent), so migrating it is a coverage GAIN, not a transfer. Its `∄ /workspace ∧ ∃ /workspaces/*` claim is an ABSOLUTE claim about each side, which a differential cannot express — that half lives in the spec-expectation twin `case-state-default-mount-target` (the T072 pattern). Image pinned to debian:bookworm-slim (V18)." + }, + { + "id": "case-state-default-mount-target", + "behaviors": [ + "bhv-state-default-workspace-mount-target" + ], + "context": [], + "oracleType": "spec-expectation", + "resourceGroup": "docker-shared", + "operations": [ + { + "id": "op-up", + "subcommand": "up", + "argv": [ + "--workspace-folder", + "${WORKSPACE}" + ], + "fixtures": [ + "fx-state-default-mount" + ] + } + ], + "expected": [ + { + "channel": "chan-exit-code", + "operation": "op-up", + "assertion": { + "equals": 0 + } + }, + { + "channel": "chan-container-state", + "operation": "op-up", + "assertion": { + "jsonSubset": { + "workspaceBindTargets": [ + "/workspaces/" + ] + } + } + } + ], + "cleanup": { + "containers": true, + "images": false, + "networks": true, + "volumes": true, + "tempdir": true + }, + "notes": "The spec-expectation twin of case-state-default-mount (024 Phase 5b), carrying the half a differential structurally cannot: the legacy unit asserted an ABSOLUTE fact about each side — NEITHER CLI mounts the workspace at `workspaceFolder` (/workspace), BOTH mount it at the spec default /workspaces/ (#273) — and a differential would pass if both regressed together. `workspaceBindTargets` is the observer's DERIVED field (the destinations of binds rooted at the workspace), so the quantified `∄ ∧ ∃` claim becomes plain equality with no new assertion predicate: a single-element list at the spec default is exactly '∃ the right one and ∄ any other', including /workspace. `` is the `workspace_basename_token` rewrite. Image pinned to debian:bookworm-slim (V18)." + }, { "id": "case-state-diff", "behaviors": [ diff --git a/conformance/registry/residuals.json b/conformance/registry/residuals.json index 901d263c..bcf0d0e6 100644 --- a/conformance/registry/residuals.json +++ b/conformance/registry/residuals.json @@ -58,7 +58,7 @@ "parity_exec::env-propagation" ], "blockedCarrier": "parity_exec", - "missingCapability": "a declarative case for `parity_exec::env-propagation`. NOTE: the previously recorded need for per-side argument vectors was DISPROVEN in 024 planning \u2014 deacon's `exec` declares `--remote-env` as the primary flag with `--env` as a legacy alias, so the single shared argv `--remote-env FOO=BAR` runs on both CLIs and no runner capability is missing. Only the case itself is outstanding.", + "missingCapability": "a declarative case for `parity_exec::env-propagation`. NOTE: the previously recorded need for per-side argument vectors was DISPROVEN in 024 planning — deacon's `exec` declares `--remote-env` as the primary flag with `--env` as a legacy alias, so the single shared argv `--remote-env FOO=BAR` runs on both CLIs and no runner capability is missing. Only the case itself is outstanding.", "followUp": "specs/024-drain-parity-residuals/tasks.md#P6", "behaviors": [ "bhv-exec-command-parity" @@ -86,7 +86,7 @@ "parity_harness_faults::q_stale_snapshot_is_reported_naming_the_drifted_field" ], "blockedCarrier": "parity_harness_faults", - "missingCapability": "fault injection against the harness itself: these units assert how the COMPARISON MACHINERY reports a missing oracle, a version mismatch, a crash, a timeout, a malformed output, a waived difference, a stale waiver, each difference class (ref-only / deacon-only / value / accept-vs-reject with direction) and each declarative outcome (allowed-difference / no-reference-for-platform / stale) \u2014 they observe the harness, not a devcontainer, so they have no observable channel and no oracle to compare against", + "missingCapability": "fault injection against the harness itself: these units assert how the COMPARISON MACHINERY reports a missing oracle, a version mismatch, a crash, a timeout, a malformed output, a waived difference, a stale waiver, each difference class (ref-only / deacon-only / value / accept-vs-reject with direction) and each declarative outcome (allowed-difference / no-reference-for-platform / stale) — they observe the harness, not a devcontainer, so they have no observable channel and no oracle to compare against", "disposition": "permanent", "outOfScopeRationale": "These units observe the COMPARISON MACHINERY, not a devcontainer: they assert how the harness reports a missing oracle, a crash, a timeout, a stale waiver and each difference class. A declarative case compares deacon to a reference across an observable channel, so a self-test of the comparator has no oracle and no observable channel to name. Out of scope structurally, not pending a feature.", "behaviors": [] @@ -102,9 +102,9 @@ "parity_registry_check::waivers_live_in_conformance_registry_not_legacy_locations" ], "blockedCarrier": "parity_registry_check", - "missingCapability": "structural self-checks of the repository's own test wiring: these units assert registry-to-test-file agreement, nextest profile selection, corpus minimums and the no-silent-skip idiom scan \u2014 repository structure, not consumer behavior, so no declarative case can express them", + "missingCapability": "structural self-checks of the repository's own test wiring: these units assert registry-to-test-file agreement, nextest profile selection, corpus minimums and the no-silent-skip idiom scan — repository structure, not consumer behavior, so no declarative case can express them", "disposition": "permanent", - "outOfScopeRationale": "These units assert repository structure \u2014 registry-to-test-file agreement, nextest profile selection, corpus minimums, the no-silent-skip idiom scan. That is a property of the repository, not consumer behavior against the spec, so no observable channel and no reference exist. Out of scope structurally, not pending a feature.", + "outOfScopeRationale": "These units assert repository structure — registry-to-test-file agreement, nextest profile selection, corpus minimums, the no-silent-skip idiom scan. That is a property of the repository, not consumer behavior against the spec, so no observable channel and no reference exist. Out of scope structurally, not pending a feature.", "behaviors": [] }, { @@ -127,7 +127,7 @@ "blockedCarrier": "parity_observable_state", "missingCapability": "reference-CLI feature-authoring interop: the unit feeds a deacon-generated lockfile to the reference's `features resolve-dependencies`, and the declarative operation surface is deliberately restricted to the consumer subcommands (Principle II)", "disposition": "permanent", - "outOfScopeRationale": "Constitution II: feature authoring is permanently out of scope for deacon, and this unit needs deacon `upgrade` plus the reference's `features resolve-dependencies` \u2014 both outside the consumer subcommand surface a declarative operation may name. The principle forbids it, so no capability will ever unblock it.", + "outOfScopeRationale": "Constitution II: feature authoring is permanently out of scope for deacon, and this unit needs deacon `upgrade` plus the reference's `features resolve-dependencies` — both outside the consumer subcommand surface a declarative operation may name. The principle forbids it, so no capability will ever unblock it.", "behaviors": [ "bhv-state-container-parity" ] @@ -198,19 +198,18 @@ ], "missingCapability": "vendored fixtures for the pinned third-party workspaces: the manifest fetches 33 public workspace snapshots over the network on demand, and vendoring third-party content into this repository is deliberately out of scope, so no case can materialize them", "disposition": "permanent", - "outOfScopeRationale": "Vendoring third-party content into this repository is out of scope by decision (research D8): the manifest names 33 public workspace snapshots fetched over the network on demand, and a declarative fixture must be committed, content-hashed input. These entries are inventoried as a recorded coverage SOURCE, never executed, so there is no capability to add \u2014 only a decision already taken.", + "outOfScopeRationale": "Vendoring third-party content into this repository is out of scope by decision (research D8): the manifest names 33 public workspace snapshots fetched over the network on demand, and a declarative fixture must be committed, content-hashed input. These entries are inventoried as a recorded coverage SOURCE, never executed, so there is no capability to add — only a decision already taken.", "behaviors": [] }, { - "id": "res-state-diff-cross-cli-snapshot", + "id": "res-state-diff-compose-command-override", "units": [ "parity_state_diff::compose-parity-with-feature-mount-gap", - "parity_state_diff::compose-sidecar-and-named-volume", - "parity_state_diff::default-workspace-mount-target-parity" + "parity_state_diff::compose-sidecar-and-named-volume" ], "blockedCarrier": "parity_state_diff", - "missingCapability": "cross-CLI container-state snapshot comparison: a normalized whole-container state snapshot taken on both sides and diffed field-by-field, which the per-channel `chan-container-state` observer does not provide", - "followUp": "specs/023-migrate-parity-to-conformance/tasks.md#T103", + "missingCapability": "not a missing capability — a deacon DEFECT the replacement cases would surface as an uncharacterized `stricter` verdict: on the compose path deacon replaces the service's declared `command` with its keep-alive instead of honoring `overrideCommand`'s compose default of false, so the compose service's own process never runs. Measured against pinned oracle 0.87.0: a service whose command touches a marker file and then sleeps creates that marker under the reference and not under deacon. The cases are held back until deacon is fixed, rather than migrated with a tolerance that would characterize a spec violation as intentional", + "followUp": "specs/023-migrate-parity-to-conformance/tasks.md#T117", "behaviors": [ "bhv-state-diff-parity", "bhv-state-container-parity" @@ -223,10 +222,11 @@ ], "blockedCarrier": "parity_state_diff", "missingCapability": "a reference-free comparison mode that diffs two DEACON runs against each other (single-container versus compose for the same logical configuration), with no reference side involved", - "followUp": "specs/023-migrate-parity-to-conformance/tasks.md#T103", "behaviors": [ "bhv-state-diff-parity" - ] + ], + "disposition": "queued", + "followUp": "specs/023-migrate-parity-to-conformance/tasks.md#T103" } ] } diff --git a/conformance/registry/sources/spec.json b/conformance/registry/sources/spec.json index d5df2240..76eba633 100644 --- a/conformance/registry/sources/spec.json +++ b/conformance/registry/sources/spec.json @@ -12,6 +12,16 @@ "bhv-state-diff-parity" ] }, + { + "id": "src-spec-default-workspace-mount", + "inventory": "spec", + "revision": "rev-spec-113500f4", + "locator": "devcontainerjson-reference.md workspaceFolder / workspaceMount", + "summary": "`workspaceFolder` sets the path to open inside the container; `workspaceMount` overrides the default workspace mount. With no `workspaceMount` the default source-to-`/workspaces/` bind applies, so `workspaceFolder` is a working directory, not a mount target.", + "behaviors": [ + "bhv-state-default-workspace-mount-target" + ] + }, { "id": "src-spec-exec-command", "inventory": "spec", diff --git a/crates/conformance/src/conservation.rs b/crates/conformance/src/conservation.rs index b36290b0..67987d76 100644 --- a/crates/conformance/src/conservation.rs +++ b/crates/conformance/src/conservation.rs @@ -47,8 +47,23 @@ pub const PRE_MIGRATION_BEHAVIORS: usize = 25; /// new *variant* its own behavior — 24 per-workspace cases proving one claim must count as /// one behavior, not 24. That guard is about **re-describing** existing coverage. /// -/// It is not about **newly observed** conformance facts, and conflating the two would make -/// the guard forbid the thing the migration is for. `chan-container-state` became an +/// It is not about **newly observed** conformance facts, nor about a claim the baseline +/// already carried that the registry had simply never NAMED — and conflating either of +/// those with a variant would make the guard forbid the thing the migration is for. +/// +/// Two categories therefore live here, and each entry says which it is: +/// +/// 1. **Newly observed** — a fact no pre-migration record could state because nothing +/// compared the field. `bhv-container-identity-labels` and +/// `bhv-container-keepalive-command` are these. +/// 2. **Newly named** — a claim a legacy unit genuinely asserted while the registry +/// described only part of it, so the unit reported several claims through one outcome. +/// That is research D2's inverse defect, and correcting it necessarily produces a +/// behavior the frozen count never held. Refusing it would mean migrating the unit at +/// reduced fidelity — losing a claim to keep a number — which is the precise failure +/// 023 exists to prevent. `bhv-state-default-workspace-mount-target` is this. +/// +/// `chan-container-state` became an /// observed channel in 024 Phase 4, which retired a blanket rule that had been hiding /// whole label namespaces from comparison; what it exposed is a real, previously /// unrecorded difference between the two CLIs. Refusing to record it would keep the @@ -106,6 +121,23 @@ pub const POST_BRANCH_BEHAVIORS: &[(&str, &str)] = &[ exit 0 vs 215 ms / exit 0); the same field's earlier 'cannot matter behaviorally' \ assumption hid a 10s stall.", ), + ( + "bhv-state-default-workspace-mount-target", + "NEWLY NAMED, not newly observed — the second category above. \ + `parity_state_diff::default-workspace-mount-target-parity` asserted TWO distinct \ + claims through one reported outcome: that the two CLIs agree, and the absolute claim \ + that NEITHER mounts the workspace at `workspaceFolder` and BOTH mount it at the spec \ + default `/workspaces/` (#273). The registry named only the agreement half \ + (`bhv-state-diff-parity`). A live differential cannot carry the absolute half — it \ + would pass if both sides regressed together — so migrating the unit faithfully needs a \ + spec-expectation case, and a spec-expectation case needs a behavior. Not a variant of \ + `bhv-state-diff-parity` or `bhv-state-container-parity`: those state that deacon \ + AGREES with the reference, this states WHERE the workspace is bound, which stays \ + false-if-broken even when both sides break identically. Dropping it to keep the \ + denominator lower would migrate the unit at reduced fidelity — losing a claim to \ + preserve a number, which is research D2's inverse defect reintroduced by the very \ + guard meant to catch it (024 Phase 5b).", + ), ]; /// The observable-channel count at the branch point (research §1g). diff --git a/specs/023-migrate-parity-to-conformance/tasks.md b/specs/023-migrate-parity-to-conformance/tasks.md index d82ee11d..214f3198 100644 --- a/specs/023-migrate-parity-to-conformance/tasks.md +++ b/specs/023-migrate-parity-to-conformance/tasks.md @@ -547,6 +547,21 @@ Per Constitution I (Deferral Tracking); rationale in [research.md §4](./researc - **Fix**: both workflow globs repointed; new `scripts/parity/prepull-fixture-images.sh` gives `make test-parity` the same warm-cache step CI has, so a local run behaves like CI; and T089 gained a THIRD direction (`no_surface_globs_a_removed_path`) — the name-based check could never have caught this, because a rotted *path* fails more quietly than a rotted *name*. - **Accounting, corrected**: nothing was ever silently dropped. With the cache warm, a full `parity_conformance_runner` run puts **`case-merged-decl-universal-jsonc` in the diverge list**, and merged is still 23-of-24 — because the 24th, `case-merged-decl-extends-child`, **agrees**: T072 re-pointed it to the `spec-expectation` oracle, since a live-differential inverted its `reference-stricter` expectation into a guaranteed failure. So 23 diverging + 1 agreeing = all 24 accounted for. The earlier "merged ×23" figure was complete; the suspicion that a case had been excluded from the denominator was reasonable but does not hold. Separately, the 59-unit equivalence run reported no `UNCLASSIFIABLE`/`UNVERDICTED` lines and exited 0, and `run_case` propagates a timeout as `Err` rather than a verdict — so every one of those 59 comparisons genuinely completed. +- [ ] T117 [Deferral, 024 Phase 5b] **deacon replaces the compose service's declared `command`, so the service's own process never runs** — a spec violation, found by migrating the two compose container-state units + - **The spec is explicit.** `devcontainerjson-reference.md`: `overrideCommand` *"Defaults to `true` for when using an image Dockerfile and **`false` when referencing a Docker Compose file**"*, with the rationale spelled out — *"Set to `false` if the default command must run for the container to function properly."* `crates/core/src/compose.rs:1323` reads `self.override_command.unwrap_or(true)`, under a comment asserting the opposite of the spec, on a code path reached ONLY for compose. `test_generate_injection_override_command_default_on` pins the wrong default and cites "Spec default" while doing it. + - **Measured, not inferred** (pinned oracle 0.87.0). Fixture: a compose service with `command: ["sh","-c","touch /tmp/SERVICE_COMMAND_RAN; sleep infinity"]`. `test -f /tmp/SERVICE_COMMAND_RAN` → **reference YES, deacon NO**. The mechanism is visible in `docker inspect`: the reference puts its keep-alive in `Entrypoint` ending in `exec "$@"` and leaves `Cmd` as the compose command, so the declared command runs and the keep-alive loop is reached only when there is no command; deacon puts its keep-alive in `Cmd` and leaves `Entrypoint` empty, discarding the command outright. + - **Consequence**: any compose service whose command matters — a server, a database bootstrap, a queue worker — silently does not start under deacon. The defect is invisible for the common `command: ["sleep","infinity"]` idiom, which is why every existing compose fixture passed. + - **Why the two units are held back rather than migrated**: their declarative replacements compare `entrypoint` and `cmd`, so they report `diverge`. Migrating them with a scoped tolerance would characterize a spec violation as intentional — the one move the model forbids. They carry `res-state-diff-compose-command-override`, whose `missingCapability` records that the blocker is a defect and not a missing capability. + - **Acceptance**: deacon honors the compose default (`overrideCommand` unset ⇒ false on the compose path) and still keeps the container alive when the service declares no command — the reference's entrypoint-wrapper shape achieves both; a regression test asserts the declared command actually ran; `case-state-compose-feature` / `case-state-compose-volume` land and report `agree`/`allowed-difference`; `res-state-diff-compose-command-override` is deleted in the same change. + +- [ ] T118 [Deferral, 024 Phase 5b] **`case-state-appport` pins host port 3000, so it is environment-dependent — green locally, RED in the `live-certification` lane** + - **Found by**: reading the CI lane's failure list rather than trusting the local run. Locally every `case-state-*` case agrees; in the lane `case-state-appport` reports `chan-container-state: Diverge` with `divergingPaths: ["chan-container-state"]` — the channel ROOT, not a field — and **all six** of its scoped tolerances reported STALE. That combination is the signature of one side having no container at all: with nothing to descend into, no path-scoped tolerance can be consumed. Pre-existing on `main`; not introduced by this branch. + - **Cause**: `fx-state-appport` declares `"appPort": [3000]`, a FIXED host port. 024 Phase 3's D-3 fix made the two sides sequential so they cannot collide with each other, but nothing makes port 3000 free on a shared CI runner. When it is taken, that side's `up` fails and the differential compares an object against an absence. + - **Why it matters beyond one red case**: it is invisible today because the lane is already red on the T113 families, so a genuinely environment-dependent case hides inside a list of known-red ones. It also makes `bhv-state-diff-parity`'s evidence for the published-ports claim non-reproducible, which is the sort of thing that silently becomes "we never actually tested that". + - **Options, in preference order**: (a) let the runner allocate a free host port and substitute it into the fixture — but that needs a new operation/fixture capability and `${WORKSPACE}`-style tokenisation for ports, which is a real design decision, not a tweak; (b) drop the fixed host binding and assert only the container-side `ExposedPorts`, losing the host-publish half of the claim; (c) keep the fixed port and characterize the case as environment-dependent, which is honest but leaves the lane permanently unable to prove it. + - **Deliberately NOT done here**: no tolerance was authored. A whole-channel divergence caused by a missing container is not a characterized difference — tolerating it would make "the container was never created" report `allowed-difference`, which is precisely the silent green pass 024 Phase 3's D-2 fix exists to prevent. + - **Acceptance**: the case either runs deterministically on a shared runner (option a or b, with the lost coverage stated if b) or carries an explicit environment-dependence record; and the lane's red list contains no case whose redness is an artifact of the runner rather than a characterized divergence. + - [ ] T110 [Deferral, US3 T054] Give `bhv-exec-container-id-metadata` its own independently-reported case - **Decision**: `parity_up_exec` asserts both up/exec parity AND `exec --container-id` metadata recovery but emits ONE `CaseResult`, so the two behaviors shared one reported outcome (research D2's inverse defect). T054 resolved the registry's over-claim by merging the two pointer cases into one truthful record; it did not create the missing evidence, because a declarative case cannot address a container the previous operation created — there is no runtime-resolved container-id token in an operation's argv. - **Acceptance**: A declarative case evidences `bhv-exec-container-id-metadata` independently (its own reported outcome), and `case-up-exec-parity` no longer claims two behaviors from one outcome.