synthesisui 0.16.419 → 0.16.421
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/agent-wiring.js +52 -8
- package/dist/changed-files.js +56 -0
- package/dist/commands/connect.js +33 -0
- package/dist/commands/doctor.js +28 -8
- package/dist/commands/hook.js +120 -28
- package/dist/commands/import.js +30 -7
- package/dist/commands/mcp.js +18 -4
- package/dist/commands/upgrade.js +17 -0
- package/dist/config.js +14 -0
- package/dist/copy/connect.pt-BR.js +12 -0
- package/dist/doctor/idiom-names.js +266 -0
- package/dist/doctor/ledger.js +9 -2
- package/dist/doctor/scan.js +38 -2
- package/dist/doctor/tokens.js +9 -1
- package/dist/governed.js +53 -0
- package/dist/guarantee.js +171 -0
- package/dist/install-marks.js +27 -5
- package/dist/merge-census.js +11 -2
- package/dist/outside-scope.js +100 -4
- package/dist/rule-touched.js +28 -38
- package/package.json +1 -1
package/dist/agent-wiring.js
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { hasCodexBlock, pinnedInCodex, withCodexBlock } from "./codex-mcp.js";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* QUANDO A CHECAGEM RODA - e por que `Bash` entrou em 12/09.
|
|
6
|
+
*
|
|
7
|
+
* ═══ O QUE O CLIENTE VIA ═══
|
|
8
|
+
*
|
|
9
|
+
* O filtro era `Write|Edit|MultiEdit`, e com isso a garantia inteira dependia de qual ferramenta o
|
|
10
|
+
* agente escolhesse. Medido no `codelevel` em três sessões: toda edição em lote passou por comando
|
|
11
|
+
* de shell e a checagem produziu **0 linhas**; na única edição feita pela ferramenta de edição ela
|
|
12
|
+
* falou na hora, com a regra do sistema nomeada. O produto promete atuar quando ele escreve
|
|
13
|
+
* frontend - não quando o agente segura a caneta certa.
|
|
14
|
+
*
|
|
15
|
+
* Um comando de shell não diz quais arquivos tocou, então quem responde é a árvore de trabalho -
|
|
16
|
+
* ver `changed-files.ts`, onde também está o custo medido.
|
|
17
|
+
*/
|
|
18
|
+
const HOOK_MATCHER = "Write|Edit|MultiEdit|Bash";
|
|
19
|
+
/**
|
|
20
|
+
* O FILTRO QUE ESTE PRODUTO ESCREVIA ANTES DE 12/09 - e a única string que ele se autoriza a
|
|
21
|
+
* alargar.
|
|
22
|
+
*
|
|
23
|
+
* Quem conectou antes disso tem no arquivo dele um filtro que NÓS escrevemos, e deixá-lo como está
|
|
24
|
+
* seria entregar a correção só para quem instala do zero. Alargar um filtro que uma PESSOA
|
|
25
|
+
* escreveu é outra coisa, e continua proibido: a comparação é por igualdade exata com a nossa
|
|
26
|
+
* string antiga, nunca por parecer com ela.
|
|
27
|
+
*/
|
|
28
|
+
const HOOK_MATCHER_BEFORE_SHELL = "Write|Edit|MultiEdit";
|
|
5
29
|
/**
|
|
6
30
|
* Exportado porque a REGRA é uma só: a pasta de uma ferramenta é a evidência de que ela é usada, e
|
|
7
31
|
* a mesma evidência decide o MCP daqui e a casa do bloco em `claude-md.ts`. Duas cópias deste
|
|
@@ -116,11 +140,27 @@ async function wireHook(root, command) {
|
|
|
116
140
|
* protegia continua protegido: o matcher fica como estiver, os hooks vizinhos ficam, e uma segunda
|
|
117
141
|
* entrada nunca é criada - duas entradas rodariam o verificador duas vezes por escrita.
|
|
118
142
|
*/
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (mine) {
|
|
143
|
+
const owner = post.find((e) => (e.hooks ?? []).some((h) => (h.command ?? "").includes("synthesisui")));
|
|
144
|
+
const mine = (owner?.hooks ?? []).find((h) => (h.command ?? "").includes("synthesisui"));
|
|
145
|
+
if (owner && mine) {
|
|
123
146
|
const was = mine.command ?? "";
|
|
147
|
+
/**
|
|
148
|
+
* O FILTRO NOSSO ANTIGO SOBE JUNTO - ver `HOOK_MATCHER_BEFORE_SHELL`.
|
|
149
|
+
*
|
|
150
|
+
* Sem isto, a checagem passar a valer para edição por shell alcançaria só quem instalasse do
|
|
151
|
+
* zero: quem já tinha conectado ficaria com o filtro estreito para sempre, e nada no produto
|
|
152
|
+
* diria isso a ele.
|
|
153
|
+
*
|
|
154
|
+
* **E SÓ QUANDO A ENTRADA É NOSSA INTEIRA** - achado da revisão de contrato no fecho, e a
|
|
155
|
+
* fronteira estava larga demais. `matcher` vale para TODOS os comandos da entrada: se ele
|
|
156
|
+
* acrescentou um hook dele ali ao lado - um formatador, um linter -, alargar o filtro passaria
|
|
157
|
+
* a disparar o comando DELE depois de todo comando de shell, sem ele ter pedido. É a mesma lei
|
|
158
|
+
* que impede escrever arquivo de um agente que ele não escolheu.
|
|
159
|
+
*/
|
|
160
|
+
const onlyOurs = (owner.hooks ?? []).every((h) => (h.command ?? "").includes("synthesisui"));
|
|
161
|
+
const widen = owner.matcher === HOOK_MATCHER_BEFORE_SHELL && onlyOurs;
|
|
162
|
+
if (widen)
|
|
163
|
+
owner.matcher = HOOK_MATCHER;
|
|
124
164
|
/**
|
|
125
165
|
* SÓ UM PIN NOSSO É SUBSTITUÍDO, e a fronteira é estreita de propósito.
|
|
126
166
|
*
|
|
@@ -132,12 +172,16 @@ async function wireHook(root, command) {
|
|
|
132
172
|
*/
|
|
133
173
|
const pinnedAt = /synthesisui@(\d+\.\d+\.\d+)/.exec(was)?.[1];
|
|
134
174
|
const proposedAt = /synthesisui@(\d+\.\d+\.\d+)/.exec(command)?.[1];
|
|
135
|
-
|
|
175
|
+
const samePin = !pinnedAt || !proposedAt || pinnedAt === proposedAt;
|
|
176
|
+
if (samePin && !widen)
|
|
136
177
|
return { status: "already there", command: was || command };
|
|
137
|
-
|
|
178
|
+
if (!samePin)
|
|
179
|
+
mine.command = command;
|
|
138
180
|
await mkdir(dir, { recursive: true });
|
|
139
181
|
await writeFile(path, `${JSON.stringify({ ...settings, hooks: { ...hooks, PostToolUse: post } }, null, 2)}\n`, "utf8");
|
|
140
|
-
return
|
|
182
|
+
return samePin
|
|
183
|
+
? { status: "updated", command: was || command }
|
|
184
|
+
: { status: "updated", command, was };
|
|
141
185
|
}
|
|
142
186
|
post.push({
|
|
143
187
|
matcher: HOOK_MATCHER,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
/**
|
|
7
|
+
* O QUE A ÁRVORE DE TRABALHO TEM DE DIFERENTE DO ÚLTIMO COMMIT, mais o que não está rastreado.
|
|
8
|
+
*
|
|
9
|
+
* `--relative` e o `cwd` na raiz do projeto: num monorepo cujo `_synthesisui` mora em
|
|
10
|
+
* `apps/landing/`, os caminhos do git saem a partir do topo da ÁRVORE, e compará-los com os do
|
|
11
|
+
* projeto é comparar dois endereços diferentes para o mesmo arquivo. É o mesmo defeito que o
|
|
12
|
+
* `HEAD:./` já conserta do outro lado.
|
|
13
|
+
*/
|
|
14
|
+
async function listed(root) {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const args of [
|
|
17
|
+
["diff", "--name-only", "--relative", "HEAD", "--", "."],
|
|
18
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
19
|
+
]) {
|
|
20
|
+
const { stdout } = await run("git", args, {
|
|
21
|
+
cwd: root,
|
|
22
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
23
|
+
}).catch(() => ({ stdout: "" }));
|
|
24
|
+
for (const line of stdout.split("\n"))
|
|
25
|
+
if (line.trim())
|
|
26
|
+
out.push(line.trim());
|
|
27
|
+
}
|
|
28
|
+
return [...new Set(out)];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* OS ARQUIVOS INTERESSANTES QUE MUDARAM DESDE `since`, do mais recente para o mais antigo.
|
|
32
|
+
*
|
|
33
|
+
* `since` é o instante da última checagem. Sem ele, a árvore inteira de trabalho responderia - e
|
|
34
|
+
* num repositório com trinta arquivos em aberto o primeiro comando de shell do dia produziria um
|
|
35
|
+
* relatório sobre trabalho de ontem. O corte por instante é o que faz a resposta ser *"o que este
|
|
36
|
+
* comando acabou de fazer"*.
|
|
37
|
+
*/
|
|
38
|
+
export async function changedSince(root, since, keep) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const rel of await listed(root)) {
|
|
41
|
+
if (!keep(rel))
|
|
42
|
+
continue;
|
|
43
|
+
const info = await stat(join(root, rel)).catch(() => null);
|
|
44
|
+
if (!info?.isFile())
|
|
45
|
+
continue;
|
|
46
|
+
/**
|
|
47
|
+
* `<` E NÃO `<=`, e o lado do erro é declarado: um arquivo escrito no MESMO milissegundo em
|
|
48
|
+
* que o relógio andou é RELATADO, não perdido. O preço é um relatório repetido numa corrida
|
|
49
|
+
* rara; o preço do outro lado é uma escrita que ninguém checa nunca mais.
|
|
50
|
+
*/
|
|
51
|
+
if (info.mtimeMs < since)
|
|
52
|
+
continue;
|
|
53
|
+
out.push({ rel, at: info.mtimeMs });
|
|
54
|
+
}
|
|
55
|
+
return out.sort((a, b) => b.at - a.at);
|
|
56
|
+
}
|
package/dist/commands/connect.js
CHANGED
|
@@ -4,6 +4,7 @@ import { codexPinBefore, exists, wireAgent } from "../agent-wiring.js";
|
|
|
4
4
|
import { AGENTS, chooseAgents, filesOf, rememberChoice, } from "../agents-chosen.js";
|
|
5
5
|
import { blockHomes, syncClaudeMd } from "../claude-md.js";
|
|
6
6
|
import { resolveRegistry } from "../config.js";
|
|
7
|
+
import { guaranteeIfNew } from "../guarantee.js";
|
|
7
8
|
import { notAProject, projectRootFrom } from "../is-a-project.js";
|
|
8
9
|
import { fmt, say } from "../lang.js";
|
|
9
10
|
import { body, bodyWrapped, paint, section, snippet } from "../output.js";
|
|
@@ -14,6 +15,7 @@ import { SKILLS } from "../skills.js";
|
|
|
14
15
|
import { add, ensureGovernanceHome } from "./add.js";
|
|
15
16
|
import { reportWhatIsLeft } from "./align.js";
|
|
16
17
|
import { ci } from "./ci.js";
|
|
18
|
+
import { loadSystem } from "./doctor.js";
|
|
17
19
|
import { MCP_TOOL_COUNT } from "./mcp.js";
|
|
18
20
|
import { installedSlugs } from "./sync.js";
|
|
19
21
|
/**
|
|
@@ -672,6 +674,37 @@ export async function connect(opts) {
|
|
|
672
674
|
console.log("");
|
|
673
675
|
await ci({ dir: root, write: true });
|
|
674
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* ATÉ ONDE A GARANTIA VAI - uma vez, e de novo só quando a resposta muda.
|
|
679
|
+
*
|
|
680
|
+
* ═══ POR QUE AQUI ═══
|
|
681
|
+
*
|
|
682
|
+
* Este é o comando que LIGA a checagem, então é o único lugar onde a fronteira dela é notícia. O
|
|
683
|
+
* que o produto passa a fazer sozinho no repositório de alguém precisa vir com o que ele NÃO
|
|
684
|
+
* faz, na mesma tela - senão o silêncio da checagem lê como aprovação, que é a lacuna calada da
|
|
685
|
+
* lei 8.
|
|
686
|
+
*
|
|
687
|
+
* A frase sai depois da fiação de propósito: ela fala do estado que este comando acabou de
|
|
688
|
+
* deixar no disco, e não do que havia antes dele.
|
|
689
|
+
*/
|
|
690
|
+
{
|
|
691
|
+
const { table } = await loadSystem(root).catch(() => ({
|
|
692
|
+
table: { name: null, slug: null, utilities: new Map() },
|
|
693
|
+
}));
|
|
694
|
+
const said = await guaranteeIfNew(root, {
|
|
695
|
+
name: table.name ?? table.slug,
|
|
696
|
+
utilities: table.utilities,
|
|
697
|
+
}).catch(() => null);
|
|
698
|
+
if (said) {
|
|
699
|
+
console.log("");
|
|
700
|
+
console.log(section(say("How far this check goes here")));
|
|
701
|
+
for (const line of said) {
|
|
702
|
+
console.log("");
|
|
703
|
+
for (const wrapped of bodyWrapped(fmt(line.text, line.values)))
|
|
704
|
+
console.log(wrapped);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
675
708
|
/** A mesma versão que a fiação do editor recebe - os dois pinam no mesmo número. */
|
|
676
709
|
await offerShellHook(opts.shell === true, opts.version);
|
|
677
710
|
/**
|
package/dist/commands/doctor.js
CHANGED
|
@@ -269,6 +269,23 @@ measured) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
css += `\n${real || root}`;
|
|
272
|
+
/**
|
|
273
|
+
* O ADAPTADOR DE TEMA ENTRA JUNTO - é ele que diz QUAIS utilities este sistema gera.
|
|
274
|
+
*
|
|
275
|
+
* `theme.css` carrega o bloco `@theme`, e é essa declaração que autoriza a régua a ler
|
|
276
|
+
* `bg-primary` como referência a `--color-primary` (ver `idiom-names.ts`). Sem ele, um projeto
|
|
277
|
+
* de idioma utility volta a ser medido com a régua do idioma vizinho - era o caso do
|
|
278
|
+
* `codelevel`, cujo `tokens.css` traz os mesmos nomes, mas dentro do escopo do sistema.
|
|
279
|
+
*
|
|
280
|
+
* As declarações são as MESMAS que já vieram acima, e a primeira vence: nenhum valor muda, só
|
|
281
|
+
* o marcador passa a ser visível. Ausente é o caso normal de um sistema adotado.
|
|
282
|
+
*/
|
|
283
|
+
const themePointer = await readFile(join(dir, "theme.css"), "utf8").catch(() => "");
|
|
284
|
+
const theme = mine?.version
|
|
285
|
+
? await readFile(join(dir, `v${mine.version}`, "theme.css"), "utf8").catch(() => "")
|
|
286
|
+
: "";
|
|
287
|
+
if (theme || themePointer)
|
|
288
|
+
css += `\n${theme || themePointer}`;
|
|
272
289
|
// An ADOPTED system has no tokens.css of ours - `adopt` deliberately
|
|
273
290
|
// writes no CSS, because the project's own stylesheet already works. Its
|
|
274
291
|
// vocabulary lives in system.json, and without this the doctor falls
|
|
@@ -894,9 +911,11 @@ export async function doctor(opts) {
|
|
|
894
911
|
: "") +
|
|
895
912
|
" (--verbose for why)"));
|
|
896
913
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
914
|
+
/**
|
|
915
|
+
* 0 de 0 não é nota máxima, é medição vazia - e desde 12/09 a própria régua carrega essa
|
|
916
|
+
* distinção em vez de cada tela refazê-la. Ver `Reach` em `scan.ts`.
|
|
917
|
+
*/
|
|
918
|
+
const measurable = d.reach.measured;
|
|
900
919
|
// Before the number, because the number is the thing that misleads. An
|
|
901
920
|
// installed-but-unwired system reads 0%, and 0% reads as "broken product"
|
|
902
921
|
// rather than "one import missing".
|
|
@@ -1230,7 +1249,7 @@ export async function doctor(opts) {
|
|
|
1230
1249
|
await appendEvent(root, {
|
|
1231
1250
|
kind: "doctor",
|
|
1232
1251
|
at: new Date().toISOString(),
|
|
1233
|
-
coverage: d.
|
|
1252
|
+
...(d.reach.measured ? { coverage: d.reach.percent } : {}),
|
|
1234
1253
|
rule: COVERAGE_RULE,
|
|
1235
1254
|
named: d.named,
|
|
1236
1255
|
...(d.findings.some((f) => f.crossFamily)
|
|
@@ -1321,9 +1340,10 @@ export async function doctor(opts) {
|
|
|
1321
1340
|
console.log(body(" and every shadcn component switches over."));
|
|
1322
1341
|
}
|
|
1323
1342
|
}
|
|
1324
|
-
if (hasSystem &&
|
|
1343
|
+
if (hasSystem && d.reach.measured) {
|
|
1344
|
+
const { percent, uses, of } = d.reach;
|
|
1325
1345
|
console.log("");
|
|
1326
|
-
console.log(body(`Token coverage ${meter(
|
|
1346
|
+
console.log(body(`Token coverage ${meter(percent)} ${paint.strong(`${String(percent).padStart(3)}%`)} ${paint.dim(`${uses} of ${of} design values come from the system`)}`));
|
|
1327
1347
|
console.log(body(paint.dim(
|
|
1328
1348
|
/**
|
|
1329
1349
|
* DE QUEM É O VOCABULÁRIO, dito na própria linha.
|
|
@@ -1435,7 +1455,7 @@ export async function doctor(opts) {
|
|
|
1435
1455
|
* O `1` não muda nada do que a frase afirma: os valores escritos à mão continuam sendo a FONTE
|
|
1436
1456
|
* de onde os tokens saíram, e não um desvio deles.
|
|
1437
1457
|
*/
|
|
1438
|
-
if (d.
|
|
1458
|
+
if (d.reach.measured && d.reach.percent === 0 && measured.system) {
|
|
1439
1459
|
console.log(body(paint.dim(` zero is the expected start here - this system was measured FROM`)));
|
|
1440
1460
|
console.log(body(paint.dim(` \`${measured.system}\`, so these values are where the tokens`)));
|
|
1441
1461
|
console.log(body(paint.dim(` came from. They count once the code points at the names they became.`)));
|
|
@@ -1478,7 +1498,7 @@ export async function doctor(opts) {
|
|
|
1478
1498
|
await appendEvent(root, {
|
|
1479
1499
|
kind: "doctor",
|
|
1480
1500
|
at: new Date().toISOString(),
|
|
1481
|
-
coverage: d.
|
|
1501
|
+
...(d.reach.measured ? { coverage: d.reach.percent } : {}),
|
|
1482
1502
|
/** Qual régua mediu - ver `COVERAGE_RULE`. Sem isto a tendência compara duas réguas. */
|
|
1483
1503
|
rule: COVERAGE_RULE,
|
|
1484
1504
|
named: d.named,
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { changedSince } from "../changed-files.js";
|
|
4
|
+
import { appendEvent, ledgerPath } from "../doctor/ledger.js";
|
|
4
5
|
import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
|
|
5
|
-
import {
|
|
6
|
+
import { governs, ungovernedIn } from "../governed.js";
|
|
7
|
+
import { namesRemoved, previousText, rulesTouchedBy, } from "../rule-touched.js";
|
|
6
8
|
import { loadSystem } from "./doctor.js";
|
|
7
9
|
const pass = () => ({ continue: true });
|
|
8
10
|
const speak = (context) => ({
|
|
@@ -12,15 +14,59 @@ const speak = (context) => ({
|
|
|
12
14
|
additionalContext: context,
|
|
13
15
|
},
|
|
14
16
|
});
|
|
15
|
-
/** Files where a design value can even appear. A hook that parses a README on
|
|
16
|
-
* every edit is paying for nothing. */
|
|
17
|
-
const UI_FILE = /\.(tsx|jsx|ts|js|css|scss|vue|svelte)$/i;
|
|
18
17
|
/** Written after the hook's first report of any kind, and never read for anything
|
|
19
18
|
* else. Lives in our own directory, so removing the system removes the memory
|
|
20
19
|
* too. It is committed with `_synthesisui/`, which means the greeting is
|
|
21
20
|
* per-project and not per-teammate - a wart worth the simplicity, since the
|
|
22
21
|
* second person to clone a wired repo is not the one wondering if it installed. */
|
|
23
22
|
const GREETED = "_synthesisui/.hook-greeted";
|
|
23
|
+
/**
|
|
24
|
+
* QUANDO ESTA CHECAGEM OLHOU PELA ÚLTIMA VEZ - e o relógio é o registro que ela já escreve.
|
|
25
|
+
*
|
|
26
|
+
* ═══ POR QUE NÃO UM ARQUIVO NOVO ═══
|
|
27
|
+
*
|
|
28
|
+
* A primeira versão criava `_synthesisui/.hook-seen` só para carregar uma data de modificação, e a
|
|
29
|
+
* revisão de QA no fecho achou o custo: um arquivo NOVO no repositório dele, não rastreado, que a
|
|
30
|
+
* lista de ignorados gerenciada só alcança em projeto novo - porque ela é escrita uma vez e nunca
|
|
31
|
+
* reescrita, e isso é decisão declarada em `commands/add.ts`.
|
|
32
|
+
*
|
|
33
|
+
* O livro-razão já responde a mesma pergunta e já é ignorado: ele ganha uma linha a cada arquivo
|
|
34
|
+
* checado, então a data de modificação dele É o instante da última checagem. Um `doctor` também o
|
|
35
|
+
* move, e isso está certo - ele olhou o projeto inteiro.
|
|
36
|
+
*
|
|
37
|
+
* ═══ E SEM ELE A RESPOSTA É "NUNCA OLHEI", que não é "olhei em 1970" ═══
|
|
38
|
+
*
|
|
39
|
+
* Zero faria a árvore inteira de trabalho responder, e o primeiro comando do dia produziria um
|
|
40
|
+
* relatório sobre o trabalho de ontem - ver `baseline`.
|
|
41
|
+
*/
|
|
42
|
+
const clockOf = async (root) => stat(ledgerPath(root)).then((s) => s.mtimeMs, () => 0);
|
|
43
|
+
/**
|
|
44
|
+
* A PRIMEIRA VEZ NÃO FALA DE TRABALHO QUE ELE NÃO ACABOU DE FAZER - achado da revisão de QA.
|
|
45
|
+
*
|
|
46
|
+
* Num repositório com cinco arquivos em aberto desde ontem, o primeiro comando de shell - um `ls`,
|
|
47
|
+
* um `git status` - encontraria os cinco e falaria de três. Isso é ruído sobre trabalho que
|
|
48
|
+
* ninguém acabou de tocar, e ruído é o que faz alguém desinstalar a checagem.
|
|
49
|
+
*
|
|
50
|
+
* Então a primeira rodada ACERTA O RELÓGIO e cala. O que se perde é um relatório, na única vez em
|
|
51
|
+
* que o produto acabou de ser instalado - e a tela do `connect` já disse que ele está vivo.
|
|
52
|
+
*/
|
|
53
|
+
const startClock = (root) => writeFile(ledgerPath(root), "", { flag: "a" }).catch(() => { });
|
|
54
|
+
/**
|
|
55
|
+
* QUANTOS ARQUIVOS UM COMANDO DE SHELL PODE FAZER O PRODUTO RELATAR DE UMA VEZ.
|
|
56
|
+
*
|
|
57
|
+
* Um script que reescreve trinta arquivos produziria trinta relatórios numa resposta só, e o
|
|
58
|
+
* cabeçalho deste arquivo já diz o que acontece com uma checagem barulhenta. Três é o que cabe numa
|
|
59
|
+
* leitura.
|
|
60
|
+
*
|
|
61
|
+
* **E O QUE FICA DE FORA É NOMEADO** - achado da revisão de QA no fecho, e o defeito era pior que o
|
|
62
|
+
* corte: o relógio andava por cima de TODOS os arquivos, então do quarto em diante eles não eram
|
|
63
|
+
* relatados por ninguém, nunca mais. Um script de dez telas rendia três relatórios e sete arquivos
|
|
64
|
+
* apagados em silêncio. Agora os outros saem pelo NOME, com o comando que os olha.
|
|
65
|
+
*
|
|
66
|
+
* E a ordem é do mais recente para o mais antigo: numa lista cortada, o que o comando acabou de
|
|
67
|
+
* escrever é o que a pessoa tem em mente.
|
|
68
|
+
*/
|
|
69
|
+
const AT_MOST = 3;
|
|
24
70
|
const markGreeted = (root) => writeFile(join(root, GREETED), "", "utf8").catch(() => { });
|
|
25
71
|
/** One sentence, once per project. Addressed to the agent because the hook has
|
|
26
72
|
* no way to reach the person, and the person is who needs to know. */
|
|
@@ -72,13 +118,13 @@ async function greet(root, rel) {
|
|
|
72
118
|
* O retorno continua sendo contexto: `continue: true` em todo caminho. E nenhum nome é proposto -
|
|
73
119
|
* a frase diz qual regra, qual nome declarado amarra a regra ao que saiu, e devolve a decisão.
|
|
74
120
|
*/
|
|
75
|
-
async function amendment(root, rel, after, rules,
|
|
121
|
+
async function amendment(root, rel, after, rules, names) {
|
|
76
122
|
if (rules.length === 0)
|
|
77
123
|
return [];
|
|
78
124
|
const before = await previousText(root, rel);
|
|
79
125
|
if (before == null)
|
|
80
126
|
return [];
|
|
81
|
-
const touched = rulesTouchedBy(namesRemoved(before, after,
|
|
127
|
+
const touched = rulesTouchedBy(namesRemoved(before, after, names), rules);
|
|
82
128
|
if (touched.length === 0)
|
|
83
129
|
return [];
|
|
84
130
|
return [
|
|
@@ -121,7 +167,7 @@ async function report(root, filePath) {
|
|
|
121
167
|
if (src == null)
|
|
122
168
|
return null;
|
|
123
169
|
const rel = relative(root, filePath);
|
|
124
|
-
const broke = await amendment(root, rel, src, doctrines.flatMap((doc) => doc.rules), table
|
|
170
|
+
const broke = await amendment(root, rel, src, doctrines.flatMap((doc) => doc.rules), table).catch(() => []);
|
|
125
171
|
const d = diagnose([scanSource(rel, src, table)]);
|
|
126
172
|
const named = d.findings.filter((f) => nameToWrite(f));
|
|
127
173
|
/**
|
|
@@ -228,6 +274,18 @@ async function report(root, filePath) {
|
|
|
228
274
|
}
|
|
229
275
|
return lines.join("\n");
|
|
230
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* O QUE O AGENTE ACABOU DE ESCREVER, pelo caminho que o cliente do agente nomeia.
|
|
279
|
+
*
|
|
280
|
+
* `Write`, `Edit` e `MultiEdit` trazem o arquivo no payload, e é a resposta mais barata que
|
|
281
|
+
* existe: nenhuma pergunta ao git, nenhum `stat`.
|
|
282
|
+
*/
|
|
283
|
+
const namedInPayload = (input) => {
|
|
284
|
+
const writes = input.tool_name === "Write" ||
|
|
285
|
+
input.tool_name === "Edit" ||
|
|
286
|
+
input.tool_name === "MultiEdit";
|
|
287
|
+
return writes ? (input.tool_input?.file_path ?? null) : null;
|
|
288
|
+
};
|
|
231
289
|
export async function hook(opts) {
|
|
232
290
|
const root = resolve(opts.dir ?? process.cwd());
|
|
233
291
|
let raw = "";
|
|
@@ -243,32 +301,66 @@ export async function hook(opts) {
|
|
|
243
301
|
process.stdout.write(`${JSON.stringify(pass())}\n`);
|
|
244
302
|
return;
|
|
245
303
|
}
|
|
246
|
-
|
|
247
|
-
input.tool_name === "Edit" ||
|
|
248
|
-
input.tool_name === "MultiEdit";
|
|
249
|
-
const file = input.tool_input?.file_path;
|
|
250
|
-
if (input.hook_event_name !== "PostToolUse" || !writes || !file) {
|
|
304
|
+
if (input.hook_event_name !== "PostToolUse") {
|
|
251
305
|
process.stdout.write(`${JSON.stringify(pass())}\n`);
|
|
252
306
|
return;
|
|
253
307
|
}
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
// outside the project is none of our business.
|
|
308
|
+
const ungoverned = await ungovernedIn(root);
|
|
309
|
+
const named = namedInPayload(input);
|
|
257
310
|
/**
|
|
258
|
-
*
|
|
311
|
+
* A FERRAMENTA DE EDIÇÃO DIZ O ARQUIVO; QUALQUER OUTRA COISA PERGUNTA À ÁRVORE.
|
|
259
312
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
313
|
+
* Um comando de shell não tem como dizer o que escreveu, e era exatamente por ali que passavam
|
|
314
|
+
* as edições em lote das duas sessões medidas em 12/09 - ver o cabeçalho deste arquivo e
|
|
315
|
+
* `changed-files.ts`. As duas chamadas ao git custaram 12ms medidos, e elas vêm ANTES de
|
|
316
|
+
* qualquer leitura de folha de estilo: quando nada mudou, isto termina aqui.
|
|
263
317
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
318
|
+
if (named) {
|
|
319
|
+
const rel = relative(root, resolve(root, named));
|
|
320
|
+
const body = governs(rel, ungoverned)
|
|
321
|
+
? await report(root, join(root, rel)).catch(() => null)
|
|
322
|
+
: null;
|
|
323
|
+
process.stdout.write(`${JSON.stringify(body ? speak(body) : pass())}\n`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const clock = await clockOf(root);
|
|
327
|
+
/** Ver `startClock`: a primeira rodada acerta o relógio e cala. */
|
|
328
|
+
if (clock === 0) {
|
|
329
|
+
await startClock(root);
|
|
269
330
|
process.stdout.write(`${JSON.stringify(pass())}\n`);
|
|
270
331
|
return;
|
|
271
332
|
}
|
|
272
|
-
const
|
|
273
|
-
|
|
333
|
+
const changed = await changedSince(root, clock, (rel) => governs(rel, ungoverned)).catch(() => []);
|
|
334
|
+
if (changed.length === 0) {
|
|
335
|
+
process.stdout.write(`${JSON.stringify(pass())}\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const said = [];
|
|
339
|
+
for (const { rel } of changed.slice(0, AT_MOST)) {
|
|
340
|
+
const body = await report(root, join(root, rel)).catch(() => null);
|
|
341
|
+
if (body)
|
|
342
|
+
said.push(body);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* O QUE FICOU DE FORA É NOMEADO, e com o comando que o olha - ver `AT_MOST`.
|
|
346
|
+
*
|
|
347
|
+
* O corte calado era pior do que parecia: o relógio anda por cima de TODOS os arquivos que esta
|
|
348
|
+
* rodada encontrou, então do quarto em diante eles não voltariam a ser vistos por ninguém. Sair
|
|
349
|
+
* pelo nome é o que transforma uma perda silenciosa numa lacuna declarada (lei 8).
|
|
350
|
+
*/
|
|
351
|
+
const rest = changed.slice(AT_MOST);
|
|
352
|
+
if (rest.length > 0)
|
|
353
|
+
said.push([
|
|
354
|
+
`${rest.length} more file${rest.length === 1 ? "" : "s"} changed in this command and ${rest.length === 1 ? "was" : "were"} not checked here: ${rest
|
|
355
|
+
.slice(0, 10)
|
|
356
|
+
.map((c) => c.rel)
|
|
357
|
+
.join(", ")}${rest.length > 10 ? `, +${rest.length - 10} more` : ""}.`,
|
|
358
|
+
"Run `npx synthesisui doctor` to see them, or write one of them again and this will check it.",
|
|
359
|
+
].join("\n"));
|
|
360
|
+
/**
|
|
361
|
+
* E O RELÓGIO ANDA AQUI, depois de olhar - `appendEvent` já o moveu para cada arquivo relatado,
|
|
362
|
+
* e esta linha cobre o caso em que todos os relatórios falharam na leitura.
|
|
363
|
+
*/
|
|
364
|
+
await startClock(root);
|
|
365
|
+
process.stdout.write(`${JSON.stringify(said.length > 0 ? speak(said.join("\n\n")) : pass())}\n`);
|
|
274
366
|
}
|
package/dist/commands/import.js
CHANGED
|
@@ -5,7 +5,7 @@ import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
|
|
|
5
5
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
6
6
|
import { craftLines } from "../baseline-craft.js";
|
|
7
7
|
import { PAGES_MAX, pageCompositionOf } from "../census-pages.js";
|
|
8
|
-
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
8
|
+
import { readCredentials, readProjectConfig, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
9
9
|
import { declaredElsewhere } from "../declared-elsewhere.js";
|
|
10
10
|
import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, packagingOf, proposeNewHome, resolvesAs, } from "../doctor/architecture.js";
|
|
11
11
|
import { findBrokenRefs } from "../doctor/broken-refs.js";
|
|
@@ -52,7 +52,7 @@ import { mergeNamespacePairs } from "../namespace-pairs.js";
|
|
|
52
52
|
import { namingQueue } from "../naming-queue.js";
|
|
53
53
|
import { notExpressed } from "../not-expressed.js";
|
|
54
54
|
import { body, paint, section } from "../output.js";
|
|
55
|
-
import { outsideScope } from "../outside-scope.js";
|
|
55
|
+
import { consumersOf, outsideScope, } from "../outside-scope.js";
|
|
56
56
|
import { phase, startProgress } from "../progress.js";
|
|
57
57
|
import { repoStateOf } from "../repo-state.js";
|
|
58
58
|
import { runtimeDeclaredVars, runtimeDeclaredVarsIn } from "../runtime-vars.js";
|
|
@@ -477,10 +477,11 @@ export async function takeCensus(root, opts) {
|
|
|
477
477
|
const repoRoot = scopeLabel && root.endsWith(join(sep, ...scopeLabel.split("/")))
|
|
478
478
|
? root.slice(0, root.length - scopeLabel.length - 1)
|
|
479
479
|
: root;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
480
|
+
/**
|
|
481
|
+
* A MEDIÇÃO EM SI ACONTECE MAIS ABAIXO, quando o nome do pacote do escopo já foi lido - ver
|
|
482
|
+
* `scopePkg`. É ele que diz quem CONSOME a biblioteca, e sem essa resposta a linha do fora
|
|
483
|
+
* nomeia os lugares sem design e cala sobre os dois apps onde ele escreve tela.
|
|
484
|
+
*/
|
|
484
485
|
/**
|
|
485
486
|
* THE LIVE CATALOGUE, once, before anything is matched.
|
|
486
487
|
*
|
|
@@ -2494,6 +2495,23 @@ export async function takeCensus(root, opts) {
|
|
|
2494
2495
|
* terceiro, e os dois casos são nós já nomeados. Sem eles a fila cobraria trabalho que não
|
|
2495
2496
|
* existe, que é exatamente o defeito que ela vem medir.
|
|
2496
2497
|
*/
|
|
2498
|
+
/**
|
|
2499
|
+
* O QUE O ESCOPO DEIXOU DE FORA, e quem continua governado apesar de estar fora dele.
|
|
2500
|
+
*
|
|
2501
|
+
* Aqui e não lá em cima porque a resposta depende de `scopePkg`: os consumidores são quem
|
|
2502
|
+
* DECLARA depender desta biblioteca, e a saída deles é a lista que ele escreveu no config.
|
|
2503
|
+
*/
|
|
2504
|
+
const outside = await outsideScope(repoRoot, [
|
|
2505
|
+
...(scopeLabel ? [scopeLabel] : []),
|
|
2506
|
+
...(opts?.usage ?? []).map((u) => u.label),
|
|
2507
|
+
], {
|
|
2508
|
+
consumers: [
|
|
2509
|
+
...(opts?.usage ?? []).map((u) => u.label),
|
|
2510
|
+
...(await consumersOf(repoRoot, scopePkg).catch(() => [])),
|
|
2511
|
+
],
|
|
2512
|
+
ungoverned: (await readProjectConfig(repoRoot).catch(() => null))
|
|
2513
|
+
?.ungoverned,
|
|
2514
|
+
}).catch(() => null);
|
|
2497
2515
|
const naming = namingQueue(looks, (name) => {
|
|
2498
2516
|
const c = components.find((x) => x.name === name && !x.from);
|
|
2499
2517
|
const target = c?.canonical ?? (c?.bucket === "exclusive" ? c.name : null);
|
|
@@ -2824,7 +2842,12 @@ export async function takeCensus(root, opts) {
|
|
|
2824
2842
|
values: d.findings.length,
|
|
2825
2843
|
named: d.named,
|
|
2826
2844
|
tokenUses: d.tokenUses,
|
|
2827
|
-
|
|
2845
|
+
/**
|
|
2846
|
+
* O PERCENTUAL SÓ EXISTE QUANDO HOUVE O QUE MEDIR - ver `Reach` em `scan.ts`. Um repositório
|
|
2847
|
+
* sem um único valor de design não tira 100: o campo simplesmente não sai, e quem lê o censo
|
|
2848
|
+
* distingue "não veio do sistema" de "não havia o que contar".
|
|
2849
|
+
*/
|
|
2850
|
+
...(d.reach.measured ? { coverage: d.reach.percent } : {}),
|
|
2828
2851
|
ownUses: d.ownUses,
|
|
2829
2852
|
phantomUses: d.phantomUses,
|
|
2830
2853
|
},
|
package/dist/commands/mcp.js
CHANGED
|
@@ -386,10 +386,24 @@ async function checkFile(root, path) {
|
|
|
386
386
|
if (reports.length === 0)
|
|
387
387
|
return fromContract(`Nothing readable at ${path}.`);
|
|
388
388
|
const d = diagnose(reports);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
389
|
+
/**
|
|
390
|
+
* `0%` DEIXOU DE RESPONDER POR TRÊS COISAS - ver `Reach` em `scan.ts`.
|
|
391
|
+
*
|
|
392
|
+
* Esta linha dizia `0% of design values come from the system` tanto para um arquivo que escreve
|
|
393
|
+
* tudo à mão quanto para um onde a régua não tinha o que ler. Medido em 12/09 no `codelevel`:
|
|
394
|
+
* `CapabilityCard.tsx` tem 0 usos e 0 achados, e a mesma conta interna que aqui devolvia `0%`
|
|
395
|
+
* devolvia `100%` no relatório do projeto inteiro. Agora a frase é outra, e ela é a verdade
|
|
396
|
+
* daquele arquivo em vez de uma nota que ninguém consegue conferir.
|
|
397
|
+
*/
|
|
398
|
+
const out = d.reach.measured
|
|
399
|
+
? [
|
|
400
|
+
`${table.name ?? table.slug}: ${d.reach.percent}% of design values come from the system - ${d.reach.uses} of ${d.reach.of}.`,
|
|
401
|
+
`${d.tokenUses} from the system, ${d.findings.length} written by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}.`,
|
|
402
|
+
]
|
|
403
|
+
: [
|
|
404
|
+
`${table.name ?? table.slug}: ${d.reach.why} here, so there is no coverage to report - not 0%, and not 100%.`,
|
|
405
|
+
"Nothing in this file writes a colour, a length, a font or a shadow.",
|
|
406
|
+
];
|
|
393
407
|
if (d.findings.length > 0) {
|
|
394
408
|
out.push("", "Written by hand:");
|
|
395
409
|
for (const f of d.findings.slice(0, 40)) {
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -335,6 +335,23 @@ export async function upgrade(asked, opts) {
|
|
|
335
335
|
}
|
|
336
336
|
if (!opts.force) {
|
|
337
337
|
console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
|
|
338
|
+
/**
|
|
339
|
+
* A FIAÇÃO É REFEITA AQUI TAMBÉM - e a falta disto era um comando que mandava rodar ELE
|
|
340
|
+
* MESMO (dono, 12/09).
|
|
341
|
+
*
|
|
342
|
+
* `rewireIfBehind` só era chamado dentro do ramo de rematerialização, ou seja: só quando a
|
|
343
|
+
* PASTA estava atrasada. A pasta e o hook são dois lugares diferentes e ficam defasados por
|
|
344
|
+
* motivos diferentes - `align` já os trata como dois fatos, e o comentário dele diz por quê:
|
|
345
|
+
* *"um alarme desligado por um conserto parcial é pior que alarme nenhum"*. Faltava o outro
|
|
346
|
+
* lado da mesma moeda: um CONSERTO parcial que desliga nada.
|
|
347
|
+
*
|
|
348
|
+
* O que ele viu: pasta em dia, hook pinado em 0.16.416, CLI rodando 0.16.419. O `upgrade`
|
|
349
|
+
* imprimia *"already at the latest version"* e saía, e o alinhamento seguinte recomendava
|
|
350
|
+
* `npx synthesisui upgrade` - o comando que ele tinha acabado de rodar.
|
|
351
|
+
*
|
|
352
|
+
* `wireAgent` é um merge idempotente: quando já está na versão certa, não escreve nada.
|
|
353
|
+
*/
|
|
354
|
+
await rewireIfBehind(root, opts.cli);
|
|
338
355
|
/**
|
|
339
356
|
* E AQUI TAMBEM, que e' o caminho mais percorrido de todos: sem gap de versao, este comando
|
|
340
357
|
* dizia uma linha e sumia. Quem editou tres componentes ficava sem saber se eles seguem sendo
|
package/dist/config.js
CHANGED
|
@@ -163,6 +163,20 @@ export async function readProjectConfig(root) {
|
|
|
163
163
|
...(parsed.absorb === "code" || parsed.absorb === "system"
|
|
164
164
|
? { absorb: parsed.absorb }
|
|
165
165
|
: {}),
|
|
166
|
+
/**
|
|
167
|
+
* O QUE ELE TIROU DA GOVERNANÇA - ver `ProjectConfig.ungoverned`.
|
|
168
|
+
*
|
|
169
|
+
* Sem o campo, a lista é vazia e tudo que consome a biblioteca segue governado. Entradas que
|
|
170
|
+
* não são caminho são descartadas em silêncio: um config meio escrito não pode governar mais
|
|
171
|
+
* do que o dono dele pediu, e também não pode derrubar o comando.
|
|
172
|
+
*/
|
|
173
|
+
...(Array.isArray(parsed.ungoverned)
|
|
174
|
+
? {
|
|
175
|
+
ungoverned: parsed.ungoverned
|
|
176
|
+
.filter((x) => typeof x === "string" && !!x.trim())
|
|
177
|
+
.map((x) => x.trim().replace(/^\.\//, "").replace(/\/$/, "")),
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
166
180
|
};
|
|
167
181
|
}
|
|
168
182
|
catch {
|