synthesisui 0.16.338 → 0.16.343

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.
@@ -313,8 +313,14 @@ const REPAIR_SAID = {
313
313
  spacing: "spacing values in your system were connected to the steps your own scale declares",
314
314
  orphans: "recipes your measured scope no longer declares were removed",
315
315
  roles: "colour roles were pointed at the ramps your repository declares",
316
- reground: "the foundation was rebuilt from your census",
317
- interpretation: "your census was re-read with a newer interpretation",
316
+ /**
317
+ * `census` SAIU DESTAS DUAS - `INV-VOC-05`. As três irmãs acima já dizem a coisa pelo efeito no
318
+ * projeto dele ("the steps your own scale declares"), e estas duas diziam pelo nome da nossa
319
+ * peça. O texto do `align` entra no contexto do modelo no `SessionStart`, e o que o agente lê é
320
+ * o que ele repete para a pessoa.
321
+ */
322
+ reground: "the foundation was rebuilt from what we measured in your repository",
323
+ interpretation: "what we measured in your repository was read again, with a newer interpretation",
318
324
  };
319
325
  export function repairSaid(body, lock) {
320
326
  if (!lock.slug)
@@ -8,7 +8,7 @@ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, }
8
8
  import { checkContracts } from "../doctor/contract-check.js";
9
9
  import { describeMissing, missingDependencies, summarizeMissing, } from "../doctor/dependencies.js";
10
10
  import { findFrozenBindings } from "../doctor/frozen.js";
11
- import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, unrepresentedFrom, summarize, } from "../doctor/ledger.js";
11
+ import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, unrepresentedFrom, } from "../doctor/ledger.js";
12
12
  import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
13
13
  import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
14
14
  import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
@@ -156,6 +156,28 @@ export async function* walkAll(roots) {
156
156
  * doctor como se fosse um componente pedido. Uma tela que rotula errado é pior que uma que não
157
157
  * rotula: quem lê decide em cima do rótulo.
158
158
  */
159
+ /**
160
+ * O EMPATE DITO EM VOZ ALTA - a metade que faltava do nome dele.
161
+ *
162
+ * O QUE ELE PERDIA: quando mais de um token DELE segura o mesmo valor, a esteira desempata por
163
+ * critérios lidos do repositório dele e o relatório imprimia só o vencedor, `→ {color.tier-gold}`,
164
+ * como se fosse fato. No sistema do dono aquele `#f59e0b` é `--color-tier-gold` E
165
+ * `--color-feedback-warning` - gamificação e estado, dois conceitos dele com o mesmo hex -, e a
166
+ * escolha entre eles nunca foi dele. Medido em 31/08 nos 26 sistemas do banco: 7 (27%) têm ao
167
+ * menos um valor com mais de um nome.
168
+ *
169
+ * A ESCOLHA CONTINUA SENDO FEITA, porque o `--fix` precisa de UM nome para escrever. O que muda é
170
+ * que ela para de passar por fato: ele vê que houve empate e pode discordar com informação, que é
171
+ * a diferença entre uma lacuna declarada e um silêncio.
172
+ *
173
+ * UM SÓ É NOMEADO, o resto vira contagem: dois nomes cabem na linha, cinco viram uma parede que
174
+ * ninguém lê - e o número já diz que a decisão existe.
175
+ */
176
+ const alsoNamed = (f) => !f.theirAlso?.length
177
+ ? ""
178
+ : f.theirAlso.length === 1
179
+ ? ` · you also call it ${f.theirAlso[0]}`
180
+ : ` · you also call it ${f.theirAlso[0]} and ${f.theirAlso.length - 1} other${f.theirAlso.length - 1 === 1 ? "" : "s"}`;
159
181
  function requestLabel(r) {
160
182
  if (r.kind === "token")
161
183
  return `token ${r.value} as ${r.name}`;
@@ -1314,7 +1336,7 @@ export async function doctor(opts) {
1314
1336
  const named = r.crossFamily && !r.theirToken
1315
1337
  ? ` → no ${r.kind} named for it · the value lives as ${r.token}`
1316
1338
  : nameToWrite(r)
1317
- ? ` → ${nameToWrite(r)}`
1339
+ ? ` → ${nameToWrite(r)}${alsoNamed(r)}`
1318
1340
  : near
1319
1341
  ? ` → nearest is ${near.theirs ?? near.name} (${near.value})`
1320
1342
  : "";
@@ -1338,7 +1360,7 @@ export async function doctor(opts) {
1338
1360
  const named = x.crossFamily && !x.theirToken
1339
1361
  ? `→ no ${x.kind} named for it · the value lives as ${x.token}`
1340
1362
  : nameToWrite(x)
1341
- ? `→ ${nameToWrite(x)}`
1363
+ ? `→ ${nameToWrite(x)}${alsoNamed(x)}`
1342
1364
  : near
1343
1365
  ? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
1344
1366
  : "→ no token holds this value yet";
@@ -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 { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, proposeNewHome, } from "../doctor/architecture.js";
8
+ import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, packagingOf, proposeNewHome, resolvesAs, } 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";
@@ -44,7 +44,7 @@ import { frontierKind, packageRoot } from "../frontier-kind.js";
44
44
  import { keyframesInSheets } from "../global-keyframes.js";
45
45
  import { globalClassesWorn } from "../global-wear.js";
46
46
  import { withLibraryStructure } from "../library-structure.js";
47
- import { mergeCensus } from "../merge-census.js";
47
+ import { architectureGaps, mergeCensus } from "../merge-census.js";
48
48
  import { claimName } from "../name-claim.js";
49
49
  import { mergeNamespacePairs } from "../namespace-pairs.js";
50
50
  import { namingQueue } from "../naming-queue.js";
@@ -567,8 +567,14 @@ export async function takeCensus(root, opts) {
567
567
  * naming how the project is organised costs no second pass. See `architecture.ts`.
568
568
  */
569
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 });
570
+ /** Uma entrada nova da árvore, com os contadores em zero e nada observado. */
571
+ const freshDir = () => ({
572
+ dirs: new Set(),
573
+ files: 0,
574
+ direct: 0,
575
+ index: false,
576
+ names: [],
577
+ });
572
578
  /** Parent → child → the files that put one inside the other. */
573
579
  const nesting = new Map();
574
580
  /**
@@ -668,6 +674,21 @@ export async function takeCensus(root, opts) {
668
674
  const home = parts.slice(0, -1).join("/");
669
675
  const entry = dirs.get(home) ?? freshDir();
670
676
  entry.direct += 1;
677
+ entry.names.push((parts[parts.length - 1] ?? "").replace(/\.[a-z]+$/i, ""));
678
+ dirs.set(home, entry);
679
+ }
680
+ /**
681
+ * O BARRIL, REGISTRADO FORA DO FILTRO DE COMPONENTE - e é por isso que ele precisa de
682
+ * bloco próprio.
683
+ *
684
+ * `index.ts` não é um arquivo de componente pela régua acima, então o bloco anterior nunca
685
+ * o vê. Sem este registro, a pasta que o cliente importa como uma unidade
686
+ * (`from "./ActivityRing"`) é indistinguível de uma pasta qualquer com um arquivo dentro.
687
+ */
688
+ if (/(^|\/)index\.(tsx|ts|jsx|js|vue|svelte)$/i.test(rel)) {
689
+ const home = rel.split("/").slice(0, -1).join("/");
690
+ const entry = dirs.get(home) ?? freshDir();
691
+ entry.index = true;
671
692
  dirs.set(home, entry);
672
693
  }
673
694
  sources.push({ file: rel, source: src });
@@ -1325,18 +1346,20 @@ export async function takeCensus(root, opts) {
1325
1346
  dirs: [...v.dirs],
1326
1347
  files: v.files,
1327
1348
  direct: v.direct,
1349
+ /** COMO ESTA PASTA RESOLVE - a decisão é de `doctor/architecture.ts`; aqui só se mede. */
1350
+ resolves: resolvesAs(path || ".", v.index, v.names),
1328
1351
  }));
