baldart 4.66.1 → 4.66.2
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 +11 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/doc-reviewer.md +5 -1
- package/framework/.claude/skills/design-system-init/SKILL.md +14 -1
- package/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs +54 -2
- package/framework/agents/component-manifest-schema.md +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.66.2] - 2026-06-23
|
|
9
|
+
|
|
10
|
+
**`token_bindings` are now validated against the real DTCG vocabulary — fixing dangling bindings found on a real upgrade.** After the v4.66.1 HEAD-serialization fix, a real `/design-system-init` upgrade still produced 26/586 `token_bindings` that didn't resolve in `.tokens.json` (`font.weight.medium` instead of `typography.fontWeight.medium`, `z.overlay` vs `zIndex.overlay`, `space.2` vs `space.sm`, …). Root cause: the agentic enricher (`doc-reviewer`) writes `token_bindings` as free text and guesses plausible-but-wrong names. A dangling binding id is `DS_TOKENS_DRIFT`. This closes the gap so it cannot be written, and instructs the enricher to pick from the actual token vocabulary.
|
|
11
|
+
|
|
12
|
+
**PATCH** — refines the v4.65.0/v4.66.1 layer; no config/install change.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Deterministic binding vocabulary guard** — `serialize-spec.mjs` gains `--tokens <.tokens.json>`: it flattens the DTCG to its real id set and **drops + reports** any `token_bindings` id that does not resolve (the `droppedBindings` count + the dropped list on stderr) — so a dangling binding can never be serialized. Exposed as `tokenIds()` + `validateBindings()`. Proven: `z.overlay`/`font.weight.medium` dropped, `zIndex.overlay`/`typography.fontWeight.medium`/`color.surface` kept.
|
|
17
|
+
- **Prevention at the source** — `/design-system-init` step 4 + `doc-reviewer` (HEAD owner) + `component-manifest-schema.md` now require `token_bindings` to be chosen from the real `${paths.design_tokens}` id list (exact ids, never invented names). The serializer invocation passes `--tokens ${paths.design_tokens}` so the guard runs every generation.
|
|
18
|
+
|
|
8
19
|
## [4.66.1] - 2026-06-23
|
|
9
20
|
|
|
10
21
|
**The component-manifest HEAD is now serialized DETERMINISTICALLY — fixing invalid-YAML HEADs found on a real `/design-system-init` upgrade.** A real upgrade run produced 74 per-component specs but **73/74 frontmatter HEADs failed to parse as YAML** — the v4.65.0 skill let the model hand-write the HEAD, so it emitted `props:{}` / `variants:[]` (no space after the colon) and unquoted/multiline `purpose`/`a11y` strings (embedded `:`, quotes, newlines). The HEADs were unparseable → "frontmatter-first" discovery silently fell back → the machine-readable benefit was lost. Same run also wired the DTCG token output to **overwrite a hand-authored `tokens.ts`** (semantic exports `uiTokens`/`categoryPalette` replaced by a generic `export const tokens` — would break every import). This release closes the model-in-the-loop-for-a-deterministic-artifact gap (the recurring lesson behind `setup-worktree.sh` / `merge-worktree.sh`).
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.66.
|
|
1
|
+
4.66.2
|
|
@@ -67,7 +67,11 @@ documentation scope.
|
|
|
67
67
|
`token_bindings`, `related`, `must_rules`, `last_verified`. You curate these —
|
|
68
68
|
you do NOT hand-write the deterministic half (`name`/`source`/`source_sha`/
|
|
69
69
|
`props`/`variants`/`composes`), which is regenerated from source by the extractor.
|
|
70
|
-
Never clobber a filled agentic field on a refresh.
|
|
70
|
+
Never clobber a filled agentic field on a refresh. **Pick `token_bindings` from
|
|
71
|
+
the REAL DTCG vocabulary** — read the id list from `${paths.design_tokens}` and
|
|
72
|
+
use exact ids (`typography.fontWeight.medium`, `zIndex.overlay`, `space.sm`),
|
|
73
|
+
never invented names (`font.weight.medium`, `z.overlay`, `space.2`); the
|
|
74
|
+
serializer drops unresolved ids as drift. (This is the design-system twin
|
|
71
75
|
of your i18n-registry ownership: you curate context, tooling fills the mechanical part.)
|
|
72
76
|
|
|
73
77
|
Standard drift checks against this corpus:
|
|
@@ -91,6 +91,12 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
91
91
|
mode, lift them from each spec's existing prose / **salvage** them from the
|
|
92
92
|
current HEAD even if it is malformed. Never clobber a filled agentic field on
|
|
93
93
|
re-run. Under Codex (no subagents) this degrades to empty fields + a TODO note.
|
|
94
|
+
**`token_bindings` MUST be chosen from the REAL token vocabulary** — read the
|
|
95
|
+
id list from `${paths.design_tokens}` (the DTCG `.tokens.json`) and pick exact
|
|
96
|
+
ids (`typography.fontWeight.medium`, `zIndex.overlay`, `space.sm`), never
|
|
97
|
+
invent plausible-but-wrong names (`font.weight.medium`, `z.overlay`, `space.2`).
|
|
98
|
+
The serializer's `--tokens` guard (step 6) drops any unknown id, but picking
|
|
99
|
+
correctly here avoids losing real intent.
|
|
94
100
|
5. **Bootstrap the DTCG token SSOT.** See § Token SSOT bootstrap below.
|
|
95
101
|
6. **Write the files — DETERMINISTICALLY (the HEAD is NEVER model-authored).**
|
|
96
102
|
Merge the deterministic JSON (step 3) with the agentic JSON (step 4) per
|
|
@@ -98,7 +104,8 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
98
104
|
```
|
|
99
105
|
node .framework/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs \
|
|
100
106
|
--bundle <bundle.json> --out-dir ${paths.design_system}/components \
|
|
101
|
-
--index ${paths.design_system}/INDEX.md --brand "${identity.brand_name}"
|
|
107
|
+
--index ${paths.design_system}/INDEX.md --brand "${identity.brand_name}" \
|
|
108
|
+
--tokens ${paths.design_tokens}
|
|
102
109
|
```
|
|
103
110
|
It emits every `components/<Name>.md` (valid-YAML HEAD via deterministic
|
|
104
111
|
serialization + preserved prose) **and** the thin INDEX router, then
|
|
@@ -107,6 +114,12 @@ precedence caveats: `framework/agents/effort-protocol.md`.
|
|
|
107
114
|
model-hand-written-YAML bug class (`props:{}` without a space, unquoted/multiline
|
|
108
115
|
`purpose`/`a11y`): the model supplies field *data*, the script owns the YAML.
|
|
109
116
|
Do NOT write the HEAD by hand or via the prose template.
|
|
117
|
+
`--tokens` (when `paths.design_tokens` is set) activates the **binding
|
|
118
|
+
vocabulary guard**: any `token_bindings` id that does not resolve in the DTCG
|
|
119
|
+
`.tokens.json` is DROPPED and REPORTED (the `droppedBindings` count + the list
|
|
120
|
+
on stderr) — so a `DS_TOKENS_DRIFT` can never be written. Review the dropped
|
|
121
|
+
list: a drop means doc-reviewer guessed a name that isn't a real token (fix the
|
|
122
|
+
enrichment or add the token), per step 4.
|
|
110
123
|
- `tokens-reference.md` — **generated** human view of `.tokens.json` (or pure
|
|
111
124
|
narrative). Not a third source of values.
|
|
112
125
|
- `patterns/.gitkeep` — empty dir for future pattern docs.
|
|
@@ -135,19 +135,71 @@ function arg(name, def = '') {
|
|
|
135
135
|
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : def;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** Flatten a DTCG `.tokens.json` tree to the set of dot-path token ids. */
|
|
139
|
+
export function tokenIds(dtcg) {
|
|
140
|
+
const ids = new Set();
|
|
141
|
+
(function walk(node, prefix) {
|
|
142
|
+
if (!node || typeof node !== 'object') return;
|
|
143
|
+
if (Object.prototype.hasOwnProperty.call(node, '$value')) { ids.add(prefix); return; }
|
|
144
|
+
for (const k of Object.keys(node)) {
|
|
145
|
+
if (k.startsWith('$')) continue;
|
|
146
|
+
walk(node[k], prefix ? `${prefix}.${k}` : k);
|
|
147
|
+
}
|
|
148
|
+
})(dtcg, '');
|
|
149
|
+
return ids;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Drop `token_bindings` ids that don't resolve in the DTCG vocabulary. The
|
|
154
|
+
* agentic enricher (doc-reviewer) writes bindings as free text and can guess
|
|
155
|
+
* names that don't exist (`font.*` vs `typography.*`, `z.*` vs `zIndex.*`); a
|
|
156
|
+
* dangling id is DS_TOKENS_DRIFT. This is the deterministic guard: keep only
|
|
157
|
+
* real ids, return what was dropped so it is SURFACED (never silently lost).
|
|
158
|
+
*/
|
|
159
|
+
export function validateBindings(components, idSet) {
|
|
160
|
+
const dropped = [];
|
|
161
|
+
for (const c of components) {
|
|
162
|
+
const kept = [];
|
|
163
|
+
for (const id of c.token_bindings || []) {
|
|
164
|
+
if (idSet.has(id)) kept.push(id);
|
|
165
|
+
else dropped.push({ component: c.name, id });
|
|
166
|
+
}
|
|
167
|
+
c.token_bindings = [...new Set(kept)];
|
|
168
|
+
}
|
|
169
|
+
return dropped;
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
// CLI
|
|
139
173
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
140
174
|
const bundlePath = arg('--bundle');
|
|
141
175
|
const outDir = arg('--out-dir');
|
|
142
176
|
const indexPath = arg('--index');
|
|
143
177
|
const brandArg = arg('--brand');
|
|
178
|
+
const tokensPath = arg('--tokens'); // optional DTCG .tokens.json → binding vocab guard
|
|
144
179
|
if (!bundlePath || !outDir) {
|
|
145
|
-
console.error('usage: serialize-spec.mjs --bundle bundle.json --out-dir <dir> [--index <path>] [--brand <name>]');
|
|
180
|
+
console.error('usage: serialize-spec.mjs --bundle bundle.json --out-dir <dir> [--index <path>] [--brand <name>] [--tokens <.tokens.json>]');
|
|
146
181
|
process.exit(2);
|
|
147
182
|
}
|
|
148
183
|
const bundle = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
|
|
149
184
|
const components = bundle.components || [];
|
|
150
185
|
const brand = brandArg || bundle.brand || 'Project';
|
|
186
|
+
|
|
187
|
+
// Token-binding vocabulary guard (since v4.66.2): drop ids absent from the DTCG.
|
|
188
|
+
let dropped = [];
|
|
189
|
+
if (tokensPath) {
|
|
190
|
+
try {
|
|
191
|
+
const ids = tokenIds(JSON.parse(fs.readFileSync(tokensPath, 'utf8')));
|
|
192
|
+
dropped = validateBindings(components, ids);
|
|
193
|
+
if (dropped.length) {
|
|
194
|
+
console.error(`token_bindings: dropped ${dropped.length} id(s) not in ${path.basename(tokensPath)} (DS_TOKENS_DRIFT avoided):`);
|
|
195
|
+
for (const d of dropped.slice(0, 20)) console.error(` - ${d.component}: ${d.id}`);
|
|
196
|
+
if (dropped.length > 20) console.error(` … +${dropped.length - 20} more`);
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.error(`token vocab guard skipped: cannot read ${tokensPath} (${e.message.split('\n')[0]})`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
151
203
|
fs.mkdirSync(outDir, { recursive: true });
|
|
152
204
|
let invalid = 0;
|
|
153
205
|
for (const c of components) {
|
|
@@ -158,7 +210,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
158
210
|
if (v.checked && !v.ok) { invalid++; console.error(`INVALID HEAD ${c.name}.md: ${v.error}`); }
|
|
159
211
|
}
|
|
160
212
|
if (indexPath) fs.writeFileSync(indexPath, emitIndex(components, brand), 'utf8');
|
|
161
|
-
const report = { schema: 'baldart.serialize-spec/1', written: components.length, index: !!indexPath, invalid };
|
|
213
|
+
const report = { schema: 'baldart.serialize-spec/1', written: components.length, index: !!indexPath, invalid, droppedBindings: dropped.length };
|
|
162
214
|
console.log(JSON.stringify(report));
|
|
163
215
|
if (invalid > 0) process.exit(1); // fail-closed: never leave invalid HEADs
|
|
164
216
|
}
|
|
@@ -115,6 +115,15 @@ and `baldart doctor` treat an unparseable HEAD as drift to repair (re-run
|
|
|
115
115
|
**salvage** the agentic values leniently, then re-serialize — never strict-parse
|
|
116
116
|
and lose them.
|
|
117
117
|
|
|
118
|
+
**Token-binding vocabulary invariant:** every `token_bindings` id MUST resolve to
|
|
119
|
+
a real node in the DTCG `.tokens.json` (`paths.design_tokens`). The agentic
|
|
120
|
+
enricher picks ids from that file's actual vocabulary — never invents
|
|
121
|
+
plausible-but-wrong names (`font.weight.medium`/`z.overlay`/`space.2` instead of
|
|
122
|
+
`typography.fontWeight.medium`/`zIndex.overlay`/`space.sm`). The serializer's
|
|
123
|
+
`--tokens` guard DROPS + REPORTS any unresolved id (so a `DS_TOKENS_DRIFT` is
|
|
124
|
+
never written); a dropped id means the enricher guessed wrong (fix it or add the
|
|
125
|
+
token).
|
|
126
|
+
|
|
118
127
|
## Regeneration contract
|
|
119
128
|
|
|
120
129
|
- **Anchor**: `source_sha`. A regeneration run skips a component whose source is
|