baldart 4.66.2 → 4.67.1

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 CHANGED
@@ -5,6 +5,32 @@ 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.67.1] - 2026-06-24
9
+
10
+ **Rule C is now i18n-aware — a presentational card that only changes labels no longer gets forced to `balanced`.** Found on a real `mayo` run: FEAT-0050 is a "PURAMENTE PRESENTAZIONALE" fidelity-drift epic, yet its child cards ran the full per-card review cluster (full-depth Codex + simplify) instead of the lighter UI path. Root cause: the i18n layer (v4.52.0) silently broke the v4.x UI-presentational carve-out in `prd-card-writer.md § Rule C`. The LIGHT branch (b) requires "≤3 files, every one a component/style file", and the SKIP rule requires "all files .md/.yml/CSS" — but with `features.has_i18n: true`, every label change forces an edit to **all** native locale files (`it.ts`/`en.ts`/`de.ts`/`es.ts`/`fr.ts`), which blows the ≤3 count and injects non-component `.ts` files. So a genuinely pixels-only card was structurally incapable of being classified `light`/`skip`.
11
+
12
+ **PATCH** — corrects a blind spot in existing Rule C logic; reuses `features.has_i18n` + `i18n.locales_root` (both already in the schema), no new config key, no install change. Schema-change propagation rule does NOT apply.
13
+
14
+ ### Fixed
15
+
16
+ - **i18n locale-file transparency in Rule C** (`prd-card-writer.md`) — when `features.has_i18n: true`, locale files under `i18n.locales_root` are now TRANSPARENT to BOTH the LIGHT branch (b) and the SKIP rule: excluded from the file count AND the component/style test, classifying the card on its non-locale files only. A locale file is a flat string table whose correctness is owned by the i18n gate (anti-hardcoded lint) + `i18n-translator`, NOT the logic cluster — so it must not disqualify the presentational carve-out. Effect: a genuinely-presentational card (`.tsx`/`.css` + N locale files) now correctly lands on `light` (full→light Codex depth, doc-review deferred to the Final FULL gate), and a pure-cosmetic CSS/copy card that retouches locale strings now correctly lands on `skip`. The DS safety net is unchanged — a `light` card still runs its per-card Codex finder + `code-reviewer` rule 8 / registry-first cascade, plus the batch-wide Final FULL gate.
17
+ - **Why Codex stays per-card (not deferred to Final)** — deferring per-card Codex onto the Final gate for UI cards was considered and rejected: it reverts the deliberate v4.18.0 choice (at `light`, Codex is the SOLE deep finder) and re-opens the refuted `slimCodex` design (v4.56.1 — per-card runs PRE-fix/PRE-E2E, the Final runs POST-fix over the whole batch; they review different states). The sanctioned lever for "less Codex on a graphic card" is the `light` profile's reduced Codex depth, which this fix now lets presentational cards actually reach.
18
+
19
+ ## [4.67.0] - 2026-06-23
20
+
21
+ **Token-binding CORRECTNESS check + PRD-Inventory-sourced enrichment — the survivor of an adversarial pass that killed a design-time "intent seed".** Goal: capture design intent ("when/how/which-tokens") so the implementing agent doesn't reverse-engineer the manifest's agentic fields from code. The proposed `component-intent.yml` seed (emitted by `/prd`) was refuted 3/3 — it duplicates the PRD's existing UI Element Inventory + the manifest (twin), goes stale via `/prd-add` REDO, survives-but-wrong (the v4.66.2 guard checks token EXISTENCE, not correctness-for-this-component), and as an on-disk prose file would be skipped/unowned/Codex-inert. This ships the deterministic survivors instead.
22
+
23
+ **MINOR** — adds a capability (usage check + intent-source wiring); no new config key, no install change. Schema-change propagation rule does NOT apply.
24
+
25
+ ### Added
26
+
27
+ - **Token-binding correctness check (`serialize-spec.mjs --verify-usage`)** — closes the half v4.66.2 left open (existence ≠ correctness): a binding can exist in the DTCG yet be wrong for this component. `verifyBindingUsage(components, cwd)` (exported) reads each component's effective styling surface — its own `source` (+ sibling `.module.css`/`.css`) **plus the sources of the primitives it directly `composes`** (a wrapper presenting an overlay via a composed Popover legitimately binds `zIndex.overlay`) — and reports any `token_bindings` id absent from that surface as `unverifiedBindings`. **ADVISORY, never a drop/block** (a binding can be legitimately indirect — Tailwind classes, a non-sibling stylesheet — so a hard block would false-fire). Resolves source from the `source` field, never a guessed path. Validated on mayo: 96% of 603 bindings verified; the residual surfaces for review, exit 0.
28
+
29
+ ### Changed
30
+
31
+ - **Intent-sourced enrichment (no new artifact)** — `doc-reviewer` + `/design-system-init` step 4 now fill the agentic HEAD fields (`purpose`/`a11y`/`must_rules`) from the PRD's existing **UI Element Inventory** (`Purpose`/`function_ref`/`Behavior`/`States`) when the component traces to a PRD (card `links.prd`/`links.design`), reconciled against the implemented source — capturing design-time intent instead of reverse-engineering it. Clean fallback to source-only when no PRD exists. `token_bindings` are now picked preferring ids the source actually uses.
32
+ - **Advisory propagated to the three Post-Intervention sites** — `design-system-protocol.md` (check 3), `code-reviewer.md` (Rule 8), and the `ds-drift` routine now flag a resolves-but-unused binding as a `DS_TOKENS_DRIFT` **advisory** (review-required: fix or justify as indirect), distinct from the hard existence drift. SSOT'd in `component-manifest-schema.md` (existence = HARD/drop; usage = ADVISORY/report). `/design-system-init` passes `--verify-usage` on every generation.
33
+
8
34
  ## [4.66.2] - 2026-06-23
