synthesisui 0.16.195 → 0.16.197
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 +2 -0
- package/dist/commands/import.js +72 -52
- package/dist/commands/sync.js +40 -2
- package/dist/index.js +2 -0
- package/dist/last-sync.js +70 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -33,6 +33,8 @@ const IGNORED = [
|
|
|
33
33
|
"census.json",
|
|
34
34
|
"ledger.jsonl",
|
|
35
35
|
".ledger-sent",
|
|
36
|
+
/** O que a última medição viu, para a próxima dizer o que mudou - descreve ESTE clone. */
|
|
37
|
+
".last-sync.json",
|
|
36
38
|
"not-expressed.md",
|
|
37
39
|
];
|
|
38
40
|
const IGNORE_HEADER = "# Managed by synthesisui. The identity and the CSS are committed so a fresh\n" +
|
package/dist/commands/import.js
CHANGED
|
@@ -329,6 +329,19 @@ function screenOf(rel) {
|
|
|
329
329
|
return `${parts[i]}/${after.slice(0, 2).join("/")}`;
|
|
330
330
|
}
|
|
331
331
|
export async function takeCensus(root, opts) {
|
|
332
|
+
/**
|
|
333
|
+
* O RELATÓRIO SÓ SAI QUANDO ALGUÉM O PEDIU - `say` cala quando o chamador é uma RE-medição.
|
|
334
|
+
*
|
|
335
|
+
* As 108 linhas deste relatório são o que faz um primeiro import ganhar confiança: elas mostram o
|
|
336
|
+
* que foi lido, o que não foi, e por quê. Numa re-medição elas são as MESMAS 108 linhas, e o dono
|
|
337
|
+
* rodou `sync` seis vezes num dia recebendo o mesmo texto (dono, 09/08). Um relatório repetido não
|
|
338
|
+
* informa - ele treina a pessoa a rolar até o fim sem ler.
|
|
339
|
+
*
|
|
340
|
+
* Então `import` continua contando tudo, e `sync` conta o que MUDOU, com este relatório a um
|
|
341
|
+
* comando de distância (`sync --full`). Silêncio não é o mesmo que esconder: nenhum número deixa de
|
|
342
|
+
* existir, e a lacuna continua alcançável por quem quiser ver.
|
|
343
|
+
*/
|
|
344
|
+
const say = opts?.quiet ? () => { } : (line) => console.log(line);
|
|
332
345
|
const scopeLabel = opts?.scopeLabel;
|
|
333
346
|
/**
|
|
334
347
|
* THE LIVE CATALOGUE, once, before anything is matched.
|
|
@@ -1203,11 +1216,11 @@ export async function takeCensus(root, opts) {
|
|
|
1203
1216
|
{ bucket: r.bucket, canonical: r.canonical, because: r.because },
|
|
1204
1217
|
]));
|
|
1205
1218
|
if (usedThere.length > 0) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1219
|
+
say("");
|
|
1220
|
+
say(body(`Read ${usageSeen.size} more file${usageSeen.size === 1 ? "" : "s"} for EVIDENCE only - ${(opts?.usage ?? []).map((u) => paint.strong(u.label)).join(", ")}. What comes from there is how often each component is used and which values get picked, which is what turns a settled choice into a law. The palette, the scales and the convention stay from ${paint.strong(scopeLabel ?? "the system")}: an app's average is not a scale, it is a set of choices made from one.`));
|
|
1208
1221
|
if (theirsOnly.length > 0) {
|
|
1209
1222
|
const shown = theirsOnly.slice(0, 6).map((c) => c.name);
|
|
1210
|
-
|
|
1223
|
+
say(body(paint.faint(`(${theirsOnly.length} component${theirsOnly.length === 1 ? "" : "s"} live there and not in the system - ${shown.join(", ")}${theirsOnly.length > shown.length ? ", …" : ""}. Left out on purpose: the system is one place, and a union of two codebases is not a system. Point --scope at them if they belong in it.)`)));
|
|
1211
1224
|
}
|
|
1212
1225
|
}
|
|
1213
1226
|
/**
|
|
@@ -1260,11 +1273,11 @@ export async function takeCensus(root, opts) {
|
|
|
1260
1273
|
*/
|
|
1261
1274
|
const signalLines = describeSignals(signals);
|
|
1262
1275
|
if (signalLines.length > 0) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1276
|
+
say("");
|
|
1277
|
+
say(section("What was countable, counted"));
|
|
1265
1278
|
for (const line of signalLines)
|
|
1266
|
-
|
|
1267
|
-
|
|
1279
|
+
say(body(line));
|
|
1280
|
+
say(body(paint.faint("Do not go looking for any of this - it is measured, and a sweep would bring back a different slice than the next one.")));
|
|
1268
1281
|
}
|
|
1269
1282
|
/**
|
|
1270
1283
|
* WHAT THE READER UNDERSTOOD AND REFUSED TO PLACE.
|
|
@@ -1281,14 +1294,14 @@ export async function takeCensus(root, opts) {
|
|
|
1281
1294
|
}));
|
|
1282
1295
|
if (unplacedByComponent.length > 0) {
|
|
1283
1296
|
const total = unplacedByComponent.reduce((n, u) => n + u.lines.length, 0);
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1297
|
+
say("");
|
|
1298
|
+
say(section("Conditional looks that were read and not placed"));
|
|
1299
|
+
say(body(`${total} decision${total === 1 ? "" : "s"} across ${unplacedByComponent.length} component${unplacedByComponent.length === 1 ? "" : "s"} were understood and left off, because putting them on the component would paint the wrong element:`));
|
|
1287
1300
|
for (const u of unplacedByComponent.slice(0, 8)) {
|
|
1288
|
-
|
|
1301
|
+
say(body(paint.faint(` ${u.component}: ${u.lines[0]}`)));
|
|
1289
1302
|
}
|
|
1290
1303
|
if (unplacedByComponent.length > 8) {
|
|
1291
|
-
|
|
1304
|
+
say(body(paint.faint(` and ${unplacedByComponent.length - 8} more components`)));
|
|
1292
1305
|
}
|
|
1293
1306
|
}
|
|
1294
1307
|
/**
|
|
@@ -1307,13 +1320,13 @@ export async function takeCensus(root, opts) {
|
|
|
1307
1320
|
}));
|
|
1308
1321
|
if (notesByComponent.length > 0) {
|
|
1309
1322
|
const total = notesByComponent.reduce((n, u) => n + u.lines.length, 0);
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1323
|
+
say("");
|
|
1324
|
+
say(section("Looks that are in the recipe and need an attribute"));
|
|
1325
|
+
say(body(`${total} decision${total === 1 ? "" : "s"} across ${notesByComponent.length} component${notesByComponent.length === 1 ? "" : "s"} were read and KEPT - they apply once the element carries the attribute the line names:`));
|
|
1313
1326
|
for (const u of notesByComponent.slice(0, 6))
|
|
1314
|
-
|
|
1327
|
+
say(body(paint.faint(` ${u.component}: ${u.lines[0]}`)));
|
|
1315
1328
|
if (notesByComponent.length > 6)
|
|
1316
|
-
|
|
1329
|
+
say(body(paint.faint(` and ${notesByComponent.length - 6} more components`)));
|
|
1317
1330
|
}
|
|
1318
1331
|
/**
|
|
1319
1332
|
* THE DROPS, SAID OUT LOUD. `p-spacing-sm` on a real menu popup read as nothing and
|
|
@@ -1330,14 +1343,14 @@ export async function takeCensus(root, opts) {
|
|
|
1330
1343
|
}));
|
|
1331
1344
|
if (unreadableByComponent.length > 0) {
|
|
1332
1345
|
const total = unreadableByComponent.reduce((n, u) => n + u.classes.length, 0);
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1346
|
+
say("");
|
|
1347
|
+
say(section("Design classes that read as nothing"));
|
|
1348
|
+
say(body(`${total} class${total === 1 ? "" : "es"} start like a design decision and resolve to nothing - usually a typo in the source, and the element renders without that decision today. Nothing was guessed on their behalf:`));
|
|
1336
1349
|
for (const u of unreadableByComponent.slice(0, 8)) {
|
|
1337
|
-
|
|
1350
|
+
say(body(paint.faint(` ${u.component}: ${u.classes.slice(0, 4).join(", ")}`)));
|
|
1338
1351
|
}
|
|
1339
1352
|
if (unreadableByComponent.length > 8) {
|
|
1340
|
-
|
|
1353
|
+
say(body(paint.faint(` and ${unreadableByComponent.length - 8} more components`)));
|
|
1341
1354
|
}
|
|
1342
1355
|
}
|
|
1343
1356
|
/**
|
|
@@ -1363,10 +1376,10 @@ export async function takeCensus(root, opts) {
|
|
|
1363
1376
|
const ledger = buildLedger(opts?.cli ?? "unknown", judgeFragments([...fragments.flat(), ...styleFiles.flatMap((s) => s.fragments)], declaredValues, admittedStyles));
|
|
1364
1377
|
const coverageLines = describeCoverage(coverage, composition);
|
|
1365
1378
|
if (coverageLines.length > 0) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1379
|
+
say("");
|
|
1380
|
+
say(section("What this version reads, on your files"));
|
|
1368
1381
|
for (const line of coverageLines)
|
|
1369
|
-
|
|
1382
|
+
say(body(line));
|
|
1370
1383
|
}
|
|
1371
1384
|
/**
|
|
1372
1385
|
* O QUE VOTA NAS LEIS DELE SEM ESTAR VIVO - dito, e nada excluído.
|
|
@@ -1377,12 +1390,12 @@ export async function takeCensus(root, opts) {
|
|
|
1377
1390
|
*/
|
|
1378
1391
|
const reachLines = reachability ? describeReachability(reachability) : [];
|
|
1379
1392
|
if (reachLines.length > 0) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1393
|
+
say("");
|
|
1394
|
+
say(section("What no route reaches"));
|
|
1382
1395
|
for (const line of reachLines)
|
|
1383
|
-
|
|
1396
|
+
say(body(line));
|
|
1384
1397
|
if (reachability && reachability.unreached.length > 0)
|
|
1385
|
-
|
|
1398
|
+
say(body(paint.faint(`${reachability.unreached
|
|
1386
1399
|
.slice(0, 4)
|
|
1387
1400
|
.map((f) => relative(root, f))
|
|
1388
1401
|
.join(", ")}${reachability.unreached.length > 4 ? `, and ${reachability.unreached.length - 4} more` : ""} - the full list travels in the census, and nothing was left out because of it`)));
|
|
@@ -1405,42 +1418,42 @@ export async function takeCensus(root, opts) {
|
|
|
1405
1418
|
*/
|
|
1406
1419
|
const ledgerLines = describeLedger(ledger);
|
|
1407
1420
|
if (ledgerLines.length > 0) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1421
|
+
say("");
|
|
1422
|
+
say(section("Every style fragment, accounted for"));
|
|
1410
1423
|
for (const line of ledgerLines.slice(0, 24))
|
|
1411
|
-
|
|
1424
|
+
say(body(line));
|
|
1412
1425
|
}
|
|
1413
1426
|
const pageRules = pageLaws(pages);
|
|
1414
1427
|
if (pageRules.length > 0) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1428
|
+
say("");
|
|
1429
|
+
say(section("What your pages agree on"));
|
|
1417
1430
|
for (const law of pageRules) {
|
|
1418
|
-
|
|
1419
|
-
|
|
1431
|
+
say(body(law.text));
|
|
1432
|
+
say(body(paint.faint(` ${law.evidence}`)));
|
|
1420
1433
|
}
|
|
1421
1434
|
}
|
|
1422
1435
|
if (architectures.length > 0) {
|
|
1423
|
-
|
|
1424
|
-
|
|
1436
|
+
say("");
|
|
1437
|
+
say(section("How this project is organised"));
|
|
1425
1438
|
const choice = describeChoice(architectures);
|
|
1426
1439
|
if (choice)
|
|
1427
|
-
|
|
1440
|
+
say(body(choice));
|
|
1428
1441
|
for (const a of architectures.slice(0, 4)) {
|
|
1429
|
-
|
|
1442
|
+
say(body(paint.faint(` ${describeArchitecture(a)}`)));
|
|
1430
1443
|
}
|
|
1431
1444
|
}
|
|
1432
1445
|
if (skips.length > 0) {
|
|
1433
|
-
|
|
1434
|
-
|
|
1446
|
+
say("");
|
|
1447
|
+
say(body(describeGate(scoped.length, skips.length)));
|
|
1435
1448
|
for (const group of groupSkips(skips)) {
|
|
1436
1449
|
const shown = group.names.slice(0, 5);
|
|
1437
1450
|
const more = group.names.length - shown.length;
|
|
1438
|
-
|
|
1451
|
+
say(body(paint.faint(` ${group.names.length} ${group.label}: ${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}`)));
|
|
1439
1452
|
}
|
|
1440
1453
|
}
|
|
1441
1454
|
if (library) {
|
|
1442
|
-
|
|
1443
|
-
|
|
1455
|
+
say("");
|
|
1456
|
+
say(body(`This scope DECLARES its components and composes almost none of them inside itself - which is what a component library looks like from the inside. So every component it exports arrives as YOURS, with its own recipe. Where one reads like something in our catalogue, that is recorded as a note instead of replacing it.`));
|
|
1444
1457
|
}
|
|
1445
1458
|
/**
|
|
1446
1459
|
* THE LAWS READ THE MERGED LIST, not the scope's.
|
|
@@ -2362,7 +2375,14 @@ function rootBehaviour(tag, sketch) {
|
|
|
2362
2375
|
? { rootProps: first.behaviourProps }
|
|
2363
2376
|
: {};
|
|
2364
2377
|
}
|
|
2365
|
-
export async function resolveReadParts(census, root
|
|
2378
|
+
export async function resolveReadParts(census, root,
|
|
2379
|
+
/** `true` numa re-medição: o resumo desta etapa fica para o `sync --full` - ver `say`. */
|
|
2380
|
+
quiet = false) {
|
|
2381
|
+
const say = quiet
|
|
2382
|
+
? () => { }
|
|
2383
|
+
: (line) => {
|
|
2384
|
+
console.log(line);
|
|
2385
|
+
};
|
|
2366
2386
|
/**
|
|
2367
2387
|
* A READING IS OPTIONAL HERE. THE SKETCH IS NOT.
|
|
2368
2388
|
*
|
|
@@ -2663,27 +2683,27 @@ export async function resolveReadParts(census, root) {
|
|
|
2663
2683
|
const authored = Object.keys(read).length;
|
|
2664
2684
|
const total = authored || Object.keys(census.looks ?? {}).length;
|
|
2665
2685
|
if (named > 0 || shaped > 0) {
|
|
2666
|
-
|
|
2667
|
-
|
|
2686
|
+
say("");
|
|
2687
|
+
say(body(
|
|
2668
2688
|
// "Your reading" only when there WAS one - otherwise the names came off
|
|
2669
2689
|
// their own markup, and claiming otherwise credits somebody's absent work.
|
|
2670
2690
|
`${named} part${named === 1 ? "" : "s"} named across ${total} component${total === 1 ? "" : "s"}${authored > 0 ? " - your reading and their markup" : " - read off their own markup"}, so each previews as what it IS rather than as a text box`));
|
|
2671
2691
|
}
|
|
2672
2692
|
if (shaped > 0) {
|
|
2673
|
-
|
|
2693
|
+
say(body(`${shaped} of them arrived with a SHAPE - the parts nested as they nest in your code, so the preview is the component and not a row of labels`));
|
|
2674
2694
|
}
|
|
2675
2695
|
// A frontier is worth its own line, because it is the half nobody else has:
|
|
2676
2696
|
// the edge is a rule an agent can obey, not a picture.
|
|
2677
2697
|
if (edges > 0) {
|
|
2678
|
-
|
|
2698
|
+
say(body(`${edges} place${edges === 1 ? "" : "s"} where one of your components is built out of another - ${edges === 1 ? "it becomes a rule" : "each becomes a rule"} about the two of them, not a guess`));
|
|
2679
2699
|
}
|
|
2680
2700
|
if (libs.size > 0) {
|
|
2681
2701
|
const named_ = [...libs].slice(0, 3).join(", ");
|
|
2682
|
-
|
|
2702
|
+
say(body(`${libs.size} librar${libs.size === 1 ? "y" : "ies"} your components need (${named_}) - ${libs.size === 1 ? "it does" : "they do"} not render here, and what to know about ${libs.size === 1 ? "it" : "them"} travels as rules`));
|
|
2683
2703
|
}
|
|
2684
2704
|
// NO SILENT FIX. A correction made quietly is a lie about what they wrote.
|
|
2685
2705
|
for (const n of notes)
|
|
2686
|
-
|
|
2706
|
+
say(body(n));
|
|
2687
2707
|
}
|
|
2688
2708
|
export async function runImport(opts) {
|
|
2689
2709
|
const root = opts.root ?? process.cwd();
|
package/dist/commands/sync.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isOlderCli } from "../cli-version.js";
|
|
|
4
4
|
import { readToken, resolveRegistry } from "../config.js";
|
|
5
5
|
import { markSent, readEvents } from "../doctor/ledger.js";
|
|
6
6
|
import { checkableName, closeRequest, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
|
|
7
|
+
import { describeDelta, readSyncMark, writeSyncMark, } from "../last-sync.js";
|
|
7
8
|
import { measuredScope, rememberScope } from "../measured-scope.js";
|
|
8
9
|
import { body, paint, section, snippet } from "../output.js";
|
|
9
10
|
import { repoStateOf } from "../repo-state.js";
|
|
@@ -74,7 +75,14 @@ export async function sync(opts) {
|
|
|
74
75
|
* ATUAL, funde a leitura no censo fresco, AVISA o que seria reescrito, e envia. Sem entrevista.
|
|
75
76
|
*/
|
|
76
77
|
if (!opts.recordOnly)
|
|
77
|
-
await remeasure({
|
|
78
|
+
await remeasure({
|
|
79
|
+
root,
|
|
80
|
+
base,
|
|
81
|
+
token,
|
|
82
|
+
slug,
|
|
83
|
+
cli: opts.cli,
|
|
84
|
+
...(opts.full ? { full: true } : {}),
|
|
85
|
+
});
|
|
78
86
|
// Verification first, so the snapshot never reports "open" about something
|
|
79
87
|
// the installed css already delivers - a request closes because a machine
|
|
80
88
|
// CHECKED, not because someone remembered to say so (dono, 29/07).
|
|
@@ -252,6 +260,8 @@ export async function remeasure(args) {
|
|
|
252
260
|
const census = await takeCensus(scope ? join(root, scope) : root, {
|
|
253
261
|
...(scope ? { scopeLabel: scope } : {}),
|
|
254
262
|
...(args.cli ? { cli: args.cli } : {}),
|
|
263
|
+
/** Uma RE-medição conta o que MUDOU; o relatório inteiro fica no `--full` - ver `say`. */
|
|
264
|
+
...(args.full ? {} : { quiet: true }),
|
|
255
265
|
/** O `--usage` é EVIDÊNCIA e leva rótulo: sem ele o censo não sabe de que app o número veio. */
|
|
256
266
|
usage: usage.map((u) => ({
|
|
257
267
|
path: join(root, u),
|
|
@@ -265,7 +275,7 @@ export async function remeasure(args) {
|
|
|
265
275
|
/** A LEITURA GRAVADA ENTRA NO CENSO FRESCO - ver a rota `reading`. */
|
|
266
276
|
if (stored.reading)
|
|
267
277
|
census.reading = stored.reading;
|
|
268
|
-
await resolveReadParts(census, root).catch(() => { });
|
|
278
|
+
await resolveReadParts(census, root, !args.full).catch(() => { });
|
|
269
279
|
/**
|
|
270
280
|
* O CENSO FRESCO TAMBÉM FICA NO DISCO.
|
|
271
281
|
*
|
|
@@ -330,6 +340,34 @@ export async function remeasure(args) {
|
|
|
330
340
|
console.log(body(paint.faint(note)));
|
|
331
341
|
if (out.url)
|
|
332
342
|
console.log(body(out.url));
|
|
343
|
+
/**
|
|
344
|
+
* O QUE MUDOU DESDE A ÚLTIMA VEZ - a linha que só uma RE-medição pode dar.
|
|
345
|
+
*
|
|
346
|
+
* O relatório longo responde "o que a esteira lê"; ele responde igual toda vez. Esta responde "o
|
|
347
|
+
* que mudou", que é a pergunta de quem já rodou isso antes - e era a única sem resposta em seis
|
|
348
|
+
* execuções seguidas (dono, 09/08).
|
|
349
|
+
*/
|
|
350
|
+
const led = census.ledger;
|
|
351
|
+
const sum = (r) => Object.values(r ?? {}).reduce((n, v) => n + v, 0);
|
|
352
|
+
const mark = {
|
|
353
|
+
at: new Date().toISOString(),
|
|
354
|
+
components: out.written ?? 0,
|
|
355
|
+
/**
|
|
356
|
+
* LIDO DO CENSO, e não recontado aqui - ver `coverage.declarations`.
|
|
357
|
+
*
|
|
358
|
+
* A primeira versão contava por conta própria e deu 165 onde o relatório diz 519: ela somava só o
|
|
359
|
+
* `base` de cada look, e a conta do produto soma `base`, `dark` e os estados. Dois números sobre
|
|
360
|
+
* um fato só é como uma tela passa a se contradizer, e a única defesa é não ter o segundo.
|
|
361
|
+
*/
|
|
362
|
+
declarations: census.coverage
|
|
363
|
+
?.declarations ?? 0,
|
|
364
|
+
seen: sum(led?.counted),
|
|
365
|
+
interpreted: sum(led?.interpreted),
|
|
366
|
+
};
|
|
367
|
+
console.log(body(paint.faint(describeDelta(mark, await readSyncMark(root)))));
|
|
368
|
+
await writeSyncMark(root, mark);
|
|
369
|
+
if (!args.full)
|
|
370
|
+
console.log(body(paint.faint("everything the reader saw: npx synthesisui sync --full")));
|
|
333
371
|
}
|
|
334
372
|
/**
|
|
335
373
|
* Pergunta uma vez, e só num terminal de verdade.
|
package/dist/index.js
CHANGED
|
@@ -550,6 +550,8 @@ async function main() {
|
|
|
550
550
|
cli: CLI_VERSION,
|
|
551
551
|
recordOnly: args.includes("--record-only"),
|
|
552
552
|
yes: args.includes("--yes"),
|
|
553
|
+
/** `--full`: o relatório inteiro do leitor. Por padrão a re-medição conta o que MUDOU. */
|
|
554
|
+
full: args.includes("--full") || flags.full === true,
|
|
553
555
|
});
|
|
554
556
|
break;
|
|
555
557
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* O QUE MUDOU DESDE A ÚLTIMA VEZ - porque a segunda leitura não é a primeira.
|
|
5
|
+
*
|
|
6
|
+
* Um primeiro import precisa das 108 linhas do relatório: é ali que a esteira mostra o que leu, o que
|
|
7
|
+
* não leu, e por quê, e é assim que ela ganha confiança. Uma RE-medição precisa de outra coisa - o
|
|
8
|
+
* dono rodou `sync` seis vezes num dia e recebeu o mesmo texto nas seis (dono, 09/08). Um relatório
|
|
9
|
+
* repetido não informa; ele treina a pessoa a rolar até o fim sem ler.
|
|
10
|
+
*
|
|
11
|
+
* Meia dúzia de números guardados ao lado do registro, no mesmo padrão do `.ledger-sent`: local,
|
|
12
|
+
* pequeno, e nunca uma segunda fonte de verdade. Se ele sumir, a próxima leitura simplesmente não tem
|
|
13
|
+
* "antes" - e diz isso, em vez de inventar um delta.
|
|
14
|
+
*/
|
|
15
|
+
export const LAST_SYNC_FILE = ".last-sync.json";
|
|
16
|
+
const path = (root) => join(root, "_synthesisui", LAST_SYNC_FILE);
|
|
17
|
+
export async function readSyncMark(root) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = JSON.parse(await readFile(path(root), "utf8"));
|
|
20
|
+
return typeof raw?.at === "string" ? raw : null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Nunca derruba o comando: um carimbo que falha custa um delta, não a medição. */
|
|
27
|
+
export async function writeSyncMark(root, mark) {
|
|
28
|
+
await writeFile(path(root), `${JSON.stringify(mark, null, 2)}\n`, "utf8").catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A LINHA DO DELTA, ou o silêncio honesto.
|
|
32
|
+
*
|
|
33
|
+
* Sem "antes" ela diz que é a primeira - inventar um delta contra zero faria toda primeira medição
|
|
34
|
+
* parecer um salto enorme. E quando nada mudou ela DIZ que nada mudou: é a resposta mais útil que uma
|
|
35
|
+
* re-medição pode dar, e é a que o relatório longo nunca conseguiu dar em seis execuções.
|
|
36
|
+
*/
|
|
37
|
+
export function describeDelta(now, before) {
|
|
38
|
+
if (!before)
|
|
39
|
+
return "first measurement recorded here - the next one compares against it";
|
|
40
|
+
const parts = [];
|
|
41
|
+
const moved = (label, from, to) => {
|
|
42
|
+
if (from === to)
|
|
43
|
+
return;
|
|
44
|
+
parts.push(`${label} ${from} → ${to}`);
|
|
45
|
+
};
|
|
46
|
+
moved("components", before.components, now.components);
|
|
47
|
+
moved("declarations", before.declarations, now.declarations);
|
|
48
|
+
moved("fragments read", before.interpreted, now.interpreted);
|
|
49
|
+
moved("fragments seen", before.seen, now.seen);
|
|
50
|
+
const when = ago(before.at);
|
|
51
|
+
return parts.length === 0
|
|
52
|
+
? `nothing moved since your last sync${when ? `, ${when}` : ""}`
|
|
53
|
+
: `${parts.join(" · ")} — since your last sync${when ? `, ${when}` : ""}`;
|
|
54
|
+
}
|
|
55
|
+
/** Quanto tempo faz, em palavras. Um carimbo ilegível não vira frase. */
|
|
56
|
+
function ago(at) {
|
|
57
|
+
const then = Date.parse(at);
|
|
58
|
+
if (!Number.isFinite(then))
|
|
59
|
+
return "";
|
|
60
|
+
const mins = Math.round((Date.now() - then) / 60_000);
|
|
61
|
+
if (mins < 1)
|
|
62
|
+
return "moments ago";
|
|
63
|
+
if (mins < 60)
|
|
64
|
+
return `${mins} minute${mins === 1 ? "" : "s"} ago`;
|
|
65
|
+
const hours = Math.round(mins / 60);
|
|
66
|
+
if (hours < 24)
|
|
67
|
+
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
68
|
+
const days = Math.round(hours / 24);
|
|
69
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
70
|
+
}
|
package/package.json
CHANGED