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
@@ -0,0 +1,100 @@
1
+ # Component Manifest + DTCG Token Layer
2
+
3
+ Authoritative design, lifecycle, and invariants for the machine-readable
4
+ component-manifest layer and the DTCG token SSOT (since v4.65.0). Companion to
5
+ the textual SSOTs: `framework/agents/component-manifest-schema.md` (the HEAD
6
+ schema) and `framework/agents/design-system-protocol.md` (the gate + cascade).
7
+
8
+ ## 1. Why this exists
9
+
10
+ On a real consumer, UI component discovery was slow: the registry's `INDEX.md`
11
+ had grown to a **33KB monolith** read in full on every UI task, there was no
12
+ `tokens-reference.md` (tokens parsed from a large hand-authored token file), and
13
+ **0 of ~135 components** had a per-component spec — so every "what does this
14
+ primitive do / can I reuse it" lookup degraded to reading component **source**.
15
+ Graphify is
16
+ the wrong lever here: component lookup is a single-symbol query, which the
17
+ code-graph protocol routes to LSP/Grep, not the graph.
18
+
19
+ The fix is not a new layer — the framework already prescribes per-component specs
20
+ (`components/<Name>.md`, owned by `doc-reviewer`, gated by `code-reviewer`,
21
+ audited by `ds-drift`). It is **(a)** making those specs machine-readable and
22
+ read **on demand**, and **(b)** giving tokens a real, interoperable SSOT.
23
+
24
+ ## 2. What BALDART adds vs what the stack already ships
25
+
26
+ | Concern | Stack / status quo | What BALDART adds |
27
+ |---|---|---|
28
+ | Component contract | prose `components/<Name>.md` (often absent) | a YAML **frontmatter HEAD** — deterministic (from TS) + agentic (curated) — read on demand |
29
+ | Discovery entry | monolithic INDEX read in full | **thin generated INDEX router** → frontmatter-first reads of only the N specs in scope |
30
+ | Token source | ad-hoc `tokens.ts` / CSS / Tailwind | **W3C DTCG `.tokens.json`** as SSOT |
31
+ | Token output | hand-authored | **generated** via `baldart tokens build` (own thin emitter registry — no Style Dictionary) |
32
+ | Generation | hand-written specs that don't scale | **hybrid**: deterministic Node extractor + agentic enrichment |
33
+
34
+ BALDART wraps the **standard** (DTCG) and owns only a thin generator — the same
35
+ "wrap best-in-class, own the adapter" ethos as the i18n and graph layers.
36
+
37
+ ## 3. The moving parts
38
+
39
+ | Piece | Path | Role |
40
+ |---|---|---|
41
+ | HEAD schema | `framework/agents/component-manifest-schema.md` | SSOT for the frontmatter fields + read/regeneration/leniency contract |
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` | zero-dep Node; name/source/source_sha/props/variants/composes (Codex-portable) |
44
+ | Spec template | `…/design-system-init/scripts/component-spec.template.md` | HEAD + prose scaffold |
45
+ | Bootstrap/upgrade | `framework/.claude/skills/design-system-init/SKILL.md` | greenfield + upgrade modes |
46
+ | Token emitters | `src/utils/token-emitters/{index,ts,css}.js` | DTCG → stack-native output (REGISTRY pattern) |
47
+ | Token generator | `src/utils/tokens-generator.js` | parse `.tokens.json`, resolve `{aliases}`, build/isStale |
48
+ | CLI verb | `src/commands/tokens.js` (`baldart tokens build`) | the operation skills/doctor CALL |
49
+ | Config | `paths.design_tokens` + `design_tokens.outputs` | propagated template→configure→update→doctor |
50
+
51
+ ## 4. Install / adoption lifecycle
52
+
53
+ ```
54
+ has_design_system: true (no separate flag — the manifest rides on it)
55
+
56
+ ├─ configure: autodetect .tokens.json → paths.design_tokens; propose ts/css outputs; baldart tokens build
57
+
58
+ ├─ /design-system-init (greenfield OR upgrade):
59
+ │ deterministic extract (no subagent) → agentic enrich (doc-reviewer)
60
+ │ → .tokens.json bootstrap + round-trip verify → thin INDEX router
61
+
62
+ ├─ per task: ui-expert/ui-design write the HEAD in the Post-Intervention Check
63
+ │ coder/architect/context-primer READ frontmatter-first
64
+
65
+ ├─ per merge: code-reviewer Rule 8 gates (HEAD-first; transition-lenient)
66
+
67
+ ├─ weekly: ds-drift (source_sha + token-output-vs-DTCG)
68
+
69
+ └─ doctor backfill: tokens-build (stale output) + ds-manifest-nudge (specs missing/prose-only)
70
+ ```
71
+
72
+ ## 5. Invariants (do not break)
73
+
74
+ - **Codex portability**: the READ path and the deterministic half (extractor,
75
+ `baldart tokens build`) run with **no subagent**. Only agentic enrichment needs
76
+ Claude; under Codex it degrades to empty/TODO fields — never a hard failure.
77
+ - **No twin**: ONE artifact per component (the spec, evolved) and ONE token SSOT
78
+ (`.tokens.json`); `tokens-reference.md` and `tokens.ts`/CSS are **generated
79
+ views/outputs**, never hand-authored sources. The INDEX is a generated router.
80
+ - **Token SSOT inversion**: when `paths.design_tokens` is set, edit `.tokens.json`
81
+ and regenerate; a hand-edit to a generated output (output ≠ `baldart tokens
82
+ build`) is `DS_TOKENS_DRIFT`. Generated files carry a `baldart-generated` banner.
83
+ - **Transition leniency (no new floor)**: a prose-only spec lacking a HEAD is
84
+ valid (not `DS_COMPONENT_STALE`); empty deterministic fields (non-TS stack) are
85
+ advisory; a missing spec for a new primitive is the same `DS_INDEX_DRIFT` as
86
+ before. The manifest is an upgrade target, not a stricter gate.
87
+ - **Ownership**: `doc-reviewer` owns the agentic HEAD fields (never clobbered on
88
+ regeneration); the deterministic fields are regenerated from source; never
89
+ hand-curate them.
90
+ - **Authority Matrix** lives in `design-system-protocol.md`, not the (generated)
91
+ INDEX.
92
+
93
+ ## 6. Adding capability later (deferred, documented)
94
+
95
+ - Extra emitters (`scss`, `tailwind`, `json`) — drop a file in `token-emitters/`.
96
+ - A git pre-commit hook to regenerate tokens on `.tokens.json` change (opt-in).
97
+ - In-place replacement of a hand-authored `tokens.ts` (after the round-trip is
98
+ trusted on real consumers).
99
+ - `framework-edit-gate` hard-deny on editing a generated token output (after one
100
+ release of the advisory banner).
@@ -14,24 +14,31 @@ prompt: |
14
14
  Run the weekly design-system drift check (only if `${paths.design_system}/`
15
15
  exists in the project — exit cleanly if absent):
16
16
 
17
- 1. **INDEX coverage**: compare `${paths.design_system}/INDEX.md` component
18
- index against the actual shared-component inventory. Flag
19
- `DS_INDEX_DRIFT` for any mismatch.
20
- 2. **Per-component accuracy**: for each `${paths.design_system}/components/<Name>.md`,
21
- verify it lines up with the source file (variants, props, exported
22
- constants). Flag `DS_COMPONENT_STALE` for docs not re-verified in >7
23
- days where the source has changed in git.
17
+ 1. **INDEX coverage**: the `${paths.design_system}/INDEX.md` router must list
18
+ every primitive that has a spec. Flag `DS_INDEX_DRIFT` for any mismatch
19
+ (fix by regenerating the router, never hand-editing it).
20
+ 2. **Per-component HEAD accuracy**: for each
21
+ `${paths.design_system}/components/<Name>.md`, compare its frontmatter HEAD
22
+ against the source — `source_sha` vs the current git blob, `props`/`variants`
23
+ vs the TS types (schema: `framework/agents/component-manifest-schema.md`).
24
+ Flag `DS_COMPONENT_STALE` when `source_sha` is stale (or >7 days unverified
25
+ with a source change). A prose-only spec with no HEAD is an upgrade candidate
26
+ (recommend `/design-system-init`), NOT itself stale.
24
27
  3. **Animations reconciliation**: `${paths.design_system}/patterns/animations.md`
25
28
  must match keyframes in the project's global styles. Flag
26
29
  `DS_ANIMATIONS_DRIFT`.
27
- 4. **Tokens reconciliation**: when tokens source files changed, verify
28
- `${paths.design_system}/tokens-reference.md` is in sync and the relevant
29
- CSS custom properties still exist. Flag `DS_TOKENS_DRIFT`.
30
+ 4. **Tokens reconciliation**: when `paths.design_tokens` is set (DTCG SSOT),
31
+ verify the generated outputs (`design_tokens.outputs`) + `tokens-reference.md`
32
+ equal `baldart tokens build` a hand-edited generated file or a dangling
33
+ `token_bindings` ID is `DS_TOKENS_DRIFT`. When unset, fall back to verifying
34
+ the project token source vs `tokens-reference.md`.
30
35
  5. Emit a consolidated report under
31
36
  `docs/reports/{{YYYYMMDD}}-ds-drift.md`.
32
37
 
33
- Apply trivial fixes inline (e.g. update an INDEX row to match the current
34
- source). Anything that requires design judgement is left as a finding.
38
+ Apply trivial fixes inline (regenerate the INDEX router; `baldart tokens build`
39
+ for token-output drift; refresh a stale deterministic HEAD via the extractor).
40
+ Anything that requires design judgement (agentic HEAD fields, new variant
41
+ semantics) is left as a finding.
35
42
 
36
43
  output:
37
44
  path: docs/reports/{{YYYYMMDD}}-ds-drift.md
@@ -35,6 +35,13 @@ paths:
35
35
  components_root: "" # e.g. src/components
36
36
  global_styles: "" # e.g. src/app/globals.css
37
37
 
38
+ # Design-token SSOT (since v4.65.0). The W3C DTCG `.tokens.json` file that is
39
+ # the SINGLE source of truth for design tokens; the stack-native artifact
40
+ # (tokens.ts / CSS vars) is GENERATED from it via `baldart tokens build` (see
41
+ # the design_tokens: block below). Only meaningful when has_design_system: true.
42
+ # Leave empty if the project has not adopted the DTCG SSOT (no inversion).
43
+ design_tokens: "" # e.g. src/ui/tokens.tokens.json
44
+
38
45
  # Workflow infrastructure (set the matching `features.*` flag).
39
46
  backlog_dir: "" # e.g. backlog
40
47
  adrs_dir: "" # e.g. docs/decisions
@@ -312,6 +319,21 @@ graph:
312
319
  # without it, so this is an enhancement, not a requirement.
313
320
  register_mcp: false
314
321
 
322
+ # ─── DESIGN TOKENS (DTCG) ──────────────────────────────────────────────────
323
+ # Generation targets for the DTCG token SSOT (since v4.65.0). The source is
324
+ # `paths.design_tokens` (a W3C DTCG `.tokens.json`). `baldart tokens build`
325
+ # reads it, resolves `{aliases}`, and regenerates each output below. The
326
+ # generated files carry a `baldart-generated` banner — edit `.tokens.json`, never
327
+ # the output. Only meaningful when has_design_system: true AND
328
+ # paths.design_tokens is set; otherwise `tokens build` is a no-op.
329
+ design_tokens:
330
+ # Each output is { format, path }. format ∈ token-emitters registry
331
+ # (src/utils/token-emitters/: ts | css — extend by dropping a new emitter).
332
+ outputs: []
333
+ # Example:
334
+ # - { format: ts, path: src/ui/tokens.ts }
335
+ # - { format: css, path: src/ui/tokens.css }
336
+
315
337
  # ─── TOOLCHAIN ─────────────────────────────────────────────────────────────
316
338
  # State of the curated dev toolchain. Populated by `baldart configure` / the
317
339
  # `/toolchain-bootstrap` skill. Only meaningful when `features.has_toolchain:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.64.0",
