@radicool/throughline 0.18.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/native-adapter-config.md +112 -25
- 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 +123 -4
- package/scripts/lib/native-literal.mjs +22 -2
- package/scripts/lib/proof.mjs +186 -0
- package/scripts/lib/sd-native.mjs +112 -25
- package/scripts/lib/source-scan.mjs +45 -0
- package/scripts/validate-adherence.mjs +895 -0
- package/scripts/validate-token-output.mjs +611 -46
- 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.
|
|
@@ -54,7 +54,8 @@ consumer's repo.
|
|
|
54
54
|
import { readFileSync } from 'node:fs';
|
|
55
55
|
import {
|
|
56
56
|
flattenDtcg,
|
|
57
|
-
|
|
57
|
+
flattenPipelineTypes,
|
|
58
|
+
extNamespace,
|
|
58
59
|
resolveValue,
|
|
59
60
|
findModeCollisions,
|
|
60
61
|
TEXT_UNIT_NAMES,
|
|
@@ -152,10 +153,25 @@ Both are fixed before Style Dictionary sees the tree.
|
|
|
152
153
|
// Marks a node whose AUTHORED $value was a whole-value reference, so the hoist
|
|
153
154
|
// can decline to override the type DTCG 5.2.2 rule 1 already determined from the
|
|
154
155
|
// referent. A WeakSet keyed on the node object, rather than a property written
|
|
155
|
-
// onto it,
|
|
156
|
-
// membership along with the rest of the identity, so
|
|
157
|
-
//
|
|
156
|
+
// onto it, keeps this mechanism from leaking across calls: structuredClone
|
|
157
|
+
// drops the membership along with the rest of the identity, so a second pass
|
|
158
|
+
// starts clean rather than inheriting the first pass's marks. That is exactly
|
|
159
|
+
// what a property written onto the node would get wrong. It is not on its own a
|
|
160
|
+
// guarantee that preprocess is idempotent — see the limit recorded at
|
|
161
|
+
// nativePlatform's preprocessors line (#90).
|
|
158
162
|
const WAS_REF = new WeakSet();
|
|
163
|
+
|
|
164
|
+
// The path the AUTHOR wrote for a node the hoist has moved. hoistDualNodes
|
|
165
|
+
// recurses depth-first, so by the time an outer frame takes Object.entries it
|
|
166
|
+
// already contains names the inner frame synthesised — and a collision reported
|
|
167
|
+
// from that frame named a path appearing nowhere in the source (#61). Recording
|
|
168
|
+
// the authored path when a node is hoisted lets the outer frame report what the
|
|
169
|
+
// author can actually search for.
|
|
170
|
+
//
|
|
171
|
+
// Keyed on node identity, like WAS_REF, and safe across calls for the same
|
|
172
|
+
// reason: preprocess structuredClones its input, so every call works on fresh
|
|
173
|
+
// objects.
|
|
174
|
+
const AUTHORED_PATH = new WeakMap();
|
|
159
175
|
const WHOLE_REF = /^\{[^}]+\}$/;
|
|
160
176
|
|
|
161
177
|
function interpolate(value, flat) {
|
|
@@ -228,20 +244,27 @@ function hoistDualNodes(node, collisions, prefix = [], groupType = undefined) {
|
|
|
228
244
|
for (const [childKey, childVal] of Object.entries(val)) {
|
|
229
245
|
if (childKey.startsWith('$') || !childVal || typeof childVal !== 'object') continue;
|
|
230
246
|
const hoisted = key + childKey[0].toUpperCase() + childKey.slice(1);
|
|
231
|
-
const from = [...prefix, key, childKey].join('.');
|
|
247
|
+
const from = AUTHORED_PATH.get(childVal) ?? [...prefix, key, childKey].join('.');
|
|
232
248
|
if (Object.hasOwn(node, hoisted)) {
|
|
233
249
|
const existingNode = node[hoisted];
|
|
234
|
-
|
|
250
|
+
// An array has no $value either, and was therefore labelled "(a group)"
|
|
251
|
+
// (#61). It is neither — an array at this position is malformed DTCG.
|
|
252
|
+
// Say so, rather than naming a shape the author will go looking for.
|
|
253
|
+
const isArray = Array.isArray(existingNode);
|
|
254
|
+
const isGroup =
|
|
255
|
+
existingNode !== null && typeof existingNode === 'object' && !isArray && !('$value' in existingNode);
|
|
235
256
|
collisions.push({
|
|
236
257
|
from,
|
|
237
258
|
onto: [...prefix, hoisted].join('.'),
|
|
238
259
|
isGroup,
|
|
260
|
+
isArray,
|
|
239
261
|
claimant: claimedBy.get(hoisted),
|
|
240
|
-
existing:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
existing:
|
|
263
|
+
isGroup || isArray
|
|
264
|
+
? undefined
|
|
265
|
+
: existingNode && typeof existingNode === 'object'
|
|
266
|
+
? existingNode.$value
|
|
267
|
+
: existingNode,
|
|
245
268
|
});
|
|
246
269
|
continue;
|
|
247
270
|
}
|
|
@@ -264,8 +287,17 @@ function hoistDualNodes(node, collisions, prefix = [], groupType = undefined) {
|
|
|
264
287
|
// type suits the child badly, because the hoist is not entitled to
|
|
265
288
|
// improve on what the source says; but the carry firing at all is the
|
|
266
289
|
// hoist saying something the source did not.
|
|
290
|
+
//
|
|
291
|
+
// A GROUP child is excluded (#67). The carry's premise is that the dual
|
|
292
|
+
// node was the child's closest $type-bearing ancestor as authored, and
|
|
293
|
+
// the hoist took that away. For a group child the premise never held:
|
|
294
|
+
// 5.2.2 inherits from the closest parent GROUP, the dual node is not
|
|
295
|
+
// one, so it was never that group's inheritance source and the hoist
|
|
296
|
+
// removed nothing. Carrying there does not repair, it invents — and it
|
|
297
|
+
// invents for every token beneath the group, not just one node.
|
|
267
298
|
if (
|
|
268
299
|
!('$type' in childVal) &&
|
|
300
|
+
'$value' in childVal &&
|
|
269
301
|
'$type' in val &&
|
|
270
302
|
!WAS_REF.has(childVal) &&
|
|
271
303
|
inherited === undefined
|
|
@@ -273,6 +305,7 @@ function hoistDualNodes(node, collisions, prefix = [], groupType = undefined) {
|
|
|
273
305
|
childVal.$type = val.$type;
|
|
274
306
|
}
|
|
275
307
|
node[hoisted] = childVal;
|
|
308
|
+
AUTHORED_PATH.set(childVal, from);
|
|
276
309
|
delete val[childKey];
|
|
277
310
|
claimedBy.set(hoisted, from);
|
|
278
311
|
}
|
|
@@ -371,6 +404,9 @@ function classifyTextUnits(node, types, prefix = []) {
|
|
|
371
404
|
types[path.join('.')] === 'dimension' &&
|
|
372
405
|
TEXT_ROLE_UNIT.test(String(val.$value).trim())
|
|
373
406
|
) {
|
|
407
|
+
// extNamespace throws with the token path if the source authored either
|
|
408
|
+
// level as a primitive — legal DTCG this module does not handle (#62).
|
|
409
|
+
extNamespace(val, path.join('.'));
|
|
374
410
|
val.$extensions ??= {};
|
|
375
411
|
val.$extensions[EXT_NS] ??= {};
|
|
376
412
|
const ns = val.$extensions[EXT_NS];
|
|
@@ -379,7 +415,9 @@ function classifyTextUnits(node, types, prefix = []) {
|
|
|
379
415
|
// one, so a unitless value is declined by every size transform regardless
|
|
380
416
|
// of what is stamped here (see isRatio, #52). Declining to overwrite IS
|
|
381
417
|
// the feature: it costs no configuration parameter, and it is what makes
|
|
382
|
-
//
|
|
418
|
+
// STAMPING idempotent — a second pass never rewrites a role a first pass
|
|
419
|
+
// set. What it does not settle is whether a second pass finds the same
|
|
420
|
+
// candidates; the hoist can rename a node between the two (#90).
|
|
383
421
|
if (!('nativeUnit' in ns)) ns.nativeUnit = 'text';
|
|
384
422
|
}
|
|
385
423
|
classifyTextUnits(val, types, path);
|
|
@@ -402,7 +440,8 @@ function classifyTextUnits(node, types, prefix = []) {
|
|
|
402
440
|
// A path may name no node at all. resolveInPlace deliberately leaves an
|
|
403
441
|
// unresolvable reference in place for Style Dictionary to report, so the graph
|
|
404
442
|
// can hold an edge to a token that does not exist. Skip it. This is also what
|
|
405
|
-
// keeps the second preprocess pass from throwing
|
|
443
|
+
// keeps the second preprocess pass from throwing — which is a precondition for
|
|
444
|
+
// idempotency, not the whole of it (#90).
|
|
406
445
|
function applyTextRoleGraph(node, typographic, types) {
|
|
407
446
|
for (const path of typographic) {
|
|
408
447
|
let target = node;
|
|
@@ -412,6 +451,7 @@ function applyTextRoleGraph(node, typographic, types) {
|
|
|
412
451
|
if (!target || typeof target !== 'object' || !('$value' in target)) continue;
|
|
413
452
|
if (types[path] !== 'dimension') continue;
|
|
414
453
|
if (!TEXT_ROLE_UNIT.test(String(target.$value).trim())) continue;
|
|
454
|
+
extNamespace(target, path);
|
|
415
455
|
target.$extensions ??= {};
|
|
416
456
|
target.$extensions[EXT_NS] ??= {};
|
|
417
457
|
const ns = target.$extensions[EXT_NS];
|
|
@@ -426,14 +466,16 @@ export function preprocess(dict) {
|
|
|
426
466
|
// the graph is made of.
|
|
427
467
|
const { typographic } = textRoleGraph(dict);
|
|
428
468
|
const resolved = resolveInPlace(structuredClone(dict), flattenDtcg(dict));
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
469
|
+
// Types for the whole tree, walked once and shared by both passes below.
|
|
470
|
+
// Neither writes $type or moves a node, so one map is correct for both, and
|
|
471
|
+
// sharing it is what makes them agree by construction rather than by two
|
|
472
|
+
// copies of the same rule staying in step (#85).
|
|
433
473
|
//
|
|
434
|
-
// Computed on the
|
|
435
|
-
//
|
|
436
|
-
|
|
474
|
+
// Computed on the RAW dict, not the resolved clone. resolveInPlace only
|
|
475
|
+
// rewrites $value strings, so paths and types are identical between the two —
|
|
476
|
+
// except for the carry, whose rule asks whether a value WAS a whole-value
|
|
477
|
+
// reference, and resolution has already destroyed that (#89).
|
|
478
|
+
const types = flattenPipelineTypes(dict);
|
|
437
479
|
const out = hoistDualNodes(
|
|
438
480
|
applyTextRoleGraph(classifyTextUnits(resolved, types), typographic, types),
|
|
439
481
|
collisions,
|
|
@@ -443,6 +485,9 @@ export function preprocess(dict) {
|
|
|
443
485
|
.slice(0, 5)
|
|
444
486
|
.map((c) => {
|
|
445
487
|
const line = ` ${c.from} -> ${c.onto}`;
|
|
488
|
+
if (c.isArray) {
|
|
489
|
+
return line + ' (an array — neither a token nor a group, and not valid DTCG here)';
|
|
490
|
+
}
|
|
446
491
|
if (c.isGroup) {
|
|
447
492
|
return line + (c.claimant ? ` (a group, already claimed by the hoist of ${c.claimant})` : ' (a group)');
|
|
448
493
|
}
|
|
@@ -629,7 +674,14 @@ const DECLINED_STOCK_TRANSFORMS = {
|
|
|
629
674
|
// Order is never compared: our lists are hand-ordered for our own reasons and
|
|
630
675
|
// do not inherit stock order. Removals are never reported: a declined name
|
|
631
676
|
// disappearing is a non-event.
|
|
632
|
-
|
|
677
|
+
// `platforms` and `declined` are parameters with the shipped config as their
|
|
678
|
+
// default, so the contradiction branch below is reachable from a test. The
|
|
679
|
+
// single caller passes neither; the audit's contract is unchanged.
|
|
680
|
+
export function auditStockGroups(
|
|
681
|
+
transformGroups,
|
|
682
|
+
platforms = PLATFORMS,
|
|
683
|
+
declined = DECLINED_STOCK_TRANSFORMS,
|
|
684
|
+
) {
|
|
633
685
|
if (typeof transformGroups !== 'object' || transformGroups === null) {
|
|
634
686
|
return [
|
|
635
687
|
"throughline: could not read Style Dictionary's stock transform groups " +
|
|
@@ -638,7 +690,29 @@ export function auditStockGroups(transformGroups) {
|
|
|
638
690
|
];
|
|
639
691
|
}
|
|
640
692
|
const warnings = [];
|
|
641
|
-
for (const [platform, preset] of Object.entries(
|
|
693
|
+
for (const [platform, preset] of Object.entries(platforms)) {
|
|
694
|
+
// A name in BOTH lists is a contradiction the unaccounted filter below
|
|
695
|
+
// cannot see, because either membership alone suppresses the warning (#75).
|
|
696
|
+
// The config would be saying "we run this" and "we deliberately do not" at
|
|
697
|
+
// once, and whichever is wrong is silently the loser: if the decline is
|
|
698
|
+
// right the transform still runs, and if the run is right the decline is a
|
|
699
|
+
// lie the next maintainer will read as settled. Reported here rather than
|
|
700
|
+
// guarded at the definition, so it travels with the rest of the audit.
|
|
701
|
+
//
|
|
702
|
+
// Checked before stockGroup, because this is wrong regardless of whether
|
|
703
|
+
// Style Dictionary still has the group to compare against.
|
|
704
|
+
if (Array.isArray(preset.transforms)) {
|
|
705
|
+
const contradictory = preset.transforms.filter((name) => Object.hasOwn(declined, name));
|
|
706
|
+
if (contradictory.length) {
|
|
707
|
+
warnings.push(
|
|
708
|
+
`throughline: PLATFORMS['${platform}'] both runs and declines ` +
|
|
709
|
+
`${contradictory.join(', ')}. A transform cannot be in transforms and in ` +
|
|
710
|
+
'DECLINED_STOCK_TRANSFORMS at once — one of the two is wrong, and the ' +
|
|
711
|
+
'audit cannot tell which. This is a throughline packaging defect — ' +
|
|
712
|
+
'please report it.',
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
642
716
|
const group = preset.stockGroup;
|
|
643
717
|
if (!group || !Array.isArray(preset.transforms)) {
|
|
644
718
|
warnings.push(
|
|
@@ -662,7 +736,7 @@ export function auditStockGroups(transformGroups) {
|
|
|
662
736
|
const unaccounted = stock.filter(
|
|
663
737
|
(name) =>
|
|
664
738
|
!preset.transforms.includes(name) &&
|
|
665
|
-
!Object.hasOwn(
|
|
739
|
+
!Object.hasOwn(declined, name),
|
|
666
740
|
);
|
|
667
741
|
if (unaccounted.length) {
|
|
668
742
|
const n = unaccounted.length;
|
|
@@ -764,8 +838,21 @@ export function nativePlatform({ platform, buildPath, className = 'Tokens', pack
|
|
|
764
838
|
// Carried here, not left to the caller: authored() reads the ORIGINAL
|
|
765
839
|
// $value, so without this preprocessor every aliased dimension still holds
|
|
766
840
|
// an unresolved {spacing.space.4}, no size transform fires, and the build
|
|
767
|
-
// emits bare px literals.
|
|
768
|
-
//
|
|
841
|
+
// emits bare px literals.
|
|
842
|
+
//
|
|
843
|
+
// A project that ALSO declares this preprocessor at top level runs it twice,
|
|
844
|
+
// which is harmless in every shape measured — and is the wiring our own
|
|
845
|
+
// usage snippet shows, so it is the common case rather than a corner.
|
|
846
|
+
//
|
|
847
|
+
// Stated no wider than it holds (#90): preprocess is idempotent except where
|
|
848
|
+
// the hoist invents a name the second pass then classifies. `a.font` with a
|
|
849
|
+
// child `size` camel-joins to `a.fontSize`; the first pass correctly declines
|
|
850
|
+
// `size`, and the second sees a typographic member name the source never
|
|
851
|
+
// authored. It changes no emitted output — Style Dictionary runs
|
|
852
|
+
// typeDtcgDelegate between the two passes and types that child anyway, so
|
|
853
|
+
// both passes reach the same file — and the repair belongs with the hoist,
|
|
854
|
+
// which has no way to record the names it invented. Pinned by test rather
|
|
855
|
+
// than papered over.
|
|
769
856
|
preprocessors: ['dtcg/resolve-dual-node'],
|
|
770
857
|
buildPath,
|
|
771
858
|
options: { outputReferences: false },
|