synthesisui 0.16.305 → 0.16.307

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.
@@ -37,20 +37,22 @@ export async function hookCommand(root, version) {
37
37
  : `npx synthesisui@${version} hook`;
38
38
  }
39
39
  /**
40
- * COMO A SESSÃO CHAMA O CLI, e por que este NÃO é pinado.
40
+ * COMO A SESSÃO CHAMA O CLI - pinado, como o hook de escrita, e por um número.
41
41
  *
42
- * O hook de escrita é pinado porque roda dezenas de vezes por sessão e um verificador que muda sob
43
- * você a cada edit é indebugável. Este roda UMA vez, e o trabalho dele é justamente dizer o que está
44
- * atrasado - inclusive o próprio CLI. Um `align` pinado no dia do `connect` não conhece nenhuma
45
- * verificação escrita depois, e passa a garantir silêncio em vez de alinho.
42
+ * Este já foi `@latest`, com a tese de que um `align` pinado não conhece verificação escrita depois
43
+ * dele. A tese perdeu para a medição de 25/08: `@latest` NUNCA cacheia - o npx faz round-trip no
44
+ * registro em TODA abertura de sessão (2,92s, sempre), enquanto a versão fixada cacheia (0,32s
45
+ * morno). O trabalho do align em si custa 50ms; o resto era o preço de manter a tese.
46
46
  *
47
- * O custo do `npx` (720ms-1.5s, medido em 27/07) é pago uma vez na abertura, não por escrita.
47
+ * E a tese tinha resposta: dizer o que está atrasado - inclusive o próprio CLI - continua sendo o
48
+ * trabalho do align, só que agora o fato vem do SERVIDOR (`cli` no `?meta=1`, ver `versionBehind`),
49
+ * que sabe o que está publicado sem que este binário precise se reinstalar para perguntar.
48
50
  */
49
- export async function alignCommand(root) {
51
+ export async function alignCommand(root, version) {
50
52
  const local = join(root, "node_modules", ".bin", "synthesisui");
51
53
  return (await exists(local))
52
54
  ? "npx --no-install synthesisui align"
53
- : "npx synthesisui@latest align";
55
+ : `npx synthesisui@${version} align`;
54
56
  }
55
57
  /**
56
58
  * Returns the command that IS in the file, not the one we would have written.
@@ -234,7 +236,7 @@ export async function wireAgent(root, version, want) {
234
236
  * logado" ou "este sistema não sabe de onde foi medido".
235
237
  */
236
238
  session: want.hook
237
- ? await wireSessionStart(root, await alignCommand(root))
239
+ ? await wireSessionStart(root, await alignCommand(root, version))
238
240
  : "skipped",
239
241
  mcp: want.mcp ? await wireMcp(root, version) : "skipped",
240
242
  };
@@ -459,6 +459,20 @@ export async function versionBehind(root, opts = {}) {
459
459
  says: `the rules that govern "${lock.slug}" changed since this repo materialized them - your agent reads the copy on disk, so it is still following the previous set.`,
460
460
  run: "npx synthesisui upgrade",
461
461
  };
462
+ /**
463
+ * ESTE BINÁRIO FICOU PARA TRÁS - a linha que devolve ao align o trabalho que o `@latest` fazia.
464
+ *
465
+ * A abertura de sessão roda uma versão PINADA desde 25/08 (o `@latest` custava 2,92s de registro
466
+ * em toda sessão, medido), então nada aqui descobre sozinho que existe CLI mais novo - o número
467
+ * vem do servidor, que consulta o npm e cala quando não sabe. Vem POR ÚLTIMO de propósito: um
468
+ * fato sobre o sistema dele vale mais que uma novidade sobre a nossa ferramenta, e esta linha só
469
+ * aparece quando todo o resto está em dia.
470
+ */
471
+ if (opts.cli && body.cli && isOlderCli(opts.cli, body.cli))
472
+ return {
473
+ says: `this session's opening check runs CLI ${opts.cli} and ${body.cli} is what installs today - the check is pinned on purpose, so it will not move by itself.`,
474
+ run: "npx synthesisui@latest connect",
475
+ };
462
476
  return null;
463
477
  }
464
478
  /**
@@ -550,7 +564,9 @@ export async function misalignments(root, opts = {}) {
550
564
  ...(opts.cli ? { cli: opts.cli } : {}),
551
565
  }).catch(() => []);
552
566
  /** A única linha que custa rede, e ela some inteira quando não há rede. */
553
- const remote = await versionBehind(root).catch(() => null);
567
+ const remote = await versionBehind(root, {
568
+ ...(opts.cli ? { cli: opts.cli } : {}),
569
+ }).catch(() => null);
554
570
  if (remote)
555
571
  items.push(remote);
556
572
  return items;
@@ -1,5 +1,5 @@
1
1
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
- import { basename, dirname, join, relative } from "node:path";
2
+ import { basename, dirname, join, relative, sep } from "node:path";
3
3
  import { anatomyFromSketch } from "../anatomy-from-sketch.js";
4
4
  import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
5
5
  import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
@@ -34,6 +34,7 @@ import { buildLedger, describeLedger, } from "../doctor/style-ledger.js";
34
34
  import { MUI_DEFAULT_SPACING, readStyledComponents, spacingOf, } from "../doctor/style-props.js";
35
35
  import { buildTable } from "../doctor/tokens.js";
36
36
  import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
37
+ import { accountClasses, invariantHolds, sumAccounts, } from "../doctor/value-ledger.js";
37
38
  import { transcribeVariants } from "../doctor/variant-read.js";
38
39
  import { frontierKind, packageRoot } from "../frontier-kind.js";
39
40
  import { keyframesInSheets } from "../global-keyframes.js";
@@ -402,8 +403,18 @@ export async function takeCensus(root, opts) {
402
403
  * Os caminhos que o leitor REALMENTE olhou são o escopo mais os de uso: `--scope packages/ui
403
404
  * --usage apps/web` lê os dois, então nenhum dos dois é "fora". Sem `scopeLabel` a função devolve
404
405
  * `null` por conta, porque aí ele apontou para a raiz.
406
+ *
407
+ * A CAMINHADA PARTE DA RAIZ DO REPO, não da raiz escopada. `root` aqui JÁ é
408
+ * `<repo>/<scopeLabel>` (o chamador faz `join(root, one)`), então medir a partir dele compara
409
+ * `src/Button.tsx` com `packages/ui/…` - nada casa, e os 121 arquivos que a esteira acabou de LER
410
+ * eram reportados como "fora do que você apontou - nada foi lido lá". Disparava SEMPRE com
411
+ * `--scope`, e o primeiro relatório do cliente abria com uma acusação falsa sobre o repositório
412
+ * dele (medido em 25/08: 121 contra os 14 reais).
405
413
  */
406
- const outside = await outsideScope(root, [
414
+ const repoRoot = scopeLabel && root.endsWith(join(sep, ...scopeLabel.split("/")))
415
+ ? root.slice(0, root.length - scopeLabel.length - 1)
416
+ : root;
417
+ const outside = await outsideScope(repoRoot, [
407
418
  ...(scopeLabel ? [scopeLabel] : []),
408
419
  ...(opts?.usage ?? []).map((u) => u.label),
409
420
  ]).catch(() => null);
@@ -2095,6 +2106,25 @@ export async function takeCensus(root, opts) {
2095
2106
  const target = c?.canonical ?? (c?.bucket === "exclusive" ? c.name : null);
2096
2107
  return target ? safePartName(target) : null;
2097
2108
  }, (pkg) => versions[pkg]);
2109
+ /**
2110
+ * A CONTABILIDADE POR VALOR, contada AQUI porque o material é daqui: os tokens de classe que
2111
+ * vestem cada componente admitido (os nós do sketch e as camadas condicionais cruas), contra
2112
+ * os tokens que o CSS dele declara. Ver `Census.values` e `doctor/value-ledger.ts` - é a
2113
+ * reconciliação do item 11, e a soma fecha com `seen` por construção.
2114
+ */
2115
+ const valueByComponent = {};
2116
+ for (const [lookName, look] of Object.entries(looks)) {
2117
+ const tokens = [];
2118
+ for (const node of look.sketch ?? []) {
2119
+ tokens.push(...(node.classes ?? "").split(/\s+/).filter(Boolean));
2120
+ }
2121
+ for (const layer of look.rawLayers ?? [])
2122
+ tokens.push(...layer.classes);
2123
+ if (tokens.length > 0) {
2124
+ valueByComponent[lookName] = accountClasses(tokens, declaredValues);
2125
+ }
2126
+ }
2127
+ const valuesTotal = sumAccounts(Object.values(valueByComponent));
2098
2128
  return {
2099
2129
  census: 1,
2100
2130
  project: {
@@ -2251,6 +2281,19 @@ export async function takeCensus(root, opts) {
2251
2281
  },
2252
2282
  }
2253
2283
  : {}),
2284
+ /**
2285
+ * A conta por VALOR - ver o tipo. Ausente quando nenhum componente veste classe, e ausente
2286
+ * quando a soma não fecha: uma conta que viola a própria invariante é bug NOSSO, e um censo
2287
+ * sem o campo é honesto enquanto um campo errado governaria telas. Hoje ela fecha por
2288
+ * construção; o portão existe para o dia em que uma derivação (a engine do D3) não fechar.
2289
+ */
2290
+ ...(valuesTotal.seen > 0 && invariantHolds(valuesTotal)
2291
+ ? {
2292
+ values: {
2293
+ classes: { ...valuesTotal, byComponent: valueByComponent },
2294
+ },
2295
+ }
2296
+ : {}),
2254
2297
  ...(architectures.length > 0
2255
2298
  ? {
2256
2299
  architectures: architectures.slice(0, 6).map((a) => ({
@@ -45,8 +45,12 @@ const STYLED_OPEN = /\bstyled(?:\.[a-zA-Z][\w]*|\([^)]{1,60}\))\s*(?:<[^>]{0,120
45
45
  const PIECES = /([^{};]*)([{};]|$)/g;
46
46
  /** Uma declaração `prop: value`, já sem o terminador. */
47
47
  const DECLARATION = /^(-{0,2}[a-zA-Z][\w-]*)\s*:\s*([^;{}]{1,200})$/;
48
- /** Uma classe que só um literal pode ter: começa com letra minúscula. */
49
- const LOOKS_LIKE_CLASS = /^[a-z[]/;
48
+ /**
49
+ * Uma classe que só um literal pode ter: começa com letra minúscula - ou com o `!` de important,
50
+ * que é como o Tailwind v3 escreve `!text-ink-900`. Sem o `!`, um className inteiro de important
51
+ * nem entrava no denominador: não era lido E não era "não lido" (25/08).
52
+ */
53
+ const LOOKS_LIKE_CLASS = /^[!a-z[]/;
50
54
  /**
51
55
  * `${styles.card}` dentro de um template: uma classe de CSS module, cujo valor mora no arquivo de
52
56
  * estilo - que a esteira lê. É a mesma regra que `CLASS_RUNTIME` já aplica a `className={styles.x}`.
@@ -669,7 +669,17 @@ function withAlpha(d, alpha) {
669
669
  }
670
670
  /** `w-1/2`, `basis-2/3` - a fraction is a width, and the `/` is not an alpha. */
671
671
  const FRACTION = /^(w|h|basis|max-w|max-h|min-w|min-h|top|right|bottom|left|inset)-(\d+)\/(\d+)$/;
672
- export function readUtility(utility, declared) {
672
+ export function readUtility(raw, declared) {
673
+ /**
674
+ * O `!` DE IMPORTANT SAI ANTES DE TUDO - `!text-ink-900` (v3) e `text-ink-900!` (v4).
675
+ *
676
+ * O important é como ele força o cascade DENTRO do app dele; a decisão de design é o valor, e
677
+ * era ela que morria: `!text-ink-900` chegava aqui inteiro, nenhum prefixo casava, e a camada
678
+ * sumia sem uma linha - com `--color-ink-900` declarado no CSS dele. 48 valores num censo real,
679
+ * 20 e 4 nas duas populações congeladas (25/08). Numa receita não há cascade para forçar, então
680
+ * despir o `!` não perde decisão nenhuma.
681
+ */
682
+ const utility = raw.replace(/^!/, "").replace(/!$/, "");
673
683
  /**
674
684
  * BEFORE THE ALPHA SPLIT, because the two share a slash and only one of them is a
675
685
  * colour. `w-1/2` was reaching the size reader as `w-1` and coming back `0.25rem` -
@@ -0,0 +1,153 @@
1
+ import { parseClass, readUtility } from "./transcribe.js";
2
+ /**
3
+ * AS PROPRIEDADES QUE SÃO ARRANJO - a definição de "estrutura", por propriedade resolvida.
4
+ *
5
+ * O critério do dono (D4): estrutura é onde "token é impossível por definição" - não existe
6
+ * decisão de vocabulário em `display: flex`. Espaçamento e tamanho NÃO entram: `gap`, `padding`
7
+ * e `height` são decisões de escala (a métrica de Spacing & Proportion existe para elas).
8
+ */
9
+ const STRUCTURAL_PROPERTIES = new Set([
10
+ "display",
11
+ "position",
12
+ "top",
13
+ "right",
14
+ "bottom",
15
+ "left",
16
+ "inset",
17
+ "insetInline",
18
+ "insetBlock",
19
+ "zIndex",
20
+ "overflow",
21
+ "overflowX",
22
+ "overflowY",
23
+ "flexDirection",
24
+ "flexWrap",
25
+ "alignItems",
26
+ "alignContent",
27
+ "alignSelf",
28
+ "justifyContent",
29
+ "justifyItems",
30
+ "justifySelf",
31
+ "placeItems",
32
+ "placeContent",
33
+ "order",
34
+ "flex",
35
+ "flexGrow",
36
+ "flexShrink",
37
+ "flexBasis",
38
+ "gridColumn",
39
+ "gridRow",
40
+ "gridTemplateColumns",
41
+ "gridTemplateRows",
42
+ "pointerEvents",
43
+ "userSelect",
44
+ "textAlign",
45
+ "verticalAlign",
46
+ "whiteSpace",
47
+ "objectFit",
48
+ "objectPosition",
49
+ "visibility",
50
+ "isolation",
51
+ "textOverflow",
52
+ "borderCollapse",
53
+ ]);
54
+ /**
55
+ * TOKENS DE ARRANJO QUE O RESOLVEDOR NÃO DEVOLVE COMO DECLARAÇÃO - a metade utilitária da mesma
56
+ * definição. `w-full` e `truncate` não são decisão de vocabulário; `hidden` e `sr-only` são
57
+ * visibilidade. Lista CONSERVADORA de propósito: o que não estiver aqui e não resolver cai em
58
+ * `unread`, que é o lado certo de errar - um arranjo cobrado como lacuna custa uma linha; uma
59
+ * decisão de estilo escondida como "estrutura" custa a confiança na métrica.
60
+ */
61
+ const STRUCTURAL_TOKENS = new Set([
62
+ /** Os valores de display crus - a mesma lista que o juiz de fragmento chama de STRUCTURE. */
63
+ "flex",
64
+ "inline-flex",
65
+ "grid",
66
+ "inline-grid",
67
+ "block",
68
+ "inline-block",
69
+ "inline",
70
+ "contents",
71
+ "flow-root",
72
+ "flex-row",
73
+ "flex-col",
74
+ "flex-row-reverse",
75
+ "flex-col-reverse",
76
+ "w-full",
77
+ "h-full",
78
+ "w-fit",
79
+ "h-fit",
80
+ "w-max",
81
+ "w-min",
82
+ "min-w-0",
83
+ "shrink",
84
+ "shrink-0",
85
+ "grow",
86
+ "grow-0",
87
+ "flex-1",
88
+ "flex-auto",
89
+ "flex-none",
90
+ "hidden",
91
+ "sr-only",
92
+ "not-sr-only",
93
+ "truncate",
94
+ "group",
95
+ "peer",
96
+ "isolate",
97
+ ]);
98
+ /**
99
+ * O destino de UM token de classe. Exportado para o julgamento por valor e a triagem lerem a
100
+ * MESMA resposta - duas cópias desta decisão é como o item 11 nasceu.
101
+ */
102
+ export function destinationOf(cls, declared) {
103
+ const { utility } = parseClass(cls);
104
+ if (STRUCTURAL_TOKENS.has(utility))
105
+ return "structure";
106
+ const read = readUtility(utility, declared);
107
+ if (read) {
108
+ return STRUCTURAL_PROPERTIES.has(read.property)
109
+ ? "structure"
110
+ : "interpreted";
111
+ }
112
+ return "unread";
113
+ }
114
+ /** A conta de uma lista de tokens - a soma SEMPRE fecha com `seen`, por construção. */
115
+ export function accountClasses(tokens, declared) {
116
+ const account = {
117
+ seen: 0,
118
+ interpreted: 0,
119
+ structure: 0,
120
+ unread: 0,
121
+ answered: 0,
122
+ unknown: 0,
123
+ };
124
+ for (const cls of tokens) {
125
+ account.seen += 1;
126
+ account[destinationOf(cls, declared)] += 1;
127
+ }
128
+ return account;
129
+ }
130
+ /** Soma de contas - o total do censo é a soma das contas por componente, nunca outra medição. */
131
+ export function sumAccounts(accounts) {
132
+ const out = {
133
+ seen: 0,
134
+ interpreted: 0,
135
+ structure: 0,
136
+ unread: 0,
137
+ answered: 0,
138
+ unknown: 0,
139
+ };
140
+ for (const a of accounts) {
141
+ out.seen += a.seen;
142
+ out.interpreted += a.interpreted;
143
+ out.structure += a.structure;
144
+ out.unread += a.unread;
145
+ out.answered += a.answered;
146
+ out.unknown += a.unknown;
147
+ }
148
+ return out;
149
+ }
150
+ /** A invariante do item 11, como pergunta - o spec das populações a assere, o portão a exige. */
151
+ export function invariantHolds(a) {
152
+ return (a.interpreted + a.structure + a.unread + a.answered + a.unknown === a.seen);
153
+ }
@@ -270,6 +270,67 @@ export function literalClasses(fragment) {
270
270
  }
271
271
  return out;
272
272
  }
273
+ /**
274
+ * OS TRECHOS DO ARQUIVO QUE CONSOMEM CLASSE - `className={…}` (e qualquer `*ClassName=`),
275
+ * e as chamadas que produzem classe (`cn`, `clsx`, `cva`, `tv`, …).
276
+ *
277
+ * Existe porque os três leitores de condição liam o arquivo INTEIRO como se toda string fosse
278
+ * classe: 531 de 1119 entradas de `rawLayers.classes` num censo real eram literais de ternário e
279
+ * de record que nunca foram classe - `gray`, `True`, `Find`, `✅`, `h1` - e o gradiente do Aurora
280
+ * deles chegava partido em `0%,rgba(…)` porque um VALOR de CSS foi partido no espaço como se
281
+ * fosse lista de classes (medido em 25/08). A forma sozinha não decide (`gray` e `hidden` têm a
282
+ * mesma forma); o consumo decide - a mesma doutrina que `looksLikeClassList` já escreveu.
283
+ */
284
+ const CLASS_ATTR = /(?:^|[^A-Za-z])[A-Za-z]*[Cc]lassName=\{/g;
285
+ const CLASS_CALL = /\b(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat|cva|tv)\(/g;
286
+ export function classSpans(source) {
287
+ const spans = [];
288
+ for (const re of [CLASS_ATTR, CLASS_CALL]) {
289
+ re.lastIndex = 0;
290
+ for (const m of source.matchAll(re)) {
291
+ const open = (m.index ?? 0) + m[0].length - 1;
292
+ const end = endOf(source, open);
293
+ if (end !== -1)
294
+ spans.push([open, end]);
295
+ }
296
+ }
297
+ return spans;
298
+ }
299
+ function inSpan(spans, index) {
300
+ return spans.some(([open, end]) => index > open && index < end);
301
+ }
302
+ /**
303
+ * O RECORD É CONSUMIDO COMO CLASSE? - direto (`styles[size]` dentro de um contexto de classe) ou
304
+ * com um pulo (`const config = statusConfig[status]` e depois `config.dotClass` num `cn()`),
305
+ * inclusive desestruturado (`const { fontSize } = sizeConfig[size]`).
306
+ *
307
+ * Um pulo só, de propósito: seguir a cadeia inteira seria reimplementar um resolvedor de escopo, e
308
+ * os 8 de 8 casos medidos no repo real (dono, 06/08) cabem em um.
309
+ */
310
+ function consumedAsClass(source, spans, symbol) {
311
+ for (const m of source.matchAll(new RegExp(`\\b${symbol}\\s*\\[`, "g"))) {
312
+ const at = m.index ?? 0;
313
+ if (inSpan(spans, at))
314
+ return true;
315
+ const before = source.slice(Math.max(0, at - 120), at);
316
+ const alias = /([A-Za-z_$][\w$]*)\s*=\s*$/.exec(before)?.[1];
317
+ const destructured = /\{([^{}]*)\}\s*=\s*$/.exec(before)?.[1];
318
+ const names = destructured
319
+ ? destructured.split(",").map((s) => s.split(":")[0].trim())
320
+ : alias
321
+ ? [alias]
322
+ : [];
323
+ for (const name of names) {
324
+ if (!name || !/^[A-Za-z_$][\w$]*$/.test(name))
325
+ continue;
326
+ for (const use of source.matchAll(new RegExp(`\\b${name}\\b`, "g"))) {
327
+ if (inSpan(spans, use.index ?? 0))
328
+ return true;
329
+ }
330
+ }
331
+ }
332
+ return false;
333
+ }
273
334
  /**
274
335
  * Line and block comments removed, quotes respected.
275
336
  *
@@ -600,8 +661,15 @@ function enclosingCallClasses(source, at) {
600
661
  }
601
662
  export function readInlineConditions(source) {
602
663
  const layers = [];
664
+ const spans = classSpans(source);
603
665
  INLINE_COND.lastIndex = 0;
604
666
  for (const m of source.matchAll(INLINE_COND)) {
667
+ /**
668
+ * `cond && "string"` fora de um contexto de classe é quase sempre CONTEÚDO de JSX
669
+ * (`{loading && "Finding..."}`), e entrava como classe - o mesmo portão dos ternários.
670
+ */
671
+ if (!inSpan(spans, m.index ?? 0) && !looksLikeClassList(m[5] ?? m[9] ?? ""))
672
+ continue;
605
673
  const negated = (m[1] ?? m[6]) === "!";
606
674
  const prop = (m[2] ?? m[7] ?? "").split(".").pop() ?? "";
607
675
  const raw = m[5] ?? m[9] ?? "";
@@ -824,6 +892,7 @@ unattributed) {
824
892
  axisOf.set(m[1], m[2]);
825
893
  }
826
894
  const layers = [];
895
+ const spans = classSpans(clean);
827
896
  LOOKUP_RECORD.lastIndex = 0;
828
897
  for (const m of clean.matchAll(LOOKUP_RECORD)) {
829
898
  const symbol = m[1];
@@ -840,6 +909,16 @@ unattributed) {
840
909
  const options = entries.filter((e) => literalClasses(e.value).length > 0);
841
910
  if (options.length < 2)
842
911
  continue;
912
+ /**
913
+ * SÓ É ESTILO O QUE UM CONTEXTO DE CLASSE CONSOME - ou o que tem a forma inequívoca de uma
914
+ * lista de classes. Um record indexado é a forma de MUITAS coisas: `TIER_LABELS[tier]` guarda
915
+ * conteúdo, `AURORA_PALETTES[palette]` guarda um gradiente que vai para `style=`, e ambos
916
+ * entravam aqui como classe - 531 de 1119 entradas do censo real eram isso (25/08). O consumo
917
+ * era a única prova, e a mesma regra já valia para os campos de uma tabela por elemento.
918
+ */
919
+ if (!consumedAsClass(clean, spans, symbol) &&
920
+ !options.some((o) => looksLikeClassList(o.value)))
921
+ continue;
843
922
  if (axesOut) {
844
923
  const names = options.map((o) => o.key);
845
924
  axesOut[axis] = [...new Set([...(axesOut[axis] ?? []), ...names])];
@@ -916,8 +995,26 @@ unattributed) {
916
995
  const CLASS_TERNARY = /([!A-Za-z_$][\w$.!]*)\s*(?:===\s*["']([^"']+)["']\s*)?\?\s*["']([^"']*)["']\s*:\s*["']([^"']*)["']/g;
917
996
  export function readClassTernaries(source) {
918
997
  const layers = [];
998
+ const clean = stripComments(source);
999
+ const spans = classSpans(clean);
919
1000
  CLASS_TERNARY.lastIndex = 0;
920
- for (const m of stripComments(source).matchAll(CLASS_TERNARY)) {
1001
+ for (const m of clean.matchAll(CLASS_TERNARY)) {
1002
+ /**
1003
+ * UM TERNÁRIO ENTRE DUAS STRINGS É A FORMA DE QUALQUER COISA - `loading ? "Finding..." :
1004
+ * "Find"` é o rótulo de um botão, `large ? "h1" : "h2"` é uma tag. Fora de um contexto de
1005
+ * classe, só a forma inequívoca de lista de classes prova estilo; dentro, o consumo prova.
1006
+ * Sem este portão, os dois exemplos acima entravam no censo como classe (25/08).
1007
+ */
1008
+ const at = m.index ?? 0;
1009
+ if (!inSpan(spans, at)) {
1010
+ const before = clean.slice(Math.max(0, at - 120), at);
1011
+ const alias = /([A-Za-z_$][\w$]*)\s*=\s*$/.exec(before)?.[1];
1012
+ const aliased = alias &&
1013
+ /^[A-Za-z_$][\w$]*$/.test(alias) &&
1014
+ [...clean.matchAll(new RegExp(`\\b${alias}\\b`, "g"))].some((u) => inSpan(spans, u.index ?? 0));
1015
+ if (!aliased && !looksLikeClassList(m[3]) && !looksLikeClassList(m[4]))
1016
+ continue;
1017
+ }
921
1018
  const negated = m[1].startsWith("!");
922
1019
  const prop = m[1].replace(/^!/, "").split(".").pop() ?? "";
923
1020
  if (!prop)
@@ -941,6 +1038,100 @@ export function readClassTernaries(source) {
941
1038
  }
942
1039
  return layers;
943
1040
  }
1041
+ /** O valor quando ele é UMA string literal inteira - `null` para objeto, template com `${}`, etc. */
1042
+ function singleStringValue(value) {
1043
+ const trimmed = value.trim();
1044
+ if (!/^["'`]/.test(trimmed))
1045
+ return null;
1046
+ const closed = endOfString(trimmed);
1047
+ if (closed === -1 || trimmed.slice(closed).trim())
1048
+ return null;
1049
+ const inner = trimmed.slice(1, closed - 1);
1050
+ if (trimmed[0] === "`" && inner.includes("${"))
1051
+ return null;
1052
+ return inner;
1053
+ }
1054
+ /**
1055
+ * UM RECORD CONSUMIDO POR `style=` É ESTILO POR PROVA, NÃO POR FORMA - e o valor viaja INTEIRO.
1056
+ *
1057
+ * const AURORA_PALETTES: Record<AuroraPalette, string> = {
1058
+ * default: "linear-gradient(var(--aurora-angle,125deg),rgba(79,70,229,0.18) 0%,…)",
1059
+ * cool: "linear-gradient(…)",
1060
+ * };
1061
+ * style={{ backgroundImage: AURORA_PALETTES[palette], … }} // ou style={mergedStyle}
1062
+ *
1063
+ * O leitor de classes partia esse gradiente no espaço e o censo carregava `0%,rgba(192,132,252,0.18)`
1064
+ * como se fosse classe - 46 valores num censo real (25/08). A propriedade é a que ELE escreveu no
1065
+ * objeto de style (`backgroundImage`), o valor é o que ELE declarou no record, e o eixo é a prop que
1066
+ * indexa - transcrever não é inventar. Um nível de indireção é seguido (`const mergedStyle = {…}` e
1067
+ * `style={mergedStyle}`), que é como o componente real escreve.
1068
+ */
1069
+ export function readStyleRecords(source, axesOut) {
1070
+ const clean = stripComments(source);
1071
+ const axisOf = new Map();
1072
+ RECORD_INDEX.lastIndex = 0;
1073
+ for (const m of clean.matchAll(RECORD_INDEX)) {
1074
+ if (!axisOf.has(m[1]))
1075
+ axisOf.set(m[1], m[2]);
1076
+ }
1077
+ if (axisOf.size === 0)
1078
+ return [];
1079
+ // symbol → option → o valor inteiro, para os records cujos valores são strings.
1080
+ const records = new Map();
1081
+ LOOKUP_RECORD.lastIndex = 0;
1082
+ for (const m of clean.matchAll(LOOKUP_RECORD)) {
1083
+ if (!axisOf.has(m[1]))
1084
+ continue;
1085
+ const open = (m.index ?? 0) + m[0].length - 1;
1086
+ const end = endOf(clean, open);
1087
+ if (end === -1)
1088
+ continue;
1089
+ const options = topLevelEntries(clean.slice(open + 1, end - 1))
1090
+ .map((e) => ({ key: e.key, value: singleStringValue(e.value) }))
1091
+ .filter((e) => e.value != null);
1092
+ if (options.length >= 2)
1093
+ records.set(m[1], options);
1094
+ }
1095
+ if (records.size === 0)
1096
+ return [];
1097
+ // Os corpos que ALIMENTAM um style=: o objeto inline, e o const que um style={nome} aponta.
1098
+ const bodies = [];
1099
+ for (const m of clean.matchAll(/style=\{\{/g)) {
1100
+ const open = (m.index ?? 0) + m[0].length - 1;
1101
+ const end = endOf(clean, open);
1102
+ if (end !== -1)
1103
+ bodies.push(clean.slice(open + 1, end - 1));
1104
+ }
1105
+ for (const m of clean.matchAll(/style=\{\s*([A-Za-z_$][\w$]*)\s*\}/g)) {
1106
+ const named = new RegExp(`(?:const|let)\\s+${m[1]}\\b[^=]*=\\s*\\{`, "g").exec(clean);
1107
+ if (!named)
1108
+ continue;
1109
+ const open = named.index + named[0].length - 1;
1110
+ const end = endOf(clean, open);
1111
+ if (end !== -1)
1112
+ bodies.push(clean.slice(open + 1, end - 1));
1113
+ }
1114
+ const layers = [];
1115
+ for (const body of bodies) {
1116
+ for (const m of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\s*\[/g)) {
1117
+ const property = m[1];
1118
+ const options = records.get(m[2]);
1119
+ const axis = axisOf.get(m[2]);
1120
+ if (!options || !axis)
1121
+ continue;
1122
+ if (axesOut)
1123
+ axesOut[axis] = [
1124
+ ...new Set([...(axesOut[axis] ?? []), ...options.map((o) => o.key)]),
1125
+ ];
1126
+ for (const option of options)
1127
+ layers.push({
1128
+ when: { variant: { [axis]: option.key } },
1129
+ style: { [property]: option.value },
1130
+ });
1131
+ }
1132
+ }
1133
+ return layers;
1134
+ }
944
1135
  /**
945
1136
  * A LOOKUP INDEXED BY A TERNARY, which is how the same record serves a boolean.
946
1137
  *
@@ -977,6 +1168,12 @@ globals) {
977
1168
  ...readLookupRecords(source, fromRecords, unslotted),
978
1169
  ...readClassTernaries(source),
979
1170
  ];
1171
+ /**
1172
+ * O record que um `style=` consome - valores prontos, sem tabela de escala no meio.
1173
+ * Entram direto nas camadas finais (e registram o eixo), nunca em `raw`: `raw` é
1174
+ * matéria-prima de reinterpretação por classes, e estes já são o valor final.
1175
+ */
1176
+ const styleRecords = readStyleRecords(source, fromRecords);
980
1177
  const axes = {};
981
1178
  const defaults = {};
982
1179
  const allLayers = [];
@@ -1087,7 +1284,7 @@ globals) {
1087
1284
  reads,
1088
1285
  axes,
1089
1286
  defaults,
1090
- layers,
1287
+ layers: [...layers, ...styleRecords],
1091
1288
  raw: allLayers,
1092
1289
  base: withGlobal(transcribe(baseClasses, declared), baseClasses, globals),
1093
1290
  unslotted,
@@ -128,7 +128,16 @@
128
128
  * é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
129
129
  * publicar no meio.
130
130
  */
131
- export const MATERIALISER_SINCE = "0.16.305";
131
+ /**
132
+ * 0.16.305 -> 0.16.306 em 25/08, e o SIM é sobre a FIAÇÃO que o `upgrade` regrava: o hook de
133
+ * `SessionStart` passa a ser pinado (`npx synthesisui@<versão> align`) em vez de `@latest`. O
134
+ * `@latest` nunca cacheia - round-trip no registro em TODA abertura de sessão, 2,92s medidos, contra
135
+ * 0,32s da versão fixada - e o trabalho do align em si custa 50ms. Um `upgrade` anterior deixa o
136
+ * `.claude/settings.json` pagando 2,9s por sessão; quem roda o upgrade novo recebe bytes diferentes
137
+ * ali, que é exatamente o que esta marca existe para dizer. Quem avisa versão nova passa a ser o
138
+ * próprio align, com o `cli` que o servidor manda no `?meta=1`.
139
+ */
140
+ export const MATERIALISER_SINCE = "0.16.306";
132
141
  /**
133
142
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
143
  *
@@ -264,7 +273,32 @@ export const CHECKER_SINCE = "0.16.250";
264
273
  *
265
274
  * É exatamente o que esta marca existe para dizer: o `align` avisa que vale remedir.
266
275
  */
267
- export const READER_SINCE = "0.16.305";
276
+ /**
277
+ * 0.16.305 -> 0.16.306 em 25/08: três leituras que devolvem estilo do cliente, medidas antes de
278
+ * escrever (itens 8, 9 e 10 do diagnóstico de 25/08):
279
+ *
280
+ * 1. O `!` de important deixa de matar o utilitário: `!text-ink-900` com `--color-ink-900`
281
+ * declarado agora resolve (48 valores no censo real; e o ledger passou a CONTAR um className
282
+ * de important, que antes nem entrava no denominador).
283
+ * 2. Um record consumido por `style=` viaja como o VALOR que ele é: o gradiente do Aurora chega
284
+ * inteiro em `layers`, em vez de partido em `0%,rgba(…)` como classe (46 valores).
285
+ * 3. Só é classe o que um contexto de classe consome: literais de ternário e de record de
286
+ * conteúdo (`Finding...`, `h1`, `✅`) saem de `rawLayers.classes` - eram 531 de 1119 entradas
287
+ * (47%) no censo real, poluindo numerador e denominador de toda métrica em cima.
288
+ *
289
+ * Um censo medido antes carrega a poluição e a perda; quem remede ganha os três de uma vez.
290
+ */
291
+ /**
292
+ * 0.16.306 -> 0.16.307 em 25/08: o censo passa a carregar `values` - a CONTABILIDADE POR VALOR
293
+ * (item 11). Todo token de classe que veste um componente admitido tem um destino, e a soma fecha
294
+ * por construção: `interpreted + structure + unread + answered + unknown == seen`. É a
295
+ * reconciliação entre o ledger (que conta lugares) e a produção (que trabalha em valores) - 86%
296
+ * contra 50% no mesmo repositório, sem uma linha ligando os dois.
297
+ *
298
+ * Campo novo: um censo medido antes não o tem, e a tela de cobertura (D4: três colunas, estrutura
299
+ * visível fora da métrica) só pode ser honesta sobre quem remedir. É o que esta marca avisa.
300
+ */
301
+ export const READER_SINCE = "0.16.307";
268
302
  /**
269
303
  * O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
270
304
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.305",
3
+ "version": "0.16.307",
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": {