synthesisui 0.16.158 → 0.16.160

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.
@@ -36,6 +36,22 @@ export async function hookCommand(root, version) {
36
36
  ? "npx --no-install synthesisui hook"
37
37
  : `npx synthesisui@${version} hook`;
38
38
  }
39
+ /**
40
+ * COMO A SESSÃO CHAMA O CLI, e por que este NÃO é pinado.
41
+ *
42
+ * O hook de escrita é pinado porque roda dezenas de vezes por sessão e um verificador que muda sob
43
+ * você a cada edit é indebugável. Este roda UMA vez, e o trabalho dele é justamente dizer o que está
44
+ * atrasado - inclusive o próprio CLI. Um `align` pinado no dia do `connect` não conhece nenhuma
45
+ * verificação escrita depois, e passa a garantir silêncio em vez de alinho.
46
+ *
47
+ * O custo do `npx` (720ms-1.5s, medido em 27/07) é pago uma vez na abertura, não por escrita.
48
+ */
49
+ export async function alignCommand(root) {
50
+ const local = join(root, "node_modules", ".bin", "synthesisui");
51
+ return (await exists(local))
52
+ ? "npx --no-install synthesisui align"
53
+ : "npx synthesisui@latest align";
54
+ }
39
55
  /**
40
56
  * Returns the command that IS in the file, not the one we would have written.
41
57
  *
@@ -211,9 +227,45 @@ export async function wireAgent(root, version, want) {
211
227
  command: hook.command,
212
228
  hook: hook.status,
213
229
  ...(hook.was ? { was: hook.was } : {}),
230
+ /**
231
+ * A VERIFICAÇÃO DE ABERTURA vai junto do hook de escrita, e por um motivo medido: as onze
232
+ * ferramentas MCP são todas PULL, o agente só descobre um desalinho se resolver perguntar, e o
233
+ * único hook instalado rodava DEPOIS de uma escrita - tarde demais para dizer "você não está
234
+ * logado" ou "este sistema não sabe de onde foi medido".
235
+ */
236
+ session: want.hook
237
+ ? await wireSessionStart(root, await alignCommand(root))
238
+ : "skipped",
214
239
  mcp: want.mcp ? await wireMcp(root, version) : "skipped",
215
240
  };
216
241
  }
