synthesisui 0.16.419 → 0.16.421

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.
@@ -226,7 +226,16 @@
226
226
  *
227
227
  * O que o cliente ganha ao rodar `upgrade`: a lista responde ao espaço, como a caixa promete.
228
228
  */
229
- export const MATERIALISER_SINCE = "0.16.416";
229
+ /**
230
+ * 0.16.416 -> 0.16.421 em 12/09, e o passo 1 deu **SIM**: muda um byte que cai na pasta dele.
231
+ *
232
+ * O filtro do hook em `.claude/settings.json` era `Write|Edit|MultiEdit` e passa a incluir a
233
+ * escrita por shell. Quem instalou antes tem no arquivo dele o filtro estreito que NÓS escrevemos,
234
+ * e sem rodar o comando ele fica com uma checagem que cala em toda edição em lote. O `upgrade`
235
+ * chama `wireAgent`, então rodar o comando é o caminho de volta - e ele só alarga a string que era
236
+ * nossa, nunca um filtro que uma pessoa escreveu.
237
+ */
238
+ export const MATERIALISER_SINCE = "0.16.421";
230
239
  /**
231
240
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
232
241
  *
@@ -334,8 +343,8 @@ export const MATERIALISER_SINCE = "0.16.416";
334
343
  * A frase viaja ao lado da versão porque as duas são uma coisa só: quem move a marca troca a
335
344
  * explicação no mesmo lugar, em vez de deixar o CI citando a causa da marca anterior.
336
345
  */
337
- export const COUNTED_DIFFERENTLY_SINCE = "0.16.408";
338
- export const COUNTED_DIFFERENTLY = "this run counts every length in a shorthand - that version counted only the first";
346
+ export const COUNTED_DIFFERENTLY_SINCE = "0.16.421";
347
+ export const COUNTED_DIFFERENTLY = "this run counts the utilities of your own theme - that version counted only `var(--token)`";
339
348
  /**
340
349
  * 0.16.408 -> 0.16.413 em 10/09: o hook passa a DIZER o valor que nada nomeia. Ele silenciava toda
341
350
  * deriva sem token de destino - a decisão estava escrita como "não vale interromper, porque o único
@@ -344,7 +353,20 @@ export const COUNTED_DIFFERENTLY = "this run counts every length in a shorthand
344
353
  * e nós engolimos". Agora sai `line 12 #ff00aa`, sem uma única proposta de nome nosso. Um hook
345
354
  * pinado antes desta versão aconselha MENOS, que é a régua desta marca.
346
355
  */
347
- export const CHECKER_SINCE = "0.16.419";
356
+ /**
357
+ * 0.16.419 -> 0.16.421 em 12/09: o hook passa a rodar quando o ARQUIVO muda, e não só quando o
358
+ * agente usa a ferramenta de edição.
359
+ *
360
+ * MEDIDO no `codelevel`, três sessões: toda edição em lote passou por comando de shell e a
361
+ * checagem produziu **0 linhas**; na única edição feita pela ferramenta de edição ela falou na
362
+ * hora. Um hook pinado antes desta versão fica calado em toda escrita por shell, que é a forma
363
+ * como um agente edita em lote - aconselha MENOS, e é a régua desta marca.
364
+ *
365
+ * E ele passa a contar a utility do tema DELE como uso de token: num projeto cujo idioma é
366
+ * utility, um arquivo escrito inteiro no vocabulário do sistema recebia zero. A contagem em si é a
367
+ * outra marca - ver `COUNTED_DIFFERENTLY_SINCE`, que sobe no mesmo diff.
368
+ */
369
+ export const CHECKER_SINCE = "0.16.421";
348
370
  /**
349
371
  * A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
350
372
  *
@@ -793,7 +815,7 @@ export const CHECKER_SINCE = "0.16.419";
793
815
  * deveriam estar lá. Quem tem censo gravado precisa de um `sync` para a fila de trabalho dele ser a
794
816
  * real; ver o livro-razão de `corpus.spec.ts`.
795
817
  */
796
- export const READER_SINCE = "0.16.417";
818
+ export const READER_SINCE = "0.16.421";
797
819
  /**
798
820
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
799
821
  *
@@ -237,11 +237,20 @@ export function mergeCensus(list) {
237
237
  const named = totals.tokenUses + (totals.ownUses ?? 0);
238
238
  const denominator = named + totals.values + (totals.phantomUses ?? 0);
239
239
  const complete = list.every((c) => c.totals.ownUses !== undefined);
240
- totals.coverage = !complete
240
+ /**
241
+ * `0 DE 0` NÃO É NOTA MÁXIMA - a mesma correção que a régua recebeu em 12/09 (ver `Reach` em
242
+ * `doctor/scan.ts`). Fundir dois escopos sem um único valor de design devolvia `100`, e o campo
243
+ * agora some: quem lê distingue "nada veio do sistema" de "não havia o que contar".
244
+ */
245
+ const share = !complete
241
246
  ? first.totals.coverage
