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.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/bin/baldart.js +15 -0
- package/framework/.claude/agents/code-reviewer.md +25 -11
- package/framework/.claude/agents/codebase-architect.md +13 -5
- package/framework/.claude/agents/doc-reviewer.md +26 -12
- package/framework/.claude/agents/ui-expert.md +18 -10
- package/framework/.claude/skills/context-primer/SKILL.md +7 -0
- package/framework/.claude/skills/design-system-init/SKILL.md +164 -156
- package/framework/.claude/skills/design-system-init/scripts/component-spec.template.md +26 -3
- package/framework/.claude/skills/design-system-init/scripts/extract-manifest.mjs +151 -0
- package/framework/.claude/skills/new/references/final-review.md +12 -2
- package/framework/.claude/skills/new/references/team-mode.md +1 -1
- package/framework/.claude/workflows/new-card-review.js +67 -13
- package/framework/.claude/workflows/new-final-review.js +63 -7
- package/framework/agents/component-manifest-schema.md +145 -0
- package/framework/agents/design-system-protocol.md +63 -23
- package/framework/agents/index.md +2 -0
- package/framework/docs/COMPONENT-MANIFEST-LAYER.md +100 -0
- package/framework/routines/ds-drift.routine.yml +19 -12
- package/framework/templates/baldart.config.template.yml +22 -0
- package/package.json +1 -1
- package/src/commands/configure.js +54 -0
- package/src/commands/doctor.js +85 -0
- package/src/commands/tokens.js +80 -0
- package/src/commands/update.js +7 -0
- package/src/utils/token-emitters/README.md +36 -0
- package/src/utils/token-emitters/css.js +18 -0
- package/src/utils/token-emitters/index.js +47 -0
- package/src/utils/token-emitters/ts.js +32 -0
- 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**:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
2. **Per-component accuracy**: for each
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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 (
|
|
34
|
-
|
|
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
|
@@ -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
|
package/src/commands/doctor.js
CHANGED
|
@@ -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 };
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
+
};
|