9
35
 
10
36
  **`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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.66.2
1
+ 4.67.1
@@ -58,7 +58,11 @@ Concrete enforcement contract for the review pass:
58
58
  2. For any component in the diff, cross-check against its
59
59
  `components/<Name>.md` HEAD — variants, props, `token_bindings`, a11y,
60
60
  `must_rules`. A `token_bindings` ID that does not resolve in the DTCG source
61
- (`paths.design_tokens`) is `DS_TOKENS_DRIFT`.
61
+ (`paths.design_tokens`) is `DS_TOKENS_DRIFT` (hard). A `token_bindings` ID that
62
+ resolves but is NOT used in the component's effective styling surface (own
63
+ source + directly-composed children) is a `DS_TOKENS_DRIFT` **advisory** (flag
64
+ for review — the binding is likely over-attributed or wrong; not a merge-block,
65
+ since it can be legitimately indirect).
62
66
  3. Enforce design-system MUST rules (the spec HEAD's `must_rules` + project INDEX)
63
67
  as HIGH-confidence findings. Typical rules include:
64
68
  - No hardcoded hex, shadow, radius, spacing literals — only tokens. When
@@ -71,7 +71,20 @@ Never clobber a filled agentic field on a refresh. **Pick `token_bindings` from
71
71
  the REAL DTCG vocabulary** — read the id list from `${paths.design_tokens}` and
72
72
  use exact ids (`typography.fontWeight.medium`, `zIndex.overlay`, `space.sm`),
73
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
74
+ serializer drops unresolved ids (existence) and **flags ids not used in the
75
+ component's source** (correctness, advisory) — so prefer the ids the source
76
+ actually consumes.
77
+
78
+ **Intent source order (when the component traces to a PRD/design).** Do NOT
79
+ reverse-engineer intent from code alone when a richer source exists. If the work
80
+ traces to a PRD (the card's `links.prd` / `links.design`), read that PRD's **UI
81
+ Element Inventory** row for this component (its `Purpose`, `function_ref`,
82
+ `Behavior`, `States`) and use it as the **primary source** for `purpose` /
83
+ `a11y` / `must_rules` — it captures the design-time intent ("when/how to use")
84
+ that the implemented code does not encode. Then **reconcile against the
85
+ implemented source** (the deterministic fields + actual token usage are ground
86
+ truth; the Inventory is intent, not API). Fallback: when there is no PRD/Inventory
87
+ for the component, infer from the source as before. (This is the design-system twin
75
88
  of your i18n-registry ownership: you curate context, tooling fills the mechanical part.)
76
89
 
77
90
  Standard drift checks against this corpus:
@@ -214,13 +214,14 @@ Apply the **first matching rule** (priority order — top wins):
214
214
  | Profile | When to assign |
215
215
  |---------|----------------|
216
216
  | **DEEP** | ANY of: `areas` includes both `api` AND `data` — OR title/scope/areas touch `auth`, `payment`, `permission`, `schema`, `migration`, `cron`, `webhook`, `transaction` — OR a path in `files_likely_touched` falls under a `paths.high_risk_modules` entry — OR `data_fields` has ≥3 entries with `status: new` or `status: modified` — OR `db_indexes` present (legacy `firestore_indexes` too) — OR acceptance criteria count > 5 — OR `estimated_complexity: HIGH` — OR API contract changed — OR `> 15` files in `files_likely_touched` (broad blast radius — review every group deeply) |
217
- | **LIGHT** | EITHER (a) Card is a `bugfix` or `refactor` — AND ≤3 files in `files_likely_touched` — AND none of the DEEP high-risk keywords/areas apply; **OR** (b) **UI-presentational card** — ALL of: the card is pure-UI — `areas` == `[ui]` exactly (no `api`/`data`/`logic`/`auth`), or when `areas` is absent the Rule B signal `owner_agent: ui-expert` stands in for it — AND ≤3 files in `files_likely_touched`, every one a component/style file (no data-fetching, state-management, API-client, or server file) — AND NO path in `files_likely_touched` falls under `paths.components_primitives` (i.e. the card only *composes* existing design-system primitives, it does NOT introduce or modify one) — AND none of the DEEP high-risk keywords/areas apply. Branch (a) is **NEVER for `feature`/`enhancement` cards**; branch (b) is the ONLY route by which a `feature`/`enhancement` card may be `light`. |
218
- | **SKIP** | Card is `docs`, `chore`, or `config` — OR all `files_likely_touched` are `.md`/non-API `.yml`/CSS with zero logic files — OR a pure-cosmetic card (typo/rename/copy/wording/style) with no code areas |
217
+ | **LIGHT** | EITHER (a) Card is a `bugfix` or `refactor` — AND ≤3 files in `files_likely_touched` — AND none of the DEEP high-risk keywords/areas apply; **OR** (b) **UI-presentational card** — ALL of: the card is pure-UI — `areas` == `[ui]` exactly (no `api`/`data`/`logic`/`auth`), or when `areas` is absent the Rule B signal `owner_agent: ui-expert` stands in for it — AND ≤3 files in `files_likely_touched`, every one a component/style file (no data-fetching, state-management, API-client, or server file) — AND NO path in `files_likely_touched` falls under `paths.components_primitives` (i.e. the card only *composes* existing design-system primitives, it does NOT introduce or modify one) — AND none of the DEEP high-risk keywords/areas apply. **i18n locale-file transparency (when `features.has_i18n: true`):** locale files under `i18n.locales_root` are TRANSPARENT to this branch — exclude them from BOTH the `≤3` count AND the "every file is component/style" test, classifying the card on its NON-locale files only. A label change forces an edit to every locale file (`it.ts`/`en.ts`/…), which would otherwise push a genuinely-presentational card over the file count and inject non-component `.ts` files; but a locale file is a flat string table whose correctness is owned by the i18n gate (anti-hardcoded lint) + `i18n-translator`, NOT the logic cluster — so it must not disqualify the carve-out. Branch (a) is **NEVER for `feature`/`enhancement` cards**; branch (b) is the ONLY route by which a `feature`/`enhancement` card may be `light`. |
218
+ | **SKIP** | Card is `docs`, `chore`, or `config` — OR all `files_likely_touched` are `.md`/non-API `.yml`/CSS with zero logic files (**when `features.has_i18n: true`, locale files under `i18n.locales_root` are EXCLUDED from this set the same way they are for the LIGHT branch a pure-cosmetic CSS/copy fix that also retouches locale strings stays `skip`; classify on the non-locale files only**) — OR a pure-cosmetic card (typo/rename/copy/wording/style) with no code areas |
219
219
  | **BALANCED** | Default for everything else — ALL `feature`/`enhancement` cards not matching DEEP, plus any `bugfix`/`refactor` with >3 files or breadth that isn't DEEP |
220
220
 
221
221
  **Critical**: `feature` and `enhancement` cards are `balanced` minimum — **with exactly one
222
222
  exception**: the LIGHT branch (b) above, a strictly UI-presentational card (`areas == [ui]`,
223
- ≤3 component/style files, composes existing primitives without touching any
223
+ ≤3 component/style files locale files under `i18n.locales_root` excluded from the count when
224
+ `features.has_i18n: true`, see the LIGHT row — composes existing primitives without touching any
224
225
  `paths.components_primitives` path). This narrow carve-out targets the `-ui` half of a
225
226
  logic/ui split pair (e.g. a `…-submit-ui` card whose logic lives in its `…-submit-logic`
226
227
  sibling): reviewing a pixels-only diff at Codex-`full` depth is waste, and the design-system
@@ -95,8 +95,15 @@ precedence caveats: `framework/agents/effort-protocol.md`.
95
95
  id list from `${paths.design_tokens}` (the DTCG `.tokens.json`) and pick exact
96
96
  ids (`typography.fontWeight.medium`, `zIndex.overlay`, `space.sm`), never
97
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.
98
+ Prefer the ids the component's **source actually uses** the serializer's
99
+ `--tokens` guard (step 6) drops unknown ids and `--verify-usage` flags ids not
100
+ used in the source as advisory drift; picking from real usage avoids both.
101
+ **Intent source order:** if the component traces to a PRD (a card's
102
+ `links.prd` / `links.design`), read that PRD's **UI Element Inventory** row
103
+ (`Purpose`/`function_ref`/`Behavior`/`States`) as the PRIMARY source for
104
+ `purpose`/`a11y`/`must_rules` (design-time intent the code does not encode),
105
+ then reconcile against the implemented source. Fallback to source-only when no
106
+ PRD/Inventory exists.
100
107
  5. **Bootstrap the DTCG token SSOT.** See § Token SSOT bootstrap below.
101
108
  6. **Write the files — DETERMINISTICALLY (the HEAD is NEVER model-authored).**
102
109
  Merge the deterministic JSON (step 3) with the agentic JSON (step 4) per
@@ -105,7 +112,7 @@ precedence caveats: `framework/agents/effort-protocol.md`.
105
112
  node .framework/framework/.claude/skills/design-system-init/scripts/serialize-spec.mjs \
106
113
  --bundle <bundle.json> --out-dir ${paths.design_system}/components \
107
114
  --index ${paths.design_system}/INDEX.md --brand "${identity.brand_name}" \
108
- --tokens ${paths.design_tokens}
115
+ --tokens ${paths.design_tokens} --verify-usage
109
116
  ```
