synthesisui 0.16.220 → 0.16.225

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.
@@ -7,6 +7,7 @@ import { readToken, resolveRegistry } from "../config.js";
7
7
  import { unsentEvents } from "../doctor/ledger.js";
8
8
  import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
9
9
  import { measuredScope } from "../measured-scope.js";
10
+ import { SKILLS } from "../skills.js";
10
11
  /**
11
12
  * QUAL CLI MEDIU O CENSO EM DISCO - e era o `reader`, um inteiro, até 11/08.
12
13
  *
@@ -150,6 +151,36 @@ opts = {}) {
150
151
  */
151
152
  run: "npx synthesisui sync",
152
153
  });
154
+ /**
155
+ * A SKILL QUE ESTE CLI TEM E ESTE REPO NÃO - e sem isto ela nunca chegaria.
156
+ *
157
+ * `connect` é idempotente e atualiza sozinho, mas ninguém roda um comando de novo sem motivo: uma
158
+ * skill nova ficava esperando o acaso. Aqui ela vira uma linha do alinho, que é a única coisa que
159
+ * fala com a pessoa sem ela pedir.
160
+ *
161
+ * Compara o CONTEÚDO e não só a existência: uma skill velha descreve um fluxo que este CLI já não
162
+ * tem, e isso é pior que não ter skill nenhuma - quem lê não tem como perceber.
163
+ */
164
+ const installedSkills = await Promise.all(SKILLS.map((skill) => readFile(join(root, skill.path), "utf8").catch(() => null)));
165
+ /**
166
+ * E SÓ FALA COM QUEM JÁ TEM ALGUMA - senão isto vira o alarme que ensina uma palavra nova a quem
167
+ * não pediu.
168
+ *
169
+ * Um repositório sem skill nenhuma nunca ligou o agente, e pode nem usar Claude Code. Dizer a essa
170
+ * pessoa que "uma skill está faltando" é nomear uma ausência que ela escolheu, e `align` é a única
171
+ * superfície que fala sem ser chamada - a regra dela é ficar calada no estado saudável.
172
+ *
173
+ * Com uma instalada, o silêncio passa a ser o erro: ela ligou o agente, e uma skill nova ficaria
174
+ * esperando o acaso de alguém rodar `connect` de novo.
175
+ */
176
+ const staleSkills = SKILLS.filter((skill, i) => installedSkills[i] !== skill.source).map((skill) => skill.label);
177
+ if (installedSkills.some((have) => have != null) && staleSkills.length > 0)
178
+ out.push({
179
+ says: staleSkills.length === 1
180
+ ? `${staleSkills[0]} is missing or older than this CLI - it is a skill your agent can invoke, and it is not here.`
181
+ : `${staleSkills.length} skills are missing or older than this CLI (${staleSkills.join(", ")}) - your agent can invoke them, and they are not here.`,
182
+ run: "npx synthesisui connect",
183
+ });
153
184
  /**
154
185
  * SÓ O QUE NÃO SUBIU - ver `unsentEvents`. Isto contava o arquivo inteiro, e como o ledger é
155
186
  * append-only e o `sync` manda tudo (a plataforma deduplica), a linha nunca mais saía da tela e o
@@ -6,8 +6,7 @@ import { resolveRegistry } from "../config.js";
6
6
  import { body, paint, section, snippet } from "../output.js";
7
7
  import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
8
8
  import { existingRc, hasHook, pinnedInHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
9
- import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "../skill-import.js";
10
- import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
9
+ import { SKILLS } from "../skills.js";
11
10
  import { add } from "./add.js";
12
11
  import { reportWhatIsLeft } from "./align.js";
13
12
  import { ci } from "./ci.js";
@@ -293,24 +292,19 @@ export async function connect(opts) {
293
292
  * something change" - so nobody has to remember a second command.
294
293
  */
