diff --git a/get-shit-done/workflows/progress.md b/get-shit-done/workflows/progress.md index 3522c0b30..977655557 100644 --- a/get-shit-done/workflows/progress.md +++ b/get-shit-done/workflows/progress.md @@ -89,6 +89,11 @@ Use this instead of manually reading/parsing ROADMAP.md. +> ⚠️ Context authority: PROJECT.md, STATE.md, and ROADMAP.md are the authoritative sources +> for project name, milestone, current phase, and next-step routing. CLAUDE.md ## Project +> blocks are a secondary config aid that may be significantly stale — do NOT use the +> CLAUDE.md project description as a source for any progress report field. + **Generate progress bar from `gsd-sdk query progress` / `progress.json`, then present rich status report:** ```bash diff --git a/tests/bug-2912-progress-context-authority.test.cjs b/tests/bug-2912-progress-context-authority.test.cjs new file mode 100644 index 000000000..e26fe0e79 --- /dev/null +++ b/tests/bug-2912-progress-context-authority.test.cjs @@ -0,0 +1,152 @@ +/** + * Tests for issue #2912 — /gsd-progress can use stale CLAUDE.md project block + * instead of GSD tracking files as authoritative source. + * + * Fix: the `report` step in get-shit-done/workflows/progress.md must contain + * an explicit "context authority" directive establishing PROJECT.md, STATE.md, + * and ROADMAP.md as the authoritative sources for the progress report, and + * forbidding the use of CLAUDE.md `## Project` blocks as a source for any + * report field. + * + * These tests parse the workflow markdown structurally (locate the + * ... block, then locate the blockquote-style + * directive inside it). They do NOT use `.includes()` over the whole file. + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const WORKFLOW_PATH = path.join( + __dirname, + '..', + 'get-shit-done', + 'workflows', + 'progress.md' +); + +/** Extract the body of a ... block by parsing tags. */ +function extractStep(workflow, stepName) { + const openTag = ``; + const start = workflow.indexOf(openTag); + if (start === -1) return null; + const bodyStart = start + openTag.length; + // Find the matching — workflow steps in this file do not nest. + const end = workflow.indexOf('', bodyStart); + if (end === -1) return null; + return workflow.slice(bodyStart, end); +} + +/** + * Extract contiguous markdown blockquote blocks from a chunk of markdown. + * A blockquote is a run of consecutive lines starting with '>' (after any + * leading whitespace). Returns the joined text of each blockquote with the + * leading '>' markers stripped. + */ +function extractBlockquotes(md) { + const lines = md.split(/\r?\n/); + const blocks = []; + let current = null; + for (const line of lines) { + const m = line.match(/^\s*>\s?(.*)$/); + if (m) { + if (current === null) current = []; + current.push(m[1]); + } else { + if (current !== null) { + blocks.push(current.join('\n')); + current = null; + } + } + } + if (current !== null) blocks.push(current.join('\n')); + return blocks; +} + +describe('#2912: progress report step has explicit context-authority directive', () => { + test('progress.md workflow file exists and is readable', () => { + const stat = fs.statSync(WORKFLOW_PATH); + assert.ok(stat.isFile(), 'workflow file should exist'); + }); + + test('progress.md has a section', () => { + const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8'); + const reportStep = extractStep(workflow, 'report'); + assert.ok(reportStep, 'workflow should contain a report step'); + assert.ok(reportStep.length > 0, 'report step body should not be empty'); + }); + + test('report step contains a blockquote directive about context authority', () => { + const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8'); + const reportStep = extractStep(workflow, 'report'); + assert.ok(reportStep, 'report step must be present'); + + const blockquotes = extractBlockquotes(reportStep); + assert.ok( + blockquotes.length > 0, + 'report step should contain at least one blockquote (the context-authority directive)' + ); + + const authorityBlock = blockquotes.find((b) => /context\s+authority/i.test(b)); + assert.ok( + authorityBlock, + 'report step should contain a blockquote whose text includes "Context authority"' + ); + }); + + test('context-authority directive names PROJECT.md, STATE.md, and ROADMAP.md as authoritative', () => { + const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8'); + const reportStep = extractStep(workflow, 'report'); + const blockquotes = extractBlockquotes(reportStep); + const authorityBlock = blockquotes.find((b) => /context\s+authority/i.test(b)); + assert.ok(authorityBlock, 'authority blockquote must exist'); + + assert.match( + authorityBlock, + /PROJECT\.md/, + 'directive should name PROJECT.md as authoritative' + ); + assert.match( + authorityBlock, + /STATE\.md/, + 'directive should name STATE.md as authoritative' + ); + assert.match( + authorityBlock, + /ROADMAP\.md/, + 'directive should name ROADMAP.md as authoritative' + ); + assert.match( + authorityBlock, + /authoritative/i, + 'directive should describe these files as authoritative' + ); + }); + + test('context-authority directive forbids using CLAUDE.md project block as a source', () => { + const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8'); + const reportStep = extractStep(workflow, 'report'); + const blockquotes = extractBlockquotes(reportStep); + const authorityBlock = blockquotes.find((b) => /context\s+authority/i.test(b)); + assert.ok(authorityBlock, 'authority blockquote must exist'); + + assert.match( + authorityBlock, + /CLAUDE\.md/, + 'directive should explicitly mention CLAUDE.md' + ); + // Must explicitly forbid CLAUDE.md as a source — look for a NOT/do not directive + // co-located with the CLAUDE.md mention. + assert.match( + authorityBlock, + /(do\s+NOT|do\s+not|must\s+NOT|must\s+not|never)/i, + 'directive should contain an explicit prohibition (do NOT / must not / never)' + ); + assert.match( + authorityBlock, + /## Project/, + 'directive should call out the CLAUDE.md "## Project" block specifically' + ); + }); +});