synthesisui 0.16.19 → 0.16.20
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/doctor.js +41 -0
- package/dist/commands/hook.js +13 -0
- package/dist/doctor/ledger.js +105 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { findDivergences } from "../doctor/coherence.js";
|
|
4
4
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
5
|
+
import { appendEvent, readEvents, summarize } from "../doctor/ledger.js";
|
|
5
6
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
6
7
|
import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
|
|
7
8
|
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
@@ -522,6 +523,46 @@ export async function doctor(opts) {
|
|
|
522
523
|
console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
|
|
523
524
|
console.log(body(` ${d.tokenUses} from the system, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`));
|
|
524
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* THE RECORD - the layer that remembers, printed where the moment is shown.
|
|
528
|
+
*
|
|
529
|
+
* A snapshot is appended only for FULL runs: a scoped run measures a corner,
|
|
530
|
+
* and a corner's coverage on the project's trend line would read as the
|
|
531
|
+
* project getting better or worse when somebody just pointed the tool at a
|
|
532
|
+
* different folder.
|
|
533
|
+
*
|
|
534
|
+
* Every line printed here is something no single run can know: whether a
|
|
535
|
+
* finding was acted on (its file checked clean afterwards - the fix is a
|
|
536
|
+
* write, so it logged itself), and which way coverage is moving. That is the
|
|
537
|
+
* question a design-system lead asks before paying for anything, and until
|
|
538
|
+
* today the honest answer was that nobody knew.
|
|
539
|
+
*/
|
|
540
|
+
if (hasSystem && measurable && scopes.length === 0) {
|
|
541
|
+
await appendEvent(root, {
|
|
542
|
+
kind: "doctor",
|
|
543
|
+
at: new Date().toISOString(),
|
|
544
|
+
coverage: d.coverage,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (hasSystem && scopes.length === 0) {
|
|
548
|
+
const record = summarize(await readEvents(root));
|
|
549
|
+
if (record.checks >= 2) {
|
|
550
|
+
console.log("");
|
|
551
|
+
console.log(section("The record"));
|
|
552
|
+
const acted = record.resolved > 0
|
|
553
|
+
? ` ${record.resolved} ${record.resolved === 1 ? "was" : "were"} fixed in a later write.`
|
|
554
|
+
: "";
|
|
555
|
+
console.log(body(`${record.checks} checks logged.${acted}${record.open.length === 0 && record.resolved > 0
|
|
556
|
+
? " Nothing is still open."
|
|
557
|
+
: ""}`));
|
|
558
|
+
for (const o of record.open.slice(0, 5)) {
|
|
559
|
+
console.log(body(` still open: ${o.file} (${o.named} named, ${o.phantoms} phantom)`));
|
|
560
|
+
}
|
|
561
|
+
if (record.coverage && record.coverage.from !== record.coverage.to) {
|
|
562
|
+
console.log(body(` coverage ${record.coverage.from}% → ${record.coverage.to}% since the first run.`));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
525
566
|
/**
|
|
526
567
|
* Its own section, above drift, because it is a worse problem wearing a
|
|
527
568
|
* better disguise.
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { appendEvent } from "../doctor/ledger.js";
|
|
3
4
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
4
5
|
import { loadSystem } from "./doctor.js";
|
|
5
6
|
const pass = () => ({ continue: true });
|
|
@@ -57,6 +58,18 @@ async function report(root, filePath) {
|
|
|
57
58
|
const d = diagnose([scanSource(rel, src, table)]);
|
|
58
59
|
const named = d.findings.filter((f) => f.token);
|
|
59
60
|
const phantoms = d.files.flatMap((f) => f.phantoms ?? []);
|
|
61
|
+
// Every check leaves one line in the ledger - INCLUDING the clean ones,
|
|
62
|
+
// because a fix is itself a write, so the clean re-check of a file that was
|
|
63
|
+
// just flagged is the evidence the agent read the report and acted. Without
|
|
64
|
+
// the clean lines the record could only ever say "found", never "fixed",
|
|
65
|
+
// which is the half we could not answer when asked (28/07).
|
|
66
|
+
await appendEvent(root, {
|
|
67
|
+
kind: "hook",
|
|
68
|
+
at: new Date().toISOString(),
|
|
69
|
+
file: rel,
|
|
70
|
+
named: named.length,
|
|
71
|
+
phantoms: phantoms.length,
|
|
72
|
+
});
|
|
60
73
|
// Unnamed drift alone is deliberately NOT worth interrupting for. There is
|
|
61
74
|
// no token to move to, so the only honest advice is "ask a person" - and
|
|
62
75
|
// saying that after every edit trains the reader to skip the block.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { appendFile, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* THE LAYER THAT REMEMBERS - governance that cannot answer "is it getting
|
|
5
|
+
* better?" is a sequence of moments, not a system.
|
|
6
|
+
*
|
|
7
|
+
* The stack up to here acts and forgets. The hook checks every write and
|
|
8
|
+
* reports into the agent's context, and we had no idea whether the agent acted
|
|
9
|
+
* on it - the owner admitted exactly that, in public, to the person who asked
|
|
10
|
+
* (28/07). The doctor measures a moment and prints it. Nothing anywhere could
|
|
11
|
+
* answer the one question a design-system lead actually asks before paying for
|
|
12
|
+
* a tool: is my team's drift going up or down?
|
|
13
|
+
*
|
|
14
|
+
* THE TRICK THAT MAKES THIS CHEAP. The hook already runs on every write, and a
|
|
15
|
+
* FIX is also a write. So if every check appends one line here, the loop closes
|
|
16
|
+
* by itself: a finding at 10:02 followed by a clean check of the same file at
|
|
17
|
+
* 10:04 IS the evidence the agent read the report and acted. Nobody logs
|
|
18
|
+
* "fixed"; the fix logs itself.
|
|
19
|
+
*
|
|
20
|
+
* LOCAL BY DESIGN. One JSONL per project, inside `_synthesisui/`, next to the
|
|
21
|
+
* system it describes. Nothing leaves the machine - "we send telemetry to our
|
|
22
|
+
* server" is how a tool that installs an editor hook loses the room. Committing
|
|
23
|
+
* it is the project's choice: do, and the ledger becomes the team's shared
|
|
24
|
+
* memory; don't, and it is a personal instrument. Both are legitimate.
|
|
25
|
+
*
|
|
26
|
+
* Append-only, one JSON object per line, unknown lines skipped on read - the
|
|
27
|
+
* same contract every log file that survives a decade has.
|
|
28
|
+
*/
|
|
29
|
+
export const LEDGER_FILE = "ledger.jsonl";
|
|
30
|
+
const ledgerPath = (root) => join(root, "_synthesisui", LEDGER_FILE);
|
|
31
|
+
/** Keep the file from growing forever: past ~1MB, keep the newest half. A
|
|
32
|
+
* trend needs recent history, not an archive. */
|
|
33
|
+
const MAX_BYTES = 1_000_000;
|
|
34
|
+
export async function appendEvent(root, event) {
|
|
35
|
+
const path = ledgerPath(root);
|
|
36
|
+
try {
|
|
37
|
+
await appendFile(path, `${JSON.stringify(event)}\n`, "utf8");
|
|
38
|
+
const size = (await stat(path)).size;
|
|
39
|
+
if (size > MAX_BYTES) {
|
|
40
|
+
const lines = (await readFile(path, "utf8")).split("\n").filter(Boolean);
|
|
41
|
+
await writeFile(path, `${lines.slice(Math.floor(lines.length / 2)).join("\n")}\n`, "utf8");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// The record must never cost the check. A ledger that can break the hook
|
|
46
|
+
// is a ledger that gets the hook uninstalled.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function readEvents(root) {
|
|
50
|
+
const raw = await readFile(ledgerPath(root), "utf8").catch(() => "");
|
|
51
|
+
if (!raw)
|
|
52
|
+
return [];
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const line of raw.split("\n")) {
|
|
55
|
+
if (!line.trim())
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
const e = JSON.parse(line);
|
|
59
|
+
if (e && (e.kind === "hook" || e.kind === "doctor") && e.at)
|
|
60
|
+
out.push(e);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A corrupt line loses itself, not the file.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export function summarize(events) {
|
|
69
|
+
const hooks = events.filter((e) => e.kind === "hook" && e.file);
|
|
70
|
+
const byFile = new Map();
|
|
71
|
+
for (const e of hooks) {
|
|
72
|
+
const key = e.file;
|
|
73
|
+
byFile.set(key, [...(byFile.get(key) ?? []), e]);
|
|
74
|
+
}
|
|
75
|
+
let resolved = 0;
|
|
76
|
+
const open = [];
|
|
77
|
+
for (const [file, list] of byFile) {
|
|
78
|
+
let dirty = false;
|
|
79
|
+
for (const e of list) {
|
|
80
|
+
const bad = (e.named ?? 0) + (e.phantoms ?? 0) > 0;
|
|
81
|
+
if (bad && !dirty)
|
|
82
|
+
dirty = true;
|
|
83
|
+
else if (!bad && dirty) {
|
|
84
|
+
dirty = false;
|
|
85
|
+
resolved++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (dirty) {
|
|
89
|
+
const last = list[list.length - 1];
|
|
90
|
+
open.push({
|
|
91
|
+
file,
|
|
92
|
+
named: last.named ?? 0,
|
|
93
|
+
phantoms: last.phantoms ?? 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const snaps = events.filter((e) => e.kind === "doctor" && typeof e.coverage === "number");
|
|
98
|
+
const coverage = snaps.length >= 2
|
|
99
|
+
? {
|
|
100
|
+
from: snaps[0].coverage,
|
|
101
|
+
to: snaps[snaps.length - 1].coverage,
|
|
102
|
+
}
|
|
103
|
+
: null;
|
|
104
|
+
return { checks: hooks.length, resolved, open, coverage };
|
|
105
|
+
}
|
package/package.json
CHANGED