synthesisui 0.16.160 → 0.16.162

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.
@@ -76,8 +76,16 @@ opts = {}) {
76
76
  const scope = await measuredScope(root);
77
77
  if (!scope.system && scope.usage.length === 0)
78
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>",
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. Sync asks once and remembers the answer.`,
80
+ /**
81
+ * O COMANDO QUE ELA JÁ RODA, e nunca um sexto.
82
+ *
83
+ * Isto dizia `import --dry --scope <folder>`, que não está no vocabulário de quem usa isto -
84
+ * `connect`, `login`, `doctor`, `sync`, `upgrade`, e a skill. O `align` é a única coisa que
85
+ * fala com a pessoa sem ela pedir, e falar ensinando uma palavra que ela nunca viu transfere
86
+ * para ela um problema que era nosso. O `sync` absorveu o caso - ver `askForScope`.
87
+ */
88
+ run: "npx synthesisui sync",
81
89
  });
82
90
  const events = await readEvents(root).catch(() => []);
83
91
  if (events.length > 0)
@@ -79,7 +79,7 @@ const MAX_PER_KIND = {
79
79
  * comes from wherever the vocabulary is centralised, and each app is a consumer
80
80
  * measured against it.
81
81
  */
82
- async function siblingProjects(root) {
82
+ export async function siblingProjects(root) {
83
83
  const apps = [];
84
84
  const shared = [];
85
85
  for (const group of ["apps", "packages", "libs"]) {
@@ -1205,7 +1205,17 @@ export async function takeCensus(root, opts) {
1205
1205
  * product is broken, reasoning correctly from what they were shown. The gap was never
1206
1206
  * the defect; the silence was (dono, 01/08).
1207
1207
  */
1208
- const coverage = summarizeCoverage(shapes, componentFiles, Object.keys(looks).length, Object.values(looks).reduce((n, look) => n +
1208
+ /**
1209
+ * O DENOMINADOR CONTA COMPONENTES, e contava ARQUIVOS.
1210
+ *
1211
+ * `looks` é por NOME e `componentFiles` é por arquivo, então um arquivo que exporta dois
1212
+ * componentes produzia "36 of 35 components came out with a look - 103%" (medido na biblioteca do
1213
+ * dono em 06/08). Uma porcentagem que passa de 100 por construção não é um arredondamento: é a
1214
+ * linha que declara a cobertura da esteira inteira se medindo contra outra unidade.
1215
+ *
1216
+ * `componentFiles` segue existindo para as formas de estilo, que SÃO por arquivo.
1217
+ */
1218
+ const coverage = summarizeCoverage(shapes, defined.length || componentFiles, Object.keys(looks).length, Object.values(looks).reduce((n, look) => n +
1209
1219
  Object.keys(look.base).length +
1210
1220
  Object.keys(look.dark ?? {}).length +
1211
1221
  Object.values(look.states).reduce((m, block) => m + Object.keys(block).length, 0) +
@@ -3,10 +3,10 @@ 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
+ import { measuredScope, rememberScope } from "../measured-scope.js";
7
7
  import { body, paint, section, snippet } from "../output.js";
8
8
  import { repoStateOf } from "../repo-state.js";
9
- import { resolveReadParts, takeCensus } from "./import.js";
9
+ import { resolveReadParts, siblingProjects, takeCensus } from "./import.js";
10
10
  /**
11
11
  * What each decision means ON THIS MACHINE - the card decides, the sync
12
12
  * enacts. The web never touches the local file; this line is printed right
@@ -206,8 +206,27 @@ export async function remeasure(args) {
206
206
  * O servidor continua valendo como segunda fonte: um clone fresco tem o `.lock` e não tem censo.
207
207
  */
208
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 ?? []);
209
+ let scope = local.system ?? stored.scope ?? null;
210
+ let usage = local.usage.length > 0 ? local.usage : (stored.usage ?? []);
211
+ /**
212
+ * E QUANDO NINGUÉM SABE, ESTE COMANDO PERGUNTA - uma vez, e grava a resposta.
213
+ *
214
+ * Um sistema que perdeu o escopo só tinha uma volta: `import --scope`. Isso é uma SEXTA palavra
215
+ * no vocabulário de quem usa isto (`connect`, `login`, `doctor`, `sync`, `upgrade`, e a skill), e
216
+ * uma mensagem que ensina uma palavra nova transfere para a pessoa um problema que era nosso.
217
+ *
218
+ * Então o comando que ela já roda absorve o caso: os candidatos saem do próprio repo, a resposta
219
+ * vai para o `.lock`, e a pergunta não volta.
220
+ */
221
+ if (!scope) {
222
+ const asked = await askForScope(root);
223
+ if (asked) {
224
+ scope = asked.scope;
225
+ if (asked.usage.length > 0)
226
+ usage = asked.usage;
227
+ await rememberScope(root, slug, scope, usage);
228
+ }
229
+ }
211
230
  console.log(section("Measuring your repository again"));
212
231
  console.log(body(paint.faint(`${scope ? `scope ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
213
232
  ? " · read from your own census, which is where the last measurement recorded it"
@@ -335,3 +354,44 @@ export async function installedSlug(root) {
335
354
  }
336
355
  return null;
337
356
  }
357
+ /**
358
+ * DE ONDE ESTE SISTEMA FOI MEDIDO - perguntado uma vez, com os candidatos do próprio repo.
359
+ *
360
+ * Os nomes não são digitados: `siblingProjects` já separa o que é biblioteca compartilhada do que é
361
+ * app, que é exatamente a distinção entre o SISTEMA e quem o CONSOME. Um monorepo responde em uma
362
+ * tecla; um repo de app único nem chega aqui, porque a raiz é a resposta óbvia e ela não precisa de
363
+ * pergunta.
364
+ *
365
+ * Sem terminal interativo devolve `null` e a medição segue como antes - um CI não pode travar
366
+ * esperando alguém, e medir a raiz num CI é o comportamento que ele sempre teve.
367
+ */
368
+ async function askForScope(root) {
369
+ const { apps, shared } = await siblingProjects(root);
370
+ if (shared.length === 0)
371
+ return null;
372
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
373
+ return null;
374
+ console.log(section("Where does this system come from?"));
375
+ console.log(body("Nothing here records it, so a re-measure would read this whole repo - which produces the average of every app you have, and that is a diagnosis rather than a system."));
376
+ shared.forEach((name, i) => {
377
+ console.log(body(` ${i + 1}. ${name}`));
378
+ });
379
+ console.log(body(` ${shared.length + 1}. the whole repo`));
380
+ if (apps.length > 0)
381
+ console.log(body(paint.faint(`${apps.length} app${apps.length === 1 ? "" : "s"} travel as evidence either way: ${apps.slice(0, 3).join(", ")}${apps.length > 3 ? ", …" : ""}`)));
382
+ const { createInterface } = await import("node:readline/promises");
383
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
384
+ try {
385
+ const answer = (await rl.question(" which one? [1]: ")).trim();
386
+ const pick = answer === "" ? 1 : Number(answer);
387
+ if (!Number.isInteger(pick) || pick < 1 || pick > shared.length)
388
+ return null;
389
+ return { scope: shared[pick - 1], usage: apps };
390
+ }
391
+ catch {
392
+ return null;
393
+ }
394
+ finally {
395
+ rl.close();
396
+ }
397
+ }
@@ -92,7 +92,16 @@ export function countShape(source, name, into) {
92
92
  }
93
93
  export function summarizeCoverage(counts, components, read, declarations) {
94
94
  return {
95
- components,
95
+ /**
96
+ * NUNCA MENOS QUE O QUE FOI LIDO.
97
+ *
98
+ * `read` conta LOOKS por nome e o denominador chegava contando ARQUIVOS - um arquivo que exporta
99
+ * dois componentes produzia `36 of 35 components came out with a look - 103%` na biblioteca do
100
+ * dono (06/08). O conserto de verdade é o chamador passar a mesma unidade, e ele passa; isto é a
101
+ * trava, para que a linha que declara a cobertura da esteira inteira não possa afirmar mais do
102
+ * que existe se outro chamador errar a unidade de novo.
103
+ */
104
+ components: Math.max(components, read),
96
105
  read,
97
106
  declarations,
98
107
  counts: [...counts.values()].sort((a, b) => b.files - a.files),
@@ -1,4 +1,4 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
1
+ import { readdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  const EMPTY = { system: null, usage: [], from: "none" };
4
4
  /**
@@ -119,3 +119,31 @@ export function describeScope(m) {
119
119
  ];
120
120
  return `read from your census: ${parts.join(" · ")}`;
121
121
  }
122
+ /**
123
+ * GRAVA ONDE ESTE SISTEMA FOI MEDIDO, no ponteiro dele.
124
+ *
125
+ * Existe para uma resposta ser dada UMA vez. O escopo se perdeu do servidor em 06/08 e o único
126
+ * caminho de volta era um `import --scope`, que é um comando fora do vocabulário de quem usa isto -
127
+ * `connect`, `login`, `doctor`, `sync`, `upgrade` e a skill. Uma mensagem que ensina uma sexta
128
+ * palavra é pior que mensagem nenhuma: ela transfere para a pessoa um problema que era nosso.
129
+ *
130
+ * Silencioso quando não há lock: não é erro, é um repo sem sistema instalado.
131
+ */
132
+ export async function rememberScope(root, slug, scope, usage) {
133
+ const path = join(root, "_synthesisui", "ds", slug, ".lock");
134
+ const raw = await readFile(path, "utf8").catch(() => null);
135
+ if (!raw)
136
+ return;
137
+ let lock;
138
+ try {
139
+ lock = JSON.parse(raw);
140
+ }
141
+ catch {
142
+ return;
143
+ }
144
+ await writeFile(path, `${JSON.stringify({
145
+ ...lock,
146
+ ...(scope ? { scope } : {}),
147
+ ...(usage.length > 0 ? { usage } : {}),
148
+ }, null, 2)}\n`, "utf8");
149
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.160",
3
+ "version": "0.16.162",
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": {