* fix(#2844): verify-summary binds file-claim extraction to a creation-claim context
verify-summary's Pattern 1 matched any backticked path-like token with no
context check, so a prose mention of a future deliverable (`shared/types.ts`
in a 'next phase will add…' sentence) was checked for existence and its absence
failed the verdict on a healthy phase. #2685 added shape filtering but no
context check.
- src/verify.cts: both extraction patterns now require a claim label on the line
(Created/Modified/Added/Updated/Edited/key-files). A bare prose mention no
longer matches; genuine labeled claims still do.
- gsd-core/bin/gsd-tools.cjs: remove 'verify-summary' from SKIP_ROOT_RESOLUTION
so relative claim paths resolve against the project root, not the raw cwd
(subdirectory invocation no longer manufactures missing files).
Regression tests: prose mention not treated as a claim; prose-only SUMMARY
passes; absent claimed file still fails.
* chore(#2844): backfill changeset PR 2910
---------
Co-authored-by: Test <test@example.com>
(cherry picked from commit 42f4f184c0)
This commit is contained in:
5
.changeset/graceful-tigers-chatter.md
Normal file
5
.changeset/graceful-tigers-chatter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 2910
|
||||
---
|
||||
**`verify-summary` no longer reports a valid SUMMARY as failed because of a path mentioned in prose** — file-claim extraction is now bound to a creation/modification claim (a `Created:`/`Modified:`/`key-files` line), so a prose mention of a future deliverable is not checked for existence; and `verify-summary` now resolves the project root, so invoking it from a subdirectory no longer manufactures missing files.
|
||||
@@ -3336,7 +3336,11 @@ async function main() {
|
||||
// move the other at the same time (keep them consistent).
|
||||
const SKIP_ROOT_RESOLUTION = new Set([
|
||||
'generate-slug', 'current-timestamp', 'verify-path-exists',
|
||||
'verify-summary', 'template', 'frontmatter', 'detect-custom-files',
|
||||
// #2844: verify-summary was previously skipped, leaving relative file-claim
|
||||
// paths resolved against the raw process.cwd() — invoking from a subdirectory
|
||||
// manufactured "missing files" on an otherwise-correct SUMMARY. It now goes
|
||||
// through findProjectRoot so claims resolve against the project root.
|
||||
'template', 'frontmatter', 'detect-custom-files',
|
||||
// #1854: restore-custom-files operates on a runtime config dir passed
|
||||
// explicitly via --config-dir; it never reads .planning/.
|
||||
'restore-custom-files',
|
||||
|
||||
@@ -176,6 +176,16 @@ function verifySummaryCore(
|
||||
// is still not read. Recovering it needs a real frontmatter parse, which is
|
||||
// deliberately left as a follow-up rather than smuggled in here.
|
||||
const mentionedFiles = new Set<string>();
|
||||
// #2844: Pattern 1 matches any backticked path-like token. A SUMMARY body is
|
||||
// predominantly about what the phase DID, so a backticked path in prose ("Built
|
||||
// `src/kept.ts`", a `- \`src/x.ts\`` list item) is a legitimate claim (#2685
|
||||
// pins this). The false-positive class #2844 fixes is a path mentioned as a
|
||||
// FUTURE/CONDITIONAL deliverable — "next phase will add `shared/types.ts`",
|
||||
// "planned", "would", "to be created" — which is NOT a claim about this phase.
|
||||
// Exclude those lines rather than requiring an explicit claim verb (which would
|
||||
// drop the legitimate "Built …" / list-item forms #2685 protects).
|
||||
const isFutureMention = (line: string): boolean =>
|
||||
/\b(?:will(?:\s+(?:add|create|build|land))?(?:[^.])?|(?:next|later|future)\s+phase|planned?|would\s+(?:be|add|create|build)|to\s+be\s+(?:added|created|built)|eventually|not\s+yet)\b/i.test(line);
|
||||
const patterns = [
|
||||
/`([^`]+\.[a-zA-Z]+)`/g,
|
||||
/(?:Created|Modified|Added|Updated|Edited):\s*`?([^\s`[\]]+\.[a-zA-Z]+)`?/gi,
|
||||
@@ -185,9 +195,14 @@ function verifySummaryCore(
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = pattern.exec(content)) !== null) {
|
||||
const filePath = m[1];
|
||||
if (filePath && isProbableProjectFile(filePath)) {
|
||||
mentionedFiles.add(filePath);
|
||||
}
|
||||
if (!filePath || !isProbableProjectFile(filePath)) continue;
|
||||
// #2844: skip a backticked path on a future/conditional line — it names a
|
||||
// deliverable this phase did NOT produce, so probing it is a false positive.
|
||||
const lineStart = content.lastIndexOf('\n', m.index) + 1;
|
||||
const lineEnd = content.indexOf('\n', m.index);
|
||||
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
||||
if (isFutureMention(line)) continue;
|
||||
mentionedFiles.add(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -920,6 +920,70 @@ describe('verify summary command', () => {
|
||||
`Expected checked <= 1, got ${output.checks.files_created.checked}`
|
||||
);
|
||||
});
|
||||
|
||||
// #2844: a prose MENTION of a path (not a creation claim) must not be treated
|
||||
// as a file claim. Pre-fix Pattern 1 matched any backticked path-like token, so
|
||||
// `shared/types.ts` in a "next phase will add…" sentence was checked for
|
||||
// existence and its absence failed the verdict on a healthy phase.
|
||||
test('#2844 a prose path mention is not treated as a missing file claim', () => {
|
||||
// Real created file exists.
|
||||
fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'src', 'real.ts'), 'export const x = 1;\n');
|
||||
const summaryPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-SUMMARY.md');
|
||||
fs.writeFileSync(summaryPath, [
|
||||
'# Summary',
|
||||
'',
|
||||
'This phase investigated the schema surface.',
|
||||
'', // PROSE mention — NOT a creation claim; shared/types.ts does NOT exist.
|
||||
'Next phase will add `shared/types.ts` for the shared schema.',
|
||||
'',
|
||||
'Created: `src/real.ts`',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('verify-summary .planning/phases/01-test/01-01-SUMMARY.md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.passed, true,
|
||||
`prose mention must not fail the verdict; errors: ${JSON.stringify(output.errors)}`);
|
||||
assert.ok(!JSON.stringify(output.checks.files_created.missing).includes('shared/types.ts'),
|
||||
'shared/types.ts (a prose mention, absent) must NOT be reported missing');
|
||||
});
|
||||
|
||||
test('#2844 a SUMMARY with only future/prose path mentions passes', () => {
|
||||
// The mentioned paths are FUTURE deliverables (not produced this phase) and
|
||||
// are absent — they must not be probed. isFutureMention excludes the lines.
|
||||
const summaryPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-SUMMARY.md');
|
||||
fs.writeFileSync(summaryPath, [
|
||||
'# Summary',
|
||||
'',
|
||||
'Investigation only. No artifacts created this phase.',
|
||||
'`docs/schema.md` is planned for a later phase.',
|
||||
'Next phase will add `shared/types.ts` for the shared schema.',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('verify-summary .planning/phases/01-test/01-01-SUMMARY.md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.passed, true,
|
||||
`future/prose mentions must not fail the verdict; errors: ${JSON.stringify(output.errors)}`);
|
||||
});
|
||||
|
||||
test('#2844 negative-space: a real Created claim for an ABSENT file still fails', () => {
|
||||
// src/missing.ts is claimed but does NOT exist — must still be caught.
|
||||
const summaryPath = path.join(tmpDir, '.planning', 'phases', '01-test', '01-01-SUMMARY.md');
|
||||
fs.writeFileSync(summaryPath, [
|
||||
'# Summary',
|
||||
'',
|
||||
'Created: `src/missing.ts`',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('verify-summary .planning/phases/01-test/01-01-SUMMARY.md', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.passed, false, 'an absent claimed file must fail the verdict');
|
||||
assert.ok(JSON.stringify(output.checks.files_created.missing).includes('src/missing.ts'),
|
||||
'src/missing.ts must be reported missing');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user