3
+ "version": "4.66.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -308,6 +308,23 @@ function detect(cwd = process.cwd()) {
308
308
  )
309
309
  );
310
310
 
311
+ // ---- Design-token SSOT (DTCG `.tokens.json`, since v4.65.0) -------------
312
+ // Detection-only: a `.tokens.json` / `.tokens` file at a conventional spot
313
+ // means the project already speaks DTCG. If absent we leave design_tokens
314
+ // empty (no inversion) — adoption is an explicit /design-system-init step,
315
+ // never auto-forced (the project's existing tokens.ts stays the source).
316
+ const designTokensPath = findFirst(
317
+ ...expandCandidates(
318
+ 'tokens.tokens.json', 'design/tokens.tokens.json',
319
+ 'src/ui/tokens.tokens.json', 'src/design-system/tokens.tokens.json',
320
+ 'tokens.json', 'design-tokens.json'
321
+ ),
322
+ ...(designSystemPath ? [
323
+ path.join(designSystemPath, 'tokens.tokens.json'),
324
+ path.join(designSystemPath, 'tokens.json'),
325
+ ] : [])
326
+ ) || '';
327
+
311
328
  // ---- API docs (widened — OpenAPI, GraphQL, populated dir) --------------
312
329
  const apiIndex = findFirst('docs/references/api/index.md', 'docs/api/index.md');
