visualproof 0.2.0 → 0.4.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 +88 -33
- package/dist/assets/tips/phone-fingers.light.mp4 +0 -0
- package/dist/assets/tips/phone-fingers.light.webp +0 -0
- package/dist/assets/tips/tour-agent.webp +0 -0
- package/dist/assets/tips/tour-arrange.webp +0 -0
- package/dist/assets/tips/tour-artwork.webp +0 -0
- package/dist/assets/tips/tour-checks.webp +0 -0
- package/dist/assets/tips/tour-export.webp +0 -0
- package/dist/assets/tips/tour-keys.webp +0 -0
- package/dist/assets/tips/tour-library.webp +0 -0
- package/dist/assets/tips/tour-project.webp +0 -0
- package/dist/assets/tips/tour-review.webp +0 -0
- package/dist/assets/tips/tour-share.webp +0 -0
- package/dist/assets/tips/tour-source.webp +0 -0
- package/dist/assets/tips/tour-target.webp +0 -0
- package/dist/assets/tips/tour-view.webp +0 -0
- package/dist/assets/tips/tour.json +289 -0
- package/dist/assets/tips/tour.webp +0 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/metrics.json +1 -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/mcp/visualproof-mcp.mjs +3 -1
- package/dist/server/agent.js +94 -91
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/children.js +85 -0
- package/dist/server/doctor.js +105 -42
- package/dist/server/events.js +10 -0
- package/dist/server/export.js +11 -4
- package/dist/server/extract.js +3 -2
- package/dist/server/figures.js +3 -2
- package/dist/server/film.js +380 -0
- package/dist/server/history.js +11 -1
- package/dist/server/hover.js +8 -5
- package/dist/server/live.js +72 -9
- package/dist/server/main.js +352 -23
- package/dist/server/mcptools.js +4 -1
- package/dist/server/media.js +114 -0
- package/dist/server/pipeline.js +21 -10
- package/dist/server/poppler.js +3 -2
- package/dist/server/readonlygate.js +4 -4
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +10 -1
- package/dist/server/sourcestore.js +3 -2
- package/dist/server/status.js +3 -2
- package/dist/server/supervise.js +219 -0
- package/dist/server/updates.js +951 -0
- package/dist/server/usage.js +3 -1
- package/dist/server/version.js +183 -0
- package/dist/server/view.js +0 -7
- package/dist/server/views.js +24 -0
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +7 -2
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/pastgraph.js +6 -6
- package/dist/shared/review.js +62 -5
- package/dist/shared/timeline.js +5 -0
- package/dist/web/bundle/app.js +3006 -413
- package/dist/web/bundle/edges.js +1 -1
- package/dist/web/index.html +13 -1
- 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 +3716 -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/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 +196 -1
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/history.md +5 -3
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/package.json +5 -1
package/dist/shared/collab.js
CHANGED
|
@@ -93,8 +93,11 @@ export function artNameId(kind, s) {
|
|
|
93
93
|
* presence carries, in bytes of their JSON: a heartbeat is at most 16 KB (server/main.ts), and the
|
|
94
94
|
* rest of where a reader is has room beside them. Names by selection are kept as what differs from
|
|
95
95
|
* As shown, so that a few names hidden or added are a few names carried. */
|
|
96
|
-
export const ART_NAMES_BYTES =
|
|
96
|
+
export const ART_NAMES_BYTES = 4000;
|
|
97
97
|
export const ART_TARGETS_BYTES = 2500;
|
|
98
|
+
/** the names a recipe's rule chose (ArtworkLook `ruledNames`): a rule names a few clusters. The four
|
|
99
|
+
* lists full, with the longest title and line, leave a heartbeat room (test/sharesnap.test.ts) */
|
|
100
|
+
export const ART_RULED_BYTES = 1000;
|
|
98
101
|
/** What `x` adds to a list's JSON: itself, quoted, and a comma. */
|
|
99
102
|
const jsonBytes = (x) => new TextEncoder().encode(JSON.stringify(x)).length + 1;
|
|
100
103
|
/** A list of names as given, checked: strings, each once, none empty or longer than a name is (1,000
|
|
@@ -148,12 +151,14 @@ export function cleanArtwork(v) {
|
|
|
148
151
|
// say them, and the key marks as the targets drew them before (with their stars, unless As any)
|
|
149
152
|
const namesFrom = o.namesFrom === "none" ? "none" : "board";
|
|
150
153
|
const hiddenNames = cleanNames(o.hiddenNames, ART_NAMES_BYTES, isArtNameId), shownNames = cleanNames(o.shownNames, ART_NAMES_BYTES, isArtNameId);
|
|
154
|
+
// the names a recipe's rule chose, said since later that night: none where a page does not say them
|
|
155
|
+
const ruledNames = cleanNames(o.ruledNames, ART_RULED_BYTES, isArtNameId);
|
|
151
156
|
const shownTargets = cleanNames(o.shownTargets, ART_TARGETS_BYTES);
|
|
152
157
|
const keys = typeof o.keys === "boolean" ? o.keys : targets !== "plain";
|
|
153
158
|
// the style and the title, said since that night: as the board, the project's own; the foot's
|
|
154
159
|
// size, said since later that night: the first
|
|
155
160
|
const style = one(ART_STYLES, o.style) ?? "board";
|
|
156
|
-
return { w, h, scheme, theme, background, labels, namesFrom, hiddenNames, shownNames, edges, notes: o.notes, targets, shownTargets, keys, selection: o.selection, source: o.source === true, style, title: cleanTitle(o.title), subtitle: cleanTitle(o.subtitle), titleSize: cleanTitleSize(o.titleSize), footSize: cleanFootSize(o.footSize), border: o.border === true, region: [...r] };
|
|
161
|
+
return { w, h, scheme, theme, background, labels, namesFrom, hiddenNames, shownNames, ruledNames, edges, notes: o.notes, targets, shownTargets, keys, selection: o.selection, source: o.source === true, style, title: cleanTitle(o.title), subtitle: cleanTitle(o.subtitle), titleSize: cleanTitleSize(o.titleSize), footSize: cleanFootSize(o.footSize), border: o.border === true, region: [...r] };
|
|
157
162
|
}
|
|
158
163
|
/** The legend's schemes (CanvasSpec, "Colour schemes"). */
|
|
159
164
|
export const SCHEMES = ["kind", "status"];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// A declaration by the name someone gives (the agents' API, server/agent.ts; the artwork command,
|
|
2
|
+
// server/artcmd.ts): the graph's own name; else the declaration a name it folds is folded into — a
|
|
3
|
+
// declaration Lean generated from a command, or a constructor, recursor, field, equation lemma of
|
|
4
|
+
// one — else, where none is, the names likely meant.
|
|
5
|
+
import { search } from "./search.js";
|
|
6
|
+
/** the last components of names Lean generates beside a declaration (extractor/Extract.lean,
|
|
7
|
+
* `generatedSuffixes`, and the like) */
|
|
8
|
+
export const GENERATED_LAST = /^(?:rec|recOn|casesOn|noConfusion|noConfusionType|ctorElim|ctorElimType|below|brecOn|binductionOn|ibelow|ctorIdx|toCtorIdx|ofNat|ofNat_ctorIdx|sizeOf_spec|injEq|inj|eq_def|induct|mutual_induct|fun_cases|induct_unfolding|ext_iff|congr_simp|mk|elim|(?:eq|match|proof)_\d+|_.*)$/;
|
|
9
|
+
/** The edit distance between two short strings, or `most + 1` once it is more than `most`. */
|
|
10
|
+
export function editDistance(a, b, most) {
|
|
11
|
+
if (Math.abs(a.length - b.length) > most)
|
|
12
|
+
return most + 1;
|
|
13
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
14
|
+
for (let i = 1; i <= a.length; i++) {
|
|
15
|
+
const row = [i];
|
|
16
|
+
let best = i;
|
|
17
|
+
for (let j = 1; j <= b.length; j++) {
|
|
18
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
19
|
+
best = Math.min(best, row[j]);
|
|
20
|
+
}
|
|
21
|
+
if (best > most)
|
|
22
|
+
return most + 1;
|
|
23
|
+
prev = row;
|
|
24
|
+
}
|
|
25
|
+
return Math.min(prev[b.length], most + 1);
|
|
26
|
+
}
|
|
27
|
+
/** The declaration a name the graph has no node for is folded into: a declaration Lean generated
|
|
28
|
+
* from a command (ViewNode.also), or a constructor, recursor, field, equation lemma… of a
|
|
29
|
+
* declaration or of one so generated, which is a name one component below it. A name one
|
|
30
|
+
* component below a declaration that is a typo or two away from a declaration beside it is
|
|
31
|
+
* taken for that typo, not for something folded, unless its last component is one Lean
|
|
32
|
+
* generates. */
|
|
33
|
+
export function foldedAt(g, name) {
|
|
34
|
+
const direct = g.foldedInto.get(name);
|
|
35
|
+
if (direct !== undefined)
|
|
36
|
+
return direct;
|
|
37
|
+
const dot = name.lastIndexOf(".");
|
|
38
|
+
if (dot <= 0)
|
|
39
|
+
return undefined;
|
|
40
|
+
const head = name.slice(0, dot), last = name.slice(dot + 1);
|
|
41
|
+
const at = g.byName.get(head) ?? g.foldedInto.get(head);
|
|
42
|
+
if (at === undefined || !last)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (!GENERATED_LAST.test(last)) {
|
|
45
|
+
const most = last.length < 5 ? 1 : 2;
|
|
46
|
+
const near = (n) => n.length > head.length + 1 && n.startsWith(head + ".") && !n.includes(".", head.length + 1) && editDistance(n.slice(head.length + 1).toLowerCase(), last.toLowerCase(), most) <= most;
|
|
47
|
+
for (const n of g.nodes)
|
|
48
|
+
if (near(n.name))
|
|
49
|
+
return undefined;
|
|
50
|
+
for (const n of g.foldedInto.keys())
|
|
51
|
+
if (near(n))
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return at;
|
|
55
|
+
}
|
|
56
|
+
/** Up to five names like `name`: the same name in another case; then the same last component,
|
|
57
|
+
* or one a typo or two away, in the same namespace first; then what the search finds. */
|
|
58
|
+
export function guessNames(g, index, name) {
|
|
59
|
+
const dot = name.lastIndexOf(".");
|
|
60
|
+
const last = name.slice(dot + 1).toLowerCase();
|
|
61
|
+
const ns = dot < 0 ? "" : name.slice(0, dot);
|
|
62
|
+
const scored = new Map();
|
|
63
|
+
const offer = (n, score) => {
|
|
64
|
+
if ((scored.get(n) ?? Infinity) > score)
|
|
65
|
+
scored.set(n, score);
|
|
66
|
+
};
|
|
67
|
+
const lower = name.toLowerCase();
|
|
68
|
+
const most = last.length < 5 ? 1 : 2;
|
|
69
|
+
// the graph's declarations, and the names Lean generated from a command that they fold
|
|
70
|
+
for (const name of [...g.nodes.map((n) => n.name), ...g.foldedInto.keys()]) {
|
|
71
|
+
if (name.toLowerCase() === lower)
|
|
72
|
+
offer(name, -1);
|
|
73
|
+
const k = name.lastIndexOf(".");
|
|
74
|
+
const d = editDistance(name.slice(k + 1).toLowerCase(), last, most);
|
|
75
|
+
if (d <= most)
|
|
76
|
+
offer(name, 2 * d + (name.slice(0, Math.max(0, k)) === ns ? 0 : 1));
|
|
77
|
+
}
|
|
78
|
+
let rank = 0;
|
|
79
|
+
for (const q of [name.slice(dot + 1), name.replace(/\./g, " ")]) {
|
|
80
|
+
if (q.trim().length < 2)
|
|
81
|
+
continue;
|
|
82
|
+
const f = search(index, q);
|
|
83
|
+
for (const h of [...f.hits.name, ...f.hits.statement].slice(0, 10))
|
|
84
|
+
offer(g.nodes[h.i].name, 10 + rank++);
|
|
85
|
+
}
|
|
86
|
+
return [...scored]
|
|
87
|
+
.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || (a[0] < b[0] ? -1 : 1))
|
|
88
|
+
.slice(0, 5)
|
|
89
|
+
.map(([n]) => n);
|
|
90
|
+
}
|
|
91
|
+
/** The declaration `name` names, its own or the one it is folded into; undefined where none is. */
|
|
92
|
+
export function declAt(g, name) {
|
|
93
|
+
const n = name.trim();
|
|
94
|
+
return g.byName.get(n) ?? foldedAt(g, n);
|
|
95
|
+
}
|
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/pastgraph.js
CHANGED
|
@@ -21,12 +21,12 @@ 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
|
|
25
|
-
*
|
|
26
|
-
function gitThen(
|
|
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
27
|
if (!commit || commit.guessed)
|
|
28
28
|
return { git: undefined };
|
|
29
|
-
return { git: { source:
|
|
29
|
+
return { git: { source: null, commit: commit.hash, branch: commit.branch, ...(commit.dirty ? { dirty: true } : {}) } };
|
|
30
30
|
}
|
|
31
31
|
/** The past graph of snapshot `snap` (its declarations and its edges, as /graph's parts give them),
|
|
32
32
|
* marked by `diff` (from it to the latest), with the project's description of `now`. */
|
|
@@ -40,8 +40,8 @@ export function pastGraph(snap, edges, now, diff) {
|
|
|
40
40
|
const meta = {
|
|
41
41
|
schema: 1,
|
|
42
42
|
layoutVersion: now.meta.layoutVersion,
|
|
43
|
-
// the commit of that build, where it is known
|
|
44
|
-
project: { ...now.meta.project, extractedAt: snap.snapshot.at, ...gitThen(
|
|
43
|
+
// the commit of that build, where it is known
|
|
44
|
+
project: { ...now.meta.project, extractedAt: snap.snapshot.at, ...gitThen(snap.snapshot.commit) },
|
|
45
45
|
modules: snap.modules.map((m) => ({ name: m.name, file: null, imports: m.imports })),
|
|
46
46
|
nodes,
|
|
47
47
|
projectCount: P,
|
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/timeline.js
CHANGED
|
@@ -15,6 +15,11 @@ import { SITE_BODY, SITE_STMT } from "./graph.js";
|
|
|
15
15
|
import { byName } from "./review.js";
|
|
16
16
|
/** at most this many names are listed in one event; the counts stay exact */
|
|
17
17
|
export const NAMES_LISTED = 100;
|
|
18
|
+
/** An event as a read-only reader and an agent are told it (UpdateSpec decision 11): a version set
|
|
19
|
+
* aside without why (what this machine and its servers said of it). */
|
|
20
|
+
export function withoutReasons(e) {
|
|
21
|
+
return e.kind === "tool" && e.setAside ? { ...e, setAside: { version: e.setAside.version } } : e;
|
|
22
|
+
}
|
|
18
23
|
/** The graph as the TCB needs it: names, content hashes, statement and body edges. */
|
|
19
24
|
export class TcbIndex {
|
|
20
25
|
names;
|