diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..eeea9f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI +on: + pull_request: + branches: [main] +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run validate + - run: npm run export:images + - run: npm run export:pptx diff --git a/CLAUDE.md b/CLAUDE.md index d41dc51..d1a5c58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,12 +149,14 @@ ai-agent-patterns/ │ └── svg/ │ └── pattern-*.svg ├── scripts/ +│ ├── validate-patterns.js │ ├── export-images.js │ ├── export-pptx.js │ └── export-pdf.js └── .github/ └── workflows/ - └── deploy.yml ← GitHub Pages deploy action + ├── deploy.yml ← GitHub Pages deploy action + └── ci.yml ← PR validation (schema + export smoke test) ``` --- @@ -190,6 +192,31 @@ jobs: --- +## GitHub Actions — PR validation + +Path: `.github/workflows/ci.yml` + +Runs on every pull request targeting `main`. Two layers of validation, +both driven by loading `index.html` in headless Chromium so they exercise +the real `PATTERNS` data and renderer rather than a separate copy: + +1. `npm run validate` (`scripts/validate-patterns.js`) — checks every + pattern against the schema documented above (required fields, valid + `cat`/`group`/`kind` enums, `members` present when `kind` is set, + no duplicate names, and that every distinct `statefulness` value has + a matching entry in `STATEFULNESS_INFO` so the tooltip never renders + empty). +2. `npm run export:images` / `npm run export:pptx` — runs the real export + scripts end-to-end as a smoke test. A thrown error here means the + renderer or PATTERNS data broke something the schema check can't see + (e.g. a layout exception), not just a malformed field. + +This does not catch visual regressions (e.g. a node clipped at the edge +of its SVG `viewBox`) — only crashes and schema violations. Visual +changes to the diagrams still need a manual screenshot check. + +--- + ## README (generate this) Claude Code should generate a `README.md` that includes: diff --git a/package.json b/package.json index b5d793a..22c0afe 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "description": "Export tooling for the AI Agent Design Patterns interactive reference", "scripts": { + "validate": "node scripts/validate-patterns.js", "export:images": "node scripts/export-images.js", "export:pptx": "node scripts/export-pptx.js" }, diff --git a/scripts/validate-patterns.js b/scripts/validate-patterns.js new file mode 100644 index 0000000..64904ed --- /dev/null +++ b/scripts/validate-patterns.js @@ -0,0 +1,103 @@ +const path = require('path'); +const { chromium } = require('playwright'); + +const ROOT = path.resolve(__dirname, '..'); +const INDEX_HTML = path.join(ROOT, 'index.html'); + +const GROUPS = new Set(['single', 'multi']); +const CATS = new Set(['llm', 'tool', 'data', 'multi']); +const FLOW_CATS = new Set(['neutral', 'llm', 'tool', 'data', 'multi']); +const KINDS = new Set(['parallel', 'branch']); +const REQUIRED_STRING_FIELDS = [ + 'section', 'group', 'name', 'sub', 'cat', 'catLabel', + 'desc', 'uses', 'complexity', 'agents', 'loops', 'statefulness', +]; + +function errorsForPattern(p, i) { + const errs = []; + const where = `PATTERNS[${i}]${p && p.name ? ` (${p.name})` : ''}`; + + for (const field of REQUIRED_STRING_FIELDS) { + if (typeof p[field] !== 'string' || p[field].trim() === '') { + errs.push(`${where}: missing or empty "${field}"`); + } + } + if (!GROUPS.has(p.group)) errs.push(`${where}: invalid group "${p.group}"`); + if (!CATS.has(p.cat)) errs.push(`${where}: invalid cat "${p.cat}"`); + if (p.loop !== undefined && typeof p.loop !== 'boolean') { + errs.push(`${where}: "loop" must be a boolean if present`); + } + if (!Array.isArray(p.flow) || p.flow.length === 0) { + errs.push(`${where}: "flow" must be a non-empty array`); + return errs; + } + + p.flow.forEach((step, j) => { + const stepWhere = `${where}.flow[${j}]`; + if (typeof step.label !== 'string' || step.label.trim() === '') { + errs.push(`${stepWhere}: missing or empty "label"`); + } + if (!FLOW_CATS.has(step.cat)) { + errs.push(`${stepWhere}: invalid cat "${step.cat}"`); + } + if (step.kind !== undefined) { + if (!KINDS.has(step.kind)) { + errs.push(`${stepWhere}: invalid kind "${step.kind}"`); + } + if (!Array.isArray(step.members) || step.members.length === 0) { + errs.push(`${stepWhere}: "members" must be a non-empty array when "kind" is set`); + } else if (step.members.some((m) => typeof m !== 'string' || m.trim() === '')) { + errs.push(`${stepWhere}: "members" must contain only non-empty strings`); + } + } + }); + + return errs; +} + +async function main() { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto(`file://${INDEX_HTML}`); + const { patterns, statefulnessInfo } = await page.evaluate(() => ({ + patterns: PATTERNS, + statefulnessInfo: typeof STATEFULNESS_INFO !== 'undefined' ? STATEFULNESS_INFO : null, + })); + await browser.close(); + + const errors = []; + + if (!Array.isArray(patterns) || patterns.length === 0) { + errors.push('PATTERNS must be a non-empty array'); + } else { + patterns.forEach((p, i) => errors.push(...errorsForPattern(p, i))); + + const names = patterns.map((p) => p.name).filter(Boolean); + const dupes = names.filter((n, i) => names.indexOf(n) !== i); + if (dupes.length) { + errors.push(`Duplicate pattern name(s): ${[...new Set(dupes)].join(', ')}`); + } + + if (statefulnessInfo) { + const statefulnessValues = new Set(patterns.map((p) => p.statefulness).filter(Boolean)); + for (const value of statefulnessValues) { + if (!statefulnessInfo[value]) { + errors.push(`No STATEFULNESS_INFO entry for statefulness value "${value}" — the tooltip will be empty`); + } + } + } + } + + if (errors.length) { + console.error(`Found ${errors.length} problem(s) in PATTERNS:\n`); + errors.forEach((e) => console.error(` - ${e}`)); + process.exit(1); + } + + console.log(`PATTERNS schema OK (${patterns.length} patterns).`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});