synthesisui 0.16.428 → 0.16.430

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.
package/dist/claude-md.js CHANGED
@@ -184,14 +184,29 @@ best reasoning you produce.`;
184
184
  * A checagem é LOCAL e grátis: o censo grava qual CLI o mediu (`ledger.cli`) e o servidor MCP É o CLI,
185
185
  * então comparar as duas versões é aritmética em disco. Nada aqui telefona para casa - é a mesma
186
186
  * decisão que mantém o hook instalado.
187
+ *
188
+ * O TEXTO DEIXOU DE DIZER "IT COSTS NOTHING" em 16/09, e a correção é de fidelidade, não de estilo.
189
+ * Medido: `remeasure` não recusa por versão - ele mede e ENVIA em toda chamada, e a comparação de
190
+ * versões só imprime um aviso. Então a frase antiga descrevia um comportamento que o código nunca
191
+ * teve, e a cota diária do plano grátis transformou essa imprecisão em consequência: o agente
192
+ * gastaria o teto da pessoa seguindo uma instrução nossa, escrita no repositório dela, dizendo que
193
+ * não custava nada.
194
+ *
195
+ * O que o bloco diz agora é o que a régua do plano diz: PERGUNTAR é livre, ENVIAR tem teto - e
196
+ * quando o teto chega, o agente repassa a frase em vez de tentar de novo.
187
197
  */
188
198
  const REFRESH = `
189
199
 
190
200
  **Call \`refresh_system\` once before you write UI here, or before you describe what
191
- this system holds.** Not "if you think the measurement is stale" - always, once per
192
- session. It costs nothing when nothing changed: the tool holds both versions and
193
- refuses politely when there is nothing to refresh, so calling it is cheaper than
194
- deciding whether to.
201
+ this system holds** - once per session, not once per file, and never in a loop.
202
+
203
+ It is an UPDATE and not a question: it re-measures this repository and sends it. On
204
+ the free plan that counts against the updates the plan covers per day, and when the
205
+ day's updates are used up the tool says so and nothing is lost - the measurement
206
+ stays on disk, and every question you ask about this system keeps being answered.
207
+ Asking is always free; it is sending that has a ceiling. If it says the ceiling was
208
+ reached, say that to the person and carry on with what you already know - do not
209
+ retry it.
195
210
 
196
211
  It re-measures this repo with the current reader, reuses what was already written
197
212
  about this app, and sends it - a newer reader sees styles the old one could not, and
@@ -47,6 +47,12 @@ const IGNORED = [
47
47
  /** O que a última medição viu, para a próxima dizer o que mudou - descreve ESTE clone. */
48
48
  ".last-sync.json",
49
49
  "not-expressed.md",
50
+ /**
51
+ * O RELATÓRIO QUE O `inspect` ESCREVE - a leitura inteira com forma, para ele abrir no navegador.
52
+ * Mesma natureza do censo: é a MEDIÇÃO, reescrita por inteiro a cada rodada, então versioná-la é
53
+ * conflito toda vez e ainda poria um HTML de meio megabyte no diff de quem só rodou o comando.
54
+ */
55
+ "report.html",
50
56
  /**
51
57
  * O MODO DELA - ver `mode.ts`. Commitado, ele barraria o time inteiro porque UMA pessoa ligou
52
58
  * o bloqueio dela, e o aceite da etapa 06 é literal: *"a escolha de uma pessoa nunca barra
@@ -78,7 +84,12 @@ export async function ensureGovernanceHome(projectRoot) {
78
84
  await mkdir(join(projectRoot, "_synthesisui"), { recursive: true });
79
85
  await writeGovernanceIgnore(projectRoot);
80
86
  }
81
- async function writeGovernanceIgnore(projectRoot) {
87
+ /**
88
+ * EXPORTADO desde 18/09 porque o `inspect` passou a escrever um arquivo na pasta: o relatório. Um
89
+ * comando de primeiro contato que deixa um HTML de 30 KB aparecendo no `git status` de quem só quis
90
+ * olhar cobra da pessoa uma limpeza que ela não pediu - e a pasta já é declarada gerenciada aqui.
91
+ */
92
+ export async function writeGovernanceIgnore(projectRoot) {
82
93
  const path = join(projectRoot, "_synthesisui", ".gitignore");
83
94
  const existing = await readFile(path, "utf8").catch(() => null);
84
95
  /**
@@ -55,12 +55,14 @@ import { body, bodyWrapped, paint, section } from "../output.js";
55
55
  import { consumersOf, outsideScope, } from "../outside-scope.js";
56
56
  import { phase, startProgress } from "../progress.js";
57
57
  import { repoStateOf } from "../repo-state.js";
58
+ import { relatorioHtml, secoesDe } from "../report-html.js";
58
59
  import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
59
60
  import { detectStack, resolveDeps, stackVersions } from "../stack.js";
61
+ import { desligarCaptura, ligarCaptura } from "../transcript.js";
60
62
  import { wiredSlugs, wiringWarning } from "../wired-slugs.js";
61
63
  import { harvestWorkspaceCss } from "../workspace-css.js";
62
64
  import { placeInWorkspace } from "../workspace-place.js";
63
- import { add } from "./add.js";
65
+ import { add, writeGovernanceIgnore } from "./add.js";
64
66
  import { walk, walkAll } from "./doctor.js";
65
67
  /**
66
68
  * UMA PÁGINA DELE, como ele a montou - ver `Census.pages`.
@@ -3720,6 +3722,145 @@ export function defaultScheme(declared) {
3720
3722
  return dark > light ? "dark" : "light";
3721
3723
  }
3722
3724
  /** On a dry run there is nobody to ask, so say what was measured. */
3725
+ /**
3726
+ * A TELA DO `inspect`, E O ARQUIVO QUE CARREGA O RESTO (dono, 18/09).
3727
+ *
3728
+ * O QUE O CLIENTE GANHA: ele decide sem rolar. Ficam na tela os quatro blocos que MUDAM o que ele
3729
+ * vai fazer em seguida - quanto foi lido, se o root tem mais de um app, o que não resolve, e onde
3730
+ * está a leitura inteira. Medido na saída dele em 18/09: 250 linhas, 94 delas a mesma frase sobre
3731
+ * nome de parte. Nada se perde: o texto inteiro está no relatório, escrito pela MESMA esteira.
3732
+ *
3733
+ * A FRASE DO CONTRATO MUDA, E É A ÚNICA COISA QUE MUDA: era "nothing was written, and nothing was
3734
+ * sent". Passa a dizer a verdade - nada enviado, um arquivo escrito, e aqui está ele. Uma frase que
3735
+ * promete silêncio enquanto o comando escreve um arquivo é pior que o arquivo.
3736
+ */
3737
+ async function mostrarInspecao(c, root, transcrito) {
3738
+ const secoes = secoesDe(transcrito);
3739
+ const valores = c.values?.classes;
3740
+ const decisoes = valores ? valores.seen - valores.structure : 0;
3741
+ const pct = valores && decisoes > 0
3742
+ ? Math.round((valores.interpreted / decisoes) * 100)
3743
+ : null;
3744
+ const lidos = c.coverage?.read ?? 0;
3745
+ const total = c.coverage?.components ?? 0;
3746
+ const arquivo = join(root, "_synthesisui", "report.html");
3747
+ const html = () => relatorioHtml({
3748
+ cabecalho: {
3749
+ repo: c.measured?.repo ?? basename(root),
3750
+ medidoEm: c.measured?.at ?? "",
3751
+ cli: c.ledger?.cli ?? "",
3752
+ },
3753
+ destaques: [
3754
+ {
3755
+ numero: `${lidos} of ${total}`,
3756
+ rotulo: "components came out with a blueprint",
3757
+ },
3758
+ ...(pct === null
3759
+ ? []
3760
+ : [
3761
+ {
3762
+ numero: `${pct}%`,
3763
+ rotulo: `of ${decisoes} style decisions carry your vocabulary`,
3764
+ },
3765
+ ]),
3766
+ {
3767
+ numero: String(c.brokenRefs?.length ?? 0),
3768
+ rotulo: "references that resolve to nothing",
3769
+ },
3770
+ ],
3771
+ secoes,
3772
+ });
3773
+ console.log(section("What came out of your files"));
3774
+ console.log(body(`${paint.strong(`${lidos} of ${total}`)} components came out with a blueprint${pct === null
3775
+ ? ""
3776
+ : `, and ${paint.strong(`${pct}%`)} of your ${decisoes} style decisions carry your own vocabulary`}.`));
3777
+ /**
3778
+ * OS DOIS BLOCOS QUE MUDAM O QUE ELE FAZ EM SEGUIDA, e eles saem do transcrito em vez de serem
3779
+ * reescritos: a régua que os produziu é a mesma, então a tela nunca diverge do relatório.
3780
+ */
3781
+ for (const titulo of DECIDEM) {
3782
+ const achada = secoes.find((s) => s.titulo === titulo);
3783
+ if (!achada)
3784
+ continue;
3785
+ console.log(section(titulo));
3786
+ for (const linha of achada.linhas)
3787
+ console.log(linha);
3788
+ }
3789
+ /**
3790
+ * O ARQUIVO SÓ NASCE SE ELE PEDIR (dono, 18/09) - e é isto que mantém `INV-INSPECT-01` de pé.
3791
+ *
3792
+ * A promessa que faz alguém rodar este comando sem medo é que nada acontece: ele lê, mostra, e
3793
+ * o repositório continua exatamente como estava. Escrever o relatório por conta própria
3794
+ * quebraria isso para TODO mundo, inclusive para quem só queria olhar - e num primeiro contato
3795
+ * essa é a confiança inteira. Perguntando, a escrita deixa de ser nossa e passa a ser dele.
3796
+ *
3797
+ * SEM TERMINAL, NÃO PERGUNTA E NÃO ESCREVE: um `inspect` dentro de um pipe, de um CI ou de um
3798
+ * agente não tem quem responda, e um default que escreve seria a mesma quebra por outra porta.
3799
+ * É a mesma guarda que `askName` já usa.
3800
+ */
3801
+ console.log(section("The whole reading, in one file"));
3802
+ console.log(body(`${secoes.length} sections did not print here - every value, every fragment, every part.`));
3803
+ console.log(body(paint.faint("A single self-contained page in your project. Nothing is sent either way.")));
3804
+ if (!(await quer(` write the full report? [Y/n]: `))) {
3805
+ console.log("");
3806
+ console.log(body("Nothing was written, and nothing was sent."));
3807
+ console.log("");
3808
+ console.log(body("To let your agent ask before it invents:"));
3809
+ console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
3810
+ console.log("");
3811
+ return;
3812
+ }
3813
+ await mkdir(dirname(arquivo), { recursive: true });
3814
+ /**
3815
+ * A PASTA SE DECLARA GERENCIADA ANTES DE RECEBER O ARQUIVO: sem isto, o HTML apareceria no
3816
+ * `git status` de quem acabou de pedir um relatório, e não um arquivo versionado.
3817
+ */
3818
+ await writeGovernanceIgnore(root).catch(() => null);
3819
+ await writeFile(arquivo, html(), "utf8");
3820
+ console.log("");
3821
+ console.log(body(` ${paint.strong(relative(root, arquivo))}`));
3822
+ console.log(body(` ${paint.dim(`file://${arquivo}`)}`));
3823
+ console.log("");
3824
+ console.log(body("To let your agent ask before it invents:"));
3825
+ console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
3826
+ console.log("");
3827
+ }
3828
+ /**
3829
+ * A PERGUNTA DE UMA TECLA, e o silêncio de quem não tem terminal.
3830
+ *
3831
+ * Fora de um terminal a resposta é NÃO, sempre: ninguém está lá para responder, e assumir o sim
3832
+ * escreveria no repositório de um CI que só queria ler. Enter é sim porque a pessoa acabou de ler
3833
+ * a oferta na linha acima - exigir uma letra para receber o que a tela ofereceu é atrito sem
3834
+ * proteção nenhuma.
3835
+ */
3836
+ async function quer(pergunta) {
3837
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
3838
+ return false;
3839
+ const { createInterface } = await import("node:readline/promises");
3840
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3841
+ try {
3842
+ console.log("");
3843
+ const resposta = (await rl.question(pergunta)).trim().toLowerCase();
3844
+ return resposta === "" || resposta === "y" || resposta === "yes";
3845
+ }
3846
+ catch {
3847
+ return false;
3848
+ }
3849
+ finally {
3850
+ rl.close();
3851
+ }
3852
+ }
3853
+ /**
3854
+ * AS SEÇÕES QUE CONTINUAM NA TELA, e o critério é um só: elas mudam o comando seguinte.
3855
+ *
3856
+ * Um root com três apps mede a média de três paletas, o que é diagnóstico e não sistema - e a saída
3857
+ * já traz o `--scope`/`--usage` exato. Um `var()` que não resolve não pinta cor nenhuma no
3858
+ * navegador dele. As outras quinze seções descrevem; estas duas pedem uma decisão.
3859
+ */
3860
+ const DECIDEM = [
3861
+ "This root holds several projects",
3862
+ "These resolve to nothing",
3863
+ ];
3723
3864
  function sayReach(c) {
3724
3865
  const reach = ladderReach(c.declared);
3725
3866
  console.log("");
@@ -4332,6 +4473,16 @@ export async function runImport(opts) {
4332
4473
  if (!opts.inspect)
4333
4474
  phase(1, 3, "Measuring the repository");
4334
4475
  console.log(section("Reading your project"));
4476
+ /**
4477
+ * A PARTIR DAQUI O `inspect` GUARDA EM VEZ DE IMPRIMIR - ver `transcript.ts`.
4478
+ *
4479
+ * As dezessete seções continuam sendo escritas exatamente como sempre foram; o que muda é o
4480
+ * destino. Elas vão para o relatório, e o terminal fica com os quatro blocos que uma pessoa
4481
+ * usa para DECIDIR. Medido na saída do `codelevel-monorepo` em 18/09: 250 linhas, das quais 94
4482
+ * eram a mesma frase sobre nome de parte - 40% do relatório num detalhe sem ação possível.
4483
+ */
4484
+ if (opts.inspect)
4485
+ ligarCaptura();
4335
4486
  /**
4336
4487
  * UMA MEDIÇÃO POR ESCOPO, E DEPOIS A FUSÃO.
4337
4488
  *
@@ -4448,13 +4599,8 @@ export async function runImport(opts) {
4448
4599
  */
4449
4600
  if (opts.inspect) {
4450
4601
  sayReach(census);
4451
- console.log("");
4452
- console.log(section("Your agent cannot ask any of this yet"));
4453
- console.log(body("Nothing was written, and nothing was sent."));
4454
- console.log("");
4455
- console.log(body("To let it ask before it invents:"));
4456
- console.log(body(` ${paint.strong("npx synthesisui@latest connect")}`));
4457
- console.log("");
4602
+ const transcrito = desligarCaptura();
4603
+ await mostrarInspecao(census, root, transcrito);
4458
4604
  return;
4459
4605
  }
4460
4606
  const out = opts.census ?? join(root, "_synthesisui", "census.json");
@@ -1790,13 +1790,27 @@ cli) {
1790
1790
  const token = await readToken();
1791
1791
  if (!token)
1792
1792
  return fromContract("Not signed in, so nothing could be sent. A person has to run `synthesisui login` once in the terminal - it opens a browser, and I cannot complete it for them.");
1793
- const { out } = await capturing(() => remeasure({
1793
+ const { out, value: refused } = await capturing(() => remeasure({
1794
1794
  root,
1795
1795
  base: resolveRegistry(),
1796
1796
  token,
1797
1797
  slug,
1798
1798
  cli: VERSION,
1799
1799
  }));
1800
+ /**
1801
+ * O TETO DO PLANO CHEGA AO AGENTE COMO RESPOSTA, e não como silêncio.
1802
+ *
1803
+ * Esta ferramenta é uma ATUALIZAÇÃO, não uma consulta: ela re-mede o repositório e envia.
1804
+ * Então ela conta contra o teto diário do plano grátis, e quando o teto é atingido o envio
1805
+ * não acontece. Sem esta linha o agente receberia a saída capturada sem a recusa dentro
1806
+ * dela - `remeasure` devolve o motivo em vez de imprimi-lo, para o `sync` poder dizê-lo por
1807
+ * último no terminal - e concluiria que a medição subiu.
1808
+ *
1809
+ * A frase vem do servidor inteira, com o endereço junto, porque aqui não há terminal para
1810
+ * quebrar linha: quem lê é o agente, e ele precisa do endereço para repassar à pessoa.
1811
+ */
1812
+ if (refused)
1813
+ return fromContract(`${refused.message}${refused.url ? `\n\n ${refused.url}` : ""}\n\nNothing was lost: the measurement is on disk and every question you ask about this system keeps being answered. Say this to the person instead of retrying.`);
1800
1814
  /**
1801
1815
  * E A DEFASAGEM DO HOOK, EM TRÊS LINHAS - não num parágrafo.
1802
1816
  *
@@ -168,8 +168,20 @@ export async function sync(opts) {
168
168
  * Então: a plataforma devolve o que já foi decidido (o escopo e a leitura), o CLI mede com o leitor
169
169
  * ATUAL, funde a leitura no censo fresco, AVISA o que seria reescrito, e envia. Sem entrevista.
170
170
  */
171
- if (!opts.recordOnly)
172
- await remeasure({
171
+ /**
172
+ * A RECUSA É GUARDADA PARA O FIM, e essa é a diferença entre informar e enganar.
173
+ *
174
+ * O comando continua depois da medição: ele fecha pedidos, manda o registro de checagens e
175
+ * termina imprimindo "N checks sent". Uma recusa dita no meio disso rola para cima, e a ÚLTIMA
176
+ * linha da tela anuncia sucesso para uma rodada em que o design system dela não foi atualizado.
177
+ *
178
+ * O registro de checagens SOBE mesmo assim, e isso é deliberado: é outra rota, é o que alimenta
179
+ * "você está na v2 e existe a v4", e não é o envio pesado que a cota governa. Dizer as duas
180
+ * coisas na ordem em que aconteceram é mais honesto que abortar tudo.
181
+ */
182
+ const refused = opts.recordOnly
183
+ ? null
184
+ : await remeasure({
173
185
  root,
174
186
  base,
175
187
  token,
@@ -206,6 +218,7 @@ export async function sync(opts) {
206
218
  if (emptyLedger && !repo && forms.length === 0) {
207
219
  console.log(section("Sync"));
208
220
  console.log(body("Nothing recorded yet. The record fills as the hook checks writes and doctor runs."));
221
+ sayRefused(refused);
209
222
  return;
210
223
  }
211
224
  const res = await fetch(`${base}/api/ledger`, {
@@ -308,21 +321,29 @@ export async function sync(opts) {
308
321
  * nada, que é o caso normal.
309
322
  */
310
323
  await reportWhatIsLeft(root);
324
+ sayRefused(refused);
311
325
  }
312
326
  /**
313
- * MEDE DE NOVO, AVISA, E ENVIA - o corpo do `sync` que faz um leitor novo chegar num sistema.
327
+ * O QUE NÃO FOI ENVIADO, DITO POR ÚLTIMO - e o "por último" é o desenho, não um detalhe.
314
328
  *
315
- * A ordem é a lei aqui, e cada passo existe por um caso real:
329
+ * A pessoa lê a última seção da tela e é ela que decide o que ela conclui do comando. O registro
330
+ * de checagens subiu de verdade, então a linha "N checks sent" é verdadeira e fica; o que não
331
+ * pode é ela ser a ÚLTIMA coisa numa rodada em que o design system não foi atualizado.
316
332
  *
317
- * 1. a plataforma devolve o ESCOPO e a LEITURA já gravados. Sem isto, o `sync` teria que
318
- * perguntar de novo o que já foi decidido, e um agente reescreveria 261 anatomias.
319
- * 2. mede com o leitor ATUAL - é o único passo que precisa estar nesta máquina, porque o fonte
320
- * dele não sai daqui.
321
- * 3. funde a leitura gravada no censo fresco. A anatomia é do agente; a medição é nossa.
322
- * 4. AVISA antes de enviar o que seria reescrito, e pergunta. Avisar depois de escrever não é
323
- * avisar.
324
- * 5. envia para a rota que atualiza o RASCUNHO pelo slug - nada publicado se move.
333
+ * NÃO SE LÊ COMO ERRO: a frase vem do servidor, que é quem conhece o teto, a contagem e a hora em
334
+ * que a próxima libera. E o endereço vai em linha própria, indentado, como toda ação que o
335
+ * terminal oferece - prosa quebra em 66 colunas e um endereço no meio de um parágrafo não se
336
+ * distingue do parágrafo.
325
337
  */
338
+ function sayRefused(refused) {
339
+ if (!refused)
340
+ return;
341
+ console.log("");
342
+ console.log(section("Your system was not updated"));
343
+ console.log(body(refused.message));
344
+ if (refused.url)
345
+ console.log(body(` ${paint.blue(refused.url)}`));
346
+ }
326
347
  export async function remeasure(args) {
327
348
  const { root, base, token, slug } = args;
328
349
  const head = await fetch(`${base}/api/registry/ds/${slug}/reading`, {
@@ -333,7 +354,7 @@ export async function remeasure(args) {
333
354
  console.log(body(head
334
355
  ? `The registry answered ${head.status} for this system's reading, so nothing was re-measured. The record below still travels.`
335
356
  : "Could not reach the registry, so nothing was re-measured. The record below still travels."));
336
- return;
357
+ return null;
337
358
  }
338
359
  const stored = (await head.json());
339
360
  /**
@@ -343,7 +364,7 @@ export async function remeasure(args) {
343
364
  if (!stored.hasCensus) {
344
365
  console.log(section("Sync"));
345
366
  console.log(body(`"${slug}" did not come from an import, so there is no measurement to refresh. Import it first: npx synthesisui import`));
346
- return;
367
+ return null;
347
368
  }
348
369
  /**
349
370
  * A DISTÂNCIA ATÉ O CLI PUBLICADO, dita onde ele já está.
@@ -461,7 +482,7 @@ export async function remeasure(args) {
461
482
  : null;
462
483
  if (!census) {
463
484
  console.log(body("The measurement failed, so nothing was sent."));
464
- return;
485
+ return null;
465
486
  }
466
487
  /** A LEITURA GRAVADA ENTRA NO CENSO FRESCO - ver a rota `reading`. */
467
488
  if (stored.reading)
@@ -564,7 +585,7 @@ export async function remeasure(args) {
564
585
  .join(" · "))));
565
586
  if (!(await askToOverwrite())) {
566
587
  console.log(body("Nothing was sent. Your draft is untouched."));
567
- return;
588
+ return null;
568
589
  }
569
590
  }
570
591
  /**
@@ -634,11 +655,33 @@ export async function remeasure(args) {
634
655
  : {}),
635
656
  }),
636
657
  }).catch(() => null);
658
+ /**
659
+ * O TETO DE ATUALIZAÇÕES DO PLANO GRÁTIS - e ele NÃO se lê como um erro, porque não é um.
660
+ *
661
+ * O QUE ELA VIVE: o envio parou porque o plano dela atualiza um número de vezes por dia, e a
662
+ * frase diz quantas já foram, que a medição continua no disco dela, que as perguntas do agente
663
+ * seguem respondendo, e a HORA em que a próxima libera. O endereço do plano vai em linha
664
+ * própria, indentado, como toda ação que o terminal oferece - prosa quebra em 66 colunas e um
665
+ * endereço enterrado no meio de um parágrafo não se distingue do parágrafo.
666
+ *
667
+ * A FRASE VEM DO SERVIDOR e não é escrita aqui: é ele quem conhece o teto, a contagem e o
668
+ * horário. Uma segunda redação deste texto no CLI é o dia em que alguém muda a cota e o
669
+ * terminal continua anunciando a antiga - e o terminal é onde a pessoa está parada.
670
+ */
671
+ if (res?.status === 429) {
672
+ const door = (await res.json().catch(() => null));
673
+ return {
674
+ kind: "quota",
675
+ message: door?.message ??
676
+ "Your free plan has reached its updates for today, so your design system was not updated.",
677
+ url: door?.url ?? null,
678
+ };
679
+ }
637
680
  if (!res?.ok) {
638
681
  console.log(body(res
639
682
  ? `The registry answered ${res.status}. Nothing was written.`
640
683
  : "Could not reach the registry. Nothing was written."));
641
- return;
684
+ return null;
642
685
  }
643
686
  const out = (await res.json());
644
687
  console.log("");
@@ -842,6 +885,8 @@ export async function remeasure(args) {
842
885
  console.log(body(paint.faint(`${work.measured} declared piece${work.measured === 1 ? "" : "s"} of work re-checked against this measurement`)));
843
886
  if (!args.full)
844
887
  console.log(body(paint.faint("everything the reader saw: npx synthesisui sync --full")));
888
+ /** Chegou até aqui: a medição foi enviada e escrita. Nada a recusar. */
889
+ return null;
845
890
  }
846
891
  /**
847
892
  * Pergunta uma vez, e só num terminal de verdade.
@@ -235,7 +235,7 @@
235
235
  * chama `wireAgent`, então rodar o comando é o caminho de volta - e ele só alarga a string que era
236
236
  * nossa, nunca um filtro que uma pessoa escreveu.
237
237
  */
238
- export const MATERIALISER_SINCE = "0.16.424";
238
+ export const MATERIALISER_SINCE = "0.16.429";
239
239
  /**
240
240
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
241
241
  *
@@ -0,0 +1,205 @@
1
+ import { semCor } from "./transcript.js";
2
+ /**
3
+ * O QUE O TERMINAL DIRIA, LIDO DE VOLTA COMO DOCUMENTO.
4
+ *
5
+ * A régua `── Título ──────` é a única marca estrutural que a saída tem, e ela é desenhada num
6
+ * lugar só (`section`), então é ela que separa os capítulos. Tudo antes da primeira régua é ruído
7
+ * de progresso e não entra: o documento começa no primeiro capítulo, não no meio do carregamento.
8
+ */
9
+ export function secoesDe(linhas) {
10
+ const secoes = [];
11
+ for (const bruta of linhas) {
12
+ for (const linha of semCor(bruta).split("\n")) {
13
+ const cabeca = /^──\s(.+?)\s─*$/.exec(linha.trimEnd());
14
+ if (cabeca) {
15
+ secoes.push({ titulo: cabeca[1].trim(), linhas: [] });
16
+ continue;
17
+ }
18
+ if (secoes.length === 0)
19
+ continue;
20
+ secoes[secoes.length - 1].linhas.push(linha.replace(/\s+$/, ""));
21
+ }
22
+ }
23
+ /** Capítulo que não disse nada não vira capítulo - uma régua sozinha não é conteúdo. */
24
+ return secoes
25
+ .map((s) => ({ ...s, linhas: aparar(s.linhas) }))
26
+ .filter((s) => s.linhas.length > 0);
27
+ }
28
+ /** Tira as linhas em branco das pontas, e nunca as do meio - lá elas são respiro. */
29
+ function aparar(linhas) {
30
+ let i = 0;
31
+ let f = linhas.length;
32
+ while (i < f && linhas[i].trim() === "")
33
+ i += 1;
34
+ while (f > i && linhas[f - 1].trim() === "")
35
+ f -= 1;
36
+ return linhas.slice(i, f);
37
+ }
38
+ function escapar(s) {
39
+ return s
40
+ .replace(/&/g, "&amp;")
41
+ .replace(/</g, "&lt;")
42
+ .replace(/>/g, "&gt;")
43
+ .replace(/"/g, "&quot;");
44
+ }
45
+ export function relatorioHtml(input) {
46
+ const { cabecalho, destaques, secoes } = input;
47
+ const quando = legivel(cabecalho.medidoEm);
48
+ const indice = secoes
49
+ .map((s, i) => `<li><a href="#s${i}">${escapar(s.titulo)}</a></li>`)
50
+ .join("\n ");
51
+ const corpo = secoes
52
+ .map((s, i) => ` <section id="s${i}">
53
+ <h2>${escapar(s.titulo)}</h2>
54
+ <pre>${escapar(s.linhas.join("\n"))}</pre>
55
+ </section>`)
56
+ .join("\n");
57
+ const numeros = destaques
58
+ .map((d) => ` <div class="stat">
59
+ <strong>${escapar(d.numero)}</strong>
60
+ <span>${escapar(d.rotulo)}</span>
61
+ </div>`)
62
+ .join("\n");
63
+ return `<!doctype html>
64
+ <html lang="en">
65
+ <head>
66
+ <meta charset="utf-8">
67
+ <meta name="viewport" content="width=device-width, initial-scale=1">
68
+ <title>${escapar(cabecalho.repo)} - read by SynthesisUI</title>
69
+ <style>
70
+ ${estilo()}
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <header>
75
+ <p class="kicker">Read by SynthesisUI</p>
76
+ <h1>${escapar(cabecalho.repo)}</h1>
77
+ <p class="meta">Read ${escapar(quando)} by CLI ${escapar(cabecalho.cli)} &middot; nothing left this machine</p>
78
+ </header>
79
+
80
+ <div class="wrap">
81
+ <div class="stats">
82
+ ${numeros}
83
+ </div>
84
+
85
+ <nav>
86
+ <p class="kicker">What is in here</p>
87
+ <ol>
88
+ ${indice}
89
+ </ol>
90
+ </nav>
91
+
92
+ <main>
93
+ ${corpo}
94
+ </main>
95
+
96
+ <footer>
97
+ <p>This file lives in your project and was never sent anywhere. Re-run the command to refresh it.</p>
98
+ </footer>
99
+ </div>
100
+ </body>
101
+ </html>
102
+ `;
103
+ }
104
+ /** A data em palavras. Data ilegível some da frase em vez de imprimir `Invalid Date`. */
105
+ function legivel(iso) {
106
+ if (!iso || Number.isNaN(Date.parse(iso)))
107
+ return "just now";
108
+ return new Date(iso).toLocaleDateString("en-US", {
109
+ month: "short",
110
+ day: "numeric",
111
+ year: "numeric",
112
+ });
113
+ }
114
+ /**
115
+ * A FOLHA, E ELA É INTEIRA DAQUI: nenhuma família remota, nenhum reset de terceiro. As famílias são
116
+ * as genéricas do sistema de quem abrir, porque baixar uma fonte para desenhar um relatório sobre o
117
+ * repositório de alguém contradiz o comando que não manda nada para lugar nenhum.
118
+ *
119
+ * E ela imprime: `@media print` existe porque o navegador já exporta PDF, e um relatório que sai
120
+ * torto no papel obriga a construir um gerador de PDF para resolver o que uma regra resolve.
121
+ */
122
+ function estilo() {
123
+ return `:root {
124
+ color-scheme: light dark;
125
+ --ink: #16161a;
126
+ --ink-soft: #55555f;
127
+ --line: #e4e4e9;
128
+ --paper: #ffffff;
129
+ --canvas: #f6f6f8;
130
+ --accent: #b4530a;
131
+ }
132
+ @media (prefers-color-scheme: dark) {
133
+ :root {
134
+ --ink: #edecea;
135
+ --ink-soft: #a0a0aa;
136
+ --line: #2a2a31;
137
+ --paper: #17171b;
138
+ --canvas: #0f0f12;
139
+ --accent: #e9a13b;
140
+ }
141
+ }
142
+ * { box-sizing: border-box; }
143
+ body {
144
+ margin: 0;
145
+ background: var(--canvas);
146
+ color: var(--ink);
147
+ font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
148
+ }
149
+ header {
150
+ padding: 56px 24px 40px;
151
+ border-bottom: 1px solid var(--line);
152
+ background: var(--paper);
153
+ }
154
+ .wrap, header > * { max-width: 900px; margin-inline: auto; }
155
+ .wrap { padding: 0 24px 72px; }
156
+ .kicker {
157
+ margin: 0;
158
+ font: 600 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
159
+ letter-spacing: 0.16em;
160
+ text-transform: uppercase;
161
+ color: var(--accent);
162
+ }
163
+ h1 { margin: 12px 0 8px; font-size: 34px; line-height: 1.15; letter-spacing: -0.02em; }
164
+ .meta { margin: 0; color: var(--ink-soft); font-size: 14px; }
165
+ .stats {
166
+ display: grid;
167
+ gap: 12px;
168
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
169
+ margin: -28px 0 40px;
170
+ }
171
+ .stat {
172
+ background: var(--paper);
173
+ border: 1px solid var(--line);
174
+ border-radius: 14px;
175
+ padding: 18px 20px;
176
+ }
177
+ .stat strong { display: block; font-size: 26px; letter-spacing: -0.02em; }
178
+ .stat span { display: block; margin-top: 4px; color: var(--ink-soft); font-size: 13px; }
179
+ nav { margin-bottom: 40px; }
180
+ nav ol { margin: 10px 0 0; padding-left: 20px; color: var(--ink-soft); }
181
+ nav li { margin: 3px 0; }
182
+ nav a { color: inherit; }
183
+ nav a:hover { color: var(--accent); }
184
+ section {
185
+ background: var(--paper);
186
+ border: 1px solid var(--line);
187
+ border-radius: 14px;
188
+ padding: 22px 24px;
189
+ margin-bottom: 16px;
190
+ }
191
+ h2 { margin: 0 0 12px; font-size: 17px; letter-spacing: -0.01em; }
192
+ pre {
193
+ margin: 0;
194
+ white-space: pre-wrap;
195
+ word-break: break-word;
196
+ font: 12.5px/1.65 ui-monospace, SFMono-Regular, Menlo, monospace;
197
+ color: var(--ink-soft);
198
+ }
199
+ footer { margin-top: 40px; color: var(--ink-soft); font-size: 13px; }
200
+ @media print {
201
+ body { background: #fff; }
202
+ .stat, section { break-inside: avoid; border-color: #ddd; }
203
+ nav { display: none; }
204
+ }`;
205
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * O QUE O TERMINAL TERIA IMPRIMIDO, GUARDADO EM VEZ DE MOSTRADO.
3
+ *
4
+ * O QUE O CLIENTE GANHA: a saída do `inspect` volta a caber numa tela, e nada do que ela dizia se
5
+ * perde - o texto inteiro vai para o relatório que ele abre quando quiser.
6
+ *
7
+ * POR QUE AQUI E NÃO EM CADA IMPRESSOR: são dezessete seções escritas em dezessete lugares, e a
8
+ * leitura delas é a mesma coisa que a esteira já sabe dizer. Reescrevê-las num gerador de HTML
9
+ * criaria duas redações da mesma medição - e no dia em que uma fosse editada, o relatório passaria
10
+ * a afirmar algo que o terminal não afirma mais. Capturando, existe UMA fonte: o que a esteira diz.
11
+ *
12
+ * ELE É DELIBERADAMENTE BURRO: liga, guarda, desliga. Sem filtro, sem formatação e sem saber o que
13
+ * é seção - quem entende do assunto é quem transforma isto em documento.
14
+ */
15
+ let guardadas = null;
16
+ let originalLog = null;
17
+ /**
18
+ * A REDE DE SEGURANÇA, e ela existe por um motivo específico: uma medição que lança no meio
19
+ * deixaria o `console.log` desviado pelo resto do processo, e a pessoa veria o comando morrer sem
20
+ * uma linha de explicação. O terminal volta ao normal mesmo quando ninguém se lembra de desligar.
21
+ */
22
+ process.once("exit", () => {
23
+ if (originalLog)
24
+ console.log = originalLog;
25
+ });
26
+ /** Começa a guardar em vez de imprimir. Chamar duas vezes não empilha - a segunda é um no-op. */
27
+ export function ligarCaptura() {
28
+ if (guardadas)
29
+ return;
30
+ guardadas = [];
31
+ originalLog = console.log;
32
+ console.log = (...args) => {
33
+ guardadas?.push(args.map((a) => (typeof a === "string" ? a : String(a))).join(" "));
34
+ };
35
+ }
36
+ /** Devolve o terminal a quem é dele, e entrega o que foi guardado. */
37
+ export function desligarCaptura() {
38
+ const linhas = guardadas ?? [];
39
+ if (originalLog)
40
+ console.log = originalLog;
41
+ guardadas = null;
42
+ originalLog = null;
43
+ return linhas;
44
+ }
45
+ /** O caractere de escape ANSI, montado por código para não viajar cru no arquivo. */
46
+ const ESC = String.fromCharCode(27);
47
+ /** Tira a cor: o que vai para um arquivo não carrega sequência de escape de terminal. */
48
+ export function semCor(line) {
49
+ return line.split(new RegExp(`${ESC}\\[[0-9;]*m`)).join("");
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.428",
3
+ "version": "0.16.430",
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": {