CRDT-native line blame: persistent per-line authorship#1
Merged
Conversation
Persist each room's full Y.Doc as a .sharemd/<path>.yjs sidecar (gc:false) so insert authorship survives restarts, and add an `authors` map inside the doc translating per-session clientIDs to durable identities. Blame is computed from the CRDT itself via snapshot diffs — no offsets, so it survives concurrent edits by construction. - markdown stays the content source of truth: offline edits, deleted files, and corrupt/truncated sidecars reconcile as a minimal "disk"-authored splice that preserves surrounding authorship - every peer (browser, MCP agent, server hydration) registers in the authors map before editing - new surfaces: GET /api/blame/<path> and a blame_document MCP tool (computed locally from the agent's own replica) - adversarial test suite: cross-peer deletes, restarts, offline edits, corrupt/truncated sidecars, traversal on the new endpoint, unregistered peers, astral chars, same-name sessions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gp1Mh7NtAG55JBosrWizKx
plosson
added a commit
that referenced
this pull request
Jul 12, 2026
CRDT-native line blame: persistent per-line authorship
plosson
pushed a commit
that referenced
this pull request
Jul 14, 2026
Adds snapshots: named checkpoints of a document you can roll back to.
The room Y.Doc is already gc:false "so authorship (and later, snapshot
history) survives" — this builds on that anticipated seam.
Server (human-only REST, consistent with the CRUD/edit-only split):
GET /api/projects/:p/docs/*d/snapshots list versions
POST /api/projects/:p/docs/*d/snapshots {label} capture current state
POST /api/projects/:p/docs/*d/snapshots/:id/restore roll back to a version
A snapshot stores the full Y.encodeStateAsUpdate (base64) plus display
metadata in a <path>.snapshots.json sidecar, which travels on rename/move
and is removed on delete like .yjs/.log. The list endpoint strips the
heavy state blob.
Restore converges the live text forward via the existing reconcileText
middle-splice, authored to the restorer — so it lands in blame and
history and is itself reversible. It deliberately does NOT re-apply the
old CRDT state (that would fork live peers); other collaborators simply
see the text change. RoomRegistry.peek() (added for #1) is unrelated here;
capture/restore use registry.open + the new Room.snapshotState /
Room.restoreContent.
Web UI: a "versions" button opens a panel to name+save the current state
and restore any prior version (confirm dialog, API errors surfaced).
Tests: server capture/list/restore, blame attribution of a restore,
sidecar travel on move + removal on delete, 404/400 guards; a Playwright
flow that saves a checkpoint, diverges, and restores it in the real UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Git-blame-style per-line authorship, computed from the CRDT itself rather than from diffs: every Yjs item permanently carries the clientID that inserted it, so blame survives concurrent edits by construction.
gc: false) persists to.sharemd/<path>.yjsalongside the markdown, so authorship survives server restarts. The markdown file remains the content source of truth; any divergence (offline edit, deleted file, corrupt/truncated sidecar) is reconciled at hydration as a minimal prefix/suffix splice attributed todisk, preserving surrounding authorship.authorsmap — a new shared contract (src/shared/blame.ts): a Y.Map inside the doc translating per-session clientIDs to durable identities. Browser, MCP agents, and server hydration all register before editing.GET /api/blame/<path>(server-side, for future UI gutters) and ablame_documentMCP tool computed locally from the agent's own replica. Lines list every contributing author with UTF-16 char counts.Tests
45 tests pass (
bun run test:all, incl. Playwright e2e). New adversarial coverage intests/blame.test.ts:diskgets only the spliced middle.sharemd/access on the new endpoint → 400; sidecar dir hidden from/api/docsunknown; same name across sessions aggregates; astral chars don't split🤖 Generated with Claude Code
https://claude.ai/code/session_01Gp1Mh7NtAG55JBosrWizKx