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
35 changes: 34 additions & 1 deletion apps/site/src/generated/repo-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2650,6 +2650,39 @@ export const repoCatalog = {
"paste"
]
},
{
"path": "labs/extensions/patch-rebase",
"name": "@interactive-os/json-document-patch-rebase",
"status": "lab-extension",
"private": true,
"publishable": false,
"version": "0.1.0",
"description": "Lab optimistic JSON Patch rebase planner for json-document documents.",
"license": "MIT",
"summary": "Lab optimistic JSON Patch rebase planner for `@interactive-os/json-document`\ndocuments.",
"guidance": {
"useFor": "plan conservative optimistic rebases over ordered applied JSON Patch batches",
"notFor": "CRDT/OT protocols, transport, durable sync queues, or merge UI"
},
"publicExports": [
"RebaseChangeInput",
"RebaseChangeResult",
"RebaseConflict",
"RebaseConflictCode",
"RebaseDiagnostic",
"RebaseDiagnosticCode",
"RebaseSchema",
"rebaseChange"
],
"publicExportCount": 8,
"keywords": [
"@interactive-os/json-document",
"conflict",
"headless",
"patch",
"rebase"
]
},
{
"path": "labs/extensions/references",
"name": "@interactive-os/json-document-references",
Expand Down Expand Up @@ -3252,7 +3285,7 @@ export const repoCatalog = {
"totals": {
"packages": 20,
"officialExtensions": 19,
"labExtensions": 38,
"labExtensions": 39,
"apps": 12
}
} as const;
Expand Down
3 changes: 2 additions & 1 deletion docs/generated/extensions-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Official extensions: 19
| `@interactive-os/json-document-search-replace` | 23 | find and replace text across document string fields | regex engines, rendered text extraction, or search UI | Official headless search and replace extension for text fields in `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-snippets` | 12 | insert reusable JSON payloads with schema-safe paste checks | slash palette UI or snippet storage | Official headless snippet insertion extension for `@interactive-os/json-document` documents. |

Lab extensions: 38
Lab extensions: 39

Lab extensions are private candidates. They are listed to show product pressure, not as shipped packages.

Expand Down Expand Up @@ -61,6 +61,7 @@ Lab extensions are private candidates. They are listed to show product pressure,
| `@interactive-os/json-document-pad-text` | lab-only | 9 | pad a string field to a minimum length (zero-padded codes/IDs) | number formatting or display-time alignment | Lab pad-text extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-paste-cells` | lab-only | 9 | paste a 2D value matrix onto a rectangular array-of-records region | TSV/CSV parsing, clipboard I/O, or auto-growing the array | Lab paste-cells extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-paste-special` | lab-only | 17 | adapt external payloads before schema-safe paste | browser clipboard I/O or autocomplete dropdowns | Lab extension for paste special. |
| `@interactive-os/json-document-patch-rebase` | lab-only | 8 | plan conservative optimistic rebases over ordered applied JSON Patch batches | CRDT/OT protocols, transport, durable sync queues, or merge UI | Lab optimistic JSON Patch rebase planner for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-references` | lab-only | 21 | track stable references and backlinks over JSON documents | route state or rendered links | Lab extension for stable references and backlinks over JSON documents. |
| `@interactive-os/json-document-renumber-items` | lab-only | 9 | sync an order/position field to each item array position after a reorder | reordering the array itself, or fractional/gap indexing | Lab renumber-items extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-round` | lab-only | 10 | round a number to a precision or nearest step (round/floor/ceil/trunc) | currency/locale formatting, or increment/clamp (see increment-number) | Lab round extension for `@interactive-os/json-document` documents. |
Expand Down
35 changes: 34 additions & 1 deletion docs/generated/repo-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2649,6 +2649,39 @@
"paste"
]
},
{
"path": "labs/extensions/patch-rebase",
"name": "@interactive-os/json-document-patch-rebase",
"status": "lab-extension",
"private": true,
"publishable": false,
"version": "0.1.0",
"description": "Lab optimistic JSON Patch rebase planner for json-document documents.",
"license": "MIT",
"summary": "Lab optimistic JSON Patch rebase planner for `@interactive-os/json-document`\ndocuments.",
"guidance": {
"useFor": "plan conservative optimistic rebases over ordered applied JSON Patch batches",
"notFor": "CRDT/OT protocols, transport, durable sync queues, or merge UI"
},
"publicExports": [
"RebaseChangeInput",
"RebaseChangeResult",
"RebaseConflict",
"RebaseConflictCode",
"RebaseDiagnostic",
"RebaseDiagnosticCode",
"RebaseSchema",
"rebaseChange"
],
"publicExportCount": 8,
"keywords": [
"@interactive-os/json-document",
"conflict",
"headless",
"patch",
"rebase"
]
},
{
"path": "labs/extensions/references",
"name": "@interactive-os/json-document-references",
Expand Down Expand Up @@ -3251,7 +3284,7 @@
"totals": {
"packages": 20,
"officialExtensions": 19,
"labExtensions": 38,
"labExtensions": 39,
"apps": 12
}
}
90 changes: 90 additions & 0 deletions labs/extensions/patch-rebase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# @interactive-os/json-document-patch-rebase

Lab optimistic JSON Patch rebase planner for `@interactive-os/json-document`
documents.

It tests how far delayed local edits can be rebased over an ordered applied-patch
stream before a collaboration protocol or CRDT is required.

```ts
const planned = rebaseChange(DocumentSchema, {
base,
concurrentBatches,
operations: [
{ op: "replace", path: "/items/1/title", value: "Reviewed" },
],
selectionAfter: "/items/1/title",
});

if (planned.ok) {
doc.commit(planned.operations, {
selectionAfter: planned.selectionAfter,
});
}
```

`base` is the immutable document snapshot from which the local operations and
the ordered, gap-free `concurrentBatches` diverged. In-process callers can retain
the structurally shared `doc.value` reference instead of cloning the whole
document. Persisted workflows must own their snapshot and patch-log format.

Pass the same Zod-compatible schema that owns the document. The lab types only
the `safeParse` slice of that schema to avoid installing a second Zod runtime;
failed parses must expose an `error.issues` collection, as core `applyPatch`
expects. The returned selection is authoritative even when its Pointer shifts;
diagnostics describe shifted operation targets and dropped selections. URI
fragment Pointers are accepted and canonicalized to ordinary RFC 6901 Pointers
in the returned plan.

## Scope

- Replay ordered concurrent batches from a shared base through public
`applyPatch` semantics.
- Validate the local batch against that same base.
- Rebase local non-root `replace` and `test` targets over concurrent `test`,
`replace`, and object/array `add` or `remove` operations.
- Shift local targets and one headless Pointer selection after concrete array
index changes.
- Treat numeric object keys as object members rather than array indexes.
- Report same-path and ancestor/descendant writes as structured conflicts.
- Revalidate the combined result so cross-field schema refinements can report a
semantic conflict.
- Return leading RFC 6902 `test` guards followed by rebased local operations for
one atomic `doc.commit`.
- Guard an independent `selectionAfter` target during the gap between planning
and commit.

## Non-goals

- No transport, server version, acknowledgement, offline queue, persistence, or
synchronization session.
- No CRDT, OT, actor identity, causal clock, tombstone, or convergence claim.
- No stable-id matching, array item identity inference, or ABA detection.
- No automatic rebase of concurrent `move`, `copy`, or root mutations.
- No automatic rebase of structural local operations while concurrent changes
exist; uncertain cases return `unsupported_operation` conflict.
- No `SelectionSnap`, multi-range selection, text-offset transform, DOM focus,
rendering, keyboard, or UI policy.
- No plugin registration and no `@interactive-os/json-document` internal
imports.

## Friction report

The public `applyPatch`, Pointer helpers, and `trackPointer` exports are enough
for a conservative pure planner. Applied operations must be used instead of raw
inputs so append paths such as `/-` have concrete coordinates.

Pointer tracking alone is not sufficient: numeric object keys look like array
indexes syntactically. The planner must inspect the replayed JSON state before
shifting array siblings. Likewise, selection freshness needs a guard even when
the selection target is independent from the mutation target.

