synthesisui 0.16.252 → 0.16.254

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.
@@ -35,6 +35,8 @@ import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, tran
35
35
  import { transcribeVariants } from "../doctor/variant-read.js";
36
36
  import { frontierKind, packageRoot } from "../frontier-kind.js";
37
37
  import { withLibraryStructure } from "../library-structure.js";
38
+ import { mergeCensus } from "../merge-census.js";
39
+ import { claimName } from "../name-claim.js";
38
40
  import { body, paint, section } from "../output.js";
39
41
  import { phase, startProgress } from "../progress.js";
40
42
  import { detectStack, resolveDeps, stackVersions } from "../stack.js";
@@ -394,6 +396,16 @@ export async function takeCensus(root, opts) {
394
396
  const internal = await internalSpecifiers(root);
395
397
  const defined = [];
396
398
  /** Nome → arquivo e fonte, para toda leitura por componente - ver o preenchimento abaixo. */
399
+ /**
400
+ * QUEM DETÉM CADA NOME - e o registro de quem pediu o mesmo.
401
+ *
402
+ * Os oito mapas deste laço são indexados por nome de componente, então dois
403
+ * `Card.tsx` disputam a mesma chave e o último do `walk` sobrescrevia os
404
+ * outros em silêncio. Medido no repo do dono: 20 nomes colidem no repo
405
+ * inteiro, 14 dentro de um app, e um dos quatro `Card` é o do design system
406
+ * dele. Ver `name-claim.ts` para o desempate e por que ele é por estrutura.
407
+ */
408
+ const claims = { by: new Map(), collisions: [] };
397
409
  const sourceOf = new Map();
398
410
  /** Nome → o que a peça é, quando o portão soube dizer. Ver `CensusLook.kind`. */
399
411
  const kindOf = new Map();
@@ -592,6 +604,21 @@ export async function takeCensus(root, opts) {
592
604
  because: verdict.because,
593
605
  });
594
606
  }
