diff --git a/.changeset/3271-sdk-adr-structure.md b/.changeset/3271-sdk-adr-structure.md new file mode 100644 index 000000000..188e8d6e5 --- /dev/null +++ b/.changeset/3271-sdk-adr-structure.md @@ -0,0 +1,5 @@ +--- +type: Enhancement +pr: 3302 +--- +**`docs/adr/` index and SDK seam ADRs (#3271)** — added `docs/adr/README.md` as an indexed entry point for all Architecture Decision Records, linking all seven ADRs. ADR 0005 documents the top-level SDK architecture seam map (Dispatch Policy Module, Model Catalog Module, Planning Workspace Module, SDK Package Seam Module, Planning Path Projection Module). ADR 0006 documents how SDK query handlers project planning paths (`cwd → effectiveRoot → .planning//...`). A structural test (`tests/enh-3271-sdk-adr-structure.test.cjs`) asserts each ADR has required headings and Status/Date metadata, and that the README links every ADR file by filename. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 000000000..f0c3bff18 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,23 @@ +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for GSD. + +Each ADR documents one architectural decision: what was decided, why, and what consequences follow. ADRs are append-only. Amendments extend existing ADRs with a dated section rather than replacing them. + +## Index + +| ADR | Title | Status | +|-----|-------|--------| +| [0001-dispatch-policy-module.md](0001-dispatch-policy-module.md) | Dispatch policy module as single seam for query execution outcomes | Accepted | +| [0002-command-contract-validation-module.md](0002-command-contract-validation-module.md) | Command Contract Validation Module | Accepted | +| [0003-model-catalog-module.md](0003-model-catalog-module.md) | Model Catalog Module as single source of truth for agent profiles and runtime tier defaults | Accepted | +| [0004-worktree-workstream-seam-module.md](0004-worktree-workstream-seam-module.md) | Planning Workspace Module as single seam for worktree and workstream state | Accepted | +| [0005-sdk-architecture-seam-map.md](0005-sdk-architecture-seam-map.md) | SDK Architecture seam map for query/runtime surfaces | Accepted | +| [0006-planning-path-projection-module.md](0006-planning-path-projection-module.md) | Planning Path Projection Module for SDK query handlers | Accepted | +| [0007-sdk-package-seam-module.md](0007-sdk-package-seam-module.md) | SDK Package Seam Module owns SDK-to-get-shit-done-cc compatibility | Accepted | + +## Seam map + +ADR 0005 is the top-level SDK seam index. It references per-seam ADRs and states the narrow-waist principle each seam follows. Use it as the entry point for understanding SDK module ownership. + +ADR 0006 documents how SDK query handlers project planning paths (`cwd → effectiveRoot → .planning//...`). Cross-reference with the Planning Workspace Module (ADR 0004) for workstream pointer policy. diff --git a/tests/enh-3271-sdk-adr-structure.test.cjs b/tests/enh-3271-sdk-adr-structure.test.cjs new file mode 100644 index 000000000..ea069b2a4 --- /dev/null +++ b/tests/enh-3271-sdk-adr-structure.test.cjs @@ -0,0 +1,215 @@ +/** + * Structural tests for ADR 0005 (SDK architecture seam-map) and + * ADR 0006 (planning-path projection module), per issue #3271. + * + * Assertions parse the markdown by splitting on heading lines and inspect + * typed records. The docs/adr/README.md index must exist and reference + * both ADRs by filename. + */ + +// allow-test-rule: heading-split structural parser for ADR markdown documents. +// Assertions target typed records (heading sets, status strings, filename refs), +// not raw .includes()/.match() on prose. + +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const ADR_DIR = path.join(__dirname, '..', 'docs', 'adr'); +const README_PATH = path.join(ADR_DIR, 'README.md'); + +// --- Helpers ----------------------------------------------------------------- + +function parseAdr(filePath) { + let raw; + try { + raw = fs.readFileSync(filePath, 'utf8'); + } catch (err) { + throw new Error(`Cannot read ADR file: ${filePath} — ${err.message}`); + } + + const lines = raw.split('\n'); + let title = null; + const headings = []; + let status = null; + let date = null; + + for (const line of lines) { + const h1 = line.match(/^#\s+(.+)$/); + if (h1 && title === null) { title = h1[1].trim(); continue; } + const h2 = line.match(/^##\s+(.+)$/); + if (h2) { headings.push(h2[1].trim().toLowerCase()); continue; } + const statusMatch = line.match(/\*\*Status:\*\*\s*(.+)/); + if (statusMatch && status === null) { status = statusMatch[1].trim(); continue; } + const dateMatch = line.match(/\*\*Date:\*\*\s*(.+)/); + if (dateMatch && date === null) { date = dateMatch[1].trim(); } + } + + return { title, headings, status, date }; +} + +function parseReadmeIndex(filePath) { + let raw; + try { + raw = fs.readFileSync(filePath, 'utf8'); + } catch (err) { + throw new Error(`Cannot read ADR README: ${filePath} — ${err.message}`); + } + + const lines = raw.split('\n'); + const linkedFiles = []; + + for (const line of lines) { + const linkRe = /\[.*?\]\(\.?\/?([^)]+\.md)\)/g; + let m; + while ((m = linkRe.exec(line)) !== null) { + linkedFiles.push(path.basename(m[1])); + } + } + + return { linkedFiles }; +} + +// --- ADR 0005: SDK Architecture seam-map ------------------------------------- + +describe('ADR 0005 — SDK architecture seam-map', () => { + const adrPath = path.join(ADR_DIR, '0005-sdk-architecture-seam-map.md'); + + test('file exists', () => { + assert.ok(fs.existsSync(adrPath), `Expected ADR file to exist: ${adrPath}`); + }); + + test('has a title (H1 heading)', () => { + const { title } = parseAdr(adrPath); + assert.ok(title && title.length > 0, 'ADR must have a non-empty H1 title'); + }); + + test('has Status metadata', () => { + const { status } = parseAdr(adrPath); + assert.ok(status && status.length > 0, 'ADR must have a **Status:** line'); + }); + + test('has Date metadata', () => { + const { date } = parseAdr(adrPath); + assert.ok(date && date.length > 0, 'ADR must have a **Date:** line'); + }); + + test('has Decision section', () => { + const { headings } = parseAdr(adrPath); + assert.ok( + headings.some(h => h === 'decision'), + `ADR must have a ## Decision section. Found headings: ${headings.join(', ')}` + ); + }); + + test('has Consequences section', () => { + const { headings } = parseAdr(adrPath); + assert.ok( + headings.some(h => h === 'consequences'), + `ADR must have a ## Consequences section. Found headings: ${headings.join(', ')}` + ); + }); + + test('cross-references at least two other ADR files', () => { + // allow-test-rule: reading markdown link targets and backtick code spans + // from ADR to build a typed set of referenced filenames. + const raw = fs.readFileSync(adrPath, 'utf8'); + const refs = new Set(); + const linkRe = /\((\d{4}-[^)]*\.md)\)/g; + let m; + while ((m = linkRe.exec(raw)) !== null) { refs.add(path.basename(m[1])); } + const codeRe = /`(\d{4}-[^`]*\.md)`/g; + while ((m = codeRe.exec(raw)) !== null) { refs.add(path.basename(m[1])); } + refs.delete('0005-sdk-architecture-seam-map.md'); + assert.ok( + refs.size >= 2, + `Seam-map ADR must cross-reference at least 2 other ADR files. Found: ${[...refs].join(', ')}` + ); + }); +}); + +// --- ADR 0006: Planning Path Projection Module -------------------------------- + +describe('ADR 0006 — planning-path projection module', () => { + const adrPath = path.join(ADR_DIR, '0006-planning-path-projection-module.md'); + + test('file exists', () => { + assert.ok(fs.existsSync(adrPath), `Expected ADR file to exist: ${adrPath}`); + }); + + test('has a title (H1 heading)', () => { + const { title } = parseAdr(adrPath); + assert.ok(title && title.length > 0, 'ADR must have a non-empty H1 title'); + }); + + test('has Status metadata', () => { + const { status } = parseAdr(adrPath); + assert.ok(status && status.length > 0, 'ADR must have a **Status:** line'); + }); + + test('has Date metadata', () => { + const { date } = parseAdr(adrPath); + assert.ok(date && date.length > 0, 'ADR must have a **Date:** line'); + }); + + test('has Decision section', () => { + const { headings } = parseAdr(adrPath); + assert.ok( + headings.some(h => h === 'decision'), + `ADR must have a ## Decision section. Found headings: ${headings.join(', ')}` + ); + }); + + test('has Consequences section', () => { + const { headings } = parseAdr(adrPath); + assert.ok( + headings.some(h => h === 'consequences'), + `ADR must have a ## Consequences section. Found headings: ${headings.join(', ')}` + ); + }); +}); + +// --- docs/adr/README.md index ------------------------------------------------ + +describe('docs/adr/README.md index', () => { + test('README file exists', () => { + assert.ok( + fs.existsSync(README_PATH), + `Expected docs/adr/README.md to exist: ${README_PATH}` + ); + }); + + test('links to ADR 0005', () => { + const { linkedFiles } = parseReadmeIndex(README_PATH); + assert.ok( + linkedFiles.some(f => f === '0005-sdk-architecture-seam-map.md'), + `README must link to 0005-sdk-architecture-seam-map.md. Found: ${linkedFiles.join(', ')}` + ); + }); + + test('links to ADR 0006', () => { + const { linkedFiles } = parseReadmeIndex(README_PATH); + assert.ok( + linkedFiles.some(f => f === '0006-planning-path-projection-module.md'), + `README must link to 0006-planning-path-projection-module.md. Found: ${linkedFiles.join(', ')}` + ); + }); + + test('links to all existing ADR files', () => { + const existingAdrs = fs.readdirSync(ADR_DIR) + .filter(f => /^\d{4}-.*\.md$/.test(f)) + .sort(); + + const { linkedFiles } = parseReadmeIndex(README_PATH); + + for (const adrFile of existingAdrs) { + assert.ok( + linkedFiles.includes(adrFile), + `README must link to every ADR. Missing: ${adrFile}` + ); + } + }); +});