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/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 { 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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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;
|
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.
|
|
@@ -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(`
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
1098
|
-
? ` → ${r
|
|
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
|
|
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
|
|
1120
|
-
? `→ ${x
|
|
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
|
|
1453
|
-
const unnamed = d.repeats.filter((x) => !x
|
|
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
|
-
|
|
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
|
|
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} -
|
|
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
|
|
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)
|
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.");
|
|
@@ -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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
/**
|
|
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). */
|