Plan 03-09 Summary: Mutation gate and controller bridge wiring
Status
Done, with three documented deviations (see below) and two open issues in sibling-owned code (see Open issues).
Files changed
src/lifecycle/mutation-gate.ts(new):gateMutation,runGatedMutation, and theGateOutcomeandGatedResulttypes. A synchronous wrapper overdecideMutation. No timer, no wait, no audit write.src/lifecycle/controller.ts: requiredbridge: BridgePortdependency;mutationGate(); bridge listener subscribed increatebefore the first handshake and released indestroy; startup handshake inside the startup budget; bridge state rules; mismatch refusal on retry; bridge teardown instopanddestroy. Operation names unchanged.test/lifecycle/mutation-gate.test.ts(new, 8 tests).test/lifecycle/bridge-states.test.ts(new, 12 tests).test/lifecycle/controller.test.ts,idempotent.test.ts,no-host-env.test.ts,startup-budget.test.ts: fixtures and constructors migrated toFakeBridge(zero latency, healthy). No Phase 2 assertion changed.
Tests
- Verify command
npx tsc -p tsconfig.json && node --testover the six lifecycle files: exit 0, 67 pass, 0 fail, 0 skipped. - Sibling compile errors still present in files this plan does not own:
test/integration/podman-gate.test.ts,test/quality/suite-taxonomy.test.ts(known, plan 03-10), andtest/runtime/session-execution.test.ts(sibling in progress). They do not affect the lifecycle files. npx eslinton all owned.tsfiles: no issues.
Requirement evidence
- CORE-05:
mutation-gate.test.tscovers every state in both modes, with named warnings and exact deny codes, synchronous results, nosetTimeoutcall, and no refusal reaching the mutation. A controller test showsmutationGate()follows the live state (refused when unbound; refused when degraded after a bridge down event). - ANDA-03:
bridge-states.test.tscovers active to degraded on down and back on up; mismatch to error from active and degraded; startup exhaustion to error withANDA_UNREACHABLE, keeping the recorded container; startup mismatch to error with retry refused; other error states retryable; events discarded in error, connecting, stopping, and unbound without a throw or transition; probe exec-only; bridge closed on stop and destroy; bridge close beyond the teardown bound givesREALM_STOPPINGand a repeat destroy resumes.
Deviations
- Bridge close is started first but not awaited before
exec.stop. The plan says stop closes the bridge beforeexec.stop. The frozen Phase 2 idempotency assertion (stop of a stopping session is a no-op while the first stop completes) requiresexec.stopto be invoked synchronously within the firststop()call. Awaiting the close first would break that assertion, and Phase 2 assertions must stay unchanged (D-72). SoteardownBridgeAndContainerinvokesbridge.close()first, thenexec.stopin the same tick, and awaits both under the teardown bound.AndaBridge.close()aborts its stopper synchronously, so the probe loop stops before the container stop begins. - Listener in
connectingis discarded, not transitioned. The create flow owns the startup outcome through theconnect()promise. A listener transition inconnectingwould race that outcome and double-transition (for example,errortoerror). D-64 allows the event to be accepted for this path, but the state change belongs to create. - Mismatch is not retryable by throwing.
create(and thereforestart) fromerrorwith reasonANDA_VERSION_MISMATCHthrowsANDA_VERSION_MISMATCHand leaves the state unchanged.stopordestroystill works from that state.
Open issues (not fixed here: outside the owned files)
AndaBridge.connectis single-use (src/anda/client.ts: "may be called once per bridge"). The controller's retry fromerrorcallsconnectagain on the same bridge. With the real bridge, that call throws a plainError, which maps toOPENSHELL_START_FAILED. The same applies to create after stop, because stop closes the bridge. The fake bridge permits repeat calls, so the unit tests do not catch this. It needs either a re-connectable bridge (plan 03-06 owner) or a fresh bridge per startup attempt supplied by the daemon (Phase 6). Not resolved in this plan.- Listener store-write failures propagate into the bridge publisher, because the listener has no catch by design (the state guard is an explicit check). A failing record write during a background event would surface as a rejection of the bridge's monitor loop. No caller can observe it today.