synthesisui 0.16.411 → 0.16.413

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.
@@ -8,10 +8,11 @@ import { detectAppDirs } from "../global-sheet.js";
8
8
  import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-templates.js";
9
9
  import { body, section, snippet } from "../output.js";
10
10
  import { findCollision, installedThemeVars, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
11
- import { fetchComponent, RegistryError } from "../registry.js";
11
+ import { fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
12
  import { installedThemeCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
13
13
  import { flavourResolver } from "../styles-flavour.js";
14
- import { inTheirTongue, projectTongue, sumSpoken, } from "../their-tongue.js";
14
+ import { inTheirTongue, projectTongue, sumSpoken, tongueFromArtifacts, } from "../their-tongue.js";
15
+ import { resolvableVars } from "../their-vars.js";
15
16
  import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
16
17
  import { readWiring } from "../wiring-read.js";
17
18
  import { editedSinceWritten, readWritten, recordWritten } from "../written.js";
@@ -114,7 +115,38 @@ export async function component(slug, name, opts) {
114
115
  * apagaria a cor. Um projeto de destino chega sem mapa no `.lock`, nada é traduzido, e a folha
115
116
  * continua sendo o caminho - o comando DIZ qual dos dois aconteceu.
116
117
  */
117
- const tongue = await projectTongue(root, slug);
118
+ const installedTongue = await projectTongue(root, slug);
119
+ /**
120
+ * E SEM O `add`, O SISTEMA VEM DO REGISTRY E O VOCABULARIO E' DERIVADO EM MEMORIA.
121
+ *
122
+ * O QUE ACONTECIA (medido em 10/09): sem `.lock`, `projectTongue` responde `null`, nada era
123
+ * traduzido, e o arquivo saia com `var(--ds-*)` cru - inutil ate' que a pessoa rodasse `add` e
124
+ * materializasse 1813 linhas, 111 variaveis e 628 referencias no repositorio dela. Pedir UM
125
+ * botao passava por instalar o sistema inteiro.
126
+ *
127
+ * A NOSSA FOLHA E' A REFERENCIA, NAO O PRE-REQUISITO (a definicao do produto, 07/09): ela existe
128
+ * para o agente dele ler e construir sobre ela. A medicao que ela carregava - quais das nossas
129
+ * variaveis tem nome no codigo dele - nao precisa de disco: e' a mesma varredura que o `add`
130
+ * faz, feita aqui e jogada fora depois de traduzir.
131
+ *
132
+ * UMA IDA A MAIS A' REDE, e so' neste caminho: quem ja' instalou o sistema le' do `.lock` como
133
+ * sempre. O `catch` mantem o comportamento antigo - sem tradução, e o bloco de setup abaixo diz
134
+ * que a folha e' o caminho.
135
+ */
136
+ const fromRegistry = installedTongue
137
+ ? null
138
+ : await fetchDesignSystem(base, slug, opts.version).catch(() => null);
139
+ /** A leitura do sistema NAO chegou - rede fora, ou sistema privado sem sessao. Ver o relato. */
140
+ const installedSheetMissing = !installedTongue && fromRegistry === null;
141
+ /**
142
+ * A NOSSA FOLHA ESTA' NA PASTA DELE? - o `.lock` responde, e e' o mesmo arquivo que o `add`
143
+ * escreve. Desde o A1 esta pergunta deixou de ser retorica: `component` roda sem ele.
144
+ */
145
+ const installedHere = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8").then(() => true, () => false);
146
+ const tongue = installedTongue ??
147
+ (fromRegistry
148
+ ? await tongueFromArtifacts(root, fromRegistry.artifacts)
149
+ : null);
118
150
  /**
119
151
  * O RELATÓRIO SÓ EXISTE DEPOIS DE OS BYTES EXISTIREM - e é essa ordem que corrige o defeito.
120
152
  *
@@ -422,6 +454,45 @@ export async function component(slug, name, opts) {
422
454
  console.log(spoken.left.length === 0
423
455
  ? ` No variable in what was just written points at our stylesheet - they are all names YOUR code declares.`
424
456
  : ` ${spoken.left.length} still point${spoken.left.length === 1 ? "s" : ""} at our stylesheet (${spoken.left.slice(0, 3).join(", ")}${spoken.left.length > 3 ? ", …" : ""}), so tokens.css carries ${spoken.left.length === 1 ? "it" : "those"}.`);
457
+ /**
458
+ * E DIZER DE ONDE VEIO O VOCABULARIO, quando o sistema NAO esta' instalado aqui.
459
+ *
460
+ * Sem esta linha o resultado seria magico: o arquivo fala a lingua dele e nada explica como.
461
+ * A frase importa porque a conclusao natural - *"entao o `add` ja' rodou"* - esta' errada, e a
462
+ * pessoa precisa saber que o que ela tem e' o componente, e que a folha continua sendo uma
463
+ * escolha e nao uma pendencia.
464
+ */
465
+ if (fromRegistry)
466
+ console.log(` Nothing of ours was installed to do that - the system was read at v${fromRegistry.version}, and the names came from your own code.`);
467
+ }
468
+ else if (!tongue) {
469
+ /**
470
+ * A LACUNA DECLARADA (lei 8): nada foi traduzido, e ISSO SE DIZ.
471
+ *
472
+ * O QUE ACONTECIA SEM ESTA LINHA: o `.tsx` saia com `var(--ds-*)` cru e o terminal ficava
473
+ * mudo. A pessoa abre o arquivo, ve' variaveis que o `globals.css` dela nao declara, e a
474
+ * unica forma de descobrir por que a cor nao apareceu e' ir ler o nosso codigo.
475
+ *
476
+ * E OS DOIS MOTIVOS SAO DIFERENTES, entao a frase distingue: o repositorio dela nao nomeia
477
+ * nenhum destes valores (o caso comum, e nao ha' o que fazer), ou a leitura do sistema nao
478
+ * chegou - rede fora, sistema privado sem sessao. O segundo tem conserto e o primeiro nao.
479
+ */
480
+ /**
481
+ * E AS TRES CAUSAS SAO DIFERENTES, entao a frase distingue - so' uma delas tem conserto na
482
+ * mao dele, e ela e' a mais provavel das tres.
483
+ *
484
+ * a leitura nao chegou rede fora, sessao ausente, sistema privado
485
+ * o build dele nao existe `resolvableVars` recusa todo par que o build nao emite, por
486
+ * desenho: um nome afirmado sem prova pinta a cor errada no dia
487
+ * em que ele o renomear. Num clone fresco isso e' o caso normal
488
+ * o codigo dele nao nomeia nada a unica que o silencio de fato descrevia
489
+ */
490
+ const noBuild = !installedSheetMissing && (await resolvableVars(root)) === null;
491
+ console.log(installedSheetMissing
492
+ ? ` Nothing was translated: "${slug}" could not be read from the registry just now, so the variables stayed as ours. Check your connection, or run: npx synthesisui login`
493
+ : noBuild
494
+ ? ` Nothing was translated: this project has no build output to read, and a name we cannot see your build emit is a name we will not write. Run your build once and ask for it again - the file then speaks your own names.`
495
+ : ` Nothing was translated: your code names none of the values this system declares yet, so the variables stayed as ours and tokens.css is what resolves them.`);
425
496
  }
426
497
  // ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
427
498
  const tailwind = flavour === "tailwind";
@@ -454,9 +525,28 @@ export async function component(slug, name, opts) {
454
525
  */
455
526
  const need = whatOnlyTheSheetResolves({
456
527
  source: writtenSource,
457
- themeCss: await installedThemeCss(root, slug),
528
+ /**
529
+ * O `@theme` DO SISTEMA - do disco quando ele instalou, do registry quando nao.
530
+ *
531
+ * A pergunta que este bloco faz e' *"sobrou classe que so' o nosso @theme gera?"*. Sem o
532
+ * `add`, `installedThemeCss` responde vazio, e vazio leria como "nao sobrou nada" - uma
533
+ * resposta certa por acidente que ficaria errada no dia em que sobrasse.
534
+ */
535
+ themeCss: fromRegistry?.artifacts?.["theme.css"] ??
536
+ (await installedThemeCss(root, slug)),
458
537
  /** O vocabulário DELE sai da conta - ver `theirNames`. O mapa do `.lock` é a via mais barata. */
459
538
  theirNames: tongue?.names ? [...tongue.names.values()] : [],
539
+ /**
540
+ * A TERCEIRA FONTE NÃO É MEDIDA AQUI, e o vazio é a resposta EXPLÍCITA disso.
541
+ *
542
+ * As classes de receita que a folha declara entrariam nesta conta se esta linha entregasse o
543
+ * `tokens.css` instalado - e o efeito está medido em `steps/11-fora-do-escopo.md`: este comando
544
+ * diz *"This file needs nothing else… It is yours."* sobre um componente cujo estilo inteiro vem
545
+ * de `.ds-button`. Ligar a medição aqui é mudar o que este comando promete, com o bloco de setup
546
+ * dele para redesenhar: é etapa própria. O que o argumento obrigatório garante é que a omissão
547
+ * está ESCRITA, e não escondida num parâmetro que ninguém passou.
548
+ */
549
+ sheetCss: "",
460
550
  });
461
551
  if (!need.needed) {
462
552
  console.log(section("This file needs nothing else"));
@@ -517,6 +607,19 @@ export async function component(slug, name, opts) {
517
607
  }
518
608
  if (need.needed && !alreadyWired) {
519
609
  console.log(section(`One-time setup (once per app, for "${slug}")`));
610
+ /**
611
+ * O PASSO ZERO VEM PRIMEIRO QUANDO A FOLHA NAO EXISTE - e desde o A1 este e' o caso comum.
612
+ *
613
+ * O QUE ACONTECIA: o passo 1 mandava colar `@import ".../tokens.css"` e o aviso de que o
614
+ * sistema nao esta' instalado saia na ULTIMA linha, entre parenteses. Quem nunca rodou `add`
615
+ * - agora um caminho normal, porque o `component` deixou de exigi-lo - lia a instrucao de
616
+ * cima para baixo e importava um caminho que nao existe. O erro do build nao fala de
617
+ * install: fala de um arquivo ausente.
618
+ *
619
+ * O motivo antes da instrucao, e nao depois dela.
620
+ */
621
+ if (!installedHere)
622
+ console.log(body(`First: "${slug}" is not installed in this project yet, so the file the import below points at does not exist. Run \`npx synthesisui add ${slug}\` before pasting it.`));
520
623
  console.log(body(need.variables.length > 0
521
624
  ? `(what still needs the sheet here: ${need.variables.slice(0, 3).join(", ")}${need.variables.length > 3 ? `, +${need.variables.length - 3}` : ""} - your code names no value for ${need.variables.length === 1 ? "it" : "them"})`
522
625
  : `(what still needs the sheet here: the ${need.classes.slice(0, 3).join(", ")}${need.classes.length > 3 ? `, +${need.classes.length - 3}` : ""} ${need.classes.length === 1 ? "utility" : "utilities"}, which only this system's @theme generates)`));
@@ -529,8 +632,10 @@ export async function component(slug, name, opts) {
529
632
  console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
530
633
  console.log("");
531
634
  console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
532
- console.log("");
533
- console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
635
+ if (installedHere) {
636
+ console.log("");
637
+ console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
638
+ }
534
639
  }
535
640
  /**
536
641
  * O ELEMENTO QUE SAIU NÃO ATIVA O QUE A RECEITA PEDE - e isso se diz, em vez de sumir (lei 8).
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { codexPinBefore, wireAgent } from "../agent-wiring.js";
4
4
  import { blockHomes, syncClaudeMd } from "../claude-md.js";
5
5
  import { resolveRegistry } from "../config.js";
6
+ import { notAProject, projectRootFrom } from "../is-a-project.js";
6
7
  import { fmt, say } from "../lang.js";
7
8
  import { body, bodyWrapped, paint, section, snippet } from "../output.js";
8
9
  import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
@@ -212,6 +213,25 @@ version) {
212
213
  }
213
214
  export async function connect(opts) {
214
215
  const root = opts.dir ?? process.cwd();
216
+ /**
217
+ * ANTES DA PRIMEIRA ESCRITA, SEMPRE - ver `is-a-project.ts`.
218
+ *
219
+ * MEDIDO EM 10/09: este comando tinha 6 escritas e 0 checagens de projeto. Rodado por engano
220
+ * na home ou numa pasta recem-criada, ele deixava `CLAUDE.md`, `.claude/settings.json`,
221
+ * `.mcp.json` e a casa da governanca ali - e um `CLAUDE.md` orfao instrui todo agente que
222
+ * abrir naquela pasta, semanas depois, sem ninguem lembrar de onde ele veio.
223
+ *
224
+ * AQUI E NAO DENTRO DE CADA ESCRITA: recusar na porta e' a unica forma de a promessa
225
+ * *"nada foi escrito"* ser verdade. Meia recusa deixaria dois arquivos e nenhuma mensagem.
226
+ */
227
+ const project = await projectRootFrom(root);
228
+ if (!project) {
229
+ console.log(section("Not a project"));
230
+ for (const line of notAProject("connect", root))
231
+ console.log(body(line));
232
+ process.exitCode = 1;
233
+ return;
234
+ }
215
235
  // Both unless one is explicitly turned off - somebody who says `--no-hook`
216
236
  // means it, and somebody who says nothing wants the thing to work.
217
237
  const want = { hook: opts.hook !== false, mcp: opts.mcp !== false };
@@ -916,6 +916,13 @@ export async function doctor(opts) {
916
916
  source: scannedSource,
917
917
  themeCss,
918
918
  theirNames: installed.theirs.byName.keys(),
919
+ /**
920
+ * A TERCEIRA FONTE NÃO É MEDIDA AQUI - ver o mesmo comentário em `component.ts` e o número em
921
+ * `steps/11-fora-do-escopo.md`. Este comando decide `unwired` e `blocked` por `needed`, então
922
+ * ligar a medição muda o veredito do `doctor` num repositório real: é etapa própria, e o vazio
923
+ * deixa a omissão escrita em vez de escondida.
924
+ */
925
+ sheetCss: "",
919
926
  });
920
927
  /**
921
928
  * BLOQUEADO SÓ QUANDO A FOLHA É NECESSÁRIA - a mesma pergunta que decide `unwired`.
@@ -7,7 +7,7 @@ import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalle
7
7
  import { postGenerate, RegistryError } from "../registry.js";
8
8
  import { flavourResolver } from "../styles-flavour.js";
9
9
  import { projectTongue } from "../their-tongue.js";
10
- import { recordWritten } from "../written.js";
10
+ import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
11
11
  /** PascalCase para o hint de import (course-card → CourseCard). */
12
12
  function pascalName(name) {
13
13
  return name.replace(/(^|[-_])([a-z0-9])/g, (_, __, c) => c.toUpperCase());
@@ -76,6 +76,23 @@ export async function generate(description, opts) {
76
76
  await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
77
77
  /** O VOCABULÁRIO DELE - um componente gerado cai no mesmo projeto e fala a mesma língua. */
78
78
  await projectTongue(root, slug), await installedThemeVars(root, slug));
79
+ /**
80
+ * A EDICAO DELE VENCE A REESCRITA (T7) - a leitura que faltava.
81
+ *
82
+ * MEDIDO EM 10/09: este comando GRAVAVA o fingerprint e nunca o lia. A promessa existia pela
83
+ * metade - o `upgrade` respeitava o arquivo dele, e o comando que o escreveu na primeira vez
84
+ * passava por cima na segunda. Um pedido parecido devolve o mesmo nome, e o ajuste dele some.
85
+ *
86
+ * O QUE ELE NAO PERDE AO SER RECUSADO: a receita e o CSS ja' estao no disco - so' o `.tsx`
87
+ * nao foi escrito. E a saida e' a mesma que o `component` oferece.
88
+ */
89
+ const edited = await theirEdits(join(root, "_synthesisui", "ds", slug), res.name, compDir);
90
+ if (edited && edited.length > 0 && !opts.force) {
91
+ console.log("");
92
+ console.log(keptLine(res.name, edited, `npx synthesisui generate "${description}" --name ${res.name} --force`));
93
+ console.log(` The recipe and the compiled CSS are on disk either way - only the .tsx was not written.`);
94
+ return;
95
+ }
79
96
  for (const file of files) {
80
97
  await writeFile(join(compDir, file.filename), file.code, "utf8");
81
98
  }
@@ -57,6 +57,20 @@ async function report(root, filePath) {
57
57
  const rel = relative(root, filePath);
58
58
  const d = diagnose([scanSource(rel, src, table)]);
59
59
  const named = d.findings.filter((f) => nameToWrite(f));
60
+ /**
61
+ * O VALOR QUE NADA NOMEIA - dito, e nunca batizado (A1 da etapa 11, escolha dele em 10/09).
62
+ *
63
+ * ERA SILÊNCIO, e o silêncio era a única resposta errada disponível. O comentário que estava aqui
64
+ * dizia que a deriva sem nome "não vale interromper, porque o único conselho honesto é pergunte a
65
+ * uma pessoa" - e a conclusão não segue da premissa: quem lê um relatório que só fala de valores
66
+ * nomeáveis não tem como distinguir "não sobrou nada" de "sobrou, e nós engolimos".
67
+ *
68
+ * ENTÃO ELE DIZ O VALOR E A LINHA, E PARA AÍ. Nenhum nome nosso é proposto: `--ds-<algo>`
69
+ * inventado aqui entraria no código dele sem uma pessoa ter decidido, e este comando roda depois
70
+ * de CADA escrita do agente - é o pior lugar do produto para inventar vocabulário. A seção 0 da
71
+ * jornada é literal: sem nome no sistema DELE, o valor fica onde está.
72
+ */
73
+ const unnamed = d.findings.filter((f) => !nameToWrite(f));
60
74
  const phantoms = d.files.flatMap((f) => f.phantoms ?? []);
61
75
  // Every check leaves one line in the ledger - INCLUDING the clean ones,
62
76
  // because a fix is itself a write, so the clean re-check of a file that was
@@ -70,21 +84,60 @@ async function report(root, filePath) {
70
84
  named: named.length,
71
85
  phantoms: phantoms.length,
72
86
  });
73
- // Unnamed drift alone is deliberately NOT worth interrupting for. There is
74
- // no token to move to, so the only honest advice is "ask a person" - and
75
- // saying that after every edit trains the reader to skip the block.
76
- if (named.length === 0 && phantoms.length === 0)
87
+ if (named.length === 0 && phantoms.length === 0 && unnamed.length === 0)
77
88
  return greet(root, rel);
78
89
  // A report IS the evidence the greeting exists to provide, so it counts as the
79
90
  // introduction. Otherwise a project whose first file had drift would get the
80
91
  // "it is live" sentence afterwards, telling somebody who just watched it work.
81
92
  await markGreeted(root);
93
+ /**
94
+ * QUANDO NÃO HÁ NADA A FAZER NAQUELE ARQUIVO, O RELATÓRIO É DE DUAS LINHAS - e isto é o que
95
+ * mantém o comando instalado.
96
+ *
97
+ * MEDIDO pela revisão de DX no fecho da etapa 11, rodando o scanner deste binário sobre os 38
98
+ * arquivos de uma biblioteca real com o vocabulário mais generoso possível (as 94 custom
99
+ * properties que o próprio repositório declara): **20 arquivos (53%) disparam pelo menos um
100
+ * achado sem nome, e 18 desses 20 têm ZERO achado nomeado** - o relatório inteiro seria o bloco
101
+ * novo. O cabeçalho deste arquivo é explícito sobre o risco: *"a hook that speaks on every edit
102
+ * is noise, and noise is what gets a hook removed"*.
103
+ *
104
+ * Então a MESMA informação sai em duas formas. Sozinha, ela é uma frase - o valor, a linha, e a
105
+ * ação, que é não agir. Ao lado de algo acionável, ela é um bloco, porque aí o contraste com a
106
+ * seta do bloco de cima é o que ensina a diferença entre os dois conjuntos.
107
+ *
108
+ * O que NÃO está resolvido, e é decisão dele: a REPETIÇÃO no tempo. Um valor cuja resposta certa
109
+ * é "deixe como está" volta a ser dito na próxima escrita naquele arquivo, porque não existe
110
+ * memória por linha e criar uma é comportamento novo. Está medido em `steps/11-fora-do-escopo.md`.
111
+ */
112
+ const sample = unnamed
113
+ .slice(0, 3)
114
+ .map((f) => `line ${f.line} ${f.literal}`)
115
+ .join(", ");
116
+ if (named.length === 0 && phantoms.length === 0)
117
+ return [
118
+ `${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` : ""}.`,
119
+ "Do not invent a name - leave them, or ask the person what they would call it.",
120
+ ].join("\n");
82
121
  const lines = [`${rel} - checked against ${table.name ?? table.slug}.`];
83
122
  if (named.length > 0) {
84
123
  lines.push("", "Values written by hand that this system already has a name for:", ...named
85
124
  .slice(0, 20)
86
125
  .map((f) => ` line ${f.line} ${f.literal} → ${nameToWrite(f)}`), "", "Replace them now, while you still have this file in mind.");
87
126
  }
127
+ if (unnamed.length > 0) {
128
+ /**
129
+ * O TÍTULO CARREGA A PROIBIÇÃO, e não a terceira frase de um parágrafo - achado da revisão de
130
+ * DX: a lista tinha a MESMA forma da lista acionável de cima (`Values written by hand that…`,
131
+ * uma linha por achado), e um agente que acabou de aprender *"lista de valores → proponha o
132
+ * token"* recebia aqui uma lista igual com a única frase que interrompe esse reflexo escondida
133
+ * no fim.
134
+ */
135
+ lines.push("", "Not yet named - do NOT invent a name for these:", ...unnamed.slice(0, 20).map((f) => ` line ${f.line} ${f.literal}`),
136
+ /** O QUE FOI CORTADO É DITO - medido num `.css` real: 20 de 252, e as 232 sumiam caladas. */
137
+ ...(unnamed.length > 20
138
+ ? [` … and ${unnamed.length - 20} more in this file`]
139
+ : []), "", "This system declares no name for them, so there is nothing to replace them with.", "Leave them as they are, or tell the person the value and what THEY would call it.");
140
+ }
88
141
  if (phantoms.length > 0) {
89
142
  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.");
90
143
  }
@@ -8,7 +8,7 @@ import { installedThemeVars, reactMajorOf, readInstalledConvention, readInstalle
8
8
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
9
9
  import { flavourResolver } from "../styles-flavour.js";
10
10
  import { projectTongue } from "../their-tongue.js";
11
- import { recordWritten } from "../written.js";
11
+ import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
12
12
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
13
13
  * a folder holding only refit artifacts doesn't count). */
14
14
  /** True when the system is actually installed (tokens.css present). */
@@ -122,6 +122,26 @@ export async function refit(file, opts) {
122
122
  const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, flavourOf(res.name), await reactMajorOf(root), await readInstalledConvention(root, slug), res.name, await readInstalledScheme(root, slug),
123
123
  /** O VOCABULÁRIO DELE - o `refit` reescreve o componente e não passava pela porta. */
124
124
  await projectTongue(root, slug), await installedThemeVars(root, slug));
125
+ /**
126
+ * A EDICAO DELE VENCE A REESCRITA (T7) - a leitura que faltava, a mesma do `generate`.
127
+ *
128
+ * Este comando REESCREVE um componente que a pessoa ja' tem, que e' exatamente a situacao em
129
+ * que o arquivo dela mais provavelmente foi tocado. Ele gravava o fingerprint e nunca o lia.
130
+ */
131
+ const edited = await theirEdits(join(root, "_synthesisui", "ds", slug), res.name, compDir);
132
+ if (edited && edited.length > 0 && !opts.force) {
133
+ console.log("");
134
+ console.log(
135
+ /**
136
+ * O COMANDO QUE RETOMA E' O QUE ELA ACABOU DE RODAR - `refit` recebe o CAMINHO do
137
+ * arquivo-fonte, nunca o nome do componente. Sugerir `refit ${res.name}` devolveria
138
+ * `Could not read "<nome>"`, e uma saida que nao funciona e' pior que nenhuma saida:
139
+ * a pessoa fez o que a tela mandou e levou uma segunda recusa.
140
+ */
141
+ keptLine(res.name, edited, `npx synthesisui refit ${file}${opts.name ? ` --name ${opts.name}` : ""} --force`));
142
+ console.log(` The recipe is saved in your system's draft either way - only the .tsx was not written.`);
143
+ return;
144
+ }
125
145
  for (const f of files) {
126
146
  await writeFile(join(compDir, f.filename), f.code, "utf8");
127
147
  }
@@ -582,6 +582,24 @@ export async function remeasure(args) {
582
582
  console.log(body(`${out.written ?? 0} of ${out.total ?? 0} components written.`));
583
583
  for (const note of out.notes ?? [])
584
584
  console.log(body(paint.faint(note)));
585
+ /**
586
+ * A RECUSA, DITA - e este era o unico numero da tela sem explicacao.
587
+ *
588
+ * O QUE O CLIENTE VIA (medido em 10/09): `60 of 62 components written.` e mais nada. Os dois
589
+ * que faltaram estavam nomeados na resposta, com o motivo, e o comando descartava o campo. Quem
590
+ * le "60 de 62" so' pode concluir uma coisa - que a esteira perdeu dois - e vai procurar um
591
+ * defeito que nao existe, ou pior, nao vai.
592
+ *
593
+ * A COPY JA' EXISTIA: `because` e' a frase do servidor, e reescreve-la aqui seria a segunda
594
+ * versao da mesma explicacao. A primeira coisa que a segunda faz e' discordar da primeira.
595
+ */
596
+ const refused = out.refused ?? [];
597
+ if (refused.length > 0) {
598
+ console.log("");
599
+ console.log(body(`${refused.length} ${refused.length === 1 ? "component was" : "components were"} refused - ${refused.length === 1 ? "it is" : "they are"} not lost, ${refused.length === 1 ? "it was" : "they were"} not written:`));
600
+ for (const r of refused)
601
+ console.log(body(paint.strong(` ✕ ${r.name} - ${r.because}`)));
602
+ }
585
603
  /**
586
604
  * A RECUSA POR ESCOPO, DITA - e não como um número que some.
587
605
  *
@@ -1,8 +1,37 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
2
+ import { dirname, join, relative } from "node:path";
3
3
  import { readProjectConfig, resolveRegistry } from "../config.js";
4
4
  import { fetchTemplate } from "../registry.js";
5
+ import { installedThemeCss, installedTokensCss, whatOnlyTheSheetResolves, } from "../sheet-needed.js";
5
6
  import { inTheirTongue, projectTongue } from "../their-tongue.js";
7
+ import { keptLine, recordWritten, editedHere as theirEdits, } from "../written.js";
8
+ /**
9
+ * O QUE O ARQUIVO DIZ TAMBÉM É COBRANÇA - e ela é a metade que FICA no repositório dele.
10
+ *
11
+ * O DEFEITO, achado pela revisão de DX no fecho da etapa 11: o fecho no terminal parou de mandar
12
+ * instalar a folha quando a tradução a tornou desnecessária, e o `.tsx` que acabou de ser escrito
13
+ * continuava abrindo com *"Ensure the DS tokens.css is imported"* e *"Keep the … ds-* classes
14
+ * (on-system…)"*. O terminal role uma vez; o comentário fica no arquivo que o time dele abre
15
+ * depois, sem o terminal por perto para desmentir. É a mesma contradição de duas frases que o
16
+ * critério conserta, um arquivo adiante.
17
+ *
18
+ * QUEM SABE A RESPOSTA É ESTE LADO, e é por isso que a linha sai aqui e não no servidor: a
19
+ * pergunta é *"o REPOSITÓRIO dele ainda precisa da folha?"*, e o codegen do servidor não conhece o
20
+ * repositório dele. Lá o header é escrito completo, que é a resposta certa para quem não traduziu
21
+ * nada.
22
+ *
23
+ * E O PAR NÃO É VIGIADO POR ACIDENTE: `apps/web/src/lib/ds/the-page-header-can-be-uncharged.spec.ts`
24
+ * assere que o header do servidor traz exatamente estas duas marcas. Se alguém reescrever a frase
25
+ * lá, aquele spec fica VERMELHO - em vez de este filtro passar a não casar em silêncio, que é
26
+ * exatamente como um `replace` por string falha aqui.
27
+ */
28
+ const SHEET_INSTRUCTION = /tokens\.css is imported|\(on-system/;
29
+ function withoutTheSheetInstruction(code) {
30
+ return code
31
+ .split("\n")
32
+ .filter((line) => !SHEET_INSTRUCTION.test(line))
33
+ .join("\n");
34
+ }
6
35
  /**
7
36
  * Materializes a whole page from a DS template into the project (hybrid
8
37
  * codegen-first): the server codegens deterministic files, we write them, and
@@ -28,6 +57,36 @@ export async function template(slug, name, opts) {
28
57
  const defaultDir = join("templates", asName ?? name);
29
58
  const pageRel = opts.out ?? join(defaultDir, pageFile.filename);
30
59
  const pageDir = dirname(join(root, pageRel));
60
+ /**
61
+ * A EDICAO DELE VENCE A REESCRITA (T7) - a MESMA guarda do `component`, no comando que a nao
62
+ * tinha.
63
+ *
64
+ * MEDIDO EM 10/09: `component.ts` consultava o registro do que nos escrevemos em seis pontos e
65
+ * `template.ts` em nenhum. E este comando escreve MAIS: uma pagina inteira, mais a folha
66
+ * co-locada. Alguem que traz um template, ajusta o texto, e roda de novo para pegar uma correcao
67
+ * perdia o ajuste - sem aviso, sem `--force`, sem uma frase.
68
+ *
69
+ * A MESMA CHAVE PARA OS DOIS LADOS: `templates/<nome>` e' o que a leitura procura e o que a
70
+ * escrita grava, entao o fingerprint de hoje e' lido pela rodada de amanha. Sem `.lock`
71
+ * (template trazido antes do install) nao ha' memoria, e o comportamento de sempre continua.
72
+ */
73
+ const slugDir = join(root, "_synthesisui", "ds", slug);
74
+ const entry = relative(root, pageDir).split(/[\\/]/).join("/");
75
+ const editedHere = await theirEdits(slugDir, entry, pageDir);
76
+ if (editedHere && editedHere.length > 0 && !opts.force) {
77
+ console.log("");
78
+ console.log(keptLine(entry, editedHere,
79
+ /**
80
+ * O RETAKE CARREGA `--out`, senao ele escreve em OUTRO LUGAR.
81
+ *
82
+ * Sem esta parte, quem rodou `template landing --out app/page.tsx` e seguiu a instrucao
83
+ * da recusa escreveria em `templates/landing/page.tsx`: o arquivo que ele editou fica
84
+ * intocado, nasce um segundo, e nada diz que aconteceu. Uma saida que escreve no lugar
85
+ * errado e' pior que uma recusa sem saida.
86
+ */
87
+ `npx synthesisui template ${slug} ${name}${asName ? ` --as ${asName}` : ""}${opts.out ? ` --out ${opts.out}` : ""} --force`));
88
+ return;
89
+ }
31
90
  /**
32
91
  * NENHUMA MATERIALIZAÇÃO VAZA VOCABULÁRIO INTERNO (INV-VOLTA-02) - a mesma porta do
33
92
  * `component`. Uma página inteira saía com `var(--ds-*)` cru enquanto um componente avulso
@@ -39,28 +98,84 @@ export async function template(slug, name, opts) {
39
98
  let named = 0;
40
99
  let inlined = 0;
41
100
  const still = new Set();
42
- await mkdir(pageDir, { recursive: true });
43
- const spokenPage = speak(pageFile.code);
44
- if (spokenPage) {
45
- named += spokenPage.named;
46
- inlined += spokenPage.inlined;
47
- for (const l of spokenPage.left)
101
+ /**
102
+ * TRADUZ TUDO ANTES DE ESCREVER NADA, porque a pergunta *"esta pagina precisa da nossa folha?"*
103
+ * se responde sobre os bytes TRADUZIDOS - e a resposta decide o que vai a disco, nao so' o que o
104
+ * terminal diz.
105
+ *
106
+ * A ordem anterior escrevia e depois media. Funcionava enquanto a medicao servia so' ao fecho no
107
+ * terminal; no momento em que ela passou a decidir uma linha do ARQUIVO, escrever primeiro era
108
+ * gravar a decisao errada e corrigi-la na frase seguinte.
109
+ */
110
+ const pieces = [];
111
+ const translate = (code) => {
112
+ const spoken = speak(code);
113
+ if (!spoken)
114
+ return code;
115
+ named += spoken.named;
116
+ inlined += spoken.inlined;
117
+ for (const l of spoken.left)
48
118
  still.add(l);
119
+ return spoken.css;
120
+ };
121
+ pieces.push({
122
+ rel: pageRel,
123
+ filename: pageRel.split(/[\\/]/).pop() ?? pageFile.filename,
124
+ content: translate(pageFile.code),
125
+ });
126
+ for (const f of siblings)
127
+ pieces.push({
128
+ rel: join(dirname(pageRel), f.filename),
129
+ filename: f.filename,
130
+ content: translate(f.code),
131
+ });
132
+ /**
133
+ * A FOLHA SÓ É COBRADA QUANDO ESTA PÁGINA PRECISA DELA (A2 da etapa 11) - ver
134
+ * `sheet-needed.ts`, a mesma porta que o `component` usa.
135
+ *
136
+ * O DEFEITO, medido em 10/09: o comando já contava em `still` quantas referências sobraram
137
+ * apontando para a nossa folha depois da tradução, imprimia o número, e três linhas abaixo
138
+ * mandava instalar a folha e colar um `@import` - inclusive quando o número era ZERO. Uma página
139
+ * que renderiza igual sem os dois passava a pedi-los, e pedir cria exatamente a dependência que
140
+ * a plataforma promete não criar: a gente é referência, não dependência.
141
+ *
142
+ * E A PERGUNTA É MAIOR QUE `still`, que é por que ela não é feita com ele: a folha entrega as
143
+ * variáveis, as classes que só o `@theme` gera E as classes das receitas (`.ds-hero`). Uma
144
+ * página veste as três, e cortar o setup contando só as variáveis entregaria uma página pelada.
145
+ *
146
+ * SEM TRADUÇÃO, NADA MUDA: sem o mapa do `.lock` o `--ds-*` cru está no arquivo, a folha é o
147
+ * caminho, e a cobrança sai inteira como sempre saiu.
148
+ */
149
+ const need = tongue
150
+ ? whatOnlyTheSheetResolves({
151
+ source: pieces.map((f) => f.content).join("\n"),
152
+ themeCss: await installedThemeCss(root, slug),
153
+ sheetCss: await installedTokensCss(root, slug),
154
+ theirNames: [...tongue.names.values()],
155
+ })
156
+ : null;
157
+ const chargesTheSheet = !need || need.needed;
158
+ await mkdir(pageDir, { recursive: true });
159
+ /** OS BYTES QUE FORAM A DISCO, para o fingerprint lembrar EXATAMENTE o que escrevemos. */
160
+ const landed = [];
161
+ for (const piece of pieces) {
162
+ const content = chargesTheSheet
163
+ ? piece.content
164
+ : withoutTheSheetInstruction(piece.content);
165
+ await writeFile(join(root, piece.rel), content, "utf8");
166
+ landed.push({ filename: piece.filename, content });
167
+ console.log(piece.rel === pageRel
168
+ ? `✓ wrote ${piece.rel} (${slug} v${generated.version})`
169
+ : `✓ wrote ${piece.rel}`);
49
170
  }
50
- await writeFile(join(root, pageRel), spokenPage ? spokenPage.css : pageFile.code, "utf8");
51
- console.log(`✓ wrote ${pageRel} (${slug} v${generated.version})`);
52
- for (const f of siblings) {
53
- const rel = join(dirname(pageRel), f.filename);
54
- const spoken = speak(f.code);
55
- if (spoken) {
56
- named += spoken.named;
57
- inlined += spoken.inlined;
58
- for (const l of spoken.left)
59
- still.add(l);
60
- }
61
- await writeFile(join(root, rel), spoken ? spoken.css : f.code, "utf8");
62
- console.log(`✓ wrote ${rel}`);
63
- }
171
+ /**
172
+ * E O REGISTRO E' GRAVADO - a outra metade da guarda, e sem ela a primeira nunca dispara.
173
+ *
174
+ * A leitura la' em cima procura o fingerprint desta escrita. Ler sem gravar seria uma guarda que
175
+ * passa em todo teste e nunca protege ninguem: a promessa so' existe quando as duas pontas
176
+ * fecham.
177
+ */
178
+ await recordWritten(slugDir, entry, landed);
64
179
  if (named > 0 || inlined > 0) {
65
180
  console.log(` ${named} reference${named === 1 ? "" : "s"} now speak${named === 1 ? "s" : ""} the name YOUR code gives the value${inlined > 0 ? `, and ${inlined} carr${inlined === 1 ? "ies" : "y"} the value because your code names no token for it` : ""}.`);
66
181
  if (still.size > 0) {
@@ -72,8 +187,28 @@ export async function template(slug, name, opts) {
72
187
  console.log("Next steps:");
73
188
  console.log(` • use it in a route, e.g. ${join(config.pagesDir, "page.tsx")}:`);
74
189
  console.log(` import Page from "@/${defaultDir.replace(/\\/g, "/")}/${pageFile.filename.replace(/\.tsx$/, "")}";`);
75
- console.log(` • ensure the DS is installed: synthesisui add ${slug} (provides tokens.css)`);
76
- console.log(` • @import "_synthesisui/ds/${slug}/tokens.css" in your global CSS`);
190
+ if (chargesTheSheet) {
191
+ /** E ELE DIZ O QUE PEDE A FOLHA - um setup cobrado sem motivo dito ensina a ignorar o próximo. */
192
+ if (need)
193
+ console.log(` • what still needs the sheet here: ${[...need.recipeClasses, ...need.classes, ...need.variables].slice(0, 3).join(", ")}`);
194
+ console.log(` • ensure the DS is installed: synthesisui add ${slug} (provides tokens.css)`);
195
+ console.log(` • @import "_synthesisui/ds/${slug}/tokens.css" in your global CSS`);
196
+ }
197
+ else {
198
+ console.log(` • nothing to install: every value in this page is a name YOUR code declares, so it renders without our stylesheet`);
199
+ }
77
200
  console.log(" • refine the file: wire real data, split into components, swap placeholders");
78
- console.log(` • keep the data-ds="${slug}" wrapper and the ds-* / layout classes (stays on-system)`);
201
+ /**
202
+ * E A LINHA DO ESCOPO SÓ SAI ONDE ELA É VERDADE - medido rodando o comando em 10/09.
203
+ *
204
+ * Ela mandava *"keep the data-ds wrapper and the ds-* classes (stays on-system)"* sempre, e no
205
+ * caminho isolado a saída ficava contradizendo a linha logo acima: *"nothing to install… it
206
+ * renders without our stylesheet"* e, uma linha depois, uma instrução para manter um escopo que
207
+ * não resolve nada naquele arquivo. É o mesmo defeito que este bloco existe para consertar - duas
208
+ * frases que não podem ser verdadeiras ao mesmo tempo -, e a medição já responde qual das duas é:
209
+ * chegar aqui com `chargesTheSheet` falso significa zero variável nossa e zero classe nossa no
210
+ * que acabou de ser escrito.
211
+ */
212
+ if (chargesTheSheet)
213
+ console.log(` • keep the data-ds="${slug}" wrapper and the ds-* / layout classes (stays on-system)`);
79
214
  }
@@ -211,6 +211,56 @@ async function theOnlyInstalled(root) {
211
211
  throw new RegistryError("No design system is installed here - run `synthesisui add <slug>` first.");
212
212
  throw new RegistryError(`More than one system is installed here (${slugs.join(", ")}) - name the one to update: \`synthesisui upgrade <slug>\`.`);
213
213
  }
214
+ /**
215
+ * O QUE ELE EDITOU DEPOIS DE NOS ESCREVERMOS - a mesma leitura, num lugar so'.
216
+ *
217
+ * A PROMESSA T7 e' *"kept YOUR file(s)"*, e ela vivia dentro do laco que regenera. Isso amarrou a
218
+ * promessa a UM caminho: o do upgrade que tem versao nova. Medido em 10/09 - com a versao igual,
219
+ * `upgrade` retornava antes de chegar ao laco, e a pessoa que editou tres componentes nao ouvia
220
+ * nada sobre eles no comando cuja funcao e' justamente dizer o que aconteceu com os arquivos dela.
221
+ *
222
+ * Extraida, a resposta e' a mesma nos tres caminhos - rematerializou, ja' estava na ultima, ou
223
+ * subiu de versao. Uma leitura, uma frase.
224
+ */
225
+ async function editedByHand(root, slug, slugDir, componentsDir) {
226
+ const componentsRoot = join(root, componentsDir);
227
+ const marker = `from the "${slug}" design system`;
228
+ const writtenMap = await readWritten(slugDir);
229
+ const out = [];
230
+ for (const entry of await readdir(componentsRoot).catch(() => [])) {
231
+ let head = "";
232
+ try {
233
+ head = (await readFile(join(componentsRoot, entry, `${entry}.tsx`), "utf8")).slice(0, 300);
234
+ }
235
+ catch {
236
+ continue; // not a materialized component folder
237
+ }
238
+ if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
239
+ continue;
240
+ const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
241
+ if (edited && edited.length > 0)
242
+ out.push({ entry, edited });
243
+ }
244
+ return out;
245
+ }
246
+ /**
247
+ * E A FRASE, DITA - nos caminhos que NAO regeneram nada.
248
+ *
249
+ * Aqui ela nao e' o relatorio de uma reescrita que respeitou o arquivo dele: e' a confirmacao de
250
+ * que este comando passou e nao tocou no que e' dele. E' a mesma informacao, e omiti-la nos
251
+ * caminhos silenciosos e' o que fazia a promessa valer so' as vezes.
252
+ */
253
+ async function sayWhatWasKept(root, slug, slugDir) {
254
+ const config = await readProjectConfig(root).catch(() => null);
255
+ if (!config || config.target !== "next")
256
+ return;
257
+ const kept = await editedByHand(root, slug, slugDir, config.componentsDir);
258
+ if (kept.length === 0)
259
+ return;
260
+ console.log("");
261
+ for (const k of kept)
262
+ console.log(body(keptLine(k.entry, k.edited, `npx synthesisui component ${slug} ${k.entry}`)));
263
+ }
214
264
  export async function upgrade(asked, opts) {
215
265
  const base = resolveRegistry(opts.registry);
216
266
  const root = opts.dir ?? process.cwd();
@@ -263,6 +313,8 @@ export async function upgrade(asked, opts) {
263
313
  * medido.
264
314
  */
265
315
  const measured = await checkAfterWriting(root, `v${installed}`);
316
+ /** ESTE CAMINHO REESCREVE OS ARTEFATOS - e o que e' dele fica onde esta'. Ver `sayWhatWasKept`. */
317
+ await sayWhatWasKept(root, slug, slugDir);
266
318
  await reportWhatIsLeft(root, {
267
319
  ...(opts.cli ? { cli: opts.cli } : {}),
268
320
  ...(measured > 0 ? { justMeasured: measured } : {}),
@@ -271,6 +323,12 @@ export async function upgrade(asked, opts) {
271
323
  }
272
324
  if (!opts.force) {
273
325
  console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
326
+ /**
327
+ * E AQUI TAMBEM, que e' o caminho mais percorrido de todos: sem gap de versao, este comando
328
+ * dizia uma linha e sumia. Quem editou tres componentes ficava sem saber se eles seguem sendo
329
+ * dele - no comando que existe para responder isso.
330
+ */
331
+ await sayWhatWasKept(root, slug, slugDir);
274
332
  await reportWhatIsLeft(root, opts.cli ? { cli: opts.cli } : {});
275
333
  return;
276
334
  }
@@ -308,8 +366,9 @@ export async function upgrade(asked, opts) {
308
366
  const regenerated = [];
309
367
  const failed = [];
310
368
  /** Componentes que ELE editou desde que os escrevemos - mantidos intactos (T7). */
311
- const kept = [];
312
- const writtenMap = await readWritten(slugDir);
369
+ const kept = await editedByHand(root, slug, slugDir, config.componentsDir);
370
+ /** Os que ELE editou saem da regeneracao - a leitura acima ja' os separou. */
371
+ const keptEntries = new Set(kept.map((k) => k.entry));
313
372
  if (config.target === "next") {
314
373
  const componentsRoot = join(root, config.componentsDir);
315
374
  const marker = `from the "${slug}" design system`;
@@ -345,16 +404,13 @@ export async function upgrade(asked, opts) {
345
404
  if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
346
405
  continue;
347
406
  /**
348
- * A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `written.ts`. O fingerprint do que NÓS
407
+ * A EDIÇÃO DELE VENCE A REGENERAÇÃO (T7) - ver `editedByHand`. O fingerprint do que NÓS
349
408
  * escrevemos foi gravado no `.lock` na hora da escrita; se o disco divergiu, ele editou, e o
350
409
  * arquivo é DELE - fica intacto e o terminal diz qual. `null` é install de antes do
351
410
  * fingerprint: sem memória não há veredito, e o comportamento de sempre continua.
352
411
  */
353
- const edited = await editedSinceWritten(writtenMap[entry], join(componentsRoot, entry));
354
- if (edited && edited.length > 0) {
355
- kept.push({ entry, edited });
412
+ if (keptEntries.has(entry))
356
413
  continue;
357
- }
358
414
  try {
359
415
  const res = await fetchComponent(base, slug, entry);
360
416
  const { files } = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, flavourOf(res.name),
@@ -460,7 +516,7 @@ export async function upgrade(asked, opts) {
460
516
  if (kept.length > 0) {
461
517
  console.log("");
462
518
  for (const k of kept)
463
- console.log(body(keptLine(slug, k.entry, k.edited)));
519
+ console.log(body(keptLine(k.entry, k.edited, `npx synthesisui component ${slug} ${k.entry}`)));
464
520
  }
465
521
  const measured = await checkAfterWriting(root, `v${latest.version}`);
466
522
  console.log(section("Migrate the app"));
@@ -23,7 +23,7 @@
23
23
  * checagem que telefona para casa é uma checagem que alguém desinstala.
24
24
  */
25
25
  import { isOlderCli } from "../cli-version.js";
26
- import { CHECKER_SINCE } from "../install-marks.js";
26
+ import { COUNTED_DIFFERENTLY, COUNTED_DIFFERENTLY_SINCE, } from "../install-marks.js";
27
27
  const byFileOf = (d) => {
28
28
  const out = {};
29
29
  for (const f of d.files)
@@ -75,10 +75,12 @@ export function compareToBaseline(d, base, now) {
75
75
  * prometia o contrário desde o primeiro dia (*"um número que sobe porque o leitor melhorou não é
76
76
  * regressão"*), e `incomparable` existia para exatamente isto.
77
77
  *
78
- * A RÉGUA É A MARCA QUE JÁ EXISTE: `CHECKER_SINCE` é a última versão em que a leitura que o CI
79
- * compara mudou de resposta, e ela sobe no mesmo PR que muda a leitura. Nada de limiar novo.
78
+ * A RÉGUA É `COUNTED_DIFFERENTLY_SINCE`, em `install-marks.ts`: a última versão em que a CONTAGEM
79
+ * que o CI compara mudou de resposta. Ela mora na casa das outras marcas porque é que está a
80
+ * disciplina de mantê-las - uma marca à mão sem portão é uma marca errada esperando a hora -, e
81
+ * não é a MESMA das outras: emprestar `CHECKER_SINCE` foi exatamente o defeito que a criou.
80
82
  */
81
- const readerMoved = base.cli !== undefined && isOlderCli(base.cli, CHECKER_SINCE);
83
+ const readerMoved = base.cli !== undefined && isOlderCli(base.cli, COUNTED_DIFFERENTLY_SINCE);
82
84
  return {
83
85
  worse: !scopeChanged &&
84
86
  !readerMoved &&
@@ -95,7 +97,7 @@ export function compareToBaseline(d, base, now) {
95
97
  }
96
98
  : readerMoved
97
99
  ? {
98
- incomparable: `the baseline was written by CLI ${base.cli}, and this run counts every length in a shorthand - that version counted only the first. The rise is in the reading, not in your code. Rewrite the line before comparing: npx synthesisui doctor --write-baseline`,
100
+ incomparable: `the baseline was written by CLI ${base.cli}, and ${COUNTED_DIFFERENTLY}. The rise is in the reading, not in your code. Rewrite the line before comparing: npx synthesisui doctor --write-baseline`,
99
101
  }
100
102
  : {}),
101
103
  };
package/dist/index.js CHANGED
@@ -117,6 +117,7 @@ Options:
117
117
  --instruction <s> refit: extra guidance for the adaptation
118
118
  --dry refit: adapt and print, but save nothing
119
119
  --force clean: apply the changes (without it, dry run)
120
+ component/template/generate/refit: overwrite a file YOU edited
120
121
  upgrade: rewrite UPGRADE.md even with no version gap left
121
122
  --from <n> upgrade --force: which older snapshot to diff from
122
123
  --strict doctor: exit 1 when drift is found in THIS repo (for CI)
@@ -155,6 +156,21 @@ Examples:
155
156
  async function main() {
156
157
  const { positionals, flags } = parseFlags(process.argv.slice(2));
157
158
  const [command, ...args] = positionals;
159
+ /**
160
+ * `synthesisui --version` RESPONDE A VERSAO - uma linha, exit 0.
161
+ *
162
+ * O QUE ACONTECIA (medido em 10/09): `node dist/index.js --version` imprimia as 117 linhas do
163
+ * help e saia 0. Quem pergunta a versao de uma ferramenta - uma pessoa num terminal, um script
164
+ * de CI, um relatorio de bug - recebia a lista de comandos, e a unica forma de descobrir a
165
+ * versao era abrir o `package.json` da instalacao.
166
+ *
167
+ * SEM COMANDO, sempre: `--version <n>` continua sendo flag do `add`, do `component` e do
168
+ * `template`, e la ela chega com um comando na frente. A distincao e' posicional, nao de nome.
169
+ */
170
+ if (!command && flags.version !== undefined) {
171
+ console.log(CLI_VERSION);
172
+ return;
173
+ }
158
174
  if (!command || flags.help || command === "help") {
159
175
  /**
160
176
  * A PRIMEIRA LINHA FALA DO PROJETO ONDE ELA FOI RODADA - ver `where-you-are.ts`.
@@ -484,6 +500,8 @@ async function main() {
484
500
  target,
485
501
  version,
486
502
  as: as_,
503
+ /** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
504
+ force: flags.force === true,
487
505
  });
488
506
  break;
489
507
  }
@@ -579,6 +597,8 @@ async function main() {
579
597
  instruction: typeof flags.instruction === "string" ? flags.instruction : undefined,
580
598
  support: typeof flags.support === "string" ? flags.support : undefined,
581
599
  dry: flags.dry === true,
600
+ /** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
601
+ force: flags.force === true,
582
602
  });
583
603
  break;
584
604
  }
@@ -681,7 +701,14 @@ async function main() {
681
701
  }
682
702
  const ds = typeof flags.ds === "string" ? flags.ds : undefined;
683
703
  const name = typeof flags.name === "string" ? flags.name : undefined;
684
- await generate(description, { registry, dir, ds, name });
704
+ await generate(description, {
705
+ registry,
706
+ dir,
707
+ ds,
708
+ name,
709
+ /** A saida da guarda T7: escrever por cima do que ELE editou e' escolha dele. */
710
+ force: flags.force === true,
711
+ });
685
712
  break;
686
713
  }
687
714
  default:
@@ -170,7 +170,24 @@
170
170
  * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
171
171
  * sistema, em vez de só receber as regras e adivinhar o resto.
172
172
  */
173
- export const MATERIALISER_SINCE = "0.16.403";
173
+ /**
174
+ * 0.16.403 -> 0.16.412 em 10/09, e o SIM é sobre TRÊS coisas que caem na pasta dele:
175
+ *
176
+ * `component` sem `add` sem `.lock`, o vocabulário do repositório dele passa a ser derivado
177
+ * em memória do sistema lido no registry. O `.tsx` que o CLI de ontem
178
+ * escrevia com `var(--ds-*)` cru sai com o nome que o código DELE dá
179
+ * àquele valor - e a nossa folha deixa de ser pré-requisito para
180
+ * trazer um componente.
181
+ * `template` com T7 a página materializada grava o fingerprint no `.lock` e passa a
182
+ * recusar a reescrita do que ele editou à mão. Campo novo no arquivo
183
+ * que o time dele commita.
184
+ * `generate` e `refit` gravavam o fingerprint e nunca o liam; agora recusam por edição
185
+ * dele, então o conjunto de bytes que uma rodada deixa é outro.
186
+ *
187
+ * O que o cliente ganha ao rodar `upgrade`: os componentes que ele já tem voltam a ser reescritos
188
+ * pelo materializador que respeita a edição dele em TODO comando, e não só em dois.
189
+ */
190
+ export const MATERIALISER_SINCE = "0.16.412";
174
191
  /**
175
192
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
176
193
  *
@@ -258,7 +275,37 @@ export const MATERIALISER_SINCE = "0.16.403";
258
275
  * não via, 14% e 16% das declarações de espaço/raio. Um hook pinado antes desta versão aconselha
259
276
  * MENOS, que é a régua desta marca.
260
277
  */
261
- export const CHECKER_SINCE = "0.16.408";
278
+ /**
279
+ * A ÚLTIMA VERSÃO EM QUE A CONTAGEM QUE O CI COMPARA MUDOU DE RESPOSTA - a régua da catraca, e ela
280
+ * mora aqui porque é onde a disciplina mora.
281
+ *
282
+ * ELA ERA `CHECKER_SINCE`, e as duas perguntas não são a mesma. Aquela significa *"o que o hook
283
+ * ENTREGA mudou"*, e sobe também quando ele passa a DIZER algo novo sobre os mesmos achados - o que
284
+ * aconteceu em 10/09. Com a régua emprestada, aquela subida declararia INCOMPARÁVEL todo baseline
285
+ * gravado entre as duas versões: a catraca pararia de reprovar regressão de verdade até o time
286
+ * regravar a linha, e a frase abaixo diria a ele um motivo que não é o dele. Foi o caso negativo do
287
+ * `doctor/ci-format.spec.ts` que acendeu isso, sozinho.
288
+ *
289
+ * A PERGUNTA DO PASSO 1 GANHA UMA SEGUNDA METADE por causa dela, e as duas se fazem no mesmo
290
+ * vermelho do fingerprint do CHECKER (que já inclui `doctor/scan.ts`, quem conta):
291
+ *
292
+ * o hook aconselha diferente? -> `CHECKER_SINCE`
293
+ * a CONTAGEM mudou de valor? -> `COUNTED_DIFFERENTLY_SINCE`, e a FRASE junto
294
+ *
295
+ * A frase viaja ao lado da versão porque as duas são uma coisa só: quem move a marca troca a
296
+ * explicação no mesmo lugar, em vez de deixar o CI citando a causa da marca anterior.
297
+ */
298
+ export const COUNTED_DIFFERENTLY_SINCE = "0.16.408";
299
+ export const COUNTED_DIFFERENTLY = "this run counts every length in a shorthand - that version counted only the first";
300
+ /**
301
+ * 0.16.408 -> 0.16.413 em 10/09: o hook passa a DIZER o valor que nada nomeia. Ele silenciava toda
302
+ * deriva sem token de destino - a decisão estava escrita como "não vale interromper, porque o único
303
+ * conselho honesto é pergunte a uma pessoa" -, e o silêncio é a única resposta errada disponível:
304
+ * quem lê um relatório que só fala de valores nomeáveis não distingue "não sobrou nada" de "sobrou,
305
+ * e nós engolimos". Agora sai `line 12 #ff00aa`, sem uma única proposta de nome nosso. Um hook
306
+ * pinado antes desta versão aconselha MENOS, que é a régua desta marca.
307
+ */
308
+ export const CHECKER_SINCE = "0.16.413";
262
309
  /**
263
310
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
264
311
  *
@@ -0,0 +1,78 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * ESTA PASTA E' UM PROJETO? - a pergunta que todo comando que ESCREVE no repositorio dele
6
+ * deveria fazer antes da primeira escrita.
7
+ *
8
+ * O QUE O CLIENTE VIVIA: `connect` rodado por engano no `~` ou num diretorio recem-criado
9
+ * escrevia `.claude/settings.json`, `CLAUDE.md`, `.mcp.json` e a pasta de governanca ali mesmo -
10
+ * seis escritas, medidas em 10/09, nenhuma delas precedida de uma checagem. Ninguem percebe no
11
+ * dia; percebe semanas depois, quando um `CLAUDE.md` orfao na home passa a instruir todo agente
12
+ * que abre naquela pasta.
13
+ *
14
+ * O CRITERIO E' O DO ECOSSISTEMA, nao um nosso: um `package.json` nesta pasta ou acima dela. A
15
+ * subida para no diretorio que carrega o `.git`, porque a raiz do repositorio e' onde o projeto
16
+ * acaba - continuar subindo alcancaria a home de quem roda, que e' exatamente o acidente que isto
17
+ * existe para recusar.
18
+ *
19
+ * GENERALIDADE: nao pergunta por Next, por React nem pela nossa pasta. Um projeto que ainda nao
20
+ * instalou nada e' um projeto; uma pasta vazia dentro de um repositorio Node tambem e', porque o
21
+ * manifesto de cima responde por ela.
22
+ */
23
+ export async function projectRootFrom(dir) {
24
+ /**
25
+ * DENTRO DE UMA DEPENDENCIA NAO E' O PROJETO DELE - e' o projeto de outra pessoa, baixado.
26
+ *
27
+ * Um `cd` de distancia do acidente: `node_modules/<lib>` tem `package.json`, entao a subida
28
+ * parava ali e o comando escrevia a fiacao do agente dentro de uma dependencia - que some no
29
+ * proximo `npm ci`, levando junto tudo que ele configurou.
30
+ */
31
+ if (dir.split(/[\\/]/).includes("node_modules"))
32
+ return null;
33
+ /**
34
+ * E A HOME E' O SEGUNDO TETO, ao lado da raiz do repositorio.
35
+ *
36
+ * O `.git` sozinho nao fechava o acidente que este modulo existe para recusar: numa maquina
37
+ * onde alguem rodou `npm init` na home - acontece -, uma pasta solta em `~/qualquer-coisa` sem
38
+ * git subia ate' `~`, encontrava aquele manifesto, e o comando escrevia `CLAUDE.md`,
39
+ * `.mcp.json` e a casa da governanca na HOME da pessoa.
40
+ *
41
+ * A home nunca e' "o projeto dele", tenha ela manifesto ou nao.
42
+ */
43
+ const home = homedir();
44
+ let at = dir;
45
+ for (;;) {
46
+ if (at === home)
47
+ return null;
48
+ if (await exists(join(at, "package.json")))
49
+ return at;
50
+ /** A raiz do repositorio e' o teto: acima dela nao existe "o projeto dele". */
51
+ if (await exists(join(at, ".git")))
52
+ return null;
53
+ const up = dirname(at);
54
+ if (up === at)
55
+ return null;
56
+ at = up;
57
+ }
58
+ }
59
+ async function exists(path) {
60
+ return stat(path).then(() => true, () => false);
61
+ }
62
+ /**
63
+ * A RECUSA, COM O MOTIVO E O QUE FAZER - nunca so' "nao posso".
64
+ *
65
+ * E ELA DIZ O QUE MEDIMOS, nao o que a pasta e'. A primeira redacao decretava
66
+ * *"<dir> is not a project"* e oferecia `npm init -y`: num repositorio Deno ou Rust a primeira
67
+ * frase e' falsa - aquilo E' um projeto - e a segunda manda sujar o repositorio dele com o
68
+ * manifesto de um ecossistema que ele nao usa. Representar o limite do leitor e' diferente de
69
+ * negar o que ele tem.
70
+ */
71
+ export function notAProject(command, dir) {
72
+ return [
73
+ `No package.json in ${dir}, or in any folder above it up to the repository root - so this reader cannot tell which project it is looking at.`,
74
+ `\`${command}\` writes agent files into a project - a hook, an MCP entry, CLAUDE.md - so it stops before writing anything.`,
75
+ `Point it at the project: npx synthesisui ${command} --dir <path/to/project>`,
76
+ `(it looks for a package.json - that is what this reader knows how to place files in)`,
77
+ ];
78
+ }
@@ -1,6 +1,74 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { TAILWIND_THEME_VARS } from "./tailwind-theme-vars.js";
4
+ /**
5
+ * AS CLASSES QUE A FOLHA COMPILADA DECLARA COMO RECEITA - o nome, não o prefixo.
6
+ *
7
+ * A convenção viaja com o sistema (`ds-`, `sui-`, o que o projeto dele usa), então perguntar pelo
8
+ * prefixo seria fixar a forma de um repositório. O que é estrutural é onde a regra MORA: o
9
+ * compilador emite uma regra por receita dentro de `@layer components`, e é só essa camada que a
10
+ * folha traz e o projeto dele não tem.
11
+ *
12
+ * A PRIMEIRA VERSÃO VARRIA O TEXTO INTEIRO, e a revisão do fecho mediu o preço na folha viva do
13
+ * `codelevel` (143.409 bytes, versão instalada): **147 nomes extraídos, 3 deles não são receita** -
14
+ * `dark`, que é a classe DELE em `@layer base`; `layer-3d`, um utilitário DELE espelhado em
15
+ * `@layer utilities`; e `background`, que aparecia como VALOR de uma declaração e nem seletor era.
16
+ * Rodada sobre o repositório dele, a função devolvia `["layer-3d", "dark"]` - duas classes DELE
17
+ * sustentando a cobrança da NOSSA folha. É o defeito de 07/09 voltando pela porta do balde novo, e
18
+ * o oposto exato do que esta medição existe para permitir.
19
+ *
20
+ * O ESCAPE CONTA como parte do nome, porque no CSS ele é como se escreve um caractere que o
21
+ * seletor não aceita cru: `.ds-w-\[10px\]` é a classe `ds-w-[10px]`, e `.sm\:ds-hero` é a variante
22
+ * `sm:` da classe `ds-hero` - que é como o código dele a escreve. Truncar no `\` produziria os
23
+ * nomes `ds-w-` e `sm`, que ninguém escreve: uma dependência real deixaria de ser contada, e esse é
24
+ * o único lado do erro que o cabeçalho deste módulo declara inaceitável.
25
+ */
26
+ export function classesTheSheetDeclares(css) {
27
+ const out = new Set();
28
+ for (const body of layerBodies(css, "components"))
29
+ for (const prelude of selectorPreludes(body))
30
+ for (const m of prelude.matchAll(/\.((?:\\.|[\w-])+)/g)) {
31
+ const bare = m[1].replace(/\\(.)/g, "$1");
32
+ /** A MESMA normalização que `classesIn` aplica ao que ele ESCREVE - `sm:ds-hero` é `ds-hero`. */
33
+ out.add((bare.split(":").pop() ?? bare).replace(/^[!-]/, ""));
34
+ }
35
+ return [...out];
36
+ }
37
+ /** O corpo de cada `@layer <nome> { … }`, com as chaves de dentro contadas. */
38
+ function layerBodies(css, name) {
39
+ const out = [];
40
+ for (const m of css.matchAll(new RegExp(`@layer\\s+${name}\\s*\\{`, "g"))) {
41
+ let depth = 1;
42
+ let i = (m.index ?? 0) + m[0].length;
43
+ const start = i;
44
+ for (; i < css.length && depth > 0; i += 1) {
45
+ const c = css[i];
46
+ if (c === "{")
47
+ depth += 1;
48
+ else if (c === "}")
49
+ depth -= 1;
50
+ }
51
+ out.push(css.slice(start, i - 1));
52
+ }
53
+ return out;
54
+ }
55
+ /**
56
+ * O PRELÚDIO DE CADA REGRA - o texto entre o fim da regra anterior e a `{` desta.
57
+ *
58
+ * É o que separa um SELETOR de um valor de declaração: `background: p.background` mora depois da
59
+ * `{`, e nunca chega aqui. Um prelúdio que começa com `@` é uma regra de agrupamento (`@media`,
60
+ * `@supports`) e não declara classe nenhuma - o corpo dela é varrido pela mesma volta do laço.
61
+ */
62
+ function selectorPreludes(body) {
63
+ const out = [];
64
+ const parts = body.split("{");
65
+ for (const part of parts.slice(0, -1)) {
66
+ const tail = part.slice(part.lastIndexOf("}") + 1).trim();
67
+ if (tail && !tail.startsWith("@"))
68
+ out.push(tail);
69
+ }
70
+ return out;
71
+ }
4
72
  /** `--animate-shimmer` no `@theme` -> o nome `shimmer`, que é o que uma classe carrega. */
5
73
  function themeNames(themeCss) {
6
74
  const out = new Set();
@@ -207,14 +275,33 @@ export function whatOnlyTheSheetResolves(input) {
207
275
  const classes = [
208
276
  ...new Set([...written].filter((c) => names.some((n) => c === n || c.endsWith(`-${n}`)))),
209
277
  ];
278
+ /** As classes que a folha declara e que ESTE texto veste - ver `recipeClasses`. */
279
+ const declared = new Set(classesTheSheetDeclares(input.sheetCss));
280
+ const recipeClasses = [
281
+ ...new Set([...written].filter((c) => declared.has(c))),
282
+ ];
210
283
  return {
211
284
  variables,
212
285
  classes,
213
- needed: variables.length > 0 || classes.length > 0,
286
+ recipeClasses,
287
+ needed: variables.length > 0 || classes.length > 0 || recipeClasses.length > 0,
214
288
  };
215
289
  }
290
+ /** A folha compilada da versão instalada - as receitas moram nela, não no `theme.css`. */
291
+ export async function installedTokensCss(root, slug) {
292
+ return installedSheet(root, slug, "tokens.css");
293
+ }
216
294
  /** O `theme.css` da versão instalada - a folha da raiz é um re-export de uma linha. */
217
295
  export async function installedThemeCss(root, slug) {
296
+ return installedSheet(root, slug, "theme.css");
297
+ }
298
+ /**
299
+ * UM ARQUIVO DA VERSÃO PINADA - o `.lock` diz qual é, e a folha da raiz é um re-export de uma linha.
300
+ *
301
+ * Uma leitura só para as duas folhas: duas cópias da mesma decisão de versão é a forma de uma delas
302
+ * ficar atrás no dia em que o formato do `.lock` mudar.
303
+ */
304
+ async function installedSheet(root, slug, filename) {
218
305
  const dir = join(root, "_synthesisui", "ds", slug);
219
306
  const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
220
307
  let version = 0;
@@ -226,5 +313,5 @@ export async function installedThemeCss(root, slug) {
226
313
  version = 0;
227
314
  }
228
315
  }
229
- return readFile(join(dir, `v${version}`, "theme.css"), "utf8").catch(() => "");
316
+ return readFile(join(dir, `v${version}`, filename), "utf8").catch(() => "");
230
317
  }
@@ -50,6 +50,33 @@ export function inTheirTongue(css, tongue, destination = "stylesheet") {
50
50
  });
51
51
  return { css: out, named, inlined, left: [...left].sort() };
52
52
  }
53
+ /**
54
+ * O VOCABULARIO DESTE REPOSITORIO **SEM O `add` TER RODADO** - a folha deixa de ser pre-requisito.
55
+ *
56
+ * O QUE ISTO MUDA PARA O CLIENTE: ele pede UM componente e recebe um arquivo que ja' fala a
57
+ * lingua do codigo dele - `var(--color-ink-900)`, o nome que ELE deu - sem antes materializar as
58
+ * 1813 linhas, 111 variaveis e 628 referencias que o `add` escreve no repositorio (medido em
59
+ * 10/09). A nossa folha e' a REFERENCIA que o agente dele le'; exigi-la para trazer um botao
60
+ * invertia isso, e contrariava a definicao de produto: nada nosso no repositorio dele.
61
+ *
62
+ * A MEDICAO E' A MESMA DO `add`, so' que em memoria: `pointTokensAtTheirNames` varre as folhas
63
+ * DELE e o build dele, e responde quais das nossas variaveis tem nome dele. A diferenca e' que
64
+ * nada disso vai para o disco - nem o mapa, nem a folha.
65
+ *
66
+ * `null` quando o repositorio dele nao nomeia nada do que este sistema declara. Ai' a folha
67
+ * continua sendo o caminho, e quem chama DIZ isso - em vez de escrever `var(--ds-*)` cru e
68
+ * deixar a pessoa descobrir por que a cor nao apareceu.
69
+ */
70
+ export async function tongueFromArtifacts(root, artifacts) {
71
+ const tokens = artifacts["tokens.css"] ?? "";
72
+ if (!tokens)
73
+ return null;
74
+ const { pointTokensAtTheirNames } = await import("./their-vars.js");
75
+ const { pairs } = await pointTokensAtTheirNames(root, { artifacts });
76
+ if (pairs.length === 0)
77
+ return null;
78
+ return tongueOf(pairs, tokens);
79
+ }
53
80
  /**
54
81
  * O VOCABULÁRIO DESTE REPOSITÓRIO, montado do que o `add` já deixou na pasta.
55
82
  *
@@ -36,6 +36,25 @@ import { detectAppDirs, sheetChainOf } from "./global-sheet.js";
36
36
  *
37
37
  * The system itself was always resolved from the root. So is this now.
38
38
  */
39
+ /**
40
+ * O QUE NOS ESCREVEMOS NAO E' FIACAO DELE - e isto era um FALSO SILENCIO, o pior dos dois.
41
+ *
42
+ * MEDIDO EM 10/09: o cabecalho que `component-codegen.ts` poe em todo `.tsx` materializado diz,
43
+ * em comentario, *"Global setup (once per app): import _synthesisui/ds/<slug>/tokens.css"* e
44
+ * *"put data-ds=<slug> on a root element"*. A varredura abaixo procura exatamente essas duas
45
+ * strings em QUALQUER arquivo do repositorio - entao, a partir do primeiro componente que nos
46
+ * mesmos escrevemos, ela passava a responder `imported: true` e `scoped: true` sobre um projeto
47
+ * que nao importa nada e nao tem escopo nenhum.
48
+ *
49
+ * O QUE O CLIENTE VIVIA: ele traz um componente, a tela diz *"This project already imports
50
+ * <slug>'s tokens.css and carries data-ds - nothing to set up"*, ele cola o componente na pagina,
51
+ * e ve' um bloco sem cor, sem tipografia e sem sombra. O comando acabou de garantir que estava
52
+ * tudo certo. O `doctor` le' pela mesma porta, entao dizia o mesmo.
53
+ *
54
+ * A instrucao que ESTE arquivo carrega e' uma instrucao, nao um cumprimento dela. Um comentario
55
+ * nosso nunca prova nada sobre o projeto dele.
56
+ */
57
+ const OURS = "Generated by SynthesisUI";
39
58
  export async function readWiring(root, slug) {
40
59
  const w = {
41
60
  imported: false,
@@ -84,6 +103,9 @@ export async function readWiring(root, slug) {
84
103
  const src = await readFile(file, "utf8").catch(() => "");
85
104
  if (!src)
86
105
  continue;
106
+ /** Ver `OURS`: o nosso proprio cabecalho respondia por ele. */
107
+ if (src.slice(0, 300).includes(OURS))
108
+ continue;
87
109
  if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
88
110
  w.imported = true;
89
111
  if (src.includes(`_synthesisui/ds/${slug}/theme.css`))
package/dist/written.js CHANGED
@@ -60,11 +60,33 @@ export async function editedSinceWritten(recorded, dir) {
60
60
  return edited;
61
61
  }
62
62
  /**
63
- * A FRASE DO TERMINAL, uma por componente mantido - o critério do T7 é a prova VISÍVEL: o dono
64
- * fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato. A
65
- * saída oferecida regenera por ESCOLHA (o `component` reescreve e regrava o fingerprint).
63
+ * A PERGUNTA, NUM LUGAR SO': *ele editou o que escrevemos aqui?*
64
+ *
65
+ * As duas linhas - ler o registro do `.lock`, comparar com o disco - estavam copiadas em cada
66
+ * comando que se lembrou de perguntar, e ausentes nos que nao se lembraram. Medido em 10/09:
67
+ * `component` 6 guardas, `upgrade` 4, `generate` e `refit` gravavam o fingerprint e nunca o liam,
68
+ * `template` nao fazia nem uma coisa nem outra.
69
+ *
70
+ * Uma porta so' torna a omissao visivel: um comando de materializacao que nao a chama aparece no
71
+ * portao de `every-writer-keeps-his-edits.spec.ts`, em vez de aparecer no repositorio de alguem.
72
+ *
73
+ * `null` e' install legado (sem fingerprint gravado) - sem memoria nao ha' veredito, e quem chama
74
+ * mantem o comportamento de sempre.
75
+ */
76
+ export async function editedHere(slugDir, entry, dir) {
77
+ return editedSinceWritten((await readWritten(slugDir))[entry], dir);
78
+ }
79
+ /**
80
+ * A FRASE DO TERMINAL, uma por arquivo mantido - o critério do T7 é a prova VISÍVEL: o dono
81
+ * fica sabendo na hora, com o arquivo nomeado e a saída dita, não num diff depois do fato.
82
+ *
83
+ * `retake` E' OBRIGATORIO, e é o comando que RETOMA a versão nova. Ele era fixo em
84
+ * `component <slug> <entry>`, o que estava certo enquanto só um comando falava esta frase: no
85
+ * `template` aquela linha mandaria a pessoa rodar o comando errado, e uma saída que não funciona
86
+ * é pior que nenhuma. Obrigatório em vez de opcional porque um default aqui volta a ser o
87
+ * comando de outro comando, calado.
66
88
  */
67
- export function keptLine(slug, entry, edited) {
89
+ export function keptLine(entry, edited, retake) {
68
90
  return (`! ${entry}: you edited ${edited.join(", ")} after we wrote it - kept YOUR file(s).\n` +
69
- ` To take the new version anyway: npx synthesisui component ${slug} ${entry}`);
91
+ ` To take the new version anyway: ${retake}`);
70
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.411",
3
+ "version": "0.16.413",
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": {