synthesisui 0.16.419 → 0.16.421
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/agent-wiring.js +52 -8
- package/dist/changed-files.js +56 -0
- package/dist/commands/connect.js +33 -0
- package/dist/commands/doctor.js +28 -8
- package/dist/commands/hook.js +120 -28
- package/dist/commands/import.js +30 -7
- package/dist/commands/mcp.js +18 -4
- package/dist/commands/upgrade.js +17 -0
- package/dist/config.js +14 -0
- package/dist/copy/connect.pt-BR.js +12 -0
- package/dist/doctor/idiom-names.js +266 -0
- package/dist/doctor/ledger.js +9 -2
- package/dist/doctor/scan.js +38 -2
- package/dist/doctor/tokens.js +9 -1
- package/dist/governed.js +53 -0
- package/dist/guarantee.js +171 -0
- package/dist/install-marks.js +27 -5
- package/dist/merge-census.js +11 -2
- package/dist/outside-scope.js +100 -4
- package/dist/rule-touched.js +28 -38
- package/package.json +1 -1
|
@@ -120,6 +120,18 @@ register("pt-BR", {
|
|
|
120
120
|
"the skills": "as skills",
|
|
121
121
|
// ── o custo do hook ──
|
|
122
122
|
"The hook resolves against the npm registry on each edit - that is the wait, not the check. Add synthesisui to your devDependencies and run this again; it switches by itself to local, several times faster.": "O hook resolve no registro do npm a cada edição - a espera é isso, não a verificação. Adicione synthesisui às suas devDependencies e rode isto de novo; ele passa a resolver local sozinho, várias vezes mais rápido.",
|
|
123
|
+
// ── até onde a garantia vai (uma vez, e de novo só quando a resposta muda) ──
|
|
124
|
+
"How far this check goes here": "Até onde esta verificação vai aqui",
|
|
125
|
+
"Every file this project writes is checked as it is written - by an edit tool or by a shell command, it makes no difference.": "Todo arquivo que este projeto escreve é verificado no momento da escrita - por uma ferramenta de edição ou por um comando de shell, dá no mesmo.",
|
|
126
|
+
"Nothing is checked automatically here yet: this project has no write check installed.": "Nada é verificado sozinho aqui ainda: este projeto não tem a verificação de escrita instalada.",
|
|
127
|
+
"What a file no longer has is measured against your last commit. A file git has never seen has no before, so nothing is reported as removed there.": "O que um arquivo deixou de ter é medido contra o seu último commit. Um arquivo que o git nunca viu não tem um antes, então nada é reportado como removido nele.",
|
|
128
|
+
"A value counts as coming from your system in two spellings: `var(--token)`, and the {n} utility names your own theme generates - `bg-primary`, `p-6`, `rounded-lg`.": "Um valor conta como vindo do seu sistema em duas grafias: `var(--token)`, e os {n} nomes de utility que o seu próprio tema gera - `bg-primary`, `p-6`, `rounded-lg`.",
|
|
129
|
+
"A value counts as coming from your system when it is written as `var(--token)`. This system declares no theme names, so a utility class is not read as a reference to it.": "Um valor conta como vindo do seu sistema quando é escrito como `var(--token)`. Este sistema não declara nomes de tema, então uma classe utility não é lida como referência a ele.",
|
|
130
|
+
"Every file in this project is checked as you write it, except {out}, which you named under `ungoverned` in `_synthesisui/config.json`.": "Todo arquivo deste projeto é verificado no momento em que você o escreve, menos {out}, que você escreveu em `ungoverned` no `_synthesisui/config.json`.",
|
|
131
|
+
"Every file in this project is checked as you write it. To leave a place out, name it under `ungoverned` in `_synthesisui/config.json`.": "Todo arquivo deste projeto é verificado no momento em que você o escreve. Para deixar um lugar de fora, escreva-o em `ungoverned` no `_synthesisui/config.json`.",
|
|
132
|
+
"What this system knows was read from {scope}. {apps} declare a dependency on `{specifier}`, so they write in its vocabulary - nothing in them was read.": "O que este sistema sabe foi lido de {scope}. {apps} declaram dependência de `{specifier}`, então escrevem no vocabulário dele - nada dentro deles foi lido.",
|
|
133
|
+
"What this system knows was read from {scope} - nothing outside it was opened.": "O que este sistema sabe foi lido de {scope} - nada fora dali foi aberto.",
|
|
134
|
+
"A value this ruler cannot read is not a value it approved - silence here is never a pass.": "Um valor que esta régua não consegue ler não é um valor que ela aprovou - silêncio aqui nunca é aprovação.",
|
|
123
135
|
// ── as descrições das skills ──
|
|
124
136
|
"the first run, start to finish": "o primeiro uso, do início ao fim",
|
|
125
137
|
"the import, orchestrated": "o import, orquestrado",
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMO ESTE PROJETO ESCREVE UM TOKEN - e por que a régua não enxergava a metade do trabalho dele.
|
|
3
|
+
*
|
|
4
|
+
* ═══ O QUE O CLIENTE VIA ═══
|
|
5
|
+
*
|
|
6
|
+
* Um arquivo da landing dele com **103 utilities do tema dele** recebia, do relatório de cobertura,
|
|
7
|
+
* **0 usos contados** (medido em 12/09, no `codelevel`). O contador só conhecia uma forma de
|
|
8
|
+
* apontar para um token - `var(--x)` -, e num projeto cujo idioma medido é utility a forma certa é
|
|
9
|
+
* outra: `bg-primary`, `p-6`, `rounded-lg`, `font-serif`.
|
|
10
|
+
*
|
|
11
|
+
* O efeito era um número que acusa quem fez tudo certo. A landing inteira escrita no vocabulário do
|
|
12
|
+
* sistema saía como `0%`, e ao lado dela um arquivo onde nada foi medido saía como `100%` - mesmo
|
|
13
|
+
* numerador, dois vereditos opostos.
|
|
14
|
+
*
|
|
15
|
+
* ═══ DE ONDE SAI A TRADUÇÃO, e ela não é nossa nem de um repositório ═══
|
|
16
|
+
*
|
|
17
|
+
* A ligação nome-de-token → utility é o CONTRATO PUBLICADO do Tailwind v4: uma custom property
|
|
18
|
+
* declarada num namespace de tema gera as utilities daquele namespace. `--color-primary` gera
|
|
19
|
+
* `bg-primary`, `text-primary`, `border-primary`; `--radius-lg` gera `rounded-lg`; `--font-serif`
|
|
20
|
+
* gera `font-serif`. Não há convenção de repositório nenhuma aqui - a tabela abaixo é a do
|
|
21
|
+
* framework, e um projeto que não declare nenhum desses namespaces recebe um índice VAZIO e se
|
|
22
|
+
* comporta exatamente como antes desta peça existir (`INV-GERAL-07`).
|
|
23
|
+
*
|
|
24
|
+
* ═══ O GATILHO É O BLOCO `@theme`, e não a forma do nome ═══
|
|
25
|
+
*
|
|
26
|
+
* A primeira versão derivava a grafia de QUALQUER nome declarado num namespace de tema, e a revisão
|
|
27
|
+
* de contrato derrubou isso com o caso certo: um projeto de CSS puro que declara
|
|
28
|
+
* `:root { --color-primary: #… }` - um nome banal - e veste classes de uma biblioteca de terceiro
|
|
29
|
+
* (`text-primary`, `bg-primary`) receberia três "usos do sistema dele" por elemento que não
|
|
30
|
+
* referencia o sistema dele nenhuma vez. Cobertura inflada é o pior sentido possível do erro: a
|
|
31
|
+
* plataforma diria que ele está em dia quando ele não está.
|
|
32
|
+
*
|
|
33
|
+
* `@theme` é a declaração pela qual o PROJETO diz ao compilador que aquelas utilities existem. É
|
|
34
|
+
* o opt-in dele, escrito na folha dele, e é isso que separa "eu uso este framework" de "eu tenho
|
|
35
|
+
* uma variável com um nome comum". Fora de um bloco desses, o índice fica vazio.
|
|
36
|
+
*
|
|
37
|
+
* ═══ A LACUNA, DECLARADA ═══
|
|
38
|
+
*
|
|
39
|
+
* Outro framework de utilities com outra tabela de namespaces não é lido por esta peça, e isso é
|
|
40
|
+
* dito (`INV-HOOK-07`) em vez de suposto: o índice fica vazio, a régua volta a contar só `var()`,
|
|
41
|
+
* e nada passa a valer mais do que vale. Ela nunca inventa um nome - só reconhece a grafia que o
|
|
42
|
+
* próprio compilador dele produziria a partir do que ele escreveu.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* OS NAMESPACES DE TEMA E AS UTILITIES QUE CADA UM GERA - a tabela do framework.
|
|
46
|
+
*
|
|
47
|
+
* Lida do mais específico para o menos: `--font-weight-medium` é do namespace `font-weight`
|
|
48
|
+
* (utility `font-medium`), não do namespace `font` com uma folha `weight-medium`. Sem a ordem, o
|
|
49
|
+
* mesmo nome produziria uma grafia que o compilador nunca emite, e contar uma grafia que não existe
|
|
50
|
+
* é o mesmo defeito que o `phantom` existe para impedir, do outro lado.
|
|
51
|
+
*/
|
|
52
|
+
const NAMESPACES = [
|
|
53
|
+
[
|
|
54
|
+
"color",
|
|
55
|
+
[
|
|
56
|
+
"bg",
|
|
57
|
+
"text",
|
|
58
|
+
"border",
|
|
59
|
+
"border-t",
|
|
60
|
+
"border-r",
|
|
61
|
+
"border-b",
|
|
62
|
+
"border-l",
|
|
63
|
+
"border-x",
|
|
64
|
+
"border-y",
|
|
65
|
+
"border-s",
|
|
66
|
+
"border-e",
|
|
67
|
+
"ring",
|
|
68
|
+
"inset-ring",
|
|
69
|
+
"fill",
|
|
70
|
+
"stroke",
|
|
71
|
+
"from",
|
|
72
|
+
"via",
|
|
73
|
+
"to",
|
|
74
|
+
"decoration",
|
|
75
|
+
"outline",
|
|
76
|
+
"accent",
|
|
77
|
+
"caret",
|
|
78
|
+
"divide",
|
|
79
|
+
"placeholder",
|
|
80
|
+
"shadow",
|
|
81
|
+
"inset-shadow",
|
|
82
|
+
"drop-shadow",
|
|
83
|
+
"text-shadow",
|
|
84
|
+
],
|
|
85
|
+
],
|
|
86
|
+
["font-weight", ["font"]],
|
|
87
|
+
["font", ["font"]],
|
|
88
|
+
["text", ["text"]],
|
|
89
|
+
["tracking", ["tracking"]],
|
|
90
|
+
["leading", ["leading"]],
|
|
91
|
+
[
|
|
92
|
+
"spacing",
|
|
93
|
+
[
|
|
94
|
+
"p",
|
|
95
|
+
"px",
|
|
96
|
+
"py",
|
|
97
|
+
"pt",
|
|
98
|
+
"pr",
|
|
99
|
+
"pb",
|
|
100
|
+
"pl",
|
|
101
|
+
"ps",
|
|
102
|
+
"pe",
|
|
103
|
+
"m",
|
|
104
|
+
"mx",
|
|
105
|
+
"my",
|
|
106
|
+
"mt",
|
|
107
|
+
"mr",
|
|
108
|
+
"mb",
|
|
109
|
+
"ml",
|
|
110
|
+
"ms",
|
|
111
|
+
"me",
|
|
112
|
+
"gap",
|
|
113
|
+
"gap-x",
|
|
114
|
+
"gap-y",
|
|
115
|
+
"space-x",
|
|
116
|
+
"space-y",
|
|
117
|
+
"w",
|
|
118
|
+
"h",
|
|
119
|
+
"size",
|
|
120
|
+
"min-w",
|
|
121
|
+
"min-h",
|
|
122
|
+
"max-w",
|
|
123
|
+
"max-h",
|
|
124
|
+
"inset",
|
|
125
|
+
"inset-x",
|
|
126
|
+
"inset-y",
|
|
127
|
+
"top",
|
|
128
|
+
"right",
|
|
129
|
+
"bottom",
|
|
130
|
+
"left",
|
|
131
|
+
"start",
|
|
132
|
+
"end",
|
|
133
|
+
"translate",
|
|
134
|
+
"translate-x",
|
|
135
|
+
"translate-y",
|
|
136
|
+
"scroll-m",
|
|
137
|
+
"scroll-p",
|
|
138
|
+
"basis",
|
|
139
|
+
"indent",
|
|
140
|
+
],
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
"radius",
|
|
144
|
+
[
|
|
145
|
+
"rounded",
|
|
146
|
+
"rounded-t",
|
|
147
|
+
"rounded-r",
|
|
148
|
+
"rounded-b",
|
|
149
|
+
"rounded-l",
|
|
150
|
+
"rounded-tl",
|
|
151
|
+
"rounded-tr",
|
|
152
|
+
"rounded-br",
|
|
153
|
+
"rounded-bl",
|
|
154
|
+
"rounded-s",
|
|
155
|
+
"rounded-e",
|
|
156
|
+
],
|
|
157
|
+
],
|
|
158
|
+
["inset-shadow", ["inset-shadow"]],
|
|
159
|
+
["drop-shadow", ["drop-shadow"]],
|
|
160
|
+
["text-shadow", ["text-shadow"]],
|
|
161
|
+
["shadow", ["shadow"]],
|
|
162
|
+
["blur", ["blur"]],
|
|
163
|
+
["perspective", ["perspective"]],
|
|
164
|
+
["aspect", ["aspect"]],
|
|
165
|
+
["ease", ["ease"]],
|
|
166
|
+
["animate", ["animate"]],
|
|
167
|
+
["container", ["max-w"]],
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* OS NOMES QUE O PROJETO DECLARA DENTRO DE UM BLOCO `@theme` - e nada fora dele.
|
|
171
|
+
*
|
|
172
|
+
* O bloco é o opt-in do projeto naquele compilador: escrever `--color-primary` ali é dizer que
|
|
173
|
+
* `bg-primary` passa a existir. A mesma variável em `:root` é só uma variável.
|
|
174
|
+
*
|
|
175
|
+
* A varredura é por profundidade de chave, e não por regex de bloco, porque um `@theme` real
|
|
176
|
+
* carrega `@property` e comentários dentro dele em projetos que usam animação; contar chaves é o
|
|
177
|
+
* que não se confunde com isso.
|
|
178
|
+
*/
|
|
179
|
+
function themeNames(css) {
|
|
180
|
+
const out = [];
|
|
181
|
+
const opens = /@theme\b[^{]*\{/g;
|
|
182
|
+
for (const m of css.matchAll(opens)) {
|
|
183
|
+
let depth = 1;
|
|
184
|
+
let i = (m.index ?? 0) + m[0].length;
|
|
185
|
+
const start = i;
|
|
186
|
+
for (; i < css.length && depth > 0; i++) {
|
|
187
|
+
if (css[i] === "{")
|
|
188
|
+
depth++;
|
|
189
|
+
else if (css[i] === "}")
|
|
190
|
+
depth--;
|
|
191
|
+
}
|
|
192
|
+
const body = css.slice(start, i - 1);
|
|
193
|
+
for (const d of body.matchAll(/(--[a-z0-9_-]+)\s*:/gi))
|
|
194
|
+
out.push(d[1]);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/** Do mais longo para o mais curto, para `font-weight` ganhar de `font`. */
|
|
199
|
+
const BY_DEPTH = [...NAMESPACES].sort((a, b) => b[0].split("-").length - a[0].split("-").length);
|
|
200
|
+
/**
|
|
201
|
+
* AS GRAFIAS QUE ESTE SISTEMA GERA - `bg-primary` -> `--color-primary`.
|
|
202
|
+
*
|
|
203
|
+
* O valor é o NOME DECLARADO, e não um booleano, porque quem lê isto precisa dos dois lados: a
|
|
204
|
+
* régua só quer saber que a grafia existe, e a ligação regra↔código precisa dizer QUAL decisão do
|
|
205
|
+
* sistema saiu do arquivo.
|
|
206
|
+
*
|
|
207
|
+
* Um nome com modificador (`--text-lg--line-height`) fica de fora: ele não é um token endereçável,
|
|
208
|
+
* é a segunda metade de um que já está no índice.
|
|
209
|
+
*/
|
|
210
|
+
export function utilitySpellings(css) {
|
|
211
|
+
const out = new Map();
|
|
212
|
+
for (const raw of themeNames(css)) {
|
|
213
|
+
const name = raw.toLowerCase();
|
|
214
|
+
if (!name.startsWith("--"))
|
|
215
|
+
continue;
|
|
216
|
+
const bare = name.slice(2);
|
|
217
|
+
if (bare.includes("--"))
|
|
218
|
+
continue;
|
|
219
|
+
for (const [namespace, prefixes] of BY_DEPTH) {
|
|
220
|
+
if (!bare.startsWith(`${namespace}-`))
|
|
221
|
+
continue;
|
|
222
|
+
const leaf = bare.slice(namespace.length + 1);
|
|
223
|
+
if (!leaf)
|
|
224
|
+
break;
|
|
225
|
+
for (const prefix of prefixes) {
|
|
226
|
+
const spelling = `${prefix}-${leaf}`;
|
|
227
|
+
/** A primeira declaração vence, a mesma regra da colheita de custom properties. */
|
|
228
|
+
if (!out.has(spelling))
|
|
229
|
+
out.set(spelling, name);
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* UMA PALAVRA DE CLASSE NUMA LINHA - e as três coisas que ela NÃO pode ser.
|
|
238
|
+
*
|
|
239
|
+
* `bg-primary`, `md:p-6`, `hover:rounded-lg!`, `bg-primary/50`: a variante e o modificador ficam de
|
|
240
|
+
* fora do casamento porque o compilador também os separa do nome do token.
|
|
241
|
+
*
|
|
242
|
+
* O que este padrão recusa é o que estragaria a contagem:
|
|
243
|
+
*
|
|
244
|
+
* `--font-serif: …` a DECLARAÇÃO do token, que não é uso dele
|
|
245
|
+
* `var(--font-serif)` já contado pela régua de `var()` - contar aqui seria contar duas vezes
|
|
246
|
+
* `font-serifed` um nome maior que por acaso começa igual
|
|
247
|
+
*
|
|
248
|
+
* As três caem pela mesma guarda: o caractere ANTES não pode ser hífen, letra, dígito ou `_`.
|
|
249
|
+
*/
|
|
250
|
+
const CLASS_WORD = /[a-z][a-z0-9]*(?:-[a-z0-9.]+)+/g;
|
|
251
|
+
/** As grafias deste sistema que esta linha realmente escreve. */
|
|
252
|
+
export function utilitiesOn(line, utilities) {
|
|
253
|
+
if (utilities.size === 0)
|
|
254
|
+
return [];
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const m of line.matchAll(CLASS_WORD)) {
|
|
257
|
+
const at = m.index ?? 0;
|
|
258
|
+
const before = at > 0 ? line[at - 1] : "";
|
|
259
|
+
if (before && /[-\w]/.test(before))
|
|
260
|
+
continue;
|
|
261
|
+
const name = utilities.get(m[0]);
|
|
262
|
+
if (name)
|
|
263
|
+
out.push(name);
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
package/dist/doctor/ledger.js
CHANGED
|
@@ -118,7 +118,7 @@ export function unrepresentedFrom(findings, matched) {
|
|
|
118
118
|
}
|
|
119
119
|
return out;
|
|
120
120
|
}
|
|
121
|
-
const ledgerPath = (root) => join(root, "_synthesisui", LEDGER_FILE);
|
|
121
|
+
export const ledgerPath = (root) => join(root, "_synthesisui", LEDGER_FILE);
|
|
122
122
|
/** Keep the file from growing forever: past ~1MB, keep the newest half. A
|
|
123
123
|
* trend needs recent history, not an archive. */
|
|
124
124
|
const MAX_BYTES = 1_000_000;
|
|
@@ -221,8 +221,14 @@ export async function readEvents(root) {
|
|
|
221
221
|
* medido: `coverage 40% -> 0% since the first run` na cópia do `web-subscribe`, e `30% -> 28%` no
|
|
222
222
|
* `frontend-hub`. O denominador cresceu porque a leitura melhorou, e comparar as duas fotos é
|
|
223
223
|
* comparar réguas diferentes.
|
|
224
|
+
*
|
|
225
|
+
* A 4 é a utility do tema DELE entrando na conta (12/09): `bg-primary` aponta para
|
|
226
|
+
* `--color-primary` exatamente como `var(--color-primary)` aponta, e só a segunda forma era vista.
|
|
227
|
+
* Aqui o salto é para CIMA e é grande - num projeto cujo idioma medido é utility, quase todo
|
|
228
|
+
* apontamento estava fora da conta -, então comparar as duas fotos diria que alguém consertou o
|
|
229
|
+
* repositório num dia. Ver `idiom-names.ts`.
|
|
224
230
|
*/
|
|
225
|
-
export const COVERAGE_RULE =
|
|
231
|
+
export const COVERAGE_RULE = 4;
|
|
226
232
|
/**
|
|
227
233
|
* POR QUE A TENDÊNCIA RECOMEÇOU - uma frase por régua, e a saída lê ESTA, nunca uma fixa.
|
|
228
234
|
*
|
|
@@ -233,6 +239,7 @@ export const COVERAGE_RULE = 3;
|
|
|
233
239
|
export const COVERAGE_RULE_REASON = {
|
|
234
240
|
2: "your own tokens count now too",
|
|
235
241
|
3: "every length in a shorthand counts now - `padding: 8px 24px` was one value and is two",
|
|
242
|
+
4: "the utilities of your own theme count now - `bg-primary` points at `--color-primary` the same way `var(--color-primary)` does",
|
|
236
243
|
};
|
|
237
244
|
export function summarize(events) {
|
|
238
245
|
const hooks = events.filter((e) => e.kind === "hook" && e.file);
|
package/dist/doctor/scan.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Text in, findings out. No filesystem, no AST, no network - a diagnosis that
|
|
10
10
|
* takes eight seconds and needs a build step is a diagnosis nobody runs.
|
|
11
11
|
*/
|
|
12
|
+
import { utilitiesOn } from "./idiom-names.js";
|
|
12
13
|
import { normalizeValue, tokenMatch } from "./tokens.js";
|
|
13
14
|
/**
|
|
14
15
|
* O NOME QUE SE ESCREVE NO ARQUIVO DELE - e existe uma função só para isto por um motivo.
|
|
@@ -241,6 +242,23 @@ const unjudgeable = (name, line) => {
|
|
|
241
242
|
line.startsWith("*") ||
|
|
242
243
|
line.startsWith("/*"));
|
|
243
244
|
};
|
|
245
|
+
/**
|
|
246
|
+
* QUANTAS VEZES ESTA LINHA APONTA PARA UM TOKEN - nas DUAS formas de apontar.
|
|
247
|
+
*
|
|
248
|
+
* ═══ O QUE O CLIENTE VIA, medido em 12/09 no `codelevel` ═══
|
|
249
|
+
*
|
|
250
|
+
* Um arquivo da landing dele com **103 utilities do tema dele** era contado com **0 usos**. O
|
|
251
|
+
* contador conhecia uma forma só - `var(--x)` -, e o idioma medido daquele projeto é outro:
|
|
252
|
+
* `conventions[".tsx"].idiom = "tailwind-utility"`, share 72,3%. A régua media com a régua do
|
|
253
|
+
* idioma vizinho, então quem escreveu a landing inteira no vocabulário do próprio sistema recebia
|
|
254
|
+
* a mesma nota de quem não usou nada dele.
|
|
255
|
+
*
|
|
256
|
+
* ═══ E A SEGUNDA FORMA SAI DO QUE ELE DECLAROU ═══
|
|
257
|
+
*
|
|
258
|
+
* `table.utilities` é derivada dos namespaces de tema que o sistema dele declara, pelo contrato
|
|
259
|
+
* publicado do framework - ver `idiom-names.ts`. Um sistema sem nenhum desses namespaces devolve
|
|
260
|
+
* índice vazio, e esta função volta a ser exatamente a de antes.
|
|
261
|
+
*/
|
|
244
262
|
function countTokenUses(line, table) {
|
|
245
263
|
let n = 0;
|
|
246
264
|
const judge = canJudgeDsNames(table);
|
|
@@ -253,7 +271,7 @@ function countTokenUses(line, table) {
|
|
|
253
271
|
if (isKnownToken(name, table))
|
|
254
272
|
n++;
|
|
255
273
|
}
|
|
256
|
-
return n;
|
|
274
|
+
return n + utilitiesOn(line, table.utilities).length;
|
|
257
275
|
}
|
|
258
276
|
/** The undeclared `--ds-` names on a line, for the report. Same rules as the
|
|
259
277
|
* counter above, so a name is never both uncounted and unmentioned. */
|
|
@@ -880,7 +898,25 @@ export function diagnose(files) {
|
|
|
880
898
|
named: flat.filter((f) => nameToWrite(f)).length,
|
|
881
899
|
tokenUses,
|
|
882
900
|
phantomUses,
|
|
883
|
-
|
|
901
|
+
reach:
|
|
902
|
+
/**
|
|
903
|
+
* NADA FOI ABERTO é diferente de ABRI E NÃO HAVIA NADA - achado da revisão de QA no fecho,
|
|
904
|
+
* e o terceiro caso existia no tipo sem nenhum produtor.
|
|
905
|
+
*
|
|
906
|
+
* Um caminho fora do escopo lido, uma pasta sem arquivo legível, um alvo que o filtro
|
|
907
|
+
* recusou: ali a régua não mediu, e um percentual - qualquer percentual - seria uma nota
|
|
908
|
+
* sobre um exame que não aconteceu.
|
|
909
|
+
*/
|
|
910
|
+
files.length === 0
|
|
911
|
+
? { measured: false, why: "never measured" }
|
|
912
|
+
: total === 0
|
|
913
|
+
? { measured: false, why: "nothing here carries a design value" }
|
|
914
|
+
: {
|
|
915
|
+
measured: true,
|
|
916
|
+
percent: Math.round(((tokenUses + ownUses) / total) * 100),
|
|
917
|
+
uses: tokenUses + ownUses,
|
|
918
|
+
of: total,
|
|
919
|
+
},
|
|
884
920
|
ownTokens: ownDeclared.size,
|
|
885
921
|
ownUses,
|
|
886
922
|
ownMirrored,
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Pure and dependency-free on purpose: every function here takes text and
|
|
12
12
|
* returns data, so the whole diagnosis is testable without a filesystem.
|
|
13
13
|
*/
|
|
14
|
+
import { utilitySpellings } from "./idiom-names.js";
|
|
14
15
|
import { DEFAULT_ROOT_PX } from "./root-size.js";
|
|
15
16
|
import { parseSchemeBlocks } from "./scheme-blocks.js";
|
|
16
17
|
import { compiledNames } from "./their-compiled-names.js";
|
|
@@ -25,6 +26,7 @@ export const EMPTY_TABLE = {
|
|
|
25
26
|
rootFrom: null,
|
|
26
27
|
aliases: new Map(),
|
|
27
28
|
declared: new Set(),
|
|
29
|
+
utilities: new Map(),
|
|
28
30
|
compiledAway: new Map(),
|
|
29
31
|
keyframes: new Set(),
|
|
30
32
|
};
|
|
@@ -368,6 +370,7 @@ export function buildTable(input) {
|
|
|
368
370
|
else
|
|
369
371
|
byCompiledValue.set(key, [one]);
|
|
370
372
|
}
|
|
373
|
+
const declaredNames = parseDeclaredNames(input.css);
|
|
371
374
|
const byValue = new Map();
|
|
372
375
|
for (const [name, value] of resolved) {
|
|
373
376
|
const key = normalizeValue(value, rootPx);
|
|
@@ -390,7 +393,12 @@ export function buildTable(input) {
|
|
|
390
393
|
rootFrom: input.rootFrom ?? null,
|
|
391
394
|
/** Vazio até `withTheirNames` ler o vocabulário dele - ver `their-names.ts`. */
|
|
392
395
|
aliases: new Map(),
|
|
393
|
-
declared:
|
|
396
|
+
declared: declaredNames,
|
|
397
|
+
/**
|
|
398
|
+
* Derivada do que ELE DECLAROU NUM BLOCO `@theme`, nunca da forma de um nome - ver
|
|
399
|
+
* `idiom-names.ts`, onde está o caso que a régua por forma quebrava.
|
|
400
|
+
*/
|
|
401
|
+
utilities: utilitySpellings(input.css),
|
|
394
402
|
compiledAway: byCompiledValue,
|
|
395
403
|
keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
|
|
396
404
|
};
|
package/dist/governed.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readProjectConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* O QUE ESTA CHECAGEM OLHA - e a resposta é UMA, lida do mesmo lugar por quem verifica e por quem
|
|
4
|
+
* explica.
|
|
5
|
+
*
|
|
6
|
+
* ═══ O DEFEITO QUE ISTO FECHA - achado da revisão de contrato no fecho da etapa 21 ═══
|
|
7
|
+
*
|
|
8
|
+
* Havia três réguas respondendo "o que está governado". O `import` derivava dos `package.json`; a
|
|
9
|
+
* frase que o `connect` imprime derivava de `census.adoption.consumers`, que só existe quando
|
|
10
|
+
* alguém digitou `--usage`; e a checagem em si não consultava nenhuma das duas - ela olhava TODO
|
|
11
|
+
* arquivo de UI do repositório.
|
|
12
|
+
*
|
|
13
|
+
* O efeito num cliente que nunca digitou `--usage`: a tela dizia *"verificado aqui: packages/ui"* e
|
|
14
|
+
* fechava com *"em qualquer outro lugar nada é lido"*, enquanto a checagem estava lendo `apps/`,
|
|
15
|
+
* `packages/db` e tudo o mais. **Lacuna declarada ERRADA é pior que lacuna calada**: ele confia num
|
|
16
|
+
* limite que não existe.
|
|
17
|
+
*
|
|
18
|
+
* ═══ A REGRA, e ela é a mais simples possível ═══
|
|
19
|
+
*
|
|
20
|
+
* Todo arquivo onde um valor de design pode aparecer está governado, menos o que é NOSSO e menos o
|
|
21
|
+
* que ELE declarou fora. Não há lista de entrada: uma lista de "governados" escrita à mão envelhece
|
|
22
|
+
* no dia em que um app novo importa a biblioteca, e aí o produto cala sobre o lugar mais novo do
|
|
23
|
+
* repositório.
|
|
24
|
+
*/
|
|
25
|
+
/** Arquivos onde um valor de design pode aparecer. Um README não tem o que ser checado. */
|
|
26
|
+
export const UI_FILE = /\.(tsx|jsx|ts|js|css|scss|vue|svelte)$/i;
|
|
27
|
+
/** O que ELE tirou da governança, lido do arquivo do repositório dele. */
|
|
28
|
+
export async function ungovernedIn(root) {
|
|
29
|
+
return (await readProjectConfig(root).catch(() => null))?.ungoverned ?? [];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Este caminho relativo está sob a checagem?
|
|
33
|
+
*
|
|
34
|
+
* Três recusas, e cada uma fecha um caso real:
|
|
35
|
+
*
|
|
36
|
+
* fora do projeto não é da nossa conta
|
|
37
|
+
* é artefato nosso `_synthesisui/` é a resposta, nunca o problema
|
|
38
|
+
* ele declarou fora `ungoverned` em `_synthesisui/config.json`
|
|
39
|
+
*
|
|
40
|
+
* O CAMINHO TESTADO É O RELATIVO - achado da revisão de QA no fecho da etapa 20.
|
|
41
|
+
* `abs.includes("_synthesisui")` olhava o caminho ABSOLUTO, então quem clonasse o projeto para
|
|
42
|
+
* qualquer diretório com esse nome no meio (`~/_synthesisui-demo/app`) tinha TODOS os arquivos
|
|
43
|
+
* pulados e uma checagem permanentemente muda.
|
|
44
|
+
*/
|
|
45
|
+
export function governs(rel, ungoverned) {
|
|
46
|
+
if (rel.startsWith(".."))
|
|
47
|
+
return false;
|
|
48
|
+
if (!UI_FILE.test(rel))
|
|
49
|
+
return false;
|
|
50
|
+
if (rel.split("/").includes("_synthesisui"))
|
|
51
|
+
return false;
|
|
52
|
+
return !ungoverned.some((p) => rel === p || rel.startsWith(`${p}/`));
|
|
53
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { hasHook } from "./agent-wiring.js";
|
|
4
|
+
import { ungovernedIn } from "./governed.js";
|
|
5
|
+
/**
|
|
6
|
+
* ATÉ ONDE A GARANTIA VAI - dito uma vez, com número, e sem virar ruído.
|
|
7
|
+
*
|
|
8
|
+
* ═══ O QUE O CLIENTE VIVIA ═══
|
|
9
|
+
*
|
|
10
|
+
* A checagem que roda a cada escrita é parcial por construção, e nada dizia isso. Ela compara o
|
|
11
|
+
* arquivo contra o último commit, então num arquivo que o git nunca viu a metade que fala de regra
|
|
12
|
+
* fica INERTE - e ficava inerte em silêncio. Ela lê duas formas de apontar para um token, e num
|
|
13
|
+
* projeto que escreve de uma terceira ela concordaria calada. Ela governa os apps que consomem a
|
|
14
|
+
* biblioteca, e ele não tinha como saber disso nem como sair.
|
|
15
|
+
*
|
|
16
|
+
* Nenhuma dessas três é defeito: são fronteiras. O defeito é confundir fronteira com aprovação -
|
|
17
|
+
* uma checagem que cala parece uma checagem que aprovou, e é assim que alguém confia numa garantia
|
|
18
|
+
* parcial sem saber que ela é parcial. A lei 8 já dizia: lacuna declarada é confiança, lacuna
|
|
19
|
+
* calada é bug.
|
|
20
|
+
*
|
|
21
|
+
* ═══ POR QUE UMA VEZ, E NÃO A CADA RODADA ═══
|
|
22
|
+
*
|
|
23
|
+
* Esta é a mesma tensão do hook: um aviso que aparece sempre é um aviso que a pessoa aprende a
|
|
24
|
+
* pular, e aí ele não serve para a vez em que era mesmo necessário. Então o texto sai quando a
|
|
25
|
+
* RESPOSTA é nova - a primeira vez, e depois só quando ela muda. A impressão digital do que foi
|
|
26
|
+
* dito fica ao lado do sistema, e quem apaga o sistema apaga a memória junto.
|
|
27
|
+
*
|
|
28
|
+
* ═══ E TODO NÚMERO AQUI É MEDIDO ═══
|
|
29
|
+
*
|
|
30
|
+
* Quantas grafias o idioma dele gera, quais lugares estão governados, o que ele declarou fora:
|
|
31
|
+
* tudo sai do vocabulário que o sistema declara, da medição que a esteira guardou e do arquivo de
|
|
32
|
+
* configuração que é dele. Nada aqui é uma promessa escrita à mão.
|
|
33
|
+
*/
|
|
34
|
+
/** Onde mora a impressão digital do que já foi dito - ver o cabeçalho. */
|
|
35
|
+
const SAID = "_synthesisui/.guarantee";
|
|
36
|
+
async function measuredPlaces(root) {
|
|
37
|
+
const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => "");
|
|
38
|
+
if (!raw)
|
|
39
|
+
return { scope: null, consumers: [], specifier: null };
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
const scope = typeof parsed.scope === "string" && parsed.scope.trim()
|
|
43
|
+
? parsed.scope.trim()
|
|
44
|
+
: null;
|
|
45
|
+
const listed = Array.isArray(parsed.adoption?.consumers)
|
|
46
|
+
? (parsed.adoption?.consumers).filter((c) => c && typeof c.label === "string")
|
|
47
|
+
: [];
|
|
48
|
+
/**
|
|
49
|
+
* `usage` É A QUEDA, e ela existe para um projeto medido antes de a adoção viajar no arquivo:
|
|
50
|
+
* ali os dois apps estão gravados como caminhos, sem a contagem. Dizer o lugar sem o número é
|
|
51
|
+
* pior que não dizer? Não - o lugar é o que ele precisa para saber onde a garantia vale.
|
|
52
|
+
*/
|
|
53
|
+
const usage = Array.isArray(parsed.usage)
|
|
54
|
+
? parsed.usage.filter((u) => typeof u === "string" && !!u.trim())
|
|
55
|
+
: [];
|
|
56
|
+
const consumers = listed.length > 0
|
|
57
|
+
? listed
|
|
58
|
+
: usage.map((label) => ({ label, files: 0, using: 0 }));
|
|
59
|
+
return {
|
|
60
|
+
scope,
|
|
61
|
+
consumers,
|
|
62
|
+
specifier: typeof parsed.adoption?.specifier === "string"
|
|
63
|
+
? parsed.adoption.specifier
|
|
64
|
+
: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { scope: null, consumers: [], specifier: null };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* O QUE A CHECAGEM ALCANÇA NESTE PROJETO - `null` quando não há sistema instalado.
|
|
73
|
+
*
|
|
74
|
+
* Sem sistema não há garantia a delimitar, e uma seção explicando as fronteiras de algo que não
|
|
75
|
+
* está ligado é a definição de ruído.
|
|
76
|
+
*/
|
|
77
|
+
export async function guarantee(root,
|
|
78
|
+
/**
|
|
79
|
+
* O VOCABULÁRIO DO SISTEMA INSTALADO - passado por quem já o carregou.
|
|
80
|
+
*
|
|
81
|
+
* `connect` lê o sistema de qualquer forma; carregá-lo de novo aqui pagaria a varredura duas
|
|
82
|
+
* vezes na tela em que a pessoa está esperando.
|
|
83
|
+
*/
|
|
84
|
+
system) {
|
|
85
|
+
if (!system.name)
|
|
86
|
+
return null;
|
|
87
|
+
const { scope, consumers, specifier } = await measuredPlaces(root);
|
|
88
|
+
/**
|
|
89
|
+
* A GOVERNANÇA VEM DA MESMA FUNÇÃO QUE A CHECAGEM USA - ver `governed.ts`.
|
|
90
|
+
*
|
|
91
|
+
* Ela vinha de `census.adoption.consumers`, que só existe quando alguém digitou `--usage`, e a
|
|
92
|
+
* checagem nunca consultou aquilo: num cliente que não digitou, esta seção afirmava um limite
|
|
93
|
+
* que não existia. Lacuna declarada ERRADA é pior que lacuna calada.
|
|
94
|
+
*/
|
|
95
|
+
const out = await ungovernedIn(root);
|
|
96
|
+
const wired = await hasHook(root).catch(() => false);
|
|
97
|
+
const lines = [];
|
|
98
|
+
const plain = (text) => ({ text, values: {} });
|
|
99
|
+
lines.push(plain(wired
|
|
100
|
+
? "Every file this project writes is checked as it is written - by an edit tool or by a shell command, it makes no difference."
|
|
101
|
+
: "Nothing is checked automatically here yet: this project has no write check installed."));
|
|
102
|
+
/**
|
|
103
|
+
* A METADE QUE FICA INERTE SEM "ANTES" - e é a lacuna que ele mais sente, porque ela é
|
|
104
|
+
* invisível: um arquivo novo nunca apagou nada, então a checagem cala com razão e a pessoa lê
|
|
105
|
+
* esse silêncio como aprovação.
|
|
106
|
+
*/
|
|
107
|
+
lines.push(plain("What a file no longer has is measured against your last commit. A file git has never seen has no before, so nothing is reported as removed there."));
|
|
108
|
+
lines.push(system.utilities.size > 0
|
|
109
|
+
? {
|
|
110
|
+
text: "A value counts as coming from your system in two spellings: `var(--token)`, and the {n} utility names your own theme generates - `bg-primary`, `p-6`, `rounded-lg`.",
|
|
111
|
+
values: { n: String(system.utilities.size) },
|
|
112
|
+
}
|
|
113
|
+
: plain("A value counts as coming from your system when it is written as `var(--token)`. This system declares no theme names, so a utility class is not read as a reference to it."));
|
|
114
|
+
/**
|
|
115
|
+
* ONDE, e a frase é a REGRA e não uma lista de lugares.
|
|
116
|
+
*
|
|
117
|
+
* A primeira versão listava o escopo medido e os consumidores e fechava com *"em qualquer outro
|
|
118
|
+
* lugar nada é lido"* - falso, porque a checagem olha todo arquivo de UI do repositório. Agora
|
|
119
|
+
* ela diz a regra que o código aplica, e a única lista que aparece é a que ELE escreveu.
|
|
120
|
+
*/
|
|
121
|
+
lines.push(out.length > 0
|
|
122
|
+
? {
|
|
123
|
+
text: "Every file in this project is checked as you write it, except {out}, which you named under `ungoverned` in `_synthesisui/config.json`.",
|
|
124
|
+
values: { out: out.map((p) => `\`${p}\``).join(", ") },
|
|
125
|
+
}
|
|
126
|
+
: plain("Every file in this project is checked as you write it. To leave a place out, name it under `ungoverned` in `_synthesisui/config.json`."));
|
|
127
|
+
/**
|
|
128
|
+
* E DE ONDE O SISTEMA FOI LIDO - outra pergunta, e por isso outra frase.
|
|
129
|
+
*
|
|
130
|
+
* Ler e vigiar não são a mesma coisa: a esteira abriu `packages/ui` e o que ela sabe sobre design
|
|
131
|
+
* vem dali. Um app que só CONSOME a biblioteca é vigiado na escrita sem nunca ter sido lido, e
|
|
132
|
+
* juntar as duas numa frase foi o que produziu um limite falso.
|
|
133
|
+
*/
|
|
134
|
+
if (scope)
|
|
135
|
+
lines.push(consumers.length > 0 && specifier
|
|
136
|
+
? {
|
|
137
|
+
text: "What this system knows was read from {scope}. {apps} declare a dependency on `{specifier}`, so they write in its vocabulary - nothing in them was read.",
|
|
138
|
+
values: {
|
|
139
|
+
scope: `\`${scope}\``,
|
|
140
|
+
apps: consumers.map((c) => `\`${c.label}\``).join(", "),
|
|
141
|
+
specifier,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
: {
|
|
145
|
+
text: "What this system knows was read from {scope} - nothing outside it was opened.",
|
|
146
|
+
values: { scope: `\`${scope}\`` },
|
|
147
|
+
});
|
|
148
|
+
lines.push(plain("A value this ruler cannot read is not a value it approved - silence here is never a pass."));
|
|
149
|
+
return {
|
|
150
|
+
lines,
|
|
151
|
+
fingerprint: lines
|
|
152
|
+
.map((l) => `${l.text}|${Object.values(l.values).join(",")}`)
|
|
153
|
+
.join("\n"),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* AS LINHAS, SE ELAS AINDA NÃO FORAM DITAS - e `null` quando a resposta é a mesma de antes.
|
|
158
|
+
*
|
|
159
|
+
* A escrita acontece ANTES de a frase sair, pelo mesmo motivo da saudação do hook: uma escrita que
|
|
160
|
+
* falha não pode produzir um produto que repete o mesmo parágrafo para sempre.
|
|
161
|
+
*/
|
|
162
|
+
export async function guaranteeIfNew(root, system) {
|
|
163
|
+
const answer = await guarantee(root, system).catch(() => null);
|
|
164
|
+
if (!answer)
|
|
165
|
+
return null;
|
|
166
|
+
const before = await readFile(join(root, SAID), "utf8").catch(() => "");
|
|
167
|
+
if (before.trim() === answer.fingerprint.trim())
|
|
168
|
+
return null;
|
|
169
|
+
await writeFile(join(root, SAID), `${answer.fingerprint}\n`, "utf8").catch(() => { });
|
|
170
|
+
return answer.lines;
|
|
171
|
+
}
|