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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -2
  3. package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
  4. package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
  5. package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
  6. package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
  7. package/dist/assets/latex/visualproof.sty +308 -0
  8. package/dist/extractor/Extract.lean +1180 -0
  9. package/dist/extractor/Usage.lean +900 -0
  10. package/dist/mcp/visualproof-mcp.mjs +476 -0
  11. package/dist/server/access.js +95 -0
  12. package/dist/server/agent.js +3235 -0
  13. package/dist/server/agentdocs.js +35 -0
  14. package/dist/server/agentlibrary.js +202 -0
  15. package/dist/server/agentnotes.js +701 -0
  16. package/dist/server/agentviews.js +703 -0
  17. package/dist/server/askfollow.js +286 -0
  18. package/dist/server/boardspots.js +159 -0
  19. package/dist/server/dirs.js +175 -0
  20. package/dist/server/doctor.js +186 -0
  21. package/dist/server/events.js +198 -0
  22. package/dist/server/export.js +420 -0
  23. package/dist/server/extract.js +204 -0
  24. package/dist/server/fetchurl.js +321 -0
  25. package/dist/server/figures.js +158 -0
  26. package/dist/server/history.js +741 -0
  27. package/dist/server/historyapi.js +304 -0
  28. package/dist/server/hover.js +595 -0
  29. package/dist/server/identity.js +114 -0
  30. package/dist/server/invites.js +241 -0
  31. package/dist/server/layoutthread.js +51 -0
  32. package/dist/server/library.js +600 -0
  33. package/dist/server/libraryroute.js +211 -0
  34. package/dist/server/live.js +1101 -0
  35. package/dist/server/lostfound.js +199 -0
  36. package/dist/server/main.js +1394 -0
  37. package/dist/server/mcptools.js +497 -0
  38. package/dist/server/pastdoc.js +73 -0
  39. package/dist/server/pipeline.js +156 -0
  40. package/dist/server/poppler.js +251 -0
  41. package/dist/server/presence.js +252 -0
  42. package/dist/server/project.js +222 -0
  43. package/dist/server/prompt.js +53 -0
  44. package/dist/server/readonly.js +152 -0
  45. package/dist/server/readonlygate.js +123 -0
  46. package/dist/server/renames.js +268 -0
  47. package/dist/server/review.js +334 -0
  48. package/dist/server/reviewapi.js +188 -0
  49. package/dist/server/snapshot.js +49 -0
  50. package/dist/server/sourcestore.js +473 -0
  51. package/dist/server/status.js +360 -0
  52. package/dist/server/talk.js +476 -0
  53. package/dist/server/tokens.js +221 -0
  54. package/dist/server/usage.js +501 -0
  55. package/dist/server/versions.js +82 -0
  56. package/dist/server/view.js +157 -0
  57. package/dist/server/views.js +1167 -0
  58. package/dist/shared/askfollow.js +46 -0
  59. package/dist/shared/board.js +332 -0
  60. package/dist/shared/build.js +2 -0
  61. package/dist/shared/collab.js +148 -0
  62. package/dist/shared/comment.js +285 -0
  63. package/dist/shared/context.js +157 -0
  64. package/dist/shared/doc/authors.js +98 -0
  65. package/dist/shared/doc/build.js +358 -0
  66. package/dist/shared/doc/latex.js +455 -0
  67. package/dist/shared/doc/markdown.js +159 -0
  68. package/dist/shared/doc/model.js +37 -0
  69. package/dist/shared/doc/order.js +107 -0
  70. package/dist/shared/doc/proof.js +272 -0
  71. package/dist/shared/doc/sections.js +93 -0
  72. package/dist/shared/doc/settings.js +75 -0
  73. package/dist/shared/edgecodec.js +102 -0
  74. package/dist/shared/editor.js +64 -0
  75. package/dist/shared/excerpt.js +87 -0
  76. package/dist/shared/graph.js +16 -0
  77. package/dist/shared/items.js +92 -0
  78. package/dist/shared/keys.js +203 -0
  79. package/dist/shared/layout.js +926 -0
  80. package/dist/shared/lexer.js +130 -0
  81. package/dist/shared/library.js +711 -0
  82. package/dist/shared/lostfound.js +150 -0
  83. package/dist/shared/moves.js +243 -0
  84. package/dist/shared/notecard.js +295 -0
  85. package/dist/shared/noteplace.js +505 -0
  86. package/dist/shared/palette.js +46 -0
  87. package/dist/shared/pastgraph.js +95 -0
  88. package/dist/shared/places.js +80 -0
  89. package/dist/shared/projectnotes.js +244 -0
  90. package/dist/shared/query.js +810 -0
  91. package/dist/shared/review.js +642 -0
  92. package/dist/shared/reviewqueue.js +333 -0
  93. package/dist/shared/ring.js +366 -0
  94. package/dist/shared/scope.js +255 -0
  95. package/dist/shared/scopeedit.js +81 -0
  96. package/dist/shared/search.js +191 -0
  97. package/dist/shared/separate.js +178 -0
  98. package/dist/shared/sharesnap.js +280 -0
  99. package/dist/shared/sticky.js +501 -0
  100. package/dist/shared/timeline.js +73 -0
  101. package/dist/shared/titles.js +298 -0
  102. package/dist/shared/viewgraph.js +139 -0
  103. package/dist/shared/views.js +859 -0
  104. package/dist/web/bundle/app.js +48733 -0
  105. package/dist/web/bundle/edges.js +409 -0
  106. package/dist/web/bundle/layout.js +1010 -0
  107. package/dist/web/bundle/pdfworker.js +48322 -0
  108. package/dist/web/index.html +239 -0
  109. package/dist/web/src/motion.js +430 -0
  110. package/dist/web/src/past.js +224 -0
  111. package/dist/web/src/scene.js +787 -0
  112. package/dist/web/style.css +2312 -0
  113. package/docs/agent-api/README.md +91 -0
  114. package/docs/agent-api/advice.md +108 -0
  115. package/docs/agent-api/export.md +120 -0
  116. package/docs/agent-api/following.md +140 -0
  117. package/docs/agent-api/history.md +142 -0
  118. package/docs/agent-api/library.md +65 -0
  119. package/docs/agent-api/mcp.md +104 -0
  120. package/docs/agent-api/notes.md +93 -0
  121. package/docs/agent-api/reading.md +166 -0
  122. package/docs/agent-api/requests.md +94 -0
  123. package/docs/agent-api/talking.md +100 -0
  124. package/docs/agent-api/tcb.md +135 -0
  125. package/docs/agent-api/views.md +124 -0
  126. package/docs/agent-api/words.md +56 -0
  127. package/package.json +39 -4
  128. package/test/toolchains.json +5 -0
