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,358 @@
1
+ // The exported document's builder (ExportSpec decisions 6, 8, 12 and 13): a pure function of the
2
+ // graph, a view, the sources and the settings, giving the model a renderer reads
3
+ // (shared/doc/model.ts). The server builds it for POST /api/export and for the agents' export
4
+ // endpoints; tests build it from a graph of their own.
5
+ //
6
+ // The view is the specification: what its board holds is what the document holds, and its board is
7
+ // the document's outline (the owner's decisions of 2026-10-12). Every declaration of the project the
8
+ // board holds is printed, whatever is folded, and what it holds from outside the project is counted
9
+ // on the head card instead; the sections are the board's cluster structure, its hidden levels taken
10
+ // out (shared/doc/sections.ts); everything comes in the board's order, the lowest first
11
+ // (shared/doc/order.ts), from where the server reads the board as the page draws it. The notes the
12
+ // view shows are the document's prose — a note attached to a declaration under it, one in a cluster
13
+ // in its section, the others at the start — with the images and clips they carry as figures and the
14
+ // whole PDFs as references. A definition is always printed in full and a proof never
15
+ // (shared/doc/proof.ts): neither a theorem's, nor one inside a definition. Nothing of the review is
16
+ // read but what the view's scope reads to say what it holds, and the trusted prefixes: no status,
17
+ // no opinion is printed. Its title is the view's, as the view is named, and its author line the
18
+ // people who left a trace on the view (ExportSpec decisions 14 and 15), whom the server finds
19
+ // (shared/doc/authors.ts) and the view brings; no setting gives either.
20
+ //
21
+ // A view of a past build makes that build's document (ExportSpec decision 16): its graph is the past
22
+ // graph its board is of (shared/pastgraph.ts), that build's declarations alone; each declaration's
23
+ // text is the one that build compiled, as the history keeps it, its proofs found by reading it (the
24
+ // usage pass reads the latest build alone); where that text is not kept, its statement then is
25
+ // printed, and the document says so under it. The head card says the
26
+ // build and its commit, and how many texts were matched to the build by their places alone.
27
+ import { excerptOf } from "../excerpt.js";
28
+ import { evaluate, printQuery } from "../query.js";
29
+ import { STANDARD_AXIOMS } from "../review.js";
30
+ import { namesOf } from "../titles.js";
31
+ import { docTitle, viewTitle } from "../views.js";
32
+ import { authorLine } from "./authors.js";
33
+ import { codeOf, noteBlocks, parseMarkdown } from "./markdown.js";
34
+ import { addedNames, clusterSubject, declSubject, fileOf, linksOf } from "../library.js";
35
+ import { DOC_VERSION } from "./model.js";
36
+ import { freshSpots, orderDoc } from "./order.js";
37
+ import { cutByText, cutProofs, statementOnly } from "./proof.js";
38
+ import { planDoc, tooMuch } from "./sections.js";
39
+ /** A view the builder cannot make a document of: it holds nothing of the project, or more than a
40
+ * document holds. The server answers it 400, with its message. */
41
+ export class DocInputError extends Error {
42
+ }
43
+ /** The kinds whose value can hold a proof the document cuts: definitions (a `module` or a `module
44
+ * type` unit among them). A structure's, a class's or an inductive type's text is its statement (a
45
+ * `by` in a field's default is data), a theorem's value is cut whole, a notation unit's expansion
46
+ * is its meaning: each is printed as written. */
47
+ const VALUE_KINDS = new Set(["def", "abbrev", "instance", "opaque"]);
48
+ /** Whether the usage pass read declaration `name` in the text `src` holds now (the compiled one):
49
+ * then its proof spans say where its proofs are, and none means none. */
50
+ function passRead(src, name) {
51
+ return !src.edited && !!src.hash && src.contexts?.hash === src.hash && Object.hasOwn(src.contexts.decls, name);
52
+ }
53
+ /** The document of a view. Throws DocInputError where the view makes none (tooMuch). */
54
+ export function buildDoc(input) {
55
+ const { g, review: d, settings: s, view } = input;
56
+ const past = input.past ?? null;
57
+ const set = evaluate(view.scope, { g, review: d });
58
+ const plan = planDoc(g, set, view.clustering, view.hidden);
59
+ const why = tooMuch(plan.count);
60
+ if (why)
61
+ throw new DocInputError(why);
62
+ // the board's order: what it draws lowest first
63
+ const board = input.board ?? freshSpots(g, set, view.clustering, view.hidden);
64
+ const order = orderDoc(plan, g, board);
65
+ const sources = new Map();
66
+ const sourceOf = (module) => {
67
+ if (!sources.has(module))
68
+ sources.set(module, input.source(module));
69
+ return sources.get(module);
70
+ };
71
+ /** The files the papers, the notes and their figures cite (ExportSpec decision 17), numbered as
72
+ * they are first cited: the entries are made in the order the document prints them. */
73
+ const references = [];
74
+ const refOf = new Map();
75
+ const cite = (f) => {
76
+ let n = refOf.get(f.sha);
77
+ if (n === undefined) {
78
+ n = references.length + 1;
79
+ refOf.set(f.sha, n);
80
+ references.push({ n, sha: f.sha, title: f.title, type: f.type, ...(f.pages ? { pages: f.pages } : {}), ...(f.url ? { url: f.url } : {}), ...(f.name ? { name: f.name } : {}) });
81
+ }
82
+ return n;
83
+ };
84
+ /** The places in papers a subject is linked to, each citing its file. */
85
+ const papersOf = (subject) => {
86
+ const out = linksOf(d, subject).flatMap((l) => {
87
+ const f = fileOf(d, l.file);
88
+ return f ? [{ label: l.label, ...(l.page ? { page: l.page } : {}), ref: cite(f) }] : [];
89
+ });
90
+ return out.length ? out : undefined;
91
+ };
92
+ // the declarations the document prints, by name, and the name a note may write for one: its own,
93
+ // or one Lean generated from its command (folded into it)
94
+ const printedNames = new Set();
95
+ for (const p of [plan.decls, ...sectionDecls(plan.sections)])
96
+ for (const i of p)
97
+ printedNames.add(g.nodes[i].name);
98
+ const mention = (name) => {
99
+ if (printedNames.has(name))
100
+ return name;
101
+ const unit = g.foldedInto.get(name);
102
+ return unit !== undefined && printedNames.has(g.nodes[unit].name) ? g.nodes[unit].name : null;
103
+ };
104
+ const files = input.files;
105
+ /** A note as the document prints it: its text, unless it says only the title of the one file it
106
+ * carries; the images and clips it carries as figures, where they can be drawn; the whole PDFs,
107
+ * and what cannot be drawn, as references. A file the library has no record of is left out. */
108
+ const noteEntry = (b) => {
109
+ const n = b.note;
110
+ const carries = n.carries ?? [];
111
+ const only = carries.length === 1 ? fileOf(d, carries[0].file) : null;
112
+ const text = only && saysTitle(n.text, only) ? null : noteBlocks(n.text, mention);
113
+ const figures = [];
114
+ const papers = [];
115
+ for (const c of carries) {
116
+ const f = fileOf(d, c.file);
117
+ if (!f)
118
+ continue;
119
+ const ref = cite(f);
120
+ const here = files?.has(f.sha) ?? true;
121
+ if (here && f.type === "pdf" && c.region && c.page) {
122
+ const p = files?.pageSize(f.sha, c.page) ?? null;
123
+ const [x0, y0, x1, y1] = c.region;
124
+ figures.push({ ref, file: f.sha, page: c.page, region: [x0, y0, x1, y1], ...(p ? { size: [round1((x1 - x0) * p.w), round1((y1 - y0) * p.h)] } : {}) });
125
+ }
126
+ else if (here && f.type !== "pdf" && (files?.drawable(f.sha, f.type) ?? true))
127
+ figures.push({ ref, file: f.sha });
128
+ else
129
+ papers.push({ label: "", ...(c.page ? { page: c.page } : {}), ref });
130
+ }
131
+ return { id: `note:${n.id}`, kind: b.kind, text, ...(figures.length ? { figures } : {}), ...(papers.length ? { papers } : {}) };
132
+ };
133
+ /** A declaration's entry: what it is (its docstring), its code, the papers it is linked to. */
134
+ const entry = (i) => {
135
+ const v = g.nodes[i];
136
+ const doc = v.doc?.trim() ? parseMarkdown(v.doc) : null;
137
+ // the docstring shown as prose is not repeated in the code
138
+ const code = codeFor(i, doc !== null);
139
+ const papers = papersOf(declSubject(v.name));
140
+ return { id: v.name, name: v.name, short: g.short(i), kind: v.kind, module: v.module, file: input.fileOf(v.module), line: v.range ? v.range[0] : null, doc, code, ...(papers ? { papers } : {}) };
141
+ };
142
+ /** A declaration's code: its source where it can be read (a theorem's up to its statement, a
143
+ * definition's whole with the proofs in it cut), else the statement the graph holds. `dropDoc`:
144
+ * the docstring it starts with is shown as prose, so the code leaves it out. A definition's
145
+ * proofs are cut where the usage pass found them when it read this very text; elsewhere (its file
146
+ * changed since it was compiled, or the pass has not read it) where reading the text finds them,
147
+ * which leaves out every `by` block, data too, and keeps a proof written as a term. Its value is
148
+ * printed either way: the reader sees what it is, whatever the pass knows. Of a past build, the
149
+ * text it compiled, its proofs found by reading it; where that text is not kept, its statement
150
+ * then, marked so, and never the latest build's text in its place. */
151
+ const codeFor = (i, dropDoc) => {
152
+ const v = g.nodes[i];
153
+ const theorem = v.kind === "theorem";
154
+ const fromGraph = () => {
155
+ // a notation unit's statement is its command's text
156
+ const head = g.isExt(i) ? v.name : `${v.kind} ${v.name}`;
157
+ const text = v.kind === "notation" ? v.type ?? v.name : v.type ? `${head} : ${v.type}` : head;
158
+ const c = codeOf(text, "statement");
159
+ if (theorem)
160
+ elide(c);
161
+ return c;
162
+ };
163
+ const src = g.isExt(i) || !v.range ? null : sourceOf(v.module);
164
+ const at = src ? excerptOf(src.lines, src.edited, v.name, v.range) : null;
165
+ // a past build's text that is not kept (HistorySpec decision 21): its statement then, said so
166
+ if (!src && past && !g.isExt(i) && v.range)
167
+ return { ...fromGraph(), notKept: true };
168
+ if (!src || !at)
169
+ return fromGraph();
170
+ // the docstring above it, shown as prose already
171
+ const start = dropDoc ? afterDoc(src.lines, at.start, at.end) : at.start;
172
+ const text = src.lines.slice(start, at.end + 1).join("\n");
173
+ const from = src.edited ? "edited" : "source";
174
+ // a past build's text matched to it by its places: the head card counts them
175
+ const placed = (c) => (src.check === "places" ? { ...c, places: true } : c);
176
+ if (theorem) {
177
+ const st = statementOnly(text);
178
+ // where the statement's end cannot be found, the graph's statement: never the proof
179
+ if (st === null)
180
+ return fromGraph();
181
+ const code = codeOf(st, from);
182
+ elide(code);
183
+ return placed(code);
184
+ }
185
+ if (!VALUE_KINDS.has(v.kind))
186
+ return placed(codeOf(text, from));
187
+ if (passRead(src, v.name)) {
188
+ // the proofs inside a definition, where the usage pass found them in this text
189
+ const cut = v.proofs?.length ? cutProofs(src.lines, start, at.end, v.proofs) : null;
190
+ return cut?.cuts ? { lines: cut.lines, proofElided: false, from: "source", proofsCut: cut.cuts } : codeOf(text, from);
191
+ }
192
+ // the pass's places are not this text's (a past build's never are): the proofs as reading the
193
+ // text finds them
194
+ const cut = cutByText(text);
195
+ return placed({ lines: cut.lines, proofElided: false, from, ...(cut.cuts ? { proofsCut: cut.cuts } : {}), textCut: past ? "past" : src.edited ? "edited" : "unread" });
196
+ };
197
+ // a declaration, then the notes attached to it, as its explanation
198
+ const declItem = (i) => {
199
+ const e = entry(i);
200
+ const ks = order.attached.get(i);
201
+ if (ks)
202
+ e.notes = ks.map((k) => noteEntry(board.notes[k]));
203
+ return e;
204
+ };
205
+ const items = (ps) => ps.map((p) => (p.t === "decl" ? { t: "decl", decl: declItem(p.i) } : p.t === "note" ? { t: "note", note: noteEntry(board.notes[p.k]) } : { t: "section", section: section(p.s) }));
206
+ // a section's papers are its cluster's, printed under its heading, before what it holds
207
+ const section = (o) => {
208
+ const p = o.plan;
209
+ const papers = papersOf(clusterSubject(p.key, view.clustering));
210
+ return { id: `section:${p.key}`, key: p.key, title: p.title, level: p.level, ...(papers ? { papers } : {}), items: items(o.items) };
211
+ };
212
+ const meta = g.meta;
213
+ // the names a title made from the scope tells apart: the graph on show's, as the view's title
214
+ // everywhere, else the graph's (shared/titles.ts)
215
+ const titles = input.titles ?? { names: namesOf(g) };
216
+ const body = items(order.items);
217
+ const printed = printedOf(body);
218
+ return {
219
+ version: DOC_VERSION,
220
+ settings: s,
221
+ view: { id: view.id, title: viewTitle(view, titles), scope: printQuery(view.scope), clustering: view.clustering, hidden: [...view.hidden] },
222
+ head: {
223
+ // the view's title and its people (ExportSpec decisions 14 and 15): no setting gives either
224
+ title: docTitle(view, titles),
225
+ author: authorLine(view.authors),
226
+ project: meta.project.name,
227
+ // the libraries, not the thousands of modules one may stand for
228
+ roots: meta.project.targets ?? meta.project.roots,
229
+ fingerprint: input.build.fingerprint,
230
+ commit: input.build.commit,
231
+ toolchain: input.build.toolchain ?? meta.project.toolchain,
232
+ past: past
233
+ ? {
234
+ snapshot: past.snapshot,
235
+ at: past.at,
236
+ when: past.when,
237
+ ...(past.approx ? { approx: true } : {}),
238
+ places: printed.filter((e) => e.code.places).length,
239
+ notKept: printed.filter((e) => e.code.notKept).length,
240
+ }
241
+ : null,
242
+ trusted: { prefixes: [...d.trusted], standardAxioms: [...STANDARD_AXIOMS] },
243
+ outside: plan.outside,
244
+ },
245
+ items: body,
246
+ count: { declarations: plan.count, sections: plan.sectionCount, notes: printedNotes({ items: body }).length },
247
+ references,
248
+ };
249
+ }
250
+ /** The declarations of the plan's sections, each section's in turn. */
251
+ function sectionDecls(ss) {
252
+ return ss.flatMap((s) => [s.decls, ...sectionDecls(s.sections)]);
253
+ }
254
+ /** A number to a tenth: a size in points, as the model keeps it. */
255
+ const round1 = (v) => Math.round(v * 10) / 10;
256
+ /** Whether a note's text says only the title of file `f`, the one it carries — its title, or the name
257
+ * it was added under, with or without its extension, as a note made for a file says it (the owner's
258
+ * decisions of 2026-10-12: such a note prints as what it carries, not as prose repeating the title). */
259
+ function saysTitle(text, f) {
260
+ const plain = (t) => t.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
261
+ const said = plain(text);
262
+ return [f.title, ...addedNames(f)].some((t) => plain(t) === said);
263
+ }
264
+ /** Every declaration a document prints, in the order it prints them. */
265
+ export function printedDecls(doc) {
266
+ return printedOf(doc.items);
267
+ }
268
+ function printedOf(items) {
269
+ const all = [];
270
+ const walk = (xs) => {
271
+ for (const x of xs) {
272
+ if (x.t === "decl")
273
+ all.push(x.decl);
274
+ else if (x.t === "section")
275
+ walk(x.section.items);
276
+ }
277
+ };
278
+ walk(items);
279
+ return all;
280
+ }
281
+ /** Every note a document prints, in the order it prints them: those attached to a declaration after
282
+ * it. */
283
+ export function printedNotes(doc) {
284
+ const all = [];
285
+ const walk = (xs) => {
286
+ for (const x of xs) {
287
+ if (x.t === "note")
288
+ all.push(x.note);
289
+ else if (x.t === "decl")
290
+ all.push(...(x.decl.notes ?? []));
291
+ else
292
+ walk(x.section.items);
293
+ }
294
+ };
295
+ walk(doc.items);
296
+ return all;
297
+ }
298
+ /** the most names a note lists */
299
+ const NOTE_NAMES = 20;
300
+ /** What an export says about how its document was made, besides the document (ExportSpec decision
301
+ * 8): the definitions whose proofs were found by reading their text rather than where the usage
302
+ * pass found them, and why; the declarations of the project printed from the graph's statement
303
+ * rather than their source. The document itself says none of it; the export's job carries it, for
304
+ * whoever wants to know. Of a past build (decision 16), which declarations' texts were matched to
305
+ * it by their places, and which are not kept, by name: the document counts the one on its head
306
+ * card and says the other under each. Empty where there is nothing to say. */
307
+ export function docNotes(doc) {
308
+ const all = printedDecls(doc);
309
+ const names = (es) => {
310
+ const shown = es.slice(0, NOTE_NAMES).map((e) => e.name).join(", ");
311
+ return es.length > NOTE_NAMES ? `${shown} and ${es.length - NOTE_NAMES} more` : shown;
312
+ };
313
+ const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
314
+ const out = [];
315
+ const why = { edited: "their file changed since it was compiled", unread: "the usage pass has not read them (it is off, still running, or failed on their module)" };
316
+ const reading = "Reading the text leaves out every `by` block, one that builds data too, and keeps a proof written as a term.";
317
+ for (const k of ["edited", "unread"]) {
318
+ const es = all.filter((e) => e.code.textCut === k);
319
+ if (es.length)
320
+ out.push(`The proofs inside ${count(es.length, "definition", "definitions")} were found by reading the text rather than where the usage pass found them, since ${why[k]}: ${names(es)}. ${reading}`);
321
+ }
322
+ // of a past build every definition is read so: counted, not named
323
+ const past = all.filter((e) => e.code.textCut === "past").length;
324
+ if (past)
325
+ out.push(`The proofs inside ${past === 1 ? "the definition" : `the ${past} definitions`} printed were found by reading the text, since it is a past build's and the usage pass reads the latest build alone. ${reading}`);
326
+ const placed = all.filter((e) => e.code.places);
327
+ if (placed.length)
328
+ out.push(`The text of ${count(placed.length, "declaration", "declarations")} of this build was matched to it by the places the build recorded — its imports, where each declaration starts and ends, its doc comments and names — since its hash was not kept: ${names(placed)}.`);
329
+ const gone = all.filter((e) => e.code.notKept);
330
+ if (gone.length)
331
+ out.push(`The text of ${count(gone.length, "declaration", "declarations")} at this build is not kept, and ${gone.length === 1 ? "its statement is" : "their statements are"} printed instead: ${names(gone)}.`);
332
+ const bare = all.filter((e) => e.code.from === "statement" && !e.code.notKept);
333
+ if (bare.length)
334
+ out.push(`${count(bare.length, "declaration of the project is", "declarations of the project are")} printed from the graph's statement rather than from the source: ${names(bare)}.`);
335
+ return out;
336
+ }
337
+ /** A theorem's proof left out: `:= …` after the statement's last line. */
338
+ function elide(c) {
339
+ if (!c.lines.length)
340
+ c.lines.push([]);
341
+ c.lines[c.lines.length - 1].push(["text", " "], ["elided", ":= …"]);
342
+ c.proofElided = true;
343
+ }
344
+ /** Where a declaration's text starts once the docstring it opens with is left out: after the
345
+ * line the docstring closes on, if the excerpt from `start` opens with one and nothing else is
346
+ * written on that line (`/-- … -/ def f …` keeps it); else `start`. */
347
+ function afterDoc(lines, start, end) {
348
+ if (!/^\s*\/--/.test(lines[start] ?? ""))
349
+ return start;
350
+ for (let k = start; k <= end; k++) {
351
+ const close = lines[k].indexOf("-/", k === start ? lines[k].indexOf("/--") + 3 : 0);
352
+ if (close < 0)
353
+ continue;
354
+ // something written after it on its line, or nothing after it at all: the docstring stays
355
+ return lines[k].slice(close + 2).trim() || k === end ? start : k + 1;
356
+ }
357
+ return start;
358
+ }