Files
msd-core/tests/state-write-path-drift-guard.test.cjs
Tom Boucher 1863f5569c enhance(#3871): the state transaction — mandatory snapshot, open()/rebuild() (#3874)
* test(#3871): failing-first regressions for the dropped curated progress block

Pins ADR-3473 §8.6 / #3756 at the consumer's output: state record-session and
state add-decision on an archived-milestone project drop the curated progress
frontmatter entirely, exit 0, and report nothing. Reproduced against the real
CLI before writing the tests, not inferred from the issue text.

Also adds the unit-level probe that applyStatePreservation's preserve-always
row is inert on a resyncing write, and an over-preservation guard that an
empty project is never inflated.

Refs #3871

* feat(#3871): make the STATE.md pre-write snapshot mandatory via open()/rebuild()

ADR-3473 §8.6. StatePreservationInput's nullable preFm and the always-present
preFmSnapshot were the same extractFrontmatter call, one of them nulled on
resync — a policy flag baked into a snapshot. Both collapse into a single
StateTransaction whose snapshot cannot be absent: openStateTransaction()
applies preservation, rebuildStateTransaction() does not, and both carry the
snapshot because the reporting phase needs it either way. An absent snapshot
is now a construction failure; an empty one stays legal, because that is what
a document with no parseable frontmatter honestly has.

writeStateMd requires a rebuild transaction, which types ADR-3408 §8.3's
closed exception list at both call sites (state sync, health --repair) instead
of matching them as strings in a ratcheted baseline.

Fixes the dropped curated progress block: an all-zero or absent derived total
set is an unmeasured scan, not a measurement, so the curated block stands.
Also fixes two defects surfaced while building — preserve-always reported a
mutation even when it restored an identical value, and it re-entered the
curated object by reference, which would alias the snapshot the next phase
diffs against.

Refs #3871

* fix(#3871): close the three remaining subsumed defects and restore the arm the type does not replace

Review of the first two commits found four things.

The guard shrink deleted the seam-bypass axis whole, but only its
writeStateMd( arm became redundant. Its other arm catches a call site
re-assembling syncStateFrontmatter + applyPostSyncPreservation instead of the
owned composition, which the transaction type does not make unrepresentable
and which #3469 found live. Restored as findCompositionBypasses, terminal
rather than ratcheted.

Three of the four issues this phase claims were untouched. All three are the
epic's own shape and are fixed at the seam: current_phase_name is reasserted
from the curated value when the caller names none, and cmdStateJson stops
carrying a hand-maintained list parallel to FIELD_CLASSIFICATION and projects
it instead.

The construction failure that is the point of this phase had no test. Every
enumerated matrix row now has one, including the measured-versus-unmeasured
coercion boundary and a seeded property that no curated key is ever dropped.

ADR-3473 §8.6 said the guard 'keeps only its raw-write check'. Verified
against next: there was no raw-write check, and four other checks it does not
name. Amended in place with the evidence. ARCHITECTURE.md separately
advertised a preservation policy the code had deleted.

Refs #3871

* fix(#3871): do not let the unmeasured-scan rule block an explicitly-requested resync

The remote matrix caught over-preservation, the failure this phase's own
negative space says must not happen. state update Progress re-derives the
block from the body the caller just rewrote; on a project with no phase dirs
the derivation yields zero totals, the unmeasured rule read that as 'the scan
measured nothing', and the stale curated percent was restored over the resync
the user asked for.

preserve-always already said what the missing condition was: never overwrite
unless the caller explicitly names this field. explicitProgressField carries
it and is derived from shouldResyncStateProgress, not set by hand at a call
site, so it cannot drift from what the caller asked for.

Two defects found in the same mechanism and fixed with it. readModifyWriteStateMd
enumerates its option keys, so a new option was silently dropped rather than
rejected. And the raw-write axis captured its first argument up to the first
comma, which lands inside a nested path.join, so a write to a STATE.md literal
was invisible to it — the prove-it-can-fail test caught that one immediately.

No test assertion was weakened; all three frontmatter rows encode #3242, #1969
B3 and #1972 and stand unchanged.

Refs #3871

* docs(#3871): record why the raw-write check is kept, not why it was named

The amendment justified findRawStateWrites as 'written because §8.6 requires
it to exist', which is cargo-culting the contract and would have been the
wrong reason to keep anything. The real reason is that writeStateMd acquires
the STATE.md lockfile and a raw fs.writeFileSync acquires nothing, so this is
a lock bypass and lost-update is the #500/#905/#1230 family — and after this
phase it is the one reachable path into the file that nothing else covers.

Also records why ADR-3408 §8.6's deletion of the 'clear' policy is not the
precedent it looks like: 'clear' was dead vocabulary in a closed enum, this is
coverage of a reachable path.

Refs #3871

* chore(#3871): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
2026-08-25 20:38:12 -04:00

30 KiB