baldart 4.64.0 → 4.66.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 (32) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/VERSION +1 -1
  4. package/bin/baldart.js +15 -0
  5. package/framework/.claude/agents/code-reviewer.md +25 -11
  6. package/framework/.claude/agents/codebase-architect.md +13 -5
  7. package/framework/.claude/agents/doc-reviewer.md +26 -12
  8. package/framework/.claude/agents/ui-expert.md +18 -10
  9. package/framework/.claude/skills/context-primer/SKILL.md +7 -0
  10. package/framework/.claude/skills/design-system-init/SKILL.md +164 -156
  11. package/framework/.claude/skills/design-system-init/scripts/component-spec.template.md +26 -3
  12. package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +151 -0
  13. package/framework/.claude/skills/new/references/final-review.md +12 -2
  14. package/framework/.claude/skills/new/references/team-mode.md +1 -1
  15. package/framework/.claude/workflows/new-card-review.js +67 -13
  16. package/framework/.claude/workflows/new-final-review.js +63 -7
  17. package/framework/agents/component-manifest-schema.md +145 -0
  18. package/framework/agents/design-system-protocol.md +63 -23
  19. package/framework/agents/index.md +2 -0
  20. package/framework/docs/COMPONENT-MANIFEST-LAYER.md +100 -0
  21. package/framework/routines/ds-drift.routine.yml +19 -12
  22. package/framework/templates/baldart.config.template.yml +22 -0
  23. package/package.json +1 -1
  24. package/src/commands/configure.js +54 -0
  25. package/src/commands/doctor.js +85 -0
  26. package/src/commands/tokens.js +80 -0
  27. package/src/commands/update.js +7 -0
  28. package/src/utils/token-emitters/README.md +36 -0
  29. package/src/utils/token-emitters/css.js +18 -0
  30. package/src/utils/token-emitters/index.js +47 -0
  31. package/src/utils/token-emitters/ts.js +32 -0
  32. package/src/utils/tokens-generator.js +165 -0
@@ -61,6 +61,47 @@ async function parallelCapped(thunks, cap) {
61
61
  return results
62
62
  }
63
63
 
64
+ // ---- Transient-aware spawn (SSOT parity with new2.js `agentSafe`, v4.66.0) ---
65
+ // A reviewer killed by a SERVER-SIDE API error (529 Overloaded, 429/5xx, rate-limit —
66
+ // the org-level limit is SHARED across every terminal/session) must NOT vanish from the
67
+ // wave review. `agent()` THROWS a transient error → retried in-workflow up to `cap`; it
68
+ // RETURNS null (terminal API death after the tool's own retries, or a skip) → recorded as
69
+ // degraded. No reliable sleep in the runtime ⇒ NOT timed backoff (absorbs brief blips only);
70
+ // a sustained outage exhausts the cap and surfaces as DEGRADED COVERAGE (and, for the
71
+ // qa-sentinel merge gate, a synthetic FAIL), NEVER a silent drop. Mirrors the transient-vs-
72
+ // genuine discipline in references/team-mode.md § empty-result classification.
73
+ const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
74
+ const isTransient = (e) => TRANSIENT.test(String((e && e.message) || e || ''))
75
+ async function agentSafe(prompt, opts, maxAttempts) {
76
+ const cap = maxAttempts || 3
77
+ let lastErr = null
78
+ for (let i = 0; i < cap; i++) {
79
+ try {
80
+ const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
81
+ return await agent(prompt, o)
82
+ } catch (e) { lastErr = e; if (!isTransient(e)) throw e } // permanent error → surface
83
+ }
84
+ const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr))
85
+ err.transientExhausted = true
86
+ throw err
87
+ }
88
+ const degradedReviewers = []
89
+ function reviewSafe(kind, card, prompt, opts) {
90
+ return agentSafe(prompt, opts)
91
+ .then((r) => {
92
+ if (r == null) {
93
+ degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: 'null-return' })
94
+ log(`Discovery: ${(opts && opts.label) || kind} returned null (terminal API error after tool retries / skip) — coverage degraded, NOT silently dropped.`)
95
+ }
96
+ return { kind, card, r }
97
+ })
98
+ .catch((e) => {
99
+ degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: e && e.transientExhausted ? 'transient-exhausted' : 'error' })
100
+ log(`Discovery: ${(opts && opts.label) || kind} FAILED after retries (${e && e.transientExhausted ? 'transient/529 exhausted' : 'terminal'}) — coverage degraded, NOT silently dropped.`)
101
+ return { kind, card, r: null }
102
+ })
103
+ }
104
+
64
105
  // Curated toolchain (since v4.41.0): when features.has_toolchain is on, the
65
106
  // consumer records LITERAL gate commands in toolchain.commands.* — agents run
66
107
  // THOSE instead of guessing (e.g. `npx biome check .` not `npm run lint`). Empty
@@ -336,10 +377,10 @@ function securityPrompt(c) {
336
377
  const findThunks = []
337
378
  for (const c of cards) {
338
379
  if (c.runSimplify !== false) {
339
- findThunks.push(() => agent(simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'simplify', card: c, r })))
380
+ findThunks.push(() => reviewSafe('simplify', c, simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', schema: FINDINGS_SCHEMA }))
340
381
  }
341
382
  if (c.hasSecurityFiles === true) {
342
- findThunks.push(() => agent(securityPrompt(c), { label: `security:${c.cardId}`, phase: 'Discovery', agentType: 'security-reviewer', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'security', card: c, r })))
383
+ findThunks.push(() => reviewSafe('security', c, securityPrompt(c), { label: `security:${c.cardId}`, phase: 'Discovery', agentType: 'security-reviewer', schema: FINDINGS_SCHEMA }))
343
384
  }
344
385
  }
