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,703 @@
1
+ // Agents and views (ViewSpec decision 10, AgentSpec decision 9): the endpoints of the agents' API
2
+ // that read, open, make and change views — boards everyone shares — within what `--agents` allows.
3
+ // Opening a view (a query nobody keeps) is reading; everything else a person does from the Views
4
+ // menu and ⚙ Board — making a view, duplicating, renaming, deleting it, changing its scope,
5
+ // arranging it (its clustering, folds and pins), laying it out afresh, linking a target to it — is
6
+ // level full, under the agent's name: in the timeline what is an
7
+ // event there (a view made, renamed, deleted, linked or laid out afresh), on the board's notes the
8
+ // rest.
9
+ //
10
+ // The endpoints are those of server/agent.ts, in its shape, so that its table takes them as they
11
+ // are (`...viewEndpoints(this.world, …)`, with what refuses an agent level full: the server's
12
+ // `--agents`, or its token's level); the views are the world's (server/views.ts). What an agent
13
+ // opens is where it is: people following it see that view, and a view nobody keeps lives while the
14
+ // agent is there (presence).
15
+ //
16
+ // A board an agent arranges is laid out here exactly as the pages lay it out (shared/board.ts), so
17
+ // that a place it gives is in the board's own coordinates; a pin beyond its parent's room is brought
18
+ // inside, as a drag would be. An agent hides levels of a board and shows them again as people do
19
+ // (ViewSpec decision 24), the pins of what moves to another parent kept where they are.
20
+ import { gunzipSync } from "node:zlib";
21
+ import { boardLayout, boardSet, layoutKey, pastSetKey, reframePins, snapshotOfKey, wholeLayout } from "../shared/board.js";
22
+ import { clusterKeys } from "../shared/layout.js";
23
+ import { CLUSTER_MODES } from "../shared/places.js";
24
+ import { printQuery } from "../shared/query.js";
25
+ import { briefWhen, namesOf } from "../shared/titles.js";
26
+ import { Graph } from "../shared/viewgraph.js";
27
+ import { fixedScope, MAX_HIDDEN, PROJECT, summary, validVersion, versionRefused, viewDescription, viewFocus, viewTitle } from "../shared/views.js";
28
+ import { applyItemOp, itemIn, itemOp } from "../shared/items.js";
29
+ import { derivesOn, existsIn } from "../shared/noteplace.js";
30
+ import { noteExcerpt } from "../shared/sticky.js";
31
+ import { fail, whoOf } from "./agent.js";
32
+ import { readQuery } from "./views.js";
33
+ const ok = (body) => ({ status: 200, body });
34
+ const MAX_PINS = 500;
35
+ const MAX_FOLDS = 2000;
36
+ /** a declaration's radius as the layout packs it (web/src/scene.ts, LEAF_R) */
37
+ const LEAF_R = 5;
38
+ /** The graph on show, read once per version. */
39
+ const graphs = new WeakMap();
40
+ export function graphOf(world) {
41
+ const w = world.graph;
42
+ if (!w)
43
+ return null;
44
+ let r = graphs.get(w.prep);
45
+ if (!r) {
46
+ const meta = JSON.parse(gunzipSync(w.prep.metaGz).toString("utf8"));
47
+ const g = new Graph(meta, new Int32Array(new Uint8Array(gunzipSync(w.prep.edgesGz)).buffer));
48
+ graphs.set(w.prep, (r = { meta, g }));
49
+ }
50
+ return r;
51
+ }
52
+ /** What the server's titles of views are told with (shared/views.ts, viewTitle): the names of the
53
+ * graph on show, and the builds the history kept, in the server's time. */
54
+ export function titleContext(world) {
55
+ const G = graphOf(world);
56
+ return {
57
+ names: G ? namesOf(G.g) : null,
58
+ build: (id) => {
59
+ const s = world.history?.get(id);
60
+ return s ? briefWhen(s.at) : null;
61
+ },
62
+ };
63
+ }
64
+ /** View `v`'s focus now (ViewSpec decision 28; shared/views.ts, viewFocus): as set, else the target
65
+ * it is linked to, else the one target its board holds on the graph on show, else null. */
66
+ export function focusOf(world, v) {
67
+ const targets = world.review.data.targets;
68
+ return viewFocus(v, (n) => Object.hasOwn(targets, n), () => {
69
+ const G = graphOf(world);
70
+ if (!G)
71
+ return [];
72
+ const bs = boardSet(G.g, world.review.data, v.scope);
73
+ return Object.keys(targets).filter((n) => {
74
+ const i = G.g.byName.get(n);
75
+ return i !== undefined && bs.set[i] === 1;
76
+ });
77
+ });
78
+ }
79
+ /** Why a layout's memory a page hands over (POST /api/views/memory) does not fit view `v`'s board
80
+ * by `mode`, with the status to answer; null when it fits. It must be for the set the board holds
81
+ * now, on the graph and review the server has, under the levels it hides (layoutKey), and speak of
82
+ * that set and of clusters a layout of it can have: a memory decides where everything is for
83
+ * everyone on the board. A view kept at a past build (`past`, its snapshot) is a board of that
84
+ * build's graph, which the pages make from the snapshot and the server does not: its memory must be
85
+ * one of that build's (shared/board.ts, pastSetKey), never the latest's, and the pages that recall it
86
+ * check it against the board they make. */
87
+ export function memoryProblem(world, v, mode, m, past = null) {
88
+ if (past !== null)
89
+ return snapshotOfKey(m.set) === past ? null : { status: 409, error: `This view shows the build of snapshot ${past}: its memory is one of that build's (its set starts with ${pastSetKey(past, "")}).` };
90
+ if (snapshotOfKey(m.set) !== null)
91
+ return { status: 409, error: "This view shows the latest build: a memory of a past build's board is not its." };
92
+ const G = graphOf(world);
93
+ if (!G)
94
+ return { status: 503, error: "The graph has not been read yet." };
95
+ const bs = boardSet(G.g, world.review.data, v.scope);
96
+ if (bs.whole)
97
+ return { status: 400, error: "This board is the project's own layout: it keeps no memory of its own." };
98
+ const key = layoutKey(bs.key, v.hidden[mode]);
99
+ if (m.set !== key)
100
+ return { status: 409, error: `This memory is for a set the board does not hold now, or other hidden levels (it is ${key}).` };
101
+ const g = bs.sub.graph;
102
+ const nodes = Object.keys(m.mem.nodes);
103
+ if (nodes.length !== g.n || nodes.some((k) => !g.byName.has(k)))
104
+ return { status: 400, error: "This memory does not place exactly what the board holds." };
105
+ const keys = clusterKeys(mode, g.nodes);
106
+ for (const k of Object.keys(m.mem.clusters))
107
+ if (!keys.has(k))
108
+ return { status: 400, error: `This board has no cluster ${k.slice(0, 200)} by ${mode}.` };
109
+ for (const k of nodes) {
110
+ const c = m.mem.nodes[k][2];
111
+ if (c !== "" && !keys.has(c))
112
+ return { status: 400, error: `This board has no cluster ${c.slice(0, 200)} by ${mode}.` };
113
+ }
114
+ return null;
115
+ }
116
+ /** How far from its parent's centre something of radius `r` may be placed (as server/agent.ts and
117
+ * the page keep a drop inside its parent's circle); Infinity at the top. */
118
+ function roomIn(L, parent, r) {
119
+ if (parent < 0)
120
+ return Infinity;
121
+ const P = L.clusters[parent];
122
+ return Math.max(0, P.r - (10 + Math.min(14, Math.sqrt(P.count))) - r);
123
+ }
124
+ /** A place brought within `room` of the centre, a hair inside so that rounding keeps it there. */
125
+ function inside(at, room) {
126
+ const d = Math.hypot(at[0], at[1]);
127
+ if (d <= room)
128
+ return at;
129
+ const k = Math.max(0, room - 0.1) / d;
130
+ return [Math.round(at[0] * k * 10) / 10, Math.round(at[1] * k * 10) / 10];
131
+ }
132
+ /** The agents' endpoints about views. `world` is the live server's, which carries its views. */
133
+ export function viewEndpoints(world, refuseFull) {
134
+ const body = (a) => (a.body && typeof a.body === "object" && !Array.isArray(a.body) ? a.body : {});
135
+ /** Writes say who makes them, and that it is an agent; changing a view needs level full. */
136
+ const who = (a, full = false) => {
137
+ if (a.caller.kind !== "agent")
138
+ return fail(400, "This API records what agents do: say you are one.", "Send X-VisualProof-Kind: agent with every request, beside X-VisualProof-Name.");
139
+ if (!a.caller.name)
140
+ return fail(401, "Say who you are first.", "Send X-VisualProof-Name: <your name>, e.g. Claude.", { who: true });
141
+ return full ? refuseFull(a.caller) : null;
142
+ };
143
+ const graph = () => graphOf(world) ?? fail(503, "The graph has not been read yet.", "The server is reading the project; ask again in a few seconds.", { retryAfter: 5 }, { "retry-after": "5" });
144
+ const find = (V, id) => (typeof id === "string" && V.book.get(id)) || fail(404, `There is no view ${String(id)}.`, "GET /api/agent/views lists them; POST /api/agent/views/open opens one on a query.");
145
+ /** What a view holds: declarations of the project, and of the outside world. */
146
+ const holds = (bs, g) => {
147
+ let own = 0;
148
+ for (let i = 0; i < g.projectCount; i++)
149
+ own += bs.set[i];
150
+ return { project: own, outside: bs.count - own };
151
+ };
152
+ const isTarget = (name) => Object.hasOwn(world.review.data.targets, name);
153
+ /** A view as agents read it: its short title, as the pages show it, what its scope holds in full,
154
+ * the targets linked to it (`targets`, ViewSpec decision 29) and its focus now (`focus`, decision
155
+ * 28). */
156
+ const shown = (v) => ({ ...summary(v), focus: focusOf(world, v), title: viewTitle(v, titleContext(world)), description: viewDescription(v) });
157
+ /** The agent is on view `v` now: people following it see it, and a view nobody keeps lives. */
158
+ const there = (name, v) => {
159
+ const was = world.presence.agentView(name) ?? { mode: "tcb", sel: null, source: false, clusterMode: "namespace", cam: null, view: null, scope: null, tabs: [] };
160
+ const tabs = [...new Set([...was.tabs, v.id])].slice(-20);
161
+ world.presence.agentBeat(name, { ...was, cam: null, clusterMode: v.clustering, view: v.id, scope: v.kept ? null : printQuery(v.scope), tabs });
162
+ };
163
+ /** A view's board by its clustering, laid out as the pages lay it out, under the levels it hides
164
+ * (or `hidden`, without handing anything over); its memory handed over when this was the first to
165
+ * lay it out for its set. */
166
+ const board = (V, v, mode, meta, g, hidden) => {
167
+ const bs = boardSet(g, world.review.data, v.scope);
168
+ const levels = hidden ?? v.hidden[mode];
169
+ if (bs.whole)
170
+ return { bs, L: wholeLayout(meta.layouts[mode], g.nodes, mode, levels), g };
171
+ const mem = V.book.memoryOf(v.id, mode);
172
+ const { layout, next } = boardLayout(bs.sub.graph, mode, bs.key, mem, levels);
173
+ // a view kept at a past build is a board of that build's graph, which only the pages lay out:
174
+ // a layout of the latest build's is never handed over as its memory
175
+ if (next && !hidden && V.pastOf(v) === null)
176
+ V.setMemory(v.id, mode, mem?.set ?? null, next);
177
+ return { bs, L: layout, g: bs.sub.graph };
178
+ };
179
+ const list = () => {
180
+ const V = world.views;
181
+ const on = new Map();
182
+ for (const p of V.peers())
183
+ if (p.view?.view && p.name)
184
+ on.set(p.view.view, [...new Set([...(on.get(p.view.view) ?? []), p.agent ? `${p.name} (agent)` : p.name])]);
185
+ const unreadable = V.unreadableList();
186
+ return ok({
187
+ views: V.book.list().map((v) => ({ ...shown(v), on: on.get(v.id) ?? [] })),
188
+ ...(unreadable.length ? { unreadable } : {}),
189
+ note: "Kept views are listed for everyone; one nobody kept lives while a page or an agent has it open. A target's view is the one whose targets name it (a view may be several targets'); a target none names has no view — marking a target makes none, and a view deleted leaves its targets with none: POST /api/agent/view {review: <target>} then opens a temporary view of its TCB, linked to it while it lasts. focus is where the review works on each." +
190
+ (unreadable.length ? " `unreadable`: kept views the server cannot read now (it keeps them as they are, untouched)." : ""),
191
+ });
192
+ };
193
+ const view = (a) => {
194
+ const V = world.views;
195
+ const G = graph();
196
+ if (!("g" in G))
197
+ return G;
198
+ const v = find(V, a.query.get("id"));
199
+ if (!("id" in v))
200
+ return v;
201
+ const mode = (a.query.get("mode") ?? v.clustering);
202
+ if (!CLUSTER_MODES.includes(mode))
203
+ return fail(400, "mode is namespace or module.", "Leave it out for the view's own clustering.");
204
+ const { bs, L, g } = board(V, v, mode, G.meta, G.g);
205
+ const members = a.query.get("members") === "1";
206
+ const pins = v.pins[mode];
207
+ return ok({
208
+ view: shown(v),
209
+ holds: holds(bs, G.g),
210
+ // the notes of the project added to it and taken out of it (the owner's decision of
211
+ // 2026-10-11): what it shows besides what its scope gives, and what it does not
212
+ items: itemsShown(v),
213
+ mode,
214
+ folds: v.folds[mode],
215
+ hidden: v.hidden[mode],
216
+ pins: { nodes: Object.keys(pins.nodes).length, clusters: Object.keys(pins.clusters).length },
217
+ // the clusters as this board has them, the outermost first: what can be folded or moved
218
+ clusters: L.clusters
219
+ .map((c) => ({ key: c.key, depth: c.depth, count: c.count, r: c.r, parent: c.parent >= 0 ? L.clusters[c.parent].key : null, room: c.parent >= 0 ? Math.floor(roomIn(L, c.parent, c.r) * 10) / 10 : null, at: [c.lx, c.ly], pinned: pins.clusters[c.key] ?? null }))
220
+ .filter((c) => c.depth <= 2)
221
+ .slice(0, 400),
222
+ ...(members ? { members: g.nodes.slice(0, 5000).map((n) => n.name), more: Math.max(0, g.n - 5000) } : {}),
223
+ });
224
+ };
225
+ const open = (a) => {
226
+ const no = who(a);
227
+ if (no)
228
+ return no;
229
+ const V = world.views;
230
+ const b = body(a);
231
+ const q = readQuery(b.scope);
232
+ if (typeof q === "string")
233
+ return fail(400, q, "e.g. {\"scope\": \"tcb(fermatLastTheoremFour) except trusted\"}; GET /api/agent/views lists the language's words in its note.");
234
+ // at a past build: the view on that query that shows it (the owner's decision of 2026-10-09)
235
+ const version = b.version === 0 ? null : b.version;
236
+ if (version !== undefined && version !== null && (!validVersion(version) || V.snapshotAt(version) === null))
237
+ return fail(404, `There is no snapshot ${String(version)}.`, "GET /api/agent/history lists the snapshots; null (or leaving it out) is the latest build.");
238
+ if (b.clustering !== undefined && b.clustering !== "namespace" && b.clustering !== "module")
239
+ return fail(400, "The clustering is namespace or module.", "It is a new view's; an open one keeps its own.");
240
+ // a new view's focus (ViewSpec decision 28): the target it is made from
241
+ if (b.focus !== undefined && b.focus !== null && (typeof b.focus !== "string" || !isTarget(b.focus)))
242
+ return fail(400, "focus is a target's full name.", "GET /api/agent/targets lists them; it is a new view's, an open one keeps its own.");
243
+ const { view: v, change } = V.book.open(q, a.caller.name, Date.now(), { agent: true, ...(typeof version === "number" ? { version } : {}), ...(b.clustering ? { clustering: b.clustering } : {}), ...(typeof b.focus === "string" ? { focus: b.focus } : {}) });
244
+ if (change)
245
+ V.changed([change], true);
246
+ there(whoOf(a.caller), v);
247
+ const G = graphOf(world);
248
+ return ok({
249
+ view: shown(v),
250
+ ...(G ? { holds: holds(boardSet(G.g, world.review.data, v.scope), G.g) } : {}),
251
+ note: (v.kept ? "A kept view." : "Nobody keeps this view: it lives while you or a page has it open. Arrange or name it to keep it for everyone.") +
252
+ (v.version !== undefined ? ` It shows the build of snapshot ${v.version} on the pages, arranged there as any view, its review the latest build's, and its exported document is that build's (GET /api/agent/export); what it holds here is the latest build's.` : ""),
253
+ });
254
+ };
255
+ const create = (a) => {
256
+ const no = who(a, true);
257
+ if (no)
258
+ return no;
259
+ const V = world.views;
260
+ const b = body(a);
261
+ const q = b.scope === undefined ? undefined : readQuery(b.scope);
262
+ if (typeof q === "string")
263
+ return fail(400, q, "Send the scope as its text, e.g. ns(Fermat42) except trusted.");
264
+ if (b.clustering !== undefined && !CLUSTER_MODES.includes(b.clustering))
265
+ return fail(400, "clustering is namespace or module.", "Leave it out for namespace.");
266
+ // the build it shows (HistorySpec decision 19): a snapshot the history has, or null (0, from a
267
+ // tool whose arguments cannot say null) for the latest
268
+ const version = b.version === 0 ? null : b.version;
269
+ if (version !== undefined && version !== null && (!validVersion(version) || V.snapshotAt(version) === null))
270
+ return fail(404, `There is no snapshot ${String(version)}.`, "GET /api/agent/history lists the snapshots; null (or leaving it out) is the latest build.");
271
+ const v = V.book.create(typeof b.name === "string" ? b.name.trim() : "", a.caller.name, { scope: q, clustering: b.clustering, from: typeof b.from === "string" ? b.from : undefined, version: version, agent: true });
272
+ if (typeof v === "string")
273
+ return fail(400, v, "Send {name, scope} (a name of 1 to 80 characters), or {name, from: <view id>} for a copy.");
274
+ V.changed([{ id: v.id, what: "kept", by: a.caller.name }], true);
275
+ there(whoOf(a.caller), v);
276
+ return ok({ view: shown(v) });
277
+ };
278
+ const scope = (a) => {
279
+ const no = who(a, true);
280
+ if (no)
281
+ return no;
282
+ const V = world.views;
283
+ const b = body(a);
284
+ const v = find(V, b.id);
285
+ if (!("id" in v))
286
+ return v;
287
+ const q = readQuery(b.scope);
288
+ if (typeof q === "string")
289
+ return fail(400, q, "Send the scope as its text or its JSON form.");
290
+ const c = V.book.setScope(v.id, q, a.caller.name);
291
+ if (typeof c === "string")
292
+ return fail(400, c, "");
293
+ if (c)
294
+ V.changed(c, true);
295
+ there(whoOf(a.caller), v);
296
+ const G = graphOf(world);
297
+ return ok({ ok: true, view: shown(v), ...(G ? { holds: holds(boardSet(G.g, world.review.data, v.scope), G.g) } : {}) });
298
+ };
299
+ const arrange = (a) => {
300
+ const no = who(a, true);
301
+ if (no)
302
+ return no;
303
+ const V = world.views;
304
+ const G = graph();
305
+ if (!("g" in G))
306
+ return G;
307
+ const b = body(a);
308
+ const v = find(V, b.id);
309
+ if (!("id" in v))
310
+ return v;
311
+ const me = a.caller.name;
312
+ // folds and pins are in the clustering the view has after; everything is checked before
313
+ // anything changes, so that a refused request leaves the view as it was
314
+ if (b.clustering !== undefined && !CLUSTER_MODES.includes(b.clustering))
315
+ return fail(400, "clustering is namespace or module.", "Leave it out to keep the view's own.");
316
+ const mode = b.clustering ?? v.clustering;
317
+ const before = board(V, v, mode, G.meta, G.g);
318
+ const ops = [];
319
+ // the build it is kept at (HistorySpec decision 19), for everyone on it: a snapshot's id, or
320
+ // null (0, from a tool whose arguments cannot say null) for the latest again
321
+ const version = b.version === 0 ? null : b.version;
322
+ if (version !== undefined) {
323
+ if (version !== null && !validVersion(version))
324
+ return fail(400, "version is a snapshot's id, or null for the latest build.", "GET /api/agent/history lists the snapshots.");
325
+ const refused = version !== null ? versionRefused(v) : null;
326
+ if (refused)
327
+ return fail(400, refused, "POST /api/agent/views/create {name, from: <this view>, version} keeps it in a view of its own.");
328
+ if ((v.version ?? null) !== version)
329
+ ops.push({ op: "version", snapshot: version });
330
+ }
331
+ // levels hidden and shown first: the folds and pins asked for are on the board as it is after,
332
+ // and the pins already there of what moves to another parent stay where they are
333
+ const now = new Set(v.hidden[mode]);
334
+ const was = new Map(before.L.clusters.map((c) => [c.key, c]));
335
+ for (const [field, on] of [["hide", true], ["show", false]]) {
336
+ const xs = b[field];
337
+ if (xs === undefined)
338
+ continue;
339
+ if (!Array.isArray(xs) || xs.length > MAX_HIDDEN || !xs.every((x) => typeof x === "string"))
340
+ return fail(400, `${field} is a list of cluster keys.`, "GET /api/agent/views/view?id=… lists this board's clusters and the levels it hides.");
341
+ for (const key of xs) {
342
+ if (on && !was.has(key) && !now.has(key))
343
+ return fail(404, `This board has no cluster ${key} by ${mode}.`, "GET /api/agent/views/view?id=… lists its clusters; a merged chain is hidden by its key, the deepest level's.");
344
+ if (!on && !now.has(key))
345
+ return fail(404, `This board does not hide ${key} by ${mode}.`, "GET /api/agent/views/view?id=… lists the levels it hides (hidden).");
346
+ if (on === now.has(key))
347
+ continue;
348
+ if (on)
349
+ now.add(key);
350
+ else
351
+ now.delete(key);
352
+ ops.push({ op: "hide", mode, key, on });
353
+ }
354
+ }
355
+ const { L, g } = ops.length ? board(V, v, mode, G.meta, G.g, [...now]) : before;
356
+ if (ops.length)
357
+ ops.push(...reframePins(g, before.L, L, v.pins[mode], mode));
358
+ const keys = new Map(L.clusters.map((c, k) => [c.key, k]));
359
+ for (const [field, on] of [["fold", true], ["unfold", false]]) {
360
+ const xs = b[field];
361
+ if (xs === undefined)
362
+ continue;
363
+ if (!Array.isArray(xs) || xs.length > MAX_FOLDS || !xs.every((x) => typeof x === "string"))
364
+ return fail(400, `${field} is a list of cluster keys.`, "GET /api/agent/views/view?id=… lists this board's clusters.");
365
+ for (const key of xs) {
366
+ if (!keys.has(key))
367
+ return fail(404, `This board has no cluster ${key} by ${mode}.`, "GET /api/agent/views/view?id=… lists its clusters; a board laid out on its own has clusters of its own.");
368
+ ops.push({ op: "fold", mode, key, on });
369
+ }
370
+ }
371
+ if (b.pins !== undefined) {
372
+ if (!Array.isArray(b.pins) || b.pins.length > MAX_PINS)
373
+ return fail(400, `pins is a list of at most ${MAX_PINS} {name or cluster, at: [x, y] or null}.`, "at is in the parent cluster's coordinates on this board.");
374
+ for (const p of b.pins) {
375
+ if (!p || typeof p !== "object")
376
+ return fail(400, "Each pin is {name or cluster, at}.", "e.g. {\"name\": \"A.b\", \"at\": [12, -40]}.");
377
+ let kind, key, room;
378
+ if (typeof p.cluster === "string") {
379
+ const c = keys.get(p.cluster);
380
+ if (c === undefined)
381
+ return fail(404, `This board has no cluster ${p.cluster} by ${mode}.`, "GET /api/agent/views/view?id=… lists its clusters.");
382
+ kind = "cluster";
383
+ key = p.cluster;
384
+ room = roomIn(L, L.clusters[c].parent, L.clusters[c].r);
385
+ }
386
+ else {
387
+ const i = typeof p.name === "string" ? g.byName.get(p.name) : undefined;
388
+ if (i === undefined)
389
+ return fail(404, `${String(p.name)} is not on this board.`, "A pin is for a declaration the view's scope holds (GET /api/agent/views/view?id=…&members=1).");
390
+ kind = "node";
391
+ key = p.name;
392
+ room = roomIn(L, L.cluster[i], LEAF_R);
393
+ }
394
+ if (p.at === null)
395
+ ops.push({ op: "unpin", mode, kind, key });
396
+ else if (Array.isArray(p.at) && p.at.length === 2 && p.at.every((x) => typeof x === "number" && Number.isFinite(x) && Math.abs(x) < 1e7))
397
+ ops.push({ op: "pin", mode, kind, key, at: inside(p.at, room) });
398
+ else
399
+ return fail(400, "at is [x, y], two finite numbers in the parent cluster's coordinates, or null to let go.", "");
400
+ }
401
+ }
402
+ if (mode !== v.clustering) {
403
+ const c = V.book.setClustering(v.id, mode, me);
404
+ if (typeof c === "string")
405
+ return fail(400, c, "clustering is namespace or module.");
406
+ if (c)
407
+ V.changed([c], true);
408
+ }
409
+ there(whoOf(a.caller), v);
410
+ if (!ops.length)
411
+ return ok({ ok: true, view: shown(v), changed: 0, hidden: v.hidden[mode] });
412
+ const err = V.arrange(v.id, ops, null, me, true);
413
+ if (err)
414
+ return fail(400, err, "");
415
+ return ok({
416
+ ok: true,
417
+ view: shown(v),
418
+ changed: ops.length,
419
+ hidden: v.hidden[mode],
420
+ placed: ops.flatMap((o) => (o.op === "pin" ? [{ key: o.key, at: o.at }] : o.op === "unpin" ? [{ key: o.key, at: null }] : [])),
421
+ ...(v.id === PROJECT ? { note: "On the Project, POST /api/agent/pins makes a batch that can be undone; this does not." } : {}),
422
+ });
423
+ };
424
+ /** A view's own focus set, for everyone on it (ViewSpec decision 28): a target, or none (null);
425
+ * each person's own focus there stays as it is. */
426
+ const focus = (a) => {
427
+ const no = who(a, true);
428
+ if (no)
429
+ return no;
430
+ const V = world.views;
431
+ const b = body(a);
432
+ const v = find(V, b.id);
433
+ if (!("id" in v))
434
+ return v;
435
+ // left out (a tool's arguments cannot say null) is none
436
+ const f = b.focus ?? null;
437
+ if (f !== null && (typeof f !== "string" || !isTarget(f)))
438
+ return fail(400, "focus is a target's full name, or null for none.", "GET /api/agent/targets lists the targets.");
439
+ const c = V.book.setFocus(v.id, f, a.caller.name);
440
+ if (typeof c === "string")
441
+ return fail(400, c, "");
442
+ if (c)
443
+ V.changed(c, true);
444
+ there(whoOf(a.caller), v);
445
+ return ok({ ok: true, changed: !!c, view: shown(v) });
446
+ };
447
+ /** A target linked to a view, for everyone (ViewSpec decision 29): clicking the target opens it; a
448
+ * view linked to other targets takes it too, and keeps its focus (the owner's decision of
449
+ * 2026-10-11), a view's first target being its focus to begin with; the view it was linked to keeps
450
+ * its scope and arrangement. */
451
+ const link = (a) => {
452
+ const no = who(a, true);
453
+ if (no)
454
+ return no;
455
+ const V = world.views;
456
+ const b = body(a);
457
+ const v = find(V, b.id);
458
+ if (!("id" in v))
459
+ return v;
460
+ if (typeof b.target !== "string" || !isTarget(b.target))
461
+ return fail(400, "target is a target's full name.", "GET /api/agent/targets lists the targets.");
462
+ const was = V.book.viewOf(b.target)?.id ?? null;
463
+ const c = V.book.link(v.id, b.target, a.caller.name);
464
+ if (typeof c === "string")
465
+ return fail(409, c, "The Project and a view kept at a past build are no target's; any other view may be linked to several.");
466
+ if (c)
467
+ V.changed(c, true);
468
+ there(whoOf(a.caller), v);
469
+ return ok({ ok: true, changed: !!c, view: shown(v), ...(c && was !== v.id && was ? { left: was } : {}) });
470
+ };
471
+ /** A view's lists as agents read them: each note added to it and taken out of it, by id with its
472
+ * first words; on the Project, which holds every note, none. */
473
+ const itemsShown = (v) => {
474
+ if (fixedScope(v))
475
+ return { all: true, add: [], remove: [] };
476
+ const words = (id) => ({ id, text: noteExcerpt(world.views.book.notes.find((n) => n.id === id)?.text ?? "", 80) });
477
+ return { add: (v.items?.add ?? []).map(words), remove: (v.items?.remove ?? []).map(words) };
478
+ };
479
+ /** Notes of the project added to a view, or taken out of it, for everyone on it (the owner's
480
+ * decision of 2026-10-11, shared/items.ts), at the level a scope change needs: each on the view's
481
+ * add list only where its scope does not give it, on its remove list only where it does, as the
482
+ * page's Add to view ▸ and Remove from this view do. The Project holds every note: there a note is
483
+ * deleted, not taken out. */
484
+ const items = (a) => {
485
+ const no = who(a, true);
486
+ if (no)
487
+ return no;
488
+ const V = world.views;
489
+ const G = graph();
490
+ if (!("g" in G))
491
+ return G;
492
+ const b = body(a);
493
+ const v = find(V, b.id);
494
+ if (!("id" in v))
495
+ return v;
496
+ if (fixedScope(v))
497
+ return fail(400, "The Project holds every note of the project: there a note is deleted, not added or taken out.", "POST /api/agent/note/delete deletes one, for everyone; add it to or take it out of another view.");
498
+ const ids = (x) => (x === undefined ? [] : Array.isArray(x) && x.length <= 200 && x.every((id) => typeof id === "string") ? x : null);
499
+ const add = ids(b.add), remove = ids(b.remove);
500
+ if (!add || !remove || (!add.length && !remove.length))
501
+ return fail(400, "Send add, remove or both: lists of notes of the project, by id (at most 200 each).", 'e.g. {"id": "v1a2b3c", "add": ["n0123456789ab"]}; GET /api/agent/notes?all=1 lists the notes.');
502
+ const note = (id) => V.book.notes.find((n) => n.id === id);
503
+ const stray = [...add, ...remove].find((id) => !note(id));
504
+ if (stray)
505
+ return fail(404, `There is no note ${stray.slice(0, 40)} of the project.`, "GET /api/agent/notes?all=1 lists them; a view's own note is not an item.");
506
+ // what the view's scope gives, on the graph on show (a view kept at a past build too, as what it
507
+ // holds is read here)
508
+ const set = boardSet(G.g, world.review.data, v.scope).set;
509
+ const gives = derivesOn(v.clustering, G.g.nodes.filter((_, i) => set[i]), existsIn(G.g.nodes), world.review.data.links);
510
+ const lists = { ...(v.items ? { items: structuredClone(v.items) } : {}) };
511
+ const ops = [];
512
+ for (const [to, list] of [["add", add], ["remove", remove]])
513
+ for (const id of list) {
514
+ const op = itemOp(lists.items, id, to, gives(note(id)));
515
+ if (op && applyItemOp(lists, op))
516
+ ops.push(op);
517
+ }
518
+ if (ops.length) {
519
+ const err = V.arrange(v.id, ops, null, a.caller.name, true);
520
+ if (err)
521
+ return fail(400, err, "");
522
+ }
523
+ there(whoOf(a.caller), v);
524
+ const now = V.book.get(v.id) ?? v;
525
+ const shows = (id) => {
526
+ const listed = itemIn(now.items, id);
527
+ return listed !== "remove" && (listed === "add" || gives(note(id)));
528
+ };
529
+ return ok({ ok: true, changed: ops.length > 0, view: shown(now), items: itemsShown(now), notes: [...new Set([...add, ...remove])].map((id) => ({ id, shown: shows(id) })) });
530
+ };
531
+ /** A copy of a view, a board of the agent's own to arrange, named as the page's Duplicate names
532
+ * one ("<title> (copy)", numbered while that is taken) unless a name is given. */
533
+ const duplicate = (a) => {
534
+ const no = who(a, true);
535
+ if (no)
536
+ return no;
537
+ const V = world.views;
538
+ const b = body(a);
539
+ const v = find(V, b.id);
540
+ if (!("id" in v))
541
+ return v;
542
+ let name = typeof b.name === "string" && b.name.trim() ? b.name.trim() : "";
543
+ if (!name) {
544
+ const ctx = titleContext(world);
545
+ const taken = new Set(V.book.list().map((x) => viewTitle(x, ctx)));
546
+ const base = `${viewTitle(v, ctx).slice(0, 60)} (copy)`;
547
+ name = base;
548
+ for (let k = 2; taken.has(name); k++)
549
+ name = `${base} ${k}`;
550
+ }
551
+ const c = V.book.create(name, a.caller.name, { from: v.id, agent: true });
552
+ if (typeof c === "string")
553
+ return fail(400, c, "A name is 1 to 80 characters, without line breaks.");
554
+ V.changed([{ id: c.id, what: "kept", by: a.caller.name }], true);
555
+ there(whoOf(a.caller), c);
556
+ return ok({ view: shown(c), from: v.id });
557
+ };
558
+ /** A view named, renamed, or (null) its name taken away; naming a view nobody kept keeps it. */
559
+ const rename = (a) => {
560
+ const no = who(a, true);
561
+ if (no)
562
+ return no;
563
+ const V = world.views;
564
+ const b = body(a);
565
+ const v = find(V, b.id);
566
+ if (!("id" in v))
567
+ return v;
568
+ if (b.name !== null && typeof b.name !== "string")
569
+ return fail(400, "name is the view's new name, or null to take its name away.", "A name is 1 to 80 characters, without line breaks.");
570
+ const name = typeof b.name === "string" && b.name.trim() ? b.name.trim() : null;
571
+ const r = V.book.rename(v.id, name, a.caller.name);
572
+ if (typeof r === "string")
573
+ return fail(400, r, "The Project keeps its name; a name is 1 to 80 characters.");
574
+ V.changed(r, true);
575
+ return ok({ ok: true, changed: r.length > 0, view: shown(v) });
576
+ };
577
+ /** A kept view deleted, for everyone (never the Project): its pins, folds and layout go with it;
578
+ * its scope can be opened again. A view targets are linked to is deleted as any is, and those
579
+ * targets have no view after (`unlinked`; the owner's decisions of 2026-10-11): a click on one
580
+ * opens a temporary view of its TCB, until someone keeps that view or links it to another. */
581
+ const remove = (a) => {
582
+ const no = who(a, true);
583
+ if (no)
584
+ return no;
585
+ const V = world.views;
586
+ const v = find(V, body(a).id);
587
+ if (!("id" in v))
588
+ return v;
589
+ const title = viewTitle(v, titleContext(world));
590
+ const scope = printQuery(v.scope);
591
+ const targets = v.targets ?? [];
592
+ const c = V.book.remove(v.id, a.caller.name);
593
+ if (typeof c === "string")
594
+ return fail(400, c, "The Project is always there; a view nobody keeps goes by itself when the last page closes it.");
595
+ V.changed([c], true);
596
+ return ok({
597
+ ok: true,
598
+ deleted: v.id,
599
+ title,
600
+ scope,
601
+ ...(targets.length ? { unlinked: targets } : {}),
602
+ note: `Pages on it went back to the Project. POST /api/agent/views/create {"name": …, "scope": ${JSON.stringify(scope)}} makes it again, without its arrangement.` +
603
+ (targets.length ? ` ${targets.join(", ")} ${targets.length === 1 ? "has" : "have"} no view now: a click on one (or POST /api/agent/view {"review": …}) opens a temporary view of its TCB, linked to it while it lasts; views/link links it to another view.` : ""),
604
+ });
605
+ };
606
+ /** A view laid out afresh, for everyone on it: its pins and its layout's memory go (the
607
+ * Project's graph is laid out anew), its folds stay. */
608
+ const relayout = (a) => {
609
+ const no = who(a, true);
610
+ if (no)
611
+ return no;
612
+ const V = world.views;
613
+ const v = find(V, body(a).id);
614
+ if (!("id" in v))
615
+ return v;
616
+ const pins = CLUSTER_MODES.reduce((n, m) => n + Object.keys(v.pins[m].nodes).length + Object.keys(v.pins[m].clusters).length, 0);
617
+ const err = V.relayout(v.id, a.caller.name, true);
618
+ if (err)
619
+ return fail(404, err, "GET /api/agent/views lists them.");
620
+ there(whoOf(a.caller), v);
621
+ return ok({ ok: true, view: shown(v), dropped: pins, note: "Every pin of this view was dropped, for everyone; that is not undone. The timeline says you did it." });
622
+ };
623
+ return [
624
+ {
625
+ method: "GET", path: "/api/agent/views", brief: "the views (POST /api/agent/views/open {scope}: one on a query)", about: "The views — boards everyone shares: the Project (everything), each target's view (the one it is linked to, which may be several targets'; a target none is linked to has none: a click on it opens a temporary view of its TCB), the views people made or arranged, and the open ones nobody keeps — with who is on each.",
626
+ params: {},
627
+ returns: "{views: [{id, title (short, as the page shows it), description (its scope in full), name, scope, clustering, kept, targets? (the targets linked to it, the first linked first), focus (its focus now, or null), by, at, version?, on: [names]}], note}",
628
+ run: () => list(),
629
+ },
630
+ {
631
+ method: "GET", path: "/api/agent/views/view", about: "One view: its scope, what it holds, its folds, the levels it hides and its pins, and its clusters as this board has them (in its own coordinates, for folding and pinning). A view kept at a past build says which (view.version, a snapshot's id: GET /api/agent/history); the pages show that build's graph on it, arranged as any view but not reviewed, and its exported document is that build's (GET /api/agent/export), while what it holds and its clusters here are the latest build's.",
632
+ params: { id: "the view's id", mode: "namespace or module (default: the view's clustering)", members: "1: the names of the declarations it holds (at most 5,000)" },
633
+ returns: "{view, holds: {project, outside}, items: {add: [{id, text}], remove: [{id, text}], all?: true on the Project, which holds every note}, mode, folds, hidden: [keys of the levels it hides], pins: {nodes, clusters}, clusters: [{key, depth, count, r, parent, room, at, pinned}], members?}",
634
+ run: (a) => view(a),
635
+ },
636
+ {
637
+ method: "POST", path: "/api/agent/views/items", needs: "full", about: "Add notes of the project to a view, or take them out of it, for everyone on it, as the note's menu does (Add to view ▸, Remove from this view): items belong to views as declarations do. The Project shows every note; another view shows what follows from its scope — a note in a cluster on its board, on the ring about a cluster under the other clustering on it, or carrying a file linked to a declaration or a cluster it holds (a note that names a declaration in backticks is not connected to it) — and what was added to it, less what was taken out. A note is put on the view's add list only where its scope does not give it, on its remove list only where it does. Not on the Project (deleting a note is note/delete). Activity records each, as a scope change.",
638
+ params: { id: "the view's id (not the Project)", add: "[note ids]: notes of the project the view is to show", remove: "[note ids]: notes it is not to show" },
639
+ returns: "{ok, changed, view, items: {add: [{id, text}], remove: [{id, text}]}, notes: [{id, shown}]}",
640
+ run: (a) => items(a),
641
+ },
642
+ {
643
+ method: "POST", path: "/api/agent/views/open", about: "Open the view on a query — the open one with the same scope and build, or a new one nobody keeps — and be on it: people following you see it. With version, it shows that build, as what is made from a board of a past build does (Zoom in on a view kept at a build). The query language: all, tcb(A), uses(A), usedBy(A), decl(A, B), ns(P), file(P), trusted, external, kind(theorem), status(unreviewed|approved|commented|stale), advice(approve|comment), sorry, nonstandard, key(T) (key(targets): every target's), joined by and, or, except.",
644
+ params: { scope: "the query, as text (e.g. \"tcb(A.b) except trusted except external\") or its JSON form", version: "a snapshot's id (GET /api/agent/history): the build it shows; null or left out for the latest", clustering: "namespace (default) or module: a new view's", focus: "a target's full name: a new view's focus, the target it is made from (as a target's Key view is)" },
645
+ returns: "{view, holds: {project, outside}, note}",
646
+ run: (a) => open(a),
647
+ },
648
+ {
649
+ method: "POST", path: "/api/agent/views/create", needs: "full", about: "Make a view for everyone, named: on a scope, or a copy of another view (its scope, clustering, pins, folds, notes added and taken out — of the Project, the notes its scope would not give, as added — and the build it is kept at). With version, it shows that build: the Time Machine's Save as new view.",
650
+ params: { name: "1 to 80 characters", scope: "the query (default all; with from, the copy holds it instead)", clustering: "namespace (default) or module", from: "a view's id, to copy it", version: "a snapshot's id (GET /api/agent/history): the build it shows; null for the latest (default: the copied view's, else the latest)" },
651
+ returns: "{view}",
652
+ run: (a) => create(a),
653
+ },
654
+ {
655
+ method: "POST", path: "/api/agent/views/scope", needs: "full", about: "Change what a view holds, for everyone on it (they are told who did).",
656
+ params: { id: "the view's id", scope: "the query" },
657
+ returns: "{ok, view, holds}",
658
+ run: (a) => scope(a),
659
+ },
660
+ {
661
+ method: "POST", path: "/api/agent/views/arrange", needs: "full", about: "Arrange a view, for everyone on it: its clustering, levels of its cluster tree hidden or shown again (a hidden cluster's sub-clusters and declarations sit in its parent; a fold on it goes; the exported document's sections follow), clusters folded or opened, pins dropped or let go (in its own coordinates, on the board as it is after the levels hidden or shown; a place beyond the parent's room is brought inside), and the build it is kept at (version: the pages show that snapshot's graph on it, arranged as any view, not reviewed, its exported document that build's; the timeline records it). Arranging a view nobody keeps keeps it. The Project and the views linked to targets always show the latest build.",
662
+ params: { id: "the view's id", clustering: "namespace or module", hide: "[cluster keys]: levels to hide, by this clustering", show: "[cluster keys]: hidden levels to show again", fold: "[cluster keys]", unfold: "[cluster keys]", pins: `[{name or cluster, at: [x, y] or null}], at most ${MAX_PINS}`, version: "a snapshot's id (GET /api/agent/history) to keep the view at that build, or null for the latest again" },
663
+ returns: "{ok, view, changed, hidden: [keys of the levels hidden now], placed: [{key, at}], note?}",
664
+ run: (a) => arrange(a),
665
+ },
666
+ {
667
+ method: "POST", path: "/api/agent/views/focus", needs: "full", about: "Set a view's own focus, for everyone on it (ViewSpec decision 28): the target the review is on there — its strip, key marks and why a declaration is in its TCB, the whole review set counted whatever the board holds. Each person's own focus there stays theirs.",
668
+ params: { id: "the view's id", focus: "a target's full name, or null (or left out) for none" },
669
+ returns: "{ok, changed, view}",
670
+ run: (a) => focus(a),
671
+ },
672
+ {
673
+ method: "POST", path: "/api/agent/views/link", needs: "full", about: "Link a target to a view, for everyone (ViewSpec decision 29): clicking the target anywhere opens that view, which shows a small star by its name. A view may be linked to several targets: its focus is the first one's to begin with, and a target linked after leaves it as it is. A target is linked to one view: the view it was linked to keeps its scope and arrangement and only stops being the target's. Linking keeps a view nobody kept. Not the Project, nor a view kept at a past build.",
674
+ params: { id: "the view's id", target: "a target's full name" },
675
+ returns: "{ok, changed, view, left?: the view it was linked to before}",
676
+ run: (a) => link(a),
677
+ },
678
+ {
679
+ method: "POST", path: "/api/agent/views/duplicate", needs: "full", about: "Duplicate a view, as the Views menu does: a kept copy with its scope, clustering, pins, folds and what it shows of the project's notes (its added and taken-out notes; of the Project, every note its scope would not give, as added) — a board of your own to arrange, or to scope otherwise. You are on it after.",
680
+ params: { id: "the view's id", name: "optional: 1 to 80 characters (default \"<its title> (copy)\")" },
681
+ returns: "{view, from}",
682
+ run: (a) => duplicate(a),
683
+ },
684
+ {
685
+ method: "POST", path: "/api/agent/views/rename", needs: "full", about: "Name or rename a view, for everyone (naming one nobody keeps keeps it); null takes a kept view's name away. The Project keeps its name.",
686
+ params: { id: "the view's id", name: "1 to 80 characters, or null" },
687
+ returns: "{ok, changed, view}",
688
+ run: (a) => rename(a),
689
+ },
690
+ {
691
+ method: "POST", path: "/api/agent/views/delete", needs: "full", about: "Delete a kept view for everyone, with its pins, folds and layout; its scope can be opened again. A view targets are linked to is deleted too, and those targets then have no view (unlinked): view {review} on one opens a temporary view of its TCB, linked to it while it lasts, and views/link links it to another; the timeline says the deletion and the unlinks in one event. The Project cannot be deleted; a view nobody keeps goes by itself.",
692
+ params: { id: "the view's id" },
693
+ returns: "{ok, deleted, title, scope, unlinked?: the targets it was linked to, which have no view now, note}",
694
+ run: (a) => remove(a),
695
+ },
696
+ {
697
+ method: "POST", path: "/api/agent/views/relayout", needs: "full", about: "Lay a view out afresh, as ⚙ Board's Re-layout does: every pin of it is dropped for everyone (not undone) and it is laid out compactly again; its folds stay. On the Project, the server lays the whole graph out anew.",
698
+ params: { id: "the view's id (project for the Project)" },
699
+ returns: "{ok, view, dropped: how many pins went, note}",
700
+ run: (a) => relayout(a),
701
+ },
702
+ ];
703
+ }