@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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/adapters/codex/prompts/component-builder.md +24 -1
  3. package/adapters/codex/prompts/document-component.md +10 -1
  4. package/adapters/codex/prompts/icon-system-builder.md +4 -0
  5. package/adapters/codex/prompts/storybook-chromatic-builder.md +114 -7
  6. package/adapters/codex/prompts/token-builder.md +110 -3
  7. package/adapters/codex/prompts/token-crosswalk-builder.md +2 -0
  8. package/adapters/codex/prompts/token-sheet-builder.md +4 -0
  9. package/adapters/codex/prompts/token-sync-layer.md +140 -20
  10. package/adapters/cursor/.cursor/commands/document-component.md +10 -1
  11. package/adapters/cursor/.cursor/rules/component-builder.mdc +24 -1
  12. package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +4 -0
  13. package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +114 -7
  14. package/adapters/cursor/.cursor/rules/token-builder.mdc +110 -3
  15. package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +2 -0
  16. package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +4 -0
  17. package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +140 -20
  18. package/adapters/generic/commands/document-component.md +10 -1
  19. package/adapters/generic/skills/component-builder/SKILL.md +24 -1
  20. package/adapters/generic/skills/icon-system-builder/SKILL.md +4 -0
  21. package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +114 -7
  22. package/adapters/generic/skills/token-builder/SKILL.md +110 -3
  23. package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +2 -0
  24. package/adapters/generic/skills/token-sheet-builder/SKILL.md +4 -0
  25. package/adapters/generic/skills/token-sync-layer/SKILL.md +140 -20
  26. package/package.json +1 -1
  27. package/references/component-doc-schema.md +11 -1
  28. package/references/figma-component-standards.md +14 -2
  29. package/references/figma-scripting.md +12 -2
  30. package/references/manifest-schema.md +42 -2
  31. package/references/proof-bundle.md +305 -0
  32. package/references/sync-adapters.md +38 -53
  33. package/scripts/README.md +140 -6
  34. package/scripts/build-doc-card-builder.mjs +1 -1
  35. package/scripts/grep-color-usage.mjs +10 -30
  36. package/scripts/guard-token-removal.mjs +10 -26
  37. package/scripts/lib/component-states.mjs +56 -0
  38. package/scripts/lib/contrast.mjs +117 -0
  39. package/scripts/lib/dtcg.mjs +3 -2
  40. package/scripts/lib/proof.mjs +186 -0
  41. package/scripts/lib/source-scan.mjs +45 -0
  42. package/scripts/validate-adherence.mjs +895 -0
  43. package/scripts/validate-token-output.mjs +468 -63
  44. package/scripts/verify-check.mjs +710 -0