242
+ /**
243
+ * O hook `SessionStart`, escrito com a mesma disciplina do de escrita: vizinho nenhum é tocado, e um
244
+ * comando escrito à mão fica como está. Só um `align` nosso apontando para outro lugar é substituído.
245
+ */
246
+ async function wireSessionStart(root, command) {
247
+ const dir = join(root, ".claude");
248
+ const path = join(dir, "settings.json");
249
+ const settings = await readJson(path);
250
+ const hooks = (settings.hooks ?? {});
251
+ const entries = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
252
+ for (const entry of entries)
253
+ for (const h of entry.hooks ?? []) {
254
+ const was = h.command ?? "";
255
+ if (!/synthesisui(@[\w.-]+)?\s+align/.test(was))
256
+ continue;
257
+ if (was === command)
258
+ return "already there";
259
+ h.command = command;
260
+ await mkdir(dir, { recursive: true });
261
+ await writeFile(path, `${JSON.stringify({ ...settings, hooks: { ...hooks, SessionStart: entries } }, null, 2)}\n`, "utf8");
262
+ return "updated";
263
+ }
264
+ entries.push({ hooks: [{ type: "command", command }] });
265
+ await mkdir(dir, { recursive: true });
266
+ await writeFile(path, `${JSON.stringify({ ...settings, hooks: { ...hooks, SessionStart: entries } }, null, 2)}\n`, "utf8");
267
+ return "added";
268
+ }
217
269
  /**
218
270
  * Whether THIS project already has the hook.
219
271
  *
@@ -4,10 +4,43 @@ import { syncClaudeMd } from "../claude-md.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
5
  import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
6
6
  import { buildGuide } from "../guide.js";
7
+ import { censusScope } from "../measured-scope.js";
7
8
  import { body as line, section, snippet } from "../output.js";
8
9
  import { fetchDesignSystem } from "../registry.js";
9
10
  import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.js";
10
11
  import { detectStack } from "../stack.js";
12
+ /**
13
+ * QUAL METADE DESTA PASTA UM TIME COMMITA.
14
+ *
15
+ * Medido no repo do dono em 06/08: `_synthesisui/` não estava no gitignore E não tinha um único
16
+ * arquivo rastreado. Ou seja, a pasta existia só na máquina de quem rodou o import - todo clone, e
17
+ * todo colega, começava sem governança nenhuma, e nada dizia isso a ninguém.
18
+ *
19
+ * A divisão não é gosto, é o que cada arquivo É:
20
+ *
21
+ * .lock, ds/<slug>/** a IDENTIDADE e o CSS que a app importa. Sem eles o build de um colega
22
+ * não tem tokens. Versionar.
23
+ * census.json 0,5 MB, regravado inteiro a cada medição. Versionar é conflito toda vez.
24
+ * ledger.jsonl registro local, append-only, uma linha por escrita. Conflito garantido.
25
+ *
26
+ * Escrito uma vez e nunca reescrito: um arquivo que alguém ajustou é decisão dele.
27
+ */
28
+ async function writeGovernanceIgnore(projectRoot) {
29
+ const path = join(projectRoot, "_synthesisui", ".gitignore");
30
+ try {
31
+ await access(path);
32
+ return;
33
+ }
34
+ catch {
35
+ // não existe - escrever
36
+ }
37
+ await writeFile(path, "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
38
+ "# clone is governed; the measurement and the local record are not, because\n" +
39
+ "# both are rewritten in full on every run and would conflict on every merge.\n" +
40
+ "census.json\n" +
41
+ "ledger.jsonl\n" +
42
+ "not-expressed.md\n", "utf8");
43
+ }
11
44
  async function readRootLock(path) {
12
45
  try {
13
46
  return JSON.parse(await readFile(path, "utf8"));
@@ -62,14 +95,25 @@ export async function add(slug, opts) {
62
95
  `@import "./v${payload.version}/${filename}";\n`, "utf8");
63
96
  }
64
97
  // 5. root pointer
98
+ /**
99
+ * A medição FRESCA vence a cópia anterior, e por isso lê o censo em vez do lock: um `--scope` novo
100
+ * chega pelo censo que acabou de rodar, e ler o lock aqui congelaria o primeiro para sempre. Sem
101
+ * censo (um `add` puro, sem import antes), o que o lock já dizia é preservado.
102
+ */
103
+ const measured = await censusScope(projectRoot);
104
+ const scope = measured.system ?? prev?.scope ?? null;
105
+ const usage = measured.usage.length > 0 ? measured.usage : (prev?.usage ?? []);
65
106
  const lock = {
66
107
  slug: payload.slug,
67
108
  name: payload.name,
68
109
  version: payload.version,
69
110
  registry: base,
70
111
  fetchedAt: new Date().toISOString(),
112
+ ...(scope ? { scope } : {}),
113
+ ...(usage.length > 0 ? { usage } : {}),
71
114
  };
72
115
  await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
116
+ await writeGovernanceIgnore(projectRoot);
73
117
  // 5b. governance rules (personal DS) → rules.md at the slug root (stable path,
74
118
  // highest authority; the GUIDE tells the agent to read it first)
75
119
  /**
@@ -0,0 +1,125 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { readEvents } from "../doctor/ledger.js";
5
+ import { measuredScope } from "../measured-scope.js";
6
+ async function locksIn(root) {
7
+ const dsDir = join(root, "_synthesisui", "ds");
8
+ const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
9
+ const out = [];
10
+ for (const entry of entries) {
11
+ if (!entry.isDirectory())
12
+ continue;
13
+ const raw = await readFile(join(dsDir, entry.name, ".lock"), "utf8").catch(() => null);
14
+ if (!raw)
15
+ continue;
16
+ try {
17
+ out.push(JSON.parse(raw));
18
+ }
19
+ catch {
20
+ // lock ilegível - o `status` já reporta isso com o nome do arquivo
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+ async function credentials(home) {
26
+ const raw = await readFile(join(home, ".synthesisui", "credentials.json"), "utf8").catch(() => null);
27
+ if (!raw)
28
+ return null;
29
+ try {
30
+ return JSON.parse(raw);
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * A metade que não depende de rede.
38
+ *
39
+ * Separada de propósito: ela é a que sempre roda, e é testável sem servidor nenhum.
40
+ */
41
+ export async function localMisalignments(root,
42
+ /**
43
+ * `home` é injetável porque a SESSÃO mora nele e não no repo - e uma verificação que lê o home da
44
+ * máquina que roda o teste não é verificável: ela passa ou falha conforme quem está logado ali.
45
+ */
46
+ opts = {}) {
47
+ const { cli, home = homedir() } = opts;
48
+ const out = [];
49
+ const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
50
+ /** Sem sistema instalado não há alinho a cobrar - não é desalinho, é um repo sem DS. */
51
+ if (locks.length === 0)
52
+ return out;
53
+ if (locks.length > 1)
54
+ out.push({
55
+ says: `${locks.length} design systems are installed here (${locks
56
+ .map((l) => l.slug)
57
+ .join(", ")}) - commands act on the first, so one of them is not being governed`,
58
+ });
59
+ const lock = locks[0];
60
+ const creds = await credentials(home);
61
+ if (!creds)
62
+ out.push({
63
+ says: "No session on this machine. A token belongs to the machine, not to the repo, so a fresh clone always lands here.",
64
+ run: "npx synthesisui login",
65
+ });
66
+ else if (creds.registry && lock.registry && creds.registry !== lock.registry)
67
+ out.push({
68
+ says: `Your session is for ${creds.registry} and this system came from ${lock.registry}. That answers 401, which reads as an expired session on a host that never issued one.`,
69
+ run: `npx synthesisui login --registry ${lock.registry}`,
70
+ });
71
+ /**
72
+ * O ESCOPO, que é o desalinho mais caro e o mais silencioso: sem ele o `sync` mede o repo inteiro
73
+ * e manda os componentes de todos os apps para um sistema que é uma biblioteca (06/08, 338 num
74
+ * sistema de 36).
75
+ */
76
+ const scope = await measuredScope(root);
77
+ if (!scope.system && scope.usage.length === 0)
78
+ out.push({
79
+ says: `"${lock.slug}" does not record where it was measured, so a re-measure would read this whole repo instead of the folder the system came from.`,
80
+ run: "npx synthesisui import --dry --scope <folder>",
81
+ });
82
+ const events = await readEvents(root).catch(() => []);
83
+ if (events.length > 0)
84
+ out.push({
85
+ says: `${events.length} checked write${events.length === 1 ? "" : "s"} recorded here and not sent - the platform is scoring this repo on older evidence.`,
86
+ run: "npx synthesisui sync",
87
+ });
88
+ if (cli && lock.version == null)
89
+ out.push({
90
+ says: `"${lock.slug}" has no version recorded, so nothing can tell whether the CSS in this repo is the one the platform publishes.`,
91
+ run: "npx synthesisui upgrade",
92
+ });
93
+ return out;
94
+ }
95
+ /**
96
+ * A linha que a sessão vê.
97
+ *
98
+ * Uma frase por desalinho e o comando ao lado - sem cabeçalho, sem contagem, sem moldura. Isto é
99
+ * injetado no contexto de um agente, e cada linha decorativa é contexto que não é sobre o trabalho
100
+ * dele.
101
+ */
102
+ export function describeMisalignments(items) {
103
+ if (items.length === 0)
104
+ return "";
105
+ return [
106
+ "Before this session starts, this environment is out of alignment with the design system that governs it:",
107
+ ...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
108
+ ].join("\n");
109
+ }
110
+ /**
111
+ * `synthesisui align` - o comando que o hook de sessão roda.
112
+ *
113
+ * Sai silencioso e com código 0 quando está tudo alinhado, porque um hook que fala toda vez deixa de
114
+ * ser lido. E nunca falha: um ambiente que não dá para verificar não pode impedir alguém de
115
+ * trabalhar.
116
+ */
117
+ export async function align(opts) {
118
+ const root = resolve(opts.dir ?? process.cwd());
119
+ const items = await localMisalignments(root, {
120
+ ...(opts.cli ? { cli: opts.cli } : {}),
121
+ }).catch(() => []);
122
+ const text = describeMisalignments(items);
123
+ if (text)
124
+ console.log(text);
125
+ }
@@ -50,6 +50,15 @@ export async function connect(opts) {
50
50
  `✓ .claude/settings.json the check moved from ${wired.was?.match(/synthesisui@(\d+\.\d+\.\d+)/)?.[1] ?? "an older version"} to this one`
51
51
  : "· .claude/settings.json already had it"));
