From 998f116b6de6ff1754ee205a73f8679d954c065b Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 20:56:17 +0000 Subject: [PATCH] F2: design-doc discovery reads docs/designs, never a root DESIGN.md A root DESIGN.md is the design system /design-consultation writes, so preferring it unconditionally reviewed plans against a token file while printing "Design doc found" and never looked at docs/designs/ feature docs. Discovery now considers only docs/designs/*.md as the repo-local design doc (still freshness-compared with the private ~/.gstack copy). The CEO finding fixture seeds its supplied design as a docs/designs doc and its test follows. Co-authored-by: Bit0ps --- scripts/resolvers/design-doc-discovery.ts | 12 ++++---- test/ceo-finding-fixture.test.ts | 34 +++++++++++++++++++++-- test/helpers/ceo-finding-fixture.ts | 8 ++++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/scripts/resolvers/design-doc-discovery.ts b/scripts/resolvers/design-doc-discovery.ts index 757d85efe..37b4455cd 100644 --- a/scripts/resolvers/design-doc-discovery.ts +++ b/scripts/resolvers/design-doc-discovery.ts @@ -3,8 +3,10 @@ * * Finds the design doc a plan review should read: newest branch-scoped doc * under ~/.gstack/projects//, falling back to newest project-scoped - * doc, then lets a repo-local doc (DESIGN.md or docs/designs/*.md) win when - * it is at least as fresh. office-hours dual-writes docs/designs/ alongside + * doc, then lets a repo-local feature doc (docs/designs/*.md) win when + * it is at least as fresh. A root DESIGN.md is never a design doc: it is the + * design system /design-consultation writes, and preferring it reviewed + * plans against a token file while printing "Design doc found" (#2839). office-hours dual-writes docs/designs/ alongside * ~/.gstack, and the committed copy is what teammates see — but a stale old * repo doc must never shadow a newer private session. * @@ -34,10 +36,8 @@ export const DESIGN_DOC_DISCOVERY_BLOCK = `_LOCALDOC=$(ls -t ~/.gstack/projects/ # see. A stale old repo doc never shadows a newer private session. _REPOTOP=$(git rev-parse --show-toplevel 2>/dev/null || echo "") _REPODOC="" -if [ -n "$_REPOTOP" ]; then - [ -f "$_REPOTOP/DESIGN.md" ] && _REPODOC="$_REPOTOP/DESIGN.md" - [ -z "$_REPODOC" ] && _REPODOC=$(ls -t "$_REPOTOP"/docs/designs/*.md 2>/dev/null | head -1) -fi +# Only docs/designs/ holds feature design docs; a root DESIGN.md is the design system (#2839). +[ -n "$_REPOTOP" ] && _REPODOC=$(ls -t "$_REPOTOP"/docs/designs/*.md 2>/dev/null | head -1) DESIGN="$_LOCALDOC" if [ -n "$_REPODOC" ] && { [ -z "$_LOCALDOC" ] || [ "$_REPODOC" -nt "$_LOCALDOC" ]; }; then DESIGN="$_REPODOC" diff --git a/test/ceo-finding-fixture.test.ts b/test/ceo-finding-fixture.test.ts index cc6fc8832..1c0fe62c5 100644 --- a/test/ceo-finding-fixture.test.ts +++ b/test/ceo-finding-fixture.test.ts @@ -291,6 +291,36 @@ test.each([ expect(pickSuppliedCeoPlanStart({ options })).toBe(expected); }); +describe('F2: design-doc discovery prefers docs/designs over a root DESIGN.md (#2839)', () => { + function discover(files: Record): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'design-doc-f2-')); + try { + const cwd = path.join(root, 'project'); + const home = path.join(root, 'home'); + fs.mkdirSync(cwd); fs.mkdirSync(home); + execFileSync('git', ['init', '-q', '-b', 'main'], { cwd, timeout: 30_000 }); + for (const [rel, body] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true }); + fs.writeFileSync(path.join(cwd, rel), body); + } + // The design system is the newest file, so mtime alone would pick it. + if (files['DESIGN.md']) fs.utimesSync(path.join(cwd, 'DESIGN.md'), new Date(), new Date(Date.now() + 60_000)); + return execFileSync('bash', ['-c', `SLUG=fixture; BRANCH=main; ${DESIGN_DOC_DISCOVERY_BLOCK}`], { + cwd, env: { PATH: process.env.PATH!, HOME: home }, encoding: 'utf8', timeout: 10_000, + }).replace(fs.realpathSync(cwd), '').replace(cwd, ''); + } finally { fs.rmSync(root, { recursive: true, force: true }); } + } + + test('a feature doc in docs/designs/ wins over a newer root DESIGN.md', () => { + expect(discover({ 'DESIGN.md': '# Design system\ncolors: {}\n', 'docs/designs/pricing.md': '# Pricing design\n' })) + .toBe('Design doc found: /docs/designs/pricing.md\n'); + }); + + test('a root DESIGN.md alone is a design system, not a design doc', () => { + expect(discover({ 'DESIGN.md': '# Design system\n' })).toBe('No design doc found\n'); + }); +}); + describe('CEO finding fixture establishes scope before launch', () => { test('a supplied design satisfies actual prerequisite discovery without becoming a branch change', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ceo-design-seed-')); @@ -304,8 +334,8 @@ describe('CEO finding fixture establishes scope before launch', () => { const output = execFileSync('bash', ['-c', `SLUG=fixture; BRANCH=main; ${DESIGN_DOC_DISCOVERY_BLOCK}`], { cwd, env: { PATH: process.env.PATH!, HOME: home }, encoding: 'utf8', timeout: 10_000, }); - expect(output).toBe(`Design doc found: ${path.join(cwd, 'DESIGN.md')}\n`); - expect(execFileSync('git', ['show', 'HEAD:DESIGN.md'], { cwd, encoding: 'utf8', timeout: 30_000 })).toBe(design); + expect(output).toBe(`Design doc found: ${path.join(cwd, 'docs', 'designs', 'feature-design.md')}\n`); + expect(execFileSync('git', ['show', 'HEAD:docs/designs/feature-design.md'], { cwd, encoding: 'utf8', timeout: 30_000 })).toBe(design); expect(execFileSync('git', ['show', 'HEAD:review-input.md'], { cwd, encoding: 'utf8', timeout: 30_000 })).toBe(plan); expect(execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8', timeout: 30_000 })).toBe(''); expect(execFileSync('git', ['diff', 'origin/main...HEAD'], { cwd, encoding: 'utf8', timeout: 30_000 })).toBe(''); diff --git a/test/helpers/ceo-finding-fixture.ts b/test/helpers/ceo-finding-fixture.ts index a5ebce8fe..22d67dc11 100644 --- a/test/helpers/ceo-finding-fixture.ts +++ b/test/helpers/ceo-finding-fixture.ts @@ -59,7 +59,11 @@ export function seedPlanReviewProject(projectDir: string, plan: string, skill: ' throw new Error('Plan review fixture requires a fresh private directory'); } fs.writeFileSync(path.join(projectDir, 'review-input.md'), plan, { flag: 'wx' }); - if (design !== undefined) fs.writeFileSync(path.join(projectDir, 'DESIGN.md'), design, { flag: 'wx' }); + // A feature design doc lives in docs/designs/; a root DESIGN.md is a design system (F2, #2839). + if (design !== undefined) { + fs.mkdirSync(path.join(projectDir, 'docs', 'designs'), { recursive: true }); + fs.writeFileSync(path.join(projectDir, 'docs', 'designs', 'feature-design.md'), design, { flag: 'wx' }); + } fs.writeFileSync(path.join(projectDir, 'README.md'), `# ${skill} fixture\n`, { flag: 'wx' }); fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), [ `# ${skill}`, '', @@ -73,7 +77,7 @@ export function seedPlanReviewProject(projectDir: string, plan: string, skill: ' ].join('\n'), { flag: 'wx' }); const git = (args: string[]) => execFileSync('git', args, { cwd: projectDir, stdio: 'pipe', timeout: 10_000 }); git(['init', '-b', 'main']); - git(['add', 'README.md', 'CLAUDE.md', 'review-input.md', ...(design === undefined ? [] : ['DESIGN.md'])]); + git(['add', 'README.md', 'CLAUDE.md', 'review-input.md', ...(design === undefined ? [] : ['docs/designs/feature-design.md'])]); git(['-c', 'user.name=Finding fixture', '-c', 'user.email=fixture@gstack.test', 'commit', '-m', 'Seed review input']); git(['update-ref', 'refs/remotes/origin/main', 'HEAD']); }