@@ -0,0 +1,186 @@
1
+ // The proof-bundle store (`docs/specs/2026-09-12-verification-proof-bundle.md`,
2
+ // step 4): load, merge and fingerprint the per-stage files at
3
+ // `design-system/proof/<stage>.json`, and read the manifest's adoption state
4
+ // for each stage. Zero dependencies.
5
+ //
6
+ // See `references/proof-bundle.md` for the file shape and the vocabulary this
7
+ // module defines.
8
+ import { existsSync, readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { stableStringify, fingerprint } from './doc-record.mjs';
11
+
12
+ export const PROOF_DIR = 'design-system/proof';
13
+
14
+ // The stage vocabulary. A change here must also update the stage table in
15
+ // `references/proof-bundle.md`: the constant and the prose are one vocabulary,
16
+ // and a stage this list accepts but the reference does not name is a stage
17
+ // nobody knows how to record.
18
+ //
19
+ // `token-builder` is system-wide. The vocabulary names the places where the
20
+ // negative conditions' evidence is produced, and token creation is now one of
21
+ // them — it is the cheapest place in the system to catch a mode built with poor
22
+ // contrast, because the mode is being defined right then and nothing is built
23
+ // on it yet. Its subject is the whole colour system, never a component.
24
+ export const STAGES = [
25
+ 'component-builder',
26
+ 'storybook-chromatic-builder',
27
+ 'token-sync-layer',
28
+ 'token-builder',
29
+ ];
30
+
31
+ // Which stages are keyed by component (subject = component name) rather than
32
+ // by the system as a whole (subject = "system"). This is the machine-readable
33
+ // copy of the split `references/proof-bundle.md` documents in prose — it is
34
+ // the constant `checkProof` asks "is this stage keyed by component?".
35
+ export const PER_COMPONENT_STAGES = new Set([
36
+ 'component-builder',
37
+ 'storybook-chromatic-builder',
38
+ ]);
39
+
40
+ // Tells the reader (Step 6's `checkProof`) how to compare a recorded derived
41
+ // check against a rerun: a check scoped to "component" is compared per
42
+ // subject; one scoped to "system" is compared once, system-wide, regardless
43
+ // of which subject it was recorded under.
44
+ export const DERIVED_RULE_SCOPE = {
45
+ 'orphan-token': 'system',
46
+ 'state-incomplete': 'component',
47
+ 'name-drift': 'component',
48
+ // Contrast is a property of the token system as a whole, not of any one
49
+ // component, so a recorded result is compared against the rerun system-wide —
50
+ // the same scope orphan-token has.
51
+ 'color-contrast': 'system',
52
+ };
53
+
54
+ // The names this stage has not yet proven, per the Adoption decision's
55
+ // capture-once-then-shrink rule. A missing `exempt` key is deliberately read
56
+ // as "exempts nothing", not "exempts everything": the only way to reach a
57
+ // stage entry with no `exempt` key is a hand-edited manifest (record mode
58
+ // always writes one for a per-component stage), and the off-switch reading
59
+ // would let a hand-edit silently stop the gate. A missing key must get
60
+ // louder, not quieter.
61
+ export function stageExempt(manifest, stage) {
62
+ return new Set(manifest.verification?.stages?.[stage]?.exempt ?? []);
63
+ }
64
+
65
+ // The human-readable "when did this system opt in", written once by record
66
+ // mode and never rewritten. No gate reads it.
67
+ //
68
+ // It is not compared against `components.meta[name].updatedAt`, and `exempt`
69
+ // is a captured list of names rather than a date comparison, for the same
70
+ // reason: `updatedAt` has a second writer.
71
+ // `storybook-chromatic-builder` refreshes it on promotion
72
+ // (`skills/storybook-chromatic-builder/SKILL.md:276-277`), a different stage
73
+ // from the one that owes the entry, so a date gate would fail a pre-existing
74
+ // component the first time an unrelated storying run promotes it.
75
+ //
76
+ // Neither `adoptedAt` nor `exempt` is derived from the stage file's entries,
77
+ // either: `mergeEntry` is latest-wins and replaces a subject's `at`, so "the
78
+ // earliest `at` in the file" would slide forward every time the earliest
79
+ // subject is rebuilt.
80
+ export function stageAdoptedAt(manifest, stage) {
81
+ return manifest.verification?.stages?.[stage]?.adoptedAt ?? null;
82
+ }
83
+
84
+ export function stagePath(root, stage) {
85
+ return join(root, PROOF_DIR, `${stage}.json`);
86
+ }
87
+
88
+ export function loadStage(root, stage) {
89
+ const path = stagePath(root, stage);
90
+ if (!existsSync(path)) return null;
91
+ return JSON.parse(readFileSync(path, 'utf8'));
92
+ }
93
+
94
+ export function stageFingerprint(stageFile) {
95
+ return fingerprint(stableStringify(stageFile));
96
+ }
97
+
98
+ const CHECK_METHODS = ['derived', 'attested'];
99
+ const CHECK_RESULTS = ['pass', 'fail'];
100
+
101
+ export function entryProblems(entry) {
102
+ const problems = [];
103
+ if (typeof entry.at !== 'string' || entry.at === '') {
104
+ problems.push('"at" must be a non-empty string');
105
+ }
106
+ if (typeof entry.advancedBecause !== 'string' || entry.advancedBecause === '') {
107
+ problems.push('"advancedBecause" must be a non-empty string');
108
+ }
109
+ if (!Array.isArray(entry.changed) || !entry.changed.every((c) => typeof c === 'string')) {
110
+ problems.push('"changed" must be an array of strings');
111
+ }
112
+ if (!Array.isArray(entry.checks) || entry.checks.length === 0) {
113
+ problems.push('"checks" must be a non-empty array');
114
+ } else {
115
+ entry.checks.forEach((check, i) => {
116
+ if (!check || typeof check !== 'object') {
117
+ problems.push(`checks[${i}] must be an object`);
118
+ return;
119
+ }
120
+ if (typeof check.name !== 'string' || check.name === '') {
121
+ problems.push(`checks[${i}].name must be a non-empty string`);
122
+ }
123
+ if (!CHECK_METHODS.includes(check.method)) {
124
+ problems.push(`checks[${i}].method must be one of ${CHECK_METHODS.join(', ')}`);
125
+ }
126
+ if (!CHECK_RESULTS.includes(check.result)) {
127
+ problems.push(`checks[${i}].result must be one of ${CHECK_RESULTS.join(', ')}`);
128
+ }
129
+ if (typeof check.evidence !== 'string') {
130
+ problems.push(`checks[${i}].evidence must be a string`);
131
+ }
132
+ // Optional, and only `contrast-baseline` uses it today — but validated
133
+ // because it is load-bearing: token-sync-layer subtracts these (fg, bg,
134
+ // mode) triples from its own gate run's color-contrast failures, so a
135
+ // malformed one is the difference between a pair the user accepted and a
136
+ // pair nobody has seen. Reject it here rather than letting the sync
137
+ // silently match nothing.
138
+ if (check.accepted !== undefined) {
139
+ if (!Array.isArray(check.accepted)) {
140
+ problems.push(`checks[${i}].accepted must be an array when present`);
141
+ } else {
142
+ check.accepted.forEach((pair, j) => {
143
+ const wellFormed =
144
+ pair &&
145
+ typeof pair === 'object' &&
146
+ ['fg', 'bg', 'mode'].every((k) => typeof pair[k] === 'string' && pair[k] !== '');
147
+ if (!wellFormed) {
148
+ problems.push(
149
+ `checks[${i}].accepted[${j}] must carry non-empty "fg", "bg" and "mode" strings`,
150
+ );
151
+ }
152
+ });
153
+ }
154
+ }
155
+ });
156
+ }
157
+ if (entry.screenshot !== undefined && typeof entry.screenshot !== 'string') {
158
+ problems.push('"screenshot" must be a string when present');
159
+ }
160
+ return problems;
161
+ }
162
+
163
+ export function mergeEntry(stageFile, stage, subject, entry) {
164
+ const subjects = { ...(stageFile?.subjects ?? {}), [subject]: entry };
165
+ const sortedSubjects = {};
166
+ for (const key of Object.keys(subjects).sort()) {
167
+ sortedSubjects[key] = subjects[key];
168
+ }
169
+ return { stage, subjects: sortedSubjects };
170
+ }
171
+
172
+ export function recordedChecks(stageFile) {
173
+ const out = [];
174
+ for (const [subject, entry] of Object.entries(stageFile.subjects ?? {})) {
175
+ for (const check of entry.checks ?? []) {
176
+ out.push({
177
+ subject,
178
+ name: check.name,
179
+ method: check.method,
180
+ result: check.result,
181
+ evidence: check.evidence,
182
+ });
183
+ }
184
+ }
185
+ return out;
186
+ }
@@ -0,0 +1,45 @@
1
+ // Walking a consumer's source tree, shared by every gate that scans one.
2
+ //
3
+ // grep-color-usage.mjs and guard-token-removal.mjs each carried their own copy
4
+ // of this — identical excludes, and a walk that differed only in which file
5
+ // extensions it yielded. The duplication was the shape #57 was filed for: two
6
+ // definitions of one rule, agreeing today, with nothing keeping them in step.
7
+ //
8
+ // The file filter is the caller's, not a built-in: a colour scan wants
9
+ // stylesheets and a symbol guard does not, and neither should have to fork the
10
+ // walker to say so.
11
+ import { readdirSync, statSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ export const DEFAULT_EXCLUDES = [
15
+ /(^|\/)node_modules(\/|$)/,
16
+ /(^|\/)generated(\/|$)/,
17
+ /\.generated\./,
18
+ /\.test\./,
19
+ /\.spec\./,
20
+ /(^|\/)__tests__(\/|$)/,
21
+ /(^|\/)dist(\/|$)/,
22
+ /(^|\/)\.next(\/|$)/,
23
+ /(^|\/)storybook-static(\/|$)/,
24
+ ];
25
+
26
+ // Every text file a design system's values can hide in.
27
+ export const SOURCE_EXT = /\.(scss|sass|css|tsx?|jsx?|mjs|cjs|vue|svelte|html|svg)$/;
28
+
29
+ export function* walk(root, { excludes = DEFAULT_EXCLUDES, fileFilter = SOURCE_EXT } = {}) {
30
+ for (const entry of readdirSync(root)) {
31
+ const full = join(root, entry);
32
+ if (excludes.some((re) => re.test(full))) continue;
33
+ const st = statSync(full);
34
+ if (st.isDirectory()) yield* walk(full, { excludes, fileFilter });
35
+ else if (fileFilter.test(full)) yield full;
36
+ }
37
+ }
38
+
39
+ // A design system records a component's DISPLAY name — a real manifest holds
40
+ // "Select Menu" — while the code exports <SelectMenu>. Nothing in the schema
41
+ // says `name` is a code identifier, and nothing should be changed to make it
42
+ // one: a display name is right for a doc surface. So the comparison normalises,
43
+ // the same way validate-token-output.mjs already folds adapter naming
44
+ // conventions onto each other.
45
+ export const normalizeName = (s) => String(s).toLowerCase().replace(/[^a-z0-9]/g, '');