fix(#3685): report phase-complete write flags from the transaction, not the filesystem (#3826)

* test(#3685): failing-first regression coverage for phase-complete write flags

`phase complete` reports `roadmap_updated`/`state_updated` from
`fs.existsSync(path)`, so both read `true` whenever the file merely exists —
including when the transaction wrote nothing. Add the regression tests that
prove it, plus the negative-space and true-direction pins, before the fix.

New in tests/phase.test.cjs:
- roadmap_updated is false when the transaction rewrites nothing (FAILS today)
- state_updated is false when the transaction rewrites nothing, and stays
  false on a third consecutive run (FAILS today)
- both flags are true when the transaction genuinely rewrites (pins the true
  direction so the fix cannot be tightened into always-false)
- each flag stays false when its file is absent

The STATE.md cases pin the clock via GSD_TEST_MODE + GSD_NOW_MS
(src/clock.cts:43-70) because syncStateFrontmatter stamps a
millisecond-resolution `last_updated:` on every write, which would otherwise
make the no-op unobservable.

Also strengthens four pre-existing `=== true` assertions on these fields that
passed vacuously: each now pairs the flag assertion with a content-changed
assertion against a pre-call snapshot, so the `true` is earned.

Refs #3685

* fix(#3685): report phase-complete write flags from the transaction, not the filesystem

`phase complete` computed `roadmap_updated` and `state_updated` as
`fs.existsSync(path)`, so both read `true` for any project that had the file at
all — including a run that rewrote nothing. The flags are the only signal a
caller has that the rollup landed, so a no-op was indistinguishable from a
successful write and a stale ROADMAP went unnoticed until something downstream
read wrong numbers.

Both flags now reflect whether that file's content actually changed in the
transaction, computed at the existing `writes.push({filePath, before, after})`
sites — the same contract `requirements_updated` has honored since #2316-3, and
the same correction #2640/#2974 already applied to `phase remove`.

Nothing about what gets written changes; only what gets reported.

Fixes #3685

* chore(changeset): backfill PR number for #3685 fragment

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-24 18:05:35 -04:00
committed by GitHub
parent 596540f864
commit 7a41248c4f
3 changed files with 159 additions and 2 deletions

View File

@@ -2261,6 +2261,12 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
? (phaseInfo['summaries'] as string[]).length
: 0;
let requirementsUpdated = false;
// #3685: mirror requirementsUpdated's diff-tracking contract at the
// writes.push({filePath, before, after}) sites below, rather than
// reporting via fs.existsSync (which is true whenever the file merely
// exists, not when the transaction actually wrote a change).
let roadmapUpdated = false;
let stateUpdated = false;
const warnings: string[] = [];
// ADR-3408 §8.5 / D2 (#3374): "liberal but visible" — when the write-seam
@@ -2680,6 +2686,7 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
before: originalRoadmapContent,
after: roadmapContent,
});
roadmapUpdated = roadmapContent !== originalRoadmapContent;
const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
if (fs.existsSync(reqPath)) {
@@ -3253,6 +3260,7 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
}
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
stateUpdated = stateContent !== originalStateContent;
}
anyPlanningWrite = writePlanningFileSet(writes) > 0;
@@ -3319,8 +3327,8 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
next_phase_name: nextPhaseName,
is_last_phase: isLastPhase,
date: today,
roadmap_updated: fs.existsSync(roadmapPath),
state_updated: fs.existsSync(statePath),
roadmap_updated: roadmapUpdated,
state_updated: stateUpdated,
requirements_updated: requirementsUpdated,
auto_pruned: autoPruned,
warnings,