visualproof 0.0.0-stage → 0.2.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 (146) 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/assets/webfonts/LICENSE-Jost.txt +93 -0
  9. package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
  10. package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
  11. package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
  12. package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
  13. package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
  14. package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
  15. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
  16. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
  17. package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  18. package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  19. package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
  20. package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
  21. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
  22. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
  23. package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
  24. package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
  25. package/dist/extractor/Extract.lean +1180 -0
  26. package/dist/extractor/Usage.lean +900 -0
  27. package/dist/mcp/visualproof-mcp.mjs +476 -0
  28. package/dist/server/access.js +95 -0
  29. package/dist/server/agent.js +3270 -0
  30. package/dist/server/agentdocs.js +35 -0
  31. package/dist/server/agentlibrary.js +202 -0
  32. package/dist/server/agentnotes.js +701 -0
  33. package/dist/server/agentviews.js +703 -0
  34. package/dist/server/askfollow.js +286 -0
  35. package/dist/server/boardspots.js +159 -0
  36. package/dist/server/dirs.js +175 -0
  37. package/dist/server/doctor.js +186 -0
  38. package/dist/server/events.js +198 -0
  39. package/dist/server/export.js +420 -0
  40. package/dist/server/extract.js +204 -0
  41. package/dist/server/fetchurl.js +321 -0
  42. package/dist/server/figures.js +158 -0
  43. package/dist/server/history.js +780 -0
  44. package/dist/server/historyapi.js +304 -0
  45. package/dist/server/hover.js +595 -0
  46. package/dist/server/identity.js +114 -0
  47. package/dist/server/invites.js +241 -0
  48. package/dist/server/layoutthread.js +51 -0
  49. package/dist/server/library.js +600 -0
  50. package/dist/server/libraryroute.js +211 -0
  51. package/dist/server/live.js +1101 -0
  52. package/dist/server/lostfound.js +199 -0
  53. package/dist/server/main.js +1406 -0
  54. package/dist/server/mcptools.js +496 -0
  55. package/dist/server/pastdoc.js +73 -0
  56. package/dist/server/pipeline.js +159 -0
  57. package/dist/server/poppler.js +251 -0
  58. package/dist/server/presence.js +252 -0
  59. package/dist/server/project.js +222 -0
  60. package/dist/server/prompt.js +53 -0
  61. package/dist/server/readonly.js +152 -0
  62. package/dist/server/readonlygate.js +123 -0
  63. package/dist/server/renames.js +268 -0
  64. package/dist/server/review.js +334 -0
  65. package/dist/server/reviewapi.js +188 -0
  66. package/dist/server/snapshot.js +50 -0
  67. package/dist/server/sourcestore.js +473 -0
  68. package/dist/server/status.js +360 -0
  69. package/dist/server/talk.js +476 -0
  70. package/dist/server/tokens.js +221 -0
  71. package/dist/server/usage.js +501 -0
  72. package/dist/server/versions.js +82 -0
  73. package/dist/server/view.js +164 -0
  74. package/dist/server/views.js +1167 -0
  75. package/dist/shared/askfollow.js +46 -0
  76. package/dist/shared/board.js +332 -0
  77. package/dist/shared/build.js +2 -0
  78. package/dist/shared/collab.js +241 -0
  79. package/dist/shared/comment.js +285 -0
  80. package/dist/shared/context.js +157 -0
  81. package/dist/shared/doc/authors.js +98 -0
  82. package/dist/shared/doc/build.js +358 -0
  83. package/dist/shared/doc/latex.js +455 -0
  84. package/dist/shared/doc/markdown.js +159 -0
  85. package/dist/shared/doc/model.js +37 -0
  86. package/dist/shared/doc/order.js +124 -0
  87. package/dist/shared/doc/outline.js +45 -0
  88. package/dist/shared/doc/proof.js +272 -0
  89. package/dist/shared/doc/sections.js +93 -0
  90. package/dist/shared/doc/settings.js +75 -0
  91. package/dist/shared/edgecodec.js +102 -0
  92. package/dist/shared/editor.js +64 -0
  93. package/dist/shared/excerpt.js +87 -0
  94. package/dist/shared/graph.js +16 -0
  95. package/dist/shared/items.js +92 -0
  96. package/dist/shared/keys.js +203 -0
  97. package/dist/shared/layout.js +926 -0
  98. package/dist/shared/lexer.js +130 -0
  99. package/dist/shared/library.js +711 -0
  100. package/dist/shared/lostfound.js +150 -0
  101. package/dist/shared/moves.js +244 -0
  102. package/dist/shared/notecard.js +295 -0
  103. package/dist/shared/noteplace.js +505 -0
  104. package/dist/shared/palette.js +46 -0
  105. package/dist/shared/pastgraph.js +103 -0
  106. package/dist/shared/places.js +80 -0
  107. package/dist/shared/projectnotes.js +244 -0
  108. package/dist/shared/query.js +818 -0
  109. package/dist/shared/review.js +642 -0
  110. package/dist/shared/reviewqueue.js +333 -0
  111. package/dist/shared/ring.js +366 -0
  112. package/dist/shared/scope.js +255 -0
  113. package/dist/shared/scopeedit.js +81 -0
  114. package/dist/shared/search.js +191 -0
  115. package/dist/shared/separate.js +178 -0
  116. package/dist/shared/sharesnap.js +281 -0
  117. package/dist/shared/sticky.js +501 -0
  118. package/dist/shared/timeline.js +73 -0
  119. package/dist/shared/titles.js +300 -0
  120. package/dist/shared/viewgraph.js +157 -0
  121. package/dist/shared/views.js +859 -0
  122. package/dist/web/bundle/app.js +52671 -0
  123. package/dist/web/bundle/edges.js +407 -0
  124. package/dist/web/bundle/layout.js +1010 -0
  125. package/dist/web/bundle/pdfworker.js +48322 -0
  126. package/dist/web/index.html +257 -0
  127. package/dist/web/src/motion.js +430 -0
  128. package/dist/web/src/past.js +224 -0
  129. package/dist/web/src/scene.js +804 -0
  130. package/dist/web/style.css +2548 -0
  131. package/docs/agent-api/README.md +91 -0
  132. package/docs/agent-api/advice.md +108 -0
  133. package/docs/agent-api/export.md +120 -0
  134. package/docs/agent-api/following.md +140 -0
  135. package/docs/agent-api/history.md +142 -0
  136. package/docs/agent-api/library.md +65 -0
  137. package/docs/agent-api/mcp.md +104 -0
  138. package/docs/agent-api/notes.md +93 -0
  139. package/docs/agent-api/reading.md +166 -0
  140. package/docs/agent-api/requests.md +94 -0
  141. package/docs/agent-api/talking.md +100 -0
  142. package/docs/agent-api/tcb.md +135 -0
  143. package/docs/agent-api/views.md +125 -0
  144. package/docs/agent-api/words.md +56 -0
  145. package/package.json +39 -4
  146. package/test/toolchains.json +5 -0
