baldart 4.76.1 → 4.78.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.
Files changed (25) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +14 -13
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/REGISTRY.md +2 -1
  5. package/framework/.claude/agents/markup-fidelity-verifier.md +239 -0
  6. package/framework/.claude/agents/prd-card-writer.md +6 -1
  7. package/framework/.claude/agents/visual-fidelity-verifier.md +22 -0
  8. package/framework/.claude/skills/design-system-init/SKILL.md +16 -6
  9. package/framework/.claude/skills/design-system-init/scripts/compile-ds-cards.mjs +2 -1
  10. package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +17 -5
  11. package/framework/.claude/skills/design-system-init/scripts/extract-ts.mjs +302 -0
  12. package/framework/.claude/skills/design-system-init/scripts/render-manifest.mjs +26 -9
  13. package/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs +4 -2
  14. package/framework/.claude/skills/ds-edit/SKILL.md +2 -1
  15. package/framework/.claude/skills/ds-new/SKILL.md +2 -2
  16. package/framework/.claude/skills/e2e-review/SKILL.md +196 -27
  17. package/framework/.claude/skills/new/references/codex-gate.md +15 -1
  18. package/framework/.claude/skills/new/references/completeness.md +2 -0
  19. package/framework/.claude/skills/new/references/review-cycle.md +14 -1
  20. package/framework/agents/component-manifest-schema.md +24 -3
  21. package/framework/agents/design-system-protocol.md +79 -1
  22. package/framework/docs/COMPONENT-MANIFEST-LAYER.md +6 -3
  23. package/framework/templates/component-spec.template.md +3 -2
  24. package/framework/templates/overlays/e2e-review.fidelity-example.md +74 -0
  25. package/package.json +1 -1
@@ -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: [a, b, c] (the enumerated literal union of the `variant` prop)
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 vr = head.match(/^variants:\s*\[(.*?)\]/m);
51
- if (vr && vr[1].trim()) {
52
- variants = vr[1].split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
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
- const others = Object.keys(props).filter((p) => p !== 'variant');
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 ? { variant: v } : {}), ...baseFixture },
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, token_bindings:[],
20
- // a11y, related:[], must_rules:[], last_verified, owner, prose } ] }
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` from the new source.
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` (zero-dep,
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 —