295
294
  /**
296
- * AS DUAS SKILLS, e o `/sui-init` é a que importa mais aqui: é a PRIMEIRA CORRIDA, e uma skill que
295
+ * AS TRÊS SKILLS, e o `/sui-init` é a que importa mais aqui: é a PRIMEIRA CORRIDA, e uma skill que
297
296
  * só aparece depois de reiniciar o editor não serve para a primeira corrida de ninguém. Ela vem no
298
- * mesmo `connect` que ainda vai pedir o reinício - então quando a pessoa reabre, as duas existem.
297
+ * mesmo `connect` que ainda vai pedir o reinício - então quando a pessoa reabre, as três existem.
298
+ *
299
+ * A TERCEIRA É DE MANUTENÇÃO, e ela é de outra natureza: as duas primeiras são de ENTRADA e rodam
300
+ * uma vez. `/sui-adapt` responde a pergunta do dia seguinte - *"isso aqui está de acordo com o meu
301
+ * design system?"* -, apontando para um componente, toda semana ou depois de mexer em alguma coisa.
302
+ *
303
+ * Ela quase ficou de fora daqui, e teria sido o erro de sempre: uma skill que só existe no NOSSO
304
+ * repositório é uma skill que só nós rodamos, e o caso de uso inteiro dela é o cliente rodando
305
+ * sozinho. Meia jornada não é meio valor.
299
306
  */
300
- const skills = [
301
- {
302
- path: INIT_SKILL_PATH,
303
- source: INIT_SKILL,
304
- label: "/sui-init",
305
- what: "the first run, start to finish",
306
- },
307
- {
308
- path: IMPORT_SKILL_PATH,
309
- source: IMPORT_SKILL,
310
- label: "/sui-import-ds",
311
- what: "the import, orchestrated",
312
- },
313
- ];
307
+ const skills = SKILLS;
314
308
  /**
315
309
  * A PASTA VELHA SAI, e isto é obrigatório numa renomeação de skill distribuída.
316
310
  *
@@ -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";
@@ -130,7 +131,34 @@ export async function* walkAll(roots) {
130
131
  * and the recipes `add` put next to them in `design-system.json`. Those
131
132
  * recipes are why the component pass can exist at all - a linter has no idea
132
133
  * what `ds-button` promised. */
133
- export async function loadSystem(root) {
134
+ /**
135
+ * COMO CADA PEDIDO SE LÊ NUMA LINHA - e o `else` desta expressão mentia.
136
+ *
137
+ * Ela dizia `component "<nome>"` para tudo que não fosse token, então um pedido de REGRA aparecia no
138
+ * doctor como se fosse um componente pedido. Uma tela que rotula errado é pior que uma que não
139
+ * rotula: quem lê decide em cima do rótulo.
140
+ */
141
+ function requestLabel(r) {
142
+ if (r.kind === "token")
143
+ return `token ${r.value} as ${r.name}`;
144
+ if (r.kind === "rule")
145
+ return `rule "${r.name}"`;
146
+ return `component "${r.name}"`;
147
+ }
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) {
134
162
  const dsDir = join(root, "_synthesisui", "ds");
135
163
  let slugs;
136
164
  try {
@@ -248,7 +276,12 @@ export async function loadSystem(root) {
248
276
  }
249
277
  }
250
278
  return {
251
- 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
+ }),
252
285
  recipes,
253
286
  documents,
254
287
  requires,
