@radicool/throughline 0.19.0 → 0.20.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/README.md +2 -2
- package/adapters/codex/prompts/component-builder.md +24 -1
- package/adapters/codex/prompts/document-component.md +10 -1
- package/adapters/codex/prompts/icon-system-builder.md +4 -0
- package/adapters/codex/prompts/storybook-chromatic-builder.md +114 -7
- package/adapters/codex/prompts/token-builder.md +110 -3
- package/adapters/codex/prompts/token-crosswalk-builder.md +2 -0
- package/adapters/codex/prompts/token-sheet-builder.md +4 -0
- package/adapters/codex/prompts/token-sync-layer.md +140 -20
- package/adapters/cursor/.cursor/commands/document-component.md +10 -1
- package/adapters/cursor/.cursor/rules/component-builder.mdc +24 -1
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +4 -0
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +114 -7
- package/adapters/cursor/.cursor/rules/token-builder.mdc +110 -3
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +2 -0
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +4 -0
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +140 -20
- package/adapters/generic/commands/document-component.md +10 -1
- package/adapters/generic/skills/component-builder/SKILL.md +24 -1
- package/adapters/generic/skills/icon-system-builder/SKILL.md +4 -0
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +114 -7
- package/adapters/generic/skills/token-builder/SKILL.md +110 -3
- package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +2 -0
- package/adapters/generic/skills/token-sheet-builder/SKILL.md +4 -0
- package/adapters/generic/skills/token-sync-layer/SKILL.md +140 -20
- package/package.json +1 -1
- package/references/component-doc-schema.md +11 -1
- package/references/figma-component-standards.md +14 -2
- package/references/figma-scripting.md +12 -2
- package/references/manifest-schema.md +42 -2
- package/references/proof-bundle.md +305 -0
- package/references/sync-adapters.md +38 -53
- package/scripts/README.md +140 -6
- package/scripts/build-doc-card-builder.mjs +1 -1
- package/scripts/grep-color-usage.mjs +10 -30
- package/scripts/guard-token-removal.mjs +10 -26
- package/scripts/lib/component-states.mjs +56 -0
- package/scripts/lib/contrast.mjs +117 -0
- package/scripts/lib/dtcg.mjs +3 -2
- package/scripts/lib/proof.mjs +186 -0
- package/scripts/lib/source-scan.mjs +45 -0
- package/scripts/validate-adherence.mjs +895 -0
- package/scripts/validate-token-output.mjs +468 -63
- package/scripts/verify-check.mjs +710 -0
|
@@ -372,8 +372,20 @@ re-describing it:
|
|
|
372
372
|
`${CLAUDE_PLUGIN_ROOT}/references/figma-scripting.md` — `getNodeByIdAsync`, and
|
|
373
373
|
an explicit `timeout` since multi-card font-loading writes blow past the default
|
|
374
374
|
~5s budget), then inside it:
|
|
375
|
-
-
|
|
376
|
-
|
|
375
|
+
- locate the status chip under either header shape, the way the doc-card
|
|
376
|
+
builder's header guard does: find the header band structurally (the card's
|
|
377
|
+
child frame that is neither `Usage` nor the specimen nor an ancestor of it),
|
|
378
|
+
then search its first child, the title row, for a descendant named `Status`
|
|
379
|
+
(to-spec cards) or `Status Pill` (legacy cards). The chip's label is its
|
|
380
|
+
`Status Label` text node; a legacy pill has no named label, so use its one
|
|
381
|
+
TEXT descendant. If no chip is found, or the label doesn't resolve to exactly
|
|
382
|
+
one text node, **skip that card** without writing anything to it and record
|
|
383
|
+
its name — catch per card, so one miss never aborts the rest of a multi-card
|
|
384
|
+
write. After the loop, tell the user by name every card that still shows the
|
|
385
|
+
old status in Figma — the manifest already says `stable`, so a silent skip
|
|
386
|
+
leaves the card lying;
|
|
387
|
+
- set the label text to the new status (e.g. `stable`);
|
|
388
|
+
- re-bind the chip fill to the matching semantic color variable
|
|
377
389
|
(`stable` → success, `draft`/`beta` → warning, `deprecated` → neutral/danger)
|
|
378
390
|
— re-bind the variable, don't hardcode a hex, so it stays mode-aware.
|
|
379
391
|
Then re-run the canonical doc-card builder
|
|
@@ -51,8 +51,9 @@ wrong document.
|
|
|
51
51
|
|
|
52
52
|
**So the preflight must assert the active file's *identity*, not just connection:**
|
|
53
53
|
|
|
54
|
-
1. **Know your target `fileKey`** before writing
|
|
55
|
-
|
|
54
|
+
1. **Know your target `fileKey`** before writing. It is `figma.fileKey` in
|
|
55
|
+
`design-system.json` at the working-directory root. If that field is absent,
|
|
56
|
+
stop and ask which file to write to — do not assume the active one is right.
|
|
56
57
|
2. **Read the active file and compare.** `figma_get_status` reports
|
|
57
58
|
`currentFileKey` / `currentFileName`; `figma_list_open_files` shows every
|
|
58
59
|
connected file and which is active. Confirm the active `fileKey` equals your
|
|
@@ -61,6 +62,15 @@ wrong document.
|
|
|
61
62
|
3. **If it drifted, navigate — don't just retry.** `figma_navigate` to the target
|
|
62
63
|
file URL, then re-read status to confirm the switch took, then write. A bare
|
|
63
64
|
re-run without navigating will hit the wrong file again.
|
|
65
|
+
4. **If it still doesn't match, stop.** Name both files. The usual cause is that
|
|
66
|
+
the target file does not have the Desktop Bridge plugin open.
|
|
67
|
+
|
|
68
|
+
Run this before the first write, and again right before any replace — the
|
|
69
|
+
build-verify-then-replace finalize deletes the existing node, so it is the write
|
|
70
|
+
that destroys work in the wrong file. The `figma-executor` agent runs it as step 1
|
|
71
|
+
of its contract. **When a skill builds inline instead** (a host with no subagent
|
|
72
|
+
dispatch, or a write-back the skill does itself), nothing runs it for you: the
|
|
73
|
+
skill has to.
|
|
64
74
|
|
|
65
75
|
This is the file-level analogue of the live-instance discipline above: the
|
|
66
76
|
instance preflight guards against *two* live bridges; this guards against the *one*
|
|
@@ -11,11 +11,11 @@ what changed. Gating decisions are made by reading this file: if a prerequisite
|
|
|
11
11
|
field is unset, the skill **offers** to run the prerequisite skill rather than
|
|
12
12
|
bailing or running silently.
|
|
13
13
|
|
|
14
|
-
## Schema (schemaVersion
|
|
14
|
+
## Schema (schemaVersion 7)
|
|
15
15
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
|
-
"schemaVersion":
|
|
18
|
+
"schemaVersion": 7,
|
|
19
19
|
"user": {
|
|
20
20
|
"codingLevel": "new"
|
|
21
21
|
},
|
|
@@ -102,6 +102,7 @@ bailing or running silently.
|
|
|
102
102
|
"completedAt": null,
|
|
103
103
|
"journalScaffolded": false
|
|
104
104
|
},
|
|
105
|
+
"verification": null,
|
|
105
106
|
"completedSkills": []
|
|
106
107
|
}
|
|
107
108
|
```
|
|
@@ -346,6 +347,41 @@ retrofit stands so a later session can resume.
|
|
|
346
347
|
- `journalScaffolded` — whether the `docs/design-system/` decision journal has been
|
|
347
348
|
created for this retrofit (offered default-on by `retrofit-planner`).
|
|
348
349
|
|
|
350
|
+
### `verification`
|
|
351
|
+
The pointer at the folder-resident proof store, where each stage records what it
|
|
352
|
+
changed, which checks ran and why it was allowed to advance. `null` until the
|
|
353
|
+
first stage records an entry. See
|
|
354
|
+
`${CLAUDE_PLUGIN_ROOT}/references/proof-bundle.md`.
|
|
355
|
+
- `path` — the store directory (`design-system/proof`).
|
|
356
|
+
- `stages` — object keyed by stage name, each holding
|
|
357
|
+
`{ at, adoptedAt, exempt?, fingerprint }`.
|
|
358
|
+
- `fingerprint` — computed over the **whole** stage file, so a hand-edited entry
|
|
359
|
+
is caught (`proof-stale`), the same way `docs:check` catches a hand-edited
|
|
360
|
+
surface.
|
|
361
|
+
- `at` — the latest recorded entry's timestamp. It moves with every record.
|
|
362
|
+
- `adoptedAt` — the first entry's timestamp: **write-once**, never rewritten, the
|
|
363
|
+
human-readable "when did this system opt in". No gate reads it.
|
|
364
|
+
- `exempt` — present only on the per-component stages (`component-builder`,
|
|
365
|
+
`storybook-chromatic-builder`) and **capture-once-then-shrink**: the stage's
|
|
366
|
+
first record captures every name then in `components.built`, and every record,
|
|
367
|
+
the first included, removes its own subject. The list never gains a name. It is
|
|
368
|
+
what scopes the per-component `proof-missing` rule to components the stage has
|
|
369
|
+
not yet proven, so adopting the bundle does not light up a failure for every
|
|
370
|
+
component built before it existed. A listed stage with **no** `exempt` key
|
|
371
|
+
exempts **nothing**, not everything — a missing list makes the gate louder, not
|
|
372
|
+
quieter.
|
|
373
|
+
|
|
374
|
+
**Pointers and hashes, never entries** — the entries themselves live in the
|
|
375
|
+
store, for the same reason `components.meta[name].doc` keeps content out of the
|
|
376
|
+
manifest. Skills write all of this only through `verify-check.mjs --record`; none
|
|
377
|
+
hand-writes a stage file or computes a fingerprint by hand.
|
|
378
|
+
|
|
379
|
+
No gate compares `components.meta[name].updatedAt` against `adoptedAt`. That
|
|
380
|
+
field is refreshed by a status promotion as well as by a rebuild, so a stage
|
|
381
|
+
other than the one that owes the entry can move it — and a date comparison would
|
|
382
|
+
fail a pre-existing component the first time an unrelated storying run promotes
|
|
383
|
+
it.
|
|
384
|
+
|
|
349
385
|
### `completedSkills`
|
|
350
386
|
- Append-only list of skill identifiers that have run to completion at least
|
|
351
387
|
once. Useful for the `/design-system-status` command and for friendly
|
|
@@ -379,3 +415,7 @@ are authored. Bump `schemaVersion` to `5`. No existing field changes.
|
|
|
379
415
|
**v5 → v6 migration:** add `figma.docCardVariables` (default `null`), populated
|
|
380
416
|
on the first doc-card render. Bump `schemaVersion` to `6`. No existing field
|
|
381
417
|
changes.
|
|
418
|
+
|
|
419
|
+
**v6 → v7 migration:** add `verification` (default `null`), populated on the
|
|
420
|
+
first recorded stage entry. Bump `schemaVersion` to `7`. No existing field
|
|
421
|
+
changes.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# The verification proof bundle (`design-system/proof/`)
|
|
2
|
+
|
|
3
|
+
Each stage of the system leaves a durable record of what it changed, which checks
|
|
4
|
+
ran, what each one found, and why it was allowed to advance. That record is the
|
|
5
|
+
proof bundle. It exists because "run it twice, get the same structure" proves
|
|
6
|
+
idempotency, not quality — and because the checks that do prove quality (the
|
|
7
|
+
Figma read-back, a reviewer verdict, a build result) are prose today, and prose
|
|
8
|
+
evaporates when the run ends.
|
|
9
|
+
|
|
10
|
+
The bundle is **the record, not the enforcement mechanism**. Gates enforce:
|
|
11
|
+
`docs:check` owns documentation drift, the `figma-executor` read-back owns
|
|
12
|
+
structure, `adherence:check` owns code adherence. The bundle persists what each
|
|
13
|
+
gate found. The `verify:check` gate reads it, recomputes what it can off disk,
|
|
14
|
+
and fails when a stored result and a fresh run disagree.
|
|
15
|
+
|
|
16
|
+
## Where it lives
|
|
17
|
+
|
|
18
|
+
`design-system/proof/<stage>.json` — one file per stage, in the user's working
|
|
19
|
+
folder, from the first component built. It is folder-resident exactly like
|
|
20
|
+
`design-system/docs/` (see
|
|
21
|
+
`${CLAUDE_PLUGIN_ROOT}/references/component-doc-schema.md`), so a stage can
|
|
22
|
+
record an entry during the Figma-only *folder* stage, before any repo exists.
|
|
23
|
+
The path is stable across folder→repo; files never move.
|
|
24
|
+
|
|
25
|
+
## The file shape
|
|
26
|
+
|
|
27
|
+
`{ "stage": "<name>", "subjects": { "<subject>": <entry> } }`
|
|
28
|
+
|
|
29
|
+
A **subject** is the thing the entry is about: a component name for a
|
|
30
|
+
per-component stage, or the literal `"system"` for a stage that is not
|
|
31
|
+
per-component. One entry per stage-and-subject, latest wins — the store models
|
|
32
|
+
current state, not history, the same way the manifest does.
|
|
33
|
+
|
|
34
|
+
## The entry shape
|
|
35
|
+
|
|
36
|
+
`{ at, changed, checks, screenshot?, advancedBecause }`
|
|
37
|
+
|
|
38
|
+
- `at` — ISO timestamp, **supplied by the caller**. `Date.now()` is not available
|
|
39
|
+
in every execution context a skill runs in, so the recorder never invents it.
|
|
40
|
+
- `changed` — array of one-line strings saying what this stage changed. May be
|
|
41
|
+
empty.
|
|
42
|
+
- `checks` — non-empty array of `{ name, method, result, evidence }`, where
|
|
43
|
+
`method` is `"derived"` or `"attested"`, `result` is `"pass"` or `"fail"`, and
|
|
44
|
+
`evidence` is a string.
|
|
45
|
+
- `checks[].accepted` — optional, and only on a check. An array of
|
|
46
|
+
`{ fg, bg, mode }` naming pairs a user explicitly accepted as failing. Only
|
|
47
|
+
`contrast-baseline` uses it today.
|
|
48
|
+
- `screenshot` — optional pointer: a Figma node id, or a repo-relative path.
|
|
49
|
+
- `advancedBecause` — one line saying why the stage was allowed to advance.
|
|
50
|
+
|
|
51
|
+
**`accepted`, and why its two readers disagree.** `fg` and `bg` are the dotted
|
|
52
|
+
`CONTRAST_PAIRS` paths, compared after the `normalizeText` fold, so
|
|
53
|
+
`color.text.onEmphasis` and `color.text.on-emphasis` are one role. `mode` is the
|
|
54
|
+
**Figma** mode name `token-builder` saw — `Light`, `Dark` — because at folder
|
|
55
|
+
stage no DTCG file exists yet to name a mode after. A reader that matches on
|
|
56
|
+
`mode` must translate it into that run's file label first.
|
|
57
|
+
|
|
58
|
+
Two skills read the field, and they match on different things:
|
|
59
|
+
|
|
60
|
+
- `token-sync-layer` drops from its own gate run's `color-contrast` failures
|
|
61
|
+
every one whose `(fg, bg, mode)` triple matches an entry. It is the only reader
|
|
62
|
+
that matches on the mode, because it is the only one that knows both names: it
|
|
63
|
+
read the Figma modes and it wrote the DTCG files.
|
|
64
|
+
- `storybook-chromatic-builder` compares its own live run's failures against the
|
|
65
|
+
`fg`/`bg` **roles alone**, to decide whether the `verify:check` it registers
|
|
66
|
+
carries `--skip color-contrast`. A per-mode test would decide nothing there —
|
|
67
|
+
`--skip` is rule-wide, so there is no way to skip contrast in Dark and keep it
|
|
68
|
+
in Light — and that skill could not translate a mode name anyway, having
|
|
69
|
+
neither read the Figma modes nor written the DTCG files.
|
|
70
|
+
|
|
71
|
+
The rule that keeps the field honest: **it is data a caller subtracts, never
|
|
72
|
+
something `verify-check.mjs` reads.** The gate does not know the field exists, and
|
|
73
|
+
an attested `fail` never switches a derived rule off inside it — that would be
|
|
74
|
+
the self-attestation this store exists to avoid. A triple that no longer matches
|
|
75
|
+
any failure is inert, which is how an acceptance retires itself once the pair is
|
|
76
|
+
fixed.
|
|
77
|
+
|
|
78
|
+
**Pointers, never content.** `evidence` and `screenshot` point at things; they do
|
|
79
|
+
not carry them. This is the same rule `components.meta[name].doc` follows, for
|
|
80
|
+
the same reason: the store is committed to the repo and read by humans. A diff
|
|
81
|
+
of a stage file should be legible, and a design system's history should not carry
|
|
82
|
+
inlined screenshots or file contents.
|
|
83
|
+
|
|
84
|
+
## `derived` vs. `attested`
|
|
85
|
+
|
|
86
|
+
This is the load-bearing distinction in the whole store, and the two are not
|
|
87
|
+
equal.
|
|
88
|
+
|
|
89
|
+
- **`derived`** — `verify:check` recomputes the check off disk on every run. The
|
|
90
|
+
stored result is a **cache**. When the rerun disagrees with it, the run fails as
|
|
91
|
+
`proof-contradicted`. A derived result is trustworthy because it is not
|
|
92
|
+
trusted.
|
|
93
|
+
- **`attested`** — an agent observed it live, somewhere the CLI cannot look:
|
|
94
|
+
Figma, a build it ran, a review it received. It is printed on the report's
|
|
95
|
+
informational line and is **never the reason a run passes**.
|
|
96
|
+
|
|
97
|
+
The house precedent for the honest half is `edit-unverified` in
|
|
98
|
+
`${CLAUDE_PLUGIN_ROOT}/references/component-doc-schema.md` — `docs:check` records
|
|
99
|
+
what it cannot read without pretending it verified it. Attested checks are the
|
|
100
|
+
same posture: recorded, visible, never load-bearing.
|
|
101
|
+
|
|
102
|
+
## The manifest pointer
|
|
103
|
+
|
|
104
|
+
`design-system.json` carries a top-level `verification` key — `null` until the
|
|
105
|
+
first stage records an entry. Content on disk, pointers and hashes in the
|
|
106
|
+
manifest, exactly as `components.meta[name].doc` does it. See
|
|
107
|
+
`${CLAUDE_PLUGIN_ROOT}/references/manifest-schema.md`.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"verification": {
|
|
112
|
+
"path": "design-system/proof",
|
|
113
|
+
"stages": {
|
|
114
|
+
"component-builder": {
|
|
115
|
+
"at": "2026-09-12T18:04:00.000Z",
|
|
116
|
+
"adoptedAt": "2026-09-12T17:41:00.000Z",
|
|
117
|
+
"exempt": ["Card"],
|
|
118
|
+
"fingerprint": "3f9c1a7b2d4e6058"
|
|
119
|
+
},
|
|
120
|
+
"token-sync-layer": {
|
|
121
|
+
"at": "2026-09-12T18:20:00.000Z",
|
|
122
|
+
"adoptedAt": "2026-09-12T18:20:00.000Z",
|
|
123
|
+
"fingerprint": "8ab240df1c9e5567"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `path` — the store directory.
|
|
131
|
+
- `fingerprint` — computed over the **whole stage file**, so a hand-edited entry
|
|
132
|
+
is caught (`proof-stale`), the same way `docs:check` catches a hand-edited
|
|
133
|
+
surface.
|
|
134
|
+
- `at` — the timestamp of the latest entry recorded for that stage. It moves with
|
|
135
|
+
every record.
|
|
136
|
+
- `adoptedAt` — **write-once**. Set on a stage's first recorded entry and never
|
|
137
|
+
rewritten: the human-readable "when did this system opt in". No gate reads it.
|
|
138
|
+
- `exempt` — the names this stage has not yet proven. Present only on the
|
|
139
|
+
per-component stages; a system-wide stage omits the key entirely.
|
|
140
|
+
|
|
141
|
+
**Skills write all of this only through `verify-check.mjs --record`.** No skill
|
|
142
|
+
hand-writes a stage file, and none computes a fingerprint by hand — that is how
|
|
143
|
+
the pointer and the file are kept from drifting apart.
|
|
144
|
+
|
|
145
|
+
### `exempt` is capture-once-then-shrink
|
|
146
|
+
|
|
147
|
+
Both halves of the rule are load-bearing, and a reader who implements only the
|
|
148
|
+
first grandfathers the whole adoption batch:
|
|
149
|
+
|
|
150
|
+
1. On a stage's **first** record, `exempt` captures **every** name then in
|
|
151
|
+
`components.built`, sorted.
|
|
152
|
+
2. On **every** record, the first included, the record's own subject is removed
|
|
153
|
+
from `exempt`.
|
|
154
|
+
|
|
155
|
+
The list never gains a name. It can only narrow. It is what scopes the
|
|
156
|
+
per-component `proof-missing` rule to components the stage has not yet proven:
|
|
157
|
+
an existing system that adopts the bundle does not light up with a failure for
|
|
158
|
+
every component built before the bundle existed, and a name leaves the list the
|
|
159
|
+
moment the stage actually proves that component.
|
|
160
|
+
|
|
161
|
+
Capture has to read the whole of `components.built` rather than "everything but
|
|
162
|
+
this subject", because a run's entire batch is already in `built` before the
|
|
163
|
+
record loop reaches the first component. A capture that excluded only its own
|
|
164
|
+
subject would permanently exempt its batch-mates — build four components on the
|
|
165
|
+
upgrade run and three of them are never owed proof again.
|
|
166
|
+
|
|
167
|
+
A stage listed in `verification.stages` with **no** `exempt` key at all exempts
|
|
168
|
+
**nothing**, not everything. A missing list makes the gate louder, not quieter.
|
|
169
|
+
|
|
170
|
+
### No gate compares `updatedAt` against `adoptedAt`
|
|
171
|
+
|
|
172
|
+
However natural it looks, nothing in the verification gate compares
|
|
173
|
+
`components.meta[name].updatedAt` against `adoptedAt`. That field has more than
|
|
174
|
+
one writer: `storybook-chromatic-builder` refreshes it when it promotes a
|
|
175
|
+
component to `"stable"`, which is a different stage from the one that owes the
|
|
176
|
+
entry. So a single storying run drags every pre-existing component's timestamp
|
|
177
|
+
past `component-builder`'s adoption moment, and a date gate would fail a system
|
|
178
|
+
where nothing went wrong. A captured list of names cannot be moved by another
|
|
179
|
+
stage's write, and it is readable in the manifest: the exemption is stated
|
|
180
|
+
rather than inferred.
|
|
181
|
+
|
|
182
|
+
For the same reason, neither `adoptedAt` nor `exempt` is derived from the stage
|
|
183
|
+
file. Entries are latest-wins, so "the earliest `at` in the file" slides forward
|
|
184
|
+
every time the earliest subject is rebuilt.
|
|
185
|
+
|
|
186
|
+
## The stage vocabulary (v1)
|
|
187
|
+
|
|
188
|
+
| Stage | Subject |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `component-builder` | component name |
|
|
191
|
+
| `storybook-chromatic-builder` | component name |
|
|
192
|
+
| `token-sync-layer` | `"system"` |
|
|
193
|
+
| `token-builder` | `"system"` |
|
|
194
|
+
|
|
195
|
+
The machine-readable copy of that split is `PER_COMPONENT_STAGES` in
|
|
196
|
+
`scripts/lib/proof.mjs`, and the full list is `STAGES` beside it. **A change here
|
|
197
|
+
must change those constants** — they are what `verify:check` asks "is this stage
|
|
198
|
+
keyed by component?"
|
|
199
|
+
|
|
200
|
+
`/document-component` and the retrofit phases do not emit entries in v1.
|
|
201
|
+
|
|
202
|
+
## The check names (v1)
|
|
203
|
+
|
|
204
|
+
| Check | Method | Recorded by |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `structural-read-back` | attested | `component-builder` |
|
|
207
|
+
| `state-baseline` | attested | `component-builder` |
|
|
208
|
+
| `figma-name` | attested | `component-builder` |
|
|
209
|
+
| `review` | attested | the dispatching skill |
|
|
210
|
+
| `build` | attested | `storybook-chromatic-builder` |
|
|
211
|
+
| `orphan-token` | derived | not recorded: re-derived on every `verify:check` run |
|
|
212
|
+
| `state-incomplete` | derived | `storybook-chromatic-builder` |
|
|
213
|
+
| `name-drift` | derived | `storybook-chromatic-builder` |
|
|
214
|
+
| `contrast-baseline` | attested | `token-builder` |
|
|
215
|
+
| `tokens:validate-output` | attested | `token-sync-layer` |
|
|
216
|
+
| `color-contrast` | derived | `token-sync-layer` |
|
|
217
|
+
|
|
218
|
+
**Two stages record derived results — `storybook-chromatic-builder` and
|
|
219
|
+
`token-sync-layer`** — and each qualifies for one reason and only that reason: it
|
|
220
|
+
runs `verify:check` before it records, so it has the result in hand rather than
|
|
221
|
+
asserting it. That is a property of what the stage does, not of which stage it
|
|
222
|
+
is; a stage that does not run the gate never qualifies. `component-builder` and
|
|
223
|
+
`token-builder` both run at folder stage, with no repo to scan, so they record
|
|
224
|
+
attested checks only.
|
|
225
|
+
|
|
226
|
+
A derived result sitting in a stage file is a cache the next run recomputes. A
|
|
227
|
+
disagreement is `proof-contradicted`.
|
|
228
|
+
|
|
229
|
+
### Contrast: two checks, two methods
|
|
230
|
+
|
|
231
|
+
`contrast-baseline` and `color-contrast` ask the same question in two places,
|
|
232
|
+
and neither substitutes for the other.
|
|
233
|
+
|
|
234
|
+
- **`contrast-baseline`** — attested, recorded by `token-builder`. The ratio an
|
|
235
|
+
agent computed in Figma from the resolved variable values, per mode, at the
|
|
236
|
+
moment the semantic tier was built. Attested because the CLI cannot read Figma.
|
|
237
|
+
- **`color-contrast`** — derived, recorded by `token-sync-layer`. The same
|
|
238
|
+
question recomputed off the DTCG source by `verify:check`, which can read it.
|
|
239
|
+
|
|
240
|
+
This is the `state-baseline` / `state-incomplete` pairing one layer down: the
|
|
241
|
+
agent asserts in the place where the thing is built, the CLI re-derives
|
|
242
|
+
independently off disk, and a system wants both. Catching a bad pair at creation
|
|
243
|
+
time is the point of the first; not having to take that catch on trust is the
|
|
244
|
+
point of the second.
|
|
245
|
+
|
|
246
|
+
## A worked example
|
|
247
|
+
|
|
248
|
+
One stage file with two subjects — `Button`, freshly built and reviewed, and
|
|
249
|
+
`Card`, rebuilt without a review:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"stage": "component-builder",
|
|
254
|
+
"subjects": {
|
|
255
|
+
"Button": {
|
|
256
|
+
"at": "2026-09-12T17:41:00.000Z",
|
|
257
|
+
"changed": [
|
|
258
|
+
"Built Button as a COMPONENT_SET with 18 variants",
|
|
259
|
+
"Bound fills, strokes and radius to semantic variables"
|
|
260
|
+
],
|
|
261
|
+
"checks": [
|
|
262
|
+
{
|
|
263
|
+
"name": "structural-read-back",
|
|
264
|
+
"method": "attested",
|
|
265
|
+
"result": "pass",
|
|
266
|
+
"evidence": "COMPONENT_SET 41:207, 18 children, axes type/size/state"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"name": "state-baseline",
|
|
270
|
+
"method": "attested",
|
|
271
|
+
"result": "pass",
|
|
272
|
+
"evidence": "state axis: default, hover, focus, active, disabled"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "figma-name",
|
|
276
|
+
"method": "attested",
|
|
277
|
+
"result": "pass",
|
|
278
|
+
"evidence": "Figma node named Button"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "review",
|
|
282
|
+
"method": "attested",
|
|
283
|
+
"result": "pass",
|
|
284
|
+
"evidence": "approved"
|
|
285
|
+
}
|
|
286
|
+
],
|
|
287
|
+
"screenshot": "41:207",
|
|
288
|
+
"advancedBecause": "Read-back and review both passed; every variant resolves to bound variables."
|
|
289
|
+
},
|
|
290
|
+
"Card": {
|
|
291
|
+
"at": "2026-09-12T18:04:00.000Z",
|
|
292
|
+
"changed": ["Rebuilt Card with the elevation variant axis"],
|
|
293
|
+
"checks": [
|
|
294
|
+
{
|
|
295
|
+
"name": "structural-read-back",
|
|
296
|
+
"method": "attested",
|
|
297
|
+
"result": "pass",
|
|
298
|
+
"evidence": "COMPONENT_SET 41:418, 6 children, axes elevation/padding"
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
"advancedBecause": "Structure verified; no review requested for a variant-axis rebuild."
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
@@ -57,39 +57,35 @@ The cause was the transform group, not the adapter concept. That configuration
|
|
|
57
57
|
now ships as tested code at
|
|
58
58
|
`${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs`, is installed into the
|
|
59
59
|
consumer's repo, and is verified end to end against a real source (195 emitted
|
|
60
|
-
symbols matched,
|
|
61
|
-
|
|
60
|
+
symbols matched, 107 of them with their magnitude also checked, zero rule
|
|
61
|
+
failures under the shipping ruleset, on both light and dark builds).
|
|
62
|
+
The badge is back on that basis. What "matched" does and does not check is in
|
|
63
|
+
*Verify, always* in `${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`.
|
|
62
64
|
|
|
63
65
|
**What the badge does not cover: nothing is compiled.** Every emitted value is
|
|
64
66
|
checked to be a well-formed Swift or Kotlin *literal* — `tokens:validate-output`'s
|
|
65
67
|
`invalid-literal` rule parses each one and fails on anything that is not
|
|
66
68
|
(#53) — but no `swiftc` or `kotlinc` runs, so a type mismatch or a call to an
|
|
67
|
-
undefined symbol would still pass.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
undefined symbol would still pass. Which values the module quotes, and which it
|
|
70
|
+
drops for having no native form, is stated in §4 and §6 of
|
|
71
|
+
`${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`. A dropped token is
|
|
72
|
+
not hidden: `tokens:validate-output` reports it as unemitted — see
|
|
73
|
+
`${CLAUDE_PLUGIN_ROOT}/scripts/README.md`.
|
|
72
74
|
|
|
73
75
|
Since 0.17.0 that gap is measured rather than assumed on ThroughLine's side:
|
|
74
76
|
this project's end-to-end runs compile what they generate — `kotlinc` typechecks
|
|
75
77
|
`Tokens.kt` against Compose stubs, `swiftc -parse` checks `Tokens.swift` —
|
|
76
|
-
against a real
|
|
78
|
+
against a real 318-token system. That is evidence about the adapter shipped to
|
|
77
79
|
you, not a gate on your build, and it does not widen what the badge asserts
|
|
78
80
|
about your output.
|
|
79
81
|
|
|
80
82
|
`android-kotlin` uses the same module and stays Tier 2: its remaining unknowns
|
|
81
83
|
are on the consumption side — Compose `dp`/`sp` behaviour against a real Compose
|
|
82
84
|
app, resource-qualifier conventions, package layout — which building tokens does
|
|
83
|
-
not exercise.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`: a scale primitive
|
|
88
|
-
**nothing references** carries no role and stays `dp`, named by a
|
|
89
|
-
`tokens:validate-output` `unreferenced-text-sibling` advisory; and an `em`
|
|
90
|
-
letterSpacing reaches Compose as a real `.em` TextUnit but is excluded from
|
|
91
|
-
Swift deliberately, since letter spacing there needs a font size no constant
|
|
92
|
-
Swift value could carry.
|
|
85
|
+
not exercise. Which tokens emit as `sp`, and the text-unit limits that remain on
|
|
86
|
+
each platform, are stated once, in §4 of
|
|
87
|
+
`${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`, which is generated
|
|
88
|
+
from the module itself.
|
|
93
89
|
`tokens:validate-output` remains what decides whether any adapter can be
|
|
94
90
|
trusted, and re-promotion is available to any adapter that passes it against a
|
|
95
91
|
real source.
|
|
@@ -144,6 +140,9 @@ emit as references to primitive vars rather than flattened literals.
|
|
|
144
140
|
Tailwind tokens; the standalone `tailwind` adapter maps tokens into the
|
|
145
141
|
Tailwind theme config directly; `mui` emits a JS theme object whose palette
|
|
146
142
|
references preserve the cascade through object structure.
|
|
143
|
+
`tokens:validate-output` checks the generated CSS against its source too, and
|
|
144
|
+
only the CSS: for `tailwind`, the file declaring the custom properties, not the
|
|
145
|
+
theme config. See `${CLAUDE_PLUGIN_ROOT}/scripts/README.md`.
|
|
147
146
|
- **Native adapters** (`ios-swift`, and generated native targets like
|
|
148
147
|
Android/Kotlin): `outputReferences: false` — references resolve to literal
|
|
149
148
|
values at build time, because the target language has no runtime var
|
|
@@ -154,45 +153,31 @@ emit as references to primitive vars rather than flattened literals.
|
|
|
154
153
|
|
|
155
154
|
## What the stock configuration gets wrong
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
transform group mishandles
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
- **CSS expressions in a value
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
-
resolver will not traverse into one, so every alias to the child fails to
|
|
178
|
-
resolve and emits as a bare literal; its collector also stops there, so the
|
|
179
|
-
child is never emitted at all. *Fix: a preprocessor that resolves aliases and
|
|
180
|
-
hoists the children.*
|
|
181
|
-
- **`%` and `em` dimensions** — parent-relative or container-relative, so there
|
|
182
|
-
genuinely is no build-time native magnitude. This one is a real limit rather
|
|
183
|
-
than a configuration gap. *Handling: filter them out of native builds — on the
|
|
184
|
-
authored value, since a `100%` token may be typed `string`, not `dimension`.*
|
|
156
|
+
Real token sources carry all of these, and a **stock** Style Dictionary
|
|
157
|
+
transform group mishandles them silently — emitting output that compiles and is
|
|
158
|
+
wrong, which is why `tokens:validate-output` exists.
|
|
159
|
+
|
|
160
|
+
The native fixes ship as a tested module at
|
|
161
|
+
`${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs`. **Import that module** rather
|
|
162
|
+
than configuring a native adapter from a stock `transformGroup` or transcribing
|
|
163
|
+
the reference doc. What goes wrong in each case, and exactly what the module does
|
|
164
|
+
about it, is stated once, in
|
|
165
|
+
`${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`, which is generated
|
|
166
|
+
from the module and checked for freshness in CI. This list only points there:
|
|
167
|
+
|
|
168
|
+
- **`px`-authored dimensions** — the stock groups assume `rem` and multiply by
|
|
169
|
+
16. See §1, *Read the authored unit*.
|
|
170
|
+
- **CSS expressions in a value**, such as `color-mix()` — see §2, *Compute
|
|
171
|
+
`color-mix()` to a literal*.
|
|
172
|
+
- **Dual-node tokens**, a node carrying both a `$value` and children — see §3,
|
|
173
|
+
*Resolve aliases and hoist dual-node children*.
|
|
174
|
+
- **`%` and `em` dimensions** — see `nativeFilter` in §4, *Assemble the platform
|
|
175
|
+
from the stock list*.
|
|
185
176
|
- **A third mode axis** — this reference models theme (`.dark` /
|
|
186
177
|
`[data-theme]`) and brand (`[data-brand]`). A viewport axis carrying its own
|
|
187
178
|
spacing and type scales is common and has no mapping here; on native it is
|
|
188
179
|
size classes and resource qualifiers, resolved by a different mechanism
|
|
189
|
-
entirely.
|
|
190
|
-
|
|
191
|
-
**Native dimension transforms must read the authored unit.** The stock
|
|
192
|
-
`ios-swift` and `compose` transform groups assume `rem` input and multiply by
|
|
193
|
-
16. Against a `px`-authored source that silently produces output at sixteen
|
|
194
|
-
times scale which compiles and ships. Emit 1:1 for `px` and unitless ratios;
|
|
195
|
-
×16 only for `rem`.
|
|
180
|
+
entirely. The module does not address this one.
|
|
196
181
|
|
|
197
182
|
## Brownfield value transforms
|
|
198
183
|
|