110
117
  It emits every `components/<Name>.md` (valid-YAML HEAD via deterministic
111
118
  serialization + preserved prose) **and** the thin INDEX router, then
@@ -115,11 +122,15 @@ precedence caveats: `framework/agents/effort-protocol.md`.
115
122
  `purpose`/`a11y`): the model supplies field *data*, the script owns the YAML.
116
123
  Do NOT write the HEAD by hand or via the prose template.
117
124
  `--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.
125
+ vocabulary guard** (existence): any `token_bindings` id that does not resolve
126
+ in the DTCG `.tokens.json` is DROPPED and REPORTED (`droppedBindings`) so a
127
+ `DS_TOKENS_DRIFT` can never be written. `--verify-usage` activates the
128
+ **correctness check**: ids that resolve but are NOT used in the component's
129
+ styling surface (own source + composed children) are REPORTED (not dropped) as
130
+ `unverifiedBindings` — an advisory review list (the binding is over-attributed
131
+ or wrong, OR legitimately indirect via Tailwind/non-sibling CSS). Review both
132
+ lists per step 4: a drop = a guessed-wrong name; an unverified id = pick the id
133
+ the source actually uses, or justify it.
123
134
  - `tokens-reference.md` — **generated** human view of `.tokens.json` (or pure
124
135
  narrative). Not a third source of values.
125
136
  - `patterns/.gitkeep` — empty dir for future pattern docs.
@@ -169,6 +169,51 @@ export function validateBindings(components, idSet) {
169
169
  return dropped;
170
170
  }
171
171
 
172
+ /**
173
+ * Correctness check (since v4.67.0): a `token_bindings` id should actually be
174
+ * USED in the component's OWN source. The v4.66.2 guard only checks the id
175
+ * EXISTS in the DTCG — a plausible-but-wrong binding (exists globally, not used
176
+ * here) survives it. This reads each component's real source (`source` field +
177
+ * sibling `.module.css`/`.css`) and marks a binding "found" if its CSS-var form
178
+ * (`--<dot-as-dash>`) or the dotted id appears.
179
+ *
180
+ * ADVISORY, never a drop/block: a binding can be legitimately indirect
181
+ * (Tailwind-class-based with no `--var`, or in a non-sibling stylesheet) — so
182
+ * unverified ids are REPORTED for review (`DS_TOKENS_DRIFT` advisory), not
183
+ * removed. The component's effective styling surface = its OWN source + the
184
+ * sources of the primitives it directly `composes` (a wrapper that presents an
185
+ * overlay via a composed Popover legitimately binds `zIndex.overlay`); only a
186
+ * binding absent from that whole surface is a genuine phantom. Resolve from the
187
+ * `source` field (NEVER a guessed `components/<Name>.tsx` path — that
188
+ * mis-resolves nested components). Returns [{component, id}] for the NOT-found.
189
+ */
190
+ export function verifyBindingUsage(components, cwd) {
191
+ const byName = new Map(components.map((c) => [c.name, c]));
192
+ const readSurface = (rel) => {
193
+ let s = '';
194
+ for (const p of [rel, rel.replace(/\.tsx?$/, '.module.css'), rel.replace(/\.tsx?$/, '.css')]) {
195
+ try { s += fs.readFileSync(path.join(cwd, p), 'utf8'); } catch (_) { /* may not exist */ }
196
+ }
197
+ return s;
198
+ };
199
+ const unverified = [];
200
+ for (const c of components) {
201
+ const b = c.token_bindings || [];
202
+ if (!b.length || !c.source) continue;
203
+ let src = readSurface(c.source);
204
+ for (const childName of c.composes || []) {
205
+ const child = byName.get(childName);
206
+ if (child && child.source) src += readSurface(child.source);
207
+ }
208
+ if (!src) continue; // cannot read any source → cannot judge; stay silent (no false flag)
209
+ for (const id of b) {
210
+ const cssVar = `--${id.replace(/\./g, '-')}`;
211
+ if (!src.includes(cssVar) && !src.includes(id)) unverified.push({ component: c.name, id });
212
+ }
213
+ }
214
+ return unverified;
215
+ }
216
+
172
217
  // CLI
173
218
  if (import.meta.url === `file://${process.argv[1]}`) {