52
52
  console.log(snippet([wired.command]));
53
+ /**
54
+ * A SEGUNDA COSTURA, dita por nome. As onze ferramentas MCP são PULL e o hook de escrita roda
55
+ * DEPOIS de uma escrita - nenhum dos dois chega a tempo de dizer "você não está logado nesta
56
+ * máquina" ou "este sistema não sabe de onde foi medido". O `SessionStart` é o único que chega.
57
+ */
58
+ if (wired.session !== "skipped")
59
+ console.log(body(wired.session === "already there"
60
+ ? "· .claude/settings.json the session check was already there"
61
+ : `✓ .claude/settings.json every session now opens with what this environment is missing${wired.session === "updated" ? ", pointed at this version" : ""}`));
53
62
  }
54
63
  /**
55
64
  * OS CAMINHOS DE MCP, POR NOME - e o número vem da LISTA, não da minha memória.
@@ -1674,6 +1674,22 @@ export async function takeCensus(root, opts) {
1674
1674
  },
1675
1675
  }
1676
1676
  : {}),
1677
+ /**
1678
+ * O ESCOPO VIAJA COM O CENSO, e até 06/08 só o `usage` viajava.
1679
+ *
1680
+ * `census.scope` era escrito pelo comando `import` (o `runImport`, mais abaixo neste arquivo) e não
1681
+ * aqui - então todo censo produzido por uma RE-MEDIÇÃO nascia sem escopo, mesmo tendo lido a pasta
1682
+ * certa. Um veneno de ação lenta, medido no repo do dono:
1683
+ *
1684
+ * 1a re-medicao le o escopo guardado, mede `packages/ui` certo (36 looks),
1685
+ * e envia um censo SEM escopo
1686
+ * 2a re-medicao nao acha escopo -> mede o repo INTEIRO -> 340 looks, 324
1687
+ * componentes, e o rascunho curado de 37 vira 339
1688
+ *
1689
+ * A primeira parece perfeita, e é isso que faz o defeito sobreviver. O `usage` estava dois campos
1690
+ * acima o tempo todo, gravado por este mesmo objeto - os dois chegam juntos e só um sobrevivia.
1691
+ */
1692
+ ...(scopeLabel ? { scope: scopeLabel } : {}),
1677
1693
  ...((opts?.usage ?? []).length > 0
1678
1694
  ? { usage: (opts?.usage ?? []).map((u) => u.label) }
1679
1695
  : {}),
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { readdir, readFile } from "node:fs/promises";
2
3
  import { join, relative, resolve } from "node:path";
3
4
  import { pinnedHookVersion } from "../agent-wiring.js";
@@ -1057,4 +1058,20 @@ export async function mcp(opts) {
1057
1058
  }
1058
1059
  }
1059
1060
  }
1060
- const VERSION = "0.16.9";
1061
+ /**
1062
+ * A NOSSA VERSÃO, LIDA DO `package.json` - e ela era uma string cravada em "0.16.9".
1063
+ *
1064
+ * 149 versões atrás. O `index.ts` já lia a dele do arquivo desde sempre; esta cópia envelheceu em
1065
+ * silêncio e apareceu de três formas no mesmo dia (dono, 06/08):
1066
+ *
1067
+ * 1. `serverInfo.version` dizia 0.16.9, então o agente reportava "as ferramentas que estou segurando
1068
+ * são 0.16.9" - e estava repetindo o nosso número, não medindo nada
1069
+ * 2. a comparação nova (`pinned !== VERSION`) ficava SEMPRE verdadeira, então o "atualize o ds antes
1070
+ * de continuar" apareceria para sempre, inclusive um segundo depois de um `connect` bem-sucedido
1071
+ * 3. `remeasure({ cli: VERSION })` gravou `ledger.cli: "0.16.9"` no censo e no relatório dele, e a
1072
+ * plataforma passou a achar que aquele sistema está 149 versões atrás
1073
+ *
1074
+ * É a mesma lição que `MCP_TOOL_COUNT` já carrega neste arquivo - "um número que alguém digita
1075
+ * envelhece" - escrita duas vezes e aprendida uma.
1076
+ */
1077
+ const VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
@@ -3,6 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { readToken, resolveRegistry } from "../config.js";
4
4
  import { readEvents } from "../doctor/ledger.js";
5
5
  import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
6
+ import { measuredScope } from "../measured-scope.js";
6
7
  import { body, paint, section, snippet } from "../output.js";
7
8
  import { repoStateOf } from "../repo-state.js";
8
9
  import { resolveReadParts, takeCensus } from "./import.js";
@@ -191,13 +192,31 @@ export async function remeasure(args) {
191
192
  console.log(paint.blue(snippet(["npx synthesisui@latest connect"])));
192
193
  console.log(body(paint.faint("That rewires the hook and the tools to the new version - the editor picks them up on the next session.")));
193
194
  }
195
+ /**
196
+ * O ESCOPO SAI DO DISCO ANTES DE SAIR DO SERVIDOR.
197
+ *
198
+ * Ele mora nos dois lugares, e o do disco é o que não pode se perder: o censo local É a foto da
199
+ * última medição e carrega o `scope` que a mediu. O servidor guarda uma cópia, e uma cópia pode
200
+ * ser sobrescrita - em 06/08 foi, e o sistema ficou sem escopo nenhum lá.
201
+ *
202
+ * O preço daquilo não foi um número errado, foi um LOOP: sem escopo, esta linha media o repo
203
+ * inteiro, mandava 338 componentes de app para um sistema de 36, e o `sync` seguinte fazia de
204
+ * novo. Perguntar ao servidor uma coisa que está do lado de cá é o que fechava o ciclo.
205
+ *
206
+ * O servidor continua valendo como segunda fonte: um clone fresco tem o `.lock` e não tem censo.
207
+ */
208
+ const local = await measuredScope(root);
209
+ const scope = local.system ?? stored.scope ?? null;
210
+ const usage = local.usage.length > 0 ? local.usage : (stored.usage ?? []);
194
211
  console.log(section("Measuring your repository again"));