607
+ /**
608
+ * O NOME É DISPUTADO AQUI, ANTES DE QUALQUER ESCRITA.
609
+ *
610
+ * Filtrar `found` fecha a porta de uma vez: os oito mapas deste laço
611
+ * derivam dele, então um componente que não detém o nome simplesmente não
612
+ * chega a nenhum deles - em vez de oito guardas que alguém esquece de
613
+ * repetir na nona escrita.
614
+ *
615
+ * O descarte NÃO é silencioso: `claims.collisions` guarda os dois caminhos
616
+ * e o censo os carrega. Trazer as duas receitas exige o contexto viajando
617
+ * na chave, que é a fatia dos grupos.
618
+ */
619
+ const claimed = found.filter((d) => claimName(claims, d.name, rel));
620
+ found.length = 0;
621
+ found.push(...claimed);
595
622
  for (const d of found) {
596
623
  const props = readDefinitionProps(d.name, src);
597
624
  propsOf.set(d.name, props);
@@ -1121,13 +1148,39 @@ export async function takeCensus(root, opts) {
1121
1148
  const edges = new Map();
1122
1149
  /** Por raiz: `@/` é uma pasta diferente em cada app - ver `reachabilityOf`. */
1123
1150
  const graphRoots = [];
1151
+ /**
1152
+ * A DISTÂNCIA, contada onde os arquivos já estão abertos - ver `Census.adoption`.
1153
+ *
1154
+ * `scopePkg` é o nome do pacote do escopo, que é como um consumidor o importa.
1155
+ * Um arquivo conta quando ele NOMEIA esse especificador, e a régua é o texto do
1156
+ * import: um alias de tsconfig que aponta para a mesma pasta contaria como
1157
+ * "não usa", e essa é a diferença entre um número menor e um número inventado.
1158
+ */
1159
+ const adoption = new Map();
1124
1160
  for (const u of opts?.usage ?? []) {
1125
1161
  const uInternal = await internalSpecifiers(u.path);
1126
1162
  graphRoots.push({ path: u.path, aliases: await aliasesOf(u.path) });
1163
+ const tally = adoption.get(u.label) ?? { files: 0, using: 0 };
1164
+ adoption.set(u.label, tally);
1127
1165
  for await (const file of walk(u.path)) {
1128
1166
  const src = await readFile(file, "utf8").catch(() => "");
1129
1167
  if (!src)
1130
1168
  continue;
1169
+ /**
1170
+ * O DENOMINADOR SÓ CONTA QUEM PINTA.
1171
+ *
1172
+ * A primeira versão contava todo `.ts` - hooks, tipos, clientes http - e o
1173
+ * app do dono saiu como 105 de 1578 (7%), quando 105 de 356 arquivos de
1174
+ * marcação é 29%. Um arquivo de tipos não deixa de usar o design system:
1175
+ * ele nunca teve como. Diluir o denominador com ele faz a migração parecer
1176
+ * mais atrasada do que é, e um número pessimista é tão falso quanto um
1177
+ * otimista.
1178
+ */
1179
+ if (/\.(tsx|jsx|vue|svelte)$/i.test(file)) {
1180
+ tally.files += 1;
1181
+ if (scopePkg && src.includes(scopePkg))
1182
+ tally.using += 1;
1183
+ }
1131
1184
  edges.set(file, edgesIn(src));
1132
1185
  const rel = join(u.label, relative(u.path, file));
1133
1186
  if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
@@ -1880,6 +1933,26 @@ export async function takeCensus(root, opts) {
1880
1933
  ...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
1881
1934
  ...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
1882
1935
  ...(brokenRefs.length > 0 ? { brokenRefs } : {}),
1936
+ /**
1937
+ * OS NOMES DISPUTADOS - ver `name-claim.ts`.
1938
+ *
1939
+ * Aditivo e omitido quando vazio, que é o caso de toda biblioteca sã: no repo
1940
+ * do dono, `packages/ui` produz zero e o repo inteiro produz 20. Um campo que
1941
+ * só aparece quando há algo a dizer não muda nada para quem já lê o censo.
1942
+ */
1943
+ ...(claims.collisions.length > 0 ? { collisions: claims.collisions } : {}),
1944
+ ...(scopePkg && adoption.size > 0
1945
+ ? {
1946
+ adoption: {
1947
+ specifier: scopePkg,
1948
+ consumers: [...adoption.entries()].map(([label, t]) => ({
1949
+ label,
1950
+ files: t.files,
1951
+ using: t.using,
1952
+ })),
1953
+ },
1954
+ }
1955
+ : {}),
1883
1956
  ...(conventions.length > 0 ? { conventions } : {}),
1884
1957
  classStyle,
1885
1958
  ...(Object.keys(looks).length > 0 ? { looks } : {}),
@@ -1995,6 +2068,8 @@ export async function takeCensus(root, opts) {
1995
2068
  named: d.named,
1996
2069
  tokenUses: d.tokenUses,
1997
2070
  coverage: d.coverage,
2071
+ ownUses: d.ownUses,
2072
+ phantomUses: d.phantomUses,
1998
2073
  },
1999
2074
  };
2000
2075
  }
@@ -2055,8 +2130,120 @@ async function summarize(c, root, scope) {
2055
2130
  }
2056
2131
  }
2057
2132
  sayConventions(c);
2133
+ sayScopes(c);
2134
+ sayAdoption(c);
2135
+ sayCollisions(c);
2058
2136
  await sayBrokenRefs(c, root, scope);
2059
2137
  }
2138
+ /**
2139
+ * OS ESCOPOS QUE VIRARAM UM SISTEMA, e o que cada um perdeu no empate.
2140
+ *
2141
+ * `--scope` aceita mais de um, e a ordem é uma declaração: o primeiro vence. Um
2142
+ * token que os dois declaram diferente não é erro de ninguém - é divergência, e
2143
+ * a lei do dono (§8.6) é que a plataforma não decide sozinha. Ela mostra os
2144
+ * dois, diz quem é a fonte declarada, e propõe alinhar o outro lado.
2145
+ *
2146
+ * Medido no monorepo dele: `packages/ui` e `apps/web-dashboard/src` declaram os
2147
+ * mesmos três `--gradient-gray-*` com valores opostos - claros na biblioteca,
2148
+ * escuros no app. Nenhum dos dois está errado, e um deles ia desaparecer calado.
2149
+ */
2150
+ function sayScopes(c) {
2151
+ const scopes = c.scopes ?? [];
2152
+ if (scopes.length < 2)
2153
+ return;
2154
+ const conflicts = c.scopeConflicts ?? [];
2155
+ console.log("");
2156
+ console.log(section("Two folders, one system"));
2157
+ console.log(body(`Measured ${scopes.length} scopes and fused them: ${scopes.map((x, i) => (i === 0 ? paint.strong(x) : x)).join(" · ")}. The first one is the authority - it wins a tie, and nothing from the others is dropped without a line.`));
2158
+ if (conflicts.length === 0)
2159
+ return;
2160
+ console.log("");
2161
+ console.log(body(`${conflicts.length} token${conflicts.length === 1 ? "" : "s"} ${conflicts.length === 1 ? "is" : "are"} declared differently in two of them:`));
2162
+ console.log("");
2163
+ for (const x of conflicts.slice(0, 5)) {
2164
+ console.log(body(`${paint.strong(x.name)}\n ${paint.faint("kept")} ${x.kept.value} ${paint.faint(`(${x.kept.scope})`)}\n ${paint.faint("also")} ${x.lost.value} ${paint.faint(`(${x.lost.scope})`)}`));
2165
+ }
2166
+ if (conflicts.length > 5)
2167
+ console.log(body(paint.faint(`(${conflicts.length - 5} more)`)));
2168
+ console.log("");
2169
+ console.log(body(paint.faint("Neither value is wrong - one of them is a different scheme, or a fork that drifted. The system carries the first; the screen can show both.")));
2170
+ }
2171
+ /**
2172
+ * QUANTO DA MIGRAÇÃO JÁ ACONTECEU - o número que faltava.
2173
+ *
2174
+ * O `intent` já dizia que alguém está migrando design para o pacote
2175
+ * compartilhado (`init --intent migrate`, versionado em `_synthesisui/config.json`
2176
+ * com a data). O que nenhuma superfície dizia é QUANTO já chegou. Medido no
2177
+ * monorepo do dono: 103 de 356 arquivos do `web-dashboard` importam
2178
+ * `@frontend-hub/ui`, e o `web-review` não o conhece.
2179
+ *
2180
+ * É a diferença entre medir estado e medir progresso: um relatório de estado é
2181
+ * igual em janeiro e em junho; este muda a cada versão.
2182
+ */
2183
+ function sayAdoption(c) {
2184
+ const a = c.adoption;
2185
+ if (!a || a.consumers.length === 0)
2186
+ return;
2187
+ const total = a.consumers.reduce((n, x) => n + x.files, 0);
2188
+ const using = a.consumers.reduce((n, x) => n + x.using, 0);
2189
+ console.log("");
2190
+ console.log(section("How far the system already reaches"));
2191
+ console.log(body(`${using} of ${total} source files across the folders you named import ${paint.strong(a.specifier)}. Everything else in them is still painting on its own.`));
2192
+ console.log("");
2193
+ for (const x of a.consumers) {
2194
+ const pct = x.files > 0 ? Math.round((x.using / x.files) * 100) : 0;
2195
+ console.log(body(`${paint.strong(x.label.padEnd(24))} ${String(x.using).padStart(4)} of ${String(x.files).padStart(4)} files ${paint.faint(`· ${pct}%`)}${x.using === 0 ? paint.faint(" - does not know the system yet") : ""}`));
2196
+ }
2197
+ console.log("");
2198
+ console.log(body(paint.faint("This is the number a migration is measured by, and it is the one that moves. Run the import again after a batch and the difference is the progress.")));
2199
+ }
2200
+ /**
2201
+ * DOIS ARQUIVOS, UM NOME - dito, com os dois caminhos.
2202
+ *
2203
+ * O silêncio aqui era o pior tipo: a pessoa recebe 36 componentes onde o repo
2204
+ * tem 39, e nada no relatório sugere que três existiram. Medido no monorepo do
2205
+ * dono: 20 nomes no repo inteiro, e um dos quatro `Card.tsx` é o do design system
2206
+ * dele competindo com dois que vivem sob rotas.
2207
+ *
2208
+ * A frase diz o que fazer, e são duas coisas diferentes: um escopo mais estreito
2209
+ * quando os homônimos são de outro projeto, ou um rename quando são do mesmo.
2210
+ */
2211
+ function sayCollisions(c) {
2212
+ const all = c.collisions ?? [];
2213
+ if (all.length === 0)
2214
+ return;
2215
+ /**
2216
+ * DUAS REGRAS, DUAS FRASES. Dentro de um escopo vence quem está mais perto da
2217
+ * raiz; entre escopos vence o primeiro `--scope`. Explicar um empate com a
2218
+ * regra do outro é descrever algo que não aconteceu.
2219
+ */
2220
+ const crossScope = all.filter((x) => x.by === "scope");
2221
+ const inScope = all.filter((x) => x.by !== "scope");
2222
+ const names = new Set(all.map((x) => x.name));
2223
+ console.log("");
2224
+ console.log(section("Two places, one name"));
2225
+ console.log(body(`${names.size} name${names.size === 1 ? "" : "s"} ${names.size === 1 ? "is" : "are"} defined more than once in what was measured, so ${all.length} definition${all.length === 1 ? "" : "s"} did not make it into this system.`));
2226
+ if (crossScope.length > 0) {
2227
+ console.log("");
2228
+ console.log(body(`${crossScope.length} of them in two different scopes - the FIRST --scope kept the name:`));
2229
+ console.log("");
2230
+ for (const x of crossScope.slice(0, 6))
2231
+ console.log(body(`${paint.strong(x.name.padEnd(22))} ${paint.faint("kept in")} ${x.kept} ${paint.faint("· also in")} ${x.dropped}`));
2232
+ if (crossScope.length > 6)
2233
+ console.log(body(paint.faint(`(${crossScope.length - 6} more)`)));
2234
+ }
2235
+ if (inScope.length > 0) {
2236
+ console.log("");
2237
+ console.log(body(`${inScope.length} inside a single scope - the file closest to its root kept the name, never the order the disk happened to return:`));
2238
+ console.log("");
2239
+ for (const x of inScope.slice(0, 6))
2240
+ console.log(body(`${paint.strong(x.name)}\n ${paint.faint("kept")} ${x.kept}\n ${paint.faint("dropped")} ${x.dropped}`));
2241
+ if (inScope.length > 6)
2242
+ console.log(body(paint.faint(`(${inScope.length - 6} more)`)));
2243
+ }
2244
+ console.log("");
2245
+ console.log(body(paint.faint("If the homonyms belong to different projects, a narrower --scope is the answer and nothing is lost. If they belong to the same one, renaming in your code is - two components with one name is one name your agent cannot resolve either.")));
2246
+ }
2060
2247
  /**
2061
2248
  * THEIR VOCABULARY, MEASURED. This section exists so every later finding can be
2062
2249
  * phrased as "you already do X, and here you did not" instead of "adopt ours".
@@ -3095,8 +3282,18 @@ export async function runImport(opts) {
3095
3282
  .trim()
3096
3283
  .replace(/^\.\/+/, "")
3097
3284
  .replace(/\/+$/, "");
3098
- const scope = opts.scope ? tidy(opts.scope) : undefined;
3099
- const readFrom = scope ? join(root, scope) : root;
3285
+ /**
3286
+ * OS ESCOPOS, na ordem em que a pessoa os escreveu - e `scope` é o primeiro.
3287
+ *
3288
+ * O parser de flags já acumula uma flag repetida (`--usage` usa isso desde
3289
+ * 01/08), então `--scope packages/ui --scope packages/charts` chega aqui como
3290
+ * array sem nenhuma mudança de gramática.
3291
+ */
3292
+ const scopes = (Array.isArray(opts.scope) ? opts.scope : opts.scope ? [opts.scope] : [])
3293
+ .map(tidy)
3294
+ .filter(Boolean)
3295
+ .filter((v, i, all) => all.indexOf(v) === i);
3296
+ const scope = scopes[0];
3100
3297
  /**
3101
3298
  * THE SECOND ROLE. `--scope` is the SYSTEM - one place, and the only source of
3102
3299
  * the palette, the scales and the convention. `--usage` is the EVIDENCE - as
@@ -3171,13 +3368,33 @@ export async function runImport(opts) {
3171
3368
  */
3172
3369
  phase(1, 3, "Measuring the repository");
3173
3370
  console.log(section("Reading your project"));
3174
- census = await takeCensus(readFrom, {
3175
- ...(usage.length > 0 ? { usage } : {}),
3176
- ...(scope ? { scopeLabel: scope } : {}),
3177
- ...(opts.cli ? { cli: opts.cli } : {}),
3178
- });
3179
- if (scope)
3180
- census.scope = scope;
3371
+ /**
3372
+ * UMA MEDIÇÃO POR ESCOPO, E DEPOIS A FUSÃO.
3373
+ *
3374
+ * `takeCensus` recebe UM `root` em cinco pontos estruturais que são por
3375
+ * PACOTE e não por sistema (`harvestOwnCss`, `internalSpecifiers`,
3376
+ * `publicApi`, o `walk`, `aliasesOf`), então ensiná-la a medir N raízes
3377
+ * seria refatorar a função de 1855 linhas que o corpus dourado protege.
3378
+ * Medir N vezes e fundir troca esse risco por uma função pura - e as regras
3379
+ * de empate ficam num arquivo em vez de espalhadas por um laço.
3380
+ *
3381
+ * A EVIDÊNCIA VIAJA COM O PRIMEIRO: `--usage` doa contagem e lei, e somar a
3382
+ * mesma evidência N vezes multiplicaria os números que decidem o que é
3383
+ * hábito. Ela entra uma vez, na medição do escopo que declara.
3384
+ */
3385
+ const measured = [];
3386
+ for (const [i, one] of (scopes.length > 0 ? scopes : [""]).entries()) {
3387
+ const at = one ? join(root, one) : root;
3388
+ const c = await takeCensus(at, {
3389
+ ...(i === 0 && usage.length > 0 ? { usage } : {}),
3390
+ ...(one ? { scopeLabel: one } : {}),
3391
+ ...(opts.cli ? { cli: opts.cli } : {}),
3392
+ });
3393
+ if (one)
3394
+ c.scope = one;
3395
+ measured.push(c);
3396
+ }
3397
+ census = mergeCensus(measured);
3181
3398
  }
3182
3399
  /**
3183
3400
  * DERIVE THE ANATOMY ON THE MEASURING PATH TOO.
package/dist/index.js CHANGED
@@ -89,7 +89,8 @@ Options:
89
89
  --version <n> install a specific version (default: latest)
90
90
  --ds <slug> init: bring this DS in right away · generate: target DS (default: installed)
91
91
  --name <name> preferred component name for generate
92
- --scope <path> import: the SYSTEM - tokens, components, convention. One folder.
92
+ --scope <path> import: the SYSTEM - tokens, components, convention. Repeatable;
93
+ the first one wins a tie.
93
94
  --usage <path> import: the EVIDENCE - counts, chosen values, laws. Repeatable.
94
95
  --scheme <s> dark|light - which end of your ladder is the default
95
96
  --target <t> template/init target: next | general (default: next)
@@ -160,7 +161,16 @@ async function main() {
160
161
  // Only these two words. Anything else is a typo that would silently
161
162
  // invert a system, so it falls through to being measured and asked.
162
163
  // WHAT to read. `--dir` stays WHERE the project is, in every command.
163
- scope: typeof flags.scope === "string" ? flags.scope : undefined,
164
+ /**
165
+ * `--scope` REPETÍVEL: um sistema pode viver em dois pacotes, e o parser
166
+ * já acumula flag repetida (`--usage` usa isso). A ordem é a que a pessoa
167
+ * escreveu, e ela decide os empates - ver `merge-census.ts`.
168
+ */
169
+ scope: Array.isArray(flags.scope)
170
+ ? flags.scope
171
+ : typeof flags.scope === "string"
172
+ ? flags.scope
173
+ : undefined,
164
174
  // WHERE THE EVIDENCE IS. Repeatable, and comma-separated works too - a
