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.
- package/dist/absorb-plan.js +57 -5
- package/dist/commands/absorb.js +5 -6
- package/dist/commands/doctor.js +130 -34
- package/dist/commands/hook.js +3 -3
- package/dist/commands/mcp.js +3 -3
- 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 +154 -0
- package/dist/doctor/tokens.js +18 -4
- package/dist/install-marks.js +28 -1
- package/dist/measured-scope.js +3 -1
- package/dist/skill-adapt.js +155 -70
- package/package.json +1 -1
package/dist/absorb-plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deltaE, JND } from "./doctor/color-distance.js";
|
|
2
|
-
import { nearestToken } from "./doctor/tokens.js";
|
|
2
|
+
import { 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,36 @@ 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 primeira palavra do nome dele é a pista, e é a mesma lista que o `CATEGORY` acima já usa. Quando
|
|
22
|
+
* nenhum candidato concorda com a natureza do achado, COR ainda passa - um hex numa posição de cor
|
|
23
|
+
* não tem ambiguidade de família - e comprimento não passa: sem concordância não há o que separasse
|
|
24
|
+
* um token de tipo de um de espaçamento, e o valor volta sem caminho para a pessoa decidir.
|
|
25
|
+
*/
|
|
26
|
+
const KIND_WORDS = {
|
|
27
|
+
color: ["color", "colour"],
|
|
28
|
+
radius: ["radius", "rounded"],
|
|
29
|
+
spacing: ["spacing", "space", "gap"],
|
|
30
|
+
font: ["font", "type", "text"],
|
|
31
|
+
motion: ["duration", "motion", "ease", "easing", "transition"],
|
|
32
|
+
};
|
|
33
|
+
function nameOf(kind, candidates) {
|
|
34
|
+
if (!candidates || candidates.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
const head = (n) => clean(n).split("-")[0] ?? "";
|
|
37
|
+
const agrees = candidates.find((n) => KIND_WORDS[kind].includes(head(n)));
|
|
38
|
+
if (agrees)
|
|
39
|
+
return agrees;
|
|
40
|
+
return kind === "color" ? candidates[0] : undefined;
|
|
41
|
+
}
|
|
14
42
|
/**
|
|
15
43
|
* O CAMINHO A PARTIR DO NOME DELES, e nada além dele.
|
|
16
44
|
*
|
|
@@ -87,10 +115,34 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
87
115
|
for (const r of unnamed) {
|
|
88
116
|
if (entries.length >= cap)
|
|
89
117
|
break;
|
|
90
|
-
|
|
91
|
-
|
|
118
|
+
/**
|
|
119
|
+
* O VALOR NORMALIZADO, e não o literal cru - era aqui que a metade pronta desta lista sumia.
|
|
120
|
+
*
|
|
121
|
+
* `byValue` é indexada pela forma canônica: cor em `#rrggbbaa`, comprimento em px sobre a raiz
|
|
122
|
+
* medida. A busca usava o literal como ele aparece no código, então:
|
|
123
|
+
*
|
|
124
|
+
* get("#fff") -> nada get("#ffffffff") -> --dashboard-white-500
|
|
125
|
+
* get("1em") -> nada get("16px") -> --spacing-md
|
|
126
|
+
* get("24px") -> acertava por acidente, porque px já é a forma canônica
|
|
127
|
+
*
|
|
128
|
+
* Cor NUNCA casava (a chave tem alfa), `em`/`rem` nunca casavam, e `px` casava por coincidência.
|
|
129
|
+
* Medido no repositório real em 14/08:
|
|
130
|
+
*
|
|
131
|
+
* na lista que ele VÊ (as 40 mais repetidas) 0 -> 5 com nome dele, 404 arquivos
|
|
132
|
+
* na fila inteira (338 valores) 1 -> 22 com nome dele, 478 arquivos
|
|
133
|
+
*
|
|
134
|
+
* A primeira leitura desta medição contou os 22 e escreveu "22 de 40", que é a fila inteira sobre
|
|
135
|
+
* o denominador da página - o número certo na conta errada. São 5 na tela e 22 na fila.
|
|
136
|
+
*
|
|
137
|
+
* O custo não era só a lista curta: o `doctor` mandava trocar `#fff` por
|
|
138
|
+
* `var(--dashboard-white-500)` e o `absorb`, no mesmo dia, pedia que ele batizasse `#fff`. Dois
|
|
139
|
+
* comandos com conselhos opostos sobre o mesmo valor.
|
|
140
|
+
*/
|
|
141
|
+
const theirName = nameOf(r.kind, theirs.byValue.get(normalizeValue(r.literal, theirs.rootPx)));
|
|
92
142
|
const path = pathFor(r.kind, theirName);
|
|
93
|
-
|
|
143
|
+
/** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
|
|
144
|
+
* vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
|
|
145
|
+
const near = theirName ? undefined : nearestOwn(theirs, r.literal, r.kind);
|
|
94
146
|
/** Já existe na fundação com OUTRO valor: absorver aqui seria repintar o token deles. */
|
|
95
147
|
if (path && have.has(path))
|
|
96
148
|
continue;
|
package/dist/commands/absorb.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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:
|
|
280
|
-
|
|
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({
|
|
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 =
|
|
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 ?? `
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
907
|
-
console.log(body(paint.dim(` from
|
|
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
|
|
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
|
|
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
|
|
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
|
/**
|
|
@@ -1087,15 +1172,15 @@ export async function doctor(opts) {
|
|
|
1087
1172
|
const w = Math.max(...repeats.map((r) => r.literal.length));
|
|
1088
1173
|
for (const r of repeats) {
|
|
1089
1174
|
const where = `${r.count}\u00d7 in ${r.files} file${r.files === 1 ? "" : "s"}`;
|
|
1090
|
-
const near = r
|
|
1175
|
+
const near = nameToWrite(r) ? null : nearestToken(table, r.literal);
|
|
1091
1176
|
/**
|
|
1092
1177
|
* COINCIDÊNCIA NÃO É RESPOSTA - ver `crossFamily` em `scan.ts`. Para aquele `kind` o
|
|
1093
1178
|
* sistema não tem nome; o valor mora noutra família, e dizer só a seta afirma o contrário.
|
|
1094
1179
|
*/
|
|
1095
1180
|
const named = r.crossFamily
|
|
1096
1181
|
? ` → no ${r.kind} named for it · the value lives as ${r.token}`
|
|
1097
|
-
: r
|
|
1098
|
-
? ` → ${r
|
|
1182
|
+
: nameToWrite(r)
|
|
1183
|
+
? ` → ${nameToWrite(r)}`
|
|
1099
1184
|
: near
|
|
1100
1185
|
? ` → nearest is ${near.name} (${near.value})`
|
|
1101
1186
|
: "";
|
|
@@ -1113,11 +1198,11 @@ export async function doctor(opts) {
|
|
|
1113
1198
|
for (const x of shown) {
|
|
1114
1199
|
// A dead end with a neighbour is not a dead end. Only for lengths -
|
|
1115
1200
|
// "nearly the same blue" is the guess this tool must never make.
|
|
1116
|
-
const near = x
|
|
1201
|
+
const near = nameToWrite(x) ? null : nearestToken(table, x.literal);
|
|
1117
1202
|
const named = x.crossFamily
|
|
1118
1203
|
? `→ no ${x.kind} named for it · the value lives as ${x.token}`
|
|
1119
|
-
: x
|
|
1120
|
-
? `→ ${x
|
|
1204
|
+
: nameToWrite(x)
|
|
1205
|
+
? `→ ${nameToWrite(x)}`
|
|
1121
1206
|
: near
|
|
1122
1207
|
? `→ nearest is ${near.name} (${near.value})`
|
|
1123
1208
|
: "→ no token holds this value yet";
|
|
@@ -1449,25 +1534,36 @@ export async function doctor(opts) {
|
|
|
1449
1534
|
* Ele não some: vai para o grupo de quem NÃO tem nome, que é literalmente o que ele é para
|
|
1450
1535
|
* aquele kind - e a linha diz onde o valor mora hoje.
|
|
1451
1536
|
*/
|
|
1452
|
-
const named = d.repeats.filter((x) => x
|
|
1453
|
-
const unnamed = d.repeats.filter((x) => !x
|
|
1537
|
+
const named = d.repeats.filter((x) => nameToWrite(x) && (!x.crossFamily || x.theirToken));
|
|
1538
|
+
const unnamed = d.repeats.filter((x) => !nameToWrite(x) || (x.crossFamily && !x.theirToken));
|
|
1454
1539
|
for (const r of named.slice(0, migrating ? 2 : 3)) {
|
|
1455
1540
|
plan.push({
|
|
1456
1541
|
rank: migrating ? 4 : 3,
|
|
1457
|
-
|
|
1542
|
+
/** Curto de propósito: a coluna trunca, e uma ressalva cortada ao meio é pior que nenhuma.
|
|
1543
|
+
* O porquê inteiro está na linha do medidor, logo acima. */
|
|
1544
|
+
what: r.fontRelative
|
|
1545
|
+
? `${r.literal} → ${nameToWrite(r)} · your call`
|
|
1546
|
+
: `${r.literal} → ${nameToWrite(r)}`,
|
|
1458
1547
|
size: `${r.files} file${r.files === 1 ? "" : "s"}`,
|
|
1459
|
-
cheap
|
|
1548
|
+
/** `cheap` promete "não muda um pixel", e um `em` pode mudar - ver `Finding.fontRelative`. */
|
|
1549
|
+
cheap: !r.fontRelative,
|
|
1460
1550
|
});
|
|
1461
1551
|
}
|
|
1462
1552
|
for (const r of unnamed.slice(0, migrating ? 3 : 2)) {
|
|
1463
1553
|
const near = nearestToken(table, r.literal);
|
|
1464
1554
|
plan.push({
|
|
1465
1555
|
rank: migrating ? 3 : 4,
|
|
1556
|
+
/**
|
|
1557
|
+
* "the system has no name for it" era verdade sobre O SISTEMA e o repositório tem dois
|
|
1558
|
+
* vocabulários - o dele também foi consultado (ver `their-names.ts`), e chegar aqui significa
|
|
1559
|
+
* que NENHUM dos dois nomeia o valor. É isso que a linha passa a dizer, e é o que separa esta
|
|
1560
|
+
* fila da anterior: aquela tem nome esperando, esta precisa de um.
|
|
1561
|
+
*/
|
|
1466
1562
|
what: near
|
|
1467
1563
|
? `${r.literal} - name it, or snap to ${near.name}`
|
|
1468
1564
|
: r.crossFamily
|
|
1469
1565
|
? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
|
|
1470
|
-
: `${r.literal} -
|
|
1566
|
+
: `${r.literal} - no name for it, here or in your CSS`,
|
|
1471
1567
|
size: `${r.files} file${r.files === 1 ? "" : "s"}`,
|
|
1472
1568
|
cheap: false,
|
|
1473
1569
|
});
|
|
@@ -1589,7 +1685,7 @@ export async function doctor(opts) {
|
|
|
1589
1685
|
* 1500 achados e depois dizer "troquei 900" é fazer a pessoa procurar a linha que importa.
|
|
1590
1686
|
*/
|
|
1591
1687
|
if (opts.fix) {
|
|
1592
|
-
const read = await readerFor(root, d.findings.filter((f) => f
|
|
1688
|
+
const read = await readerFor(root, d.findings.filter((f) => nameToWrite(f)).map((f) => f.file));
|
|
1593
1689
|
const { result, next } = planFix(d, read);
|
|
1594
1690
|
const writing = opts.write === true;
|
|
1595
1691
|
if (writing)
|
package/dist/commands/hook.js
CHANGED
|
@@ -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
|
|
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
|
|
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.");
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
|
257
|
-
? ` ${f.file}:${f.line} ${f.literal} - this
|
|
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.");
|
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
|
-
/**
|
|
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
|
-
? "
|
|
150
|
-
: "
|
|
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
|
|
162
|
+
: "nobody chose, so this is the default";
|
|
156
163
|
const flip = source.intent === "migrate"
|
|
157
|
-
? "`doctor --adopt`
|
|
158
|
-
: "`doctor --migrate`
|
|
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). */
|
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)
|