174
219
  const bundlePath = arg('--bundle');
@@ -176,8 +221,10 @@ if (import.meta.url === `file://${process.argv[1]}`) {
176
221
  const indexPath = arg('--index');
177
222
  const brandArg = arg('--brand');
178
223
  const tokensPath = arg('--tokens'); // optional DTCG .tokens.json → binding vocab guard
224
+ const verifyUsage = process.argv.includes('--verify-usage'); // advisory source-usage check
225
+ const cwd = arg('--cwd', process.cwd());
179
226
  if (!bundlePath || !outDir) {
180
- console.error('usage: serialize-spec.mjs --bundle bundle.json --out-dir <dir> [--index <path>] [--brand <name>] [--tokens <.tokens.json>]');
227
+ console.error('usage: serialize-spec.mjs --bundle bundle.json --out-dir <dir> [--index <path>] [--brand <name>] [--tokens <.tokens.json>] [--verify-usage] [--cwd <dir>]');
181
228
  process.exit(2);
182
229
  }
183
230
  const bundle = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
@@ -200,6 +247,18 @@ if (import.meta.url === `file://${process.argv[1]}`) {
200
247
  }
201
248
  }
202
249
 
250
+ // Binding correctness check (since v4.67.0): ADVISORY — report bindings not
251
+ // used in the component's own source (never drop; can be legitimately indirect).
252
+ let unverified = [];
253
+ if (verifyUsage) {
254
+ unverified = verifyBindingUsage(components, cwd);
255
+ if (unverified.length) {
256
+ console.error(`token_bindings: ${unverified.length} id(s) not found in their component source (DS_TOKENS_DRIFT advisory — review: fix or justify as indirect):`);
257
+ for (const u of unverified.slice(0, 20)) console.error(` - ${u.component}: ${u.id}`);
258
+ if (unverified.length > 20) console.error(` … +${unverified.length - 20} more`);
259
+ }
260
+ }
261
+
203
262
  fs.mkdirSync(outDir, { recursive: true });