165
175
  // person typing one path with a comma in it means two paths.
166
176
  usage: [flags.usage]
@@ -0,0 +1,231 @@
1
+ const asRecord = (v) => (v ?? {});
2
+ /** Nome do escopo para as mensagens - `.` quando a medição não escopou nada. */
3
+ const nameOf = (c, i) => c.scope ?? (i === 0 ? "." : `#${i + 1}`);
4
+ /**
5
+ * OS TOKENS, com o primeiro escopo mandando - e o conflito registrado.
6
+ *
7
+ * `declared` é o coração: é dele que saem os nomes que viajam para a tela. Um
8
+ * `--brand-500` que vale `#1394dc` na biblioteca e `#0f6fa8` no pacote de
9
+ * gráficos não é um erro de ninguém, é uma divergência - e a lei do dono
10
+ * (§8.6) é que a plataforma não decide sozinha: ela mostra os dois e diz quem é
11
+ * a fonte declarada.
12
+ */
13
+ function mergeDeclared(into, from, keptScope, fromScope, conflicts) {
14
+ for (const [name, value] of Object.entries(from)) {
15
+ const held = into[name];
16
+ if (held === undefined) {
17
+ into[name] = value;
18
+ continue;
19
+ }
20
+ if (held === value)
21
+ continue;
22
+ conflicts.push({
23
+ kind: "token",
24
+ name,
25
+ kept: { scope: keptScope, value: held },
26
+ lost: { scope: fromScope, value },
27
+ });
28
+ }
29
+ }
30
+ /** Os valores medidos, somados por (natureza, valor) - e o nome vem de quem o tem. */
31
+ function mergeObserved(a, b) {
32
+ const out = a.map((v) => ({ ...v }));
33
+ const at = new Map(out.map((v, i) => [`${v.kind}:${v.value}`, i]));
34
+ for (const v of b) {
35
+ const key = `${v.kind}:${v.value}`;
36
+ const i = at.get(key);
37
+ if (i === undefined) {
38
+ at.set(key, out.length);
39
+ out.push({ ...v });
40
+ continue;
41
+ }
42
+ out[i].count += v.count;
43
+ out[i].files += v.files;
44
+ /** Um nome não se apaga por o outro escopo não ter um. */
45
+ if (!out[i].token && v.token)
46
+ out[i].token = v.token;
47
+ }
48
+ return out.sort((x, y) => y.count - x.count);
49
+ }
50
+ /** União preservando a ordem de chegada - a do primeiro escopo primeiro. */
51
+ const union = (a, b) => [
52
+ ...new Set([...(a ?? []), ...(b ?? [])]),
53
+ ];
54
+ const concat = (a, b) => [
55
+ ...(a ?? []),
56
+ ...(b ?? []),
57
+ ];
58
+ /** Soma campo a campo dois objetos de contagem, mantendo o que só um tem. */
59
+ function sumInto(a, b) {
60
+ if (!a)
61
+ return b;
62
+ if (!b)
63
+ return a;
64
+ const out = { ...a };
65
+ for (const [k, v] of Object.entries(b)) {
66
+ const mine = out[k];
67
+ if (typeof v === "number" && typeof mine === "number")
68
+ out[k] = mine + v;
69
+ else if (mine === undefined)
70
+ out[k] = v;
71
+ }
72
+ return out;
73
+ }
74
+ /**
75
+ * OS CENSOS, FUNDIDOS NA ORDEM EM QUE A PESSOA OS PEDIU.
76
+ *
77
+ * Devolve o próprio objeto quando há um só: o caminho comum - um escopo - não
78
+ * paga nada por esta função existir.
79
+ */
80
+ export function mergeCensus(list) {
81
+ if (list.length === 0)
82
+ throw new Error("mergeCensus needs at least one census");
83
+ if (list.length === 1)
84
+ return list[0];
85
+ const first = list[0];
86
+ const conflicts = [];
87
+ const collisions = [
88
+ ...(first.collisions ?? []),
89
+ ];
90
+ const declared = { ...asRecord(first.declared) };
91
+ const declaredAlt = { ...asRecord(first.declaredAlt) };
92
+ const keyframes = { ...(first.keyframes ?? {}) };
93
+ const looks = { ...(first.looks ?? {}) };
94
+ let observed = first.observed.map((v) => ({ ...v }));
95
+ let totals = { ...first.totals };
96
+ let coverage = first.coverage;
97
+ let gate = first.gate;
98
+ let reachability = first.reachability;
99
+ let usage = first.usage ?? undefined;
100
+ let animations = first.animations ?? undefined;
101
+ let skipped = first.skipped ?? undefined;
102
+ let brokenRefs = first.brokenRefs ?? undefined;
103
+ let conventions = first.conventions ?? undefined;
104
+ let architectures = first.architectures ?? undefined;
105
+ let components = first.components ?? undefined;
106
+ let defined = first.defined ?? undefined;
107
+ const stack = [...(first.project.stack ?? [])];
108
+ for (let i = 1; i < list.length; i++) {
109
+ const c = list[i];
110
+ const scopeName = nameOf(c, i);
111
+ const firstName = nameOf(first, 0);
112
+ mergeDeclared(declared, asRecord(c.declared), firstName, scopeName, conflicts);
113
+ mergeDeclared(declaredAlt, asRecord(c.declaredAlt), firstName, scopeName, conflicts);
114
+ /**
115
+ * O COMPONENTE QUE OS DOIS DEFINEM fica com a receita do primeiro escopo, e
116
+ * o descarte entra em `collisions` - a mesma lista que `name-claim.ts`
117
+ * alimenta dentro de UM escopo. Uma lista, dois níveis: o arquivo e o
118
+ * pacote, e a tela não precisa saber a diferença para dizer a verdade.
119
+ */
120
+ for (const [name, look] of Object.entries(c.looks ?? {})) {
121
+ if (looks[name] === undefined) {
122
+ looks[name] = look;
123
+ continue;
124
+ }
125
+ /** `by` diz que este empate foi resolvido pela ORDEM DOS ESCOPOS. */
126
+ collisions.push({
127
+ name,
128
+ kept: firstName,
129
+ dropped: scopeName,
130
+ by: "scope",
131
+ });
132
+ }
133
+ for (const [name, frames] of Object.entries(c.keyframes ?? {}))
134
+ if (keyframes[name] === undefined)
135
+ keyframes[name] = frames;
136
+ observed = mergeObserved(observed, c.observed);
137
+ totals = {
138
+ scanned: totals.scanned + c.totals.scanned,
139
+ values: totals.values + c.totals.values,
140
+ named: totals.named + c.totals.named,
141
+ tokenUses: totals.tokenUses + c.totals.tokenUses,
142
+ ownUses: (totals.ownUses ?? 0) + (c.totals.ownUses ?? 0),
143
+ phantomUses: (totals.phantomUses ?? 0) + (c.totals.phantomUses ?? 0),
144
+ /** Recalculado abaixo - somar percentuais não significa nada. */
145
+ coverage: 0,
146
+ };
147
+ coverage = sumInto(coverage, c.coverage);
148
+ gate = sumInto(gate, c.gate);
149
+ reachability = sumInto(reachability, c.reachability);
150
+ usage = union(usage, c.usage);
151
+ animations = union(animations, c.animations);
152
+ skipped = concat(skipped, c.skipped);
153
+ brokenRefs = concat(brokenRefs, c.brokenRefs);
154
+ conventions = concat(conventions, c.conventions);
155
+ architectures = concat(architectures, c.architectures);
156
+ components = concat(components, c.components);
157
+ defined = concat(defined, c.defined);
158
+ for (const s of c.project.stack ?? [])
159
+ if (!stack.includes(s))
160
+ stack.push(s);
161
+ }
162
+ /**
163
+ * A COBERTURA, RECALCULADA COM A FÓRMULA DE VERDADE.
164
+ *
165
+ * `(tokenUses + ownUses) / (tokenUses + ownUses + values + phantomUses)` - a
166
+ * mesma de `doctor/scan.ts:796`. A primeira versão desta função usou
167
+ * `named/values`, que parece razoável e não é a fórmula: fundir 48% e 30%
168
+ * devolvia 10% no monorepo do dono. Um número que não descreve nenhuma das
169
+ * medições é pior que não ter o número.
170
+ *
171
+ * Quando os censos vêm de um CLI que não gravava `ownUses`, a soma perde esses
172
+ * termos e o resultado seria menor que a verdade - então neste caso a
173
+ * cobertura do PRIMEIRO escopo é preservada, que é uma medição real de uma
174
+ * parte real, em vez de uma conta feita com metade dos termos.
175
+ */
176
+ const named = totals.tokenUses + (totals.ownUses ?? 0);
177
+ const denominator = named + totals.values + (totals.phantomUses ?? 0);
178
+ const complete = list.every((c) => c.totals.ownUses !== undefined);
179
+ totals.coverage = !complete
180
+ ? first.totals.coverage
181
+ : denominator > 0
182
+ ? Math.round((named / denominator) * 100)
183
+ : 100;
184
+ const out = {
185
+ ...first,
186
+ project: { ...first.project, stack },
187
+ /**
188
+ * `scope` SEGUE SENDO O PRIMEIRO, e não uma lista, porque há quem compare por
189
+ * igualdade: `live-census.ts:44` faz `census.scope === LIBRARY_SCOPE`, e um
190
+ * array ali seria um falso silencioso. `scopes` é aditivo - quem não sabe
191
+ * dele continua lendo o censo como antes.
192
+ */
193
+ scopes: list.map((c, i) => nameOf(c, i)),
194
+ declared,
195
+ observed,
196
+ totals,
197
+ };
198
+ if (Object.keys(declaredAlt).length > 0)
199
+ out.declaredAlt = declaredAlt;
200
+ if (Object.keys(keyframes).length > 0)
201
+ out.keyframes = keyframes;
202
+ if (Object.keys(looks).length > 0)
203
+ out.looks = looks;
204
+ if (coverage)
205
+ out.coverage = coverage;
206
+ if (gate)
207
+ out.gate = gate;
208
+ if (reachability)
209
+ out.reachability = reachability;
210
+ if (usage && usage.length > 0)
211
+ out.usage = usage;
212
+ if (animations && animations.length > 0)
213
+ out.animations = animations;
214
+ if (skipped && skipped.length > 0)
215
+ out.skipped = skipped;
216
+ if (brokenRefs && brokenRefs.length > 0)
217
+ out.brokenRefs = brokenRefs;
218
+ if (conventions && conventions.length > 0)
219
+ out.conventions = conventions;
220
+ if (architectures && architectures.length > 0)
221
+ out.architectures = architectures;
222
+ if (components && components.length > 0)
223
+ out.components = components;
224
+ if (defined && defined.length > 0)
225
+ out.defined = defined;
226
+ if (collisions.length > 0)
227
+ out.collisions = collisions;
228
+ if (conflicts.length > 0)
229
+ out.scopeConflicts = conflicts;
230
+ return out;
231
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * UM NOME, UM COMPONENTE - e o que fazer quando dois pedem o mesmo.
3
+ *
4
+ * O laço de leitura do import escreve em oito mapas indexados por NOME de
5
+ * componente (`looks`, `sourceOf`, `kindOf`, `propsOf`, `runtimeOf`,
6
+ * `variantAxes`, `islands`, `keyframes`). Dois `Card.tsx` em pastas diferentes
7
+ * disputam a mesma chave, e até aqui o último que o `walk` visitou sobrescrevia
8
+ * os outros - em silêncio, e por uma ordem que é do sistema de arquivos e não
9
+ * uma decisão de ninguém.
10
+ *
11
+ * Medido no monorepo do dono (19/08): 20 nomes colidem no repo inteiro, 14 dentro
12
+ * de `apps/web-dashboard/src`, e quatro arquivos se chamam `Card.tsx` - um deles
13
+ * é o `Card` do design system dele, e dois estão sob `_legacy`. `packages/ui`
14
+ * sozinho não tem colisão nenhuma: o problema mora em escopo largo, que é
15
+ * exatamente o default que a flexibilidade pede.
16
+ *
17
+ * ─────────────────────────────────────────────────────────────────────────
18
+ * O DESEMPATE É POR ESTRUTURA, E DETERMINÍSTICO
19
+ *
20
+ * A intuição primeira era "quem o pacote EXPORTA fica com o nome", e ela não
21
+ * responde: `publicApi().exports` é um conjunto de NOMES, e os dois candidatos
22
+ * têm o mesmo nome. Escolher pelo que o pacote realmente publica exige o grafo a
23
+ * partir da entrada do manifesto, e essa é a fatia que também entrega os grupos.
24
+ *
25
+ * Então: menos segmentos de caminho vence, e empate resolve alfabeticamente. É
26
+ * pouco, e é honesto - a diferença que importa é que a resposta deixa de mudar
27
+ * conforme a ordem em que o disco devolve os arquivos.
28
+ *
29
+ * E NUNCA POR NOME DE PASTA. Descartar `_legacy` por se chamar `_legacy` é a lei
30
+ * 14 sendo furada: no repo dele `_legacy` tem 178 arquivos e 106 estão VIVOS,
31
+ * montados por rota (medido 07/08). Classificação é por estrutura, nunca por nome.
32
+ *
33
+ * ─────────────────────────────────────────────────────────────────────────
34
+ * O PERDEDOR NÃO ENTRA, E ISSO É DITO
35
+ *
36
+ * Esta fatia não traz as duas receitas - trazer os dois `Card` exige o contexto
37
+ * viajando na chave, que é a fatia dos GRUPOS. O que ela conserta é a perda
38
+ * calada: cada descarte vira uma linha do censo com os dois caminhos, e o
39
+ * relatório os imprime. Uma lacuna declarada é confiança; calada é bug.
40
+ */
41
+ /** Profundidade como o caminho a declara - o número de pastas até o arquivo. */
42
+ const depth = (file) => file.split("/").length;
43
+ /**
44
+ * ESTE ARQUIVO FICA COM ESTE NOME?
45
+ *
46
+ * `true` quando ele detém o nome depois desta chamada - e aí quem chamou pode
47
+ * escrever nos mapas. `false` quando outro arquivo o detém, e a disputa foi
48
+ * registrada em `collisions`.
49
+ *
50
+ * Idempotente para o mesmo arquivo: o laço lê um arquivo uma vez, mas um
51
+ * componente pode ser reivindicado por mais de um caminho de código, e cobrar
52
+ * uma colisão de um arquivo contra si mesmo seria inventar um conflito.
53
+ */
54
+ export function claimName(claims, name, file) {
55
+ const held = claims.by.get(name);
56
+ if (held === undefined) {
57
+ claims.by.set(name, file);
58
+ return true;
59
+ }
60
+ if (held === file)
61
+ return true;
62
+ const incumbentWins = depth(held) < depth(file) ||
63
+ (depth(held) === depth(file) && held.localeCompare(file) <= 0);
64
+ if (incumbentWins) {
65
+ claims.collisions.push({ name, kept: held, dropped: file, by: "path" });
66
+ return false;
67
+ }
68
+ claims.by.set(name, file);
69
+ claims.collisions.push({ name, kept: file, dropped: held, by: "path" });
70
+ return true;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.252",
3
+ "version": "0.16.254",
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": {