345
386
  if (codexResolved) {
@@ -348,10 +389,10 @@ if (codexResolved) {
348
389
  // can't depend on the tier. The relay now returns RAW command outputs and the JS fan-in owns the
349
390
  // success decision (+ a one-shot deterministic re-extract), so a downgraded relay can't drop a
350
391
  // finding. We still REQUEST sonnet because when capacity allows it follows steps more reliably.
351
- findThunks.push(() => agent(codexPrompt, { label: 'codex', phase: 'Discovery', model: 'sonnet', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', card: null, r })))
392
+ findThunks.push(() => reviewSafe('codex', null, codexPrompt, { label: 'codex', phase: 'Discovery', model: 'sonnet', schema: CODEX_SCHEMA }))
352
393
  }
353
394
  if (maxQaTier === 'full') {
354
- findThunks.push(() => agent(qaPrompt, { label: 'qa-sentinel', phase: 'Discovery', agentType: 'qa-sentinel', schema: GATES_SCHEMA }).then((r) => ({ kind: 'qa', card: null, r })))
395
+ findThunks.push(() => reviewSafe('qa', null, qaPrompt, { label: 'qa-sentinel', phase: 'Discovery', agentType: 'qa-sentinel', schema: GATES_SCHEMA }))
355
396
  } else {
356
397
  log('Discovery: qa-sentinel SKIPPED (wave max tier ≤ light — full suite deferred to the Final FULL gate).')
357
398
  }
@@ -390,7 +431,16 @@ for (const item of findResults) {
390
431
  log(`Discovery: Codex relay returned no parseable JSON (marker=${item.r && item.r.marker})${codexProse ? ' — salvaging its prose as fallback leads' : ''} — falling back to code-reviewer.`)
391
432
  }
392
433
  } else if (item.kind === 'qa') {
393
- gateTable = (item.r && item.r.gates) || []
434
+ if (item.r && Array.isArray(item.r.gates)) {
435
+ gateTable = item.r.gates
436
+ } else {
437
+ // qa-sentinel DIED (null after retries — the 529 case). The full-tier mechanical gate is
438
+ // UNKNOWN. NEVER pass blind: inject a synthetic FAIL so summary.failingGates + the skill's
439
+ // gateTable-OR gate treat it as non-PASS (blocks the wave boundary). An empty [] would
440
+ // silently read as "no gates ran ⇒ nothing to block" — the bug.
441
+ gateTable = [{ gate: 'mechanical-gates (qa-sentinel)', status: 'FAIL', detail: 'qa-sentinel agent died (transient/529 exhausted or terminal API error) — full-tier mechanical gates UNKNOWN, treated as non-PASS. Re-run the review (or re-run lint/tsc/test/build manually) before merging the wave.' }]
442
+ log('Discovery: qa-sentinel DIED — injected a synthetic FAIL mechanical gate (wave-blocking, non-silent).')
443
+ }
394
444
  } else if (item.r && Array.isArray(item.r.findings)) {
395
445
  // simplify / security own their lane and FP-check their OWN findings → already validated.
396
446
  raw.push(...item.r.findings.map((f) => ({ ...f, source: item.kind, preValidated: true })))
@@ -583,12 +633,15 @@ const fixPhaseTouchedCode = appliedIds.size > 0
583
633
  if (maxQaTier !== 'full' && fixPhaseTouchedCode) {
584
634
  const scopedQaPrompt = qaPrompt +
585
635
  `\n\nDIFF-SCOPED RUN: do NOT run the whole test suite. Scope tests to the files that import the changed files above (the post-fix blast radius) — run only those, plus lint/type-check on the changed files. If the test runner cannot select a diff-scoped subset, SKIP the test gate gracefully (return status SKIP with a one-line reason) rather than running the full suite.`
586
- const scopedQa = await agent(scopedQaPrompt, { label: 'qa-sentinel (light-tier post-fix)', phase: 'Fix', agentType: 'qa-sentinel', schema: GATES_SCHEMA })
587
- const scopedGates = (scopedQa && Array.isArray(scopedQa.gates)) ? scopedQa.gates : []
588
- if (scopedGates.length) {
589
- gateTable = [...gateTable, ...scopedGates]
590
- log(`Fix: light-tier diff-scoped qa-sentinel ran after applying code fixes ${scopedGates.filter((g) => g.status === 'FAIL').length} failing gate(s) merged into gateTable.`)
591
- }
636
+ let scopedQa = null
637
+ try { scopedQa = await agentSafe(scopedQaPrompt, { label: 'qa-sentinel (light-tier post-fix)', phase: 'Fix', agentType: 'qa-sentinel', schema: GATES_SCHEMA }) } catch (e) { scopedQa = null }
638
+ const scopedGates = (scopedQa && Array.isArray(scopedQa.gates)) ? scopedQa.gates
639
+ // qa-sentinel DIED after a code fix actually touched code → the post-fix regression gate is
640
+ // UNKNOWN. NEVER pass blind: a synthetic FAIL so the skill's gateTable-OR catches it.
641
+ : [{ gate: 'mechanical-gates (qa-sentinel, light-tier post-fix)', status: 'FAIL', detail: 'diff-scoped qa-sentinel died (transient/529 exhausted or terminal API error) after code fixes were applied — post-fix regression UNKNOWN, treated as non-PASS.' }]
642
+ gateTable = [...gateTable, ...scopedGates]
643
+ if (!(scopedQa && Array.isArray(scopedQa.gates))) degradedReviewers.push({ reviewer: 'qa-sentinel (light-tier post-fix)', reason: 'died-synthetic-fail' })
644
+ log(`Fix: light-tier diff-scoped qa-sentinel ${scopedQa && Array.isArray(scopedQa.gates) ? 'ran' : 'DIED (synthetic FAIL injected)'} after applying code fixes — ${scopedGates.filter((g) => g.status === 'FAIL').length} failing gate(s) merged into gateTable.`)
592
645
  }
593
646
 
594
647
  // ───────────────────────────────────────────────────────────────────────────
@@ -681,8 +734,9 @@ const summary = makeSummary({
681
734
  failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate), // qa-sentinel FAIL gates (test/build/audit/markdownlint)
682
735
  blockers: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
683
736
  highs: surviving.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
737
+ degradedReviewers, // reviewers that died after retries (transient/529 or terminal) — coverage was incomplete; NEVER a silent drop.
684
738
  })
685
- log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.docApplied} doc-applied, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
739
+ log(`Wave review done: ${summary.fixesApplied} fixed, ${summary.docApplied} doc-applied, ${summary.codeResidual} code-residual, ${summary.docResidual} doc-residual, ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${degradedReviewers.length ? `, ${degradedReviewers.length} DEGRADED reviewer(s): ${degradedReviewers.map((d) => d.reviewer).join(', ')}` : ''}${checksFailed ? ', post-fix checks FAILED' : ''}. Engine: ${codexEngine}.`)
686
740
 
687
741
  return { codexEngine, perCard, gateTable, summary }
688
742
 
@@ -690,7 +744,7 @@ return { codexEngine, perCard, gateTable, summary }
690
744
  function asArr(x) { return Array.isArray(x) ? x.filter(Boolean) : [] }
691
745
  function dedupe(xs) { return Array.from(new Set(asArr(xs))) }
692
746
  function makeSummary(o) {
693
- return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0 }, o || {})
747
+ return Object.assign({ cards: 0, totalFindings: 0, verified: 0, noAction: 0, falsePositive: 0, needsManual: 0, fixesApplied: 0, docApplied: 0, docResidual: 0, codeResidual: 0, qaRan: false, checksFailed: false, failingGates: [], blockers: 0, highs: 0, degradedReviewers: [] }, o || {})
694
748
  }
