synthesisui 0.16.227 → 0.16.231

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.
@@ -10,6 +10,15 @@
10
10
  * takes eight seconds and needs a build step is a diagnosis nobody runs.
11
11
  */
12
12
  import { normalizeValue, tokenMatch } from "./tokens.js";
13
+ /**
14
+ * O NOME QUE SE ESCREVE NO ARQUIVO DELE - e existe uma função só para isto por um motivo.
15
+ *
16
+ * Seis lugares decidem o que a pessoa vê ou o que o `--fix` grava: o relatório, o plano, o hook, o
17
+ * MCP, a aplicação e a contagem. Deixar cada um lembrar de preferir `theirToken` é o padrão do
18
+ * argumento opcional que se esquece - e o sintoma seria o pior possível: o comando aconselhando um
19
+ * nome e o hook aconselhando outro, sobre o mesmo arquivo.
20
+ */
21
+ export const nameToWrite = (f) => f.theirToken ?? f.token;
13
22
  /**
14
23
  * `next/og` renders JSX to a PNG on the server. There is no document, so there
15
24
  * is no `var(--ds-*)` to read: every colour in such a file MUST be a literal.
@@ -54,9 +63,35 @@ const OPEN = `["']?`;
54
63
  /** `rounded-[14px]`, `border-radius: 14px`, `borderRadius: "14px"`. Zero and
55
64
  * full pills are idiom, not drift - nobody tokenizes `0` or `9999px`. */
56
65
  const RADIUS = new RegExp(`(?:border-?[Rr]adius\\s*:\\s*${OPEN}|rounded(?:-[a-z]+)?-\\[)(-?\\d*\\.?\\d+)(px|rem|em)`, "g");
57
- /** Arbitrary spacing: `p-[18px]`, `gap-[7px]`, `margin: 18px`, `gap: "18px"`.
58
- * The JSX side also writes `paddingLeft`, `marginTop` and friends. */
59
- const SPACING = new RegExp(`(?:\\b[pmg](?:[trblxy])?-\\[|gap-\\[|(?:padding|margin|gap)(?:[A-Z][a-z]+)?\\s*:\\s*${OPEN})(-?\\d*\\.?\\d+)(px|rem)`, "g");
66
+ /**
67
+ * Arbitrary spacing: `p-[18px]`, `gap-[7px]`, `margin: 18px`, `gap: "18px"`.
68
+ * The JSX side also writes `paddingLeft`, `marginTop` and friends.
69
+ *
70
+ * `em` ENTROU EM 13/08, e a falta dele era uma palavra: o padrão do RAIO logo acima já aceitava as
71
+ * três unidades e o do espaçamento aceitava duas. Não era doutrina, era assimetria - e o custo dela é
72
+ * o pior tipo, porque um valor que a varredura não vê não aparece nem como lacuna.
73
+ *
74
+ * Medido no repositório real, o que estava invisível:
75
+ *
76
+ * 933 espaçamentos em `em` 300×0.5em · 276×1em · 116×0.25em
77
+ * 144 raios em `em` lidos, mas nunca nomeados (ver `normalizeValue`)
78
+ * 325 arquivos
79
+ *
80
+ * O dono achou pelo caminho que a gente não quer que ele precise usar (13/08): a skill abriu o
81
+ * `.scss` por conta própria e listou quatro `em` que o comando tinha acabado de chamar de limpo -
82
+ * *"não chegou nem a entrar no css, acho isso errado"*.
83
+ *
84
+ * E O KEBAB VEIO NA MESMA PUXADA, escondido atrás do primeiro. O sufixo aceito era só o camelCase
85
+ * do JSX (`paddingLeft`), então `padding-right: 1em` - a forma que TODO css escreve - não casava com
86
+ * nada. Foram os quatro `em` daquele mesmo arquivo que denunciaram: com `em` já no padrão, um
87
+ * continuou aparecendo e três não.
88
+ *
89
+ * 1175 `padding-*`/`margin-*` em kebab, em 335 arquivos do repositório real
90
+ *
91
+ * Duas gramáticas para a mesma propriedade é o tipo de coisa que só aparece quando alguém aponta
92
+ * para um arquivo e conta na mão.
93
+ */
94
+ const SPACING = new RegExp(`(?:\\b[pmg](?:[trblxy])?-\\[|gap-\\[|(?:padding|margin|gap)(?:[A-Z][a-z]+|(?:-[a-z]+)+)?\\s*:\\s*${OPEN})(-?\\d*\\.?\\d+)(px|rem|em)`, "g");
60
95
  /** A font stack written by hand rather than taken from the type scale. */
