Files
msd-core/docs/how-to/interpret-state-validate-results.md
Tom Boucher 693f12ad56 refactor(#3187): give state field extraction one canonical owner (#3283)
* refactor(#3187): give state field extraction one canonical owner

stateFieldValue in state-document.cts becomes the single owner of the #1760
frontmatter-then-body fallback chain. The new whole-repo guard found 14
independent re-derivations where the epic scoped 5, all now routed through it:
cmdStateSnapshot (11), cmdStatePrune (2) and smart-entry fmScalar (1).

state validate was a gate that could not fail. Every warning it could emit sat
behind a phase resolved without the frontmatter tier, so a STATE.md whose phase
lives only in frontmatter skipped the drift scan entirely and returned
valid:true. It also read unstripped content, letting a frontmatter status: key
shadow the body field (#1255 class). Both fixed; output gains a scope field so
could-not-look stops being output-identical to looked-and-clean.

Verified on the remote runner.

* docs(#3187): document the state validate scope field and its reason codes

Adds docs/how-to/interpret-state-validate-results.md so a reader can tell
nothing-to-report from could-not-look, updates the COMMANDS.md and USER-GUIDE.md
entries, corrects the CONTEXT.md glossary overstatement about Current Position
sole ownership, and drops the changeset fragment.

* fix(#3187): close three drift-guard evasion shapes and test the refuse path

The isolated adversarial review found the ladder detector was evadable by
ordinary reformatting, not just deliberately: a member or computed operand
(fm.key / fm[key]) missed the bare-identifier backreference, a swapped tier
order missed a hardcoded number-then-boolean sequence, and a ladder wrapped
across lines missed single-line detection. All three now caught, each with its
own test plus a proven boundary control.

The frontmatter-parse refuse path on the destructive complete-phase route was
unreachable and therefore untested. It is now driven by an injected parse
failure and asserts STATE.md is byte-identical after the refusal, rather than
shipping untested defensive code on a path that rewrites user state.

Verified on the remote runner.

* fix(#3187): widen the drift guard to the prompt layer and disclose tier-2 changes

The code-review spec axis found the guard's scan surface was src/ only, which is
Decision 4(d)'s forbidden allowlist one directory wide - and it had a live miss:
gsd-core/workflows/smart-entry.md tells an agent to read status from frontmatter
or the body, a prose expression of this same chain. The surface now covers the
prompt layer. That one site carries a permanent written exemption rather than a
ratchet: it is the gsd-tools-is-down fallback, so it cannot call the owner by
construction, and a ratchet would imply removable debt that does not exist.

Two tier-2 output changes shipped undisclosed and are now named in the changeset
and docs: complete-phase's idempotency guard consulting frontmatter, and the
workstream inventory resolving frontmatter-only fields. docs/COMMANDS.md gains a
state complete-phase entry, which it never had.

Also records Amendment 5 on ADR-3180, extracts the duplicated frontmatter-parse
block the epic's own thesis forbids, and re-points two assertions from free-form
warning prose onto the structured drift object.

Verified on the remote runner.

* chore(#3187): backfill changeset PR number

pr:0 placeholder replaced with the real PR number now that #3283 exists.

---------

Co-authored-by: sim <sim@local>
2026-08-09 22:49:39 -04:00

5.3 KiB

How to interpret state validate results

Goal: Read a state validate report correctly — distinguish "nothing to report" from "could not look", so a passing valid:true is never mistaken for a guarantee the check actually ran.

Prerequisites: A .planning/STATE.md file. Run the check with:

node gsd-tools.cjs state validate

For the flag/output reference, see state validate. This guide covers only how to read and act on the result.


Read the two fields separately

state validate returns two fields that answer two different questions, and conflating them recreates the exact defect (#3162) this command's scope field exists to prevent:

Field Question it answers
valid "Did the drift scan find any problems?" — true means no warnings were raised.
scope "Could the drift scan actually run?" — reports whether the check had what it needed to look.

valid is never derived from scope. A document the scan could not check at all (scope other than complete) still reports valid:true, because zero warnings really were raised — there just wasn't anything to raise them from. Reading valid:true alone as "STATE.md is clean" is the trap: always check scope first.


The scope reason-code table

scope What it means What caused it What to do
complete The derivation ran over usable input — a resolvable Current Phase and, if a matching phase directory exists, a readable disk scan of it. valid/warnings/drift are a real, trustworthy answer. Normal operation: STATE.md's phase resolved (from frontmatter or body) and the filesystem was readable. Trust the result as-is. If valid:false, act on the listed warnings/drift entries (typically state sync).
truncated Part of the input was cut short before the scan finished — the phase directory's plan/summary scan hit an internal cap partway through. The valid/drift answer may be incomplete, not necessarily wrong. An unusually large phase directory (many plan/summary files) exceeded the scan's bounded window. Do not treat valid:true here as a clean bill of health. Inspect the phase directory directly (ls .planning/phases/<phase>/) to confirm counts by hand, or reduce/split the phase's plan set if this recurs.
unscoped Current Phase could not be resolved from either the frontmatter scalar or the body field — there was no phase to scope the disk lookup to, so the drift derivation never ran at all. Most commonly a freshly-initialized project with no phase set yet (a genuine, supported state). Less commonly, a STATE.md whose Current Phase field was dropped or malformed. If the project has not started a phase yet, this is expected — no action needed. If the project is active and you expect a phase to be set, open STATE.md and check the current_phase frontmatter key and the body's **Current Phase:** row; run state sync to reconstruct it from disk if it is missing.
unreadable An input the scan needed could not be consulted at all — either the frontmatter block failed to parse, or a filesystem read (the phases directory scan) failed mid-scan. An unterminated/malformed YAML frontmatter fence, or a filesystem error (permissions, a race with a concurrent write) while reading .planning/phases/. Treat valid:true here as not trustworthy — the scan degraded silently before this field existed, and now surfaces that instead of hiding it. Check that STATE.md's frontmatter fence (--- / ---) is well-formed, and that .planning/phases/ is readable by the current user. Re-run state validate after fixing either.

The case this exists for: "valid:true — but is it trustworthy?"

If you only ever read valid, every one of the four scope values above looks identical: true. That collapse is the exact bug this field was added to close (#3162) — a STATE.md whose phase lived only in frontmatter used to silently skip the entire drift scan and report {valid:true, warnings:[], drift:{}}, indistinguishable from a phase that was checked and found clean.

So before trusting a green state validate, always inspect scope:

  • scope:'complete' — trustworthy. The scan ran; valid:true means clean.
  • Anything else — not yet checked, or only partially checked. valid:true here means "no problems were found in what could be looked at," which is a materially weaker claim. Use the table above to find out why, and whether that is expected (a fresh project, unscoped) or a problem worth fixing (unreadable, or a truncated scan on a large phase).

A freshly-initialized project is the clearest example of a legitimate non-complete scope: it reports {valid:true, warnings:[], drift:{}, scope:'unscoped'}, which reads as "nothing was found wrong, and the phase could not be checked" — not as a defect to fix.