synthesisui 0.16.235 → 0.16.238

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.
@@ -1,1316 +1,15 @@
1
1
  /**
2
- * THE IMPORT SKILL, as the CLI ships it.
2
+ * O STUB DA SKILL - o corpo é SERVIDO, não embarcado (R1, dono 16/08).
3
3
  *
4
- * It lives here rather than as a file copied at build time for one reason: the
5
- * build is `tsc` and nothing else, so a `.md` would need a copy step that can
6
- * silently not run. A TypeScript module cannot fail to be packaged.
4
+ * O playbook inteiro morava aqui (65 KB) e, por consequência, no tarball
5
+ * público do npm e no repo de cada cliente em texto plano. Ele mudou para a
6
+ * plataforma (apps/web/src/lib/ds/playbooks.ts, gêmeo por spec do
7
+ * `.claude/skills/sui-import-ds/SKILL.md`) e é servido pelo catalogue autenticado -
8
+ * a mesma doutrina de sempre: SERVED, NOT SHIPPED. O agente busca o sumário
9
+ * e depois SÓ o capítulo do passo em que está (a dieta de contexto).
7
10
  *
8
- * THIS IS THE SOURCE. The copy in our own `.claude/skills/` is what our editor
9
- * reads, and `skill-import.spec.ts` asserts the two are identical - so drift
10
- * turns a test red instead of shipping a skill that describes a pipeline the
11
- * CLI no longer has.
11
+ * O frontmatter fica INTEIRO no stub: é a description que faz a skill ser
12
+ * invocada, e ela não é segredo - a esteira é.
12
13
  */
