Changes touching this path

  • day 0: loot hosts loot f4c30e75 · dbf3dbe6…
  • evidence: crew minted and verified (#86) 1fada823 · dbf3dbe6…diff
  • hard embargo engine/wire lands (#14, format v5) 7784bcac · dbf3dbe6…diff
  • normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text) e58fdda6 · dbf3dbe6…diff
  • S0: stable-change-id data model + FORMAT_MAJOR 6 (#143) Implement the durable change-id data model per ADR 0029 — the keystone the jj-ergonomics trio (map #142) builds on. Two ids per change: the existing content-derived **version id** (`ChangeNode.id`, unchanged role: dedup, DAG edges, sync addressing) and a NEW random 16-byte **change id**, a durable handle stable across a working change's re-snapshots. - `ChangeNode` gains additive `change_id: Option<[u8; 16]>`; never folded into any hash. - The Workspace mints a fresh change id when a change begins and carries it across every re-snapshot (`snapshot_allowing` reads the prior working node's id before dropping it; `record_carrying` carries, `record` mints when authored). Keyless/bridge/legacy changes stay `None`. - Finalize signs over `version_id ‖ change_id` (new `change_signing_message`); `verify_authored_change` checks the same. A legacy change (`change_id = None`) signs over the version id alone, so pre-v6 signatures still verify unchanged. All four CLI finalize paths widened (finalize_working, sign_change, resolve). - Wire/durable codecs carry the change id after author+sig, gated on major >= 6 (`put_change_id`/`read_change_id`); idempotent on re-receipt. - FORMAT_MAJOR 5 -> 6; legacy decodes as `None`, no backfill. v6 goldens added, v5 kept as decode-compat. Parents, dedup, sync addressing, convergence: unchanged (they key on the version id). No display/verb changes — that is S2. Tests: change id stable across re-snapshots while version id rewrites; keyless mints none; v6 signed-over-both-ids verifies through apply; relabelling the change id after signing is rejected; v6 bundle/graph round-trip; v<=5 loads as legacy. Verified end-to-end via the CLI (alice bundles a v6 signed change, bob applies and verifies). f253ce09 · dbf3dbe6…diff
  • Merge pull request #157 from Connor-Miller/s0-stable-change-id-format-6 S0 — Stable-id data model + FORMAT_MAJOR 6 (keystone) (#143) Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 419d6996diff
  • R7: RepoStore is the objects dir's only owner (ADR 0017, #183) 2328b920 · dbf3dbe6…diff
  • loot edit: amend a finalized change; supersession travels as signed predecessors (ADR 0032, #171) Implement the amend model: `loot edit <change-id>` reopens a finalized tip change as the working change - a sibling (parent = its parent, tree carried address-for-address, durable handle kept) whose `predecessors` names the reopened version - so once `loot new` signs the amend, the claim that X-prime replaces X is signed data that travels, not a local-only abandon. - Format: FORMAT_MAJOR 6 -> 7 (ADR 0019). ChangeNode.predecessors: Vec<Oid> rides the bundle + durable graph after the change id, canonically sorted, empty = ordinary; folded into the version-id computation (a no-op amend still mints a distinct version) AND into the finalize signature (version_id || change_id || predecessors) - ingest trusts received ids, so stripping/forging a supersession claim on the wire must break the signature directly. v7 reads v<=6 as predecessors-empty; goldens updated, v5/v6 kept decode-compat. - Liveness (amends ADR 0029's definition): superseded - named as a predecessor by any in-graph same-cid version, regardless of that supersessor's own abandoned/superseded state - joins abandoned as a live-view filter in divergence detection, versions_of_change, and log/status rows. Abandon means kill, never revert. - Converge: converge_heads drops superseded heads before collapsing forks (a solo amend lands at peers as a clean replacement, never content-merged with the version it replaced); dock merge adopts an amend of our tip as a fast-forward and treats the mirror case as a no-op (supersedes() requires the claim to sit ON the other line). - The verb: a named Workspace mutation; refuses on an in-progress or uncaptured working tree (the documented ADR 0030 exception - edit replaces the working change and never implicit-captures), on a divergent handle (abandon first), and on descendants (tip-only v1). One undoable op (ADR 0031); output through the render String seam. - dock switch: an idle dock no longer parks a tip-duplicate working child on its tip (the finalize_capturing duplicate-drop now runs there too) - the stray polluted the tip descendants and, post-0032, would have content-merged against amends. Tests: engine liveness + canonical hashing + signature strip/forge; codec round-trips + v7 goldens; workspace edit e2e / guards / undo / dock-merge FF / converge drop. Live-verified on the built binary (edit -> amend -> new; guards; undo). 366 tests + clippy clean. 8176f2e0 · dbf3dbe6…diff
  • fix(loot-core): unique object temp-suffix + atomic custody-metadata writes (#252) 3bf5c96b · dbf3dbe6…diff
  • Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322) 20ac82e1 · dbf3dbe6…diff
  • Add loot verify: object-store integrity check (#19) - rehash every loose object against its address, report corrupt and missing by address, exit 1 on problems; load-free so a corrupt store can still be diagnosed aeb147b3 · dbf3dbe6…diff
  • Build loot burn: destroy + tombstone, no resurrection (#344) 0d1cc5af · dbf3dbe6…diff
  • Fix Windows rename-window read races + move contention stress tests out of the land gate (#476) Root cause of the describe_contention flake: on Windows, atomic_write's rename-replace leaves a brief window where a concurrent reader's open of the target transiently fails (PermissionDenied / ERROR_SHARING_VIOLATION os32, or NotFound as it swaps). Fixes: - store::read_replaced now retries os error 32 (sharing violation), not just PermissionDenied. - New store::read_replaced_required also retries NotFound (for provably-present files: graph/identity/keyring) — a required file blinking missing mid-rename is transient, not absent. - load_objects_loose reads each object via read_replaced (was raw fs::read, the un-retried path open_at hits before the graph). This cut the tear from ~195 to ~0 under contention. Since the 3 describe_contention tests are strict-zero TIMING stress tests (cargo runs them in parallel, self-amplifying load), they're now #[ignore]d out of the routine 'cargo test' land gate and run via 'cargo test -- --ignored'; their invariants stay guarded in-gate by deterministic unit tests (incl. the new transient-error predicate test). be7e9418 · dbf3dbe6…diff
  • Rename the Public visibility tier to Internal (ADR 0041 §2, #480) 6969626d · dbf3dbe6…diff
  • Stop shipping grantee nicknames on the wire (ADR 0015, #521) 55ba71eb · dbf3dbe6…diff
  • loot-core durable decoders cap the reservation, never the count (#620) 537c4219 · dbf3dbe6…diff
  • a change records when it was authored, so a projected commit stops reading '6 years ago' on GitHub (ADR 0043) loot changes carried no timestamp, so the git bridge fabricated one: BASE_EPOCH + generation, one second per ancestor depth from a 2020 epoch. All 526 commits sat inside seven minutes of September 2020. Earlier repairs (#626's floor, the missing-generation refusal) fixed ORDERING and never touched the absolute date, which is why this kept coming back. ADR 0028 inherited the no-timestamp constraint rather than choosing it. The real reason is upstream: a version id is blake3 over authored content, and a clock inside that hash gives two peers different ids for identical content, destroying the dedup and convergence of ADR 0001/0004. So authored_at rides the label seam ADR 0029 already cut for change_id -- covered by the finalize signature (no relay can restamp it) but never folded into the version id. The wasm golden vectors prove the separation held: FROZEN_VERSION_ID, FROZEN_OBJ_ADDR and FROZEN_SIGN are byte-identical, and only the version marker and one presence byte moved. Advisory, and never an ordering input: a self-reported clock is a claim, not evidence (ADR 0025). in_order/ids_topo, buoy and path_touch.ordinal are all untouched, the forge indexes nothing on it, and the projection floors it past every git parent so ancestry holds whoever's clock is wrong. Format v11: additive for readers (a v<=10 change decodes as None and an absent timestamp adds nothing to the signed message, so every existing signature still verifies), breaking for writers, so loot-cli and loot-forge go to 0.4.0 in lockstep. Forge migration 0004 stores it as bigint, not timestamptz -- signed data must round-trip bit-exact or pullers reject the change. Only new work gets real dates. Every commit on main predates v11, main is push-fast-forward-only, and backfilling would fabricate the very claim this replaces. Perf-Baseline: reset the change body grew one presence byte, plus eight where a timestamp is present, so bundle_bytes/store_bytes/wire_bytes step once at the v11 boundary eaa56d99 · dbf3dbe6…diff
  • a relay is measured, not capped, and the only bytes it may forget are torn writes (#817) f52d53e4 · dbf3dbe6…diff
  • a lineage catch-up brings the objects and keys its nodes reference, and a ferry that dies mid-projection names the oid in hex and the recovery (#778) The filed hypothesis was that a change node can become visible before the objects it references are durable. It is wrong: save_to already writes objects before the graph that names them, and that order is deliberate and commented. The real defect needs no race at all. The object store and the keyring are loaded exactly once, at load_from, while the shared graph is deliberately re-read mid-process by ingest_shared_lineage (the 265 catch-up primitive). So any peer that finalizes after this process opened contributes a node whose bytes and keys this process will never hold. A land guarantees a long window there: it opens the workspace, runs the pre-land tests, and only then ferries, so the ferry aborts mid-projection while holding the harbor lock. ingest_shared_lineage now refreshes objects and custody after it re-reads the graph, and load_from reads in the reverse of save_to write order, so a reader never blends a newer graph with an older object scan. Also: an Oid renders as hex instead of a 32-element decimal byte array (Debug full width, error strings short), and a mid-ferry abort now names its recovery with flags checked against the verbs that parse them. hex moved from loot-core to loot-codec because RepoError is defined in loot-codec and the dependency runs one way only; loot-core re-exports it, so every call site is unchanged. 0ba6d577 · dbf3dbe6…diff
  • the store spells a path one way: tree keys go slash-separated at every boundary, and a windows store loads nested on posix (#988) d2889f60 · dbf3dbe6…diff
  • custody at rest seals to your own key behind an optional passphrase, unlock is a session file or an env var, and no non-interactive caller starts prompting (#1138) c8ffee3b · dbf3dbe6…diff
  • the keyring and escrow share one codec so a custody decision is written once, the locked refusal has one home instead of five drifted ones, and the terminal predicates each say why they differ (#1174) 33aca9d9 · dbf3dbe6…diff
  • a one-path capture stops paying a stat per stored object and a 29.2 MiB graph rewrite, because persist now writes the objects put already reported new instead of re-deriving that from the filesystem, and skips the whole-graph read-merge-write when an outline of the file proves it would reproduce itself byte for byte (#1420) dfebf64a · dbf3dbe6…diff
  • five claims from this run's review sweep get pins instead of louder prose: sealed.rs's header names can_open beside the chokepoint it did not weaken, is_canonical is held to encode_graph(decode_graph(bytes)) == bytes over every golden with the rewrite predicate no longer said in two files, the #988 separator arm is pinned by bytes encode_graph cannot write instead of an if that never ran, the inflate guard's third arm gets the test its prose stood in for - which measures that arm as a narrowing of where a lie is refused rather than of whether - and a refusal that could only print one number twice prints both (#1431) 102fbec6 · 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 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
  • a repo open stops materializing half a million manifest entries no verb asked for: ChangeNode.tree becomes a Manifest holding either the decoded map or a proved-decodable byte range into one shared Arc, so topology decodes eagerly because reachability needs it while a change's path manifest decodes only when something reads it - which is where the cost actually was, since framing the 37.9 MB graph without building the maps takes 6.8 ms against 507 ms to build them, and that 507 ms was the bulk of the 1050 ms every verb paid, including loot whoami whose whole job is to print 93 bytes out of id.pub. Deref carries the ~270 existing node.tree read sites unchanged and Deferred is module-private, so a deferred manifest cannot exist over bytes Manifest::walk has not already refused with the same framing the eager pass uses, which is what earns an infallible &TreeManifest return rather than a Result; and an unmaterialized manifest is pinned never to read as an empty one, the rule read_graph_required already states for an absent graph file. Verbs also declare what they read now: RepoNeed rides beside CustodyNeed through the already-pub(crate) open_at_clocked_needing, so no new public door is added and the sealing door custody_lock pins by string and call-site count is untouched, with whoami, conflicts and manifest narrowing to POSITION_ONLY, defaulting to EVERYTHING everywhere else, and a withheld graph or store aborting loudly rather than reading as empty. whoami 555 to 42 ms against a 40 ms control, conflicts 557 to 41, manifest 557 to 42, log -n 5 626 to 144, status 704 to 227, lanes 4069 to 717, peak RSS for whoami 148 to 7.1 MB, and twelve verbs byte-identical against main including 414 KB of loot log. Three of this ticket's premises were wrong and are corrected rather than worked around: the keyring it named as the second cost measures 1.1 ms and is left alone, read_graph_outline did not fit because it discards the parents reachability needs, and the object-store half it called the boolean that costs a gigabyte had already landed in #1545 - while the stat cache stays unbuilt, which is the whole of what status and lanes still spend (#1536) b923abb1 · dbf3dbe6…diff
  • the loot-core tests stop sharing one repo and the shared store stops growing, and the design was already written one line above the bug: tmp said the temp root every fixture repo lives under while returning the root itself, so returning a fresh directory beneath a per-process root makes that sentence true as written with no doc edit and none of the 161 call sites changed. uniqueness is per call rather than per process, because the sites run on the harness's parallel threads inside one process, which is the half a pid does not touch and the half the isolation gradient had already separated - lib alone green, workspace red. a third hole neither ticket named is the larger one: 48 sites spelled DagRepo::init with the bare temp dir longhand, bypassing the helper entirely and invisible to both tickets, so the real set is 51 statements and not the sixty a three-line window had suggested - established by reading whole statements, since that window reads past short ones and stops short inside this crate's many multi-line format calls. the evidence is a paired experiment rather than a green run: two concurrent processes reddened object_reads five times in five, asymmetrically with the earlier starter losing every time, while the shared-store half needed six concurrent runs to reproduce at all and then failed 28 of 36 across four test names, two of them never recorded before, which is the fifth and sixth name for a ticket whose own point is that the name is not stable. after, the same experiments are ten of ten and thirty-six of thirty-six green, and a paired count on an idle machine has the unfixed arm adding 44 objects and rewriting graph, keyring and heads while the fixed arm moves nothing, byte-identical with mtimes unchanged to the millisecond. a census derives the property from source with no allowlist and was proved red four ways, including one site from the third hole that any list written from the two tickets would have passed, and including the census blinded, which reported ok on an empty set and was caught only by its own non-vacuity guard (#1667, #1688) 87c3aacb · dbf3dbe6…diff
  • a move becomes a recorded fact instead of a later guess, and the premise this ticket rested on was false: #98's object reuse is keyed by PATH, so a moved path is absent from the outgoing tree at its new key, falls through to put_sealed and gets a fresh address - the same object under a different key was not a fact waiting to be read off the tree, it had to be made true by extending the reuse across the move, and everything else follows from that. The rule is that a move is recorded only when the path's sealed object survives it, so a move whose content also changed in the same capture window shares no object, records nothing and is two rows, while a move and then an edit across two captures of one change composes and reads as one row saying the content also changed. Empty content and any ambiguity are refused for the same reason the whole design exists: zero bytes equal all zero bytes, and choosing between two vanished twins would make a signed fact depend on iteration order. On the boundary the render side compares nothing at all - resolve reads keys and never an address, a visibility or a byte, pinned by running it twice over trees that agree on every key and differ on every address at three unopenable tiers, with a positive control beside it so agreement is not evidence it answers nothing. The capture side compares once: it opens a vanished object with this identity's own keys, fail-closed so an unopenable object is never read or hashed, and pairs only against an addition at an identical visibility and publication tier, with the digest living for one call, never an address, never stored and never on the wire. What a relay newly learns is that two keys held equal ciphertext, which recording a move states in plaintext anyway since tree paths are plaintext at every tier, so it is inherent to the feature rather than the ADR 0004 oracle - written into that ADR rather than left in a message. Renames ride the label seam change_id and authored_at already use: covered by the finalize signature so a relay cannot rewrite a move, never folded into the version id so two peers reaching one tree by different routes still agree on its address, which is why every existing id and signature is unchanged and a v11 store reads with no move recorded. That was tested rather than argued, against a real store built by the shipped binary, where the old move still renders as a delete and an add in the same repo the new one renders as a rename, because inferring the old one would be the equality oracle arriving through the compatibility door. Three further things were wrong and are corrected rather than worked around: ADR 0019 says an additive change bumps the minor and has never described this project, since the minor is still zero and all five additive changes took the major; plan_moves own doc comment claimed the tier is checked before the digest, three lines above code that does the lookup first, when the property actually holds by the stronger route that nothing unopenable enters the index at all; and the empty-content refusal was written on both sides of the pairing, where each made the other unprovable and removing either left the test green - a duplicated guard is not belt and braces, it is two guards neither of which can be shown to be doing anything (#1539) 3c7e029b · dbf3dbe6…diff
  • packed object storage lands its READ half, and the decision that could not wait for the writer is what a pack is ALLOWED TO GROUP: membership is the address first byte, order is the address order, and the reader REFUSES both violations rather than the packer promising them. a pack adds structure over addresses and that structure sits OUTSIDE the ciphertext, so a packer grouping by grant list or visibility tier would make the pack boundary BE the set of objects a reader cannot open - an authorization-set disclosure produced by a storage layout, which no encryption below it helps. the rule is safe structurally rather than by good behaviour: addresses are blake3 over nonce and ciphertext (ADR 0004), so bucketing by addr[0] is provably uncorrelated with custody and a packer CANNOT group everything one identity can open. the stronger rule, no loose object in a packed bucket, is rejected as unimplementable, since one object arriving after a repack would make every store refused; the residual packed-or-loose bit is an arrival-order fact mtime already discloses. FORMAT_MAJOR goes 13 to 14 at the READER and not at the writer, because the number should mean written by a binary that can read packs, and deferring it to 1525 leaves a window where some binaries read packs and some do not and all report 13. what the bump buys is a named refusal instead of a silent lie: pack/ is not a 64 character hex name, so a v13 walk SKIPS it and reports every packed object MISSING, referenced by change and by path, where v14 answers UnsupportedFormat found 14 supported 13. v14 is the first major in this history that moves no wire layout at all, measured rather than argued - the two frozen parity vectors moved by their marker byte and every other frozen vector beside them is byte identical. two findings beyond the criteria. packing breaks ADR 0038 burn SILENTLY: destroy_objects_loose deletes a file, cannot reach a span in a pack, and would not fail either, the already-destroyed arm swallowing it while the caller records a tombstone over LIVE ciphertext - it now refuses, and the guard asks the DISK rather than the live index, because the index version was vacuously green with burn having removed the address before the save runs. and a packed read must re-hash where a loose read need not, since a wrong offset yields a DIFFERENT VALID object under the wrong address rather than garbage. the membership pin was vacuous on its first writing too, filtering with the same function the code bucketed with, so it asserted only that the two agreed (#1524) eb753b9f · dbf3dbe6…diff
  • the first review sweep of this run finds three live defects and six sentences that read wider than their subject, and the one worth the sweep is that status and log DISAGREED: a working change is a head, so RemoteDelta counted it, while scoped_view drops it from the listing - status said 3 where log listed 2, and the extra was a change push would never send. it was unpinned because every fixture FINALIZES, so the fixture came first and settled it rather than the argument; the subtraction is spelled as the two node projections, author present and signature absent, which is the same discriminator the bundle uses and ferry already reads. verify graded a pack I/O failure as object CORRUPTION while the loose arm propagated the identical fact, twelve lines under a doc saying cannot-read must never pass as verified - now verifies() is one predicate literally shared by both arms rather than two spellings that agree today, and scan_one_pack is split out so the propagating half is TESTABLE at all: an index read and a body read are two opens of a file not immutable at its name, so the only deterministic way to fail the second and not the first is to take the index and then remove the file. and [info] swallowed a real failure, since any resolve_remote error read as unconfigured including an unreadable settings file, so the arm now asks settings() a second time rather than matching on prose, keeping unconfigured for a genuine solo repo. the prose half is the doc scope-drift class six times over. read_index claimed to open a store in fewer syscalls WITHOUT reading a body and reads the whole file into memory; the syscall half was true and the bytes half was not, and the ranged read is left to 1525 with the obligation recorded there, including that a span past the end is currently checked against a buffer read_index already holds. two operator surfaces still said LOOSE objects after 1524 taught the census to count packed, and a THIRD site the ticket never named was found in DagRepo::census own doc. ADR 0023 claimed nothing changes what an unasked status emits, which was false twice - the JSON gained a field and 1524 moved contract 13 to 14 on the same shape in the same diff - so the surviving claim is the narrow one, that neither channel can be misparsed, for two DIFFERENT reasons. ADR 0019 now records that contract 14 names two status JSON shapes, because the marker buys a REFUSAL rather than shape identity, a floor and not an equality. the verbs census sentence had gone wrong one run after 1799 guarded the number beside it and warned in that very file that a derived figure can sit next to a claim that has not moved. the forge arm of the privacy pin was prose only and is now tested, and deliberately NOT by byte equality, because a forge envelope names its asker under ADR 0061 - what is pinned is that the payload unwrapped through the real unwrap_envelope is ZERO bytes on both positions, with the greater-than-64-byte envelope beside it refuting the recorder cannot see bodies (#1819) 124faca4 · dbf3dbe6…diff
  • loot gc --pack lands the write half, and the claim ADR 0083 deferred to this ticket is taken and comes back NEGATIVE: there is no size win and there cannot be one. a packed object is byte identical to the loose file it replaces, so a pack weighs its bodies plus 11 plus 44 per entry and the census total always RISES - measured through the CLI on a throwaway fixture, 12 objects in 11 buckets, 12094 to 12743 bytes. the groupings that WOULD compress are exactly the ones the ADR rejects as leaks, so the absence is the PRICE OF THE ANTI-LEAK RULE rather than an optimization nobody wrote, and the verb says so at the number instead of leaving a true pair that reads as a loss. the repack IS the collector: a rewritten bucket carries forward only the keep set, so an unreachable entry is simply not copied and there is no delete-a-span path to get wrong. what that leaves is a plain gc unable to collect packed garbage, and the hazard is the SILENCE rather than the retention, because nothing to prune - every stored object is referenced by a change is true of the files the pruner walked and reads as a claim about the store, printed at the operator; so a plain gc now OBSERVES it, one header and table read per pack, and prints what it could not reach beside the flag that reaches it. the census asymmetry is untouched: reclaimable_bytes stays a dry run of the plain collector, because a census agreeing with the collection is the whole reason it is trustworthy. the ranged read is implemented and it reintroduced a hazard the ticket never named - a table sized from a corruptible u32 is u32::MAX times 44, 188 GiB of prealloc - bounded now by the file, and the mutation removing that bound ABORTS the test process rather than failing it. burn still refuses a packed address and the remedy now exists and is pinned, since gc_keep_set already drops burned oids so the ordinary collector rewrites the bucket without one and no burn-shaped branch exists in the packer; it is deliberately NOT wired into destroy_objects, which runs from save taking a shared reference, because a bucket rewrite moves every OTHER member offset and the live index spans for the burned object innocent bucket-mates would go stale instantly - a destruction path that repairs itself by breaking unrelated reads is not the fix. crash safety is temp, read back through this module own reader, rename, caller unlinks, and the invariant at every instant is that every address being carried forward is readable from at least one complete file at a stable name, both of them in between; an interrupted repack is not a new failure mode but one of 817 torn writes, since the stage name is skipped by read_all and collected by the existing sweeper. two pins the mutation sweep caught as weak are fixed, one vacuous because a repo that put its own objects holds them in memory where the re-point deliberately does not reach, and one mis-named because its injected failure fires BEFORE the ordering it claimed to prove; the read-back-before-rename ordering has NO fault injection pin and that is said rather than invented, argued structurally from write_bucket having exactly one rename as its last act. and the verbs census attribution went stale a THIRD run running, caught in review here rather than after landing (#1525) bbbe984d · dbf3dbe6…diff
  • loot verify --unreachable lands the dangling report and closes the packing family, and the thing it does NOT do is walk: it answers from gc_keep_set, so the whole new computation is a SET DIFFERENCE over the scan the integrity check was already doing - no third reachability walk and no second directory pass either. 1523 refused to write the second one and this refuses to write the third, which matters because a report that could disagree with the collection it names as the remedy is worse than no report. the two directions deliberately take DIFFERENT root sets and that is the subtle half: verify own node list stays the roots for MISSING, because it carries the referencing change and path that a bare oid set cannot, while the keep set is the roots for UNREACHABLE, because it additionally holds the loaded graph and it DROPS burned oids under ADR 0038 - so using it for missing would hide deliberate destruction, and reporting a burned object as missing damage is exactly the inversion burn exists to avoid. the primary own working change is seeded once in each, by the code that already seeded it. the exit rule is structural rather than a renderer discipline: is_clean does not read unreachable at all, so garbage alone cannot fail a verify however the report is rendered, and the process pin deletes an object the listing did NOT name, chosen by subtraction, so its missing half is a positive control on its unreachable half rather than a second assertion of the same thing. size is the SPAN length for a packed object and the file length for a loose one, never plaintext, which loot cannot know without keys - and the two halves are reported APART rather than summed, because that is what keeps each one checkable against its own collector: the packed half is GcReport packed_orphans by construction and the loose half is the census unreachable figure, and the census must go on excluding packed garbage to keep the promise it is pinned to. ObjectScan now carries how each present object is stored, which costs ZERO extra syscalls because both arms already held both facts at the instant they graded an address. one residual is documented rather than hidden: an address stored both loose and packed, the window inside a repack, is attributed once to its loose copy. and the empty case prints that every stored object is referenced by a change - the exact sentence 1525 had to qualify at gc as scope drift - where here it is NOT drift, because the enumeration behind it is scan_objects and that sees packed spans too; the asymmetry is pinned as a test rather than asserted in prose. the verbs census moved to 345 and its sentence moved with it, the first run in four where that did not go stale (#1526) 061af7f3 · dbf3dbe6…diff
  • the second review sweep of this run finds gc --pack crediting packing with the PRUNE bytes and staying silent about it, and the repair is a NARROWER SIGNATURE rather than a wider condition: size_note took the before and after totals and returned nothing whenever the store shrank, so on this repo own store, 2330 unreachable objects against roughly 250 KB of table, it would print 146.7 MiB falling to 110 MiB and say NOTHING - the exact misreading ADR 0083 warns about in its own text, in the verb written to honour that warning. packing_note now takes the REPORT and cannot see the totals at all, so no combined direction is able to suppress it, and it fires on buckets_repacked. the figure is measured by the packer rather than derived from the pair: overhead_of is header plus table, encode sizes its first body offset FROM it, and the report carries the table a rewritten bucket now holds LESS the one it replaced, computed above the dry-run bail so a dry run reports what a real run does. the pin the attribution actually rests on is new and runs a real repack over a store holding a loose orphan AND a packed one, asserting that before minus pruned minus dropped plus table equals after with every term from its own producer and the sum checked against a census none of them took, 11012 - 4141 - 4141 + 275 = 3005, and asserting the two directions genuinely disagree, which is the defect in miniature on real bytes. the pin that encoded the WRONG rule is deleted rather than preserved. four weak pins have now been found in one run and the last two were invisible to the mutation sweeps, because a sweep only tests the pins you AIM it at and therefore cannot find a pin that tests nothing - the count rule of 1812 proves a pin is LIVE, not that it is aimed at anything, and that limit is now written down. two mutations make the point by experiment rather than argument: under a copy-instead-of-rename and under a helper that unlinks a bucket without writing its pack, the OLD tests stay GREEN. one proposed fix is refused with its reason, that routing a reader fixture through the writer would assert only that the two agree and go on agreeing if both move together, which is the same vacuity the membership pin was written to avoid and which eight reader tests depend on not having; the real complaint is fixed by asserting a structural post-condition instead. the walk census contradicted itself inside one commit and the count is now stated ONCE in CONTEXT.md with the code referring to it, choosing live set as the load-bearing word because verify own node list is a ROOT SET FOR ABSENCE that is never consulted about deletion and deliberately KEEPS burned oids where a live set drops them. the commit record of c6b1cae carries the wrong version and cannot be fixed, so CONTEXT.md records that too (#1823) 907d2a9d · dbf3dbe6…diff
  • the one flake four runbooks name as the thing to re-run CANNOT FAIL A LAND, and has not been able to for the whole life of the advice: all three describe_contention tests have carried the ignore attribute since #476, whose own subject is move contention stress tests out of the land gate - the gate is cargo test --locked and never passes --ignored, so run directly the file reads 0 passed, 0 failed, 3 ignored. the advice naming it was written by #406 and #404, both ANCESTORS of #476 as merge-base confirms, and nothing updated it when the ground moved. that is the strongest possible argument against writing a list, so what replaces it is the PROPERTY with the fixed instances as examples: the two recurring shapes, a control failure and a Windows sharing error out of a rename or a remove; a four-step triage that captures to a FILE before re-running, reads the COUNT rather than the word, and records that a green re-run is NOT evidence; and an explicit what-would-make-this-section-wrong clause. the concurrency framing this ticket was filed under does not survive anywhere, because the fifth occurrence fired with NO second session running and a process check to prove it - so running alone is not evidence against a flake, and no runbook now says otherwise. the root cause is confirmed FIXED rather than assumed: a statement-scoped census over the WHOLE workspace, not just the crate its own census test covers, reads ten temp_dir occurrences and zero shared roots, each read individually - two are pure string tests, one is a read_dir gated on a prefix that cannot reach the testkit dirs, two are children carrying their own pid, two are false positives whose pid arrives in the next statement, and one derives its stem from a fixture dir both constructors give a pid. the two originally named tests split, and the split is the point. the settled ledger one is COVERED: at the fix parent it read the bare shared root, so resolved() was the single file under TEMP that all 161 fixture sites shared, and a sibling saving an empty ledger between this test save and its load reads back exactly the reported empty map - it is also the test the fix own perturbation experiment PREDICTED and then hit. the persist codec one is NOT covered and never was: it has never used the shared root, its directory has carried a pid and a counter since #252, an ANCESTOR of the fix, so #1667 neither caused nor cures it - its race is its own eight threads renaming onto one destination, and per this ticket explicit non-goal NO RETRY was added, only the two facts recorded where the next reader meets them. and the control-failure signature is corrected before being written down, because tonight refuted its narrowing: #1864 also failed at a control and its medium was a process-global static rather than a directory, so the rule is that a control failure is the signature of SHARED STATE something outside the test wrote, with both media named. it is deliberately not written as therefore it is a flake, because a control can also fail when the change broke the setup path - it says where to look FIRST, not what to conclude (#1606) c87a13f6 · dbf3dbe6…diff
  • the open two biggest arms become COUNTABLE, so a regression in them can fail a land instead of passing all three checks: store.rs held no instrumentation whatsoever, no Work, no tally, no bump - and that NEGATIVE was re-verified before anything was built on it, because a negative that has quietly become false is exactly how this class of blindness starts. every line number had moved and every one is re-derived: the graph read at 2200 rather than 1993, the keyring read at 2228 rather than 2019, the two call sites at 4467 and 4510 rather than 3468 and 3502. the unit is READS, one counter rather than a pair, and the reasoning is recorded rather than assumed. a byte count is NOT 0%-gatable here, being a function of fixture contents and encoding, so every format change would fail a land for a reason its author cannot act on - and tolerancing it instead re-creates precisely the load-sensitive alarm ADR 0073 rejected the timings for. at this seam a byte count would also be blind to its own stated reason, since the lazy half of #1536 already landed and read_graph_required still reads the file WHOLE while deferring the per-change manifests, so bytes READ did not move for #1536 and will not, while bytes DECODED are a different counter at a different seam whose value depends on which manifests a verb happens to touch. the object pair precedent does not transfer either: those are two because a MEMO sits between them, and nothing analogous splits a metadata read. disjointness is BUILT rather than assumed, because object files are whole files in the store too - the object store now reads through its own door, same body, same retry budget, no tally - and the two of that door four sites which tally NOTHING today are named in the code and deliberately left alone, since closing that gap would move an already-gated number. the tally sits OUTSIDE the retry loop, because inside it would count attempts and read 1 idle against 17 busy, and that placement is pinned DETERMINISTICALLY rather than by timing luck, since an absent path walks the whole budget by construction. the structural-zero bar is met by SHAPE rather than by not-zero: across a tenfold fixture the object counters go 60 to 600 and 20 to 200 while the store counter STANDS STILL AT 24 - per-open and not per-object - and no tally left on the object door, nor one that had picked up a per-path read, can produce that pair of lines. four mutations with counts read, and the discrimination one is a discrimination rather than a constant because it ALSO fails the excluded-counters pin, catching it from both directions. the gate was then run for real in a release build: exit 0, the workload id UNMOVED so no baseline is orphaned, and the new metric absent from the baseline, so the first armed land records it and the land after that is armed. and ADR 0073 consequence clause is amended exactly where it read as full coverage, with the correction stated in as many words, alongside its table row, its status header, and six other surfaces carrying the same claim (#1662) 904f6996 · dbf3dbe6…diff
  • loot lanes now reads the shared graph and scans the object directory once per board draw rather than once per registered lane: a new DagRepo::load_positions_from reads the position files of every lane first and the shared half once after them, so each row keeps the read order load_from_needing documents while every row reports against one moment of the shared store. that is the decision the ticket asked for, recorded on peek_lanes with its reasons, among them that option c cannot help because a row needs both the graph and the objects. a single load now reads the working change before the graph, the order that doc states. on a scratch repo with twelve lanes and a 4.9 MB graph the board went from 237 to 190 ms, paired in both orders with loot status flat as the control, and its porcelain and json output is unchanged apart from heartbeat age. a new board test relating the store file reads of the board to those of its rows built one at a time went red on the unchanged code and with per-row loads put back (0 passed, 1 failed each), and an engine test comparing a batch load with solo loads went red with the mixed-store refusal removed and with one position half reused (0 passed, 1 failed each). the perf gate reads no move, CONTEXT.md follows, and the workspace suite is green (#1782) caed7d82 · dbf3dbe6…diff
  • review sweep 8 fix-up: the owed relay push recovery now lives once, in a new workflow.md section, paying an owed relay push, which covers relay=FAILED and relay=REFUSED in both shells with the position on every command, the build job cap and the in-flight guard, and states one done-when: the push succeeded, or its format skew is recorded on the issue as owed to the release relay redeploy. land-change and afk-loop point at it instead of carrying their drifted copies, afk-loop refers to that item and to its sections by name rather than by step number, as AGENTS.md now does too, and its when-it-goes-wrong list gains relay=FAILED. probe_pusher now says why a report was refused, with the parser message or the version check, and keeps the pre-1776 explanation for output that does not open a JSON object, and the pusher= docs say UNKNOWN means the report was not read. the new pin and the tightened pre-1776 pin went red before the change (0 passed, 2 failed), and red again with the pre-1776 arm removed (9 passed, 1 failed) and with the pre-1776 explanation given for the new pin reports (9 passed, 1 failed). LanePeek becomes a struct and a dead unwrap_or_default becomes an expect, the 4.2 s lanes figures in heads.rs and lanes.rs are labelled as history, the PositionHalf doc credits the reorder with matching the documented load order, the ObjectIndexScan doc no longer names its CLI consumer, ADR 0028 says merge_tips reads tree_at itself, and rewrap leftovers in land-change, format.rs and CONTEXT.md are joined. the workspace suite is green (#1983) 2ee76c18 · dbf3dbe6…diff
  • a save no longer fails because something opened the staging file it was about to rename: persist_codec::save_objects_loose finished a loose object with a bare fs::rename, under a comment in store.rs asserting that this rename creates rather than replaces and so opens no such window, which is true of the destination and silent about the source. a rename takes DELETE on the name it moves, so a scanner holding the staging file refuses it as a reader holding the destination refuses a replace, and that is what #1897 measured as an ordinary save failing with an opaque backend error in 11 of 50 paired rounds and what the suite saw as this crate racing itself. that rename now goes through store::rename_retrying, whose predicate already answered to both spellings the contention takes, error 5 through the permission-denied kind and raw error 32; the kind-versus-raw half of that was resting on a platform fact nobody had asserted, so it is asserted now where the platform can answer it. two pins carry it. a staging file held without FILE_SHARE_DELETE refuses a bare rename with error 32 and does not refuse the retrying one; and a scanner that watches the object directory and opens whatever tmp file turns up does not fail a save, with rounds retried until the handle is confirmed open, so a run that never entered the window fails rather than going green having measured nothing. red first with the counts read: with the bare rename restored the scanner pin fails with backend os error 32 out of save_objects_loose and the census reddens naming save_objects_loose, which no row covers (1 passed and 1 failed in the lib, 1 passed and 1 failed in the census); with the scanner looking for a suffix nothing has it refuses on zero catches over eight rounds rather than passing vacuously (0 passed and 1 failed); and with raw 32 dropped from the predicate the held-staging pin, the scanner pin and the predicate pin all go red (0 passed and 3 failed). the census of the other renames is derived rather than listed: tests/store_rename_census.rs walks every fs::rename under crates/*/src, keys each by its file and the fn it sits in, and asserts the set both ways, so a new rename arrives red until somebody names which end of it can be held and a named one that stops renaming arrives red too; with the relay index rename swapped for a copy the missing row reddens it (1 passed and 1 failed). the relay mailbox and the identity rotation carry the same source window and are named there with why each is out of reach from here rather than fixed. no migration and no wire or format byte moves. the relay links this crate and does reach this path, so its bytes rebuild, but the window is windows sharing contention and the relay runs on linux, where the only thing the predicate answers to there is a permanent permission denial that now waits out its budget before reporting, exactly as atomic_write already does; so this owes no deploy. the workspace suite is green (3985 passed over 124 binaries, 7 ignored) (#2026) e57db04d · dbf3dbe6…diff
  • a save no longer fails when the rename that finishes a loose object is refused over an address another writer already landed. persist_codec::save_objects_loose probes for the address before it stages, a writer of the same address can land it between that probe and the rename, and the rename is then a replace, which a reader holding the landed copy without FILE_SHARE_DELETE refuses as os error 5 for as long as it holds, past the retry budget. finish_stage now answers a refused rename with the address on disk by dropping the stage, since the name is the content address, and still propagates a refusal with the address absent. this is the second sighting on this ticket, concurrent_stage_of_same_address_does_not_tear red on Access is denied (os error 5): a hold on the destination refuses a rename as 5 and a hold on the stage as 32, and with a reader holding the landed address and a sleep both added temporarily that test went red at the same unwrap with the same message in 8 of 10 runs, and green in 10 of 10 with the fix. who held it in the sighting is not established, since a stripped copy of the race counting every refusal drew none idle or with the CPU saturated. the first sighting, a_scanner_holding_the_staging_file_does_not_fail_a_save red on os error 32, is not reproduced and this does not claim it: under a saturating load a 120 ms sleep, the length of the scanner hold, woke within about 206 ms while the retry budget stretched to about 1.5 s. red under mutation, counts read each time: the fallback disabled (0 passed and 1 failed), the stage left behind (0 and 1), every refusal swallowed (0 and 1), restored to 1 passed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4263 passed over 136 binaries, 10 ignored) (#2233) 4470683c · 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
  • the graph rewrite a finalize pays reads the graph file back deferred and copies each manifest stored frames where Manifest::frames proves them to be what encoding its map writes (no backslash key, keys strictly ascending in Path order, UTF-8 holder names), and falls back to the eager read otherwise, so save_to no longer decodes every path of every change in history to add one change. the new --graph-rewrite opt-in half times that whole save at 200 paths; paired, three interleaved rounds, the gate reading load idle at 1 to 4 percent: 48.18 to 48.48 ms down to 12.65 to 12.72 ms at depth 256, 183.43 to 183.49 down to 39.14 to 39.47 ms at 1024, and 750.3 to 759.4 down to 179.5 to 182.6 ms at 4096. counted by the new graph_rewrite_allocations test, the save at depth 1024 allocates 6,039 and 6,061 times on two runs against 695,198. the file written is byte-identical to the decoding rewrite, pinned over every golden and a lone y//ab key (copied), the legacy separator and the #2273 shapes (not copied, rebuilt from the eager read), and a key or holder name that is not UTF-8 (not copied, and the save still refuses it). red under mutation, counts read each time: the eager read put back (1 passed and 1 failed), the holder check dropped, the ascent check dropped and the eager fallback dropped (1 and 1 each), the counts check dropped (0 and 2), restored to 2 passed each. riding along: HUNT-PERF names the new half, and the visibility census names the moved and new bindings. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4278 passed over 137 binaries, 11 ignored) (#2280) e7d666e0 · dbf3dbe6…diff
  • loot blame and loot log --path read the one path they ask of each change through the new GraphView::address_at, which answers off the manifest frames through the new Manifest::address_of where the frames provably are the map (no backslash key, keys strictly ascending in Path order, checked by the keys_ascend step Manifest::addresses uses) and from the decoded map otherwise, so a history walk no longer decodes every manifest on the lineage to read one entry of each, and blame no longer clones each tree; has_path keeps decoding, since its remaining production caller asks many paths of each of a few changes. the new --path-history opt-in half times address_at over every change of a re-opened chain at 200 paths, asking the path that orders last; paired, one loot-perf-gate --features count built per arm with the frame arm never taken as the before, interleaved, the gate reading load idle at 1 to 4 percent (peak 7): 34.87 to 34.97 ms down to 4.46 to 4.53 ms at depth 256, 138.8 to 139.3 down to 18.01 to 18.03 at 1024, and 279.2 to 280.6 down to 35.9 to 36.2 at 2048; counted by the new path_history_allocations test at width 200, the walk allocates 484,352 times at depth 1024 against 0. through the binary, against the landing position binary over a repo 1,025 changes deep and 203 paths wide, min of 15 over two rounds: loot blame 160.4 to 160.6 ms down to 33.7 to 33.8, loot log --path 164.8 to 165.8 down to 43.7 to 44.0, plain loot log 28.9 to 29.2 on both, and every output compared was byte identical with its exit code and stderr: blame plain, --porcelain, --json, with a selector and on an absent path, log --path on flat, nested, dotted and absent paths, and shortlog --path. new pins: over every golden, a file whose Path order is not its byte order, the legacy separator file and the three #2273 shapes, address_of answers what the eager map answers for every key and for probes that are not keys, decoding nothing exactly where the frames are the map, and it answers off the frames past a holder name that is not UTF-8; a spawned blame and log --path find a/b.txt beside a-b.txt and a.txt. red under mutation, counts read each time: the frame arm never taken (0 passed and 1 failed in the codec pin, 1 and 1 in path_history_allocations), the ascent check dropped (0 and 1, on the answer), the counts check dropped (0 and 1, on the control, since Windows reads a backslash as a separator), byte order in the stop (0 and 1 in the codec pin, 0 and 1 in the spawned pin), each restored to green. riding along: HUNT-PERF names the new half. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4289 passed over 137 binaries, 12 ignored) (#2283) 045c7e42 · dbf3dbe6…diff
  • a_scanner_holding_the_staging_file_does_not_fail_a_save waits for its scanner to finish a first scan before it starts the save, instead of starting it the moment the scanner thread is spawned: under 48 cpu-burn and 16 file-churn threads on 24 logical CPUs, measured at 100 percent, an instrumented run showed the scanner had scanned zero times in 48 of 48 rounds, its first instruction 113 to 518 ms after the spawn while the whole save took 2 to 8 ms, so every round missed the same way and the round count was never what failed. the wait is the condition, bounded by the scanner reaching its first scan whatever that scan returned, so a scanner that cannot read the directory still fails at the guard rather than hanging; the 8 rounds stay, now covering only the race a polling scanner has by construction, and 42 of 64 rounds caught the file under the same load. no reason for 8 was recorded when #2026 chose it, and it is not raised. interleaved under that load the old test passed 1 of 10 runs alone (9 failed at its guard) and the fixed one 10 of 10; the loot-core suite failed at the guard under load with the old test (689 passed and 1 failed) and passed 3 of 3 with the fix (690 passed each). the guard and the property stay live, counts read each time, each restored green: the scanner looking for a suffix nothing has (0 passed and 1 failed at the guard), the scanner reading a directory that is absent (0 and 1 at the guard, no hang), and a bare rename in save_objects_loose (0 and 1, os error 32 out of the save). not an instance of the #2258 naming shape: its fixtures come from testkit::tmp and its staging names from stage_tmp_name, both pid plus a per-process counter. test-only; no migration, format byte or wire byte moves, so this owes no deploy. the workspace suite is green (4431 passed over 139 binaries, 13 ignored) (#2269) 8f1a341f · 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.