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.
- package/dist/commands/align.js +57 -1
- package/dist/doctor/components-scan.js +167 -15
- package/package.json +1 -1
package/dist/commands/align.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
|
11
|
-
/**
|
|
12
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
/**
|
|
187
|
-
|
|
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(
|
|
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
|
-
|
|
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(([
|
|
243
|
-
|
|
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