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,818 @@
1
+ // One query language for what is on a board and for what lights up (ViewSpec decision 4): a view's
2
+ // scope is a query with fixed roots (`tcb(fermatLastTheoremFour) except trusted`), and so is what the
3
+ // reader selected, which is what is lit: one part per click, joined by `or`
4
+ // (`decl(A.b) or tcb(ns(A.C) except external)`, web/src/selection.ts). The same query has a structured form (a
5
+ // JSON tree, what the server stores and agents may send) and a short text form (what the address,
6
+ // agents and the scope chips carry); the two are parsed and printed both ways, and the text printed
7
+ // is canonical, so two scopes are the same when their texts are.
8
+ //
9
+ // The text form, left to right:
10
+ //
11
+ // all | selection | decl(A, B) | tcb(A) | uses(A, B) | usedBy(selection) | refs(A)
12
+ // tcb(ns(Fermat42) except external) a closure of the set a query holds
13
+ // ns(Fermat42) declarations whose name is under the prefix
14
+ // file(Mathlib.NumberTheory) declarations of the modules under the prefix
15
+ // trusted | external | sorry | nonstandard
16
+ // kind(theorem, def) | status(unreviewed, stale) | advice(comment)
17
+ // key(fermatLastTheoremFour) the target and the declarations marked key for it
18
+ // key(targets) the same for every target
19
+ // X and Y | X or Y | X except Y left to right, one level; parentheses group
20
+ //
21
+ // A name that holds a space, a parenthesis, a comma or a quote — or is the word `selection`, or
22
+ // `targets` inside key(…) — is written as a JSON string. Inside a closure's parentheses, what starts with a parenthesis, with a
23
+ // word and a parenthesis (`ns(`), or joins with and, or, except is a query; a query that is one
24
+ // plain word is written in parentheses there (`tcb((external))`). Words are read without regard to case (`usedby`); `namespace` and
25
+ // `module` may stand for `ns` and `file`.
26
+ import { adviceOf, keyMarks, STANDARD_AXIOMS, statusOf, trusted, under } from "./review.js";
27
+ export const KINDS = ["theorem", "def", "abbrev", "instance", "structure", "class", "inductive", "opaque", "axiom", "quot", "constructor", "recursor", "notation", "unknown"];
28
+ export const STATUSES = ["unreviewed", "approved", "commented", "stale"];
29
+ const VERDICTS = ["approve", "comment"];
30
+ const CLOSURES = ["tcb", "uses", "usedBy", "refs"];
31
+ const PLAIN = ["all", "selection", "trusted", "external", "sorry", "nonstandard"];
32
+ const COMBINATORS = ["and", "or", "except"];
33
+ /** words for the same thing, read as the first */
34
+ const ALIASES = { usedby: "usedBy", namespace: "ns", module: "file", decls: "decl" };
35
+ /** Lighting up (ViewSpec decision 4): the tools that are closures of the graph, as queries relative
36
+ * to the selection (the Keys step of Rests on is keysQuery). */
37
+ export const LIGHT = {
38
+ plain: { op: "selection" },
39
+ tcb: { op: "tcb", of: "selection" },
40
+ deps: { op: "uses", of: "selection" },
41
+ refs: { op: "refs", of: "selection" },
42
+ apps: { op: "usedBy", of: "selection" },
43
+ };
44
+ /** Which walk of the graph each closure word is (shared/viewgraph.ts); `refs` is one step. */
45
+ const CLOSURE_MODE = { tcb: "tcb", uses: "deps", usedBy: "apps", refs: "refs" };
46
+ // ---------------------------------------------------------------- how large a query may be
47
+ /** How large a query may be: one set of bounds for its text and its JSON form, checked when either
48
+ * is read (parseQuery, cleanQuery), so that what one form takes the other takes too — a scope
49
+ * saved from its text is read back from its JSON at the next start. The bounds hold what a scope
50
+ * or a selection needs with room to spare (900 picks of a selection fit), and keep reading,
51
+ * printing and evaluating any query a matter of milliseconds: a closure walks the whole graph, and
52
+ * every function on a query is recursive. */
53
+ export const QUERY_LIMITS = {
54
+ /** characters of its canonical text (printQuery), which is what the JSON form stands for */
55
+ text: 20_000,
56
+ /** nesting: a closure of a query, and a combination on the right of and, or, except — what the
57
+ * text writes in parentheses — each one level deeper (`tcb(ns(A) except (B or C))`: 2) */
58
+ depth: 32,
59
+ /** its parts: all, decl(…), tcb(…), ns(…), kind(…), … wherever they stand */
60
+ terms: 1_000,
61
+ /** names and prefixes, all together */
62
+ names: 2_000,
63
+ /** characters of one name or prefix */
64
+ name: 2_000,
65
+ /** closures (tcb, uses, usedBy, refs), each a walk of the graph; each target of a `key(…)` counts as
66
+ * one too, since its review set is walked to keep only the marks still in it. `key(targets)`
67
+ * counts as one: how many targets there are is the project's, not the query's, and every
68
+ * `key(targets)` of a query shares the same walks (evaluateIn) */
69
+ closures: 64,
70
+ };
71
+ /** Parentheses within parentheses the reader of the text follows: twice the depth, since a
72
+ * closure of one plain word takes two (`tcb((all))`), with a margin. Only a guard for the stack;
73
+ * what a query may hold is QUERY_LIMITS, checked on what was read. */
74
+ const MAX_PARENS = 2 * QUERY_LIMITS.depth + 8;
75
+ /** Why a query is larger than QUERY_LIMITS allows, or null. Called on a tree already bounded in
76
+ * size by whoever built it, so that its recursion is shallow. */
77
+ export function beyondLimits(q) {
78
+ let terms = 0, names = 0, closures = 0, longest = 0;
79
+ const name = (s) => {
80
+ names++;
81
+ longest = Math.max(longest, s.length);
82
+ };
83
+ const depth = (x) => {
84
+ switch (x.op) {
85
+ case "and":
86
+ case "or":
87
+ case "except":
88
+ return Math.max(depth(x.a), depth(x.b) + (COMBINATORS.includes(x.b.op) ? 1 : 0));
89
+ case "tcb":
90
+ case "uses":
91
+ case "usedBy":
92
+ case "refs":
93
+ closures++;
94
+ if (x.of !== "selection" && !Array.isArray(x.of)) {
95
+ terms++;
96
+ return 1 + depth(x.of);
97
+ }
98
+ if (Array.isArray(x.of))
99
+ x.of.forEach(name);
100
+ terms++;
101
+ return 0;
102
+ case "decl":
103
+ x.names.forEach(name);
104
+ terms++;
105
+ return 0;
106
+ case "key":
107
+ if (x.of === "targets")
108
+ closures++;
109
+ else {
110
+ x.of.forEach(name);
111
+ closures += x.of.length;
112
+ }
113
+ terms++;
114
+ return 0;
115
+ case "ns":
116
+ case "file":
117
+ name(x.prefix);
118
+ terms++;
119
+ return 0;
120
+ default:
121
+ terms++;
122
+ return 0;
123
+ }
124
+ };
125
+ const L = QUERY_LIMITS;
126
+ const d = depth(q);
127
+ if (d > L.depth)
128
+ return `the query nests ${d} levels deep, more than ${L.depth}`;
129
+ if (terms > L.terms)
130
+ return `the query has ${terms} parts, more than ${L.terms.toLocaleString("en")}`;
131
+ if (names > L.names)
132
+ return `the query names ${names} declarations or prefixes, more than ${L.names.toLocaleString("en")}`;
133
+ if (longest > L.name)
134
+ return `a name in the query is longer than ${L.name.toLocaleString("en")} characters`;
135
+ if (closures > L.closures)
136
+ return `the query has ${closures} closures (tcb, uses, usedBy, refs), more than ${L.closures}`;
137
+ const length = printQuery(q).length;
138
+ if (length > L.text)
139
+ return `the query is ${length.toLocaleString("en")} characters long as printed, more than ${L.text.toLocaleString("en")}`;
140
+ return null;
141
+ }
142
+ // ---------------------------------------------------------------- the text form
143
+ export class QueryError extends Error {
144
+ /** where in the text it went wrong */
145
+ at;
146
+ constructor(message, at) {
147
+ super(message);
148
+ this.at = at;
149
+ }
150
+ }
151
+ function tokens(text) {
152
+ const out = [];
153
+ let i = 0;
154
+ while (i < text.length) {
155
+ const c = text[i];
156
+ if (/\s/.test(c)) {
157
+ i++;
158
+ }
159
+ else if (c === "(" || c === ")" || c === ",") {
160
+ out.push({ t: c, at: i });
161
+ i++;
162
+ }
163
+ else if (c === '"') {
164
+ // a JSON string: escapes as JSON has them
165
+ let j = i + 1;
166
+ while (j < text.length && text[j] !== '"')
167
+ j += text[j] === "\\" ? 2 : 1;
168
+ if (j >= text.length)
169
+ throw new QueryError("a quoted name is not closed", i);
170
+ let v;
171
+ try {
172
+ v = JSON.parse(text.slice(i, j + 1));
173
+ }
174
+ catch {
175
+ throw new QueryError("a quoted name is not a JSON string", i);
176
+ }
177
+ out.push({ t: "string", v: v, at: i });
178
+ i = j + 1;
179
+ }
180
+ else {
181
+ let j = i;
182
+ while (j < text.length && !/[\s(),"]/.test(text[j]))
183
+ j++;
184
+ out.push({ t: "word", v: text.slice(i, j), at: i });
185
+ i = j;
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+ /** The query a text says, within QUERY_LIMITS; throws a QueryError that says where it went wrong,
191
+ * or what is too large. */
192
+ export function parseQuery(text) {
193
+ if (text.length > QUERY_LIMITS.text)
194
+ throw new QueryError(`the query is longer than ${QUERY_LIMITS.text.toLocaleString("en")} characters`, 0);
195
+ let q;
196
+ try {
197
+ q = parse(text);
198
+ }
199
+ catch (e) {
200
+ // the reader follows parentheses by recursion; a guard stops it long before the stack ends,
201
+ // but whatever runs out of stack is a query too large, said as one (a 400, not a 500)
202
+ if (e instanceof RangeError)
203
+ throw new QueryError("the query is nested too deeply to read", 0);
204
+ throw e;
205
+ }
206
+ const why = beyondLimits(q);
207
+ if (why)
208
+ throw new QueryError(why, 0);
209
+ return q;
210
+ }
211
+ function parse(text) {
212
+ const ts = tokens(text);
213
+ let k = 0;
214
+ // what has been read so far, against the bounds: the reader stops as soon as one is passed, so
215
+ // that what it builds stays small enough for everything after it
216
+ let parens = 0, terms = 0, names = 0;
217
+ const nest = (at, read) => {
218
+ if (++parens > MAX_PARENS)
219
+ throw new QueryError(`the query nests more than ${QUERY_LIMITS.depth} levels deep`, at);
220
+ const r = read();
221
+ parens--;
222
+ return r;
223
+ };
224
+ const end = text.length;
225
+ const peek = () => ts[k];
226
+ const where = () => ts[k]?.at ?? end;
227
+ const word = (t) => (t && t.t === "word" ? ALIASES[t.v.toLowerCase()] ?? t.v : null);
228
+ const keyword = (t) => {
229
+ const w = word(t);
230
+ if (w === null)
231
+ return null;
232
+ const lower = w.toLowerCase();
233
+ // keywords are read without regard to case; `usedBy` keeps its capital
234
+ return lower === "usedby" ? "usedBy" : lower;
235
+ };
236
+ const text1 = (t) => ("v" in t ? t.v : t.t);
237
+ const args = () => {
238
+ if (peek()?.t !== "(")
239
+ throw new QueryError(`${text1(ts[k - 1])} takes its arguments in parentheses, e.g. ${text1(ts[k - 1])}(A.b)`, where());
240
+ k++;
241
+ const out = [];
242
+ if (peek()?.t === ")") {
243
+ k++;
244
+ return out;
245
+ }
246
+ for (;;) {
247
+ const t = peek();
248
+ if (!t || (t.t !== "word" && t.t !== "string"))
249
+ throw new QueryError("a name or a word was expected", where());
250
+ if (++names > QUERY_LIMITS.names)
251
+ throw new QueryError(`the query names more than ${QUERY_LIMITS.names.toLocaleString("en")} declarations or prefixes`, t.at);
252
+ if (t.v.length > QUERY_LIMITS.name)
253
+ throw new QueryError(`a name is longer than ${QUERY_LIMITS.name.toLocaleString("en")} characters`, t.at);
254
+ out.push(t);
255
+ k++;
256
+ const sep = peek();
257
+ if (sep?.t === ")") {
258
+ k++;
259
+ return out;
260
+ }
261
+ if (sep?.t !== ",")
262
+ throw new QueryError("a comma or a closing parenthesis was expected", where());
263
+ k++;
264
+ }
265
+ };
266
+ const choose = (xs, allowed, what) => {
267
+ if (!xs.length)
268
+ throw new QueryError(`${what}: give at least one`, where());
269
+ return xs.map((x) => {
270
+ const v = x.t === "word" ? x.v.toLowerCase() : null;
271
+ const hit = allowed.find((a) => a.toLowerCase() === v);
272
+ if (!hit)
273
+ throw new QueryError(`${text1(x)} is not one of ${allowed.join(", ")}`, x.at);
274
+ return hit;
275
+ });
276
+ };
277
+ const atom = () => {
278
+ const t = peek();
279
+ if (!t)
280
+ throw new QueryError("the query ends too early", end);
281
+ if (t.t === "(") {
282
+ k++;
283
+ const q = nest(t.at, expr);
284
+ if (peek()?.t !== ")")
285
+ throw new QueryError("a closing parenthesis was expected", where());
286
+ k++;
287
+ return q;
288
+ }
289
+ const w = keyword(t);
290
+ if (w === null)
291
+ throw new QueryError(t.t === "string" ? "a quoted name stands only inside decl(…), tcb(…), uses(…), usedBy(…) or refs(…)" : `${t.t} is out of place`, t.at);
292
+ if (++terms > QUERY_LIMITS.terms)
293
+ throw new QueryError(`the query has more than ${QUERY_LIMITS.terms.toLocaleString("en")} parts`, t.at);
294
+ k++;
295
+ if (PLAIN.includes(w))
296
+ return { op: w };
297
+ if (CLOSURES.includes(w)) {
298
+ // a query inside: it starts with a parenthesis or a word and its parenthesis, or a name is
299
+ // joined to something (a list of names never is)
300
+ const [a, b] = [ts[k + 1], ts[k + 2]];
301
+ const join = keyword(b);
302
+ if (peek()?.t === "(" && (a?.t === "(" || (a?.t === "word" && (b?.t === "(" || (join !== null && COMBINATORS.includes(join)))))) {
303
+ k++;
304
+ const of = nest(t.at, expr);
305
+ if (peek()?.t !== ")")
306
+ throw new QueryError("a closing parenthesis was expected", where());
307
+ k++;
308
+ return { op: w, of };
309
+ }
310
+ const xs = args();
311
+ if (xs.length === 1 && xs[0].t === "word" && xs[0].v === "selection")
312
+ return { op: w, of: "selection" };
313
+ if (!xs.length)
314
+ throw new QueryError(`${w}: give the declarations it starts from`, where());
315
+ return { op: w, of: xs.map(text1) };
316
+ }
317
+ if (w === "decl") {
318
+ const xs = args();
319
+ if (!xs.length)
320
+ throw new QueryError("decl: give at least one name", where());
321
+ return { op: "decl", names: xs.map(text1) };
322
+ }
323
+ if (w === "key") {
324
+ const xs = args();
325
+ if (!xs.length)
326
+ throw new QueryError("key: give at least one target", where());
327
+ if (xs.length === 1 && xs[0].t === "word" && xs[0].v === "targets")
328
+ return { op: "key", of: "targets" };
329
+ return { op: "key", of: xs.map(text1) };
330
+ }
331
+ if (w === "ns" || w === "file") {
332
+ const xs = args();
333
+ if (xs.length !== 1)
334
+ throw new QueryError(`${w}: give one prefix`, where());
335
+ return { op: w, prefix: text1(xs[0]) };
336
+ }
337
+ if (w === "kind")
338
+ return { op: "kind", kinds: choose(args(), KINDS, "kind") };
339
+ if (w === "status")
340
+ return { op: "status", statuses: choose(args(), STATUSES, "status") };
341
+ if (w === "advice")
342
+ return { op: "advice", verdicts: choose(args(), VERDICTS, "advice") };
343
+ throw new QueryError(`${text1(t)} is not a word of the query language (all, tcb, uses, usedBy, refs, decl, key, ns, file, trusted, external, kind, status, advice, sorry, nonstandard)`, t.at);
344
+ };
345
+ const expr = () => {
346
+ let q = atom();
347
+ for (;;) {
348
+ const op = keyword(peek());
349
+ if (!op || !COMBINATORS.includes(op))
350
+ return q;
351
+ k++;
352
+ q = { op: op, a: q, b: atom() };
353
+ }
354
+ };
355
+ const q = expr();
356
+ if (k < ts.length)
357
+ throw new QueryError(`"${text1(ts[k])}" was not expected here; join conditions with and, or, except`, ts[k].at);
358
+ return q;
359
+ }
360
+ /** A name as the text form writes it: bare where that reads back the same, quoted otherwise. */
361
+ function arg(s) {
362
+ return /^[^\s(),"]+$/.test(s) && s !== "selection" ? s : JSON.stringify(s);
363
+ }
364
+ /** The canonical text of a query. */
365
+ export function printQuery(q) {
366
+ switch (q.op) {
367
+ case "all":
368
+ case "selection":
369
+ case "trusted":
370
+ case "external":
371
+ case "sorry":
372
+ case "nonstandard":
373
+ return q.op;
374
+ case "decl":
375
+ return `decl(${q.names.map(arg).join(", ")})`;
376
+ case "key":
377
+ // a target named `targets` is a name, quoted: the bare word is every target
378
+ return `key(${q.of === "targets" ? "targets" : q.of.map((t) => (t === "targets" ? JSON.stringify(t) : arg(t))).join(", ")})`;
379
+ case "tcb":
380
+ case "uses":
381
+ case "usedBy":
382
+ case "refs": {
383
+ if (q.of === "selection" || Array.isArray(q.of))
384
+ return `${q.op}(${q.of === "selection" ? "selection" : q.of.map(arg).join(", ")})`;
385
+ // a query of one plain word would read as a name
386
+ const inner = printQuery(q.of);
387
+ return `${q.op}(${PLAIN.includes(q.of.op) ? `(${inner})` : inner})`;
388
+ }
389
+ case "ns":
390
+ case "file":
391
+ return `${q.op}(${arg(q.prefix)})`;
392
+ case "kind":
393
+ return `kind(${q.kinds.join(", ")})`;
394
+ case "status":
395
+ return `status(${q.statuses.join(", ")})`;
396
+ case "advice":
397
+ return `advice(${q.verdicts.join(", ")})`;
398
+ case "and":
399
+ case "or":
400
+ case "except": {
401
+ // read left to right: only a combination on the right needs its parentheses
402
+ const b = COMBINATORS.includes(q.b.op) ? `(${printQuery(q.b)})` : printQuery(q.b);
403
+ return `${printQuery(q.a)} ${q.op} ${b}`;
404
+ }
405
+ }
406
+ }
407
+ /** A query sent as JSON, checked and bounded as its text would be (QUERY_LIMITS); null when it is
408
+ * not one. Text is parsed. */
409
+ export function cleanQuery(x) {
410
+ return typeof x === "string" ? readText(x) : readJson(x);
411
+ }
412
+ function readText(x) {
413
+ try {
414
+ return parseQuery(x);
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ }
420
+ /** Why a query's JSON form is not one, or is too large; null when it is fine. */
421
+ export function invalidQuery(x) {
422
+ if (typeof x === "string") {
423
+ try {
424
+ parseQuery(x);
425
+ return null;
426
+ }
427
+ catch (e) {
428
+ return e instanceof QueryError ? e.message : String(e);
429
+ }
430
+ }
431
+ const q = readJson(x, true);
432
+ return typeof q === "string" ? q : null;
433
+ }
434
+ function readJson(x, why = false) {
435
+ // whatever names a text could hold, a JSON string can (an empty one included: `decl("")`); a
436
+ // list longer than a query may name is refused before it is looked at
437
+ const str = (s) => typeof s === "string";
438
+ let large = null;
439
+ const names = (xs) => {
440
+ if (Array.isArray(xs) && xs.length > QUERY_LIMITS.names)
441
+ large = `the query names more than ${QUERY_LIMITS.names.toLocaleString("en")} declarations or prefixes`;
442
+ return Array.isArray(xs) && xs.length > 0 && xs.length <= QUERY_LIMITS.names && xs.every(str);
443
+ };
444
+ // a JSON tree can be as deep as it likes: the walk stops once it has seen more parts than a
445
+ // query may have, so that its own recursion, and everything's after it, stays shallow
446
+ let nodes = 0;
447
+ const walk = (v) => {
448
+ if (!v || typeof v !== "object" || ++nodes > 2 * QUERY_LIMITS.terms + 1)
449
+ return null;
450
+ const o = v;
451
+ switch (o.op) {
452
+ case "all":
453
+ case "selection":
454
+ case "trusted":
455
+ case "external":
456
+ case "sorry":
457
+ case "nonstandard":
458
+ return { op: o.op };
459
+ case "decl":
460
+ return names(o.names) ? { op: "decl", names: [...o.names] } : null;
461
+ case "key":
462
+ if (o.of === "targets")
463
+ return { op: "key", of: "targets" };
464
+ return names(o.of) ? { op: "key", of: [...o.of] } : null;
465
+ case "tcb":
466
+ case "uses":
467
+ case "usedBy":
468
+ case "refs": {
469
+ if (o.of === "selection")
470
+ return { op: o.op, of: "selection" };
471
+ if (o.of && typeof o.of === "object" && !Array.isArray(o.of)) {
472
+ const of = walk(o.of);
473
+ return of ? { op: o.op, of } : null;
474
+ }
475
+ return names(o.of) ? { op: o.op, of: [...o.of] } : null;
476
+ }
477
+ case "ns":
478
+ case "file":
479
+ return str(o.prefix) ? { op: o.op, prefix: o.prefix } : null;
480
+ case "kind":
481
+ return Array.isArray(o.kinds) && o.kinds.length && o.kinds.every((k) => KINDS.includes(k)) ? { op: "kind", kinds: [...o.kinds] } : null;
482
+ case "status":
483
+ return Array.isArray(o.statuses) && o.statuses.length && o.statuses.every((s) => STATUSES.includes(s))
484
+ ? { op: "status", statuses: [...o.statuses] }
485
+ : null;
486
+ case "advice":
487
+ return Array.isArray(o.verdicts) && o.verdicts.length && o.verdicts.every((s) => VERDICTS.includes(s)) ? { op: "advice", verdicts: [...o.verdicts] } : null;
488
+ case "and":
489
+ case "or":
490
+ case "except": {
491
+ const a = walk(o.a), b = a && walk(o.b);
492
+ return a && b ? { op: o.op, a, b } : null;
493
+ }
494
+ default:
495
+ return null;
496
+ }
497
+ };
498
+ const q = walk(x);
499
+ if (nodes > 2 * QUERY_LIMITS.terms + 1)
500
+ large = `the query has more than ${QUERY_LIMITS.terms.toLocaleString("en")} parts`;
501
+ if (!q)
502
+ return why ? large ?? "it is neither a query's text nor its JSON form" : null;
503
+ const over = beyondLimits(q);
504
+ return over ? (why ? over : null) : q;
505
+ }
506
+ /** Whether two queries say the same: their canonical texts are one. */
507
+ export function sameQuery(a, b) {
508
+ return printQuery(a) === printQuery(b);
509
+ }
510
+ export function chainOf(q) {
511
+ const conditions = [];
512
+ let x = q;
513
+ while (x.op === "and" || x.op === "or" || x.op === "except") {
514
+ conditions.unshift({ op: x.op, q: x.b });
515
+ x = x.a;
516
+ }
517
+ return { base: x, conditions };
518
+ }
519
+ export function fromChain(base, conditions) {
520
+ return conditions.reduce((a, c) => ({ op: c.op, a, b: c.q }), base);
521
+ }
522
+ /** What a view opened from a board holds (Zoom in): `q` among what the board holds — the board's
523
+ * whole scope, not only the conditions it ends with, so that the view holds exactly what was lit
524
+ * there (on a board of a target's review set, what is in the target's TCB). On a board of everything, or one that is
525
+ * `q` already, `q` itself. (Whether `q` holds anything the board leaves out is a question of sets:
526
+ * shared/board.ts, zoomScope.) */
527
+ export function within(q, board) {
528
+ return board.op === "all" || sameQuery(q, board) ? q : { op: "and", a: q, b: board };
529
+ }
530
+ /** Every declaration a query names (its roots and decl lists), once each. */
531
+ export function namesIn(q) {
532
+ const out = new Set();
533
+ const walk = (x) => {
534
+ if (x.op === "decl")
535
+ x.names.forEach((n) => out.add(n));
536
+ else if (x.op === "key" && x.of !== "targets")
537
+ x.of.forEach((n) => out.add(n));
538
+ else if (x.op === "tcb" || x.op === "uses" || x.op === "usedBy" || x.op === "refs") {
539
+ if (Array.isArray(x.of))
540
+ x.of.forEach((n) => out.add(n));
541
+ else if (x.of !== "selection")
542
+ walk(x.of);
543
+ }
544
+ else if (x.op === "and" || x.op === "or" || x.op === "except") {
545
+ walk(x.a);
546
+ walk(x.b);
547
+ }
548
+ };
549
+ walk(q);
550
+ return [...out];
551
+ }
552
+ /** Whether the query reads the review (trust, statuses, advice): its set changes with opinions. */
553
+ export function readsReview(q) {
554
+ if (q.op === "trusted" || q.op === "status" || q.op === "advice" || q.op === "key")
555
+ return true;
556
+ if ((q.op === "tcb" || q.op === "uses" || q.op === "usedBy" || q.op === "refs") && q.of !== "selection" && !Array.isArray(q.of))
557
+ return readsReview(q.of);
558
+ if (q.op === "and" || q.op === "or" || q.op === "except")
559
+ return readsReview(q.a) || readsReview(q.b);
560
+ return false;
561
+ }
562
+ /** The query with the selection replaced by `names`: what is lit now, as a scope (Zoom in). */
563
+ export function withSelection(q, names) {
564
+ switch (q.op) {
565
+ case "selection":
566
+ return { op: "decl", names: [...names] };
567
+ case "tcb":
568
+ case "uses":
569
+ case "usedBy":
570
+ case "refs":
571
+ if (q.of === "selection")
572
+ return { op: q.op, of: [...names] };
573
+ return Array.isArray(q.of) ? q : { op: q.op, of: withSelection(q.of, names) };
574
+ case "and":
575
+ case "or":
576
+ case "except":
577
+ return { op: q.op, a: withSelection(q.a, names), b: withSelection(q.b, names) };
578
+ default:
579
+ return q;
580
+ }
581
+ }
582
+ /** A target's review set (ReviewSpec decision 3): what its review counts (decided 2026-10-08,
583
+ * shared/reviewqueue.ts) — its TCB without what is trusted, an untrusted outside declaration
584
+ * included, and the non-standard axioms it rests on through statements, bodies or proofs, which
585
+ * no prefix trusts: `tcb(T) except trusted or (uses(T) and kind(axiom) and nonstandard)`. */
586
+ export function reviewScope(target) {
587
+ const axioms = { op: "and", a: { op: "and", a: { op: "uses", of: [target] }, b: { op: "kind", kinds: ["axiom"] } }, b: { op: "nonstandard" } };
588
+ return fromChain({ op: "tcb", of: [target] }, [
589
+ { op: "except", q: { op: "trusted" } },
590
+ { op: "or", q: axioms },
591
+ ]);
592
+ }
593
+ /** What the Keys step of Rests on selects (ViewSpec decision 20): what was clicked — a declaration,
594
+ * or the set a cluster holds — and the key declarations in its TCB, of the targets `keys` names
595
+ * (`targets`: every target). That is `decl(X) or (tcb(X) and key(T…))`, written
596
+ * `tcb(X) and (key(T…) or decl(X))`: the same set, since a TCB holds what it starts from, as one
597
+ * part that a selection's `or`s never split (web/src/selection.ts, picksOf). */
598
+ export function keysQuery(from, keys) {
599
+ const self = typeof from === "string" ? { op: "decl", names: [from] } : from;
600
+ return { op: "and", a: { op: "tcb", of: typeof from === "string" ? [from] : from }, b: { op: "or", a: { op: "key", of: keys === "targets" ? keys : [...keys] }, b: self } };
601
+ }
602
+ /** What a query of the Keys step starts from, and whose key declarations it holds; null for any
603
+ * other query. */
604
+ export function readKeys(q) {
605
+ if (q.op !== "and" || q.a.op !== "tcb" || q.b.op !== "or" || q.b.a.op !== "key")
606
+ return null;
607
+ const of = q.a.of;
608
+ const from = of === "selection" ? null : Array.isArray(of) ? (of.length === 1 ? of[0] : null) : of;
609
+ return from !== null && sameQuery(keysQuery(from, q.b.a.of), q) ? { from, keys: q.b.a.of } : null;
610
+ }
611
+ /** Whether the board leaves the outside world out: a condition `except external` on its chain. */
612
+ export function withoutOutside(q) {
613
+ return chainOf(q).conditions.some((c) => c.op === "except" && c.q.op === "external");
614
+ }
615
+ /** The scope with the outside world in (every `except external` taken off its chain) or out (one
616
+ * added at the end): what the External switch did, now a condition of the board. */
617
+ export function setOutside(q, on) {
618
+ const { base, conditions } = chainOf(q);
619
+ const rest = conditions.filter((c) => !(c.op === "except" && c.q.op === "external"));
620
+ return fromChain(base, on ? rest : [...rest, { op: "except", q: { op: "external" } }]);
621
+ }
622
+ // ---------------------------------------------------------------- in words
623
+ const last = (name) => {
624
+ const parts = name.split(".");
625
+ const tail = parts[parts.length - 1];
626
+ return /^\d+$/.test(tail) || tail.length <= 2 ? parts.slice(-2).join(".") : tail;
627
+ };
628
+ const list = (names) => (names.length > 2 ? `${names.slice(0, 2).map(last).join(", ")} +${names.length - 2}` : names.map(last).join(", "));
629
+ const roots = (of) => (of === "selection" ? "the selection" : Array.isArray(of) ? list(of) : describe(of));
630
+ const or = (xs) => (xs.length > 1 ? `${xs.slice(0, -1).join(", ")} or ${xs[xs.length - 1]}` : xs[0]);
631
+ // a Map, not an object: `constructor` is a kind, and an object's own property besides
632
+ const PLURAL = new Map([["theorem", "theorems"], ["def", "definitions"], ["abbrev", "abbreviations"], ["instance", "instances"], ["structure", "structures"], ["class", "classes"], ["inductive", "inductive types"], ["opaque", "opaque constants"], ["axiom", "axioms"], ["notation", "notations"]]);
633
+ /** A set in a few words, for a chip: "TCB of fermatLastTheoremFour", "everything". */
634
+ export function describe(q) {
635
+ // the Keys step's one part (keysQuery), said as what it is rather than as its and and or
636
+ const k = readKeys(q);
637
+ if (k) {
638
+ const what = typeof k.from === "string" ? last(k.from) : describe(k.from);
639
+ const whose = k.keys === "targets" || (k.keys.length === 1 && k.keys[0] === k.from) ? "" : `of ${list(k.keys)} that `;
640
+ return `key declarations ${whose}${what} rests on`;
641
+ }
642
+ switch (q.op) {
643
+ case "all":
644
+ return "everything";
645
+ case "selection":
646
+ return "the selection";
647
+ case "decl":
648
+ return list(q.names);
649
+ case "key":
650
+ return q.of === "targets" ? "key declarations of every target" : `key declarations of ${list(q.of)}`;
651
+ case "tcb":
652
+ return `TCB of ${roots(q.of)}`;
653
+ case "uses":
654
+ return `what ${roots(q.of)} uses`;
655
+ case "refs":
656
+ return `what ${roots(q.of)} uses directly`;
657
+ case "usedBy":
658
+ return `what uses ${roots(q.of)}`;
659
+ case "ns":
660
+ return `namespace ${q.prefix}`;
661
+ case "file":
662
+ return `files under ${q.prefix}`;
663
+ case "trusted":
664
+ return "what is trusted";
665
+ case "external":
666
+ return "the outside world";
667
+ case "sorry":
668
+ return "what uses sorry";
669
+ case "nonstandard":
670
+ return "what rests on non-standard axioms";
671
+ case "kind":
672
+ return or(q.kinds.map((k) => PLURAL.get(k) ?? k));
673
+ case "status":
674
+ return or(q.statuses.map((s) => (s === "unreviewed" ? "not yet reviewed" : s)));
675
+ case "advice":
676
+ return `what an agent ${or(q.verdicts.map((v) => (v === "approve" ? "approves" : "comments on")))}`;
677
+ case "and":
678
+ case "or":
679
+ case "except":
680
+ // read left to right, as the text is: a combination on the right in parentheses
681
+ return `${describe(q.a)} ${q.op} ${COMBINATORS.includes(q.b.op) ? `(${describe(q.b)})` : describe(q.b)}`;
682
+ }
683
+ }
684
+ /** A condition in a few words, as its chip reads: "without the outside world", "only theorems". */
685
+ export function describeCondition(c) {
686
+ const q = c.q;
687
+ if (c.op === "except" && q.op === "external")
688
+ return "without the outside world";
689
+ if (c.op === "except" && q.op === "trusted")
690
+ return "except what is trusted";
691
+ if (c.op === "and") {
692
+ if (q.op === "sorry")
693
+ return "only with sorry";
694
+ if (q.op === "nonstandard")
695
+ return "only with non-standard axioms";
696
+ if (q.op === "ns")
697
+ return `only in namespace ${q.prefix}`;
698
+ if (q.op === "file")
699
+ return `only in files under ${q.prefix}`;
700
+ if (q.op === "kind" || q.op === "status" || q.op === "advice" || q.op === "key")
701
+ return `only ${describe(q)}`;
702
+ return `and ${describe(q)}`;
703
+ }
704
+ if (c.op === "except")
705
+ return `except ${describe(q)}`;
706
+ return `or ${describe(q)}`;
707
+ }
708
+ /** The declarations of `ctx.g` a query holds, as a 0/1 array. Names the graph does not hold stand
709
+ * for nothing. */
710
+ export function evaluate(q, ctx) {
711
+ return evaluateIn(q, ctx, new Map());
712
+ }
713
+ /** `evaluate`, with the closures this evaluation has walked so far, by what each is of: the same
714
+ * closure asked twice in one query (`uses(usedBy(X)) or usedBy(X)`) walks the graph once. With
715
+ * QUERY_LIMITS bounding how many a query holds, no query costs more than a few dozen walks. */
716
+ function evaluateIn(q, ctx, walks) {
717
+ const { g, review: d } = ctx;
718
+ const n = g.n;
719
+ const out = new Uint8Array(n);
720
+ const each = (f) => {
721
+ for (let i = 0; i < n; i++)
722
+ if (f(i))
723
+ out[i] = 1;
724
+ return out;
725
+ };
726
+ const idx = (names) => names.map((x) => g.byName.get(x)).filter((i) => i !== undefined);
727
+ const roots = (of) => {
728
+ if (of === "selection")
729
+ return (ctx.selection ?? []).filter((i) => i >= 0 && i < n);
730
+ if (Array.isArray(of))
731
+ return idx(of);
732
+ const set = evaluateIn(of, ctx, walks);
733
+ const out = [];
734
+ for (let i = 0; i < n; i++)
735
+ if (set[i])
736
+ out.push(i);
737
+ return out;
738
+ };
739
+ switch (q.op) {
740
+ case "all":
741
+ return out.fill(1);
742
+ case "selection":
743
+ for (const i of roots("selection"))
744
+ out[i] = 1;
745
+ return out;
746
+ case "decl":
747
+ for (const i of idx(q.names))
748
+ out[i] = 1;
749
+ return out;
750
+ case "key":
751
+ // each target, and what was marked key for it that its review still holds (its Review
752
+ // view's set, shared/reviewqueue.ts): a mark on what has left the review counts for nothing
753
+ for (const t of q.of === "targets" ? Object.keys(d.targets) : q.of) {
754
+ const root = g.byName.get(t);
755
+ if (root === undefined)
756
+ continue;
757
+ out[root] = 1;
758
+ const marks = keyMarks(d, t).map((x) => g.byName.get(x)).filter((i) => i !== undefined);
759
+ if (!marks.length)
760
+ continue;
761
+ const set = evaluateIn(reviewScope(t), ctx, walks);
762
+ for (const i of marks)
763
+ if (set[i])
764
+ out[i] = 1;
765
+ }
766
+ return out;
767
+ case "tcb":
768
+ case "uses":
769
+ case "usedBy":
770
+ case "refs": {
771
+ const key = `${q.op}\u0000${q.of === "selection" ? "\u0001" : Array.isArray(q.of) ? JSON.stringify(q.of) : printQuery(q.of)}`;
772
+ let walked = walks.get(key);
773
+ if (!walked) {
774
+ const r = roots(q.of);
775
+ walked = r.length ? g.closure(CLOSURE_MODE[q.op], r) : out;
776
+ walks.set(key, walked);
777
+ }
778
+ // a copy: what combines it below writes into the set it is given
779
+ return walked.slice();
780
+ }
781
+ case "ns":
782
+ return each((i) => under(g.nodes[i].name, q.prefix));
783
+ case "file":
784
+ return each((i) => under(g.nodes[i].module, q.prefix));
785
+ case "trusted":
786
+ return each((i) => trusted(d, g.nodes[i].name, g.nodes[i].module, g.isExt(i)));
787
+ case "external":
788
+ return each((i) => g.isExt(i));
789
+ case "sorry":
790
+ return each((i) => g.axiomsOf(i).includes("sorryAx"));
791
+ case "nonstandard":
792
+ return each((i) => g.axiomsOf(i).some((a) => !STANDARD_AXIOMS.includes(a)));
793
+ case "kind": {
794
+ const ks = new Set(q.kinds);
795
+ return each((i) => ks.has(g.nodes[i].kind));
796
+ }
797
+ case "status": {
798
+ const ss = new Set(q.statuses.map((s) => (s === "unreviewed" ? "none" : s)));
799
+ return each((i) => ss.has(statusOf(d, g.nodes[i].name, g.nodes[i].hash)));
800
+ }
801
+ case "advice": {
802
+ const vs = new Set(q.verdicts);
803
+ return each((i) => {
804
+ const a = adviceOf(d, g.nodes[i].name, g.nodes[i].hash);
805
+ return a !== null && vs.has(a);
806
+ });
807
+ }
808
+ case "and":
809
+ case "or":
810
+ case "except": {
811
+ const a = evaluateIn(q.a, ctx, walks);
812
+ const b = evaluateIn(q.b, ctx, walks);
813
+ for (let i = 0; i < n; i++)
814
+ a[i] = q.op === "and" ? a[i] & b[i] : q.op === "or" ? a[i] | b[i] : a[i] & (1 - b[i]);
815
+ return a;
816
+ }
817
+ }
818
+ }