@openlfcp/shared-objects 0.1.5 → 0.2.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +15 -2
  2. package/dist/admission/actor.d.ts +16 -0
  3. package/dist/admission/actor.js +35 -0
  4. package/dist/{canonical.d.ts → admission/canonical.d.ts} +9 -0
  5. package/dist/{canonical.js → admission/canonical.js} +43 -3
  6. package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +41 -1
  7. package/dist/admission/framing.js +242 -0
  8. package/dist/admission/index.d.ts +26 -0
  9. package/dist/admission/index.js +26 -0
  10. package/dist/{chunk-limits.d.ts → admission/limits.d.ts} +11 -1
  11. package/dist/{chunk-limits.js → admission/limits.js} +25 -10
  12. package/dist/{references.d.ts → admission/references.d.ts} +1 -1
  13. package/dist/{references.js → admission/references.js} +9 -3
  14. package/dist/admission/sequence.d.ts +81 -0
  15. package/dist/admission/sequence.js +147 -0
  16. package/dist/concurrent.d.ts +15 -0
  17. package/dist/concurrent.js +55 -0
  18. package/dist/data-profile.d.ts +1 -1
  19. package/dist/data-profile.js +3 -7
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/replica.d.ts +32 -2
  23. package/dist/replica.js +165 -209
  24. package/dist/sections/admission.d.ts +35 -0
  25. package/dist/sections/admission.js +328 -0
  26. package/dist/sections/data-profile.d.ts +195 -0
  27. package/dist/sections/data-profile.js +383 -0
  28. package/dist/sections/dispatch.d.ts +41 -0
  29. package/dist/sections/dispatch.js +33 -0
  30. package/dist/sections/document.d.ts +54 -0
  31. package/dist/sections/document.js +101 -0
  32. package/dist/sections/index.d.ts +18 -0
  33. package/dist/sections/index.js +18 -0
  34. package/dist/sections/replica.d.ts +406 -0
  35. package/dist/sections/replica.js +1227 -0
  36. package/dist/sections/schema.d.ts +77 -0
  37. package/dist/sections/schema.js +548 -0
  38. package/dist/sections/tree.d.ts +83 -0
  39. package/dist/sections/tree.js +401 -0
  40. package/dist/sections/values.d.ts +30 -0
  41. package/dist/sections/values.js +36 -0
  42. package/dist/values.d.ts +3 -18
  43. package/dist/values.js +7 -95
  44. package/package.json +12 -4
  45. package/dist/automerge-bytes.js +0 -111
@@ -0,0 +1,83 @@
1
+ import * as A from "@automerge/automerge";
2
+ import { type SectionValidation } from "./schema.js";
3
+ /**
4
+ * The effective tree of a section and its structural facts
5
+ * (SHARED-SECTIONS-PROFILE-01 §7, §9, §14.3; LFCP-02-014), derived from the
6
+ * fully merged state, never from arrival order:
7
+ *
8
+ * - a node with more than one concurrent placement is PLACEMENT_CONFLICT;
9
+ * its lifecycle (a Task node's: its Task's) with concurrent different
10
+ * values is LIFECYCLE_CONFLICT (equal values agree);
11
+ * - every member of a cycle of the selected parent graph is PARENT_CYCLE;
12
+ * - every node under a conflicted, cyclic, invalid or collided node is
13
+ * BLOCKED_PARENT, to a fixed point;
14
+ * - a deleted node, and every node under one, is hidden; a node whose
15
+ * lifecycle is in conflict is not: its branch is blocked (§7.6);
16
+ * - the tree scans each children list in its merged order and emits a node
17
+ * only when it is eligible, visible and selects exactly that entry.
18
+ *
19
+ * No winner is chosen and nothing is rewritten: blocked and hidden content
20
+ * stays in the document, and recovery lists it. Traversals are iterative,
21
+ * so deep or adversarial graphs do not depend on the call stack (§16.4).
22
+ */
23
+ export type StructuralFact = "PLACEMENT_CONFLICT" | "PARENT_CYCLE" | "BLOCKED_PARENT" | "LIFECYCLE_CONFLICT";
24
+ export interface TreeEntry {
25
+ readonly id: string;
26
+ /** The section ID or the parent node's ID. */
27
+ readonly parent: string;
28
+ /** 0 under the section. */
29
+ readonly depth: number;
30
+ readonly kind: string;
31
+ }
32
+ export interface SectionTree {
33
+ /**
34
+ * PROFILE_INVALID: section-level problems, nothing projected; IMPORTING:
35
+ * no `ready` (§12.1), nothing projected; STRUCTURAL_ATTENTION: a
36
+ * recovery fact, an invalid node or a collision; VALID otherwise.
37
+ */
38
+ readonly classification: "PROFILE_INVALID" | "IMPORTING" | "STRUCTURAL_ATTENTION" | "VALID";
39
+ /** The visible nodes in projection order (preorder). */
40
+ readonly tree: readonly TreeEntry[];
41
+ /** Nodes hidden by their own or an ancestor's deletion, sorted. */
42
+ readonly hidden: readonly string[];
43
+ /** Blocked nodes with their fact, sorted by node ID for presentation only. */
44
+ readonly recovery: readonly {
45
+ readonly id: string;
46
+ readonly code: StructuralFact;
47
+ }[];
48
+ /** Invalid nodes with their §14.2 diagnostic, sorted. */
49
+ readonly invalid: readonly {
50
+ readonly id: string;
51
+ readonly diagnostic: string;
52
+ }[];
53
+ /** Colliding IDs (§14.2), sorted. */
54
+ readonly collisions: readonly string[];
55
+ /**
56
+ * §9, §14.3 EDIT_UNDER_DELETED_ANCESTOR: hidden nodes whose content (the
57
+ * node created, its Text edited, its Task's title or status set) changed
58
+ * concurrently with the deletion of the node or an ancestor; sorted. The
59
+ * edit is retained, not visibly applied: it needs the user's attention.
60
+ */
61
+ readonly retainedConcurrentEdits: readonly string[];
62
+ /**
63
+ * SOP §44–§47: the Task scalar fields with concurrent values (title,
64
+ * status, lifecycle, due, priority), each with its values sorted. A
65
+ * provisional value may show; the conflict stays visible until a user
66
+ * resolves it.
67
+ */
68
+ readonly scalarConflicts: readonly {
69
+ readonly id: string;
70
+ readonly field: string;
71
+ readonly values: readonly string[];
72
+ }[];
73
+ /** Each blocked node's candidate placements and their parents (§7 recovery information). */
74
+ readonly candidates: ReadonlyMap<string, readonly {
75
+ readonly placement: string;
76
+ readonly parent: string;
77
+ }[]>;
78
+ /** The schema validation the tree was derived from. */
79
+ readonly validation: SectionValidation;
80
+ }
81
+ /** §7: the effective tree and structural facts of `doc`. Reads only. */
82
+ export declare function deriveTree(doc: A.Doc<unknown>, validation?: SectionValidation): SectionTree;
83
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1,401 @@
1
+ import * as A from "@automerge/automerge";
2
+ import { distinctValues } from "../concurrent.js";
3
+ import { validateSection } from "./schema.js";
4
+ /** The Task scalar fields whose concurrent values tree() reports. */
5
+ const CONFLICT_FIELDS = ["title", "status", "lifecycle", "due", "priority"];
6
+ const str = (v) => A.isImmutableString(v) ? v.toString() : typeof v === "string" ? v : undefined;
7
+ /** The distinct concurrent values of `map[key]` in `doc`, as strings (SOP §45: equal values agree). */
8
+ function values(doc, map, key) {
9
+ return distinctValues(doc, map, key).map(str);
10
+ }
11
+ const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
12
+ /** §7: the effective tree and structural facts of `doc`. Reads only. */
13
+ export function deriveTree(doc, validation = validateSection(doc)) {
14
+ const root = doc;
15
+ const nodes = (root.nodes ?? {});
16
+ const placements = (root.placements ?? {});
17
+ const objects = (root.objects ?? {});
18
+ const sectionId = validation.sectionId;
19
+ const invalid = validation.nodes;
20
+ const collided = new Set(validation.collided);
21
+ const blocked = new Map();
22
+ const parents = new Map();
23
+ const candidates = new Map();
24
+ const keys = Object.keys(nodes).sort();
25
+ const node = (id) => nodes[id];
26
+ const owner = (id) => (str(node(id)?.kind) === "task" ? objects[id] : node(id));
27
+ // One pass over the nodes: selected parent, deletion, conflicts.
28
+ const deleted = new Set();
29
+ for (const id of keys) {
30
+ // §7.6: a lifecycle conflict has no winner. It blocks the branch (below)
31
+ // instead of the provisional value hiding it: the node is not deleted,
32
+ // and edits under it are not under a deleted ancestor.
33
+ const lifecycles = new Set(values(doc, owner(id), "lifecycle"));
34
+ if (lifecycles.size === 1 && lifecycles.has("deleted"))
35
+ deleted.add(id);
36
+ if (collided.has(id))
37
+ continue;
38
+ const n = node(id);
39
+ const selected = values(doc, n, "placement");
40
+ const parentOf = (p) => p === undefined ? undefined : str(placements[p]?.parent_id);
41
+ // The default value selects the parent used for hiding and for the walk below.
42
+ const parent = parentOf(str(n.placement));
43
+ if (parent !== undefined)
44
+ parents.set(id, parent);
45
+ if (selected.length > 1) {
46
+ blocked.set(id, "PLACEMENT_CONFLICT");
47
+ candidates.set(id, selected.map((p) => ({ placement: p ?? "", parent: parentOf(p) ?? "" })));
48
+ }
49
+ if (lifecycles.size > 1)
50
+ blocked.set(id, "LIFECYCLE_CONFLICT");
51
+ }
52
+ for (const id of invalid.keys())
53
+ blocked.delete(id);
54
+ // Every member of a cycle of the selected parent graph (§7.4), whatever
55
+ // the start: each node has one parent, so one coloured walk per start
56
+ // that stops at any node already done finds them all in linear time.
57
+ const done = new Set();
58
+ for (const start of keys) {
59
+ const path = [];
60
+ const onPath = new Map();
61
+ let n = start;
62
+ while (n !== undefined &&
63
+ n !== sectionId &&
64
+ node(n) !== undefined &&
65
+ !done.has(n) &&
66
+ !blocked.has(n) &&
67
+ !collided.has(n) &&
68
+ !invalid.has(n)) {
69
+ const at = onPath.get(n);
70
+ if (at !== undefined) {
71
+ for (const c of path.slice(at))
72
+ blocked.set(c, "PARENT_CYCLE");
73
+ break;
74
+ }
75
+ onPath.set(n, path.length);
76
+ path.push(n);
77
+ n = parents.get(n);
78
+ }
79
+ for (const c of path)
80
+ done.add(c);
81
+ }
82
+ /**
83
+ * Resolves `fact(id)` = own(id) || fact(parent) along the parent chain,
84
+ * iteratively and memoized; a walk that comes back to a node of its own
85
+ * path stops there, with what the loop holds.
86
+ */
87
+ const chained = (own) => {
88
+ const memo = new Map();
89
+ return (id) => {
90
+ const path = [];
91
+ const onPath = new Set();
92
+ let tail = false;
93
+ let n = id;
94
+ while (n !== undefined && n !== sectionId && node(n) !== undefined) {
95
+ const known = memo.get(n);
96
+ if (known !== undefined) {
97
+ tail = known;
98
+ break;
99
+ }
100
+ if (onPath.has(n)) {
101
+ // A loop: every node of it sees all of it.
102
+ tail = path.slice(path.indexOf(n)).some(own);
103
+ break;
104
+ }
105
+ onPath.add(n);
106
+ path.push(n);
107
+ n = parents.get(n);
108
+ }
109
+ for (let k = path.length - 1; k >= 0; k--) {
110
+ tail = tail || own(path[k]);
111
+ memo.set(path[k], tail);
112
+ }
113
+ return memo.get(id) ?? tail;
114
+ };
115
+ };
116
+ // §7.5: blocked structure propagates to every descendant.
117
+ const out = (id) => id !== undefined && (blocked.has(id) || invalid.has(id) || collided.has(id));
118
+ const isOut = new Set(keys.filter((id) => out(id)));
119
+ const blockedAbove = chained((id) => isOut.has(id));
120
+ for (const id of keys) {
121
+ const parent = parents.get(id);
122
+ if (!isOut.has(id) && parent !== undefined && blockedAbove(parent))
123
+ blocked.set(id, "BLOCKED_PARENT");
124
+ }
125
+ // §9: a deleted node or a descendant of one is hidden, not removed.
126
+ const isHidden = chained((id) => deleted.has(id));
127
+ const hidden = new Set(keys.filter((id) => isHidden(id)));
128
+ const retained = hidden.size === 0 ? [] : retainedEdits(doc, hidden, (id) => parents.get(id), sectionId);
129
+ const tree = [];
130
+ const projectable = validation.state === "ready" && sectionId !== undefined;
131
+ if (projectable) {
132
+ const laneOf = (p) => ((p === sectionId ? root.section.children : node(p)?.children) ?? []);
133
+ // An explicit stack of (parent, depth, next index) instead of recursion.
134
+ const stack = [
135
+ { parent: sectionId, depth: 0, lane: laneOf(sectionId), i: 0 },
136
+ ];
137
+ const emitted = new Set();
138
+ while (stack.length > 0) {
139
+ const top = stack[stack.length - 1];
140
+ if (top.i >= top.lane.length) {
141
+ stack.pop();
142
+ continue;
143
+ }
144
+ const slot = str(top.lane[top.i++]);
145
+ if (slot === undefined || collided.has(slot))
146
+ continue;
147
+ const id = str(placements[slot]?.node_id);
148
+ if (id === undefined || node(id) === undefined || out(id) || hidden.has(id))
149
+ continue;
150
+ if (str(node(id)?.placement) !== slot || emitted.has(id))
151
+ continue;
152
+ emitted.add(id);
153
+ tree.push(Object.freeze({
154
+ id,
155
+ parent: top.parent,
156
+ depth: top.depth,
157
+ kind: str(node(id)?.kind) ?? "",
158
+ }));
159
+ stack.push({ parent: id, depth: top.depth + 1, lane: laneOf(id), i: 0 });
160
+ }
161
+ }
162
+ const classification = validation.state === "invalid"
163
+ ? "PROFILE_INVALID"
164
+ : validation.state === "importing"
165
+ ? "IMPORTING"
166
+ : blocked.size > 0 || invalid.size > 0 || validation.collisions.length > 0
167
+ ? "STRUCTURAL_ATTENTION"
168
+ : "VALID";
169
+ return Object.freeze({
170
+ classification,
171
+ tree: Object.freeze(tree),
172
+ hidden: Object.freeze([...hidden].sort()),
173
+ recovery: Object.freeze([...blocked].map(([id, code]) => ({ id, code })).sort(byId)),
174
+ invalid: Object.freeze([...invalid].map(([id, p]) => ({ id, diagnostic: p.diagnostic })).sort(byId)),
175
+ collisions: validation.collisions,
176
+ retainedConcurrentEdits: Object.freeze(retained),
177
+ scalarConflicts: Object.freeze(Object.keys(objects).flatMap((id) => CONFLICT_FIELDS.flatMap((field) => {
178
+ const vs = values(doc, objects[id], field);
179
+ return vs.length > 1 ? [{ id, field, values: vs.map((v) => v ?? "").sort() }] : [];
180
+ }))),
181
+ candidates,
182
+ validation,
183
+ });
184
+ }
185
+ const summarise = (bytes) => {
186
+ const c = A.decodeChange(bytes);
187
+ return {
188
+ hash: c.hash,
189
+ deps: c.deps,
190
+ actor: c.actor,
191
+ seq: c.seq,
192
+ ops: c.ops.map((op) => {
193
+ const o = op;
194
+ return { obj: o.obj, action: o.action, key: o.key, value: o.value };
195
+ }),
196
+ };
197
+ };
198
+ /**
199
+ * The decoded history of the documents read last, by their heads (a few,
200
+ * least recently used last out). A change is the same wherever it is, so a
201
+ * later revision of the same document decodes only the changes since a
202
+ * revision read before, whose heads it has: decoding the whole history on
203
+ * every new revision was linear in it, per pass (soak #11).
204
+ */
205
+ const histories = new Map();
206
+ const HISTORIES = 16;
207
+ function historyOf(doc) {
208
+ const key = [...A.getHeads(doc)].sort().join(",");
209
+ const known = histories.get(key);
210
+ if (known !== undefined) {
211
+ histories.delete(key);
212
+ histories.set(key, known);
213
+ return known;
214
+ }
215
+ let out;
216
+ for (const [k, list] of [...histories].reverse()) {
217
+ const heads = k === "" ? [] : k.split(",");
218
+ if (A.getMissingDeps(doc, heads).length > 0)
219
+ continue;
220
+ out = [...list, ...A.getChangesSince(doc, heads).map(summarise)];
221
+ break;
222
+ }
223
+ out ??= A.getAllChanges(doc).map(summarise);
224
+ histories.set(key, out);
225
+ while (histories.size > HISTORIES)
226
+ histories.delete(histories.keys().next().value);
227
+ return out;
228
+ }
229
+ function retainedEdits(doc, hidden, parentOf, sectionId) {
230
+ const root = doc;
231
+ const nodes = (root.nodes ?? {});
232
+ const objects = (root.objects ?? {});
233
+ const nodesObj = A.getObjectId(root, "nodes");
234
+ // Object IDs to the node whose content or lifecycle they hold.
235
+ const textOf = new Map();
236
+ const ownerOf = new Map();
237
+ const taskOf = new Map();
238
+ // The object ID of each node's own map, for a move written to its `placement`.
239
+ const nodeMapOf = new Map();
240
+ for (const id of Object.keys(nodes)) {
241
+ const n = nodes[id];
242
+ const nodeMap = A.getObjectId(n);
243
+ if (nodeMap !== null)
244
+ nodeMapOf.set(nodeMap, id);
245
+ const text = A.getObjectId(n, "text");
246
+ if (text !== null)
247
+ textOf.set(text, id);
248
+ const isTask = str(n.kind) === "task";
249
+ const task = isTask ? objects[id] : undefined;
250
+ const owner = isTask ? (task === undefined ? null : A.getObjectId(task)) : A.getObjectId(n);
251
+ if (owner !== null)
252
+ ownerOf.set(owner, id);
253
+ if (task !== undefined) {
254
+ const t = A.getObjectId(task);
255
+ if (t !== null)
256
+ taskOf.set(t, id);
257
+ }
258
+ }
259
+ const deps = new Map();
260
+ // Each change's actor and sequence number, for its vector clock below.
261
+ const author = new Map();
262
+ const deletes = [];
263
+ // Every lifecycle write (any value), to tell whether a deletion is still held.
264
+ const lifecycleWrites = [];
265
+ const edits = [];
266
+ for (const change of historyOf(doc)) {
267
+ deps.set(change.hash, change.deps);
268
+ author.set(change.hash, { actor: change.actor, seq: change.seq });
269
+ const lastLifecycle = new Map();
270
+ const touched = new Set();
271
+ for (const op of change.ops) {
272
+ const key = op.key;
273
+ const value = op.value;
274
+ if (op.obj === nodesObj && op.action === "makeMap" && key !== undefined && hidden.has(key))
275
+ touched.add(key);
276
+ const text = textOf.get(op.obj);
277
+ if (text !== undefined && hidden.has(text))
278
+ touched.add(text);
279
+ const task = taskOf.get(op.obj);
280
+ if (task !== undefined && hidden.has(task) && (key === "title" || key === "status"))
281
+ touched.add(task);
282
+ // §8, §9: a move writes the node's `placement` register (node.move or
283
+ // node.resolve_placement). A creation also writes it and is already
284
+ // counted above; what this adds is a move of an existing node.
285
+ const moved = nodeMapOf.get(op.obj);
286
+ if (moved !== undefined && key === "placement" && hidden.has(moved))
287
+ touched.add(moved);
288
+ const owner = ownerOf.get(op.obj);
289
+ if (owner !== undefined && key === "lifecycle")
290
+ lastLifecycle.set(owner, value);
291
+ }
292
+ for (const [owner, value] of lastLifecycle) {
293
+ lifecycleWrites.push({ hash: change.hash, node: owner });
294
+ if (value === "deleted")
295
+ deletes.push({ hash: change.hash, node: owner });
296
+ }
297
+ for (const node of touched)
298
+ edits.push({ hash: change.hash, node });
299
+ }
300
+ if (deletes.length === 0 || edits.length === 0)
301
+ return [];
302
+ /**
303
+ * Each change's vector clock: the highest sequence number of every actor
304
+ * in its causal history, itself included. When each change of an actor
305
+ * has that actor's previous change in its history (as Automerge writes
306
+ * them), `a` is in the history of `b` exactly when b's clock has a's actor
307
+ * at a's sequence or later: O(history × actors), where a set of ancestors
308
+ * per change was O(history²) (soak #11: seconds per pass after ~3,000
309
+ * changes in a section with deleted Tasks). A history where that does not
310
+ * hold (a change made without its actor's previous one), or one with more
311
+ * than 32 actors, falls back to the exact sets.
312
+ */
313
+ const clocks = new Map();
314
+ // A clock holds an entry per actor: with many actors (a hostile or unusual
315
+ // history), the exact sets are cheaper.
316
+ let regular = new Set([...author.values()].map((x) => x.actor)).size <= 32;
317
+ for (const start of regular ? deps.keys() : []) {
318
+ const stack = [start];
319
+ while (stack.length > 0) {
320
+ const h = stack[stack.length - 1];
321
+ if (clocks.has(h)) {
322
+ stack.pop();
323
+ continue;
324
+ }
325
+ const pending = (deps.get(h) ?? []).filter((d) => deps.has(d) && !clocks.has(d));
326
+ if (pending.length > 0) {
327
+ stack.push(...pending);
328
+ continue;
329
+ }
330
+ stack.pop();
331
+ const clock = new Map();
332
+ for (const d of deps.get(h) ?? [])
333
+ for (const [actor, seq] of clocks.get(d) ?? [])
334
+ if (seq > (clock.get(actor) ?? 0))
335
+ clock.set(actor, seq);
336
+ const me = author.get(h);
337
+ if (me !== undefined) {
338
+ if ((clock.get(me.actor) ?? 0) !== me.seq - 1)
339
+ regular = false;
340
+ if (me.seq > (clock.get(me.actor) ?? 0))
341
+ clock.set(me.actor, me.seq);
342
+ }
343
+ clocks.set(h, clock);
344
+ }
345
+ }
346
+ /** Every change `hash` depends on, transitively, memoized (the exact fallback). */
347
+ const ancestry = new Map();
348
+ const ancestorsOf = (hash) => {
349
+ const known = ancestry.get(hash);
350
+ if (known !== undefined)
351
+ return known;
352
+ const out = new Set();
353
+ const todo = [...(deps.get(hash) ?? [])];
354
+ while (todo.length > 0) {
355
+ const h = todo.pop();
356
+ if (out.has(h))
357
+ continue;
358
+ out.add(h);
359
+ for (const d of deps.get(h) ?? [])
360
+ if (!out.has(d))
361
+ todo.push(d);
362
+ }
363
+ ancestry.set(hash, out);
364
+ return out;
365
+ };
366
+ /** Whether change `a` is in the causal history of change `b` (a ≠ b). */
367
+ const before = (a, b) => {
368
+ if (a === b)
369
+ return false;
370
+ if (!regular)
371
+ return ancestorsOf(b).has(a);
372
+ const me = author.get(a);
373
+ return me !== undefined && (clocks.get(b)?.get(me.actor) ?? 0) >= me.seq;
374
+ };
375
+ const concurrent = (a, b) => a !== b && !before(a, b) && !before(b, a);
376
+ // §9 (baseline.9): a deletion counts only while the lifecycle register still
377
+ // holds it. A later write to the same register — one that has the deletion in
378
+ // its dependencies — supersedes it, so deleting a node again clears the
379
+ // attention a concurrent edit raised. Two concurrent deletions supersede
380
+ // neither, so both still count (SS86); the writes come from each change's
381
+ // operations, which also keeps a deletion merged after a concurrent one.
382
+ const heldDeletes = deletes.filter((d) => !lifecycleWrites.some((w) => w.node === d.node && w.hash !== d.hash && before(d.hash, w.hash)));
383
+ const deletesOf = new Map();
384
+ for (const d of heldDeletes)
385
+ deletesOf.set(d.node, [...(deletesOf.get(d.node) ?? []), d.hash]);
386
+ const attention = new Set();
387
+ for (const edit of edits) {
388
+ if (attention.has(edit.node))
389
+ continue;
390
+ const seen = new Set();
391
+ for (let p = edit.node; p !== undefined && p !== sectionId && nodes[p] !== undefined && !seen.has(p); p = parentOf(p)) {
392
+ seen.add(p);
393
+ if ((deletesOf.get(p) ?? []).some((d) => concurrent(d, edit.hash))) {
394
+ attention.add(edit.node);
395
+ break;
396
+ }
397
+ }
398
+ }
399
+ return [...attention].sort();
400
+ }
401
+ //# sourceMappingURL=tree.js.map
@@ -0,0 +1,30 @@
1
+ import type { PrincipalId, ResourceId } from "@openlfcp/core";
2
+ /**
3
+ * Identifiers and value domains of SHARED-SECTIONS-PROFILE-01 (Working
4
+ * Draft 0.3): the profile identifier, the actor domain of its §2 binding,
5
+ * and the enumerations of §4.
6
+ */
7
+ /** The profile identifier, as a Resource's Genesis names it. */
8
+ export declare const SECTIONS_PROFILE_ID = "org.openlfcp.shared-sections.v1";
9
+ /** §2: the actor domain this profile gives the inherited SOP §8 binding. */
10
+ export declare const SECTIONS_ACTOR_DOMAIN = "OPENLFCP-SHARED-SECTIONS-ACTOR-v1";
11
+ /**
12
+ * §2: the Automerge actor ID of `principal` editing the section `resource`:
13
+ * SHA-256(ASCII("OPENLFCP-SHARED-SECTIONS-ACTOR-v1") || resource_id || principal_id),
14
+ * from the raw 32-byte IDs. It differs from the Shared Objects actor of the
15
+ * same pair, so a change of one profile never passes as the other's.
16
+ */
17
+ export declare function deriveSectionActorId(resource: ResourceId, principal: PrincipalId): Uint8Array;
18
+ /** §4.2: the kinds of a node. */
19
+ export declare const NODE_KINDS: readonly ["task", "paragraph", "item", "raw"];
20
+ export type NodeKind = (typeof NODE_KINDS)[number];
21
+ /** §4.2: the kinds that hold collaborative Text. */
22
+ export declare const TEXT_KINDS: ReadonlySet<NodeKind>;
23
+ /** §4.2: the kinds that can parent content; paragraph and raw nodes cannot. */
24
+ export declare const PARENT_KINDS: ReadonlySet<NodeKind>;
25
+ /** §4.2: list_style, on task and item nodes only. */
26
+ export declare const LIST_STYLES: readonly ["bullet", "ordered"];
27
+ export type ListStyle = (typeof LIST_STYLES)[number];
28
+ /** §3: the root containers, each a map. */
29
+ export declare const ROOT_MAPS: readonly ["section", "objects", "nodes", "placements", "extensions"];
30
+ //# sourceMappingURL=values.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { deriveDomainActorId } from "../admission/actor.js";
2
+ /**
3
+ * Identifiers and value domains of SHARED-SECTIONS-PROFILE-01 (Working
4
+ * Draft 0.3): the profile identifier, the actor domain of its §2 binding,
5
+ * and the enumerations of §4.
6
+ */
7
+ /** The profile identifier, as a Resource's Genesis names it. */
8
+ export const SECTIONS_PROFILE_ID = "org.openlfcp.shared-sections.v1";
9
+ /** §2: the actor domain this profile gives the inherited SOP §8 binding. */
10
+ export const SECTIONS_ACTOR_DOMAIN = "OPENLFCP-SHARED-SECTIONS-ACTOR-v1";
11
+ /**
12
+ * §2: the Automerge actor ID of `principal` editing the section `resource`:
13
+ * SHA-256(ASCII("OPENLFCP-SHARED-SECTIONS-ACTOR-v1") || resource_id || principal_id),
14
+ * from the raw 32-byte IDs. It differs from the Shared Objects actor of the
15
+ * same pair, so a change of one profile never passes as the other's.
16
+ */
17
+ export function deriveSectionActorId(resource, principal) {
18
+ return deriveDomainActorId(SECTIONS_ACTOR_DOMAIN, resource, principal);
19
+ }
20
+ /** §4.2: the kinds of a node. */
21
+ export const NODE_KINDS = Object.freeze(["task", "paragraph", "item", "raw"]);
22
+ /** §4.2: the kinds that hold collaborative Text. */
23
+ export const TEXT_KINDS = new Set(["paragraph", "item", "raw"]);
24
+ /** §4.2: the kinds that can parent content; paragraph and raw nodes cannot. */
25
+ export const PARENT_KINDS = new Set(["task", "item"]);
26
+ /** §4.2: list_style, on task and item nodes only. */
27
+ export const LIST_STYLES = Object.freeze(["bullet", "ordered"]);
28
+ /** §3: the root containers, each a map. */
29
+ export const ROOT_MAPS = Object.freeze([
30
+ "section",
31
+ "objects",
32
+ "nodes",
33
+ "placements",
34
+ "extensions",
35
+ ]);
36
+ //# sourceMappingURL=values.js.map
package/dist/values.d.ts CHANGED
@@ -25,28 +25,13 @@ export declare function isUtcTimestamp(text: unknown): text is string;
25
25
  export declare const isReverseDomain: (text: unknown) => text is string;
26
26
  /** §33 ABNF namespaced-value: x/<reverse-domain>/<value>, value non-empty without "/". */
27
27
  export declare const isNamespacedValue: (text: unknown) => text is string;
28
+ /** §8: the actor domain of this profile. */
29
+ export declare const ACTOR_DOMAIN = "OPENLFCP-SHARED-OBJECTS-ACTOR-v1";
28
30
  /**
29
31
  * §8: the Automerge actor ID of `principal` editing `resource`:
30
32
  * SHA-256(ASCII("OPENLFCP-SHARED-OBJECTS-ACTOR-v1") || resource_id || principal_id),
31
33
  * from the raw 32-byte IDs.
32
34
  */
33
35
  export declare function deriveActorId(resource: ResourceId, principal: PrincipalId): Uint8Array;
34
- /** §11, §13: the only framing version. */
35
- export declare const FRAMING_VERSION = 1;
36
- /**
37
- * Deterministic CBOR of [1, bstr] (§11 shared-objects-change, §13
38
- * shared-objects-snapshot): the profile plaintext of a Data Unit (one
39
- * Automerge change) or a Snapshot (one Automerge full save). The bytes are
40
- * framed as given; checking that they are a valid Automerge change or save
41
- * is the Automerge binding (LFCP-031).
42
- */
43
- export declare function frameProfilePayload(bytes: Uint8Array): Uint8Array;
44
- /**
45
- * The inner bytes of a framed profile plaintext. §11: a receiver MUST
46
- * reject plaintext that is not valid CBOR, not a two-element array, or
47
- * uses an unsupported framing version; this also requires the
48
- * deterministic encoding and no trailing bytes. Throws PROFILE_INVALID /
49
- * INVALID_AUTOMERGE_BYTES (§74.1).
50
- */
51
- export declare function unframeProfilePayload(framed: Uint8Array): Uint8Array;
36
+ export { FRAMING_VERSION, frameProfilePayload, unframeProfilePayload, } from "./admission/framing.js";
52
37
  //# sourceMappingURL=values.d.ts.map