313
330
  const apiSchemas = findFirst('docs/references/api/schemas.md', 'docs/api/schemas.md');
@@ -425,6 +442,7 @@ function detect(cwd = process.cwd()) {
425
442
  components_primitives: componentsPrimitives,
426
443
  components_root: componentsRoot,
427
444
  global_styles: globalStyles,
445
+ design_tokens: designTokensPath,
428
446
  backlog_dir: exists('backlog') ? 'backlog' : '',
429
447
  adrs_dir: countMatches('docs/decisions', /^ADR-.*\.md$/) > 0 ? 'docs/decisions' : '',
430
448
  prd_dir: exists('docs/prd') ? 'docs/prd' : '',
@@ -732,6 +750,7 @@ async function interactivePrompts(merged, detected) {
732
750
  ['components_primitives', 'UI primitives directory (e.g. src/components/ui)', () => true],
733
751
  ['components_root', 'Components root directory', () => true],
734
752
  ['global_styles', 'Global stylesheet path', () => true],
753
+ ['design_tokens', 'DTCG token SSOT (.tokens.json) — leave empty if not adopted', () => merged.features.has_design_system],
735
754
  ['backlog_dir', 'Backlog directory', () => merged.features.has_backlog],
736
755
  ['adrs_dir', 'ADR directory', () => merged.features.has_adrs],
737
756
  ['prd_dir', 'PRD directory', () => merged.features.has_prd_workflow],
@@ -750,6 +769,41 @@ async function interactivePrompts(merged, detected) {
750
769
  merged.paths[key] = await promptForKey(`${label}`, proposed);
751
770
  }
752
771
 
772
+ // ---- Design tokens (DTCG SSOT, since v4.65.0) -------------------------
773
+ // Only when the project adopted the DTCG source (paths.design_tokens set).
774
+ // The block holds the GENERATION targets; `baldart tokens build` (+ doctor
775
+ // backfill) regenerate them. We never invert an existing hand-authored
776
+ // tokens.ts here — that migration is an explicit /design-system-init step.
777
+ merged.design_tokens = merged.design_tokens || { outputs: [] };
778
+ if (merged.features.has_design_system === true && merged.paths.design_tokens) {
779
+ UI.section('Design tokens (DTCG SSOT → generated artifacts)');
780
+ const TokensGenerator = require('../utils/tokens-generator');
781
+ const gen = new TokensGenerator(process.cwd());
782
+ if (!gen.detect(merged.paths.design_tokens)) {
783
+ UI.info(`DTCG source \`${merged.paths.design_tokens}\` not found yet — run /design-system-init to scaffold it. Leaving design_tokens.outputs empty.`);
784
+ } else if (!(merged.design_tokens.outputs || []).length) {
785
+ // Propose the conventional outputs next to the existing token consumers.
786
+ const proposed = [];
787
+ const tsGuess = merged.paths.global_styles ? '' : 'src/ui/tokens.ts';
788
+ if (await UI.confirm('Generate a TypeScript token object (tokens.ts) from the DTCG source?', true)) {
789
+ const p = await promptForKey(' tokens.ts output path', tsGuess || 'src/ui/tokens.ts');
790
+ if (p) proposed.push({ format: 'ts', path: p });
791
+ }
792
+ if (await UI.confirm('Also generate a CSS custom-property sheet?', false)) {
793
+ const p = await promptForKey(' tokens.css output path', 'src/ui/tokens.css');
794
+ if (p) proposed.push({ format: 'css', path: p });
795
+ }
796
+ merged.design_tokens.outputs = proposed;
797
+ if (proposed.length) {
798
+ const res = gen.build({ source: merged.paths.design_tokens, outputs: proposed });
799
+ if (res.ok) UI.success(`Tokens built (${res.tokenCount}) → ${res.written.join(', ')}`);
800
+ else UI.warning(`Token build failed: ${res.error} (fix the .tokens.json then run \`baldart tokens build\`).`);
801
+ }
802
+ }
803
+ } else {
804
+ merged.design_tokens.outputs = merged.design_tokens.outputs || [];
805
+ }
806
+
753
807
  // ---- E2E review tuning (since v3.18.0) --------------------------------
754
808
  // Only runs when the user opted in above. Populates the e2e_review.*
755
809
  // sub-section with the severity floor, self-heal cap, pixel pre-filter
@@ -479,6 +479,54 @@ async function detectState(cwd, opts = {}) {
479
479
  }
480
480
  } catch (_) { /* never block doctor on i18n probe */ }
481
481
 
482
+ // ---- Design-system manifest + DTCG tokens (since v4.65.0) ----------
483
+ // Two backfills, both gated on has_design_system (no separate flag — the
484
+ // manifest rides on the design-system layer). (1) "default-on + nudge":
485
+ // when the registry exists but specs are missing or still prose-only (no
486
+ // machine-readable HEAD), nudge /design-system-init. (2) token SSOT: when
487
+ // paths.design_tokens is set, flag generated outputs that drifted from the
488
+ // DTCG source (or a hand-edited generated file).
489
+ state.dsManifestNudge = false;
490
+ state.dsManifestReason = '';
491
+ state.tokensStale = false;
492
+ state.tokensStalePaths = [];
493
+ try {
494
+ if (config && !config.__malformed && config.features && config.features.has_design_system === true) {
495
+ const dsRoot = (config.paths && config.paths.design_system) || '';
496
+ if (dsRoot) {
497
+ const compDir = path.join(cwd, dsRoot, 'components');
498
+ let specs = [];
499
+ try { specs = fs.readdirSync(compDir).filter((f) => f.endsWith('.md')); } catch (_) { specs = []; }
500
+ if (specs.length === 0) {
501
+ state.dsManifestNudge = true;
502
+ state.dsManifestReason = 'no per-component specs found';
503
+ } else {
504
+ // Cheap sample: does ANY spec carry a frontmatter HEAD (starts with `---`)?
505
+ const structured = specs.slice(0, 8).some((f) => {
506
+ try { return fs.readFileSync(path.join(compDir, f), 'utf8').startsWith('---'); }
507
+ catch (_) { return false; }
508
+ });
509
+ if (!structured) {
510
+ state.dsManifestNudge = true;
511
+ state.dsManifestReason = 'specs are prose-only (no machine-readable HEAD)';
512
+ }
513
+ }
514
+ }
515
+ // DTCG token SSOT staleness.
516
+ const src = (config.paths && config.paths.design_tokens) || '';
517
+ const outs = (config.design_tokens && config.design_tokens.outputs) || [];
518
+ if (src && outs.length) {
519
+ const TokensGenerator = require('../utils/tokens-generator');
520
+ const st = new TokensGenerator(cwd).isStale({ source: src, outputs: outs });
521
+ if (st.stale) {
522
+ state.tokensStale = true;
523
+ state.tokensStalePaths = (st.reasons || []).map((r) => r.path);
524
+ state.tokensStaleError = st.error || null;
525
+ }
526
+ }
527
+ }
528
+ } catch (_) { /* never block doctor on design-system probe */ }
529
+
482
530
  // ---- Auto-deploy allowlist presence (since v4.59.0) ----------------
483
531
  // Purely informational: git.auto_deploy bounds what `/new -auto-ship`
484
532
  // may execute as an OUTWARD action. An empty allowlist is the SAFE
@@ -1013,6 +1061,43 @@ function planActions(state) {
1013
1061
  });
