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,64 @@
|
|
|
1
|
+
// "Open in editor": a link from a declaration to its place in an editor, made from a URL template.
|
|
2
|
+
// The server can be started with one (`--editor`), and a reader can set their own in the page (the
|
|
3
|
+
// ? sheet), because what opens an editor depends on the machine the page is read on: VS Code on the
|
|
4
|
+
// server itself (`vscode://file/{path}:{line}:{col}`), VS Code on a laptop reaching the server over
|
|
5
|
+
// SSH (`vscode://vscode-remote/ssh-remote+HOST{path}:{line}`), or a web view of the repository
|
|
6
|
+
// (`https://github.com/…/blob/main/{rel}#L{line}`).
|
|
7
|
+
//
|
|
8
|
+
// Placeholders: {path} is the file's absolute path on the server, {rel} its path in the project,
|
|
9
|
+
// {line} and {col} where the declaration starts, both counted from 1. The paths are percent-encoded
|
|
10
|
+
// where a URL needs it; the rest of the template is the owner's, and is left as written.
|
|
11
|
+
export const PLACEHOLDERS = ["path", "rel", "line", "col"];
|
|
12
|
+
/** Schemes that do something inside the page rather than open something outside it: a template
|
|
13
|
+
* with one would run whatever it says the moment a reader clicks the link. */
|
|
14
|
+
const REFUSED = new Set(["javascript", "data", "vbscript", "blob", "about"]);
|
|
15
|
+
/** A place to try a template on, so that one which cannot make a URL is refused before anyone
|
|
16
|
+
* clicks it. */
|
|
17
|
+
const SAMPLE = { path: "/home/a b/Project/A#1.lean", rel: "A#1.lean", line: 12, col: 3 };
|
|
18
|
+
/** Why `t` cannot be used as a template, or null when it can. */
|
|
19
|
+
export function invalidTemplate(t) {
|
|
20
|
+
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(t);
|
|
21
|
+
if (!scheme)
|
|
22
|
+
return "Not a URL: it has to start with a scheme, like vscode:// or https://.";
|
|
23
|
+
if (REFUSED.has(scheme[1].toLowerCase()))
|
|
24
|
+
return `A ${scheme[1]}: link runs inside the page; it cannot open an editor.`;
|
|
25
|
+
if (/[\s\x00-\x1f\x7f]/.test(t))
|
|
26
|
+
return "A URL has no spaces in it; write a space as %20.";
|
|
27
|
+
for (const [, name] of t.matchAll(/\{([^{}]*)\}/g))
|
|
28
|
+
if (!PLACEHOLDERS.includes(name))
|
|
29
|
+
return `There is no {${name}}: the placeholders are {path}, {rel}, {line} and {col}.`;
|
|
30
|
+
if (!/\{(?:path|rel)\}/.test(t))
|
|
31
|
+
return "It names no file: put {path} (the file on the server) or {rel} (the file in the project) in it.";
|
|
32
|
+
if (expand(t, SAMPLE) === null)
|
|
33
|
+
return "This does not make a URL once the placeholders are filled in.";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** A path as it may stand in a URL: percent-encoded, except for the slashes and the characters a
|
|
37
|
+
* path may hold as they are. `?` and `#` would end the path, so they are encoded too. */
|
|
38
|
+
function encodePath(p) {
|
|
39
|
+
return encodeURI(p).replace(/[?#]/g, (c) => encodeURIComponent(c));
|
|
40
|
+
}
|
|
41
|
+
function expand(t, p) {
|
|
42
|
+
try {
|
|
43
|
+
const out = t.replace(/\{(path|rel|line|col)\}/g, (_, name, at) => {
|
|
44
|
+
if (name === "line" || name === "col")
|
|
45
|
+
return String(Math.max(1, Math.trunc(name === "line" ? p.line : p.col) || 1));
|
|
46
|
+
const s = encodePath(name === "path" ? p.path : p.rel);
|
|
47
|
+
// `vscode://file/{path}` with an absolute path: one slash, not two, since `file//data/…`
|
|
48
|
+
// would name a host "data". A double slash (`file://{path}`) is the URL's own and stays.
|
|
49
|
+
if (name === "path" && s.startsWith("/") && t[at - 1] === "/" && t[at - 2] !== "/")
|
|
50
|
+
return s.slice(1);
|
|
51
|
+
return s;
|
|
52
|
+
});
|
|
53
|
+
new URL(out);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// a path with half a character in it (encodeURI throws), or no URL at all
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The link to `place` that template `t` makes; null when the template cannot be used. */
|
|
62
|
+
export function editorLink(t, place) {
|
|
63
|
+
return invalidTemplate(t) ? null : expand(t, place);
|
|
64
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Which lines of a file are a declaration's source. When the file is the one the graph was read
|
|
2
|
+
// from, its compiled place says so exactly (with the docstring and attributes above it). When the
|
|
3
|
+
// file changed since, that place means nothing any more: the declaration is looked for by name in
|
|
4
|
+
// the present text, and the reader is told it is not what was compiled.
|
|
5
|
+
const DECL = /^\s*(?:@\[[^\]]*\]\s*)*(?:(?:private|protected|noncomputable|partial|unsafe|nonrec|scoped|local)\s+)*(?:theorem|lemma|def|abbrev|instance|structure|class|inductive|axiom|opaque|example|irreducible_def)\s+(\S+)/;
|
|
6
|
+
/** a line that starts what comes after a declaration */
|
|
7
|
+
const NEXT = /^(?:@\[|\/--|\/-!|--|#|(?:(?:private|protected|noncomputable|partial|unsafe|nonrec|scoped|local)\s+)*(?:theorem|lemma|def|abbrev|instance|structure|class|inductive|axiom|opaque|example|irreducible_def|namespace|section|end|open|variable|universe|attribute|set_option|mutual|macro|syntax|notation|infix|infixl|infixr|prefix|postfix|elab|deriving)\b)/;
|
|
8
|
+
/** Pull what belongs to the declaration from directly above `line`: attribute lines, line
|
|
9
|
+
* comments, and a whole docstring — never across a blank line, and never a line that has more on it
|
|
10
|
+
* than attributes (`@[simp] theorem id_apply … := rfl` is the declaration before, not part of this
|
|
11
|
+
* one; a blank line and that once let declarations show their neighbour's text, and record it as
|
|
12
|
+
* what a reviewer judged). Lean's own range starts at the docstring already. */
|
|
13
|
+
function withPreamble(lines, line) {
|
|
14
|
+
let start = line;
|
|
15
|
+
while (start > 0 && line - start < 40) {
|
|
16
|
+
const above = lines[start - 1];
|
|
17
|
+
if (/^\s*@\[[^\]]*\]\s*$/.test(above) || /^\s*--/.test(above)) {
|
|
18
|
+
start--;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
// a docstring that ends right above: the whole of it, from its `/--`
|
|
22
|
+
if (/-\/\s*$/.test(above)) {
|
|
23
|
+
let k = start - 1;
|
|
24
|
+
while (k > 0 && line - k < 60 && !/^\s*\/-/.test(lines[k]))
|
|
25
|
+
k--;
|
|
26
|
+
if (/^\s*\/--/.test(lines[k])) {
|
|
27
|
+
start = k;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return start;
|
|
34
|
+
}
|
|
35
|
+
/** The excerpt at the compiled place `range` = [line, column, endLine], 1-based lines. */
|
|
36
|
+
export function compiledExcerpt(lines, range) {
|
|
37
|
+
const l0 = Math.max(0, range[0] - 1);
|
|
38
|
+
const l1 = Math.min(lines.length - 1, Math.max(l0, range[2] - 1));
|
|
39
|
+
return { start: withPreamble(lines, l0), end: l1 };
|
|
40
|
+
}
|
|
41
|
+
/** Where declaration `name` is in its file's `lines`: at its compiled place `range` when the file
|
|
42
|
+
* is the text that was compiled, else found by name; null when it is not there any more. */
|
|
43
|
+
export function excerptOf(lines, edited, name, range) {
|
|
44
|
+
return edited ? findExcerpt(lines, name, range[0]) : compiledExcerpt(lines, range);
|
|
45
|
+
}
|
|
46
|
+
/** A declaration's text in its file, as the source window shows it (with its context above it,
|
|
47
|
+
* shared/context.ts, it is what an opinion records). `edited`: the file is not the text that
|
|
48
|
+
* was compiled. */
|
|
49
|
+
export function declarationText(lines, edited, name, range) {
|
|
50
|
+
const at = excerptOf(lines, edited, name, range);
|
|
51
|
+
return at && { text: lines.slice(at.start, at.end + 1).join("\n"), edited };
|
|
52
|
+
}
|
|
53
|
+
/** The declaration `name` looked for in `lines`, nearest `near` (a 1-based line) when several
|
|
54
|
+
* match; null when it is not there. */
|
|
55
|
+
export function findExcerpt(lines, name, near) {
|
|
56
|
+
const parts = name.split(".");
|
|
57
|
+
const short = parts[parts.length - 1].replace(/^«|»$/g, "");
|
|
58
|
+
const tail2 = parts.slice(-2).join(".");
|
|
59
|
+
let best = -1;
|
|
60
|
+
let bestScore = Infinity;
|
|
61
|
+
for (let i = 0; i < lines.length; i++) {
|
|
62
|
+
const m = DECL.exec(lines[i]);
|
|
63
|
+
if (!m)
|
|
64
|
+
continue;
|
|
65
|
+
const declared = m[1].replace(/[«»]/g, "");
|
|
66
|
+
// declared as `short` inside its namespace, or with part of its namespace in front
|
|
67
|
+
if (declared !== short && declared !== tail2.replace(/[«»]/g, "") && declared !== name && !name.endsWith("." + declared))
|
|
68
|
+
continue;
|
|
69
|
+
const score = near ? Math.abs(i + 1 - near) : i;
|
|
70
|
+
if (score < bestScore) {
|
|
71
|
+
bestScore = score;
|
|
72
|
+
best = i;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (best < 0)
|
|
76
|
+
return null;
|
|
77
|
+
let end = best;
|
|
78
|
+
for (let i = best + 1; i < lines.length && i < best + 400; i++) {
|
|
79
|
+
if (NEXT.test(lines[i]))
|
|
80
|
+
break;
|
|
81
|
+
end = i;
|
|
82
|
+
}
|
|
83
|
+
// trailing blank lines are not the declaration
|
|
84
|
+
while (end > best && !lines[end].trim())
|
|
85
|
+
end--;
|
|
86
|
+
return { start: withPreamble(lines, best), end };
|
|
87
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The data that crosses process boundaries: what the Lean extractor writes, and what the
|
|
2
|
+
// server hands the page. Kept free of runtime code so both sides can import it.
|
|
3
|
+
/** Edge sites, as bits. A reference "in a proof" sits in a subterm whose type is a Prop. A
|
|
4
|
+
* reference "on grammar" is a statement or a body written with a syntax that only fixes the
|
|
5
|
+
* grammar (a notation unit flagged `grammar`: `syntax`, `declare_syntax_cat`), or a rule's
|
|
6
|
+
* dependency on the syntax it interprets: what the tree means is a rule's business, which a use
|
|
7
|
+
* reaches on its own, so a TCB follows neither proofs nor grammar (TCBSpec.md, decision 2). */
|
|
8
|
+
export const SITE_STMT = 1;
|
|
9
|
+
export const SITE_BODY = 2;
|
|
10
|
+
export const SITE_PROOF = 4;
|
|
11
|
+
export const SITE_GRAMMAR = 8;
|
|
12
|
+
/** The sites a TCB follows. */
|
|
13
|
+
export const SITE_TCB = SITE_STMT | SITE_BODY;
|
|
14
|
+
/** Which way of laying out produced a layout. Places remembered under another version (the
|
|
15
|
+
* server's layout memory, places dropped in a browser) do not carry over. 2: dependency order. */
|
|
16
|
+
export const LAYOUT_VERSION = 2;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// A view's items (the owner's decision of 2026-10-11, AttachmentSpec decision 17): items belong to
|
|
2
|
+
// views as declarations do. An item is a note of the project — among them the notes that carry the
|
|
3
|
+
// library's files — and the Project holds every one. Any other view shows an item where it follows
|
|
4
|
+
// from the view's scope (shared/noteplace.ts, `derivedWhy`: the declaration it is attached to on the
|
|
5
|
+
// board — a reader's visitor counts, and a board of a past build reads it by that build's name (the
|
|
6
|
+
// owner's decision of 2026-10-12) —, its cluster on the board, its cluster under the other clustering
|
|
7
|
+
// on the board for a note on the ring, or a file it carries linked to what the board holds), or where
|
|
8
|
+
// it was added to the view, unless it was taken out of it.
|
|
9
|
+
//
|
|
10
|
+
// What was added and what was taken out are two lists the view keeps beside its scope, never in
|
|
11
|
+
// the scope's text (ViewSpec decision 25: titles stay short): `items: {add, remove}` in its record.
|
|
12
|
+
// They are changed by a view operation, `item`, that puts a note in one list or in neither, shared
|
|
13
|
+
// live as pins are and recorded by the timeline as a scope change is. The page and the agents' API
|
|
14
|
+
// decide which list from what the scope gives (`itemOp`), as the canvas's menu writes a scope that
|
|
15
|
+
// stays short: adding back what was taken out only takes it off the list that took it out, where
|
|
16
|
+
// the scope gives it already, and taking out what was added only takes it off the list that added
|
|
17
|
+
// it, where the scope does not give it. A note deleted, or made a view's own, leaves every list.
|
|
18
|
+
import { MAX_PROJECT_NOTES } from "./projectnotes.js";
|
|
19
|
+
import { validNoteId } from "./sticky.js";
|
|
20
|
+
/** the most notes a list holds: every note of the project */
|
|
21
|
+
export const MAX_ITEMS = MAX_PROJECT_NOTES;
|
|
22
|
+
/** Which of view `items`'s lists holds note `id`, or null. */
|
|
23
|
+
export function itemIn(items, id) {
|
|
24
|
+
if (!items)
|
|
25
|
+
return null;
|
|
26
|
+
return items.add.includes(id) ? "add" : items.remove.includes(id) ? "remove" : null;
|
|
27
|
+
}
|
|
28
|
+
/** Why an item operation cannot be taken, or null. */
|
|
29
|
+
export function invalidItemOp(o) {
|
|
30
|
+
if (!validNoteId(o.note))
|
|
31
|
+
return "note";
|
|
32
|
+
return o.in === null || o.in === "add" || o.in === "remove" ? null : "in";
|
|
33
|
+
}
|
|
34
|
+
/** Apply an item operation to a view's record in place; true when anything changed. A list that
|
|
35
|
+
* is full takes no more, and a record whose lists are both empty keeps none. */
|
|
36
|
+
export function applyItemOp(v, o) {
|
|
37
|
+
const was = itemIn(v.items, o.note);
|
|
38
|
+
if (was === o.in)
|
|
39
|
+
return false;
|
|
40
|
+
const items = v.items ?? { add: [], remove: [] };
|
|
41
|
+
if (o.in && items[o.in].length >= MAX_ITEMS)
|
|
42
|
+
return false;
|
|
43
|
+
if (was)
|
|
44
|
+
items[was] = items[was].filter((id) => id !== o.note);
|
|
45
|
+
if (o.in)
|
|
46
|
+
items[o.in].push(o.note);
|
|
47
|
+
if (items.add.length || items.remove.length)
|
|
48
|
+
v.items = items;
|
|
49
|
+
else
|
|
50
|
+
delete v.items;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** A view's lists as a file holds them, checked: an id that cannot be one is left out, as is one
|
|
54
|
+
* named twice; none where both are empty. */
|
|
55
|
+
export function cleanItems(raw) {
|
|
56
|
+
const r = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const list = (x) => {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const id of Array.isArray(x) ? x : []) {
|
|
61
|
+
if (!validNoteId(id) || seen.has(id) || out.length >= MAX_ITEMS)
|
|
62
|
+
continue;
|
|
63
|
+
seen.add(id);
|
|
64
|
+
out.push(id);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
};
|
|
68
|
+
const add = list(r.add);
|
|
69
|
+
const remove = list(r.remove);
|
|
70
|
+
return add.length || remove.length ? { items: { add, remove } } : {};
|
|
71
|
+
}
|
|
72
|
+
/** The operation that adds note `note` to a view (Add to view ▸, `to` "add") or takes it out of the
|
|
73
|
+
* view (Remove from this view, `to` "remove"), `derived` saying whether the view's scope gives it:
|
|
74
|
+
* added, it is in the view's add list only where the scope does not give it, and taken off its
|
|
75
|
+
* remove list either way; taken out, it is on the remove list only where the scope gives it, and
|
|
76
|
+
* off its add list either way. Null where the view is as asked already. */
|
|
77
|
+
export function itemOp(items, note, to, derived) {
|
|
78
|
+
const want = to === "add" ? (derived ? null : "add") : derived ? "remove" : null;
|
|
79
|
+
return itemIn(items, note) === want ? null : { op: "item", note, in: want };
|
|
80
|
+
}
|
|
81
|
+
/** Whether a view shows a note after an item operation changed it from `was`: what the timeline
|
|
82
|
+
* says of it — added (put on the add list, or taken off the remove list) or taken out. */
|
|
83
|
+
export const itemShown = (was, o) => o.in === "add" || (o.in === null && was === "remove");
|
|
84
|
+
/** The operations that take note `id` off every list of `views` that names it (a note deleted, or
|
|
85
|
+
* made a view's own), by view. */
|
|
86
|
+
export function dropItem(views, id) {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const v of views)
|
|
89
|
+
if (itemIn(v.items, id))
|
|
90
|
+
out.push({ view: v.id, op: { op: "item", note: id, in: null } });
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Key declarations (ExportSpec decisions 1, 3 and 4): the members of a target's review that matter
|
|
2
|
+
// for understanding it — the central definitions, the structures they are built on, the main theorem —
|
|
3
|
+
// marked by people, or by agents at level full, per target; the target itself is always key. What each key declaration rests
|
|
4
|
+
// on beyond itself (its support), and how the key declarations rest on one another (the key graph),
|
|
5
|
+
// are pure functions of the graph, the review and the target's review set, over the same set and
|
|
6
|
+
// edges the review queue walks (shared/reviewqueue.ts): the page, the agents' API and the exported
|
|
7
|
+
// document (shared/doc/) all read them here.
|
|
8
|
+
//
|
|
9
|
+
// A member x that is not key is *supported by* a key declaration k when x is reachable from k
|
|
10
|
+
// through members that are not key, along statement and body references. The key graph has an edge
|
|
11
|
+
// from k₁ to k₂ when k₂ is reachable from k₁ that way. Two refinements keep "every member is
|
|
12
|
+
// supported by some key declaration" true where the review set is more than what the target
|
|
13
|
+
// reaches through its members:
|
|
14
|
+
//
|
|
15
|
+
// - a declaration of the project in the target's TCB that is trusted, by module or by name (a layer
|
|
16
|
+
// reviewed elsewhere), is not a member, but what it uses may be: the walk passes through it as
|
|
17
|
+
// through a member that is not key (the outside world never uses the project, so nothing else
|
|
18
|
+
// outside the set can lead back into it);
|
|
19
|
+
// - a non-standard axiom the target rests on only through a proof is a member that no statement or
|
|
20
|
+
// body reaches: it is the target's support, as `viaProof` says.
|
|
21
|
+
//
|
|
22
|
+
// A review holds many members and a target a dozen key declarations: after one walk over the
|
|
23
|
+
// target's TCB, each walk reads the edges of the members and of what it passes through, never the
|
|
24
|
+
// whole graph's (the showcase's `Dim24.MainTheorem`: its TCB of 226 in 72 ms, its keys in 1 ms).
|
|
25
|
+
import { keyMarks, statusOf } from "./review.js";
|
|
26
|
+
import { TCB_SITES } from "./reviewqueue.js";
|
|
27
|
+
/** The key declarations of `root` among the members of its review (`order`, the queue's order —
|
|
28
|
+
* shared/reviewqueue.ts), in that order: the target, and each member marked key. A mark on
|
|
29
|
+
* a declaration that has left the review (now trusted, or no longer in the TCB) is kept in the
|
|
30
|
+
* review but counts for nothing until it is back. */
|
|
31
|
+
export function keySet(g, d, root, order) {
|
|
32
|
+
const marked = new Set(keyMarks(d, g.nodes[root].name));
|
|
33
|
+
return order.filter((i) => i === root || marked.has(g.nodes[i].name));
|
|
34
|
+
}
|
|
35
|
+
/** `root`'s key graph over its review: `order` is the review's members in review order (the
|
|
36
|
+
* queue's `order`). */
|
|
37
|
+
export function keyGraph(g, d, root, order) {
|
|
38
|
+
const keys = keySet(g, d, root, order);
|
|
39
|
+
// the target is always key, also where the review does not hold it (a test, a graph read again)
|
|
40
|
+
if (!keys.includes(root))
|
|
41
|
+
keys.push(root);
|
|
42
|
+
const keyAt = new Map();
|
|
43
|
+
keys.forEach((k, j) => keyAt.set(k, j));
|
|
44
|
+
const position = new Map();
|
|
45
|
+
order.forEach((i, k) => position.set(i, k));
|
|
46
|
+
const member = (v) => position.has(v);
|
|
47
|
+
// what a walk may pass through besides members: the project's own declarations in the target's
|
|
48
|
+
// TCB that are not members, which only a trusted prefix keeps out of the review
|
|
49
|
+
const tcb = g.reach([root], TCB_SITES, "out");
|
|
50
|
+
const passable = (v) => member(v) || (tcb[v] === 1 && !g.isExt(v));
|
|
51
|
+
const supporters = new Map();
|
|
52
|
+
const support = keys.map(() => []);
|
|
53
|
+
const edges = new Set();
|
|
54
|
+
const K = keys.length;
|
|
55
|
+
const seen = new Int32Array(g.n).fill(-1);
|
|
56
|
+
for (let kj = 0; kj < K; kj++) {
|
|
57
|
+
const k = keys[kj];
|
|
58
|
+
seen[k] = kj;
|
|
59
|
+
const stack = [k];
|
|
60
|
+
while (stack.length) {
|
|
61
|
+
const u = stack.pop();
|
|
62
|
+
for (let e = g.outStart[u]; e < g.outStart[u + 1]; e++) {
|
|
63
|
+
if (!(g.outSite[e] & TCB_SITES))
|
|
64
|
+
continue;
|
|
65
|
+
const v = g.outDst[e];
|
|
66
|
+
if (seen[v] === kj)
|
|
67
|
+
continue;
|
|
68
|
+
seen[v] = kj;
|
|
69
|
+
const kv = keyAt.get(v);
|
|
70
|
+
// a key declaration is where a walk stops: what lies beyond it is its own support
|
|
71
|
+
if (kv !== undefined) {
|
|
72
|
+
edges.add(kj * K + kv);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!passable(v))
|
|
76
|
+
continue;
|
|
77
|
+
if (member(v)) {
|
|
78
|
+
const s = supporters.get(v);
|
|
79
|
+
if (s)
|
|
80
|
+
s.push(kj);
|
|
81
|
+
else
|
|
82
|
+
supporters.set(v, [kj]);
|
|
83
|
+
support[kj].push(v);
|
|
84
|
+
}
|
|
85
|
+
stack.push(v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// what no statement or body reaches: the target rests on it through a proof
|
|
90
|
+
const rootAt = keyAt.get(root);
|
|
91
|
+
const viaProof = [];
|
|
92
|
+
for (const i of order) {
|
|
93
|
+
if (keyAt.has(i) || supporters.has(i))
|
|
94
|
+
continue;
|
|
95
|
+
supporters.set(i, [rootAt]);
|
|
96
|
+
support[rootAt].push(i);
|
|
97
|
+
viaProof.push(i);
|
|
98
|
+
}
|
|
99
|
+
for (const s of support)
|
|
100
|
+
s.sort((a, b) => position.get(a) - position.get(b));
|
|
101
|
+
return {
|
|
102
|
+
root,
|
|
103
|
+
keys,
|
|
104
|
+
supporters,
|
|
105
|
+
support,
|
|
106
|
+
edges: [...edges].sort((a, b) => a - b).map((x) => [Math.floor(x / K), x % K]),
|
|
107
|
+
viaProof,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** The key graph's edges to draw while the tool is the Keys step of Rests on and something is lit
|
|
111
|
+
* (ViewSpec decision 20): of each key graph given — the focus's target's, or elsewhere every
|
|
112
|
+
* target's, which makes the union over the targets whose key declarations are lit — the edges
|
|
113
|
+
* whose two ends are lit, as [from, to] declarations, each once, in order. An edge from A to B
|
|
114
|
+
* says that A rests on B through declarations that are not key. */
|
|
115
|
+
export function litKeyEdges(kgs, lit) {
|
|
116
|
+
const out = new Map();
|
|
117
|
+
for (const kg of kgs) {
|
|
118
|
+
for (const [a, b] of kg.edges) {
|
|
119
|
+
const u = kg.keys[a], v = kg.keys[b];
|
|
120
|
+
if (lit(u) && lit(v))
|
|
121
|
+
out.set(`${u} ${v}`, [u, v]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...out.values()].sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
125
|
+
}
|
|
126
|
+
/** How the declarations `xs` stand, by people's review status. */
|
|
127
|
+
export function statusCounts(g, d, xs) {
|
|
128
|
+
const c = { total: 0, approved: 0, commented: 0, stale: 0, unreviewed: 0 };
|
|
129
|
+
for (const i of xs) {
|
|
130
|
+
c.total++;
|
|
131
|
+
const s = statusOf(d, g.nodes[i].name, g.nodes[i].hash);
|
|
132
|
+
if (s === "approved")
|
|
133
|
+
c.approved++;
|
|
134
|
+
else if (s === "commented")
|
|
135
|
+
c.commented++;
|
|
136
|
+
else if (s === "stale")
|
|
137
|
+
c.stale++;
|
|
138
|
+
else
|
|
139
|
+
c.unreviewed++;
|
|
140
|
+
}
|
|
141
|
+
return c;
|
|
142
|
+
}
|
|
143
|
+
/** The review's order with its key declarations first (ExportSpec decision 4): the key ones in
|
|
144
|
+
* review order, then the rest in review order — how the agents' API lists a review set with
|
|
145
|
+
* `key=first`. */
|
|
146
|
+
export function keyFirst(order, isKey) {
|
|
147
|
+
return [...order.filter(isKey), ...order.filter((i) => !isKey(i))];
|
|
148
|
+
}
|
|
149
|
+
/** The key graph's layers, top-down (an index per key declaration, into `kg.keys`): the target at
|
|
150
|
+
* 0, each other one a layer below every key declaration that rests on it — the longest path from
|
|
151
|
+
* the target, key declarations that rest on one another counted as one. A key declaration no
|
|
152
|
+
* statement or body reaches (an axiom reached through a proof) is put just under the target. */
|
|
153
|
+
export function keyLayers(kg) {
|
|
154
|
+
const K = kg.keys.length;
|
|
155
|
+
// reachability within the key graph: it is small (a dozen, a few dozen at most)
|
|
156
|
+
const reach = kg.keys.map(() => new Uint8Array(K));
|
|
157
|
+
for (const [a, b] of kg.edges)
|
|
158
|
+
reach[a][b] = 1;
|
|
159
|
+
for (let m = 0; m < K; m++)
|
|
160
|
+
for (let a = 0; a < K; a++)
|
|
161
|
+
if (reach[a][m])
|
|
162
|
+
for (let b = 0; b < K; b++)
|
|
163
|
+
if (reach[m][b])
|
|
164
|
+
reach[a][b] = 1;
|
|
165
|
+
// a component: those that reach one another, named by its first member
|
|
166
|
+
const comp = kg.keys.map((_, a) => {
|
|
167
|
+
for (let b = 0; b < a; b++)
|
|
168
|
+
if (reach[a][b] && reach[b][a])
|
|
169
|
+
return b;
|
|
170
|
+
return a;
|
|
171
|
+
});
|
|
172
|
+
const root = kg.keys.indexOf(kg.root);
|
|
173
|
+
const layer = new Array(K).fill(-1);
|
|
174
|
+
layer[root] = 0;
|
|
175
|
+
// longest path on the components, relaxed until nothing moves: at most K rounds
|
|
176
|
+
for (let round = 0; round <= K; round++) {
|
|
177
|
+
let moved = false;
|
|
178
|
+
for (const [a, b] of kg.edges) {
|
|
179
|
+
if (layer[a] < 0 || comp[a] === comp[b])
|
|
180
|
+
continue;
|
|
181
|
+
if (layer[b] < layer[a] + 1) {
|
|
182
|
+
layer[b] = layer[a] + 1;
|
|
183
|
+
moved = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// a component's members share its deepest layer
|
|
187
|
+
for (let a = 0; a < K; a++)
|
|
188
|
+
if (layer[a] >= 0 && layer[comp[a]] < layer[a])
|
|
189
|
+
(layer[comp[a]] = layer[a]), (moved = true);
|
|
190
|
+
for (let a = 0; a < K; a++)
|
|
191
|
+
if (layer[comp[a]] > layer[a])
|
|
192
|
+
(layer[a] = layer[comp[a]]), (moved = true);
|
|
193
|
+
if (!moved)
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
return layer.map((l, a) => (l >= 0 ? l : a === root ? 0 : 1));
|
|
197
|
+
}
|
|
198
|
+
/** The key declarations top-down (indices into `kg.keys`): the target, then layer by layer, within
|
|
199
|
+
* a layer the later in review order first — what rests on more comes before what it rests on. */
|
|
200
|
+
export function topDown(kg) {
|
|
201
|
+
const layer = keyLayers(kg);
|
|
202
|
+
return kg.keys.map((_, a) => a).sort((a, b) => layer[a] - layer[b] || b - a);
|
|
203
|
+
}
|