204
263
  let invalid = 0;
205
264
  for (const c of components) {
@@ -210,7 +269,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
210
269
  if (v.checked && !v.ok) { invalid++; console.error(`INVALID HEAD ${c.name}.md: ${v.error}`); }
211
270
  }
212
271
  if (indexPath) fs.writeFileSync(indexPath, emitIndex(components, brand), 'utf8');
213
- const report = { schema: 'baldart.serialize-spec/1', written: components.length, index: !!indexPath, invalid, droppedBindings: dropped.length };
272
+ const report = { schema: 'baldart.serialize-spec/1', written: components.length, index: !!indexPath, invalid, droppedBindings: dropped.length, unverifiedBindings: unverified.length };
214
273
  console.log(JSON.stringify(report));
215
274
  if (invalid > 0) process.exit(1); // fail-closed: never leave invalid HEADs
216
275
  }
@@ -115,15 +115,28 @@ 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
118
+ **Token-binding vocabulary invariant (existence — HARD):** every `token_bindings`
119
+ id MUST resolve to a real node in the DTCG `.tokens.json` (`paths.design_tokens`).
120
+ The agentic enricher picks ids from that file's actual vocabulary — never invents
121
121
  plausible-but-wrong names (`font.weight.medium`/`z.overlay`/`space.2` instead of
122
122
  `typography.fontWeight.medium`/`zIndex.overlay`/`space.sm`). The serializer's