695
749
  function slimFinding(f) {
696
750
  return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification, card: f.card }
@@ -72,6 +72,51 @@ async function parallelCapped(thunks, cap) {
72
72
  return results
73
73
  }
74
74
 
75
+ // ---- Transient-aware spawn (SSOT parity with new2.js `agentSafe`, v4.66.0) ---
76
+ // A reviewer killed by a SERVER-SIDE API error (529 Overloaded, 429/5xx, rate-limit —
77
+ // the org-level limit is SHARED across every terminal/session) must NOT vanish from the
78
+ // batch review. Two failure shapes exist and BOTH are handled: (1) `agent()` THROWS a
79
+ // transient error → retried in-workflow up to `cap`; (2) `agent()` RETURNS null (terminal
80
+ // API death after the tool's own retries, or a user skip) → recorded as degraded. There is
81
+ // NO reliable sleep/jitter in the workflow runtime, so this is NOT timed backoff — it
82
+ // absorbs brief blips; a sustained outage exhausts the cap and surfaces as DEGRADED COVERAGE
83
+ // (and, for the qa-sentinel merge gate, a synthetic FAIL), NEVER a silent drop. Mirrors the
84
+ // transient-vs-genuine discipline in references/team-mode.md § empty-result classification.
85
+ const TRANSIENT = /overload|529|429|rate.?limit|5\d\d|econn|etimedout|timeout|temporar|unavailable/i
86
+ const isTransient = (e) => TRANSIENT.test(String((e && e.message) || e || ''))
87
+ async function agentSafe(prompt, opts, maxAttempts) {
88
+ const cap = maxAttempts || 3
89
+ let lastErr = null
90
+ for (let i = 0; i < cap; i++) {
91
+ try {
92
+ const o = i === 0 ? opts : Object.assign({}, opts, { label: `${(opts && opts.label) || 'agent'}~retry${i}` })
93
+ return await agent(prompt, o)
94
+ } catch (e) { lastErr = e; if (!isTransient(e)) throw e } // permanent error → surface
95
+ }
96
+ const err = new Error('transient-exhausted: ' + String((lastErr && lastErr.message) || lastErr))
97
+ err.transientExhausted = true
98
+ throw err
99
+ }
100
+ // Degraded-coverage ledger — a reviewer that died (transient-exhausted OR null terminal
101
+ // return) is RECORDED here and surfaced in the return summary, so the skill's F.5 knows the
102
+ // batch review ran with incomplete coverage instead of mistaking it for "clean".
103
+ const degradedReviewers = []
104
+ function reviewSafe(kind, prompt, opts) {
105
+ return agentSafe(prompt, opts)
106
+ .then((r) => {
107
+ if (r == null) {
108
+ degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: 'null-return' })
109
+ log(`Review: ${(opts && opts.label) || kind} returned null (terminal API error after tool retries / skip) — coverage degraded, NOT silently dropped.`)
110
+ }
111
+ return { kind, r }
112
+ })
113
+ .catch((e) => {
114
+ degradedReviewers.push({ reviewer: (opts && opts.label) || kind, reason: e && e.transientExhausted ? 'transient-exhausted' : 'error' })
115
+ log(`Review: ${(opts && opts.label) || kind} FAILED after retries (${e && e.transientExhausted ? 'transient/529 exhausted' : 'terminal'}) — coverage degraded, NOT silently dropped.`)
116
+ return { kind, r: null }
117
+ })
118
+ }
119
+
75
120
  if (!scope.length) {
76
121
  log('new-final-review: empty review scope — nothing to review.')
77
122
  return { codexEngine: 'none', findings: [], gateTable: [], summary: emptySummary() }
@@ -325,10 +370,10 @@ const slimDoc = a.slimDoc !== undefined ? a.slimDoc === true : a.singleCard ===
325
370
  const slimApi = a.slimApi !== undefined ? a.slimApi === true : a.singleCard === true
326
371
  // qa-sentinel always runs — the merge integrity gate reads its PASS/FAIL table.
327
372
  const reviewThunks = [
328
- () => agent(qaPrompt, { label: 'qa-sentinel', phase: 'Review', agentType: 'qa-sentinel', schema: GATES_SCHEMA }).then((r) => ({ kind: 'qa', r })),
373
+ () => reviewSafe('qa', qaPrompt, { label: 'qa-sentinel', phase: 'Review', agentType: 'qa-sentinel', schema: GATES_SCHEMA }),
329
374
  ]
330
375
  if (!slimDoc) {
331
- reviewThunks.unshift(() => agent(docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'doc', r })))
376
+ reviewThunks.unshift(() => reviewSafe('doc', docPrompt, { label: 'doc-reviewer', phase: 'Review', agentType: 'doc-reviewer', schema: FINDINGS_SCHEMA }))
332
377
  } else {
333
378
  log('Review: single-card batch — final doc-reviewer skipped (already ran per-card); kept cross-model Codex + qa gates.')
334
379
  }
