Changes touching this path
- day 0: loot hosts loot
f4c30e75 · dbf3dbe6… - evidence: crew minted and verified (#86)
1fada823 · dbf3dbe6…diff - catch up: GB1 loot ferry lands (PR #114)
6816f50d · dbf3dbe6…diff - normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text)
e58fdda6 · dbf3dbe6…diff - loot-first tooling: ferry --with-wip review lane + loot-first.ps1 (#155) (#160)
The build half of map #148. `loot ferry --with-wip` projects the ambient
dock's unfinalized working change to a sealed-free review/<dock> branch:
provisional commits carry Loot-Change-Id + Loot-Provisional and NO
Loot-Signature (the missing trailer is the machine-checkable "not
finalized" marker) while staying SSHSIG-signed for integrity. Lanes are
keyed by the durable change_id in .loot/git-mirror/wip - deliberately
outside the mark map - and reaped lazily on every ferry pass once the
change id turns up signed (landed) or gone (abandoned). Rounds append
per revision (#150); ingest refuses provisional commits and mark
rebuilds skip them, so nothing throwaway can enter the round-trip spine.
tools/loot-first.ps1 is the orchestrator that owns every GitHub call
(loot core stays git-agnostic): `review` publishes the branch by
single-ref inline-URL push and opens the PR (pr-map ledger, #153 seam
2); `land -Pr` verifies approval, finalizes on the PR's dock (seam 3),
ferries, fast-forwards GitHub main and points the PR head at the landed
sha so GitHub marks it Merged by reachability, then pushes the relay;
`init-hook` installs the warn-only direct-commit guard (#151).
Shipped git-first, deliberately: this commit is the last one that has
to be - the tooling it lands is what flips the workflow.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
533d94d5diff - S2: reconcile the verb surface — read-only status, columnar log, eager change ids (#145)
Reconciles the daily verb surface to ADR 0030, consuming S0's durable
change_id (#143) and S1's implicit snapshot (#144).
- status is now READ-ONLY: it recomputes the pending delta live and never
persists a snapshot; -m is dropped (naming is describe's job). It shows the
working change's durable change_id alongside a live, non-durable version id
(a plaintext content fingerprint, distinct by construction from the sealed
snapshot id — Seam #1).
- new is the finalize/sign boundary and eagerly mints + prints the *next*
change's durable handle, so a fresh change has a name from birth. Adds the
convenience `new -m`. init mints the first change's handle too.
- log/status go columnar: change · version · message · vis · author, with the
change id as reverse-hex LETTERS and the version id as hex DIGITS (ADR 0029)
so the two ids disambiguate at a glance. The working change is rendered once,
as a live row shared with status (log agrees with status).
- status porcelain/JSON gain an `@` change-identity header carrying change_id
+ version id (hex); the ~ path rows and status chars stay a frozen contract.
Engine: snapshot_assigning carries an eagerly-minted handle onto a fresh
change's first version; working_preview computes the live, non-durable version
id + emptiness without sealing or recording; mint_next_change_id gates on
authorship. Store persists a per-dock next-change handle.
bd5d6a8f · dbf3dbe6…diff - Merge pull request #159 from Connor-Miller/s2-verb-surface
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
42d3411bdiff - S4: operation log + undo (#146)
Implement ADR 0031: an append-only, repo-wide, local-only operation log
(`.loot/ops`) backing `loot undo` / `loot op log` / `loot op restore`, the
safety net that makes ADR 0030's implicit auto-snapshot safe to trust.
Core (`loot_core::oplog`):
- `Operation` captures the resulting **view** — change-graph heads, each dock's
working/tip pointers, the conflicts set, and the ambient-dock pointer — as raw
pointer-file bytes, so restore is a pure pointer reset that never touches the
object store or the append-only graph (nothing is ever deleted).
- `record` appends one op per view-changing command; `undo` steps the view back
one op and appends a compensating op (the log grows on undo, so redo lands);
`op restore <n>` jumps to any op. A 1-based ordinal is the durable ref; a
`pos` field walks the history back one step at a time.
- Barriers: `push`/`grant`/`maroon`/`pull-grants` are recorded non-undoable;
undo refuses to cross one and names the real remedy (keyring/manifest are
one-way state a view reset cannot retract).
CLI:
- `loot undo`, `loot op log`, `loot op restore <n>` verbs + USAGE.
- One `record_op` per view-changing command; read-only verbs record nothing.
loot has no standalone snapshot op — S2 made `status` read-only, so every
capture rides a mutating verb and that verb is the one op.
- undo/restore reload from the restored files and re-materialize the ambient
dock (writing the restored tree, pruning what the step removed).
The oplog never enters a bundle (bundle serializes changes/objects/keys, never
reads `.loot/ops`) — asserted by test. Tests cover append-on-undo, walk-back,
barrier refusal, absent-pointer round-trip, and full CLI undo/redo. clippy clean.
ce2f96a7 · dbf3dbe6…diff - Merge pull request #162 from Connor-Miller/s4-oplog-undo
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
4204625fdiff - S3: divergent change — marker + loot abandon (#147)
Surface and collapse a divergent change per ADR 0029/0030 — the last slice of
the jj-ergonomics trio (map #142). A divergent change is one durable change id
carrying more than one live version id (two writers rewriting one change id); it
is data, not an error, and is detected per change id, not by head-counting.
- Engine: `divergent_change_ids(abandoned)` scans every node (a divergence can
sit under a single graph head, e.g. as merge parents, with identical trees) and
returns change ids with >1 live version; `versions_of_change`; `abandon_head`
drops a version from the live heads. `record_carrying` is now public — the
amend primitive that makes divergence exist (and constructs it in tests).
- `log`/`status` render a trailing `!` on a divergent change id and list each
version (shared `change_col` helper). A log whose only multi-head reason is one
divergent change stays the flat listing — routed by distinct change *lines*,
not head count, so the "run `loot apply` to converge" branch never mis-claims a
divergence apply cannot collapse (Act 3 reproduces flat, as in the prototype).
- `loot abandon <version-id>` drops a version, leaving the other live version(s)
under the change id. Nothing is deleted — the version stops being a live head
and joins a local-only `.loot/abandoned` set the live view filters. It refuses
a non-divergent change (never hides a change's sole version), and is one
undoable operation: the oplog View now also captures `.loot/abandoned`, so
`loot undo` brings the version back. `resolve`/`dock merge` are untouched.
Tests: engine detection + abandon; workspace abandon→collapse→undo and the
non-divergent refusal; store abandoned round-trip; `change_col` `!` rendering.
Live-verified Act 3 (flat `!` listing → abandon → undo restores). clippy clean.
43d5cd72 · dbf3dbe6…diff - Merge pull request #168 from Connor-Miller/s3-divergent-abandon
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
dd573561diff - loot dock rm: remove a dock, dropping parked unsigned WIP; undoable (ADR 0022 amendment, #212)
c14695b4 · dbf3dbe6…diff - Spawn DevX (#232): loot lanes observability, --ticket lane spawn, wayfinder claim-to-lane
f0a885c2 · dbf3dbe6…diff - The harbor: an on-demand lock serializes landing to git-main (#229, closes #195)
8adf34a2 · dbf3dbe6…diff - gc roots the whole shared store and catch-up verbs ingest the harbor lineage, so a lane-landed change the primary never adopted fast-forwards instead of merging, duplicating, or being pruned (#265, #263 prevention)
4c03b44c · dbf3dbe6…diff - Fix describe/finalize losing writes and crashing under shared-store contention (#293) (#303)
Two concurrent lanes over one shared store could corrupt each other's
persisted state because DagRepo::save_to persists the shared, append-only
surface by a read-modify-write of whole files, unserialized, and (for
everything but the graph) by a blind overwrite from stale in-memory state.
Root cause (a) - describe/finalize reports success but does not stick:
a finalized change vanishes from the shared graph and a content key vanishes
from the shared keyring when a racing lane's save reads the same on-disk
version and writes back its own, dropping the other's append. The verb still
returns Ok (its own lane files persisted), so no read path ever sees the
change, and visible content reads back as "content you can't see".
Fix: hold a store-level lock (.loot/store.lock, RAII, stale-breaking) across
the shared critical section and MERGE each append-only structure against the
current on-disk version instead of clobbering it (graph already unioned;
now keyring - re-honoring hard-maroon purges so a removal is not resurrected -
escrow, manifest, purges, attestations). The lock closes the read-modify-write
race so the merge always sees the other writer's already-persisted append.
Root cause (b) - intermittent exit 255 on successful-looking verbs:
graph and identity were written with plain std::fs::write, which truncates
then writes, so a concurrent reader tears the file (~11% empty/short, measured)
and load_from/save_to fail mid-command; on a torn length prefix decode_nodes'
Vec::with_capacity aborts the process (0xC0000409, shell-mapped to 255) after
the buffered success line already printed. #252 had moved the other shared
files to atomic temp+rename for exactly this reason but missed these two and
the lane-owned process files. Fix: route graph/identity and the lane-owned
writers (working, tip, tree-hash, next-change, heads, working-change,
abandoned) through atomic temp+rename, so a reader always sees the whole
prior-or-next file.
Adds tests/describe_contention.rs: describe sticks under a hammering lane;
concurrent finalizes from two lanes all survive the shared graph; loads never
tear while a lane rewrites the shared graph.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
dc8b7ecbdiff - Tolerate the Windows rename-replace window when reading replaced store files (#293 tail)
The #293 fix made every store write atomic (stage + rename-over), but left
readers intolerant of the replace itself: on Windows the rename leaves a brief
delete-pending window in which opening the destination transiently fails with
PermissionDenied. The loading_never_tears_while_a_lane_rewrites_the_shared_graph
stress test caught it on merged main (~1 failed load per few thousand opens
under a hammering writer).
New store::read_replaced: bounded retry on PermissionDenied only — NotFound
still means absent, and a persistent permission error still propagates.
Routed through it: load_from's shared-file reads (identity/graph/keyring/
escrow/manifest/purges/conflicts/attestations), read_shared_graph, gc's
cross-lane working-change read, and the store pointer/heads/working-change
readers. save_to's union reads stay plain — they run under the store lock, so
no rename is ever in flight there.
Stress test now 5/5 green (was ~1-in-3 red); full workspace suite 570 green.
ba1c4215 · dbf3dbe6…diff - Catch up to git main 809ddfe: #260 smoke default, #271 arm64 build, loot diff (#1), ADR 0034 dock retirement (#253)
587684dd · dbf3dbe6…diff - ferry ingest: compose the full parent tree or refuse; roll back an aborted pass's ingest (#307)
cc115440 · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - loot verify: name each missing object's referencing change+path, and add --accept-loss lost-ledger (#335) - missing report carries provenance (MissingObject/MissingRef); .loot/lost records operator-acknowledged unrecoverable losses so verify gates CI again while new damage still fails; primary-only, store-locked
57350aa4 · dbf3dbe6…diff - Fix pr-map ledger lost-update: serialize writes under pr-map.lock (#336)
review and land both did read -> mutate -> whole-file rewrite of
.loot/git-mirror/pr-map with no lock, and land's read happens minutes before
its write (tests, ferry, push) - a land clobbered all three rows sibling
reviews recorded mid-flight (live, 2026-07-18).
Every ledger write now funnels through one door, update_pr_map: take
git-mirror/pr-map.lock, re-read fresh, apply only this operation's own row
add/remove, replace atomically. Early reads in review/land are lookups only,
never written back. The ledger lock is deliberately separate from the harbor
lock: reviews must not queue behind a land's git-main section, and land's
ledger close-out runs after harbor release anyway.
The write is store::atomic_write like every replaced git-mirror spine file
(#307), and readers (orchestrator + loot lanes) use store::read_replaced so
the Windows rename-replace window (#293 tail) cannot read as an empty
ledger. HarborLock::acquire_contending carries a caller-supplied contended
message so a wedged ledger never claims to be a wedged harbor. ADR 0034
amended; concurrent.md ownership note refined.
11addd8f · dbf3dbe6…diff - loot grants --quarantined / --trust: review and trust quarantined senders (#12)
86c46a0d · dbf3dbe6…diff - Build loot burn: destroy + tombstone, no resurrection (#344)
0d1cc5af · dbf3dbe6…diff - loot bisect: binary-search history to find a regression (#390)
bec6bfc5 · dbf3dbe6…diff - Fix Windows rename-window read races + move contention stress tests out of the land gate (#476)
Root cause of the describe_contention flake: on Windows, atomic_write's rename-replace leaves a brief window where a concurrent reader's open of the target transiently fails (PermissionDenied / ERROR_SHARING_VIOLATION os32, or NotFound as it swaps). Fixes:
- store::read_replaced now retries os error 32 (sharing violation), not just PermissionDenied.
- New store::read_replaced_required also retries NotFound (for provably-present files: graph/identity/keyring) — a required file blinking missing mid-rename is transient, not absent.
- load_objects_loose reads each object via read_replaced (was raw fs::read, the un-retried path open_at hits before the graph).
This cut the tear from ~195 to ~0 under contention. Since the 3 describe_contention tests are strict-zero TIMING stress tests (cargo runs them in parallel, self-amplifying load), they're now #[ignore]d out of the routine 'cargo test' land gate and run via 'cargo test -- --ignored'; their invariants stay guarded in-gate by deterministic unit tests (incl. the new transient-error predicate test).
be7e9418 · dbf3dbe6…diff - Authenticate the purge lane: a purge is a signed request (#503)
A wire purge carried only an oid and yielded an unauthored tombstone, and
stow honored it before storing objects -- so any authenticated pusher could
destroy any oid across every tenant, needing no key, only the address.
ADR 0038 already called a purge event a request asking cooperating relays
and peers to destroy their copy. Cooperating meant nothing: loot honored
anyone. The signature now says who is asking; a per-receiver policy decides
whether to honor. Peers honor registered senders and quarantine strangers;
relays honor their push allowlist. Local burn is untouched and needs no key
-- only propagation requires a signature, so a keyless repo burns locally
and refuses to propagate, explicitly.
No global entitlement predicate exists: ChangeNode.tree is a full manifest
and loot duplicate copies a tree address-for-address, so authored-a-change-
referencing-this-oid is satisfiable by any cloner. Both withdrawn rules
have regression tests.
Format 9 to 10. A v10 reader parses a legacy purge lane and drops it, so no
unsigned request is honored while the rest of a v9 bundle still decodes; an
older client hard-fails on v10.
Destruction is structurally unreachable without verification: HonoredBurn
has a private field and authorize_burn is its only constructor.
Both halves are signed, with domain-separated schemes -- without tags a
maroon signature re-encodes byte-for-byte as a burn tombstone at path
length 31. Maroon entitlement is an exact Manifest grantor join, and a
grant only records a grantor when it actually installed a key, so a crafted
grant cannot plant one for content you already hold.
Closes #503.
913c5cc6 · dbf3dbe6…diff - Retry the rename in atomic_write, closing #476's other half (#531)
f07c66d8 · dbf3dbe6…diff - Collapse the two atomic_write bodies into one (#565)
10eec1b6 · dbf3dbe6…diff - store: report object count and total bytes from the layout owner (#632)
1bc2e66a · dbf3dbe6…diff - burn::Burn: one home for the burn/purge concept (#662)
8e7834a7 · dbf3dbe6…diff - GitMirror: the bridge owns its own layout (#665)
b0461c4c · dbf3dbe6…diff - Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681)
26cfbaa9 · dbf3dbe6…diff - store: race the atomic-write test in production's shape — serialized writers, read_replaced readers (#691)
83667f7c · dbf3dbe6…diff - the pre-land perf gate: counters block at 0 percent, timing warns, and a lane pins itself (#637)
52a2341e · dbf3dbe6…diff - a resolution records which side it answered, so catching up after resolving no longer re-raises the path (#744); ADR 0039 says what the one-commit promise actually guarantees (#653)
b837289e · dbf3dbe6…diff - a lane carries its own hold, and custody crosses back whole at finalize (#811)
941cafb9 · dbf3dbe6…diff - every persisted artifact crosses the store as its own type, and raw paths retire to the four recorded owners (#863)
Land A of #863 (decision #851): RepoStore gains a typed read/write pair per
artifact — identity, graph, keyring/escrow and their shared halves, manifest,
purges, attestations, burn log, conflicts, resolved — each owning the file,
the atomic replace, and the absent-file semantics, with the codec staying
where it lives. The engine's save_to/load_from/refresh_hold/gc/verify raw
sites, the op log's capture/restore and ops file, and the bisect session all
migrate through; the ops file and view restore gain the temp+rename atomicity
they lacked, and the op-log capture now waits out the Windows replace window
instead of recording a live file as absent. Absent is now NotFound alone
everywhere but `resolved` (best-effort by contract): a torn read fails loudly
rather than reading as an empty artifact. The migrated getters drop to
pub(crate); `objects/`, the keypair/peers, `git-mirror/`, and doctor keep raw
access as the recorded second owners.
0edf769d · dbf3dbe6…diff - the store grows the inventory: classification as data, an exhaustive save walk, and a View derived from the captured rows (#863)
Land B of #863 (decision #851), closing the ticket. One static row per
artifact in loot_core::store carries what the getters' doc comments said in
prose: ownership class (ADR 0034's three), undo membership (captured at a
pinned wire ordinal, or excluded with the reason as data — `lost` stays
shared AND excluded, because a view reset must not un-acknowledge accepted
loss), and the artifact's place in the walk. The engine's save_to persists
the shared surface by one exhaustive dispatch over the table, so adding an
artifact without deciding its persist story fails to compile; the op-log
View's entries derive from the captured rows in ordinal order, with ordinals
below the pinned inline count encoding inside each op and every later
ordinal riding its own trailing section — the discipline bisect (#390) and
resolved (#744) established by hand, now produced by the ordinals
themselves, so old op logs decode unchanged and a new captured row is one
inventory row whose existence IS its undo coverage. Structural tests pin the
table: dense unique ordinals, capture implies position ownership, and every
row's path agrees with its class (keyring/escrow project, ADR 0047).
d05a0c46 · dbf3dbe6…diff - a store knows which machine materialized it: an arrived copy refuses to record until loot rehome, so a bootstrap cannot silently capture every sealed path as deleted (#986)
4c15a2ca · dbf3dbe6…diff - a relay's deposit stops standing in for the forge's: the push-time dedupe moves off the Manifest onto a per-remote deposit ledger, so relay-first Restricted custody reaches the forge inbox the browser reads (#1052)
499739af · dbf3dbe6…diff - every durable .loot artifact declares its format discipline in one inventory, and the fixed-width set files refuse malformed bytes instead of reading as empty (#1135)
58072a13 · dbf3dbe6…diff - custody at rest seals to your own key behind an optional passphrase, unlock is a session file or an env var, and no non-interactive caller starts prompting (#1138)
c8ffee3b · dbf3dbe6…diff - the unlock session file is 0600 from birth, the format guard walks a locked store, and id lock re-encrypts before it rewrites the session (#1175)
fd154ee0 · dbf3dbe6…diff - the grant-path skill states how an embargoed key actually reaches a relay, the last stale qualifier drops, and two doc drifts close (#1179)
eee134c9 · dbf3dbe6…diff - the atomic-write race test names which of its three sites failed, with the writer and the iteration, and AGENTS.md stops the next agent filtering the panic away (#1102)
ffe33025 · dbf3dbe6…diff - the keyring and escrow share one codec so a custody decision is written once, the locked refusal has one home instead of five drifted ones, and the terminal predicates each say why they differ (#1174)
33aca9d9 · dbf3dbe6…diff - the getting-started demo hands the repo to a second keypair instead of renaming the actor, so the first block a stranger pastes proves the thesis its caption claims, and a test runs the published bytes rather than reading them (#1214)
05629898 · dbf3dbe6…diff - an ingest records which paths it did not write, because that is the last moment absence and deletion are different facts, so a capture after --no-surface stops recording the content it just fetched as removed (#1227)
cbf4a65f · dbf3dbe6…diff - the custody sweep residuals are answered on the record: the sealed-path demo is re-run so the evidence page shows the binary it names, the disk scan learns base64 and UTF-16, the pull entry points drop their bool, and the mailbox keeps failing loud by decision (#1232)
333e9517 · dbf3dbe6…diff - an ingest records the digest of the disk it did not overwrite, so a capture can tell a stale copy from a real edit: the stale copy stops reverting the peer and an edit over it becomes a conflict instead of a silent winner (#1256)
97fe45f0 · dbf3dbe6…diff - one typed artifact door: seventeen store artifacts read and write through StoreArtifact with the malformed policy an associated choice, and the five zero-caller accessors retire (#1294)
b11d5a7d · dbf3dbe6…diff - lane gc takes the tree down marker-last so a reap that fails on a held file leaves the lane identifiable and reapable on retry, a held file is named as one with the re-run advice, and an emptied shell that will not go is re-stamped with its id (#1275)
c1f6a485 · dbf3dbe6…diff - the working-change artifact declares the fall-through its readers always had and the walk reader is the one recorded refuse-shaped exemption, and the test-side repo walk-up has one home in tests/common with main.rs named as its only twin (#1309)
51361264 · dbf3dbe6…diff - the Network sync entry says what a multi-batch pull actually declares, the measure rule records why its selectors stay direct-child, four restated stories become pointers to one telling each, and the last two hand-rolled repo walk-ups join tests/common (#1314)
a906a965 · dbf3dbe6…diff - discovery asks one structural question at every depth instead of two that disagreed, so the installer's own dot-loot is never a repo even standing in it and unlock can no longer be handed that directory, while a damaged repo refuses in its own name from a subdirectory rather than letting an ancestor answer
974715e8 · dbf3dbe6…diff - the docs demo runs between two markers that cannot silently widen, and the six surfaces still sending readers to a dead /docs anchor name the route that exists, with a test that fails the next time either class rots (#1394)
3493a354 · dbf3dbe6…diff - a one-path capture stops paying a stat per stored object and a 29.2 MiB graph rewrite, because persist now writes the objects put already reported new instead of re-deriving that from the filesystem, and skips the whole-graph read-merge-write when an outline of the file proves it would reproduce itself byte for byte (#1420)
dfebf64a · dbf3dbe6…diff - the store stops threading a dock nothing can ever produce through 43 signatures, because a store instance is already the position ADR 0034 retired named docks in favor of, and the sixteen path getters no caller outside loot-core reaches stop being public while the one main reservation that guards a git ref name stays (#1413)
23ab3abd · dbf3dbe6…diff - a repo open stops materializing half a million manifest entries no verb asked for: ChangeNode.tree becomes a Manifest holding either the decoded map or a proved-decodable byte range into one shared Arc, so topology decodes eagerly because reachability needs it while a change's path manifest decodes only when something reads it - which is where the cost actually was, since framing the 37.9 MB graph without building the maps takes 6.8 ms against 507 ms to build them, and that 507 ms was the bulk of the 1050 ms every verb paid, including loot whoami whose whole job is to print 93 bytes out of id.pub. Deref carries the ~270 existing node.tree read sites unchanged and Deferred is module-private, so a deferred manifest cannot exist over bytes Manifest::walk has not already refused with the same framing the eager pass uses, which is what earns an infallible &TreeManifest return rather than a Result; and an unmaterialized manifest is pinned never to read as an empty one, the rule read_graph_required already states for an absent graph file. Verbs also declare what they read now: RepoNeed rides beside CustodyNeed through the already-pub(crate) open_at_clocked_needing, so no new public door is added and the sealing door custody_lock pins by string and call-site count is untouched, with whoami, conflicts and manifest narrowing to POSITION_ONLY, defaulting to EVERYTHING everywhere else, and a withheld graph or store aborting loudly rather than reading as empty. whoami 555 to 42 ms against a 40 ms control, conflicts 557 to 41, manifest 557 to 42, log -n 5 626 to 144, status 704 to 227, lanes 4069 to 717, peak RSS for whoami 148 to 7.1 MB, and twelve verbs byte-identical against main including 414 KB of loot log. Three of this ticket's premises were wrong and are corrected rather than worked around: the keyring it named as the second cost measures 1.1 ms and is left alone, read_graph_outline did not fit because it discards the parents reachability needs, and the object-store half it called the boolean that costs a gigabyte had already landed in #1545 - while the stat cache stays unbuilt, which is the whole of what status and lanes still spend (#1536)
b923abb1 · dbf3dbe6…diff - the loot-core tests stop sharing one repo and the shared store stops growing, and the design was already written one line above the bug: tmp said the temp root every fixture repo lives under while returning the root itself, so returning a fresh directory beneath a per-process root makes that sentence true as written with no doc edit and none of the 161 call sites changed. uniqueness is per call rather than per process, because the sites run on the harness's parallel threads inside one process, which is the half a pid does not touch and the half the isolation gradient had already separated - lib alone green, workspace red. a third hole neither ticket named is the larger one: 48 sites spelled DagRepo::init with the bare temp dir longhand, bypassing the helper entirely and invisible to both tickets, so the real set is 51 statements and not the sixty a three-line window had suggested - established by reading whole statements, since that window reads past short ones and stops short inside this crate's many multi-line format calls. the evidence is a paired experiment rather than a green run: two concurrent processes reddened object_reads five times in five, asymmetrically with the earlier starter losing every time, while the shared-store half needed six concurrent runs to reproduce at all and then failed 28 of 36 across four test names, two of them never recorded before, which is the fifth and sixth name for a ticket whose own point is that the name is not stable. after, the same experiments are ten of ten and thirty-six of thirty-six green, and a paired count on an idle machine has the unfixed arm adding 44 objects and rewriting graph, keyring and heads while the fixed arm moves nothing, byte-identical with mtimes unchanged to the millisecond. a census derives the property from source with no allowlist and was proved red four ways, including one site from the third hole that any list written from the two tickets would have passed, and including the census blinded, which reported ok on an empty set and was caught only by its own non-vacuity guard (#1667, #1688)
87c3aacb · dbf3dbe6…diff - a move becomes a recorded fact instead of a later guess, and the premise this ticket rested on was false: #98's object reuse is keyed by PATH, so a moved path is absent from the outgoing tree at its new key, falls through to put_sealed and gets a fresh address - the same object under a different key was not a fact waiting to be read off the tree, it had to be made true by extending the reuse across the move, and everything else follows from that. The rule is that a move is recorded only when the path's sealed object survives it, so a move whose content also changed in the same capture window shares no object, records nothing and is two rows, while a move and then an edit across two captures of one change composes and reads as one row saying the content also changed. Empty content and any ambiguity are refused for the same reason the whole design exists: zero bytes equal all zero bytes, and choosing between two vanished twins would make a signed fact depend on iteration order. On the boundary the render side compares nothing at all - resolve reads keys and never an address, a visibility or a byte, pinned by running it twice over trees that agree on every key and differ on every address at three unopenable tiers, with a positive control beside it so agreement is not evidence it answers nothing. The capture side compares once: it opens a vanished object with this identity's own keys, fail-closed so an unopenable object is never read or hashed, and pairs only against an addition at an identical visibility and publication tier, with the digest living for one call, never an address, never stored and never on the wire. What a relay newly learns is that two keys held equal ciphertext, which recording a move states in plaintext anyway since tree paths are plaintext at every tier, so it is inherent to the feature rather than the ADR 0004 oracle - written into that ADR rather than left in a message. Renames ride the label seam change_id and authored_at already use: covered by the finalize signature so a relay cannot rewrite a move, never folded into the version id so two peers reaching one tree by different routes still agree on its address, which is why every existing id and signature is unchanged and a v11 store reads with no move recorded. That was tested rather than argued, against a real store built by the shipped binary, where the old move still renders as a delete and an add in the same repo the new one renders as a rename, because inferring the old one would be the equality oracle arriving through the compatibility door. Three further things were wrong and are corrected rather than worked around: ADR 0019 says an additive change bumps the minor and has never described this project, since the minor is still zero and all five additive changes took the major; plan_moves own doc comment claimed the tier is checked before the digest, three lines above code that does the lookup first, when the property actually holds by the stronger route that nothing unopenable enters the index at all; and the empty-content refusal was written on both sides of the pairing, where each made the other unprovable and removing either left the test green - a duplicated guard is not belt and braces, it is two guards neither of which can be shown to be doing anything (#1539)
3c7e029b · dbf3dbe6…diff - 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 - 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 - packed object storage lands its READ half, and the decision that could not wait for the writer is what a pack is ALLOWED TO GROUP: membership is the address first byte, order is the address order, and the reader REFUSES both violations rather than the packer promising them. a pack adds structure over addresses and that structure sits OUTSIDE the ciphertext, so a packer grouping by grant list or visibility tier would make the pack boundary BE the set of objects a reader cannot open - an authorization-set disclosure produced by a storage layout, which no encryption below it helps. the rule is safe structurally rather than by good behaviour: addresses are blake3 over nonce and ciphertext (ADR 0004), so bucketing by addr[0] is provably uncorrelated with custody and a packer CANNOT group everything one identity can open. the stronger rule, no loose object in a packed bucket, is rejected as unimplementable, since one object arriving after a repack would make every store refused; the residual packed-or-loose bit is an arrival-order fact mtime already discloses. FORMAT_MAJOR goes 13 to 14 at the READER and not at the writer, because the number should mean written by a binary that can read packs, and deferring it to 1525 leaves a window where some binaries read packs and some do not and all report 13. what the bump buys is a named refusal instead of a silent lie: pack/ is not a 64 character hex name, so a v13 walk SKIPS it and reports every packed object MISSING, referenced by change and by path, where v14 answers UnsupportedFormat found 14 supported 13. v14 is the first major in this history that moves no wire layout at all, measured rather than argued - the two frozen parity vectors moved by their marker byte and every other frozen vector beside them is byte identical. two findings beyond the criteria. packing breaks ADR 0038 burn SILENTLY: destroy_objects_loose deletes a file, cannot reach a span in a pack, and would not fail either, the already-destroyed arm swallowing it while the caller records a tombstone over LIVE ciphertext - it now refuses, and the guard asks the DISK rather than the live index, because the index version was vacuously green with burn having removed the address before the save runs. and a packed read must re-hash where a loose read need not, since a wrong offset yields a DIFFERENT VALID object under the wrong address rather than garbage. the membership pin was vacuous on its first writing too, filtering with the same function the code bucketed with, so it asserted only that the two agreed (#1524)
eb753b9f · dbf3dbe6…diff - shallow clone lands with the cut on the RECEIVING side, and that is why no wire moves and no format major does either: the fetch body is a format-marked pair of oid runs, so a depth field would be a new wire shape and therefore a bump, where taking the whole change lane and KEEPING n generations needs no new field, no new endpoint and no server change - a shallow client works against every relay and forge already deployed, including older ones. the price is stated rather than glossed: the change lane metadata crosses once in full on the first round, and what is saved is the object bodies, which is where a history bytes are, exact from the second round onward. the no-false-absence guard lives in THREE places and none of them is a verb - assemble, which every CLI open lands in, measures the frontier; apply_bundle_reaching, the only thing that can move one, re-measures it; and the dispatcher states it on stderr after BOTH the success and the refusal arm, because a refusal is a false absence WORST shape. it cannot be bypassed by a verb that forgets to ask: there is no path from the CLI to history that skips assemble, and the one way to open without measuring is to declare RepoNeed without graph, which makes the first history read PANIC - so the declaration that would silence the notice is the same one that aborts the verb. it rides stderr rather than the shape, so json and porcelain stay byte identical under ADR 0023, and a complete position emits nothing at all. depth never reaches the remote AT ALL, pinned three ways: every recorded request re-encoded through the real codec is exactly header plus 32 bytes per id with no room for a depth or a path, the union of every have and wants is a SUBSET of what the relay itself named in a prior answer, and the aimed one is that two positions cloning the same history at the same depth and differing ONLY in their sparse view emit BYTE IDENTICAL requests. the test relay recorder had to start capturing IDS rather than counts, because a privacy claim about a request cannot be checked against a length. two findings came out of the sweep rather than the design. one mutation stayed GREEN and refuted a claim already written into four files - that shallowness is stable because the frontier id rides the declared closure - since a declared have IS a closure claim and the held tips therefore already subtract everything behind the cut; every occurrence is now the narrow true sentence with the refutation beside it. and a count assertion caught a silent no-op: the obvious deepen posture, the closure minus the frontier, comes back with an EMPTY change lane REPORTING SUCCESS, because the remaining ids are still descendants of the cut - a deepen must declare NOTHING, and the posture is now derived from the bound so the wrong pair cannot be spelled. the body-deferring filter is NOT attempted and is the one criterion left: it needs a lazy object read on every get, surface and diff path plus a policy for what happens offline, and half-building it would put a FIFTH kind of not-here into a store that already distinguishes four (#1527)
2cccbe27 · dbf3dbe6…diff - the open two biggest arms become COUNTABLE, so a regression in them can fail a land instead of passing all three checks: store.rs held no instrumentation whatsoever, no Work, no tally, no bump - and that NEGATIVE was re-verified before anything was built on it, because a negative that has quietly become false is exactly how this class of blindness starts. every line number had moved and every one is re-derived: the graph read at 2200 rather than 1993, the keyring read at 2228 rather than 2019, the two call sites at 4467 and 4510 rather than 3468 and 3502. the unit is READS, one counter rather than a pair, and the reasoning is recorded rather than assumed. a byte count is NOT 0%-gatable here, being a function of fixture contents and encoding, so every format change would fail a land for a reason its author cannot act on - and tolerancing it instead re-creates precisely the load-sensitive alarm ADR 0073 rejected the timings for. at this seam a byte count would also be blind to its own stated reason, since the lazy half of #1536 already landed and read_graph_required still reads the file WHOLE while deferring the per-change manifests, so bytes READ did not move for #1536 and will not, while bytes DECODED are a different counter at a different seam whose value depends on which manifests a verb happens to touch. the object pair precedent does not transfer either: those are two because a MEMO sits between them, and nothing analogous splits a metadata read. disjointness is BUILT rather than assumed, because object files are whole files in the store too - the object store now reads through its own door, same body, same retry budget, no tally - and the two of that door four sites which tally NOTHING today are named in the code and deliberately left alone, since closing that gap would move an already-gated number. the tally sits OUTSIDE the retry loop, because inside it would count attempts and read 1 idle against 17 busy, and that placement is pinned DETERMINISTICALLY rather than by timing luck, since an absent path walks the whole budget by construction. the structural-zero bar is met by SHAPE rather than by not-zero: across a tenfold fixture the object counters go 60 to 600 and 20 to 200 while the store counter STANDS STILL AT 24 - per-open and not per-object - and no tally left on the object door, nor one that had picked up a per-path read, can produce that pair of lines. four mutations with counts read, and the discrimination one is a discrimination rather than a constant because it ALSO fails the excluded-counters pin, catching it from both directions. the gate was then run for real in a release build: exit 0, the workload id UNMOVED so no baseline is orphaned, and the new metric absent from the baseline, so the first armed land records it and the land after that is armed. and ADR 0073 consequence clause is amended exactly where it read as full coverage, with the correction stated in as many words, alongside its table row, its status header, and six other surfaces carrying the same claim (#1662)
904f6996 · dbf3dbe6…diff - the caller count is DELETED rather than corrected, because changing two to three leaves a number a FIFTH caller falsifies and the next reader writes this ticket again - the shape the previous commit had just fixed, re-created one commit later, which is the #1553-into-#1554 recurrence with the doors swapped. both surfaces now state the PROPERTY and name no count: the function tallies nothing, counting an object read is the CALLER job, only one door does it, and one of the callers structurally CANNOT, because loot-net declares no loot-count dependency at all. a census is built where a census can be honest: it derives every call site from source across the workspace with line-comments stripped and the needle assembled at runtime so the file cannot match itself, and asserts a SET EQUALITY of file and occurrences rather than a count or a non-zero - so a new, moved or door-changed call site reddens with a message naming the decision the change owes - plus a second test that fails if loot-net ever gains the counting dependency. and what the census deliberately does NOT do is stated in its own header rather than discovered later: it does not derive TALLY-STATUS, because whether a call site tallies is a property of its ENCLOSING FUNCTION and no text scan takes that scope honestly - a line window reads past short functions and stops short inside long ones - so a guard that read complete and was not would be one more instance of the class it exists to catch. tally-status stays prose; what the census closes is the door that let the prose go stale. the false universal is restated rather than weakened: every-read-out-of-the-object-directory-comes-through-here was false, since a verify scan reads bare and pack files live in that directory and are opened raw - so the sentence now names the property that actually carries the gate, that no COUNTED door reads out of the object directory, with the counted doors enumerated. that is not weaker for gating, because only a counted read can move the number, and raw reads existing is precisely WHY they cannot disturb it. the same false universal was then found on two further surfaces the ticket had not named, and fixed there too. the glossary sentence becomes GENERATED and is held to the gate own membership by a new pin, with the stale second sentence DELETED rather than corrected and the pin own limit stated in both the file and the test. the ADR summary states the property instead of a number, its compiled-out bullet is emitted one per counter row, and its both-counters line is scoped to the object pair. five mutations with counts read - and the last two are EACH OTHER discrimination, one reddening the glossary while the signals table stays green and the other the reverse, which is what shows the second doc pin is not a duplicate of the first (#1903)
c0678a74 · dbf3dbe6…diff - three sweeps running, the commit that DELETES an asserted-complete list writes a new one in the sentence that replaces it - so the recurrence itself is recorded as a named shape rather than fixed a fourth time and forgotten: the replacement is where the next list gets written, because the author is mid-sentence explaining what the true set IS. and this change caught itself committing the class TWICE while writing, which is the evidence rather than an embarrassment - an amendment draft saying the page said twelve in FOUR places where it was five, and a draft of the replacement paragraph writing six of the NINETEEN artifacts. both numbers are gone. the six-item metadata list becomes the PROPERTY: the set is the StoreArtifact trait, RepoStore::read routes every implementor through one door, and what keeps the conclusion true as the set GROWS is each impl path resolving to a named file under the store root - verified across all nineteen, none into the object directory. the conclusion that carries the 0% gate is kept verbatim, because it was never the wrong part. a THIRD copy of that enumeration, which the ticket did not name, was found and folded in. the twelves take a RULE rather than a sweep: drop the numeral where the sentence is a present-tense claim about the current table, KEEP it where the sentence narrates what ADR 0073 decided or refused as an EVENT. fifteen sites fixed, six deliberately LEFT - including the ADR title, which is the name the decision was accepted under, and a dated amendment recording what one mechanical edit touched, since correcting THAT is precisely the defect this ticket warns about. one of the twelves was already off by one and now states the property instead. the durable guard refuses a cardinal standing before opt-in halves across derived directories, and its LIMITS are in its own header rather than discovered later. the mutation that matters most is M2: writing todays CORRECT number, fourteen, is still RED, which is what stops the next reader fixing it to a value that expires. and the control that mattered is M3: the first draft tokenized raw text, so a count split by an 80-column comment wrap read clean - it PASSED OVER ITS OWN DOC COMMENT, which was quoting the offending phrase - and with the comment-marker strip removed and the defect still in place the pin goes GREEN, which is what proves the strip load-bearing rather than decorative. the fourth count of the roster population is deleted and all three of its escape routes closed, and the ADR stops citing a miscounting line as its authority for the run-versus-night correction, making the landing times the authority and citing that line only for the NAME. the negative-control heading is narrowed to what was actually measured, on the fixture rather than on the metric, and the two unreconciled readings of one measurement are reconciled once - where the ticket own framing was itself slightly off, since the two are the same commit pair measured in two sessions (#1907)
b781aefd · 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 - a save no longer fails because something opened the staging file it was about to rename: persist_codec::save_objects_loose finished a loose object with a bare fs::rename, under a comment in store.rs asserting that this rename creates rather than replaces and so opens no such window, which is true of the destination and silent about the source. a rename takes DELETE on the name it moves, so a scanner holding the staging file refuses it as a reader holding the destination refuses a replace, and that is what #1897 measured as an ordinary save failing with an opaque backend error in 11 of 50 paired rounds and what the suite saw as this crate racing itself. that rename now goes through store::rename_retrying, whose predicate already answered to both spellings the contention takes, error 5 through the permission-denied kind and raw error 32; the kind-versus-raw half of that was resting on a platform fact nobody had asserted, so it is asserted now where the platform can answer it. two pins carry it. a staging file held without FILE_SHARE_DELETE refuses a bare rename with error 32 and does not refuse the retrying one; and a scanner that watches the object directory and opens whatever tmp file turns up does not fail a save, with rounds retried until the handle is confirmed open, so a run that never entered the window fails rather than going green having measured nothing. red first with the counts read: with the bare rename restored the scanner pin fails with backend os error 32 out of save_objects_loose and the census reddens naming save_objects_loose, which no row covers (1 passed and 1 failed in the lib, 1 passed and 1 failed in the census); with the scanner looking for a suffix nothing has it refuses on zero catches over eight rounds rather than passing vacuously (0 passed and 1 failed); and with raw 32 dropped from the predicate the held-staging pin, the scanner pin and the predicate pin all go red (0 passed and 3 failed). the census of the other renames is derived rather than listed: tests/store_rename_census.rs walks every fs::rename under crates/*/src, keys each by its file and the fn it sits in, and asserts the set both ways, so a new rename arrives red until somebody names which end of it can be held and a named one that stops renaming arrives red too; with the relay index rename swapped for a copy the missing row reddens it (1 passed and 1 failed). the relay mailbox and the identity rotation carry the same source window and are named there with why each is out of reach from here rather than fixed. no migration and no wire or format byte moves. the relay links this crate and does reach this path, so its bytes rebuild, but the window is windows sharing contention and the relay runs on linux, where the only thing the predicate answers to there is a permanent permission denial that now waits out its budget before reporting, exactly as atomic_write already does; so this owes no deploy. the workspace suite is green (3985 passed over 124 binaries, 7 ignored) (#2026)
e57db04d · dbf3dbe6…diff - the relay mailbox takes the door the relay already writes objects through, and the keypair archive reports a held file rather than waiting it out: #2026 routed the object store loose rename through store::rename_retrying and left two sites its census named, and the question here was where that helper should live. it does not move. the mailbox needed no export at all, because store::atomic_write is already public and loot-net has called it for every loose object since storage::write_loose_object was written, so save_index and write_blob now stage through that door and both ends of their renames are waited out, the reader holding the index it replaces and the scanner holding the staging file, and the fixed index.tmp that two concurrent writers shared is gone with it. the wait is spent on the host too, where rename has no sharing window and the refusals that predicate answers to are permanent, and that is accepted rather than gated on cfg(windows): a second spelling of one predicate is the shape #565 is the record of, and the relay has paid this on every loose object all along. loot-identity cannot reach the helper and the helper cannot come to it, since that crate depends on no workspace crate but loot-codec, which is the no-fs wasm core a filesystem retry loop is defined not to live in, so the choice was a second copy or a report. it reports, and on its own terms: a store staging rename waits out a scanner passing over a file microseconds old, where what is held here is a long-lived keypair file whose holder no budget outlasts, and a rotation is one operator-driven act where a refusal costs a re-run. what makes that safe is the order, so id.pub now moves before id and a refusal leaves the private key in the active slot, where archiving id first left a repo whose key had moved and whose re-run met NoKeypair; the refusal names both paths. the census loses its two mailbox rows and its blind-instrument guard stops reading a file that renames today, asserting instead that every crate directory the tree declares is in the walk. red under mutation, counts read each time: the index save put back to a bare rename (loot-net mailbox 20 passed and 1 failed), the blob write put back (20 passed and 1 failed, refused with os error 32 on the source), the archive order restored (loot-identity 3 passed and 1 failed, the private key gone), the paths dropped from the refusal (3 passed and 1 failed), a retry loop given to the archive (3 passed and 1 failed, the rotation waiting the hold out and returning Ok), a bare rename put back where the census cannot see it (store_rename_census 3 passed and 1 failed) and the walk cut to three crates (2 passed and 2 failed). ADR 0016 records the archive decision and store.rs records where the helper stays. no migration, no wire or format byte moves and no schema moves, but the relay binary changes on its own write path, so this owes a relay redeploy. the workspace suite is green (4083 passed over 130 binaries, 8 ignored) (#2112)
8b194a68 · dbf3dbe6…diff - a save no longer fails when the rename that finishes a loose object is refused over an address another writer already landed. persist_codec::save_objects_loose probes for the address before it stages, a writer of the same address can land it between that probe and the rename, and the rename is then a replace, which a reader holding the landed copy without FILE_SHARE_DELETE refuses as os error 5 for as long as it holds, past the retry budget. finish_stage now answers a refused rename with the address on disk by dropping the stage, since the name is the content address, and still propagates a refusal with the address absent. this is the second sighting on this ticket, concurrent_stage_of_same_address_does_not_tear red on Access is denied (os error 5): a hold on the destination refuses a rename as 5 and a hold on the stage as 32, and with a reader holding the landed address and a sleep both added temporarily that test went red at the same unwrap with the same message in 8 of 10 runs, and green in 10 of 10 with the fix. who held it in the sighting is not established, since a stripped copy of the race counting every refusal drew none idle or with the CPU saturated. the first sighting, a_scanner_holding_the_staging_file_does_not_fail_a_save red on os error 32, is not reproduced and this does not claim it: under a saturating load a 120 ms sleep, the length of the scanner hold, woke within about 206 ms while the retry budget stretched to about 1.5 s. red under mutation, counts read each time: the fallback disabled (0 passed and 1 failed), the stage left behind (0 and 1), every refusal swallowed (0 and 1), restored to 1 passed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4263 passed over 136 binaries, 10 ignored) (#2233)
4470683c · dbf3dbe6…diff - the graph rewrite a finalize pays reads the graph file back deferred and copies each manifest stored frames where Manifest::frames proves them to be what encoding its map writes (no backslash key, keys strictly ascending in Path order, UTF-8 holder names), and falls back to the eager read otherwise, so save_to no longer decodes every path of every change in history to add one change. the new --graph-rewrite opt-in half times that whole save at 200 paths; paired, three interleaved rounds, the gate reading load idle at 1 to 4 percent: 48.18 to 48.48 ms down to 12.65 to 12.72 ms at depth 256, 183.43 to 183.49 down to 39.14 to 39.47 ms at 1024, and 750.3 to 759.4 down to 179.5 to 182.6 ms at 4096. counted by the new graph_rewrite_allocations test, the save at depth 1024 allocates 6,039 and 6,061 times on two runs against 695,198. the file written is byte-identical to the decoding rewrite, pinned over every golden and a lone y//ab key (copied), the legacy separator and the #2273 shapes (not copied, rebuilt from the eager read), and a key or holder name that is not UTF-8 (not copied, and the save still refuses it). red under mutation, counts read each time: the eager read put back (1 passed and 1 failed), the holder check dropped, the ascent check dropped and the eager fallback dropped (1 and 1 each), the counts check dropped (0 and 2), restored to 2 passed each. riding along: HUNT-PERF names the new half, and the visibility census names the moved and new bindings. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4278 passed over 137 binaries, 11 ignored) (#2280)
e7d666e0 · dbf3dbe6…diff - LineagePool::read, the one read of the shared graph file an adopt or ferry pass catches up from, reads it through read_graph_deferred, the door #2280 added, and leaves every manifest undecoded, so an ingest no longer decodes every path of every change in history to splice nodes whose manifests it never reads; before moving any node the splice asks the new Manifest::decodes, which steps the frames for a key or holder name that is not UTF-8, and refuses the ingest naming the change when a node it would splice holds one, which the eager read refused, so the #2275 panic gains no new route. paired --ingest readings, one loot-perf-gate --features count built per arm, interleaved, the gate reading load idle at 1 to 6 percent (peak 9): 162.7 to 164.6 ms down to 12.04 to 12.39 ms at depth 1024 and 331.4 to 331.5 down to 26.9 to 27.6 ms at 2048, the check costing 0.2 to 0.7 ms at 1024; counted at depth 128 and width 200 the region allocates 1,057 times against 61,604, the read band 279 against 60,826 and the splice band 778 on both arms. the memory trade the splice comment left open is measured and taken: over a copy of this repo graph file (85.7 MB) the deferred read peaks at 171.3 MB against 308.5 and holds 87.9 against 222.2 while the pool lives, and once the pool drops a splice of ten nodes keeps the 85.7 MB buffer where the eager read kept 1.2 MB, one buffer per read that splices, for the rest of the verb. two new pins: over canonical, nested, legacy separator and the three #2273 shapes a spliced node stays undecoded and answers the eager read tree, addresses and saved tree, with tier_counts answering as the open does where two backslash-free keys name one path; and a key or holder name that is not UTF-8 refuses the splice and splices nothing. lineage_allocations now pins that the ingest count does not follow the entries. red under mutation, counts read each time: the eager read put back (0 passed and 2 failed in the engine pins, 2 and 1 in lineage_allocations), the splice check dropped (1 and 1), the holder check dropped (1 and 1), restored to 2 passed and 3 passed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4282 passed over 136 binaries, 11 ignored) (#2282)
2350f8dd · 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 - the review-sweep fix-up over #2291, #2043 and #2080. loot verify no longer walks past the deferred-bodies refusal: it finds its store by layout through resolve_store_dot and never met the record, so in a store carrying .loot/deferred-bodies it reported a body it had never fetched as missing, and verify --accept-loss wrote that body into .loot/lost as unrecoverable while the remote still held it. the refusal is now decided once, in BodyNeed::admit, which the open (Workspace::assemble) and the layout door both call, so the doors it guards are that function callers: resolve_store_dot takes a BodyNeed, verify declares Held and is refused naming the state, and doctor and config declare the new Unread, since they read no body. the relay doors take none, and loot_net::is_relay records why: each refuses a store without the relay role marker before reading an object, no code that writes the record makes a relay, and gc there removes only objects the store holds; gc --dir, count-objects --dir, relay reap-grants, relay abandon-head and serve --dir were each driven through the binary at a deferred store and each refused it. the refusal now says the verb did not declare it fetches first rather than that it reads a body, since whoami, status and log read none, and doctor, ADR 0093 section 3 and CONTEXT.md say the same; section 3 is restated as the property and records the #2295 correction. false sentences fixed: the counts beside GATES in spawn.rs and orchestrator.rs now point at the rows, the sdk gate doc and SDK_GATE_STEPS say build:wasm can reach the network and refuses rather than degrading there, the in_progress verb and operand docs state the property instead of listing verbs, HUNT-PERF stops counting the reads around the graph load, and the rewrap leftovers in the land-change skill and ADR 0071 are fixed; resolve_store_dot also gets back its own doc, which sat on registered_peer_keys. red first through the binary: verify answered 1 missing (0 passed, 1 failed). red under mutation, counts read each time: verify declaring Unread (1 passed and 1 failed), verify refused but --accept-loss let through, which accepted 1 missing object as lost (1 and 1), the refusal in admit keyed on the wrong need (0 and 2), each restored to green. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4323 passed over 137 binaries, 12 ignored) (#2295)
ded7dfa3 · dbf3dbe6…diff - a push now carries an attestation recorded over a change the remote already holds, so loot tag after loot push reaches a relay and a forge instead of being left behind under a success line: a local attestation ledger (.loot/attestation-ledger) records per remote what each push delivered and is read by a push and by no open or save, written by RepoStore::record_attestations_sent as a read-merge-write under the shared-store lock; the push sends the attestations over the held changes of the remote that the ledger has not recorded beside the send set and prints how many, the bundle builder keeps a late attestation only over a change inside the have closure of the recipient whoever handed it in, the forge /ingest keeps one over any change its repo holds through a new changes_held store read that costs no query when every attestation rides its change, and /info gains an additive late_attestations field so a forge that does not advertise it is sent none, has nothing recorded as sent, and the push warns how many it left behind. the land gate store_file_reads is 24 on its workload with no move, where a first cut that read the ledger on every open measured 26 and was refused; an open reads 20 store files, 21 with that cut. the #48 bound holds on the wire: a push carrying one late tag sent 256 B at a relay and 306 B at a forge over both 2 and 24 held tags. the ticket recipe, whose fresh clone lacked late-tag2 through the 0.4.24 binary, shows it through a lane build. red under mutation, counts read each time, each restored green: the open reading the ledger again (0 passed and 1 failed), the ledger write overwriting instead of merging (0 and 1), the ledger ignored (0 and 2), the late lane dropped (0 and 2), the privacy filter removed (1 and 1), the forge back to in-this-bundle (2 and 1, and end to end 1 and 1), the forge keeping any change (2 and 1), a relay push recording nothing (0 and 2), a push recording to a forge that does not keep them (1 and 1), the /info flag ignored (1 and 1). no format constant, codec byte or migration moves; the forge change is live once the forge is redeployed. the pull half is not built: a pull still carries an attestation only with a change it sends. the workspace suite is green under bash ci/local.sh against Postgres 18 (4425 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2251)
414ba6b2 · dbf3dbe6…diff - the attestation ledger records what a remote is known to keep rather than what a push put on the wire, the review-sweep fix-up over #2251: a push records rows only for a host whose /info answered, as a relay or as a forge advertising late_attestations, and a host whose /info did not answer is still sent them but has nothing recorded; against a forge without the flag it records what rode with its change, so its left-behind warning counts the late ones and no longer grows; the ledger keeps per forge the tip generation the last recorded push committed at, and a push that reads a lower one sends every attestation over the changes that forge holds again and says it may have been restored, with deleting .loot/attestation-ledger documented as the recovery for a relay and for a restore hidden by later pushes; a ledger that will not read or write warns and never fails a push or skips its deposits, and a record over an unreadable one starts it afresh. the forge misread as a relay the sweep named fails at its unsigned /wants before sending, pinned; the rule stands without it. red first with each fix undone, counts read each time, each restored green: an unanswered /info read as keeping them (0 passed and 2 failed), a forge without the flag recording nothing (0 and 1, left behind 2 where 1), a restored forge unnoticed (0 and 1), an unreadable ledger fatal (0 and 1), an unwritable ledger fatal (0 and 1), the first-seal summary defaulting an unknown change to no rows (0 and 1). also stated: the privacy filter rests on the declared have, a forge holding a head without its ancestry drops a late attestation it is recorded as keeping, a ledger is one clone, late_attestations states the property rather than naming verbs, RemoteSync bound records nothing and its docs are current, the first-seal summary refuses an unknown change and names its tree clone, and the Route doc states where hosting is decided. the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. no format constant, codec byte or migration moves. the workspace suite is green under bash ci/local.sh against Postgres 18 (4430 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2355)
9511111e · dbf3dbe6…diff - the review-sweep fix-up over #2355 and #2258: the forge restore note says what the push does for the host it is talking to, so a forge without late_attestations, the live forge until redeployed, is told none is sent and that its left-behind count includes attestations it may still hold, where it was told they are sent again; and the restore mark is lowered only by a push that sends what it owes to a forge known to keep it, so a push that cannot deliver leaves the restore visible to the next one that can. the unknown-host rule of #2355 is pinned through push itself, against a front that refuses /info before a real relay. red with each fix undone, counts read each time, each restored green: the relay arm recording what it carried (2 passed and 1 failed), the note saying sent again to a forge without the flag (2 and 1), a push that does not deliver lowering the mark (2 and 1). record_attestations_sent is renamed record_attestations_kept for what it records. stated rather than overstated: the #2258 scratch naming reproduces the recorded message and whether it caused that sighting is not established, and its pin is red only where the clock is coarse; CONTEXT.md names the restore limit as pushes bringing the generation back up to the mark; RemoteSync bound states the property of its callers and that it records no row but a forge push still records the mark; ADR 0018 counts no bullets; a comment in store.rs is indented to its block. no format constant, codec byte or migration moves, and the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. the workspace suite is green under bash ci/local.sh against Postgres 18 (4434 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2360)
3248da71 · dbf3dbe6…diff - the deferring manifest walk checks every stored key and Restricted holder name for UTF-8 as the eager decoder does, so a graph file holding one it refuses is refused naming the file at the open, at the adopt and ferry pool read and at the save, where the open passed it and the first read of that manifest panicked; measured first, the check shares one pass over each key with the backslash test it replaces and the open got faster, one loot-perf-gate --features count per arm, interleaved, load idle: --graph-load 9.47 to 9.54 ms before and 8.93 to 9.04 after at depth 1024, --manifest-breadth 33.9 to 35.9 against 32.3 to 32.9 at 872 paths, store_file_reads 24 on both, and over a copy of this repo graph file (93.5 MB, 1,634 nodes) the deferring decode 16.6 to 17.1 ms before and 8.8 to 9.3 after, where handing every key to the validator read 24.9 to 25.8. Manifest::decodes, the splice check of #2282 and the refusal left to the save by #2287 go, since the read now refuses what they caught. through the binary, loot log and loot status over such a file panic on 0.4.24 and refuse on the lane build. red with each piece undone, counts read each time, each restored green: the key check dropped (0 passed and 2 failed), the holder check dropped (0 and 2), a valid non-ASCII key refused (1 and 1), the refusal not naming the file (1 and 1), the open alone not naming it (0 and 1). no format constant, codec byte or migration moves. the workspace suite is green (4433 passed over 138 binaries, 13 ignored) (#2275)
23fec061 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.