13
- export const IMPORT_SKILL = `---
14
- name: sui-import-ds
15
- description: Turn a codebase the user ALREADY has into a SynthesisUI design system. Use when someone points at an existing repo, app or component library and asks to import it, adopt it, bring it in, or "make a design system from this" (e.g. "/sui-import-ds", "importa o meu packages/ui", "turn this app into a design system"). Drives the full pipeline - census → your reading → import → v2 proposal - and answers the questions arithmetic cannot.
16
- ---
17
-
18
- # Import Design System
19
-
20
- Read a project the user already built and give it back to them as a system that governs it.
21
-
22
- The census is **arithmetic** and it is not yours to redo. Every colour ranked, every ΔE
23
- measured, every component crosswalked - the CLI does all of it, deterministically, for free,
24
- and it will be right. Your job is the part with no closed form: **understanding how this
25
- project is structured.**
26
-
27
- ## The golden rule
28
-
29
- **You may judge. You may never invent.**
30
-
31
- Every hex you name has to be one the census already observed - the endpoint drops the rest
32
- silently, and a "faithful" import carrying a colour the author never wrote is the one failure
33
- this whole pipeline exists to prevent. The same discipline applies to everything else you
34
- report: if you did not read it in their files, do not claim it.
35
-
36
- When you are unsure, say so in \`concept\` and leave the field out. An absent answer falls back
37
- to arithmetic, which is honest. A confident wrong answer inverts someone's product.
38
-
39
- ## The pipeline
40
-
41
- ### 0. Check the login FIRST, and stop if there is none
42
-
43
- \`\`\`
44
- cat ~/.synthesisui/credentials.json 2>/dev/null || echo "NO TOKEN"
45
- \`\`\`
46
-
47
- No token, or a run that later comes back **HTTP 401**, means there is nothing to send to. **Stop
48
- there.** Do not measure, do not read the project, do not write a reading - say this and wait:
49
-
50
- \`\`\`
51
- You need to authenticate first:
52
-
53
- npx synthesisui login
54
- \`\`\`
55
-
56
- It is a device flow: it opens a browser and needs a person. You cannot complete it, and there
57
- is no point doing twenty minutes of reading against a session that will refuse the payload at
58
- the end (dono, 31/07 - a full read ended on a 401 that a first command would have caught).
59
-
60
- **Read the \`registry\` field in that file, and pass it to EVERY later command.** A token belongs
61
- to the host that issued it. One minted by \`login --registry http://localhost:3000\` and then
62
- sent to the default (production) earns a 401 that reads as an expired session - and a session
63
- cannot expire on a host that never issued it. That is how a full read ended in a dead end
64
- twice (dono, 31/07).
65
-
66
- So if the file says \`"registry": "http://localhost:3000"\`, every command in this skill carries
67
- \`--registry http://localhost:3000\`. If it says production, pass nothing. **Read that file ONCE**
68
- and carry the answer for the whole run - re-reading it before each command is a round trip that
69
- answers a question already answered. The CLI now refuses a
70
- mismatch instead of letting the server reject it, but the point is not to reach that.
71
-
72
- **Do this silently.** Which host you are pointed at and which flags follow from it is plumbing,
73
- and narrating it - *"registry is production, so no --registry flag needed"* - spends the
74
- person's attention on a decision that was never theirs (dono, 01/08). Say something only when
75
- there is nothing to say to: no token, or the wrong host.
76
-
77
- ### 0b. ASK THE REGISTRY WHAT EXISTS - never your notes
78
-
79
- If your memory says this repo already produced a system, **verify it before you say it out loud**,
80
- and verify it per SLUG:
81
-
82
- \`\`\`
83
- curl -s -o /dev/null -w "%{http_code}" <registry>/api/registry/ds/<slug>
84
- \`\`\`
85
-
86
- \`404\` means it does not exist. \`200\` means it does. That is the whole check, and it costs one call.
87
-
88
- **\`synthesisui list\` does NOT answer this.** It returns the public gallery - 13 systems, none of
89
- them private - so a private system of theirs is invisible to it and a \`list\` that does not mention
90
- \`their-slug\` proves nothing.
91
-
92
- This exists because of a real run (dono, 05/08). The owner had deleted every system on his account,
93
- and the skill opened with *"my notes from the previous twelve runs say this repo has already
94
- produced two published systems"* - then offered three options built on that: upgrade a slug the
95
- registry answers \`404\` for, avoid a name collision that no longer existed, and warn that a \`--dry\`
96
- would wipe files it does not touch. One \`curl\` would have replaced all of it.
97
-
98
- **A memory may report the past; it may never assert the present.** *"The last run produced
99
- \`signalui\`"* is a fine sentence. *"This repo has two published systems"* is a claim about the server,
100
- and the server is the only thing that can make it.
101
-
102
- ## How to narrate
103
-
104
- The person cares about **what you found in their code** and **what you are about to create**.
105
- They do not care which flag you passed, which grep you ran, or what you checked and ruled out.
106
-
107
- - Report findings, not mechanics. *"Their docs call Ocean the primary branding scale"* earns
108
- its line. *"Let me check whether packages/ui exists"* does not.
109
- - Every decision you make on their behalf belongs in **one** confirmation at the end, not
110
- scattered through the work as fait accompli.
111
- - Never end on a wall of prose that happens to contain a question. If you need an answer, the
112
- last thing on screen is the question.
113
-
114
- ### 1. Measure
115
-
116
- **Always run from the project root, and always twice in a monorepo.** The root is where the
117
- governance lives - \`_synthesisui/config.json\`, \`ds/<slug>/\`, and the census. One root, one
118
- census; re-measuring overwrites it rather than leaving copies to send the wrong one.
119
-
120
- First, unscoped, to find out what kind of repo this is:
121
-
122
- \`\`\`
123
- npx synthesisui import --dry
124
- \`\`\`
125
-
126
- If it is a single app, that census is the one. If it holds several projects, the CLI says so
127
- and **names the candidates itself**, ranked by how many tokens each declares:
128
-
129
- \`\`\`
130
- This root holds several projects
131
- 3 apps (apps/web-admin, apps/web-dashboard, apps/web-review) - and this census
132
- is the average of all of them. That is a fine DIAGNOSIS and a poor system: a
133
- light app and a dark one average into a palette that is neither.
134
-
135
- synthesisui import --scope packages/ui --usage apps/web-dashboard
136
- \`\`\`
137
-
138
- Then measure again, narrowed - and **name both roles**:
139
-
140
- \`\`\`
141
- npx synthesisui import --dry --scope packages/ui --usage apps/web-dashboard
142
- \`\`\`
143
-
144
- ### The two roles, and why one flag could not do both
145
-
146
- \`\`\`
147
- --scope the SYSTEM tokens, components, the way they name a class ONE folder
148
- --usage the EVIDENCE how often, which values get picked, the laws as many as they have
149
- \`\`\`
150
-
151
- \`--usage\` is repeatable and never contributes a token, a scale or a component. That split is
152
- not tidiness, it is the two mistakes this pipeline exists to avoid, and each one has been made:
153
-
154
- **Do not import the average.** Three apps measured together produce a palette that belongs to
155
- none of them, and it will look plausible - a real monorepo reported 36% coverage across 2797
156
- files at the root and 47% across 114 in its shared package. The second number is the system;
157
- the first is a diagnosis. Their scale is a DECLARATION; an app's average is a set of choices
158
- made from one.
159
-
160
- **Do not import a library alone either.** A law needs three files that agree, and a library has
161
- ONE file per component - so measured by itself, a real one came back with \`usage\` empty on all
162
- 23 components and not a single observed law. Not because nothing is settled: because the
163
- agreeing happens in the app (dono, 01/08). If they have apps, name them.
164
-
165
- Point \`--usage\` at every app that consumes the system. A component that lives in an app and
166
- not in the system is reported and left out - the system is one place, and a union of two
167
- codebases is not a system.
168
-
169
- The census still lands at \`<root>/_synthesisui/census.json\` and records both what it measured
170
- and where the evidence came from, so you always send the same path and there is never a second
171
- file to pick between. Nothing is sent by \`--dry\`. Read the file.
172
-
173
- ### 2. Walk them through the decisions - ALL of them, before any reading
174
-
175
- **WHEN \`sui-init\` SENT YOU HERE, THESE ARE ALREADY ANSWERED. Ask nothing.**
176
-
177
- The first-run skill collects source, name, primary, theme, canvas, architecture and \`standards\`
178
- before it hands over, precisely so this step is silent. Asking again is not thoroughness - it is the
179
- same six questions twice in one conversation, which reads as a tool that forgot what it was told, and
180
- it is the single fastest way to make a first run feel long. Take the answers, skip to step 3, and
181
- confirm nothing.
182
-
183
- If any of them is missing from the handover, ask ONLY that one.
184
-
185
- **Every question this skill asks is answerable from the census you just took.** Source, name,
186
- primary, theme, canvas, architecture - the numbers behind all six are in the file on their disk,
187
- and none of them needs the anatomy.
188
-
189
- They used to be asked AFTER the reading, and that put six hundred lines of work between the
190
- person's first answer and their second (dono, 01/08). A person who answers six questions in
191
- ninety seconds and then watches a bar has had a good four minutes; the same person interrupted
192
- twice in the middle of a silence has had a bad twenty.
193
-
194
- So: ask everything here, in one pass, and **do not come back**. If something you learn during
195
- the reading contradicts an answer, you do not reopen the question - you say so in the report at
196
- the end and let them change it there.
197
-
198
- Six decisions are theirs. **Ask them as separate questions with selectable options** - use your
199
- question tool, one call per decision, so they pick instead of reading a wall and composing a
200
- reply. A single block containing everything is a report, and a report gets read, not answered.
201
-
202
- Every question carries **your recommendation first, marked as such**, and the evidence for it in
203
- one line. They are choosing, not auditing you.
204
-
205
- **2a. Source** - the folder you measure the system out of.
206
-
207
- Options are the candidates the CLI already ranked, each with what it would produce, plus the
208
- whole repo, plus *somewhere else* for a path they type:
209
-
210
- \`\`\`
211
- Where should the system come from?
212
-
213
- packages/ui (recommended) 114 files · 47% of its values already named ·
214
- 92 tokens of their own · every app imports it
215
- the whole repo 2797 files · 36% · the average of one dark app
216
- and two light ones, which is a diagnosis and
217
- not a system
218
- apps/web-dashboard the heaviest consumer, if you want one app's
219
- vocabulary rather than the shared one
220
- somewhere else a path you name
221
- \`\`\`
222
-
223
- If they name a path you cannot use, say which of the two it is and offer the list again:
224
-
225
- - **it does not exist**, or holds no files you can read
226
- - **it holds no design values** - no colours, no radii, no tokens. A folder of server code or
227
- config is not a design system, and measuring it produces a system of nothing.
228
-
229
- **2b. Name** - suggest theirs, and let Enter take it:
230
-
231
- \`\`\`
232
- What should it be called? (SignalUI)
233
- \`\`\`
234
-
235
- Say where the suggestion came from - \`Introduction.mdx\`, the package name, the folder. A name
236
- they recognise is worth more than a clever one. The slug is derived once and never changes, so
237
- this is the moment.
238
-
239
- **2c. Primary** - and explain the role before offering the choice, because *primary* is our word
240
- and not necessarily theirs:
241
-
242
- \`\`\`
243
- Primary is the colour that carries action - buttons, links, focus, the thing
244
- you want pressed. Everything else in the system is measured for contrast
245
- against it.
246
-
247
- #059aed ocean-500 (recommended) your ColorPalette.mdx calls Ocean the
248
- "Primary branding scale"
249
- #1a4ed8 royal-blue-500 your docs label this one secondary
250
- #ec4899 vivid-pink-500 half of your signature AI gradient
251
- #4A90E2 blue-500 the most-painted blue in the folder,
252
- but it sits after your own
253
- /* End - Signal UI */ marker
254
- \`\`\`
255
-
256
- Always name the hex AND the token, and always give the reason a value is *not* recommended when
257
- frequency would have picked it. That last row is the whole point of asking.
258
-
259
- **2d. Default theme** - which face it opens in:
260
-
261
- \`\`\`
262
- dark (recommended) data-theme="dark" in the root layout, next-themes
263
- defaultTheme="dark", enableSystem={false}
264
- light the other one, also real here
265
- \`\`\`
266
-
267
- **Two is all there is** - a SynthesisUI system carries one palette and one alternate, and the
268
- schema is \`"light" | "dark"\`. So this is not "which themes do you support", it is "which one do
269
- docs and installs open in". If both are real, both get built from their own tokens; this picks
270
- the face.
271
-
272
- **2e. Canvas** - confirm, do not offer a list. The page is measured, not chosen:
273
-
274
- \`\`\`
275
- canvas #222326 darkgray-500 - what DashboardLayout paints on <main>
276
- (your sidebar sits a step deeper at darkgray-700)
277
- \`\`\`
278
-
279
- Say it and let them object. Offering five near-blacks to pick between is asking someone to
280
- re-decide something their code already decided.
281
-
282
- **2f. Our kit** - do not ask. A project that already has a component library gets **theirs
283
- alone**, and you say so in one line:
284
-
285
- \`\`\`
286
- This system is yours alone - the 37 components you wrote, and none of ours.
287
- Add any of ours later from the library, one at a time.
288
- \`\`\`
289
-
290
- Send it as \`"standards": "none"\` in the reading. Every time, with one exception below.
291
-
292
- **Why this stopped being a question.** Somebody who points us at their own library is telling us
293
- what their system is, and 45 components of ours landing beside 37 of theirs makes a system of 82
294
- where only 37 are governed by anything they wrote - the number on their own screen stops being
295
- about them. The kit is one click away in the library afterwards, so choosing wrong here costs a
296
- click and choosing to ask costs a pause in the middle of their import.
297
-
298
- **The exception, and it is not a preference.** An app with NO components of its own - \`--scope\`
299
- pointed at an app rather than a library, and the CLI reported no component library - must NOT get
300
- \`"standards": "none"\`: theirs alone plus nothing of theirs is a system with zero components. Leave
301
- the field out there, exactly as before.
302
-
303
- Two things to be straight about, because both are true and neither is obvious:
304
-
305
- - **the foundations still come from the seed.** Their palette, their scales, their type - all
306
- theirs - but the seven type slots, the easing and the neutral floor arrive from ours, because
307
- the alternative to a type scale is no components at all rather than their components.
308
- - **our layouts are written in our recipes.** A landing that composes \`button\`, \`card\` and
309
- \`badge\` needs those to exist. Theirs alone keeps the layouts whose recipes they happen to have
310
- and drops the rest - a layout pointing at a recipe nobody has renders as holes. Say that number
311
- when the CLI reports it, so a shorter list of layouts is a fact they were told rather than one
312
- they discover.
313
-
314
- **2g. Which architecture has priority** - ask only when the CLI reported more than
315
- one, which it does under "How this project is organised".
316
-
317
- A project having two shapes is not a project with a mistake: a library organised
318
- atomically inside a monorepo whose apps are organised by feature is two true answers.
319
- Say that before you ask, or the question reads as an accusation.
320
-
321
- \`\`\`
322
- Two shapes, and neither is wrong:
323
-
324
- atomic under packages/ui (recommended) 114 files - atoms/molecules/organisms,
325
- and this is where the system comes from
326
- feature under apps/web-dashboard 900 files - features/, where the app
327
- consumes it
328
- \`\`\`
329
-
330
- The one they pick becomes a rule that reaches \`CLAUDE.md\`, so **every prompt after this
331
- knows where a new component goes** - not what the folders are called, but which rung or
332
- which feature a new file belongs to. Recommend the one the system came from.
333
-
334
- **2h. Fonts** - do not ask at all. Report and move on:
335
-
336
- \`\`\`
337
- fonts Figtree for both display and body - the only family declared
338
- (--font-sans, and next/font Figtree in all three root layouts)
339
- \`\`\`
340
-
341
- If a project genuinely declares two families, say which is which and why you paired them that
342
- way. If it declares one, there is nothing to decide.
343
-
344
- **A note on colour swatches.** You cannot paint a hex in this conversation, so the token name
345
- carries the weight - \`ocean-500\` tells someone more than a square would. When they run
346
- \`npx synthesisui import\` themselves in a terminal, the CLI paints them.
347
-
348
- ### 3. Read what arithmetic cannot - and do it in ONE uninterrupted run
349
-
350
- Every answer is in hand, so nothing below needs the person. **Do not ask, do not confirm, do
351
- not narrate the mechanics.** Three lines on screen for the whole phase:
352
-
353
- \`\`\`
354
- 1/3 Measuring the repository... (step 1, already done)
355
- 2/3 Naming the anatomy and validating... (this step)
356
- 3/3 Sending v1 and v2... (step 4)
357
- \`\`\`
358
-
359
- **Narrate findings, never mechanics.** A grep, a glob, a file you opened and ruled out, a draft
360
- you rewrote - none of those is a finding, and a run that prints them reads as a machine talking
361
- to itself. *"Their docs call Ocean the primary branding scale"* is a finding and earns its line.
362
-
363
- **The census answers first, always.** Before opening any file, check whether \`signals\`, \`sketch\`,
364
- \`declared\` or \`coverage\` already hold it - they usually do, and a file read whose content the
365
- census carries is the failure this pipeline keeps paying for. The sketch now runs to 150 nodes,
366
- so a big component arrives WHOLE: \`TextEditor\` used to lose its editable region at node 60 and
367
- that single truncation is what forced a hand-read of the file (dono, 01/08).
368
-
369
- \`signals.theme\` includes the SWITCH - \`attribute\`, \`defaultTheme\`, \`enableSystem\` and what
370
- \`<html>\` carries, \`colorScheme\` included - read from the \`--usage\` roots as well as the scope,
371
- because a component library declares the dark utilities and the APP declares the switch. When it
372
- came from an app, \`switchedIn\` names which one: say "the system is measured in \`packages/ui\`, the
373
- theme is switched in \`apps/web-dashboard\`" rather than reporting that the library declares a
374
- provider it does not have. Answering 2d never needs a layout file opened.
375
-
376
- **Validate in ONE call, at the end.** Build every recipe in memory first, then send them all to
377
- \`validate_recipes\` - one round trip for the whole reading instead of one per component. A reader
378
- with 35 anatomies spent 35 turns asking the same question, and none of that was thinking. Only
379
- after the batch answers do you fix what it named and, if you fixed anything, ask once more.
380
-
381
-
382
-
383
- Open the project and answer the questions below. Then add a \`reading\` object to
384
- \`_synthesisui/census.json\`:
385
-
386
- \`\`\`json
387
- "reading": {
388
- "themes": { "default": "dark", "has": ["dark"] },
389
- "roles": { "canvas": "#050505", "foreground": "#f9fafb", "primary": "#4A90E2" },
390
- "fonts": { "display": "Inter", "body": "Inter" },
391
- "typeRoles": { "base": "body-m", "display": "h1", "xs": "caption" },
392
- "concept": "one paragraph on what this product is",
393
- "mood": ["focused", "dense", "technical"],
394
- "rules": [
395
- {
396
- "text": "A RootWrapper is what makes the app-shell components position correctly - never render a Sidebar or Topbar outside one",
397
- "applies": ["RootWrapper", "Sidebar"],
398
- "kind": "implementation",
399
- "when": ["next"],
400
- "files": 3,
401
- "evidence": "all three app shells wrap them; none renders them loose"
402
- },
403
- {
404
- "text": "MetricCard is only used inside a grid, never on its own",
405
- "applies": ["MetricCard"],
406
- "kind": "limit",
407
- "files": 1,
408
- "evidence": "one dashboard page; may just be how it happened"
409
- },
410
- {
411
- "text": "TextEditor's editable region is <EditorContent> - never render children into it directly",
412
- "applies": ["TextEditor"],
413
- "kind": "implementation",
414
- "fact": true,
415
- "evidence": "read in TextEditor itself: useEditor() feeds a single <EditorContent>"
416
- }
417
- ],
418
- "components": {
419
- "MetricCard": {
420
- "anatomy": [
421
- { "as": "icon", "name": "icon", "at": 1 },
422
- {
423
- "as": "stack",
424
- "name": "body",
425
- "at": 2,
426
- "children": [
427
- { "as": "component", "ref": "Text", "at": 3 },
428
- { "as": "heading", "name": "value", "at": 4 }
429
- ]
430
- },
431
- {
432
- "as": "component",
433
- "ref": "Tooltip",
434
- "at": 5,
435
- "children": [{ "as": "icon", "name": "info", "at": 6 }]
436
- }
437
- ]
438
- },
439
- "ArticleCard": {
440
- "root": "Card",
441
- "anatomy": [
442
- { "as": "image", "name": "cover", "at": 1 },
443
- { "as": "heading", "name": "title", "at": 2 },
444
- { "as": "component", "ref": "TextEditor", "at": 5 },
445
- {
446
- "as": "row",
447
- "name": "footer",
448
- "at": 3,
449
- "children": [
450
- { "as": "button", "name": "publish", "at": 4 },
451
- { "as": "button", "name": "discard", "classes": "btn btn-ghost" }
452
- ]
453
- }
454
- ]
455
- },
456
- "TextEditor": {
457
- "anatomy": [
458
- {
459
- "as": "row",
460
- "name": "toolbar",
461
- "classes": "flex gap-1 border-b p-2",
462
- "children": [{ "as": "component", "ref": "ToolbarButton" }]
463
- },
464
- { "as": "external", "from": "@tiptap/react" }
465
- ]
466
- }
467
- },
468
- "by": "claude"
469
- }
470
- \`\`\`
471
-
472
- Every field is optional. Leave out what you did not establish.
473
-
474
- **\`concept\` and \`mood\` - the words the system opens with.**
475
-
476
- The guide, the vitrine and every page the platform writes about this system lead with these. Until
477
- 07/08 nothing filled them, so a real import shipped with the seed's line - *"A calm grayscale
478
- starting point to make your own"* - as the first sentence of somebody's own design system. A sentence
479
- of ours at the top of their system says, in the first line, that nothing was read.
480
-
481
- \`concept\` is one paragraph on what this product IS, in their words where you can find them: the
482
- README, the docs site, the introduction their library ships with. \`mood\` is two to six words for how
483
- the product FEELS - \`focused\`, \`dense\`, \`editorial\`, \`playful\`. It is a judgement and it is yours to
484
- make, which is exactly why the arithmetic cannot: no reader infers a mood from a hex.
485
-
486
- Leave \`mood\` out when you did not form one. The seed's three words stay and the import SAYS they are
487
- ours, which is honest - three words you picked to fill a field are not.
488
-
489
- **\`themes\` - which themes the product actually ships, and which one it opens in.**
490
-
491
- This is the field that matters most and the one only you can fill. The ladder can say which
492
- *values* exist; it cannot say which themes the product *has*, because that answer lives in a
493
- \`dark:\` class name, a \`ThemeProvider\`, a toggle component, a \`prefers-color-scheme\` query -
494
- places no token reader sees. A real library declared nine near-blacks and five light greys in
495
- one \`:root\` block, and the import handed its near-black dashboard a white page.
496
-
497
- **DO NOT GO LOOKING. It is already counted**, under "What was countable, counted" in the
498
- census output:
499
-
500
- \`\`\`
501
- 782 dark utilities across 79 files - so a second scheme is real here, not aspirational.
502
- switched by a ThemeProvider of their own, bound to data-theme, opening dark, with the
503
- OS overruled.
504
- html carries lang="en" data-theme="dark" - which is the ancestor every dark rule of
505
- theirs matches, and therefore the one ours must match.
506
- \`\`\`
507
-
508
- That is a real run. Your job is to CONFIRM OR CORRECT it, not to reproduce it - and a sweep
509
- would bring back a different slice than the next one, which is the worse property of the two.
510
-
511
- **Confirmation costs ZERO reads.** A real run spent 7 pattern searches and 39 file reads to
512
- "confirm" a theme the census had already counted (test14, 01/08) - that is re-deriving, not
513
- confirming. The rule: the census numbers are TRUE unless something you already read while
514
- doing other work contradicts them. Only on a contradiction do you open a file, and only the
515
- one file the evidence names. If nothing contradicts, write the census's answer down and move
516
- on - it costs one sentence.
517
-
518
- \`has: ["dark"]\` on a dark-only product is the right answer. It is better than filling a light
519
- slot on their behalf, and it makes adding light a deliberate act they take later.
520
-
521
- **\`roles\` - which of their values carries which meaning**, when their token names do not say.
522
- A project naming its neutrals by temperature (\`--color-darkgray-900\`) has told you nothing
523
- about which one is the page - and the census now answers that too, under "the page paints":
524
-
525
- \`\`\`
526
- the dark page paints darkgray-500 (2x on <main>/<body>, 2x full-viewport container)
527
- the light page paints #EDF3FA (1x body in global CSS), lightgray-200 (2x viewport)
528
- \`\`\`
529
-
530
- Three countable sources, strongest first: \`body { background }\` in global CSS, a background
531
- on \`<main>\`/\`<body>\`, and a full-viewport container (\`min-h-screen\` + a background IS the
532
- page). **Do not hunt for the canvas** - pick from this evidence, and name the source when you
533
- report it. When the sources disagree, that disagreement is itself the finding to surface.
534
-
535
- **\`fonts\` and \`concept\`** - the voice, and one paragraph on what this product is. The concept
536
- feeds every recommendation downstream, so a real one beats a generic one by a wide margin.
537
-
538
- **\`typeRoles\` - which of THEIR type steps plays each of our seven slots.**
539
-
540
- Their scale arrives under their own names now - \`h1\`, \`body-m\`, \`caption\`, \`overline\` - read
541
- straight off their \`--text-*\` declarations. What arithmetic cannot know is which of those steps
542
- is *body copy* and which is *the display size*, and our own components ask for it by slot:
543
-
544
- \`\`\`json
545
- "typeRoles": {
546
- "xs": "caption", "sm": "body-s", "base": "body-m", "lg": "body-l",
547
- "xl": "h3", "2xl": "h2", "display": "h1"
548
- }
549
- \`\`\`
550
-
551
- Leave it out and we pick by size, which is usually right and occasionally silly - a project whose
552
- \`overline\` is tiny and whose \`caption\` is tinier gets them the wrong way round. One line from you
553
- fixes it, and a person can correct it later in the studio either way.
554
-
555
- **Do not rename their steps to match ours.** \`h1\` stays \`h1\`. The whole point is that editing
556
- \`body-m\` in the studio moves the text in their app.
557
-
558
- **\`rules\` - how this company BUILDS, which is half of what they actually made.**
559
-
560
- A design system that arrives as tokens and recipes is only the vocabulary. The other half is the
561
- grammar: that a wrapper exists so the thing inside it works, that state arrives through a
562
- particular hook, that a card is never used loose. That is how the developer already thinks, and
563
- writing it down is what makes it survive them.
564
-
565
- Two dimensions, and they are independent:
566
-
567
- \`\`\`
568
- applies [] the whole system
569
- [a] that component
570
- [a, b] a RELATION - two components that only work together
571
-
572
- kind limit a boundary, which the doctor can measure
573
- implementation how it is built, which no linter checks and an agent
574
- must be told
575
- \`\`\`
576
-
577
- The relation is the valuable one. \`[RootWrapper, Sidebar]\` says something neither name says
578
- alone, and it is exactly what an agent needs in order not to assemble it wrongly.
579
-
580
- **\`when\` - the environments a rule is true in.** Leave it out and the rule holds everywhere.
581
-
582
- This exists because the system gets installed places it was never written for. *"The state arrives
583
- through this hook"* is true about Next and **actively wrong** about Vue, so a rule that depends on
584
- the framework has to say so - and the install filters against the project it lands in, because
585
- only the project knows what it is built with.
586
-
587
- One dimension: a member of the stack (\`next\`, \`vue\`, \`react\`, \`svelte\`, \`tailwind\`, \`shadcn/ui\`).
588
- A rule may list several. It cannot combine conditions - \`next && !tailwind\` would be a query
589
- language, and then somebody maintains an interpreter forever.
590
-
591
- Use it only when the rule genuinely depends on the environment. Most rules do not: *"never render
592
- a Sidebar loose"* is true about their design regardless of framework, and pinning it to \`next\`
593
- would quietly drop it the day they add a second app.
594
-
595
- **Report \`files\` honestly - it decides whether an OBSERVED rule governs.** Three or more files
596
- is a habit and the rule arrives active; one file is a coincidence and it arrives as a candidate,
597
- inactive, waiting for the person to promote it. You do not make that call; you report the
598
- evidence and a threshold makes it. So a pattern you saw once should say \`"files": 1\` even when
599
- you are confident - being wrong about a law is worse than being slow about one.
600
-
601
- **\`fact: true\` - for a rule you read in the DEFINITION, where counting is the wrong question.**
602
-
603
- Three of these were reported as \`files: 1\` and arrived inactive, which was correct arithmetic on
604
- the wrong kind of claim (dono, 01/08):
605
-
606
- \`\`\`
607
- the editable region is <EditorContent>
608
- toolbar actions go through editor.chain().focus()
609
- extensions are configured at construction
610
- \`\`\`
611
-
612
- None of those is a coincidence waiting for a second sighting. They are how the component IS
613
- built, read off its own source, and they are true the moment somebody wrote it. Counting how many
614
- files agree would leave every construction law in the codebase inactive forever.
615
-
616
- So the test is **where you read it**, not how sure you feel:
617
-
618
- \`\`\`
619
- fact: true you read it inside the component's own definition - its imports, its
620
- JSX, how its state is wired. True by construction.
621
- files: N you inferred it from how the component is USED across the project.
622
- An observation, and the count is what makes it a habit.
623
- \`\`\`
624
-
625
- A rule can carry \`fact\` OR \`files\`, never both. If you find yourself wanting both, it is an
626
- observation - use \`files\`.
627
-
628
- **Write \`evidence\` as what you actually saw.** "All three app shells wrap them; none renders
629
- them loose" lets somebody disagree with a fact. "Best practice" lets them disagree only with
630
- you.
631
-
632
- Things worth looking for, none of them guessable from tokens:
633
-
634
- - a wrapper or provider whose whole purpose is to make something else work
635
- - an environment law - "this is Next, so the state comes through this hook" - which travels
636
- with the system and only applies where that environment is
637
- - a component that is never used alone, or never used outside something
638
- - an axis that is always passed the same way in one place and never in another
639
-
640
- If you cannot say where a rule came from, do not send it. An invented law is worse than a
641
- missing one, because it will be obeyed.
642
-
643
- **\`components[Name].anatomy\` - what each component is MADE OF, and in what shape.** This is the
644
- field that decides whether a component previews as itself or as a grey box with a sentence in it,
645
- and only you can fill it.
646
-
647
- The census reads the ROOT element's classes and stops there, on purpose: descending a fixed
648
- number of levels picks a layout wrapper as often as a semantic part. **You read the component, so
649
- you decide the shape.** Send a tree; the CLI turns the classes into declarations and the platform
650
- turns declarations into roles.
651
-
652
- ### The three frontiers
653
-
654
- Every node you send is one of three things, and knowing which is the whole job:
655
-
656
- \`\`\`
657
- a PART an element of theirs it has styles, and we draw it
658
- a COMPONENT a component of theirs it has a recipe of its own → named block + a RULE
659
- an EXTERNAL a third-party library we do not have it and never will → block + rules
660
- \`\`\`
661
-
662
- **Depth is not a number, it is where the frontier sits.** Descend until you meet another
663
- component or a library, stop there, and record the edge. That is why no parameter tells you how
664
- deep to go: a \`Divider\` is one node deep and a dashboard shell is five, and both are complete.
665
-
666
- **You decide the depth, and nothing downstream caps it.** The platform follows a
667
- \`component\` edge into that component's own anatomy, and then into ITS edges, as far as the chain
668
- goes - \`Chat → Message → TypingIndicator\` renders all three. So a frontier is not a dead end you
669
- are apologising for; it is how the chain gets walked. Record the edge and stop, and the whole
670
- depth appears anyway.
671
-
672
- ### The ROOT is a frontier too
673
-
674
- \`\`\`json
675
- "components": {
676
- "ArticleCard": {
677
- "root": "Card",
678
- "anatomy": [ … ]
679
- },
680
- "Modal": {
681
- "root": "BaseDialog.Root",
682
- "anatomy": [ … ]
683
- },
684
- "MetricCard": {
685
- "root": { "name": "Card", "at": 1 },
686
- "anatomy": [ … ]
687
- }
688
- }
689
- \`\`\`
690
-
691
- **A library root whose name is not dotted needs its PACKAGE.** \`Pill\` returns Base UI's
692
- \`Toggle\`, and \`"root": "Toggle"\` would point at THEIR own Toggle - a switch - inventing a
693
- relation that does not exist. Send where it came from instead:
694
-
695
- \`\`\`json
696
- "Pill": { "root": { "from": "@base-ui/react/toggle" }, "anatomy": [ … ] }
697
- \`\`\`
698
-
699
- \`Radio.Root\` needs none of this: the dot already says it is somebody's namespace. Leaving the
700
- root out was the old workaround and it cost the surface its name (not-expressed.md, dono, 01/08).
701
-
702
- **The root takes what the PARENT paints on it.** When the returned root carries an override at
703
- the call site - \`MetricCard\` returns \`<Card className="flex-1 dark:bg-darkgray-300">\` - send the
704
- object form and point \`at\` the sketch node: \`{ "name": "Card", "at": 1 }\`. The string form still
705
- works and means "no override". Sending the root as an anatomy node instead would make it a leaf
706
- and orphan the content inside it, which is why it is a field on the root rather than a node
707
- (not-expressed.md, dono, 01/08).
708
-
709
- **Say what the component RETURNS when it is not a plain tag.** Measured on a real library
710
- (dono, 01/08): 14 of 23 components return one of their own - \`ArticleCard\` returns a \`<Card>\`,
711
- \`Button\` and \`Text\` return a \`<Component>\` - and 17 of 23 have no style of their own at all,
712
- because the surface belongs to the root.
713
-
714
- Without \`root\`, an \`ArticleCard\` previewed as floating text: the background, the border, the
715
- radius and the padding had nowhere to come from. With it, the card wears \`Card\`'s recipe as its
716
- shell and its own parts inside.
717
-
718
- - **their component** - the name, as their code spells it: \`"root": "Card"\`
719
- - **a library** - the dotted namespace, verbatim: \`"root": "Radio.Root"\`,
720
- \`"root": "BaseDialog.Root"\`, \`"root": "Popover.Root"\`
721
- - **a plain tag** - leave it out. \`<div>\`, \`<td>\`, \`<button>\` are not frontiers.
722
-
723
- ### The ten forms
724
-
725
- \`as\` says what a node IS, and the renderer draws that. Nothing else is accepted:
726
-
727
- \`\`\`
728
- image a picture region: cover, thumbnail, media
729
- heading the title line
730
- text body copy, a label, a value
731
- button an action
732
- field an input somebody types into
733
- icon a glyph, or a bare shape with no text
734
- row arranges its children ACROSS
735
- stack arranges its children DOWN
736
- component their component → needs "ref": the name as their code spells it
737
- external a library → needs "from": the package name
738
- slot the CALLER's content → takes "expects": what goes there, in their words
739
- \`\`\`
740
-
741
- \`row\` and \`stack\` ARRANGE their children. A \`component\` or \`external\` node HOLDS them -
742
- what their code nests inside a component they compose is markup THEY wrote, wearing their
743
- classes, and it belongs in the tree:
744
-
745
- \`\`\`json
746
- { "as": "component", "ref": "Card", "at": 3, "children": [
747
- { "as": "row", "name": "header", "at": 4 },
748
- { "as": "external", "from": "recharts", "at": 8 } ] }
749
- \`\`\`
750
-
751
- A \`button\` holds a glyph and its words - \`<button><Upload/>Upload</button>\` is valid HTML,
752
- because a button's content model is *phrasing* and an icon is phrasing. So an \`icon\` and a
753
- \`text\` may sit inside one:
754
-
755
- \`\`\`json
756
- { "as": "button", "name": "upload", "at": 9, "children": [{ "as": "icon", "name": "upload-icon", "at": 10 }] }
757
- \`\`\`
758
-
759
- Everything else is a leaf, and a leaf that holds elements has them HOISTED OUT to become
760
- its siblings - which destroys the nesting you read. If something arranges, say \`row\`/\`stack\`.
761
-
762
- **A frontier keeps its own \`at\`, so it keeps its own classes.** This is the one that gets
763
- talked out of: \`<Text className="text-lightgray-500 font-medium">\` is one of THEIR
764
- components, so it is a \`component\` node - and pointing \`at\` its sketch index carries the
765
- typography onto it exactly as a part would. Reading it as a \`text\` part instead is a
766
- downgrade with a real cost: \`composes\` is built from \`component\` nodes, so a library whose
767
- most-used atom is composed 363 times reported composing it ZERO times, and its strongest
768
- composition law became invisible (not-expressed.md, dono, 03/08).
769
-
770
- <Tooltip><Info/></Tooltip> frontier with a child, both survive
771
- <Text className="…">Label</Text> frontier with "at", the type survives
772
-
773
- If you find yourself writing "the contract cannot hold this, so I sent something else",
774
- check it against \`recipe_vocabulary\` before you write the workaround - and if the check
775
- disagrees with this file, the check is right and this file is old.
776
-
777
- ### \`{children}\` is a node, not an absence
778
-
779
- Four things decide what a component looks like on screen, and only the first is in its own source:
780
-
781
- \`\`\`
782
- its own classes you are reading them
783
- what the caller passes <tr>{children}</tr> → slot
784
- its runtime state value={62} drives the width → a part with no static size
785
- a library drawing it <EditorContent> → external
786
- \`\`\`
787
-
788
- A component whose content is not its own had nothing to send, so it sent nothing and previewed as
789
- a blank box. A real \`DataTableRow\` is sixteen lines - a \`<tr>\` carrying a border, a hover and
790
- \`{children}\` - and it came back empty, which is not wrong so much as unreadable (dono, 01/08).
791
-
792
- So send the slot. \`expects\` is what the caller is supposed to put there, in the words their own
793
- code uses - read the prop type, the JSDoc, or a call site:
794
-
795
- \`\`\`json
796
- { "as": "slot", "name": "row", "classes": "border-b hover:bg-ocean-50/30", "expects": "the cells" }
797
- \`\`\`
798
-
799
- It keeps its \`name\` and \`classes\` like any other node - the border and the hover are real and
800
- belong on that element. What it does not do is invent content: the preview says "the cells go
801
- here", which is exactly what the file says.
802
-
803
- Good \`expects\`: \`"the cells"\`, \`"one row per item"\`, \`"the form"\`, \`"the page body"\`. Leave it
804
- out rather than writing \`"children"\` - the field is for what a person would call it.
805
-
806
- **A part can carry its own variants and states**, not only a flat class string. Send
807
- \`classes\` for the resting look and let the prefixes travel inside it - \`hover:\`,
808
- \`focus-visible:\`, \`disabled:\`, \`group-hover:\`, \`md:\`, \`data-[state=open]:\` are all read
809
- and land where they belong. \`group-hover:\` in particular now has somewhere to go: it
810
- means "when the component around me is hovered", which is what a real table row is built
811
- on and what nothing could express before.
812
-
813
- **\`name\` is the part name**, and it is what carries the styles - flat, lowercase, kebab. Never
814
- nested: \`"actions.generate"\` compiles to two CSS classes and is invalid, so name it
815
- \`"generate-action"\`. Name what it IS - \`label\`, \`value\`, \`delta\`, \`cover\`, \`toolbar\` - because the
816
- name becomes a class in their stylesheet and \`div2\` is a name somebody has to live with.
817
-
818
- \`root\`, \`wrapper\`, \`container\`, \`base\` and \`content\` are the element itself: **do not send them as
819
- nodes.** Their styles already sit on the component, and a node for them draws a box inside its own
820
- box.
821
-
822
- A node with no \`classes\` is fine - it still carries structure, which is most of the value. A
823
- \`component\` or \`external\` node takes no \`name\` and no \`classes\`: the styles there are not theirs
824
- to hold.
825
-
826
- ### A component of theirs is a NAMED BLOCK, not an expansion
827
-
828
- When you hit \`<TextEditor>\` inside \`<ArticleCard>\`, send
829
- \`{ "as": "component", "ref": "TextEditor" }\` and **stop**. Do not inline what TextEditor is made
830
- of. It previews as a block carrying its name, and it becomes a **rule** - \`ArticleCard\` +
831
- \`TextEditor\`, a relation, which is the one thing a flat list of prose could never say.
832
-
833
- That edge is a **fact, not a habit**: you saw it in the definition, so it is true once and for
834
- all, and it arrives active without waiting for a third sighting.
835
-
836
- **Send what the PARENT paints on it.** A frontier node takes \`at\` (or \`classes\`) like any other,
837
- and it means something different there: not the child's look, which has a recipe of its own, but
838
- the override the parent applies where it composes the child. Measured on a real library, four of
839
- these were being lost:
840
-
841
- \`\`\`
842
- Paginate <Button className="min-w-[120px] dark:bg-ocean-800 …"> a 120px floor
843
- MetricCard <Card className="flex-1 dark:bg-darkgray-300"> overrides Card's dark
844
- ArticleCard <Tag className="w-fit">
845
- Message <ReactMarkdown className="text-royal-blue-500 underline …">
846
- \`\`\`
847
-
848
- The MetricCard one is the sharpest: losing that override means the preview draws the wrong dark.
849
- Same for an \`external\` node - a library's component still wears what their code puts on it.
850
-
851
- Three things that are NOT edges, and sending them as such would be wrong:
852
-
853
- - \`motion.div\`, \`Dialog.Root\`, \`Radio.Item\` - a library's namespace, not a component of theirs
854
- - an HTML element with a capital in a variable name
855
- - **a name the file binds itself and does not export.** Capitalisation is React's rule for "not an
856
- html tag", which is a different question from "is this a component of the system". Both of these
857
- arrived as components of a real library and neither is one (dono, 01/08):
858
-
859
- \`\`\`tsx
860
- function Text({ as: Component = "p" }) // a prop rename - the polymorphic idiom
861
- const CustomLegend = () => (…) // a closure inside LineChart
862
- \`\`\`
863
-
864
- The test is reachability: a component of the system is imported from somewhere or exported to
865
- somewhere. A name that only lives inside one function body is that function's private wiring,
866
- and giving it a recipe invents a component nobody can import.
867
-
868
- If the component came from a package, it is \`external\`, not \`component\`.
869
-
870
- ### An external dependency does not render, and that is the answer
871
-
872
- A \`TextEditor\` built on tiptap cannot be previewed: the editable region is tiptap's, and drawing
873
- a fake one would be inventing. So it previews as a block naming the package, and the value moves
874
- into the **rules** - which is where an agent will read it anyway.
875
-
876
- A library is never one rule, it is a family, and you are reading the file so you can see all of
877
- them. Send them as normal \`rules\` with \`applies: ["TextEditor"]\` and
878
- \`kind: "implementation"\`:
879
-
880
- \`\`\`
881
- requires @tiptap/react
882
- the editable region is <EditorContent> - never render children into it directly
883
- toolbar buttons go through editor.chain().focus()
884
- extensions are configured at construction, not toggled later
885
- \`\`\`
886
-
887
- **Name the package in the rule; leave the version to the evidence.** The CLI reads the version out
888
- of their manifest and attaches it (\`"^2.1.0 in packages/ui"\`), so the rule does not go stale the
889
- day they upgrade and the information is not lost either.
890
-
891
- ### If \`recipe_vocabulary\` is missing, the MCP registration is stale
892
-
893
- A real run reported "recipe_vocabulary and validate_recipe do not exist on this server" and
894
- sent every anatomy unvalidated - the repo's \`.mcp.json\` pinned \`synthesisui@0.16.78\` while
895
- the import itself ran unpinned at latest: **two CLI versions in one repo, in one run** (dono,
896
- 01/08). Before concluding a tool does not exist, check the registration:
897
-
898
- \`\`\`json
899
- "args": ["synthesisui@latest", "mcp"]
900
- \`\`\`
901
-
902
- Tell the person to fix the pin and restart the server. Do not work around a stale server by
903
- skipping validation - unvalidated is how correct work arrives as nothing.
904
-
905
- ### Ask what a recipe can hold, BEFORE you write one
906
-
907
- \`\`\`
908
- recipe_vocabulary what a recipe CAN hold - states, forms, the floor per kind
909
- validate_recipe(name, r) would this survive? what would NOT, by name
910
- \`\`\`
911
-
912
- Both are MCP tools on the \`synthesisui\` server. **Call \`recipe_vocabulary\` once, at the
913
- start**, and work from what it says rather than from what you remember - it is served from
914
- the live contract, so it is right about a state that was added last week and this file is
915
- not.
916
-
917
- Then **validate every recipe before you send it.** This is not a formality. Nothing checked
918
- a recipe before these tools existed, and four separate readings were lost in silence: a
919
- \`dark:\` inside a variant, a state the compiler cannot spell, a part name with a dot in it,
920
- a layer with an empty style. Each of those was correct work that arrived as nothing.
921
-
922
- **A FIELD OF THE READING IS NOT A FIELD OF THE RECIPE.** \`looks[X].dark\` is how the census
923
- reports a dark half it measured; a recipe has one way to say the same thing, and it is a
924
- layer:
925
-
926
- \`\`\`json
927
- { "when": { "scheme": "dark" }, "style": { "backgroundColor": "{color.darkgray.300}" } }
928
- \`\`\`
929
-
930
- Copying \`dark\` in at the top level of a recipe costs you that block, silently, on every
931
- component that has one - and \`import\` already converts it for the recipes IT writes, so the
932
- only place this can be lost is a recipe you hand-built (not-expressed.md, dono, 03/08).
933
-
934
- **Which means: do NOT pre-convert the dark half inside \`looks[X]\`.** The conversion already
935
- happens on the way in, so adding a \`scheme: "dark"\` layer to the census's own look while leaving
936
- the \`dark\` key in place produces the SAME layer twice - measured at 28 components in one real
937
- import, with the properties in a different key order so it did not even look like a duplicate
938
- (dono, 05/08). \`validate_recipe\` telling you a top-level \`dark\` would be dropped is true about a
939
- RECIPE and says nothing about the reading: you send a reading, and the reading's \`dark\` is read.
940
-
941
- **In ONE call.** Build every recipe in memory, then send the whole reading to
942
- \`validate_recipes\` - one round trip for 35 components instead of 35. \`validate_recipe\`
943
- (singular) is for the one recipe you rewrote after the batch named it; reaching for it in a
944
- loop is the shape this optimisation exists to kill (dono, 01/08).
945
-
946
- \`\`\`
947
- validate_recipes({ recipes: [{ name, recipe }, …] }) the whole reading, one question
948
- validate_recipe(name, recipe) the one you just fixed
949
- \`\`\`
950
-
951
- \`validate_recipe\` never answers yes or no. It answers with what would not survive:
952
-
953
- \`\`\`
954
- REFUSED by the contract fix it or the WHOLE recipe is dropped
955
- HELD BY THE SCHEMA AND the silent half - it validates, it compiles to nothing.
956
- DROPPED BY THE COMPILER This is the one worth fixing before sending.
957
- BELOW THE FLOOR it would arrive and not be drawable
958
- \`\`\`
959
-
960
- ### Write down what we could not hold
961
-
962
- Everything \`validate_recipe\` reports goes into \`_synthesisui/not-expressed.md\`, appended
963
- per component:
964
-
965
- \`\`\`
966
- ## MetricCard
967
- - layers[2].when.scheme … (what the validator said, VERBATIM)
968
- - below the floor: a fill, so it reads as a surface rather than as bare text
969
- - the source you read, quoted, so the gap can be checked without opening their repo
970
- \`\`\`
971
-
972
- **Verbatim, and with the source beside it.** This file is how a gap on OUR side gets fixed
973
- instead of being worked around: said only in chat it evaporates, and paraphrased it becomes
974
- unfindable. If the file ends up empty, say so - that is the good outcome and it is worth
975
- one line.
976
-
977
- ### The floor: what a preview needs before it can draw anything
978
-
979
- The owner asked this directly - "to render the button, what do I need to have?" - and the
980
- honest answer per kind is short. **Work the list for the component's kind and say which
981
- ones their code answers.** A reader holding this knows that a button read with no hover
982
- means the hover has not been found yet, and goes back for it.
983
-
984
- \`\`\`
985
- action the resting fill and the text colour on it · padding and radius ·
986
- text size and weight · hover, focus-visible AND disabled - three looks,
987
- not one · gap and alignment if it can carry an icon
988
- field the fill of the typing area and the text colour in it · the edge at rest
989
- and the edge when focused · padding and radius · the placeholder colour
990
- (a different decision) · the invalid look if their code has one
991
- control the off look and the on look, which IS the component · the size · the
992
- focus ring, since a control is reached by keyboard · the disabled look
993
- surface the fill, and the edge or the elevation that separates it from the page ·
994
- radius and inner padding · the gap between what it holds · whether it
995
- changes at a breakpoint
996
- pill the fill and text colour, per status if it has statuses · radius and
997
- horizontal padding · the text size
998
- indicator the colour, or the two if it has a track and a fill · the size or ratio ·
999
- WHAT DRIVES IT - which prop moves the number
1000
- text the colour · the size and line height, or a note that both are inherited ·
1001
- the weight and family if they differ from body
1002
- \`\`\`
1003
-
1004
- **A component genuinely has none of something, and saying so is a real answer.** This is a
1005
- list to work through, not a gate: nothing is blocked for failing it, and inventing a
1006
- background to satisfy it would be the exact opposite of the point.
1007
-
1008
- There is one hard line under all of it: a recipe that declares NO colour, NO edge and NO
1009
- size cannot be told apart from the page. The platform says so rather than printing a
1010
- score, and it follows the root first - a component whose surface belongs to the \`Card\` it
1011
- returns is complete.
1012
-
1013
- ### Media is not a style question
1014
-
1015
- An \`image\` node draws a grey region and carries nothing about how to use it, so an agent
1016
- composing with it invents the answer. Send these as \`rules\` on that component:
1017
-
1018
- \`\`\`
1019
- a ratio or an explicit size a region with neither collapses before the image loads,
1020
- and the page jumps when it arrives
1021
- an object-fit cover crops and contain letterboxes; which one is right
1022
- is about the content, not a default
1023
- a corner radius an image inside a rounded surface has to say whether it
1024
- follows the curve
1025
- what shows while it loads a skeleton, a blur, or the surface colour
1026
- what shows when it is missing a missing asset must never look broken
1027
- \`\`\`
1028
-
1029
- ### The census is the ONLY source of truth - four universal rules
1030
-
1031
- These hold in ANY project, whatever its folders, and they exist because a run spent most
1032
- of its tokens re-reading what the census already carried (dono, 01/08):
1033
-
1034
- 1. **No iterative sweeps by layer or folder.** Never walk atoms/, then molecules/, then
1035
- organisms/ reading files - the census walked everything once.
1036
- 2. **Batch over the census's own data.** Everything you consume comes out of
1037
- \`_synthesisui/census.json\` - the looks, the sketch, the signals, the coverage.
1038
- 3. **Metadata is one pass, already done.** Theme, fonts, page, libraries, architecture
1039
- arrive counted. You confirm in a sentence; you never re-derive.
1040
- 4. **A file read whose content the census carries is a PIPELINE FAILURE.** Say so in
1041
- not-expressed.md when you catch yourself - that report is how the census grows.
1042
-
1043
- ### Name the parts from the SKETCH - the file is already in the census
1044
-
1045
- Every component's look carries \`sketch\`: its own markup as data - tag, class string,
1046
- depth and text per element, in document order. That is everything you used to open the
1047
- file for, minus the one thing that is genuinely yours: the NAMES.
1048
-
1049
- \`\`\`json
1050
- "sketch": [
1051
- { "tag": "div", "depth": 0, "classes": "rounded-md border ..." },
1052
- { "tag": "button", "depth": 1, "classes": "flex w-full ..." },
1053
- { "tag": "span", "depth": 2, "classes": "size-2 rounded-full bg-success-500" },
1054
- { "tag": "span", "depth": 2, "text": "Status" },
1055
- { "tag": "ChevronDownIcon", "depth": 2 }
1056
- ]
1057
- \`\`\`
1058
-
1059
- Read that and write the anatomy: depth 1 is \`trigger\`, the dot is \`status-dot\`, the
1060
- capitalised tag is a frontier. **Do not open the component file** - if the sketch is
1061
- missing or visibly truncated (150-node cap), say so in not-expressed.md and only then
1062
- read, because that gap is the census's to close.
1063
-
1064
- **NAME THE NODE BY ITS INDEX - never retype the class string.** Each anatomy node carries
1065
- \`"at": <index into this sketch>\`, and the CLI reads the exact string the census measured:
1066
-
1067
- \`\`\`json
1068
- { "as": "button", "name": "trigger", "at": 1 }
1069
- \`\`\`
1070
-
1071
- Retyping is where a modifier goes missing. Six real components arrived with \`hover:\` and
1072
- \`focus:\` in their source and no state at all in the recipe, and the parser was not the
1073
- problem - the copy was (audit, dono, 01/08). An index cannot lose a class. \`classes\` still
1074
- works and still wins when you send both, which is for the one node the sketch could not
1075
- reach; every other node uses \`at\`.
1076
-
1077
- ### What the CLI now reads for you, so do not spend a turn on it
1078
-
1079
- **The rule under all of this: you are given EVIDENCE and asked to DECIDE.** You are never
1080
- given a question and asked to search. A grep is a tool call whose entire output lands in your
1081
- context to answer something that is one number, and two readers sweeping the same repo bring
1082
- back different slices - so the answer changes between runs, which is the part that actually
1083
- costs.
1084
-
1085
- Measured on a real run: the census counts the theme signals, the libraries and their
1086
- versions, and the conflicts. The reader that swept for them anyway spent a dozen shell calls
1087
- to arrive at a subset of what was already on screen.
1088
-
1089
- Four things are read deterministically from the source. Reporting them back as if you
1090
- found them wastes a turn and risks contradicting the measurement:
1091
-
1092
- - **variant and state styles** out of \`cva\`, \`tv\` and \`cond === "x" && "classes"\` -
1093
- including a hover PER variant, compound variants, and \`[var(--their-token)]\`
1094
- - **whether a component forwards the rest of its props**, and which it names
1095
- - **which components appear inside which** at their call sites, as pairs
1096
- - **the folder architecture**, and it will ask you which one has priority
1097
- - **their gradients**, as tokens: \`--gradient-ui\` and its Tailwind-namespace twin land on
1098
- \`{gradients.ui}\`, composed \`bg-gradient-to-r from-x to-y\` stops become one gradient in
1099
- their own colour tokens, and a signature gradient is no longer a string trapped in class
1100
- names
1101
- - **the theme**: how many dark utilities, in how many files, which library switches it, which
1102
- attribute it is bound to, what \`<html>\` carries, and how many \`prefers-color-scheme\` rules
1103
- - **the libraries and their versions** - motion, icons, primitives, charts, editors - with the
1104
- file count for each, and a CONFLICT when one job is done by more than one of them
1105
- - **which of your components are ours**, matched against the LIVE catalogue rather than a
1106
- list baked into the CLI. If a run says it fell back to the built-in names, the match is a
1107
- smaller answer than it should be and the reason is on screen
1108
-
1109
- What is still yours: the anatomy tree, the part names, the frontiers, the roles, the
1110
- concept, and the rules that are not about a class name.
1111
-
1112
- ### A scale's INTENT is a rule, and their docs already wrote it
1113
-
1114
- A three-step spacing scale with documented meaning - sm "close UI relationships", md
1115
- "standard internal padding", lg "layout gutters" - carries a law nothing else expresses:
1116
- **this system has exactly three steps, and a fourth is a mistake.** A real import read the
1117
- scale and lost the sentence (dono, 01/08).
1118
-
1119
- When their docs (a \`Geometry.mdx\`, a design-tokens page, comments beside the \`@theme\`)
1120
- state what a step is FOR, send it as system-wide rules - \`applies: []\`, \`fact: true\`, the
1121
- doc as evidence.
1122
-
1123
- **One doc pass, and only for what the census cannot hold.** A scale's INTENT is prose, so it
1124
- genuinely needs a human sentence somebody wrote - but a scale's VALUES, its token names, the
1125
- theme, the class convention and the architecture are all measured and sitting in \`declared\`,
1126
- \`signals\` and \`conventions\`. Opening a docs page to confirm a number the census already
1127
- carries costs a turn and answers nothing. Read the docs ONCE, for the sentences; never in a
1128
- loop, and never to check arithmetic.
1129
-
1130
- Send it like this:
1131
-
1132
- \`\`\`
1133
- this system's spacing has exactly three steps - sm for close relationships inside a
1134
- control, md for standard internal padding, lg for layout gutters. A fourth step is a
1135
- mistake, not a gap (Geometry.mdx)
1136
- \`\`\`
1137
-
1138
- \`applies: []\` is what makes it reach CLAUDE.md and every prompt after - the same route the
1139
- architecture rule travels.
1140
-
1141
- ### How much to send
1142
-
1143
- Send an anatomy for **every component with visible structure**, which is nearly all of them. A
1144
- \`Chat\` has a message list and a composer; a \`MetricCard\` has a label and a value; a
1145
- \`CircularProgress\` has a track and a fill. Sending none is right only for something genuinely
1146
- undivided - a \`Divider\`, a \`Spacer\`.
1147
-
1148
- **There is no node budget, and the one I gave you was wrong.** "Three to six nodes is a
1149
- recognisable component" cost a real \`ArticleCard\` most of itself: it has a carousel with arrows,
1150
- three image buttons, a source chip, a refresh, a rich body, a source line and a "Similar
1151
- published content" region with two selects and two buttons - about fourteen regions - and the
1152
- reading came back with six because the guidance said so (dono, 01/08).
1153
-
1154
- **One node per region a person can point at.** If they can say "that part", it is a node. What
1155
- you still collapse is a wrapper whose only job is \`flex\` - fold it into the \`row\` it already is,
1156
- because that is not a region, it is plumbing.
1157
-
1158
- The cost of sending none is not neutral. A component with no anatomy previews as a grey box with
1159
- a sentence in it, or - if its kind is \`indicator\` - as a small blank shape. A component with a
1160
- real tree previews as itself, and its spec view shows what the system understood. That is the
1161
- whole reason this field exists, and skipping it quietly is how a real library came back looking
1162
- empty (dono, 01/08).
1163
-
1164
- The older flat form - \`"parts": [{ "name": "label", "classes": "..." }]\` - still works and still
1165
- styles correctly. It just carries no shape, so the preview lays the parts out in a row and the
1166
- spec view has no nesting to draw.
1167
-
1168
- ### 4. Send it
1169
-
1170
- Everything is answered and everything is validated:
1171
-
1172
- \`\`\`
1173
- npx synthesisui import --census _synthesisui/census.json --name "<the name>" [--registry <from step 0>]
1174
- \`\`\`
1175
-
1176
- **This is the one real write.** Everything before it is on their disk and costs nothing to redo.
1177
-
1178
- Pass every answer explicitly - \`--name\`, \`--scope\` and \`--usage\` if they changed either, and the reading rewritten
1179
- with their primary and their theme. The CLI cannot ask anything when you are the one running it:
1180
- it has no terminal, so its own prompts are skipped by design. **Whatever you did not ask, nobody
1181
- asked.** Skip 2a and the first time they learn where their system came from is when they open
1182
- it.
1183
-
1184
- If it refuses, it says which of the three it is: no session, an expired one, or a token issued
1185
- by a different host. All are recoverable from the census on disk; none need a re-measure.
1186
-
1187
- ### 5. Tell them exactly what they have, and what is theirs to decide
1188
-
1189
- The import publishes **v1** as a faithful baseline and opens a **v2 draft** holding the
1190
- proposal. Say all of this in your own words - do not just print the link:
1191
-
1192
- **What landed in v1**, and it is theirs, not ours:
1193
-
1194
- - the scheme it opens in, and whether a second one was built
1195
- - their ramps under their own family names
1196
- - their exclusive components as contracts - **the axes their types declare, the look transcribed
1197
- out of their own class names where it was readable, and the anatomy you sent.** Say what is
1198
- still empty out loud, because a half-written recipe looks like a failure until someone explains
1199
- which half was deliberate: nothing was invented, so a component whose classes named no token
1200
- arrives with structure and no colour. The platform shows that as *Not written yet*, not as a
1201
- zero.
1202
- - **their own ladder**, if their repo has one. Components under \`atoms/\`, \`molecules/\` and
1203
- \`organisms/\` arrive grouped that way in the vitrine, read off the folder rather than guessed
1204
- from a name. You do not send this and you should not try to: a project that does not organise
1205
- itself this way gets one honest group instead of three invented ones.
1206
- - **the shape of each component**, if you sent one: its parts nested as they nest in their code,
1207
- the components of theirs it composes, and the libraries it needs. Each edge is also a rule now,
1208
- which is what makes it survive being looked at once.
1209
-
1210
- **What is waiting in v2**, which only they can approve: near-duplicate colours collapsed,
1211
- unnamed heavy hitters given a place. Point at the link the CLI printed and name the two or
1212
- three biggest items so they know whether it is worth opening now.
1213
-
1214
- **What the report said no to.** The notes carry every silent drop made loud - a value not
1215
- found in their code, a token that is not ramp-shaped, a name the document refused. Read them
1216
- and pass on the ones that matter.
1217
-
1218
- **And the notes now judge YOUR reading, which nothing did before.** \`validate_recipes\` looks at
1219
- the \`anatomy\` inside a recipe; the anatomy you author travels in \`reading.components[X].anatomy\`,
1220
- and until 05/08 no one looked at it - so a note may name findings the batch validator never saw.
1221
- Two kinds, and both are yours to act on:
1222
-
1223
- - **structure that does not survive the reader.** Nine of these landed on a real import, every one
1224
- the same shape: a \`button\` holding a \`stack\`. Only \`row\` and \`stack\` arrange, so the children are
1225
- hoisted out and become siblings - the nesting they wrote is gone. Fix it in the reading and send
1226
- again; the note tells you the component and the path.
1227
- - **references pointing at components the system does not contain.** 215 of 660 on that same
1228
- import. This one is usually NOT a mistake of yours: their component composes something the gate
1229
- left out, or something outside the scope that was measured. Say the number to the person and name
1230
- the top few, because a specimen there draws the reference and nothing else - and if the missing
1231
- piece is central to their system, the answer is a wider \`--scope\`, not a rewritten reading.
1232
-
1233
- ### 6. Close the loop in their repo
1234
-
1235
- The system is only worth something once it governs the code it came from:
1236
-
1237
- \`\`\`
1238
- npx synthesisui add <slug> [--registry …] # INSTALLS it: writes _synthesisui/ds/<slug>/
1239
- npx synthesisui doctor [paths…] # the contract starts being enforced
1240
- npx synthesisui upgrade <slug> [--registry …] # LATER, when a new version is published
1241
- \`\`\`
1242
-
1243
- **The order is not a preference: \`upgrade\` reads the \`.lock\` of an installed DS and refuses
1244
- without one** - \`"<slug>" is not installed here - run \\\`synthesisui add <slug>\\\` first.\` So
1245
- offering \`upgrade\` to somebody who has never installed hands them a command that fails on contact
1246
- (dono, 05/08). Check for \`_synthesisui/ds/<slug>/.lock\` before you recommend either one.
1247
-
1248
- And say what each writes, because one of them rewrites code: \`add\` and \`upgrade\` write inside
1249
- \`_synthesisui/\`, and \`upgrade\` ALSO regenerates components - but only files carrying the
1250
- \`Generated by SynthesisUI\` header, so their own components are never touched.
1251
-
1252
- \`doctor\` is where the promise pays off: every design value written by hand, the token their
1253
- own system already has for it, and the components used outside the axes their contract
1254
- declares. In a monorepo, scope it per app - one system, N consumers.
1255
-
1256
- ## What good looks like
1257
-
1258
- The user should end up with a system that reads like theirs and not like ours:
1259
-
1260
- - **their names travel** - \`vivid-pink\`, \`darkgray-900\`, whatever they called it
1261
- - **their default face** - a dark product opens dark
1262
- - **no borrowed colour** - if it is in the system, it is in their code
1263
- - **their exclusive components arrive as contracts** - the axes declared, the look transcribed and
1264
- never invented, and the anatomy read out of their own JSX
1265
- - **a component previews as itself** - an \`ArticleCard\` shows a cover, a title and a footer of
1266
- buttons, and says out loud that the editor in the middle is their \`TextEditor\` on tiptap
1267
- - **the duplicates are named out loud** - three components that all read as a badge, a brand
1268
- colour painted 275 times with no token
1269
-
1270
- ## What to resist
1271
-
1272
- - **Do not let memory speak for the server.** Notes report the past; a claim about what exists
1273
- now needs the one \`curl\` in step 0b. Four statements in one real run were wrong this way, and
1274
- every option offered on top of them was wrong too (dono, 05/08).
1275
- - **Do not run \`import\` without \`--dry\` to explore.** It creates a real system on their
1276
- account. Measure with \`--dry\`, read, then send once.
1277
- - **\`--dry\` does not delete anything.** It writes \`census.json\` and creates the folder if needed -
1278
- there is no \`rm\` in the import path. What it DOES do is overwrite that one file, so a reading
1279
- injected into it is lost; everything else under \`_synthesisui/\` (the config, \`ds/<slug>/\`, the
1280
- hook's marker, \`not-expressed.md\`) survives untouched. Telling somebody a measure will wipe their
1281
- install talks them out of a safe command (dono, 05/08).
1282
- - **Do not redo the arithmetic.** If you find yourself counting colours, you are in the wrong
1283
- half of the pipeline.
1284
- - **Do not fill \`roles\` with what looks nice.** It is a reading of what their code already
1285
- does, not a redesign. The v2 proposal is where improvement belongs, and the user approves it.
1286
- - **Do not guess \`themes\` to be helpful.** Omitting it falls back to counting rungs, which is
1287
- a guess the product labels as a guess. A wrong confident answer does not get labelled.
1288
- - **Do not apologise for the empty contracts.** They are the design. Explain them.
1289
- - **Do not come back with a question once step 3 has started.** Everything answerable was
1290
- answered in step 2. An interruption in the middle of a silent phase costs the person the
1291
- whole phase, because they now have to hold what you were doing in their head.
1292
- - **Do not narrate the mechanics.** No grep, no glob, no "let me check whether X exists", no
1293
- draft you rewrote. Findings earn a line; the machine talking to itself does not.
1294
- - **Do not open a file to confirm something the census already carries.** \`signals\`, \`sketch\`,
1295
- \`declared\`, \`conventions\` and \`coverage\` are the answer to almost every "let me just check".
1296
- - **Do not validate in a loop.** One \`validate_recipes\` for the whole reading.
1297
-
1298
- ## Not yet true
1299
-
1300
- Say so if it comes up, rather than implying otherwise:
1301
-
1302
- - **The v2 proposal covers colour only.** Near-duplicate collapse and unnamed heavy hitters.
1303
- It does not yet propose swapping a raw value in a recipe for the token that holds it, nor
1304
- retiring an option nothing passes, nor a categorical chart palette - all measured, none
1305
- offered.
1306
- - **You do not write recipes.** Exclusive components arrive as contracts and stay that way
1307
- until someone writes the look. When that lands, the rule will be **provenance, not
1308
- membership**: a value transcribed out of their file cites the file and line, and is
1309
- legitimate even if the census never saw it, because the census measures less than a project
1310
- contains.
1311
- - **Nothing confirms the import back.** You send, the CLI prints a link, and nobody asks the
1312
- platform whether what arrived is what you meant. Until that exists, tell them to open the
1313
- link and check.
1314
- `;
1315
- /** Where it lands in the consumer's repo. */
1316
14
  export const IMPORT_SKILL_PATH = ".claude/skills/sui-import-ds/SKILL.md";
15
+ export const IMPORT_SKILL = '---\nname: sui-import-ds\ndescription: Turn a codebase the user ALREADY has into a SynthesisUI design system. Use when someone points at an existing repo, app or component library and asks to import it, adopt it, bring it in, or "make a design system from this" (e.g. "/sui-import-ds", "importa o meu packages/ui", "turn this app into a design system"). Drives the full pipeline - census \u2192 your reading \u2192 import \u2192 v2 proposal - and answers the questions arithmetic cannot.\n---\n\n# Import Design System - served live\n\nThis playbook is served from the platform, not shipped in this file - it is\nalways current, and your context only carries the step you are on.\n\n1. Call the `playbook` tool on the `synthesisui` MCP server with\n { "skill": "import" } - you get the framing and a table of contents.\n2. Fetch ONLY the chapter for the step you are on:\n { "skill": "import", "section": "<id from the toc>" }. Never fetch more\n than the current step needs.\n3. Follow it exactly. When the step is done, fetch the next chapter.\n\nIf the tool answers that you are not signed in, run `npx synthesisui login`\nin the terminal and call it again. If the `synthesisui` MCP server is not\navailable at all, run `npx synthesisui connect`, restart the session, and\ninvoke this skill again.\n';