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,80 @@
1
+ // Shared pins (CollabSpec decision 2), as the server keeps them and the pages change them: per way
2
+ // of clustering, a node's or cluster's place in its parent's coordinates under the name the page
3
+ // knows it by. Operations are applied the same way on both sides; the last drop wins, and a drop
4
+ // moves its pin to the end, so that the order is the order things were dropped in.
5
+ export const CLUSTER_MODES = ["namespace", "module"];
6
+ export const emptyLayouts = () => ({ namespace: { nodes: {}, clusters: {} }, module: { nodes: {}, clusters: {} } });
7
+ /** Why a list of operations is refused, or null. */
8
+ export function invalidOps(ops) {
9
+ if (!Array.isArray(ops) || ops.length === 0 || ops.length > 2000)
10
+ return "ops";
11
+ for (const o of ops) {
12
+ if (!o || typeof o !== "object")
13
+ return "op";
14
+ if (o.op === "clear")
15
+ continue;
16
+ if (o.op !== "pin" && o.op !== "unpin")
17
+ return "op";
18
+ if (!CLUSTER_MODES.includes(o.mode))
19
+ return "mode";
20
+ if (o.kind !== "node" && o.kind !== "cluster")
21
+ return "kind";
22
+ if (typeof o.key !== "string" || !o.key || o.key.length > 2000)
23
+ return "key";
24
+ if (o.op === "pin") {
25
+ const at = o.at;
26
+ if (!Array.isArray(at) || at.length !== 2 || !at.every((v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 1e7))
27
+ return "at";
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ /** Apply operations to `layouts` in place; true when anything changed. */
33
+ export function applyOps(layouts, ops) {
34
+ let changed = false;
35
+ for (const o of ops) {
36
+ if (o.op === "clear") {
37
+ for (const m of CLUSTER_MODES) {
38
+ const p = layouts[m];
39
+ if (Object.keys(p.nodes).length + Object.keys(p.clusters).length)
40
+ changed = true;
41
+ layouts[m] = { nodes: {}, clusters: {} };
42
+ }
43
+ continue;
44
+ }
45
+ const set = o.kind === "node" ? layouts[o.mode].nodes : layouts[o.mode].clusters;
46
+ if (o.op === "unpin") {
47
+ if (o.key in set) {
48
+ delete set[o.key];
49
+ changed = true;
50
+ }
51
+ continue;
52
+ }
53
+ const at = [Math.round(o.at[0] * 10) / 10, Math.round(o.at[1] * 10) / 10];
54
+ const was = set[o.key];
55
+ if (was && was[0] === at[0] && was[1] === at[1] && Object.keys(set).pop() === o.key)
56
+ continue;
57
+ // dropped again: to the end, as the latest drop
58
+ delete set[o.key];
59
+ set[o.key] = at;
60
+ changed = true;
61
+ }
62
+ return changed;
63
+ }
64
+ /** The operations that turn `before` into `after` (one way of clustering), in drop order. */
65
+ export function diffPins(mode, before, after) {
66
+ const ops = [];
67
+ for (const kind of ["cluster", "node"]) {
68
+ const b = kind === "node" ? before.nodes : before.clusters;
69
+ const a = kind === "node" ? after.nodes : after.clusters;
70
+ for (const key of Object.keys(b))
71
+ if (!(key in a))
72
+ ops.push({ op: "unpin", mode, kind, key });
73
+ for (const [key, at] of Object.entries(a)) {
74
+ const was = b[key];
75
+ if (!was || was[0] !== at[0] || was[1] !== at[1])
76
+ ops.push({ op: "pin", mode, kind, key, at });
77
+ }
78
+ }
79
+ return ops;
80
+ }
@@ -0,0 +1,244 @@
1
+ // The project's notes (AttachmentSpec decisions 13–19, the owner's of 2026-10-11): a note is the
2
+ // project's, one note, an item that belongs to views as declarations do — the Project holds every
3
+ // one, another view those its scope gives and those added to it (shared/items.ts) — unless it is
4
+ // made a view's own (shared/sticky.ts, a view note). It is kept in the server's state beside the views — not in
5
+ // the review's log — and changed by operations applied the same way on the server and in every page
6
+ // (shared/views.ts, ViewBook), so that it is shared live as the views' operations are. Writing,
7
+ // editing and deleting one are events of the timeline; opening, collapsing and moving one are not.
8
+ //
9
+ // Where a project note is: one place per clustering (decision 15). Under a clustering it is either
10
+ // in a cluster — by the cluster's key and every key around it, each with the note's spot as a
11
+ // fraction of that cluster's radius, so that it is the same spot of the circle on every board
12
+ // whatever the circle's size — or at the top level, on the ring around the board at the angle where
13
+ // it was let go; or it has no place there yet, and is on the ring at the angle of what it is about
14
+ // (decision 16). Moving it on a board sets its place under that board's clustering alone. A note may
15
+ // also be attached to a declaration (the owner's decision of 2026-10-12): a declaration is under
16
+ // both clusterings, so the attachment is its place under every one, and its places are where it
17
+ // falls back to when the declaration is gone with no rename. How a place is read on a board, and
18
+ // which boards show the note, is shared/noteplace.ts's.
19
+ import { cleanCard, cleanCarries, cleanDecl, cleanEdited, invalidCard, invalidCarries, invalidDecl, invalidNoteText, sameCarries, setCard, setCarries, setDecl, stampWho, validNoteId } from "./sticky.js";
20
+ import { validPersonId } from "./review.js";
21
+ export const PROJECT_NOTE_OPS = ["pnote", "pnote-edit", "pnote-move", "pnote-open", "pnote-card", "pnote-decl", "pnote-delete"];
22
+ /** the most notes the project holds: each shows only where what it is about is, so a project holds
23
+ * more than a board (MAX_NOTES), but it is still not a document */
24
+ export const MAX_PROJECT_NOTES = 1000;
25
+ const CLUSTER_MODES = ["namespace", "module"];
26
+ /** how far out a spot's fraction of its radius may be as sent: it is brought inside the circle where
27
+ * it is drawn */
28
+ const FAR = 4;
29
+ /** Why a place under one clustering cannot be taken, or null. */
30
+ export function invalidClusterPlace(x) {
31
+ const p = x;
32
+ if (!p || typeof p !== "object")
33
+ return "place";
34
+ if ("ring" in p)
35
+ return typeof p.ring === "number" && Number.isFinite(p.ring) && Math.abs(p.ring) <= 3600 && Object.keys(p).length === 1 ? null : "place.ring";
36
+ if (!Array.isArray(p.in) || p.in.length === 0 || p.in.length > 64)
37
+ return "place.in";
38
+ for (const e of p.in) {
39
+ if (!Array.isArray(e) || e.length !== 3 || typeof e[0] !== "string" || !e[0] || e[0].length > 2000)
40
+ return "place.in";
41
+ if (![e[1], e[2]].every((v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= FAR))
42
+ return "place.in";
43
+ }
44
+ return null;
45
+ }
46
+ /** Why a note's places cannot be taken, or null. */
47
+ export function invalidPlaces(x) {
48
+ if (!x || typeof x !== "object" || Array.isArray(x))
49
+ return "places";
50
+ for (const [m, p] of Object.entries(x)) {
51
+ if (!CLUSTER_MODES.includes(m))
52
+ return "places";
53
+ const why = invalidClusterPlace(p);
54
+ if (why)
55
+ return why;
56
+ }
57
+ return null;
58
+ }
59
+ /** Why an operation on the project's notes is refused, or null. */
60
+ export function invalidProjectNoteOp(o) {
61
+ if (o.op === "pnote") {
62
+ const n = o.note;
63
+ if (!n || typeof n !== "object")
64
+ return "note";
65
+ if (!validNoteId(n.id))
66
+ return "note.id";
67
+ const t = invalidNoteText(n.text);
68
+ if (t)
69
+ return t;
70
+ if (typeof n.open !== "boolean")
71
+ return "note.open";
72
+ if (n.card !== undefined && invalidCard(n.card, false))
73
+ return "note.card";
74
+ if (n.decl !== undefined && invalidDecl(n.decl, false))
75
+ return "note.decl";
76
+ return invalidPlaces(n.places) ?? invalidCarries(n.carries);
77
+ }
78
+ if (!validNoteId(o.id))
79
+ return "id";
80
+ if (o.op === "pnote-edit")
81
+ return invalidNoteText(o.text) ?? invalidCarries(o.carries);
82
+ if (o.op === "pnote-move")
83
+ return !CLUSTER_MODES.includes(o.mode) ? "mode" : o.place === null ? null : invalidClusterPlace(o.place);
84
+ if (o.op === "pnote-open")
85
+ return typeof o.open === "boolean" ? null : "open";
86
+ if (o.op === "pnote-card")
87
+ return invalidCard(o.card);
88
+ if (o.op === "pnote-decl")
89
+ return invalidDecl(o.decl);
90
+ return o.op === "pnote-delete" ? null : "op";
91
+ }
92
+ /** Why a list of operations on the project's notes is refused, or null. */
93
+ export function invalidProjectNoteOps(ops) {
94
+ if (!Array.isArray(ops) || ops.length === 0 || ops.length > 400)
95
+ return "ops";
96
+ for (const o of ops) {
97
+ if (!o || typeof o !== "object")
98
+ return "op";
99
+ const why = invalidProjectNoteOp(o);
100
+ if (why)
101
+ return why;
102
+ }
103
+ return null;
104
+ }
105
+ /** A place under one clustering as a record keeps it: a ring's angle within a turn, a spot's
106
+ * fractions to the tenth of a percent. */
107
+ export function cleanClusterPlace(p) {
108
+ if ("ring" in p)
109
+ return { ring: Math.round((((p.ring % 360) + 360) % 360) * 10) / 10 };
110
+ const f = (v) => Math.round(v * 1000) / 1000;
111
+ return { in: p.in.map(([k, x, y]) => [k, f(x), f(y)]) };
112
+ }
113
+ /** Apply one operation to the project's notes in place; true when anything changed. A note beyond
114
+ * MAX_PROJECT_NOTES, or one whose id is taken, is not added. */
115
+ export function applyProjectNoteOp(notes, o) {
116
+ if (o.op === "pnote") {
117
+ if (notes.length >= MAX_PROJECT_NOTES || notes.some((n) => n.id === o.note.id))
118
+ return false;
119
+ const n = structuredClone(o.note);
120
+ for (const m of CLUSTER_MODES)
121
+ if (n.places[m])
122
+ n.places[m] = cleanClusterPlace(n.places[m]);
123
+ if (n.decl)
124
+ n.decl = cleanDecl(n.decl).decl;
125
+ if (!n.decl)
126
+ delete n.decl;
127
+ notes.push(n);
128
+ return true;
129
+ }
130
+ const k = notes.findIndex((n) => n.id === o.id);
131
+ if (k < 0)
132
+ return false;
133
+ const n = notes[k];
134
+ switch (o.op) {
135
+ case "pnote-edit":
136
+ if (n.text === o.text && (o.carries === undefined || sameCarries(n.carries, o.carries)))
137
+ return false;
138
+ n.text = o.text;
139
+ if (o.carries !== undefined)
140
+ setCarries(n, o.carries);
141
+ if (o.at)
142
+ n.edited = { by: o.by ?? null, ...(o.agent ? { agent: true } : {}), ...(o.person ? { person: o.person } : {}), at: o.at };
143
+ return true;
144
+ case "pnote-move":
145
+ if (o.place === null) {
146
+ if (!n.places[o.mode])
147
+ return false;
148
+ delete n.places[o.mode];
149
+ }
150
+ else
151
+ n.places[o.mode] = cleanClusterPlace(o.place);
152
+ if (o.by && o.at)
153
+ n.edited = { by: o.by, at: o.at };
154
+ return true;
155
+ case "pnote-open":
156
+ if (n.open === o.open)
157
+ return false;
158
+ n.open = o.open;
159
+ return true;
160
+ case "pnote-card":
161
+ return setCard(n, o.card);
162
+ case "pnote-decl":
163
+ return setDecl(n, o.decl);
164
+ case "pnote-delete":
165
+ notes.splice(k, 1);
166
+ return true;
167
+ }
168
+ }
169
+ /** Stamp who does an operation on the project's notes, and when, as for a view note: the author of
170
+ * a new note, the editor of an edit. A page's move says nothing of who: only the server records a
171
+ * carry as its edit. */
172
+ export function stampProjectNoteOp(o, who, at) {
173
+ if (o.op === "pnote") {
174
+ stampWho(o.note, who, at);
175
+ delete o.note.edited;
176
+ }
177
+ else if (o.op === "pnote-edit")
178
+ stampWho(o, who, at);
179
+ else if (o.op === "pnote-move") {
180
+ delete o.by;
181
+ delete o.at;
182
+ }
183
+ }
184
+ /** The project's notes as a file holds them, checked: one that cannot be read is left out. */
185
+ export function cleanProjectNotes(raw) {
186
+ if (!Array.isArray(raw))
187
+ return [];
188
+ const out = [];
189
+ for (const n of raw) {
190
+ if (!n || typeof n !== "object" || !validNoteId(n.id) || invalidNoteText(n.text) || invalidPlaces(n.places) || typeof n.open !== "boolean")
191
+ continue;
192
+ if (out.some((x) => x.id === n.id) || out.length >= MAX_PROJECT_NOTES)
193
+ continue;
194
+ const places = {};
195
+ for (const m of CLUSTER_MODES) {
196
+ const p = n.places[m];
197
+ if (p)
198
+ places[m] = cleanClusterPlace(p);
199
+ }
200
+ out.push({
201
+ id: n.id,
202
+ text: n.text,
203
+ places,
204
+ open: n.open,
205
+ by: typeof n.by === "string" ? n.by : null,
206
+ ...(n.agent === true ? { agent: true } : {}),
207
+ ...(validPersonId(n.person) ? { person: n.person } : {}),
208
+ at: typeof n.at === "string" ? n.at : new Date(0).toISOString(),
209
+ ...cleanEdited(n.edited),
210
+ ...cleanCarries(n.carries),
211
+ ...cleanCard(n.card),
212
+ ...cleanDecl(n.decl),
213
+ });
214
+ }
215
+ return out;
216
+ }
217
+ /** What both kinds of note have besides where they are: what a switch between them keeps. */
218
+ function common(n) {
219
+ return {
220
+ id: n.id,
221
+ text: n.text,
222
+ open: n.open,
223
+ by: n.by,
224
+ ...(n.agent ? { agent: true } : {}),
225
+ ...(n.person ? { person: n.person } : {}),
226
+ at: n.at,
227
+ ...(n.edited ? { edited: structuredClone(n.edited) } : {}),
228
+ ...(n.carries?.length ? { carries: structuredClone(n.carries) } : {}),
229
+ ...(n.card ? { card: [n.card[0], n.card[1]] } : {}),
230
+ ...(n.decl ? { decl: { name: n.decl.name, at: [n.decl.at[0], n.decl.at[1]] } } : {}),
231
+ };
232
+ }
233
+ /** A project note made a view's own (decision 14, Only in this view): its text, author, state, what
234
+ * it carries, where its card hangs and the declaration it is attached to kept, at `place`, where it
235
+ * is on the board it was switched on — its fallback, where it is attached. */
236
+ export function asViewNote(n, place) {
237
+ return { ...common(n), place: structuredClone(place) };
238
+ }
239
+ /** A view note made the project's (In every view): kept as it is, attached to the declaration it was
240
+ * attached to, at `place` under the clustering of the board it was switched on — where it is there,
241
+ * its fallback where it is attached — and with no place under the other. */
242
+ export function asProjectNote(n, mode, place) {
243
+ return { ...common(n), places: { [mode]: cleanClusterPlace(place) } };
244
+ }