* fix(#628): read snake_case requirements_completed in summary-extract cmdSummaryExtract read only the kebab frontmatter key `requirements-completed`, but the tool's own JSON output key and the milestone-audit `--pick` both use the snake form `requirements_completed`. A SUMMARY written in the snake form the tool itself emits was silently read back as [] — a false negative in milestone requirement traceability with no diagnostic. Make the reader tolerant of both key forms (kebab takes precedence), so a round-tripped field is no longer dropped. extractFrontmatter does no hyphen<->underscore normalization, so distinct keys had to be read explicitly. Adds regression tests: snake-only fixture now returns the IDs, and a both-forms-present fixture asserts kebab precedence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#628): add changeset for summary-extract snake-key fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/clever-deer-wander.md
Normal file
5
.changeset/clever-deer-wander.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 640
|
||||
---
|
||||
**`gsd-tools query summary-extract` no longer drops snake_case `requirements_completed`** — the reader now accepts both the kebab `requirements-completed` and the snake `requirements_completed` key forms (the snake form is what the tool's own JSON output and the milestone audit `--pick` emit), so a round-tripped requirements field is no longer silently read back as empty.
|
||||
@@ -750,7 +750,11 @@ function cmdSummaryExtract(cwd: string, summaryPath: string | undefined, fields:
|
||||
tech_added: (techStack && techStack['added']) || [],
|
||||
patterns: fm['patterns-established'] || [],
|
||||
decisions: parseDecisions(fm['key-decisions']),
|
||||
requirements_completed: fm['requirements-completed'] || [],
|
||||
// Tolerate both key forms: the template/reader use kebab `requirements-completed`,
|
||||
// but the tool's own JSON output and the milestone audit `--pick` use snake
|
||||
// `requirements_completed`. Reading both prevents a snake-keyed SUMMARY (the form the
|
||||
// tool emits) from being silently dropped to []. See #628.
|
||||
requirements_completed: fm['requirements-completed'] ?? fm['requirements_completed'] ?? [],
|
||||
};
|
||||
|
||||
// If fields specified, filter to only those fields
|
||||
|
||||
@@ -424,6 +424,61 @@ one-liner: Minimal summary
|
||||
assert.deepStrictEqual(output.requirements_completed, [], 'requirements_completed defaults to empty');
|
||||
});
|
||||
|
||||
test('reads requirements in snake_case form the tool itself emits (#628)', () => {
|
||||
// Regression: the tool's JSON output key and the milestone-audit `--pick` both use the
|
||||
// snake form `requirements_completed`, so operators naturally write that into SUMMARY
|
||||
// frontmatter. The reader must accept it, not silently drop it to [].
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '01-01-SUMMARY.md'),
|
||||
`---
|
||||
one-liner: Snake-keyed summary
|
||||
requirements_completed:
|
||||
- REQ-1
|
||||
- REQ-2
|
||||
---
|
||||
|
||||
# Summary
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.deepStrictEqual(output.requirements_completed, ['REQ-1', 'REQ-2'],
|
||||
'snake-case requirements_completed should be read, not dropped to []');
|
||||
});
|
||||
|
||||
test('prefers kebab requirements-completed when both key forms are present (#628)', () => {
|
||||
// kebab is the documented template form and must win the tolerance fallback.
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '01-01-SUMMARY.md'),
|
||||
`---
|
||||
one-liner: Both key forms present
|
||||
requirements-completed:
|
||||
- KEBAB-1
|
||||
requirements_completed:
|
||||
- SNAKE-1
|
||||
---
|
||||
|
||||
# Summary
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.deepStrictEqual(output.requirements_completed, ['KEBAB-1'],
|
||||
'kebab key should take precedence over snake when both are present');
|
||||
});
|
||||
|
||||
test('parses key-decisions with rationale', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user