baldart 4.76.0 → 4.77.0
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/CHANGELOG.md +30 -0
- package/VERSION +1 -1
- package/framework/.claude/skills/design-system-init/SKILL.md +36 -7
- package/framework/.claude/skills/design-system-init/scripts/compile-ds-cards.mjs +2 -1
- package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +17 -5
- package/framework/.claude/skills/design-system-init/scripts/extract-ts.mjs +302 -0
- package/framework/.claude/skills/design-system-init/scripts/render-manifest.mjs +26 -9
- package/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs +4 -2
- package/framework/.claude/skills/ds-edit/SKILL.md +2 -1
- package/framework/.claude/skills/ds-new/SKILL.md +2 -2
- package/framework/.claude/skills/ds-render/SKILL.md +5 -1
- package/framework/agents/component-manifest-schema.md +24 -3
- package/framework/agents/design-system-protocol.md +1 -1
- package/framework/docs/COMPONENT-MANIFEST-LAYER.md +6 -3
- package/framework/templates/component-spec.template.md +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.77.0] - 2026-06-29
|
|
9
|
+
|
|
10
|
+
**TS-aware design-system extractor + the `variant_prop` HEAD field.** The component-manifest extractor was pure-regex (zero-dep) and could not resolve TypeScript: a `type Variant = 'a'|'b'; variant: Variant` captured the alias *name* `"Variant"`, not its members, so on a TS-strict consumer (mayo) every HEAD came out with `variants: []` and ~25 with `props: {}`. Since the HEAD is the SSOT every UI agent reads for discovery, `variants: []` on a 6-variant Button induces wrong code. Separately, the variant axis was **hardcoded** to the prop name `variant` in three places — wrong for a `Chip`/`Pill` whose axis is `tone`. This release makes the extractor **optionally TS-aware** (it borrows the consumer's own `typescript`, resolved from the repo root) and adds a deterministic **`variant_prop`** field naming the real axis.
|
|
11
|
+
|
|
12
|
+
**MINOR** — adds capability + a new deterministic HEAD field. **No new `baldart.config.yml` key** (rides on `features.has_design_system`), so the schema-change propagation rule does NOT apply. **Zero regression**: when typescript is absent (Codex / greenfield / non-TS stack) or a file fails to parse, the extractor falls back to the existing zero-dep regex path per file; legacy HEADs (pre-v4.77.0, no `variant_prop`) default the render axis to `'variant'` so existing registries render identically until a one-time re-extract backfills the field.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`extract-ts.mjs`** (`framework/.claude/skills/design-system-init/scripts/extract-ts.mjs`) — optional TS-aware resolver. Probes `typescript` via `require.resolve('typescript', { paths: [process.cwd()] })` (the **consumer's** dependency, not BALDART's), parses syntactically with `ts.createSourceFile` (no tsconfig / Program / TypeChecker — fast, in-file, deterministic), and resolves type **aliases**, **unions**, **intersections**, **`Omit`/`Pick`**, interface `extends`, and `forwardRef`/`memo`/`React.FC` wrappers. Opaque / imported types (`ComponentPropsWithoutRef<'button'>`, `HTMLAttributes`) stay **empty** — never a DOM-attribute dump. Per-file try/catch → regex fallback. The typescript-free `computeVariantProp` / `pureStringLiteralUnion` / `literalMembers` are **shared** with the regex path so the axis ranking is byte-identical on both.
|
|
17
|
+
- **`variant_prop` deterministic HEAD field** — which prop IS the variant axis, ranked `variant > tone > intent > kind > appearance > state > status > color > severity` (else the sole literal-union prop, else `''`). `variants` is the literal members of `variant_prop`'s type. Emitted by `serialize-spec.mjs`, consumed as the axis key by `render-manifest.mjs` (`entriesFor`) and `compile-ds-cards.mjs` (row label) instead of the hardcoded `'variant'`. Documented in `component-manifest-schema.md` (SSOT), the canonical `component-spec.template.md`, the `/design-system-init` `/ds-new` `/ds-edit` skills, `COMPONENT-MANIFEST-LAYER.md`, and `design-system-protocol.md`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`render-manifest.mjs parseHead` now reads block-style `variants`** — the serializer emits non-empty `variants` as a YAML **block list** (`variants:\n - a`), but `parseHead` only matched flow `variants: [a, b]`, so on any real serialized spec the variant matrix read `[]` (every component collapsed to a single `--default` entry). It now accepts both block and flow forms; with `variant_prop` this makes the render/publish axis work end-to-end (a `Chip` mounts `{ tone }` per variant).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **`extract-manifest.mjs extractFile`** — TS-path-first with per-file regex fallback; emits `variant_prop`; `extractVariants(props)` generalized to `extractVariantsFor(props, variantProp)` (the axis is no longer hardcoded to `variant`).
|
|
26
|
+
- **`/design-system-init` UPGRADE mode** (`SKILL.md`) — a one-line **pre-v4.77.0 backfill** note: re-extract all components ignoring `source_sha` once to populate `variant_prop` + resolve aliases, agentic fields preserved by the existing non-clobber rule.
|
|
27
|
+
|
|
28
|
+
## [4.76.1] - 2026-06-26
|
|
29
|
+
|
|
30
|
+
**`/design-system-init` offers a render surface (opt-in Storybook scaffold).** The render harness (Stage C) needs a Storybook, and a registry-only project has none — so the harness was a silent no-op with nowhere to go. Now `/design-system-init`, after scaffolding the registry, OFFERS (default-NO, JS/TS stacks only) to run the **official `npx storybook init`** so the harness has a surface. It **never hand-writes a per-stack render surface and never auto-wires the project's providers** — that "auto-provider-shim" is the refuted false-fidelity trap; instead it leaves a `.storybook/preview` decorator TODO so **the user owns the context-shimming** (theme/i18n/router), which is how Storybook does it correctly. `/ds-render`'s no-op message now points here. Declined / Codex / non-JS → skipped (publish still works via static-spec cards).
|
|
31
|
+
|
|
32
|
+
**PATCH** — opt-in, no behaviour change for projects that decline or already have Storybook; no new config key.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- **`/design-system-init` step 7b** (`framework/.claude/skills/design-system-init/SKILL.md`) — opt-in `npx storybook init` offer + the user-owned-decorator TODO; cross-ref `/ds-render`.
|
|
36
|
+
- **`/ds-render` no-Storybook message** (`framework/.claude/skills/ds-render/SKILL.md`) — points to the init offer instead of a dead no-op.
|
|
37
|
+
|
|
8
38
|
## [4.76.0] - 2026-06-26
|
|
9
39
|
|
|
10
40
|
**Closed-loop design system: BALDART ⇄ Claude Design.** BALDART owns the design system; Claude Design is where designers draw mockups. The two **diverge** over time — v4.75 made every new mockup born on the project's tokens, but that alignment decays as the in-repo registry evolves and the satellite stays frozen. This release adds the machinery to keep them in sync with **ONE authority** (the in-repo registry — `.tokens.json` + `components/<Name>.md` HEAD + INDEX/Selection-Policy + `ds-gate`); Claude Design is a **satellite mirror**, never a 2nd SSOT. Planned via a multi-agent workflow (map → per-axis design → adversarial refutation → completeness/SSOT critic → synthesis) which **refuted** the obvious-but-wrong pieces before any code.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.77.0
|
|
@@ -104,10 +104,19 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
104
104
|
extractor (`scripts/extract-manifest.mjs` via Bash) over
|
|
105
105
|
`${paths.components_primitives}` (+ `${paths.components_root}/shared`) to read
|
|
106
106
|
from TS types: exported name, `source`, `source_sha`, `props`
|
|
107
|
-
({type,required,default}), `
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
({type,required,default}), `variant_prop` (which prop is the variant axis,
|
|
108
|
+
ranked), `variants` (literal members of the axis), `composes` (in-registry
|
|
109
|
+
imports), `category` (import-fan-in heuristic). When `typescript` is resolvable
|
|
110
|
+
from the consumer root the extractor is **TS-aware** (resolves aliases / unions /
|
|
111
|
+
intersections / `Omit` / `Pick`); else it falls back to regex per file. **This
|
|
112
|
+
half runs with NO subagent** — it is the Codex-portable path. Empty results on a
|
|
113
|
+
non-TS stack are acceptable (advisory), never a failure.
|
|
114
|
+
- **Pre-v4.77.0 backfill (UPGRADE):** a registry whose HEADs were generated
|
|
115
|
+
before v4.77.0 lacks `variant_prop` and may have unresolved aliases. Re-extract
|
|
116
|
+
ALL components **ignoring `source_sha`** once (it is cheap + deterministic),
|
|
117
|
+
then re-serialize — non-empty agentic fields are preserved by the existing
|
|
118
|
+
non-clobber rule. The render harness defaults the axis to `'variant'` until
|
|
119
|
+
then, so an un-backfilled registry still renders correctly.
|
|
111
120
|
4. **Enrich — AGENTIC, AS DATA (never hand-write YAML).** Launch `doc-reviewer`
|
|
112
121
|
to produce the agentic fields (`purpose`, `a11y`, `token_bindings`, `related`,
|
|
113
122
|
`must_rules`, **plus the selection-policy fields `canonical_for` / `use_when` /
|
|
@@ -234,9 +243,28 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
234
243
|
`paths.design_system: <target>`, and (when tokens bootstrapped)
|
|
235
244
|
`paths.design_tokens` + `design_tokens.outputs`. Use the same YAML write path
|
|
236
245
|
as `configure` (preserve user values, never clobber unrelated keys).
|
|
246
|
+
7b. **Offer a render surface — OPT-IN, never forced (closed-loop Stage C enabler).**
|
|
247
|
+
A registry documents components but cannot RENDER them in isolation — the render
|
|
248
|
+
harness (`baldart render` / `/ds-render`) needs a Storybook. If **no `.storybook/`
|
|
249
|
+
exists** AND the stack is JS/TS (`stack.framework` is react/vue/svelte/etc.),
|
|
250
|
+
ASK the user (one consolidated question, default-NO): *"Vuoi che inizializzi
|
|
251
|
+
Storybook così il render harness può montare i primitivi in isolamento? (serve
|
|
252
|
+
per la verifica di qualità isolata e per il render dei componenti)"*. On YES:
|
|
253
|
+
- **Delegate to the official scaffolder** — run `npx storybook init` (stack-aware;
|
|
254
|
+
installs the right `@storybook/<framework>` devDeps + writes `.storybook/main`
|
|
255
|
+
+ `preview`). NEVER hand-write a per-stack render surface and NEVER auto-wire
|
|
256
|
+
the project's providers — that "auto-provider-shim" is the REFUTED false-fidelity
|
|
257
|
+
trap. Instead leave a **TODO in `.storybook/preview`**: *"add your ThemeProvider
|
|
258
|
+
/ i18n provider / router here as a decorator so isolated primitives render with
|
|
259
|
+
real context"* — **the user owns the decorators** (that is how Storybook does the
|
|
260
|
+
context-shimming correctly).
|
|
261
|
+
- On Codex / non-JS stack / user declines → skip silently; the harness simply
|
|
262
|
+
stays a no-op (publish still works via static-spec cards). This is an offer, not
|
|
263
|
+
a requirement.
|
|
237
264
|
8. **Print next-steps.** Specs needing agentic review (empty fields), the
|
|
238
265
|
round-trip token diff if non-empty, suggested follow-up (`/design-review` on a
|
|
239
|
-
key route)
|
|
266
|
+
key route); if Storybook was initialized, the `.storybook/preview` decorator TODO
|
|
267
|
+
+ `/ds-render` to render the primitives.
|
|
240
268
|
|
|
241
269
|
## Workflow
|
|
242
270
|
|
|
@@ -249,8 +277,9 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
249
277
|
node .framework/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs \
|
|
250
278
|
--roots <primitives>,<shared> --json
|
|
251
279
|
```
|
|
252
|
-
It emits, per component: `name`, `source`, `source_sha`, `props`, `
|
|
253
|
-
`composes`, `category
|
|
280
|
+
It emits, per component: `name`, `source`, `source_sha`, `props`, `variant_prop`,
|
|
281
|
+
`variants`, `composes`, `category` (TS-aware when typescript is resolvable; regex
|
|
282
|
+
fallback otherwise). Empty fields on a non-TS stack are advisory, not errors.
|
|
254
283
|
|
|
255
284
|
3. **Agentic enrichment (`doc-reviewer`).** Fill `purpose`, `a11y`,
|
|
256
285
|
`token_bindings`, `related`, `must_rules`, and the selection-policy fields
|
|
@@ -64,7 +64,8 @@ function loadTokens(file) {
|
|
|
64
64
|
function isColor(v) { return /^#([0-9a-f]{3,8})$|^(rgb|hsl|oklch|color)\(/i.test(String(v).trim()); }
|
|
65
65
|
|
|
66
66
|
function card({ spec, group, tokens }) {
|
|
67
|
-
const
|
|
67
|
+
const axisLabel = spec.variant_prop || 'variant';
|
|
68
|
+
const variantRows = (spec.variants.length ? spec.variants : ['—']).map((v) => `<tr><td>${esc(axisLabel)}</td><td><code>${esc(v)}</code></td></tr>`).join('');
|
|
68
69
|
const propRows = Object.entries(spec.props || {}).map(([p, d]) => `<tr><td><code>${esc(p)}</code></td><td><code>${esc(d.type)}</code></td><td>${esc(d.default)}</td></tr>`).join('');
|
|
69
70
|
// Token swatches the component declares (token_bindings would be richer; v1 shows the project palette subset by colour).
|
|
70
71
|
const swatches = Object.entries(tokens).filter(([, v]) => isColor(v)).slice(0, 24)
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { TS_AVAILABLE, extractPropsTs, computeVariantProp } from './extract-ts.mjs';
|
|
22
23
|
|
|
23
24
|
function arg(name, def = '') {
|
|
24
25
|
const i = process.argv.indexOf(name);
|
|
@@ -97,9 +98,9 @@ function extractProps(src, name) {
|
|
|
97
98
|
return props;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
/** Variants = literal union of the
|
|
101
|
-
function
|
|
102
|
-
const v = props
|
|
101
|
+
/** Variants = literal union of the variant-axis prop (`variant_prop`). */
|
|
102
|
+
function extractVariantsFor(props, variantProp) {
|
|
103
|
+
const v = variantProp && props[variantProp] && props[variantProp].type;
|
|
103
104
|
if (!v) return [];
|
|
104
105
|
const lits = [...v.matchAll(/'([^']+)'|"([^"]+)"/g)].map((m) => m[1] || m[2]);
|
|
105
106
|
return lits;
|
|
@@ -129,7 +130,17 @@ export function extractFile(file) {
|
|
|
129
130
|
try { src = fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
130
131
|
const name = exportedName(src, file);
|
|
131
132
|
if (!name) return null;
|
|
132
|
-
|
|
133
|
+
// TS-aware path first (resolves aliases/unions/intersections/Omit/Pick); on any
|
|
134
|
+
// miss it returns null and we fall back to the zero-dep regex path for THIS file.
|
|
135
|
+
let props, variantProp, variants;
|
|
136
|
+
const tsOut = TS_AVAILABLE ? extractPropsTs(src, name, file) : null;
|
|
137
|
+
if (tsOut) {
|
|
138
|
+
({ props, variantProp, variants } = tsOut);
|
|
139
|
+
} else {
|
|
140
|
+
props = extractProps(src, name);
|
|
141
|
+
variantProp = computeVariantProp(props); // shared rule — identical on both paths
|
|
142
|
+
variants = extractVariantsFor(props, variantProp);
|
|
143
|
+
}
|
|
133
144
|
return {
|
|
134
145
|
name,
|
|
135
146
|
source: path.relative(process.cwd(), file),
|
|
@@ -137,7 +148,8 @@ export function extractFile(file) {
|
|
|
137
148
|
status: 'stable',
|
|
138
149
|
category: 'primitive',
|
|
139
150
|
props,
|
|
140
|
-
|
|
151
|
+
variant_prop: variantProp,
|
|
152
|
+
variants,
|
|
141
153
|
composes: extractComposes(src).filter((c) => c !== name),
|
|
142
154
|
};
|
|
143
155
|
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// TS-aware component-manifest extraction (since v4.77.0).
|
|
3
|
+
//
|
|
4
|
+
// OPTIONAL companion to the zero-dep regex extractor (extract-manifest.mjs). It
|
|
5
|
+
// resolves TypeScript type aliases / unions / intersections / Omit / Pick in a
|
|
6
|
+
// component's prop type — the cases the pure-regex path cannot see
|
|
7
|
+
// (`type Variant = 'a'|'b'; variant: Variant` captures the alias NAME, not its
|
|
8
|
+
// members). The fix is SYNTACTIC: it parses the file with `ts.createSourceFile`
|
|
9
|
+
// (no Program / TypeChecker / tsconfig — fast, in-file, deterministic) and walks
|
|
10
|
+
// the AST. Opaque / imported types (`ComponentPropsWithoutRef<'button'>`,
|
|
11
|
+
// `HTMLAttributes`) stay EMPTY — we never dump DOM attributes.
|
|
12
|
+
//
|
|
13
|
+
// Reliability contract (the whole reason this is a separate, probed module):
|
|
14
|
+
// - `typescript` is the CONSUMER's dependency, not BALDART's. It is resolved
|
|
15
|
+
// from `process.cwd()` (the repo root the extractor runs against), NOT from
|
|
16
|
+
// this script's location inside `.framework/` where it is absent.
|
|
17
|
+
// - When typescript is unavailable (Codex / greenfield / non-TS stack) OR the
|
|
18
|
+
// file fails to parse/traverse, `extractPropsTs` returns `null` and the
|
|
19
|
+
// caller falls back to the regex path for THAT file — zero regression.
|
|
20
|
+
// - `computeVariantProp` / `pureStringLiteralUnion` / `literalMembers` carry NO
|
|
21
|
+
// typescript dependency and are SHARED with the regex path, so the
|
|
22
|
+
// variant-axis ranking rule is byte-identical on both paths (determinism).
|
|
23
|
+
//
|
|
24
|
+
// This module imports NOTHING from extract-manifest.mjs (one-way, no cycle).
|
|
25
|
+
// Schema: framework/agents/component-manifest-schema.md.
|
|
26
|
+
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
// --- typescript probe: resolve from the CONSUMER root (process.cwd()), not from
|
|
32
|
+
// this script's location inside .framework/ (where typescript is not installed). ---
|
|
33
|
+
let ts = null;
|
|
34
|
+
try {
|
|
35
|
+
ts = require(require.resolve('typescript', { paths: [process.cwd()] }));
|
|
36
|
+
} catch (_) {
|
|
37
|
+
ts = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when the consumer has `typescript` resolvable; else the TS path no-ops. */
|
|
41
|
+
export const TS_AVAILABLE = !!ts;
|
|
42
|
+
|
|
43
|
+
// === Shared, typescript-FREE helpers (used by BOTH the TS and regex paths) ===
|
|
44
|
+
|
|
45
|
+
/** Variant-axis ranking: which prop name wins when several are literal unions. */
|
|
46
|
+
const RANK = ['variant', 'tone', 'intent', 'kind', 'appearance', 'state', 'status', 'color', 'severity'];
|
|
47
|
+
|
|
48
|
+
/** A type text that is a pure union (or single) of string literals: `'a' | 'b'`. */
|
|
49
|
+
export function pureStringLiteralUnion(typeText) {
|
|
50
|
+
if (!typeText) return false;
|
|
51
|
+
const parts = String(typeText).split('|').map((s) => s.trim()).filter(Boolean);
|
|
52
|
+
if (!parts.length) return false;
|
|
53
|
+
return parts.every((p) => /^'[^']*'$/.test(p) || /^"[^"]*"$/.test(p));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The string-literal members of a union type text, in source order. */
|
|
57
|
+
export function literalMembers(typeText) {
|
|
58
|
+
if (!typeText) return [];
|
|
59
|
+
return [...String(typeText).matchAll(/'([^']*)'|"([^"]*)"/g)].map((m) => (m[1] !== undefined ? m[1] : m[2]));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The variant-axis prop name for a props map. A candidate is any prop whose
|
|
64
|
+
* resolved type is a pure string-literal union; the winner is the first by RANK,
|
|
65
|
+
* else the sole candidate, else '' (no resolvable axis). This is the single rule
|
|
66
|
+
* shared by the TS and regex paths — never fork it.
|
|
67
|
+
*/
|
|
68
|
+
export function computeVariantProp(props) {
|
|
69
|
+
if (!props || typeof props !== 'object') return '';
|
|
70
|
+
const candidates = Object.keys(props).filter((k) => pureStringLiteralUnion(props[k] && props[k].type));
|
|
71
|
+
if (!candidates.length) return '';
|
|
72
|
+
for (const r of RANK) if (candidates.includes(r)) return r;
|
|
73
|
+
return candidates.length === 1 ? candidates[0] : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// === TS path (no-ops entirely when typescript is unavailable) ===
|
|
77
|
+
|
|
78
|
+
const MAX_DEPTH = 8; // alias-recursion cap (with `seen`, prevents pathological/cyclic blowups)
|
|
79
|
+
|
|
80
|
+
/** Map of top-level type aliases + interfaces declared IN this file (by name). */
|
|
81
|
+
function buildDeclMap(sf) {
|
|
82
|
+
const map = new Map();
|
|
83
|
+
for (const st of sf.statements) {
|
|
84
|
+
if ((ts.isTypeAliasDeclaration(st) || ts.isInterfaceDeclaration(st)) && st.name) {
|
|
85
|
+
map.set(st.name.text, st);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return map;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function paramTypeOfFunction(fn) {
|
|
92
|
+
return (fn.parameters && fn.parameters[0] && fn.parameters[0].type) || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** React.FC<P> / FC<P> / FunctionComponent<P> → the first type argument (P). */
|
|
96
|
+
function propsFromComponentTypeAnnotation(typeNode) {
|
|
97
|
+
if (!ts.isTypeReferenceNode(typeNode) || !typeNode.typeArguments || !typeNode.typeArguments.length) return null;
|
|
98
|
+
const tn = typeNode.typeName;
|
|
99
|
+
const simple = ts.isQualifiedName(tn) ? tn.right.text : tn.text;
|
|
100
|
+
if (simple === 'FC' || simple === 'FunctionComponent') return typeNode.typeArguments[0];
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Locate the exported component `name` and return the TypeNode of its props. */
|
|
105
|
+
function findComponentParamType(sf, name) {
|
|
106
|
+
let fallback = null;
|
|
107
|
+
for (const st of sf.statements) {
|
|
108
|
+
// function Name(props: T) / export [default] function Name(props: T)
|
|
109
|
+
if (ts.isFunctionDeclaration(st)) {
|
|
110
|
+
const isDefault = st.modifiers && st.modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
111
|
+
if ((st.name && st.name.text === name) || (!st.name && isDefault)) {
|
|
112
|
+
const t = paramTypeOfFunction(st);
|
|
113
|
+
if (t) return t;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// export default Name; (anonymous arrow assigned to const handled below)
|
|
118
|
+
if (ts.isVariableStatement(st)) {
|
|
119
|
+
for (const decl of st.declarationList.declarations) {
|
|
120
|
+
if (!decl.name || !ts.isIdentifier(decl.name) || decl.name.text !== name) continue;
|
|
121
|
+
// const Name: React.FC<P> = ...
|
|
122
|
+
if (decl.type) {
|
|
123
|
+
const t = propsFromComponentTypeAnnotation(decl.type);
|
|
124
|
+
if (t) return t;
|
|
125
|
+
}
|
|
126
|
+
const init = decl.initializer;
|
|
127
|
+
if (!init) continue;
|
|
128
|
+
// forwardRef<Ref, Props>(...) / memo<Props>(...)
|
|
129
|
+
if (ts.isCallExpression(init)) {
|
|
130
|
+
const callee = init.expression;
|
|
131
|
+
const calleeName = ts.isPropertyAccessExpression(callee)
|
|
132
|
+
? callee.name.text
|
|
133
|
+
: (ts.isIdentifier(callee) ? callee.text : '');
|
|
134
|
+
if (init.typeArguments && init.typeArguments.length) {
|
|
135
|
+
if (calleeName === 'forwardRef' && init.typeArguments[1]) return init.typeArguments[1];
|
|
136
|
+
if (calleeName === 'memo' && init.typeArguments[0]) return init.typeArguments[0];
|
|
137
|
+
}
|
|
138
|
+
// unwrap the first function argument (forwardRef/memo wrapping a component fn)
|
|
139
|
+
const fn = init.arguments && init.arguments[0];
|
|
140
|
+
if (fn && (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) {
|
|
141
|
+
const t = paramTypeOfFunction(fn);
|
|
142
|
+
if (t) return t;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
146
|
+
const t = paramTypeOfFunction(init);
|
|
147
|
+
if (t) return t;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return fallback;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* If a prop's type is (via single-file type aliases) a PURE string-literal union,
|
|
157
|
+
* return its expanded text (`'a' | 'b'`); else null. This is what makes
|
|
158
|
+
* `variant: Variant` (where `type Variant = 'a'|'b'`) resolve to the members — the
|
|
159
|
+
* core alias-resolution the regex path cannot do. Interfaces can't be a literal
|
|
160
|
+
* union, so only type-alias references are followed.
|
|
161
|
+
*/
|
|
162
|
+
function unionLiteralText(node, ctx, seen) {
|
|
163
|
+
const { declMap } = ctx;
|
|
164
|
+
if (!node) return null;
|
|
165
|
+
if (ts.isParenthesizedTypeNode(node)) return unionLiteralText(node.type, ctx, seen);
|
|
166
|
+
if (ts.isLiteralTypeNode(node) && node.literal && ts.isStringLiteral(node.literal)) return `'${node.literal.text}'`;
|
|
167
|
+
if (ts.isUnionTypeNode(node)) {
|
|
168
|
+
const parts = node.types.map((t) => unionLiteralText(t, ctx, seen));
|
|
169
|
+
return parts.every((p) => p !== null) ? parts.join(' | ') : null;
|
|
170
|
+
}
|
|
171
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
172
|
+
const tn = node.typeName;
|
|
173
|
+
const simple = ts.isQualifiedName(tn) ? tn.right.text : tn.text;
|
|
174
|
+
if (declMap.has(simple) && !seen.has(simple)) {
|
|
175
|
+
const decl = declMap.get(simple);
|
|
176
|
+
if (ts.isTypeAliasDeclaration(decl)) {
|
|
177
|
+
const next = new Set(seen); next.add(simple);
|
|
178
|
+
return unionLiteralText(decl.type, ctx, next);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** PropertySignature members of a TypeLiteral / InterfaceDeclaration body. */
|
|
186
|
+
function membersOf(node, ctx) {
|
|
187
|
+
const { sf } = ctx;
|
|
188
|
+
const out = {};
|
|
189
|
+
const members = node.members || [];
|
|
190
|
+
for (const m of members) {
|
|
191
|
+
if (!ts.isPropertySignature(m) || !m.name) continue;
|
|
192
|
+
const pname = (ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) ? m.name.text : m.name.getText(sf);
|
|
193
|
+
// Resolve aliased literal unions to their members; else keep the raw type text.
|
|
194
|
+
const resolved = m.type ? unionLiteralText(m.type, ctx, new Set()) : null;
|
|
195
|
+
const type = resolved || (m.type ? m.type.getText(sf).replace(/\s+/g, ' ').trim() : 'any');
|
|
196
|
+
out[pname] = { type, required: !m.questionToken };
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** String-literal keys named by an Omit/Pick second argument (`'a'` or `'a'|'b'`). */
|
|
202
|
+
function extractKeyLiterals(node) {
|
|
203
|
+
if (!node) return [];
|
|
204
|
+
if (ts.isLiteralTypeNode(node) && node.literal && ts.isStringLiteral(node.literal)) return [node.literal.text];
|
|
205
|
+
if (ts.isUnionTypeNode(node)) return node.types.flatMap((t) => extractKeyLiterals(t));
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Resolve a TypeNode to `{ [prop]: { type, required } }` — syntactic, in-file only. */
|
|
210
|
+
function resolveType(node, ctx, depth, seen) {
|
|
211
|
+
const { declMap } = ctx;
|
|
212
|
+
if (!node || depth > MAX_DEPTH) return {};
|
|
213
|
+
if (ts.isParenthesizedTypeNode(node)) return resolveType(node.type, ctx, depth + 1, seen);
|
|
214
|
+
|
|
215
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
216
|
+
const out = {};
|
|
217
|
+
// `extends` heritage first (own members override below).
|
|
218
|
+
for (const hc of node.heritageClauses || []) {
|
|
219
|
+
for (const et of hc.types || []) {
|
|
220
|
+
const exprName = et.expression && et.expression.text;
|
|
221
|
+
if (exprName && declMap.has(exprName) && !seen.has(exprName)) {
|
|
222
|
+
const next = new Set(seen); next.add(exprName);
|
|
223
|
+
Object.assign(out, resolveType(declMap.get(exprName), ctx, depth + 1, next));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
Object.assign(out, membersOf(node, ctx));
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (ts.isTypeLiteralNode(node)) return membersOf(node, ctx);
|
|
232
|
+
|
|
233
|
+
if (ts.isIntersectionTypeNode(node)) {
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const t of node.types) {
|
|
236
|
+
const m = resolveType(t, ctx, depth + 1, seen);
|
|
237
|
+
for (const k of Object.keys(m)) {
|
|
238
|
+
out[k] = out[k] ? { type: out[k].type, required: out[k].required || m[k].required } : m[k];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (ts.isUnionTypeNode(node)) {
|
|
245
|
+
const arms = node.types.map((t) => resolveType(t, ctx, depth + 1, seen)).filter((m) => Object.keys(m).length);
|
|
246
|
+
if (!arms.length) return {};
|
|
247
|
+
const out = {};
|
|
248
|
+
for (const k of new Set(arms.flatMap((m) => Object.keys(m)))) {
|
|
249
|
+
const present = arms.filter((m) => m[k]);
|
|
250
|
+
const inAll = present.length === arms.length;
|
|
251
|
+
const requiredInAll = inAll && present.every((m) => m[k].required);
|
|
252
|
+
out[k] = { type: present[0][k].type, required: requiredInAll };
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
258
|
+
const tn = node.typeName;
|
|
259
|
+
const simple = ts.isQualifiedName(tn) ? tn.right.text : tn.text;
|
|
260
|
+
if ((simple === 'Omit' || simple === 'Pick') && node.typeArguments && node.typeArguments.length >= 2) {
|
|
261
|
+
const base = resolveType(node.typeArguments[0], ctx, depth + 1, seen);
|
|
262
|
+
const keys = new Set(extractKeyLiterals(node.typeArguments[1]));
|
|
263
|
+
const out = {};
|
|
264
|
+
for (const k of Object.keys(base)) {
|
|
265
|
+
const inSet = keys.has(k);
|
|
266
|
+
if (simple === 'Omit' ? !inSet : inSet) out[k] = base[k];
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
if (declMap.has(simple) && !seen.has(simple)) {
|
|
271
|
+
const next = new Set(seen); next.add(simple);
|
|
272
|
+
const decl = declMap.get(simple);
|
|
273
|
+
const target = ts.isInterfaceDeclaration(decl) ? decl : decl.type;
|
|
274
|
+
return resolveType(target, ctx, depth + 1, next);
|
|
275
|
+
}
|
|
276
|
+
return {}; // opaque / imported — never enumerate (no DOM-attr dump)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* TS-aware extraction for ONE component. Returns `{ props, variantProp, variants }`
|
|
284
|
+
* on success, or `null` to signal the caller to use the regex path for this file
|
|
285
|
+
* (typescript unavailable, no resolvable param type, or any parse/traversal error).
|
|
286
|
+
*/
|
|
287
|
+
export function extractPropsTs(src, name, fileName) {
|
|
288
|
+
if (!ts) return null;
|
|
289
|
+
try {
|
|
290
|
+
const kind = /\.tsx$/.test(fileName || '') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
291
|
+
const sf = ts.createSourceFile(fileName || 'component.tsx', src, ts.ScriptTarget.Latest, true, kind);
|
|
292
|
+
const typeNode = findComponentParamType(sf, name);
|
|
293
|
+
if (!typeNode) return null; // untyped param → let regex try the `${name}Props` convention
|
|
294
|
+
const props = resolveType(typeNode, { sf, declMap: buildDeclMap(sf) }, 0, new Set());
|
|
295
|
+
if (!props || typeof props !== 'object') return null;
|
|
296
|
+
const variantProp = computeVariantProp(props);
|
|
297
|
+
const variants = variantProp ? literalMembers(props[variantProp].type) : [];
|
|
298
|
+
return { props, variantProp, variants };
|
|
299
|
+
} catch (_) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -45,13 +45,27 @@ function parseHead(md) {
|
|
|
45
45
|
if (!name) return null;
|
|
46
46
|
const source = scalar('source');
|
|
47
47
|
|
|
48
|
-
// variants
|
|
48
|
+
// variants — the enumerated literal union of the variant-axis prop. The
|
|
49
|
+
// serializer emits a BLOCK list for non-empty arrays (`variants:\n - a`) and
|
|
50
|
+
// flow `[]` when empty, so accept BOTH block and flow `[a, b]` here.
|
|
49
51
|
let variants = [];
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
variants =
|
|
52
|
+
const vFlow = head.match(/^variants:\s*\[(.*?)\]/m);
|
|
53
|
+
if (vFlow && vFlow[1].trim()) {
|
|
54
|
+
variants = vFlow[1].split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
55
|
+
} else {
|
|
56
|
+
const vBlock = head.match(/^variants:\s*\n((?:[ \t]+-[ \t]*.+\n?)+)/m);
|
|
57
|
+
if (vBlock) {
|
|
58
|
+
variants = vBlock[1].split('\n')
|
|
59
|
+
.map((l) => { const m2 = l.match(/^\s*-\s*(.+?)\s*$/); return m2 ? m2[1].replace(/^["']|["']$/g, '') : null; })
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
// variant_prop: which prop is the variant axis (since v4.77.0). Legacy HEADs
|
|
65
|
+
// (pre-v4.77.0) lack it → default to 'variant' when variants are present, so
|
|
66
|
+
// existing registries keep the same axis/rendering.
|
|
67
|
+
const variant_prop = scalar('variant_prop') || (variants.length ? 'variant' : '');
|
|
68
|
+
|
|
55
69
|
// props block: each line ` <name>: { type: "...", required: ..., default: ... }`
|
|
56
70
|
const props = {};
|
|
57
71
|
const pm = head.match(/^props:\s*\n([\s\S]*?)(?=^\S|\n# ===|\Z)/m);
|
|
@@ -68,7 +82,7 @@ function parseHead(md) {
|
|
|
68
82
|
props[pname] = { type: strip(typeM && typeM[1]) || '', default: strip(defM && defM[1]) };
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
|
-
return { name, source, variants, props };
|
|
85
|
+
return { name, source, variant_prop, variants, props };
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
// Enumerable iff the prop type is a literal union of string/number/boolean
|
|
@@ -89,7 +103,10 @@ function enumMembers(type) {
|
|
|
89
103
|
|
|
90
104
|
function entriesFor(spec, full) {
|
|
91
105
|
const { name, variants, props } = spec;
|
|
92
|
-
|
|
106
|
+
// The variant axis is the prop named by `variant_prop` (default 'variant' for
|
|
107
|
+
// legacy HEADs) — NOT hardcoded, so a Chip whose axis is `tone` mounts `tone`.
|
|
108
|
+
const axis = spec.variant_prop || (variants.length ? 'variant' : '');
|
|
109
|
+
const others = Object.keys(props).filter((p) => p !== axis);
|
|
93
110
|
const baseFixture = {};
|
|
94
111
|
for (const p of others) if (props[p].default !== undefined && props[p].default !== null) baseFixture[p] = props[p].default;
|
|
95
112
|
|
|
@@ -97,8 +114,8 @@ function entriesFor(spec, full) {
|
|
|
97
114
|
const variantValues = variants.length ? variants : ['default'];
|
|
98
115
|
let entries = variantValues.map((v) => ({
|
|
99
116
|
id: `${name}--${v}`,
|
|
100
|
-
props: { ...(variants.length ? {
|
|
101
|
-
axis: 'variant',
|
|
117
|
+
props: { ...(variants.length && axis ? { [axis]: v } : {}), ...baseFixture },
|
|
118
|
+
axis: axis || 'variant',
|
|
102
119
|
}));
|
|
103
120
|
|
|
104
121
|
if (full) {
|
|
@@ -150,7 +167,7 @@ function main() {
|
|
|
150
167
|
spec = parseHead(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
151
168
|
} catch (_) { spec = null; }
|
|
152
169
|
if (!spec) { skipped.push(file); continue; }
|
|
153
|
-
components.push({ name: spec.name, source: spec.source || null, variants: spec.variants, entries: entriesFor(spec, full) });
|
|
170
|
+
components.push({ name: spec.name, source: spec.source || null, variant_prop: spec.variant_prop || null, variants: spec.variants, entries: entriesFor(spec, full) });
|
|
154
171
|
}
|
|
155
172
|
|
|
156
173
|
emit({
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// Usage:
|
|
17
17
|
// node serialize-spec.mjs --bundle bundle.json --out-dir <dir> [--index <path>] [--brand <name>]
|
|
18
18
|
// bundle.json = { brand?, components: [ { name, source, source_sha, status,
|
|
19
|
-
// category, props:{}, variants:[], composes:[], purpose,
|
|
20
|
-
// a11y, related:[], must_rules:[], last_verified, owner,
|
|
19
|
+
// category, props:{}, variant_prop, variants:[], composes:[], purpose,
|
|
20
|
+
// token_bindings:[], a11y, related:[], must_rules:[], last_verified, owner,
|
|
21
|
+
// prose } ] }
|
|
21
22
|
// Exits non-zero if any emitted HEAD fails to re-parse (when js-yaml is present).
|
|
22
23
|
|
|
23
24
|
import fs from 'node:fs';
|
|
@@ -72,6 +73,7 @@ export function emitHead(c) {
|
|
|
72
73
|
L.push(`status: ${scalar(c.status || 'stable')}`);
|
|
73
74
|
L.push(`category: ${scalar(c.category || 'primitive')}`);
|
|
74
75
|
L.push(propsBlock(c.props));
|
|
76
|
+
L.push(`variant_prop: ${scalar(c.variant_prop ?? '')}`);
|
|
75
77
|
L.push(list('variants', c.variants));
|
|
76
78
|
L.push(list('composes', c.composes));
|
|
77
79
|
L.push('# AGENTIC (curated by doc-reviewer — preserved across regeneration):');
|
|
@@ -110,7 +110,8 @@ Each step is a delegation. Read the cited SSOT; do not paraphrase it.
|
|
|
110
110
|
describe the required source change as a TODO and require the user to apply it.
|
|
111
111
|
3. **Re-extract the deterministic HEAD.**
|
|
112
112
|
`node .framework/framework/.claude/skills/design-system-init/scripts/extract-one.mjs --source <path.tsx> --json`
|
|
113
|
-
→ the updated `props` / `variants` / `composes` / `source_sha`
|
|
113
|
+
→ the updated `props` / `variant_prop` / `variants` / `composes` / `source_sha`
|
|
114
|
+
from the new source (TS-aware: resolves type aliases / unions / `Omit` / `Pick`).
|
|
114
115
|
4. **Re-verify the agentic fields.** DELEGATE `doc-reviewer` to update ONLY what the
|
|
115
116
|
change affects (`must_rules` / `a11y` / `token_bindings` / `purpose`) and keep the
|
|
116
117
|
rest. Existing values are preserved by the serializer; never blank a field you
|
|
@@ -102,8 +102,8 @@ Each step is a delegation. Read the cited SSOT; do not paraphrase it.
|
|
|
102
102
|
path. The skill itself NEVER writes component source.
|
|
103
103
|
3. **Extract the deterministic HEAD.**
|
|
104
104
|
`node .framework/framework/.claude/skills/design-system-init/scripts/extract-one.mjs --source <path.tsx> --json`
|
|
105
|
-
→ `name`/`source`/`source_sha`/`props`/`variants`/`composes`
|
|
106
|
-
Codex-portable, no subagent).
|
|
105
|
+
→ `name`/`source`/`source_sha`/`props`/`variant_prop`/`variants`/`composes`
|
|
106
|
+
(TS-aware when typescript resolves, else zero-dep regex; Codex-portable, no subagent).
|
|
107
107
|
4. **Enrich the agentic fields AS DATA.** DELEGATE `doc-reviewer` (Task tool) to
|
|
108
108
|
produce `purpose` / `token_bindings` (picked from the REAL ids in
|
|
109
109
|
`${paths.design_tokens}`) / `a11y` / `related` / `must_rules` as JSON values —
|
|
@@ -43,7 +43,11 @@ Baseline `medium`. Honor an inline `effort=<level>` override.
|
|
|
43
43
|
|
|
44
44
|
1. **Build the surface.** `baldart render build [--full]` → emits
|
|
45
45
|
`render-manifest.json` (from the component HEADs) + generates the adapter render
|
|
46
|
-
surface (ephemeral `.baldart.stories.tsx`). No Storybook → STOP (clean no-op)
|
|
46
|
+
surface (ephemeral `.baldart.stories.tsx`). **No Storybook → STOP (clean no-op)**
|
|
47
|
+
and tell the user the harness needs a render surface: offer to run
|
|
48
|
+
`/design-system-init` (which proposes `npx storybook init`, opt-in — § step 7b)
|
|
49
|
+
or to `npx storybook init` directly, then re-run `/ds-render`. Never auto-install
|
|
50
|
+
Storybook from this skill.
|
|
47
51
|
2. **Run the screenshot loop** (this skill owns the browser via `playwright-skill`):
|
|
48
52
|
`npx storybook build -o .harness/storybook-static`, then for each story
|
|
49
53
|
`iframe.html?id=<storyId>&viewMode=story` capture a PNG into `.harness/shots/`.
|
|
@@ -51,11 +51,12 @@ source: components/ui/Button.tsx # path relative to repo root
|
|
|
51
51
|
source_sha: a1b2c3d # git blob sha of source at last generation (drift anchor)
|
|
52
52
|
status: stable # stable | experimental | deprecated (default stable; `deprecated` is a hand override)
|
|
53
53
|
category: primitive # primitive | composite | feature (heuristic on import fan-in; hand-overridable)
|
|
54
|
-
props: # from the TS prop type / interface
|
|
54
|
+
props: # from the TS prop type / interface (TS-aware: resolves aliases / unions / intersections / Omit / Pick)
|
|
55
55
|
variant: { type: "'primary'|'ghost'|'destructive'", required: false, default: "'primary'" }
|
|
56
56
|
size: { type: "'sm'|'md'|'lg'", required: false, default: "'md'" }
|
|
57
57
|
disabled: { type: boolean, required: false, default: false }
|
|
58
|
-
|
|
58
|
+
variant_prop: variant # which prop IS the variant axis (ranked; '' if none). NOT hardcoded to `variant` — a Chip whose axis is `tone` reports `tone`.
|
|
59
|
+
variants: [primary, ghost, destructive] # enumerated literal union of the `variant_prop` type
|
|
59
60
|
composes: [Slot, Spinner] # in-registry primitives imported by this component
|
|
60
61
|
# === AGENTIC — curated by doc-reviewer; survives regeneration, never clobbered when non-empty ===
|
|
61
62
|
purpose: "Primary action trigger; one primary per surface."
|
|
@@ -76,7 +77,27 @@ owner: doc-reviewer # curation owner (constant; documents
|
|
|
76
77
|
|
|
77
78
|
| Origin | Fields | Rule |
|
|
78
79
|
|---|---|---|
|
|
79
|
-
| **Deterministic** (TS/git) | `name`, `source`, `source_sha`, `status` (default), `category` (heuristic), `props`, `variants`, `composes` | Always safe to **overwrite** on regeneration — they are derived. Extracted by a pure Node script (no agent), so the path is **portable to Codex**. |
|
|
80
|
+
| **Deterministic** (TS/git) | `name`, `source`, `source_sha`, `status` (default), `category` (heuristic), `props`, `variant_prop`, `variants`, `composes` | Always safe to **overwrite** on regeneration — they are derived. Extracted by a pure Node script (no agent), so the path is **portable to Codex**. |
|
|
81
|
+
|
|
82
|
+
**TS-aware extraction (since v4.77.0).** When `typescript` is resolvable from the
|
|
83
|
+
consumer root, the extractor parses the source syntactically (`ts.createSourceFile`
|
|
84
|
+
— no tsconfig/TypeChecker) and resolves type **aliases**, **unions**,
|
|
85
|
+
**intersections**, and **`Omit`/`Pick`** in the props type (`type V='a'|'b'; variant: V`
|
|
86
|
+
→ `['a','b']`). Opaque / imported types (`ComponentPropsWithoutRef<'button'>`,
|
|
87
|
+
`HTMLAttributes`) stay **empty** — never a DOM-attribute dump. When typescript is
|
|
88
|
+
absent (Codex / greenfield / non-TS stack) or a file fails to parse, it falls back
|
|
89
|
+
to the zero-dep **regex** path for that file (today's behaviour, no regression).
|
|
90
|
+
|
|
91
|
+
**`variant_prop` (the variant axis).** A prop is a variant-axis *candidate* iff its
|
|
92
|
+
resolved type is a pure string-literal union; the winner is the first by the rank
|
|
93
|
+
`variant > tone > intent > kind > appearance > state > status > color > severity`,
|
|
94
|
+
else the sole candidate, else `''`. `variants` is the literal members of
|
|
95
|
+
`variant_prop`'s type. Consumers (render harness, `@dsCard` compiler) use
|
|
96
|
+
`variant_prop` as the axis key — so a `Chip` whose axis is `tone` mounts `tone`,
|
|
97
|
+
not a phantom `variant`. **Legacy HEADs** (pre-v4.77.0, no `variant_prop`) **default
|
|
98
|
+
the axis to `'variant'`** when `variants` is non-empty, so existing registries keep
|
|
99
|
+
rendering identically; a one-time re-extract backfills the field (see
|
|
100
|
+
`/design-system-init` UPGRADE mode).
|
|
80
101
|
| **Agentic** (doc-reviewer) | `purpose`, `token_bindings`, `a11y`, `related`, `must_rules`, `canonical_for`, `use_when`, `selection_closed`, `last_verified`; plus `status: deprecated` as a judgment | **Never clobbered** when non-empty. Empty agentic fields = "not yet enriched", filled later by `doc-reviewer` / `ds-drift`, never auto-invented. |
|
|
81
102
|
|
|
82
103
|
### Selection-policy fields (the closed-set layer)
|
|
@@ -600,7 +600,7 @@ and the advisory check 6 (mirror, non-blocking):
|
|
|
600
600
|
2. **Modified primitive ⇒ updated spec**: did the change alter a primitive's
|
|
601
601
|
props / variants / accessibility behavior? If yes, the corresponding
|
|
602
602
|
`components/<Name>.md` MUST be updated to match — including its **frontmatter
|
|
603
|
-
HEAD** (deterministic fields `props`/`variants`/`composes`/`source_sha`
|
|
603
|
+
HEAD** (deterministic fields `props`/`variant_prop`/`variants`/`composes`/`source_sha`
|
|
604
604
|
regenerated from source; agentic fields re-verified). Stale spec =
|
|
605
605
|
`DS_COMPONENT_STALE` finding, task blocked. (A prose-only spec lacking a HEAD
|
|
606
606
|
is NOT stale merely for that — see `agents/component-manifest-schema.md`
|
|
@@ -40,7 +40,7 @@ BALDART wraps the **standard** (DTCG) and owns only a thin generator — the sam
|
|
|
40
40
|
|---|---|---|
|
|
41
41
|
| HEAD schema | `framework/agents/component-manifest-schema.md` | SSOT for the frontmatter fields + read/regeneration/leniency contract |
|
|
42
42
|
| Gate + cascade | `framework/agents/design-system-protocol.md` | BLOCKING reads, drift codes, token SSOT inversion, Authority Matrix |
|
|
43
|
-
| Deterministic extractor | `framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs` (`extractFile`) + `extract-one.mjs` (single-component wrapper)
|
|
43
|
+
| Deterministic extractor | `framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs` (`extractFile`) + `extract-one.mjs` (single-component wrapper) + `extract-ts.mjs` (optional TS-aware resolver) | Node; name/source/source_sha/props/variant_prop/variants/composes. **TS-aware** when `typescript` resolves from the consumer root (aliases/unions/intersections/Omit/Pick); **zero-dep regex fallback** otherwise (Codex-portable) |
|
|
44
44
|
| Spec template | `framework/templates/component-spec.template.md` | CANONICAL prose-body template (SSOT); serializer fills it on creation via `--prose-template` (default) |
|
|
45
45
|
| Bootstrap/upgrade | `framework/.claude/skills/design-system-init/SKILL.md` | greenfield + upgrade modes |
|
|
46
46
|
| Token emitters | `src/utils/token-emitters/{index,ts,css}.js` | DTCG → stack-native output (REGISTRY pattern) |
|
|
@@ -72,8 +72,11 @@ has_design_system: true (no separate flag — the manifest rides on it)
|
|
|
72
72
|
## 5. Invariants (do not break)
|
|
73
73
|
|
|
74
74
|
- **Codex portability**: the READ path and the deterministic half (extractor,
|
|
75
|
-
`baldart tokens build`) run with **no subagent**.
|
|
76
|
-
|
|
75
|
+
`baldart tokens build`) run with **no subagent**. The TS-aware resolver
|
|
76
|
+
(`extract-ts.mjs`) is **optional** — it uses the consumer's own `typescript` when
|
|
77
|
+
resolvable and falls back to the **zero-dep regex** path per file otherwise, so
|
|
78
|
+
Codex / non-TS stacks are unaffected. Only agentic enrichment needs Claude; under
|
|
79
|
+
Codex it degrades to empty/TODO fields — never a hard failure.
|
|
77
80
|
- **No twin**: ONE artifact per component (the spec, evolved) and ONE token SSOT
|
|
78
81
|
(`.tokens.json`); `tokens-reference.md` and `tokens.ts`/CSS are **generated
|
|
79
82
|
views/outputs**, never hand-authored sources. The INDEX is a generated router.
|
|
@@ -24,8 +24,9 @@ source: {{file_path}}
|
|
|
24
24
|
source_sha: {{source_sha}}
|
|
25
25
|
status: {{status}} # stable | experimental | deprecated (default stable)
|
|
26
26
|
category: {{category}} # primitive | composite | feature
|
|
27
|
-
props: {{props_yaml}} # name → { type, required, default } ({} if unresolved)
|
|
28
|
-
|
|
27
|
+
props: {{props_yaml}} # name → { type, required, default } ({} if unresolved; TS-aware: resolves aliases/unions/intersections/Omit/Pick)
|
|
28
|
+
variant_prop: {{variant_prop}} # which prop is the variant axis (ranked; '' if none / unresolved; legacy HEADs default to 'variant')
|
|
29
|
+
variants: {{variants_yaml}} # [] if none / unresolved (literal members of variant_prop's type)
|
|
29
30
|
composes: {{composes_yaml}} # in-registry primitives this imports
|
|
30
31
|
# AGENTIC fields (curated by doc-reviewer — preserved across regeneration):
|
|
31
32
|
purpose: "{{purpose}}"
|