synthesisui 0.16.228 → 0.16.233

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { deltaE, JND } from "./doctor/color-distance.js";
2
- import { nearestToken } from "./doctor/tokens.js";
2
+ import { familySays, nearestToken, normalizeValue, } from "./doctor/tokens.js";
3
3
  /** Onde cada tipo de valor mora na fundação. `color` é o único com dois segmentos. */
4
4
  const HOME = {
5
5
  color: "color",
@@ -9,8 +9,31 @@ const HOME = {
9
9
  motion: "motion.durations",
10
10
  };
11
11
  /** Uma palavra de categoria no começo do nome deles não é família - é a categoria repetida. */
12
- const CATEGORY = /^(color|colour|radius|rounded|spacing|space|gap|font|type|duration|motion|ease|easing)-/;
12
+ const CATEGORY = /^(color|colour|radius|rounded|spacing|space|gap|font|type|text|duration|motion|ease|easing)-/;
13
13
  const clean = (name) => name.replace(/^--/, "").toLowerCase();
14
+ /**
15
+ * QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e aqui a natureza do ACHADO decide.
16
+ *
17
+ * `24px` é `--text-h2` E `--spacing-lg` no vocabulário do repositório real. Um `[0]` pega o de tipo
18
+ * para um achado de espaçamento, e o caminho que sai daí é `typography.h2` sobre uma margem - o mesmo
19
+ * erro de categoria que o `crossFamily` foi criado para impedir do outro lado da esteira.
20
+ *
21
+ * A pista são as palavras de família do nome dele - `familySays`, a MESMA tabela que o mapa de
22
+ * vocabulário usa para recusar uma sugestão de categoria trocada. Duas listas para a mesma pergunta é
23
+ * como duas telas passam a discordar sobre o mesmo `12px`.
24
+ *
25
+ * Quando nenhum candidato concorda com a natureza do achado, COR ainda passa - um hex numa posição de
26
+ * cor não tem ambiguidade de família - e comprimento não passa: sem concordância não há o que
27
+ * separasse um token de tipo de um de espaçamento, e o valor volta sem caminho para a pessoa decidir.
28
+ */
29
+ function nameOf(kind, candidates) {
30
+ if (!candidates || candidates.length === 0)
31
+ return undefined;
32
+ const agrees = candidates.find((n) => familySays(kind, n));
33
+ if (agrees)
34
+ return agrees;
35
+ return kind === "color" ? candidates[0] : undefined;
36
+ }
14
37
  /**
15
38
  * O CAMINHO A PARTIR DO NOME DELES, e nada além dele.
16
39
  *
@@ -87,10 +110,34 @@ export function absorbPlan(d, theirs, have, cap = 40) {
87
110
  for (const r of unnamed) {
88
111
  if (entries.length >= cap)
89
112
  break;
90
- const theirNames = theirs.byValue.get(r.literal.toLowerCase()) ?? [];
91
- const theirName = theirNames[0];
113
+ /**
114
+ * O VALOR NORMALIZADO, e não o literal cru - era aqui que a metade pronta desta lista sumia.
115
+ *
116
+ * `byValue` é indexada pela forma canônica: cor em `#rrggbbaa`, comprimento em px sobre a raiz
117
+ * medida. A busca usava o literal como ele aparece no código, então:
118
+ *
119
+ * get("#fff") -> nada get("#ffffffff") -> --dashboard-white-500
120
+ * get("1em") -> nada get("16px") -> --spacing-md
121
+ * get("24px") -> acertava por acidente, porque px já é a forma canônica
122
+ *
123
+ * Cor NUNCA casava (a chave tem alfa), `em`/`rem` nunca casavam, e `px` casava por coincidência.
124
+ * Medido no repositório real em 14/08:
125
+ *
126
+ * na lista que ele VÊ (as 40 mais repetidas) 0 -> 5 com nome dele, 404 arquivos
127
+ * na fila inteira (338 valores) 1 -> 22 com nome dele, 478 arquivos
128
+ *
129
+ * A primeira leitura desta medição contou os 22 e escreveu "22 de 40", que é a fila inteira sobre
130
+ * o denominador da página - o número certo na conta errada. São 5 na tela e 22 na fila.
131
+ *
132
+ * O custo não era só a lista curta: o `doctor` mandava trocar `#fff` por
133
+ * `var(--dashboard-white-500)` e o `absorb`, no mesmo dia, pedia que ele batizasse `#fff`. Dois
134
+ * comandos com conselhos opostos sobre o mesmo valor.
135
+ */
136
+ const theirName = nameOf(r.kind, theirs.byValue.get(normalizeValue(r.literal, theirs.rootPx)));
92
137
  const path = pathFor(r.kind, theirName);
93
- const near = nearestOwn(theirs, r.literal, r.kind);
138
+ /** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
139
+ * vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
140
+ const near = theirName ? undefined : nearestOwn(theirs, r.literal, r.kind);
94
141
  /** Já existe na fundação com OUTRO valor: absorver aqui seria repintar o token deles. */
95
142
  if (path && have.has(path))
96
143
  continue;
@@ -3,7 +3,6 @@ import { dirname, join, resolve } from "node:path";
3
3
  import { absorbPlan, describeAbsorb, needingName, } from "../absorb-plan.js";
4
4
  import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
5
5
  import { diagnose, scanSource } from "../doctor/scan.js";
6
- import { buildTable } from "../doctor/tokens.js";
7
6
  import { measuredScope, scopePaths } from "../measured-scope.js";
8
7
  import { body, paint, section, snippet } from "../output.js";
9
8
  import { loadSystem, walkAll } from "./doctor.js";
@@ -48,12 +47,12 @@ export async function absorb(opts) {
48
47
  * eles chamam o valor, e é de lá que o nome sai. Lida da RAIZ mesmo quando a medição é escopada: o
49
48
  * vocabulário é do projeto, e um app que pega um token emprestado do `packages/ui` deles não o
50
49
  * redeclara.
50
+ *
51
+ * A varredura era daqui e mudou de lugar: `loadSystem` a faz agora, porque o `doctor`, o `hook` e
52
+ * o MCP passaram a precisar da MESMA tabela dele - ver `their-names.ts`. Este comando descreveu a
53
+ * ideia primeiro e ficou com a implementação de todo mundo.
51
54
  */
52
- let css = "";
53
- for await (const file of walkAll([root]))
54
- if (/\.(css|scss|sass|less)$/i.test(file))
55
- css += `\n${await readFile(file, "utf8").catch(() => "")}`;
56
- const theirs = buildTable({ css, source: "yours" });
55
+ const theirs = installed.theirs;
57
56
  const reports = [];
58
57
  for await (const file of walkAll(roots)) {
59
58
  const source = await readFile(file, "utf8").catch(() => null);
@@ -12,8 +12,9 @@ import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, } f
12
12
  import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
13
13
  import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
14
14
  import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
15
- import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
15
+ import { diagnose, nameToWrite, scanSource, siblingTokens, } from "../doctor/scan.js";
16
16
  import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
17
+ import { withTheirNames } from "../doctor/their-names.js";
17
18
  import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
18
19
  import { describeScope, measuredScope, scopePaths } from "../measured-scope.js";
19
20
  import { body, paint, section, snippet } from "../output.js";
@@ -167,8 +168,12 @@ measured) {
167
168
  .map((e) => e.name);
168
169
  }
169
170
  catch {
171
+ /** Sem nada nosso instalado, o vocabulário dele é o ÚNICO que existe - e é contra ele que a
172
+ * medição acontece. Era a queda que já existia, agora servida do mesmo lugar que o resto. */
173
+ const theirs = await harvestOwnTokens([root], measured);
170
174
  return {
171
175
  table: EMPTY_TABLE,
176
+ theirs,
172
177
  recipes: new Map(),
173
178
  documents: [],
174
179
  requires: [],
@@ -275,13 +280,39 @@ measured) {
275
280
  }
276
281
  }
277
282
  }