@@ -338,11 +383,11 @@ if (codexResolved) {
338
383
  // so correctness can't depend on the tier. The relay returns RAW outputs and the JS fan-in owns the
339
384
  // success decision (+ a one-shot deterministic re-extract), so a downgraded relay can't drop a finding.
340
385
  // We still REQUEST sonnet: this is the last cross-model gate before merge and a dropped find is final.
341
- reviewThunks.unshift(() => agent(codexPrompt, { label: 'codex', phase: 'Review', model: 'sonnet', schema: CODEX_SCHEMA }).then((r) => ({ kind: 'codex', r })))
386
+ reviewThunks.unshift(() => reviewSafe('codex', codexPrompt, { label: 'codex', phase: 'Review', model: 'sonnet', schema: CODEX_SCHEMA }))
342
387
  }
343
388
  // api-perf-cost-auditor: skipped when no API/data files OR when it already ran per-card (slimApi).
344
389
  if (!slimApi && a.hasApiDataFiles !== false) {
345
- reviewThunks.push(() => agent(apiPrompt, { label: 'api-perf-cost-auditor', phase: 'Review', agentType: 'api-perf-cost-auditor', schema: FINDINGS_SCHEMA }).then((r) => ({ kind: 'api', r })))
390
+ reviewThunks.push(() => reviewSafe('api', apiPrompt, { label: 'api-perf-cost-auditor', phase: 'Review', agentType: 'api-perf-cost-auditor', schema: FINDINGS_SCHEMA }))
346
391
  } else {
347
392
  log(slimApi
348
393
  ? 'Review: single-card batch — final api-perf-cost-auditor skipped (already ran per-card).'
@@ -383,7 +428,17 @@ for (const item of reviewResults) {
383
428
  log(`Review: Codex relay returned no parseable JSON (marker=${item.r && item.r.marker})${codexProse ? ' — salvaging its prose as fallback leads' : ''} — falling back to code-reviewer.`)
384
429
  }
385
430
  } else if (item.kind === 'qa') {
386
- gateTable = (item.r && item.r.gates) || []
431
+ if (item.r && Array.isArray(item.r.gates)) {
432
+ gateTable = item.r.gates
433
+ } else {
434
+ // qa-sentinel DIED (null after retries — the screenshotted 529 case). The mechanical
435
+ // merge gate (lint/tsc/test/build/audit/i18n) is UNKNOWN. NEVER pass blind: inject a
436
+ // synthetic FAIL so summary.failingGates + the skill's F.5 gate treat it as non-PASS
437
+ // (merge-blocking), and the O5(b) build-claim demotion below is suppressed (buildGateFail).
438
+ // An empty [] here would silently read as "no gates ran ⇒ nothing to block" — the bug.
439
+ gateTable = [{ gate: 'mechanical-gates (qa-sentinel)', status: 'FAIL', detail: 'qa-sentinel agent died (transient/529 exhausted or terminal API error) — mechanical gates UNKNOWN, treated as non-PASS to block the merge. Re-run the final review (or re-run lint/tsc/test/build manually) before merging.' }]
440
+ log('Final review: qa-sentinel DIED — injected a synthetic FAIL mechanical gate (merge-blocking, non-silent).')
441
+ }
387
442
  } else if (item.r && Array.isArray(item.r.findings)) {
388
443
  // doc-reviewer / api-perf-cost-auditor own their domain and FP-check their OWN findings in
389
444
  // the finding pass (their prompts mandate it) → already validated, no cross-domain re-judge.
@@ -526,8 +581,9 @@ const summary = {
526
581
  blockers: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'BLOCKER').length,
527
582
  highs: classified.filter((f) => f.classification === 'VERIFIED' && f.severity === 'HIGH').length,
528
583
  failingGates: gateTable.filter((g) => g.status === 'FAIL').map((g) => g.gate),
584
+ degradedReviewers, // reviewers that died after retries (transient/529 or terminal) — coverage was incomplete; NEVER a silent drop.
529
585
  }
530
- log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)
586
+ log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s)${degradedReviewers.length ? `, ${degradedReviewers.length} DEGRADED reviewer(s): ${degradedReviewers.map((d) => d.reviewer).join(', ')}` : ''}. Engine: ${codexEngine}.`)
531
587
 
532
588
  // ───────────────────────────────────────────────────────────────────────────
533
589
  // Phase Fix (P0-A, v4.59.0) — OPT-IN. Mirrors new-card-review.js's Fix+Doc phases EXACTLY:
@@ -670,7 +726,7 @@ const base = { codexEngine, findings, noActionFindings, gateTable, summary }
670
726
  return applyFixes ? { ...base, fixesApplied, docFixesApplied, residual } : base
671
727
 
672
728
  function emptySummary() {
673
- return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [] }
729
+ return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [], degradedReviewers: [] }
674
730
  }
675
731
  function slimFinding(f) {
676
732
  return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification }
@@ -0,0 +1,145 @@
1
+ # Component Manifest Schema
2
+
3
+ ## Purpose
4
+
5
+ Define the **machine-readable HEAD** every per-component spec
6
+ (`${paths.design_system}/components/<Name>.md`) carries so agents discover and
7
+ reuse primitives by reading a small, structured frontmatter **on demand** — not
8
+ by parsing a monolithic `INDEX.md` or the component source. This module is the
9
+ **single source of truth** for *which fields a component spec needs* and *in what
10
+ state*; every writer and every consumer cites it.
11
+
12
+ It is the design-system analogue of `agents/card-schema.md`: a schema that ≥6
13
+ surfaces produce or consume (`design-system-init`, `doc-reviewer`, `ui-expert`,
14
+ `code-reviewer`, `codebase-architect`, `context-primer`, `ds-drift`) and must
15
+ therefore live in one place that none of them re-describe.
16
+
17
+ ## Scope
18
+
19
+ **In**: the frontmatter field table, the deterministic-vs-agentic split, the
20
+ regeneration contract, the on-demand read contract, the thin-INDEX router shape,
21
+ and the transition-leniency rule.
22
+
23
+ **Out**: the *enforcement* (when a missing/stale spec blocks) and the drift codes
24
+ (`DS_INDEX_DRIFT` / `DS_COMPONENT_STALE` / `DS_TOKENS_DRIFT`) — those stay in
25
+ `agents/design-system-protocol.md` (§ Post-Intervention Coherence Check). This
26
+ module defines the *fields those codes are checked against*; it **never copies**
27
+ the codes or the gate (that would re-introduce the drift the SSOT split prevents).
28
+ Token *values* and the DTCG source live in the token layer
29
+ (`${paths.design_tokens}` → `.tokens.json`); this module references token **IDs**,
30
+ never values.
31
+
32
+ ## Gating
33
+
34
+ Same as the design-system protocol: meaningful only when
35
+ `features.has_design_system: true`. There is **no separate feature flag** — the
36
+ manifest rides on the design-system layer. When the flag is `false`, component
37
+ specs (and this schema) do not apply.
38
+
39
+ ## The frontmatter HEAD
40
+
41
+ Additive: a YAML frontmatter block is prepended to the EXISTING prose spec. The
42
+ prose body (Purpose, Props table, Accessibility, Anti-patterns, …) is preserved
43
+ verbatim — the HEAD makes the *mechanical* half machine-addressable; the prose
44
+ stays the human-readable long form.
45
+
46
+ ```yaml
47
+ ---
48
+ # === DETERMINISTIC — extracted from source (TS types) + git; regenerable, never hand-curated ===
49
+ name: Button # exported component name
50
+ source: components/ui/Button.tsx # path relative to repo root
51
+ source_sha: a1b2c3d # git blob sha of source at last generation (drift anchor)
52
+ status: stable # stable | experimental | deprecated (default stable; `deprecated` is a hand override)
53
+ category: primitive # primitive | composite | feature (heuristic on import fan-in; hand-overridable)
54
+ props: # from the TS prop type / interface
55
+ variant: { type: "'primary'|'ghost'|'destructive'", required: false, default: "'primary'" }
56
+ size: { type: "'sm'|'md'|'lg'", required: false, default: "'md'" }
57
+ disabled: { type: boolean, required: false, default: false }
58
+ variants: [primary, ghost, destructive] # enumerated literal union of the `variant` prop
59
+ composes: [Slot, Spinner] # in-registry primitives imported by this component
60
+ # === AGENTIC — curated by doc-reviewer; survives regeneration, never clobbered when non-empty ===
61
+ purpose: "Primary action trigger; one primary per surface."
62
+ token_bindings: [color.action.primary, radius.md, space.2, motion.fast] # DTCG token IDs (not values)
63
+ a11y: "Icon-only requires aria-label; ≥44px target; respects prefers-reduced-motion."
64
+ related: [IconButton, Link] # sibling primitives (not composition)
65
+ must_rules: ["MUST consume color.action.primary", "MUST set aria-label when icon-only"]
66
+ last_verified: 2026-06-23 # set by doc-reviewer / ds-drift on semantic re-verify
67
+ owner: doc-reviewer # curation owner (constant; documents the SSOT)
68
+ ---
69
+ ```
70
+
71
+ ### Field origin
72
+
73
+ | Origin | Fields | Rule |
74
+ |---|---|---|
75
+ | **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**. |
76
+ | **Agentic** (doc-reviewer) | `purpose`, `token_bindings`, `a11y`, `related`, `must_rules`, `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. |
77
+
78
+ ## Read contract (the context win)
79
+
80
+ 1. An agent doing reuse-discovery reads the thin `INDEX.md` (router) → then **only
81
+ the frontmatter** of the N specs in scope (front-load the first `---…---`
82
+ block), NOT the prose body and NOT the source.
83
+ 2. The prose body is read only when the agent is about to **modify** that
84
+ component.
85
+ 3. `token_bindings` are IDs into `.tokens.json`, so one already-read tokens file
86
+ answers every component's bindings — no per-component token re-read, and the
87
+ binding stays stable when a token *value* changes.
88
+
89
+ This is the whole point: replace "read a 33KB monolith + fall to source" with
90
+ "read a small router + N small frontmatters".
91
+
92
+ ## Thin INDEX router
93
+
94
+ `${paths.design_system}/INDEX.md` is **generated** and holds only routing data —
95
+ one row per primitive: `name | source | one-line purpose | spec link`. It carries
96
+ NO human-curated judgment (the Authority Matrix moved to
97
+ `design-system-protocol.md`; per-component detail lives in each spec). Because it
98
+ is generated, it is always safe to regenerate from the specs — never hand-edit it.
99
+
100
+ ## Regeneration contract
101
+
102
+ - **Anchor**: `source_sha`. A regeneration run skips a component whose source is
103
+ unchanged AND whose spec already has a HEAD (no-op); regenerates only
104
+ stale/missing ones.
105
+ - **Deterministic fields**: overwritten from current source every run.
106
+ - **Agentic fields**: preserved unless empty; an empty field is a candidate for
107
+ enrichment, never a reason to clobber a filled one.
108
+ - **Idempotent + resumable**: a kill mid-run leaves a mix of HEAD-bearing and
109
+ prose-only specs — all valid (see leniency below). No half-written state.
110
+
111
+ ## Transition leniency (do not raise the floor)
112
+
113
+ The HEAD is an **upgrade target, not a new floor**. During and after adoption:
114
+
115
+ - A **prose-only spec** (no HEAD) is still a valid spec — it does NOT trip
116
+ `DS_COMPONENT_STALE` merely for lacking a HEAD.
117
+ - **Empty deterministic fields** (e.g. a JS/Vue/Svelte consumer with no TS types,
118
+ or an unresolved prop) are **advisory** — "not yet enriched", never a blocking
119
+ finding.
120
+ - A **missing** spec for a new primitive remains exactly the `DS_INDEX_DRIFT` it
121
+ is today — the manifest adds no new failure mode, it only makes the existing
122
+ spec richer where the data is available.
123
+
124
+ This keeps consumers that have not yet upgraded (and non-TS stacks) from becoming
125
+ *more* broken than before the manifest existed.
126
+
127
+ ## Producers & consumers
128
+
129
+ - **Bootstrap / upgrade**: `/design-system-init` (deterministic extract + agentic
130
+ enrich; greenfield and upgrade modes).
131
+ - **Per-task write**: `ui-expert` / `ui-design` / `frontend-design` in the
132
+ Post-Intervention Coherence Check — a new/modified primitive regenerates its
133
+ HEAD in the same change.
134
+ - **Curation owner**: `doc-reviewer` (agentic fields).
135
+ - **Per-merge gate**: `code-reviewer` (reads HEAD-first; enforces the drift codes
136
+ per the protocol).
137
+ - **Read-first discovery**: `codebase-architect`, `context-primer`.
138
+ - **Weekly net**: `ds-drift` (compares `source_sha`/HEAD against source).
139
+
140
+ ## See also
141
+
142
+ - `agents/design-system-protocol.md` — the enforcement gate + drift codes + read
143
+ cascade this schema feeds.
144
+ - `agents/card-schema.md` — the sibling schema module this one mirrors.
145
+ - `agents/project-context.md` — `baldart.config.yml` + overlay resolution.
@@ -47,13 +47,21 @@ To bootstrap a registry on a project that does not yet have one, invoke the
47
47
 
