synthesisui 0.16.306 → 0.16.308

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.
@@ -19,7 +19,9 @@ import { keyframeOffsets, moduleImports, partialCandidates, readGlobalClasses, r
19
19
  import { dataContract } from "../doctor/data-contract.js";
20
20
  import { inherit, readDeclaredForms, } from "../doctor/declared-forms.js";
21
21
  import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
22
+ import { accountExportsInto, emptyExportAccount, exportInvariantHolds, } from "../doctor/export-census.js";
22
23
  import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
24
+ import { useFrameworkMajor } from "../doctor/framework-palette.js";
23
25
  import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
24
26
  import { pageLaws } from "../doctor/page-laws.js";
25
27
  import { callSiteNode, placeLayers, } from "../doctor/place-layers.js";
@@ -34,6 +36,7 @@ import { buildLedger, describeLedger, } from "../doctor/style-ledger.js";
34
36
  import { MUI_DEFAULT_SPACING, readStyledComponents, spacingOf, } from "../doctor/style-props.js";
35
37
  import { buildTable } from "../doctor/tokens.js";
36
38
  import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
39
+ import { accountClasses, invariantHolds, sumAccounts, } from "../doctor/value-ledger.js";
37
40
  import { transcribeVariants } from "../doctor/variant-read.js";
38
41
  import { frontierKind, packageRoot } from "../frontier-kind.js";
39
42
  import { keyframesInSheets } from "../global-keyframes.js";
@@ -50,6 +53,7 @@ import { repoStateOf } from "../repo-state.js";
50
53
  import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
51
54
  import { detectStack, resolveDeps, stackVersions } from "../stack.js";
52
55
  import { wiredSlugs, wiringWarning } from "../wired-slugs.js";
56
+ import { harvestWorkspaceCss } from "../workspace-css.js";
53
57
  import { placeInWorkspace } from "../workspace-place.js";
54
58
  import { add } from "./add.js";
55
59
  import { walk, walkAll } from "./doctor.js";
@@ -456,6 +460,16 @@ export async function takeCensus(root, opts) {
456
460
  const fragments = [];
457
461
  /** Arquivos de estilo que pertencem a componente admitido - o veredito do `css` sai daqui. */
458
462
  const admittedStyles = new Set();
463
+ /** O denominador de exports antes do portão - ver `export-census.ts` (item 13). */
464
+ const exportAccount = emptyExportAccount();
465
+ /**
466
+ * A VERSÃO DO FRAMEWORK, ANTES DA VARREDURA - a paleta publicada muda de língua na maior
467
+ * (v4 é oklch, v3 é hex), e o resolvedor é chamado fundo demais para receber mais um
468
+ * parâmetro. Hoisted de depois da varredura, onde só os signals a usavam.
469
+ */
470
+ const versions = await resolveDeps(root).catch(() => ({}));
471
+ useFrameworkMajor(Number.parseInt(/\d+/.exec(versions.tailwindcss ?? "")?.[0] ?? "", 10) ||
472
+ null);
459
473
  /** What the gate turned away, with the reason. Never dropped in silence. */
460
474
  const skips = [];
461
475
  // Kept for the reference check below, which needs every file at once: a name
@@ -468,6 +482,16 @@ export async function takeCensus(root, opts) {
468
482
  if (!declaredValues.has(m[1]))
469
483
  declaredValues.set(m[1], m[2].trim());
470
484
  }
485
+ /**
486
+ * O VOCABULÁRIO DO PACOTE AO LADO (item 19) - ver `workspace-css.ts`. Genérico por decisão do
487
+ * dono (25/08): qualquer pacote de WORKSPACE que o escopo importa, nunca `node_modules` e nunca
488
+ * um nome cravado. O escopo dele sempre vence (a trava é o `scopeDeclares`); no repositório
489
+ * real, 223 valores (37% do não-lido) vestiam variáveis declaradas um pacote ao lado.
490
+ */
491
+ const workspace = await harvestWorkspaceCss(root, (name) => declaredValues.has(name)).catch(() => ({ packages: [], declared: new Map() }));
492
+ for (const [name, value] of workspace.declared) {
493
+ declaredValues.set(name, value);
494
+ }
471
495
  /**
472
496
  * AS REGRAS DE CLASSE DAS FOLHAS GLOBAIS - ver `readGlobalClasses`. Uma regra que um
473
497
  * componente admitido veste é a receita dele; `claimed` guarda quais foram vestidas,
@@ -634,7 +658,14 @@ export async function takeCensus(root, opts) {
634
658
  screensOf.set(m[1], at);
635
659
  }
636
660
  const found = [];
637
- for (const d of scanDefinitions(rel, src)) {
661
+ const defs = scanDefinitions(rel, src);
662
+ /**
663
+ * O DENOMINADOR ANTES DO PORTÃO - ver `export-census.ts` (item 13). Todo nome que o scan
664
+ * produziu TEM destino (admitido ou recusado); o contador mede o que sobra fora do funil.
665
+ */
666
+ const openedNames = new Set(defs.map((d) => d.name));
667
+ accountExportsInto(exportAccount, rel, src, (n) => openedNames.has(n));
668
+ for (const d of defs) {
638
669
  const verdict = gateComponent({
639
670
  name: d.name,
640
671
  file: rel,
@@ -1952,7 +1983,6 @@ export async function takeCensus(root, opts) {
1952
1983
  * versão (medido 16/08). `resolveDeps` sobe até a raiz do workspace e anda para os irmãos que os
1953
1984
  * `workspaces` declaram, que é onde recharts/chakra/mui realmente moram.
1954
1985
  */
1955
- const versions = await resolveDeps(root).catch(() => ({}));
1956
1986
  finishSignals(signals, importTally, versions);
1957
1987
  // Every name their CSS defines, from the same harvest the token table came
1958
1988
  // from - so a reference is judged against what actually exists, not against
@@ -1961,6 +1991,9 @@ export async function takeCensus(root, opts) {
1961
1991
  for (const m of css.matchAll(/(--[a-zA-Z0-9_-]+)\s*:/g)) {
1962
1992
  declaredNames.add(m[1]);
1963
1993
  }
1994
+ /** O pacote ao lado também declara (item 19) - sem isto a ref dele viraria "quebrada". */
1995
+ for (const name of workspace.declared.keys())
1996
+ declaredNames.add(name);
1964
1997
  /**
1965
1998
  * E O QUE ELE DECLARA FORA DO CSS - ver `runtimeDeclaredVars`.
1966
1999
  *
@@ -2105,6 +2138,25 @@ export async function takeCensus(root, opts) {
2105
2138
  const target = c?.canonical ?? (c?.bucket === "exclusive" ? c.name : null);
2106
2139
  return target ? safePartName(target) : null;
2107
2140
  }, (pkg) => versions[pkg]);
2141
+ /**
2142
+ * A CONTABILIDADE POR VALOR, contada AQUI porque o material é daqui: os tokens de classe que
2143
+ * vestem cada componente admitido (os nós do sketch e as camadas condicionais cruas), contra
2144
+ * os tokens que o CSS dele declara. Ver `Census.values` e `doctor/value-ledger.ts` - é a
2145
+ * reconciliação do item 11, e a soma fecha com `seen` por construção.
2146
+ */
2147
+ const valueByComponent = {};
2148
+ for (const [lookName, look] of Object.entries(looks)) {
2149
+ const tokens = [];
2150
+ for (const node of look.sketch ?? []) {
2151
+ tokens.push(...(node.classes ?? "").split(/\s+/).filter(Boolean));
2152
+ }
2153
+ for (const layer of look.rawLayers ?? [])
2154
+ tokens.push(...layer.classes);
2155
+ if (tokens.length > 0) {
2156
+ valueByComponent[lookName] = accountClasses(tokens, declaredValues);
2157
+ }
2158
+ }
2159
+ const valuesTotal = sumAccounts(Object.values(valueByComponent));
2108
2160
  return {
2109
2161
  census: 1,
2110
2162
  project: {
@@ -2261,6 +2313,44 @@ export async function takeCensus(root, opts) {
2261
2313
  },
2262
2314
  }
2263
2315
  : {}),
2316
+ /**
2317
+ * A conta por VALOR - ver o tipo. Ausente quando nenhum componente veste classe, e ausente
2318
+ * quando a soma não fecha: uma conta que viola a própria invariante é bug NOSSO, e um censo
2319
+ * sem o campo é honesto enquanto um campo errado governaria telas. Hoje ela fecha por
2320
+ * construção; o portão existe para o dia em que uma derivação (a engine do D3) não fechar.
2321
+ */
2322
+ ...(valuesTotal.seen > 0 && invariantHolds(valuesTotal)
2323
+ ? {
2324
+ values: {
2325
+ classes: { ...valuesTotal, byComponent: valueByComponent },
2326
+ },
2327
+ }
2328
+ : {}),
2329
+ /**
2330
+ * O DENOMINADOR ANTES DO PORTÃO - ver `export-census.ts` (item 13). Com ele, "o portão pega
2331
+ * 100%" vira aritmética conferível: candidatos têm destino no gate, formas deliberadas têm
2332
+ * contagem, e o resíduo tem LINHA. Mesmo padrão do `values`: a soma que não fecha não é
2333
+ * emitida - censo sem o campo é mais honesto que campo errado.
2334
+ */
2335
+ ...(exportAccount.seen > 0 && exportInvariantHolds(exportAccount)
2336
+ ? {
2337
+ exports: {
2338
+ seen: exportAccount.seen,
2339
+ byForm: exportAccount.byForm,
2340
+ ...(exportAccount.unopened.length > 0
2341
+ ? { unopened: exportAccount.unopened.slice(0, 512) }
2342
+ : {}),
2343
+ },
2344
+ }
2345
+ : {}),
2346
+ /**
2347
+ * DE ONDE VEIO O VOCABULÁRIO EMPRESTADO (item 19) - o pacote workspace e quantas variáveis
2348
+ * ele doou. Viaja para a tela poder dizer "estes nomes moram em `@acme/ui`" em vez de o
2349
+ * cliente procurar no escopo uma declaração que não está lá.
2350
+ */
2351
+ ...(workspace.packages.length > 0
2352
+ ? { workspaceCss: { packages: workspace.packages } }
2353
+ : {}),
2264
2354
  ...(architectures.length > 0
2265
2355
  ? {
2266
2356
  architectures: architectures.slice(0, 6).map((a) => ({
@@ -434,6 +434,43 @@ export function tallyToInventory(tally, max = 80) {
434
434
  .sort((a, b) => b.files - a.files || b.count - a.count)
435
435
  .slice(0, max);
436
436
  }
437
+ /**
438
+ * OS PACOTES DO WORKSPACE, com nome E pasta - a metade que `internalSpecifiers` jogava fora.
439
+ *
440
+ * A mesma caminhada (até 4 níveis acima, grupos `packages`/`libs`/`apps`), extraída para UMA
441
+ * fonte: o item 19 precisa da PASTA de cada pacote para colher o CSS dele, e uma segunda cópia
442
+ * da regra de descoberta é como duas descobertas começam a discordar.
443
+ */
444
+ export async function workspacePackages(root) {
445
+ const out = new Map();
446
+ for (let up = 0, dir = root; up < 4; up++) {
447
+ for (const group of ["packages", "libs", "apps"]) {
448
+ for (const e of await readdir(join(dir, group), {
449
+ withFileTypes: true,
450
+ }).catch(() => [])) {
451
+ if (!e.isDirectory())
452
+ continue;
453
+ const raw = await readFile(join(dir, group, e.name, "package.json"), "utf8").catch(() => null);
454
+ if (!raw)
455
+ continue;
456
+ try {
457
+ const name = JSON.parse(raw).name;
458
+ if (typeof name === "string" && name && !out.has(name)) {
459
+ out.set(name, join(dir, group, e.name));
460
+ }
461
+ }
462
+ catch {
463
+ // manifesto ilegível custa aquele pacote, nunca a descoberta
464
+ }
465
+ }
466
+ }
467
+ const parent = join(dir, "..");
468
+ if (parent === dir)
469
+ break;
470
+ dir = parent;
471
+ }
472
+ return [...out.entries()].map(([name, dir]) => ({ name, dir }));
473
+ }
437
474
  /**
438
475
  * Specifier prefixes this workspace owns: the aliases its tsconfig declares and
439
476
  * the names of its sibling packages. Everything else that is not relative comes
@@ -459,29 +496,13 @@ export async function internalSpecifiers(root) {
459
496
  // unreadable config costs the aliases, not the run
460
497
  }
461
498
  }
462
- for (const group of ["packages", "libs", "apps"]) {
463
- for (const e of await readdir(join(dir, group), {
464
- withFileTypes: true,
465
- }).catch(() => [])) {
466
- if (!e.isDirectory())
467
- continue;
468
- const raw = await readFile(join(dir, group, e.name, "package.json"), "utf8").catch(() => null);
469
- if (!raw)
470
- continue;
471
- try {
472
- const name = JSON.parse(raw).name;
473
- if (typeof name === "string" && name)
474
- out.add(name);
475
- }
476
- catch {
477
- // same
478
- }
479
- }
480
- }
481
499
  const parent = join(dir, "..");
482
500
  if (parent === dir)
483
501
  break;
484
502
  dir = parent;
485
503
  }
504
+ /** Os nomes dos irmãos vêm da MESMA descoberta que o item 19 usa - uma fonte, nunca duas. */
505
+ for (const pkg of await workspacePackages(root))
506
+ out.add(pkg.name);
486
507
  return [...out];
487
508
  }
@@ -0,0 +1,111 @@
1
+ /** `export function X` / `export const x =` / `export default class X` - o nome inline. */
2
+ const NAMED = /export\s+(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?|class|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/g;
3
+ /** `export type X` / `export interface X` - vocabulário, não peça. */
4
+ const TYPE_EXPORT = /export\s+(?:type|interface)\s+[A-Za-z_$]/g;
5
+ /** `export { A, B } from "x"` e `export * from "x"` - repasse. */
6
+ const RE_EXPORT = /export\s*(?:\*|\{[^}]*\})\s*from\s*["']/g;
7
+ /** `export { A, B }` SEM from - nomes declarados neste arquivo, exportados em lista. */
8
+ const LIST_EXPORT = /export\s*\{([^}]*)\}(?!\s*from)/g;
9
+ /** `export default (…)=>` / `export default function (` / objeto/literal - sem nome legível. */
10
+ const ANONYMOUS_DEFAULT = /export\s+default\s+(?:(?:React\.)?(?:memo|forwardRef)\s*\(\s*)*(?:async\s+)?(?:\(|function\s*\(|\{|\[|`|['"\d])/g;
11
+ /** `export default Name;` - com nome; o funil decide se é definição ou repasse. */
12
+ const NAMED_DEFAULT = /export\s+default\s+(?:(?:React\.)?(?:memo|forwardRef)\s*\(\s*)*([A-Za-z_$][\w$]*)\s*[;)\n]/g;
13
+ /** O nome é construído NESTE arquivo? - a mesma pergunta do scanner de definições. */
14
+ function declaresName(source, name) {
15
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16
+ return new RegExp(`(?:^|\\n)\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?(?:function|class|const|let|var)\\s+${escaped}\\b`).test(source);
17
+ }
18
+ const count = (account, form) => {
19
+ account.seen += 1;
20
+ account.byForm[form] = (account.byForm[form] ?? 0) + 1;
21
+ };
22
+ /**
23
+ * Conta os exports de UM arquivo de componente, contra o que o funil abriu dele.
24
+ *
25
+ * `opened` responde se um nome virou candidato (admitido OU recusado - os dois são destino).
26
+ * Muta `account` em vez de devolver, porque a soma do censo é a soma dos arquivos e um merge de
27
+ * records a cada arquivo é custo quadrático num monorepo de 966.
28
+ */
29
+ export function accountExportsInto(account, file, source, opened) {
30
+ if (!/\.(tsx|jsx|vue|svelte)$/i.test(file))
31
+ return;
32
+ const push = (line) => {
33
+ /** Teto por censo, aplicado por quem monta - aqui só não duplica dentro do arquivo. */
34
+ if (!account.unopened.some((u) => u.file === line.file && u.name === line.name)) {
35
+ account.unopened.push(line);
36
+ }
37
+ };
38
+ NAMED.lastIndex = 0;
39
+ for (const m of source.matchAll(NAMED)) {
40
+ const name = m[1];
41
+ if (!/^[A-Z]/.test(name)) {
42
+ count(account, "helper");
43
+ continue;
44
+ }
45
+ if (opened(name)) {
46
+ count(account, "candidate");
47
+ continue;
48
+ }
49
+ count(account, "unopened");
50
+ push({ file, name, form: "unopened" });
51
+ }
52
+ TYPE_EXPORT.lastIndex = 0;
53
+ for (const _ of source.matchAll(TYPE_EXPORT))
54
+ count(account, "type");
55
+ RE_EXPORT.lastIndex = 0;
56
+ for (const _ of source.matchAll(RE_EXPORT))
57
+ count(account, "re-export");
58
+ LIST_EXPORT.lastIndex = 0;
59
+ for (const m of source.matchAll(LIST_EXPORT)) {
60
+ for (const raw of m[1].split(",")) {
61
+ /** `Card as default` conta pelo nome local; o scanner de definições já lê essa grafia. */
62
+ const name = raw
63
+ .trim()
64
+ .split(/\s+as\s+/)[0]
65
+ ?.trim();
66
+ if (!name || !/^[A-Za-z_$]/.test(name))
67
+ continue;
68
+ if (!/^[A-Z]/.test(name)) {
69
+ count(account, "helper");
70
+ continue;
71
+ }
72
+ if (!declaresName(source, name)) {
73
+ count(account, "re-export");
74
+ continue;
75
+ }
76
+ if (opened(name)) {
77
+ count(account, "candidate");
78
+ continue;
79
+ }
80
+ count(account, "export-list");
81
+ push({ file, name, form: "export-list" });
82
+ }
83
+ }
84
+ ANONYMOUS_DEFAULT.lastIndex = 0;
85
+ for (const _ of source.matchAll(ANONYMOUS_DEFAULT)) {
86
+ count(account, "anonymous-default");
87
+ push({ file, form: "anonymous-default" });
88
+ }
89
+ NAMED_DEFAULT.lastIndex = 0;
90
+ for (const m of source.matchAll(NAMED_DEFAULT)) {
91
+ const name = m[1];
92
+ if (!declaresName(source, name)) {
93
+ count(account, "re-export");
94
+ continue;
95
+ }
96
+ /** Declarado aqui: ou o funil o abriu (candidato), ou é resíduo com linha. */
97
+ if (opened(name)) {
98
+ count(account, "candidate");
99
+ continue;
100
+ }
101
+ count(account, "unopened");
102
+ push({ file, name, form: "unopened" });
103
+ }
104
+ }
105
+ export function emptyExportAccount() {
106
+ return { seen: 0, byForm: {}, unopened: [] };
107
+ }
108
+ /** A invariante do denominador: toda contagem por forma soma o total visto. */
109
+ export function exportInvariantHolds(account) {
110
+ return (Object.values(account.byForm).reduce((a, b) => a + b, 0) === account.seen);
111
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * A IDENTIDADE DA FORMA (item 12, 25/08) - a unidade de uma pergunta de self-healing.
3
+ *
4
+ * `FragmentShape` tem 7 valores e é grosso demais: o balde `css` lê 42% no frontend-hub e 0% no
5
+ * codelevel - o MESMO nome com leitores opostos. E texto distinto é fino demais: 10 825 usos do
6
+ * frontend-hub viram 140 textos, mas cada texto ainda é uma pergunta. A granularidade que decide
7
+ * é a FORMA - o texto com os valores abstraídos:
8
+ *
9
+ * body { -webkit-font-smoothing: antialiased } -> -webkit-font-smoothing: <keyword>
10
+ * background: linear-gradient(90deg, #a, #b) -> background: <gradient>
11
+ * hover:-translate-y-px · -translate-y-px -> -translate-y-px (uma forma)
12
+ * text-rose-700 · text-amber-300 -> text-<word>-<n> (uma forma)
13
+ *
14
+ * Medido no diagnóstico: 18 473 declarações das 706 folhas do app inteiro comprimem para 194
15
+ * formas - DEZENAS, não milhares. Uma resposta dele (D2) responde a FORMA, não o texto: quem
16
+ * ensina a ler `text-<word>-<n>` ensinou as centenas de uma vez.
17
+ */
18
+ /** O tipo de UM valor de CSS, abstraído - a escada é determinística e a ordem importa. */
19
+ function kindOfValue(raw) {
20
+ const value = raw.trim();
21
+ if (/gradient\s*\(/i.test(value))
22
+ return "<gradient>";
23
+ if (/^var\(\s*--[\w-]+\s*\)$/i.test(value))
24
+ return "<var>";
25
+ if (/^#[0-9a-f]{3,8}$/i.test(value))
26
+ return "<color>";
27
+ if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|color|color-mix)\s*\(/i.test(value))
28
+ return "<color>";
29
+ if (/url\s*\(/i.test(value))
30
+ return "<url>";
31
+ if (/^calc\s*\(/i.test(value))
32
+ return "<calc>";
33
+ if (/^-?[\d.]+(?:px|rem|em|%|s|ms|vh|vw|vmin|vmax|fr|ch|ex|pt)$/i.test(value))
34
+ return "<length>";
35
+ if (/^-?[\d.]+$/.test(value))
36
+ return "<number>";
37
+ if (/^[a-z][a-z-]*$/i.test(value))
38
+ return "<keyword>";
39
+ /** Sombra, transição, pilha de fonte - vários valores numa declaração só. */
40
+ return "<composite>";
41
+ }
42
+ /**
43
+ * UM token utilitário, com os valores abstraídos: dígitos viram `<n>`, frações `<n>/<n>`,
44
+ * hex `<hex>`, valor arbitrário `[…]` - e os MODIFICADORES saem, porque `hover:-translate-y-px`
45
+ * e `-translate-y-px` são a mesma lacuna de leitura.
46
+ */
47
+ function classForm(token) {
48
+ const utility = token.split(":").pop() ?? token;
49
+ return utility
50
+ .replace(/\[[^\]]*\]/g, "[…]")
51
+ .replace(/#[0-9a-f]{3,8}/gi, "<hex>")
52
+ .replace(/(\d+)\/(\d+)/g, "<n>/<n>")
53
+ .replace(/\/(\d+)/g, "/<n>")
54
+ .replace(/\d+(?:\.\d+)?/g, "<n>")
55
+ .replace(/(?<=-)[a-z]+(?=-<n>(?:\/<n>)?$)/, "<word>");
56
+ }
57
+ /** `sel { prop: value; … }` ou `prop: value` - a primeira declaração nomeia a forma. */
58
+ const DECLARATION = /(-{0,2}[a-zA-Z][\w-]*)\s*:\s*([^;{}]+)/;
59
+ /**
60
+ * A forma de UM fragmento não lido. Determinística, derivada só do texto - a mesma regra do
61
+ * backfill do D3: nada aqui é chute, e um texto que não abre vira a forma `<opaque>`, dita.
62
+ */
63
+ export function formOf(shape, text) {
64
+ const clean = text.replace(/\s+/g, " ").trim();
65
+ if (shape === "class" || shape === "template") {
66
+ const forms = [
67
+ ...new Set(clean.split(/\s+/).filter(Boolean).map(classForm)),
68
+ ];
69
+ return forms.slice(0, 4).join(" ") || "<opaque>";
70
+ }
71
+ const declaration = DECLARATION.exec(clean);
72
+ if (!declaration)
73
+ return "<opaque>";
74
+ return `${declaration[1]}: ${kindOfValue(declaration[2])}`;
75
+ }