synthesisui 0.16.336 → 0.16.338

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.
@@ -5,7 +5,7 @@ import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
5
5
  import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
6
6
  import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
7
7
  import { declaredElsewhere } from "../declared-elsewhere.js";
8
- import { architectureRule, componentHome, describeArchitecture, describeChoice, detectArchitectures, homeLine, proposeNewHome, } from "../doctor/architecture.js";
8
+ import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, proposeNewHome, } from "../doctor/architecture.js";
9
9
  import { findBrokenRefs } from "../doctor/broken-refs.js";
10
10
  import { nestingRules, propRules, readDefinitionProps, readNesting, readRuntime, } from "../doctor/call-sites.js";
11
11
  import { asCatalogueTable, describeFallback, fetchCatalogue, } from "../doctor/catalogue-fetch.js";
@@ -388,6 +388,21 @@ const MAX_GLOBAL_CLASSES = 300;
388
388
  * dois com folga e continua sendo um teto de verdade, alinhado com `DISTINCT_CAP` do ledger.
389
389
  */
390
390
  const SKIPPED_CAP = 4096;
391
+ /**
392
+ * UM ARQUIVO QUE MOSTRA UM COMPONENTE NÃO É UM COMPONENTE - a mesma exclusão nos três lugares.
393
+ *
394
+ * Stories and tests compose components to SHOW them; counting those as the product's own
395
+ * composition is the same lie the colour census told before they were set aside.
396
+ *
397
+ * E ela vale igual para a CONTAGEM que a organização declara. Medido em 30/08 em
398
+ * `codelevel-monorepo/packages/ui`: 75 arquivos `.tsx`, dos quais 38 são `*.stories.tsx` e 37 são
399
+ * componente. Dizer "we read 75 component files" a quem tem 37 é metade do número inventada por
400
+ * nós - e a frase existe justamente para o caso em que o cliente vai conferir o número na mão.
401
+ *
402
+ * UMA constante, e não três literais: a terceira cópia já existia antes desta, e uma delas
403
+ * envelhecer sozinha significa dois denominadores diferentes para a mesma palavra na mesma saída.
404
+ */
405
+ const SHOWS_A_COMPONENT = /(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/;
391
406
  export async function takeCensus(root, opts) {
392
407
  /**
393
408
  * O RELATÓRIO SÓ SAI QUANDO ALGUÉM O PEDIU - `say` cala quando o chamador é uma RE-medição.
@@ -552,6 +567,8 @@ export async function takeCensus(root, opts) {
552
567
  * naming how the project is organised costs no second pass. See `architecture.ts`.
553
568
  */
554
569
  const dirs = new Map();
570
+ /** Uma entrada nova da árvore, com os três contadores em zero. */
571
+ const freshDir = () => ({ dirs: new Set(), files: 0, direct: 0 });
555
572
  /** Parent → child → the files that put one inside the other. */
556
573
  const nesting = new Map();
557
574
  /**
@@ -621,17 +638,37 @@ export async function takeCensus(root, opts) {
621
638
  continue;
622
639
  const rel = relative(root, file);
623
640
  progress.step(rel);
624
- if (/\.(tsx|jsx|vue|svelte)$/i.test(rel)) {
641
+ /**
642
+ * A ORGANIZAÇÃO SÓ CONTA COMPONENTE - `SHOWS_A_COMPONENT`, a mesma exclusão de baixo.
643
+ *
644
+ * Este bloco roda onze linhas ANTES do filtro, e por isso contava story e teste como código
645
+ * do produto: 75 arquivos onde o cliente tem 37 (`codelevel-monorepo/packages/ui`, 30/08). O
646
+ * número que a organização declara é o que ele confere na mão, então ele conta o que ele
647
+ * conta quando confere.
648
+ */
649
+ if (/\.(tsx|jsx|vue|svelte)$/i.test(rel) && !SHOWS_A_COMPONENT.test(rel)) {
625
650
  const parts = rel.split("/");
626
651
  // Every ancestor learns the name of the child directly under it, which is all
627
652
  // the shape detector needs and is one pass rather than a tree walk.
628
653
  for (let i = 0; i < parts.length - 1; i++) {
629
654
  const at = parts.slice(0, i).join("/");
630
- const entry = dirs.get(at) ?? { dirs: new Set(), files: 0 };
655
+ const entry = dirs.get(at) ?? freshDir();
631
656
  entry.dirs.add(parts[i]);
632
657
  entry.files += 1;
633
658
  dirs.set(at, entry);
634
659
  }
660
+ /**
661
+ * A PASTA-MÃE, CREDITADA PELO ARQUIVO QUE ELA SEGURA - ver `DirEntry.direct`.
662
+ *
663
+ * O laço acima nunca roda para um arquivo na RAIZ do escopo (`parts.length - 1 === 0`), e
664
+ * era isso que fazia um projeto de quatro componentes soltos - uma forma real de biblioteca
665
+ * pequena - não produzir entrada nenhuma e ficar em silêncio absoluto sobre a própria
666
+ * organização. Aqui a raiz é uma pasta como as outras.
667
+ */
668
+ const home = parts.slice(0, -1).join("/");
669
+ const entry = dirs.get(home) ?? freshDir();
670
+ entry.direct += 1;
671
+ dirs.set(home, entry);
635
672
  }
636
673
  sources.push({ file: rel, source: src });
637
674
  readSignalsInto(signals, importTally, rel, src);
@@ -639,7 +676,7 @@ export async function takeCensus(root, opts) {
639
676
  // Stories and tests compose components to SHOW them; counting those as the
640
677
  // product's own composition is the same lie the colour census told before
641
678
  // they were set aside.
642
- if (!/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
679
+ if (!SHOWS_A_COMPONENT.test(rel)) {
643
680
  scanComponentsInto(tally, rel, src, internal);
644
681
  /**
645
682
  * The other half: what this file EXPORTS, with the axes its types declare. A
@@ -1282,11 +1319,24 @@ export async function takeCensus(root, opts) {
1282
1319
  }
1283
1320
  }
1284
1321
  progress.done();
1285
- const architectures = detectArchitectures([...dirs.entries()].map(([path, v]) => ({
1322
+ /** A ÁRVORE MEDIDA, uma vez - as três leituras abaixo saem todas dela. */
1323
+ const dirEntries = [...dirs.entries()].map(([path, v]) => ({
1286
1324
  path: path || ".",
1287
1325
  dirs: [...v.dirs],
1288
1326
  files: v.files,
1289
- })));
1327
+ direct: v.direct,
1328
+ }));
1329
+ const architectures = detectArchitectures(dirEntries);
1330
+ /**
1331
+ * A LACUNA, quando nada foi reconhecido - ver `architectureGap` (`INV-COB-08`). Nenhuma
1332
+ * decisão aqui: este arquivo IMPRIME e GRAVA o que aquela camada decidiu.
1333
+ */
1334
+ const gap = architectureGap(dirEntries, architectures);
1335
+ /**
1336
+ * ONDE O PRÓXIMO COMPONENTE VAI - derivado do escopo que ele apontou, e por isso a
1337
+ * pergunta sobrevive a não reconhecermos organização nenhuma.
1338
+ */
1339
+ const newHome = proposeNewHome(dirEntries, scopeLabel);
1290
1340
  const d = diagnose(reports);
1291
1341
  const allComposed = tallyToInventory(tally, 400);
1292
1342
  /**
@@ -1419,7 +1469,7 @@ export async function takeCensus(root, opts) {
1419
1469
  }
1420
1470
  edges.set(file, edgesIn(src));
1421
1471
  const rel = join(u.label, relative(u.path, file));
1422
- if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
1472
+ if (SHOWS_A_COMPONENT.test(rel)) {
1423
1473
  continue;
1424
1474
  }
1425
1475
  usageSeen.add(rel);
@@ -2007,6 +2057,16 @@ export async function takeCensus(root, opts) {
2007
2057
  say(body(paint.faint(` ${describeArchitecture(a)}`)));
2008
2058
  }
2009
2059
  }
2060
+ else if (gap) {
2061
+ /**
2062
+ * A MESMA SEÇÃO, NO MESMO LUGAR DA SAÍDA - `INV-COB-08`. Uma leitura que rodou e não
2063
+ * reconheceu nada tinha esta seção inteira desaparecendo, e a ausência lê como "não
2064
+ * medimos nada aqui" em vez de "medimos, e a régua é nossa".
2065
+ */
2066
+ say("");
2067
+ say(section("How this project is organised"));
2068
+ say(body(describeGap(gap, scopeLabel)));
2069
+ }
2010
2070
  if (skips.length > 0) {
2011
2071
  say("");
2012
2072
  say(body(describeGate(scoped.length, skips.length)));
@@ -2492,13 +2552,24 @@ export async function takeCensus(root, opts) {
2492
2552
  line: homeLine(a),
2493
2553
  rule: architectureRule(a, scopeLabel),
2494
2554
  })),