123
123
  `--tokens` guard DROPS + REPORTS any unresolved id (so a `DS_TOKENS_DRIFT` is
124
124
  never written); a dropped id means the enricher guessed wrong (fix it or add the
125
125
  token).
126
126
 
127
+ **Token-binding correctness invariant (usage — ADVISORY):** existence is not
128
+ correctness — a token can exist globally yet be wrong for this component. A
129
+ `token_bindings` id SHOULD actually be USED in the component's effective styling
130
+ surface: its own source (`source` + sibling `.module.css`/`.css`) or the source
131
+ of a primitive it directly `composes` (a wrapper presenting an overlay via a
132
+ composed Popover legitimately binds `zIndex.overlay`). The serializer's
133
+ `--verify-usage` check REPORTS — never drops — any id absent from that surface as
134
+ a `DS_TOKENS_DRIFT` **advisory** (review-required), because a binding can be
135
+ legitimately indirect (Tailwind classes, a non-sibling stylesheet). The reviewer
136
+ either corrects the binding or justifies it; it is NOT a hard merge-block (a hard
137
+ block would false-fire). Pick bindings the source actually uses to keep this
138
+ clean.
139
+
127
140
  ## Regeneration contract
128
141
 
129
142
  - **Anchor**: `source_sha`. A regeneration run skips a component whose source is
@@ -482,8 +482,13 @@ For each item in the diff that touched a visual surface, run these 4 checks:
482
482
  set, the change MUST be made in `.tokens.json` and the stack-native output
483
483
  regenerated via `baldart tokens build` (a hand-edit to the generated output is
484
484
  the drift). Otherwise update the project's token source + reference. Either
485
- way, a dangling `token_bindings` ID is drift. Stale = `DS_TOKENS_DRIFT`,
486
- task blocked.
485
+ way, a dangling `token_bindings` ID (not in the DTCG) is drift. Stale =
486
+ `DS_TOKENS_DRIFT`, task blocked. **Binding correctness (advisory):** a
487
+ `token_bindings` id that exists in the DTCG but is NOT used in the component's
488
+ effective styling surface (own source + directly-composed children) is a
489
+ `DS_TOKENS_DRIFT` **advisory** finding (review-required, NOT a block) — the
490
+ `serialize-spec.mjs --verify-usage` check reports these; the reviewer corrects
491
+ the binding or justifies it as legitimately indirect.
487
492
  4. **Surfaced silent primitive ⇒ added to INDEX**: during the work, did you
488
493
  reuse a primitive that exists in the source tree but is **not** in
489
494
  `INDEX.md`? If yes, add it to the INDEX in the same change (or open a
@@ -34,7 +34,11 @@ prompt: |
34
34
  verify the generated outputs (`design_tokens.outputs`) + `tokens-reference.md`
35
35
  equal `baldart tokens build` — a hand-edited generated file or a dangling
36
36
  `token_bindings` ID is `DS_TOKENS_DRIFT`. When unset, fall back to verifying
37
- the project token source vs `tokens-reference.md`.
37
+ the project token source vs `tokens-reference.md`. Also run the binding
38
+ CORRECTNESS check (`serialize-spec.mjs --verify-usage`): a `token_bindings`
39
+ id not used in its component's styling surface is a `DS_TOKENS_DRIFT`
40
+ **advisory** — list it for review (fix the binding or justify as indirect),
41
+ do not auto-remove.
38
42
  5. Emit a consolidated report under
39
43
  `docs/reports/{{YYYYMMDD}}-ds-drift.md`.
40
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.66.2",
3
+ "version": "4.67.1",
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"