synthesisui 0.16.134 → 0.16.136
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/commands/gaps.js +47 -0
- package/dist/commands/import.js +86 -9
- package/dist/doctor/css-modules.js +44 -0
- package/dist/doctor/fragments.js +19 -2
- package/dist/doctor/gap-triage.js +193 -0
- package/dist/doctor/style-ledger.js +1 -0
- package/dist/index.js +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `synthesisui gaps` - o que esta esteira NÃO leu do projeto, triado.
|
|
3
|
+
*
|
|
4
|
+
* O `import` já imprime o ledger no fim de uma execução, e é ali que a pessoa o vê uma vez. Depois
|
|
5
|
+
* disso o número fica em `_synthesisui/census.json` e não existe pergunta que se possa fazer a ele:
|
|
6
|
+
* "quanto disso é trabalho de vocês?" não tinha resposta em lugar nenhum, e sem ela o ciclo de
|
|
7
|
+
* conserto continua dependendo de alguém reportar (dono, 05/08).
|
|
8
|
+
*
|
|
9
|
+
* Não mede nada: lê o censo em disco. Medir aqui seria uma segunda leitura dos arquivos deles, com
|
|
10
|
+
* uma segunda chance de discordar do que a receita diz.
|
|
11
|
+
*
|
|
12
|
+
* E o texto que ele imprime é o que a pessoa manda para quem vai escrever o leitor - por isso ele
|
|
13
|
+
* nomeia o módulo e nunca sugere código.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { describeTriage, triageLedger } from "../doctor/gap-triage.js";
|
|
18
|
+
import { body, section } from "../output.js";
|
|
19
|
+
export async function gaps(opts) {
|
|
20
|
+
const root = opts.dir ?? process.cwd();
|
|
21
|
+
const path = opts.census ?? join(root, "_synthesisui", "census.json");
|
|
22
|
+
const raw = await readFile(path, "utf8").catch(() => null);
|
|
23
|
+
console.log(section("What this pipeline did not read"));
|
|
24
|
+
if (!raw) {
|
|
25
|
+
console.log(body(`No census at ${path}. Run \`synthesisui import --dry\` first - that is what measures your files, and it writes the census there.`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let ledger;
|
|
29
|
+
try {
|
|
30
|
+
ledger = JSON.parse(raw).ledger;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
console.log(body(`${path} is not readable JSON.`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* UM CENSO SEM LEDGER NÃO É UM PROJETO SEM LACUNA. Ele foi medido por uma versão que não
|
|
38
|
+
* contabilizava, e dizer "0 gaps" aqui seria a mentira mais barata que este comando poderia
|
|
39
|
+
* contar.
|
|
40
|
+
*/
|
|
41
|
+
if (!ledger) {
|
|
42
|
+
console.log(body(`This census was written by a CLI that did not count style fragments yet, so the numbers do not exist rather than being zero. Run \`synthesisui import --dry\` again with ${opts.cli} to measure.`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const line of describeTriage(triageLedger(ledger, opts.cli)))
|
|
46
|
+
console.log(body(line));
|
|
47
|
+
}
|
package/dist/commands/import.js
CHANGED
|
@@ -13,7 +13,7 @@ import { describeGate, gateComponent, groupSkips, SCREENS_FOR_SYSTEM, } from "..
|
|
|
13
13
|
import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
|
|
14
14
|
import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/coverage.js";
|
|
15
15
|
import { crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
|
|
16
|
-
import { keyframeOffsets, moduleImports, readModuleCss, readModuleUsage, transcribeModule, } from "../doctor/css-modules.js";
|
|
16
|
+
import { keyframeOffsets, moduleImports, partialCandidates, readModuleCss, readModuleUsage, sheetImports, transcribeModule, } from "../doctor/css-modules.js";
|
|
17
17
|
import { dataContract } from "../doctor/data-contract.js";
|
|
18
18
|
import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
|
|
19
19
|
import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
|
|
@@ -128,6 +128,48 @@ async function siblingProjects(root) {
|
|
|
128
128
|
* this" - could not be said, and we ended up rediscovering by colour distance
|
|
129
129
|
* what their stylesheet had spelled out all along.
|
|
130
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* UMA FOLHA E TUDO O QUE ELA TRAZ PARA DENTRO DE SI, na ordem em que o sass compila.
|
|
133
|
+
*
|
|
134
|
+
* Uma partial (`_darkMode.scss`) nunca é compilada sozinha: ela entra em quem a importa, e portanto
|
|
135
|
+
* herda o escopo de quem a importa. Ler o `.module.scss` sem seguir os `@import` dele deixava de
|
|
136
|
+
* fora 1 400 declarações (91% de tudo o que a esteira chamava de "CSS global sem leitor") - e elas
|
|
137
|
+
* são, no build dele, as faces light e dark de classes que o componente JÁ veste (medido, 05/08).
|
|
138
|
+
*
|
|
139
|
+
* Devolve o CSS concatenado e os arquivos que entraram, porque o ledger precisa saber que a partial
|
|
140
|
+
* pertence a um componente admitido - senão ela continua contabilizada como forma sem leitor, e o
|
|
141
|
+
* relatório passa a discordar da receita.
|
|
142
|
+
*/
|
|
143
|
+
async function sheetWithImports(path, seen = new Set(), depth = 0) {
|
|
144
|
+
if (depth > 6 || seen.has(path))
|
|
145
|
+
return { css: "", files: [] };
|
|
146
|
+
seen.add(path);
|
|
147
|
+
const body = await readFile(path, "utf8").catch(() => "");
|
|
148
|
+
if (!body)
|
|
149
|
+
return { css: "", files: [] };
|
|
150
|
+
let css = "";
|
|
151
|
+
const files = [path];
|
|
152
|
+
/**
|
|
153
|
+
* O IMPORTADO VEM ANTES, que é a ordem do sass: quem importa sobrescreve o que importou. Inverter
|
|
154
|
+
* isto faria a face dark de uma partial vencer a base do próprio module.
|
|
155
|
+
*/
|
|
156
|
+
for (const spec of sheetImports(body)) {
|
|
157
|
+
for (const candidate of partialCandidates(spec)) {
|
|
158
|
+
const child = join(dirname(path), candidate);
|
|
159
|
+
const nested = await sheetWithImports(child, seen, depth + 1);
|
|
160
|
+
if (nested.css || nested.files.length > 1 || (await exists(child))) {
|
|
161
|
+
css += nested.css;
|
|
162
|
+
files.push(...nested.files);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { css: `${css}\n${body}`, files };
|
|
168
|
+
}
|
|
169
|
+
/** Existe no disco - o candidato de partial que o sass acharia primeiro. */
|
|
170
|
+
async function exists(path) {
|
|
171
|
+
return await readFile(path, "utf8").then(() => true, () => false);
|
|
172
|
+
}
|
|
131
173
|
async function harvestOwnCss(root,
|
|
132
174
|
/**
|
|
133
175
|
* O LEDGER TAMBÉM COME AQUI, e por um motivo de promessa: este é o único laço que vê TODO
|
|
@@ -139,17 +181,40 @@ async function harvestOwnCss(root,
|
|
|
139
181
|
* não admitido é um motivo, não um desaparecimento.
|
|
140
182
|
*/
|
|
141
183
|
into) {
|
|
142
|
-
|
|
184
|
+
/**
|
|
185
|
+
* DOIS PASSES, porque o escopo de uma folha está no GRAFO e não no nome dela.
|
|
186
|
+
*
|
|
187
|
+
* O primeiro junta tudo; o segundo julga cada folha sabendo quem importa quem. Um pass só
|
|
188
|
+
* decidia por `/\.module\./` e chamava de "CSS global sem leitor" 1 400 declarações que o build
|
|
189
|
+
* deles compila dentro de um module (medido, 05/08).
|
|
190
|
+
*/
|
|
191
|
+
const sheets = [];
|
|
143
192
|
for await (const file of walkAll([root])) {
|
|
144
193
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
145
194
|
continue;
|
|
146
195
|
const body = await readFile(file, "utf8").catch(() => "");
|
|
196
|
+
sheets.push({ file, body });
|
|
197
|
+
}
|
|
198
|
+
/** Todo caminho que alguma folha importa, resolvido como o sass resolveria. */
|
|
199
|
+
const imported = new Set();
|
|
200
|
+
for (const sheet of sheets)
|
|
201
|
+
for (const spec of sheetImports(sheet.body))
|
|
202
|
+
for (const candidate of partialCandidates(spec))
|
|
203
|
+
imported.add(join(dirname(sheet.file), candidate));
|
|
204
|
+
let css = "";
|
|
205
|
+
for (const { file, body } of sheets) {
|
|
147
206
|
css += `\n${body}`;
|
|
148
|
-
if (into
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
207
|
+
if (!into || !body)
|
|
208
|
+
continue;
|
|
209
|
+
const partial = basename(file).startsWith("_");
|
|
210
|
+
into.push({
|
|
211
|
+
file: relative(root, file),
|
|
212
|
+
fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file) || (partial && imported.has(file))
|
|
213
|
+
? "scoped"
|
|
214
|
+
: partial
|
|
215
|
+
? "dead"
|
|
216
|
+
: "global"),
|
|
217
|
+
});
|
|
153
218
|
}
|
|
154
219
|
return css;
|
|
155
220
|
}
|
|
@@ -564,7 +629,13 @@ export async function takeCensus(root, opts) {
|
|
|
564
629
|
let fromModule = null;
|
|
565
630
|
for (const mod of modules) {
|
|
566
631
|
const cssPath = join(dirname(file), mod.specifier);
|
|
567
|
-
|
|
632
|
+
/**
|
|
633
|
+
* O MODULE E AS PARTIALS QUE ELE IMPORTA, que no build dele são um arquivo só. Ler apenas
|
|
634
|
+
* o `.module.scss` deixava 1 400 declarações fora - as faces light e dark de classes que
|
|
635
|
+
* o componente já veste, escritas em `_lightMode.scss`/`_darkMode.scss` ao lado.
|
|
636
|
+
*/
|
|
637
|
+
const sheet = await sheetWithImports(cssPath);
|
|
638
|
+
const moduleCss = sheet.css.trim();
|
|
568
639
|
if (!moduleCss)
|
|
569
640
|
continue;
|
|
570
641
|
const usage = readModuleUsage(src, mod.local);
|
|
@@ -574,8 +645,14 @@ export async function takeCensus(root, opts) {
|
|
|
574
645
|
* ESTE MODULE PERTENCE A UM COMPONENTE ADMITIDO - o único fato que este laço tem e o
|
|
575
646
|
* ledger precisa. Os fragmentos dele vêm do `harvestOwnCss`, que vê todos os arquivos
|
|
576
647
|
* de estilo e não só os de componente aceito.
|
|
648
|
+
*
|
|
649
|
+
* A PARTIAL ENTRA JUNTO: ela é parte deste module, então o veredito dela é o mesmo. Sem
|
|
650
|
+
* isto o relatório continuaria chamando de "forma sem leitor" exatamente o que a receita
|
|
651
|
+
* acabou de ler - relatório e receita discordando, que é o defeito que o ledger existe
|
|
652
|
+
* para não ter.
|
|
577
653
|
*/
|
|
578
|
-
|
|
654
|
+
for (const part of sheet.files)
|
|
655
|
+
admittedStyles.add(relative(root, part));
|
|
579
656
|
const readModule = transcribeModule(readModuleCss(moduleCss), usage, declaredValues);
|
|
580
657
|
if (readModule.read === 0)
|
|
581
658
|
continue;
|
|
@@ -335,6 +335,50 @@ export function moduleImports(source) {
|
|
|
335
335
|
specifier: m[2],
|
|
336
336
|
}));
|
|
337
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* `@import "./_darkMode"`, `@use "./_lightMode.scss"` - o que uma folha traz para dentro de si.
|
|
340
|
+
*
|
|
341
|
+
* UMA PARTIAL NÃO É UMA FOLHA. Um arquivo `_nome.scss` nunca é compilado sozinho - o sass o
|
|
342
|
+
* compila DENTRO de quem o importa, e por isso o escopo dele é o de quem importa. No app do dono,
|
|
343
|
+
* 122 das 138 folhas "globais" são partials e 120 delas são importadas por um `.module.scss` do
|
|
344
|
+
* mesmo diretório: 1 400 declarações (91%) que a esteira classificava como CSS global sem leitor
|
|
345
|
+
* são, no build dele, regras de module - hasheadas e escopadas ao componente (medido, 05/08).
|
|
346
|
+
*
|
|
347
|
+
* Decidir escopo por NOME DE ARQUIVO (`/\.module\./`) é a lei 14 um nível abaixo: a resposta está
|
|
348
|
+
* no grafo de imports, não na string do caminho.
|
|
349
|
+
*/
|
|
350
|
+
const SHEET_IMPORT = /@(?:import|use|forward)\s+["']([^"']+)["']/g;
|
|
351
|
+
export function sheetImports(css) {
|
|
352
|
+
SHEET_IMPORT.lastIndex = 0;
|
|
353
|
+
return ([...stripCssComments(css).matchAll(SHEET_IMPORT)]
|
|
354
|
+
.map((m) => m[1].trim())
|
|
355
|
+
/**
|
|
356
|
+
* Só o que é relativo. `@use "sass:math"` e um pacote do node_modules não carregam decisão de
|
|
357
|
+
* aparência deles, e seguir esses caminhos é como um leitor de CSS vira um resolvedor de
|
|
358
|
+
* módulos pela metade.
|
|
359
|
+
*/
|
|
360
|
+
.filter((spec) => spec.startsWith(".") || !spec.includes(":"))
|
|
361
|
+
.filter((spec) => !spec.startsWith("~") && !spec.startsWith("url(")));
|
|
362
|
+
}
|
|
363
|
+
/** `./_darkMode` → os nomes de arquivo que o sass tentaria, na ordem em que ele tenta. */
|
|
364
|
+
export function partialCandidates(specifier) {
|
|
365
|
+
const clean = specifier.replace(/^\.\//, "");
|
|
366
|
+
const slash = clean.lastIndexOf("/");
|
|
367
|
+
const dir = slash === -1 ? "" : `${clean.slice(0, slash)}/`;
|
|
368
|
+
const base = slash === -1 ? clean : clean.slice(slash + 1);
|
|
369
|
+
if (/\.(css|scss|sass|less)$/.test(base))
|
|
370
|
+
return base.startsWith("_")
|
|
371
|
+
? [`${dir}${base}`]
|
|
372
|
+
: [`${dir}${base}`, `${dir}_${base}`];
|
|
373
|
+
const stem = base.startsWith("_") ? base : `_${base}`;
|
|
374
|
+
return [
|
|
375
|
+
`${dir}${base}.scss`,
|
|
376
|
+
`${dir}${stem}.scss`,
|
|
377
|
+
`${dir}${base}.sass`,
|
|
378
|
+
`${dir}${stem}.sass`,
|
|
379
|
+
`${dir}${base}.css`,
|
|
380
|
+
];
|
|
381
|
+
}
|
|
338
382
|
/**
|
|
339
383
|
* A generic in type position, not an element.
|
|
340
384
|
*
|
package/dist/doctor/fragments.js
CHANGED
|
@@ -110,7 +110,20 @@ export function fragmentsOfSource(file, source) {
|
|
|
110
110
|
* contagem: um global com regra de CLASSE é forma que nenhum leitor cobre hoje - 399 declarações
|
|
111
111
|
* no repo dele, e a única lacuna que estava calada em vez de declarada (04/08).
|
|
112
112
|
*/
|
|
113
|
-
export function fragmentsOfStylesheet(file, css,
|
|
113
|
+
export function fragmentsOfStylesheet(file, css,
|
|
114
|
+
/**
|
|
115
|
+
* COMO ESTA FOLHA CHEGA NO BUILD DELES, e isso vem do GRAFO DE IMPORTS e nunca do nome do
|
|
116
|
+
* arquivo. Uma partial (`_darkMode.scss`) é compilada dentro de quem a importa e herda o escopo
|
|
117
|
+
* dele: 122 das 138 folhas "globais" do app dele são partials, e 120 entram num `.module.scss`
|
|
118
|
+
* ao lado (medido, 05/08).
|
|
119
|
+
*
|
|
120
|
+
* scoped module, ou partial de um module: o veredito é do componente dono, e
|
|
121
|
+
* `judgeFragments` o dá olhando se o escopo o admitiu
|
|
122
|
+
* global uma folha que o código importa: forma que nenhum leitor cobre hoje
|
|
123
|
+
* dead partial que ninguém importa: o build deles não a compila, e ela não
|
|
124
|
+
* pinta nada - aqui, nem lá
|
|
125
|
+
*/
|
|
126
|
+
kind) {
|
|
114
127
|
const out = [];
|
|
115
128
|
let selector = "";
|
|
116
129
|
for (const [i, raw] of css.split("\n").entries()) {
|
|
@@ -143,7 +156,11 @@ export function fragmentsOfStylesheet(file, css, scoped) {
|
|
|
143
156
|
line: i + 1,
|
|
144
157
|
text: `${selector ? `${selector} ` : ""}{ ${m[1]}: ${m[2].trim()} }`,
|
|
145
158
|
read: false,
|
|
146
|
-
...(
|
|
159
|
+
...(kind === "global"
|
|
160
|
+
? { reason: "shape-not-read" }
|
|
161
|
+
: kind === "dead"
|
|
162
|
+
? { reason: "sheet-not-imported" }
|
|
163
|
+
: {}),
|
|
147
164
|
});
|
|
148
165
|
}
|
|
149
166
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE FAZER COM O QUE NÃO FOI LIDO - triado, e nem tudo é trabalho.
|
|
3
|
+
*
|
|
4
|
+
* O ledger diz quanto e onde. Essa é metade da resposta, e sozinha ela ENGANA: no dashboard do
|
|
5
|
+
* dono, 6 610 dos 8 523 fragmentos não interpretados (78%) não são lacuna de leitura nenhuma - são
|
|
6
|
+
* o portão recusando um componente e classe montada em tempo de execução. Uma pessoa olhando "56%
|
|
7
|
+
* interpretado" conclui que falta ler 44% do app e vai escrever o leitor errado (medido, 04/08).
|
|
8
|
+
*
|
|
9
|
+
* ENTÃO A TRIAGEM É ESTA, e a ordem dela é a ordem em que alguém decide:
|
|
10
|
+
*
|
|
11
|
+
* older-cli medido por uma versão antiga. Re-medir ANTES de qualquer conclusão -
|
|
12
|
+
* a lacuna pode já estar fechada e o trabalho já estar feito.
|
|
13
|
+
* no-reader nenhum leitor entende esta forma. É trabalho, e o módulo é nomeado.
|
|
14
|
+
* value-gap a forma é lida e o VALOR não. O trabalho é nas tabelas de escala,
|
|
15
|
+
* não num leitor novo - confundir os dois custa um módulo inteiro.
|
|
16
|
+
* by-design o portão recusou, ou o valor não existe na fonte. NÃO É LACUNA, e a
|
|
17
|
+
* triagem para aqui em vez de mandar alguém consertar o que não quebrou.
|
|
18
|
+
*
|
|
19
|
+
* O QUE ELA NUNCA FAZ: sugerir código. Ela nomeia o módulo onde o leitor mora ou moraria, e a
|
|
20
|
+
* decisão de escrever é de quem lê - a esteira opinando sobre o nosso backlog no terminal do
|
|
21
|
+
* cliente é ela falando de um assunto que não é dela (dono, 05/08).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* QUEM É DONO DE CADA FORMA, hoje.
|
|
25
|
+
*
|
|
26
|
+
* Nomear o módulo é a diferença entre "isto não é lido" e "isto é trabalho aqui". A forma que não
|
|
27
|
+
* tem dono ganha `null`, e um leitor novo é um módulo novo - dizer que ele "vai em transcribe"
|
|
28
|
+
* quando não vai é pior que não dizer nada.
|
|
29
|
+
*/
|
|
30
|
+
const OWNER = {
|
|
31
|
+
class: "src/doctor/transcribe.ts",
|
|
32
|
+
template: "src/doctor/sketch.ts (the literal halves) + src/doctor/variant-read.ts",
|
|
33
|
+
inline: "src/doctor/transcribe.ts (readInlineStyle)",
|
|
34
|
+
css: "src/doctor/css-modules.ts",
|
|
35
|
+
sx: null,
|
|
36
|
+
styled: null,
|
|
37
|
+
runtime: null,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* O motivo decide o veredito, nunca a forma.
|
|
41
|
+
*
|
|
42
|
+
* `css` aparece nos dois lados: uma declaração de module de componente admitido é LIDA, a mesma
|
|
43
|
+
* declaração num `.css` global não tem leitor, e a de um componente que o portão não admite está
|
|
44
|
+
* lida e sem receita que a reivindique. Três destinos, uma forma - por isso o veredito lê o motivo.
|
|
45
|
+
*/
|
|
46
|
+
function verdictOf(reason) {
|
|
47
|
+
switch (reason) {
|
|
48
|
+
case "shape-not-read":
|
|
49
|
+
return "no-reader";
|
|
50
|
+
case "value-not-read":
|
|
51
|
+
return "value-gap";
|
|
52
|
+
default:
|
|
53
|
+
// `computed` e `component-not-admitted`: nenhum leitor muda o resultado.
|
|
54
|
+
return "by-design";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Onde o trabalho mora, para cada veredito - ou por que não existe trabalho. */
|
|
58
|
+
function whereOf(shape, reason) {
|
|
59
|
+
switch (verdictOf(reason)) {
|
|
60
|
+
case "no-reader": {
|
|
61
|
+
/**
|
|
62
|
+
* A REGRA DE CLASSE NUM CSS GLOBAL é a lacuna de leitura mais barata que existe hoje no
|
|
63
|
+
* repo do dono - 1 520 declarações em 135 arquivos - e ela não tem dono: `css-modules.ts` lê
|
|
64
|
+
* o module (escopo garantido pelo bundler), e um global vale para toda a página.
|
|
65
|
+
*/
|
|
66
|
+
const owner = shape === "css" ? null : OWNER[shape];
|
|
67
|
+
return owner
|
|
68
|
+
? {
|
|
69
|
+
module: owner,
|
|
70
|
+
work: `a reader exists and does not cover this shape: ${owner}`,
|
|
71
|
+
}
|
|
72
|
+
: {
|
|
73
|
+
module: null,
|
|
74
|
+
work: "no module owns this shape - a reader here is a new module, and it needs an app in the corpus before it exists",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "value-gap":
|
|
78
|
+
return {
|
|
79
|
+
module: "src/doctor/transcribe.ts",
|
|
80
|
+
work: "the shape is already read; what is missing is a SCALE that recognises this value - a table, not a new reader",
|
|
81
|
+
};
|
|
82
|
+
default:
|
|
83
|
+
return {
|
|
84
|
+
module: null,
|
|
85
|
+
work: reason === "component-not-admitted"
|
|
86
|
+
? "the gate refused the component: these styles exist and no recipe claims them. Changing that means moving the GATE - a decision about the vitrine, not a reader to write"
|
|
87
|
+
: reason === "sheet-not-imported"
|
|
88
|
+
? /**
|
|
89
|
+
* NÃO É LACUNA NOSSA NEM DELES-QUE-A-GENTE-CONSERTA: é um arquivo que o build deles
|
|
90
|
+
* não compila. Cair na frase de runtime dizia que o valor é montado em tempo de
|
|
91
|
+
* execução, o que é falso e mandaria alguém procurar o que não existe.
|
|
92
|
+
*/
|
|
93
|
+
"a sass partial no stylesheet imports: their own build never compiles it, so it paints nothing today - here or in their app. Deleting it is their call, not a reader to write"
|
|
94
|
+
: "the value is assembled at render time and is not in the source at any level - there is nothing to read",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A triagem de um ledger, contra a versão que está triando.
|
|
100
|
+
*
|
|
101
|
+
* Pura: ninguém abre arquivo aqui. O ledger já carrega tudo - inclusive a versão que o mediu, que
|
|
102
|
+
* é o campo sem o qual esta função não teria como responder a primeira pergunta.
|
|
103
|
+
*/
|
|
104
|
+
export function triageLedger(ledger, triagedBy) {
|
|
105
|
+
const sum = (counts) => Object.values(counts).reduce((n, k) => n + k, 0);
|
|
106
|
+
const counted = sum(ledger.counted);
|
|
107
|
+
const interpreted = sum(ledger.interpreted);
|
|
108
|
+
const unread = counted - interpreted;
|
|
109
|
+
const totals = { "no-reader": 0, "value-gap": 0, "by-design": 0 };
|
|
110
|
+
const rank = {
|
|
111
|
+
"no-reader": 0,
|
|
112
|
+
"value-gap": 1,
|
|
113
|
+
"by-design": 2,
|
|
114
|
+
};
|
|
115
|
+
const gaps = ledger.unread.map((group) => {
|
|
116
|
+
const verdict = verdictOf(group.reason);
|
|
117
|
+
totals[verdict] += group.uses;
|
|
118
|
+
const { module, work } = whereOf(group.shape, group.reason);
|
|
119
|
+
return {
|
|
120
|
+
shape: group.shape,
|
|
121
|
+
reason: group.reason,
|
|
122
|
+
verdict,
|
|
123
|
+
uses: group.uses,
|
|
124
|
+
files: group.files,
|
|
125
|
+
share: unread > 0 ? Math.round((group.uses / unread) * 100) : 0,
|
|
126
|
+
module,
|
|
127
|
+
work,
|
|
128
|
+
because: group.because,
|
|
129
|
+
examples: group.examples,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
gaps.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.uses - a.uses);
|
|
133
|
+
return {
|
|
134
|
+
measuredBy: ledger.cli,
|
|
135
|
+
triagedBy,
|
|
136
|
+
stale: isOlder(ledger.cli, triagedBy),
|
|
137
|
+
counted,
|
|
138
|
+
interpreted,
|
|
139
|
+
percent: counted > 0 ? Math.round((interpreted / counted) * 100) : 0,
|
|
140
|
+
totals,
|
|
141
|
+
gaps,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** `0.16.133` é mais velha que `0.16.134`. Compara número por número, e o que não é número não
|
|
145
|
+
* desempata nada - uma pré-release não é motivo para dizer que a medição está velha. */
|
|
146
|
+
function isOlder(measured, current) {
|
|
147
|
+
const parts = (v) => v.split(".").map((p) => Number.parseInt(p, 10));
|
|
148
|
+
const a = parts(measured);
|
|
149
|
+
const b = parts(current);
|
|
150
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
151
|
+
const left = a[i];
|
|
152
|
+
const right = b[i];
|
|
153
|
+
if (!Number.isFinite(left) || !Number.isFinite(right))
|
|
154
|
+
return false;
|
|
155
|
+
if (left !== right)
|
|
156
|
+
return left < right;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A triagem em texto, na ordem em que uma pessoa decide.
|
|
162
|
+
*
|
|
163
|
+
* Lidera pelo que NÃO é trabalho quando ele domina, porque é o número que muda a conclusão: 78%
|
|
164
|
+
* do não interpretado ser o portão funcionando é a informação mais importante da tela, e ela vem
|
|
165
|
+
* antes de qualquer lista de tarefa.
|
|
166
|
+
*/
|
|
167
|
+
export function describeTriage(t) {
|
|
168
|
+
const unread = t.counted - t.interpreted;
|
|
169
|
+
const lines = [
|
|
170
|
+
`${t.counted} style fragments accounted for, ${t.interpreted} interpreted (${t.percent}%). The ${unread} below are sorted by what to do about them.`,
|
|
171
|
+
];
|
|
172
|
+
if (t.stale) {
|
|
173
|
+
lines.push("", `MEASURED BY CLI ${t.measuredBy}, TRIAGED BY ${t.triagedBy}. Re-run \`import\` before concluding anything from the numbers below - a gap here may already be closed, and the work already done.`);
|
|
174
|
+
}
|
|
175
|
+
const label = {
|
|
176
|
+
"no-reader": "NO READER FOR THIS SHAPE - this is work",
|
|
177
|
+
"value-gap": "THE SHAPE IS READ, THE VALUE IS NOT - a scale, not a reader",
|
|
178
|
+
"by-design": "NOT A READING GAP - nothing a reader would change, and no work here",
|
|
179
|
+
};
|
|
180
|
+
for (const verdict of ["no-reader", "value-gap", "by-design"]) {
|
|
181
|
+
const group = t.gaps.filter((g) => g.verdict === verdict);
|
|
182
|
+
if (group.length === 0)
|
|
183
|
+
continue;
|
|
184
|
+
const total = t.totals[verdict];
|
|
185
|
+
lines.push("", `${label[verdict]} - ${total} fragment${total === 1 ? "" : "s"}, ${unread > 0 ? Math.round((total / unread) * 100) : 0}% of what was not interpreted`);
|
|
186
|
+
for (const gap of group) {
|
|
187
|
+
lines.push(` ${gap.uses} ${gap.shape} in ${gap.files} file${gap.files === 1 ? "" : "s"} (${gap.share}%) - ${gap.work}`);
|
|
188
|
+
for (const example of gap.examples.slice(0, 2))
|
|
189
|
+
lines.push(` ${example.file}:${example.line} ${example.text.slice(0, 76)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
@@ -54,6 +54,7 @@ const BECAUSE = {
|
|
|
54
54
|
"value-not-read": "the shape is read and this value is not - a utility or property outside the scales this project declares",
|
|
55
55
|
computed: "the value is assembled at render time, so it is not in the source at any level and there is nothing to read",
|
|
56
56
|
"component-not-admitted": "read, and it belongs to a component this scope does not admit into the system - the styles exist and no recipe claims them",
|
|
57
|
+
"sheet-not-imported": "this is a sass partial and no stylesheet imports it, so your own build never compiles it - it paints nothing today, here or anywhere",
|
|
57
58
|
};
|
|
58
59
|
/**
|
|
59
60
|
* O ledger de uma varredura, a partir dos fragmentos vistos.
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { clean } from "./commands/clean.js";
|
|
|
7
7
|
import { component } from "./commands/component.js";
|
|
8
8
|
import { connect } from "./commands/connect.js";
|
|
9
9
|
import { doctor } from "./commands/doctor.js";
|
|
10
|
+
import { gaps } from "./commands/gaps.js";
|
|
10
11
|
import { generate } from "./commands/generate.js";
|
|
11
12
|
import { hook } from "./commands/hook.js";
|
|
12
13
|
import { runImport } from "./commands/import.js";
|
|
@@ -48,6 +49,9 @@ Usage - governance (deterministic, FREE):
|
|
|
48
49
|
synthesisui doctor [paths…] [--verbose] audit for DRIFT: every design value written by hand, the
|
|
49
50
|
token your system already has for it, coherence, and the
|
|
50
51
|
record over time
|
|
52
|
+
synthesisui gaps [--census <p>] what this pipeline did NOT read of your project, triaged:
|
|
53
|
+
a reader that is missing, a value outside your scales, or
|
|
54
|
+
something no reader would change
|
|
51
55
|
synthesisui request [component|token] the queue of what your agent needed and the system
|
|
52
56
|
refused to invent (--done <id> closes one)
|
|
53
57
|
synthesisui sync send the local record - checks, fixes, open requests -
|
|
@@ -247,6 +251,21 @@ async function main() {
|
|
|
247
251
|
laws: flags.laws === true,
|
|
248
252
|
});
|
|
249
253
|
break;
|
|
254
|
+
case "gaps":
|
|
255
|
+
/**
|
|
256
|
+
* O LEDGER JÁ ESTÁ NO CENSO - isto não mede nada, tria.
|
|
257
|
+
*
|
|
258
|
+
* Existe para que a triagem seja alcançável por uma pessoa, e não só pelo spec: quem rodou
|
|
259
|
+
* `import` uma vez tem o número em disco e nenhuma forma de perguntar "e o que eu faço com
|
|
260
|
+
* isso". O texto que sai daqui é exatamente o que ele me manda quando quer que eu escreva o
|
|
261
|
+
* leitor (dono, 05/08).
|
|
262
|
+
*/
|
|
263
|
+
await gaps({
|
|
264
|
+
dir,
|
|
265
|
+
census: typeof flags.census === "string" ? flags.census : undefined,
|
|
266
|
+
cli: CLI_VERSION,
|
|
267
|
+
});
|
|
268
|
+
break;
|
|
250
269
|
case "list":
|
|
251
270
|
await list({ registry });
|
|
252
271
|
break;
|
package/package.json
CHANGED