<objective> Freeze the core tables: the six-state transition table and the 20-code typed error catalogue, each proven by tests written first.
Purpose: Every later module imports these exact tables and never redefines them. Output: src/core/states.ts, src/core/errors.ts, and their contract tests, all green under the plan 01-03 toolchain. </objective>
<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context>
<context> @.planning/PROJECT.md @.planning/phases/01-scaffold-and-frozen-contracts/01-CONTEXT.md @.planning/phases/01-scaffold-and-frozen-contracts/01-PATTERNS.md @package.json @tsconfig.json </context>
<feature> <name>Frozen state table and typed error catalogue</name> <files>src/core/states.ts, src/core/errors.ts, test/core/states.test.ts, test/core/errors.test.ts</files> <behavior> Cases: isTransitionAllowed(unbound -> connecting) returns true; isTransitionAllowed(active -> degraded) returns true; isTransitionAllowed(error -> connecting) returns true; isTransitionAllowed(stopping -> unbound) returns true; every other ordered pair among the six states, including active -> unbound and all six self-transitions, returns false and surfaces REALM_INVALID_TRANSITION. makeRealmError(code) for each of the 20 codes returns { code, reason non-empty, retryable } with retryable true exactly for REALM_NOT_ACTIVE, STARTUP_BUDGET_EXCEEDED, TOOL_TIMEOUT, PODMAN_MACHINE_DOWN, OPENSHELL_START_FAILED, AGENTOS_CRASH_LOOP, ANDA_UNREACHABLE, LOCK_CONFLICT, WRITE_NOT_ATOMIC and false for the other 11. Edge coverage: QUAL-01 adjacency is the self-transition rejection test; FOUND-02 unclassified resolves to the full pair-matrix test. </behavior> <implementation>Implement the pure transition-check function plus the exported allowed-edge table with exactly the D-11 edges, and the RealmError { code, reason, retryable } shape with a single code-to-retryable lookup holding exactly the 20 D-10 codes; no behavior, events, or display mapping. RED: write both test files first and watch them fail. GREEN: write minimal source to pass. REFACTOR only if tests stay green.</implementation> </feature>
<tasks>
<task type="tracer"> <name>Task 1 (tracer, starts red): End-to-end frozen-core slice on the scaffold</name> <files>src/core/states.ts, src/core/errors.ts, test/core/states.test.ts, test/core/errors.test.ts</files> <read_first> - .planning/phases/01-scaffold-and-frozen-contracts/01-CONTEXT.md (D-10 catalogue and retryable lists, D-11 edge table) - package.json (test script invoking the Node built-in test runner) - tsconfig.json (strict + ESM constraints the new files compile under) </read_first> <action>Write the failing end-to-end test first: one test file importing the states table and the error catalogue together, asserting the single happy path (unbound -> connecting accepted) plus one rejected pair rejected with REALM_INVALID_TRANSITION plus one RealmError construction with non-empty code and reason; run it red against not-yet-existing source. Then write the minimal src/core/states.ts transition-check function with the exact D-11 allowed edges and src/core/errors.ts RealmError shape with the code-to-retryable lookup covering the exact 20 D-10 codes, and run the slice green with npm test. This tracer proves the scaffold compiles real contract source and runs real contract tests end to end; the remaining tasks expand it to full coverage. Reversibility costly per D-10 and D-11: codes and edges are machine-readable and imported by every module, so renames propagate everywhere.</action> <acceptance_criteria> - Test run observed red before source existed and green after, recorded in the task output - npm test passes for the tracer slice; npm run typecheck and npm run lint pass - No @ts-ignore, eslint-disable, skip, or todo in any touched file </acceptance_criteria> <verify> <automated>npm test -- test/core/states.test.ts test/core/errors.test.ts</automated> <fails_when>non-zero exit, or "0 passed" in the summary line</fails_when> </verify> <done>The single happy path plus one rejection plus one typed error construct and pass end to end on the scaffold; RED then GREEN observed.</done> </task>
<task type="auto" tdd="true"> <name>Task 2: Expand to the full transition matrix and catalogue assertions</name> <files>src/core/states.ts, src/core/errors.ts, test/core/states.test.ts, test/core/errors.test.ts</files> <read_first> - .planning/phases/01-scaffold-and-frozen-contracts/01-CONTEXT.md (D-10 true/false retryable lists, D-11 exact edges; costly reversibility) - src/core/states.ts (tracer slice to expand, not replace) - src/core/errors.ts (tracer slice to expand, not replace) </read_first> <behavior> - Test: all 36 ordered state pairs checked; the 12 D-11 allowed edges accepted, the other 24 rejected with REALM_INVALID_TRANSITION - Test: all 20 codes present, no 21st code constructible; retryable matches the D-10 lists entry by entry - Test: every catalogue entry yields a RealmError with non-empty code and non-empty reason </behavior> <action>Extend the tracer tests to the full 36-pair matrix (12 allowed accepted, 24 disallowed rejected including the six self-transitions per D-11) and to whole-catalogue assertions (exactly the 20 D-10 codes, retryable values matching the D-10 true/false lists, non-empty code and reason per entry per QUAL-01); extend source only as needed to pass, keeping the transition check a pure pair lookup so output order is irrelevant (FOUND-03/QUAL-01 ordering edges are not applicable: no sorted output exists) and keeping all numerics out of these files (QUAL-01 precision edge is not applicable: no numbers, no rounding). No behavior, events, timestamps, or display mapping.</action> <acceptance_criteria> - test/core/states.test.ts asserts all 36 ordered pairs with the 12 allowed edges accepted - test/core/errors.test.ts asserts exactly 20 codes, the D-10 retryable mapping, and non-empty code plus reason for every entry - npm test, npm run typecheck, and npm run lint all exit 0 </acceptance_criteria> <verify> <automated>npm test -- test/core/states.test.ts test/core/errors.test.ts</automated> <fails_when>non-zero exit, or "0 passed" in the summary line</fails_when> </verify> <done>Full pair matrix and full catalogue asserted; all green with no suppressions.</done> </task>
</tasks>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| src/core -> all modules | Core tables are trusted imports; they must contain no executable input handling |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-01-07 | T (Tampering) | src/core/states.ts edge table | medium | mitigate | Full 36-pair matrix test pins the table; any edge change fails loudly |
| T-01-08 | T (Tampering) | src/core/errors.ts catalogue | medium | mitigate | Whole-catalogue test pins exactly 20 codes with retryable values; additions fail |
| T-01-SC | T (Tampering) | npm installs | high | mitigate | No installs in this plan; toolchain comes from plan 01-03 only |
| </threat_model> |
<verification> npm run typecheck, npm run lint, and npm test all exit 0; grep for suppressions returns nothing; catalogue holds exactly the 20 D-10 codes. </verification>
<success_criteria> D-11 table and D-10 catalogue frozen with RED-then-GREEN evidence, full matrix and catalogue coverage, no behavior beyond the tables, no suppressions. </success_criteria>
<output> Create .planning/phases/01-scaffold-and-frozen-contracts/01-04-SUMMARY.md when done </output>
Artifacts this phase produces
- src/core/states.ts (six states, pure transition-check function, exported D-11 allowed-edge table; costly reversibility)
- src/core/errors.ts (RealmError { code, reason, retryable }, single code-to-retryable lookup, exactly 20 D-10 codes; costly reversibility)
- test/core/states.test.ts (36-pair matrix: 12 allowed accepted, 24 rejected incl. self-transitions)
- test/core/errors.test.ts (20 codes present, retryable mapping, non-empty code and reason per entry)