fix(#2640): report truthful state_updated + keep progress frontmatter in sync after phase remove (#2974)

* test(#2640): add regression for state_updated false positive + stale progress

Three cases: (1) state_updated reflects actual content change, not just
file existence; (2) progress.total_phases resync'd even when the body lacks
'Total Phases:' (the no-op guard was skipping syncStateFrontmatter);
(3) state_updated is false when STATE.md doesn't exist.

* fix(#2640): report truthful state_updated + force frontmatter resync

Two defects in cmdPhaseRemove:

1. state_updated was fs.existsSync(statePath) — trivially true, since the file
   existed before and readModifyWriteStateMd never deletes it. Now captures
   the boolean return from readModifyWriteStateMd (changed from void to
   boolean: true when content was written, false on no-op).

2. progress.* frontmatter stayed stale when the body lacked 'Total Phases:'
   or 'of N' — readModifyWriteStateMd's no-op guard (#948) skipped
   syncStateFrontmatter when the body transform was unchanged. Now the
   transform forces a body diff when a phase was actually removed, so the
   guard passes and syncStateFrontmatter rebuilds progress.* from the
   post-deletion disk/ROADMAP state.

* fix(#2640): address review — gate forced-diff on targetDir, strengthen assertions

Two MAJOR findings from isolated adversarial review:
1. Forced-diff injected a spurious 'Total Phases:' line even when no directory
   was removed (targetDir === null). Now gated on targetDir !== null.
2. Test #2 asserted 'not 3' instead of '2' — would pass for any wrong count.
   Now asserts exact value. Test #1 strengthened to assert body Total Phases
   and frontmatter total_phases both equal 1.

* chore(#2640): add changeset fragment

* chore(#2640): backfill changeset PR number 2974

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-01 11:56:34 -04:00
committed by GitHub
parent 000a322489
commit f0bb0787c9
4 changed files with 124 additions and 11 deletions

View File

@@ -2213,7 +2213,7 @@ function writeStateMd(statePath: string, content: string, cwd?: string, clock?:
* @param clock
* Optional clock seam; defaults to realClock. Passed through to acquireStateLock.
*/
function readModifyWriteStateMd(statePath: string, transformFn: (content: string) => string, cwd: string, options?: ReadModifyWriteOptions, clock?: StateLockClock): void {
function readModifyWriteStateMd(statePath: string, transformFn: (content: string) => string, cwd: string, options?: ReadModifyWriteOptions, clock?: StateLockClock): boolean {
const resync = !options || options.resync !== false;
const lockPath = acquireStateLock(statePath, clock);
try {
@@ -2261,7 +2261,7 @@ function readModifyWriteStateMd(statePath: string, transformFn: (content: string
// content already returns the mutated string, and callers that detect a
// no-op explicitly return the original content unchanged.
if (modified === content) {
return;
return false;
}
let synced = syncStateFrontmatter(modified, cwd, options?.authoritativeFm);
@@ -2317,6 +2317,7 @@ function readModifyWriteStateMd(statePath: string, transformFn: (content: string
}
platformWriteSync(statePath, synced);
return true;
} finally {
releaseStateLock(lockPath);
}