refactor(#1786): ADR-1769 Phase 4 — plannedPhase + milestoneSwitch migration (#1788)

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
This commit is contained in:
Tom Boucher
2026-06-27 15:24:18 -04:00
committed by GitHub
parent 6f80524be1
commit 91704c9fcf
5 changed files with 567 additions and 204 deletions

View File

@@ -746,3 +746,172 @@ describe('ADR-1769 Phase 3: completePhase edge cases', () => {
assert.strictEqual(stateExtractField(result.content, 'Current Phase'), '4');
});
});
// ADR-1769 Phase 4: plannedPhase + milestoneSwitch
function plannedPhaseBody() {
return [
'# Project State',
'',
'**Status:** Planning',
'**Total Plans in Phase:** 0',
'**Last Activity:** 2026-06-20',
'**Last Activity Description:** previous planning',
'',
'## Current Position',
'',
'Phase: 3 (Test Phase) — EXECUTING',
'Plan: —',
'Status: Executing Phase 3',
'Last activity: 2026-06-20 — mid-flight',
'',
].join('\n');
}
describe('ADR-1769 Phase 4: plannedPhase transition — body field updates', () => {
const deps = { clock: fixedClock, progressProvider: noProgress };
test('Status advances to "Ready to execute" when the existing value is a template default (Planning)', () => {
const result = transitionCore(plannedPhaseBody(), { kind: 'plannedPhase', phaseNumber: 3, planCount: 4 }, deps);
assert.strictEqual(stateExtractField(result.content, 'Status'), 'Ready to execute');
assert.ok(result.updated.includes('Status'));
});
test('Total Plans in Phase is set to planCount', () => {
const result = transitionCore(plannedPhaseBody(), { kind: 'plannedPhase', phaseNumber: 3, planCount: 4 }, deps);
assert.strictEqual(stateExtractField(result.content, 'Total Plans in Phase'), '4');
assert.ok(result.updated.includes('Total Plans in Phase'));
});
test('Last Activity is refreshed to clock.today() when the existing value is a date (template default)', () => {
const result = transitionCore(plannedPhaseBody(), { kind: 'plannedPhase', phaseNumber: 3, planCount: 4 }, deps);
assert.strictEqual(stateExtractField(result.content, 'Last Activity'), '2026-06-27');
});
test('Last Activity Description carries the planning-complete narrative', () => {
const result = transitionCore(plannedPhaseBody(), { kind: 'plannedPhase', phaseNumber: 3, planCount: 4 }, deps);
assert.strictEqual(
stateExtractField(result.content, 'Last Activity Description'),
'Phase 3 planning complete — 4 plans ready',
);
});
test('Current Position Status + Last activity are updated', () => {
const result = transitionCore(plannedPhaseBody(), { kind: 'plannedPhase', phaseNumber: 3, planCount: 4 }, deps);
assert.ok(result.updated.includes('Current Position'),
`updated should include Current Position; got ${JSON.stringify(result.updated)}`);
// The Current Position section should now carry the planning-complete narrative.
assert.ok(/Phase 3 planning complete/.test(result.content));
});
test('executor-authored Status is preserved (Knuth invariant — non-template value not overwritten)', () => {
const custom = plannedPhaseBody().replace('**Status:** Planning', '**Status:** Awaiting human design review');
const result = transitionCore(custom, { kind: 'plannedPhase', phaseNumber: 3, planCount: 4 }, deps);
assert.strictEqual(stateExtractField(result.content, 'Status'), 'Awaiting human design review');
assert.ok(!result.updated.includes('Status'),
`Status must not be in updated for an executor-authored value; got ${JSON.stringify(result.updated)}`);
});
test('planCount=null leaves Total Plans in Phase untouched', () => {
const result = transitionCore(plannedPhaseBody(), { kind: 'plannedPhase', phaseNumber: 3, planCount: null }, deps);
assert.strictEqual(stateExtractField(result.content, 'Total Plans in Phase'), '0');
assert.ok(!result.updated.includes('Total Plans in Phase'));
});
test('frontmatter is preserved and body Status (not YAML status) is updated (#1255)', () => {
const input = [
'---',
'status: planning',
'---',
'',
'# Project State',
'',
'**Status:** Planning',
'**Total Plans in Phase:** 0',
'**Last Activity:** 2026-06-20',
'**Last Activity Description:** prev',
'',
'## Current Position',
'',
'Status: Executing Phase 3',
'Last activity: 2026-06-20 — mid',
'',
].join('\n');
const result = transitionCore(input, { kind: 'plannedPhase', phaseNumber: 3, planCount: 2 }, deps);
assert.strictEqual(stateExtractField(result.content, 'Status'), 'Ready to execute');
assert.ok(/^---\r?\n[\s\S]*?\r?\n---/.test(result.content), 'frontmatter block preserved');
});
});
describe('ADR-1769 Phase 4: milestoneSwitch transition — milestone reset', () => {
const deps = { clock: fixedClock, progressProvider: noProgress };
function milestoneBody() {
return [
'---',
'gsd_state_version: 1.0',
'milestone: v1.0',
'milestone_name: Old Milestone',
'status: executing',
'current_phase: "3"',
'progress:',
' total_phases: 5',
' completed_phases: 2',
' percent: 40',
'---',
'',
'# Project State',
'',
'## Current Position',
'',
'Phase: 3 — EXECUTING',
'Plan: 2 of 5',
'Status: Executing Phase 3',
'Last activity: 2026-06-20 — mid-flight',
'',
].join('\n');
}
test('frontmatter milestone + milestone_name are reset to the new version', () => {
const result = transitionCore(milestoneBody(), { kind: 'milestoneSwitch', version: 'v2.0', name: 'New Milestone' }, deps);
const fmLine = (key) => result.content.split('\n').find((l) => new RegExp(`^${key}:`).test(l));
assert.strictEqual(fmLine('milestone'), 'milestone: v2.0');
assert.strictEqual(fmLine('milestone_name'), 'milestone_name: New Milestone');
});
test('frontmatter status resets to planning and progress resets to zero', () => {
const result = transitionCore(milestoneBody(), { kind: 'milestoneSwitch', version: 'v2.0', name: 'New Milestone' }, deps);
assert.strictEqual(result.content.split('\n').find((l) => /^status:/.test(l)), 'status: planning');
assert.ok(/total_phases:\s*0/.test(result.content), 'total_phases should reset to 0');
assert.ok(/completed_phases:\s*0/.test(result.content), 'completed_phases should reset to 0');
assert.ok(/percent:\s*0/.test(result.content), 'percent should reset to 0');
});
test('gsd_state_version is preserved across the reset', () => {
const result = transitionCore(milestoneBody(), { kind: 'milestoneSwitch', version: 'v2.0', name: 'New Milestone' }, deps);
assert.ok(/gsd_state_version:\s*1\.0/.test(result.content), 'gsd_state_version must be preserved');
});
test('Current Position section is reset to "Not started (defining requirements)"', () => {
const result = transitionCore(milestoneBody(), { kind: 'milestoneSwitch', version: 'v2.0', name: 'New Milestone' }, deps);
assert.ok(/Phase: Not started \(defining requirements\)/.test(result.content));
assert.ok(/Status: Defining requirements/.test(result.content));
assert.ok(new RegExp(`Last activity: 2026-06-27 — Milestone v2.0 started`).test(result.content));
});
test('Accumulated Context / body content outside Current Position is preserved', () => {
const input = milestoneBody() +
'\n## Accumulated Context\n\n- An important decision we must keep.\n';
const result = transitionCore(input, { kind: 'milestoneSwitch', version: 'v2.0', name: 'New Milestone' }, deps);
assert.ok(/An important decision we must keep/.test(result.content),
'Accumulated Context must survive the milestone reset');
});
test('blank name falls back to the "milestone" placeholder', () => {
const result = transitionCore(milestoneBody(), { kind: 'milestoneSwitch', version: 'v2.0', name: '' }, deps);
assert.strictEqual(
result.content.split('\n').find((l) => /^milestone_name:/.test(l)),
'milestone_name: milestone',
);
});
});