@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
|
@@ -49,6 +49,22 @@ Membership is closed. New archetypes enter only as pack data, ledger-recorded
|
|
|
49
49
|
|
|
50
50
|
**Navigation derives from the object model, never decorated on.** Top-level nav entries are the primary entities and their home surfaces; secondary entities are reached through their parent's detail surface, not promoted. The unit of work is the home. Cap top-level entries at what stays countable on one hand — nav categories the model does not carry are a structural defect, not a convenience.
|
|
51
51
|
|
|
52
|
+
## The delight budget — permission is structural
|
|
53
|
+
|
|
54
|
+
The object pass classifies **every** surface with a mandatory `budget` — `baseline` or `brand-forward`. Permission is a structural fact decided here, not a look value: the budget grants which token *vocabulary* a surface may consume, never a value. `scaffold.mjs` stamps the budget into each route file's header comment (`// Surface: baseline` / `// Surface: brand-forward (delight-allowing)`); `phase-gate.mjs surfaces` reads the stamp back.
|
|
55
|
+
|
|
56
|
+
**Defaults by archetype** — assign the default, override only against a recorded reason:
|
|
57
|
+
|
|
58
|
+
| Archetype | Default | Note |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `list-detail`, `log-stream`, `command-center` | `baseline` | throughput surfaces; decoration is noise here |
|
|
61
|
+
| `dashboard`, `entity-detail` (header region), `create-edit` (empty / success moments) | `brand-forward` **candidate** | granted when the look's `delight` is `expressive` or the brief asks for presence; else `baseline` |
|
|
62
|
+
|
|
63
|
+
- **Overridable, against a recorded reason.** A budget that contradicts the archetype default — a `brand-forward` stamp on a `baseline`-default archetype — carries a `budget-reason` on that surface entry, or the surfaces gate fails. Candidacy is not a grant: a candidate archetype still defaults `baseline` and only rises to `brand-forward` when the look or brief earns it.
|
|
64
|
+
- **`baseline` blocks decoration, never signal.** A `baseline` stamp never suppresses status or feedback color — error red, warning amber, the success tick all render at every budget. It withholds *ornament*: the display ramp step, elevation lifts, washes, arrival motion.
|
|
65
|
+
- **What `brand-forward` unlocks** — all still tokens, all still on-scale: the `display` ramp step (one per surface), `elevation.lg`, the `wash.*` tokens, the motion moments in `motion.csv`, and the generous spacing tier.
|
|
66
|
+
- **What no budget ever unlocks:** literal colors, off-manifest values, slop-gradient vocabulary, serif in dashboards. The floor is budget-independent — a brand-forward surface clears the same rules a baseline one does. Expressiveness is granted *inside* the floor, never as an exception to it.
|
|
67
|
+
|
|
52
68
|
## The map: `docs/design/structure.md`
|
|
53
69
|
|
|
54
70
|
The derivation's whole output. Human prose above the block; the fenced `yaml structure` block is the machine contract — `scaffold.mjs` reads it, so the field names are law. Scaffold generates routes, navigation wiring, and per-surface state-shell stubs from `surfaces[]`, and keys fixture wiring off `entities[]`.
|
|
@@ -73,6 +89,8 @@ surfaces:
|
|
|
73
89
|
route: /<path>
|
|
74
90
|
entity: <Entity>
|
|
75
91
|
density-modifier: <integer step; positive = denser; mirrors the catalog's per-archetype modifier>
|
|
92
|
+
budget: baseline | brand-forward # mandatory; the surface's delight budget
|
|
93
|
+
budget-reason: <one line> # required only when budget overrides the archetype default
|
|
76
94
|
states: [loading, empty, error, partial] # applicable subset; data surfaces carry all four
|
|
77
95
|
```
|
|
78
96
|
|
|
@@ -120,19 +138,22 @@ surfaces:
|
|
|
120
138
|
route: /tickets
|
|
121
139
|
entity: Ticket
|
|
122
140
|
density-modifier: 1
|
|
141
|
+
budget: baseline
|
|
123
142
|
states: [loading, empty, error, partial]
|
|
124
143
|
- name: Ticket Detail
|
|
125
144
|
archetype: entity-detail
|
|
126
145
|
route: /tickets/:id
|
|
127
146
|
entity: Ticket
|
|
128
147
|
density-modifier: 0
|
|
148
|
+
budget: baseline
|
|
129
149
|
states: [loading, empty, error, partial]
|
|
130
150
|
- name: New Ticket
|
|
131
151
|
archetype: create-edit
|
|
132
152
|
route: /tickets/new
|
|
133
153
|
entity: Ticket
|
|
134
154
|
density-modifier: -1
|
|
155
|
+
budget: baseline
|
|
135
156
|
states: [loading, error]
|
|
136
157
|
```
|
|
137
158
|
|
|
138
|
-
Navigation: top-level is **Tickets** — the unit of work's home. Customer and Agent are reached through Ticket Detail, not promoted to top-level nav (derived, not decorated). History — the audit of status changes and replies — surfaces as a timeline on the detail, not a fifth temporal key. OOUX pass: the status pill renders identically on queue and detail (no Shapeshifter), and Customer links out to its own detail rather than living only as a string (no Masked).
|
|
159
|
+
Navigation: top-level is **Tickets** — the unit of work's home. Customer and Agent are reached through Ticket Detail, not promoted to top-level nav (derived, not decorated). History — the audit of status changes and replies — surfaces as a timeline on the detail, not a fifth temporal key. OOUX pass: the status pill renders identically on queue and detail (no Shapeshifter), and Customer links out to its own detail rather than living only as a string (no Masked). Budgets: every surface stamps `baseline` — the brief states no expressive delight, so the two candidate archetypes (Ticket Detail's header, New Ticket's success moment) stay at their default; a queue is `baseline` outright. No `budget-reason` is needed because no stamp overrides an archetype default.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// skills/design/scripts/convert-design-md.mjs — mechanical design.md → design-tokens converter (schema design-tokens/
|
|
2
|
+
// skills/design/scripts/convert-design-md.mjs — mechanical design.md → design-tokens converter (schema design-tokens/v2, DTCG 2025.10): parses each source's frontmatter + two machine blocks + §3 type ramp, emits design.tokens.json / design.tokens.dark.json / design.resolver.json per data/design-tokens/SCHEMA.md (running the v1→v2 upgrade transform over each converted entry), self-validates (schema rules 2–10 + the v2 groups), offers --upgrade-v2 to run the same idempotent transform over existing artifact directories, and --strip to remove the machine blocks (and their orphaned prose references) from source files once the byte-match gate passes.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
5
|
// node convert-design-md.mjs convert every entry (nurix.md + library/*.md), then validate
|
|
6
6
|
// node convert-design-md.mjs --only <slug> convert/validate a single entry
|
|
7
7
|
// node convert-design-md.mjs --validate validate existing artifacts only (no conversion)
|
|
8
|
+
// node convert-design-md.mjs --upgrade-v2 idempotent v1→v2 migration over data/design-tokens/*/, then validate
|
|
8
9
|
// node convert-design-md.mjs --report <file> also write the JSON conversion report to <file>
|
|
9
10
|
// node convert-design-md.mjs --strip <file...> remove both machine blocks + orphaned prose from the named
|
|
10
11
|
// .md files IN PLACE (explicit paths required — never implicit)
|
|
@@ -22,7 +23,8 @@ const tokensDir = join(skillDir, "data", "design-tokens");
|
|
|
22
23
|
// Vocabulary (names are law — mirrors data/design-tokens/SCHEMA.md)
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
|
|
25
|
-
// The 26
|
|
26
|
+
// The 26 v1 source primitives in emission order, with their DTCG $type (the design.md machine
|
|
27
|
+
// blocks carry these; primitive.stagger-unit is added by the v1→v2 upgrade transform).
|
|
26
28
|
const PRIMITIVE_TYPES = new Map([
|
|
27
29
|
["radius", "dimension"], ["shadow-intensity", "number"], ["border-weight", "dimension"],
|
|
28
30
|
["density-gap", "number"], ["density-inset", "number"],
|
|
@@ -40,7 +42,12 @@ const PRIMITIVE_TYPES = new Map([
|
|
|
40
42
|
]);
|
|
41
43
|
const PRIMITIVES = [...PRIMITIVE_TYPES.keys()];
|
|
42
44
|
|
|
43
|
-
// The
|
|
45
|
+
// The 27 v2 primitives with stagger-unit in its emission slot (after ease-default).
|
|
46
|
+
const PRIMITIVE_TYPES_V2 = new Map(
|
|
47
|
+
[...PRIMITIVE_TYPES].flatMap(([k, t]) => (k === "ease-default" ? [[k, t], ["stagger-unit", "duration"]] : [[k, t]])),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// The 34 v1 source color roles in emission order (17 semantic + 17 substrate; the machine blocks carry these).
|
|
44
51
|
const COLOR_ROLES = [
|
|
45
52
|
"background", "foreground", "card", "card-foreground", "muted", "muted-foreground",
|
|
46
53
|
"border", "input", "ring", "primary", "primary-foreground", "accent", "accent-foreground",
|
|
@@ -51,7 +58,12 @@ const COLOR_ROLES = [
|
|
|
51
58
|
"sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring",
|
|
52
59
|
];
|
|
53
60
|
|
|
61
|
+
// The 36 v2 roles: the dark-ladder pair (surface-raised/surface-overlay) closes the semantic set at 19.
|
|
62
|
+
const COLOR_ROLES_V2 = COLOR_ROLES.flatMap((r) => (r === "warning" ? [r, "surface-raised", "surface-overlay"] : [r]));
|
|
63
|
+
|
|
54
64
|
const RAMP_STEPS = ["h1", "h2", "h3", "body", "small", "caption"];
|
|
65
|
+
const RAMP_STEPS_V2 = [...RAMP_STEPS, "display"];
|
|
66
|
+
const ELEVATION_TOKENS = ["sm", "md", "lg", "overlay"];
|
|
55
67
|
// Source §3 row names that fill the caption slot when no literal `caption` row exists.
|
|
56
68
|
const CAPTION_ALIASES = ["caption", "eyebrow / caption", "eyebrow", "label", "mono-label"];
|
|
57
69
|
|
|
@@ -59,8 +71,8 @@ const LEGAL_VERBOSITY = ["full", "default", "compact", "expert"];
|
|
|
59
71
|
const LEGAL_FEEDBACK = ["subtle", "minimal"];
|
|
60
72
|
const LEGAL_DENSITY = ["spacious", "standard", "dense"];
|
|
61
73
|
const LEGAL_PROVENANCE = ["project", "extracted", "library", "default"];
|
|
62
|
-
const LEGAL_TYPES = ["color", "dimension", "duration", "cubicBezier", "fontFamily", "fontWeight", "number", "typography"];
|
|
63
|
-
const SHADOW_RANK = { none: 0, "2xs": 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7 };
|
|
74
|
+
const LEGAL_TYPES = ["color", "dimension", "duration", "cubicBezier", "fontFamily", "fontWeight", "number", "typography", "shadow", "gradient"];
|
|
75
|
+
const SHADOW_RANK = { none: 0, "2xs": 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7, overlay: 8 };
|
|
64
76
|
const EXT = "com.nurix.design";
|
|
65
77
|
|
|
66
78
|
// Provisional values worn when a source key is a `hint:` (the Nurix defaults, per SCHEMA.md examples).
|
|
@@ -151,16 +163,38 @@ function parseColorCss(css) {
|
|
|
151
163
|
return value;
|
|
152
164
|
}
|
|
153
165
|
m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(s);
|
|
166
|
+
if (!m) m = /^rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s); // modern space syntax
|
|
154
167
|
if (m) {
|
|
155
168
|
const bytes = [m[1], m[2], m[3]].map(Number);
|
|
156
169
|
const hex = "#" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
157
170
|
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex };
|
|
158
|
-
if (m[4] !== undefined) value.alpha = parseFloat(m[4]);
|
|
171
|
+
if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
|
|
159
172
|
return value;
|
|
160
173
|
}
|
|
161
174
|
throw new Error(`unparseable color: ${JSON.stringify(css)}`);
|
|
162
175
|
}
|
|
163
176
|
|
|
177
|
+
// Parse one shadow-list string (the elevation byte form) into the DTCG layer array; byte-compatible
|
|
178
|
+
// with derive-personality.mjs. A layer alpha may ride calc(A * var(--shadow-intensity)) — A is the alpha.
|
|
179
|
+
function parseShadowCss(css) {
|
|
180
|
+
return splitTopLevel(css, ",").map((layerRaw) => {
|
|
181
|
+
const s = layerRaw.trim();
|
|
182
|
+
const colorStart = s.search(/rgba?\(|oklch\(|hsla?\(|#[0-9a-fA-F]/);
|
|
183
|
+
if (colorStart < 0) throw new Error(`shadow layer names no color: ${JSON.stringify(s)}`);
|
|
184
|
+
const colorCss = s.slice(colorStart).trim().replace(/calc\(\s*([\d.]+)\s*\*\s*var\(--shadow-intensity\)\s*\)/, "$1");
|
|
185
|
+
const dims = s.slice(0, colorStart).trim().split(/\s+/);
|
|
186
|
+
if (dims.length < 3 || dims.length > 4) throw new Error(`shadow layer wants 3-4 lengths: ${JSON.stringify(s)}`);
|
|
187
|
+
const dim = (d) => (d === "0" ? { value: 0, unit: "px" } : parseDimensionCss(d));
|
|
188
|
+
return {
|
|
189
|
+
color: parseColorCss(colorCss),
|
|
190
|
+
offsetX: dim(dims[0]),
|
|
191
|
+
offsetY: dim(dims[1]),
|
|
192
|
+
blur: dim(dims[2]),
|
|
193
|
+
spread: dims[3] !== undefined ? dim(dims[3]) : { value: 0, unit: "px" },
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
164
198
|
// Parse a tracking string ("0" | "0em" | "-0.015em") into its em-multiplier number.
|
|
165
199
|
function parseTrackingCss(css) {
|
|
166
200
|
const m = /^([+-]?\d*\.?\d+)(em)?$/.exec(css.trim());
|
|
@@ -637,6 +671,202 @@ function convertEntry(source, slug) {
|
|
|
637
671
|
return { base, dark, resolver, notes };
|
|
638
672
|
}
|
|
639
673
|
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
// v1 → v2 upgrade transform (SCHEMA.md migration; idempotent — every step skips when already applied)
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
// The v1 hardcoded slate shadow recipes (the old globals.css @theme block) reified as elevation byte
|
|
679
|
+
// forms — value-preserving: the calc(α × --shadow-intensity) rider keeps the twin's shadow-intensity
|
|
680
|
+
// as the master multiplier, so emitted --shadow-sm/md/lg are byte-identical before/after migration.
|
|
681
|
+
// overlay is the widest-blur member of the same family (new in v2, additive).
|
|
682
|
+
const V1_SHADOW_RECIPES = {
|
|
683
|
+
sm: "0 1px 3px 0 rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 1px 2px -1px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)))",
|
|
684
|
+
md: "0 4px 6px -1px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 2px 4px -2px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)))",
|
|
685
|
+
lg: "0 10px 15px -3px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 4px 6px -4px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)))",
|
|
686
|
+
overlay: "0 20px 25px -5px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity))), 0 8px 10px -6px rgb(15 23 42 / calc(0.10 * var(--shadow-intensity)))",
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Collect design.md code spans that name real shadow values — a color function (shadow tints ride
|
|
690
|
+
// alpha'd rgba/oklch) or box-shadow geometry (>= 2 consecutive lengths) on a shadow-context line.
|
|
691
|
+
// A bare hex near the word "shadow" (a border color in a "never shadow" sentence) is not one.
|
|
692
|
+
function findShadowProse(designMd) {
|
|
693
|
+
if (designMd === null) return [];
|
|
694
|
+
const evidence = [];
|
|
695
|
+
const lines = designMd.split("\n");
|
|
696
|
+
for (const [i, line] of lines.entries()) {
|
|
697
|
+
// The shadow context may sit on the previous line when the prose wraps; a negated mention
|
|
698
|
+
// ("no shadow", "not shadow", "never ... shadow", "shadowless") is not a shadow context.
|
|
699
|
+
const window = `${lines[i - 1] ?? ""} ${line}`
|
|
700
|
+
.replace(/\b(?:no|not|never|without|zero)\b[^.;:]*?shadow\w*/gi, "")
|
|
701
|
+
.replace(/shadow-?less/gi, "");
|
|
702
|
+
if (!/shadow/i.test(window)) continue;
|
|
703
|
+
for (const m of line.matchAll(/`[^`]+`/g)) {
|
|
704
|
+
const span = m[0];
|
|
705
|
+
const hasColorFn = /rgba?\(|oklch\(|hsla?\(/.test(span);
|
|
706
|
+
const hasGeometry = /(?:^|[\s`])(?:-?\d+(?:\.\d+)?px|0)\s+(?:-?\d+(?:\.\d+)?px|0)(?:\s|`|,)/.test(span);
|
|
707
|
+
if (hasColorFn || hasGeometry) evidence.push(span);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return evidence;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// True when a design.md declares the dark theme as the product's default.
|
|
714
|
+
function declaresDarkFirst(designMd) {
|
|
715
|
+
return designMd !== null && /dark by default|default to dark|wear the dark theme by default/i.test(designMd);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Rebuild an object with `additions` ([key, value] pairs) inserted immediately after `afterKey`.
|
|
719
|
+
function insertAfter(obj, afterKey, additions) {
|
|
720
|
+
const out = {};
|
|
721
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
722
|
+
out[k] = v;
|
|
723
|
+
if (k === afterKey) for (const [ak, av] of additions) out[ak] = av;
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Apply the seven-step v1→v2 migration to one artifact's documents in place-order (idempotent:
|
|
729
|
+
// every step is skipped when its key already exists). Returns per-entry stats.
|
|
730
|
+
function upgradeArtifact(base, dark, resolver, designMd, slug) {
|
|
731
|
+
const stats = { elevationHinted: false, darkFirst: false, displayHinted: false };
|
|
732
|
+
const payload = base.$extensions?.[EXT];
|
|
733
|
+
if (!payload) throw new Error("upgrade: no payload on the base root");
|
|
734
|
+
|
|
735
|
+
// Step 1 — the schema literal.
|
|
736
|
+
payload.schema = "design-tokens/v2";
|
|
737
|
+
|
|
738
|
+
// Step 6 — theme-default: light everywhere except twins whose design.md declares dark-first
|
|
739
|
+
// (only a twin shipping a dark set can boot dark); the resolver's theme default mirrors it.
|
|
740
|
+
if (payload["theme-default"] === undefined) {
|
|
741
|
+
const darkFirst = dark !== null && declaresDarkFirst(designMd);
|
|
742
|
+
base.$extensions[EXT] = insertAfter(payload, "sourced", [["theme-default", darkFirst ? "dark" : "light"]]);
|
|
743
|
+
}
|
|
744
|
+
stats.darkFirst = base.$extensions[EXT]["theme-default"] === "dark";
|
|
745
|
+
const themeModifier = (resolver.modifiers ?? []).find((m) => m?.name === "theme");
|
|
746
|
+
if (themeModifier) themeModifier.default = base.$extensions[EXT]["theme-default"];
|
|
747
|
+
|
|
748
|
+
// The Nurix default's stated v2 shadow vocabulary (SCHEMA.md elevation section).
|
|
749
|
+
if (slug === "nurix") base.$extensions[EXT]["lever-manifest"]["shadow-levels"] = ["none", "sm", "md", "lg", "overlay"];
|
|
750
|
+
|
|
751
|
+
// Step 2 — elevation from the v1 slate recipes × the twin's shadow-intensity; hinted when the
|
|
752
|
+
// design.md names real shadow values (judgment owed: the composites should carry that prose).
|
|
753
|
+
if (base.elevation === undefined) {
|
|
754
|
+
const prose = findShadowProse(designMd);
|
|
755
|
+
stats.elevationHinted = prose.length > 0;
|
|
756
|
+
const elevation = {};
|
|
757
|
+
for (const level of ELEVATION_TOKENS) {
|
|
758
|
+
const css = V1_SHADOW_RECIPES[level];
|
|
759
|
+
const ext = { css };
|
|
760
|
+
if (prose.length > 0) {
|
|
761
|
+
ext.hint = `elevation unset — the design.md names real shadow values (${prose.slice(0, 3).join("; ")}); resolve the composites from that prose at the data build`;
|
|
762
|
+
}
|
|
763
|
+
elevation[level] = {
|
|
764
|
+
$type: "shadow",
|
|
765
|
+
$value: parseShadowCss(css),
|
|
766
|
+
$description: "derived: v1 shadow-intensity migration",
|
|
767
|
+
$extensions: { [EXT]: ext },
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
base.elevation = elevation;
|
|
771
|
+
} else {
|
|
772
|
+
stats.elevationHinted = ELEVATION_TOKENS.some((l) => base.elevation?.[l]?.$extensions?.[EXT]?.hint !== undefined);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Step 3 — the dark-ladder roles as aliases: zero visual change in light; a dark set gains the
|
|
776
|
+
// same aliases (they resolve post-overlay to the dark card/popover).
|
|
777
|
+
if (base.color["surface-raised"] === undefined) {
|
|
778
|
+
base.color = insertAfter(base.color, "warning", [
|
|
779
|
+
["surface-raised", { $type: "color", $value: "{color.card}", $description: "derived: v1→v2 migration — light aliases card; dark looks carry the surface-lightness ladder" }],
|
|
780
|
+
["surface-overlay", { $type: "color", $value: "{color.popover}", $description: "derived: v1→v2 migration — light aliases popover; dark looks carry the surface-lightness ladder" }],
|
|
781
|
+
]);
|
|
782
|
+
}
|
|
783
|
+
if (dark !== null && dark.color["surface-raised"] === undefined) {
|
|
784
|
+
dark.color = insertAfter(dark.color, "warning", [
|
|
785
|
+
["surface-raised", { $type: "color", $value: "{color.card}", $description: "derived: v1→v2 migration — aliased to the dark card; the +4–6 L surface ladder is owed at budget-time" }],
|
|
786
|
+
["surface-overlay", { $type: "color", $value: "{color.popover}", $description: "derived: v1→v2 migration — aliased to the dark popover; the +4–6 L surface ladder is owed at budget-time" }],
|
|
787
|
+
]);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Step 4 — the display step, hinted (provisional: h1 × 1.75, strong weight). The Nurix default
|
|
791
|
+
// adopts the provisional derivation as its own look, assigned, so emission stays possible.
|
|
792
|
+
if (base.typography?.ramp && base.typography.ramp.display === undefined) {
|
|
793
|
+
const h1 = base.typography.ramp.h1;
|
|
794
|
+
if (!h1?.$value?.fontSize?.value) throw new Error("upgrade: ramp.h1 carries no fontSize to scale");
|
|
795
|
+
const display = {
|
|
796
|
+
$type: "typography",
|
|
797
|
+
$value: {
|
|
798
|
+
fontFamily: h1.$value.fontFamily,
|
|
799
|
+
fontSize: { value: Math.round(h1.$value.fontSize.value * 1.75), unit: "px" },
|
|
800
|
+
lineHeight: h1.$value.lineHeight,
|
|
801
|
+
fontWeight: "{primitive.font-weight-strong}",
|
|
802
|
+
letterSpacing: h1.$value.letterSpacing,
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
if (slug === "nurix") {
|
|
806
|
+
display.$description = "derived: v2 display step — the provisional h1 × 1.75 derivation adopted as the default's own look (KPI numerals, the one focal metric)";
|
|
807
|
+
} else {
|
|
808
|
+
display.$description = "derived: v1→v2 migration — provisional display step (h1 × 1.75, strong weight)";
|
|
809
|
+
display.$extensions = { [EXT]: { hint: "display step unset — resolve from the product's design.md or leave for budget-time" } };
|
|
810
|
+
}
|
|
811
|
+
base.typography.ramp.display = display;
|
|
812
|
+
}
|
|
813
|
+
stats.displayHinted = base.typography?.ramp?.display?.$extensions?.[EXT]?.hint !== undefined;
|
|
814
|
+
|
|
815
|
+
// Step 5 — the stagger base, 30ms default (the prior derived clamp's value).
|
|
816
|
+
if (base.primitive["stagger-unit"] === undefined) {
|
|
817
|
+
base.primitive = insertAfter(base.primitive, "ease-default", [
|
|
818
|
+
["stagger-unit", { $type: "duration", $value: { value: 30, unit: "ms" }, $description: "derived: v1→v2 migration — the 30ms default stagger base (0–60ms legal)" }],
|
|
819
|
+
]);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return stats;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Every artifact directory under data/design-tokens/ (a directory holding design.tokens.json), sorted.
|
|
826
|
+
function listArtifactSlugs() {
|
|
827
|
+
return readdirSync(tokensDir, { withFileTypes: true })
|
|
828
|
+
.filter((d) => d.isDirectory() && existsSync(join(tokensDir, d.name, "design.tokens.json")))
|
|
829
|
+
.map((d) => d.name)
|
|
830
|
+
.sort();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// The design.md source text for one artifact slug, or null when the twin ships no source.
|
|
834
|
+
function readDesignMd(slug) {
|
|
835
|
+
const path = slug === "nurix" ? join(designMdDir, "nurix.md") : join(designMdDir, "library", `${slug}.md`);
|
|
836
|
+
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Run the idempotent v1→v2 migration over the existing artifact directories, then re-validate.
|
|
840
|
+
function runUpgradeV2(only) {
|
|
841
|
+
const slugs = listArtifactSlugs().filter((s) => !only || s === only);
|
|
842
|
+
if (slugs.length === 0) fail(`no artifact directory named ${only}`);
|
|
843
|
+
const totals = { upgraded: 0, elevationHinted: [], darkFirst: [], displayHinted: [] };
|
|
844
|
+
for (const slug of slugs) {
|
|
845
|
+
const dir = join(tokensDir, slug);
|
|
846
|
+
const base = JSON.parse(readFileSync(join(dir, "design.tokens.json"), "utf8"));
|
|
847
|
+
const resolver = JSON.parse(readFileSync(join(dir, "design.resolver.json"), "utf8"));
|
|
848
|
+
const darkPath = join(dir, "design.tokens.dark.json");
|
|
849
|
+
const dark = existsSync(darkPath) ? JSON.parse(readFileSync(darkPath, "utf8")) : null;
|
|
850
|
+
|
|
851
|
+
const stats = upgradeArtifact(base, dark, resolver, readDesignMd(slug), slug);
|
|
852
|
+
writeFileSync(join(dir, "design.tokens.json"), JSON.stringify(base, null, 2) + "\n");
|
|
853
|
+
writeFileSync(join(dir, "design.resolver.json"), JSON.stringify(resolver, null, 2) + "\n");
|
|
854
|
+
if (dark !== null) writeFileSync(darkPath, JSON.stringify(dark, null, 2) + "\n");
|
|
855
|
+
|
|
856
|
+
totals.upgraded++;
|
|
857
|
+
if (stats.elevationHinted) totals.elevationHinted.push(slug);
|
|
858
|
+
if (stats.darkFirst) totals.darkFirst.push(slug);
|
|
859
|
+
if (stats.displayHinted) totals.displayHinted.push(slug);
|
|
860
|
+
const marks = [stats.darkFirst ? "dark-first" : null, stats.elevationHinted ? "elevation hinted" : null, stats.displayHinted ? "display hinted" : "display assigned"].filter(Boolean);
|
|
861
|
+
console.log(`UPGRADE OK ${slug} (${marks.join(", ")})`);
|
|
862
|
+
}
|
|
863
|
+
console.log(`\nupgraded: ${totals.upgraded}`);
|
|
864
|
+
console.log(`elevation hinted (design.md names shadow values): ${totals.elevationHinted.length} — ${totals.elevationHinted.join(", ") || "none"}`);
|
|
865
|
+
console.log(`dark-first (theme-default: dark): ${totals.darkFirst.join(", ") || "none"}`);
|
|
866
|
+
console.log(`display hinted: ${totals.displayHinted.length} of ${totals.upgraded}`);
|
|
867
|
+
return totals;
|
|
868
|
+
}
|
|
869
|
+
|
|
640
870
|
// ---------------------------------------------------------------------------
|
|
641
871
|
// Validation — SCHEMA.md rules 2–10 over an emitted artifact directory
|
|
642
872
|
// ---------------------------------------------------------------------------
|
|
@@ -696,10 +926,13 @@ function validateArtifact(dir) {
|
|
|
696
926
|
try { doc = read("design.tokens.json"); } catch (e) { return [`design.tokens.json: ${e.message}`]; }
|
|
697
927
|
try { resolver = read("design.resolver.json"); } catch (e) { return [`design.resolver.json: ${e.message}`]; }
|
|
698
928
|
|
|
929
|
+
// An alias-valued token ("{path.to.token}") defers shape/.css to its target (rule 6 resolves it).
|
|
930
|
+
const isAliasValue = (t) => typeof t?.$value === "string" && /^\{[^}]+\}$/.test(t.$value);
|
|
931
|
+
|
|
699
932
|
// Rule 2 — payload.
|
|
700
933
|
const payload = doc.$extensions?.[EXT];
|
|
701
934
|
if (!payload) { errors.push("payload: missing $extensions[com.nurix.design] on the base root"); return errors; }
|
|
702
|
-
if (payload.schema !== "design-tokens/
|
|
935
|
+
if (payload.schema !== "design-tokens/v2") errors.push(`payload.schema: ${payload.schema}`);
|
|
703
936
|
if (payload.dtcg !== "2025.10") errors.push(`payload.dtcg: ${payload.dtcg}`);
|
|
704
937
|
if (!payload.product) errors.push("payload.product: empty");
|
|
705
938
|
if (!LEGAL_PROVENANCE.includes(payload.provenance)) errors.push(`payload.provenance: ${payload.provenance}`);
|
|
@@ -708,6 +941,10 @@ function validateArtifact(dir) {
|
|
|
708
941
|
if (!LEGAL_FEEDBACK.includes(payload.feedback)) errors.push(`payload.feedback: ${payload.feedback}`);
|
|
709
942
|
if (!LEGAL_DENSITY.includes(payload["density-tier"])) errors.push(`payload.density-tier: ${payload["density-tier"]}`);
|
|
710
943
|
if (!Array.isArray(payload["product-traits"]) || payload["product-traits"].length === 0) errors.push("payload.product-traits: empty");
|
|
944
|
+
if (payload["theme-default"] !== undefined && !["light", "dark"].includes(payload["theme-default"])) errors.push(`payload.theme-default: ${payload["theme-default"]}`);
|
|
945
|
+
if (payload.anchors !== undefined && (!Array.isArray(payload.anchors) || payload.anchors.length < 2 || payload.anchors.length > 3 || payload.anchors.some((a) => typeof a !== "string" || a.trim() === ""))) {
|
|
946
|
+
errors.push(`payload.anchors: must be 2–3 non-empty strings (${JSON.stringify(payload.anchors)})`);
|
|
947
|
+
}
|
|
711
948
|
const lm = payload["lever-manifest"];
|
|
712
949
|
for (const key of ["spacing-scale", "radius-scale", "shadow-levels", "motion", "icons", "accent-count"]) {
|
|
713
950
|
if (lm?.[key] === undefined) errors.push(`lever-manifest.${key}: missing`);
|
|
@@ -715,34 +952,64 @@ function validateArtifact(dir) {
|
|
|
715
952
|
if (errors.length > 0) return errors;
|
|
716
953
|
|
|
717
954
|
// Rule 3 — primitive coverage with exact $type.
|
|
718
|
-
for (const [key, type] of
|
|
955
|
+
for (const [key, type] of PRIMITIVE_TYPES_V2) {
|
|
719
956
|
const t = doc.primitive?.[key];
|
|
720
957
|
if (!t) errors.push(`primitive.${key}: missing`);
|
|
721
958
|
else if (t.$type !== type) errors.push(`primitive.${key}: $type ${t.$type} (want ${type})`);
|
|
722
959
|
else if (t.$value === undefined || t.$value === "") errors.push(`primitive.${key}: empty $value`);
|
|
723
960
|
}
|
|
961
|
+
const stagger = doc.primitive?.["stagger-unit"]?.$value?.value;
|
|
962
|
+
if (typeof stagger === "number" && (stagger < 0 || stagger > 60)) errors.push(`primitive.stagger-unit: ${stagger}ms outside 0–60ms`);
|
|
724
963
|
|
|
725
|
-
// Rule 4 — color coverage.
|
|
726
|
-
for (const role of
|
|
964
|
+
// Rule 4 — color coverage (an aliased role takes its target's shape and .css).
|
|
965
|
+
for (const role of COLOR_ROLES_V2) {
|
|
727
966
|
const t = doc.color?.[role];
|
|
728
967
|
if (!t) { errors.push(`color.${role}: missing`); continue; }
|
|
729
968
|
if (t.$type !== "color") errors.push(`color.${role}: $type ${t.$type}`);
|
|
969
|
+
if (isAliasValue(t)) continue;
|
|
730
970
|
if (!["oklch", "srgb"].includes(t.$value?.colorSpace)) errors.push(`color.${role}: colorSpace ${t.$value?.colorSpace}`);
|
|
731
971
|
if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`color.${role}: missing .css`);
|
|
732
972
|
}
|
|
733
973
|
|
|
734
|
-
// Rule 5 — typography coverage.
|
|
974
|
+
// Rule 5 — typography coverage (the seven-step ramp; family.display optional).
|
|
735
975
|
for (const fam of ["heading", "body", "mono"]) {
|
|
736
976
|
const t = doc.typography?.family?.[fam];
|
|
737
977
|
if (!t || t.$type !== "fontFamily") errors.push(`typography.family.${fam}: missing or mistyped`);
|
|
738
978
|
}
|
|
739
|
-
|
|
979
|
+
if (doc.typography?.family?.display !== undefined && doc.typography.family.display.$type !== "fontFamily") {
|
|
980
|
+
errors.push("typography.family.display: mistyped (optional slot, fontFamily shape)");
|
|
981
|
+
}
|
|
982
|
+
for (const step of RAMP_STEPS_V2) {
|
|
740
983
|
const t = doc.typography?.ramp?.[step];
|
|
741
984
|
if (!t || t.$type !== "typography") { errors.push(`typography.ramp.${step}: missing or mistyped`); continue; }
|
|
742
985
|
for (const sub of ["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"]) {
|
|
743
986
|
if (t.$value?.[sub] === undefined) errors.push(`typography.ramp.${step}.${sub}: missing sub-value`);
|
|
744
987
|
}
|
|
745
988
|
}
|
|
989
|
+
|
|
990
|
+
// Elevation coverage (rule 13) — four shadow composites, byte form preserved and parse-backed.
|
|
991
|
+
for (const level of ELEVATION_TOKENS) {
|
|
992
|
+
const t = doc.elevation?.[level];
|
|
993
|
+
if (!t || t.$type !== "shadow") { errors.push(`elevation.${level}: missing or not $type shadow`); continue; }
|
|
994
|
+
if (!Array.isArray(t.$value) || t.$value.length === 0) { errors.push(`elevation.${level}: $value must be a non-empty layer array`); continue; }
|
|
995
|
+
const css = t.$extensions?.[EXT]?.css;
|
|
996
|
+
if (typeof css !== "string") { errors.push(`elevation.${level}: missing .css`); continue; }
|
|
997
|
+
try {
|
|
998
|
+
if (JSON.stringify(parseShadowCss(css)) !== JSON.stringify(t.$value)) errors.push(`elevation.${level}: css does not parse back to $value`);
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
errors.push(`elevation.${level}: ${e.message}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Wash shape (rule 14) — optional group, ≤ 2 gradient tokens.
|
|
1005
|
+
if (doc.wash !== undefined) {
|
|
1006
|
+
const washKeys = Object.keys(doc.wash).filter((k) => !k.startsWith("$"));
|
|
1007
|
+
if (washKeys.length > 2) errors.push(`wash: at most 2 tokens (found ${washKeys.length})`);
|
|
1008
|
+
for (const key of washKeys) {
|
|
1009
|
+
if (!["surface", "emphasis"].includes(key)) errors.push(`wash.${key}: not a legal slot`);
|
|
1010
|
+
else if (doc.wash[key].$type !== "gradient") errors.push(`wash.${key}: not $type gradient`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
746
1013
|
if (errors.length > 0) return errors;
|
|
747
1014
|
|
|
748
1015
|
// Rule 6 — alias integrity (token values and composite sub-values), with type expectations.
|
|
@@ -774,16 +1041,20 @@ function validateArtifact(dir) {
|
|
|
774
1041
|
if (!lm.icons?.family) errors.push("lever-manifest.icons.family: empty");
|
|
775
1042
|
if (!(lm["accent-count"] >= 1)) errors.push(`lever-manifest.accent-count: ${lm["accent-count"]}`);
|
|
776
1043
|
|
|
777
|
-
// Rule 8 — the look's own base values sit on its own allowlist
|
|
778
|
-
|
|
1044
|
+
// Rule 8 — the look's own base values sit on its own allowlist; an off-manifest value is legal
|
|
1045
|
+
// only as a recorded deviation (a deviation: $description — rule 15's vocabulary).
|
|
1046
|
+
const deviationTagged = (t) => typeof t?.$description === "string" && t.$description.startsWith("deviation:");
|
|
1047
|
+
if (!lm["radius-scale"].includes(doc.primitive.radius.$value.value) && !deviationTagged(doc.primitive.radius)) errors.push(`radius ${doc.primitive.radius.$value.value} not on radius-scale`);
|
|
779
1048
|
for (const key of ["duration-fast", "duration-normal", "duration-slow"]) {
|
|
780
|
-
if (!lm.motion.durations.includes(doc.primitive[key].$value.value)) errors.push(`${key} ${doc.primitive[key].$value.value} not in motion.durations`);
|
|
1049
|
+
if (!lm.motion.durations.includes(doc.primitive[key].$value.value) && !deviationTagged(doc.primitive[key])) errors.push(`${key} ${doc.primitive[key].$value.value} not in motion.durations`);
|
|
781
1050
|
}
|
|
782
1051
|
const easeCss = doc.primitive["ease-default"].$extensions?.[EXT]?.css;
|
|
783
|
-
if (!lm.motion.easings.includes(easeCss)) errors.push(`ease-default ${easeCss} not in motion.easings`);
|
|
1052
|
+
if (!lm.motion.easings.includes(easeCss) && !deviationTagged(doc.primitive["ease-default"])) errors.push(`ease-default ${easeCss} not in motion.easings`);
|
|
784
1053
|
|
|
785
|
-
// Rule 9 — parse-back for every preserved .css.
|
|
786
|
-
for (const role of
|
|
1054
|
+
// Rule 9 — parse-back for every preserved .css (aliased roles carry none).
|
|
1055
|
+
for (const role of COLOR_ROLES_V2) {
|
|
1056
|
+
if (!isAliasValue(doc.color[role])) verifyColorParseBack(`color.${role}`, doc.color[role], errors);
|
|
1057
|
+
}
|
|
787
1058
|
const ht = doc.primitive["heading-tracking"];
|
|
788
1059
|
const htCss = ht.$extensions?.[EXT]?.css;
|
|
789
1060
|
if (typeof htCss !== "string") errors.push("primitive.heading-tracking: missing .css");
|
|
@@ -796,7 +1067,7 @@ function validateArtifact(dir) {
|
|
|
796
1067
|
if (typeof css !== "string") { errors.push(`primitive.${key}: missing .css`); continue; }
|
|
797
1068
|
if (JSON.stringify(parseFontStackCss(css)) !== JSON.stringify(t.$value)) errors.push(`primitive.${key}: css/$value mismatch`);
|
|
798
1069
|
}
|
|
799
|
-
for (const step of
|
|
1070
|
+
for (const step of RAMP_STEPS_V2) {
|
|
800
1071
|
const t = doc.typography.ramp[step];
|
|
801
1072
|
const css = t.$extensions?.[EXT]?.css;
|
|
802
1073
|
if (css?.letterSpacing !== undefined) {
|
|
@@ -805,25 +1076,27 @@ function validateArtifact(dir) {
|
|
|
805
1076
|
}
|
|
806
1077
|
}
|
|
807
1078
|
|
|
808
|
-
// Rule 10 + resolver coherence — the dark file carries exactly the
|
|
1079
|
+
// Rule 10 + resolver coherence — the dark file carries exactly the 36-role color group; the
|
|
1080
|
+
// resolver's theme default mirrors the payload's theme-default.
|
|
809
1081
|
const darkNamed = resolver.modifiers?.[0]?.contexts?.dark !== undefined;
|
|
810
1082
|
const darkExists = existsSync(join(dir, "design.tokens.dark.json"));
|
|
811
1083
|
if (darkNamed !== darkExists) errors.push(`dark file/resolver mismatch: named=${darkNamed} exists=${darkExists}`);
|
|
812
1084
|
if (resolver.schema !== "design-resolver/v1") errors.push(`resolver.schema: ${resolver.schema}`);
|
|
813
|
-
|
|
1085
|
+
const expectedDefault = payload["theme-default"] ?? "light";
|
|
1086
|
+
if (resolver.modifiers?.[0]?.default !== expectedDefault) errors.push(`resolver: theme default ${resolver.modifiers?.[0]?.default} does not mirror payload theme-default ${expectedDefault}`);
|
|
814
1087
|
if (darkExists) {
|
|
815
1088
|
const dark = read("design.tokens.dark.json");
|
|
816
1089
|
const rootKeys = Object.keys(dark);
|
|
817
1090
|
if (rootKeys.length !== 1 || rootKeys[0] !== "color") errors.push(`dark file: root keys ${rootKeys.join(",")} (want exactly color)`);
|
|
818
|
-
for (const role of
|
|
1091
|
+
for (const role of COLOR_ROLES_V2) {
|
|
819
1092
|
const t = dark.color?.[role];
|
|
820
1093
|
if (!t || t.$type !== "color") { errors.push(`dark color.${role}: missing or mistyped`); continue; }
|
|
821
|
-
verifyColorParseBack(`dark color.${role}`, t, errors);
|
|
1094
|
+
if (!isAliasValue(t)) verifyColorParseBack(`dark color.${role}`, t, errors);
|
|
822
1095
|
}
|
|
823
|
-
for (const role of Object.keys(dark.color ?? {})) if (!
|
|
1096
|
+
for (const role of Object.keys(dark.color ?? {})) if (!COLOR_ROLES_V2.includes(role)) errors.push(`dark color.${role}: not a legal role`);
|
|
824
1097
|
}
|
|
825
1098
|
|
|
826
|
-
// Rule 11 (type whitelist sweep) — only the
|
|
1099
|
+
// Rule 11 (type whitelist sweep) — only the ten legal $type values appear.
|
|
827
1100
|
for (const [path, token] of tokens) {
|
|
828
1101
|
if (!LEGAL_TYPES.includes(token.$type)) errors.push(`${path}: illegal $type ${token.$type}`);
|
|
829
1102
|
}
|
|
@@ -943,7 +1216,11 @@ function runConvert(only, reportPath) {
|
|
|
943
1216
|
for (const { slug, path } of sources) {
|
|
944
1217
|
let result;
|
|
945
1218
|
try {
|
|
946
|
-
|
|
1219
|
+
const sourceText = readFileSync(path, "utf8");
|
|
1220
|
+
result = convertEntry(sourceText, slug);
|
|
1221
|
+
// The converter emits the v2 schema: the same idempotent upgrade transform the migration
|
|
1222
|
+
// mode runs, applied to the freshly converted (v1-shaped) documents.
|
|
1223
|
+
upgradeArtifact(result.base, result.dark, result.resolver, sourceText, slug);
|
|
947
1224
|
} catch (e) {
|
|
948
1225
|
report.failed.push({ slug, error: e.message });
|
|
949
1226
|
console.log(`CONVERT FAIL ${slug}: ${e.message}`);
|
|
@@ -1018,6 +1295,9 @@ const opt = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1
|
|
|
1018
1295
|
|
|
1019
1296
|
if (flag("--strip")) {
|
|
1020
1297
|
runStrip(args.slice(args.indexOf("--strip") + 1).filter((a) => !a.startsWith("--")));
|
|
1298
|
+
} else if (flag("--upgrade-v2")) {
|
|
1299
|
+
runUpgradeV2(opt("--only"));
|
|
1300
|
+
process.exit(runValidate(opt("--only")) ? 0 : 1);
|
|
1021
1301
|
} else if (flag("--validate")) {
|
|
1022
1302
|
process.exit(runValidate(opt("--only")) ? 0 : 1);
|
|
1023
1303
|
} else {
|