@nurix/etna 0.19.3 → 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/harness.json +1 -1
- 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/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// skills/design/scripts/lib/checks.mjs — the machine-check engine (beam 2, D19): every detector=source catalog rule as pure regex/token analysis over .tsx/.ts/.css/.html text, plus the lever-manifest drift check (cod-02, D10) whose vocabulary is read from the project's design-tokens artifact (design.tokens.json). Pure library — no argv, no CLI banner, no process.exit; fired at structural boundaries by phase-gate.mjs, preship-sweep.mjs, and self-integrity.mjs. Findings are [{rule, file, line, excerpt, fix}].
|
|
2
2
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { findBudgetStamps, MOTION_STAGGER_CLASS, STAGGER_CHILD_CAP } from './vocabulary.mjs';
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// File-type routing
|
|
@@ -123,6 +124,32 @@ function normColor(lit) {
|
|
|
123
124
|
// ---------------------------------------------------------------------------
|
|
124
125
|
|
|
125
126
|
const RE_COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/g;
|
|
127
|
+
|
|
128
|
+
// CSS generic + system family keywords — never counted as declared app families (typ-06);
|
|
129
|
+
// narrower than SYSTEM_FONT_KEYWORDS, whose web-safe named fonts (Roboto, Arial…) DO count.
|
|
130
|
+
const GENERIC_FAMILY_KEYWORDS = new Set([
|
|
131
|
+
'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy', 'math', 'emoji', 'fangsong',
|
|
132
|
+
'system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded',
|
|
133
|
+
'-apple-system', 'blinkmacsystemfont', 'sfmono-regular',
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// Base signature of a color literal — the first three channels with the alpha slot stripped,
|
|
137
|
+
// hex unified onto rgb — so an emitted color whose alpha rides var()/calc() (the intensity-
|
|
138
|
+
// multiplied shadow layers, which RE_COLOR_LITERAL truncates mid-calc) can still be matched
|
|
139
|
+
// against the manifest vocabulary.
|
|
140
|
+
function colorBaseSig(lit) {
|
|
141
|
+
const s = lit.trim().toLowerCase();
|
|
142
|
+
if (/^#[0-9a-f]{3,8}$/.test(s)) {
|
|
143
|
+
const h = expandHex(s);
|
|
144
|
+
return `rgb:${parseInt(h.slice(0, 2), 16)},${parseInt(h.slice(2, 4), 16)},${parseInt(h.slice(4, 6), 16)}`;
|
|
145
|
+
}
|
|
146
|
+
const fn = s.match(/^(rgba?|hsla?|oklch)\(([^/)]*)/);
|
|
147
|
+
if (!fn) return null;
|
|
148
|
+
const nums = fn[2].match(/-?\d*\.?\d+%?/g);
|
|
149
|
+
if (!nums || nums.length < 3) return null;
|
|
150
|
+
const kind = fn[1].startsWith('rgb') ? 'rgb' : fn[1].startsWith('hsl') ? 'hsl' : 'oklch';
|
|
151
|
+
return `${kind}:${nums.slice(0, 3).join(',')}`;
|
|
152
|
+
}
|
|
126
153
|
const TW_UTILITY = '(?:bg|text|border|ring|ring-offset|fill|stroke|from|via|to|outline|divide|decoration|caret|accent|placeholder|shadow)';
|
|
127
154
|
const PALETTE_HUES = 'red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|grey|zinc|neutral|stone';
|
|
128
155
|
const RE_PALETTE_CLASS = new RegExp(`\\b${TW_UTILITY}-(${PALETTE_HUES})-(\\d{2,3})\\b`, 'g');
|
|
@@ -244,7 +271,10 @@ export function checkLine(add, file, ext, lineNo, line, fileText) {
|
|
|
244
271
|
{
|
|
245
272
|
const neg = line.match(/(?:letter-spacing\s*:\s*|tracking-\[)(-?\d*\.?\d+)em/);
|
|
246
273
|
if (neg && parseFloat(neg[1]) < -0.04) add('typ-08', file, lineNo, neg[0], 'Letter-spacing never below the -0.04em floor.');
|
|
247
|
-
|
|
274
|
+
// A `--tracking-tighter: var(…)` @theme line REMAPS the utility onto an authored token —
|
|
275
|
+
// that definition is the fix for this rule, not a violation; literal values still fall
|
|
276
|
+
// to the numeric floor check above.
|
|
277
|
+
if (/\btracking-tighter\b/.test(line) && !/--tracking-tighter\s*:\s*var\(/.test(line)) add('typ-08', file, lineNo, 'tracking-tighter', 'tracking-tighter (-0.05em) is below the -0.04em floor; use tracking-tight at most.');
|
|
248
278
|
if (/\buppercase\b/.test(line) && !/tracking-(?:wide|wider|widest|\[)/.test(line)) add('typ-08', file, lineNo, 'uppercase', 'All-caps micro-labels carry positive tracking (>= 0.02em, caps band 0.05-0.12em); add tracking-wide.');
|
|
249
279
|
}
|
|
250
280
|
// typ-09: viewport-fluid font sizing.
|
|
@@ -406,6 +436,35 @@ export function checkLine(add, file, ext, lineNo, line, fileText) {
|
|
|
406
436
|
if (d > 0 && (d < 150 || d > 300)) add('mot-07', file, lineNo, m[0], 'Loading-indicator delay sits in the 150-300ms band (nothing appears under 500ms).');
|
|
407
437
|
}
|
|
408
438
|
}
|
|
439
|
+
// mot-08 (numeric half): the per-item stagger unit holds the 0-60ms band, a declared cascade never
|
|
440
|
+
// reaches past the child cap, and no stagger delay lands at or over the 1s sequence ceiling. Child
|
|
441
|
+
// count and the actual total are confirmed on the rendered DOM at the render rung.
|
|
442
|
+
{
|
|
443
|
+
const unit = line.match(/--stagger-unit\s*:\s*(-?\d*\.?\d+)ms/);
|
|
444
|
+
if (unit && (parseFloat(unit[1]) < 0 || parseFloat(unit[1]) > 60)) {
|
|
445
|
+
add('mot-08', file, lineNo, unit[0], 'The stagger-unit token holds 0-60ms per item; pull the unit back into the band.');
|
|
446
|
+
}
|
|
447
|
+
const idxDelay = line.match(/(?:animationDelay|animation-delay|transitionDelay|transition-delay)[^;,}]*?(?:\b(?:i|idx|index)\s*\*\s*(\d*\.?\d+)|(\d*\.?\d+)\s*\*\s*(?:i|idx|index)\b)/);
|
|
448
|
+
if (idxDelay && parseFloat(idxDelay[1] ?? idxDelay[2]) > 60) {
|
|
449
|
+
add('mot-08', file, lineNo, idxDelay[0].slice(0, 60), 'Per-item stagger delay exceeds the 0-60ms unit band; derive it from the stagger-unit token (0-60ms).');
|
|
450
|
+
}
|
|
451
|
+
const calcUnit = line.match(/(?:animation|transition)-delay\s*:\s*calc\(\s*(\d*\.?\d+)ms\s*\*/);
|
|
452
|
+
if (calcUnit && parseFloat(calcUnit[1]) > 60) {
|
|
453
|
+
add('mot-08', file, lineNo, calcUnit[0], 'Per-item stagger delay exceeds the 0-60ms unit band; ride the stagger-unit token instead of a raw multiplier.');
|
|
454
|
+
}
|
|
455
|
+
if (/(?:animation|transition)-delay/.test(line)) {
|
|
456
|
+
const nth = line.match(/nth-child\((\d+)\)/);
|
|
457
|
+
if (nth && parseInt(nth[1], 10) > STAGGER_CHILD_CAP) {
|
|
458
|
+
add('mot-08', file, lineNo, nth[0], `Stagger cascades cap at ${STAGGER_CHILD_CAP} children; children past the cap arrive together.`);
|
|
459
|
+
}
|
|
460
|
+
if (/stagger|nth-child/i.test(line)) {
|
|
461
|
+
for (const m of line.matchAll(/(?:animation|transition)-delay\s*:\s*(\d*\.?\d+)(ms|s)\b/g)) {
|
|
462
|
+
const ms = m[2] === 's' ? parseFloat(m[1]) * 1000 : parseFloat(m[1]);
|
|
463
|
+
if (ms >= 1000) add('mot-08', file, lineNo, m[0], 'A stagger sequence totals under 1s; this per-item delay alone reaches the ceiling.');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
409
468
|
// mot-09: scroll-event listener driving animation.
|
|
410
469
|
if (/addEventListener\(\s*["']scroll["']/.test(line)) add('mot-09', file, lineNo, 'addEventListener("scroll")', 'Ban scroll-event listeners for animation; use IntersectionObserver or scroll-linked scrubbing.');
|
|
411
470
|
// mot-10: useState fed by a continuous scroll/pointer value.
|
|
@@ -561,6 +620,30 @@ export function checkFile(add, file, ext, text, lines) {
|
|
|
561
620
|
}
|
|
562
621
|
}
|
|
563
622
|
|
|
623
|
+
// typ-20 / col-16 / mot-08 (orchestration half): budget-gated vocabulary against the surface's
|
|
624
|
+
// budget stamp (lib/vocabulary.mjs). The display ramp step, wash consumption, and arrival cascades
|
|
625
|
+
// are legal only on a file stamped brand-forward; a baseline-stamped or unstamped file consuming
|
|
626
|
+
// them is the deterministic violation. Fired at the surfaces gate and the preship sweep through
|
|
627
|
+
// this shared engine.
|
|
628
|
+
if (ext === 'tsx' || ext === 'html') {
|
|
629
|
+
const brandForward = findBudgetStamps(text).includes('brand-forward');
|
|
630
|
+
const lineOf = (idx) => text.slice(0, idx).split('\n').length;
|
|
631
|
+
const displayUses = [...text.matchAll(/\btext-display\b|var\(--text-display\b/g)];
|
|
632
|
+
if (displayUses.length && !brandForward) {
|
|
633
|
+
add('typ-20', file, lineOf(displayUses[0].index), displayUses[0][0], 'The display ramp step appears only on surfaces stamped brand-forward; re-classify the surface with a recorded reason, or step this element down the ramp (text-h1).');
|
|
634
|
+
} else if (displayUses.length > 1) {
|
|
635
|
+
add('typ-20', file, lineOf(displayUses[1].index), `${displayUses.length} display-step elements`, 'At most one display-step element per surface; keep the focal one and step the rest down the ramp.');
|
|
636
|
+
}
|
|
637
|
+
const washUse = text.match(/var\(--wash-[a-z-]+\)/);
|
|
638
|
+
if (washUse && !brandForward) {
|
|
639
|
+
add('col-16', file, lineOf(washUse.index), washUse[0], 'A wash token is consumed only on brand-forward-stamped surfaces; re-classify the surface with a recorded reason, or remove the wash.');
|
|
640
|
+
}
|
|
641
|
+
const staggerUse = text.match(new RegExp(`\\b${MOTION_STAGGER_CLASS}\\b`));
|
|
642
|
+
if (staggerUse && !brandForward) {
|
|
643
|
+
add('mot-08', file, lineOf(staggerUse.index), staggerUse[0], 'Baseline-stamped surfaces skip arrival cascades; remove motion-stagger, or re-classify the surface with a recorded reason.');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
564
647
|
// int-01: a :focus / :focus-visible CSS rule that removes the outline with no visible ring replacement.
|
|
565
648
|
if (ext === 'css') {
|
|
566
649
|
for (const m of text.matchAll(/:focus(?:-visible)?\s*\{([^}]*)\}/g)) {
|
|
@@ -576,8 +659,10 @@ export function checkFile(add, file, ext, text, lines) {
|
|
|
576
659
|
add('int-10', file, text.slice(0, text.search(/setInterval\(/)).split('\n').length, 'polling status update without aria-live', 'An aria-live region is present wherever async status updates render; wrap the live status in role="status" / aria-live="polite".');
|
|
577
660
|
}
|
|
578
661
|
|
|
579
|
-
// cod-01: a token-defining CSS file that omits some of the
|
|
580
|
-
|
|
662
|
+
// cod-01: a token-defining CSS file that omits some of the 36 consumed tokens. The trigger
|
|
663
|
+
// requires DEFINITIONS at property position — a consumer file's `--color-background: var(--background)`
|
|
664
|
+
// @theme mapping must not read as a token-defining file.
|
|
665
|
+
if (ext === 'css' && /^\s*--background\s*:/m.test(text) && /^\s*--foreground\s*:/m.test(text)) {
|
|
581
666
|
const missing = REQUIRED_TOKENS.filter((t) => !new RegExp(`--${t}\\s*:`).test(text));
|
|
582
667
|
if (missing.length) add('cod-01', file, 1, `missing tokens: ${missing.slice(0, 6).join(', ')}${missing.length > 6 ? '…' : ''}`, 'Emitted CSS defines every token the substrate consumes; add the missing --token definitions.');
|
|
583
668
|
}
|
|
@@ -599,6 +684,7 @@ export function checkFile(add, file, ext, text, lines) {
|
|
|
599
684
|
const REQUIRED_TOKENS = [
|
|
600
685
|
'background', 'foreground', 'card', 'card-foreground', 'muted', 'muted-foreground', 'border', 'input', 'ring',
|
|
601
686
|
'primary', 'primary-foreground', 'accent', 'accent-foreground', 'destructive', 'destructive-foreground', 'success', 'warning',
|
|
687
|
+
'surface-raised', 'surface-overlay',
|
|
602
688
|
'popover', 'popover-foreground', 'secondary', 'secondary-foreground',
|
|
603
689
|
'chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5',
|
|
604
690
|
'sidebar', 'sidebar-foreground', 'sidebar-primary', 'sidebar-primary-foreground',
|
|
@@ -611,8 +697,10 @@ const REQUIRED_TOKENS = [
|
|
|
611
697
|
|
|
612
698
|
// Run the checks that need the whole passed-in file set (token-set counts, distinct sizes).
|
|
613
699
|
export function checkCrossFile(add, docs) {
|
|
614
|
-
// typ-06: more than
|
|
700
|
+
// typ-06: more than three font families declared across the passed files (fourth banned), or three
|
|
701
|
+
// families none of which carries the numeric-mono job.
|
|
615
702
|
const families = new Set();
|
|
703
|
+
const monoFamilies = new Set();
|
|
616
704
|
const iconLibs = new Set();
|
|
617
705
|
const explicitSizes = new Map(); // px -> {file,line,text}
|
|
618
706
|
const hexByHue = new Map(); // hue bucket -> Set(normHex)
|
|
@@ -622,11 +710,17 @@ export function checkCrossFile(add, docs) {
|
|
|
622
710
|
|
|
623
711
|
for (const d of docs) {
|
|
624
712
|
d.lines.forEach((line, i) => {
|
|
625
|
-
// font families from declarations / --font-* / font-[Family]
|
|
626
|
-
|
|
713
|
+
// font families from declarations / --font-* family slots / font-[Family].
|
|
714
|
+
// --font-weight-*/--font-size-* carry numbers, not families; a stack whose first entry
|
|
715
|
+
// is a CSS generic (the emitted baseline's generics-only stacks) declares no family.
|
|
716
|
+
for (const m of line.matchAll(/font-family\s*:\s*([^;{}]+)[;}]?|--font-(?!weight-|size-)[\w-]+\s*:\s*([^;]+);|font-\[["']?([A-Za-z][\w\s-]+)["']?\]/g)) {
|
|
627
717
|
const raw = (m[1] || m[2] || m[3] || '').trim();
|
|
628
718
|
const first = raw.split(',')[0].replace(/["']/g, '').trim().toLowerCase();
|
|
629
|
-
if (first && !/^var\(/.test(first)
|
|
719
|
+
if (first && !/^var\(/.test(first) && !GENERIC_FAMILY_KEYWORDS.has(first)) {
|
|
720
|
+
families.add(first);
|
|
721
|
+
// The numeric-mono job: declared as the mono slot or stacked onto a monospace generic.
|
|
722
|
+
if (/^--font-mono/.test(m[0]) || /\b(?:ui-)?monospace\b/i.test(raw)) monoFamilies.add(first);
|
|
723
|
+
}
|
|
630
724
|
}
|
|
631
725
|
// icon libraries from imports
|
|
632
726
|
const imp = line.match(/from\s+["'](lucide-react|@heroicons\/\w+|react-icons(?:\/\w+)?|@phosphor-icons\/\w+|phosphor-react)["']/);
|
|
@@ -651,9 +745,19 @@ export function checkCrossFile(add, docs) {
|
|
|
651
745
|
});
|
|
652
746
|
}
|
|
653
747
|
|
|
654
|
-
if (families.size >
|
|
748
|
+
if (families.size > 3) {
|
|
749
|
+
const d = docs[0];
|
|
750
|
+
add('typ-06', d.file, 1, `${families.size} font families: ${[...families].join(', ')}`, 'At most three font families per app, each with a distinct job (display / body / numeric-mono); a fourth family is banned — drop the extra.');
|
|
751
|
+
} else if (families.size === 3 && monoFamilies.size === 0) {
|
|
655
752
|
const d = docs[0];
|
|
656
|
-
add('typ-06', d.file, 1,
|
|
753
|
+
add('typ-06', d.file, 1, `3 font families, none mono: ${[...families].join(', ')}`, 'Three families need three distinct jobs (display / body / numeric-mono); none of these is a declared mono — give the third family a real job or drop it.');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// typ-06 (scale half): distinct explicit sizes beyond the declared ramp's step count.
|
|
757
|
+
if (explicitSizes.size > 7) {
|
|
758
|
+
const anchorPx = [...explicitSizes.keys()].sort((a, b) => a - b)[7];
|
|
759
|
+
const anc = explicitSizes.get(anchorPx);
|
|
760
|
+
add('typ-06', anc.file, anc.line, `${explicitSizes.size} distinct explicit sizes`, 'The type scale holds the declared ramp\'s steps (<= 7); collapse the extras onto the ramp utilities (text-h1 … text-display).');
|
|
657
761
|
}
|
|
658
762
|
|
|
659
763
|
// typ-10: adjacent explicitly-declared type-scale steps closer than 25%.
|
|
@@ -709,6 +813,56 @@ export function checkSpacingScale(add, docs) {
|
|
|
709
813
|
}
|
|
710
814
|
}
|
|
711
815
|
|
|
816
|
+
// CSS generic families, the ui-* keywords, and the conventional OS fallback faces that need no
|
|
817
|
+
// package to render — the only stack entries cod-16 accepts without a registering import/@font-face.
|
|
818
|
+
// Exported so the fail-closed cod-16 gate at derive-personality --emit (stack vs package.json
|
|
819
|
+
// binding) reads the same vocabulary as this source shell.
|
|
820
|
+
export const SYSTEM_FONT_KEYWORDS = new Set([
|
|
821
|
+
'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy', 'math', 'emoji', 'fangsong',
|
|
822
|
+
'system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded',
|
|
823
|
+
'-apple-system', 'blinkmacsystemfont', 'segoe ui', 'roboto', 'helvetica', 'helvetica neue', 'arial',
|
|
824
|
+
'sfmono-regular', 'sf mono', 'menlo', 'consolas', 'courier new', 'liberation mono', 'monaco',
|
|
825
|
+
'georgia', 'times new roman', 'inherit', 'initial', 'unset',
|
|
826
|
+
]);
|
|
827
|
+
|
|
828
|
+
// Slugify a family name the way a fontsource package specifier spells it ("Inter Variable" -> inter).
|
|
829
|
+
export function familySlug(name) {
|
|
830
|
+
return name.toLowerCase().replace(/\s+variable$/, '').trim().replace(/[^a-z0-9]+/g, '-');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// cod-16 (source shell): every named family in an emitted font stack binds to a registered family or
|
|
834
|
+
// a system generic. Registration is an @font-face declaring the family or an @import whose package
|
|
835
|
+
// specifier carries the family's slug (@fontsource/<slug>). The check arms only when the passed file
|
|
836
|
+
// set carries at least one registration — the no-fonts-css-at-all case belongs to the fail-closed
|
|
837
|
+
// package.json binding at derive-personality --emit, which owns this rule's gate.
|
|
838
|
+
export function checkFontBinding(add, docs) {
|
|
839
|
+
const cssDocs = docs.filter((d) => d.ext === 'css');
|
|
840
|
+
const importSpecs = [];
|
|
841
|
+
const registered = new Set();
|
|
842
|
+
for (const d of cssDocs) {
|
|
843
|
+
for (const m of d.text.matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)) importSpecs.push(m[1].toLowerCase());
|
|
844
|
+
for (const m of d.text.matchAll(/@font-face\s*\{[^}]*font-family\s*:\s*["']?([^;"'}]+)/g)) registered.add(m[1].trim().toLowerCase());
|
|
845
|
+
}
|
|
846
|
+
if (!importSpecs.length && !registered.size) return;
|
|
847
|
+
const isBound = (name) => {
|
|
848
|
+
const n = name.toLowerCase();
|
|
849
|
+
if (SYSTEM_FONT_KEYWORDS.has(n) || registered.has(n)) return true;
|
|
850
|
+
const slug = familySlug(name);
|
|
851
|
+
return slug !== '' && importSpecs.some((spec) => spec.includes(`/${slug}`) || spec === slug);
|
|
852
|
+
};
|
|
853
|
+
for (const d of cssDocs) {
|
|
854
|
+
d.lines.forEach((line, i) => {
|
|
855
|
+
for (const m of line.matchAll(/(?:--font-[\w-]+|font-family)\s*:\s*([^;}]+)/g)) {
|
|
856
|
+
for (const fam of m[1].split(',')) {
|
|
857
|
+
const name = fam.replace(/["']/g, '').trim();
|
|
858
|
+
if (!name || /^var\(/.test(name)) continue;
|
|
859
|
+
if (!isBound(name)) add('cod-16', d.file, i + 1, name, `Font family "${name}" binds to nothing installed: add its registering @fontsource import (or @font-face) to the emitted CSS, or fall back to a system generic.`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
712
866
|
// Bucket a 6-digit hex into a coarse hue family name for shade counting.
|
|
713
867
|
function hexHueBucket(hex) {
|
|
714
868
|
const h = expandHex(hex);
|
|
@@ -808,7 +962,37 @@ export function loadManifestVocab(manifestPath) {
|
|
|
808
962
|
const strokeToken = primitive['icon-stroke'] ? resolveAlias(root, primitive['icon-stroke']) : null;
|
|
809
963
|
const iconStroke = strokeToken && typeof strokeToken.$value === 'number' ? strokeToken.$value : null;
|
|
810
964
|
|
|
811
|
-
|
|
965
|
+
// The v2 elevation/wash vocabularies: every color literal inside a shadow composite's or a wash
|
|
966
|
+
// gradient's preserved .css enters the color allowlist — both as the drift extractor will match it
|
|
967
|
+
// in emitted CSS (the raw RE_COLOR_LITERAL slice) and as the layer's own typed hex — so consuming
|
|
968
|
+
// the artifact's designed shadows/washes never reads as drift.
|
|
969
|
+
const addExpressiveGroup = (group) => {
|
|
970
|
+
for (const token of Object.values(group || {})) {
|
|
971
|
+
if (!token || typeof token !== 'object') continue;
|
|
972
|
+
const css = preservedCss(token);
|
|
973
|
+
if (css) {
|
|
974
|
+
for (const m of css.matchAll(RE_COLOR_LITERAL)) colors.add(normColor(m[0]));
|
|
975
|
+
}
|
|
976
|
+
const layers = Array.isArray(token.$value) ? token.$value : [];
|
|
977
|
+
for (const layer of layers) {
|
|
978
|
+
const c = layer && (layer.color || null);
|
|
979
|
+
if (c && typeof c === 'object' && typeof c.hex === 'string') colors.add(normColor(c.hex));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
addExpressiveGroup(root.elevation);
|
|
984
|
+
addExpressiveGroup(root.wash);
|
|
985
|
+
|
|
986
|
+
// Alpha-stripped base signatures of every manifest color, plus the emitter's fixed neutral
|
|
987
|
+
// hairline base rgb(15 23 42) (the 2xs/xs shadow utilities the library leans on) — consulted
|
|
988
|
+
// only for literals whose alpha slot rides var()/calc(), where exact normalization is defeated.
|
|
989
|
+
const colorBases = new Set(['rgb:15,23,42']);
|
|
990
|
+
for (const c of colors) {
|
|
991
|
+
const b = colorBaseSig(c);
|
|
992
|
+
if (b) colorBases.add(b);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return { colors, spacing, radius, durations, fonts, iconStroke, colorBases };
|
|
812
996
|
}
|
|
813
997
|
|
|
814
998
|
// Flag any color/spacing/radius/duration/font literal in the diff files outside the manifest vocabulary (cod-02).
|
|
@@ -822,7 +1006,11 @@ export function checkDrift(add, docs, vocab) {
|
|
|
822
1006
|
// colors
|
|
823
1007
|
if (vocab.colors.size) {
|
|
824
1008
|
for (const m of line.matchAll(RE_COLOR_LITERAL)) {
|
|
825
|
-
if (
|
|
1009
|
+
if (vocab.colors.has(normColor(m[0]))) continue;
|
|
1010
|
+
// A var()/calc() alpha slot truncates the regex match mid-function and defeats exact
|
|
1011
|
+
// normalization; such intensity-riding literals match on their base channels instead.
|
|
1012
|
+
if (/(?:var|calc)\(/.test(m[0]) && vocab.colorBases && vocab.colorBases.has(colorBaseSig(m[0]))) continue;
|
|
1013
|
+
add('cod-02', d.file, i + 1, m[0], "Value drifts from the lever manifest; use a color already in the artifact's color tokens, not a fresh literal.");
|
|
826
1014
|
}
|
|
827
1015
|
}
|
|
828
1016
|
// spacing (explicit only)
|
|
@@ -871,6 +1059,7 @@ export function checkDocs(docs, { vocab = null } = {}) {
|
|
|
871
1059
|
}
|
|
872
1060
|
checkCrossFile(sink.add, docs);
|
|
873
1061
|
checkSpacingScale(sink.add, docs);
|
|
1062
|
+
checkFontBinding(sink.add, docs);
|
|
874
1063
|
if (vocab) checkDrift(sink.add, docs, vocab);
|
|
875
1064
|
// Stable sort: by file, then line, then rule id.
|
|
876
1065
|
sink.list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.rule.localeCompare(b.rule));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// skills/design/scripts/lib/vocabulary.mjs — single-source shared runtime vocabulary for the design skill's scripts: the delight-budget enum + file-header stamps, the surface-archetype budget defaults, the motion-moment class names, and the theme attribute convention. scaffold.mjs writes these strings; phase-gate.mjs reads them at the surfaces gate; the preship sweep and emission read them in later steps. One module so the stamp text, the enum, and the theme names never drift between the writer and its readers.
|
|
2
|
+
|
|
3
|
+
// The two delight-budget values a surface can carry (references/structure.md, the delight budget).
|
|
4
|
+
export const BUDGETS = ['baseline', 'brand-forward'];
|
|
5
|
+
|
|
6
|
+
// The verbatim file-header comment scaffold.mjs writes at the top of each surface route file, keyed by budget.
|
|
7
|
+
export const BUDGET_STAMPS = {
|
|
8
|
+
baseline: '// Surface: baseline',
|
|
9
|
+
'brand-forward': '// Surface: brand-forward (delight-allowing)',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// The stamp comment for a budget, or null when it is not one of BUDGETS.
|
|
13
|
+
export function stampFor(budget) {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(BUDGET_STAMPS, budget) ? BUDGET_STAMPS[budget] : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Matches any `// Surface:` header line, well-formed or not — gates count these to catch a malformed
|
|
18
|
+
// or duplicated stamp against the well-formed matches below.
|
|
19
|
+
export const BUDGET_STAMP_LOOSE_RE = /\/\/ Surface:.*/g;
|
|
20
|
+
|
|
21
|
+
// Every well-formed budget stamp in a source blob, in order, as budget values ('baseline' | 'brand-forward').
|
|
22
|
+
// Gates count occurrences (exactly one per route file) and read the stamped budget from this.
|
|
23
|
+
export function findBudgetStamps(source) {
|
|
24
|
+
const re = /\/\/ Surface: (brand-forward \(delight-allowing\)|baseline)(?=\s|$)/g;
|
|
25
|
+
const found = [];
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = re.exec(String(source))) !== null) {
|
|
28
|
+
found.push(m[1].startsWith('brand-forward') ? 'brand-forward' : 'baseline');
|
|
29
|
+
}
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Surface archetypes whose delight budget defaults to `baseline` — a `brand-forward` stamp on one of
|
|
34
|
+
// these needs a recorded reason in the surface map (references/structure.md, the delight budget).
|
|
35
|
+
export const BASELINE_DEFAULT_ARCHETYPES = ['list-detail', 'log-stream', 'command-center'];
|
|
36
|
+
|
|
37
|
+
// Surface archetypes that are `brand-forward` *candidates* — granted when the look's delight is
|
|
38
|
+
// expressive or the brief asks for presence, else baseline; no recorded reason needed either way.
|
|
39
|
+
export const BRAND_FORWARD_CANDIDATE_ARCHETYPES = ['dashboard', 'entity-detail', 'create-edit'];
|
|
40
|
+
|
|
41
|
+
// True when a `brand-forward` stamp on this archetype requires a recorded reason in the surface map.
|
|
42
|
+
export function requiresBudgetReason(archetype, budget) {
|
|
43
|
+
return budget === 'brand-forward' && BASELINE_DEFAULT_ARCHETYPES.includes(String(archetype));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Motion-moment CSS classes (references/structure.md the delight budget; data/combinations/motion.csv).
|
|
47
|
+
// scaffold consumes them where the archetype calls: table rows ride motion-lift (hover feedback,
|
|
48
|
+
// budget-free); card grids ride motion-stagger on brand-forward surfaces only — baseline-stamped
|
|
49
|
+
// surfaces skip arrival cascades (mot-08). The render rung audits rendered stagger containers.
|
|
50
|
+
export const MOTION_LIFT_CLASS = 'motion-lift';
|
|
51
|
+
export const MOTION_STAGGER_CLASS = 'motion-stagger';
|
|
52
|
+
// The derived custom property the stagger cascade reads; cap the staggered children at STAGGER_CHILD_CAP.
|
|
53
|
+
export const STAGGER_DELAY_VAR = '--stagger-delay';
|
|
54
|
+
export const STAGGER_CHILD_CAP = 8;
|
|
55
|
+
|
|
56
|
+
// The theme runtime attribute: its name and the two legal rendered values. look.theme `both` boots
|
|
57
|
+
// light with a working toggle, so the emitted default is always one of these two.
|
|
58
|
+
export const THEME_ATTR = 'data-theme';
|
|
59
|
+
export const THEME_VALUES = ['light', 'dark'];
|
|
60
|
+
|
|
61
|
+
// Resolve a look.theme (light | dark | both) to the emitted default data-theme value (both → light);
|
|
62
|
+
// anything unrecognized falls back to light.
|
|
63
|
+
export function resolveThemeDefault(theme) {
|
|
64
|
+
return theme === 'dark' ? 'dark' : 'light';
|
|
65
|
+
}
|
|
@@ -4,12 +4,20 @@ import { join, dirname, resolve, relative } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import { runChecks, formatFindings, walkUiFiles } from './lib/checks.mjs';
|
|
7
|
+
import { findBudgetStamps, BUDGET_STAMP_LOOSE_RE, requiresBudgetReason } from './lib/vocabulary.mjs';
|
|
7
8
|
|
|
8
9
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const PHASES = ['intake', 'structure', 'scaffold', 'tokens', 'fixtures', 'surfaces'];
|
|
10
11
|
const ARCHETYPES = new Set(['list-detail', 'dashboard', 'entity-detail', 'create-edit', 'log-stream', 'command-center']);
|
|
11
12
|
const PROVENANCE = new Set(['project', 'extracted', 'library', 'default']);
|
|
12
13
|
const STATE_SYNONYMS = { loading: ['loading', 'skeleton'], empty: ['empty'], error: ['error'], partial: ['partial', 'degraded', 'stale'] };
|
|
14
|
+
// The intake look block's enums (references/create.md, the look block). theme carries `both`; the emitted
|
|
15
|
+
// data-theme value the attribute cascade uses is light|dark (both boots light) — that pair lives in vocabulary.mjs.
|
|
16
|
+
const LOOK_THEMES = new Set(['light', 'dark', 'both']);
|
|
17
|
+
const DELIGHT_LEVELS = new Set(['baseline', 'expressive']);
|
|
18
|
+
const RESOLUTION_MODES = new Set(['confirmed', 'recorded']);
|
|
19
|
+
// The resolution spine's legal shapes: project | library:<slug> | seed:<style-row>+<palette-row> | default.
|
|
20
|
+
const RESOLUTION_RE = /^(project|default|library:.+|seed:.+\+.+)$/i;
|
|
13
21
|
|
|
14
22
|
const useColor = process.stdout.isTTY;
|
|
15
23
|
// Wrap a string in green/red when writing to a TTY, plain otherwise.
|
|
@@ -163,6 +171,11 @@ function requireMap(phase, project) {
|
|
|
163
171
|
|
|
164
172
|
// Collect all .ts/.tsx source under a route directory into one lowercased blob.
|
|
165
173
|
function routeSource(dir) {
|
|
174
|
+
return routeSourceRaw(dir).toLowerCase();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Collect all .ts/.tsx source under a route directory, case preserved (the budget stamp is case-sensitive).
|
|
178
|
+
function routeSourceRaw(dir) {
|
|
166
179
|
let out = '';
|
|
167
180
|
const stack = [dir];
|
|
168
181
|
while (stack.length) {
|
|
@@ -175,7 +188,7 @@ function routeSource(dir) {
|
|
|
175
188
|
else if (/\.(tsx|ts)$/.test(ent.name)) out += `\n${readText(full)}`;
|
|
176
189
|
}
|
|
177
190
|
}
|
|
178
|
-
return out
|
|
191
|
+
return out;
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
// Return the candidate route-dir slugs for a surface (name-kebab, route segments, archetype).
|
|
@@ -205,7 +218,7 @@ function missingStates(dir, states) {
|
|
|
205
218
|
});
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
// Validate the load-bearing
|
|
221
|
+
// Validate the load-bearing five-step globals.css import order (Cascade Contract 2).
|
|
209
222
|
function checkImportOrder(rawCss) {
|
|
210
223
|
const css = rawCss.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
211
224
|
const marks = [
|
|
@@ -217,7 +230,7 @@ function checkImportOrder(rawCss) {
|
|
|
217
230
|
const absent = marks.filter(([, i]) => i < 0).map(([n]) => n);
|
|
218
231
|
if (absent.length) {
|
|
219
232
|
return { problem: `globals.css is missing: ${absent.join(', ')}.`, fix: [
|
|
220
|
-
'Emit the
|
|
233
|
+
'Emit the five-step cascade order (references/personality.md §4, Cascade Contract 2):',
|
|
221
234
|
'@import "./personality.css"; -> @import "tailwindcss"; -> @import "@nurix/components/styles.css"; -> @theme inline { ... }',
|
|
222
235
|
] };
|
|
223
236
|
}
|
|
@@ -230,9 +243,63 @@ function checkImportOrder(rawCss) {
|
|
|
230
243
|
return null;
|
|
231
244
|
}
|
|
232
245
|
|
|
246
|
+
// Validate the payload's mandatory look block (references/create.md, the look block): presence, the theme /
|
|
247
|
+
// delight / resolution-mode enums, 2–3 named anchor strings, the resolution shape, and — when the
|
|
248
|
+
// resolution was authorized autonomously (resolution-mode: recorded) — that the artifact root
|
|
249
|
+
// $description carries a resolution record naming the runner-up. Fails the gate with the exact fix.
|
|
250
|
+
function validateLookBlock(payload, tokens) {
|
|
251
|
+
const look = payload.look;
|
|
252
|
+
if (!look || typeof look !== 'object' || Array.isArray(look)) {
|
|
253
|
+
fail('intake', 'payload carries no look block (payload.look).', [
|
|
254
|
+
'Add the mandatory look block to the artifact payload (references/create.md, the look block):',
|
|
255
|
+
'look: { theme: light|dark|both, anchors: ["Product A", "Product B"], delight: baseline|expressive,',
|
|
256
|
+
' resolution: project | library:<slug> | seed:<style>+<palette> | default, resolution-mode: confirmed|recorded }.',
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
if (!LOOK_THEMES.has(look.theme)) {
|
|
260
|
+
fail('intake', `look.theme is "${look.theme ?? '(missing)'}"; must be light | dark | both.`, [
|
|
261
|
+
'Set look.theme from the brief; it is binding when stated, default light (references/create.md, the look block).',
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
if (!DELIGHT_LEVELS.has(look.delight)) {
|
|
265
|
+
fail('intake', `look.delight is "${look.delight ?? '(missing)'}"; must be baseline | expressive.`, [
|
|
266
|
+
'Set look.delight — the global delight ceiling per-surface budgets refine (references/create.md the look block, references/structure.md the delight budget).',
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
const mode = look['resolution-mode'];
|
|
270
|
+
if (!RESOLUTION_MODES.has(mode)) {
|
|
271
|
+
fail('intake', `look.resolution-mode is "${mode ?? '(missing)'}"; must be confirmed | recorded.`, [
|
|
272
|
+
'Set look.resolution-mode: confirmed when a confirmer authorized the pick, recorded when it was resolved autonomously (references/create.md, Resolution).',
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
const anchors = look.anchors;
|
|
276
|
+
if (!Array.isArray(anchors) || anchors.length < 2 || anchors.length > 3 || !anchors.every((a) => typeof a === 'string' && a.trim())) {
|
|
277
|
+
fail('intake', `look.anchors must be 2–3 non-empty named strings; got ${JSON.stringify(anchors ?? null)}.`, [
|
|
278
|
+
'Name two or three real products (["Linear", "Stripe Dashboard"]); adjectives are not anchors —',
|
|
279
|
+
'convert an adjective to a named product by asking (interactive) or inferring-and-recording (autonomous) via the design-md resemblance lookup (references/create.md, the look block).',
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
if (typeof look.resolution !== 'string' || !RESOLUTION_RE.test(look.resolution.trim())) {
|
|
283
|
+
fail('intake', `look.resolution is "${look.resolution ?? '(missing)'}"; must match project | library:<slug> | seed:<style>+<palette> | default.`, [
|
|
284
|
+
'Set look.resolution to the resolved source from the precedence spine (references/create.md, Resolution).',
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
if (mode === 'recorded') {
|
|
288
|
+
const desc = tokens.$description;
|
|
289
|
+
if (typeof desc !== 'string' || !desc.trim() || !/runner-up/i.test(desc)) {
|
|
290
|
+
fail('intake', 'resolution-mode is recorded but the artifact root $description carries no resolution record naming the runner-up.', [
|
|
291
|
+
'Write the resolution record into the artifact root $description — winner, runner-up, and the deciding evidence, e.g.:',
|
|
292
|
+
'resolution: library:stripe (recorded; runner-up palettes.csv#3; brief phrase "Stripe-dashboard territory" matched product + fintech traits).',
|
|
293
|
+
'A recorded (autonomous) resolution is legitimate only once it is auditable (references/create.md, Resolution).',
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
233
299
|
// intake gate: a structurally sound design-tokens artifact sits at project root — both files parse, the dark
|
|
234
|
-
// file agrees with the resolver,
|
|
235
|
-
// deep validation (coverage, aliases, parse-back)
|
|
300
|
+
// file agrees with the resolver, the payload carries a legal schema/provenance/product, and the mandatory
|
|
301
|
+
// look block is present and well-formed. Light checks only; deep validation (coverage, aliases, parse-back)
|
|
302
|
+
// is derive-personality.mjs --check at the tokens gate.
|
|
236
303
|
function gateIntake(project) {
|
|
237
304
|
const tokensPath = join(project, 'design.tokens.json');
|
|
238
305
|
const resolverPath = join(project, 'design.resolver.json');
|
|
@@ -269,10 +336,11 @@ function gateIntake(project) {
|
|
|
269
336
|
'Add the file-level payload (schema, dtcg, product, provenance, sourced, lever-manifest, …) per data/design-tokens/SCHEMA.md.',
|
|
270
337
|
]);
|
|
271
338
|
}
|
|
272
|
-
if (payload.schema !== 'design-tokens/
|
|
339
|
+
if (payload.schema !== 'design-tokens/v2') fail('intake', `payload schema is "${payload.schema || '(missing)'}", expected design-tokens/v2.`, ['Set schema: "design-tokens/v2" in the payload (data/design-tokens/SCHEMA.md).']);
|
|
273
340
|
if (!PROVENANCE.has(payload.provenance)) fail('intake', `payload provenance is "${payload.provenance || '(missing)'}"; must be one of project|extracted|library|default.`, ['Set provenance to one of: project | extracted | library | default (data/design-tokens/SCHEMA.md).']);
|
|
274
341
|
if (typeof payload.product !== 'string' || !payload.product.trim()) fail('intake', 'payload product is missing or empty.', ['Set product to the product name; slugified it becomes the data-style value (data/design-tokens/SCHEMA.md).']);
|
|
275
|
-
|
|
342
|
+
validateLookBlock(payload, tokens);
|
|
343
|
+
pass('intake', `design.tokens.json + design.resolver.json present and parsing; payload schema design-tokens/v2, provenance "${payload.provenance}"${namesDark ? ', dark set named and present' : ', light-only'}; look block valid (theme ${payload.look.theme}, delight ${payload.look.delight}, ${Array.isArray(payload.look.anchors) ? payload.look.anchors.length : 0} anchors, resolution-mode ${payload.look['resolution-mode']}).`);
|
|
276
344
|
}
|
|
277
345
|
|
|
278
346
|
// structure gate: the map parses, is populated, has a temporal block, and every surface archetype is one of the six.
|
|
@@ -325,7 +393,7 @@ function gateScaffold(project) {
|
|
|
325
393
|
if (noWire.length) fail('scaffold', `Route(s) not wired into the router: ${noWire.join('; ')}.`, ['Every surface route must be registered in src/App.tsx (react-router-dom) — routes and nav derive from the map, never decorated on.']);
|
|
326
394
|
if (noShell.length) fail('scaffold', `Missing state-shell stubs: ${noShell.join('; ')}.`, ['Each surface needs its loading / empty / error / degraded shells wired (references/create.md -> scaffold).']);
|
|
327
395
|
const globals = join(project, 'src/styles/globals.css');
|
|
328
|
-
if (!existsSync(globals)) fail('scaffold', 'Missing src/styles/globals.css (Tailwind glue with the
|
|
396
|
+
if (!existsSync(globals)) fail('scaffold', 'Missing src/styles/globals.css (Tailwind glue with the five-step import order).', ['Emit src/styles/globals.css with the Cascade Contract 2 import order (references/personality.md §4).']);
|
|
329
397
|
const orderErr = checkImportOrder(readText(globals));
|
|
330
398
|
if (orderErr) fail('scaffold', orderErr.problem, orderErr.fix);
|
|
331
399
|
pass('scaffold', `Shell, ${blk.surfaces.length} wired route dir(s) with state-shells, and globals.css import order all in place.`);
|
|
@@ -384,7 +452,11 @@ function gateFixtures(project) {
|
|
|
384
452
|
pass('fixtures', `Seeded fixture module present for all ${blk.entities.length} entit(ies).`);
|
|
385
453
|
}
|
|
386
454
|
|
|
387
|
-
// surfaces gate: every surface's route dir exists
|
|
455
|
+
// surfaces gate: every surface's route dir exists, its shell contains every declared state branch, and
|
|
456
|
+
// it carries exactly one well-formed budget stamp — with a recorded budget-reason in the map when a
|
|
457
|
+
// brand-forward stamp overrides a baseline-default archetype (references/structure.md, the delight budget). The
|
|
458
|
+
// budget-gated vocabulary sweeps (display step typ-20, wash col-16) land with the rule catalog in a
|
|
459
|
+
// later step; the stamp is parsed here via lib/vocabulary.mjs so those sweeps reuse the same reader.
|
|
388
460
|
function gateSurfaces(project) {
|
|
389
461
|
const blk = requireMap('surfaces', project);
|
|
390
462
|
const routesDir = join(project, 'src/routes');
|
|
@@ -396,14 +468,26 @@ function gateSurfaces(project) {
|
|
|
396
468
|
const states = (s.states && s.states.length) ? s.states : ['loading', 'empty', 'error'];
|
|
397
469
|
const miss = missingStates(dir, states);
|
|
398
470
|
if (miss.length) problems.push(`${name}: shell missing state branch(es) ${miss.join(', ')}`);
|
|
471
|
+
const src = routeSourceRaw(dir);
|
|
472
|
+
const looseCount = (src.match(BUDGET_STAMP_LOOSE_RE) || []).length;
|
|
473
|
+
const stamps = findBudgetStamps(src);
|
|
474
|
+
if (looseCount === 0) {
|
|
475
|
+
problems.push(`${name}: no budget stamp at the top of the route file`);
|
|
476
|
+
} else if (stamps.length !== 1 || looseCount !== 1) {
|
|
477
|
+
problems.push(`${name}: expected exactly one well-formed budget stamp, found ${looseCount} "// Surface:" line(s) (${stamps.length} well-formed)`);
|
|
478
|
+
} else if (requiresBudgetReason(s.archetype, stamps[0]) && !(typeof s['budget-reason'] === 'string' && s['budget-reason'].trim())) {
|
|
479
|
+
problems.push(`${name}: brand-forward stamp on a ${s.archetype} surface (archetype defaults baseline) with no budget-reason in the surface map`);
|
|
480
|
+
}
|
|
399
481
|
}
|
|
400
482
|
if (problems.length) {
|
|
401
|
-
fail('surfaces', `${problems.length} surface(s) not fully composed:`, [
|
|
483
|
+
fail('surfaces', `${problems.length} surface(s) not fully composed. Handle these before finalizing:`, [
|
|
402
484
|
...problems.map((p) => `- ${p}`),
|
|
403
485
|
'Fill every declared lifecycle state into its pre-created shell (references/create.md -> surfaces); states are composed with the surface, not bolted on.',
|
|
486
|
+
'Every surface route file carries exactly one stamp — "// Surface: baseline" or "// Surface: brand-forward (delight-allowing)"; scaffold.mjs writes it from the map budget, so re-classify by editing the surface budget in docs/design/structure.md and re-running scaffold.mjs.',
|
|
487
|
+
'A brand-forward stamp on a baseline-default archetype (list-detail / log-stream / command-center) needs a recorded budget-reason on that surface entry (references/structure.md, the delight budget).',
|
|
404
488
|
]);
|
|
405
489
|
}
|
|
406
|
-
pass('surfaces', `All ${blk.surfaces.length} surface(s) have a route dir
|
|
490
|
+
pass('surfaces', `All ${blk.surfaces.length} surface(s) have a route dir, every declared state branch, and exactly one legal budget stamp.`);
|
|
407
491
|
}
|
|
408
492
|
|
|
409
493
|
const GATES = { intake: gateIntake, structure: gateStructure, scaffold: gateScaffold, tokens: gateTokens, fixtures: gateFixtures, surfaces: gateSurfaces };
|