48
48
  ## Canonical sources (resolve via `baldart.config.yml`)
49
49
 
50
- - `${paths.design_system}/INDEX.md` — component index + Canonical Authority
51
- Matrix + quick MUST rules. Entry point for every UI task.
52
- - `${paths.design_system}/tokens-reference.md` human-readable token contract
53
- (colors, spacing, shadows, radii, motion). Single source of truth for any
54
- value that would otherwise be hardcoded.
55
- - `${paths.design_system}/components/<Name>.md` per-component spec (props,
56
- variants, accessibility, anti-patterns). One file per primitive.
50
+ - `${paths.design_system}/INDEX.md` — **generated** thin router: one row per
51
+ primitive (`name | source | one-line purpose | spec link`). Entry point for
52
+ every UI task. Human-curated judgement does NOT live here (it moved to the
53
+ Authority Matrix below + each spec's frontmatter) never hand-edit the INDEX.
54
+ - `${paths.design_tokens}` `.tokens.json` **the token SSOT**, W3C DTCG
55
+ format (`$type`/`$value`). The stack-native artifact (`tokens.ts` / CSS custom
56
+ properties) is **generated** from it via `baldart tokens build`; editing that
57
+ generated output by hand is drift (see Token Contract).
58
+ - `${paths.design_system}/tokens-reference.md` — **generated/narrative** human
59
+ view of the tokens. NOT a third source of values: either generated from
60
+ `.tokens.json` or pure narrative ("how to use tokens"), never hand-authored
61
+ values that duplicate the DTCG source.
62
+ - `${paths.design_system}/components/<Name>.md` — per-component spec. Carries a
63
+ machine-readable frontmatter HEAD (see `agents/component-manifest-schema.md`)
64
+ + prose body (props, variants, accessibility, anti-patterns). One per primitive.
57
65
  - `${paths.design_system}/patterns/*.md` — opt-in pattern docs (theming,
58
66
  overlays, motion choreography, multi-tenant pairing rules).
59
67
  - `${paths.ui_guidelines}` — brand voice, typography, philosophy. Always read.
@@ -64,9 +72,12 @@ To bootstrap a registry on a project that does not yet have one, invoke the
64
72
 
65
73
  Before producing any new UI code, mockup, or design review:
66
74
 
67
- 1. **BLOCKING** — read `${paths.design_system}/INDEX.md`. Extract: the
68
- component index, the Authority Matrix for the surface you are about to
69
- touch, any pattern docs the index points to.
75
+ 1. **BLOCKING** — read `${paths.design_system}/INDEX.md` (the thin router) to
76
+ locate the primitives in scope, then read **only the frontmatter HEAD** of
77
+ each in-scope `components/<Name>.md` (per `agents/component-manifest-schema.md`)
78
+ — not the prose body, not the source. Read the prose body only when you are
79
+ about to modify that primitive. Also read the Authority Matrix (below) + any
80
+ pattern docs the router points to.
70
81
  2. **BLOCKING** — read `${paths.ui_guidelines}` for visual language, typography,
71
82
  accessibility, and brand voice. The `identity.design_philosophy` (when set)
72
83
  is the lens through which these guidelines apply.
@@ -120,10 +131,26 @@ any code or mockup produced under this protocol:
120
131
  the `motion-design` skill).
