synthesisui 0.16.222 → 0.16.227

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.
@@ -11,6 +11,7 @@ import { findFrozenBindings } from "../doctor/frozen.js";
11
11
  import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, } from "../doctor/ledger.js";
12
12
  import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
13
13
  import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
14
+ import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
14
15
  import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
15
16
  import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
16
17
  import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
@@ -144,7 +145,20 @@ function requestLabel(r) {
144
145
  return `rule "${r.name}"`;
145
146
  return `component "${r.name}"`;
146
147
  }
147
- export async function loadSystem(root) {
148
+ export async function loadSystem(root,
149
+ /**
150
+ * A RAIZ JÁ MEDIDA, quando quem chama já varreu as folhas de estilo.
151
+ *
152
+ * O `doctor` mede (ele já anda no repositório inteiro) e passa. O `hook` NÃO mede, e isso é uma
153
+ * escolha declarada: ele roda depois de cada escrita com orçamento de ~50ms, e varrer css a cada
154
+ * edição trocaria um relatório instantâneo por um que a pessoa desliga.
155
+ *
156
+ * A consequência, dita em voz alta: num projeto que redefine a raiz (`html { font-size: 62.5% }`),
157
+ * o hook compara px e rem por 16 e deixa de sugerir algumas trocas que o `doctor` sugere. Ele erra
158
+ * para o lado de oferecer MENOS, nunca de oferecer a troca errada - e o `--fix`, que é quem escreve,
159
+ * vem sempre do `doctor`.
160
+ */
161
+ measured) {
148
162
  const dsDir = join(root, "_synthesisui", "ds");
149
163
  let slugs;
150
164
  try {
@@ -262,7 +276,12 @@ export async function loadSystem(root) {
262
276
  }
263
277
  }
264
278
  return {
265
- table: buildTable({ css, lock, source: adopted ? "adopted" : "installed" }),
279
+ table: buildTable({
280
+ css,
281
+ lock,
282
+ source: adopted ? "adopted" : "installed",
283
+ ...(measured ? { rootPx: measured.px, rootFrom: measured.from } : {}),
284
+ }),
266
285
  recipes,
267
286
  documents,
268
287
  requires,
@@ -283,6 +302,19 @@ export async function loadSystem(root) {
283
302
  * Reads stylesheets only, and only when nothing of ours is installed, so the
284
303
  * common path pays nothing for it.
285
304
  */
305
+ /** As folhas de estilo dele, para a medição da raiz. Só css - nada de `.tsx`. */
306
+ async function sheetsIn(roots) {
307
+ const out = [];
308
+ for await (const file of walkAll(roots)) {
309
+ if (!/\.(css|scss|sass|less)$/i.test(file))
310
+ continue;
311
+ const css = await readFile(file, "utf8").catch(() => "");
312
+ /** Só carrega adiante o que pode conter a declaração - o resto é peso à toa. */
313
+ if (/(?:html|:root)[^{]*\{[^}]*font-size/i.test(css))
314
+ out.push({ file: relative(roots[0] ?? file, file) || file, css });
315
+ }
316
+ return out;
317
+ }
286
318
  async function harvestOwnTokens(roots) {
287
319
  let css = "";
288
320
  for await (const file of walkAll(roots)) {
@@ -483,7 +515,18 @@ export async function doctor(opts) {
483
515
  const fullRun = (opts.scopes ?? []).length === 0;
484
516
  /** A intenção ordena o relatório, e a precedência é flag > config > default. */
485
517
  const intent = intentOf(await readProjectConfig(root), opts.intent);
486
- const installed = await loadSystem(root);
518
+ /**
519
+ * A RAIZ, MEDIDA ANTES DE COMPARAR VALOR NENHUM - ver `rootSizeOf`.
520
+ *
521
+ * `4px` e `0.25rem` só são o mesmo valor se alguém disser quantos pixels vale 1rem AQUI, e supor 16
522
+ * num projeto que escreve `html { font-size: 62.5% }` erraria em todos os lugares de uma vez, com a
523
+ * confiança de quem acertou. Uma passada barata: só folhas de estilo, e só o bloco `html`/`:root`.
524
+ */
525
+ const rootSize = rootSizeOf(await sheetsIn(scopes.length > 0 ? scopes : [root]));
526
+ const installed = await loadSystem(root, {
527
+ px: rootSize.ambiguous ? DEFAULT_ROOT_PX : rootSize.px,
528
+ from: rootSize.from,
529
+ });
487
530
  const { recipes, documents } = installed;
488
531
  let table = installed.table;
489
532
  // Nothing of ours here does not mean nothing to measure against. Fall back
@@ -614,6 +657,18 @@ export async function doctor(opts) {
614
657
  const said = measured.from === "census" ? describeScope(measured) : null;
615
658
  console.log(body(said ?? `scope: ${relScopes.join(", ")}`));
616
659
  }
660
+ /**
661
+ * A RAIZ QUE ESTA RODADA USOU - e ela é impressa SEMPRE, inclusive quando é o padrão.
662
+ *
663
+ * Pedido do dono em 13/08: *"analisar antes se tem algo que interfere no size, e deixar registrado
664
+ * como uma regra que está sendo usada"*. É o que transforma uma suposição nossa num fato que ele
665
+ * pode conferir: `4px` e `0.25rem` só são o mesmo valor por causa deste número, e ele decide quantas
666
+ * trocas o comando oferece.
667
+ *
668
+ * Impressa mesmo no caso padrão porque é aí que ela é mais fácil de esquecer - e um relatório que só
669
+ * fala quando é exceção ensina que o silêncio significa "não olhei".
670
+ */
671
+ console.log(body(saidOfRoot(rootSize)));
617
672
  /**
618
673
  * E A INTENÇÃO, em toda rodada, com a data e o jeito de inverter.
619
674
  *
@@ -16,11 +16,26 @@ import { resolveReadParts, siblingProjects, takeCensus } from "./import.js";
16
16
  * after `closeRequest` runs here, so the person knows what happened and what
17
17
  * (if anything) is theirs to do next.
18
18
  */
19
- export function decisionLine(d, slug) {
19
+ export function decisionLine(d, slug,
20
+ /** Onde o sistema dele vive - o `publish` mora lá, e sem o endereço a frase manda procurar. */
21
+ base) {
20
22
  const note = d.note ? ` - "${d.note}"` : "";
21
23
  switch (d.status) {
24
+ /**
25
+ * AUTORIZAR ESCREVE O RASCUNHO, e esta linha dizia o contrário.
26
+ *
27
+ * `authorPersonalToken` termina em `writeDraft`, e o registry serve a última PUBLICADA - por lei,
28
+ * desde 29/07: se o rascunho fluísse para o repo, o botão Publish não seguraria nada.
29
+ *
30
+ * Então "Get it: upgrade" mandava a pessoa rodar um comando que responde `already at the latest
31
+ * version` e não traz o token. Ela seguiu a instrução, não recebeu nada, e fica sem saber se
32
+ * autorizou errado ou se a ferramenta falhou - que é o pior lugar para deixar alguém que acabou
33
+ * de fazer exatamente o que a gente pediu (dono, 13/08, vendo isso no terminal dele).
34
+ *
35
+ * A frase agora nomeia os DOIS passos, na ordem, e o primeiro é dele.
36
+ */
22
37
  case "authored":
23
- return ` ✓ ${d.id} authored on the platform${note}. Get it: npx synthesisui@latest upgrade ${slug}`;
38
+ return ` ✓ ${d.id} authored into your draft${note}. It reaches this repo once you publish${base ? `: ${base}/dashboard/mine/${slug}/publish` : ""}\n then: npx synthesisui@latest upgrade ${slug}`;
24
39
  case "declined":
25
40
  return ` ✕ ${d.id} declined${note}. Now a RULE of the system - it travels with the next upgrade, and agents obey it.`;
26
41
  case "platform":
@@ -150,7 +165,7 @@ export async function sync(opts) {
150
165
  console.log(body("Answered on the platform, closed here:"));
151
166
  for (const d of decisions) {
152
167
  await closeRequest(root, d.id);
153
- console.log(body(decisionLine(d, slug)));
168
+ console.log(body(decisionLine(d, slug, base)));
154
169
  }
155
170
  }
156
171
  console.log("");
@@ -276,6 +291,64 @@ export async function remeasure(args) {
276
291
  if (stored.reading)
277
292
  census.reading = stored.reading;
278
293
  await resolveReadParts(census, root, !args.full).catch(() => { });
294
+ /**
295
+ * O QUE A RE-MEDIÇÃO NÃO MEDE, ELA NÃO PODE APAGAR - e apagava quatro campos de uma vez.
296
+ *
297
+ * O `sync` mede o CÓDIGO dele de novo. Ele não mede polaridade, não pergunta escopo e não refaz a
298
+ * leitura do agente: esses três são respostas que alguém já deu, e `runImport` é quem as grava.
299
+ * Escrever por cima com um censo que não os tem não é re-medir - é esquecer.
300
+ *
301
+ * O que isso custava, medido no censo do dono em 13/08 (`scope`, `usage`, `scheme` e `reading`
302
+ * ausentes depois de um sync):
303
+ *
304
+ * scheme `censusToPatch` lê `reading.themes.default ?? census.scheme ?? "light"`. Sem os
305
+ * dois primeiros ele cai em CLARO - e o sistema dele abre ESCURO. Uma
306
+ * re-interpretação a partir desse censo aterra a face errada, no servidor, sem
307
+ * ninguém tocar em nada
308
+ * scope o próprio comentário de `takeCensus` chama isso de veneno de ação lenta: a
309
+ * primeira re-medição parece perfeita e a segunda mede o repositório inteiro
310
+ * reading a leitura que o agente autorou, que é justamente o que o `sync` promete não mexer
311
+ *
312
+ * Complemento, nunca correção: o que a medição de hoje TEM continua ganhando. Isto só recoloca o
313
+ * que ela não tinha como saber.
314
+ */
315
+ const before = await readFile(join(root, "_synthesisui", "census.json"), "utf8")
316
+ .then((raw) => JSON.parse(raw))
317
+ .catch(() => null);
318
+ const fresh = census;
319
+ /** `scope` e `usage` o `sync` SABE - ele acabou de resolvê-los. Os outros vêm do que já existia. */
320
+ if (scope)
321
+ fresh.scope = scope;
322
+ /**
323
+ * E A CONTRADIÇÃO GRITA, porque uma vez ela aconteceu e ninguém viu.
324
+ *
325
+ * Em 13/08 o censo do dono foi gravado SEM escopo tendo sido medido COM ele - 36 looks, a
326
+ * biblioteca, não os ~340 do repositório inteiro. Nenhum caminho de código explica: `takeCensus`
327
+ * emite o campo desde 06/08, o `sync` passa o rótulo desde 07/08, e a 0.16.222 publicada - a que
328
+ * carimbou o arquivo - emite quando testada. O arquivo foi sobrescrito antes de eu poder abri-lo.
329
+ *
330
+ * Uma causa que não se achou não está fechada. Então o que estava mudo vira barulho: gravar sem
331
+ * escopo enquanto o `.lock` tem um é o estado impossível, e ele passa a se anunciar em vez de
332
+ * envenenar a próxima re-medição em silêncio (`import.ts`: sem escopo, a medição seguinte lê o
333
+ * repositório inteiro e o rascunho curado de 37 vira 339).
334
+ */
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.`));
337
+ if (usage.length > 0)
338
+ fresh.usage = usage;
339
+ for (const key of ["scheme", "reading"])
340
+ if (fresh[key] == null && before?.[key] != null)
341
+ fresh[key] = before[key];
342
+ /**
343
+ * E A POLARIDADE VEM DA PLATAFORMA quando nem a medição nem o arquivo a têm.
344
+ *
345
+ * Carregar do arquivo anterior conserta quem ainda não perdeu. Quem já perdeu - o censo do dono,
346
+ * medido em 13/08 - ficaria sem polaridade para sempre, porque o `sync` não a mede: ela é uma
347
+ * resposta dada no import. O documento guardado sabe (`meta.scheme`), então a rota devolve e isto
348
+ * recoloca. O próximo `sync` de quem estava furado repara o censo dele.
349
+ */
350
+ if (fresh.scheme == null && stored.scheme)
351
+ fresh.scheme = stored.scheme;
279
352
  /**
280
353
  * O CENSO FRESCO TAMBÉM FICA NO DISCO.
281
354
  *
@@ -0,0 +1,101 @@
1
+ /**
2
+ * QUAL É A RAIZ DESTE PROJETO - medida antes de comparar valor nenhum.
3
+ *
4
+ * `4px` e `0.25rem` são o mesmo valor, e o comparador tratava os dois como textos diferentes. Medido
5
+ * no repo real em 13/08: 1320 literais em px que um token `--ds-*` já nomeia em rem, em 364 arquivos,
6
+ * contra 253 trocas que o doctor conseguia oferecer no repositório inteiro. A maior parte do trabalho
7
+ * fácil estava escondida atrás de uma comparação de string.
8
+ *
9
+ * A conversão exige uma raiz, e `1rem = 16px` é o padrão do navegador - mas é só o padrão. Um projeto
10
+ * que escreve `html { font-size: 62.5% }` tem raiz de 10px, e converter por 16 ali erraria em todos os
11
+ * lugares de uma vez, com a confiança de quem acertou.
12
+ *
13
+ * Então a raiz não é suposta: é MEDIDA no css dele, e DITA em voz alta no relatório (dono, 13/08:
14
+ * *"analisar antes se tem algo que interfere no size, e deixar registrado como uma regra que está
15
+ * sendo usada"*). Uma suposição escondida num comentário do nosso código não é lida por ninguém; um
16
+ * fato impresso no cabeçalho é conferível por quem conhece o projeto.
17
+ *
18
+ * E QUANDO NÃO DÁ PARA SABER, NÃO CONVERTE. Duas raízes diferentes, um `calc()`, um `var()`: a
19
+ * resposta é dizer que não sabe. Deixar de oferecer uma troca custa uma troca; oferecer a errada em
20
+ * 1320 lugares custa a confiança no comando.
21
+ */
22
+ /** O padrão do navegador, e o que vale quando ninguém redefine. */
23
+ export const DEFAULT_ROOT_PX = 16;
24
+ /** `html`/`:root` com uma declaração de `font-size` dentro - o bloco inteiro, para ler o valor. */
25
+ const ROOT_BLOCK = /(?:^|[},;])\s*(html|:root)\s*(?:,[^{]*)?\{([^}]*)\}/gi;
26
+ const FONT_SIZE = /(?:^|;)\s*font-size\s*:\s*([^;}]+)/i;
27
+ /**
28
+ * O valor de uma declaração de raiz em pixels, ou `null` quando não dá para saber.
29
+ *
30
+ * `%` e `em` na RAIZ são relativos ao padrão do navegador, que é o único ancestral que ela tem - por
31
+ * isso `62.5%` é 10px e não uma incógnita. `rem` na raiz é a mesma coisa, e é como alguns projetos
32
+ * escrevem `1rem` só para deixar explícito.
33
+ */
34
+ export function rootPxOf(raw) {
35
+ const v = raw.trim().toLowerCase();
36
+ const m = /^(\d*\.?\d+)(px|%|r?em)$/.exec(v);
37
+ if (!m)
38
+ return null;
39
+ const n = Number.parseFloat(m[1]);
40
+ if (!Number.isFinite(n) || n <= 0)
41
+ return null;
42
+ if (m[2] === "px")
43
+ return n;
44
+ if (m[2] === "%")
45
+ return (n / 100) * DEFAULT_ROOT_PX;
46
+ return n * DEFAULT_ROOT_PX;
47
+ }
48
+ /**
49
+ * A raiz deste projeto, lida das folhas de estilo dele.
50
+ *
51
+ * Recebe os arquivos já lidos porque quem varre é o comando - esta função não sabe andar em disco, e
52
+ * é isso que a torna testável com um objeto em vez de um diretório temporário.
53
+ */
54
+ export function rootSizeOf(sheets) {
55
+ /** Valor em px → onde ele foi declarado pela primeira vez. */
56
+ const seen = new Map();
57
+ const unreadable = [];
58
+ for (const sheet of sheets) {
59
+ ROOT_BLOCK.lastIndex = 0;
60
+ for (const block of sheet.css.matchAll(ROOT_BLOCK)) {
61
+ const decl = FONT_SIZE.exec(block[2]);
62
+ if (!decl)
63
+ continue;
64
+ const px = rootPxOf(decl[1]);
65
+ if (px == null) {
66
+ unreadable.push(`${decl[1].trim()} (${sheet.file})`);
67
+ continue;
68
+ }
69
+ if (!seen.has(px))
70
+ seen.set(px, `${block[1]}, ${sheet.file}`);
71
+ }
72
+ }
73
+ /**
74
+ * UM VALOR ILEGÍVEL SÓ CONTAMINA SE FOR O ÚNICO SINAL - ou se discordar do que foi lido.
75
+ *
76
+ * Um `font-size: var(--app-root)` ao lado de um `10px` declarado não torna o projeto ambíguo: o
77
+ * que se sabe continua sabido. O que torna é não haver resposta, ou haver duas.
78
+ */
79
+ if (seen.size > 1)
80
+ return {
81
+ px: DEFAULT_ROOT_PX,
82
+ from: null,
83
+ ambiguous: {
84
+ saw: [...seen.entries()].map(([px, at]) => `${px}px (${at})`),
85
+ },
86
+ };
87
+ if (seen.size === 0 && unreadable.length > 0)
88
+ return { px: DEFAULT_ROOT_PX, from: null, ambiguous: { saw: unreadable } };
89
+ const only = [...seen.entries()][0];
90
+ return only
91
+ ? { px: only[0], from: only[1] }
92
+ : { px: DEFAULT_ROOT_PX, from: null };
93
+ }
94
+ /** A linha do relatório - o fato, e de onde ele veio. Nunca um número pelado. */
95
+ export function saidOfRoot(root) {
96
+ if (root.ambiguous)
97
+ return `root font-size: not one answer (${root.ambiguous.saw.slice(0, 3).join(" · ")}) - px and rem are not compared here`;
98
+ return root.from
99
+ ? `root font-size: ${root.px}px (${root.from}) - px and rem are compared against this`
100
+ : `root font-size: ${DEFAULT_ROOT_PX}px (browser default - nothing here redefines it)`;
101
+ }
@@ -316,7 +316,7 @@ function scanCore(file, source, table) {
316
316
  * e é por VALOR, não por nome: `--color-ocean-500` e `--ds-color-ocean-500` só são a mesma
317
317
  * decisão porque os dois seguram `#059aed`.
318
318
  */
319
- ...(table.byValue.has(normalizeValue(value))
319
+ ...(table.byValue.has(normalizeValue(value, table.rootPx))
320
320
  ? { mirrored: true }
321
321
  : null),
322
322
  });
@@ -11,6 +11,17 @@
11
11
  * Pure and dependency-free on purpose: every function here takes text and
12
12
  * returns data, so the whole diagnosis is testable without a filesystem.
13
13
  */
14
+ /**
15
+ * Where the vocabulary being measured against came from.
16
+ *
17
+ * `"installed"` is a system we wrote. `"yours"` is the project's OWN custom
18
+ * properties, harvested from its stylesheets - the case that matters most,
19
+ * because the people who feel this problem hardest already have a design
20
+ * system and had no reason to adopt ours before seeing a number.
21
+ */
22
+ /** `"adopted"` is theirs too, but described by `adopt` and therefore NAMED -
23
+ * it must not be offered a system to install, having just adopted one. */
24
+ import { DEFAULT_ROOT_PX } from "./root-size.js";
14
25
  export const EMPTY_TABLE = {
15
26
  source: null,
16
27
  name: null,
@@ -18,6 +29,8 @@ export const EMPTY_TABLE = {
18
29
  version: null,
19
30
  byName: new Map(),
20
31
  byValue: new Map(),
32
+ rootPx: DEFAULT_ROOT_PX,
33
+ rootFrom: null,
21
34
  declared: new Set(),
22
35
  keyframes: new Set(),
23
36
  };
@@ -90,8 +103,30 @@ function args(body) {
90
103
  *
91
104
  * Anything that is not a colour passes through lowercased and collapsed.
92
105
  */
93
- export function normalizeValue(raw) {
106
+ export function normalizeValue(raw, rootPx) {
94
107
  const v = raw.trim().toLowerCase().replace(/\s+/g, " ");
108
+ /**
109
+ * COMPRIMENTO CAI EM PIXELS, pelo mesmo motivo que tempo cai em milissegundos logo abaixo.
110
+ *
111
+ * `--ds-radius-xs: 0.25rem` e um `borderRadius: "4px"` no código dele são o MESMO valor, e o
112
+ * comparador os tratava como textos diferentes. Medido no repo real (13/08): 1320 literais em px
113
+ * que um token já nomeia em rem, contra 253 trocas que o comando conseguia oferecer no repositório
114
+ * inteiro.
115
+ *
116
+ * `rootPx` é obrigatório de propósito. O padrão do navegador é 16, mas um projeto que escreve
117
+ * `html { font-size: 62.5% }` tem raiz de 10 - e converter por 16 ali erra em todos os lugares de
118
+ * uma vez. Quem chama tem que dizer qual raiz mediu; ver `rootSizeOf`.
119
+ *
120
+ * A FAMÍLIA CONTINUA MANDANDO: `4px` passa a casar com `--ds-radius-xs` E com `--ds-spacing-3xs`,
121
+ * e é `tokenMatch` quem escolhe pelo `kind` - um raio não vira espaçamento dentro de um `gap`.
122
+ */
123
+ const len = /^(-?\d*\.?\d+)(px|rem)$/.exec(v);
124
+ if (len) {
125
+ const n = Number.parseFloat(len[1]);
126
+ const px = len[2] === "rem" ? n * rootPx : n;
127
+ /** Arredonda o rastro binário: `0.35rem * 16` é 5.6000000000000005. */
128
+ return `${Math.round(px * 1e4) / 1e4}px`;
129
+ }
95
130
  // Time lands on milliseconds: `.3s`, `0.3s` and `300ms` are one value, the
96
131
  // same way every colour lands on 8-digit hex. Without this, a system that
97
132
  // authors `--ds-motion-durations-base: 0.3s` never names an author's `300ms`.
@@ -353,6 +388,7 @@ export function parseRootTokens(css) {
353
388
  }
354
389
  export function buildTable(input) {
355
390
  const source = input.source ?? "installed";
391
+ const rootPx = input.rootPx ?? DEFAULT_ROOT_PX;
356
392
  const byName = source === "installed"
357
393
  ? parseTokens(input.css)
358
394
  : parseRootTokens(input.css);
@@ -377,7 +413,7 @@ export function buildTable(input) {
377
413
  const isAlias = (name) => /^var\(/.test((raw.get(name) ?? "").trim());
378
414
  const byValue = new Map();
379
415
  for (const [name, value] of resolved) {
380
- const key = normalizeValue(value);
416
+ const key = normalizeValue(value, rootPx);
381
417
  const list = byValue.get(key);
382
418
  if (!list)
383
419
  byValue.set(key, [name]);
@@ -393,6 +429,8 @@ export function buildTable(input) {
393
429
  version: input.lock?.version ?? null,
394
430
  byName,
395
431
  byValue,
432
+ rootPx,
433
+ rootFrom: input.rootFrom ?? null,
396
434
  declared: parseDeclaredNames(input.css),
397
435
  keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
398
436
  };
@@ -480,7 +518,7 @@ const FAMILY = {
480
518
  * `family`.
481
519
  */
482
520
  export function tokenMatch(table, literal, kind) {
483
- const hit = table.byValue.get(normalizeValue(literal));
521
+ const hit = table.byValue.get(normalizeValue(literal, table.rootPx));
484
522
  if (!hit || hit.length === 0)
485
523
  return null;
486
524
  const prefix = kind ? FAMILY[kind] : undefined;
@@ -90,7 +90,13 @@ export const MATERIALISER_SINCE = "0.16.220";
90
90
  * agente dele que o fundo do `card` deveria apontar para `foreground` - o papel do TEXTO -, e é
91
91
  * exatamente uma verificação que o pinado faz diferente.
92
92
  */
93
- export const CHECKER_SINCE = "0.16.219";
93
+ /**
94
+ * 0.16.219 -> 0.16.223 em 13/08: `px` e `rem` viraram o mesmo valor na comparação. O hook roda a
95
+ * mesma varredura depois de cada escrita, e um pin anterior lê o mesmo arquivo e diz "o sistema não
96
+ * tem nome para isto" sobre um valor que o sistema dela nomeia - medido no repo real, 243 trocas
97
+ * viraram 764 sobre os mesmos 4433 valores à mão.
98
+ */
99
+ export const CHECKER_SINCE = "0.16.223";
94
100
  /**
95
101
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
96
102
  *
@@ -20,6 +20,11 @@ description: Confronta UM componente (ou uma página, ou uma pasta) contra o des
20
20
 
21
21
  # Adaptar uma peça ao sistema
22
22
 
23
+ **Seu primeiro comando é a medição.** Não leia \`.mcp.json\`, não abra o censo, não liste
24
+ arquivo: \`check_file\` ou \`doctor <alvo>\` responde tudo isso em um passo, e é o que o resto
25
+ desta skill consome. Um agente que sai explorando antes gasta a paciência de quem pediu e
26
+ chega ao mesmo lugar.
27
+
23
28
  O propósito do produto, que decide todo empate abaixo: **ler o repositório do cliente e
24
29
  devolver receitas com paridade visual, semântica e funcional, sem supor e sem inventar
25
30
  nada.**
@@ -61,6 +66,13 @@ ao lado é justamente onde os valores à mão se escondem.
61
66
 
62
67
  Nunca meça os dois e escolha o maior. Diga o que mediu.
63
68
 
69
+ **O alvo fora da pasta importada é o caso COMUM, e não um erro.** O sistema nasceu de uma
70
+ pasta (o \`scope\` no \`.lock\`), e uma tela de app que consome o DS está fora dela - é
71
+ literalmente o pedido *"conserta essa página pro meu design system"*. Ali a cobertura de
72
+ token vale igual, porque a camada de token é global; o que não existe é receita daquele
73
+ componente. Diga isso em uma linha e siga - e quando faltar uma peça, o destino é
74
+ \`request component\`.
75
+
64
76
  ## 2. MEÇA - e a medida é determinística, não sua
65
77
 
66
78
  Duas portas, mesma resposta. Use a que a sessão tiver:
@@ -91,32 +103,56 @@ no name for it o sistema não nomeia -> DECISÃO dele, é o passo 5
91
103
 
92
104
  Não recalcule nada disso de cabeça. O número que você reporta é o que o comando disse.
93
105
 
94
- ## 3. PROPONHA O MECÂNICO, E SÓ DEPOIS ESCREVA
106
+ ## 3. MONTE A FILA, E CONTE OS ITENS ANTES DE COMEÇAR
95
107
 
96
- \`--fix\` mostra; \`--write\` escreve. Rode o primeiro, cole a saída, espere.
108
+ Aqui é onde esta skill se ganha ou se perde. Despejar tudo de uma vez - duas regras, quatro
109
+ decisões, dez trocas - não é um relatório, é uma parede. Quem lê não tem como agir; só
110
+ concordar ou fechar a aba.
111
+
112
+ Junte tudo o que você achou (o mecânico do passo 2, as regras do passo 4, o que sobrou do
113
+ passo 5) numa fila ÚNICA, e **ordene por custo**:
97
114
 
98
115
  \`\`\`
99
- npx synthesisui doctor <alvo> --fix
116
+ 1º não muda um pixel troca por token de mesmo valor
117
+ 2º muda o pixel colapsar um passo, adotar um token semântico
118
+ 3º não é troca, é pedido token novo, peça nova, regra nova
100
119
  \`\`\`
101
120
 
121
+ Assim ele despacha o barato primeiro e para quando quiser, sem ficar devendo nada.
122
+
123
+ Anuncie o tamanho antes do primeiro item, sempre:
124
+
102
125
  \`\`\`
103
- Would replace 1 hand-written value with the token your system already has, across 1 file.
104
- var(--ds-color-ocean-50) · 1 time
105
- 1 finding left: values your system has no name for. Those are decisions.
126
+ 7 itens nesta fila: 2 sem mudar pixel, 3 que mudam, 2 pedidos.
106
127
  \`\`\`
107
128
 
108
- Ofereça exatamente três saídas, nesta ordem:
129
+ ## 4. UM ITEM POR VEZ, COM OPÇÕES - e espere a resposta
130
+
131
+ Nunca apresente o item 2 antes de ele responder o 1. O cabeçalho carrega a posição, para
132
+ ele saber onde está e quanto falta:
109
133
 
110
134
  \`\`\`
111
- confirmar npx synthesisui doctor <alvo> --fix --write
112
- à mão você lista as trocas e ele edita - use quando ele quiser revisar linha a linha
113
- cancelar e a medição fica, que já é resultado: ele sabe onde está
135
+ item 1 de 7 · radius 4px · 2 lugares · não muda um pixel
136
+
137
+ DeliveredBox/index.tsx:76 borderRadius: "4px" -> var(--ds-radius-xs)
138
+ TotalSentBox/index.tsx:71 borderRadius: "4px" -> var(--ds-radius-xs)
139
+
140
+ [aplicar] [pular] [ver o diff] [parar por aqui]
114
141
  \`\`\`
115
142
 
116
- \`--fix\` só troca o que o sistema DELE já nomeia. Ele nunca inventa um nome, e é por isso
117
- que o conserto pode ser mecânico.
143
+ Quatro opções, e nenhuma a mais:
118
144
 
119
- ## 4. AS REGRAS, QUE É A METADE QUE NENHUM COMANDO FAZ
145
+ \`\`\`
146
+ aplicar você roda o comando ou faz a edição, e confirma em uma linha
147
+ pular segue para o próximo, e ele entra no resumo como não mexido
148
+ ver o diff mostre e volte a perguntar - não conte isso como resposta
149
+ parar por aqui fecha o resumo com o que andou até aqui. É sempre legítimo
150
+ \`\`\`
151
+
152
+ Para um item mecânico o "aplicar" é \`npx synthesisui doctor <alvo> --fix --write\`. Para os
153
+ outros é edição, e você mostra exatamente as linhas antes.
154
+
155
+ ## 5. AS REGRAS, QUE É A METADE QUE NENHUM COMANDO FAZ
120
156
 
121
157
  O passo 2 é determinístico e sai de graça. Este não: o sistema carrega uma doutrina em
122
158
  prosa, e **nada a verifica mecanicamente**. É aqui que você trabalha.
@@ -130,53 +166,84 @@ Leia as regras e confronte o componente com cada uma. Três respostas possíveis
130
166
  e a terceira é a que interessa:
131
167
 
132
168
  \`\`\`
133
- cumpre diga em uma linha, sem cerimônia
134
- NÃO cumpre cite a regra, o lugar no arquivo, e proponha o conserto
135
- a regra não fala sobre isto <- o passo 5
169
+ cumpre some do relatório. Diga só o total no fim
170
+ NÃO cumpre vira um ITEM da fila, com arquivo, linha e a troca proposta
171
+ a regra não fala sobre isto vira um item de PEDIDO - \`request rule\`
136
172
  \`\`\`
137
173
 
138
174
  Uma regra que você teve que interpretar para aplicar não é "cumpre". É o terceiro caso.
139
175
 
140
- ## 5. O QUE SOBROU, CLASSIFICADO - e cada classe tem um destino
176
+ E nunca escreva "3 de 5 cumpridas". Isso lê como boletim, e as 2 que faltam são justamente
177
+ as que TÊM conserto pronto - é a melhor notícia do relatório vestida como a pior.
141
178
 
142
- Aqui a pergunta deixa de ser "está certo?" e passa a ser **"isto vira sistema, ou fica
143
- aqui?"**. Três destinos, e todos existem como comando:
179
+ ## 6. OS PEDIDOS - você mostra o comando, ele roda
180
+
181
+ Três destinos, e todos existem:
144
182
 
145
183
  \`\`\`
146
184
  valor sem nome no sistema
147
185
  -> npx synthesisui request token --value "<valor>" --name "<como se chamaria>" --for "<o caso>"
148
- ou, pelo MCP: request_token
149
-
150
186
  peça que falta
151
187
  -> npx synthesisui request component --name "<nome>" --for "<o caso>"
152
- ou: request_component
153
-
154
188
  a doutrina não cobre este caso
155
189
  -> npx synthesisui request rule --name "<o que a regra diria>" --for "<o caso que pediu>"
156
- ou: request_rule
157
190
  \`\`\`
158
191
 
159
- **Você mostra o comando. Ele roda.** Um pedido é uma decisão entrando na fila do sistema
160
- dele - e a fila viaja para a plataforma no próximo \`sync\`, que é o que a torna
161
- compartilhável quando houver time. Arquivar em nome dele seria decidir por ele e ainda
162
- tirar dele a chance de dizer "não, isso fica local mesmo".
192
+ **Arquivar em nome dele seria decidir por ele** - e tirar dele a chance de dizer "não, isso
193
+ fica local mesmo". Antes de propor \`request rule\`, leia a doutrina inteira: uma regra que já
194
+ existe e você não achou vira duplicata na fila, e a fila perde valor na terceira.
163
195
 
164
- Antes de propor \`request rule\`, leia a doutrina inteira. Uma regra que já existe e você não
165
- achou vira uma regra duplicada na fila, e a fila perde valor na terceira duplicata.
196
+ ## 6b. A RÉGUA É O ALCANÇÁVEL, E O TETO SE DIZ JUNTO
166
197
 
167
- ## 6. FECHE EM QUATRO LINHAS
198
+ **100% quase nunca é alcançável hoje, e isso não é falha dele.** Se o sistema não tem nome
199
+ para \`#555\`, ninguém chega a 100% sem antes decidir criar esse nome. Um medidor que mostra
200
+ 0% contra um teto imaginário faz trabalho completo parecer trabalho pela metade.
168
201
 
169
- Sem prosa. O cliente precisa decidir, não auditar:
202
+ Os números para a conta certa já vêm do comando:
203
+
204
+ \`\`\`
205
+ 3 valores à mão
206
+ 2 o sistema já nomeia -> alcançável hoje: 67%
207
+ 1 o sistema não nomeia -> precisa de uma decisão dele
208
+ \`\`\`
209
+
210
+ Então o teto de hoje é 67%, e aplicar as duas trocas é **chegar no teto** - 100% do que dá
211
+ para fazer com o vocabulário que existe.
212
+
213
+ ## 6c. FECHE PELO QUE ANDOU, E MOSTRE O CAMINHO ATÉ 100%
170
214
 
171
215
  \`\`\`
172
216
  <Componente> <n> arquivos
173
- cobertura X% -> Y% (<n> trocas escritas · <n> continuam à mão)
174
- regras <n> de <n> cumpridas <as que não, nomeadas>
175
- na sua mão <n> decisões <token · componente · regra>, com o comando ao lado
217
+
218
+ alcançável hoje 67% é o que o vocabulário do sistema cobre
219
+ aplicado 67% ✓ no teto - nada mecânico ficou para trás
220
+ pulado 8 espaçamentos que mudam layout (item 5, quando quiser)
221
+ na sua fila 1 #555, que o sistema ainda não nomeia
176
222
  \`\`\`
177
223
 
178
- Se nada mudou, diga isso e pare. Uma skill que sempre encontra trabalho é uma skill que
179
- inventa trabalho.
224
+ Nunca "0% -> 0%". Se nada era mecânico, o teto era zero, e a frase é *"não havia nada que
225
+ o vocabulário de hoje resolvesse - o que existe são N decisões suas"*.
226
+
227
+ E quando sobrou pedido, termine com o caminho, porque ele é de dois passos e o primeiro é
228
+ dele:
229
+
230
+ \`\`\`
231
+ para chegar a 100%, faltam dois passos:
232
+ 1. autorize o pedido no dashboard (ele já está na fila; \`sync\` o levou)
233
+ 2. publique, e rode \`npx synthesisui upgrade\` aqui
234
+ depois disso, /sui-adapt neste componente fecha em 100%
235
+ \`\`\`
236
+
237
+ Autorizar escreve o RASCUNHO, e o repo recebe a última PUBLICADA - por isso os dois passos,
238
+ e por isso o \`sync\` também diz isso quando a decisão volta. Prometer que \`upgrade\` sozinho
239
+ traz o token é mandar a pessoa rodar um comando que responde "already at the latest version".
240
+
241
+ Se a fila esvaziou, o fim é uma linha só:
242
+
243
+ \`\`\`
244
+ alcançável hoje 100%
245
+ aplicado 100% ✓ este componente está inteiro no sistema
246
+ \`\`\`
180
247
 
181
248
  ## 7. ROTINA
182
249
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.222",
3
+ "version": "0.16.227",
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": {