1014
1062
  }
1015
1063
 
1064
+ // ---- Design-system manifest + DTCG tokens (since v4.65.0) ----------
1065
+ // (1) Rebuild stale generated token artifacts from the DTCG SSOT — offline,
1066
+ // deterministic, idempotent → autoOk. (2) Nudge the manifest bootstrap when
1067
+ // the registry is present but specs are missing/prose-only (the "default-on
1068
+ // + nudge" behavior; informational, never auto-mutates source).
1069
+ if (state.tokensStale) {
1070
+ actions.push({
1071
+ key: 'tokens-build',
1072
+ label: state.tokensStaleError
1073
+ ? 'Design tokens: DTCG source has an error — fix then rebuild'
1074
+ : `Rebuild generated token artifacts from the DTCG source (${state.tokensStalePaths.join(', ')})`,
1075
+ why: state.tokensStaleError
1076
+ ? `paths.design_tokens is set but the DTCG source could not be parsed: ${state.tokensStaleError}. Fix the .tokens.json, then \`baldart tokens build\`.`
1077
+ : 'The generated token output(s) drifted from the DTCG SSOT (a hand-edit to a generated file, or the source changed without a rebuild). `baldart tokens build` regenerates them (offline, no API cost). Edit `.tokens.json`, never the generated output.',
1078
+ autoOk: !state.tokensStaleError, // a parse error needs a human; a plain rebuild is safe
1079
+ run: async () => {
1080
+ if (state.tokensStaleError) {
1081
+ UI.warning(`DTCG source error: ${state.tokensStaleError}. Fix it, then run \`baldart tokens build\`.`);
1082
+ return;
1083
+ }
1084
+ const tokens = require('./tokens');
1085
+ await tokens.build({ cwd: state.cwd });
1086
+ },
1087
+ });
1088
+ }
1089
+ if (state.dsManifestNudge) {
1090
+ actions.push({
1091
+ key: 'ds-manifest-nudge',
1092
+ label: `Design-system specs need a machine-readable upgrade — run /design-system-init (${state.dsManifestReason})`,
1093
+ why: `features.has_design_system is true but ${state.dsManifestReason}. Agents fall back to reading the monolithic INDEX + component source (slow). Running /design-system-init (upgrade mode) generates the per-component frontmatter HEAD + a thin INDEX router so discovery reads small structured specs on demand. See framework/agents/component-manifest-schema.md.`,
1094
+ autoOk: false, // a skill that reads source + enriches — never run unattended from doctor
1095
+ run: async () => {
1096
+ UI.info('Run `/design-system-init` in your editor to upgrade the per-component specs to the machine-readable HEAD and regenerate the thin INDEX router. See framework/docs/COMPONENT-MANIFEST-LAYER.md.');
1097
+ },
1098
+ });
1099
+ }
1100
+
1016
1101
  // Auto-deploy allowlist (since v4.59.0). Advisory print-only — surfaced ONLY
