synthesisui 0.16.133 → 0.16.135

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
+ }
@@ -1509,6 +1509,20 @@ export async function takeCensus(root, opts) {
1509
1509
  skipped: skips
1510
1510
  .slice(0, 400)
1511
1511
  .map(({ name, why, because }) => ({ name, why, because })),
1512
+ /**
1513
+ * OS NÚMEROS VERDADEIROS DO PORTÃO, porque a lista acima é cortada em 400 e num
1514
+ * monorepo real o corte estoura (712 recusados, 400 gravados). Contar a lista daria
1515
+ * 400 e pareceria completo.
1516
+ */
1517
+ gate: {
1518
+ admitted: scoped.length,
1519
+ refused: skips.length,
1520
+ groups: groupSkips(skips).map((g) => ({
1521
+ why: g.why,
1522
+ label: g.label,
1523
+ count: g.names.length,
1524
+ })),
1525
+ },
1512
1526
  }
1513
1527
  : {}),
1514
1528
  ...((opts?.usage ?? []).length > 0
@@ -0,0 +1,186 @@
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
+ : "the value is assembled at render time and is not in the source at any level - there is nothing to read",
88
+ };
89
+ }
90
+ }
91
+ /**
92
+ * A triagem de um ledger, contra a versão que está triando.
93
+ *
94
+ * Pura: ninguém abre arquivo aqui. O ledger já carrega tudo - inclusive a versão que o mediu, que
95
+ * é o campo sem o qual esta função não teria como responder a primeira pergunta.
96
+ */
97
+ export function triageLedger(ledger, triagedBy) {
98
+ const sum = (counts) => Object.values(counts).reduce((n, k) => n + k, 0);
99
+ const counted = sum(ledger.counted);
100
+ const interpreted = sum(ledger.interpreted);
101
+ const unread = counted - interpreted;
102
+ const totals = { "no-reader": 0, "value-gap": 0, "by-design": 0 };
103
+ const rank = {
104
+ "no-reader": 0,
105
+ "value-gap": 1,
106
+ "by-design": 2,
107
+ };
108
+ const gaps = ledger.unread.map((group) => {
109
+ const verdict = verdictOf(group.reason);
110
+ totals[verdict] += group.uses;
111
+ const { module, work } = whereOf(group.shape, group.reason);
112
+ return {
113
+ shape: group.shape,
114
+ reason: group.reason,
115
+ verdict,
116
+ uses: group.uses,
117
+ files: group.files,
118
+ share: unread > 0 ? Math.round((group.uses / unread) * 100) : 0,
119
+ module,
120
+ work,
121
+ because: group.because,
122
+ examples: group.examples,
123
+ };
124
+ });
125
+ gaps.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.uses - a.uses);
126
+ return {
127
+ measuredBy: ledger.cli,
128
+ triagedBy,
129
+ stale: isOlder(ledger.cli, triagedBy),
130
+ counted,
131
+ interpreted,
132
+ percent: counted > 0 ? Math.round((interpreted / counted) * 100) : 0,
133
+ totals,
134
+ gaps,
135
+ };
136
+ }
137
+ /** `0.16.133` é mais velha que `0.16.134`. Compara número por número, e o que não é número não
138
+ * desempata nada - uma pré-release não é motivo para dizer que a medição está velha. */
139
+ function isOlder(measured, current) {
140
+ const parts = (v) => v.split(".").map((p) => Number.parseInt(p, 10));
141
+ const a = parts(measured);
142
+ const b = parts(current);
143
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
144
+ const left = a[i];
145
+ const right = b[i];
146
+ if (!Number.isFinite(left) || !Number.isFinite(right))
147
+ return false;
148
+ if (left !== right)
149
+ return left < right;
150
+ }
151
+ return false;
152
+ }
153
+ /**
154
+ * A triagem em texto, na ordem em que uma pessoa decide.
155
+ *
156
+ * Lidera pelo que NÃO é trabalho quando ele domina, porque é o número que muda a conclusão: 78%
157
+ * do não interpretado ser o portão funcionando é a informação mais importante da tela, e ela vem
158
+ * antes de qualquer lista de tarefa.
159
+ */
160
+ export function describeTriage(t) {
161
+ const unread = t.counted - t.interpreted;
162
+ const lines = [
163
+ `${t.counted} style fragments accounted for, ${t.interpreted} interpreted (${t.percent}%). The ${unread} below are sorted by what to do about them.`,
164
+ ];
165
+ if (t.stale) {
166
+ 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.`);
167
+ }
168
+ const label = {
169
+ "no-reader": "NO READER FOR THIS SHAPE - this is work",
170
+ "value-gap": "THE SHAPE IS READ, THE VALUE IS NOT - a scale, not a reader",
171
+ "by-design": "NOT A READING GAP - nothing a reader would change, and no work here",
172
+ };
173
+ for (const verdict of ["no-reader", "value-gap", "by-design"]) {
174
+ const group = t.gaps.filter((g) => g.verdict === verdict);
175
+ if (group.length === 0)
176
+ continue;
177
+ const total = t.totals[verdict];
178
+ lines.push("", `${label[verdict]} - ${total} fragment${total === 1 ? "" : "s"}, ${unread > 0 ? Math.round((total / unread) * 100) : 0}% of what was not interpreted`);
179
+ for (const gap of group) {
180
+ lines.push(` ${gap.uses} ${gap.shape} in ${gap.files} file${gap.files === 1 ? "" : "s"} (${gap.share}%) - ${gap.work}`);
181
+ for (const example of gap.examples.slice(0, 2))
182
+ lines.push(` ${example.file}:${example.line} ${example.text.slice(0, 76)}`);
183
+ }
184
+ }
185
+ return lines;
186
+ }
@@ -35,6 +35,20 @@ const SHAPES = [
35
35
  "styled",
36
36
  "runtime",
37
37
  ];
38
+ /**
39
+ * Como cada forma se chama no terminal. É cópia do RELATÓRIO, não do contrato: a tela da
40
+ * plataforma escreve a sua própria (o dado que viaja é a chave da forma), porque uma linha de
41
+ * terminal alinhada em coluna e um rótulo de painel não são o mesmo texto.
42
+ */
43
+ const SHAPE_LABEL = {
44
+ class: "class lists",
45
+ template: "template strings",
46
+ inline: "style={{ }}",
47
+ css: "stylesheet declarations",
48
+ sx: "sx props",
49
+ styled: "styled-components",
50
+ runtime: "classes built at runtime",
51
+ };
38
52
  const BECAUSE = {
39
53
  "shape-not-read": "no reader in this version understands this shape, so the styles it holds reach no recipe",
40
54
  "value-not-read": "the shape is read and this value is not - a utility or property outside the scales this project declares",
@@ -107,6 +121,19 @@ export function describeLedger(l) {
107
121
  const lines = [
108
122
  `${total} style fragments in your files, and every one is accounted for: ${read} interpreted (${Math.round((read / total) * 100)}%), ${total - read} listed below with the file and line where each lives.`,
109
123
  ];
124
+ /**
125
+ * POR FORMA, ANTES DOS GRUPOS. O total responde "quanto"; esta tabela responde "de que jeito o
126
+ * projeto escreve estilo", que é a pergunta que decide qual leitor vale construir. Sem ela, 56%
127
+ * num app dominado por CSS Modules e 56% num app de Tailwind leem igual e pedem trabalhos
128
+ * opostos.
129
+ */
130
+ for (const shape of SHAPES) {
131
+ const seen = l.counted[shape];
132
+ if (seen === 0)
133
+ continue;
134
+ const got = l.interpreted[shape];
135
+ lines.push(` ${SHAPE_LABEL[shape].padEnd(28)} ${String(seen).padStart(6)} seen ${String(got).padStart(6)} interpreted${got === seen ? "" : ` (${seen - got} listed below)`}`);
136
+ }
110
137
  for (const g of l.unread) {
111
138
  lines.push(` ${g.uses} ${g.shape}${g.uses === 1 ? "" : "s"} in ${g.files} file${g.files === 1 ? "" : "s"} - ${g.because}`);
112
139
  for (const e of g.examples)
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.133",
3
+ "version": "0.16.135",
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": {