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,281 @@
1
+ // A share link that carries exactly what the sharer sees (CollabSpec decision 7, the owner's decision
2
+ // of 2026-10-12): a snapshot of the parts following carries (web/src/surfaces.ts), which the page that
3
+ // opens the link applies once, as starting to follow applies them, after which the reader is free.
4
+ //
5
+ // It travels in the address's fragment, which no server sees, after the address as a page writes it
6
+ // (web/src/state.ts): `#v=…&mode=…&sel=…&snap=<data>`. The two agree, being written from one state
7
+ // at once: a page that reads the snapshot takes the board from the address first (its query's build,
8
+ // `at=`, is in no snapshot, presence not carrying it), then everything else from the snapshot; a page
9
+ // that cannot read it opens the address as before. The access link's key and a personal link's token
10
+ // are in the path before it, and stay as they are.
11
+ //
12
+ // The snapshot is each followed part in a form of its own (`SNAPSHOT_FORMS`, one per part of the
13
+ // registry): the fields as presence carries them (shared/collab.ts, PresenceView), checked on the way
14
+ // back as the server checks a heartbeat (`cleanView`), but for two. The camera travels as the region
15
+ // of the board it showed — its centre, and the width and height shown, in the board's world — and is
16
+ // fitted into the opener's screen, so that another screen shows the same region, not the same zoom.
17
+ // The visitors leave out where they were drawn in the world, which only pages from before 2026-10-11
18
+ // read. Written as JSON, deflated where that is shorter, in base64url: `z` and the deflated bytes, or
19
+ // `j` and the JSON's.
20
+ import { cleanView } from "./collab.js";
21
+ /** the fragment's part that holds the snapshot */
22
+ export const SNAP_PARAM = "snap";
23
+ /** the most characters of a snapshot a page reads, and the most bytes it inflates to: a link is
24
+ * anyone's to write, and what the page is handed stays small whatever it says */
25
+ export const SNAPSHOT_MAX = 32_000;
26
+ const INFLATED_MAX = 256_000;
27
+ /** the camera's zoom as the canvas bounds it */
28
+ const K_MIN = 0.02, K_MAX = 12;
29
+ /** The room a screen leaves for the board: where it is and how large; the whole canvas where the
30
+ * panels leave none. */
31
+ function room(s) {
32
+ const w = s.w - s.insets.left - s.insets.right;
33
+ const h = s.h - s.insets.top - s.insets.bottom;
34
+ if (!(w > 0 && h > 0))
35
+ return { cx: s.w / 2, cy: s.h / 2, w: Math.max(1, s.w), h: Math.max(1, s.h) };
36
+ return { cx: s.insets.left + w / 2, cy: s.insets.top + h / 2, w, h };
37
+ }
38
+ /** The region of the board camera `cam` shows on screen `s`: the room the panels leave. */
39
+ export function regionOf(cam, s) {
40
+ const r = room(s);
41
+ return { x: (r.cx - s.w / 2) / cam.k + cam.x, y: (r.cy - s.h / 2) / cam.k + cam.y, w: r.w / cam.k, h: r.h / cam.k };
42
+ }
43
+ /** The camera that shows region `g` on screen `s`: all of it, centred in the room the panels leave,
44
+ * as large as that room allows. */
45
+ export function camOf(g, s) {
46
+ const r = room(s);
47
+ const k = Math.min(K_MAX, Math.max(K_MIN, Math.min(r.w / g.w, r.h / g.h)));
48
+ return { x: g.x - (r.cx - s.w / 2) / k, y: g.y - (r.cy - s.h / 2) / k, k };
49
+ }
50
+ /** A camera and a screen that show exactly region `g`: for a region given as one (an agent's link),
51
+ * which the camera's form writes as it is. */
52
+ export function showing(g) {
53
+ return { cam: { x: g.x, y: g.y, k: 1 }, screen: { w: g.w, h: g.h, insets: { left: 0, right: 0, top: 0, bottom: 0 } } };
54
+ }
55
+ /** A region as a link writes it, `[x, y, w, h]`, read back: null where it is not one. */
56
+ export function readRegion(x) {
57
+ if (!Array.isArray(x) || x.length !== 4 || !x.every((n) => typeof n === "number" && Number.isFinite(n) && Math.abs(n) < 1e9))
58
+ return null;
59
+ const [cx, cy, w, h] = x;
60
+ return w > 0 && h > 0 ? { x: cx, y: cy, w, h } : null;
61
+ }
62
+ /** A part carried as presence carries it: each of its fields the page says, read back as the server
63
+ * reads a heartbeat. */
64
+ const same = (...fields) => ({
65
+ fields,
66
+ write: (v) => Object.fromEntries(fields.filter((f) => v[f] !== undefined).map((f) => [f, v[f]])),
67
+ read: (o, clean) => Object.fromEntries(fields.filter((f) => f in o && clean[f] !== undefined).map((f) => [f, clean[f]])),
68
+ });
69
+ /** to a tenth of a unit of the board's world: a tenth of a pixel at the closest zoom but one */
70
+ const tenth = (n) => Math.round(n * 10) / 10;
71
+ /** The form of every followed part, by its name in the registry (web/src/surfaces.ts, which holds the
72
+ * two to each other): a part declared followed without one does not typecheck there, and
73
+ * test/sharesnap.test.ts fails. */
74
+ export const SNAPSHOT_FORMS = {
75
+ view: same("view", "scope", "clusterMode"),
76
+ history: same("history"),
77
+ // the region the camera showed, fitted into the opener's screen
78
+ camera: {
79
+ fields: ["cam"],
80
+ write: (v, screen) => {
81
+ if (!v.cam || !screen)
82
+ return {};
83
+ const g = regionOf(v.cam, screen);
84
+ return { region: [tenth(g.x), tenth(g.y), tenth(g.w), tenth(g.h)] };
85
+ },
86
+ read: (o, _clean, screen) => {
87
+ const g = readRegion(o.region);
88
+ return g && screen ? { cam: camOf(g, screen) } : {};
89
+ },
90
+ },
91
+ tool: same("mode"),
92
+ selection: same("selection"),
93
+ focus: same("focus"),
94
+ // each visitor's declaration, what it was reached from and its place; not where it was drawn in the
95
+ // world, which only pages from before 2026-10-11 read
96
+ visitors: {
97
+ fields: ["visitors"],
98
+ write: (v) => (v.visitors?.length ? { visitors: v.visitors.map(({ at: _at, ...rest }) => rest) } : {}),
99
+ read: (o, clean) => ("visitors" in o && clean.visitors ? { visitors: clean.visitors } : {}),
100
+ },
101
+ panel: same("sel"),
102
+ sources: same("source", "sourceCollapsed", "pinned"),
103
+ paper: same("paper"),
104
+ activity: same("activity"),
105
+ lostFound: same("lostFound"),
106
+ colouring: same("scheme"),
107
+ edges: same("edges"),
108
+ linear: same("linear"),
109
+ artwork: same("artwork"),
110
+ };
111
+ /** The snapshot of where a page is (`v`, as presence carries it: the registry's parts, read by the
112
+ * page), as a link carries it after `snap=`. */
113
+ export async function writeSnapshot(v, screen) {
114
+ const o = {};
115
+ for (const form of Object.values(SNAPSHOT_FORMS))
116
+ Object.assign(o, form.write(v, screen));
117
+ const json = new TextEncoder().encode(JSON.stringify(o));
118
+ const z = await deflate(json);
119
+ return z && z.length < json.length ? "z" + base64url(z) : "j" + base64url(json);
120
+ }
121
+ /** A link's snapshot read back for a page on `screen`: null where it cannot be read (written by
122
+ * hand, cut short, too long, or deflated where this browser cannot inflate). */
123
+ export async function readSnapshot(text, screen) {
124
+ if (text.length < 2 || text.length > SNAPSHOT_MAX)
125
+ return null;
126
+ const bytes = unbase64url(text.slice(1));
127
+ if (!bytes)
128
+ return null;
129
+ const json = text[0] === "z" ? await inflate(bytes) : text[0] === "j" ? bytes : null;
130
+ if (!json)
131
+ return null;
132
+ let o;
133
+ try {
134
+ o = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(json));
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ if (!o || typeof o !== "object" || Array.isArray(o))
140
+ return null;
141
+ const raw = o;
142
+ const clean = cleanView({ ...raw, cam: null, tabs: [] });
143
+ const view = {};
144
+ for (const form of Object.values(SNAPSHOT_FORMS))
145
+ Object.assign(view, form.read(raw, clean, screen));
146
+ return { view, region: readRegion(raw.region) };
147
+ }
148
+ /** The snapshot a fragment carries (`snap=`), or null. */
149
+ export function snapshotIn(hash) {
150
+ const text = hash.startsWith("#") ? hash.slice(1) : hash;
151
+ return text ? new URLSearchParams(text).get(SNAP_PARAM) : null;
152
+ }
153
+ /** A fragment (`#…`, or none) with the snapshot after the address it holds. */
154
+ export function withSnapshot(hash, snap) {
155
+ const text = (hash.startsWith("#") ? hash.slice(1) : hash).split("&").filter((p) => p && !p.startsWith(`${SNAP_PARAM}=`)).join("&");
156
+ return `#${text ? text + "&" : ""}${SNAP_PARAM}=${snap}`;
157
+ }
158
+ /** The address as a page writes it (web/src/state.ts), for a link made off the page (an agent's): the
159
+ * view by its id, or by its query and the build it shows where nobody keeps it; the tool; the
160
+ * selection; and the declaration the panel tells of. */
161
+ export function addressOf(v) {
162
+ const p = new URLSearchParams();
163
+ if (v.scope) {
164
+ p.set("q", v.scope);
165
+ if (typeof v.version === "number")
166
+ p.set("at", String(v.version));
167
+ }
168
+ else if (v.view)
169
+ p.set("v", v.view);
170
+ p.set("mode", v.mode);
171
+ if (v.selection)
172
+ p.set("sel", v.selection);
173
+ if (v.sel)
174
+ p.set("s", v.sel);
175
+ return p.toString();
176
+ }
177
+ /** A snapshot sorted out before it is applied, by what the opener's page has: a build the history no
178
+ * longer holds, and a file of the library or a link no longer there, are taken out — the Time
179
+ * Machine would open at the latest build instead, the reference window say there is no such file —
180
+ * and said; the reference window opens on its file at its page where only the link is gone. */
181
+ export function sortOut(v, has) {
182
+ const view = { ...v };
183
+ const gone = {};
184
+ if (view.history && !has.build(view.history.snapshot)) {
185
+ delete view.history;
186
+ gone.build = true;
187
+ }
188
+ if (view.paper && !has.file(view.paper.file)) {
189
+ delete view.paper;
190
+ gone.file = true;
191
+ }
192
+ else if (view.paper?.link && !has.link(view.paper.link)) {
193
+ const { link: _gone, ...rest } = view.paper;
194
+ view.paper = rest;
195
+ gone.link = true;
196
+ }
197
+ return { view, gone };
198
+ }
199
+ /** What a snapshot names that the board's graph does not hold, once it is applied — what the panel
200
+ * tells of, the declarations of the selection (`picked`), the visitors, the pinned windows — and a
201
+ * focus on what is no target now. */
202
+ export function goneNames(v, picked, has) {
203
+ const named = [v.sel, ...picked, ...(v.visitors ?? []).map((w) => w.decl), ...(v.pinned ?? []).map((w) => w.decl)];
204
+ const decls = [...new Set(named.filter((n) => !!n && !has.decl(n)))];
205
+ return { decls, ...(typeof v.focus === "string" && !has.target(v.focus) ? { focus: v.focus } : {}) };
206
+ }
207
+ /** What could not be shown of a link's snapshot, said in one short line; null where everything was. */
208
+ export function missingLine(m) {
209
+ const short = (n) => n.split(".").pop() ?? n;
210
+ const decls = [...new Set(m.decls ?? [])];
211
+ const said = [
212
+ m.view ? "its view" : null,
213
+ m.build ? "its build" : null,
214
+ decls.length ? `${decls.length === 1 ? "1 declaration" : `${decls.length} declarations`} (${decls.slice(0, 3).map(short).join(", ")}${decls.length > 3 ? ", …" : ""})` : null,
215
+ m.focus ? `the focus ★${short(m.focus)}` : null,
216
+ m.file ? "the reference file" : m.link ? "the link in the reference" : null,
217
+ ].filter((x) => x !== null);
218
+ return said.length ? `Not shown from the link (no longer here): ${said.join(", ")}.` : null;
219
+ }
220
+ // ------------------------------------------------------------------ bytes
221
+ /** `bytes` deflated (raw), or null where the browser cannot. */
222
+ async function deflate(bytes) {
223
+ if (typeof CompressionStream === "undefined")
224
+ return null;
225
+ try {
226
+ const s = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
227
+ return new Uint8Array(await new Response(s).arrayBuffer());
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ /** `bytes` inflated, or null: where the browser cannot, where they are not deflated, or where they
234
+ * inflate to more than a snapshot ever holds. */
235
+ async function inflate(bytes) {
236
+ if (typeof DecompressionStream === "undefined")
237
+ return null;
238
+ try {
239
+ const reader = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw")).getReader();
240
+ const parts = [];
241
+ let total = 0;
242
+ for (;;) {
243
+ const { done, value } = await reader.read();
244
+ if (done)
245
+ break;
246
+ total += value.length;
247
+ if (total > INFLATED_MAX) {
248
+ void reader.cancel().catch(() => { });
249
+ return null;
250
+ }
251
+ parts.push(value);
252
+ }
253
+ const out = new Uint8Array(total);
254
+ let at = 0;
255
+ for (const p of parts) {
256
+ out.set(p, at);
257
+ at += p.length;
258
+ }
259
+ return out;
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ }
265
+ function base64url(b) {
266
+ let s = "";
267
+ for (let i = 0; i < b.length; i += 0x8000)
268
+ s += String.fromCharCode(...b.subarray(i, i + 0x8000));
269
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
270
+ }
271
+ function unbase64url(t) {
272
+ if (!/^[A-Za-z0-9_-]*$/.test(t))
273
+ return null;
274
+ try {
275
+ const s = atob(t.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((t.length + 3) % 4));
276
+ return Uint8Array.from(s, (c) => c.charCodeAt(0));
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }