synthesisui 0.16.197 → 0.16.199

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.
@@ -188,6 +188,43 @@ opts = {}) {
188
188
  * Melhor esforço: offline, sem sessão, ou registry fora do ar não produzem linha nenhuma. Uma
189
189
  * verificação de abertura não pode transformar um voo sem internet num aviso.
190
190
  */
191
+ /**
192
+ * O QUE DIZER QUANDO A PLATAFORMA ENDIREITOU ALGO - e como voltar ao que se estava fazendo.
193
+ *
194
+ * O pedido do dono, em 10/08: o agente checa se mudou alguma coisa, diz o comando, e devolve a linha
195
+ * que retoma a sessão de onde ela parou. A retomada não é conforto - sem ela, a pessoa roda o
196
+ * comando, perde o contexto do que estava fazendo, e o custo do conserto passa a ser a tarefa que ela
197
+ * estava no meio.
198
+ */
199
+ const REPAIR_SAID = {
200
+ spacing: "spacing values in your system were connected to the steps your own scale declares",
201
+ orphans: "recipes your measured scope no longer declares were removed",
202
+ roles: "colour roles were pointed at the ramps your repository declares",
203
+ reground: "the foundation was rebuilt from your census",
204
+ interpretation: "your census was re-read with a newer interpretation",
205
+ };
206
+ export function repairSaid(body, lock) {
207
+ if (!lock.slug)
208
+ return null;
209
+ if (!body.repairedAt)
210
+ return null;
211
+ /**
212
+ * SEM CARIMBO LOCAL, SILÊNCIO. Um repo instalado antes de `installedAt` existir não tem com o que
213
+ * comparar, e avisar sempre transformaria este aviso em ruído permanente - que é como um aviso
214
+ * deixa de ser lido.
215
+ */
216
+ if (!lock.fetchedAt)
217
+ return null;
218
+ if (Date.parse(body.repairedAt) <= Date.parse(lock.fetchedAt))
219
+ return null;
220
+ const what = body.repaired ? REPAIR_SAID[body.repaired] : undefined;
221
+ return {
222
+ says: `Something was put right in "${lock.slug}" on the platform after this repo installed it${what ? `: ${what}` : ""}. The files here still hold what was measured before that.`,
223
+ run: `npx synthesisui upgrade ${lock.slug}`,
224
+ /** E o caminho de volta para o que ela estava fazendo. */
225
+ then: "claude --continue --dangerously-skip-permissions",
226
+ };
227
+ }
191
228
  export async function versionBehind(root, opts = {}) {
192
229
  const locks = (await locksIn(root)).filter((l) => l.slug && !l.adopted);
193
230
  const lock = locks[0];
@@ -210,6 +247,24 @@ export async function versionBehind(root, opts = {}) {
210
247
  const body = (await res.json().catch(() => null));
211
248
  if (!body?.version)
212
249
  return null;
250
+ /**
251
+ * ALGO FOI ENDIREITADO NA PLATAFORMA DESDE QUE ESTE REPO INSTALOU.
252
+ *
253
+ * `version` responde "publicaram algo mais novo". O Apply do Studio não publica - ele escreve o
254
+ * rascunho -, então a versão fica parada e este comando ficava mudo sobre uma mudança real no
255
+ * design system dele (dono, 10/08). Quem descobre por acaso duas semanas depois é quem deixa de
256
+ * confiar no que a esteira diz.
257
+ *
258
+ * A frase é escrita AQUI, com o texto de hoje: só a data e a chave viajam pela rede. Uma frase que
259
+ * atravessa a rede congela na versão que a mandou.
260
+ *
261
+ * E ela vem ANTES do teste de versão de propósito: quando os dois são verdade, o reparo é a causa e
262
+ * a versão é a consequência - dizer a consequência primeiro manda a pessoa rodar o comando sem
263
+ * saber por quê.
264
+ */
265
+ const repaired = repairSaid(body, lock);
266
+ if (repaired)
267
+ return repaired;
213
268
  if (body.version > lock.version)
214
269
  return {
215
270
  says: `v${body.version} of "${lock.slug}" is published and this repo is on v${lock.version}. The CSS here and the rules your agent reads are both v${lock.version} - they move together, which is why this is worth saying rather than applying.`,
@@ -273,7 +328,8 @@ from = "session") {
273
328
  : from === "shell"
274
329
  ? "This repo is out of alignment with the design system that governs it:"
275
330
  : "Before this session starts, this environment is out of alignment with the design system that governs it:",
276
- ...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}`),
331
+ /** O `then` sai numa linha própria e rotulada: um segundo comando solto lê como parte do primeiro. */
332
+ ...items.map((m) => ` - ${m.says}${m.run ? `\n ${m.run}` : ""}${m.then ? `\n then, to pick up where you left off:\n ${m.then}` : ""}`),
277
333
  ].join("\n");
278
334
  }
279
335
  /**
@@ -2,14 +2,146 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  /**
4
4
  * Opens a JSX element whose name is capitalised - which is React's own rule for
5
- * "this is a component, not an html tag".
5
+ * "this is a component, not an html tag". The name allows dots (`Card.Header`).
6
6
  *
7
- * The name allows dots (`Card.Header`) and the body is captured lazily up to
8
- * the first `>` that is not inside a quoted value.
7
+ * SÓ A ABERTURA. O corpo NÃO vem daqui - ver `bodyOf`.
9
8
  */
10
- const ELEMENT = /<([A-Z][A-Za-z0-9_]*(?:\.[A-Z][A-Za-z0-9_]*)*)(\s[^>]*?)?\/?>/gs;
11
- /** `variant="primary"` or `variant={"primary"}` - a literal a person typed. */
12
- const LITERAL_PROP = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{?\s*["']([^"']*)["']\s*\}?/g;
9
+ const OPENS = /<([A-Z][A-Za-z0-9_]*(?:\.[A-Z][A-Za-z0-9_]*)*)/g;
10
+ /**
11
+ * ONDE A TAG TERMINA DE VERDADE - e isto era uma varredura de caractere.
12
+ *
13
+ * O corpo era `(\s[^>]*?)?`: para no primeiro `>` que aparecer, sem saber o que é aspa e
14
+ * sem saber o que é aninhamento. Medido no repositório do dono em 10/08, isso produzia
15
+ * DOIS defeitos que pareciam não ter relação:
16
+ *
17
+ * <WidgetCard rightTitleSlot={ <Button variant="neutral" onClick={() => …
18
+ * os atributos do FILHO caem no corpo do PAI - o censo registrou `variant: neutral`
19
+ * no WidgetCard, que declara `default | left-border | full-border`. A tela reportou
20
+ * "uso fora do contrato" sobre um uso que nunca existiu.
21
+ *
22
+ * value="… Domain 'sg.futurescope.com' … the 'Sending' tab …"
23
+ * a aspa simples dentro da dupla fechava o valor cedo, o resto da frase ficava no
24
+ * corpo, e o passe de booleano nu colhia palavra por palavra: `the`, `following`,
25
+ * `registrar`, `domains`. Dezenove palavras viraram props do Textarea.
26
+ *
27
+ * Um defeito INVENTADO é pior que um não achado: o não achado custa uma lacuna, o
28
+ * inventado custa a confiança na tela inteira. Por isso a resposta não é acrescentar
29
+ * casos à regex - é um scanner que sabe o que está lendo, e que fecha por construção.
30
+ *
31
+ * Devolve o corpo e onde continuar a varredura. Uma tag sem fechamento (arquivo cortado,
32
+ * TypeScript genérico que não era elemento) devolve `null` e não vira uso.
33
+ */
34
+ function bodyOf(source, from) {
35
+ let quote = null;
36
+ let depth = 0;
37
+ for (let i = from; i < source.length; i++) {
38
+ const ch = source[i];
39
+ if (quote) {
40
+ /** Escape dentro de string: `\"` não fecha nada. */
41
+ if (ch === "\\")
42
+ i += 1;
43
+ else if (ch === quote)
44
+ quote = null;
45
+ continue;
46
+ }
47
+ if (ch === '"' || ch === "'" || ch === "`") {
48
+ quote = ch;
49
+ continue;
50
+ }
51
+ if (ch === "{")
52
+ depth += 1;
53
+ else if (ch === "}")
54
+ depth -= 1;
55
+ /** Em profundidade zero e fora de aspas: aqui a tag realmente fecha. */ else if (ch === ">" &&
56
+ depth <= 0)
57
+ return {
58
+ body: withoutExpressions(source.slice(from, i)).replace(/\/$/, ""),
59
+ end: i + 1,
60
+ };
61
+ }
62
+ return null;
63
+ }
64
+ /**
65
+ * O QUE ESTÁ DENTRO DE CHAVES NÃO É PROP DESTA TAG - e sem isto o corpo termina no lugar certo
66
+ * e ainda entrega o filho errado.
67
+ *
68
+ * `<WidgetCard rightTitleSlot={<Button variant="neutral" />} />` fecha corretamente no `/>` do
69
+ * WidgetCard: o `>` do Button está em profundidade 1. Mas o corpo continua carregando
70
+ * `variant="neutral"`, porque a atribuição do filho mora dentro da expressão do slot.
71
+ *
72
+ * Então a expressão vira espaço - e o que ela contém não se perde: o `<Button` dentro dela é
73
+ * encontrado pela própria varredura, no lugar dele, com as props dele.
74
+ *
75
+ * COM UMA EXCEÇÃO, E ELA É MEDIDA: `variant={"info"}` é o MESMO literal que `variant="info"`, e um
76
+ * spec já dizia isso. A primeira versão deste passe apagava os dois e a suíte reprovou - eu tinha
77
+ * escrito no comentário que o `LITERAL_PROP` via o corpo cru antes, o que o código não fazia. Uma
78
+ * chave contendo só um literal volta como valor; qualquer outra coisa dentro é expressão.
79
+ */
80
+ function withoutExpressions(body) {
81
+ let out = "";
82
+ let inner = "";
83
+ let depth = 0;
84
+ let quote = null;
85
+ /** Fecha uma expressão: um literal puro é VALOR e volta; o resto é do que estiver dentro. */
86
+ const close = () => {
87
+ out += /^\s*(["'`])(?:\\.|(?!\1)[^\\])*\1\s*$/.test(inner)
88
+ ? `{${inner}}`
89
+ : " ";
90
+ inner = "";
91
+ };
92
+ for (let i = 0; i < body.length; i++) {
93
+ const ch = body[i];
94
+ const keep = (text) => {
95
+ if (depth === 0)
96
+ out += text;
97
+ else
98
+ inner += text;
99
+ };
100
+ if (quote) {
101
+ if (ch === "\\") {
102
+ keep(body.slice(i, i + 2));
103
+ i += 1;
104
+ }
105
+ else {
106
+ keep(ch);
107
+ if (ch === quote)
108
+ quote = null;
109
+ }
110
+ continue;
111
+ }
112
+ if (ch === '"' || ch === "'" || ch === "`") {
113
+ quote = ch;
114
+ keep(ch);
115
+ continue;
116
+ }
117
+ if (ch === "{") {
118
+ depth += 1;
119
+ if (depth > 1)
120
+ inner += ch;
121
+ continue;
122
+ }
123
+ if (ch === "}") {
124
+ depth -= 1;
125
+ if (depth === 0)
126
+ close();
127
+ else if (depth > 0)
128
+ inner += ch;
129
+ else
130
+ depth = 0;
131
+ continue;
132
+ }
133
+ keep(ch);
134
+ }
135
+ return out;
136
+ }
137
+ /**
138
+ * `variant="primary"` ou `variant={'primary'}` - um literal que alguém digitou.
139
+ *
140
+ * A ASPA CASA COM ELA MESMA (`(["'])…\2`), e não com qualquer uma. `["']([^"']*)["']`
141
+ * tratava as duas como intercambiáveis, então um valor com apóstrofo dentro truncava - e o
142
+ * que sobrava virava prop. Ver `bodyOf`.
143
+ */
144
+ const LITERAL_PROP = /([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*\{?\s*(["'])((?:\\.|(?!\2)[^\\])*)\2\s*\}?/g;
13
145
  /** `dense` with no value is a boolean prop set to true, and that IS a literal
14
146
  * decision worth counting. */
15
147
  const BARE_PROP = /(^|\s)([a-z][a-zA-Z0-9_]*)(?=\s|$)/g;
@@ -171,8 +303,12 @@ internal = []) {
171
303
  return;
172
304
  const owners = importedNames(source, internal);
173
305
  const private_ = localOnlyNames(source);
174
- ELEMENT.lastIndex = 0;
175
- for (const m of source.matchAll(ELEMENT)) {
306
+ /**
307
+ * Abertura por regex, corpo por SCANNER - ver `bodyOf`. A regex só encontra `<Nome`; onde a
308
+ * tag termina é uma pergunta que só um scanner com estado responde.
309
+ */
310
+ OPENS.lastIndex = 0;
311
+ for (const m of source.matchAll(OPENS)) {
176
312
  if (m.index != null && looksLikeType(source, m.index))
177
313
  continue;
178
314
  const name = m[1];
@@ -180,11 +316,25 @@ internal = []) {
180
316
  // shadow nothing and still bind a name it also imports.
181
317
  if (!owners.has(name) && private_.has(name.split(".")[0]))
182
318
  continue;
183
- const body = m[2] ?? "";
319
+ const read = bodyOf(source, (m.index ?? 0) + m[0].length);
320
+ /** Tag sem fechamento não é uso: arquivo cortado, ou um genérico que não era elemento. */
321
+ if (!read)
322
+ continue;
323
+ const body = read.body;
184
324
  // A dotted name belongs to whoever exported its root (`Popover.Root`).
185
325
  const owner = owners.get(name) ?? owners.get(name.split(".")[0]);
186
- /** Ver `ComponentTally`: dois `Button` de pacotes diferentes são duas linhas, não uma. */
187
- let hit = tally.get(name);
326
+ /**
327
+ * A ORIGEM ENTRA NA CHAVE - a lei 13 do CLAUDE.md, que este comentário já afirmava e o código
328
+ * não cumpria: a chave era só o nome.
329
+ *
330
+ * Medido no repositório do dono em 10/08: os apps usam o `Button` DELE e o do MUI, e as
331
+ * variantes do MUI (`contained`, `outlined`, `text`) caíam na conta do Button dele, que declara
332
+ * `ocean`, `royal`, `neutral`. O mesmo com `CircularProgress` e `determinate`. E era a mesma
333
+ * causa do `own: true` valer em 7 de 128: uma linha que mistura duas origens nunca tem TODAS
334
+ * as origens de dentro.
335
+ */
336
+ const key = `${name}\u0000${owner ?? ""}`;
337
+ let hit = tally.get(key);
188
338
  if (!hit) {
189
339
  hit = {
190
340
  count: 0,
@@ -194,7 +344,7 @@ internal = []) {
194
344
  ...(owner && owner !== OWN ? { from: owner } : {}),
195
345
  origins: new Set(),
196
346
  };
197
- tally.set(name, hit);
347
+ tally.set(key, hit);
198
348
  }
199
349
  /** TODA origem vista, e não só a primeira - ver `origins` no tipo. */
200
350
  if (owner)
@@ -204,7 +354,8 @@ internal = []) {
204
354
  LITERAL_PROP.lastIndex = 0;
205
355
  const named = new Set();
206
356
  for (const p of body.matchAll(LITERAL_PROP)) {
207
- const [, prop, value] = p;
357
+ /** O grupo 2 é a ASPA (a backreference); o valor é o 3 - ver `LITERAL_PROP`. */
358
+ const [, prop, , value] = p;
208
359
  named.add(prop);
209
360
  if (NOT_A_DECISION.test(prop))
210
361
  continue;
@@ -239,8 +390,9 @@ internal = []) {
239
390
  /** The inventory, commonest first. */
240
391
  export function tallyToInventory(tally, max = 80) {
241
392
  return [...tally.entries()]
242
- .map(([name, v]) => ({
243
- name,
393
+ .map(([key, v]) => ({
394
+ /** A chave carrega a origem para separar as linhas - o nome é a primeira metade dela. */
395
+ name: key.split("\u0000")[0],
244
396
  ...(v.from ? { from: v.from } : {}),
245
397
  /** Só dele quando TODA aparição veio de dentro - ver `origins` e `OWN`. */
246
398
  ...(v.origins.size > 0 && [...v.origins].every((o) => o === OWN)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.197",
3
+ "version": "0.16.199",
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": {