diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b571819e..90066707 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,15 @@ jobs: - run: node scripts/check-hook-tokens.js - run: node scripts/check-mirrors.js - run: node scripts/check-bundle-defs.js + # Strings that must never ship: hardcoded colour literals outside the + # token source files, and external URLs in a built bundle. Runs after + # check-artifacts.js, which rebuilds dist/ — so the bundle rules have + # something to scan. + - run: npm run check:forbidden-strings + # docs/token-renames.json must stay truthful: every rename target live, + # no old name still live. A stale map migrates theme files onto dead + # tokens, which is worse than having no map at all. + - run: npm run check:token-renames dependency-audit: name: Dependency vulnerability audit diff --git a/CLAUDE.md b/CLAUDE.md index 53ab14a8..712f2a21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,9 @@ requires a rebuild+redeploy, not just a file edit. | `npm run check:layer-order` | Verify `docs/architecture.md`'s `@layer` block and specificity ladder match `core/layers.css` (CI gate) | | `npm run check:macros` | Verify `.sf-*` macro classes match `docs/macros.md` (CI gate) | | `npm run check:registry` | Verify `token-registry.json` is in sync with source (CI gate) | +| `npm run check:forbidden-strings` | Verify no hardcoded colour literal sits outside the token source files and no shipped bundle contains an external URL (CI gate) | +| `npm run check:token-renames` | Verify `docs/token-renames.json` is truthful — every rename target is a live token, no old name still is (CI gate) | +| `npm run migrate:theme -- [--write]` | Migrate a `*.slashed-theme.json` theme file onto the current token API | | `npm run audit:check` | Verify `docs/registry.json` matches source without writing (CI gate) | | `npm run lint:css` | Lint all CSS source with stylelint (CI gate) | | `npm run lint:css:fix` | Lint CSS source and auto-fix violations | @@ -140,6 +143,40 @@ instance token, an example of a component the framework does not ship) — recor it in `docs/ref-allowlist.json` with a reason. `docs/migration.md` (historical) and `docs/roadmap.md` (forward-looking) are whole-doc exclusions. +## Token renames — MANDATORY + +`docs/token-renames.json` is the machine-readable mirror of `docs/migration.md`. +It is what lets a **theme file** (`*.slashed-theme.json` — the portable, +name-keyed override snapshot, see `scripts/lib/theme-file.js`) survive a rename: +`npm run migrate:theme -- --write` rewrites old names, drops removed ones +with the reason, and never discards an override it does not recognise. + +**Any PR that renames or removes a `--sf-*` token must add the corresponding +entry**, in the same PR as the CSS change: + +- **Renamed** → add to `renames` as `"--sf-old": "--sf-new"`. Record the + *fully resolved* destination, never an intermediate name: rename targets must + be live, so a chain (`a → b → c`) fails the gate by construction. +- **Removed with no replacement** → add to `removals` with a reason saying what + to use instead. A removal without a reason is rejected. + +This cannot be generated. `token-registry.json` keeps ids permanent, but +`check-token-registry.js` deliberately permits renames as *in-place name +updates on the same id* — so after a rename the old name is simply gone, with +nothing to look it up by. That is harmless for the share-link codec (it stores +ids) and fatal for a name-keyed theme file. A rename and a delete+add pair are +also indistinguishable to a generator, so the map is curated by hand. + +```bash +npm run check:token-renames # must pass — CI fails if it doesn't +``` + +The gate holds the map to three invariants: every rename target is live, no old +name is still live, and renames and removals are disjoint. Note that "live" +includes tokens merely *declared* in `core/`/`optional/` CSS — so a leftover +declaration of a supposedly-renamed token will fail this gate, which is how it +catches a half-finished rename. + ## Tests ```bash diff --git a/configurator/scripts/sync-api.mjs b/configurator/scripts/sync-api.mjs index c5cf95f9..60529052 100644 --- a/configurator/scripts/sync-api.mjs +++ b/configurator/scripts/sync-api.mjs @@ -43,12 +43,14 @@ const SOURCE = const ANNOTATIONS_FILE = path.join(FRAMEWORK_ROOT, 'docs', 'token-annotations.json'); const BUNDLE_CONFIG_FILE = path.join(FRAMEWORK_ROOT, 'bundle.config.json'); const REGISTRY_FILE = path.join(FRAMEWORK_ROOT, 'token-registry.json'); +const RENAMES_FILE = path.join(FRAMEWORK_ROOT, 'docs', 'token-renames.json'); const OUT_DIR = path.join(CONFIGURATOR_ROOT, 'src', 'data'); const OUT = path.join(OUT_DIR, 'api-index.generated.json'); const CLASSES_OUT = path.join(OUT_DIR, 'classes.generated.json'); const BUNDLES_OUT = path.join(OUT_DIR, 'bundles.generated.json'); const REGISTRY_OUT = path.join(OUT_DIR, 'token-registry.generated.json'); +const RENAMES_OUT = path.join(OUT_DIR, 'token-renames.generated.json'); // jsDelivr serves the published dist branch (see .github/workflows/publish-dist.yml) // at the repo root, so a bundle's minified file is /slashed..min.css. @@ -314,6 +316,10 @@ function main() { // verbatim so the configurator imports it the same way model.js imports the // api-index — and so the runtime can never drift from the committed registry. syncRegistry(); + + // Rename/removal map for theme-file import (src/lib/themeFile.ts), so an + // override set authored against an older SLASHED can be migrated on load. + syncRenames(); } /** @@ -348,4 +354,44 @@ function syncRegistry() { ); } +/** + * Copy docs/token-renames.json → src/data/token-renames.generated.json, so the + * configurator's theme-file import can migrate an old override set without + * reaching outside its own package at runtime (the @framework-css alias is + * remapped by the WP plugin, so cross-boundary runtime imports are not safe + * here — a generated data file is). + * + * The map's truthfulness is guaranteed upstream by scripts/check-token-renames.js. + */ +function syncRenames() { + if (!fs.existsSync(RENAMES_FILE)) { + console.error( + `[configurator:sync] token-renames.json not found at ${RENAMES_FILE}\n` + + `It is a hand-maintained mirror of docs/migration.md — it should be committed.` + ); + process.exit(1); + } + let map; + try { + map = JSON.parse(fs.readFileSync(RENAMES_FILE, 'utf8')); + } catch (err) { + console.error(`[configurator:sync] ${RENAMES_FILE} is not valid JSON (${err.message}).`); + process.exit(1); + } + const out = { + _sync: { + generatedBy: 'configurator/scripts/sync-api.mjs', + source: 'docs/token-renames.json', + }, + renames: map.renames ?? {}, + removals: map.removals ?? {}, + }; + fs.writeFileSync(RENAMES_OUT, JSON.stringify(out, null, 2) + '\n', 'utf8'); + console.log( + `[configurator:sync] ${path.relative(FRAMEWORK_ROOT, RENAMES_OUT)} ← ` + + `docs/token-renames.json (${Object.keys(out.renames).length} renames, ` + + `${Object.keys(out.removals).length} removals)` + ); +} + main(); diff --git a/configurator/src/components/DomainPanel.svelte b/configurator/src/components/DomainPanel.svelte index 89c8b55e..fb7270cb 100644 --- a/configurator/src/components/DomainPanel.svelte +++ b/configurator/src/components/DomainPanel.svelte @@ -64,7 +64,7 @@ {:else if domain === "wcag"} {:else if domain === "setup"} - + {:else if domain === "cheatsheet"} {/if} diff --git a/configurator/src/components/panels/ExportPanel.svelte b/configurator/src/components/panels/ExportPanel.svelte index 1d014fbd..1851c829 100644 --- a/configurator/src/components/panels/ExportPanel.svelte +++ b/configurator/src/components/panels/ExportPanel.svelte @@ -1,16 +1,61 @@