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.
Files changed (128) 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/extractor/Extract.lean +1180 -0
  9. package/dist/extractor/Usage.lean +900 -0
  10. package/dist/mcp/visualproof-mcp.mjs +476 -0
  11. package/dist/server/access.js +95 -0
  12. package/dist/server/agent.js +3235 -0
  13. package/dist/server/agentdocs.js +35 -0
  14. package/dist/server/agentlibrary.js +202 -0
  15. package/dist/server/agentnotes.js +701 -0
  16. package/dist/server/agentviews.js +703 -0
  17. package/dist/server/askfollow.js +286 -0
  18. package/dist/server/boardspots.js +159 -0
  19. package/dist/server/dirs.js +175 -0
  20. package/dist/server/doctor.js +186 -0
  21. package/dist/server/events.js +198 -0
  22. package/dist/server/export.js +420 -0
  23. package/dist/server/extract.js +204 -0
  24. package/dist/server/fetchurl.js +321 -0
  25. package/dist/server/figures.js +158 -0
  26. package/dist/server/history.js +741 -0
  27. package/dist/server/historyapi.js +304 -0
  28. package/dist/server/hover.js +595 -0
  29. package/dist/server/identity.js +114 -0
  30. package/dist/server/invites.js +241 -0
  31. package/dist/server/layoutthread.js +51 -0
  32. package/dist/server/library.js +600 -0
  33. package/dist/server/libraryroute.js +211 -0
  34. package/dist/server/live.js +1101 -0
  35. package/dist/server/lostfound.js +199 -0
  36. package/dist/server/main.js +1394 -0
  37. package/dist/server/mcptools.js +497 -0
  38. package/dist/server/pastdoc.js +73 -0
  39. package/dist/server/pipeline.js +156 -0
  40. package/dist/server/poppler.js +251 -0
  41. package/dist/server/presence.js +252 -0
  42. package/dist/server/project.js +222 -0
  43. package/dist/server/prompt.js +53 -0
  44. package/dist/server/readonly.js +152 -0
  45. package/dist/server/readonlygate.js +123 -0
  46. package/dist/server/renames.js +268 -0
  47. package/dist/server/review.js +334 -0
  48. package/dist/server/reviewapi.js +188 -0
  49. package/dist/server/snapshot.js +49 -0
  50. package/dist/server/sourcestore.js +473 -0
  51. package/dist/server/status.js +360 -0
  52. package/dist/server/talk.js +476 -0
  53. package/dist/server/tokens.js +221 -0
  54. package/dist/server/usage.js +501 -0
  55. package/dist/server/versions.js +82 -0
  56. package/dist/server/view.js +157 -0
  57. package/dist/server/views.js +1167 -0
  58. package/dist/shared/askfollow.js +46 -0
  59. package/dist/shared/board.js +332 -0
  60. package/dist/shared/build.js +2 -0
  61. package/dist/shared/collab.js +148 -0
  62. package/dist/shared/comment.js +285 -0
  63. package/dist/shared/context.js +157 -0
  64. package/dist/shared/doc/authors.js +98 -0
  65. package/dist/shared/doc/build.js +358 -0
  66. package/dist/shared/doc/latex.js +455 -0
  67. package/dist/shared/doc/markdown.js +159 -0
  68. package/dist/shared/doc/model.js +37 -0
  69. package/dist/shared/doc/order.js +107 -0
  70. package/dist/shared/doc/proof.js +272 -0
  71. package/dist/shared/doc/sections.js +93 -0
  72. package/dist/shared/doc/settings.js +75 -0
  73. package/dist/shared/edgecodec.js +102 -0
  74. package/dist/shared/editor.js +64 -0
  75. package/dist/shared/excerpt.js +87 -0
  76. package/dist/shared/graph.js +16 -0
  77. package/dist/shared/items.js +92 -0
  78. package/dist/shared/keys.js +203 -0
  79. package/dist/shared/layout.js +926 -0
  80. package/dist/shared/lexer.js +130 -0
  81. package/dist/shared/library.js +711 -0
  82. package/dist/shared/lostfound.js +150 -0
  83. package/dist/shared/moves.js +243 -0
  84. package/dist/shared/notecard.js +295 -0
  85. package/dist/shared/noteplace.js +505 -0
  86. package/dist/shared/palette.js +46 -0
  87. package/dist/shared/pastgraph.js +95 -0
  88. package/dist/shared/places.js +80 -0
  89. package/dist/shared/projectnotes.js +244 -0
  90. package/dist/shared/query.js +810 -0
  91. package/dist/shared/review.js +642 -0
  92. package/dist/shared/reviewqueue.js +333 -0
  93. package/dist/shared/ring.js +366 -0
  94. package/dist/shared/scope.js +255 -0
  95. package/dist/shared/scopeedit.js +81 -0
  96. package/dist/shared/search.js +191 -0
  97. package/dist/shared/separate.js +178 -0
  98. package/dist/shared/sharesnap.js +280 -0
  99. package/dist/shared/sticky.js +501 -0
  100. package/dist/shared/timeline.js +73 -0
  101. package/dist/shared/titles.js +298 -0
  102. package/dist/shared/viewgraph.js +139 -0
  103. package/dist/shared/views.js +859 -0
  104. package/dist/web/bundle/app.js +48733 -0
  105. package/dist/web/bundle/edges.js +409 -0
  106. package/dist/web/bundle/layout.js +1010 -0
  107. package/dist/web/bundle/pdfworker.js +48322 -0
  108. package/dist/web/index.html +239 -0
  109. package/dist/web/src/motion.js +430 -0
  110. package/dist/web/src/past.js +224 -0
  111. package/dist/web/src/scene.js +787 -0
  112. package/dist/web/style.css +2312 -0
  113. package/docs/agent-api/README.md +91 -0
  114. package/docs/agent-api/advice.md +108 -0
  115. package/docs/agent-api/export.md +120 -0
  116. package/docs/agent-api/following.md +140 -0
  117. package/docs/agent-api/history.md +142 -0
  118. package/docs/agent-api/library.md +65 -0
  119. package/docs/agent-api/mcp.md +104 -0
  120. package/docs/agent-api/notes.md +93 -0
  121. package/docs/agent-api/reading.md +166 -0
  122. package/docs/agent-api/requests.md +94 -0
  123. package/docs/agent-api/talking.md +100 -0
  124. package/docs/agent-api/tcb.md +135 -0
  125. package/docs/agent-api/views.md +124 -0
  126. package/docs/agent-api/words.md +56 -0
  127. package/package.json +39 -4
  128. 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
+ }