Skip to content

refactor: consolidate commit ingest, subscribe auth, and bind-twin fixtures - #1274

Draft
joepio wants to merge 7 commits into
developfrom
cursor/consolidate-ingest-subscribe-auth-88f8
Draft

refactor: consolidate commit ingest, subscribe auth, and bind-twin fixtures#1274
joepio wants to merge 7 commits into
developfrom
cursor/consolidate-ingest-subscribe-auth-88f8

Conversation

@joepio

@joepio joepio commented Aug 15, 2026

Copy link
Copy Markdown
Member

Related Issues

Follows the duplication audit in #1273. This branch includes that planning work plus the first consolidations that pass the contract.

Checklist

  • Add changelog entry linking to issue, describe API changes
  • Add or update tests if needed
  • Update docs if needed

Consolidation 1 — commit ingest + subscribe auth

  • Kind: policy-split (commit ingest) + delete-duplicate (subscribe check_read)
  • Job: apply a signed JSON-AD commit to a Db; authorize a WS subscribe.
  • Scope paths: lib/src/sync/engine.rs, lib/src/sync/ingest.rs, lib/src/sync/ws_apply.rs, lib/src/sync/mod.rs, server/src/commit_monitor.rs, server/src/handlers/commit.rs
  • Characterization tests committed before the production diff: yes (test(sync): pin replica WS commit ingest skipping write rights)
  • scripts/consolidation-measure.py --baseline exits 0: yes
Before (develop) After
Production non-blank 2358 2284 (-74)
Largest file engine.rs 1084 engine.rs 918
Public items 39 39

CommitIngestOpts::replica() still skips write-rights and timestamp. WASM applyCommit is not folded in (signature off — a fourth policy).

Remaining path: ingest_commit_json with CommitIngestOpts::{hub,peer,replica}. Subscribe handlers call authorize_read.

Consolidation 2 — bind-twins (search + server URL)

  • Kind: bind-twins
  • Job: keep Rust/TS Tantivy escape and browser/Flutter normalizeServerUrl producing the same bytes.
  • Scope paths: lib/src/client/search.rs, browser/lib/src/search.ts, browser/data-browser/src/helpers/serverUrl.ts, flutter/lib/atomic/server_url.dart
  • Shared fixtures: testdata/search-query.json, testdata/server-url.json
  • scripts/consolidation-measure.py --kind bind-twins --baseline exits 0: yes (365 → 365, no third copy)
  • Deleted the third Tantivy-escape body in file_search_repro.rs (looks up the fixture)
  • Empty server-URL input is unbound: TS returns https://, Dart returns ''. Not "fixed" here.
  • Flutter was not available in this environment; Dart tests load the same JSON the browser suite already passed.

Also

  • flutter/AGENTS.md no longer claims Loro is missing (Phase A landed).
  • Measure script accepts --kind bind-twins so a flat line count is not a failed gate.
  • CI mounts the whole testdata/ directory (JS, Rust, Flutter).
Open in Web Open in Cursor 

@cursor cursor Bot changed the title refactor(sync): one commit ingest path and one subscribe auth check refactor: consolidate commit ingest, subscribe auth, and bind-twin fixtures Aug 15, 2026
cursoragent and others added 7 commits August 18, 2026 17:49
Map duplicated logic across Rust, TypeScript, and Flutter, and classify
what should stay dual-maintained versus what should merge. Links existing
plans instead of restating them.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
A cleanup is only allowed to land if characterization tests pin the old
paths first, then scripts/consolidation-measure.py shows production lines
in scope went down. Policy-splits (same shape, different CommitOpts) are
called out so they cannot hide a behavior change.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Flutter catch-up applies a signed commit without re-checking ACL. Hub and
peer ingest still reject the same unauthorized signer. Characterization
for folding ws_apply::apply_commit_json into ingest_commit_json.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Fold Flutter WS apply_commit_json into ingest_commit_json via
CommitIngestOpts::replica() (signature on, rights/timestamp off). Hub and
peer keep enforcing rights. Named presets replace repeated struct
literals.