The remaining hard cases are identity and causality. A positional Pointer
cannot prove that an item removed and reinserted at the same index is the same
logical node. A later stable-id rebase experiment should compose the official
id resolver before this lab grows a protocol or adapter interface.

Freshness guards copy the guarded JSON values. A selection outside every local
mutation therefore adds its own guard and can be expensive when it points at a
large subtree. This is deliberate for the tracer; a production protocol needs
stable identity or a cheaper revision/identity token before relaxing it.
49 changes: 49 additions & 0 deletions labs/extensions/patch-rebase/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@interactive-os/json-document-patch-rebase",
"version": "0.1.0",
"description": "Lab optimistic JSON Patch rebase planner for json-document documents.",
"private": true,
"type": "module",
"license": "MIT",
"sideEffects": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/developer-1px/json-document.git",
"directory": "labs/extensions/patch-rebase"
},
"keywords": [
"@interactive-os/json-document",
"rebase",
"conflict",
"patch",
"headless"
],
"files": [
"README.md",
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"clean": "rm -rf dist",
"build": "npm run clean && tsc -p tsconfig.json",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc -p tsconfig.test.json --noEmit",
"verify": "npm run typecheck && npm test && npm run build"
},
"peerDependencies": {
"@interactive-os/json-document": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^4.1.7",
"zod": "^4.0.0",
"@interactive-os/json-document": "*"
}
}
14 changes: 14 additions & 0 deletions labs/extensions/patch-rebase/src/copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {
JSONPatchOperation,
} from "@interactive-os/json-document";

export function copyOperations(
operations: ReadonlyArray<JSONPatchOperation>,
): JSONPatchOperation[] {
return operations.map((operation) => cloneJson(operation));
}

export function cloneJson<T>(value: T): T {
if (value === undefined) return undefined as T;
return JSON.parse(JSON.stringify(value)) as T;
}
58 changes: 58 additions & 0 deletions labs/extensions/patch-rebase/src/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
parentPointer,
type JSONPatchOperation,
type Pointer,
} from "@interactive-os/json-document";

import {
cloneJson,
} from "./copy.js";
import {
pointerRelation,
readPointerValue,
} from "./pointer.js";

export function createGuards(
state: unknown,
operations: ReadonlyArray<JSONPatchOperation>,
selectionAfter: Pointer | undefined,
): { ok: true; operations: JSONPatchOperation[] } | { ok: false; pointer: Pointer } {
const paths: Pointer[] = [];
for (const operation of operations) {
for (const path of guardPaths(operation)) addGuardPath(paths, path);
}
if (
selectionAfter !== undefined
&& !paths.some((path) => {
const relation = pointerRelation(path, selectionAfter);
return relation === "same" || relation === "ancestor";
})
) {
addGuardPath(paths, selectionAfter);
}

const guards: JSONPatchOperation[] = [];
for (const path of paths) {
const value = readPointerValue(state, path);
if (!value.ok) return { ok: false, pointer: path };
guards.push({ op: "test", path, value: cloneJson(value.value) });
}
return { ok: true, operations: guards };
}

function addGuardPath(paths: Pointer[], candidate: Pointer): void {
for (let index = paths.length - 1; index >= 0; index -= 1) {
const relation = pointerRelation(paths[index]!, candidate);
if (relation === "same" || relation === "ancestor") return;
if (relation === "descendant") paths.splice(index, 1);
}
paths.push(candidate);
}

function guardPaths(operation: JSONPatchOperation): Pointer[] {
if (operation.op === "add") return [parentPointer(operation.path) ?? ""];
if (operation.op === "move" || operation.op === "copy") {
return [operation.from, parentPointer(operation.path) ?? ""];
}
return [operation.path];
}
10 changes: 10 additions & 0 deletions labs/extensions/patch-rebase/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { rebaseChange } from "./plan.js";
export type {
RebaseChangeInput,
RebaseChangeResult,
RebaseConflict,
RebaseConflictCode,
RebaseDiagnostic,
RebaseDiagnosticCode,
RebaseSchema,
} from "./types.js";
Loading