Storybook source shape
Storybook is the fidelity oracle, not the runtime. The converter bundles the package's compiled dist/ into _ds_bundle.js — the same bundle the claude.ai/design agent builds with — and generates each preview by compiling the story source module itself (hooks, fixtures, local helpers — the whole closure comes along), with every component import resolved to that shipped bundle (lib/story-imports.mjs redirects package and relative component imports to window.<Global>). The repo's own storybook render is the ground truth those previews must match: a compare harness screenshots each story in the reference storybook and the matching preview render side by side, and you iterate until they match. Nothing from storybook-static is uploaded, and no story code is ever evaluated at build time — stories run only in the browser, against the real artifact.
Requires React 18+. Playwright + chromium are required for this shape (the compare loop is the verification), not optional.
First sync or re-sync? A re-sync is marked by a config whose projectId and pkg were both in place before this run started — most of this document then doesn't apply; go to §7, where one driver run routes the work and untouched components cost nothing. Everything else takes the full flow (§2 build → §3 self-heal → §4 match → conventions header (base SKILL.md, before upload) → §6 upload), where every component gets verified and graded once — that includes a partial config left by an aborted run, and a pin this run itself just recorded in the base skill's §1. (Only the old design-sync.config.json present? Move it first and commit: mkdir -p .design-sync && mv -n design-sync.config.json .design-sync/config.json, then apply the same test.)
2. Build, then run the converter
Build the DS package and its workspace dependencies. The converter bundles dist/ into window.<Global>. Run <pm> run build; in a monorepo use turbo run build --filter=<pkg> or pnpm -F "<pkg>..." build (the trailing ... is required — bare -F <pkg> skips dependencies and you'll see Cannot find module '@scope/tokens'). If package.json module/exports['.'] points at TS source, find the actual built entry and pass it via --entry. Do this before step 2 — storybook often imports sibling packages from their built dist/.
Build the reference storybook ONCE into .design-sync/sb-reference/ — NOT under ds-bundle/ (the converter wipes --out on every rebuild, and storybook builds take minutes; the reference must survive the fix loop):
npx storybook build -c <storybookConfigDir> -o .design-sync/sb-reference
Run it from the directory whose package.json has the storybook devDependencies — usually the one containing .storybook/; monorepos often have several storybooks, so pick the one covering the package you're syncing. Make -o the repo-root path (e.g. -o "$(git rev-parse --show-toplevel)/.design-sync/sb-reference"): the converter and compare resolve .design-sync/ from the repo root, so a cwd-relative -o in a subpackage puts the reference where nothing will find it. Use npx storybook build directly, not the repo's npm run build-storybook script (wrong output dir). Then check .design-sync/sb-reference/iframe.html exists and is >10KB — index.json alone can exist with a failed build.
Long builds: background them through your shell tool's background mode only and wait for the completion notification. Never a bare & (untracked — the notification never comes), and never a pgrep -f '<script>' poll loop (it matches its own command line and spins to timeout). Headless / -p sessions: run long commands synchronously instead — there is no task-notification re-invocation there, so a backgrounded run is never resumed.
.gitignore additions: .design-sync/sb-reference/, .design-sync/learnings/, .design-sync/.cache/, .design-sync/node_modules (fork symlink — recreated per clone), .ds-sync/, ds-bundle/ — build artifact, transient scratch, verification working state, the symlink, staged scripts, regenerated output. Committed: the durable set (the rule in non-storybook §2, same here: everything under .design-sync/ not gitignored — previews/ holds your authored files ONLY; generated story-module wrappers live in .design-sync/.cache/previews/ and regenerate every build; the converter never writes or deletes anything in previews/). Verification state is never committed — cross-machine carry-forward comes from the uploaded project's _ds_sync.json. Rebuild the reference only when stories or the DS source change.
Write .design-sync/config.json — only pkg and globalName required. If it already exists, read it first and keep what's there — titleMap, overrides, and provider accumulate fixes from prior syncs. Also Read .design-sync/NOTES.md first — its Re-sync risks section is the prior run's watch-list; re-verify those items instead of assuming carry-forward covers them. The package-shape field table in ../non-storybook/SKILL.md §2.6 applies verbatim; the fields that matter most here:
| Field |
Value |
pkg / globalName |
pkg required; globalName auto-derived from it when omitted |
shape |
"storybook" — pins detection |
storybookStatic |
".design-sync/sb-reference" — so re-syncs and compare find the reference without flags |
storybookConfigDir |
the .storybook/ dir (monorepos) |
buildCmd |
what to re-run before the converter on re-sync |
titleMap |
{title: ExportName} when story titles don't match export names; {title: null} excludes a non-visual/internal component from the sync entirely |
overrides |
{<Name>: {skip: [storyIds], cardMode: "single"|"column", primaryStory: "<Export>", viewport: "WxH"}} — skip for stories that can't render statically; cardMode: "single" for overlay components (§4a.5, §5), "column" for stories wider than a grid cell (the [GRID_OVERFLOW] row in §3) |
provider |
usually unnecessary for previews — .storybook/preview decorators are auto-bundled; set only when that fails. Before §6 upload, distill decorator-provided context into cfg.provider — README/prompt.md wrap guidance is generated from config only (decorator-only wrapping ships a generic note). Setting it also replaces the decorators as the preview wrapper on the next build: scoped-compare a themed component after the switch — an incomplete distillation regresses previews the decorators rendered fine, and carried-forward grades won't catch it. Format: {"component": "ThemeProvider", "props": {…}, "inner": {…}} — a nested chain, outermost first; each component must be a bundle export. Literal props are for small scalars ("theme": "light") and stable snippets. For data that already exists in the repo — a locale JSON, a theme object — prefer {"$ref": "<export>"} backed by a 2-line module added via cfg.extraEntries (e.g. export { default as previewI18n } from '../locales/en.json'): a $ref emits window.<Global>.<export>, so the data lives once in the bundle and re-reads from its source file on every build. Inlining a copy is acceptable for something tiny and stable, but know the cost — a literal duplicates into every card's html and silently rots when the source file changes, so anything sizable or evolving belongs behind a $ref. Path forms for extraEntries: a bare name resolves from node_modules; a repo-owned module needs an explicit .//../ package-relative path (workspace-bounded — the build logs ! extraEntries: … skipped if it escapes). |
Stage scripts + install converter deps (isolated in .ds-sync/, repo lockfile untouched):
mkdir -p .ds-sync && cp -r "<skill-base-dir>"/package-build.mjs "<skill-base-dir>"/package-validate.mjs "<skill-base-dir>"/resync.mjs "<skill-base-dir>"/lib "<skill-base-dir>"/storybook "<skill-base-dir>"/non-storybook .ds-sync/
echo '{"name":"ds-sync-deps","private":true}' > .ds-sync/package.json
(cd .ds-sync && npm i esbuild ts-morph @types/react playwright && npx playwright install chromium)
If chromium install fails, npx playwright install-deps chromium first; if the environment can't install chromium, set DS_CHROMIUM_PATH=<system-chromium>.
Run the converter, validator, and compare — synchronously, stopping at the first non-zero exit (compare only runs once build + validate are clean — §3). Large DSes (≈100+ components) may need NODE_OPTIONS=--max-old-space-size=<MB> for the build; never pipe the build through head/tail (the pipeline masks the exit code — an OOM looks like success); redirect to a file and read it:
node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules <pkg-node-modules> \
--entry <built-dist-entry> --out ./ds-bundle
node .ds-sync/package-validate.mjs ./ds-bundle
node .ds-sync/storybook/compare.mjs --out ./ds-bundle --storybook-static .design-sync/sb-reference \
--components <solo-phase picks> # scope the FIRST compare to the §4b solo components
In a monorepo, --node-modules is the DS package's own node_modules — unless hoisting leaves it sparse (yarn's node-modules linker keeps react only at the repo root): if react/ or react-dom/ is missing inside, pass the repo-root node_modules instead. In the DS's own source repo node_modules/<pkg> doesn't exist, hence --entry. The build logs [ICON_PKG] / [TOKENS_PKG] auto-detections and bundles .storybook/preview decorators as the preview wrapper (preview-decorators.js) so previews get the same provider chain stories do.
Scope the first compare run: a full capture of a large DS is thousands of chromium navigations — pointless before the solo phase has flushed global issues (each global fix invalidates every capture). The first roster-wide run happens per §4b step 3 — and on a DS over 20 storied components even that is size-gated into §4c's scoped batches, so the only mandatory full-roster run is the §4d receipt, which carries graded work forward instead of recapturing it. For a DS with >100 storied components, also tell the user the expected scale (components × stories) before fan-out and let them narrow scope if they want.
3. Self-heal loop (build + validate)
Fix [TAG] errors → rebuild → re-validate until both exit 0, before starting the compare loop in §4 — there's no point pixel-matching previews while the bundle itself is broken. Shared converter tags ([NO_DIST], [WORKSPACE_SIBLING], [CSS_*], [FONT_*], [TOKENS_MISSING], [DTS_*], [RENDER*], …) behave identically to the package shape — use the table in ../non-storybook/SKILL.md §3. Lines printed as hypothesis: under an error are leads, not instructions: run their verify step first, and if it doesn't confirm, drop the hypothesis and diagnose from the error text itself. Storybook-specific:
| Tag |
Symptom |
Fix |
[SB_REFERENCE_MISSING] |
compare can't find iframe.html |
Build the reference (§2.2); set cfg.storybookStatic. |
[SB_BUILD_FAIL] |
converter's own storybook build failed |
You skipped §2.2 — build the reference yourself and set cfg.storybookStatic so the converter never needs to. |
[ZERO_MATCH] (storybook flavor) |
no story entries matched |
Check the storybook config's stories glob; then titleMap. |
[TITLE_UNMAPPED] |
N titles don't match an export |
cfg.titleMap {<title-name>: <export-name>}. |
(preview: <Name> — no story exports paired …) |
index story names couldn't be matched to module export keys (pairing tries the display name, then the story ID's tail) |
the component shows the floor card; fix the pairing — usually an owned .tsx re-exporting the stories under matchable names. |
a preview cell errors with undefined-component / wrong-context messages |
a story import resolved the wrong way — relative, tsconfig-alias, and bare-workspace imports all go through the same policy (see lib/story-imports.mjs's rules) |
cfg.storyImports.shim / cfg.storyImports.bundle substring patterns force the resolution per resolved path — the cheap fix before forking the seam. |
! preview build failed: <Name> |
the story module didn't COMPILE (top-level await, an import of a package esbuild can't resolve, an asset extension with no loader) |
read the esbuild error above the line. Unknown asset extension → cfg.storyImports.loaders (merged over the defaults, e.g. {".yaml": "text"}); unresolvable import → own the .tsx and drop it. The component shows the floor card until fixed. |
| a story's own stylesheet is missing from its cell |
story-local .css/.scss side-effect imports compile as empty (component styles ship via the bundle css). Exception: .module.css IS compiled — classes resolve and _preview/<Name>.css is linked automatically |
usually nothing — the styles are decoration the storybook page adds. If the story genuinely depends on them, inline the styles in an owned .tsx. |
[BUNDLE_EXPORT] |
components aren't functions on window.<Global> |
extraEntries for subpath/icon exports; check the dist entry is the full build. |
[SCHEDULER_MISSING] |
dist imports scheduler |
react-dom leaked into the DS dist — check its build's externals. |
! preview decorator bundle failed |
decorators couldn't be bundled |
Set cfg.provider manually, or run node .ds-sync/storybook/probe.mjs --storybook-static .design-sync/sb-reference to infer the chain from the live storybook (replace each $hint with a real value). |
previews error at _vendor/preview-decorators.js load (storybook-API undefined errors) |
the .storybook/preview import graph reached a storybook-runtime module the stubs don't cover |
manager-api/preview-api are stubbed with functional no-op hooks and every other @storybook/*/msw module with inert callables (fn(), action(), setupWorker() at module scope all evaluate harmlessly); if some other API still crashes, set cfg.provider explicitly — it skips decorator bundling entirely. |
[ASSETS_BLOCKED] from compare |
the capture browser inherited a network-sandboxed shell — story assets (CDN images/fonts) failed on both panels, so grades can falsely pass while end users see different output |
re-run package-validate.mjs + compare.mjs --force from a shell with egress to the listed hosts: approve running the command without the sandbox when prompted, or add the hosts to the sandbox allowlist. Don't grade image-bearing components while this prints. |
Incremental path (base SKILL.md §3) — this is the open-the-channel gate. The first time build + validate both exit 0, open the upload channel before starting §4: the user approves once here, then watches components land as grading proceeds. Nothing uploads until the first graded batch — the shared base files ride with it — and the batch pushes come from §4b/§4c. (Atomic path: nothing uploads until §6.)
4. Match previews to storybook
compare.mjs is a capture harness — it photographs, you grade. It computes no similarity heuristics (pixel/text/font scores mislead whenever framing legitimately differs); the judgment is made from the two true screenshots. Compiled previews capture per story — each story renders alone via ?story=<Export> at the full capture viewport, exactly as storybook frames the reference side — so sibling stories can't interfere (portal stacking, shared radio-group names, focus, container measurement). Two output tiers:
- Transient (under
ds-bundle/, wiped by rebuilds): _screenshots/compare/<group>__<Name>.png — sheet with one row per story: the true storybook render | the true preview render, side by side. Sheet images are shrunk to fit; the full-resolution originals are in …/compare/raw/ (…__sb.png / …__ds.png) — Read those when the sheet is too small to judge confidently.
- Campaign state (in
.design-sync/.cache/compare/, gitignored): <Name>.grade.json — your verdicts — and <Name>.json — capture facts: story↔cell pairing, shot paths, previewKind, the component's srcSha (story-file fingerprint), spot-check anchors. Reconstructible — absence just means "capture again". The only verdicts the script emits are factual: sb-error (story doesn't render in storybook), unpaired (no preview cell for the story), error (cell threw); every rendered pair is needs-grade.
Compare captures at most 6 stories per component by default — [STORY_CAP] in the log names components with more, and --max-stories <n> raises the cap. The cap is NOT part of the grade contract: raising it just captures the tail stories for incremental grading, and existing verdicts survive. One consequence to know: a capped component that grades fully match/close is verified-by-upload in full on future syncs even though its tail stories were never individually graded — raise the cap when those tail stories carry distinct variants worth verifying. Fan-out subagents must not change it mid-wave (sheets would cover different story sets than the orchestrator's worklist assumed).
State across runs — the first run verifies everything once; after that, one rule: grades follow your sources — the story files, your owned previews, the story set, the preview-affecting config (provider/storyImports/extraEntries/overrides/titleMap), and committed .design-sync/overrides/ forks. Pipeline churn (a skill or toolchain update re-rendering everything) is auto-verified by a sampled [SPOT_CHECK] with grades kept; your edits re-grade only what they touch. Pixel jitter can never churn grades.
- Sources unchanged + fully graded
match/close → skipped outright (carried forward): no capture, no re-grade — even when the bundle, styling, storybook, or the converter itself were rebuilt. --force recaptures everything and clears all grades — systemic re-verification, not casual sheet regeneration.
- Sources changed (story edited,
.tsx edited, config/fork edited) → recapture, grade cleared, re-grade from the fresh sheet. [STORY_CHANGED] marks stories whose code moved — those are the ones where an OWNED .tsx must be updated (generated previews re-derive automatically); a recapture without [STORY_CHANGED] usually just needs the re-grade.
[SPOT_CHECK] → re-captures named components without clearing their grades; Read the fresh sheets and confirm they still match the recorded grades. It can arrive driver-triggered after pipeline churn — the normal verification of a skill/toolchain update, not a bug. Divergence remediation scales with the churned set: a couple of components → re-grade just those; widespread → stop, diagnose, then --force a full pass. --spot-check N tunes the full-run random sample (0 disables); --spot-check-components A,B names picks explicitly, honored on scoped runs too (the §7 step-4 audit).
[REFERENCE_STALE?] → the bundle changed but the reference storybook didn't. If the DS source changed, rebuild .design-sync/sb-reference before grading — a stale reference makes every grade a comparison against the old design.
- A story renders differently every capture (
new Date()/Math.random() content) → the fingerprint is the story FILE, so the contract is stable — but the pixels aren't, and grading judges pixels. The frozen capture clock stabilizes date renders; for truly random content, pin values in an owned .tsx or cfg.overrides.<Name>.skip the story with a NOTES.md line.
Captures are stabilized for grading comparability (animations fast-forwarded, reduced motion, frozen clock — both panels show the same settled frame, the same rendered date). This is verification-only: shipped previews are untouched and fully animated.
Grading is done by whoever is working the component — you in the solo phase, each subagent for its own components in fan-out. After each compare run: Read the sheet (and raw PNGs when in doubt), judge each story from the images alone, Write the verdicts to .design-sync/.cache/compare/<Name>.grade.json (campaign-local working state — what makes a verdict durable is the upload: the uploaded _ds_sync.json anchors verified-by-upload skips on every future sync, any machine):
{"stories": {"Default": {"verdict": "match"}, "Compact": {"verdict": "match", "basis": "sibling-trusted"}}}
{"stories": {"Loading": {"verdict": "mismatch", "note": "spinner missing — story uses MSW mock"}}}
(Two components' files: a clean one graded under the sampling rule below — Default is the image-judged primary story, match on a warning-free component, which is what licenses the sibling-trusted entries — and a mismatching one, whose note drives the next fix.)
Rubric — grade what a designer would care about, looking at the two renders:
match — same content, composition, and styling. Ignore antialiasing fuzz, scrollbar slivers, sub-5px offsets, and framing differences (the storybook canvas and the preview page frame differently — judge the component, not its surroundings).
close — recognizably the same rendering with a minor delta (slightly different padding, focus ring, placeholder text). close is still a fix target, not an exit: if you can name the delta, you can usually name the knob — keep iterating. Accept close only after an iteration fails to improve it or no actionable cause remains, and the note must then say both what's off and what you tried / why it's not fixable (e.g. "focus ring color differs — storybook applies a global focus addon, not part of the DS").
mismatch — wrong/missing content, unstyled output, wrong variant, missing icons/images, default fonts. The note must say what differs — it drives the next fix.
When the REFERENCE side is the artifact — storybook gates the story behind UI chrome (a theme/control toggle message) while the preview renders the real component — judge the component render on its own and note the gating; a preview that renders more than the gated reference is not close.
Grade the primary story, trust the rest. Sibling stories of one component run through the same pipeline — same imports, same provider chain, same CSS — so when one of them renders faithfully the rest almost always do too. On a first sync, judge from images the component's primary story only (cfg.overrides.<Name>.primaryStory when set — the same story the single-mode card renders — else the sheet's first story). If it grades match and the component is clean — no sb-error/unpaired/error cells, no [PORTAL?], no [RENDER_BLANK], no blank or size-anomalous shots — write match for the remaining stories with a basis marker, {"verdict": "match", "basis": "sibling-trusted"}, so the record says how each verdict was reached (compare reads only the verdict string). All of a component's verdicts — the image-judged primary plus every sibling-trusted entry — go in its one grade.json Write: trusted siblings cost no image opens and no per-story passes. Grade exhaustively, story by story, when the component has portals/overlays, theme or provider sensitivity, an owned preview, or any warning — and always for the §4b solo set, whose exhaustive grading is what earns the trust in the first place.
Capture photographs every story either way — sampling saves grading attention, not capture time, and the sheets stay available for any deliberate later look (the §7 step-4 carried-grade audit uses the same grades-kept spot-check path). This is the same trust class as [STORY_CAP]'s ungraded tail stories, applied deliberately. Sampling never relaxes [FONT_MISSING] (§4a) — that check is invisible to the compare images either way.
4a. Fix decision tree — global first
Work top-down; a global fix repairs every component at once, a per-component fix repairs one:
- Most/all components wrong the same way → global, fix in config + full rebuild:
- Context/provider errors in cells (
use<X> must be inside <Provider>) → decorators didn't bundle (§3 ! preview decorator bundle failed rows) → cfg.provider.
- Everything unstyled / default fonts →
cfg.cssEntry (check [CSS_FROM_STORYBOOK] in the build log), cfg.tokensPkg, cfg.extraFonts.
[FONT_MISSING] — the compare loop cannot see this one. When neither side ships the font, both panels render the same chromium fallback, so the sheets look "matching" while every claude.ai/design user gets the wrong font — never accept "both sides fall back the same way" as a pass. Resolve per the [FONT_MISSING] row in ../non-storybook/SKILL.md §3; storybook-specific extras: cfg.extraFonts paths are bounded by the git repo enclosing dirname(--node-modules) — sibling typography packages in the monorepo work as-is; only with no .git ancestor does the bound narrow to dirname(--node-modules), and if you add a font the reference lacks, inject the same @font-face into .design-sync/sb-reference/iframe.html so the oracle verifies with the real font on both sides.
- Icons missing everywhere →
cfg.extraEntries (check [ICON_PKG]).
- One component,
unpaired or fallback preview → its .tsx lacks a cell for that story. Previews compile the story MODULE whole (hooks, fixtures, local helpers all included — closures are not a failure mode), so the causes are: pairing failed (storyName override), the wrapper build failed (! preview build failed in the build log), or the module threw at load — check the sheet's (page) error row for the real exception (module-scope calls into a package the stubs don't cover). Open the wrapper (generated: .design-sync/.cache/previews/<Name>.tsx; owned: .design-sync/previews/<Name>.tsx), add/rename the export or drop the offending import — and if it's the generated one, save your fix as .design-sync/previews/<Name>.tsx WITHOUT the first-line marker (an in-place cache edit is preserved on this machine but gitignored — it vanishes on a fresh clone, and it recompiles without ever re-grading; only the owned copy moves the grade contract, and the rebuild warns about edited cache twins). Story imports use the location-independent @ds-stories/<repo-relative path> form, so the file works unchanged from either home.
- One component, you graded
mismatch → wrong props/composition. Read the story source; mirror it in an owned .design-sync/previews/<Name>.tsx (copy the cache wrapper there minus its marker line). That's the only lever for compiled story previews.
sb-error → the story doesn't render in storybook either (data-fetching, interaction-driven). Add its id to cfg.overrides.<Name>.skip and note why in NOTES.md.
[PORTAL?] / overlay components (Dialog/Tooltip/Toast) → grading is already isolated (per-story capture), but the PRODUCT card renders the whole grid html, so open-overlay stories paint over sibling cells there too. Set cfg.overrides.<Name>.cardMode: "single" — the card renders one story (primaryStory picks it; first export otherwise) full-bleed in a wrapper that contains position:fixed descendants, and declares the grading viewport on the card so the product renders at the size you verified. For stories that are merely too WIDE for a grid cell (data tables, full-width bars — validate flags these as [GRID_OVERFLOW] … wide), use cardMode: "column" instead: every story keeps full card width, nothing is dropped. Targeted-rebuild that component (preview-rebuild.mjs --components <Name>, seconds) — grades carry (cardMode/primaryStory aren't in the grade key or the stamped config slices); only a viewport change re-grades (it's the capture viewport) and needs the full build (it moves the slices).
Rebuild rules — rebuild only what the change can reach. Styling changes (css/fonts/tokens) re-render every preview without moving any grade contract — grades carry forward. Provider, storyImports, extraEntries, and fork edits are part of the grade contract (they change what the preview mounts) — affected grades clear and re-grade on the rebuild.
| You changed |
Rebuild |
Compare |
a preview .tsx only |
targeted loop below (seconds) |
scoped --components <Name> — its grade cleared, re-grade |
overrides (skip/viewport) / titleMap |
full package-build.mjs + package-validate.mjs (re-stamps the config keys targeted rebuilds check) |
full compare.mjs — the touched components re-grade; carried match/close components skip outright, and the still-pending set gets fresh sheets (the full build wiped them — the next wave reads those sheets) |
overrides (cardMode/primaryStory only) |
targeted loop (preview-rebuild.mjs --components <Name>, seconds) — presentation keys aren't in the stamped config slices, so [CONFIG_STALE] doesn't trip; the loop re-emits the card html and patches its renderHash |
no re-grade: presentation-only keys aren't in the grade contract — grades carry; the changed card html re-ships and a re-sync may spot-check it |
provider / storyImports / .design-sync/overrides/ forks |
full build + validate |
full compare.mjs — affected grades re-grade per the rule above |
| css / fonts / tokens |
package-build.mjs --skip-dts + validate |
full compare.mjs — cheap: carried match/close components skip outright, so only the pending set recaptures against the new styling. Grades carry — zero-regrade, not zero-touch: the changed bytes still re-ship, and a re-sync may surface them as a verification.canary spot-check |
entry / extraEntries |
full build + validate — never --skip-dts (they change the bundle and export surface) |
full compare.mjs — affected grades re-grade |
Mid-campaign — §4c waves still pending — read this table's "full compare.mjs" as eventually, via the batches: the rebuild clears the affected grades either way, the next wave's scoped runs recapture those components, and the §4d receipt is the roster-wide settlement (§4c between-waves step 2). Pay an immediate roster-wide compare only when no waves remain.
--skip-dts skips the per-component type extraction — the slow part of a large-DS build — and emits stub .d.ts bodies, so its validate fails [DTS_STUBBED] by design (the render checks still answer "did the fix work?"); the §4d/§6 gate's validate-exits-0 requirement forces the final build to run without it. Expect stub-build floor cards and README blurbs to look bare — the final build restores them. --skip-dts is for fix-loop iteration only: any build that an upload reads — an incremental batch push (base SKILL.md §3) as much as the §6 close-out — must be a real one, so if .ds-build-meta.json still carries dtsStubbed, rebuild without the flag before pushing (batch pushes upload the on-disk .d.ts).
Batch config edits into one cycle. Before paying a rebuild, sweep every pending sheet verdict and known issue for ALL the config edits they imply (skips, titleMap entries, cardModes) and apply them together — two edits discovered minutes apart must not cost two rebuild+validate+compare cycles.
Compare run died partway (browser crash, OOM): the sheets it captured are valid — grade them first, then re-run; carry-forward scopes the recapture to the gap. Never restart a crashed run with --force (it clears the grades you just earned).
On a large DS, verify the fix is right BEFORE paying the full rebuild: run the targeted loop below on one affected component (or probe its rendered page) first — a wrong guess validated by a full rebuild costs the whole cycle. Intermediate validates can sample: global breakage is systemic by nature, so --render-sample 10 answers "did the fix work?" at a fraction of the cost; the FULL render-check is required at the §4d/§6 upload gate whenever anything render-affecting moved — on an anchored re-sync the §7 driver applies that rule automatically (the tier rule lives there).
The .tsx-only targeted loop:
node .ds-sync/lib/preview-rebuild.mjs --config .design-sync/config.json --node-modules <nm> --out ./ds-bundle --components <Name>
node .ds-sync/storybook/compare.mjs --out ./ds-bundle --storybook-static .design-sync/sb-reference --components <Name>
The targeted loop recompiles previews but does not re-key grade contracts from source: a story-file edit followed by only this loop carries the old grade until the next full build or driver run re-keys it — route story edits through a full build (the driver does that automatically).
4b. Solo phase — one, then a few
Do NOT fan out immediately. Global issues must be flushed into config first, or every subagent rediscovers them.
- One component. Pick a simple, well-storied one (Button-like: several stories, no portals). Run the §4a loop until you've graded every story
match from its images — settle for close only when an iteration stops improving it (rubric above). Every fix becomes a bullet in .design-sync/NOTES.md: symptom → root cause → fix, marked [GENERAL] when it isn't component-specific.
- Three more, chosen for diversity: one compound/overlay (Dialog/Tabs), one icon- or asset-heavy whose stories load remote images (this is the
[ASSETS_BLOCKED] canary — §3's row: a network-sandboxed shell blanks assets on BOTH panels, so grades falsely pass; surfacing it here costs one component's recapture, surfacing it after a roster-wide pass costs the whole pass), one theme/provider-sensitive — and make sure the set spans one text-heavy component (font/typography bugs hide from button-only solos and then invalidate a whole grading wave). Same loop, solo. Incremental path: the solo set, once every story grades match (or close per the rubric's acceptance bar), is the first verified batch — push it (base SKILL.md §3).
- First roster-wide capture — size-gated on the storied-component count.
- 20 or fewer: run one full
compare.mjs over the roster. Background it through the shell tool's background mode and wait for the completion notification — §2.2's rule, restated here because this is where it gets violated: a foreground sleep-poll blocks the very notification that would wake you, and a pgrep -f loop matches its own command line and spins to timeout. (Headless / -p session: run it synchronously instead — there is no task-notification re-invocation in headless mode, so a backgrounded run is never resumed.) If ≥30% of components fail with the same reason, that's a global issue you missed — fix it in config and re-run before fanning out. Batch every skip and pairing fix the listing shows before rebuilding — each rebuild+compare cycle costs minutes; fixing them one at a time pays that cost per item.
- More than 20: do NOT run a monolithic full capture. Capture happens inside §4c's batches — each subagent runs one scoped
compare.mjs --components <its batch> and grades the sheets it just captured. This buys three things: scoped captures run concurrently (the roster renders in a fraction of a serial sweep's wall-clock); grading starts when the first batch's sheets exist instead of after the last component renders; and when a wave surfaces a [GENERAL] issue, the work at risk is the few batches graded so far, not the whole roster's captures and grades. The ≥30% same-reason check moves with the capture — it becomes the wave-1 learnings review (§4c between-waves). The roster-wide run you do NOT skip is the §4d receipt: by then everything is graded, so it carries components forward instead of recapturing them and costs seconds, not minutes.
4c. Fan-out — parallel subagents
Partition the components that still need work into batches of 5–8 — on a large DS (§4b step 3's >20 gate) that is every component outside the solo set, most with no sheet captured yet; after a small-DS full capture it is the non-matching set. Group related components together (shared providers, shared fixtures — one diagnosis then serves the whole batch). Launch up to 4 subagents per wave (Agent tool, in one message so they run concurrently). Four is also the browser-concurrency cap: each subagent's scoped compare runs its own chromium, and more than ~4 concurrent captures risks launch failures from machine-level contention. For each subagent, fill every {…} in this prompt and paste the current NOTES.md content in (subagents inherit the solo phase's learnings through it):
Fix design-sync previews so they match the repo's own storybook render.
Repo: {REPO_ROOT}. Your components (yours alone): {COMPONENT_LIST}.
Why this matters: this design system is being synced to claude.ai/design, where
a design agent will build real UIs from this exact compiled bundle. The
storybook render is the proof of how each component is supposed to look; a
preview that matches it proves the component arrived intact, and one that
doesn't means every design the agent builds with it will be wrong the same way.
Artifacts per component (read these first):
- {OUT}/_screenshots/compare/<group>__<Name>.png — the true storybook render (left) vs the true preview render (right), per story. Full-res originals in {OUT}/_screenshots/compare/raw/.
- .design-sync/.cache/compare/<Name>.json — pairing facts + shot paths (no similarity scores — your eyes are the judge).
- The preview source (real JSX importing from '{PKG}'): .design-sync/previews/<Name>.tsx when owned, else the generated .design-sync/.cache/previews/<Name>.tsx. Your fixes are written to .design-sync/previews/<Name>.tsx (step 2).
- {OUT}/.stories-map.json — maps components to story ids; find each story's source file via its id in .design-sync/sb-reference/index.json (`importPath`). The story source is the authority on intended props/composition.
- .ds-sync/storybook/SKILL.md §4 — the grading rubric and fix decision tree.
First action, once for the whole batch: if any of your components has no compare sheet yet, run
node .ds-sync/storybook/compare.mjs --out {OUT} --storybook-static {SB_REF} --components {COMPONENT_LIST}
One scoped run captures every missing sheet in your batch (one browser launch, not one per component); components already graded with unchanged sources skip automatically.
Per component (max 3 iterations):
1. Read the sheet; judge the primary story FROM THE TWO IMAGES (raw PNGs when the sheet is too small) per the §4 sampling rule — exhaustively when the component has portals, theme/provider sensitivity, an owned preview, or any warning; diagnose failures via the decision tree.
2. Copy .design-sync/.cache/previews/<Name>.tsx to .design-sync/previews/<Name>.tsx and DELETE its first-line `// @ds-preview generated …` marker (owned files live in previews/, win over the generated twin, and are durable + committed; an in-place cache edit survives rebuilds on this machine but is gitignored and vanishes on a fresh clone). The `@ds-stories/...` imports work unchanged from the new location. Mirror the story's JSX; inline story-local fixture data.
3. node .ds-sync/lib/preview-rebuild.mjs --config .design-sync/config.json --node-modules {NM} --out {OUT} --components <Name>
4. node .ds-sync/storybook/compare.mjs --out {OUT} --storybook-static {SB_REF} --components <Name> (your edit changed the component's contract, so this clears its old grade — that's intended)
5. Re-Read the fresh sheet and Write your verdicts to .design-sync/.cache/compare/<Name>.grade.json ({"stories": {"<story>": {"verdict": "match|close|mismatch", "note": "…"}}}); siblings you trust under the §4 sampling rule get {"verdict": "match", "basis": "sibling-trusted"} — written in the same single grade.json Write, no image opens for them. Done when you grade every story match. A close story is still a fix target — if you can name the delta, try the knob for it; accept close only when an iteration didn't improve it or there's
…(truncated)
1---2name: storybook-source-shape3description: Storybook is the fidelity oracle, not the runtime. The converter bundles the package's compiled dist/ into dsbundle.js — the same bundle the claude.ai/design agent builds with — and generates each preview by compiling the story source module itself (hooks, fixtures, local helpers — the whole closure comes along), with every component import resolved to that shipped bundle (lib/story-imports.mjs redirects package and relative component imports to window.<Global>). The repo's own storybook render is the ground truth those previews must match: a compare harness screenshots each story in the refer4---5# Storybook source shape67Storybook is the **fidelity oracle, not the runtime**. The converter bundles the package's compiled `dist/` into `_ds_bundle.js` — the same bundle the claude.ai/design agent builds with — and generates each preview by **compiling the story source module itself** (hooks, fixtures, local helpers — the whole closure comes along), with every component import resolved to that shipped bundle (`lib/story-imports.mjs` redirects package *and* relative component imports to `window.<Global>`). The repo's own storybook render is the ground truth those previews must match: a compare harness screenshots each story in the reference storybook and the matching preview render side by side, and you iterate until they match. Nothing from storybook-static is uploaded, and no story code is ever evaluated at build time — stories run only in the browser, against the real artifact.8910Requires React 18+. Playwright + chromium are **required** for this shape (the compare loop is the verification), not optional.1112**First sync or re-sync?** A re-sync is marked by a config whose `projectId` and `pkg` were both in place before this run started — most of this document then doesn't apply; go to §7, where one driver run routes the work and untouched components cost nothing. Everything else takes the full flow (§2 build → §3 self-heal → §4 match → conventions header (base SKILL.md, before upload) → §6 upload), where every component gets verified and graded once — that includes a partial config left by an aborted run, and a pin this run itself just recorded in the base skill's §1. (Only the old `design-sync.config.json` present? Move it first and commit: `mkdir -p .design-sync && mv -n design-sync.config.json .design-sync/config.json`, then apply the same test.)1314## 2. Build, then run the converter15161. **Build the DS package *and its workspace dependencies*.** The converter bundles `dist/` into `window.<Global>`. Run `<pm> run build`; in a monorepo use `turbo run build --filter=<pkg>` or `pnpm -F "<pkg>..." build` (the trailing `...` is required — bare `-F <pkg>` skips dependencies and you'll see `Cannot find module '@scope/tokens'`). If `package.json` `module`/`exports['.']` points at TS source, find the actual built entry and pass it via `--entry`. **Do this before step 2** — storybook often imports sibling packages from their built `dist/`.172. **Build the reference storybook ONCE into `.design-sync/sb-reference/`** — NOT under `ds-bundle/` (the converter wipes `--out` on every rebuild, and storybook builds take minutes; the reference must survive the fix loop):1819 ```bash20 npx storybook build -c <storybookConfigDir> -o .design-sync/sb-reference21 ```2223 Run it from the directory whose `package.json` has the storybook devDependencies — usually the one containing `.storybook/`; monorepos often have several storybooks, so pick the one covering the package you're syncing. **Make `-o` the repo-root path** (e.g. `-o "$(git rev-parse --show-toplevel)/.design-sync/sb-reference"`): the converter and compare resolve `.design-sync/` from the repo root, so a cwd-relative `-o` in a subpackage puts the reference where nothing will find it. Use `npx storybook build` directly, **not** the repo's `npm run build-storybook` script (wrong output dir). Then check `.design-sync/sb-reference/iframe.html` exists and is >10KB — `index.json` alone can exist with a failed build.2425 Long builds: background them **through your shell tool's background mode only** and wait for the completion notification. Never a bare `&` (untracked — the notification never comes), and never a `pgrep -f '<script>'` poll loop (it matches its own command line and spins to timeout). Headless / `-p` sessions: run long commands synchronously instead — there is no task-notification re-invocation there, so a backgrounded run is never resumed.2627 `.gitignore` additions: `.design-sync/sb-reference/`, `.design-sync/learnings/`, `.design-sync/.cache/`, `.design-sync/node_modules` (fork symlink — recreated per clone), `.ds-sync/`, `ds-bundle/` — build artifact, transient scratch, verification working state, the symlink, staged scripts, regenerated output. Committed: the durable set (the rule in non-storybook §2, same here: everything under `.design-sync/` not gitignored — previews/ holds your authored files ONLY; generated story-module wrappers live in `.design-sync/.cache/previews/` and regenerate every build; the converter never writes or deletes anything in `previews/`). Verification state is never committed — cross-machine carry-forward comes from the uploaded project's `_ds_sync.json`. Rebuild the reference only when stories or the DS source change.283. **Write `.design-sync/config.json`** — only `pkg` and `globalName` required. **If it already exists, read it first and keep what's there** — `titleMap`, `overrides`, and `provider` accumulate fixes from prior syncs. Also Read `.design-sync/NOTES.md` first — its **Re-sync risks** section is the prior run's watch-list; re-verify those items instead of assuming carry-forward covers them. The package-shape field table in `../non-storybook/SKILL.md` §2.6 applies verbatim; the fields that matter most here:2930 | Field | Value |31 |---|---|32 | `pkg` / `globalName` | `pkg` required; `globalName` auto-derived from it when omitted |33 | `shape` | `"storybook"` — pins detection |34 | `storybookStatic` | `".design-sync/sb-reference"` — so re-syncs and compare find the reference without flags |35 | `storybookConfigDir` | the `.storybook/` dir (monorepos) |36 | `buildCmd` | what to re-run before the converter on re-sync |37 | `titleMap` | `{title: ExportName}` when story titles don't match export names; `{title: null}` excludes a non-visual/internal component from the sync entirely |38 | `overrides` | `{<Name>: {skip: [storyIds], cardMode: "single"\|"column", primaryStory: "<Export>", viewport: "WxH"}}` — `skip` for stories that can't render statically; `cardMode: "single"` for overlay components (§4a.5, §5), `"column"` for stories wider than a grid cell (the `[GRID_OVERFLOW]` row in §3) |39 | `provider` | usually unnecessary for **previews** — `.storybook/preview` decorators are auto-bundled; set only when that fails. Before §6 upload, distill decorator-provided context into `cfg.provider` — README/prompt.md wrap guidance is generated from config only (decorator-only wrapping ships a generic note). **Setting it also replaces the decorators as the preview wrapper on the next build**: scoped-compare a themed component after the switch — an incomplete distillation regresses previews the decorators rendered fine, and carried-forward grades won't catch it. Format: `{"component": "ThemeProvider", "props": {…}, "inner": {…}}` — a nested chain, outermost first; each `component` must be a bundle export. Literal `props` are for small scalars (`"theme": "light"`) and stable snippets. For data that already exists in the repo — a locale JSON, a theme object — **prefer `{"$ref": "<export>"}`** backed by a 2-line module added via `cfg.extraEntries` (e.g. `export { default as previewI18n } from '../locales/en.json'`): a `$ref` emits `window.<Global>.<export>`, so the data lives once in the bundle and re-reads from its source file on every build. Inlining a copy is acceptable for something tiny and stable, but know the cost — a literal duplicates into every card's html and silently rots when the source file changes, so anything sizable or evolving belongs behind a `$ref`. Path forms for `extraEntries`: a bare name resolves from `node_modules`; a repo-owned module needs an explicit `./`/`../` package-relative path (workspace-bounded — the build logs `! extraEntries: … skipped` if it escapes). |40414. **Stage scripts + install converter deps** (isolated in `.ds-sync/`, repo lockfile untouched):4243 ```bash44 mkdir -p .ds-sync && cp -r "<skill-base-dir>"/package-build.mjs "<skill-base-dir>"/package-validate.mjs "<skill-base-dir>"/resync.mjs "<skill-base-dir>"/lib "<skill-base-dir>"/storybook "<skill-base-dir>"/non-storybook .ds-sync/45 echo '{"name":"ds-sync-deps","private":true}' > .ds-sync/package.json46 (cd .ds-sync && npm i esbuild ts-morph @types/react playwright && npx playwright install chromium)47 ```4849 If chromium install fails, `npx playwright install-deps chromium` first; if the environment can't install chromium, set `DS_CHROMIUM_PATH=<system-chromium>`.505. **Run the converter, validator, and compare** — synchronously, stopping at the first non-zero exit (compare only runs once build + validate are clean — §3). Large DSes (≈100+ components) may need `NODE_OPTIONS=--max-old-space-size=<MB>` for the build; **never pipe the build through `head`/`tail`** (the pipeline masks the exit code — an OOM looks like success); redirect to a file and read it:5152 ```bash53 node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules <pkg-node-modules> \54 --entry <built-dist-entry> --out ./ds-bundle55 node .ds-sync/package-validate.mjs ./ds-bundle56 node .ds-sync/storybook/compare.mjs --out ./ds-bundle --storybook-static .design-sync/sb-reference \57 --components <solo-phase picks> # scope the FIRST compare to the §4b solo components58 ```5960 In a monorepo, `--node-modules` is the DS package's own `node_modules` — unless hoisting leaves it sparse (yarn's `node-modules` linker keeps `react` only at the repo root): if `react/` or `react-dom/` is missing inside, pass the repo-root `node_modules` instead. In the DS's own source repo `node_modules/<pkg>` doesn't exist, hence `--entry`. The build logs `[ICON_PKG]` / `[TOKENS_PKG]` auto-detections and bundles `.storybook/preview` decorators as the preview wrapper (`preview-decorators.js`) so previews get the same provider chain stories do.6162 Scope the first compare run: a full capture of a large DS is thousands of chromium navigations — pointless before the solo phase has flushed global issues (each global fix invalidates every capture). The first roster-wide run happens per §4b step 3 — and on a DS over 20 storied components even that is size-gated into §4c's scoped batches, so the only mandatory full-roster run is the §4d receipt, which carries graded work forward instead of recapturing it. For a DS with >100 storied components, also tell the user the expected scale (components × stories) before fan-out and let them narrow scope if they want.6364## 3. Self-heal loop (build + validate)6566Fix `[TAG]` errors → rebuild → re-validate until both exit 0, **before** starting the compare loop in §4 — there's no point pixel-matching previews while the bundle itself is broken. Shared converter tags (`[NO_DIST]`, `[WORKSPACE_SIBLING]`, `[CSS_*]`, `[FONT_*]`, `[TOKENS_MISSING]`, `[DTS_*]`, `[RENDER*]`, …) behave identically to the package shape — use the table in `../non-storybook/SKILL.md` §3. Lines printed as `hypothesis:` under an error are leads, not instructions: run their verify step first, and if it doesn't confirm, drop the hypothesis and diagnose from the error text itself. Storybook-specific:6768| Tag | Symptom | Fix |69|---|---|---|70| `[SB_REFERENCE_MISSING]` | compare can't find `iframe.html` | Build the reference (§2.2); set `cfg.storybookStatic`. |71| `[SB_BUILD_FAIL]` | converter's own storybook build failed | You skipped §2.2 — build the reference yourself and set `cfg.storybookStatic` so the converter never needs to. |72| `[ZERO_MATCH]` (storybook flavor) | no story entries matched | Check the storybook config's `stories` glob; then `titleMap`. |73| `[TITLE_UNMAPPED]` | N titles don't match an export | `cfg.titleMap {<title-name>: <export-name>}`. |74| `(preview: <Name> — no story exports paired …)` | index story names couldn't be matched to module export keys (pairing tries the display name, then the story ID's tail) | the component shows the floor card; fix the pairing — usually an owned `.tsx` re-exporting the stories under matchable names. |75| a preview cell errors with `undefined`-component / wrong-context messages | a story import resolved the wrong way — relative, tsconfig-alias, and bare-workspace imports all go through the same policy (see `lib/story-imports.mjs`'s rules) | `cfg.storyImports.shim` / `cfg.storyImports.bundle` substring patterns force the resolution per resolved path — the cheap fix before forking the seam. |76| `! preview build failed: <Name>` | the story module didn't COMPILE (top-level await, an import of a package esbuild can't resolve, an asset extension with no loader) | read the esbuild error above the line. Unknown asset extension → `cfg.storyImports.loaders` (merged over the defaults, e.g. `{".yaml": "text"}`); unresolvable import → own the `.tsx` and drop it. The component shows the floor card until fixed. |77| a story's own stylesheet is missing from its cell | story-local `.css`/`.scss` side-effect imports compile as empty (component styles ship via the bundle css). Exception: `.module.css` IS compiled — classes resolve and `_preview/<Name>.css` is linked automatically | usually nothing — the styles are decoration the storybook page adds. If the story genuinely depends on them, inline the styles in an owned `.tsx`. |78| `[BUNDLE_EXPORT]` | components aren't functions on `window.<Global>` | `extraEntries` for subpath/icon exports; check the dist entry is the full build. |79| `[SCHEDULER_MISSING]` | dist imports `scheduler` | react-dom leaked into the DS dist — check its build's externals. |80| `! preview decorator bundle failed` | decorators couldn't be bundled | Set `cfg.provider` manually, or run `node .ds-sync/storybook/probe.mjs --storybook-static .design-sync/sb-reference` to infer the chain from the live storybook (replace each `$hint` with a real value). |81| previews error at `_vendor/preview-decorators.js` load (storybook-API `undefined` errors) | the `.storybook/preview` import graph reached a storybook-runtime module the stubs don't cover | `manager-api`/`preview-api` are stubbed with functional no-op hooks and every other `@storybook/*`/`msw` module with inert callables (`fn()`, `action()`, `setupWorker()` at module scope all evaluate harmlessly); if some other API still crashes, set `cfg.provider` explicitly — it skips decorator bundling entirely. |82| `[ASSETS_BLOCKED]` from compare | the capture browser inherited a network-sandboxed shell — story assets (CDN images/fonts) failed on **both** panels, so grades can falsely pass while end users see different output | re-run `package-validate.mjs` + `compare.mjs --force` from a shell with egress to the listed hosts: approve running the command without the sandbox when prompted, or add the hosts to the sandbox allowlist. Don't grade image-bearing components while this prints. |8384**Incremental path (base SKILL.md §3) — this is the open-the-channel gate.** The first time build + validate both exit 0, open the upload channel before starting §4: the user approves once here, then watches components land as grading proceeds. Nothing uploads until the first graded batch — the shared base files ride with it — and the batch pushes come from §4b/§4c. (Atomic path: nothing uploads until §6.)8586## 4. Match previews to storybook8788`compare.mjs` is a **capture harness — it photographs, you grade.** It computes no similarity heuristics (pixel/text/font scores mislead whenever framing legitimately differs); the judgment is made from the two true screenshots. Compiled previews capture **per story** — each story renders alone via `?story=<Export>` at the full capture viewport, exactly as storybook frames the reference side — so sibling stories can't interfere (portal stacking, shared radio-group names, focus, container measurement). Two output tiers:89- **Transient** (under `ds-bundle/`, wiped by rebuilds): `_screenshots/compare/<group>__<Name>.png` — sheet with one row per story: the **true storybook render | the true preview render**, side by side. Sheet images are shrunk to fit; the full-resolution originals are in `…/compare/raw/` (`…__sb.png` / `…__ds.png`) — Read those when the sheet is too small to judge confidently.90- **Campaign state** (in `.design-sync/.cache/compare/`, gitignored): `<Name>.grade.json` — your verdicts — and `<Name>.json` — capture facts: story↔cell pairing, shot paths, `previewKind`, the component's `srcSha` (story-file fingerprint), spot-check anchors. Reconstructible — absence just means "capture again". The only verdicts the script emits are factual: `sb-error` (story doesn't render in storybook), `unpaired` (no preview cell for the story), `error` (cell threw); every rendered pair is `needs-grade`.9192Compare captures at most 6 stories per component by default — `[STORY_CAP]` in the log names components with more, and `--max-stories <n>` raises the cap. The cap is NOT part of the grade contract: raising it just captures the tail stories for incremental grading, and existing verdicts survive. One consequence to know: a capped component that grades fully `match`/`close` is verified-by-upload in full on future syncs even though its tail stories were never individually graded — raise the cap when those tail stories carry distinct variants worth verifying. Fan-out subagents must not change it mid-wave (sheets would cover different story sets than the orchestrator's worklist assumed).9394**State across runs** — the first run verifies everything once; after that, one rule: **grades follow your sources** — the story files, your owned previews, the story set, the preview-affecting config (`provider`/`storyImports`/`extraEntries`/`overrides`/`titleMap`), and committed `.design-sync/overrides/` forks. Pipeline churn (a skill or toolchain update re-rendering everything) is auto-verified by a sampled `[SPOT_CHECK]` with grades kept; your edits re-grade only what they touch. Pixel jitter can never churn grades.95- *Sources unchanged* + fully graded `match`/`close` → **skipped outright** (`carried forward`): no capture, no re-grade — even when the bundle, styling, storybook, or the converter itself were rebuilt. `--force` recaptures everything **and clears all grades** — systemic re-verification, not casual sheet regeneration.96- *Sources changed* (story edited, `.tsx` edited, config/fork edited) → recapture, grade cleared, re-grade from the fresh sheet. `[STORY_CHANGED]` marks stories whose code moved — those are the ones where an OWNED `.tsx` **must be updated** (generated previews re-derive automatically); a recapture *without* `[STORY_CHANGED]` usually just needs the re-grade.97- *`[SPOT_CHECK]`* → re-captures named components **without clearing their grades**; Read the fresh sheets and confirm they still match the recorded grades. It can arrive driver-triggered after pipeline churn — the normal verification of a skill/toolchain update, not a bug. Divergence remediation scales with the churned set: a couple of components → re-grade just those; widespread → stop, diagnose, then `--force` a full pass. `--spot-check N` tunes the full-run random sample (0 disables); `--spot-check-components A,B` names picks explicitly, honored on scoped runs too (the §7 step-4 audit).98- *`[REFERENCE_STALE?]`* → the bundle changed but the reference storybook didn't. If the DS source changed, rebuild `.design-sync/sb-reference` before grading — a stale reference makes every grade a comparison against the *old* design.99- *A story renders differently every capture* (`new Date()`/`Math.random()` content) → the fingerprint is the story FILE, so the contract is stable — but the pixels aren't, and grading judges pixels. The frozen capture clock stabilizes date renders; for truly random content, pin values in an owned `.tsx` or `cfg.overrides.<Name>.skip` the story with a NOTES.md line.100101Captures are stabilized for grading comparability (animations fast-forwarded, reduced motion, frozen clock — both panels show the same settled frame, the same rendered date). This is verification-only: shipped previews are untouched and fully animated.102103**Grading is done by whoever is working the component** — you in the solo phase, each subagent for its own components in fan-out. After each compare run: Read the sheet (and raw PNGs when in doubt), judge each story **from the images alone**, Write the verdicts to `.design-sync/.cache/compare/<Name>.grade.json` (campaign-local working state — what makes a verdict durable is the upload: the uploaded `_ds_sync.json` anchors verified-by-upload skips on every future sync, any machine):104105```json106{"stories": {"Default": {"verdict": "match"}, "Compact": {"verdict": "match", "basis": "sibling-trusted"}}}107{"stories": {"Loading": {"verdict": "mismatch", "note": "spinner missing — story uses MSW mock"}}}108```109110(Two components' files: a clean one graded under the sampling rule below — `Default` is the image-judged primary story, `match` on a warning-free component, which is what licenses the sibling-trusted entries — and a mismatching one, whose note drives the next fix.)111112Rubric — grade what a designer would care about, looking at the two renders:113- `match` — same content, composition, and styling. Ignore antialiasing fuzz, scrollbar slivers, sub-5px offsets, and framing differences (the storybook canvas and the preview page frame differently — judge the component, not its surroundings).114- `close` — recognizably the same rendering with a minor delta (slightly different padding, focus ring, placeholder text). **`close` is still a fix target, not an exit:** if you can name the delta, you can usually name the knob — keep iterating. Accept `close` only after an iteration fails to improve it or no actionable cause remains, and the note must then say both *what's off* and *what you tried / why it's not fixable* (e.g. "focus ring color differs — storybook applies a global focus addon, not part of the DS").115- `mismatch` — wrong/missing content, unstyled output, wrong variant, missing icons/images, default fonts. The note must say *what* differs — it drives the next fix.116117When the REFERENCE side is the artifact — storybook gates the story behind UI chrome (a theme/control toggle message) while the preview renders the real component — judge the component render on its own and note the gating; a preview that renders *more* than the gated reference is not `close`.118119**Grade the primary story, trust the rest.** Sibling stories of one component run through the same pipeline — same imports, same provider chain, same CSS — so when one of them renders faithfully the rest almost always do too. On a first sync, judge from images the component's **primary story** only (`cfg.overrides.<Name>.primaryStory` when set — the same story the single-mode card renders — else the sheet's first story). If it grades `match` and the component is clean — no `sb-error`/`unpaired`/`error` cells, no `[PORTAL?]`, no `[RENDER_BLANK]`, no blank or size-anomalous shots — write `match` for the remaining stories with a basis marker, `{"verdict": "match", "basis": "sibling-trusted"}`, so the record says how each verdict was reached (compare reads only the `verdict` string). All of a component's verdicts — the image-judged primary plus every sibling-trusted entry — go in its one `grade.json` Write: trusted siblings cost no image opens and no per-story passes. Grade exhaustively, story by story, when the component has portals/overlays, theme or provider sensitivity, an owned preview, or any warning — and always for the §4b solo set, whose exhaustive grading is what earns the trust in the first place.120121Capture photographs every story either way — sampling saves grading attention, not capture time, and the sheets stay available for any deliberate later look (the §7 step-4 carried-grade audit uses the same grades-kept spot-check path). This is the same trust class as `[STORY_CAP]`'s ungraded tail stories, applied deliberately. Sampling never relaxes `[FONT_MISSING]` (§4a) — that check is invisible to the compare images either way.122123### 4a. Fix decision tree — global first124125Work top-down; a global fix repairs every component at once, a per-component fix repairs one:1261271. **Most/all components wrong the same way** → global, fix in config + full rebuild:128 - Context/provider errors in cells (`use<X> must be inside <Provider>`) → decorators didn't bundle (§3 `! preview decorator bundle failed` rows) → `cfg.provider`.129 - Everything unstyled / default fonts → `cfg.cssEntry` (check `[CSS_FROM_STORYBOOK]` in the build log), `cfg.tokensPkg`, `cfg.extraFonts`.130 - **`[FONT_MISSING]` — the compare loop cannot see this one.** When neither side ships the font, both panels render the same chromium fallback, so the sheets look "matching" while every claude.ai/design user gets the wrong font — never accept "both sides fall back the same way" as a pass. Resolve per the `[FONT_MISSING]` row in `../non-storybook/SKILL.md` §3; storybook-specific extras: `cfg.extraFonts` paths are bounded by the git repo enclosing `dirname(--node-modules)` — sibling typography packages in the monorepo work as-is; only with no `.git` ancestor does the bound narrow to `dirname(--node-modules)`, and if you add a font the reference lacks, inject the same `@font-face` into `.design-sync/sb-reference/iframe.html` so the oracle verifies with the real font on both sides.131 - Icons missing everywhere → `cfg.extraEntries` (check `[ICON_PKG]`).1322. **One component, `unpaired` or `fallback preview`** → its `.tsx` lacks a cell for that story. Previews compile the story MODULE whole (hooks, fixtures, local helpers all included — closures are not a failure mode), so the causes are: pairing failed (`storyName` override), the wrapper build failed (`! preview build failed` in the build log), or the module threw at load — check the sheet's `(page)` error row for the real exception (module-scope calls into a package the stubs don't cover). Open the wrapper (generated: `.design-sync/.cache/previews/<Name>.tsx`; owned: `.design-sync/previews/<Name>.tsx`), add/rename the export or drop the offending import — and if it's the generated one, save your fix as `.design-sync/previews/<Name>.tsx` WITHOUT the first-line marker (an in-place cache edit is preserved on this machine but gitignored — it vanishes on a fresh clone, and it recompiles without ever re-grading; only the owned copy moves the grade contract, and the rebuild warns about edited cache twins). Story imports use the location-independent `@ds-stories/<repo-relative path>` form, so the file works unchanged from either home.1333. **One component, you graded `mismatch`** → wrong props/composition. Read the story source; mirror it in an owned `.design-sync/previews/<Name>.tsx` (copy the cache wrapper there minus its marker line). That's the only lever for compiled story previews.1344. **`sb-error`** → the story doesn't render in storybook either (data-fetching, interaction-driven). Add its id to `cfg.overrides.<Name>.skip` and note why in NOTES.md.1355. **`[PORTAL?]` / overlay components** (Dialog/Tooltip/Toast) → grading is already isolated (per-story capture), but the PRODUCT card renders the whole grid html, so open-overlay stories paint over sibling cells there too. Set `cfg.overrides.<Name>.cardMode: "single"` — the card renders one story (`primaryStory` picks it; first export otherwise) full-bleed in a wrapper that contains `position:fixed` descendants, and declares the grading viewport on the card so the product renders at the size you verified. For stories that are merely too WIDE for a grid cell (data tables, full-width bars — validate flags these as `[GRID_OVERFLOW] … wide`), use `cardMode: "column"` instead: every story keeps full card width, nothing is dropped. Targeted-rebuild that component (`preview-rebuild.mjs --components <Name>`, seconds) — **grades carry** (`cardMode`/`primaryStory` aren't in the grade key or the stamped config slices); only a `viewport` change re-grades (it's the capture viewport) and needs the full build (it moves the slices).136137**Rebuild rules — rebuild only what the change can reach.** Styling changes (css/fonts/tokens) re-render every preview without moving any grade contract — grades carry forward. Provider, `storyImports`, `extraEntries`, and fork edits are part of the grade contract (they change what the preview mounts) — affected grades clear and re-grade on the rebuild.138139| You changed | Rebuild | Compare |140|---|---|---|141| a preview `.tsx` only | targeted loop below (seconds) | scoped `--components <Name>` — its grade cleared, re-grade |142| `overrides` (`skip`/`viewport`) / `titleMap` | full `package-build.mjs` + `package-validate.mjs` (re-stamps the config keys targeted rebuilds check) | full `compare.mjs` — the touched components re-grade; carried `match`/`close` components skip outright, and the still-pending set gets fresh sheets (the full build wiped them — the next wave reads those sheets) |143| `overrides` (`cardMode`/`primaryStory` only) | **targeted loop** (`preview-rebuild.mjs --components <Name>`, seconds) — presentation keys aren't in the stamped config slices, so `[CONFIG_STALE]` doesn't trip; the loop re-emits the card html and patches its renderHash | **no re-grade**: presentation-only keys aren't in the grade contract — grades carry; the changed card html re-ships and a re-sync may spot-check it |144| `provider` / `storyImports` / `.design-sync/overrides/` forks | full build + validate | full `compare.mjs` — affected grades re-grade per the rule above |145| css / fonts / tokens | `package-build.mjs --skip-dts` + validate | full `compare.mjs` — cheap: carried `match`/`close` components skip outright, so only the pending set recaptures against the new styling. Grades carry — zero-regrade, not zero-touch: the changed bytes still re-ship, and a re-sync may surface them as a `verification.canary` spot-check |146| `entry` / `extraEntries` | full build + validate — never `--skip-dts` (they change the bundle and export surface) | full `compare.mjs` — affected grades re-grade |147148Mid-campaign — §4c waves still pending — read this table's "full `compare.mjs`" as *eventually, via the batches*: the rebuild clears the affected grades either way, the next wave's scoped runs recapture those components, and the §4d receipt is the roster-wide settlement (§4c between-waves step 2). Pay an immediate roster-wide compare only when no waves remain.149150`--skip-dts` skips the per-component type extraction — the slow part of a large-DS build — and emits stub `.d.ts` bodies, so its validate fails `[DTS_STUBBED]` by design (the render checks still answer "did the fix work?"); the §4d/§6 gate's validate-exits-0 requirement forces the final build to run without it. Expect stub-build floor cards and README blurbs to look bare — the final build restores them. `--skip-dts` is for fix-loop iteration only: any build that an upload reads — an incremental batch push (base SKILL.md §3) as much as the §6 close-out — must be a real one, so if `.ds-build-meta.json` still carries `dtsStubbed`, rebuild without the flag before pushing (batch pushes upload the on-disk `.d.ts`).151152**Batch config edits into one cycle.** Before paying a rebuild, sweep every pending sheet verdict and known issue for ALL the config edits they imply (`skip`s, `titleMap` entries, `cardMode`s) and apply them together — two edits discovered minutes apart must not cost two rebuild+validate+compare cycles.153154**Compare run died partway** (browser crash, OOM): the sheets it captured are valid — grade them first, then re-run; carry-forward scopes the recapture to the gap. Never restart a crashed run with `--force` (it clears the grades you just earned).155156**On a large DS, verify the fix is right BEFORE paying the full rebuild**: run the targeted loop below on one affected component (or probe its rendered page) first — a wrong guess validated by a full rebuild costs the whole cycle. **Intermediate validates can sample**: global breakage is systemic by nature, so `--render-sample 10` answers "did the fix work?" at a fraction of the cost; the FULL render-check is required at the §4d/§6 upload gate whenever anything render-affecting moved — on an anchored re-sync the §7 driver applies that rule automatically (the tier rule lives there).157158The `.tsx`-only targeted loop:159 ```bash160 node .ds-sync/lib/preview-rebuild.mjs --config .design-sync/config.json --node-modules <nm> --out ./ds-bundle --components <Name>161 node .ds-sync/storybook/compare.mjs --out ./ds-bundle --storybook-static .design-sync/sb-reference --components <Name>162 ```163164 The targeted loop recompiles previews but does not re-key grade contracts from source: a story-file edit followed by only this loop carries the old grade until the next full build or driver run re-keys it — route story edits through a full build (the driver does that automatically).165166### 4b. Solo phase — one, then a few167168Do NOT fan out immediately. Global issues must be flushed into config first, or every subagent rediscovers them.1691701. **One component.** Pick a simple, well-storied one (Button-like: several stories, no portals). Run the §4a loop until you've graded every story `match` from its images — settle for `close` only when an iteration stops improving it (rubric above). **Every fix becomes a bullet in `.design-sync/NOTES.md`**: symptom → root cause → fix, marked `[GENERAL]` when it isn't component-specific.1712. **Three more, chosen for diversity:** one compound/overlay (Dialog/Tabs), one icon- or asset-heavy **whose stories load remote images** (this is the `[ASSETS_BLOCKED]` canary — §3's row: a network-sandboxed shell blanks assets on BOTH panels, so grades falsely pass; surfacing it here costs one component's recapture, surfacing it after a roster-wide pass costs the whole pass), one theme/provider-sensitive — and make sure the set spans one **text-heavy** component (font/typography bugs hide from button-only solos and then invalidate a whole grading wave). Same loop, solo. *Incremental path:* the solo set, once every story grades `match` (or `close` per the rubric's acceptance bar), is the first verified batch — push it (base SKILL.md §3).1723. **First roster-wide capture — size-gated on the storied-component count.**173 - **20 or fewer:** run one full `compare.mjs` over the roster. Background it through the shell tool's background mode and wait for the completion notification — §2.2's rule, restated here because this is where it gets violated: a foreground `sleep`-poll blocks the very notification that would wake you, and a `pgrep -f` loop matches its own command line and spins to timeout. (Headless / `-p` session: run it synchronously instead — there is no task-notification re-invocation in headless mode, so a backgrounded run is never resumed.) If ≥30% of components fail with the *same* reason, that's a global issue you missed — fix it in config and re-run before fanning out. **Batch every skip and pairing fix the listing shows before rebuilding** — each rebuild+compare cycle costs minutes; fixing them one at a time pays that cost per item.174 - **More than 20: do NOT run a monolithic full capture. Capture happens inside §4c's batches** — each subagent runs one scoped `compare.mjs --components <its batch>` and grades the sheets it just captured. This buys three things: scoped captures run concurrently (the roster renders in a fraction of a serial sweep's wall-clock); grading starts when the first batch's sheets exist instead of after the last component renders; and when a wave surfaces a `[GENERAL]` issue, the work at risk is the few batches graded so far, not the whole roster's captures and grades. The ≥30% same-reason check moves with the capture — it becomes the wave-1 learnings review (§4c between-waves). The roster-wide run you do NOT skip is the §4d receipt: by then everything is graded, so it carries components forward instead of recapturing them and costs seconds, not minutes.175176### 4c. Fan-out — parallel subagents177178Partition the components that still need work into batches of 5–8 — on a large DS (§4b step 3's >20 gate) that is every component outside the solo set, most with no sheet captured yet; after a small-DS full capture it is the non-matching set. Group related components together (shared providers, shared fixtures — one diagnosis then serves the whole batch). Launch up to 4 subagents per wave (Agent tool, in one message so they run concurrently). Four is also the browser-concurrency cap: each subagent's scoped compare runs its own chromium, and more than ~4 concurrent captures risks launch failures from machine-level contention. For each subagent, fill every `{…}` in this prompt and paste the **current** NOTES.md content in (subagents inherit the solo phase's learnings through it):179180```text181Fix design-sync previews so they match the repo's own storybook render.182Repo: {REPO_ROOT}. Your components (yours alone): {COMPONENT_LIST}.183184Why this matters: this design system is being synced to claude.ai/design, where185a design agent will build real UIs from this exact compiled bundle. The186storybook render is the proof of how each component is supposed to look; a187preview that matches it proves the component arrived intact, and one that188doesn't means every design the agent builds with it will be wrong the same way.189190Artifacts per component (read these first):191- {OUT}/_screenshots/compare/<group>__<Name>.png — the true storybook render (left) vs the true preview render (right), per story. Full-res originals in {OUT}/_screenshots/compare/raw/.192- .design-sync/.cache/compare/<Name>.json — pairing facts + shot paths (no similarity scores — your eyes are the judge).193- The preview source (real JSX importing from '{PKG}'): .design-sync/previews/<Name>.tsx when owned, else the generated .design-sync/.cache/previews/<Name>.tsx. Your fixes are written to .design-sync/previews/<Name>.tsx (step 2).194- {OUT}/.stories-map.json — maps components to story ids; find each story's source file via its id in .design-sync/sb-reference/index.json (`importPath`). The story source is the authority on intended props/composition.195- .ds-sync/storybook/SKILL.md §4 — the grading rubric and fix decision tree.196197First action, once for the whole batch: if any of your components has no compare sheet yet, run198 node .ds-sync/storybook/compare.mjs --out {OUT} --storybook-static {SB_REF} --components {COMPONENT_LIST}199One scoped run captures every missing sheet in your batch (one browser launch, not one per component); components already graded with unchanged sources skip automatically.200201Per component (max 3 iterations):2021. Read the sheet; judge the primary story FROM THE TWO IMAGES (raw PNGs when the sheet is too small) per the §4 sampling rule — exhaustively when the component has portals, theme/provider sensitivity, an owned preview, or any warning; diagnose failures via the decision tree.2032. Copy .design-sync/.cache/previews/<Name>.tsx to .design-sync/previews/<Name>.tsx and DELETE its first-line `// @ds-preview generated …` marker (owned files live in previews/, win over the generated twin, and are durable + committed; an in-place cache edit survives rebuilds on this machine but is gitignored and vanishes on a fresh clone). The `@ds-stories/...` imports work unchanged from the new location. Mirror the story's JSX; inline story-local fixture data.2043. node .ds-sync/lib/preview-rebuild.mjs --config .design-sync/config.json --node-modules {NM} --out {OUT} --components <Name>2054. node .ds-sync/storybook/compare.mjs --out {OUT} --storybook-static {SB_REF} --components <Name> (your edit changed the component's contract, so this clears its old grade — that's intended)2065. Re-Read the fresh sheet and Write your verdicts to .design-sync/.cache/compare/<Name>.grade.json ({"stories": {"<story>": {"verdict": "match|close|mismatch", "note": "…"}}}); siblings you trust under the §4 sampling rule get {"verdict": "match", "basis": "sibling-trusted"} — written in the same single grade.json Write, no image opens for them. Done when you grade every story match. A close story is still a fix target — if you can name the delta, try the knob for it; accept close only when an iteration didn't improve it or there's207208…(truncated)