Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
29 changes: 28 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

---
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
103 changes: 103 additions & 0 deletions scripts/validate-patterns.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading