visualproof 0.1.0 → 0.3.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/README.md +40 -26
- package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
- package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
- package/dist/server/agent.js +125 -92
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/history.js +39 -0
- package/dist/server/historyapi.js +2 -2
- package/dist/server/hover.js +3 -3
- package/dist/server/layoutthread.js +1 -1
- package/dist/server/live.js +6 -0
- package/dist/server/main.js +151 -11
- package/dist/server/mcptools.js +6 -4
- package/dist/server/pastdoc.js +1 -1
- package/dist/server/pipeline.js +18 -9
- package/dist/server/readonlygate.js +2 -2
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +13 -3
- package/dist/server/usage.js +2 -2
- package/dist/server/views.js +25 -1
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +99 -1
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/doc/order.js +17 -0
- package/dist/shared/doc/outline.js +45 -0
- package/dist/shared/keys.js +1 -1
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/moves.js +2 -1
- package/dist/shared/pastgraph.js +9 -1
- package/dist/shared/query.js +21 -13
- package/dist/shared/review.js +62 -5
- package/dist/shared/sharesnap.js +2 -1
- package/dist/shared/titles.js +2 -0
- package/dist/shared/viewgraph.js +18 -0
- package/dist/web/bundle/app.js +7709 -3222
- package/dist/web/bundle/edges.js +4 -6
- package/dist/web/index.html +24 -6
- package/dist/web/src/agentmark.js +14 -0
- package/dist/web/src/artwork.js +1000 -0
- package/dist/web/src/bundles.js +151 -0
- package/dist/web/src/canvas.js +3694 -0
- package/dist/web/src/drag.js +61 -0
- package/dist/web/src/edgequeue.js +204 -0
- package/dist/web/src/gl/gl.js +123 -0
- package/dist/web/src/gl/renderer.js +451 -0
- package/dist/web/src/halo.js +95 -0
- package/dist/web/src/highlight.js +108 -0
- package/dist/web/src/overview.js +393 -0
- package/dist/web/src/reviewqueue.js +250 -0
- package/dist/web/src/scene.js +17 -0
- package/dist/web/src/selection.js +272 -0
- package/dist/web/src/status.js +80 -0
- package/dist/web/src/visitors.js +318 -0
- package/dist/web/src/visits.js +174 -0
- package/dist/web/style.css +255 -12
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/following.md +2 -2
- package/docs/agent-api/history.md +2 -1
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/docs/agent-api/views.md +3 -2
- package/package.json +3 -1
- package/test/toolchains.json +2 -2
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The document's outline (ExportSpec decision 18, the linear form): what the document of a view
|
|
2
|
+
// prints, in its order, without the text — its sections, numbered as the PDF numbers them, the
|
|
3
|
+
// declarations and the notes in each, and the notes attached to a declaration after it. The page
|
|
4
|
+
// draws the board as this outline (web/src/linear.ts); it is made from the same plan and the same
|
|
5
|
+
// order the document is (shared/doc/sections.ts, shared/doc/order.ts), so that the two agree.
|
|
6
|
+
import { orderDoc } from "./order.js";
|
|
7
|
+
/** A note's first words: its first line, Markdown marks taken out, at most 60 characters. */
|
|
8
|
+
export function firstWords(text) {
|
|
9
|
+
const line = text.split("\n").find((l) => l.trim()) ?? "";
|
|
10
|
+
const plain = line.replace(/[*_`#>]+/g, "").replace(/\s+/g, " ").trim();
|
|
11
|
+
return plain.length > 60 ? `${plain.slice(0, 59).trimEnd()}…` : plain;
|
|
12
|
+
}
|
|
13
|
+
/** The outline of the document of `plan` (of graph `g`) in the order of the board `spots` gives. */
|
|
14
|
+
export function outlineOf(plan, g, spots) {
|
|
15
|
+
const order = orderDoc(plan, g, spots);
|
|
16
|
+
const note = (k) => {
|
|
17
|
+
const n = spots.notes[k];
|
|
18
|
+
return { t: "note", id: n.note.id, kind: n.kind, words: firstWords(n.note.text) };
|
|
19
|
+
};
|
|
20
|
+
const item = (p, prefix, at) => {
|
|
21
|
+
if (p.t === "note")
|
|
22
|
+
return note(p.k);
|
|
23
|
+
if (p.t === "decl") {
|
|
24
|
+
const ks = order.attached.get(p.i);
|
|
25
|
+
return { t: "decl", name: g.nodes[p.i].name, ...(ks?.length ? { notes: ks.map(note) } : {}) };
|
|
26
|
+
}
|
|
27
|
+
return section(p.s, prefix, at);
|
|
28
|
+
};
|
|
29
|
+
// numbered as the PDF numbers its headings (shared/doc/latex.ts): the first three levels, each
|
|
30
|
+
// within the one above; a paragraph-level heading has none
|
|
31
|
+
const section = (o, prefix, at) => {
|
|
32
|
+
const p = o.plan;
|
|
33
|
+
at.k++;
|
|
34
|
+
const number = p.level < 4 ? (prefix ? `${prefix}.${at.k}` : `${at.k}`) : null;
|
|
35
|
+
const inner = { k: 0 };
|
|
36
|
+
const items = o.items.map((x) => item(x, p.level < 3 ? (number ?? "") : "", inner));
|
|
37
|
+
return { t: "section", key: p.key, keys: [...p.keys], title: p.title, number, level: p.level, items };
|
|
38
|
+
};
|
|
39
|
+
const top = { k: 0 };
|
|
40
|
+
return {
|
|
41
|
+
items: order.items.map((x) => item(x, "", top)),
|
|
42
|
+
count: { declarations: plan.count, sections: plan.sectionCount },
|
|
43
|
+
...(spots.fresh ? { fresh: true } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/shared/keys.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
//
|
|
22
22
|
// A review holds many members and a target a dozen key declarations: after one walk over the
|
|
23
23
|
// target's TCB, each walk reads the edges of the members and of what it passes through, never the
|
|
24
|
-
// whole graph's (
|
|
24
|
+
// whole graph's (Sphere-Packing-Lean's `Dim24.MainTheorem`: its TCB of 226 in 72 ms, its keys in 1 ms).
|
|
25
25
|
import { keyMarks, statusOf } from "./review.js";
|
|
26
26
|
import { TCB_SITES } from "./reviewqueue.js";
|
|
27
27
|
/** The key declarations of `root` among the members of its review (`order`, the queue's order —
|
package/dist/shared/lostfound.js
CHANGED
|
@@ -64,6 +64,7 @@ export function lostEntries(d, views, has, known = (n) => n.includes("."), pinWh
|
|
|
64
64
|
kind: "opinion",
|
|
65
65
|
name,
|
|
66
66
|
...who(o),
|
|
67
|
+
...(o.recordedBy ? { recordedBy: o.recordedBy } : {}),
|
|
67
68
|
verdict: o.verdict,
|
|
68
69
|
...(o.comment ? { comment: o.comment } : {}),
|
|
69
70
|
...(was.length ? { aliases: was.map((n) => lostKey.opinion(name, n, o.at)) } : {}),
|
package/dist/shared/moves.js
CHANGED
package/dist/shared/pastgraph.js
CHANGED
|
@@ -21,6 +21,13 @@ export const PAST_GHOST = 1;
|
|
|
21
21
|
export const PAST_CHANGED = 4;
|
|
22
22
|
/** renamed since: shown under its name then */
|
|
23
23
|
export const PAST_RENAMED = 8;
|
|
24
|
+
/** The git commit of a past build; none where it is not known, or only guessed. Its source is the
|
|
25
|
+
* project's, which the page has now (web/src/main.ts, GET /api/git). */
|
|
26
|
+
function gitThen(commit) {
|
|
27
|
+
if (!commit || commit.guessed)
|
|
28
|
+
return { git: undefined };
|
|
29
|
+
return { git: { source: null, commit: commit.hash, branch: commit.branch, ...(commit.dirty ? { dirty: true } : {}) } };
|
|
30
|
+
}
|
|
24
31
|
/** The past graph of snapshot `snap` (its declarations and its edges, as /graph's parts give them),
|
|
25
32
|
* marked by `diff` (from it to the latest), with the project's description of `now`. */
|
|
26
33
|
export function pastGraph(snap, edges, now, diff) {
|
|
@@ -33,7 +40,8 @@ export function pastGraph(snap, edges, now, diff) {
|
|
|
33
40
|
const meta = {
|
|
34
41
|
schema: 1,
|
|
35
42
|
layoutVersion: now.meta.layoutVersion,
|
|
36
|
-
|
|
43
|
+
// the commit of that build, where it is known
|
|
44
|
+
project: { ...now.meta.project, extractedAt: snap.snapshot.at, ...gitThen(snap.snapshot.commit) },
|
|
37
45
|
modules: snap.modules.map((m) => ({ name: m.name, file: null, imports: m.imports })),
|
|
38
46
|
nodes,
|
|
39
47
|
projectCount: P,
|
package/dist/shared/query.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// The text form, left to right:
|
|
10
10
|
//
|
|
11
|
-
// all | selection | decl(A, B) | tcb(A) | uses(A, B) | usedBy(selection)
|
|
11
|
+
// all | selection | decl(A, B) | tcb(A) | uses(A, B) | usedBy(selection) | refs(A)
|
|
12
12
|
// tcb(ns(Fermat42) except external) a closure of the set a query holds
|
|
13
13
|
// ns(Fermat42) declarations whose name is under the prefix
|
|
14
14
|
// file(Mathlib.NumberTheory) declarations of the modules under the prefix
|
|
@@ -27,7 +27,7 @@ import { adviceOf, keyMarks, STANDARD_AXIOMS, statusOf, trusted, under } from ".
|
|
|
27
27
|
export const KINDS = ["theorem", "def", "abbrev", "instance", "structure", "class", "inductive", "opaque", "axiom", "quot", "constructor", "recursor", "notation", "unknown"];
|
|
28
28
|
export const STATUSES = ["unreviewed", "approved", "commented", "stale"];
|
|
29
29
|
const VERDICTS = ["approve", "comment"];
|
|
30
|
-
const CLOSURES = ["tcb", "uses", "usedBy"];
|
|
30
|
+
const CLOSURES = ["tcb", "uses", "usedBy", "refs"];
|
|
31
31
|
const PLAIN = ["all", "selection", "trusted", "external", "sorry", "nonstandard"];
|
|
32
32
|
const COMBINATORS = ["and", "or", "except"];
|
|
33
33
|
/** words for the same thing, read as the first */
|
|
@@ -38,10 +38,11 @@ export const LIGHT = {
|
|
|
38
38
|
plain: { op: "selection" },
|
|
39
39
|
tcb: { op: "tcb", of: "selection" },
|
|
40
40
|
deps: { op: "uses", of: "selection" },
|
|
41
|
+
refs: { op: "refs", of: "selection" },
|
|
41
42
|
apps: { op: "usedBy", of: "selection" },
|
|
42
43
|
};
|
|
43
|
-
/** Which
|
|
44
|
-
const CLOSURE_MODE = { tcb: "tcb", uses: "deps", usedBy: "apps" };
|
|
44
|
+
/** Which walk of the graph each closure word is (shared/viewgraph.ts); `refs` is one step. */
|
|
45
|
+
const CLOSURE_MODE = { tcb: "tcb", uses: "deps", usedBy: "apps", refs: "refs" };
|
|
45
46
|
// ---------------------------------------------------------------- how large a query may be
|
|
46
47
|
/** How large a query may be: one set of bounds for its text and its JSON form, checked when either
|
|
47
48
|
* is read (parseQuery, cleanQuery), so that what one form takes the other takes too — a scope
|
|
@@ -61,7 +62,7 @@ export const QUERY_LIMITS = {
|
|
|
61
62
|
names: 2_000,
|
|
62
63
|
/** characters of one name or prefix */
|
|
63
64
|
name: 2_000,
|
|
64
|
-
/** closures (tcb, uses, usedBy), each a walk of the graph; each target of a `key(…)` counts as
|
|
65
|
+
/** closures (tcb, uses, usedBy, refs), each a walk of the graph; each target of a `key(…)` counts as
|
|
65
66
|
* one too, since its review set is walked to keep only the marks still in it. `key(targets)`
|
|
66
67
|
* counts as one: how many targets there are is the project's, not the query's, and every
|
|
67
68
|
* `key(targets)` of a query shares the same walks (evaluateIn) */
|
|
@@ -88,6 +89,7 @@ export function beyondLimits(q) {
|
|
|
88
89
|
case "tcb":
|
|
89
90
|
case "uses":
|
|
90
91
|
case "usedBy":
|
|
92
|
+
case "refs":
|
|
91
93
|
closures++;
|
|
92
94
|
if (x.of !== "selection" && !Array.isArray(x.of)) {
|
|
93
95
|
terms++;
|
|
@@ -131,7 +133,7 @@ export function beyondLimits(q) {
|
|
|
131
133
|
if (longest > L.name)
|
|
132
134
|
return `a name in the query is longer than ${L.name.toLocaleString("en")} characters`;
|
|
133
135
|
if (closures > L.closures)
|
|
134
|
-
return `the query has ${closures} closures (tcb, uses, usedBy), more than ${L.closures}`;
|
|
136
|
+
return `the query has ${closures} closures (tcb, uses, usedBy, refs), more than ${L.closures}`;
|
|
135
137
|
const length = printQuery(q).length;
|
|
136
138
|
if (length > L.text)
|
|
137
139
|
return `the query is ${length.toLocaleString("en")} characters long as printed, more than ${L.text.toLocaleString("en")}`;
|
|
@@ -286,7 +288,7 @@ function parse(text) {
|
|
|
286
288
|
}
|
|
287
289
|
const w = keyword(t);
|
|
288
290
|
if (w === null)
|
|
289
|
-
throw new QueryError(t.t === "string" ? "a quoted name stands only inside decl(…), tcb(…), uses(…) or
|
|
291
|
+
throw new QueryError(t.t === "string" ? "a quoted name stands only inside decl(…), tcb(…), uses(…), usedBy(…) or refs(…)" : `${t.t} is out of place`, t.at);
|
|
290
292
|
if (++terms > QUERY_LIMITS.terms)
|
|
291
293
|
throw new QueryError(`the query has more than ${QUERY_LIMITS.terms.toLocaleString("en")} parts`, t.at);
|
|
292
294
|
k++;
|
|
@@ -338,7 +340,7 @@ function parse(text) {
|
|
|
338
340
|
return { op: "status", statuses: choose(args(), STATUSES, "status") };
|
|
339
341
|
if (w === "advice")
|
|
340
342
|
return { op: "advice", verdicts: choose(args(), VERDICTS, "advice") };
|
|
341
|
-
throw new QueryError(`${text1(t)} is not a word of the query language (all, tcb, uses, usedBy, decl, key, ns, file, trusted, external, kind, status, advice, sorry, nonstandard)`, t.at);
|
|
343
|
+
throw new QueryError(`${text1(t)} is not a word of the query language (all, tcb, uses, usedBy, refs, decl, key, ns, file, trusted, external, kind, status, advice, sorry, nonstandard)`, t.at);
|
|
342
344
|
};
|
|
343
345
|
const expr = () => {
|
|
344
346
|
let q = atom();
|
|
@@ -376,7 +378,8 @@ export function printQuery(q) {
|
|
|
376
378
|
return `key(${q.of === "targets" ? "targets" : q.of.map((t) => (t === "targets" ? JSON.stringify(t) : arg(t))).join(", ")})`;
|
|
377
379
|
case "tcb":
|
|
378
380
|
case "uses":
|
|
379
|
-
case "usedBy":
|
|
381
|
+
case "usedBy":
|
|
382
|
+
case "refs": {
|
|
380
383
|
if (q.of === "selection" || Array.isArray(q.of))
|
|
381
384
|
return `${q.op}(${q.of === "selection" ? "selection" : q.of.map(arg).join(", ")})`;
|
|
382
385
|
// a query of one plain word would read as a name
|
|
@@ -461,7 +464,8 @@ function readJson(x, why = false) {
|
|
|
461
464
|
return names(o.of) ? { op: "key", of: [...o.of] } : null;
|
|
462
465
|
case "tcb":
|
|
463
466
|
case "uses":
|
|
464
|
-
case "usedBy":
|
|
467
|
+
case "usedBy":
|
|
468
|
+
case "refs": {
|
|
465
469
|
if (o.of === "selection")
|
|
466
470
|
return { op: o.op, of: "selection" };
|
|
467
471
|
if (o.of && typeof o.of === "object" && !Array.isArray(o.of)) {
|
|
@@ -531,7 +535,7 @@ export function namesIn(q) {
|
|
|
531
535
|
x.names.forEach((n) => out.add(n));
|
|
532
536
|
else if (x.op === "key" && x.of !== "targets")
|
|
533
537
|
x.of.forEach((n) => out.add(n));
|
|
534
|
-
else if (x.op === "tcb" || x.op === "uses" || x.op === "usedBy") {
|
|
538
|
+
else if (x.op === "tcb" || x.op === "uses" || x.op === "usedBy" || x.op === "refs") {
|
|
535
539
|
if (Array.isArray(x.of))
|
|
536
540
|
x.of.forEach((n) => out.add(n));
|
|
537
541
|
else if (x.of !== "selection")
|
|
@@ -549,7 +553,7 @@ export function namesIn(q) {
|
|
|
549
553
|
export function readsReview(q) {
|
|
550
554
|
if (q.op === "trusted" || q.op === "status" || q.op === "advice" || q.op === "key")
|
|
551
555
|
return true;
|
|
552
|
-
if ((q.op === "tcb" || q.op === "uses" || q.op === "usedBy") && q.of !== "selection" && !Array.isArray(q.of))
|
|
556
|
+
if ((q.op === "tcb" || q.op === "uses" || q.op === "usedBy" || q.op === "refs") && q.of !== "selection" && !Array.isArray(q.of))
|
|
553
557
|
return readsReview(q.of);
|
|
554
558
|
if (q.op === "and" || q.op === "or" || q.op === "except")
|
|
555
559
|
return readsReview(q.a) || readsReview(q.b);
|
|
@@ -563,6 +567,7 @@ export function withSelection(q, names) {
|
|
|
563
567
|
case "tcb":
|
|
564
568
|
case "uses":
|
|
565
569
|
case "usedBy":
|
|
570
|
+
case "refs":
|
|
566
571
|
if (q.of === "selection")
|
|
567
572
|
return { op: q.op, of: [...names] };
|
|
568
573
|
return Array.isArray(q.of) ? q : { op: q.op, of: withSelection(q.of, names) };
|
|
@@ -647,6 +652,8 @@ export function describe(q) {
|
|
|
647
652
|
return `TCB of ${roots(q.of)}`;
|
|
648
653
|
case "uses":
|
|
649
654
|
return `what ${roots(q.of)} uses`;
|
|
655
|
+
case "refs":
|
|
656
|
+
return `what ${roots(q.of)} uses directly`;
|
|
650
657
|
case "usedBy":
|
|
651
658
|
return `what uses ${roots(q.of)}`;
|
|
652
659
|
case "ns":
|
|
@@ -759,7 +766,8 @@ function evaluateIn(q, ctx, walks) {
|
|
|
759
766
|
return out;
|
|
760
767
|
case "tcb":
|
|
761
768
|
case "uses":
|
|
762
|
-
case "usedBy":
|
|
769
|
+
case "usedBy":
|
|
770
|
+
case "refs": {
|
|
763
771
|
const key = `${q.op}\u0000${q.of === "selection" ? "\u0001" : Array.isArray(q.of) ? JSON.stringify(q.of) : printQuery(q.of)}`;
|
|
764
772
|
let walked = walks.get(key);
|
|
765
773
|
if (!walked) {
|
package/dist/shared/review.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// Declarations are keyed by name, and a name can be anything Lean allows: `toString`,
|
|
41
41
|
// `constructor`, even `__proto__`. Every object keyed by names here has no prototype (`byName`),
|
|
42
42
|
// is read with Object.hasOwn, and is never spread or assigned into a plain object.
|
|
43
|
+
import { validName } from "./collab.js";
|
|
43
44
|
import { applyLibrary, invalidLibrary, isLibraryOp, libraryEventOf, libraryFromJson, libraryNoChange, readLibraryEvent, renamePerson } from "./library.js";
|
|
44
45
|
import { applyMove, moveIsIdle, readMove } from "./moves.js";
|
|
45
46
|
export const DEFAULT_TRUSTED = ["Batteries", "Init", "Lean", "Mathlib", "Std"];
|
|
@@ -102,6 +103,19 @@ export function invalid(r) {
|
|
|
102
103
|
return "agent";
|
|
103
104
|
if (o.person !== undefined && (!validPersonId(o.person) || o.agent))
|
|
104
105
|
return "person";
|
|
106
|
+
// an opinion recorded for someone else (ReviewSpec decision 8): a person's, for another person
|
|
107
|
+
if (o.for !== undefined || o.forPerson !== undefined) {
|
|
108
|
+
if (o.op !== "opinion")
|
|
109
|
+
return "only an opinion is recorded for someone else";
|
|
110
|
+
if (o.agent)
|
|
111
|
+
return "an agent records for no one";
|
|
112
|
+
if (typeof o.for !== "string" || !validName(o.for.trim()))
|
|
113
|
+
return "for";
|
|
114
|
+
if (o.forPerson !== undefined && !validPersonId(o.forPerson))
|
|
115
|
+
return "forPerson";
|
|
116
|
+
if (o.for.trim() === o.by.trim() || (o.forPerson !== undefined && o.forPerson === o.person))
|
|
117
|
+
return "an opinion of one's own is recorded as one's own";
|
|
118
|
+
}
|
|
105
119
|
switch (o.op) {
|
|
106
120
|
case "opinion":
|
|
107
121
|
if (o.verdict !== "approve" && o.verdict !== "comment")
|
|
@@ -153,6 +167,12 @@ export function eventOf(d, r, toolchain, now) {
|
|
|
153
167
|
switch (r.op) {
|
|
154
168
|
case "opinion": {
|
|
155
169
|
const e = { op: "opinion", name: r.name, by, ...person, verdict: r.verdict, at: now, toolchain };
|
|
170
|
+
// recorded for someone else (ReviewSpec decision 8)
|
|
171
|
+
if (r.for?.trim()) {
|
|
172
|
+
e.for = r.for.trim();
|
|
173
|
+
if (r.forPerson)
|
|
174
|
+
e.forPerson = r.forPerson;
|
|
175
|
+
}
|
|
156
176
|
if (r.comment?.trim())
|
|
157
177
|
e.comment = r.comment.trim();
|
|
158
178
|
if (r.agent)
|
|
@@ -160,7 +180,7 @@ export function eventOf(d, r, toolchain, now) {
|
|
|
160
180
|
// the text matters only to an opinion that can go stale, one bound to a version
|
|
161
181
|
if (r.hash) {
|
|
162
182
|
e.hash = r.hash;
|
|
163
|
-
const was = (Object.hasOwn(d.opinions, r.name) ? d.opinions[r.name] : []).findLast((p) => author(p) ===
|
|
183
|
+
const was = (Object.hasOwn(d.opinions, r.name) ? d.opinions[r.name] : []).findLast((p) => author(p) === voice(e));
|
|
164
184
|
const same = was?.hash === r.hash ? judgedOn(d, r.name, was) : null;
|
|
165
185
|
if (r.text !== undefined && !(same?.of === "source" && same.text === r.text))
|
|
166
186
|
e.text = r.text;
|
|
@@ -212,8 +232,8 @@ export function applyEvent(d, e) {
|
|
|
212
232
|
case "name":
|
|
213
233
|
return;
|
|
214
234
|
case "opinion": {
|
|
215
|
-
const { op: _op, name, ...o } = e;
|
|
216
|
-
(Object.hasOwn(d.opinions, name) ? d.opinions[name] : (d.opinions[name] = [])).push(o);
|
|
235
|
+
const { op: _op, name, for: forName, forPerson, ...o } = e;
|
|
236
|
+
(Object.hasOwn(d.opinions, name) ? d.opinions[name] : (d.opinions[name] = [])).push(forName === undefined ? o : recordedFor(d, o, forName, forPerson));
|
|
217
237
|
return;
|
|
218
238
|
}
|
|
219
239
|
case "target":
|
|
@@ -256,15 +276,27 @@ function named(d, id, name) {
|
|
|
256
276
|
if (p?.name === name)
|
|
257
277
|
return;
|
|
258
278
|
if (!p) {
|
|
259
|
-
|
|
279
|
+
// first seen after opinions were recorded for them (decision 8): those show under this name,
|
|
280
|
+
// the names they were written under kept, so that a quote of one still finds it
|
|
281
|
+
const was = new Set();
|
|
282
|
+
for (const ops of Object.values(d.opinions))
|
|
283
|
+
for (const o of ops)
|
|
284
|
+
if (o.person === id && o.by !== name) {
|
|
285
|
+
was.add(o.by);
|
|
286
|
+
o.by = name;
|
|
287
|
+
}
|
|
288
|
+
d.people[id] = { name, was: [...was] };
|
|
260
289
|
return;
|
|
261
290
|
}
|
|
262
291
|
p.was = [...p.was.filter((n) => n !== p.name && n !== name), p.name];
|
|
263
292
|
p.name = name;
|
|
264
293
|
for (const ops of Object.values(d.opinions))
|
|
265
|
-
for (const o of ops)
|
|
294
|
+
for (const o of ops) {
|
|
266
295
|
if (o.person === id)
|
|
267
296
|
o.by = name;
|
|
297
|
+
if (o.recordedBy?.person === id)
|
|
298
|
+
o.recordedBy.by = name;
|
|
299
|
+
}
|
|
268
300
|
for (const t of Object.values(d.targets))
|
|
269
301
|
if (t.person === id)
|
|
270
302
|
t.by = name;
|
|
@@ -351,6 +383,20 @@ export function readEvent(raw, agentLog, personLog = null) {
|
|
|
351
383
|
}
|
|
352
384
|
if (e.text !== undefined && e.statement !== undefined)
|
|
353
385
|
return "a text and a statement";
|
|
386
|
+
// recorded for someone else (ReviewSpec decision 8): a person's line, for another person
|
|
387
|
+
if (o.for !== undefined || o.forPerson !== undefined) {
|
|
388
|
+
if (agentLog)
|
|
389
|
+
return "an agent's opinion recorded for someone else";
|
|
390
|
+
if (typeof o.for !== "string" || !validName(o.for))
|
|
391
|
+
return "for";
|
|
392
|
+
if (o.forPerson !== undefined && !validPersonId(o.forPerson))
|
|
393
|
+
return "forPerson";
|
|
394
|
+
if (o.for === by || (o.forPerson !== undefined && o.forPerson === personLog))
|
|
395
|
+
return "an opinion recorded for its own writer";
|
|
396
|
+
e.for = o.for;
|
|
397
|
+
if (o.forPerson !== undefined)
|
|
398
|
+
e.forPerson = o.forPerson;
|
|
399
|
+
}
|
|
354
400
|
if (agentLog)
|
|
355
401
|
e.agent = true;
|
|
356
402
|
return e;
|
|
@@ -503,6 +549,17 @@ export function problemsText(ps) {
|
|
|
503
549
|
function author(o) {
|
|
504
550
|
return o.person ? `link ${o.person}` : (o.agent ? "agent " : "person ") + o.by;
|
|
505
551
|
}
|
|
552
|
+
/** Whose an opinion line is: theirs it was recorded for (ReviewSpec decision 8), else its writer's. */
|
|
553
|
+
function voice(e) {
|
|
554
|
+
return e.for === undefined ? author(e) : author({ by: e.for, verdict: e.verdict, at: e.at, ...(e.forPerson ? { person: e.forPerson } : {}) });
|
|
555
|
+
}
|
|
556
|
+
/** An opinion recorded for someone else (ReviewSpec decision 8), as the review holds it: theirs —
|
|
557
|
+
* under their latest name where they are invited — saying who recorded it. */
|
|
558
|
+
function recordedFor(d, o, forName, forPerson) {
|
|
559
|
+
const { by, person, ...rest } = o;
|
|
560
|
+
const name = forPerson && Object.hasOwn(d.people, forPerson) ? d.people[forPerson].name : forName;
|
|
561
|
+
return { ...rest, by: name, ...(forPerson ? { person: forPerson } : {}), recordedBy: { by, ...(person ? { person } : {}) } };
|
|
562
|
+
}
|
|
506
563
|
/** Each author's latest opinion on a declaration, people's or agents': a new one replaces their
|
|
507
564
|
* earlier one. */
|
|
508
565
|
function latest(d, name, agents) {
|
package/dist/shared/sharesnap.js
CHANGED
|
@@ -105,7 +105,8 @@ export const SNAPSHOT_FORMS = {
|
|
|
105
105
|
lostFound: same("lostFound"),
|
|
106
106
|
colouring: same("scheme"),
|
|
107
107
|
edges: same("edges"),
|
|
108
|
-
|
|
108
|
+
linear: same("linear"),
|
|
109
|
+
artwork: same("artwork"),
|
|
109
110
|
};
|
|
110
111
|
/** The snapshot of where a page is (`v`, as presence carries it: the registry's parts, read by the
|
|
111
112
|
* page), as a link carries it after `snap=`. */
|
package/dist/shared/titles.js
CHANGED
|
@@ -224,6 +224,8 @@ function said(q, N) {
|
|
|
224
224
|
return `TCB of ${roots(q.of, N)}`;
|
|
225
225
|
case "uses":
|
|
226
226
|
return `what ${roots(q.of, N)} uses`;
|
|
227
|
+
case "refs":
|
|
228
|
+
return `what ${roots(q.of, N)} uses directly`;
|
|
227
229
|
case "usedBy":
|
|
228
230
|
return `what uses ${roots(q.of, N)}`;
|
|
229
231
|
case "ns":
|
package/dist/shared/viewgraph.js
CHANGED
|
@@ -9,6 +9,7 @@ export const MODE_SITES = {
|
|
|
9
9
|
plain: ALL_SITES,
|
|
10
10
|
tcb: SITE_STMT | SITE_BODY,
|
|
11
11
|
deps: ALL_SITES,
|
|
12
|
+
refs: ALL_SITES,
|
|
12
13
|
apps: ALL_SITES,
|
|
13
14
|
};
|
|
14
15
|
export class Graph {
|
|
@@ -69,6 +70,20 @@ export class Graph {
|
|
|
69
70
|
isExt(i) {
|
|
70
71
|
return i >= this.projectCount;
|
|
71
72
|
}
|
|
73
|
+
/** `from` and what is one edge from it, along edges whose sites meet `sites`. */
|
|
74
|
+
step(from, sites, dir) {
|
|
75
|
+
const seen = new Uint8Array(this.n);
|
|
76
|
+
const start = dir === "out" ? this.outStart : this.inStart;
|
|
77
|
+
const adj = dir === "out" ? this.outDst : this.inSrc;
|
|
78
|
+
const site = dir === "out" ? this.outSite : this.inSite;
|
|
79
|
+
for (const u of from) {
|
|
80
|
+
seen[u] = 1;
|
|
81
|
+
for (let e = start[u]; e < start[u + 1]; e++)
|
|
82
|
+
if (site[e] & sites)
|
|
83
|
+
seen[adj[e]] = 1;
|
|
84
|
+
}
|
|
85
|
+
return seen;
|
|
86
|
+
}
|
|
72
87
|
/** Everything reachable from `from` (included) along edges whose sites meet `sites`. */
|
|
73
88
|
reach(from, sites, dir) {
|
|
74
89
|
const seen = new Uint8Array(this.n);
|
|
@@ -108,6 +123,9 @@ export class Graph {
|
|
|
108
123
|
return this.reach(focus, MODE_SITES.tcb, "out");
|
|
109
124
|
case "deps":
|
|
110
125
|
return this.reach(focus, ALL_SITES, "out");
|
|
126
|
+
// Direct: what it uses directly, one step (ViewSpec decision 20, the owner's decision of 2026-10-14)
|
|
127
|
+
case "refs":
|
|
128
|
+
return this.step(focus, ALL_SITES, "out");
|
|
111
129
|
case "apps":
|
|
112
130
|
return this.reach(focus, ALL_SITES, "in");
|
|
113
131
|
}
|