synthesisui 0.16.238 → 0.16.240

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.
@@ -417,6 +417,7 @@ sxSpacing) {
417
417
  (kind === "headless" || kind === "motion" || kind === "icon")) {
418
418
  node_.from = measured.from;
419
419
  node_.refName = measured.tag;
420
+ // a versão vem do apanhador geral abaixo - um lugar só, depois de todos os ramos
420
421
  }
421
422
  }
422
423
  /**
@@ -431,6 +432,20 @@ sxSpacing) {
431
432
  if (measured?.motionProps)
432
433
  node_.motionProps = measured.motionProps;
433
434
  }
435
+ /**
436
+ * TODO NÓ COM PACOTE LEVA A VERSÃO - o apanhador geral, depois de todos os ramos.
437
+ *
438
+ * A costura já falhou QUATRO vezes ramo a ramo (frontier-kind, import 09/08, o ramo de
439
+ * fronteira, o ramo icon/motion), porque cada caminho novo que carimba `from` esquecia de
440
+ * carimbar a versão. Medido no caminho DERIVADO em 16/08: base-ui com 1 de 23 nós
441
+ * versionados e lucide com 11 de 45 - os nós nasciam no `anatomyFromSketch` com `from` e
442
+ * sem versão, e nenhum ramo daqui os alcançava. Um lugar só, depois de todos.
443
+ */
444
+ if (node_.from && !node_.version) {
445
+ const v = deps?.[packageRoot(String(node_.from))] ?? deps?.[String(node_.from)];
446
+ if (v)
447
+ node_.version = v;
448
+ }
434
449
  // The NAME is what carries the styles, so it is claimed even when nothing
435
450
  // resolved: the tree triggers on presence, and structure is most of the
436
451
  // value. A node with no name is pure structure and that is legitimate.
package/dist/claude-md.js CHANGED
@@ -186,9 +186,9 @@ session. It costs nothing when nothing changed: the tool holds both versions and
186
186
  refuses politely when there is nothing to refresh, so calling it is cheaper than
187
187
  deciding whether to.
188
188
 
189
- It re-measures this repo with the current reader, reuses the reading that was already
190
- authored, and sends it - a newer reader sees styles the old one could not, and until
191
- it runs, this project's system is missing them.
189
+ It re-measures this repo with the current reader, reuses what was already written
190
+ about this app, and sends it - a newer reader sees styles the old one could not, and
191
+ until it runs, this project's system is missing them.
192
192
 
193
193
  It never asks anyone anything, and it never overwrites a recipe a person wrote on
194
194
  the screen: when a send would rewrite one, it refuses and names them. If it says
@@ -196,8 +196,8 @@ nothing changed, nothing changed - carry on.
196
196
 
197
197
  **And when you describe this system, open with the age of the measurement:** the
198
198
  \`ledger.cli\` and the date in \`_synthesisui/census.json\`. A long, correct report
199
- about a census taken by an older reader is the worst way to be right - every absence
200
- in it may have been filled since, and the reader who trusts it has no way to know.
199
+ about a measurement taken by an older reader is the worst way to be right - every
200
+ absence in it may have been filled since, and the reader who trusts it has no way to know.
201
201
 
202
202
  **And if it says the design system needs updating, do these three things in order:**
203
203
  say the one line, ask, and run it.
@@ -334,7 +334,8 @@ extend that. The person who asked you for a feature should never have to know th
334
334
  names or type them; finding the right component is your job, not theirs.
335
335
 
336
336
  Overriding a style a component already sets just works - write
337
- \`className="text-info"\`, with no \`!\`. The generated components resolve your class
337
+ \`className="text-<role>"\` with one of this system's own colour roles (the GUIDE
338
+ lists them), with no \`!\`. The generated components resolve your class
338
339
  against their own, so the last one written wins, which is what the call site reads like.
339
340
  If an override is ignored, that component predates the resolver: regenerate it with
340
341
  \`npx synthesisui@latest component <slug> <name>\` rather than reaching for \`!\`.
@@ -134,6 +134,14 @@ async function findTokens(root, only) {
134
134
  /** The contract. Names THEIR tokens - there is no translation layer. */
135
135
  function guide(name, slug, tokens) {
136
136
  const lines = [...tokens.entries()].map(([k, v]) => ` ${k}: ${v};`);
137
+ /**
138
+ * O EXEMPLO É UM TOKEN REAL DELE. A promessa três linhas abaixo é "YOUR tokens, under YOUR
139
+ * names" - e o exemplo inventava `--<slug>-color-primary`, um prefixo derivado por nós + um papel
140
+ * nosso, que pode não existir no CSS de ninguém. O primeiro token de cor DELE cumpre a promessa.
141
+ */
142
+ const example = [...tokens.entries()].find(([, v]) => /^#|^(rgb|hsl|oklch|color)\(/i.test(v.trim()))?.[0] ??
143
+ [...tokens.keys()][0] ??
144
+ `--${slug}-color-primary`;
137
145
  return `# ${name} - the contract your agent follows
