Changes touching this path
- verbs/: seam scaffold + the repo family (init/clone/config) (#661)
28849eaa · dbf3dbe6… - verbs/custody: surface/grant/grant-status/embargo-status/maroon/migrate/burn (#661)
e3e7937f · dbf3dbe6…diff - emit_snapshot calls verbs in-process; test-support feature retired (#661)
8033f451 · dbf3dbe6…diff - a sparse view scopes what the working tree materializes, and nothing about what syncs (#39)
fa6c6749 · dbf3dbe6…diff - a relay is measured, not capped, and the only bytes it may forget are torn writes (#817)
f52d53e4 · dbf3dbe6…diff - a passphrase-locked repo can pull from a relay, because filing keys needs only id.pub, and it says plainly that it wrote no tree and folded no heads instead of looking like a whole pull (#1172)
cbc60097 · dbf3dbe6…diff - a wrong LOOT_PASSPHRASE stops shutting the locked-pull door it was never a key for, the one verb a locked repo runs names the session file it refused, and ADR 0068 stops contradicting itself about its own cryptography and its own amendments (#1231)
ddcf499d · dbf3dbe6…diff - a verb run from a subdirectory finds the repo instead of advising an init that would nest a second one, path arguments rebase from the cwd the way a tester types them while view's globs stay root-relative, and the resolved root is named on stderr whenever cwd is not it
eca43819 · 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 - 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 run's own signature defect, found inside the file it built to fix it: source_walk said four consumers while the very next land added a fifth, and the sentence is now generated rather than corrected - it walks src and tests through the module's own code mask so a mention in a comment or a string is not a call, groups by enclosing function because the destination census reaches the walk through a use import and a call-site count would miss it entirely, and pins the shape as well as the number by requiring every consumer's file be named, the declaring units equal the consuming units, and every consumer call the discrimination floor first. that last requirement is where this ticket was wrong: it said one consumer skipped the floor and that two others called it, but the crate held exactly two call sites, so three of five skipped it - all five call it now, and the reason is recorded at the verbs site, since its three anchors check that this walk found Workspace's methods while the shared floor checks the walker on the two cases it was written for and this tier has none of. ADR 0074's section 9 stops describing a walk that no longer exists: only the mechanism sentence moves, with a dated amendment quoting the old text verbatim and saying why it was worse than stale, because it named a bug the walk did not have while carrying two it did, while the purpose paragraph and the six-entry allowlist claim are untouched. the trust page's twelve-item enumeration goes, and the durable half is a fourth pinned surface beside install, the ADR and CONTEXT: the positive arm pulls line three out of ADR 0066 itself and requires both pronoun-free halves verbatim rather than typing them, the negative arm forbids a code element inside the alpha section because that is the regrowth shape twice observed, and both were proved red before being trusted. two stale copies this ticket did not name are repointed at the derived header instead of restating a number, and ADR 0066's own amendment turns out to name four things while listing five, which git show against the pre-amendment line settles as five (#1676)
c1a195b9 · dbf3dbe6…diff - the mis-seal question gets a verb, and the reason it could not have one turns out to be that the decision existed three times: visibility_for, is_published and public_by_fallthrough were three near-identical loops over the attribute rules, so there was nowhere to hang an explanation off and the rule against writing a second matcher was impossible to honour until they collapsed into one Attributes::first_match. That collapse, not the line numbers, is this change - the line numbers were already tracked since #1329 for MalformedRule, including the convention of counting blanks and comments so a number matches what an editor shows. loot explain now names the .lootattributes line that decided a path, lists what it passed over so first-match-wins is visible rather than asserted, reports which .lootignore line covered it, and answers for a path with no file on disk, which is the mis-seal question asked before the mistake rather than after. Because explain and the real decision are projections of one traversal they cannot drift, and that is pinned twice rather than assumed: one test deletes the line explain named and asserts the real visibility moves, then deletes each line explain called skipped and asserts it does not, hardcoding nothing about which rule should win; the other runs a real capture and asserts that for every recorded path the sealed tier equals explain's token, with the ignored file absent and a positive control that the sealed set is non-empty. Eight breakages were each proved red before the tests were trusted, including giving explain its own matcher while the decision kept the old one. And loot explain . would have printed a fully furnished answer about a path that is not one, because ** matches the empty string in this glob dialect and the door rebases a bare dot at the repo root to the empty relative path, so in any repo carrying the catch-all ADR 0038 says every real repo wants it would have named a tier and a line for nothing at all - refused at the door now, with a control showing the rules really would have answered. Found by running it rather than reading it. Five censuses needed updating where the plan named two, and the two nobody would predict are the README verb block, which is a docs file failing in the bin suite, and the derived impl-width sentence in verbs/mod.rs, because adding a single inherent method to Workspace reddens a doc comment (#1521)
ebd3285f · dbf3dbe6…diff - user-declared gates arrive as data rather than as hooks, and the decision the ticket owed splits rather than picking one of its three options, because the gate vocabulary is two halves with different properties. The mechanism moves down into loot-cli, since its value is concentrated in the fake - a FakeSpawner that panics on an unstubbed step is what turns no process spawned from a claim into a failure, and two fakes drift with a fake that has quietly stopped intercepting being indistinguishable from one that works; loot-first keeps a re-export shim so no orchestrator caller changed. The phase axis is duplicated deliberately because it is not the same axis: loot-first asks about two trees, one of which only a land produces, while the CLI asks about two commitments, and a shared enum would carry four variants of which each consumer must refuse two - a rule nothing checks, where two small enums are checked by the type system. A new crate was rejected on arithmetic rather than taste, since loot-first already depends on loot-cli and there is no pair needing a bridge; loot-core was rejected because only half of #1682's criterion transfers, and that half is now a census asserting the engine spawns nothing rather than a sentence. A gate runs after the capture and can never prevent it: it does not decide whether the tree is recorded, only whether it is signed or sent, so a refusal costs the signature and nothing else, which is the ordering #174 already chose. On the confidentiality boundary the absolute form of the criterion is unachievable by any mechanism that runs a command in the working tree, and saying so is better than a strong claim that will not hold - loot surface writes and never deletes, so a path whose grant lapsed stays on disk as stale plaintext readable by cat, grep, the editor and every process running as the operator. The guarantee is therefore the narrow checkable one, that a gate is granted no read the operator does not already have, held up by four facts each of which a regression would have to break first: the runner never sees a Workspace, nothing is materialized, a Step carries program args and cwd with no environment channel, and there is no shell. A fifth fact answers a different attack - the declaration is untracked, so a pull cannot write one, which is git's own reason for never transporting hooks and matters more here since ADR 0075 has the forge holding proposals. This ticket also understated the surface that already exists: loot resolve --tool hands a user command decrypted plaintext today via LOOT_BASE and its siblings, so gates are not the first such door but they are the one that opens none (#1538)
44d4869a · 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 - loot lane new stops paying the whole lane board to print the one row whose id it already holds: lane_views filtered after lane_statuses had finished sweeping, and that sweep does a full DagRepo::load_from, a read_tree_at over a FOREIGN working root and an owner read per registered lane, so a spawn cost N graph loads and N whole-tree walks and then discarded all but one. It now resolves the spawned id directly through a new Workspace::lane_status, which reaches read_lane_entry - literally the per-id call list_lane_entries loops - and shares one row derivation with the board through two extracted helpers, so the two answers cannot drift. tree_walks goes N to 1 at every size measured (3 to 1, 7 to 1, 13 to 1), and policy_loads tracks it identically because both are one per read_tree_at; the spawn path itself contributes zero walks, which is why the before was exactly N rather than N plus something. The pin asserts CONSTANCY across three registry sizes rather than any numeric target, because a literal is the wrong shape for a claim about a slope, and it carries two controls rather than one for a reason that was found by experiment and not by taste: blinding the fixture so it registers nothing fires the first control, but deleting that control then fails on the second - the registry did not grow across the sizes, so the slope is unmeasured - and a REGISTRY edited to equal sizes passes the first control honestly because each such fixture is truthful about itself. lane_views is deleted rather than left with a dead keep parameter, since lanes was its only remaining caller and that verb's question genuinely is the whole registry, so it keeps the full sweep deliberately. The comment above the call said this was a cheap side-effect-free registry read with no format-gated cost to avoid, which was half true and wholly misleading: side-effect-free it was, cheap it was not, and heads.rs has recorded the same sweep at 4.2 seconds for some time. The replacement quotes the old sentence and refutes it, and the cost argument is hung on Workspace::lane_statuses itself rather than on this one call site, because that is the symbol the next caller with this defect will reach for. find_lane and ensure_lane_name_free do filter after list_lane_entries and are left alone: that is a registry-only read with no graph load and no tree walk - it is what the old comment described, just not what it sat beside - and both are whole-set questions anyway. The land's gate reads flat here and is structurally incapable of anything else, since loot-perf links loot-core and never loot-cli, WORK_COUNTERS is ObjectGets and ObjectDiskReads only, and the_excluded_counters_are_zero_on_this_tier pins TreeWalks at a structural zero - so the pin lives in loot-cli's own tests and a second gate reading would have been theatre against an unchanged binary (#1705)
49b0799a · dbf3dbe6…diff - a contributor can offer work to a repo they do not own: loot propose over a three-path family - posture, propose and withdraw - and none of them is the ref-moving endpoint, which stays single because ADR 0075 refused putting the contribution path one flag away from the path that advances someone's tip. The bytes ride /stow unchanged, since the forge's stow literally calls the same push, so the batches are byte-identical and there is no new transport. One premise had to be WIDENED and the reason is a real replay: the ticket says sign the claim exactly as the head declaration does, but a head declaration names no repo and does not have to, because require_pusher catches a replay at another namespace - while a proposal is BY DEFINITION a write by a non-owner, so every repo with an open door admits the same signer, and a tip-and-base-only envelope is replayable by any observer at every one of them, in the contributor's name. The claim therefore carries the repo path and the server compares it against the URL's namespace, proved by RUNNING the replay and watching the second namespace admit it rather than by arguing it. The disclosure is an ORDERING rule and the posture route exists only to make it one: a client that learned metadata_public from the act's own answer could only ever speak afterwards, so posture comes first, the disclosure is printed second, and only then the ref read, the stow and the claim. It is pinned twice - once by a transport that records every call into the SAME trace as the output lines, so the assertion compares positions rather than presence, and once by byte offsets in real CLI stdout against a real forge - and a shut door still discloses first and sends nothing at all. Withdrawal is the tip change's AUTHOR and only them, where visibility admits three readers, because seeing is not acting and the carrier's remedies are the author's withdrawal or the owner's decline; it does not consult the door, since a shut door must not strand what is already offered, and absent and concealed refuse in byte-identical sentences. The lane refusal keeps every one of its old assertions, including that nothing reaches the forge before it fires, and gains the sentence naming the alternative. Twelve breaks were each run and read before being restored byte-identical, and the ones worth naming are the replay above, deleting the author check so a presenter can withdraw work they did not write, adding the door to the withdraw path so a shut door strands an offer already disclosed, and declaring the tip as a head so the CAS counter moves. The ref-moving path is asserted untouched rather than described as untouched: a proposal leaves generation and head set alone, and the SAME fixture then lands a change the ordinary way and the generation does move, which is the control (#1651)
83c5afa5 · dbf3dbe6…diff - converge retires a superseded head durably instead of only in memory, and the investigation had to come before the fix because the ticket named a symptom whose two halves have different answers. The stray head b0cffc66 is a superseded version under ADR 0032 of change txmplwto, #1042's forge-push work: same parent, 19 minutes earlier than the version that landed, so it is an AMEND SIBLING and not a child, which is why nothing that walks parents ever excluded it. It carries nothing a converge could merge, established three ways rather than asserted - the replacement was the primary's sole head at ops 975 to 979 and is on landed main; own reads 0 over the liveness-filtered walk, and since a head is always in its own exclusive line, own 0 can only mean the head itself is not live; and Liveness::partition puts it in stale, which converge abandons without merging. lane rm and lane gc structurally COULD NOT have taken it, which retires the ticket's own framing: both verbs are the same two steps, delete the lane directory and delete the lane entry, and neither opens the primary's .loot/heads - a lane's own head set lives in the lane's file, which rm removes with the directory, so #532's owner scoping never came into play. The SEATING is not reproduced and that is reported rather than papered over: today's lane new puts one head in and one head out against a copy of the real store, the derive_all_heads fallback produces 213 tips rather than 2, a local edit-and-new leaves one head, and an apply of a superseding bundle drops the predecessor and persists - four hypotheses refuted by running them. The PERSISTENCE is reproduced and is the live bug: converge abandons stale heads in memory, but the only persist on that arm is the settle that runs when the tip MOVES, so when the survivor is already the tip - which is exactly this ticket's orientation, a primary on landed main beside a superseded original - nothing is written and the drop dies with the Workspace. Measured on a metadata copy of the real store, converge printed already on one line, nothing to converge and left the head file at 64 bytes with the stray head intact, so loot log's own advice to run pull was a no-op for this whole class, and converge and heads reported the same head set while disagreeing about it. Removing the persist fails the new test on the REOPEN with both heads back while every assertion above the reopen still passes, which is precisely why this lasted 490 ops. A second and separate hazard was found on the way and is fixed with it: derive_all_heads, the fallback when the head file is absent or empty, excluded only nodes named as a parent, so every amended-then-landed original was a permanent derived tip - verbatim what the artifact's own doc says must not happen, and the case #1135 refused for a MALFORMED file while the empty one still fell through. Predecessors are covered now, and on a copy of the real store with the head file removed it derives 31 tips where it derived 213. The reap decision is that lane rm and lane gc should NOT grow head reaping: the head was never theirs, a lane's finalized head is a signed change already in the shared store, and stacking an irreversible head deletion behind an irreversible directory delete is two irreversibles - a head may be retired automatically only under a predicate proving it is not the sole reference to anything live, and that predicate's consumer is converge, which also holds the merge base, the conflict classifier and undo, none of which lane gc has. Neither change can destroy work: abandon retires a head ENTRY, the graph is written as a union with disk, and gc roots from that file - after the fixed converge the head file went 64 bytes to 32 and the graph stayed 41,598,284 bytes with the change still present. The ticket's claim that loot log exits 255 is stale and is recorded as such: it exits 0 today and renders both heads (#1477)
8448e586 · 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 - the dirty question stops reading every file to answer one boolean, and the two clocks it depends on are not the same clock: a stat cache lands at .loot/<position>/stat-cache as a PositionOwned artifact the store treats as opaque bytes, so nothing derived from it reaches the graph, a manifest, a bundle, the wire or any peer - its version header is a local self-check whose only failure mode is read as no cache, which is the opposite of a compatibility break since an older loot does the full read and gets the same answer. An entry is trusted only when size and mtime match AND mtime is older than the walk's own stamp, so a write inside the walk's tick is always re-read and no granularity is assumed on any platform. What that missed on the first attempt is that the stamp comes from the PROCESS clock and every mtime from the FILESYSTEM clock, which on Windows are the precise and coarse system times about 15.6 ms apart - warm scans hit nothing at all, because the cache file's own mtime read as older than the stamp it carried. A one-second margin fixes it in the safe direction and is self-healing, and gross skew is caught separately by discarding a table whose file is older than the stamp it declares. loot lanes reads a foreign lane's cache and deliberately never writes one: the write would have been correct, and it was refused on a different ground, that ticket-to-lane's runbook promises this verb is read-only and agents run it against live lanes to read the claim board. So TreeScan has two named doors, read for a position walking its own tree and peek for one walking someone else's, and which a seam takes is spelled at the call site rather than inferred from the store it was handed. The cost of that refusal is named rather than hidden: with lanes warm, which is the steady state since every lane's session runs status, it is 13.6 to 16.8 percent against the 16.1 the write bought, and with every lane cold it falls to 4.5, which is not the cache at all but the seam now hashing each plaintext once where it hashed twice. status --porcelain is 24.8 percent. And the pin that #1536 asked for cannot do the job it was asked for: delete the cache and get the same answer passes on every answer assertion even when the cache is never consulted, because an inert cache answers correctly by doing all the work - what distinguishes them is counting hits and reads, whose cold zero-and-N is its own positive control, and a poisoned row that keeps size and mtime, carries a wrong digest over a genuinely clean tree, and must answer dirty. Three of this ticket's premises were also wrong: the 46 ms status baseline was measured where status never walks the tree, since with no working change it returns at is_clean, so the gap over the control was never the tree read; the byte read is a quarter of what holds these verbs up rather than the whole of it; and human status on a dirty tree cannot benefit at all, because rendering a per-path delta needs the plaintext (#1669)
96d922e8 · dbf3dbe6…diff - loot merge <head> is converge restricted to one head rather than a second fold: merge_head and converge_heads both reach the graph through one private fold_onto, so the three-way content merge is the ADR 0044 seam unchanged, the conflict record is the one loot conflicts and loot resolve already read, the merge node carries a minted mechanical subject naming the head it folded, and loot undo steps it back through an op recorded off the merge node rather than off an outcome map that can be empty. What it deliberately does not do is retire anything: converge drops superseded heads on the way past, and a retirement is a change to a head the operator did not name, which is the whole of what separates this verb from the one it restricts. A flat head is refused rather than silently skipped, because naming a divergent co-version does not make divergence content-mergeable. Two premises of the ticket were wrong. The un-described-change refusal does not apply as it does to the other merges: that refusal belongs to adopt, raised inside fold_line_in, which captures and signs its own merge parent, while converge never reaches it at all because capture-first defers on any working change one branch earlier - so this verb inherits the deferral, and the refusal is unreachable by construction rather than reimplemented. And loot heads resolves no selector, it only lists, so the spelling merge takes comes from loot abandon --head, which routes through resolve_selector. A third premise is narrower: conflicts are recorded and nothing proceeds, but a single named head has no loop to stop, so that criterion is merge_tips behaviour unchanged rather than anything this change makes true. One inherited shape is named rather than altered - a deferred merge --porcelain prints nothing and exits 0, exactly as a deferred converge does, because a deferral rides the human field while the machine contract is verdict rows (#1505)
6107561a · dbf3dbe6…diff - loot count-objects lands, and the constraint it is built under is that its number has to be the number gc acts on rather than a second opinion about it: the unreachable count and the reclaim figure are gc OWN pruner and sweeper called with dry_run, over a keep set extracted out of gc as DagRepo::gc_keep_set the way 1505 pulled fold_onto out of converge, so a census that disagrees with a collection is not a thing that can be written, and the pin measures a store, really collects it, then asserts the two halves and the sum all match. One premise of the ticket was wrong in the direction that saved work: nothing reported how large a store is as a VERB, but the totals half was already factored, since RepoStore::objects_size has been the perf record load-bearing counter since 632, so this adds no third walk of the object directory at all, it adds a caller. The extraction surfaced something gc had been getting away with: its keep set was position-dependent, because the primary own working change is the one root in neither the shared graph file nor the lane registry and verify has been adding it by hand since 1309. gc could not notice, since it refuses from anywhere but the primary, where those oids ride in the loaded graph anyway; a read-only census can run from a lane, so the root moved into the shared function and the answer stopped depending on who asks. Machine output is this verb product rather than a garnish, which is why the porcelain is one row per metric keyed by a leading mark instead of one wide row: ADR 0023 makes a column unaddable, and a census is the shape most likely to be asked for one more number. The anti-vacuity pin is the same fixture reading 1 and then 0 with nothing changed but a change naming the orphan, and the read-only pin fingerprints every file under .loot rather than trusting the op log, which would never have shown a pruned object. On this lane own store it reads 7910 objects at 146.7 MiB with 2330 of them unreachable at 36.9 MiB, which is the 1524 baseline (#1523)
f10ba1d9 · dbf3dbe6…diff - loot restore grows --source and it is the same verb with a different base rather than a second reader or a second writer: Workspace::delta_against takes the base mirrored_delta used to hardcode, so the disclosure gate, the four-kind absence ladder and the refuse-whole rule are inherited rather than restated, and what the seam hands back is now the base TREE rather than its key set because a caller re-sealing restored content has to be able to say what the base recorded it as. AC3 is plan-then-apply: every action is materialized from verdicts that were all decided above the refusal gate, so an openable path named FIRST beside an unopenable one is not written, which is the assertion a write-as-you-go loop would fail and the pin names it in that order. Three things are decided rather than inherited. A path the SOURCE does not record is reported and its local copy kept unless --delete-absent is passed - refused without --source, where it could only be a silent no-op wearing a destructive word - but a path in neither the source nor the working tree still refuses, because that is a typo and #67 says a typo must not exit zero looking clean, and AC4s deletion half presupposes a local copy to delete. The loss sentence is a different sentence: what a --source write replaces is recorded content plus whatever uncaptured edit sat on top of it, so #1498s nothing ever recorded them would tell an operator they had lost something loot cat prints in one command. And AC6 is a sentence rather than a behaviour, which is the ticket premise that turned out wrong in the useful direction: restore writes plaintext and seals nothing, so the next capture already derives the tier from todays .lootattributes and there was no publication decision to get backwards - what was missing was the report saying so, per path and on the dry side too. Two further premises were wrong. The raw --dry-run scan #1612 replaced was correct only by a coincidence this ticket expired, since --source is the verbs first valued flag and loot restore --source --dry-run a.rs now reads --dry-run as the selectors value. And the shared recoverability sentence opened in the past tense, so a dry run printed running this without --dry-run REPLACES ... what those paths held on disk is gone - found by running the shipped binary, not by review, and the tense now lives in the real runs frame where it is true (#1499)
c193cf35 · 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 - every conflict-stopping verb gets a named way out, and building it corrected the ticket in both directions rather than just adding flags: merge does not stop on a conflict at all, since merge_tips records the triples and fold_onto then signs the merge node and materializes the merged tree, so an abort there is a view step off signed history rather than the forgetting of a stop; while cherry-pick, revert and squash do the opposite and return before writing anything of their own, so their stop leaves even the paths that merged cleanly unapplied. the five named verbs are also not the whole set: converge, pull, adopt and ferrys carry bounce reach the same fold and the same ingest, and the stale-disk settle files a conflict on the way past any capturing verb with no operation behind it at all, so the covered set is now a census rather than an assumption. the state is one position-owned artifact at .loot/position/in-progress carrying the verb, its operand, and the op-log index the view sat at before the verb ran, declared Refuse on malformed content because a torn record read as absent answers nothing to abort over a tree that really is mid-operation, which is the single failure the record exists to prevent. it is a real artifact rather than a derivation because conflicts-are-non-empty-and-the-newest-op-is-a-stopping-verb answers no the instant the last resolve lands, which is exactly when continue must still work. nothing here re-implements a rollback: the inventory row is Captured at ordinal 7, so abort is that one index handed to op restore, which already restores the view, re-materializes the tree, refuses over edits made after the stop, and appends the compensating operation the op-log criterion asked for; force is declared on the five verbs and refused without abort. continue closes the stop rather than replaying it, because the re-run was built first and measured to reproduce the identical conflict: these verbs are capture-first and a resolve on the pre-dock home position never writes the tree, so the closure states instead that the operation applied nothing. status and conflicts name the operation through one derivation, and the pin asserts the identical line in both (#1506)
21b9faf1 · dbf3dbe6…diff - three copies of one false sentence collapse into one statement the other five are pinned to: the help for cherry-pick and revert said --continue re-runs the operation while squash, apply and merge said it closes the stop three lines away, so the shipped help disagreed with ADR 0080 section 4, with resume::cont, and with itself, on exactly the two verbs whose re-run was built first and measured wrong. USAGE is a literal and there is no const concatenation, so the fact now lives once as resume::USAGE_NOTE and a new census asserts all five USAGE lines carry those bytes verbatim, which means a reword turns five red together where three hand-typed copies turned none; the comments in pick.rs and in_progress.rs now point at that statement instead of paraphrasing it. ere.rs cited the help of loot grep for a claim the help never made, so the sentence exists now as ere::HELP_NOTE and is pinned the same way: a dot matches one BYTE, and [:alpha:], \d, \b and \s are not implemented. the help for restore asserted that nothing ever recorded those bytes of every run, while the module it documents splits IRRECOVERABLE from OVERWRITTEN precisely because that is false under --source. item 5 was wrong twice: a stop cannot be the FIRST operation of a repo, because loot init records a genesis floor op, and the refusal is not an off-by-one either, since op_index answers 0 for an empty log, no operation 0 exists to hold a captured view, and the pre-operation pointer bytes were never snapshotted, so no other number reaches them. what is true is that .loot/ops is lane-owned and nothing plants a floor in a spawned lane, making the unabortable case an ordinary one: the first view-changing command an agent runs inside a fresh lane. so the refusal stands, the message now says that and names --continue, and it is pinned through a really spawned lane whose empty op log is the control. the ATTACHED census control is now derived from the census rather than kept as a second hand-written list, and resolves each censused group name to its canonical const so an empty group of the same name cannot satisfy it vacuously, proven by a mutation that leaves the census green and turns the control red on grep, the one attached leaf that had no control at all (#1799)
51aea3d1 · dbf3dbe6…diff - loot config grows a second scope and the local one is untracked because of where it sits rather than because a rule says so: .loot/config.local is skipped by the tree walk at name == DOT, ahead of every ignore call, which is a stronger guarantee than the obvious alternative - a root-level file plus a .lootignore line could not have held, since .lootignore is itself tracked and a fresh loot init writes none, so the first status in every new repo would capture a per-operator default identity as internal, and the dialect has no negation to walk it back with. verified rather than argued: the same content under .loot/ is invisible to status while at the repo root it lands as internal on the very next command. precedence is one function and the merged listing is defined through it, so the file a row names and the value that --get prints cannot disagree; --get exits non-zero on an unset key, pinned through the shipped binary because a library Result proves nothing about what a shell branches on, and a flag-shaped --get value is refused rather than looked up, since the door hands a valued flag whatever follows it. bare set and unset write GLOBAL, inverting git deliberately: loot config set identity is the route init own refusal advertises as one name across every repo on this machine. the settings are read where they matter - remote is consulted by resolve_remote, so a bare push, pull, grants, pull-grants or propose now contacts what the repo names, with origin still the answer when nothing says otherwise. two of the ticket premises were wrong. identity cannot do what it asks: a repo identity is already repo-local in .loot/identity, a config key that overrode it would manufacture the exact name-to-key drift doctor checks for, and loot init can never read a local answer at all because the file lives inside the .loot it is about to create - so the live case is clone run from inside a repo, which is what the pin asserts against a repo configuring nothing as its control. and the local file is not a store artifact: ON_MALFORMED has nothing to choose between, because the key = value dialect cannot produce an undecodable value - the real hazard was an unreadable file reading as absent and falling through to the other scope, which Layer::load now refuses outright. one argv finding rode along: config is the first family whose flags may precede its subcommand word, and args[0] was wrong in both directions, so the leaf now resolves from the first token that is neither a flag nor a flag value (#1507)
2c12d0f2 · dbf3dbe6…diff - nearest-landmark naming lands, and the one real trap in this ticket is that distance is not well defined on a DAG: the number printed is the count of changes reachable from the selector and not from the landmark, which is what git rev-list counts for landmark..selector, and it is not a path length - pinned over a diamond where one input reads 5 as a count, 3 as the shortest path and 4 as the longest, because a straight-line fixture cannot tell the three apart and would have pinned nothing. the briefing had this backwards: git describe does not count the longest path, its number is the cardinality of the same set difference, so taking the count here agrees with git rather than departing from it. a shortest path would call you 2 changes past a landmark that fifty changes have landed on top of, and a count is the size of a set, so nothing in it depends on a traversal order or on a clock - which matters on a lane where authored_at is advisory and never an ordering input. the candidate set is deliberately not the one loot buoy resolves: buoy keeps only the maximal attested change per name, so running nearest over that answer would report no landmark for every change below the tip of a name that has moved, when an older v1.0 is exactly what a change under the newer one comes after - so attested applies the same three ADR 0025 filters across every non-reserved role and skips the maximality reduction, and that split is pinned by asserting both halves in one test. ties are refused rather than broken: two landmark NAMES at the minimum distance exit 3 and are named, the ground ADR 0025 already refuses an ambiguous buoy on, while one name on two tied changes is not ambiguous at all, since every tied change renders the identical string and there is nothing to pick between. three things were widened rather than copied - the ancestor walk now has one home in buoy::ancestors, reached by the maximality reduction, by the new distance and by Workspace::ancestors_of, where there were about to be three walks; the three attestation filters are one predicate; and --dirty asks the same reflected-tree question the converge chokepoint and bisect start already ask. --nearest is the first valued flag this verb has ever had, so the door hands it the token that follows and loot buoy --nearest --dirty would have resolved a selector spelled --dirty, silently dropping the marker: refused by name, the remedy config --get landed, with the door handing the flag over asserted as the control. no landmark anywhere in the ancestry falls back to the short version id and still exits 0, because an answer was produced rather than withheld. (#1509)
b7b0b031 · dbf3dbe6…diff - loot apply-patch lands as the inbound half of patch interchange, and the limit 1512 deliberately left open is CLOSED BY REFUSING: CRLF is refused on both sides, because the two halves are not the same kind of problem. A patch declaring lossy: crlf-normalized cannot reconstruct the bytes the change it names recorded - that is fidelity, read off the exporters own measurement rather than re-derived, since by the time the content is in the file it is LF. The half that actually bites is the LOCAL one: a hunks line space is LF, so writing an applied result back over a CRLF working file rewrites EVERY line ending in it, a whole-file change no hunk describes, that no report would show as more than the lines the patch touched, and that the next capture would sign. Reconstruction was rejected as a guess - the line space carries no endings at all, so an inserted line in a mixed file has nothing to decide from - and documentation as too weak for a silent whole-file rewrite. The cost is named rather than hidden: a CRLF repo cannot use patch interchange in either direction, and the remedy is a bundle, which carries content verbatim because it carries objects. Refusing is also what makes the round-trip pin true rather than approximately true. This is a separate verb from loot apply and not a mode of it, since a bundle carries objects, keys and signed change nodes while a patch carries none of those; dispatch is an exact-name lookup so nothing can collide there, and the place that DOES match loosely is verb_usage, which compares the token after loot rather than a prefix - pinned, because a prefix match would have dragged the whole apply-patch usage line into loot apply --help. Hunks apply strictly at the lines they declare, and what does not falls back to the ADR 0044 three-way against the change the patch names in its own parent row, which is the git am -3 shape and the reason that row is full width; loot-core gains one narrow door, converge::compose, because the theirs side of a patch is text at NO address while three_way_merge exists to pick between two addresses. A contended path is recorded and the run stops with the tree untouched, which makes this the sixth member of the 1506 resume family, and the census is what said so. Three of the ticket premises moved under it. The AC3 re-seal under the local .lootattributes is a SENTENCE and not a step, exactly as 1499 found for restore: the verb writes plaintext and then captures, so the tier is todays local policy by construction, and the export side already carries no tier for it to have inherited - the work was pinning it and saying it in the report. The AC4 no-partial-application rule forced the capture-first snapshot to run AFTER the plan rather than before, found by a fixture rather than reasoned out: a repo holding a path it cannot open has that paths plaintext on disk, capture_uncaptured_edits refuses to re-seal content it cannot read, and with the capture first the verbs own refusal was never reached at all. And a hunk body must be consumed by the counts in its @@ header and never by scanning for the closing separator, because a deleted line whose content is exactly a dash renders as two dashes, byte-identical to it. ADR 0082 freezes the header grammar now that a reader exists, which is why the 1786 fidelity rows went in before this ticket rather than after; the machine shape 1512 expected this ticket to name is deferred, with the reason written into ADR 0066 and PROSE_ONLY_CEILING, because it is one shape shared by both verbs and freezing the import side alone is the same half-contract 1512 declined on its own side (#1513)
d5fba81e · dbf3dbe6…diff - loot fetch lands as the first half of pull by call rather than by imitation: the negotiate-fetch-apply loop comes out of pull_via_declaring as Workspace::fetch_via, and pull now runs capture-first, that call, then the fold, so the two verbs cannot drift about what arriving means - the same extraction merge made of fold_onto. the custody routing comes out of pull_routed the same way, so a locked fetch takes the exact fork pull takes, relay unsigned and forge refused by name, and fetch becomes the second caller of the sealing door, which the custody gate now enumerates by name instead of counting to one. safe on a dirty tree is structural rather than guarded, because capture-first sits above the extracted half, and it is pinned by byte-comparing status porcelain across the call with a modified tracked file in the fixture - the live version id is what moves, and a capture-first mutation reddens it while the path list stays identical. no re-fetch is measured rather than asserted: the test relay now records the raw wants count per round, a later pull is required to ask for zero addresses, and the non-zero ask the fetch itself made is the positive control - an ingest-free mutation makes that number read 2. what integrating would involve is read off the head partition converge executes, through a shared converge_partition, so fast-forward means no merge node rather than one head. two premises of the ticket are wrong. a subsequent converge or pull completes the GRAPH integration but not the working tree when the repo was strictly behind, because converge_heads returns before materializing on a single head and the auto-surface of pull fires only on a non-empty apply, so the content lands in the graph and never on disk while the pull prints nothing new - the same un-materialized state a no-surface pull and a locked pull leave, and fetch now prints that and names loot rehome first. and --remote is the first valued flag of six verbs rather than of this one, so loot fetch --remote --json resolved a remote named --json; the refusal went into remote_sel, where push, pull, propose, grants and pull-grants all reach it (#1514)
577ae796 · dbf3dbe6…diff - loot clean lands, and the two nevers the ticket asks for are properties of how the candidate set is BUILT rather than filters over one that already exists: the protected set is the union over every change tree in the graph, consulted inside the keep predicate of the tree walk itself, so a recorded path never enters a list at all, and nothing under .loot can be reached because that same walk already skips it at name == DOT ahead of every rule, which is why this file adds no second check free to drift from the first. the walk grew a WalkFilter trait so that skip stayed one line with two implementations rather than a second walk beside it. AC6 is the one that eats work and it has its own fixture: a delta compares path-and-address pairs, so a file the graph records which currently holds an uncaptured edit reads as absent and gets deleted - which is why the set is keyed by PATH and drawn from ANY change rather than the current one. a second fixture pins the other half of that word: a path only the FIRST of two changes records, which a set read off the tip or off surface_target calls unrecorded. both were run red by named mutations before being left green, and deleting the DOT skip put twenty three .loot/objects entries in the candidate list, so the pin is not vacuous. the default is a listing and --force is the only thing that deletes; -n is accepted as the explicit spelling of that default rather than a fourth spelling of the preview idea 1809 is open about, and -n --force is refused because one of the two readings deletes files. -x and -X choose which side of .lootignore the run acts on, and the rules are read by the same parser a capture uses so first-match-wins and the absence of negation hold; -e is repeatable through a new FlagSpec::values that value now delegates to, is parsed by that same parser, holds under -x the way git keeps its own, refuses a leading bang because unlike an inherited ignore file the operator is authoring it right now, and refuses a value spelled like a flag because a valued flag swallowing the bare flag after it has been the defect five times this run. the verb is prose-only and the ceiling moved to 55 with its reason beside it: the shape worth freezing is shared with loot view --prune, which has none, and the column that matters is the per-path spared-reason neither remover computes yet (#1515)
c0b301ed · dbf3dbe6…diff - ahead and behind against a remote without pushing to find out, and the thesis constraint is met as a property of the REQUEST rather than as advice: the question carries an EMPTY PAYLOAD and every comparison is local. a relay is asked POST /haves with a zero length body, a forge POST /ref with a signed envelope over zero bytes, so a repo of one path emits BYTE IDENTICAL bytes to a repo of ten thousand and the question has no room to encode paths, object addresses, or even our own heads. the two endpoints that could are forbidden on this path and ADR 0021 now records why: /wants sends our object addresses, which are per-content identifiers, so a status in a loop hands the relay a per-path edit-frequency profile of ciphertext it cannot open - ADR 0083 refusal at a higher call rate - and /offer sends our head ids, which a push may do because a push is a CHOSEN act and status is not. the pin asserts the recorded path-and-body list SET EQUAL to exactly /info and /haves both empty, asserts the two recordings equal across two very different positions, and carries a positive control that a real push through the same stub records a NON-EMPTY body; mutated into the rejected design it goes red with /wants carrying a literal 32 byte address. offline is split POSITIVELY, the site gate SKIPPED-OFFLINE rule: only reqwest own is_connect and is_timeout may be called offline, and anything that ARRIVED - a refusal, a 404, a proxy page, a truncated body - is unusable, because those are different facts and collapsing them is how a guess gets reported as a measurement. the relay answered and holds nothing is a THIRD thing and reads as declared with a count. every unknown count renders dash or null and NEVER zero, so a machine that never reached the network cannot emit a number, and log --unpushed REFUSES rather than printing an empty listing, because a listing has no row meaning I could not ask and an empty one reads as everything is pushed. the asymmetry is stated rather than faked: unpushed is exact, being the same change lane a push would send through the same ancestor_closure, while unpulled is zero exactly when every declared head is held here and UNKNOWN otherwise, since a head declaration says what the tips are and not how deep they run. no FORMAT_MAJOR bump and the reason is recorded: ADR 0019 marker exists to prevent misparse of a durable or on-wire artifact, an unasked status emits byte identical porcelain, and the R row lives behind a flag that did not exist when the shape froze. no revset predicate either, because loot-revset is handed a GraphView and a KeyOracle and nothing else (#868), so a predicate answering over HTTP would put a network round trip inside revset::select and therefore inside grep and format-patch too (#1522)
2237a331 · dbf3dbe6…diff - 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 - shallow clone lands with the cut on the RECEIVING side, and that is why no wire moves and no format major does either: the fetch body is a format-marked pair of oid runs, so a depth field would be a new wire shape and therefore a bump, where taking the whole change lane and KEEPING n generations needs no new field, no new endpoint and no server change - a shallow client works against every relay and forge already deployed, including older ones. the price is stated rather than glossed: the change lane metadata crosses once in full on the first round, and what is saved is the object bodies, which is where a history bytes are, exact from the second round onward. the no-false-absence guard lives in THREE places and none of them is a verb - assemble, which every CLI open lands in, measures the frontier; apply_bundle_reaching, the only thing that can move one, re-measures it; and the dispatcher states it on stderr after BOTH the success and the refusal arm, because a refusal is a false absence WORST shape. it cannot be bypassed by a verb that forgets to ask: there is no path from the CLI to history that skips assemble, and the one way to open without measuring is to declare RepoNeed without graph, which makes the first history read PANIC - so the declaration that would silence the notice is the same one that aborts the verb. it rides stderr rather than the shape, so json and porcelain stay byte identical under ADR 0023, and a complete position emits nothing at all. depth never reaches the remote AT ALL, pinned three ways: every recorded request re-encoded through the real codec is exactly header plus 32 bytes per id with no room for a depth or a path, the union of every have and wants is a SUBSET of what the relay itself named in a prior answer, and the aimed one is that two positions cloning the same history at the same depth and differing ONLY in their sparse view emit BYTE IDENTICAL requests. the test relay recorder had to start capturing IDS rather than counts, because a privacy claim about a request cannot be checked against a length. two findings came out of the sweep rather than the design. one mutation stayed GREEN and refuted a claim already written into four files - that shallowness is stable because the frontier id rides the declared closure - since a declared have IS a closure claim and the held tips therefore already subtract everything behind the cut; every occurrence is now the narrow true sentence with the refutation beside it. and a count assertion caught a silent no-op: the obvious deepen posture, the closure minus the frontier, comes back with an EMPTY change lane REPORTING SUCCESS, because the remaining ids are still descendants of the cut - a deepen must declare NOTHING, and the posture is now derived from the bound so the wrong pair cannot be spelled. the body-deferring filter is NOT attempted and is the one criterion left: it needs a lazy object read on every get, surface and diff path plus a policy for what happens offline, and half-building it would put a FIFTH kind of not-here into a store that already distinguishes four (#1527)
2cccbe27 · dbf3dbe6…diff - bisect grows log, replay and terms, and the replay refuses WHOLE at a gap rather than replaying up to it, because a partial session is INDISTINGUISHABLE FROM A WHOLE ONE IN THE ARTIFACT: status renders both identically, the search then runs from a range missing a bound it was TOLD about, and the only evidence is a warning that has scrolled away. all-or-nothing has no partial result to mistake. the pre-flight resolves every id against the graph before writing anything and names EVERY missing change with the line that asked for it, and on a shallow position it says so and names --deepen rather than pull. SESSION_VERSION stays 1 and that is the shape of the preservation criterion rather than a promise about it: the terms pair is a TRAILING codec section written only when non-default, so a default session is byte identical to what the previous release wrote and a pre-1532 session still decodes. the transcript is an INPUT and what is frozen is its GRAMMAR rather than its emitted text - what the parser accepts today it accepts forever with the same meaning, and later work may add directives rather than re-read existing lines. an unparseable line refuses the whole file and points at the line, which is the rule 1516 set for an unreadable ignore file, and EMPTY or all-comment is refused TOO, because zero directives and a session with no marks are DIFFERENT FACTS and a truncated download is exactly how the first arrives looking like the second. ids are full 64 hex only, no prefixes and no HEAD, because a transcript is read on a machine where neither means what it meant there. the vocabulary TRAVELS with the transcript: a terms line rides ahead of the marks and replay resolves mark words against a RUNNING table, so it round trips, and a transcript written under old and new whose terms line is missing is REFUSED rather than re-read as good and bad - the one reading that would silently FLIP A VERDICT. the rename ADDS a spelling rather than retiring one, so bisect good keeps working afterwards, and that is safe ONLY because a term may not shadow a subcommand name, which is what makes the inversion terms bad good unspellable. two of its own assertions were not AIMED until they were fixed: the start assertion was vacuous because the fixture made start and the transcript bad the same id, repaired by finalizing a sixth change between the log and the replay, and a doc claimed its consequence one assertion earlier than the one that fires. and the tests produced a finding worth keeping: writing the transcript INSIDE the repo makes it an uncaptured edit, so replay refuses over work the operator did not knowingly make - the guard was deliberately NOT widened, since a replay checks a midpoint out and must refuse over the same work bisect start does, so instead the refusal names the likely culprit and the smoke test ASSERTS that refusal rather than arranging to avoid it (#1532)
05f9263e · dbf3dbe6…diff - loot move lands as the sixth member of the REFIT FAMILY rather than as machinery of its own, and the whole re-anchor is ONE DELTA over tree entries: the shift, the new parent tree against the old parent, applied to every node of the moved line - so node N tree becomes old_parent then delta N, which is why DESCENDANTS NEED NO SEPARATE RULE and why the clash set is decidable from trees BEFORE anything is minted. the naming decision is the ticket own proposal taken rather than a third option invented on taste: loot move <selector> --onto <selector>, with no collision to find - loot has no mv, and a rename is something a capture RECORDS since v12 - and it is recorded in CONTEXT.md as a new Move entry that states the decision against the word rebase, which the glossary deliberately avoids. AC6 follows an EXISTING mechanism rather than a new one: split moves an entry whole and duplicate reuses entries entry-for-entry under ADR 0004, so move reuses that shape, and the structural proof is that there is NO put_sealed and NO ObjectStore::get on the move path at all - a re-seal would require acquiring a key the code never asks for. its pin carries a POSITIVE CONTROL, open_object failing before AND after, because without one an entry comparing equal proves nothing. the clash arm is the squash clash copied in shape - record conflicts, persist, op, mint nothing - and move joins STOPPING so it carries the abort, continue and force trio. two things are reported rather than papered over: the ticket parent line calls this the start of the jj rebase -r item but AC2 asks for the OPPOSITE of -r, descendants travelling WITH the change rather than re-parenting onto the old parent, and the AC as WRITTEN is what shipped. and the working change is itself a descendant, so it travels, re-recorded as a superseding version but deliberately UNSIGNED, because signing it would mint permanent history from work nobody named - which also means a clean move reports two descendants where the operator sees one, and that is pinned as true rather than hidden. eleven pins, each reddened by a named mutation with the count read rather than the word ok (#1517)
7ff4ef4d · dbf3dbe6…diff - the branch and reorder forms land as FLAGS ON MOVE rather than as verbs of their own, and the branch form is a SELECTOR SUBSTITUTION rather than a second planner: branch_root walks the first-parent line and stops at the first node the destination already descends from, the change just below it becomes the source, and the ordinary plan_move decides everything else - so the branch form CANNOT DRIFT from the subtree form refusals, clash set or sealed-carry rule, because there is one planner under both. reorder is DELTA COMPOSITION: the moved manifest becomes anchor then lower-over-upper, the overtaken one anchor then lower then upper, which equals its old tree exactly when the two touch disjoint paths - which is WHY descendants are re-parented with their manifests UNTOUCHED - and the intersection of the two deltas is the clash set, decided from trees before anything is minted. a DEFECT IN WHAT LANDED AN HOUR AGO is fixed here rather than filed: loot move <finalized> --onto @ was NOT refused, because @ on a different fork passes every #1517 guard - not self, not a descendant, not the current parent, not a merge - so it would have minted SIGNED HISTORY PARENTED ON THE UNSIGNED WORKING CHANGE, a version no push, bundle or fetch carries. the repair is stated as a rule over the parent-to-child EDGES a restructure creates, asked of the DRAFT POINTER and never the signature, because a keyless repo signs nothing - so both forms and any seventh Refit inherit the refusal instead of each enumerating its own cases. AC1 was already satisfied by #1517 and is CITED rather than re-pinned, verified against move_onto live-descendant closure rather than taken on trust. non-adjacent insertion is REFUSED BY NAME rather than half-built, because inserting at an arbitrary point re-parents the line it lands in front of, which is the -r shape #1836 holds. fourteen pins, each reddened by a named mutation with the count read - including two draft pins that go red together, which is the census proving the rule sits on the edge rather than on the verb. and two prose censuses caught the USAGE text itself, where a -b inside a jj rebase citation tokenized as an undeclared flag (#1518)
066564a1 · dbf3dbe6…diff - the note rule moves to the LANE WRITE SEAM and stops being a property of one verb: Workspace::attest takes a Record rather than a role string, so there is NO OTHER WAY TO SPELL A ROLE IT ACCEPTS, and two wildcard-free matches answer for every variant - role mints the string and refuses an operator one in the reserved namespace, prose declares the free-text payload and Some IS the gate. adding a fifth variant produces TWO compile errors, one per match, which is the obligation put where a list cannot be forgotten. all three doors were REPRODUCED FIRST and a FOURTH was found that no ticket named - tag --retire -m writes the operator reason to the same lane - which is why the repair is a seam rather than three verbs remembering. door one is REACHABLE rather than theoretical: unwrap_or_default meant an unreadable tree read as NO SEALED PATHS and allowed the write, and a shallow position supplies one, because HEAD~n walks one parent edge past the oldest change HELD and resolves onto a FRONTIER ID - signed, named as a parent, not held - so the gate now fails CLOSED and names fetch --deepen. door two leaked real bytes end to end: notes add refused, then attest with a reserved-shape role exited 0 and show printed the secret on a Restricted change. and ADR 0025 had that door WRITTEN DOWN AS AN ACCEPTED COST, saying a user who reaches past the verb can still attest the sentinel and the signature binds it to their key - the hole was documented as the price of the encoding, and it was a DEFECT rather than a cost, because the role parses back as a note, renders as one and rides the bundle as one. tag -m is GATED rather than excepted, and the line is KEY VERSUS PAYLOAD: a landmark name is a namespace key a reader re-types and matches by equality, so gating it would take ADR 0018 sign-off and ADR 0025 resolver down on every change in a repo with one sealed path, while a message, a reason and a note body are prose ABOUT one change content. that refuses tag -m in THIS repo, where .lootattributes seals a path on every change, and the bare name, the bare retirement, buoy and attest all still work. eight mutations with counts read, and mutation three is the argument: neutering the seam reddens three pins while #1519 OWN PIN STAYS GREEN, which is why the first gate could not see any of this (#1847)
93cc4944 · dbf3dbe6…diff - hunk selection lands on split and restore and AC2 is STRUCTURAL RATHER THAN CHECKED: everything the picker shows comes off the ONE DELTA SEAM, whose sides resolve through visible_paths_at - the key check plus embargo escrow plus grant expiry, the same door grep and archive take since #1500 - and the hunk grain has EXACTLY ONE CONSTRUCTION SITE, reachable only where both sides hold bytes, so an unopenable path is an Unreadable row that HAS NO HUNKS IN IT and there is no second visibility test to keep in step. such a path is still OFFERED AS AN OPAQUE WHOLE rather than dropped, because dropping it silently is #67 at the verb where it costs content, and its entry then moves BY ADDRESS with nothing decrypted. the keys are git add -p keys derived from ONE TABLE so parser, prompt and help cannot drift, with two forced divergences stated as such: q ABANDONS THE WHOLE SELECTION, because git keeps what it staged and loot has no index to keep it in, and there is no edit or navigate arm because each would mint a second grouping rule or a screen model. the terminal is a DOOR rather than a check - demand is the only constructor of the only Answers the binary has, so a verb cannot obtain a keyboard without crossing the refusal - and -p with --dry-run refuses as a PAIR, because -p IS the preview and a dry run would ask every question then discard the answers. content that does not survive a line split is offered whole, MEASURED BY ROUND-TRIPPING rather than by enumerating hazards. eighteen mutations, all red with counts read, and AC2 carries a POSITIVE CONTROL that moves the other way: emitting no hunk grain at all leaves the never-a-sealed-hunk half GREEN and reddens nine, which is exactly what that half alone cannot see. ONE ACCEPTANCE CRITERION IS DELIBERATELY NOT BUILT and that is the finding: AC1 names shelve -p, and loot shelve does NOT EXIST - #1502 was closed NOT PLANNED as premature scaffolding, so building a shelf to hang a flag on would reverse a recorded decision sideways from inside a ticket about something else (#1529)
1eb98dc8 · dbf3dbe6…diff - the note rule is a refusal on a LOCAL WRITE and the documents now say so in the first sentence a reader meets, because the claim that it is a property of the LANE was false in the direction nobody had checked: ingest inserts on att.verify alone, so a peer prose about a sealed change is stored and printed while the IDENTICAL TEXT FROM THE LOCAL OPERATOR ON THE IDENTICAL CHANGE IS REFUSED. that was built end to end rather than argued - a peer DagRepo not running the seam, a real sync bundle, apply_bundle, and the secret read back in the clear. gating ingest is REJECTED on four counts and the first two decide it: a drop destroys the local holder ONLY EVIDENCE OF A DISCLOSURE THAT ALREADY HAPPENED, at the one party who can act on it with burn, re-seal or rotate, and the remote author already had the plaintext so refusing un-tells nobody; and it would make the lane contents READER-DEPENDENT, where membership today is a property of the BYTES - a signature verifies or it does not, the same answer at every reader - so one peer would keep what its neighbour drops, the dropper would stop forwarding, and the lane would PARTITION. the third count is that it is not even COMPUTABLE where the ingest happens: the attestation loop runs BEFORE change nodes enter the graph, so a first-delivery record has no tree, and a gate that fails closed would refuse the ORDINARY case. the note codec header carried the sharpest error, since that module is the DECODER and a claim that such records cannot exist is exactly backwards there. and tag partial write is made UNREPRESENTABLE rather than documented: attest_all judges every record - role minted, prose gated - and only THEN records them all and persists once, so an annotated create is TWO RECORDS AND ONE ACT and the pre-check is DELETED rather than marked load-bearing. it had been copied from the notes verb along with the sentence saying deleting it costs a message and never the rule, which is true of a verb that writes ONCE and false of one that writes twice - the refusal now leaves NOTHING behind, not even the landmark, verified end to end at 0 landmarks. four mutations with counts read, and the ingest pin fails on its LOCAL-WRITE CONTROL half when the local gate is neutered, so both directions are live in one test (#1855)
2bc8c51a · 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 catch-up stops leaving its own consequence to a runbook the reader may never have loaded: adopt now prints the git step IT JUST CREATED, and only when it actually moved the tree and the checkout is measurably behind - naming the paths it wrote, which is exactly the set needing the proof. the ancestry is judged on the LOOT GRAPH through the mark map rather than by asking git, because git cannot answer it: the checkout has not fetched the landed commit. there are two arms by position, since which git command is correct depends on where HEAD is, and neither arm ever recommends git before a catch-up, because git-first WEDGES the primary and loot-side-first is and stays the order. and the land closing note is narrowed so its final word about the primary cannot read as being about the checkout: it now says which thing the fast-forward moves, says HEAD does not move with the dock, and names the verb that says the rest. the finding that changed the procedure is bigger than the ticket: the skill had the FETCH AFTER THE PROOFS, and a land pushes main FROM THE MIRROR, while the only fetches a land runs in the checkout are its pre-land drift and ADR guards - so at the exact moment an operator needs this procedure their origin/main is stale by that very land, every landed path reads as differing, and the freshly landed perf point does not resolve at all. followed literally the skill produced the precise wrong conclusion it exists to prevent, so the fetch is hoisted with the reason recorded, and the printed block fetches first by construction. twelve mutations with counts read, of which two are worth the reading. the tenth is a CONTROL: the skill quote wraps across two indented lines, so with the whitespace flattening removed the citation pin reddens over LAYOUT rather than over words, which is what makes the flattening load-bearing rather than decorative. and the fifth is the one to learn from - the lane guard test PASSED WITH THE GUARD DELETED, because the lane was silent for an unrelated reason, landed main having been recorded on the primary so the lane lineage-filtered graph had never seen it and the ancestry answered false. a pass that survives deleting the thing under test is not a pass, so the arrangement was rebuilt with landed main recorded THROUGH the lane and two explicit preconditions proving the lane reaches the checkout and can answer the ancestry at all - after which the same mutation reddens. what is NOT done is said rather than implied: a bare ferry still says nothing about git, its catch-up being a different path, so the note claims only the dock for it; the block deliberately stays silent on a repeat adopt, because its claim is that THIS run wrote over your tree and a no-op cannot make that claim; and none of this was reproduced live (#1679)
c8ebe232 · 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 - one delta derivation had THREE spellings rather than the two the review named, and now has one producer: the surface plan, the uncaptured-paths filter and restore each built the same since-the-surface-target, walk-the-disk delta on its own - and restore doc claimed to share its base with the overwrite guard while nothing enforced it. the single place that picks the delta spec and the walk policy is now one function, and one producer picks the base and hands back base and delta together, with all three callers going through it. the vouching #1710 made into a type is UNTOUCHED: the trusted set still comes only from the live arm after its own successful read, and the producer merely passes it along, which its doc says. the store-memo cost argument is made to SURVIVE the store changing its mind rather than pinned. the bundle memo now holds only a yes-or-no answer per address and never the object, so a future bound or eviction on the store own memo cannot quietly turn it into whole-history ciphertext held for the length of a bundle. pinning the retention instead was refused, because it would fail a perfectly reasonable memory-pressure change for the wrong reason. the one path that would need bytes again now re-fetches them, and no caller reaches it today, since both passes decide to send bytes from the address alone - so if that ever changes, the cost appears as a get the counters can SEE rather than as memory nobody measures, and a new test drives that path directly. the gate reads no move on every gated counter, the probe pinning exactly one get per distinct address stays green, and the method-width tripwire moves by exactly one - where a first draft that added two was caught by the suite and a helper inlined. and the shared base is shown load-bearing rather than asserted: moving it off the surface target reddens ten tests, across the clobber guards, the due-embargo cases, a process-level bisect and the plan test (#1711)
53c0e176 · dbf3dbe6…diff - the opt-in telemetry channel is built, and off means no call because a paired test says so rather than a sentence
loot#1658, building what ADR 0074 specified and loot#1626 shipped only the
guard for. loot telemetry on | off | status | show; an append-only accumulator
that writes one line per dispatched verb, the dispatch-table name, with burn and
maroon collapsed to other AT WRITE TIME so no local trace of an incident exists
either; a sender that runs in main after the verb has printed and decided its
exit code, at most once per 24 hours, behind a 3-second hard timeout, that can
never print, never change the code, and never delay a verb; a spool that IS the
outbox, so show reads the same entry the sender transmits; and a 16-byte id
minted on opt-in and deleted on opt-out, never machine-id.
The guarantee is the pairing. One local listener, the real binary, a sandboxed
config home: init, status, doctor and show with telemetry off make ZERO
connections; on, the same verbs make EXACTLY ONE, and the same verbs again in
the same period make no more; off again makes none. A lone assert-zero passes
when the listener is broken, the binary crashed early, or it pointed at the
wrong port - the exact shape of the logrotate -f check that could never fail.
The endpoint override is https only with the loopback LITERAL the sole
exception, enforced twice, and localhost is refused because what it resolves
to is the resolver business; the refusal happens BEFORE an id is minted, so a
broken override never leaves an identifier that can never be used.
Clause 5 of the field rule is a property of the wire bytes, not a promise:
a test parses every string out of the JSON and requires each to be a
compile-time constant, a member of a fixed array, a dispatch-table name, or
the hex id. The platform is a build-script constant rather than a runtime
assembly from std consts, which is what the rule forbids spelled from constants.
The verb emits machine output - show --json IS the report - so it joins the
emitting tier rather than the prose-only debt list the ceiling forbids growing.
Every count that moved was moved in the documents that state it, because the
censuses read them: 129 verb lines in three sites, 82 dispatched and 80 in the
table and 25 emitting in ADR 0066 with telemetry in the named list, all 82
verbs and the block in the README, the CLI page, and ADR 0074 eight-entry
allowlist - because the endpoint is now the binary ONE destination it contacts
rather than prints, and its ALLOWED row says under what condition.
The destination census flagged three lines that dial nothing: a printed hint
naming http://127.0.0.1 and the two scheme-prefix tests in check_endpoint. They
are respelled rather than exempted - the check splits on the separator and
compares the scheme WORD, same rule, no allowlist row - because a row is a
sentence someone has to be able to defend and these had nothing to defend.
The site half: POST /api/telemetry validates a report against the SAME closed
sets the binary declares, pinned cross-repo by reading the Rust source, and
refuses WHOLE on any field outside them. A valid report becomes source=cli
rows, one per verb invocation under /cli/<verb> plus two bucket rows, all
stamped with the arrival time so no ordering exists to reconstruct; the
telemetry_id rides the row and expires with the 30-day partitions. It sits
behind the same /api/ limit_req the beacon needed, which is why the ADR put it
there. doctor prints on/off, the EFFECTIVE endpoint, last send and the spool.
The notice gains the CLI tier, as ADR 0074 §11 requires: sends nothing unless
you run on, the id links reports and off deletes it, what is never in a report,
show prints it before you decide, and the erasure route - quote your id within
30 days, you hold the key. The test that asserted the page does NOT collect
telemetry is inverted, as it was for the visitor key.
1214 lib and 118 bin tests, 51 suites, 634 site tests, budget and font gates
green, rendered and read.
8a919b01 · dbf3dbe6…diff - lane gc and lane rm name what holds a lane they cannot reap and give a way back for a lane that lost its lane-id. on Windows a held path that is a directory is now reported as held by a file open inside it or by a process whose current directory is inside it, which holds the directory with no file open, a held file is told a process has it open, and both say loot cannot name the holder, point at Resource Monitor, and say to leave the lane alone and re-run; POSIX, where a current directory does not block removal, is told the path is busy. the refusal for a directory without a matching lane-id now names loot lane rm <id> --deregister, a new flag on rm that drops the registry entry and leaves the directory untouched, keeps the ownership guard, and refuses while the directory still carries the matching lane-id, because there the entry is what lets a retried reap finish, which also answers the ticket question about deregistering a held landed lane: no. a new test spawns a real sleeper process whose current directory is a temp lane and went red with the old advice and with the sleeper removed; its POSIX branch asserts the sweep reaps and was not run here. a deregister test went red with the recovery hint, the still-a-lane refusal or the ownership guard removed and with the directory deleted, and a cli test went red with the flag not passed through. the verb tier width moves to the derived count, and CONTEXT.md and the concurrent and issue-tracker runbooks say to leave a held lane alone and name the recovery. the workspace suite is green (#1760)
16968085 · dbf3dbe6…diff - sweep 5 fix-up: the lane usage refusal no longer prints a run of spaces, because the rewrapped literal gets its line continuation back, and the subcommand gate test now asserts that no line of that refusal holds two spaces past its leading indent, which went red on the old text (0 passed, 1 failed). the cli smoke deregister case writes its seed with an escaped newline again and spawns on one line. rm and rm --deregister now share one owner-scoped lookup and one entry removal, and deregister drops a redundant exists check; disabling the shared ownership guard turned both the rm and the deregister test red (1 passed, 2 failed), and the verb tier width moves to the derived count. the census lexer reads a C-string raw literal, cr with or without hashes, as raw, with a fixture that failed first on a brace count that did not close (3 passed, 1 failed). ADR 0035 gains a dated amendment for --deregister and for keeping the entry of a held lane, the verb tier doc stops narrating per-ticket widenings as a decision beside the #1922 tripwire one, and the proposal loop argument in propose.rs and ADR 0075 is stated as a rule about any route that calls record_proposal or close_proposal instead of naming the routes. doc fixes: the LANE_BARE doc points at the union test, remove_lane says --reap-others also authorizes --deregister, the Resource Monitor advice is hedged, store.rs loses a method count and corrects its upsert heading, ADR 0075 says the old read would have been refused, and rewrap leftovers are reflowed. the workspace suite is green, and the loot-forge suite is green against a throwaway cluster with the CI env block (#1968)
15bcde31 · dbf3dbe6…diff - loot resolve now refuses, naming the path, when the bytes on disk at the path it resolves differ both from the bytes recorded for it, in the change the disk mirrors or as the conflicts ours side, and from the resolution, so the one-path write #1798 gave every position no longer destroys an uncaptured edit to that file, while an in-place resolve still proceeds. the ours side is there because a home position with more than one head can mirror a head the disk does not show, which a ferry bounce test hit. spawned-binary tests on the home position and in a lane went red with the guard call removed (2 passed, 2 failed), the in-place pair went red with the resolution check removed (2 passed, 2 failed), and dropping the recorded check turned six resume tests red (10 passed, 6 failed). the line-ending land refusal now also refuses a NUL byte in the files it walks, which held none: a planted NUL went red before the change and with the check emptied (13 passed, 1 failed each). ADR 0080 scopes its ledger reason to revert and cherry-pick and records the guard, workflow.md and two skills say a format skew owes a relay and a forge redeploy and that the relay run needs the ALLOW_FORMAT_MAJOR_CHANGE override, and the sweep 9 citation, rationale, doc, long-line and short-line leftovers are fixed. the workspace suite is green (#1988)
a1bfdaa2 · dbf3dbe6…diff - loot resolve no longer refuses a delete/modify conflict that ours deleted: the #1988 guard reads a conflict record whose ours is its base as a deletion, so a disk with no file there matches it and a file written back there is still refused over, and on a shared primary its refusal names loot describe --allow-primary, the capture that works there. spawned-binary tests on a ferry-bounced two-head home position went red before the change for the ours-deleted resolve and the restored file (1 passed, 3 failed with the primary hint test), and with the deleted-ours arm removed (3 passed, 1 failed), with ours read as bytes whatever the record says (2 passed, 2 failed) and with a position-blind hint (3 passed, 1 failed). the implement brief now takes the job cap as a placeholder, keeps deploys inside the warrant and names SKILL.md, cites only what AGENTS.md says, says which commands need the lane position and gives the CR count and describe in PowerShell too; AGENTS.md gains the cargo fmt ban, ADR 0062 names the per-ticket safeguard that replaced the implement review, and the redeploy commands in workflow.md and diagnose-push-rejected name the new tag, scope the relay override to a forward bump and say the forge takes none, as the scripts repo reads them. ADR 0082 and apply-patch say the text deletion arm checks no preimage either (#1992) and pin that --check refuses a binary deletion the same way, the line-ending docs say CRLF and NUL, nul_offenders is private, and the sweep 10 wrap leftovers are fixed. the workspace suite is green (#1993)
7ecaa467 · dbf3dbe6…diff - a locked pull stops leaving a stale copy for the next verb to capture: the ingest parks the claim it cannot check, and the first open holding the key finishes it. #1256 recorded nothing when it could not open the pre-ingest content, on the ground that loot rehome materializes and moots the question, which it does not do when it refuses, and in that state it refuses: the untouched pre-pull copy read as an edit no change had recorded, so rehome declined it and loot converge, the next step the pull note names, captured it silently, one change below the version that had just arrived. the claim, being the path, the pre-ingest oid and the digest of the bytes on disk, now goes to .loot/stale-disk-unverified, a position-owned artifact of the same shape and codec that nothing in force ever reads, and Workspace::settle_unverified_stale_disk makes the same comparison at the first unlocked open that was given a graph, beside heal_hold and under its gate: bytes that match move into the record in force, bytes that do not are the operator own work and the claim dies, an open that cannot answer leaves it parked, and a write that disposes of the bytes a claim is about retires it, whether it wrote over them or pruned them. rehome refusal stops asserting the arrival guard would refuse a capture where no guard stands: it asks Workspace::arrived_unmaterialized, keeps the circle where the guard does stand, and elsewhere says what recording those bytes actually does and that a verb capturing on entry records them whatever rehome refused. the locked pull note says to stop if rehome refuses. review fixes: the pin for the printed order now runs the three printed commands and nothing else, since any extra verb opens the repo and an open is what settles the claim, with the record own story moved to a second test; and the three malformed-record refusals stop offering a first remedy the refusal itself prevents, because they refuse at the open, so the file has to go before any verb can run. the Known Issues entry stays, wearing FIXED_IN_MAIN, because v0.4.20 is the binary a reader can install, and pin 5 comes back over it as its own retirement note instructed. red first: the note recipe, run in order, left the arrived version on disk only after the fix (0 passed, 1 failed). red under mutation, each 0 passed and 1 failed unless noted: the claim dropped rather than parked, the open never settling, the settle promoting without the check, dropping rather than keeping what it could not check, leaving a checked claim parked, and running on an open given no history; the surface, the whole-tree write and the one-path write each keeping a refuted claim (that last two 6 passed and 1 or 2 failed); the disposed set naming only what a surface wrote and not what it pruned; rehome claiming the guard everywhere and rehome never claiming it at all; the note without its stop clause; the page deleting the held entry, the page dropping the marker, and the held row witnessing a fix the tree does not carry. no migration, no wire or format byte moves, since the new file is local to a position and never bundled, and no forge or relay byte moves, so this owes no deploy; the CLI change owes a release, and the Known Issues entry leaves with it. the workspace suite is green (3962 passed over 122 binaries, 7 ignored) (#1963)
f7b7e533 · dbf3dbe6…diff - loot seek answers for a loot repository on a relay or a forge without a working tree and without its bodies, and does so beside #2043 rather than behind it: a loot host, named by a loot+ URL, by an http URL whose info endpoint answers, or by a bare word the ambient repo config resolves as a remote name, is read through a body-less cache position, an ordinary loot store under the cache home that copies the ambient repo keypair, or the one --identity-from names, and never mints one, because a fresh key is 403 at a forge and reads only Published on a relay. each invocation refreshes it with the metadata half of a pull only, the closure-declared fetch every bounded pull starts with, ingested at depth one and with no wants round, through the routed transport a pull already uses, and collects a forge standing self-grants as clone does. a key rides beside its ciphertext, so a body-less change carries no key: a listing gates on the tier the tree records, Internal listed and Restricted or a live Embargo counted sealed, and the real key gate decides after a search or a read has fetched the listable bodies in one batched round, only those the store does not hold. the store already tolerates withheld bodies and this verb never reads one it has not fetched, so the fifth kind of absence #2043 would name is never classified here; what #2043 still owes stays its own. the workspace gains the two thin seams, pull_metadata_via and fetch_objects_via, and a holds check the engine now exports. ADR 0090 records the decision the map left to #2043, in docs/adr where a decision lives: the cache directory is this verb alone, the private key copy is refused for a sealed source and a failed make leaves none behind, offline is a refusal never a stale answer, and grants are collected without acking so a peer grant is never consumed into a cache. the two-axis review before landing put the pull doc back on its function, made the listing gate Internal-only because an embargo key has no bundle lane even after its instant, refused a multi-head remote without --at, excluded burned objects from a prefetch, probed the host once per invocation, made --identity-from a plain word read from the process directory, split collect_grants out of pull-grants with the ack a choice, folded the loot loop into loot_answer beside git_answer with one row push, and pinned the refresh closure declaration, the cache object store through the binary, and a remote name resolving to the same cache. the unit tests pin the seams over the in-memory relay, the tier gate, the object fetch that brings only what was asked and declares the closure, a refresh that sees a later change at depth one, and the cache over a spawned relay carrying the source identity; the spawned-binary pin pushes the fixture to a relay spawned in the test, holds the remote listing to the local one by path and visibility with the sealed path withheld and counted, checks the cache identity is the repo's, reads a body verbatim, refuses from outside any repo naming --identity-from, and answers with it. red under mutation: the metadata pull asking for every object (0 passed, 1 failed), held bodies fetched again (0 passed, 1 failed), the object fetch declaring nothing (0 passed, 1 failed), an embargoed path listable without its body (0 passed, 1 failed), the cache minting an identity (0 passed, 1 failed), a second open making a second position (0 passed, 1 failed), and the refresh skipped (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 (3972 passed over 123 binaries, 7 ignored) (#2092)
669baef4 · 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 - ADR 0076 amended by #2101: .loot/gates gains a land phase and two hand-offs, and git hook names are mapped or refused by name. Phase::ALL has three members and every sentence that spells the set derives from it (Phase::words, Phase::usage_alternatives), so the parser refusal, the loot gates usage and the --phase errors moved without a second edit. A land gate is the user-gates row of orchestrator::GATES, between the text gates and the expensive ones, PreLandAndMerged and Outside: it runs on the lane tree and again on the merged tree when the converge moved the position, --skip-tests never silences it, and loot-first land gains --skip-gates which honors honored and never required per declared posture and records the skip in the op log after each pass, because a gate holds no Workspace. A finalize or land gate may write {message}, which expands to the path of a temp file holding the message about to be signed, newline-terminated and unlinked the moment the child returns, and {paths}, the touched paths root-relative with forward slashes, sorted, deduplicated, one argument each, deletions included; a push gate that names either is malformed, since a push has no single change, by Phase::commits_one_change rather than a list. The hand-off is built by Workspace::gate_handoff only when a gate in the phase names a placeholder, so a declaration that never asks never pays the working-delta walk, and gates::run still takes no Workspace; that accessor is the 384th Workspace method and the verb tier module doc now says so. A {message} with no message is unstartable, never a pass. The spelling was decided by running the tools, not reading them: pre-commit 4.6.2 files stage hands a hook the paths as separate arguments and its commit-msg stage hands exactly one filename, and a bare script reads that file, so a file path and argv tokens are the compatible spellings; pre-commit itself needs a .git and so cannot run inside a lane, recorded in the amendment. git pre-commit and pre-push are refused naming finalize and push; any other word is refused by name with the one sentence that holds for every unmapped hook, no index, no checkout, no rebase, no merge commit, and GIT_HOOK_PHASES is the map, never a claim about git set; the optional init --from-git was not built. Measured through the real binary in a throwaway repo, since .loot/gates is shared over the store and a land line there would make every other session binary refuse its finalize phase as malformed, which the amendment and workflow.md now warn about: two Python gates over 13 paths and the message cost 77 ms and 64 ms wall for the whole phase against 23 ms for a listing, so a contended land pays about 140 ms; loot new through the binary handed the message file and 13 paths, and --skip-gates recorded skipped p in the op log. Five pins each broken once by a named mutation and confirmed red at 0 passed 1 failed, then green at 1 passed: the row demoted to PreLand, the message file cut to its first line, the sort dropped, pre-commit unmapped, and required skipped under the flag. CONTEXT.md Phase entry and workflow.md step 7 say the same. loot-cli gates 25 passed, change and sync 65 passed, loot-first lib 225 passed after the two table assertions learned the ninth row; two lands refused on censuses the targeted runs never selected, the module-doc method count and the help placeholder classification in revset_everywhere, and each fix is one line saying the new spelling. No wire, format or store byte moves, no migration (#2101)
5c4e3996 · dbf3dbe6…diff - loot pipeline (#2127, ADR 0091, map #2099 ticket 3): the tracked .lootpipeline at the repo root, in the .lootattributes grain with no dependency column, one step per line as name, trigger, kind and what, where the trigger names the tree a step judges, change or main, never the moment, and the kind is run, argv through the spawn seam with {message} and {paths} as gates reads them through one shared builder gates::spawnable, or check, a loot-native gate deferred naming #2128 until the checks live where the verb can reach them, and a deferred step is a refusal exiting 1, never a pass. loot pipeline lists and spawns nothing; run judges the working tree by default or, under --version, a scratch tree of a held version written by Workspace::scratch_tree from readable_tree_at so the readability decision precedes any file, holding the version own .lootpipeline, and removed on drop, with the hand-off from Workspace::version_handoff naming the paths whose readable content differs from the first parent; --only narrows to one step and an unknown name is refused naming the declared set; status reads the pipeline/TRIGGER/KIND passes and pipeline/approve admissions trusted keys signed on a version through attester_trust, passes only. A tracked file never runs implicitly: loot new with a run step declared and no .loot/gates line reaches no spawner, pinned. Frozen shapes in ADR 0023 amendment, exit code on the shape. Measured on the release binary: a two-step run 74 ms, run --version 47 ms and a listing 18 ms on a three-file repo; on this repo of 1452 tracked files, run --version HEAD materializes the whole readable tree in 1.2 s against a 0.15 s listing. Found by that measurement, not by thought: under load a Windows scanner held a freshly written scratch file, the one-shot remove_dir_all failed and its swallowed error left plaintext in the temp dir, so ScratchTree drop now retries over half a second and names the path on stderr if it still cannot remove it. Seven censuses moved, each read off its own refusal: the verb tier method count to 387, MACHINE_OUTPUT gains pipeline in sorted position, the usage spells the list leaf as an invocation line, README block and its all 84 verbs sentence, 133 verb lines in three places, the revset placeholder lists take <change|main>, <step> and <id> and SELECTOR_ONLY takes pipeline status, and the site CLI page names the verb; ADR 0066 counts 84 dispatched, 82 in the table, 32 emit machine output with an amendment. CONTEXT.md Pipeline entry says what is built. Nine unit pins plus two in change.rs, five broken once by a named mutation and confirmed red at 0 passed 1 failed then green at 1 passed: a check made a pass, the trigger filter widened, the scratch removal removed, the approval role made a job, and the tracked file run at a finalize. loot-cli and loot-first green across 64 binaries, 2162 passed; the site verb census green. No wire, format or store byte moves, no migration (#2127)
fe693773 · dbf3dbe6…diff - adopt and ferry stop re-reading the whole shared graph once per absent tip, and the correctness question came before the perf one: ingest_shared_lineage is the one seam that re-reads that file mid-process (#265) because another session may have written it, so nothing may be memoised across a verb, and what lands is a LineagePool the verb owns rather than a cache the repo holds. a pool is one read of the file, handed to the tips of a single pass and dropped with it, and it is never trusted to say no: a tip it cannot name refills it from disk, which is the authoritative answer the per-tip loop used to take every time, so a change another session lands mid-pass is still found. WHAT IS GIVEN UP IS NAMED RATHER THAN LEFT TO BE FOUND: a node the pool holds and the file no longer does is spliced where a fresh read would have called it absent, which needs a prune inside the window between a pass reading the graph and that same pass reaching a tip. objects and custody catch up once per pool at its first arrival instead of once per tip, on the ordering the per-tip version already rested on, a node the pool names having been in the graph file before that scan ran and its objects written before its graph entry. measured in store_file_reads, a count that is bit-exact under load, and not in wall clock, because another session had the desktop. a ferry pass over trailered commits whose changes landed outside the lineage-filtered load read 42 store files over 2 such commits and 48 over 5 before, and reads 40 at both after; resolving an adopt prefix over the same two shapes read 6 and 12 before and reads 3 at both after, the prefix search having been a whole read of the same file on its own. red under mutation, counts read each time: ferry put back to a read per commit failed its pin (0 passed and 1 failed, 43 against 49), the adopt resolution put back to a read per tip failed its own (0 passed and 1 failed, 6 against 12), a pool believed on a miss failed the later-arrival pin (1 passed and 1 failed) and a pool that never catches up failed the every-tip-brings-its-bytes pin (1 passed and 1 failed), each restored. the hand-written workspace width in verbs/mod.rs moves to 389, which its own derived census demanded. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4162 passed over 132 binaries, 8 ignored) (#2224)
96a35669 · dbf3dbe6…diff - a deposit plan builds the tree it ships once instead of once per custody lane, and the first act had to be an instrument that could see the difference: no counter a land can read moved over a repeated whole-graph pass, so Work::GraphSorts now tallies ChangeGraph::in_order and is gated at 0% beside the object pair and store_file_reads, reading 8 on the gated fixture and one string across the drift pin repetitions. THE TICKET ASKED FOR THE TALLY TO REUSE Work::TreeWalks INSIDE finalized_tree and for ADR 0073 to re-decide that exclusion, and that was refused rather than followed: tree_walks counts a whole-tree FILESYSTEM walk of a working tree and this is a graph pass, and the in-process tier links loot-core and never loot-cli while nothing in loot-core outside its own tests calls the deposit lanes, so a tally there would have read zero on that tier anyway and the pin would have stayed green while its stated reason went false. the exclusion therefore stands unmoved on its own measurement, and the ADR records the trigger that did not fire rather than a re-decision it did not force. measured in the counter and not in wall clock: a forge plan over a fixture carrying an embargoed path, a Restricted path and the Internal default read 3 sorts before and 1 after, a relay plan 2 before and 1 after, the three lanes now taking the finalized tree as an argument plan_deposits builds once. the tips membership test inside that build became a set lookup rather than a Vec scan, which no counter can see and which is named as such rather than claimed. red under mutation, counts read each time: the shared build removed so each lane derives its own again failed the new pin (0 passed and 1 failed, graph_sorts 3 against 1, and with the forge arm relaxed the relay arm failed at 2 against 1), the tally dropped from in_order failed the anti-vacuity pin (10 passed and 1 failed) and the new pin (0 passed and 1 failed), and graph_sorts dropped from gate::COUNTERS failed four at once (7 passed and 4 failed), each restored to 11 and 1 passed. the two generated membership sentences in CONTEXT.md and HUNT-PERF.md are pasted by hand as their pins demanded, the hand-written workspace width in verbs/mod.rs moves to 390, the visibility census gains the two argument bindings the by-reference lanes create, and a count in loot-count that was wrong in the commit that wrote it is replaced by the property. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy, though the gate records a new metric from the next land. the workspace suite is green (4174 passed over 132 binaries, 8 ignored) (#2225)
fe6a089f · dbf3dbe6…diff - every land now states which versions it published that the lander did not write, and refuses nothing - ADR 0092's detection half, part 1 of 2 of this ticket. a version is foreign when its author is not in the lander's own key set, which is new: loot_identity::own_signing_keys reads the active id.pub plus every id.pub.rotated archive that loot id rotate writes, exposed as Workspace::own_signing_keys beside author_pubkey rather than instead of it, because the two answer different questions. carry_line's Foreign asks may I RE-AUTHOR this and only the current key can sign, while the gate asks did I WRITE this and authorship is historical, so a rotated operator's own change is foreign to the carry and theirs to the gate and both are right - each site now names its own question and points at the other, since unifying them breaks a caller in either direction. the judged set is the ancestor closure of the landed change minus the closure of the main it was judged against, read BEFORE the pass because by verdict time main already includes the landed change and the difference would be empty on every land, and it is the whole closure rather than carry_line's first-parent chain because a stack carrying a merge of its own would otherwise have one side waved through. an absent author is foreign, which is carry_line's answer too, so the two predicates disagree only about which keys are mine. the count rides the verdict line as foreign= and a block names the versions, since the count says that and the next question is which. foreign= sits immediately BEFORE pusher= and not after it, which I got wrong first: pusher= carries an arbitrary percent-encoded path and is designed as the tail, workflow.md says the path comes last, and five pusher tests read it with ends_with - appending after it broke exactly those, while inserting before it breaks only a fixed-index reader and the repo has none, and those five pass unmodified. the key set reads only id.pub.rotated and never the private id.rotated spelling, which holds a secret key and has no business answering a question about authorship. a machine that pulled rather than rotated holds no archive, so there your own pre-rotation work reads as foreign, which fails toward asking for an approval nobody owed and never toward waiving one, and the block and workflow.md both say so. red under mutation, counts read each time: the key set ignoring the archive (52 passed and 1 failed), the filter admitting the private archive spelling (52 passed and 1 failed), and an unauthored change read as the lander's own (202 passed and 1 failed), each restored to 53, 53 and 203. ADR 0092's status now says which sections are built, because section 5 - the gating column, review/approve and the refusals - is part 2 and until it lands detection reports and nothing refuses, which is that section's decided default anyway. no migration, no wire or format byte moves, and nothing on a host moves, so this owes no deploy. the workspace suite is green (4189 passed over 133 binaries) (#2178)
8cb77a56 · dbf3dbe6…diff - loot runners lists the runner rows a forge holds for this repo, part 2 of 2 of this ticket: name, key, the step kinds each may claim, live or retired, and when the forge last heard from it, owner-only because it is the owner configuration, retired rows included because who was allowed to run a version is what an audit asks. last seen needed a column, migration 0021 runner.last_seen_at, written by the runner door on every admitted request and throttled in its WHERE clause to one write per runner per 60 seconds, so a runner polling every second costs one write a minute; a registration never sets or clears it, so a runner registered and not started reads never seen, and deriving it from job rows was rejected because an idle runner finding no work leaves none. the first spelling of that throttle, last_seen_at <= $3 - $4, failed on Postgres 18 at the first touch, which would have made every runner request a 500 on the live forge while every in-memory test passed; the edge is computed in Rust now, and landed part 1 never wrote the column. the shape is born frozen under ADR 0023: porcelain pubkey state capabilities last_seen and the name last because it may hold a space, JSON with contract, remote and one object per runner, absent times as null, both from one value and pinned byte for byte. the verb census cost, counted: MACHINE_OUTPUT gains runners, the README block and its sentence move to 85 verbs, verb lines 133 to 134 at three main.rs sites and one ADR 0066 claim, dispatched verbs 84 to 85, dispatch table 83 to 84, machine output 32 to 33 with its named list, and two ADR 0066 lines that are dated records were left alone as the census skips them; the site verb list gains runners and its gate is green. the verb runs end to end through the spawned binary against a real forge. red under mutation, counts read each time: the touch unthrottled (1 passed and 1 failed), the door never touching (0 passed and 1 failed), the name not last (0 and 1), an absent time spelled as a dash in JSON (0 and 1), and a registration allowed to set last seen (1 and 1) once its pin moved into the store, where the defence lives, after a first attempt at the route proved unobservable; on Postgres 18 the throttle removed from the SQL went red at 0 passed and 1 failed after a green unmutated arm, each restored green. bash ci/local.sh is green against Postgres 18 (4258 passed over 136 binaries, 10 ignored). migration 0021 rides the forge binary, so the forge owes a deploy (#2158)
6dd6ae69 · dbf3dbe6…diff - shared_version_ids is deleted from DagRepo and Workspace, since LineagePool::ids answers the same whole-store id set out of the pool the adopt resolver already holds and the door had no caller left outside a test; the #829 reason it was added now sits on LineagePool::ids, and the adopt test that used it as a precondition asks the pool instead. the two #2224 counter pins stay two tests, and the adopt pin now says why at the code: what they share is the relation and its non-vacuity check, while what gives each its meaning is a fixture building a different position, which is where #2247 differs, its guards being one question put to one ingest timed and counted. the verb tier module doc now states 390 methods, the width its derived pin reads after the deletion. riding along, the can_open block no longer calls the discarded 1,091.9 ms reading neighbourless in the sentence that says its neighbours did not support it. red under mutation, counts read: the pool precondition inverted (0 passed and 1 failed), restored to 1 passed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4254 passed, 10 ignored) (#2236)
160e9a33 · dbf3dbe6…diff - loot doctor names the deferred absence and every ordinary open refuses a store in it, the first stage of the body-deferring filter: a store whose bodies are fetched on demand now carries .loot/deferred-bodies (the remote and what asked, read fail-closed so a record that will not parse still counts), the seek cache position writes it whenever it opens a position lacking it, doctor reports it as an info bodies line, and Workspace::assemble refuses such a store naming the state for every open except the new Workspace::open_fetching_bodies_at, which the cache opens through, so a verb that would read an unfetched body as absent or sealed is refused rather than answered; a body fetch that fails in the cache now refuses naming the state and the host. ADR 0093 records the state, the refusal at the open and the offline policy (a refusal, never an absence and never a stale answer, reachability never recorded), ADR 0090 gains a #2043 amendment and CONTEXT.md gains the deferred kind under Absence. what #2043 still owes is the filter on a working position, whose flag name is left open, and verbs that fault a body in instead of refusing. red under mutation, counts read each time: the open refusal dropped (0 passed and 2 failed in the seek pins, 0 and 1 through the binary), the record read fail-open (0 and 1), doctor never naming the state (0 and 2), the fetch refusal unwrapped (0 and 1), the record written only when made (0 and 1), each restored to green. a new plain-text store file, no format byte moves and no migration, so this owes no deploy. the workspace suite is green (4313 passed over 138 binaries, 12 ignored) (#2043)
ee57a2ef · dbf3dbe6…diff - loot new prints each first-seal tier off the seal the path was sealed under, so a path the same finalize had just published no longer reads internal: the summary printed the .lootattributes resolution seal_gate hands back, a Visibility, which cannot spell published because a published object is Internal plus the @world marker (#481). the tier is now read back off the signed change, seal_visibility and published_marker_to_carry over the address the finalized tree holds at each path, rendered through a new render::tier_label that restore now shares. reading rather than folding the standing rule in, as with_publish_status does for surface, is what makes it right under --no-snapshot, where the finalize signs an earlier capture and the rules may have moved in either direction; a row the finalized tree does not hold is left out, and an unreadable seal prints as a refusal and never as a tier. both directions pinned red first (0 passed and 2 failed): every Tier plus published on a fresh capture, and under --no-snapshot a published seal whose rule was deleted, a restricted seal whose rule was deleted and an internal seal given a publish rule since. red under mutation, counts read each time, each restored green: the row printed again (0 and 2), the marker ignored (0 and 2), the marker taken from the standing rule (1 and 1), seal or standing rule, the overstating repair (1 and 1, late.txt read published), the Visibility taken from the row (1 and 1), internal spelled published (0 and 2), an unreadable seal falling back to the row (2 and 1), a path the change does not hold kept (2 and 1). reproduced through the 0.4.24 binary before and a lane build after. CONTEXT.md, the census row for new_with and the Workspace width move with it. no migration, format byte or wire byte moves, so this owes no deploy. the workspace suite is green (4407 passed over 139 binaries, 13 ignored) (#2219)
a4254982 · dbf3dbe6…diff - a push now carries an attestation recorded over a change the remote already holds, so loot tag after loot push reaches a relay and a forge instead of being left behind under a success line: a local attestation ledger (.loot/attestation-ledger) records per remote what each push delivered and is read by a push and by no open or save, written by RepoStore::record_attestations_sent as a read-merge-write under the shared-store lock; the push sends the attestations over the held changes of the remote that the ledger has not recorded beside the send set and prints how many, the bundle builder keeps a late attestation only over a change inside the have closure of the recipient whoever handed it in, the forge /ingest keeps one over any change its repo holds through a new changes_held store read that costs no query when every attestation rides its change, and /info gains an additive late_attestations field so a forge that does not advertise it is sent none, has nothing recorded as sent, and the push warns how many it left behind. the land gate store_file_reads is 24 on its workload with no move, where a first cut that read the ledger on every open measured 26 and was refused; an open reads 20 store files, 21 with that cut. the #48 bound holds on the wire: a push carrying one late tag sent 256 B at a relay and 306 B at a forge over both 2 and 24 held tags. the ticket recipe, whose fresh clone lacked late-tag2 through the 0.4.24 binary, shows it through a lane build. red under mutation, counts read each time, each restored green: the open reading the ledger again (0 passed and 1 failed), the ledger write overwriting instead of merging (0 and 1), the ledger ignored (0 and 2), the late lane dropped (0 and 2), the privacy filter removed (1 and 1), the forge back to in-this-bundle (2 and 1, and end to end 1 and 1), the forge keeping any change (2 and 1), a relay push recording nothing (0 and 2), a push recording to a forge that does not keep them (1 and 1), the /info flag ignored (1 and 1). no format constant, codec byte or migration moves; the forge change is live once the forge is redeployed. the pull half is not built: a pull still carries an attestation only with a change it sends. the workspace suite is green under bash ci/local.sh against Postgres 18 (4425 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2251)
414ba6b2 · dbf3dbe6…diff - the attestation ledger records what a remote is known to keep rather than what a push put on the wire, the review-sweep fix-up over #2251: a push records rows only for a host whose /info answered, as a relay or as a forge advertising late_attestations, and a host whose /info did not answer is still sent them but has nothing recorded; against a forge without the flag it records what rode with its change, so its left-behind warning counts the late ones and no longer grows; the ledger keeps per forge the tip generation the last recorded push committed at, and a push that reads a lower one sends every attestation over the changes that forge holds again and says it may have been restored, with deleting .loot/attestation-ledger documented as the recovery for a relay and for a restore hidden by later pushes; a ledger that will not read or write warns and never fails a push or skips its deposits, and a record over an unreadable one starts it afresh. the forge misread as a relay the sweep named fails at its unsigned /wants before sending, pinned; the rule stands without it. red first with each fix undone, counts read each time, each restored green: an unanswered /info read as keeping them (0 passed and 2 failed), a forge without the flag recording nothing (0 and 1, left behind 2 where 1), a restored forge unnoticed (0 and 1), an unreadable ledger fatal (0 and 1), an unwritable ledger fatal (0 and 1), the first-seal summary defaulting an unknown change to no rows (0 and 1). also stated: the privacy filter rests on the declared have, a forge holding a head without its ancestry drops a late attestation it is recorded as keeping, a ledger is one clone, late_attestations states the property rather than naming verbs, RemoteSync bound records nothing and its docs are current, the first-seal summary refuses an unknown change and names its tree clone, and the Route doc states where hosting is decided. the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. no format constant, codec byte or migration moves. the workspace suite is green under bash ci/local.sh against Postgres 18 (4430 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2355)
9511111e · dbf3dbe6…diff - loot show, the deletion rows of loot status and a surface listing name the published tier off the marker the object carries rather than off the .lootattributes on disk, so a published object whose rule was deleted no longer prints internal and a plain one given a rule since no longer prints published: the delta seam reads each row marker in Workspace::rows_of, off the side that carries the row, and keeps the rule for a live side (ADR 0079 section 1) and for a seal it cannot read, while the clobber guards take ResolvedDelta::classes and pay no read; surface, rehome and the pull auto-surface list through the new Workspace::with_sealed_publish_status. the status machine listing is the working tree on disk and keeps the rule. measured over a 400-path tree with the counters, every added read is a memo hit: object_gets moves by one per row with a recorded side (show of a half-edited change 1602 to 1802, status with a third deleted 804 to 938, surface --json 802 to 1203) and object_disk_reads is flat on each (601, 401, 401), a clean status is flat on both, and a hyperfine A/B over 2000 paths reads within noise (show 98.2 vs 97.3 ms, status 93.1 vs 92.6, surface --porcelain 165.4 vs 168.3, status with deletions 130.8 vs 129.8, load 0 to 6 percent). surface_object_reads counts the listing marker read apart from the opens, and the restore pin reads the seals its capture wrote. both directions pinned red first (0 passed and 2 failed with the fix undone), each through the verb: show and status deletion rows, and surface over trees ferried from git commits that delete and add a published rule. red with each piece undone, counts read each time, each restored green: the delta seam on the rule (1 passed and 1 failed), marker or rule, the overstating repair (1 and 1), the new side asked for a deletion (1 and 1), the listing on the rule (1 and 1), the listing as marker or rule (1 and 1), the guard paying the reads (0 and 2 in surface_object_reads). the published values in diff and surface machine output change for such rows and no key or column moves (ADR 0023). no format, wire or migration byte moves, so this owes no deploy. the workspace suite is green (4527 passed over 140 binaries, 13 ignored) (#2352)
0c27bd77 · dbf3dbe6…diff - loot apply-patch names each applied path by the tier its seal records, and loot new --no-snapshot lists the paths the signed change first seals: the apply-patch report printed the .lootattributes resolution, a Visibility, which cannot spell published, so a path this repo publishes read internal while loot show said published, and it now reads each row through Workspace::tiers_as_sealed, the #2219 read generalized from first_seals_as_sealed to any paths of a recorded change, rendered through render::tier_label. the first-seal summary took its set of paths from the disk walk of seal_gate, so under --no-snapshot a .lootattributes the earlier capture first sealed and the disk had since lost was left off; finalize_capturing_over now returns the paths the signed change holds and the anchor the gates judged does not (Workspace::first_seals_of), paths alone, since the tier is the seal. both pinned red first (0 passed and 1 failed each): published, internal and restricted rows through the apply-patch verb, and a --no-snapshot summary listing the lost rules file and neither a path the disk gained since nor one the anchor holds. red with each piece undone, counts read each time, each restored green: the marker ignored, published claimed for every row, the rule resolution printed (0 and 1 each on the apply-patch pin), the set taken from the disk gate, the anchor filter dropped, the anchor read after the finalize (0 and 1 each on the summary pin). reproduced through the 0.4.24 binary before and a lane build after. CONTEXT.md and the Workspace width move with it. no format, wire or migration byte moves, so this owes no deploy. the workspace suite is green (4532 passed over 139 binaries, 13 ignored) (#2353)
22faa0d6 · dbf3dbe6…diff - review sweep 16 fix-up over #2382, #2353 and #2358: the test scratch helper hands every path out inside one directory named for the process and claimed once before its first path, so a sibling a fixture derives, the <path>-lanes/ a lane spawns in, is inside what the claim cleared and a reused pid no longer hands a later run the lane an earlier run left, which stacked() in propose.rs took from read_dir; a retired key that wrote a proposal tip is refused the withdrawal in live_account_in words, decided by the retirement rule ADR 0075 already applies to an offer and ADR 0095 to a comment withdrawal, and recorded as an ADR 0075 amendment, with the account live key still withdrawing; the rows_of unread-seal branch is pinned through loot diff --json, which the doc said no verb reached; with_sealed_publish_status refuses a change this position does not hold, sharing held_tree with tiers_as_sealed, where it answered every row unpublished; the speaks_for, sees, rows_of, with_sealed_publish_status and scratch guard docs, CONTEXT.md and ADR 0095 say what the code does, the by-account property scoped to the forge routes. red with each undone, counts read each time and restored: only the handed path cleared (2 passed and 1 failed in scratch_dirs), no retired-key refusal on withdraw (2 passed and 1 failed over the three rotation pins), the decline gate by key through visible_to (0 passed and 1 failed), the rule fallback back in rows_of (0 and 1), and the empty-tree default back in with_sealed_publish_status (0 and 1). bash ci/local.sh is green against Postgres 18, 4606 passed over 142 binaries with 13 ignored and the site live suites at 78 passed. no migration; the withdraw route refuses a retired key now, so this owes a forge deploy (#2386)
6621b15f · 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.