synthesisui 0.16.286 → 0.16.290

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.
@@ -32,6 +32,7 @@
32
32
  import { readSxProps } from "./doctor/style-props.js";
33
33
  import { transcribe, } from "./doctor/transcribe.js";
34
34
  import { frontierKind, packageRoot } from "./frontier-kind.js";
35
+ import { withGlobal } from "./global-wear.js";
35
36
  /** The ten forms, closed. A renderer that accepts any tag draws anything. */
36
37
  const FORMS = new Set([
37
38
  "image",
@@ -252,7 +253,18 @@ sketch,
252
253
  * A escala de espaçamento que o projeto DELES declara para o `sx`. Ausente = o default
253
254
  * documentado do MUI, que é fato da biblioteca que eles escolheram - ver `spacingOf`.
254
255
  */
255
- sxSpacing) {
256
+ sxSpacing,
257
+ /**
258
+ * AS CLASSES DA FOLHA GLOBAL DELE - ver `Census.globalClasses` e `wearGlobal`.
259
+ *
260
+ * O QUE ISSO ALCANÇA QUE NADA MAIS ALCANÇAVA: uma utility que ele escreve num NÓ FILHO. O
261
+ * `LeaderboardRow` do `codelevel-ui` põe `text-grad-xp` num `<span>` dentro de um ternário, e o
262
+ * `QuestItem` põe `text-grad` numa tabela que pertence ao filho - as duas passam por aqui, e não
263
+ * pela raiz nem pela tabela de variantes do componente.
264
+ *
265
+ * Ausente é a resposta de quem não tem folha global, e nesse caso nada muda.
266
+ */
267
+ globals) {
256
268
  /**
257
269
  * THE PACKAGE THE RETURNED ELEMENT CAME FROM, measured rather than spelled.
258
270
  * Node 0 of the sketch IS what the component returns, and the reader stamped its
@@ -474,7 +486,7 @@ sxSpacing) {
474
486
  ? (sketch?.[node.at]?.classes ?? "")
475
487
  : "";
476
488
  const classes = raw.split(/\s+/).filter(Boolean);
477
- const t = transcribe(classes, declared);
489
+ const t = withGlobal(transcribe(classes, declared), classes, globals);
478
490
  /**
479
491
  * A PROP DE ESTILO DESTE NÓ, sob a classe dele.
480
492
  *
@@ -618,7 +630,7 @@ sxSpacing) {
618
630
  * otherwise. So a drawn root sends nothing here at all.
619
631
  */
620
632
  const rootStyle = rootPainted
621
- ? transcribe(rootPainted.split(/\s+/).filter(Boolean), declared)
633
+ ? withGlobal(transcribe(rootPainted.split(/\s+/).filter(Boolean), declared), rootPainted.split(/\s+/).filter(Boolean), globals)
622
634
  : null;
623
635
  const paints = rootStyle && Object.keys(rootStyle.base).length > 0
624
636
  ? { style: rootStyle.base }
@@ -710,13 +722,16 @@ sxSpacing) {
710
722
  * correctly, and the absence of a tree means the preview lays them out in a row
711
723
  * exactly as it did before.
712
724
  */
713
- export function resolveFlatParts(read, declared) {
725
+ export function resolveFlatParts(read, declared,
726
+ /** As classes da folha global dele - ver `wearGlobal`. */
727
+ globals) {
714
728
  const parts = {};
715
729
  for (const part of read) {
716
730
  const name = safePartName(String(part?.name ?? ""));
717
731
  if (!name || typeof part.classes !== "string")
718
732
  continue;
719
- const t = transcribe(part.classes.split(/\s+/).filter(Boolean), declared);
733
+ const partClasses = part.classes.split(/\s+/).filter(Boolean);
734
+ const t = withGlobal(transcribe(partClasses, declared), partClasses, globals);
720
735
  parts[name] = { base: t.base, dark: t.dark, states: t.states };
721
736
  }
722
737
  return { parts, tree: [], composes: [], external: [], notes: [], partAt: {} };
@@ -268,6 +268,8 @@ export async function add(slug, opts) {
268
268
  * repositório, e é ele que avisa quando uma delas deixa de resolver.
269
269
  */
270
270
  ...(theirVars.pointed > 0 ? { theirVars: theirVars.pointed } : {}),
271
+ /** Ver `RootLock.tokenMap`: o fato, ao lado do número que ele produz. */
272
+ ...(theirVars.pairs.length > 0 ? { tokenMap: theirVars.pairs } : {}),
271
273
  ...(scope ? { scope } : {}),
272
274
  ...(systems.length > 0 ? { scopes: systems } : {}),
273
275
  ...(usage.length > 0 ? { usage } : {}),
@@ -394,6 +396,29 @@ export async function add(slug, opts) {
394
396
  * `var(--color-ink-500)` num arquivo "gerenciado pelo synthesisui" e não tem como saber de onde
395
397
  * veio. Dito assim, ele lê a linha como o que ela é - o sistema seguindo o vocabulário dele.
396
398
  */
399
+ /**
400
+ * ELE RENOMEOU UM TOKEN, E A GENTE DIZ - antes era um silêncio que o sistema pagava.
401
+ *
402
+ * `their-vars.ts` decidiu NÃO emitir `var(--x, #valor)` justamente para não pintar o valor velho
403
+ * por cima de um rename dele: *"se ele renomear, quebra, e o `doctor` diz qual"* (dono, 22/08). A
404
+ * quebra é deliberada e continua. O que faltava era ele saber ANTES de abrir a tela e ver a cor
405
+ * sumida - o mapa anterior está no `.lock`, o novo acabou de ser calculado, e a diferença é uma
406
+ * comparação.
407
+ *
408
+ * SÓ O RENAME, e não a troca de valor: trocar o valor não quebra nada - a folha aponta para o nome
409
+ * dele e a cor nova passa a valer, que é o comportamento que a gente promete. Renomear é o que
410
+ * deixa a referência apontando para o vazio.
411
+ */
412
+ const renamed = (prev?.tokenMap ?? []).filter((before) => {
413
+ const now = theirVars.pairs.find((p) => p.ours === before.ours);
414
+ return now && now.theirs !== before.theirs;
415
+ });
416
+ if (renamed.length > 0) {
417
+ console.log(` ${renamed.length} token${renamed.length === 1 ? "" : "s"} you renamed since the last install: ${renamed
418
+ .slice(0, 3)
419
+ .map((r) => `${r.theirs} → ${theirVars.pairs.find((p) => p.ours === r.ours)?.theirs ?? "?"}`)
420
+ .join(", ")}${renamed.length > 3 ? ", …" : ""}. The system follows the new name from here.`);
421
+ }
397
422
  if (theirVars.pointed > 0)
398
423
  console.log(` ${theirVars.pointed} value${theirVars.pointed === 1 ? "" : "s"} in tokens.css now point at the name YOUR code already gives ${theirVars.pointed === 1 ? "it" : "them"} - change yours and the system follows${theirVars.pruned > 0 ? `; ${theirVars.pruned} matched but your build does not emit ${theirVars.pruned === 1 ? "that name" : "those names"}, so ${theirVars.pruned === 1 ? "it keeps" : "they keep"} the value` : ""}`);
399
424
  /**
@@ -8,6 +8,7 @@ import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-tem
8
8
  import { body, section, snippet } from "../output.js";
9
9
  import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
10
10
  import { fetchComponent, RegistryError } from "../registry.js";
11
+ import { inTheirTongue, tongueOf } from "../their-tongue.js";
11
12
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
12
13
  /**
13
14
  * Writes the shared `cn.ts` next to the components, built from THIS project's
@@ -65,12 +66,41 @@ export async function component(slug, name, opts) {
65
66
  // o .tsx é materializado logo abaixo, ninguém as lê depois (medido na
66
67
  // auditoria de 16/08), e cada arquivo a mais no repo dele é superfície.
67
68
  const config = await readProjectConfig(root);
69
+ /**
70
+ * O CSS PASSA A FALAR A LÍNGUA DELE - ver `inTheirTongue`.
71
+ *
72
+ * O QUE MUDA NO ARQUIVO QUE ELE ABRE: `color: var(--ds-color-ink-900)` vira
73
+ * `color: var(--color-ink-900)`, o nome que o código dele já dá àquele valor. É isso que faz a
74
+ * nossa folha de 3945 linhas deixar de ser necessária no repositório de origem - o `globals.css`
75
+ * dele já declara o token, e a decisão continua sendo dele.
76
+ *
77
+ * AQUI E NÃO NO SERVIDOR, que é onde o CSS é compilado: o mapa descreve ESTE repositório. O mesmo
78
+ * sistema instalado em outro projeto dele não tem os mesmos nomes declarados, e traduzir lá
79
+ * apagaria a cor. Um projeto de destino chega sem mapa no `.lock`, nada é traduzido, e a folha
80
+ * continua sendo o caminho - o comando DIZ qual dos dois aconteceu.
81
+ */
82
+ const tongue = await tongueFor(root, slug);
83
+ const spoken = tongue ? inTheirTongue(res.css, tongue) : null;
84
+ const css = spoken ? spoken.css : res.css;
85
+ /**
86
+ * E ELE FICA SABENDO - traduzir em silêncio é a outra metade do mesmo erro.
87
+ *
88
+ * A última linha é a que decide se a nossa folha ainda é necessária aqui, e ela é a diferença
89
+ * entre "seu repositório dispensa a folha" e "quase" - dizer a primeira quando a verdade é a
90
+ * segunda é a promessa que quebra na tela dele.
91
+ */
92
+ if (spoken && (spoken.named > 0 || spoken.inlined > 0)) {
93
+ console.log(` ${spoken.named} reference${spoken.named === 1 ? "" : "s"} now speak${spoken.named === 1 ? "s" : ""} the name YOUR code gives the value${spoken.inlined > 0 ? `, and ${spoken.inlined} carr${spoken.inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
94
+ console.log(spoken.left.length === 0
95
+ ? ` Nothing in this file points at our stylesheet - it renders on your own tokens alone.`
96
+ : ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css is still needed here.`);
97
+ }
68
98
  const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
69
99
  if (artifactsAreTheProduct) {
70
100
  const dir = join(root, "_synthesisui", "ds", slug, "components");
71
101
  await mkdir(dir, { recursive: true });
72
102
  await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
73
- await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
103
+ await writeFile(join(dir, `${res.name}.css`), `${css}\n`, "utf8");
74
104
  console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
75
105
  }
76
106
  // 2. YOUR component - a real, importable `export function <Pascal>()` in the
@@ -151,12 +181,12 @@ export async function component(slug, name, opts) {
151
181
  // template drives itself off the .ds-* classes.
152
182
  const tsx = interactiveTemplate(res.name);
153
183
  await writeFile(join(compDir, `${res.name}.tsx`), tsx, "utf8");
154
- await writeFile(join(compDir, `${res.name}.css`), `${res.css}\n`, "utf8");
184
+ await writeFile(join(compDir, `${res.name}.css`), `${css}\n`, "utf8");
155
185
  await writeFile(join(compDir, "index.ts"), `export * from "./${res.name}";\n`, "utf8");
156
186
  filenames = [`${res.name}.tsx`, `${res.name}.css`, "index.ts"];
157
187
  }
158
188
  else {
159
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles, await reactMajorOf(root),
189
+ const files = generateComponentFiles(slug, res.name, res.recipe, css, res.version, config.styles, await reactMajorOf(root),
160
190
  /**
161
191
  * THE SPELLING, FROM THE VERSION WE JUST FETCHED.
162
192
  *
@@ -246,3 +276,35 @@ export async function component(slug, name, opts) {
246
276
  }
247
277
  console.log("");
248
278
  }
279
+ /**
280
+ * O VOCABULÁRIO DESTE REPOSITÓRIO, do que o `add` já deixou na pasta - ver `tongueOf`.
281
+ *
282
+ * `null` quando não há mapa: é a resposta de um projeto de destino, de um repositório que nunca
283
+ * buildou, ou de um install feito por um CLI anterior a 0.16.290. Nos três casos nada é traduzido e
284
+ * a folha instalada continua sendo o caminho, que é o comportamento de sempre.
285
+ */
286
+ async function tongueFor(root, slug) {
287
+ const dir = join(root, "_synthesisui", "ds", slug);
288
+ const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
289
+ if (!lock)
290
+ return null;
291
+ let map = [];
292
+ let version = 0;
293
+ try {
294
+ const parsed = JSON.parse(lock);
295
+ map = parsed.tokenMap ?? [];
296
+ version = parsed.version ?? 0;
297
+ }
298
+ catch {
299
+ return null;
300
+ }
301
+ if (map.length === 0)
302
+ return null;
303
+ /**
304
+ * O VALOR COMPILADO MORA NA PASTA DA VERSÃO - a folha da raiz é um re-export de uma linha, escrito
305
+ * assim de propósito para que o `@import` dele nunca mude entre updates (ver `add.ts`). É de lá
306
+ * que sai o literal para as variáveis que não têm nome dele.
307
+ */
308
+ const installed = await readFile(join(dir, `v${version}`, "tokens.css"), "utf8").catch(() => "");
309
+ return tongueOf(map, installed);
310
+ }
@@ -40,7 +40,9 @@ import { keyframesInSheets } from "../global-keyframes.js";
40
40
  import { withLibraryStructure } from "../library-structure.js";
41
41
  import { mergeCensus } from "../merge-census.js";
42
42
  import { claimName } from "../name-claim.js";
43
+ import { mergeNamespacePairs } from "../namespace-pairs.js";
43
44
  import { namingQueue } from "../naming-queue.js";
45
+ import { notExpressed } from "../not-expressed.js";
44
46
  import { body, paint, section } from "../output.js";
45
47
  import { outsideScope } from "../outside-scope.js";
46
48
  import { phase, startProgress } from "../progress.js";
@@ -367,6 +369,8 @@ function screenOf(rel) {
367
369
  const after = parts.slice(i + 1).filter((p) => !p.startsWith("("));
368
370
  return `${parts[i]}/${after.slice(0, 2).join("/")}`;
369
371
  }
372
+ /** Quantas classes da folha global viajam no censo. Ver o aviso em `takeCensus`. */
373
+ const MAX_GLOBAL_CLASSES = 300;
370
374
  export async function takeCensus(root, opts) {
371
375
  /**
372
376
  * O RELATÓRIO SÓ SAI QUANDO ALGUÉM O PEDIU - `say` cala quando o chamador é uma RE-medição.
@@ -450,6 +454,17 @@ export async function takeCensus(root, opts) {
450
454
  * porque é isso que o juiz do ledger precisa para chamar a classe e a regra de LIDAS.
451
455
  */
452
456
  const globalClasses = readGlobalClasses(globalSheets, declaredValues);
457
+ /**
458
+ * TETO DAS CLASSES GLOBAIS QUE VIAJAM NO CENSO - e quando ele corta, ele fala.
459
+ *
460
+ * Medido nas três populações: 24 classes na folha do `codelevel-ui`, 22 na nossa, 0 no
461
+ * `apps/web-dashboard` do repo real, que é CSS Modules. O teto é para a folha de um app que
462
+ * escreva utilitário por atacado, e existe porque um censo que dobra de tamanho em silêncio é o
463
+ * tipo de surpresa que ninguém pede.
464
+ */
465
+ if (globalClasses.size > MAX_GLOBAL_CLASSES) {
466
+ 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 census - the first ${MAX_GLOBAL_CLASSES} are.`)));
467
+ }
453
468
  /**
454
469
  * OS KEYFRAMES DA FOLHA DELE, colhidos onde as folhas globais já estão na mão.
455
470
  *
@@ -735,16 +750,18 @@ export async function takeCensus(root, opts) {
735
750
  /**
736
751
  * Ver `CensusLook.aside` - o segundo componente do arquivo também.
737
752
  *
738
- * AQUI OS EIXOS NÃO SÃO CONHECIDOS: `transcribeVariants` roda para o primeiro
739
- * componente do arquivo, e este é o segundo. Passar `undefined` é dizer a verdade
740
- * ("não sei se declara"), e não uma omissão - e é por isso que o veto final, em
741
- * `exclusive-contract.ts`, pergunta pelos eixos em vez de confiar neste campo.
753
+ * E COM OS EIXOS QUE O TIPO DELE DECLARA. `transcribeVariants` roda para o primeiro
754
+ * componente do arquivo, então aqui não tabela de variantes - mas `extra.axes` é a
755
+ * declaração da interface, lida por `scanDefinitions` para TODA definição do arquivo.
756
+ * Era `undefined`, e isso pôs de lado o `ToastIcon` dele: terceiro componente do
757
+ * `Toast.tsx`, cinco tons declarados no tipo, um gradiente para cada. Ver
758
+ * `classifyAside`.
742
759
  */
743
- ...(classifyAside({ name: extra.name, declaredAxes: undefined })
760
+ ...(classifyAside({ name: extra.name, declaredAxes: extra.axes })
744
761
  ? {
745
762
  aside: classifyAside({
746
763
  name: extra.name,
747
- declaredAxes: undefined,
764
+ declaredAxes: extra.axes,
748
765
  }),
749
766
  }
750
767
  : {}),
@@ -782,7 +799,7 @@ export async function takeCensus(root, opts) {
782
799
  * for the result, which is the "valid and unread" failure this pipeline has
783
800
  * paid for twice already.
784
801
  */
785
- const v = transcribeVariants(src, declaredValues);
802
+ const v = transcribeVariants(src, declaredValues, globalClasses);
786
803
  /**
787
804
  * THE RESTING OPTION, from the definition itself. `cva` says it in
788
805
  * `defaultVariants`; a lookup-record component says it in the destructuring -
@@ -1040,14 +1057,20 @@ export async function takeCensus(root, opts) {
1040
1057
  /**
1041
1058
  * Ver `CensusLook.aside`: o fato viaja, a regra fica de um lado só.
1042
1059
  *
1043
- * `v.axes` é a DECLARAÇÃO de variação deste componente, e é ela que separa um glifo de
1044
- * um componente cujo nome parece com um. Ver `classifyAside`.
1060
+ * AS DUAS FONTES DE EIXO, e é a mesma decisão do autor nas duas. `found[0].axes` é o que
1061
+ * o TIPO declara (`interface ToastIconProps { tone?: "gold" | "cool" }`); `v.axes` é o
1062
+ * que a TABELA declara (`cva({ variants: … })`). Consultar só a segunda pôs de lado um
1063
+ * componente que pinta por `Record<tone, gradiente>` - forma que a tabela não vê.
1064
+ * Ver `classifyAside`.
1045
1065
  */
1046
- ...(classifyAside({ name: found[0].name, declaredAxes: v.axes })
1066
+ ...(classifyAside({
1067
+ name: found[0].name,
1068
+ declaredAxes: { ...found[0].axes, ...v.axes },
1069
+ })
1047
1070
  ? {
1048
1071
  aside: classifyAside({
1049
1072
  name: found[0].name,
1050
- declaredAxes: v.axes,
1073
+ declaredAxes: { ...found[0].axes, ...v.axes },
1051
1074
  }),
1052
1075
  }
1053
1076
  : {}),
@@ -1864,7 +1887,14 @@ export async function takeCensus(root, opts) {
1864
1887
  list.push(kebab(pair.child));
1865
1888
  companionsOf.set(pair.component, list);
1866
1889
  }
1867
- const components = merged.map((c) => ({
1890
+ /**
1891
+ * `Modal.Header` E `ModalHeader` CONTADOS UMA VEZ - ver `mergeNamespacePairs`.
1892
+ *
1893
+ * Antes do enriquecimento porque veredito, leis e companheiros são indexados por NOME: fundir
1894
+ * depois deixaria metade do que se sabe sobre a peça pendurado no nome que deixou de existir.
1895
+ */
1896
+ const paired = mergeNamespacePairs(merged, new Set(defined.map((d) => d.name)));
1897
+ const components = paired.map((c) => ({
1868
1898
  ...c,
1869
1899
  ...(verdicts.get(idOf(c)) ?? {}),
1870
1900
  ...(laws.has(c.name) ? { laws: laws.get(c.name) } : {}),
@@ -2071,6 +2101,25 @@ export async function takeCensus(root, opts) {
2071
2101
  }
2072
2102
  : {}),
2073
2103
  ...(conventions.length > 0 ? { conventions } : {}),
2104
+ /**
2105
+ * AS CLASSES DA FOLHA GLOBAL DELE - ver `Census.globalClasses`.
2106
+ *
2107
+ * TODAS AS QUE A FOLHA DECLARA, e não só as que a raiz vestiu. A primeira tentativa levou apenas
2108
+ * as reivindicadas por `globalWear`, que só roda na raiz do componente - e as utilities usadas
2109
+ * numa tabela de variantes ou num nó filho, que são a maioria, ficavam de fora justamente do
2110
+ * campo que existe para alcançá-las. Duas de 24 chegavam.
2111
+ *
2112
+ * O CUSTO ESTÁ MEDIDO: a folha do `codelevel-ui` declara 24 classes, a nossa 22, e o
2113
+ * `apps/web-dashboard` do repo real declara 0 - ele é CSS Modules. Nas três populações isso é
2114
+ * ruído no tamanho do censo. O teto de `MAX_GLOBAL_CLASSES` existe para a folha de um app que
2115
+ * escreva utilitário por atacado, e quando ele corta, o número cortado é DITO em vez de
2116
+ * desaparecer.
2117
+ */
2118
+ ...(globalClasses.size > 0
2119
+ ? {
2120
+ globalClasses: Object.fromEntries([...globalClasses.entries()].slice(0, MAX_GLOBAL_CLASSES)),
2121
+ }
2122
+ : {}),
2074
2123
  classStyle,
2075
2124
  ...(Object.keys(looks).length > 0 ? { looks } : {}),
2076
2125
  ...(naming.components > 0 ? { naming } : {}),
@@ -3166,6 +3215,16 @@ export function rootBehaviour(tag, sketch) {
3166
3215
  export async function resolveReadParts(census, root,
3167
3216
  /** `true` numa re-medição: o resumo desta etapa fica para o `sync --full` - ver `say`. */
3168
3217
  quiet = false) {
3218
+ /**
3219
+ * AS CLASSES DA FOLHA GLOBAL DELE, DO CENSO - ver `Census.globalClasses`.
3220
+ *
3221
+ * Esta etapa roda depois da medição, e também no `sync` e do lado da plataforma, sobre censo já
3222
+ * guardado - então ela não tem como voltar à folha no disco. Vindo do censo, uma utility que ele
3223
+ * escreve num NÓ FILHO chega ao look: o `LeaderboardRow` põe `text-grad-xp` num `<span>` dentro de
3224
+ * um ternário, e é aqui que aquele nó é lido.
3225
+ */
3226
+ const globalClasses = new Map(Object.entries(census
3227
+ .globalClasses ?? {}));
3169
3228
  const say = quiet
3170
3229
  ? () => { }
3171
3230
  : (line) => {
@@ -3278,13 +3337,13 @@ quiet = false) {
3278
3337
  ? resolveAnatomy(framed, declared, deps, resolveRef, entry?.root,
3279
3338
  // The component's own sketch, so a node naming itself by index reads
3280
3339
  // the exact class string the census measured (dono, 01/08).
3281
- looks[component]?.sketch, census.sxSpacing)
3340
+ looks[component]?.sketch, census.sxSpacing, globalClasses)
3282
3341
  : patched
3283
- ? resolveAnatomy(patched.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
3342
+ ? resolveAnatomy(patched.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing, globalClasses)
3284
3343
  : derived && derived.read.length > 0
3285
- ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
3344
+ ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing, globalClasses)
3286
3345
  : Array.isArray(entry?.parts) && entry.parts.length > 0
3287
- ? resolveFlatParts(entry.parts, declared)
3346
+ ? resolveFlatParts(entry.parts, declared, globalClasses)
3288
3347
  : null;
3289
3348
  if (!resolved)
3290
3349
  continue;
@@ -3699,6 +3758,23 @@ export async function runImport(opts) {
3699
3758
  const out = opts.census ?? join(root, "_synthesisui", "census.json");
3700
3759
  await mkdir(dirname(out), { recursive: true });
3701
3760
  await writeFile(out, `${JSON.stringify(census, null, 2)}\n`, "utf8");
3761
+ /**
3762
+ * O QUE NÃO CHEGOU, DECLARADO PELA ESTEIRA - ver `notExpressed`.
3763
+ *
3764
+ * Escrito ao lado do censo e na mesma rodada, porque a lacuna e a medição são a mesma verdade
3765
+ * vista dos dois lados. Era o agente que escrevia este arquivo, instruído por uma frase dentro da
3766
+ * descrição de uma tool - e em 23/08 o mesmo repositório foi importado duas vezes, a primeira
3767
+ * declarando 37 linhas de lacuna e a segunda nenhuma, com as lacunas todas no lugar. Um pedido a
3768
+ * um modelo não é um artefato.
3769
+ *
3770
+ * O agente ainda ACRESCENTA o que só ele vê. O piso deixou de depender dele.
3771
+ */
3772
+ const gaps = notExpressed(census);
3773
+ if (gaps) {
3774
+ const gapFile = join(dirname(out), "not-expressed.md");
3775
+ await writeFile(gapFile, gaps, "utf8");
3776
+ console.log(body(`Gaps written to ${paint.strong(relative(root, gapFile))}`));
3777
+ }
3702
3778
  console.log("");
3703
3779
  console.log(body(`Written to ${paint.strong(relative(root, out))}`));
3704
3780
  if (opts.dry) {
@@ -19,6 +19,8 @@
19
19
  */
20
20
  import { frontierKind } from "../frontier-kind.js";
21
21
  import { importMap } from "./imports.js";
22
+ /** Quantos lugares viajam por referência quebrada. Ver `BrokenRef.at`. */
23
+ const MAX_PLACES = 3;
22
24
  /** `var(--x)`, including inside a Tailwind arbitrary value: `bg-[var(--x)]`. */
23
25
  const VAR_REF = /var\(\s*(--[a-zA-Z0-9_-]+)/g;
24
26
  /**
@@ -96,9 +98,20 @@ export function findBrokenRefs(sources, declared) {
96
98
  continue;
97
99
  if (runtime && (RUNTIME_ANCHOR.has(name) || RUNTIME_PREFIX.test(name)))
98
100
  continue;
99
- const hit = seen.get(name) ?? { count: 0, files: new Set() };
101
+ const hit = seen.get(name) ?? {
102
+ count: 0,
103
+ files: new Set(),
104
+ at: [],
105
+ };
100
106
  hit.count += 1;
101
107
  hit.files.add(file);
108
+ /** A linha contada do índice do match - ver `BrokenRef.at` para o teto. */
109
+ if (hit.at.length < MAX_PLACES) {
110
+ hit.at.push({
111
+ file,
112
+ line: source.slice(0, m.index ?? 0).split("\n").length,
113
+ });
114
+ }
102
115
  seen.set(name, hit);
103
116
  }
104
117
  }
@@ -111,6 +124,7 @@ export function findBrokenRefs(sources, declared) {
111
124
  count: hit.count,
112
125
  files: hit.files.size,
113
126
  ...(meant ? { meant } : {}),
127
+ ...(hit.at.length > 0 ? { at: hit.at } : {}),
114
128
  });
115
129
  }
116
130
  // A broken reference somebody typed nine times is worse than one typed once,
@@ -61,6 +61,8 @@ const PROVIDER_SUFFIX = /(Provider|Providers|Context|Boundary|Guard|Wrapper)$/;
61
61
  /** Any JSX at all in the file. A capitalised export with none is a constant, a
62
62
  * config object or a helper - `export const ROUTES = {…}` is not a component. */
63
63
  const HAS_JSX = /<[A-Za-z][^>]*>|<>/;
64
+ /** `ICON_SIZE`, `AURORA_PALETTES`, `ARTICLE_TAB` - a convenção universal para valor, não para peça. */
65
+ const SCREAMING_SNAKE = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/;
64
66
  /**
65
67
  * Any sign that this file decides how something LOOKS.
66
68
  *
@@ -71,7 +73,30 @@ const HAS_JSX = /<[A-Za-z][^>]*>|<>/;
71
73
  const HAS_STYLING = /className|class=|style=|styled[.(]|css`|cva\(|\btv\(|makeStyles|sx=|tw`/;
72
74
  /** `createContext` + a `.Provider` in the return is a provider whatever it is
73
75
  * called - `RootWrapper` and `CommonProviders` both read this way. */
74
- const IS_CONTEXT = /createContext\s*[<(]/;
76
+ /**
77
+ * QUEM RECEBE O `createContext` - o nome, e não o arquivo em volta dele.
78
+ *
79
+ * O QUE O CLIENTE PERDIA: o `Tabs` inteiro. `Tabs.tsx` declara `const TabsCtx = createContext(…)`, e
80
+ * a pergunta era se o ARQUIVO tem contexto - então `Tabs`, `TabsList`, `TabsTrigger` e `TabsPanel`
81
+ * saíram juntos, com `cn("flex flex-col gap-4")` escrito neles (`codelevel-ui`, 23/08). Composto com
82
+ * contexto é o padrão dominante de React - Tabs, Accordion, Select, Dropdown -, então recusar o
83
+ * arquivo recusa a família toda.
84
+ *
85
+ * É a mesma falha de forma que `HAS_JSX` já cometeu do outro lado: uma pergunta sobre o arquivo
86
+ * respondendo sobre o export. Um provider é UM nome; os vizinhos respondem por si.
87
+ *
88
+ * Medido em três populações - `codelevel-ui` recupera 4, o dashboard do `frontend-hub` recupera 7
89
+ * (o `Pagination` inteiro), e os 6 que terminam em `Provider` continuam fora pelo sufixo.
90
+ */
91
+ const CONTEXT_OWNER = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]*)?=\s*(?:React\.)?createContext\s*[<(]/g;
92
+ /** Os nomes que ESTE arquivo atribui a um `createContext`. */
93
+ function contextNames(source) {
94
+ CONTEXT_OWNER.lastIndex = 0;
95
+ const out = new Set();
96
+ for (const m of source.matchAll(CONTEXT_OWNER))
97
+ out.add(m[1]);
98
+ return out;
99
+ }
75
100
  /** A lowercase JSX tag: an html element this file renders ITSELF. */
76
101
  const OWN_ELEMENT = /<(?:[a-z][a-z0-9]*)(\s|>|\/)/;
77
102
  /** Every capitalised JSX tag the file opens, dots included. */
@@ -205,7 +230,7 @@ export function gateComponent(input) {
205
230
  because: `\`${name}\` names a screen, and composes ${shape.composes} piece${shape.composes === 1 ? "" : "s"} across ${shape.nodes} element${shape.nodes === 1 ? "" : "s"} - too thin to be a template of yours. A design system is made of the pieces a screen is built FROM`,
206
231
  };
207
232
  }
208
- if (PROVIDER_SUFFIX.test(name) || IS_CONTEXT.test(source)) {
233
+ if (PROVIDER_SUFFIX.test(name) || contextNames(source).has(name)) {
209
234
  return {
210
235
  ok: false,
211
236
  why: "provider",
@@ -232,6 +257,38 @@ export function gateComponent(input) {
232
257
  */
233
258
  if (STYLED_FACTORY.test(source))
234
259
  return { ok: true };
260
+ /**
261
+ * UMA CONSTANTE NÃO É COMPONENTE, e o arquivo em volta dela não muda isso.
262
+ *
263
+ * O QUE O CLIENTE VIA: `ICON_SIZE` e `AURORA_PALETTES` ocupando vaga na vitrine do
264
+ * `codelevel-ui`, e uma delas com forma desenhada - um mapa de tamanhos renderizado como se fosse
265
+ * peça. Ele contava 59 componentes no design system dele e dois não existiam.
266
+ *
267
+ * O teste de markup logo abaixo já recusa constante, e ele pergunta pelo ARQUIVO INTEIRO:
268
+ * `ICON_SIZE` mora dentro do `Icon.tsx`, que tem JSX de sobra, então ela entrava pela porta do
269
+ * vizinho. Vem ANTES dele por isso - é a mesma falha de forma que o teste de contexto acima
270
+ * corrigiu do outro lado.
271
+ *
272
+ * MEDIDO EM QUATRO POPULAÇÕES, e as três encontradas são objeto literal no código real:
273
+ *
274
+ * codelevel-ui packages/ui 75 nomes · 2 (ICON_SIZE, AURORA_PALETTES)
275
+ * frontend-hub packages/ui 69 nomes · 0
276
+ * frontend-hub apps/web-dashboard 526 nomes · 1 (ARTICLE_TAB, um enum de aba)
277
+ * synthesisui apps/web · 0
278
+ *
279
+ * Nenhum componente das quatro usa este formato de nome - JSX escreve `<Button>` e nunca
280
+ * `<ICON_SIZE>` -, então a regra não recusa peça de ninguém. E ela só pôde entrar junto com o
281
+ * leitor de `export default` em linha própria: sozinha, ela deixava órfãs as 294 declarações de
282
+ * CSS Modules do único arquivo do dashboard onde a constante era a ÚNICA coisa que o leitor via.
283
+ * O dono daquele arquivo é o `CurateContentSection`, que agora é lido.
284
+ */
285
+ if (SCREAMING_SNAKE.test(name)) {
286
+ return {
287
+ ok: false,
288
+ why: "config",
289
+ because: `\`${name}\` is spelled as a constant - a value the code reads, not a component anything renders`,
290
+ };
291
+ }
235
292
  if (!HAS_JSX.test(source)) {
236
293
  return {
237
294
  ok: false,