138
146
 
139
147
  Adopted from this repository by \`synthesisui adopt\`. These are YOUR tokens,
@@ -145,7 +153,7 @@ When writing or editing UI in this project, use these custom properties.
145
153
  Do not write raw colours, spacings or radii that a token already covers.
146
154
 
147
155
  ✗ background: #3b82f6
148
- ✓ background: var(--${slug}-color-primary)
156
+ ✓ background: var(${example})
149
157
 
150
158
  If a value has no token, say so instead of inventing one silently - a new
151
159
  token is a decision for a person to make.
@@ -1179,17 +1179,21 @@ export async function doctor(opts) {
1179
1179
  const w = Math.max(...repeats.map((r) => r.literal.length));
1180
1180
  for (const r of repeats) {
1181
1181
  const where = `${r.count}\u00d7 in ${r.files} file${r.files === 1 ? "" : "s"}`;
1182
- const near = nameToWrite(r) ? null : nearestToken(table, r.literal);
1182
+ const near = nameToWrite(r)
1183
+ ? null
1184
+ : nearestToken(table, r.literal, r.kind);
1183
1185
  /**
1184
1186
  * COINCIDÊNCIA NÃO É RESPOSTA - ver `crossFamily` em `scan.ts`. Para aquele `kind` o
1185
1187
  * sistema não tem nome; o valor mora noutra família, e dizer só a seta afirma o contrário.
1188
+ * MAS quando o REPO DELE nomeia o valor (`theirToken`), esse nome vale - `apply-fix.ts:140`
1189
+ * e a classificação de sugestão já tratam assim; só a impressão tinha ficado para trás.
1186
1190
  */
1187
- const named = r.crossFamily
1191
+ const named = r.crossFamily && !r.theirToken
1188
1192
  ? ` → no ${r.kind} named for it · the value lives as ${r.token}`
1189
1193
  : nameToWrite(r)
1190
1194
  ? ` → ${nameToWrite(r)}`
1191
1195
  : near
1192
- ? ` → nearest is ${near.name} (${near.value})`
1196
+ ? ` → nearest is ${near.theirs ?? near.name} (${near.value})`
1193
1197
  : "";
1194
1198
  say(` ${r.literal.padEnd(w)} ${where}${named}`);
1195
1199
  }
@@ -1205,13 +1209,15 @@ export async function doctor(opts) {
1205
1209
  for (const x of shown) {
1206
1210
  // A dead end with a neighbour is not a dead end. Only for lengths -
1207
1211
  // "nearly the same blue" is the guess this tool must never make.
1208
- const near = nameToWrite(x) ? null : nearestToken(table, x.literal);
1209
- const named = x.crossFamily
1212
+ const near = nameToWrite(x)
1213
+ ? null
1214
+ : nearestToken(table, x.literal, x.kind);
1215
+ const named = x.crossFamily && !x.theirToken
1210
1216
  ? `→ no ${x.kind} named for it · the value lives as ${x.token}`
1211
1217
  : nameToWrite(x)
1212
1218
  ? `→ ${nameToWrite(x)}`
1213
1219
  : near
1214
- ? `→ nearest is ${near.name} (${near.value})`
1220
+ ? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
1215
1221
  : "→ no token holds this value yet";
1216
1222
  say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
1217
1223
  }
@@ -1308,13 +1314,14 @@ export async function doctor(opts) {
1308
1314
  if (frozen.length > 0) {
1309
1315
  say(section("These will not follow your other scheme"));
1310
1316
  say(body(frozen.length === 1
1311
- ? "One recipe names a primitive where a SURFACE role of yours holds the same value."
1312
- : `${frozen.length} recipes name a primitive where a SURFACE role of yours holds the same value.`));
1317
+ ? "One recipe pins a fixed shade where a surface colour of this system holds the same value."
1318
+ : `${frozen.length} recipes pin a fixed shade where a surface colour of this system holds the same value.`));
1313
1319
  say("");
1314
1320
  for (const f of frozen) {
1315
1321
  say(body(`ds-${f.component} · ${f.where}`));
1316
1322
  say(` binds ${f.wrote}`);
1317
- say(` {color.semantic.${f.role}} holds that, and becomes ${f.becomes}`);
1323
+ /** A grafia que EXISTE no repo dele - o CSS compilado carrega esta variável; `{color.semantic.*}` é formato nosso e não sai. */
1324
+ say(` var(--ds-color-semantic-${f.role}) holds that, and becomes ${f.becomes}`);
1318
1325
  say("");
1319
1326
  }
1320
1327
  say(body("The value resolves and the CSS compiles, so nothing"));
@@ -1557,7 +1564,7 @@ export async function doctor(opts) {
1557
1564
  });
1558
1565
  }
1559
1566
  for (const r of unnamed.slice(0, migrating ? 3 : 2)) {
1560
- const near = nearestToken(table, r.literal);
1567
+ const near = nearestToken(table, r.literal, r.kind);
1561
1568
  plan.push({
1562
1569
  rank: migrating ? 3 : 4,
1563
1570
  /**
@@ -1567,7 +1574,7 @@ export async function doctor(opts) {
1567
1574
  * fila da anterior: aquela tem nome esperando, esta precisa de um.
1568
1575
  */
1569
1576
  what: near
1570
- ? `${r.literal} - name it, or snap to ${near.name}`
1577
+ ? `${r.literal} - name it, or snap to ${near.theirs ?? near.name}`
1571
1578
  : r.crossFamily
1572
1579
  ? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
1573
1580
  : `${r.literal} - no name for it, here or in your CSS`,
@@ -1753,7 +1760,8 @@ export async function doctor(opts) {
1753
1760
  const verdict = compareToBaseline(d, base, {
1754
1761
  ...(relScopes.length ? { scope: relScopes.join(",") } : {}),
1755
1762
  });
1756
- console.log(section(verdict.worse ? "Drift went up" : "Ratchet"));
1763
+ /** "Drift" é a nossa palavra e não sai (ver a tradução da seção principal); aqui idem. */
1764
+ console.log(section(verdict.worse ? "Hand-written values went up" : "Holding the line"));
1757
1765
  for (const line of describeVerdict(verdict))
1758
1766
  console.log(body(line));
1759
1767
  if (verdict.worse)
@@ -22,7 +22,7 @@ export async function gaps(opts) {
22
22
  const raw = await readFile(path, "utf8").catch(() => null);
23
23
  console.log(section("What this pipeline did not read"));
24
24
  if (!raw) {
25
- console.log(body(`No census at ${path}. Run \`synthesisui import --dry\` first - that is what measures your files, and it writes the census there.`));
25
+ console.log(body(`No measurement at ${path}. Run \`synthesisui import --dry\` first - that is what measures your files, and it writes the result there.`));
26
26
  return;
