From ee0fd8c3b76dcad85ddbe2185b8a9d3e6b2276a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 12:24:52 +0000 Subject: [PATCH 1/2] feat(docs): add CI gate to keep llm-guide.md in sync with token registry Adds scripts/check-llm-guide.js with two checks: - Hard fail: any --sf-* name in the guide that is not a live token in token-registry.json (catches renames/deletions immediately). - Warning: PUBLIC/PUBLIC-ADVANCED knob tokens absent from the guide (surfaces coverage gaps without blocking CI). Wires the script into the artifacts-freshness CI job and npm run check:llm-guide. Fixes five stale token references caught by the new gate (--sf-color-code-block-{bg,text}, --sf-field-{border,text}-color, --sf-section-pad-xl-plus). Adds a mandatory "LLM guide sync" section to CLAUDE.md documenting the qualitative update obligation. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01GBbqrooCfW68LAjUMuNnYY --- .github/workflows/ci.yml | 1 + CLAUDE.md | 27 ++++++++++ docs/llm-guide.md | 13 ++--- package.json | 1 + scripts/check-llm-guide.js | 102 +++++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 scripts/check-llm-guide.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 290df526..99c3b57e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,7 @@ jobs: - run: node scripts/check-artifacts.js --check - run: node scripts/check-version-sync.js - run: node scripts/check-token-registry.js + - run: node scripts/check-llm-guide.js dependency-audit: name: Dependency vulnerability audit diff --git a/CLAUDE.md b/CLAUDE.md index 376021fa..756bc44b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,7 @@ requires a rebuild+redeploy, not just a file edit. | `npm run build` | Build all CSS bundles + docs + sync configurator API | | `npm run version-sync` | Sync all version references to root `package.json` | | `npm run check:version` | Verify all version references match (CI gate — run before every commit that touches versions) | +| `npm run check:llm-guide` | Verify `docs/llm-guide.md` only references live tokens (CI gate) | | `npm run docs` | Regenerate docs and sync configurator API index | | `npm run configurator:sync` | Push `docs/api-index.json` → `configurator/src/data/api-index.generated.json` | | `npm run audit` | Audit CSS tokens for consistency | @@ -83,6 +84,32 @@ After the tag is pushed, GitHub Actions (`release.yml`) does the rest: - The stamp version must match `package.json` — `release.yml` verifies this before publishing the GitHub Release. +## LLM guide sync — MANDATORY + +`docs/llm-guide.md` is the authoritative LLM reference for the framework API. +It must stay in sync with the live token set. The CI gate `check:llm-guide` +enforces this mechanically — but the gate only catches **renamed or deleted +tokens**. You are responsible for the qualitative layer: + +**Any PR that touches `core/*.css`, `optional/*.css`, or `token-registry.json` +must also review `docs/llm-guide.md` and update it if needed.** + +Changes that always require a guide update: +- New PUBLIC or PUBLIC-ADVANCED token added → add it to the relevant section +- Token renamed or deleted → the CI gate will catch stale refs; fix them +- New token role, tier, or behaviour documented in a token's description + +Changes that may require a guide update: +- Default value changed for a widely-used knob +- New layout primitive or macro added +- New browser support floor or feature gating change + +After any token-touching PR, verify with: + +```bash +npm run check:llm-guide # must pass — CI fails if it doesn't +``` + ## Tests ```bash diff --git a/docs/llm-guide.md b/docs/llm-guide.md index c0499663..2052058b 100644 --- a/docs/llm-guide.md +++ b/docs/llm-guide.md @@ -121,7 +121,7 @@ Slashed requires modern CSS features. Effective minimum: | Chrome / Edge | 125+ | CSS `pow()` in `calc()` (generative scale) | | Safari | 18.0+ | `oklch(from …)` relative color + `light-dark()` | | Firefox | 129+ | `color-mix()` + relative color syntax | -| Chrome / Edge | 138+ (for auto-contrast) | `sign()` in `oklch()` for `--sf-color-text--on-*` | +| Chrome / Edge | 138+ (for auto-contrast) | `sign()` in `oklch()` for `--sf-color-text--on-{family}` | **Graceful degradation:** color tokens live inside `@supports` blocks, so older engines receive sensible fallbacks (the `initial-value` from `@property`). The generative fluid scale requires CSS `pow()` — without it the token resolves to its `@property` initial value. @@ -297,9 +297,6 @@ Auto-switch between light and dark modes. These are the tokens you reference in --sf-color-code-bg /* Inline code background — var(--sf-color-inset) */ --sf-color-code-text /* Inline code text — auto-contrasts with code bg */ -/* Per-block overrides (for .sf-code-block or your own classes) */ ---sf-color-code-block-bg ---sf-color-code-block-text ``` ### 5.10 Semantic shade aliases @@ -517,7 +514,7 @@ Not applied automatically — opt in: `line-height: var(--sf-text-xl-line-height ### 6.8 Heading aliases h1–h6 -Pattern: `--sf-h{N}-{property}` +Pattern: `--sf-h1-{property}` through `--sf-h6-{property}` ```css --sf-h1-size: var(--sf-text-4xl) @@ -955,8 +952,6 @@ Used by `.is-active`, `.is-current`, etc. in `core/states.css`. Allow components ```css --sf-field-required-marker: " *" /* required field marker */ --sf-link-external-marker: " ↗" /* marker for .sf-link-external */ ---sf-field-border-color: var(--sf-color-border) /* scoped override for fields */ ---sf-field-text-color: var(--sf-color-text) /* scoped override */ ``` Component tokens from `optional/tokens.components.css`: @@ -1296,8 +1291,8 @@ a:visited { color: var(--sf-color-link--visited); } /* Correct */ .spacious-section { padding-block: calc(var(--sf-section-pad) * 1.5); } -/* Wrong */ -:root { --sf-section-pad-xl-plus: 8rem; } +/* Wrong — don't invent new tokens; compose with calc() instead */ +:root { --my-section-pad-xl-plus: 8rem; } ``` **5. `knob` tokens are for setting; `consumption` tokens are for using.** diff --git a/package.json b/package.json index f4057e63..4d21ef0f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "docs": "node scripts/audit.js && node scripts/gen-token-reference.js && node scripts/gen-class-reference.js && node scripts/gen-token-index.js && node scripts/gen-api-index.js && npm run gen:registry && npm run configurator:sync", "gen:registry": "node scripts/gen-token-registry.js", "check:registry": "node scripts/check-token-registry.js", + "check:llm-guide": "node scripts/check-llm-guide.js", "configurator:sync": "node configurator/scripts/sync-api.mjs", "docs:tokens": "node scripts/gen-token-reference.js", "docs:index": "node scripts/gen-token-index.js", diff --git a/scripts/check-llm-guide.js b/scripts/check-llm-guide.js new file mode 100644 index 00000000..ca71c5fb --- /dev/null +++ b/scripts/check-llm-guide.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * CI gate: docs/llm-guide.md must stay in sync with the live token registry. + * + * Check 1 (hard fail): every --sf-* name mentioned in the guide must exist in + * token-registry.json as a live (non-removed) token. A stale reference means + * the guide documents a renamed or deleted token. + * + * Check 2 (warning): PUBLIC and PUBLIC-ADVANCED *knob* tokens absent from the + * guide are reported so authors know what coverage gaps exist. This is a + * warning, not a failure — the guide is intentionally curated, not exhaustive. + * + * Run: + * node scripts/check-llm-guide.js # check only + * npm run check:llm-guide # same via npm + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(import.meta.dirname, '..'); +const GUIDE = path.join(ROOT, 'docs', 'llm-guide.md'); +const REGISTRY = path.join(ROOT, 'token-registry.json'); +const API_INDEX = path.join(ROOT, 'docs', 'api-index.json'); + +function readJson(file) { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')); + } catch (err) { + console.error(`check:llm-guide FAILED: cannot read ${file} (${err.message})`); + process.exit(1); + } +} + +if (!fs.existsSync(GUIDE)) { + console.error('check:llm-guide FAILED: docs/llm-guide.md not found.'); + process.exit(1); +} + +const guideText = fs.readFileSync(GUIDE, 'utf8'); +const registry = readJson(REGISTRY); +const apiIndex = readJson(API_INDEX); + +// Live token names (not flagged removed). +const liveTokens = new Set( + registry.tokens.filter((t) => !t.removed).map((t) => t.name), +); + +// PUBLIC + PUBLIC-ADVANCED knob tokens — the ones most likely to need docs. +const publicKnobs = new Set( + (Array.isArray(apiIndex.entries) ? apiIndex.entries : []) + .filter((e) => e.type === 'token' && e.role === 'knob' && + (e.tier === 'PUBLIC' || e.tier === 'PUBLIC-ADVANCED')) + .map((e) => e.name), +); + +// Extract every --sf-* token name from the guide. Exclude bare prefixes (names +// that end with a hyphen) — those are explanatory glob-like patterns in prose, +// not actual token references (e.g. "--sf-color-text--on-{family}"). +const TOKEN_RE = /--sf-[a-z0-9_-]+/g; +const guideRefs = new Set( + [...guideText.matchAll(TOKEN_RE)] + .map((m) => m[0]) + .filter((name) => !name.endsWith('-')), +); + +// ── Check 1: stale references ──────────────────────────────────────────────── +const stale = [...guideRefs].filter((name) => !liveTokens.has(name)).sort(); + +// ── Check 2: undocumented PUBLIC knob tokens (warning only) ────────────────── +const undocumented = [...publicKnobs].filter((name) => !guideRefs.has(name)).sort(); + +// ── Report ─────────────────────────────────────────────────────────────────── +let failed = false; + +if (stale.length > 0) { + failed = true; + console.error('check:llm-guide FAILED — guide references tokens not in the live registry:'); + for (const name of stale) console.error(` stale: ${name}`); + console.error( + '\nFix: either update the guide to use the current token name, ' + + 'or remove the reference if the token was deleted.', + ); +} + +if (undocumented.length > 0) { + console.warn( + `\ncheck:llm-guide WARNING — ${undocumented.length} PUBLIC/PUBLIC-ADVANCED knob token(s) ` + + 'not mentioned in docs/llm-guide.md:', + ); + for (const name of undocumented) console.warn(` undocumented: ${name}`); + console.warn('\nThese may or may not need guide entries — review and add as appropriate.'); +} + +if (failed) { + process.exit(1); +} + +console.log( + `check:llm-guide OK — ${guideRefs.size} token refs validated, ` + + `${undocumented.length} undocumented PUBLIC knobs (warning only).`, +); From a3dd3e49a64899b5ea392434bfdf543966a93566 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 13:44:59 +0000 Subject: [PATCH 2/2] fix(docs): restore scoped override tokens; extend live set to CSS source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stale check incorrectly excluded --sf-field-border-color, --sf-field-text-color, --sf-color-code-block-bg, and --sf-color-code-block-text — these are real PUBLIC scoped override hooks declared and consumed in core/states.css, optional/forms.css, and core/base.css, but absent from token-registry.json (the registry only covers catalogued tokens, not all framework-level properties). Fix: extend the live token set to also include any --sf-* custom property that appears as a declaration in core/ or optional/ CSS files. This ensures scoped override hooks that haven't yet been added to the registry don't trigger false stale failures. Also re-adds the four scoped override tokens to docs/llm-guide.md with accurate descriptions of their scoping purpose. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01GBbqrooCfW68LAjUMuNnYY --- docs/llm-guide.md | 7 +++++++ scripts/check-llm-guide.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/llm-guide.md b/docs/llm-guide.md index 2052058b..5a26a620 100644 --- a/docs/llm-guide.md +++ b/docs/llm-guide.md @@ -297,6 +297,9 @@ Auto-switch between light and dark modes. These are the tokens you reference in --sf-color-code-bg /* Inline code background — var(--sf-color-inset) */ --sf-color-code-text /* Inline code text — auto-contrasts with code bg */ +/* Scoped override hooks for code blocks — set these on .sf-code-block or your own class */ +--sf-color-code-block-bg /* falls back to --sf-color-code-bg when unset */ +--sf-color-code-block-text /* falls back to inherit when unset */ ``` ### 5.10 Semantic shade aliases @@ -952,6 +955,10 @@ Used by `.is-active`, `.is-current`, etc. in `core/states.css`. Allow components ```css --sf-field-required-marker: " *" /* required field marker */ --sf-link-external-marker: " ↗" /* marker for .sf-link-external */ + +/* Scoped override hooks — set per-field/form to override global borders/text */ +--sf-field-border-color /* set by validation states (error/success/warning/info/danger) */ +--sf-field-text-color /* set by validation states for text color feedback */ ``` Component tokens from `optional/tokens.components.css`: diff --git a/scripts/check-llm-guide.js b/scripts/check-llm-guide.js index ca71c5fb..57481c20 100644 --- a/scripts/check-llm-guide.js +++ b/scripts/check-llm-guide.js @@ -1,15 +1,23 @@ #!/usr/bin/env node /** - * CI gate: docs/llm-guide.md must stay in sync with the live token registry. + * CI gate: docs/llm-guide.md must stay in sync with the live token set. * - * Check 1 (hard fail): every --sf-* name mentioned in the guide must exist in - * token-registry.json as a live (non-removed) token. A stale reference means - * the guide documents a renamed or deleted token. + * Check 1 (hard fail): every --sf-* name mentioned in the guide must exist as + * a live token. The live set is the union of: + * a) token-registry.json (non-removed entries) — the catalogued public API + * b) --sf-* custom property declarations in core/ and optional/ CSS files — + * catches scoped override hooks (e.g. --sf-field-border-color) that are + * actively used in the framework but not yet catalogued in the registry. * * Check 2 (warning): PUBLIC and PUBLIC-ADVANCED *knob* tokens absent from the * guide are reported so authors know what coverage gaps exist. This is a * warning, not a failure — the guide is intentionally curated, not exhaustive. * + * Note on shorthand notation: the guide uses compact forms like + * "--sf-animation-fade-in / -fade-out". Only the first fully-prefixed name in + * each group is validated by Check 1. Shorthand suffixes (/ -foo) are not + * individually checked — authors must verify those manually when renaming tokens. + * * Run: * node scripts/check-llm-guide.js # check only * npm run check:llm-guide # same via npm @@ -41,11 +49,24 @@ const guideText = fs.readFileSync(GUIDE, 'utf8'); const registry = readJson(REGISTRY); const apiIndex = readJson(API_INDEX); -// Live token names (not flagged removed). +// Live set a): token-registry.json (non-removed entries). const liveTokens = new Set( registry.tokens.filter((t) => !t.removed).map((t) => t.name), ); +// Live set b): any --sf-* declared as a custom property in CSS source files. +// Matches " --sf-foo:" (declaration) and "--sf-foo," / "--sf-foo)" in +// comment-listed token inventories in tokens.css headers. +const CSS_DECL_RE = /--sf-[a-z0-9_-]+(?=\s*[:,)])/g; +const cssDirs = [path.join(ROOT, 'core'), path.join(ROOT, 'optional')]; +for (const dir of cssDirs) { + if (!fs.existsSync(dir)) continue; + for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.css'))) { + const text = fs.readFileSync(path.join(dir, file), 'utf8'); + for (const m of text.matchAll(CSS_DECL_RE)) liveTokens.add(m[0]); + } +} + // PUBLIC + PUBLIC-ADVANCED knob tokens — the ones most likely to need docs. const publicKnobs = new Set( (Array.isArray(apiIndex.entries) ? apiIndex.entries : [])