Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions apps/site/src/generated/repo-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1844,25 +1844,35 @@ export const repoCatalog = {
"version": "0.1.0",
"description": "Lab causal JSON Patch inbox for json-document documents.",
"license": "MIT",
"summary": "Lab module for admitting dependency-declared JSON Patch envelopes, holding\nout-of-order work, and applying each ready envelope to one `JSONDocument`.",
"summary": "Lab module for admitting dependency-declared JSON Patch or authored edit\nenvelopes, holding out-of-order work, and committing each envelope when it\nbecomes causally ready on one `JSONDocument`.",
"guidance": {
"useFor": "queue and deduplicate dependency-declared envelopes, applying each as one core JSON Patch batch",
"notFor": "CRDT/OT convergence, transport, persistence, ready-time rebase, or conflict resolution"
"useFor": "queue dependency-declared envelopes and materialize delayed positional or stable-id edits when causally ready",
"notFor": "CRDT/OT convergence, transport, persistence, or automatic conflict resolution"
},
"publicExports": [
"CausalAuthoredIntent",
"CausalEnvelope",
"CausalIntentEnvelope",
"CausalMaterializationDiagnostic",
"CausalMaterializationPolicy",
"CausalPatchEnvelope",
"CausalPatchFailure",
"CausalPatchInbox",
"CausalPatchInboxOptions",
"CausalPatchInboxSnapshot",
"CausalPatchIngestError",
"CausalPatchIngestErrorCode",
"CausalPatchIngestOk",
"CausalPatchIngestResult",
"CausalPositionalIntent",
"CausalStableIdReplaceIntent",
"FailedCausalMaterialization",
"FailedCausalPatch",
"FaultedCausalPatch",
"QueuedCausalPatch",
"createCausalPatchInbox"
],
"publicExportCount": 11,
"publicExportCount": 21,
"keywords": [
"@interactive-os/json-document",
"causal",
Expand Down
15 changes: 14 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,20 @@ All notable changes to this project are documented here.

## Unreleased

No unreleased changes.
### Added

- Added lab-only ready-time materialization to the causal patch inbox for
delayed positional edits and stable-id field replacements. Authored intents
are admitted and deduplicated immutably, then materialized only after their
declared dependencies are applied.
- Added structured policy-specific materialization failures and successful
rebase diagnostics without changing direct-operation result shapes.

### Changed

- Changed the causal patch inbox to commit each ready direct or materialized
envelope once through `doc.commit`, including a rebased `selectionAfter`, and
to advance its applied ledger and causal frontier only after success.

## 1.1.0-rc.0 - 2026-07-11

Expand Down
2 changes: 1 addition & 1 deletion docs/generated/extensions-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Lab extensions are private candidates. They are listed to show product pressure,
| `@interactive-os/json-document-batch-update` | lab-only | 10 | set a field across a list of selected item pointers to a constant or computed value | selecting which items to edit, or JSONPath query-driven replacement | Lab batch-update extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-bookmarks` | lab-only | 8 | keep named JSON Pointer locations stable across edits | browser bookmarks or route state | Headless bookmark tracking extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-calculated-fields` | lab-only | 12 | sync host-computed derived JSON fields | formula languages or dependency runtimes | Lab calculated field extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-causal-patch-inbox` | lab-only | 11 | queue and deduplicate dependency-declared envelopes, applying each as one core JSON Patch batch | CRDT/OT convergence, transport, persistence, ready-time rebase, or conflict resolution | Lab module for admitting dependency-declared JSON Patch envelopes, holding out-of-order work, and applying each ready envelope to one `JSONDocument`. |
| `@interactive-os/json-document-causal-patch-inbox` | lab-only | 21 | queue dependency-declared envelopes and materialize delayed positional or stable-id edits when causally ready | CRDT/OT convergence, transport, persistence, or automatic conflict resolution | Lab module for admitting dependency-declared JSON Patch or authored edit envelopes, holding out-of-order work, and committing each envelope when it becomes causally ready on one `JSONDocument`. |
| `@interactive-os/json-document-change-case` | lab-only | 9 | apply case/whitespace transforms (upper, lower, trim, title) to a string field | locale-aware casing, rich text formatting toolbars, or find/replace | Lab change-case extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-checkpoints` | lab-only | 13 | name and restore document snapshots | durable version graphs or cloud backup | Lab checkpoint extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-clear-contents` | lab-only | 10 | reset selected fields to schema-derived empty values, keeping structure | structural delete, caller-supplied bulk replace, or enum/object default policy | Lab clear-contents extension for `@interactive-os/json-document` documents. |
Expand Down
18 changes: 14 additions & 4 deletions docs/generated/repo-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -1843,25 +1843,35 @@
"version": "0.1.0",
"description": "Lab causal JSON Patch inbox for json-document documents.",
"license": "MIT",
"summary": "Lab module for admitting dependency-declared JSON Patch envelopes, holding\nout-of-order work, and applying each ready envelope to one `JSONDocument`.",
"summary": "Lab module for admitting dependency-declared JSON Patch or authored edit\nenvelopes, holding out-of-order work, and committing each envelope when it\nbecomes causally ready on one `JSONDocument`.",
"guidance": {
"useFor": "queue and deduplicate dependency-declared envelopes, applying each as one core JSON Patch batch",
"notFor": "CRDT/OT convergence, transport, persistence, ready-time rebase, or conflict resolution"
"useFor": "queue dependency-declared envelopes and materialize delayed positional or stable-id edits when causally ready",
"notFor": "CRDT/OT convergence, transport, persistence, or automatic conflict resolution"
},
"publicExports": [
"CausalAuthoredIntent",
"CausalEnvelope",
"CausalIntentEnvelope",
"CausalMaterializationDiagnostic",
"CausalMaterializationPolicy",
"CausalPatchEnvelope",
"CausalPatchFailure",
"CausalPatchInbox",
"CausalPatchInboxOptions",
"CausalPatchInboxSnapshot",
"CausalPatchIngestError",
"CausalPatchIngestErrorCode",
"CausalPatchIngestOk",
"CausalPatchIngestResult",
"CausalPositionalIntent",
"CausalStableIdReplaceIntent",
"FailedCausalMaterialization",
"FailedCausalPatch",
"FaultedCausalPatch",
"QueuedCausalPatch",
"createCausalPatchInbox"
],
"publicExportCount": 11,
"publicExportCount": 21,
"keywords": [
"@interactive-os/json-document",
"causal",
Expand Down
162 changes: 122 additions & 40 deletions labs/extensions/causal-patch-inbox/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# @interactive-os/json-document-causal-patch-inbox

Lab module for admitting dependency-declared JSON Patch envelopes, holding
out-of-order work, and applying each ready envelope to one `JSONDocument`.
Lab module for admitting dependency-declared JSON Patch or authored edit
envelopes, holding out-of-order work, and committing each envelope when it
becomes causally ready on one `JSONDocument`.

```ts
const inbox = createCausalPatchInbox(doc);
Expand Down Expand Up @@ -31,36 +32,86 @@ if (result.ok) {
}
```

The envelope operations are already-materialized JSON Patch. They can be raw
protocol output or guarded operations produced by another planner. This module
owns causal admission and scheduling, not target recovery or merge policy.
Direct `operations` envelopes remain supported with the same runtime result
shape. A closed alternative envelope carries one `intent` instead:

An `ingest` batch is preflighted before any new envelope is admitted. An exact
duplicate is a no-op; the same id with different dependencies or operations is
`duplicate_mismatch`. Dependency order has set semantics, while operation and
array order remain significant. A known graph cycle rejects the closing batch.
```ts
const positional = createCausalPatchInbox(doc, {
positionalSchema: DocumentSchema,
});

positional.ingest({
id: "local-title",
dependsOn: ["observed-parent"],
intent: {
kind: "positional",
base: authoredDocument,
operations: [
{ op: "replace", path: "/items/1/title", value: "Reviewed" },
],
selectionAfter: "/items/1/title",
},
});

const stable = createCausalPatchInbox(doc, {
stableIdScopes: cardScopes,
});

stable.ingest({
id: "stable-title",
dependsOn: ["observed-parent"],
intent: {
kind: "stable-id-replace",
target: { scope: "card", id: "b" },
relativePath: "/title",
expected: "Draft",
value: "Reviewed",
relativeSelectionAfter: "/title",
},
});
```

The positional `base` must represent the authored projection containing the
transitive causal past declared by `dependsOn`. Once that envelope is ready,
the inbox replays successful applied batches outside that causal past in their
actual local commit order. Stable-id replacement instead resolves its target
against the current projection with the host scopes supplied once at factory
creation. Neither policy is run while its envelope is waiting.

An `ingest` batch is preflighted and defensively copied before any new envelope
is admitted. An exact duplicate compares the authored direct operations or
intent, never a later materialized patch. The same id with a different authored
payload is `duplicate_mismatch`. A missing policy configuration rejects the
whole batch as `policy_not_configured` before admission. Dependency order has
set semantics, while operation and array order remain significant. A known
graph cycle rejects the closing batch.

Ready envelopes currently known to one drain are applied by locale-independent
id order. Each envelope is one atomic `doc.patch`; causal frontier state moves
only after that patch succeeds. A batch is not one transaction: earlier
envelopes remain applied when a later envelope fails. Patch failure globally
blocks this first tracer, preserving the failed envelope and its descendants
for inspection. Recovery, skipping, and conflict resolution need a later
policy rather than an implicit retry.
id order. A direct envelope uses its operations unchanged. A delayed intent is
materialized at that point, then the resulting operations and optional
selection are passed to exactly one atomic `doc.commit`. The concrete
`doc.lastPatch` is recorded only after success, and causal frontier state moves
only after that commit. A batch is not one transaction: earlier envelopes
remain applied when a later envelope fails. Patch or materialization failure
globally blocks the inbox, preserving the failed envelope and descendants for
inspection without an implicit retry.

`current().frontier` is the maximal set in the declared dependency DAG. It is
not an actor clock or version vector. `queued[].missing` contains dependencies
that have not been applied by this inbox. Snapshot status `active` describes a
usable inbox, including one that has pending dependencies; it does not mean
that a ready envelope currently exists. `blocked` preserves a structured patch
failure, while `faulted` preserves an unexpected thrown execution failure that
has no `JSONResult`. Full queue snapshots are produced only by `current()`.
Successful `ingest` results report only this call's `applied`, `pending`, and
`duplicates` ids.
or policy-specific materialization failure. `faulted` preserves an unexpected
thrown execution failure; a planner throw also records
`phase: "materialization"`. Full queue snapshots are produced only by
`current()`. Successful `ingest` results report this call's `applied`,
`pending`, and `duplicates` ids. A non-empty materialization diagnostic list is
added as `diagnostics`; direct-only results remain unchanged.

## Scope

- Defensively validate and copy one envelope or a batch before admission.
- Defensively validate and copy one closed direct-operation or authored-intent
envelope union before admission.
- Queue envelopes whose declared dependencies have not been applied.
- Distinguish exact duplicate delivery from an id/payload mismatch.
- Reject empty ids, invalid dependency ids, self/duplicate dependencies,
Expand All @@ -69,23 +120,31 @@ Successful `ingest` results report only this call's `applied`, `pending`, and
- Choose the smallest currently ready id with a private min-heap.
- Wake dependents through a reverse dependency index rather than rescanning the
whole queue after every successful patch.
- Apply one envelope through one public `doc.patch` call and advance causal
state only after success.
- Materialize positional edits from their authored base plus applied
non-ancestor batches when they become ready.
- Resolve stable-id replacements against the current projection using
host-supplied resolver scopes when they become ready.
- Commit one ready envelope through exactly one public `doc.commit`, including
its materialized `selectionAfter`, and advance causal state only after
success.
- Permit an empty projection patch to advance causal state.
- Stop with structured `patch_failed` state for non-strict documents; preserve
the core `JSONDocumentError` throw policy for strict documents.
- Move to terminal `faulted` state when patch execution throws without changing
the projection and without supplying a structured `JSONDocumentError`.
- Reject reentrant ingestion as `busy` while a document patch is publishing.
- Move to terminal `faulted` state when materialization or commit execution
throws without changing the projection and without supplying a structured
`JSONDocumentError`.
- Reject reentrant ingestion as `busy` while admission, materialization, or a
document commit is running.
- Detect direct document mutation and move the inbox to `diverged` rather than
inferring a causal envelope from an observed patch.
- Dispose the document subscription explicitly.

## Performance

- Envelope validation, defensive copying, and exact comparison are linear in
envelope payload size; dependency normalization also sorts the dependency
ids.
envelope payload size. A positional base is part of that payload, so its
admission and deduplication cost grow with the base document. Dependency
normalization also sorts the dependency ids.
- Known-id lookup is average `O(1)`. Ready selection is `O(log V)`, and each
reverse dependency edge is released once.
- Cycle preflight first consults the reverse dependency index. A new id with no
Expand All @@ -96,10 +155,15 @@ Successful `ingest` results report only this call's `applied`, `pending`, and
- `current()` sorts and defensively copies frontier and queue observations, so
its cost grows with the exposed state. `ingest()` does not call `current()`;
callers pay that observation cost only when they request a snapshot.
- The module does not clone, diff, or stringify the whole document. Core patch
application and schema/refinement validation can still dominate runtime.
- Exact dedup retains accepted envelope payloads, and the causal ledger has no
compaction in this lab. Memory therefore grows with accepted work.
- Ready positional materialization walks the envelope's transitive causal past
and filters the applied ledger before delegating to the rebase planner.
Stable-id materialization pays the configured resolver scan and document
preflight costs.
- Exact dedup retains accepted authored payloads, and the causal ledger has no
compaction in this lab. An inbox configured for positional materialization
also retains its concrete applied-patch ledger. Memory therefore grows with
accepted work, while direct-only and stable-id-only inboxes avoid the extra
applied-patch copy.

## Non-goals

Expand All @@ -109,10 +173,14 @@ Successful `ingest` results report only this call's `applied`, `pending`, and
ready set known to the current drain is ordered deterministically.
- No local submit/envelope generation, acknowledgement, transport, reconnect,
offline queue, persistence, checkpoint, or compaction format.
- No generic protocol or materialization Adapter interface yet.
- No ready-time positional rebase, stable-id lookup, target repair, automatic
retry, conflict resolution, or independent-branch skipping after failure.
- No selection, presence, DOM input, render scheduling, or focus policy.
- No generic protocol or materialization Adapter interface. The two concrete
policies are deliberately a closed union.
- No target repair beyond conservative positional rebase or one stable-id
field replacement; no automatic retry, conflict resolution, or
independent-branch skipping after failure.
- No DOM Selection, multi-range or text-offset transform, presence, DOM input,
render scheduling, or focus policy beyond committing one headless Pointer
`selectionAfter` returned by a materializer.
- No support for multiple causal owners mutating the same document projection.
- No change to the `JSONDocument` public interface or core state.

Expand Down Expand Up @@ -144,8 +212,22 @@ that publication back across modules. This lab detects the changed projection
and halts as `diverged`; it does not claim crash-atomicity across arbitrary host
callbacks.

Already-materialized guarded operations can become stale while waiting for
dependencies. The inbox then blocks without rewriting their intent. The next
useful experiment is ready-time materialization with at least two concrete
policies, such as positional patch rebase and stable-id target recovery. That
evidence should come before exposing a generic Adapter seam or choosing OT/CRDT.
Ready-time materialization removes the known stale-planning gap without adding
a protocol abstraction. Positional correctness still depends on the host
supplying a base that corresponds to `dependsOn`; the inbox has no clock or
checkpoint with which to prove that relationship. Stable-id recovery still
inherits resolver scan cost, host scope correctness, and the planner's
conservative entity guard.

The positional planner also requires its concurrent batches to be ordered and
gap-free from the supplied base. A causal past need not be a prefix of this
inbox's local applied order, so filtering non-ancestors cannot prove that replay
relationship. Planner conflicts and final commit guards prevent an unsafe
overwrite, but a valid authored edit can conservatively become a false conflict
or a commit-time `patch_failed`. This tracer makes that limitation observable;
it does not claim a convergence protocol.

The concrete rebase modules are in-process dependencies. Their results are
preserved as structured failures rather than hidden behind a generic Adapter
seam. This supplies evidence for two real policies while keeping transport,
clocks, CRDT/OT, async execution, and universal intent languages out of scope.
4 changes: 4 additions & 0 deletions labs/extensions/causal-patch-inbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"typecheck": "tsc -p tsconfig.test.json --noEmit",
"verify": "npm run typecheck && npm test && npm run build"
},
"dependencies": {
"@interactive-os/json-document-patch-rebase": "0.1.0",
"@interactive-os/json-document-stable-id-rebase": "0.1.0"
},
"peerDependencies": {
"@interactive-os/json-document": "^1.0.0"
},
Expand Down
Loading