242
247
  : denominator > 0
243
248
  ? Math.round((named / denominator) * 100)
244
- : 100;
249
+ : undefined;
250
+ if (share === undefined)
251
+ delete totals.coverage;
252
+ else
253
+ totals.coverage = share;
245
254
  const out = {
246
255
  ...first,
247
256
  project: { ...first.project, stack },
@@ -1,4 +1,4 @@
1
- import { readdir } from "node:fs/promises";
1
+ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative, sep } from "node:path";
3
3
  /**
4
4
  * O QUE O ESCOPO EXCLUIU - o único denominador que a esteira não tinha.
@@ -78,11 +78,26 @@ async function countIn(dir, root, inside, tally, depth = 0) {
78
78
  * escopo" não é uma categoria que existe - e emitir uma linha de zero ensinaria o cliente a ignorar a
79
79
  * seção quando ela tiver algo.
80
80
  */
81
- export async function outsideScope(root, scopes) {
82
- const declared = scopes.map((s) => s.replace(/^\.\//, "").replace(/\/$/, ""));
81
+ export async function outsideScope(root, scopes,
82
+ /**
83
+ * QUEM MAIS ESTÁ GOVERNADO, e quem ele tirou.
84
+ *
85
+ * Os dois vêm MEDIDOS ou DECLARADOS, nunca supostos: `consumers` é `census.adoption.consumers`,
86
+ * que a esteira produz ao ver quem importa a biblioteca; `ungoverned` é a lista que ELE escreveu
87
+ * em `_synthesisui/config.json`. Sem os dois, esta função responde exatamente o que respondia
88
+ * antes de a governança existir.
89
+ */
90
+ also = {}) {
91
+ const clean = (s) => s.replace(/^\.\//, "").replace(/\/$/, "");
92
+ const declared = scopes.map(clean);
83
93
  if (declared.length === 0)
84
94
  return null;
85
- const inside = (rel) => declared.some((s) => rel === s || rel.startsWith(`${s}/`));
95
+ const out = (also.ungoverned ?? []).map(clean);
96
+ const isOut = (rel) => out.some((s) => rel === s || rel.startsWith(`${s}/`));
97
+ /** Um consumidor que ele declarou fora volta a ser fora - a saída é dele. */
98
+ const consumers = (also.consumers ?? []).map(clean).filter((c) => !isOut(c));
99
+ const under = (paths, rel) => paths.some((s) => rel === s || rel.startsWith(`${s}/`));
100
+ const inside = (rel) => under(declared, rel);
86
101
  const tally = new Map();
87
102
  await countIn(root, root, inside, tally);
88
103
  const places = [...tally]
@@ -91,6 +106,20 @@ export async function outsideScope(root, scopes) {
91
106
  const files = places.reduce((n, p) => n + p.files, 0);
92
107
  if (files === 0)
93
108
  return null;
109
+ /**
110
+ * A GOVERNANÇA É UM CAMPO AO LADO, e NUNCA um desconto nesta conta - achado da revisão de
111
+ * contrato no fecho, e ele estava certo pelo motivo exato.
112
+ *
113
+ * A primeira versão tirava os consumidores de `files`/`places`, e isso misturava duas réguas num
114
+ * número só: um app que ninguém APONTOU continua não tendo sido LIDO, e esta conta existe para
115
+ * dizer o tamanho da porta fechada. Descontá-lo aqui fazia 26 arquivos de um repositório real
116
+ * saírem do denominador sem uma linha deles ter sido aberta - o oposto do que o cabeçalho
117
+ * promete.
118
+ *
119
+ * A separação é por LUGAR porque a unidade da frase é o lugar: `apps/landing` é o que ele
120
+ * reconhece, e `countIn` já agrupa por diretório de topo.
121
+ */
122
+ const governed = places.filter((p) => under(consumers, p.path));
94
123
  const named = places
95
124
  .slice(0, 3)
96
125
  .map((p) => `${p.path} (${p.files})`)
@@ -98,6 +127,73 @@ export async function outsideScope(root, scopes) {
98
127
  return {
99
128
  files,
100
129
  places,
130
+ governed,
131
+ /** A frase é só sobre LEITURA - a da governança mora em `guarantee.ts`, e é outra régua. */
101
132
  said: `${files} more readable file${files === 1 ? "" : "s"} live outside what you pointed at - ${named}${places.length > 3 ? `, and ${places.length - 3} more place${places.length - 3 === 1 ? "" : "s"}` : ""}. Nothing was read there, so nothing about them is in this measurement: point at them too if their design belongs in this system.`,
102
133
  };
103
134
  }
135
+ /**
136
+ * QUEM DEPENDE DESTA BIBLIOTECA, lido do arquivo em que o próprio gerenciador de pacotes decide.
137
+ *
138
+ * ═══ O QUE ISTO DESTRAVA ═══
139
+ *
140
+ * Os apps que consomem a biblioteca passam a estar governados SEM ninguém digitar um `--usage`.
141
+ * Medido em 12/09 no `codelevel`: o censo conhecia `apps/landing` e `apps/web` porque alguém os
142
+ * apontou à mão; num repositório onde ninguém apontou, os dois lugares onde a tela é escrita
143
+ * ficavam do lado de fora da garantia, ao lado de `packages/cli` e `packages/db`.
144
+ *
145
+ * ═══ POR QUE `package.json`, E NÃO UMA VARREDURA DE IMPORTS ═══
146
+ *
147
+ * Um app que escreve `import { Card } from "@repo/ui"` é um consumidor, e descobrir isso lendo
148
+ * todo arquivo do repositório custa a varredura inteira. O `package.json` dele já declara a
149
+ * dependência - é o que o gerenciador de pacotes lê para instalar -, e uma dependência declarada é
150
+ * evidência mais forte que uma string encontrada: ela é a intenção, não o rastro.
151
+ *
152
+ * O QUE ISTO NÃO ALCANÇA, declarado: um repositório de um pacote só, sem workspaces, não tem um
153
+ * segundo `package.json` para ler - e ali não há "consumidor" separado a governar, porque o escopo
154
+ * medido e o app são o mesmo lugar.
155
+ */
156
+ export async function consumersOf(root, specifier) {
157
+ if (!specifier)
158
+ return [];
159
+ const found = [];
160
+ const visit = async (dir, depth) => {
161
+ if (depth > 3)
162
+ return;
163
+ let entries;
164
+ try {
165
+ entries = await readdir(dir, { withFileTypes: true });
166
+ }
167
+ catch {
168
+ return;
169
+ }
170
+ for (const entry of entries) {
171
+ if (entry.name.startsWith(".") || SKIP.has(entry.name))
172
+ continue;
173
+ if (!entry.isDirectory())
174
+ continue;
175
+ const here = join(dir, entry.name);
176
+ const raw = await readFile(join(here, "package.json"), "utf8").catch(() => "");
177
+ if (raw) {
178
+ try {
179
+ const pkg = JSON.parse(raw);
180
+ const deps = ["dependencies", "devDependencies", "peerDependencies"]
181
+ .map((k) => pkg[k])
182
+ .filter((d) => !!d && typeof d === "object");
183
+ if (deps.some((d) => specifier in d)) {
184
+ const rel = relative(root, here);
185
+ /** O próprio pacote não é consumidor de si mesmo. */
186
+ if (rel && pkg.name !== specifier)
187
+ found.push(rel);
188
+ }
189
+ }
190
+ catch {
191
+ // Um `package.json` ilegível custa a descoberta daquele lugar, nunca a rodada.
192
+ }
193
+ }
194
+ await visit(here, depth + 1);
195
+ }
196
+ };
197
+ await visit(root, 0);
198
+ return found.sort();
199
+ }
@@ -1,40 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  const run = promisify(execFile);
4
- /**
5
- * O QUE ESTA EDIÇÃO APAGOU, E QUAL REGRA DELE ISSO TOCA.
6
- *
7
- * ═══ O CASO, medido em 12/09 ═══
8
- *
9
- * O agente dele apagou a serif itálica de seis lugares da landing do `codelevel`. A regra existia,
10
- * instalada: *"A display headline breaks onto a line set in the serif, italic - that line carries
11
- * the emphasis of the sentence, never the whole headline."* Nada avisou.
12
- *
13
- * Duas razões, e este arquivo é as duas. A checagem lia o ARQUIVO depois da escrita - e uma
14
- * remoção é, por construção, aquilo que não está mais lá. E ela media VALOR SOLTO contra token,
15
- * nunca regra.
16
- *
17
- * ═══ O ANTES VEM DO GIT, e não do payload do cliente ═══
18
- *
19
- * O hook recebe `tool_input.file_path` e o cliente PODE mandar o texto anterior - mas o formato é
20
- * de cada cliente, muda entre versões, e um produto que se apoia nele funciona num editor e cala
21
- * no seguinte. O git é o mesmo em todo lugar e responde a pergunta certa: *o que esta árvore
22
- * removeu em relação ao último commit?* - que é exatamente o que ele vai empurrar.
23
- *
24
- * SEM GIT, SEM ARQUIVO NO HEAD, OU ARQUIVO NOVO: não há "antes", então não há remoção. Silêncio é
25
- * a resposta correta, e não uma falha - um arquivo novo não apagou nada de ninguém.
26
- *
27
- * ═══ E O CASAMENTO É POR VOCABULÁRIO DECLARADO, nunca por palavra em inglês ═══
28
- *
29
- * A tentação é procurar as palavras da regra no diff. Isso casaria *"never"* com qualquer coisa e
30
- * transformaria a checagem numa fonte de falso positivo - e um falso positivo custa mais que
31
- * silêncio aqui, porque ele treina a pessoa a ignorar a saída e depois a desinstalar o hook.
32
- *
33
- * Então os dois lados são amarrados pelo que o SISTEMA DELE DECLARA. Uma regra "toca" o que foi
34
- * apagado quando um nome declarado aparece nos dois: no texto da regra e no que saiu do arquivo.
35
- * No caso real: o sistema declara `--font-serif`, a regra fala em `serif`, e a edição removeu
36
- * `font-serif` seis vezes.
37
- */
38
4
  /** Como o texto anterior era, ou `null` quando não há "antes" que se possa provar. */
39
5
  export async function previousText(root, rel) {
40
6
  try {
@@ -78,11 +44,20 @@ export async function previousText(root, rel) {
78
44
  * arquivo. É a troca certa. A regra fala sobre o sistema ter aquela decisão presente, e um uso que
79
45
  * fica mantém a decisão presente; acusar por contagem transformaria toda refatoração num alarme.
80
46
  */
81
- export function namesRemoved(before, after, declared) {
82
- const had = declaredNamesIn(before, declared);
47
+ export function namesRemoved(before, after,
48
+ /**
49
+ * A TABELA INTEIRA, e não o conjunto de nomes - porta fechada em vez de vigiada.
50
+ *
51
+ * Ela era `declared: ReadonlySet<string>`, e quando a leitura por idioma entrou o chamador
52
+ * passaria a precisar de um segundo argumento. Um argumento a mais é um argumento esquecido: o
53
+ * comando leria as utilities dele e o hook não, e o mesmo repositório receberia duas respostas
54
+ * sobre a mesma regra.
55
+ */
56
+ table) {
57
+ const had = declaredNamesIn(before, table);
83
58
  if (had.length === 0)
84
59
  return [];
85
- const kept = new Set(declaredNamesIn(after, declared));
60
+ const kept = new Set(declaredNamesIn(after, table));
86
61
  return had.filter((name) => !kept.has(name));
87
62
  }
88
63
  /**
@@ -124,13 +99,28 @@ const mentions = (text, word) => new RegExp(`(^|[^a-z0-9-])${word}([^a-z0-9-]|$)
124
99
  * `--font-serif` cru numa declaração, e `font-serif` como utility - que é o idioma medido em 72%
125
100
  * dos `.tsx` dele. Sem a terceira, um repositório Tailwind inteiro não referenciaria nada.
126
101
  */
127
- export function declaredNamesIn(text, declared) {
102
+ export function declaredNamesIn(text, { declared, utilities }) {
128
103
  const found = new Set();
129
104
  for (const name of declared) {
130
105
  const bare = name.replace(/^--/, "");
131
106
  if (text.includes(name) || mentions(text, bare))
132
107
  found.add(name);
133
108
  }
109
+ /**
110
+ * E A QUARTA FORMA, que era a maior lacuna - a utility que o idioma dele RENOMEIA.
111
+ *
112
+ * As três acima só fecham onde o nome do token É a string escrita (`--font-serif` →
113
+ * `font-serif`). Onde o compilador renomeia - `--color-primary` → `bg-primary`,
114
+ * `--spacing-6` → `p-6` - nada casava, e medido em 12/09 na população viva: **5 das 25 regras
115
+ * dele eram alcançadas**, todas por `--font-*`, `--animate-*`, `--ease-*`, `--shadow-sm`,
116
+ * `--text-sm`. Zero cor, zero espaçamento, zero raio - que é a maior classe de regra de qualquer
117
+ * design system.
118
+ *
119
+ * O índice sai do que ELE declarou, pelo contrato do framework - ver `idiom-names.ts`.
120
+ */
121
+ for (const [spelling, name] of utilities)
122
+ if (mentions(text, spelling))
123
+ found.add(name);
134
124
  return [...found];
135
125
  }
136
126
  /** A regra escreve este nome - por inteiro, ou pela folha dele. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.419",
3
+ "version": "0.16.421",
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": {