1329
1352
  const architectures = detectArchitectures(dirEntries);
1330
1353
  /**
1331
1354
  * A LACUNA, quando nada foi reconhecido - ver `architectureGap` (`INV-COB-08`). Nenhuma
1332
1355
  * decisão aqui: este arquivo IMPRIME e GRAVA o que aquela camada decidiu.
1333
1356
  */
1334
- const gap = architectureGap(dirEntries, architectures);
1335
1357
  /**
1336
1358
  * ONDE O PRÓXIMO COMPONENTE VAI - derivado do escopo que ele apontou, e por isso a
1337
1359
  * pergunta sobrevive a não reconhecermos organização nenhuma.
1338
1360
  */
1339
1361
  const newHome = proposeNewHome(dirEntries, scopeLabel);
1362
+ const gap = architectureGap(dirEntries, architectures, scopeLabel ?? ".", newHome);
1340
1363
  const d = diagnose(reports);
1341
1364
  const allComposed = tallyToInventory(tally, 400);
1342
1365
  /**
@@ -2054,7 +2077,12 @@ export async function takeCensus(root, opts) {
2054
2077
  if (choice)
2055
2078
  say(body(choice));
2056
2079
  for (const a of architectures.slice(0, 4)) {
2057
- say(body(paint.faint(` ${describeArchitecture(a)}`)));
2080
+ /**
2081
+ * O ESCOPO VAI JUNTO, ou o terminal imprime um caminho que não existe na raiz dele.
2082
+ * `a.root` é relativo ao escopo medido - num monorepo lido com `--scope packages/ui`,
2083
+ * `src/lib/SignalUI` sozinho manda a pessoa procurar na pasta errada.
2084
+ */
2085
+ say(body(paint.faint(` ${describeArchitecture(a, scopeLabel)}`)));
2058
2086
  }
2059
2087
  }
