Files
Teknium 251ab05000 feat(skills): add auteur optional skill — cinematic web design with executable anti-slop gates
Port of agiwhitelist/auteur (MIT, ~1k stars), snapshot 9bca227d. Three
registers (build / direct / system) on one taste core: commit-sheet-first
art direction, asset generation via image_generate + local CLIs, and
node-based quality gates (slopscan anti-slop linter, motionqa frame-drop
check, systemscan cross-route drift) run through playwright.

- optional-skills/creative/auteur: SKILL.md (de-Clauded, Hermes tool
  framing), LICENSE (upstream MIT), 11 references, 8 verbatim upstream
  .mjs scripts (all pass node --check; slopscan smoke-run verified),
  6 templates. README gallery assets not vendored (size cap).
- tests/skills/test_auteur_skill.py: frontmatter, path-annotation
  invariant, de-Claude residue, related_skills resolution.
- Docs: catalog row, sidebar entry, generated skill page (scoped regen).
2026-09-15 04:03:43 -07:00

429 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* systemscan.mjs — the multi-screen gate: does this product still have ONE design system?
*
* slopscan reads source and catches defaults. shoot.mjs photographs one page. Neither can see the
* failure mode of a real app: drift. Screen 1 has three button variants, screen 7 invents a fourth,
* and nobody notices because every screen looks fine on its own. This crawls every route, reads what
* the browser ACTUALLY PAINTED, and reports the system as built rather than as documented.
*
* Usage:
* node scripts/systemscan.mjs <url> [<url>...]
* node scripts/systemscan.mjs http://localhost:3000 --routes /,/settings,/billing
*
* Options:
* --routes a,b,c paths to append to the first url (instead of listing full urls)
* --out DIR output dir (default design/system)
* --max-variants B variant budget: a number, or per kind — `button=5,select=1,4` (default 4)
* --width N viewport width (default 1440)
* --no-shots skip the component contact sheet
*
* Writes DIR/SYSTEM-REPORT.md (read it), DIR/system.json, DIR/components.png.
* Exit 1 when the system is provably broken: a control type over budget, an interactive element with
* no visible focus state, or a token used on exactly one route.
*/
import { mkdir, writeFile } from 'fs/promises';
import { resolve, join } from 'path';
const args = process.argv.slice(2);
if (!args.length || args.includes('--help')) {
console.log(`systemscan.mjs — cross-route design-system drift gate
node scripts/systemscan.mjs <url> [<url>...]
node scripts/systemscan.mjs http://localhost:3000 --routes /,/settings,/billing
--out DIR --max-variants 4|button=5,select=1 --width 1440 --no-shots`);
process.exit(0);
}
const get = (f, d) => { const i = args.indexOf(f); return i !== -1 ? args[i + 1] : d; };
const has = f => args.includes(f);
const outDir = resolve(get('--out', 'design/system'));
// Per-kind budgets: `--max-variants button=5,select=1` (a bare number sets the default for the rest).
// One global number meant a sheet declaring button:5 and select:1 had to pass 5, leaving every
// smaller kind unpoliced — the sheet's central promise enforced for exactly one control kind.
const rawBudget = get('--max-variants', '4');
const budget = { default: 4, byKind: {} };
for (const part of String(rawBudget).split(',').map(s => s.trim()).filter(Boolean)) {
const m = part.match(/^([a-z]+)\s*=\s*(\d+)$/i);
if (m) budget.byKind[m[1].toLowerCase()] = +m[2];
else if (/^\d+$/.test(part)) budget.default = +part;
else console.error(`[systemscan] ignoring unparseable budget "${part}" — use 4 or button=5,select=1`);
}
const budgetFor = kind => budget.byKind[kind] ?? budget.default;
const width = parseInt(get('--width', '1440'), 10) || 1440;
const shots = !has('--no-shots');
let urls = args.filter(a => /^https?:\/\//.test(a) || a.startsWith('file://'));
const routes = get('--routes', null);
if (routes && urls.length) {
const base = urls[0].replace(/\/$/, '');
urls = routes.split(',').map(r => base + (r.startsWith('/') ? r : '/' + r));
}
if (!urls.length) { console.error('[systemscan] no urls. Pass them, or a base url + --routes /a,/b'); process.exit(1); }
let chromium;
try { ({ chromium } = await import('playwright')); }
catch { console.error('playwright not found. Install: npm i -D playwright && npx playwright install chromium'); process.exit(2); }
// Everything below is read off getComputedStyle, i.e. the system as PAINTED. A token that exists in
// the stylesheet but is never rendered is not part of the system; a one-off inline style is.
const COLLECT = () => {
const cs = getComputedStyle;
const px = v => Math.round(parseFloat(v) || 0);
const vis = el => {
const s = cs(el);
if (s.display === 'none' || s.visibility === 'hidden' || +s.opacity === 0) return false;
const r = el.getBoundingClientRect();
return r.width > 1 && r.height > 1;
};
const all = [...document.querySelectorAll('*')].slice(0, 6000).filter(vis);
const controlKind = el => {
const t = el.tagName.toLowerCase();
const role = el.getAttribute('role');
if (t === 'button' || role === 'button') return 'button';
if (t === 'a' && /(^|\s)(btn|button)/i.test(el.className || '')) return 'button';
if (t === 'a') return 'link';
if (t === 'input') return ['checkbox', 'radio'].includes(el.type) ? 'toggle' : 'input';
if (t === 'select') return 'select';
if (t === 'textarea') return 'input';
if (role === 'tab') return 'tab';
return null;
};
// The signature is what a user can SEE. Two buttons with different class names but identical
// paint are one variant; two with the same class and different paint are two.
const sig = el => {
const s = cs(el);
return [
s.backgroundColor, s.color, s.borderColor,
`${px(s.borderTopWidth)}b`, `${px(s.borderTopLeftRadius)}r`,
`${px(s.fontSize)}/${s.fontWeight}`,
`${px(s.paddingTop)}x${px(s.paddingLeft)}`,
s.boxShadow === 'none' ? 'noshadow' : 'shadow',
].join(' | ');
};
// A STATE is not a variant. A disabled secondary button paints differently from an enabled one by
// design — that is the state matrix system.md demands, and counting it as a fifth button punishes
// the team that built it while a system with no disabled state at all sails through. Same for a
// control that inverts because the row it sits in is in an alert state: the component did not
// multiply, its container changed colour underneath it. Both are counted and reported separately.
const stateOf = el => {
if (el.disabled || el.getAttribute('aria-disabled') === 'true') return 'disabled';
if (el.getAttribute('aria-current') || el.getAttribute('aria-selected') === 'true') return 'current';
for (let n = el.parentElement, hops = 0; n && hops < 4; n = n.parentElement, hops++) {
const st = n.getAttribute?.('data-state');
if (st && st !== 'default') return `in-${st}`;
}
return null;
};
const controls = {};
const states = {};
let shotId = 0;
for (const el of all) {
const kind = controlKind(el);
if (!kind) continue;
const state = stateOf(el);
if (state) { (states[kind] ??= {})[state] = ((states[kind] ??= {})[state] || 0) + 1; continue; }
const k = sig(el);
(controls[kind] ??= {});
if (!controls[kind][k]) {
// Tag the exemplar NOW and re-select it by attribute in the screenshot pass. Handing a
// positional index between two independent DOM walks produced tiles that showed a parent or a
// sibling rather than the control they were captioned with — which destroys the sheet's whole
// purpose, because a tile showing the wrong element looks different for the wrong reason.
const tag = `ss${shotId++}`;
el.setAttribute('data-ss-shot', tag);
controls[kind][k] = { count: 0, sample: (el.innerText || el.value || el.type || '').trim().slice(0, 24), tag };
}
controls[kind][k].count++;
}
const tally = (map, key) => { map[key] = (map[key] || 0) + 1; };
const colors = {}, type = {}, radii = {}, shadows = {}, space = {};
for (const el of all) {
const s = cs(el);
if (s.backgroundColor && s.backgroundColor !== 'rgba(0, 0, 0, 0)') tally(colors, s.backgroundColor);
// `color` on a wrapper paints no glyph. Tallying it unfiltered made <html>'s inherited initial
// colour the most common "colour in the product" and inflates the headline count for everyone.
const leafText = el.children.length === 0 && (el.textContent || '').trim();
if (s.color && leafText) tally(colors, s.color);
if (leafText) {
tally(type, `${px(s.fontSize)}px/${s.fontWeight}/${s.fontFamily.split(',')[0].replace(/["']/g, '')}`);
}
const r = px(s.borderTopLeftRadius); if (r) tally(radii, `${r}px`);
if (s.boxShadow && s.boxShadow !== 'none') tally(shadows, s.boxShadow.slice(0, 60));
for (const v of [s.paddingTop, s.paddingLeft, s.marginTop]) { const n = px(v); if (n) tally(space, `${n}px`); }
}
return {
controls, states, colors, type, radii, shadows, space,
focusable: all.filter(el => el.matches('a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])')).length,
hasDisabled: all.some(el => el.matches('[disabled],[aria-disabled="true"]')),
title: document.title.slice(0, 60),
landmarks: ['header', 'nav', 'main', 'footer', 'aside'].filter(t => document.querySelector(t)).join(','),
h1: document.querySelectorAll('h1').length,
};
};
/**
* Focus state cannot be read from a stylesheet: a `:focus-visible` rule may exist and be overridden,
* and `:focus-visible` itself is a heuristic that programmatic `.focus()` does not reliably trigger.
* So drive the real thing — press Tab and look at what the browser actually paints. Tabbing also
* skips disabled and unfocusable elements for free, which a `.focus()` loop reports as failures.
*/
const PROBE_FOCUS = async (page, maxStops = 40) => {
const paint = () => {
const cs = getComputedStyle;
// Only properties that actually PAINT. outline-offset alone draws nothing, and including it
// let an element that kills its outline still read as "focus state changed".
const sig = el => { const s = cs(el); return `${s.outlineStyle === 'none' ? 'no-outline' : s.outline} ${s.boxShadow} ${s.borderColor} ${s.backgroundColor} ${s.color} ${s.textDecorationLine}`; };
const els = [...document.querySelectorAll('a[href],button,input,select,textarea,summary,[tabindex]:not([tabindex="-1"])')];
els.forEach((el, i) => el.setAttribute('data-ss-i', String(i)));
return els.map(sig);
};
const unfocused = await page.evaluate(paint);
await page.evaluate(() => (document.activeElement || document.body).blur?.());
const bad = [], seen = new Set();
for (let i = 0; i < maxStops; i++) {
await page.keyboard.press('Tab');
const stop = await page.evaluate(() => {
const el = document.activeElement;
if (!el || el === document.body || !el.hasAttribute?.('data-ss-i')) return null;
const s = getComputedStyle(el);
const r = el.getBoundingClientRect();
return {
i: +el.getAttribute('data-ss-i'),
sig: `${s.outlineStyle === 'none' ? 'no-outline' : s.outline} ${s.boxShadow} ${s.borderColor} ${s.backgroundColor} ${s.color} ${s.textDecorationLine}`,
label: el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') + ' “' + (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0, 20) + '”',
offscreen: r.width < 1 || r.height < 1,
};
});
if (!stop) continue;
if (seen.has(stop.i)) break; // wrapped around the tab ring
seen.add(stop.i);
if (!stop.offscreen && unfocused[stop.i] === stop.sig) bad.push(stop.label);
}
return { probed: seen.size, noFocusRing: bad };
};
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ viewport: { width, height: 900 } });
await mkdir(outDir, { recursive: true });
const perRoute = [];
for (const url of urls) {
const page = await ctx.newPage();
const errs = [];
page.on('pageerror', e => errs.push(e.message));
page.on('console', m => m.type() === 'error' && errs.push(m.text()));
try {
process.stderr.write(`[systemscan] ${url}\n`);
const resp = await page.goto(url, { waitUntil: 'commit', timeout: 30_000 });
const dcl = await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).then(() => true).catch(() => false);
await page.waitForTimeout(dcl ? 2500 : 6000);
if (!dcl) await page.evaluate(() => window.stop()).catch(() => {});
const data = await page.evaluate(COLLECT);
const focus = await PROBE_FOCUS(page);
const status = resp?.status?.() ?? 0;
// A route that renders nothing contributes nothing to the counts, so a mistyped route list made
// the gate QUIETER instead of louder. A gate that goes green on a 404 is worse than no gate.
const empty = !data.landmarks && !data.h1 && !data.focusable;
perRoute.push({ url, ...data, focus, errors: errs, status, empty });
if (status >= 400) console.error(`[systemscan] ${url} → HTTP ${status}`);
else if (empty) console.error(`[systemscan] ${url} → rendered nothing measurable (no landmark, no h1, no focusable)`);
if (shots) {
// one exemplar screenshot per distinct control variant, so the report has a picture of the
// drift and not only a count of it
for (const [kind, variants] of Object.entries(data.controls)) {
let i = 0;
for (const [, v] of Object.entries(variants)) {
if (!v.tag) continue;
const name = `${kind}-${perRoute.length}-${i++}.png`;
try {
const box = await page.$(`[data-ss-shot="${v.tag}"]`); // the element we actually measured
if (box) { await box.scrollIntoViewIfNeeded({ timeout: 4000 }); await box.screenshot({ path: join(outDir, 'components', name), timeout: 8000 }); v.shot = `components/${name}`; }
} catch { /* an element that will not sit still is not worth failing the run over */ }
}
}
}
} catch (e) {
perRoute.push({ url, error: e.message.split('\n')[0] });
} finally { await page.close(); }
}
// ---- aggregate across routes -------------------------------------------------
const ok = perRoute.filter(r => !r.error);
if (!ok.length) { console.error('[systemscan] no route could be read.'); await browser.close(); process.exit(1); }
// A document scanned twice (`/settings` and `/settings#state-error` are the same DOM) must not count
// twice — summing across URLs meant scanning MORE thoroughly hid genuine one-off variants, which is
// the opposite of what `system.md` tells you to do. Count per document, take the max, then sum.
const docOf = u => u.split('#')[0];
const docs = [...new Set(ok.map(r => docOf(r.url)))];
const mergeCount = key => {
const m = {};
for (const r of ok) for (const [k, n] of Object.entries(r[key] || {})) {
(m[k] ??= { perDoc: {} });
const d = docOf(r.url);
m[k].perDoc[d] = Math.max(m[k].perDoc[d] || 0, n);
}
return Object.entries(m)
.map(([k, v]) => ({ k, total: Object.values(v.perDoc).reduce((a, b) => a + b, 0), routes: Object.keys(v.perDoc).length }))
.sort((a, b) => b.total - a.total);
};
const controlVariants = {};
for (const r of ok) for (const [kind, vars] of Object.entries(r.controls || {})) {
(controlVariants[kind] ??= {});
for (const [sig, v] of Object.entries(vars)) {
const slot = (controlVariants[kind][sig] ??= { perDoc: {}, sample: v.sample, shot: v.shot });
const d = docOf(r.url);
slot.perDoc[d] = Math.max(slot.perDoc[d] || 0, v.count);
slot.shot ??= v.shot;
}
}
for (const vars of Object.values(controlVariants)) for (const v of Object.values(vars)) {
v.count = Object.values(v.perDoc).reduce((a, b) => a + b, 0);
v.routes = new Set(Object.keys(v.perDoc));
}
// States, gathered the same way but never counted against the variant budget.
const controlStates = {};
for (const r of ok) for (const [kind, st] of Object.entries(r.states || {}))
for (const [name, n] of Object.entries(st)) {
((controlStates[kind] ??= {})[name] ??= 0);
controlStates[kind][name] += n;
}
const fails = [], warns = [];
for (const [kind, vars] of Object.entries(controlVariants)) {
const n = Object.keys(vars).length, b = budgetFor(kind);
if (n > b) fails.push(`${kind}: ${n} distinct rendered variants (budget ${b}). A variant nobody can name is drift.`);
for (const [sig, v] of Object.entries(vars)) {
if (v.count === 1 && n > 1) warns.push(`${kind} variant used exactly once ("${v.sample}") — either promote it into the system or delete it: ${sig}`);
}
}
const blank = perRoute.filter(r => !r.error && (r.status >= 400 || r.empty));
if (blank.length) fails.push(`${blank.length} route(s) rendered nothing measurable or returned an error status — a gate that goes green on a 404 is worse than no gate: ${blank.map(r => `${r.url}${r.status >= 400 ? ` (HTTP ${r.status})` : ''}`).join(', ')}`);
const noFocus = ok.flatMap(r => (r.focus?.noFocusRing || []).map(t => `${r.url} → ${t}`));
if (noFocus.length) fails.push(`${noFocus.length} interactive element(s) paint identically when focused — keyboard users cannot see where they are`);
const colors = mergeCount('colors'), type = mergeCount('type'), radii = mergeCount('radii'), shadows = mergeCount('shadows');
if (docs.length > 1) {
for (const [label, list] of [['colour', colors], ['type step', type], ['radius', radii]]) {
const singles = list.filter(x => x.routes === 1 && x.total >= 3);
if (singles.length) warns.push(`${singles.length} ${label}(s) appear on exactly one route and nowhere else — that is where the system is splitting: ${singles.slice(0, 4).map(s => s.k).join(' · ')}`);
}
}
if (type.length > 12) warns.push(`${type.length} distinct type steps across the product — a scale nobody can hold in their head is not a scale`);
if (!ok.some(r => r.hasDisabled)) warns.push('no disabled control appeared on any route — the disabled state is probably undesigned, not absent');
// ---- component contact sheet -------------------------------------------------
let sheet = null;
const tiles = Object.entries(controlVariants).flatMap(([kind, vars]) =>
Object.entries(vars).filter(([, v]) => v.shot).map(([, v], i) => ({ kind, i, ...v })));
if (shots && tiles.length) {
const html = `<!doctype html><meta charset="utf-8"><style>
body{margin:0;background:#141414;font:12px/1.3 ui-monospace,monospace;color:#ddd}
.g{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;padding:12px}
figure{margin:0;background:#1e1e1e;border-radius:4px;overflow:hidden;padding:10px}
img{display:block;max-width:100%;margin:0 auto 8px}
b{color:#fff}
</style><div class=g>${tiles.map(t =>
`<figure><img src="${t.shot}"><figcaption><b>${t.kind}</b> ×${t.count} · ${t.routes.size} route(s)</figcaption></figure>`).join('')}</div>`;
const f = join(outDir, '_components.html');
await writeFile(f, html, 'utf8');
const p = await ctx.newPage();
await p.setViewportSize({ width: 1200, height: 900 });
await p.goto('file:///' + f.replace(/\\/g, '/'), { waitUntil: 'load', timeout: 20_000 }).catch(() => {});
await p.waitForTimeout(900);
// shoot the grid, not the viewport — a fullPage shot of a two-row sheet is mostly empty canvas
const grid = await p.$('.g');
await (grid || p).screenshot({ path: join(outDir, 'components.png') });
await p.close();
sheet = 'components.png';
}
await browser.close();
// ---- report ------------------------------------------------------------------
const L = [];
L.push(`# SYSTEM-REPORT — ${ok.length} route(s), ${new Date().toISOString().slice(0, 10)}`);
L.push('');
L.push('The system as **painted**, not as documented. A token in the stylesheet that never renders is');
L.push('not part of the system; a one-off inline style is. Read this against `design/DESIGN.md` — every');
L.push('number below that DESIGN.md does not account for is drift.');
L.push('');
if (sheet) L.push(`**Look at \`${sheet}\`** — one tile per distinct rendered control variant. Two tiles that look the same to you but appear separately are the drift.\n`);
L.push('## Controls');
L.push('');
L.push('| kind | distinct variants | budget | total instances |');
L.push('|---|---|---|---|');
for (const [kind, vars] of Object.entries(controlVariants)) {
const n = Object.keys(vars).length;
L.push(`| ${kind} | **${n}** | ${budgetFor(kind)} | ${Object.values(vars).reduce((s, v) => s + v.count, 0)} |`);
}
L.push('');
for (const [kind, vars] of Object.entries(controlVariants)) {
L.push(`### ${kind}`);
for (const [sig, v] of Object.entries(vars)) L.push(`- ×${v.count} on ${v.routes.size} route(s) — "${v.sample}" — \`${sig}\``);
if (controlStates[kind]) {
const st = Object.entries(controlStates[kind]).map(([n, c]) => `${n} ×${c}`).join(' · ');
L.push(`- *states (not counted as variants): ${st}*`);
}
L.push('');
}
if (Object.keys(controlStates).length) {
L.push('> States — disabled, current, and controls inside a row carrying a `data-state` — are');
L.push('> excluded from the variant budget. A disabled button paints differently on purpose; a');
L.push('> product that has no disabled state at all should not score better than one that does.');
L.push('');
}
L.push('## Tokens as rendered');
L.push('');
for (const [label, list] of [['Colour', colors], ['Type step', type], ['Radius', radii], ['Shadow', shadows]]) {
L.push(`**${label}** (${list.length} distinct)`);
for (const x of list.slice(0, 10)) L.push(`- \`${x.k}\` — ${x.total}× on ${x.routes} route(s)`);
if (list.length > 10) L.push(`- …and ${list.length - 10} more`);
L.push('');
}
L.push('## Per route');
L.push('');
L.push('| route | landmarks | h1 | focusable | console errors |');
L.push('|---|---|---|---|---|');
for (const r of ok) L.push(`| ${r.url} | ${r.landmarks || '—'} | ${r.h1} | ${r.focusable} | ${r.errors.length} |`);
for (const r of perRoute.filter(r => r.error)) L.push(`| ${r.url} | **unreachable** — ${r.error} | | | |`);
L.push('');
if (fails.length) { L.push('## FAIL'); fails.forEach(f => L.push(`- ${f}`)); L.push(''); }
if (noFocus.length) { L.push('### Elements with no visible focus state'); noFocus.slice(0, 20).forEach(t => L.push(`- ${t}`)); L.push(''); }
if (warns.length) { L.push('## WARN'); warns.forEach(w => L.push(`- ${w}`)); L.push(''); }
if (!fails.length && !warns.length) L.push('No drift detected. The product renders one system.\n');
await writeFile(join(outDir, 'SYSTEM-REPORT.md'), L.join('\n'), 'utf8');
await writeFile(join(outDir, 'system.json'), JSON.stringify({
routes: perRoute.map(r => ({ ...r, controls: undefined })),
controls: Object.fromEntries(Object.entries(controlVariants).map(([k, v]) =>
[k, Object.entries(v).map(([sig, x]) => ({ sig, count: x.count, routes: [...x.routes], sample: x.sample }))])),
colors, type, radii, shadows, fails, warns,
}, null, 2), 'utf8');
console.log(`\n=== systemscan ===`);
console.log(`routes : ${ok.length}/${perRoute.length} read (${docs.length} distinct document(s))`);
console.log(`controls: ${Object.entries(controlVariants).map(([k, v]) => `${k} ${Object.keys(v).length}`).join(' · ') || '—'}`);
console.log(`tokens : ${colors.length} colours · ${type.length} type steps · ${radii.length} radii · ${shadows.length} shadows`);
console.log(`report : ${join(outDir, 'SYSTEM-REPORT.md')}${sheet ? `\nsheet : ${join(outDir, sheet)}` : ''}`);
for (const f of fails) console.log(`FAIL ${f}`);
for (const w of warns.slice(0, 5)) console.log(`WARN ${w}`);
process.exit(fails.length ? 1 : 0);