@@ -0,0 +1,473 @@
1
+ // The source text of every build the history keeps (HistorySpec decisions 20–23, the owner's of
2
+ // 2026-10-10), borrowing git's content-addressed design: each text of a project file is a blob named
3
+ // by its SHA-256, `history/sources/<sha256>.gz`, written once whatever number of builds compiled it,
4
+ // and a snapshot's manifest says, per module, which blob its build was compiled from. A build so
5
+ // adds only the files that changed.
6
+ //
7
+ // Only what was built is kept. Lake's trace records the hash of the text each module was compiled
8
+ // from (server/status.ts, `compiledTextHash`); a text is kept for a module only where its Lake hash
9
+ // is that one: the file as it is when the build is read, else the file in the commit checked out
10
+ // then. A file edited after the build matches neither, and its text is not kept: a text that is not
11
+ // the build's is never shown as the build's.
12
+ //
13
+ // The snapshots made before the history kept sources are backfilled once, in the background, at
14
+ // start (`backfillSources`). Their builds' hashes are known where the project's cache still holds what
15
+ // says them — the view of an extraction of that build (`view/<fingerprint>-v*.meta.json.gz`,
16
+ // `modules[].compiled`), or the present trace of a module not compiled since — and a text is kept
17
+ // there only where its Lake hash is that one. Elsewhere the hash is gone, and the best check left is
18
+ // the places the snapshot recorded: a text is kept where it is the one candidate that agrees with
19
+ // every one of them (`misfit`) — its project imports, where each declaration starts and ends, its
20
+ // doc comment and the name it declares — and the manifest says so (`check: "places"`), so that the
21
+ // page and the agents can say how the text was matched to its build. Where several agree, the
22
+ // commit of a build read clean is taken, and of a build read with edits none is, one of them not
23
+ // being the build's. Candidates are read with git,
24
+ // read-only: the build's commit, the commits made on top of it, the working tree and the blobs kept
25
+ // already. Nothing is ever written into the project or its `.git`.
26
+ import { spawn, spawnSync } from "node:child_process";
27
+ import { createHash } from "node:crypto";
28
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { gunzipSync, gzipSync } from "node:zlib";
31
+ import { compiledTextHash, lakeTextHash, parseImports } from "./status.js";
32
+ const sha256 = (text) => createHash("sha256").update(text, "utf8").digest("hex");
33
+ /** The blobs: each text once, gzipped, named by its SHA-256. */
34
+ export class SourceStore {
35
+ dir;
36
+ /** blobs known to be on disk, and their gzipped sizes */
37
+ sizes = new Map();
38
+ constructor(historyDir) {
39
+ this.dir = join(historyDir, "sources");
40
+ }
41
+ /** Keep `text`: its blob, its gzipped size, and whether it was written now (no build had it). */
42
+ put(text) {
43
+ const blob = sha256(text);
44
+ let size = this.sizes.get(blob);
45
+ if (size !== undefined)
46
+ return { blob, size, fresh: false };
47
+ const f = join(this.dir, `${blob}.gz`);
48
+ if (existsSync(f)) {
49
+ size = statSync(f).size;
50
+ this.sizes.set(blob, size);
51
+ return { blob, size, fresh: false };
52
+ }
53
+ mkdirSync(this.dir, { recursive: true });
54
+ const gz = gzipSync(text);
55
+ writeFileSync(`${f}.tmp`, gz);
56
+ renameSync(`${f}.tmp`, f);
57
+ this.sizes.set(blob, gz.length);
58
+ return { blob, size: gz.length, fresh: true };
59
+ }
60
+ /** A blob's gzipped size; 0 where it is not there. */
61
+ size(blob) {
62
+ let size = this.sizes.get(blob);
63
+ if (size === undefined) {
64
+ try {
65
+ size = statSync(join(this.dir, `${blob}.gz`)).size;
66
+ }
67
+ catch {
68
+ return 0;
69
+ }
70
+ this.sizes.set(blob, size);
71
+ }
72
+ return size;
73
+ }
74
+ /** A blob's text; null where it is not there, or is not what its name says. */
75
+ text(blob) {
76
+ if (!/^[0-9a-f]{64}$/.test(blob))
77
+ return null;
78
+ try {
79
+ const text = gunzipSync(readFileSync(join(this.dir, `${blob}.gz`))).toString("utf8");
80
+ return sha256(text) === blob ? text : null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /** The bytes the blobs hold on disk. */
87
+ bytes() {
88
+ let total = 0;
89
+ try {
90
+ for (const f of readdirSync(this.dir))
91
+ total += statSync(join(this.dir, f)).size;
92
+ }
93
+ catch {
94
+ // nothing there yet
95
+ }
96
+ return total;
97
+ }
98
+ }
99
+ /** a doc comment's words, however they are laid out */
100
+ const words = (s) => s.replace(/\s+/g, " ").trim();
101
+ /** Where `text` disagrees with what the snapshot recorded of a module compiled from it (`shape`);
102
+ * null where it agrees with every place. Its project imports (`isModule` tells a project module)
103
+ * are the module's; each declaration's range lies in it, starting on a token and ending on one; a
104
+ * range that opens with a doc comment holds the declaration's doc; and where its command writes a
105
+ * name, it is the declaration's. A text that agrees may still differ inside a declaration, where no
106
+ * place moves: that is why a Lake hash, where known, is the check. */
107
+ export function misfit(text, shape, isModule) {
108
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
109
+ const imports = new Set(parseImports(text).filter(isModule));
110
+ if (imports.size !== new Set(shape.imports).size || shape.imports.some((m) => !imports.has(m)))
111
+ return "its imports differ";
112
+ const cps = new Map();
113
+ const chars = (l) => {
114
+ let c = cps.get(l);
115
+ if (!c)
116
+ cps.set(l, (c = [...lines[l - 1]]));
117
+ return c;
118
+ };
119
+ for (const n of shape.nodes) {
120
+ const r = n.range;
121
+ if (!r)
122
+ continue;
123
+ const [l0, c0, l1, c1] = r;
124
+ if (l0 < 1 || l1 < l0 || l1 > lines.length)
125
+ return `${n.name} lies past its end`;
126
+ // Lean's columns count code points, and a range is its first token to its last
127
+ const first = chars(l0);
128
+ const last = chars(l1);
129
+ if (c0 >= first.length || /\s/.test(first[c0]) || (c0 > 0 && !/\s/.test(first[c0 - 1])))
130
+ return `${n.name} does not start where it did`;
131
+ if (c1 < 1 || c1 > last.length || /\s/.test(last[c1 - 1]))
132
+ return `${n.name} does not end where it did`;
133
+ const opening = first.slice(c0).join("");
134
+ if (n.doc !== undefined && opening.startsWith("/--")) {
135
+ const body = lines.slice(l0 - 1, l1).join("\n").slice(lines[l0 - 1].length - opening.length);
136
+ const end = body.indexOf("-/");
137
+ if (end < 0 || words(body.slice(3, end)) !== words(n.doc))
138
+ return `${n.name}'s doc comment differs`;
139
+ }
140
+ // where its command writes a name, it is the declaration's: whole, or its last parts, written
141
+ // inside the namespace
142
+ if (n.gen === undefined && n.kind !== "instance" && n.kind !== "notation") {
143
+ const declared = lines.slice(l0 - 1, l1).flatMap((l) => {
144
+ const m = DECLARES.exec(l);
145
+ return m ? [m[1].replace(/^_root_\./, "").replace(/\.\{.*$/, "").replace(/[«»]/g, "")] : [];
146
+ });
147
+ const own = n.name.replace(/[«»]/g, "");
148
+ if (declared.length && !declared.some((d) => d === own || own.endsWith(`.${d}`)))
149
+ return `${n.name} is not declared where it was`;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ /** a line that declares something by name, as shared/excerpt.ts reads one, and the name */
155
+ const DECLARES = /^\s*(?:@\[[^\]]*\]\s*)*(?:(?:private|protected|noncomputable|partial|unsafe|nonrec|scoped|local)\s+)*(?:class\s+inductive|theorem|lemma|def|abbrev|structure|class|inductive|axiom|opaque|irreducible_def)\s+([^\s:({[]\S*)/;
156
+ // ------------------------------------------------------------------------------------------- git
157
+ /** Git, read-only and taking no lock, run from the project's folder: its output, or null. */
158
+ function gitSync(root, args) {
159
+ const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf8", timeout: 10_000, maxBuffer: 256 << 20, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, stdio: ["ignore", "pipe", "ignore"] });
160
+ return r.status === 0 ? r.stdout : null;
161
+ }
162
+ /** The same, off the event loop, with `input` on its standard input: its output as bytes, or null. */
163
+ function git(root, args, input) {
164
+ return new Promise((ok) => {
165
+ const p = spawn("git", ["-C", root, ...args], { env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }, stdio: ["pipe", "pipe", "ignore"] });
166
+ const out = [];
167
+ p.stdout.on("data", (b) => out.push(b));
168
+ p.on("error", () => ok(null));
169
+ p.on("close", (code) => ok(code === 0 ? Buffer.concat(out) : null));
170
+ p.stdin.on("error", () => { });
171
+ p.stdin.end(input ?? "");
172
+ });
173
+ }
174
+ /** A file as commit `commit` has it (`git show <commit>:./<file>`, relative to the project); null
175
+ * where it has none. */
176
+ export function fileAt(root, commit, file) {
177
+ if (!/^[0-9a-f]{7,64}$/.test(commit))
178
+ return null;
179
+ return gitSync(root, ["show", `${commit}:./${file}`]);
180
+ }
181
+ // --------------------------------------------------------------------------- at a build's reading
182
+ /** The texts a build being read was compiled from (HistorySpec decision 21), kept in `store`: per
183
+ * module of the graph that has a file, the Lake hash its trace records and, where the file as it is
184
+ * now has that hash — else the file in `commit`, the commit checked out now — that text; and the
185
+ * gzipped bytes of the blobs written for them now. */
186
+ export function keepSources(p, modules, commit, store) {
187
+ const out = {};
188
+ let written = 0;
189
+ const put = (text) => {
190
+ const r = store.put(text);
191
+ if (r.fresh)
192
+ written += r.size;
193
+ return r.blob;
194
+ };
195
+ for (const m of modules) {
196
+ const file = p.files.get(m);
197
+ if (!file)
198
+ continue;
199
+ const lake = compiledTextHash(p, m, file);
200
+ if (!lake) {
201
+ out[m] = { file, why: "its build left no hash of the text it was compiled from" };
202
+ continue;
203
+ }
204
+ let text = null;
205
+ try {
206
+ text = readFileSync(join(p.root, file), "utf8");
207
+ }
208
+ catch {
209
+ // gone since the build: the commit may have it
210
+ }
211
+ if (text !== null && lakeTextHash(text) === lake) {
212
+ out[m] = { file, lake, blob: put(text), check: "lake", from: "tree" };
213
+ continue;
214
+ }
215
+ const then = commit ? fileAt(p.root, commit, file) : null;
216
+ if (then !== null && lakeTextHash(then) === lake) {
217
+ out[m] = { file, lake, blob: put(then), check: "lake", from: `git:${commit}` };
218
+ continue;
219
+ }
220
+ out[m] = { file, lake, why: "its file was edited after the build, and the commit does not have the text it was compiled from" };
221
+ }
222
+ return { map: out, written };
223
+ }
224
+ /** What a manifest's texts come to: kept and of how many, matched by places, and their bytes. */
225
+ export function summarize(map, store, written) {
226
+ let kept = 0, places = 0, all = 0;
227
+ const seen = new Set();
228
+ for (const r of Object.values(map)) {
229
+ if (!r.blob)
230
+ continue;
231
+ kept++;
232
+ if (r.check === "places")
233
+ places++;
234
+ if (!seen.has(r.blob)) {
235
+ seen.add(r.blob);
236
+ all += store.size(r.blob);
237
+ }
238
+ }
239
+ return { kept, of: Object.keys(map).length, places, bytes: { new: written, all } };
240
+ }
241
+ /** The Lake hashes of the texts a snapshot's build compiled, from what the project's cache still holds:
242
+ * the view of any extraction it was read from, and the present trace of every module whose `.olean`
243
+ * is not newer than the build (not compiled since, so its trace is the build's). */
244
+ function knownHashes(cacheDir, p, info, reads, modules) {
245
+ const hashes = new Map();
246
+ let view = 0, trace = 0;
247
+ const viewDir = join(cacheDir, "view");
248
+ let files = [];
249
+ try {
250
+ files = readdirSync(viewDir);
251
+ }
252
+ catch {
253
+ // no views kept
254
+ }
255
+ for (const fp of reads) {
256
+ for (const f of files.filter((x) => x.startsWith(`${fp}-v`) && x.endsWith(".meta.json.gz"))) {
257
+ try {
258
+ const meta = JSON.parse(gunzipSync(readFileSync(join(viewDir, f))).toString("utf8"));
259
+ for (const m of meta.modules)
260
+ if (m.compiled && !hashes.has(m.name)) {
261
+ hashes.set(m.name, m.compiled);
262
+ view++;
263
+ }
264
+ }
265
+ catch {
266
+ // a view being written, or of another shape: the traces may say
267
+ }
268
+ }
269
+ }
270
+ const at = Date.parse(info.at);
271
+ for (const m of modules) {
272
+ if (hashes.has(m))
273
+ continue;
274
+ const file = p.files.get(m);
275
+ if (!file)
276
+ continue;
277
+ try {
278
+ if (statSync(join(p.libDir, ...m.split(".")) + ".olean").mtimeMs > at + 1)
279
+ continue;
280
+ }
281
+ catch {
282
+ continue;
283
+ }
284
+ const h = compiledTextHash(p, m, file);
285
+ if (h) {
286
+ hashes.set(m, h);
287
+ trace++;
288
+ }
289
+ }
290
+ return { hashes, view, trace };
291
+ }
292
+ /** The commit a snapshot was read at, and the commits made on top of it, nearest first, at most
293
+ * `most`: where a build's edits were committed afterwards. */
294
+ async function commitsFrom(root, commit, most = 64) {
295
+ const out = await git(root, ["log", "--format=%H", "--ancestry-path", "--reverse", "--all", `^${commit}`]);
296
+ const later = out ? out.toString("utf8").split("\n").filter((h) => /^[0-9a-f]{40,64}$/.test(h)) : [];
297
+ return [commit, ...later.slice(0, most)];
298
+ }
299
+ /** Each `.lean` file's blob in each of `commits`, by path relative to the project. */
300
+ async function treesOf(root, commits) {
301
+ const out = new Map();
302
+ for (const c of commits) {
303
+ const ls = await git(root, ["ls-tree", "-r", c, "--", "."]);
304
+ const files = new Map();
305
+ for (const line of ls ? ls.toString("utf8").split("\n") : []) {
306
+ const m = /^\d+ blob ([0-9a-f]+)\t(.+\.lean)$/.exec(line);
307
+ if (m)
308
+ files.set(m[2], m[1]);
309
+ }
310
+ out.set(c, files);
311
+ }
312
+ return out;
313
+ }
314
+ /** Git's blobs, read at once (`git cat-file --batch`): by their ids, as text. */
315
+ async function readBlobs(root, ids) {
316
+ const out = new Map();
317
+ for (let k = 0; k < ids.length; k += 500) {
318
+ const part = ids.slice(k, k + 500);
319
+ const buf = await git(root, ["cat-file", "--batch"], part.join("\n") + "\n");
320
+ if (!buf)
321
+ continue;
322
+ let at = 0;
323
+ while (at < buf.length) {
324
+ const nl = buf.indexOf(10, at);
325
+ if (nl < 0)
326
+ break;
327
+ const head = buf.toString("utf8", at, nl).split(" ");
328
+ at = nl + 1;
329
+ if (head[1] !== "blob")
330
+ continue;
331
+ const size = Number(head[2]);
332
+ out.set(head[0], buf.toString("utf8", at, at + size));
333
+ at += size + 1;
334
+ }
335
+ }
336
+ return out;
337
+ }
338
+ const pause = () => new Promise((ok) => setImmediate(ok));
339
+ /** Backfill the texts of the snapshots whose manifests keep none (made before the history kept
340
+ * sources), oldest first, once each: a manifest backfilled keeps its texts and is not looked at
341
+ * again. Runs off the event loop's way, git read-only; what it finds is logged, a line per
342
+ * snapshot. */
343
+ export async function backfillSources(h, p, cacheDir, log) {
344
+ const store = h.sources;
345
+ const reports = [];
346
+ const todo = h.list().filter((s) => h.sourcesOf(s.id) === null);
347
+ if (!todo.length)
348
+ return reports;
349
+ log(`history: backfilling the source texts of ${todo.length} snapshot(s) kept before the texts were`);
350
+ const t0 = Date.now();
351
+ // the blobs kept already, by file: candidates for every build
352
+ const keptFor = new Map();
353
+ const remember = (file, blob) => {
354
+ const s = keptFor.get(file);
355
+ if (s)
356
+ s.add(blob);
357
+ else
358
+ keptFor.set(file, new Set([blob]));
359
+ };
360
+ for (const s of h.list())
361
+ for (const r of Object.values(h.sourcesOf(s.id) ?? {}))
362
+ if (r.blob)
363
+ remember(r.file, r.blob);
364
+ const repo = gitSync(p.root, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
365
+ // what a text's imports are read against: every module of the project any build had, so that a
366
+ // later text that imports a module this build did not have yet does not agree with it
367
+ const project = new Set(h.list().flatMap((s) => h.modulesOf(s.id)));
368
+ for (const s of todo) {
369
+ const modules = h.modulesOf(s.id);
370
+ const known = knownHashes(cacheDir, p, s, h.readsOf(s.id), modules);
371
+ const commit = s.commit?.hash && repo ? s.commit.hash : null;
372
+ const commits = commit ? await commitsFrom(p.root, commit) : [];
373
+ const trees = commits.length ? await treesOf(p.root, commits) : new Map();
374
+ const ids = new Set();
375
+ for (const m of modules) {
376
+ const file = p.files.get(m);
377
+ if (file)
378
+ for (const t of trees.values())
379
+ if (t.has(file))
380
+ ids.add(t.get(file));
381
+ }
382
+ const blobs = await readBlobs(p.root, [...ids]);
383
+ const map = {};
384
+ const report = { id: s.id, of: 0, lake: 0, places: 0, missing: {}, hashes: { view: known.view, trace: known.trace }, written: 0 };
385
+ const miss = (m, ref, why) => {
386
+ map[m] = { ...ref, why };
387
+ report.missing[why] = (report.missing[why] ?? 0) + 1;
388
+ };
389
+ let n = 0;
390
+ for (const m of modules) {
391
+ // a module no file stands for now (removed since) is read under the path its name gives
392
+ const file = p.files.get(m) ?? `${m.split(".").join("/")}.lean`;
393
+ report.of++;
394
+ if (++n % 50 === 0)
395
+ await pause();
396
+ // the candidates, likeliest first: the commit read, those made on top of it, the working
397
+ // tree, the blobs kept already; each text once
398
+ const cands = [];
399
+ const seen = new Set();
400
+ const add = (text, from) => {
401
+ if (text === null || text === undefined)
402
+ return;
403
+ const k = sha256(text);
404
+ if (seen.has(k))
405
+ return;
406
+ seen.add(k);
407
+ cands.push({ text, from });
408
+ };
409
+ for (const c of commits)
410
+ add(blobs.get(trees.get(c)?.get(file) ?? ""), `git:${c}`);
411
+ try {
412
+ add(readFileSync(join(p.root, file), "utf8"), "tree");
413
+ }
414
+ catch {
415
+ // not there now
416
+ }
417
+ for (const b of keptFor.get(file) ?? [])
418
+ add(store.text(b), "kept");
419
+ const lake = known.hashes.get(m);
420
+ const ref = { file, ...(lake ? { lake } : {}) };
421
+ let pick = null;
422
+ if (lake) {
423
+ const c = cands.find((x) => lakeTextHash(x.text) === lake);
424
+ if (!c) {
425
+ miss(m, ref, "no text with the hash its build compiled was found");
426
+ continue;
427
+ }
428
+ pick = { ...c, check: "lake" };
429
+ }
430
+ else {
431
+ const shape = h.moduleShape(s.id, m);
432
+ if (!shape) {
433
+ miss(m, ref, "its snapshot cannot be read");
434
+ continue;
435
+ }
436
+ const fit = cands.filter((x) => misfit(x.text, shape, (y) => project.has(y) || p.files.has(y)) === null);
437
+ if (!fit.length) {
438
+ miss(m, ref, cands.length ? "no text agrees with the places its build recorded" : "no text of its file was found");
439
+ continue;
440
+ }
441
+ // several texts agree: the commit read wins where the build was read clean from it, else
442
+ // nothing is kept, since one of them is not the build's
443
+ const one = fit.length === 1 ? fit[0] : !s.commit?.dirty && !s.commit?.guessed && commit ? fit.find((x) => x.from === `git:${commit}`) : undefined;
444
+ if (!one) {
445
+ miss(m, ref, "several texts agree with the places its build recorded");
446
+ continue;
447
+ }
448
+ pick = { ...one, check: "places" };
449
+ }
450
+ const put = store.put(pick.text);
451
+ if (put.fresh)
452
+ report.written += put.size;
453
+ remember(file, put.blob);
454
+ map[m] = { ...ref, blob: put.blob, check: pick.check, from: pick.from };
455
+ if (pick.check === "lake")
456
+ report.lake++;
457
+ else
458
+ report.places++;
459
+ }
460
+ // a reading of the build since the backfill began kept its texts itself: those stand
461
+ if (!h.setSources(s.id, map, report.written)) {
462
+ log(`history: snapshot ${s.id} was read again meanwhile, and kept its texts itself`);
463
+ continue;
464
+ }
465
+ reports.push(report);
466
+ const missing = Object.entries(report.missing).map(([why, k]) => `${k} ${why}`).join("; ");
467
+ log(`history: snapshot ${s.id} (${s.commit ? `${s.commit.hash.slice(0, 7)}${s.commit.dirty ? " dirty" : ""}` : "no commit"}): ` +
468
+ `${report.lake + report.places} of ${report.of} module texts kept — ${report.lake} by their Lake hash (${known.view} hashes from a view of the build, ${known.trace} from present traces), ${report.places} by their places alone` +
469
+ `${missing ? `; not kept: ${missing}` : ""}; ${(report.written / 1024).toFixed(0)} KB written`);
470
+ }
471
+ log(`history: source texts backfilled in ${((Date.now() - t0) / 1000).toFixed(1)} s`);
472
+ return reports;
473
+ }