283
+ const ours = buildTable({
284
+ css,
285
+ lock,
286
+ source: adopted ? "adopted" : "installed",
287
+ ...(measured ? { rootPx: measured.px, rootFrom: measured.from } : {}),
288
+ });
289
+ /**
290
+ * O VOCABULÁRIO DELE, LIDO SEMPRE - e isto revisa uma escolha declarada, então ela se revisa em
291
+ * voz alta.
292
+ *
293
+ * A nota antiga aqui dizia que o `hook` NÃO varre css de propósito, por orçamento: ele roda depois
294
+ * de cada escrita, e uma varredura por edição trocaria um relatório instantâneo por um que a
295
+ * pessoa desliga. O princípio dela continua valendo e é ele que manda mudar agora - *"erra para o
296
+ * lado de oferecer MENOS, nunca de oferecer a troca errada"*. Sem o vocabulário dele, o hook não
297
+ * oferece menos: ele oferece o NOME ERRADO, mandando escrever `--ds-color-gray-400` num arquivo de
298
+ * um repositório que declara `--color-lightgray-700` para aquele mesmo valor.
299
+ *
300
+ * O custo, medido ponta a ponta no repositório real em 13/08 - o hook inteiro, e não a varredura
301
+ * isolada, porque é o hook que tem o orçamento:
302
+ *
303
+ * antes 80ms mediana de 5 execuções
304
+ * depois 84ms mediana de 9, sobre 686 folhas e 797KB de css
305
+ *
306
+ * A primeira estimativa desta varredura foi 28ms, medida num processo à parte e sem o `walkAll`
307
+ * do CLI, que pula o que não interessa. Ficam os 4ms medidos, que são o que a pessoa paga.
308
+ *
309
+ * E a raiz medida atravessa junto, para as duas tabelas normalizarem `4px` e `0.25rem` na mesma
310
+ * unidade. Uma raiz por tabela seria a forma mais silenciosa de discordar.
311
+ */
312
+ const theirs = await harvestOwnTokens([root], measured);
278
313
  return {
279
- table: buildTable({
280
- css,
281
- lock,
282
- source: adopted ? "adopted" : "installed",
283
- ...(measured ? { rootPx: measured.px, rootFrom: measured.from } : {}),
284
- }),
314
+ table: withTheirNames(ours, theirs),
315
+ theirs,
285
316
  recipes,
286
317
  documents,
287
318
  requires,
@@ -315,14 +346,22 @@ async function sheetsIn(roots) {
315
346
  }
316
347
  return out;
317
348
  }
