synthesisui 0.16.412 → 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.
@@ -536,6 +536,17 @@ export async function component(slug, name, opts) {
536
536
  (await installedThemeCss(root, slug)),
537
537
  /** O vocabulário DELE sai da conta - ver `theirNames`. O mapa do `.lock` é a via mais barata. */
538
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: "",
539
550
  });
540
551
  if (!need.needed) {
541
552
  console.log(section("This file needs nothing else"));
@@ -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`.
@@ -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
  }
@@ -2,8 +2,36 @@ import { mkdir, writeFile } from "node:fs/promises";
2
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";
6
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
+ }
7
35
  /**
8
36
  * Materializes a whole page from a DS template into the project (hybrid
9
37
  * codegen-first): the server codegens deterministic files, we write them, and
@@ -70,36 +98,75 @@ export async function template(slug, name, opts) {
70
98
  let named = 0;
71
99
  let inlined = 0;
72
100
  const still = new Set();
73
- await mkdir(pageDir, { recursive: true });
74
- /** OS BYTES QUE FORAM A DISCO, para o fingerprint lembrar EXATAMENTE o que escrevemos. */
75
- const landed = [];
76
- const spokenPage = speak(pageFile.code);
77
- if (spokenPage) {
78
- named += spokenPage.named;
79
- inlined += spokenPage.inlined;
80
- 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)
81
118
  still.add(l);
82
- }
83
- const pageContent = spokenPage ? spokenPage.css : pageFile.code;
84
- await writeFile(join(root, pageRel), pageContent, "utf8");
85
- landed.push({
119
+ return spoken.css;
120
+ };
121
+ pieces.push({
122
+ rel: pageRel,
86
123
  filename: pageRel.split(/[\\/]/).pop() ?? pageFile.filename,
87
- content: pageContent,
124
+ content: translate(pageFile.code),
88
125
  });
89
- console.log(`✓ wrote ${pageRel} (${slug} v${generated.version})`);
90
- for (const f of siblings) {
91
- const rel = join(dirname(pageRel), f.filename);
92
- const spoken = speak(f.code);
93
- if (spoken) {
94
- named += spoken.named;
95
- inlined += spoken.inlined;
96
- for (const l of spoken.left)
97
- still.add(l);
98
- }
99
- const content = spoken ? spoken.css : f.code;
100
- await writeFile(join(root, rel), content, "utf8");
101
- landed.push({ filename: f.filename, content });
102
- console.log(`✓ wrote ${rel}`);
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}`);
103
170
  }
104
171
  /**
105
172
  * E O REGISTRO E' GRAVADO - a outra metade da guarda, e sem ela a primeira nunca dispara.
@@ -120,8 +187,28 @@ export async function template(slug, name, opts) {
120
187
  console.log("Next steps:");
121
188
  console.log(` • use it in a route, e.g. ${join(config.pagesDir, "page.tsx")}:`);
122
189
  console.log(` import Page from "@/${defaultDir.replace(/\\/g, "/")}/${pageFile.filename.replace(/\.tsx$/, "")}";`);
123
- console.log(` • ensure the DS is installed: synthesisui add ${slug} (provides tokens.css)`);
124
- 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
+ }
125
200
  console.log(" • refine the file: wire real data, split into components, swap placeholders");
126
- 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)`);
127
214
  }
@@ -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
  };
@@ -275,7 +275,37 @@ export const MATERIALISER_SINCE = "0.16.412";
275
275
  * não via, 14% e 16% das declarações de espaço/raio. Um hook pinado antes desta versão aconselha
276
276
  * MENOS, que é a régua desta marca.
277
277
  */
278
- 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";
279
309
  /**
280
310
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
281
311
  *
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.412",
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": {