* fix(#1761): skip conflated progress in state json read-path when milestone unbounded
ADR-1769 Phase 7 (#1794) closed the state sync WRITE path — when a milestone
version is asserted in frontmatter but the ROADMAP has no versioned heading
for it, sync leaves Progress untouched. But the state json READ path rebuilds
progress via buildStateFrontmatter, whose roadmapPhaseCount loop counts phase
headings across the WHOLE document when extractCurrentMilestone can't bound
the milestone. state json therefore reported a conflated total_phases (sum of
sibling milestones) + a derived percent — exactly the value the sync guard
was added to prevent. (Repro from the issue: total_phases 8 = 4+4, percent 13.)
Mirror the cmdStateSync guard inside buildStateFrontmatter: when the asserted
milestone cannot be bounded to a versioned ROADMAP heading (the same
versionedHeading test the sync path uses), fall back to the on-disk
phase-dir count for total_phases and skip percent. Bounded milestones
(versioned ROADMAP, or no milestone asserted) are unchanged. The signal rides
on the existing _diskScanCache (new milestoneBounded field) so neither
extractCurrentMilestone's return contract nor its other callers change.
Regression: extend tests/bug-1761-state-sync-wrong-progress.test.cjs with the
read-path case (unbounded → no percent, no conflated total_phases) and a
bounded control (versioned ROADMAP → unchanged percent + total_phases).
* docs(#1761): add changeset fragment for state json read-path fix
Migrate cmdStateSync, cmdStatePrune, cmdStateUpdate (state.cts) onto the
STATE.md Transition Module substrate and close the maintenance bug pair
#1760/#1761 (ADR-1769, epic #1769). Completes the substrate: all 10
lifecycle/maintenance transitions now route through transitionCore.
- Add {kind: 'sync'|'prune'|'update'} to StateTransitionIntent with syncCore,
pruneCore, updateCore in src/state-transition.cts.
- updateCore: body-only single-field update (strip/reassemble), mirroring the
pre-migration cmdStateUpdate contract.
- pruneCore: pure content→content section pruning (Decisions / Recently
Completed / resolved Blockers / Performance Metrics rows at or below cutoff),
byte-identical tokenizeHeadings splicing. Adapter owns currentPhase, dry-run,
and STATE-ARCHIVE.md.
- syncCore: body writes (Total Plans in Phase, Progress bar, Last Activity)
given disk-derived numbers. Adapter owns the disk scan + roadmap scope.
- #1760: cmdStatePrune now derives currentPhase from 'Current Phase' OR 'Phase'
(the canonical template emits 'Phase: X of Y'), so prune engages on
template-conformant STATE.md instead of bailing 'Only 0 phases'.
- #1761: cmdStateSync skips the Progress write (percent=null) when a milestone
version is set in frontmatter but the ROADMAP has no versioned heading for it
(milestone cannot be bounded). Projects without a milestone version are
unaffected. Leaves progress untouched rather than silently writing fallback-
derived wrong values.
- Regression: bug-1760 (prune engages on template field) + bug-1761 (sync
leaves progress untouched when unbounded). ADR-1769 marked Accepted.
All 82 transition + 515 regression tests pass.
Closes#1793