@@ -0,0 +1,188 @@
1
+ // What the review records, as the server checks and binds it (decided 2026-10-08): whoever asks —
2
+ // a page through POST /api/review, an agent through /api/agent — says only what they decide (approve,
3
+ // or comment with text; a target on or off; trust on or off; a key declaration of a target on
4
+ // or off, ExportSpec decision 1). Who asks is the writer, a person or an
5
+ // agent, as the request came in: an agent's events go to its own log, marked, its opinions as
6
+ // advice (AgentSpec decisions 2 and 9); whether an agent may record each is its level's to say,
7
+ // checked before it gets here (server/agent.ts, `needOf`). What an opinion is about is the
8
+ // server's to say, from the graph and the compiled file: the declaration's content hash and the text
9
+ // judged (the context it is written in and the compiled excerpt, as the source window shows them,
10
+ // or the statement where there is no source);
11
+ // whatever hash or text a request carries is ignored. A name that is not a declaration of the graph
12
+ // is refused, and so is an opinion on a declaration whose file changed since it was compiled (the
13
+ // graph, and the hash, are about the compiled text, not what the reviewer would read). A target is a
14
+ // declaration of the project. A key declaration is marked for a target, and is a member of what its
15
+ // review holds (shared/reviewqueue.ts).
16
+ //
17
+ // Every refusal is an answer: a status, and words that say what to do instead.
18
+ import { readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { exactContext, judgedText } from "../shared/context.js";
21
+ import { MAX_TEXT, STANDARD_AXIOMS, validPrefix } from "../shared/review.js";
22
+ import { reviewSet } from "../shared/reviewqueue.js";
23
+ import { lakeTextHash } from "./status.js";
24
+ const refuse = (status, error, hint) => ({ status, error, hint });
25
+ const answer = (r) => ({ status: r.status, body: { error: r.error, hint: r.hint } });
26
+ export class ReviewApi {
27
+ world;
28
+ /** the graph on show, or null until it has been read */
29
+ view;
30
+ constructor(world, view) {
31
+ this.world = world;
32
+ this.view = view;
33
+ }
34
+ /** The graph, or the answer that there is none yet. */
35
+ graph() {
36
+ return this.view() ?? refuse(503, "The graph has not been read yet.", "The server is reading the project; try again in a few seconds.");
37
+ }
38
+ /** What an opinion on `name` is about: bound to the declaration's present, compiled version. */
39
+ bind(v, name) {
40
+ if (typeof name !== "string" || !name.trim() || name.length > 2000)
41
+ return refuse(400, "name is missing.", "Give the declaration's full name.");
42
+ const { g, meta } = v;
43
+ const i = g.byName.get(name);
44
+ if (i === undefined) {
45
+ // an axiom the target rests on that the graph has no declaration for (shared/reviewqueue.ts):
46
+ // it has a name and nothing else, which is what the opinion is bound to
47
+ if (meta.axioms.includes(name) && !STANDARD_AXIOMS.includes(name))
48
+ return { name, axiom: true, external: true };
49
+ return refuse(404, `There is no declaration ${name} in the graph.`, "Opinions are given on the graph's declarations, by full name.");
50
+ }
51
+ const n = g.nodes[i];
52
+ const axiom = n.kind === "axiom" && !STANDARD_AXIOMS.includes(n.name);
53
+ if (g.isExt(i))
54
+ return { name, axiom, external: true };
55
+ const rel = this.world.project.files.get(n.module);
56
+ let lines = null;
57
+ let hash = null;
58
+ if (rel) {
59
+ try {
60
+ const text = readFileSync(join(this.world.project.root, rel), "utf8");
61
+ const compiled = v.modules.get(n.module)?.compiled ?? null;
62
+ hash = lakeTextHash(text);
63
+ if (compiled && hash !== compiled)
64
+ return refuse(409, `${n.module} changed since it was compiled: an opinion is about the compiled version, which is not what is there now.`, "Review it once the file is compiled again.");
65
+ lines = text.split("\n");
66
+ }
67
+ catch {
68
+ // a file gone since the build: the statement is what there is to judge
69
+ }
70
+ }
71
+ // the text judged is the declaration's with the context it is written in, as the page shows
72
+ // it (shared/context.ts)
73
+ const t = lines && n.range ? judgedText(lines, false, n.name, n.range, exactContext(this.world.contextsOf?.(n.module), n.name, hash)) : null;
74
+ const judged = t && t.text && t.text.length <= MAX_TEXT ? { text: t.text } : n.type && n.type.length <= MAX_TEXT ? { statement: n.type } : {};
75
+ return { name, ...(n.hash ? { hash: n.hash } : {}), ...judged, axiom, external: false };
76
+ }
77
+ /** What a writer records: a person from the page (POST /api/review), under the name the
78
+ * request's headers give (CollabSpec decision 3) or, invited by a personal link, under their link
79
+ * (decision 9), or an agent, under its name, through either door (its API, or the page's at
80
+ * level full), marked as an agent's. */
81
+ post(body, who) {
82
+ if (!body || typeof body !== "object" || Array.isArray(body))
83
+ return answer(refuse(400, "The body is not a JSON object.", "Send {op, …}."));
84
+ const b = body;
85
+ const d = this.world.review.data;
86
+ const me = who.name;
87
+ const by = me;
88
+ // every event an agent records says so, and the timeline's own events say so too; so does every
89
+ // one of someone invited by a personal link, by the link's id
90
+ const agent = { ...(who.agent ? { agent: true } : {}), ...(who.person ? { person: who.person } : {}) };
91
+ switch (b.op) {
92
+ case "opinion": {
93
+ // "dispute" was the old name of a comment (renamed 2026-10-09): refused, with the new name
94
+ if (b.verdict === "dispute")
95
+ return answer(refuse(400, "verdict is approve or comment: \"dispute\" is now \"comment\".", "Use comment: send {op: \"opinion\", name, verdict: \"comment\", comment}."));
96
+ if (b.verdict !== "approve" && b.verdict !== "comment")
97
+ return answer(refuse(400, "verdict is approve or comment.", "Send {op: \"opinion\", name, verdict, comment}."));
98
+ if (b.comment !== undefined && typeof b.comment !== "string")
99
+ return answer(refuse(400, "comment is text.", "Send the comment as a string."));
100
+ const comment = typeof b.comment === "string" ? b.comment.trim() : "";
101
+ if (comment.length > 10000)
102
+ return answer(refuse(400, "The comment is longer than 10,000 characters.", "Say it shorter."));
103
+ if (b.verdict === "comment" && !comment)
104
+ return answer(refuse(400, "A comment needs text.", "Write it in the box, then Comment."));
105
+ const v = this.graph();
106
+ if ("status" in v)
107
+ return answer(v);
108
+ const x = this.bind(v, b.name);
109
+ if ("status" in x)
110
+ return answer(x);
111
+ // 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);
113
+ }
114
+ case "target": {
115
+ if (typeof b.on !== "boolean")
116
+ return answer(refuse(400, "on is true or false.", "Send {op: \"target\", name, on}."));
117
+ if (typeof b.name !== "string" || !b.name)
118
+ return answer(refuse(400, "name is missing.", "Give the declaration's full name."));
119
+ // a target no longer in the graph (renamed, deleted) can still be taken off
120
+ if (b.on) {
121
+ const v = this.graph();
122
+ if ("status" in v)
123
+ return answer(v);
124
+ const i = v.g.byName.get(b.name);
125
+ if (i === undefined || v.g.isExt(i))
126
+ return answer(refuse(404, `${b.name} is not a declaration of the project.`, "A target is a declaration of the project, whose TCB is reviewed."));
127
+ }
128
+ const was = Object.hasOwn(d.targets, b.name);
129
+ const r = this.record({ op: "target", name: b.name, by, on: b.on, ...agent }, who);
130
+ // a target taken off leaves no trace in the review's state: the timeline keeps who did it
131
+ if (r.status === 200 && was && !b.on)
132
+ this.world.timeline.add({ kind: "untarget", by: me, ...agent, name: b.name });
133
+ return r;
134
+ }
135
+ case "trust": {
136
+ if (!validPrefix(b.prefix))
137
+ return answer(refuse(400, "prefix is not a dotted Lean name.", "A module or a namespace, e.g. Mathlib.Order or Nat; @external for the whole outside world."));
138
+ if (typeof b.on !== "boolean")
139
+ return answer(refuse(400, "on is true or false.", "Send {op: \"trust\", prefix, on}."));
140
+ const was = d.trusted.includes(b.prefix);
141
+ const r = this.record({ op: "trust", prefix: b.prefix, by, on: b.on, ...agent }, who);
142
+ // the review's state keeps the list, not who changed it: the timeline does
143
+ if (r.status === 200 && was !== b.on)
144
+ this.world.timeline.add({ kind: b.on ? "trust" : "untrust", by: me, ...agent, prefix: b.prefix });
145
+ return r;
146
+ }
147
+ case "key": {
148
+ if (typeof b.on !== "boolean")
149
+ return answer(refuse(400, "on is true or false.", "Send {op: \"key\", target, name, on}."));
150
+ if (typeof b.target !== "string" || !b.target || b.target.length > 2000)
151
+ return answer(refuse(400, "target is missing.", "Give the target's full name."));
152
+ if (typeof b.name !== "string" || !b.name || b.name.length > 2000)
153
+ return answer(refuse(400, "name is missing.", "Give the declaration's full name."));
154
+ if (b.name === b.target)
155
+ return answer(refuse(400, "A target is always key for itself.", "Mark the declarations its review holds that matter for reading it."));
156
+ // a mark on what is gone from the graph, or from the review, can still be taken off
157
+ if (b.on) {
158
+ const v = this.graph();
159
+ if ("status" in v)
160
+ return answer(v);
161
+ const t = v.g.byName.get(b.target);
162
+ if (t === undefined || !Object.hasOwn(d.targets, b.target))
163
+ return answer(refuse(409, `${b.target} is not a target.`, "Key declarations are a target's: mark it as a target first (☆ Mark as target)."));
164
+ const i = v.g.byName.get(b.name);
165
+ if (i === undefined)
166
+ return answer(refuse(404, `There is no declaration ${b.name} in the graph.`, "Mark declarations of the graph, by full name."));
167
+ if (!reviewSet(v.g, d, t)[i])
168
+ return answer(refuse(409, `${b.name} is not among what the review of ${b.target} holds.`, "A key declaration is one of the declarations its target's review holds: its TCB without what is trusted, and the non-standard axioms it rests on."));
169
+ }
170
+ return this.record({ op: "key", target: b.target, name: b.name, by, on: b.on, ...agent }, who);
171
+ }
172
+ default:
173
+ return answer(refuse(400, "op is opinion, target, trust or key.", "Send {op, …}."));
174
+ }
175
+ }
176
+ /** Append a writer's event to their log; the answer carries the change, which the page applies
177
+ * at once, and the event as written. */
178
+ record(op, who) {
179
+ return appended(this.world.review.append(who, op, this.world.project.toolchain));
180
+ }
181
+ }
182
+ /** An append's result as an answer: the change and the event written (none where nothing changed:
183
+ * a target marked that is one), or why nothing was written. */
184
+ export function appended(r) {
185
+ if ("status" in r)
186
+ return { status: r.status, body: { error: r.error, hint: r.status === 409 ? "Mend that line of the log by hand (or ask its writer to); VisualProof never rewrites a log." : "Send {op, …}." } };
187
+ return { status: 200, body: { ok: true, change: r.change, ...(r.change ? { event: r.event } : {}) } };
188
+ }
@@ -0,0 +1,50 @@
1
+ // `visualproof export`: the page, its code and its data in one HTML file that opens from disk.
2
+ // A snapshot is frozen: it shows the build it was taken from and never updates.
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { gunzipSync, gzipSync } from "node:zlib";
6
+ import { WEB, WEBFONTS, bundleDir } from "./pipeline.js";
7
+ import { lakeTextHash } from "./status.js";
8
+ /** Text that sits inside a <script> element must not close it. */
9
+ function scriptSafe(s) {
10
+ return s.replace(/<\/script/gi, "<\\/script");
11
+ }
12
+ export function writeSnapshot(project, prep, out, label, review, views) {
13
+ const meta = JSON.parse(gunzipSync(prep.metaGz).toString("utf8"));
14
+ // the sources a card can show: every project module's file, with the usage pass's contexts where
15
+ // they are of that text
16
+ const sources = {};
17
+ for (const m of meta.modules) {
18
+ if (!m.file)
19
+ continue;
20
+ try {
21
+ const text = readFileSync(join(project.root, m.file), "utf8");
22
+ const hash = lakeTextHash(text);
23
+ const contexts = prep.usage?.contexts[m.name];
24
+ sources[m.name] = { file: m.file, text, hash, ...(contexts && contexts.hash === hash ? { contexts } : {}) };
25
+ }
26
+ catch {
27
+ // a file deleted since the build: the card says so
28
+ }
29
+ }
30
+ const b64 = (buf) => buf.toString("base64");
31
+ // the artwork's fonts within the page, which has no server to ask for them
32
+ 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)))}")`);
33
+ const js = readFileSync(join(bundleDir(), "app.js"), "utf8").replace(/\n\/\/# sourceMappingURL=.*$/m, "");
34
+ const info = { createdAt: new Date().toISOString(), label };
35
+ const html = readFileSync(join(WEB, "index.html"), "utf8")
36
+ .replace(`<link rel="stylesheet" href="/style.css">`, () => `<style>\n${css}\n</style>`)
37
+ .replace(`<script type="module" src="/app.js"></script>`, () => `<script id="vp-static" type="application/json">${scriptSafe(JSON.stringify(info))}</script>\n` +
38
+ `<script id="vp-meta" type="application/octet-stream">${b64(prep.metaGz)}</script>\n` +
39
+ `<script id="vp-edges" type="application/octet-stream">${b64(prep.edgesGz)}</script>\n` +
40
+ `<script id="vp-sources" type="application/octet-stream">${b64(gzipSync(JSON.stringify(sources)))}</script>\n` +
41
+ // the review as it stood: shown, not changed (ReviewSpec decision 7)
42
+ `<script id="vp-review" type="application/octet-stream">${b64(gzipSync(JSON.stringify(review)))}</script>\n` +
43
+ // the kept views (ViewSpec.md), the targets' links with them, and their layouts' memory: the
44
+ // boards as they were; the project's notes (AttachmentSpec decision 13), each shown where its
45
+ // boards hold it
46
+ `<script id="vp-views" type="application/octet-stream">${b64(gzipSync(JSON.stringify({ views: views.kept(), memory: Object.fromEntries(views.kept().map((v) => [v.id, views.memory.get(v.id) ?? {}])), notes: views.notes })))}</script>\n` +
47
+ `<script type="module">\n${scriptSafe(js)}\n</script>`);
48
+ writeFileSync(out, html);
49
+ return html.length;
50
+ }