@@ -269,6 +302,19 @@ export async function loadSystem(root) {
269
302
  * Reads stylesheets only, and only when nothing of ours is installed, so the
270
303
  * common path pays nothing for it.
271
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
+ }
272
318
  async function harvestOwnTokens(roots) {
273
319
  let css = "";
274
320
  for await (const file of walkAll(roots)) {
@@ -469,7 +515,18 @@ export async function doctor(opts) {
469
515
  const fullRun = (opts.scopes ?? []).length === 0;
470
516
  /** A intenção ordena o relatório, e a precedência é flag > config > default. */
471
517
  const intent = intentOf(await readProjectConfig(root), opts.intent);
472
- 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
+ });
473
530
  const { recipes, documents } = installed;
474
531
  let table = installed.table;
475
532
  // Nothing of ours here does not mean nothing to measure against. Fall back
@@ -600,6 +657,18 @@ export async function doctor(opts) {
600
657
  const said = measured.from === "census" ? describeScope(measured) : null;
601
658
  console.log(body(said ?? `scope: ${relScopes.join(", ")}`));
602
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)));
603
672
  /**
604
673
  * E A INTENÇÃO, em toda rodada, com a data e o jeito de inverter.
605
674
  *
@@ -941,7 +1010,7 @@ export async function doctor(opts) {
941
1010
  console.log("");
942
1011
  console.log(section("What your agent asked for"));
943
1012
  for (const r of requests.slice(0, 8)) {
944
- console.log(body(` ${r.id} ${r.kind === "token" ? `token ${r.value} as ${r.name}` : `component "${r.name}"`}${r.area === "platform" ? " [platform]" : ""}`));
1013
+ console.log(body(` ${r.id} ${requestLabel(r)}${r.area === "platform" ? " [platform]" : ""}`));
945
1014
  console.log(body(` for: ${r.purpose}`));
946
1015
  if (r.considered)
947
1016
  console.log(body(` considered: ${r.considered}`));
@@ -200,6 +200,29 @@ const TOOLS = [
200
200
  required: ["value", "name", "purpose"],
201
201
  },
202
202
  },
203
+ {
204
+ name: "request_rule",
205
+ description: "File a rule request when this component does something the system's doctrine does not cover, and you had to decide alone. Read `system_doctrine` first: if a rule already answers it, follow it instead of filing. This is for the case with no rule - what the rule would say, and the case that asked for it. Do NOT invent a convention and move on; a decision that lives only in one file is not a decision of the system.",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ name: {
210
+ type: "string",
211
+ description: "What the rule would say, in one line: 'a card that opens a dialog carries the trigger, never the panel'",
212
+ },
213
+ purpose: {
214
+ type: "string",
215
+ description: "The case that asked for it - what you were building when no rule answered.",
216
+ },
217
+ considered: {
218
+ type: "string",
219
+ description: "Which existing rules you read and why they did not answer.",
220
+ },
221
+ file: { type: "string", description: "Where the case lives." },
222
+ },
223
+ required: ["name", "purpose"],
224
+ },
225
+ },
203
226
  ];
204
227
  /**
205
228
  * QUANTAS FERRAMENTAS ESTE SERVIDOR SERVE, lido da lista.
@@ -1037,6 +1060,24 @@ cli) {
1037
1060
  ? `Filed as ${r.id} and routed to the synthesisui platform team - the system's contract already promises this and the shipped css does not deliver it. No one needs to act: it closes itself when an update lands. Keep the quiet base meanwhile.`
1038
1061
  : `Filed as ${r.id}. Do not add the token yourself - the request shows up in \`synthesisui doctor\` for a person to decide.`);
1039
1062
  }
1063
+ case "request_rule": {
1064
+ /**
1065
+ * SEM TRIAGEM AUTOMÁTICA, e de propósito.
1066
+ *
1067
+ * `request_token` sabe rotear para a plataforma quando o contrato do sistema já promete o
1068
+ * nome - é uma pergunta que a máquina responde. "Isto deveria ser uma regra?" não é: ela é a
1069
+ * decisão de quem é dono do sistema, e roteá-la sozinho seria inventar a resposta em vez de
1070
+ * abrir a pergunta.
1071
+ */
1072
+ const r = await fileRequest(root, {
1073
+ kind: "rule",
1074
+ name: String(args.name ?? ""),
1075
+ purpose: String(args.purpose ?? ""),
1076
+ considered: args.considered ? String(args.considered) : undefined,
1077
+ file: args.file ? String(args.file) : undefined,
1078
+ });
1079
+ return text(`Filed as ${r.id}. Do not adopt the convention as if it were a rule - it shows up in \`synthesisui doctor\` and travels to the system's queue, for the owner to make it a rule or decline it.`);
1080
+ }
1040
1081
  default:
1041
1082
  return text(`No tool named ${name}.`, true);
1042
1083
  }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from "node:path";
2
- import { closeRequest, fileRequest, readRequests } from "../doctor/requests.js";
2
+ import { closeRequest, fileRequest, KINDS, readRequests, } from "../doctor/requests.js";
3
3
  import { body, section } from "../output.js";
