synthesisui 0.16.253 → 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,7 @@ 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";
38
39
  import { claimName } from "../name-claim.js";
39
40
  import { body, paint, section } from "../output.js";
40
41
  import { phase, startProgress } from "../progress.js";
@@ -1147,13 +1148,39 @@ export async function takeCensus(root, opts) {
1147
1148
  const edges = new Map();
1148
1149
  /** Por raiz: `@/` é uma pasta diferente em cada app - ver `reachabilityOf`. */
1149
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();
1150
1160
  for (const u of opts?.usage ?? []) {
1151
1161
  const uInternal = await internalSpecifiers(u.path);
1152
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);
1153
1165
  for await (const file of walk(u.path)) {
1154
1166
  const src = await readFile(file, "utf8").catch(() => "");
1155
1167
  if (!src)
1156
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
+ }
1157
1184
  edges.set(file, edgesIn(src));
1158
1185
  const rel = join(u.label, relative(u.path, file));
1159
1186
  if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
@@ -1914,6 +1941,18 @@ export async function takeCensus(root, opts) {
1914
1941
  * só aparece quando há algo a dizer não muda nada para quem já lê o censo.
1915
1942
  */
1916
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
+ : {}),
1917
1956
  ...(conventions.length > 0 ? { conventions } : {}),
1918
1957
  classStyle,
1919
1958
  ...(Object.keys(looks).length > 0 ? { looks } : {}),
@@ -2029,6 +2068,8 @@ export async function takeCensus(root, opts) {
2029
2068
  named: d.named,
2030
2069
  tokenUses: d.tokenUses,
2031
2070
  coverage: d.coverage,
2071
+ ownUses: d.ownUses,
2072
+ phantomUses: d.phantomUses,
2032
2073
  },
2033
2074
  };
2034
2075
  }
@@ -2089,9 +2130,73 @@ async function summarize(c, root, scope) {
2089
2130
  }
2090
2131
  }
2091
2132
  sayConventions(c);
2133
+ sayScopes(c);
2134
+ sayAdoption(c);
2092
2135
  sayCollisions(c);
2093
2136
  await sayBrokenRefs(c, root, scope);
2094
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
+ }
2095
2200
  /**
2096
2201
  * DOIS ARQUIVOS, UM NOME - dito, com os dois caminhos.
2097
2202
  *
@@ -2107,16 +2212,35 @@ function sayCollisions(c) {
2107
2212
  const all = c.collisions ?? [];
2108
2213
  if (all.length === 0)
2109
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");
2110
2222
  const names = new Set(all.map((x) => x.name));
2111
2223
  console.log("");
2112
- console.log(section("Two files, one name"));
2113
- 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. The one closest to the root of the scope kept the name - never the order the disk happened to return.`));
2114
- console.log("");
2115
- for (const x of all.slice(0, 6)) {
2116
- console.log(body(`${paint.strong(x.name)} ${paint.faint("kept")} ${x.kept}\n ${paint.faint("dropped")} ${x.dropped}`));
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)`)));
2117
2243
  }
2118
- if (all.length > 6)
2119
- console.log(body(paint.faint(`(${all.length - 6} more)`)));
2120
2244
  console.log("");
2121
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.")));
2122
2246
  }
@@ -3158,8 +3282,18 @@ export async function runImport(opts) {
3158
3282
  .trim()
3159
3283
  .replace(/^\.\/+/, "")
3160
3284
  .replace(/\/+$/, "");
3161
- const scope = opts.scope ? tidy(opts.scope) : undefined;
3162
- 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];
3163
3297
  /**
3164
3298
  * THE SECOND ROLE. `--scope` is the SYSTEM - one place, and the only source of
3165
3299
  * the palette, the scales and the convention. `--usage` is the EVIDENCE - as
@@ -3234,13 +3368,33 @@ export async function runImport(opts) {
3234
3368
  */
3235
3369
  phase(1, 3, "Measuring the repository");
3236
3370
  console.log(section("Reading your project"));
3237
- census = await takeCensus(readFrom, {
3238
- ...(usage.length > 0 ? { usage } : {}),
3239
- ...(scope ? { scopeLabel: scope } : {}),
3240
- ...(opts.cli ? { cli: opts.cli } : {}),
3241
- });
3242
- if (scope)
3243
- 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);
3244
3398
  }
3245
3399
  /**
3246
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
+ }
@@ -62,10 +62,10 @@ export function claimName(claims, name, file) {
62
62
  const incumbentWins = depth(held) < depth(file) ||
63
63
  (depth(held) === depth(file) && held.localeCompare(file) <= 0);
64
64
  if (incumbentWins) {
65
- claims.collisions.push({ name, kept: held, dropped: file });
65
+ claims.collisions.push({ name, kept: held, dropped: file, by: "path" });
66
66
  return false;
67
67
  }
68
68
  claims.by.set(name, file);
69
- claims.collisions.push({ name, kept: file, dropped: held });
69
+ claims.collisions.push({ name, kept: file, dropped: held, by: "path" });
70
70
  return true;
71
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.253",
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": {