synthesisui 0.16.253 → 0.16.255
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add.js +15 -1
- package/dist/commands/doctor.js +16 -2
- package/dist/commands/import.js +208 -19
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/sync.js +51 -15
- package/dist/index.js +12 -2
- package/dist/install-marks.js +1 -1
- package/dist/measured-scope.js +67 -16
- package/dist/merge-census.js +231 -0
- package/dist/name-claim.js +2 -2
- package/dist/workspace-place.js +22 -6
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -154,7 +154,20 @@ export async function add(slug, opts) {
|
|
|
154
154
|
* censo (um `add` puro, sem import antes), o que o lock já dizia é preservado.
|
|
155
155
|
*/
|
|
156
156
|
const measured = await censusScope(projectRoot);
|
|
157
|
-
|
|
157
|
+
/**
|
|
158
|
+
* OS DOIS CAMPOS: `scopes` com tudo e `scope` com o primeiro.
|
|
159
|
+
*
|
|
160
|
+
* O lock é o arquivo que um time COMMITA, e é o que faz um clone fresco saber de
|
|
161
|
+
* onde o sistema foi medido sem ida à rede. Guardar só o primeiro escopo ali
|
|
162
|
+
* fazia o `sync` de um clone novo remedir metade de um sistema de dois escopos.
|
|
163
|
+
* Um CLI anterior lê `scope` e segue funcionando.
|
|
164
|
+
*/
|
|
165
|
+
const systems = measured.systems.length > 0
|
|
166
|
+
? measured.systems
|
|
167
|
+
: measured.system
|
|
168
|
+
? [measured.system]
|
|
169
|
+
: (prev?.scopes ?? (prev?.scope ? [prev.scope] : []));
|
|
170
|
+
const scope = systems[0] ?? null;
|
|
158
171
|
const usage = measured.usage.length > 0 ? measured.usage : (prev?.usage ?? []);
|
|
159
172
|
/**
|
|
160
173
|
* `fetchedAt` SÓ ANDA QUANDO ALGO ATERROU - e a alternativa sujava o git de um time inteiro.
|
|
@@ -177,6 +190,7 @@ export async function add(slug, opts) {
|
|
|
177
190
|
...(payload.compiler != null ? { compiler: payload.compiler } : {}),
|
|
178
191
|
...(payload.rulesStamp ? { rules: payload.rulesStamp } : {}),
|
|
179
192
|
...(scope ? { scope } : {}),
|
|
193
|
+
...(systems.length > 0 ? { scopes: systems } : {}),
|
|
180
194
|
...(usage.length > 0 ? { usage } : {}),
|
|
181
195
|
};
|
|
182
196
|
const landed = (() => {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -42,7 +42,21 @@ import { resolveDeps } from "../stack.js";
|
|
|
42
42
|
/** Files that compose components to SHOW them, not to ship them. */
|
|
43
43
|
const IS_ASIDE = /(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/;
|
|
44
44
|
const EXTS = [".tsx", ".ts", ".jsx", ".js", ".css", ".scss", ".vue", ".svelte"];
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* EXPORTADO porque quem mede o repositório fora deste arquivo tem que pular as
|
|
47
|
+
* MESMAS pastas.
|
|
48
|
+
*
|
|
49
|
+
* `workspace-place.ts` contava as declarações de cada pacote com a sua própria
|
|
50
|
+
* regra (dotdir + node_modules) e somava `packages/ui/dist/index.css`: 496
|
|
51
|
+
* declarações de bundle contra 97 de fonte no repo do dono, medido em 19/08. O
|
|
52
|
+
* número que decidia qual pasta recomendar era cinco sextos build output - e o
|
|
53
|
+
* censo, que usa este SKIP, nunca leu nada daquilo.
|
|
54
|
+
*
|
|
55
|
+
* Duas listas de pastas a ignorar é a mesma classe de divergência que
|
|
56
|
+
* `loadSystem` existe para matar: nenhuma delas está errada isolada, e juntas
|
|
57
|
+
* dizem números diferentes sobre o mesmo repositório.
|
|
58
|
+
*/
|
|
59
|
+
export const SKIP = new Set([
|
|
46
60
|
"node_modules",
|
|
47
61
|
".next",
|
|
48
62
|
".git",
|
|
@@ -551,7 +565,7 @@ export async function doctor(opts) {
|
|
|
551
565
|
* lista de prioridades tirada de um app consumidor num repo de 2787 arquivos (dono, 06/08).
|
|
552
566
|
*/
|
|
553
567
|
const measured = (opts.scopes ?? []).length > 0
|
|
554
|
-
? { system: null, usage: [], from: "none" }
|
|
568
|
+
? { system: null, systems: [], usage: [], from: "none" }
|
|
555
569
|
: await measuredScope(root);
|
|
556
570
|
const relScopes = (opts.scopes ?? []).length > 0
|
|
557
571
|
? opts.scopes
|
package/dist/commands/import.js
CHANGED
|
@@ -35,6 +35,7 @@ import { definitionSpan, parseClass, readInlineStyle, rootClasses, rootTag, tran
|
|
|
35
35
|
import { transcribeVariants } from "../doctor/variant-read.js";
|
|
36
36
|
import { frontierKind, packageRoot } from "../frontier-kind.js";
|
|
37
37
|
import { withLibraryStructure } from "../library-structure.js";
|
|
38
|
+
import { mergeCensus } from "../merge-census.js";
|
|
38
39
|
import { claimName } from "../name-claim.js";
|
|
39
40
|
import { body, paint, section } from "../output.js";
|
|
40
41
|
import { phase, startProgress } from "../progress.js";
|
|
@@ -1147,13 +1148,39 @@ export async function takeCensus(root, opts) {
|
|
|
1147
1148
|
const edges = new Map();
|
|
1148
1149
|
/** Por raiz: `@/` é uma pasta diferente em cada app - ver `reachabilityOf`. */
|
|
1149
1150
|
const graphRoots = [];
|
|
1151
|
+
/**
|
|
1152
|
+
* A DISTÂNCIA, contada onde os arquivos já estão abertos - ver `Census.adoption`.
|
|
1153
|
+
*
|
|
1154
|
+
* `scopePkg` é o nome do pacote do escopo, que é como um consumidor o importa.
|
|
1155
|
+
* Um arquivo conta quando ele NOMEIA esse especificador, e a régua é o texto do
|
|
1156
|
+
* import: um alias de tsconfig que aponta para a mesma pasta contaria como
|
|
1157
|
+
* "não usa", e essa é a diferença entre um número menor e um número inventado.
|
|
1158
|
+
*/
|
|
1159
|
+
const adoption = new Map();
|
|
1150
1160
|
for (const u of opts?.usage ?? []) {
|
|
1151
1161
|
const uInternal = await internalSpecifiers(u.path);
|
|
1152
1162
|
graphRoots.push({ path: u.path, aliases: await aliasesOf(u.path) });
|
|
1163
|
+
const tally = adoption.get(u.label) ?? { files: 0, using: 0 };
|
|
1164
|
+
adoption.set(u.label, tally);
|
|
1153
1165
|
for await (const file of walk(u.path)) {
|
|
1154
1166
|
const src = await readFile(file, "utf8").catch(() => "");
|
|
1155
1167
|
if (!src)
|
|
1156
1168
|
continue;
|
|
1169
|
+
/**
|
|
1170
|
+
* O DENOMINADOR SÓ CONTA QUEM PINTA.
|
|
1171
|
+
*
|
|
1172
|
+
* A primeira versão contava todo `.ts` - hooks, tipos, clientes http - e o
|
|
1173
|
+
* app do dono saiu como 105 de 1578 (7%), quando 105 de 356 arquivos de
|
|
1174
|
+
* marcação é 29%. Um arquivo de tipos não deixa de usar o design system:
|
|
1175
|
+
* ele nunca teve como. Diluir o denominador com ele faz a migração parecer
|
|
1176
|
+
* mais atrasada do que é, e um número pessimista é tão falso quanto um
|
|
1177
|
+
* otimista.
|
|
1178
|
+
*/
|
|
1179
|
+
if (/\.(tsx|jsx|vue|svelte)$/i.test(file)) {
|
|
1180
|
+
tally.files += 1;
|
|
1181
|
+
if (scopePkg && src.includes(scopePkg))
|
|
1182
|
+
tally.using += 1;
|
|
1183
|
+
}
|
|
1157
1184
|
edges.set(file, edgesIn(src));
|
|
1158
1185
|
const rel = join(u.label, relative(u.path, file));
|
|
1159
1186
|
if (/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
|
|
@@ -1914,6 +1941,18 @@ export async function takeCensus(root, opts) {
|
|
|
1914
1941
|
* só aparece quando há algo a dizer não muda nada para quem já lê o censo.
|
|
1915
1942
|
*/
|
|
1916
1943
|
...(claims.collisions.length > 0 ? { collisions: claims.collisions } : {}),
|
|
1944
|
+
...(scopePkg && adoption.size > 0
|
|
1945
|
+
? {
|
|
1946
|
+
adoption: {
|
|
1947
|
+
specifier: scopePkg,
|
|
1948
|
+
consumers: [...adoption.entries()].map(([label, t]) => ({
|
|
1949
|
+
label,
|
|
1950
|
+
files: t.files,
|
|
1951
|
+
using: t.using,
|
|
1952
|
+
})),
|
|
1953
|
+
},
|
|
1954
|
+
}
|
|
1955
|
+
: {}),
|
|
1917
1956
|
...(conventions.length > 0 ? { conventions } : {}),
|
|
1918
1957
|
classStyle,
|
|
1919
1958
|
...(Object.keys(looks).length > 0 ? { looks } : {}),
|
|
@@ -2029,6 +2068,8 @@ export async function takeCensus(root, opts) {
|
|
|
2029
2068
|
named: d.named,
|
|
2030
2069
|
tokenUses: d.tokenUses,
|
|
2031
2070
|
coverage: d.coverage,
|
|
2071
|
+
ownUses: d.ownUses,
|
|
2072
|
+
phantomUses: d.phantomUses,
|
|
2032
2073
|
},
|
|
2033
2074
|
};
|
|
2034
2075
|
}
|
|
@@ -2089,9 +2130,73 @@ async function summarize(c, root, scope) {
|
|
|
2089
2130
|
}
|
|
2090
2131
|
}
|
|
2091
2132
|
sayConventions(c);
|
|
2133
|
+
sayScopes(c);
|
|
2134
|
+
sayAdoption(c);
|
|
2092
2135
|
sayCollisions(c);
|
|
2093
2136
|
await sayBrokenRefs(c, root, scope);
|
|
2094
2137
|
}
|
|
2138
|
+
/**
|
|
2139
|
+
* OS ESCOPOS QUE VIRARAM UM SISTEMA, e o que cada um perdeu no empate.
|
|
2140
|
+
*
|
|
2141
|
+
* `--scope` aceita mais de um, e a ordem é uma declaração: o primeiro vence. Um
|
|
2142
|
+
* token que os dois declaram diferente não é erro de ninguém - é divergência, e
|
|
2143
|
+
* a lei do dono (§8.6) é que a plataforma não decide sozinha. Ela mostra os
|
|
2144
|
+
* dois, diz quem é a fonte declarada, e propõe alinhar o outro lado.
|
|
2145
|
+
*
|
|
2146
|
+
* Medido no monorepo dele: `packages/ui` e `apps/web-dashboard/src` declaram os
|
|
2147
|
+
* mesmos três `--gradient-gray-*` com valores opostos - claros na biblioteca,
|
|
2148
|
+
* escuros no app. Nenhum dos dois está errado, e um deles ia desaparecer calado.
|
|
2149
|
+
*/
|
|
2150
|
+
function sayScopes(c) {
|
|
2151
|
+
const scopes = c.scopes ?? [];
|
|
2152
|
+
if (scopes.length < 2)
|
|
2153
|
+
return;
|
|
2154
|
+
const conflicts = c.scopeConflicts ?? [];
|
|
2155
|
+
console.log("");
|
|
2156
|
+
console.log(section("Two folders, one system"));
|
|
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
|
+
if (conflicts.length === 0)
|
|
2159
|
+
return;
|
|
2160
|
+
console.log("");
|
|
2161
|
+
console.log(body(`${conflicts.length} token${conflicts.length === 1 ? "" : "s"} ${conflicts.length === 1 ? "is" : "are"} declared differently in two of them:`));
|
|
2162
|
+
console.log("");
|
|
2163
|
+
for (const x of conflicts.slice(0, 5)) {
|
|
2164
|
+
console.log(body(`${paint.strong(x.name)}\n ${paint.faint("kept")} ${x.kept.value} ${paint.faint(`(${x.kept.scope})`)}\n ${paint.faint("also")} ${x.lost.value} ${paint.faint(`(${x.lost.scope})`)}`));
|
|
2165
|
+
}
|
|
2166
|
+
if (conflicts.length > 5)
|
|
2167
|
+
console.log(body(paint.faint(`(${conflicts.length - 5} more)`)));
|
|
2168
|
+
console.log("");
|
|
2169
|
+
console.log(body(paint.faint("Neither value is wrong - one of them is a different scheme, or a fork that drifted. The system carries the first; the screen can show both.")));
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* QUANTO DA MIGRAÇÃO JÁ ACONTECEU - o número que faltava.
|
|
2173
|
+
*
|
|
2174
|
+
* O `intent` já dizia que alguém está migrando design para o pacote
|
|
2175
|
+
* compartilhado (`init --intent migrate`, versionado em `_synthesisui/config.json`
|
|
2176
|
+
* com a data). O que nenhuma superfície dizia é QUANTO já chegou. Medido no
|
|
2177
|
+
* monorepo do dono: 103 de 356 arquivos do `web-dashboard` importam
|
|
2178
|
+
* `@frontend-hub/ui`, e o `web-review` não o conhece.
|
|
2179
|
+
*
|
|
2180
|
+
* É a diferença entre medir estado e medir progresso: um relatório de estado é
|
|
2181
|
+
* igual em janeiro e em junho; este muda a cada versão.
|
|
2182
|
+
*/
|
|
2183
|
+
function sayAdoption(c) {
|
|
2184
|
+
const a = c.adoption;
|
|
2185
|
+
if (!a || a.consumers.length === 0)
|
|
2186
|
+
return;
|
|
2187
|
+
const total = a.consumers.reduce((n, x) => n + x.files, 0);
|
|
2188
|
+
const using = a.consumers.reduce((n, x) => n + x.using, 0);
|
|
2189
|
+
console.log("");
|
|
2190
|
+
console.log(section("How far the system already reaches"));
|
|
2191
|
+
console.log(body(`${using} of ${total} source files across the folders you named import ${paint.strong(a.specifier)}. Everything else in them is still painting on its own.`));
|
|
2192
|
+
console.log("");
|
|
2193
|
+
for (const x of a.consumers) {
|
|
2194
|
+
const pct = x.files > 0 ? Math.round((x.using / x.files) * 100) : 0;
|
|
2195
|
+
console.log(body(`${paint.strong(x.label.padEnd(24))} ${String(x.using).padStart(4)} of ${String(x.files).padStart(4)} files ${paint.faint(`· ${pct}%`)}${x.using === 0 ? paint.faint(" - does not know the system yet") : ""}`));
|
|
2196
|
+
}
|
|
2197
|
+
console.log("");
|
|
2198
|
+
console.log(body(paint.faint("This is the number a migration is measured by, and it is the one that moves. Run the import again after a batch and the difference is the progress.")));
|
|
2199
|
+
}
|
|
2095
2200
|
/**
|
|
2096
2201
|
* DOIS ARQUIVOS, UM NOME - dito, com os dois caminhos.
|
|
2097
2202
|
*
|
|
@@ -2107,16 +2212,35 @@ function sayCollisions(c) {
|
|
|
2107
2212
|
const all = c.collisions ?? [];
|
|
2108
2213
|
if (all.length === 0)
|
|
2109
2214
|
return;
|
|
2215
|
+
/**
|
|
2216
|
+
* DUAS REGRAS, DUAS FRASES. Dentro de um escopo vence quem está mais perto da
|
|
2217
|
+
* raiz; entre escopos vence o primeiro `--scope`. Explicar um empate com a
|
|
2218
|
+
* regra do outro é descrever algo que não aconteceu.
|
|
2219
|
+
*/
|
|
2220
|
+
const crossScope = all.filter((x) => x.by === "scope");
|
|
2221
|
+
const inScope = all.filter((x) => x.by !== "scope");
|
|
2110
2222
|
const names = new Set(all.map((x) => x.name));
|
|
2111
2223
|
console.log("");
|
|
2112
|
-
console.log(section("Two
|
|
2113
|
-
console.log(body(`${names.size} name${names.size === 1 ? "" : "s"} ${names.size === 1 ? "is" : "are"} defined more than once in what was measured, so ${all.length} definition${all.length === 1 ? "" : "s"} did not make it into this system
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
console.log(body(`${
|
|
2224
|
+
console.log(section("Two places, one name"));
|
|
2225
|
+
console.log(body(`${names.size} name${names.size === 1 ? "" : "s"} ${names.size === 1 ? "is" : "are"} defined more than once in what was measured, so ${all.length} definition${all.length === 1 ? "" : "s"} did not make it into this system.`));
|
|
2226
|
+
if (crossScope.length > 0) {
|
|
2227
|
+
console.log("");
|
|
2228
|
+
console.log(body(`${crossScope.length} of them in two different scopes - the FIRST --scope kept the name:`));
|
|
2229
|
+
console.log("");
|
|
2230
|
+
for (const x of crossScope.slice(0, 6))
|
|
2231
|
+
console.log(body(`${paint.strong(x.name.padEnd(22))} ${paint.faint("kept in")} ${x.kept} ${paint.faint("· also in")} ${x.dropped}`));
|
|
2232
|
+
if (crossScope.length > 6)
|
|
2233
|
+
console.log(body(paint.faint(`(${crossScope.length - 6} more)`)));
|
|
2234
|
+
}
|
|
2235
|
+
if (inScope.length > 0) {
|
|
2236
|
+
console.log("");
|
|
2237
|
+
console.log(body(`${inScope.length} inside a single scope - the file closest to its root kept the name, never the order the disk happened to return:`));
|
|
2238
|
+
console.log("");
|
|
2239
|
+
for (const x of inScope.slice(0, 6))
|
|
2240
|
+
console.log(body(`${paint.strong(x.name)}\n ${paint.faint("kept")} ${x.kept}\n ${paint.faint("dropped")} ${x.dropped}`));
|
|
2241
|
+
if (inScope.length > 6)
|
|
2242
|
+
console.log(body(paint.faint(`(${inScope.length - 6} more)`)));
|
|
2117
2243
|
}
|
|
2118
|
-
if (all.length > 6)
|
|
2119
|
-
console.log(body(paint.faint(`(${all.length - 6} more)`)));
|
|
2120
2244
|
console.log("");
|
|
2121
2245
|
console.log(body(paint.faint("If the homonyms belong to different projects, a narrower --scope is the answer and nothing is lost. If they belong to the same one, renaming in your code is - two components with one name is one name your agent cannot resolve either.")));
|
|
2122
2246
|
}
|
|
@@ -2474,8 +2598,21 @@ async function sayAboutPlace(root, scope) {
|
|
|
2474
2598
|
console.log(section("This folder is one package of a workspace"));
|
|
2475
2599
|
console.log(body(`${paint.strong(place.here)} is one of ${place.packages.length} packages under ${paint.strong(place.workspaceRoot)}.`));
|
|
2476
2600
|
console.log("");
|
|
2477
|
-
console.log(body(
|
|
2478
|
-
|
|
2601
|
+
console.log(body(
|
|
2602
|
+
/**
|
|
2603
|
+
* A FRASE SEGUE O CRITÉRIO, e o critério é estrutural.
|
|
2604
|
+
*
|
|
2605
|
+
* Enquanto a contagem incluía `dist`, dizer "o vocabulário não está aqui"
|
|
2606
|
+
* fechava com os números: 764 contra 114. Com o build output fora, a
|
|
2607
|
+
* biblioteca declara 97 e o APP declara 114 - a frase antiga passaria a
|
|
2608
|
+
* contradizer os dois números que ela mesma imprime.
|
|
2609
|
+
*
|
|
2610
|
+
* O que faz `packages/ui` ser a fonte de verdade não é volume: é os outros
|
|
2611
|
+
* pacotes o importarem. Dizer isso é dizer a razão verdadeira, e ela
|
|
2612
|
+
* sobrevive ao caso em que o app declara mais.
|
|
2613
|
+
*/
|
|
2614
|
+
`${paint.strong(vocab.rel)} is what the other ${vocab.importedBy} package${vocab.importedBy === 1 ? "" : "s"} here import${vocab.importedBy === 1 ? "s" : ""} - that is the shared vocabulary, ${vocab.declarations} token${vocab.declarations === 1 ? "" : "s"} across ${vocab.files} file${vocab.files === 1 ? "" : "s"}. This package declares ${mine?.declarations ?? 0} of its own across ${mine?.files ?? 0}, and those are this app's.`));
|
|
2615
|
+
console.log(body(paint.faint("A system measured from the app is one consumer's set of choices; the shared vocabulary is where the library declares it.")));
|
|
2479
2616
|
console.log("");
|
|
2480
2617
|
console.log(body("The system is one place, and the evidence is another:"));
|
|
2481
2618
|
console.log("");
|
|
@@ -3158,8 +3295,18 @@ export async function runImport(opts) {
|
|
|
3158
3295
|
.trim()
|
|
3159
3296
|
.replace(/^\.\/+/, "")
|
|
3160
3297
|
.replace(/\/+$/, "");
|
|
3161
|
-
|
|
3162
|
-
|
|
3298
|
+
/**
|
|
3299
|
+
* OS ESCOPOS, na ordem em que a pessoa os escreveu - e `scope` é o primeiro.
|
|
3300
|
+
*
|
|
3301
|
+
* O parser de flags já acumula uma flag repetida (`--usage` usa isso desde
|
|
3302
|
+
* 01/08), então `--scope packages/ui --scope packages/charts` chega aqui como
|
|
3303
|
+
* array sem nenhuma mudança de gramática.
|
|
3304
|
+
*/
|
|
3305
|
+
const scopes = (Array.isArray(opts.scope) ? opts.scope : opts.scope ? [opts.scope] : [])
|
|
3306
|
+
.map(tidy)
|
|
3307
|
+
.filter(Boolean)
|
|
3308
|
+
.filter((v, i, all) => all.indexOf(v) === i);
|
|
3309
|
+
const scope = scopes[0];
|
|
3163
3310
|
/**
|
|
3164
3311
|
* THE SECOND ROLE. `--scope` is the SYSTEM - one place, and the only source of
|
|
3165
3312
|
* the palette, the scales and the convention. `--usage` is the EVIDENCE - as
|
|
@@ -3234,13 +3381,33 @@ export async function runImport(opts) {
|
|
|
3234
3381
|
*/
|
|
3235
3382
|
phase(1, 3, "Measuring the repository");
|
|
3236
3383
|
console.log(section("Reading your project"));
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3384
|
+
/**
|
|
3385
|
+
* UMA MEDIÇÃO POR ESCOPO, E DEPOIS A FUSÃO.
|
|
3386
|
+
*
|
|
3387
|
+
* `takeCensus` recebe UM `root` em cinco pontos estruturais que são por
|
|
3388
|
+
* PACOTE e não por sistema (`harvestOwnCss`, `internalSpecifiers`,
|
|
3389
|
+
* `publicApi`, o `walk`, `aliasesOf`), então ensiná-la a medir N raízes
|
|
3390
|
+
* seria refatorar a função de 1855 linhas que o corpus dourado protege.
|
|
3391
|
+
* Medir N vezes e fundir troca esse risco por uma função pura - e as regras
|
|
3392
|
+
* de empate ficam num arquivo em vez de espalhadas por um laço.
|
|
3393
|
+
*
|
|
3394
|
+
* A EVIDÊNCIA VIAJA COM O PRIMEIRO: `--usage` doa contagem e lei, e somar a
|
|
3395
|
+
* mesma evidência N vezes multiplicaria os números que decidem o que é
|
|
3396
|
+
* hábito. Ela entra uma vez, na medição do escopo que declara.
|
|
3397
|
+
*/
|
|
3398
|
+
const measured = [];
|
|
3399
|
+
for (const [i, one] of (scopes.length > 0 ? scopes : [""]).entries()) {
|
|
3400
|
+
const at = one ? join(root, one) : root;
|
|
3401
|
+
const c = await takeCensus(at, {
|
|
3402
|
+
...(i === 0 && usage.length > 0 ? { usage } : {}),
|
|
3403
|
+
...(one ? { scopeLabel: one } : {}),
|
|
3404
|
+
...(opts.cli ? { cli: opts.cli } : {}),
|
|
3405
|
+
});
|
|
3406
|
+
if (one)
|
|
3407
|
+
c.scope = one;
|
|
3408
|
+
measured.push(c);
|
|
3409
|
+
}
|
|
3410
|
+
census = mergeCensus(measured);
|
|
3244
3411
|
}
|
|
3245
3412
|
/**
|
|
3246
3413
|
* DERIVE THE ANATOMY ON THE MEASURING PATH TOO.
|
|
@@ -3270,8 +3437,30 @@ export async function runImport(opts) {
|
|
|
3270
3437
|
* `sayAboutPlace` faz as duas perguntas na ordem: onde este comando está, e
|
|
3271
3438
|
* onde as declarações estão. Ele só fala quando as respostas divergem.
|
|
3272
3439
|
*/
|
|
3273
|
-
|
|
3440
|
+
/**
|
|
3441
|
+
* OS CANDIDATOS ENTRAM NO CENSO, e é o mesmo `placeInWorkspace` que a mensagem
|
|
3442
|
+
* de lugar já usa - uma medição, dois consumidores.
|
|
3443
|
+
*
|
|
3444
|
+
* Vive aqui e não dentro de `takeCensus` porque a pergunta é sobre o
|
|
3445
|
+
* WORKSPACE, e `takeCensus` mede um escopo: chamá-la de lá faria cada escopo
|
|
3446
|
+
* medido recontar os mesmos cinco pacotes.
|
|
3447
|
+
*/
|
|
3448
|
+
if (!opts.census) {
|
|
3449
|
+
const place = await placeInWorkspace(root).catch(() => null);
|
|
3450
|
+
if (place)
|
|
3451
|
+
census.workspace = {
|
|
3452
|
+
root: place.workspaceRoot,
|
|
3453
|
+
here: place.here,
|
|
3454
|
+
packages: place.packages.map((p) => ({
|
|
3455
|
+
rel: p.rel,
|
|
3456
|
+
name: p.name,
|
|
3457
|
+
declarations: p.declarations,
|
|
3458
|
+
files: p.files,
|
|
3459
|
+
importedBy: p.importedBy,
|
|
3460
|
+
})),
|
|
3461
|
+
};
|
|
3274
3462
|
await sayAboutPlace(root, scope);
|
|
3463
|
+
}
|
|
3275
3464
|
// A census handed to us is written BACK TO ITSELF; one we took lands at the
|
|
3276
3465
|
// ROOT, whatever it measured. Everything else in `_synthesisui/` is anchored
|
|
3277
3466
|
// there - config, `ds/<slug>/`, the hook's marker - and the census was the
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1042,6 +1042,23 @@ async function describeComponent(root, name) {
|
|
|
1042
1042
|
if ((recipe.companions ?? []).length > 0) {
|
|
1043
1043
|
out.push("", `Goes with: ${(recipe.companions ?? []).join(", ")} - measured pairings; call describe_component on one before composing them together.`);
|
|
1044
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* ONDE ELE MORA, E COMO CHEGAR NELE - a linha que decide se o agente importa
|
|
1047
|
+
* ou reescreve.
|
|
1048
|
+
*
|
|
1049
|
+
* `api.from` é o especificador do pacote que o EXPORTA (a lei 13 já o mediu), e
|
|
1050
|
+
* ele só existe para um componente de biblioteca. Quando há um, importar é a
|
|
1051
|
+
* resposta e escrever de novo é duplicar. Quando não há, o caminho é o que
|
|
1052
|
+
* localiza o arquivo - e num sistema de dois escopos ele é a única coisa que
|
|
1053
|
+
* diz de qual dos dois este componente é.
|
|
1054
|
+
*/
|
|
1055
|
+
if (recipe.source?.file || recipe.api?.from) {
|
|
1056
|
+
const where = recipe.source?.file;
|
|
1057
|
+
const pkg = recipe.api?.from;
|
|
1058
|
+
out.push("", pkg
|
|
1059
|
+
? `Lives in: ${where ?? pkg} - exported by ${pkg}. Import it from there; writing it again duplicates a component this system already owns.`
|
|
1060
|
+
: `Lives in: ${where}${recipe.source?.scope ? ` (scope ${recipe.source.scope})` : ""} - local to that folder, and not exported by a package.`);
|
|
1061
|
+
}
|
|
1045
1062
|
if (recipe.usageStats && recipe.usageStats.count > 0) {
|
|
1046
1063
|
const u = recipe.usageStats;
|
|
1047
1064
|
out.push("", `In use: ${u.count} instance${u.count === 1 ? "" : "s"} across ${u.files} file${u.files === 1 ? "" : "s"}${u.projects?.length ? ` in ${u.projects.join(", ")}` : ""}.`);
|
package/dist/commands/sync.js
CHANGED
|
@@ -6,10 +6,11 @@ import { markSent, readEvents } from "../doctor/ledger.js";
|
|
|
6
6
|
import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
7
7
|
import { describeDelta, fingerprintReadings, readSyncMark, writeSyncMark, } from "../last-sync.js";
|
|
8
8
|
import { measuredScope, rememberScope } from "../measured-scope.js";
|
|
9
|
+
import { mergeCensus } from "../merge-census.js";
|
|
9
10
|
import { body, paint, section, snippet } from "../output.js";
|
|
10
11
|
import { repoStateOf } from "../repo-state.js";
|
|
11
12
|
import { reportWhatIsLeft } from "./align.js";
|
|
12
|
-
import { resolveReadParts, siblingProjects, takeCensus } from "./import.js";
|
|
13
|
+
import { resolveReadParts, siblingProjects, takeCensus, } from "./import.js";
|
|
13
14
|
/**
|
|
14
15
|
* What each decision means ON THIS MACHINE - the card decides, the sync
|
|
15
16
|
* enacts. The web never touches the local file; this line is printed right
|
|
@@ -247,7 +248,25 @@ export async function remeasure(args) {
|
|
|
247
248
|
* O servidor continua valendo como segunda fonte: um clone fresco tem o `.lock` e não tem censo.
|
|
248
249
|
*/
|
|
249
250
|
const local = await measuredScope(root);
|
|
250
|
-
|
|
251
|
+
/**
|
|
252
|
+
* TODOS os escopos do sistema, não só o primeiro.
|
|
253
|
+
*
|
|
254
|
+
* Esta linha lia `local.system` - singular - e um sistema importado de dois
|
|
255
|
+
* escopos remedia só um deles: 351 componentes nasciam e 36 eram remedidos. Os
|
|
256
|
+
* outros 315 não eram apagados (`presenceOf` os conta como fora do escopo
|
|
257
|
+
* medido, de propósito) e também nunca mais eram atualizados - uma feature que
|
|
258
|
+
* emudece no dia seguinte.
|
|
259
|
+
*
|
|
260
|
+
* A queda para `scope` mantém todo sistema anterior lendo como antes.
|
|
261
|
+
*/
|
|
262
|
+
let scopes = local.systems.length > 0
|
|
263
|
+
? local.systems
|
|
264
|
+
: local.system
|
|
265
|
+
? [local.system]
|
|
266
|
+
: stored.scope
|
|
267
|
+
? [stored.scope]
|
|
268
|
+
: [];
|
|
269
|
+
let scope = scopes[0] ?? null;
|
|
251
270
|
let usage = local.usage.length > 0 ? local.usage : (stored.usage ?? []);
|
|
252
271
|
/**
|
|
253
272
|
* E QUANDO NINGUÉM SABE, ESTE COMANDO PERGUNTA - uma vez, e grava a resposta.
|
|
@@ -263,26 +282,43 @@ export async function remeasure(args) {
|
|
|
263
282
|
const asked = await askForScope(root);
|
|
264
283
|
if (asked) {
|
|
265
284
|
scope = asked.scope;
|
|
285
|
+
scopes = [asked.scope];
|
|
266
286
|
if (asked.usage.length > 0)
|
|
267
287
|
usage = asked.usage;
|
|
268
|
-
await rememberScope(root, slug,
|
|
288
|
+
await rememberScope(root, slug, scopes, usage);
|
|
269
289
|
}
|
|
270
290
|
}
|
|
271
291
|
console.log(section("Measuring your repository again"));
|
|
272
|
-
console.log(body(paint.faint(`${scope ? `reading ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
|
|
292
|
+
console.log(body(paint.faint(`${scopes.length > 1 ? `reading ${scopes.join(" + ")}` : scope ? `reading ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
|
|
273
293
|
? " · read from _synthesisui/census.json, where the last measurement recorded it"
|
|
274
294
|
: ""} · what you already wrote about your app travels unchanged`)));
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
/**
|
|
296
|
+
* UMA MEDIÇÃO POR ESCOPO, FUNDIDAS - o mesmo desenho do `import`.
|
|
297
|
+
*
|
|
298
|
+
* `takeCensus` recebe um `root` em cinco pontos que são por PACOTE, então medir
|
|
299
|
+
* N raízes de dentro dela seria refatorar a função que o corpus dourado guarda.
|
|
300
|
+
* A evidência (`--usage`) entra UMA vez, com o primeiro escopo: somá-la N vezes
|
|
301
|
+
* multiplicaria as contagens que decidem o que é hábito.
|
|
302
|
+
*/
|
|
303
|
+
const measuredList = [];
|
|
304
|
+
for (const [i, one] of (scopes.length > 0 ? scopes : [""]).entries()) {
|
|
305
|
+
const c = await takeCensus(one ? join(root, one) : root, {
|
|
306
|
+
...(one ? { scopeLabel: one } : {}),
|
|
307
|
+
...(args.cli ? { cli: args.cli } : {}),
|
|
308
|
+
/** Uma RE-medição conta o que MUDOU; o relatório inteiro fica no `--full` - ver `say`. */
|
|
309
|
+
...(args.full ? {} : { quiet: true }),
|
|
310
|
+
/** O `--usage` é EVIDÊNCIA e leva rótulo: sem ele o censo não sabe de que app o número veio. */
|
|
311
|
+
usage: i === 0 ? usage.map((u) => ({ path: join(root, u), label: u })) : [],
|
|
312
|
+
}).catch(() => null);
|
|
313
|
+
if (!c)
|
|
314
|
+
break;
|
|
315
|
+
if (one)
|
|
316
|
+
c.scope = one;
|
|
317
|
+
measuredList.push(c);
|
|
318
|
+
}
|
|
319
|
+
const census = measuredList.length === (scopes.length > 0 ? scopes.length : 1)
|
|
320
|
+
? mergeCensus(measuredList)
|
|
321
|
+
: null;
|
|
286
322
|
if (!census) {
|
|
287
323
|
console.log(body("The measurement failed, so nothing was sent."));
|
|
288
324
|
return;
|
package/dist/index.js
CHANGED
|
@@ -89,7 +89,8 @@ Options:
|
|
|
89
89
|
--version <n> install a specific version (default: latest)
|
|
90
90
|
--ds <slug> init: bring this DS in right away · generate: target DS (default: installed)
|
|
91
91
|
--name <name> preferred component name for generate
|
|
92
|
-
--scope <path> import: the SYSTEM - tokens, components, convention.
|
|
92
|
+
--scope <path> import: the SYSTEM - tokens, components, convention. Repeatable;
|
|
93
|
+
the first one wins a tie.
|
|
93
94
|
--usage <path> import: the EVIDENCE - counts, chosen values, laws. Repeatable.
|
|
94
95
|
--scheme <s> dark|light - which end of your ladder is the default
|
|
95
96
|
--target <t> template/init target: next | general (default: next)
|
|
@@ -160,7 +161,16 @@ async function main() {
|
|
|
160
161
|
// Only these two words. Anything else is a typo that would silently
|
|
161
162
|
// invert a system, so it falls through to being measured and asked.
|
|
162
163
|
// WHAT to read. `--dir` stays WHERE the project is, in every command.
|
|
163
|
-
|
|
164
|
+
/**
|
|
165
|
+
* `--scope` REPETÍVEL: um sistema pode viver em dois pacotes, e o parser
|
|
166
|
+
* já acumula flag repetida (`--usage` usa isso). A ordem é a que a pessoa
|
|
167
|
+
* escreveu, e ela decide os empates - ver `merge-census.ts`.
|
|
168
|
+
*/
|
|
169
|
+
scope: Array.isArray(flags.scope)
|
|
170
|
+
? flags.scope
|
|
171
|
+
: typeof flags.scope === "string"
|
|
172
|
+
? flags.scope
|
|
173
|
+
: undefined,
|
|
164
174
|
// WHERE THE EVIDENCE IS. Repeatable, and comma-separated works too - a
|
|
165
175
|
// person typing one path with a comma in it means two paths.
|
|
166
176
|
usage: [flags.usage]
|
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.255";
|
|
104
104
|
/**
|
|
105
105
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
106
106
|
*
|
package/dist/measured-scope.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
const EMPTY = {
|
|
3
|
+
const EMPTY = {
|
|
4
|
+
system: null,
|
|
5
|
+
systems: [],
|
|
6
|
+
usage: [],
|
|
7
|
+
from: "none",
|
|
8
|
+
};
|
|
4
9
|
/**
|
|
5
10
|
* O ESCOPO NO PONTEIRO DO SISTEMA, que é onde ele não se perde.
|
|
6
11
|
*
|
|
@@ -27,22 +32,42 @@ async function scopeInLock(root) {
|
|
|
27
32
|
catch {
|
|
28
33
|
continue;
|
|
29
34
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `scopes` PRIMEIRO, `scope` como queda - e nunca o contrário.
|
|
37
|
+
*
|
|
38
|
+
* Um lock escrito antes desta versão só tem `scope`, e ele continua sendo lido
|
|
39
|
+
* exatamente como antes. Um lock novo tem os dois, e a lista é a verdade
|
|
40
|
+
* completa: ler o singular ali daria um sistema de um escopo onde há dois.
|
|
41
|
+
*/
|
|
42
|
+
const listed = Array.isArray(lock.scopes)
|
|
43
|
+
? lock.scopes.filter((x) => typeof x === "string" && !!x.trim())
|
|
44
|
+
: [];
|
|
45
|
+
const scope = listed[0] ??
|
|
46
|
+
(typeof lock.scope === "string" && lock.scope.trim()
|
|
47
|
+
? lock.scope.trim()
|
|
48
|
+
: null);
|
|
49
|
+
const declared = listed.length > 0 ? listed : scope ? [scope] : [];
|
|
33
50
|
const usage = Array.isArray(lock.usage)
|
|
34
51
|
? lock.usage.filter((u) => typeof u === "string" && !!u.trim())
|
|
35
52
|
: [];
|
|
36
|
-
if (
|
|
53
|
+
if (declared.length === 0 && usage.length === 0)
|
|
37
54
|
continue;
|
|
38
|
-
const
|
|
55
|
+
const systems = [];
|
|
56
|
+
for (const d of declared)
|
|
57
|
+
if (await present(root, d))
|
|
58
|
+
systems.push(d);
|
|
39
59
|
const kept = [];
|
|
40
60
|
for (const u of usage)
|
|
41
61
|
if (await present(root, u))
|
|
42
62
|
kept.push(u);
|
|
43
|
-
if (
|
|
63
|
+
if (systems.length === 0 && kept.length === 0)
|
|
44
64
|
continue;
|
|
45
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
system: systems[0] ?? null,
|
|
67
|
+
systems,
|
|
68
|
+
usage: kept,
|
|
69
|
+
from: "lock",
|
|
70
|
+
};
|
|
46
71
|
}
|
|
47
72
|
return null;
|
|
48
73
|
}
|
|
@@ -81,20 +106,29 @@ export async function censusScope(root) {
|
|
|
81
106
|
catch {
|
|
82
107
|
return EMPTY;
|
|
83
108
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
/** `scopes` primeiro, `scope` como queda - ver `scopeInLock`, mesma regra. */
|
|
110
|
+
const listed = Array.isArray(parsed.scopes)
|
|
111
|
+
? parsed.scopes.filter((x) => typeof x === "string" && !!x.trim())
|
|
112
|
+
: [];
|
|
113
|
+
const scope = listed[0] ??
|
|
114
|
+
(typeof parsed.scope === "string" && parsed.scope.trim()
|
|
115
|
+
? parsed.scope.trim()
|
|
116
|
+
: null);
|
|
117
|
+
const declared = listed.length > 0 ? listed : scope ? [scope] : [];
|
|
87
118
|
const usage = Array.isArray(parsed.usage)
|
|
88
119
|
? parsed.usage.filter((u) => typeof u === "string" && !!u.trim())
|
|
89
120
|
: [];
|
|
90
|
-
const
|
|
121
|
+
const systems = [];
|
|
122
|
+
for (const d of declared)
|
|
123
|
+
if (await present(root, d))
|
|
124
|
+
systems.push(d);
|
|
91
125
|
const kept = [];
|
|
92
126
|
for (const u of usage)
|
|
93
127
|
if (await present(root, u))
|
|
94
128
|
kept.push(u);
|
|
95
|
-
if (
|
|
129
|
+
if (systems.length === 0 && kept.length === 0)
|
|
96
130
|
return EMPTY;
|
|
97
|
-
return { system, usage: kept, from: "census" };
|
|
131
|
+
return { system: systems[0] ?? null, systems, usage: kept, from: "census" };
|
|
98
132
|
}
|
|
99
133
|
/**
|
|
100
134
|
* As pastas a LER, na ordem em que importam - o sistema primeiro.
|
|
@@ -103,7 +137,12 @@ export async function censusScope(root) {
|
|
|
103
137
|
* anotações no CI, decide o que chega ao PR.
|
|
104
138
|
*/
|
|
105
139
|
export function scopePaths(m) {
|
|
106
|
-
|
|
140
|
+
/**
|
|
141
|
+
* TODOS os sistemas, não só o primeiro - senão `absorb` e `summary` veem meia
|
|
142
|
+
* biblioteca num sistema importado de dois escopos.
|
|
143
|
+
*/
|
|
144
|
+
const systems = m.systems.length > 0 ? m.systems : m.system ? [m.system] : [];
|
|
145
|
+
return [...systems, ...m.usage];
|
|
107
146
|
}
|
|
108
147
|
/** Como dizer o que foi lido. Uma leitura que não declara o universo é um número sem denominador. */
|
|
109
148
|
export function describeScope(m) {
|
|
@@ -131,7 +170,18 @@ export function describeScope(m) {
|
|
|
131
170
|
*
|
|
132
171
|
* Silencioso quando não há lock: não é erro, é um repo sem sistema instalado.
|
|
133
172
|
*/
|
|
134
|
-
export async function rememberScope(root, slug,
|
|
173
|
+
export async function rememberScope(root, slug,
|
|
174
|
+
/**
|
|
175
|
+
* OS ESCOPOS, na ordem em que a pessoa os pediu - o primeiro é a autoridade.
|
|
176
|
+
*
|
|
177
|
+
* Aceita a lista e grava OS DOIS campos: `scopes` com tudo e `scope` com o
|
|
178
|
+
* primeiro. Um CLI anterior lê `scope` e continua funcionando; um lock antigo
|
|
179
|
+
* lido por este CLI cai no singular. É a compatibilidade nos dois sentidos, e
|
|
180
|
+
* ela custa uma linha.
|
|
181
|
+
*/
|
|
182
|
+
scopes, usage) {
|
|
183
|
+
const list = (Array.isArray(scopes) ? scopes : scopes ? [scopes] : []).filter(Boolean);
|
|
184
|
+
const scope = list[0] ?? null;
|
|
135
185
|
const path = join(root, "_synthesisui", "ds", slug, ".lock");
|
|
136
186
|
const raw = await readFile(path, "utf8").catch(() => null);
|
|
137
187
|
if (!raw)
|
|
@@ -146,6 +196,7 @@ export async function rememberScope(root, slug, scope, usage) {
|
|
|
146
196
|
await writeFile(path, `${JSON.stringify({
|
|
147
197
|
...lock,
|
|
148
198
|
...(scope ? { scope } : {}),
|
|
199
|
+
...(list.length > 0 ? { scopes: list } : {}),
|
|
149
200
|
...(usage.length > 0 ? { usage } : {}),
|
|
150
201
|
}, null, 2)}\n`, "utf8");
|
|
151
202
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const asRecord = (v) => (v ?? {});
|
|
2
|
+
/** Nome do escopo para as mensagens - `.` quando a medição não escopou nada. */
|
|
3
|
+
const nameOf = (c, i) => c.scope ?? (i === 0 ? "." : `#${i + 1}`);
|
|
4
|
+
/**
|
|
5
|
+
* OS TOKENS, com o primeiro escopo mandando - e o conflito registrado.
|
|
6
|
+
*
|
|
7
|
+
* `declared` é o coração: é dele que saem os nomes que viajam para a tela. Um
|
|
8
|
+
* `--brand-500` que vale `#1394dc` na biblioteca e `#0f6fa8` no pacote de
|
|
9
|
+
* gráficos não é um erro de ninguém, é uma divergência - e a lei do dono
|
|
10
|
+
* (§8.6) é que a plataforma não decide sozinha: ela mostra os dois e diz quem é
|
|
11
|
+
* a fonte declarada.
|
|
12
|
+
*/
|
|
13
|
+
function mergeDeclared(into, from, keptScope, fromScope, conflicts) {
|
|
14
|
+
for (const [name, value] of Object.entries(from)) {
|
|
15
|
+
const held = into[name];
|
|
16
|
+
if (held === undefined) {
|
|
17
|
+
into[name] = value;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (held === value)
|
|
21
|
+
continue;
|
|
22
|
+
conflicts.push({
|
|
23
|
+
kind: "token",
|
|
24
|
+
name,
|
|
25
|
+
kept: { scope: keptScope, value: held },
|
|
26
|
+
lost: { scope: fromScope, value },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Os valores medidos, somados por (natureza, valor) - e o nome vem de quem o tem. */
|
|
31
|
+
function mergeObserved(a, b) {
|
|
32
|
+
const out = a.map((v) => ({ ...v }));
|
|
33
|
+
const at = new Map(out.map((v, i) => [`${v.kind}:${v.value}`, i]));
|
|
34
|
+
for (const v of b) {
|
|
35
|
+
const key = `${v.kind}:${v.value}`;
|
|
36
|
+
const i = at.get(key);
|
|
37
|
+
if (i === undefined) {
|
|
38
|
+
at.set(key, out.length);
|
|
39
|
+
out.push({ ...v });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
out[i].count += v.count;
|
|
43
|
+
out[i].files += v.files;
|
|
44
|
+
/** Um nome não se apaga por o outro escopo não ter um. */
|
|
45
|
+
if (!out[i].token && v.token)
|
|
46
|
+
out[i].token = v.token;
|
|
47
|
+
}
|
|
48
|
+
return out.sort((x, y) => y.count - x.count);
|
|
49
|
+
}
|
|
50
|
+
/** União preservando a ordem de chegada - a do primeiro escopo primeiro. */
|
|
51
|
+
const union = (a, b) => [
|
|
52
|
+
...new Set([...(a ?? []), ...(b ?? [])]),
|
|
53
|
+
];
|
|
54
|
+
const concat = (a, b) => [
|
|
55
|
+
...(a ?? []),
|
|
56
|
+
...(b ?? []),
|
|
57
|
+
];
|
|
58
|
+
/** Soma campo a campo dois objetos de contagem, mantendo o que só um tem. */
|
|
59
|
+
function sumInto(a, b) {
|
|
60
|
+
if (!a)
|
|
61
|
+
return b;
|
|
62
|
+
if (!b)
|
|
63
|
+
return a;
|
|
64
|
+
const out = { ...a };
|
|
65
|
+
for (const [k, v] of Object.entries(b)) {
|
|
66
|
+
const mine = out[k];
|
|
67
|
+
if (typeof v === "number" && typeof mine === "number")
|
|
68
|
+
out[k] = mine + v;
|
|
69
|
+
else if (mine === undefined)
|
|
70
|
+
out[k] = v;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* OS CENSOS, FUNDIDOS NA ORDEM EM QUE A PESSOA OS PEDIU.
|
|
76
|
+
*
|
|
77
|
+
* Devolve o próprio objeto quando há um só: o caminho comum - um escopo - não
|
|
78
|
+
* paga nada por esta função existir.
|
|
79
|
+
*/
|
|
80
|
+
export function mergeCensus(list) {
|
|
81
|
+
if (list.length === 0)
|
|
82
|
+
throw new Error("mergeCensus needs at least one census");
|
|
83
|
+
if (list.length === 1)
|
|
84
|
+
return list[0];
|
|
85
|
+
const first = list[0];
|
|
86
|
+
const conflicts = [];
|
|
87
|
+
const collisions = [
|
|
88
|
+
...(first.collisions ?? []),
|
|
89
|
+
];
|
|
90
|
+
const declared = { ...asRecord(first.declared) };
|
|
91
|
+
const declaredAlt = { ...asRecord(first.declaredAlt) };
|
|
92
|
+
const keyframes = { ...(first.keyframes ?? {}) };
|
|
93
|
+
const looks = { ...(first.looks ?? {}) };
|
|
94
|
+
let observed = first.observed.map((v) => ({ ...v }));
|
|
95
|
+
let totals = { ...first.totals };
|
|
96
|
+
let coverage = first.coverage;
|
|
97
|
+
let gate = first.gate;
|
|
98
|
+
let reachability = first.reachability;
|
|
99
|
+
let usage = first.usage ?? undefined;
|
|
100
|
+
let animations = first.animations ?? undefined;
|
|
101
|
+
let skipped = first.skipped ?? undefined;
|
|
102
|
+
let brokenRefs = first.brokenRefs ?? undefined;
|
|
103
|
+
let conventions = first.conventions ?? undefined;
|
|
104
|
+
let architectures = first.architectures ?? undefined;
|
|
105
|
+
let components = first.components ?? undefined;
|
|
106
|
+
let defined = first.defined ?? undefined;
|
|
107
|
+
const stack = [...(first.project.stack ?? [])];
|
|
108
|
+
for (let i = 1; i < list.length; i++) {
|
|
109
|
+
const c = list[i];
|
|
110
|
+
const scopeName = nameOf(c, i);
|
|
111
|
+
const firstName = nameOf(first, 0);
|
|
112
|
+
mergeDeclared(declared, asRecord(c.declared), firstName, scopeName, conflicts);
|
|
113
|
+
mergeDeclared(declaredAlt, asRecord(c.declaredAlt), firstName, scopeName, conflicts);
|
|
114
|
+
/**
|
|
115
|
+
* O COMPONENTE QUE OS DOIS DEFINEM fica com a receita do primeiro escopo, e
|
|
116
|
+
* o descarte entra em `collisions` - a mesma lista que `name-claim.ts`
|
|
117
|
+
* alimenta dentro de UM escopo. Uma lista, dois níveis: o arquivo e o
|
|
118
|
+
* pacote, e a tela não precisa saber a diferença para dizer a verdade.
|
|
119
|
+
*/
|
|
120
|
+
for (const [name, look] of Object.entries(c.looks ?? {})) {
|
|
121
|
+
if (looks[name] === undefined) {
|
|
122
|
+
looks[name] = look;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
/** `by` diz que este empate foi resolvido pela ORDEM DOS ESCOPOS. */
|
|
126
|
+
collisions.push({
|
|
127
|
+
name,
|
|
128
|
+
kept: firstName,
|
|
129
|
+
dropped: scopeName,
|
|
130
|
+
by: "scope",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const [name, frames] of Object.entries(c.keyframes ?? {}))
|
|
134
|
+
if (keyframes[name] === undefined)
|
|
135
|
+
keyframes[name] = frames;
|
|
136
|
+
observed = mergeObserved(observed, c.observed);
|
|
137
|
+
totals = {
|
|
138
|
+
scanned: totals.scanned + c.totals.scanned,
|
|
139
|
+
values: totals.values + c.totals.values,
|
|
140
|
+
named: totals.named + c.totals.named,
|
|
141
|
+
tokenUses: totals.tokenUses + c.totals.tokenUses,
|
|
142
|
+
ownUses: (totals.ownUses ?? 0) + (c.totals.ownUses ?? 0),
|
|
143
|
+
phantomUses: (totals.phantomUses ?? 0) + (c.totals.phantomUses ?? 0),
|
|
144
|
+
/** Recalculado abaixo - somar percentuais não significa nada. */
|
|
145
|
+
coverage: 0,
|
|
146
|
+
};
|
|
147
|
+
coverage = sumInto(coverage, c.coverage);
|
|
148
|
+
gate = sumInto(gate, c.gate);
|
|
149
|
+
reachability = sumInto(reachability, c.reachability);
|
|
150
|
+
usage = union(usage, c.usage);
|
|
151
|
+
animations = union(animations, c.animations);
|
|
152
|
+
skipped = concat(skipped, c.skipped);
|
|
153
|
+
brokenRefs = concat(brokenRefs, c.brokenRefs);
|
|
154
|
+
conventions = concat(conventions, c.conventions);
|
|
155
|
+
architectures = concat(architectures, c.architectures);
|
|
156
|
+
components = concat(components, c.components);
|
|
157
|
+
defined = concat(defined, c.defined);
|
|
158
|
+
for (const s of c.project.stack ?? [])
|
|
159
|
+
if (!stack.includes(s))
|
|
160
|
+
stack.push(s);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A COBERTURA, RECALCULADA COM A FÓRMULA DE VERDADE.
|
|
164
|
+
*
|
|
165
|
+
* `(tokenUses + ownUses) / (tokenUses + ownUses + values + phantomUses)` - a
|
|
166
|
+
* mesma de `doctor/scan.ts:796`. A primeira versão desta função usou
|
|
167
|
+
* `named/values`, que parece razoável e não é a fórmula: fundir 48% e 30%
|
|
168
|
+
* devolvia 10% no monorepo do dono. Um número que não descreve nenhuma das
|
|
169
|
+
* medições é pior que não ter o número.
|
|
170
|
+
*
|
|
171
|
+
* Quando os censos vêm de um CLI que não gravava `ownUses`, a soma perde esses
|
|
172
|
+
* termos e o resultado seria menor que a verdade - então neste caso a
|
|
173
|
+
* cobertura do PRIMEIRO escopo é preservada, que é uma medição real de uma
|
|
174
|
+
* parte real, em vez de uma conta feita com metade dos termos.
|
|
175
|
+
*/
|
|
176
|
+
const named = totals.tokenUses + (totals.ownUses ?? 0);
|
|
177
|
+
const denominator = named + totals.values + (totals.phantomUses ?? 0);
|
|
178
|
+
const complete = list.every((c) => c.totals.ownUses !== undefined);
|
|
179
|
+
totals.coverage = !complete
|
|
180
|
+
? first.totals.coverage
|
|
181
|
+
: denominator > 0
|
|
182
|
+
? Math.round((named / denominator) * 100)
|
|
183
|
+
: 100;
|
|
184
|
+
const out = {
|
|
185
|
+
...first,
|
|
186
|
+
project: { ...first.project, stack },
|
|
187
|
+
/**
|
|
188
|
+
* `scope` SEGUE SENDO O PRIMEIRO, e não uma lista, porque há quem compare por
|
|
189
|
+
* igualdade: `live-census.ts:44` faz `census.scope === LIBRARY_SCOPE`, e um
|
|
190
|
+
* array ali seria um falso silencioso. `scopes` é aditivo - quem não sabe
|
|
191
|
+
* dele continua lendo o censo como antes.
|
|
192
|
+
*/
|
|
193
|
+
scopes: list.map((c, i) => nameOf(c, i)),
|
|
194
|
+
declared,
|
|
195
|
+
observed,
|
|
196
|
+
totals,
|
|
197
|
+
};
|
|
198
|
+
if (Object.keys(declaredAlt).length > 0)
|
|
199
|
+
out.declaredAlt = declaredAlt;
|
|
200
|
+
if (Object.keys(keyframes).length > 0)
|
|
201
|
+
out.keyframes = keyframes;
|
|
202
|
+
if (Object.keys(looks).length > 0)
|
|
203
|
+
out.looks = looks;
|
|
204
|
+
if (coverage)
|
|
205
|
+
out.coverage = coverage;
|
|
206
|
+
if (gate)
|
|
207
|
+
out.gate = gate;
|
|
208
|
+
if (reachability)
|
|
209
|
+
out.reachability = reachability;
|
|
210
|
+
if (usage && usage.length > 0)
|
|
211
|
+
out.usage = usage;
|
|
212
|
+
if (animations && animations.length > 0)
|
|
213
|
+
out.animations = animations;
|
|
214
|
+
if (skipped && skipped.length > 0)
|
|
215
|
+
out.skipped = skipped;
|
|
216
|
+
if (brokenRefs && brokenRefs.length > 0)
|
|
217
|
+
out.brokenRefs = brokenRefs;
|
|
218
|
+
if (conventions && conventions.length > 0)
|
|
219
|
+
out.conventions = conventions;
|
|
220
|
+
if (architectures && architectures.length > 0)
|
|
221
|
+
out.architectures = architectures;
|
|
222
|
+
if (components && components.length > 0)
|
|
223
|
+
out.components = components;
|
|
224
|
+
if (defined && defined.length > 0)
|
|
225
|
+
out.defined = defined;
|
|
226
|
+
if (collisions.length > 0)
|
|
227
|
+
out.collisions = collisions;
|
|
228
|
+
if (conflicts.length > 0)
|
|
229
|
+
out.scopeConflicts = conflicts;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
package/dist/name-claim.js
CHANGED
|
@@ -62,10 +62,10 @@ export function claimName(claims, name, file) {
|
|
|
62
62
|
const incumbentWins = depth(held) < depth(file) ||
|
|
63
63
|
(depth(held) === depth(file) && held.localeCompare(file) <= 0);
|
|
64
64
|
if (incumbentWins) {
|
|
65
|
-
claims.collisions.push({ name, kept: held, dropped: file });
|
|
65
|
+
claims.collisions.push({ name, kept: held, dropped: file, by: "path" });
|
|
66
66
|
return false;
|
|
67
67
|
}
|
|
68
68
|
claims.by.set(name, file);
|
|
69
|
-
claims.collisions.push({ name, kept: file, dropped: held });
|
|
69
|
+
claims.collisions.push({ name, kept: file, dropped: held, by: "path" });
|
|
70
70
|
return true;
|
|
71
71
|
}
|
package/dist/workspace-place.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
+
import { SKIP } from "./commands/doctor.js";
|
|
3
4
|
const MANIFEST = "package.json";
|
|
4
5
|
const SHEET = /\.(css|scss|sass|less)$/i;
|
|
5
6
|
const DECL = /(^|[\s{;])--[a-zA-Z0-9-]+\s*:/g;
|
|
@@ -28,9 +29,11 @@ function workspaceGlobs(p) {
|
|
|
28
29
|
* Quantas custom properties um pacote declara - o mesmo peso que
|
|
29
30
|
* `siblingProjects` já usa para escolher entre `packages/core` e `packages/ui`.
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* A LISTA DE PASTAS IGNORADAS É A DO `walk`, importada e não reescrita. A primeira
|
|
33
|
+
* versão pulava dotdir e `node_modules` por conta própria e somava
|
|
34
|
+
* `packages/ui/dist/index.css`: 496 declarações de bundle contra 97 de fonte no
|
|
35
|
+
* repo do dono (19/08). O número que escolhia a pasta recomendada era cinco
|
|
36
|
+
* sextos build output, e o censo - que usa o SKIP - nunca leu nada daquilo.
|
|
34
37
|
*/
|
|
35
38
|
async function weigh(dir) {
|
|
36
39
|
let declarations = 0;
|
|
@@ -39,7 +42,7 @@ async function weigh(dir) {
|
|
|
39
42
|
if (depth > 8)
|
|
40
43
|
return;
|
|
41
44
|
for (const e of await readdir(at, { withFileTypes: true }).catch(() => [])) {
|
|
42
|
-
if (e.name.startsWith(".") || e.name
|
|
45
|
+
if (e.name.startsWith(".") || SKIP.has(e.name))
|
|
43
46
|
continue;
|
|
44
47
|
const full = join(at, e.name);
|
|
45
48
|
if (e.isDirectory()) {
|
|
@@ -125,16 +128,29 @@ export async function placeInWorkspace(root) {
|
|
|
125
128
|
declarations,
|
|
126
129
|
files,
|
|
127
130
|
dependsOn: Object.keys(deps).filter((k) => local.has(k)),
|
|
131
|
+
/** Preenchido no passo seguinte, quando todos os `dependsOn` existem. */
|
|
132
|
+
importedBy: 0,
|
|
128
133
|
});
|
|
129
134
|
}
|
|
135
|
+
for (const pkg of packages) {
|
|
136
|
+
if (!pkg.name)
|
|
137
|
+
continue;
|
|
138
|
+
pkg.importedBy = packages.filter((other) => other !== pkg && other.dependsOn.includes(pkg.name)).length;
|
|
139
|
+
}
|
|
130
140
|
if (packages.length === 0)
|
|
131
141
|
return null;
|
|
132
142
|
const rel = relative(workspaceRoot, root).split("\\").join("/");
|
|
133
|
-
|
|
143
|
+
/**
|
|
144
|
+
* A ORDEM: quem os outros importam vem primeiro, e a contagem só desempata.
|
|
145
|
+
* Um pacote que ninguém importa e que declara muito é um consumidor com
|
|
146
|
+
* vocabulário próprio - é um candidato legítimo, e não é a biblioteca.
|
|
147
|
+
*/
|
|
148
|
+
const ranked = [...packages].sort((a, b) => b.importedBy - a.importedBy || b.declarations - a.declarations);
|
|
149
|
+
const lead = ranked[0];
|
|
134
150
|
return {
|
|
135
151
|
workspaceRoot,
|
|
136
152
|
here: rel === "" ? null : rel,
|
|
137
153
|
packages,
|
|
138
|
-
vocabulary:
|
|
154
|
+
vocabulary: lead && (lead.declarations > 0 || lead.importedBy > 0) ? lead : null,
|
|
139
155
|
};
|
|
140
156
|
}
|
package/package.json
CHANGED