@radicool/throughline 0.14.0 → 0.15.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/adapters/codex/AGENTS.md +10 -10
- package/adapters/codex/prompts/component-builder.md +59 -15
- package/adapters/codex/prompts/document-component.md +42 -10
- package/adapters/codex/prompts/storybook-chromatic-builder.md +52 -9
- package/adapters/cursor/.cursor/commands/document-component.md +42 -10
- package/adapters/cursor/.cursor/rules/component-builder.mdc +60 -16
- package/adapters/cursor/.cursor/rules/component-pipeline.mdc +1 -1
- package/adapters/cursor/.cursor/rules/design-system-audit.mdc +1 -1
- package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +1 -1
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/repository-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +1 -1
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +53 -10
- package/adapters/cursor/.cursor/rules/token-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +1 -1
- package/adapters/generic/AGENTS.md +10 -10
- package/adapters/generic/commands/document-component.md +42 -10
- package/adapters/generic/skills/component-builder/SKILL.md +59 -15
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +52 -9
- package/package.json +1 -1
- package/references/component-doc-archetypes.md +15 -11
- package/references/component-doc-schema.md +23 -5
- package/references/doc-card-builder.md +565 -0
- package/references/doc-writing-standard.md +144 -0
- package/references/figma-component-standards.md +63 -16
- package/references/guide-voice.md +96 -0
- package/references/manifest-schema.md +22 -4
- package/scripts/README.md +23 -0
- package/scripts/build-doc-card-builder.mjs +143 -0
- package/scripts/docs-check.mjs +18 -4
- package/scripts/docs-lint.mjs +163 -0
- package/scripts/install.mjs +13 -1
- package/scripts/lib/doc-card-plan.mjs +101 -0
- package/scripts/lib/doc-card-render.figma.js +371 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Doc-writing standard
|
|
2
|
+
|
|
3
|
+
The writing standard for all doc-record (`.doc.json`) content. Applied by the
|
|
4
|
+
authoring pipeline in `component-builder` and `/document-component`. Every
|
|
5
|
+
projection — Figma component description, doc card, Storybook MDX, AI digest —
|
|
6
|
+
inherits this text unchanged; none of them are written separately.
|
|
7
|
+
|
|
8
|
+
**Governing rule: describe the thing and how to use it, never how it was
|
|
9
|
+
made.**
|
|
10
|
+
|
|
11
|
+
## Register
|
|
12
|
+
|
|
13
|
+
Plain reference: neutral and declarative for descriptions, imperative for
|
|
14
|
+
guidance — the register Polaris and Carbon use.
|
|
15
|
+
|
|
16
|
+
This is **not** the conversational guide voice of `references/guide-voice.md`.
|
|
17
|
+
Guide voice is for skill conversation — a guide talking to the person building
|
|
18
|
+
the system. This standard is for doc-record content — what gets written about
|
|
19
|
+
the component itself, read by whoever uses it later. Different audience,
|
|
20
|
+
different register; do not blend them.
|
|
21
|
+
|
|
22
|
+
## Per-block rules
|
|
23
|
+
|
|
24
|
+
Before/after pairs are the real Button record
|
|
25
|
+
(`throughline-sample/design-system/docs/components/Button.doc.json`).
|
|
26
|
+
|
|
27
|
+
### `description`
|
|
28
|
+
|
|
29
|
+
2–3 sentences: what it is, what it's for, and the one thing that most changes
|
|
30
|
+
how you use it. Never variant/size counts (the specimen and legend already show
|
|
31
|
+
them), never token binding, never a slot inventory.
|
|
32
|
+
|
|
33
|
+
> **Before:** "A clickable control that triggers an action — submitting a
|
|
34
|
+
> form, confirming a decision, or opening a dialog. It comes in six emphasis
|
|
35
|
+
> variants across three sizes, supports optional leading and trailing icons
|
|
36
|
+
> and a loading state, and binds every color, spacing, radius, and type value
|
|
37
|
+
> to the system's semantic tokens."
|
|
38
|
+
>
|
|
39
|
+
> **After:** "A clickable control that starts an action: saving a form,
|
|
40
|
+
> confirming a choice, opening a dialog. Its emphasis levels signal how
|
|
41
|
+
> important an action is."
|
|
42
|
+
|
|
43
|
+
### `whenToUse` / `whenNotToUse`
|
|
44
|
+
|
|
45
|
+
Situations, never an echo of `summary`. `whenNotToUse` always names the
|
|
46
|
+
alternative.
|
|
47
|
+
|
|
48
|
+
> **Before:** summary "Triggers an action or event." → whenToUse[0] "Trigger
|
|
49
|
+
> an action or event — submit, confirm, open a dialog"
|
|
50
|
+
>
|
|
51
|
+
> **After:** "Something happens on the current page — save, confirm, open a
|
|
52
|
+
> dialog"
|
|
53
|
+
|
|
54
|
+
### `variants` / `states`
|
|
55
|
+
|
|
56
|
+
Lead with meaning; visual treatment is optional and never the whole entry.
|
|
57
|
+
|
|
58
|
+
> **Before:** "Highest-emphasis, solid brand fill — the one primary action in
|
|
59
|
+
> a view." → **After:** "The one main action in a view."
|
|
60
|
+
>
|
|
61
|
+
> **Before:** "Non-interactive and not focusable; reduced opacity." →
|
|
62
|
+
> **After:** "Can't be clicked or tabbed to."
|
|
63
|
+
|
|
64
|
+
### `dos` / `donts`
|
|
65
|
+
|
|
66
|
+
Imperative, one action per entry, ≤ 14 words, full stop. Don'ts open with
|
|
67
|
+
*Don't / Never / Avoid* and name the alternative.
|
|
68
|
+
|
|
69
|
+
> **Before:** "Don't use a button for navigation — use a Link" →
|
|
70
|
+
> **After:** "Don't use a button to navigate. Use a Link."
|
|
71
|
+
|
|
72
|
+
### `accessibility.notes`
|
|
73
|
+
|
|
74
|
+
What the reader must do, not what the framework emits.
|
|
75
|
+
|
|
76
|
+
> **Cut:** "Renders a native `<button>`, so `role=\"button\"` is implicit."
|
|
77
|
+
>
|
|
78
|
+
> **Before:** "An icon-only button needs an aria-label" → **After:** "An
|
|
79
|
+
> icon-only button needs an aria-label so screen readers can announce it."
|
|
80
|
+
|
|
81
|
+
## Vocabulary
|
|
82
|
+
|
|
83
|
+
Technical terms that are the real names of things stay — `aria-label`, `role`,
|
|
84
|
+
`Enter`, `Space` are what a reader would search for. What is banned from
|
|
85
|
+
user-facing prose is the system's own machinery vocabulary: tokens,
|
|
86
|
+
variables, bindings, fingerprints, provenance, projections, surfaces (in the
|
|
87
|
+
machinery sense). `tokensUsed` keeps its token names — it is a structured
|
|
88
|
+
field, machine-useful, and never rendered as prose.
|
|
89
|
+
|
|
90
|
+
**No inline-code markup.** Write `aria-label`, `Enter`, `role` as plain words.
|
|
91
|
+
One record string is projected to three surfaces that render backticks three
|
|
92
|
+
different ways — MDX styles them, Figma's plain-text `description` strips them,
|
|
93
|
+
and the doc card shows the literal character — so the record carries no
|
|
94
|
+
presentation markup at all. Where formatting is wanted, the projection template
|
|
95
|
+
supplies it: the Storybook MDX wraps variant and state keys in backticks itself.
|
|
96
|
+
|
|
97
|
+
## Global rules
|
|
98
|
+
|
|
99
|
+
- Full sentences. No em-dash label-fragments bolting a clause onto a fragment.
|
|
100
|
+
- One set of strings for humans and AI — no dual copy. The digest (`llms.txt`
|
|
101
|
+
/ `index.json`) inherits the same text.
|
|
102
|
+
|
|
103
|
+
## The lint — `scripts/docs-lint.mjs`
|
|
104
|
+
|
|
105
|
+
Checks the mechanically reliable subset of this standard. Zero-dependency,
|
|
106
|
+
**warnings only, always exits 0**.
|
|
107
|
+
|
|
108
|
+
| Rule | Checks | Threshold |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `machinery-vocabulary` | banned-word list in user-facing prose (all prose fields; `tokensUsed`, `name`, `status`, `provenance` exempt) | — |
|
|
111
|
+
| `summary-echo` | `whenToUse[0]` reusing the summary's content words (stopwords removed, naive plural/verb-s stemming) | > 60% overlap |
|
|
112
|
+
| `run-on-sentence` | sentence length | > 35 words |
|
|
113
|
+
| `summary-length` | `summary` length | > 12 words |
|
|
114
|
+
| `description-length` | `description` length | outside 15–70 words |
|
|
115
|
+
| `guidance-length` | `dos` / `donts` entry length | > 14 words |
|
|
116
|
+
| `dont-shape` | a `donts` entry not opening with *Don't / Never / Avoid* | — |
|
|
117
|
+
| `terminal-stop` | a `dos` / `donts` entry not ending with a full stop | — |
|
|
118
|
+
| `treatment-lead` | a variant/state meaning opening with a visual-treatment word ({fill, filled, solid, stroke, border, bordered, outline, shadow, opacity, elevation}) | first 4 words |
|
|
119
|
+
| `empty-meaning` | a variant or state meaning that's too short to say anything | < 3 words |
|
|
120
|
+
| `no-inline-code` | backticks in user-facing prose (the card renders them literally; Figma strips them from `description`) | — |
|
|
121
|
+
|
|
122
|
+
**Output contract:** always exits 0; one warning per line as
|
|
123
|
+
`<file>: <block-path>: <rule>: <message>`; `--json` emits
|
|
124
|
+
`{"warnings": [{path, rule, message}]}`.
|
|
125
|
+
|
|
126
|
+
**Deliberately not linted: verb-presence.** Not reliably detectable in plain
|
|
127
|
+
JS without an NLP dependency, and a rule that fires wrongly is worse than no
|
|
128
|
+
rule. It stays a prose rule, caught by the authoring pipeline and the user
|
|
129
|
+
approval step.
|
|
130
|
+
|
|
131
|
+
## Sequencing
|
|
132
|
+
|
|
133
|
+
1. Draft the record.
|
|
134
|
+
2. Write the file to disk.
|
|
135
|
+
3. `node ${CLAUDE_PLUGIN_ROOT}/scripts/docs-lint.mjs <file>`
|
|
136
|
+
4. Fix warnings.
|
|
137
|
+
5. Show the user.
|
|
138
|
+
|
|
139
|
+
The lint shapes the draft before approval — it does not nag afterward.
|
|
140
|
+
|
|
141
|
+
`imported` / `user` provenance blocks are never silently rewritten. The lint
|
|
142
|
+
still warns on them; the proposed rewrite is carried into the single
|
|
143
|
+
record-approval gate (shown as before/after, labelled with provenance).
|
|
144
|
+
Blocks the user clears are stamped `imported+user`.
|
|
@@ -281,9 +281,28 @@ frames. Applies to `component-builder`, `icon-system-builder`, and
|
|
|
281
281
|
|
|
282
282
|
### Every component sits on its own documentation card
|
|
283
283
|
|
|
284
|
-
Wrap each generated component in a "doc card" — a
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
Wrap each generated component in a "doc card" — a **vertical, three-band
|
|
285
|
+
auto-layout frame**: a **header** band, a **specimen** band (holding the
|
|
286
|
+
component set — the builder's specimen contract is the card's `COMPONENT_SET`,
|
|
287
|
+
not a band name), and a **`Usage`** band holding the documentation body. Never
|
|
288
|
+
leave components floating on bare canvas.
|
|
289
|
+
|
|
290
|
+
**The `Usage` band is never hand-built.** It is rendered by the canonical
|
|
291
|
+
builder snippet in `${CLAUDE_PLUGIN_ROOT}/references/doc-card-builder.md`
|
|
292
|
+
(generated — that file carries the full call contract: the record/fingerprint
|
|
293
|
+
slots, the nine required semantic variables, the `Body/Default` text style, and
|
|
294
|
+
the returned summary you verify and stamp the manifest from). The builder
|
|
295
|
+
computes the card's width from the body type — a column-unit grid whose text
|
|
296
|
+
fills its block, not the card. The column count comes from the record's
|
|
297
|
+
content — the widest row's block count, with a floor of three. It rebuilds
|
|
298
|
+
the `Usage` frame and rewrites the header's short description and date from
|
|
299
|
+
the record; it reads the specimen and never writes it, and the status chip
|
|
300
|
+
keeps its own owner (the finalize write-back below). The header's
|
|
301
|
+
short-description text node is clamped to one column unit wide
|
|
302
|
+
(`summary.columnUnit` from the builder's return), so it never stretches
|
|
303
|
+
across a wide matrix.
|
|
304
|
+
|
|
305
|
+
The header shows:
|
|
287
306
|
|
|
288
307
|
- **Component name** (the deterministic name, matching code).
|
|
289
308
|
- **Short description** (what it is / when to use it).
|
|
@@ -293,9 +312,11 @@ shows:
|
|
|
293
312
|
`components.meta[name].status`. **Name the chip frame `Status` and its label
|
|
294
313
|
text node `Status Label`** so the finalize write-back (below) can find and
|
|
295
314
|
update them later — a chip with no deterministic name can't be promoted.
|
|
296
|
-
- **Last updated** — a date, from `
|
|
297
|
-
|
|
298
|
-
|
|
315
|
+
- **Last updated** — a date, from the doc record's `updatedAt` field
|
|
316
|
+
(`record.updatedAt` — the single source for the header date; not
|
|
317
|
+
`components.meta[name].updatedAt`, which is separate manifest bookkeeping),
|
|
318
|
+
refreshed whenever the component is rebuilt. **Name this text node
|
|
319
|
+
`Last Updated`** for the same reason.
|
|
299
320
|
|
|
300
321
|
**Always separate the header from the component area with a division element.** The
|
|
301
322
|
header block (name, description, status, date) and the component/variant area below
|
|
@@ -341,8 +362,11 @@ re-describing it:
|
|
|
341
362
|
Figma: flip the status chips amber → green and set Last Updated to today.
|
|
342
363
|
Confirm?"* One confirmation covers the whole batch; don't ask per card.
|
|
343
364
|
1. Set `components.meta[name].status` to the new status (`stable` on finalize) and
|
|
344
|
-
`components.meta[name].updatedAt` to today (ISO date)
|
|
345
|
-
|
|
365
|
+
`components.meta[name].updatedAt` to today (ISO date) — this is manifest
|
|
366
|
+
bookkeeping, separate from the header date. Also set `updatedAt` to today in
|
|
367
|
+
the component's `.doc.json` record: `record.updatedAt` is the single source
|
|
368
|
+
the doc card's header renders its date from, so this is the write that
|
|
369
|
+
actually moves the date the user sees.
|
|
346
370
|
2. If Figma is connected (use `figma.mechanism`), locate the component's doc card
|
|
347
371
|
by its deterministic name (script the write-back per
|
|
348
372
|
`${CLAUDE_PLUGIN_ROOT}/references/figma-scripting.md` — `getNodeByIdAsync`, and
|
|
@@ -351,14 +375,20 @@ re-describing it:
|
|
|
351
375
|
- set the `Status Label` text to the new status (e.g. `stable`);
|
|
352
376
|
- re-bind the `Status` chip fill to the matching semantic color variable
|
|
353
377
|
(`stable` → success, `draft`/`beta` → warning, `deprecated` → neutral/danger)
|
|
354
|
-
— re-bind the variable, don't hardcode a hex, so it stays mode-aware
|
|
355
|
-
-
|
|
356
|
-
|
|
357
|
-
|
|
378
|
+
— re-bind the variable, don't hardcode a hex, so it stays mode-aware.
|
|
379
|
+
Then re-run the canonical doc-card builder
|
|
380
|
+
(`${CLAUDE_PLUGIN_ROOT}/references/doc-card-builder.md`) against the same card
|
|
381
|
+
to refresh the header date from the `record.updatedAt` just set in step 1 — the
|
|
382
|
+
builder locates the date node under either header shape (legacy label/value
|
|
383
|
+
frame, or the to-spec `Last Updated` text node) and writes it there, so this
|
|
384
|
+
step never hand-stamps today's date onto a guessed node. Then run the
|
|
385
|
+
visual-validation loop (screenshot → confirm the chip recolored and the date
|
|
386
|
+
changed → re-screenshot).
|
|
358
387
|
3. If Figma is **not** connected, still do step 1, and tell the user the card will
|
|
359
|
-
reconcile
|
|
360
|
-
|
|
361
|
-
|
|
388
|
+
reconcile the next time a Figma session runs — its status chip renders from
|
|
389
|
+
`components.meta[name].status` and its header date from `record.updatedAt`,
|
|
390
|
+
both already updated by step 1. Offer to reconnect and update it now if they
|
|
391
|
+
want it reflected immediately.
|
|
362
392
|
|
|
363
393
|
**Icons are the one exception:** the whole icon set lives on a *single* doc card
|
|
364
394
|
holding the icon grid — one card for all icons, not one card per icon.
|
|
@@ -410,6 +440,10 @@ The doc card is a **vertical, top-to-bottom auto-layout** frame
|
|
|
410
440
|
overlapping text is almost always absolutely-positioned or mis-sized nodes, and
|
|
411
441
|
proper auto layout eliminates it.
|
|
412
442
|
|
|
443
|
+
The `Usage` band's internal layout (rows, wrapping, block widths) is entirely
|
|
444
|
+
the builder's job — these auto-layout rules apply to the header band and any
|
|
445
|
+
hand-built chrome, not to nodes inside `Usage`.
|
|
446
|
+
|
|
413
447
|
### Arrange cards in a parent container (fixes overlapping artboards)
|
|
414
448
|
|
|
415
449
|
Never drop cards onto blank canvas at coordinates that can collide. Place all doc
|
|
@@ -486,6 +520,11 @@ For each generated artboard / doc card / icon grid, read the nodes back (via
|
|
|
486
520
|
3. **Variables bound** — every fill, stroke, text color, corner radius,
|
|
487
521
|
`itemSpacing`, and padding resolves to a **bound variable** (`boundVariables`
|
|
488
522
|
present), not a raw hex/px. No hardcoded values anywhere in the doc-card chrome.
|
|
523
|
+
**Two documented exceptions inside the `Usage` band** (layout chrome, not
|
|
524
|
+
design values, both produced by the builder): the computed column-unit width
|
|
525
|
+
on blocks, and the derived eyebrow type (size/case/tracking/weight). All
|
|
526
|
+
other `Usage` properties — padding, gaps, text colors, dividers — must still
|
|
527
|
+
resolve to bound variables.
|
|
489
528
|
**Check container and component-set background fills specifically** — a paint bind
|
|
490
529
|
that didn't stick renders the placeholder color instead (a pure-black placeholder
|
|
491
530
|
reads as accidental dark mode), so read back `fills[0].boundVariables.color` on
|
|
@@ -522,9 +561,17 @@ For each generated artboard / doc card / icon grid, read the nodes back (via
|
|
|
522
561
|
be retrofitted to the current recipe (see "State handling").
|
|
523
562
|
10. **Visual** — the screenshot (from the validation loop) shows no overlaps,
|
|
524
563
|
misalignment, lopsided hug/fill sizing, or clipped strokes/focus rings.
|
|
564
|
+
11. **Usage band rendered by the builder** — the card's `Usage` frame was
|
|
565
|
+
created by `renderDocCard`
|
|
566
|
+
(`${CLAUDE_PLUGIN_ROOT}/references/doc-card-builder.md`) in this session, and
|
|
567
|
+
the returned summary matches the record: `rowsRendered` and `blocksCreated`
|
|
568
|
+
line up with the record's populated blocks, `cardWidth` is a whole multiple
|
|
569
|
+
of `columnUnit` (minimum 3), and the frame contains the deterministic names
|
|
570
|
+
(`Usage`, `Usage Row 1..3`, `Block: …`, `Doc Fingerprint`). Verify from the
|
|
571
|
+
summary, not a screenshot. A hand-assembled usage body is a fail.
|
|
525
572
|
|
|
526
573
|
If any item fails, **fix and re-audit** — don't hand off a partial pass. Iterate
|
|
527
|
-
with the same ~3-pass budget as the visual loop. Only when all
|
|
574
|
+
with the same ~3-pass budget as the visual loop. Only when all eleven pass is the
|
|
528
575
|
build done.
|
|
529
576
|
|
|
530
577
|
## Naming
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Guide Voice
|
|
2
|
+
|
|
3
|
+
ThroughLine walks someone through building a design system. That framing is the
|
|
4
|
+
whole product. Every skill should sound like a guide standing next to you, not a
|
|
5
|
+
tool printing status. This file is the source of truth for that voice, and
|
|
6
|
+
specifically for how a flow **closes** — the moment a skill finishes a unit of
|
|
7
|
+
work and hands back to the user.
|
|
8
|
+
|
|
9
|
+
Skills that end a flow point here. Do not restate the rules inline; reference this
|
|
10
|
+
file and follow it.
|
|
11
|
+
|
|
12
|
+
## The principle
|
|
13
|
+
|
|
14
|
+
A guide has a point of view and shares it. A config wizard lays out options and
|
|
15
|
+
makes you choose. The difference the user feels is whether they leave a step with
|
|
16
|
+
a clear next move or with homework.
|
|
17
|
+
|
|
18
|
+
The failure this corrects: closing a flow with a flat menu of unlike options, each
|
|
19
|
+
ranked for the user to adjudicate, with no recommendation. It reads as cold and it
|
|
20
|
+
pushes the thinking back onto the person you were supposed to be guiding.
|
|
21
|
+
|
|
22
|
+
## The flow-close pattern
|
|
23
|
+
|
|
24
|
+
When a skill finishes, close in four beats, in prose. Not a table. Not an option
|
|
25
|
+
card. Prose.
|
|
26
|
+
|
|
27
|
+
**1. Outcome.** One line. What got done, and whether it is in sync.
|
|
28
|
+
|
|
29
|
+
> "Button is documented. The canonical record is written, and the Figma
|
|
30
|
+
> description and doc card are both projected and in sync."
|
|
31
|
+
|
|
32
|
+
**2. What I set aside, and why.** Name any gap you deferred, in a clause, with the
|
|
33
|
+
reason. Read existing state to know what is actually outstanding (for the docs
|
|
34
|
+
flow, a component at `status: "draft"` with no code surface in
|
|
35
|
+
`meta[name].doc.surfaces` means the code side is not built yet). Give the reason
|
|
36
|
+
so the deferral reads as a decision, not an omission.
|
|
37
|
+
|
|
38
|
+
> "I left the code-side render for later on purpose. It is a one-time,
|
|
39
|
+
> system-wide scaffold that every future component rides, so it is worth doing
|
|
40
|
+
> deliberately as its own step, not half-built mid-flow."
|
|
41
|
+
|
|
42
|
+
**3. My recommendation.** A short, sequenced, reasoned plan. State it as the
|
|
43
|
+
default. End on one clear next action the user can say yes to.
|
|
44
|
+
|
|
45
|
+
> "My take: document Input and Card next to pressure-test the model on
|
|
46
|
+
> higher-surface-area components, then stand up the code side once, against all
|
|
47
|
+
> three at once. Want me to start on Input?"
|
|
48
|
+
|
|
49
|
+
**4. Or.** At most one genuinely comparable alternative, demoted to a light aside.
|
|
50
|
+
If there is not a real alternative, leave this out entirely. Do not manufacture
|
|
51
|
+
one for symmetry.
|
|
52
|
+
|
|
53
|
+
> "(If you would rather lay the code rails first so everything after Button rides
|
|
54
|
+
> them, say the word and I will do that instead.)"
|
|
55
|
+
|
|
56
|
+
## The hard rule
|
|
57
|
+
|
|
58
|
+
**Never present a decision point as a grid of co-equal options.** At the close of
|
|
59
|
+
a flow, an `AskUserQuestion`-style option menu is the anti-pattern itself. The
|
|
60
|
+
whole point is that you have a recommendation. Lead with it.
|
|
61
|
+
|
|
62
|
+
Use a real option list only when the choices are genuinely comparable, mutually
|
|
63
|
+
exclusive, and you honestly have no basis to recommend one. That is rare at a flow
|
|
64
|
+
close. It is common mid-flow (which component to build, which framework), and
|
|
65
|
+
there an option list is fine.
|
|
66
|
+
|
|
67
|
+
## What good looks like, versus what to avoid
|
|
68
|
+
|
|
69
|
+
Avoid:
|
|
70
|
+
|
|
71
|
+
> "Two honest notes. The JSDoc surface is untouched. Nothing is committed. Want me
|
|
72
|
+
> to open a PR, or keep going and document the next batch?"
|
|
73
|
+
|
|
74
|
+
Two unlike options, no recommendation, a respected boundary dressed up as a
|
|
75
|
+
decision. That is the config wizard.
|
|
76
|
+
|
|
77
|
+
Better:
|
|
78
|
+
|
|
79
|
+
> "The JSDoc call was right. Leaving hand-written imported docs untouched is the
|
|
80
|
+
> record model working as designed, not a gap. My take: open the PR now, before
|
|
81
|
+
> the next batch. It is a clean, self-contained unit, and opening it is what
|
|
82
|
+
> actually exercises the docs:check CI gate end-to-end. So: branch, PR, let CI
|
|
83
|
+
> prove the gate, then document the next batch on landed foundations. (If you want
|
|
84
|
+
> to keep momentum, batching the next few first is defensible, you are just
|
|
85
|
+
> trading review clarity for it.)"
|
|
86
|
+
|
|
87
|
+
Same facts. One affirms the boundary instead of dressing it as a choice, collapses
|
|
88
|
+
the fork to one recommended path with the reason, and demotes the alternative to
|
|
89
|
+
an aside.
|
|
90
|
+
|
|
91
|
+
## Adoption
|
|
92
|
+
|
|
93
|
+
`/document-component` is the first flow to adopt this. Every other flow picks it up
|
|
94
|
+
when it is next touched, not in a speculative sweep. Persisting *why* a stage was
|
|
95
|
+
allowed to advance as structured state is a separate, larger piece (the
|
|
96
|
+
verification proof bundle) and is out of scope here.
|
|
@@ -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 6)
|
|
15
15
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
|
-
"schemaVersion":
|
|
18
|
+
"schemaVersion": 6,
|
|
19
19
|
"user": {
|
|
20
20
|
"codingLevel": "new"
|
|
21
21
|
},
|
|
@@ -42,7 +42,8 @@ bailing or running silently.
|
|
|
42
42
|
"coverPageBuilt": false,
|
|
43
43
|
"canPublish": null,
|
|
44
44
|
"libraryPublished": false,
|
|
45
|
-
"publishedAt": null
|
|
45
|
+
"publishedAt": null,
|
|
46
|
+
"docCardVariables": null
|
|
46
47
|
},
|
|
47
48
|
"tokens": {
|
|
48
49
|
"intakeMode": null,
|
|
@@ -189,6 +190,16 @@ bailing or running silently.
|
|
|
189
190
|
discipline in `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`.**
|
|
190
191
|
- `publishedAt` — ISO timestamp the user last confirmed a publish, for "you may
|
|
191
192
|
need to re-publish after adding components" messaging.
|
|
193
|
+
- `docCardVariables` — object mapping the doc-card builder's nine semantic
|
|
194
|
+
variable roles to the variable name each resolves to in this project's file:
|
|
195
|
+
`{ textDefault, textMuted, tonePositive, toneNegative, border, spacePadding,
|
|
196
|
+
spaceRowGap, spaceBlockGap, spaceItemGap }`. `null` until set. Written once,
|
|
197
|
+
on the first doc-card render in a project, and read by every render after
|
|
198
|
+
(see `${CLAUDE_PLUGIN_ROOT}/references/doc-card-builder.md`). Exists because
|
|
199
|
+
the roles are project-agnostic but the variable names are not — an
|
|
200
|
+
unrecorded mapping drifts between renders (different roles resolved to
|
|
201
|
+
different variables each time) and leaves cards visually inconsistent. E.g.
|
|
202
|
+
`{ "textDefault": "color/text/primary", "textMuted": "color/text/secondary", "tonePositive": "color/success/default", "toneNegative": "color/danger/default", "border": "color/border/default", "spacePadding": "space/inset/lg", "spaceRowGap": "space/gap/xl", "spaceBlockGap": "space/gap/lg", "spaceItemGap": "space/gap/sm" }`.
|
|
192
203
|
|
|
193
204
|
### `tokens`
|
|
194
205
|
- `intakeMode` — how the user started: `"generative"` (seed expanded by AI),
|
|
@@ -253,10 +264,13 @@ bailing or running silently.
|
|
|
253
264
|
- `meta[name].doc` — documentation pointer + per-surface fingerprints for the
|
|
254
265
|
component (v1: components only). **Pointers and hashes, never content** — the
|
|
255
266
|
content lives in `design-system/docs/components/<name>.doc.json`. Shape:
|
|
256
|
-
`{ path, fingerprint, surfaces: { <surfaceName>: { src, render, file? } } }`,
|
|
267
|
+
`{ path, fingerprint, surfaces: { <surfaceName>: { src, render, file?, renderer? } } }`,
|
|
257
268
|
where `fingerprint` is the canonical fingerprint at last render, `src` is the
|
|
258
269
|
canonical fingerprint a surface was rendered from (detects stale), `render` is a
|
|
259
270
|
hash of the surface's rendered content (detects edits, for re-readable surfaces),
|
|
271
|
+
`renderer` (docCard only) is the layout version of the builder that last
|
|
272
|
+
rendered the card (`DOC_CARD_RENDERER_VERSION`); missing/lower is reported by
|
|
273
|
+
`docs:check` as the informational `layout-upgrade-available`, never as drift,
|
|
260
274
|
and `file` is the repo-relative path of a code surface. Written by
|
|
261
275
|
`component-builder` (Figma + card surfaces) and `storybook-chromatic-builder`
|
|
262
276
|
(code surfaces); read by the `docs:check` gate. See
|
|
@@ -361,3 +375,7 @@ retrofit stands so a later session can resume.
|
|
|
361
375
|
**v4 → v5 migration:** add `audit.docSurface` (default `null`) and the `docs`
|
|
362
376
|
retrofit phase; `components.meta[name].doc` is added lazily per component as docs
|
|
363
377
|
are authored. Bump `schemaVersion` to `5`. No existing field changes.
|
|
378
|
+
|
|
379
|
+
**v5 → v6 migration:** add `figma.docCardVariables` (default `null`), populated
|
|
380
|
+
on the first doc-card render. Bump `schemaVersion` to `6`. No existing field
|
|
381
|
+
changes.
|
package/scripts/README.md
CHANGED
|
@@ -14,6 +14,29 @@ tested here; copied verbatim by `token-crosswalk-builder` into the user's
|
|
|
14
14
|
| `crosswalk.schema.json` | The finalized JSON Schema for `crosswalk.json` (contract + editor support). | copied beside `crosswalk.json` |
|
|
15
15
|
| `build-docs-digest.mjs` | Aggregate every `design-system/docs/components/*.doc.json` into `design-system/docs/index.json` + `llms.txt` for AI/human consumers. | `docs:digest` |
|
|
16
16
|
| `docs-check.mjs` | Drift gate — verifies each component's doc surfaces still match its canonical record (via `lib/doc-record.mjs` fingerprints). Exits 1 on drift. | `docs:check` |
|
|
17
|
+
| `docs-lint.mjs` | Copy lint for .doc.json records — warnings only, always exits 0 on a parseable record; the mechanical subset of `references/doc-writing-standard.md`. | `docs:lint` |
|
|
18
|
+
| `lib/doc-record.mjs` | Canonical record load + `canonicalFingerprint` (sha256 over the record minus `provenance`). The fingerprint every surface is stamped with. | copied alongside docs-check.mjs |
|
|
19
|
+
| `lib/doc-card-render.figma.js` | Figma renderer template for the doc card's `Usage` band and header. Inlined into `references/doc-card-builder.md`; never executed as a module. | plugin-internal (not installed) |
|
|
20
|
+
| `lib/doc-card-plan.mjs` | Pure layout planner for the doc card's `Usage` band + `DOC_CARD_RENDERER_VERSION` (single source of the layout version). Inlined into `references/doc-card-builder.md`; imported by `docs-check.mjs`. | copied alongside docs-check.mjs; also inlined into the generated builder |
|
|
21
|
+
| `build-doc-card-builder.mjs` | Generate `references/doc-card-builder.md` from the planner + the Figma renderer template (`lib/doc-card-render.figma.js`). `--check` gates CI. | plugin-internal (not installed) |
|
|
22
|
+
|
|
23
|
+
**Documentation scripts — install as a set.** Copying these files without
|
|
24
|
+
registering them leaves a repo with a script on disk and no entry point, which
|
|
25
|
+
is how a stale `docs:check` went unnoticed for a full release. Both
|
|
26
|
+
`storybook-chromatic-builder` (first-time setup) and `/document-component`
|
|
27
|
+
(freshness refresh) install the same five files and register the same three
|
|
28
|
+
scripts:
|
|
29
|
+
|
|
30
|
+
| File | npm script |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `build-docs-digest.mjs` | `"docs:digest": "node scripts/build-docs-digest.mjs"` |
|
|
33
|
+
| `docs-check.mjs` | `"docs:check": "node scripts/docs-check.mjs"` |
|
|
34
|
+
| `docs-lint.mjs` | `"docs:lint": "node scripts/docs-lint.mjs"` |
|
|
35
|
+
| `lib/doc-record.mjs` | — (imported by the above) |
|
|
36
|
+
| `lib/doc-card-plan.mjs` | — (imported by the above) |
|
|
37
|
+
|
|
38
|
+
A refresh that adds a file must also add its npm script; check `package.json`
|
|
39
|
+
for all three every time, not just the file that changed.
|
|
17
40
|
|
|
18
41
|
The crosswalk contract is documented in
|
|
19
42
|
`${CLAUDE_PLUGIN_ROOT}/references/crosswalk-schema.md`.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Generates references/doc-card-builder.md — the canonical figma_execute
|
|
2
|
+
// snippet that renders a doc card's Usage band — by inlining the pure planner
|
|
3
|
+
// (lib/doc-card-plan.mjs) above the Figma renderer template
|
|
4
|
+
// (lib/doc-card-render.figma.js). Mirrors the adapters generate.mjs idiom:
|
|
5
|
+
// run bare to write, run with --check to gate CI. Zero dependencies.
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const PLANNER = join(REPO_ROOT, 'scripts', 'lib', 'doc-card-plan.mjs');
|
|
12
|
+
const RENDERER = join(REPO_ROOT, 'scripts', 'lib', 'doc-card-render.figma.js');
|
|
13
|
+
const OUT = join(REPO_ROOT, 'references', 'doc-card-builder.md');
|
|
14
|
+
|
|
15
|
+
const HEADER = [
|
|
16
|
+
'# Doc-card Usage-band builder (GENERATED)',
|
|
17
|
+
'',
|
|
18
|
+
'> **GENERATED FILE — do not edit by hand.** Sources: `scripts/lib/doc-card-plan.mjs`',
|
|
19
|
+
'> (the pure planner, unit-tested in Node) + `scripts/lib/doc-card-render.figma.js`',
|
|
20
|
+
'> (the Figma renderer). Regenerate with `node scripts/build-doc-card-builder.mjs`;',
|
|
21
|
+
'> CI gates freshness with `--check`.',
|
|
22
|
+
'',
|
|
23
|
+
'The canonical `figma_execute` snippet that renders a component doc card\'s',
|
|
24
|
+
'`Usage` band from its `.doc.json` record. Every card is identical by',
|
|
25
|
+
'construction — never hand-build the usage body. The builder owns the `Usage`',
|
|
26
|
+
'band and the header\'s record-derived content (its short description and date);',
|
|
27
|
+
'it reads the specimen and never writes it. The status chip keeps its own owner',
|
|
28
|
+
'— the finalize write-back in `references/figma-component-standards.md`.',
|
|
29
|
+
'',
|
|
30
|
+
'## How to call it',
|
|
31
|
+
'',
|
|
32
|
+
'1. Load the record and compute its canonical fingerprint in Node',
|
|
33
|
+
' (`canonicalFingerprint` in `scripts/lib/doc-record.mjs`).',
|
|
34
|
+
'2. Read `figma.docCardVariables` from `design-system.json`.',
|
|
35
|
+
' - If present, resolve each of the nine roles to a Variable object **by',
|
|
36
|
+
' the recorded name** — do not re-derive, do not substitute a similar',
|
|
37
|
+
' name. Look each name up via `figma_get_variables`, then in the script',
|
|
38
|
+
' fetch it as a Variable object with',
|
|
39
|
+
' `figma.variables.getVariableByIdAsync(id)`. If a recorded name no',
|
|
40
|
+
' longer resolves to exactly one variable in the file, **throw** rather',
|
|
41
|
+
' than guess — the token was renamed or removed, and silently picking a',
|
|
42
|
+
' neighbour is how cards drift apart.',
|
|
43
|
+
' - If the field is absent (a project\'s first doc-card render, or any',
|
|
44
|
+
' render after the field is cleared), do not resolve fresh by judgement',
|
|
45
|
+
' yet — first check whether a doc card already exists in the file. If',
|
|
46
|
+
' one does, recover all nine roles from it by resolving each bound',
|
|
47
|
+
' variable id back to its name (`figma.variables.getVariableByIdAsync(id)`):',
|
|
48
|
+
' - `spacePadding` ← the `Usage` frame\'s `paddingLeft`.',
|
|
49
|
+
' - `spaceRowGap` ← the `Usage` frame\'s `itemSpacing`.',
|
|
50
|
+
' - `spaceBlockGap` ← a `Usage Row *` frame\'s `itemSpacing`.',
|
|
51
|
+
' - `spaceItemGap` ← a `Block: *` frame\'s `itemSpacing` (blocks are the',
|
|
52
|
+
' children of a `Usage Row *`).',
|
|
53
|
+
' - `border` ← a `Row Divider` frame\'s',
|
|
54
|
+
' `fills[0].boundVariables.color`.',
|
|
55
|
+
' - `tonePositive` ← the first TEXT child of the `Block: Do` frame\'s',
|
|
56
|
+
' `fills[0].boundVariables.color`.',
|
|
57
|
+
' - `toneNegative` ← the first TEXT child of the `Block: Don\'t` frame,',
|
|
58
|
+
' same property.',
|
|
59
|
+
' - `textMuted` ← the first TEXT child of any block other than',
|
|
60
|
+
' `Block: Do` / `Block: Don\'t`, same property (tone blocks colour',
|
|
61
|
+
' their eyebrow differently, so exclude them here).',
|
|
62
|
+
' - `textDefault` ← the second child of that same block when it is a',
|
|
63
|
+
' TEXT node — `Block: Overview` is reliable; definition blocks nest',
|
|
64
|
+
' frames there instead, so skip those. Same property.',
|
|
65
|
+
' A single-row card has no `Row Divider` (no `border`); a card without',
|
|
66
|
+
' `Block: Do` / `Block: Don\'t` yields no `tonePositive` / `toneNegative`.',
|
|
67
|
+
' Read another rendered card for the roles that specific card can\'t',
|
|
68
|
+
' yield, or fall back to judgement for just those. Only when no',
|
|
69
|
+
' rendered card exists at all does the caller choose every role by',
|
|
70
|
+
' judgement — establishing the project\'s rhythm, not guessing at one.',
|
|
71
|
+
' Either way, resolve the nine roles once, **write the mapping back to',
|
|
72
|
+
' `design-system.json`** as `figma.docCardVariables`, then render. Every',
|
|
73
|
+
' later render reads it.',
|
|
74
|
+
' The nine roles: `textDefault`, `textMuted` (text colors), `tonePositive`,',
|
|
75
|
+
' `toneNegative` (Do/Don\'t eyebrow colors — success/danger roles), `border`',
|
|
76
|
+
' (row dividers), `spacePadding`, `spaceRowGap`, `spaceBlockGap`,',
|
|
77
|
+
' `spaceItemGap` (spacing roles: band padding, row gap, block gutter,',
|
|
78
|
+
' within-block gap).',
|
|
79
|
+
'3. Find the body text style: `(await figma.getLocalTextStylesAsync())',
|
|
80
|
+
' .find((s) => s.name === \'Body/Default\')`. Missing variables or style =',
|
|
81
|
+
' the builder throws (bind-or-throw — the gap is in the token set; fix it',
|
|
82
|
+
' there, never hardcode around it).',
|
|
83
|
+
'4. Prepend the two slots, then the snippet below, then the call:',
|
|
84
|
+
'',
|
|
85
|
+
'```js',
|
|
86
|
+
'const RECORD = /* the parsed .doc.json object */;',
|
|
87
|
+
'const CANONICAL_FP = \'/* canonicalFingerprint(RECORD), 16 hex chars */\';',
|
|
88
|
+
'// … the generated snippet …',
|
|
89
|
+
'const card = await figma.getNodeByIdAsync(cardNodeId);',
|
|
90
|
+
'const summary = await renderDocCard({ card, record: RECORD, vars, bodyTextStyle });',
|
|
91
|
+
'```',
|
|
92
|
+
'',
|
|
93
|
+
'5. Pass an explicit `timeout` (30000 is right for one card; the ~30s',
|
|
94
|
+
' `figma_execute` ceiling fits a single card comfortably — render cards one',
|
|
95
|
+
' call at a time, never batched).',
|
|
96
|
+
'6. Verify from the returned summary — `rowsRendered`, `blocksCreated`,',
|
|
97
|
+
' `cardWidth` — not from a screenshot, then stamp the manifest from it:',
|
|
98
|
+
' `surfaces.docCard = { src: summary.fingerprint, render: summary.renderHash,',
|
|
99
|
+
' renderer: summary.rendererVersion }`. Never re-read the card to stamp.',
|
|
100
|
+
'',
|
|
101
|
+
'## The snippet',
|
|
102
|
+
'',
|
|
103
|
+
'```js',
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
const FOOTER = [
|
|
107
|
+
'```',
|
|
108
|
+
'',
|
|
109
|
+
'Layout contract and rationale:',
|
|
110
|
+
'`docs/superpowers/specs/2026-08-09-doc-card-layout-and-voice-design.md`.',
|
|
111
|
+
'',
|
|
112
|
+
].join('\n');
|
|
113
|
+
|
|
114
|
+
export function buildDocCardBuilder({ plannerSource, rendererSource }) {
|
|
115
|
+
const inlined = plannerSource
|
|
116
|
+
.replace(/^export const /gm, 'const ')
|
|
117
|
+
.replace(/^export function /gm, 'function ');
|
|
118
|
+
for (const [name, src] of [['doc-card-plan.mjs', inlined], ['doc-card-render.figma.js', rendererSource]]) {
|
|
119
|
+
if (/^\s*(import|export)\b/m.test(src)) {
|
|
120
|
+
throw new Error(`${name} must stay import-free (only top-level \`export const\`/\`export function\` allowed in the planner) — it is inlined into the Figma snippet where no module system exists`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return `${HEADER}\n${inlined}\n${rendererSource}${FOOTER}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
127
|
+
const result = buildDocCardBuilder({
|
|
128
|
+
plannerSource: readFileSync(PLANNER, 'utf8'),
|
|
129
|
+
rendererSource: readFileSync(RENDERER, 'utf8'),
|
|
130
|
+
});
|
|
131
|
+
if (process.argv.includes('--check')) {
|
|
132
|
+
let onDisk = null;
|
|
133
|
+
try { onDisk = readFileSync(OUT, 'utf8'); } catch (e) { /* missing counts as drift */ }
|
|
134
|
+
if (onDisk !== result) {
|
|
135
|
+
console.error('✗ references/doc-card-builder.md out of date; run: node scripts/build-doc-card-builder.mjs');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
console.log('✓ doc-card builder in sync');
|
|
139
|
+
} else {
|
|
140
|
+
writeFileSync(OUT, result);
|
|
141
|
+
console.log('✓ wrote references/doc-card-builder.md');
|
|
142
|
+
}
|
|
143
|
+
}
|