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.
@@ -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
+ }
@@ -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
- let css = "";
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 && body)
149
- into.push({
150
- file: relative(root, file),
151
- fragments: fragmentsOfStylesheet(relative(root, file), body, /\.module\./.test(file)),
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
- const moduleCss = await readFile(cssPath, "utf8").catch(() => "");
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
- admittedStyles.add(relative(root, cssPath));
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
  *
@@ -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, scoped) {
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
- ...(scoped ? {} : { reason: "shape-not-read" }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.134",
3
+ "version": "0.16.136",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {