synthesisui 0.16.423 → 0.16.425

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.
@@ -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 MODO DELA - ver `mode.ts`. Commitado, ele barraria o time inteiro porque UMA pessoa ligou
52
+ * o bloqueio dela, e o aceite da etapa 06 é literal: *"a escolha de uma pessoa nunca barra
53
+ * outra"*. Esta lista reconcilia, então quem já está conectado também recebe a linha.
54
+ */
55
+ ".mode",
50
56
  ];
51
57
  const IGNORE_HEADER = "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
52
58
  "# clone is governed; the measurement and the local record are not, because\n" +
@@ -1,9 +1,12 @@
1
1
  import { readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import { changedSince } from "../changed-files.js";
4
+ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
5
+ import { checkContracts } from "../doctor/contract-check.js";
4
6
  import { appendEvent, ledgerPath } from "../doctor/ledger.js";
5
7
  import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
6
8
  import { governs, ungovernedIn } from "../governed.js";
9
+ import { readMode } from "../mode.js";
7
10
  import { namesRemoved, previousText, rulesTouchedBy, } from "../rule-touched.js";
8
11
  import { loadSystem } from "./doctor.js";
9
12
  const pass = () => ({ continue: true });
@@ -14,6 +17,28 @@ const speak = (context) => ({
14
17
  additionalContext: context,
15
18
  },
16
19
  });
20
+ /**
21
+ * A RECUSA - e ela só existe porque a PRÓPRIA pessoa ligou o modo CONSUMIR.
22
+ *
23
+ * `continue: true` continua ali: interromper a sessão dela seria uma punição, e o que se recusa é
24
+ * a mudança, não o trabalho. `decision: "block"` é o campo com que este evento devolve uma recusa
25
+ * ao agente, e o motivo vai JUNTO como contexto pelo mesmo caminho de sempre - um cliente que
26
+ * ignore o primeiro campo ainda entrega a frase à pessoa, e um bloqueio que some é pior que
27
+ * bloqueio nenhum.
28
+ *
29
+ * O QUE ELA NÃO É: desfazer o arquivo. Esta checagem roda DEPOIS da escrita, então o que ela
30
+ * devolve é a recusa da mudança e o que usar no lugar - e a frase diz isso com todas as letras,
31
+ * em vez de sugerir que a plataforma reverteu alguma coisa.
32
+ */
33
+ const refuse = (context) => ({
34
+ continue: true,
35
+ decision: "block",
36
+ reason: context,
37
+ hookSpecificOutput: {
38
+ hookEventName: "PostToolUse",
39
+ additionalContext: context,
40
+ },
41
+ });
17
42
  /** Written after the hook's first report of any kind, and never read for anything
18
43
  * else. Lives in our own directory, so removing the system removes the memory
19
44
  * too. It is committed with `_synthesisui/`, which means the greeting is
@@ -67,6 +92,14 @@ const startClock = (root) => writeFile(ledgerPath(root), "", { flag: "a" }).catc
67
92
  * escrever é o que a pessoa tem em mente.
68
93
  */
69
94
  const AT_MOST = 3;
95
+ /**
96
+ * QUANTOS ARQUIVOS ALÉM DO RELATÓRIO A RECUSA AINDA ALCANÇA - ver o laço no fim deste arquivo.
97
+ *
98
+ * O relatório tem teto porque ele é leitura de pessoa; a RECUSA tem teto porque ela é a promessa
99
+ * que ela ligou. Quarenta cobre uma rodada de trabalho de agente com folga e para antes de uma
100
+ * troca de árvore inteira virar quarenta leituras de arquivo.
101
+ */
102
+ const REFUSAL_CEILING = 40;
70
103
  const markGreeted = (root) => writeFile(join(root, GREETED), "", "utf8").catch(() => { });
71
104
  /** One sentence, once per project. Addressed to the agent because the hook has
72
105
  * no way to reach the person, and the person is who needs to know. */
@@ -153,8 +186,64 @@ async function amendment(root, rel, after, rules, names) {
153
186
  "Tell the person which it is: the rule changes, or this case is an exception worth recording.",
154
187
  ];
155
188
  }
156
- async function report(root, filePath) {
157
- const { table, doctrines } = await loadSystem(root);
189
+ /**
190
+ * O QUE ESTE ARQUIVO ESCREVEU QUE O SISTEMA DELA NÃO DECLARA - a régua da etapa 06.
191
+ *
192
+ * ELA É A MESMA NOS DOIS MODOS, e isto é a promessa inteira: a checagem sabe exatamente a mesma
193
+ * coisa em CONSUMIR e em CRIAR - que aquele eixo tem opções declaradas e que esta não é uma delas.
194
+ * O que muda é o que se FAZ com o que ela sabe.
195
+ *
196
+ * E ELA SÓ RODA QUANDO HÁ MODO. Sem modo, o comando é exatamente o que era antes desta etapa -
197
+ * nem esta leitura acontece. Um produto que passa a falar de composição para todo mundo porque
198
+ * uma pessoa ganhou um interruptor é ruído que ninguém pediu, e ruído é o que faz uma checagem
199
+ * ser desinstalada.
200
+ *
201
+ * A CHECAGEM É A QUE O `doctor` JÁ FAZ - `checkContracts`, uma fonte só. Ela é silenciosa por
202
+ * construção: um eixo que o contrato não declara não é checado, um componente que o sistema não
203
+ * carrega não é checado, e um nome sem origem legível não é cobrado de ninguém.
204
+ */
205
+ async function composition(root, rel, src, documents, systemName, mode) {
206
+ if (documents.length === 0)
207
+ return [];
208
+ const tally = emptyTally();
209
+ scanComponentsInto(tally, rel, src, await internalSpecifiers(root));
210
+ const breaches = checkContracts(tallyToInventory(tally, 80), documents);
211
+ if (breaches.length === 0)
212
+ return [];
213
+ return [
214
+ `${rel} - ${systemName} does not declare ${breaches
215
+ .map((b) => `\`${b.axis}="${b.used}"\` on ${b.element}`)
216
+ .join(", ")}.`,
217
+ "",
218
+ ...breaches.map((b) => ` ${b.element} ${b.axis}: ${b.offered.join(", ")}`),
219
+ "",
220
+ ...(mode === "consume"
221
+ ? [
222
+ /**
223
+ * A FRASE NÃO FINGE QUE DESFEZ NADA. Esta checagem roda depois da escrita, então o
224
+ * arquivo está no disco - dizer "recusado" sem dizer o que fazer com o que já está lá
225
+ * deixaria a pessoa achando que a plataforma reverteu por ela.
226
+ */
227
+ "You asked for consume mode, so this is refused: the file is already written, and it needs one of the options above.",
228
+ "Use one of them, or ask the person for a new one - do NOT invent an option.",
229
+ /**
230
+ * A SAÍDA VAI JUNTO COM A RECUSA - achado da revisão de DX no fecho.
231
+ *
232
+ * Quem lê isto é o agente, no meio de uma tarefa, e ele não tem como saber que o
233
+ * bloqueio tem um interruptor. Uma recusa sem a porta de saída visível é o adversário
234
+ * que `INV-HOOK-01` existe para não deixar nascer - mesmo sendo um adversário que a
235
+ * própria pessoa ligou.
236
+ */
237
+ "If this is not the work you meant to be doing, `npx synthesisui mode off` ends it.",
238
+ ]
239
+ : [
240
+ "Create mode, so this is recorded and not refused - you are growing the system.",
241
+ "If this option is meant to stay, tell the person, so it enters the system as a declared one.",
242
+ ]),
243
+ ];
244
+ }
245
+ async function report(root, filePath, mode) {
246
+ const { table, doctrines, documents } = await loadSystem(root);
158
247
  if (table.byName.size === 0)
159
248
  return null;
160
249
  /**
@@ -168,6 +257,15 @@ async function report(root, filePath) {
168
257
  return null;
169
258
  const rel = relative(root, filePath);
170
259
  const broke = await amendment(root, rel, src, doctrines.flatMap((doc) => doc.rules), table).catch(() => []);
260
+ /**
261
+ * A COMPOSIÇÃO SÓ É MEDIDA QUANDO ELA LIGOU UM MODO - ver `composition`.
262
+ *
263
+ * Guardada como todo o resto deste arquivo: um erro ao ler o tsconfig ou o documento custa a
264
+ * metade de composição do relatório, nunca a rodada.
265
+ */
266
+ const composed = mode
267
+ ? await composition(root, rel, src, documents, table.name ?? table.slug ?? "this system", mode).catch(() => [])
268
+ : [];
171
269
  const d = diagnose([scanSource(rel, src, table)]);
172
270
  const named = d.findings.filter((f) => nameToWrite(f));
173
271
  /**
@@ -197,11 +295,17 @@ async function report(root, filePath) {
197
295
  named: named.length,
198
296
  phantoms: phantoms.length,
199
297
  });
200
- if (broke.length === 0 &&
298
+ /** O bloqueio de CONSUMIR é a única saída que recusa, e ele acompanha o relatório seja qual for. */
299
+ const block = mode === "consume" && composed.length > 0;
300
+ const say = (text) => ({ text, block });
301
+ if (composed.length === 0 &&
302
+ broke.length === 0 &&
201
303
  named.length === 0 &&
202
304
  phantoms.length === 0 &&
203
- unnamed.length === 0)
204
- return greet(root, rel);
305
+ unnamed.length === 0) {
306
+ const hello = await greet(root, rel);
307
+ return hello ? say(hello) : null;
308
+ }
205
309
  // A report IS the evidence the greeting exists to provide, so it counts as the
206
310
  // introduction. Otherwise a project whose first file had drift would get the
207
311
  // "it is live" sentence afterwards, telling somebody who just watched it work.
@@ -231,22 +335,34 @@ async function report(root, filePath) {
231
335
  * Ela é a mais cara de ignorar: um valor solto se conserta depois, e uma regra apagada vira o
232
336
  * próximo agente restaurando a serif citando a doutrina, com o dono achando que ele alucinou.
233
337
  */
234
- if (broke.length > 0 && named.length === 0 && phantoms.length === 0)
235
- return broke.join("\n");
338
+ if (composed.length === 0 &&
339
+ broke.length > 0 &&
340
+ named.length === 0 &&
341
+ phantoms.length === 0)
342
+ return say(broke.join("\n"));
236
343
  const sample = unnamed
237
344
  .slice(0, 3)
238
345
  .map((f) => `line ${f.line} ${f.literal}`)
239
346
  .join(", ");
240
- if (broke.length === 0 && named.length === 0 && phantoms.length === 0)
241
- return [
347
+ if (composed.length === 0 &&
348
+ broke.length === 0 &&
349
+ named.length === 0 &&
350
+ phantoms.length === 0)
351
+ return say([
242
352
  `${rel} - ${unnamed.length} value${unnamed.length === 1 ? "" : "s"} here ${unnamed.length === 1 ? "has" : "have"} no name in this system, and nothing to replace ${unnamed.length === 1 ? "it" : "them"} with: ${sample}${unnamed.length > 3 ? `, +${unnamed.length - 3} more` : ""}.`,
243
353
  "Do not invent a name - leave them, or ask the person what they would call it.",
244
- ].join("\n");
354
+ ].join("\n"));
245
355
  /**
246
356
  * E QUANDO HÁ AS DUAS COISAS, a emenda lidera o relatório - mesma razão: ela é a que ninguém
247
357
  * conserta depois, e a lista de valores abaixo dela ensina a diferença entre deriva e emenda.
248
358
  */
359
+ /**
360
+ * E A COMPOSIÇÃO LIDERA QUANDO ELA EXISTE, pela mesma razão da emenda: em CONSUMIR ela é a única
361
+ * parte do relatório que RECUSA, e uma recusa escondida embaixo de uma lista de valores é uma
362
+ * recusa que ninguém lê.
363
+ */
249
364
  const lines = [
365
+ ...(composed.length > 0 ? [...composed, ""] : []),
250
366
  ...(broke.length > 0 ? [...broke, ""] : []),
251
367
  `${rel} - checked against ${table.name ?? table.slug}.`,
252
368
  ];
@@ -272,7 +388,7 @@ async function report(root, filePath) {
272
388
  if (phantoms.length > 0) {
273
389
  lines.push("", "Names this system does not declare. These look tokenized and apply nothing at all:", ...phantoms.slice(0, 20).map((p) => ` line ${p.line} ${p.name}`), "", "Use a name the system has, or say which value you need and what you would call it. Do NOT invent a token.");
274
390
  }
275
- return lines.join("\n");
391
+ return say(lines.join("\n"));
276
392
  }
277
393
  /**
278
394
  * O QUE O AGENTE ACABOU DE ESCREVER, pelo caminho que o cliente do agente nomeia.
@@ -306,6 +422,11 @@ export async function hook(opts) {
306
422
  return;
307
423
  }
308
424
  const ungoverned = await ungovernedIn(root);
425
+ /**
426
+ * O MODO QUE ELA LIGOU NESTE PROJETO - ver `mode.ts`. Uma leitura de arquivo pequeno, e a
427
+ * ausência (o caso de todo projeto que existe hoje) é o produto como ele sempre foi.
428
+ */
429
+ const mode = await readMode(root).catch(() => null);
309
430
  const named = namedInPayload(input);
310
431
  /**
311
432
  * A FERRAMENTA DE EDIÇÃO DIZ O ARQUIVO; QUALQUER OUTRA COISA PERGUNTA À ÁRVORE.
@@ -318,9 +439,9 @@ export async function hook(opts) {
318
439
  if (named) {
319
440
  const rel = relative(root, resolve(root, named));
320
441
  const body = governs(rel, ungoverned)
321
- ? await report(root, join(root, rel)).catch(() => null)
442
+ ? await report(root, join(root, rel), mode).catch(() => null)
322
443
  : null;
323
- process.stdout.write(`${JSON.stringify(body ? speak(body) : pass())}\n`);
444
+ process.stdout.write(`${JSON.stringify(body ? (body.block ? refuse(body.text) : speak(body.text)) : pass())}\n`);
324
445
  return;
325
446
  }
326
447
  const clock = await clockOf(root);
@@ -336,10 +457,17 @@ export async function hook(opts) {
336
457
  return;
337
458
  }
338
459
  const said = [];
460
+ /**
461
+ * UMA RECUSA EM QUALQUER ARQUIVO DA RODADA RECUSA A RODADA - e é o lado certo: um comando de
462
+ * shell que reescreve dez arquivos e acerta nove continua tendo escrito o décimo.
463
+ */
464
+ let block = false;
339
465
  for (const { rel } of changed.slice(0, AT_MOST)) {
340
- const body = await report(root, join(root, rel)).catch(() => null);
341
- if (body)
342
- said.push(body);
466
+ const body = await report(root, join(root, rel), mode).catch(() => null);
467
+ if (!body)
468
+ continue;
469
+ said.push(body.text);
470
+ block = block || body.block;
343
471
  }
344
472
  /**
345
473
  * O QUE FICOU DE FORA É NOMEADO, e com o comando que o olha - ver `AT_MOST`.
@@ -349,6 +477,45 @@ export async function hook(opts) {
349
477
  * pelo nome é o que transforma uma perda silenciosa numa lacuna declarada (lei 8).
350
478
  */
351
479
  const rest = changed.slice(AT_MOST);
480
+ /**
481
+ * E EM CONSUMIR, O TETO DO RELATÓRIO NÃO É O TETO DA RECUSA - achado da revisão de QA no fecho,
482
+ * e ele falsificava a invariante inteira.
483
+ *
484
+ * O CASO MEDIDO: ela liga CONSUMIR, o agente escreve quatro arquivos por um comando de shell, e
485
+ * a quebra está no quarto. O corte de três deixava aquilo passar com um aviso de "não checado
486
+ * aqui" - que, para quem pediu um bloqueio, é um passe. E é exatamente a população para a qual o
487
+ * caminho da árvore de trabalho foi construído: em 12/09, toda edição em lote do `codelevel`
488
+ * passou por shell.
489
+ *
490
+ * O RELATÓRIO CONTINUA EM TRÊS, e é outra pergunta: trinta relatórios numa resposta só é o ruído
491
+ * que faz uma checagem ser desinstalada. O que sobe é só a RECUSA - a composição dos demais é
492
+ * medida, e o que ela encontra entra como a lista do que precisa voltar, não como trinta
493
+ * relatórios.
494
+ *
495
+ * O TETO DE SEGURANÇA existe porque um `git checkout` de uma branch inteira também chega aqui: a
496
+ * recusa cobre uma rodada de trabalho, não uma troca de árvore.
497
+ */
498
+ const refusals = [];
499
+ if (mode === "consume") {
500
+ const { table, documents } = await loadSystem(root).catch(() => ({
501
+ table: null,
502
+ documents: [],
503
+ }));
504
+ if (table && table.byName.size > 0) {
505
+ for (const { rel } of rest.slice(0, REFUSAL_CEILING)) {
506
+ const src = await readFile(join(root, rel), "utf8").catch(() => null);
507
+ if (src == null)
508
+ continue;
509
+ const found = await composition(root, rel, src, documents, table.name ?? table.slug ?? "this system", "consume").catch(() => []);
510
+ if (found.length > 0)
511
+ refusals.push(found.join("\n"));
512
+ }
513
+ }
514
+ }
515
+ if (refusals.length > 0) {
516
+ said.push(...refusals);
517
+ block = true;
518
+ }
352
519
  if (rest.length > 0)
353
520
  said.push([
354
521
  `${rest.length} more file${rest.length === 1 ? "" : "s"} changed in this command and ${rest.length === 1 ? "was" : "were"} not checked here: ${rest
@@ -362,5 +529,6 @@ export async function hook(opts) {
362
529
  * e esta linha cobre o caso em que todos os relatórios falharam na leitura.
363
530
  */
364
531
  await startClock(root);
365
- process.stdout.write(`${JSON.stringify(said.length > 0 ? speak(said.join("\n\n")) : pass())}\n`);
532
+ const context = said.join("\n\n");
533
+ process.stdout.write(`${JSON.stringify(said.length === 0 ? pass() : block ? refuse(context) : speak(context))}\n`);
366
534
  }
@@ -0,0 +1,48 @@
1
+ import { resolve } from "node:path";
2
+ import { modePath, readMode, writeMode } from "../mode.js";
3
+ import { ensureGovernanceHome } from "./add.js";
4
+ /**
5
+ * `synthesisui mode` - ela liga e desliga o bloqueio dela, do próprio terminal.
6
+ *
7
+ * SEM ARGUMENTO ELE RESPONDE, e nunca muda nada. Um comando que muda estado ao ser chamado sem
8
+ * argumento é um comando que alguém dispara para consultar e descobre, depois, que ligou algo.
9
+ *
10
+ * E O QUE ELE IMPRIME É O ESTADO LIDO DE VOLTA, nunca o que ele tentou escrever: um interruptor
11
+ * que parece ligado e não está é pior que um que recusa.
12
+ */
13
+ const SAYS = {
14
+ consume: "consume - building with what this system already has. The check refuses an option the system does not declare, and tells you what it offers instead.",
15
+ create: "create - growing the system. The check reports and gets out of the way.",
16
+ };
17
+ const OFF = "no mode - nothing is blocked, which is how this worked before you had the choice.";
18
+ export async function mode(opts) {
19
+ const root = resolve(opts.dir ?? process.cwd());
20
+ const asked = opts.wanted?.trim().toLowerCase();
21
+ if (!asked) {
22
+ const current = await readMode(root);
23
+ console.log(`\n${current ? SAYS[current] : OFF}\n`);
24
+ return;
25
+ }
26
+ if (asked !== "create" && asked !== "consume" && asked !== "off") {
27
+ console.log(`\n"${opts.wanted}" is not a mode. The three are: consume, create, off.\n`);
28
+ return;
29
+ }
30
+ /**
31
+ * A CASA ANTES DA ESCOLHA, e a frase abaixo depende disto para ser verdade.
32
+ *
33
+ * O DEFEITO QUE ISTO FECHA, achado na revisão de QA do fecho: este comando criava a pasta e
34
+ * gravava o modo, e nada reconciliava a lista de ignorados. Num clone conectado ANTES desta
35
+ * versão essa lista não tem `.mode` - então a pessoa ligava o bloqueio dela, commitava sem
36
+ * perceber, e a escolha de uma pessoa passava a barrar o time inteiro. O terminal, enquanto
37
+ * isso, imprimia *"local to this clone, never committed"*.
38
+ *
39
+ * `ensureGovernanceHome` cria a pasta e ACRESCENTA à lista o que falta - ela nunca reescreve o
40
+ * arquivo, então uma linha que a pessoa escreveu ali continua onde está.
41
+ */
42
+ await ensureGovernanceHome(root);
43
+ await writeMode(root, asked === "off" ? null : asked);
44
+ /** Lido DE VOLTA do disco - ver o cabeçalho. */
45
+ const now = await readMode(root);
46
+ console.log(`\n${now ? SAYS[now] : OFF}\n`);
47
+ console.log(` ${modePath(root)} - local to this clone, never committed.\n`);
48
+ }
@@ -42,6 +42,21 @@ import { resolveReadParts, siblingProjects, takeCensus, } from "./import.js";
42
42
  export function draftLine(slug, base) {
43
43
  return `this went into your DRAFT - your repository still has what you installed. It reaches the disk once you publish${base ? `: ${base}/dashboard/mine/${slug}/publish` : ""}\n then: npx synthesisui@latest upgrade ${slug}`;
44
44
  }
45
+ /**
46
+ * O TRABALHO DELA FOI PARA A RAMIFICAÇÃO DELA, E ISSO SE DIZ - `INV-BRANCH-02`.
47
+ *
48
+ * O QUE O CLIENTE VIA SEM ISTO: `62 of 62 components written.` - a mesma frase de quem move a
49
+ * linha principal. Ela fecharia o terminal achando que o time recebeu, e descobriria o contrário
50
+ * num dia em que estivesse contando com aquilo. É a lacuna calada que a lei 8 proíbe: o cliente
51
+ * perdoa o que a gente diz que não faz, e não perdoa descobrir sozinho.
52
+ *
53
+ * A FRASE DIZ TRÊS COISAS, nesta ordem: onde o trabalho está, que ele está inteiro, e o que falta
54
+ * para alcançar o time. Sem a terceira ela é um aviso sem saída - e quem lê não tem o que fazer
55
+ * com ele.
56
+ */
57
+ export function branchLine(slug, base) {
58
+ return `this went to YOUR branch, not to the main line your team installs - nothing of it was lost. It reaches them when whoever owns this system approves it${base ? `: ${base}/dashboard/mine/${slug}` : ""}`;
59
+ }
45
60
  export function decisionLine(d, slug,
46
61
  /** Onde o sistema dele vive - o `publish` mora lá, e sem o endereço a frase manda procurar. */
47
62
  base) {
@@ -629,6 +644,12 @@ export async function remeasure(args) {
629
644
  console.log("");
630
645
  console.log(section("Sent"));
631
646
  console.log(body(`${out.written ?? 0} of ${out.total ?? 0} components written.`));
647
+ /**
648
+ * E ONDE ELE CAIU, logo abaixo do número - porque é o número que ela leria como "o time
649
+ * recebeu".
650
+ */
651
+ if (out.landedOn === "branch")
652
+ console.log(body(paint.strong(branchLine(slug, base))));
632
653
  /**
633
654
  * O QUE VEIO DOS ARQUIVOS DELE, DITO - e antes de qualquer outra nota, porque é a única parte
634
655
  * desta saída que fala do trabalho que ELE acabou de fazer.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import { list } from "./commands/list.js";
22
22
  import { login } from "./commands/login.js";
23
23
  import { logout } from "./commands/logout.js";
24
24
  import { mcp } from "./commands/mcp.js";
25
+ import { mode } from "./commands/mode.js";
25
26
  import { refit } from "./commands/refit.js";
26
27
  import { request } from "./commands/request.js";
27
28
  import { status } from "./commands/status.js";
@@ -83,6 +84,10 @@ Usage - governance (deterministic, FREE):
83
84
  the ratchet, open requests - local first, no login needed
84
85
  synthesisui ci [--write] the two steps that put drift in your PR - annotations on
85
86
  the line, and a ratchet that fails only on regression
87
+ synthesisui mode [consume|create|off] consume: the check REFUSES an option your system does not
88
+ declare, and says what it offers instead. create: it
89
+ reports and gets out of the way. No argument answers
90
+ where you are. Local to this clone, never committed
86
91
  synthesisui hook the check itself; installed by connect, run by your editor
87
92
  synthesisui mcp the system as tools; installed by connect, run by your agent
88
93
 
@@ -275,6 +280,12 @@ async function main() {
275
280
  case "hook":
276
281
  await hook({ dir });
277
282
  return;
283
+ /**
284
+ * O INTERRUPTOR DELA - ver `mode.ts`. Sem argumento ele RESPONDE, e só com um deles muda.
285
+ */
286
+ case "mode":
287
+ await mode({ dir, ...(args[0] ? { wanted: args[0] } : {}) });
288
+ break;
278
289
  // Long-lived: it owns stdin/stdout until the client closes the pipe, so
279
290
  // it must not be reached by anything that prints.
280
291
  case "mcp":
@@ -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.423";
238
+ export const MATERIALISER_SINCE = "0.16.424";
239
239
  /**
240
240
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
241
241
  *
@@ -366,7 +366,7 @@ export const COUNTED_DIFFERENTLY = "this run counts a value as named only when Y
366
366
  * utility, um arquivo escrito inteiro no vocabulário do sistema recebia zero. A contagem em si é a
367
367
  * outra marca - ver `COUNTED_DIFFERENTLY_SINCE`, que sobe no mesmo diff.
368
368
  */
369
- export const CHECKER_SINCE = "0.16.422";
369
+ export const CHECKER_SINCE = "0.16.424";
370
370
  /**
371
371
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
372
372
  *
package/dist/mode.js ADDED
@@ -0,0 +1,34 @@
1
+ import { readFile, rm, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /**
4
+ * NO PROJETO DELA, E LOCAL - e as duas metades são a promessa.
5
+ *
6
+ * NO PROJETO porque o sistema é COMPARTILHADO: um modo guardado na plataforma faria a escolha de
7
+ * uma pessoa barrar todas as outras que usam aquele design system.
8
+ *
9
+ * LOCAL - e não em `config.json`, que é versionado - porque o mesmo argumento vale um nível
10
+ * abaixo: um modo commitado barra o time inteiro porque uma pessoa ligou o dela. Este arquivo
11
+ * entra na lista de ignorados que a nossa pasta gerencia (ver `IGNORED` em `commands/add.ts`), e
12
+ * essa lista RECONCILIA, então quem já está conectado também o recebe.
13
+ */
14
+ export const modePath = (root) => join(root, "_synthesisui", ".mode");
15
+ /**
16
+ * O QUE ELA ESCOLHEU, OU NADA.
17
+ *
18
+ * QUALQUER COISA QUE NÃO SEJA UM DOS DOIS É "NADA", e isso é o lado seguro: um arquivo editado à
19
+ * mão, escrito por uma versão futura ou corrompido não é uma decisão de bloquear. Ler lixo como
20
+ * CONSUMIR seria a plataforma decidindo por ela a partir de algo que ela não escreveu.
21
+ */
22
+ export async function readMode(root) {
23
+ const raw = await readFile(modePath(root), "utf8").catch(() => "");
24
+ const value = raw.trim().toLowerCase();
25
+ return value === "create" || value === "consume" ? value : null;
26
+ }
27
+ /** Escolher, ou desligar. `null` volta a não ter modo, em vez de virar o outro. */
28
+ export async function writeMode(root, mode) {
29
+ if (mode === null) {
30
+ await rm(modePath(root), { force: true });
31
+ return;
32
+ }
33
+ await writeFile(modePath(root), `${mode}\n`, "utf8");
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.423",
3
+ "version": "0.16.425",
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": {