Subscribe, SubscribeDrive, and SubscribeQuery share authorize_read so
the three check_read copies cannot drift.

Measure: 2333 → 2257 production non-blank in scope; largest file
engine.rs 1084 → 918.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Mark the two landed items in the audit and add an UNRELEASED changelog
entry so the remaining work list stays accurate.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Rust, TypeScript, and Dart now load testdata/search-query.json and
testdata/server-url.json instead of each suite inventing the same
expected bytes. The File-picker repro looks up the escaped isA key
instead of shipping a third escape implementation. CI mounts the
whole testdata directory so new fixtures do not need a one-off file
mount. Empty server-URL input is left unbound: TS returns https://,
Dart returns empty.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
flutter/AGENTS.md still said Loro was missing; Phase A already landed.
scripts/consolidation-measure.py --kind bind-twins allows a flat
production line count so a shared fixture is not rejected as a no-op.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
@joepio

joepio commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Decision (accepted 2026-09-01, planning/commit-retention-floor-decision.md and runtime-boundary-decision.md): this PR goes first in the sequence #1274#1313#1254.
One ask: keep the IngestPolicy naming/shape reusable by the AtomicNode slice in lib/src/runtime/ (branch feat/atomic-node-slice), which will call the same ingest path.

joepio added a commit that referenced this pull request Sep 1, 2026
Add RFC-style decision documents, each with a "Decision needed by
maintainer" box and consequences for open PRs:

- runtime-boundary-decision.md: Rust decides, TS displays; twins only for
  pure fixtured functions; first AtomicNode slice.
- authority-unit-decision.md: drive stays the identity/replication unit;
  additive zone chain is the rights unit; what #1254 must change.
- commit-retention-floor-decision.md: envelope-on-resource floor;
  sequence #1274 -> #1313 -> #1254.
- trust-model-decision.md: the node that owns the URL is trusted with
  plaintext; anything that only stores is blind.
- schema-routes-decision.md: did:ad:frozen is the on-ramp; optional
  schema is policy; defaults fingerprint closes the repopulate gap.

Index them under "Decisions pending" in planning/README.md.

Claude-Session: https://claude.ai/code/session_019asLKBrBWY5ovyeCgtmdSd
joepio added a commit that referenced this pull request Sep 1, 2026
* docs(planning): five architecture decision documents

Add RFC-style decision documents, each with a "Decision needed by
maintainer" box and consequences for open PRs:

- runtime-boundary-decision.md: Rust decides, TS displays; twins only for
  pure fixtured functions; first AtomicNode slice.
- authority-unit-decision.md: drive stays the identity/replication unit;
  additive zone chain is the rights unit; what #1254 must change.
- commit-retention-floor-decision.md: envelope-on-resource floor;
  sequence #1274 -> #1313 -> #1254.
- trust-model-decision.md: the node that owns the URL is trusted with
  plaintext; anything that only stores is blind.
- schema-routes-decision.md: did:ad:frozen is the on-ramp; optional
  schema is policy; defaults fingerprint closes the repopulate gap.

Index them under "Decisions pending" in planning/README.md.

Claude-Session: https://claude.ai/code/session_019asLKBrBWY5ovyeCgtmdSd

* docs(planning): match the decisions index to the documents

Claude-Session: https://claude.ai/code/session_019asLKBrBWY5ovyeCgtmdSd

* docs(planning): record the five architecture decisions as accepted

All five RFCs accepted 2026-09-01. Status boxes record the outcome, the
README table is renamed to "Decisions", and encryption.md is closed to
"at-rest + vault" with an explicit reopen test.

Claude-Session: https://claude.ai/code/session_019asLKBrBWY5ovyeCgtmdSd
joepio added a commit that referenced this pull request Sep 1, 2026
Adds `atomic_lib::runtime::AtomicNode`, the thin node surface proposed in
planning/runtime-boundary-decision.md. It wraps `Db` and delegates every
method to code that already existed, so there is no behaviour change:

- `open(NodeConfig)` -> `Db::init_memory/init_redb/init_redb_file/init_redb_opfs`
- `get` -> `get_resource_extended`, `query` -> `Storelike::query`
- `apply_commit(json, IngestPolicy)`:
  Hub/Peer -> `sync::engine::ingest_commit` (extracted from
  `ingest_commit_json`, which now wraps it and serialises the response),
  Replica -> `sync::ws_apply::apply_commit_json` (now returns
  `CommitResponse`), LocalCache -> the WASM `applyCommit` path.
- `mutate(ResourceEdit)` -> `Resource::save_locally` / `save_as_genesis`
- `subscribe()` -> `Db::subscribe_events`
- `sync_with_peer` (iroh) -> `sync::peer::sync_drive_with_peer_outcome`

`IngestPolicy::{Hub, Peer, Replica, LocalCache}` mirrors the
`CommitIngestOpts::{hub, peer, replica}` presets of #1274 plus the WASM
no-validation path, so that PR can map onto it 1:1 once merged.

The WASM `ClientDb` is the first consumer: it now holds an `AtomicNode`
and routes `applyCommit` and `query` through it.

Tests: two nodes in one process, `mutate` on one, `apply_commit(Peer)`
on the other, query and `DbEvent` reflect it; LocalCache accepts a
tampered signature while Peer rejects it; mutate without agent errors.

Claude-Session: https://claude.ai/code/session_019asLKBrBWY5ovyeCgtmdSd
joepio added a commit that referenced this pull request Sep 1, 2026
Adds `atomic_lib::runtime::AtomicNode`, the thin node surface proposed in
planning/runtime-boundary-decision.md. It wraps `Db` and delegates every
method to code that already existed, so there is no behaviour change:

- `open(NodeConfig)` -> `Db::init_memory/init_redb/init_redb_file/init_redb_opfs`
- `get` -> `get_resource_extended`, `query` -> `Storelike::query`
- `apply_commit(json, IngestPolicy)`:
  Hub/Peer -> `sync::engine::ingest_commit` (extracted from
  `ingest_commit_json`, which now wraps it and serialises the response),
  Replica -> `sync::ws_apply::apply_commit_json` (now returns
  `CommitResponse`), LocalCache -> the WASM `applyCommit` path.
- `mutate(ResourceEdit)` -> `Resource::save_locally` / `save_as_genesis`
- `subscribe()` -> `Db::subscribe_events`
- `sync_with_peer` (iroh) -> `sync::peer::sync_drive_with_peer_outcome`

`IngestPolicy::{Hub, Peer, Replica, LocalCache}` mirrors the
`CommitIngestOpts::{hub, peer, replica}` presets of #1274 plus the WASM
no-validation path, so that PR can map onto it 1:1 once merged.

The WASM `ClientDb` is the first consumer: it now holds an `AtomicNode`
and routes `applyCommit` and `query` through it.

Tests: two nodes in one process, `mutate` on one, `apply_commit(Peer)`
on the other, query and `DbEvent` reflect it; LocalCache accepts a
tampered signature while Peer rejects it; mutate without agent errors.

Claude-Session: https://claude.ai/code/session_019asLKBrBWY5ovyeCgtmdSd
joepio added a commit that referenced this pull request Sep 5, 2026
… History

Every applied signed commit leaves its JSON-AD on the resource it changed,
keyed pure_id || createdAt || signature, written in the apply transaction.
Not a resource, not indexed. Retention per node: latest (default, the
envelope that produced the current state) or all (a signed audit log),
via --envelope-retention / ATOMIC_ENVELOPE_RETENTION.

Every commit's Loro change carries a token (browser drain token; Rust
builder and create_did now tag too), so an envelope maps to the History
version it produced. envelopes::attribute_history verifies signatures
with the apply code, credits each token to one envelope (the genesis
carrier only to a genesis envelope), and reports completeness.

Read paths: GET /history-attribution (read-gated), WASM
ClientDb.historyAttribution, Store.getHistoryAttribution merging both.
History shows by <agent> Verified / Unverified / Unattributed.

The destroy envelope on the tombstone value (#1370) is now the subject's
latest row in this tree; the tombstone is a marker again.

Tests: lib envelopes (8), server it history_attribution, browser lib
history-attribution (5), e2e history assertions. Planning: decision doc
amended (no #1274 gating, retention knob), auditability doc now Building.
joepio added a commit that referenced this pull request Sep 5, 2026
…on, attributable History (#1313)

* refactor(lib): persist only critical and creation commit envelopes

Ordinary content commits are applied and then discarded. Genesis, rights,
parent, and destroy stay, including unflagged creations. Drop previous-commit
validation, stop indexing Loro binaries as KV keys, and stop creating the
/commits collection.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>

* refactor(browser): stop treating discarded commit envelopes as resources

UI reads author and dates from the resource. History no longer offers
Show Commit, the Sync page does not link commit ids, and sequential
saves no longer set previousCommit.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>

* docs: describe commits as signed envelopes, not an event log

Align public docs, README, changelogs, and test-coverage notes with
apply-and-discard for ordinary content commits.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>

* docs(planning): verifiable History must survive clone

Proofs have to replicate with the resource. Preferred path is a sibling
Loro envelopes container so snapshot and SYNC_PUSH carry the signed log.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>

* style(lib): collapse store.ts Commit import to satisfy oxfmt

CI failed on @tomic/lib format-check: the multi-line import of
CommitBuilder / commitIdOf / Commit is a single-line import.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>

* feat: keep signed envelopes per resource (Tree::Envelopes), attribute History

Every applied signed commit leaves its JSON-AD on the resource it changed,
keyed pure_id || createdAt || signature, written in the apply transaction.
Not a resource, not indexed. Retention per node: latest (default, the
envelope that produced the current state) or all (a signed audit log),
via --envelope-retention / ATOMIC_ENVELOPE_RETENTION.

Every commit's Loro change carries a token (browser drain token; Rust
builder and create_did now tag too), so an envelope maps to the History
version it produced. envelopes::attribute_history verifies signatures
with the apply code, credits each token to one envelope (the genesis
carrier only to a genesis envelope), and reports completeness.

Read paths: GET /history-attribution (read-gated), WASM
ClientDb.historyAttribution, Store.getHistoryAttribution merging both.
History shows by <agent> Verified / Unverified / Unattributed.

The destroy envelope on the tombstone value (#1370) is now the subject's
latest row in this tree; the tombstone is a marker again.

Tests: lib envelopes (8), server it history_attribution, browser lib
history-attribution (5), e2e history assertions. Planning: decision doc
amended (no #1274 gating, retention knob), auditability doc now Building.

* style: oxfmt history-attribution test with the lib config

* fix(history): bind envelopes to deltas by version range, flat i18n-safe VersionTitle

- attribute_history read each envelope's tokens by importing its update
  into an empty doc; a browser delta has dependencies, sits pending there
  and lists no changes, so every browser edit came back with no tokens.
  Tokens are now read from the stored doc over the update's [start, end)
  range (AtomicLoroDoc::change_messages_in / update_range). Regression
  test with a real delta.
- getLoroHistory strips the drain token from Version.message for display;
  expose it as Version.token so attributionForVersion can match it.
- VersionTitle is split into flat pieces: the i18n extractor turned the
  element-spanning ternary into one placeholder message that rendered as
  [i18n-404:…] (the CI failure). Catalogues re-extracted.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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.

2 participants