Migrate the STATE.md write path inside cmdMilestoneComplete (milestone.cts)
onto the STATE.md Transition Module substrate (ADR-1769, epic #1769).
- Add {kind: 'milestoneComplete'} to StateTransitionIntent, with
milestoneCompleteCore in src/state-transition.cts (consulting the
field-classification table). Owns the closure write: Status
('<version> milestone complete'), Last Activity, Last Activity Description,
a Current Position reset to 'Awaiting next milestone', and an Operator Next
Steps reset pointing at the next-milestone command.
- Collapse the inline STATE.md transform in cmdMilestoneComplete to a
transitionCore dispatch. The adapter retains writeStateMd (lock + steady-state
syncStateFrontmatter post-sync) and resolves the runtime-specific
next-milestone slash command, injecting it via intent.nextMilestoneCommand.
- The two section resets carry their pre-seam allow-adhoc-markdown waivers
(regex semantics pinned by existing tests; pending collectSection #1372).
- realClock.today() is byte-identical to milestone.cts's local today
(both new Date().toISOString().split('T')[0]).
- Characterization tests pin field updates, both section resets (replace +
insert paths), and frontmatter #1255 parity.
All 66 transition + milestone + state tests pass.
Closes#1789
Migrate cmdStatePlannedPhase and cmdStateMilestoneSwitch (state.cts) onto the
STATE.md Transition Module substrate (ADR-1769, epic #1769).
- Add {kind: 'plannedPhase'} and {kind: 'milestoneSwitch'} to
StateTransitionIntent, with plannedPhaseCore and milestoneSwitchCore in
src/state-transition.cts (consulting the field-classification table).
- plannedPhaseCore owns the template-aware Status/Last Activity updates, Total
Plans in Phase, Last Activity Description, and the Current Position section
(via the inlined mutateCurrentPositionForAdvance twin). The adapter keeps the
resync:false readModifyWriteStateMd wrapper (#500 RC1).
- milestoneSwitchCore owns the new-milestone reset: rebuilt frontmatter
(milestone/name/status='planning'/zeroed progress) + Current Position body
reset. The adapter keeps acquireStateLock + platformWriteSync (NOT
readModifyWriteStateMd — milestoneSwitch rebuilds frontmatter directly).
- Collapse both callbacks to transitionCore dispatches. The now-dead
updateCurrentPositionFields helper and KNOWN_STATUS_PATTERNS import are
removed (their behavior lives in the transition module's
mutateCurrentPositionForAdvance).
- Characterization tests pin the template-aware preserve-authored invariant,
Total Plans, Last Activity narrative, Current Position update, and the full
milestone reset (frontmatter + position body + gsd_state_version preserve +
Accumulated Context preserve).
All 58 transition + 177 state + phase + bug-2630/bug-905 regression tests pass.
Closes#1786
Migrate the STATE.md write path inside cmdPhaseComplete (phase.cts) onto the
STATE.md Transition Module substrate (ADR-1769, epic #1769).
- Add {kind: 'completePhase'} to StateTransitionIntent + completePhaseCore in
src/state-transition.cts. Pure body field mutations (Current Phase shape/name,
Status, Current Plan, Last Activity + Description, Completed/Total Phases +
Progress percent), consulting the field-classification table for touched keys.
Roadmap progress injected via a new optional deps.roadmapProvider.
- Collapse the ~90-line inline STATE.md transform in cmdPhaseComplete to a
transitionCore dispatch. The adapter retains updatePerformanceMetricsSection
(section table) + syncStateFrontmatter (disk-scan post-sync) and the
multi-file atomic transaction (STATE is committed with ROADMAP/REQUIREMENTS,
so readModifyWriteStateMd is not used here).
- deriveProgressFromRoadmap/clampPercent/stateReplaceFieldWithFallback move from
the phase.cts call site into the pure core (no circular dep: phase-lifecycle
and state-document don't import state.cts).
- Characterization tests in tests/state-transition.test.cjs pin the field
updates, roadmap progress derivation, #1255 frontmatter parity, and the
'Phase:' fallback. All 44 transition + 193 phase tests pass.
No user-visible behavior change. cmdPhaseComplete CLI output and 'phase complete'
behavior unchanged.
Closes#1784
* refactor(#1771): ADR-1769 Phase 1 — STATE.md Transition Module substrate + beginPhase
Lands the Phase 1 substrate per ADR-1769:
- src/state-transition.cts (new Module):
- Field-classification table (FieldClass enum + FIELD_CLASSIFICATION rows)
- STATE_MD_SECTIONS constants block
- Pure transitionCore(content, intent, deps) dispatch
- beginPhase intent implementation (first-time + #3127 resume paths)
- src/state.cts:cmdStateBeginPhase — collapses ~190 lines to a thin
dispatch onto transitionCore via readModifyWriteStateMd. The lock,
no-op write guard, and #1230 post-sync delta heuristic stay in the
RMW seam; the body-mutation policy moves to transitionCore.
- tests/state-transition.test.cjs (24 tests):
- Substrate invariants (table enum, section constants)
- Characterization: 6 first-time body field updates
- Characterization: 5 #3127 idempotency-guard resume behaviors
- Characterization: 3 Current Position section mutations
- Characterization: Current focus body text line (#1104)
- Property (RULESET.TESTS.property-based-testing): beginPhase status
propagation + FIELD_CLASSIFICATION own-property contract
- Resume Current Position mutation (preserves Plan/Phase/Status)
No external behavior change. Full state.test.cjs regression (177 tests)
plus bug-3127/#3242/#905/#948 pass. Property tests surfaced two
pre-existing quirks (state-document.cjs greedy \s* on whitespace-only
field values; Object.prototype method leakage on FIELD_CLASSIFICATION
lookups for strings like 'toString') — documented in test comments;
fix-out-of-scope for Phase 1.
Closes#1771
* refactor(#1771): ADR-1769 Phase 1 codex review corrections
Addresses 3 blocking findings from codex gpt-5.5/high review:
1. FIELD_CLASSIFICATION shape (state-transition.cts):
- Was flat FieldClass enum (collapsed source + preservation)
- Now two-column {source, preservation} rows per ADR-1769 §4
- Added missing fields verified via Memtrace against
buildStateFrontmatter (state.cts:1633-1653): gsd_state_version,
last_updated, last_activity_desc, progress.{total_phases,
completed_phases, total_plans, completed_plans, percent}
- Field 2-7 preservation dispatch can now consult the table
2. Prototype-pollution hardening (state-transition.cts):
- Table is now Object.freeze(Object.assign(Object.create(null), {...}))
- getFieldClassification() helper uses Object.hasOwn; returns null
for inherited prototype methods (toString/valueOf/__proto__)
- Old code: FIELD_CLASSIFICATION['toString'] returned the function
3. STATE_MD_SECTIONS aligned to canonical template:
- Verified against gsd-core/templates/state.md via Memtrace
- Was: 8 entries including non-template sections (## Session,
## Decisions, ## Operator Next Steps, ## Session Log,
## Roadmap Evolution)
- Now: 6 canonical top-level sections (## Project Reference,
## Current Position, ## Performance Metrics, ## Accumulated
Context, ## Deferred Items, ## Session Continuity)
Also: beginPhase now consults getFieldClassification() per touched
field (codex finding: 'table not consulted by transitionCore').
Unknown fields raise immediately — adding a field without a table
row is caught at runtime.
Repo-hygiene catches from gsd-test (not node --test, which missed
these):
- gsd-core/bin/lib/state-transition.cjs added to eslint.config.mjs
ignore list (ADR-457 tsc-generated)
- docs/INVENTORY-MANIFEST.json regenerated via
node scripts/gen-inventory-manifest.cjs --write
Property test for Object.prototype leakage tightened to verify
getFieldClassification() returns null for toString/valueOf/__proto__.
Ref #1771
* fix(#1771): cast Object.create(null) to satisfy @typescript-eslint/no-unsafe-assignment
ESLint CI failed on src/state-transition.cts:72:14 — Object.create(null)
returns `any`, which leaked through Object.assign to the typed
`FIELD_CLASSIFICATION` declaration. Adding an explicit cast to
`Record<string, FieldClassification>` eliminates the unsafe-assignment
while preserving the null-prototype protection codex review recommended.
gsd-test: 21888/21888 PASS.
* fix(#1771): add 'see #1771' to allow-test-rule exemption per ADR-456
CI lint-allow-test-rule-refs failed: 'New allow-test-rule exemption
without an issue ref — add `see #NNN` per ADR-456'. Updated comment
on tests/state-transition.test.cjs to reference the Phase 1 issue.
* fix(#1771): remove unnecessary allow-test-rule exemption
The exemption was added speculatively. The test file does not use
readFileSync + .includes()/.match()/.startsWith() on source content —
it calls transitionCore() with string literals and verifies results
via stateExtractField() and array .includes() on the updated[] array.
No exemption needed.