@nurix/etna 0.18.20 → 0.18.21
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 +1 -1
- package/data/harnesses/code/agents/brainstorm.md +1 -1
- package/data/harnesses/code/agents/maintenance.md +2 -2
- 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 +1 -1
- package/data/harnesses/code/skills/brainstorm/SKILL.md +1 -1
- package/data/harnesses/code/skills/brainstorm/exit-artifacts.md +4 -4
- package/data/harnesses/code/skills/brainstorm/templates/source-of-truth.md +3 -3
- 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 +305 -51
- package/data/harnesses/etna/CLAUDE.md +1 -1
- package/data/harnesses/etna/agents/brainstorm.md +1 -1
- package/data/harnesses/etna/agents/maintenance.md +2 -2
- 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 +1 -1
- package/data/harnesses/etna/skills/brainstorm/SKILL.md +1 -1
- package/data/harnesses/etna/skills/brainstorm/exit-artifacts.md +4 -4
- package/data/harnesses/etna/skills/brainstorm/templates/source-of-truth.md +3 -3
- 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 +305 -51
- package/data/harnesses/mobile/CLAUDE.md +1 -1
- package/data/harnesses/mobile/agents/brainstorm.md +1 -1
- package/data/harnesses/mobile/agents/maintenance.md +2 -2
- 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 +1 -1
- package/data/harnesses/mobile/skills/brainstorm/SKILL.md +1 -1
- package/data/harnesses/mobile/skills/brainstorm/exit-artifacts.md +4 -4
- package/data/harnesses/mobile/skills/brainstorm/templates/source-of-truth.md +3 -3
- 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 +305 -51
- package/package.json +1 -1
- package/data/harnesses/code/skills/design/scripts/hook-config.json +0 -11
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// skills/design/scripts/convert-design-md.mjs — mechanical design.md → design-tokens converter (schema design-tokens/v1, 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, self-validates (schema rules 2–10), and offers --strip to remove the machine blocks (and their orphaned prose references) from source files once the byte-match gate passes.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node convert-design-md.mjs convert every entry (nurix.md + library/*.md), then validate
|
|
6
|
+
// node convert-design-md.mjs --only <slug> convert/validate a single entry
|
|
7
|
+
// node convert-design-md.mjs --validate validate existing artifacts only (no conversion)
|
|
8
|
+
// node convert-design-md.mjs --report <file> also write the JSON conversion report to <file>
|
|
9
|
+
// node convert-design-md.mjs --strip <file...> remove both machine blocks + orphaned prose from the named
|
|
10
|
+
// .md files IN PLACE (explicit paths required — never implicit)
|
|
11
|
+
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
13
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const skillDir = dirname(scriptsDir);
|
|
18
|
+
const designMdDir = join(skillDir, "data", "design-md");
|
|
19
|
+
const tokensDir = join(skillDir, "data", "design-tokens");
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Vocabulary (names are law — mirrors data/design-tokens/SCHEMA.md)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
// The 26 visual primitives in emission order, with their DTCG $type.
|
|
26
|
+
const PRIMITIVE_TYPES = new Map([
|
|
27
|
+
["radius", "dimension"], ["shadow-intensity", "number"], ["border-weight", "dimension"],
|
|
28
|
+
["density-gap", "number"], ["density-inset", "number"],
|
|
29
|
+
["font-sans", "fontFamily"], ["font-mono", "fontFamily"],
|
|
30
|
+
["font-weight-normal", "fontWeight"], ["font-weight-strong", "fontWeight"],
|
|
31
|
+
["heading-tracking", "number"],
|
|
32
|
+
["duration-unit", "duration"], ["duration-fast", "duration"], ["duration-normal", "duration"],
|
|
33
|
+
["duration-slow", "duration"], ["ease-default", "cubicBezier"],
|
|
34
|
+
["contrast-boost", "number"],
|
|
35
|
+
["icon-stroke", "number"], ["icon-size-base", "dimension"],
|
|
36
|
+
["sidebar-width-expanded", "dimension"], ["sidebar-width-collapsed", "dimension"],
|
|
37
|
+
["topbar-height", "dimension"], ["side-panel-width", "dimension"],
|
|
38
|
+
["side-panel-width-rich", "dimension"], ["modal-width-max", "dimension"],
|
|
39
|
+
["form-max-width", "dimension"], ["table-row-height", "dimension"],
|
|
40
|
+
]);
|
|
41
|
+
const PRIMITIVES = [...PRIMITIVE_TYPES.keys()];
|
|
42
|
+
|
|
43
|
+
// The 34 color roles in emission order (17 semantic + 17 substrate).
|
|
44
|
+
const COLOR_ROLES = [
|
|
45
|
+
"background", "foreground", "card", "card-foreground", "muted", "muted-foreground",
|
|
46
|
+
"border", "input", "ring", "primary", "primary-foreground", "accent", "accent-foreground",
|
|
47
|
+
"destructive", "destructive-foreground", "success", "warning",
|
|
48
|
+
"popover", "popover-foreground", "secondary", "secondary-foreground",
|
|
49
|
+
"chart-1", "chart-2", "chart-3", "chart-4", "chart-5",
|
|
50
|
+
"sidebar", "sidebar-foreground", "sidebar-primary", "sidebar-primary-foreground",
|
|
51
|
+
"sidebar-accent", "sidebar-accent-foreground", "sidebar-border", "sidebar-ring",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const RAMP_STEPS = ["h1", "h2", "h3", "body", "small", "caption"];
|
|
55
|
+
// Source §3 row names that fill the caption slot when no literal `caption` row exists.
|
|
56
|
+
const CAPTION_ALIASES = ["caption", "eyebrow / caption", "eyebrow", "label", "mono-label"];
|
|
57
|
+
|
|
58
|
+
const LEGAL_VERBOSITY = ["full", "default", "compact", "expert"];
|
|
59
|
+
const LEGAL_FEEDBACK = ["subtle", "minimal"];
|
|
60
|
+
const LEGAL_DENSITY = ["spacious", "standard", "dense"];
|
|
61
|
+
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 };
|
|
64
|
+
const EXT = "com.nurix.design";
|
|
65
|
+
|
|
66
|
+
// Provisional values worn when a source key is a `hint:` (the Nurix defaults, per SCHEMA.md examples).
|
|
67
|
+
const HINT_PROVISIONALS = new Map([
|
|
68
|
+
["radius", "6px"], ["shadow-intensity", "0.4"], ["border-weight", "1px"], ["density-gap", "1"],
|
|
69
|
+
["density-inset", "1"], ["font-sans", "Inter, system-ui, sans-serif"],
|
|
70
|
+
["font-mono", "Geist Mono, ui-monospace, SFMono-Regular, monospace"],
|
|
71
|
+
["font-weight-normal", "400"], ["font-weight-strong", "600"], ["heading-tracking", "-0.01em"],
|
|
72
|
+
["duration-unit", "50ms"], ["duration-fast", "100ms"], ["duration-normal", "150ms"],
|
|
73
|
+
["duration-slow", "250ms"], ["ease-default", "cubic-bezier(0.4,0,0.2,1)"], ["contrast-boost", "1"],
|
|
74
|
+
["icon-stroke", "1.75"], ["icon-size-base", "16px"], ["sidebar-width-expanded", "248px"],
|
|
75
|
+
["sidebar-width-collapsed", "56px"], ["topbar-height", "52px"], ["side-panel-width", "420px"],
|
|
76
|
+
["side-panel-width-rich", "640px"], ["modal-width-max", "560px"], ["form-max-width", "640px"],
|
|
77
|
+
["table-row-height", "40px"],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Low-level text helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
// Split a raw line at its first unquoted, whitespace-preceded '#': { body, comment|null }.
|
|
85
|
+
function splitLineComment(line) {
|
|
86
|
+
let inDouble = false, inSingle = false;
|
|
87
|
+
for (let i = 0; i < line.length; i++) {
|
|
88
|
+
const c = line[i];
|
|
89
|
+
if (c === '"' && !inSingle && line[i - 1] !== "\\") inDouble = !inDouble;
|
|
90
|
+
else if (c === "'" && !inDouble) inSingle = !inSingle;
|
|
91
|
+
else if (c === "#" && !inDouble && !inSingle && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
92
|
+
return { body: line.slice(0, i).replace(/\s+$/, ""), comment: line.slice(i + 1).trim() };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { body: line.replace(/\s+$/, ""), comment: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Strip one layer of matching surrounding quotes and unescape \" within double quotes.
|
|
99
|
+
function unquote(raw) {
|
|
100
|
+
const s = raw.trim();
|
|
101
|
+
if (s.length >= 2 && s[0] === '"' && s.at(-1) === '"') return s.slice(1, -1).replaceAll('\\"', '"');
|
|
102
|
+
if (s.length >= 2 && s[0] === "'" && s.at(-1) === "'") return s.slice(1, -1);
|
|
103
|
+
return s;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Split on a delimiter at bracket-depth 0 and outside quotes.
|
|
107
|
+
function splitTopLevel(str, delim) {
|
|
108
|
+
const parts = [];
|
|
109
|
+
let depth = 0, inDouble = false, inSingle = false, cur = "";
|
|
110
|
+
for (let i = 0; i < str.length; i++) {
|
|
111
|
+
const c = str[i];
|
|
112
|
+
if (c === '"' && !inSingle && str[i - 1] !== "\\") { inDouble = !inDouble; cur += c; }
|
|
113
|
+
else if (c === "'" && !inDouble) { inSingle = !inSingle; cur += c; }
|
|
114
|
+
else if (!inDouble && !inSingle && (c === "[" || c === "{" || c === "(")) { depth++; cur += c; }
|
|
115
|
+
else if (!inDouble && !inSingle && (c === "]" || c === "}" || c === ")")) { depth--; cur += c; }
|
|
116
|
+
else if (!inDouble && !inSingle && depth === 0 && c === delim) { parts.push(cur); cur = ""; }
|
|
117
|
+
else cur += c;
|
|
118
|
+
}
|
|
119
|
+
parts.push(cur);
|
|
120
|
+
return parts;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Round to at most 5 decimals (the srgb component precision).
|
|
124
|
+
function round5(n) {
|
|
125
|
+
return Math.round(n * 1e5) / 1e5;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fail(msg) {
|
|
129
|
+
console.error(`convert-design-md: ${msg}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// CSS value parsers (single source of truth — conversion AND parse-back verify use these)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
// Parse any corpus color string (oklch / #rrggbb / #rrggbbaa / rgb() / rgba()) into a typed DTCG color $value.
|
|
138
|
+
function parseColorCss(css) {
|
|
139
|
+
const s = css.trim();
|
|
140
|
+
let m = /^oklch\(\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*(?:\/\s*([\d.]+%?)\s*)?\)$/.exec(s);
|
|
141
|
+
if (m) {
|
|
142
|
+
const value = { colorSpace: "oklch", components: [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])] };
|
|
143
|
+
if (m[4] !== undefined) value.alpha = m[4].endsWith("%") ? round5(parseFloat(m[4]) / 100) : parseFloat(m[4]);
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
m = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
|
|
147
|
+
if (m) {
|
|
148
|
+
const bytes = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
|
|
149
|
+
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex: `#${m[1].toLowerCase()}` };
|
|
150
|
+
if (m[2] !== undefined) value.alpha = round5(parseInt(m[2], 16) / 255);
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(s);
|
|
154
|
+
if (m) {
|
|
155
|
+
const bytes = [m[1], m[2], m[3]].map(Number);
|
|
156
|
+
const hex = "#" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
157
|
+
const value = { colorSpace: "srgb", components: bytes.map((b) => round5(b / 255)), hex };
|
|
158
|
+
if (m[4] !== undefined) value.alpha = parseFloat(m[4]);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`unparseable color: ${JSON.stringify(css)}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parse a tracking string ("0" | "0em" | "-0.015em") into its em-multiplier number.
|
|
165
|
+
function parseTrackingCss(css) {
|
|
166
|
+
const m = /^([+-]?\d*\.?\d+)(em)?$/.exec(css.trim());
|
|
167
|
+
if (!m) throw new Error(`unparseable tracking: ${JSON.stringify(css)}`);
|
|
168
|
+
return parseFloat(m[1]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse "cubic-bezier(x1,y1,x2,y2)" into its four numbers.
|
|
172
|
+
function parseCubicBezierCss(css) {
|
|
173
|
+
const m = /^cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)$/.exec(css.trim());
|
|
174
|
+
if (!m) throw new Error(`unparseable cubic-bezier: ${JSON.stringify(css)}`);
|
|
175
|
+
return [1, 2, 3, 4].map((i) => parseFloat(m[i]));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse a font stack string into its array of unquoted family names.
|
|
179
|
+
function parseFontStackCss(css) {
|
|
180
|
+
return splitTopLevel(css, ",").map((p) => unquote(p)).filter((p) => p !== "");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Parse a ramp letterSpacing string ("+0.02em" | "-0.28px" | "0") into an em multiplier, px scaled by fontSize.
|
|
184
|
+
function parseLetterSpacingCss(css, fontSizePx) {
|
|
185
|
+
const m = /^([+-]?\d*\.?\d+)(em|px)?$/.exec(css.trim());
|
|
186
|
+
if (!m) throw new Error(`unparseable letterSpacing: ${JSON.stringify(css)}`);
|
|
187
|
+
const n = parseFloat(m[1]);
|
|
188
|
+
return m[2] === "px" ? round5(n / fontSizePx) : n;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseDimensionCss(css) {
|
|
192
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(css.trim());
|
|
193
|
+
if (!m) throw new Error(`unparseable dimension: ${JSON.stringify(css)}`);
|
|
194
|
+
return { value: parseFloat(m[1]), unit: "px" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseDurationCss(css) {
|
|
198
|
+
const m = /^(\d*\.?\d+)ms$/.exec(css.trim());
|
|
199
|
+
if (!m) throw new Error(`unparseable duration: ${JSON.stringify(css)}`);
|
|
200
|
+
return { value: parseFloat(m[1]), unit: "ms" };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseNumberCss(css) {
|
|
204
|
+
const n = parseFloat(css.trim());
|
|
205
|
+
if (Number.isNaN(n) || !/^[+-]?\d*\.?\d+$/.test(css.trim())) throw new Error(`unparseable number: ${JSON.stringify(css)}`);
|
|
206
|
+
return n;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Source parsing — frontmatter, the two machine blocks, the §3 ramp table
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function parseFrontmatter(source) {
|
|
214
|
+
const m = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(source);
|
|
215
|
+
if (!m) return null;
|
|
216
|
+
const out = {};
|
|
217
|
+
for (const raw of m[1].split("\n")) {
|
|
218
|
+
const { body } = splitLineComment(raw);
|
|
219
|
+
const ci = body.indexOf(":");
|
|
220
|
+
if (ci > 0) out[body.slice(0, ci).trim()] = unquote(body.slice(ci + 1));
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Extract the raw body of the ```yaml <label> fenced block, or null.
|
|
226
|
+
function extractFencedBlock(source, label) {
|
|
227
|
+
const re = new RegExp("^`{3,}\\s*yaml\\s+" + label + "\\s*$", "m");
|
|
228
|
+
const open = re.exec(source);
|
|
229
|
+
if (!open) return null;
|
|
230
|
+
const start = source.indexOf("\n", open.index) + 1;
|
|
231
|
+
const close = /^`{3,}\s*$/m.exec(source.slice(start));
|
|
232
|
+
if (!close) return null;
|
|
233
|
+
return source.slice(start, start + close.index);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Parse the design-primitives block, keeping raw value strings and per-line comments.
|
|
237
|
+
function parsePrimitivesBlock(text) {
|
|
238
|
+
const entries = new Map();
|
|
239
|
+
const standalone = [];
|
|
240
|
+
for (const raw of text.split("\n")) {
|
|
241
|
+
if (raw.trim() === "") continue;
|
|
242
|
+
const { body, comment } = splitLineComment(raw);
|
|
243
|
+
if (body.trim() === "") { if (comment !== null) standalone.push(comment); continue; }
|
|
244
|
+
const m = /^([a-z-]+):\s*(.*)$/.exec(body.trim());
|
|
245
|
+
if (!m) throw new Error(`unparseable design-primitives line: ${JSON.stringify(raw)}`);
|
|
246
|
+
entries.set(m[1], { raw: m[2].trim(), comment });
|
|
247
|
+
}
|
|
248
|
+
return { entries, standalone };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Parse an inline flow array "[a, b, c]" into raw scalar strings.
|
|
252
|
+
function parseFlowArray(raw) {
|
|
253
|
+
const inner = raw.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
254
|
+
return splitTopLevel(inner, ",").map((p) => unquote(p)).filter((p) => p !== "");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Parse an inline flow object "{ k: v, ... }" into raw scalar/array values keyed by name.
|
|
258
|
+
function parseFlowObject(raw) {
|
|
259
|
+
const inner = raw.trim().replace(/^\{/, "").replace(/\}$/, "");
|
|
260
|
+
const out = {};
|
|
261
|
+
for (const part of splitTopLevel(inner, ",")) {
|
|
262
|
+
if (part.trim() === "") continue;
|
|
263
|
+
const ci = part.indexOf(":");
|
|
264
|
+
const key = part.slice(0, ci).trim();
|
|
265
|
+
const val = part.slice(ci + 1).trim();
|
|
266
|
+
out[key] = val.startsWith("[") ? parseFlowArray(val) : unquote(val);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Parse the lever-manifest block: scalars, the light/dark color maps (with comments), non-token comments.
|
|
272
|
+
function parseManifestBlock(text) {
|
|
273
|
+
const out = { scalars: new Map(), light: new Map(), dark: null, standalone: [] };
|
|
274
|
+
let section = null;
|
|
275
|
+
for (const raw of text.split("\n")) {
|
|
276
|
+
if (raw.trim() === "") continue;
|
|
277
|
+
const { body, comment } = splitLineComment(raw);
|
|
278
|
+
if (body.trim() === "") { if (comment !== null) out.standalone.push({ where: "lever-manifest", comment }); continue; }
|
|
279
|
+
const indent = body.length - body.trimStart().length;
|
|
280
|
+
const t = body.trim();
|
|
281
|
+
if (indent === 0) {
|
|
282
|
+
const m = /^([a-z-]+):\s*(.*)$/.exec(t);
|
|
283
|
+
if (!m) throw new Error(`unparseable lever-manifest line: ${JSON.stringify(raw)}`);
|
|
284
|
+
if (m[1] === "color-tokens") {
|
|
285
|
+
section = "color-tokens";
|
|
286
|
+
if (comment !== null) out.standalone.push({ where: "color-tokens", comment });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
section = null;
|
|
290
|
+
out.scalars.set(m[1], { raw: m[2].trim(), comment });
|
|
291
|
+
} else if (["color-tokens", "light", "dark"].includes(section) && indent === 2 && /^(light|dark):$/.test(t)) {
|
|
292
|
+
section = t.slice(0, -1) === "light" ? "light" : "dark";
|
|
293
|
+
if (section === "dark") out.dark = new Map();
|
|
294
|
+
if (comment !== null) out.standalone.push({ where: `color-tokens.${section}`, comment });
|
|
295
|
+
} else if ((section === "light" || section === "dark") && indent >= 4) {
|
|
296
|
+
const m = /^([a-z0-9-]+):\s*(.+)$/.exec(t);
|
|
297
|
+
if (!m) throw new Error(`unparseable color-token line: ${JSON.stringify(raw)}`);
|
|
298
|
+
(section === "light" ? out.light : out.dark).set(m[1], { raw: unquote(m[2]), comment });
|
|
299
|
+
} else {
|
|
300
|
+
throw new Error(`unexpected lever-manifest structure: ${JSON.stringify(raw)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Extract the §3 typography table as header-mapped rows: { name, role, family, size, weight }.
|
|
307
|
+
function parseRampTable(source) {
|
|
308
|
+
const sec = /^## 3\b.*?\n([\s\S]*?)(?=^## \d|\n```yaml )/m.exec(source);
|
|
309
|
+
if (!sec) return null;
|
|
310
|
+
const lines = sec[1].split("\n").filter((l) => l.trim().startsWith("|"));
|
|
311
|
+
if (lines.length < 3) return null;
|
|
312
|
+
const cells = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
313
|
+
const header = cells(lines[0]).map((h) => h.toLowerCase());
|
|
314
|
+
const col = (name) => header.findIndex((h) => h.includes(name));
|
|
315
|
+
const iStep = col("step"), iRole = col("role"), iFam = col("family"), iSize = col("size"), iWeight = col("weight");
|
|
316
|
+
const rows = [];
|
|
317
|
+
for (const line of lines.slice(2)) {
|
|
318
|
+
const c = cells(line);
|
|
319
|
+
if (c.length < header.length) continue;
|
|
320
|
+
rows.push({
|
|
321
|
+
name: c[iStep], role: c[iRole], family: iFam >= 0 ? c[iFam] : null,
|
|
322
|
+
size: c[iSize], weight: c[iWeight],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return rows;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Conversion — one source entry → the token artifact
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
// Build one primitive token (typed $value, comment → $description, preserved css / hint where the schema requires).
|
|
333
|
+
function buildPrimitiveToken(key, rawValue, comment, notes) {
|
|
334
|
+
const type = PRIMITIVE_TYPES.get(key);
|
|
335
|
+
const token = { $type: type };
|
|
336
|
+
let raw = rawValue;
|
|
337
|
+
let hint = null;
|
|
338
|
+
if (raw.startsWith("hint:")) {
|
|
339
|
+
hint = raw.slice(5).trim();
|
|
340
|
+
raw = HINT_PROVISIONALS.get(key);
|
|
341
|
+
notes.hints.push({ key, hint });
|
|
342
|
+
}
|
|
343
|
+
const ext = {};
|
|
344
|
+
if (type === "dimension") {
|
|
345
|
+
token.$value = parseDimensionCss(raw);
|
|
346
|
+
if (`${token.$value.value}px` !== raw) notes.irregularities.push(`primitive ${key}: non-canonical byte form ${JSON.stringify(raw)}`);
|
|
347
|
+
} else if (type === "duration") {
|
|
348
|
+
token.$value = parseDurationCss(raw);
|
|
349
|
+
if (`${token.$value.value}ms` !== raw) notes.irregularities.push(`primitive ${key}: non-canonical byte form ${JSON.stringify(raw)}`);
|
|
350
|
+
} else if (type === "fontWeight") {
|
|
351
|
+
token.$value = parseNumberCss(raw);
|
|
352
|
+
} else if (type === "cubicBezier") {
|
|
353
|
+
token.$value = parseCubicBezierCss(raw);
|
|
354
|
+
ext.css = raw;
|
|
355
|
+
} else if (type === "fontFamily") {
|
|
356
|
+
token.$value = parseFontStackCss(raw);
|
|
357
|
+
ext.css = raw;
|
|
358
|
+
} else if (key === "heading-tracking") {
|
|
359
|
+
token.$value = parseTrackingCss(raw);
|
|
360
|
+
ext.css = raw;
|
|
361
|
+
} else {
|
|
362
|
+
token.$value = parseNumberCss(raw);
|
|
363
|
+
if (String(token.$value) !== raw) notes.irregularities.push(`primitive ${key}: non-canonical byte form ${JSON.stringify(raw)}`);
|
|
364
|
+
}
|
|
365
|
+
if (comment !== null) { token.$description = comment; notes.migrated++; }
|
|
366
|
+
if (hint !== null) ext.hint = hint;
|
|
367
|
+
if (Object.keys(ext).length > 0) token.$extensions = { [EXT]: ext };
|
|
368
|
+
return token;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Build one color token: typed $value + mandatory preserved css; comment → $description.
|
|
372
|
+
function buildColorToken(css, comment, notes) {
|
|
373
|
+
const token = { $type: "color", $value: parseColorCss(css) };
|
|
374
|
+
if (comment !== null) { token.$description = comment; notes.migrated++; }
|
|
375
|
+
token.$extensions = { [EXT]: { css } };
|
|
376
|
+
return token;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Interpret one §3 weight·tracking cell → { weight, tracking, trackingCss, mono, parenWeights }.
|
|
380
|
+
function parseWeightCell(cell, fontSizePx) {
|
|
381
|
+
const cleaned = cell.replace(/`/g, "");
|
|
382
|
+
const parenWeights = [...cleaned.matchAll(/\((\d{3})\)/g)].map((m) => Number(m[1]));
|
|
383
|
+
const rest = cleaned.replace(/\([^)]*\)/g, " ");
|
|
384
|
+
const tokens = rest.split(/[·,]/).map((t) => t.trim()).filter((t) => t !== "");
|
|
385
|
+
let weight = null, tracking = null, trackingCss = null, mono = false;
|
|
386
|
+
for (const tok of tokens) {
|
|
387
|
+
if (/^font-weight-strong$/.test(tok)) weight = "{primitive.font-weight-strong}";
|
|
388
|
+
else if (/^font-weight-normal$/.test(tok)) weight = "{primitive.font-weight-normal}";
|
|
389
|
+
else if (/^heading-tracking$/.test(tok)) tracking = "{primitive.heading-tracking}";
|
|
390
|
+
else if (/^[+-]?\d*\.?\d+(em|px)$/.test(tok)) { tracking = parseLetterSpacingCss(tok, fontSizePx); trackingCss = tok; }
|
|
391
|
+
else if (/^\d{3}$/.test(tok) && weight === null) weight = Number(tok);
|
|
392
|
+
else if (tok === "0") tracking = 0;
|
|
393
|
+
else if (/\bfont-mono\b|^mono\b/.test(tok)) mono = true;
|
|
394
|
+
// remaining words (uppercase, sentence case, no tracking, positive, …) are usage prose — the .md keeps them
|
|
395
|
+
}
|
|
396
|
+
return { weight, tracking, trackingCss, mono, parenWeights };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Build typography.family.* + the six-step typography.ramp.* from the §3 table rows.
|
|
400
|
+
function buildTypography(rows, primitives, notes) {
|
|
401
|
+
const family = {
|
|
402
|
+
heading: { $type: "fontFamily", $value: "{primitive.font-sans}" },
|
|
403
|
+
body: { $type: "fontFamily", $value: "{primitive.font-sans}" },
|
|
404
|
+
mono: { $type: "fontFamily", $value: "{primitive.font-mono}" },
|
|
405
|
+
};
|
|
406
|
+
if (rows === null) throw new Error("no §3 typography table found");
|
|
407
|
+
|
|
408
|
+
// Map source rows onto the six fixed step names; leftovers are reported, never emitted.
|
|
409
|
+
const bySlot = new Map();
|
|
410
|
+
const leftovers = [];
|
|
411
|
+
for (const row of rows) {
|
|
412
|
+
const name = row.name.toLowerCase();
|
|
413
|
+
if (RAMP_STEPS.includes(name) && !bySlot.has(name)) bySlot.set(name, row);
|
|
414
|
+
else leftovers.push(row);
|
|
415
|
+
}
|
|
416
|
+
if (!bySlot.has("caption")) {
|
|
417
|
+
for (const alias of CAPTION_ALIASES) {
|
|
418
|
+
const i = leftovers.findIndex((r) => r.name.toLowerCase() === alias);
|
|
419
|
+
if (i >= 0) {
|
|
420
|
+
const row = leftovers.splice(i, 1)[0];
|
|
421
|
+
bySlot.set("caption", row);
|
|
422
|
+
notes.ramp.push(`caption ← source step \`${row.name}\``);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const row of leftovers) notes.ramp.push(`unmapped §3 row \`${row.name}\` (${row.role}) — no ramp slot`);
|
|
428
|
+
|
|
429
|
+
// The look pairs a serif display face the machine blocks don't carry — note it, alias stays font-sans.
|
|
430
|
+
const serifRows = [...bySlot.values()].filter((r) => r.family && /serif/i.test(r.family) && !/sans/i.test(r.family));
|
|
431
|
+
if (serifRows.length > 0) {
|
|
432
|
+
family.heading.$description =
|
|
433
|
+
"source §3 pairs a serif display family for " + serifRows.map((r) => r.name).join("/") +
|
|
434
|
+
"; its stack is prose-carried (the agent applies it by hand) — aliased to font-sans mechanically";
|
|
435
|
+
notes.ramp.push(`serif heading family noted on typography.family.heading (rows: ${serifRows.map((r) => r.name).join(", ")})`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const ramp = {};
|
|
439
|
+
for (const step of RAMP_STEPS) {
|
|
440
|
+
let row = bySlot.get(step);
|
|
441
|
+
let synthesized = null;
|
|
442
|
+
if (!row) {
|
|
443
|
+
row = bySlot.get("body");
|
|
444
|
+
if (!row) throw new Error(`§3 table lacks both \`${step}\` and \`body\` rows`);
|
|
445
|
+
synthesized = `synthesized: source §3 ramp carries no \`${step}\` step; values copied from body (role: ${row.role})`;
|
|
446
|
+
notes.ramp.push(`${step} synthesized from body (missing in source §3 table)`);
|
|
447
|
+
}
|
|
448
|
+
const sm = /(\d+(?:\.\d+)?)px\s*\/\s*(\d+(?:\.\d+)?)/.exec(row.size);
|
|
449
|
+
if (!sm) throw new Error(`unparseable §3 size cell for ${step}: ${JSON.stringify(row.size)}`);
|
|
450
|
+
const fontSize = parseFloat(sm[1]);
|
|
451
|
+
const cell = parseWeightCell(row.weight, fontSize);
|
|
452
|
+
|
|
453
|
+
// A parenthetical weight that contradicts the aliased primitive is a source irregularity.
|
|
454
|
+
for (const pw of cell.parenWeights) {
|
|
455
|
+
const aliasKey = cell.weight === "{primitive.font-weight-strong}" ? "font-weight-strong"
|
|
456
|
+
: cell.weight === "{primitive.font-weight-normal}" ? "font-weight-normal" : null;
|
|
457
|
+
if (aliasKey && parseNumberCss(primitives.get(aliasKey).raw) !== pw) {
|
|
458
|
+
notes.irregularities.push(`§3 ${step}: parenthetical weight (${pw}) contradicts primitive ${aliasKey}: ${primitives.get(aliasKey).raw}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (cell.weight === null) {
|
|
462
|
+
cell.weight = "{primitive.font-weight-normal}";
|
|
463
|
+
notes.ramp.push(`${step}: §3 row names no weight — defaulted to font-weight-normal`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const monoFamily = cell.mono || (row.family !== null && /^mono/i.test(row.family));
|
|
467
|
+
const familyAlias = monoFamily ? "{typography.family.mono}"
|
|
468
|
+
: (step === "h1" || step === "h2" || step === "h3") ? "{typography.family.heading}"
|
|
469
|
+
: "{typography.family.body}";
|
|
470
|
+
|
|
471
|
+
const token = {
|
|
472
|
+
$type: "typography",
|
|
473
|
+
$value: {
|
|
474
|
+
fontFamily: familyAlias,
|
|
475
|
+
fontSize: { value: fontSize, unit: "px" },
|
|
476
|
+
lineHeight: parseFloat(sm[2]),
|
|
477
|
+
fontWeight: cell.weight,
|
|
478
|
+
letterSpacing: cell.tracking === null ? 0 : cell.tracking,
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const descriptions = [];
|
|
482
|
+
if (step === "caption") descriptions.push(row.role);
|
|
483
|
+
if (synthesized) descriptions.push(synthesized);
|
|
484
|
+
if (descriptions.length > 0) token.$description = descriptions.join(" — ");
|
|
485
|
+
if (cell.trackingCss !== null) token.$extensions = { [EXT]: { css: { letterSpacing: cell.trackingCss } } };
|
|
486
|
+
ramp[step] = token;
|
|
487
|
+
if (cell.trackingCss !== null && cell.trackingCss.endsWith("px")) {
|
|
488
|
+
notes.ramp.push(`${step}: px tracking ${cell.trackingCss} → ${token.$value.letterSpacing}em (at ${fontSize}px)`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return { family, ramp };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Build the payload lever-manifest from the source block, dropping token-duplicating keys (fonts, icons.stroke, version).
|
|
495
|
+
function buildLeverManifest(manifest, primitives, notes) {
|
|
496
|
+
const scalar = (k) => manifest.scalars.get(k);
|
|
497
|
+
const nums = (arr) => arr.map((v) => parseNumberCss(v));
|
|
498
|
+
|
|
499
|
+
// fonts duplicates primitive.font-sans/font-mono — the allowlist reads the tokens now.
|
|
500
|
+
const fonts = parseFlowObject(scalar("fonts").raw);
|
|
501
|
+
for (const [slot, primKey] of [["sans", "font-sans"], ["mono", "font-mono"]]) {
|
|
502
|
+
const a = parseFontStackCss(fonts[slot] ?? "");
|
|
503
|
+
const b = parseFontStackCss(primitives.get(primKey).raw);
|
|
504
|
+
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
505
|
+
notes.irregularities.push(`manifest fonts.${slot} differs from primitive ${primKey}: ${JSON.stringify(fonts[slot])} vs ${JSON.stringify(primitives.get(primKey).raw)}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
notes.drops.push("fonts (duplicates primitive.font-sans / primitive.font-mono)");
|
|
509
|
+
|
|
510
|
+
const icons = parseFlowObject(scalar("icons").raw);
|
|
511
|
+
if (icons.stroke !== undefined) {
|
|
512
|
+
if (icons.stroke !== primitives.get("icon-stroke").raw) {
|
|
513
|
+
notes.irregularities.push(`manifest icons.stroke ${icons.stroke} differs from primitive icon-stroke ${primitives.get("icon-stroke").raw}`);
|
|
514
|
+
}
|
|
515
|
+
notes.drops.push("icons.stroke (duplicates primitive.icon-stroke)");
|
|
516
|
+
}
|
|
517
|
+
if (scalar("version") !== undefined) notes.drops.push("version (superseded by the payload `schema` literal)");
|
|
518
|
+
|
|
519
|
+
const motion = parseFlowObject(scalar("motion").raw);
|
|
520
|
+
return {
|
|
521
|
+
"spacing-scale": nums(parseFlowArray(scalar("spacing-scale").raw)),
|
|
522
|
+
"radius-scale": nums(parseFlowArray(scalar("radius-scale").raw)),
|
|
523
|
+
"shadow-levels": parseFlowArray(scalar("shadow-levels").raw),
|
|
524
|
+
motion: { durations: nums(motion.durations), easings: motion.easings },
|
|
525
|
+
icons: { family: icons.family },
|
|
526
|
+
"accent-count": parseNumberCss(scalar("accent-count").raw),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Convert one parsed source entry into its three artifact documents.
|
|
531
|
+
function convertEntry(source, slug) {
|
|
532
|
+
const notes = { migrated: 0, rootAttached: [], dropped: [], hints: [], ramp: [], drops: [], irregularities: [], darkFile: false };
|
|
533
|
+
const frontmatter = parseFrontmatter(source);
|
|
534
|
+
if (!frontmatter) throw new Error("missing frontmatter");
|
|
535
|
+
if (frontmatter.schema !== "design-md/v1") throw new Error(`unknown source schema: ${frontmatter.schema}`);
|
|
536
|
+
|
|
537
|
+
const primText = extractFencedBlock(source, "design-primitives");
|
|
538
|
+
const maniText = extractFencedBlock(source, "lever-manifest");
|
|
539
|
+
if (primText === null || maniText === null) throw new Error("missing machine block(s)");
|
|
540
|
+
const prims = parsePrimitivesBlock(primText);
|
|
541
|
+
const manifest = parseManifestBlock(maniText);
|
|
542
|
+
|
|
543
|
+
// Non-token comments (dials, scales, standalone lines) have no token to land on — they're preserved
|
|
544
|
+
// on the artifact root $description and reported, never dropped silently.
|
|
545
|
+
const rootNotes = [];
|
|
546
|
+
const attachRoot = (where, comment) => { rootNotes.push(`[${where}] ${comment}`); notes.rootAttached.push({ where, comment }); };
|
|
547
|
+
|
|
548
|
+
// Primitive tokens.
|
|
549
|
+
const primitive = {};
|
|
550
|
+
for (const key of PRIMITIVES) {
|
|
551
|
+
const entry = prims.entries.get(key);
|
|
552
|
+
if (!entry) throw new Error(`missing primitive: ${key}`);
|
|
553
|
+
primitive[key] = buildPrimitiveToken(key, entry.raw, entry.comment, notes);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Dials + traits → payload; their comments are non-token.
|
|
557
|
+
const dial = (key) => {
|
|
558
|
+
const entry = prims.entries.get(key);
|
|
559
|
+
if (!entry) throw new Error(`missing dial: ${key}`);
|
|
560
|
+
if (entry.comment !== null) attachRoot(key, entry.comment);
|
|
561
|
+
return entry.raw;
|
|
562
|
+
};
|
|
563
|
+
const verbosity = dial("verbosity"), feedback = dial("feedback"), densityTier = dial("density-tier");
|
|
564
|
+
const traits = parseFlowArray(dial("product-traits"));
|
|
565
|
+
for (const c of prims.standalone) attachRoot("design-primitives", c);
|
|
566
|
+
for (const key of prims.entries.keys()) {
|
|
567
|
+
if (!PRIMITIVES.includes(key) && !["verbosity", "feedback", "density-tier", "product-traits"].includes(key)) {
|
|
568
|
+
notes.irregularities.push(`unknown design-primitives key: ${key}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Color tokens (light) — every role required.
|
|
573
|
+
const color = {};
|
|
574
|
+
for (const role of COLOR_ROLES) {
|
|
575
|
+
const entry = manifest.light.get(role);
|
|
576
|
+
if (!entry) throw new Error(`missing color token: ${role}`);
|
|
577
|
+
color[role] = buildColorToken(entry.raw, entry.comment, notes);
|
|
578
|
+
}
|
|
579
|
+
for (const role of manifest.light.keys()) if (!COLOR_ROLES.includes(role)) notes.irregularities.push(`unknown light color token: ${role}`);
|
|
580
|
+
|
|
581
|
+
// Manifest scalar comments + standalone comments are non-token.
|
|
582
|
+
for (const [key, { comment }] of manifest.scalars) if (comment !== null) attachRoot(key, comment);
|
|
583
|
+
for (const c of manifest.standalone) attachRoot(c.where, c.comment);
|
|
584
|
+
|
|
585
|
+
const leverManifest = buildLeverManifest(manifest, prims.entries, notes);
|
|
586
|
+
const typography = buildTypography(parseRampTable(source), prims.entries, notes);
|
|
587
|
+
|
|
588
|
+
// Root $description: the source H1 title, then any root-attached provenance notes.
|
|
589
|
+
const h1 = /^# (.+)$/m.exec(source);
|
|
590
|
+
const $description = [h1 ? h1[1].trim() : frontmatter.product, ...rootNotes].join("\n");
|
|
591
|
+
|
|
592
|
+
const base = {
|
|
593
|
+
$description,
|
|
594
|
+
$extensions: {
|
|
595
|
+
[EXT]: {
|
|
596
|
+
schema: "design-tokens/v1",
|
|
597
|
+
dtcg: "2025.10",
|
|
598
|
+
product: frontmatter.product,
|
|
599
|
+
provenance: frontmatter.provenance,
|
|
600
|
+
sourced: frontmatter.sourced,
|
|
601
|
+
"product-traits": traits,
|
|
602
|
+
verbosity, feedback, "density-tier": densityTier,
|
|
603
|
+
"lever-manifest": leverManifest,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
primitive, color, typography,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Dark set — the color group remapped, nothing else; partial maps inherit light with a marker.
|
|
610
|
+
let dark = null;
|
|
611
|
+
if (manifest.dark !== null) {
|
|
612
|
+
notes.darkFile = true;
|
|
613
|
+
const darkColor = {};
|
|
614
|
+
for (const role of COLOR_ROLES) {
|
|
615
|
+
const entry = manifest.dark.get(role);
|
|
616
|
+
if (entry) {
|
|
617
|
+
darkColor[role] = buildColorToken(entry.raw, entry.comment, notes);
|
|
618
|
+
} else {
|
|
619
|
+
darkColor[role] = structuredClone(color[role]);
|
|
620
|
+
darkColor[role].$description = "dark: inherited from light (source map partial)";
|
|
621
|
+
notes.irregularities.push(`dark map partial: ${role} inherited from light`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const role of manifest.dark.keys()) if (!COLOR_ROLES.includes(role)) notes.irregularities.push(`unknown dark color token: ${role}`);
|
|
625
|
+
dark = { color: darkColor };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const resolver = {
|
|
629
|
+
schema: "design-resolver/v1",
|
|
630
|
+
sets: [{ name: "base", values: ["design.tokens.json"] }],
|
|
631
|
+
modifiers: [{
|
|
632
|
+
name: "theme", default: "light",
|
|
633
|
+
contexts: dark ? { light: [], dark: ["design.tokens.dark.json"] } : { light: [] },
|
|
634
|
+
}],
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
return { base, dark, resolver, notes };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// Validation — SCHEMA.md rules 2–10 over an emitted artifact directory
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
// Collect every token in the artifact as path → token for alias resolution and type sweeps.
|
|
645
|
+
function collectTokens(doc) {
|
|
646
|
+
const tokens = new Map();
|
|
647
|
+
const walk = (node, path) => {
|
|
648
|
+
if (node && typeof node === "object" && "$type" in node) { tokens.set(path.join("."), node); return; }
|
|
649
|
+
if (node && typeof node === "object") {
|
|
650
|
+
for (const [k, v] of Object.entries(node)) if (!k.startsWith("$")) walk(v, [...path, k]);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
walk(doc, []);
|
|
654
|
+
return tokens;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Resolve an alias chain to its target token; records an error string instead on failure.
|
|
658
|
+
function resolveAlias(ref, tokens, errors, context) {
|
|
659
|
+
const seen = new Set();
|
|
660
|
+
let cur = ref;
|
|
661
|
+
while (typeof cur === "string" && /^\{[^}]+\}$/.test(cur)) {
|
|
662
|
+
const path = cur.slice(1, -1);
|
|
663
|
+
if (seen.has(path)) { errors.push(`${context}: alias cycle through {${path}}`); return null; }
|
|
664
|
+
seen.add(path);
|
|
665
|
+
const target = tokens.get(path);
|
|
666
|
+
if (!target) { errors.push(`${context}: alias {${path}} does not resolve`); return null; }
|
|
667
|
+
cur = target.$value;
|
|
668
|
+
if (typeof cur !== "string" || !/^\{[^}]+\}$/.test(cur)) return target;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Verify a preserved-css color token: parse-back equality plus the srgb hex round-trip.
|
|
674
|
+
function verifyColorParseBack(name, token, errors) {
|
|
675
|
+
const css = token.$extensions?.[EXT]?.css;
|
|
676
|
+
if (typeof css !== "string") { errors.push(`${name}: color token missing preserved .css`); return; }
|
|
677
|
+
let reparsed;
|
|
678
|
+
try { reparsed = parseColorCss(css); } catch (e) { errors.push(`${name}: ${e.message}`); return; }
|
|
679
|
+
if (JSON.stringify(reparsed) !== JSON.stringify(token.$value)) {
|
|
680
|
+
errors.push(`${name}: parse-back mismatch — css ${JSON.stringify(css)} → ${JSON.stringify(reparsed)} != $value ${JSON.stringify(token.$value)}`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (token.$value.colorSpace === "srgb") {
|
|
684
|
+
const viaHex = parseColorCss(token.$value.hex);
|
|
685
|
+
if (JSON.stringify(viaHex.components) !== JSON.stringify(token.$value.components)) {
|
|
686
|
+
errors.push(`${name}: srgb components do not round-trip through hex ${token.$value.hex}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Validate one artifact directory against SCHEMA.md rules 2–10 (plus the type whitelist); returns error strings.
|
|
692
|
+
function validateArtifact(dir) {
|
|
693
|
+
const errors = [];
|
|
694
|
+
const read = (f) => JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
695
|
+
let doc, resolver;
|
|
696
|
+
try { doc = read("design.tokens.json"); } catch (e) { return [`design.tokens.json: ${e.message}`]; }
|
|
697
|
+
try { resolver = read("design.resolver.json"); } catch (e) { return [`design.resolver.json: ${e.message}`]; }
|
|
698
|
+
|
|
699
|
+
// Rule 2 — payload.
|
|
700
|
+
const payload = doc.$extensions?.[EXT];
|
|
701
|
+
if (!payload) { errors.push("payload: missing $extensions[com.nurix.design] on the base root"); return errors; }
|
|
702
|
+
if (payload.schema !== "design-tokens/v1") errors.push(`payload.schema: ${payload.schema}`);
|
|
703
|
+
if (payload.dtcg !== "2025.10") errors.push(`payload.dtcg: ${payload.dtcg}`);
|
|
704
|
+
if (!payload.product) errors.push("payload.product: empty");
|
|
705
|
+
if (!LEGAL_PROVENANCE.includes(payload.provenance)) errors.push(`payload.provenance: ${payload.provenance}`);
|
|
706
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(payload.sourced ?? "")) errors.push(`payload.sourced: ${payload.sourced}`);
|
|
707
|
+
if (!LEGAL_VERBOSITY.includes(payload.verbosity)) errors.push(`payload.verbosity: ${payload.verbosity}`);
|
|
708
|
+
if (!LEGAL_FEEDBACK.includes(payload.feedback)) errors.push(`payload.feedback: ${payload.feedback}`);
|
|
709
|
+
if (!LEGAL_DENSITY.includes(payload["density-tier"])) errors.push(`payload.density-tier: ${payload["density-tier"]}`);
|
|
710
|
+
if (!Array.isArray(payload["product-traits"]) || payload["product-traits"].length === 0) errors.push("payload.product-traits: empty");
|
|
711
|
+
const lm = payload["lever-manifest"];
|
|
712
|
+
for (const key of ["spacing-scale", "radius-scale", "shadow-levels", "motion", "icons", "accent-count"]) {
|
|
713
|
+
if (lm?.[key] === undefined) errors.push(`lever-manifest.${key}: missing`);
|
|
714
|
+
}
|
|
715
|
+
if (errors.length > 0) return errors;
|
|
716
|
+
|
|
717
|
+
// Rule 3 — primitive coverage with exact $type.
|
|
718
|
+
for (const [key, type] of PRIMITIVE_TYPES) {
|
|
719
|
+
const t = doc.primitive?.[key];
|
|
720
|
+
if (!t) errors.push(`primitive.${key}: missing`);
|
|
721
|
+
else if (t.$type !== type) errors.push(`primitive.${key}: $type ${t.$type} (want ${type})`);
|
|
722
|
+
else if (t.$value === undefined || t.$value === "") errors.push(`primitive.${key}: empty $value`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Rule 4 — color coverage.
|
|
726
|
+
for (const role of COLOR_ROLES) {
|
|
727
|
+
const t = doc.color?.[role];
|
|
728
|
+
if (!t) { errors.push(`color.${role}: missing`); continue; }
|
|
729
|
+
if (t.$type !== "color") errors.push(`color.${role}: $type ${t.$type}`);
|
|
730
|
+
if (!["oklch", "srgb"].includes(t.$value?.colorSpace)) errors.push(`color.${role}: colorSpace ${t.$value?.colorSpace}`);
|
|
731
|
+
if (typeof t.$extensions?.[EXT]?.css !== "string") errors.push(`color.${role}: missing .css`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Rule 5 — typography coverage.
|
|
735
|
+
for (const fam of ["heading", "body", "mono"]) {
|
|
736
|
+
const t = doc.typography?.family?.[fam];
|
|
737
|
+
if (!t || t.$type !== "fontFamily") errors.push(`typography.family.${fam}: missing or mistyped`);
|
|
738
|
+
}
|
|
739
|
+
for (const step of RAMP_STEPS) {
|
|
740
|
+
const t = doc.typography?.ramp?.[step];
|
|
741
|
+
if (!t || t.$type !== "typography") { errors.push(`typography.ramp.${step}: missing or mistyped`); continue; }
|
|
742
|
+
for (const sub of ["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"]) {
|
|
743
|
+
if (t.$value?.[sub] === undefined) errors.push(`typography.ramp.${step}.${sub}: missing sub-value`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (errors.length > 0) return errors;
|
|
747
|
+
|
|
748
|
+
// Rule 6 — alias integrity (token values and composite sub-values), with type expectations.
|
|
749
|
+
const tokens = collectTokens(doc);
|
|
750
|
+
const expectType = (ref, want, context) => {
|
|
751
|
+
const target = resolveAlias(ref, tokens, errors, context);
|
|
752
|
+
if (target && want && target.$type !== want) errors.push(`${context}: alias ${ref} resolves to $type ${target.$type} (want ${want})`);
|
|
753
|
+
};
|
|
754
|
+
for (const [path, token] of tokens) {
|
|
755
|
+
if (typeof token.$value === "string" && token.$value.startsWith("{")) expectType(token.$value, token.$type, path);
|
|
756
|
+
if (token.$type === "typography") {
|
|
757
|
+
const subTypes = { fontFamily: "fontFamily", fontWeight: "fontWeight", letterSpacing: "number", fontSize: "dimension" };
|
|
758
|
+
for (const [sub, want] of Object.entries(subTypes)) {
|
|
759
|
+
const v = token.$value[sub];
|
|
760
|
+
if (typeof v === "string" && v.startsWith("{")) expectType(v, want, `${path}.${sub}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Rule 7 — manifest scales.
|
|
766
|
+
const ascending = (arr) => arr.every((v, i) => i === 0 || v > arr[i - 1]);
|
|
767
|
+
for (const key of ["spacing-scale", "radius-scale"]) {
|
|
768
|
+
if (!Array.isArray(lm[key]) || lm[key].length === 0 || !ascending(lm[key])) errors.push(`lever-manifest.${key}: not non-empty ascending`);
|
|
769
|
+
}
|
|
770
|
+
if (!Array.isArray(lm.motion.durations) || lm.motion.durations.length === 0 || !ascending(lm.motion.durations)) errors.push("lever-manifest.motion.durations: not non-empty ascending");
|
|
771
|
+
if (!Array.isArray(lm.motion.easings) || lm.motion.easings.length === 0) errors.push("lever-manifest.motion.easings: empty");
|
|
772
|
+
const ranks = (lm["shadow-levels"] ?? []).map((s) => SHADOW_RANK[s]);
|
|
773
|
+
if (ranks.length === 0 || ranks.some((r) => r === undefined) || !ascending(ranks)) errors.push(`lever-manifest.shadow-levels: ${JSON.stringify(lm["shadow-levels"])}`);
|
|
774
|
+
if (!lm.icons?.family) errors.push("lever-manifest.icons.family: empty");
|
|
775
|
+
if (!(lm["accent-count"] >= 1)) errors.push(`lever-manifest.accent-count: ${lm["accent-count"]}`);
|
|
776
|
+
|
|
777
|
+
// Rule 8 — the look's own base values sit on its own allowlist.
|
|
778
|
+
if (!lm["radius-scale"].includes(doc.primitive.radius.$value.value)) errors.push(`radius ${doc.primitive.radius.$value.value} not on radius-scale`);
|
|
779
|
+
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`);
|
|
781
|
+
}
|
|
782
|
+
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`);
|
|
784
|
+
|
|
785
|
+
// Rule 9 — parse-back for every preserved .css.
|
|
786
|
+
for (const role of COLOR_ROLES) verifyColorParseBack(`color.${role}`, doc.color[role], errors);
|
|
787
|
+
const ht = doc.primitive["heading-tracking"];
|
|
788
|
+
const htCss = ht.$extensions?.[EXT]?.css;
|
|
789
|
+
if (typeof htCss !== "string") errors.push("primitive.heading-tracking: missing .css");
|
|
790
|
+
else if (parseTrackingCss(htCss) !== ht.$value) errors.push(`primitive.heading-tracking: css ${htCss} != $value ${ht.$value}`);
|
|
791
|
+
if (easeCss === undefined) errors.push("primitive.ease-default: missing .css");
|
|
792
|
+
else if (JSON.stringify(parseCubicBezierCss(easeCss)) !== JSON.stringify(doc.primitive["ease-default"].$value)) errors.push("primitive.ease-default: css/$value mismatch");
|
|
793
|
+
for (const key of ["font-sans", "font-mono"]) {
|
|
794
|
+
const t = doc.primitive[key];
|
|
795
|
+
const css = t.$extensions?.[EXT]?.css;
|
|
796
|
+
if (typeof css !== "string") { errors.push(`primitive.${key}: missing .css`); continue; }
|
|
797
|
+
if (JSON.stringify(parseFontStackCss(css)) !== JSON.stringify(t.$value)) errors.push(`primitive.${key}: css/$value mismatch`);
|
|
798
|
+
}
|
|
799
|
+
for (const step of RAMP_STEPS) {
|
|
800
|
+
const t = doc.typography.ramp[step];
|
|
801
|
+
const css = t.$extensions?.[EXT]?.css;
|
|
802
|
+
if (css?.letterSpacing !== undefined) {
|
|
803
|
+
const reparsed = parseLetterSpacingCss(css.letterSpacing, t.$value.fontSize.value);
|
|
804
|
+
if (reparsed !== t.$value.letterSpacing) errors.push(`typography.ramp.${step}: letterSpacing css ${css.letterSpacing} → ${reparsed} != ${t.$value.letterSpacing}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Rule 10 + resolver coherence — the dark file carries exactly the 34-role color group.
|
|
809
|
+
const darkNamed = resolver.modifiers?.[0]?.contexts?.dark !== undefined;
|
|
810
|
+
const darkExists = existsSync(join(dir, "design.tokens.dark.json"));
|
|
811
|
+
if (darkNamed !== darkExists) errors.push(`dark file/resolver mismatch: named=${darkNamed} exists=${darkExists}`);
|
|
812
|
+
if (resolver.schema !== "design-resolver/v1") errors.push(`resolver.schema: ${resolver.schema}`);
|
|
813
|
+
if (resolver.modifiers?.[0]?.default !== "light") errors.push("resolver: theme default must be light");
|
|
814
|
+
if (darkExists) {
|
|
815
|
+
const dark = read("design.tokens.dark.json");
|
|
816
|
+
const rootKeys = Object.keys(dark);
|
|
817
|
+
if (rootKeys.length !== 1 || rootKeys[0] !== "color") errors.push(`dark file: root keys ${rootKeys.join(",")} (want exactly color)`);
|
|
818
|
+
for (const role of COLOR_ROLES) {
|
|
819
|
+
const t = dark.color?.[role];
|
|
820
|
+
if (!t || t.$type !== "color") { errors.push(`dark color.${role}: missing or mistyped`); continue; }
|
|
821
|
+
verifyColorParseBack(`dark color.${role}`, t, errors);
|
|
822
|
+
}
|
|
823
|
+
for (const role of Object.keys(dark.color ?? {})) if (!COLOR_ROLES.includes(role)) errors.push(`dark color.${role}: not a legal role`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Rule 11 (type whitelist sweep) — only the eight legal $type values appear.
|
|
827
|
+
for (const [path, token] of tokens) {
|
|
828
|
+
if (!LEGAL_TYPES.includes(token.$type)) errors.push(`${path}: illegal $type ${token.$type}`);
|
|
829
|
+
}
|
|
830
|
+
return errors;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// Strip mode — remove the machine blocks and their orphaned prose references
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
// A sentence or parenthetical that references the (removed) machine blocks.
|
|
838
|
+
const ORPHAN_PATTERN = /machine blocks?|machine-carried|lever[- ]manifest|design-primitives|`hint:`|manifest carries|manifest ships|manifest holds|the manifest|manifest's|both blocks/i;
|
|
839
|
+
// A reference to the in-block `# rationale:` / `# derived:` comment convention — also orphaned by the strip.
|
|
840
|
+
const ANNOTATION_PATTERN = /`# ?(rationale|derived|corpus|note)/i;
|
|
841
|
+
|
|
842
|
+
// Re-wrap a paragraph's text at ~100 columns, preserving a leading indent/bullet prefix.
|
|
843
|
+
function rewrap(text, prefix = "", width = 100) {
|
|
844
|
+
const words = text.split(/\s+/).filter((w) => w !== "");
|
|
845
|
+
const lines = [];
|
|
846
|
+
let cur = prefix;
|
|
847
|
+
let bare = prefix.replace(/\S/g, " ");
|
|
848
|
+
for (const w of words) {
|
|
849
|
+
if (cur.trim() === "" || (cur + " " + w).length <= width) cur = cur.trim() === "" && cur === prefix ? prefix + w : cur + " " + w;
|
|
850
|
+
else { lines.push(cur); cur = bare + w; }
|
|
851
|
+
}
|
|
852
|
+
if (cur.trim() !== "") lines.push(cur);
|
|
853
|
+
return lines;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Lossless sentence split: cut after . ! ? only when a new sentence plausibly starts (whitespace + capital/opener).
|
|
857
|
+
function splitSentences(text) {
|
|
858
|
+
const out = [];
|
|
859
|
+
let start = 0;
|
|
860
|
+
for (let i = 0; i < text.length; i++) {
|
|
861
|
+
if (".!?".includes(text[i]) && /^\s+["'*(\[`—A-Z0-9]/.test(text.slice(i + 1))) {
|
|
862
|
+
const end = i + 1 + /^\s+/.exec(text.slice(i + 1))[0].length;
|
|
863
|
+
out.push(text.slice(start, end));
|
|
864
|
+
start = end;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (start < text.length) out.push(text.slice(start));
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Remove orphaned block-referencing parentheticals and sentences from one prose paragraph; null = unchanged.
|
|
872
|
+
function stripParagraph(lines) {
|
|
873
|
+
const joined = lines.map((l) => l.trim()).join(" ");
|
|
874
|
+
// 1. surgical: drop "(…)" parentheticals whose content references the blocks.
|
|
875
|
+
let text = joined.replace(/\s*\(([^()]*)\)/g, (m, inner) => (ORPHAN_PATTERN.test(inner) ? "" : m));
|
|
876
|
+
// 2. clause-level: drop a "; …" clause that only cites the annotation convention, keeping its sentence.
|
|
877
|
+
text = text.replace(/;\s*[^.!?;]*`# ?(rationale|derived|corpus|note)[^.!?]*?(?=[.!?])/gi, "");
|
|
878
|
+
// 3. sentence-level: drop whole sentences that still reference the blocks or their annotations.
|
|
879
|
+
const sentences = splitSentences(text);
|
|
880
|
+
if (sentences.join("") !== text) throw new Error(`lossy sentence split in: ${text.slice(0, 80)}…`);
|
|
881
|
+
text = sentences.filter((s) => !ORPHAN_PATTERN.test(s) && !ANNOTATION_PATTERN.test(s)).join("").trim();
|
|
882
|
+
if (text === joined) return null;
|
|
883
|
+
if (text === "") return [];
|
|
884
|
+
const m = /^(\s*(?:[-*]\s+|\d+\.\s+)?)/.exec(lines[0]);
|
|
885
|
+
return rewrap(text, m[1] ?? "");
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Strip both machine blocks (and orphaned prose) from one source text; returns { text, summary }.
|
|
889
|
+
function stripSource(source) {
|
|
890
|
+
const summary = { blocksRemoved: 0, paragraphsEdited: 0 };
|
|
891
|
+
let text = source;
|
|
892
|
+
for (const label of ["design-primitives", "lever-manifest"]) {
|
|
893
|
+
const re = new RegExp("\\n*^`{3,}\\s*yaml\\s+" + label + "\\s*$\\n[\\s\\S]*?^`{3,}\\s*$\\n?", "m");
|
|
894
|
+
if (re.test(text)) { text = text.replace(re, "\n"); summary.blocksRemoved++; }
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Paragraph pass: skip frontmatter, headings, tables, and fenced code — only plain prose is edited.
|
|
898
|
+
const lines = text.split("\n");
|
|
899
|
+
const out = [];
|
|
900
|
+
let i = 0;
|
|
901
|
+
let inFence = false;
|
|
902
|
+
if (lines[0] === "---") {
|
|
903
|
+
out.push(lines[0]);
|
|
904
|
+
for (i = 1; i < lines.length; i++) { out.push(lines[i]); if (lines[i] === "---") { i++; break; } }
|
|
905
|
+
}
|
|
906
|
+
while (i < lines.length) {
|
|
907
|
+
const line = lines[i];
|
|
908
|
+
if (/^`{3,}/.test(line.trim())) inFence = !inFence;
|
|
909
|
+
const isProse = !inFence && line.trim() !== "" && !/^(#|\||`{3,}|>)/.test(line.trim());
|
|
910
|
+
if (!isProse) { out.push(line); i++; continue; }
|
|
911
|
+
const para = [];
|
|
912
|
+
while (i < lines.length && lines[i].trim() !== "" && !/^(#|\||`{3,}|>)/.test(lines[i].trim())) {
|
|
913
|
+
// a new bullet starts a new paragraph unit
|
|
914
|
+
if (para.length > 0 && /^\s*[-*]\s+/.test(lines[i])) break;
|
|
915
|
+
para.push(lines[i]); i++;
|
|
916
|
+
}
|
|
917
|
+
const replaced = stripParagraph(para);
|
|
918
|
+
if (replaced === null) out.push(...para);
|
|
919
|
+
else { summary.paragraphsEdited++; out.push(...replaced); }
|
|
920
|
+
}
|
|
921
|
+
let result = out.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n");
|
|
922
|
+
return { text: result, summary };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
// Drivers
|
|
927
|
+
// ---------------------------------------------------------------------------
|
|
928
|
+
|
|
929
|
+
// Enumerate the source entries: nurix.md + every library/*.md, slug = filename minus .md.
|
|
930
|
+
function listSources() {
|
|
931
|
+
const entries = [{ slug: "nurix", path: join(designMdDir, "nurix.md") }];
|
|
932
|
+
for (const f of readdirSync(join(designMdDir, "library")).filter((f) => f.endsWith(".md")).sort()) {
|
|
933
|
+
entries.push({ slug: basename(f, ".md"), path: join(designMdDir, "library", f) });
|
|
934
|
+
}
|
|
935
|
+
return entries;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function runConvert(only, reportPath) {
|
|
939
|
+
const sources = listSources().filter((s) => !only || s.slug === only);
|
|
940
|
+
if (sources.length === 0) fail(`no source entry named ${only}`);
|
|
941
|
+
const report = { converted: [], failed: [], totals: { migrated: 0, rootAttached: 0, dropped: 0, hints: 0, darkFiles: [], fontDrops: 0 } };
|
|
942
|
+
|
|
943
|
+
for (const { slug, path } of sources) {
|
|
944
|
+
let result;
|
|
945
|
+
try {
|
|
946
|
+
result = convertEntry(readFileSync(path, "utf8"), slug);
|
|
947
|
+
} catch (e) {
|
|
948
|
+
report.failed.push({ slug, error: e.message });
|
|
949
|
+
console.log(`CONVERT FAIL ${slug}: ${e.message}`);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const dir = join(tokensDir, slug);
|
|
953
|
+
mkdirSync(dir, { recursive: true });
|
|
954
|
+
writeFileSync(join(dir, "design.tokens.json"), JSON.stringify(result.base, null, 2) + "\n");
|
|
955
|
+
writeFileSync(join(dir, "design.resolver.json"), JSON.stringify(result.resolver, null, 2) + "\n");
|
|
956
|
+
if (result.dark) writeFileSync(join(dir, "design.tokens.dark.json"), JSON.stringify(result.dark, null, 2) + "\n");
|
|
957
|
+
|
|
958
|
+
const n = result.notes;
|
|
959
|
+
report.converted.push({ slug, migrated: n.migrated, rootAttached: n.rootAttached, dropped: n.dropped, hints: n.hints, ramp: n.ramp, drops: n.drops, irregularities: n.irregularities, darkFile: n.darkFile });
|
|
960
|
+
report.totals.migrated += n.migrated;
|
|
961
|
+
report.totals.rootAttached += n.rootAttached.length;
|
|
962
|
+
report.totals.dropped += n.dropped.length;
|
|
963
|
+
report.totals.hints += n.hints.length;
|
|
964
|
+
if (n.darkFile) report.totals.darkFiles.push(slug);
|
|
965
|
+
if (n.drops.some((d) => d.startsWith("fonts"))) report.totals.fontDrops++;
|
|
966
|
+
console.log(`CONVERT OK ${slug} (annotations: ${n.migrated}${n.darkFile ? ", dark" : ""}${n.hints.length ? `, hints: ${n.hints.length}` : ""})`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// _dropped: comments that landed nowhere. Non-token comments are preserved on the root $description
|
|
970
|
+
// and listed per entry as rootAttached — reported here so nothing disappears silently.
|
|
971
|
+
const droppedReport = report.converted.flatMap((e) => e.dropped.map((d) => ({ slug: e.slug, ...d })));
|
|
972
|
+
console.log(`\n_dropped (unattachable comments): ${droppedReport.length}`);
|
|
973
|
+
for (const d of droppedReport) console.log(` ${d.slug}: ${JSON.stringify(d)}`);
|
|
974
|
+
const rootAttached = report.converted.flatMap((e) => e.rootAttached.map((r) => ({ slug: e.slug, ...r })));
|
|
975
|
+
console.log(`non-token comments preserved on root $description: ${rootAttached.length}`);
|
|
976
|
+
for (const r of rootAttached) console.log(` ${r.slug} [${r.where}] ${r.comment}`);
|
|
977
|
+
|
|
978
|
+
if (reportPath) writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
|
|
979
|
+
return report;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function runValidate(only) {
|
|
983
|
+
const sources = listSources().filter((s) => !only || s.slug === only);
|
|
984
|
+
let pass = 0, failCount = 0;
|
|
985
|
+
for (const { slug } of sources) {
|
|
986
|
+
const dir = join(tokensDir, slug);
|
|
987
|
+
if (!existsSync(join(dir, "design.tokens.json"))) { console.log(`VALIDATE FAIL ${slug}: no artifact`); failCount++; continue; }
|
|
988
|
+
let errors;
|
|
989
|
+
try { errors = validateArtifact(dir); } catch (e) { errors = [`validator crash: ${e.message}`]; }
|
|
990
|
+
if (errors.length === 0) { console.log(`VALIDATE OK ${slug}`); pass++; }
|
|
991
|
+
else {
|
|
992
|
+
failCount++;
|
|
993
|
+
console.log(`VALIDATE FAIL ${slug}`);
|
|
994
|
+
for (const err of errors) console.log(` ${err}`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
console.log(`\nvalidation: ${pass} passed, ${failCount} failed of ${sources.length}`);
|
|
998
|
+
return failCount === 0;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function runStrip(paths) {
|
|
1002
|
+
if (paths.length === 0) fail("--strip requires explicit file paths (it rewrites sources in place; never run it implicitly)");
|
|
1003
|
+
for (const p of paths) {
|
|
1004
|
+
const abs = resolve(p);
|
|
1005
|
+
const { text, summary } = stripSource(readFileSync(abs, "utf8"));
|
|
1006
|
+
writeFileSync(abs, text);
|
|
1007
|
+
console.log(`STRIP ${abs}: ${summary.blocksRemoved} machine block(s) removed, ${summary.paragraphsEdited} paragraph(s) edited`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ---------------------------------------------------------------------------
|
|
1012
|
+
// CLI
|
|
1013
|
+
// ---------------------------------------------------------------------------
|
|
1014
|
+
|
|
1015
|
+
const args = process.argv.slice(2);
|
|
1016
|
+
const flag = (name) => args.includes(name);
|
|
1017
|
+
const opt = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : null; };
|
|
1018
|
+
|
|
1019
|
+
if (flag("--strip")) {
|
|
1020
|
+
runStrip(args.slice(args.indexOf("--strip") + 1).filter((a) => !a.startsWith("--")));
|
|
1021
|
+
} else if (flag("--validate")) {
|
|
1022
|
+
process.exit(runValidate(opt("--only")) ? 0 : 1);
|
|
1023
|
+
} else {
|
|
1024
|
+
const report = runConvert(opt("--only"), opt("--report"));
|
|
1025
|
+
const ok = runValidate(opt("--only"));
|
|
1026
|
+
console.log(`\nconverted: ${report.converted.length}, failed: ${report.failed.length}`);
|
|
1027
|
+
console.log(`annotations migrated to token $description: ${report.totals.migrated}`);
|
|
1028
|
+
console.log(`dark files: ${report.totals.darkFiles.join(", ") || "none"}`);
|
|
1029
|
+
console.log(`source hints found: ${report.totals.hints}`);
|
|
1030
|
+
console.log(`manifest fonts duplication dropped: ${report.totals.fontDrops} entries`);
|
|
1031
|
+
process.exit(ok && report.failed.length === 0 ? 0 : 1);
|
|
1032
|
+
}
|