synthesisui 0.16.220 → 0.16.225
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 +31 -0
- package/dist/commands/connect.js +12 -18
- package/dist/commands/doctor.js +73 -4
- package/dist/commands/mcp.js +41 -0
- package/dist/commands/request.js +20 -5
- package/dist/commands/sync.js +61 -3
- package/dist/doctor/requests.js +10 -1
- package/dist/doctor/root-size.js +101 -0
- package/dist/doctor/scan.js +1 -1
- package/dist/doctor/tokens.js +41 -3
- package/dist/install-marks.js +7 -1
- package/dist/skill-adapt.js +262 -0
- package/dist/skills.js +39 -0
- package/package.json +1 -1
package/dist/commands/align.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readToken, resolveRegistry } from "../config.js";
|
|
|
7
7
|
import { unsentEvents } from "../doctor/ledger.js";
|
|
8
8
|
import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
|
|
9
9
|
import { measuredScope } from "../measured-scope.js";
|
|
10
|
+
import { SKILLS } from "../skills.js";
|
|
10
11
|
/**
|
|
11
12
|
* QUAL CLI MEDIU O CENSO EM DISCO - e era o `reader`, um inteiro, até 11/08.
|
|
12
13
|
*
|
|
@@ -150,6 +151,36 @@ opts = {}) {
|
|
|
150
151
|
*/
|
|
151
152
|
run: "npx synthesisui sync",
|
|
152
153
|
});
|
|
154
|
+
/**
|
|
155
|
+
* A SKILL QUE ESTE CLI TEM E ESTE REPO NÃO - e sem isto ela nunca chegaria.
|
|
156
|
+
*
|
|
157
|
+
* `connect` é idempotente e atualiza sozinho, mas ninguém roda um comando de novo sem motivo: uma
|
|
158
|
+
* skill nova ficava esperando o acaso. Aqui ela vira uma linha do alinho, que é a única coisa que
|
|
159
|
+
* fala com a pessoa sem ela pedir.
|
|
160
|
+
*
|
|
161
|
+
* Compara o CONTEÚDO e não só a existência: uma skill velha descreve um fluxo que este CLI já não
|
|
162
|
+
* tem, e isso é pior que não ter skill nenhuma - quem lê não tem como perceber.
|
|
163
|
+
*/
|
|
164
|
+
const installedSkills = await Promise.all(SKILLS.map((skill) => readFile(join(root, skill.path), "utf8").catch(() => null)));
|
|
165
|
+
/**
|
|
166
|
+
* E SÓ FALA COM QUEM JÁ TEM ALGUMA - senão isto vira o alarme que ensina uma palavra nova a quem
|
|
167
|
+
* não pediu.
|
|
168
|
+
*
|
|
169
|
+
* Um repositório sem skill nenhuma nunca ligou o agente, e pode nem usar Claude Code. Dizer a essa
|
|
170
|
+
* pessoa que "uma skill está faltando" é nomear uma ausência que ela escolheu, e `align` é a única
|
|
171
|
+
* superfície que fala sem ser chamada - a regra dela é ficar calada no estado saudável.
|
|
172
|
+
*
|
|
173
|
+
* Com uma instalada, o silêncio passa a ser o erro: ela ligou o agente, e uma skill nova ficaria
|
|
174
|
+
* esperando o acaso de alguém rodar `connect` de novo.
|
|
175
|
+
*/
|
|
176
|
+
const staleSkills = SKILLS.filter((skill, i) => installedSkills[i] !== skill.source).map((skill) => skill.label);
|
|
177
|
+
if (installedSkills.some((have) => have != null) && staleSkills.length > 0)
|
|
178
|
+
out.push({
|
|
179
|
+
says: staleSkills.length === 1
|
|
180
|
+
? `${staleSkills[0]} is missing or older than this CLI - it is a skill your agent can invoke, and it is not here.`
|
|
181
|
+
: `${staleSkills.length} skills are missing or older than this CLI (${staleSkills.join(", ")}) - your agent can invoke them, and they are not here.`,
|
|
182
|
+
run: "npx synthesisui connect",
|
|
183
|
+
});
|
|
153
184
|
/**
|
|
154
185
|
* SÓ O QUE NÃO SUBIU - ver `unsentEvents`. Isto contava o arquivo inteiro, e como o ledger é
|
|
155
186
|
* append-only e o `sync` manda tudo (a plataforma deduplica), a linha nunca mais saía da tela e o
|
package/dist/commands/connect.js
CHANGED
|
@@ -6,8 +6,7 @@ import { resolveRegistry } from "../config.js";
|
|
|
6
6
|
import { body, paint, section, snippet } from "../output.js";
|
|
7
7
|
import { readShellAnswer, rememberShellNo } from "../shell-answer.js";
|
|
8
8
|
import { existingRc, hasHook, pinnedInHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
|
|
9
|
-
import {
|
|
10
|
-
import { INIT_SKILL, INIT_SKILL_PATH } from "../skill-init.js";
|
|
9
|
+
import { SKILLS } from "../skills.js";
|
|
11
10
|
import { add } from "./add.js";
|
|
12
11
|
import { reportWhatIsLeft } from "./align.js";
|
|
13
12
|
import { ci } from "./ci.js";
|
|
@@ -293,24 +292,19 @@ export async function connect(opts) {
|
|
|
293
292
|
* something change" - so nobody has to remember a second command.
|
|
294
293
|
*/
|
|
295
294
|
/**
|
|
296
|
-
* AS
|
|
295
|
+
* AS TRÊS SKILLS, e o `/sui-init` é a que importa mais aqui: é a PRIMEIRA CORRIDA, e uma skill que
|
|
297
296
|
* só aparece depois de reiniciar o editor não serve para a primeira corrida de ninguém. Ela vem no
|
|
298
|
-
* mesmo `connect` que ainda vai pedir o reinício - então quando a pessoa reabre, as
|
|
297
|
+
* mesmo `connect` que ainda vai pedir o reinício - então quando a pessoa reabre, as três existem.
|
|
298
|
+
*
|
|
299
|
+
* A TERCEIRA É DE MANUTENÇÃO, e ela é de outra natureza: as duas primeiras são de ENTRADA e rodam
|
|
300
|
+
* uma vez. `/sui-adapt` responde a pergunta do dia seguinte - *"isso aqui está de acordo com o meu
|
|
301
|
+
* design system?"* -, apontando para um componente, toda semana ou depois de mexer em alguma coisa.
|
|
302
|
+
*
|
|
303
|
+
* Ela quase ficou de fora daqui, e teria sido o erro de sempre: uma skill que só existe no NOSSO
|
|
304
|
+
* repositório é uma skill que só nós rodamos, e o caso de uso inteiro dela é o cliente rodando
|
|
305
|
+
* sozinho. Meia jornada não é meio valor.
|
|
299
306
|
*/
|
|
300
|
-
const skills =
|
|
301
|
-
{
|
|
302
|
-
path: INIT_SKILL_PATH,
|
|
303
|
-
source: INIT_SKILL,
|
|
304
|
-
label: "/sui-init",
|
|
305
|
-
what: "the first run, start to finish",
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
path: IMPORT_SKILL_PATH,
|
|
309
|
-
source: IMPORT_SKILL,
|
|
310
|
-
label: "/sui-import-ds",
|
|
311
|
-
what: "the import, orchestrated",
|
|
312
|
-
},
|
|
313
|
-
];
|
|
307
|
+
const skills = SKILLS;
|
|
314
308
|
/**
|
|
315
309
|
* A PASTA VELHA SAI, e isto é obrigatório numa renomeação de skill distribuída.
|
|
316
310
|
*
|
package/dist/commands/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { findFrozenBindings } from "../doctor/frozen.js";
|
|
|
11
11
|
import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, } from "../doctor/ledger.js";
|
|
12
12
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
13
13
|
import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
14
|
+
import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
|
|
14
15
|
import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
|
|
15
16
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
16
17
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
@@ -130,7 +131,34 @@ export async function* walkAll(roots) {
|
|
|
130
131
|
* and the recipes `add` put next to them in `design-system.json`. Those
|
|
131
132
|
* recipes are why the component pass can exist at all - a linter has no idea
|
|
132
133
|
* what `ds-button` promised. */
|
|
133
|
-
|
|
134
|
+
/**
|
|
135
|
+
* COMO CADA PEDIDO SE LÊ NUMA LINHA - e o `else` desta expressão mentia.
|
|
136
|
+
*
|
|
137
|
+
* Ela dizia `component "<nome>"` para tudo que não fosse token, então um pedido de REGRA aparecia no
|
|
138
|
+
* doctor como se fosse um componente pedido. Uma tela que rotula errado é pior que uma que não
|
|
139
|
+
* rotula: quem lê decide em cima do rótulo.
|
|
140
|
+
*/
|
|
141
|
+
function requestLabel(r) {
|
|
142
|
+
if (r.kind === "token")
|
|
143
|
+
return `token ${r.value} as ${r.name}`;
|
|
144
|
+
if (r.kind === "rule")
|
|
145
|
+
return `rule "${r.name}"`;
|
|
146
|
+
return `component "${r.name}"`;
|
|
147
|
+
}
|
|
148
|
+
export async function loadSystem(root,
|
|
149
|
+
/**
|
|
150
|
+
* A RAIZ JÁ MEDIDA, quando quem chama já varreu as folhas de estilo.
|
|
151
|
+
*
|
|
152
|
+
* O `doctor` mede (ele já anda no repositório inteiro) e passa. O `hook` NÃO mede, e isso é uma
|
|
153
|
+
* escolha declarada: ele roda depois de cada escrita com orçamento de ~50ms, e varrer css a cada
|
|
154
|
+
* edição trocaria um relatório instantâneo por um que a pessoa desliga.
|
|
155
|
+
*
|
|
156
|
+
* A consequência, dita em voz alta: num projeto que redefine a raiz (`html { font-size: 62.5% }`),
|
|
157
|
+
* o hook compara px e rem por 16 e deixa de sugerir algumas trocas que o `doctor` sugere. Ele erra
|
|
158
|
+
* para o lado de oferecer MENOS, nunca de oferecer a troca errada - e o `--fix`, que é quem escreve,
|
|
159
|
+
* vem sempre do `doctor`.
|
|
160
|
+
*/
|
|
161
|
+
measured) {
|
|
134
162
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
135
163
|
let slugs;
|
|
136
164
|
try {
|
|
@@ -248,7 +276,12 @@ export async function loadSystem(root) {
|
|
|
248
276
|
}
|
|
249
277
|
}
|
|
250
278
|
return {
|
|
251
|
-
table: buildTable({
|
|
279
|
+
table: buildTable({
|
|
280
|
+
css,
|
|
281
|
+
lock,
|
|
282
|
+
source: adopted ? "adopted" : "installed",
|
|
283
|
+
...(measured ? { rootPx: measured.px, rootFrom: measured.from } : {}),
|
|
284
|
+
}),
|
|
252
285
|
recipes,
|
|
253
286
|
documents,
|
|
254
287
|
requires,
|
|
@@ -269,6 +302,19 @@ export async function loadSystem(root) {
|
|
|
269
302
|
* Reads stylesheets only, and only when nothing of ours is installed, so the
|
|
270
303
|
* common path pays nothing for it.
|
|
271
304
|
*/
|
|
305
|
+
/** As folhas de estilo dele, para a medição da raiz. Só css - nada de `.tsx`. */
|
|
306
|
+
async function sheetsIn(roots) {
|
|
307
|
+
const out = [];
|
|
308
|
+
for await (const file of walkAll(roots)) {
|
|
309
|
+
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
310
|
+
continue;
|
|
311
|
+
const css = await readFile(file, "utf8").catch(() => "");
|
|
312
|
+
/** Só carrega adiante o que pode conter a declaração - o resto é peso à toa. */
|
|
313
|
+
if (/(?:html|:root)[^{]*\{[^}]*font-size/i.test(css))
|
|
314
|
+
out.push({ file: relative(roots[0] ?? file, file) || file, css });
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
272
318
|
async function harvestOwnTokens(roots) {
|
|
273
319
|
let css = "";
|
|
274
320
|
for await (const file of walkAll(roots)) {
|
|
@@ -469,7 +515,18 @@ export async function doctor(opts) {
|
|
|
469
515
|
const fullRun = (opts.scopes ?? []).length === 0;
|
|
470
516
|
/** A intenção ordena o relatório, e a precedência é flag > config > default. */
|
|
471
517
|
const intent = intentOf(await readProjectConfig(root), opts.intent);
|
|
472
|
-
|
|
518
|
+
/**
|
|
519
|
+
* A RAIZ, MEDIDA ANTES DE COMPARAR VALOR NENHUM - ver `rootSizeOf`.
|
|
520
|
+
*
|
|
521
|
+
* `4px` e `0.25rem` só são o mesmo valor se alguém disser quantos pixels vale 1rem AQUI, e supor 16
|
|
522
|
+
* num projeto que escreve `html { font-size: 62.5% }` erraria em todos os lugares de uma vez, com a
|
|
523
|
+
* confiança de quem acertou. Uma passada barata: só folhas de estilo, e só o bloco `html`/`:root`.
|
|
524
|
+
*/
|
|
525
|
+
const rootSize = rootSizeOf(await sheetsIn(scopes.length > 0 ? scopes : [root]));
|
|
526
|
+
const installed = await loadSystem(root, {
|
|
527
|
+
px: rootSize.ambiguous ? DEFAULT_ROOT_PX : rootSize.px,
|
|
528
|
+
from: rootSize.from,
|
|
529
|
+
});
|
|
473
530
|
const { recipes, documents } = installed;
|
|
474
531
|
let table = installed.table;
|
|
475
532
|
// Nothing of ours here does not mean nothing to measure against. Fall back
|
|
@@ -600,6 +657,18 @@ export async function doctor(opts) {
|
|
|
600
657
|
const said = measured.from === "census" ? describeScope(measured) : null;
|
|
601
658
|
console.log(body(said ?? `scope: ${relScopes.join(", ")}`));
|
|
602
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* A RAIZ QUE ESTA RODADA USOU - e ela é impressa SEMPRE, inclusive quando é o padrão.
|
|
662
|
+
*
|
|
663
|
+
* Pedido do dono em 13/08: *"analisar antes se tem algo que interfere no size, e deixar registrado
|
|
664
|
+
* como uma regra que está sendo usada"*. É o que transforma uma suposição nossa num fato que ele
|
|
665
|
+
* pode conferir: `4px` e `0.25rem` só são o mesmo valor por causa deste número, e ele decide quantas
|
|
666
|
+
* trocas o comando oferece.
|
|
667
|
+
*
|
|
668
|
+
* Impressa mesmo no caso padrão porque é aí que ela é mais fácil de esquecer - e um relatório que só
|
|
669
|
+
* fala quando é exceção ensina que o silêncio significa "não olhei".
|
|
670
|
+
*/
|
|
671
|
+
console.log(body(saidOfRoot(rootSize)));
|
|
603
672
|
/**
|
|
604
673
|
* E A INTENÇÃO, em toda rodada, com a data e o jeito de inverter.
|
|
605
674
|
*
|
|
@@ -941,7 +1010,7 @@ export async function doctor(opts) {
|
|
|
941
1010
|
console.log("");
|
|
942
1011
|
console.log(section("What your agent asked for"));
|
|
943
1012
|
for (const r of requests.slice(0, 8)) {
|
|
944
|
-
console.log(body(` ${r.id} ${r
|
|
1013
|
+
console.log(body(` ${r.id} ${requestLabel(r)}${r.area === "platform" ? " [platform]" : ""}`));
|
|
945
1014
|
console.log(body(` for: ${r.purpose}`));
|
|
946
1015
|
if (r.considered)
|
|
947
1016
|
console.log(body(` considered: ${r.considered}`));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -200,6 +200,29 @@ const TOOLS = [
|
|
|
200
200
|
required: ["value", "name", "purpose"],
|
|
201
201
|
},
|
|
202
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: "request_rule",
|
|
205
|
+
description: "File a rule request when this component does something the system's doctrine does not cover, and you had to decide alone. Read `system_doctrine` first: if a rule already answers it, follow it instead of filing. This is for the case with no rule - what the rule would say, and the case that asked for it. Do NOT invent a convention and move on; a decision that lives only in one file is not a decision of the system.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
name: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "What the rule would say, in one line: 'a card that opens a dialog carries the trigger, never the panel'",
|
|
212
|
+
},
|
|
213
|
+
purpose: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "The case that asked for it - what you were building when no rule answered.",
|
|
216
|
+
},
|
|
217
|
+
considered: {
|
|
218
|
+
type: "string",
|
|
219
|
+
description: "Which existing rules you read and why they did not answer.",
|
|
220
|
+
},
|
|
221
|
+
file: { type: "string", description: "Where the case lives." },
|
|
222
|
+
},
|
|
223
|
+
required: ["name", "purpose"],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
203
226
|
];
|
|
204
227
|
/**
|
|
205
228
|
* QUANTAS FERRAMENTAS ESTE SERVIDOR SERVE, lido da lista.
|
|
@@ -1037,6 +1060,24 @@ cli) {
|
|
|
1037
1060
|
? `Filed as ${r.id} and routed to the synthesisui platform team - the system's contract already promises this and the shipped css does not deliver it. No one needs to act: it closes itself when an update lands. Keep the quiet base meanwhile.`
|
|
1038
1061
|
: `Filed as ${r.id}. Do not add the token yourself - the request shows up in \`synthesisui doctor\` for a person to decide.`);
|
|
1039
1062
|
}
|
|
1063
|
+
case "request_rule": {
|
|
1064
|
+
/**
|
|
1065
|
+
* SEM TRIAGEM AUTOMÁTICA, e de propósito.
|
|
1066
|
+
*
|
|
1067
|
+
* `request_token` sabe rotear para a plataforma quando o contrato do sistema já promete o
|
|
1068
|
+
* nome - é uma pergunta que a máquina responde. "Isto deveria ser uma regra?" não é: ela é a
|
|
1069
|
+
* decisão de quem é dono do sistema, e roteá-la sozinho seria inventar a resposta em vez de
|
|
1070
|
+
* abrir a pergunta.
|
|
1071
|
+
*/
|
|
1072
|
+
const r = await fileRequest(root, {
|
|
1073
|
+
kind: "rule",
|
|
1074
|
+
name: String(args.name ?? ""),
|
|
1075
|
+
purpose: String(args.purpose ?? ""),
|
|
1076
|
+
considered: args.considered ? String(args.considered) : undefined,
|
|
1077
|
+
file: args.file ? String(args.file) : undefined,
|
|
1078
|
+
});
|
|
1079
|
+
return text(`Filed as ${r.id}. Do not adopt the convention as if it were a rule - it shows up in \`synthesisui doctor\` and travels to the system's queue, for the owner to make it a rule or decline it.`);
|
|
1080
|
+
}
|
|
1040
1081
|
default:
|
|
1041
1082
|
return text(`No tool named ${name}.`, true);
|
|
1042
1083
|
}
|
package/dist/commands/request.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { closeRequest, fileRequest, readRequests } from "../doctor/requests.js";
|
|
2
|
+
import { closeRequest, fileRequest, KINDS, readRequests, } from "../doctor/requests.js";
|
|
3
3
|
import { body, section } from "../output.js";
|
|
4
4
|
/**
|
|
5
5
|
* `synthesisui request` - the queue of what the agent needed and was refused.
|
|
@@ -12,6 +12,12 @@ import { body, section } from "../output.js";
|
|
|
12
12
|
* Closing is deliberately explicit. A request nobody got to is still a
|
|
13
13
|
* request; nothing here expires.
|
|
14
14
|
*/
|
|
15
|
+
/** Como cada tipo se lê numa linha - um lugar, três telas. */
|
|
16
|
+
const labelOf = (r) => r.kind === "token"
|
|
17
|
+
? `token ${r.value} as ${r.name}`
|
|
18
|
+
: r.kind === "rule"
|
|
19
|
+
? `rule "${r.name}"`
|
|
20
|
+
: `component "${r.name}"`;
|
|
15
21
|
export async function request(opts) {
|
|
16
22
|
const root = resolve(opts.dir ?? process.cwd());
|
|
17
23
|
if (opts.done) {
|
|
@@ -29,7 +35,7 @@ export async function request(opts) {
|
|
|
29
35
|
return;
|
|
30
36
|
}
|
|
31
37
|
for (const r of all) {
|
|
32
|
-
console.log(body(` ${r.id} ${r
|
|
38
|
+
console.log(body(` ${r.id} ${labelOf(r)}`));
|
|
33
39
|
console.log(body(` for: ${r.purpose}`));
|
|
34
40
|
if (r.considered)
|
|
35
41
|
console.log(body(` considered: ${r.considered}`));
|
|
@@ -40,14 +46,23 @@ export async function request(opts) {
|
|
|
40
46
|
console.log(body("Close one: synthesisui request --done <id>"));
|
|
41
47
|
return;
|
|
42
48
|
}
|
|
43
|
-
if (
|
|
44
|
-
console.log(`Unknown kind "${opts.kind}" - component or
|
|
49
|
+
if (!KINDS.has(opts.kind)) {
|
|
50
|
+
console.log(`Unknown kind "${opts.kind}" - component, token or rule.`);
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* UMA REGRA PRECISA DO CASO, e é por isso que ela exige `--for` como as outras.
|
|
55
|
+
*
|
|
56
|
+
* "Isto deveria ser uma regra" sem o caso que a motivou é uma opinião. Com o caso, quem decidir do
|
|
57
|
+
* outro lado tem o que a doutrina não cobria e onde isso apareceu - que é a diferença entre uma
|
|
58
|
+
* fila que vira decisão e uma que vira backlog.
|
|
59
|
+
*/
|
|
47
60
|
if (!opts.name || !opts.purpose || (opts.kind === "token" && !opts.value)) {
|
|
48
61
|
console.log(opts.kind === "token"
|
|
49
62
|
? "A token request needs --value, --name and --for."
|
|
50
|
-
:
|
|
63
|
+
: opts.kind === "rule"
|
|
64
|
+
? "A rule request needs --name and --for - what the rule would say, and the case that asked for it."
|
|
65
|
+
: "A component request needs --name and --for.");
|
|
51
66
|
return;
|
|
52
67
|
}
|
|
53
68
|
const r = await fileRequest(root, {
|
package/dist/commands/sync.js
CHANGED
|
@@ -16,11 +16,26 @@ import { resolveReadParts, siblingProjects, takeCensus } from "./import.js";
|
|
|
16
16
|
* after `closeRequest` runs here, so the person knows what happened and what
|
|
17
17
|
* (if anything) is theirs to do next.
|
|
18
18
|
*/
|
|
19
|
-
export function decisionLine(d, slug
|
|
19
|
+
export function decisionLine(d, slug,
|
|
20
|
+
/** Onde o sistema dele vive - o `publish` mora lá, e sem o endereço a frase manda procurar. */
|
|
21
|
+
base) {
|
|
20
22
|
const note = d.note ? ` - "${d.note}"` : "";
|
|
21
23
|
switch (d.status) {
|
|
24
|
+
/**
|
|
25
|
+
* AUTORIZAR ESCREVE O RASCUNHO, e esta linha dizia o contrário.
|
|
26
|
+
*
|
|
27
|
+
* `authorPersonalToken` termina em `writeDraft`, e o registry serve a última PUBLICADA - por lei,
|
|
28
|
+
* desde 29/07: se o rascunho fluísse para o repo, o botão Publish não seguraria nada.
|
|
29
|
+
*
|
|
30
|
+
* Então "Get it: upgrade" mandava a pessoa rodar um comando que responde `already at the latest
|
|
31
|
+
* version` e não traz o token. Ela seguiu a instrução, não recebeu nada, e fica sem saber se
|
|
32
|
+
* autorizou errado ou se a ferramenta falhou - que é o pior lugar para deixar alguém que acabou
|
|
33
|
+
* de fazer exatamente o que a gente pediu (dono, 13/08, vendo isso no terminal dele).
|
|
34
|
+
*
|
|
35
|
+
* A frase agora nomeia os DOIS passos, na ordem, e o primeiro é dele.
|
|
36
|
+
*/
|
|
22
37
|
case "authored":
|
|
23
|
-
return ` ✓ ${d.id} authored
|
|
38
|
+
return ` ✓ ${d.id} authored into your draft${note}. It reaches this repo once you publish${base ? `: ${base}/dashboard/mine/${slug}/publish` : ""}\n then: npx synthesisui@latest upgrade ${slug}`;
|
|
24
39
|
case "declined":
|
|
25
40
|
return ` ✕ ${d.id} declined${note}. Now a RULE of the system - it travels with the next upgrade, and agents obey it.`;
|
|
26
41
|
case "platform":
|
|
@@ -150,7 +165,7 @@ export async function sync(opts) {
|
|
|
150
165
|
console.log(body("Answered on the platform, closed here:"));
|
|
151
166
|
for (const d of decisions) {
|
|
152
167
|
await closeRequest(root, d.id);
|
|
153
|
-
console.log(body(decisionLine(d, slug)));
|
|
168
|
+
console.log(body(decisionLine(d, slug, base)));
|
|
154
169
|
}
|
|
155
170
|
}
|
|
156
171
|
console.log("");
|
|
@@ -276,6 +291,49 @@ export async function remeasure(args) {
|
|
|
276
291
|
if (stored.reading)
|
|
277
292
|
census.reading = stored.reading;
|
|
278
293
|
await resolveReadParts(census, root, !args.full).catch(() => { });
|
|
294
|
+
/**
|
|
295
|
+
* O QUE A RE-MEDIÇÃO NÃO MEDE, ELA NÃO PODE APAGAR - e apagava quatro campos de uma vez.
|
|
296
|
+
*
|
|
297
|
+
* O `sync` mede o CÓDIGO dele de novo. Ele não mede polaridade, não pergunta escopo e não refaz a
|
|
298
|
+
* leitura do agente: esses três são respostas que alguém já deu, e `runImport` é quem as grava.
|
|
299
|
+
* Escrever por cima com um censo que não os tem não é re-medir - é esquecer.
|
|
300
|
+
*
|
|
301
|
+
* O que isso custava, medido no censo do dono em 13/08 (`scope`, `usage`, `scheme` e `reading`
|
|
302
|
+
* ausentes depois de um sync):
|
|
303
|
+
*
|
|
304
|
+
* scheme `censusToPatch` lê `reading.themes.default ?? census.scheme ?? "light"`. Sem os
|
|
305
|
+
* dois primeiros ele cai em CLARO - e o sistema dele abre ESCURO. Uma
|
|
306
|
+
* re-interpretação a partir desse censo aterra a face errada, no servidor, sem
|
|
307
|
+
* ninguém tocar em nada
|
|
308
|
+
* scope o próprio comentário de `takeCensus` chama isso de veneno de ação lenta: a
|
|
309
|
+
* primeira re-medição parece perfeita e a segunda mede o repositório inteiro
|
|
310
|
+
* reading a leitura que o agente autorou, que é justamente o que o `sync` promete não mexer
|
|
311
|
+
*
|
|
312
|
+
* Complemento, nunca correção: o que a medição de hoje TEM continua ganhando. Isto só recoloca o
|
|
313
|
+
* que ela não tinha como saber.
|
|
314
|
+
*/
|
|
315
|
+
const before = await readFile(join(root, "_synthesisui", "census.json"), "utf8")
|
|
316
|
+
.then((raw) => JSON.parse(raw))
|
|
317
|
+
.catch(() => null);
|
|
318
|
+
const fresh = census;
|
|
319
|
+
/** `scope` e `usage` o `sync` SABE - ele acabou de resolvê-los. Os outros vêm do que já existia. */
|
|
320
|
+
if (scope)
|
|
321
|
+
fresh.scope = scope;
|
|
322
|
+
if (usage.length > 0)
|
|
323
|
+
fresh.usage = usage;
|
|
324
|
+
for (const key of ["scheme", "reading"])
|
|
325
|
+
if (fresh[key] == null && before?.[key] != null)
|
|
326
|
+
fresh[key] = before[key];
|
|
327
|
+
/**
|
|
328
|
+
* E A POLARIDADE VEM DA PLATAFORMA quando nem a medição nem o arquivo a têm.
|
|
329
|
+
*
|
|
330
|
+
* Carregar do arquivo anterior conserta quem ainda não perdeu. Quem já perdeu - o censo do dono,
|
|
331
|
+
* medido em 13/08 - ficaria sem polaridade para sempre, porque o `sync` não a mede: ela é uma
|
|
332
|
+
* resposta dada no import. O documento guardado sabe (`meta.scheme`), então a rota devolve e isto
|
|
333
|
+
* recoloca. O próximo `sync` de quem estava furado repara o censo dele.
|
|
334
|
+
*/
|
|
335
|
+
if (fresh.scheme == null && stored.scheme)
|
|
336
|
+
fresh.scheme = stored.scheme;
|
|
279
337
|
/**
|
|
280
338
|
* O CENSO FRESCO TAMBÉM FICA NO DISCO.
|
|
281
339
|
*
|
package/dist/doctor/requests.js
CHANGED
|
@@ -22,6 +22,8 @@ import { join } from "node:path";
|
|
|
22
22
|
* how a team shares a queue.
|
|
23
23
|
*/
|
|
24
24
|
export const REQUESTS_FILE = "requests.jsonl";
|
|
25
|
+
/** Os tipos que a fila aceita, num lugar só - ver `GapRequest.kind`. */
|
|
26
|
+
export const KINDS = new Set(["component", "token", "rule"]);
|
|
25
27
|
const path = (root) => join(root, "_synthesisui", REQUESTS_FILE);
|
|
26
28
|
/** Stable-enough id from content: 6 chars, collision-safe at queue scale. */
|
|
27
29
|
function idOf(kind, name, at) {
|
|
@@ -65,7 +67,14 @@ export async function readRequests(root) {
|
|
|
65
67
|
continue;
|
|
66
68
|
try {
|
|
67
69
|
const r = JSON.parse(line);
|
|
68
|
-
|
|
70
|
+
/**
|
|
71
|
+
* O FILTRO DA LEITURA - e ele é um dos três lugares onde um tipo novo some CALADO.
|
|
72
|
+
*
|
|
73
|
+
* Uma linha com `kind` desconhecido é descartada aqui sem erro, então acrescentar um tipo sem
|
|
74
|
+
* passar por este ponto produz um pedido que o agente arquiva, o arquivo guarda, e ninguém
|
|
75
|
+
* nunca lê.
|
|
76
|
+
*/
|
|
77
|
+
if (r?.id && r.name && KINDS.has(r.kind))
|
|
69
78
|
out.push(r);
|
|
70
79
|
}
|
|
71
80
|
catch {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUAL É A RAIZ DESTE PROJETO - medida antes de comparar valor nenhum.
|
|
3
|
+
*
|
|
4
|
+
* `4px` e `0.25rem` são o mesmo valor, e o comparador tratava os dois como textos diferentes. Medido
|
|
5
|
+
* no repo real em 13/08: 1320 literais em px que um token `--ds-*` já nomeia em rem, em 364 arquivos,
|
|
6
|
+
* contra 253 trocas que o doctor conseguia oferecer no repositório inteiro. A maior parte do trabalho
|
|
7
|
+
* fácil estava escondida atrás de uma comparação de string.
|
|
8
|
+
*
|
|
9
|
+
* A conversão exige uma raiz, e `1rem = 16px` é o padrão do navegador - mas é só o padrão. Um projeto
|
|
10
|
+
* que escreve `html { font-size: 62.5% }` tem raiz de 10px, e converter por 16 ali erraria em todos os
|
|
11
|
+
* lugares de uma vez, com a confiança de quem acertou.
|
|
12
|
+
*
|
|
13
|
+
* Então a raiz não é suposta: é MEDIDA no css dele, e DITA em voz alta no relatório (dono, 13/08:
|
|
14
|
+
* *"analisar antes se tem algo que interfere no size, e deixar registrado como uma regra que está
|
|
15
|
+
* sendo usada"*). Uma suposição escondida num comentário do nosso código não é lida por ninguém; um
|
|
16
|
+
* fato impresso no cabeçalho é conferível por quem conhece o projeto.
|
|
17
|
+
*
|
|
18
|
+
* E QUANDO NÃO DÁ PARA SABER, NÃO CONVERTE. Duas raízes diferentes, um `calc()`, um `var()`: a
|
|
19
|
+
* resposta é dizer que não sabe. Deixar de oferecer uma troca custa uma troca; oferecer a errada em
|
|
20
|
+
* 1320 lugares custa a confiança no comando.
|
|
21
|
+
*/
|
|
22
|
+
/** O padrão do navegador, e o que vale quando ninguém redefine. */
|
|
23
|
+
export const DEFAULT_ROOT_PX = 16;
|
|
24
|
+
/** `html`/`:root` com uma declaração de `font-size` dentro - o bloco inteiro, para ler o valor. */
|
|
25
|
+
const ROOT_BLOCK = /(?:^|[},;])\s*(html|:root)\s*(?:,[^{]*)?\{([^}]*)\}/gi;
|
|
26
|
+
const FONT_SIZE = /(?:^|;)\s*font-size\s*:\s*([^;}]+)/i;
|
|
27
|
+
/**
|
|
28
|
+
* O valor de uma declaração de raiz em pixels, ou `null` quando não dá para saber.
|
|
29
|
+
*
|
|
30
|
+
* `%` e `em` na RAIZ são relativos ao padrão do navegador, que é o único ancestral que ela tem - por
|
|
31
|
+
* isso `62.5%` é 10px e não uma incógnita. `rem` na raiz é a mesma coisa, e é como alguns projetos
|
|
32
|
+
* escrevem `1rem` só para deixar explícito.
|
|
33
|
+
*/
|
|
34
|
+
export function rootPxOf(raw) {
|
|
35
|
+
const v = raw.trim().toLowerCase();
|
|
36
|
+
const m = /^(\d*\.?\d+)(px|%|r?em)$/.exec(v);
|
|
37
|
+
if (!m)
|
|
38
|
+
return null;
|
|
39
|
+
const n = Number.parseFloat(m[1]);
|
|
40
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
41
|
+
return null;
|
|
42
|
+
if (m[2] === "px")
|
|
43
|
+
return n;
|
|
44
|
+
if (m[2] === "%")
|
|
45
|
+
return (n / 100) * DEFAULT_ROOT_PX;
|
|
46
|
+
return n * DEFAULT_ROOT_PX;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A raiz deste projeto, lida das folhas de estilo dele.
|
|
50
|
+
*
|
|
51
|
+
* Recebe os arquivos já lidos porque quem varre é o comando - esta função não sabe andar em disco, e
|
|
52
|
+
* é isso que a torna testável com um objeto em vez de um diretório temporário.
|
|
53
|
+
*/
|
|
54
|
+
export function rootSizeOf(sheets) {
|
|
55
|
+
/** Valor em px → onde ele foi declarado pela primeira vez. */
|
|
56
|
+
const seen = new Map();
|
|
57
|
+
const unreadable = [];
|
|
58
|
+
for (const sheet of sheets) {
|
|
59
|
+
ROOT_BLOCK.lastIndex = 0;
|
|
60
|
+
for (const block of sheet.css.matchAll(ROOT_BLOCK)) {
|
|
61
|
+
const decl = FONT_SIZE.exec(block[2]);
|
|
62
|
+
if (!decl)
|
|
63
|
+
continue;
|
|
64
|
+
const px = rootPxOf(decl[1]);
|
|
65
|
+
if (px == null) {
|
|
66
|
+
unreadable.push(`${decl[1].trim()} (${sheet.file})`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!seen.has(px))
|
|
70
|
+
seen.set(px, `${block[1]}, ${sheet.file}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* UM VALOR ILEGÍVEL SÓ CONTAMINA SE FOR O ÚNICO SINAL - ou se discordar do que foi lido.
|
|
75
|
+
*
|
|
76
|
+
* Um `font-size: var(--app-root)` ao lado de um `10px` declarado não torna o projeto ambíguo: o
|
|
77
|
+
* que se sabe continua sabido. O que torna é não haver resposta, ou haver duas.
|
|
78
|
+
*/
|
|
79
|
+
if (seen.size > 1)
|
|
80
|
+
return {
|
|
81
|
+
px: DEFAULT_ROOT_PX,
|
|
82
|
+
from: null,
|
|
83
|
+
ambiguous: {
|
|
84
|
+
saw: [...seen.entries()].map(([px, at]) => `${px}px (${at})`),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (seen.size === 0 && unreadable.length > 0)
|
|
88
|
+
return { px: DEFAULT_ROOT_PX, from: null, ambiguous: { saw: unreadable } };
|
|
89
|
+
const only = [...seen.entries()][0];
|
|
90
|
+
return only
|
|
91
|
+
? { px: only[0], from: only[1] }
|
|
92
|
+
: { px: DEFAULT_ROOT_PX, from: null };
|
|
93
|
+
}
|
|
94
|
+
/** A linha do relatório - o fato, e de onde ele veio. Nunca um número pelado. */
|
|
95
|
+
export function saidOfRoot(root) {
|
|
96
|
+
if (root.ambiguous)
|
|
97
|
+
return `root font-size: not one answer (${root.ambiguous.saw.slice(0, 3).join(" · ")}) - px and rem are not compared here`;
|
|
98
|
+
return root.from
|
|
99
|
+
? `root font-size: ${root.px}px (${root.from}) - px and rem are compared against this`
|
|
100
|
+
: `root font-size: ${DEFAULT_ROOT_PX}px (browser default - nothing here redefines it)`;
|
|
101
|
+
}
|
package/dist/doctor/scan.js
CHANGED
|
@@ -316,7 +316,7 @@ function scanCore(file, source, table) {
|
|
|
316
316
|
* e é por VALOR, não por nome: `--color-ocean-500` e `--ds-color-ocean-500` só são a mesma
|
|
317
317
|
* decisão porque os dois seguram `#059aed`.
|
|
318
318
|
*/
|
|
319
|
-
...(table.byValue.has(normalizeValue(value))
|
|
319
|
+
...(table.byValue.has(normalizeValue(value, table.rootPx))
|
|
320
320
|
? { mirrored: true }
|
|
321
321
|
: null),
|
|
322
322
|
});
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
* Pure and dependency-free on purpose: every function here takes text and
|
|
12
12
|
* returns data, so the whole diagnosis is testable without a filesystem.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Where the vocabulary being measured against came from.
|
|
16
|
+
*
|
|
17
|
+
* `"installed"` is a system we wrote. `"yours"` is the project's OWN custom
|
|
18
|
+
* properties, harvested from its stylesheets - the case that matters most,
|
|
19
|
+
* because the people who feel this problem hardest already have a design
|
|
20
|
+
* system and had no reason to adopt ours before seeing a number.
|
|
21
|
+
*/
|
|
22
|
+
/** `"adopted"` is theirs too, but described by `adopt` and therefore NAMED -
|
|
23
|
+
* it must not be offered a system to install, having just adopted one. */
|
|
24
|
+
import { DEFAULT_ROOT_PX } from "./root-size.js";
|
|
14
25
|
export const EMPTY_TABLE = {
|
|
15
26
|
source: null,
|
|
16
27
|
name: null,
|
|
@@ -18,6 +29,8 @@ export const EMPTY_TABLE = {
|
|
|
18
29
|
version: null,
|
|
19
30
|
byName: new Map(),
|
|
20
31
|
byValue: new Map(),
|
|
32
|
+
rootPx: DEFAULT_ROOT_PX,
|
|
33
|
+
rootFrom: null,
|
|
21
34
|
declared: new Set(),
|
|
22
35
|
keyframes: new Set(),
|
|
23
36
|
};
|
|
@@ -90,8 +103,30 @@ function args(body) {
|
|
|
90
103
|
*
|
|
91
104
|
* Anything that is not a colour passes through lowercased and collapsed.
|
|
92
105
|
*/
|
|
93
|
-
export function normalizeValue(raw) {
|
|
106
|
+
export function normalizeValue(raw, rootPx) {
|
|
94
107
|
const v = raw.trim().toLowerCase().replace(/\s+/g, " ");
|
|
108
|
+
/**
|
|
109
|
+
* COMPRIMENTO CAI EM PIXELS, pelo mesmo motivo que tempo cai em milissegundos logo abaixo.
|
|
110
|
+
*
|
|
111
|
+
* `--ds-radius-xs: 0.25rem` e um `borderRadius: "4px"` no código dele são o MESMO valor, e o
|
|
112
|
+
* comparador os tratava como textos diferentes. Medido no repo real (13/08): 1320 literais em px
|
|
113
|
+
* que um token já nomeia em rem, contra 253 trocas que o comando conseguia oferecer no repositório
|
|
114
|
+
* inteiro.
|
|
115
|
+
*
|
|
116
|
+
* `rootPx` é obrigatório de propósito. O padrão do navegador é 16, mas um projeto que escreve
|
|
117
|
+
* `html { font-size: 62.5% }` tem raiz de 10 - e converter por 16 ali erra em todos os lugares de
|
|
118
|
+
* uma vez. Quem chama tem que dizer qual raiz mediu; ver `rootSizeOf`.
|
|
119
|
+
*
|
|
120
|
+
* A FAMÍLIA CONTINUA MANDANDO: `4px` passa a casar com `--ds-radius-xs` E com `--ds-spacing-3xs`,
|
|
121
|
+
* e é `tokenMatch` quem escolhe pelo `kind` - um raio não vira espaçamento dentro de um `gap`.
|
|
122
|
+
*/
|
|
123
|
+
const len = /^(-?\d*\.?\d+)(px|rem)$/.exec(v);
|
|
124
|
+
if (len) {
|
|
125
|
+
const n = Number.parseFloat(len[1]);
|
|
126
|
+
const px = len[2] === "rem" ? n * rootPx : n;
|
|
127
|
+
/** Arredonda o rastro binário: `0.35rem * 16` é 5.6000000000000005. */
|
|
128
|
+
return `${Math.round(px * 1e4) / 1e4}px`;
|
|
129
|
+
}
|
|
95
130
|
// Time lands on milliseconds: `.3s`, `0.3s` and `300ms` are one value, the
|
|
96
131
|
// same way every colour lands on 8-digit hex. Without this, a system that
|
|
97
132
|
// authors `--ds-motion-durations-base: 0.3s` never names an author's `300ms`.
|
|
@@ -353,6 +388,7 @@ export function parseRootTokens(css) {
|
|
|
353
388
|
}
|
|
354
389
|
export function buildTable(input) {
|
|
355
390
|
const source = input.source ?? "installed";
|
|
391
|
+
const rootPx = input.rootPx ?? DEFAULT_ROOT_PX;
|
|
356
392
|
const byName = source === "installed"
|
|
357
393
|
? parseTokens(input.css)
|
|
358
394
|
: parseRootTokens(input.css);
|
|
@@ -377,7 +413,7 @@ export function buildTable(input) {
|
|
|
377
413
|
const isAlias = (name) => /^var\(/.test((raw.get(name) ?? "").trim());
|
|
378
414
|
const byValue = new Map();
|
|
379
415
|
for (const [name, value] of resolved) {
|
|
380
|
-
const key = normalizeValue(value);
|
|
416
|
+
const key = normalizeValue(value, rootPx);
|
|
381
417
|
const list = byValue.get(key);
|
|
382
418
|
if (!list)
|
|
383
419
|
byValue.set(key, [name]);
|
|
@@ -393,6 +429,8 @@ export function buildTable(input) {
|
|
|
393
429
|
version: input.lock?.version ?? null,
|
|
394
430
|
byName,
|
|
395
431
|
byValue,
|
|
432
|
+
rootPx,
|
|
433
|
+
rootFrom: input.rootFrom ?? null,
|
|
396
434
|
declared: parseDeclaredNames(input.css),
|
|
397
435
|
keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
|
|
398
436
|
};
|
|
@@ -480,7 +518,7 @@ const FAMILY = {
|
|
|
480
518
|
* `family`.
|
|
481
519
|
*/
|
|
482
520
|
export function tokenMatch(table, literal, kind) {
|
|
483
|
-
const hit = table.byValue.get(normalizeValue(literal));
|
|
521
|
+
const hit = table.byValue.get(normalizeValue(literal, table.rootPx));
|
|
484
522
|
if (!hit || hit.length === 0)
|
|
485
523
|
return null;
|
|
486
524
|
const prefix = kind ? FAMILY[kind] : undefined;
|
package/dist/install-marks.js
CHANGED
|
@@ -90,7 +90,13 @@ export const MATERIALISER_SINCE = "0.16.220";
|
|
|
90
90
|
* agente dele que o fundo do `card` deveria apontar para `foreground` - o papel do TEXTO -, e é
|
|
91
91
|
* exatamente uma verificação que o pinado faz diferente.
|
|
92
92
|
*/
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* 0.16.219 -> 0.16.223 em 13/08: `px` e `rem` viraram o mesmo valor na comparação. O hook roda a
|
|
95
|
+
* mesma varredura depois de cada escrita, e um pin anterior lê o mesmo arquivo e diz "o sistema não
|
|
96
|
+
* tem nome para isto" sobre um valor que o sistema dela nomeia - medido no repo real, 243 trocas
|
|
97
|
+
* viraram 764 sobre os mesmos 4433 valores à mão.
|
|
98
|
+
*/
|
|
99
|
+
export const CHECKER_SINCE = "0.16.223";
|
|
94
100
|
/**
|
|
95
101
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
96
102
|
*
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A SKILL DE MANUTENÇÃO, como o CLI a distribui.
|
|
3
|
+
*
|
|
4
|
+
* Mesmo motivo do `skill-init.ts` e do `skill-import.ts`: o build é `tsc` e nada mais, então um `.md`
|
|
5
|
+
* precisaria de um passo de cópia que pode silenciosamente não rodar. Um módulo TypeScript não pode
|
|
6
|
+
* falhar em ser empacotado.
|
|
7
|
+
*
|
|
8
|
+
* ESTA É A FONTE. A cópia em `.claude/skills/` é o que o nosso editor lê, e o spec assere que as duas
|
|
9
|
+
* são idênticas.
|
|
10
|
+
*
|
|
11
|
+
* E ela é a TERCEIRA que o `connect` instala - as outras duas são de ENTRADA (primeira corrida,
|
|
12
|
+
* import). Esta responde a pergunta do dia seguinte, apontando para uma tela: *"isso aqui está de
|
|
13
|
+
* acordo com o meu design system?"*. Deixá-la fora do `connect` faria dela uma skill nossa, e o caso
|
|
14
|
+
* de uso que a motivou é o cliente rodando sozinho toda semana.
|
|
15
|
+
*/
|
|
16
|
+
export const ADAPT_SKILL = `---
|
|
17
|
+
name: sui-adapt
|
|
18
|
+
description: Confronta UM componente (ou uma página, ou uma pasta) contra o design system instalado e adapta o que der - cobertura de token medida, conserto mecânico proposto antes de escrever, e o que sobra classificado em "regra nova" ou "conserto local". Use quando alguém aponta para uma peça e pergunta se ela está de acordo com o sistema (ex. "/sui-adapt components/ui/card", "analisa esse componente aqui", "isso aqui segue o meu design system?"), como rotina semanal, ou logo depois de criar/alterar um componente. Mede antes de propor, propõe antes de escrever, e nunca arquiva pedido em nome de ninguém.
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Adaptar uma peça ao sistema
|
|
22
|
+
|
|
23
|
+
**Seu primeiro comando é a medição.** Não leia \`.mcp.json\`, não abra o censo, não liste
|
|
24
|
+
arquivo: \`check_file\` ou \`doctor <alvo>\` responde tudo isso em um passo, e é o que o resto
|
|
25
|
+
desta skill consome. Um agente que sai explorando antes gasta a paciência de quem pediu e
|
|
26
|
+
chega ao mesmo lugar.
|
|
27
|
+
|
|
28
|
+
O propósito do produto, que decide todo empate abaixo: **ler o repositório do cliente e
|
|
29
|
+
devolver receitas com paridade visual, semântica e funcional, sem supor e sem inventar
|
|
30
|
+
nada.**
|
|
31
|
+
|
|
32
|
+
Esta skill é a ponta de manutenção. As outras duas que o cliente tem são de ENTRADA -
|
|
33
|
+
\`sui-init\` e \`sui-import-ds\` transformam o repositório dele em sistema. Esta responde a
|
|
34
|
+
pergunta do dia seguinte, que ele faz apontando para uma tela: *"isso aqui está de acordo
|
|
35
|
+
com o meu design system?"*.
|
|
36
|
+
|
|
37
|
+
\`CLAUDE.md\` manda. Quando os dois divergirem, este arquivo é que está velho.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 0. O QUE ESTA SKILL NÃO FAZ
|
|
42
|
+
|
|
43
|
+
Três limites, e cada um existe por um motivo que já custou alguma coisa:
|
|
44
|
+
|
|
45
|
+
\`\`\`
|
|
46
|
+
não escreve sem propor é o repositório DELE. \`--fix --write\` sem confirmação é
|
|
47
|
+
outra categoria de confiança, e uma skill que perde essa
|
|
48
|
+
confiança não é rodada uma segunda vez
|
|
49
|
+
não arquiva pedido \`request\` fila uma decisão na plataforma. A skill MOSTRA o
|
|
50
|
+
comando; quem roda é ele
|
|
51
|
+
não inventa token, nome um agente que cala um relatório fazendo o sistema crescer é
|
|
52
|
+
nem regra pior que a deriva que ele veio medir
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
## 1. O ALVO, E POR QUE O ESCOPO É DECISÃO DA SKILL
|
|
56
|
+
|
|
57
|
+
Componente quase nunca é um arquivo. \`Card.tsx\` costuma vir com \`Card.css\`, \`Card.stories.tsx\`
|
|
58
|
+
e às vezes um \`index.ts\` - e medir só o \`.tsx\` produz um número que mente por omissão: o css
|
|
59
|
+
ao lado é justamente onde os valores à mão se escondem.
|
|
60
|
+
|
|
61
|
+
\`\`\`
|
|
62
|
+
1. resolva o alvo o que ele apontou, ou o arquivo aberto, ou o que ele acabou de mexer
|
|
63
|
+
2. suba para a PASTA quando o irmão existir (mesmo nome, extensão diferente)
|
|
64
|
+
3. DIGA qual escopo você usou, com o número de arquivos
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
Nunca meça os dois e escolha o maior. Diga o que mediu.
|
|
68
|
+
|
|
69
|
+
**O alvo fora da pasta importada é o caso COMUM, e não um erro.** O sistema nasceu de uma
|
|
70
|
+
pasta (o \`scope\` no \`.lock\`), e uma tela de app que consome o DS está fora dela - é
|
|
71
|
+
literalmente o pedido *"conserta essa página pro meu design system"*. Ali a cobertura de
|
|
72
|
+
token vale igual, porque a camada de token é global; o que não existe é receita daquele
|
|
73
|
+
componente. Diga isso em uma linha e siga - e quando faltar uma peça, o destino é
|
|
74
|
+
\`request component\`.
|
|
75
|
+
|
|
76
|
+
## 2. MEÇA - e a medida é determinística, não sua
|
|
77
|
+
|
|
78
|
+
Duas portas, mesma resposta. Use a que a sessão tiver:
|
|
79
|
+
|
|
80
|
+
\`\`\`
|
|
81
|
+
MCP check_file { path }
|
|
82
|
+
terminal npx synthesisui doctor <alvo>
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
O que volta, medido num componente real (\`ArticleCard\`, 13/08):
|
|
86
|
+
|
|
87
|
+
\`\`\`
|
|
88
|
+
SignalUI v7 - 181 tokens, 1 file read
|
|
89
|
+
scope: packages/ui/src/lib/SignalUI/organisms/ArticleCard/ArticleCard.tsx
|
|
90
|
+
|
|
91
|
+
Token coverage ░░░░░░░░░░░░░░░░░░░░░░░░ 0%
|
|
92
|
+
0 from the system, 2 by hand
|
|
93
|
+
50% is one command away - 1 of those have a name waiting
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
Três números, e eles já vêm separados por natureza:
|
|
97
|
+
|
|
98
|
+
\`\`\`
|
|
99
|
+
from the system já usa o vocabulário. Nada a fazer
|
|
100
|
+
have a name waiting o sistema JÁ nomeia esse valor -> mecânico, é o passo 3
|
|
101
|
+
no name for it o sistema não nomeia -> DECISÃO dele, é o passo 5
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
Não recalcule nada disso de cabeça. O número que você reporta é o que o comando disse.
|
|
105
|
+
|
|
106
|
+
## 3. MONTE A FILA, E CONTE OS ITENS ANTES DE COMEÇAR
|
|
107
|
+
|
|
108
|
+
Aqui é onde esta skill se ganha ou se perde. Despejar tudo de uma vez - duas regras, quatro
|
|
109
|
+
decisões, dez trocas - não é um relatório, é uma parede. Quem lê não tem como agir; só
|
|
110
|
+
concordar ou fechar a aba.
|
|
111
|
+
|
|
112
|
+
Junte tudo o que você achou (o mecânico do passo 2, as regras do passo 4, o que sobrou do
|
|
113
|
+
passo 5) numa fila ÚNICA, e **ordene por custo**:
|
|
114
|
+
|
|
115
|
+
\`\`\`
|
|
116
|
+
1º não muda um pixel troca por token de mesmo valor
|
|
117
|
+
2º muda o pixel colapsar um passo, adotar um token semântico
|
|
118
|
+
3º não é troca, é pedido token novo, peça nova, regra nova
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
Assim ele despacha o barato primeiro e para quando quiser, sem ficar devendo nada.
|
|
122
|
+
|
|
123
|
+
Anuncie o tamanho antes do primeiro item, sempre:
|
|
124
|
+
|
|
125
|
+
\`\`\`
|
|
126
|
+
7 itens nesta fila: 2 sem mudar pixel, 3 que mudam, 2 pedidos.
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
## 4. UM ITEM POR VEZ, COM OPÇÕES - e espere a resposta
|
|
130
|
+
|
|
131
|
+
Nunca apresente o item 2 antes de ele responder o 1. O cabeçalho carrega a posição, para
|
|
132
|
+
ele saber onde está e quanto falta:
|
|
133
|
+
|
|
134
|
+
\`\`\`
|
|
135
|
+
item 1 de 7 · radius 4px · 2 lugares · não muda um pixel
|
|
136
|
+
|
|
137
|
+
DeliveredBox/index.tsx:76 borderRadius: "4px" -> var(--ds-radius-xs)
|
|
138
|
+
TotalSentBox/index.tsx:71 borderRadius: "4px" -> var(--ds-radius-xs)
|
|
139
|
+
|
|
140
|
+
[aplicar] [pular] [ver o diff] [parar por aqui]
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
Quatro opções, e nenhuma a mais:
|
|
144
|
+
|
|
145
|
+
\`\`\`
|
|
146
|
+
aplicar você roda o comando ou faz a edição, e confirma em uma linha
|
|
147
|
+
pular segue para o próximo, e ele entra no resumo como não mexido
|
|
148
|
+
ver o diff mostre e volte a perguntar - não conte isso como resposta
|
|
149
|
+
parar por aqui fecha o resumo com o que andou até aqui. É sempre legítimo
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
Para um item mecânico o "aplicar" é \`npx synthesisui doctor <alvo> --fix --write\`. Para os
|
|
153
|
+
outros é edição, e você mostra exatamente as linhas antes.
|
|
154
|
+
|
|
155
|
+
## 5. AS REGRAS, QUE É A METADE QUE NENHUM COMANDO FAZ
|
|
156
|
+
|
|
157
|
+
O passo 2 é determinístico e sai de graça. Este não: o sistema carrega uma doutrina em
|
|
158
|
+
prosa, e **nada a verifica mecanicamente**. É aqui que você trabalha.
|
|
159
|
+
|
|
160
|
+
\`\`\`
|
|
161
|
+
MCP system_doctrine
|
|
162
|
+
terminal as regras viajam no documento instalado (_synthesisui/ds/<slug>/doctrine.json)
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
Leia as regras e confronte o componente com cada uma. Três respostas possíveis por regra,
|
|
166
|
+
e a terceira é a que interessa:
|
|
167
|
+
|
|
168
|
+
\`\`\`
|
|
169
|
+
cumpre some do relatório. Diga só o total no fim
|
|
170
|
+
NÃO cumpre vira um ITEM da fila, com arquivo, linha e a troca proposta
|
|
171
|
+
a regra não fala sobre isto vira um item de PEDIDO - \`request rule\`
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
Uma regra que você teve que interpretar para aplicar não é "cumpre". É o terceiro caso.
|
|
175
|
+
|
|
176
|
+
E nunca escreva "3 de 5 cumpridas". Isso lê como boletim, e as 2 que faltam são justamente
|
|
177
|
+
as que TÊM conserto pronto - é a melhor notícia do relatório vestida como a pior.
|
|
178
|
+
|
|
179
|
+
## 6. OS PEDIDOS - você mostra o comando, ele roda
|
|
180
|
+
|
|
181
|
+
Três destinos, e todos existem:
|
|
182
|
+
|
|
183
|
+
\`\`\`
|
|
184
|
+
valor sem nome no sistema
|
|
185
|
+
-> npx synthesisui request token --value "<valor>" --name "<como se chamaria>" --for "<o caso>"
|
|
186
|
+
peça que falta
|
|
187
|
+
-> npx synthesisui request component --name "<nome>" --for "<o caso>"
|
|
188
|
+
a doutrina não cobre este caso
|
|
189
|
+
-> npx synthesisui request rule --name "<o que a regra diria>" --for "<o caso que pediu>"
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
**Arquivar em nome dele seria decidir por ele** - e tirar dele a chance de dizer "não, isso
|
|
193
|
+
fica local mesmo". Antes de propor \`request rule\`, leia a doutrina inteira: uma regra que já
|
|
194
|
+
existe e você não achou vira duplicata na fila, e a fila perde valor na terceira.
|
|
195
|
+
|
|
196
|
+
## 6b. A RÉGUA É O ALCANÇÁVEL, E O TETO SE DIZ JUNTO
|
|
197
|
+
|
|
198
|
+
**100% quase nunca é alcançável hoje, e isso não é falha dele.** Se o sistema não tem nome
|
|
199
|
+
para \`#555\`, ninguém chega a 100% sem antes decidir criar esse nome. Um medidor que mostra
|
|
200
|
+
0% contra um teto imaginário faz trabalho completo parecer trabalho pela metade.
|
|
201
|
+
|
|
202
|
+
Os números para a conta certa já vêm do comando:
|
|
203
|
+
|
|
204
|
+
\`\`\`
|
|
205
|
+
3 valores à mão
|
|
206
|
+
2 o sistema já nomeia -> alcançável hoje: 67%
|
|
207
|
+
1 o sistema não nomeia -> precisa de uma decisão dele
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
Então o teto de hoje é 67%, e aplicar as duas trocas é **chegar no teto** - 100% do que dá
|
|
211
|
+
para fazer com o vocabulário que existe.
|
|
212
|
+
|
|
213
|
+
## 6c. FECHE PELO QUE ANDOU, E MOSTRE O CAMINHO ATÉ 100%
|
|
214
|
+
|
|
215
|
+
\`\`\`
|
|
216
|
+
<Componente> <n> arquivos
|
|
217
|
+
|
|
218
|
+
alcançável hoje 67% é o que o vocabulário do sistema cobre
|
|
219
|
+
aplicado 67% ✓ no teto - nada mecânico ficou para trás
|
|
220
|
+
pulado 8 espaçamentos que mudam layout (item 5, quando quiser)
|
|
221
|
+
na sua fila 1 #555, que o sistema ainda não nomeia
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
Nunca "0% -> 0%". Se nada era mecânico, o teto era zero, e a frase é *"não havia nada que
|
|
225
|
+
o vocabulário de hoje resolvesse - o que existe são N decisões suas"*.
|
|
226
|
+
|
|
227
|
+
E quando sobrou pedido, termine com o caminho, porque ele é de dois passos e o primeiro é
|
|
228
|
+
dele:
|
|
229
|
+
|
|
230
|
+
\`\`\`
|
|
231
|
+
para chegar a 100%, faltam dois passos:
|
|
232
|
+
1. autorize o pedido no dashboard (ele já está na fila; \`sync\` o levou)
|
|
233
|
+
2. publique, e rode \`npx synthesisui upgrade\` aqui
|
|
234
|
+
depois disso, /sui-adapt neste componente fecha em 100%
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
Autorizar escreve o RASCUNHO, e o repo recebe a última PUBLICADA - por isso os dois passos,
|
|
238
|
+
e por isso o \`sync\` também diz isso quando a decisão volta. Prometer que \`upgrade\` sozinho
|
|
239
|
+
traz o token é mandar a pessoa rodar um comando que responde "already at the latest version".
|
|
240
|
+
|
|
241
|
+
Se a fila esvaziou, o fim é uma linha só:
|
|
242
|
+
|
|
243
|
+
\`\`\`
|
|
244
|
+
alcançável hoje 100%
|
|
245
|
+
aplicado 100% ✓ este componente está inteiro no sistema
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
## 7. ROTINA
|
|
249
|
+
|
|
250
|
+
Ela foi desenhada para duas horas do dia, e a segunda é a que mais rende:
|
|
251
|
+
|
|
252
|
+
\`\`\`
|
|
253
|
+
semanal "roda o sui-adapt no dashboard" - pega deriva antes de virar hábito
|
|
254
|
+
depois de mexer componente novo, ou alteração grande: rode ANTES do commit, enquanto a
|
|
255
|
+
decisão ainda está quente e o conserto ainda é barato
|
|
256
|
+
\`\`\`
|
|
257
|
+
|
|
258
|
+
O hook (\`PostToolUse\`) já roda a metade determinística a cada escrita, calado quando não há
|
|
259
|
+
o que dizer. Esta skill é o passo deliberado: ela junta o hook, a doutrina e a fila numa
|
|
260
|
+
conversa só, e termina com o cliente decidindo - não com um relatório.
|
|
261
|
+
`;
|
|
262
|
+
export const ADAPT_SKILL_PATH = ".claude/skills/sui-adapt/SKILL.md";
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ADAPT_SKILL, ADAPT_SKILL_PATH } from "./skill-adapt.js";
|
|
2
|
+
import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "./skill-import.js";
|
|
3
|
+
import { INIT_SKILL, INIT_SKILL_PATH } from "./skill-init.js";
|
|
4
|
+
/**
|
|
5
|
+
* AS SKILLS QUE O CLI DISTRIBUI, numa lista só - e ela existe porque DOIS comandos precisam dela.
|
|
6
|
+
*
|
|
7
|
+
* `connect` escreve; `align` cobra o que falta. Enquanto a lista morava dentro do `connect`, o
|
|
8
|
+
* `align` não tinha como saber que existia uma terceira - e uma skill nova só chegava a quem, por
|
|
9
|
+
* conta própria, rodasse `connect` de novo. Ninguém roda um comando de novo sem motivo.
|
|
10
|
+
*
|
|
11
|
+
* É a lei 8 no caso mais barato dela: a lacuna existe, a gente sabe qual é, e dizer custa uma linha.
|
|
12
|
+
*/
|
|
13
|
+
export const SKILLS = [
|
|
14
|
+
{
|
|
15
|
+
path: INIT_SKILL_PATH,
|
|
16
|
+
source: INIT_SKILL,
|
|
17
|
+
label: "/sui-init",
|
|
18
|
+
what: "the first run, start to finish",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: IMPORT_SKILL_PATH,
|
|
22
|
+
source: IMPORT_SKILL,
|
|
23
|
+
label: "/sui-import-ds",
|
|
24
|
+
what: "the import, orchestrated",
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* A DE MANUTENÇÃO, e ela é de outra natureza que as duas acima.
|
|
28
|
+
*
|
|
29
|
+
* As duas primeiras são de ENTRADA: rodam uma vez, e depois nunca mais. Esta responde a pergunta do
|
|
30
|
+
* dia seguinte, apontando para uma tela - *"isso aqui está de acordo com o meu design system?"* -,
|
|
31
|
+
* toda semana ou depois de mexer em alguma coisa. É a primeira que uma pessoa roda mais de uma vez.
|
|
32
|
+
*/
|
|
33
|
+
{
|
|
34
|
+
path: ADAPT_SKILL_PATH,
|
|
35
|
+
source: ADAPT_SKILL,
|
|
36
|
+
label: "/sui-adapt",
|
|
37
|
+
what: "one component against the system, and what to do about it",
|
|
38
|
+
},
|
|
39
|
+
];
|
package/package.json
CHANGED