@nurix/etna 0.18.20 → 0.18.22
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/README.md +1 -1
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +3 -3
- package/data/harnesses/code/agents/brainstorm.md +1 -1
- package/data/harnesses/code/agents/maintenance.md +8 -7
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/rules/database.md +38 -0
- package/data/harnesses/code/rules/express.md +28 -0
- package/data/harnesses/code/rules/frontend.md +14 -0
- package/data/harnesses/code/rules/general.md +2 -40
- package/data/harnesses/code/rules/nomenclature.md +1 -8
- package/data/harnesses/code/rules/nustack.md +150 -0
- package/data/harnesses/code/skills/brainstorm/README.md +4 -4
- package/data/harnesses/code/skills/brainstorm/SKILL.md +5 -3
- package/data/harnesses/code/skills/brainstorm/exit-artifacts.md +15 -11
- package/data/harnesses/code/skills/brainstorm/templates/source-of-truth.md +5 -5
- package/data/harnesses/code/skills/design/SKILL.md +14 -11
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +4 -4
- package/data/harnesses/code/skills/design/data/combinations/Readme.md +29 -0
- package/data/harnesses/code/skills/design/data/combinations/charts.csv +33 -0
- package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +437 -0
- package/data/harnesses/code/skills/design/data/combinations/icons.csv +157 -0
- package/data/harnesses/code/skills/design/data/combinations/landing.csv +38 -0
- package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +2001 -0
- package/data/harnesses/code/skills/design/data/combinations/manifest.json +283 -0
- package/data/harnesses/code/skills/design/data/combinations/palettes.csv +49 -0
- package/data/harnesses/code/skills/design/data/combinations/product-router.csv +80 -0
- package/data/harnesses/code/skills/design/data/combinations/styles.csv +54 -0
- package/data/harnesses/code/skills/design/data/combinations/typography.csv +61 -0
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +27 -238
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +7 -88
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +1 -82
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +4 -85
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +5 -86
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +4 -87
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +7 -86
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +4 -84
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +5 -86
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +3 -84
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +2 -82
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +10 -91
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +2 -84
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +3 -85
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +7 -90
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +0 -80
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +0 -80
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +5 -85
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +2 -82
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +5 -86
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +9 -91
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +9 -89
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +0 -115
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +5 -85
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +7 -88
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +3 -84
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +4 -85
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +8 -88
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +8 -88
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +4 -85
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +4 -84
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +7 -88
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +5 -86
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +6 -87
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +9 -91
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +5 -88
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +9 -89
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +4 -119
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +4 -84
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +0 -82
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +3 -86
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +13 -94
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +2 -82
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +6 -88
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +13 -94
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +4 -86
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +7 -88
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +6 -88
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +5 -86
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +6 -86
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +12 -93
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +11 -91
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +8 -88
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +2 -82
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +11 -126
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +8 -88
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +3 -83
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +10 -91
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +7 -90
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +6 -88
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +9 -89
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +4 -87
- package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +580 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +965 -0
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +954 -0
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +949 -0
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +962 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +953 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +973 -0
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +954 -0
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +963 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +968 -0
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +981 -0
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +962 -0
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +959 -0
- package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +251 -0
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +965 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +978 -0
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +982 -0
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +23 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +589 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +955 -0
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +977 -0
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +950 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +976 -0
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +955 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +953 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +904 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +957 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +945 -0
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +953 -0
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +955 -0
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +23 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +613 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +968 -0
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +956 -0
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +980 -0
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +957 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +974 -0
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +973 -0
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +966 -0
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +961 -0
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +977 -0
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +991 -0
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +966 -0
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +988 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +23 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +602 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +974 -0
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +968 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +965 -0
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +988 -0
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +978 -0
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +1 -1
- package/data/harnesses/code/skills/design/references/create.md +14 -9
- package/data/harnesses/code/skills/design/references/diagnosis.md +1 -1
- package/data/harnesses/code/skills/design/references/hard-rules.md +4 -4
- package/data/harnesses/code/skills/design/references/improve.md +11 -9
- package/data/harnesses/code/skills/design/references/introspect.md +1 -1
- package/data/harnesses/code/skills/design/references/personality.md +337 -413
- package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +1032 -0
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +596 -296
- package/data/harnesses/code/skills/design/scripts/{detector.mjs → lib/checks.mjs} +145 -140
- package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +26 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +105 -37
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +60 -73
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +1 -1
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +1 -1
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +205 -37
- package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +68 -0
- package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +314 -0
- package/data/harnesses/code/skills/documentation/SKILL.md +61 -17
- package/data/harnesses/code/skills/documentation/references/examples.md +41 -0
- package/data/harnesses/code/skills/documentation/references/style-guide.md +14 -9
- package/data/harnesses/code/skills/documentation/references/templates.md +306 -52
- package/data/harnesses/etna/CLAUDE.md +3 -3
- package/data/harnesses/etna/agents/brainstorm.md +1 -1
- package/data/harnesses/etna/agents/maintenance.md +8 -7
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/rules/general.md +2 -40
- package/data/harnesses/etna/rules/nomenclature.md +1 -8
- package/data/harnesses/etna/skills/brainstorm/README.md +4 -4
- package/data/harnesses/etna/skills/brainstorm/SKILL.md +5 -3
- package/data/harnesses/etna/skills/brainstorm/exit-artifacts.md +15 -11
- package/data/harnesses/etna/skills/brainstorm/templates/source-of-truth.md +5 -5
- package/data/harnesses/etna/skills/documentation/SKILL.md +61 -17
- package/data/harnesses/etna/skills/documentation/references/examples.md +41 -0
- package/data/harnesses/etna/skills/documentation/references/style-guide.md +14 -9
- package/data/harnesses/etna/skills/documentation/references/templates.md +306 -52
- package/data/harnesses/mobile/CLAUDE.md +3 -3
- package/data/harnesses/mobile/agents/brainstorm.md +1 -1
- package/data/harnesses/mobile/agents/maintenance.md +8 -7
- package/data/harnesses/mobile/harness.json +2 -2
- package/data/harnesses/mobile/rules/database.md +38 -0
- package/data/harnesses/mobile/rules/general.md +2 -40
- package/data/harnesses/mobile/rules/nomenclature.md +1 -8
- package/data/harnesses/mobile/rules/nustack.md +150 -0
- package/data/harnesses/mobile/skills/brainstorm/README.md +4 -4
- package/data/harnesses/mobile/skills/brainstorm/SKILL.md +5 -3
- package/data/harnesses/mobile/skills/brainstorm/exit-artifacts.md +15 -11
- package/data/harnesses/mobile/skills/brainstorm/templates/source-of-truth.md +5 -5
- package/data/harnesses/mobile/skills/documentation/SKILL.md +61 -17
- package/data/harnesses/mobile/skills/documentation/references/examples.md +41 -0
- package/data/harnesses/mobile/skills/documentation/references/style-guide.md +14 -9
- package/data/harnesses/mobile/skills/documentation/references/templates.md +306 -52
- package/package.json +1 -1
- package/data/harnesses/code/skills/design/scripts/hook-config.json +0 -11
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// skills/design/scripts/derive-personality.mjs — design
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
// skills/design/scripts/derive-personality.mjs — design-tokens artifact resolver, coverage gate (--check; SCHEMA.md rules 1–12), and CSS emitter (--emit) for the operator-UI personality system (D29, D6).
|
|
3
|
+
//
|
|
4
|
+
// The reference consumer of data/design-tokens/SCHEMA.md: resolves an artifact directory
|
|
5
|
+
// (design.tokens.json + design.resolver.json [+ design.tokens.dark.json]) through the resolver's
|
|
6
|
+
// deterministic order — concatenate the set files, overlay the selected theme context, resolve
|
|
7
|
+
// aliases — then validates (rules 1–12) or emits personality.css / globals.css / the <html> attributes.
|
|
8
|
+
//
|
|
9
|
+
// Usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir>] [--theme light|dark]
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
12
|
import { dirname, join } from "node:path";
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
]
|
|
14
|
+
const EXT = "com.nurix.design";
|
|
15
|
+
|
|
16
|
+
// The 26 visual primitives and their DTCG $type, in emission order (leaf names are law; no leading --).
|
|
17
|
+
const PRIMITIVE_TYPES = new Map([
|
|
18
|
+
["radius", "dimension"], ["shadow-intensity", "number"], ["border-weight", "dimension"],
|
|
19
|
+
["density-gap", "number"], ["density-inset", "number"],
|
|
20
|
+
["font-sans", "fontFamily"], ["font-mono", "fontFamily"],
|
|
21
|
+
["font-weight-normal", "fontWeight"], ["font-weight-strong", "fontWeight"],
|
|
22
|
+
["heading-tracking", "number"],
|
|
23
|
+
["duration-unit", "duration"], ["duration-fast", "duration"], ["duration-normal", "duration"],
|
|
24
|
+
["duration-slow", "duration"], ["ease-default", "cubicBezier"],
|
|
25
|
+
["contrast-boost", "number"],
|
|
26
|
+
["icon-stroke", "number"], ["icon-size-base", "dimension"],
|
|
27
|
+
["sidebar-width-expanded", "dimension"], ["sidebar-width-collapsed", "dimension"],
|
|
28
|
+
["topbar-height", "dimension"], ["side-panel-width", "dimension"],
|
|
29
|
+
["side-panel-width-rich", "dimension"], ["modal-width-max", "dimension"],
|
|
30
|
+
["form-max-width", "dimension"], ["table-row-height", "dimension"],
|
|
31
|
+
]);
|
|
32
|
+
const PRIMITIVES = [...PRIMITIVE_TYPES.keys()];
|
|
17
33
|
|
|
18
34
|
// The 17 semantic color tokens, in emission order.
|
|
19
35
|
const SEMANTIC_TOKENS = [
|
|
@@ -32,312 +48,574 @@ const SUBSTRATE_TOKENS = [
|
|
|
32
48
|
|
|
33
49
|
const ALL_TOKENS = [...SEMANTIC_TOKENS, ...SUBSTRATE_TOKENS];
|
|
34
50
|
|
|
51
|
+
const RAMP_STEPS = ["h1", "h2", "h3", "body", "small", "caption"];
|
|
35
52
|
const LEGAL_VERBOSITY = ["full", "default", "compact", "expert"];
|
|
36
53
|
const LEGAL_FEEDBACK = ["subtle", "minimal"];
|
|
37
54
|
const LEGAL_DENSITY = ["spacious", "standard", "dense"];
|
|
38
55
|
const LEGAL_PROVENANCE = ["project", "extracted", "library", "default"];
|
|
56
|
+
const LEGAL_TYPES = ["color", "dimension", "duration", "cubicBezier", "fontFamily", "fontWeight", "number", "typography"];
|
|
57
|
+
const LEGAL_THEMES = ["light", "dark"];
|
|
58
|
+
const MANIFEST_KEYS = ["spacing-scale", "radius-scale", "shadow-levels", "motion", "icons", "accent-count"];
|
|
39
59
|
|
|
40
60
|
// Shadow-level names ranked by increasing intensity, for the ordering check.
|
|
41
61
|
const SHADOW_RANK = { none: 0, "2xs": 1, xs: 2, sm: 3, md: 4, lg: 5, xl: 6, "2xl": 7 };
|
|
42
62
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else if (c === "#" && !inSingle && !inDouble) return line.slice(0, i);
|
|
51
|
-
}
|
|
52
|
-
return line;
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// CSS value parsers — the parse-back authority (rule 9); byte-compatible with convert-design-md.mjs
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
// Round to at most 5 decimals (the srgb component precision).
|
|
68
|
+
function round5(n) {
|
|
69
|
+
return Math.round(n * 1e5) / 1e5;
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
//
|
|
72
|
+
// Strip one layer of matching surrounding quotes and unescape \" within double quotes.
|
|
56
73
|
function unquote(raw) {
|
|
57
74
|
const s = raw.trim();
|
|
58
|
-
if (s.length >= 2 &&
|
|
59
|
-
|
|
60
|
-
}
|
|
75
|
+
if (s.length >= 2 && s[0] === '"' && s.at(-1) === '"') return s.slice(1, -1).replaceAll('\\"', '"');
|
|
76
|
+
if (s.length >= 2 && s[0] === "'" && s.at(-1) === "'") return s.slice(1, -1);
|
|
61
77
|
return s;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
// Split
|
|
80
|
+
// Split on a delimiter at bracket-depth 0 and outside quotes.
|
|
65
81
|
function splitTopLevel(str, delim) {
|
|
66
82
|
const parts = [];
|
|
67
|
-
let depth = 0,
|
|
68
|
-
for (
|
|
69
|
-
|
|
83
|
+
let depth = 0, inDouble = false, inSingle = false, cur = "";
|
|
84
|
+
for (let i = 0; i < str.length; i++) {
|
|
85
|
+
const c = str[i];
|
|
86
|
+
if (c === '"' && !inSingle && str[i - 1] !== "\\") { inDouble = !inDouble; cur += c; }
|
|
70
87
|
else if (c === "'" && !inDouble) { inSingle = !inSingle; cur += c; }
|
|
71
|
-
else if (!
|
|
72
|
-
else if (!
|
|
73
|
-
else if (!
|
|
88
|
+
else if (!inDouble && !inSingle && (c === "[" || c === "{" || c === "(")) { depth++; cur += c; }
|
|
89
|
+
else if (!inDouble && !inSingle && (c === "]" || c === "}" || c === ")")) { depth--; cur += c; }
|
|
90
|
+
else if (!inDouble && !inSingle && depth === 0 && c === delim) { parts.push(cur); cur = ""; }
|
|
74
91
|
else cur += c;
|
|
75
92
|
}
|
|
76
93
|
parts.push(cur);
|
|
77
94
|
return parts;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
// Parse
|
|
81
|
-
function
|
|
82
|
-
const s =
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Parse an inline { k: v, ... } object into a plain JS object.
|
|
96
|
-
function parseInlineObject(s) {
|
|
97
|
-
const inner = s.slice(s.indexOf("{") + 1, s.lastIndexOf("}"));
|
|
98
|
-
const obj = {};
|
|
99
|
-
for (const part of splitTopLevel(inner, ",")) {
|
|
100
|
-
if (part.trim() === "") continue;
|
|
101
|
-
const ci = part.indexOf(":");
|
|
102
|
-
if (ci < 0) continue;
|
|
103
|
-
obj[part.slice(0, ci).trim()] = parseScalar(part.slice(ci + 1));
|
|
104
|
-
}
|
|
105
|
-
return obj;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Turn a block's raw text into meaningful {indent, content} nodes (comment-stripped, blank-dropped).
|
|
109
|
-
function toNodes(text) {
|
|
110
|
-
const nodes = [];
|
|
111
|
-
for (const rawLine of text.split("\n")) {
|
|
112
|
-
const line = stripComment(rawLine);
|
|
113
|
-
if (line.trim() === "") continue;
|
|
114
|
-
const indent = line.length - line.trimStart().length;
|
|
115
|
-
nodes.push({ indent, content: line.trim() });
|
|
116
|
-
}
|
|
117
|
-
return nodes;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Recursively build a nested map from indentation nodes (tolerant mini-YAML for 2-space maps).
|
|
121
|
-
function buildMap(nodes, start, indent) {
|
|
122
|
-
const obj = {};
|
|
123
|
-
let i = start;
|
|
124
|
-
while (i < nodes.length) {
|
|
125
|
-
const node = nodes[i];
|
|
126
|
-
if (node.indent < indent) break;
|
|
127
|
-
if (node.indent > indent) { i++; continue; }
|
|
128
|
-
const ci = node.content.indexOf(":");
|
|
129
|
-
if (ci < 0) { i++; continue; }
|
|
130
|
-
const key = node.content.slice(0, ci).trim();
|
|
131
|
-
const rest = node.content.slice(ci + 1).trim();
|
|
132
|
-
if (rest === "") {
|
|
133
|
-
if (i + 1 < nodes.length && nodes[i + 1].indent > indent) {
|
|
134
|
-
const [child, next] = buildMap(nodes, i + 1, nodes[i + 1].indent);
|
|
135
|
-
obj[key] = child;
|
|
136
|
-
i = next;
|
|
137
|
-
} else {
|
|
138
|
-
obj[key] = {};
|
|
139
|
-
i++;
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
obj[key] = parseScalar(rest);
|
|
143
|
-
i++;
|
|
144
|
-
}
|
|
97
|
+
// Parse any corpus color string (oklch / #rrggbb / #rrggbbaa / rgb() / rgba()) into a typed DTCG color $value.
|
|
98
|
+
function parseColorCss(css) {
|
|
99
|
+
const s = css.trim();
|
|
100
|
+
let m = /^oklch\(\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s);
|
|
101
|
+
if (m) {
|
|
102
|
+
const value = { colorSpace: "oklch", components: [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])] };
|
|
103
|
+
if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
m = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
|
|
107
|
+
if (m) {
|
|
108
|
+
const bytes = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
|
|
109
|
+
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex: `#${m[1].toLowerCase()}` };
|
|
110
|
+
if (m[2] !== undefined) value.alpha = round5(parseInt(m[2], 16) / 255);
|
|
111
|
+
return value;
|
|
145
112
|
}
|
|
146
|
-
|
|
113
|
+
m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(s);
|
|
114
|
+
if (m) {
|
|
115
|
+
const bytes = [m[1], m[2], m[3]].map(Number);
|
|
116
|
+
const hex = "#" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
117
|
+
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex };
|
|
118
|
+
if (m[4] !== undefined) value.alpha = parseFloat(m[4]);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`unparseable color: ${JSON.stringify(css)}`);
|
|
147
122
|
}
|
|
148
123
|
|
|
149
|
-
// Parse a
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
124
|
+
// Parse a tracking string ("0" | "0em" | "-0.015em") into its em-multiplier number.
|
|
125
|
+
function parseTrackingCss(css) {
|
|
126
|
+
const m = /^([+-]?\d*\.?\d+)(em)?$/.exec(css.trim());
|
|
127
|
+
if (!m) throw new Error(`unparseable tracking: ${JSON.stringify(css)}`);
|
|
128
|
+
return parseFloat(m[1]);
|
|
153
129
|
}
|
|
154
130
|
|
|
155
|
-
//
|
|
156
|
-
function
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
131
|
+
// Parse "cubic-bezier(x1,y1,x2,y2)" into its four numbers.
|
|
132
|
+
function parseCubicBezierCss(css) {
|
|
133
|
+
const m = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/.exec(css.trim());
|
|
134
|
+
if (!m) throw new Error(`unparseable cubic-bezier: ${JSON.stringify(css)}`);
|
|
135
|
+
return [1, 2, 3, 4].map((i) => parseFloat(m[i]));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Parse a font stack string into its array of unquoted family names.
|
|
139
|
+
function parseFontStackCss(css) {
|
|
140
|
+
return splitTopLevel(css, ",").map((p) => unquote(p)).filter((p) => p !== "");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Parse a ramp letterSpacing string ("+0.02em" | "-0.28px" | "0") into an em multiplier, px scaled by fontSize.
|
|
144
|
+
function parseLetterSpacingCss(css, fontSizePx) {
|
|
145
|
+
const m = /^([+-]?\d*\.?\d+)(em|px)?$/.exec(css.trim());
|
|
146
|
+
if (!m) throw new Error(`unparseable letterSpacing: ${JSON.stringify(css)}`);
|
|
147
|
+
const n = parseFloat(m[1]);
|
|
148
|
+
return m[2] === "px" ? round5(n / fontSizePx) : n;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseDimensionCss(css) {
|
|
152
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(css.trim());
|
|
153
|
+
if (!m) throw new Error(`unparseable dimension: ${JSON.stringify(css)}`);
|
|
154
|
+
return { value: parseFloat(m[1]), unit: "px" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseDurationCss(css) {
|
|
158
|
+
const m = /^(\d*\.?\d+)ms$/.exec(css.trim());
|
|
159
|
+
if (!m) throw new Error(`unparseable duration: ${JSON.stringify(css)}`);
|
|
160
|
+
return { value: parseFloat(m[1]), unit: "ms" };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseNumberCss(css) {
|
|
164
|
+
const n = parseFloat(css.trim());
|
|
165
|
+
if (Number.isNaN(n) || !/^[+-]?\d*\.?\d+$/.test(css.trim())) throw new Error(`unparseable number: ${JSON.stringify(css)}`);
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Serialization — the schema's deterministic byte forms for tokens without preserved .css
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
// Serialize a concrete $value to its deterministic CSS byte form (the fallback when .css is absent).
|
|
174
|
+
function serializeValue(type, value) {
|
|
175
|
+
if (type === "dimension" || type === "duration") return `${value.value}${value.unit}`;
|
|
176
|
+
if (type === "number" || type === "fontWeight") return String(value);
|
|
177
|
+
if (type === "cubicBezier") return `cubic-bezier(${value.join(", ")})`;
|
|
178
|
+
if (type === "fontFamily") {
|
|
179
|
+
return (Array.isArray(value) ? value : [value]).map((f) => (/\s/.test(f) ? `"${f}"` : f)).join(", ");
|
|
180
|
+
}
|
|
181
|
+
if (type === "color") {
|
|
182
|
+
if (value.colorSpace === "srgb" && value.hex) return value.hex;
|
|
183
|
+
return `oklch(${value.components.join(" ")}${value.alpha !== undefined ? ` / ${value.alpha}` : ""})`;
|
|
184
|
+
}
|
|
185
|
+
return String(value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Artifact loading & resolution (SCHEMA.md resolver section — deterministic order)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
// True when a $value is an alias reference string ("{path.to.token}").
|
|
193
|
+
function isAlias(v) {
|
|
194
|
+
return typeof v === "string" && /^\{[^}]+\}$/.test(v);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// True when a node is a token (an object carrying $type).
|
|
198
|
+
function isToken(node) {
|
|
199
|
+
return node !== null && typeof node === "object" && !Array.isArray(node) && "$type" in node;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Collect every token in a tree as a path → token map ($-prefixed keys are metadata, not groups).
|
|
203
|
+
function collectTokens(root) {
|
|
204
|
+
const tokens = new Map();
|
|
205
|
+
const walk = (node, path) => {
|
|
206
|
+
if (isToken(node)) { tokens.set(path.join("."), node); return; }
|
|
207
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
208
|
+
for (const [k, v] of Object.entries(node)) if (!k.startsWith("$")) walk(v, [...path, k]);
|
|
166
209
|
}
|
|
167
|
-
if (closeRe.test(line.trim())) return body.join("\n");
|
|
168
|
-
body.push(line);
|
|
169
|
-
}
|
|
170
|
-
return capturing ? body.join("\n") : null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Parse the leading --- frontmatter into a flat object.
|
|
174
|
-
function parseFrontmatter(source) {
|
|
175
|
-
const m = /^---\s*\n([\s\S]*?)\n---\s*(\n|$)/.exec(source);
|
|
176
|
-
if (!m) return null;
|
|
177
|
-
const obj = {};
|
|
178
|
-
for (const rawLine of m[1].split("\n")) {
|
|
179
|
-
const line = stripComment(rawLine);
|
|
180
|
-
const ci = line.indexOf(":");
|
|
181
|
-
if (ci < 0) continue;
|
|
182
|
-
obj[line.slice(0, ci).trim()] = unquote(line.slice(ci + 1));
|
|
183
|
-
}
|
|
184
|
-
return obj;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Parse a whole design.md into its three parsed parts.
|
|
188
|
-
function parseDesignMd(source) {
|
|
189
|
-
const frontmatter = parseFrontmatter(source);
|
|
190
|
-
const primitivesText = extractFencedBlock(source, "design-primitives");
|
|
191
|
-
const manifestText = extractFencedBlock(source, "lever-manifest");
|
|
192
|
-
return {
|
|
193
|
-
frontmatter,
|
|
194
|
-
primitives: primitivesText == null ? null : parseBlock(primitivesText),
|
|
195
|
-
manifest: manifestText == null ? null : parseBlock(manifestText),
|
|
196
210
|
};
|
|
211
|
+
walk(root, []);
|
|
212
|
+
return tokens;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Overlay one modifier file onto the base at token-path granularity; an overlay may only
|
|
216
|
+
// override paths that exist in the base, never introduce new ones.
|
|
217
|
+
function overlayTree(base, overlay, file, errors, path = []) {
|
|
218
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
219
|
+
if (key.startsWith("$")) continue;
|
|
220
|
+
const here = [...path, key];
|
|
221
|
+
if (!(key in base)) {
|
|
222
|
+
errors.push(`rule 1 — resolver overlay ${file}: introduces ${here.join(".")}, absent from the base set`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
|
|
226
|
+
base[key] = value;
|
|
227
|
+
} else {
|
|
228
|
+
overlayTree(base[key], value, file, errors, here);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
//
|
|
200
|
-
function
|
|
201
|
-
|
|
233
|
+
// Concatenate a later set file onto the accumulated base (a later file overrides per token path; new paths are legal here).
|
|
234
|
+
function concatTree(base, add) {
|
|
235
|
+
for (const [key, value] of Object.entries(add)) {
|
|
236
|
+
if (key.startsWith("$")) continue;
|
|
237
|
+
if (!(key in base) || isToken(value) || isToken(base[key]) || typeof value !== "object" || value === null) {
|
|
238
|
+
base[key] = value;
|
|
239
|
+
} else {
|
|
240
|
+
concatTree(base[key], value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
202
243
|
}
|
|
203
244
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
245
|
+
// Load and resolve one artifact directory: parse files (rule 1), concatenate sets, build one
|
|
246
|
+
// resolved view per theme context. Returns null when the artifact is unreadable.
|
|
247
|
+
function loadArtifact(dir, errors) {
|
|
248
|
+
const readJson = (name) => {
|
|
249
|
+
const path = join(dir, name);
|
|
250
|
+
if (!existsSync(path)) { errors.push(`rule 1 — ${name}: missing`); return null; }
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
253
|
+
} catch (e) {
|
|
254
|
+
errors.push(`rule 1 — ${name}: unparseable JSON (${e.message})`);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const base = readJson("design.tokens.json");
|
|
260
|
+
const resolver = readJson("design.resolver.json");
|
|
261
|
+
if (base === null || resolver === null) return null;
|
|
262
|
+
|
|
263
|
+
if (resolver.schema !== "design-resolver/v1") {
|
|
264
|
+
errors.push(`rule 1 — design.resolver.json: schema must be "design-resolver/v1" (found ${JSON.stringify(resolver.schema)})`);
|
|
265
|
+
}
|
|
266
|
+
if (!Array.isArray(resolver.sets) || resolver.sets.length === 0) {
|
|
267
|
+
errors.push("rule 1 — design.resolver.json: sets must be a non-empty array");
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const themeModifier = (resolver.modifiers ?? []).find((m) => m?.name === "theme");
|
|
271
|
+
for (const m of resolver.modifiers ?? []) {
|
|
272
|
+
if (m?.name !== "theme") errors.push(`rule 1 — design.resolver.json: unknown modifier ${JSON.stringify(m?.name)} (exactly one modifier today: theme)`);
|
|
273
|
+
}
|
|
274
|
+
if (!themeModifier) {
|
|
275
|
+
errors.push("rule 1 — design.resolver.json: missing the theme modifier");
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (themeModifier.default !== "light") errors.push("rule 1 — design.resolver.json: theme default must be light (light-first)");
|
|
279
|
+
if (!Array.isArray(themeModifier.contexts?.light) || themeModifier.contexts.light.length !== 0) {
|
|
280
|
+
errors.push("rule 1 — design.resolver.json: the light context must be the empty overlay [] (the base set is the light rendering)");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Every file the resolver references exists beside it — and parses.
|
|
284
|
+
const files = new Map([["design.tokens.json", base]]);
|
|
285
|
+
const referenced = new Set(["design.tokens.json"]);
|
|
286
|
+
const loadReferenced = (name, where) => {
|
|
287
|
+
referenced.add(name);
|
|
288
|
+
if (files.has(name)) return;
|
|
289
|
+
const doc = readJson(name);
|
|
290
|
+
if (doc === null) errors.push(`rule 1 — resolver ${where} references ${name}`);
|
|
291
|
+
else files.set(name, doc);
|
|
292
|
+
};
|
|
293
|
+
for (const set of resolver.sets) {
|
|
294
|
+
for (const name of set?.values ?? []) loadReferenced(name, `set "${set.name}"`);
|
|
295
|
+
}
|
|
296
|
+
for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
|
|
297
|
+
for (const name of names ?? []) loadReferenced(name, `theme context "${context}"`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The dark file exists exactly when the resolver's dark context names it.
|
|
301
|
+
const darkNamed = (themeModifier.contexts?.dark ?? []).includes("design.tokens.dark.json");
|
|
302
|
+
const darkExists = existsSync(join(dir, "design.tokens.dark.json"));
|
|
303
|
+
if (darkNamed && !darkExists) errors.push("rule 1 — design.tokens.dark.json: named by the resolver's dark context but absent");
|
|
304
|
+
if (!darkNamed && darkExists) errors.push("rule 1 — design.tokens.dark.json: present but not named by a resolver dark context");
|
|
305
|
+
|
|
306
|
+
// Resolution: concatenate the set files in array order, then one view per theme context.
|
|
307
|
+
const merged = {};
|
|
308
|
+
for (const set of resolver.sets) {
|
|
309
|
+
for (const name of set?.values ?? []) {
|
|
310
|
+
if (files.has(name)) concatTree(merged, files.get(name));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const views = new Map();
|
|
314
|
+
for (const [context, names] of Object.entries(themeModifier.contexts ?? {})) {
|
|
315
|
+
const view = structuredClone(merged);
|
|
316
|
+
for (const name of names ?? []) {
|
|
317
|
+
if (files.has(name)) overlayTree(view, files.get(name), name, errors);
|
|
318
|
+
}
|
|
319
|
+
views.set(context, view);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { base, resolver, files, views, payload: base?.$extensions?.[EXT] };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Follow an alias chain to its final concrete token; records rule-6 errors (dangling refs, cycles, type mismatches).
|
|
326
|
+
function resolveAlias(ref, tokens, expectedType, context, errors) {
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
let cur = ref;
|
|
329
|
+
while (isAlias(cur)) {
|
|
330
|
+
const path = cur.slice(1, -1);
|
|
331
|
+
if (seen.has(path)) { errors.push(`rule 6 — ${context}: alias cycle through {${path}}`); return null; }
|
|
332
|
+
seen.add(path);
|
|
333
|
+
const target = tokens.get(path);
|
|
334
|
+
if (!target) { errors.push(`rule 6 — ${context}: alias {${path}} does not resolve`); return null; }
|
|
335
|
+
if (expectedType && target.$type !== expectedType) {
|
|
336
|
+
errors.push(`rule 6 — ${context}: alias {${path}} resolves to $type ${target.$type} (want ${expectedType})`);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
if (!isAlias(target.$value)) return target;
|
|
340
|
+
cur = target.$value;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The emitted CSS string for a token path: follow aliases, prefer the preserved .css, fall back
|
|
346
|
+
// to the schema's deterministic serialization.
|
|
347
|
+
function cssFor(path, tokens, errors) {
|
|
348
|
+
const token = tokens.get(path);
|
|
349
|
+
if (!token) { errors.push(`emit — ${path}: token not found`); return ""; }
|
|
350
|
+
let target = token;
|
|
351
|
+
if (isAlias(token.$value)) {
|
|
352
|
+
target = resolveAlias(token.$value, tokens, token.$type, path, errors);
|
|
353
|
+
if (!target) return "";
|
|
354
|
+
}
|
|
355
|
+
const css = target.$extensions?.[EXT]?.css;
|
|
356
|
+
if (typeof css === "string") return css;
|
|
357
|
+
return serializeValue(target.$type, target.$value);
|
|
207
358
|
}
|
|
208
359
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Validation — SCHEMA.md rules 1–12 (rule 12 is enforced at emit time)
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
// True when a token's $value is absent or vacuously empty.
|
|
365
|
+
function isEmptyValue(v) {
|
|
366
|
+
return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
215
367
|
}
|
|
216
368
|
|
|
217
|
-
//
|
|
218
|
-
function
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
errors.push(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
369
|
+
// Verify a preserved-css color token: parse-back equality plus the srgb hex round-trip.
|
|
370
|
+
function verifyColorParseBack(name, token, errors) {
|
|
371
|
+
const css = token.$extensions?.[EXT]?.css;
|
|
372
|
+
if (typeof css !== "string" || isAlias(token.$value)) return;
|
|
373
|
+
let reparsed;
|
|
374
|
+
try { reparsed = parseColorCss(css); } catch (e) { errors.push(`rule 9 — ${name}: ${e.message}`); return; }
|
|
375
|
+
if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) {
|
|
376
|
+
errors.push(`rule 9 — ${name}: parse-back mismatch — css ${JSON.stringify(css)} → ${JSON.stringify(reparsed)} != $value ${JSON.stringify(token.$value)}`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (token.$value.colorSpace === "srgb") {
|
|
380
|
+
const viaHex = parseColorCss(token.$value.hex);
|
|
381
|
+
if (JSON.stringify(viaHex.components) !== JSON.stringify(token.$value.components)) {
|
|
382
|
+
errors.push(`rule 9 — ${name}: srgb components do not round-trip through hex ${token.$value.hex}`);
|
|
228
383
|
}
|
|
229
|
-
|
|
230
|
-
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Rule 9 for one non-color token carrying a preserved string .css: parsing it must reproduce $value.
|
|
388
|
+
function verifyScalarParseBack(name, token, css, errors) {
|
|
389
|
+
if (isAlias(token.$value)) return; // an aliased token takes the target's .css; rule 6 owns alias integrity
|
|
390
|
+
try {
|
|
391
|
+
if (token.$type === "cubicBezier") {
|
|
392
|
+
if (JSON.stringify(parseCubicBezierCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
393
|
+
} else if (token.$type === "fontFamily") {
|
|
394
|
+
if (JSON.stringify(parseFontStackCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
395
|
+
} else if (token.$type === "number") {
|
|
396
|
+
if (parseTrackingCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
|
|
397
|
+
} else if (token.$type === "dimension") {
|
|
398
|
+
if (JSON.stringify(parseDimensionCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
399
|
+
} else if (token.$type === "duration") {
|
|
400
|
+
if (JSON.stringify(parseDurationCss(css)) !== JSON.stringify(token.$value)) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value`);
|
|
401
|
+
} else if (token.$type === "fontWeight") {
|
|
402
|
+
if (parseNumberCss(css) !== token.$value) errors.push(`rule 9 — ${name}: css ${JSON.stringify(css)} != $value ${token.$value}`);
|
|
231
403
|
}
|
|
404
|
+
} catch (e) {
|
|
405
|
+
errors.push(`rule 9 — ${name}: ${e.message}`);
|
|
232
406
|
}
|
|
407
|
+
}
|
|
233
408
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
409
|
+
// Run the full coverage gate (rules 1–11) over one artifact directory; returns everything emit needs.
|
|
410
|
+
function runCheck(dir, theme) {
|
|
411
|
+
const errors = [];
|
|
412
|
+
const artifact = loadArtifact(dir, errors);
|
|
413
|
+
if (!artifact) return { errors };
|
|
414
|
+
const { base, files, views, payload } = artifact;
|
|
238
415
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (v === undefined) errors.push(`missing primitive: ${key}`);
|
|
243
|
-
else if (!isFilled(v)) errors.push(`empty primitive: ${key}`);
|
|
416
|
+
if (!views.has(theme)) {
|
|
417
|
+
errors.push(`rule 1 — resolver: --theme ${theme} requested but the resolver carries no ${theme} context`);
|
|
418
|
+
return { errors };
|
|
244
419
|
}
|
|
245
420
|
|
|
246
|
-
// Rule
|
|
247
|
-
if (!
|
|
248
|
-
errors.push(`
|
|
421
|
+
// Rule 2 — the file-level payload on the base root.
|
|
422
|
+
if (!payload || typeof payload !== "object") {
|
|
423
|
+
errors.push(`rule 2 — payload: missing $extensions["${EXT}"] on the base file's root group`);
|
|
424
|
+
return { errors };
|
|
249
425
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
426
|
+
if (payload.schema !== "design-tokens/v1") errors.push(`rule 2 — payload.schema: must be "design-tokens/v1" (found ${JSON.stringify(payload.schema)})`);
|
|
427
|
+
if (payload.dtcg !== "2025.10") errors.push(`rule 2 — payload.dtcg: must be "2025.10" (found ${JSON.stringify(payload.dtcg)})`);
|
|
428
|
+
if (typeof payload.product !== "string" || payload.product.trim() === "") errors.push("rule 2 — payload.product: missing or empty");
|
|
429
|
+
if (!LEGAL_PROVENANCE.includes(payload.provenance)) errors.push(`rule 2 — payload.provenance: must be one of ${LEGAL_PROVENANCE.join("|")} (found ${JSON.stringify(payload.provenance)})`);
|
|
430
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.sourced ?? "")) errors.push(`rule 2 — payload.sourced: must be an ISO date (found ${JSON.stringify(payload.sourced)})`);
|
|
431
|
+
if (!LEGAL_VERBOSITY.includes(payload.verbosity)) errors.push(`rule 2 — payload.verbosity: must be one of ${LEGAL_VERBOSITY.join("|")} (found ${JSON.stringify(payload.verbosity)})`);
|
|
432
|
+
if (!LEGAL_FEEDBACK.includes(payload.feedback)) errors.push(`rule 2 — payload.feedback: must be one of ${LEGAL_FEEDBACK.join("|")} (found ${JSON.stringify(payload.feedback)})`);
|
|
433
|
+
if (!LEGAL_DENSITY.includes(payload["density-tier"])) errors.push(`rule 2 — payload.density-tier: must be one of ${LEGAL_DENSITY.join("|")} (found ${JSON.stringify(payload["density-tier"])})`);
|
|
434
|
+
if (!Array.isArray(payload["product-traits"]) || payload["product-traits"].length === 0) errors.push("rule 2 — payload.product-traits: must be a non-empty array");
|
|
435
|
+
const lm = payload["lever-manifest"];
|
|
436
|
+
for (const key of MANIFEST_KEYS) {
|
|
437
|
+
if (lm?.[key] === undefined) errors.push(`rule 2 — payload.lever-manifest.${key}: missing`);
|
|
252
438
|
}
|
|
253
|
-
|
|
254
|
-
|
|
439
|
+
|
|
440
|
+
const lightView = views.get("light");
|
|
441
|
+
const lightTokens = collectTokens(lightView);
|
|
442
|
+
|
|
443
|
+
// Rule 3 — all 26 primitives present with the exact $type, each assigned or hinted.
|
|
444
|
+
for (const [key, type] of PRIMITIVE_TYPES) {
|
|
445
|
+
const t = lightView.primitive?.[key];
|
|
446
|
+
if (!isToken(t)) { errors.push(`rule 3 — primitive.${key}: missing`); continue; }
|
|
447
|
+
if (t.$type !== type) errors.push(`rule 3 — primitive.${key}: $type ${JSON.stringify(t.$type)} (want ${type})`);
|
|
448
|
+
if (isEmptyValue(t.$value)) errors.push(`rule 3 — primitive.${key}: empty $value`);
|
|
255
449
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
450
|
+
|
|
451
|
+
// Rule 4 — all 34 color roles present, typed color, legal colorSpace, .css present (via alias when aliased).
|
|
452
|
+
for (const role of ALL_TOKENS) {
|
|
453
|
+
const t = lightView.color?.[role];
|
|
454
|
+
if (!isToken(t)) { errors.push(`rule 4 — color.${role}: missing`); continue; }
|
|
455
|
+
if (t.$type !== "color") { errors.push(`rule 4 — color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
|
|
456
|
+
const target = isAlias(t.$value) ? resolveAlias(t.$value, lightTokens, "color", `color.${role}`, []) : t;
|
|
457
|
+
if (!target) continue; // dangling alias — rule 6 reports it
|
|
458
|
+
if (!["oklch", "srgb"].includes(target.$value?.colorSpace)) errors.push(`rule 4 — color.${role}: colorSpace ${JSON.stringify(target.$value?.colorSpace)} (want oklch|srgb)`);
|
|
459
|
+
if (typeof target.$extensions?.[EXT]?.css !== "string") errors.push(`rule 4 — color.${role}: missing preserved .css`);
|
|
259
460
|
}
|
|
260
461
|
|
|
261
|
-
// Rule 5 —
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
errors.push(
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
462
|
+
// Rule 5 — typography coverage: the three families and the six-step ramp with all five sub-values.
|
|
463
|
+
for (const fam of ["heading", "body", "mono"]) {
|
|
464
|
+
const t = lightView.typography?.family?.[fam];
|
|
465
|
+
if (!isToken(t) || t.$type !== "fontFamily") errors.push(`rule 5 — typography.family.${fam}: missing or not $type fontFamily`);
|
|
466
|
+
}
|
|
467
|
+
for (const step of RAMP_STEPS) {
|
|
468
|
+
const t = lightView.typography?.ramp?.[step];
|
|
469
|
+
if (!isToken(t) || t.$type !== "typography") { errors.push(`rule 5 — typography.ramp.${step}: missing or not $type typography`); continue; }
|
|
470
|
+
for (const sub of ["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"]) {
|
|
471
|
+
if (t.$value?.[sub] === undefined) errors.push(`rule 5 — typography.ramp.${step}.${sub}: missing sub-value`);
|
|
270
472
|
}
|
|
271
473
|
}
|
|
272
474
|
|
|
273
|
-
// Rule 6 —
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const ranks = shadow.map((s) => SHADOW_RANK[String(s).trim()]);
|
|
286
|
-
if (ranks.every((r) => r !== undefined)) {
|
|
287
|
-
for (let i = 1; i < ranks.length; i++) {
|
|
288
|
-
if (!(ranks[i] > ranks[i - 1])) { errors.push(`shadow-levels: must be ordered by increasing intensity (found ${shadow.join(", ")})`); break; }
|
|
475
|
+
// Rule 6 — alias integrity over every resolved context view (aliases resolve after overlay).
|
|
476
|
+
const SUB_TYPES = { fontFamily: "fontFamily", fontWeight: "fontWeight", letterSpacing: "number", fontSize: "dimension", lineHeight: "number" };
|
|
477
|
+
for (const [context, view] of views) {
|
|
478
|
+
const tokens = collectTokens(view);
|
|
479
|
+
const label = context === "light" ? "" : ` [${context}]`;
|
|
480
|
+
for (const [path, token] of tokens) {
|
|
481
|
+
if (isAlias(token.$value)) resolveAlias(token.$value, tokens, token.$type, `${path}${label}`, errors);
|
|
482
|
+
if (token.$type === "typography" && token.$value && typeof token.$value === "object") {
|
|
483
|
+
for (const [sub, want] of Object.entries(SUB_TYPES)) {
|
|
484
|
+
const v = token.$value[sub];
|
|
485
|
+
if (isAlias(v)) resolveAlias(v, tokens, want, `${path}.${sub}${label}`, errors);
|
|
486
|
+
}
|
|
289
487
|
}
|
|
290
488
|
}
|
|
291
489
|
}
|
|
292
490
|
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
491
|
+
// Rule 7 — manifest scales.
|
|
492
|
+
if (lm && typeof lm === "object") {
|
|
493
|
+
const ascending = (arr) => arr.every((v, i) => i === 0 || v > arr[i - 1]);
|
|
494
|
+
for (const key of ["spacing-scale", "radius-scale"]) {
|
|
495
|
+
if (!Array.isArray(lm[key]) || lm[key].length === 0 || !ascending(lm[key])) errors.push(`rule 7 — lever-manifest.${key}: must be non-empty ascending (found ${JSON.stringify(lm[key])})`);
|
|
496
|
+
}
|
|
497
|
+
if (!Array.isArray(lm.motion?.durations) || lm.motion.durations.length === 0 || !ascending(lm.motion.durations)) errors.push(`rule 7 — lever-manifest.motion.durations: must be non-empty ascending (found ${JSON.stringify(lm.motion?.durations)})`);
|
|
498
|
+
if (!Array.isArray(lm.motion?.easings) || lm.motion.easings.length === 0) errors.push("rule 7 — lever-manifest.motion.easings: must be non-empty");
|
|
499
|
+
const shadows = lm["shadow-levels"];
|
|
500
|
+
const ranks = (Array.isArray(shadows) ? shadows : []).map((s) => SHADOW_RANK[s]);
|
|
501
|
+
if (ranks.length === 0 || ranks.some((r) => r === undefined) || !ascending(ranks)) {
|
|
502
|
+
errors.push(`rule 7 — lever-manifest.shadow-levels: must be non-empty, drawn from ${Object.keys(SHADOW_RANK).join(" ")}, strictly increasing (found ${JSON.stringify(shadows)})`);
|
|
503
|
+
}
|
|
504
|
+
if (typeof lm.icons?.family !== "string" || lm.icons.family === "") errors.push("rule 7 — lever-manifest.icons.family: missing or empty");
|
|
505
|
+
if (!(lm["accent-count"] >= 1)) errors.push(`rule 7 — lever-manifest.accent-count: must be >= 1 (found ${JSON.stringify(lm?.["accent-count"])})`);
|
|
300
506
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
507
|
+
// Rule 8 — the look's own base values sit on its own allowlist.
|
|
508
|
+
const radius = lightView.primitive?.radius?.$value?.value;
|
|
509
|
+
if (Array.isArray(lm["radius-scale"]) && radius !== undefined && !lm["radius-scale"].includes(radius)) {
|
|
510
|
+
errors.push(`rule 8 — primitive.radius: ${radius} not on lever-manifest.radius-scale`);
|
|
511
|
+
}
|
|
512
|
+
for (const key of ["duration-fast", "duration-normal", "duration-slow"]) {
|
|
513
|
+
const ms = lightView.primitive?.[key]?.$value?.value;
|
|
514
|
+
if (Array.isArray(lm.motion?.durations) && ms !== undefined && !lm.motion.durations.includes(ms)) {
|
|
515
|
+
errors.push(`rule 8 — primitive.${key}: ${ms} not in lever-manifest.motion.durations`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const ease = lightView.primitive?.["ease-default"];
|
|
519
|
+
if (isToken(ease) && Array.isArray(lm.motion?.easings)) {
|
|
520
|
+
const easeCss = typeof ease.$extensions?.[EXT]?.css === "string" ? ease.$extensions[EXT].css : serializeValue("cubicBezier", ease.$value);
|
|
521
|
+
if (!lm.motion.easings.includes(easeCss)) errors.push(`rule 8 — primitive.ease-default: ${JSON.stringify(easeCss)} not in lever-manifest.motion.easings`);
|
|
522
|
+
}
|
|
307
523
|
}
|
|
308
524
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
525
|
+
// Rule 9 — parse-back for every token carrying a preserved .css, per file.
|
|
526
|
+
for (const [fileName, doc] of files) {
|
|
527
|
+
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
528
|
+
for (const [path, token] of collectTokens(doc)) {
|
|
529
|
+
const css = token.$extensions?.[EXT]?.css;
|
|
530
|
+
if (css === undefined) continue;
|
|
531
|
+
const name = `${prefix}${path}`;
|
|
532
|
+
if (token.$type === "color") {
|
|
533
|
+
verifyColorParseBack(name, token, errors);
|
|
534
|
+
} else if (typeof css === "string") {
|
|
535
|
+
verifyScalarParseBack(name, token, css, errors);
|
|
536
|
+
} else if (token.$type === "typography" && typeof css === "object" && css !== null) {
|
|
537
|
+
if (typeof css.letterSpacing === "string" && typeof token.$value?.letterSpacing === "number" && typeof token.$value?.fontSize?.value === "number") {
|
|
538
|
+
try {
|
|
539
|
+
const reparsed = parseLetterSpacingCss(css.letterSpacing, token.$value.fontSize.value);
|
|
540
|
+
if (reparsed !== token.$value.letterSpacing) errors.push(`rule 9 — ${name}: letterSpacing css ${css.letterSpacing} → ${reparsed} != ${token.$value.letterSpacing}`);
|
|
541
|
+
} catch (e) {
|
|
542
|
+
errors.push(`rule 9 — ${name}: ${e.message}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
315
547
|
}
|
|
316
548
|
|
|
317
|
-
|
|
318
|
-
|
|
549
|
+
// Rule 10 — the dark file, when present, carries exactly the 34-role color group and nothing else.
|
|
550
|
+
const darkDoc = files.get("design.tokens.dark.json");
|
|
551
|
+
if (darkDoc !== undefined) {
|
|
552
|
+
const rootKeys = Object.keys(darkDoc).filter((k) => !k.startsWith("$"));
|
|
553
|
+
if (rootKeys.length !== 1 || rootKeys[0] !== "color") errors.push(`rule 10 — design.tokens.dark.json: root groups ${JSON.stringify(rootKeys)} (want exactly ["color"])`);
|
|
554
|
+
if (darkDoc.$extensions?.[EXT] !== undefined) errors.push("rule 10 — design.tokens.dark.json: carries a payload (the base file is the payload's single carrier)");
|
|
555
|
+
for (const role of ALL_TOKENS) {
|
|
556
|
+
const t = darkDoc.color?.[role];
|
|
557
|
+
if (!isToken(t)) { errors.push(`rule 10 — dark color.${role}: missing`); continue; }
|
|
558
|
+
if (t.$type !== "color") { errors.push(`rule 10 — dark color.${role}: $type ${JSON.stringify(t.$type)} (want color)`); continue; }
|
|
559
|
+
if (isAlias(t.$value)) continue; // resolves post-overlay; rules 4/6 cover it on the dark view
|
|
560
|
+
if (!["oklch", "srgb"].includes(t.$value?.colorSpace)) errors.push(`rule 10 — dark color.${role}: colorSpace ${JSON.stringify(t.$value?.colorSpace)} (want oklch|srgb)`);
|
|
561
|
+
if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`rule 10 — dark color.${role}: missing preserved .css`);
|
|
562
|
+
}
|
|
563
|
+
for (const role of Object.keys(darkDoc.color ?? {})) {
|
|
564
|
+
if (!ALL_TOKENS.includes(role)) errors.push(`rule 10 — dark color.${role}: not a legal role`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
319
567
|
|
|
320
|
-
|
|
321
|
-
|
|
568
|
+
// Rule 11 — only the eight legal $type values appear, in any file.
|
|
569
|
+
for (const [fileName, doc] of files) {
|
|
570
|
+
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
571
|
+
for (const [path, token] of collectTokens(doc)) {
|
|
572
|
+
if (!LEGAL_TYPES.includes(token.$type)) errors.push(`rule 11 — ${prefix}${path}: illegal $type ${JSON.stringify(token.$type)}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
322
575
|
|
|
323
|
-
//
|
|
324
|
-
function collectBareHints(doc) {
|
|
576
|
+
// Rule 12 inventory — every token still carrying a hint (passes --check; --emit refuses).
|
|
325
577
|
const hints = [];
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
for (const
|
|
329
|
-
|
|
578
|
+
for (const [fileName, doc] of files) {
|
|
579
|
+
const prefix = fileName === "design.tokens.json" ? "" : `${fileName} `;
|
|
580
|
+
for (const [path, token] of collectTokens(doc)) {
|
|
581
|
+
const hint = token.$extensions?.[EXT]?.hint;
|
|
582
|
+
if (hint !== undefined) hints.push({ key: `${prefix}${path}`, text: String(hint) });
|
|
330
583
|
}
|
|
331
584
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
585
|
+
|
|
586
|
+
return { errors, payload, views, hints, hasDark: files.has("design.tokens.dark.json") };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// Emitter feed — flatten the resolved views into the value map the emitters consume
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
// Assemble the emitters' document: 26 primitive CSS strings + the payload dials, and the
|
|
594
|
+
// 34-role CSS string map per theme (preferring each token's preserved .css).
|
|
595
|
+
function buildEmitterDoc(views, payload, errors) {
|
|
596
|
+
const lightTokens = collectTokens(views.get("light"));
|
|
597
|
+
const primitives = {};
|
|
598
|
+
for (const key of PRIMITIVES) primitives[key] = cssFor(`primitive.${key}`, lightTokens, errors);
|
|
599
|
+
primitives.verbosity = payload.verbosity;
|
|
600
|
+
primitives.feedback = payload.feedback;
|
|
601
|
+
primitives["density-tier"] = payload["density-tier"];
|
|
602
|
+
|
|
603
|
+
const colorTokens = { light: {} };
|
|
604
|
+
for (const role of ALL_TOKENS) colorTokens.light[role] = cssFor(`color.${role}`, lightTokens, errors);
|
|
605
|
+
|
|
606
|
+
if (views.has("dark")) {
|
|
607
|
+
const darkTokens = collectTokens(views.get("dark"));
|
|
608
|
+
colorTokens.dark = {};
|
|
609
|
+
for (const role of ALL_TOKENS) colorTokens.dark[role] = cssFor(`color.${role}`, darkTokens, errors);
|
|
337
610
|
}
|
|
338
|
-
|
|
611
|
+
|
|
612
|
+
return { primitives, manifest: { "color-tokens": colorTokens } };
|
|
339
613
|
}
|
|
340
614
|
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// Emitters (values come from the token artifact; non-comment output is v1 byte-identical)
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
|
|
341
619
|
// Derive a rosterless data-style slug from the product name.
|
|
342
620
|
function slugify(name) {
|
|
343
621
|
return String(name || "project").toLowerCase().trim()
|
|
@@ -388,28 +666,48 @@ function buildStyleBlockLines(primitives) {
|
|
|
388
666
|
return lines.join("\n");
|
|
389
667
|
}
|
|
390
668
|
|
|
391
|
-
// Emit the full src/styles/personality.css content for a passing
|
|
392
|
-
function emitPersonalityCss(doc, slug) {
|
|
669
|
+
// Emit the full src/styles/personality.css content for a passing artifact.
|
|
670
|
+
function emitPersonalityCss(doc, slug, meta) {
|
|
393
671
|
const p = doc.primitives;
|
|
394
672
|
const light = doc.manifest["color-tokens"].light;
|
|
395
673
|
const density = p["density-tier"];
|
|
396
674
|
const verbosity = p.verbosity;
|
|
397
675
|
const feedback = p.feedback;
|
|
398
676
|
|
|
677
|
+
const dark = doc.manifest["color-tokens"].dark;
|
|
678
|
+
|
|
399
679
|
const semanticLines = SEMANTIC_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
|
|
400
680
|
const substrateLines = SUBSTRATE_TOKENS.map((t) => ` --${t}: ${light[t]};`).join("\n");
|
|
401
681
|
const styleLines = buildStyleBlockLines(p);
|
|
402
682
|
|
|
683
|
+
const darkSection = dark
|
|
684
|
+
? `/* ── Dark theme — OPT-IN ONLY (this look ships a dark overlay). ──
|
|
685
|
+
* design.tokens.dark.json remaps all 34 color tokens; <html data-theme="dark">
|
|
686
|
+
* opts in. Light stays the default. */
|
|
687
|
+
[data-theme="dark"] {
|
|
688
|
+
/* semantic (17) */
|
|
689
|
+
${SEMANTIC_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
|
|
690
|
+
/* substrate (17, the D6 set) — popover/secondary/chart/sidebar the library consumes */
|
|
691
|
+
${SUBSTRATE_TOKENS.map((t) => ` --${t}: ${dark[t]};`).join("\n")}
|
|
692
|
+
}`
|
|
693
|
+
: `/* ── Dark theme — OPT-IN ONLY. ──
|
|
694
|
+
* Emitted only when the artifact ships a design.tokens.dark.json dark overlay.
|
|
695
|
+
* Light stays the default; this block never appears for a light-only project.
|
|
696
|
+
[data-theme="dark"] {
|
|
697
|
+
--background: ...; --foreground: ...; ... all 34 tokens remapped ...
|
|
698
|
+
} */`;
|
|
699
|
+
|
|
403
700
|
return `/* src/styles/personality.css — operator-UI personality tokens (emitted; do not hand-edit).
|
|
404
701
|
*
|
|
405
|
-
* Active personality — code is the cache.
|
|
406
|
-
*
|
|
407
|
-
*
|
|
702
|
+
* Active personality — code is the cache. These values and the <html data-*>
|
|
703
|
+
* attributes in index.html are emitted from the design-tokens artifact. To change
|
|
704
|
+
* the look, edit design.tokens.json and re-run derive-personality.mjs --emit.
|
|
408
705
|
* style: ${slug} (<html data-style="${slug}">) — the project's own personality (rosterless)
|
|
409
706
|
* density: ${density} (<html data-density="${density}">)
|
|
410
707
|
* verbosity: ${verbosity} (<html data-verbosity="${verbosity}">)
|
|
411
708
|
* feedback: ${feedback} (<html data-feedback="${feedback}">)
|
|
412
|
-
* theme:
|
|
709
|
+
* theme: ${meta.theme} (<html data-theme="${meta.theme}">) — light is the default; dark is opt-in
|
|
710
|
+
* artifact: design.tokens.json (resolved via design.resolver.json) — provenance: ${meta.provenance}, sourced ${meta.sourced}
|
|
413
711
|
*
|
|
414
712
|
* Semantic-token doctrine: components name purpose (bg-card, text-foreground, border-border);
|
|
415
713
|
* this file resolves those names to values. Components never use dark: prefixes or literal colors.
|
|
@@ -480,8 +778,8 @@ ${substrateLines}
|
|
|
480
778
|
}
|
|
481
779
|
|
|
482
780
|
/* ── The project personality: the single [data-style] block (D3, rosterless). ──
|
|
483
|
-
* Named after the project — not a preset. This materializes the
|
|
484
|
-
*
|
|
781
|
+
* Named after the project — not a preset. This materializes the artifact's
|
|
782
|
+
* primitive token group: all 26 primitives + the derived temporal. It is the
|
|
485
783
|
* one block improve/extract rewrites. density-gap/inset are governed refs, so
|
|
486
784
|
* the global tier and per-surface step still compose. */
|
|
487
785
|
[data-style="${slug}"] {
|
|
@@ -508,16 +806,11 @@ ${styleLines}
|
|
|
508
806
|
* A surface element carries data-density-step; it re-derives one step off the
|
|
509
807
|
* inherited base. Reads --density-base-* (never itself) so it composes with the
|
|
510
808
|
* tier and never absolutely overrides it. The recomputed --density-gap flows into
|
|
511
|
-
* the library's p
|
|
809
|
+
* the library's p-* and gap-* through the @theme --spacing remap. */
|
|
512
810
|
[data-density-step="tighter"] { --density-gap: calc(var(--density-base-gap) * 0.85); --density-inset: calc(var(--density-base-inset) * 0.85); }
|
|
513
811
|
[data-density-step="looser"] { --density-gap: calc(var(--density-base-gap) * 1.15); --density-inset: calc(var(--density-base-inset) * 1.15); }
|
|
514
812
|
|
|
515
|
-
|
|
516
|
-
* Emitted only when the design.md ships a lever-manifest color-tokens.dark map.
|
|
517
|
-
* Light stays the default; this block never appears for a light-only project.
|
|
518
|
-
[data-theme="dark"] {
|
|
519
|
-
--background: ...; --foreground: ...; ... all 34 tokens remapped ...
|
|
520
|
-
} */
|
|
813
|
+
${darkSection}
|
|
521
814
|
|
|
522
815
|
/* ── Accessibility ── */
|
|
523
816
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -548,7 +841,7 @@ function emitGlobalsCss(doc) {
|
|
|
548
841
|
/* CASCADE CONTRACT 2: this four-step order is LOAD-BEARING. The library's
|
|
549
842
|
* styles.css ships its own :root token defaults and its own @theme radius scale;
|
|
550
843
|
* our @theme MUST come AFTER the import to win the radius scale and own every
|
|
551
|
-
* mapping. rules/
|
|
844
|
+
* mapping. rules/frontend.md states the same order — co-authority. */
|
|
552
845
|
@import "./personality.css"; /* 1. token source (must be first) */
|
|
553
846
|
@import "tailwindcss"; /* 2. framework */
|
|
554
847
|
@import "@nurix/components/styles.css"; /* 3. library (before our @theme) */
|
|
@@ -561,9 +854,9 @@ function emitGlobalsCss(doc) {
|
|
|
561
854
|
--radius-lg: var(--radius);
|
|
562
855
|
--radius-xl: calc(var(--radius) + 4px);
|
|
563
856
|
|
|
564
|
-
/* density remap — the single line that scales ALL library spacing. Every p
|
|
565
|
-
* is calc(var(--spacing)
|
|
566
|
-
* AND the per-surface density step into the library's internals. */
|
|
857
|
+
/* density remap — the single line that scales ALL library spacing. Every p-* and
|
|
858
|
+
* gap-* is calc(var(--spacing) x N); driving --spacing off --density-gap flows the
|
|
859
|
+
* tier AND the per-surface density step into the library's internals. */
|
|
567
860
|
--spacing: calc(0.25rem * var(--density-gap));
|
|
568
861
|
|
|
569
862
|
/* shadow — alpha rides --shadow-intensity (matches the library's shadow-xs/sm/md/lg use) */
|
|
@@ -611,18 +904,18 @@ ${colorMaps}
|
|
|
611
904
|
}
|
|
612
905
|
|
|
613
906
|
// Emit the index.html <html> attribute snippet the shell must carry.
|
|
614
|
-
function emitIndexHtmlAttributes(doc, slug) {
|
|
907
|
+
function emitIndexHtmlAttributes(doc, slug, meta) {
|
|
615
908
|
const p = doc.primitives;
|
|
616
909
|
return `<!-- Personality state lives in these <html data-*> attributes — code is the cache.
|
|
617
|
-
|
|
618
|
-
re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
|
|
910
|
+
The design-tokens artifact is the source. To change the look, edit design.tokens.json
|
|
911
|
+
and re-run derive-personality.mjs --emit; to nudge one axis, edit an attribute here. -->
|
|
619
912
|
<html
|
|
620
913
|
lang="en"
|
|
621
914
|
data-style="${slug}"
|
|
622
915
|
data-density="${p["density-tier"]}"
|
|
623
916
|
data-verbosity="${p.verbosity}"
|
|
624
917
|
data-feedback="${p.feedback}"
|
|
625
|
-
data-theme="
|
|
918
|
+
data-theme="${meta.theme}"
|
|
626
919
|
>
|
|
627
920
|
`;
|
|
628
921
|
}
|
|
@@ -633,64 +926,71 @@ function writeFileEnsured(path, content) {
|
|
|
633
926
|
writeFileSync(path, content);
|
|
634
927
|
}
|
|
635
928
|
|
|
929
|
+
// ---------------------------------------------------------------------------
|
|
930
|
+
// CLI
|
|
931
|
+
// ---------------------------------------------------------------------------
|
|
932
|
+
|
|
636
933
|
// Print usage and exit non-zero.
|
|
637
934
|
function usage() {
|
|
638
|
-
process.stderr.write("usage: derive-personality.mjs <
|
|
935
|
+
process.stderr.write("usage: derive-personality.mjs <artifact-dir> [--check | --emit <outdir>] [--theme light|dark]\n");
|
|
639
936
|
process.exit(2);
|
|
640
937
|
}
|
|
641
938
|
|
|
642
|
-
// Entry point: parse argv, run --check or --emit.
|
|
939
|
+
// Entry point: parse argv, run --check or --emit over the artifact directory.
|
|
643
940
|
function main() {
|
|
644
941
|
const argv = process.argv.slice(2);
|
|
645
|
-
let
|
|
942
|
+
let artifactDir = null;
|
|
646
943
|
let mode = "check";
|
|
647
944
|
let outdir = null;
|
|
945
|
+
let theme = "light";
|
|
648
946
|
for (let i = 0; i < argv.length; i++) {
|
|
649
947
|
const a = argv[i];
|
|
650
948
|
if (a === "--check") mode = "check";
|
|
651
949
|
else if (a === "--emit") { mode = "emit"; outdir = argv[++i] ?? null; }
|
|
652
|
-
else if (
|
|
950
|
+
else if (a === "--theme") { theme = argv[++i] ?? null; }
|
|
951
|
+
else if (!a.startsWith("-") && artifactDir === null) artifactDir = a;
|
|
653
952
|
else usage();
|
|
654
953
|
}
|
|
655
|
-
if (!
|
|
954
|
+
if (!artifactDir) usage();
|
|
656
955
|
if (mode === "emit" && !outdir) usage();
|
|
956
|
+
if (!LEGAL_THEMES.includes(theme)) usage();
|
|
657
957
|
|
|
658
|
-
|
|
659
|
-
try {
|
|
660
|
-
source = readFileSync(designPath, "utf8");
|
|
661
|
-
} catch {
|
|
662
|
-
process.stderr.write(`error: cannot read ${designPath}\n`);
|
|
663
|
-
process.exit(2);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const doc = parseDesignMd(source);
|
|
667
|
-
const { errors } = runCoverageGate(doc);
|
|
668
|
-
const slug = slugify(doc.frontmatter && doc.frontmatter.product);
|
|
958
|
+
const { errors, payload, views, hints, hasDark } = runCheck(artifactDir, theme);
|
|
669
959
|
|
|
670
960
|
if (errors.length > 0) {
|
|
671
|
-
process.stderr.write(`
|
|
961
|
+
process.stderr.write(`check FAILED (${artifactDir}): ${errors.length} issue(s)\n`);
|
|
672
962
|
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
673
963
|
process.exit(1);
|
|
674
964
|
}
|
|
675
965
|
|
|
966
|
+
const slug = slugify(payload.product);
|
|
967
|
+
|
|
676
968
|
if (mode === "check") {
|
|
677
|
-
const
|
|
678
|
-
process.stdout.write(`
|
|
969
|
+
const hintNote = hints.length > 0 ? `, ${hints.length} hint(s) outstanding (emit will refuse)` : "";
|
|
970
|
+
process.stdout.write(`check PASSED (${artifactDir}): rules 1–11 OK — 26 primitives, 34 color roles, typography 3+6, payload/manifest/parse-back OK — provenance=${payload.provenance}, style=${slug}, theme=${theme}${hasDark ? ", dark overlay present" : ""}${hintNote}\n`);
|
|
679
971
|
process.exit(0);
|
|
680
972
|
}
|
|
681
973
|
|
|
682
|
-
//
|
|
683
|
-
const hints = collectBareHints(doc);
|
|
974
|
+
// Rule 12 — emission precondition: refuse while any token carries a hint.
|
|
684
975
|
if (hints.length > 0) {
|
|
685
|
-
process.stderr.write(`emit REFUSED (${
|
|
976
|
+
process.stderr.write(`emit REFUSED (${artifactDir}): ${hints.length} unresolved hint(s) — rule 12: every gated token must be concrete before emission.\n`);
|
|
686
977
|
for (const h of hints) process.stderr.write(` - ${h.key}: hint: ${h.text}\n`);
|
|
687
|
-
process.stderr.write(" Resolve with judgment
|
|
978
|
+
process.stderr.write(" Resolve each with judgment: set the real $value, delete the hint key, record the rationale in $description — then re-run --emit.\n");
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const emitErrors = [];
|
|
983
|
+
const doc = buildEmitterDoc(views, payload, emitErrors);
|
|
984
|
+
if (emitErrors.length > 0) {
|
|
985
|
+
process.stderr.write(`emit FAILED (${artifactDir}): ${emitErrors.length} issue(s)\n`);
|
|
986
|
+
for (const e of emitErrors) process.stderr.write(` - ${e}\n`);
|
|
688
987
|
process.exit(1);
|
|
689
988
|
}
|
|
690
989
|
|
|
691
|
-
const
|
|
990
|
+
const meta = { provenance: payload.provenance, sourced: payload.sourced, theme };
|
|
991
|
+
const personalityCss = emitPersonalityCss(doc, slug, meta);
|
|
692
992
|
const globalsCss = emitGlobalsCss(doc);
|
|
693
|
-
const htmlAttrs = emitIndexHtmlAttributes(doc, slug);
|
|
993
|
+
const htmlAttrs = emitIndexHtmlAttributes(doc, slug, meta);
|
|
694
994
|
const personalityPath = join(outdir, "src/styles/personality.css");
|
|
695
995
|
const globalsPath = join(outdir, "src/styles/globals.css");
|
|
696
996
|
const attrsPath = join(outdir, "index-html-attributes.txt");
|