synthesisui 0.16.257 → 0.16.260
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/claude-md.js +2 -1
- package/dist/commands/absorb.js +2 -1
- package/dist/commands/add.js +13 -8
- package/dist/commands/align.js +3 -2
- package/dist/commands/doctor.js +29 -3
- package/dist/commands/import.js +4 -4
- package/dist/commands/summary.js +1 -1
- package/dist/commands/sync.js +63 -26
- package/dist/config.js +32 -2
- package/dist/doctor/architecture.js +44 -5
- package/dist/group-role.js +54 -0
- package/dist/index.js +9 -1
- package/dist/install-marks.js +1 -1
- package/dist/measured-scope.js +57 -3
- package/dist/merge-census.js +34 -14
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { hasHook } from "./agent-wiring.js";
|
|
4
|
+
import { declaredReference } from "./group-role.js";
|
|
4
5
|
const START = "<!-- synthesisui:start -->";
|
|
5
6
|
const END = "<!-- synthesisui:end -->";
|
|
6
7
|
/** Reads the installed DSs from the .lock files in _synthesisui/ds/<slug>/. */
|
|
@@ -326,7 +327,7 @@ async function renderRegion(projectRoot, installed) {
|
|
|
326
327
|
* repo de um sistema só para negar uma relação que ninguém cogitou é ruído no
|
|
327
328
|
* arquivo mais lido do projeto.
|
|
328
329
|
*/
|
|
329
|
-
const ref = installed
|
|
330
|
+
const ref = declaredReference(installed);
|
|
330
331
|
if (installed.length > 1 && ref) {
|
|
331
332
|
sections.push(`\n**${ref.name}** (\`${ref.slug}\`) is the reference system of this group. When two of the systems above name the same thing differently, its answer is the one to follow - the others are moving towards it.`);
|
|
332
333
|
}
|
package/dist/commands/absorb.js
CHANGED
|
@@ -49,7 +49,8 @@ export async function absorb(opts) {
|
|
|
49
49
|
* dela - o mesmo caminho que `absorb: "code"` já servia a quem escolheu. Ver `sendProposal`.
|
|
50
50
|
*/
|
|
51
51
|
const hasSystem = Boolean(installed.table.slug);
|
|
52
|
-
|
|
52
|
+
/** O escopo DESTE sistema - ver `actingSlug`: sem o slug o vizinho responde. */
|
|
53
|
+
const measured = await measuredScope(root, installed.table.slug ?? undefined);
|
|
53
54
|
const rel = scopePaths(measured);
|
|
54
55
|
const roots = rel.length > 0 ? rel.map((s) => resolve(root, s)) : [root];
|
|
55
56
|
/**
|
package/dist/commands/add.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
5
|
import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
|
|
6
|
+
import { lockReference } from "../group-role.js";
|
|
6
7
|
import { buildGuide } from "../guide.js";
|
|
7
8
|
import { censusScope } from "../measured-scope.js";
|
|
8
9
|
import { body as line, section, snippet } from "../output.js";
|
|
@@ -195,15 +196,19 @@ export async function add(slug, opts) {
|
|
|
195
196
|
/**
|
|
196
197
|
* A RELAÇÃO ENTRE OS SISTEMAS DESTE REPO, vinda da plataforma.
|
|
197
198
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
199
|
+
* `payload.group` é a resposta COMPLETA - ver `RegistryPayload.group`: quando
|
|
200
|
+
* ela vem, ela manda, inclusive para APAGAR a referência. Foi a metade que
|
|
201
|
+
* faltava: clicar "eles são independentes" na tela do grupo não chegava aqui,
|
|
202
|
+
* porque o campo ausente era indistinguível de servidor antigo.
|
|
203
|
+
*
|
|
204
|
+
* Sem `group`, o comportamento antigo continua - preservar o que está no lock,
|
|
205
|
+
* senão a resposta oscilaria entre dois `add` contra um registry que não
|
|
206
|
+
* conhece o campo.
|
|
201
207
|
*/
|
|
202
|
-
...(
|
|
203
|
-
|
|
204
|
-
:
|
|
205
|
-
|
|
206
|
-
: {}),
|
|
208
|
+
...(() => {
|
|
209
|
+
const reference = lockReference(payload, prev);
|
|
210
|
+
return reference ? { reference } : {};
|
|
211
|
+
})(),
|
|
207
212
|
};
|
|
208
213
|
const landed = (() => {
|
|
209
214
|
if (!prev?.fetchedAt)
|
package/dist/commands/align.js
CHANGED
|
@@ -6,6 +6,7 @@ import { isOlderCli } from "../cli-version.js";
|
|
|
6
6
|
import { readToken, resolveRegistry } from "../config.js";
|
|
7
7
|
import { unsentEvents } from "../doctor/ledger.js";
|
|
8
8
|
import { readRequests } from "../doctor/requests.js";
|
|
9
|
+
import { declaredReference } from "../group-role.js";
|
|
9
10
|
import { CHECKER_SINCE, installedBehind, MATERIALISER_SINCE, READER_SINCE, } from "../install-marks.js";
|
|
10
11
|
import { measuredScope } from "../measured-scope.js";
|
|
11
12
|
import { body } from "../output.js";
|
|
@@ -110,7 +111,7 @@ opts = {}) {
|
|
|
110
111
|
* comandos seguem agindo sobre o primeiro, e agora dá para dizer QUAL deveria
|
|
111
112
|
* ser. Sem referência, o aviso é o de sempre.
|
|
112
113
|
*/
|
|
113
|
-
const declared = locks
|
|
114
|
+
const declared = declaredReference(locks);
|
|
114
115
|
if (locks.length > 1) {
|
|
115
116
|
const names = locks.map((l) => l.slug).join(", ");
|
|
116
117
|
if (declared && locks.some((l) => l.slug === declared.slug)) {
|
|
@@ -161,7 +162,7 @@ opts = {}) {
|
|
|
161
162
|
* e manda os componentes de todos os apps para um sistema que é uma biblioteca (06/08, 338 num
|
|
162
163
|
* sistema de 36).
|
|
163
164
|
*/
|
|
164
|
-
const scope = await measuredScope(root);
|
|
165
|
+
const scope = await measuredScope(root, lock.slug);
|
|
165
166
|
if (!scope.system && scope.usage.length === 0)
|
|
166
167
|
out.push({
|
|
167
168
|
says: `"${lock.slug}" does not record where it was measured, so a re-measure would read this whole repo instead of the folder the system came from. Sync asks once and remembers the answer.`,
|
package/dist/commands/doctor.js
CHANGED
|
@@ -16,7 +16,8 @@ import { diagnose, nameToWrite, scanSource, siblingTokens, } from "../doctor/sca
|
|
|
16
16
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
17
17
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
18
18
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
19
|
-
import {
|
|
19
|
+
import { groupRole } from "../group-role.js";
|
|
20
|
+
import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
|
|
20
21
|
import { body, paint, section, snippet } from "../output.js";
|
|
21
22
|
import { resolveDeps } from "../stack.js";
|
|
22
23
|
/**
|
|
@@ -192,6 +193,7 @@ measured) {
|
|
|
192
193
|
documents: [],
|
|
193
194
|
requires: [],
|
|
194
195
|
doctrines: [],
|
|
196
|
+
groupLocks: [],
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
// Several systems can live side by side; every token is prefixed --ds-, so
|
|
@@ -203,6 +205,7 @@ measured) {
|
|
|
203
205
|
const documents = [];
|
|
204
206
|
const requires = [];
|
|
205
207
|
const doctrines = [];
|
|
208
|
+
const groupLocks = [];
|
|
206
209
|
for (const slug of slugs) {
|
|
207
210
|
const dir = join(dsDir, slug);
|
|
208
211
|
const raw = await readFile(join(dir, ".lock"), "utf8").catch(() => "");
|
|
@@ -214,6 +217,11 @@ measured) {
|
|
|
214
217
|
mine = null;
|
|
215
218
|
}
|
|
216
219
|
lock ??= mine;
|
|
220
|
+
if (mine?.slug)
|
|
221
|
+
groupLocks.push({
|
|
222
|
+
slug: mine.slug,
|
|
223
|
+
...(mine.reference ? { reference: mine.reference } : {}),
|
|
224
|
+
});
|
|
217
225
|
// The file at the root is a POINTER - `@import "./v1/tokens.css"` - so the
|
|
218
226
|
// pinned folder is where the declarations actually live. Read that first,
|
|
219
227
|
// and fall back to following whatever the root imports, for a project that
|
|
@@ -331,6 +339,7 @@ measured) {
|
|
|
331
339
|
documents,
|
|
332
340
|
requires,
|
|
333
341
|
doctrines,
|
|
342
|
+
groupLocks,
|
|
334
343
|
};
|
|
335
344
|
}
|
|
336
345
|
/**
|
|
@@ -566,7 +575,15 @@ export async function doctor(opts) {
|
|
|
566
575
|
*/
|
|
567
576
|
const measured = (opts.scopes ?? []).length > 0
|
|
568
577
|
? { system: null, systems: [], usage: [], from: "none" }
|
|
569
|
-
:
|
|
578
|
+
: /**
|
|
579
|
+
* O ESCOPO DO SISTEMA SOBRE O QUAL ESTE COMANDO AGE - ver `actingSlug`.
|
|
580
|
+
*
|
|
581
|
+
* Sem o slug, um repo de dois sistemas podia responder com o escopo do
|
|
582
|
+
* outro: `scopeInLock` devolvia o primeiro lock QUE TEM escopo, e o
|
|
583
|
+
* comando age no primeiro lock que EXISTE. Num monorepo isso é a
|
|
584
|
+
* diferença entre ler a biblioteca e ler um app de 780 arquivos.
|
|
585
|
+
*/
|
|
586
|
+
await measuredScope(root, (await actingSlug(root)) ?? undefined);
|
|
570
587
|
const relScopes = (opts.scopes ?? []).length > 0
|
|
571
588
|
? opts.scopes
|
|
572
589
|
: scopePaths(measured);
|
|
@@ -582,7 +599,6 @@ export async function doctor(opts) {
|
|
|
582
599
|
*/
|
|
583
600
|
const fullRun = (opts.scopes ?? []).length === 0;
|
|
584
601
|
/** A intenção ordena o relatório, e a precedência é flag > config > default. */
|
|
585
|
-
const intent = intentOf(await readProjectConfig(root), opts.intent);
|
|
586
602
|
/**
|
|
587
603
|
* A RAIZ, MEDIDA ANTES DE COMPARAR VALOR NENHUM - ver `rootSizeOf`.
|
|
588
604
|
*
|
|
@@ -595,6 +611,16 @@ export async function doctor(opts) {
|
|
|
595
611
|
px: rootSize.ambiguous ? DEFAULT_ROOT_PX : rootSize.px,
|
|
596
612
|
from: rootSize.from,
|
|
597
613
|
});
|
|
614
|
+
/**
|
|
615
|
+
* A INTENÇÃO SABE DA REFERÊNCIA - é o que faz as duas superfícies falarem a
|
|
616
|
+
* mesma língua.
|
|
617
|
+
*
|
|
618
|
+
* A tela do grupo declara qual sistema é a fonte; este comando ordena a lista
|
|
619
|
+
* por isso, sem perguntar de novo. Um sistema que não é a referência do seu
|
|
620
|
+
* grupo está migrando para ela - e antes disto a referência existia na
|
|
621
|
+
* plataforma e o comando que ordena a lista nunca ficava sabendo.
|
|
622
|
+
*/
|
|
623
|
+
const intent = intentOf(await readProjectConfig(root), opts.intent, groupRole(installed.groupLocks, installed.table.slug));
|
|
598
624
|
const { recipes, documents } = installed;
|
|
599
625
|
/**
|
|
600
626
|
* A TABELA JÁ VEM COM O VOCABULÁRIO DELE DENTRO - ver `loadSystem` e `their-names.ts`.
|
package/dist/commands/import.js
CHANGED
|
@@ -2165,11 +2165,11 @@ function sayScopes(c) {
|
|
|
2165
2165
|
* `packages/ui` e `apps/web-dashboard` dão exatamente 0%.
|
|
2166
2166
|
*/
|
|
2167
2167
|
const conv = c.scopeConvergence;
|
|
2168
|
-
if (conv) {
|
|
2168
|
+
if (conv && conv.percent !== null) {
|
|
2169
2169
|
console.log("");
|
|
2170
|
-
console.log(body(conv.
|
|
2171
|
-
? `${paint.strong(
|
|
2172
|
-
: `${paint.strong(`${conv.
|
|
2170
|
+
console.log(body(conv.covered === 0
|
|
2171
|
+
? `${paint.strong(`None of the ${conv.declares} names`)} the other scope${scopes.length > 2 ? "s" : ""} declare${scopes.length > 2 ? "" : "s"} exists in ${paint.strong(scopes[0])} yet - not by name and not by value. These are different vocabularies, not one drifting from the other.`
|
|
2172
|
+
: `${paint.strong(`${conv.covered} of ${conv.declares} names`)} the other scope${scopes.length > 2 ? "s" : ""} declare${scopes.length > 2 ? "" : "s"} already exist in ${paint.strong(scopes[0])} (${conv.percent}%)${conv.aliased > 0 ? `, and ${conv.aliased} of those under a DIFFERENT name - one decision written twice` : ""}.`));
|
|
2173
2173
|
/**
|
|
2174
2174
|
* E A PERGUNTA, dita em voz alta em vez de assumida.
|
|
2175
2175
|
*
|
package/dist/commands/summary.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function summary(slug, opts) {
|
|
|
34
34
|
const lock = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8")
|
|
35
35
|
.then((raw) => JSON.parse(raw))
|
|
36
36
|
.catch(() => null);
|
|
37
|
-
const measured = await measuredScope(root);
|
|
37
|
+
const measured = await measuredScope(root, slug);
|
|
38
38
|
const name = lock?.name ?? slug;
|
|
39
39
|
/**
|
|
40
40
|
* `looks`, NUNCA `components`.
|
package/dist/commands/sync.js
CHANGED
|
@@ -73,7 +73,26 @@ export async function sync(opts) {
|
|
|
73
73
|
console.log(body(" synthesisui login"));
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* QUAL SISTEMA - e num repo com dois isto deixou de ser óbvio.
|
|
78
|
+
*
|
|
79
|
+
* `installedSlug` devolve o primeiro em ordem alfabética, então o segundo
|
|
80
|
+
* sistema de um repo NUNCA era sincronizado: o comando não tinha como
|
|
81
|
+
* alcançá-lo. O `align` avisava ("commands act on the first") e não havia o que
|
|
82
|
+
* fazer com o aviso.
|
|
83
|
+
*
|
|
84
|
+
* `synthesisui sync <slug>` resolve, e sem argumento nada muda para quem tem um
|
|
85
|
+
* sistema só - que é praticamente todo mundo.
|
|
86
|
+
*/
|
|
87
|
+
const installed = await installedSlugs(root);
|
|
88
|
+
const slug = opts.slug
|
|
89
|
+
? installed.find((x) => x === opts.slug)
|
|
90
|
+
: installed[0];
|
|
91
|
+
if (opts.slug && !slug) {
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(body(`No system called ${paint.strong(opts.slug)} is installed here.${installed.length > 0 ? ` This repo has: ${installed.map((x) => paint.strong(x)).join(", ")}.` : ""}`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
77
96
|
if (!slug) {
|
|
78
97
|
console.log(section("Sync"));
|
|
79
98
|
console.log(body("No installed system here, so there is nowhere to sync to."));
|
|
@@ -247,7 +266,14 @@ export async function remeasure(args) {
|
|
|
247
266
|
*
|
|
248
267
|
* O servidor continua valendo como segunda fonte: um clone fresco tem o `.lock` e não tem censo.
|
|
249
268
|
*/
|
|
250
|
-
|
|
269
|
+
/**
|
|
270
|
+
* O ESCOPO DESTE SISTEMA, e não o do vizinho - ver `measuredScope`.
|
|
271
|
+
*
|
|
272
|
+
* Num repo com dois sistemas instalados, o escopo vinha do primeiro lock que
|
|
273
|
+
* tivesse um, enquanto `installedSlug` escolhia o primeiro que tivesse slug.
|
|
274
|
+
* Quando não eram o mesmo, este comando media um sistema e mandava para o outro.
|
|
275
|
+
*/
|
|
276
|
+
const local = await measuredScope(root, slug);
|
|
251
277
|
/**
|
|
252
278
|
* TODOS os escopos do sistema, não só o primeiro.
|
|
253
279
|
*
|
|
@@ -506,35 +532,36 @@ async function askToOverwrite() {
|
|
|
506
532
|
}
|
|
507
533
|
}
|
|
508
534
|
/**
|
|
509
|
-
*
|
|
535
|
+
* TODOS os sistemas instalados, na ordem em que o disco os devolve.
|
|
536
|
+
*
|
|
537
|
+
* Era `installedSlug`, singular, e devolvia o primeiro - o que fazia o segundo
|
|
538
|
+
* sistema de um repo ser inalcançável por este comando. O `align` avisava
|
|
539
|
+
* ("commands act on the first") e não havia o que fazer com o aviso.
|
|
510
540
|
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* não tem linha no dashboard para onde sincronizar - discordaria entre duas cópias na primeira vez que
|
|
514
|
-
* alguém a mudasse.
|
|
541
|
+
* `adopted` fica fora: um sistema adotado é o vocabulário do próprio projeto, sem
|
|
542
|
+
* versão publicada, e não há para onde sincronizá-lo.
|
|
515
543
|
*/
|
|
516
|
-
export async function
|
|
544
|
+
export async function installedSlugs(root) {
|
|
517
545
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
546
|
+
const out = [];
|
|
547
|
+
const slugs = (await readdir(dsDir, { withFileTypes: true }).catch(() => []))
|
|
548
|
+
.filter((d) => d.isDirectory())
|
|
549
|
+
.map((d) => d.name)
|
|
550
|
+
.sort();
|
|
551
|
+
for (const name of slugs) {
|
|
552
|
+
const raw = await readFile(join(dsDir, name, ".lock"), "utf8").catch(() => null);
|
|
553
|
+
if (!raw)
|
|
554
|
+
continue;
|
|
555
|
+
try {
|
|
556
|
+
const lock = JSON.parse(raw);
|
|
557
|
+
if (lock.slug && !lock.adopted)
|
|
558
|
+
out.push(lock.slug);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// lock ilegível: aquele sistema não entra, e o resto segue
|
|
532
562
|
}
|
|
533
563
|
}
|
|
534
|
-
|
|
535
|
-
// no _synthesisui at all
|
|
536
|
-
}
|
|
537
|
-
return null;
|
|
564
|
+
return out;
|
|
538
565
|
}
|
|
539
566
|
/**
|
|
540
567
|
* DE ONDE ESTE SISTEMA FOI MEDIDO - perguntado uma vez, com os candidatos do próprio repo.
|
|
@@ -577,3 +604,13 @@ async function askForScope(root) {
|
|
|
577
604
|
rl.close();
|
|
578
605
|
}
|
|
579
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* O PRIMEIRO sistema instalado - o que o MCP usa quando a pergunta é "deste repo".
|
|
609
|
+
*
|
|
610
|
+
* Mantido porque as ferramentas do MCP falam de UM sistema por natureza: quem
|
|
611
|
+
* pergunta "qual é a doutrina daqui" quer a do sistema que governa este repo, e a
|
|
612
|
+
* escolha entre dois é uma decisão de quem opera, não do servidor.
|
|
613
|
+
*/
|
|
614
|
+
export async function installedSlug(root) {
|
|
615
|
+
return (await installedSlugs(root))[0] ?? null;
|
|
616
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -156,7 +156,19 @@ export async function readProjectConfig(root) {
|
|
|
156
156
|
* porque é o único dos dois cuja lista tem conserto automático (`--fix`). Migrar é uma fase, e uma
|
|
157
157
|
* fase se declara.
|
|
158
158
|
*/
|
|
159
|
-
export function intentOf(config, flag
|
|
159
|
+
export function intentOf(config, flag,
|
|
160
|
+
/**
|
|
161
|
+
* O PAPEL DESTE SISTEMA NO GRUPO, lido dos locks por `groupRole`.
|
|
162
|
+
*
|
|
163
|
+
* Recebe o papel inteiro e não um slug solto, porque as duas metades decidem
|
|
164
|
+
* coisas opostas e um argumento que se pode esquecer é um argumento que alguém
|
|
165
|
+
* vai esquecer: `follows` sozinho derivaria `adopt` para a própria referência.
|
|
166
|
+
*
|
|
167
|
+
* Derivar em vez de perguntar de novo é o que faz as duas superfícies falarem a
|
|
168
|
+
* mesma língua - a tela do grupo declara uma vez, e o doctor de todo repo do
|
|
169
|
+
* grupo passa a ordenar por aquilo.
|
|
170
|
+
*/
|
|
171
|
+
group) {
|
|
160
172
|
if (flag === "migrate" || flag === "adopt")
|
|
161
173
|
return { intent: flag, from: "flag" };
|
|
162
174
|
if (config.intent)
|
|
@@ -164,7 +176,14 @@ export function intentOf(config, flag) {
|
|
|
164
176
|
intent: config.intent,
|
|
165
177
|
from: "config",
|
|
166
178
|
...(config.intentAt ? { at: config.intentAt } : {}),
|
|
179
|
+
...(group?.follows ? { towards: group.follows } : {}),
|
|
167
180
|
};
|
|
181
|
+
/** Sou a fonte: o design dos outros vem para cá, e absorver é o que eu faço. */
|
|
182
|
+
if (group?.isReference)
|
|
183
|
+
return { intent: "migrate", from: "reference" };
|
|
184
|
+
/** Sigo a fonte: o meu vocabulário converge, então eu adoto os nomes dela. */
|
|
185
|
+
if (group?.follows)
|
|
186
|
+
return { intent: "adopt", from: "reference", towards: group.follows };
|
|
168
187
|
return { intent: "adopt", from: "default" };
|
|
169
188
|
}
|
|
170
189
|
/**
|
|
@@ -183,7 +202,18 @@ export function describeIntent(source) {
|
|
|
183
202
|
? "this run only"
|
|
184
203
|
: source.from === "config"
|
|
185
204
|
? `set in _synthesisui/config.json${source.at ? ` on ${source.at.slice(0, 10)}` : ""}`
|
|
186
|
-
:
|
|
205
|
+
: source.from === "reference"
|
|
206
|
+
? /**
|
|
207
|
+
* A frase diz que ninguém digitou isto, e de onde saiu: uma intenção
|
|
208
|
+
* derivada que se apresenta como escolhida é a etiqueta que envelhece
|
|
209
|
+
* sem ninguém notar. E ela nomeia o outro sistema nos dois sentidos,
|
|
210
|
+
* porque "sou a referência" e "sigo a referência" ordenam a lista de
|
|
211
|
+
* jeitos opostos.
|
|
212
|
+
*/
|
|
213
|
+
source.towards
|
|
214
|
+
? `derived: this group's reference is \`${source.towards}\`, and your vocabulary is converging on it`
|
|
215
|
+
: "derived: this is the group's reference system, so the design moves in"
|
|
216
|
+
: "nobody chose, so this is the default";
|
|
187
217
|
const flip = source.intent === "migrate"
|
|
188
218
|
? "`doctor --adopt` sorts the other way"
|
|
189
219
|
: "`doctor --migrate` sorts the other way";
|
|
@@ -115,6 +115,38 @@ export function architectureRule(a) {
|
|
|
115
115
|
evidence: `${a.evidence.join("/")} under ${a.root}, holding ${a.files} component file${a.files === 1 ? "" : "s"}`,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* A ESTRUTURA, ESCRITA - as pastas que a forma tem, na ordem em que ela as usa.
|
|
120
|
+
*
|
|
121
|
+
* Uma opção que diz só o nome da forma e um número não é uma escolha de
|
|
122
|
+
* organização: "escada atômica, 6 arquivos" não diz a ninguém o que ele está
|
|
123
|
+
* escolhendo. O dono leu duas opções assim e a pergunta ficou ilegível (19/08).
|
|
124
|
+
* Então cada opção mostra a árvore, e ela é medida - `evidence` são os diretórios
|
|
125
|
+
* que fizeram a leitura.
|
|
126
|
+
*/
|
|
127
|
+
export function describeShape(a) {
|
|
128
|
+
return `${a.root}/${a.evidence.length > 0 ? `{${a.evidence.join(", ")}}` : ""}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* QUAIS FORMAS SÃO OPÇÃO, E QUAIS SÃO NOTA DE PÉ - decidido pela escala.
|
|
132
|
+
*
|
|
133
|
+
* Duas formas só são uma escolha quando as duas governam código de verdade. No
|
|
134
|
+
* monorepo do dono a segunda tinha 6 arquivos contra 780 da primeira - duas ordens
|
|
135
|
+
* de magnitude -, e oferecê-las lado a lado transformou um fato ("existe um canto
|
|
136
|
+
* organizado assim") numa pergunta que sugere que as duas disputam o repositório.
|
|
137
|
+
*
|
|
138
|
+
* O corte é um DÉCIMO da dominante: abaixo dele a forma continua verdadeira e é
|
|
139
|
+
* dita, mas não pede decisão. Um décimo e não um número redondo qualquer porque é
|
|
140
|
+
* a fronteira entre "convive" e "um canto".
|
|
141
|
+
*/
|
|
142
|
+
export const MINOR_SHARE = 0.1;
|
|
143
|
+
export function splitByWeight(found) {
|
|
144
|
+
if (found.length === 0)
|
|
145
|
+
return { options: [], notes: [] };
|
|
146
|
+
const top = found[0].files;
|
|
147
|
+
const options = found.filter((a) => a.files >= top * MINOR_SHARE);
|
|
148
|
+
return { options, notes: found.filter((a) => !options.includes(a)) };
|
|
149
|
+
}
|
|
118
150
|
/**
|
|
119
151
|
* One line naming what was found, and the choice it leaves open.
|
|
120
152
|
*
|
|
@@ -125,10 +157,17 @@ export function describeChoice(found) {
|
|
|
125
157
|
if (found.length === 0)
|
|
126
158
|
return null;
|
|
127
159
|
if (found.length === 1) {
|
|
128
|
-
return `One shape, read off your directories: ${found[0].kind}
|
|
160
|
+
return `One shape, read off your directories: ${found[0].kind} in \`${describeShape(found[0])}\`.`;
|
|
161
|
+
}
|
|
162
|
+
const { options, notes } = splitByWeight(found);
|
|
163
|
+
const say = (a) => `${a.kind} in \`${describeShape(a)}\` (${a.files} files)`;
|
|
164
|
+
/**
|
|
165
|
+
* Uma dominante e o resto em cantos: isto não é uma pergunta sobre o retrato, é
|
|
166
|
+
* uma pergunta sobre a REGRA daqui em diante - e a skill é quem a faz.
|
|
167
|
+
*/
|
|
168
|
+
if (options.length === 1) {
|
|
169
|
+
return `One shape governs this code: ${say(options[0])}. Also here, and staying true without deciding anything: ${notes.map(say).join(", ")}.`;
|
|
129
170
|
}
|
|
130
|
-
const list =
|
|
131
|
-
|
|
132
|
-
.join(", ");
|
|
133
|
-
return `${found.length} shapes, and none of them is a mistake: ${list}. A library organised atomically inside a monorepo whose apps are organised by feature is two true answers. Pick which one has PRIORITY - it becomes the rule every prompt reads, and the others stay true without deciding.`;
|
|
171
|
+
const list = options.map(say).join(", ");
|
|
172
|
+
return `${options.length} shapes govern real code, and none of them is a mistake: ${list}.${notes.length > 0 ? ` Smaller corners, reported and not asked about: ${notes.map(say).join(", ")}.` : ""} Pick which one has PRIORITY - it becomes the rule every prompt reads, and the others stay true without deciding.`;
|
|
134
173
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lê o papel dos locks instalados. Sem referência declarada no grupo, os dois
|
|
3
|
+
* campos ficam vazios: independentes é uma resposta, não a ausência de uma.
|
|
4
|
+
*/
|
|
5
|
+
export function groupRole(locks,
|
|
6
|
+
/**
|
|
7
|
+
* `null` quando não há sistema instalado - e aí não há papel, nem derivação. É
|
|
8
|
+
* o estado de um projeto que só rodou `import`: sem lock, sem grupo.
|
|
9
|
+
*/
|
|
10
|
+
self) {
|
|
11
|
+
if (!self)
|
|
12
|
+
return { isReference: false };
|
|
13
|
+
const mine = locks.find((l) => l.slug === self);
|
|
14
|
+
const follows = mine?.reference?.slug;
|
|
15
|
+
/**
|
|
16
|
+
* `follows` e `isReference` não coexistem: quem segue não é seguido. Se um repo
|
|
17
|
+
* chegar nesse estado, o lock que aponta para mim está velho - e é o `align` que
|
|
18
|
+
* fala de lock velho, não esta função, que responde pelo MEU lock.
|
|
19
|
+
*/
|
|
20
|
+
const isReference = !follows &&
|
|
21
|
+
locks.some((l) => l.reference?.slug === self && l.slug !== self);
|
|
22
|
+
return {
|
|
23
|
+
...(follows ? { follows } : {}),
|
|
24
|
+
isReference,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* QUE REFERÊNCIA O `.lock` PASSA A GUARDAR depois de uma busca no registry.
|
|
29
|
+
*
|
|
30
|
+
* Uma função em vez de um ternário dentro do `add` porque a decisão tem três
|
|
31
|
+
* entradas e um caso que ninguém adivinha (apagar), e um ternário de três níveis
|
|
32
|
+
* no meio de um objeto de 12 campos é onde o caso apagar se perdeu por semanas.
|
|
33
|
+
*/
|
|
34
|
+
export function lockReference(payload, prev) {
|
|
35
|
+
/** A resposta completa manda, e ela também apaga. */
|
|
36
|
+
if (payload.group)
|
|
37
|
+
return payload.group.reference;
|
|
38
|
+
/** Um servidor que só conhece a metade antiga: ele afirma, nunca nega. */
|
|
39
|
+
if (payload.groupReference)
|
|
40
|
+
return payload.groupReference;
|
|
41
|
+
return prev?.reference;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A REFERÊNCIA DO GRUPO, lida dos locks - a mesma leitura para todo mundo.
|
|
45
|
+
*
|
|
46
|
+
* Três lugares faziam esta linha à mão (`align`, o `CLAUDE.md` e o doctor), e
|
|
47
|
+
* "todos falam a mesma língua" começa por ler o fato no mesmo lugar. O lock que
|
|
48
|
+
* aponta para si mesmo é ignorado: é lock velho, e ele nomearia como referência do
|
|
49
|
+
* grupo alguém que já não é.
|
|
50
|
+
*/
|
|
51
|
+
export function declaredReference(locks) {
|
|
52
|
+
return locks.find((l) => l.reference && l.reference.slug !== l.slug)
|
|
53
|
+
?.reference;
|
|
54
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ Usage - governance (deterministic, FREE):
|
|
|
65
65
|
something no reader would change
|
|
66
66
|
synthesisui request [component|token] the queue of what your agent needed and the system
|
|
67
67
|
refused to invent (--done <id> closes one)
|
|
68
|
-
synthesisui sync re-measure this repo with the current reader and send it,
|
|
68
|
+
synthesisui sync [<slug>] re-measure this repo with the current reader and send it,
|
|
69
69
|
reusing the reading you already authored, plus the local
|
|
70
70
|
record - checks, fixes, open requests (--record-only skips
|
|
71
71
|
the measurement, --yes skips the overwrite question)
|
|
@@ -528,6 +528,14 @@ async function main() {
|
|
|
528
528
|
dir,
|
|
529
529
|
registry,
|
|
530
530
|
cli: CLI_VERSION,
|
|
531
|
+
/**
|
|
532
|
+
* `sync <slug>`: qual sistema, num repo que tem mais de um.
|
|
533
|
+
*
|
|
534
|
+
* Sem ele o comando agia sempre no primeiro em ordem alfabética, e o
|
|
535
|
+
* segundo sistema de um repositório era inalcançável - o `align` avisava
|
|
536
|
+
* e não havia o que fazer com o aviso.
|
|
537
|
+
*/
|
|
538
|
+
...(args[0] ? { slug: args[0] } : {}),
|
|
531
539
|
recordOnly: boolFlag(flags, "record-only"),
|
|
532
540
|
yes: boolFlag(flags, "yes"),
|
|
533
541
|
/** `--full`: o relatório inteiro do leitor. Por padrão a re-medição conta o que MUDOU. */
|
package/dist/install-marks.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
*
|
|
101
101
|
* Quem instalou antes desta versão tem o stub que estanca: o `upgrade`/`connect` é o que alcança.
|
|
102
102
|
*/
|
|
103
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
103
|
+
export const MATERIALISER_SINCE = "0.16.259";
|
|
104
104
|
/**
|
|
105
105
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
106
106
|
*
|
package/dist/measured-scope.js
CHANGED
|
@@ -16,12 +16,26 @@ const EMPTY = {
|
|
|
16
16
|
*
|
|
17
17
|
* Um lock sem `scope` é o caso de todo sistema instalado antes disto: cai no censo, como antes.
|
|
18
18
|
*/
|
|
19
|
-
async function scopeInLock(root
|
|
19
|
+
async function scopeInLock(root,
|
|
20
|
+
/**
|
|
21
|
+
* DE QUAL SISTEMA - e sem isto o escopo era o do PRIMEIRO diretório que tinha um.
|
|
22
|
+
*
|
|
23
|
+
* Com dois sistemas instalados (um fork da galeria, sem escopo, e um importado,
|
|
24
|
+
* com escopo) o laço pulava o primeiro e devolvia o escopo do segundo, enquanto
|
|
25
|
+
* `installedSlug` devolvia o PRIMEIRO. O `sync` então media o escopo de um
|
|
26
|
+
* sistema e mandava para o outro - provado num repo de teste em 19/08.
|
|
27
|
+
*
|
|
28
|
+
* Ausente mantém o comportamento antigo, que é o certo para um sistema só.
|
|
29
|
+
*/
|
|
30
|
+
slug) {
|
|
20
31
|
const dsDir = join(root, "_synthesisui", "ds");
|
|
21
32
|
const names = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
|
|
22
33
|
for (const entry of names) {
|
|
23
34
|
if (!entry.isDirectory())
|
|
24
35
|
continue;
|
|
36
|
+
/** Pedido um sistema, é o dele que responde - nunca o do vizinho. */
|
|
37
|
+
if (slug && entry.name !== slug)
|
|
38
|
+
continue;
|
|
25
39
|
const raw = await readFile(join(dsDir, entry.name, ".lock"), "utf8").catch(() => null);
|
|
26
40
|
if (!raw)
|
|
27
41
|
continue;
|
|
@@ -75,6 +89,37 @@ async function scopeInLock(root) {
|
|
|
75
89
|
async function present(root, rel) {
|
|
76
90
|
return stat(resolve(root, rel)).then((s) => s.isDirectory(), () => false);
|
|
77
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* O SISTEMA SOBRE O QUAL OS COMANDOS AGEM - o primeiro em ordem alfabética.
|
|
94
|
+
*
|
|
95
|
+
* Não é uma escolha nova: é a que `loadSystem` já faz (`slugs.sort()`, e o
|
|
96
|
+
* primeiro lock legível ganha), e o `align` já avisa sobre ela. Ela existe aqui
|
|
97
|
+
* para que quem precisa do ESCOPO possa dizer DE QUEM - sem isto, `measuredScope`
|
|
98
|
+
* responde com o primeiro lock que tem escopo, que num repo de dois sistemas pode
|
|
99
|
+
* ser o do outro. Provado em 19/08: um fork sem escopo mais um importado com
|
|
100
|
+
* escopo, e a medição de um ia para o outro.
|
|
101
|
+
*/
|
|
102
|
+
export async function actingSlug(root) {
|
|
103
|
+
const dsDir = join(root, "_synthesisui", "ds");
|
|
104
|
+
const dirs = (await readdir(dsDir, { withFileTypes: true }).catch(() => []))
|
|
105
|
+
.filter((d) => d.isDirectory())
|
|
106
|
+
.map((d) => d.name)
|
|
107
|
+
.sort();
|
|
108
|
+
for (const name of dirs) {
|
|
109
|
+
const raw = await readFile(join(dsDir, name, ".lock"), "utf8").catch(() => null);
|
|
110
|
+
if (!raw)
|
|
111
|
+
continue;
|
|
112
|
+
try {
|
|
113
|
+
const lock = JSON.parse(raw);
|
|
114
|
+
if (lock.slug)
|
|
115
|
+
return lock.slug;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// lock ilegível: o próximo responde
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
78
123
|
/**
|
|
79
124
|
* Lê `_synthesisui/census.json` e devolve o que ele diz sobre ONDE.
|
|
80
125
|
*
|
|
@@ -82,8 +127,17 @@ async function present(root, rel) {
|
|
|
82
127
|
* desde então não pode fazer o doctor estourar - ela simplesmente sai da lista, e a lista vazia cai no
|
|
83
128
|
* comportamento antigo (a raiz).
|
|
84
129
|
*/
|
|
85
|
-
export async function measuredScope(root
|
|
86
|
-
|
|
130
|
+
export async function measuredScope(root,
|
|
131
|
+
/**
|
|
132
|
+
* O SISTEMA CUJO ESCOPO SE QUER - e passar isto deixou de ser opcional na
|
|
133
|
+
* prática quando um repo pode ter dois.
|
|
134
|
+
*
|
|
135
|
+
* Quem sincroniza um sistema tem que medir O ESCOPO DELE. Sem o slug, o
|
|
136
|
+
* primeiro lock com escopo respondia por todos, e num repo com um fork sem
|
|
137
|
+
* escopo mais um importado com escopo o `sync` media um e mandava para o outro.
|
|
138
|
+
*/
|
|
139
|
+
slug) {
|
|
140
|
+
const fromLock = await scopeInLock(root, slug);
|
|
87
141
|
if (fromLock)
|
|
88
142
|
return fromLock;
|
|
89
143
|
return censusScope(root);
|
package/dist/merge-census.js
CHANGED
|
@@ -237,26 +237,46 @@ export function mergeCensus(list) {
|
|
|
237
237
|
* não se parecem.
|
|
238
238
|
*/
|
|
239
239
|
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
/**
|
|
241
|
+
* O NÚMERO É DIRECIONAL: quanto do vocabulário dos escopos SEGUINTES o
|
|
242
|
+
* PRIMEIRO já nomeia. A primeira versão media `agree/union`, simétrico, e o
|
|
243
|
+
* dono estranhou na hora - com razão: um percentual simétrico lê como NOTA, e
|
|
244
|
+
* se os escopos são vocabulários diferentes de propósito não existe nota a
|
|
245
|
+
* dar. A pergunta útil é a da migração, e ela tem um sentido.
|
|
246
|
+
*
|
|
247
|
+
* E conta POR VALOR além do nome: `--dashboard-blue-500` e `--color-blue-500`
|
|
248
|
+
* são a mesma decisão com dois nomes - invisível numa comparação por nome, e
|
|
249
|
+
* é o item mais acionável que sai daqui (dois casos no repo do dono).
|
|
250
|
+
*/
|
|
251
|
+
const norm = (v) => v.trim().toLowerCase().replace(/\s+/g, " ");
|
|
252
|
+
const first = asRecord(list[0].declared);
|
|
253
|
+
const byValue = new Map();
|
|
254
|
+
for (const [name, value] of Object.entries(first))
|
|
255
|
+
if (!byValue.has(norm(value)))
|
|
256
|
+
byValue.set(norm(value), name);
|
|
257
|
+
let declares = 0;
|
|
258
|
+
let covered = 0;
|
|
259
|
+
let aliased = 0;
|
|
260
|
+
for (const c of list.slice(1)) {
|
|
244
261
|
for (const [name, value] of Object.entries(asRecord(c.declared))) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
262
|
+
declares += 1;
|
|
263
|
+
const sameName = first[name];
|
|
264
|
+
if (sameName !== undefined) {
|
|
265
|
+
if (sameName === value)
|
|
266
|
+
covered += 1;
|
|
248
267
|
continue;
|
|
249
268
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
if (byValue.has(norm(value))) {
|
|
270
|
+
covered += 1;
|
|
271
|
+
aliased += 1;
|
|
272
|
+
}
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
out.scopeConvergence = {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
percent:
|
|
276
|
+
declares,
|
|
277
|
+
covered,
|
|
278
|
+
aliased,
|
|
279
|
+
percent: declares === 0 ? null : Math.round((covered / declares) * 100),
|
|
260
280
|
};
|
|
261
281
|
}
|
|
262
282
|
return out;
|
package/package.json
CHANGED