318
- async function harvestOwnTokens(roots) {
349
+ async function harvestOwnTokens(roots,
350
+ /** A MESMA raiz que a nossa tabela usa - duas raízes seriam a forma mais silenciosa de discordar,
351
+ * e `4px` deixaria de casar com `0.25rem` de um lado só. */
352
+ measured) {
319
353
  let css = "";
320
354
  for await (const file of walkAll(roots)) {
321
355
  if (!/\.(css|scss|sass|less)$/i.test(file))
322
356
  continue;
323
357
  css += `\n${await readFile(file, "utf8").catch(() => "")}`;
324
358
  }
325
- return buildTable({ css, source: "yours" });
359
+ return buildTable({
360
+ css,
361
+ source: "yours",
362
+ rootPx: measured?.px,
363
+ rootFrom: measured?.from ?? null,
364
+ });
326
365
  }
327
366
  const KIND_LABEL = {
328
367
  color: "colour",
@@ -528,6 +567,14 @@ export async function doctor(opts) {
528
567
  from: rootSize.from,
529
568
  });
530
569
  const { recipes, documents } = installed;
570
+ /**
571
+ * A TABELA JÁ VEM COM O VOCABULÁRIO DELE DENTRO - ver `loadSystem` e `their-names.ts`.
572
+ *
573
+ * O que muda aqui é só a QUEDA: quando não há nada nosso instalado, a dele passa a ser a tabela
574
+ * inteira, que é o que sempre foi. Ela é lida da RAIZ mesmo com a leitura escopada, pela mesma
575
+ * razão de sempre: o vocabulário é do projeto, e um app que pega um token emprestado do
576
+ * `packages/ui` dele não o redeclara.
577
+ */
531
578
  let table = installed.table;
532
579
  // Nothing of ours here does not mean nothing to measure against. Fall back
533
580
  // to whatever vocabulary the project already declares for itself.
@@ -545,9 +592,20 @@ export async function doctor(opts) {
545
592
  * Same rule our own installed system already follows: found from the root,
546
593
  * measured in the scope.
547
594
  */
548
- table = await harvestOwnTokens([root]);
595
+ table = withTheirNames(installed.theirs, installed.theirs);
549
596
  }
550
597
  const hasSystem = table.byName.size > 0;
598
+ /**
599
+ * O NOME DELE, ONDE ANTES ESTAVA "the system".
600
+ *
601
+ * O cabeçalho já abre com `SignalUI v8` e três linhas abaixo o mesmo relatório dizia *"0 from the
602
+ * system"* num repositório que tem DOIS vocabulários - o dele e o nosso. "the system" obriga quem lê
603
+ * a adivinhar de qual dos dois a linha fala, e a resposta certa é a que ele mesmo batizou. Mesma
604
+ * decisão do #937 um nível acima: o nome que a gente devolve é o nome dele.
605
+ *
606
+ * Quando não há sistema instalado, cai em "this system", porque aí não existe nome para usar.
607
+ */
608
+ const systemName = table.name ?? table.slug ?? "this system";
551
609
  const reports = [];
552
610
  const overrides = [];
553
611
  const used = new Map();
@@ -655,7 +713,7 @@ export async function doctor(opts) {
655
713
  */
656
714
  if (scopes.length > 0) {
657
715
  const said = measured.from === "census" ? describeScope(measured) : null;
658
- console.log(body(said ?? `scope: ${relScopes.join(", ")}`));
716
+ console.log(body(said ?? `read from: ${relScopes.join(", ")}`));
659
717
  }
660
718
  /**
661
719
  * A RAIZ QUE ESTA RODADA USOU - e ela é impressa SEMPRE, inclusive quando é o padrão.
@@ -690,7 +748,7 @@ export async function doctor(opts) {
690
748
  const asideTotal = [...aside.values()].reduce((n, v) => n + v, 0);
691
749
  if (verbose) {
692
750
  for (const [reason, count] of aside) {
693
- console.log(body(`set aside: ${plural(count, "value")} in ${reason}`));
751
+ console.log(body(`out of the count: ${plural(count, "value")} in ${reason}`));
694
752
  }
695
753
  }
696
754
  else if (asideTotal > 0) {
@@ -699,9 +757,13 @@ export async function doctor(opts) {
699
757
  // `var(--ds-color-semantic-knob, #ffffff)` - is set aside for the opposite
700
758
  // reason: it is already tokenized. The summary said the false half out
701
759
  // loud and hid the true half behind a flag.
702
- console.log(body(`set aside ${plural(asideTotal, "value")} that are not drift` +
760
+ /**
761
+ * "not drift" era a nossa palavra para o motivo, e ela não diz o motivo. As duas razões reais
762
+ * cabem na linha: ou nenhum token poderia segurar aquele valor, ou ele já vem de um.
763
+ */
764
+ console.log(body(`${plural(asideTotal, "value")} left out of the count - a token could never hold them, or they already come from one` +
703
765
  (skippedProjects.length > 0
704
- ? ` and ${plural(skippedProjects.length, "nested project")} that speak their own systems`
766
+ ? `, and ${plural(skippedProjects.length, "nested project")} that speak their own systems`
705
767
  : "") +
706
768
  " (--verbose for why)"));
707
769
  }
@@ -784,7 +846,7 @@ export async function doctor(opts) {
784
846
  ...(d.findings.some((f) => f.crossFamily)
785
847
  ? { crossFamily: d.findings.filter((f) => f.crossFamily).length }
786
848
  : {}),
787
- ...(d.repeats.some((r) => r.token)
849
+ ...(d.repeats.some((r) => nameToWrite(r))
788
850
  ? { matched: suggestionsFrom(d.repeats) }
789
851
  : {}),
790
852
  }).catch(() => { });
@@ -835,7 +897,7 @@ export async function doctor(opts) {
835
897
  if (hasSystem && measurable) {
836
898
  console.log("");
837
899
  console.log(body(`Token coverage ${meter(d.coverage)} ${paint.strong(`${String(d.coverage).padStart(3)}%`)}`));
838
- console.log(body(paint.dim(` ${d.tokenUses} from the system${d.ownUses > 0 ? `, ${d.ownUses} from your own tokens` : ""}, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`)));
900
+ console.log(body(paint.dim(` ${d.tokenUses} from ${systemName}${d.ownUses > 0 ? `, ${d.ownUses} from your own tokens` : ""}, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`)));
839
901
  /**
840
902
  * A CAMADA DE TOKEN DELE, CONTADA - e a decisão é do dono, em 13/08.
841
903
  *
@@ -850,7 +912,7 @@ export async function doctor(opts) {
850
912
  * os 122 usos seguem o sistema sem tocar em um componente sequer.
851
913
  */
852
914
  if (d.ownTokens > 0) {
853
- console.log(body(paint.dim(` ${d.ownTokens} token${d.ownTokens === 1 ? "" : "s"} of your own, used ${d.ownUses}x${d.ownMirrored > 0 ? ` - ${d.ownMirrored} hold a value this system also names` : ""}`)));
915
+ console.log(body(paint.dim(` ${d.ownTokens} token${d.ownTokens === 1 ? "" : "s"} of your own, used ${d.ownUses}x${d.ownMirrored > 0 ? ` - ${d.ownMirrored} hold a value ${systemName} also names` : ""}`)));
854
916
  if (d.ownMirrored > 0)
855
917
  console.log(body(paint.dim(` point those at the \`--ds-*\` that holds it: ${d.ownMirrored} lines, one file`)));
856
918
  }
@@ -869,12 +931,34 @@ export async function doctor(opts) {
869
931
  * Medido no repo do dono: 1 uso do sistema, 4 433 à mão, 243 com nome - o medidor diz 0% e a
870
932
  * linha diz 5%.
871
933
  */
872
- const nameable = d.findings.filter((f) => f.token).length;
934
+ const named = d.findings.filter((f) => nameToWrite(f));
935
+ /**
936
+ * "UM COMANDO DE DISTÂNCIA" SÓ VALE PARA O QUE O COMANDO ESCREVE - ver `Finding.fontRelative`.
937
+ *
938
+ * `1em` ganhou nome nesta versão, e ganhar nome não é ganhar troca automática: o `--fix` recusa
939
+ * o comprimento que segue a fonte do elemento, porque o pixel pode mudar. Contá-los aqui faria a
940
+ * tela prometer 100% e o comando entregar 0 - e um relatório que promete o que o comando seguinte
941
+ * recusa gasta mais confiança do que teria custado dizer o número menor.
942
+ *
943
+ * Medido no arquivo que o dono apontou (13/08): 4 valores, 4 com nome, e os 4 são `em`. A linha
944
+ * dizia "100% is one command away" sobre um `--fix` que não escreveria nenhum deles.
945
+ */
946
+ const nameable = named.filter((f) => !f.fontRelative).length;
947
+ const yours = named.length - nameable;
873
948
  if (nameable > 0) {
874
949
  const reach = Math.round(((d.tokenUses + nameable) / (d.tokenUses + d.findings.length)) * 100);
875
950
  console.log(body(paint.dim(` ${reach}% is one command away - ${nameable} of those have a name waiting`)));
876
951
  console.log(body(paint.dim(" npx synthesisui doctor --fix")));
877
952
  }
953
+ /**
954
+ * E O QUE TEM NOME MAS NÃO TEM TROCA APARECE MESMO ASSIM, com o motivo.
955
+ *
956
+ * Sem esta linha, o `em` volta a ser invisível por outra porta: ele sai da conta de "um comando
957
+ * de distância" e não entra em lugar nenhum, e a pessoa fica sem saber que o sistema dela nomeia
958
+ * aquele valor. A lacuna é declarada, não escondida.
959
+ */
960
+ if (yours > 0)
961
+ console.log(body(paint.dim(` ${yours} more your system names, in \`em\` - the same number, but it follows the element's font size, so that swap is yours to make`)));
878
962
  /**
879
963
  * ZERO NUM REPO QUE ORIGINOU O SISTEMA É O ESTADO CERTO, e sem esta linha ele lê como falha
880
964
  * NOSSA.
@@ -903,14 +987,14 @@ export async function doctor(opts) {
903
987
  */
904
988
  if (d.coverage === 0 && measured.system) {
905
989
  console.log(body(paint.dim(` zero is the expected start here - this system was measured FROM`)));
906
- console.log(body(paint.dim(` \`${measured.system}\`, so these values are its source, not a drift`)));
907
- console.log(body(paint.dim(` from it. They count once the code points at the names they became.`)));
990
+ console.log(body(paint.dim(` \`${measured.system}\`, so these values are where the tokens`)));
991
+ console.log(body(paint.dim(` came from. They count once the code points at the names they became.`)));
908
992
  }
909
993
  // The strongest number leads, not trails: it used to sit two screens
910
994
  // down, after the phantom list (30/07). For the reader who already owns
911
995
  // a system - the ICP - THIS line is the report.
912
996
  if (table.source === "yours" && d.findings.length > 0) {
913
- const named = d.findings.filter((f) => f.token).length;
997
+ const named = d.findings.filter((f) => nameToWrite(f)).length;
914
998
  if (named > 0) {
915
999
  console.log("");
916
1000
  console.log(body(`${paint.strong(String(named))} of the ${d.findings.length} hand-written values have a name waiting in YOUR system - they are still literals.`));
@@ -950,7 +1034,7 @@ export async function doctor(opts) {
950
1034
  named: d.named,
951
1035
  ...(crossFamily > 0 ? { crossFamily } : {}),
952
1036
  /** E os pares em si, para a plataforma reavaliar sem esperar outra medição - ver `matched`. */
953
- ...(d.repeats.some((r) => r.token)
1037
+ ...(d.repeats.some((r) => nameToWrite(r))
954
1038
  ? { matched: suggestionsFrom(d.repeats) }
955
1039
  : {}),
956
1040
  });
@@ -979,8 +1063,9 @@ export async function doctor(opts) {
979
1063
  * para 37% sem uma linha de código mudada. Somar as duas fotos anunciaria um progresso
980
1064
  * inventado; calar a mudança deixaria alguém procurar o que fez o número pular.
981
1065
  */
1066
+ /** E SEM O NÚMERO DA VERSÃO: o que mudou a conta é a conta, não a nossa release. */
982
1067
  if (record.ruleChanged)
983
- console.log(body(" the coverage ruler changed in 0.16.219 - your own tokens now count, so the trend restarts here."));
1068
+ console.log(body(" the way this is counted changed - your own tokens count now too, so the trend restarts here."));
984
1069
  }
985
1070
  }
986
1071
  /**
@@ -1072,7 +1157,14 @@ export async function doctor(opts) {
1072
1157
  console.log(body("Add the name to the system, or use one it has. Do not leave it."));
1073
1158
  }
1074
1159
  if (d.findings.length > 0) {
1075
- say(section("Drift"));
1160
+ /**
1161
+ * "Drift" É A NOSSA PALAVRA para um valor fora do sistema, e era o TÍTULO de uma seção inteira.
1162
+ *
1163
+ * A rodada padrão perdeu o vocabulário nosso em 13/08 e o `--verbose` ficou - a guarda rodava
1164
+ * sobre a saída sem `--verbose`, que é o que a maioria lê. O que a seção lista é literal escrito
1165
+ * à mão, e é isso que o título passa a dizer.
1166
+ */
1167
+ say(section("Values written by hand"));
1076
1168
  const order = ["color", "radius", "spacing", "font"].filter((k) => d.counts[k] > 0);
1077
1169
  for (const kind of order) {
1078
1170
  say(body(`${d.counts[kind]} ${KIND_LABEL[kind]}`));
@@ -1087,15 +1179,15 @@ export async function doctor(opts) {
1087
1179
  const w = Math.max(...repeats.map((r) => r.literal.length));
1088
1180
  for (const r of repeats) {
1089
1181
  const where = `${r.count}\u00d7 in ${r.files} file${r.files === 1 ? "" : "s"}`;
1090
- const near = r.token ? null : nearestToken(table, r.literal);
1182
+ const near = nameToWrite(r) ? null : nearestToken(table, r.literal);
1091
1183
  /**
1092
1184
  * COINCIDÊNCIA NÃO É RESPOSTA - ver `crossFamily` em `scan.ts`. Para aquele `kind` o
1093
1185
  * sistema não tem nome; o valor mora noutra família, e dizer só a seta afirma o contrário.
1094
1186
  */
1095
1187
  const named = r.crossFamily
1096
1188
  ? ` → no ${r.kind} named for it · the value lives as ${r.token}`
1097
- : r.token
1098
- ? ` → ${r.token}`
1189
+ : nameToWrite(r)
1190
+ ? ` → ${nameToWrite(r)}`
1099
1191
  : near
1100
1192
  ? ` → nearest is ${near.name} (${near.value})`
1101
1193
  : "";
@@ -1113,11 +1205,11 @@ export async function doctor(opts) {
1113
1205
  for (const x of shown) {
1114
1206
  // A dead end with a neighbour is not a dead end. Only for lengths -
1115
1207
  // "nearly the same blue" is the guess this tool must never make.
1116
- const near = x.token ? null : nearestToken(table, x.literal);
1208
+ const near = nameToWrite(x) ? null : nearestToken(table, x.literal);
1117
1209
  const named = x.crossFamily
1118
1210
  ? `→ no ${x.kind} named for it · the value lives as ${x.token}`
1119
- : x.token
1120
- ? `→ ${x.token}`
1211
+ : nameToWrite(x)
1212
+ ? `→ ${nameToWrite(x)}`
1121
1213
  : near
1122
1214
  ? `→ nearest is ${near.name} (${near.value})`
1123
1215
  : "→ no token holds this value yet";
@@ -1449,25 +1541,36 @@ export async function doctor(opts) {
1449
1541
  * Ele não some: vai para o grupo de quem NÃO tem nome, que é literalmente o que ele é para
1450
1542
  * aquele kind - e a linha diz onde o valor mora hoje.
1451
1543
  */
1452
- const named = d.repeats.filter((x) => x.token && !x.crossFamily);
1453
- const unnamed = d.repeats.filter((x) => !x.token || x.crossFamily);
1544
+ const named = d.repeats.filter((x) => nameToWrite(x) && (!x.crossFamily || x.theirToken));
1545
+ const unnamed = d.repeats.filter((x) => !nameToWrite(x) || (x.crossFamily && !x.theirToken));
1454
1546
  for (const r of named.slice(0, migrating ? 2 : 3)) {
1455
1547
  plan.push({
1456
1548
  rank: migrating ? 4 : 3,
1457
- what: `${r.literal} → ${r.token}`,
1549
+ /** Curto de propósito: a coluna trunca, e uma ressalva cortada ao meio é pior que nenhuma.
1550
+ * O porquê inteiro está na linha do medidor, logo acima. */
1551
+ what: r.fontRelative
1552
+ ? `${r.literal} → ${nameToWrite(r)} · your call`
1553
+ : `${r.literal} → ${nameToWrite(r)}`,
1458
1554
  size: `${r.files} file${r.files === 1 ? "" : "s"}`,
1459
- cheap: true,
1555
+ /** `cheap` promete "não muda um pixel", e um `em` pode mudar - ver `Finding.fontRelative`. */
1556
+ cheap: !r.fontRelative,
1460
1557
  });
1461
1558
  }
1462
1559
  for (const r of unnamed.slice(0, migrating ? 3 : 2)) {
1463
1560
  const near = nearestToken(table, r.literal);
1464
1561
  plan.push({
1465
1562
  rank: migrating ? 3 : 4,
1563
+ /**
1564
+ * "the system has no name for it" era verdade sobre O SISTEMA e o repositório tem dois
1565
+ * vocabulários - o dele também foi consultado (ver `their-names.ts`), e chegar aqui significa
1566
+ * que NENHUM dos dois nomeia o valor. É isso que a linha passa a dizer, e é o que separa esta
1567
+ * fila da anterior: aquela tem nome esperando, esta precisa de um.
1568
+ */
1466
1569
  what: near
1467
1570
  ? `${r.literal} - name it, or snap to ${near.name}`
1468
1571
  : r.crossFamily
1469
1572
  ? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
1470
- : `${r.literal} - the system has no name for it`,
1573
+ : `${r.literal} - no name for it, here or in your CSS`,
1471
1574
  size: `${r.files} file${r.files === 1 ? "" : "s"}`,
1472
1575
  cheap: false,
1473
1576
  });
@@ -1589,7 +1692,7 @@ export async function doctor(opts) {
1589
1692
  * 1500 achados e depois dizer "troquei 900" é fazer a pessoa procurar a linha que importa.
1590
1693
  */
1591
1694
  if (opts.fix) {
1592
- const read = await readerFor(root, d.findings.filter((f) => f.token).map((f) => f.file));
1695
+ const read = await readerFor(root, d.findings.filter((f) => nameToWrite(f)).map((f) => f.file));
1593
1696
  const { result, next } = planFix(d, read);
1594
1697
  const writing = opts.write === true;
1595
1698
  if (writing)
@@ -1,7 +1,7 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import { appendEvent } from "../doctor/ledger.js";
4
- import { diagnose, scanSource } from "../doctor/scan.js";
4
+ import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
5
5
  import { loadSystem } from "./doctor.js";
6
6
  const pass = () => ({ continue: true });
7
7
  const speak = (context) => ({
@@ -56,7 +56,7 @@ async function report(root, filePath) {
56
56
  return null;
57
57
  const rel = relative(root, filePath);
58
58
  const d = diagnose([scanSource(rel, src, table)]);
59
- const named = d.findings.filter((f) => f.token);
59
+ const named = d.findings.filter((f) => nameToWrite(f));
60
60
  const phantoms = d.files.flatMap((f) => f.phantoms ?? []);
61
61
  // Every check leaves one line in the ledger - INCLUDING the clean ones,
62
62
  // because a fix is itself a write, so the clean re-check of a file that was
@@ -83,7 +83,7 @@ async function report(root, filePath) {
83
83
  if (named.length > 0) {
84
84
  lines.push("", "Values written by hand that this system already has a name for:", ...named
85
85
  .slice(0, 20)
86
- .map((f) => ` line ${f.line} ${f.literal} → ${f.token}`), "", "Replace them now, while you still have this file in mind.");
86
+ .map((f) => ` line ${f.line} ${f.literal} → ${nameToWrite(f)}`), "", "Replace them now, while you still have this file in mind.");
87
87
  }
88
88
  if (phantoms.length > 0) {
89
89
  lines.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms.slice(0, 20).map((p) => ` line ${p.line} ${p.name}`), "", "Use a name the system has, or say which value you need and what you would call it. Do NOT invent a token.");
@@ -5,7 +5,7 @@ import { pinnedHookVersion } from "../agent-wiring.js";
5
5
  import { readToken, resolveRegistry } from "../config.js";
6
6
  import { readEvents } from "../doctor/ledger.js";
7
7
  import { fileRequest } from "../doctor/requests.js";
8
- import { diagnose, scanSource } from "../doctor/scan.js";
8
+ import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
9
9
  import { nearestToken, tokenFor } from "../doctor/tokens.js";
10
10
  import { repoStateOf } from "../repo-state.js";
11
11
  import { component } from "./component.js";
@@ -253,8 +253,8 @@ async function checkFile(root, path) {
253
253
  if (d.findings.length > 0) {
254
254
  out.push("", "Written by hand:");
255
255
  for (const f of d.findings.slice(0, 40)) {
256
- out.push(f.token
257
- ? ` ${f.file}:${f.line} ${f.literal} - this system calls it ${f.token}`
256
+ out.push(nameToWrite(f)
257
+ ? ` ${f.file}:${f.line} ${f.literal} - this repo calls it ${nameToWrite(f)}`
258
258
  : ` ${f.file}:${f.line} ${f.literal} - no token holds this value`);
259
259
  }
260
260
  out.push("", "Replace the ones that have a token. For a value with none, do NOT invent a token: say which value it is and what you would call it, and let a person decide.");
@@ -1,3 +1,4 @@
1
+ import { PHRASING_FORMS, } from "./types.js";
1
2
  export const DEFAULT_CONVENTION = {
2
3
  prefix: "ds-",
3
4
  partSeparator: "-",
@@ -106,11 +107,22 @@ function elementFor(name, recipe) {
106
107
  * UI and a hand-written disclosure, because it reads the SHAPE.
107
108
  */
108
109
  if (tag === "button") {
109
- const holdsBlock = (nodes) => nodes.some((n) => n.as === "slot" ||
110
- n.as === "button" ||
111
- n.as === "field" ||
112
- n.as === "component" ||
113
- n.as === "external" ||
110
+ /**
111
+ * A LISTA SAI DE `PHRASING_FORMS`, e a enumeração à mão estava incompleta - conserto de 14/08.
112
+ *
113
+ * Ela esquecia `heading`, `row` e `stack`, e o custo é a marcação que a lei nomeia logo acima: uma
114
+ * raiz `action` com um heading no topo saía como `<button><h3>…</h3></button>`. Reproduzido pelo
115
+ * próprio codegen:
116
+ *
117
+ * preview.kind = "action", parts = [{ as: "heading" }] -> ComponentProps<"button">
118
+ *
119
+ * E o ramo abaixo já sabia que heading importa - ele devolve `article` quando o topo tem um -,
120
+ * então era código morto: o gate nunca deixava chegar até ele.
121
+ *
122
+ * O complemento de phrasing é a pergunta certa, e não uma lista de formas de bloco: são três nomes
123
+ * de um lado contra oito do outro, e a lista curta é a que não esquece um.
124
+ */
125
+ const holdsBlock = (nodes) => nodes.some((n) => !PHRASING_FORMS.includes(n.as) ||
114
126
  (n.children != null && n.children.length > 0));
115
127
  const tree = recipe.preview?.parts;
116
128
  if (tree && tree.length > 0 && holdsBlock(tree)) {
package/dist/config.js CHANGED
@@ -143,19 +143,26 @@ export function intentOf(config, flag) {
143
143
  };
144
144
  return { intent: "adopt", from: "default" };
145
145
  }
146
- /** A frase que acompanha o relatório - ver `intentOf` para por que ela é obrigatória. */
146
+ /**
147
+ * A frase que acompanha o relatório - ver `intentOf` para por que ela é obrigatória.
148
+ *
149
+ * E ELA DIZ O QUE FAZ, não como a gente chama - dono, 13/08, sobre a saída inteira do doctor:
150
+ * *"ele quer saber do que é feito no projeto dele"*. `ADOPTION` e `MIGRATION` são a nossa taxonomia,
151
+ * e quem lê esta linha não tem como saber que uma ordena por "já tem nome" e a outra por "ainda não
152
+ * tem". A informação é a ORDEM DA LISTA que ele está prestes a ler, então é isso que a frase diz.
153
+ */
147
154
  export function describeIntent(source) {
148
155
  const what = source.intent === "migrate"
149
- ? "reading as a MIGRATION - values your system does not name yet come first"
150
- : "reading as ADOPTION - values your system already names come first";
156
+ ? "sorted by what your design system does not name yet"
157
+ : "sorted by what your design system already names";
151
158
  const where = source.from === "flag"
152
159
  ? "this run only"
153
160
  : source.from === "config"
154
161
  ? `set in _synthesisui/config.json${source.at ? ` on ${source.at.slice(0, 10)}` : ""}`
155
- : "nobody declared one, so this is the default";
162
+ : "nobody chose, so this is the default";
156
163
  const flip = source.intent === "migrate"
157
- ? "`doctor --adopt` flips it"
158
- : "`doctor --migrate` flips it";
164
+ ? "`doctor --adopt` sorts the other way"
165
+ : "`doctor --migrate` sorts the other way";
159
166
  return `${what} · ${where} · ${flip}`;
160
167
  }
161
168
  /** Writes the project config (committable, plain JSON). */