121
132
 
122
133
  The exact token consumption mechanism (CSS custom properties, Tailwind theme
123
- extend, theme provider) is project-specific — defer to
124
- `${paths.design_system}/tokens-reference.md` and the project's chosen
134
+ extend, theme provider) is project-specific — defer to the project's chosen
125
135
  mechanism.
126
136
 
137
+ **Token SSOT inversion (when `paths.design_tokens` is set).** The canonical
138
+ token source is the DTCG `.tokens.json` at `${paths.design_tokens}`. The
139
+ stack-native artifact agents/components consume (`tokens.ts`, a CSS custom-property
140
+ sheet, …) is **generated** from it by `baldart tokens build` and carries a
141
+ `// baldart-generated from .tokens.json — do not edit` marker. Consequences:
142
+
143
+ - To add/change a token, edit `.tokens.json` and regenerate — never hand-edit the
144
+ generated output.
145
+ - `DS_TOKENS_DRIFT` is now mechanically checkable: the generated output must equal
146
+ `baldart tokens build` from the current `.tokens.json`. A hand-edit to the
147
+ generated file (output ≠ rebuild) is the violation — port it to `.tokens.json`.
148
+ - A `token_bindings` ID in a component HEAD must resolve to a real node in
149
+ `.tokens.json`; a dangling ID is `DS_TOKENS_DRIFT`.
150
+
151
+ When `paths.design_tokens` is unset, the project has not adopted the DTCG SSOT —
152
+ fall back to the project's existing token file as the source (no inversion).
153
+
127
154
  ## Reference Tables (embed)
