visualproof 0.1.0 → 0.3.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/README.md +40 -26
- package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
- package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
- package/dist/server/agent.js +125 -92
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/history.js +39 -0
- package/dist/server/historyapi.js +2 -2
- package/dist/server/hover.js +3 -3
- package/dist/server/layoutthread.js +1 -1
- package/dist/server/live.js +6 -0
- package/dist/server/main.js +151 -11
- package/dist/server/mcptools.js +6 -4
- package/dist/server/pastdoc.js +1 -1
- package/dist/server/pipeline.js +18 -9
- package/dist/server/readonlygate.js +2 -2
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +13 -3
- package/dist/server/usage.js +2 -2
- package/dist/server/views.js +25 -1
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +99 -1
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/doc/order.js +17 -0
- package/dist/shared/doc/outline.js +45 -0
- package/dist/shared/keys.js +1 -1
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/moves.js +2 -1
- package/dist/shared/pastgraph.js +9 -1
- package/dist/shared/query.js +21 -13
- package/dist/shared/review.js +62 -5
- package/dist/shared/sharesnap.js +2 -1
- package/dist/shared/titles.js +2 -0
- package/dist/shared/viewgraph.js +18 -0
- package/dist/web/bundle/app.js +7709 -3222
- package/dist/web/bundle/edges.js +4 -6
- package/dist/web/index.html +24 -6
- package/dist/web/src/agentmark.js +14 -0
- package/dist/web/src/artwork.js +1000 -0
- package/dist/web/src/bundles.js +151 -0
- package/dist/web/src/canvas.js +3694 -0
- package/dist/web/src/drag.js +61 -0
- package/dist/web/src/edgequeue.js +204 -0
- package/dist/web/src/gl/gl.js +123 -0
- package/dist/web/src/gl/renderer.js +451 -0
- package/dist/web/src/halo.js +95 -0
- package/dist/web/src/highlight.js +108 -0
- package/dist/web/src/overview.js +393 -0
- package/dist/web/src/reviewqueue.js +250 -0
- package/dist/web/src/scene.js +17 -0
- package/dist/web/src/selection.js +272 -0
- package/dist/web/src/status.js +80 -0
- package/dist/web/src/visitors.js +318 -0
- package/dist/web/src/visits.js +174 -0
- package/dist/web/style.css +255 -12
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/following.md +2 -2
- package/docs/agent-api/history.md +2 -1
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/docs/agent-api/views.md +3 -2
- package/package.json +3 -1
- package/test/toolchains.json +2 -2
package/dist/server/main.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// visualproof serve <path> — read a built Lean project and serve its declaration graph
|
|
3
3
|
// visualproof export <path> — the same page, frozen into one HTML file
|
|
4
4
|
import { historyAnswer } from "./historyapi.js";
|
|
5
|
+
import { gitSource } from "./history.js";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { createServer } from "node:http";
|
|
8
|
-
import { extname, join, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
9
11
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
10
|
-
import { WEB, bundleDir, bundleDirIn, ensureBundle, prepare, useBundleDir } from "./pipeline.js";
|
|
12
|
+
import { WEB, WEBFONTS, bundleDir, bundleDirIn, ensureBundle, prepare, useBundleDir } from "./pipeline.js";
|
|
13
|
+
import { makeArtwork, pngOf, projectView } from "./artcmd.js";
|
|
11
14
|
import { SiteBusy, cacheHome, dataHome, lockSite, openSite, projectDirs } from "./dirs.js";
|
|
12
15
|
import { ProjectError, openProject, targetsInWords } from "./project.js";
|
|
13
16
|
import { Live } from "./live.js";
|
|
@@ -15,8 +18,8 @@ import { ReviewStore } from "./review.js";
|
|
|
15
18
|
import { isDeclName, LeanHover } from "./hover.js";
|
|
16
19
|
import { Identities, layoutAuthor } from "./identity.js";
|
|
17
20
|
import { LAYOUT_VERSION } from "../shared/graph.js";
|
|
18
|
-
import { MAX_MEMORY_BYTES, Views } from "./views.js";
|
|
19
|
-
import { cleanView, defaultAgentName, validSession } from "../shared/collab.js";
|
|
21
|
+
import { MAX_MEMORY_BYTES, readProjectView, Views } from "./views.js";
|
|
22
|
+
import { cleanView, defaultAgentName, validSession, withoutArtwork } from "../shared/collab.js";
|
|
20
23
|
import { writeSnapshot } from "./snapshot.js";
|
|
21
24
|
import { lakeTextHash } from "./status.js";
|
|
22
25
|
import { invalidTemplate } from "../shared/editor.js";
|
|
@@ -48,6 +51,8 @@ function usage(code = 2) {
|
|
|
48
51
|
visualproof purge-file <lean-project> <sha256> [--site dir] [--cache dir]
|
|
49
52
|
visualproof export <lean-project> -o page.html [--label text] [--root Name]... [--graph saved.json]
|
|
50
53
|
[--jobs ${DEFAULT_JOBS}] [--site dir] [--cache dir]
|
|
54
|
+
visualproof artwork <lean-project> --recipe recipe.json [-o name] [--png] [--svg] [--root Name]...
|
|
55
|
+
[--site dir] [--cache dir] [--jobs ${DEFAULT_JOBS}] [--screen 1920x1200] [--graph saved.json]
|
|
51
56
|
visualproof doctor [<lean-project>] [--root Name]... [--site dir] [--cache dir]
|
|
52
57
|
visualproof --version
|
|
53
58
|
|
|
@@ -72,6 +77,17 @@ function usage(code = 2) {
|
|
|
72
77
|
the build; the project path still supplies the sources
|
|
73
78
|
export writes one self-contained HTML file: the page, its data and the sources, frozen
|
|
74
79
|
|
|
80
|
+
artwork makes the picture an artwork recipe says (the agents' API's, docs/agent-api/export.md:
|
|
81
|
+
the size, the look, names by a rule, the targets, the selection), as the page's
|
|
82
|
+
artwork mode makes it, without a browser: an SVG and a PNG (--png or --svg for one),
|
|
83
|
+
named by -o (a name, or a path without its extension), else the recipe's title, else
|
|
84
|
+
the project. With --site, the board as that site keeps it (its layout, pins and folds)
|
|
85
|
+
and its review's targets; without, the board as a new site would lay it out, and the
|
|
86
|
+
targets the recipe names. --screen: the screen the board is drawn on, as a reader's
|
|
87
|
+
(default 1920x1200, at least 640x480): how large names are in the picture. It reads
|
|
88
|
+
the site, never writes it; it writes the files and the cache's extraction. Anything
|
|
89
|
+
that fails ends it with status 1. Notes are not drawn yet.
|
|
90
|
+
|
|
75
91
|
--identity-header behind a proxy that authenticates (Tailscale, oauth2-proxy, …): take each
|
|
76
92
|
reader's name from this header instead of the name the page gives. Only use it when
|
|
77
93
|
the proxy is the only way to reach the server: the header is believed as it comes.
|
|
@@ -131,9 +147,9 @@ function parseArgs(argv) {
|
|
|
131
147
|
console.log(packageVersion());
|
|
132
148
|
process.exit(0);
|
|
133
149
|
}
|
|
134
|
-
if (cmd !== "serve" && cmd !== "export" && cmd !== "purge-file" && cmd !== "doctor")
|
|
150
|
+
if (cmd !== "serve" && cmd !== "export" && cmd !== "purge-file" && cmd !== "doctor" && cmd !== "artwork")
|
|
135
151
|
usage();
|
|
136
|
-
const o = { cmd, path: "", roots: [], port: 4173, host: "127.0.0.1", site: null, cache: null, graph: null, out: null, label: "VisualProof", identityHeader: null, editor: null, agents: "full", accessKeyFile: null, jobs: DEFAULT_JOBS, publicUrl: null, libraryLimit: DEFAULT_LIBRARY_LIMIT, sha: null, publicRead: false };
|
|
152
|
+
const o = { cmd, path: "", roots: [], port: 4173, host: "127.0.0.1", site: null, cache: null, graph: null, out: null, label: "VisualProof", identityHeader: null, editor: null, agents: "full", accessKeyFile: null, jobs: DEFAULT_JOBS, publicUrl: null, libraryLimit: DEFAULT_LIBRARY_LIMIT, sha: null, publicRead: false, recipe: null, png: false, svg: false, screen: { w: 1920, h: 1200 } };
|
|
137
153
|
for (let i = 0; i < rest.length; i++) {
|
|
138
154
|
const a = rest[i];
|
|
139
155
|
const val = () => rest[++i] ?? usage();
|
|
@@ -161,6 +177,18 @@ function parseArgs(argv) {
|
|
|
161
177
|
o.accessKeyFile = resolve(val());
|
|
162
178
|
else if (a === "--public")
|
|
163
179
|
o.publicRead = true;
|
|
180
|
+
else if (a === "--recipe" && cmd === "artwork")
|
|
181
|
+
o.recipe = resolve(val());
|
|
182
|
+
else if (a === "--png" && cmd === "artwork")
|
|
183
|
+
o.png = true;
|
|
184
|
+
else if (a === "--svg" && cmd === "artwork")
|
|
185
|
+
o.svg = true;
|
|
186
|
+
else if (a === "--screen" && cmd === "artwork") {
|
|
187
|
+
const m = /^(\d+)x(\d+)$/.exec(val());
|
|
188
|
+
if (!m || +m[1] < 640 || +m[2] < 480 || +m[1] > 10000 || +m[2] > 10000)
|
|
189
|
+
usage();
|
|
190
|
+
o.screen = { w: +m[1], h: +m[2] };
|
|
191
|
+
}
|
|
164
192
|
else if (a === "--public-url") {
|
|
165
193
|
const u = val();
|
|
166
194
|
if (!/^https?:\/\/[^\s/?#]+\/?$/.test(u)) {
|
|
@@ -196,7 +224,7 @@ function parseArgs(argv) {
|
|
|
196
224
|
else
|
|
197
225
|
usage();
|
|
198
226
|
}
|
|
199
|
-
if ((!o.path && cmd !== "doctor") || (cmd === "export" && !o.out))
|
|
227
|
+
if ((!o.path && cmd !== "doctor") || (cmd === "export" && !o.out) || (cmd === "artwork" && !o.recipe))
|
|
200
228
|
usage();
|
|
201
229
|
// read-only for whoever comes, beside those the key or a link lets in: without a key, everyone is in
|
|
202
230
|
// already; behind a sign-in, there is nobody else
|
|
@@ -221,6 +249,11 @@ export class App {
|
|
|
221
249
|
live;
|
|
222
250
|
hover;
|
|
223
251
|
ids;
|
|
252
|
+
/** how long a read of the project's git (GET /api/git) is kept, whoever asks: git is run at most
|
|
253
|
+
* once in that time, so what readers cost the server is bounded (CollabSpec decision 11); 0 in
|
|
254
|
+
* the tests */
|
|
255
|
+
gitFreshMs = 3000;
|
|
256
|
+
gitRead = null;
|
|
224
257
|
/** the agents' API (server/agent.ts), and what agents may change */
|
|
225
258
|
agents;
|
|
226
259
|
/** who may come in at all (server/access.ts); null: anyone who reaches the port */
|
|
@@ -265,6 +298,11 @@ export class App {
|
|
|
265
298
|
async export(req, res, url, agent, readOnly = false) {
|
|
266
299
|
const path = url.pathname;
|
|
267
300
|
const api = this.agents;
|
|
301
|
+
// the document's outline, which the linear form draws: nothing is typeset for it
|
|
302
|
+
if (path === "/api/export/outline") {
|
|
303
|
+
const o = api.exportOutline(url.searchParams.get("view") ?? "", url.searchParams.get("version") ?? undefined);
|
|
304
|
+
return sendJson(res, o.body, o.status, o.headers);
|
|
305
|
+
}
|
|
268
306
|
if (!this.exporter)
|
|
269
307
|
return sendJson(res, { error: "This server does not export documents." }, 404);
|
|
270
308
|
let a;
|
|
@@ -817,6 +855,9 @@ export class App {
|
|
|
817
855
|
return sendFile(req, res, join(WEB, "index.html"));
|
|
818
856
|
if (path === "/style.css")
|
|
819
857
|
return sendFile(req, res, join(WEB, "style.css"));
|
|
858
|
+
// the fonts an artwork is drawn in (ExportSpec decision 19)
|
|
859
|
+
if (FONT_PATH.test(path))
|
|
860
|
+
return sendFile(req, res, join(WEBFONTS, path.slice("/fonts/".length)));
|
|
820
861
|
// the page, and its workers: the one that lays out the Time Machine's past graphs
|
|
821
862
|
// (web/src/layoutworker.ts), the one that chooses the overview's edges (web/src/edgeworker.ts),
|
|
822
863
|
// and pdf.js's, which reads the library's PDFs for the reference window (AttachmentSpec decision 11)
|
|
@@ -937,8 +978,9 @@ export class App {
|
|
|
937
978
|
// its own event stream only, by the secret the stream was given (`hello`)
|
|
938
979
|
const name = this.ids.who(req);
|
|
939
980
|
const priv = !agent && b.private === true;
|
|
981
|
+
// agents have no artwork (ExportSpec decision 19): the page draws it
|
|
940
982
|
if (agent && name)
|
|
941
|
-
live.presence.agentBeat(name, cleanView(b.view));
|
|
983
|
+
live.presence.agentBeat(name, withoutArtwork(cleanView(b.view)));
|
|
942
984
|
else if (!live.beat(b.token, priv ? null : name, priv ? null : cleanView(b.view), !priv && b.idle === true, priv, !!this.ids.person(req)))
|
|
943
985
|
return sendJson(res, { error: "This heartbeat is not from an open event stream of this page's: it is sent with the token the stream says hello with.", stream: false }, 403);
|
|
944
986
|
return sendJson(res, { ok: true });
|
|
@@ -967,6 +1009,13 @@ export class App {
|
|
|
967
1009
|
const a = this.agents.undo(me, agent, r.body?.id);
|
|
968
1010
|
return sendJson(res, a.body, a.status);
|
|
969
1011
|
}
|
|
1012
|
+
// the project's git source and commit as git says them now, not as the build was read (an
|
|
1013
|
+
// artwork's foot, ExportSpec decision 19; the owner's decision of 2026-10-15)
|
|
1014
|
+
if (path === "/api/git") {
|
|
1015
|
+
if (!this.gitRead || Date.now() - this.gitRead.at >= this.gitFreshMs)
|
|
1016
|
+
this.gitRead = { git: gitSource(live.project.root), at: Date.now() };
|
|
1017
|
+
return sendJson(res, { git: this.gitRead.git });
|
|
1018
|
+
}
|
|
970
1019
|
if (path === "/api/meta" || path === "/api/edges") {
|
|
971
1020
|
const g = live.graph;
|
|
972
1021
|
if (!g)
|
|
@@ -1080,7 +1129,10 @@ export class App {
|
|
|
1080
1129
|
}
|
|
1081
1130
|
}
|
|
1082
1131
|
}
|
|
1132
|
+
/** an artwork's font file (assets/webfonts/) */
|
|
1133
|
+
const FONT_PATH = /^\/fonts\/(source-sans-3|source-serif-4|jost)-[a-z-]+\.woff2$/;
|
|
1083
1134
|
const TYPES = {
|
|
1135
|
+
".woff2": "font/woff2",
|
|
1084
1136
|
".html": "text/html; charset=utf-8",
|
|
1085
1137
|
".css": "text/css; charset=utf-8",
|
|
1086
1138
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -1282,8 +1334,10 @@ async function main() {
|
|
|
1282
1334
|
console.log(printReport(report, o.path || null));
|
|
1283
1335
|
process.exit(report.checks.some((c) => c.level === "fail") ? 1 : 0);
|
|
1284
1336
|
}
|
|
1285
|
-
// a live server outlives any one failed task: log it and keep serving
|
|
1286
|
-
|
|
1337
|
+
// a live server outlives any one failed task: log it and keep serving (a picture is no server:
|
|
1338
|
+
// whatever fails ends it, with status 1)
|
|
1339
|
+
if (o.cmd !== "artwork")
|
|
1340
|
+
process.on("unhandledRejection", (e) => console.error("unhandled:", e instanceof Error ? e.message : e));
|
|
1287
1341
|
let project;
|
|
1288
1342
|
try {
|
|
1289
1343
|
project = openProject(o.path, o.roots, o.graph === null);
|
|
@@ -1295,6 +1349,9 @@ async function main() {
|
|
|
1295
1349
|
}
|
|
1296
1350
|
throw e;
|
|
1297
1351
|
}
|
|
1352
|
+
// a picture: the site read, never made, locked or written (ExportSpec decision 21)
|
|
1353
|
+
if (o.cmd === "artwork")
|
|
1354
|
+
return artworkCommand(project, o);
|
|
1298
1355
|
const dirs = projectDirs(project.root, o.site, o.cache);
|
|
1299
1356
|
openSite(dirs.site);
|
|
1300
1357
|
mkdirSync(dirs.cache, { recursive: true });
|
|
@@ -1389,6 +1446,89 @@ async function main() {
|
|
|
1389
1446
|
});
|
|
1390
1447
|
live.start();
|
|
1391
1448
|
}
|
|
1449
|
+
/** The artwork command (ExportSpec decision 21): the build read as an export reads it, the picture a
|
|
1450
|
+
* recipe says made of it (server/artcmd.ts), its files written; what it named said as the agents'
|
|
1451
|
+
* API says it; problems said, and exit status 1. */
|
|
1452
|
+
async function artworkCommand(project, o) {
|
|
1453
|
+
const fail = (why) => {
|
|
1454
|
+
console.error(`visualproof artwork: ${why}`);
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
};
|
|
1457
|
+
try {
|
|
1458
|
+
await artwork(project, o, fail);
|
|
1459
|
+
}
|
|
1460
|
+
catch (e) {
|
|
1461
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function artwork(project, o, fail) {
|
|
1465
|
+
const dirs = projectDirs(project.root, o.site, o.cache);
|
|
1466
|
+
const site = o.site ? dirs.site : null;
|
|
1467
|
+
if (site && !existsSync(site))
|
|
1468
|
+
fail(`there is no site at ${site}`);
|
|
1469
|
+
let recipe;
|
|
1470
|
+
try {
|
|
1471
|
+
recipe = JSON.parse(readFileSync(o.recipe, "utf8"));
|
|
1472
|
+
}
|
|
1473
|
+
catch (e) {
|
|
1474
|
+
fail(`the recipe ${o.recipe} cannot be read: ${e instanceof Error ? e.message : String(e)}`);
|
|
1475
|
+
}
|
|
1476
|
+
// -o: a name, or a path without its extension, in a folder that is there
|
|
1477
|
+
const outBase = o.out ? o.out.replace(/\.(png|svg)$/i, "") : null;
|
|
1478
|
+
const dir = outBase ? dirname(outBase) : process.cwd();
|
|
1479
|
+
if (!existsSync(dir))
|
|
1480
|
+
fail(`there is no folder ${dir} to write the files in`);
|
|
1481
|
+
mkdirSync(dirs.cache, { recursive: true });
|
|
1482
|
+
const log = (l) => console.log(l);
|
|
1483
|
+
let ex = o.graph ? undefined : await extract(project, dirs, log);
|
|
1484
|
+
if (ex && o.jobs > 0) {
|
|
1485
|
+
const pass = new UsagePass(project, dirs, o.jobs, log);
|
|
1486
|
+
const plan = pass.plan(ex.graph);
|
|
1487
|
+
if (plan.todo.length)
|
|
1488
|
+
log(`usage pass: elaborating ${plan.todo.length} of ${plan.modules.length} modules, ${o.jobs} at a time`);
|
|
1489
|
+
const results = await pass.run(plan);
|
|
1490
|
+
if (results) {
|
|
1491
|
+
ex = mergeUsage(ex, plan, results, pass.status.failed, pass.status.ms ?? 0);
|
|
1492
|
+
saveMerged(dirs.cache, ex);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// the site's layout memory read and never written, nor the views laid out written to the cache;
|
|
1496
|
+
// without a site, none read: the board as a new site lays it out
|
|
1497
|
+
const prep = await prepare(project, dirs, o.graph, log, { ...(ex ? { extraction: ex } : {}), readOnlySite: true, ...(site ? {} : { freshLayout: true }) });
|
|
1498
|
+
const kept = site ? readProjectView(site, LAYOUT_VERSION) : { view: null };
|
|
1499
|
+
if (kept.why)
|
|
1500
|
+
console.error(`visualproof artwork: the site's views cannot be read (${kept.why}): the Project view as a new site has it`);
|
|
1501
|
+
const review = site ? new ReviewStore(project.root, site).data : null;
|
|
1502
|
+
const a = makeArtwork(prep, review, projectView(kept.view), { recipe, screen: o.screen, name: outBase ? basename(outBase) : null, root: project.root });
|
|
1503
|
+
if (!("svg" in a)) {
|
|
1504
|
+
for (const p of a.problems)
|
|
1505
|
+
console.error(`visualproof artwork: ${p}`);
|
|
1506
|
+
process.exit(1);
|
|
1507
|
+
}
|
|
1508
|
+
const list = (xs) => (xs.length ? xs.join(", ") : "none");
|
|
1509
|
+
console.log(`names written: ${list(a.names.written)}`);
|
|
1510
|
+
if (a.names.ruledSkipped.length)
|
|
1511
|
+
console.log(`names a rule chose, left out where they would cover another: ${list(a.names.ruledSkipped)}`);
|
|
1512
|
+
if (a.names.left)
|
|
1513
|
+
console.log(`names or targets left out for room: ${a.names.left}`);
|
|
1514
|
+
console.log(`targets marked: ${list(a.targets)}`);
|
|
1515
|
+
console.log(`selection: ${a.selection ? `${a.selection.mode} of ${a.selection.name}` : "none"}`);
|
|
1516
|
+
if (a.notesLeftOut)
|
|
1517
|
+
console.log(`notes: ${a.notesLeftOut} on the board, not drawn (the command draws no notes yet)`);
|
|
1518
|
+
const both = !o.png && !o.svg;
|
|
1519
|
+
const mb = (n) => `${(n / 1e6).toFixed(1)} MB`;
|
|
1520
|
+
if (both || o.svg) {
|
|
1521
|
+
const file = join(dir, a.file("svg"));
|
|
1522
|
+
writeFileSync(file, a.svg);
|
|
1523
|
+
console.log(`wrote ${file} (${mb(Buffer.byteLength(a.svg))})`);
|
|
1524
|
+
}
|
|
1525
|
+
if (both || o.png) {
|
|
1526
|
+
const png = await pngOf(a.svg);
|
|
1527
|
+
const file = join(dir, a.file("png"));
|
|
1528
|
+
writeFileSync(file, png);
|
|
1529
|
+
console.log(`wrote ${file} (${mb(png.length)}, ${a.look.w} × ${a.look.h})`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1392
1532
|
// run as the command (`node src/server/main.ts`, the `visualproof` bin), not when a test imports App
|
|
1393
1533
|
if (import.meta.main)
|
|
1394
1534
|
void main();
|
package/dist/server/mcptools.js
CHANGED
|
@@ -149,7 +149,7 @@ export const TOOLS = [
|
|
|
149
149
|
default: "move",
|
|
150
150
|
actions: {
|
|
151
151
|
move: { method: "POST", path: "/api/agent/view", says: "go to a view, a target's view, a declaration, its source; fields left out keep their value", args: ["view", "review", "sel", "selection", "mode", "source", "paper", "focus", "leave"] },
|
|
152
|
-
link: { method: "POST", path: "/api/agent/view/link", says: "a link that opens exactly what you look at, or a place you describe, to hand people in a reply", args: ["view", "review", "sel", "selection", "mode", "source", "paper", "focus", "sourceCollapsed", "pinned", "history", "region", "activity", "lostFound", "scheme", "edges", "
|
|
152
|
+
link: { method: "POST", path: "/api/agent/view/link", says: "a link that opens exactly what you look at, or a place you describe, to hand people in a reply", args: ["view", "review", "sel", "selection", "mode", "source", "paper", "focus", "sourceCollapsed", "pinned", "history", "region", "activity", "lostFound", "scheme", "edges", "exact"] },
|
|
153
153
|
ask_follow: { method: "POST", path: "/api/agent/ask-follow", says: "ask the people on the page to follow you first", args: ["who", "wait"] },
|
|
154
154
|
},
|
|
155
155
|
args: {
|
|
@@ -157,7 +157,7 @@ export const TOOLS = [
|
|
|
157
157
|
review: str("a target: be on its view"),
|
|
158
158
|
sel: str("the declaration you look at"),
|
|
159
159
|
selection: str("what you select, as a query: decl(A.b) or tcb(A.c)"),
|
|
160
|
-
mode: str("how you select: plain; Rests on's deps, tcb, keys; apps", ["plain", "deps", "tcb", "keys", "apps"]),
|
|
160
|
+
mode: str("how you select: plain; Rests on's deps, tcb, keys, refs (what it uses directly); apps", ["plain", "deps", "tcb", "keys", "refs", "apps"]),
|
|
161
161
|
source: bool("the source window open on sel"),
|
|
162
162
|
paper: PAPER,
|
|
163
163
|
focus: str("your focus on the view: a target, \"none\", or \"view\" for the view's own"),
|
|
@@ -170,7 +170,6 @@ export const TOOLS = [
|
|
|
170
170
|
lostFound: bool("link: the lost-and-found open"),
|
|
171
171
|
scheme: str("link: the colouring", ["kind", "status"]),
|
|
172
172
|
edges: bool("link: edges drawn"),
|
|
173
|
-
proofEdges: bool("link: proof edges drawn"),
|
|
174
173
|
exact: bool("link: false for the view's address alone"),
|
|
175
174
|
who: str("ask_follow: a person's name; left out, everyone"),
|
|
176
175
|
wait: bool("ask_follow: false answers at once (default: wait for them, at most 30 s)"),
|
|
@@ -374,12 +373,13 @@ export const TOOLS = [
|
|
|
374
373
|
},
|
|
375
374
|
{
|
|
376
375
|
name: "export",
|
|
377
|
-
about: "A view's document — every declaration of the project its board holds, in its sections and order: its model as JSON, or typeset as a PDF and saved on your machine.",
|
|
376
|
+
about: "A view's document — every declaration of the project its board holds, in its sections and order: its model as JSON, or typeset as a PDF and saved on your machine. Or a picture of the board: a link that opens it in the artwork mode as a recipe says.",
|
|
378
377
|
actions: {
|
|
379
378
|
model: { method: "GET", path: "/api/agent/export", says: "the document's model", args: ["view", "settings", "version"] },
|
|
380
379
|
typeset: { method: "POST", path: "/api/agent/export", says: "start typesetting: a job", args: ["view", "settings", "version"] },
|
|
381
380
|
job: { method: "GET", path: "/api/agent/export/job", says: "how the job is doing", args: ["id"] },
|
|
382
381
|
file: { method: "GET", path: "/api/agent/export/file", saves: true, says: "a file of it saved on your machine", args: ["id", "what", "saveTo"] },
|
|
382
|
+
artwork: { method: "POST", path: "/api/agent/artwork", says: "a link to a picture of the board, as a recipe says", args: ["recipe", "save"] },
|
|
383
383
|
},
|
|
384
384
|
args: {
|
|
385
385
|
view: str("the view's id: a target's Key view is open on key(<target>)"),
|
|
@@ -388,6 +388,8 @@ export const TOOLS = [
|
|
|
388
388
|
id: str("the job's id"),
|
|
389
389
|
what: str("pdf (default), tex, sty or zip", ["pdf", "tex", "sty", "zip"]),
|
|
390
390
|
saveTo: SAVE_TO,
|
|
391
|
+
recipe: { type: "object", description: "the picture: size, orientation, style, theme, names (e.g. {clusters: 1}), targets, selection {name, mode}, title, subtitle, edges… (docs: export)" },
|
|
392
|
+
save: str("png, svg or png,svg: saved by the page once ready"),
|
|
391
393
|
},
|
|
392
394
|
},
|
|
393
395
|
];
|
package/dist/server/pastdoc.js
CHANGED
|
@@ -14,7 +14,7 @@ import { pastGraph } from "../shared/pastgraph.js";
|
|
|
14
14
|
import { pageGraph } from "./history.js";
|
|
15
15
|
import { MOST_DIFF, snapshotDiff } from "./historyapi.js";
|
|
16
16
|
/** how many past builds are kept: a past graph and the texts read for a large view are large on a
|
|
17
|
-
* large project (
|
|
17
|
+
* large project (Sphere-Packing-Lean's past graph: 13.7 MB of JSON) */
|
|
18
18
|
const KEEP = 2;
|
|
19
19
|
export class PastBuilds {
|
|
20
20
|
w;
|
package/dist/server/pipeline.js
CHANGED
|
@@ -13,6 +13,9 @@ import { buildViewOffThread } from "./view.js";
|
|
|
13
13
|
import { cacheHome } from "./dirs.js";
|
|
14
14
|
/** the tool's own folder, `src/`; the repository is above it */
|
|
15
15
|
export const SRC = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
/** the fonts the page draws an artwork's names and title in (ExportSpec decision 19): Source Sans 3
|
|
17
|
+
* and Source Serif 4, their Latin and Greek, and Jost, its Latin, under the SIL Open Font License */
|
|
18
|
+
export const WEBFONTS = join(SRC, "assets", "webfonts");
|
|
16
19
|
export const REPO = resolve(SRC, "..");
|
|
17
20
|
export const WEB = join(SRC, "web");
|
|
18
21
|
/** The page's bundle made when the package was built (scripts/build.mjs), beside the page: an
|
|
@@ -38,8 +41,9 @@ export function bundleDir() {
|
|
|
38
41
|
* views from an older server are rebuilt rather than served. 7: a notation unit's statement is
|
|
39
42
|
* its command's text. 8: notation units carry their Lean constant, display form, what they
|
|
40
43
|
* interpret and their rules. 9: a node's hash is its version, beside the kernel's
|
|
41
|
-
* (server/versions.ts).
|
|
42
|
-
|
|
44
|
+
* (server/versions.ts). 10: the project's git source and commit (server/history.ts, gitSource). 11:
|
|
45
|
+
* not them any more: they are asked when wanted (GET /api/git), not kept with the view. */
|
|
46
|
+
const VIEW_VERSION = 11;
|
|
43
47
|
function loadMemory(siteDir) {
|
|
44
48
|
try {
|
|
45
49
|
const mem = JSON.parse(readFileSync(join(siteDir, "layout-memory.json"), "utf8"));
|
|
@@ -60,7 +64,9 @@ export async function prepare(project, dirs, saved, log, opts = {}) {
|
|
|
60
64
|
// `reuse`: lay out an extraction already held, without reading the build (which may be
|
|
61
65
|
// half-written right now); `extraction`: one in hand (the usage pass's, merged); `provisional`:
|
|
62
66
|
// the graph waits for its usage pass; `completes`: the fingerprint of the build's extraction that
|
|
63
|
-
// this graph, the usage pass's merged, completes
|
|
67
|
+
// this graph, the usage pass's merged, completes; `readOnlySite`: the site's layout memory read,
|
|
68
|
+
// never written, and the view laid out not kept in the cache either, which a server would take for
|
|
69
|
+
// its site's (the artwork command, ExportSpec decision 21)
|
|
64
70
|
const held = opts.extraction ?? (opts.reuse ? cachedExtraction(dirs.cache, opts.reuse) : null);
|
|
65
71
|
if (opts.reuse && !held)
|
|
66
72
|
throw new Error("the graph on screen is no longer cached; it is laid out afresh at the next build");
|
|
@@ -69,7 +75,6 @@ export async function prepare(project, dirs, saved, log, opts = {}) {
|
|
|
69
75
|
log(ex.cached ? `extraction ${ex.fingerprint} reused` : `extraction ${ex.fingerprint} done`);
|
|
70
76
|
const usage = ex.graph.usage ?? null;
|
|
71
77
|
const viewDir = join(dirs.cache, "view");
|
|
72
|
-
mkdirSync(viewDir, { recursive: true });
|
|
73
78
|
const metaFile = join(viewDir, `${ex.fingerprint}-v${VIEW_VERSION}.meta.json.gz`);
|
|
74
79
|
const edgeFile = join(viewDir, `${ex.fingerprint}-v${VIEW_VERSION}.edges.bin.gz`);
|
|
75
80
|
const memory = opts.freshLayout ? null : loadMemory(dirs.site);
|
|
@@ -84,13 +89,17 @@ export async function prepare(project, dirs, saved, log, opts = {}) {
|
|
|
84
89
|
const completing = memory !== null && opts.completes !== undefined && memory.fingerprint === opts.completes && memory.before !== undefined;
|
|
85
90
|
const against = completing ? (memory.before ?? null) : (memory?.layouts ?? null);
|
|
86
91
|
const view = await buildViewOffThread(project, ex, log, against);
|
|
87
|
-
|
|
92
|
+
if (!opts.readOnlySite)
|
|
93
|
+
saveMemory(dirs.site, ex.fingerprint, view.meta, opts.provisional ? against : undefined);
|
|
88
94
|
const metaGz = gzipSync(JSON.stringify(view.meta));
|
|
89
95
|
const edgesGz = gzipSync(Buffer.from(view.edges.buffer, view.edges.byteOffset, view.edges.byteLength));
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
if (!opts.readOnlySite) {
|
|
97
|
+
mkdirSync(viewDir, { recursive: true });
|
|
98
|
+
writeFileSync(metaFile, metaGz);
|
|
99
|
+
writeFileSync(edgeFile, edgesGz);
|
|
100
|
+
prune(viewDir, /\.meta\.json\.gz$/, 3);
|
|
101
|
+
prune(viewDir, /\.edges\.bin\.gz$/, 3);
|
|
102
|
+
}
|
|
94
103
|
return { fingerprint: ex.fingerprint, cached: ex.cached, metaGz, edgesGz, usage };
|
|
95
104
|
}
|
|
96
105
|
function readdirRec(dir) {
|
|
@@ -7,12 +7,12 @@ export function readOnlyAllows(method, path) {
|
|
|
7
7
|
const read = method === "GET" || method === "HEAD";
|
|
8
8
|
if (read) {
|
|
9
9
|
// the page and its files
|
|
10
|
-
if (path === "/" || path === "/index.html" || path === "/style.css" || /^\/(app|layout|edges|pdfworker)\.js(\.map)?$/.test(path) || path.startsWith("/pdfjs/"))
|
|
10
|
+
if (path === "/" || path === "/index.html" || path === "/style.css" || /^\/(app|layout|edges|pdfworker)\.js(\.map)?$/.test(path) || path.startsWith("/pdfjs/") || path.startsWith("/fonts/"))
|
|
11
11
|
return true;
|
|
12
12
|
// the graph, its sources and Lean's answers, the review, what happened, the views, the history,
|
|
13
13
|
// the library, the printed document's state and files, who the reader is and the link they
|
|
14
14
|
// came by, and the event stream every change comes by
|
|
15
|
-
if (["/api/status", "/api/meta", "/api/edges", "/api/source", "/api/review", "/api/activity", "/api/identity", "/api/share"].includes(path))
|
|
15
|
+
if (["/api/status", "/api/meta", "/api/edges", "/api/git", "/api/source", "/api/review", "/api/activity", "/api/identity", "/api/share"].includes(path))
|
|
16
16
|
return true;
|
|
17
17
|
// a stream is opened by GET alone: a HEAD would take one of their places for nothing
|
|
18
18
|
if (path === "/api/events")
|
package/dist/server/review.js
CHANGED
|
@@ -263,7 +263,10 @@ export class ReviewStore {
|
|
|
263
263
|
this.refresh();
|
|
264
264
|
const file = logFileName(name, false, person);
|
|
265
265
|
const now = Object.hasOwn(this.data.people, person) ? this.data.people[person].name : null;
|
|
266
|
-
|
|
266
|
+
// one who has no log yet, but whom opinions were recorded for (ReviewSpec decision 8), is
|
|
267
|
+
// given one by the rename, so that those follow it
|
|
268
|
+
const spokenFor = Object.values(this.data.opinions).some((ops) => ops.some((o) => o.person === person && o.recordedBy));
|
|
269
|
+
if ((!this.files.has(file) && !spokenFor) || now === name.trim())
|
|
267
270
|
return { change: null, unchanged: true };
|
|
268
271
|
return this.write(file, () => nameEvent(person, name, this.nextTime()));
|
|
269
272
|
}
|
package/dist/server/reviewapi.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// Every refusal is an answer: a status, and words that say what to do instead.
|
|
18
18
|
import { readFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
+
import { MAX_NAME, sameName, validName } from "../shared/collab.js";
|
|
20
21
|
import { exactContext, judgedText } from "../shared/context.js";
|
|
21
22
|
import { MAX_TEXT, STANDARD_AXIOMS, validPrefix } from "../shared/review.js";
|
|
22
23
|
import { reviewSet } from "../shared/reviewqueue.js";
|
|
@@ -102,6 +103,9 @@ export class ReviewApi {
|
|
|
102
103
|
return answer(refuse(400, "The comment is longer than 10,000 characters.", "Say it shorter."));
|
|
103
104
|
if (b.verdict === "comment" && !comment)
|
|
104
105
|
return answer(refuse(400, "A comment needs text.", "Write it in the box, then Comment."));
|
|
106
|
+
const voice = this.voice(b.for, who);
|
|
107
|
+
if ("status" in voice)
|
|
108
|
+
return answer(voice);
|
|
105
109
|
const v = this.graph();
|
|
106
110
|
if ("status" in v)
|
|
107
111
|
return answer(v);
|
|
@@ -109,7 +113,7 @@ export class ReviewApi {
|
|
|
109
113
|
if ("status" in x)
|
|
110
114
|
return answer(x);
|
|
111
115
|
// an agent's opinion is advice, whatever its level (AgentSpec decision 2)
|
|
112
|
-
return this.record({ op: "opinion", name: x.name, by, verdict: b.verdict, ...(comment ? { comment } : {}), ...(x.hash ? { hash: x.hash } : {}), ...(x.text !== undefined ? { text: x.text } : x.statement !== undefined ? { statement: x.statement } : {}), ...agent }, who);
|
|
116
|
+
return this.record({ op: "opinion", name: x.name, by, verdict: b.verdict, ...(comment ? { comment } : {}), ...(x.hash ? { hash: x.hash } : {}), ...(x.text !== undefined ? { text: x.text } : x.statement !== undefined ? { statement: x.statement } : {}), ...voice, ...agent }, who);
|
|
113
117
|
}
|
|
114
118
|
case "target": {
|
|
115
119
|
if (typeof b.on !== "boolean")
|
|
@@ -173,6 +177,36 @@ export class ReviewApi {
|
|
|
173
177
|
return answer(refuse(400, "op is opinion, target, trust or key.", "Send {op, …}."));
|
|
174
178
|
}
|
|
175
179
|
}
|
|
180
|
+
/** Whose an opinion is to be (ReviewSpec decision 8): the asker's own where `asked` is absent or
|
|
181
|
+
* names them; else another person's, recorded for them — by their link's id where `asked` is an
|
|
182
|
+
* invited person's name, a guest's as the logs write it — and never an agent's, nor by an agent. */
|
|
183
|
+
voice(asked, who) {
|
|
184
|
+
if (asked === undefined)
|
|
185
|
+
return {};
|
|
186
|
+
if (who.agent)
|
|
187
|
+
return refuse(403, "An agent records no opinion for anyone else.", "Leave out for: your opinion is your own, and advice.");
|
|
188
|
+
if (typeof asked !== "string" || !validName(asked.trim()))
|
|
189
|
+
return refuse(400, `for is the name of the person the opinion is recorded for: 1–${MAX_NAME} characters, no line breaks.`, "Send {op: \"opinion\", name, verdict, for: \"<their name>\"}.");
|
|
190
|
+
const name = asked.trim();
|
|
191
|
+
const held = this.world.invited?.(name);
|
|
192
|
+
// one's own name, or one's own link: one's own opinion
|
|
193
|
+
if (held ? held.id === who.person : sameName(name, who.name.trim()))
|
|
194
|
+
return {};
|
|
195
|
+
// the writers the logs name, people's and agents', in any case
|
|
196
|
+
const d = this.world.review.data;
|
|
197
|
+
const writers = [
|
|
198
|
+
...Object.values(d.opinions).flatMap((ops) => ops.map((o) => ({ by: o.by, agent: !!o.agent, person: !!o.person }))),
|
|
199
|
+
...Object.values(d.targets).map((t) => ({ by: t.by, agent: !!t.agent, person: !!t.person })),
|
|
200
|
+
...Object.values(d.keys).flatMap((marks) => Object.values(marks).map((m) => ({ by: m.by, agent: !!m.agent, person: !!m.person }))),
|
|
201
|
+
];
|
|
202
|
+
if (writers.some((w) => w.agent && sameName(w.by, name)))
|
|
203
|
+
return refuse(400, `${name} is an agent: nobody records an opinion for an agent.`, "Record it for a person, or as your own.");
|
|
204
|
+
if (held)
|
|
205
|
+
return { for: held.name, forPerson: held.id };
|
|
206
|
+
// a guest already in the logs, as they write their name: "bob" is Bob
|
|
207
|
+
const guest = writers.find((w) => !w.agent && !w.person && sameName(w.by, name));
|
|
208
|
+
return { for: guest?.by ?? name };
|
|
209
|
+
}
|
|
176
210
|
/** Append a writer's event to their log; the answer carries the change, which the page applies
|
|
177
211
|
* at once, and the event as written. */
|
|
178
212
|
record(op, who) {
|
package/dist/server/snapshot.js
CHANGED
|
@@ -3,12 +3,20 @@
|
|
|
3
3
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
6
|
-
import { WEB, bundleDir } from "./pipeline.js";
|
|
6
|
+
import { WEB, WEBFONTS, bundleDir } from "./pipeline.js";
|
|
7
|
+
import { gitSource } from "./history.js";
|
|
7
8
|
import { lakeTextHash } from "./status.js";
|
|
8
9
|
/** Text that sits inside a <script> element must not close it. */
|
|
9
10
|
function scriptSafe(s) {
|
|
10
11
|
return s.replace(/<\/script/gi, "<\\/script");
|
|
11
12
|
}
|
|
13
|
+
/** A snapshot's graph: the view's, with the project's git source and commit as the snapshot is
|
|
14
|
+
* taken, since a snapshot has no server to ask (GET /api/git); as it was where the project is no
|
|
15
|
+
* git repository. */
|
|
16
|
+
export function withGit(root, meta, metaGz) {
|
|
17
|
+
const git = gitSource(root);
|
|
18
|
+
return git ? gzipSync(JSON.stringify({ ...meta, project: { ...meta.project, git } })) : metaGz;
|
|
19
|
+
}
|
|
12
20
|
export function writeSnapshot(project, prep, out, label, review, views) {
|
|
13
21
|
const meta = JSON.parse(gunzipSync(prep.metaGz).toString("utf8"));
|
|
14
22
|
// the sources a card can show: every project module's file, with the usage pass's contexts where
|
|
@@ -28,13 +36,15 @@ export function writeSnapshot(project, prep, out, label, review, views) {
|
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
const b64 = (buf) => buf.toString("base64");
|
|
31
|
-
const
|
|
39
|
+
const metaGz = withGit(project.root, meta, prep.metaGz);
|
|
40
|
+
// the artwork's fonts within the page, which has no server to ask for them
|
|
41
|
+
const css = readFileSync(join(WEB, "style.css"), "utf8").replace(/url\("\/fonts\/([a-z0-9-]+\.woff2)"\)/g, (_, f) => `url("data:font/woff2;base64,${b64(readFileSync(join(WEBFONTS, f)))}")`);
|
|
32
42
|
const js = readFileSync(join(bundleDir(), "app.js"), "utf8").replace(/\n\/\/# sourceMappingURL=.*$/m, "");
|
|
33
43
|
const info = { createdAt: new Date().toISOString(), label };
|
|
34
44
|
const html = readFileSync(join(WEB, "index.html"), "utf8")
|
|
35
45
|
.replace(`<link rel="stylesheet" href="/style.css">`, () => `<style>\n${css}\n</style>`)
|
|
36
46
|
.replace(`<script type="module" src="/app.js"></script>`, () => `<script id="vp-static" type="application/json">${scriptSafe(JSON.stringify(info))}</script>\n` +
|
|
37
|
-
`<script id="vp-meta" type="application/octet-stream">${b64(
|
|
47
|
+
`<script id="vp-meta" type="application/octet-stream">${b64(metaGz)}</script>\n` +
|
|
38
48
|
`<script id="vp-edges" type="application/octet-stream">${b64(prep.edgesGz)}</script>\n` +
|
|
39
49
|
`<script id="vp-sources" type="application/octet-stream">${b64(gzipSync(JSON.stringify(sources)))}</script>\n` +
|
|
40
50
|
// the review as it stood: shown, not changed (ReviewSpec decision 7)
|
package/dist/server/usage.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// One process per module: a process cannot import twice. `jobs` run at once; import is bound by
|
|
13
13
|
// memory bandwidth, and each worker holds hundreds of MB of its own besides the `.olean` files they
|
|
14
|
-
// all map (on
|
|
14
|
+
// all map (on Sphere-Packing-Lean about 150 MB of its own, 1.2 to 1.6 GB resident with them). A module's result is cached in the
|
|
15
15
|
// project's cache directory under a key
|
|
16
16
|
// made of its `.olean` hash, the text it was compiled from, the options Lake compiled it with, and
|
|
17
17
|
// the keys of the project modules it imports: after a build only the modules whose key changed run
|
|
@@ -25,7 +25,7 @@ import { SITE_BODY, SITE_GRAMMAR, SITE_PROOF, SITE_TCB } from "../shared/graph.j
|
|
|
25
25
|
import { USAGE, USAGE_PROGRAM, leanProgram, prune } from "./extract.js";
|
|
26
26
|
import { compiledTextHash, lakeTextHash } from "./status.js";
|
|
27
27
|
/** How many workers run at once unless `--jobs` says otherwise: past about a quarter of the cores,
|
|
28
|
-
* more workers gain less and less (
|
|
28
|
+
* more workers gain less and less (Sphere-Packing-Lean's cold pass on 32 cores: 320 s with 4, 195 s with
|
|
29
29
|
* 8, 148 s with 16). */
|
|
30
30
|
export const DEFAULT_JOBS = 8;
|
|
31
31
|
/** A worker still running after this long is stopped, and its module reported as failed. */
|
package/dist/server/views.js
CHANGED
|
@@ -59,7 +59,7 @@ const HOLD_MS = 60_000;
|
|
|
59
59
|
/** the most views one private page holds */
|
|
60
60
|
const MAX_HOLDS = 100;
|
|
61
61
|
/** The most a board's layout memory may be when a page hands it over: room for a board of a large
|
|
62
|
-
* project's declarations many times over (
|
|
62
|
+
* project's declarations many times over (Sphere-Packing-Lean's whole board, 7,851 declarations, is
|
|
63
63
|
* 1.8 MB; 50 MB is the owner's decision of 2026-10-14). */
|
|
64
64
|
export const MAX_MEMORY_BYTES = 50 << 20;
|
|
65
65
|
const isPins = (p) => {
|
|
@@ -161,6 +161,30 @@ function cleanMemory(x) {
|
|
|
161
161
|
return null;
|
|
162
162
|
return { set: m.set, mem: { nodes, clusters } };
|
|
163
163
|
}
|
|
164
|
+
/** The Project view as a site keeps it — its pins, folds, hidden levels and clustering — read and
|
|
165
|
+
* nothing written (the artwork command, ExportSpec decision 21; the Views class sets aside a file
|
|
166
|
+
* it cannot read): null where the site keeps none, or `why` where its views cannot be read. Pins
|
|
167
|
+
* dropped under another way of laying out mean nothing in this one, as Views reads them. */
|
|
168
|
+
export function readProjectView(siteDir, layoutVersion) {
|
|
169
|
+
const file = join(siteDir, "views.json");
|
|
170
|
+
if (!existsSync(file))
|
|
171
|
+
return { view: null };
|
|
172
|
+
try {
|
|
173
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
174
|
+
for (const r of Array.isArray(raw.views) ? raw.views : []) {
|
|
175
|
+
const v = readView(r);
|
|
176
|
+
if (typeof v === "string" || v.id !== PROJECT)
|
|
177
|
+
continue;
|
|
178
|
+
if (raw.layoutVersion !== layoutVersion)
|
|
179
|
+
v.pins = emptyLayouts();
|
|
180
|
+
return { view: v };
|
|
181
|
+
}
|
|
182
|
+
return { view: null };
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
return { view: null, why: e instanceof Error ? e.message : String(e) };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
164
188
|
export class Views {
|
|
165
189
|
book;
|
|
166
190
|
file;
|