Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
20 changes: 11 additions & 9 deletions docs/llm-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -297,9 +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 */

/* Per-block overrides (for .sf-code-block or your own classes) */
--sf-color-code-block-bg
--sf-color-code-block-text
/* 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
Expand Down Expand Up @@ -517,7 +517,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)
Expand Down Expand Up @@ -955,8 +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 */
--sf-field-border-color: var(--sf-color-border) /* scoped override for fields */
--sf-field-text-color: var(--sf-color-text) /* scoped override */

/* 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`:
Expand Down Expand Up @@ -1296,8 +1298,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.**
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
123 changes: 123 additions & 0 deletions scripts/check-llm-guide.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env node
/**
* 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 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
*/

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 set a): token-registry.json (non-removed entries).
const liveTokens = new Set(
registry.tokens.filter((t) => !t.removed).map((t) => t.name),
Comment on lines +53 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include live scoped tokens outside the registry

Using token-registry.json as the sole source of live tokens makes the new gate reject supported scoped override hooks that are not catalogued there. For example, core/tokens.css lists --sf-color-code-block-bg, --sf-color-code-block-text, --sf-field-border-color, and --sf-field-text-color as PUBLIC scoped override tokens, and core/base.css/optional/forms.css still consume them, but they are absent from the registry; this already forced their removal from the LLM guide and will block re-documenting those supported APIs. The live set needs to include these non-registered public hooks or the registry generator needs to catalogue them before this hard-fail check runs.

Useful? React with 👍 / 👎.

);

// 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 : [])
.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('-')),
Comment on lines +81 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expand shorthand token references before validation

The extractor only records the first fully prefixed token, so existing guide shorthand such as --sf-animation-fade-in / -fade-out, --sf-grid-min-xs / -s / -m, and --sf-safe-top / -bottom leaves the suffixed tokens out of both the stale-token check and the coverage calculation. If one of those documented shorthand tokens is renamed or removed, CI still passes because the reference is never validated, while the warning list also incorrectly reports several documented knobs as missing.

Useful? React with 👍 / 👎.

);

// ── 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).`,
);