128
155
 
129
156
  These tables are the SSOT for the *numeric ranges* every agent/skill should
@@ -306,13 +333,16 @@ visually uneven steps because HSL lightness is not perceptual.
306
333
 
307
334
  ## Authority Matrix
308
335
 
309
- `${paths.design_system}/INDEX.md` declares, for each primitive, who is
310
- authoritative over which dimension:
336
+ This matrix is **canonical here** (protocol-level), not in `INDEX.md` the INDEX
337
+ is a generated router and cannot hold curated judgement. For each primitive, it
338
+ declares who is authoritative over which dimension:
311
339
 
312
- - **Visual authority** — colors, spacing, shadows, radii: the registry +
313
- tokens-reference. Agents/skills may not deviate.
340
+ - **Visual authority** — colors, spacing, shadows, radii, motion: the DTCG token
341
+ SSOT (`${paths.design_tokens}` → `.tokens.json`, surfaced via the generated
342
+ token reference). Agents/skills may not deviate.
314
343
  - **Behavioral authority** — props, variants, accessibility hooks: the
315
- per-component spec.
344
+ per-component spec (its frontmatter HEAD for the machine-readable contract, its
345
+ prose body for rationale).
316
346
  - **Composition authority** — how primitives are combined into patterns: the
317
347
  pattern docs.
318
348
 
@@ -441,12 +471,19 @@ For each item in the diff that touched a visual surface, run these 4 checks:
441
471
  in the same change. Missing spec = `DS_INDEX_DRIFT` finding, task blocked.
