synthesisui 0.16.256 → 0.16.259
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 +25 -0
- package/dist/commands/summary.js +1 -1
- package/dist/commands/sync.js +63 -26
- package/dist/config.js +32 -2
- 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 +52 -0
- 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
|
@@ -2155,6 +2155,31 @@ function sayScopes(c) {
|
|
|
2155
2155
|
console.log("");
|
|
2156
2156
|
console.log(section("Two folders, one system"));
|
|
2157
2157
|
console.log(body(`Measured ${scopes.length} scopes and fused them: ${scopes.map((x, i) => (i === 0 ? paint.strong(x) : x)).join(" · ")}. The first one is the authority - it wins a tie, and nothing from the others is dropped without a line.`));
|
|
2158
|
+
/**
|
|
2159
|
+
* A CONVERGÊNCIA VEM ANTES DOS CONFLITOS, porque ela decide se a lista de
|
|
2160
|
+
* conflitos é o assunto.
|
|
2161
|
+
*
|
|
2162
|
+
* Com 60% compartilhado, três divergências são o trabalho. Com 0%, elas são um
|
|
2163
|
+
* detalhe de dois vocabulários que não se encontram - e a conversa que precisa
|
|
2164
|
+
* acontecer é outra: isto é um sistema ou são dois? Medido no monorepo do dono,
|
|
2165
|
+
* `packages/ui` e `apps/web-dashboard` dão exatamente 0%.
|
|
2166
|
+
*/
|
|
2167
|
+
const conv = c.scopeConvergence;
|
|
2168
|
+
if (conv && conv.percent !== null) {
|
|
2169
|
+
console.log("");
|
|
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
|
+
/**
|
|
2174
|
+
* E A PERGUNTA, dita em voz alta em vez de assumida.
|
|
2175
|
+
*
|
|
2176
|
+
* A fusão presume que os escopos são um sistema só. Abaixo de um terço de
|
|
2177
|
+
* convergência essa premissa deixa de se sustentar sozinha, e quem decide não
|
|
2178
|
+
* é a esteira - é quem conhece os dois produtos.
|
|
2179
|
+
*/
|
|
2180
|
+
if (conv.percent < 34)
|
|
2181
|
+
console.log(body(paint.faint("Worth answering before this becomes one system: are these two ONE vocabulary, or two that live in the same repo? Two is a legitimate answer - import each scope on its own, and name which of them is the reference on the group's screen.")));
|
|
2182
|
+
}
|
|
2158
2183
|
if (conflicts.length === 0)
|
|
2159
2184
|
return;
|
|
2160
2185
|
console.log("");
|
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";
|
|
@@ -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
|
@@ -227,5 +227,57 @@ export function mergeCensus(list) {
|
|
|
227
227
|
out.collisions = collisions;
|
|
228
228
|
if (conflicts.length > 0)
|
|
229
229
|
out.scopeConflicts = conflicts;
|
|
230
|
+
/**
|
|
231
|
+
* A CONVERGÊNCIA ENTRE OS ESCOPOS, calculada aqui porque é aqui que os
|
|
232
|
+
* vocabulários separados ainda existem - depois da fusão eles são um só.
|
|
233
|
+
*
|
|
234
|
+
* `shared` conta os nomes que mais de um escopo declara; `agree` são os que
|
|
235
|
+
* resolvem igual. Zero compartilhado não é "quase lá": é a informação de que
|
|
236
|
+
* não há design em comum, e que fundir produziria a média de dois sistemas que
|
|
237
|
+
* não se parecem.
|
|
238
|
+
*/
|
|
239
|
+
{
|
|
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)) {
|
|
261
|
+
for (const [name, value] of Object.entries(asRecord(c.declared))) {
|
|
262
|
+
declares += 1;
|
|
263
|
+
const sameName = first[name];
|
|
264
|
+
if (sameName !== undefined) {
|
|
265
|
+
if (sameName === value)
|
|
266
|
+
covered += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (byValue.has(norm(value))) {
|
|
270
|
+
covered += 1;
|
|
271
|
+
aliased += 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
out.scopeConvergence = {
|
|
276
|
+
declares,
|
|
277
|
+
covered,
|
|
278
|
+
aliased,
|
|
279
|
+
percent: declares === 0 ? null : Math.round((covered / declares) * 100),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
230
282
|
return out;
|
|
231
283
|
}
|
package/package.json
CHANGED