Changes touching this path

  • Extract the Surface face into engine/surface.rs The five projection methods — surface_with_report (write a change's visible tree to disk), readable_tree (its in-memory twin, the primitive `loot grep` searches), visible_paths_at, burned_paths_at, and materialize (dock-switch: surface + prune) — sat in engine.rs's "History & identity face" though they form one cohesive unit: turn a change + reader into the tree that reader may see, decrypting through the key oracle and honouring visibility/embargo/grant-expiry/ burn. They are the read-side twin of the Snapshot face (tree -> change). Relocate them into a new engine/surface.rs submodule — the "Surface face" — matching the negotiation/custody/merge precedent (impl DagRepo via `use super::*;`). A pure relocation: no interface or behaviour change. No result types move (the methods return tuples/Vecs), so no re-export is needed — just `mod surface;`; loot-cli's call sites are DagRepo methods, untouched. Deliberately narrow: the shared `grant_expired_for` gate stays put (the Repo trait's own `surface` also uses it), the Repo trait's `fn surface` stays (an independent implementation), and the loose accessor groups (identity, graph queries, record primitives, attestations, log) stay in engine.rs — splitting those would be navigability churn, not depth. The projection tests stay in engine.rs (they share the test module's helpers), the same convention the merge/negotiation extractions followed. engine.rs: 5889 -> 5715 lines. Full workspace suite green. 54e80d3a · dbf3dbe6…
  • 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
  • a sparse view scopes what the working tree materializes, and nothing about what syncs (#39) fa6c6749 · dbf3dbe6…diff
  • absence is not one boolean: surface writes what it can and names the rest, and capture asks whether the tree is a materialization (#891) ad28eb57 · 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
  • three papercuts from the alpha sweep: a defaulted diff on a clean tree agrees with status instead of exiting 1, evolog's when column shows the authored clock instead of the git-bridge stamp while porcelain keeps the frozen one, and view stops claiming a deletion it never performs and counts what it left on disk 19473311 · dbf3dbe6…diff
  • loot view --prune removes the paths a narrowed view stopped covering and refuses whole, naming every one, when any of them holds an edit no change records, taking nothing out of history so a clear writes them all back de9f395a · dbf3dbe6…diff
  • the ratchet records a build that exists again, ADR 0006 finally carries the verb that leans on its third case, and three documents stop asserting things the code beside them contradicts including a pin that held the number and not the strictness it was written for 62bf03ff · dbf3dbe6…diff
  • loot diff stops decrypting every path on both of its sides to fill in a bool, because the visibility gate now asks the cipher-free can_open #1422 built for exactly this beside the reader-scoped expiry check it must keep asking, measured at -58.7% by the first fixture in either tier that touches diff at all (#1426) 699a58e8 · 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
  • loot migrate and loot maroon stop refusing an author's own revealed content as unauthorized, because the two re-seals promote due embargoed keys at their own first line instead of borrowing one from a capture that --no-snapshot skips outright and that an unchanged working tree short-circuits past before the engine is ever called (#1485) 34b5bb79 · dbf3dbe6…diff
  • the reader seam's own doc stops telling a loot-core author that content can only be read through it, because the sentence #1464 wrote into four places and #1471 corrected in three is now enumerated in ADR 0007 as one thing that changes together, and spawn.rs likewise stops claiming every command every gate runs while GATES row 0 shells out to git beside it (#1486) b6657789 · dbf3dbe6…diff
  • the per-path custody gate stops building a heap key before it can ask its question, because the grant manifest is indexed as a map of oid to a map of grantee rather than one map under an (oid, grantee) pair, so grant_for probes both levels by borrow and the 4,000 Strings a timed visible_paths call allocated and threw away are gone, taking the visibility pair a loot diff makes over a 2000-path tree from 0.7026 ms to 0.6069 ms in both directions (#1550) ebe1b5c0 · dbf3dbe6…diff
  • six sentences that read wider than their subject are narrowed rather than deleted, because in at least two of them the sentence is the argument: the verb tier's width figure stops being typed at all and is generated off the impl blocks with the predicate written beside it, since this ticket's own 244 is not reproducible under any predicate - pub fn alone gives 202, every fn including test-gated gives 320, production-only gives 309 - which is the ticket committing a mild form of the class it was filed about, as was its claim that the figure had gone stale a second time when git log -S finds the sentence written once and never corrected; finalized_anchor is repointed to the two hops it really takes rather than dropped from the list of delegates; the duplicated surface tail is extracted and not deleted because BOTH copies are live, one a trait method the bench calls and one the escrow-promoted twin, so establish-which-is-live had the answer both; thiserror leaves two manifests that never derive it, each row replaced by why the absence is deliberate now that RepoError is loot-codec's type re-exported; the push-shape row is documented and pinned rather than renamed, because a metric string is output and this change is behaviour-neutral, and the new pin found the drift is two rows and not one; and the whole cost of opening a repo is narrowed in all three live copies rather than the single one named here, since correcting one of three is the trap #1629 records, while ADR 0012's copy stays because its subject is the loose-object slice and not the harness. one finding was simply wrong and is reported as wrong with no edit, since run_checks has a production caller and fourteen tests and the file this ticket cited does not exist. and load_from turns out not to be what the open calls at all: assemble asks load_from_needing with the verb's declared RepoNeed since #1536, so the timed call is the widest spelling rather than the live one, which is now said in both places that time it (#1656) 44b5b98a · 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
  • loot surface stops opening every object four times, and the instrument had to come first because the land's own gate is structurally blind to this: the gated fixture reaches the engine's Repo::surface, a different loop with no view filter, no report, no clobber guard and no CLI wrapper, and loot surface is in no cli tier sample, so a flat gate here says nothing either way and the named signal is a new loot_count::during region instead. The win is that the clobber guard has already resolved the exact Modified set and the materialize then rewrote every path anyway, so a SurfacePlan carrying at_risk, measured_at and already_on_disk hands that answer down: object_gets falls 4N to 3N at every one of the ticket's sizes (200 to 150, 800 to 600, 3,200 to 2,400) with object_disk_reads flat at N, which is the shape of an answer that got cheaper rather than one that changed. The dangerous part is not the speed but the authorization, because the third gate is inside the very step the set skips - sealed::open's embargo-then-visibility check is the one chokepoint and a trusted path is not re-authorized - so the trust is taken BELOW the view (#39) and grant-expiry (#20) gates rather than above them, and both orderings are pinned by tests that were proved red by moving the trust up: an expired grant materialized, and a path outside the view written. A plan measured against another change is refused by measured_at, and that breakage went red with disk holding the midpoint's bytes where the tip's were owed, which is content corruption rather than a slow path. Building the set by stat instead of a proven get went red twice, once on an embargo that has not lifted and once on --force restoring recorded content, so the set only ever names a path a Readable::get already returned Ok for - a precondition on the caller that the engine cannot re-derive, stated in the doc of the pub method rather than implied. The pin asserts the multiplier is identical at all three sizes rather than any literal, and its control refuses a store that cannot reach disk, which is #1576's --closure lesson made executable: the blind shape is the Workspace that authored the change, where put filed the plaintext and a surface decrypts from a map at object_disk_reads 0; the control is demonstrated refusing that, accepting a reopened one, and a separate non-vacuity guard is demonstrated firing on an empty fixture. One of this ticket's premises is wrong and is pinned rather than repeated: surface --json has always materialized once, not twice, because the machine arm returns before the human arm ever reaches surface_with_report - measured at 3N over a wiped tree - while rehome --json really did run two complete materialize passes and now reads the written it already had. The readable_at-per-path hoist is declined with the reason written into resolve_delta rather than left silent: on an empty escrow, which is every repo with no pending embargo including this ticket's own fixture, Escrow::flush is a map emptiness check collecting an empty iterator and contributes nothing to object_gets, so the hoist would measure flat in its own signal and wants an instrument that can see escrowed entries before it wants an edit. absent_paths_at keeps its N opens deliberately, because it walks the whole recorded tree including out-of-view paths the materialize loop never reaches and deriving it from that loop would silently narrow the listing. One TOCTOU window widens in kind and not in size: a file another process edits between the disk walk and the materialize is now left alone while still reported materialized, the same window the guard's answer already went stale in and in the operator-safe direction, so the type doc says surface's disk-mirrors-this-change promise is that much less absolute (#1703) 43a89f31 · dbf3dbe6…diff
  • the fix-up a cross-ticket review sweep asked for, and both halves were the same defect wearing different clothes: a claim stated where nothing can check it. surface_with_report_reusing's doc said Workspace::surface_plan is the only caller and meets the precondition by construction, which was wrong twice - surface_plan calls it nowhere, being the PRODUCER of the set, and it lives in loot-cli while the door lives in loot-core, so the sentence named a function one crate away as a caller it is not. The door is also pub, which makes the only caller half unenforceable in principle rather than merely untrue today: a caller in any crate compiles without meeting anything. The replacement names the real set, verified by grep - surface_with_report passing an empty set, Workspace::surface_with_report_reusing as the one non-empty caller, and three in-module #1703 tests that hand-build the set, two of them naming a path the reader may not open, which are the standing demonstration that this walk will accept anything at all. A second sentence understated its own mechanism and is corrected in the same pass: surface_plan does not merely record which change it measured, SurfacePlan::already_on_disk_at COMPARES and returns an empty set on mismatch, so the same-change precondition does have an enforcing mechanism and it too is one crate away. That asymmetry is worth naming because it sizes #1710: the set-vouching half has no mechanism anywhere, the same-change half has one in the wrong place, and only one of them needs a type moved. No API changes here - making the door private is #1710 and is deliberately not smuggled in. The other half is three authored figures sitting inside the doc of prove_the_walk_repeats, the non-vacuity control this whole file rests on, saying 192 occurrences against 31 addresses and a 6.2x gap while the assertion below pinned none of them, so editing PATHS or CHANGES or EDITS left all three silently stale in the very place that exists to stop un-grounded claims. OCCURRENCES is now derived as CHANGES times PATHS, the bare literal 4 already in the assertion is named MIN_GAP, and the floor is a const assert that fails at COMPILE time before any fixture is built. Four arms proved it rather than argued it: the baseline reproduces 192 and 31 and 6.2x exactly, so the fixture being reasoned about is the one the old prose described; widening to twelve paths moves the figures to 372 and 42 and 8.9x with everything green, which is precisely the silent staleness that used to be possible; collapsing the gap to one path fails to compile with the floor's own message; and re-authoring OCCURRENCES back to 192 against twelve paths goes red at 288 versus 192. The module header's other readings are marked a dated reading that nothing asserts, following lane_new_sweep.rs, because they come from fixtures this file cannot see and deriving them here would be a second invented claim rather than a fix (#1709) 1b45fed0 · dbf3dbe6…diff
  • loot grep grows the options that make a search usable, and two of the ticket's acceptance criteria turned out to ask for something this verb already did unconditionally: -n wants line numbers, which loot grep has printed since #391 with no flag at all, so making them conditional would silently change the output of every invocation ever typed and -n ships instead as a pinned compatibility spelling of the default, exactly as -F now spells out the fixed-string matcher that stays the default with the regex behind -E. the same compatibility argument decides the row shape, so the change column is printed only when the selector was a REVSET, a thing this verb could not search before, while a point selector keeps the three-column row it always had. the pathspec is the declared option group and not a fourth hand-rolled one, which cost the leaf its NO_PATHS declaration first: a group's trailing list begins where the leaf's own arity ends, so grep declares two opaque positionals and attaches PATHSPEC, and because its selector is optional a bare trailing path still reads as the selector and the operator says which they meant with the separator. -E is a hand-written POSIX ERE over bytes rather than the regex crate, which is loot-cli's standing dependency-light no-clap stance applied rather than a new decision; that stance is argued in the ctrlc note in the workspace manifest, not by adr 0041, which governs loot-core and the server-side dependencies and does not reach this; and the cost is measured rather than guessed, by resolving regex alone and diffing its closure against this lock: three new entries, regex and regex-syntax and aho-corasick, since memchr and regex-automata are already in the graph through bstr from gix, though regex-automata is linked there with default features off and carries no dependencies at all in this lock, so regex would compile far more of it than anything does today, all to serve one optional flag on one read verb. AC7 is a real requirement and it is keyed on the content address rather than the path, since repo.put mints a fresh address per call and only a carried tree entry reuses one: measured, a two-change revset over three paths each opens four objects where the natural walk opens six and returns byte-identical rows, which is why the pin counts ObjectGets instead of reading the answer. AC6's aggregation needed a fixture the engine refuses to build the obvious way, because a path this identity cannot open cannot be re-captured while it sits on disk, and the count is per change and path, so one sealed path carried through two changes is two skips. the per-object read is loot-core's own readable_tree classification lifted out rather than copied, so grep and surface cannot come to disagree about what a burned path is (#1500) c1ad796e · dbf3dbe6…diff
  • loot archive grows prefix, format, -o and a pathspec, and the pathspec is the DECLARED group rather than a fourth matcher: no Glob, no matches and no string compare against a pattern is written anywhere in this change, so the empty-match refusal and the 1330 cwd rebase arrive free with the attachment. the attachment forced one thing the ticket did not name - archive was declared NO_PATHS, so under 1551 the group trailing list would have begun at position ZERO and swallowed the output file - and it now states its own arity of two opaque slots first, both opaque because a file name, an -o value and a prefix are not repo paths and must not be rebased. the honesty criterion is the one that carried risk, since this adds a SECOND reason an archive can be partial beside the reason 416 built the summary for, and the two must not collapse into one number that reads as either: three populations are kept apart, written, then unreadable as selected minus written which KEEPS 416 word partial, then outside as total minus selected which gets its own clause, its own denominator and the explicit words by request not unreadable. the first denominator switches to selected and SAYS selected exactly when a pathspec is in force, so 1 of 2 cannot be read against a five path change, and the pathspec clause prints EVEN AT ZERO because silence there is indistinguishable from no pathspec at all. written is derived from selected by MEMBERSHIP, so written under selected under total holds by construction rather than by a subtraction that could underflow, and the engine skipped count is deliberately NOT used because it is taken over the whole tree and would therefore count paths the operator never asked about - that IS the collapse. the format is tar.zst and it adds an EDGE rather than a crate: zstd is already default-on in loot-codec and loot-core takes it with default features, so it already compiles into the shipped binary and the lock gained exactly one line with no new package. gzip was rejected on MEASUREMENT rather than taste - flate2 is in the lock but its only dependent is automerge, which only spike-crdt links, so it reaches NO shipped binary and choosing it would put a second compressor in the released loot; the wider extractor reach is the real cost and the format table says so. it does not reuse the codec own compressor either, because sharing it would weld archive bytes, a thing people compare hashes of, to a level chosen for objects in a store. the byte-reproducibility promise is NARROWED where an operator reads it: the ustar bytes are host independent, the zstd framing is reproducible only for a given zstd build. two pins were aimed rather than merely live - the -o ambiguity mutation is invoked with @ rather than HEAD precisely so the mutated build SUCCEEDS, which makes refusal the assertion, and tar.zst is checked by DECOMPRESSING rather than by length, since a wrap that compressed the wrong buffer stays green under a size assertion. and a stale doc claim was found on the way: readable_tree was described as what loot grep searches, false since 1500 moved grep to readable_object, narrowed here and in the copy of it that archive carried (#1530) 95a1cfeb · dbf3dbe6…diff
  • grant promotes at the clock it grants at, and the conservative alternative turns out to be an ORDERING ARTIFACT rather than a policy: skipped_unheld recorded only whether some earlier command in the repo whole history happened to open a reader, so one input at one clock gave two answers, and no state anywhere expresses that a reader has revealed an embargo. the flush goes on grant first line and rotate_regrants inherits it BY CONSTRUCTION rather than by a second flush, because it reads no content key except through self.grant - so the two cannot drift apart the way four hand copies of one body did. grant_sealed gets NOTHING and the ticket premise is refuted there: its keyring-OR-escrow lookup is already order-independent, which is why its pin was the one GREEN BEFORE THE FIX, and that measurement is recorded rather than a flush added to make the set look uniform. the eager direction, promoting a not-yet-due key, is not expressible through this seam because a flush IS the reveal gate applied - Escrow::flush now >= reveal_at is sealed::open own comparison. what WOULD be a hole is an escrow FALLBACK that skips the gate, which grant_sealed has deliberately for ADR 0027 timed deposit and grant must never copy, since a tag-1 bundle is a plaintext key in a file. the cost is asymmetric and lands on rotation: a grant dropped from the loot id rotate wave is access PERMANENTLY LOST, and the report line about what the outgoing key can no longer read was false for a due embargo. three pins, ONE REPO PER ARM because Escrow::flush promotes EVERY due entry and a second embargoed path in one repo makes the second arm vacuous - the trap #1485 hit - and both clocks pinned on each, since a fix that reveals early is worse than the bug. four mutations make the case: deleting the flush reddens grant AND rotate, which is what proves the by-construction inheritance, and giving grant the escrow fallback reddens the CONTROL arms instead. the caller list on flush_due_keys stops being a hand-written count and becomes a membership rule with a grep census, which is what kept it right while it gained a caller (#1488) 1d44cc64 · dbf3dbe6…diff
  • the KEYRING IS A CACHE, NOT THE GATE, and the reveal comparison is asked at every read off the sealed object own header rather than inherited from whatever a persisted keyring happens to hold. #1488 repaired the un-promoted state and left the other one: flush PROMOTES AND NEVER DEMOTES, and the promotion persists, so at a clock short of reveal_at a never-promoted repo REFUSED and an already-promoted repo GRANTED - one input, one clock, two answers, in the direction that ticket own done-bar calls worse than the bug. the sweep claim was REPRODUCED BEFORE ANYTHING WAS FIXED, twice: swapping the two arms of the old pin reddens its control, and two fresh repos differing ONLY in whether a flush ran first answer granted against Unauthorized. grant now reaches its object and key through ONE PRIVATE DOOR that flushes, looks the key up, then asks sealed can_open - which is open steps 1 and 2 VERBATIM, so the gate is the SAME FUNCTION a plain read uses and cannot drift from it. demotion was rejected on merits rather than taste: it would need the escrow to retain an entry it has HANDED AWAY, it would make a READ MUTATE CUSTODY BACKWARDS, and it would be one more copy of a rule vis already carries. the refusal stays Unauthorized deliberately, because rotate_regrants classifies by VARIANT and an Embargoed would turn a skipped standing embargo into a FAILED ROTATION WAVE. the doc defect is repaired where it actually sits: membership of the flush census is NECESSARY AND NEVER SUFFICIENT, since a flush repairs only the un-promoted arm, and the #1488 amendment now records the invariant it kept rather than the one it claimed. reachability is reported honestly rather than dramatised - a forward-only wall clock CANNOT reach it, because a promotion requires the clock to have passed reveal_at, so it needs a now BEHIND a clock at which a promotion already happened: the engine API directly, where a perf harness already passes a backdated now, or a backwards wall clock, which is ADR 0007 own D-threat. the structural violation is unconditional and is where the claim is made and measured. three mutations, and the two halves fail in OPPOSITE directions: removing the gate reddens the new pin and the re-pinned control while the rotation pin stays green, removing the flush reddens the two due-direction pins while the new one stays green - which is the separability #1488 lacked. and one limit is stated rather than dressed up: the both-ways-round half builds a fresh repo per arm, so no mutation can redden only the second run, and the doc says so (#1840) ae911b61 · dbf3dbe6…diff
  • two readers stop answering a visibility question out of a book the enforcement path never opens - and the ticket OWN worry about which ROW is REFUTED before anything is built on it: path_in_history consults current_tree FIRST and returns outright, with the reverse-topo history walk only a fallback for a path the live heads no longer carry. the keeps-every-change-whose-tree-contains-the-path shape belongs to change_has_path and filter_history_to_path, which is what log --path filters on and which embargo-status never touches. so the verb was already answering about the CURRENT recording, and its defect was purely WHICH BOOK rather than which row - which makes the fix cleaner than the ticket allowed for, since the tree entry is still read, being what LOCATES the object, and only the visibility moved. embargo-status now asks a new seal_visibility, the refusing twin of visibility_of, sharing held_but_unreadable with embargo_reveal_at, which is re-expressed over it so there is ONE read door. the tree entry answers ONLY when the seal cannot be produced, and then the output SAYS SO, naming the source it read, the guarantee it does not carry, and the verb that tells a missing object from a corrupt one. seal-only was rejected with its reason recorded: this verb exists for why is this file not visible after a pull, which is exactly the case where the object legitimately is not here - a withheld forge object, a mid-pull absence - so refusing there would delete the verb purpose. healthy output is byte-identical to before. conflict_side loses its fallback rather than gaining a guard: one object read returns both the seal visibility and the open verdict, and sealed_to_us is extracted so the two doors cannot drift about which errors mean sealed-to-you. the sizing is stated rather than flattered - NO observable answer changes on any state reachable today, because old and new both propagate the rot, and what changed is REACHABILITY: the mutation restoring the pre-fix shape reproduces the defect exactly, a truncated object rendering as internal. ADR 0012 gains a row for each site, and its visibility_of row is CORRECTED, having claimed conflict_side as a live caller. the wire-format option is neither taken nor touched: folding visibility into the change id is a FORMAT_MAJOR decision, the ticket ruled it outside an AFK warrant, and the enforcement question is written up to be filed rather than begun. seven mutations, every pin asserting a VALUE - a reveal instant, an oid, an error code - and never merely that something failed, which is the trap #1578 pin fell into. and the vacuous-filter trap fired once and was caught by reading the NAMES rather than the word: a filter on embargo printed 22 passed, and none of those were the three new render tests, whose names do not contain it (#1581) 74526297 · dbf3dbe6…diff
  • the precondition that was prose becomes a TYPE AT THE SEAM, and both halves take that answer rather than only the cheap one. the vouching type moves into the crate that holds the door, with four private fields, and the door takes IT instead of a bare path set - so the only thing that can widen the set in a shipped build sits DOWNSTREAM OF ITS OWN GET. and that is not a second read, it IS the read the producer was already doing: the live arm was already exactly that expression, so the engine now RECORDS the open rather than the CLI asserting afterwards that one happened. one get either way. the same-change half, which already had a mechanism but one crate away from the door it protected, moves across the seam and gains two clauses NOTHING checked anywhere before - the reader and the clock - and it compares rather than records, returning the empty set on mismatch rather than an error, because a bisect midpoint must still pay the walk in full. the plan can no longer hold a set and a measurement that disagree, for the simple reason that it no longer has two fields. a THIRD precondition falls out structurally rather than being written down: only the live arm can vouch, so a plan built from a both-recorded spec now arrives EMPTY rather than full and unearned. the door stays public and that is now a DECISION with its reasoning in three places rather than an omission - making it private, as its neighbour three lines below already is, would close the hole for one crate while the public trait keeps surfacing content, so an embedder would lose the skip and nothing else. and the cross-key door is split out to take NO vouching parameter at all, because a move labels and never vouches: the hazard stops being expressible instead of being filtered by a dead guard. the mutations bought a REAL HOLE rather than confirming the work. two of them survived the first round, because every test built its vouch THROUGH the door, so the insert-side reader and clock checks were SHADOWED by the door-side ones - one identity open could fill another identity set, and an open at one clock could back a set labelled another. an isolating test was added, and both redden now. the counters are MEASURED rather than argued: an identical probe over surface and status at three sizes, run against a byte-identical copy of the unmodified tree, reads the same on every gated counter - with the honest caveat that the allocations column is VACUOUS, since that probe installs no counting allocator, so it is not claimed. and the single claim resting on the type system alone is NAMED as such: that no other crate can widen the set is held by the compiler privacy rules and not by any test, because this workspace has no compile-fail harness to assert it with (#1710) aca3a188 · dbf3dbe6…diff
  • the commit that derived a list precisely so it could not go stale had typed its cardinality into the doc above it, and both counts are gone: the doc now says the refusal names the whole flag set - every verb this binary dispatches and nothing else, pinned in both directions - and that what the one-line shape rests on is READABILITY rather than a size. the obvious fix, pinning the count as well, is refused on purpose, because it would fail a land for the offence of adding a sixth verb, and that refusal is PROVED by a mutation adding one to both sides that stays green. the distance-crate check stops presenting a denylist as a property: it is widened with the five crates the review named, loses its own typed length so the constant carries no count either, and says plainly that it is a named list of common spellings and not a test for the category - naming BOTH holes in the voice its other stated limit already uses, a crate under a name nobody wrote down and a renamed dependency, the second confirmed by a mutation that stays green on purpose. matching on any dependency that merely looks like a distance crate was rejected as a guess dressed as a check. the derived method width is decided to be a TRIPWIRE and not a ledger, so its gap prose is dropped rather than extended - it was wrong both ways at once, excusing a gap that another clause already attributed while a new one had opened unnamed - and the doc now says outright that nobody keeps the per-ticket clauses summing to the figure. and the flag argument left at the vouching seam is examined rather than refactored for its own sake: the triple riding three doors IS the resolved delta still being assembled, so a new struct would only be a second name for it, and a bare bool would be the same flag one level down. the one real narrowing is taken instead - a caller that always passed Some now takes the set directly, so no door a caller can reach lets it choose to skip recording the open (#1922) 0ac07831 · dbf3dbe6…diff
  • the trait surface stops being a second materialize loop and becomes a one-line adapter over the walk loot surface runs, so a withheld object no longer fails it and the land gate counters fixture now measures that walk: the two promoted walks share one classifier deciding which failed reads are invisibility and which are faults, a burned read kept distinct because the surface walk labels it rather than counting it as sealed. a new test drives a withheld object through the trait and went red with object not found before the change, and red again when the adapter was reverted or the classifier arm broken. every gated counter reads the same before and after on the gated fixture, as expected for a fixture with no view and every object held. the 1703 sentence calling the gate a different loop is corrected to what it still cannot see, and the stale two-of-five counts in measure.rs, work_counters.rs and the ADR 0073 heading are replaced by what defines each set rather than a new number (#1927) f930875a · dbf3dbe6…diff
  • the Admitted and VerbRun docs and CONTEXT.md stop saying every argv reader goes through the spec: the readers that parse positionals and flags do, the consents and relay keys now say on their own docs that they scan argv by flag name, and the named exceptions lose their count and point at the census, as bisect dispatch now does. the stale arity-census counts in main.rs and loot-core flags.rs are gone, and the owed-arity rule is one function that the top-tier census and the counting pin both call, and each still went red when burn was re-opened. the flag census and the Admitted census now share one src walk keyed by relative path, and the Admitted census went red on planted leading_word reads in cmd_bundle and verbs/mod.rs, naming the second by its path. the surface.rs privacy argument now matches Repo::surface reaching the walk unpromoted, CONTEXT.md and the resolve comment stop calling its refusal the usage line, the Verb and pick.rs doc comments are separated so rustdoc keeps them apart, and two rewrap leftovers are fixed (#1939) 7b04e214 · dbf3dbe6…diff
  • the tree writers no longer remove the file they just wrote on a recorded case-only rename: materialize_target and materialize_promoted wrote the target tree and then removed each old path the new tree no longer holds by name, so where the filesystem folds case, as this windows machine and a default macos volume do, the removal took off disk what the write had just put there, and loot undo over such a rename reported pruned 1 path off disk, left the directory empty and made the next status read the recorded path as deleted, which loot move and converge did too, each reproduced by hand with the pre-change binary. both writers now prune through loot_core::disk_entry::prune_unwritten, the one home for the file and directory-entry rule apply-patch has taken since #2007 and #2014: an old path that is the directory entry of a path just written is renamed onto the written spelling, which keeps the bytes and gives the last name component the recorded case, the old name of a hard link is still removed by name, NotFound is a path that was not on disk rather than a path pruned, and any other removal failure is an error naming the path, where materialize_promoted discarded the error and materialize_target counted a failed removal as pruned either way. the identity helpers move out of apply_patch.rs into the new module, drop_empty_parents is shared with the view prune, and the prune makes no filesystem call beyond its removals unless an old path and a written path fold alike. the new pins went red with the entry rule removed (cli 1 passed, 3 failed; core 4 passed, 1 failed), with the old path kept and not renamed (cli 1 passed, 3 failed; core 4 passed, 1 failed) and with the removal error discarded and counted anyway (core 3 passed, 2 failed; the cli pins stay green, since none of them fails a removal). the perf gate dry run reads every counter the same before and after and says no move, ADR 0022 carries the amendment, ADR 0082 section 4 says where the rule lives, CONTEXT.md names it beside the apply-patch rule and in the re-pointing entry, and the workspace suite is green (#2011) de9fb5b0 · dbf3dbe6…diff
  • cherry-pick, revert and a surface that settles a stale-disk debt no longer destroy a case-only rename where the filesystem folds case, and the first no longer records the loss: #2011 routed two tree writers through loot_core::disk_entry and wrote that every writer which removes a path it may just have written asks that module, while two that did not were sitting in the tree as the sentence was written. Workspace::apply_change_delta, the one core cherry-pick and revert share, walked the merged actions as a BTreeMap and so wrote A.txt and then removed a.txt by name, which where case folds is the file it had just written, and it snapshots, so loot revert over a recorded case-only rename reported both paths converged over an empty directory and minted a change recording 1 deletion. Workspace::stale_debt_the_surface_left, the one scoped deletion a surface is allowed, compared the written set by name, and a case-only rename preserves the bytes, so the digest guard that exists to protect the operator matched the file the surface had just written: loot surface printed the path it had surfaced, left the directory empty, and the next loot status read it as deleted, which is the reproduction #2011 declined on the false reading that a surface prunes nothing. Each was reproduced by hand through the spawned binary first and then as a new spawned pin, and the pair went red over an empty listing before the change (4 passed, 2 failed) and is green after (6 passed); with the delta writer put back to removing by name they go red again (5 passed, 1 failed) and so does the new census (0 passed, 1 failed), and the stale-debt prune put back reddens the same two with the same counts. Both writers prune through prune_unwritten now. Which working-tree removals are weighed stops being a sentence: workspace.rs carries a census that reads every removal joined onto a working-tree root out of loot-cli/src and loot-core/src, names what each one removes and what the walk cannot see, and it named the offending function under each of those two mutations, went red at its own classifier control with ROOTS emptied and went red with a name dropped from its expected set (0 passed, 1 failed each). one_entry refuses a path that is or passes through a symbolic link now, since file_id follows links and on Windows two paths with one id were otherwise one entry, so fs::rename would have moved a link over the file it points at; the check is the symlink_on apply-patch already made, moved into the module, pinned with a junction here and red without it (5 passed, 1 failed). The hard-link pin names its links A.txt and a.txt so the fold map reaches the entry rule at all, where old.txt and new.txt never called it, and it skips a filesystem that folds case with a note; it stays unix-only and its red-first has not run here, though the body type-checks and runs on windows with the cfg lifted (7 passed). The four case probes become one loot_core::disk_entry::folds_case, and with it answering no the entry pin and two apply-patch case pins go red (5 passed, 1 failed; 18 passed, 2 failed). enclosing_fn moves into source_walk beside the #1929 census that asked it first and strips any visibility, after pub(super) made the new census name the wrong function. ADR 0022 carries the correction and its prunes-nothing claim is gone, ADR 0082 section 4 states where the probe and the link refusal live, CONTEXT.md names both writers and the census, the surface.rs and materialize_target notes stop claiming the two prunes answer the same set of failures, and the rewrap leftovers in ADR 0022, CONTEXT.md and the account page are fixed. The perf gate dry run reads every counter the same before and after and says no move both times, the site gate is green, and the workspace suite is green (3824 passed) (#2018) baec4112 · dbf3dbe6…diff
  • loot seek reads what a question keeps and nothing else: the loot backend read every change through readable_tree_at, which decrypts the whole tree and only then narrows to the pathspec, so a scoped search cost the repo and a listing decrypted every file to print its name. a search now opens each kept path once through readable_object_at, keyed on the content address so an object carried across the changes of a revset is read once, the rule grep has followed since #1500, and a listing decrypts nothing: a local target asks the visibility gate per kept path through a new per-object form of the same predicate, Readable::visible_object and Workspace::may_open, together with the store index and the burn log, which names the set surface prints; a remote target keeps the tier gate. the counted read pins it in tests/seek_object_reads.rs with its control: a whole-tree search opens one object per path, a listing reads one sealed header per kept path and none outside the pathspec, a scoped search opens the subtree and not the tree, and a revset over two changes opens a carried object once, seven where the old walk opened twelve. a listing still reads each kept header once because the key gate reads the sealed header to ask the keyring, which the counter counts as a get; it decrypts nothing, and that is stated where the pin is rather than claimed as zero. measured on this repo: a scoped search over crates/loot-cli 189 ms before and 123 ms after, a bare listing 167 ms before and 124 ms after, a whole-tree search 241 ms before and 223 ms after, best of three each; this repo holds small objects, so the win here is the fixed per-invocation cost and the pins are what carry the claim at scale. the two-axis review before landing made the engine gate one function the tree walk and the per-object view both call rather than two copies under a parity sentence, reduced the listing to the oracle alone since can_open is already false for an unheld or burned object, named the scan cache Scanned with unreadable, silent and hits, said the count is not surface count because surface labels a burn and this verb counts it sealed as before, pinned a burned path leaving the listing and never being opened, and recorded that the ticket premise of a returning visible_paths_at was false. red under mutation: the address cache dropped (0 passed, 1 failed), a search reading the whole tree (0 passed, 1 failed), a listing gating the whole tree (0 passed, 1 failed), a listing skipping the key gate (0 passed, 1 failed), and the engine gate no longer asking the keyring (0 passed, 1 failed). no migration, no wire or format byte moves, and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3981 passed over 124 binaries, 7 ignored) (#2102) d7606a23 · 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.