diff --git a/.changeset/graceful-tigers-chatter.md b/.changeset/graceful-tigers-chatter.md new file mode 100644 index 000000000..2bc47bc5c --- /dev/null +++ b/.changeset/graceful-tigers-chatter.md @@ -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. diff --git a/gsd-core/bin/gsd-tools.cjs b/gsd-core/bin/gsd-tools.cjs index baa31cedb..e17a54f75 100755 --- a/gsd-core/bin/gsd-tools.cjs +++ b/gsd-core/bin/gsd-tools.cjs @@ -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', diff --git a/src/verify.cts b/src/verify.cts index 75e676783..f64be88be 100644 --- a/src/verify.cts +++ b/src/verify.cts @@ -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(); + // #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); } } diff --git a/tests/verify.test.cjs b/tests/verify.test.cjs index c8efb929a..daf3bd66b 100644 --- a/tests/verify.test.cjs +++ b/tests/verify.test.cjs @@ -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'); + }); }); // ─────────────────────────────────────────────────────────────────────────────