visualproof 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3235 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +741 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1394 -0
- package/dist/server/mcptools.js +497 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +156 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +49 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +157 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +148 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +107 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +243 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +95 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +810 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +280 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +298 -0
- package/dist/shared/viewgraph.js +139 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +48733 -0
- package/dist/web/bundle/edges.js +409 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +239 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +787 -0
- package/dist/web/style.css +2312 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +124 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// `visualproof doctor [<lean-project>]`: what this machine, and a project, need for VisualProof to
|
|
2
|
+
// run, each checked, with what to do where it falls short. It changes nothing: it runs no Lake (in a
|
|
3
|
+
// project's folder that would install its toolchain, and Lake may build), and makes no folder.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { projectDirs, siteHolder } from "./dirs.js";
|
|
8
|
+
import { ProjectError, openProject, targetsInWords } from "./project.js";
|
|
9
|
+
export const run = (cmd, args) => {
|
|
10
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
11
|
+
return r.error || r.status !== 0 ? null : `${r.stdout}${r.stderr}`;
|
|
12
|
+
};
|
|
13
|
+
/** The package's own file, read where both a checkout and the installed package have it. */
|
|
14
|
+
function packageFile(rel) {
|
|
15
|
+
return join(import.meta.dirname, "..", "..", rel);
|
|
16
|
+
}
|
|
17
|
+
export function packageVersion() {
|
|
18
|
+
return JSON.parse(readFileSync(packageFile("package.json"), "utf8")).version;
|
|
19
|
+
}
|
|
20
|
+
function parseVersion(v) {
|
|
21
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
22
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
23
|
+
}
|
|
24
|
+
function cmp(a, b) {
|
|
25
|
+
for (let i = 0; i < 3; i++)
|
|
26
|
+
if (a[i] !== b[i])
|
|
27
|
+
return a[i] - b[i];
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
/** Whether a version meets a range of the forms package.json's `engines` uses: alternatives
|
|
31
|
+
* joined by `||`, each of `^x.y.z`, `>=x.y.z`, `<x.y.z`, joined by spaces. */
|
|
32
|
+
export function satisfies(version, range) {
|
|
33
|
+
const v = parseVersion(version);
|
|
34
|
+
if (!v)
|
|
35
|
+
return false;
|
|
36
|
+
return range.split("||").some((alt) => alt.trim().split(/\s+/).every((part) => {
|
|
37
|
+
const m = /^(\^|>=|<|)(\d+\.\d+\.\d+)$/.exec(part);
|
|
38
|
+
if (!m)
|
|
39
|
+
return false;
|
|
40
|
+
const w = parseVersion(m[2]);
|
|
41
|
+
if (m[1] === "^")
|
|
42
|
+
return v[0] === w[0] && cmp(v, w) >= 0;
|
|
43
|
+
if (m[1] === ">=")
|
|
44
|
+
return cmp(v, w) >= 0;
|
|
45
|
+
if (m[1] === "<")
|
|
46
|
+
return cmp(v, w) < 0;
|
|
47
|
+
return cmp(v, w) === 0;
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/** A toolchain as elan names it: `leanprover/lean4:v4.33.1`, whether the project writes it so, or
|
|
51
|
+
* as a bare version, or elan lists it as its folder's name (`leanprover--lean4---v4.33.1`). */
|
|
52
|
+
export function toolchainName(t) {
|
|
53
|
+
const s = t.trim().replace(/\s+\(.*\)$/, "");
|
|
54
|
+
if (/^[\w.-]+--[\w.-]+---/.test(s))
|
|
55
|
+
return s.replace("---", ":").replace("--", "/");
|
|
56
|
+
return s.includes(":") ? s : `leanprover/lean4:${s.startsWith("v") ? s : `v${s}`}`;
|
|
57
|
+
}
|
|
58
|
+
/** The Lean versions supported (test/toolchains.json): the oldest, and the newest tested. */
|
|
59
|
+
export function supportedToolchains() {
|
|
60
|
+
const t = JSON.parse(readFileSync(packageFile("test/toolchains.json"), "utf8"));
|
|
61
|
+
const newest = [...t.toolchains].sort((a, b) => cmp(parseVersion(a), parseVersion(b))).at(-1);
|
|
62
|
+
return { floor: t.floor, newest };
|
|
63
|
+
}
|
|
64
|
+
/** Where a toolchain stands: before the oldest supported, within what is tested, or after it. A
|
|
65
|
+
* release candidate counts as its release. */
|
|
66
|
+
export function toolchainSupport(toolchain, s = supportedToolchains()) {
|
|
67
|
+
const name = toolchainName(toolchain);
|
|
68
|
+
if (!name.startsWith("leanprover/lean4:"))
|
|
69
|
+
return "other";
|
|
70
|
+
const v = parseVersion(name);
|
|
71
|
+
if (!v)
|
|
72
|
+
return "other";
|
|
73
|
+
if (cmp(v, parseVersion(s.floor)) < 0)
|
|
74
|
+
return "old";
|
|
75
|
+
return cmp(v, parseVersion(s.newest)) > 0 ? "new" : "tested";
|
|
76
|
+
}
|
|
77
|
+
/** Whether a folder could be made and written here: it, or the nearest folder above it that is. */
|
|
78
|
+
function writable(dir) {
|
|
79
|
+
let d = resolve(dir);
|
|
80
|
+
while (!existsSync(d) && dirname(d) !== d)
|
|
81
|
+
d = dirname(d);
|
|
82
|
+
try {
|
|
83
|
+
accessSync(d, constants.W_OK);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function install(what, platform) {
|
|
91
|
+
if (what === "poppler")
|
|
92
|
+
return platform === "darwin" ? "brew install poppler" : "install poppler-utils (Debian, Ubuntu: sudo apt install poppler-utils)";
|
|
93
|
+
return platform === "darwin" ? "install MacTeX, or brew install --cask mactex-no-gui" : "install TeX Live's XeLaTeX (Debian, Ubuntu: sudo apt install texlive-xetex texlive-latex-extra)";
|
|
94
|
+
}
|
|
95
|
+
export function doctor(o) {
|
|
96
|
+
const exec = o.run ?? run;
|
|
97
|
+
const node = o.node ?? process.versions.node;
|
|
98
|
+
const platform = o.platform ?? process.platform;
|
|
99
|
+
const checks = [];
|
|
100
|
+
const engines = JSON.parse(readFileSync(packageFile("package.json"), "utf8")).engines?.node ?? "";
|
|
101
|
+
checks.push(satisfies(node, engines)
|
|
102
|
+
? { level: "ok", what: `Node ${node}` }
|
|
103
|
+
: { level: "fail", what: `Node ${node}: VisualProof needs ${engines.replace(/\s*\|\|\s*/g, " or ")}`, fix: "install a newer Node (https://nodejs.org), the current LTS" });
|
|
104
|
+
const elan = exec("elan", ["--version"]);
|
|
105
|
+
const installed = new Set((elan ? (exec("elan", ["toolchain", "list"]) ?? "") : "")
|
|
106
|
+
.split("\n")
|
|
107
|
+
.map((l) => l.trim())
|
|
108
|
+
.filter((l) => l && !/^no installed toolchains/i.test(l))
|
|
109
|
+
.map(toolchainName));
|
|
110
|
+
checks.push(elan
|
|
111
|
+
? { level: "ok", what: `${elan.trim().split("\n")[0].replace(/\s+\(.*\)$/, "")}, ${installed.size} toolchain(s) installed` }
|
|
112
|
+
: { level: "fail", what: "elan not found: it runs each project's Lean and Lake", fix: "install elan: https://github.com/leanprover/elan" });
|
|
113
|
+
const poppler = ["pdfinfo", "pdftotext", "pdftoppm"].filter((p) => exec(p, ["-v"]) === null);
|
|
114
|
+
checks.push(poppler.length === 0
|
|
115
|
+
? { level: "ok", what: `Poppler ${/version\s+([\d.]+)/.exec(exec("pdfinfo", ["-v"]) ?? "")?.[1] ?? ""}`.trimEnd() }
|
|
116
|
+
: { level: "warn", what: `Poppler: ${poppler.join(", ")} not found: the library cannot read PDFs`, fix: install("poppler", platform) });
|
|
117
|
+
const xelatex = exec("xelatex", ["--version"]);
|
|
118
|
+
checks.push(xelatex
|
|
119
|
+
? { level: "ok", what: `XeLaTeX (${/TeX Live \d+|MiKTeX[^\s)]*/.exec(xelatex)?.[0] ?? "found"})` }
|
|
120
|
+
: { level: "info", what: "XeLaTeX not found: needed only for the printed document (Export)", fix: install("xelatex", platform) });
|
|
121
|
+
if (exec("git", ["--version"]) === null) {
|
|
122
|
+
checks.push({ level: "info", what: "git not found: builds are kept without their commit, and past sources only as the builds kept them" });
|
|
123
|
+
}
|
|
124
|
+
if (o.path === null)
|
|
125
|
+
return { checks, ready: null };
|
|
126
|
+
let project;
|
|
127
|
+
try {
|
|
128
|
+
project = openProject(o.path, o.roots, false);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
if (!(e instanceof ProjectError))
|
|
132
|
+
throw e;
|
|
133
|
+
checks.push({ level: "fail", what: e.message });
|
|
134
|
+
return { checks, ready: null };
|
|
135
|
+
}
|
|
136
|
+
const { root, toolchain, roots, libDir } = project;
|
|
137
|
+
const name = toolchainName(toolchain);
|
|
138
|
+
const support = toolchainSupport(name);
|
|
139
|
+
const range = supportedToolchains();
|
|
140
|
+
checks.push(support === "tested"
|
|
141
|
+
? { level: "ok", what: `${name}: supported (${range.floor} to ${range.newest} are tested)` }
|
|
142
|
+
: support === "old"
|
|
143
|
+
? { level: "fail", what: `${name}: older than ${range.floor}, the oldest Lean VisualProof reads`, fix: "update the project's toolchain, or use it with a Lean VisualProof supports" }
|
|
144
|
+
: support === "new"
|
|
145
|
+
? { level: "warn", what: `${name}: newer than ${range.newest}, the newest tested; it probably works`, fix: "if the graph cannot be read, report it: https://github.com/LucianoXu/VisualProof/issues" }
|
|
146
|
+
: { level: "warn", what: `${name}: not a release of leanprover/lean4; VisualProof is tested on releases` });
|
|
147
|
+
if (elan && !installed.has(name)) {
|
|
148
|
+
checks.push({ level: "warn", what: `${name} is not installed: elan downloads it when VisualProof first runs Lake here`, fix: `elan toolchain install ${name}` });
|
|
149
|
+
}
|
|
150
|
+
const from = o.roots.length ? "--root" : existsSync(join(root, "lakefile.toml")) ? "lakefile.toml" : "lakefile.lean";
|
|
151
|
+
const missing = roots.filter((r) => !existsSync(join(libDir, ...r.split(".")) + ".olean"));
|
|
152
|
+
const mathlib = ["lakefile.toml", "lakefile.lean", "lake-manifest.json"].some((f) => existsSync(join(root, f)) && /mathlib/i.test(readFileSync(join(root, f), "utf8")));
|
|
153
|
+
checks.push(missing.length === 0
|
|
154
|
+
? { level: "ok", what: `built: ${targetsInWords(project)} (from ${from})` }
|
|
155
|
+
: {
|
|
156
|
+
level: "fail",
|
|
157
|
+
what: `not built: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ` and ${missing.length - 5} more` : ""} (${relative(root, join(libDir, ...missing[0].split("."))) + ".olean"} is missing; from ${from})`,
|
|
158
|
+
fix: `in ${root}: ${mathlib ? "lake exe cache get && " : ""}lake build${o.roots.length === 0 && from === "lakefile.lean" ? ", or name the modules to read with --root" : ""}`,
|
|
159
|
+
});
|
|
160
|
+
const dirs = projectDirs(root, o.site, o.cache);
|
|
161
|
+
for (const [what, dir] of [["site", dirs.site], ["cache", dirs.cache]]) {
|
|
162
|
+
if (!writable(dir))
|
|
163
|
+
checks.push({ level: "fail", what: `the ${what} ${dir} cannot be written`, fix: `give --${what} a folder of your own` });
|
|
164
|
+
}
|
|
165
|
+
const holder = siteHolder(dirs.site);
|
|
166
|
+
checks.push(holder === null
|
|
167
|
+
? { level: "ok", what: `site ${dirs.site}` }
|
|
168
|
+
: { level: "info", what: `site ${dirs.site}: a server (process ${holder}) is serving it; another needs a --site of its own` });
|
|
169
|
+
const failed = checks.some((c) => c.level === "fail");
|
|
170
|
+
const args = [o.path, ...o.roots.flatMap((r) => ["--root", r]), ...(o.site ? ["--site", o.site] : []), ...(o.cache ? ["--cache", o.cache] : [])];
|
|
171
|
+
return { checks, ready: failed ? null : `visualproof serve ${args.map((a) => (/^[\w./:@%+=-]+$/.test(a) ? a : JSON.stringify(a))).join(" ")}` };
|
|
172
|
+
}
|
|
173
|
+
const MARK = { ok: "✓", fail: "✗", warn: "!", info: "·" };
|
|
174
|
+
export function printReport(r, project) {
|
|
175
|
+
const lines = [`VisualProof ${packageVersion()}`];
|
|
176
|
+
for (const c of r.checks) {
|
|
177
|
+
lines.push(` ${MARK[c.level]} ${c.what}`);
|
|
178
|
+
if (c.fix && c.level !== "ok")
|
|
179
|
+
lines.push(` ${c.fix}`);
|
|
180
|
+
}
|
|
181
|
+
if (r.ready)
|
|
182
|
+
lines.push("", `Ready: ${r.ready}`);
|
|
183
|
+
else if (project === null && !r.checks.some((c) => c.level === "fail"))
|
|
184
|
+
lines.push("", "To check a project too: visualproof doctor <lean-project>");
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// The timeline (CollabSpec decision 5), as the server keeps it: one event per line in
|
|
2
|
+
// `<site>/events.jsonl`, the last 90 days and at most 5,000 events. It notices what nobody records
|
|
3
|
+
// by hand — a build that finished, files that failed or were fixed, `sorry` that came or went, and a
|
|
4
|
+
// target's TCB that changed (with the opinions that went stale by it) — and is told of re-layouts,
|
|
5
|
+
// of targets taken off and of prefixes trusted or no longer trusted (the review's state keeps the
|
|
6
|
+
// trusted list, not who changed it), and of what agents do beside their advice: notes, layout
|
|
7
|
+
// batches and their undoing (server/agent.ts), and of views saved, renamed, deleted and laid out
|
|
8
|
+
// afresh (server/views.ts). Opinions, targets and key marks are in the review's logs
|
|
9
|
+
// (server/review.ts); the page reads them from there.
|
|
10
|
+
//
|
|
11
|
+
// A target's TCB is compared with the one it had at the last graph, kept in
|
|
12
|
+
// `<site>/tcb-baselines.json`, so a change made while the server was down is noticed when it
|
|
13
|
+
// comes back.
|
|
14
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { gunzipSync } from "node:zlib";
|
|
17
|
+
import { byName, current } from "../shared/review.js";
|
|
18
|
+
import { NAMES_LISTED, TcbIndex, tcbDiff } from "../shared/timeline.js";
|
|
19
|
+
const KEEP = 5000;
|
|
20
|
+
const KEEP_MS = 90 * 24 * 3600 * 1000;
|
|
21
|
+
export class Timeline {
|
|
22
|
+
file;
|
|
23
|
+
baselineFile;
|
|
24
|
+
events = [];
|
|
25
|
+
send;
|
|
26
|
+
/** each file's state when no build was running, last time */
|
|
27
|
+
settled = null;
|
|
28
|
+
/** the graph on show, for TCBs */
|
|
29
|
+
index = null;
|
|
30
|
+
/** each target's TCB at the last graph; keyed by names, so without prototypes */
|
|
31
|
+
baselines = byName();
|
|
32
|
+
constructor(siteDir, send) {
|
|
33
|
+
this.send = send;
|
|
34
|
+
this.file = join(siteDir, "events.jsonl");
|
|
35
|
+
this.baselineFile = join(siteDir, "tcb-baselines.json");
|
|
36
|
+
if (existsSync(this.file)) {
|
|
37
|
+
const lines = readFileSync(this.file, "utf8").split("\n");
|
|
38
|
+
for (const l of lines) {
|
|
39
|
+
if (!l.trim())
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
this.events.push(JSON.parse(l));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// a line cut short (the server stopped while writing): left out
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const kept = this.pruned();
|
|
49
|
+
if (kept.length !== this.events.length) {
|
|
50
|
+
this.events = kept;
|
|
51
|
+
this.rewrite();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const raw = JSON.parse(readFileSync(this.baselineFile, "utf8"));
|
|
56
|
+
for (const [target, tcb] of Object.entries(raw && typeof raw === "object" ? raw : {})) {
|
|
57
|
+
if (!tcb || typeof tcb !== "object")
|
|
58
|
+
continue;
|
|
59
|
+
const m = byName();
|
|
60
|
+
for (const [k, v] of Object.entries(tcb))
|
|
61
|
+
if (typeof v === "string")
|
|
62
|
+
m[k] = v;
|
|
63
|
+
this.baselines[target] = m;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// none yet, or unreadable: every target starts again from the TCB it has now
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
pruned() {
|
|
71
|
+
const after = Date.now() - KEEP_MS;
|
|
72
|
+
return this.events.filter((e) => Date.parse(e.at) >= after).slice(-KEEP);
|
|
73
|
+
}
|
|
74
|
+
rewrite() {
|
|
75
|
+
const tmp = `${this.file}.tmp-${process.pid}`;
|
|
76
|
+
writeFileSync(tmp, this.events.map((e) => JSON.stringify(e) + "\n").join(""));
|
|
77
|
+
renameSync(tmp, this.file);
|
|
78
|
+
}
|
|
79
|
+
/** Record an event, and tell every page. */
|
|
80
|
+
add(e) {
|
|
81
|
+
const full = { ...e, id: (this.events[this.events.length - 1]?.id ?? 0) + 1, at: new Date().toISOString() };
|
|
82
|
+
this.events.push(full);
|
|
83
|
+
appendFileSync(this.file, JSON.stringify(full) + "\n");
|
|
84
|
+
if (this.events.length > KEEP + 500) {
|
|
85
|
+
this.events = this.pruned();
|
|
86
|
+
this.rewrite();
|
|
87
|
+
}
|
|
88
|
+
this.send(full);
|
|
89
|
+
return full;
|
|
90
|
+
}
|
|
91
|
+
/** The build status, every time it changes: once no build runs, what changed since the last
|
|
92
|
+
* quiet moment is one event. The first status after a start is where the comparison begins. */
|
|
93
|
+
buildStatus(b) {
|
|
94
|
+
if (b.active)
|
|
95
|
+
return;
|
|
96
|
+
const now = new Map(b.files.map((f) => [f.module, f]));
|
|
97
|
+
const was = this.settled;
|
|
98
|
+
this.settled = now;
|
|
99
|
+
if (!was)
|
|
100
|
+
return;
|
|
101
|
+
const failed = [], fixed = [], sorry = [], sorryGone = [];
|
|
102
|
+
let rebuilt = 0;
|
|
103
|
+
for (const [m, f] of now) {
|
|
104
|
+
const p = was.get(m);
|
|
105
|
+
if (f.builtAt && p?.builtAt !== f.builtAt)
|
|
106
|
+
rebuilt++;
|
|
107
|
+
if (f.state === "failed" && p?.state !== "failed")
|
|
108
|
+
failed.push(m);
|
|
109
|
+
if (p?.state === "failed" && (f.state === "ok" || f.state === "sorry"))
|
|
110
|
+
fixed.push(m);
|
|
111
|
+
if (f.state === "sorry" && p?.state !== "sorry")
|
|
112
|
+
sorry.push(m);
|
|
113
|
+
if (p?.state === "sorry" && f.state === "ok")
|
|
114
|
+
sorryGone.push(m);
|
|
115
|
+
}
|
|
116
|
+
if (failed.length || fixed.length || sorry.length || sorryGone.length || rebuilt)
|
|
117
|
+
this.add({ kind: "build", failed: failed.sort(), fixed: fixed.sort(), sorry: sorry.sort(), sorryGone: sorryGone.sort(), rebuilt });
|
|
118
|
+
}
|
|
119
|
+
/** A new graph: every target's TCB is compared with the one it had, and a change is an event.
|
|
120
|
+
* `renamed`: the renames found against the build before (HistorySpec.md), old name → new: a
|
|
121
|
+
* declaration renamed is the same one, neither removed nor added, and a target renamed keeps its
|
|
122
|
+
* baseline. */
|
|
123
|
+
graphChanged(prep, review, renamed = new Map()) {
|
|
124
|
+
const meta = JSON.parse(gunzipSync(prep.metaGz).toString("utf8"));
|
|
125
|
+
const raw = gunzipSync(prep.edgesGz);
|
|
126
|
+
const edges = new Int32Array(new Uint8Array(raw).buffer);
|
|
127
|
+
this.index = new TcbIndex(meta.nodes, edges);
|
|
128
|
+
// a baseline kept before versions holds the kernel's hashes (server/versions.ts): one that is
|
|
129
|
+
// the kernel's hash of the content there now is that content's version
|
|
130
|
+
const kernel = new Map();
|
|
131
|
+
for (const v of meta.nodes)
|
|
132
|
+
if (v.kernel && v.hash)
|
|
133
|
+
kernel.set(v.name, v.kernel);
|
|
134
|
+
if (renamed.size) {
|
|
135
|
+
for (const t of Object.keys(this.baselines)) {
|
|
136
|
+
const b = this.baselines[t];
|
|
137
|
+
const now = byName();
|
|
138
|
+
for (const k of Object.keys(b))
|
|
139
|
+
now[renamed.get(k) ?? k] = b[k];
|
|
140
|
+
delete this.baselines[t];
|
|
141
|
+
this.baselines[renamed.get(t) ?? t] = now;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const target of Object.keys(review.targets)) {
|
|
145
|
+
const root = this.index.byName.get(target);
|
|
146
|
+
if (root === undefined)
|
|
147
|
+
continue;
|
|
148
|
+
const now = this.index.tcb(root);
|
|
149
|
+
const before = Object.hasOwn(this.baselines, target) ? this.baselines[target] : null;
|
|
150
|
+
if (before)
|
|
151
|
+
for (const k of Object.keys(before))
|
|
152
|
+
if (Object.hasOwn(now, k) && kernel.get(k) === before[k])
|
|
153
|
+
before[k] = now[k];
|
|
154
|
+
this.baselines[target] = now;
|
|
155
|
+
if (!before)
|
|
156
|
+
continue;
|
|
157
|
+
const d = tcbDiff(before, now);
|
|
158
|
+
if (!d.added.length && !d.removed.length && !d.changed.length)
|
|
159
|
+
continue;
|
|
160
|
+
// opinions given on the version before, for what changed: stale now
|
|
161
|
+
let stale = 0;
|
|
162
|
+
for (const name of d.changed)
|
|
163
|
+
stale += current(review, name).filter((o) => o.hash && o.hash === before[name]).length;
|
|
164
|
+
this.add({
|
|
165
|
+
kind: "tcb",
|
|
166
|
+
target,
|
|
167
|
+
added: d.added.slice(0, NAMES_LISTED),
|
|
168
|
+
removed: d.removed.slice(0, NAMES_LISTED),
|
|
169
|
+
changed: d.changed.slice(0, NAMES_LISTED),
|
|
170
|
+
counts: { added: d.added.length, removed: d.removed.length, changed: d.changed.length },
|
|
171
|
+
stale,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
this.saveBaselines();
|
|
175
|
+
}
|
|
176
|
+
/** The review changed: a new target starts from the TCB it has now. */
|
|
177
|
+
reviewChanged(review) {
|
|
178
|
+
if (!this.index)
|
|
179
|
+
return;
|
|
180
|
+
let added = false;
|
|
181
|
+
for (const target of Object.keys(review.targets)) {
|
|
182
|
+
if (Object.hasOwn(this.baselines, target))
|
|
183
|
+
continue;
|
|
184
|
+
const root = this.index.byName.get(target);
|
|
185
|
+
if (root === undefined)
|
|
186
|
+
continue;
|
|
187
|
+
this.baselines[target] = this.index.tcb(root);
|
|
188
|
+
added = true;
|
|
189
|
+
}
|
|
190
|
+
if (added)
|
|
191
|
+
this.saveBaselines();
|
|
192
|
+
}
|
|
193
|
+
saveBaselines() {
|
|
194
|
+
const tmp = `${this.baselineFile}.tmp-${process.pid}`;
|
|
195
|
+
writeFileSync(tmp, JSON.stringify(this.baselines));
|
|
196
|
+
renameSync(tmp, this.baselineFile);
|
|
197
|
+
}
|
|
198
|
+
}
|