Skip to content

Make a change's Lamport clock a plain integer (protocol fix, breaking) - #229

Merged
crs48 merged 3 commits into
mainfrom
claude/lamport-integer
Jun 22, 2026
Merged

Make a change's Lamport clock a plain integer (protocol fix, breaking)#229
crs48 merged 3 commits into
mainfrom
claude/lamport-integer

Conversation

@crs48

@crs48 crs48 commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What & why

While scoping a live Swift↔hub sync client for XNetKit, I found a real spec↔implementation divergence: the runtime hashed a change's lamport field as the object { time, author }, but the protocol spec (02-data-model.md §5: lamport: number), the conformance golden vectors, and the Python/Swift reference kernels (and XNetKit) all hash it as a plain integer.

Empirically, the same change hashed two different ways:

  • runtime (object lamport): cid:blake3:1555ea15…
  • spec/vectors/kernels (integer lamport): cid:blake3:…76fdfa20…

So real runtime changes did not match the conformance corpus, and a spec-conformant re-implementation (XNetKit) would be rejected by the hub with INVALID_HASH. The conformance corpus — the centerpiece of the "portable protocol" — didn't actually represent the protocol.

Per direction (no backwards-compat needed, not live), this aligns the runtime to the spec: lamport is an integer; it never made sense as an object since the author already travels in authorDID.

The change (breaking)

  • Change.lamport: LamportTimestamp { time, author }number.
  • The author tiebreak for ordering/LWW comes from Change.authorDID. Change ordering is (lamport, wallTime, authorDID).
  • Materialized PropertyTimestamp gains an author: DID field (was relying on lamport.author).
  • The clock (LamportTimestamp/LamportClock in clock.ts) keeps its internal (time, author) pair; only the value placed on a change and hashed is now a number.

Swept across sync (change/chain/integrity/yjs-change/serializers v1–v3), data (NodeStore LWW, PropertyTimestamp, adapters), hub (node-relay deserialize), history, runtime, react, data-bridge, devtools, cli, and the Electron IPC layer. 56 files.

Verification

  • pnpm turbo run typecheck88/88 tasks pass, zero errors.
  • Conformance drift guard → 24/24 (now representing real runtime changes).
  • Per-package suites green: sync 570, data 1480, hub 400, history 138, runtime 128, data-bridge 198, react 222, devtools 51, electron 126.
  • Swift reference kernel 18/18 and XNetKit 14/14 unchanged (already integer-lamport — this PR makes the runtime match them).

This unblocks cross-language interop: a change created by the TypeScript runtime now hashes identically to one from the Python/Swift kernels and XNetKit.

🤖 Generated with Claude Code

xNet Test and others added 2 commits June 21, 2026 19:20
BREAKING: `Change.lamport` changes from `LamportTimestamp { time, author }`
to a plain `number` (the logical clock value). The author tiebreak for LWW
now comes from `Change.authorDID` (where it already lived), and the
materialized `PropertyTimestamp` gains an `author: DID` field.

This fixes a spec↔implementation divergence found while scoping the Swift
sync client: the protocol spec (02-data-model §5: `lamport: number`), the
conformance vectors, and the Python/Swift reference kernels all hash lamport
as a NUMBER, but the runtime hashed it as the object `{author,time}` — so real
changes (`cid:blake3:1555ea15…`) did not match the vectors (`…76fdfa20…`) and a
spec-conformant re-implementation (e.g. XNetKit) would be rejected by the hub.
Aligning the runtime to the spec makes the conformance corpus finally
represent reality and unblocks cross-language interop. No backwards-compat /
migration needed (not live yet).

The clock (`LamportTimestamp`/`LamportClock` in clock.ts) keeps its internal
(time, author) pair; only the value placed on the change and hashed is now a
number. Change ordering uses (lamport, wallTime, authorDID).

Touches sync (change/chain/integrity/yjs-change/serializers v1-v3), data
(NodeStore LWW + PropertyTimestamp + adapters), hub (node-relay deserialize),
history, runtime, react, data-bridge, devtools, cli, and apps/electron IPC.
88/88 typecheck tasks pass; conformance 24/24; Swift kernel 18/18; XNetKit
14/14; all per-package test suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-229 June 22, 2026 02:21 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 22, 2026
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #229.

@crs48
crs48 temporarily deployed to pr-229 June 22, 2026 02:26 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 22, 2026
@crs48
crs48 merged commit 077e515 into main Jun 22, 2026
12 of 13 checks passed
@crs48
crs48 deleted the claude/lamport-integer branch June 22, 2026 02:32
github-actions Bot added a commit that referenced this pull request Jun 22, 2026
crs48 added a commit that referenced this pull request Jun 22, 2026
## What & why

