synthesisui 0.16.254 → 0.16.255

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.
@@ -154,7 +154,20 @@ export async function add(slug, opts) {
154
154
  * censo (um `add` puro, sem import antes), o que o lock já dizia é preservado.
155
155
  */
156
156
  const measured = await censusScope(projectRoot);
157
- const scope = measured.system ?? prev?.scope ?? null;
157
+ /**
158
+ * OS DOIS CAMPOS: `scopes` com tudo e `scope` com o primeiro.
159
+ *
160
+ * O lock é o arquivo que um time COMMITA, e é o que faz um clone fresco saber de
161
+ * onde o sistema foi medido sem ida à rede. Guardar só o primeiro escopo ali
162
+ * fazia o `sync` de um clone novo remedir metade de um sistema de dois escopos.
163
+ * Um CLI anterior lê `scope` e segue funcionando.
164
+ */
165
+ const systems = measured.systems.length > 0
166
+ ? measured.systems
167
+ : measured.system
168
+ ? [measured.system]
169
+ : (prev?.scopes ?? (prev?.scope ? [prev.scope] : []));
170
+ const scope = systems[0] ?? null;
158
171
  const usage = measured.usage.length > 0 ? measured.usage : (prev?.usage ?? []);
159
172
  /**
160
173
  * `fetchedAt` SÓ ANDA QUANDO ALGO ATERROU - e a alternativa sujava o git de um time inteiro.
@@ -177,6 +190,7 @@ export async function add(slug, opts) {
177
190
  ...(payload.compiler != null ? { compiler: payload.compiler } : {}),
178
191
  ...(payload.rulesStamp ? { rules: payload.rulesStamp } : {}),
179
192
  ...(scope ? { scope } : {}),
193
+ ...(systems.length > 0 ? { scopes: systems } : {}),
180
194
  ...(usage.length > 0 ? { usage } : {}),
181
195
  };
182
196
  const landed = (() => {
@@ -42,7 +42,21 @@ import { resolveDeps } from "../stack.js";
42
42
  /** Files that compose components to SHOW them, not to ship them. */
43
43
  const IS_ASIDE = /(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/;
44
44
  const EXTS = [".tsx", ".ts", ".jsx", ".js", ".css", ".scss", ".vue", ".svelte"];
45
- const SKIP = new Set([
45
+ /**
46
+ * EXPORTADO porque quem mede o repositório fora deste arquivo tem que pular as
47
+ * MESMAS pastas.
48
+ *
49
+ * `workspace-place.ts` contava as declarações de cada pacote com a sua própria
50
+ * regra (dotdir + node_modules) e somava `packages/ui/dist/index.css`: 496
51
+ * declarações de bundle contra 97 de fonte no repo do dono, medido em 19/08. O
52
+ * número que decidia qual pasta recomendar era cinco sextos build output - e o
53
+ * censo, que usa este SKIP, nunca leu nada daquilo.
54
+ *
55
+ * Duas listas de pastas a ignorar é a mesma classe de divergência que
56
+ * `loadSystem` existe para matar: nenhuma delas está errada isolada, e juntas
57
+ * dizem números diferentes sobre o mesmo repositório.
58
+ */
59
+ export const SKIP = new Set([
46
60
  "node_modules",
47
61
  ".next",
48
62
  ".git",
@@ -551,7 +565,7 @@ export async function doctor(opts) {
551
565
  * lista de prioridades tirada de um app consumidor num repo de 2787 arquivos (dono, 06/08).
552
566
  */
553
567
  const measured = (opts.scopes ?? []).length > 0
554
- ? { system: null, usage: [], from: "none" }
568
+ ? { system: null, systems: [], usage: [], from: "none" }
555
569
  : await measuredScope(root);
556
570
  const relScopes = (opts.scopes ?? []).length > 0
557
571
  ? opts.scopes
@@ -2598,8 +2598,21 @@ async function sayAboutPlace(root, scope) {
2598
2598
  console.log(section("This folder is one package of a workspace"));
2599
2599
  console.log(body(`${paint.strong(place.here)} is one of ${place.packages.length} packages under ${paint.strong(place.workspaceRoot)}.`));
2600
2600
  console.log("");
2601
- console.log(body(`And the vocabulary is not here: ${paint.strong(vocab.rel)} declares ${vocab.declarations} token${vocab.declarations === 1 ? "" : "s"} across ${vocab.files} file${vocab.files === 1 ? "" : "s"}, while this package declares ${mine?.declarations ?? 0} across ${mine?.files ?? 0}.`));
2602
- console.log(body(paint.faint("A system built from the app is the average of one consumer; the tokens live where the library declares them.")));
2601
+ console.log(body(
2602
+ /**
2603
+ * A FRASE SEGUE O CRITÉRIO, e o critério é estrutural.
2604
+ *
2605
+ * Enquanto a contagem incluía `dist`, dizer "o vocabulário não está aqui"
2606
+ * fechava com os números: 764 contra 114. Com o build output fora, a
2607
+ * biblioteca declara 97 e o APP declara 114 - a frase antiga passaria a
2608
+ * contradizer os dois números que ela mesma imprime.
2609
+ *
2610
+ * O que faz `packages/ui` ser a fonte de verdade não é volume: é os outros
2611
+ * pacotes o importarem. Dizer isso é dizer a razão verdadeira, e ela
2612
+ * sobrevive ao caso em que o app declara mais.
2613
+ */
2614
+ `${paint.strong(vocab.rel)} is what the other ${vocab.importedBy} package${vocab.importedBy === 1 ? "" : "s"} here import${vocab.importedBy === 1 ? "s" : ""} - that is the shared vocabulary, ${vocab.declarations} token${vocab.declarations === 1 ? "" : "s"} across ${vocab.files} file${vocab.files === 1 ? "" : "s"}. This package declares ${mine?.declarations ?? 0} of its own across ${mine?.files ?? 0}, and those are this app's.`));
2615
+ console.log(body(paint.faint("A system measured from the app is one consumer's set of choices; the shared vocabulary is where the library declares it.")));
2603
2616
  console.log("");
2604
2617
  console.log(body("The system is one place, and the evidence is another:"));
2605
2618
  console.log("");
@@ -3424,8 +3437,30 @@ export async function runImport(opts) {
3424
3437
  * `sayAboutPlace` faz as duas perguntas na ordem: onde este comando está, e
3425
3438
  * onde as declarações estão. Ele só fala quando as respostas divergem.
3426
3439
  */
3427
- if (!opts.census)
3440
+ /**
3441
+ * OS CANDIDATOS ENTRAM NO CENSO, e é o mesmo `placeInWorkspace` que a mensagem
3442
+ * de lugar já usa - uma medição, dois consumidores.
3443
+ *
3444
+ * Vive aqui e não dentro de `takeCensus` porque a pergunta é sobre o
3445
+ * WORKSPACE, e `takeCensus` mede um escopo: chamá-la de lá faria cada escopo
3446
+ * medido recontar os mesmos cinco pacotes.
3447
+ */
3448
+ if (!opts.census) {
3449
+ const place = await placeInWorkspace(root).catch(() => null);
3450
+ if (place)
3451
+ census.workspace = {
3452
+ root: place.workspaceRoot,
3453
+ here: place.here,
3454
+ packages: place.packages.map((p) => ({
3455
+ rel: p.rel,
3456
+ name: p.name,
3457
+ declarations: p.declarations,
3458
+ files: p.files,
3459
+ importedBy: p.importedBy,
3460
+ })),
3461
+ };
3428
3462
  await sayAboutPlace(root, scope);
3463
+ }
3429
3464
  // A census handed to us is written BACK TO ITSELF; one we took lands at the
3430
3465
  // ROOT, whatever it measured. Everything else in `_synthesisui/` is anchored
3431
3466
  // there - config, `ds/<slug>/`, the hook's marker - and the census was the
@@ -1042,6 +1042,23 @@ async function describeComponent(root, name) {
1042
1042
  if ((recipe.companions ?? []).length > 0) {
1043
1043
  out.push("", `Goes with: ${(recipe.companions ?? []).join(", ")} - measured pairings; call describe_component on one before composing them together.`);
1044
1044
  }
1045
+ /**
1046
+ * ONDE ELE MORA, E COMO CHEGAR NELE - a linha que decide se o agente importa
1047
+ * ou reescreve.
1048
+ *
1049
+ * `api.from` é o especificador do pacote que o EXPORTA (a lei 13 já o mediu), e
1050
+ * ele só existe para um componente de biblioteca. Quando há um, importar é a
1051
+ * resposta e escrever de novo é duplicar. Quando não há, o caminho é o que
1052
+ * localiza o arquivo - e num sistema de dois escopos ele é a única coisa que
1053
+ * diz de qual dos dois este componente é.
1054
+ */
1055
+ if (recipe.source?.file || recipe.api?.from) {
1056
+ const where = recipe.source?.file;
1057
+ const pkg = recipe.api?.from;
1058
+ out.push("", pkg
1059
+ ? `Lives in: ${where ?? pkg} - exported by ${pkg}. Import it from there; writing it again duplicates a component this system already owns.`
1060
+ : `Lives in: ${where}${recipe.source?.scope ? ` (scope ${recipe.source.scope})` : ""} - local to that folder, and not exported by a package.`);
1061
+ }
1045
1062
  if (recipe.usageStats && recipe.usageStats.count > 0) {
1046
1063
  const u = recipe.usageStats;
1047
1064
  out.push("", `In use: ${u.count} instance${u.count === 1 ? "" : "s"} across ${u.files} file${u.files === 1 ? "" : "s"}${u.projects?.length ? ` in ${u.projects.join(", ")}` : ""}.`);
@@ -6,10 +6,11 @@ import { markSent, readEvents } from "../doctor/ledger.js";
6
6
  import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
7
7
  import { describeDelta, fingerprintReadings, readSyncMark, writeSyncMark, } from "../last-sync.js";
8
8
  import { measuredScope, rememberScope } from "../measured-scope.js";
9
+ import { mergeCensus } from "../merge-census.js";
9
10
  import { body, paint, section, snippet } from "../output.js";
10
11
  import { repoStateOf } from "../repo-state.js";
11
12
  import { reportWhatIsLeft } from "./align.js";
12
- import { resolveReadParts, siblingProjects, takeCensus } from "./import.js";
13
+ import { resolveReadParts, siblingProjects, takeCensus, } from "./import.js";
13
14
  /**
14
15
  * What each decision means ON THIS MACHINE - the card decides, the sync
15
16
  * enacts. The web never touches the local file; this line is printed right
@@ -247,7 +248,25 @@ export async function remeasure(args) {
247
248
  * O servidor continua valendo como segunda fonte: um clone fresco tem o `.lock` e não tem censo.
248
249
  */
249
250
  const local = await measuredScope(root);
250
- let scope = local.system ?? stored.scope ?? null;
251
+ /**
252
+ * TODOS os escopos do sistema, não só o primeiro.
253
+ *
254
+ * Esta linha lia `local.system` - singular - e um sistema importado de dois
255
+ * escopos remedia só um deles: 351 componentes nasciam e 36 eram remedidos. Os
256
+ * outros 315 não eram apagados (`presenceOf` os conta como fora do escopo
257
+ * medido, de propósito) e também nunca mais eram atualizados - uma feature que
258
+ * emudece no dia seguinte.
259
+ *
260
+ * A queda para `scope` mantém todo sistema anterior lendo como antes.
261
+ */
262
+ let scopes = local.systems.length > 0
263
+ ? local.systems
264
+ : local.system
265
+ ? [local.system]
266
+ : stored.scope
267
+ ? [stored.scope]
268
+ : [];
269
+ let scope = scopes[0] ?? null;
251
270
  let usage = local.usage.length > 0 ? local.usage : (stored.usage ?? []);
252
271
  /**
253
272
  * E QUANDO NINGUÉM SABE, ESTE COMANDO PERGUNTA - uma vez, e grava a resposta.
@@ -263,26 +282,43 @@ export async function remeasure(args) {
263
282
  const asked = await askForScope(root);
264
283
  if (asked) {
265
284
  scope = asked.scope;
285
+ scopes = [asked.scope];
266
286
  if (asked.usage.length > 0)
267
287
  usage = asked.usage;
268
- await rememberScope(root, slug, scope, usage);
288
+ await rememberScope(root, slug, scopes, usage);
269
289
  }
270
290
  }
271
291
  console.log(section("Measuring your repository again"));
272
- console.log(body(paint.faint(`${scope ? `reading ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
292
+ console.log(body(paint.faint(`${scopes.length > 1 ? `reading ${scopes.join(" + ")}` : scope ? `reading ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
273
293
  ? " · read from _synthesisui/census.json, where the last measurement recorded it"
274
294
  : ""} · what you already wrote about your app travels unchanged`)));
275
- const census = await takeCensus(scope ? join(root, scope) : root, {
276
- ...(scope ? { scopeLabel: scope } : {}),
277
- ...(args.cli ? { cli: args.cli } : {}),
278
- /** Uma RE-medição conta o que MUDOU; o relatório inteiro fica no `--full` - ver `say`. */
279
- ...(args.full ? {} : { quiet: true }),
280
- /** O `--usage` é EVIDÊNCIA e leva rótulo: sem ele o censo não sabe de que app o número veio. */
281
- usage: usage.map((u) => ({
282
- path: join(root, u),
283
- label: u,
284
- })),
285
- }).catch(() => null);
295
+ /**
296
+ * UMA MEDIÇÃO POR ESCOPO, FUNDIDAS - o mesmo desenho do `import`.
297
+ *
298
+ * `takeCensus` recebe um `root` em cinco pontos que são por PACOTE, então medir
299
+ * N raízes de dentro dela seria refatorar a função que o corpus dourado guarda.
300
+ * A evidência (`--usage`) entra UMA vez, com o primeiro escopo: somá-la N vezes
301
+ * multiplicaria as contagens que decidem o que é hábito.
302
+ */
303
+ const measuredList = [];
304
+ for (const [i, one] of (scopes.length > 0 ? scopes : [""]).entries()) {
305
+ const c = await takeCensus(one ? join(root, one) : root, {
306
+ ...(one ? { scopeLabel: one } : {}),
307
+ ...(args.cli ? { cli: args.cli } : {}),
308
+ /** Uma RE-medição conta o que MUDOU; o relatório inteiro fica no `--full` - ver `say`. */
309
+ ...(args.full ? {} : { quiet: true }),
310
+ /** O `--usage` é EVIDÊNCIA e leva rótulo: sem ele o censo não sabe de que app o número veio. */
311
+ usage: i === 0 ? usage.map((u) => ({ path: join(root, u), label: u })) : [],
312
+ }).catch(() => null);
313
+ if (!c)
314
+ break;
315
+ if (one)
316
+ c.scope = one;
317
+ measuredList.push(c);
318
+ }
319
+ const census = measuredList.length === (scopes.length > 0 ? scopes.length : 1)
320
+ ? mergeCensus(measuredList)
321
+ : null;
286
322
  if (!census) {
287
323
  console.log(body("The measurement failed, so nothing was sent."));
288
324
  return;
@@ -100,7 +100,7 @@
100
100
  *
101
101
  * Quem instalou antes desta versão tem o stub que estanca: o `upgrade`/`connect` é o que alcança.
102
102
  */
103
- export const MATERIALISER_SINCE = "0.16.241";
103
+ export const MATERIALISER_SINCE = "0.16.255";
104
104
  /**
105
105
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
106
106
  *
@@ -1,6 +1,11 @@
1
1
  import { readdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
- const EMPTY = { system: null, usage: [], from: "none" };
3
+ const EMPTY = {
4
+ system: null,
5
+ systems: [],
6
+ usage: [],
7
+ from: "none",
8
+ };
4
9
  /**
5
10
  * O ESCOPO NO PONTEIRO DO SISTEMA, que é onde ele não se perde.
6
11
  *
@@ -27,22 +32,42 @@ async function scopeInLock(root) {
27
32
  catch {
28
33
  continue;
29
34
  }
30
- const scope = typeof lock.scope === "string" && lock.scope.trim()
31
- ? lock.scope.trim()
32
- : null;
35
+ /**
36
+ * `scopes` PRIMEIRO, `scope` como queda - e nunca o contrário.
37
+ *
38
+ * Um lock escrito antes desta versão só tem `scope`, e ele continua sendo lido
39
+ * exatamente como antes. Um lock novo tem os dois, e a lista é a verdade
40
+ * completa: ler o singular ali daria um sistema de um escopo onde há dois.
41
+ */
42
+ const listed = Array.isArray(lock.scopes)
43
+ ? lock.scopes.filter((x) => typeof x === "string" && !!x.trim())
44
+ : [];
45
+ const scope = listed[0] ??
46
+ (typeof lock.scope === "string" && lock.scope.trim()
47
+ ? lock.scope.trim()
48
+ : null);
49
+ const declared = listed.length > 0 ? listed : scope ? [scope] : [];
33
50
  const usage = Array.isArray(lock.usage)
34
51
  ? lock.usage.filter((u) => typeof u === "string" && !!u.trim())
35
52
  : [];
36
- if (!scope && usage.length === 0)
53
+ if (declared.length === 0 && usage.length === 0)
37
54
  continue;
38
- const system = scope && (await present(root, scope)) ? scope : null;
55
+ const systems = [];
56
+ for (const d of declared)
57
+ if (await present(root, d))
58
+ systems.push(d);
39
59
  const kept = [];
40
60
  for (const u of usage)
41
61
  if (await present(root, u))
42
62
  kept.push(u);
43
- if (!system && kept.length === 0)
63
+ if (systems.length === 0 && kept.length === 0)
44
64
  continue;
45
- return { system, usage: kept, from: "lock" };
65
+ return {
66
+ system: systems[0] ?? null,
67
+ systems,
68
+ usage: kept,
69
+ from: "lock",
70
+ };
46
71
  }
47
72
  return null;
48
73
  }
@@ -81,20 +106,29 @@ export async function censusScope(root) {
81
106
  catch {
82
107
  return EMPTY;
83
108
  }
84
- const scope = typeof parsed.scope === "string" && parsed.scope.trim()
85
- ? parsed.scope.trim()
86
- : null;
109
+ /** `scopes` primeiro, `scope` como queda - ver `scopeInLock`, mesma regra. */
110
+ const listed = Array.isArray(parsed.scopes)
111
+ ? parsed.scopes.filter((x) => typeof x === "string" && !!x.trim())
112
+ : [];
113
+ const scope = listed[0] ??
114
+ (typeof parsed.scope === "string" && parsed.scope.trim()
115
+ ? parsed.scope.trim()
116
+ : null);
117
+ const declared = listed.length > 0 ? listed : scope ? [scope] : [];
87
118
  const usage = Array.isArray(parsed.usage)
88
119
  ? parsed.usage.filter((u) => typeof u === "string" && !!u.trim())
89
120
  : [];
90
- const system = scope && (await present(root, scope)) ? scope : null;
121
+ const systems = [];
122
+ for (const d of declared)
123
+ if (await present(root, d))
124
+ systems.push(d);
91
125
  const kept = [];
92
126
  for (const u of usage)
93
127
  if (await present(root, u))
94
128
  kept.push(u);
95
- if (!system && kept.length === 0)
129
+ if (systems.length === 0 && kept.length === 0)
96
130
  return EMPTY;
97
- return { system, usage: kept, from: "census" };
131
+ return { system: systems[0] ?? null, systems, usage: kept, from: "census" };
98
132
  }
99
133
  /**
100
134
  * As pastas a LER, na ordem em que importam - o sistema primeiro.
@@ -103,7 +137,12 @@ export async function censusScope(root) {
103
137
  * anotações no CI, decide o que chega ao PR.
104
138
  */
105
139
  export function scopePaths(m) {
106
- return [...(m.system ? [m.system] : []), ...m.usage];
140
+ /**
141
+ * TODOS os sistemas, não só o primeiro - senão `absorb` e `summary` veem meia
142
+ * biblioteca num sistema importado de dois escopos.
143
+ */
144
+ const systems = m.systems.length > 0 ? m.systems : m.system ? [m.system] : [];
145
+ return [...systems, ...m.usage];
107
146
  }
108
147
  /** Como dizer o que foi lido. Uma leitura que não declara o universo é um número sem denominador. */
109
148
  export function describeScope(m) {
@@ -131,7 +170,18 @@ export function describeScope(m) {
131
170
  *
132
171
  * Silencioso quando não há lock: não é erro, é um repo sem sistema instalado.
133
172
  */
134
- export async function rememberScope(root, slug, scope, usage) {
173
+ export async function rememberScope(root, slug,
174
+ /**
175
+ * OS ESCOPOS, na ordem em que a pessoa os pediu - o primeiro é a autoridade.
176
+ *
177
+ * Aceita a lista e grava OS DOIS campos: `scopes` com tudo e `scope` com o
178
+ * primeiro. Um CLI anterior lê `scope` e continua funcionando; um lock antigo
179
+ * lido por este CLI cai no singular. É a compatibilidade nos dois sentidos, e
180
+ * ela custa uma linha.
181
+ */
182
+ scopes, usage) {
183
+ const list = (Array.isArray(scopes) ? scopes : scopes ? [scopes] : []).filter(Boolean);
184
+ const scope = list[0] ?? null;
135
185
  const path = join(root, "_synthesisui", "ds", slug, ".lock");
136
186
  const raw = await readFile(path, "utf8").catch(() => null);
137
187
  if (!raw)
@@ -146,6 +196,7 @@ export async function rememberScope(root, slug, scope, usage) {
146
196
  await writeFile(path, `${JSON.stringify({
147
197
  ...lock,
148
198
  ...(scope ? { scope } : {}),
199
+ ...(list.length > 0 ? { scopes: list } : {}),
149
200
  ...(usage.length > 0 ? { usage } : {}),
150
201
  }, null, 2)}\n`, "utf8");
151
202
  }
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
+ import { SKIP } from "./commands/doctor.js";
3
4
  const MANIFEST = "package.json";
4
5
  const SHEET = /\.(css|scss|sass|less)$/i;
5
6
  const DECL = /(^|[\s{;])--[a-zA-Z0-9-]+\s*:/g;
@@ -28,9 +29,11 @@ function workspaceGlobs(p) {
28
29
  * Quantas custom properties um pacote declara - o mesmo peso que
29
30
  * `siblingProjects` já usa para escolher entre `packages/core` e `packages/ui`.
30
31
  *
31
- * Diretório com ponto e `node_modules` ficam fora pelo mesmo motivo que o `walk`
32
- * do doctor os ignora: `.next/` guarda CSS COMPILADO, e contá-lo faria o build
33
- * output votar em onde mora o vocabulário.
32
+ * A LISTA DE PASTAS IGNORADAS É A DO `walk`, importada e não reescrita. A primeira
33
+ * versão pulava dotdir e `node_modules` por conta própria e somava
34
+ * `packages/ui/dist/index.css`: 496 declarações de bundle contra 97 de fonte no
35
+ * repo do dono (19/08). O número que escolhia a pasta recomendada era cinco
36
+ * sextos build output, e o censo - que usa o SKIP - nunca leu nada daquilo.
34
37
  */
35
38
  async function weigh(dir) {
36
39
  let declarations = 0;
@@ -39,7 +42,7 @@ async function weigh(dir) {
39
42
  if (depth > 8)
40
43
  return;
41
44
  for (const e of await readdir(at, { withFileTypes: true }).catch(() => [])) {
42
- if (e.name.startsWith(".") || e.name === "node_modules")
45
+ if (e.name.startsWith(".") || SKIP.has(e.name))
43
46
  continue;
44
47
  const full = join(at, e.name);
45
48
  if (e.isDirectory()) {
@@ -125,16 +128,29 @@ export async function placeInWorkspace(root) {
125
128
  declarations,
126
129
  files,
127
130
  dependsOn: Object.keys(deps).filter((k) => local.has(k)),
131
+ /** Preenchido no passo seguinte, quando todos os `dependsOn` existem. */
132
+ importedBy: 0,
128
133
  });
129
134
  }
135
+ for (const pkg of packages) {
136
+ if (!pkg.name)
137
+ continue;
138
+ pkg.importedBy = packages.filter((other) => other !== pkg && other.dependsOn.includes(pkg.name)).length;
139
+ }
130
140
  if (packages.length === 0)
131
141
  return null;
132
142
  const rel = relative(workspaceRoot, root).split("\\").join("/");
133
- const ranked = [...packages].sort((a, b) => b.declarations - a.declarations);
143
+ /**
144
+ * A ORDEM: quem os outros importam vem primeiro, e a contagem só desempata.
145
+ * Um pacote que ninguém importa e que declara muito é um consumidor com
146
+ * vocabulário próprio - é um candidato legítimo, e não é a biblioteca.
147
+ */
148
+ const ranked = [...packages].sort((a, b) => b.importedBy - a.importedBy || b.declarations - a.declarations);
149
+ const lead = ranked[0];
134
150
  return {
135
151
  workspaceRoot,
136
152
  here: rel === "" ? null : rel,
137
153
  packages,
138
- vocabulary: ranked[0]?.declarations > 0 ? ranked[0] : null,
154
+ vocabulary: lead && (lead.declarations > 0 || lead.importedBy > 0) ? lead : null,
139
155
  };
140
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.254",
3
+ "version": "0.16.255",
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": {