27
27
  }
28
28
  let ledger;
@@ -39,7 +39,7 @@ export async function gaps(opts) {
39
39
  * contar.
40
40
  */
41
41
  if (!ledger) {
42
- console.log(body(`This census was written by a CLI that did not count style fragments yet, so the numbers do not exist rather than being zero. Run \`synthesisui import --dry\` again with ${opts.cli} to measure.`));
42
+ console.log(body(`This measurement was written by a tool version that did not count style fragments yet, so the numbers do not exist rather than being zero. Run \`synthesisui import --dry\` again with ${opts.cli} to measure.`));
43
43
  return;
44
44
  }
45
45
  for (const line of describeTriage(triageLedger(ledger, opts.cli)))
@@ -36,7 +36,7 @@ import { frontierKind, packageRoot } from "../frontier-kind.js";
36
36
  import { withLibraryStructure } from "../library-structure.js";
37
37
  import { body, paint, section } from "../output.js";
38
38
  import { phase, startProgress } from "../progress.js";
39
- import { detectStack, resolveDeps } from "../stack.js";
39
+ import { detectStack, resolveDeps, stackVersions } from "../stack.js";
40
40
  import { walk, walkAll } from "./doctor.js";
41
41
  /**
42
42
  * How many distinct values travel, PER KIND.
@@ -1739,18 +1739,23 @@ export async function takeCensus(root, opts) {
1739
1739
  }));
1740
1740
  const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
1741
1741
  let name = null;
1742
- /** Their pinned ranges, so a library signal cites the version rather than going stale. */
1743
- let versions = {};
1744
1742
  if (pkgRaw) {
1745
1743
  try {
1746
- const pkg = JSON.parse(pkgRaw);
1747
- name = pkg.name ?? null;
1748
- versions = { ...pkg.devDependencies, ...pkg.dependencies };
1744
+ name = JSON.parse(pkgRaw).name ?? null;
1749
1745
  }
1750
1746
  catch {
1751
1747
  // an unreadable package.json costs the name, not the run
1752
1748
  }
1753
1749
  }
1750
+ /**
1751
+ * Their pinned ranges, so a library signal cites the version rather than going stale.
1752
+ *
1753
+ * PELO RESOLVEDOR, nunca pela leitura crua de um nível: com `--scope packages/ui` este `root` é a
1754
+ * pasta do escopo, cujo manifesto tem ZERO deps - e 15 de 15 bibliotecas do censo vivo saíam sem
1755
+ * versão (medido 16/08). `resolveDeps` sobe até a raiz do workspace e anda para os irmãos que os
1756
+ * `workspaces` declaram, que é onde recharts/chakra/mui realmente moram.
1757
+ */
1758
+ const versions = await resolveDeps(root).catch(() => ({}));
1754
1759
  finishSignals(signals, importTally, versions);
1755
1760
  // Every name their CSS defines, from the same harvest the token table came
1756
1761
  // from - so a reference is judged against what actually exists, not against
@@ -1862,7 +1867,13 @@ export async function takeCensus(root, opts) {
1862
1867
  const classStyle = detectClassStyle(sources);
1863
1868
  return {
1864
1869
  census: 1,
1865
- project: { name, stack: await detectStack(root) },
1870
+ project: {
1871
+ name,
1872
+ stack: await detectStack(root),
1873
+ ...(Object.keys(stackVersions(versions)).length > 0
1874
+ ? { versions: stackVersions(versions) }
1875
+ : {}),
1876
+ },
1866
1877
  declared: Object.fromEntries(table.byName),
1867
1878
  ...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
1868
1879
  ...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
@@ -3113,7 +3124,7 @@ export async function runImport(opts) {
3113
3124
  if (creds && !sameRegistry(creds.registry, base)) {
3114
3125
  console.log("");
3115
3126
  console.log(body(`You are logged in to ${paint.strong(creds.registry ?? "another registry")}, and this would go to ${paint.strong(base)}.`));
3116
- console.log(body("The census is on disk. Either name the one you meant:"));
3127
+ console.log(body(`Your measurement is saved at ${out}. Either name the one you meant:`));
3117
3128
  console.log("");
3118
3129
  console.log(body(` synthesisui import --census ${out} --registry ${creds.registry}`));
3119
3130
  console.log(body("or log in to this one:"));
@@ -3143,7 +3154,7 @@ export async function runImport(opts) {
3143
3154
  // which of the two it was (dono, 31/07). A stale token gets past the
3144
3155
  // `readToken` guard above and only fails here.
3145
3156
  if (res?.status === 401) {
3146
- console.log(body("Your session has expired. The census is on disk."));
3157
+ console.log(body(`Your session has expired. Your measurement is saved at ${out}.`));
3147
3158
  console.log("");
3148
3159
  console.log(body(` ${paint.strong("synthesisui login")}`));
3149
3160
  console.log(body(` synthesisui import --census ${out}${chosen ? ` --name "${chosen}"` : ""}`));
@@ -3152,7 +3163,7 @@ export async function runImport(opts) {
3152
3163
  }
3153
3164
  console.log(body(res
3154
3165
  ? `The registry refused it (HTTP ${res.status})${detail ? `: ${detail}` : "."}`
3155
- : "Could not reach the registry. The census is on disk - try again later."));
3166
+ : `Could not reach the registry. Your measurement is saved at ${out} - try again later.`));
3156
3167
  console.log("");
3157
3168
  return;
3158
3169
  }
@@ -6,7 +6,7 @@ import { readToken, resolveRegistry } from "../config.js";
6
6
  import { readEvents } from "../doctor/ledger.js";
7
7
  import { fileRequest } from "../doctor/requests.js";
8
8
  import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
9
- import { nearestToken, tokenFor } from "../doctor/tokens.js";
9
+ import { nearestToken, normalizeValue, tokenFor } from "../doctor/tokens.js";
10
10
  import { repoStateOf } from "../repo-state.js";
11
11
  import { component } from "./component.js";
12
12
  import { loadSystem, walkAll } from "./doctor.js";
@@ -290,12 +290,29 @@ async function findToken(root, value) {
290
290
  const { table } = await loadSystem(root);
291
291
  if (table.byName.size === 0)
292
292
  return "No design system installed here.";
293
+ /**
294
+ * O NOME DELE PRIMEIRO, como no doctor (`nameToWrite`): `loadSystem` já devolve a tabela COM os
295
+ * aliases dele (`withTheirNames`) e este era o único leitor que nunca os consultava. Sem o `kind`
296
+ * do contexto, a busca varre as famílias - um alias em qualquer uma delas é o nome que o repo
297
+ * DELE dá àquele valor.
298
+ */
299
+ const theirNameFor = (v) => {
300
+ const norm = normalizeValue(v, table.rootPx);
301
+ for (const [key, name] of table.aliases)
302
+ if (key.endsWith(`:${norm}`))
303
+ return name;
304
+ return null;
305
+ };
293
306
  const exact = tokenFor(table, value);
294
- if (exact)
295
- return `${value} is ${exact} in this system. Use var(${exact}).`;
307
+ if (exact) {
308
+ const shown = theirNameFor(value) ?? exact;
309
+ return `${value} is ${shown} in this project. Use var(${shown}).`;
310
+ }
296
311
  const near = nearestToken(table, value);
297
- if (near)
298
- return `No token holds ${value}. The closest is ${near.name} at ${near.value}. If that is what you meant, use it - if it genuinely is not, say so rather than inventing a token.`;
312
+ if (near) {
313
+ const shown = theirNameFor(near.value) ?? near.name;
314
+ return `No token holds ${value}. The closest is ${shown} at ${near.value}. If that is what you meant, use it - if it genuinely is not, say so rather than inventing a token.`;
315
+ }
299
316
  // Same words the managed block uses. A refusal is only useful if it is the
300
317
  // same refusal every time.
301
318
  return `No token in this system holds ${value}, and nothing is close. Do NOT invent one. Say which value you need and what you would call it, and let a person decide.`;
@@ -269,9 +269,9 @@ export async function remeasure(args) {
269
269
  }
270
270
  }
271
271
  console.log(section("Measuring your repository again"));
272
- console.log(body(paint.faint(`${scope ? `scope ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
273
- ? " · read from your own census, which is where the last measurement recorded it"
274
- : ""} · the reading you already authored travels unchanged`)));
272
+ console.log(body(paint.faint(`${scope ? `reading ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
273
+ ? " · read from _synthesisui/census.json, where the last measurement recorded it"
274
+ : ""} · what you already wrote about your app travels unchanged`)));
275
275
  const census = await takeCensus(scope ? join(root, scope) : root, {
276
276
  ...(scope ? { scopeLabel: scope } : {}),
277
277
  ...(args.cli ? { cli: args.cli } : {}),
@@ -333,7 +333,7 @@ export async function remeasure(args) {
333
333
  * repositório inteiro e o rascunho curado de 37 vira 339).
334
334
  */
335
335
  if (!fresh.scope && local.system)
336
- console.log(body(`⚠ the census was written without a scope while "${local.system}" is the one recorded - the next re-measure would read the whole repo. Please report this: it is a state we have not reproduced.`));
336
+ console.log(body(`⚠ the last measurement was saved without the folder it read, while "${local.system}" is the one recorded - the next one would read the whole repo. Please report this: it is a state we have not reproduced.`));
337
337
  if (usage.length > 0)
338
338
  fresh.usage = usage;
339
339
  for (const key of ["scheme", "reading"])
@@ -57,14 +57,43 @@ export async function use(slug, intent, opts) {
57
57
  : "",
58
58
  `- ${base}/v${version}/GUIDE.md - how to apply the system, the recipes and the token vocabulary`,
59
59
  ].filter(Boolean);
60
+ /**
61
+ * OS EXEMPLOS SÃO DO SISTEMA DELE, não uma lista nossa de cor. `bg-primary`/`p-md` cravados
62
+ * ensinavam classes que um sistema importado pode nem compilar - os nomes reais moram no
63
+ * `design-system.json` instalado, e é dele que os exemplos saem. Ilegível, ficam os genéricos.
64
+ */
65
+ let utilities = "`bg-primary`, `text-foreground`, `p-md`, `rounded-lg`, `font-display`";
66
+ try {
67
+ const doc = JSON.parse(await readFile(join(slugDir, `v${version}`, "design-system.json"), "utf8"));
68
+ const roles = Object.keys(doc.foundations?.color?.semantic ?? {});
69
+ const spacing = Object.keys(doc.foundations?.spacing ?? {});
70
+ const radius = Object.keys(doc.foundations?.radius ?? {});
71
+ const parts = [
72
+ roles.length > 0
73
+ ? `\`bg-${roles.includes("primary") ? "primary" : roles[0]}\``
74
+ : null,
75
+ roles.includes("foreground")
76
+ ? "`text-foreground`"
77
+ : roles[1]
78
+ ? `\`text-${roles[1]}\``
79
+ : null,
80
+ spacing[0] ? `\`p-${spacing[0]}\`` : null,
81
+ radius[0] ? `\`rounded-${radius[0]}\`` : null,
82
+ "`font-display`",
83
+ ].filter(Boolean);
84
+ if (parts.length >= 3)
85
+ utilities = parts.join(", ");
86
+ }
87
+ catch {
88
+ // sem manifesto legível, os exemplos genéricos ficam - pior seria nenhum
89
+ }
60
90
  // The styling contract differs by target: Next projects in this product use
61
91
  // Tailwind v4 backed by the DS; the "general" target is framework-agnostic CSS.
62
92
  const stylingRule = config.target === "general"
63
93
  ? `- Style with the design system only: reuse the \`.ds-*\` recipe classes and the ` +
64
94
  `\`var(--ds-*)\` custom properties. Never use raw hex/px outside the system's scale.`
65
95
  : `- Style with the design system only: reuse the \`.ds-*\` recipe classes and the ` +
66
- `DS-backed Tailwind utilities (\`bg-primary\`, \`text-foreground\`, \`p-md\`, \`rounded-lg\`, ` +
67
- `\`font-display\`…). Never use raw hex/px outside the system's scale.`;
96
+ `DS-backed Tailwind utilities (${utilities}…). Never use raw hex/px outside the system's scale.`;
68
97
  const task = intent.trim() || "build the UI I describe next";
69
98
  const prompt = [
70
99
  `Use the "${name}" design system (slug: ${slug}, v${version}) to: ${task}`,
@@ -79,12 +79,20 @@ export function compareToBaseline(d, base, now) {
79
79
  : {}),
80
80
  };
81
81
  }
82
- /** O que a mensagem de uma anotação diz, e é a mesma frase nos dois formatos. */
82
+ /**
83
+ * O que a mensagem de uma anotação diz, e é a mesma frase nos dois formatos.
84
+ *
85
+ * O NOME DELE PRIMEIRO, como em todo o resto do doctor (`nameToWrite`): este era o único caminho
86
+ * que ignorava `their-names` por completo - e é o que aparece anotado na linha do PR de todo o
87
+ * time dele. Um `theirToken` também desarma o caso `crossFamily`: o repo DELE nomeia o valor, então
88
+ * a troca existe (mesma regra de `apply-fix.ts`).
89
+ */
83
90
  function messageFor(f) {
84
- return f.token
85
- ? f.crossFamily
91
+ const shown = f.theirToken ?? f.token;
92
+ return shown
93
+ ? f.crossFamily && !f.theirToken
86
94
  ? `${f.literal} is written by hand, and this project's system has no ${f.kind} named for it. The value does exist elsewhere in the system - as \`${f.token}\`, in another family - so this is a decision to make, not a swap: name a ${f.kind}, or leave it.`
87
- : `${f.literal} is written by hand, and this project's system already names it: var(${f.token}). Use the token.`
95
+ : `${f.literal} is written by hand, and this project already names it: var(${shown}). Use the name.`
88
96
  : `${f.literal} is a ${f.kind} value written by hand, and the system has no name for it yet. Name it, or file a token request: npx synthesisui request token`;
89
97
  }
90
98
  /**
@@ -192,7 +200,8 @@ export function describeVerdict(v) {
192
200
  lines.push(`Baseline not comparable: ${v.incomparable}.`);
193
201
  return lines;
194
202
  }
195
- lines.push(`Drift ${v.drift.was} → ${v.drift.now}${v.phantoms.was || v.phantoms.now ? ` · phantom tokens ${v.phantoms.was} → ${v.phantoms.now}` : ""}.`);
203
+ /** "Drift"/"phantom" são palavras nossas - a linha fala do que ELE vê: valores à mão e nomes que não resolvem. */
204
+ lines.push(`Hand-written values ${v.drift.was} → ${v.drift.now}${v.phantoms.was || v.phantoms.now ? ` · names that resolve to nothing ${v.phantoms.was} → ${v.phantoms.now}` : ""}.`);
196
205
  if (v.improved > 0)
197
206
  lines.push(`${v.improved} file${v.improved === 1 ? "" : "s"} improved since the baseline.`);
198
207
  if (v.regressed.length === 0) {
@@ -467,7 +467,9 @@ export function buildTable(input) {
467
467
  * Deliberately narrow: same unit, within 25% or 8px, and never for a colour -
468
468
  * "nearly the same blue" is exactly the guess this tool must not make.
469
469
  */
470
- export function nearestToken(table, literal) {
470
+ export function nearestToken(table, literal,
471
+ /** A família do achado - com ela, o vizinho também é oferecido pelo NOME DELE quando o repo o nomeia. */
472
+ kind) {
471
473
  const m = /^(-?\d*\.?\d+)(px|rem)$/.exec(literal.trim().toLowerCase());
472
474
  if (!m)
473
475
  return null;
@@ -490,7 +492,17 @@ export function nearestToken(table, literal) {
490
492
  if (!best)
491
493
  return null;
492
494
  const limit = unit === "rem" ? Math.max(n * 0.25, 0.5) : Math.max(n * 0.25, 8);
493
- return best.delta <= limit ? best : null;
495
+ if (best.delta > limit)
496
+ return null;
497
+ /**
498
+ * O VIZINHO PELO NOME DELE - a mesma chave que `theirNames` construiu (`kind:valor`), porque o
499
+ * valor sozinho não decide a família. Só a tabela nossa era consultada aqui, então a oferta saía
500
+ * sempre no nosso vocabulário mesmo quando o repo dele nomeia o mesmo valor.
501
+ */
502
+ const theirs = kind
503
+ ? table.aliases.get(`${kind}:${normalizeValue(best.value, table.rootPx)}`)
504
+ : undefined;
505
+ return theirs ? { ...best, theirs } : best;
494
506
  }
495
507
  /**
496
508
  * The family a token belongs to, from the drift it was found in.
@@ -80,7 +80,16 @@
80
80
  * Zero ocorrências no sistema real medido (todos os headings dele estão aninhados, e o `children > 0`
81
81
  * já os pegava), então para ele o `upgrade` é no-op. A marca é sobre o caso geral.
82
82
  */
83
- export const MATERIALISER_SINCE = "0.16.237";
83
+ /**
84
+ * 0.16.237 -> 0.16.239 em 16/08: os arquivos que o agente dele LÊ mudam de conselho, não só de
85
+ * bytes. O bloco gerenciado do CLAUDE.md ensinava `text-info` cravado - uma classe que um sistema
86
+ * importado pode nem compilar - e passa a apontar para os papéis que o GUIDE do sistema DELE lista;
87
+ * o GUIDE do sistema adotado exemplificava com `--<slug>-color-primary`, um nome que nós derivamos,
88
+ * e passa a usar o primeiro token de cor REAL dele (a promessa três linhas acima do exemplo é
89
+ * "YOUR tokens, under YOUR names"). É a fatia de CLI da decisão de 16/08: toda superfície fala a
90
+ * língua do cliente.
91
+ */
92
+ export const MATERIALISER_SINCE = "0.16.239";
84
93
  /**
85
94
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
86
95
  *
@@ -175,8 +184,14 @@ export const CHECKER_SINCE = "0.16.232";
175
184
  * comparação com UM literal, e a divisão do motivo as tirou dele em silêncio
176
185
  *
177
186
  * Um censo medido antes disto carrega as duas leituras erradas.
187
+ *
188
+ * 0.16.240 a VERSÃO passa a viajar (fase B1, 16/08): `project.versions` (frameworks pinados),
189
+ * 15 de 15 `signals.libraries` com versão (o leitor de manifesto lia o package.json
190
+ * do ESCOPO, que tinha zero deps), e todo nó de fronteira com pacote carimba o range
191
+ * (era 53% no melhor caminho; o derivado dava 38%). Um censo medido antes disto não
192
+ * tem versão nenhuma - e o mapa de blueprints por versão não tem o que consultar.
178
193
  */
179
- export const READER_SINCE = "0.16.215";
194
+ export const READER_SINCE = "0.16.240";
180
195
  /**
181
196
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
182
197
  *
@@ -12,4 +12,4 @@
12
12
  * invocada, e ela não é segredo - a esteira é.
13
13
  */
14
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';
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 - measurement \u2192 your reading of the app \u2192 import \u2192 upgrade 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';
package/dist/stack.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * One implementation, because two detectors that disagreed would put a rule in a
10
10
  * project the census said was something else.
11
11
  */
12
- import { readFile } from "node:fs/promises";
12
+ import { readdir, readFile } from "node:fs/promises";
13
13
  import { join } from "node:path";
14
14
  /**
15
15
  * Dependencies as the project actually resolves them - which means reading
@@ -19,36 +19,98 @@ import { join } from "node:path";
19
19
  * Next + React + Tailwind app, because a workspace hoists those to the root and
20
20
  * the leaf package.json lists only its own icons. Three levels up covers every
21
21
  * pnpm/npm workspace layout without wandering into someone's home directory.
22
+ *
23
+ * E PARA OS LADOS TAMBÉM, pelos `workspaces` do manifesto que os declara. Subir
24
+ * sozinho deixou 15 de 15 bibliotecas do censo vivo sem versão (16/08): o escopo
25
+ * era `packages/ui` (zero deps) e recharts/chakra/mui moram em `apps/*`, um
26
+ * IRMÃO - invisível para uma caminhada que só sobe. O componente medido vem do
27
+ * app que usa o sistema, então o manifesto daquele app é evidência tanto quanto
28
+ * o da raiz.
22
29
  */
23
30
  export async function resolveDeps(root) {
24
31
  const deps = {};
25
- let dir = root;
26
- for (let up = 0; up < 4; up++) {
32
+ const workspaceDirs = [];
33
+ const readManifest = async (dir) => {
27
34
  const raw = await readFile(join(dir, "package.json"), "utf8").catch(() => null);
28
- if (raw) {
29
- try {
30
- const p = JSON.parse(raw);
31
- // The nearest package.json wins on a version clash; we only ever ask
32
- // whether a name is present, so first-seen is enough.
33
- for (const [k, v] of Object.entries({
34
- ...p.dependencies,
35
- ...p.devDependencies,
36
- })) {
37
- if (deps[k] == null)
38
- deps[k] = String(v);
39
- }
35
+ if (!raw)
36
+ return;
37
+ try {
38
+ const p = JSON.parse(raw);
39
+ // The nearest package.json wins on a version clash; we only ever ask
40
+ // whether a name is present, so first-seen is enough. The up-chain is
41
+ // read before any sibling, so siblings only ever FILL, never override.
42
+ for (const [k, v] of Object.entries({
43
+ ...p.dependencies,
44
+ ...p.devDependencies,
45
+ })) {
46
+ if (deps[k] == null)
47
+ deps[k] = String(v);
40
48
  }
41
- catch {
42
- // unreadable manifest costs the detection, not the run
49
+ const globs = Array.isArray(p.workspaces)
50
+ ? p.workspaces
51
+ : Array.isArray(p.workspaces?.packages)
52
+ ? p.workspaces.packages
53
+ : [];
54
+ for (const g of globs) {
55
+ if (typeof g !== "string")
56
+ continue;
57
+ // Only the two shapes real manifests use: `packages/*` and a literal
58
+ // path. Anything fancier is skipped rather than guessed at.
59
+ if (g.endsWith("/*")) {
60
+ const parent = join(dir, g.slice(0, -2));
61
+ const children = await readdir(parent, {
62
+ withFileTypes: true,
63
+ }).catch(() => []);
64
+ for (const c of children)
65
+ if (c.isDirectory())
66
+ workspaceDirs.push(join(parent, c.name));
67
+ }
68
+ else if (!g.includes("*")) {
69
+ workspaceDirs.push(join(dir, g));
70
+ }
43
71
  }
44
72
  }
73
+ catch {
74
+ // unreadable manifest costs the detection, not the run
75
+ }
76
+ };
77
+ let dir = root;
78
+ for (let up = 0; up < 4; up++) {
79
+ await readManifest(dir);
45
80
  const parent = join(dir, "..");
46
81
  if (parent === dir)
47
82
  break;
48
83
  dir = parent;
49
84
  }
85
+ for (const w of workspaceDirs)
86
+ await readManifest(w);
50
87
  return deps;
51
88
  }
89
+ /**
90
+ * OS FRAMEWORKS DO PROJETO, COM A VERSÃO PINADA - o que `Census.project.versions` carrega.
91
+ *
92
+ * `detectStack` colapsa `next@16.0.8` na string `"next"`, e nada no censo guardava versão de
93
+ * framework nenhuma (medido 16/08: zero em todos os campos). Curado de propósito: framework aqui,
94
+ * biblioteca por biblioteca em `signals.libraries` - duas listas com dois papéis, nunca o
95
+ * package.json inteiro dentro do censo.
96
+ */
97
+ const STACK_PACKAGES = [
98
+ "next",
99
+ "react",
100
+ "react-dom",
101
+ "vue",
102
+ "svelte",
103
+ "vite",
104
+ "tailwindcss",
105
+ "typescript",
106
+ ];
107
+ export function stackVersions(deps) {
108
+ const out = {};
109
+ for (const name of STACK_PACKAGES)
110
+ if (deps[name])
111
+ out[name] = deps[name];
112
+ return out;
113
+ }
52
114
  export async function detectStack(root) {
53
115
  const stack = [];
54
116
  const has = async (f) => (await readFile(join(root, f), "utf8").catch(() => null)) !== null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.238",
3
+ "version": "0.16.240",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {