visualproof 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +3235 -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 +741 -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 +1394 -0
- package/dist/server/mcptools.js +497 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +156 -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 +49 -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 +157 -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 +148 -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 +107 -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 +243 -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 +95 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +810 -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 +280 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +298 -0
- package/dist/shared/viewgraph.js +139 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +48733 -0
- package/dist/web/bundle/edges.js +409 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +239 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +787 -0
- package/dist/web/style.css +2312 -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 +124 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- 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
|
+
}
|