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.
- package/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
- package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3270 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +780 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1406 -0
- package/dist/server/mcptools.js +496 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +159 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +50 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +164 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +241 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +124 -0
- package/dist/shared/doc/outline.js +45 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +244 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +103 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +818 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +281 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +300 -0
- package/dist/shared/viewgraph.js +157 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +52671 -0
- package/dist/web/bundle/edges.js +407 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +257 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +804 -0
- package/dist/web/style.css +2548 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +125 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// What a view nobody named is called (the owner's decision of 2026-10-10): a short title, said as the
|
|
2
|
+
// selection's chips say a pick ("TCB of ns Fermat42"), wherever a title shows — a tab, the Views menu,
|
|
3
|
+
// Activity, presence, the exported document, the agents' answers — with the full description of the
|
|
4
|
+
// scope (shared/query.ts, `describe`) and the query behind it where the page shows the title. A name
|
|
5
|
+
// people gave is never changed: shared/views.ts, viewTitle, uses it as it is.
|
|
6
|
+
//
|
|
7
|
+
// A namespace, a file or a declaration is named by its last part and as many of its parents as tell
|
|
8
|
+
// it apart from the project's other names of its kind (`TitleNames`, made from the graph on show); a
|
|
9
|
+
// cluster's set is read as the cluster (`ns Fermat42`, not "namespace Fermat42 except the outside world", the
|
|
10
|
+
// outside world being left out of a namespace's cluster by default); the conditions on a scope are
|
|
11
|
+
// condensed (`− trusted`, `+ helper`, `in ★T`, `, only theorems`); a title is about 30
|
|
12
|
+
// characters and never more than `TITLE_MAX`, conditions that do not fit said by an ellipsis.
|
|
13
|
+
import { readKeys, reviewScope, sameQuery } from "./query.js";
|
|
14
|
+
/** the most characters a title made from a scope has */
|
|
15
|
+
export const TITLE_MAX = 48;
|
|
16
|
+
/** A name's parts: what its dots separate, a part in «» kept whole though it holds one. */
|
|
17
|
+
export function nameParts(name) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let cur = "";
|
|
20
|
+
let depth = 0;
|
|
21
|
+
for (const ch of name) {
|
|
22
|
+
if (ch === "«")
|
|
23
|
+
depth++;
|
|
24
|
+
else if (ch === "»")
|
|
25
|
+
depth = Math.max(0, depth - 1);
|
|
26
|
+
if (ch === "." && depth === 0) {
|
|
27
|
+
out.push(cur);
|
|
28
|
+
cur = "";
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
cur += ch;
|
|
32
|
+
}
|
|
33
|
+
out.push(cur);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** How many parts a name is said by at least: its last, or the last two where the last says little
|
|
37
|
+
* alone (a number, two letters), as the chips do. */
|
|
38
|
+
function least(parts) {
|
|
39
|
+
const tail = parts[parts.length - 1];
|
|
40
|
+
return parts.length > 1 && (/^\d+$/.test(tail) || tail.length <= 2) ? 2 : 1;
|
|
41
|
+
}
|
|
42
|
+
/** A name's last part, as a chip says it ("FLT", not "Mathlib.NumberTheory.FLT"). */
|
|
43
|
+
export function lastPart(name) {
|
|
44
|
+
const parts = nameParts(name);
|
|
45
|
+
return parts.slice(-least(parts)).join(".");
|
|
46
|
+
}
|
|
47
|
+
/** The names of one kind, and how many of them end with each run of last parts: made when first
|
|
48
|
+
* asked for. */
|
|
49
|
+
class Kind {
|
|
50
|
+
make;
|
|
51
|
+
names = null;
|
|
52
|
+
ends = new Map();
|
|
53
|
+
said = new Map();
|
|
54
|
+
constructor(make) {
|
|
55
|
+
this.make = make;
|
|
56
|
+
}
|
|
57
|
+
short(name) {
|
|
58
|
+
let s = this.said.get(name);
|
|
59
|
+
if (s !== undefined)
|
|
60
|
+
return s;
|
|
61
|
+
if (!this.names) {
|
|
62
|
+
this.names = new Set(this.make());
|
|
63
|
+
for (const n of this.names) {
|
|
64
|
+
const parts = nameParts(n);
|
|
65
|
+
let end = "";
|
|
66
|
+
for (let k = parts.length - 1; k >= 0; k--) {
|
|
67
|
+
end = end ? `${parts[k]}.${end}` : parts[k];
|
|
68
|
+
this.ends.set(end, (this.ends.get(end) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const parts = nameParts(name);
|
|
73
|
+
const own = this.names.has(name) ? 1 : 0;
|
|
74
|
+
s = name;
|
|
75
|
+
for (let k = least(parts); k < parts.length; k++) {
|
|
76
|
+
const end = parts.slice(-k).join(".");
|
|
77
|
+
if ((this.ends.get(end) ?? 0) <= own) {
|
|
78
|
+
s = end;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.said.set(name, s);
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The names a title tells apart: the declarations (the outside world's too), the project's
|
|
87
|
+
* namespaces (every prefix of a declaration's name) and its files and folders (every module and
|
|
88
|
+
* every prefix of one). */
|
|
89
|
+
export function titleNames(decls, namespaces, files) {
|
|
90
|
+
const d = new Kind(decls), n = new Kind(namespaces), f = new Kind(files);
|
|
91
|
+
return { decl: (x) => d.short(x), ns: (x) => n.short(x), file: (x) => f.short(x) };
|
|
92
|
+
}
|
|
93
|
+
const made = new WeakMap();
|
|
94
|
+
/** The names of graph `g` (the graph on show), made once per graph. */
|
|
95
|
+
export function namesOf(g) {
|
|
96
|
+
let t = made.get(g);
|
|
97
|
+
if (!t) {
|
|
98
|
+
const prefixes = (names, whole) => {
|
|
99
|
+
const out = new Set();
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
const parts = nameParts(name);
|
|
102
|
+
let p = "";
|
|
103
|
+
for (let k = 0; k < parts.length - (whole ? 0 : 1); k++)
|
|
104
|
+
out.add((p = p ? `${p}.${parts[k]}` : parts[k]));
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
};
|
|
108
|
+
t = titleNames(() => g.nodes.map((v) => v.name), () => prefixes(g.nodes.slice(0, g.projectCount).map((v) => v.name), false), () => prefixes(g.meta.modules.map((m) => m.name), true));
|
|
109
|
+
made.set(g, t);
|
|
110
|
+
}
|
|
111
|
+
return t;
|
|
112
|
+
}
|
|
113
|
+
/** Without the graph: each name by its last part. */
|
|
114
|
+
const PLAIN = { decl: lastPart, ns: lastPart, file: lastPart };
|
|
115
|
+
/** A build in a few words, for a title: "Oct 9 22:27", in the time of whoever reads it. */
|
|
116
|
+
export function briefWhen(at) {
|
|
117
|
+
const d = new Date(at);
|
|
118
|
+
return `${MONTHS[d.getMonth()]} ${d.getDate()} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
119
|
+
}
|
|
120
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
121
|
+
/** A build in full, for a document that is read later and elsewhere (ExportSpec decision 16): "Oct 9,
|
|
122
|
+
* 2026, 22:27 (UTC+2)", in the time of whoever writes it, which it names. */
|
|
123
|
+
export function fullWhen(at) {
|
|
124
|
+
const d = new Date(at);
|
|
125
|
+
const off = -d.getTimezoneOffset();
|
|
126
|
+
const zone = off ? `UTC${off > 0 ? "+" : "-"}${Math.floor(Math.abs(off) / 60)}${Math.abs(off) % 60 ? `:${String(Math.abs(off) % 60).padStart(2, "0")}` : ""}` : "UTC";
|
|
127
|
+
return `${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}, ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")} (${zone})`;
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------- a scope in a few words
|
|
130
|
+
// a Map, not an object: `constructor` is a kind, and an object's own property besides
|
|
131
|
+
const PLURAL = new Map([["theorem", "theorems"], ["def", "defs"], ["abbrev", "abbrevs"], ["instance", "instances"], ["structure", "structures"], ["class", "classes"], ["inductive", "inductives"], ["opaque", "opaques"], ["axiom", "axioms"], ["notation", "notations"], ["constructor", "constructors"], ["recursor", "recursors"]]);
|
|
132
|
+
const SIGN = { and: "∩", or: "+", except: "−" };
|
|
133
|
+
const or = (xs) => xs.join(" or ");
|
|
134
|
+
/** The target whose review set a query is (shared/query.ts, reviewScope), or null. */
|
|
135
|
+
function reviewOf(q) {
|
|
136
|
+
if (q.op !== "or" || q.a.op !== "except" || q.a.a.op !== "tcb" || !Array.isArray(q.a.a.of) || q.a.a.of.length !== 1)
|
|
137
|
+
return null;
|
|
138
|
+
const t = q.a.a.of[0];
|
|
139
|
+
return sameQuery(q, reviewScope(t)) ? t : null;
|
|
140
|
+
}
|
|
141
|
+
/** A cluster's set, as a cluster pick writes it (web/src/selection.ts, clusterSet), in a few words —
|
|
142
|
+
* a namespace's without what is outside the project, which is how a namespace is read — or null. */
|
|
143
|
+
function clusterWords(q, N) {
|
|
144
|
+
if (q.op === "external")
|
|
145
|
+
return "outside world";
|
|
146
|
+
if (q.op === "file")
|
|
147
|
+
return `file ${N.file(q.prefix)}`;
|
|
148
|
+
if (q.op === "except" && q.a.op === "ns" && q.b.op === "external")
|
|
149
|
+
return `ns ${N.ns(q.a.prefix)}`;
|
|
150
|
+
if (q.op === "and" && q.a.op === "file" && q.b.op === "external")
|
|
151
|
+
return `file ${lastPart(q.a.prefix)} (outside)`;
|
|
152
|
+
if (q.op === "except" && q.a.op === "and" && q.a.a.op === "ns" && q.a.b.op === "external" && q.b.op === "decl" && q.b.names.length === 1 && q.b.names[0] === q.a.a.prefix)
|
|
153
|
+
return `ns ${lastPart(q.a.a.prefix)} (outside)`;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/** Whether a query is read as one set, never split into a set and its conditions: a cluster's, a
|
|
157
|
+
* Keys pick, a target's review set. */
|
|
158
|
+
const whole = (q, N) => !!readKeys(q) || reviewOf(q) !== null || clusterWords(q, N) !== null;
|
|
159
|
+
const isCombination = (q) => q.op === "and" || q.op === "or" || q.op === "except";
|
|
160
|
+
function names(xs, N) {
|
|
161
|
+
const shown = xs.slice(0, 2).map(N.decl).join(", ");
|
|
162
|
+
return xs.length > 2 ? `${shown} +${xs.length - 2}` : shown;
|
|
163
|
+
}
|
|
164
|
+
function roots(of, N) {
|
|
165
|
+
if (of === "selection")
|
|
166
|
+
return "selection";
|
|
167
|
+
if (Array.isArray(of))
|
|
168
|
+
return names(of, N);
|
|
169
|
+
return clusterWords(of, N) ?? wrapped(of, N);
|
|
170
|
+
}
|
|
171
|
+
/** A set in a few words, in parentheses where it is a combination. */
|
|
172
|
+
function wrapped(q, N) {
|
|
173
|
+
const s = said(q, N);
|
|
174
|
+
return isCombination(q) && !whole(q, N) ? `(${s})` : s;
|
|
175
|
+
}
|
|
176
|
+
/** What a condition `and` keeps, where it is a filter rather than a set: "theorems", "stale". */
|
|
177
|
+
function filterWords(q) {
|
|
178
|
+
switch (q.op) {
|
|
179
|
+
case "kind":
|
|
180
|
+
return or(q.kinds.map((k) => PLURAL.get(k) ?? k));
|
|
181
|
+
case "status":
|
|
182
|
+
return or(q.statuses);
|
|
183
|
+
case "advice":
|
|
184
|
+
return or(q.verdicts.map((v) => (v === "approve" ? "agent-approved" : "agent-commented")));
|
|
185
|
+
case "sorry":
|
|
186
|
+
return "with sorry";
|
|
187
|
+
case "nonstandard":
|
|
188
|
+
return "on non-standard axioms";
|
|
189
|
+
case "trusted":
|
|
190
|
+
return "trusted";
|
|
191
|
+
case "external":
|
|
192
|
+
return "outside";
|
|
193
|
+
default:
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** A set in a few words, as a chip says it. */
|
|
198
|
+
function said(q, N) {
|
|
199
|
+
const k = readKeys(q);
|
|
200
|
+
if (k) {
|
|
201
|
+
const what = typeof k.from === "string" ? N.decl(k.from) : roots(k.from, N);
|
|
202
|
+
// every target's, or the one clicked: what it rests on; another target's: that target's keys in
|
|
203
|
+
// its TCB
|
|
204
|
+
if (k.keys === "targets" || (k.keys.length === 1 && k.keys[0] === k.from))
|
|
205
|
+
return `keys ${what} rests on`;
|
|
206
|
+
return `keys of ${names(k.keys, N)} in TCB of ${what}`;
|
|
207
|
+
}
|
|
208
|
+
const review = reviewOf(q);
|
|
209
|
+
if (review !== null)
|
|
210
|
+
return `review ★${N.decl(review)}`;
|
|
211
|
+
const cluster = clusterWords(q, N);
|
|
212
|
+
if (cluster)
|
|
213
|
+
return cluster;
|
|
214
|
+
switch (q.op) {
|
|
215
|
+
case "all":
|
|
216
|
+
return "everything";
|
|
217
|
+
case "selection":
|
|
218
|
+
return "selection";
|
|
219
|
+
case "decl":
|
|
220
|
+
return names(q.names, N);
|
|
221
|
+
case "key":
|
|
222
|
+
return q.of === "targets" ? "keys of all targets" : `keys of ${names(q.of, N)}`;
|
|
223
|
+
case "tcb":
|
|
224
|
+
return `TCB of ${roots(q.of, N)}`;
|
|
225
|
+
case "uses":
|
|
226
|
+
return `what ${roots(q.of, N)} uses`;
|
|
227
|
+
case "refs":
|
|
228
|
+
return `what ${roots(q.of, N)} uses directly`;
|
|
229
|
+
case "usedBy":
|
|
230
|
+
return `what uses ${roots(q.of, N)}`;
|
|
231
|
+
case "ns":
|
|
232
|
+
return `ns ${N.ns(q.prefix)}`;
|
|
233
|
+
case "file":
|
|
234
|
+
return `file ${N.file(q.prefix)}`;
|
|
235
|
+
case "sorry":
|
|
236
|
+
return "what uses sorry";
|
|
237
|
+
case "trusted":
|
|
238
|
+
case "external":
|
|
239
|
+
case "nonstandard":
|
|
240
|
+
case "kind":
|
|
241
|
+
case "status":
|
|
242
|
+
case "advice":
|
|
243
|
+
return filterWords(q);
|
|
244
|
+
case "and":
|
|
245
|
+
case "or":
|
|
246
|
+
case "except":
|
|
247
|
+
return `${said(q.a, N)} ${SIGN[q.op]} ${wrapped(q.b, N)}`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** A condition in a few words, with what joins it to what it follows: "− trusted", " in ns Fermat42",
|
|
251
|
+
* " in ★T" (within a target's review set), ", only theorems". */
|
|
252
|
+
function conditionWords(op, q, N) {
|
|
253
|
+
if (op === "except")
|
|
254
|
+
return ` − ${q.op === "external" ? "outside" : wrapped(q, N)}`;
|
|
255
|
+
if (op === "or")
|
|
256
|
+
return ` + ${wrapped(q, N)}`;
|
|
257
|
+
const review = reviewOf(q);
|
|
258
|
+
if (review !== null)
|
|
259
|
+
return ` in ★${N.decl(review)}`;
|
|
260
|
+
const f = filterWords(q);
|
|
261
|
+
return f ? `, only ${f}` : ` in ${wrapped(q, N)}`;
|
|
262
|
+
}
|
|
263
|
+
/** Text cut to `max` characters at most, an ellipsis where it was cut. */
|
|
264
|
+
export function cut(text, max) {
|
|
265
|
+
return text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1)).trimEnd()}…`;
|
|
266
|
+
}
|
|
267
|
+
/** A scope in a few words (`names`: the graph on show's, else each name by its last part): the set
|
|
268
|
+
* it starts from, then its conditions as long as they fit in `max` characters, an ellipsis for the
|
|
269
|
+
* rest. */
|
|
270
|
+
export function scopeTitle(q, names = null, max = TITLE_MAX) {
|
|
271
|
+
const N = names ?? PLAIN;
|
|
272
|
+
// the set it starts from, and the conditions applied to it in turn (shared/query.ts, chainOf),
|
|
273
|
+
// a set read whole never split
|
|
274
|
+
const conditions = [];
|
|
275
|
+
let x = q;
|
|
276
|
+
while ((x.op === "and" || x.op === "or" || x.op === "except") && !whole(x, N)) {
|
|
277
|
+
conditions.unshift({ op: x.op, q: x.b });
|
|
278
|
+
x = x.a;
|
|
279
|
+
}
|
|
280
|
+
let out = cut(said(x, N), conditions.length ? max - 2 : max);
|
|
281
|
+
for (let k = 0; k < conditions.length; k++) {
|
|
282
|
+
const next = out + conditionWords(conditions[k].op, conditions[k].q, N);
|
|
283
|
+
// room is left for the ellipsis while more conditions follow
|
|
284
|
+
if (next.length > (k < conditions.length - 1 ? max - 2 : max))
|
|
285
|
+
return `${out} …`;
|
|
286
|
+
out = next;
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
/** A build added to a title, " @ Oct 9 22:27": what a view kept at a past build is called, and what
|
|
291
|
+
* Save as new view names the view it makes (HistorySpec decision 19). */
|
|
292
|
+
export function atBuild(title, when) {
|
|
293
|
+
return `${cut(title, TITLE_MAX - when.length - 3)} @ ${when}`;
|
|
294
|
+
}
|
|
295
|
+
/** A title without the build a name made by Save as new view ends with (" @ Oct 9 22:27", or the
|
|
296
|
+
* older " · Oct 9, 22:27"), and the number that kept it apart: a view saved from it is named for
|
|
297
|
+
* its own build. */
|
|
298
|
+
export function withoutBuild(title) {
|
|
299
|
+
return title.replace(/ (?:@ [A-Z][a-z]{2} \d{1,2} \d{2}:\d{2}|@ build \d+|· [A-Z][a-z]{2} \d{1,2}, \d{2}:\d{2})(?: \(\d+\))?$/, "");
|
|
300
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The graph as the page reads it: adjacency in both directions, and the closures the modes draw.
|
|
2
|
+
// The agent API (server/agent.ts) reads the same graph the same way, so it lives here.
|
|
3
|
+
import { SITE_BODY, SITE_GRAMMAR, SITE_PROOF, SITE_STMT } from "./graph.js";
|
|
4
|
+
export const ALL_SITES = SITE_STMT | SITE_BODY | SITE_PROOF | SITE_GRAMMAR;
|
|
5
|
+
/** Which edges each mode follows. TCB never passes through a proof: by proof irrelevance a
|
|
6
|
+
* reference inside one cannot change what the statement it supports means; nor through grammar
|
|
7
|
+
* (SITE_GRAMMAR): what a syntax tree means is the rule's that interprets it. */
|
|
8
|
+
export const MODE_SITES = {
|
|
9
|
+
plain: ALL_SITES,
|
|
10
|
+
tcb: SITE_STMT | SITE_BODY,
|
|
11
|
+
deps: ALL_SITES,
|
|
12
|
+
refs: ALL_SITES,
|
|
13
|
+
apps: ALL_SITES,
|
|
14
|
+
};
|
|
15
|
+
export class Graph {
|
|
16
|
+
meta;
|
|
17
|
+
nodes;
|
|
18
|
+
n;
|
|
19
|
+
projectCount;
|
|
20
|
+
outStart;
|
|
21
|
+
outDst;
|
|
22
|
+
outSite;
|
|
23
|
+
inStart;
|
|
24
|
+
inSrc;
|
|
25
|
+
inSite;
|
|
26
|
+
byName = new Map();
|
|
27
|
+
/** a declaration Lean generated from a command → the unit it is folded into (ViewNode.also) */
|
|
28
|
+
foldedInto = new Map();
|
|
29
|
+
constructor(meta, edges) {
|
|
30
|
+
this.meta = meta;
|
|
31
|
+
this.nodes = meta.nodes;
|
|
32
|
+
this.n = meta.nodes.length;
|
|
33
|
+
this.projectCount = meta.projectCount;
|
|
34
|
+
const m = edges.length / 3;
|
|
35
|
+
const n = this.n;
|
|
36
|
+
this.outStart = new Int32Array(n + 1);
|
|
37
|
+
this.inStart = new Int32Array(n + 1);
|
|
38
|
+
for (let e = 0; e < m; e++) {
|
|
39
|
+
this.outStart[edges[3 * e] + 1]++;
|
|
40
|
+
this.inStart[edges[3 * e + 1] + 1]++;
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < n; i++) {
|
|
43
|
+
this.outStart[i + 1] += this.outStart[i];
|
|
44
|
+
this.inStart[i + 1] += this.inStart[i];
|
|
45
|
+
}
|
|
46
|
+
this.outDst = new Int32Array(m);
|
|
47
|
+
this.outSite = new Uint8Array(m);
|
|
48
|
+
this.inSrc = new Int32Array(m);
|
|
49
|
+
this.inSite = new Uint8Array(m);
|
|
50
|
+
const oc = this.outStart.slice(0, n);
|
|
51
|
+
const ic = this.inStart.slice(0, n);
|
|
52
|
+
for (let e = 0; e < m; e++) {
|
|
53
|
+
const s = edges[3 * e];
|
|
54
|
+
const d = edges[3 * e + 1];
|
|
55
|
+
const site = edges[3 * e + 2];
|
|
56
|
+
this.outDst[oc[s]] = d;
|
|
57
|
+
this.outSite[oc[s]++] = site;
|
|
58
|
+
this.inSrc[ic[d]] = s;
|
|
59
|
+
this.inSite[ic[d]++] = site;
|
|
60
|
+
}
|
|
61
|
+
meta.nodes.forEach((v, i) => {
|
|
62
|
+
this.byName.set(v.name, i);
|
|
63
|
+
for (const a of v.also ?? [])
|
|
64
|
+
this.foldedInto.set(a, i);
|
|
65
|
+
// a notation unit answers to its Lean constant too, as to what it folds
|
|
66
|
+
if (v.leanName)
|
|
67
|
+
this.foldedInto.set(v.leanName, i);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
isExt(i) {
|
|
71
|
+
return i >= this.projectCount;
|
|
72
|
+
}
|
|
73
|
+
/** `from` and what is one edge from it, along edges whose sites meet `sites`. */
|
|
74
|
+
step(from, sites, dir) {
|
|
75
|
+
const seen = new Uint8Array(this.n);
|
|
76
|
+
const start = dir === "out" ? this.outStart : this.inStart;
|
|
77
|
+
const adj = dir === "out" ? this.outDst : this.inSrc;
|
|
78
|
+
const site = dir === "out" ? this.outSite : this.inSite;
|
|
79
|
+
for (const u of from) {
|
|
80
|
+
seen[u] = 1;
|
|
81
|
+
for (let e = start[u]; e < start[u + 1]; e++)
|
|
82
|
+
if (site[e] & sites)
|
|
83
|
+
seen[adj[e]] = 1;
|
|
84
|
+
}
|
|
85
|
+
return seen;
|
|
86
|
+
}
|
|
87
|
+
/** Everything reachable from `from` (included) along edges whose sites meet `sites`. */
|
|
88
|
+
reach(from, sites, dir) {
|
|
89
|
+
const seen = new Uint8Array(this.n);
|
|
90
|
+
const stack = [];
|
|
91
|
+
for (const f of from) {
|
|
92
|
+
if (!seen[f]) {
|
|
93
|
+
seen[f] = 1;
|
|
94
|
+
stack.push(f);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const start = dir === "out" ? this.outStart : this.inStart;
|
|
98
|
+
const adj = dir === "out" ? this.outDst : this.inSrc;
|
|
99
|
+
const site = dir === "out" ? this.outSite : this.inSite;
|
|
100
|
+
while (stack.length) {
|
|
101
|
+
const u = stack.pop();
|
|
102
|
+
for (let e = start[u]; e < start[u + 1]; e++) {
|
|
103
|
+
if (!(site[e] & sites))
|
|
104
|
+
continue;
|
|
105
|
+
const v = adj[e];
|
|
106
|
+
if (!seen[v]) {
|
|
107
|
+
seen[v] = 1;
|
|
108
|
+
stack.push(v);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return seen;
|
|
113
|
+
}
|
|
114
|
+
closure(mode, focus) {
|
|
115
|
+
switch (mode) {
|
|
116
|
+
case "plain": {
|
|
117
|
+
const s = new Uint8Array(this.n);
|
|
118
|
+
for (const f of focus)
|
|
119
|
+
s[f] = 1;
|
|
120
|
+
return s;
|
|
121
|
+
}
|
|
122
|
+
case "tcb":
|
|
123
|
+
return this.reach(focus, MODE_SITES.tcb, "out");
|
|
124
|
+
case "deps":
|
|
125
|
+
return this.reach(focus, ALL_SITES, "out");
|
|
126
|
+
// Direct: what it uses directly, one step (ViewSpec decision 20, the owner's decision of 2026-10-14)
|
|
127
|
+
case "refs":
|
|
128
|
+
return this.step(focus, ALL_SITES, "out");
|
|
129
|
+
case "apps":
|
|
130
|
+
return this.reach(focus, ALL_SITES, "in");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
axiomsOf(i) {
|
|
134
|
+
const set = this.meta.axiomSets[this.nodes[i].ax] ?? [];
|
|
135
|
+
return set.map((k) => this.meta.axioms[k]);
|
|
136
|
+
}
|
|
137
|
+
/** The last name component, the way a card titles a node (`shortName`). */
|
|
138
|
+
short(i) {
|
|
139
|
+
return shortName(this.nodes[i]);
|
|
140
|
+
}
|
|
141
|
+
/** Whether a node is a notation unit that only declares syntax (TCBSpec.md, decision 2). */
|
|
142
|
+
isGrammar(i) {
|
|
143
|
+
return !!this.nodes[i].flags?.includes("grammar");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** What a card calls a node: a notation unit by its display form (`notation · I · in
|
|
147
|
+
* UnitLemmas`, RawNode.display), anything else by the last component of its name — a numeric or
|
|
148
|
+
* generated tail reads better with its parent. */
|
|
149
|
+
export function shortName(v) {
|
|
150
|
+
if (v.display)
|
|
151
|
+
return v.display;
|
|
152
|
+
const parts = v.name.split(".");
|
|
153
|
+
let last = parts[parts.length - 1];
|
|
154
|
+
if (/^\d+$/.test(last) || last.length <= 2)
|
|
155
|
+
last = parts.slice(-2).join(".");
|
|
156
|
+
return last;
|
|
157
|
+
}
|