synthesisui 0.16.349 → 0.16.351

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * COMO ELE MONTA UMA PÁGINA - a régua única, num módulo só.
3
+ *
4
+ * O que o cliente ganha: o planejador de página oferece o esqueleto que ELE já usa - as peças
5
+ * dele, na ordem em que ele as escreve - em vez dos três layouts que NÓS escrevemos. Ver
6
+ * `INV-COLETA-12` em `contracts/camada-1-coleta.md`.
7
+ *
8
+ * POR QUE ISTO NÃO MORA EM `import.ts`: três lugares precisam da mesma resposta - a varredura do
9
+ * escopo, a varredura da evidência (`--usage`, que é onde as páginas moram em todo monorepo) e a
10
+ * FUSÃO de dois escopos medidos. Duas cópias da transcrição seriam duas réguas no dia em que uma
11
+ * fosse editada, e o teto declarado num arquivo e furado no outro seria um teto que não existe.
12
+ */
13
+ import { sketchOf } from "./doctor/sketch.js";
14
+ import { transcribe } from "./doctor/transcribe.js";
15
+ /**
16
+ * QUANTAS PÁGINAS VIAJAM NO CENSO.
17
+ *
18
+ * O teto existe pela mesma razão que o de `skipped`: num monorepo o número estoura e o censo
19
+ * viraria um dump do repositório. Medido em 01/09, o `frontend-hub` tem 145 arquivos de página e
20
+ * o nosso app 65, então 400 cabe as duas populações inteiras e o corte só aparece em repositório
21
+ * bem maior que os que a gente mede.
22
+ *
23
+ * E QUANDO ELE CORTAR, `pagesTotal` diz quantas eram. Uma tela que contasse `pages.length` diria
24
+ * 400 e pareceria completa, que é a forma mais barata de um corte silencioso mentir.
25
+ */
26
+ export const PAGES_MAX = 400;
27
+ /**
28
+ * UMA PÁGINA DELE, TRANSCRITA - a moldura da raiz e as peças que ela compõe.
29
+ *
30
+ * `from` VEM DO SKETCH e não de uma segunda leitura do arquivo: o nó já carrega o especificador
31
+ * que o import dele declara, e reabrir o arquivo para descobrir o mesmo fato é a falha de
32
+ * esteira que o dono nomeou em 01/08.
33
+ */
34
+ export function pageCompositionOf(rel, src, name, declaredValues) {
35
+ const sketch = sketchOf(src, name);
36
+ const at = sketch[0];
37
+ return {
38
+ file: rel,
39
+ root: at?.classes
40
+ ? transcribe(at.classes.split(/\s+/).filter(Boolean), declaredValues).base
41
+ : {},
42
+ composes: sketch
43
+ .filter((n) => /^[A-Z]/.test(n.tag))
44
+ .map((n) => ({
45
+ name: n.tag.split(".")[0],
46
+ ...(n.from ? { from: n.from } : {}),
47
+ depth: n.depth,
48
+ })),
49
+ };
50
+ }
51
+ /**
52
+ * AS PÁGINAS DE DOIS ESCOPOS MEDIDOS, SOMADAS - e o teto reaplicado sobre a soma.
53
+ *
54
+ * Cada escopo mediu arquivos DIFERENTES, então isto é soma e não desempate: nenhuma regra do
55
+ * primeiro-escopo-vence se aplica a um conjunto disjunto. O que se preserva é o denominador -
56
+ * `pagesTotal` conta quantas foram VISTAS em cada medição, inclusive as que aquela medição já
57
+ * havia cortado, senão a soma de dois cortes leria como o total do projeto.
58
+ */
59
+ export function mergePages(list) {
60
+ const all = list.flatMap((c) => c.pages ?? []);
61
+ if (all.length === 0)
62
+ return {};
63
+ const seen = list.reduce((n, c) => n + (c.pagesTotal ?? (c.pages ?? []).length), 0);
64
+ const pages = all.slice(0, PAGES_MAX);
65
+ return { pages, ...(seen > pages.length ? { pagesTotal: seen } : {}) };
66
+ }
@@ -3,6 +3,7 @@ import { basename, dirname, join, relative, sep } from "node:path";
3
3
  import { anatomyFromSketch } from "../anatomy-from-sketch.js";