2495
- ...(proposeNewHome(architectures, scopeLabel)
2496
- ? {
2497
- newComponentHome: proposeNewHome(architectures, scopeLabel),
2498
- }
2499
- : {}),
2500
2555
  }
2501
2556
  : {}),
2557
+ /**
2558
+ * A LEITURA QUE RODOU E NÃO RECONHECEU NADA - `INV-COB-08`, ver `architectureGap`.
2559
+ *
2560
+ * Só os DOIS números medidos. Sem nome de forma, sem regra e sem caminho: nada foi
2561
+ * reconhecido, então qualquer um dos três seria adivinhação nossa gravada como fato dele.
2562
+ */
2563
+ ...(gap ? { architectureGap: gap } : {}),
2564
+ /**
2565
+ * FORA DO `if` DAS FORMAS, e é isso que mantém a pergunta viva - `INV-COB-08`.
2566
+ *
2567
+ * "Daqui em diante vai num lugar novo" é derivada do escopo que ele apontou e não depende
2568
+ * de reconhecermos organização nenhuma. Presa dentro do bloco acima, ela sumia junto com o
2569
+ * resto justamente para quem mais precisa dela: o projeto sobre o qual não temos nada a
2570
+ * dizer é o único cujo agente não recebe regra de onde criar componente.
2571
+ */
2572
+ ...(newHome ? { newComponentHome: newHome } : {}),
2502
2573
  ...(skips.length > 0
2503
2574
  ? {
2504
2575
  /**
@@ -52,12 +52,98 @@ const SHAPES = [
52
52
  describe: (a) => `Every component sits directly under \`${a.root}\`, one folder deep. It is the shape that stays legible longest while a system is small, and the one to revisit when the folder passes about forty names.`,
53
53
  },
54
54
  ];
55
+ /**
56
+ * A MESMA ORGANIZAÇÃO VISTA DE MAIS PERTO NÃO É UMA SEGUNDA ESCOLHA (`INV-COB-09`).
57
+ *
58
+ * O QUE O CLIENTE GANHA: a pergunta "qual organização tem PRIORIDADE" só lhe oferece
59
+ * organizações que disputam de verdade. Sem isto ele escolhe entre uma escada atômica e uma
60
+ * pasta que mora DENTRO dela - a mesma organização, medida de duas distâncias -, e qualquer
61
+ * resposta que ele der é a mesma resposta. Uma pergunta cujas opções não se excluem não é uma
62
+ * pergunta: é um formulário que ele preenche sem que nada mude no que os agentes dele escrevem.
63
+ *
64
+ * A PROPRIEDADE: uma organização é reconhecida no lugar mais RASO que a explica. Uma forma é a
65
+ * mesma organização vista de mais perto quando é do MESMO tipo de uma que já a cobre, ou quando é
66
+ * a leitura genérica (`flat`) de um lugar que outra forma já nomeia com mais informação. Formas de
67
+ * tipos DIFERENTES descrevem fatos diferentes, e nenhuma apaga a outra - o peso (`MINOR_SHARE`) já
68
+ * decide qual delas pede decisão e qual é dita como canto.
69
+ *
70
+ * O DESEMPATE NO MESMO ROOT continua sendo só o de `flat`, e ele não muda aqui: duas formas
71
+ * específicas na mesma pasta (`layered` e `colocated` em `src`, por exemplo) são duas leituras
72
+ * legítimas do mesmo lugar, e escolher entre elas é justamente a decisão que a pergunta existe
73
+ * para pedir. `flat` é a forma mais permissiva das cinco - basta uma pasta `components` -, então
74
+ * onde uma específica também casa, ela é a mesma coisa dita com menos informação.
75
+ *
76
+ * MEDIÇÃO de 31/08 - população e régua de enumeração de escopo escritas UMA vez em
77
+ * `contracts/camada-4-cobertura.md` (`INV-COB-09`), e não recopiadas aqui: 65 formas reconhecidas
78
+ * em 43 escopos passam a 56; as 9 subordinadas dizem o mesmo fato de um ancestral, e estão em 6
79
+ * escopos. Foto daquelas populações naquele dia, nunca invariante.
80
+ */
81
+ export function dropShapesAlreadyExplained(found) {
82
+ return found.filter((a) => !found.some((b) => b !== a && explains(b, a)));
83
+ }
84
+ /**
85
+ * `other` é a MESMA organização que `shape`, vista de mais longe?
86
+ *
87
+ * A pergunta não é "está por perto": é se as duas dizem o mesmo fato. Uma forma é a mesma
88
+ * organização vista de mais perto quando é do MESMO tipo de uma que já a cobre, ou quando é a
89
+ * leitura genérica (`flat`) de um lugar que outra forma já nomeia com mais informação.
90
+ *
91
+ * FORMAS DE TIPOS DIFERENTES DESCREVEM FATOS DIFERENTES, e nenhuma apaga a outra - nem quando uma
92
+ * mora dentro da outra. O layout React mais comum que existe é a prova: `src/{components}` com
93
+ * `src/components/{atoms, molecules, organisms}` embaixo. Com o ancestral vencendo sempre, a
94
+ * escada atômica SOME, e o que sobra é a leitura mais pobre das duas - o cliente recebe no
95
+ * `CLAUDE.md` dele a regra "Every component sits directly under `src`, one folder deep", que é
96
+ * FALSA no repositório dele, e o caminho `src/{components}`, que manda o agente parar um degrau
97
+ * antes de onde o componente nasce. Uma opção errada é pior que duas opções: ele nem sabe que
98
+ * havia o que decidir.
99
+ *
100
+ * Quem decide qual das duas pede decisão e qual é dita como canto é o PESO (`MINOR_SHARE`), e não
101
+ * este predicado - uma escada de 6 arquivos dentro de um `colocated` de 780 continua sendo dita,
102
+ * e continua não virando pergunta.
103
+ */
104
+ function explains(other, shape) {
105
+ if (isUnderPath(other.root, shape.root))
106
+ return other.kind === shape.kind || shape.kind === "flat";
107
+ return (pathKey(other.root) === pathKey(shape.root) &&
108
+ shape.kind === "flat" &&
109
+ other.kind !== "flat");
110
+ }
111
+ /**
112
+ * O CAMINHO COMPARÁVEL - a raiz do escopo é escrita `.` pela caminhada, e comparada com `""` ela é
113
+ * a mesma pasta. Comparar as strings cruas faria a raiz deixar de explicar o que está embaixo
114
+ * dela, que é o caso mais comum de todos.
115
+ *
116
+ * AS DUAS OUTRAS NORMALIZAÇÕES SÃO DEFENSIVAS, e ficam nomeadas como tal: barra final e `./` na
117
+ * frente não são alcançáveis pela caminhada real - `import.ts` escreve `path || "."` e monta o
118
+ * resto com `parts.slice(…).join("/")` -, então nenhum teste pode reprová-las por comportamento.
119
+ * Elas custam duas chamadas de `replace` e protegem quem montar a árvore à mão.
120
+ */
121
+ function pathKey(path) {
122
+ const tidy = path.replace(/\/+$/, "").replace(/^\.\//, "");
123
+ return tidy === "." ? "" : tidy;
124
+ }
125
+ /**
126
+ * `path` está DENTRO de `ancestor` - por SEGMENTO, nunca por prefixo de texto.
127
+ *
128
+ * `"src/ui-kit".startsWith("src/ui")` é verdadeiro e está errado: são duas pastas irmãs, e por
129
+ * essa comparação uma organização real desapareceria da pergunta sem que nada a explicasse.
130
+ */
131
+ function isUnderPath(ancestor, path) {
132
+ const a = pathKey(ancestor);
133
+ const p = pathKey(path);
134
+ /**
135
+ * A MESMA PASTA JÁ CAI FORA POR CONSTRUÇÃO, e por isso não há guarda para ela: com `a === p`,
136
+ * `p.startsWith(`${a}/`)` é falso (uma pasta não é filha de si mesma) e a raiz cai no `p !== ""`.
137
+ * Uma guarda a mais aqui seria linha que nenhum caso alcança - e código que nenhum caso alcança
138
+ * é código que nenhum teste pode reprovar.
139
+ */
140
+ return a === "" ? p !== "" : p.startsWith(`${a}/`);
141
+ }
55
142
  /**
56
143
  * Which shapes this tree actually has, ranked by how much code each holds.
57
144
  *
58
- * `flat` is checked last and only reported when nothing more specific matched under the
59
- * same root: a `components/` folder inside an atomic library is not a second
60
- * architecture, it is the same one seen from further away.
145
+ * O que sobra é o que governa código que nenhuma outra forma já governa - a subordinação inteira
146
+ * mora em `dropShapesAlreadyExplained` (`INV-COB-09`), numa redação só.
61
147
  */
62
148
  export function detectArchitectures(entries) {
63
149
  const found = [];
@@ -76,10 +162,6 @@ export function detectArchitectures(entries) {
76
162
  }
77
163
  if (!complete)
78
164
  continue;
79
- if (shape.kind === "flat" &&
80
- found.some((f) => f.root === entry.path && f.kind !== "flat")) {
81
- continue;
82
- }
83
165
  found.push({
84
166
  kind: shape.kind,
85
167
  root: entry.path,
@@ -88,10 +170,7 @@ export function detectArchitectures(entries) {
88
170
  });
89
171
  }
90
172
  }
91
- return found
92
- .filter((a) => a.kind !== "flat" ||
93
- !found.some((b) => b.root === a.root && b.kind !== "flat"))
94
- .sort((a, b) => b.files - a.files || a.root.localeCompare(b.root));
173
+ return dropShapesAlreadyExplained(found).sort((a, b) => b.files - a.files || a.root.localeCompare(b.root));
95
174
  }
96
175
  /** The sentence that becomes the rule. Written for an agent about to add a file. */
97
176
  export function describeArchitecture(a) {
@@ -165,13 +244,26 @@ export function homeLine(a) {
165
244
  * ela a pergunta é sobre o passado.
166
245
  *
167
246
  * O caminho é DERIVADO, não inventado: fica dentro do escopo que a pessoa já apontou como
168
- * fonte de verdade, e usa `src/` só quando as formas medidas mostram que o escopo tem `src`.
247
+ * fonte de verdade, e usa `src/` só quando a árvore medida mostra que o escopo tem `src`.
169
248
  * Sem nome de sistema dentro dele de propósito - quem quer isso digita, e digitar um nome
170
249
  * é mais honesto do que a gente adivinhar a grafia dele.
171
250
  */
172
- export function proposeNewHome(found, scope) {
251
+ export function proposeNewHome(
252
+ /**
253
+ * A ÁRVORE MEDIDA, e não a lista do que foi reconhecido - a porta fechada.
254
+ *
255
+ * `hasSrc` era lido da lista de formas, e com ela VAZIA - que é exatamente o caso em que
256
+ * nenhuma organização conhecida apareceu - a resposta era sempre "não tem `src`": num
257
+ * projeto cujos 75 arquivos moram todos sob `src/`, a proposta mandava o cliente para uma
258
+ * pasta na raiz. Duas entradas (a árvore e as formas) podem discordar; uma não pode, e as
259
+ * formas saem da árvore aqui dentro.
260
+ */
261
+ entries, scope) {
262
+ const found = detectArchitectures(entries);
173
263
  const base = scope ? scope.replace(/\/+$/, "") : "";
174
- const hasSrc = found.some((a) => a.root === "src" || a.root.startsWith("src/"));
264
+ const hasSrc = entries.some((e) => e.path === "src" ||
265
+ e.path.startsWith("src/") ||
266
+ (isRoot(e.path) && e.dirs.includes("src")));
175
267
  const trunk = `${base ? `${base}/` : ""}${hasSrc ? "src/components" : "components"}`;
176
268
  const fresh = `${trunk}/{atoms, molecules, organisms}`;
177
269
  /** Se a proposta é o que já existe, ela não é uma pasta nova - e oferecê-la seria repetir a opção 1. */
@@ -233,3 +325,90 @@ export function describeChoice(found) {
233
325
  const list = options.map(say).join(", ");
234
326
  return `${options.length} shapes govern real code, and none of them is a mistake: ${list}.${notes.length > 0 ? ` Smaller corners, reported and not asked about: ${notes.map(say).join(", ")}.` : ""} Pick which one has PRIORITY - it becomes the rule every prompt reads, and the others stay true without deciding.`;
235
327
  }
328
+ /** A raiz do escopo, que a caminhada escreve como `.` e o mapa guarda como "". */
329
+ function isRoot(path) {
330
+ return path === "." || path === "";
331
+ }
332
+ /**
333
+ * A ORGANIZAÇÃO QUE NINGUÉM SOUBE NOMEAR - declarada, nunca calada (`INV-COB-08`).
334
+ *
335
+ * O QUE O CLIENTE GANHA: quando a leitura roda e nenhuma das organizações conhecidas
336
+ * aparece, ele continua recebendo uma resposta - quantos arquivos de componente foram
337
+ * medidos, e em quantas pastas eles estão - com a frase dizendo que o limite é a nossa
338
+ * régua e não o projeto dele. Sem isto a seção inteira sobre organização SOME da saída, e
339
+ * silêncio é indistinguível de "não medimos nada aqui": ele fica sem saber que a régua
340
+ * rodou, sem o número que ela mediu, e sem a chance de dizer onde o próximo componente vai.
341
+ *
342
+ * MEDIÇÃO de 30/08 - a população e a régua de enumeração de escopo estão escritas UMA vez, em
343
+ * `contracts/camada-4-cobertura.md` (`INV-COB-08`), e não são recopiadas aqui: 6 de 43 escopos
344
+ * com componentes (14%) não casam nenhuma organização conhecida, e o maior deles tem 37
345
+ * arquivos. No banco de produção, na mesma data, há 1 sistema com censo de import guardado, e
346
+ * ele é um dos mudos - 1 de 1. Foto daquelas populações naquele dia, nunca invariante.
347
+ *
348
+ * O QUE ESTE BLOCO NÃO FAZ, de propósito: reconhecer uma organização nova. Uma sexta forma é
349
+ * outra entrega. Aqui a ausência de reconhecimento vira DECLARAÇÃO, nunca um palpite.
350
+ */
351
+ /**
352
+ * ABAIXO DISTO NÃO HÁ ORGANIZAÇÃO A DECLARAR - há conteúdo.
353
+ *
354
+ * MEDIÇÃO de 30/08 - população e régua de enumeração de escopo em
355
+ * `contracts/camada-4-cobertura.md` (`INV-COB-08`): dos 6 escopos mudos, 4 têm um ou dois
356
+ * arquivos de componente ao todo. Um ou dois arquivos não organizam nada, e dizer "não sei
357
+ * nomear a sua organização" sobre eles é ruído com cara de lacuna. Os 2 que sobram são os que a
358
+ * frase existe para alcançar - 37 arquivos em 37 pastas, e 7 arquivos em 1 pasta. O 3 é lido
359
+ * dessa foto, e não é regra do produto.
360
+ *
361
+ * O piso é ancorado por LITERAIS no spec (2 cala, 3 fala) e não por esta constante: um teste
362
+ * escrito como `GAP_MIN_FILES - 1` acompanha a constante para onde ela for, e com ela em zero o
363
+ * produto imprimiria "We read 0 component files in 0 folders" - o zero pelado da lei 14 - com a
364
+ * suíte inteira verde.
365
+ */
366
+ export const GAP_MIN_FILES = 3;
367
+ /**
368
+ * A LEITURA QUE RODOU E NÃO RECONHECEU NADA - o estado `unknown` desta leitura.
369
+ *
370
+ * Devolve `null` quando há forma reconhecida (aí quem fala é `describeChoice`) e quando o
371
+ * escopo é pequeno demais para ter organização (`GAP_MIN_FILES`).
372
+ */
373
+ export function architectureGap(entries, found) {
374
+ if (found.length > 0)
375
+ return null;
376
+ /**
377
+ * SOMAR `direct`, e nunca `files`: a caminhada credita cada arquivo a TODOS os ancestrais,
378
+ * então somar `files` conta o mesmo arquivo uma vez por nível. `direct` é o arquivo na pasta
379
+ * onde ele está, uma vez só - a mesma conta que o `find` acima faz na máquina dele.
380
+ */
381
+ let files = 0;
382
+ let folders = 0;
383
+ for (const e of entries) {
384
+ if (e.direct <= 0)
385
+ continue;
386
+ files += e.direct;
387
+ folders += 1;
388
+ }
389
+ if (files < GAP_MIN_FILES)
390
+ return null;
391
+ return { files, folders };
392
+ }
393
+ /**
394
+ * A FRASE, no tom das outras desta camada: o número medido, o motivo, e nenhuma acusação.
395
+ *
396
+ * A ordem é a da dúvida de quem lê - o que medimos, de quem é o limite, e o que isso custa
397
+ * a ele hoje. A segunda parte não é gentileza: sem ela a frase lê como um defeito do
398
+ * repositório dele, e o defeito é da régua.
399
+ */
400
+ export function describeGap(g,
401
+ /**
402
+ * ONDE MEDIMOS, quando a leitura foi apontada para uma parte do repositório.
403
+ *
404
+ * "here" não resolve para nada na cabeça de quem rodou `--scope packages/ui` da raiz de um
405
+ * monorepo, e num import de dois lugares a mesma seção sai duas vezes falando de pastas
406
+ * diferentes. Sem rótulo a redação continua a mesma: uma leitura da raiz não tem outro
407
+ * lugar a que se referir.
408
+ */
409
+ where) {
410
+ const files = `${g.files} component file${g.files === 1 ? "" : "s"}`;
411
+ const folders = `${g.folders} folder${g.folders === 1 ? "" : "s"}`;
412
+ const at = where ? `under \`${where}\`` : "here";
413
+ return `We read ${files} in ${folders} ${at}, and none of the ways of organising code this version can name fits them. That does not mean your project has no structure; it means ours does not have a name for yours yet. Nothing about where code lives was written into your rules, so an agent reading your \`CLAUDE.md\` guesses where a new component goes until you tell it.`;
414
+ }
@@ -375,7 +375,47 @@ export const CHECKER_SINCE = "0.16.308";
375
375
  * nunca era alcançado. Medido: 1134 componentes, 77 mudaram, e a receita que o cliente recebe muda
376
376
  * com eles. Uma medição anterior não produz o conserto, então a marca sobe.
377
377
  */
378
- export const READER_SINCE = "0.16.336";
378
+ /**
379
+ * 0.16.336 -> 0.16.337 em 30/08 (INV-COB-08): a contagem de arquivos por organização deixou de
380
+ * incluir story, spec e teste. Um censo já gravado carrega o número inflado - `atomic@src/lib/SignalUI`
381
+ * dizia 69 arquivos onde há 38 - e esse número viaja para o CLAUDE.md do cliente como a EVIDÊNCIA da
382
+ * regra (`architectureRule().evidence`, "holding N component files"). Não há como corrigi-lo a partir
383
+ * do censo: saber quais daqueles arquivos eram story exige reler o disco, e só a remedição lê. Por
384
+ * isso a marca sobe.
385
+ *
386
+ * MEDIÇÃO de 30/08 - população e régua de enumeração de escopo em
387
+ * `contracts/camada-4-cobertura.md` (`INV-COB-08`): `frontend-hub/packages/ui` sai de 69 para 38
388
+ * (mesma forma, mesmo lugar); `codelevel/packages/ui` sai de 75 para 37. O ranking não mudou em
389
+ * nenhum dos 43 escopos medidos, então ninguém recebe uma organização DIFERENTE - só o número
390
+ * honesto.
391
+ */
392
+ /**
393
+ * 0.16.337 -> 0.16.338 em 31/08 (`INV-COB-09`): uma organização passou a ser reconhecida no lugar
394
+ * mais RASO que a explica. Uma forma do MESMO tipo de outra que já a cobre, ou a leitura genérica
395
+ * de um lugar que outra já nomeia com mais informação, deixa de ser gravada como uma segunda
396
+ * organização. Tipos DIFERENTES continuam os dois gravados: eles dizem fatos diferentes sobre o
397
+ * mesmo código, e apagar o de dentro entregaria a leitura mais pobre como se fosse a única.
398
+ *
399
+ * A marca sobe porque um censo já gravado carrega as entradas duplicadas e não há como saber quais
400
+ * eram subordinadas sem reler a árvore do disco: a subordinação se decide comparando os CAMINHOS
401
+ * medidos, e um censo antigo guarda o que foi reconhecido, não a caminhada que o reconheceu. Só a
402
+ * remedição lê o disco.
403
+ *
404
+ * O QUE O `sync` ENTREGA, e o que ele NÃO entrega. Ele remede com `takeCensus`, então o campo
405
+ * `architectures` do censo é reescrito sem as entradas subordinadas e o censo reenviado já chega
406
+ * limpo. Ele NÃO reimprime a seção de organização no terminal - essa seção é do relatório de
407
+ * `import` -, e NÃO reescreve a regra no `CLAUDE.md` dele: aquela linha nasce da conversa de
408
+ * import, que lê `architectures[].rule` do censo, e `rulesFromCensus` não olha para este campo.
409
+ * Quem já tem a regra escrita a partir de uma organização subordinada continua com ela até um
410
+ * import novo, e dizer o contrário seria prometer que um comando conserta o que ele não conserta.
411
+ *
412
+ * MEDIÇÃO de 31/08 - população e régua de enumeração de escopo em
413
+ * `contracts/camada-4-cobertura.md` (`INV-COB-09`): 65 formas reconhecidas em 43 escopos passam a
414
+ * 56; as 9 subordinadas dizem o mesmo fato de um ancestral, e estão em 6 escopos. O hash do corpus
415
+ * NÃO se moveu, e isso é a informação: nenhum dos 28 apps dourados tem forma repetindo o que uma
416
+ * ancestral já diz, então a estabilidade ali prova ausência de regressão e não ausência de efeito.
417
+ */
418
+ export const READER_SINCE = "0.16.338";
379
419
  /**
380
420
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
381
421
  *
@@ -119,6 +119,32 @@ export function mergeCensus(list) {
119
119
  let brokenRefs = first.brokenRefs ?? undefined;
120
120
  let conventions = first.conventions ?? undefined;
121
121
  let architectures = first.architectures ?? undefined;
122
+ /**
123
+ * A LACUNA DE ORGANIZAÇÃO, SOMADA - `INV-COB-08`, ver `architectureGap` em `import.ts`.
124
+ *
125
+ * A declaração de qualquer escopo SOBREVIVE à fusão. Ela diz "medimos aqui e não soubemos
126
+ * nomear"; deixá-la de fora porque o outro escopo foi reconhecido devolve o silêncio que a
127
+ * invariante existe para eliminar, um passo adiante - e o cliente que rodou com dois escopos
128
+ * é justamente quem tem mais lugar onde a resposta pode sumir.
129
+ *
130
+ * SOMADOS, e não o primeiro que aparece, porque os dois são CONTAGENS e contagem é aditiva:
131
+ * o total descreve exatamente as partes que ficaram sem nome, e nenhum arquivo contado aqui
132
+ * pertence a um escopo reconhecido. É o oposto do caso da cobertura logo abaixo, onde somar
133
+ * percentuais produziria um número que não descreve medição nenhuma.
134
+ */
135
+ let gapFiles = first.architectureGap
136
+ ?.files;
137
+ let gapFolders = first.architectureGap
138
+ ?.folders;
139
+ /**
140
+ * ONDE O PRÓXIMO COMPONENTE VAI - o PRIMEIRO escopo vence, a mesma regra de `scope`.
141
+ *
142
+ * É um caminho, não uma contagem: dois caminhos não somam, e escolher o do escopo seguinte
143
+ * mandaria o agente para um lugar fora do escopo que o censo declara como o dele.
144
+ */
145
+ const newComponentHome = list
146
+ .map((c) => c.newComponentHome)
147
+ .find((h) => typeof h === "string" && h.length > 0);
122
148
  let components = first.components ?? undefined;
123
149
  let defined = first.defined ?? undefined;
124
150
  const stack = [...(first.project.stack ?? [])];
@@ -177,6 +203,11 @@ export function mergeCensus(list) {
177
203
  brokenRefs = concat(brokenRefs, c.brokenRefs);
178
204
  conventions = concat(conventions, c.conventions);
179
205
  architectures = concat(architectures, c.architectures);
206
+ const gap = c.architectureGap;
207
+ if (gap) {
208
+ gapFiles = (gapFiles ?? 0) + (gap.files ?? 0);
209
+ gapFolders = (gapFolders ?? 0) + (gap.folders ?? 0);
210
+ }
180
211
  components = concat(components, c.components);
181
212
  defined = concat(defined, c.defined);
182
213
  for (const s of c.project.stack ?? [])
@@ -260,6 +291,10 @@ export function mergeCensus(list) {
260
291
  out.conventions = conventions;
261
292
  if (architectures && architectures.length > 0)
262
293
  out.architectures = architectures;
294
+ if (gapFiles !== undefined && gapFolders !== undefined)
295
+ out.architectureGap = { files: gapFiles, folders: gapFolders };
296
+ if (newComponentHome)
297
+ out.newComponentHome = newComponentHome;
263
298
  if (components && components.length > 0)
264
299
  out.components = components;
265
300
  if (defined && defined.length > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.336",
3
+ "version": "0.16.338",
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": {