@lazily-hub/lazily-js 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.
@@ -0,0 +1,48 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: release-${{ github.ref }}
11
+ cancel-in-progress: false
12
+
13
+ jobs:
14
+ publish-npm:
15
+ name: Publish to npm (OIDC trusted publishing)
16
+ runs-on: ubuntu-latest
17
+ # OIDC trusted publishing: no long-lived NPM_TOKEN. The package must already
18
+ # exist on npm (npm has no "pending publisher"), then configure the Trusted
19
+ # Publisher on the package: user/org = lazily-hub, repo = lazily-js,
20
+ # workflow = release.yml, action = "npm publish".
21
+ permissions:
22
+ id-token: write
23
+ contents: read
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - uses: actions/setup-node@v4
29
+ with:
30
+ node-version: 24
31
+ registry-url: https://registry.npmjs.org
32
+
33
+ # Trusted publishing needs npm CLI >= 11.5.1; Node 24's bundled npm may lag.
34
+ - name: Upgrade npm for OIDC trusted publishing
35
+ run: npm install -g npm@latest
36
+
37
+ - name: Install dependencies
38
+ run: npm ci || npm install
39
+
40
+ - name: Test
41
+ run: npm test --if-present
42
+
43
+ # Published under the `prev` dist-tag so this v0.x line does not steal
44
+ # `latest` from a higher already-published version; move `latest` explicitly
45
+ # (`npm dist-tag add @lazily-hub/lazily-js@<v> latest`) once the accidental
46
+ # v1.x releases are removed.
47
+ - name: Publish
48
+ run: npm publish --access public --tag prev
package/README.md ADDED
@@ -0,0 +1,356 @@
1
+ # lazily-js
2
+
3
+ Native JavaScript port of the **lazily** reactive family. `@lazily-hub/lazily-js`
4
+ is the JavaScript/TypeScript binding alongside [`lazily-rs`][rs],
5
+ [`lazily-py`][py], [`lazily-zig`][zig], [`lazily-kt`][kt], and
6
+ [`lazily-dart`][dart]. It ships:
7
+
8
+ - a full reactive graph (`Context`, `Cell`, `Slot`, `Signal`, `Effect`);
9
+ - the [`lazily-spec`][spec] IPC wire types (`Snapshot`, `Delta`, `CrdtSync`,
10
+ capability negotiation, and default-deny peer permissions);
11
+ - keyed cell collections, move-minimized reconciliation, and a memoized
12
+ semantic tree;
13
+ - move-aware sequence CRDT, Fugue/RGA text CRDT, and manufactured text identity;
14
+ - a Cell-backed flat state machine plus a full Harel/SCXML state-chart
15
+ interpreter with the typed `ChartBuilder` API;
16
+ - a koffi-backed state-projection consumer for agent-doc host projections.
17
+
18
+ > **Package note.** Earlier `@lazily-hub/js` releases were only a state-projection
19
+ > consumer. `@lazily-hub/lazily-js` is the current full reactive binding.
20
+
21
+ Pure ES modules. The reactive, IPC, collections, CRDT, state-machine, and
22
+ state-chart modules have no runtime dependencies; `koffi` is loaded only when
23
+ the FFI projection transport is used.
24
+
25
+ ## Feature Set
26
+
27
+ The full `lazily` capability set and its cross-language coverage (`lazily-rs`,
28
+ `lazily-kt`, `lazily-js`). `✅` shipped, `~` partial, `—` not applicable/absent.
29
+
30
+ | Feature | Rust | Kotlin | JS |
31
+ |---------|:----:|:------:|:--:|
32
+ | Reactive graph — `Context`, `Slot`, `Cell`, `memo`, `Signal` (eager), `Effect`, `batch` | ✅ | ✅ | ✅ |
33
+ | Thread-safe `Context` (`Send + Sync`, lock-backed) | ✅ | ✅ | — |
34
+ | Async reactive `Context` | ✅ | ✅ | — |
35
+ | Statechart (Harel) + state machine | ✅ | ✅ | ✅ |
36
+ | Keyed cell collections + `reconcile` + `SemTree` (keyed tree) | ✅ | ✅ | ✅ |
37
+ | Stable-id alignment (manufactured identity) | ✅ | ✅ | ✅ |
38
+ | Free-text character CRDT (`TextCrdt`) | ✅ | ✅ | ✅ |
39
+ | **`TextCrdt` delta sync — `version_vector` / `delta_since` / `apply_delta` (`#lztextsync`)** | ✅ | ✅ | ✅ |
40
+ | Move-aware sequence CRDT (`SeqCrdt`) | ✅ | ✅ | ✅ |
41
+ | Registers (LWW / MV), `PnCounter`, `CellCrdt` | ✅ | ✅ | ✅ |
42
+ | IPC wire — `Snapshot` + `Delta` + `CrdtSync` + shared-memory blobs | ✅ | ✅ | ~ |
43
+ | State projection / mirror | ✅ | ✅ | ✅ |
44
+ | FFI boundary | ✅ | ✅ | n/a |
45
+ | Distributed plane (WebRTC transport + signaling) | ✅ | — | — |
46
+ | Instrumentation / benchmarks | ✅ | — | — |
47
+
48
+ CRDT convergence and the wire protocol are pinned by the shared conformance fixtures
49
+ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
50
+ ## Packages
51
+
52
+ | Import | What it is |
53
+ |--------|------------|
54
+ | `@lazily-hub/lazily-js` | `lazily-spec` IPC wire types: `Snapshot`, `Delta`, `DeltaOp`, `IpcMessage` (`Snapshot` / `Delta` / `CrdtSync`), `NodeState`, `IpcValue`, `PeerPermissions`, `SessionHandshake`, `BINDING_CAPABILITIES` |
55
+ | `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `Context`, `Cell`, `Slot`, `Signal`, `Effect` |
56
+ | `@lazily-hub/lazily-js/state-machine` | Flat finite-state-machine kernel backed by a reactive `Cell` |
57
+ | `@lazily-hub/lazily-js/statechart` | Harel/SCXML chart interpreter plus `ChartBuilder`, `StateBuilder`, `TransitionBuilder` |
58
+ | `@lazily-hub/lazily-js/collections` | `CellMap`, `CellTree`, keyed reconciliation, and LIS move minimization |
59
+ | `@lazily-hub/lazily-js/sem-tree` | Memoized semantic tree over `CellTree`-shaped data |
60
+ | `@lazily-hub/lazily-js/seq-crdt` | Move-aware sequence CRDT using independent LWW value / position / deletion registers |
61
+ | `@lazily-hub/lazily-js/text-crdt` | Fugue/RGA character CRDT |
62
+ | `@lazily-hub/lazily-js/stable-id` | Manufactured text identity: anchors, content hashes, similarity alignment |
63
+ | `@lazily-hub/lazily-js/state-projection` | koffi FFI consumer for agent-doc `DocumentStateProjection` |
64
+
65
+ ## Reactive graph
66
+
67
+ `Context` mirrors the single-threaded lazily-rs `Context` semantics in native
68
+ JavaScript. The family is:
69
+
70
+ - **Slot** - lazy, memoized derived value;
71
+ - **Cell** - mutable source value;
72
+ - **Signal** - eager derived value that re-materializes as soon as a dependency
73
+ invalidates;
74
+ - **Effect** - side-effecting observer with cleanup.
75
+
76
+ Dependencies are discovered dynamically while a slot/effect/signal computes.
77
+ Invalidation is pull-based and glitch-free; `memo`/`Signal` use equality guards
78
+ to suppress downstream work when a recompute produces the same value. `batch`
79
+ coalesces invalidations and effect reruns.
80
+
81
+ ```js
82
+ import { Context } from "@lazily-hub/lazily-js/reactive";
83
+
84
+ const ctx = new Context();
85
+ const a = ctx.cell(2);
86
+ const b = ctx.cell(3);
87
+
88
+ const sum = ctx.memo(() => ctx.getCell(a) + ctx.getCell(b));
89
+ ctx.get(sum); // 5
90
+
91
+ ctx.setCell(a, 10);
92
+ ctx.get(sum); // 13, recomputed lazily on read
93
+
94
+ const parity = ctx.signal(() => (ctx.getCell(a) % 2 === 0 ? "even" : "odd"));
95
+ ctx.setCell(a, 11);
96
+ ctx.getSignal(parity); // "odd", already materialized
97
+ ```
98
+
99
+ ## State machine and state charts
100
+
101
+ `StateMachine` is the flat finite-state-machine kernel: a pure
102
+ `(state, event) -> nextState | null` transition backed by a reactive `Cell`.
103
+ Accepted self-transitions to an equal state are suppressed by the Cell equality
104
+ guard.
105
+
106
+ ```js
107
+ import { Context } from "@lazily-hub/lazily-js/reactive";
108
+ import { StateMachine } from "@lazily-hub/lazily-js/state-machine";
109
+
110
+ const ctx = new Context();
111
+ const light = new StateMachine(ctx, "Red", (state, event) =>
112
+ event === "advance" ? { Red: "Green", Green: "Yellow", Yellow: "Red" }[state] : null,
113
+ );
114
+
115
+ light.send("advance"); // true
116
+ light.state; // "Green"
117
+ ```
118
+
119
+ `StateChart` implements the full Harel/SCXML subset from
120
+ [`lazily-spec/docs/state-charts.md`][statecharts]: compound states, parallel
121
+ regions, shallow/deep history, entry/exit/transition actions, named guards
122
+ (fail-closed), internal/external transitions, and final leaves. Charts are
123
+ **compute, not protocol**: the chart itself is not serialized as a special wire
124
+ kind; only an application-level active configuration would cross IPC as ordinary
125
+ payload state.
126
+
127
+ The normative definition path is the declarative JSON chart consumed by the
128
+ shared conformance fixtures:
129
+
130
+ ```js
131
+ import { ChartDef, StateChart } from "@lazily-hub/lazily-js/statechart";
132
+
133
+ const def = ChartDef.fromChart({
134
+ initial: "root",
135
+ states: {
136
+ root: { parallel: true },
137
+ flow: { parent: "root", initial: "idle" },
138
+ idle: { parent: "flow", on: { go: { target: "done", guard: "ready" } } },
139
+ done: { parent: "flow", kind: "final" },
140
+ net: { parent: "root", initial: "up" },
141
+ up: { parent: "net", on: { drop: "down" } },
142
+ down: { parent: "net", on: { restore: "up" } },
143
+ },
144
+ });
145
+
146
+ const chart = new StateChart(def);
147
+ chart.activeLeaves(); // ["idle", "up"]
148
+ chart.send("drop"); // true
149
+ chart.send("go", { ready: true }); // true
150
+ chart.matches("done"); // true
151
+ ```
152
+
153
+ For typed JavaScript/TypeScript authoring, `ChartBuilder` builds the same
154
+ `ChartDef` through the same validation/assembly path. It is an ergonomic API,
155
+ not a second semantics:
156
+
157
+ ```js
158
+ import {
159
+ ChartBuilder,
160
+ StateBuilder,
161
+ StateChart,
162
+ } from "@lazily-hub/lazily-js/statechart";
163
+
164
+ const def = new ChartBuilder()
165
+ .state(StateBuilder.parallel("root"))
166
+ .state(StateBuilder.compound("flow", "idle").parent("root"))
167
+ .state(StateBuilder.atomic("idle").parent("flow").onGuarded("go", "done", "ready"))
168
+ .state(StateBuilder.final("done").parent("flow"))
169
+ .state(StateBuilder.compound("net", "up").parent("root"))
170
+ .state(StateBuilder.atomic("up").parent("net").on("drop", "down"))
171
+ .state(StateBuilder.atomic("down").parent("net").on("restore", "up"))
172
+ .build();
173
+
174
+ const chart = new StateChart(def);
175
+ chart.send("go", { ready: false }); // false, guards fail closed
176
+ ```
177
+
178
+ ## Keyed collections and semantic tree
179
+
180
+ `CellMap` and `CellTree` implement the lazily-spec keyed collections layer:
181
+ value, membership, and order readers invalidate independently; stable handles
182
+ survive moves; and an atomic move bumps order without touching values.
183
+ `reconcileCollections` emits the LIS-minimized `{ insert, remove, move, update }`
184
+ operation set. `SemTree` adds a memoized ancestor-chain fold: editing one leaf
185
+ recomputes only that leaf's ancestor path, and equal folded results are
186
+ suppressed by the memo guard.
187
+
188
+ ```js
189
+ import { CellMap, reconcileCollections } from "@lazily-hub/lazily-js/collections";
190
+ import { Context } from "@lazily-hub/lazily-js/reactive";
191
+ import { SemTree } from "@lazily-hub/lazily-js/sem-tree";
192
+
193
+ const map = CellMap.from({ order: ["a", "b"], values: { a: 1, b: 2 } });
194
+ map.moveBefore("b", "a"); // order reader invalidates; value readers do not
195
+
196
+ reconcileCollections(
197
+ { order: ["a", "b"], values: { a: 1, b: 2 } },
198
+ { order: ["b", "a", "c"], values: { a: 1, b: 2, c: 3 } },
199
+ ).ops; // move-minimized patch
200
+
201
+ const ctx = new Context();
202
+ const rootSpec = {
203
+ id: "root",
204
+ value: 0,
205
+ children: {
206
+ order: ["leaf"],
207
+ values: { leaf: { id: "leaf", value: 1 } },
208
+ },
209
+ };
210
+ const tree = new SemTree(ctx, rootSpec, (value, children) =>
211
+ value + children.reduce((sum, child) => sum + child, 0),
212
+ );
213
+
214
+ tree.value(); // 1
215
+ tree.setValue("leaf", 99); // only the ancestor chain recomputes
216
+ ```
217
+
218
+ ## CRDTs
219
+
220
+ `SeqCrdt` is the move-aware sequence CRDT: each element has independent LWW
221
+ registers for value, position, and deletion, so a move is one position
222
+ assignment rather than delete plus reinsert. `TextCrdt` is a Fugue/RGA
223
+ character CRDT: concurrent same-point inserts are preserved, deletes are sticky
224
+ tombstones, and merge is commutative / associative / idempotent. Both expose
225
+ tombstone GC behind caller-supplied causal-stability watermarks.
226
+
227
+ ```js
228
+ import { SeqCrdt } from "@lazily-hub/lazily-js/seq-crdt";
229
+ import { TextCrdt } from "@lazily-hub/lazily-js/text-crdt";
230
+
231
+ const seq = new SeqCrdt(1);
232
+ seq.insertBack("a", 0, 1);
233
+ seq.moveAfter("a", "b", 10);
234
+
235
+ const text = TextCrdt.fromStr(1, "hi");
236
+ const peer = text.fork(2);
237
+ peer.insert(2, "!");
238
+ text.merge(peer); // converges
239
+ ```
240
+
241
+ ## IPC wire types and capability negotiation
242
+
243
+ Every IPC value round-trips the canonical externally-tagged
244
+ [`lazily-spec`][spec] JSON shape through `toWire()` / `fromWire()`.
245
+ `IpcMessage` adds `encodeJson()` / `decodeJson()`. `Snapshot` and `Delta`
246
+ represent the single-writer graph-state plane; `CrdtSync` carries the
247
+ multi-writer CRDT anti-entropy plane. `PeerPermissions` is default-deny and
248
+ filters unreadable nodes/ops out of snapshots and deltas. `SessionHandshake`
249
+ performs the fail-closed protocol/version/codec/feature check before graph
250
+ frames flow.
251
+
252
+ ```js
253
+ import {
254
+ Delta,
255
+ DeltaOp,
256
+ IpcMessage,
257
+ NodeSnapshot,
258
+ PeerPermissions,
259
+ RemoteOp,
260
+ Snapshot,
261
+ } from "@lazily-hub/lazily-js";
262
+
263
+ const snapshot = new Snapshot({
264
+ epoch: 7,
265
+ nodes: [NodeSnapshot.payload(1, "counter", new Uint8Array([42]))],
266
+ roots: [1],
267
+ });
268
+
269
+ const wire = IpcMessage.snapshot(snapshot).encodeJson();
270
+ IpcMessage.decodeJson(wire).snapshot.epoch; // 7
271
+
272
+ const delta = Delta.next(7, [DeltaOp.cellSet(1, [43])]);
273
+ IpcMessage.delta(delta).toWire();
274
+
275
+ const permissions = new PeerPermissions();
276
+ permissions.allow(10, RemoteOp.read(1));
277
+ snapshot.filterReadable(permissions, 10).nodes.length; // 1
278
+ snapshot.filterReadable(permissions, 11).nodes.length; // 0
279
+ ```
280
+
281
+ `BINDING_CAPABILITIES` advertises the JS binding truthfully: reactive core,
282
+ IPC, CRDT, keyed collections, semantic tree, sequence/text CRDT, stable-id,
283
+ state machine, state charts, permissions, and capability negotiation are
284
+ shipped; C-ABI FFI is `none` because browser/Worker JS cannot host a native
285
+ in-process ABI; async context, signaling, and WebRTC transports are not shipped
286
+ by this package. The same payload types can still be carried by any transport a
287
+ host application owns.
288
+
289
+ ## Conformance
290
+
291
+ lazily-js replays the shared `lazily-spec` fixtures for IPC, agent-doc state,
292
+ keyed collections (`CellMap`, `CellTree`, LIS reconciliation), semantic tree,
293
+ sequence and text CRDTs, manufactured text identity, and Harel state charts.
294
+ It also validates generated wire values against the canonical JSON Schemas.
295
+
296
+ `npm test` builds the [`lazily-formal`][formal] Lean 4 model when that sibling
297
+ checkout and the `lake` toolchain are present. The script exits successfully
298
+ when they are absent, so npm tarball consumers and shallow clones are not forced
299
+ to install Lean; full CI verifies the proofs.
300
+
301
+ Each formal module with a JS counterpart has a matching property test that
302
+ names the Lean theorems it mirrors:
303
+
304
+ | lazily-formal module | JS test file | Mirrored theorems |
305
+ |----------------------|--------------|-------------------|
306
+ | `StateMachine` | `state-machine.test.js` | `guard_rejection_preserves_state`, `accepted_transition_advances_state`, `send_preserves_transition` |
307
+ | `StateChart` | `statechart-properties.test.js` | `enabled_empty_rejects`, `parallel_region_confluence`, `single_region_refines_flat_machine`, `single_region_enabled_at_most_one`, `recordHistory_idempotent`, `send_actions_empty_when_rejected`, `send_preserves_chart`, determinism-by-construction |
308
+ | `Reactive` | `reactive-properties.test.js` | `setCell_equal_preserves_graph`, `setCell_different_invalidates_dependents`, `recomputeSlot_equal_preserves_dependents`, `recomputeSlot_different_invalidates_dependents`, `signal_materialized_after_recompute` |
309
+ | `Collection` | `collection-properties.test.js` | `setEntryValue_preserves_{membership,order,siblings}`, `moveKey_preserves_{membership,values}`, `moveKey_advances_order`, `addKey_advances_membership_and_order`, `Family.get_idempotent_after_first` |
310
+ | `Tree` | `tree-properties.test.js` | `setNodeValue_preserves_{other_nodes,node_signals}`, `moveChild_preserves_{non_parent,parent_value}`, `moveChild_advances_order_signal_only` |
311
+ | `Reconciliation` | `reconciliation-properties.test.js` | `lisBy_longest`, `reconcile_move_minimized`, `reconcile_stable_not_invalidated` |
312
+
313
+ `AsyncSlotState`, `AsyncEffect`, and thread-safe context theorems are not mirrored
314
+ because lazily-js does not ship async or thread-safe context surfaces.
315
+
316
+ ## The lazily family
317
+
318
+ | Binding | Language | Package / role |
319
+ |---------|----------|----------------|
320
+ | [`lazily-rs`][rs] | Rust | `lazily` on crates.io; single-threaded, thread-safe, and async context layers |
321
+ | [`lazily-py`][py] | Python | `lazily` on PyPI; dict-backed context plus IPC/shared-blob host types |
322
+ | **`lazily-js`** | JavaScript / TypeScript | `@lazily-hub/lazily-js`; reactive core, spec wire types, state charts, CRDTs |
323
+ | [`lazily-zig`][zig] | Zig | Zig library / FFI-oriented embedding surface |
324
+ | [`lazily-kt`][kt] | Kotlin/JVM | Kotlin reactive core plus typed state charts |
325
+ | [`lazily-dart`][dart] | Dart | Dart binding with statechart conformance |
326
+ | [`lazily-spec`][spec] | Specification | wire protocol, JSON Schemas, conformance fixtures |
327
+ | [`lazily-formal`][formal] | Lean 4 | executable formal model for the shared primitives, FSM, and state charts |
328
+
329
+ ## Development
330
+
331
+ ```bash
332
+ make check # npm run build && npm test
333
+ ```
334
+
335
+ - `npm run build` runs `node --check` over every shipped module.
336
+ - `npm run test:formal` builds `lazily-formal` when the sibling checkout and
337
+ `lake` are present.
338
+ - `npm test` runs the formal check and the Node test suite.
339
+
340
+ ## See also
341
+
342
+ - [`lazily-spec`][spec] - language-agnostic wire protocol, schemas, and
343
+ conformance fixtures.
344
+ - [`lazily-formal`][formal] - Lean 4 formal model behind the shared behavioral
345
+ guarantees.
346
+ - [`lazily-rs`][rs] / [`lazily-py`][py] / [`lazily-zig`][zig] /
347
+ [`lazily-kt`][kt] / [`lazily-dart`][dart] - sibling bindings.
348
+
349
+ [rs]: https://github.com/lazily-hub/lazily-rs
350
+ [py]: https://github.com/lazily-hub/lazily-py
351
+ [zig]: https://github.com/lazily-hub/lazily-zig
352
+ [kt]: https://github.com/lazily-hub/lazily-kt
353
+ [dart]: https://github.com/lazily-hub/lazily-dart
354
+ [spec]: https://github.com/lazily-hub/lazily-spec
355
+ [formal]: https://github.com/lazily-hub/lazily-formal
356
+ [statecharts]: https://github.com/lazily-hub/lazily-spec/blob/main/docs/state-charts.md
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@lazily-hub/lazily-js",
3
+ "version": "0.4.0",
4
+ "description": "Native JavaScript port of the lazily reactive core: a full reactive graph (Cell/Slot/Signal/Effect), the lazily-spec IPC wire types, keyed cell collections + LIS reconciliation, the memoized semantic tree, the move-aware sequence CRDT, the Fugue/RGA text CRDT, manufactured text identity, full-Harel state charts, and an FFI state-projection consumer.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./reactive": {
14
+ "types": "./src/reactive.d.ts",
15
+ "default": "./src/reactive.js"
16
+ },
17
+ "./state-machine": {
18
+ "types": "./src/state-machine.d.ts",
19
+ "default": "./src/state-machine.js"
20
+ },
21
+ "./statechart": {
22
+ "types": "./src/statechart.d.ts",
23
+ "default": "./src/statechart.js"
24
+ },
25
+ "./collections": {
26
+ "types": "./src/collections.d.ts",
27
+ "default": "./src/collections.js"
28
+ },
29
+ "./sem-tree": {
30
+ "types": "./src/sem-tree.d.ts",
31
+ "default": "./src/sem-tree.js"
32
+ },
33
+ "./stable-id": {
34
+ "types": "./src/stable-id.d.ts",
35
+ "default": "./src/stable-id.js"
36
+ },
37
+ "./seq-crdt": {
38
+ "types": "./src/seq-crdt.d.ts",
39
+ "default": "./src/seq-crdt.js"
40
+ },
41
+ "./text-crdt": {
42
+ "types": "./src/text-crdt.d.ts",
43
+ "default": "./src/text-crdt.js"
44
+ },
45
+ "./state-projection": {
46
+ "types": "./src/state-projection.d.ts",
47
+ "default": "./src/state-projection.js"
48
+ }
49
+ },
50
+ "scripts": {
51
+ "build": "node --check src/index.js && node --check src/reactive.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/state-projection.js",
52
+ "test:formal": "node scripts/formal-check.mjs",
53
+ "test": "npm run test:formal && node --test test/*.test.js"
54
+ },
55
+ "dependencies": {
56
+ "koffi": "^3.0.2"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.0.0",
60
+ "ajv": "^8.20.0",
61
+ "tsx": "^4.19.0",
62
+ "typescript": "^5.6.0"
63
+ }
64
+ }
@@ -0,0 +1,110 @@
1
+ export type CollectionKey = string;
2
+
3
+ export type InvalidationReport = {
4
+ value: CollectionKey[];
5
+ membership: boolean;
6
+ order: boolean;
7
+ };
8
+
9
+ export type CellMapOp =
10
+ | { type: "set_value"; key: CollectionKey; value: unknown }
11
+ | { type: "insert"; key: CollectionKey; value: unknown; at?: "end" | "start" | number | CollectionKey }
12
+ | { type: "remove"; key: CollectionKey }
13
+ | { type: "move_to"; key: CollectionKey; index: number }
14
+ | { type: "move_before"; key: CollectionKey; before: CollectionKey }
15
+ | { type: "move_after"; key: CollectionKey; after: CollectionKey };
16
+
17
+ export type CellMapSnapshot = {
18
+ order: CollectionKey[];
19
+ values: Record<string, unknown>;
20
+ };
21
+
22
+ export class CellMap {
23
+ constructor(initial?: { order?: CollectionKey[]; values?: Record<string, unknown> });
24
+ order: CollectionKey[];
25
+ values: Map<CollectionKey, unknown>;
26
+ keys(): CollectionKey[];
27
+ has(key: CollectionKey): boolean;
28
+ get(key: CollectionKey): unknown;
29
+ handle(key: CollectionKey): number | undefined;
30
+ snapshot(): CellMapSnapshot;
31
+ apply(op: CellMapOp): InvalidationReport;
32
+ setValue(key: CollectionKey, value: unknown): InvalidationReport;
33
+ insert(
34
+ key: CollectionKey,
35
+ value: unknown,
36
+ at?: "end" | "start" | number | CollectionKey,
37
+ ): InvalidationReport;
38
+ remove(key: CollectionKey): InvalidationReport;
39
+ moveTo(key: CollectionKey, index: number): InvalidationReport;
40
+ moveBefore(key: CollectionKey, beforeKey: CollectionKey): InvalidationReport;
41
+ moveAfter(key: CollectionKey, afterKey: CollectionKey): InvalidationReport;
42
+ static from(initial: { order?: CollectionKey[]; values?: Record<string, unknown> }): CellMap;
43
+ }
44
+
45
+ export type ReconcileOp =
46
+ | { type: "remove"; key: CollectionKey }
47
+ | { type: "move"; key: CollectionKey; after: CollectionKey | null }
48
+ | { type: "insert"; key: CollectionKey; value: unknown; after: CollectionKey | null };
49
+
50
+ export type ReconcileResult = {
51
+ ops: ReconcileOp[];
52
+ result_order: CollectionKey[];
53
+ stable_keys_not_invalidated: CollectionKey[];
54
+ };
55
+
56
+ export function reconcileCollections(
57
+ prior: { order: CollectionKey[]; values?: Record<string, unknown> },
58
+ target: { order: CollectionKey[]; values?: Record<string, unknown> },
59
+ ): ReconcileResult;
60
+
61
+ // Ordered keyed tree (cell-model.md § Ordered keyed tree).
62
+ export type TreeNodeSpec = {
63
+ id?: string;
64
+ value?: unknown;
65
+ children?: { order?: CollectionKey[]; values?: Record<string, TreeNodeSpec> };
66
+ };
67
+
68
+ export type TreeNodeSnapshot = {
69
+ id?: string;
70
+ value?: unknown;
71
+ children: { order: CollectionKey[]; values: Record<string, TreeNodeSnapshot> };
72
+ };
73
+
74
+ export type TreeInvalidationReport = {
75
+ path: CollectionKey[];
76
+ value: CollectionKey[];
77
+ membership: boolean;
78
+ order: boolean;
79
+ };
80
+
81
+ export class TreeNode {
82
+ constructor(id: unknown, value: unknown, children: CellMap);
83
+ id: unknown;
84
+ value: unknown;
85
+ children: CellMap;
86
+ snapshot(): TreeNodeSnapshot;
87
+ }
88
+
89
+ export class CellTree {
90
+ constructor(rootSpec: TreeNodeSpec | TreeNode);
91
+ root: TreeNode;
92
+ static from(rootSpec: TreeNodeSpec | TreeNode): CellTree;
93
+ nodeAt(path: CollectionKey[] | CollectionKey): TreeNode | undefined;
94
+ getValue(path: CollectionKey[] | CollectionKey): unknown;
95
+ setValue(path: CollectionKey[] | CollectionKey, value: unknown): TreeInvalidationReport;
96
+ hasChild(path: CollectionKey[] | CollectionKey, key: CollectionKey): boolean;
97
+ childKeys(path: CollectionKey[] | CollectionKey): CollectionKey[];
98
+ childHandle(path: CollectionKey[] | CollectionKey, key: CollectionKey): number | undefined;
99
+ insertChild(
100
+ path: CollectionKey[] | CollectionKey,
101
+ key: CollectionKey,
102
+ childSpec: TreeNodeSpec | TreeNode,
103
+ at?: "end" | "start" | number | CollectionKey,
104
+ ): TreeInvalidationReport;
105
+ removeChild(path: CollectionKey[] | CollectionKey, key: CollectionKey): TreeInvalidationReport;
106
+ moveChildTo(path: CollectionKey[] | CollectionKey, key: CollectionKey, index: number): TreeInvalidationReport;
107
+ moveChildBefore(path: CollectionKey[] | CollectionKey, key: CollectionKey, beforeKey: CollectionKey): TreeInvalidationReport;
108
+ moveChildAfter(path: CollectionKey[] | CollectionKey, key: CollectionKey, afterKey: CollectionKey): TreeInvalidationReport;
109
+ snapshot(): TreeNodeSnapshot;
110
+ }