fix(3265): prefer YAML frontmatter for state-snapshot canonical fields (#3275)

* fix(3265): prefer YAML frontmatter for state-snapshot canonical fields

stateSnapshot in both sdk/src/query/state.ts and the CJS twin
(get-shit-done/bin/lib/state.cjs cmdStateSnapshot) passed the whole
STATE.md blob to stateExtractField, whose bold pattern (**Field:**)
has no line anchor.  A body table cell such as
"**Status:** to ✅ COMPLETE" therefore silenced the correct YAML
frontmatter value.

Fix: extractFrontmatter(content) first; stripFrontmatter(content) for
the body passed to stateExtractField; for each canonical scalar field
prefer the non-empty frontmatter value, falling back to body extraction
when the key is absent or the file has no frontmatter block at all.

Regression tests added in sdk/src/query/state.test.ts (vitest) and
tests/state.test.cjs (node:test) covering:
- frontmatter status beats **Status:** inside a table cell
- frontmatter current_plan beats bold body value
- no-frontmatter files continue to extract from body
- field absent from frontmatter falls through to body extractor

Fixes #3265

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

* chore: add changeset for #3275

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

* test: reproduce fmStr drops non-string YAML scalars (#3275 CR finding)

Add tests/bug-3275-fmstr-non-string-scalars.test.cjs with 5 cases covering
CJS state-snapshot with numeric frontmatter scalars (current_phase: 19,
total_phases: 7, total_plans_in_phase: 5), string regression, and
no-frontmatter body fallback regression.

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

* fix(state): fmStr accepts numeric/boolean YAML scalars (CR finding)

Rename `fmStr` to `fmScalar` in both state.cjs and sdk/src/query/state.ts
and broaden the type guard so that non-null number/boolean frontmatter values
are coerced to String(v) instead of being discarded.

The previous `typeof v === 'string'` check was a latent bug: if the YAML
parser ever returns typed scalars (e.g. `current_phase: 19` as the number 19),
the frontmatter value would be silently dropped and the stale body value used
instead.  Both files are updated identically (k014 parity).

Also adds three SDK vitest regression cases (numeric current_phase,
total_phases, total_plans_in_phase) in sdk/src/query/state.test.ts.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-08 23:53:21 -04:00
committed by GitHub
parent 96806003c5
commit ecd57e622c
6 changed files with 518 additions and 28 deletions

View File

@@ -191,6 +191,98 @@ describe('state-snapshot command', () => {
});
});
// ─── Regression: #3265 — frontmatter wins over bold-body cell ─────────────
describe('state-snapshot — bug #3265 frontmatter precedence', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns frontmatter status, not **Status:** value embedded in a body table cell', () => {
// Reproduce the collision: frontmatter says "executing", but the body
// contains a Markdown table cell with "**Status:** to ✅ COMPLETE ..."
// which stateExtractField (bold pattern) would match before the YAML line.
const stateContent = [
'---',
'gsd_state_version: 1.0',
'status: executing',
'current_plan: 19.5-05',
'---',
'',
'# Project State',
'',
'## Recent Quick Tasks',
'',
'| Date | Task | Notes |',
'|------|------|-------|',
'| 2026-05-01 | Reopened Plan 19.5-05. **Status:** to ✅ COMPLETE | done |',
'',
'**Current Phase:** 19',
'**Current Plan:** archived-lane',
'',
].join('\n');
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateContent);
const result = runGsdTools('state-snapshot', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// Frontmatter status must win over the table cell's **Status:** match
assert.strictEqual(output.status, 'executing', 'frontmatter status beats body table cell');
});
test('returns frontmatter current_plan, not bold body value when both present', () => {
const stateContent = [
'---',
'gsd_state_version: 1.0',
'status: executing',
'current_plan: 19.5-05',
'---',
'',
'# Project State',
'',
'**Current Phase:** 19',
'**Current Plan:** archived-lane',
'',
].join('\n');
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateContent);
const result = runGsdTools('state-snapshot', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.current_plan, '19.5-05', 'frontmatter current_plan beats body bold value');
});
test('falls back to body extraction when no frontmatter block is present', () => {
const stateContent = [
'# Project State',
'',
'**Current Phase:** 07',
'**Status:** paused',
'',
].join('\n');
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateContent);
const result = runGsdTools('state-snapshot', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
// No frontmatter — body extraction must still work
assert.strictEqual(output.status, 'paused', 'body extraction works without frontmatter');
assert.strictEqual(output.current_phase, '07', 'body extraction works without frontmatter');
});
});
describe('state mutation commands', () => {
let tmpDir;