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,80 @@
|
|
|
1
|
+
// Shared pins (CollabSpec decision 2), as the server keeps them and the pages change them: per way
|
|
2
|
+
// of clustering, a node's or cluster's place in its parent's coordinates under the name the page
|
|
3
|
+
// knows it by. Operations are applied the same way on both sides; the last drop wins, and a drop
|
|
4
|
+
// moves its pin to the end, so that the order is the order things were dropped in.
|
|
5
|
+
export const CLUSTER_MODES = ["namespace", "module"];
|
|
6
|
+
export const emptyLayouts = () => ({ namespace: { nodes: {}, clusters: {} }, module: { nodes: {}, clusters: {} } });
|
|
7
|
+
/** Why a list of operations is refused, or null. */
|
|
8
|
+
export function invalidOps(ops) {
|
|
9
|
+
if (!Array.isArray(ops) || ops.length === 0 || ops.length > 2000)
|
|
10
|
+
return "ops";
|
|
11
|
+
for (const o of ops) {
|
|
12
|
+
if (!o || typeof o !== "object")
|
|
13
|
+
return "op";
|
|
14
|
+
if (o.op === "clear")
|
|
15
|
+
continue;
|
|
16
|
+
if (o.op !== "pin" && o.op !== "unpin")
|
|
17
|
+
return "op";
|
|
18
|
+
if (!CLUSTER_MODES.includes(o.mode))
|
|
19
|
+
return "mode";
|
|
20
|
+
if (o.kind !== "node" && o.kind !== "cluster")
|
|
21
|
+
return "kind";
|
|
22
|
+
if (typeof o.key !== "string" || !o.key || o.key.length > 2000)
|
|
23
|
+
return "key";
|
|
24
|
+
if (o.op === "pin") {
|
|
25
|
+
const at = o.at;
|
|
26
|
+
if (!Array.isArray(at) || at.length !== 2 || !at.every((v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 1e7))
|
|
27
|
+
return "at";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** Apply operations to `layouts` in place; true when anything changed. */
|
|
33
|
+
export function applyOps(layouts, ops) {
|
|
34
|
+
let changed = false;
|
|
35
|
+
for (const o of ops) {
|
|
36
|
+
if (o.op === "clear") {
|
|
37
|
+
for (const m of CLUSTER_MODES) {
|
|
38
|
+
const p = layouts[m];
|
|
39
|
+
if (Object.keys(p.nodes).length + Object.keys(p.clusters).length)
|
|
40
|
+
changed = true;
|
|
41
|
+
layouts[m] = { nodes: {}, clusters: {} };
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const set = o.kind === "node" ? layouts[o.mode].nodes : layouts[o.mode].clusters;
|
|
46
|
+
if (o.op === "unpin") {
|
|
47
|
+
if (o.key in set) {
|
|
48
|
+
delete set[o.key];
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const at = [Math.round(o.at[0] * 10) / 10, Math.round(o.at[1] * 10) / 10];
|
|
54
|
+
const was = set[o.key];
|
|
55
|
+
if (was && was[0] === at[0] && was[1] === at[1] && Object.keys(set).pop() === o.key)
|
|
56
|
+
continue;
|
|
57
|
+
// dropped again: to the end, as the latest drop
|
|
58
|
+
delete set[o.key];
|
|
59
|
+
set[o.key] = at;
|
|
60
|
+
changed = true;
|
|
61
|
+
}
|
|
62
|
+
return changed;
|
|
63
|
+
}
|
|
64
|
+
/** The operations that turn `before` into `after` (one way of clustering), in drop order. */
|
|
65
|
+
export function diffPins(mode, before, after) {
|
|
66
|
+
const ops = [];
|
|
67
|
+
for (const kind of ["cluster", "node"]) {
|
|
68
|
+
const b = kind === "node" ? before.nodes : before.clusters;
|
|
69
|
+
const a = kind === "node" ? after.nodes : after.clusters;
|
|
70
|
+
for (const key of Object.keys(b))
|
|
71
|
+
if (!(key in a))
|
|
72
|
+
ops.push({ op: "unpin", mode, kind, key });
|
|
73
|
+
for (const [key, at] of Object.entries(a)) {
|
|
74
|
+
const was = b[key];
|
|
75
|
+
if (!was || was[0] !== at[0] || was[1] !== at[1])
|
|
76
|
+
ops.push({ op: "pin", mode, kind, key, at });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return ops;
|
|
80
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// The project's notes (AttachmentSpec decisions 13–19, the owner's of 2026-10-11): a note is the
|
|
2
|
+
// project's, one note, an item that belongs to views as declarations do — the Project holds every
|
|
3
|
+
// one, another view those its scope gives and those added to it (shared/items.ts) — unless it is
|
|
4
|
+
// made a view's own (shared/sticky.ts, a view note). It is kept in the server's state beside the views — not in
|
|
5
|
+
// the review's log — and changed by operations applied the same way on the server and in every page
|
|
6
|
+
// (shared/views.ts, ViewBook), so that it is shared live as the views' operations are. Writing,
|
|
7
|
+
// editing and deleting one are events of the timeline; opening, collapsing and moving one are not.
|
|
8
|
+
//
|
|
9
|
+
// Where a project note is: one place per clustering (decision 15). Under a clustering it is either
|
|
10
|
+
// in a cluster — by the cluster's key and every key around it, each with the note's spot as a
|
|
11
|
+
// fraction of that cluster's radius, so that it is the same spot of the circle on every board
|
|
12
|
+
// whatever the circle's size — or at the top level, on the ring around the board at the angle where
|
|
13
|
+
// it was let go; or it has no place there yet, and is on the ring at the angle of what it is about
|
|
14
|
+
// (decision 16). Moving it on a board sets its place under that board's clustering alone. A note may
|
|
15
|
+
// also be attached to a declaration (the owner's decision of 2026-10-12): a declaration is under
|
|
16
|
+
// both clusterings, so the attachment is its place under every one, and its places are where it
|
|
17
|
+
// falls back to when the declaration is gone with no rename. How a place is read on a board, and
|
|
18
|
+
// which boards show the note, is shared/noteplace.ts's.
|
|
19
|
+
import { cleanCard, cleanCarries, cleanDecl, cleanEdited, invalidCard, invalidCarries, invalidDecl, invalidNoteText, sameCarries, setCard, setCarries, setDecl, stampWho, validNoteId } from "./sticky.js";
|
|
20
|
+
import { validPersonId } from "./review.js";
|
|
21
|
+
export const PROJECT_NOTE_OPS = ["pnote", "pnote-edit", "pnote-move", "pnote-open", "pnote-card", "pnote-decl", "pnote-delete"];
|
|
22
|
+
/** the most notes the project holds: each shows only where what it is about is, so a project holds
|
|
23
|
+
* more than a board (MAX_NOTES), but it is still not a document */
|
|
24
|
+
export const MAX_PROJECT_NOTES = 1000;
|
|
25
|
+
const CLUSTER_MODES = ["namespace", "module"];
|
|
26
|
+
/** how far out a spot's fraction of its radius may be as sent: it is brought inside the circle where
|
|
27
|
+
* it is drawn */
|
|
28
|
+
const FAR = 4;
|
|
29
|
+
/** Why a place under one clustering cannot be taken, or null. */
|
|
30
|
+
export function invalidClusterPlace(x) {
|
|
31
|
+
const p = x;
|
|
32
|
+
if (!p || typeof p !== "object")
|
|
33
|
+
return "place";
|
|
34
|
+
if ("ring" in p)
|
|
35
|
+
return typeof p.ring === "number" && Number.isFinite(p.ring) && Math.abs(p.ring) <= 3600 && Object.keys(p).length === 1 ? null : "place.ring";
|
|
36
|
+
if (!Array.isArray(p.in) || p.in.length === 0 || p.in.length > 64)
|
|
37
|
+
return "place.in";
|
|
38
|
+
for (const e of p.in) {
|
|
39
|
+
if (!Array.isArray(e) || e.length !== 3 || typeof e[0] !== "string" || !e[0] || e[0].length > 2000)
|
|
40
|
+
return "place.in";
|
|
41
|
+
if (![e[1], e[2]].every((v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= FAR))
|
|
42
|
+
return "place.in";
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** Why a note's places cannot be taken, or null. */
|
|
47
|
+
export function invalidPlaces(x) {
|
|
48
|
+
if (!x || typeof x !== "object" || Array.isArray(x))
|
|
49
|
+
return "places";
|
|
50
|
+
for (const [m, p] of Object.entries(x)) {
|
|
51
|
+
if (!CLUSTER_MODES.includes(m))
|
|
52
|
+
return "places";
|
|
53
|
+
const why = invalidClusterPlace(p);
|
|
54
|
+
if (why)
|
|
55
|
+
return why;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Why an operation on the project's notes is refused, or null. */
|
|
60
|
+
export function invalidProjectNoteOp(o) {
|
|
61
|
+
if (o.op === "pnote") {
|
|
62
|
+
const n = o.note;
|
|
63
|
+
if (!n || typeof n !== "object")
|
|
64
|
+
return "note";
|
|
65
|
+
if (!validNoteId(n.id))
|
|
66
|
+
return "note.id";
|
|
67
|
+
const t = invalidNoteText(n.text);
|
|
68
|
+
if (t)
|
|
69
|
+
return t;
|
|
70
|
+
if (typeof n.open !== "boolean")
|
|
71
|
+
return "note.open";
|
|
72
|
+
if (n.card !== undefined && invalidCard(n.card, false))
|
|
73
|
+
return "note.card";
|
|
74
|
+
if (n.decl !== undefined && invalidDecl(n.decl, false))
|
|
75
|
+
return "note.decl";
|
|
76
|
+
return invalidPlaces(n.places) ?? invalidCarries(n.carries);
|
|
77
|
+
}
|
|
78
|
+
if (!validNoteId(o.id))
|
|
79
|
+
return "id";
|
|
80
|
+
if (o.op === "pnote-edit")
|
|
81
|
+
return invalidNoteText(o.text) ?? invalidCarries(o.carries);
|
|
82
|
+
if (o.op === "pnote-move")
|
|
83
|
+
return !CLUSTER_MODES.includes(o.mode) ? "mode" : o.place === null ? null : invalidClusterPlace(o.place);
|
|
84
|
+
if (o.op === "pnote-open")
|
|
85
|
+
return typeof o.open === "boolean" ? null : "open";
|
|
86
|
+
if (o.op === "pnote-card")
|
|
87
|
+
return invalidCard(o.card);
|
|
88
|
+
if (o.op === "pnote-decl")
|
|
89
|
+
return invalidDecl(o.decl);
|
|
90
|
+
return o.op === "pnote-delete" ? null : "op";
|
|
91
|
+
}
|
|
92
|
+
/** Why a list of operations on the project's notes is refused, or null. */
|
|
93
|
+
export function invalidProjectNoteOps(ops) {
|
|
94
|
+
if (!Array.isArray(ops) || ops.length === 0 || ops.length > 400)
|
|
95
|
+
return "ops";
|
|
96
|
+
for (const o of ops) {
|
|
97
|
+
if (!o || typeof o !== "object")
|
|
98
|
+
return "op";
|
|
99
|
+
const why = invalidProjectNoteOp(o);
|
|
100
|
+
if (why)
|
|
101
|
+
return why;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** A place under one clustering as a record keeps it: a ring's angle within a turn, a spot's
|
|
106
|
+
* fractions to the tenth of a percent. */
|
|
107
|
+
export function cleanClusterPlace(p) {
|
|
108
|
+
if ("ring" in p)
|
|
109
|
+
return { ring: Math.round((((p.ring % 360) + 360) % 360) * 10) / 10 };
|
|
110
|
+
const f = (v) => Math.round(v * 1000) / 1000;
|
|
111
|
+
return { in: p.in.map(([k, x, y]) => [k, f(x), f(y)]) };
|
|
112
|
+
}
|
|
113
|
+
/** Apply one operation to the project's notes in place; true when anything changed. A note beyond
|
|
114
|
+
* MAX_PROJECT_NOTES, or one whose id is taken, is not added. */
|
|
115
|
+
export function applyProjectNoteOp(notes, o) {
|
|
116
|
+
if (o.op === "pnote") {
|
|
117
|
+
if (notes.length >= MAX_PROJECT_NOTES || notes.some((n) => n.id === o.note.id))
|
|
118
|
+
return false;
|
|
119
|
+
const n = structuredClone(o.note);
|
|
120
|
+
for (const m of CLUSTER_MODES)
|
|
121
|
+
if (n.places[m])
|
|
122
|
+
n.places[m] = cleanClusterPlace(n.places[m]);
|
|
123
|
+
if (n.decl)
|
|
124
|
+
n.decl = cleanDecl(n.decl).decl;
|
|
125
|
+
if (!n.decl)
|
|
126
|
+
delete n.decl;
|
|
127
|
+
notes.push(n);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const k = notes.findIndex((n) => n.id === o.id);
|
|
131
|
+
if (k < 0)
|
|
132
|
+
return false;
|
|
133
|
+
const n = notes[k];
|
|
134
|
+
switch (o.op) {
|
|
135
|
+
case "pnote-edit":
|
|
136
|
+
if (n.text === o.text && (o.carries === undefined || sameCarries(n.carries, o.carries)))
|
|
137
|
+
return false;
|
|
138
|
+
n.text = o.text;
|
|
139
|
+
if (o.carries !== undefined)
|
|
140
|
+
setCarries(n, o.carries);
|
|
141
|
+
if (o.at)
|
|
142
|
+
n.edited = { by: o.by ?? null, ...(o.agent ? { agent: true } : {}), ...(o.person ? { person: o.person } : {}), at: o.at };
|
|
143
|
+
return true;
|
|
144
|
+
case "pnote-move":
|
|
145
|
+
if (o.place === null) {
|
|
146
|
+
if (!n.places[o.mode])
|
|
147
|
+
return false;
|
|
148
|
+
delete n.places[o.mode];
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
n.places[o.mode] = cleanClusterPlace(o.place);
|
|
152
|
+
if (o.by && o.at)
|
|
153
|
+
n.edited = { by: o.by, at: o.at };
|
|
154
|
+
return true;
|
|
155
|
+
case "pnote-open":
|
|
156
|
+
if (n.open === o.open)
|
|
157
|
+
return false;
|
|
158
|
+
n.open = o.open;
|
|
159
|
+
return true;
|
|
160
|
+
case "pnote-card":
|
|
161
|
+
return setCard(n, o.card);
|
|
162
|
+
case "pnote-decl":
|
|
163
|
+
return setDecl(n, o.decl);
|
|
164
|
+
case "pnote-delete":
|
|
165
|
+
notes.splice(k, 1);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Stamp who does an operation on the project's notes, and when, as for a view note: the author of
|
|
170
|
+
* a new note, the editor of an edit. A page's move says nothing of who: only the server records a
|
|
171
|
+
* carry as its edit. */
|
|
172
|
+
export function stampProjectNoteOp(o, who, at) {
|
|
173
|
+
if (o.op === "pnote") {
|
|
174
|
+
stampWho(o.note, who, at);
|
|
175
|
+
delete o.note.edited;
|
|
176
|
+
}
|
|
177
|
+
else if (o.op === "pnote-edit")
|
|
178
|
+
stampWho(o, who, at);
|
|
179
|
+
else if (o.op === "pnote-move") {
|
|
180
|
+
delete o.by;
|
|
181
|
+
delete o.at;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** The project's notes as a file holds them, checked: one that cannot be read is left out. */
|
|
185
|
+
export function cleanProjectNotes(raw) {
|
|
186
|
+
if (!Array.isArray(raw))
|
|
187
|
+
return [];
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const n of raw) {
|
|
190
|
+
if (!n || typeof n !== "object" || !validNoteId(n.id) || invalidNoteText(n.text) || invalidPlaces(n.places) || typeof n.open !== "boolean")
|
|
191
|
+
continue;
|
|
192
|
+
if (out.some((x) => x.id === n.id) || out.length >= MAX_PROJECT_NOTES)
|
|
193
|
+
continue;
|
|
194
|
+
const places = {};
|
|
195
|
+
for (const m of CLUSTER_MODES) {
|
|
196
|
+
const p = n.places[m];
|
|
197
|
+
if (p)
|
|
198
|
+
places[m] = cleanClusterPlace(p);
|
|
199
|
+
}
|
|
200
|
+
out.push({
|
|
201
|
+
id: n.id,
|
|
202
|
+
text: n.text,
|
|
203
|
+
places,
|
|
204
|
+
open: n.open,
|
|
205
|
+
by: typeof n.by === "string" ? n.by : null,
|
|
206
|
+
...(n.agent === true ? { agent: true } : {}),
|
|
207
|
+
...(validPersonId(n.person) ? { person: n.person } : {}),
|
|
208
|
+
at: typeof n.at === "string" ? n.at : new Date(0).toISOString(),
|
|
209
|
+
...cleanEdited(n.edited),
|
|
210
|
+
...cleanCarries(n.carries),
|
|
211
|
+
...cleanCard(n.card),
|
|
212
|
+
...cleanDecl(n.decl),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/** What both kinds of note have besides where they are: what a switch between them keeps. */
|
|
218
|
+
function common(n) {
|
|
219
|
+
return {
|
|
220
|
+
id: n.id,
|
|
221
|
+
text: n.text,
|
|
222
|
+
open: n.open,
|
|
223
|
+
by: n.by,
|
|
224
|
+
...(n.agent ? { agent: true } : {}),
|
|
225
|
+
...(n.person ? { person: n.person } : {}),
|
|
226
|
+
at: n.at,
|
|
227
|
+
...(n.edited ? { edited: structuredClone(n.edited) } : {}),
|
|
228
|
+
...(n.carries?.length ? { carries: structuredClone(n.carries) } : {}),
|
|
229
|
+
...(n.card ? { card: [n.card[0], n.card[1]] } : {}),
|
|
230
|
+
...(n.decl ? { decl: { name: n.decl.name, at: [n.decl.at[0], n.decl.at[1]] } } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** A project note made a view's own (decision 14, Only in this view): its text, author, state, what
|
|
234
|
+
* it carries, where its card hangs and the declaration it is attached to kept, at `place`, where it
|
|
235
|
+
* is on the board it was switched on — its fallback, where it is attached. */
|
|
236
|
+
export function asViewNote(n, place) {
|
|
237
|
+
return { ...common(n), place: structuredClone(place) };
|
|
238
|
+
}
|
|
239
|
+
/** A view note made the project's (In every view): kept as it is, attached to the declaration it was
|
|
240
|
+
* attached to, at `place` under the clustering of the board it was switched on — where it is there,
|
|
241
|
+
* its fallback where it is attached — and with no place under the other. */
|
|
242
|
+
export function asProjectNote(n, mode, place) {
|
|
243
|
+
return { ...common(n), places: { [mode]: cleanClusterPlace(place) } };
|
|
244
|
+
}
|