Native Swift can now **sync with a running XNet hub**, and it's proven
**end-to-end against the real TypeScript hub** — the first true
cross-language round-trip. This was unblocked by the integer-lamport
protocol fix ([#229](#229)).

`xnet-sync-demo`, run live against `node packages/hub/dist/cli.js
--no-auth`:

```
→ connected to ws://localhost:31999 as did:key:z6Mkv1o2…
→ created node IDF8vLzk-1v5Z75daBfvX (1 signed change)
→ published to room 'swift-interop-…' — the hub verifies hash + Ed25519 signature
← hub returned 1 change(s) for the room
← reader verified + applied 1 change(s)
✅ ROUND-TRIP OK
   reader sees: title=Sync from native Swift status=todo
```

A Swift-signed, integer-lamport change is **verified (hash + Ed25519)
and stored by the TypeScript hub**, then caught up by a *second* Swift
client (different identity) and materialized.

## What's added (XNetKit)

- **`HubConnection`** — an L2 replication client over
`URLSessionWebSocketTask`: version handshake, room subscribe,
`node-change` publish, and `node-sync-request`/`-response` catch-up
([`03-replication.md`](docs/specs/protocol/03-replication.md)).
- **`WireCodec`** — maps a Swift `Change` to/from the hub's
`SerializedNodeChange` wire shape (flat `lamportTime`/`lamportAuthor`,
base64 `signatureB64`).
- **`NodeStore.onLocalChange`** hook (publishes local writes) +
**`JSONValue.toFoundation()`** serializer.
- **`xnet-sync-demo`** executable + a headless **`WireCodecTests`** JSON
round-trip.

```swift
let conn = HubConnection(url: URL(string: "wss://hub.xnet.app")!, did: identity.did)
try await conn.connect()
store.onLocalChange = { change in Task { try await conn.publish(change, room: docId) } }
for change in try await conn.syncRequest(room: docId, sinceLamport: 0) { store.apply(change) }
```

## Verification

- `swift test` → **16 pass** (kernel/store/live-query/canonical +
wire-codec round-trip).
- **Live E2E**: `swift run xnet-sync-demo ws://localhost:31999` against
an anonymous reference hub → `✅ ROUND-TRIP OK`.
- The Swift package is top-level (`swift/`, outside the pnpm workspace)
so it doesn't touch TS CI; changelog fragment included.

## Scope

`HubConnection` does publish + catch-up. Still pending (tracked in the
exploration): a long-lived streaming subscription for real-time inbound
relays + awareness/presence, a SwiftUI sample app, persistence (GRDB),
and the web-app-on-the-same-hub LWW convergence test. The live E2E
requires a running hub, so (like the conformance kernels) it's a
documented manual check, not a CI job.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48 added a commit that referenced this pull request Jun 22, 2026
## What & why

Completes the live-sync story for XNetKit: a native Swift client can now
**stream relayed changes in real time**, not just publish + catch up on
demand. Built on [#231](#231) (publish
+ catch-up) and unblocked by
[#229](#229) (integer lamport).

Proven **end-to-end against the reference TypeScript hub**
(`xnet-sync-demo`):

```
→ writer connected … created node … and published its signed change
✅ CATCH-UP OK — reader sees: title=Sync from native Swift
✅ STREAMING OK — reader received a live relayed update: title=Updated live from Swift
```

A second native-Swift client (different identity) catches up via
`node-sync-request`, then **receives a hub-relayed update the moment the
writer publishes it** — full bidirectional, real-time native-Swift ↔
TS-hub sync.

## What's added

- **`HubConnection.subscribe(room:)`** — sends `{type:'subscribe',
topics:['xnet-doc-<room>']}`.
- **`HubConnection.startStreaming()`** + **`onRemoteChange`** — a
background read loop that decodes relayed `{type:'publish',
data:{type:'node-change', change}}` frames and delivers each (verified)
change to the handler in real time.
- `xnet-sync-demo` extended to demonstrate catch-up **and** a live
streamed update.

```swift
conn.onRemoteChange = { change in store.apply(change) }   // verifies before applying
try await conn.subscribe(room: docId)
conn.startStreaming()
```

The control phase (handshake, `syncRequest`) reads directly; the
streaming phase uses the background loop. `publish` is safe during
streaming; `syncRequest` should precede it on a given connection
(documented).

## Verification

- `swift test` → **16 pass**.
- **Live E2E**: `swift run xnet-sync-demo ws://localhost:32001` against
an anonymous reference hub → `✅ CATCH-UP OK` + `✅ STREAMING OK`.
- Top-level `swift/` package (outside the pnpm workspace) — doesn't
touch TS CI; changelog fragment included. The live E2E needs a running
hub, so (like the conformance kernels) it's a documented manual check.

## Scope

Still pending (tracked in the exploration): awareness/presence, the Yjs
document-body codec, a SwiftUI sample app, persistence (GRDB), and the
unified send+receive router for `syncRequest` during an active stream.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant