Repository navigation
feat(applier): slice 2, a replica bootstrapped from an as-of share - #803
Conversation
…ls its source A replica built only from /changes records diverged from its source in two ways (local-sync-mode.md F3, F4): - Records carried no valid time, so a fact valid since 2025 replayed as valid from its commit time. Records now carry valid_from and valid_to. A later retract closes the asserted row in place, so a row a later transaction closed reports valid_to null and the closing arrives as the retract's own record. Otherwise a served record would change after the fact, and a replica would assert an already-closed fact its retract could not find. - Byte values were reduced to bytes_len. They now also carry the body as base64. Both are additive fields. New replay tests rebuild a store from the feed and compare a canonical live-fact digest: a plain control, valid time (open and bounded), bytes, and an assert record that reads the same before and after a later retract. Removing each change turns its test red. Refs aegis-xdvg.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185RxTpLsPCQFB9ni72Nzac
A functional-property supersede closed the prior value with an UPDATE and wrote no op=0 row, unlike an explicit retract. The change feed derives its records from rows, so it showed the new value and never the end of the old one, and a replica built from the feed kept both (local-sync-mode.md F1). The supersede now writes the same immutable op=0 row an explicit retract writes, at the superseding transaction. The resident read model reads op=1 rows and their retracted_tx stamp, so it is unaffected. The replay test asserts the replica holds exactly one current value; removing the new row turns it red. Refs aegis-xdvg.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185RxTpLsPCQFB9ni72Nzac
… the stream A /changes/stream page larger than the 1 MiB frame budget sent quipu.error without advancing the cursor and ended the stream. A consumer reconnecting at that cursor hit the same transaction and the same dead end forever (local-sync-mode.md F6). Such a transaction now arrives as quipu.changes.begin, numbered quipu.changes.part frames and a quipu.changes.commit barrier (design §6). Only the barrier carries the SSE id, so Last-Event-ID never lands inside a transaction and a reconnect replays it from the first part. The barrier digest is SHA-256 over the parts' data exactly as sent; each part carries the hash of the parts before it. A single record larger than a frame is refused with quipu.changes.tx_too_large and no id, leaving the cursor unchanged. Tests stream a 2500-record transaction: every part fits, the digests chain, the reassembled records equal the page, the next transaction follows, and a reconnect replays from part 1. Disabling the split turns the test red. Refs aegis-xdvg.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185RxTpLsPCQFB9ni72Nzac
chunk_transaction now returns transport-neutral frames (event name, optional cursor id, data string), and the SSE stream turns them into events only at the edge. Another transport can send the same frames and the same digests unchanged. A new test checks that the frames SSE sends equal the ones chunk_transaction returns, byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
A change page did not say which store produced it, and nothing marked a history boundary, so a follower could not tell a restored or cloned primary, or one that had removed served records, from the one it was following (local-sync-mode.md §5, F5). Every page now carries source.store_id and source.log_epoch. The epoch lives in a new one-row log_epoch table, minted when the store is created (or first opened by this version) and minted again by Store::mint_log_epoch for an operation that removes served history. Share reconstruction excludes the table, so a restored copy mints its own epoch and its followers re-bootstrap. The explicit restore/adopt ownership step is deferred to aegis-s24.2, per the xdvg.1 ruling. Tests cover the page source, an epoch stable across reopen, a new epoch after minting, and a store that predates the table getting one on first open. Removing the field turns both tests red. Refs aegis-xdvg.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185RxTpLsPCQFB9ni72Nzac
An oversized transaction's begin and commit frames carried no source,
so a follower could not epoch-check a chunked transaction the way it
checks a whole page, although a changed epoch must mean re-bootstrap,
never splice. Both frames now carry the page's {store_id, log_epoch}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…d feed One component turns the committed feed into a read-only replica, so the HA follower, a scoped follower and the in-page browser store never write their own projection logic (docs/design/shared-applier-api.md). This slice applies the frames that ship today, given as (event, data) text exactly as SSE or a WebSocket delivers them: /changes pages and the begin/part/commit frames of an oversized transaction. It does no I/O, so it builds for wasm32 too. - The first frame binds the replica to its source (store_id, log_epoch); a frame from another source returns Rebootstrap, never a splice. - Each source transaction applies in one savepoint with the persisted cursor; a chunked one is buffered, its running digest checked part by part, and nothing is visible until the commit verifies (or the caller is told to resume from the cursor). - A record the replica cannot reproduce exactly (a byte value sent as its length only, an unknown value shape) refuses the whole transaction. - Freshness is (last applied tx, time since the last frame), never bounded. The feed-replay conformance tests now use the same record->datum conversion (record_to_datum) and share live_digest, so there is one mapping. The cursor table is excluded from shares, like the log epoch it is bound to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…tract The replica equality oracle now sits beside the code it judges, so prove it sees valid time, byte content and graph (review ask). Outcome and Freshness serialize tagged by kind, the contract a browser bridge returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
A /changes page covers every transaction in its scope after the cursor it was asked for, but did not say which cursor that was. Transaction ids are global and a per-graph page skips other graphs' ids, so the applier could not tell a lost page from an ordinary skip and would splice past it. - Every page now carries `since`, and the chunked `begin` and `commit` frames repeat it. - The applier resumes (nothing applied) when a page or a chunked transaction answers a cursor above its own. A new replica starts at 0 until a bootstrap exists. A page that names no `since` is refused. - A page arriving while a chunked transaction is open discards the chunk and resumes. Before, the page could apply the chunk's transaction, move the cursor past it, and the commit was then dropped as a duplicate. Seven tests: - a dropped page; - a first page not at 0; - a page without `since`; - a page inside an open chunk; - an interrupted chunk restarted from a new begin; - a re-opened applier holding no staged parts; - a chunk that skips a transaction. With the two checks disabled, four of them fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…e applier The applier commit added `pub mod applier` to src/lib.rs, so its repository-knowledge witness hash went stale (the pendant source controls job). The record's summary still holds; only the hash changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
The shared applier's bootstrap (docs/design/as-of-share.md). A share carries the source's live facts at its watermark as ordinary change records, so bootstrap and tail use ONE record_to_datum and prove equality with the same live digest. It arrives as one `quipu.asof` frame, or as `quipu.asof.begin` / `.part` / `.commit` with the same digest chain as a chunked transaction (the part and commit checks are now shared `Chunk` methods). All or nothing. These refuse with nothing written: - a replica that is not empty; - a manifest that is not quipu-asof-share/v1, or does not claim valid time and lossless bytes; - a record that is not an assert, or is in a graph the manifest does not list; - a graph whose records do not match the manifest's count and digest (SHA-256 over sorted, key-sorted compact JSON lines, `share_graph_digest`). On success the facts and the cursor (store_id, log_epoch, watermark_tx) commit in one savepoint, and slice 1 tails from /changes?since=watermark. Seven tests: - bootstrap then tail to equality with the source (valid time, bytes, named graph, a later retraction), and the cursor survives reopening; - chunked equals single-frame, and nothing is visible before the commit; - a dropped part resumes with nothing written, and a restarted cut converges; - every untrustworthy share refused, with nothing written and no cursor; - only an empty replica; - a new epoch after bootstrap means rebootstrap; - the digest ignores key and record order but not content. With the count/digest and emptiness checks disabled, two tests fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…Database The shared applier joins the binding facade, so the in-page store and any other binding reach it through one engine: Engine::replica_apply_frame, replica_progress and replica_freshness, exported to JavaScript on the wasm Database as replicaApplyFrame / replicaProgress / replicaFreshness (JSON text in the contract Outcome and Freshness serialize to). - The first replica call makes the store a read-only replica: local writes through invoke (knot, episode, set, retract, shapes load/remove) are refused before dispatch with read_only_replica / not_started. Reads work. - The applier writes what the source already accepted, so local write validation is suspended for the applier's own writes and restored after. Re-judging source data against local shapes could refuse it and fork the replica silently. A test proves both halves: the adopted shape refuses the data when written locally, and the replica applies it from the feed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
15c8128 changed docs/book/src/reference/rest-api.md without moving its witness, so the repository-knowledge check refused the registry. Only the source_sha256 moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…n digest
Rule 6 of the shared applier API (docs/design/shared-applier-api.md): a
transaction at or below the cursor was skipped as a duplicate without
looking at it, so a redelivered tx 5 with changed records answered exactly
like an exact replay.
The applier now computes each transaction's content digest itself from the
decoded records (domain tag, store id, log epoch, source tx, then each
record's length-prefixed exact encoding, sorted), independent of framing,
and retains it in replica_applied_tx in the same savepoint as the records
and the cursor. A redelivery is Duplicate only when its digest equals the
retained one; a different digest is Stopped { DigestMismatch }, no
retained row is Stopped { DigestUnverifiable }, nothing is written, and the
stop is sticky. A frame from a new source still answers Rebootstrap.
replica_applied_tx is excluded from shares with the cursor it belongs to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
record_source uses TS in every build, so the no-default-features test build failed to compile (E0425). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…bootstrap-20261024
…ng it Slice 2 buffered every part of a chunked share in memory until the commit (found on a 9.16M-record share). The begin frame now opens ONE local savepoint; each part's facts are written as it arrives, in batches the size of a part, and its canonical lines go to a temporary table so the per-graph digests are computed in sorted order at the commit. Any failed check rolls the savepoint back with nothing written, as before; the facts and the cursor still commit together. Same-connection reads now see a share's parts before its commit. The replica has no cursor until Bootstrapped, and a reader must not use it before then. A tail frame mid-share aborts the share, and an applier opened on a store left mid-share rolls it back. The digest chain moves to src/applier_chunk.rs and counts records without keeping them when the caller streams. examples/applier_bootstrap_rss measures peak RSS for a synthetic share at a chosen size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
|
Fix for the buffering defect, now at 4cf73df. It is restacked onto #796 at 3442c9d. BEFORE: the applier held every part of a chunked share in memory until the commit. Found on a 9.16M-record share. AFTER:
Behaviour changes, stated for review:
New control: PEAK-RSS CONTROL (synthetic one-graph share, 1000 records/part, file-backed replica, release build;
The peak is flat from 1M to 3M (+0.2 MiB for 3x the records), so it no longer grows with the share. A measured run on the same 9.16M-record fixture as the producer is still OPEN: it needs that fixture and resource admission on the host. It is not claimed here. Local checks with a private target:
Still a draft on the F-series. |
…0261024 # Conflicts: # docs/knowledge/repository-knowledge.json # src/server/feed_stream_tests.rs # src/store/changes_replay_tests.rs
…bootstrap-20261024
Frame contract (wu, aegis-s24.1; consumer quipu #803): - a cut that fits one frame is a single quipu.asof {manifest, records}; - larger cuts keep F6's begin/part/commit chain; - records carry their transaction's timestamp, actor and source, as /changes records do; - "ROOT" can be cut when named; - the per-graph digest is the applier's share_graph_digest: SHA-256 over the canonical record lines, sorted, joined by "\n". The producer gets there by sorting runs of at most 32 MiB, spilling them beside the spool and merging, so memory stays bounded. A test checks it against the applier's function, verbatim, at three run sizes. Ceilings (grant's review of the design): - The time and WAL ceilings also run as SQLite's progress handler during the read phase, so one slow step is interrupted mid-statement instead of waiting for the next 10,000-record check. - Spilled digest runs count against the spool quota as they are written. Sabotages: a handler that never stops, unsorted spilled runs and an unsorted in-memory sort each turn a test red. examples/asof_budget measures a cut on a scratch copy against the design budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
Frame contract (wu, aegis-s24.1; consumer quipu #803): - a cut that fits one frame is a single quipu.asof {manifest, records}; - larger cuts keep F6's begin/part/commit chain; - records carry their transaction's timestamp, actor and source, as /changes records do; - "ROOT" can be cut when named; - the per-graph digest is the applier's share_graph_digest: SHA-256 over the canonical record lines, sorted, joined by "\n". The producer gets there by sorting runs of at most 32 MiB, spilling them beside the spool and merging, so memory stays bounded. A test checks it against the applier's function, verbatim, at three run sizes. Ceilings (grant's review of the design): - The time and WAL ceilings also run as SQLite's progress handler during the read phase, so one slow step is interrupted mid-statement instead of waiting for the next 10,000-record check. - Spilled digest runs count against the spool quota as they are written. Sabotages: a handler that never stops, unsorted spilled runs and an unsorted in-memory sort each turn a test red. examples/asof_budget measures a cut on a scratch copy against the design budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF
…0261024 # Conflicts: # src/server/feed_stream.rs # src/share_completeness.rs
…bootstrap-20261024
…refused Review findings on the shared applier: - P1: the sticky Stopped outcome lived only in memory, so a fresh Applier on the same replica tailed on past a recorded digest mismatch. The stop is now kept in replica_stop (first stop wins) and restored by open; a replica whose history disagreed with its source stays stopped until it is rebuilt. - P2: any op other than "retract" was applied as an assert. Only "assert" and "retract" are accepted, in the record conversion and the transaction digest alike; anything else refuses the transaction with nothing written. replica_stop is excluded from shares with the cursor it belongs to. Controls: a stop survives reopening (fails with open ignoring the stop), and an unknown op is refused (fails with unknown ops read as assert). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…bootstrap-20261024 # Conflicts: # src/applier.rs
Review 6387176506 (CHANGES): replica_apply_frame promoted any Engine to a read-only replica on its first call, and the applier tailed the feed into a store that already held local facts. The replica would then equal no source while its cursor and digests claim it does. - Applier::new_replica_refusal: a store may start a replica only if it already follows a source or holds no facts (shapes are not facts). - The applier refuses a tail into such a store with nothing written. - The binding checks before promoting: the frame is refused and the store stays an ordinary writable store. Paired controls: with either gate removed, its test fails. record_to_datum and value_of move unchanged to applier_record.rs, and the gate lives in applier_start.rs, to keep applier.rs under the size gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
…bridge-20261024 # Conflicts: # src/applier.rs # src/applier_tests.rs
…lier-s2-bootstrap-20261024 # Conflicts: # src/applier.rs
* fix(freeze): mint a new log epoch when freeze or thaw changes served history Deep freeze relocates a graph's rows into a pack. The graph reads the same, so a following replica needs no record, but the change feed no longer serves those rows: a replay from an older cursor silently returns fewer records (local-sync-mode.md F2). Thaw re-imports the rows behind every cursor. Both now mint a new log epoch inside their savepoint, so a follower sees the epoch change and re-bootstraps instead of trusting its cursor. Freeze emits no tombstone records: that would make a replica drop facts the source still serves. The test checks that the window graph's feed records go from present to absent and that the epoch changes on freeze and again on thaw. Removing either mint turns it red. Refs aegis-xdvg.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185RxTpLsPCQFB9ni72Nzac * fix(freeze): keep the log epoch when a freeze or thaw moves no rows A freeze that removed no feed rows, or a thaw that restored none, changed no served history. Minting a new epoch then would make every follower re-bootstrap for nothing. A refused freeze already rolled back before the mint, and a test now pins that too. The freeze test also checks that the next change page names the new epoch, which is how a follower learns its cursor belongs to the old history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * refactor(stream): build oversized-transaction parts one record at a time Move the F6 part builder into the library as feed_parts::PartBuilder. It hands out each part as it closes, so a caller can write parts to disk as they are made, and memory holds one part rather than the whole transaction. chunk_transaction now drives it. Part layout, sizes and the digest chain are byte-identical; the stream tests pass unchanged. This lets the as-of share send a cut with the same frames and digests instead of a second chunking scheme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * feat(asof-share): cut a replica bootstrap at one transaction asof_share::cut takes the live facts of 1..=64 committed graphs at transaction T, inside one read transaction, and writes them as /changes assert records into an on-disk spool of feed parts. The manifest binds source store and log epoch, watermark_tx, scope, policy and shapes digests, and per-graph record counts and digests. The read transaction is open only while spooling. Wall time and WAL growth are checked every 10,000 records, and either ceiling aborts the cut and deletes the spool. A full spool directory refuses the cut before the transaction opens. The scan rides idx_geav, so it needs no sort. Graphs served from an attached or frozen pack are refused until the frozen-pack read lands (s24.1.1). Conformance tests bootstrap a replica from the cut, tail /changes from T, and compare live-fact digests with the source. They cover: a retract after T, a write committed during the cut, a pinned T with a later assert, splitting, the ceilings and the refusals. Four sabotages (no retract bound, no tx bound, raw valid_to, no WAL check) each turn the suite red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * feat(asof-share): POST /share/asof streams a cut from its spool The handler cuts under read admission, then streams the spool as quipu.asof.begin (manifest, totals, digest, source), numbered quipu.asof.part frames and a quipu.asof.commit barrier. Only the barrier carries an SSE id. Every refusal and abort is answered before the stream starts. A cut that cannot be finished never sends a barrier. Admission: - one read phase per source; a second request gets 503 cut_in_progress with Retry-After and is not queued, so cuts never extend the pinned window back to back; - a send slot from the change stream's pool is taken first; - the spool is deleted when the stream ends, and spools older than an hour are swept. The route is write-class: it writes nothing, but this keeps it fail-closed until per-graph read authorization has its own ruling. docs: REST reference entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * feat(asof-share): meet the pinned frame contract and bound every stall Frame contract (wu, aegis-s24.1; consumer quipu #803): - a cut that fits one frame is a single quipu.asof {manifest, records}; - larger cuts keep F6's begin/part/commit chain; - records carry their transaction's timestamp, actor and source, as /changes records do; - "ROOT" can be cut when named; - the per-graph digest is the applier's share_graph_digest: SHA-256 over the canonical record lines, sorted, joined by "\n". The producer gets there by sorting runs of at most 32 MiB, spilling them beside the spool and merging, so memory stays bounded. A test checks it against the applier's function, verbatim, at three run sizes. Ceilings (grant's review of the design): - The time and WAL ceilings also run as SQLite's progress handler during the read phase, so one slow step is interrupted mid-statement instead of waiting for the next 10,000-record check. - Spilled digest runs count against the spool quota as they are written. Sabotages: a handler that never stops, unsorted spilled runs and an unsorted in-memory sort each turn a test red. examples/asof_budget measures a cut on a scratch copy against the design budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): tagged scope digest, final ceiling check, disk allowance From grant's review of the design (#799): - scope_digest is SHA-256 over compact JSON {"context_graphs","scope"}, each list sorted. Moving an IRI between the lists changes the digest; reordering a list does not. - The final time/WAL check runs after the spool is flushed and before any manifest exists, so a cut of any size, including an empty one, ends inside its ceilings or is aborted with no spool left. - The disk allowance is fixed before the transaction opens: min(quota - everything already under the spool directory, free disk - disk_floor_bytes (1 GiB)) Nested run directories are counted. No allowance refuses the cut without reading. Parts and sorted runs are charged before they are written. A write that hits ENOSPC aborts as disk_full, with cleanup. Sabotages of each (a concatenated digest, no final check, a flat usage count, no disk floor) turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * docs(asof-share): REST reference for the pinned frames; refresh witnesses The REST entry now covers: - the single quipu.asof frame; - "ROOT" when named; - record transaction fields; - the per-graph and scope digest definitions; - disk_full; - the disk allowance. rest-api.md and src/lib.rs changed, so their recorded source_sha256 witnesses are refreshed. Their summaries still hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): clippy then_some; budget harness cuts ROOT too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * perf(asof-share): one sequential rowid-order pass instead of index-order lookups The full-board budget missed its read target (611.6 s partly cold, 163.5 s warm, against 120 s). The cause is inferred from 23% CPU while cold: random I/O. The scan walked idx_geav (g, e, a, v) with one table lookup per row, plus one transactions lookup per record. The cut is now one pass over facts in rowid order (NOT INDEXED) for every selected graph at once: - the read is sequential; - consecutive rows share a transaction, so its metadata is read once per transaction; - the per-graph digest sorts its lines, so emission order does not reach it; - every graph's digest run shares one memory budget, and the largest run spills first. Tests: the plan is exactly "SCAN facts". Each record carries its own transaction's time. Sabotages (a stale transaction cache, unsorted spilled runs) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): name the transaction metadata tuple for clippy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * perf(asof-share): serialize each record once; close the read before merging One instrumented cold release run on the production-sized scratch copy (23.8 us/record): frame 38.4%, sql_step 20.5%, serialize 18.2%, terms 11.4%. perf is unavailable (perf_event_paranoid 4), so the cut times its own phases. Fixing the top two: - frame: every record was serialized three times (digest line, size check, part text). PartBuilder now holds serialized lines and writes the part text directly, byte-identical to the serde_json form. A test keeps the old serde_json construction as the oracle. - serialize: the record is written from an AssertRecord struct whose fields are in alphabetical order (the canonical line, with no map per record), and the one line feeds both the digest and the part. Also, from grant's #799 point R2: the read transaction now closes right after the scan and its final ceiling check. Merging digest runs and writing the last part happen afterwards and never pin the WAL. Ceilings are checked after every spill. A test proves a TRUNCATE checkpoint from another connection completes at that point, and is blocked while reading. Sabotages (a part key renamed, record fields reordered, no early close) each turn a test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * docs(asof-share): describe the graph digest as it is computed GraphCompleteness.digest is the applier's share_graph_digest (sorted lines joined by a newline), not lines in send order; and digest runs share one memory budget across graphs. The budget harness also reports read plus digest merge, the baselines' boundary. (wu, s24.2 seam check.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): refuse a frozen graph instead of cutting it empty The refusal checked only attached_graphs, which lists graphs CONTRIBUTED by an attachment (registry source = alias). A frozen graph stays a local graph whose rows moved into its own pack, so it was not on that list. The cut read main for it and emitted it with zero records and no error: the silent-empty-graph failure the design forbids. This was found by writing the frozen-graph test first (s24.1.1): the cut returned Ok. is_frozen checks the frozen-pack registry (mounted, not thawed). The test freezes a graph and requires a refusal, both in-session and after reopening. Sabotage (drop the check) turns it red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * refactor(asof-share): split under the file-size ratchet CI's file-size ratchet failed on #805: src/asof_share.rs had 706 code lines and src/server.rs 503, against a limit of 500. - asof_share/manifest.rs: the manifest types, re-exported unchanged. - asof_share/support.rs: the record form, the read-phase guard and ceilings, disk accounting, the graph digest and the shapes hash. - The asof handler module and its route move into publication.rs, beside /acquire, so server.rs is back at its 500-line baseline. The handler uses crate:: paths, and server/asof.rs leaves the route-free list. Code moves only; no behaviour change. asof_share.rs is 450 code lines, server.rs 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF * fix(asof-share): give each cut its own spool Review on 5c86758: two cuts of the same source at the same T share a manifest, and both were renamed to <manifest>.parts. Dropping the first response deleted that path, so the second, already-complete cut emitted quipu.error instead of its commit barrier. The spool is now <manifest>-<pid>-<n>.parts, owned by one response for its whole life. The manifest digest is unchanged. identical_cuts_own_their_spools abandons, then completes, the first of two identical cuts while the second still has to open and read its spool, with a control that both share the manifest. Restoring the shared name turns it red at the commit-barrier assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzgSY2LpdGhBhb7D1nHbiF --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ootstrap-20261024 # Conflicts: # src/applier.rs
…seam Now that #805 is on main: a real asof_share::cut of a file-backed source (300 items, unicode and escaped titles, bytes, a superseded value), framed as the server sends it (one quipu.asof frame, or begin/parts/commit), bootstraps an empty replica to the source's live facts; one tampered part with the chain left intact is refused with nothing written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
|
Restacked onto main d68a89a (#798 landed); new head 4a6864a. Review scope (own commits beyond main), unchanged from before plus one test-only commit:
Seam test:
Local results:
MERGEABLE against main. Ready for one non-author codex review after fresh green. #816 restacks onto this head next. 🤖 Generated with Claude Code |
|
CHANGES at 4a6864a R1 P2 — bootstrap leaves its savepoint and partial facts live after a progress-write error. In src/applier_bootstrap.rs:238, finish_share takes self.share before INSERT INTO replica_progress. If that INSERT fails, Independent bounded file-backed counterexample: install a private BEFORE INSERT replica_progress trigger raising ABORT, then apply a valid two-fact single bootstrap. apply_frame returns Err and progress is None, but the same Store reads two facts. A second SQLite connection reads zero: this is an uncommitted transaction leak, NOT a durable partial commit. Dropping/reopening Applier clears the facts, and removing the trigger permits the exact share to bootstrap successfully. Require immediate cleanup on write/verification/commit errors, retain rollback ownership until commit succeeds, and pair error cleanup with a same-instance retry positive. Do not change source data or silently delete prior data. ONE non-author audit of all six proposal paths, after exact-head complete green and native T2 b873 PREPARED. Six private test groups: 5 PASS / 1 FAIL (the error-cleanup counterexample); positive groups cover exact integer/byte/interval values, independent-reader commit atomicity, six damaged chunk variants, seven malformed manifest/record variants, and interruption/reopen recovery. HOME157 watched paths +5 UID resources CLEAN, inner test exit101 retained. No source edits, production import, approval, merge or runtime acceptance. Tim retains SAME repair-delta custody after author repair, fresh complete green and current native admission. |
Review 6388941162 (CHANGES P2): when the progress write failed after the
share was taken, finish_share returned the error with the bootstrap
savepoint still open, so the share's facts stayed visible on the same
connection until a reopen.
Every store error while a share is open now discards the share and its
savepoint before the error is returned (bootstrap_frame wraps the share
handling once, so verify, write and finish errors are all covered).
Paired control: a_store_error_mid_share_leaves_no_fact_visible_even_on_the_same_connection
fails with the rollback removed ("the share's savepoint is still open")
and passes with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb
|
Repair for review 6388941162 (CHANGES P2), new head 988e647. One commit. Finding. A failed progress write after Fix. Paired control. Local results:
Ready for your SAME review after fresh green. 🤖 Generated with Claude Code |
|
APPROVE at 988e647 SAME repair-delta review of the prior six-path source audit. The two-file repair routes bootstrap store errors through rollback even after finish_share has taken the share state. The original progress-write failure now leaves no facts visible on the same Store; the independent-reader and reopen controls remain intact. A new independent control also verifies successful retry on the same Applier after the transient metadata failure is removed. The original six private test groups are unchanged: before 5 PASS / 1 FAIL, after all 6 PASS. The additional same-instance retry group passes, for 7 PASS / 0 FAIL total; HOME escape/resource guard clean. Exact-head hosted CI complete green, current native content review accepted 106d5342. Only the two repair paths changed; no second broad audit, production bootstrap, installation or runtime freshness claim. Prior CHANGES and its executed counterexample remain in the review history. Normal current preflight, merge hold, whole-source inverse and exact-main watch still apply. |
Slice 2 of the shared applier: bootstrap from an as-of share (
docs/design/as-of-share.md, #799). Stacked on #796 (S1, including its witness refresh c34da2b). The review scope is the last commit, b81ab48. DRAFT until the F-series and #796 land.The frame contract (for the s24.1a producer)
quipu.asof, data{"manifest": {...}, "records": [...]}.quipu.asof.begin{manifest, parts, records, digest}; thenquipu.asof.part{source_tx: watermark_tx, index, digest_before, records}; thenquipu.asof.commit{source_tx, digest}. This is the same chain as F6's chunked transaction:digestis SHA-256 over the parts' data exactly as sent, anddigest_beforeis the hash of the parts before it.schema: "quipu-asof-share/v1";source {store_id, log_epoch};watermark_tx;completeness {valid_time: true, bytes: "lossless", graphs: {<graph IRI or "ROOT">: {records, digest, frozen}}}.digestisshare_graph_digest: SHA-256 over that graph's records as compact JSON with keys sorted at every level, lines sorted, joined by\n. It may carry asha256:prefix./changesrecord form,op: "assert"only, with typed values, base64 bytes andvalid_from/valid_to.Behaviour
All or nothing, with nothing written on any refusal:
On success the facts and the cursor
(store_id, log_epoch, watermark_tx)commit in one savepoint, with outcome{"kind": "bootstrapped", "source_tx": N}. S1 then tails from/changes?since=watermark_tx.Tests (7 new)
With the count/digest and emptiness checks disabled, 2 of them fail.
Local checks:
--no-default-features,shaclandshacl,onnx,server: clean;cargo check: clean.Known inherited red: the
docs/book/src/reference/rest-api.mdknowledge witness, which went stale in F6 (#788) and is fixed there.🤖 Generated with Claude Code
https://claude.ai/code/session_01BkdpDkAkbTvXvNbNacVpJb