442
472
  2. **Modified primitive ⇒ updated spec**: did the change alter a primitive's
443
473
  props / variants / accessibility behavior? If yes, the corresponding
444
- `components/<Name>.md` MUST be updated to match. Stale spec =
445
- `DS_COMPONENT_STALE` finding, task blocked.
446
- 3. **New / changed token updated reference**: did the change introduce or
447
- modify a token (color, spacing, shadow, radius, motion)? If yes,
448
- `${paths.design_system}/tokens-reference.md` MUST reflect it. Stale
449
- reference = `DS_TOKENS_DRIFT` finding, task blocked.
474
+ `components/<Name>.md` MUST be updated to match including its **frontmatter
475
+ HEAD** (deterministic fields `props`/`variants`/`composes`/`source_sha`
476
+ regenerated from source; agentic fields re-verified). Stale spec =
477
+ `DS_COMPONENT_STALE` finding, task blocked. (A prose-only spec lacking a HEAD
478
+ is NOT stale merely for that — see `agents/component-manifest-schema.md`
479
+ § Transition leniency.)
480
+ 3. **New / changed token ⇒ updated SSOT**: did the change introduce or modify a
481
+ token (color, spacing, shadow, radius, motion)? If `paths.design_tokens` is
482
+ set, the change MUST be made in `.tokens.json` and the stack-native output
483
+ regenerated via `baldart tokens build` (a hand-edit to the generated output is
484
+ the drift). Otherwise update the project's token source + reference. Either
485
+ way, a dangling `token_bindings` ID is drift. Stale = `DS_TOKENS_DRIFT`,
486
+ task blocked.
450
487
  4. **Surfaced silent primitive ⇒ added to INDEX**: during the work, did you
451
488
  reuse a primitive that exists in the source tree but is **not** in
452
489
  `INDEX.md`? If yes, add it to the INDEX in the same change (or open a
@@ -529,6 +566,9 @@ When changing the BLOCKING cascade, change it here — not in the consumers.
529
566
 
530
567
  ## See also
531
568
 
569
+ - `framework/agents/component-manifest-schema.md` — the machine-readable
570
+ frontmatter HEAD on each `components/<Name>.md`, the on-demand read contract,
571
+ and the regeneration + transition-leniency rules this gate is checked against.
532
572
  - `framework/agents/project-context.md` — always-ask / never-assume contract
533
573
  and the `baldart.config.yml` resolution flow.
534
574
  - `framework/agents/design-review.md` — design-review workflow and report
@@ -20,6 +20,7 @@ Route agents to the right module with minimal reading.
20
20
  - If touching database schema or fields -> read `agents/data-model.md` and `${paths.references_dir}/data-model.md`.
21
21
  - If touching UI pages/routes or flows -> read `${paths.references_dir}/ui/index.md` (then specific UI module for your domain).
22
22
  - If touching ANY visual surface (component, page, mockup, design review) and `features.has_design_system: true` -> read `agents/design-system-protocol.md` and treat the BLOCKING cascade (`${paths.design_system}/INDEX.md` + `tokens-reference.md` + `components/<Name>.md`) as mandatory pre-work. When the flag is `false`, recommend `/design-system-init` to scaffold a registry.
23
+ - If CREATING, UPDATING, or READING a per-component spec (`${paths.design_system}/components/<Name>.md`) -> read `agents/component-manifest-schema.md` for the machine-readable frontmatter HEAD (deterministic-from-TS + agentic fields) and the on-demand read contract (INDEX router → frontmatter-first, prose only when modifying). The drift codes + gate stay in `design-system-protocol.md`.
23
24
  - If touching design-review workflows or UI guidelines -> read `agents/design-review.md` and project-specific UI guidelines.
24
25
  - If touching architecture, auth, or tech stack -> read `agents/architecture.md`.
25
26
  - If touching workflow/process/commits/backlog -> read `agents/workflows.md`.
@@ -72,6 +73,7 @@ When adding or updating agents, update REGISTRY.md — not this file.
72
73
  - `agents/i18n-protocol.md` — Internationalization discipline: no hardcoded user-facing strings, context registry (`paths.i18n_registry`) + ICU + context-aware translation; BLOCKING anti-hardcoded lint gate (since v4.52.0, gated on `features.has_i18n`)
73
74
  - `agents/design-system-protocol.md` — Registry-first discipline for UI work: BLOCKING cascade on `INDEX.md` + `tokens-reference.md` + `components/<Name>.md` (since v3.11.0, gated on `features.has_design_system`)
74
75
  - `agents/card-schema.md` — Atomic Card Baseline Schema: the universal, profile-aware (epic/child/standalone) field contract every backlog card satisfies, plus the consumer HALT/BACK-FILL/WARN contract (since v4.35.0)
76
+ - `agents/component-manifest-schema.md` — Component Manifest Schema: the machine-readable frontmatter HEAD on each `components/<Name>.md` (deterministic-from-TS + agentic fields), on-demand read contract, regeneration + transition-leniency rules (since v4.65.0, gated on `features.has_design_system`)
75
77
  - `agents/return-contract-protocol.md` — Subagent return-message economy: COMPACT (bounded headline + `path:line` findings + disk pointer) vs FULL, persist-then-summarize — the input-side twin of `effort-protocol.md` (since v4.59.0)
76
78
 
77
79
  ## Where to Document (Decision Tree)