195
- console.log(body(paint.faint(`${stored.scope ? `scope ${stored.scope}` : "the whole repo"} · the reading you already authored travels unchanged`)));
196
- const census = await takeCensus(stored.scope ? join(root, stored.scope) : root, {
197
- ...(stored.scope ? { scopeLabel: stored.scope } : {}),
212
+ console.log(body(paint.faint(`${scope ? `scope ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
213
+ ? " · read from your own census, which is where the last measurement recorded it"
214
+ : ""} · the reading you already authored travels unchanged`)));
215
+ const census = await takeCensus(scope ? join(root, scope) : root, {
216
+ ...(scope ? { scopeLabel: scope } : {}),
198
217
  ...(args.cli ? { cli: args.cli } : {}),
199
218
  /** O `--usage` é EVIDÊNCIA e leva rótulo: sem ele o censo não sabe de que app o número veio. */
200
- usage: (stored.usage ?? []).map((u) => ({
219
+ usage: usage.map((u) => ({
201
220
  path: join(root, u),
202
221
  label: u,
203
222
  })),
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { absorb } from "./commands/absorb.js";
4
4
  import { add } from "./commands/add.js";
5
5
  import { adopt } from "./commands/adopt.js";
6
6
  import { advise } from "./commands/advise.js";
7
+ import { align } from "./commands/align.js";
7
8
  import { ci } from "./commands/ci.js";
8
9
  import { clean } from "./commands/clean.js";
9
10
  import { component } from "./commands/component.js";
@@ -547,6 +548,13 @@ async function main() {
547
548
  yes: args.includes("--yes"),
548
549
  });
549
550
  break;
551
+ /**
552
+ * O que o hook `SessionStart` roda. Silencioso quando está alinhado, e nunca falha - ver
553
+ * `align.ts` para os nove estados e por que sete deles não tinham verificação nenhuma.
554
+ */
555
+ case "align":
556
+ await align({ dir, cli: CLI_VERSION });
557
+ break;
550
558
  case "request": {
551
559
  // `synthesisui request` lists; `request component --name x --for "..."`
552
560
  // files; `request --done <id>` closes. The agent's front door is the MCP
@@ -1,6 +1,51 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  const EMPTY = { system: null, usage: [], from: "none" };
4
+ /**
5
+ * O ESCOPO NO PONTEIRO DO SISTEMA, que é onde ele não se perde.
6
+ *
7
+ * O censo é grande (0,5 MB), é regravado a cada medição e ninguém versiona um arquivo assim sem
8
+ * pagar conflito. O `.lock` tem cinco campos e é o arquivo desta pasta que um time COMMITA - então é
9
+ * ele que faz um clone fresco saber de onde este sistema foi medido, sem uma ida à rede e sem
10
+ * depender de uma cópia no servidor que já foi sobrescrita uma vez (06/08).
11
+ *
12
+ * Um lock sem `scope` é o caso de todo sistema instalado antes disto: cai no censo, como antes.
13
+ */
14
+ async function scopeInLock(root) {
15
+ const dsDir = join(root, "_synthesisui", "ds");
16
+ const names = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
17
+ for (const entry of names) {
18
+ if (!entry.isDirectory())
19
+ continue;
20
+ const raw = await readFile(join(dsDir, entry.name, ".lock"), "utf8").catch(() => null);
21
+ if (!raw)
22
+ continue;
23
+ let lock;
24
+ try {
25
+ lock = JSON.parse(raw);
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ const scope = typeof lock.scope === "string" && lock.scope.trim()
31
+ ? lock.scope.trim()
32
+ : null;
33
+ const usage = Array.isArray(lock.usage)
34
+ ? lock.usage.filter((u) => typeof u === "string" && !!u.trim())
35
+ : [];
36
+ if (!scope && usage.length === 0)
37
+ continue;
38
+ const system = scope && (await present(root, scope)) ? scope : null;
39
+ const kept = [];
40
+ for (const u of usage)
41
+ if (await present(root, u))
42
+ kept.push(u);
43
+ if (!system && kept.length === 0)
44
+ continue;
45
+ return { system, usage: kept, from: "lock" };
46
+ }
47
+ return null;
48
+ }
4
49
  /** Existe como diretório? Um escopo gravado e depois renomeado não pode derrubar o comando. */
5
50
  async function present(root, rel) {
6
51
  return stat(resolve(root, rel)).then((s) => s.isDirectory(), () => false);
@@ -13,6 +58,19 @@ async function present(root, rel) {
13
58
  * comportamento antigo (a raiz).
14
59
  */
15
60
  export async function measuredScope(root) {
61
+ const fromLock = await scopeInLock(root);
62
+ if (fromLock)
63
+ return fromLock;
64
+ return censusScope(root);
65
+ }
66
+ /**
67
+ * O que o CENSO diz sobre onde, sem olhar o lock.
68
+ *
69
+ * Quem ESCREVE o lock lê por aqui, e a ordem é o contrário da leitura de propósito: o censo é a
70
+ * medição fresca que acabou de rodar, e o lock é a cópia da anterior. Ler o lock para escrever o
71
+ * lock congelaria o primeiro escopo para sempre, e um `--scope` novo nunca chegaria lá.
72
+ */
73
+ export async function censusScope(root) {
16
74
  const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => null);
17
75
  if (!raw)
18
76
  return EMPTY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.158",
3
+ "version": "0.16.160",
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": {