1. Overview
This document specifies the Shared Multi-User OCR System for Mokuro Library, implementing a database-master hybrid model with per-user caching.
Core Principles
- Database as Master: PostgreSQL/SQLite is the single source of truth for all history and state.
- Per-User Snapshot Caching: Each user maintains a private snapshot file for read performance.
- Copy-on-Write Branching: Users view Admin’s “Official” branch by default; edits fork into a private branch.
- Cascade Cleanup: Deleting a branch root automatically removes all descendant patches.
2. Database Schema
2.1 OcrBranch
Tracks the current state of a volume for a specific user.
model OcrBranch { id String @id @default(ulid()) volumeId String userId String
headPatchId String // Current HEAD (latest state) headPatch Patch @relation("BranchHead", fields: [headPatchId], references: [id])
rootPatchId String? // First private patch (NULL = clean/synced) rootPatch Patch? @relation("BranchRoot", fields: [rootPatchId], references: [id])
snapshotPatchId String? // Patch ID of cached snapshot version Int @default(0) // Optimistic lock counter
@@unique([volumeId, userId])}2.2 Patch
Doubly-linked list for admin, singly-linked for users. Cascade delete enables cleanup.
model Patch { id String @id @default(ulid()) parentId String? parent Patch? @relation("HistoryTree", fields: [parentId], references: [id], onDelete: Cascade) children Patch[] @relation("HistoryTree")
nextPatchId String? @unique // Forward pointer (admin only) nextPatch Patch? @relation("AdminChain", fields: [nextPatchId], references: [id])
volumeId String userId String operation String // JSON: PatchOperation createdAt DateTime @default(now())}Genesis Patch: parentId = NULL, operation = { op: 'genesis', path: mokuroPath }
2.3 Snapshot Strategy
2.3.1 Snapshot Validity and Failure Modes
A snapshot represents the fully materialized document state at a specific patch. Snapshots are binary-valid: they are either fully valid or fully stale.
Invariant (Snapshot Validity):
A snapshot is valid if and only if snapshotPatchId === headPatchId and the ancestry
of headPatchId has not been rewritten.
Snapshots may become unusable in two distinct ways:
-
Stale snapshot The snapshot exists and its internal patch identifier matches
snapshotPatchId, butsnapshotPatchId ≠ headPatchId. In this case, the snapshot MUST be synchronized by replaying patches forward or backward until it reachesheadPatchId. -
Corrupt or missing snapshot The snapshot file is missing, or its internal patch identifier does not match
snapshotPatchId. In this case, the snapshot MUST be regenerated from genesis.
Snapshots are never partially valid.
2.3.2 Snapshot Synchronization
When a snapshot is stale, it MUST be synchronized to the current branch HEAD by replaying patches.
Synchronization MAY require replaying patches forward or backward, depending on whether the snapshot patch precedes or follows the current HEAD.
Backward synchronization may occur after undo operations.
2.3.3 Snapshot Invalidation Semantics
A snapshot MUST be treated as invalid whenever the branch’s effective state changes.
In particular, snapshot invalidation occurs when:
- The branch HEAD changes (patch, undo, redo, rebase completion, reset, officialize)
- The branch root changes (first fork, branch drag, reset, officialize)
- A rebase aborts or completes
- Patch compression rewrites the branch history
Snapshot invalidation is determined solely by observable branch state changes and does not independently reason about patch ancestry.
2.4 Lazy Genesis
Branches and genesis patches are created on-demand when a volume is first accessed.
2.5 Steady-State Invariants
The system maintains the following invariants at all times:
rootPatchId = nullif and only if the branch has no private edits.- User patches never participate in the admin forward chain.
- Redo requires a unique forward continuation; ambiguity is an invalid system state.
- Snapshots are valid only at the branch HEAD.
3. Data Types
3.1 PatchOperation
export type PatchOperation = | { op: 'genesis'; path: string } | { op: 'replace'; path: string; value: PatchValue; old_value: PatchValue } | { op: 'add'; path: string; value: PatchValue } | { op: 'remove'; path: string; old_value: PatchValue } | { op: 'reorder'; path: string; new_order: number[] };3.2 Value Types
export type Quad = [[number, number], [number, number], [number, number], [number, number]];export type Rect = [number, number, number, number];
export interface UnifiedLine { text: string; coords: Quad; }export interface UnifiedBlock { box: Rect; vertical: boolean; font_size?: number; lines: UnifiedLine[]; }
export type PatchValue = string | boolean | number | Rect | Quad | UnifiedBlock | UnifiedLine;4. Workflows
4.1 Editing (Fork-on-Write)
-
User edits trigger patch creation.
-
If
rootPatchId === null(clean), setrootPatchId = newPatch.id(fork). -
Update
headPatchId = newPatch.id. -
Prune future history If the branch HEAD is not at the tip of its private timeline (i.e., redo history exists), applying a new patch deletes all forward patches from the current HEAD to preserve a single forward path.
4.2 Undo / Redo (User: Pointer Navigation)
User undo and redo navigate a user branch’s history by moving the branch HEAD pointer backward or forward through the patch timeline. These operations do not rewrite history; they only change which patch is currently active.
Undo and redo are defined only for user branches. Admin redo is not supported.
4.2.1 User Undo
User undo reverts the most recently applied patch.
Conceptually, undo performs a backward crawl:
- If the current HEAD has a parent, the HEAD moves to that parent.
- If the current HEAD has no parent, the branch is at the start of its history and undo is not possible.
The server returns the inverse of the undone patch so the client can immediately revert local state.
4.2.2 User Redo (Forward Navigation Priority)
Redo advances the branch HEAD by exactly one patch, following a unique forward continuation determined by the following priority order:
-
Re-enter private history If the branch has a private root and the current HEAD is positioned at the fork point, redo advances into the branch’s private root.
-
Follow authoritative forward edge If the current patch defines a forward continuation via an authoritative forward edge, redo advances along that edge.
-
Follow unique child Otherwise, if the current patch has exactly one child, redo advances to that child.
-
Invalid state If multiple forward paths exist, redo is undefined and treated as an invalid system state.
Redo always advances the HEAD by exactly one patch.
4.2.3 Invalid States
Redo assumes the forward direction from the current HEAD is unambiguous.
If the patch graph provides multiple competing forward paths, or if branch pointers violate expected ordering, redo is undefined and treated as an invalid system state rather than user error.
4.2.4 Guarantees
- Undo moves the branch HEAD backward by one patch.
- Redo moves the branch HEAD forward by one patch.
- Neither operation rewrites or deletes patches.
- User branches always observe a consistent and continuous history during undo/redo navigation.
4.2.5 Backward Snapshot Synchronization
After undo operations, it is possible for a snapshot to represent a patch that is chronologically ahead of the current HEAD.
In such cases, the snapshot MUST be synchronized backward to the current HEAD before any new edits are applied.
4.3 Admin Undo With Branch Drag
Admin undo reverts the current admin HEAD by one patch. Because this operation rewrites shared history, it is only permitted when its effects on user branches are unambiguous.
The defining behavior of admin undo is branch drag: when a user branch depends on the undone admin patch, the branch’s root pointer is dragged backward together with the admin HEAD so that the branch’s history remains continuous after the rewrite.
4.3.1 Dependency Types
A user branch is considered dependent on the current admin HEAD if either:
-
HEAD-dependent branch The branch has no private edits (
rootPatchId = null) and itsheadPatchIdequals the admin head. -
ROOT-dependent branch The branch has private edits whose root patch descends directly from the admin head.
In both cases, removing the admin patch without adjustment would leave the branch with a broken ancestry.
4.3.2 Undo Rules
Let P be the current admin head and P.parent the new admin head after undo.
-
If no user branch depends on
P: The admin head pointer moves backward toP.parent. No user branches are affected. -
If exactly one user branch depends on
P: The admin head pointer moves backward toP.parent, and the dependent user branch’s root pointer is dragged backward so that the undone patch remains part of the branch’s private history. -
If more than one user branch depends on
P: Undo is rejected. Admin history is considered solidified.
4.3.3 Branch Drag Semantics
When branch drag occurs:
- The user branch’s
rootPatchIdis updated to point to the undone patch. - If the branch previously had no private history, its
headPatchIdis also updated to this patch. - If the branch already had private edits, the existing root is reattached beneath the dragged root.
Conceptually, the branch’s root moves backward together with the admin undo, preserving the branch’s visible history while allowing the admin branch to rewind.
Any patch duplication required to support this behavior is an implementation detail and does not affect the logical history model.
4.3.4 Guarantees
- Admin undo never leaves a user branch with a dangling or discontinuous history.
- Admin redo is not supported.
- User branches always observe a consistent ancestry, even when admin history is rewritten.
4.3.5 Admin Chain Invariant
After a successful admin undo, the new admin HEAD MUST NOT retain any forward
(nextPatchId) links.
Admin history may be rewritten only while the dependent user branch can be uniquely identified and dragged backward.
Admin history is always a single forward chain with no redo capability.
4.4 Rebase (Effect Propagation Model)
Rebase synchronizes a user branch with the current admin branch by replaying user edits on top of admin changes. This is implemented as an effect propagation algorithm: each admin patch is converted into an abstract effect, which is then pushed forward through the user’s patch sequence.
Rebase never mutates existing patches. Instead, it computes a transformed user patch list and later replays it on top of the admin head.
4.4.1 Patch Sequences
Let:
A = [A₁, A₂, …, Aₙ]be admin patches since the fork point (oldest → newest)U = [U₁, U₂, …, Uₘ]be user patches since the fork point (oldest → newest)
Rebase processes admin patches one at a time, updating the user patch list after each step.
4.4.2 Core Loop
For each admin patch Aᵢ:
-
Convert
Aᵢ.operationinto an EffectE(see Appendix B.2 for effect types). -
Initialize an empty list
U'. -
Iterate through the current user patch list
Ufrom left to right:- Apply
transform(Uⱼ, E) - If a conflict requiring user choice is detected, rebase pauses.
- If the transformed user patch is not discarded, append it to
U'. - Update
Eto the returned effect.
- Apply
-
Replace
U ← U'and continue with the next admin patch.
After all admin patches are processed, the final U represents the rebased user edits.
4.4.3 Effects
An Effect represents the semantic impact of an admin patch on later operations:
- Structural effects (
shift_up,shift_down,permute) describe how array indices change. - Content effects describe value replacement at an exact path.
- Effects may evolve as they pass through user patches, or be absorbed entirely.
Reorder effects store the inverse permutation of the admin reorder so that composition during rebase is well-defined.
4.4.4 Transform Semantics
Each (user patch, effect) pair is classified by their path relationship
(e.g. no overlap, structural interaction, direct conflict).
- Non-conflicting interactions deterministically rewrite paths or permutations.
- Conflicting interactions may either auto-resolve or require user choice.
- In many resolutions, the effect is absorbed and becomes identity.
Exact behaviors are defined in Appendix B.
4.4.5 Termination and Complexity (Effect-Based Rebase)
This section analyzes the termination and time complexity of the effect propagation rebase model described in Appendix B.
Termination
Effect-based rebase always terminates.
- Each user patch is transported through a finite sequence of upstream effects.
- Structural effects (index shifts, permutations) monotonically simplify as they are absorbed, rewritten, or eliminated.
- Conflict resolution rules either:
- drop a patch,
- rewrite it to be compatible with the effect, or
- absorb the effect into the patch (effect → identity).
No rule introduces new effects or increases effect complexity. Therefore, the process strictly progresses and must terminate.
Complexity Model
Let:
|A|be the number of upstream (admin) patches,|U|be the number of user patches,Dbe the maximum patch path depth (number of identity-bearing container levels),Lbe the average container size affected by structural operations.
Rebase proceeds by transporting each user patch through each upstream effect,
yielding |A| × |U| patch–effect interactions.
For each interaction:
- Intersection classification requires comparing patch paths and effect paths,
which is
O(D). - Structural transport (index shifting or permutation application) is
O(L)in the worst case. - Payload rewriting (when required) is proportional to the payload size,
which is also
O(L)on average.
Thus, each interaction costs O(D + L).
Total Time Complexity
The total time complexity is therefore: O(|A| × |U| × (D + L))
Space Complexity
The algorithm retains:
- the upstream effects
A, - the user patch sequence
U, - and the rewritten user patch sequence
U′.
Because each patch stores a path of depth D and a payload whose average size is
O(L), the space required to store a patch is O(D + L).
Therefore, space complexity is: O((|A| + |U|) × (D + L))
If effects are streamed and not retained, this reduces to: O(|U| × (D + L))
Invariant: User patches are never applied directly to admin history. All synchronization is performed by transforming user patches relative to admin effects.
Conflict Resolution: See Appendix B for detailed conflict tables.
4.5 Reset
Reset discards a user’s private edits and realigns the branch with the admin state.
Reset is performed in two phases:
- Branch state is updated atomically in the database.
- The branch snapshot is rebuilt from the admin state if required.
4.6 Officialize (Admin Fast-Forward)
Admin incorporates user’s patches:
- Verify user branch is direct descendant of admin HEAD
- Update admin
headPatchIdto user’s HEAD - Set
nextPatchIdon merged patches (doubly-link admin chain) - Reset user branch to clean state
4.7 Patch Compression
Patch compression reduces a patch sequence by eliminating edits that do not contribute to the final document state. Compression does not introduce a new reconciliation or transformation mechanism. Instead, it reuses the rebase machinery directly, applying it to a counterfactual change in history.
Where rebase introduces a new patch into the past and rewrites future patches to compensate, compression removes an existing patch from the past and rewrites future patches in the same manner.
4.7.1 Conceptual Model
At any point during compression, the patch sequence is partitioned as:
C · P · T
where:
Cis the compressed prefix (patches already accepted as part of history),Pis the patch under consideration,Tis the remaining suffix.
Compression asks the following counterfactual question:
If patch
Phad never occurred, could the remaining patches be rewritten—using the same rules as rebase—to produce the same final document state?
This question is evaluated by treating the removal of P as a change to the past and forcing the future patches to reinterpret themselves accordingly.
4.7.2 Elimination Test (Rebase Reuse)
To test whether P can be eliminated, compression performs a rebase-style propagation:
-
Compute the inverse of
Pand convert it into an effectE. -
Propagate
Eforward through the suffixTusing the same effect transformation logic used during rebase. -
During propagation, conflicts are resolved using keep_mine semantics, exactly as in rebase.
-
As in rebase, propagation may rewrite, transform, or discard patches in
T, producing a rewritten suffixT'. -
If propagation terminates with the identity effect, then the disappearance of
Phas been fully absorbed by rewriting the future.Pis eliminated.- The patch sequence becomes
C · T'.
-
If propagation terminates with a non-identity effect, then the disappearance of
Pcannot be hidden from the future.Pis subsumed into the compressed prefix.- The patch sequence becomes
C' · T, whereC' = C · P.
Compression then continues with the updated (C, T).
4.7.3 Resolution Semantics (keep_mine)
All propagation during compression uses the same resolution semantics as rebase.
Specifically, conflicts are resolved using keep_mine, which rewrites future patches to behave as if the modified history had always been the case. Resolution does not select between alternatives; it retroactively adjusts assumptions about the past while preserving the intent of the future.
Resolution never halts and may freely rewrite, transform, or discard patches in the suffix, exactly as during rebase.
4.7.4 Properties
- Patch compression is implemented by reusing the rebase effect-propagation machinery.
- Compression treats patch removal as a change to history, not as a local edit.
- No canonical patch representation is required.
- Structural and content patches are handled uniformly.
- Compression preserves the final document state.
- Time complexity is quadratic in the number of patches.
Invariant:
A patch P is eliminated if and only if rebase-style propagation of its inverse through the remaining patches—under keep_mine semantics—fully absorbs the effect of its removal.
5. API Specification
5.1 API Tree
/api/library/volume/:id├── /patch POST - Apply edit patch├── /undo POST - Undo last patch├── /redo POST - Redo undone patch├── /reset POST - Reset to admin state (user only)├── /officialize POST - Fast-forward admin to user (admin only)├── /rebase│ ├── /start POST - Start rebase session│ ├── /continue POST - Continue with resolution│ └── /abort POST - Abort rebase session├── /status GET - Get sync status├── /history GET - Get patch history└── /snapshot POST - Force snapshot rebuild5.2 Patching & Editing
POST /api/library/volume/:id/patch
- Body:
{ operation: PatchOperation, branchVersion: number } - Response:
{ success, newHeadId, newVersion, patch } - Errors:
400(validation),404(not found),409(version mismatch)
POST /api/library/volume/:id/undo
- Body:
{ branchVersion: number } - Response:
{ success, newHeadId, newVersion, patch }(patch is inverse) - Errors:
400(at genesis),404,409
POST /api/library/volume/:id/redo
- Body:
{ branchVersion: number } - Response:
{ success, newHeadId, newVersion, patch } - Errors:
400(nothing to redo),404,405(admin),409
5.3 Synchronization
POST /api/library/volume/:id/rebase/start
- Body:
{ }(empty) - Response:
{ status: 'complete', newHeadId }or{ status: 'paused', rebaseId, conflict }
POST /api/library/volume/:id/rebase/continue
- Body:
{ rebaseId, resolution: 'keep_admin' | 'keep_mine' } - Response: Same as start
POST /api/library/volume/:id/rebase/abort
- Body:
{ rebaseId } - Response:
{ status: 'aborted' }
POST /api/library/volume/:id/reset
- Body:
{ } - Response:
{ success: true } - Errors:
404,405(admin cannot reset)
5.4 Officialize
POST /api/library/volume/:id/officialize
- Body:
{ sourceBranchUserId: string } - Response:
{ success, newHeadId } - Errors:
400(cannot fast-forward),403(non-admin),404
5.5 Status & History
GET /api/library/volume/:id/status
- Response:
{ hasAhead, hasBehind, version, headPatchId }
GET /api/library/volume/:id/history
- Query:
limit,offset - Response:
{ patches: [...], total }
5.6 Strategy Pattern
| Endpoint | User | Admin |
|---|---|---|
patch | Fork-on-write | Direct edit |
undo | HEAD crawl | Branch drag or blocked |
redo | Follow child | Not available (405) |
reset | Discard edits | Not applicable (405) |
rebase | Sync with admin | Not applicable (405) |
officialize | Not applicable (403) | Fast-forward |
6. Security & Migration
6.1 Security Considerations
- Ownership checks: Users can only modify their own branches
- Admin-only: Officialize requires admin authentication
- Optimistic locking: Prevents concurrent edit conflicts
6.2 Migration
Existing .mokuro files work without migration. Genesis patches are created lazily on first access.
Appendix A: Path Specification
This appendix defines the user-facing patch operation paths. Paths use JSON-pointer–like segments with arrays addressed by numeric indices. Array appends use index array.length.
A.1 Block Operations
| Operation | Path | Value Type |
|---|---|---|
| Add Block | /pages/{p}/blocks/{b} | UnifiedBlock |
| Remove Block | /pages/{p}/blocks/{b} | N/A (old_value required) |
| Reorder Blocks | /pages/{p}/blocks | N/A (new_order required) |
| Resize Box | /pages/{p}/blocks/{b}/box | Rect |
| Set Vertical | /pages/{p}/blocks/{b}/vertical | boolean |
| Set Font Size | /pages/{p}/blocks/{b}/font_size | number |
A.2 Line Operations
| Operation | Path | Value Type |
|---|---|---|
| Add Line | /pages/{p}/blocks/{b}/lines/{l} | UnifiedLine |
| Remove Line | /pages/{p}/blocks/{b}/lines/{l} | N/A (old_value required) |
| Reorder Lines | /pages/{p}/blocks/{b}/lines | N/A (new_order required) |
| Edit Text | /pages/{p}/blocks/{b}/lines/{l}/text | string |
| Edit Coords | /pages/{p}/blocks/{b}/lines/{l}/coords | Quad |
Notes
- To append, use
{b}or{l}equal toarray.length. - Direct modification of the
lines_coordsarray is forbidden; use/lines/{l}/coords.
Appendix B: Rebase Semantics
This appendix defines how a user patch operation is transported through an upstream effect (admin change) during rebase.
B.0 Conventions
Structural effects
shift_up(A, k): an element was inserted into array at pathAat indexk.shift_down(A, k, D): an element atA/kwas removed;Dis the deleted payload.permute(A, π): array atAwas reordered by permutationπ.content(P, vA): leaf at pathPwas replaced to valuevA.
Permutation convention
π(i)maps old index → new index.- Composition is function composition where inner happens first:
- [(outer ∘ inner)(i) = outer(inner(i))]
- (matches
result[i] = outer[inner[i]])
Internal implementation note (not user-facing): reorder/permute are treated as acting “at the array node” for intersection checks (often represented by a sentinel like /-1). The semantics below remain the same.
B.1 Intersection Classification
| Type | Meaning | What Transforms |
|---|---|---|
no_hit | No overlap between user patch and effect | None |
collateral_ancestor_hit | Effect is structural on an array A, and the user patch targets a descendant inside A (but not at the same array level) | User patch path (index transport) |
collateral_descendant_hit | User patch is structural on an array A, and the effect targets a descendant inside A | Effect path (index transport) |
sibling_hit | Both are structural at the same array level | Both (mutual transport) |
direct_hit | Exact target match at the same node | Conflict |
ancestor_hit | User patch targets inside a subtree whose root node was affected | Conflict |
descendant_hit | Effect targets inside a subtree whose root node was affected by the user patch | Conflict |
Important nuance: shift_up is treated as “inserting into a gap,” so it does not produce direct_hit / ancestor_hit with an element path; it interacts structurally (e.g., sibling_hit / descendant_hit cases).
B.2 Deterministic Structural Transforms (No Conflict)
These cases rewrite indices/paths without invoking conflict resolution.
B.2.1 collateral_ancestor_hit (structural effect on array, user patch inside that array)
Let effect act on array A and user patch target A/i/... (so the first segment under A is an index i).
| effect.type | Index transport for user index i | Description |
|---|---|---|
shift_up(A, k) | i' = i + 1 if i ≥ k, else i' = i | insertion pushes later indices right |
shift_down(A, k, D) | i' = i − 1 if i > k, else i' = i | deletion pulls strictly-later indices left |
permute(A, π) | i' = π(i) | reorder maps old index to new index |
User path becomes A/i'/.... Effect itself is unchanged.
B.2.2 collateral_descendant_hit (user structural patch on array, effect inside that array)
Let user patch act on array A and effect target A/i/....
| user.op | Index transport for effect index i | Description |
|---|---|---|
add at A/k | i' = i + 1 if i ≥ k, else i' = i | insertion shifts later effect targets right |
remove at A/k | i' = i − 1 if i > k, else i' = i | deletion shifts strictly-later effect targets left |
reorder(A, ρ) | i' = ρ(i) | reorder maps old index to new index |
Effect path becomes A/i'/.... User op is unchanged.
B.2.3 sibling_hit (both structural on the same array)
Here both sides are structural on the same array A. Rebase proceeds as mutual transport:
- transport the user index/permutation through the effect, and
- update the effect index/permutation to account for the user structural change.
(a) user reorder(A, ρ) vs effect shift_up(A, k) or shift_down(A, k, D)
| effect.type | Description |
|---|---|
shift_up(A, k) | Extend the user permutation to length+1 to include the inserted element at index k (preserving relative order of existing elements). Then map the inserted element’s index through the extended permutation to determine the effect’s new index. |
shift_down(A, k, D) | First map the deleted element’s index through the user permutation to determine where that element would be after user reorder; then shrink the user permutation to length−1 by removing the deleted element’s slot. |
Note: user reorder vs effect permute is treated as direct_hit (reorder_collision), not a sibling transform.
(b) user add/remove vs effect shift_up/shift_down
Let user index be u (for add or remove at A/u) and effect index be k.
| effect.type | Transport user index u through effect | Then update effect index k through user structural change |
|---|---|---|
shift_up(A, k) | u' = u+1 if u ≥ k, else u' = u | if user inserts/removes at u' and u' < k, then k shifts by +1 (insert) or −1 (remove) |
shift_down(A, k, D) | u' = u−1 if u > k, else u' = u (the equality case is a direct_hit conflict) | if user inserts/removes at u' and u' < k, then k shifts by +1 (insert) or −1 (remove) |
(c) user add/remove vs effect permute(A, π)
| user.op | Description |
|---|---|
add at A/u | Expand effect permutation π to length+1 to account for the inserted element, then transport the user insertion index through the expanded permutation to get the rebased insertion index. The effect keeps the expanded permutation. |
remove at A/u | Transport the user removal index through π to get the rebased removal index, then shrink effect permutation to length−1 by removing the removed slot. The effect keeps the shrunk permutation. |
B.3 Conflict Resolution Matrix
Conflicts arise only for direct_hit, ancestor_hit, and specific descendant_hit cases.
| Intersection | effect.type | user.op | Conflict | Auto | keep_admin | keep_mine |
|---|---|---|---|---|---|---|
direct_hit | shift_down | add | shift_down_into_add | ✓ | keep patch; move delete index from k to k+1 | — |
direct_hit | shift_down | remove | double_delete | ✓ | drop patch; effect → identity | — |
direct_hit | permute | reorder | reorder_collision | ✗ | drop user reorder; effect permutation becomes ρ⁻¹ ∘ π | keep reorder; rewrite to π⁻¹ ∘ ρ; effect → identity |
direct_hit | content | replace | content_conflict | ✗ | drop user replace; keep effect | keep replace, but set old_value := vA; effect → identity |
ancestor_hit | shift_down | any (inside deleted subtree) | dead_zone | ✗ | drop user op; but fold user change into deleted payload D := Apply(D, r, user_op) | resurrect: output add at deleted path with D := Apply(D, r, user_op); effect → identity |
descendant_hit | any | remove | reverse_dead_zone | ✗ | drop user remove; output shift_up reinserting the (updated) old payload | keep user remove but update old_value by folding admin change into it; effect → identity |
descendant_hit | any | add | effect_shift | ✓ | keep patch; transport effect target path through the insertion (i ≥ k ⇒ i+1) | — |
descendant_hit | any | reorder | effect_shift | ✓ | keep patch; transport effect target path through the reorder (i → ρ(i)) | — |
Where:
- In
dead_zone,ris the relative path tail inside the deleted element: user targetsA/k/r. - In
reverse_dead_zone, the “fold admin change into payload” applies the admin effect (converted to an op when possible) into the removed element’s payload at the appropriate relative tail.
Invariant (operational):
keep_minereturns a concrete user op and absorbs the effect (effect → identity).keep_adminreturns no user op (op → null) and preserves a residual effect, sometimes rewritten to account for the user’s original intent.
B.4 Inversion Property (keep_admin from keep_mine)
For every conflict that supports both resolutions (dead_zone, reverse_dead_zone, reorder_collision, content_conflict), the keep_admin outcome can be derived as:
Do
keep_minefirst, obtaining(op_mine, identity),
then move the inverse ofop_mineinto the effect and setop := null.
Formally: [ \texttt{keep_admin}(u,e) \equiv (,op := null,; effect := \text(op_^),) \quad\text\quad (op_, id)=\texttt{keep_mine}(u,e) ]
Why it matches exactly here: in content_conflict, keep_mine first rewrites the user replace to have old_value := vA (the admin-set value). That makes its inverse effect precisely “set to vA,” which is exactly the keep_admin residual content effect.