61
96
  const FONT = /font-family\s*:\s*([^;}\n]+)/g;
62
97
  /**
@@ -370,11 +405,22 @@ function scanCore(file, source, table) {
370
405
  // The kind is already known here, and without passing it the lookup
371
406
  // answers a `gap` with a radius token.
372
407
  const match = tokenMatch(table, literal, kind);
408
+ /**
409
+ * O NOME DELE VEM DA MESMA CHAVE QUE A NOSSA FAMÍLIA VALIDOU - ver `theirNames`.
410
+ *
411
+ * A chave carrega o `kind` porque o valor sozinho não decide: `4px` é `--radius-xs` e
412
+ * `--spacing` no vocabulário dele, e qual dos dois está certo depende de onde o literal está.
413
+ */
414
+ const theirs = table.aliases.get(`${kind}:${normalizeValue(literal, table.rootPx)}`);
373
415
  findings.push({
374
416
  kind,
375
417
  line: at,
376
418
  literal,
377
419
  token: match?.token ?? null,
420
+ ...(/(?<!r)em$/i.test(literal.trim())
421
+ ? { fontRelative: true }
422
+ : {}),
423
+ ...(theirs ? { theirToken: theirs } : {}),
378
424
  /**
379
425
  * A COINCIDÊNCIA VIAJA COM O ACHADO - ver `tokenMatch`.
380
426
  *
@@ -562,6 +608,8 @@ export function diagnose(files) {
562
608
  byLiteral.set(key, {
563
609
  kind: f.kind,
564
610
  token: f.token,
611
+ ...(f.theirToken ? { theirToken: f.theirToken } : {}),
612
+ ...(f.fontRelative ? { fontRelative: true } : {}),
565
613
  count: 1,
566
614
  files: new Set([f.file]),
567
615
  ...(f.crossFamily ? { crossFamily: true } : {}),
@@ -573,6 +621,8 @@ export function diagnose(files) {
573
621
  literal: key.slice(key.indexOf(":") + 1),
574
622
  kind: v.kind,
575
623
  token: v.token,
624
+ ...(v.theirToken ? { theirToken: v.theirToken } : {}),
625
+ ...(v.fontRelative ? { fontRelative: true } : {}),
576
626
  count: v.count,
577
627
  files: v.files.size,
578
628
  ...(v.crossFamily ? { crossFamily: true } : {}),
@@ -585,7 +635,15 @@ export function diagnose(files) {
585
635
  files: files.filter((f) => f.findings.length > 0 || (f.phantoms?.length ?? 0) > 0),
586
636
  findings: flat,
587
637
  counts,
588
- named: flat.filter((f) => f.token).length,
638
+ /**
639
+ * QUANTOS JÁ TÊM NOME NESTE REPOSITÓRIO - e o dele conta.
640
+ *
641
+ * Este número é a promessa que a tela faz ("9 of those have a name waiting"), e enquanto ele
642
+ * contava só os nossos, 852 achados que o repositório DELE nomeia saíam como "the system has no
643
+ * name for it". Uma promessa medida por metade do vocabulário que existe é uma promessa menor
644
+ * que a verdade.
645
+ */
646
+ named: flat.filter((f) => nameToWrite(f)).length,
589
647
  tokenUses,
590
648
  phantomUses,
591
649
  coverage: total === 0 ? 100 : Math.round(((tokenUses + ownUses) / total) * 100),
@@ -0,0 +1,154 @@
1
+ import { DS_FAMILY, normalizeValue } from "./tokens.js";
2
+ /**
3
+ * COMO ELE CHAMA O VALOR - para a gente parar de renomear as variáveis dele.
4
+ *
5
+ * O doctor sempre soube dizer o que o sistema chama de `#888`. O que ele fazia com isso é que estava
6
+ * errado: mandava trocar por `var(--ds-color-gray-400)` num repositório que declara
7
+ * `--color-lightgray-700: #888888` desde antes de a gente existir. `--fix --write` escrevia o NOSSO
8
+ * nome dentro do arquivo dele.
9
+ *
10
+ * Medido no repositório vivo em 13/08, repo inteiro:
11
+ *
12
+ * 141 tokens que ELE declara
13
+ * 181 tokens que o sistema declara
14
+ * 114 dos nossos seguram um valor que um dos dele também segura
15
+ *
16
+ * 995 achados com nome nosso
17
+ * 737 deles (74%) renomeavam uma variável que ele já tem
18
+ *
19
+ * E o grupo que saía como "the system has no name for it":
20
+ *
21
+ * 5383 sem nome nosso
22
+ * 852 ...mas o repositório DELE nomeia (843 cor · 8 tipo · 1 motion)
23
+ *
24
+ * A LEI JÁ EXISTIA, num sentido só. `absorb-plan.ts` diz, sobre a direção repo -> sistema: *"quando o
25
+ * vocabulário DELES já nomeia o valor, o nome é o deles"*. Na direção sistema -> repo a gente fazia o
26
+ * contrário. É a mesma lei 11 um nível abaixo da cor: não reescrever o vocabulário dele.
27
+ *
28
+ * ESTE MAPA NÃO É UM ARQUIVO, e é de propósito (dono, 13/08: *"talvez essas informações possam vir do
29
+ * MCP para não levar isso para o usuário ficar vendo"*). Ele é derivado das folhas de estilo dele a
30
+ * cada rodada - 686 folhas e 797KB no repo real, 4ms no hook inteiro (80ms -> 84ms, medido ponta a
31
+ * ponta), e o `absorb` já pagava essa varredura hoje. Um
32
+ * mapa gravado envelhece no dia em que ele renomeia um token, e aí a gente sugere um nome que não
33
+ * existe mais. O MCP ganha de graça, porque `check_file` roda este mesmo doctor.
34
+ *
35
+ * E ele nunca aparece na tela: o que aparece é o nome DELE, no lugar onde antes aparecia o nosso.
36
+ */
37
+ /**
38
+ * QUANDO A FORMA DO VALOR JÁ DECIDE A FAMÍLIA - e é isto que a segunda porta pede.
39
+ *
40
+ * A porta admitia só COR, e o motivo estava certo pela metade: o que impede um comprimento de entrar
41
+ * é a AMBIGUIDADE (`24px` pode ser raio, espaçamento ou tamanho de fonte, e sem um token nosso não há
42
+ * o que decidisse), não o fato de ser cor. Uma pilha de fontes não é ambígua - `'Figtree', sans-serif`
43
+ * não pode ser um espaçamento -, e uma duração também não: `s` e `ms` só existem em motion.
44
+ *
45
+ * Então a regra é a forma, e o comprimento continua de fora, declarado. Medido no repositório real em
46
+ * 14/08: 7 achados em 6 arquivos saíam como "the system has no name for it" sobre a família de fontes
47
+ * que o próprio css dele nomeia `--dashboard-font-family`. É pouco, e é o único grupo que sobrou do
48
+ * item que eu tinha nomeado - os `em` saíram na leva anterior.
49
+ */
50
+ const UNAMBIGUOUS = {
51
+ /** `#rrggbbaa`, que é onde toda a família de dialetos cai. */
52
+ color: /^#[0-9a-f]{8}$/,
53
+ /** `s` e `ms` não aparecem em nenhuma outra família do contrato. */
54
+ motion: /^-?[\d.]+ms$/,
55
+ /** Uma pilha de fontes: tem vírgula, ou é uma das palavras genéricas do CSS. */
56
+ font: /,|^(?:serif|sans-serif|monospace|cursive|fantasy|system-ui)$/,
57
+ };
58
+ const segments = (name) => name.replace(/^--/, "").split("-");
59
+ /**
60
+ * QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e são 18 no repo real.
61
+ *
62
+ * O empate não é decorativo: `4px` é `--radius-xs` E `--spacing` lá dentro, `24px` é `--text-h2` E
63
+ * `--spacing-lg`. Escolher errado escreve um token de tipo numa posição de espaçamento, que é
64
+ * exatamente o erro de categoria que o `crossFamily` foi criado para impedir do nosso lado.
65
+ *
66
+ * Três critérios, nesta ordem, e todos LIDOS do repositório dele:
67
+ *
68
+ * 1. quantos segmentos ele compartilha com o nosso token daquela família
69
+ * `--ds-radius-xs` puxa `--radius-xs` (2) e não `--spacing` (0)
70
+ * 2. a convenção dominante dele - o primeiro segmento mais frequente entre os tokens dele
71
+ * no repo real, `--color-*` (57) contra `--dashboard-*` (25)
72
+ * 3. ordem de declaração, que é o desempate que sempre existe e nunca inventa
73
+ */
74
+ function pick(candidates, ours, convention) {
75
+ const mine = ours ? new Set(segments(ours)) : null;
76
+ let best = candidates[0];
77
+ let bestScore = [-1, -1];
78
+ for (const name of candidates) {
79
+ const parts = segments(name);
80
+ const shared = mine ? parts.filter((p) => mine.has(p)).length : 0;
81
+ const score = [
82
+ shared,
83
+ convention.get(parts[0] ?? "") ?? 0,
84
+ ];
85
+ if (score[0] > bestScore[0] ||
86
+ (score[0] === bestScore[0] && score[1] > bestScore[1])) {
87
+ best = name;
88
+ bestScore = score;
89
+ }
90
+ }
91
+ return best;
92
+ }
93
+ /**
94
+ * O MAPA DE UMA RODADA: `<natureza>:<valor normalizado>` -> como ELE chama.
95
+ *
96
+ * A chave carrega a natureza porque o valor sozinho não decide: o mesmo `4px` tem dois nomes no
97
+ * vocabulário dele, e qual dos dois está certo depende de o literal estar num raio ou num
98
+ * espaçamento. É a mesma razão pela qual `tokenMatch` recebe o `kind`.
99
+ *
100
+ * DUAS PORTAS, e a segunda é o que faz o número dobrar:
101
+ *
102
+ * o sistema nomeia o valor a NOSSA família valida a natureza, e o nome dele entra
103
+ * o sistema NÃO nomeia entra o valor cuja FORMA já decide a família - ver `UNAMBIGUOUS`
104
+ *
105
+ * O que fica de fora da segunda porta é o COMPRIMENTO, e ele fica por um motivo e não por descuido:
106
+ * `24px` pode ser raio, espaçamento ou tamanho de fonte, e sem um token nosso não há o que validasse.
107
+ * Esses continuam saindo como "no name for it", que é verdade e é o caminho do `absorb`.
108
+ */
109
+ export function theirNames(ours, theirs) {
110
+ const out = new Map();
111
+ if (theirs.byName.size === 0)
112
+ return out;
113
+ /** A convenção dominante dele, contada e não suposta - ver `pick`. */
114
+ const convention = new Map();
115
+ for (const name of theirs.byName.keys()) {
116
+ const head = segments(name)[0] ?? "";
117
+ convention.set(head, (convention.get(head) ?? 0) + 1);
118
+ }
119
+ for (const [kind, prefix] of Object.entries(DS_FAMILY)) {
120
+ for (const [name, raw] of ours.byName) {
121
+ if (!name.startsWith(prefix))
122
+ continue;
123
+ const value = normalizeValue(raw, ours.rootPx);
124
+ const key = `${kind}:${value}`;
125
+ if (out.has(key))
126
+ continue;
127
+ const candidates = theirs.byValue.get(value);
128
+ if (!candidates || candidates.length === 0)
129
+ continue;
130
+ out.set(key, pick(candidates, name, convention));
131
+ }
132
+ }
133
+ for (const [value, candidates] of theirs.byValue) {
134
+ for (const [kind, form] of Object.entries(UNAMBIGUOUS)) {
135
+ if (!form.test(value))
136
+ continue;
137
+ const key = `${kind}:${value}`;
138
+ if (out.has(key))
139
+ continue;
140
+ out.set(key, pick(candidates, null, convention));
141
+ }
142
+ }
143
+ return out;
144
+ }
145
+ /**
146
+ * A TABELA COM O VOCABULÁRIO DELE DENTRO - um passo, e é este que os comandos chamam.
147
+ *
148
+ * `ours` continua sendo a lei: é ela que diz se `2rem` é tipo ou raio, e é dela que sai a cobertura.
149
+ * A dele decide só COMO SE ESCREVE. Separar as duas é o mesmo desenho que o `absorb` já usa
150
+ * ("DUAS TABELAS, e é a diferença entre elas que define o trabalho").
151
+ */
152
+ export function withTheirNames(ours, theirs) {
153
+ return { ...ours, aliases: theirNames(ours, theirs) };
154
+ }
@@ -31,6 +31,7 @@ export const EMPTY_TABLE = {
31
31
  byValue: new Map(),
32
32
  rootPx: DEFAULT_ROOT_PX,
33
33
  rootFrom: null,
34
+ aliases: new Map(),
34
35
  declared: new Set(),
35
36
  keyframes: new Set(),
36
37
  };
@@ -120,10 +121,19 @@ export function normalizeValue(raw, rootPx) {
120
121
  * A FAMÍLIA CONTINUA MANDANDO: `4px` passa a casar com `--ds-radius-xs` E com `--ds-spacing-3xs`,
121
122
  * e é `tokenMatch` quem escolhe pelo `kind` - um raio não vira espaçamento dentro de um `gap`.
122
123
  */
123
- const len = /^(-?\d*\.?\d+)(px|rem)$/.exec(v);
124
+ /**
125
+ * `em` CAI AQUI JUNTO, e o que impede isso de virar uma suposição é o achado, não o normalizador.
126
+ *
127
+ * Converter `1em` por `rootPx` é certo sempre que a fonte do elemento é a da raiz, e errado quando
128
+ * um ancestral a mudou - e isto aqui não tem como saber qual dos dois. Ele converte para a
129
+ * CONVERSA existir (sem isto, `1em` não casa com degrau nenhum e sai como "o sistema não nomeia
130
+ * isto", sobre um valor que o sistema nomeia); quem impede a troca automática é `Finding.fontRelative`,
131
+ * que o `--fix` recusa. Ver `scan.ts`.
132
+ */
133
+ const len = /^(-?\d*\.?\d+)(px|r?em)$/.exec(v);
124
134
  if (len) {
125
135
  const n = Number.parseFloat(len[1]);
126
- const px = len[2] === "rem" ? n * rootPx : n;
136
+ const px = len[2] === "px" ? n : n * rootPx;
127
137
  /** Arredonda o rastro binário: `0.35rem * 16` é 5.6000000000000005. */
128
138
  return `${Math.round(px * 1e4) / 1e4}px`;
129
139
  }
@@ -431,6 +441,8 @@ export function buildTable(input) {
431
441
  byValue,
432
442
  rootPx,
433
443
  rootFrom: input.rootFrom ?? null,
444
+ /** Vazio até `withTheirNames` ler o vocabulário dele - ver `their-names.ts`. */
445
+ aliases: new Map(),
434
446
  declared: parseDeclaredNames(input.css),
435
447
  keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
436
448
  };
@@ -492,7 +504,9 @@ export function nearestToken(table, literal) {
492
504
  * radius is one a person stops reading, and this one has exactly one job that
493
505
  * nobody else does: naming the right token.
494
506
  */
495
- const FAMILY = {
507
+ /** Exportado para `their-names.ts`, que precisa da MESMA divisão de família para validar a natureza
508
+ * de um nome dele - duas tabelas de família seriam duas leis. */
509
+ export const DS_FAMILY = {
496
510
  color: "--ds-color-",
497
511
  radius: "--ds-radius-",
498
512
  spacing: "--ds-spacing-",
@@ -521,7 +535,7 @@ export function tokenMatch(table, literal, kind) {
521
535
  const hit = table.byValue.get(normalizeValue(literal, table.rootPx));
522
536
  if (!hit || hit.length === 0)
523
537
  return null;
524
- const prefix = kind ? FAMILY[kind] : undefined;
538
+ const prefix = kind ? DS_FAMILY[kind] : undefined;
525
539
  const family = prefix ? hit.filter((n) => n.startsWith(prefix)) : [];
526
540
  const pool = family.length > 0 ? family : hit;
527
541
  // Semantic roles name intent; primitives name a shelf. Prefer intent.
@@ -96,7 +96,34 @@ export const MATERIALISER_SINCE = "0.16.220";
96
96
  * tem nome para isto" sobre um valor que o sistema dela nomeia - medido no repo real, 243 trocas
97
97
  * viraram 764 sobre os mesmos 4433 valores à mão.
98
98
  */
99
- export const CHECKER_SINCE = "0.16.223";
99
+ /**
100
+ * 0.16.223 -> 0.16.229 em 13/08, e o SIM mais forte que esta marca já teve: o hook passou a
101
+ * aconselhar o nome DELE. Um pin anterior lê o mesmo arquivo e manda escrever
102
+ * `var(--ds-color-gray-400)` num repositório que declara `--color-lightgray-700: #888888` - e a
103
+ * pessoa acaba com dois conselhos diferentes sobre o mesmo valor, porque o `doctor` de hoje diz o
104
+ * nome dela. Medido no repo real: 737 de 995 sugestões renomeavam uma variável dela, e outras 852
105
+ * saíam como "o sistema não nomeia isto" sobre valores que o repositório dela nomeia.
106
+ *
107
+ * É também a primeira vez que o hook VARRE as folhas de estilo, revisando a escolha declarada em
108
+ * `loadSystem` - medido ponta a ponta no repo real, 80ms -> 84ms, contra oferecer o nome errado.
109
+ */
110
+ /**
111
+ * 0.16.229 -> 0.16.230 em 13/08: `em` passou a ser um comprimento como os outros. O padrão do RAIO
112
+ * já aceitava as três unidades e o do ESPAÇAMENTO aceitava duas - assimetria, não doutrina - e um
113
+ * hook pinado antes disto lê o mesmo arquivo e não vê 933 espaçamentos e 144 raios, em 325 arquivos
114
+ * do repositório real. Não é medir menos: é chamar de limpo o que não é.
115
+ *
116
+ * A 0.16.229 nunca chegou ao npm, mas a marca sobe assim mesmo - uma marca que aponta para uma
117
+ * versão que ninguém pode instalar é uma marca que nunca dispara.
118
+ */
119
+ /**
120
+ * 0.16.230 -> 0.16.231 em 14/08: a segunda porta do mapa de vocabulário deixou de ser "só cor" e
121
+ * passou a ser "a forma que já decide a família" - uma pilha de fontes não pode ser um espaçamento, e
122
+ * `ms` só existe em motion. Um hook pinado antes disto olha o arquivo que a pessoa acabou de escrever
123
+ * e diz que o sistema não nomeia a fonte que o css dela nomeia. Medido no repositório real: as trocas
124
+ * com nome disponível foram de 1624 para 1631.
125
+ */
126
+ export const CHECKER_SINCE = "0.16.231";
100
127
  /**
101
128
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
102
129
  *
@@ -117,7 +117,9 @@ export function describeScope(m) {
117
117
  ]
118
118
  : []),
119
119
  ];
120
- return `read from your census: ${parts.join(" · ")}`;
120
+ /** O ARQUIVO, e não a palavra: "census" é o nosso nome para a medição; `_synthesisui/census.json`
121
+ * é um caminho que ele abre. Ver a guarda de vocabulário em `doctor-intent.spec.ts`. */
122
+ return `read from _synthesisui/census.json: ${parts.join(" · ")}`;
121
123
  }
122
124
  /**
123
125
  * GRAVA ONDE ESTE SISTEMA FOI MEDIDO, no ponteiro dele.