4
4
  /**
5
5
  * `synthesisui request` - the queue of what the agent needed and was refused.
@@ -12,6 +12,12 @@ import { body, section } from "../output.js";
12
12
  * Closing is deliberately explicit. A request nobody got to is still a
13
13
  * request; nothing here expires.
14
14
  */
15
+ /** Como cada tipo se lê numa linha - um lugar, três telas. */
16
+ const labelOf = (r) => r.kind === "token"
17
+ ? `token ${r.value} as ${r.name}`
18
+ : r.kind === "rule"
19
+ ? `rule "${r.name}"`
20
+ : `component "${r.name}"`;
15
21
  export async function request(opts) {
16
22
  const root = resolve(opts.dir ?? process.cwd());
17
23
  if (opts.done) {
@@ -29,7 +35,7 @@ export async function request(opts) {
29
35
  return;
30
36
  }
31
37
  for (const r of all) {
32
- console.log(body(` ${r.id} ${r.kind === "token" ? `token ${r.value} as ${r.name}` : `component "${r.name}"`}`));
38
+ console.log(body(` ${r.id} ${labelOf(r)}`));
33
39
  console.log(body(` for: ${r.purpose}`));
34
40
  if (r.considered)
35
41
  console.log(body(` considered: ${r.considered}`));
@@ -40,14 +46,23 @@ export async function request(opts) {
40
46
  console.log(body("Close one: synthesisui request --done <id>"));
41
47
  return;
42
48
  }
43
- if (opts.kind !== "component" && opts.kind !== "token") {
44
- console.log(`Unknown kind "${opts.kind}" - component or token.`);
49
+ if (!KINDS.has(opts.kind)) {
50
+ console.log(`Unknown kind "${opts.kind}" - component, token or rule.`);
45
51
  return;
46
52
  }
53
+ /**
54
+ * UMA REGRA PRECISA DO CASO, e é por isso que ela exige `--for` como as outras.
55
+ *
56
+ * "Isto deveria ser uma regra" sem o caso que a motivou é uma opinião. Com o caso, quem decidir do
57
+ * outro lado tem o que a doutrina não cobria e onde isso apareceu - que é a diferença entre uma
58
+ * fila que vira decisão e uma que vira backlog.
59
+ */
47
60
  if (!opts.name || !opts.purpose || (opts.kind === "token" && !opts.value)) {
48
61
  console.log(opts.kind === "token"
49
62
  ? "A token request needs --value, --name and --for."
50
- : "A component request needs --name and --for.");
63
+ : opts.kind === "rule"
64
+ ? "A rule request needs --name and --for - what the rule would say, and the case that asked for it."
65
+ : "A component request needs --name and --for.");
51
66
  return;
52
67
  }
53
68
  const r = await fileRequest(root, {
@@ -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,49 @@ 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
+ if (usage.length > 0)
323
+ fresh.usage = usage;
324
+ for (const key of ["scheme", "reading"])
325
+ if (fresh[key] == null && before?.[key] != null)
326
+ fresh[key] = before[key];
327
+ /**
328
+ * E A POLARIDADE VEM DA PLATAFORMA quando nem a medição nem o arquivo a têm.
329
+ *
330
+ * Carregar do arquivo anterior conserta quem ainda não perdeu. Quem já perdeu - o censo do dono,
331
+ * medido em 13/08 - ficaria sem polaridade para sempre, porque o `sync` não a mede: ela é uma
332
+ * resposta dada no import. O documento guardado sabe (`meta.scheme`), então a rota devolve e isto
333
+ * recoloca. O próximo `sync` de quem estava furado repara o censo dele.
334
+ */
335
+ if (fresh.scheme == null && stored.scheme)
336
+ fresh.scheme = stored.scheme;
279
337
  /**
280
338
  * O CENSO FRESCO TAMBÉM FICA NO DISCO.
281
339
  *
@@ -22,6 +22,8 @@ import { join } from "node:path";
22
22
  * how a team shares a queue.
23
23
  */
24
24
  export const REQUESTS_FILE = "requests.jsonl";
25
+ /** Os tipos que a fila aceita, num lugar só - ver `GapRequest.kind`. */
26
+ export const KINDS = new Set(["component", "token", "rule"]);
25
27
  const path = (root) => join(root, "_synthesisui", REQUESTS_FILE);
26
28
  /** Stable-enough id from content: 6 chars, collision-safe at queue scale. */
27
29
  function idOf(kind, name, at) {
@@ -65,7 +67,14 @@ export async function readRequests(root) {
65
67
  continue;
66
68
  try {
67
69
  const r = JSON.parse(line);
68
- if (r?.id && r.name && (r.kind === "component" || r.kind === "token"))
70
+ /**
71
+ * O FILTRO DA LEITURA - e ele é um dos três lugares onde um tipo novo some CALADO.
72
+ *
73
+ * Uma linha com `kind` desconhecido é descartada aqui sem erro, então acrescentar um tipo sem
74
+ * passar por este ponto produz um pedido que o agente arquiva, o arquivo guarda, e ninguém
75
+ * nunca lê.
76
+ */
77
+ if (r?.id && r.name && KINDS.has(r.kind))
69
78
  out.push(r);
70
79
  }
71
80
  catch {
@@ -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
  *
@@ -0,0 +1,262 @@
1
+ /**
2
+ * A SKILL DE MANUTENÇÃO, como o CLI a distribui.
3
+ *
4
+ * Mesmo motivo do `skill-init.ts` e do `skill-import.ts`: o build é `tsc` e nada mais, então um `.md`
5
+ * precisaria de um passo de cópia que pode silenciosamente não rodar. Um módulo TypeScript não pode
6
+ * falhar em ser empacotado.
7
+ *
8
+ * ESTA É A FONTE. A cópia em `.claude/skills/` é o que o nosso editor lê, e o spec assere que as duas
9
+ * são idênticas.
10
+ *
11
+ * E ela é a TERCEIRA que o `connect` instala - as outras duas são de ENTRADA (primeira corrida,
12
+ * import). Esta responde a pergunta do dia seguinte, apontando para uma tela: *"isso aqui está de
13
+ * acordo com o meu design system?"*. Deixá-la fora do `connect` faria dela uma skill nossa, e o caso
14
+ * de uso que a motivou é o cliente rodando sozinho toda semana.
15
+ */
16
+ export const ADAPT_SKILL = `---
17
+ name: sui-adapt
18
+ description: Confronta UM componente (ou uma página, ou uma pasta) contra o design system instalado e adapta o que der - cobertura de token medida, conserto mecânico proposto antes de escrever, e o que sobra classificado em "regra nova" ou "conserto local". Use quando alguém aponta para uma peça e pergunta se ela está de acordo com o sistema (ex. "/sui-adapt components/ui/card", "analisa esse componente aqui", "isso aqui segue o meu design system?"), como rotina semanal, ou logo depois de criar/alterar um componente. Mede antes de propor, propõe antes de escrever, e nunca arquiva pedido em nome de ninguém.
19
+ ---
20
+
21
+ # Adaptar uma peça ao sistema
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
+
28
+ O propósito do produto, que decide todo empate abaixo: **ler o repositório do cliente e
29
+ devolver receitas com paridade visual, semântica e funcional, sem supor e sem inventar
30
+ nada.**
31
+
32
+ Esta skill é a ponta de manutenção. As outras duas que o cliente tem são de ENTRADA -
33
+ \`sui-init\` e \`sui-import-ds\` transformam o repositório dele em sistema. Esta responde a
34
+ pergunta do dia seguinte, que ele faz apontando para uma tela: *"isso aqui está de acordo
35
+ com o meu design system?"*.
36
+
37
+ \`CLAUDE.md\` manda. Quando os dois divergirem, este arquivo é que está velho.
38
+
39
+ ---
40
+
41
+ ## 0. O QUE ESTA SKILL NÃO FAZ
42
+
43
+ Três limites, e cada um existe por um motivo que já custou alguma coisa:
44
+
45
+ \`\`\`
46
+ não escreve sem propor é o repositório DELE. \`--fix --write\` sem confirmação é
47
+ outra categoria de confiança, e uma skill que perde essa
48
+ confiança não é rodada uma segunda vez
49
+ não arquiva pedido \`request\` fila uma decisão na plataforma. A skill MOSTRA o
50
+ comando; quem roda é ele
51
+ não inventa token, nome um agente que cala um relatório fazendo o sistema crescer é
52
+ nem regra pior que a deriva que ele veio medir
53
+ \`\`\`
54
+
55
+ ## 1. O ALVO, E POR QUE O ESCOPO É DECISÃO DA SKILL
56
+
57
+ Componente quase nunca é um arquivo. \`Card.tsx\` costuma vir com \`Card.css\`, \`Card.stories.tsx\`
58
+ e às vezes um \`index.ts\` - e medir só o \`.tsx\` produz um número que mente por omissão: o css
59
+ ao lado é justamente onde os valores à mão se escondem.
60
+
61
+ \`\`\`
62
+ 1. resolva o alvo o que ele apontou, ou o arquivo aberto, ou o que ele acabou de mexer
63
+ 2. suba para a PASTA quando o irmão existir (mesmo nome, extensão diferente)
64
+ 3. DIGA qual escopo você usou, com o número de arquivos
65
+ \`\`\`
66
+
67
+ Nunca meça os dois e escolha o maior. Diga o que mediu.
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
+
76
+ ## 2. MEÇA - e a medida é determinística, não sua
77
+
78
+ Duas portas, mesma resposta. Use a que a sessão tiver:
79
+
80
+ \`\`\`
81
+ MCP check_file { path }
82
+ terminal npx synthesisui doctor <alvo>
83
+ \`\`\`
84
+
85
+ O que volta, medido num componente real (\`ArticleCard\`, 13/08):
86
+
87
+ \`\`\`
88
+ SignalUI v7 - 181 tokens, 1 file read
89
+ scope: packages/ui/src/lib/SignalUI/organisms/ArticleCard/ArticleCard.tsx
90
+
91
+ Token coverage ░░░░░░░░░░░░░░░░░░░░░░░░ 0%
92
+ 0 from the system, 2 by hand
93
+ 50% is one command away - 1 of those have a name waiting
94
+ \`\`\`
95
+
96
+ Três números, e eles já vêm separados por natureza:
97
+
98
+ \`\`\`
99
+ from the system já usa o vocabulário. Nada a fazer
100
+ have a name waiting o sistema JÁ nomeia esse valor -> mecânico, é o passo 3
101
+ no name for it o sistema não nomeia -> DECISÃO dele, é o passo 5
102
+ \`\`\`
103
+
104
+ Não recalcule nada disso de cabeça. O número que você reporta é o que o comando disse.
105
+
106
+ ## 3. MONTE A FILA, E CONTE OS ITENS ANTES DE COMEÇAR
107
+
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**:
114
+
115
+ \`\`\`
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
119
+ \`\`\`
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
+
125
+ \`\`\`
126
+ 7 itens nesta fila: 2 sem mudar pixel, 3 que mudam, 2 pedidos.
127
+ \`\`\`
128
+
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:
133
+
134
+ \`\`\`
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]
141
+ \`\`\`
142
+
143
+ Quatro opções, e nenhuma a mais:
144
+
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
156
+
157
+ O passo 2 é determinístico e sai de graça. Este não: o sistema carrega uma doutrina em
158
+ prosa, e **nada a verifica mecanicamente**. É aqui que você trabalha.
159
+
160
+ \`\`\`
161
+ MCP system_doctrine
162
+ terminal as regras viajam no documento instalado (_synthesisui/ds/<slug>/doctrine.json)
163
+ \`\`\`
164
+
165
+ Leia as regras e confronte o componente com cada uma. Três respostas possíveis por regra,
166
+ e a terceira é a que interessa:
167
+
168
+ \`\`\`
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\`
172
+ \`\`\`
173
+
174
+ Uma regra que você teve que interpretar para aplicar não é "cumpre". É o terceiro caso.
175
+
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.
178
+
179
+ ## 6. OS PEDIDOS - você mostra o comando, ele roda
180
+
181
+ Três destinos, e todos existem:
182
+
183
+ \`\`\`
184
+ valor sem nome no sistema
185
+ -> npx synthesisui request token --value "<valor>" --name "<como se chamaria>" --for "<o caso>"
186
+ peça que falta
187
+ -> npx synthesisui request component --name "<nome>" --for "<o caso>"
188
+ a doutrina não cobre este caso
189
+ -> npx synthesisui request rule --name "<o que a regra diria>" --for "<o caso que pediu>"
190
+ \`\`\`
191
+
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.
195
+
196
+ ## 6b. A RÉGUA É O ALCANÇÁVEL, E O TETO SE DIZ JUNTO
197
+
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.
201
+
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%
214
+
215
+ \`\`\`
216
+ <Componente> <n> arquivos
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
222
+ \`\`\`
223
+
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
+ \`\`\`
247
+
248
+ ## 7. ROTINA
249
+
250
+ Ela foi desenhada para duas horas do dia, e a segunda é a que mais rende:
251
+
252
+ \`\`\`
253
+ semanal "roda o sui-adapt no dashboard" - pega deriva antes de virar hábito
254
+ depois de mexer componente novo, ou alteração grande: rode ANTES do commit, enquanto a
255
+ decisão ainda está quente e o conserto ainda é barato
256
+ \`\`\`
257
+
258
+ O hook (\`PostToolUse\`) já roda a metade determinística a cada escrita, calado quando não há
259
+ o que dizer. Esta skill é o passo deliberado: ela junta o hook, a doutrina e a fila numa
260
+ conversa só, e termina com o cliente decidindo - não com um relatório.
261
+ `;
262
+ export const ADAPT_SKILL_PATH = ".claude/skills/sui-adapt/SKILL.md";
package/dist/skills.js ADDED
@@ -0,0 +1,39 @@
1
+ import { ADAPT_SKILL, ADAPT_SKILL_PATH } from "./skill-adapt.js";
2
+ import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "./skill-import.js";
3
+ import { INIT_SKILL, INIT_SKILL_PATH } from "./skill-init.js";
4
+ /**
5
+ * AS SKILLS QUE O CLI DISTRIBUI, numa lista só - e ela existe porque DOIS comandos precisam dela.
6
+ *
7
+ * `connect` escreve; `align` cobra o que falta. Enquanto a lista morava dentro do `connect`, o
8
+ * `align` não tinha como saber que existia uma terceira - e uma skill nova só chegava a quem, por
9
+ * conta própria, rodasse `connect` de novo. Ninguém roda um comando de novo sem motivo.
10
+ *
11
+ * É a lei 8 no caso mais barato dela: a lacuna existe, a gente sabe qual é, e dizer custa uma linha.
12
+ */
13
+ export const SKILLS = [
14
+ {
15
+ path: INIT_SKILL_PATH,
16
+ source: INIT_SKILL,
17
+ label: "/sui-init",
18
+ what: "the first run, start to finish",
19
+ },
20
+ {
21
+ path: IMPORT_SKILL_PATH,
22
+ source: IMPORT_SKILL,
23
+ label: "/sui-import-ds",
24
+ what: "the import, orchestrated",
25
+ },
26
+ /**
27
+ * A DE MANUTENÇÃO, e ela é de outra natureza que as duas acima.
28
+ *
29
+ * As duas primeiras são de ENTRADA: rodam uma vez, e depois nunca mais. Esta responde a pergunta do
30
+ * dia seguinte, apontando para uma tela - *"isso aqui está de acordo com o meu design system?"* -,
31
+ * toda semana ou depois de mexer em alguma coisa. É a primeira que uma pessoa roda mais de uma vez.
32
+ */
33
+ {
34
+ path: ADAPT_SKILL_PATH,
35
+ source: ADAPT_SKILL,
36
+ label: "/sui-adapt",
37
+ what: "one component against the system, and what to do about it",
38
+ },
39
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.220",
3
+ "version": "0.16.225",
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": {