Changes touching this path
- Candidate 4 (part 1): extract the engine's sync-negotiation face
engine.rs's own doc calls DagRepo "a thin composition… no storage or merge
logic itself," but the object-level sync-negotiation face — apply_with,
offered_objects, negotiation_have, closure_complete, missing_objects,
bundle_wanted, bundle_wanted_batched, and the shared bundle_impl builder — sat
inline as one more impl DagRepo block (~250 lines) among ten. Lift it into an
engine::negotiation child module (the codebase-design review's candidate 4),
the same mechanical shape as the Custody #323 extraction.
Pure relocation, zero interface/behaviour change: the methods are still
DagRepo's, and a child module reaches the engine's private object store,
change graph, and custody exactly as they did inline (super::*). bundle_impl
becomes pub(crate) because bundle_full in the engine proper still delegates to
it. loot-core (337) and loot-net (20) suites green.
Deferred (candidate 4 part 2): decoupling bundle_codec from engine::ChangeNode
with a byte-identical wire node type. That one touches the wire format, so it
carries the "must not bump FORMAT_MAJOR, prove byte-identical" constraint and
is worth its own focused pass — especially with the relay already rejecting v8.
24cc0a19 · dbf3dbe6… - Authenticate the purge lane: a purge is a signed request (#503)
A wire purge carried only an oid and yielded an unauthored tombstone, and
stow honored it before storing objects -- so any authenticated pusher could
destroy any oid across every tenant, needing no key, only the address.
ADR 0038 already called a purge event a request asking cooperating relays
and peers to destroy their copy. Cooperating meant nothing: loot honored
anyone. The signature now says who is asking; a per-receiver policy decides
whether to honor. Peers honor registered senders and quarantine strangers;
relays honor their push allowlist. Local burn is untouched and needs no key
-- only propagation requires a signature, so a keyless repo burns locally
and refuses to propagate, explicitly.
No global entitlement predicate exists: ChangeNode.tree is a full manifest
and loot duplicate copies a tree address-for-address, so authored-a-change-
referencing-this-oid is satisfiable by any cloner. Both withdrawn rules
have regression tests.
Format 9 to 10. A v10 reader parses a legacy purge lane and drops it, so no
unsigned request is honored while the rest of a v9 bundle still decodes; an
older client hard-fails on v10.
Destruction is structurally unreachable without verification: HonoredBurn
has a private field and authorize_burn is its only constructor.
Both halves are signed, with domain-separated schemes -- without tags a
maroon signature re-encodes byte-for-byte as a burn tombstone at path
length 31. Maroon entitlement is an exact Manifest grantor join, and a
grant only records a grantor when it actually installed a key, so a crafted
grant cannot plant one for content you already hold.
Closes #503.
913c5cc6 · dbf3dbe6…diff - loot-core testkit: extracted engine modules own their tests again (#660)
3412c046 · dbf3dbe6…diff - burn::Burn: one home for the burn/purge concept (#662)
8e7834a7 · dbf3dbe6…diff - Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681)
26cfbaa9 · dbf3dbe6…diff - loot push: send the change delta once per push, not once per 32-object batch (#680)
The defect, receiver-counted (#633) by the CLI tier (#635): a push of the
2000-file fixture put 14.4 MB on the wire for ~0.5 MB of bundle, because
every 32-object batch re-carried the full change delta — O(batches x delta)
where O(delta) is available, the same shape #98/#99 fixed one level up.
bundle_wanted_batched now builds the first bundle exactly as before (change
delta, attestations, purge lane, first object batch) and every follow-up as
object-only: the batch ciphertext plus the public keys for exactly those
objects. Keys stay per-batch because a receiver files a riding key only for
an object stored from the same bundle; everything else lands with bundle
one, ahead of all object bytes (the sequential send in cmd_push is
load-bearing).
Compatibility class: no format change in either direction. A Sync frame
with changes=0 has been a valid encoding since v1 (the golden fixtures pin
it) and stow stores objects without requiring a change in the same bundle,
so the deployed relay ingests the new shape on its existing path — and an
old client re-sending the delta every batch stays accepted (pinned by a
forge test). bundle_impl is untouched, so single-bundle products —
bundle_bytes, the fetch server, the metadata-only /ingest bundle — are
byte-identical.
Resume after interruption still works and is tested: an interrupted push
leaves the relay with an incomplete closure, negotiation_have (#217)
refuses to claim such heads, and the re-run re-offers the delta in its own
first bundle — once per attempt, never once per batch.
Forge: /stow defers the change at batch one and no longer resolves it
alone; the closing /ingest (whole delta, all keys, unchanged) does. An
object arriving in a change-less follow-up takes introduced_by from the
envelope pusher — same key as the author on a self-push. Documented at
loot-forge::ingest and in the ADR 0024 amendment.
Measured with loot-perf-cli from this lane against a loopback relay,
small_files/2000, reps 2, idle box: wire_bytes 14392934 -> 600236 bytes
(-95.8 percent), bit-exact story unchanged (0 discarded batches).
1f59fa97 · dbf3dbe6…diff - push negotiates the change lane against the relay /haves so a small land ships its delta, not 7 MB of re-sent history (#728)
2df34147 · dbf3dbe6…diff - the relay expands a pulling client's declared heads, so a one-behind pull ships its delta and not the whole change lane (#734)
e3f7604c · dbf3dbe6…diff - what a peer declares in a negotiation is a type now, so the head list that strands a pull cannot be passed (#799)
#40's fault-injection harness found a peer holding 3 of 6 files whose head had
advanced to the sender's tip, with the negotiation reporting up to date. The
ticket read that as "an interrupted pull cannot resume." It does resume:
`pull_via_interrupted_fetch_resumes` has been proving that since #217, and
`pull_via` declares `negotiation_have()` at all three of its call sites.
The harness resumed by declaring `bob.repo.heads()` instead. That is the
defect — not the sync path, but the fact that `have` accepted any `Vec<Oid>`
and the obvious thing to reach for was the wrong one. `Repo::heads`' own doc
said "what a peer passes as have", so the trait was teaching it. `loot clone`
was doing it too, correct only because a freshly inited repo has no heads.
What a peer may declare is now `Have`, and `negotiation_have()` is the only
thing that makes one — the #217 filter is the check the type certifies.
`Have::nothing()` is the one other constructor, for transports probing a relay
and clients with no repo to ask yet; declaring less can only over-send, and
over-declaring is the strandable direction. Threaded through the
receiver-declares half only: SyncTransport, loot_net::{offer,fetch,pull} and
the forge's signed twins.
The sender side keeps `&[Oid]` deliberately. `have` means one thing in both
directions — what the recipient of the bundle holds — so there is no second
concept to name; what differs is provenance, and a type cannot carry a
guarantee across a network. Wrapping it would advertise a check that does not
happen. That reasoning lives on `Have`'s doc, which is the only place it is
written.
Candidate 1 from the ticket — refuse to advance the head over an incomplete
closure — is not built, because ADR 0024 already priced and rejected it under
"cross-batch atomicity is deliberately not provided". A confirmation note says
so there, so the next reader does not re-derive it. The new
`tests/sync_resume.rs` asserts the head advance rather than treating it as the
failure, and covers the fault the suite genuinely missed: a bundle that is
short but valid, where every batch succeeds, the pull returns Ok, and the
operator is told it worked while holding half the files. The next pull heals
it, which is what is pinned.
Acceptance criterion 3 ("am I up to date" must answer no while any object is
absent) is deliberately not built: `closure_complete` is unsatisfiable on the
forge path, where a reader legitimately never receives ciphertext it holds no
grant for, so the warning would fire forever on every forge repo with
restricted content. That is #803.
`an_interrupted_transfer_resumes_to_convergence` runs with its ignore deleted.
271dd5cf · dbf3dbe6…diff - a key's provenance is stated at the one door, and a grant is refused at the two that bypass it (#864)
82e06601 · dbf3dbe6…diff - wants answer by address: a declared head no longer forfeits its objects (#803)
c8602e19 · dbf3dbe6…diff - the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803)
f15d576e · dbf3dbe6…diff - a push's follow-up batches stop re-walking every change and every manifest to locate their 32 addresses, because the membership and key answers that walk produced are now taken once per transfer, which makes bundle_wanted_batched's own O(total_objects x num_batches) disclaimer true for the first time, measured at -44.1% by a new bundle_batched timing that is the only number in the harness reaching a follow-up bundle
5d7fdccf · dbf3dbe6…diff - the closure walk in front of every bare mutating verb borrows each ancestor manifest instead of cloning it, which is 17.6 ms to 7.7 ms on the 1024-change closure_complete sample, and the one arm where borrowing could have changed the verdict, an ancestor the store does not hold, is pinned (#1421)
9b2a0071 · dbf3dbe6…diff - content whose embargo has already lifted stops reading as sealed, because promoting due keys out of escrow is now the construction of the reader every content read hangs off instead of a doc-comment obligation six callers hoisted by hand, and deleting four of those hoists left all 2438 tests green while loot surface told an author to request a grant from herself (#1464)
e59e46b3 · dbf3dbe6…diff - a verb stops reading every loose object in the store before it can open one, because the open now indexes the object directory and reads the file at the address it is asked for, so one complete index still answers membership, the persist's candidate set, gc's sweep and ADR 0038's burn while a burned address keeps no route back through the directory it came from, and the sixth opt-in half that can see any of this refuses a fixture whose object directory does not hold the objects it claims (#1545)
5c98eff6 · dbf3dbe6…diff - a push's first bundle stops asking the object store about every change it has already decided not to send, because the entry's ride decision is taken before the probe rather than after it, so an entry whose bytes and key both stay home is skipped instead of being looked up and discarded — the shape #1425 removed from the follow-up bundles only — and that arm stops being unmeasurable, because --batched passes have = &[] and can therefore never enter it, so a new --push-shape half passes the change one behind the tip and refuses outright any fixture whose have does not resolve to the history behind it (#1546)
a8d1806a · dbf3dbe6…diff - a push stops walking its whole object store to answer one yes-or-no, because the unsigned-tip refusal now asks whether anything is offered at all and stops at the first thing it finds, instead of building the entire unfiltered offer and then discarding it — on every push whose relay declared heads — to walk again scoped to them; and that walk stops being unmeasurable, because no cli sample can reach it at all: the tier gives every push repetition a fresh relay, so have is always empty there and the discarding branch is never entered, which a new --offer-guard half over a 128-change history reads at 34.3 ms against 17.6 us (#1559)
dca66add · dbf3dbe6…diff - a pull, a wants answer and the relay's cache restore stop faulting the whole loose object store in from disk, because the receiver's membership question is now asked of the index that put's dedup, the persist's candidate set, gc's sweep and ADR 0038's burn already answer rather than through the read #1545 made lazy, so a held address whose file vanished under a live process is no longer re-fetched, which was a five-commit-old accident that would have asked a relay to resurrect what another process had just burned, while an address absent from the object directory at open is still named and a withheld one still asks forever (#1565)
113b4588 · dbf3dbe6…diff - a document, an ADR and five code comments stop asserting things the code does not do, because the interval a perf doc invented for its own blind fixture is deleted rather than corrected, the store census that moved three times in 36 hours is deleted at all four sites that shipped it, and every place a rotted object's failed read becomes a silent negative is now a recorded decision instead of an accident of is_ok() - including the cross-store save that dropped a held object and returned Ok(()), which now propagates (#1566)
5c97b104 · dbf3dbe6…diff - the implicit-capture door stops faulting the whole object store in from disk before every bare mutating verb, because the closure walk it gates on asks the index the membership question it always meant, and that door reaches a skip only through false so a held-but-unreadable address can no longer switch it into the narrower policy #891 wrote for an incomplete closure, while the half that watches the walk stops timing a store with no files in it (#1576)
732d63fa · dbf3dbe6…diff - the negotiation walk stops re-probing an address once per change that references it, and the spelling that does it is not the one the ticket proposed: a change carries its whole tree (#288), so offered_objects probed the store 218,000 times to produce 935 distinct addresses on a 436-path 500-change fixture, 99.6 percent of it redundant, and the gated point decomposed to 2,328 of its 2,928 object_gets being negotiation alone. The skip is a probed set rather than the ticket body's addrs.contains, because addrs holds only the ACCEPTED set: a withheld (#891), burned or in-flight address is Err on every occurrence, never enters addrs, and would keep paying the full per-change cost - which is also the one case where the probe is a failed file read rather than a memo hit. That difference is not an argument, it is a test: weakening the skip to the ticket's spelling leaves the offer pin green and fails an_address_the_store_does_not_hold_is_refused_once_not_once_per_change at 55 gets where 32 are owed, which is the number its own message predicts. bundle_impl memoises exactly the two address-scoped facts, whether the store can produce it and whether it is ANYONE-granted, and that second one is what kills the per-occurrence linear grant_ids scan; vis and bytes_ride are occurrence-scoped and stay in the loop, and hoisting the Embargoed gate into the memo - the plausible over-reach, and the one a reviewer would have to rule out - was proved red by a peer receiving ciphertext it can never open. locally_missing_objects carries the same shape and is deliberately scoped out rather than folded in: its probe is ObjectStore::contains, which carries no loot_count tally at all, so this ticket's own named signal is structurally blind to it, and under the rule that a change measuring flat does not land it needs an index-probe counter before it needs an edit. The producer spelling is untouched throughout (#1565), so the first probe of every address is unchanged and this is not the contains swap #1559 measured and refused. object_gets falls 2,928 to 743, -74.62 percent against a predicted -74.6, while object_disk_reads stays flat at 200 and offer_addresses, offer_addresses_delta1 and offer_bytes are byte-identical, which is the pin that the answer did not change rather than merely got cheaper. ADR 0073's present-tense cell reading 2,928 on the gated fixture is repointed to name docs/benchmarks/series as the owner of the current figure rather than deleted, and its dated evidence blocks are left exactly as taken (#1701)
Perf-Baseline: reset #1701 dedupes the negotiation walk's per-occurrence store probes, so object_gets drops 2,928 to 743 with every artifact counter and every negotiation counter unmoved
f3cdd5b2 · dbf3dbe6…diff - bundle_impl stops visiting every manifest entry of every held-back change to answer a wants question, and this ticket's proposed fix was the wrong one: reaching the existing wanted_finalized_entries index would have moved the cost rather than removed it, because that function is the SAME O(changes times entries) walk - #1425's saving was amortising one walk across many batches, not making the walk cheaper - and it moves nothing at all on the relay /fetch path, which builds one bundle and never batches. It also cannot serve batch zero on its own terms: it has no have, so no send set and no attestations, and it is wants-scoped while bundle_impl's key lane fires for non-want entries of sending changes. The missing piece was never an index, it was a STOP RULE, so both walks got one. bundle_impl becomes two passes over one topological order, the send set unchanged and a second wants lane over held-back changes that is skipped outright when wants is absent or empty and stops as soon as every want is settled, and wanted_finalized_entries gets the same rule walked newest-first. The consequence worth naming is that held-back manifests are no longer materialized at all, which is the cost d6be741 removed from the repo open and this path was re-paying at push time; it is pinned over a saved-and-reopened repo through Manifest::is_materialized with a control that was proved to fire, since an in-memory repo defers nothing and would have made the pin vacuous. Byte-identity is differenced against one_pass_bundle, a statement-for-statement transcription of the base walk, over ten have-and-wants shapes, because the whole claim is that this is a cost change and not a behaviour change. This ticket's 512.6 ms does not reproduce: at 436 paths and 128 changes the release cost is 7.43 ms, which extrapolates in depth to about 58 ms at the ticket's 1000, roughly nine times smaller, and three confounds compound - the debug-build suspicion #1704 raised, #1701's probe memoisation landing in between, and this half keeping its objects in memory. The shape was real and the growth was real, so the fix is worth 86.0 percent at 64 by 128 and 92.5 percent at 436 by 128 on the named half, but the number that ranked this finding was wrong and saying so is worth more than repeating it. A defect this change introduced was caught by its own review and is recorded rather than quietly fixed: ride_entry returned true on a store refusal without consulting bytes_ride, so a non-want entered settled, and a sent change naming one burned or withheld address made the settled count reach the wants count with a real want still unanswered - silently skipping the #803 recovery lane and breaking byte-identity. The fixture had no unproducible address, so the headline pin was blind to it; the fixture now carries a burned one in the send set and the defect was proved red before the fix. Four loot-perf doc sites went stale by this change and are amended here rather than left: they described the held-back walk as the cost this half watches ACCUMULATE, where it is now the cost it holds GONE - the half is not blind, since re-nesting the loops returns the reading, but a future comparison is against 126 microseconds and not the number the row was born with (#1702)
68a0bded · dbf3dbe6…diff - ancestor_closure stops re-walking the whole ancestry once per seed, and unlike its two siblings from the same hunt this ticket's numbers REPRODUCE in release - 94.97 ms against a claimed 101.1 and 383.9x against a claimed 424x, both within ten percent - so the debug-build correction #1704 and #1702 each needed does not apply here and the ranking that placed this finding stands. What does repeat is the other family trait: this ticket also said the fix already exists one file away, and that was wrong again, though for a narrower reason than #1702's. GraphView::ancestor_closure is NOT set-identical, proved by running rather than assumed - it seeds its out set with whatever it is handed, so an unrecognized seed comes back inside its own closure where the per-seed loop skipped it through graph.get(h).is_some(). A probe printed DagRepo 0 against GraphView 1 for a stranger alone and 6 against 7 mixed, differing by exactly the stranger, and this is not a corner because sync.rs hands it a PEER's declared heads. A dangling parent reached mid-walk is kept by both, which is why the fix is the shared walk plus a SEED filter and not either one alone. The instrument was blind for a reason worth naming precisely: --push-shape passed vec![parent], which is the shape of a /haves REPLY, where sync.rs passes that id's whole closure - so the half was already not describing the code, making this #1576's extend-in-place case rather than #1425's honest-but-narrow one, and #1702 had re-based the same readings one commit earlier so there was no unbroken series to protect. Widening it moved the instrument 4.9x against the UNCHANGED engine, 273 microseconds to 1.33 ms at 200 by 128, which is the non-vacuity proof taken before any fix was trusted. After, the quadratic term is gone: per doubling of depth the cost grew 2.8x, 4.0x and 4.3x before and grows 1.2x, 1.4x and 1.6x after, and at depth 512 the half reads 22.713 ms against 0.608. The evidence is therefore a THREE-point chain and not a pair - the landing position's 273 microseconds, the widened fixture on the old engine at 1.33 ms, and the widened fixture on the new engine at 283 - and only the last two are the A/B, because the first is a different fixture; reporting the outer pair alone reads as a regression, so that rule is written into HUNT-PERF.md rather than left for the next reader to rediscover. The control on the widened have is the part most worth recording: the first version was a count-nonzero floor, and a two-element have of parent and root clears a floor of two, passes coverage, and still seeds a 127-deep walk twice - about ninety-nine percent of the blindness back with every control green. It is now set equality against the have's own closure, the fixed point sync.rs actually passes, with a negative arm proving that closure is a fixed point and a red proof that reverting to the floor fails on the two-seed case (#1700)
04859438 · dbf3dbe6…diff - the second review sweep's fix-up, and the item that mattered most was a correction to a correction: #1704's amendment to ADR 0072 refuted a sentence the bullet does not contain. The bullet credits an EXACT-VALUE pin, and the amendment answered that a positive-value pin is not the protection it credits - true about a greater-than-zero guard, untrue about the thing being amended, which is doc scope-drift inside the ADR whose subject is that class. Blinding the instrument settles it by running rather than by reading: with the tally back on Attributes::load, status_derives_its_policy_a_fixed_number_of_times fails on its VALUE half, and the ticket's own arithmetic was wrong in the same direction as the amendment - the blind arm reads 2 and not 1, because status still reaches Attributes::load once, while 1 is the ferry fixture's reading. Both numbers are now named in the text, the greater-than-zero finding is kept because it is real and newly demonstrated, and the head count stays at two of the four with the reason narrowed: what is still missing is a refusing FIXTURE, not a refusing pin, since two pins were each measured refusing a mis-placed tally. ferry.rs claimed the pin asserts the 2 plus term, which was wrong twice over - nothing asserted a constant at all, and the 2 being described is the per-commit coefficient rather than the pass-level term - so rather than correct the sentence the constant is now ASSERTED, as parses equals PER_PASS plus PER_COMMIT times ingested over both sweeps, fitted to measurement rather than predicted. Its red proof is the regression the old comment claimed was already pinned: a third unconditional parse per commit moves the readings to 14/14/14 and 8/14/26, where the constancy half passes AND the growth half passes and only the new assertion fires. offers_any_object's note that no address is ever reached twice is repointed because it is false in exactly the case the memo two screens up is built for - a store where every named address answers Err never returns early and re-probes each repeated address per change - and that arm is left uncovered with its cost stated as unmeasured rather than justified. The 291-against-283 disagreement turns out not to be one: four fresh runs read 296.3, 289.6, 293.2 and 283.6 microseconds, so both recorded figures sit inside the instrument's own run-to-run spread of about 4.5 percent, which is wider than the 8 microseconds they differed by. Recording a POINT was the defect, so it is now a dated spread of about 290 with its four raw readings written in one place, and measure.rs carries no number at all but points at PUSH_SHAPE - killing the duplicate rather than syncing it. PUSH_SHAPE_DEPTH's pre-#1700 pair gains the marker the three sibling sites already carried (#1717)
d895d727 · dbf3dbe6…diff - ahead and behind against a remote without pushing to find out, and the thesis constraint is met as a property of the REQUEST rather than as advice: the question carries an EMPTY PAYLOAD and every comparison is local. a relay is asked POST /haves with a zero length body, a forge POST /ref with a signed envelope over zero bytes, so a repo of one path emits BYTE IDENTICAL bytes to a repo of ten thousand and the question has no room to encode paths, object addresses, or even our own heads. the two endpoints that could are forbidden on this path and ADR 0021 now records why: /wants sends our object addresses, which are per-content identifiers, so a status in a loop hands the relay a per-path edit-frequency profile of ciphertext it cannot open - ADR 0083 refusal at a higher call rate - and /offer sends our head ids, which a push may do because a push is a CHOSEN act and status is not. the pin asserts the recorded path-and-body list SET EQUAL to exactly /info and /haves both empty, asserts the two recordings equal across two very different positions, and carries a positive control that a real push through the same stub records a NON-EMPTY body; mutated into the rejected design it goes red with /wants carrying a literal 32 byte address. offline is split POSITIVELY, the site gate SKIPPED-OFFLINE rule: only reqwest own is_connect and is_timeout may be called offline, and anything that ARRIVED - a refusal, a 404, a proxy page, a truncated body - is unusable, because those are different facts and collapsing them is how a guess gets reported as a measurement. the relay answered and holds nothing is a THIRD thing and reads as declared with a count. every unknown count renders dash or null and NEVER zero, so a machine that never reached the network cannot emit a number, and log --unpushed REFUSES rather than printing an empty listing, because a listing has no row meaning I could not ask and an empty one reads as everything is pushed. the asymmetry is stated rather than faked: unpushed is exact, being the same change lane a push would send through the same ancestor_closure, while unpulled is zero exactly when every declared head is held here and UNKNOWN otherwise, since a head declaration says what the tips are and not how deep they run. no FORMAT_MAJOR bump and the reason is recorded: ADR 0019 marker exists to prevent misparse of a durable or on-wire artifact, an unasked status emits byte identical porcelain, and the R row lives behind a flag that did not exist when the shape froze. no revset predicate either, because loot-revset is handed a GraphView and a KeyOracle and nothing else (#868), so a predicate answering over HTTP would put a network round trip inside revset::select and therefore inside grep and format-patch too (#1522)
2237a331 · dbf3dbe6…diff - shallow clone lands with the cut on the RECEIVING side, and that is why no wire moves and no format major does either: the fetch body is a format-marked pair of oid runs, so a depth field would be a new wire shape and therefore a bump, where taking the whole change lane and KEEPING n generations needs no new field, no new endpoint and no server change - a shallow client works against every relay and forge already deployed, including older ones. the price is stated rather than glossed: the change lane metadata crosses once in full on the first round, and what is saved is the object bodies, which is where a history bytes are, exact from the second round onward. the no-false-absence guard lives in THREE places and none of them is a verb - assemble, which every CLI open lands in, measures the frontier; apply_bundle_reaching, the only thing that can move one, re-measures it; and the dispatcher states it on stderr after BOTH the success and the refusal arm, because a refusal is a false absence WORST shape. it cannot be bypassed by a verb that forgets to ask: there is no path from the CLI to history that skips assemble, and the one way to open without measuring is to declare RepoNeed without graph, which makes the first history read PANIC - so the declaration that would silence the notice is the same one that aborts the verb. it rides stderr rather than the shape, so json and porcelain stay byte identical under ADR 0023, and a complete position emits nothing at all. depth never reaches the remote AT ALL, pinned three ways: every recorded request re-encoded through the real codec is exactly header plus 32 bytes per id with no room for a depth or a path, the union of every have and wants is a SUBSET of what the relay itself named in a prior answer, and the aimed one is that two positions cloning the same history at the same depth and differing ONLY in their sparse view emit BYTE IDENTICAL requests. the test relay recorder had to start capturing IDS rather than counts, because a privacy claim about a request cannot be checked against a length. two findings came out of the sweep rather than the design. one mutation stayed GREEN and refuted a claim already written into four files - that shallowness is stable because the frontier id rides the declared closure - since a declared have IS a closure claim and the held tips therefore already subtract everything behind the cut; every occurrence is now the narrow true sentence with the refutation beside it. and a count assertion caught a silent no-op: the obvious deepen posture, the closure minus the frontier, comes back with an EMPTY change lane REPORTING SUCCESS, because the remaining ids are still descendants of the cut - a deepen must declare NOTHING, and the posture is now derived from the bound so the wrong pair cannot be spelled. the body-deferring filter is NOT attempted and is the one criterion left: it needs a lazy object read on every get, surface and diff path plus a policy for what happens offline, and half-building it would put a FIFTH kind of not-here into a store that already distinguishes four (#1527)
2cccbe27 · dbf3dbe6…diff - the removal a land tripped over is retried on the store OWN transient budget and exits on the DIRECTORY SCAN rather than on what remove_file returned - and the mechanism the ticket named is REFUTED by a standalone probe. a handle held WITH share-delete unlinks with POSIX semantics on this toolchain: remove_file returns Ok and read_dir is empty IMMEDIATELY, so the pending-delete-still-appears-in-a-scan story does not exist here. the real mechanism is the handle WITHOUT share-delete, the antivirus and indexer shape, where remove_file returns os error 32 and the file is simply still there - which is why retrying is the right option rather than reconstructing the state. the reproduction is DETERMINISTIC rather than statistical: an aggressor holding that handle across the removal and releasing after 120 ms reddens the pre-fix spelling at 0 passed 2 failed and leaves the fixed one at 2 passed 0 failed, and a holder that NEVER releases, past the whole budget, fails LOUDLY with the helper own message naming the address and the last OS error rather than passing vacuously. under four emulated scanners opening every fixture object share-read-only plus four spinners, paired and interleaved across 150 rounds, the unfixed arm is 117 green and 33 RED at 22 percent while the fixed arm is 150 green and 0 red, the two disjoint - and an honest negative is recorded beside it, because an earlier weaker scanner gave 15 of 15 green on BOTH arms and therefore proves nothing on its own. the helper asserts the address is NAMED first, so it cannot pass vacuously against a store that never held it, and it is applied at all FOUR sites of the family rather than the two the ticket named, because the closure_complete siblings share the shape and flake the same way. reconstruction was rejected on evidence rather than on preference: the held-but-unreadable state is STILL PRODUCED, since object errs and holds_object holds, and four mutations show each test still biting on its own arm. and the third sighting belongs to a DIFFERENT ticket: all three predate #1667, which fixed tmp() returning the temp root ITSELF, so that 161 fixture sites shared one store across parallel threads - reproduced by running today code with the pre-#1667 body, six concurrent full suites, all six failing and this family reddening in three of them at a save. so the first control message is the shared-store class already removed, and NOTHING was added on the create side (#1596)
8fadcf40 · dbf3dbe6…diff - the removal wait stops reading a FAILED SCAN as an absent file, and the hole was that ONE fallible answer served two callers needing opposite failure behaviour: the precondition, where false-on-failure makes the assert FIRE and is safe, and the exit, where it makes the wait STOP and is not. the scan now answers three ways rather than two - named, not named, or the scan did not run. NotFound stays not-named, because an absent directory naming nothing is a statement rather than a failure; every other error is an Err; and each caller decides explicitly, the precondition panicking with its own message about failing to establish its own precondition, and the exit leaving ONLY through a scan that RAN and did not name the address, waiting a transient error out on the same store budget the removal already followed one level up. flatten is GONE, and it matters at the exit for the same reason, one entry wide: the entry whose read failed may be the very address being waited on, so flatten reports not-named for a name the scan never reached. the proof is a REAL failing scan rather than a simulated one - a regular file standing where the objects directory goes is a genuine OS refusal, error 267, reachable with no second process - and the two arrangements are DISJOINT on one fixture: with the fixed exit it is 0 passed 4 failed naming that error, and with the pre-fix exit restored it is 4 passed 0 failed, which IS the quiet success, reproduced rather than argued. #1596 is otherwise untouched, same helper and same budget. the projection neither surface derived is settled by naming WHICH QUANTITY SCALES: the honest half, being the only arm a design satisfying the never-authoritative rule can reach - so 22.3 becomes about 223 at ten times the paths, on BOTH surfaces, with measured now separated from extrapolated, since the read COUNT is linear and pinned at three sizes while the TIME was measured at one. 223 is therefore the order of magnitude at which to re-open the question rather than a reading, and the other arm about 439 is named as explicitly not the number to quote. the pin the ADR claimed is now the pin the test asserts, strengthened rather than narrowed because the numbers had already been observed: the two-per-path-plus-one relation holds EXACTLY at all three sizes, run rather than trusted, 101 against 50, 401 against 200 and 1601 against 800 - with the per-path multiplier and the fixed overhead kept as SEPARATE constants, since two-N-plus-one and three-N agree only at one, and with the old greater-than line deliberately NOT kept beside it, because over the constants this file writes it is green whatever the code does. four prose corrections ride along: a step that stated the conclusion its own section refuses, a caveat a commit message claimed and no file carried, two runbook short forms stronger than the long form they point at, and a count of three defects that lists two - which STOPS COUNTING rather than inventing a third (#1899)
a9018dad · dbf3dbe6…diff - one delta derivation had THREE spellings rather than the two the review named, and now has one producer: the surface plan, the uncaptured-paths filter and restore each built the same since-the-surface-target, walk-the-disk delta on its own - and restore doc claimed to share its base with the overwrite guard while nothing enforced it. the single place that picks the delta spec and the walk policy is now one function, and one producer picks the base and hands back base and delta together, with all three callers going through it. the vouching #1710 made into a type is UNTOUCHED: the trusted set still comes only from the live arm after its own successful read, and the producer merely passes it along, which its doc says. the store-memo cost argument is made to SURVIVE the store changing its mind rather than pinned. the bundle memo now holds only a yes-or-no answer per address and never the object, so a future bound or eviction on the store own memo cannot quietly turn it into whole-history ciphertext held for the length of a bundle. pinning the retention instead was refused, because it would fail a perfectly reasonable memory-pressure change for the wrong reason. the one path that would need bytes again now re-fetches them, and no caller reaches it today, since both passes decide to send bytes from the address alone - so if that ever changes, the cost appears as a get the counters can SEE rather than as memory nobody measures, and a new test drives that path directly. the gate reads no move on every gated counter, the probe pinning exactly one get per distinct address stays green, and the method-width tripwire moves by exactly one - where a first draft that added two was caught by the suite and a helper inlined. and the shared base is shown load-bearing rather than asserted: moving it off the surface target reddens ten tests, across the clobber guards, the due-embargo cases, a process-level bisect and the plan test (#1711)
53c0e176 · dbf3dbe6…diff - the policy counter is renamed PolicyParses and policy_parses because it has counted parses since #1704, and the policy.rs block and the ADR 0073 row now point at the variant doc instead of restating why; no stored perf record carried the old key, because the gate does not record this counter. PUSH_SHAPE_DEPTH, OFFER_GUARD_DEPTH and MISSING_DEPTH are literals rather than aliases because their reasons have diverged, and each is still 128. ride_entry takes one RideState instead of three maps, while the one_pass_bundle oracle keeps its own transcribed walk and memo so the byte-identity test still compares two walks, and still went red when the key arm was disabled. the gated counters read 743, 200 and 24 before and after (#1718)
bbd04ef4 · dbf3dbe6…diff - the zero-arity pin now asks owes_an_arity which verbs owe an arity instead of spelling its own narrower rule, and expects the same verbs as before; it went red when owes_an_arity alone exempted heads and when the heads row was re-opened. the OPEN_BUT_TAKES_NONE doc and CONTEXT.md stop listing or counting its readers and point at owes_an_arity. the Admitted doc and CONTEXT.md name the methods that do not read through the spec, as a reading of the impl rather than a rule, and with_leading_word now says it rewrites argv by position. the flags.rs censuses take the src half of the source_walk walk instead of a copy, and the Admitted census still went red on a planted leading_word read in verbs/change.rs; the consumer count is unchanged, and the stale main.rs consumer count in the consumer walk doc is gone. the loot-perf depth docs stop promising an equality nothing checks, change.rs points at the PolicyParses variant doc, and the rewrap leftovers the ticket lists are fixed, with a long line in the CONTEXT.md arity paragraph (#1944)
c5819020 · dbf3dbe6…diff - every bare remove_file of a loose object in loot-core fixtures now waits for the absence it asserts, and the set that does is a census rather than a sentence: #1596 measured that a handle held without FILE_SHARE_DELETE makes remove_file return os error 32 and leave the file, fixed the sites in negotiation.rs and swept no further because its aggressor had reddened nothing else, and #1897 asked whether the rest were safe or merely unexposed. they were unexposed. the aggressor was rebuilt and lives in the tree now as testkit::hold_without_share_delete, and with it holding one handle across one removal the whole family went red site by site rather than statistically: the selection reads 1 passed, 10 failed, every panic os error 32, with the already-fixed negotiation site under the identical hold as the green control, and the second removal inside accept_loss measured on its own with the first hold lifted (0 passed, 1 failed). with the helper at all of them the same selection under the same hold is 11 passed. the helper moved from negotiation.rs into testkit keyed on the OBJECT DIRECTORY rather than a store directory, because the object_store.rs fixtures are an object directory with no store around them, and #1899 exit rule and the three-way scan answer came with it unchanged. the ticket list was wrong in BOTH directions, which is the finding: it named sites a realistic scan does not reach and MISSED two of the most exposed, the live-repo removals in engine.rs and custody.rs that are #1596 own shape; under an emulated indexer scanning the fixture roots, 50 paired interleaved rounds, the bare tree is red at a removal in 36 rounds over four sites, two of them the ones the list omitted, and the converted tree is red at a removal in ZERO. what is bare and why is now derived: tests/loose_object_removal_census.rs reads every removal whose statement or whose binding names a hex-encoded address out of src and tests, and names the one home, the aggressor pin own deliberate bare arm and the two PRODUCTION removals, which return their error rather than panicking and are right to. run against the pre-change files the census names exactly the nine test functions that were converted, the tenth site being the one it states it is blind to, a removal by directory entry, which was given its address so it could take the helper and so the census could see it. mutations: a bare removal put back single-line, multi-line and through a let binding reddens the census each time naming that function (2 passed, 1 failed each); a name dropped from the expected set reddens it (2 passed, 1 failed); blinding the address needle reddens the classifier fixture and the not-gone-blind guard too (0 passed, 3 failed); giving the aggressor FILE_SHARE_DELETE reddens the new pin because the bare removal then succeeds (4 passed, 1 failed); dropping the named-first precondition reddens the should-panic pin (4 passed, 1 failed); and the pre-#1899 scan spelling reddens the moved scan pin (4 passed, 1 failed). docs/agents/workflow.md flake section carries the rule, the aggressor and the census, and stops saying the fix ends at one file. a latent write-side exposure was found on the way and is NOT fixed here: save_objects_loose renames its staging file without store.rs retry, so a scan holding the stage makes an ordinary save fail with os error 32, which is the create side #1596 explicitly left alone. every edit is inside a cfg(test) item or a doc comment, so no production byte moves and no perf gate is owed. the workspace suite is green (3843 passed over 120 binaries, 7 ignored) (#1897)
b8eb322d · dbf3dbe6…diff - a fetch gains a depth the host honours: the request carries a trailing depth after its wants, written only above zero so a request at zero is byte for byte what every client sent before, and an old host decoder returns after the wants and never sees it, which is what makes it safe to send to any host; the relay and the forge confine the change lane to the nodes within that many generations of their live heads, one being the heads and nothing older, intersected with the delta past have and, on the forge, after the entitlement gate, so a caller refused metadata in a full bundle is refused it at any depth and a node the caller holds is never re-sent, while a want is answered by address whatever the depth, the lane walking the cut changes as it walks the held ones; the seeds are live heads and not childless ids, the forge reading the ref declared set a client computed with its retire applied and the engine excluding any version a node names as a predecessor, since on a host that ingests amends a superseded sibling stays childless and would otherwise seed a generation of its own, 213 such tips against 31 real on this repo; /info advertises it as fetch_depth, false when absent, so a caller that needs the bound refuses a host without it and a caller that can afford the fallback proceeds. the receiver still cuts and that stays the rule, ADR 0089 amended rather than overturned: pull_metadata_via and the depth round of a bounded pull ask the host for the depth they will keep, FromTips as its n and a deepen as zero since the frontier is this position own, and applies IngestDepth to whatever arrives, so a host that predates the field sends the history and the position is the same one generation deep, which the test relay pins both ways by playing a host that honours the depth and one that does not. the WASM core encodes the same bytes, frozen in the parity suite against the native vector at depth one. decided by a grill of six questions on 2026-09-20 and recorded on the ticket and in the ADR: bounded per invocation regardless of persistence, a depth on the existing fetch rather than a listing endpoint, counted from the host heads, search kept on the receiving side over bodies fetched by address, the host cut an optimisation the client never depends on, and the SDK and the cache cap to follow. this is the wire half of the browser SDK stopping its 53 MB stateless read and of the serverless runner per-job pull; measured against the live relay in the landing comment once the hosts are deployed. pinned on the same shapes at each host so the two walks are held to one answer, a chain at depths one, two and zero and past a have, a fork whose two heads are both seeds, a superseded sibling that never is, and depth zero byte-identical to the one-pass walk this walk replaced, the forge with a reader the gate refuses getting nothing at any depth; in loot-net on the field decoded both ways with an old body as zero, an odd remainder refused, a pre-field info as false and one frozen vector both encoders pin; over the wire on a spawned relay and a spawned forge each answering one node of two at depth one and advertising the cut, with a want riding across the cut; and in the CLI cache refresh asking depth one. a clone at a depth asks the host too, so its declined count is 0 under a host that cut, since nothing behind the cut arrives to be declined, and the frontier width is what says the position is bounded, which the shallow suite now pins against a host that honours the depth and one that predates it; the count stays exact on a deepen, which asks the host for no cut because the frontier it deepens from is this position own. red under mutation, counts read each time: the engine ignoring the depth (0 passed and 1 failed), the engine cut counting one generation too many (0 passed and 1 failed), the depth never written (1 passed and 1 failed), the depth never read (1 passed and 1 failed), the relay not advertising the cut (1 passed and 1 failed), the relay handler dropping the depth (0 passed and 1 failed), the forge ignoring the depth (0 passed and 1 failed), the forge cutting before the gate (0 passed and 1 failed), the forge not advertising the cut (0 passed and 1 failed), the cache refresh asking no depth (0 passed and 1 failed), the receiver stopping its own cut when the host cuts (0 passed and 1 failed), the wasm framing never writing the depth (0 passed and 1 failed), the forge handler dropping the depth (0 passed and 1 failed), the engine seeding from a superseded version (0 passed and 1 failed), the forge seeding from every childless id declared or not (0 passed and 1 failed), an odd remainder read as no depth (1 passed and 1 failed), and the wants lane skipping the cut changes (0 passed and 1 failed). no migration and no format major move: a trailing field the old side never reads is a minor move, and /info default false is the whole compatibility story; the relay and the forge owe a deploy, which the release cut carries. the workspace suite is green (4017 passed over 126 binaries, 7 ignored) (#2123)
e3ddfdce · dbf3dbe6…diff - the ingest check is refused and the census that would have judged it is what lands: a change tree entry records a visibility that sealed::seal leaves outside the address, that compute_change_id_raw discards on the way into the version id the finalize signature covers, and that ObjectStore::put keeps from whichever bundle arrived first, so on a tree that came from a peer that field is a claim under no signature. tree_entry_visibility_census.rs derives every two-identifier vis pair spelled under crates/*/src and carries per site what a lie in that field would change - rendered, acted on, re-recorded, or not a tree entry at all - keyed by file, enclosing fn, which declaration of that name and which binding under it, because bundle_codec and loot-wasm each declare a name this keys on more than once and a key on the name alone would hand two functions one ordinal run, the ambiguity store_rename_census holds out of reach with a guard instead. the class is a judgement and is not measured; what is measured is that a row cannot be written without one. the sites it turns up as acted on are what the ADR 0012 entry rests on: ride_entry reads the tree entry rather than the seal when it decides whether an anyone-granted content key rides a bundle, embargoed_paths hands a timed relay grant the reveal_at the entry records, and publish_gate refuses an embargoed publication on the anchor entry tier - each of them holding or reaching the object it could ask instead, which is why the repair is per site rather than a global refusal at apply_sync, where the check would be partial exactly where it is wanted (a change ships its whole tree, ciphertext rides only for the addresses a bundle carries), where disagrees is not is false once put_vis_redacted has stripped a holder list on the wire and same_seal exists because of it, and where one refusal rejects the whole bundle. the workspace walk moves into census_text under its own admission rule, now that a second workspace-scoped census wants it. red under mutation, counts read each time: a planted binding in maroon_inner (2 passed and 1 failed, naming maroon_inner@1#2), a named row deleted (2 passed and 1 failed, naming bundle_impl_within@1#1), the type exclusion dropped from the needle (1 passed and 2 failed, the fixture naming qualified@1#1 off an (Oid, Visibility) annotation), the declaration ordinal dropped from the key (2 passed and 1 failed, twice@1#2 where twice@2#1 belongs, the table green beside it) and the walk blinded (0 passed and 3 failed, the guard saying gone blind rather than clean). no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4096 passed over 132 binaries, 8 ignored) (#1892)
2ece96df · dbf3dbe6…diff - the three sites that acted on a tree entry unsigned visibility ask the seal now, and the bundle key lane stops reading that field at all: #1892 censused who reads a change tree entry visibility, refused a blanket ingest check at apply_sync, and left these three deciding on a value that sits outside the content address, outside the change id and outside the finalize signature, so on a tree that came from a peer it is a claim under no signature. ride_entry read open off the manifest entry while reading anyone off the object a line above, so an entry recording Internal over an embargoed seal put that content key into a sync bundle any peer can hold; it reads the seal the same probe already returned, memoised per address beside anyone, and costs no store read the walk was not already paying. the same predicate on the batched path moves with it, since a transfer with a second batch would otherwise reach the old answer: the key rule comes out of wanted_finalized_entries, which answers presence alone now, and bundle_objects_only asks the object it is already holding. embargoed_paths takes the reveal_at a timed relay grant is withheld until from embargo_reveal_at, the door #1578 built for that number at loot grant --relay, and the entry number survives only where the seal cannot be produced, which is a state where grant_sealed cannot produce a deposit either. publish_gate asks seal_visibility beside the oid_is_published read it already makes of the same object, and refuses where the seal cannot be read rather than swallowing, because a swallowed published-ness read asks for consent already given while a swallowed embargo publishes content nothing could say was unsealed. each pin carries both directions, since a repair that only tightens is as wrong as one that only loosens, and none of them compares the two recordings, which ADR 0012 records as supposed to differ once put_vis_redacted has stripped a holder list on the wire. the rows for the sites that stopped binding the field left #1892 census rather than changing class, a discard being no row by its own definition. red under mutation, counts read each time: ride_entry open restored to the manifest entry (negotiation 34 passed and 3 failed, the byte-identity difference red beside both direction pins, and the census 2 passed and 1 failed naming bundle_impl_within@1#1 and @1#2), the batched key arm stopped from asking (35 passed and 2 failed, naming the follow-up batch arm), the deposit reveal time taken off the entry again (custody 65 passed and 1 failed, reading Some(0) where Some(9000) belongs), the publish gate embargo read taken off the anchor entry again (loot-cli 0 passed and 2 failed, red in both directions, and the census 2 passed and 1 failed naming publish_gate@1#1) and the unreadable seal swallowed (2 passed and 1 failed, naming ghost.txt). no migration and no wire or format byte moves, but the key lane decision moves on the client bundle builder and on the relay fetch path, so this rides the next release and owes a relay deploy. the workspace suite is green (4101 passed over 132 binaries, 8 ignored) (#2185)
0a33e5c4 · dbf3dbe6…diff - the timed deposit lane asks the seal whether a path belongs in it and not only when its key is released: #2185 took the reveal instant off the seal at embargoed_paths and left the tree entry deciding whether that lane was reached at all, so an entry claiming an embargo over a seal that records none was answered 0 by embargo_reveal_at, the number for content under no embargo, and a Restricted seal content key was fanned out to every registered peer as a timed grant the relay releases on arrival - the same disclosure direction widened by the repair that narrowed the other one. reproduced at the plan before deciding, running the disagreement cases through embargoed_paths, restricted_paths and internal_paths and through plan_timed and plan_standing, where the row read c.txt to bob and to carol at 0. membership of the lane comes out of seal_visibility now rather than embargo_reveal_at, because the tier and the instant come out of one read and the number alone cannot tell an agreeing entry from one whose seal records no embargo; the unreadable seal fallback stays, the deposit it feeds refusing on the same read. which lane a path is offered to is still matched off the entry and stays with #2187, deferred on the store read per finalized tree path it would cost rather than on impossibility, and both missing directions are pinned: the claimed embargo the seal denies, and the internal claiming entry over an embargoed seal, where what keeps the untimed lane off the path is the key and not the tier, a live embargo key being staged in the escrow that lane does not read. the sentence naming the internal lane as the one a lying entry moves to now names what selects a lane, the reason that argued a seal knows nothing about paths says cost and #2187 instead, the negotiation stop rule comment cites its pin under the name #2185 gave it, the key lane fixture says what defines the set of builders rather than counting them, the census helper doc says compiled into rather than asking, the spike crdt key lane records why the census cannot see it, and BTreeSet stops being spelled in full beside an imported BTreeMap. red under mutation, counts read each time: the dropped arm made to plan the row again (custody 67 passed and 1 failed, reading Some(0) where None belongs), internal_paths widened to the escrow (67 passed and 1 failed, the live embargo reaching the untimed lane), and the lane selection made to ask the seal, which is the #2187 repair (67 passed and 1 failed, the timed lane reading cve.txt and other.txt where other.txt alone belongs), each restored to 68 passed and 0 failed. no migration, no wire or format byte moves and no host behaviour moves, but what a push deposits moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4103 passed over 132 binaries, 8 ignored) (#2188)
efb3d8ed · dbf3dbe6…diff - both grant doors weigh the seal before they choose a lane, so an early-releasing sender can no longer file a live embargo key into a receiving position Keyring: #2205 asked for every premise to be re-verified on the tree first and each of them held, a tag-1 frame asking no embargo question at all and a tag-3 grant filing by the frame reveal_at, which is the sender word, so at a clock of 0 against a seal recording Embargoed reveal_at 9_000 both doors left keyring.holds true and escrow.holds false. the framing question is answered where a reader meets it rather than inherited by proximity: which lane a key waits in is decided once, at the door that files it, because a route carrying a key from one position custody to another reads one lane and writes the same one and Escrow::flush promotes but never demotes, so a filing is the answer every position that key later reaches inherits with nothing re-asking, and ADR 0007 states its guarantee over identities rather than over one door, which makes a door that does not ask a gap in it rather than the Escrow scope. one rule in one body, because two doors asking one question in two places is how they come to disagree about it: a grant-borne key is staged until the latest instant any party to the handoff named, so a party added later is another term of the same max rather than another arm. a disagreement withholds rather than refusing or dropping, refusing costing a recipient a grant over a claim they did not write with a remedy that is not theirs to act on, and taking the seal instant alone being strictly weaker, because a grantor own delay over content under no embargo is ADR 0027 timed deposit and a seal contributing 0 would release it on arrival; withholding costs only the wait the seal already imposes on every reader of those bytes, sealed::open embargo gate refusing them at that clock whichever lane the key sits in. the seal is read back from this store and never off the arriving bundle, because the address does not cover vis and put is first-write-wins, so weighing the incoming copy is reading the sender word a second time under another name, pinned on a fixture whose lying copy keeps the address and is therefore a dedup. the two doors get one answer for two reasons and the difference is recorded: tag 3 had a recorded cooperative-defence posture and this applies it to a second party, which is why ADR 0007 takes a #2205 amendment, while tag 1 had no decision at all, existing to bypass the entitlement question and having taken the embargo one with it, which sealed::open first gate separates in four words, time not identity. the cost is measured rather than assumed: one object get per key the door files and zero disk reads where the grant carried the object its key is for, eight keys costing eight gets beside a ninth address the same bundle carried no key for. red under mutation, counts read each time: the seal term dropped from the staging max (654 passed and 3 failed), the tag-1 door reverted to filing into the Keyring (654 passed and 3 failed), the frame term dropped (655 passed and 2 failed), the staging comparison widened to greater-or-equal so an undue key stages (653 passed and 4 failed), the seal weighed off the bundle copy rather than this store (654 passed and 3 failed, the held-seal pin naming the lane), the question moved ahead of the key guard (656 passed and 1 failed, object_gets reading 17 where 8 belongs), and each of the two fixtures inverted as a vacuity control (656 passed and 1 failed, the control firing), each restored to 657 passed and 0 failed. ADR 0012 takes a twelfth amendment recording that this class is a sibling of its own, the disagreement being with a frame rather than a tree entry so no census row moves, and that keyring.holds is still not a bound, a key some door filed before this change being in .loot/keyring still. no migration, no wire or format byte moves and no host behaviour moves, but which lane a grant-borne key is filed into moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4131 passed over 132 binaries, 8 ignored) (#2205)
ec80ad4d · dbf3dbe6…diff - the sync ingest door weighs the seal this store holds rather than the one that arrived beside the key, so a tag-0 bundle spelling a weaker tier at an address this position already owns can no longer file a live embargo's key into the Keyring: #2212 asked for item 1 to be demonstrated before it was repaired and the reproduction read exactly as the ticket claimed, bob holding oid sealed Embargoed 9_000 and a Sync frame carrying a byte-identical object that spells vis Internal beside the real key leaving bob's Keyring holding that key at a clock of 0, which is the state #2205's own new pin forbids reached by changing the frame tag, no plaintext escaping because sealed::open's header gate still refuses. the same read carries grant_ids and that half was demonstrated too, a copy spelling the ANYONE marker over a held Restricted seal getting a key past the entitlement filter #864 built, so every question this door puts to a seal now goes to the copy that will stand at the address. it is asked before the put while the arriving object is still in hand, because first-write-wins makes the held copy the standing one only where the store holds the address at all, so the cost is zero store reads on a fresh address and one on a dedup, which is the read the key verification already owed and now answers the seal's questions with its own; the already-held guard consults both lanes, so the lane the first door chose is the one that stands. the prose is the harder half and the lesson is sharper than do not list members: the sentence that failed was in the correct derived shape, the set of them is Keyring::insert's callers which the compiler enumerates, with a hand-maintained count welded on in the same breath, and the count is the half that was wrong, so every replacement names what decides membership and stops there, at ADR 0007's amendment, escrow.rs, CONTEXT.md, custody.rs twice, ADR 0012's tenth and twelfth amendments, negotiation.rs and the grant-door pin, and escrow.rs's headline stops claiming that no route moves a key between lanes when flush is one and a grant is a new filing at the recipient rather than a carry. secondary items: the stale pin citation and the now-false claim around it, spawn.rs's three false statements about the orphaned child, the unproducible-seal fallback recorded as releasing nothing only at the instant it files, expires_at declined as a term of the staging max with the reason at the code, the demotion refusal naming which of the two recordings fired, the census group sentence that named its members, ADR 0012's push qualifier at the tip with no want, the ingest cost fixture given a publishes-nothing control, workflow.md's three refusals derived from CargoTestFailure and the PRE_LAND constants, a usize subtraction restated as a sum so the sentence beside it can print, and orchestrator.rs's tombstoned pin names declined with the reason. red under mutation, counts read each time: the vis term reverted to the arriving copy (659 passed and 1 failed), the grant_ids term reverted (659 passed and 1 failed), the already-held guard narrowed to the one lane it writes (659 passed and 1 failed), the seal question asked through a second store read (659 passed and 1 failed, object_gets reading 8 where 0 belongs), the lying sync copy made a different object (659 passed and 1 failed, the vacuity control firing), the lying grant ids made to agree (659 passed and 1 failed, the second vacuity control firing), the publishes-nothing control inverted (1353 passed and 1 failed), the refusal made to say both either way (1352 passed and 2 failed) and the ingest cost relation moved by one (1353 passed and 1 failed), each restored to 660 and 1354 passed with 0 failed. no migration, no wire or format byte moves and no host behaviour moves, but which lane a sync-borne key is filed into moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4140 passed over 132 binaries, 8 ignored) (#2212)
c643aadc · dbf3dbe6…diff - the probe memo #2226 asked for was built and measured and then refused, because the probe it removes is under a twentieth of the walk: locally_missing_objects asks its membership question once per change and manifest entry pair while the answer is a set of distinct addresses, which is 25600 probes for 327 answers on a 200 path missing fixture, and the obvious shape is the probed BTreeSet copied from offered_objects. the named signal reads WORSE with it, in a paired opt-in --missing A/B at the fixture depth, one binary built from each arm and the gate reporting an idle machine on every reading: 18.3 ms to 20.9 ms at width 200 and 96 ms to 110 ms at width 800, because the probe here is a single BTreeMap lookup in the store index rather than the object read the sibling spells, so a set insert over a map of the same order costs more than the lookup it skips. THE CEILING IS WHAT MAKES THAT A REFUTATION RATHER THAN ONE BAD MEMO: a knowingly wrong binary with the probe deleted outright reads 17.5 ms and 91.5 ms on those two fixtures, so any probe-side change on this walk is bidding for under a twentieth of it, and with the manifest walk deleted too the same measurement reads 0.03 ms, which says what the walk spends is materializing and walking each deferred manifest to read the addresses out of it, a loot-codec seam and not this one. so the code is UNCHANGED and what lands is the reading, recorded on the function where the next hunt reads it before refiling. one list-class defect is corrected in the same doc: three callers stood asserted as the whole set beside a pub passthrough on Workspace that is a fourth, and the sentence now names what the callers share instead of how many there are. no mutation proof, because nothing was fixed and no pin was added, and the measurement controls stand in its place: the ceiling binary moves the reading at all, so the half is not blind to the probe, and restoring the pristine body returned it to the landing band. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4174 passed over 132 binaries, 8 ignored) (#2226)
cacd34af · dbf3dbe6…diff - the attribution the #2223 refutation asserted is now measured and the conclusion stands, because a nested count region says which span the surviving reads belong to: with this body live the open inside vouch_if_on_disk reads 0 files over 200 paths, and with the ceiling in place that same span reads 199 on status and 200 on surface, so the reads move rather than go away and a header only body would add its own opens on top of them. the size axis gains the fourth column the ticket asked for and the effect does not run away with it: at 1 MB objects over a 210 MB store the ceiling leads by about 8.5 ms on either verb, under a fiftieth of the verb, against 14.24 and 20.91 and 69.00 and 455.60 ms for status and 16.67 and 22.92 and 67.37 and 461.02 for surface. THE 3.3x DISCREPANCY IS RECONCILED AND THE GUESS ABOUT IT WAS WRONG: the loose store weighs 213,800 and 3,285,800 and 26,223,400 bytes here, the three figures the ticket body records, to the byte, so both arms ran the same incompressible fixture and compressibility explains nothing; what differs is that the ticket timed the process, which reads 22.7 and 29.8 and 77.6 ms over these same fixtures and meets it at the two smaller sizes, while its largest reading of 226.2 ms reproduces in neither arrangement against seven repetitions spanning 69.00 to 71.96 ms and a first unwarmed reading of 71.13. so the 11x move it was filed on rests on one reading, and the move these fixtures carry over that range is about 3.4x. #2226 gains the depth axis its own cost model lives on, swept without touching MISSING_DEPTH so no fixture and no workload_id moves: at width 200 the walk is linear in depth over 1.16 and 4.66 and 18.49 and 74.04 ms at depths 8 and 32 and 128 and 512, the probe deleted binary reads 1.08 and 4.38 and 17.38 and 68.82, and the probe share does not move across that sweep, which it would not, because probes and manifest entries are both O(changes x paths). that doc now cites #2240, records what the half is, and says the probe is two map lookups rather than one. ONE SWEEP ITEM IS REFUTED RATHER THAN FIXED: the short line in gate.rs is not a rewrap leftover, it is short because the 33 character intra doc link after it does not fit at this column norm, and the same file carries a 44 column line for the same link higher up. the cheapest item had the only seam and it is pinned: the WORK_COUNTERS exclusion table read as the excluded variants and nothing else, which is the definer #2234 sent a reader to, and it is now read back against Work::ALL filtered by gate::COUNTERS. red under mutation, counts read each time: graph_sorts dropped from gate::COUNTERS failed the new pin (0 passed and 1 failed, three variants named against four owed), the PolicyParses row deleted failed it (0 passed and 1 failed, two against three), and the table header renamed so the scan reaches no rows failed the anti vacuity guard (0 passed and 1 failed), each restored to 1 passed and the file to 12 passed. the two ADR 0073 enumerations are deleted rather than corrected, the CONTEXT and gate paragraphs are reflowed with identical word streams, and no code outside a test changes, so this owes no deploy. the workspace suite is green (4178 passed over 133 binaries, 9 ignored) (#2242)
d6bf7f55 · dbf3dbe6…diff - the receiver wants walk reads each address off its manifest frames instead of decoding the manifest, so locally_missing_objects no longer materializes the deferred manifest of each change (#1536) to keep only its addresses. Manifest::addresses steps the frames with the non-allocating twins Manifest::walk already uses, and does so only when the deferring walk recorded counts, the condition tier_counts already rests on for frames and map entries to correspond one to one; a legacy separator manifest or an already decoded one answers from its map. the ceiling was re-taken on the landing position first: 17.96 to 17.99 ms at width 200 and depth 128, against 0.026 ms with the walk over the manifests deleted. paired --missing readings, one loot-perf-gate --features count built per arm, three interleaved rounds, the gate reading load idle at 3 to 9 percent: 17.93 to 18.29 ms down to 0.905 to 0.913 ms at width 200, and 96.13 to 96.34 ms down to 5.70 to 5.89 ms at width 800. along depth at width 200, 1.11 / 4.47 / 17.96 / 72.2 ms down to 0.050 / 0.20 / 0.86 / 3.69 ms at depths 8 / 32 / 128 / 512, still linear in depth with the slope fallen, and one walk at 200 x 128 allocates 25 times against 60,569. the gated counters do not move, object_gets 743 and object_disk_reads 200 on both arms. what it gives up is the decode memo: a later walk in the same process steps the frames again, 0.71 to 0.73 ms against 0.60 to 0.62 ms at 200 x 128. walks of the same shape it did not measure still decode. red under mutation, counts read each time: the walk put back on c.tree.values() (1 passed and 1 failed), the frame arm never taken (0 and 2), the collapse guard dropped (1 and 1), the visibility stepped as one byte (1 and 1), each restored to 2 passed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4265 passed over 135 binaries, 10 ignored) (#2240)
0c212874 · dbf3dbe6…diff - Manifest::addresses reads the frames only after a step over them shows the stored keys strictly ascend in Path order, so a manifest whose frames are not its map, two keys naming one path or keys out of the map order, answers from the map, and a pull over a corrupt local graph file no longer asks for an address the map dropped. tier_counts keeps its backslash test, now documented as not a proof, and the a.txt and a/b ordering it cites is corrected to component order in both places. what the check costs, paired --missing readings, one loot-perf-gate --features count per arm, eight interleaved rounds, load idle at 2 to 10 percent: 0.875 to 0.924 ms without it against 2.09 to 2.18 ms with it at width 200, and 5.76 to 5.88 against 10.50 to 10.63 ms at width 800, where a decode arm read 18.75 to 19.13 and 100.0 to 100.8 ms, so about 9x against the decode where it was about 21x and 17x; the Path comparison is most of it. red under mutation, counts read each time: the check removed, compared by byte, not strict, byte equality only and path equality only (1 passed and 1 failed each), restored to 2 passed. riding along: store.rs and ADR 0075 say the ingest transaction still reads for checks of its own and that the landing decision is what reads nothing inside it, that a push with no proposal open pays 0.28 to 0.43 ms where #2177 asked for nothing measurable, and that the fallback decision rests on one repo size; finish_stage says why any refusal is answered by the address on disk; the dated test counts in format_skew_gate.rs and workflow.md say at the time; two rewraps; the calls.rs runner stubs use the file imports. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4274 passed over 135 binaries, 10 ignored) (#2273)
0a64d280 · dbf3dbe6…diff - closure_complete, the walk the implicit-capture door takes in front of each bare mutating verb, reads each ancestor address off its manifest frames through Manifest::addresses, the spelling locally_missing_objects has used since #2240, where it decoded each ancestor manifest to keep only the addresses; a manifest whose frames are not provably its map, a legacy separator key or the #2273 shapes, still answers from the map. paired --closure readings at depth 1024, one loot-perf-gate --features count built per arm, interleaved, the gate reading load idle at 1 to 4 percent (peak 7): 166.1 to 167.9 ms down to 19.6 to 20.0 ms at width 200 over three rounds, and 382.7 to 383.2 down to 42.8 to 42.9 ms at width 400 over two, the gated counters the same on both arms. two new pins: a reopened repo answers both verdicts decoding no manifest, and over canonical, nested, legacy separator and the three #2273 shapes, with no hole and with each object removed, the deferred and the pre-decoded answers equal a walk over the decoded manifests, with a control that each collapsing shape reached the arm where frames and map disagree. red under mutation, counts read each time: the walk put back on values (0 passed and 2 failed), the ascent check dropped from Manifest::addresses (1 and 1, on the answer itself), the frame arm never taken (0 and 2), restored to 2 passed; the backslash condition dropped alone stays green on Windows, where Path reads a backslash as a separator and the ascent check catches the collapse, which the test doc records. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4280 passed over 137 binaries, 11 ignored) (#2281)
a2fe24af · dbf3dbe6…diff - the review-sweep fix-up over #2280, #2281 and #2282. the closure-walk pin stored x/abc after x/ab, which a random address beginning with c completes, so its stored-once control failed 5 runs in 400; it now uses y/abc and read 400 of 400 green. the spliced-node pin claimed the tree a later save writes back, but that save writes the copy the graph file already holds, since the rewrite inserts what it reads back first and ChangeGraph::insert keeps the first node for an id; it now saves into a store with no graph file, the splice comment says why, and a splice that blanks each spliced manifest goes red on that assertion (0 passed, 1 failed). decided and pinned: an ingest past a key or holder name that is not UTF-8 in a node it does not splice returns Ok and the next rewriting save refuses, kept because the open already defers such a file and stepping every pool node reads 13.8 to 14.4 ms beside a 54.4 to 55.0 ms deferred read of this repo graph file (red when the check scans the whole pool and when the save fallback is dropped, 0 passed and 1 failed each). measured with a tracking allocator over that 85.7 MB file: the rewrite union holds 88.2 MB deferred against 222.5 eager and peaks at 193.9 against 382.0, both freed when the save returns; a pool keeps one copy of the file per read that splices, 85.7 to 342.7 MB over one to four reads against 1.2 to 6.7 eager, recorded at the splice. the move-not-clone property lost its count pin to deferred manifests (a clone costs about two allocations per spliced node, 78 against 65 at the pin shape) and is pinned by the pool ids instead (red under the clone, 0 and 1). the closure walk doc names the holder-name case where it answers and the decoding walk panicked, pinned (red when the ascent check also steps holders, 0 and 1). each mutation restored to green. list-class sentences in HUNT-PERF and the loot-perf fixture rows now name what defines the set of graph file reads, and a rewrap leftover and a stale keys_ascend reference are fixed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4285 passed over 137 binaries, 11 ignored) (#2287)
a8c863e3 · dbf3dbe6…diff - a push now carries an attestation recorded over a change the remote already holds, so loot tag after loot push reaches a relay and a forge instead of being left behind under a success line: a local attestation ledger (.loot/attestation-ledger) records per remote what each push delivered and is read by a push and by no open or save, written by RepoStore::record_attestations_sent as a read-merge-write under the shared-store lock; the push sends the attestations over the held changes of the remote that the ledger has not recorded beside the send set and prints how many, the bundle builder keeps a late attestation only over a change inside the have closure of the recipient whoever handed it in, the forge /ingest keeps one over any change its repo holds through a new changes_held store read that costs no query when every attestation rides its change, and /info gains an additive late_attestations field so a forge that does not advertise it is sent none, has nothing recorded as sent, and the push warns how many it left behind. the land gate store_file_reads is 24 on its workload with no move, where a first cut that read the ledger on every open measured 26 and was refused; an open reads 20 store files, 21 with that cut. the #48 bound holds on the wire: a push carrying one late tag sent 256 B at a relay and 306 B at a forge over both 2 and 24 held tags. the ticket recipe, whose fresh clone lacked late-tag2 through the 0.4.24 binary, shows it through a lane build. red under mutation, counts read each time, each restored green: the open reading the ledger again (0 passed and 1 failed), the ledger write overwriting instead of merging (0 and 1), the ledger ignored (0 and 2), the late lane dropped (0 and 2), the privacy filter removed (1 and 1), the forge back to in-this-bundle (2 and 1, and end to end 1 and 1), the forge keeping any change (2 and 1), a relay push recording nothing (0 and 2), a push recording to a forge that does not keep them (1 and 1), the /info flag ignored (1 and 1). no format constant, codec byte or migration moves; the forge change is live once the forge is redeployed. the pull half is not built: a pull still carries an attestation only with a change it sends. the workspace suite is green under bash ci/local.sh against Postgres 18 (4425 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2251)
414ba6b2 · dbf3dbe6…diff - the attestation ledger records what a remote is known to keep rather than what a push put on the wire, the review-sweep fix-up over #2251: a push records rows only for a host whose /info answered, as a relay or as a forge advertising late_attestations, and a host whose /info did not answer is still sent them but has nothing recorded; against a forge without the flag it records what rode with its change, so its left-behind warning counts the late ones and no longer grows; the ledger keeps per forge the tip generation the last recorded push committed at, and a push that reads a lower one sends every attestation over the changes that forge holds again and says it may have been restored, with deleting .loot/attestation-ledger documented as the recovery for a relay and for a restore hidden by later pushes; a ledger that will not read or write warns and never fails a push or skips its deposits, and a record over an unreadable one starts it afresh. the forge misread as a relay the sweep named fails at its unsigned /wants before sending, pinned; the rule stands without it. red first with each fix undone, counts read each time, each restored green: an unanswered /info read as keeping them (0 passed and 2 failed), a forge without the flag recording nothing (0 and 1, left behind 2 where 1), a restored forge unnoticed (0 and 1), an unreadable ledger fatal (0 and 1), an unwritable ledger fatal (0 and 1), the first-seal summary defaulting an unknown change to no rows (0 and 1). also stated: the privacy filter rests on the declared have, a forge holding a head without its ancestry drops a late attestation it is recorded as keeping, a ledger is one clone, late_attestations states the property rather than naming verbs, RemoteSync bound records nothing and its docs are current, the first-seal summary refuses an unknown change and names its tree clone, and the Route doc states where hosting is decided. the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. no format constant, codec byte or migration moves. the workspace suite is green under bash ci/local.sh against Postgres 18 (4430 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2355)
9511111e · dbf3dbe6…diff - the deferring manifest walk checks every stored key and Restricted holder name for UTF-8 as the eager decoder does, so a graph file holding one it refuses is refused naming the file at the open, at the adopt and ferry pool read and at the save, where the open passed it and the first read of that manifest panicked; measured first, the check shares one pass over each key with the backslash test it replaces and the open got faster, one loot-perf-gate --features count per arm, interleaved, load idle: --graph-load 9.47 to 9.54 ms before and 8.93 to 9.04 after at depth 1024, --manifest-breadth 33.9 to 35.9 against 32.3 to 32.9 at 872 paths, store_file_reads 24 on both, and over a copy of this repo graph file (93.5 MB, 1,634 nodes) the deferring decode 16.6 to 17.1 ms before and 8.8 to 9.3 after, where handing every key to the validator read 24.9 to 25.8. Manifest::decodes, the splice check of #2282 and the refusal left to the save by #2287 go, since the read now refuses what they caught. through the binary, loot log and loot status over such a file panic on 0.4.24 and refuse on the lane build. red with each piece undone, counts read each time, each restored green: the key check dropped (0 passed and 2 failed), the holder check dropped (0 and 2), a valid non-ASCII key refused (1 and 1), the refusal not naming the file (1 and 1), the open alone not naming it (0 and 1). no format constant, codec byte or migration moves. the workspace suite is green (4433 passed over 138 binaries, 13 ignored) (#2275)
23fec061 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.