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
32 changes: 31 additions & 1 deletion apps/site/src/generated/repo-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2913,6 +2913,36 @@ export const repoCatalog = {
"text"
]
},
{
"path": "labs/extensions/stable-id-rebase",
"name": "@interactive-os/json-document-stable-id-rebase",
"status": "lab-extension",
"private": true,
"publishable": false,
"version": "0.1.0",
"description": "Lab stable-id anchored delayed change planner for json-document documents.",
"license": "MIT",
"summary": "Lab planner for materializing one delayed field replacement against the current\nPointer of a host-owned stable id.",
"guidance": {
"useFor": "materialize one delayed field replacement against a current stable-id target",
"notFor": "id policy, structural patches, CRDT/OT protocols, transport, or merge UI"
},
"publicExports": [
"StableIdRebaseDiagnostic",
"StableIdRebaseResult",
"StableIdReplaceInput",
"StableIdTarget",
"rebaseStableChange"
],
"publicExportCount": 5,
"keywords": [
"@interactive-os/json-document",
"conflict",
"headless",
"rebase",
"stable-id"
]
},
{
"path": "labs/extensions/swap-items",
"name": "@interactive-os/json-document-swap-items",
Expand Down Expand Up @@ -3285,7 +3315,7 @@ export const repoCatalog = {
"totals": {
"packages": 20,
"officialExtensions": 19,
"labExtensions": 39,
"labExtensions": 40,
"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: 39
Lab extensions: 40

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

Expand Down Expand Up @@ -68,6 +68,7 @@ Lab extensions are private candidates. They are listed to show product pressure,
| `@interactive-os/json-document-sort-items` | lab-only | 13 | sort or reverse JSON array items | query views, filters, or server sorting | Lab collection sort extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-sparse-record` | lab-only | 14 | set or remove keyed entries in sparse JSON records with add/replace/remove/no-op planning | 2D grid coordinate expansion, product key normalization, or rendered selection | Lab sparse-record entry editing extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-split-text` | lab-only | 9 | split a string into array items by a delimiter (tag input, paste-as-list) | CSV/TSV quoting, split-to-columns, or clipboard access | Lab split-text extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-stable-id-rebase` | lab-only | 5 | materialize one delayed field replacement against a current stable-id target | id policy, structural patches, CRDT/OT protocols, transport, or merge UI | Lab planner for materializing one delayed field replacement against the current Pointer of a host-owned stable id. |
| `@interactive-os/json-document-swap-items` | lab-only | 8 | exchange the positions of two items in the same array | cross-array swaps or moving to an arbitrary index | Lab swap-items extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-toggle-option` | lab-only | 9 | toggle, add, or remove a value's presence in a JSON array (tag/multi-select) | ordered insertion position or deduping an existing array | Lab toggle-option extension for `@interactive-os/json-document` documents. |
| `@interactive-os/json-document-toggle-value` | lab-only | 10 | toggle a boolean or advance an enum/value field (enum options come from the schema) | rendered toggle controls or keyboard policy | Lab toggle-value extension for `@interactive-os/json-document` documents. |
Expand Down
32 changes: 31 additions & 1 deletion docs/generated/repo-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2912,6 +2912,36 @@
"text"
]
},
{
"path": "labs/extensions/stable-id-rebase",
"name": "@interactive-os/json-document-stable-id-rebase",
"status": "lab-extension",
"private": true,
"publishable": false,
"version": "0.1.0",
"description": "Lab stable-id anchored delayed change planner for json-document documents.",
"license": "MIT",
"summary": "Lab planner for materializing one delayed field replacement against the current\nPointer of a host-owned stable id.",
"guidance": {
"useFor": "materialize one delayed field replacement against a current stable-id target",
"notFor": "id policy, structural patches, CRDT/OT protocols, transport, or merge UI"
},
"publicExports": [
"StableIdRebaseDiagnostic",
"StableIdRebaseResult",
"StableIdReplaceInput",
"StableIdTarget",
"rebaseStableChange"
],
"publicExportCount": 5,
"keywords": [
"@interactive-os/json-document",
"conflict",
"headless",
"rebase",
"stable-id"
]
},
{
"path": "labs/extensions/swap-items",
"name": "@interactive-os/json-document-swap-items",
Expand Down Expand Up @@ -3284,7 +3314,7 @@
"totals": {
"packages": 20,
"officialExtensions": 19,
"labExtensions": 39,
"labExtensions": 40,
"apps": 12
}
}
101 changes: 101 additions & 0 deletions labs/extensions/stable-id-rebase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# @interactive-os/json-document-stable-id-rebase

Lab planner for materializing one delayed field replacement against the current
Pointer of a host-owned stable id.

It complements the positional `patch-rebase` lab. Instead of replaying a patch
log, it assumes remote changes are already present in the document and asks the
official id resolver where the authored target lives now.

```ts
const planned = rebaseStableChange(doc, {
scopes: [{
scope: "card",
query: "$.columns[*].cards[*]",
readId: value => isCard(value) ? value.id : undefined,
}],
target: { scope: "card", id: "b" },
relativePath: "/title",
expected: "Draft",
value: "Reviewed",
relativeSelectionAfter: "/title",
});

if (planned.ok) {
const options = planned.selectionAfter === undefined
? {}
: { selectionAfter: planned.selectionAfter };
doc.commit(planned.operations, options);
}
```

`relativePath` and `relativeSelectionAfter` are RFC 6901 Pointers rooted at the
stable entity. A successful result's `selectionAfter` is instead an absolute
document Pointer. URI-fragment forms are accepted and returned as ordinary
canonical Pointers. `expected` is the field value observed when the delayed
input was authored; a different current value produces `target_changed` rather
than an overwrite.

## Scope

- Bind the official id resolver to the supplied document from scope
descriptors, so a resolver for another document cannot be passed by mistake.
- Resolve one delayed non-root field replacement against the current document.
- Preserve a remote move, reorder, or unrelated field edit on the same entity.
- Return the resolver's missing, duplicate, invalid-query, and missing-scope
meanings as structured identity failures.
- Prefix the replacement with an entity snapshot guard and an authored-value
guard for one atomic `doc.commit`.
- Re-run the target scope's `readId` against the preview entity and reject an
`readId`-changing replacement.
- Rebase one Pointer selection relative to the same stable entity, or report
that it was dropped when it will not exist after the change.
- Preflight the complete guarded plan through public `doc.canPatch` semantics.

## Non-goals

- No patch-log replay, transport, acknowledgement, offline queue, causal clock,
CRDT, OT, convergence, or replicated undo.
- No id generation, rekeying, repair, tombstone, or universal id field.
- No stable-entity root replacement, structural add/remove/move/copy, or
multiple target batch language.
- No identity inference below `relativePath`; nested array indexes remain
positional.
- No ABA detection when an identical entity disappears and reappears.
- No scope-wide protection against a duplicate id introduced after planning;
products needing that invariant must enforce it in their document schema.
- No text offset, multi-range selection, DOM focus, render, or input policy.
- No mutation during planning and no plugin registration.

## Friction report

The existing resolver is sufficient to reconnect a delayed input after a move
without adding identity to core. The planner creates it internally from the
provided scopes, eliminating the unsafe possibility of resolving against a
different document. Scope names must be unique and `readId` must be deterministic
and non-mutating.

After the structural dry-run, the planner applies the matching scope's `readId`
to the preview entity and verifies that the id is unchanged. It cannot rerun the
scope's JSONPath query against the preview without another schema-backed
document, so a field that changes query membership while preserving `readId`
remains a caller-owned constraint in this tracer.

The resolver exposes the current entity Pointer but not a cheap identity guard
Pointer or revision token. The planner therefore guards the entire current
entity value. That stops a second move between planning and commit, but copying
and comparing a large entity is expensive and an unrelated post-plan field edit
causes a conservative conflict. This is concrete pressure for an indexed
resolver plus identity/revision token; it is not a reason to add clocks or CRDT
state to `JSONDocument` core.

The official resolver currently scans its configured query on each `resolve`.
This tracer calls it once, but a broad scope is still linear in its match set.
`doc.canPatch` also pays the document's schema/refinement cost. One additional
structural dry-run always verifies preview identity, and selection checking
reuses that preview when requested.

The module intentionally handles one replacement. If multiple anchored edits,
identity-based destinations, or structural operations become real callers,
they should first prove a shared operation language rather than widening this
tracer speculatively.
51 changes: 51 additions & 0 deletions labs/extensions/stable-id-rebase/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@interactive-os/json-document-stable-id-rebase",
"version": "0.1.0",
"description": "Lab stable-id anchored delayed change 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/stable-id-rebase"
},
"keywords": [
"@interactive-os/json-document",
"stable-id",
"rebase",
"conflict",
"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",
"@interactive-os/json-document-id-resolver": "^0.1.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^4.1.7",
"zod": "^4.0.0",
"@interactive-os/json-document": "*",
"@interactive-os/json-document-id-resolver": "*"
}
}
7 changes: 7 additions & 0 deletions labs/extensions/stable-id-rebase/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { rebaseStableChange } from "./plan.js";
export type {
StableIdReplaceInput,
StableIdRebaseResult,
StableIdRebaseDiagnostic,
StableIdTarget,
} from "./types.js";
Loading