1017
1102
  // when the allowlist is non-empty (the safe default of empty/unset opens
1018
1103
  // nothing, so it warrants no nag). Reminds the user that `/new -auto-ship`
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `baldart tokens build` — regenerate the stack-native token artifacts from the
3
+ * DTCG SSOT (since v4.65.0).
4
+ *
5
+ * The design-token source of truth is the DTCG `.tokens.json` at
6
+ * `paths.design_tokens`; the artifacts agents/components consume (tokens.ts, a
7
+ * CSS custom-property sheet) are GENERATED from it and carry a
8
+ * `baldart-generated` banner. This verb is what the `/design-system-init` skill,
9
+ * `baldart doctor`, and (later) a git hook CALL — none of them reimplement the
10
+ * generation (the SSOT for that is `src/utils/tokens-generator.js`).
11
+ *
12
+ * Config consumed (baldart.config.yml):
13
+ * paths.design_tokens → the DTCG source (.tokens.json)
14
+ * design_tokens.outputs[] → [{ format: ts|css, path: <file> }]
15
+ *
16
+ * Always safe: no source / no outputs → a clear no-op message, exit 0. A bad
17
+ * DTCG file or a write failure → exit 1 with the reason (it IS a build step).
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const yaml = require('js-yaml');
23
+ const UI = require('../utils/ui');
24
+ const TokensGenerator = require('../utils/tokens-generator');
25
+
26
+ const CONFIG_FILE = 'baldart.config.yml';
27
+
28
+ function loadConfig(cwd) {
29
+ const full = path.join(cwd, CONFIG_FILE);
30
+ try { return yaml.load(fs.readFileSync(full, 'utf8')) || {}; }
31
+ catch (_) { return null; }
32
+ }
33
+
34
+ async function tokensBuild(opts = {}) {
35
+ const cwd = opts.cwd || process.cwd();
36
+ const json = !!opts.json;
37
+ const config = loadConfig(cwd);
38
+
39
+ const emit = (obj, exit = 0) => {
40
+ if (json) process.stdout.write(JSON.stringify(obj) + '\n');
41
+ return exit;
42
+ };
43
+
44
+ if (!config) {
45
+ if (!json) UI.warning(`No (or malformed) ${CONFIG_FILE} — run \`npx baldart configure\` first.`);
46
+ return emit({ schema: 'baldart.tokens-build/1', ok: false, reason: 'no-config' }, 1);
47
+ }
48
+
49
+ const source = config.paths && config.paths.design_tokens;
50
+ const outputs = (config.design_tokens && config.design_tokens.outputs) || [];
51
+
52
+ if (!source) {
53
+ if (!json) UI.info('No `paths.design_tokens` set — DTCG token SSOT not adopted on this project. Nothing to build.');
54
+ return emit({ schema: 'baldart.tokens-build/1', ok: true, skipped: 'no-source' });
55
+ }
56
+
57
+ const gen = new TokensGenerator(cwd);
58
+ if (!gen.detect(source)) {
59
+ if (!json) UI.warning(`design tokens source not found at \`${source}\`. Create it (e.g. via /design-system-init) then re-run.`);
60
+ return emit({ schema: 'baldart.tokens-build/1', ok: false, reason: 'source-missing', source }, 1);
61
+ }
62
+ if (!outputs.length) {
63
+ if (!json) UI.warning('`design_tokens.outputs` is empty — nothing to generate. Add at least one { format, path }.');
64
+ return emit({ schema: 'baldart.tokens-build/1', ok: true, skipped: 'no-outputs', source });
65
+ }
66
+
67
+ const res = gen.build({ source, outputs });
68
+ if (!res.ok) {
69
+ if (!json) UI.error(`Token build failed: ${res.error}`);
70
+ return emit({ schema: 'baldart.tokens-build/1', ok: false, reason: res.error, source }, 1);
71
+ }
72
+
73
+ if (!json) {
74
+ UI.success(`Tokens built from ${source} (${res.tokenCount} tokens):`);
75
+ for (const p of res.written) UI.info(` ✓ ${p}`);
76
+ }
77
+ return emit({ schema: 'baldart.tokens-build/1', ok: true, source, written: res.written, tokenCount: res.tokenCount });
78
+ }
79
+
80
+ module.exports = { build: tokensBuild };
@@ -1320,6 +1320,12 @@ async function update(options = {}, unknownArgs = []) {
1320
1320
  const missingI18n = Object.keys(tpl.i18n || {})
1321
1321
  .filter((k) => !(k in (cur2.i18n || {})))
1322
1322
  .map((k) => `i18n.${k}`);
1323
+ // `design_tokens:` (since v4.65.0) — same nested-block contract.
1324
+ // Its only key `outputs` is an ARRAY, so (per the v4.46.0 array
1325
+ // lesson) we diff presence of the key, not deep value equality.
1326
+ const missingDesignTokens = Object.keys(tpl.design_tokens || {})
1327
+ .filter((k) => !(k in (cur2.design_tokens || {})))
1328
+ .map((k) => `design_tokens.${k}`);
1323
1329
  const allMissing = [
1324
1330
  ...missingPaths,
1325
1331
  ...missingFeatures,
@@ -1329,6 +1335,7 @@ async function update(options = {}, unknownArgs = []) {
1329
1335
  ...missingToolchain,
1330
1336
  ...missingToolchainCmds,
1331
1337
  ...missingI18n,
1338
+ ...missingDesignTokens,
1332
1339
  ];
1333
1340
  if (allMissing.length) {
1334
1341
  UI.newline();
@@ -0,0 +1,36 @@
1
+ # token-emitters
2
+
3
+ Per-format emitters for the DTCG token generator (`../tokens-generator.js`).
4
+ The DTCG `.tokens.json` (`paths.design_tokens` in `baldart.config.yml`) is the
5
+ design-token SSOT; each emitter turns the resolved token set into ONE
6
+ stack-native artifact. Same REGISTRY pattern as `lsp-adapters/`,
7
+ `routine-adapters/`, `tool-adapters/`, `toolchain-adapters/`.
8
+
9
+ ## Adding a format
10
+
11
+ 1. Create `<format>.js` exporting `module.exports = function emit({ resolved, nested, marker }) { return '<file contents>'; }`.
12
+ - `resolved`: `Map<dotId, { type, value }>` — fully ref-resolved leaf tokens, source order.
13
+ - `nested`: nested plain object of resolved **values** (DTCG tree minus `$type`/`$value`).
14
+ - `marker`: the generated-file banner line for your format (already comment-wrapped).
15
+ 2. Register it in `index.js`'s `EMITTERS` map.
16
+ 3. If the comment syntax differs from `//` and `/* */`, extend `markerFor()` in `index.js`.
17
+
18
+ That's it — `tokens-generator.js`, `baldart tokens build`, and `doctor` iterate
19
+ the registry; nothing else changes.
20
+
21
+ ## Invariants
22
+
23
+ - **Deterministic, diff-stable output**: emit in source order, no timestamps, no
24
+ randomness — so `isStale()` (on-disk vs fresh render byte-compare) is reliable.
25
+ - **Always emit the `marker`** as the first line — it is how humans and the
26
+ `framework-edit-gate` know the file is generated and must not be hand-edited.
27
+ - **Pure**: emitters never touch the filesystem; the generator writes.
28
+ - **Zero dependency**: no Style Dictionary, no npm — keep it portable (the build
29
+ path must run identically under Codex).
30
+
31
+ ## Shipped formats
32
+
33
+ | format | output |
34
+ |--------|--------|
35
+ | `ts` | `export const tokens = {…} as const;` + `export type Tokens` |
36
+ | `css` | `:root { --<dot-id-as-dashes>: <value>; }` |
@@ -0,0 +1,18 @@
1
+ // CSS emitter: DTCG → `:root { --<dot-id-as-dashes>: <value>; }`.
2
+ //
3
+ // Token id `color.action.primary` → `--color-action-primary`. Emits resolved
4
+ // values (refs collapsed) in source order so the output is diff-stable.
5
+
6
+ function cssVarName(dotId) {
7
+ return `--${dotId.replace(/\./g, '-')}`;
8
+ }
9
+
10
+ module.exports = function emitCss({ resolved, marker }) {
11
+ const lines = [];
12
+ for (const [id, tok] of resolved) {
13
+ lines.push(` ${cssVarName(id)}: ${tok.value};`);
14
+ }
15
+ return `${marker}\n:root {\n${lines.join('\n')}\n}\n`;
16
+ };
17
+
18
+ module.exports.cssVarName = cssVarName;
@@ -0,0 +1,47 @@
1
+ // Token emitter registry (since v4.65.0).
2
+ //
3
+ // DTCG (`.tokens.json`) is the design-token SSOT; each emitter turns the
4
+ // resolved token set into ONE stack-native artifact (a TS object, a CSS
5
+ // custom-property sheet, …). This is the same REGISTRY pattern as
6
+ // `lsp-adapters/`, `routine-adapters/`, `tool-adapters/`, `toolchain-adapters/`:
7
+ // every other layer iterates this map, so adding a format = drop a file +
8
+ // register it here. Zero npm dependency (no Style Dictionary) — see
9
+ // `framework/docs/COMPONENT-MANIFEST-LAYER.md`.
10
+ //
11
+ // An emitter exports a single function:
12
+ // emit({ resolved, nested, marker }) -> string (the file contents)
13
+ // where:
14
+ // resolved : Map<dotId, { type, value }> fully ref-resolved leaf tokens
15
+ // nested : nested plain object of resolved VALUES (DTCG tree minus $type/$value)
16
+ // marker : the "baldart-generated" banner line for this format
17
+
18
+ const ts = require('./ts');
19
+ const css = require('./css');
20
+
21
+ const EMITTERS = { ts, css };
22
+
23
+ /** Stable, format-appropriate generated-file banner. */
24
+ function markerFor(format) {
25
+ const text = 'baldart-generated from .tokens.json — do not edit (run `baldart tokens build`)';
26
+ if (format === 'css') return `/* ${text} */`;
27
+ return `// ${text}`; // ts / js / any C-style
28
+ }
29
+
30
+ /**
31
+ * Emit one output. Returns { ok, format, content } or { ok:false, error }.
32
+ * Pure — does not touch the filesystem (the caller writes).
33
+ */
34
+ function emit(format, { resolved, nested }) {
35
+ const fn = EMITTERS[format];
36
+ if (typeof fn !== 'function') {
37
+ return { ok: false, error: `unknown token output format: "${format}" (known: ${Object.keys(EMITTERS).join(', ')})` };
38
+ }
39
+ const marker = markerFor(format);
40
+ try {
41
+ return { ok: true, format, content: fn({ resolved, nested, marker }) };
42
+ } catch (e) {
43
+ return { ok: false, error: `emitter "${format}" failed: ${e.message}` };
44
+ }
45
+ }
46
+
47
+ module.exports = { emit, markerFor, knownFormats: () => Object.keys(EMITTERS) };
@@ -0,0 +1,32 @@
1
+ // TS emitter: DTCG → a typed `export const tokens = {...} as const;` object.
2
+ //
3
+ // Emits the nested tree of RESOLVED values (refs already collapsed), mirroring
4
+ // the DTCG group structure minus the $type/$value wrappers. Deterministic key
5
+ // order (insertion order of the source) so the output is diff-stable.
6
+
7
+ function literal(value) {
8
+ // Tokens resolve to strings (colors, dimensions, durations) or numbers.
9
+ if (typeof value === 'number') return String(value);
10
+ return JSON.stringify(String(value));
11
+ }
12
+
13
+ function render(node, indent) {
14
+ const pad = ' '.repeat(indent);
15
+ const padInner = ' '.repeat(indent + 1);
16
+ const keys = Object.keys(node);
17
+ const lines = keys.map((k) => {
18
+ const v = node[k];
19
+ // A safe JS identifier key can be bare; otherwise quote it (e.g. "2").
20
+ const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
21
+ if (v && typeof v === 'object') {
22
+ return `${padInner}${key}: ${render(v, indent + 1)},`;
23
+ }
24
+ return `${padInner}${key}: ${literal(v)},`;
25
+ });
26
+ return `{\n${lines.join('\n')}\n${pad}}`;
27
+ }
28
+
29
+ module.exports = function emitTs({ nested, marker }) {
30
+ const body = render(nested, 0);
31
+ return `${marker}\nexport const tokens = ${body} as const;\n\nexport type Tokens = typeof tokens;\n`;
32
+ };