4
4
  import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
5
5
  import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
6
+ import { PAGES_MAX, pageCompositionOf } from "../census-pages.js";
6
7
  import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
7
8
  import { declaredElsewhere } from "../declared-elsewhere.js";
8
9
  import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, packagingOf, proposeNewHome, resolvesAs, } from "../doctor/architecture.js";
@@ -68,17 +69,12 @@ import { walk, walkAll } from "./doctor.js";
68
69
  * nunca o nome do arquivo - e é isso que a faz valer no projeto de alguém que a gente nunca viu.
69
70
  */
70
71
  /**
71
- * QUANTAS PÁGINAS VIAJAM NO CENSO.
72
+ * O TETO DE PÁGINAS, re-exportado de onde a régua inteira mora - ver `census-pages.ts`.
72
73
  *
73
- * O teto existe pela mesma razão que o de `skipped`: num monorepo o número estoura e o censo
74
- * viraria um dump do repositório. Medido em 01/09, o `frontend-hub` tem 145 arquivos de página e
75
- * o nosso app 65, então 400 cabe as duas populações inteiras e o corte só aparece em repositório
76
- * bem maior que os que a gente mede.
77
- *
78
- * E QUANDO ELE CORTAR, `pagesTotal` diz quantas eram. Uma tela que contasse `pages.length` diria
79
- * 400 e pareceria completa, que é a forma mais barata de um corte silencioso mentir.
74
+ * Ele vive lá porque a FUSÃO de dois escopos precisa reaplicá-lo sobre a soma, e um teto
75
+ * declarado num arquivo e furado no outro é um teto que não existe.
80
76
  */
81
- export const PAGES_MAX = 400;
77
+ export { PAGES_MAX };
82
78
  /**
83
79
  * How many distinct values travel, PER KIND.
84
80
  *
@@ -770,26 +766,7 @@ export async function takeCensus(root, opts) {
770
766
  * que falta para alguém escrever a próxima (04/08).
771
767
  */
772
768
  if (verdict.why === "route" || verdict.why === "screen") {
773
- const sketch = sketchOf(src, d.name);
774
- const root = sketch[0];
775
- pages.push({
776
- file: rel,
777
- root: root?.classes
778
- ? transcribe(root.classes.split(/\s+/).filter(Boolean), declaredValues).base
779
- : {},
780
- /**
781
- * `from` VEM DO SKETCH e não de uma segunda leitura do arquivo: o nó já carrega o
782
- * especificador que o import dele declara, e reabrir o arquivo para descobrir o
783
- * mesmo fato é a falha de esteira que o dono nomeou em 01/08.
784
- */
785
- composes: sketch
786
- .filter((n) => /^[A-Z]/.test(n.tag))
787
- .map((n) => ({
788
- name: n.tag.split(".")[0],
789
- ...(n.from ? { from: n.from } : {}),
790
- depth: n.depth,
791
- })),
792
- });
769
+ pages.push(pageCompositionOf(rel, src, d.name, declaredValues));
793
770
  }
