visualproof 0.0.0-stage → 0.1.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 (128) 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/extractor/Extract.lean +1180 -0
  9. package/dist/extractor/Usage.lean +900 -0
  10. package/dist/mcp/visualproof-mcp.mjs +476 -0
  11. package/dist/server/access.js +95 -0
  12. package/dist/server/agent.js +3235 -0
  13. package/dist/server/agentdocs.js +35 -0
  14. package/dist/server/agentlibrary.js +202 -0
  15. package/dist/server/agentnotes.js +701 -0
  16. package/dist/server/agentviews.js +703 -0
  17. package/dist/server/askfollow.js +286 -0
  18. package/dist/server/boardspots.js +159 -0
  19. package/dist/server/dirs.js +175 -0
  20. package/dist/server/doctor.js +186 -0
  21. package/dist/server/events.js +198 -0
  22. package/dist/server/export.js +420 -0
  23. package/dist/server/extract.js +204 -0
  24. package/dist/server/fetchurl.js +321 -0
  25. package/dist/server/figures.js +158 -0
  26. package/dist/server/history.js +741 -0
  27. package/dist/server/historyapi.js +304 -0
  28. package/dist/server/hover.js +595 -0
  29. package/dist/server/identity.js +114 -0
  30. package/dist/server/invites.js +241 -0
  31. package/dist/server/layoutthread.js +51 -0
  32. package/dist/server/library.js +600 -0
  33. package/dist/server/libraryroute.js +211 -0
  34. package/dist/server/live.js +1101 -0
  35. package/dist/server/lostfound.js +199 -0
  36. package/dist/server/main.js +1394 -0
  37. package/dist/server/mcptools.js +497 -0
  38. package/dist/server/pastdoc.js +73 -0
  39. package/dist/server/pipeline.js +156 -0
  40. package/dist/server/poppler.js +251 -0
  41. package/dist/server/presence.js +252 -0
  42. package/dist/server/project.js +222 -0
  43. package/dist/server/prompt.js +53 -0
  44. package/dist/server/readonly.js +152 -0
  45. package/dist/server/readonlygate.js +123 -0
  46. package/dist/server/renames.js +268 -0
  47. package/dist/server/review.js +334 -0
  48. package/dist/server/reviewapi.js +188 -0
  49. package/dist/server/snapshot.js +49 -0
  50. package/dist/server/sourcestore.js +473 -0
  51. package/dist/server/status.js +360 -0
  52. package/dist/server/talk.js +476 -0
  53. package/dist/server/tokens.js +221 -0
  54. package/dist/server/usage.js +501 -0
  55. package/dist/server/versions.js +82 -0
  56. package/dist/server/view.js +157 -0
  57. package/dist/server/views.js +1167 -0
  58. package/dist/shared/askfollow.js +46 -0
  59. package/dist/shared/board.js +332 -0
  60. package/dist/shared/build.js +2 -0
  61. package/dist/shared/collab.js +148 -0
  62. package/dist/shared/comment.js +285 -0
  63. package/dist/shared/context.js +157 -0
  64. package/dist/shared/doc/authors.js +98 -0
  65. package/dist/shared/doc/build.js +358 -0
  66. package/dist/shared/doc/latex.js +455 -0
  67. package/dist/shared/doc/markdown.js +159 -0
  68. package/dist/shared/doc/model.js +37 -0
  69. package/dist/shared/doc/order.js +107 -0
  70. package/dist/shared/doc/proof.js +272 -0
  71. package/dist/shared/doc/sections.js +93 -0
  72. package/dist/shared/doc/settings.js +75 -0
  73. package/dist/shared/edgecodec.js +102 -0
  74. package/dist/shared/editor.js +64 -0
  75. package/dist/shared/excerpt.js +87 -0
  76. package/dist/shared/graph.js +16 -0
  77. package/dist/shared/items.js +92 -0
  78. package/dist/shared/keys.js +203 -0
  79. package/dist/shared/layout.js +926 -0
  80. package/dist/shared/lexer.js +130 -0
  81. package/dist/shared/library.js +711 -0
  82. package/dist/shared/lostfound.js +150 -0
  83. package/dist/shared/moves.js +243 -0
  84. package/dist/shared/notecard.js +295 -0
  85. package/dist/shared/noteplace.js +505 -0
  86. package/dist/shared/palette.js +46 -0
  87. package/dist/shared/pastgraph.js +95 -0
  88. package/dist/shared/places.js +80 -0
  89. package/dist/shared/projectnotes.js +244 -0
  90. package/dist/shared/query.js +810 -0
  91. package/dist/shared/review.js +642 -0
  92. package/dist/shared/reviewqueue.js +333 -0
  93. package/dist/shared/ring.js +366 -0
  94. package/dist/shared/scope.js +255 -0
  95. package/dist/shared/scopeedit.js +81 -0
  96. package/dist/shared/search.js +191 -0
  97. package/dist/shared/separate.js +178 -0
  98. package/dist/shared/sharesnap.js +280 -0
  99. package/dist/shared/sticky.js +501 -0
  100. package/dist/shared/timeline.js +73 -0
  101. package/dist/shared/titles.js +298 -0
  102. package/dist/shared/viewgraph.js +139 -0
  103. package/dist/shared/views.js +859 -0
  104. package/dist/web/bundle/app.js +48733 -0
  105. package/dist/web/bundle/edges.js +409 -0
  106. package/dist/web/bundle/layout.js +1010 -0
  107. package/dist/web/bundle/pdfworker.js +48322 -0
  108. package/dist/web/index.html +239 -0
  109. package/dist/web/src/motion.js +430 -0
  110. package/dist/web/src/past.js +224 -0
  111. package/dist/web/src/scene.js +787 -0
  112. package/dist/web/style.css +2312 -0
  113. package/docs/agent-api/README.md +91 -0
  114. package/docs/agent-api/advice.md +108 -0
  115. package/docs/agent-api/export.md +120 -0
  116. package/docs/agent-api/following.md +140 -0
  117. package/docs/agent-api/history.md +142 -0
  118. package/docs/agent-api/library.md +65 -0
  119. package/docs/agent-api/mcp.md +104 -0
  120. package/docs/agent-api/notes.md +93 -0
  121. package/docs/agent-api/reading.md +166 -0
  122. package/docs/agent-api/requests.md +94 -0
  123. package/docs/agent-api/talking.md +100 -0
  124. package/docs/agent-api/tcb.md +135 -0
  125. package/docs/agent-api/views.md +124 -0
  126. package/docs/agent-api/words.md +56 -0
  127. package/package.json +39 -4
  128. package/test/toolchains.json +5 -0
