* fix(#2202): preserve unknown frontmatter keys in syncStateFrontmatter syncStateFrontmatter rebuilds frontmatter from a fixed schema, dropping any custom/unknown key on every mutating verb. Before reconstruction, merge any existing frontmatter key the schema does not own. Schema keys still win. Closes #2202 * docs(#2202): add changeset fragment * docs(#2202): backfill PR number * fix(#2202): add regression test + remove redundant type assertion - tests/state.test.cjs: behavioral regression test asserting custom/unknown STATE.md frontmatter keys survive a mutating verb (they were silently dropped before the syncStateFrontmatter carry-forward). - src/state.cts: drop the unnecessary `as Record<string, unknown>` assertion that tripped @typescript-eslint/no-unnecessary-type-assertion (the lint-tests gate failure). Refs #2202 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/agile-pandas-dance.md
Normal file
5
.changeset/agile-pandas-dance.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2233
|
||||
---
|
||||
**Custom STATE.md frontmatter keys are no longer dropped on every mutating verb** — syncStateFrontmatter rebuilt the frontmatter from a fixed schema, silently dropping any custom key. It now carries forward existing keys the schema does not own. (#2202)
|
||||
@@ -1650,6 +1650,16 @@ function syncStateFrontmatter(content: string, cwd: string | undefined): string
|
||||
derivedFm['progress'] = normalizeProgressNumbers(existingFm['progress']);
|
||||
}
|
||||
|
||||
// #2202: carry forward any existing frontmatter key that the schema does not
|
||||
// own, so custom/unknown keys are not silently dropped on every mutating verb.
|
||||
// Schema-owned keys (already in derivedFm from buildStateFrontmatter + the
|
||||
// preserve guards above) still win.
|
||||
for (const key of Object.keys(existingFm)) {
|
||||
if (!(key in derivedFm) && existingFm[key] !== undefined) {
|
||||
derivedFm[key] = existingFm[key];
|
||||
}
|
||||
}
|
||||
|
||||
const yamlStr = reconstructFrontmatter(derivedFm as unknown as Frontmatter);
|
||||
return `---\n${yamlStr}\n---\n\n${body}`;
|
||||
}
|
||||
|
||||
@@ -611,6 +611,37 @@ milestone: v1.0
|
||||
assert.ok(!content.includes('status: unknown'), 'should not contain unknown status');
|
||||
});
|
||||
|
||||
test('#2202: preserves unknown frontmatter keys the schema does not own', () => {
|
||||
// Regression: a mutating verb rewrites STATE.md via syncStateFrontmatter,
|
||||
// which rebuilds frontmatter from the body + schema. Before #2202 it dropped
|
||||
// any frontmatter key the schema does not own; custom/tooling keys must
|
||||
// survive every write.
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`---
|
||||
status: executing
|
||||
milestone: v1.0
|
||||
custom_tracking_id: ABC-123
|
||||
team: platform
|
||||
---
|
||||
|
||||
# Project State
|
||||
|
||||
**Current Phase:** 03
|
||||
**Current Plan:** 03-02
|
||||
`
|
||||
);
|
||||
|
||||
// Any writeStateMd triggers syncStateFrontmatter.
|
||||
runGsdTools('state update "Current Plan" "03-03"', tmpDir);
|
||||
|
||||
const content = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.match(content, /custom_tracking_id: ABC-123/, 'unknown key custom_tracking_id must be preserved');
|
||||
assert.match(content, /team: platform/, 'unknown key team must be preserved');
|
||||
// Schema-owned keys still win / survive alongside the carried-forward keys.
|
||||
assert.ok(content.includes('status: executing'), 'schema-owned status still preserved');
|
||||
});
|
||||
|
||||
test('round-trip: write then read via state json', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
|
||||
Reference in New Issue
Block a user