synthesisui 0.16.269 → 0.16.271

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/claude-md.js CHANGED
@@ -312,8 +312,35 @@ async function readInterfaceLanguage(projectRoot) {
312
312
  return null;
313
313
  }
314
314
  async function renderRegion(projectRoot, installed) {
315
+ /**
316
+ * AINDA NÃO EXISTE SISTEMA, E O BLOCO PASSA A DIZER ISSO - antes ele saía vazio.
317
+ *
318
+ * Medido no repo do dono (21/08): 52 bytes, só o par de marcadores. A informação existia por três
319
+ * canais e os três dependem de execução - as skills e o MCP exigem REABRIR a sessão, e o
320
+ * `SessionStart` exige `npx ... align` rodar. O CLAUDE.md é o único que não depende de nada, e era
321
+ * justamente o calado: se o SessionStart falhar, o agente abre sem saber que há algo a fazer aqui.
322
+ *
323
+ * UMA LINHA, NÃO UM MANUAL, e nada sobre token ou componente: um bloco que falasse de
324
+ * `--ds-color-*` num repo sem sistema mandaria o agente escrever refs que não resolvem. O erro
325
+ * oposto é pior que o silêncio.
326
+ */
315
327
  if (installed.length === 0) {
316
- return `${START}\n${END}`;
328
+ return [
329
+ START,
330
+ "## Design system",
331
+ "",
332
+ "This project is wired to synthesisui and has **no design system yet** - so there is",
333
+ "no token vocabulary to follow, and nothing here to obey.",
334
+ "",
335
+ "To turn this repository into one, run `/sui-import-ds` and I will read what is",
336
+ "already in the code: the colours, the type, the shapes and the components. Nothing",
337
+ "is invented, and nothing is copied out.",
338
+ "",
339
+ "The `synthesisui` MCP server is registered, so ASK it instead of guessing - it",
340
+ "answers what exists and what does not. This block fills itself in once a system is",
341
+ "installed.",
342
+ END,
343
+ ].join("\n");
317
344
  }
318
345
  /**
319
346
  * A CAPACIDADE, MEDIDA UMA VEZ POR ARQUIVO - ver `recallAvailable`.
@@ -41,6 +41,27 @@ const IGNORED = [
41
41
  const IGNORE_HEADER = "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
42
42
  "# clone is governed; the measurement and the local record are not, because\n" +
43
43
  "# both are rewritten in full on every run and would conflict on every merge.\n";
44
+ /**
45
+ * A CASA DO REGISTRO LOCAL, ABERTA ANTES DE ALGUÉM ESCREVER NELA.
46
+ *
47
+ * Medido no repo do dono em 21/08, depois de apagar tudo e rodar só o `connect`: `.claude/`,
48
+ * `.mcp.json` e `CLAUDE.md` nasceram, e `_synthesisui/` não. `appendEvent` grava com `appendFile` e
49
+ * nunca cria diretório - sem a pasta é `ENOENT`, e o `catch` dele engole em silêncio. Então TODO
50
+ * evento entre o `connect` e o primeiro `import` se perdia: o `run` do próprio connect, e cada check
51
+ * do hook depois de cada escrita.
52
+ *
53
+ * POR QUE AQUI E NÃO NO `appendEvent`: fazer o gravador criar a pasta faria um `npx synthesisui list`
54
+ * de curiosidade plantar `_synthesisui/` no repo de quem só estava olhando - e aquele `catch` existe
55
+ * exatamente para esse caso. O `connect` é o comando que INSTALA, e é o único lugar onde abrir a casa
56
+ * é uma decisão que a pessoa tomou.
57
+ *
58
+ * A casa nasce com o `.gitignore`, senão o primeiro `ledger.jsonl` aparece no `git status` dele antes
59
+ * de o `add` rodar - a diferença entre um registro local e um arquivo que ele commita por engano.
60
+ */
61
+ export async function ensureGovernanceHome(projectRoot) {
62
+ await mkdir(join(projectRoot, "_synthesisui"), { recursive: true });
63
+ await writeGovernanceIgnore(projectRoot);
64
+ }
44
65
  async function writeGovernanceIgnore(projectRoot) {
45
66
  const path = join(projectRoot, "_synthesisui", ".gitignore");
46
67
  const existing = await readFile(path, "utf8").catch(() => null);
@@ -7,7 +7,7 @@ import { body, paint, section, snippet } from "../output.js";
7
7
  import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
8
8
  import { existingRc, hasHook, pinnedInHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
9
9
  import { SKILLS } from "../skills.js";
10
- import { add } from "./add.js";
10
+ import { add, ensureGovernanceHome } from "./add.js";
11
11
  import { reportWhatIsLeft } from "./align.js";
12
12
  import { ci } from "./ci.js";
13
13
  import { MCP_TOOL_COUNT } from "./mcp.js";
@@ -209,6 +209,14 @@ export async function connect(opts) {
209
209
  * `syncClaudeMd` abaixo tem que ser o último a falar sobre ele.
210
210
  */
211
211
  const refreshed = await refreshInstall(root, opts.version, opts.registry).catch(() => null);
212
+ /**
213
+ * A CASA DO REGISTRO LOCAL, ANTES DA PRIMEIRA ESCRITA - ver `ensureGovernanceHome`.
214
+ *
215
+ * Aqui e não depois: o `wireAgent` já é um comando que roda, e o `run` desta própria execução é
216
+ * gravado no fim pelo wrapper do `index.ts`. Sem a pasta ele cai num `catch` silencioso, junto com
217
+ * todo check do hook até o primeiro `import` - medido no repo do dono em 21/08.
218
+ */
219
+ await ensureGovernanceHome(root);
212
220
  const wired = await wireAgent(root, opts.version, want);
213
221
  // The block reads the settings we just wrote, so it must be regenerated
214
222
  // after them, not before.
@@ -299,9 +307,20 @@ export async function connect(opts) {
299
307
  const moved = contract.changed.includes(home);
300
308
  console.log(body(`${moved ? "✓" : "·"} ${home.padEnd(22)} ${moved
301
309
  ? home === "CLAUDE.md"
302
- ? "rewritten for what is installed"
310
+ ? /**
311
+ * A FRASE DIZ O QUE O BLOCO CARREGA, e ela descrevia um conteúdo que não existia.
312
+ *
313
+ * Num repo sem sistema o bloco saía vazio e esta linha dizia "rewritten for what is
314
+ * installed" - nada estava instalado, e nada foi escrito. Agora o bloco diz o que
315
+ * fazer, e a linha diz qual dos dois ele é (dono, 21/08).
316
+ */
317
+ contract.count === 0
318
+ ? "how to turn this repo into your system"
319
+ : "rewritten for what is installed"
303
320
  : "the same rules, where this agent reads them"
304
- : "already says what is installed"}`));
321
+ : contract.count === 0
322
+ ? "already says how to start"
323
+ : "already says what is installed"}`));
305
324
  }
306
325
  /**
307
326
  * The fourth layer, and the one that had no installer at all: the import
@@ -17,6 +17,7 @@ import { countShape, describeCoverage, summarizeCoverage, } from "../doctor/cove
17
17
  import { classifyAside, crosswalk, floorSize, isLibrary, observedRules, useLiveCatalogue, } from "../doctor/crosswalk.js";
18
18
  import { keyframeOffsets, moduleImports, partialCandidates, readGlobalClasses, readModuleCss, readModuleUsage, sheetImports, transcribeModule, } from "../doctor/css-modules.js";
19
19
  import { dataContract } from "../doctor/data-contract.js";
20
+ import { inherit, readDeclaredForms, } from "../doctor/declared-forms.js";
20
21
  import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
21
22
  import { fragmentsOfSource, fragmentsOfStylesheet, judgeFragments, } from "../doctor/fragments.js";
22
23
  import { describeConvention, describeRemainder, detectConventions, } from "../doctor/idiom.js";
@@ -35,6 +36,7 @@ import { buildTable } from "../doctor/tokens.js";
35
36
  import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
36
37
  import { transcribeVariants } from "../doctor/variant-read.js";
37
38
  import { frontierKind, packageRoot } from "../frontier-kind.js";
39
+ import { keyframesInSheets } from "../global-keyframes.js";
38
40
  import { withLibraryStructure } from "../library-structure.js";
39
41
  import { mergeCensus } from "../merge-census.js";
40
42
  import { claimName } from "../name-claim.js";
@@ -435,6 +437,18 @@ export async function takeCensus(root, opts) {
435
437
  * porque é isso que o juiz do ledger precisa para chamar a classe e a regra de LIDAS.
436
438
  */
437
439
  const globalClasses = readGlobalClasses(globalSheets, declaredValues);
440
+ /**
441
+ * OS KEYFRAMES DA FOLHA DELE, colhidos onde as folhas globais já estão na mão.
442
+ *
443
+ * Eram 35 dos 84 tokens declarados do codelevel - 42% do vocabulário - e o censo saía com
444
+ * `keyframes: undefined`, porque o leitor só olhava CSS Module e `<style>` island. Ver
445
+ * `global-keyframes.ts` para o porquê e para o que o valor de cada `--animate-*` carrega.
446
+ *
447
+ * Aqui e não dentro de `harvestOwnCss` porque aquela função é o laço de COLHEITA, e esta é uma
448
+ * leitura sobre o que ela colheu - a mesma divisão que `readGlobalClasses` já respeita, uma linha
449
+ * acima.
450
+ */
451
+ const globalKeyframes = keyframesInSheets(globalSheets);
438
452
  const globalClassesClaimed = new Set();
439
453
  /** Por arquivo, as classes cuja regra a `<style>` island já entregou - ver o juiz. */
440
454
  const islandClassesRead = new Map();
@@ -1646,11 +1660,36 @@ export async function takeCensus(root, opts) {
1646
1660
  * Montado aqui porque é o primeiro momento em que as três coisas existem juntas: os fragmentos
1647
1661
  * vistos, os tokens declarados, e quais arquivos de estilo pertencem a componente admitido.
1648
1662
  */
1663
+ /**
1664
+ * O QUE ELE DECLAROU SOBRE AS FORMAS DELE - lido do disco, offline, antes de julgar.
1665
+ *
1666
+ * `_synthesisui/interpretation.jsonl` é escrito pela ferramenta `declare_interpretation` do MCP e
1667
+ * enviado pelo `sync`. Ler aqui é o que faz a declaração valer na PRÓXIMA medição, sem esperar
1668
+ * deploy nosso - que é a razão de a feature existir.
1669
+ */
1670
+ const declaredForms = await readDeclaredForms(root);
1649
1671
  const ledger = buildLedger(opts?.cli ?? "unknown", judgeFragments([...fragments.flat(), ...styleFiles.flatMap((s) => s.fragments)], declaredValues, admittedStyles, {
1650
1672
  wornGlobal: globalClassesClaimed,
1651
1673
  islandRead: islandClassesRead,
1652
1674
  refused: new Set(skips.map((s) => s.file)),
1653
- }));
1675
+ },
1676
+ /**
1677
+ * AS FORMAS QUE ELE DECLAROU - e é aqui que a declaração dele muda o veredito.
1678
+ *
1679
+ * Sem esta linha o arquivo `interpretation.jsonl` existiria e nada o leria: um `tierColor(gold)`
1680
+ * declarado continuaria contado como `computed`, e o cliente teria ensinado a esteira sem efeito
1681
+ * nenhum. É o defeito do `keep` outra vez, e o guard de órfãos pegou antes de subir.
1682
+ */
1683
+ declaredForms),
1684
+ /**
1685
+ * OS NOMES QUE ELE DECLARA, para a contagem de `withTheirTokens`.
1686
+ *
1687
+ * Medido no frontend-hub (21/08): um grupo reporta 10.206 declarações "não lidas" e 1.841 delas
1688
+ * (28,8%) usam `var(--...)` de um token dele. O veredito está certo - é o portão recusando página
1689
+ * -, mas o cliente lia "não lida" sobre uma declaração cujo valor está inteiramente no sistema
1690
+ * dele. O contador dá a segunda frase sem apagar a primeira.
1691
+ */
1692
+ new Set(declaredValues.keys()));
1654
1693
  const coverageLines = describeCoverage(coverage, composition, skips.length);
1655
1694
  if (coverageLines.length > 0) {
1656
1695
  say("");
@@ -1952,7 +1991,28 @@ export async function takeCensus(root, opts) {
1952
1991
  : {}),
1953
1992
  },
1954
1993
  declared: Object.fromEntries(table.byName),
1955
- ...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
1994
+ /**
1995
+ * AS FORMAS QUE ELE DECLAROU, VIAJANDO COM O CENSO - e sem isto a hierarquia não acumula nada.
1996
+ *
1997
+ * A declaração é escrita na máquina dele (`declare_interpretation` -> `interpretation.jsonl`) e
1998
+ * vale ali no mesmo instante. O que ela NÃO faz sozinha é chegar à plataforma: sem este campo, a
1999
+ * convenção seria dele para sempre, e o degrau `global` - que exige dois USUÁRIOS DISTINTOS -
2000
+ * nunca teria como ser proposto.
2001
+ *
2002
+ * Omitido quando não há nenhuma, que é o caso de todo primeiro import.
2003
+ */
2004
+ ...(declaredForms.length > 0 ? { declaredForms } : {}),
2005
+ /**
2006
+ * A FOLHA DELE ENTRA POR ÚLTIMO, e a ordem é a resposta a quem manda.
2007
+ *
2008
+ * `keyframes` já traz o que veio de CSS Module e de `<style>` island - definições de COMPONENTE.
2009
+ * A folha global é a base do app, então ela preenche o que falta e não sobrescreve o que um
2010
+ * componente declarou para si. Sem esta linha o codelevel saía com `keyframes: undefined` e 35 dos
2011
+ * 84 tokens dele - 42% do vocabulário - não chegavam a lugar nenhum.
2012
+ */
2013
+ ...(Object.keys({ ...globalKeyframes, ...keyframes }).length > 0
2014
+ ? { keyframes: { ...globalKeyframes, ...keyframes } }
2015
+ : {}),
1956
2016
  ...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
1957
2017
  ...(brokenRefs.length > 0 ? { brokenRefs } : {}),
1958
2018
  /**
@@ -3647,8 +3707,19 @@ export async function runImport(opts) {
3647
3707
  return;
3648
3708
  }
3649
3709
  const payload = (await res.json().catch(() => null));
3710
+ /**
3711
+ * O QUE ELE HERDA DO TIME, GRAVADO - e é isto que faz a hierarquia render em vez de contabilizar.
3712
+ *
3713
+ * Um segundo repositório do mesmo workspace nasce já sabendo ler a convenção que o primeiro ensinou.
3714
+ * Sem esta linha, cada dev reensinaria a mesma coisa e o degrau `workspace` existiria só como linha
3715
+ * no banco.
3716
+ */
3717
+ const inheritedHere = await inherit(root, payload?.inheritedForms ?? []);
3650
3718
  console.log("");
3651
3719
  console.log(section("Your system exists"));
3720
+ if (inheritedHere > 0) {
3721
+ console.log(body(`${inheritedHere} form${inheritedHere === 1 ? "" : "s"} your team already taught the pipeline apply here too - the next measurement reads them without you declaring anything.`));
3722
+ }
3652
3723
  console.log(body(`${paint.strong(payload?.name ?? "Your system")} - v1 mirrors your tokens exactly, nothing improved yet.`));
3653
3724
  /**
3654
3725
  * ONDE ELE NASCEU, dito pela plataforma e não pelo que foi pedido.
@@ -6,6 +6,8 @@ import { pinnedHookVersion } from "../agent-wiring.js";
6
6
  import { elementClass, kebabAxis } from "../component-codegen.js";
7
7
  import { composePlan, FAMILIES, isFamily, noFamilyAnswer, renderPlan, } from "../compose-context.js";
8
8
  import { readToken, resolveRegistry } from "../config.js";
9
+ import { declareForm } from "../doctor/declared-forms.js";
10
+ import { describeTriage, triageLedger } from "../doctor/gap-triage.js";
9
11
  import { readEvents } from "../doctor/ledger.js";
10
12
  import { fileRequest } from "../doctor/requests.js";
11
13
  import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
@@ -88,6 +90,60 @@ const TOOLS = [
88
90
  required: ["value"],
89
91
  },
90
92
  },
93
+ {
94
+ /**
95
+ * O QUE A ESTEIRA NÃO LEU DESTE REPOSITÓRIO, TRIADO - e a ferramenta que faltava ao agente.
96
+ *
97
+ * Medido na auditoria de 21/08: dos 26 comandos, 20 não têm equivalente aqui, e dezenove estão
98
+ * certos assim - `connect`, `login`, `hook`, `ci` são do dono e da máquina; `add` e `upgrade` são
99
+ * decisão de versão. Este é o único que faz falta.
100
+ *
101
+ * Sem ele, o agente encontra um valor que não virou token e não distingue "lacuna nossa" de "o
102
+ * portão recusou de propósito" de "convenção dele, que ele pode declarar" - três situações com
103
+ * ações opostas. O padrão sem resposta é improvisar, que é o evento que a plataforma mais quer
104
+ * evitar.
105
+ */
106
+ /**
107
+ * DECLARAR COMO LER UMA FORMA DELE - o par do `gaps`, e a razão de ele existir.
108
+ *
109
+ * O `gaps` mostra que uma forma não foi lida; esta resolve o caso em que a resposta é DELE. Medido
110
+ * nas duas populações, `computed` é ~1% do não interpretado - pouco em volume, e o único caso em
111
+ * que o cliente não precisa esperar o nosso backlog.
112
+ *
113
+ * Só o DECLARÁVEL entra: um padrão e um destino. O que exige cálculo (`lighten(color, 20)`) é
114
+ * leitor, é nosso, e não cabe aqui - ver `declared-forms.ts`.
115
+ */
116
+ name: "declare_interpretation",
117
+ description: 'Teach the pipeline how to read a form of THEIRS that no reader understands - a helper, an accessor, a macro. Give the pattern and the token it means: { kind: "call", match: "tierColor({arg})", resolves: "--color-tier-{arg}" }. It takes effect for THIS project immediately and needs no deploy from us. Only declare what is a straight mapping: if the value is computed (lighten, darken, arithmetic) there is no token to name, and you should say so instead. Call `gaps` first to see which forms are worth declaring.',
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ kind: {
122
+ type: "string",
123
+ enum: ["call", "member", "template", "tagged", "at-rule"],
124
+ description: "How they write it: a function call, a property access, a template literal, a tagged template, or an at-rule.",
125
+ },
126
+ match: {
127
+ type: "string",
128
+ description: "The pattern as they write it, with {name} where the value varies: `tierColor({arg})`, `theme.brand`.",
129
+ },
130
+ resolves: {
131
+ type: "string",
132
+ description: "The token it means, reusing the same {name}: `--color-tier-{arg}`. Must name a custom property.",
133
+ },
134
+ why: {
135
+ type: "string",
136
+ description: "One sentence in their terms - somebody reads this before promoting the form to a wider scope.",
137
+ },
138
+ },
139
+ required: ["kind", "match", "resolves"],
140
+ },
141
+ },
142
+ {
143
+ name: "gaps",
144
+ description: "What this pipeline did NOT read from this repository, triaged - so you can tell OUR gap from the gate refusing on purpose from a convention of theirs. Leads with the gate, which is most of what a big number is: measured on a real dashboard, 96% of the unread was the gate and not a gap. Reads the census already on disk and measures nothing. Call it before assuming a value is missing, and before filing request_token.",
145
+ inputSchema: { type: "object", properties: {} },
146
+ },
91
147
  {
92
148
  name: "system_doctrine",
93
149
  description: "The rules and the philosophy of the installed system - maximum authority, read this BEFORE building anything. It answers from the version this project has pinned, which is the same version whose tokens are on disk, so the rules you follow and the values you write come from one place. It also says when a newer version has been published, without applying it.",
@@ -975,6 +1031,99 @@ query) {
975
1031
  };
976
1032
  }
977
1033
  }
1034
+ /**
1035
+ * O QUE A ESTEIRA NÃO LEU, TRIADO - a mesma triagem do comando `gaps`, servida ao agente.
1036
+ *
1037
+ * NÃO MEDE NADA: lê o `census.json` em disco. Medir aqui seria uma segunda leitura dos arquivos dele,
1038
+ * com uma segunda chance de discordar do que a receita diz - a mesma razão pela qual o comando também
1039
+ * não mede.
1040
+ *
1041
+ * E É A MESMA FUNÇÃO, não uma segunda opinião: `triageLedger` e `describeTriage` são as que o terminal
1042
+ * usa. Duas implementações de um julgamento é como elas começam a discordar - e neste repositório isso
1043
+ * já aconteceu com o `customFontFamilies`, que existia em três cópias e quebrou nas três no mesmo dia.
1044
+ */
1045
+ /**
1046
+ * REGISTRA A DECLARAÇÃO, e recusa em voz alta o que não é declarável.
1047
+ *
1048
+ * A recusa é o mais importante daqui: um `resolves` que não nomeia token, ou um `{nome}` que o match
1049
+ * não captura, produziriam lixo na receita - e esse tipo de erro só aparece na tela do cliente, semanas
1050
+ * depois. `declareForm` faz os dois portões e a mensagem dele é a que sai aqui.
1051
+ *
1052
+ * OFFLINE: grava em `_synthesisui/interpretation.jsonl` e o `sync` envia. O agente não pode depender de
1053
+ * rede para registrar o que acabou de descobrir.
1054
+ */
1055
+ async function declareAnswer(root, args) {
1056
+ try {
1057
+ const form = await declareForm(root, {
1058
+ kind: String(args.kind ?? "call"),
1059
+ match: String(args.match ?? ""),
1060
+ resolves: String(args.resolves ?? ""),
1061
+ ...(args.why ? { why: String(args.why) } : {}),
1062
+ });
1063
+ return fromContract([
1064
+ `Recorded: \`${form.match}\` reads as \`${form.resolves}\`, for this project.`,
1065
+ "",
1066
+ "It applies from the next measurement of this repository, and it needs nothing from us - no deploy, no release. `synthesisui sync` sends it, and from there the platform can see whether other projects write the same way.",
1067
+ "",
1068
+ "Keep building. If you hit a form whose value is COMPUTED rather than named - `lighten(x, 20)`, arithmetic, a conditional - do not declare it: say in your summary that the value is assembled at render time, which is the honest answer.",
1069
+ ].join("\n"));
1070
+ }
1071
+ catch (error) {
1072
+ /** A mensagem de `declareForm` diz exatamente o que está errado no que ele passou. */
1073
+ return {
1074
+ body: `Not recorded. ${error instanceof Error ? error.message : String(error)}`,
1075
+ source: {
1076
+ kind: "none",
1077
+ reason: "token",
1078
+ detail: "declaration refused",
1079
+ },
1080
+ };
1081
+ }
1082
+ }
1083
+ async function gapsAnswer(root, cli) {
1084
+ const path = join(root, "_synthesisui", "census.json");
1085
+ const raw = await readFile(path, "utf8").catch(() => null);
1086
+ if (!raw) {
1087
+ return {
1088
+ body: `No measurement at \`_synthesisui/census.json\`, so there is nothing to triage yet. A person has to run \`npx synthesisui import --dry\` in the terminal - it is local, needs no account, and it writes that file. Until then, treat a value you cannot resolve as unknown rather than as a gap.`,
1089
+ source: { kind: "none", reason: "system", detail: "no census on disk" },
1090
+ };
1091
+ }
1092
+ let ledger;
1093
+ try {
1094
+ ledger = JSON.parse(raw).ledger;
1095
+ }
1096
+ catch {
1097
+ return {
1098
+ body: `\`_synthesisui/census.json\` is not readable JSON, so the triage cannot run. Say so rather than guessing what is missing.`,
1099
+ source: { kind: "none", reason: "system", detail: "census not JSON" },
1100
+ };
1101
+ }
1102
+ if (!ledger) {
1103
+ return {
1104
+ body: `The census on disk carries no style ledger - it was measured by a CLI older than the one that records what went unread. A person can re-measure with \`npx synthesisui import --dry\`.`,
1105
+ source: {
1106
+ kind: "none",
1107
+ reason: "system",
1108
+ detail: "census has no ledger",
1109
+ },
1110
+ };
1111
+ }
1112
+ const lines = describeTriage(triageLedger(ledger, cli));
1113
+ return fromContract([
1114
+ ...lines,
1115
+ "",
1116
+ /**
1117
+ * O QUE FAZER COM CADA VEREDITO - porque uma triagem sem ação vira um relatório que o agente lê
1118
+ * e ignora. E a ordem é a das consequências: o portão primeiro, porque é a maior parte.
1119
+ */
1120
+ "What to do with each of these:",
1121
+ " NOT A READING GAP - nothing to do, and nothing to file. Those styles belong to components this scope does not admit; the ones wearing a token of theirs already have their value in the system.",
1122
+ " NO READER FOR THIS SHAPE - ours to build. File request_token for the value you needed so it becomes a decision somebody can make, then keep going.",
1123
+ " THE SHAPE IS READ, THE VALUE IS NOT - the value sits outside their scales. That is a naming decision, and the screen offers it: do not invent a name here.",
1124
+ " A value assembled at render time cannot be read from source at all. Use what the recipe declares, and say in your summary that the computed value was left as it is.",
1125
+ ].join("\n"));
1126
+ }
978
1127
  async function describeComponent(root, name) {
979
1128
  const { documents, requires, table } = await loadSystem(root);
980
1129
  let recipe;
@@ -1293,7 +1442,7 @@ const SUBJECT_ARG = {
1293
1442
  async function callTool(root, name, args,
1294
1443
  /** A versão que reporta o estado do repo no ping - ver `repoStateOf`. */
1295
1444
  cli) {
1296
- const answer = await runTool(root, name, args);
1445
+ const answer = await runTool(root, name, args, cli);
1297
1446
  const gesture = BY_COMPONENT[name];
1298
1447
  const subject = SUBJECT_ARG[name];
1299
1448
  const target = subject
@@ -1347,12 +1496,24 @@ async function localObservation(root) {
1347
1496
  return null;
1348
1497
  }
1349
1498
  }
1350
- async function runTool(root, name, args) {
1499
+ async function runTool(root, name, args,
1500
+ /**
1501
+ * A VERSÃO QUE ESTÁ TRIANDO - e só o `gaps` precisa dela.
1502
+ *
1503
+ * A triagem compara o CLI que MEDIU o censo com o que está triando: sem isso, o veredito
1504
+ * `older-cli` não existe, e alguém escreve um leitor para uma forma que a versão seguinte já lê.
1505
+ * Opcional porque os specs chamam sem ela, e ali a versão não é o que está sob teste.
1506
+ */
1507
+ cli) {
1351
1508
  switch (name) {
1352
1509
  case "check_file":
1353
1510
  return checkFile(root, String(args.path ?? "."));
1354
1511
  case "find_token":
1355
1512
  return findToken(root, String(args.value ?? ""));
1513
+ case "declare_interpretation":
1514
+ return declareAnswer(root, args);
1515
+ case "gaps":
1516
+ return gapsAnswer(root, cli ?? "unknown");
1356
1517
  case "system_doctrine":
1357
1518
  return fromContract(await systemDoctrine(root));
1358
1519
  case "list_components":
@@ -2,6 +2,7 @@ import { readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { isOlderCli } from "../cli-version.js";
4
4
  import { readToken, resolveRegistry } from "../config.js";
5
+ import { inherit, readDeclaredForms, } from "../doctor/declared-forms.js";
5
6
  import { markSent, readEvents } from "../doctor/ledger.js";
6
7
  import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
7
8
  import { describeDelta, fingerprintReadings, readSyncMark, writeSyncMark, } from "../last-sync.js";
@@ -127,6 +128,8 @@ export async function sync(opts) {
127
128
  const events = await readEvents(root);
128
129
  const repo = opts.cli ? await repoStateOf(root, slug, opts.cli) : null;
129
130
  const requests = await readRequests(root);
131
+ /** O que ele declarou sobre as formas dele - ver `declared-forms.ts`. */
132
+ const forms = await readDeclaredForms(root);
130
133
  if (events.length === 0 && requests.length === 0) {
131
134
  console.log(section("Sync"));
132
135
  console.log(body("Nothing recorded yet. The record fills as the hook checks writes and doctor runs."));
@@ -147,6 +150,16 @@ export async function sync(opts) {
147
150
  slug,
148
151
  events,
149
152
  requests,
153
+ /**
154
+ * AS FORMAS QUE ELE DECLAROU, e sem isto elas nunca saíam da máquina dele.
155
+ *
156
+ * O import leva as que existiam NAQUELE momento - e declarar depois é o caso NORMAL, porque ele
157
+ * só descobre a lacuna usando o sistema. Sem esta linha, uma convenção declarada na terça
158
+ * ficaria dele para sempre, e a plataforma nunca saberia que outro cliente escreve igual.
159
+ *
160
+ * Omitido quando não há nenhuma, como todo campo aditivo deste payload.
161
+ */
162
+ ...(forms.length > 0 ? { declaredForms: forms } : {}),
150
163
  ...(repo ? { repo } : {}),
151
164
  }),
152
165
  }).catch(() => null);
@@ -167,8 +180,20 @@ export async function sync(opts) {
167
180
  */
168
181
  await markSent(root, events);
169
182
  const out = (await res.json());
183
+ /**
184
+ * O QUE ELE HERDA DO TIME, GRAVADO - e é isto que faz a hierarquia render em vez de contabilizar.
185
+ *
186
+ * A plataforma devolve as formas de escopo `workspace`, `user` ou `global` que alcançam este
187
+ * contexto. Sem gravá-las aqui, o colega dele reensinaria a mesma convenção e o degrau existiria só
188
+ * como linha no banco. O que ele já declarou neste projeto vence - a mais estreita ganha.
189
+ */
190
+ const gained = await inherit(root, out.inheritedForms ?? []);
170
191
  console.log(section("Sync"));
171
192
  console.log(body(`${out.eventsReceived} checks sent, ${out.eventsNew} new. ${out.requestsNow} open request${out.requestsNow === 1 ? "" : "s"}.`));
193
+ if (gained > 0) {
194
+ console.log("");
195
+ console.log(body(`${gained} form${gained === 1 ? "" : "s"} your team already taught the pipeline now apply here too - the next measurement reads them without you declaring anything.`));
196
+ }
172
197
  // Closed by CHECKING, not by word: the installed css now delivers these.
173
198
  if (verified.length > 0) {
174
199
  console.log("");
@@ -0,0 +1,166 @@
1
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ /**
4
+ * COMO LER UMA FORMA QUE A ESTEIRA NÃO ENTENDE, DECLARADO PELO CLIENTE - e valendo no mesmo instante.
5
+ *
6
+ * O PROBLEMA. Quando nenhum leitor entende uma forma, existe uma saída só: nós escrevermos o leitor, e
7
+ * o cliente esperar o nosso backlog. Medido nas duas populações, o `computed` - valor montado em tempo
8
+ * de render - é ~1% do não interpretado: 117 de 10.843 no frontend-hub, 2 de 222 no codelevel. Pouco em
9
+ * volume, e o único caso em que o cliente pode resolver HOJE.
10
+ *
11
+ * A DISTINÇÃO QUE DECIDE TUDO:
12
+ *
13
+ * DECLARÁVEL um padrão e um destino. É DADO, e não exige código nosso
14
+ * tierColor(x) -> --color-tier-{x}
15
+ * theme.brand -> --color-brand-blue
16
+ * NÃO DECLARÁVEL exige cálculo, e aí é leitor - nosso, e só existe em `global`
17
+ * lighten(color, 20) o resultado não é um nome
18
+ *
19
+ * OFFLINE-FIRST, como o resto do CLI: grava em `_synthesisui/interpretation.jsonl` e o `sync` envia.
20
+ * Mesmo desenho de `requests.jsonl`, e pelo mesmo motivo - o agente não pode depender de rede para
21
+ * registrar o que acabou de descobrir.
22
+ *
23
+ * E ISTO NÃO É REGRA DE CONDUTA: muda o que NÓS conseguimos ler, não o que ele pode escrever. O
24
+ * `--color-tier-gold` dele continua intacto no CSS. Por isso a declaração é promovível entre escopos -
25
+ * é conhecimento de interpretação, não lei sobre o código de ninguém.
26
+ */
27
+ export const FORMS_FILE = "interpretation.jsonl";
28
+ /** Os `{nome}` de um padrão, na ordem em que aparecem. */
29
+ const holes = (s) => [...s.matchAll(/\{([a-z0-9_]+)\}/gi)].map((m) => m[1]);
30
+ /**
31
+ * A CHAVE, derivada do match - e é o que faz dois clientes com o mesmo helper contarem como um.
32
+ *
33
+ * Só o nome, sem argumento: `tierColor({arg})` e `tierColor({tier})` são o mesmo helper escrito por
34
+ * duas pessoas, e tratá-los como formas diferentes mataria a contagem de origem independente.
35
+ */
36
+ export function formKey(kind, match) {
37
+ const name = match.split("(")[0].trim();
38
+ return `${kind}:${name}`;
39
+ }
40
+ /**
41
+ * APLICA AS FORMAS DECLARADAS a um valor que nenhum leitor resolveu.
42
+ *
43
+ * Devolve `null` quando nada casa, e é o que permite o chamador seguir tratando aquilo como não lido em
44
+ * vez de inventar um token.
45
+ *
46
+ * O ARGUMENTO TEM QUE SER PALAVRA SIMPLES: `tierColor(user.tier)` é montado em runtime de verdade - o
47
+ * argumento é uma expressão, e transformá-lo em `--color-tier-user.tier` produziria um token que não
48
+ * existe. Aquilo continua sendo `computed`, e dizer isso é mais honesto que resolver errado.
49
+ */
50
+ export function applyDeclaredForms(value, forms) {
51
+ const text = value.trim();
52
+ for (const form of forms) {
53
+ const name = form.match.split("(")[0].trim();
54
+ const hole = holes(form.match)[0];
55
+ if (!hole) {
56
+ /** Sem lacuna: o match é literal, e o valor tem que ser exatamente ele. */
57
+ if (text === name)
58
+ return `var(${form.resolves})`;
59
+ continue;
60
+ }
61
+ /** `nome( "arg" )` - espaço e aspas porque código real tem os dois. */
62
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
+ const call = new RegExp(`^${escaped}\\s*\\(\\s*["']?([a-z0-9_-]+)["']?\\s*\\)$`, "i").exec(text);
64
+ if (!call)
65
+ continue;
66
+ return `var(${form.resolves.replaceAll(`{${hole}}`, call[1])})`;
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * ================================================================================================
72
+ * DAQUI PARA BAIXO É O QUE TOCA DISCO, e é o que NÃO tem gêmeo do outro lado.
73
+ *
74
+ * O bloco acima - o tipo, `holes`, `formKey` e `applyDeclaredForms` - é byte-idêntico a
75
+ * `apps/web/src/lib/ds/declared-forms-shape.ts`, garantido por spec. Ele está contíguo de propósito:
76
+ * um bloco puro espalhado entre funções de arquivo é um bloco que ninguém consegue comparar, e a
77
+ * comparação é a única defesa contra as duas cópias divergirem.
78
+ * ================================================================================================
79
+ */
80
+ const path = (root) => join(root, "_synthesisui", FORMS_FILE);
81
+ /**
82
+ * DECLARA, com dois portões que existem porque o erro deles só aparece na tela do cliente.
83
+ *
84
+ * o destino tem que NOMEAR UM TOKEN senão a esteira escreve lixo na receita
85
+ * todo `{nome}` do destino tem que `--color-tier-{tier}` com um match que captura `{arg}`
86
+ * ser capturado pelo match produziria a string literal `{tier}` no nome do token
87
+ */
88
+ export async function declareForm(root, form) {
89
+ const resolves = form.resolves.trim();
90
+ if (!resolves.startsWith("--")) {
91
+ throw new Error(`"${resolves}" does not name a token. A declared form has to resolve to a custom property (\`--something\`) - anything else would put a literal in the recipe.`);
92
+ }
93
+ const captured = new Set(holes(form.match));
94
+ const missing = holes(resolves).filter((h) => !captured.has(h));
95
+ if (missing.length > 0) {
96
+ throw new Error(`"${resolves}" uses {${missing.join("}, {")}}, which the match "${form.match}" never captures. The token name would come out with the braces still in it.`);
97
+ }
98
+ const full = {
99
+ key: formKey(form.kind, form.match),
100
+ kind: form.kind,
101
+ match: form.match.trim(),
102
+ resolves,
103
+ ...(form.why ? { why: form.why } : {}),
104
+ at: form.at ?? new Date().toISOString(),
105
+ };
106
+ await mkdir(dirname(path(root)), { recursive: true });
107
+ await appendFile(path(root), `${JSON.stringify(full)}\n`, "utf8");
108
+ return full;
109
+ }
110
+ /**
111
+ * O QUE ELE DECLAROU, com a ÚLTIMA palavra por forma.
112
+ *
113
+ * Append-only no disco e dedup na leitura, como o ledger: uma correção não precisa reescrever o
114
+ * arquivo, e o histórico de que ele mudou de ideia continua lá para quem for promover.
115
+ */
116
+ export async function readDeclaredForms(root) {
117
+ const raw = await readFile(path(root), "utf8").catch(() => "");
118
+ if (!raw)
119
+ return [];
120
+ const byKey = new Map();
121
+ for (const line of raw.split("\n")) {
122
+ if (!line.trim())
123
+ continue;
124
+ try {
125
+ const form = JSON.parse(line);
126
+ if (form.key)
127
+ byKey.set(form.key, form);
128
+ }
129
+ catch {
130
+ /** Uma linha corrompida não pode custar as outras - o mesmo contrato do ledger. */
131
+ }
132
+ }
133
+ return [...byKey.values()];
134
+ }
135
+ /**
136
+ * O QUE ELE HERDA DO TIME, GRAVADO AQUI - e sem isto a hierarquia é contabilidade sem efeito.
137
+ *
138
+ * A plataforma devolve, no import e no `sync`, as formas de escopo mais largo que alcançam este
139
+ * contexto: a convenção do workspace, a do usuário, a global. Sem gravá-las, o colega dele reensinaria
140
+ * a mesma coisa - e o degrau `workspace` existiria só como linha no banco.
141
+ *
142
+ * MARCADAS COMO HERDADAS: elas entram no mesmo arquivo, com `from` dizendo de onde vieram. Isso é o que
143
+ * permite a próxima medição usá-las e ninguém confundi-las com o que ele mesmo escreveu - e é o que
144
+ * evita reenviar de volta o que a plataforma acabou de mandar.
145
+ */
146
+ export async function inherit(root, forms) {
147
+ const mine = new Set((await readDeclaredForms(root)).map((f) => f.key));
148
+ let wrote = 0;
149
+ for (const form of forms) {
150
+ /** O que ele já declarou aqui vence: a mais estreita ganha, como em toda a hierarquia. */
151
+ if (mine.has(form.key) || !form.resolves)
152
+ continue;
153
+ await mkdir(dirname(path(root)), { recursive: true });
154
+ await appendFile(path(root), `${JSON.stringify({
155
+ key: form.key,
156
+ kind: form.kind,
157
+ /** O nome sai da chave: `call:tierColor` -> `tierColor({arg})`. */
158
+ match: `${form.key.split(":")[1] ?? form.key}({arg})`,
159
+ resolves: form.resolves,
160
+ from: form.scope,
161
+ at: new Date().toISOString(),
162
+ })}\n`, "utf8");
163
+ wrote += 1;
164
+ }
165
+ return wrote;
166
+ }
@@ -13,6 +13,7 @@
13
13
  * se ele foi lido é a interpretação, que roda depois. Juntar as duas coisas numa função seria
14
14
  * repetir a mistura que a tabela de cobertura já paga: forma e veredito são fatos diferentes.
15
15
  */
16
+ import { applyDeclaredForms } from "./declared-forms.js";
16
17
  import { readStyledComponents, readSxProps } from "./style-props.js";
17
18
  import { readInlineStyle, transcribe } from "./transcribe.js";
18
19
  /** `className="…"` e `className={"…"}` - o literal puro. */
@@ -279,6 +280,18 @@ const STRUCTURE = new Set([
279
280
  "inline",
280
281
  "contents",
281
282
  ]);
283
+ /**
284
+ * UMA FORMA DECLARADA POR ELE RESOLVE ISTO? - o portão que transforma `computed` em lido.
285
+ *
286
+ * Aqui e não dentro de `applyDeclaredForms` porque o julgamento só precisa do SIM ou NÃO: o valor
287
+ * resolvido é assunto do transcritor, e duplicar a resolução em dois lugares é como eles começam a
288
+ * discordar.
289
+ */
290
+ function declaredReads(text, forms) {
291
+ if (forms.length === 0)
292
+ return false;
293
+ return applyDeclaredForms(text, forms) != null;
294
+ }
282
295
  export function judgeFragments(seen, declared, admitted,
283
296
  /**
284
297
  * AS CLASSES DE FOLHA GLOBAL QUE UM COMPONENTE ADMITIDO VESTIU - ver
@@ -297,7 +310,18 @@ export function judgeFragments(seen, declared, admitted,
297
310
  * refused arquivos de componente que o portão recusou de propósito: a classe
298
311
  * deles é `component-not-admitted` (by-design), nunca lacuna de valor
299
312
  */
300
- elsewhere) {
313
+ elsewhere,
314
+ /**
315
+ * AS FORMAS QUE ELE DECLAROU - e é o que faz um `computed` deixar de ser lacuna.
316
+ *
317
+ * Medido nas duas populações, `computed` é ~1% do não interpretado: 117 de 10.843 no frontend-hub, 2
318
+ * de 222 no codelevel. Pouco em volume, e o único caso em que o CLIENTE resolve hoje, sem deploy
319
+ * nosso - ver `declared-forms.ts`.
320
+ *
321
+ * Último parâmetro e opcional: quem julga de um contexto sem projeto (um spec, o hook) não tem
322
+ * declaração nenhuma, e ali o veredito é o de sempre.
323
+ */
324
+ forms = []) {
301
325
  const wornGlobal = elsewhere?.wornGlobal ?? new Set();
302
326
  const islandRead = elsewhere?.islandRead ?? new Map();
303
327
  const refused = elsewhere?.refused ?? new Set();
@@ -359,7 +383,9 @@ elsewhere) {
359
383
  if (f.shape === "template" && !/[a-z]/i.test(literal))
360
384
  return modules.length > 0
361
385
  ? { ...f, read: true }
362
- : { ...f, read: false, reason: "computed" };
386
+ : declaredReads(f.text, forms)
387
+ ? { ...f, read: true }
388
+ : { ...f, read: false, reason: "computed" };
363
389
  const t = transcribe(literal.split(/\s+/).filter(Boolean), declared);
364
390
  /**
365
391
  * AS QUATRO CASAS DA TRANSCRIÇÃO, não três. O `dark:data-[checked]:bg-…` do
@@ -407,7 +433,9 @@ elsewhere) {
407
433
  const read = Object.keys(readInlineStyle(`style={{${f.text}}}`)).length > 0;
408
434
  return read
409
435
  ? { ...f, read: true }
410
- : { ...f, read: false, reason: "computed" };
436
+ : declaredReads(f.text, forms)
437
+ ? { ...f, read: true }
438
+ : { ...f, read: false, reason: "computed" };
411
439
  }
412
440
  case "css":
413
441
  return admitted.has(f.file)
@@ -132,6 +132,10 @@ export function triageLedger(ledger, triagedBy) {
132
132
  work,
133
133
  because: group.because,
134
134
  examples: group.examples,
135
+ /** Passa adiante, e ausente quando é zero - a mesma regra do ledger. */
136
+ ...(group.withTheirTokens && group.withTheirTokens > 0
137
+ ? { withTheirTokens: group.withTheirTokens }
138
+ : {}),
135
139
  };
136
140
  });
137
141
  gaps.sort((a, b) => rank[a.verdict] - rank[b.verdict] || b.uses - a.uses);
@@ -187,7 +191,17 @@ export function describeTriage(t) {
187
191
  if (group.length === 0)
188
192
  continue;
189
193
  const total = t.totals[verdict];
190
- lines.push("", `${label[verdict]} - ${total} fragment${total === 1 ? "" : "s"}, ${unread > 0 ? Math.round((total / unread) * 100) : 0}% of what was not interpreted`);
194
+ /**
195
+ * QUANTOS DESTE VEREDITO JÁ VESTEM UM TOKEN DELE - a segunda frase de um número grande.
196
+ *
197
+ * Medido no frontend-hub: o `by-design` soma 10.394 fragmentos, e 1.841 deles carregam `var(--)`
198
+ * de um token declarado. "Nenhum trabalho aqui" já era verdade; o que faltava era dizer que o
199
+ * VALOR daquilo está no sistema, e não só que não há o que consertar.
200
+ */
201
+ const theirs = group.reduce((n, g) => n + (g.withTheirTokens ?? 0), 0);
202
+ lines.push("", `${label[verdict]} - ${total} fragment${total === 1 ? "" : "s"}, ${unread > 0 ? Math.round((total / unread) * 100) : 0}% of what was not interpreted${theirs > 0
203
+ ? ` — and ${theirs} of them already wear a token you declare, so their value is in the system`
204
+ : ""}`);
191
205
  for (const gap of group) {
192
206
  lines.push(` ${gap.uses} ${gap.shape} in ${gap.files} file${gap.files === 1 ? "" : "s"} (${gap.share}%) - ${gap.work}`);
193
207
  for (const example of gap.examples.slice(0, 2))
@@ -101,7 +101,15 @@ const BECAUSE = {
101
101
  * texto de um fragmento LIDO é descartado aqui de propósito - a interpretação dele já vive no
102
102
  * look, e reter as duas cópias é o que dobraria o censo.
103
103
  */
104
- export function buildLedger(cli, seen) {
104
+ export function buildLedger(cli, seen,
105
+ /**
106
+ * OS NOMES QUE ELE DECLARA - para a contagem de `withTheirTokens`.
107
+ *
108
+ * Opcional porque quem chama de um contexto sem censo (o hook, um spec) não tem a tabela dele à
109
+ * mão, e ali a conta simplesmente não é feita. Um número ausente é honesto; um zero inventado diria
110
+ * que nenhuma declaração usa token.
111
+ */
112
+ declared) {
105
113
  const counted = blank();
106
114
  const interpreted = blank();
107
115
  const groups = new Map();
@@ -122,9 +130,24 @@ export function buildLedger(cli, seen) {
122
130
  examples: [],
123
131
  unreadable: [],
124
132
  seenFiles: new Set(),
133
+ withTheirTokens: 0,
125
134
  };
126
135
  group.uses += 1;
127
136
  group.seenFiles.add(f.file);
137
+ /**
138
+ * O VALOR É UM TOKEN DELE? A referência tem que apontar para algo DECLARADO - um
139
+ * `var(--nao-existe)` é descartado pelo browser, e ali não há token a reivindicar. Mesma regra do
140
+ * desvio de `binding-not-read` em `fragments.ts`, e de propósito: duas contagens da mesma pergunta
141
+ * é como elas começam a discordar.
142
+ */
143
+ if (declared) {
144
+ for (const m of f.text.matchAll(/var\(\s*(--[a-z0-9-]+)/gi)) {
145
+ if (declared.has(m[1])) {
146
+ group.withTheirTokens = (group.withTheirTokens ?? 0) + 1;
147
+ break;
148
+ }
149
+ }
150
+ }
128
151
  if (group.examples.length < 3 &&
129
152
  !group.examples.some((e) => e.file === f.file))
130
153
  group.examples.push({
@@ -162,7 +185,12 @@ export function buildLedger(cli, seen) {
162
185
  counted,
163
186
  interpreted,
164
187
  unread: [...groups.values()]
165
- .map(({ seenFiles, ...group }) => ({ ...group, files: seenFiles.size }))
188
+ .map(({ seenFiles, withTheirTokens, ...group }) => ({
189
+ ...group,
190
+ files: seenFiles.size,
191
+ /** Ausente quando é zero: um campo zerado em todo grupo é ruído em cada relatório. */
192
+ ...(withTheirTokens && withTheirTokens > 0 ? { withTheirTokens } : {}),
193
+ }))
166
194
  .sort((a, b) => b.uses - a.uses),
167
195
  };
168
196
  }
@@ -198,7 +226,17 @@ export function describeLedger(l) {
198
226
  lines.push(` ${SHAPE_LABEL[shape].padEnd(28)} ${String(seen).padStart(6)} seen ${String(got).padStart(6)} interpreted${got === seen ? "" : ` (${seen - got} listed below)`}`);
199
227
  }
200
228
  for (const g of l.unread) {
201
- lines.push(` ${g.uses} ${g.shape}${g.uses === 1 ? "" : "s"} in ${g.files} file${g.files === 1 ? "" : "s"} - ${g.because}`);
229
+ /**
230
+ * A SEGUNDA FRASE, quando parte daquilo já veste um token dele - ver `withTheirTokens`.
231
+ *
232
+ * Sem ela o cliente lê "10.206 não lidas" e conclui que a plataforma não entendeu o repositório
233
+ * dele. No frontend-hub, 1.841 daquelas (28,8%) carregam `var(--)` de um token que ele declara: o
234
+ * valor está inteiramente no sistema, e o que falta é só saber qual elemento o veste.
235
+ */
236
+ const theirs = g.withTheirTokens && g.withTheirTokens > 0
237
+ ? ` — and ${g.withTheirTokens} of them already wear a token you declare, so their value is in the system`
238
+ : "";
239
+ lines.push(` ${g.uses} ${g.shape}${g.uses === 1 ? "" : "s"} in ${g.files} file${g.files === 1 ? "" : "s"} - ${g.because}${theirs}`);
202
240
  for (const e of g.examples)
203
241
  lines.push(` ${e.file}:${e.line} ${e.text.slice(0, 80)}`);
204
242
  }
@@ -0,0 +1,46 @@
1
+ import { keyframeOffsets, readModuleCss } from "./doctor/css-modules.js";
2
+ /**
3
+ * OS KEYFRAMES QUE A FOLHA DELE DECLARA - e eram 42% do vocabulário do codelevel.
4
+ *
5
+ * O leitor colhia keyframes de duas fontes: o CSS Module de um componente e o `<style>` island. Não da
6
+ * folha GLOBAL, que é exatamente onde um projeto Tailwind v4 com `@theme` os escreve. Medido no censo
7
+ * do dono (21/08): 35 dos 84 tokens declarados são `--animate-*`, e o censo saía com
8
+ * `keyframes: undefined`.
9
+ *
10
+ * O valor de cada atalho carrega tudo o que importa, e nada disso chegava:
11
+ *
12
+ * --animate-fade-slide-in: fade-slide-in 1s var(--ease-out-soft) both
13
+ * ^ o keyframe ^ dur ^ o easing DELE ^ fill
14
+ *
15
+ * E a consequência não era só perder token. `unknownAnimations` crescia: o documento recusava definir
16
+ * quatro animações que os componentes dele nomeiam - a nota dizia "a plugin's own" - quando a definição
17
+ * estava na folha dele, a um parser de distância.
18
+ *
19
+ * NADA AQUI É PARSER NOVO. `readModuleCss` já extrai `@keyframes` de qualquer CSS e `keyframeOffsets`
20
+ * já vira o corpo cru na forma que o documento quer (offset → declarações). Só ninguém os chamava para
21
+ * a folha global. É o mesmo defeito de forma que apareceu seis vezes nesta rodada: a varredura
22
+ * enumerando as fontes que já conhecia.
23
+ *
24
+ * MATERIALISER - um cliente pinado recebe diferente? Não: isto muda o CENSO, e o que cai na pasta dele
25
+ * vem do documento que o servidor compila depois. Ele só vê a diferença após um re-import, e é por isso
26
+ * que esta é a única entrega da rodada que exige um.
27
+ */
28
+ export function keyframesInSheets(sheets) {
29
+ const out = {};
30
+ for (const sheet of sheets) {
31
+ if (!sheet.body.includes("@keyframes"))
32
+ continue;
33
+ const read = readModuleCss(sheet.body);
34
+ for (const [name, body] of Object.entries(read.keyframes)) {
35
+ /** A PRIMEIRA declaração vence, como em todo leitor daqui: duas folhas com o mesmo nome é
36
+ * theming por área, e escolher a última faria a ordem de caminhada decidir o resultado. */
37
+ if (out[name])
38
+ continue;
39
+ const offsets = keyframeOffsets(body);
40
+ /** Um nome sem corpo legível viraria uma referência que não anima nada. */
41
+ if (Object.keys(offsets).length > 0)
42
+ out[name] = offsets;
43
+ }
44
+ }
45
+ return out;
46
+ }
@@ -115,7 +115,20 @@
115
115
  * O índice de componentes é BYTE-IDÊNTICO nos dois caminhos - mesma fonte canônica, mesmos nomes,
116
116
  * mesma quantidade (`claude-md.f0.spec.ts`). O corte troca uma frase, nunca o inventário.
117
117
  */
118
- export const MATERIALISER_SINCE = "0.16.269";
118
+ /**
119
+ * 0.16.269 -> 0.16.270 em 21/08, e a correção é de uma ARMADILHA que vai repetir.
120
+ *
121
+ * A marca foi escrita como "0.16.269" porque era o que o `package.json` dizia naquele instante. Só que
122
+ * o dono PUBLICOU o 0.16.269 antes de o código entrar - o pacote no npm saiu 17:46 e o PR foi mergeado
123
+ * 18:53. Então a marca apontava para uma versão que existe e NÃO tem a mudança, e o efeito era o pior
124
+ * possível: `installedBehind("0.16.269", "0.16.270", "0.16.269")` é FALSO, então o cliente que instalou
125
+ * aquele 0.16.269 nunca seria avisado.
126
+ *
127
+ * A REGRA, para não repetir: a marca aponta para a versão que vai ser PUBLICADA COM a mudança, e essa
128
+ * é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
129
+ * publicar no meio.
130
+ */
131
+ export const MATERIALISER_SINCE = "0.16.270";
119
132
  /**
120
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
121
134
  *
@@ -217,7 +230,30 @@ export const CHECKER_SINCE = "0.16.250";
217
230
  * (era 53% no melhor caminho; o derivado dava 38%). Um censo medido antes disto não
218
231
  * tem versão nenhuma - e o mapa de blueprints por versão não tem o que consultar.
219
232
  */
220
- export const READER_SINCE = "0.16.261";
233
+ /**
234
+ * `0.16.261` -> `0.16.269` em 21/08: os keyframes que a folha GLOBAL dele declara passaram a ser
235
+ * lidos - ver `global-keyframes.ts`.
236
+ *
237
+ * O portão desta marca é o corpus dourado, e ele NÃO cobrou: o censo dos treze apps saiu idêntico,
238
+ * porque nenhuma das treze fixtures escreve `@keyframes` em folha global. A marca sobe por decisão,
239
+ * não por vermelho - e o número que justifica está no repo real: 35 dos 84 tokens declarados do
240
+ * codelevel são `--animate-*`, o censo dele saía com `keyframes: undefined`, e agora sai com 35.
241
+ *
242
+ * Sem subir, o `align` nunca diria a quem já importou que vale remedir - que é a única coisa que esta
243
+ * marca existe para fazer.
244
+ */
245
+ /** 0.16.269 -> 0.16.270 pelo mesmo motivo do MATERIALISER acima: o 0.16.269 foi publicado sem o
246
+ * leitor de keyframes globais, e uma marca que aponta para ele cala o aviso para quem o instalou. */
247
+ /**
248
+ * 0.16.270 -> 0.16.271: o ledger passou a contar quantos fragmentos NÃO LIDOS já vestem um token que
249
+ * ele declara (`withTheirTokens`). É campo novo no censo, então um censo medido antes não o tem - e a
250
+ * segunda frase do relatório ("1.841 delas já usam os seus tokens") não pode ser dita sobre ele.
251
+ *
252
+ * A marca aponta para a versão DESTE PR, nunca para a que o `package.json` já carregava: o 0.16.270 foi
253
+ * publicado antes deste código existir, e uma marca nele calaria o aviso para quem o instalou. Mesma
254
+ * lição de algumas horas antes, na mesma sessão.
255
+ */
256
+ export const READER_SINCE = "0.16.271";
221
257
  /**
222
258
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
223
259
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.269",
3
+ "version": "0.16.271",
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": {