2060
2088
  else if (gap) {
@@ -2065,7 +2093,7 @@ export async function takeCensus(root, opts) {
2065
2093
  */
2066
2094
  say("");
2067
2095
  say(section("How this project is organised"));
2068
- say(body(describeGap(gap, scopeLabel)));
2096
+ say(body(describeGap(gap)));
2069
2097
  }
2070
2098
  if (skips.length > 0) {
2071
2099
  say("");
@@ -2545,22 +2573,35 @@ export async function takeCensus(root, opts) {
2545
2573
  : {}),
2546
2574
  ...(architectures.length > 0
2547
2575
  ? {
2548
- architectures: architectures.slice(0, 6).map((a) => ({
2549
- ...a,
2550
- /** O caminho e a frase que a PERGUNTA usa verbatim - ver `componentHome`. */
2551
- home: componentHome(a, scopeLabel),
2552
- line: homeLine(a),
2553
- rule: architectureRule(a, scopeLabel),
2554
- })),
2576
+ architectures: architectures.slice(0, 6).map((a) => {
2577
+ /**
2578
+ * O EMPACOTAMENTO DAQUELE LUGAR, medido uma vez e usado nas duas pontas: ele entra
2579
+ * na regra que vai para o `CLAUDE.md` dele e viaja no censo como fato conferível.
2580
+ */
2581
+ const packaging = packagingOf(dirEntries, a.root);
2582
+ return {
2583
+ ...a,
2584
+ /** O caminho e a frase que a PERGUNTA usa verbatim - ver `componentHome`. */
2585
+ home: componentHome(a, scopeLabel),
2586
+ line: homeLine(a),
2587
+ rule: architectureRule(a, packaging, scopeLabel),
2588
+ ...(packaging ? { packaging } : {}),
2589
+ };
2590
+ }),
2555
2591
  }
2556
2592
  : {}),
2557
2593
  /**
2558
2594
  * A LEITURA QUE RODOU E NÃO RECONHECEU NADA - `INV-COB-08`, ver `architectureGap`.
2559
2595
  *
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.
2596
+ * UMA LISTA DE UM, porque esta função mede UM lugar. O merge concatena as de cada escopo,
2597
+ * e é a lista que mantém cada número junto do lugar onde ele foi medido.
2598
+ *
2599
+ * Os dois números, o lugar, e a frase que os declara. Sem nome de forma, sem regra e sem
2600
+ * caminho de destino: nada foi reconhecido, então qualquer um dos três seria adivinhação
2601
+ * nossa gravada como fato dele. A frase é a exceção, e ela é declarada no tipo: fala da
2602
+ * NOSSA régua, e a única oração com data de validade fica no terminal.
2562
2603
  */
2563
- ...(gap ? { architectureGap: gap } : {}),
2604
+ ...(gap ? { architectureGaps: [gap] } : {}),
2564
2605
  /**
2565
2606
  * FORA DO `if` DAS FORMAS, e é isso que mantém a pergunta viva - `INV-COB-08`.
2566
2607
  *
@@ -4063,6 +4104,20 @@ export async function runImport(opts) {
4063
4104
  }
4064
4105
  try {
4065
4106
  census = JSON.parse(raw);
4107
+ /**
4108
+ * A LACUNA DE UM CENSO ANTIGO, LIDA AQUI - `INV-COB-08`.
4109
+ *
4110
+ * Este é o único caminho por onde um censo medido por outra versão entra: a fusão só vê o
4111
+ * que `takeCensus` acabou de produzir. Um arquivo de 0.16.337 ou 0.16.338 traz o par somado
4112
+ * `architectureGap`, que nenhuma superfície de hoje lê - sem esta linha, o agente que
4113
+ * anotou aquele censo e o reenvia perde a declaração de que a leitura rodou e não
4114
+ * reconheceu nada, que é exatamente o silêncio que a invariante existe para eliminar.
4115
+ *
4116
+ * O lugar não é inventado: a forma antiga não o guardou, e a entrada convertida diz isso.
4117
+ */
4118
+ const carried = architectureGaps(census);
4119
+ if (carried.length > 0)
4120
+ census.architectureGaps = carried;
4066
4121
  }
4067
4122
  catch {
4068
4123
  console.log(section("Import"));
@@ -404,9 +404,9 @@ async function findToken(root, value) {
404
404
  */
405
405
  const theirNameFor = (v) => {
406
406
  const norm = normalizeValue(v, table.rootPx);
407
- for (const [key, name] of table.aliases)
407
+ for (const [key, theirs] of table.aliases)
408
408
  if (key.endsWith(`:${norm}`))
409
- return name;
409
+ return theirs.name;
410
410
  return null;
411
411
  };
412
412
  const exact = tokenFor(table, value);
@@ -115,7 +115,7 @@ export async function sync(opts) {
115
115
  */
116
116
  const measured = existsSync(join(root, "_synthesisui", "census.json"));
117
117
  console.log(body(measured
118
- ? "This repo has a census but no installed system, so there is nowhere to send it. `import` created the system on the platform; bringing it back is what tells sync which one this repo feeds:"
118
+ ? "This repo has been measured but has no installed system, so there is nowhere to send it. `import` created the system on the platform; bringing it back is what tells sync which one this repo feeds:"
119
119
  : "No installed system here, so there is nowhere to sync to. Install one first:"));
120
120
  console.log(body(paint.blue(" npx synthesisui@latest add <slug>")));
121
121
  console.log(body(paint.faint(" npx synthesisui@latest list --mine # the slugs you own")));
@@ -26,12 +26,12 @@ const SHAPES = [
26
26
  ["molecules", "molecule"],
27
27
  ["organisms", "organism"],
28
28
  ],
29
- describe: (a) => `Components live in an atomic ladder under \`${a.root}\` - ${a.evidence.join("/")}. A new component goes on the rung that matches what it is made of: an atom composes nothing of yours, a molecule composes atoms, an organism is a whole region of a screen. Put it on the wrong rung and the ladder stops being true.`,
29
+ describe: (a, where) => `Components live in an atomic ladder under \`${where}\` - ${a.evidence.join("/")}. A new component goes on the rung that matches what it is made of: an atom composes nothing of yours, a molecule composes atoms, an organism is a whole region of a screen. Put it on the wrong rung and the ladder stops being true.`,
30
30
  },
31
31
  {
32
32
  kind: "feature",
33
33
  needs: [["features", "modules", "domains"]],
34
- describe: (a) => `Code is organised BY FEATURE under \`${a.root}\`. A new component belongs to the feature that uses it, beside its own logic, and only moves up to the shared layer when a second feature needs it. Reaching across features is the thing this shape exists to prevent.`,
34
+ describe: (_a, where) => `Code is organised BY FEATURE under \`${where}\`. A new component belongs to the feature that uses it, beside its own logic, and only moves up to the shared layer when a second feature needs it. Reaching across features is the thing this shape exists to prevent.`,
35
35
  },
36
36
  {
37
37
  kind: "layered",
@@ -39,19 +39,116 @@ const SHAPES = [
39
39
  ["ui", "components"],
40
40
  ["hooks", "lib", "utils"],
41
41
  ],
42
- describe: (a) => `Code is split by LAYER under \`${a.root}\` - ${a.evidence.join(", ")}. A component goes in the ui layer and nothing else does; state and helpers stay in theirs. A component reaching into another layer is fine, a helper reaching back into ui is not.`,
42
+ describe: (a, where) => `Code is split by LAYER under \`${where}\` - ${a.evidence.join(", ")}. A component goes in the ui layer and nothing else does; state and helpers stay in theirs. A component reaching into another layer is fine, a helper reaching back into ui is not.`,
43
43
  },
44
44
  {
45
45
  kind: "colocated",
46
46
  needs: [["app", "routes", "pages"]],
47
- describe: (a) => `Components sit BESIDE THE ROUTE that uses them, under \`${a.root}\`. A new one starts local to its page and only moves to the shared folder when a second page needs it - which keeps the shared folder meaning "shared" rather than "everything".`,
47
+ describe: (_a, where) => `Components sit BESIDE THE ROUTE that uses them, under \`${where}\`. A new one starts local to its page and only moves to the shared folder when a second page needs it - which keeps the shared folder meaning "shared" rather than "everything".`,
48
48
  },
49
49
  {
50
50
  kind: "flat",
51
51
  needs: [["components"]],
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.`,
52
+ describe: (_a, where) => `Every component sits directly under \`${where}\`, 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
+ * O MESMO NOME, MÓDULO CAIXA E SEPARADOR - a única normalização deste arquivo.
57
+ *
58
+ * Ela não inventa convenção: `Tooltip` e `tooltip`, `ContentWrapper` e `content-wrapper` são o
59
+ * mesmo nome escrito com a preferência de quem digitou. Comparar as strings cruas faria a
60
+ * régua responder por convenção de nome de arquivo, e duas pessoas com a MESMA organização
61
+ * receberiam respostas opostas.
62
+ */
63
+ export function sameName(a, b) {
64
+ const tidy = (x) => x.toLowerCase().replace(/[-_.\s]/g, "");
65
+ return tidy(a) === tidy(b);
66
+ }
67
+ /**
68
+ * COMO UMA PASTA RESOLVE - lido dos fatos que a caminhada mediu, num lugar só.
69
+ *
70
+ * O barril vence a eponímia quando os dois existem, porque é ele que decide o que o import
71
+ * escreve: numa pasta com `index.ts` e `Button.tsx`, quem importa escreve o nome da PASTA.
72
+ */
73
+ export function resolvesAs(folder, hasIndex, componentNames) {
74
+ const leaf = folder.split("/").pop() ?? folder;
75
+ const eponymous = componentNames.some((n) => sameName(n, leaf));
76
+ /**
77
+ * UM BARRIL NÃO BASTA - e este era o falso positivo que uma revisão de QA pegou.
78
+ *
79
+ * `src/components/{Card,Pill,Tag}.tsx` com um `index.ts` ao lado tinha barril e virava
80
+ * "módulo de um componente". Num repositório onde NENHUM componente mora em pasta própria, a
81
+ * regra passava a mandar o agente criar `src/Toast/Toast.tsx` - o defeito do `frontend-hub`
82
+ * ao contrário, e uma pessoa move o arquivo do mesmo jeito.
83
+ *
84
+ * O que separa os dois não é o barril: é a pasta ser DE UM componente. Ela é, quando carrega
85
+ * o arquivo de mesmo nome - `Select/` com `SelectItem` e `SelectOption` dentro continua sendo
86
+ * a pasta do `Select` -, ou quando carrega UM componente só, que é o `Button/index.tsx` puro.
87
+ */
88
+ if (eponymous)
89
+ return hasIndex ? "index" : "eponymous";
90
+ return hasIndex && componentNames.length <= 1 ? "index" : null;
91
+ }
92
+ /**
93
+ * O EMPACOTAMENTO DE UM LUGAR, medido das pastas que estão DIRETAMENTE sob ele.
94
+ *
95
+ * `null` quando não há o que medir - menos de duas pastas com componente, ou nenhuma que
96
+ * resolva. Um lugar sem empacotamento observável não recebe campo, em vez de receber zero: um
97
+ * zero pelado aqui leria como "o projeto dele não embala nada", e o que é verdade é que a
98
+ * nossa leitura não viu.
99
+ */
100
+ export function packagingOf(entries,
101
+ /** O lugar, como a caminhada o escreve - `"."` para a raiz do escopo. */
102
+ root) {
103
+ /**
104
+ * TODAS AS PASTAS SOB O LUGAR, e não só as filhas diretas - o primeiro rascunho errava aqui.
105
+ *
106
+ * Medido: com as filhas diretas, o `SignalUI` não era encontrado. A forma reconhecida ali é
107
+ * `atomic@src/lib/SignalUI`, e as filhas diretas dela são `atoms`, `molecules`, `organisms` -
108
+ * que não seguram componente NENHUM diretamente: os componentes moram um degrau abaixo, em
109
+ * `atoms/Button/Button.tsx`. A pergunta é sobre as pastas onde o componente MORA, e elas
110
+ * podem estar a qualquer profundidade sob o lugar.
111
+ */
112
+ const kids = entries.filter((e) => e.direct > 0 && e.path !== root && isUnderPath(root, e.path));
113
+ if (kids.length < 2)
114
+ return null;
115
+ const resolving = kids.filter((c) => c.resolves !== null);
116
+ if (resolving.length === 0)
117
+ return null;
118
+ const byIndex = resolving.filter((c) => c.resolves === "index").length;
119
+ return {
120
+ folders: kids.length,
121
+ resolving: resolving.length,
122
+ file: byIndex === resolving.length
123
+ ? "index"
124
+ : byIndex === 0
125
+ ? "eponymous"
126
+ : "mixed",
127
+ };
128
+ }
129
+ /**
130
+ * A SEGUNDA FRASE DA REGRA - o que o agente precisa saber para criar o próximo arquivo.
131
+ *
132
+ * `null` quando o empacotamento NÃO é a convenção do lugar. O limiar é a maioria simples, e
133
+ * ele está aqui em vez de dentro da medição de propósito: a maioria é o que torna a frase
134
+ * verdadeira como REGRA ("é assim que se faz aqui"), e abaixo dela os dois números continuam
135
+ * gravados como fato sem virar instrução.
136
+ *
137
+ * O QUE ELA NÃO AFIRMA, e cada omissão é uma medição que não temos: nada sobre onde a story ou
138
+ * o teste moram - a caminhada os exclui desde 0.16.337, então nunca os contamos; e nada sobre
139
+ * haver um arquivo só por pasta - o caso de uma pasta com o componente MAIS as partes dele é
140
+ * real, e a régua o aceita de propósito.
141
+ */
142
+ export function packagingRule(p) {
143
+ if (p.resolving * 2 <= p.folders)
144
+ return null;
145
+ const how = p.file === "index"
146
+ ? "each one holding an `index` that re-exports it"
147
+ : p.file === "eponymous"
148
+ ? "each one holding a file of the same name"
149
+ : "some holding an `index` that re-exports it, some a file of the same name";
150
+ return `A new component starts a folder of its own, named after it, ${how} - ${p.resolving} of the ${p.folders} folders here are written that way. Whatever belongs only to that component goes in the same folder; a \`SelectItem\` beside \`Select\` is still one folder, not two. This says nothing about where a story or a test lives - the reading never counted those.`;
151
+ }
55
152
  /**
56
153
  * A MESMA ORGANIZAÇÃO VISTA DE MAIS PERTO NÃO É UMA SEGUNDA ESCOLHA (`INV-COB-09`).
57
154
  *
@@ -173,9 +270,12 @@ export function detectArchitectures(entries) {
173
270
  return dropShapesAlreadyExplained(found).sort((a, b) => b.files - a.files || a.root.localeCompare(b.root));
174
271
  }
175
272
  /** The sentence that becomes the rule. Written for an agent about to add a file. */
176
- export function describeArchitecture(a) {
273
+ export function describeArchitecture(a, scope) {
274
+ const where = pathUnderScope(a.root, scope);
177
275
  const shape = SHAPES.find((s) => s.kind === a.kind);
178
- return shape ? shape.describe(a) : `Components live under \`${a.root}\`.`;
276
+ return shape
277
+ ? shape.describe(a, where)
278
+ : `Components live under \`${where}\`.`;
179
279
  }
180
280
  /**
181
281
  * The rule itself, in the shape `reading.rules` holds.
@@ -186,6 +286,15 @@ export function describeArchitecture(a) {
186
286
  * true once, not a habit that needs a third sighting.
187
287
  */
188
288
  export function architectureRule(a,
289
+ /**
290
+ * COMO O COMPONENTE É EMBALADO ALI - a segunda frase, quando ela é a convenção do lugar.
291
+ *
292
+ * OBRIGATÓRIO, e `null` é uma resposta: sem isso a regra volta a dizer só onde o componente
293
+ * nasce, num repositório onde todo componente é uma pasta - e o agente cria `atoms/Toast.tsx`
294
+ * onde o projeto inteiro escreve `atoms/Toast/Toast.tsx`. Um argumento opcional aqui seria a
295
+ * família do `fixReach`: o chamador esquece, `tsc` aceita, e a regra sai pela metade.
296
+ */
297
+ packaging,
189
298
  /**
190
299
  * O ESCOPO, para a regra dizer um caminho que existe. `a.root` é relativo ao escopo, e uma
191
300
  * regra no CLAUDE.md de um monorepo que diz `src/lib/SignalUI` manda o agente para um caminho
@@ -193,12 +302,14 @@ export function architectureRule(a,
193
302
  */
194
303
  scope) {
195
304
  const home = componentHome(a, scope);
305
+ const where = pathUnderScope(a.root, scope);
306
+ const packs = packaging ? packagingRule(packaging) : null;
196
307
  return {
197
- text: `${describeArchitecture(a)} A new component goes under \`${home}\`.`,
308
+ text: `${describeArchitecture(a, scope)} A new component goes under \`${home}\`.${packs ? ` ${packs}` : ""}`,
198
309
  applies: [],
199
310
  kind: "implementation",
200
311
  fact: true,
201
- evidence: `${a.evidence.join("/")} under ${scope ? `${scope}/${a.root}` : a.root}, holding ${a.files} component file${a.files === 1 ? "" : "s"}`,
312
+ evidence: `${a.evidence.join("/")} under ${where}, holding ${a.files} component file${a.files === 1 ? "" : "s"}`,
202
313
  };
203
314
  }
204
315
  /**
@@ -215,10 +326,30 @@ scope) {
215
326
  * na frente é ambíguo num monorepo, e é para o CLAUDE.md que essa linha vai.
216
327
  */
217
328
  export function componentHome(a, scope) {
218
- const base = scope ? `${scope.replace(/\/+$/, "")}/${a.root}` : a.root;
219
- const tidy = base.replace(/\/\.$/, "").replace(/^\.\//, "");
329
+ const tidy = pathUnderScope(a.root, scope);
220
330
  return a.evidence.length > 0 ? `${tidy}/{${a.evidence.join(", ")}}` : tidy;
221
331
  }
332
+ /**
333
+ * O LUGAR, COMPOSTO UMA VEZ - a régua de caminho que todas as superfícies usam.
334
+ *
335
+ * O QUE O CLIENTE GANHA: o caminho que ele lê na regra, na evidência e na pergunta é sempre o
336
+ * mesmo, e sempre resolve a partir da raiz do repositório dele.
337
+ *
338
+ * ELA ERA DUAS, E AS DUAS DISCORDAVAM. `componentHome` normalizava barra final, `./` na frente e
339
+ * `/.` no fim; a `evidence` concatenava cru. Medido em 31/08, com o escopo real de um monorepo:
340
+ *
341
+ * scope="packages/ui/" root="src/components" evidência dizia packages/ui//src/components
342
+ * scope="packages/ui" root="." evidência dizia packages/ui/.
343
+ *
344
+ * A raiz do escopo é `"."` pela caminhada (`import.ts` escreve `path || "."`), então o segundo
345
+ * caso não é defensivo: é a organização reconhecida na raiz de um pacote, que é comum. Duas
346
+ * redações da mesma régua são duas réguas no dia em que uma for editada, e neste caso elas já
347
+ * tinham sido.
348
+ */
349
+ export function pathUnderScope(root, scope) {
350
+ const base = scope ? `${scope.replace(/\/+$/, "")}/${root}` : root;
351
+ return base.replace(/\/\.$/, "").replace(/^\.\//, "");
352
+ }
222
353
  /**
223
354
  * A MESMA FORMA EM UMA FRASE - o que decide, e o número que a torna credível.
224
355
  *
@@ -370,7 +501,20 @@ export const GAP_MIN_FILES = 3;
370
501
  * Devolve `null` quando há forma reconhecida (aí quem fala é `describeChoice`) e quando o
371
502
  * escopo é pequeno demais para ter organização (`GAP_MIN_FILES`).
372
503
  */
373
- export function architectureGap(entries, found) {
504
+ export function architectureGap(entries, found,
505
+ /**
506
+ * ONDE ESTA LEITURA MEDIU - `"."` para a raiz, ou o caminho apontado. OBRIGATÓRIO.
507
+ *
508
+ * Não tem default de propósito: um default aqui devolve o parâmetro opcional que a frase
509
+ * tinha, e com ele um chamador podia esquecer o lugar sem que nada reprovasse.
510
+ */
511
+ scope,
512
+ /**
513
+ * A PASTA NOVA DESTE ESCOPO - `proposeNewHome` sobre a MESMA árvore, ou `null`.
514
+ *
515
+ * Obrigatório pelo mesmo motivo de `scope`: uma proposta de outro lugar é pior que nenhuma.
516
+ */
517
+ newHome) {
374
518
  if (found.length > 0)
375
519
  return null;
376
520
  /**
@@ -388,27 +532,61 @@ export function architectureGap(entries, found) {
388
532
  }
389
533
  if (files < GAP_MIN_FILES)
390
534
  return null;
391
- return { files, folders };
535
+ /**
536
+ * O EMPACOTAMENTO NÃO ENTRA AQUI, e a ausência é decisão declarada.
537
+ *
538
+ * A leitura sabe medi-lo mesmo num escopo mudo, e o cliente que menos tem regra é justamente
539
+ * esse. Mas o caminho pelo qual o empacotamento alcança alguém é `architectures[].rule`, que
540
+ * uma lacuna não tem: gravá-lo aqui criaria um campo sem leitor - o defeito exato que esta
541
+ * mesma entrega existe para consertar um campo ao lado. Ele entra no dia em que a lacuna
542
+ * ganhar a frase que o carrega, e entra com o leitor junto.
543
+ */
544
+ return {
545
+ scope,
546
+ files,
547
+ folders,
548
+ reason: gapReason({ scope, files, folders }),
549
+ newHome,
550
+ };
392
551
  }
393
552
  /**
394
- * A FRASE, no tom das outras desta camada: o número medido, o motivo, e nenhuma acusação.
553
+ * O LUGAR, EM PALAVRAS - a única tradução de `scope` para prosa neste produto.
395
554
  *
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.
555
+ * `"."` não resolve para nada na cabeça de quem lê um relatório, e "here" não resolve para nada
556
+ * na cabeça de quem rodou a leitura sobre uma parte de um monorepo. Uma leitura da raiz não tem
557
+ * outro lugar a que se referir, então ela é a única que pode dizer "here".
399
558
  */
400
- export function describeGap(g,
559
+ function whereWeRead(scope) {
560
+ return scope === null || isRoot(scope) ? "here" : `under \`${scope}\``;
561
+ }
401
562
  /**
402
- * ONDE MEDIMOS, quando a leitura foi apontada para uma parte do repositório.
563
+ * O QUE MEDIMOS, E DE QUEM É O LIMITE - a metade da frase que sobrevive a ser GRAVADA.
403
564
  *
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.
565
+ * As duas orações que são verdade para sempre sobre aquela leitura: os números medidos, e que
566
+ * a régua é nossa. Nada aqui tem data de validade, e é por isso que esta é a parte que vai
567
+ * para o censo e para a pergunta que o agente faz.
568
+ *
569
+ * A segunda oração não é gentileza: sem ela a frase lê como um defeito do repositório dele, e
570
+ * o defeito é da régua.
408
571
  */
409
- where) {
572
+ export function gapReason(g) {
410
573
  const files = `${g.files} component file${g.files === 1 ? "" : "s"}`;
411
574
  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.`;
575
+ return `We read ${files} in ${folders} ${whereWeRead(g.scope)}, 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.`;
576
+ }
577
+ /**
578
+ * A FRASE INTEIRA, para o TERMINAL - a declaração gravável mais a consequência de hoje.
579
+ *
580
+ * A ordem é a da dúvida de quem lê: o que medimos, de quem é o limite, e o que isso custa a ele
581
+ * agora. As duas primeiras vêm de `gapReason` e são as mesmas que o censo carrega - uma redação
582
+ * só, e não duas que divergem no dia em que alguém editar uma.
583
+ *
584
+ * A TERCEIRA MORA SÓ AQUI, e é a razão de esta função existir separada. Ela afirma o estado do
585
+ * `CLAUDE.md` DELE, o que é verdade no instante do relatório e falso depois - inclusive falso
586
+ * por causa da pergunta que este mesmo relatório abre. Gravada no censo, ela viraria medição
587
+ * corrente promovida a fato permanente, e seria reescrita como nova a cada `sync` sobre um
588
+ * repositório que já tem a regra.
589
+ */
590
+ export function describeGap(g) {
591
+ return `${g.reason} 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
592
  }
@@ -420,7 +420,8 @@ function scanCore(file, source, table) {
420
420
  ...(/(?<!r)em$/i.test(literal.trim())
421
421
  ? { fontRelative: true }
422
422
  : {}),
423
- ...(theirs ? { theirToken: theirs } : {}),
423
+ ...(theirs ? { theirToken: theirs.name } : {}),
424
+ ...(theirs?.also.length ? { theirAlso: theirs.also } : {}),
424
425
  /**
425
426
  * A COINCIDÊNCIA VIAJA COM O ACHADO - ver `tokenMatch`.
426
427
  *
@@ -609,6 +610,7 @@ export function diagnose(files) {
609
610
  kind: f.kind,
610
611
  token: f.token,
611
612
  ...(f.theirToken ? { theirToken: f.theirToken } : {}),
613
+ ...(f.theirAlso?.length ? { theirAlso: f.theirAlso } : {}),
612
614
  ...(f.fontRelative ? { fontRelative: true } : {}),
613
615
  count: 1,
614
616
  files: new Set([f.file]),
@@ -622,6 +624,7 @@ export function diagnose(files) {
622
624
  kind: v.kind,
623
625
  token: v.token,
624
626
  ...(v.theirToken ? { theirToken: v.theirToken } : {}),
627
+ ...(v.theirAlso?.length ? { theirAlso: v.theirAlso } : {}),
625
628
  ...(v.fontRelative ? { fontRelative: true } : {}),
626
629
  count: v.count,
627
630
  files: v.files.size,
@@ -98,6 +98,18 @@ function pick(candidates, ours, convention) {
98
98
  * `24px` pode ser raio, espaçamento ou tamanho de fonte, e sem um token nosso não há o que validasse.
99
99
  * Esses continuam saindo como "no name for it", que é verdade e é o caminho do `absorb`.
100
100
  */
101
+ /**
102
+ * O VENCEDOR E OS PRETERIDOS, juntos - porque um empate silencioso é o defeito.
103
+ *
104
+ * `pick` escolhe entre nomes DELE por critérios lidos do repositório dele, e isso continua certo:
105
+ * o achado precisa de UM nome para escrever. O que estava errado era descartar os outros. No
106
+ * sistema do dono, `#f59e0b` é `--color-tier-gold` E `--color-feedback-warning` - dois conceitos
107
+ * dele, gamificação e estado -, e o relatório dizia `→ {color.tier-gold}` como se fosse fato.
108
+ */
109
+ function escolha(candidates, ours, convention) {
110
+ const name = pick(candidates, ours, convention);
111
+ return { name, also: candidates.filter((c) => c !== name) };
112
+ }
101
113
  export function theirNames(ours, theirs) {
102
114
  const out = new Map();
103
115
  if (theirs.byName.size === 0)
@@ -139,7 +151,7 @@ export function theirNames(ours, theirs) {
139
151
  : candidates.filter((n) => familySays(kind, n));
140
152
  if (usable.length === 0)
141
153
  continue;
142
- out.set(key, pick(usable, name, convention));
154
+ out.set(key, escolha(usable, name, convention));
143
155
  }
144
156
  }
145
157
  for (const [value, candidates] of theirs.byValue) {
@@ -149,7 +161,7 @@ export function theirNames(ours, theirs) {
149
161
  const key = `${kind}:${value}`;
150
162
  if (out.has(key))
151
163
  continue;
152
- out.set(key, pick(candidates, null, convention));
164
+ out.set(key, escolha(candidates, null, convention));
153
165
  }
154
166
  }
155
167
  return out;
@@ -418,7 +418,7 @@ kind) {
418
418
  const theirs = kind
419
419
  ? table.aliases.get(`${kind}:${normalizeValue(best.value, table.rootPx)}`)
420
420
  : undefined;
421
- return theirs ? { ...best, theirs } : best;
421
+ return theirs ? { ...best, theirs: theirs.name } : best;
422
422
  }
423
423
  /**
424
424
  * The family a token belongs to, from the drift it was found in.
@@ -415,7 +415,76 @@ export const CHECKER_SINCE = "0.16.308";
415
415
  * NÃO se moveu, e isso é a informação: nenhum dos 28 apps dourados tem forma repetindo o que uma
416
416
  * ancestral já diz, então a estabilidade ali prova ausência de regressão e não ausência de efeito.
417
417
  */
418
- export const READER_SINCE = "0.16.338";
418
+ /**
419
+ * 0.16.338 -> 0.16.339 em 31/08 (`INV-COB-08`): a lacuna de organização passou a viajar no censo
420
+ * como LISTA, com o lugar de cada leitura e a frase pronta que a declara - `architectureGaps[]`,
421
+ * no lugar do par somado `architectureGap`.
422
+ *
423
+ * A marca sobe porque o AGENTE dele é o leitor. A skill `sui-import-ds` copia
424
+ * `architectureGaps[].reason` verbatim para abrir a pergunta de onde nasce um componente novo, e
425
+ * um censo medido antes não carrega esse campo: o agente volta ao estado em que a leitura rodou,
426
+ * não reconheceu nada, e nenhuma pergunta chegou à tela dele. O par antigo também não guarda o
427
+ * LUGAR da medição, e nada pode recuperá-lo sem reler o disco - a caminhada é o que sabe onde
428
+ * cada arquivo estava.
429
+ *
430
+ * O QUE O `sync` ENTREGA: ele remede com `takeCensus`, então o censo em disco e o reenviado já
431
+ * nascem com a lista e a frase. O que ele NÃO entrega continua sendo a linha do `CLAUDE.md` dele,
432
+ * pelo mesmo motivo da marca anterior - `rulesFromCensus` não olha para este campo, e a regra de
433
+ * organização nasce da conversa de import.
434
+ *
435
+ * MEDIÇÃO de 31/08, no banco de produção: **1 sistema com censo de import guardado, e `0` com a
436
+ * forma antiga** - aquele censo é anterior a 0.16.337 e não carrega campo de organização nenhum.
437
+ * Foto daquela população naquele dia, nunca invariante.
438
+ *
439
+ * A COMPATIBILIDADE NÃO É O QUE ALCANÇA ELE, e a distinção foi medida por uma revisão de QA. A
440
+ * leitura da forma antiga (`architectureGaps`, em `merge-census.ts`) roda em UM caminho:
441
+ * `import --census <arquivo>`, onde um agente reenvia um censo que ele mesmo anotou. A FUSÃO
442
+ * nunca a alcança - ela só vê censos que `takeCensus` acabou de medir, e esses já nascem na forma
443
+ * nova. Quem tem `census.json` em disco e não usa aquele comando é alcançado pelo `sync`, que
444
+ * remede e reescreve o arquivo: é isso, e não a compatibilidade, que esta marca anuncia.
445
+ */
446
+ /**
447
+ * 0.16.339 -> 0.16.340 em 31/08 (`INV-COB-10`): a leitura passou a medir COMO o componente é
448
+ * embalado - se as pastas sob um lugar resolvem como o módulo dele, por barril `index.*` ou por
449
+ * arquivo de mesmo nome módulo caixa e separador - e a regra que vai para o `CLAUDE.md` dele
450
+ * ganhou uma segunda frase dizendo isso.
451
+ *
452
+ * A marca sobe porque o fato nasce da CAMINHADA. Um censo medido antes não carrega `resolves`
453
+ * por pasta nem `packaging` por lugar, e nada aqui pode recuperá-los sem reler o disco: o
454
+ * `index.ts` que faz `ActivityRing/` resolver como módulo não é arquivo de componente e nunca
455
+ * foi contado. Só a remedição vê.
456
+ *
457
+ * O QUE O CLIENTE GANHA: hoje a regra dele diz onde o próximo componente nasce e cala sobre o
458
+ * empacotamento. O agente cria `atoms/Toast.tsx` num repositório onde tudo é
459
+ * `atoms/Toast/Toast.tsx` com `index.ts` ao lado, e uma pessoa move o arquivo toda vez. A regra
460
+ * estava certa e incompleta.
461
+ *
462
+ * O `sync` remede com `takeCensus`, então o censo em disco e o reenviado já nascem com os dois
463
+ * campos. O que ele NÃO faz continua sendo reescrever a regra já escrita no `CLAUDE.md` dele.
464
+ *
465
+ * MEDIÇÃO de 31/08, duas populações: 35 formas reconhecidas ganham a segunda frase, 3 têm o
466
+ * fato e ficam abaixo da maioria, 37 não têm empacotamento observável.
467
+ */
468
+ /**
469
+ * 0.16.340 -> 0.16.341 em 31/08 (`INV-COB-11`): o caminho que a regra de organização cita passou
470
+ * a resolver a partir da raiz do repositório, nas QUATRO superfícies que o dizem.
471
+ *
472
+ * O QUE O CLIENTE DE MONOREPO VIVIA: a regra que governa todo prompt dele se contradizia na mesma
473
+ * frase - *"Components live in an atomic ladder under `src/components`. (...) A new component goes
474
+ * under `packages/ui/src/components/{…}`"*. A primeira metade é a que descreve o projeto, e ela
475
+ * mandava o agente procurar uma pasta que não existe na raiz.
476
+ *
477
+ * A marca sobe porque o campo é COMPOSTO na medição: `architectures[].rule.text` já está gravado
478
+ * com o caminho pelado, e nada do lado de cá o reescreve. O `sync` remede com `takeCensus`, então
479
+ * quem rodar recebe a regra certa no censo em disco e no reenviado; sem a marca, o `align` não
480
+ * chamaria ninguém para rodar.
481
+ *
482
+ * QUEM NÃO É AFETADO, e é a maioria: quem mede a raiz do repositório. Sem escopo o caminho já era
483
+ * o mesmo nas quatro superfícies - o defeito só existe quando a leitura foi apontada para uma
484
+ * parte do repositório. O corpus dourado NÃO se moveu por isso: nenhum dos 29 apps é medido com
485
+ * escopo, e o hash parado aqui é ausência de cobertura, nunca ausência de efeito.
486
+ */
487
+ export const READER_SINCE = "0.16.341";
419
488
  /**
420
489
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
421
490
  *
@@ -1,3 +1,4 @@
1
+ import { gapReason } from "./doctor/architecture.js";
1
2
  const asRecord = (v) => (v ?? {});
2
3
  /** Nome do escopo para as mensagens - `.` quando a medição não escopou nada. */
3
4
  const nameOf = (c, i) => c.scope ?? (i === 0 ? "." : `#${i + 1}`);
@@ -120,22 +121,30 @@ export function mergeCensus(list) {
120
121
  let conventions = first.conventions ?? undefined;
121
122
  let architectures = first.architectures ?? undefined;
122
123
  /**
123
- * A LACUNA DE ORGANIZAÇÃO, SOMADA - `INV-COB-08`, ver `architectureGap` em `import.ts`.
124
+ * AS LACUNAS DE ORGANIZAÇÃO, CONCATENADAS - `INV-COB-08`, ver `architectureGaps` em
125
+ * `import.ts`.
124
126
  *
125
127
  * A declaração de qualquer escopo SOBREVIVE à fusão. Ela diz "medimos aqui e não soubemos
126
128
  * nomear"; deixá-la de fora porque o outro escopo foi reconhecido devolve o silêncio que a
127
129
  * invariante existe para eliminar, um passo adiante - e o cliente que rodou com dois escopos
128
130
  * é justamente quem tem mais lugar onde a resposta pode sumir.
129
131
  *
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.
132
+ * CONCATENADAS, E NUNCA SOMADAS, e este era o defeito: contagem é aditiva, mas contagem COM
133
+ * UM LUGAR não é. Somando, dois escopos mudos de 37 e 7 arquivos produziam `{files: 44}` com
134
+ * a mesma forma de uma leitura só - e a frase construída sobre aquilo diria "44 arquivos sob
135
+ * `packages/ui`", que é falso, ou "44 arquivos aqui", que não é conferível. O próprio tipo
136
+ * promete que o cliente confere os dois números com um `find` de uma linha sobre UMA pasta, e
137
+ * nenhum `find` devolve 44.
138
+ *
139
+ * É o mesmo defeito que `scope-of.ts` já consertou um campo adiante (`census.scope` era o
140
+ * PRIMEIRO escopo, e todo componente do segundo era atribuído a ele), e a mesma forma que
141
+ * `architectures` já usa: cada entrada carrega o seu lugar embutido.
142
+ *
143
+ * O PISO É POR ESCOPO, e a lista é o que o mantém assim. Dois escopos de 2 arquivos calam
144
+ * cada um por `GAP_MIN_FILES`; somados antes do piso, os 4 falariam sobre uma organização que
145
+ * nenhum dos dois tem.
134
146
  */
135
- let gapFiles = first.architectureGap
136
- ?.files;
137
- let gapFolders = first.architectureGap
138
- ?.folders;
147
+ let gaps = architectureGaps(first);
139
148
  /**
140
149
  * ONDE O PRÓXIMO COMPONENTE VAI - o PRIMEIRO escopo vence, a mesma regra de `scope`.
141
150
  *
@@ -203,11 +212,7 @@ export function mergeCensus(list) {
203
212
  brokenRefs = concat(brokenRefs, c.brokenRefs);
204
213
  conventions = concat(conventions, c.conventions);
205
214
  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
- }
215
+ gaps = [...gaps, ...architectureGaps(c)];
211
216
  components = concat(components, c.components);
212
217
  defined = concat(defined, c.defined);
213
218
  for (const s of c.project.stack ?? [])
@@ -291,8 +296,8 @@ export function mergeCensus(list) {
291
296
  out.conventions = conventions;
292
297
  if (architectures && architectures.length > 0)
293
298
  out.architectures = architectures;
294
- if (gapFiles !== undefined && gapFolders !== undefined)
295
- out.architectureGap = { files: gapFiles, folders: gapFolders };
299
+ if (gaps.length > 0)
300
+ out.architectureGaps = gaps;
296
301
  if (newComponentHome)
297
302
  out.newComponentHome = newComponentHome;
298
303
  if (components && components.length > 0)
@@ -357,3 +362,52 @@ export function mergeCensus(list) {
357
362
  }
358
363
  return out;
359
364
  }
365
+ /**
366
+ * AS LACUNAS DE UM CENSO, NAS DUAS FORMAS QUE EXISTEM - `INV-COB-08`.
367
+ *
368
+ * O QUE O CLIENTE GANHA: quem entrega um censo JÁ MEDIDO continua recebendo a declaração de que
369
+ * a leitura rodou e não reconheceu nada. Descartar a forma antiga devolveria a esse cliente o
370
+ * silêncio exato que a invariante existe para eliminar, e ele nem saberia que houve leitura.
371
+ *
372
+ * ONDE ISTO É ALCANÇADO, e não é aqui dentro: a fusão só vê censos que `takeCensus` acabou de
373
+ * medir no mesmo processo, e esses já nascem na forma nova. O caminho por onde um censo ANTIGO
374
+ * entra de verdade é `import --census <arquivo>`, que lê o JSON do disco e o usa como está - o
375
+ * comando que a nossa própria skill ensina a um agente que anotou o censo. Uma revisão de QA
376
+ * mediu que este ramo só tinha o spec como chamador; ele agora roda naquele caminho, e por isso
377
+ * é exportado.
378
+ *
379
+ * A FORMA ANTIGA NÃO GUARDA O LUGAR, e nada aqui pode inventá-lo. Um censo de 0.16.337 ou
380
+ * 0.16.338 traz `architectureGap: {files, folders}` e nada mais - possivelmente já somado de
381
+ * dois escopos pela versão anterior desta função. Assumir a raiz produziria uma frase apontando
382
+ * para um lugar onde a contagem não fecha, que é pior que dizer que não sabemos. `null` é o
383
+ * terceiro estado do campo, e ele NÃO é a raiz: a frase resultante diz "here", que é a redação
384
+ * honesta para uma leitura sem lugar registrado, e o dado continua dizendo que não sabemos.
385
+ *
386
+ * `reason` é RECOMPOSTO e nunca herdado, porque a frase antiga foi escrita com um argumento de
387
+ * escopo que este censo não guardou.
388
+ */
389
+ export function architectureGaps(c) {
390
+ const anyish = c;
391
+ const fresh = anyish.architectureGaps;
392
+ if (Array.isArray(fresh))
393
+ return fresh;
394
+ const legacy = anyish.architectureGap;
395
+ if (!legacy)
396
+ return [];
397
+ const files = legacy.files ?? 0;
398
+ const folders = legacy.folders ?? 0;
399
+ const scope = null;
400
+ return [
401
+ {
402
+ scope,
403
+ files,
404
+ folders,
405
+ reason: gapReason({ scope, files, folders }),
406
+ /**
407
+ * A FORMA ANTIGA NÃO GUARDOU A PROPOSTA, e o `newComponentHome` do topo é o do PRIMEIRO
408
+ * escopo - oferecê-lo aqui apontaria a pergunta para um lugar que pode não ser este.
409
+ */
410
+ newHome: null,
411
+ },
412
+ ];
413
+ }
@@ -115,7 +115,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
115
115
  const kind = Object.entries(FAMILY_KIND).find(([prefix]) => name.startsWith(prefix))?.[1];
116
116
  if (!kind)
117
117
  return line;
118
- const theirName = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`);
118
+ const theirName = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`)?.name;
119
119
  if (!theirName || theirName === name)
120
120
  return line;
121
121
  if (!resolvable.has(theirName)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.338",
3
+ "version": "0.16.343",
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": {