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,165 @@
1
+ // Tokens generator — thin orchestrator over the DTCG SSOT (since v4.65.0).
2
+ //
3
+ // DTCG (`.tokens.json`, W3C Design Tokens Community Group: `$type`/`$value`,
4
+ // `{ref}` aliases) is the design-token source of truth. This class reads it,
5
+ // resolves aliases, and fans out to the per-format emitters in
6
+ // `token-emitters/` to write the stack-native artifacts (tokens.ts, CSS vars).
7
+ // Zero npm dependency — DTCG parsing is plain JSON traversal. Mirrors the thin-
8
+ // wrapper shape of `graphify-installer.js` (detect / isStale / build).
9
+ //
10
+ // Authoritative design: framework/docs/COMPONENT-MANIFEST-LAYER.md.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const emitters = require('./token-emitters');
15
+
16
+ class TokensGenerator {
17
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
18
+
19
+ /** True when the DTCG source file exists. */
20
+ detect(source) {
21
+ if (!source) return false;
22
+ try { return fs.statSync(path.join(this.cwd, source)).isFile(); }
23
+ catch (_) { return false; }
24
+ }
25
+
26
+ /** Read + JSON.parse the DTCG source. Throws a clear error on bad input. */
27
+ load(source) {
28
+ const abs = path.join(this.cwd, source);
29
+ let raw;
30
+ try { raw = fs.readFileSync(abs, 'utf8'); }
31
+ catch (e) { throw new Error(`cannot read design tokens at ${source}: ${e.message}`); }
32
+ try { return JSON.parse(raw); }
33
+ catch (e) { throw new Error(`${source} is not valid JSON: ${e.message}`); }
34
+ }
35
+
36
+ /**
37
+ * Flatten the DTCG tree to leaf tokens. A node is a token when it carries
38
+ * `$value`. `$type` is inherited from the nearest ancestor that declares it.
39
+ * Returns Map<dotId, { type, raw }> in source order.
40
+ */
41
+ flatten(tree) {
42
+ const out = new Map();
43
+ const walk = (node, prefix, inheritedType) => {
44
+ if (!node || typeof node !== 'object') return;
45
+ const type = Object.prototype.hasOwnProperty.call(node, '$type') ? node.$type : inheritedType;
46
+ if (Object.prototype.hasOwnProperty.call(node, '$value')) {
47
+ out.set(prefix, { type, raw: node.$value });
48
+ return;
49
+ }
50
+ for (const key of Object.keys(node)) {
51
+ if (key.startsWith('$')) continue; // $type / $description / $extensions
52
+ walk(node[key], prefix ? `${prefix}.${key}` : key, type);
53
+ }
54
+ };
55
+ walk(tree, '', undefined);
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Resolve `{a.b.c}` aliases to concrete values (chains + cycle guard).
61
+ * Returns Map<dotId, { type, value }> in the same order as `flat`.
62
+ */
63
+ resolve(flat) {
64
+ const resolved = new Map();
65
+ const resolveOne = (id, seen) => {
66
+ if (resolved.has(id)) return resolved.get(id);
67
+ const tok = flat.get(id);
68
+ if (!tok) throw new Error(`token reference {${id}} does not resolve to a known token`);
69
+ let value = tok.raw;
70
+ if (typeof value === 'string') {
71
+ const m = value.match(/^\{([^}]+)\}$/);
72
+ if (m) {
73
+ const ref = m[1];
74
+ if (seen.has(ref)) throw new Error(`cyclic token reference: ${[...seen, ref].join(' → ')}`);
75
+ const target = resolveOne(ref, new Set([...seen, id]));
76
+ value = target.value;
77
+ }
78
+ }
79
+ const entry = { type: tok.type, value };
80
+ resolved.set(id, entry);
81
+ return entry;
82
+ };
83
+ for (const id of flat.keys()) resolveOne(id, new Set());
84
+ return resolved;
85
+ }
86
+
87
+ /** Build the nested object of resolved VALUES (DTCG tree minus $wrappers). */
88
+ nest(resolved) {
89
+ const root = {};
90
+ for (const [id, tok] of resolved) {
91
+ const parts = id.split('.');
92
+ let cur = root;
93
+ for (let i = 0; i < parts.length - 1; i++) {
94
+ cur[parts[i]] = cur[parts[i]] || {};
95
+ cur = cur[parts[i]];
96
+ }
97
+ cur[parts[parts.length - 1]] = tok.value;
98
+ }
99
+ return root;
100
+ }
101
+
102
+ /**
103
+ * Produce { outputs: [{ format, path, content }], resolved } WITHOUT writing.
104
+ * Used by both build() and isStale().
105
+ */
106
+ render({ source, outputs }) {
107
+ const tree = this.load(source);
108
+ const flat = this.flatten(tree);
109
+ if (flat.size === 0) throw new Error(`${source} contains no DTCG tokens ($value leaves)`);
110
+ const resolved = this.resolve(flat);
111
+ const nested = this.nest(resolved);
112
+ const rendered = [];
113
+ for (const o of outputs || []) {
114
+ const r = emitters.emit(o.format, { resolved, nested });
115
+ if (!r.ok) throw new Error(r.error);
116
+ rendered.push({ format: o.format, path: o.path, content: r.content });
117
+ }
118
+ return { outputs: rendered, resolved };
119
+ }
120
+
121
+ /**
122
+ * Build: render + write every output. Returns { ok, written:[paths] } or
123
+ * { ok:false, error }.
124
+ */
125
+ build({ source, outputs }) {
126
+ let r;
127
+ try { r = this.render({ source, outputs }); }
128
+ catch (e) { return { ok: false, error: e.message }; }
129
+ const written = [];
130
+ for (const o of r.outputs) {
131
+ const abs = path.join(this.cwd, o.path);
132
+ try {
133
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
134
+ fs.writeFileSync(abs, o.content, 'utf8');
135
+ written.push(o.path);
136
+ } catch (e) {
137
+ return { ok: false, error: `failed writing ${o.path}: ${e.message}`, written };
138
+ }
139
+ }
140
+ return { ok: true, written, tokenCount: r.resolved.size };
141
+ }
142
+
143
+ /**
144
+ * isStale: true when any output on disk differs from a fresh render (i.e. a
145
+ * hand-edit to a generated file, or the source changed without a rebuild).
146
+ * Returns { stale, reasons:[{path, reason}] } or { stale:false } / error.
147
+ */
148
+ isStale({ source, outputs }) {
149
+ if (!this.detect(source)) return { stale: false, sourceMissing: true };
150
+ let r;
151
+ try { r = this.render({ source, outputs }); }
152
+ catch (e) { return { stale: true, error: e.message, reasons: [] }; }
153
+ const reasons = [];
154
+ for (const o of r.outputs) {
155
+ const abs = path.join(this.cwd, o.path);
156
+ let onDisk;
157
+ try { onDisk = fs.readFileSync(abs, 'utf8'); }
158
+ catch (_) { reasons.push({ path: o.path, reason: 'missing' }); continue; }
159
+ if (onDisk !== o.content) reasons.push({ path: o.path, reason: 'out-of-date' });
160
+ }
161
+ return { stale: reasons.length > 0, reasons };
162
+ }
163
+ }
164
+
165
+ module.exports = TokensGenerator;