@nurix/etna 0.19.2 → 0.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +2 -2
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/rules/frontend.md +3 -3
- package/data/harnesses/code/skills/design/SKILL.md +20 -8
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +10 -5
- package/data/harnesses/code/skills/design/data/combinations/Readme.md +8 -4
- package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +113 -0
- package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +11 -0
- package/data/harnesses/code/skills/design/data/combinations/manifest.json +57 -10
- package/data/harnesses/code/skills/design/data/combinations/motion.csv +10 -0
- package/data/harnesses/code/skills/design/data/combinations/palettes.csv +92 -49
- package/data/harnesses/code/skills/design/data/combinations/styles.csv +67 -54
- package/data/harnesses/code/skills/design/data/combinations/typography.csv +64 -61
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +4 -4
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +5 -5
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +6 -6
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +4 -4
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +4 -4
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +2 -2
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +6 -6
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +5 -5
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +11 -7
- package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +256 -141
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +123 -123
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +1 -1
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +10 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +307 -2
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +1 -1
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +10 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +310 -2
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +313 -1
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +1 -1
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +10 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +309 -1
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +25 -25
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +18 -18
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +82 -105
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +97 -93
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +16 -15
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +34 -34
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +5 -5
- package/data/harnesses/code/skills/design/references/create.md +49 -7
- package/data/harnesses/code/skills/design/references/diagnosis.md +3 -3
- package/data/harnesses/code/skills/design/references/hard-rules.md +25 -3
- package/data/harnesses/code/skills/design/references/improve.md +32 -3
- package/data/harnesses/code/skills/design/references/interaction.md +3 -3
- package/data/harnesses/code/skills/design/references/introspect.md +1 -1
- package/data/harnesses/code/skills/design/references/lifecycle.md +2 -2
- package/data/harnesses/code/skills/design/references/personality.md +280 -133
- package/data/harnesses/code/skills/design/references/structure.md +22 -1
- package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +306 -26
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +596 -104
- package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +200 -11
- package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +65 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +95 -11
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +50 -5
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +214 -14
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +242 -25
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +81 -6
- package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +101 -5
- package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +142 -17
- package/data/harnesses/etna/CLAUDE.md +1 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// skills/design/scripts/preship-sweep.mjs — verification-ladder rung 1 (D21/D23): static sweep — per-file catalog + manifest drift via lib/checks.mjs + cross-file machine checks + mandatory-procedures checklist + scope cross-check. --files <a> <b> … scopes the sweep (improve's end-of-run verify); no args = whole-app. --manifest <design.tokens.json | artifact dir> overrides the drift vocabulary source (default: <project>/design.tokens.json).
|
|
1
|
+
// skills/design/scripts/preship-sweep.mjs — verification-ladder rung 1 (D21/D23): static sweep — per-file catalog + manifest drift via lib/checks.mjs (incl. the budget-stamp vocabulary rules typ-20/col-16/mot-08) + cross-file machine checks + mandatory-procedures checklist + the resolved style row's checklist walk (the corpus's one positive verification) + scope cross-check. --files <a> <b> … scopes the sweep (improve's end-of-run verify); no args = whole-app. --manifest <design.tokens.json | artifact dir> overrides the drift vocabulary source (default: <project>/design.tokens.json).
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, dirname, resolve, relative } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -194,6 +194,44 @@ function procChecklist(catalog) {
|
|
|
194
194
|
return lines;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Resolve a --manifest argument or project default to the design.tokens.json file path, or null.
|
|
198
|
+
function tokensFileOf(manifestPath) {
|
|
199
|
+
if (!manifestPath || !existsSync(manifestPath)) return null;
|
|
200
|
+
try {
|
|
201
|
+
if (statSync(manifestPath).isDirectory()) {
|
|
202
|
+
const candidate = join(manifestPath, 'design.tokens.json');
|
|
203
|
+
return existsSync(candidate) ? candidate : null;
|
|
204
|
+
}
|
|
205
|
+
} catch { return null; }
|
|
206
|
+
return manifestPath;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// The resolved style row's checklist items, walked at preship — the sweep's one positive verification:
|
|
210
|
+
// the catalog proves nothing is broken; these items assert the resolved style actually landed. The row
|
|
211
|
+
// is read from the artifact root $description's resolution record (a "styles.csv#<id>" reference from
|
|
212
|
+
// seed composition); library/default/project looks carry no style row and print a one-line note.
|
|
213
|
+
function styleChecklist(manifestPath, section) {
|
|
214
|
+
const tokensFile = tokensFileOf(manifestPath);
|
|
215
|
+
if (!tokensFile) { section.push(' (no design.tokens.json found — no resolved style row to walk)'); return; }
|
|
216
|
+
let root;
|
|
217
|
+
try { root = JSON.parse(readText(tokensFile)); } catch { section.push(' (design.tokens.json does not parse — no resolved style row to walk)'); return; }
|
|
218
|
+
const record = typeof root?.$description === 'string' ? root.$description : '';
|
|
219
|
+
const ref = record.match(/styles\.csv#(\d+)/);
|
|
220
|
+
if (!ref) { section.push(' (the artifact resolution record names no styles.csv row — library/default/project looks carry no style checklist)'); return; }
|
|
221
|
+
const rows = parseCsv(readText(join(SCRIPT_DIR, '..', 'data', 'combinations', 'styles.csv')));
|
|
222
|
+
if (rows.length < 2) { section.push(yellow(' data/combinations/styles.csv missing or empty — cannot walk the resolved style row\'s checklist.')); return; }
|
|
223
|
+
const header = rows[0];
|
|
224
|
+
const idCol = header.indexOf('id');
|
|
225
|
+
const checklistCol = header.indexOf('checklist');
|
|
226
|
+
const row = rows.slice(1).find((r) => (r[idCol] || '').trim() === ref[1]);
|
|
227
|
+
if (!row || checklistCol < 0) { section.push(yellow(` resolution record names styles.csv#${ref[1]} but no such row (or no checklist column) exists — reconcile the record.`)); return; }
|
|
228
|
+
const items = (row[checklistCol] || '').split(';').map((s) => s.trim()).filter(Boolean);
|
|
229
|
+
if (!items.length) { section.push(` styles.csv#${ref[1]} carries no checklist items.`); return; }
|
|
230
|
+
section.push(` resolved style row: styles.csv#${ref[1]} — walk every box now, on the built surfaces:`);
|
|
231
|
+
for (const item of items) section.push(` [ ] ${item}`);
|
|
232
|
+
section.push(' Handle these before finalizing: an unchecked box means the resolved style did not land — fix the surface, not the checklist.');
|
|
233
|
+
}
|
|
234
|
+
|
|
197
235
|
// Assemble and print the whole sweep, then exit non-zero when catalog or cross-file checks found violations.
|
|
198
236
|
function main() {
|
|
199
237
|
const args = process.argv.slice(2);
|
|
@@ -237,7 +275,13 @@ function main() {
|
|
|
237
275
|
console.log(procChecklist(catalog).join('\n'));
|
|
238
276
|
console.log('');
|
|
239
277
|
|
|
240
|
-
console.log(bold('(
|
|
278
|
+
console.log(bold('(d) Resolved style row — checklist walk (the positive verification)'));
|
|
279
|
+
const secD = [];
|
|
280
|
+
styleChecklist(manifestPath, secD);
|
|
281
|
+
console.log(secD.join('\n'));
|
|
282
|
+
console.log('');
|
|
283
|
+
|
|
284
|
+
console.log(bold('(e) Scope cross-check (proc-14)'));
|
|
241
285
|
const surfaces = countSurfaces(project);
|
|
242
286
|
const deliverables = countDeliverables(project);
|
|
243
287
|
if (surfaces == null) {
|
|
@@ -248,7 +292,7 @@ function main() {
|
|
|
248
292
|
}
|
|
249
293
|
console.log('');
|
|
250
294
|
} else {
|
|
251
|
-
console.log('(scoped run — the mandatory-procedures checklist and scope cross-check run on the whole-app sweep)');
|
|
295
|
+
console.log('(scoped run — the mandatory-procedures checklist, style-row walk, and scope cross-check run on the whole-app sweep)');
|
|
252
296
|
console.log('');
|
|
253
297
|
}
|
|
254
298
|
|
|
@@ -257,10 +301,11 @@ function main() {
|
|
|
257
301
|
const reasons = [];
|
|
258
302
|
if (catalogCount > 0) reasons.push(`${catalogCount} per-file violation(s)`);
|
|
259
303
|
if (crossCount > 0) reasons.push(`${crossCount} cross-file violation(s)`);
|
|
260
|
-
console.log(red(`FAIL preship-sweep — ${reasons.join('; ')}.
|
|
304
|
+
console.log(red(`FAIL preship-sweep — ${reasons.join('; ')}. Handle every finding above before finalizing, then re-run.`));
|
|
261
305
|
process.exit(1);
|
|
262
306
|
}
|
|
263
|
-
console.log(green(scoped ? 'PASS preship-sweep (scoped) — no static violations in the given files.' : 'PASS preship-sweep — no static violations; work the
|
|
307
|
+
console.log(green(scoped ? 'PASS preship-sweep (scoped) — no static violations in the given files.' : 'PASS preship-sweep — no static violations; work the checklists above before ship.'));
|
|
308
|
+
console.log('A clean scan means nothing is mechanically broken — it does not mean the design is good; the checklist walks above are where good is decided.');
|
|
264
309
|
process.exit(0);
|
|
265
310
|
}
|
|
266
311
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// skills/design/scripts/render-rung.mjs — verification-ladder rung 2: deterministic render checks + pixel instruments (D32)
|
|
1
|
+
// skills/design/scripts/render-rung.mjs — verification-ladder rung 2: deterministic render checks + pixel instruments (D32). --require makes the rung a gate (create's ladder: fail-closed when no headless browser is importable — emission contract 4); without it a skip degrades gracefully but always lands as a named finding, never a one-line notice. --screenshot <outdir> saves one PNG per viewport.
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { inflateSync } from 'node:zlib';
|
|
5
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { join, resolve } from 'node:path';
|
|
7
|
+
import { MOTION_STAGGER_CLASS, STAGGER_CHILD_CAP } from './lib/vocabulary.mjs';
|
|
7
8
|
|
|
8
9
|
// The four viewports the ladder sweeps; width is the breakpoint, height is a fixed tall canvas.
|
|
9
10
|
const VIEWPORTS = [375, 768, 1024, 1440];
|
|
@@ -19,7 +20,11 @@ const DEV_PORTS = [5173, 4173, 3000, 8080];
|
|
|
19
20
|
// Severity rank for the delivered (severity-ranked) list; auto-critical is never minted by machine render rules.
|
|
20
21
|
const SEVERITY_RANK = { 'auto-critical': 0, critical: 1, major: 2, minor: 3 };
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// The exact install command --require prints when no headless browser is importable (contract 4).
|
|
24
|
+
const BROWSER_INSTALL_CMD = 'npm install -D playwright && npx playwright install chromium';
|
|
25
|
+
|
|
26
|
+
// The token trio cod-15 measures per declared theme context.
|
|
27
|
+
const THEME_DRIFT_PROPS = ['--background', '--foreground', '--primary'];
|
|
23
28
|
|
|
24
29
|
// ----------------------------------------------------------------------------
|
|
25
30
|
// Pure helpers (exported for unit testing — no browser required)
|
|
@@ -205,19 +210,77 @@ function rgbKey(rgb) {
|
|
|
205
210
|
// CLI plumbing
|
|
206
211
|
// ----------------------------------------------------------------------------
|
|
207
212
|
|
|
208
|
-
// Parse argv into { project, url }.
|
|
213
|
+
// Parse argv into { project, url, require, screenshot }.
|
|
209
214
|
function parseArgs(argv) {
|
|
210
|
-
const out = { project: process.cwd(), url: null };
|
|
215
|
+
const out = { project: process.cwd(), url: null, require: false, screenshot: null };
|
|
211
216
|
for (let i = 0; i < argv.length; i++) {
|
|
212
217
|
const a = argv[i];
|
|
213
218
|
if (a === '--project') out.project = resolve(argv[++i] ?? '.');
|
|
214
219
|
else if (a.startsWith('--project=')) out.project = resolve(a.slice('--project='.length));
|
|
215
220
|
else if (a === '--url') out.url = argv[++i] ?? null;
|
|
216
221
|
else if (a.startsWith('--url=')) out.url = a.slice('--url='.length);
|
|
222
|
+
else if (a === '--require') out.require = true;
|
|
223
|
+
else if (a === '--screenshot') out.screenshot = resolve(argv[++i] ?? '.');
|
|
224
|
+
else if (a.startsWith('--screenshot=')) out.screenshot = resolve(a.slice('--screenshot='.length));
|
|
217
225
|
}
|
|
218
226
|
return out;
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
// Report a skipped rung and return the process exit code. Never a one-line notice: without
|
|
230
|
+
// --require the skip lands as a named finding in the fix-or-classify register (exit 0 — improve
|
|
231
|
+
// keeps graceful degradation); with --require (create's ladder) the rung is a gate and fails closed
|
|
232
|
+
// with the exact remedy (emission contract 4 — a create run cannot complete without having rendered).
|
|
233
|
+
function reportSkip(requireMode, reason, remedy) {
|
|
234
|
+
console.log(`[${requireMode ? 'critical' : 'major'}] render-rung Verification — render rung skipped: ${reason}`);
|
|
235
|
+
console.log(` fix · ${remedy}`);
|
|
236
|
+
console.log(' consequence · the render-only rules (cod-15, spc-16, mot-08 DOM, spc-10, typ-01, int-17/18, cod-12) did not run; the surface ships unrendered.');
|
|
237
|
+
if (requireMode) {
|
|
238
|
+
console.log(' --require: this rung is a gate on create runs — fix the remedy above and re-run.');
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
console.log(' classify this skip in the ship report or fix it — never ignore silently.');
|
|
242
|
+
console.log('');
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// cod-15 ground truth: the authored --background/--foreground/--primary byte forms per theme
|
|
247
|
+
// context the resolver declares, read from the project-root design-tokens artifact. Returns
|
|
248
|
+
// { light: { '--background': css, … }, dark?: … } or null when no artifact is readable.
|
|
249
|
+
export function loadAuthoredThemeColors(projectDir) {
|
|
250
|
+
const readJson = (name) => {
|
|
251
|
+
try { return JSON.parse(readFileSync(join(resolve(projectDir), name), 'utf8')); } catch { return null; }
|
|
252
|
+
};
|
|
253
|
+
const base = readJson('design.tokens.json');
|
|
254
|
+
const resolver = readJson('design.resolver.json');
|
|
255
|
+
if (!base || !base.color || !resolver) return null;
|
|
256
|
+
const themeModifier = (resolver.modifiers || []).find((m) => m && m.name === 'theme');
|
|
257
|
+
const contexts = Object.keys((themeModifier && themeModifier.contexts) || { light: [] });
|
|
258
|
+
const dark = readJson('design.tokens.dark.json');
|
|
259
|
+
const out = {};
|
|
260
|
+
for (const context of contexts) {
|
|
261
|
+
// The dark overlay carries the full color group (rule 10), so a token-level merge is the overlay.
|
|
262
|
+
const colorGroup = context === 'dark' && dark && dark.color ? { ...base.color, ...dark.color } : base.color;
|
|
263
|
+
const cssOf = (role) => {
|
|
264
|
+
let token = colorGroup[role];
|
|
265
|
+
for (let hops = 0; token && hops < 10; hops++) {
|
|
266
|
+
const ext = token.$extensions && token.$extensions['com.nurix.design'];
|
|
267
|
+
if (ext && typeof ext.css === 'string') return ext.css;
|
|
268
|
+
const v = token.$value;
|
|
269
|
+
if (typeof v === 'string' && /^\{color\.[^}]+\}$/.test(v)) token = colorGroup[v.slice(7, -1)];
|
|
270
|
+
else return null;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
};
|
|
274
|
+
const values = {};
|
|
275
|
+
for (const prop of THEME_DRIFT_PROPS) {
|
|
276
|
+
const css = cssOf(prop.slice(2));
|
|
277
|
+
if (css) values[prop] = css;
|
|
278
|
+
}
|
|
279
|
+
if (Object.keys(values).length) out[context] = values;
|
|
280
|
+
}
|
|
281
|
+
return Object.keys(out).length ? out : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
221
284
|
// Try to import playwright, playwright-core, then puppeteer from the target project; return a driver adapter or null.
|
|
222
285
|
async function loadBrowserDriver(projectDir) {
|
|
223
286
|
const require = createRequire(join(resolve(projectDir), 'package.json'));
|
|
@@ -415,6 +478,77 @@ function inPagePerfRead() {
|
|
|
415
478
|
return window.__perf || { cls: 0, lcp: 0 };
|
|
416
479
|
}
|
|
417
480
|
|
|
481
|
+
// cod-15: flip data-theme through each declared context and read the rendered token trio off the
|
|
482
|
+
// root element, restoring the boot attribute afterwards.
|
|
483
|
+
function inPageThemeTokens(payload) {
|
|
484
|
+
const html = document.documentElement;
|
|
485
|
+
const previous = html.getAttribute('data-theme');
|
|
486
|
+
const out = {};
|
|
487
|
+
for (const context of payload.contexts) {
|
|
488
|
+
html.setAttribute('data-theme', context);
|
|
489
|
+
const cs = getComputedStyle(html);
|
|
490
|
+
const read = {};
|
|
491
|
+
for (const prop of payload.props) read[prop] = cs.getPropertyValue(prop).trim();
|
|
492
|
+
out[context] = read;
|
|
493
|
+
}
|
|
494
|
+
if (previous === null) html.removeAttribute('data-theme');
|
|
495
|
+
else html.setAttribute('data-theme', previous);
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// spc-16: fixed-width table/grid cells whose rendered text exceeds the cell (scrollWidth >
|
|
500
|
+
// clientWidth) with no truncate or scroll affordance on the cell or a descendant wrapper.
|
|
501
|
+
function inPageCellTextFit(cap) {
|
|
502
|
+
const path = (el) => {
|
|
503
|
+
let s = el.tagName.toLowerCase();
|
|
504
|
+
if (el.id) s += '#' + el.id;
|
|
505
|
+
else if (el.classList.length) s += '.' + el.classList[0];
|
|
506
|
+
return s;
|
|
507
|
+
};
|
|
508
|
+
const offenders = [];
|
|
509
|
+
const cells = document.querySelectorAll('td, th, [role="cell"], [role="gridcell"], [role="columnheader"]');
|
|
510
|
+
for (const cell of cells) {
|
|
511
|
+
const cs = getComputedStyle(cell);
|
|
512
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') continue;
|
|
513
|
+
if (cell.scrollWidth <= cell.clientWidth + 1) continue; // content fits — an auto-layout cell grows instead of overflowing
|
|
514
|
+
let excused = false;
|
|
515
|
+
for (const node of [cell, ...cell.querySelectorAll('*')]) {
|
|
516
|
+
const ns = getComputedStyle(node);
|
|
517
|
+
if (/(auto|scroll)/.test(ns.overflowX)) { excused = true; break; }
|
|
518
|
+
if (ns.textOverflow === 'ellipsis' && /(hidden|clip)/.test(ns.overflowX)) { excused = true; break; }
|
|
519
|
+
if (ns.webkitLineClamp && ns.webkitLineClamp !== 'none') { excused = true; break; }
|
|
520
|
+
}
|
|
521
|
+
if (excused) continue;
|
|
522
|
+
offenders.push({ selector: path(cell), scrollWidth: Math.round(cell.scrollWidth), clientWidth: Math.round(cell.clientWidth) });
|
|
523
|
+
if (offenders.length >= cap) break;
|
|
524
|
+
}
|
|
525
|
+
return offenders;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// mot-08 (DOM half): every rendered stagger container's child count and its total sequence length,
|
|
529
|
+
// the latter as the largest resolved child animation-delay.
|
|
530
|
+
function inPageStaggerAudit(payload) {
|
|
531
|
+
const path = (el) => {
|
|
532
|
+
let s = el.tagName.toLowerCase();
|
|
533
|
+
if (el.id) s += '#' + el.id;
|
|
534
|
+
else if (el.classList.length) s += '.' + el.classList[0];
|
|
535
|
+
return s;
|
|
536
|
+
};
|
|
537
|
+
const out = [];
|
|
538
|
+
for (const container of document.querySelectorAll('.' + payload.className)) {
|
|
539
|
+
let maxDelayMs = 0;
|
|
540
|
+
for (const child of container.children) {
|
|
541
|
+
for (const raw of getComputedStyle(child).animationDelay.split(',')) {
|
|
542
|
+
const t = raw.trim();
|
|
543
|
+
const ms = t.endsWith('ms') ? parseFloat(t) : parseFloat(t) * 1000;
|
|
544
|
+
if (!Number.isNaN(ms)) maxDelayMs = Math.max(maxDelayMs, ms);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
out.push({ selector: path(container), childCount: container.children.length, maxDelayMs: Math.round(maxDelayMs) });
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
|
|
418
552
|
// Read the page's effective background colour for the white-space instrument.
|
|
419
553
|
function inPageBackground() {
|
|
420
554
|
const parse = (s) => {
|
|
@@ -457,12 +591,11 @@ function printFinding(f) {
|
|
|
457
591
|
|
|
458
592
|
// Run the render rung: load a browser, sweep viewports for machine findings, compute pixel instruments.
|
|
459
593
|
async function main() {
|
|
460
|
-
const { project, url: urlArg } = parseArgs(process.argv.slice(2));
|
|
594
|
+
const { project, url: urlArg, require: requireMode, screenshot: screenshotDir } = parseArgs(process.argv.slice(2));
|
|
461
595
|
|
|
462
596
|
const driver = await loadBrowserDriver(project);
|
|
463
597
|
if (!driver) {
|
|
464
|
-
|
|
465
|
-
return 0;
|
|
598
|
+
return reportSkip(requireMode, 'no headless browser importable from this project (tried playwright, playwright-core, puppeteer)', `install one: ${BROWSER_INSTALL_CMD}`);
|
|
466
599
|
}
|
|
467
600
|
|
|
468
601
|
let url = urlArg;
|
|
@@ -471,8 +604,7 @@ async function main() {
|
|
|
471
604
|
if (url) console.log(`render rung: no --url given; using detected dev server ${url}`);
|
|
472
605
|
}
|
|
473
606
|
if (!url) {
|
|
474
|
-
|
|
475
|
-
return 0;
|
|
607
|
+
return reportSkip(requireMode, 'no --url given and no dev server detected on common ports', 'start the dev server (npm run dev) or pass --url <dev-server-url>');
|
|
476
608
|
}
|
|
477
609
|
|
|
478
610
|
// Resolve axe-core from the project if present; gate only the accessible-name + ARIA-validity families.
|
|
@@ -493,6 +625,13 @@ async function main() {
|
|
|
493
625
|
console.log('render rung: no src/styles/personality.css found; the rendered token-drift check (cod-02) is skipped.');
|
|
494
626
|
}
|
|
495
627
|
|
|
628
|
+
// Load the authored per-theme trio for cod-15 from the project-root design-tokens artifact.
|
|
629
|
+
const authoredThemes = loadAuthoredThemeColors(project);
|
|
630
|
+
if (!authoredThemes) {
|
|
631
|
+
console.log('render rung: no readable design-tokens artifact at the project root; the per-theme rendered token drift check (cod-15) is skipped.');
|
|
632
|
+
}
|
|
633
|
+
let themeDriftChecked = false;
|
|
634
|
+
|
|
496
635
|
const findings = new Map();
|
|
497
636
|
const instruments = {};
|
|
498
637
|
const axeRules = [...(axeSource ? AXE_ACCESSIBLE_NAME.concat(AXE_ARIA_VALIDITY) : [])];
|
|
@@ -508,9 +647,8 @@ async function main() {
|
|
|
508
647
|
try {
|
|
509
648
|
await page.goto(url, { waitUntil: 'load', timeout: 30000 });
|
|
510
649
|
} catch (err) {
|
|
511
|
-
console.log(`render rung: could not load ${url} at ${width}px (${err.message}); is the dev server running? rung skipped.`);
|
|
512
650
|
await browser.close();
|
|
513
|
-
return
|
|
651
|
+
return reportSkip(requireMode, `could not load ${url} at ${width}px (${err.message})`, 'start the dev server (npm run dev) or pass --url <dev-server-url>');
|
|
514
652
|
}
|
|
515
653
|
await new Promise((r) => setTimeout(r, 150));
|
|
516
654
|
|
|
@@ -570,7 +708,65 @@ async function main() {
|
|
|
570
708
|
}
|
|
571
709
|
}
|
|
572
710
|
|
|
573
|
-
// (5)
|
|
711
|
+
// (5) cod-15 — per-theme rendered token drift (viewport-independent; measured once, with
|
|
712
|
+
// data-theme flipped through each context the resolver declares).
|
|
713
|
+
if (authoredThemes && !themeDriftChecked) {
|
|
714
|
+
themeDriftChecked = true;
|
|
715
|
+
try {
|
|
716
|
+
const rendered = await page.evaluate(inPageThemeTokens, { contexts: Object.keys(authoredThemes), props: THEME_DRIFT_PROPS });
|
|
717
|
+
const normalize = (s) => String(s || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
718
|
+
for (const [context, roles] of Object.entries(authoredThemes)) {
|
|
719
|
+
for (const [prop, authored] of Object.entries(roles)) {
|
|
720
|
+
const got = (rendered[context] && rendered[context][prop]) || '';
|
|
721
|
+
if (normalize(got) === normalize(authored)) continue;
|
|
722
|
+
addFinding(findings, width, {
|
|
723
|
+
anchor: 'cod-15', dimension: 'S3 Design Tokens', statement: 'rendered token drifts from the authored value in this theme context',
|
|
724
|
+
evidenceKey: `${context}|${prop}`, where: `data-theme="${context}" · ${prop} renders ${JSON.stringify(got || '(empty)')}`,
|
|
725
|
+
before: got || '(empty)', after: `${authored} (the ${context} context's authored value)`,
|
|
726
|
+
consequence: "the theme context is cascade-dead — the library's later :root block wins and the surface silently reverts to shadcn gray", severity: 'critical',
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
} catch (err) {
|
|
731
|
+
console.log(`render rung: the per-theme token drift check (cod-15) failed to run (${err.message}); continuing.`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// (6) spc-16 — fixed-width cell text fit: rendered text exceeds the cell with no truncate or
|
|
736
|
+
// scroll affordance (document-level overflow cannot see intra-row column paint-over).
|
|
737
|
+
const cellOffenders = await page.evaluate(inPageCellTextFit, 40);
|
|
738
|
+
for (const cell of cellOffenders) {
|
|
739
|
+
addFinding(findings, width, {
|
|
740
|
+
anchor: 'spc-16', dimension: 'P3 Spatial', statement: 'rendered text exceeds its fixed-width cell with no truncate or scroll affordance',
|
|
741
|
+
evidenceKey: cell.selector, where: `${cell.selector} · content ${cell.scrollWidth}px > cell ${cell.clientWidth}px`,
|
|
742
|
+
before: `${cell.scrollWidth}px of content in a ${cell.clientWidth}px cell`, after: 'text fits, truncates (truncate), or scrolls (overflow-x-auto)',
|
|
743
|
+
consequence: 'cell text paints over or clips against the neighbouring column; the row reads as broken data', severity: 'major',
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// (7) mot-08 (DOM half) — rendered stagger containers stay within the child cap and the
|
|
748
|
+
// 1s total-sequence budget (the source shell owns the per-item band and the budget stamp).
|
|
749
|
+
const staggerContainers = await page.evaluate(inPageStaggerAudit, { className: MOTION_STAGGER_CLASS });
|
|
750
|
+
for (const s of staggerContainers) {
|
|
751
|
+
if (s.childCount > STAGGER_CHILD_CAP) {
|
|
752
|
+
addFinding(findings, width, {
|
|
753
|
+
anchor: 'mot-08', dimension: 'B2 Motion', statement: `arrival cascade exceeds the ${STAGGER_CHILD_CAP}-child cap`,
|
|
754
|
+
evidenceKey: `count|${s.selector}`, where: `${s.selector} · ${s.childCount} children in a ${MOTION_STAGGER_CLASS} container`,
|
|
755
|
+
before: `${s.childCount} staggered children`, after: `<= ${STAGGER_CHILD_CAP} (children past the cap arrive with the last delayed one)`,
|
|
756
|
+
consequence: 'a long cascade turns arrival feedback into a wait; the operator reads it as slowness', severity: 'major',
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
if (s.maxDelayMs >= 1000) {
|
|
760
|
+
addFinding(findings, width, {
|
|
761
|
+
anchor: 'mot-08', dimension: 'B2 Motion', statement: 'total stagger sequence at or over the 1s budget',
|
|
762
|
+
evidenceKey: `sequence|${s.selector}`, where: `${s.selector} · last child arrives at ${s.maxDelayMs}ms`,
|
|
763
|
+
before: `${s.maxDelayMs}ms total sequence`, after: '< 1000ms (per-item delay x child count under the budget)',
|
|
764
|
+
consequence: 'content the operator came for is still arriving after a second; the cascade reads as lag, not polish', severity: 'major',
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// (8) int-17 / int-18 — axe accessible-name + ARIA-validity families.
|
|
574
770
|
if (axeSource) {
|
|
575
771
|
try {
|
|
576
772
|
await page.addScriptTag({ content: axeSource });
|
|
@@ -596,7 +792,7 @@ async function main() {
|
|
|
596
792
|
}
|
|
597
793
|
}
|
|
598
794
|
|
|
599
|
-
// (
|
|
795
|
+
// (9) cod-12 — runtime perf budgets measurable in a passive headless load: LCP and CLS.
|
|
600
796
|
// INP and frame time require real interaction and belong to the interactive rung (3), not this one.
|
|
601
797
|
try {
|
|
602
798
|
const perf = await page.evaluate(inPagePerfRead);
|
|
@@ -621,6 +817,10 @@ async function main() {
|
|
|
621
817
|
// Instruments — pixel evidence, never a verdict.
|
|
622
818
|
try {
|
|
623
819
|
const shot = Buffer.from(await page.screenshot({ type: 'png' }));
|
|
820
|
+
if (screenshotDir) {
|
|
821
|
+
mkdirSync(screenshotDir, { recursive: true });
|
|
822
|
+
writeFileSync(join(screenshotDir, `${width}.png`), shot);
|
|
823
|
+
}
|
|
624
824
|
const bg = await page.evaluate(inPageBackground);
|
|
625
825
|
const img = decodePng(shot);
|
|
626
826
|
instruments[width] = {
|