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.
- package/dist/absorb-plan.js +52 -5
- package/dist/commands/absorb.js +5 -6
- package/dist/commands/doctor.js +139 -36
- package/dist/commands/hook.js +3 -3
- package/dist/commands/mcp.js +3 -3
- package/dist/component-codegen.js +17 -5
- package/dist/config.js +13 -6
- package/dist/doctor/apply-fix.js +40 -5
- package/dist/doctor/scan.js +62 -4
- package/dist/doctor/their-names.js +175 -0
- package/dist/doctor/tokens.js +59 -4
- package/dist/install-marks.js +45 -2
- package/dist/measured-scope.js +3 -1
- package/dist/skill-adapt.js +137 -83
- package/dist/types.js +10 -1
- package/package.json +1 -1
package/dist/doctor/apply-fix.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { readFile, writeFile } from "node:fs/promises";
|
|
27
27
|
import { join } from "node:path";
|
|
28
|
+
import { nameToWrite } from "./scan.js";
|
|
28
29
|
/**
|
|
29
30
|
* A substituição numa linha só, e ela é deliberadamente burra.
|
|
30
31
|
*
|
|
@@ -56,7 +57,15 @@ export function planFix(d, read) {
|
|
|
56
57
|
* literal no texto que o primeiro já deixou.
|
|
57
58
|
*/
|
|
58
59
|
for (const f of d.findings) {
|
|
59
|
-
|
|
60
|
+
/**
|
|
61
|
+
* O NOME DELE GANHA - ver `nameToWrite` em `scan.ts`.
|
|
62
|
+
*
|
|
63
|
+
* `--fix --write` edita o arquivo DELE, então o nome que entra ali é o do vocabulário dele
|
|
64
|
+
* quando existe um. Era aqui que a troca virava renomeação: 737 das 995 sugestões medidas no
|
|
65
|
+
* repo vivo escreviam `--ds-*` sobre um valor que uma variável dele já nomeia.
|
|
66
|
+
*/
|
|
67
|
+
const name = nameToWrite(f);
|
|
68
|
+
if (!name) {
|
|
60
69
|
skipped.push({
|
|
61
70
|
file: f.file,
|
|
62
71
|
line: f.line,
|
|
@@ -73,7 +82,23 @@ export function planFix(d, read) {
|
|
|
73
82
|
* `var(--ds-typography-scale-h1-font-size)` no repo do dono (05/08) - um canto que se move no dia
|
|
74
83
|
* em que alguém edita a escala de tipo. Medido lá: 14 de 129, e 0 de 76 na biblioteca dele.
|
|
75
84
|
*/
|
|
76
|
-
|
|
85
|
+
/**
|
|
86
|
+
* O COMPRIMENTO QUE SEGUE A FONTE É DECISÃO DELE - ver `Finding.fontRelative`.
|
|
87
|
+
*
|
|
88
|
+
* `1em` e o degrau `1rem` do sistema são o mesmo número e não necessariamente o mesmo pixel. O
|
|
89
|
+
* comando MOSTRA a troca, porque sem ela não há conversa; escrevê-la sozinho em 933 lugares seria
|
|
90
|
+
* mudar o layout dele em silêncio por uma equivalência que a gente não pode conferir.
|
|
91
|
+
*/
|
|
92
|
+
if (f.fontRelative) {
|
|
93
|
+
skipped.push({
|
|
94
|
+
file: f.file,
|
|
95
|
+
line: f.line,
|
|
96
|
+
literal: f.literal,
|
|
97
|
+
because: "font-relative",
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (f.crossFamily && !f.theirToken) {
|
|
77
102
|
skipped.push({
|
|
78
103
|
file: f.file,
|
|
79
104
|
line: f.line,
|
|
@@ -100,7 +125,7 @@ export function planFix(d, read) {
|
|
|
100
125
|
continue;
|
|
101
126
|
const idx = f.line - 1;
|
|
102
127
|
const current = body[idx];
|
|
103
|
-
const swapped = current === undefined ? null : swap(current, f.literal,
|
|
128
|
+
const swapped = current === undefined ? null : swap(current, f.literal, name);
|
|
104
129
|
if (swapped === null) {
|
|
105
130
|
/** O arquivo mudou desde a medição, ou o literal já foi trocado por outro achado. */
|
|
106
131
|
skipped.push({
|
|
@@ -116,7 +141,7 @@ export function planFix(d, read) {
|
|
|
116
141
|
file: f.file,
|
|
117
142
|
line: f.line,
|
|
118
143
|
literal: f.literal,
|
|
119
|
-
token:
|
|
144
|
+
token: name,
|
|
120
145
|
});
|
|
121
146
|
}
|
|
122
147
|
for (const [file, body] of lines) {
|
|
@@ -154,11 +179,15 @@ export function describeFix(result, dry) {
|
|
|
154
179
|
const coincidence = skipped.filter((s) => s.because === "cross-family").length;
|
|
155
180
|
const unread = skipped.filter((s) => s.because === "unreadable").length;
|
|
156
181
|
const decisions = skipped.filter((s) => s.because === "no-token").length;
|
|
182
|
+
/** O `em` que o comando mostra e não escreve - ver o guard em `planFix`. */
|
|
183
|
+
const relative = skipped.filter((s) => s.because === "font-relative").length;
|
|
157
184
|
const lines = [];
|
|
158
185
|
if (applied.length === 0) {
|
|
159
186
|
lines.push(decisions > 0
|
|
160
187
|
? `Nothing to apply. All ${decisions} findings are values your system has no name for - those are design decisions, not fixes.`
|
|
161
|
-
:
|
|
188
|
+
: relative > 0
|
|
189
|
+
? `Nothing to apply. All ${relative} findings are lengths in \`em\`, which follow the element's font size - swapping them can move the layout, so that call is yours.`
|
|
190
|
+
: "Nothing to apply.");
|
|
162
191
|
return lines;
|
|
163
192
|
}
|
|
164
193
|
lines.push(`${dry ? "Would replace" : "Replaced"} ${applied.length} hand-written value${applied.length === 1 ? "" : "s"} with the token your system already has, across ${result.files} file${result.files === 1 ? "" : "s"}.`);
|
|
@@ -169,6 +198,12 @@ export function describeFix(result, dry) {
|
|
|
169
198
|
.sort((a, b) => b[1] - a[1])
|
|
170
199
|
.slice(0, 6))
|
|
171
200
|
lines.push(` var(${token}) · ${n} time${n === 1 ? "" : "s"}`);
|
|
201
|
+
/**
|
|
202
|
+
* DITO SEMPRE QUE ACONTECE - senão o número de "trocado" some da conta sem explicação, e a pessoa
|
|
203
|
+
* conclui que o comando falhou onde ele se recusou de propósito.
|
|
204
|
+
*/
|
|
205
|
+
if (relative > 0)
|
|
206
|
+
lines.push(` ${relative} more ${relative === 1 ? "is a length" : "are lengths"} in \`em\`, which follow the element's font size - your system names the same number, but the pixel may differ. Left for you to decide.`);
|
|
172
207
|
if (moved > 0)
|
|
173
208
|
lines.push(`${moved} line${moved === 1 ? "" : "s"} changed since the scan and ${moved === 1 ? "was" : "were"} left alone - run it again.`);
|
|
174
209
|
if (coincidence > 0)
|
package/dist/doctor/scan.js
CHANGED
|
@@ -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
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
|
|
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
|
-
|
|
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,175 @@
|
|
|
1
|
+
import { DS_FAMILY, familySays, 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
|
+
/**
|
|
131
|
+
* E A CATEGORIA DO NOME DELE TEM QUE FECHAR - senão o nosso nome fica, e ele está certo.
|
|
132
|
+
*
|
|
133
|
+
* O NOSSO prefixo carrega a família por construção; o dele não carrega nada garantido. Quando a
|
|
134
|
+
* FORMA do valor já decide (hex, `ms`, pilha de fontes) não há o que proteger e o nome dele
|
|
135
|
+
* entra. Quando o valor é um comprimento cru, `12px` pode ser `--text-caption` ou `--spacing-sm`
|
|
136
|
+
* no vocabulário dele, e escolher errado escreve um token de tipo num `border-radius`.
|
|
137
|
+
*
|
|
138
|
+
* Medido no repositório real em 14/08, antes desta linha: 290 sugestões com a categoria trocada,
|
|
139
|
+
* 66 delas gravadas por um `--fix --write`, e a pior na PRIMEIRA página do relatório -
|
|
140
|
+
* `0.25em → --radius-xs` em 116 arquivos. Ver `FAMILY_WORDS`.
|
|
141
|
+
*
|
|
142
|
+
* Recusar aqui não perde informação: sem alias, `nameToWrite` cai no NOSSO token, que é da
|
|
143
|
+
* família certa porque foi o prefixo dela que o trouxe a este laço.
|
|
144
|
+
*/
|
|
145
|
+
const formDecides = Object.values(UNAMBIGUOUS).some((f) => f.test(value));
|
|
146
|
+
const usable = formDecides
|
|
147
|
+
? candidates
|
|
148
|
+
: candidates.filter((n) => familySays(kind, n));
|
|
149
|
+
if (usable.length === 0)
|
|
150
|
+
continue;
|
|
151
|
+
out.set(key, pick(usable, name, convention));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const [value, candidates] of theirs.byValue) {
|
|
155
|
+
for (const [kind, form] of Object.entries(UNAMBIGUOUS)) {
|
|
156
|
+
if (!form.test(value))
|
|
157
|
+
continue;
|
|
158
|
+
const key = `${kind}:${value}`;
|
|
159
|
+
if (out.has(key))
|
|
160
|
+
continue;
|
|
161
|
+
out.set(key, pick(candidates, null, convention));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A TABELA COM O VOCABULÁRIO DELE DENTRO - um passo, e é este que os comandos chamam.
|
|
168
|
+
*
|
|
169
|
+
* `ours` continua sendo a lei: é ela que diz se `2rem` é tipo ou raio, e é dela que sai a cobertura.
|
|
170
|
+
* A dele decide só COMO SE ESCREVE. Separar as duas é o mesmo desenho que o `absorb` já usa
|
|
171
|
+
* ("DUAS TABELAS, e é a diferença entre elas que define o trabalho").
|
|
172
|
+
*/
|
|
173
|
+
export function withTheirNames(ours, theirs) {
|
|
174
|
+
return { ...ours, aliases: theirNames(ours, theirs) };
|
|
175
|
+
}
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -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
|
-
|
|
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] === "
|
|
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,13 +504,56 @@ 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
|
-
|
|
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-",
|
|
499
513
|
font: "--ds-typography-",
|
|
500
514
|
motion: "--ds-motion-",
|
|
501
515
|
};
|
|
516
|
+
/**
|
|
517
|
+
* AS PALAVRAS COM QUE O MUNDO NOMEIA CADA FAMÍLIA - e é isto que impede um erro de categoria.
|
|
518
|
+
*
|
|
519
|
+
* O NOSSO prefixo carrega a família (`--ds-spacing-md` é espaçamento por construção). O DELE não
|
|
520
|
+
* carrega nada garantido, e nada verificava: `--text-caption` e `--spacing-sm` podem segurar o mesmo
|
|
521
|
+
* `12px`, e sugerir o primeiro para um `border-radius` é escrever um token de tipo numa posição de
|
|
522
|
+
* raio. É exatamente o erro que o `crossFamily` foi criado para impedir do NOSSO lado, e ele passou
|
|
523
|
+
* a acontecer pelo lado dele quando o mapa de vocabulário nasceu.
|
|
524
|
+
*
|
|
525
|
+
* Medido no repositório real em 14/08, antes desta trava:
|
|
526
|
+
*
|
|
527
|
+
* 2680 achados com nome dele
|
|
528
|
+
* 1635 categoria concorda
|
|
529
|
+
* 290 categoria DISCORDA <- sugestão errada na tela
|
|
530
|
+
* 66 dessas o `--fix --write` GRAVARIA (o resto é `em`, que já tem trava)
|
|
531
|
+
* 755 nome sem palavra de categoria - e são todos cor (748) e fonte (7)
|
|
532
|
+
*
|
|
533
|
+
* A pior estava na primeira página: `0.25em → --radius-xs` em 116 arquivos, na lista das mais
|
|
534
|
+
* repetidas. E `4px → --radius-xs` num padding é uma escrita silenciosa, sem `em` para barrá-la.
|
|
535
|
+
*
|
|
536
|
+
* `text` está na lista de tipo porque `--text-h2` é a forma dominante no mundo real, e `transition`
|
|
537
|
+
* na de motion pelo mesmo motivo.
|
|
538
|
+
*/
|
|
539
|
+
export const FAMILY_WORDS = {
|
|
540
|
+
color: ["color", "colour"],
|
|
541
|
+
radius: ["radius", "rounded", "corner"],
|
|
542
|
+
spacing: ["spacing", "space", "gap", "inset"],
|
|
543
|
+
font: ["font", "type", "text"],
|
|
544
|
+
motion: ["duration", "motion", "ease", "easing", "transition"],
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* O NOME DELE DIZ QUE É DESTA FAMÍLIA - por QUALQUER segmento, não só pelo primeiro.
|
|
548
|
+
*
|
|
549
|
+
* `--dashboard-radius-lg` é um raio e o primeiro segmento é o namespace dele. Olhar só o começo
|
|
550
|
+
* recusaria um nome certo por causa de um prefixo de produto, que é a metade oposta do mesmo erro.
|
|
551
|
+
*/
|
|
552
|
+
export const familySays = (kind, name) => {
|
|
553
|
+
const words = FAMILY_WORDS[kind] ?? [];
|
|
554
|
+
const segments = name.replace(/^--/, "").toLowerCase().split("-");
|
|
555
|
+
return segments.some((seg) => words.includes(seg));
|
|
556
|
+
};
|
|
502
557
|
/**
|
|
503
558
|
* O token que carrega este valor, e SE ELE É DA MESMA FAMÍLIA.
|
|
504
559
|
*
|
|
@@ -521,7 +576,7 @@ export function tokenMatch(table, literal, kind) {
|
|
|
521
576
|
const hit = table.byValue.get(normalizeValue(literal, table.rootPx));
|
|
522
577
|
if (!hit || hit.length === 0)
|
|
523
578
|
return null;
|
|
524
|
-
const prefix = kind ?
|
|
579
|
+
const prefix = kind ? DS_FAMILY[kind] : undefined;
|
|
525
580
|
const family = prefix ? hit.filter((n) => n.startsWith(prefix)) : [];
|
|
526
581
|
const pool = family.length > 0 ? family : hit;
|
|
527
582
|
// Semantic roles name intent; primitives name a shelf. Prefer intent.
|
package/dist/install-marks.js
CHANGED
|
@@ -71,7 +71,16 @@
|
|
|
71
71
|
* a prop, e 104 das 207 condições sem expressão nenhuma. Um `upgrade` anterior a esta versão reescreve
|
|
72
72
|
* os componentes dele com a perda intacta.
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* 0.16.220 -> 0.16.233 em 14/08: o componente materializado muda de bytes. A regra de contenção HTML
|
|
76
|
+
* era enumerada à mão em `elementFor` e a enumeração esquecia `heading`, `row` e `stack` - uma raiz de
|
|
77
|
+
* papel `action` com um heading no topo saía como `<button><h3>…</h3></button>`, que é marcação
|
|
78
|
+
* inválida embarcada num copy-paste. Agora sai `<article>`.
|
|
79
|
+
*
|
|
80
|
+
* Zero ocorrências no sistema real medido (todos os headings dele estão aninhados, e o `children > 0`
|
|
81
|
+
* já os pegava), então para ele o `upgrade` é no-op. A marca é sobre o caso geral.
|
|
82
|
+
*/
|
|
83
|
+
export const MATERIALISER_SINCE = "0.16.233";
|
|
75
84
|
/**
|
|
76
85
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
77
86
|
*
|
|
@@ -96,7 +105,41 @@ export const MATERIALISER_SINCE = "0.16.220";
|
|
|
96
105
|
* tem nome para isto" sobre um valor que o sistema dela nomeia - medido no repo real, 243 trocas
|
|
97
106
|
* viraram 764 sobre os mesmos 4433 valores à mão.
|
|
98
107
|
*/
|
|
99
|
-
|
|
108
|
+
/**
|
|
109
|
+
* 0.16.223 -> 0.16.229 em 13/08, e o SIM mais forte que esta marca já teve: o hook passou a
|
|
110
|
+
* aconselhar o nome DELE. Um pin anterior lê o mesmo arquivo e manda escrever
|
|
111
|
+
* `var(--ds-color-gray-400)` num repositório que declara `--color-lightgray-700: #888888` - e a
|
|
112
|
+
* pessoa acaba com dois conselhos diferentes sobre o mesmo valor, porque o `doctor` de hoje diz o
|
|
113
|
+
* nome dela. Medido no repo real: 737 de 995 sugestões renomeavam uma variável dela, e outras 852
|
|
114
|
+
* saíam como "o sistema não nomeia isto" sobre valores que o repositório dela nomeia.
|
|
115
|
+
*
|
|
116
|
+
* É também a primeira vez que o hook VARRE as folhas de estilo, revisando a escolha declarada em
|
|
117
|
+
* `loadSystem` - medido ponta a ponta no repo real, 80ms -> 84ms, contra oferecer o nome errado.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* 0.16.229 -> 0.16.230 em 13/08: `em` passou a ser um comprimento como os outros. O padrão do RAIO
|
|
121
|
+
* já aceitava as três unidades e o do ESPAÇAMENTO aceitava duas - assimetria, não doutrina - e um
|
|
122
|
+
* hook pinado antes disto lê o mesmo arquivo e não vê 933 espaçamentos e 144 raios, em 325 arquivos
|
|
123
|
+
* do repositório real. Não é medir menos: é chamar de limpo o que não é.
|
|
124
|
+
*
|
|
125
|
+
* A 0.16.229 nunca chegou ao npm, mas a marca sobe assim mesmo - uma marca que aponta para uma
|
|
126
|
+
* versão que ninguém pode instalar é uma marca que nunca dispara.
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* 0.16.230 -> 0.16.231 em 14/08: a segunda porta do mapa de vocabulário deixou de ser "só cor" e
|
|
130
|
+
* passou a ser "a forma que já decide a família" - uma pilha de fontes não pode ser um espaçamento, e
|
|
131
|
+
* `ms` só existe em motion. Um hook pinado antes disto olha o arquivo que a pessoa acabou de escrever
|
|
132
|
+
* e diz que o sistema não nomeia a fonte que o css dela nomeia. Medido no repositório real: as trocas
|
|
133
|
+
* com nome disponível foram de 1624 para 1631.
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* 0.16.231 -> 0.16.232 em 14/08, e este é o pior que esta marca já carregou: o pinado não erra por
|
|
137
|
+
* omissão, ele erra o CONSELHO. O mapa de vocabulário escolhia entre os nomes DELE sem conferir a
|
|
138
|
+
* categoria, então um hook pinado antes disto olha um `border-radius: 12px` recém-escrito e manda usar
|
|
139
|
+
* `var(--text-caption)` - um token de tipo numa posição de raio. Medido no repositório real: 290
|
|
140
|
+
* sugestões com a categoria trocada, 66 delas graváveis por um `--fix --write`.
|
|
141
|
+
*/
|
|
142
|
+
export const CHECKER_SINCE = "0.16.232";
|
|
100
143
|
/**
|
|
101
144
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
102
145
|
*
|
package/dist/measured-scope.js
CHANGED
|
@@ -117,7 +117,9 @@ export function describeScope(m) {
|
|
|
117
117
|
]
|
|
118
118
|
: []),
|
|
119
119
|
];
|
|
120
|
-
|
|
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.
|