@@ -0,0 +1,430 @@
1
+ // Elastic motion, computed along the hierarchy (CanvasSpec.md, "Motion").
2
+ //
3
+ // Every cluster is a frame: its direct children are the bodies it simulates, in its own local
4
+ // coordinates — nodes as small discs, sub-clusters as discs of their drawn radius. Forces:
5
+ // - the anchor spring pulls each body back to its anchor (elastic return);
6
+ // - collisions keep siblings apart (resolved positionally, which is stable at any stiffness);
7
+ // - dependency springs join the two children of the lowest common ancestor of an edge's ends,
8
+ // the same decomposition a bundled edge follows, with the anchors' distance as rest length:
9
+ // at rest they do nothing, when a body is moved what it is tied to follows and springs back.
10
+ // Collisions act while something is being dragged: that is when bodies must make way. Once let
11
+ // go, every body returns to its anchor straight through the others, so nothing can get stuck
12
+ // behind a neighbour in a tight packing (anchors never overlap, so the rest is clean).
13
+ // A frame sleeps once nothing in it moves. Moving a cluster body carries its subtree for free,
14
+ // since everything under it is local. During a drag only, a contact pass lets the dragged body
15
+ // push individual nodes of other clusters, so what it touches makes way.
16
+ import { SITE_BODY, SITE_STMT } from "../../shared/graph.js";
17
+ import { LEAF_R } from "./scene.js";
18
+ const K_ANCHOR = 140; // s^-2: comes back in about half a second
19
+ const DAMPING = 2 * 0.62 * Math.sqrt(K_ANCHOR); // slightly under critical: a liquid settle
20
+ const K_DEP = 18;
21
+ const SUBSTEP = 1 / 120;
22
+ const REST = 0.03;
23
+ const ROOT = -1;
24
+ export class Motion {
25
+ S;
26
+ vcx;
27
+ vcy;
28
+ vnx;
29
+ vny;
30
+ /** springs per frame (cluster id, or -1 for the root), between body ids */
31
+ springs = new Map();
32
+ awake = new Set();
33
+ /** the body under the pointer: kept where the pointer puts it */
34
+ drag = null;
35
+ /** the dragged body's anchor before the drag, in its frame: what its springs measure their rest
36
+ * length from, so that what it is tied to follows it and springs back after */
37
+ dragFrom = null;
38
+ /** visible nodes near a world point; the canvas answers from its hit grid */
39
+ near = () => [];
40
+ constructor(scene, g) {
41
+ this.S = scene;
42
+ this.vcx = new Float32Array(scene.nc);
43
+ this.vcy = new Float32Array(scene.nc);
44
+ this.vnx = new Float32Array(scene.n);
45
+ this.vny = new Float32Array(scene.n);
46
+ this.buildSprings(g);
47
+ }
48
+ /** Body ids: a cluster is its index, a node is nc + its index. */
49
+ frameOf(body) {
50
+ const S = this.S;
51
+ return body < S.nc ? S.parent[body] : S.nodeCluster[body - S.nc];
52
+ }
53
+ /** An edge u → v becomes a spring in the frame where the two first meet. */
54
+ buildSprings(g) {
55
+ const S = this.S;
56
+ const count = new Map(); // frame → (pair key → count)
57
+ const up = (i) => {
58
+ // (frame, body) pairs from the node up to the root
59
+ const frames = [];
60
+ const bodies = [];
61
+ let body = S.nc + i;
62
+ let frame = S.nodeCluster[i];
63
+ for (;;) {
64
+ frames.push(frame);
65
+ bodies.push(body);
66
+ if (frame === ROOT)
67
+ break;
68
+ body = frame;
69
+ frame = S.parent[frame];
70
+ }
71
+ return [frames, bodies];
72
+ };
73
+ const memo = new Map();
74
+ const path = (i) => {
75
+ let p = memo.get(i);
76
+ if (!p)
77
+ memo.set(i, (p = up(i)));
78
+ return p;
79
+ };
80
+ for (let u = 0; u < g.n; u++) {
81
+ for (let e = g.outStart[u]; e < g.outStart[u + 1]; e++) {
82
+ if (!(g.outSite[e] & (SITE_STMT | SITE_BODY)))
83
+ continue;
84
+ const v = g.outDst[e];
85
+ const [fu, bu] = path(u);
86
+ const [fv, bv] = path(v);
87
+ // lowest frame on both paths
88
+ const at = new Map();
89
+ fu.forEach((f, k) => at.set(f, k));
90
+ for (let k = 0; k < fv.length; k++) {
91
+ const ku = at.get(fv[k]);
92
+ if (ku === undefined)
93
+ continue;
94
+ const a = bu[ku], b = bv[k];
95
+ if (a !== b) {
96
+ const key = a < b ? a * 4_194_304 + b : b * 4_194_304 + a;
97
+ let m = count.get(fv[k]);
98
+ if (!m)
99
+ count.set(fv[k], (m = new Map()));
100
+ m.set(key, (m.get(key) ?? 0) + 1);
101
+ }
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ for (const [frame, m] of count) {
107
+ const deg = new Map();
108
+ for (const key of m.keys()) {
109
+ const a = Math.floor(key / 4_194_304), b = key % 4_194_304;
110
+ deg.set(a, (deg.get(a) ?? 0) + 1);
111
+ deg.set(b, (deg.get(b) ?? 0) + 1);
112
+ }
113
+ const list = [];
114
+ for (const [key, n] of m) {
115
+ const a = Math.floor(key / 4_194_304), b = key % 4_194_304;
116
+ // heavier bundles pull harder; busy bodies are not pulled apart by their many ties
117
+ list.push({ a, b, k: (K_DEP * Math.log1p(n)) / Math.sqrt(Math.max(deg.get(a), deg.get(b))) });
118
+ }
119
+ this.springs.set(frame, list);
120
+ }
121
+ }
122
+ wake(frame) {
123
+ this.awake.add(frame);
124
+ }
125
+ wakeBody(body) {
126
+ this.awake.add(this.frameOf(body));
127
+ }
128
+ get moving() {
129
+ return this.awake.size > 0;
130
+ }
131
+ /** Wake every frame whose bodies are away from their anchors (after folds moved anchors). */
132
+ wakeDisplaced() {
133
+ const S = this.S;
134
+ for (let c = 0; c < S.nc; c++) {
135
+ if (Math.abs(S.curCX[c] - S.ancCX[c]) > REST || Math.abs(S.curCY[c] - S.ancCY[c]) > REST)
136
+ this.awake.add(S.parent[c]);
137
+ }
138
+ for (let i = 0; i < S.n; i++) {
139
+ if (Math.abs(S.curNX[i] - S.ancNX[i]) > REST || Math.abs(S.curNY[i] - S.ancNY[i]) > REST)
140
+ this.awake.add(S.nodeCluster[i]);
141
+ }
142
+ }
143
+ // body accessors
144
+ px(b) {
145
+ return b < this.S.nc ? this.S.curCX[b] : this.S.curNX[b - this.S.nc];
146
+ }
147
+ py(b) {
148
+ return b < this.S.nc ? this.S.curCY[b] : this.S.curNY[b - this.S.nc];
149
+ }
150
+ setP(b, x, y) {
151
+ if (b < this.S.nc) {
152
+ this.S.curCX[b] = x;
153
+ this.S.curCY[b] = y;
154
+ }
155
+ else {
156
+ this.S.curNX[b - this.S.nc] = x;
157
+ this.S.curNY[b - this.S.nc] = y;
158
+ }
159
+ }
160
+ ax(b) {
161
+ return b < this.S.nc ? this.S.ancCX[b] : this.S.ancNX[b - this.S.nc];
162
+ }
163
+ ay(b) {
164
+ return b < this.S.nc ? this.S.ancCY[b] : this.S.ancNY[b - this.S.nc];
165
+ }
166
+ radius(b) {
167
+ return b < this.S.nc ? Math.max(this.S.drawnR[b], LEAF_R) : LEAF_R;
168
+ }
169
+ /** Advance the awake frames by dt ms. */
170
+ step(dtMs) {
171
+ const S = this.S;
172
+ let steps = Math.max(1, Math.min(8, Math.round(dtMs / 1000 / SUBSTEP)));
173
+ while (steps-- > 0) {
174
+ for (const f of [...this.awake])
175
+ this.stepFrame(f);
176
+ }
177
+ if (this.drag !== null)
178
+ this.contact();
179
+ // a frame whose contents now reach beyond its disc pushes on its siblings: wake the parent
180
+ for (const f of [...this.awake]) {
181
+ if (f === ROOT)
182
+ continue;
183
+ let reach = 0;
184
+ for (const c of S.kidsC[f])
185
+ reach = Math.max(reach, Math.hypot(S.curCX[c], S.curCY[c]) + S.drawnR[c]);
186
+ for (const i of S.kidsN[f])
187
+ if (S.nodeRep[i] < 0)
188
+ reach = Math.max(reach, Math.hypot(S.curNX[i], S.curNY[i]) + LEAF_R);
189
+ if (reach > S.curR[f] + 2) {
190
+ S.curR[f] = reach;
191
+ this.awake.add(S.parent[f]);
192
+ }
193
+ }
194
+ }
195
+ /** A frame's bodies and springs in local indices, cached until what is shut changes. */
196
+ frames = new Map();
197
+ data(f) {
198
+ const S = this.S;
199
+ const hit = this.frames.get(f);
200
+ if (hit && hit.rep === S.repVersion)
201
+ return hit;
202
+ const ids = [];
203
+ const kc = f === ROOT ? S.rootC : S.kidsC[f];
204
+ const kn = f === ROOT ? S.rootN : S.kidsN[f];
205
+ for (const c of kc)
206
+ if (S.rep[c] < 0 || S.rep[c] === c)
207
+ ids.push(c);
208
+ const nBig = ids.length;
209
+ for (const i of kn)
210
+ if (S.nodeRep[i] < 0)
211
+ ids.push(S.nc + i);
212
+ const index = new Map();
213
+ ids.forEach((b, k) => index.set(b, k));
214
+ const sa = [], sb = [], sk = [];
215
+ for (const sp of this.springs.get(f) ?? []) {
216
+ const a = index.get(sp.a), b = index.get(sp.b);
217
+ if (a === undefined || b === undefined)
218
+ continue;
219
+ sa.push(a);
220
+ sb.push(b);
221
+ sk.push(sp.k);
222
+ }
223
+ const n = ids.length;
224
+ const d = {
225
+ rep: S.repVersion, ids: Int32Array.from(ids), nBig,
226
+ sa: Int32Array.from(sa), sb: Int32Array.from(sb), sk: Float64Array.from(sk),
227
+ x: new Float64Array(n), y: new Float64Array(n), vx: new Float64Array(n), vy: new Float64Array(n),
228
+ ax: new Float64Array(n), ay: new Float64Array(n), r: new Float64Array(n), fx: new Float64Array(n), fy: new Float64Array(n),
229
+ cell: new Float64Array(n), order: new Int32Array(n),
230
+ };
231
+ this.frames.set(f, d);
232
+ return d;
233
+ }
234
+ stepFrame(f) {
235
+ const S = this.S;
236
+ // a frame inside a shut cluster is not drawn: everything rests at its anchor
237
+ if (f !== ROOT && S.rep[f] >= 0) {
238
+ for (const c of S.kidsC[f])
239
+ this.rest(c);
240
+ for (const i of S.kidsN[f])
241
+ this.rest(S.nc + i);
242
+ this.awake.delete(f);
243
+ return;
244
+ }
245
+ const D = this.data(f);
246
+ const n = D.ids.length;
247
+ const { x, y, vx, vy, ax, ay, r, fx, fy } = D;
248
+ let drag = -1;
249
+ for (let k = 0; k < n; k++) {
250
+ const b = D.ids[k];
251
+ x[k] = this.px(b);
252
+ y[k] = this.py(b);
253
+ vx[k] = this.vx(b);
254
+ vy[k] = this.vy(b);
255
+ ax[k] = this.ax(b);
256
+ ay[k] = this.ay(b);
257
+ r[k] = this.radius(b);
258
+ if (b === this.drag)
259
+ drag = k;
260
+ fx[k] = K_ANCHOR * (ax[k] - x[k]) - DAMPING * vx[k];
261
+ fy[k] = K_ANCHOR * (ay[k] - y[k]) - DAMPING * vy[k];
262
+ }
263
+ for (let q = 0; q < D.sa.length; q++) {
264
+ const a = D.sa[q], b = D.sb[q];
265
+ const dx = x[b] - x[a], dy = y[b] - y[a];
266
+ const d = Math.hypot(dx, dy) || 1e-6;
267
+ const from = this.dragFrom;
268
+ const [pax, pay] = a === drag && from ? from : [ax[a], ay[a]];
269
+ const [pbx, pby] = b === drag && from ? from : [ax[b], ay[b]];
270
+ const rest = Math.hypot(pbx - pax, pby - pay);
271
+ const f0 = (D.sk[q] * (d - rest)) / d;
272
+ fx[a] += f0 * dx;
273
+ fy[a] += f0 * dy;
274
+ fx[b] -= f0 * dx;
275
+ fy[b] -= f0 * dy;
276
+ }
277
+ const dt = SUBSTEP;
278
+ let energy = 0;
279
+ for (let k = 0; k < n; k++) {
280
+ if (k === drag) {
281
+ vx[k] = vy[k] = 0;
282
+ continue;
283
+ }
284
+ vx[k] += fx[k] * dt;
285
+ vy[k] += fy[k] * dt;
286
+ x[k] += vx[k] * dt;
287
+ y[k] += vy[k] * dt;
288
+ }
289
+ if (this.drag !== null)
290
+ this.collide(D, drag);
291
+ // asleep when nothing in the frame moves any more; a body held off its anchor by a
292
+ // neighbour (a pin dropped on it) rests where it is
293
+ for (let k = 0; k < n; k++)
294
+ energy = Math.max(energy, Math.hypot(vx[k], vy[k]) * dt);
295
+ if (drag >= 0)
296
+ energy = Infinity;
297
+ const sleep = energy < REST * 0.05;
298
+ for (let k = 0; k < n; k++) {
299
+ const b = D.ids[k];
300
+ if (sleep) {
301
+ if (Math.hypot(x[k] - ax[k], y[k] - ay[k]) < 0.5) {
302
+ x[k] = ax[k];
303
+ y[k] = ay[k];
304
+ }
305
+ vx[k] = vy[k] = 0;
306
+ }
307
+ this.setP(b, x[k], y[k]);
308
+ this.setV(b, vx[k], vy[k]);
309
+ }
310
+ if (sleep)
311
+ this.awake.delete(f);
312
+ }
313
+ /** Push overlapping siblings apart, the lighter one more; the dragged body does not yield.
314
+ * Discs are checked against everything; nodes against their grid neighbours, found by
315
+ * sorting the bodies by cell. */
316
+ collide(D, drag) {
317
+ const { x, y, r, cell, order } = D;
318
+ const n = D.ids.length;
319
+ const CELL = 2 * LEAF_R + 2;
320
+ const W = 1 << 20;
321
+ const resolve = (a, b) => {
322
+ const dx = x[b] - x[a], dy = y[b] - y[a];
323
+ const need = r[a] + r[b] - 0.5;
324
+ const d2 = dx * dx + dy * dy;
325
+ if (d2 >= need * need)
326
+ return;
327
+ const d = Math.sqrt(d2) || 1e-3;
328
+ const push = need - d;
329
+ const wa = a === drag ? 0 : b === drag ? 1 : (r[b] * r[b]) / (r[a] * r[a] + r[b] * r[b]);
330
+ const ux = d2 ? dx / d : 1, uy = d2 ? dy / d : 0;
331
+ x[a] -= ux * push * wa;
332
+ y[a] -= uy * push * wa;
333
+ x[b] += ux * push * (1 - wa);
334
+ y[b] += uy * push * (1 - wa);
335
+ };
336
+ for (let a = 0; a < D.nBig; a++)
337
+ for (let b = a + 1; b < n; b++)
338
+ resolve(a, b);
339
+ const small = n - D.nBig;
340
+ if (small < 2)
341
+ return;
342
+ for (let k = D.nBig; k < n; k++) {
343
+ cell[k] = Math.floor(x[k] / CELL) * W + Math.floor(y[k] / CELL);
344
+ order[k - D.nBig] = k;
345
+ }
346
+ const ord = order.subarray(0, small);
347
+ ord.sort((p, q) => cell[p] - cell[q]);
348
+ const keys = new Float64Array(small);
349
+ for (let k = 0; k < small; k++)
350
+ keys[k] = cell[ord[k]];
351
+ const find = (key) => {
352
+ let lo = 0, hi = small;
353
+ while (lo < hi) {
354
+ const mid = (lo + hi) >> 1;
355
+ if (keys[mid] < key)
356
+ lo = mid + 1;
357
+ else
358
+ hi = mid;
359
+ }
360
+ return lo;
361
+ };
362
+ for (let s = 0; s < small; s++) {
363
+ const a = ord[s];
364
+ const key = keys[s];
365
+ // the same cell after this one, then the neighbouring cells "ahead" of it
366
+ for (let t = s + 1; t < small && keys[t] === key; t++)
367
+ resolve(a, ord[t]);
368
+ for (const off of [W, 1, W + 1, W - 1]) {
369
+ for (let t = find(key + off); t < small && keys[t] === key + off; t++)
370
+ resolve(a, ord[t]);
371
+ }
372
+ }
373
+ }
374
+ /** The dragged body pushes the nodes it touches, whatever cluster they are in. */
375
+ contact() {
376
+ const S = this.S;
377
+ const d = this.drag;
378
+ const own = this.frameOf(d);
379
+ const wx = d < S.nc ? S.cwx[d] : S.nwx[d - S.nc];
380
+ const wy = d < S.nc ? S.cwy[d] : S.nwy[d - S.nc];
381
+ const rd = this.radius(d) * (d < S.nc ? this.scaleOf(own) : 1);
382
+ for (const j of this.near(wx, wy, rd + LEAF_R * 2)) {
383
+ if (S.nc + j === d || S.nodeCluster[j] === own)
384
+ continue;
385
+ // inside the dragged cluster: it carries those, it does not push them
386
+ if (d < S.nc && this.inside(j, d))
387
+ continue;
388
+ const dx = S.nwx[j] - wx, dy = S.nwy[j] - wy;
389
+ const dist = Math.hypot(dx, dy) || 1e-3;
390
+ const need = rd + LEAF_R + 1;
391
+ if (dist >= need)
392
+ continue;
393
+ const f = S.nodeCluster[j];
394
+ const s = this.scaleOf(f) || 1;
395
+ const push = (need - dist) / s;
396
+ S.curNX[j] += (dx / dist) * push;
397
+ S.curNY[j] += (dy / dist) * push;
398
+ this.awake.add(f);
399
+ }
400
+ }
401
+ inside(i, c) {
402
+ for (let x = this.S.nodeCluster[i]; x >= 0; x = this.S.parent[x])
403
+ if (x === c)
404
+ return true;
405
+ return false;
406
+ }
407
+ scaleOf(f) {
408
+ return f === ROOT ? 1 : this.S.scale[f];
409
+ }
410
+ vx(b) {
411
+ return b < this.S.nc ? this.vcx[b] : this.vnx[b - this.S.nc];
412
+ }
413
+ vy(b) {
414
+ return b < this.S.nc ? this.vcy[b] : this.vny[b - this.S.nc];
415
+ }
416
+ setV(b, x, y) {
417
+ if (b < this.S.nc) {
418
+ this.vcx[b] = x;
419
+ this.vcy[b] = y;
420
+ }
421
+ else {
422
+ this.vnx[b - this.S.nc] = x;
423
+ this.vny[b - this.S.nc] = y;
424
+ }
425
+ }
426
+ rest(b) {
427
+ this.setP(b, this.ax(b), this.ay(b));
428
+ this.setV(b, 0, 0);
429
+ }
430
+ }
@@ -0,0 +1,224 @@
1
+ // The Time Machine's model (HistorySpec decisions 16–19), without the page: the ruler — a tick per
2
+ // build at equal intervals, which ticks are long, how wide it is drawn and where it scrolls — and
3
+ // which build a drag or a step lands on, which build the reader is going to and which the board shows
4
+ // meanwhile (`Seek`), and how a board of a past graph is laid out and marked. The past graph a
5
+ // snapshot makes — that build's declarations alone, each marked against the graph on show — is
6
+ // shared/pastgraph.ts's, which the server makes too to print a view of a past build. web/src/timemachine.ts draws it and fetches what it reads; the tests read it here.
7
+ import { memoryOf } from "../../shared/layout.js";
8
+ import { PAST_CHANGED, PAST_GHOST, PAST_RENAMED } from "../../shared/pastgraph.js";
9
+ // the past graph and its marks are shared with the server, which prints a view of a past build from
10
+ // the same graph (shared/pastgraph.ts)
11
+ export { PAST_CHANGED, PAST_GHOST, PAST_RENAMED, pastGraph, reviewThen } from "../../shared/pastgraph.js";
12
+ /** a day in the reader's time, to compare two builds' */
13
+ const dayOf = (t) => new Date(t).toDateString();
14
+ export function timeline(snaps) {
15
+ const sorted = [...snaps].sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : a.id - b.id));
16
+ const ticks = sorted.map((s, k) => {
17
+ const at = Date.parse(s.at);
18
+ const before = k > 0 ? sorted[k - 1] : null;
19
+ const day = !before || dayOf(at) !== dayOf(Date.parse(before.at));
20
+ const major = !before || (s.commit || before.commit ? s.commit?.hash !== before.commit?.hash : day);
21
+ return { id: s.id, at, approx: !!s.approx, major, day };
22
+ });
23
+ return { ticks };
24
+ }
25
+ /** The ruler's measures, in pixels: the inset at either end; the closest two ticks are drawn, which
26
+ * keeps every tick one a pointer can land on and its hollow head clear of the next; the farthest,
27
+ * so that a few builds do not stretch across the bar; the narrowest the ruler is, so that it stays
28
+ * something to grab. */
29
+ export const RULER = { pad: 7, minStep: 8, maxStep: 22, minWidth: 90 };
30
+ /** The ruler of `n` builds in `room` pixels. Few builds are drawn at most `maxStep` apart, the ruler
31
+ * only as wide as they need (and `minWidth` at least, the ticks spread across it); where `n` builds
32
+ * do not fit `minStep` apart, the ruler takes the whole room and its strip scrolls. */
33
+ export function ruler(n, room) {
34
+ const { pad, minStep, maxStep, minWidth } = RULER;
35
+ if (n <= 1) {
36
+ const w = Math.min(room, minWidth);
37
+ return { n, step: 0, width: w, strip: w };
38
+ }
39
+ let step = Math.max(minStep, Math.min(maxStep, (room - 2 * pad) / (n - 1)));
40
+ let width = Math.min(room, (n - 1) * step + 2 * pad);
41
+ if (width < minWidth && room > width) {
42
+ width = Math.min(room, minWidth);
43
+ step = (width - 2 * pad) / (n - 1);
44
+ }
45
+ return { n, step, width, strip: Math.max(width, (n - 1) * step + 2 * pad) };
46
+ }
47
+ /** Where the `i`th tick is on the strip; a lone one stands in the middle. */
48
+ export function tickX(r, i) {
49
+ return r.n <= 1 ? r.width / 2 : RULER.pad + i * r.step;
50
+ }
51
+ /** How far the strip scrolls at most. */
52
+ export const maxScroll = (r) => Math.max(0, r.strip - r.width);
53
+ /** The scroll that shows the `i`th tick with a margin of two steps (a quarter of the window at most)
54
+ * on either side, moving as little as it can from `scroll`. */
55
+ export function keepInView(r, scroll, i) {
56
+ const x = tickX(r, i);
57
+ const margin = Math.min(2 * r.step, r.width / 4);
58
+ let s = Math.min(Math.max(0, scroll), maxScroll(r));
59
+ if (x - margin < s)
60
+ s = x - margin;
61
+ else if (x + margin > s + r.width)
62
+ s = x + margin - r.width;
63
+ return Math.min(Math.max(0, s), maxScroll(r));
64
+ }
65
+ /** The tick nearest to a place on the strip (pixels from its left): where a drag lands. */
66
+ export function nearestTick(t, r, x) {
67
+ const n = t.ticks.length;
68
+ if (!n)
69
+ return null;
70
+ const i = n === 1 ? 0 : Math.round((x - RULER.pad) / r.step);
71
+ return t.ticks[Math.min(n - 1, Math.max(0, i))].id;
72
+ }
73
+ /** The snapshot one step from `id` (← −1, → +1), in build order; null past either end. */
74
+ export function step(t, id, dir) {
75
+ const k = t.ticks.findIndex((x) => x.id === id);
76
+ if (k < 0)
77
+ return t.ticks.length ? t.ticks[dir < 0 ? 0 : t.ticks.length - 1].id : null;
78
+ return t.ticks[k + dir]?.id ?? null;
79
+ }
80
+ // ------------------------------------------------------------------------------------- browsing
81
+ /** how many builds beyond the one aimed at, in the way the reader goes, are laid out ahead */
82
+ export const AHEAD = 2;
83
+ /** Where the reader is going on the timeline, and what the board shows meanwhile (the owner's
84
+ * decision of 2026-10-09: a drag loads the builds it crosses, not only the one it is let go on).
85
+ * The reader aims at a build — the one under the pointer as it drags, a step, the build a followed
86
+ * reader is at — and the board shows it once it is ready, keeping the build it shows until then.
87
+ * The latest aim wins: a build the reader has left is never shown, though its load finishes after a
88
+ * later one's, and what is in flight for it is stopped unless it is of use ahead — the builds
89
+ * beside the one aimed at are fetched, and the two beyond it in the way the reader goes laid out,
90
+ * so that a slow drag finds the next ones ready. */
91
+ export class Seek {
92
+ /** the builds, oldest first (the timeline's ticks) */
93
+ order = [];
94
+ /** the build aimed at: the bar shows it, presence carries it */
95
+ target = null;
96
+ /** the build the board shows */
97
+ shown = null;
98
+ /** which way the reader last went: −1 back, +1 on */
99
+ dir = -1;
100
+ /** whether the reader browses (the Time Machine is open): only then is anything wanted ahead */
101
+ browsing = false;
102
+ /** Start at the build the board shows (the Time Machine opened), or stop (left: null, or the build
103
+ * the view is kept at): nothing aimed at but it. */
104
+ reset(at, browsing) {
105
+ this.target = this.shown = at;
106
+ this.browsing = browsing;
107
+ }
108
+ /** Aim at a build. */
109
+ aim(id) {
110
+ const a = this.target === null ? -1 : this.order.indexOf(this.target);
111
+ const b = this.order.indexOf(id);
112
+ if (a >= 0 && b >= 0 && a !== b)
113
+ this.dir = b < a ? -1 : 1;
114
+ this.target = id;
115
+ }
116
+ /** A build's graph and layout are ready: true where the board is to show it now — it is the build
117
+ * aimed at, and not on show (the board shows it from now on); false for one the reader has left,
118
+ * which is dropped, and for the one on show. */
119
+ ready(id) {
120
+ if (id !== this.target || id === this.shown)
121
+ return false;
122
+ this.shown = id;
123
+ return true;
124
+ }
125
+ /** The board does not show the build aimed at yet. */
126
+ get waiting() {
127
+ return this.target !== null && this.target !== this.shown;
128
+ }
129
+ /** The build beside the one aimed at, `dir` away; null past either end, or while not browsing. */
130
+ beside(dir) {
131
+ if (!this.browsing || this.target === null)
132
+ return null;
133
+ const k = this.order.indexOf(this.target);
134
+ return k < 0 ? null : this.order[k + dir] ?? null;
135
+ }
136
+ /** The builds beyond the one aimed at, in the way the reader goes, nearest first: laid out ahead. */
137
+ get beyond() {
138
+ const out = [];
139
+ if (!this.browsing || this.target === null)
140
+ return out;
141
+ const k = this.order.indexOf(this.target);
142
+ for (let d = 1; k >= 0 && d <= AHEAD; d++) {
143
+ const id = this.order[k + d * this.dir];
144
+ if (id === undefined)
145
+ break;
146
+ out.push(id);
147
+ }
148
+ return out;
149
+ }
150
+ /** How much what is fetched for a build is wanted: 2 the build aimed at, or the one shown; 1 a
151
+ * build beside the one aimed at or beyond it, fetched ahead; 0 none — what is in flight for it is
152
+ * stopped. */
153
+ fetchWant(id) {
154
+ if (id === this.target || id === this.shown)
155
+ return 2;
156
+ return id === this.beside(-1) || id === this.beside(1) || this.beyond.includes(id) ? 1 : 0;
157
+ }
158
+ /** How much a build's layout is wanted, the more the sooner: 3 the build aimed at, or the one
159
+ * shown; then the builds beyond it, the nearest first; 0 none — dropped before it starts, stopped
160
+ * where it runs. */
161
+ layoutWant(id) {
162
+ if (id === this.target || id === this.shown)
163
+ return AHEAD + 1;
164
+ const k = this.beyond.indexOf(id);
165
+ return k < 0 ? 0 : AHEAD - k;
166
+ }
167
+ }
168
+ /** The latest build's snapshot: the graph on show. */
169
+ export function latestOf(snaps) {
170
+ let best = null;
171
+ for (const s of snaps)
172
+ if (!best || s.at > best.at || (s.at === best.at && s.id > best.id))
173
+ best = s;
174
+ return best;
175
+ }
176
+ // ------------------------------------------------------------------------------------ the graph
177
+ /** The memory a past board is laid out from where it has none of its own (shared/layout.ts,
178
+ * stabilize): the layout of the board of now, so that what both have stays where it is; a
179
+ * declaration renamed since is remembered under its name then. */
180
+ export function pastMemory(layout, nodes, nowName) {
181
+ return renamedMemory(memoryOf(layout, nodes), nowName);
182
+ }
183
+ /** A memory of the board of now read for a past build: a declaration renamed since remembered under
184
+ * its name then too. `mem` is left as it is (the page keeps it for every build): a copy where a
185
+ * rename adds a name, else `mem` itself. */
186
+ export function renamedMemory(mem, nowName) {
187
+ let out = null;
188
+ for (const [then, now] of nowName) {
189
+ if (!mem.nodes[now] || mem.nodes[then])
190
+ continue;
191
+ out ??= { nodes: { ...mem.nodes }, clusters: mem.clusters };
192
+ out.nodes[then] = mem.nodes[now];
193
+ }
194
+ return out ?? mem;
195
+ }
196
+ /** The key of a past board's layout as the Time Machine keeps it: the snapshot, what the board holds,
197
+ * the clustering, the levels it hides and the memory it is laid out from. */
198
+ export function pastLayoutKey(snapshot, set, mode, hidden, memory) {
199
+ return [snapshot, set, mode, [...hidden].sort().join("\u0001"), memory].join("\u0000");
200
+ }
201
+ /** A declaration's marks in words, for its card, the tooltip and the panel. */
202
+ export function markWords(marks, now) {
203
+ const out = [];
204
+ if (marks & PAST_GHOST)
205
+ out.push("gone now");
206
+ if (marks & PAST_RENAMED)
207
+ out.push(now ? `now ${now}` : "renamed since");
208
+ if (marks & PAST_CHANGED)
209
+ out.push("changed since");
210
+ return out;
211
+ }
212
+ /** The marks of the declarations of a board (`toFull`: board → past graph), in the board's
213
+ * numbering, and the names now of those renamed. */
214
+ export function boardMarks(p, n, toFull) {
215
+ const marks = new Uint8Array(n);
216
+ const now = new Map();
217
+ for (let j = 0; j < n; j++) {
218
+ const i = toFull(j);
219
+ marks[j] = p.marks[i];
220
+ if (p.marks[i] & PAST_RENAMED)
221
+ now.set(j, p.nowName.get(p.graph.nodes[i].name));
222
+ }
223
+ return { marks, now };
224
+ }