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,150 @@
|
|
|
1
|
+
// The lost-and-found (HistorySpec decisions 12–14, the owner's of 2026-10-09): what people and agents
|
|
2
|
+
// said and marked of a declaration the graph no longer has — an opinion with its comment, a target
|
|
3
|
+
// mark, a key mark, a pin on a view, a note's mention of it — that no rename in force carried to a
|
|
4
|
+
// new name. Nothing is deleted when a declaration goes: what was said of it waits here, under the
|
|
5
|
+
// name it was said of, until someone attaches it to a declaration of the graph (a move, HistorySpec
|
|
6
|
+
// decision 9) or archives it, which only hides it.
|
|
7
|
+
//
|
|
8
|
+
// The entries are computed, never stored: from the review and the views as they are, against the
|
|
9
|
+
// graph on show. Each is keyed by what it is and where, so that the same thing is the same entry on
|
|
10
|
+
// every page and in the archive (server/lostfound.ts), which is the one thing kept:
|
|
11
|
+
//
|
|
12
|
+
// opinion:<name>#<by>@<at> an opinion (the reference comments use, shared/comment.ts)
|
|
13
|
+
// target:<name> a target mark
|
|
14
|
+
// key:<target>#<name> a key mark, lost where either name is gone
|
|
15
|
+
// pin:<view>:<mode>:<name> a declaration's pin on a view, under one way of clustering
|
|
16
|
+
// note:<view>:<note id>:<name> a note's mention of a full name in backticks
|
|
17
|
+
// link:<link id> a link into the library (AttachmentSpec decision 6), its
|
|
18
|
+
// declaration gone, or its cluster (listed as `ns(P)`, `file(P)`)
|
|
19
|
+
// move:<migration id> a migration (server/renames.ts), listed beside the lost
|
|
20
|
+
//
|
|
21
|
+
// This file is what the server and the pages share: the entries, their keys, and the shape of the
|
|
22
|
+
// answer the server gives (GET /api/history/lost).
|
|
23
|
+
import { opinionRef } from "./comment.js";
|
|
24
|
+
import { CLUSTER_MODES } from "./places.js";
|
|
25
|
+
import { viewTitle } from "./views.js";
|
|
26
|
+
/** The order the kinds are listed in, under one name. */
|
|
27
|
+
const KIND_ORDER = ["target", "key", "opinion", "link", "note", "pin"];
|
|
28
|
+
/** A name as a note writes one in full between backticks: a dotted or plain Lean name, nothing
|
|
29
|
+
* that reads as code (spaces, operators). */
|
|
30
|
+
const NAME_LIKE = /^[\p{L}_«][^\s`(){}[\],;:=+*<>|&^%$#@~"/\\]*$/u;
|
|
31
|
+
export const lostKey = {
|
|
32
|
+
opinion: (name, by, at) => opinionRef(name, by, at),
|
|
33
|
+
target: (name) => `target:${name}`,
|
|
34
|
+
key: (target, name) => `key:${target}#${name}`,
|
|
35
|
+
pin: (view, mode, name) => `pin:${view}:${mode}:${name}`,
|
|
36
|
+
note: (view, note, name) => `note:${view}:${note}:${name}`,
|
|
37
|
+
link: (id) => `link:${id}`,
|
|
38
|
+
move: (id) => `move:${id}`,
|
|
39
|
+
};
|
|
40
|
+
/** Every entry of the lost-and-found: what the review (`d`) and the views hold of a name the graph
|
|
41
|
+
* on show does not have (`has`). A note's mention counts where its name is not in the graph, no
|
|
42
|
+
* rename in force leads from it to one that is (`d.renamed`), and it was a declaration (`known`:
|
|
43
|
+
* the history had it; without a history, a dotted name). `pinWho` says who dropped a pin, where
|
|
44
|
+
* the caller knows (the server does; pages are not told); `titleOf` what a view is called (the
|
|
45
|
+
* server's titles know the graph's names). A view kept at a past build is a board of that build
|
|
46
|
+
* (HistorySpec decision 19): a pin or a mention of a declaration its build had is not lost
|
|
47
|
+
* (`atBuild`). Sorted by name, then kind, then time. */
|
|
48
|
+
export function lostEntries(d, views, has, known = (n) => n.includes("."), pinWho = () => null, titleOf = (v) => viewTitle(v), atBuild = () => false, notes = [], hasCluster = null) {
|
|
49
|
+
const out = [];
|
|
50
|
+
const who = (x) => ({
|
|
51
|
+
by: x.by,
|
|
52
|
+
...(x.agent ? { agent: true } : {}),
|
|
53
|
+
...(x.person ? { person: x.person } : {}),
|
|
54
|
+
at: x.at,
|
|
55
|
+
});
|
|
56
|
+
for (const [name, ops] of Object.entries(d.opinions)) {
|
|
57
|
+
if (has(name))
|
|
58
|
+
continue;
|
|
59
|
+
for (const o of ops) {
|
|
60
|
+
// an archiving made while the writer went by another name is kept under that name's key
|
|
61
|
+
const was = o.person && Object.hasOwn(d.people, o.person) ? d.people[o.person].was.filter((n) => n !== o.by) : [];
|
|
62
|
+
out.push({
|
|
63
|
+
key: lostKey.opinion(name, o.by, o.at),
|
|
64
|
+
kind: "opinion",
|
|
65
|
+
name,
|
|
66
|
+
...who(o),
|
|
67
|
+
verdict: o.verdict,
|
|
68
|
+
...(o.comment ? { comment: o.comment } : {}),
|
|
69
|
+
...(was.length ? { aliases: was.map((n) => lostKey.opinion(name, n, o.at)) } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const [name, t] of Object.entries(d.targets))
|
|
74
|
+
if (!has(name))
|
|
75
|
+
out.push({ key: lostKey.target(name), kind: "target", name, ...who(t) });
|
|
76
|
+
// a key mark is lost where the declaration marked is gone (as a member), or its target (as a
|
|
77
|
+
// target's own); it is listed under the member's name where that is gone
|
|
78
|
+
for (const [target, marks] of Object.entries(d.keys))
|
|
79
|
+
for (const [member, m] of Object.entries(marks))
|
|
80
|
+
if (!has(member) || !has(target))
|
|
81
|
+
out.push({ key: lostKey.key(target, member), kind: "key", name: has(member) ? target : member, ...who(m), target, member });
|
|
82
|
+
// a link whose declaration is gone (a move would have carried it), or whose cluster is: listed
|
|
83
|
+
// under the declaration's name, or under the cluster as its subject names it
|
|
84
|
+
for (const l of d.links ?? []) {
|
|
85
|
+
const m = /^(decl|ns|file)\((.+)\)$/s.exec(l.subject);
|
|
86
|
+
if (!m)
|
|
87
|
+
continue;
|
|
88
|
+
const gone = m[1] === "decl" ? !has(m[2]) : hasCluster !== null && !hasCluster(m[1], m[2]);
|
|
89
|
+
if (!gone)
|
|
90
|
+
continue;
|
|
91
|
+
const f = d.files && Object.hasOwn(d.files, l.file) ? d.files[l.file] : null;
|
|
92
|
+
out.push({ key: lostKey.link(l.id), kind: "link", name: m[1] === "decl" ? m[2] : l.subject, ...who(l), link: l.id, label: l.label, ...(f ? { fileTitle: f.title } : {}) });
|
|
93
|
+
}
|
|
94
|
+
// the names each name became, through the renames in force: a note may name one before its rename
|
|
95
|
+
const ahead = new Map();
|
|
96
|
+
for (const [to, from] of Object.entries(d.renamed ?? {}))
|
|
97
|
+
ahead.set(from, to);
|
|
98
|
+
const leads = (name) => {
|
|
99
|
+
let n = name;
|
|
100
|
+
for (let k = 0; k < 1000 && ahead.has(n); k++) {
|
|
101
|
+
n = ahead.get(n);
|
|
102
|
+
if (has(n))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
/** the mentions in a note's text of names that are gone, each an entry */
|
|
108
|
+
const mentions = (n, view, title, gone) => {
|
|
109
|
+
const names = new Set();
|
|
110
|
+
for (const m of n.text.matchAll(/`([^`\n]+)`/g))
|
|
111
|
+
names.add(m[1]);
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
if (!NAME_LIKE.test(name) || has(name) || leads(name) || !known(name) || !gone(name))
|
|
114
|
+
continue;
|
|
115
|
+
out.push({ key: lostKey.note(view, n.id, name), kind: "note", name, ...who(n), view, title, note: n.id, text: n.text });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
// the project's notes (AttachmentSpec decision 13): view "", on no board of a past build
|
|
119
|
+
for (const n of notes)
|
|
120
|
+
mentions(n, "", "", () => true);
|
|
121
|
+
for (const v of views) {
|
|
122
|
+
const title = titleOf(v);
|
|
123
|
+
for (const mode of CLUSTER_MODES)
|
|
124
|
+
for (const name of Object.keys(v.pins[mode].nodes)) {
|
|
125
|
+
if (has(name) || atBuild(v, name))
|
|
126
|
+
continue;
|
|
127
|
+
const w = pinWho(v.id, mode, name);
|
|
128
|
+
out.push({ key: lostKey.pin(v.id, mode, name), kind: "pin", name, by: w?.by ?? null, ...(w?.agent ? { agent: true } : {}), at: w?.at ?? "", view: v.id, title, mode });
|
|
129
|
+
}
|
|
130
|
+
for (const n of v.notes ?? [])
|
|
131
|
+
mentions(n, v.id, title, (name) => !atBuild(v, name));
|
|
132
|
+
}
|
|
133
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
134
|
+
return out.sort((a, b) => cmp(a.name, b.name) || KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) || cmp(a.at, b.at) || cmp(a.key, b.key));
|
|
135
|
+
}
|
|
136
|
+
/** What a migration carried, as words: "2 opinions, a target mark, 3 pins". */
|
|
137
|
+
export function carriedText(c) {
|
|
138
|
+
const n = (k, one, many = `${one}s`) => (k ? (k === 1 ? `1 ${one}` : `${k} ${many}`) : "");
|
|
139
|
+
const parts = [
|
|
140
|
+
n(c.opinions, "opinion"),
|
|
141
|
+
n(c.targets, "target mark"),
|
|
142
|
+
n(c.keys, "key mark"),
|
|
143
|
+
n(c.links ?? 0, "link"),
|
|
144
|
+
n(c.reviewViews, "target's view", "targets' views"),
|
|
145
|
+
n(c.pins, "pin"),
|
|
146
|
+
n(c.notes, "note"),
|
|
147
|
+
n(c.scopes, "view's scope", "views' scopes"),
|
|
148
|
+
].filter(Boolean);
|
|
149
|
+
return parts.length ? parts.join(", ") : "nothing said of it: its identity alone";
|
|
150
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Moves (HistorySpec.md, the owner's decisions of 2026-10-09): what was said about a declaration
|
|
2
|
+
// follows it to another name. A move is an event of the review's logs, appended like any other and
|
|
3
|
+
// never rewritten: replayed, it carries the opinions given on `from` (their comments with them), its
|
|
4
|
+
// target mark and the key marks on it — as a declaration of a target's review, and as a target —
|
|
5
|
+
// over to `to`. Each thing carried keeps the names it was on before (`was`), so that undoing a move,
|
|
6
|
+
// an un-move (`on: false`), takes back exactly what that move carried and leaves what was said of
|
|
7
|
+
// `to` since. A move the server found as a rename, or one a person made onto a declaration that
|
|
8
|
+
// appeared as the other went, also says that `to` *is* `from` renamed (`rename`): the version of
|
|
9
|
+
// whatever refers to it is then made with `from`'s identity (TCBSpec decision 3), so that renaming
|
|
10
|
+
// it stales nothing. Since 2026-10-11 a move carries the links on the declaration too
|
|
11
|
+
// (AttachmentSpec decision 6): a link on `decl(from)` is on `decl(to)` after it, its `was` saying
|
|
12
|
+
// where it was. Moving a note's mention, a pin or a view's scope is the views' business
|
|
13
|
+
// (server/views.ts); this file is what the server and the pages replay.
|
|
14
|
+
//
|
|
15
|
+
// Declarations are keyed by name; every object keyed by names here has no prototype, as in
|
|
16
|
+
// shared/review.ts.
|
|
17
|
+
/** A declaration as a link's subject names it (shared/library.ts, declSubject). */
|
|
18
|
+
const declOf = (name) => `decl(${name})`;
|
|
19
|
+
const own = (o, k) => Object.hasOwn(o, k);
|
|
20
|
+
const noProto = () => Object.create(null);
|
|
21
|
+
const byTime = (xs) => xs.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0));
|
|
22
|
+
/** One move replayed onto a review. */
|
|
23
|
+
export function applyMove(d, e) {
|
|
24
|
+
if (e.from === e.to)
|
|
25
|
+
return;
|
|
26
|
+
if (e.on)
|
|
27
|
+
carry(d, e.from, e.to);
|
|
28
|
+
else
|
|
29
|
+
takeBack(d, e.from, e.to);
|
|
30
|
+
if (!e.rename)
|
|
31
|
+
return;
|
|
32
|
+
const links = (d.renamed ??= noProto());
|
|
33
|
+
if (e.on) {
|
|
34
|
+
// renamed back (`to` is where `from` came from): `to` is itself again, and `from` is no one's
|
|
35
|
+
if (identityIn(links, e.from) === e.to) {
|
|
36
|
+
delete links[e.to];
|
|
37
|
+
delete links[e.from];
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
links[e.to] = e.from;
|
|
41
|
+
}
|
|
42
|
+
else if (own(links, e.to) && links[e.to] === e.from)
|
|
43
|
+
delete links[e.to];
|
|
44
|
+
}
|
|
45
|
+
function carry(d, from, to) {
|
|
46
|
+
const moved = (x) => ({ ...x, was: [...(x.was ?? []), from] });
|
|
47
|
+
if (own(d.opinions, from)) {
|
|
48
|
+
const os = d.opinions[from].map(moved);
|
|
49
|
+
delete d.opinions[from];
|
|
50
|
+
d.opinions[to] = byTime([...(own(d.opinions, to) ? d.opinions[to] : []), ...os]);
|
|
51
|
+
}
|
|
52
|
+
if (own(d.targets, from)) {
|
|
53
|
+
if (!own(d.targets, to))
|
|
54
|
+
d.targets[to] = moved(d.targets[from]);
|
|
55
|
+
delete d.targets[from];
|
|
56
|
+
}
|
|
57
|
+
// a key mark on it, in each target's review
|
|
58
|
+
for (const t of Object.keys(d.keys)) {
|
|
59
|
+
const ks = d.keys[t];
|
|
60
|
+
if (!own(ks, from))
|
|
61
|
+
continue;
|
|
62
|
+
if (!own(ks, to))
|
|
63
|
+
ks[to] = moved(ks[from]);
|
|
64
|
+
delete ks[from];
|
|
65
|
+
}
|
|
66
|
+
// its own key marks, as a target
|
|
67
|
+
if (own(d.keys, from)) {
|
|
68
|
+
const ks = (own(d.keys, to) ? d.keys[to] : (d.keys[to] = noProto()));
|
|
69
|
+
for (const [n, m] of Object.entries(d.keys[from]))
|
|
70
|
+
if (n !== to && !own(ks, n))
|
|
71
|
+
ks[n] = { ...m, wasOf: [...(m.wasOf ?? []), from] };
|
|
72
|
+
delete d.keys[from];
|
|
73
|
+
}
|
|
74
|
+
// the links on it, in the order they were made
|
|
75
|
+
if (d.links)
|
|
76
|
+
d.links = d.links.map((l) => (l.subject === declOf(from) ? { ...moved(l), subject: declOf(to) } : l));
|
|
77
|
+
}
|
|
78
|
+
function takeBack(d, from, to) {
|
|
79
|
+
const came = (x) => x.was?.[x.was.length - 1] === from;
|
|
80
|
+
const back = (x) => {
|
|
81
|
+
const was = x.was.slice(0, -1);
|
|
82
|
+
const y = { ...x };
|
|
83
|
+
if (was.length)
|
|
84
|
+
y.was = was;
|
|
85
|
+
else
|
|
86
|
+
delete y.was;
|
|
87
|
+
return y;
|
|
88
|
+
};
|
|
89
|
+
if (own(d.opinions, to)) {
|
|
90
|
+
const all = d.opinions[to];
|
|
91
|
+
const os = all.filter(came);
|
|
92
|
+
if (os.length) {
|
|
93
|
+
const stay = all.filter((o) => !came(o));
|
|
94
|
+
if (stay.length)
|
|
95
|
+
d.opinions[to] = stay;
|
|
96
|
+
else
|
|
97
|
+
delete d.opinions[to];
|
|
98
|
+
d.opinions[from] = byTime([...(own(d.opinions, from) ? d.opinions[from] : []), ...os.map(back)]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (own(d.targets, to) && came(d.targets[to])) {
|
|
102
|
+
if (!own(d.targets, from))
|
|
103
|
+
d.targets[from] = back(d.targets[to]);
|
|
104
|
+
delete d.targets[to];
|
|
105
|
+
}
|
|
106
|
+
for (const t of Object.keys(d.keys)) {
|
|
107
|
+
const ks = d.keys[t];
|
|
108
|
+
if (!own(ks, to) || !came(ks[to]))
|
|
109
|
+
continue;
|
|
110
|
+
if (!own(ks, from))
|
|
111
|
+
ks[from] = back(ks[to]);
|
|
112
|
+
delete ks[to];
|
|
113
|
+
}
|
|
114
|
+
if (own(d.keys, to)) {
|
|
115
|
+
const ks = d.keys[to];
|
|
116
|
+
const marks = Object.entries(ks).filter(([, m]) => m.wasOf?.[m.wasOf.length - 1] === from);
|
|
117
|
+
if (marks.length) {
|
|
118
|
+
const into = own(d.keys, from) ? d.keys[from] : (d.keys[from] = noProto());
|
|
119
|
+
for (const [n, m] of marks) {
|
|
120
|
+
const wasOf = m.wasOf.slice(0, -1);
|
|
121
|
+
const y = { ...m };
|
|
122
|
+
if (wasOf.length)
|
|
123
|
+
y.wasOf = wasOf;
|
|
124
|
+
else
|
|
125
|
+
delete y.wasOf;
|
|
126
|
+
if (!own(into, n))
|
|
127
|
+
into[n] = y;
|
|
128
|
+
delete ks[n];
|
|
129
|
+
}
|
|
130
|
+
if (!Object.keys(ks).length)
|
|
131
|
+
delete d.keys[to];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (d.links)
|
|
135
|
+
d.links = d.links.map((l) => (l.subject === declOf(to) && came(l) ? { ...back(l), subject: declOf(from) } : l));
|
|
136
|
+
}
|
|
137
|
+
/** The identity of a unit's name under the renames in force (`renamed`: `to` → `from`): the first
|
|
138
|
+
* name it had, followed back through each rename. */
|
|
139
|
+
export function identityIn(renamed, name) {
|
|
140
|
+
if (!renamed)
|
|
141
|
+
return name;
|
|
142
|
+
let n = name;
|
|
143
|
+
// a chain is as long as the renames of one declaration; a loop cannot be made (applyMove), but a
|
|
144
|
+
// log edited by hand could hold one
|
|
145
|
+
for (let k = 0; k < 1000 && own(renamed, n); k++) {
|
|
146
|
+
const p = renamed[n];
|
|
147
|
+
if (p === name)
|
|
148
|
+
break;
|
|
149
|
+
n = p;
|
|
150
|
+
}
|
|
151
|
+
return n;
|
|
152
|
+
}
|
|
153
|
+
/** Whether a move would change nothing: nothing to carry or take back, and no identity to link or
|
|
154
|
+
* unlink. Such a request writes nothing. */
|
|
155
|
+
export function moveIsIdle(d, e) {
|
|
156
|
+
if (e.from === e.to)
|
|
157
|
+
return true;
|
|
158
|
+
const came = (x) => x.was?.[x.was.length - 1] === e.from;
|
|
159
|
+
const links = d.renamed ?? noProto();
|
|
160
|
+
if (e.on) {
|
|
161
|
+
const carries = own(d.opinions, e.from) || own(d.targets, e.from) || own(d.keys, e.from) || Object.values(d.keys).some((ks) => own(ks, e.from)) || !!d.links?.some((l) => l.subject === declOf(e.from));
|
|
162
|
+
return !carries && (!e.rename || (own(links, e.to) && links[e.to] === e.from));
|
|
163
|
+
}
|
|
164
|
+
const back = (own(d.opinions, e.to) && d.opinions[e.to].some(came)) ||
|
|
165
|
+
(own(d.targets, e.to) && came(d.targets[e.to])) ||
|
|
166
|
+
Object.values(d.keys).some((ks) => own(ks, e.to) && came(ks[e.to])) ||
|
|
167
|
+
(own(d.keys, e.to) && Object.values(d.keys[e.to]).some((m) => m.wasOf?.[m.wasOf.length - 1] === e.from)) ||
|
|
168
|
+
!!d.links?.some((l) => l.subject === declOf(e.to) && came(l));
|
|
169
|
+
return !back && !(e.rename && own(links, e.to) && links[e.to] === e.from);
|
|
170
|
+
}
|
|
171
|
+
/** A move's line, read: the event, or why it is not one. */
|
|
172
|
+
export function readMove(o, at, by, agent) {
|
|
173
|
+
const str = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
174
|
+
if (!str(o.from, 2000))
|
|
175
|
+
return "from";
|
|
176
|
+
if (!str(o.to, 2000))
|
|
177
|
+
return "to";
|
|
178
|
+
if (o.from === o.to)
|
|
179
|
+
return "a move to the same name";
|
|
180
|
+
if (typeof o.on !== "boolean")
|
|
181
|
+
return "on";
|
|
182
|
+
if (o.rename !== undefined && o.rename !== true)
|
|
183
|
+
return "rename";
|
|
184
|
+
if (o.auto !== undefined && !str(o.auto, 200))
|
|
185
|
+
return "auto";
|
|
186
|
+
if (o.snap !== undefined && !(Number.isInteger(o.snap) && o.snap > 0))
|
|
187
|
+
return "snap";
|
|
188
|
+
return {
|
|
189
|
+
op: "move",
|
|
190
|
+
at,
|
|
191
|
+
by,
|
|
192
|
+
from: o.from,
|
|
193
|
+
to: o.to,
|
|
194
|
+
on: o.on,
|
|
195
|
+
...(o.rename ? { rename: true } : {}),
|
|
196
|
+
...(o.auto !== undefined ? { auto: o.auto } : {}),
|
|
197
|
+
...(o.snap !== undefined ? { snap: o.snap } : {}),
|
|
198
|
+
...(agent ? { agent: true } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** A view's scope with declaration `from` named `to` instead: in `decl(…)`, the roots of a closure,
|
|
202
|
+
* and the targets of `key(…)`. The same object where it does not name it. */
|
|
203
|
+
export function renameInQuery(q, from, to) {
|
|
204
|
+
const names = (xs) => (xs.includes(from) ? [...new Set(xs.map((x) => (x === from ? to : x)))] : xs);
|
|
205
|
+
switch (q.op) {
|
|
206
|
+
case "decl": {
|
|
207
|
+
const n = names(q.names);
|
|
208
|
+
return n === q.names ? q : { ...q, names: n };
|
|
209
|
+
}
|
|
210
|
+
case "tcb":
|
|
211
|
+
case "uses":
|
|
212
|
+
case "usedBy": {
|
|
213
|
+
if (q.of === "selection")
|
|
214
|
+
return q;
|
|
215
|
+
if (Array.isArray(q.of)) {
|
|
216
|
+
const n = names(q.of);
|
|
217
|
+
return n === q.of ? q : { ...q, of: n };
|
|
218
|
+
}
|
|
219
|
+
const of = renameInQuery(q.of, from, to);
|
|
220
|
+
return of === q.of ? q : { ...q, of };
|
|
221
|
+
}
|
|
222
|
+
case "key": {
|
|
223
|
+
if (q.of === "targets")
|
|
224
|
+
return q;
|
|
225
|
+
const n = names(q.of);
|
|
226
|
+
return n === q.of ? q : { ...q, of: n };
|
|
227
|
+
}
|
|
228
|
+
case "and":
|
|
229
|
+
case "or":
|
|
230
|
+
case "except": {
|
|
231
|
+
const a = renameInQuery(q.a, from, to);
|
|
232
|
+
const b = renameInQuery(q.b, from, to);
|
|
233
|
+
return a === q.a && b === q.b ? q : { ...q, a, b };
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
return q;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** A note's text with declaration `from`, written in full between backticks, named `to` instead
|
|
240
|
+
* (`` `A.f` `` → `` `A.g` ``); the same text where it does not mention it so. */
|
|
241
|
+
export function renameInNote(text, from, to) {
|
|
242
|
+
return text.includes("`" + from + "`") ? text.split("`" + from + "`").join("`" + to + "`") : text;
|
|
243
|
+
}
|