synthesisui 0.16.136 → 0.16.138

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.
@@ -29,6 +29,7 @@
29
29
  * validate everywhere and render as a hole, and "valid and unread" is the worst
30
30
  * of the three outcomes this pipeline has paid for twice.
31
31
  */
32
+ import { readSxProps } from "./doctor/style-props.js";
32
33
  import { transcribe, } from "./doctor/transcribe.js";
33
34
  import { frontierKind } from "./frontier-kind.js";
34
35
  /** The ten forms, closed. A renderer that accepts any tag draws anything. */
@@ -141,6 +142,23 @@ function frontierClasses(node, sketch) {
141
142
  const trimmed = raw?.trim();
142
143
  return trimmed ? { classes: trimmed } : {};
143
144
  }
145
+ /**
146
+ * A PROP DE ESTILO DE UM NÓ, verbatim, para o snippet devolvê-la.
147
+ *
148
+ * Numa biblioteca OPACA a gente nomeia e para - ela se desenha sozinha, e as declarações do `sx`
149
+ * não viram estilo nosso. Mas sem a prop de volta, o snippet entrega um `<Box>` que perdeu a
150
+ * margem que eles escreveram: 57 de 57 `sx` do app dele estão exatamente aí (05/08). É a mesma
151
+ * decisão que `motionProps` já carrega.
152
+ */
153
+ function frontierSx(node, sketch) {
154
+ const raw = typeof node.sx === "string"
155
+ ? node.sx
156
+ : typeof node.at === "number"
157
+ ? sketch?.[node.at]?.sx
158
+ : undefined;
159
+ const trimmed = raw?.trim();
160
+ return trimmed ? { sx: trimmed.slice(0, 400) } : {};
161
+ }
144
162
  export function resolveAnatomy(read, declared, deps,
145
163
  /** Their name → the slug it reaches in this system. See `RefResolver`. */
146
164
  resolve,
@@ -155,7 +173,12 @@ resolve,
155
173
  root,
156
174
  /** This component's sketch, so a node can name its classes by INDEX instead
157
175
  * of retyping them - see `AnatomyRead.at`. */
158
- sketch) {
176
+ sketch,
177
+ /**
178
+ * A escala de espaçamento que o projeto DELES declara para o `sx`. Ausente = o default
179
+ * documentado do MUI, que é fato da biblioteca que eles escolheram - ver `spacingOf`.
180
+ */
181
+ sxSpacing) {
159
182
  /**
160
183
  * THE PACKAGE THE RETURNED ELEMENT CAME FROM, measured rather than spelled.
161
184
  * Node 0 of the sketch IS what the component returns, and the reader stamped its
@@ -261,6 +284,7 @@ sketch) {
261
284
  from,
262
285
  ...(version ? { version } : {}),
263
286
  ...frontierClasses(node, sketch),
287
+ ...frontierSx(node, sketch),
264
288
  ...holds,
265
289
  });
266
290
  continue;
@@ -386,7 +410,25 @@ sketch) {
386
410
  : "";
387
411
  const classes = raw.split(/\s+/).filter(Boolean);
388
412
  const t = transcribe(classes, declared);
389
- parts[key] = { base: t.base, dark: t.dark, states: t.states };
413
+ /**
414
+ * A PROP DE ESTILO DESTE NÓ, sob a classe dele.
415
+ *
416
+ * `sx={{ width: "100%" }}` pertence ao elemento que a carrega - a mesma atribuição que a
417
+ * classe tem, e é por isso que ela viaja no sketch. Sob a classe porque uma prop é a
418
+ * camada mais fraca: se a classe declara a mesma propriedade, a classe ganha (é o que o
419
+ * `style={{ }}` já faz na raiz).
420
+ */
421
+ const sxText = typeof node.at === "number"
422
+ ? sketch?.[node.at]?.sx
423
+ : undefined;
424
+ const fromSx = sxText
425
+ ? readSxProps(sxText, sxSpacing).declarations
426
+ : {};
427
+ parts[key] = {
428
+ base: { ...fromSx, ...t.base },
429
+ dark: t.dark,
430
+ states: t.states,
431
+ };
390
432
  /**
391
433
  * A drop on a PART is still a drop. The root path already says these out loud;
392
434
  * this one silently swallowed `p-spacing-sm` on a real menu popup, so the part
@@ -27,6 +27,7 @@ import { describeSignals, emptySignals, finishSignals, readSignalsInto, } from "
27
27
  import { sketchOf } from "../doctor/sketch.js";
28
28
  import { describeForeign, readStyleIslands, } from "../doctor/style-island.js";
29
29
  import { buildLedger, describeLedger, } from "../doctor/style-ledger.js";
30
+ import { MUI_DEFAULT_SPACING, readStyledComponents, spacingOf, } from "../doctor/style-props.js";
30
31
  import { buildTable } from "../doctor/tokens.js";
31
32
  import { definitionSpan, readInlineStyle, rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
32
33
  import { transcribeVariants } from "../doctor/variant-read.js";
@@ -418,6 +419,8 @@ export async function takeCensus(root, opts) {
418
419
  const islands = new Map();
419
420
  /** How each component expresses style, so the census can state its own coverage. */
420
421
  const shapes = new Map();
422
+ /** A escala de espaçamento que o projeto declara para o `sx` - ver o uso abaixo. */
423
+ let sxSpacing;
421
424
  /** Component FILES the gate accepted - the honest denominator for coverage. A first
422
425
  * version used the whole inventory, which includes everything they compose from a
423
426
  * package, and reported 128 for a library of 36 (probe, 01/08). */
@@ -546,6 +549,20 @@ export async function takeCensus(root, opts) {
546
549
  * permite o relatório dizer o que existe e não foi lido, em vez de só o que foi.
547
550
  */
548
551
  fragments.push(fragmentsOfSource(rel, src));
552
+ /**
553
+ * A ESCALA DE ESPAÇAMENTO DELES, se eles declaram uma.
554
+ *
555
+ * `mt: 2` num `sx` significa `margin-top: 2 * spacing`, e o `spacing` é decisão do projeto.
556
+ * 8px é o default DOCUMENTADO do MUI - fato da biblioteca que eles escolheram, não palpite
557
+ * nosso - mas um projeto que escreve `createTheme({ spacing: 4 })` está declarando outra
558
+ * coisa, e a convenção do usuário ganha da nossa sempre. Medido uma vez, aqui, onde os
559
+ * arquivos já estão abertos: reabri-los depois seria a esteira medindo duas vezes.
560
+ */
561
+ if (sxSpacing == null && /createTheme\s*\(/.test(src)) {
562
+ const declaredSpacing = spacingOf(src);
563
+ if (declaredSpacing !== MUI_DEFAULT_SPACING)
564
+ sxSpacing = declaredSpacing;
565
+ }
549
566
  for (const d of found) {
550
567
  sourceOf.set(d.name, { file: rel, source: src });
551
568
  /** O que a peça É, do portão - ver `GateVerdict.kind` e `CensusLook.kind`. */
@@ -674,18 +691,34 @@ export async function takeCensus(root, opts) {
674
691
  * not more honest than literal - v1 mirrors, v2 normalises.
675
692
  */
676
693
  const inlineStyle = readInlineStyle(src);
694
+ /**
695
+ * O QUE UM COMPONENTE `styled` DECLARA - e ele não tem JSX, então nada até aqui o leu.
696
+ *
697
+ * `const Title = styled.h1\`font-size: 1.5rem\`` é a aparência de um `<h1>` inteiro, com o
698
+ * nome do componente na variável. O corpo é lido pelo MESMO leitor de regra que lê um
699
+ * module, então `&:hover` e `@media` vêm de graça; o que depende de `${}` é contado e não
700
+ * lido, porque não está na fonte (medido no app dele: 9 componentes, 100 declarações, 5
701
+ * com interpolação).
702
+ */
703
+ const styledHere = readStyledComponents(src).find((c) => c.name === found[0].name);
677
704
  const base = {
678
705
  ...inlineStyle,
706
+ ...styledHere?.base,
679
707
  ...fromModule?.base,
680
708
  ...v.base.base,
681
709
  ...t.base,
682
710
  };
683
711
  const states = {
712
+ ...styledHere?.states,
684
713
  ...fromModule?.states,
685
714
  ...v.base.states,
686
715
  ...t.states,
687
716
  };
688
- const dark = { ...fromModule?.dark, ...t.dark };
717
+ const dark = {
718
+ ...styledHere?.dark,
719
+ ...fromModule?.dark,
720
+ ...t.dark,
721
+ };
689
722
  const layers = [...(fromModule?.layers ?? []), ...v.layers];
690
723
  const parts = { ...fromModule?.parts, ...t.parts };
691
724
  const tree = fromModule?.tree ?? [];
@@ -1545,6 +1578,11 @@ export async function takeCensus(root, opts) {
1545
1578
  * ninguém ter que reportar (dono, 04/08).
1546
1579
  */
1547
1580
  ledger,
1581
+ /**
1582
+ * A ESCALA QUE O `sx` DELES USA, só quando eles declaram uma diferente do default do MUI.
1583
+ * Medida no censo e consumida pela derivação - ver `readSxProps`.
1584
+ */
1585
+ ...(sxSpacing ? { sxSpacing } : {}),
1548
1586
  /**
1549
1587
  * THE SHAPE, AND THE RULE IT BECOMES.
1550
1588
  *
@@ -2305,9 +2343,9 @@ async function resolveReadParts(census, root) {
2305
2343
  ? resolveAnatomy(framed, declared, deps, resolveRef, entry?.root,
2306
2344
  // The component's own sketch, so a node naming itself by index reads
2307
2345
  // the exact class string the census measured (dono, 01/08).
2308
- looks[component]?.sketch)
2346
+ looks[component]?.sketch, census.sxSpacing)
2309
2347
  : derived && derived.read.length > 0
2310
- ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch)
2348
+ ? resolveAnatomy(derived.read, declared, deps, resolveRef, entry?.root, looks[component]?.sketch, census.sxSpacing)
2311
2349
  : Array.isArray(entry?.parts) && entry.parts.length > 0
2312
2350
  ? resolveFlatParts(entry.parts, declared)
2313
2351
  : null;
@@ -4,6 +4,7 @@ import { readToken, resolveRegistry } from "../config.js";
4
4
  import { fileRequest } from "../doctor/requests.js";
5
5
  import { diagnose, scanSource } from "../doctor/scan.js";
6
6
  import { nearestToken, tokenFor } from "../doctor/tokens.js";
7
+ import { repoStateOf } from "../repo-state.js";
7
8
  import { component } from "./component.js";
8
9
  import { loadSystem, walkAll } from "./doctor.js";
9
10
  const send = (msg) => {
@@ -577,7 +578,9 @@ async function validateRecipes(root, entries) {
577
578
  * verdade. Nunca bloqueia a resposta: sem sessão ou sem rede, não conta e não quebra -
578
579
  * telemetria que atrasa a ferramenta é telemetria que alguém desliga.
579
580
  */
580
- function countAgentRead(root, component, action) {
581
+ function countAgentRead(root, component, action,
582
+ /** A versão que está reportando - ver `repoStateOf`. Ausente = não reporta estado. */
583
+ cli) {
581
584
  void (async () => {
582
585
  try {
583
586
  const token = await readToken();
@@ -604,6 +607,7 @@ function countAgentRead(root, component, action) {
604
607
  }
605
608
  if (!slug)
606
609
  return;
610
+ const repo = cli ? await repoStateOf(root, slug, cli) : null;
607
611
  const base = resolveRegistry();
608
612
  await fetch(`${base}/api/telemetry/agent-read`, {
609
613
  method: "POST",
@@ -611,7 +615,20 @@ function countAgentRead(root, component, action) {
611
615
  Authorization: `Bearer ${token}`,
612
616
  "content-type": "application/json",
613
617
  },
614
- body: JSON.stringify({ slug, component, action }),
618
+ /**
619
+ * O ESTADO DO REPO PEGA CARONA AQUI - ver `repoStateOf`.
620
+ *
621
+ * Este ping já é automático e já manda slug/componente/ação em toda leitura de receita
622
+ * pelo agente. "Em que versão eu estou" é o mesmo tipo de sinal, no mesmo momento, e
623
+ * assim a tela tem o número sem depender de o cliente lembrar de rodar `sync` - que é
624
+ * manual de propósito e vai continuar sendo.
625
+ */
626
+ body: JSON.stringify({
627
+ slug,
628
+ component,
629
+ action,
630
+ ...(repo ? { repo } : {}),
631
+ }),
615
632
  });
616
633
  }
617
634
  catch {
@@ -798,7 +815,9 @@ async function addComponent(root, name) {
798
815
  return `${out}\n\nIt is real code in this project now - import it and extend it rather than writing your own.`;
799
816
  }
800
817
  // ── The protocol ─────────────────────────────────────────────────────────────
801
- async function callTool(root, name, args) {
818
+ async function callTool(root, name, args,
819
+ /** A versão que reporta o estado do repo no ping - ver `countAgentRead`. */
820
+ cli) {
802
821
  switch (name) {
803
822
  case "check_file":
804
823
  return text(await checkFile(root, String(args.path ?? ".")));
@@ -807,10 +826,10 @@ async function callTool(root, name, args) {
807
826
  case "list_components":
808
827
  return text(await listComponents(root));
809
828
  case "describe_component":
810
- countAgentRead(root, String(args.name ?? ""), "describe");
829
+ countAgentRead(root, String(args.name ?? ""), "describe", cli);
811
830
  return text(await describeComponent(root, String(args.name ?? "")));
812
831
  case "add_component":
813
- countAgentRead(root, String(args.name ?? ""), "add");
832
+ countAgentRead(root, String(args.name ?? ""), "add", cli);
814
833
  return text(await addComponent(root, String(args.name ?? "")));
815
834
  case "recipe_vocabulary":
816
835
  return text(await recipeVocabulary());
@@ -893,7 +912,12 @@ async function isPromisedByContract(root, name) {
893
912
  * Split out from the stdin loop so the protocol can be exercised without a
894
913
  * pipe: the loop below is framing, this is the protocol.
895
914
  */
896
- export async function handleRequest(root, req) {
915
+ export async function handleRequest(root, req,
916
+ /**
917
+ * A versão que reporta o estado do repo no ping do agente - ver `repoStateOf`. Opcional porque os
918
+ * specs chamam esta função direto, e ali a versão não é o que está sob teste.
919
+ */
920
+ cli) {
897
921
  const id = req.id;
898
922
  if (id === undefined)
899
923
  return null;
@@ -919,7 +943,11 @@ export async function handleRequest(root, req) {
919
943
  case "tools/call": {
920
944
  const name = String(req.params?.name ?? "");
921
945
  const args = (req.params?.arguments ?? {});
922
- return { jsonrpc: "2.0", id, result: await callTool(root, name, args) };
946
+ return {
947
+ jsonrpc: "2.0",
948
+ id,
949
+ result: await callTool(root, name, args, cli),
950
+ };
923
951
  }
924
952
  default:
925
953
  return {
@@ -960,7 +988,7 @@ export async function mcp(opts) {
960
988
  catch {
961
989
  continue; // Unparseable frame with no id: nobody to answer.
962
990
  }
963
- const res = await handleRequest(root, req);
991
+ const res = await handleRequest(root, req, opts.cli);
964
992
  if (res)
965
993
  send(res);
966
994
  }
@@ -4,6 +4,7 @@ import { readToken, resolveRegistry } from "../config.js";
4
4
  import { readEvents } from "../doctor/ledger.js";
5
5
  import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
6
6
  import { body, section } from "../output.js";
7
+ import { repoStateOf } from "../repo-state.js";
7
8
  /**
8
9
  * What each decision means ON THIS MACHINE - the card decides, the sync
9
10
  * enacts. The web never touches the local file; this line is printed right
@@ -87,6 +88,7 @@ export async function sync(opts) {
87
88
  // CHECKED, not because someone remembered to say so (dono, 29/07).
88
89
  const verified = await verifyAndCloseRequests(root);
89
90
  const events = await readEvents(root);
91
+ const repo = opts.cli ? await repoStateOf(root, slug, opts.cli) : null;
90
92
  const requests = await readRequests(root);
91
93
  if (events.length === 0 && requests.length === 0) {
92
94
  console.log(section("Sync"));
@@ -99,7 +101,17 @@ export async function sync(opts) {
99
101
  "content-type": "application/json",
100
102
  Authorization: `Bearer ${token}`,
101
103
  },
102
- body: JSON.stringify({ slug, events, requests }),
104
+ /**
105
+ * O ESTADO DO REPO VAI JUNTO - ver `repoStateOf`. É o `sync` que ELE dispara, então nada
106
+ * passou a ser enviado sem que já fosse: a versão instalada viaja no mesmo pacote que o
107
+ * registro de checagens, e é o que permite a tela dizer "você está em v2 e existe v4".
108
+ */
109
+ body: JSON.stringify({
110
+ slug,
111
+ events,
112
+ requests,
113
+ ...(repo ? { repo } : {}),
114
+ }),
103
115
  }).catch(() => null);
104
116
  if (!res) {
105
117
  console.log(section("Sync"));
@@ -220,6 +220,18 @@ export function gateComponent(input) {
220
220
  because: `\`${name}\` renders \`${adapter}\`'s components and no element of its own - its markup and every pixel are that library's, so there is nothing here for this system to re-theme`,
221
221
  };
222
222
  }
223
+ /**
224
+ * UM COMPONENTE `styled` NÃO TEM MARKUP E É UM COMPONENTE.
225
+ *
226
+ * `const Title = styled.h1\`font-size: 1.5rem\`` não escreve JSX nenhum, então caía em "config":
227
+ * uma constante. Mas ele decide a aparência de um `<h1>` inteiro e o nome dele é o nome do
228
+ * componente - recusá-lo é a mesma falha de classificar por FORMA DO ARQUIVO em vez de pelo que
229
+ * a coisa é (medido no app dele: 9 desses, 100 declarações).
230
+ *
231
+ * Antes do teste de JSX, e não depois, porque o teste de JSX é exatamente o que erra aqui.
232
+ */
233
+ if (STYLED_FACTORY.test(source))
234
+ return { ok: true };
223
235
  if (!HAS_JSX.test(source)) {
224
236
  return {
225
237
  ok: false,
@@ -252,6 +264,8 @@ export function gateComponent(input) {
252
264
  * components", which is the one thing a census must never imply. Every name is
253
265
  * carried; the printer shows the first few per group and says how many more.
254
266
  */
267
+ /** `styled.h1\`…\`` / `styled(Botao)\`…\`` - uma fábrica de componente, não uma constante. */
268
+ const STYLED_FACTORY = /\bstyled(?:\.[a-zA-Z][\w]*|\([^)]{1,60}\))\s*(?:<[^>]{0,120}>)?\s*`/;
255
269
  export const SKIP_LABEL = {
256
270
  route: "routes and framework files",
257
271
  screen: "whole screens, and the frames around them",
@@ -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 { readStyledComponents, readSxProps } from "./style-props.js";
16
17
  import { readInlineStyle, transcribe } from "./transcribe.js";
17
18
  /** `className="…"` e `className={"…"}` - o literal puro. */
18
19
  const CLASS_LITERAL = /className=(?:"([^"]{0,400})"|\{\s*"([^"]{0,400})"\s*\})/g;
@@ -25,10 +26,16 @@ const CALL_LITERAL = /\b(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(([^)]
25
26
  const STRING_IN_CALL = /"([^"]{0,300})"|'([^']{0,300})'/g;
26
27
  /** `style={{ … }}`. */
27
28
  const INLINE = /style=\{\{([^}]{0,400})\}\}/g;
28
- /** `sx={{ … }}`. */
29
- const SX = /sx=\{\{([^}]{0,400})\}\}/g;
30
- /** `styled.div\`…\`` e `styled(X)\`…\``. */
31
- const STYLED = /styled[.(][^`]{0,60}`([^`]{0,400})`/g;
29
+ /**
30
+ * `sx={{ … }}` e `styled.x\`…\``, achados por CHAVE BALANCEADA e sem teto.
31
+ *
32
+ * As duas versões anteriores eram um regex com `[^}]{0,400}` e `[^`]{0,400}`, e as duas mentiam:
33
+ * `sx={{ "&:hover": { … } }}` para no primeiro `}` e não casa, e um template de 24 declarações
34
+ * passa de 400 chars e desaparece. Medido no app dele: 5 dos 58 `sx` e 3 dos 9 `styled` eram
35
+ * invisíveis - num ledger cuja promessa é que nada é descartado em silêncio (05/08).
36
+ */
37
+ const SX_OPEN = /\bsx=\{\{/g;
38
+ const STYLED_OPEN = /\bstyled(?:\.[a-zA-Z][\w]*|\([^)]{1,60}\))\s*(?:<[^>]{0,120}>)?\s*`/g;
32
39
  /**
33
40
  * Um pedaço de linha de estilo e o que o termina: `{` faz do pedaço um seletor, `;` e `}` fazem
34
41
  * dele uma declaração. Ler a linha inteira de uma vez perde `.card { padding: 8px }` numa linha só,
@@ -97,12 +104,55 @@ export function fragmentsOfSource(file, source) {
97
104
  }
98
105
  for (const m of source.matchAll(INLINE))
99
106
  push("inline", m.index ?? 0, m[1] ?? "");
100
- for (const m of source.matchAll(SX))
101
- push("sx", m.index ?? 0, m[1] ?? "", "shape-not-read");
102
- for (const m of source.matchAll(STYLED))
103
- push("styled", m.index ?? 0, m[1] ?? "", "shape-not-read");
107
+ for (const m of source.matchAll(SX_OPEN)) {
108
+ const from = (m.index ?? 0) + m[0].length;
109
+ push("sx", m.index ?? 0, source.slice(from, balanced(source, from, "}")));
110
+ }
111
+ for (const m of source.matchAll(STYLED_OPEN)) {
112
+ const from = (m.index ?? 0) + m[0].length;
113
+ push("styled", m.index ?? 0, source.slice(from, backtick(source, from)));
114
+ }
104
115
  return out;
105
116
  }
117
+ /** O fim de um objeto aberto com duas chaves - `sx={{ … }}` fecha em duas. */
118
+ function balanced(source, from, close) {
119
+ let depth = 2;
120
+ let i = from;
121
+ while (i < source.length && depth > 0) {
122
+ if (source[i] === "{")
123
+ depth += 1;
124
+ else if (source[i] === close)
125
+ depth -= 1;
126
+ i += 1;
127
+ }
128
+ return i - 2;
129
+ }
130
+ /** O backtick que fecha um template, pulando `${…}` e escape. */
131
+ function backtick(source, from) {
132
+ let i = from;
133
+ while (i < source.length) {
134
+ if (source[i] === "\\") {
135
+ i += 2;
136
+ continue;
137
+ }
138
+ if (source[i] === "`")
139
+ return i;
140
+ if (source[i] === "$" && source[i + 1] === "{") {
141
+ let depth = 1;
142
+ i += 2;
143
+ while (i < source.length && depth > 0) {
144
+ if (source[i] === "{")
145
+ depth += 1;
146
+ else if (source[i] === "}")
147
+ depth -= 1;
148
+ i += 1;
149
+ }
150
+ continue;
151
+ }
152
+ i += 1;
153
+ }
154
+ return source.length;
155
+ }
106
156
  /**
107
157
  * Os fragmentos de um arquivo de estilo: uma declaração é um fragmento.
108
158
  *
@@ -229,6 +279,36 @@ export function judgeFragments(seen, declared, admitted) {
229
279
  read: false,
230
280
  reason: "component-not-admitted",
231
281
  };
282
+ case "sx": {
283
+ /**
284
+ * O MESMO LEITOR QUE A RECEITA USA - `readSxProps`. Um segundo julgamento escrito à parte
285
+ * é a maneira mais rápida de o relatório e a receita começarem a discordar.
286
+ */
287
+ const read = readSxProps(f.text);
288
+ return Object.keys(read.declarations).length > 0
289
+ ? { ...f, read: true }
290
+ : {
291
+ ...f,
292
+ read: false,
293
+ /**
294
+ * Nada lido de um `sx` que EXISTE só acontece por valor: um ternário, um objeto
295
+ * responsivo, ou um seletor que aponta para o interior de uma biblioteca. A forma é
296
+ * lida - dizer `shape-not-read` aqui mandaria alguém reescrever o leitor que
297
+ * acabou de rodar.
298
+ */
299
+ reason: "value-not-read",
300
+ };
301
+ }
302
+ case "styled": {
303
+ const read = readStyledComponents(`const X = styled.div\`${f.text}\`;`);
304
+ const first = read[0];
305
+ const got = Object.keys(first?.base ?? {}).length +
306
+ Object.keys(first?.dark ?? {}).length +
307
+ Object.keys(first?.states ?? {}).length;
308
+ return got > 0
309
+ ? { ...f, read: true }
310
+ : { ...f, read: false, reason: "value-not-read" };
311
+ }
232
312
  default:
233
313
  return { ...f, read: false, reason: "shape-not-read" };
234
314
  }
@@ -431,6 +431,29 @@ function closeOf(source, open) {
431
431
  * `className={styles.content}` e `className={s.content}` - a forma que um app de CSS Modules
432
432
  * usa em todo elemento. O identificador é o import do module, e a propriedade é a classe.
433
433
  */
434
+ /**
435
+ * O CORPO DE `sx={{ … }}`, por chave balanceada.
436
+ *
437
+ * Um regex `[^}]` para no primeiro `}` e perde `sx={{ "&:hover": { … } }}` inteiro, que é
438
+ * justamente a forma em que a declaração aninhada mora.
439
+ */
440
+ function sxBody(body) {
441
+ const at = body.search(/\bsx=\{\{/);
442
+ if (at === -1)
443
+ return undefined;
444
+ const from = at + (/\bsx=\{\{/.exec(body)?.[0].length ?? 0);
445
+ let depth = 2;
446
+ let i = from;
447
+ while (i < body.length && depth > 0) {
448
+ if (body[i] === "{")
449
+ depth += 1;
450
+ else if (body[i] === "}")
451
+ depth -= 1;
452
+ i += 1;
453
+ }
454
+ const text = body.slice(from, i - 2).trim();
455
+ return text || undefined;
456
+ }
434
457
  const MODULE_CLASS = /className=\{\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\}/;
435
458
  export function sketchOf(source, name) {
436
459
  /**
@@ -528,6 +551,10 @@ export function sketchOf(source, name) {
528
551
  const mod = MODULE_CLASS.exec(event.body);
529
552
  if (mod)
530
553
  node.moduleClass = mod[2];
554
+ /** A prop de estilo que ele carrega - ver `sx`. */
555
+ const sx = sxBody(event.body);
556
+ if (sx)
557
+ node.sx = sx;
531
558
  // What has to be true for this element to exist at all - see `guardAt`.
532
559
  const guard = guardAt(source, event.at);
533
560
  if (guard) {
Binary file
package/dist/index.js CHANGED
@@ -235,7 +235,8 @@ async function main() {
235
235
  // Long-lived: it owns stdin/stdout until the client closes the pipe, so
236
236
  // it must not be reached by anything that prints.
237
237
  case "mcp":
238
- await mcp({ dir });
238
+ // A versão que reporta o estado do repo no ping do agente - ver `repoStateOf`.
239
+ await mcp({ dir, cli: CLI_VERSION });
239
240
  return;
240
241
  case "doctor":
241
242
  // positional paths scope the READING (the system is still found from the
@@ -421,7 +422,8 @@ async function main() {
421
422
  await clean({ dir, force: flags.force === true });
422
423
  break;
423
424
  case "sync":
424
- await sync({ dir, registry });
425
+ // A versão que reporta o estado do repo - ver `repoStateOf`.
426
+ await sync({ dir, registry, cli: CLI_VERSION });
425
427
  break;
426
428
  case "request": {
427
429
  // `synthesisui request` lists; `request component --name x --for "..."`
@@ -0,0 +1,64 @@
1
+ /**
2
+ * O ESTADO DO REPO DELE, medido no repo dele.
3
+ *
4
+ * A plataforma sabe o que publicou e não sabia em que versão o repo do cliente está - então a tela
5
+ * não podia escrever "você está em v2 e existe v4", porque ela não tinha o primeiro número (dono,
6
+ * 05/08).
7
+ *
8
+ * O `.lock` de cada DS instalado (`_synthesisui/ds/<slug>/.lock`) tem esse número desde sempre. O
9
+ * que faltava era ele viajar.
10
+ *
11
+ * E ELE VIAJA NOS CANAIS QUE JÁ FALAM, nunca num novo:
12
+ *
13
+ * `sync` manual, e é assim que a landing promete. Leva o registro local
14
+ * inteiro quando ELE decide.
15
+ * ping do MCP automático, e já existe: toda leitura de receita pelo agente já
16
+ * manda slug/componente/ação. "Em que versão eu estou" é o mesmo
17
+ * tipo de sinal, no mesmo momento.
18
+ *
19
+ * Nada passou a ser enviado que já não fosse, e nenhum canal novo foi aberto. Transformar o `sync`
20
+ * em telemetria silenciosa mudaria o contrato de privacidade da landing, e isso não se faz de lado.
21
+ *
22
+ * NADA POR PESSOA. Nem nome de branch, nem autor, nem caminho de arquivo: só a versão instalada, a
23
+ * versão do CLI, e quando o doctor rodou. Um painel que ranqueia desenvolvedor transforma a
24
+ * checagem em adversário, e adversário é desinstalado.
25
+ */
26
+ import { readFile } from "node:fs/promises";
27
+ import { join } from "node:path";
28
+ import { readEvents } from "./doctor/ledger.js";
29
+ /**
30
+ * O estado de um slug neste projeto, ou nada quando não há `.lock`.
31
+ *
32
+ * Silencioso de propósito: um projeto sem o DS instalado não é um erro a reportar, e um `.lock`
33
+ * ilegível não vale derrubar um `sync` que existe para levar o registro de checagens.
34
+ */
35
+ export async function repoStateOf(projectRoot, slug, cli) {
36
+ const lock = await readJson(join(projectRoot, "_synthesisui", "ds", slug, ".lock"));
37
+ /**
38
+ * O ÚLTIMO DOCTOR, PELO MESMO LEITOR QUE O `sync` USA (`readEvents`). Abrir o arquivo por conta
39
+ * própria aqui seria uma segunda implementação do formato do registro - e a primeira coisa que
40
+ * ela faria é discordar da outra sobre o que é uma linha válida.
41
+ */
42
+ const doctor = (await readEvents(projectRoot))
43
+ .filter((e) => e.kind === "doctor" && e.at)
44
+ .sort((a, b) => String(b.at).localeCompare(String(a.at)))[0];
45
+ const payload = {
46
+ ...(typeof lock?.version === "number" ? { installed: lock.version } : {}),
47
+ ...(lock?.adopted === true ? { adopted: true } : {}),
48
+ cli,
49
+ ...(doctor?.at ? { doctorAt: doctor.at } : {}),
50
+ ...(typeof doctor?.phantoms === "number"
51
+ ? { drifts: doctor.phantoms }
52
+ : {}),
53
+ };
54
+ /** Só o `cli` não é estado: sem nada do repo, não há o que reportar. */
55
+ return Object.keys(payload).length > 1 ? payload : null;
56
+ }
57
+ async function readJson(path) {
58
+ try {
59
+ return JSON.parse(await readFile(path, "utf8"));
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.136",
3
+ "version": "0.16.138",
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": {