fix(#4488): report state update as successful when the value is already correct (#4581)

* fix(#4488): report state update as successful when the value is already correct

`cmdStateUpdate` unconditionally overwrote `updateCore`'s own `updated:true`
signal with `reconcileReportedFields`'s disk-diff result. That diff reports
`[]` -- by design -- whenever `readModifyWriteStateMd`'s #948 no-op guard
fires because the transform's output was byte-identical to the input, which
happens precisely when the requested value already equals what's on disk.
The field genuinely was found and matched; there was simply nothing left to
change. Collapsing that into the same `false`/"not found" response as a
genuine miss produced an actively wrong diagnostic message and a silent
same-day no-op in gsd-ship + gsd-extract-learnings, which both write
`Last Activity` to today's date.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4488): backfill changeset pr number to 4581

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4488): untrack tdd-red-evidence.cjs, completing its ADR-457 gitignore migration

Bundled discovery from this PR's own CI run: tests/lint-compiled-artifact-
sync.test.cjs's full tsc compile (which runs whenever ANY compiled artifact
remains tracked) SIGTERM'd under shard contention. gsd-core/bin/lib/tdd-red-
evidence.cjs (introduced by #3770/PR #4279) was the sole remaining tracked
artifact -- a tenth, later, separate instance of the #2657/#2653
migration-gap defect class this test file's closed nine-item list doesn't
cover. Untracked it and added the .gitignore entry, same fix shape as the
original nine. This eliminates the slow tsc-compile path entirely (verified:
0.1s vs ~7s locally) rather than papering over a timeout. Added a generic
regression test asserting the tracked set is fully empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-09 18:32:54 -04:00
committed by GitHub
parent 615b74ff45
commit 7fe440a838
6 changed files with 221 additions and 142 deletions

View File

@@ -808,7 +808,31 @@ function cmdStateUpdate(cwd: string, field: string | undefined, value: string |
// (#3345's direction) — reported separately from `updated` because this
// command's contract is a single-field boolean, not a per-field array.
const reconciled = reconcileReportedFields(statePath, preWriteState, updated ? [field as string] : [], divergedFields);
updated = reconciled.includes(field as string);
// #4488: `updateCore` itself already told us whether it matched the field
// (`updated`, captured above `readModifyWriteStateMd` runs it) — that is a
// real signal, not a guess. `reconcileReportedFields` answers a DIFFERENT
// question ("what changed on disk") and, per its own docstring, reports
// `[]` whenever `preWriteState.fm` is `undefined`. That happens in two
// known cases, both of which mean "no snapshot was ever captured", not
// "nothing happened": (a) `readModifyWriteStateMd`'s #948 no-op guard
// fires because the transform's output was byte-identical to the input —
// the requested value already equals what's on disk, so the field WAS
// found and there was simply nothing left to change; (b)
// `applyPostSyncPreservation`'s `isUnparseableFrontmatter` early return —
// the ORIGINAL frontmatter block was malformed, so preservation never
// runs, yet `readModifyWriteStateMd` still persists the transform's raw
// output via `platformWriteSync`. In neither case did preservation
// discard or rewrite what the transform wrote, so trusting the
// transform's own `updated` signal here is never a false positive.
// Collapsing either case into the same `false` as "field not found" is
// the #4488 bug — `explainUpdateFailure` then reports a message that is
// actively false (it tells the caller to add a line that is already
// there). Every other `false` origin (case-D fallback did not apply, or
// the transform genuinely found nothing) is unaffected: there
// `preWriteState.fm` is defined (a normal sync ran) or `updated` was
// already false before this line.
const noopBecauseAlreadyCorrect = updated && preWriteState.fm === undefined;
updated = reconciled.includes(field as string) || noopBecauseAlreadyCorrect;
const preserved = reconciled.filter((f) => f !== field);
if (updated) {