794
771
  skips.push({
795
772
  name: d.name,
@@ -1456,6 +1433,11 @@ export async function takeCensus(root, opts) {
1456
1433
  * not `reports`: an app's classes are choices made from a scale, and folding
1457
1434
  * them into the census would put the average back where the declaration goes.
1458
1435
  *
1436
+ * A ÚNICA COISA QUE ESTE LAÇO ESCREVE ALÉM DA CONTAGEM É `pages`, e a razão é que uma página
1437
+ * não é uma escolha de estilo: é a COMPOSIÇÃO dela - quais peças, em que ordem, sob que
1438
+ * moldura. Ela não entra em `defined`, não declara token e não vota em escala. Ver o bloco
1439
+ * `AS PÁGINAS MORAM AQUI` mais abaixo, com o número das duas populações.
1440
+ *
1459
1441
  * Deliberately its own tally so the LIBRARY QUESTION is asked of the scope
1460
1442
  * alone. Fold the app in first and every export has a count, `isLibrary` reads
1461
1443
  * false, and the crosswalk starts substituting the components it was just
@@ -1548,6 +1530,41 @@ export async function takeCensus(root, opts) {
1548
1530
  // source - so measuring nesting only on the defined loop reported zero
1549
1531
  // companions on a 128-component library (e2e, 01/08).
1550
1532
  readNesting(rel, src, nesting);
1533
+ /**
1534
+ * AS PÁGINAS MORAM AQUI, E ATÉ AGORA O CENSO NÃO GUARDAVA NENHUMA.
1535
+ *
1536
+ * `Census.pages` é como ELE monta uma tela - a moldura da raiz e a ordem das peças -, e é
1537
+ * o único dado a partir do qual alguém escreve a próxima página no vocabulário dele. Ele
1538
+ * só era colhido na varredura do ESCOPO, e o escopo é a biblioteca: uma `packages/ui` não
1539
+ * tem `app/` nem `pages/`, então a resposta era sempre zero. Medido em 01/09 nas duas
1540
+ * populações, com o mesmo `--scope`/`--usage` que o `import` e o `sync` passam:
1541
+ *
1542
+ * codelevel 0 páginas guardadas · 9 existem (apps/web 3, apps/landing 6)
1543
+ * frontend-hub 0 páginas guardadas · 156 existem (apps/web-dashboard)
1544
+ *
1545
+ * Isto NÃO afrouxa a lei da passagem de evidência. Uma página não entra em `defined`, não
1546
+ * doa token e não vota em escala: o que ela doa é COMPOSIÇÃO - quais peças dele seguram
1547
+ * quais telas -, que é exatamente a classe de fato que `--usage` existe para trazer.
1548
+ *
1549
+ * A régua é a mesma do escopo, de propósito: o portão recusa a rota (`route`/`screen`) e
1550
+ * é essa recusa que a identifica como página. Só o que o portão RECUSA por ser tela vira
1551
+ * página, então nada que já é componente é contado duas vezes.
1552
+ */
1553
+ if (/\.(tsx|jsx)$/i.test(rel)) {
1554
+ for (const d of scanDefinitions(rel, src)) {
1555
+ const verdict = gateComponent({
1556
+ name: d.name,
1557
+ file: rel,
1558
+ source: src,
1559
+ internal: [...shared, ...uInternal],
1560
+ });
1561
+ if (verdict.ok)
1562
+ continue;
1563
+ if (verdict.why !== "route" && verdict.why !== "screen")
1564
+ continue;
1565
+ pages.push(pageCompositionOf(rel, src, d.name, declaredValues));
1566
+ }
1567
+ }
1551
1568
  // Which PROJECT composes it - the credibility panel's number, per root.
1552
1569
  for (const m of src.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)) {
1553
1570
  const at = projectsOf.get(m[1]) ?? new Set();
@@ -2000,6 +2017,21 @@ export async function takeCensus(root, opts) {
2000
2017
  ]),
2001
2018
  islandRead: islandClassesRead,
2002
2019
  refused: new Set(skips.map((s) => s.file)),
2020
+ /**
2021
+ * AS ANIMAÇÕES QUE O CENSO CAPTUROU - ver o veredito em `judgeFragments`.
2022
+ *
2023
+ * `keyframes` é o objeto que já viaja no censo e que o compilador emite. Um frame de uma
2024
+ * delas chegou por essa porta, então cobrá-lo como lacuna conta a mesma coisa duas vezes.
2025
+ * A LISTA É A MESMA FUSÃO QUE O CENSO GRAVA, e não a metade de componente.
2026
+ *
2027
+ * `keyframes` sozinho é o que os componentes declaram; `globalKeyframes` é a folha dele, e
2028
+ * é ali que os 35 do codelevel moram - o censo funde os dois na hora de escrever. Ler só o
2029
+ * primeiro aqui declarava capturado um conjunto VAZIO e o conserto não movia nada: a
2030
+ * primeira medição depois de ligar isto deu exatamente os mesmos 130 de antes.
2031
+ *
2032
+ * Se a captura falhar, o frame volta a ser lacuna sozinho, que é a resposta certa.
2033
+ */
2034
+ keyframes: new Set(Object.keys({ ...globalKeyframes, ...keyframes })),
2003
2035
  },
2004
2036
  /**
2005
2037
  * AS FORMAS QUE ELE DECLAROU - e é aqui que a declaração dele muda o veredito.
@@ -302,12 +302,24 @@ kind) {
302
302
  continue;
303
303
  }
304
304
  const selector = selectorOf(open);
305
+ /**
306
+ * DE QUAL ANIMAÇÃO ESTE FRAME É - ver `Fragment.inKeyframe`.
307
+ *
308
+ * `selectorOf` devolve `0%`, porque o bloco do frame é um seletor comum e o at-rule só
309
+ * vence quando não há nenhum. O nome fica na pilha e é a única coisa que liga esta
310
+ * declaração ao keyframe que o censo capturou.
311
+ */
312
+ const inKeyframe = open
313
+ .map((b) => /^@keyframes\s+([^\s{]+)/.exec(b)?.[1])
314
+ .filter(Boolean)
315
+ .pop();
305
316
  out.push({
306
317
  shape: "css",
307
318
  file,
308
319
  line: i + 1,
309
320
  text: `${selector ? `${selector} ` : ""}{ ${m[1]}: ${m[2].trim()} }`,
310
321
  read: false,
322
+ ...(inKeyframe ? { inKeyframe } : {}),
311
323
  ...(kind === "global"
312
324
  ? { reason: "shape-not-read" }
313
325
  : kind === "dead"
@@ -440,7 +452,30 @@ forms = []) {
440
452
  const wornGlobal = elsewhere?.wornGlobal ?? new Set();
441
453
  const islandRead = elsewhere?.islandRead ?? new Map();
442
454
  const refused = elsewhere?.refused ?? new Set();
455
+ const captured = elsewhere?.keyframes ?? new Set();
443
456
  return seen.map((f) => {
457
+ /**
458
+ * UM FRAME DE UM KEYFRAME CAPTURADO CHEGOU - a mesma família de `wornGlobal` logo abaixo, e a
459
+ * mesma família de `INV-INTERP-15`: a régua acusava um fato que já estava no censo.
460
+ *
461
+ * `0% { opacity: 0 }` não tem leitor de RECEITA e nunca vai ter - um frame não é a decisão de
462
+ * um componente, é um instante de uma animação. Mas a animação inteira viaja em
463
+ * `census.keyframes`, com todos os seus frames, e o compilador a emite. Cobrar o frame como
464
+ * lacuna é contar duas vezes a mesma coisa e chamar a segunda de perda.
465
+ *
466
+ * MEDIDO EM 02/09, nas duas populações e com o mesmo `--scope`/`--usage` do produto:
467
+ *
468
+ * codelevel 35 keyframes capturados · 87 de 105 declarações sem leitor eram frames (83%)
469
+ * frontend-hub 1 keyframe capturado · 0 de 6 eram frames (0%)
470
+ *
471
+ * As duas discordam por larga margem, e é isso que decide o desenho: a pergunta não é sobre
472
+ * volume, é sobre a porta por onde a declaração passou. O nome tem que estar CAPTURADO - um
473
+ * frame de uma animação que o censo não colheu continua sendo lacuna, porque ali ela é real.
474
+ */
475
+ if (f.inKeyframe && captured.has(f.inKeyframe)) {
476
+ const { reason: _unread, ...rest } = f;
477
+ return { ...rest, read: true };
478
+ }
444
479
  /**
445
480
  * A REGRA DE CLASSE GLOBAL QUE ALGUÉM VESTE FOI LIDA - antes do desvio de
446
481
  * `binding`, porque lida é mais forte que ligada. `.root { isolation: isolate }`
@@ -532,7 +532,42 @@ export const CHECKER_SINCE = "0.16.308";
532
532
  * si, o campo fica ausente, e ausente ali continua sendo a resposta certa - o `codelevel-ui` dá
533
533
  * 0 de 0 contra 145 arquivos de página do `frontend-hub`.
534
534
  */
535
- export const READER_SINCE = "0.16.348";
535
+ /**
536
+ * 0.16.349 -> 0.16.350 em 01/09 (`INV-COLETA-12`, a metade do MONOREPO): as páginas do app dele
537
+ * passam a chegar ao censo mesmo quando a leitura foi apontada para a biblioteca.
538
+ *
539
+ * O QUE ELE GANHA COM O `sync`: a linha *"N of your components hold up M pages"* - quais peças
540
+ * dele são a espinha das telas dele. Quem roda `--scope packages/ui --usage apps/web` recebia
541
+ * ZERO página e o relatório calava as duas seções que falam delas.
542
+ *
543
+ * A CORREÇÃO DE UMA FRASE QUE ESTAVA AQUI: "ausente continua sendo a resposta certa para quem
544
+ * apontou a leitura para uma biblioteca" só valia para quem NÃO passou `--usage`. As páginas
545
+ * moram no app, e a varredura da evidência lia cada um desses arquivos para contagem e lei e
546
+ * passava direto pela composição. Medido em 01/09, com o mesmo `--scope`/`--usage` que o
547
+ * `import` e o `sync` passam:
548
+ *
549
+ * codelevel 0 páginas guardadas · 9 existem (apps/web 3, apps/landing 6)
550
+ * frontend-hub 0 páginas guardadas · 156 existem (apps/web-dashboard)
551
+ *
552
+ * QUEM NÃO É AFETADO: quem mede um app inteiro sem escopo - esse caminho já carregava as páginas
553
+ * desde 0.16.348. E quem aponta para uma biblioteca sem `--usage`: ela não compõe telas dentro de
554
+ * si, o campo segue ausente, e ali a ausência é a resposta certa de verdade.
555
+ */
556
+ /**
557
+ * 0.16.350 -> 0.16.351 em 02/09 (`INV-COLETA-16`): um frame de uma animação que o censo CAPTUROU
558
+ * deixa de ser contado como declaração sem leitor.
559
+ *
560
+ * O QUE ELE GANHA COM O `sync`: o número de cobertura para de contar como perdido o que a
561
+ * plataforma leu inteiro. `0% { opacity: 0 }` nunca vai ter leitor de RECEITA - um frame não é a
562
+ * decisão de um componente -, mas a animação viaja em `census.keyframes` e o compilador a emite.
563
+ *
564
+ * codelevel 35 keyframes capturados · shape-not-read 130 -> 18 · CSS lido 35% -> 88%
565
+ * frontend-hub 1 keyframe capturado · shape-not-read 6 -> 6 · nada muda
566
+ *
567
+ * QUEM NÃO É AFETADO: quem não escreve `@keyframes`. E quem escreve um que a captura NÃO alcançou -
568
+ * ali o frame continua sendo lacuna, porque ali a perda é real.
569
+ */
570
+ export const READER_SINCE = "0.16.351";
536
571
  /**
537
572
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
538
573
  *
@@ -1,3 +1,4 @@
1
+ import { mergePages } from "./census-pages.js";
1
2
  import { gapReason } from "./doctor/architecture.js";
2
3
  const asRecord = (v) => (v ?? {});
3
4
  /** Nome do escopo para as mensagens - `.` quando a medição não escopou nada. */
@@ -296,6 +297,25 @@ export function mergeCensus(list) {
296
297
  out.conventions = conventions;
297
298
  if (architectures && architectures.length > 0)
298
299
  out.architectures = architectures;
300
+ /**
301
+ * AS PÁGINAS DOS ESCOPOS SOMAM - ver `mergePages` em `census-pages.ts`.
302
+ *
303
+ * `out` nasce de um spread do PRIMEIRO censo e depois é montado campo a campo, então `pages`
304
+ * do primeiro escopo viajava de carona e a dos SEGUINTES caía inteira, sem uma linha dizendo
305
+ * isso. Com um escopo só o defeito não aparece (a fusão de uma lista devolve o próprio censo),
306
+ * e é por isso que ele sobreviveu: o caminho que o expõe é `--scope A --scope B`, que existe
307
+ * justamente para o sistema que mora em dois pacotes.
308
+ *
309
+ * `pagesTotal` também precisava ser recontado: herdado do primeiro, ele descrevia uma medição
310
+ * e era lido como o total do projeto.
311
+ */
312
+ const merged = mergePages(list);
313
+ delete out.pages;
314
+ delete out.pagesTotal;
315
+ if (merged.pages)
316
+ out.pages = merged.pages;
317
+ if (merged.pagesTotal)
318
+ out.pagesTotal = merged.pagesTotal;
299
319
  if (gaps.length > 0)
300
320
  out.architectureGaps = gaps;
301
321
  if (newComponentHome)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.349",
3
+ "version": "0.16.351",
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": {