@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
|
@@ -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, '');
|