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,505 @@
1
+ // Where a note is on a board, and which boards show it (AttachmentSpec decisions 15–19, the owner's
2
+ // of 2026-10-11), as pure rules run the same in the pages, on the server (the agents' API, the
3
+ // export's authors, a build carrying notes) and in the tests.
4
+ //
5
+ // A board is read here by its keys: every cluster key of either clustering that holds one of its
6
+ // declarations, in the tree before chains are merged and levels hidden (shared/layout.ts,
7
+ // innerKeys), and for each key of the board's own clustering the circle drawn for it — itself; the
8
+ // circle a chain of clusters holding one cluster each was merged into, which counts as each of
9
+ // them; the nearest drawn circle around a hidden level (ViewSpec decision 24); or the top level.
10
+ //
11
+ // Which boards show a note of the project (decision 17, the owner's decision of 2026-10-11: items
12
+ // belong to views as declarations do): the Project shows every one. Any other board shows one that
13
+ // follows from its scope — attached to a declaration on the board; in a cluster that is on the
14
+ // board; on the ring under the board's
15
+ // clustering, where its cluster under the other clustering has a declaration on the board; or
16
+ // carrying a file linked (decision 4) to a declaration or a cluster on the board — or that was added
17
+ // to its view, unless it was taken out of it (shared/items.ts). A note that names a declaration in
18
+ // backticks is not connected to it. Read live, as a scope is: a link made or gone, a note moved,
19
+ // shows or hides it at once.
20
+ //
21
+ // - A project note in a cluster under the board's clustering that the board draws is at its spot
22
+ // of the circle drawn for it: the spot is kept as a fraction of each circle's radius, so it is the
23
+ // same spot on every board whatever the circle's size, and it is brought inside the circle as
24
+ // drawn. A cluster that is gone from the board's graph (a build that took it away) gives way to
25
+ // the nearest one around it that is still there, at the spot the note had in it; none left, the
26
+ // note is at the top level.
27
+ // - At the top level, or shown on a board that does not draw its cluster (added, or through a
28
+ // link), it is on the ring: at the angle where it was let go, else toward what it carries a file
29
+ // linked to on this board, else at the angle of the centre of the declarations its cluster under
30
+ // the other clustering holds on the board (decision 16), else in the order notes were written.
31
+ // - A view note under its own clustering is where it always was (shared/sticky.ts); under the other
32
+ // it is on the ring about the cluster it was put in, never in a cluster of the same key. View notes
33
+ // are their view's alone, and none of the rules of items touches them (decision 14).
34
+ // - A note of either kind attached to a declaration (the owner's decision of 2026-10-12) is at that
35
+ // declaration wherever the board holds it — a reader's visitor counts — under every clustering,
36
+ // and a board that holds the declaration shows a note of the project attached to it (the derived
37
+ // clause of items). A board of a past build reads the declaration by its name in that build. Where
38
+ // the board's graph does not have the declaration (gone, with no rename carrying the note), its
39
+ // places stand instead, as any note's do (decision 18); where the graph has it but the board does
40
+ // not, a note of the project shows only where it is linked or added, on the ring, and a view note
41
+ // where its place puts it.
42
+ //
43
+ // A key is never matched across clusterings: a namespace and a folder may share a key.
44
+ import { itemIn } from "./items.js";
45
+ import { clusterKeys, innerKeys, splitName } from "./layout.js";
46
+ import { parseSubject } from "./library.js";
47
+ import { resolvePlace } from "./sticky.js";
48
+ export const otherMode = (m) => (m === "namespace" ? "module" : "namespace");
49
+ /** A cluster key and every key around it, innermost first ("" has none). */
50
+ export function keyChain(key) {
51
+ if (!key)
52
+ return [];
53
+ const parts = splitName(key);
54
+ const out = [];
55
+ for (let k = parts.length; k > 0; k--)
56
+ out.push(parts.slice(0, k).join("."));
57
+ return out;
58
+ }
59
+ /** Whether key `k` is circle `c` of board `b` — the circle itself, or a key a chain merged into it —
60
+ * rather than a hidden level that only stands in it. */
61
+ export function counts(b, k, c) {
62
+ if (c < 0 || b.stands.get(k) !== c)
63
+ return false;
64
+ const own = b.keys[c];
65
+ return own === k || own.startsWith(k + ".");
66
+ }
67
+ /** The board of layout `L` (drawn: chains merged, hidden levels out) by `mode`, of `nodes` (the
68
+ * board's declarations, in the layout's numbering). */
69
+ export function noteBoard(L, mode, nodes) {
70
+ const inner = { namespace: innerKeys("namespace", nodes), module: innerKeys("module", nodes) };
71
+ const on = { namespace: new Set(), module: new Set() };
72
+ const stands = new Map();
73
+ // the innermost circle that holds every declaration a key holds on the board
74
+ const meet = (a, b) => {
75
+ while (a !== b) {
76
+ if (a < 0 || b < 0)
77
+ return -1;
78
+ if (L.clusters[a].depth >= L.clusters[b].depth)
79
+ a = L.clusters[a].parent;
80
+ else
81
+ b = L.clusters[b].parent;
82
+ }
83
+ return a;
84
+ };
85
+ // the circle each innermost key's declarations meet in, then each key around it: a key and its
86
+ // chain worked out once, however many declarations it holds
87
+ const own = new Map();
88
+ inner[mode].forEach((key, j) => {
89
+ const c = L.cluster[j] ?? -1;
90
+ const was = own.get(key);
91
+ own.set(key, was === undefined ? c : meet(was, c));
92
+ });
93
+ for (const [key, c] of own)
94
+ for (const k of keyChain(key)) {
95
+ const was = stands.get(k);
96
+ stands.set(k, was === undefined ? c : meet(was, c));
97
+ }
98
+ for (const k of stands.keys())
99
+ on[mode].add(k);
100
+ // the other clustering: only which keys hold something
101
+ const other = otherMode(mode);
102
+ for (const key of new Set(inner[other]))
103
+ for (const k of keyChain(key))
104
+ on[other].add(k);
105
+ return { mode, inner, on, stands, keys: L.clusters.map((c) => c.key), index: new Map(nodes.map((n, j) => [n.name, j])) };
106
+ }
107
+ /** The declarations of the board that key `key` holds by `mode`, in the board's numbering. */
108
+ export function membersOf(b, mode, key) {
109
+ const out = [];
110
+ const pre = key + ".";
111
+ b.inner[mode].forEach((k, j) => {
112
+ if (k === key || k.startsWith(pre))
113
+ out.push(j);
114
+ });
115
+ return out;
116
+ }
117
+ /** The cluster that holds a note under `mode` on a graph (`exists`: whether a key is a cluster of
118
+ * that graph by that clustering): the first key of its place that is still one — its own, else the
119
+ * nearest around it that is left (decision 18) — and where it is in that place; null at the top
120
+ * level (on the ring, no place, or none of its clusters left). */
121
+ export function holderOf(place, mode, exists) {
122
+ if (!place || !("in" in place))
123
+ return null;
124
+ for (let k = 0; k < place.in.length; k++)
125
+ if (exists(mode, place.in[k][0]))
126
+ return { key: place.in[k][0], at: k };
127
+ return null;
128
+ }
129
+ export function linksByFile(links) {
130
+ const out = new Map();
131
+ for (const l of links ?? []) {
132
+ const list = out.get(l.file) ?? [];
133
+ if (!list.includes(l.subject))
134
+ list.push(l.subject);
135
+ out.set(l.file, list);
136
+ }
137
+ return out;
138
+ }
139
+ /** A past graph's renames (PastGraph.nowName: each declaration's name then to its name now) turned
140
+ * round: its name now to its name then, worked out once per map. */
141
+ const turned = new WeakMap();
142
+ export function thenNames(nowName) {
143
+ if (!nowName?.size)
144
+ return undefined;
145
+ let t = turned.get(nowName);
146
+ if (!t)
147
+ turned.set(nowName, (t = new Map([...nowName].map(([then, now]) => [now, then]))));
148
+ return t;
149
+ }
150
+ /** Where the declaration note `n` is attached to is for board `b`: on it (`j`, its index there, or
151
+ * -1 for one of the reader's visitors; `name`, the board's name for it); `off`, in the board's graph
152
+ * but not on the board; `gone`, not in the board's graph (`exists.decl`), no rename having carried
153
+ * the note; null where it is attached to none. */
154
+ export function declOf(n, b, exists) {
155
+ if (!n.decl)
156
+ return null;
157
+ const name = b.then?.get(n.decl.name) ?? n.decl.name;
158
+ const j = b.index.get(name);
159
+ if (j !== undefined)
160
+ return { j, name };
161
+ if (b.visiting?.has(name))
162
+ return { j: -1, name };
163
+ return exists.decl && !exists.decl(name) ? "gone" : "off";
164
+ }
165
+ /** The board of `nodes` (its declarations) by `mode`, read without laying it out: what holds what,
166
+ * not where (the agents' API and the export's authors, for a board they do not draw). */
167
+ export function itemBoard(mode, nodes) {
168
+ const on = { namespace: new Set(), module: new Set() };
169
+ for (const m of ["namespace", "module"])
170
+ for (const key of new Set(innerKeys(m, nodes)))
171
+ for (const k of keyChain(key))
172
+ on[m].add(k);
173
+ return { mode, on, index: new Map(nodes.map((n, j) => [n.name, j])) };
174
+ }
175
+ /** The subjects of the links into the files note `n` carries (whole or as clips) that reach what
176
+ * board `b` holds: a declaration on it (`decl(X)`), a namespace (`ns(P)`; a structure is its
177
+ * namespace, so `ns(S)` reaches the structure `S` too) or a folder (`file(P)`) that holds one of its
178
+ * declarations. Once each, in the order of what it carries. */
179
+ export function linkedOn(n, b, links) {
180
+ const out = [];
181
+ for (const c of n.carries ?? [])
182
+ for (const subject of links.get(c.file) ?? []) {
183
+ if (out.includes(subject))
184
+ continue;
185
+ const s = parseSubject(subject);
186
+ if (!s)
187
+ continue;
188
+ const on = s.kind === "decl" ? b.index.has(s.name) : s.kind === "ns" ? b.on.namespace.has(s.name) || b.index.has(s.name) : b.on.module.has(s.name);
189
+ if (on)
190
+ out.push(subject);
191
+ }
192
+ return out;
193
+ }
194
+ /** Why board `b` shows note `n` by what follows from its scope alone (decision 17): `decl`, it is
195
+ * attached to a declaration on the board (a reader's visitor counts; the owner's decision of
196
+ * 2026-10-12); `cluster`, its cluster under the board's clustering is on the board; `ring`, it is at
197
+ * the top level under the board's clustering and its cluster under the other clustering has a
198
+ * declaration on the board; `link`, it carries a file linked to a declaration or a cluster on the
199
+ * board. Its places count only where it is attached to none, or its declaration is gone: attached to
200
+ * one off the board, that is where it is. Null where none does: at the top level under both
201
+ * clusterings, a note is the Project's, not every board's. */
202
+ export function derivedWhy(n, b, exists, links) {
203
+ const at = declOf(n, b, exists);
204
+ if (at && typeof at === "object")
205
+ return "decl";
206
+ if (at !== "off") {
207
+ const h = holderOf(n.places[b.mode], b.mode, exists);
208
+ if (h && b.on[b.mode].has(h.key))
209
+ return "cluster";
210
+ if (!h) {
211
+ const o = holderOf(n.places[otherMode(b.mode)], otherMode(b.mode), exists);
212
+ if (o && b.on[otherMode(b.mode)].has(o.key))
213
+ return "ring";
214
+ }
215
+ }
216
+ return linkedOn(n, b, links).length ? "link" : null;
217
+ }
218
+ /** Why the board of view `view` shows note `n`, or null where it does not (decision 17): `all`, the
219
+ * view holds everything (the Project); `items`, its lists (shared/items.ts). */
220
+ export function itemWhy(n, view, b, exists, links) {
221
+ if (view.all)
222
+ return "project";
223
+ const listed = itemIn(view.items, n.id);
224
+ if (listed === "remove")
225
+ return null;
226
+ return derivedWhy(n, b, exists, links) ?? (listed === "add" ? "added" : null);
227
+ }
228
+ /** The cluster keys of a whole graph by each clustering, and its declarations' names, worked out
229
+ * once per graph: whether a note's cluster is still one (decision 18), and whether the declaration
230
+ * a note is attached to is. */
231
+ const graphKeys = new WeakMap();
232
+ export function existsIn(full) {
233
+ const keys = () => {
234
+ const k = graphKeys.get(full) ?? {};
235
+ graphKeys.set(full, k);
236
+ return k;
237
+ };
238
+ const f = (m, key) => {
239
+ const k = keys();
240
+ return (k[m] ??= clusterKeys(m, full)).has(key);
241
+ };
242
+ f.decl = (name) => {
243
+ const k = keys();
244
+ return (k.names ??= new Set(full.map((n) => n.name))).has(name);
245
+ };
246
+ return f;
247
+ }
248
+ /** Whether a board gives each note from what follows from its scope alone (derivedWhy), its view's
249
+ * lists left aside: what Add to view and Remove from this view decide by (shared/items.ts, itemOp),
250
+ * and which notes a copy of the Project takes as added. `nodes` its declarations, `exists` whether a
251
+ * key is a cluster of the whole graph it is of (existsIn), `mode` its clustering, `nowName` a past
252
+ * build's renames (PastGraph.nowName), under whose names then it reads a note's attachment. */
253
+ export function derivesOn(mode, nodes, exists, links, nowName) {
254
+ const b = { ...itemBoard(mode, nodes), then: thenNames(nowName) };
255
+ const index = linksByFile(links);
256
+ return (n) => derivedWhy(n, b, exists, index) !== null;
257
+ }
258
+ /** The project's notes the board of view `view` shows (decision 17), from what it holds alone,
259
+ * without laying it out: `nodes` its declarations, `full` those of the whole graph it is of, `mode`
260
+ * its clustering, `links` the library's links as that board reads them, `nowName` the renames of a
261
+ * past build it is of (PastGraph.nowName). What a view's exported document counts among its
262
+ * authors' traces (shared/doc/authors.ts). */
263
+ export function itemsShownOn(notes, view, mode, nodes, full, links, nowName) {
264
+ if (view.all || !notes.length)
265
+ return [...notes];
266
+ const derives = derivesOn(mode, nodes, existsIn(full), links, nowName);
267
+ return notes.filter((n) => {
268
+ const listed = itemIn(view.items, n.id);
269
+ return listed !== "remove" && (listed === "add" || derives(n));
270
+ });
271
+ }
272
+ /** Where project note `n` is on board `b`, which shows it (itemWhy): at the declaration it is
273
+ * attached to, where the board holds it (a visitor counts); attached to one the board's graph has
274
+ * but the board does not, on the ring, toward what it carries a file linked to on this board; else,
275
+ * attached to none or its declaration gone, in its cluster under the board's clustering where the
276
+ * board draws it; else on the ring — at the angle where it was let go, else toward what it carries
277
+ * a file linked to on this board (`linked`, linkedOn), else about its cluster under the other
278
+ * clustering where that holds a declaration of the board (decision 16), else in the order notes were
279
+ * written. */
280
+ export function itemSpot(n, b, exists, linked = []) {
281
+ const at = declOf(n, b, exists);
282
+ if (at && typeof at === "object")
283
+ return { at: "decl", j: at.j, name: at.name, d: [n.decl.at[0], n.decl.at[1]] };
284
+ if (at === "off")
285
+ return { at: "ring", angle: null, about: linked.length ? { links: [...linked] } : null };
286
+ const m = b.mode;
287
+ const place = n.places[m];
288
+ const h = holderOf(place, m, exists);
289
+ const c = h ? b.stands.get(h.key) : undefined;
290
+ if (h && c !== undefined && place && "in" in place) {
291
+ // its cluster stands at the top level here (a hidden top level): on the ring, about itself
292
+ if (c < 0)
293
+ return { at: "ring", angle: null, about: { mode: m, key: h.key } };
294
+ // the spot it has in the circle drawn for it: the first of its keys that is that circle — a
295
+ // hidden level is in the nearest drawn around it, at its spot there (decision 19) — else its own
296
+ let f = [place.in[h.at][1], place.in[h.at][2]];
297
+ for (let k = h.at; k < place.in.length; k++)
298
+ if (counts(b, place.in[k][0], c)) {
299
+ f = [place.in[k][1], place.in[k][2]];
300
+ break;
301
+ }
302
+ return { at: "cluster", c, f: inside(f) };
303
+ }
304
+ // on the ring: the top level, or a board that does not draw its cluster (it was added, or a link
305
+ // brought it)
306
+ const o = holderOf(n.places[otherMode(m)], otherMode(m), exists);
307
+ const about = linked.length ? { links: [...linked] } : o && b.on[otherMode(m)].has(o.key) ? { mode: otherMode(m), key: o.key } : null;
308
+ return { at: "ring", angle: place && "ring" in place ? place.ring : null, about };
309
+ }
310
+ /** The declarations of board `b` a note on the ring is about, in the board's numbering: those of
311
+ * its cluster, or those its files are linked to — a declaration, or what a namespace or a folder
312
+ * holds on the board. */
313
+ export function aboutMembers(b, about) {
314
+ if (!about)
315
+ return [];
316
+ if ("key" in about)
317
+ return membersOf(b, about.mode, about.key);
318
+ const out = new Set();
319
+ for (const subject of about.links) {
320
+ const s = parseSubject(subject);
321
+ if (!s)
322
+ continue;
323
+ const j = b.index.get(s.name);
324
+ if (s.kind !== "file" && j !== undefined)
325
+ out.add(j);
326
+ if (s.kind !== "decl")
327
+ for (const k of membersOf(b, s.kind === "ns" ? "namespace" : "module", s.name))
328
+ out.add(k);
329
+ }
330
+ return [...out].sort((x, y) => x - y);
331
+ }
332
+ /** Where view note `n` is on its board `b`: at the declaration it is attached to, where the board
333
+ * holds it (a visitor counts), under every clustering; else under its own clustering as it always
334
+ * was, by the keys of the layout drawn (`index`); under the other, on the ring about the innermost
335
+ * cluster of its place that holds a declaration of the board. */
336
+ export function viewSpot(n, b, index) {
337
+ const at = declOf(n, b, NO_GRAPH);
338
+ if (at && typeof at === "object")
339
+ return { at: "decl", j: at.j, name: at.name, d: [n.decl.at[0], n.decl.at[1]] };
340
+ const r = resolvePlace(n.place, b.mode, index);
341
+ if (r)
342
+ return r.c >= 0 ? { at: "offset", c: r.c, d: [r.dx, r.dy] } : { at: "world", x: r.dx, y: r.dy };
343
+ const key = n.place.in.map((e) => e[0]).find((k) => b.on[n.place.mode].has(k));
344
+ return { at: "ring", angle: null, about: key ? { mode: n.place.mode, key } : null };
345
+ }
346
+ /** what a view note's attachment reads where the graph is not asked: off the board or gone, its place
347
+ * stands all the same */
348
+ const NO_GRAPH = () => true;
349
+ /** The place under a layout's clustering of a note attached to declaration `i` of layout `L` (its
350
+ * numbering), as the layout has it: the declaration's own circle and every one around it, each with
351
+ * the spot — its point moved by `d`, in the layout's units — as a fraction of the circle's radius,
352
+ * brought inside it; null at the top level. Where a note attached on a board falls back to under the
353
+ * clustering that board is not shown by, read on the Project's layout of it (the server's). */
354
+ export function declPlace(L, i, d = [0, 0]) {
355
+ const out = [];
356
+ const x = L.x[i] + d[0], y = L.y[i] + d[1];
357
+ for (let c = L.cluster[i] ?? -1; c >= 0; c = L.clusters[c].parent) {
358
+ const C = L.clusters[c];
359
+ const r = Math.max(C.r, 1e-6);
360
+ const [fx, fy] = inside([(x - C.x) / r, (y - C.y) / r]);
361
+ out.push([C.key, Math.round(fx * 1000) / 1000, Math.round(fy * 1000) / 1000]);
362
+ }
363
+ return out.length ? { in: out } : null;
364
+ }
365
+ /** A fraction of a radius brought inside the circle. */
366
+ function inside(f) {
367
+ const len = Math.hypot(f[0], f[1]);
368
+ return len > 1 ? [f[0] / len, f[1] / len] : f;
369
+ }
370
+ /** The place under the board's clustering of a spot of the board (decision 15): in the most specific
371
+ * open cluster there — its key and every key around it, each with the spot as a fraction of the
372
+ * radius of the circle drawn for it, brought inside it — or, where no cluster is there, null: the
373
+ * caller puts it on the ring at the spot's angle. `clusters` are the layout's, as drawn now. */
374
+ export function placeIn(world, clusters, b) {
375
+ const [wx, wy] = world;
376
+ let best = -1;
377
+ clusters.forEach((c, k) => {
378
+ if (!c.open || (c.x - wx) ** 2 + (c.y - wy) ** 2 > c.r * c.r)
379
+ return;
380
+ if (best < 0 || c.depth > clusters[best].depth)
381
+ best = k;
382
+ });
383
+ if (best < 0)
384
+ return null;
385
+ return spotIn(clusters[best].key, world, clusters, b);
386
+ }
387
+ /** The place in cluster `key` (a key of the board's clustering) of a spot of the board: the
388
+ * fraction of the spot in the circle drawn for each of its keys — a key a chain merged into a circle
389
+ * is that circle — those that have no circle of their own here (a hidden level, the top level) left
390
+ * out; null where the board does not draw that cluster. */
391
+ export function spotIn(key, world, clusters, b) {
392
+ const out = [];
393
+ for (const k of keyChain(key)) {
394
+ const c = b.stands.get(k);
395
+ if (c === undefined || !counts(b, k, c))
396
+ continue;
397
+ const C = clusters[c];
398
+ const r = Math.max(C.r, 1e-6);
399
+ const [fx, fy] = inside([(world[0] - C.x) / r, (world[1] - C.y) / r]);
400
+ out.push([k, Math.round(fx * 1000) / 1000, Math.round(fy * 1000) / 1000]);
401
+ }
402
+ return out.length && out[0][0] === key ? { in: out } : null;
403
+ }
404
+ /** The places of `n` notes of the project put at one spot together (files dropped at once, decision
405
+ * 12): the first's (`first`, the place a note let go at `world` takes) and, beside it, the others,
406
+ * `step` apart to the right in the world, in the same cluster and brought inside its circle; on the
407
+ * ring, all at the first's place, which the ring spreads as it spreads every note on it. */
408
+ export function spacedPlaces(first, world, n, step, clusters, b) {
409
+ const key = "in" in first ? first.in[0][0] : null;
410
+ return Array.from({ length: n }, (_, j) => (j && key ? spotIn(key, [world[0] + j * step, world[1]], clusters, b) : null) ?? first);
411
+ }
412
+ /** The place of a note of the project put beside declaration `j` of board `b` (the board's
413
+ * numbering), at `spot`: in the innermost of its keys under the board's clustering that is a circle
414
+ * drawn — a hidden level's note is in the one drawn around it — whatever is folded or however small
415
+ * it is on the screen; null where the declaration is at the top level, for the caller to put the
416
+ * note on the ring (Clip to a note, decision 12). */
417
+ export function besidePlace(j, spot, clusters, b) {
418
+ for (const key of keyChain(b.inner[b.mode][j] ?? "")) {
419
+ const place = spotIn(key, spot, clusters, b);
420
+ if (place)
421
+ return place;
422
+ }
423
+ return null;
424
+ }
425
+ /** A build's moves of whole clusters (decision 18): for each project note whose cluster under a
426
+ * clustering is gone in `after` though `before` had it, the cluster that took all its declarations
427
+ * — each that is still there, by its name or the one a rename of high confidence gave it — as a
428
+ * folder or a namespace renamed does: the same structure under another key, every declaration
429
+ * `K.rest` now `K'.rest`, and at least one of them still there. What it holds that is gone with no
430
+ * rename is gone, not moved. The notes, each with the clustering, the key it had and the place it
431
+ * is carried to; a note whose cluster went elsewhere in pieces, or nowhere, is left as it is, and
432
+ * is in the nearest cluster around it that is left. */
433
+ export function carriedNotes(notes, before, after, renamed) {
434
+ const out = [];
435
+ for (const mode of ["namespace", "module"]) {
436
+ const holding = notes.filter((n) => {
437
+ const p = n.places[mode];
438
+ return !!p && "in" in p;
439
+ });
440
+ if (!holding.length)
441
+ continue;
442
+ const was = innerKeys(mode, before);
443
+ const now = innerKeys(mode, after);
444
+ const keysNow = new Set();
445
+ for (const k of now)
446
+ for (const c of keyChain(k))
447
+ keysNow.add(c);
448
+ const keysThen = new Set();
449
+ for (const k of was)
450
+ for (const c of keyChain(k))
451
+ keysThen.add(c);
452
+ const where = new Map();
453
+ after.forEach((n, i) => where.set(n.name, now[i]));
454
+ const moved = new Map();
455
+ /** the key `K` was moved to as a whole, or null */
456
+ const target = (K) => {
457
+ if (moved.has(K))
458
+ return moved.get(K);
459
+ let to;
460
+ const pre = K + ".";
461
+ for (let i = 0; i < before.length && to !== null; i++) {
462
+ const k = was[i];
463
+ if (k !== K && !k.startsWith(pre))
464
+ continue;
465
+ const name = renamed.get(before[i].name) ?? before[i].name;
466
+ const there = where.get(name);
467
+ // gone with no rename: not moved anywhere
468
+ if (there === undefined)
469
+ continue;
470
+ const rest = k.slice(K.length);
471
+ if (!there.endsWith(rest) || there.length === rest.length) {
472
+ to = null;
473
+ break;
474
+ }
475
+ const key = there.slice(0, there.length - rest.length);
476
+ if (to === undefined)
477
+ to = key;
478
+ else if (to !== key)
479
+ to = null;
480
+ }
481
+ const r = to && to !== K && keysNow.has(to) ? to : null;
482
+ moved.set(K, r);
483
+ return r;
484
+ };
485
+ for (const n of holding) {
486
+ const p = n.places[mode];
487
+ const K = p.in[0][0];
488
+ if (keysNow.has(K) || !keysThen.has(K))
489
+ continue;
490
+ const to = target(K);
491
+ if (!to)
492
+ continue;
493
+ // the spot it had in its cluster, in the one that took it; around it, the spots it had in the
494
+ // clusters that are still around it, else that same spot
495
+ const own = [p.in[0][1], p.in[0][2]];
496
+ const had = new Map(p.in.map((e) => [e[0], [e[1], e[2]]]));
497
+ const chain = keyChain(to).map((k, j) => {
498
+ const f = j === 0 ? own : had.get(k) ?? own;
499
+ return [k, f[0], f[1]];
500
+ });
501
+ out.push({ id: n.id, mode, from: K, to, place: { in: chain } });
502
+ }
503
+ }
504
+ return out;
505
+ }
@@ -0,0 +1,46 @@
1
+ // VisualProof's colours, as the page's light theme has them (web/style.css, `:root`): the kinds
2
+ // (`--k-*`), the Status scheme (`--status-*`), the code's tokens (`--tk-*`), the target's star and
3
+ // a key declaration's diamond.
4
+ // The exported document (shared/doc/) takes its code's colours from here, so that a highlighted
5
+ // line reads as it does on the page; test/doc.test.ts checks that the two agree.
6
+ // Paper is light: the dark theme's values are the page's alone.
7
+ /** A kind's colour, by kind; `external` for a declaration outside the project. */
8
+ export const KIND_COLORS = {
9
+ def: "#0a84ff",
10
+ abbrev: "#40c8e0",
11
+ structure: "#30b158",
12
+ class: "#2fa37a",
13
+ inductive: "#63c75d",
14
+ instance: "#ff9f0a",
15
+ theorem: "#a395e0",
16
+ opaque: "#ff6b3d",
17
+ axiom: "#ff375f",
18
+ notation: "#a2845e",
19
+ external: "#aeaeb2",
20
+ };
21
+ /** The Status scheme's colours (web/src/status.ts, STATUSES): Okabe and Ito's palette. */
22
+ export const STATUS_COLORS = {
23
+ failed: "#4d4d52",
24
+ building: "#56b4e9",
25
+ sorry: "#cc79a7",
26
+ commented: "#2a62c0",
27
+ stale: "#e69f00",
28
+ approved: "#009e73",
29
+ unreviewed: "#c7c7cc",
30
+ trusted: "#b3e2c2",
31
+ };
32
+ /** The code's token colours (web/src/highlight.ts). `sorry` is drawn white on its colour. */
33
+ export const TOKEN_COLORS = {
34
+ keyword: "#ad3da4",
35
+ type: "#3e7fd6",
36
+ comment: "#8a8a8e",
37
+ string: "#c4392b",
38
+ number: "#6c5ce7",
39
+ attr: "#b5762c",
40
+ op: "#ad3da4",
41
+ sorry: "#ff375f",
42
+ };
43
+ /** The target's star. */
44
+ export const STAR_COLOR = "#f5b301";
45
+ /** A key declaration's diamond (`--key-mark`): the star's family, a shade deeper. */
46
+ export const KEY_COLOR = "#d99a00";
@@ -0,0 +1,103 @@
1
+ // The past graph (HistorySpec decisions 17 and 19): a snapshot of the history as a graph of its own,
2
+ // each declaration marked against the graph on show, and the review as that graph reads it. The
3
+ // page's Time Machine draws boards of it (web/src/past.ts, web/src/timemachine.ts); the server makes
4
+ // the same graph to print a view of a past build (server/pastdoc.ts, ExportSpec decision 16), so
5
+ // that the document holds what the board shows.
6
+ //
7
+ // A past graph holds that build's declarations and nothing else (the owner's decision of
8
+ // 2026-10-10): what was added since is not of that build, and is on none of its boards. A declaration
9
+ // renamed since stands under its name then, and knows its name now. Its marks: *ghost*, gone now with
10
+ // no rename carrying it; *changed*, another version now; *renamed*. The versions compared are both
11
+ // made under the renames in force, by the server (`/diff`), which reads what an earlier one refers to
12
+ // under its names now.
13
+ //
14
+ // A board of a past build is a board of that graph and nothing else: its scope evaluated on it
15
+ // (shared/board.ts, boardSet), what is lit computed on it, its numbering its own.
16
+ import { byName } from "./review.js";
17
+ import { Graph } from "./viewgraph.js";
18
+ /** gone now, and no rename since carried it */
19
+ export const PAST_GHOST = 1;
20
+ /** another version now */
21
+ export const PAST_CHANGED = 4;
22
+ /** renamed since: shown under its name then */
23
+ export const PAST_RENAMED = 8;
24
+ /** The git source of a past build: the project's source, the build's own commit; none where the
25
+ * build's commit is not known, or only guessed. */
26
+ function gitThen(now, commit) {
27
+ if (!commit || commit.guessed)
28
+ return { git: undefined };
29
+ return { git: { source: now?.source ?? null, commit: commit.hash, branch: commit.branch, ...(commit.dirty ? { dirty: true } : {}) } };
30
+ }
31
+ /** The past graph of snapshot `snap` (its declarations and its edges, as /graph's parts give them),
32
+ * marked by `diff` (from it to the latest), with the project's description of `now`. */
33
+ export function pastGraph(snap, edges, now, diff) {
34
+ const P = snap.projectCount;
35
+ const nowName = new Map(diff.renamed);
36
+ const thenName = new Map(diff.renamed.map(([a, b]) => [b, a]));
37
+ const removed = new Set(diff.removed);
38
+ const changed = new Set(diff.changed.map((n) => thenName.get(n) ?? n));
39
+ const nodes = snap.nodes;
40
+ const meta = {
41
+ schema: 1,
42
+ layoutVersion: now.meta.layoutVersion,
43
+ // the commit of that build, where it is known; the source is the project's now
44
+ project: { ...now.meta.project, extractedAt: snap.snapshot.at, ...gitThen(now.meta.project.git, snap.snapshot.commit) },
45
+ modules: snap.modules.map((m) => ({ name: m.name, file: null, imports: m.imports })),
46
+ nodes,
47
+ projectCount: P,
48
+ axioms: snap.axioms,
49
+ axiomSets: snap.axiomSets,
50
+ layouts: {},
51
+ edgeCount: edges.length / 3,
52
+ };
53
+ const graph = new Graph(meta, Int32Array.from(edges));
54
+ // a declaration renamed since answers to its name now too: a view's record, the review, a pin, a
55
+ // note and a selection speak the names of now (a move carries them, HistorySpec decision 8), and
56
+ // reach it at this build by them; a name now that another declaration had then stays that one's
57
+ for (const [then, now] of nowName) {
58
+ const i = graph.byName.get(then);
59
+ if (i !== undefined && !graph.byName.has(now))
60
+ graph.byName.set(now, i);
61
+ }
62
+ const marks = new Uint8Array(graph.n);
63
+ for (let i = 0; i < P; i++) {
64
+ const name = nodes[i].name;
65
+ if (removed.has(name))
66
+ marks[i] |= PAST_GHOST;
67
+ if (changed.has(name))
68
+ marks[i] |= PAST_CHANGED;
69
+ if (nowName.has(name))
70
+ marks[i] |= PAST_RENAMED;
71
+ }
72
+ return { snapshot: snap.snapshot, graph, marks, nowName, thenName, kept: new Map(Object.entries(snap.sources ?? {})) };
73
+ }
74
+ /** The review as a past graph reads it (`nowName`: name then → name now, of what was renamed since):
75
+ * what was said of a declaration renamed since is kept under its name now (a move carried it), and
76
+ * is read under its name then too — its opinions, its target mark, its key marks as a member and as
77
+ * a target — so that what reads the review by a declaration's name on that graph (its status, the
78
+ * review box, the queues, a scope's conditions) finds it. `d` itself is left as it is. */
79
+ export function reviewThen(d, nowName) {
80
+ if (!nowName.size)
81
+ return d;
82
+ const opinions = Object.assign(byName(), d.opinions);
83
+ const targets = Object.assign(byName(), d.targets);
84
+ const keys = byName();
85
+ const add = (into, from) => {
86
+ for (const [then, now] of nowName)
87
+ if (Object.hasOwn(from, now) && !Object.hasOwn(from, then))
88
+ into[then] = from[now];
89
+ };
90
+ add(opinions, d.opinions);
91
+ add(targets, d.targets);
92
+ for (const [t, marks] of Object.entries(d.keys)) {
93
+ const m = Object.assign(byName(), marks);
94
+ add(m, marks);
95
+ keys[t] = m;
96
+ }
97
+ add(keys, { ...d.keys, ...keys });
98
+ // a link on a declaration renamed since is read under its name then too (AttachmentSpec decision
99
+ // 6): the past board's panel lists it, and the past build's document prints it
100
+ const thenOf = new Map([...nowName].map(([then, now]) => [`decl(${now})`, `decl(${then})`]));
101
+ const links = d.links?.flatMap((l) => (thenOf.has(l.subject) ? [l, { ...l, subject: thenOf.get(l.subject) }] : [l]));
102
+ return { ...d, opinions, targets, keys, ...(links ? { links } : {}) };
103
+ }