synthesisui 0.16.436 → 0.16.439

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.
@@ -44,7 +44,7 @@ import { variantCallsIn } from "../doctor/variant-calls.js";
44
44
  import { transcribeVariants } from "../doctor/variant-read.js";
45
45
  import { frontierKind, packageRoot } from "../frontier-kind.js";
46
46
  import { keyframesInSheets } from "../global-keyframes.js";
47
- import { globalClassesWorn } from "../global-wear.js";
47
+ import { globalClassesWorn, porNecessidade } from "../global-wear.js";
48
48
  import { withLibraryStructure } from "../library-structure.js";
49
49
  import { architectureGaps, mergeCensus } from "../merge-census.js";
50
50
  import { claimName } from "../name-claim.js";
@@ -57,6 +57,7 @@ import { phase, startProgress } from "../progress.js";
57
57
  import { repoStateOf } from "../repo-state.js";
58
58
  import { relatorioHtml, secoesDe } from "../report-html.js";
59
59
  import { abertura, aneis, historiaDeCor, oQueMuda, passos, } from "../report-insight.js";
60
+ import { naoLido } from "../report-unread.js";
60
61
  import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
61
62
  import { detectStack, resolveDeps, stackVersions } from "../stack.js";
62
63
  import { desligarCaptura, ligarCaptura } from "../transcript.js";
@@ -246,7 +247,22 @@ async function harvestOwnCss(root,
246
247
  * (dono, 04/08). O veredito de cada uma sai depois, em `judgeFragments`: pertencer a componente
247
248
  * não admitido é um motivo, não um desaparecimento.
248
249
  */
249
- into) {
250
+ into,
251
+ /**
252
+ * A RAIZ DO REPOSITÓRIO, quando a medição está escopada - e ela entra SÓ pelas folhas globais.
253
+ *
254
+ * O QUE O CLIENTE GANHA (dono, 19/09): a classe que o componente dele veste é resolvida onde ela
255
+ * for DEFINIDA no repositório, e não só se ela morar dentro do escopo apontado. Medido nos
256
+ * repositórios reais: dentro do escopo de um deles existem 19 folhas globais e 7 classes
257
+ * aproveitáveis, enquanto o repositório inteiro declara 815 em 24 folhas - e os nomes que o
258
+ * componente veste e a leitura não resolvia (`d-flex`, `col-xs-12`, `icon-round`) estavam
259
+ * justamente do lado de fora.
260
+ *
261
+ * SÓ AS CLASSES, e esta é a linha que o parâmetro não cruza: `css` continua sendo o do escopo,
262
+ * porque é dele que sai a tabela de tokens DECLARADOS. Puxar a folha do repositório inteiro para
263
+ * lá faria o vocabulário do sistema absorver todo CSS do projeto, que é outra promessa.
264
+ */
265
+ alsoGlobalsFrom) {
250
266
  /**
251
267
  * DOIS PASSES, porque o escopo de uma folha está no GRAFO e não no nome dela.
252
268
  *
@@ -261,6 +277,24 @@ into) {
261
277
  const body = await readFile(file, "utf8").catch(() => "");
262
278
  sheets.push({ file, body });
263
279
  }
280
+ /**
281
+ * AS FOLHAS DO RESTO DO REPOSITÓRIO - lidas com a MESMA régua, e nunca com uma segunda.
282
+ * `deForaDoEscopo` marca quais são, porque elas só podem virar classe global: nem `css`, nem
283
+ * ledger, nem fragmento. Duas classificações da mesma folha seriam duas verdades.
284
+ */
285
+ const deForaDoEscopo = new Set();
286
+ if (alsoGlobalsFrom && alsoGlobalsFrom !== root) {
287
+ const dentro = `${root}${sep}`;
288
+ for await (const file of walkAll([alsoGlobalsFrom])) {
289
+ if (!/\.(css|scss|sass|less)$/i.test(file))
290
+ continue;
291
+ if (file === root || file.startsWith(dentro))
292
+ continue;
293
+ const body = await readFile(file, "utf8").catch(() => "");
294
+ deForaDoEscopo.add(file);
295
+ sheets.push({ file, body });
296
+ }
297
+ }
264
298
  /** Todo caminho que alguma folha importa, resolvido como o sass resolveria. */
265
299
  const imported = new Set();
266
300
  for (const sheet of sheets)
@@ -269,8 +303,15 @@ into) {
269
303
  imported.add(join(dirname(sheet.file), candidate));
270
304
  let css = "";
271
305
  const globals = [];
272
- for (const { file, body } of sheets) {
273
- css += `\n${body}`;
306
+ /** As de fora entram ANTES, para que uma classe do escopo ganhe dela no cascade do leitor. */
307
+ const ordenadas = [
308
+ ...sheets.filter((s) => deForaDoEscopo.has(s.file)),
309
+ ...sheets.filter((s) => !deForaDoEscopo.has(s.file)),
310
+ ];
311
+ for (const { file, body } of ordenadas) {
312
+ const fora = deForaDoEscopo.has(file);
313
+ if (!fora)
314
+ css += `\n${body}`;
274
315
  if (!body)
275
316
  continue;
276
317
  const partial = basename(file).startsWith("_");
@@ -280,8 +321,14 @@ into) {
280
321
  ? "dead"
281
322
  : "global";
282
323
  if (kind === "global")
283
- globals.push({ file: relative(root, file), body });
284
- if (!into)
324
+ globals.push({
325
+ /** O caminho de uma folha de fora se lê da RAIZ, senão ele sai cheio de `../`. */
326
+ file: relative(fora ? (alsoGlobalsFrom ?? root) : root, file),
327
+ body,
328
+ ...(fora ? { outside: true } : {}),
329
+ });
330
+ /** Uma folha de fora do escopo não vira fragmento: o ledger conta o que ELE mandou medir. */
331
+ if (fora || !into)
285
332
  continue;
286
333
  into.push({
287
334
  file: relative(root, file),
@@ -503,7 +550,7 @@ export async function takeCensus(root, opts) {
503
550
  useLiveCatalogue(fetched.ok ? asCatalogueTable(fetched.index) : null);
504
551
  /** Todo arquivo de estilo do escopo, para o ledger - ver o parâmetro `into`. */
505
552
  const styleFiles = [];
506
- const { css, globals: globalSheets } = await harvestOwnCss(root, styleFiles);
553
+ const { css, globals: globalSheets } = await harvestOwnCss(root, styleFiles, repoRoot);
507
554
  const table = buildTable({ css, source: "yours" });
508
555
  const schemes = parseSchemeBlocks(css);
509
556
  const reports = [];
@@ -577,6 +624,16 @@ export async function takeCensus(root, opts) {
577
624
  * escreva utilitário por atacado, e existe porque um censo que dobra de tamanho em silêncio é o
578
625
  * tipo de surpresa que ninguém pede.
579
626
  */
627
+ /**
628
+ * O QUE VEIO DE FORA DO ESCOPO É DITO, e não colhido em silêncio (dono, 19/09).
629
+ *
630
+ * Ele apontou uma pasta e a leitura foi buscar a definição de classe no resto do repositório. Isso
631
+ * melhora o número dele, e um número que melhora sem explicação é um número em que ninguém confia.
632
+ */
633
+ const deForaDoEscopo = globalSheets.filter((g) => g.outside).length;
634
+ if (deForaDoEscopo > 0) {
635
+ console.log(body(paint.faint(`${deForaDoEscopo} stylesheets outside the folder you pointed at were read for class definitions only - a class your components wear is resolved where it is defined, and nothing from there becomes a recipe.`)));
636
+ }
580
637
  if (globalClasses.size > MAX_GLOBAL_CLASSES) {
581
638
  console.log(body(paint.faint(`${globalClasses.size} classes in your global sheet, and ${globalClasses.size - MAX_GLOBAL_CLASSES} of them are not travelling with this measurement - the first ${MAX_GLOBAL_CLASSES} are.`)));
582
639
  }
@@ -2052,6 +2109,8 @@ export async function takeCensus(root, opts) {
2052
2109
  ...globalClassesClaimed,
2053
2110
  ...globalClassesWorn(looks, globalClasses),
2054
2111
  ]),
2112
+ /** Toda classe que a folha DELE define - ver `definedGlobal` em `judgeFragments`. */
2113
+ definedGlobal: new Set(globalClasses.keys()),
2055
2114
  islandRead: islandClassesRead,
2056
2115
  refused: new Set(skips.map((s) => s.file)),
2057
2116
  /**
@@ -2149,7 +2208,9 @@ export async function takeCensus(root, opts) {
2149
2208
  for (const layer of look.rawLayers ?? [])
2150
2209
  tokens.push(...layer.classes);
2151
2210
  if (tokens.length > 0) {
2152
- valueByComponent[lookName] = accountClasses(tokens, declaredValues, answeredTokens);
2211
+ valueByComponent[lookName] = accountClasses(tokens, declaredValues, answeredTokens,
2212
+ /** A MESMA resposta do julgamento por fragmento - ver `definedGlobal`. */
2213
+ new Set(globalClasses.keys()));
2153
2214
  }
2154
2215
  }
2155
2216
  const valuesTotal = sumAccounts(Object.values(valueByComponent));
@@ -2619,7 +2680,10 @@ export async function takeCensus(root, opts) {
2619
2680
  */
2620
2681
  ...(globalClasses.size > 0
2621
2682
  ? {
2622
- globalClasses: Object.fromEntries([...globalClasses.entries()].slice(0, MAX_GLOBAL_CLASSES)),
2683
+ globalClasses: Object.fromEntries(porNecessidade(globalClasses, new Set([
2684
+ ...globalClassesClaimed,
2685
+ ...globalClassesWorn(looks, globalClasses),
2686
+ ])).slice(0, MAX_GLOBAL_CLASSES)),
2623
2687
  }
2624
2688
  : {}),
2625
2689
  classStyle,
@@ -3786,6 +3850,11 @@ async function mostrarInspecao(c, root, transcrito) {
3786
3850
  */
3787
3851
  abertura: abertura(c, c.measured?.repo ?? basename(root)),
3788
3852
  aneis: aneis(c),
3853
+ /**
3854
+ * QUAIS SAO OS NAO LIDOS, LOGO ABAIXO DO ANEL QUE OS ANUNCIA (dono, 19/09) - e partidos pela
3855
+ * pergunta que decide se aquilo e' problema dele: veste um componente que ele entrega, ou nao?
3856
+ */
3857
+ naoLido: naoLido(c),
3789
3858
  cor: historiaDeCor(c),
3790
3859
  /**
3791
3860
  * O COMANDO NO TOPO E NO FIM (dono, 18/09): quem decidiu adotar no meio da leitura nao
@@ -529,6 +529,7 @@ elsewhere,
529
529
  */
530
530
  forms = []) {
531
531
  const wornGlobal = elsewhere?.wornGlobal ?? new Set();
532
+ const definedGlobal = elsewhere?.definedGlobal ?? new Set();
532
533
  const islandRead = elsewhere?.islandRead ?? new Map();
533
534
  const refused = elsewhere?.refused ?? new Set();
534
535
  const captured = elsewhere?.keyframes ?? new Set();
@@ -646,6 +647,7 @@ forms = []) {
646
647
  const readElsewhere = tokens.length > 0 &&
647
648
  tokens.every((tk) => STRUCTURE.has(tk) ||
648
649
  wornGlobal.has(tk) ||
650
+ definedGlobal.has(tk) ||
649
651
  islandRead.get(f.file)?.has(tk));
650
652
  if (readElsewhere)
651
653
  return { ...f, read: true };
@@ -105,7 +105,19 @@ const STRUCTURAL_TOKENS = new Set([
105
105
  * disputa o que o leitor resolve sozinho. Sem o conjunto (import fresco, triagem, julgamento por
106
106
  * fragmento) a conta é a mesma de sempre.
107
107
  */
108
- export function destinationOf(cls, declared, answered) {
108
+ export function destinationOf(cls, declared, answered,
109
+ /**
110
+ * AS CLASSES QUE A FOLHA DELE DEFINE - e é isto que fecha a conta com o julgamento por fragmento
111
+ * (dono, 19/09).
112
+ *
113
+ * Um nome como `icon-round` não é utilitário de escala nenhuma: ele é uma classe que o CSS do
114
+ * projeto declara, com as declarações dela na mão. Chamá-lo de não lido conta como perda uma
115
+ * coisa que a leitura entendeu inteira. Medido num repositório real: sem esta linha o ledger
116
+ * reporta 645 fragmentos com valor não lido, com ela 379 - e a conta do anel, que é esta função,
117
+ * ficava presa no número velho enquanto o capítulo já mostrava o novo. Dois números para a mesma
118
+ * pergunta na mesma página é o defeito que a página acabou de deixar de ter.
119
+ */
120
+ defined) {
109
121
  const { utility } = parseClass(cls);
110
122
  if (STRUCTURAL_TOKENS.has(utility))
111
123
  return "structure";
@@ -118,10 +130,14 @@ export function destinationOf(cls, declared, answered) {
118
130
  /** A grafia respondida é a do código dele - com o modificador fora, como `applyAnswers` casa. */
119
131
  if (answered && (answered.has(utility) || answered.has(cls)))
120
132
  return "answered";
133
+ if (defined?.has(cls) || defined?.has(utility))
134
+ return "interpreted";
121
135
  return "unread";
122
136
  }
123
137
  /** A conta de uma lista de tokens - a soma SEMPRE fecha com `seen`, por construção. */
124
- export function accountClasses(tokens, declared, answered) {
138
+ export function accountClasses(tokens, declared, answered,
139
+ /** As classes que a folha dele define - ver `destinationOf`. */
140
+ defined) {
125
141
  const account = {
126
142
  seen: 0,
127
143
  interpreted: 0,
@@ -132,7 +148,7 @@ export function accountClasses(tokens, declared, answered) {
132
148
  };
133
149
  for (const cls of tokens) {
134
150
  account.seen += 1;
135
- account[destinationOf(cls, declared, answered)] += 1;
151
+ account[destinationOf(cls, declared, answered, defined)] += 1;
136
152
  }
137
153
  return account;
138
154
  }
@@ -77,3 +77,22 @@ export function globalClassesWorn(looks, globals) {
77
77
  }
78
78
  return worn;
79
79
  }
80
+ /**
81
+ * QUAIS CLASSES VIAJAM QUANDO O TETO MORDE - e a ordem é a NECESSIDADE, nunca a do arquivo.
82
+ *
83
+ * O QUE O CLIENTE GANHA: o censo continua do mesmo tamanho e passa a levar as classes que os
84
+ * componentes dele REALMENTE vestem. Medido num repositório real: o repositório declara 815 classes
85
+ * globais e o teto deixa passar 300 - escolhidas por ordem de leitura de arquivo, o que fazia a
86
+ * classe de um componente entregue perder o lugar para a de um relatório de teste, e o valor dela
87
+ * aparecia como não lido do outro lado.
88
+ *
89
+ * VESTIDA PRIMEIRO, o resto na ordem em que foi lido. Não é um corte novo: é o mesmo corte
90
+ * escolhendo por quem precisa.
91
+ */
92
+ export function porNecessidade(classes, vestidas) {
93
+ const entradas = [...classes.entries()];
94
+ return [
95
+ ...entradas.filter(([nome]) => vestidas.has(nome)),
96
+ ...entradas.filter(([nome]) => !vestidas.has(nome)),
97
+ ];
98
+ }
@@ -815,7 +815,7 @@ export const CHECKER_SINCE = "0.16.424";
815
815
  * deveriam estar lá. Quem tem censo gravado precisa de um `sync` para a fila de trabalho dele ser a
816
816
  * real; ver o livro-razão de `corpus.spec.ts`.
817
817
  */
818
- export const READER_SINCE = "0.16.421";
818
+ export const READER_SINCE = "0.16.439";
819
819
  /**
820
820
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
821
821
  *
@@ -93,7 +93,7 @@ ${layout()}
93
93
  <div class="bar-fim">
94
94
  <p class="meta">${escapar(quando)} &middot; CLI ${escapar(cabecalho.cli)} &middot; nothing left this machine</p>${input.comando
95
95
  ? `
96
- <code class="comando">${escapar(input.comando)}</code>`
96
+ <a class="comando topo" href="${INSTALL_URL}" target="_blank" rel="noreferrer">${escapar(input.comando)}<i aria-hidden="true">\u2192</i></a>`
97
97
  : ""}
98
98
  </div>
99
99
  </div>
@@ -115,6 +115,7 @@ ${layout()}
115
115
  ${numeros}
116
116
  </div>
117
117
  ${desenharAneis(input.aneis ?? [])}
118
+ ${desenharNaoLido(input.naoLido)}
118
119
  ${desenharCor(input.cor)}
119
120
  </section>
120
121
 
@@ -208,6 +209,70 @@ function desenharAneis(aneis) {
208
209
  ${cartoes}
209
210
  </section>`;
210
211
  }
212
+ /**
213
+ * O CAPÍTULO QUE RESPONDE "QUAIS SÃO OS 8%", LOGO ABAIXO DO ANEL QUE OS ANUNCIA (dono, 19/09).
214
+ *
215
+ * O QUE O CLIENTE GANHA: ele para de procurar. O anel dizia que o resto estava nomeado "nos
216
+ * capítulos abaixo" e os capítulos contavam outra população - medido na página dele, 143 contra
217
+ * 1143. Aqui as duas contas se encontram, e o não lido chega partido pela única pergunta que muda
218
+ * o que ele faz em seguida: isto veste um componente que eu entrego, ou não?
219
+ *
220
+ * A PILHA DE FORA VEM COM AS PASTAS. Uma frase dizendo "não afeta seus componentes" pede confiança;
221
+ * `_local/bkp - 872` entrega a razão junto, e ele reconhece a própria pasta sem abrir arquivo.
222
+ *
223
+ * E O QUE NÃO DÁ PARA NOMEAR É DITO. Quando o anel conta mais decisões do que este capítulo mostra
224
+ * com arquivo e linha, a diferença aparece em uma frase - porque a promessa quebrada de nomear é
225
+ * exatamente o defeito que este capítulo existe para consertar.
226
+ */
227
+ function desenharNaoLido(n) {
228
+ if (!n || n.total === 0)
229
+ return "";
230
+ const semTexto = n.contagem.semTexto;
231
+ const reconciliacao = n.contagem.anel > 0
232
+ ? `Your components hold ${n.contagem.anel} style ${plural(n.contagem.anel, "decision")} this version did not read. Below is every unread fragment whose text we kept, split by the only question that changes what you do next: does it dress a component you ship?`
233
+ : `Below is every unread fragment whose text we kept, split by the only question that changes what you do next: does it dress a component you ship?`;
234
+ const lacuna = semTexto > 0
235
+ ? ` <p class="callout">${escapar(`${n.total} fragments were counted in all; ${semTexto} of them were counted without their text being kept, so for those this version can tell you how many and not which - that is our gap, not a shape of yours.`)}</p>\n`
236
+ : "";
237
+ const pastas = n.pastas.length > 0
238
+ ? ` <p class="pastas">${n.pastas
239
+ .map((p) => `<span><b>${escapar(p.pasta)}</b> ${p.fragmentos}</span>`)
240
+ .join("")}</p>\n`
241
+ : "";
242
+ return ` <section class="chapter nao-lido" id="not-read" style="--ordem:2">
243
+ <h2><span>What we could not read, and whether it touches you</span></h2>
244
+ <p class="callout">${escapar(reconciliacao)}</p>
245
+ ${lacuna} <div class="pilhas">
246
+ <div class="pilha toca">
247
+ <h3>On components you ship</h3>
248
+ <p class="pilha-conta"><b>${n.dentro.fragmentos}</b> ${plural(n.dentro.fragmentos, "fragment")}</p>
249
+ <p class="pilha-peso">${escapar(n.dentro.fragmentos === 0 ? "Nothing here is waiting on us: every style decision on the components you ship was read." : "This is the pile that still stands between your report and 100%, and closing it is reading work on our side - nothing for you to change.")}</p>
250
+ ${grupos(n.dentro.grupos)} </div>
251
+ <div class="pilha fora">
252
+ <h3>Outside what you ship</h3>
253
+ <p class="pilha-conta"><b>${n.fora.fragmentos}</b> ${plural(n.fora.fragmentos, "fragment")}</p>
254
+ <p class="pilha-peso">${escapar("These sit in files that dress no component in your system - a global sheet, a folder you do not ship, a component this scope does not admit. Reading them would not change the consistency of your design.")}</p>
255
+ ${pastas}${grupos(n.fora.grupos)} </div>
256
+ </div>
257
+ </section>`;
258
+ }
259
+ /** Os grupos de uma pilha: a frase do motivo, o tamanho, e as linhas que provam. */
260
+ function grupos(lista) {
261
+ if (lista.length === 0)
262
+ return "";
263
+ return `${lista
264
+ .map((g) => ` <div class="motivo">
265
+ <p><b>${g.fragmentos}</b> in ${g.arquivos} ${plural(g.arquivos, "file")} - ${escapar(g.porque)}</p>
266
+ ${g.exemplos
267
+ .map((e) => ` <code class="linha">${escapar(e.file)}${typeof e.line === "number" ? `:${e.line}` : ""} ${escapar(e.text)}</code>`)
268
+ .join("\n")}
269
+ </div>`)
270
+ .join("\n")}\n`;
271
+ }
272
+ /** Singular quando é um. Um relatório que escreve "1 files" perde a autoridade na primeira linha. */
273
+ function plural(n, palavra) {
274
+ return n === 1 ? palavra : `${palavra}s`;
275
+ }
211
276
  /**
212
277
  * A HISTÓRIA DAS CORES, com a paleta desenhada do lado - as cores DELA, nos valores dela.
213
278
  *
@@ -220,7 +285,7 @@ function desenharCor(cor) {
220
285
  const amostras = cor.paleta
221
286
  .map((c) => `<i style="--amostra:${escapar(c)}" title="${escapar(c)}"></i>`)
222
287
  .join("");
223
- return ` <section class="chapter cor" style="--ordem:2">
288
+ return ` <section class="chapter cor" style="--ordem:3">
224
289
  <h2><span>Your colours</span></h2>
225
290
  ${cor.linhas.map((l) => ` <p class="callout">${escapar(l)}</p>`).join("\n")}
226
291
  <div class="paleta" aria-hidden="true">${amostras}</div>
@@ -764,6 +829,60 @@ footer { margin-top: 40px; color: var(--ds-color-semantic-muted); font-size: 13p
764
829
  }
765
830
  .botao:hover { opacity: 0.9; }
766
831
 
832
+ /* O COMANDO DO TOPO E' UM BOTAO: ele abre a pagina de instalacao, entao nao se comporta como
833
+ texto para selecionar - e a seta diz que leva a algum lugar antes de alguem descobrir clicando. */
834
+ a.comando.topo {
835
+ display: inline-flex;
836
+ align-items: center;
837
+ gap: 8px;
838
+ text-decoration: none;
839
+ user-select: none;
840
+ }
841
+ a.comando.topo i { font-style: normal; color: var(--ds-color-semantic-accent); }
842
+ a.comando.topo:hover { border-color: var(--ds-color-semantic-accent); }
843
+
844
+ /* AS DUAS PILHAS DO NAO LIDO, LADO A LADO: a comparacao e' o conteudo, entao elas nao se empilham
845
+ enquanto houver largura - ler "isto me afeta" e "isto nao" em sequencia perde a comparacao. */
846
+ .pilhas { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 4px; }
847
+ @media (max-width: 860px) { .pilhas { grid-template-columns: 1fr; } }
848
+ .pilha {
849
+ border: 1px solid var(--ds-color-semantic-border);
850
+ border-radius: var(--ds-radius-lg, 12px);
851
+ padding: 18px 20px;
852
+ min-width: 0;
853
+ }
854
+ .pilha.toca { border-color: var(--ds-color-semantic-accent); }
855
+ .pilha h3 { margin: 0; font-size: 14px; letter-spacing: 0.04em; text-transform: uppercase; }
856
+ .pilha-conta { margin: 10px 0 6px; font-size: 14px; color: var(--ds-color-semantic-muted); }
857
+ .pilha-conta b { font-size: 26px; letter-spacing: -0.02em; color: var(--ds-color-semantic-foreground); }
858
+ .pilha-peso { margin: 0 0 14px; font-size: 14px; line-height: 1.55; color: var(--ds-color-semantic-muted); }
859
+ /* AS PASTAS: o reconhecimento vem antes da explicacao - ele ve a propria pasta e ja' sabe. */
860
+ .pastas { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
861
+ .pastas span {
862
+ font-family: ${mono};
863
+ font-size: 12px;
864
+ padding: 4px 10px;
865
+ border-radius: var(--ds-radius-sm, 8px);
866
+ background: var(--ds-color-semantic-canvas);
867
+ border: 1px solid var(--ds-color-semantic-border);
868
+ color: var(--ds-color-semantic-muted);
869
+ }
870
+ .pastas b { color: var(--ds-color-semantic-foreground); font-weight: 600; }
871
+ .motivo { margin: 0 0 14px; }
872
+ .motivo p { margin: 0 0 6px; font-size: 14px; line-height: 1.5; color: var(--ds-color-semantic-muted); }
873
+ .motivo p b { color: var(--ds-color-semantic-foreground); }
874
+ .linha {
875
+ display: block;
876
+ font-family: ${mono};
877
+ font-size: 12px;
878
+ line-height: 1.6;
879
+ padding: 3px 0 3px 12px;
880
+ border-left: 2px solid var(--ds-color-semantic-border);
881
+ color: var(--ds-color-semantic-muted);
882
+ white-space: pre-wrap;
883
+ word-break: break-word;
884
+ }
885
+
767
886
  .lista-passos { list-style: none; margin: 0; padding: 0; display: grid; gap: 22px; }
768
887
  .lista-passos li {
769
888
  border-left: 2px solid var(--ds-color-semantic-border);
@@ -39,7 +39,7 @@ export function aneis(c) {
39
39
  numero: `${pct(cl.interpreted, decisoes)}%`,
40
40
  parte: cl.interpreted,
41
41
  de: decisoes,
42
- frase: `Of the ${decisoes} style decisions in your components, we understood ${cl.interpreted}. The remaining ${decisoes - cl.interpreted} are shapes this version does not read yet - they are named in the chapters below, with the file and line.`,
42
+ frase: `Of the ${decisoes} style decisions in your components, we understood ${cl.interpreted}. The remaining ${decisoes - cl.interpreted} are shapes this version does not read yet - the chapter right below says where each one lives, and whether it touches a component you ship.`,
43
43
  });
44
44
  }
45
45
  return saida;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * O QUE A LEITURA NÃO ENTENDEU, RESPONDIDO NUMA TELA SÓ (dono, 19/09).
3
+ *
4
+ * O QUE O CLIENTE GANHA. O anel diz "92%" e manda procurar o resto "nos capítulos abaixo". Medido
5
+ * no relatório dele: o anel conta 143 decisões não lidas e o capítulo que deveria nomeá-las conta
6
+ * 1143 fragmentos - populações diferentes, e nenhuma frase ligando as duas. Quem quer saber quais
7
+ * são os 8% abre dezesseis capítulos e não acha. Aqui ele lê uma vez: quais, por quê, e se aquilo
8
+ * toca o código que ele entrega.
9
+ *
10
+ * A CLASSIFICAÇÃO É DERIVADA, NUNCA FIXADA. O que separa as duas pilhas é o que O PROJETO DELE
11
+ * declara: um fragmento está DENTRO quando o arquivo dele é o arquivo de um componente que o censo
12
+ * admite (`looks[X].api.file`), e FORA quando não é - folha global, pasta de backup, componente que
13
+ * o escopo não admite. Nenhum nome de pasta, de framework ou de convenção entra nesta decisão:
14
+ * num projeto que ninguém aqui viu, a lista de componentes é a dele, e a régua acompanha.
15
+ *
16
+ * POR QUE A SEPARAÇÃO É O PRODUTO, e não um detalhe de apresentação. "8% não lido" lê-se como
17
+ * dívida enquanto ninguém diz onde aquilo mora. A pilha de FORA não muda a consistência do design
18
+ * dele - são declarações em arquivos que não vestem componente nenhum -, e dizê-lo transforma o
19
+ * mesmo número em "pronto". A pilha de DENTRO é a única que ainda separa o relatório dos 100%, e é
20
+ * ela que vira trabalho de leitura do nosso lado.
21
+ */
22
+ /**
23
+ * OS DOIS MOTIVOS QUE JÁ DIZEM "FORA" SEM PRECISAR DO ARQUIVO.
24
+ *
25
+ * Um componente que o escopo não admite e uma folha que build nenhum compila estão fora do que ele
26
+ * entrega POR DEFINIÇÃO - e nos dois casos o arquivo pode até ser de componente, o que faria a
27
+ * regra de caminho classificá-los errado. O motivo é mais específico que o caminho, então ele ganha.
28
+ */
29
+ const FORA_POR_MOTIVO = new Set([
30
+ "component-not-admitted",
31
+ "sheet-not-imported",
32
+ ]);
33
+ export function naoLido(c) {
34
+ const grupos = c.ledger?.unread ?? [];
35
+ if (grupos.length === 0)
36
+ return null;
37
+ const deComponente = arquivosDeComponente(c);
38
+ const baldes = new Map();
39
+ const arquivos = { dentro: new Set(), fora: new Set() };
40
+ const porPasta = new Map();
41
+ let total = 0;
42
+ let comTexto = 0;
43
+ for (const g of grupos) {
44
+ const itens = (g.unreadable ?? []).filter((f) => f.file && f.text);
45
+ const mostrar = itens.length > 0 ? itens : (g.examples ?? []);
46
+ /**
47
+ * `uses` é a contagem do ledger e ela vale mais que o tamanho da lista: a lista tem teto, e
48
+ * preferir o comprimento dela encolheria o total do capítulo sem avisar ninguém.
49
+ */
50
+ total += typeof g.uses === "number" ? g.uses : mostrar.length;
51
+ const porque = g.because ?? g.reason ?? "not interpreted";
52
+ /**
53
+ * DOIS MOTIVOS JÁ DIZEM "FORA" SEM PRECISAR DO ARQUIVO, e eles ganham do caminho: um componente
54
+ * que o escopo não admite e uma folha que build nenhum compila estão fora do que ele entrega
55
+ * por definição, e nos dois casos o arquivo PODE ser de componente.
56
+ */
57
+ const foraPorMotivo = FORA_POR_MOTIVO.has(g.reason ?? "");
58
+ for (const f of mostrar) {
59
+ if (!f.file || !f.text)
60
+ continue;
61
+ comTexto += 1;
62
+ /**
63
+ * A PILHA É DECIDIDA POR FRAGMENTO, NUNCA PELO GRUPO. Medido no repositório dele: um grupo
64
+ * de 54 fragmentos tinha DOIS arquivos - um componente entregue e um `.jsx` numa pasta de
65
+ * backup -, e decidir pelo grupo levava os 54 inteiros para a pilha que toca componente. A
66
+ * pessoa lia "54 esperando por nós" sobre um arquivo que ela não entrega.
67
+ */
68
+ const fora = foraPorMotivo || !deComponente.has(comEscopo(c, f.file));
69
+ const chave = `${fora ? "fora" : "dentro"}\u0000${porque}`;
70
+ let balde = baldes.get(chave);
71
+ if (!balde) {
72
+ balde = { porque, fragmentos: 0, arquivos: 0, exemplos: [], fora };
73
+ baldes.set(chave, balde);
74
+ }
75
+ balde.fragmentos += 1;
76
+ arquivos[fora ? "fora" : "dentro"].add(f.file);
77
+ if (fora)
78
+ porPasta.set(pastaDe(f.file), (porPasta.get(pastaDe(f.file)) ?? 0) + 1);
79
+ if (balde.exemplos.length < (fora ? 3 : 6))
80
+ balde.exemplos.push({
81
+ file: f.file,
82
+ ...(typeof f.line === "number" ? { line: f.line } : {}),
83
+ text: f.text,
84
+ });
85
+ }
86
+ }
87
+ /** Quantos ARQUIVOS distintos cada grupo toca - contado do que ele realmente viu. */
88
+ for (const balde of baldes.values()) {
89
+ balde.arquivos = new Set(balde.exemplos.map((e) => e.file)).size;
90
+ }
91
+ const pilha = (fora) => {
92
+ const grupos = [...baldes.values()]
93
+ .filter((b) => b.fora === fora)
94
+ .map(({ fora: _, ...g }) => g)
95
+ .sort((a, b) => b.fragmentos - a.fragmentos);
96
+ return {
97
+ fragmentos: grupos.reduce((a, g) => a + g.fragmentos, 0),
98
+ arquivos: arquivos[fora ? "fora" : "dentro"].size,
99
+ grupos,
100
+ };
101
+ };
102
+ const cl = c.values?.classes;
103
+ const decisoes = (cl?.seen ?? 0) - (cl?.structure ?? 0);
104
+ return {
105
+ total,
106
+ dentro: pilha(false),
107
+ fora: pilha(true),
108
+ pastas: [...porPasta.entries()]
109
+ .map(([pasta, fragmentos]) => ({ pasta, fragmentos }))
110
+ .sort((a, b) => b.fragmentos - a.fragmentos)
111
+ .slice(0, 6),
112
+ contagem: {
113
+ anel: Math.max(0, decisoes - (cl?.interpreted ?? 0)),
114
+ nomeadas: comTexto,
115
+ semTexto: Math.max(0, total - comTexto),
116
+ },
117
+ };
118
+ }
119
+ /**
120
+ * OS ARQUIVOS QUE VESTEM UM COMPONENTE QUE O SISTEMA ENTREGA - e eles saem do censo, não de uma
121
+ * lista nossa. `looks` é o que aquele projeto declara ter; num projeto arbitrário essa é a única
122
+ * fonte que sabe quais arquivos importam.
123
+ */
124
+ function arquivosDeComponente(c) {
125
+ const out = new Set();
126
+ for (const look of Object.values(c.looks ?? {}))
127
+ if (look?.api?.file)
128
+ out.add(look.api.file);
129
+ return out;
130
+ }
131
+ /**
132
+ * O CAMINHO DO FRAGMENTO É RELATIVO AO ESCOPO MEDIDO e o do componente é relativo à raiz. Comparar
133
+ * os dois sem o prefixo não casa NENHUM - medido no censo do cliente: 0 casam sem ele, 4 com ele.
134
+ */
135
+ function comEscopo(c, file) {
136
+ return c.scope ? `${c.scope}/${file}` : file;
137
+ }
138
+ /** A pasta, com dois níveis: fundo o bastante para distinguir `_local/bkp` de `apps/landing`. */
139
+ function pastaDe(file) {
140
+ const partes = file.split("/");
141
+ if (partes.length <= 1)
142
+ return ".";
143
+ return partes.slice(0, Math.min(2, partes.length - 1)).join("/");
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.436",
3
+ "version": "0.16.439",
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": {