Changes touching this path
- day 0: loot hosts loot
f4c30e75 · dbf3dbe6… - evidence: crew minted and verified (#86)
1fada823 · dbf3dbe6…diff - hard embargo engine/wire lands (#14, format v5)
7784bcac · dbf3dbe6…diff - embargo CLI (#88) + attack demo (#89) + section-B evidence + maroon propagation fix + drive setup
c670cc2b · dbf3dbe6…diff - catch up: GB1 loot ferry lands (PR #114)
6816f50d · dbf3dbe6…diff - (working change)
02306b53 · dbf3dbe6…diff - Ferry drive binding: close the uncaptured-WIP clobber hole; ritual runs the bridge (#115)
ferry_capture now snapshots unconditionally: disk edits that never saw a
`loot status` had no working change, so the adopt path's full-tree
materialize silently overwrote them. A capture snapshot identical to the
anchor is dropped from the graph again (new DagRepo::drop_working over
remove_head), so a quiet pass mints no redundant change and leaves no
stray head for reconcile or the next pass's anchor derivation.
tools/loot-day.ps1 now runs the daily ritual through the bridge (ADR
0028): capture day WIP when git sees it (-ForceSnapshot for git-ignored
loot paths like docs/pitch), sync the local-only private mirror at
.loot/git-mirror/mirror.git, one `loot ferry` pass, then the relay push
and drive-log entry as before.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
481fbc26diff - Ferry: recognize pulled content, or the co-located mirror walks off main (#116)
In the co-located binding (the dogfood repo), git pull writes incoming
content to disk before ferry runs, so the pre-ingest capture snapshotted
it as a spurious working change, merged it with the identical ingested
line, projected both, and moved the mirror main ahead of the checkout -
breaking the next day's mirror-sync fast-forward push.
The pass now ingests first (graph-only), then captures against BOTH the
pinned anchor and the incoming target, forking explicitly from the
anchor (snapshot_from) so a pre-dock home capture cannot fold the fresh
ingested head in unclassified. Tree equality is by CONTENT
(DagRepo::same_tree_content): sealing mints a fresh key+nonce per write
(#98), so the capture and the ingest give the same bytes different
addresses.
New regression: pulled_content_is_not_recaptured (pure ingest, no merge,
mirror main stays at the native commit, single head after reload).
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
28d855d0diff - resolve conflict at crates\loot-core\src\engine.rs
04416c05 · dbf3dbe6…diff - S1: implicit auto-snapshot on mutating verbs + demotion guard (#144) (#156)
Flip the snapshot trigger from explicit to implicit (ADR 0030): every mutating
verb captures the working tree first, so edits are never lost between commands
and no manual `loot status` is needed. Read-only verbs still never snapshot.
- Add `implicit_snapshot` + `SnapshotOpts` (--allow-demote repeatable,
--no-snapshot/--ignore-working-copy) and wire it into `new`, `describe`,
`grant` (both forms), `maroon`, `migrate`. A `positionals` helper strips the
`--allow-demote <path>` value so it is never read as a verb positional.
- `loot new` now captures pending edits before finalizing via
`Workspace::finalize_capturing`, dropping an empty/tip-duplicate capture so a
bare `new` mints no empty signed change. `working_message` preserves a
described name across an implicit capture.
- Demotion guard (#62) rides the implicit snapshot and is now a typed
`RepoError::Demotion { paths }` (matchable, not a Backend string); the message
is verb-agnostic ("re-run with --allow-demote").
- Docs: CONTEXT.md working-change + .lootattributes notes; USAGE.
Verified end-to-end on the built binary (new/grant capture without status;
demotion aborts + --allow-demote overrides; --no-snapshot skips) plus 6 new
tests. Full workspace test suite + clippy green (no new warnings).
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
8dc00304diff - S0: stable-change-id data model + FORMAT_MAJOR 6 (#143)
Implement the durable change-id data model per ADR 0029 — the keystone the
jj-ergonomics trio (map #142) builds on. Two ids per change: the existing
content-derived **version id** (`ChangeNode.id`, unchanged role: dedup, DAG
edges, sync addressing) and a NEW random 16-byte **change id**, a durable
handle stable across a working change's re-snapshots.
- `ChangeNode` gains additive `change_id: Option<[u8; 16]>`; never folded into
any hash.
- The Workspace mints a fresh change id when a change begins and carries it
across every re-snapshot (`snapshot_allowing` reads the prior working node's
id before dropping it; `record_carrying` carries, `record` mints when
authored). Keyless/bridge/legacy changes stay `None`.
- Finalize signs over `version_id ‖ change_id` (new `change_signing_message`);
`verify_authored_change` checks the same. A legacy change (`change_id = None`)
signs over the version id alone, so pre-v6 signatures still verify unchanged.
All four CLI finalize paths widened (finalize_working, sign_change, resolve).
- Wire/durable codecs carry the change id after author+sig, gated on major >= 6
(`put_change_id`/`read_change_id`); idempotent on re-receipt.
- FORMAT_MAJOR 5 -> 6; legacy decodes as `None`, no backfill. v6 goldens added,
v5 kept as decode-compat.
Parents, dedup, sync addressing, convergence: unchanged (they key on the
version id). No display/verb changes — that is S2.
Tests: change id stable across re-snapshots while version id rewrites; keyless
mints none; v6 signed-over-both-ids verifies through apply; relabelling the
change id after signing is rejected; v6 bundle/graph round-trip; v<=5 loads as
legacy. Verified end-to-end via the CLI (alice bundles a v6 signed change, bob
applies and verifies).
f253ce09 · dbf3dbe6…diff - Merge pull request #157 from Connor-Miller/s0-stable-change-id-format-6
S0 — Stable-id data model + FORMAT_MAJOR 6 (keystone) (#143)
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
419d6996diff - 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 - 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 - R7: RepoStore is the objects dir's only owner (ADR 0017, #183)
2328b920 · dbf3dbe6…diff - R3: DagRepo carved into role-scoped faces; dead pubs pruned (#179)
d6579cfa · dbf3dbe6…diff - loot edit: amend a finalized change; supersession travels as signed predecessors (ADR 0032, #171)
Implement the amend model: `loot edit <change-id>` reopens a finalized
tip change as the working change - a sibling (parent = its parent, tree
carried address-for-address, durable handle kept) whose `predecessors`
names the reopened version - so once `loot new` signs the amend, the
claim that X-prime replaces X is signed data that travels, not a
local-only abandon.
- Format: FORMAT_MAJOR 6 -> 7 (ADR 0019). ChangeNode.predecessors:
Vec<Oid> rides the bundle + durable graph after the change id,
canonically sorted, empty = ordinary; folded into the version-id
computation (a no-op amend still mints a distinct version) AND into
the finalize signature (version_id || change_id || predecessors) -
ingest trusts received ids, so stripping/forging a supersession claim
on the wire must break the signature directly. v7 reads v<=6 as
predecessors-empty; goldens updated, v5/v6 kept decode-compat.
- Liveness (amends ADR 0029's definition): superseded - named as a
predecessor by any in-graph same-cid version, regardless of that
supersessor's own abandoned/superseded state - joins abandoned as a
live-view filter in divergence detection, versions_of_change, and
log/status rows. Abandon means kill, never revert.
- Converge: converge_heads drops superseded heads before collapsing
forks (a solo amend lands at peers as a clean replacement, never
content-merged with the version it replaced); dock merge adopts an
amend of our tip as a fast-forward and treats the mirror case as a
no-op (supersedes() requires the claim to sit ON the other line).
- The verb: a named Workspace mutation; refuses on an in-progress or
uncaptured working tree (the documented ADR 0030 exception - edit
replaces the working change and never implicit-captures), on a
divergent handle (abandon first), and on descendants (tip-only v1).
One undoable op (ADR 0031); output through the render String seam.
- dock switch: an idle dock no longer parks a tip-duplicate working
child on its tip (the finalize_capturing duplicate-drop now runs
there too) - the stray polluted the tip descendants and, post-0032,
would have content-merged against amends.
Tests: engine liveness + canonical hashing + signature strip/forge;
codec round-trips + v7 goldens; workspace edit e2e / guards / undo /
dock-merge FF / converge drop. Live-verified on the built binary
(edit -> amend -> new; guards; undo). 366 tests + clippy clean.
8176f2e0 · dbf3dbe6…diff - (working change)
64d767d4 · dbf3dbe6…diff - Liveness: one loot-core home for live/superseded/divergent/parked + the head partition (#216); CONTEXT.md vocabulary; rides along: cargo-dist release config (dist-workspace.toml, release workflow, 0.1.0 manifests) from the install-prototype session (loot-site map, #206)
3630469b · dbf3dbe6…diff - pull_via over a SyncTransport seam: the pull pipeline gets an interface and tests; negotiation uses complete heads (#217)
68836d58 · dbf3dbe6…diff - One materialize chokepoint: pull/apply capture-first, converge waits (#219)
pull/apply now capture uncaptured disk edits into the working change before
they touch the tree, like every other mutating verb (ADR 0030 amendment). A
dirty pull ingests (graph append is always safe) but DEFERS convergence — the
working-change guard makes converge a no-op for that pass — and emits a note
("captured working change <id>; heads left unconverged — finalize then
re-run"). A clean pull converges as before.
The seam: one internal tree-write chokepoint. converge_heads evaluates
dirtiness ONCE at entry (before any head is dropped, so the reference stays
queryable) and the adopt/merge materialize paths refuse over uncaptured dirt
rather than clobber it; undo/abandon resurface is exempt by intent. capture
skips only a mid-flight transfer (anchor closure incomplete, via new engine
closure_complete) — a genuine delete-all still captures, never refuses.
- workspace: capture_uncaptured_edits, tree_is_dirty_over chokepoint, PullReport
{ outcomes, deferred }; converge_heads gates both writes on disk_dirty.
- cli: cmd_pull renders the defer note + records the op; cmd_apply captures.
- core: extract closure_complete (negotiation_have reuses it).
- ADR 0030 dated amendment + CONTEXT.md glossary; #169 pull-over-dirty gotcha
becomes a guarantee.
- tests: dirty divergent pull, dirty independent-head pull (defer then
converge-after-finalize), chokepoint invariant, delete-all-captures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d5dd28e4 · dbf3dbe6…diff - fix(loot-core): unique object temp-suffix + atomic custody-metadata writes (#252)
3bf5c96b · 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 - Judge the tip-duplicate drop by recorded manifests, so deletions count (#289)
A change whose only content was DELETING files was silently destroyed at
finalize: same_tree_content compared tree_at ancestry overlays, and the
union resurrects an ancestor's entry for every path the child deleted, so a
deletion-only capture read as content-identical to the tip and
drop_capture_if_redundant ate it - describe message and all. This very bug
ate its discoverer's cleanup change (#284/PR #285); the workaround that
landed was riding a one-line edit alongside the deletions.
same_tree_content now compares the two changes' recorded manifests (every
change carries its complete tree, deletion = absence): path set AND
content. An empty capture is redundant only when nothing is held to compare
against - over a non-empty tip it is a delete-everything change and signs
like any other work. Same judgment at the adopt catch-up's inline site, the
live status row, and working_preview's tip slice (a clean tree no longer
reads dirty forever once a deletion lands). Truly identical captures - the
bare new on a clean tree, the co-located checkout after a git pull - still
evaporate; an undescribed deletion now hits the #174 refusal instead of
silently no-op'ing. ADR 0030 amended.
06d1880e · dbf3dbe6…diff - Fix reconcile-merge resurrection of long-deleted files (#288): a change tree is a manifest, not an ancestry overlay
Live incident 2026-07-16: the #281 land's `ferry --with-wip` reconcile minted
merge d3ca4b8 carrying tools/loot-first.ps1 (deleted in the #218 tail) and
crates/loot-first/src/ledger.rs (moved in #232) — deleted months earlier on
every line involved; neither merge parent held them. Published to origin/main,
cleaned up by PR #286.
Root cause (loot-core, not the bridge, and not the merge base): every recorded
change carries a FULL path->address manifest — snapshot, ingest_change and
merge_tips all record whole trees, and deletion is absence from the child's
manifest — but ChangeGraph::tree_at/current_tree computed a tip's tree by
unioning every ANCESTOR's tree child-wins (delta semantics no production node
ever had). Every path ever deleted anywhere in the ancestry re-entered the
computed tree forever. merge_tips fed those polluted trees to the converge
classifier, which saw the same stale address on both sides (untouched) and
kept it; projection then faithfully published the merge manifest. The suspicion
that the merge base predated the deletions was wrong — common_ancestor_tree
always returned the ancestor's exact manifest; the resurrection needed no base
at all (both POLLUTED inputs re-raised the paths, base or no base). The live
history had exactly two ever-deleted files, and the merge resurrected exactly
those two — confirming the union mechanism.
Fix: tree_at returns the change's own manifest; current_tree unions the HEAD
manifests only (preserving the pre-dock multi-head view). Repro tests at all
three layers, each proven red under the old semantics: change_graph
(tree_at_honors_a_deletion_instead_of_unioning_the_ancestry), engine
(merge_tips_does_not_resurrect_a_path_deleted_before_the_fork), and the
incident-shaped ferry test
(reconcile_merge_does_not_resurrect_files_deleted_on_the_spine): spine deletes
a file, lines fork after it, a git-native commit lands concurrently, the
reconcile merges — the deleted path must appear in neither the merged loot
manifest nor the projected merge commit's git tree nor on disk.
Two tests that encoded the union semantics were corrected to record full
manifests (an empty/partial tree in a seeded change means delete-all, which is
what those tests accidentally said). ADR 0028 gains an amendment. Out of scope,
noted there: the classifier still has no deletion-vs-base rule, so a path
freshly deleted on ONE side since the fork is still re-adopted from the other
side of a reconcile merge — follow-up ticket to come.
77f95feb · dbf3dbe6…diff - Restore the #289 regression tests the #288 reconcile clobbered; re-delete the resurrected pair
The #288 land (tree_at/current_tree = recorded manifests) crossed this
lane mid-flight; its reconcile-merge took the landed engine.rs and
ferry.rs wholesale, dropping the #289 additions there. Restored: the
engine-level same_tree_content deletion pin, the end-to-end
deletion-projection ferry test, and the frozen-primary fixture line in
the gc-pruned adopt test (an unmaterialized dir now reads as a
delete-everything edit - #289 deletions are real work).
Also re-deletes tools/loot-first.ps1 and crates/loot-first/src/ledger.rs
a third time: this lane's earlier reconcile merges were minted by a
pre-#288 binary whose overlay merge manifests resurrected them, and the
final reconcile re-added them right after the #288 land's 7bcdf8e
re-deletion. The lane binary now carries both fixes, so this deletion
can sign (#289) and the merge line stays a manifest (#288).
81d04c47 · dbf3dbe6…diff - resolve conflict at crates\loot-core\src\engine.rs
adf9896e · dbf3dbe6…diff - Re-delete the two files pre-#288 sibling reconciles resurrected; carry the #288 fix line to main
The #288 root fix (change pyzurxul, projected commit 9763144) is already on
this line: ChangeGraph::tree_at/current_tree treated node trees as ancestry
deltas while every recorded change carries a full manifest, so every path ever
deleted re-entered every computed tree and merge_tips resurrected long-deleted
files into reconcile merges (live incident d3ca4b8, cleaned by PR #286).
tree_at now returns the change's own manifest; current_tree unions the head
manifests only. Repro tests proven red at three layers (change_graph, engine
merge_tips, and the incident-shaped ferry reconcile test); ADR 0028 amended.
This change is the tail of the same land: while this lane reviewed, sibling
lanes running pre-fix binaries reconciled and the bug fired AGAIN — merge
07d2e19 re-raised tools/loot-first.ps1 and crates/loot-first/src/ledger.rs
into the shared mirror main line, and this lane's (fixed, deletion-safe)
reconcile could only adopt what the incoming manifests carried. Deleting them
here makes the landed tip clean, the third time these two files have been
deleted (69fd3cd/4f0a34b, f0525bf, here) and the last: with the union overlay
gone their deletion can no longer be forgotten. Also folds in the #287
(pre-land gate tree) and #289 (deletion-aware tip-duplicate drop) lands via
conflict resolutions on ferry.rs/workspace.rs/engine.rs — both sides kept,
full suite green (551 tests).
0e0fdbb6 · dbf3dbe6…diff - resolve conflict at crates\loot-core\src\engine.rs
462e9c9b · dbf3dbe6…diff - Restore the #289 workspace.rs side the two-lane resolve ping-pong dropped
Two lanes (#288, #289) each held workspace.rs conflicts and each resolve
took its own side wholesale: the #288 land carried a workspace.rs without
the #289 finalize-path changes, and the reconcile materialized it over
this lane. Restored from this lane's sealed 8517905: the
drop_capture_if_redundant empty-arm (an empty capture over a non-empty
tip is a delete-everything change - only a fresh repo's bare new drops
it), the live status row manifest judgment, the adopt catch-up inline
site, the five finalize-path regression tests, and the divergence-test
manifest fixture. engine.rs resolves to the landed union (direct-manifest
same_tree_content + #288 tree_at + the #289 engine pin) - both fixes,
one file, no losers.
5047a070 · dbf3dbe6…diff - primary catch-up: checkout at the #291 squash (site/ scaffold) folded for the drift reconcile (#297)
e6f7079a · 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 - converge: three-way deletion-vs-base rule (#295) (#300)
merge_trees had no deletion-vs-base case: a path in theirs but not ours was
always AdoptTheirs, and a path in ours but not theirs was never visited (the
walk only iterated theirs). Either way a one-side deletion since the fork was
silently undone -- it fired twice while landing #288, re-adopting freshly
deleted files from the sibling line.
Apply the standard 3-way rule against the base merge_tips already computes:
- one side deleted + other unchanged-from-base -> the deletion wins (both
directions; a symmetric ours-only pass covers the path theirs dropped);
- one side deleted + other edited-since-base -> a delete/edit Conflict,
recorded so it surfaces through the harbor bounce (ADR 0036) rather than
silently resurrecting or deleting (base content stands in for the deleted
side's missing oid);
- base lacks the path -> a genuine add, adopted as before.
Unchanged-ness: address equality first, then plaintext via the key oracle
(re-seals mint fresh addresses, #65/#98); an unopenable side keeps the
conservative pre-#295 keep/adopt. New same_content helper.
Tests: converge unit tests for both delete-vs-untouched directions (incl. a
reseal-address case), both delete-vs-modified conflict directions, delete-vs-
delete clean, base-lacks-path add, unopenable conservative, and a classify
theirs-side case; engine merge_tips tests proving the one-side deletion is
honored (both orders, clean) and a delete/edit collision surfaces a conflict.
Amends ADR 0028 (new #295 amendment) + CONTEXT.md Convergence-classifier and
Adopt entries + concurrent.md, retiring the "the merge is what resurrects
files deleted upstream" caveat for the reconcile/adopt merge.
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
9fed0444diff - 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 - Relay: explicit 64 MiB body limit; push batches byte-capped (#309)
0ac2fa23 · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - Extract Custody from DagRepo (mechanical) (#323)
8957dcf1 · dbf3dbe6…diff - loot log --path <file>: filter history to a path (#6)
f3de23a6 · dbf3dbe6…diff - Add loot verify: object-store integrity check (#19) - rehash every loose object against its address, report corrupt and missing by address, exit 1 on problems; load-free so a corrupt store can still be diagnosed
aeb147b3 · dbf3dbe6…diff - resolve conflict at crates\loot-core\src\engine.rs
909065c2 · 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 - loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15)
cb471c93 · dbf3dbe6…diff - Bounced-land resolutions inherit the described ours-line subject (#337)
fbcb501d · dbf3dbe6…diff - loot embargo-status <path>: report embargoed/revealed/not-embargoed (#15) (conflict resolution: crates/loot-core/src/engine.rs)
129054f7 · dbf3dbe6…diff - Grant expiry: optional expires_at on GrantEntry and tag-3 wire (#20)
a01c3d53 · dbf3dbe6…diff - Build loot burn: destroy + tombstone, no resurrection (#344)
0d1cc5af · dbf3dbe6…diff - loot id rotate: new keypair, expiry-preserving re-grant wave, archived old key (#16)
2b16256f · dbf3dbe6…diff - ADR 0039 build: pure-projection review + carry-at-land (#362)
Review mode (`ferry --with-wip` / `loot-first review`) is now a pure
projection: no ingest, no dock reconcile, no mirror-main advance, no
spine rewrite - it mints the provisional commit from the lane's own
anchor marks and pushes only review/<position>. A lane behind git main
reviews normally; REFUSE_REVIEW_STALE_ANCHOR is deleted with the fold
it guarded (#292/#302), and the #349 review-mode trigger is
structurally gone.
Reconcile lives only at the signing verbs, and its diverged-line shape
is now the carry (DagRepo::carry_line): a self-authored suffix replays
onto landed main as superseding versions - same change id, same
subject, single parent, stale original kept as predecessor - so landed
history stays exactly one commit per change with no ferry:
1412f811 · dbf3dbe6…diff - Candidate 4 (part 1): extract the engine's sync-negotiation face
engine.rs's own doc calls DagRepo "a thin composition… no storage or merge
logic itself," but the object-level sync-negotiation face — apply_with,
offered_objects, negotiation_have, closure_complete, missing_objects,
bundle_wanted, bundle_wanted_batched, and the shared bundle_impl builder — sat
inline as one more impl DagRepo block (~250 lines) among ten. Lift it into an
engine::negotiation child module (the codebase-design review's candidate 4),
the same mechanical shape as the Custody #323 extraction.
Pure relocation, zero interface/behaviour change: the methods are still
DagRepo's, and a child module reaches the engine's private object store,
change graph, and custody exactly as they did inline (super::*). bundle_impl
becomes pub(crate) because bundle_full in the engine proper still delegates to
it. loot-core (337) and loot-net (20) suites green.
Deferred (candidate 4 part 2): decoupling bundle_codec from engine::ChangeNode
with a byte-identical wire node type. That one touches the wire format, so it
carries the "must not bump FORMAT_MAJOR, prove byte-identical" constraint and
is worth its own focused pass — especially with the relay already rejecting v8.
24cc0a19 · dbf3dbe6…diff - loot grep: content search across history via the key oracle (#391)
e47d1412 · dbf3dbe6…diff - loot cherry-pick + revert: apply/invert a change's delta to the current line (#392, #393)
0db6666c · dbf3dbe6…diff - loot split + squash: move/fold change content via ADR-0032 supersede (#395, #396)
9f3cbe33 · dbf3dbe6…diff - conflicts map stores base OID + loot resolve --tool for external 3-way merge (#400, #401)
a0711731 · dbf3dbe6…diff - Fix Windows rename-window read races + move contention stress tests out of the land gate (#476)
Root cause of the describe_contention flake: on Windows, atomic_write's rename-replace leaves a brief window where a concurrent reader's open of the target transiently fails (PermissionDenied / ERROR_SHARING_VIOLATION os32, or NotFound as it swaps). Fixes:
- store::read_replaced now retries os error 32 (sharing violation), not just PermissionDenied.
- New store::read_replaced_required also retries NotFound (for provably-present files: graph/identity/keyring) — a required file blinking missing mid-rename is transient, not absent.
- load_objects_loose reads each object via read_replaced (was raw fs::read, the un-retried path open_at hits before the graph).
This cut the tear from ~195 to ~0 under contention. Since the 3 describe_contention tests are strict-zero TIMING stress tests (cargo runs them in parallel, self-amplifying load), they're now #[ignore]d out of the routine 'cargo test' land gate and run via 'cargo test -- --ignored'; their invariants stay guarded in-gate by deterministic unit tests (incl. the new transient-error predicate test).
be7e9418 · dbf3dbe6…diff - Rename the Public visibility tier to Internal (ADR 0041 §2, #480)
6969626d · dbf3dbe6…diff - Extract the engine's merge algorithm into engine/merge.rs
The three change-level merge operations — merge_tips (2-parent merge node),
carry_line (ADR 0039 superseding replay of a diverged suffix), and
change_delta_merge (cherry-pick/revert parent-delta core) — sat buried in the
1200-line "History & identity face" of engine.rs, though they form one cohesive
unit: all combine divergent history over converge.rs's per-path classifier.
Relocate them, with their dedicated result types (CarryOutcome, DeltaMerge,
DeltaAction), into a new engine/merge.rs submodule — the "Merge face" — matching
the engine/negotiation.rs and engine/custody.rs precedent (impl DagRepo via
`use super::*;`). A pure relocation: no interface or behaviour change. engine.rs
re-exports the three types so loot_core's public surface and loot-cli's call
sites are untouched.
Boundary is deliberately narrow: the "Reconcile & relay face" (apply_sync/stow/
resolve) stays put — the call graph shows it uses converge::merge_trees directly
and never the merge fns. The integration tests stay in engine.rs (they share the
engine test module's helpers), the same convention negotiation's extraction
followed.
engine.rs: 6280 -> 5889 lines. Full workspace suite green.
aecc2779 · dbf3dbe6…diff - Extract the Surface face into engine/surface.rs
The five projection methods — surface_with_report (write a change's visible
tree to disk), readable_tree (its in-memory twin, the primitive `loot grep`
searches), visible_paths_at, burned_paths_at, and materialize (dock-switch:
surface + prune) — sat in engine.rs's "History & identity face" though they form
one cohesive unit: turn a change + reader into the tree that reader may see,
decrypting through the key oracle and honouring visibility/embargo/grant-expiry/
burn. They are the read-side twin of the Snapshot face (tree -> change).
Relocate them into a new engine/surface.rs submodule — the "Surface face" —
matching the negotiation/custody/merge precedent (impl DagRepo via
`use super::*;`). A pure relocation: no interface or behaviour change. No result
types move (the methods return tuples/Vecs), so no re-export is needed — just
`mod surface;`; loot-cli's call sites are DagRepo methods, untouched.
Deliberately narrow: the shared `grant_expired_for` gate stays put (the Repo
trait's own `surface` also uses it), the Repo trait's `fn surface` stays (an
independent implementation), and the loose accessor groups (identity, graph
queries, record primitives, attestations, log) stay in engine.rs — splitting
those would be navigability churn, not depth. The projection tests stay in
engine.rs (they share the test module's helpers), the same convention the
merge/negotiation extractions followed.
engine.rs: 5889 -> 5715 lines. Full workspace suite green.
54e80d3a · dbf3dbe6…diff - Publish mechanism: the `published` keyword + @world grantee (ADR 0041, #481)
a6758352 · 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 - Forge service skeleton: /ingest, the sync surface, and CLI forge remotes (#502)
A new `loot-forge` crate and binary, sibling to the relay and never a mutation
of it: `loot-net` is a dependency of `loot-cli`, so a storage driver in the
relay would ship inside the released CLI binary (ADR 0041:53-57). The ban runs
one way -- the forge may use the other crates; none of them may gain a driver.
Every endpoint is envelope-authenticated, where a relay authenticates only
/stow. The one declared exception is GET /info: a client reads the capability
probe before it knows how to authenticate.
POST /ingest = envelope_sign(bundle || head_declaration), one signature over
both, so a genuine bundle can never be replayed under someone else's head
claim. CAS first; nothing is written before the generation compare. A lost CAS
answers with a 409 BODY carrying the current generation and head set -- behind,
not bounced -- so the client re-carries with no extra round trip.
/stow keeps relay parity and moves no ref, and consumes no generation either,
since a client reads the generation once and then stows N batches. It DEFERS a
change whose objects have not all arrived; /ingest REFUSES one, because a
declared head whose objects are missing is a repo no pull can complete. That
asymmetry is what keeping /stow is for.
Bundles are filtered PER OBJECT by the caller's access class, closing the leak
the ticket names: bundle_impl ships a key for every ANYONE-granted object, i.e.
every Internal object. Change metadata rides the second #494 axis and gates the
whole walk -- a ChangeNode carries the full tree, so shipping one while
withholding its bytes still discloses every path, address, message and author.
Validation before any write: author signatures, and a tree whose visibility
contradicts its objects is rejected. vis_tag is derived from the SealedObject
and never trusted from the tree, which is what makes content-addressed upserts
safe.
Forge purge policy: burn iff burner == object.introduced_by, recorded at first
arrival as introducing.author else the envelope pubkey; maroon read off
grant_log, global and exact. Both are judged before any object is stored and
outside the CAS transaction.
Repos are pubkey-addressed at /k/<pubkey>/<repo>, auto-created on first push,
and may_push holds iff the signer owns the namespace. resolve_remote is
untouched -- still no default host. CORS from day one.
Storage sits behind three narrow traits with in-process reference
implementations the whole suite runs against; the Postgres DDL they mirror is
checked in at docs/sql/forge-schema.sql and its driver is #516.
CLI: /info capability detection, the live head declaration off the Liveness
view, and a refusal to push a lane -- v1 ingests the primary position only.
verify_authored_change moves to loot-codec so the forge runs the identical gate
without linking the fs-hardwired engine.
Recorded in ADR 0041, the spec and CONTEXT.md: the forge stores no content key
but a published one, because filing the ANYONE key lane would let the server
read every Internal object, and the keystone is that only Published is
server-readable. The cost is stated rather than hidden -- a fresh clone of your
own Internal content from a forge is ciphertext you cannot open until #488
lands.
Closes #502.
300e06ff · 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 - loot-core testkit: extracted engine modules own their tests again (#660)
3412c046 · dbf3dbe6…diff - burn::Burn: one home for the burn/purge concept (#662)
8e7834a7 · dbf3dbe6…diff - Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681)
26cfbaa9 · dbf3dbe6…diff - loot adopt: the git-first catch-up fast-forwards content-identical work unsigned and names foreign paths (#705)
044c84ac · dbf3dbe6…diff - a change records when it was authored, so a projected commit stops reading '6 years ago' on GitHub (ADR 0043)
loot changes carried no timestamp, so the git bridge fabricated one:
BASE_EPOCH + generation, one second per ancestor depth from a 2020 epoch.
All 526 commits sat inside seven minutes of September 2020. Earlier repairs
(#626's floor, the missing-generation refusal) fixed ORDERING and never
touched the absolute date, which is why this kept coming back.
ADR 0028 inherited the no-timestamp constraint rather than choosing it. The
real reason is upstream: a version id is blake3 over authored content, and a
clock inside that hash gives two peers different ids for identical content,
destroying the dedup and convergence of ADR 0001/0004.
So authored_at rides the label seam ADR 0029 already cut for change_id --
covered by the finalize signature (no relay can restamp it) but never folded
into the version id. The wasm golden vectors prove the separation held:
FROZEN_VERSION_ID, FROZEN_OBJ_ADDR and FROZEN_SIGN are byte-identical, and
only the version marker and one presence byte moved.
Advisory, and never an ordering input: a self-reported clock is a claim, not
evidence (ADR 0025). in_order/ids_topo, buoy and path_touch.ordinal are all
untouched, the forge indexes nothing on it, and the projection floors it past
every git parent so ancestry holds whoever's clock is wrong.
Format v11: additive for readers (a v<=10 change decodes as None and an absent
timestamp adds nothing to the signed message, so every existing signature
still verifies), breaking for writers, so loot-cli and loot-forge go to 0.4.0
in lockstep. Forge migration 0004 stores it as bigint, not timestamptz --
signed data must round-trip bit-exact or pullers reject the change.
Only new work gets real dates. Every commit on main predates v11, main is
push-fast-forward-only, and backfilling would fabricate the very claim this
replaces.
Perf-Baseline: reset the change body grew one presence byte, plus eight where a timestamp is present, so bundle_bytes/store_bytes/wire_bytes step once at the v11 boundary
eaa56d99 · dbf3dbe6…diff - a 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 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) (conflict resolution: crates\loot-core\src\engine.rs)
45057d9f · dbf3dbe6…diff - a publication survives a merge, an ingest and a resolve, so a catch-up stops silently reverting one (#783)
The reported symptom was consent fatigue: every capture in the repo refused with
"refusing to publish README.md", including changes touching nothing published.
The cause was the opposite of the symptom -- README was not published any more,
and the gate was correctly reporting a new @world transition on a publication
that had been reverted.
The ticket's hypothesis (an adopt/ferry fold re-sealing README from git-side
content) was not the mechanism. Reading the real graph, #750's own publication
change never published README at all, while it did publish both licenses. The
difference is that the licenses were NEW paths, so the snapshot seal minted them
@world, while README already existed and went through the merge -- where converge
picked between two addresses holding identical plaintext under identical
Visibility and its tie-break took the unpublished one.
@world is address-neutral, outside every signature and ignored by open,
key-travel and compression. That inertness is what made publication cheap; it is
also why nothing carries the marker structurally. It survives only where the code
writing a tree entry chooses to carry it, and four writers did not: converge, the
git bridge's ingest, resolve (so a bounce quietly unpublished the conflicted
path) and absorb's span rebuild. The snapshot seal learned this at #481 and
nothing generalized it, so Repo::put_sealed now is the generalization.
converge gains an is_published question on KeyOracle and compares publish-status
as part of a path's content, which makes the existing 3-way answer publish flips
correctly in both directions: a publication since the fork is an edit and wins
over a stale untouched side, and a deliberate unpublish is equally an edit and
still lands. On identical bytes with no base to judge from, the published side
wins -- widening was consented to once, and reverting it on no evidence is the
only outcome that loses something silently.
The other three carry, and carry ONLY. A re-seal never originates a publication:
ingest takes its policy from a git commit, which reaches loot without passing a
consent seam, so a break-glass commit adding a `published` rule seals plain and
leaves the transition for the capture gate to ask about in the usual place.
Sealing more tightly than the rule says is always safe; more loosely is what this
subsystem exists to prevent.
Ask 2 -- should an unpublish need consent? No. A prompt would make consent MORE
frequent, which is the failure the ticket is actually about: a gate that fires
routinely teaches operators and agents to pass publication flags reflexively. The
fix is fewer ways to unpublish, not another prompt, so dropping the rule is now
the only one. For the same reason the gate still asks the anchor OBJECT's marker
and not the anchor's .lootattributes: policy and marker disagree both when a
marker was lost and when a rule has just begun publishing a path, nothing there
can tell them apart, and trusting policy would turn the standing rule into a way
to publish unasked. A future marker-losing path would make the gate nag again,
and that is the failure worth having.
Ask 4 -- the forge's published index cannot disagree with main: this repo pushes
to relay.millerbyte.com and has never pushed to the forge, so the index holds no
README row. Had the push happened inside the window, it would have received the
unpublished object.
ADR 0041 gains a 2026-08-04 amendment; CONTEXT.md's Published entry records the
carry/originate split.
ec50222e · dbf3dbe6…diff - a sparse view scopes what the working tree materializes, and nothing about what syncs (#39)
fa6c6749 · dbf3dbe6…diff - what a peer declares in a negotiation is a type now, so the head list that strands a pull cannot be passed (#799)
#40's fault-injection harness found a peer holding 3 of 6 files whose head had
advanced to the sender's tip, with the negotiation reporting up to date. The
ticket read that as "an interrupted pull cannot resume." It does resume:
`pull_via_interrupted_fetch_resumes` has been proving that since #217, and
`pull_via` declares `negotiation_have()` at all three of its call sites.
The harness resumed by declaring `bob.repo.heads()` instead. That is the
defect — not the sync path, but the fact that `have` accepted any `Vec<Oid>`
and the obvious thing to reach for was the wrong one. `Repo::heads`' own doc
said "what a peer passes as have", so the trait was teaching it. `loot clone`
was doing it too, correct only because a freshly inited repo has no heads.
What a peer may declare is now `Have`, and `negotiation_have()` is the only
thing that makes one — the #217 filter is the check the type certifies.
`Have::nothing()` is the one other constructor, for transports probing a relay
and clients with no repo to ask yet; declaring less can only over-send, and
over-declaring is the strandable direction. Threaded through the
receiver-declares half only: SyncTransport, loot_net::{offer,fetch,pull} and
the forge's signed twins.
The sender side keeps `&[Oid]` deliberately. `have` means one thing in both
directions — what the recipient of the bundle holds — so there is no second
concept to name; what differs is provenance, and a type cannot carry a
guarantee across a network. Wrapping it would advertise a check that does not
happen. That reasoning lives on `Have`'s doc, which is the only place it is
written.
Candidate 1 from the ticket — refuse to advance the head over an incomplete
closure — is not built, because ADR 0024 already priced and rejected it under
"cross-batch atomicity is deliberately not provided". A confirmation note says
so there, so the next reader does not re-derive it. The new
`tests/sync_resume.rs` asserts the head advance rather than treating it as the
failure, and covers the fault the suite genuinely missed: a bundle that is
short but valid, where every batch succeeds, the pull returns Ok, and the
operator is told it worked while holding half the files. The next pull heals
it, which is what is pinned.
Acceptance criterion 3 ("am I up to date" must answer no while any object is
absent) is deliberately not built: `closure_complete` is unsatisfiable on the
forge path, where a reader legitimately never receives ciphertext it holds no
grant for, so the warning would fire forever on every forge repo with
restricted content. That is #803.
`an_interrupted_transfer_resumes_to_convergence` runs with its ignore deleted.
271dd5cf · dbf3dbe6…diff - a lane carries its own hold, and custody crosses back whole at finalize (#811)
941cafb9 · dbf3dbe6…diff - adopt resolves its target in the shared graph, so a lane-landed change is refused by name, not reported missing (#829)
52dd2c3d · dbf3dbe6…diff - a relay is measured, not capped, and the only bytes it may forget are torn writes (#817)
f52d53e4 · dbf3dbe6…diff - a lineage catch-up brings the objects and keys its nodes reference, and a ferry that dies mid-projection names the oid in hex and the recovery (#778)
The filed hypothesis was that a change node can become visible before the
objects it references are durable. It is wrong: save_to already writes objects
before the graph that names them, and that order is deliberate and commented.
The real defect needs no race at all. The object store and the keyring are
loaded exactly once, at load_from, while the shared graph is deliberately
re-read mid-process by ingest_shared_lineage (the 265 catch-up primitive). So
any peer that finalizes after this process opened contributes a node whose
bytes and keys this process will never hold. A land guarantees a long window
there: it opens the workspace, runs the pre-land tests, and only then ferries,
so the ferry aborts mid-projection while holding the harbor lock.
ingest_shared_lineage now refreshes objects and custody after it re-reads the
graph, and load_from reads in the reverse of save_to write order, so a reader
never blends a newer graph with an older object scan.
Also: an Oid renders as hex instead of a 32-element decimal byte array (Debug
full width, error strings short), and a mid-ferry abort now names its recovery
with flags checked against the verbs that parse them. hex moved from loot-core
to loot-codec because RepoError is defined in loot-codec and the dependency
runs one way only; loot-core re-exports it, so every call site is unchanged.
0ba6d577 · dbf3dbe6…diff - 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 change graph gets one read view, and the two forwarding bands over it retire (#868)
826c3339 · 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 key's provenance is stated at the one door, and a grant is refused at the two that bypass it (#864)
82e06601 · dbf3dbe6…diff - the change graph gets one read view, and the two forwarding bands over it retire (#868) (conflict resolution: crates/loot-core/src/engine.rs)
62c012aa · dbf3dbe6…diff - the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803)
f15d576e · dbf3dbe6…diff - the store spells a path one way: tree keys go slash-separated at every boundary, and a windows store loads nested on posix (#988)
d2889f60 · dbf3dbe6…diff - the relay stops re-serving a dead line: an operator retires a stale head from the live view, and nothing is forgotten (#996)
fabfaddc · dbf3dbe6…diff - an empty holder list means unknown, not nobody: a pulled Restricted path stops reading as a permanent local edit, and one engine rule answers for every content comparison (#1005)
65b6946c · 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 - an ingest pins the primary line before a sibling head can land, so a capture stops forking from the union of both heads and silently dropping the un-surfaced side out of the recorded tree (#1197)
fc6b0b7c · 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 - 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 - the consent slices become one Allowances struct a swap cannot compile against, the lockout gate calls the engine predicate it used to mirror, and the site gate steps destructure so a third step stops compiling (#1272)
ac6e2096 · 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 - 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 - the ratchet records a build that exists again, ADR 0006 finally carries the verb that leans on its third case, and three documents stop asserting things the code beside them contradicts including a pin that held the number and not the strictness it was written for
62bf03ff · dbf3dbe6…diff - a snapshot opens each base object once and answers both gates from that one read, so a delta capture stops paying a second AES key schedule for a path it has already opened (#1382)
3fe019a9 · dbf3dbe6…diff - a snapshot borrows the fork base it only reads, so a capture stops building and dropping a copy of the whole manifest it never writes to (#1385)
99108b7c · dbf3dbe6…diff - the implicit capture in front of every bare mutating verb reads the working tree once, so the tree it judges clean is the tree it records, and the doors already committed to capturing ask their refusals before paying for a read (#1395)
7f1fb453 · 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 - a delta snapshot stops decrypting all 2000 base objects to fill in a bool, because the two gates that only ever read is_err now ask a cipher-free can_open lifted from sealed::open's own embargo and key-custody steps, and the one case that changes, a held key that will not decrypt, is deliberately readable because surface propagates that error rather than skipping the path the carry-forward exists for (#1422)
06a47a15 · dbf3dbe6…diff - loot diff stops decrypting every path on both of its sides to fill in a bool, because the visibility gate now asks the cipher-free can_open #1422 built for exactly this beside the reader-scoped expiry check it must keep asking, measured at -58.7% by the first fixture in either tier that touches diff at all (#1426)
699a58e8 · dbf3dbe6…diff - five claims from this run's review sweep get pins instead of louder prose: sealed.rs's header names can_open beside the chokepoint it did not weaken, is_canonical is held to encode_graph(decode_graph(bytes)) == bytes over every golden with the rewrite predicate no longer said in two files, the #988 separator arm is pinned by bytes encode_graph cannot write instead of an if that never ran, the inflate guard's third arm gets the test its prose stood in for - which measures that arm as a narrowing of where a lie is refused rather than of whether - and a refusal that could only print one number twice prints both (#1431)
102fbec6 · dbf3dbe6…diff - a perf half added tomorrow cannot go undocumented, because one table now drives the gate's parse, its usage line, its module doc and both operator docs — and the figures that had drifted from whatever owns them, including a timing resolution quoted ten times better than the shipped tolerance, point at the owner instead of restating it (#1436)
4568d998 · 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 - one whole-tree decrypt stops being spent to answer a question the first differing path already settled, and working_preview gets the in-process sample that can see it, because the cli tier could only ever ask a 476 ms process to resolve half a percent of itself (#1423)
f5ce12eb · dbf3dbe6…diff - content whose embargo has already lifted stops reading as sealed, because promoting due keys out of escrow is now the construction of the reader every content read hangs off instead of a doc-comment obligation six callers hoisted by hand, and deleting four of those hoists left all 2438 tests green while loot surface told an author to request a grant from herself (#1464)
e59e46b3 · dbf3dbe6…diff - a verb stops reading every loose object in the store before it can open one, because the open now indexes the object directory and reads the file at the address it is asked for, so one complete index still answers membership, the persist's candidate set, gc's sweep and ADR 0038's burn while a burned address keeps no route back through the directory it came from, and the sixth opt-in half that can see any of this refuses a fixture whose object directory does not hold the objects it claims (#1545)
5c98eff6 · dbf3dbe6…diff - a pull, a wants answer and the relay's cache restore stop faulting the whole loose object store in from disk, because the receiver's membership question is now asked of the index that put's dedup, the persist's candidate set, gc's sweep and ADR 0038's burn already answer rather than through the read #1545 made lazy, so a held address whose file vanished under a live process is no longer re-fetched, which was a five-commit-old accident that would have asked a relay to resurrect what another process had just burned, while an address absent from the object directory at open is still named and a withheld one still asks forever (#1565)
113b4588 · dbf3dbe6…diff - a document, an ADR and five code comments stop asserting things the code does not do, because the interval a perf doc invented for its own blind fixture is deleted rather than corrected, the store census that moved three times in 36 hours is deleted at all four sites that shipped it, and every place a rotted object's failed read becomes a silent negative is now a recorded decision instead of an accident of is_ok() - including the cross-store save that dropped a held object and returned Ok(()), which now propagates (#1566)
5c97b104 · dbf3dbe6…diff - loot grant stops issuing an untimed key for an embargoed path whose ciphertext has rotted, because the verb asks for a reveal_at directly instead of reconstructing one from a visibility whose absence has meant held-but-unreadable as well as not-held since #1545, and a timing that cannot be read now refuses instead of defaulting to zero (#1578)
d291d168 · dbf3dbe6…diff - a repo stops going unopenable by every verb at once somewhere past 2100 changes, because the open's topo pass and the two loads shaped like it now share one explicit-stack walk instead of spending a call frame per ancestor, and that walk is pinned to emit exactly what the recursion emitted so the derived heads and the graph file's byte order cannot move (#1585)
a463423e · dbf3dbe6…diff - the per-dock load stops materializing the change graph twice, because reachable_from now owns its pool and moves each reachable node out of it instead of deep-copying a complete path manifest per change and dropping the original, and it moves them in a second pass over the ids the shared topo walk emits so that walk keeps yielding borrows and the graph file byte order stays the one #1585 pinned (#1547)
ea3859e4 · dbf3dbe6…diff - a publication marker that could not be read stops being carried as an unpublished one, because the three re-seals that carry @world across a rewrite now ask a read that refuses instead of a predicate whose swallow was argued for the publish guard alone, and the refusal is a typed variant rather than prose so a corrupt seal reaches the run-loot-verify remedy a vanished one already did, while ADR 0012 table rows name their consumer and state the grep the census is the output of (#1582)
6ee38984 · dbf3dbe6…diff - loot status stops re-reading .lootattributes once per listed path, because the publish column now comes from the one-load derivation #481 built for exactly this, so a 2000-path listing pays one policy read instead of 2001 and that count is pinned as a number that does not grow with the tree, while the two whole-tree decrypts this ticket named measure at 1.5 ms and 4 ms and are left alone with those numbers written at the seams (#1591)
24dd342b · dbf3dbe6…diff - a corrected census stops being replaced by a fresh one, because ADR 0012's table drops the completeness claim that had already missed a consumer on the day it landed and states its rule instead, the swallowing publication read that consumer used is deleted along with it so outside loot-core there is no route to one left to enumerate, and HUNT-PERF's list of which perf halves re-open their fixture per repetition, which refuted itself inside a single sentence, is deleted in favour of each half's own doc rather than pinned as prose, which is #1587's half of the same defect (#1593)
4995641a · 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 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 - 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 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 - 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 - the first review sweep of this run finds three live defects and six sentences that read wider than their subject, and the one worth the sweep is that status and log DISAGREED: a working change is a head, so RemoteDelta counted it, while scoped_view drops it from the listing - status said 3 where log listed 2, and the extra was a change push would never send. it was unpinned because every fixture FINALIZES, so the fixture came first and settled it rather than the argument; the subtraction is spelled as the two node projections, author present and signature absent, which is the same discriminator the bundle uses and ferry already reads. verify graded a pack I/O failure as object CORRUPTION while the loose arm propagated the identical fact, twelve lines under a doc saying cannot-read must never pass as verified - now verifies() is one predicate literally shared by both arms rather than two spellings that agree today, and scan_one_pack is split out so the propagating half is TESTABLE at all: an index read and a body read are two opens of a file not immutable at its name, so the only deterministic way to fail the second and not the first is to take the index and then remove the file. and [info] swallowed a real failure, since any resolve_remote error read as unconfigured including an unreadable settings file, so the arm now asks settings() a second time rather than matching on prose, keeping unconfigured for a genuine solo repo. the prose half is the doc scope-drift class six times over. read_index claimed to open a store in fewer syscalls WITHOUT reading a body and reads the whole file into memory; the syscall half was true and the bytes half was not, and the ranged read is left to 1525 with the obligation recorded there, including that a span past the end is currently checked against a buffer read_index already holds. two operator surfaces still said LOOSE objects after 1524 taught the census to count packed, and a THIRD site the ticket never named was found in DagRepo::census own doc. ADR 0023 claimed nothing changes what an unasked status emits, which was false twice - the JSON gained a field and 1524 moved contract 13 to 14 on the same shape in the same diff - so the surviving claim is the narrow one, that neither channel can be misparsed, for two DIFFERENT reasons. ADR 0019 now records that contract 14 names two status JSON shapes, because the marker buys a REFUSAL rather than shape identity, a floor and not an equality. the verbs census sentence had gone wrong one run after 1799 guarded the number beside it and warned in that very file that a derived figure can sit next to a claim that has not moved. the forge arm of the privacy pin was prose only and is now tested, and deliberately NOT by byte equality, because a forge envelope names its asker under ADR 0061 - what is pinned is that the payload unwrapped through the real unwrap_envelope is ZERO bytes on both positions, with the greater-than-64-byte envelope beside it refuting the recorder cannot see bodies (#1819)
124faca4 · dbf3dbe6…diff - loot gc --pack lands the write half, and the claim ADR 0083 deferred to this ticket is taken and comes back NEGATIVE: there is no size win and there cannot be one. a packed object is byte identical to the loose file it replaces, so a pack weighs its bodies plus 11 plus 44 per entry and the census total always RISES - measured through the CLI on a throwaway fixture, 12 objects in 11 buckets, 12094 to 12743 bytes. the groupings that WOULD compress are exactly the ones the ADR rejects as leaks, so the absence is the PRICE OF THE ANTI-LEAK RULE rather than an optimization nobody wrote, and the verb says so at the number instead of leaving a true pair that reads as a loss. the repack IS the collector: a rewritten bucket carries forward only the keep set, so an unreachable entry is simply not copied and there is no delete-a-span path to get wrong. what that leaves is a plain gc unable to collect packed garbage, and the hazard is the SILENCE rather than the retention, because nothing to prune - every stored object is referenced by a change is true of the files the pruner walked and reads as a claim about the store, printed at the operator; so a plain gc now OBSERVES it, one header and table read per pack, and prints what it could not reach beside the flag that reaches it. the census asymmetry is untouched: reclaimable_bytes stays a dry run of the plain collector, because a census agreeing with the collection is the whole reason it is trustworthy. the ranged read is implemented and it reintroduced a hazard the ticket never named - a table sized from a corruptible u32 is u32::MAX times 44, 188 GiB of prealloc - bounded now by the file, and the mutation removing that bound ABORTS the test process rather than failing it. burn still refuses a packed address and the remedy now exists and is pinned, since gc_keep_set already drops burned oids so the ordinary collector rewrites the bucket without one and no burn-shaped branch exists in the packer; it is deliberately NOT wired into destroy_objects, which runs from save taking a shared reference, because a bucket rewrite moves every OTHER member offset and the live index spans for the burned object innocent bucket-mates would go stale instantly - a destruction path that repairs itself by breaking unrelated reads is not the fix. crash safety is temp, read back through this module own reader, rename, caller unlinks, and the invariant at every instant is that every address being carried forward is readable from at least one complete file at a stable name, both of them in between; an interrupted repack is not a new failure mode but one of 817 torn writes, since the stage name is skipped by read_all and collected by the existing sweeper. two pins the mutation sweep caught as weak are fixed, one vacuous because a repo that put its own objects holds them in memory where the re-point deliberately does not reach, and one mis-named because its injected failure fires BEFORE the ordering it claimed to prove; the read-back-before-rename ordering has NO fault injection pin and that is said rather than invented, argued structurally from write_bucket having exactly one rename as its last act. and the verbs census attribution went stale a THIRD run running, caught in review here rather than after landing (#1525)
bbbe984d · dbf3dbe6…diff - loot verify --unreachable lands the dangling report and closes the packing family, and the thing it does NOT do is walk: it answers from gc_keep_set, so the whole new computation is a SET DIFFERENCE over the scan the integrity check was already doing - no third reachability walk and no second directory pass either. 1523 refused to write the second one and this refuses to write the third, which matters because a report that could disagree with the collection it names as the remedy is worse than no report. the two directions deliberately take DIFFERENT root sets and that is the subtle half: verify own node list stays the roots for MISSING, because it carries the referencing change and path that a bare oid set cannot, while the keep set is the roots for UNREACHABLE, because it additionally holds the loaded graph and it DROPS burned oids under ADR 0038 - so using it for missing would hide deliberate destruction, and reporting a burned object as missing damage is exactly the inversion burn exists to avoid. the primary own working change is seeded once in each, by the code that already seeded it. the exit rule is structural rather than a renderer discipline: is_clean does not read unreachable at all, so garbage alone cannot fail a verify however the report is rendered, and the process pin deletes an object the listing did NOT name, chosen by subtraction, so its missing half is a positive control on its unreachable half rather than a second assertion of the same thing. size is the SPAN length for a packed object and the file length for a loose one, never plaintext, which loot cannot know without keys - and the two halves are reported APART rather than summed, because that is what keeps each one checkable against its own collector: the packed half is GcReport packed_orphans by construction and the loose half is the census unreachable figure, and the census must go on excluding packed garbage to keep the promise it is pinned to. ObjectScan now carries how each present object is stored, which costs ZERO extra syscalls because both arms already held both facts at the instant they graded an address. one residual is documented rather than hidden: an address stored both loose and packed, the window inside a repack, is attributed once to its loose copy. and the empty case prints that every stored object is referenced by a change - the exact sentence 1525 had to qualify at gc as scope drift - where here it is NOT drift, because the enumeration behind it is scan_objects and that sees packed spans too; the asymmetry is pinned as a test rather than asserted in prose. the verbs census moved to 345 and its sentence moved with it, the first run in four where that did not go stale (#1526)
061af7f3 · dbf3dbe6…diff - the second review sweep of this run finds gc --pack crediting packing with the PRUNE bytes and staying silent about it, and the repair is a NARROWER SIGNATURE rather than a wider condition: size_note took the before and after totals and returned nothing whenever the store shrank, so on this repo own store, 2330 unreachable objects against roughly 250 KB of table, it would print 146.7 MiB falling to 110 MiB and say NOTHING - the exact misreading ADR 0083 warns about in its own text, in the verb written to honour that warning. packing_note now takes the REPORT and cannot see the totals at all, so no combined direction is able to suppress it, and it fires on buckets_repacked. the figure is measured by the packer rather than derived from the pair: overhead_of is header plus table, encode sizes its first body offset FROM it, and the report carries the table a rewritten bucket now holds LESS the one it replaced, computed above the dry-run bail so a dry run reports what a real run does. the pin the attribution actually rests on is new and runs a real repack over a store holding a loose orphan AND a packed one, asserting that before minus pruned minus dropped plus table equals after with every term from its own producer and the sum checked against a census none of them took, 11012 - 4141 - 4141 + 275 = 3005, and asserting the two directions genuinely disagree, which is the defect in miniature on real bytes. the pin that encoded the WRONG rule is deleted rather than preserved. four weak pins have now been found in one run and the last two were invisible to the mutation sweeps, because a sweep only tests the pins you AIM it at and therefore cannot find a pin that tests nothing - the count rule of 1812 proves a pin is LIVE, not that it is aimed at anything, and that limit is now written down. two mutations make the point by experiment rather than argument: under a copy-instead-of-rename and under a helper that unlinks a bucket without writing its pack, the OLD tests stay GREEN. one proposed fix is refused with its reason, that routing a reader fixture through the writer would assert only that the two agree and go on agreeing if both move together, which is the same vacuity the membership pin was written to avoid and which eight reader tests depend on not having; the real complaint is fixed by asserting a structural post-condition instead. the walk census contradicted itself inside one commit and the count is now stated ONCE in CONTEXT.md with the code referring to it, choosing live set as the load-bearing word because verify own node list is a ROOT SET FOR ABSENCE that is never consulted about deletion and deliberately KEEPS burned oids where a live set drops them. the commit record of c6b1cae carries the wrong version and cannot be fixed, so CONTEXT.md records that too (#1823)
907d2a9d · dbf3dbe6…diff - 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 - loot notes lands as a SECOND WRITER ON THE ATTESTATION LANE rather than as a record type of its own, so AC3 and AC6 arrive FREE and the wire does not move: a note is an attestation carrying a reserved role, the sibling of #1508 landmark record in the namespace the TAG field was shaped to admit, so every shipped reader parses the bytes, FORMAT_MAJOR does not move and NO RELAY OR FORGE REDEPLOY IS OWED. the visibility rule is the decision and it is DERIVED rather than chosen: the attestation lane carries no key and no grant list and rides the bundle beside the change it names, so a note audience is EVERY HOLDER OF THE CHANGE including a relay holding no content key - on a wholly-Internal change that audience can already open every byte, so no text ABOUT it can tell them anything new, and that implication FAILS THE INSTANT one path is Restricted or Embargoed. so the verb REFUSES on a change recording any path sealed narrower than Internal and names the paths, while Published PASSES because it is Internal plus world, a WIDER audience. inherit was rejected on its own terms - a Change HAS NO SINGLE VISIBILITY, that is the founding sentence of the glossary, so inheriting means a key granted to the INTERSECTION of holders which goes stale on every grant and maroon - and own-visibility was rejected because it is the only option that can OVER-disclose, making the safe answer an operator choice every time on an append-only already-delivered lane. an allow-leak hatch was declined because it would consent to nothing enforceable. the gate is the unconditional FIRST STATEMENT of the write path above a match exhaustive over the three writing ops, so a fourth does not COMPILE until it has an arm, and the limit is stated rather than implied: the check is at WRITE time, a path demoted afterwards leaves the note standing, and burn is the verb for that. seventeen mutations, each red with the count read, and both risky pins carry POSITIVE CONTROLS - the version-id pin mints a real version three lines below the assertion that notes did not, and the visibility pin has a peer who provably CANNOT open the sealed object reading the note in the clear beside it, each half the control for the other. a smoke run caught a defect this change had introduced rather than inherited: a multi-line note broke the attested-by line at column zero, so the cut is made AT THE DOOR, which covers a multi-line tag message that was latent and never exercised (#1519)
0ec3069f · 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 - the adopt and ferry re-read stops deep-copying a pool it is about to drop, and the instrument that can SEE that was built first, because NONE existed: measure.rs had no reference to ingest anywhere, and both depth-capable halves time DagRepo::load_from, which is the OPEN - ingest_shared_lineage re-reads the shared graph AFTER it, so no fixture built on load_from executes a line of it at any depth. the new opt-in half sizes #778 own reproduction: a dock owns the store and records one change of its own, then a lane over the SAME shared store lands a depth-change line into the graph file while the dock heads file stays put, so an open of the dock is filtered back to its one change and the landed line is exactly what the ingest must splice. the instrument is proved by a POSITIVE CONTROL rather than by an argument that it ought to work - the clone line made knowingly expensive moves the number 168.3 to 188.5 to 203.7 ms at one, two and five copies, while the graph-load half on the SAME knowingly-wrong binary reads 10.30 ms UNMOVED. the ceiling was measured before the percentage was trusted: a second discarded read_graph takes depth 1024 from 171.2 to 322.9 ms, so the EAGER graph decode is about 89% of the timed region, because this seam uses read_graph while the open uses the deferred read_graph_required of #1536 - a real and much larger finding, deliberately NOT made here, written down at the call site with its correctness question, which is that the deferred manifest holds an Arc into the buffer this function drops. the change itself is the #1547 arrangement at the other site of the same shape: collect the walk order as IDS, then MOVE the nodes out with remove instead of cloning them. the re-read semantics are untouched on purpose - same node set, same emitted order, same insert rather than from_parents_first, because this is a splice into a LIVE graph, and refresh_objects_loose and the keyring catch-up still run AFTER the splice, which is #778 ordering. it MOVED: interleaved, ten reps a side, every run exit 0 and carrying a VALUE rather than reading skipped, with the idle probe at 2 to 8 percent - min of ten per round goes 185.1 to 145.6 ms at depth 1024 and 375.1 to 291.9 at 2048, sets DISJOINT at both, and the graph-load half flat as the negative control. the magnitude is qualified rather than quoted flat: an earlier pair of the same two engine sides read minus 10.0 and minus 10.9 percent, and re-running all four binaries interleaved reproduces each level to within one percent, so the shift is CODEGEN LAYOUT rather than machine state - direction certain, sets never overlapping, minus 10 percent the conservative figure, recorded at the code. and one doc claim is REFUTED rather than restated: the function said the parents-before-children order is why head tracking stays exact, and the mutation that reversed the order came back GREEN, because the splice goes through insert, which is right under any arrival order - so the sentence now says what the order actually buys, and no pin was added to assert a courtesy (#1572)
ff123eb9 · dbf3dbe6…diff - a published speedup that exceeded a ceiling measured three paragraphs below it is WITHDRAWN rather than explained, and the instrument that measured the ceiling is REFUTED BY MEASUREMENT rather than by argument: a discarded second read_graph measures the MARGINAL cost of an extra decode while the pool is still alive, not the first call share - it reads 151.5 ms against a whole region of 148.4 on one after binary and 162.5 against 157.7 on a second, over 100 percent of the quantity it is a fraction of, twice, on separately built binaries. the replacement stops the function instead of doubling the read, and its bands SUM to the region: read_graph plus pool build 154.3 ms at 89.8 percent, walk and splice 16.2, objects and keyring 1.4, against a region of 171.9 - so the ~89 percent decode finding SURVIVES the loss of the instrument that first produced it, and on that pair the change removes 13.9 of the 16.2 ms it could touch, a delta that FITS. the magnitude itself is re-measured across 156 gate runs in one sitting on an idle machine, every run exit 0, discarded 0 batches, carrying a VALUE rather than reading skipped, with the harness own probe at 1 to 9 percent and no exit 3 or 4 anywhere: three independently built before-and-after pairs read minus 13.6, minus 8.1 and minus 10.6 percent at depth 1024, disjoint at both depths in every pair, with graph-load flat across the same invocations - so the answer is published as minus 8 percent conservative and about minus 10 middle, and the spread is the BINARY rather than the machine. the residual is stated and NOT reconciled: the clean pair 22.5 ms exceeds the 16.2 ms band it should sit inside, and the clean pair own band cannot be measured without making it un-clean, with corroborating evidence that the instrument perturbs what it measures - the stop-instrumented after binary reads a read-and-pool band ABOVE its own whole region, an early return costing more than the work it skips, six rounds out of six. what would settle it is named as an exact allocation counter over the region that no half records today, and it is left as instrument work rather than done here. the copy in the fixture now quotes NO figure at all, keeping only the shape, because a figure quoted anywhere else is a copy. the in-loop guard stops checking the OTHER guard than the one its prose names and is asked every rep of the position under measurement - proved by a mutation pair where a no-op ingest is caught RED at 156 passed 2 failed and, with the same guard disabled, GREEN at 158 passed 0 failed, which demonstrates the blindness rather than arguing it. the ADR masthead stops enumerating its own amendments and points at the inline marks that already carry their tickets, the fixture stops claiming the ancestry is the real path when its root is parentless and the walk stop predicate never fires, the Err arm stops asserting a merge as the cause it never established and hands over the selector quoted refusal instead, and the shallow header stops calling a convention mechanical when nothing enforces it (#1888)
dc524dd2 · dbf3dbe6…diff - two readers stop answering a visibility question out of a book the enforcement path never opens - and the ticket OWN worry about which ROW is REFUTED before anything is built on it: path_in_history consults current_tree FIRST and returns outright, with the reverse-topo history walk only a fallback for a path the live heads no longer carry. the keeps-every-change-whose-tree-contains-the-path shape belongs to change_has_path and filter_history_to_path, which is what log --path filters on and which embargo-status never touches. so the verb was already answering about the CURRENT recording, and its defect was purely WHICH BOOK rather than which row - which makes the fix cleaner than the ticket allowed for, since the tree entry is still read, being what LOCATES the object, and only the visibility moved. embargo-status now asks a new seal_visibility, the refusing twin of visibility_of, sharing held_but_unreadable with embargo_reveal_at, which is re-expressed over it so there is ONE read door. the tree entry answers ONLY when the seal cannot be produced, and then the output SAYS SO, naming the source it read, the guarantee it does not carry, and the verb that tells a missing object from a corrupt one. seal-only was rejected with its reason recorded: this verb exists for why is this file not visible after a pull, which is exactly the case where the object legitimately is not here - a withheld forge object, a mid-pull absence - so refusing there would delete the verb purpose. healthy output is byte-identical to before. conflict_side loses its fallback rather than gaining a guard: one object read returns both the seal visibility and the open verdict, and sealed_to_us is extracted so the two doors cannot drift about which errors mean sealed-to-you. the sizing is stated rather than flattered - NO observable answer changes on any state reachable today, because old and new both propagate the rot, and what changed is REACHABILITY: the mutation restoring the pre-fix shape reproduces the defect exactly, a truncated object rendering as internal. ADR 0012 gains a row for each site, and its visibility_of row is CORRECTED, having claimed conflict_side as a live caller. the wire-format option is neither taken nor touched: folding visibility into the change id is a FORMAT_MAJOR decision, the ticket ruled it outside an AFK warrant, and the enforcement question is written up to be filed rather than begun. seven mutations, every pin asserting a VALUE - a reveal instant, an oid, an error code - and never merely that something failed, which is the trap #1578 pin fell into. and the vacuous-filter trap fired once and was caught by reading the NAMES rather than the word: a filter on embargo printed 22 passed, and none of those were the three new render tests, whose names do not contain it (#1581)
74526297 · dbf3dbe6…diff - the precondition that was prose becomes a TYPE AT THE SEAM, and both halves take that answer rather than only the cheap one. the vouching type moves into the crate that holds the door, with four private fields, and the door takes IT instead of a bare path set - so the only thing that can widen the set in a shipped build sits DOWNSTREAM OF ITS OWN GET. and that is not a second read, it IS the read the producer was already doing: the live arm was already exactly that expression, so the engine now RECORDS the open rather than the CLI asserting afterwards that one happened. one get either way. the same-change half, which already had a mechanism but one crate away from the door it protected, moves across the seam and gains two clauses NOTHING checked anywhere before - the reader and the clock - and it compares rather than records, returning the empty set on mismatch rather than an error, because a bisect midpoint must still pay the walk in full. the plan can no longer hold a set and a measurement that disagree, for the simple reason that it no longer has two fields. a THIRD precondition falls out structurally rather than being written down: only the live arm can vouch, so a plan built from a both-recorded spec now arrives EMPTY rather than full and unearned. the door stays public and that is now a DECISION with its reasoning in three places rather than an omission - making it private, as its neighbour three lines below already is, would close the hole for one crate while the public trait keeps surfacing content, so an embedder would lose the skip and nothing else. and the cross-key door is split out to take NO vouching parameter at all, because a move labels and never vouches: the hazard stops being expressible instead of being filtered by a dead guard. the mutations bought a REAL HOLE rather than confirming the work. two of them survived the first round, because every test built its vouch THROUGH the door, so the insert-side reader and clock checks were SHADOWED by the door-side ones - one identity open could fill another identity set, and an open at one clock could back a set labelled another. an isolating test was added, and both redden now. the counters are MEASURED rather than argued: an identical probe over surface and status at three sizes, run against a byte-identical copy of the unmodified tree, reads the same on every gated counter - with the honest caveat that the allocations column is VACUOUS, since that probe installs no counting allocator, so it is not claimed. and the single claim resting on the type system alone is NAMED as such: that no other crate can widen the set is held by the compiler privacy rules and not by any test, because this workspace has no compile-fail harness to assert it with (#1710)
aca3a188 · dbf3dbe6…diff - the trait surface stops being a second materialize loop and becomes a one-line adapter over the walk loot surface runs, so a withheld object no longer fails it and the land gate counters fixture now measures that walk: the two promoted walks share one classifier deciding which failed reads are invisibility and which are faults, a burned read kept distinct because the surface walk labels it rather than counting it as sealed. a new test drives a withheld object through the trait and went red with object not found before the change, and red again when the adapter was reverted or the classifier arm broken. every gated counter reads the same before and after on the gated fixture, as expected for a fixture with no view and every object held. the 1703 sentence calling the gate a different loop is corrected to what it still cannot see, and the stale two-of-five counts in measure.rs, work_counters.rs and the ADR 0073 heading are replaced by what defines each set rather than a new number (#1927)
f930875a · dbf3dbe6…diff - loot lanes now reads the shared graph and scans the object directory once per board draw rather than once per registered lane: a new DagRepo::load_positions_from reads the position files of every lane first and the shared half once after them, so each row keeps the read order load_from_needing documents while every row reports against one moment of the shared store. that is the decision the ticket asked for, recorded on peek_lanes with its reasons, among them that option c cannot help because a row needs both the graph and the objects. a single load now reads the working change before the graph, the order that doc states. on a scratch repo with twelve lanes and a 4.9 MB graph the board went from 237 to 190 ms, paired in both orders with loot status flat as the control, and its porcelain and json output is unchanged apart from heartbeat age. a new board test relating the store file reads of the board to those of its rows built one at a time went red on the unchanged code and with per-row loads put back (0 passed, 1 failed each), and an engine test comparing a batch load with solo loads went red with the mixed-store refusal removed and with one position half reused (0 passed, 1 failed each). the perf gate reads no move, CONTEXT.md follows, and the workspace suite is green (#1782)
caed7d82 · dbf3dbe6…diff - review sweep 8 fix-up: the owed relay push recovery now lives once, in a new workflow.md section, paying an owed relay push, which covers relay=FAILED and relay=REFUSED in both shells with the position on every command, the build job cap and the in-flight guard, and states one done-when: the push succeeded, or its format skew is recorded on the issue as owed to the release relay redeploy. land-change and afk-loop point at it instead of carrying their drifted copies, afk-loop refers to that item and to its sections by name rather than by step number, as AGENTS.md now does too, and its when-it-goes-wrong list gains relay=FAILED. probe_pusher now says why a report was refused, with the parser message or the version check, and keeps the pre-1776 explanation for output that does not open a JSON object, and the pusher= docs say UNKNOWN means the report was not read. the new pin and the tightened pre-1776 pin went red before the change (0 passed, 2 failed), and red again with the pre-1776 arm removed (9 passed, 1 failed) and with the pre-1776 explanation given for the new pin reports (9 passed, 1 failed). LanePeek becomes a struct and a dead unwrap_or_default becomes an expect, the 4.2 s lanes figures in heads.rs and lanes.rs are labelled as history, the PositionHalf doc credits the reorder with matching the documented load order, the ObjectIndexScan doc no longer names its CLI consumer, ADR 0028 says merge_tips reads tree_at itself, and rewrap leftovers in land-change, format.rs and CONTEXT.md are joined. the workspace suite is green (#1983)
2ee76c18 · dbf3dbe6…diff - every bare remove_file of a loose object in loot-core fixtures now waits for the absence it asserts, and the set that does is a census rather than a sentence: #1596 measured that a handle held without FILE_SHARE_DELETE makes remove_file return os error 32 and leave the file, fixed the sites in negotiation.rs and swept no further because its aggressor had reddened nothing else, and #1897 asked whether the rest were safe or merely unexposed. they were unexposed. the aggressor was rebuilt and lives in the tree now as testkit::hold_without_share_delete, and with it holding one handle across one removal the whole family went red site by site rather than statistically: the selection reads 1 passed, 10 failed, every panic os error 32, with the already-fixed negotiation site under the identical hold as the green control, and the second removal inside accept_loss measured on its own with the first hold lifted (0 passed, 1 failed). with the helper at all of them the same selection under the same hold is 11 passed. the helper moved from negotiation.rs into testkit keyed on the OBJECT DIRECTORY rather than a store directory, because the object_store.rs fixtures are an object directory with no store around them, and #1899 exit rule and the three-way scan answer came with it unchanged. the ticket list was wrong in BOTH directions, which is the finding: it named sites a realistic scan does not reach and MISSED two of the most exposed, the live-repo removals in engine.rs and custody.rs that are #1596 own shape; under an emulated indexer scanning the fixture roots, 50 paired interleaved rounds, the bare tree is red at a removal in 36 rounds over four sites, two of them the ones the list omitted, and the converted tree is red at a removal in ZERO. what is bare and why is now derived: tests/loose_object_removal_census.rs reads every removal whose statement or whose binding names a hex-encoded address out of src and tests, and names the one home, the aggressor pin own deliberate bare arm and the two PRODUCTION removals, which return their error rather than panicking and are right to. run against the pre-change files the census names exactly the nine test functions that were converted, the tenth site being the one it states it is blind to, a removal by directory entry, which was given its address so it could take the helper and so the census could see it. mutations: a bare removal put back single-line, multi-line and through a let binding reddens the census each time naming that function (2 passed, 1 failed each); a name dropped from the expected set reddens it (2 passed, 1 failed); blinding the address needle reddens the classifier fixture and the not-gone-blind guard too (0 passed, 3 failed); giving the aggressor FILE_SHARE_DELETE reddens the new pin because the bare removal then succeeds (4 passed, 1 failed); dropping the named-first precondition reddens the should-panic pin (4 passed, 1 failed); and the pre-#1899 scan spelling reddens the moved scan pin (4 passed, 1 failed). docs/agents/workflow.md flake section carries the rule, the aggressor and the census, and stops saying the fix ends at one file. a latent write-side exposure was found on the way and is NOT fixed here: save_objects_loose renames its staging file without store.rs retry, so a scan holding the stage makes an ordinary save fail with os error 32, which is the create side #1596 explicitly left alone. every edit is inside a cfg(test) item or a doc comment, so no production byte moves and no perf gate is owed. the workspace suite is green (3843 passed over 120 binaries, 7 ignored) (#1897)
b8eb322d · dbf3dbe6…diff - a superseded head is listed, not counted: loot log and loot heads stop counting a version superseded under ADR 0032 as a head, so a clean loot move --before no longer prints 2 heads - diverged; finalize then converge for a divergence that does not exist. the rule is spelled once as Liveness::is_superseded, which the head partition drops stale by, loot heads prints its superseded annotation by, and the counts now read: history() routes, counts and sections over the heads that predicate rejects, through a new LogGraph::retain_heads that re-indexes reachability into the kept heads, and the heads header counts the rows whose superseded flag is false and says how many superseded versions follow, still listed under the annotation. the head set itself is untouched - the stale head stays in .loot/heads until a converge retires it. reproduced first through the spawned binary on the lane tree at #1837 exactly as the ticket describes, and measured on move --onto too, where the stale head is not empty (B stays reachable only through it), which is why the rule is superseded and not own == 0. status never counted heads and never marks this with !, pinned rather than assumed. every machine shape is byte-identical modulo ids except the rows that were wrong: log --porcelain loses its two H rows and the working-change * row the fork view printed, log --json goes heads null, and heads --porcelain, heads --json and all three status shapes do not move. red first, four ways: the count rule undone reddens the workspace pin and the spawned reorder pin (0 passed, 1 failed; 1 passed, 1 failed), the header count undone reddens the heads pin and the spawned pin (6 passed, 1 failed; 1 passed, 1 failed), every head uncounted reddens the positive controls while the negative pin stays green (5 passed, 3 failed; 1 passed, 1 failed), and the predicate blinded reddens the partition, the annotation and the count together (6 passed, 1 failed; 0 passed, 2 failed). the #1477 production-shape pin moves to the new header, and ADR 0032 and CONTEXT.md record the amendment. no migration, no wire or format byte moves, and no forge or relay behaviour moves, so this owes no deploy. the workspace suite is green (3887 passed over 122 binaries, 7 ignored) (#1839)
8e6c0841 · dbf3dbe6…diff - attest_all no longer has a fallible step that can fail part-way through its write loop. the keypair lookup and the signing move into the judging phase, and the write is one call to a new DagRepo::add_attestations, which verifies the whole batch before storing any of it, so a refusal leaves memory as it was found and not only disk. the old loop fetched the keypair and verified record by record, and only the unreached persist kept an earlier record off disk. the sentence saying nothing can reach the lane one record at a time is restated in workspace.rs, verbs/attest.rs and CONTEXT.md, since attest is public and is that slice with one record. the tag seam pin now also reads the store back after the refused act. red before the fix, the engine batch pin with record-as-you-verify (0 passed, 1 failed); red under mutation: the batch verifying only its first record (0 passed, 1 failed), and attest_all recording each record as it is judged (0 passed, 1 failed, on the in-memory assertion while the disk read-back stayed green). 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 (3926 passed over 123 binaries, 7 ignored) (#1861)
4dbc7c83 · dbf3dbe6…diff - a lander can no longer read a pre-land finding as the refusal whose recovery is to change nothing, and the gate claims no verdict only when neither run reached one. the no-verdict refusal opens on a heading of its own, pre-land cargo test: NO VERDICT, and a finding keeps the old heading and never says in words of its own that the compiler did not answer, so the crash it quotes from the first run cannot be mistaken for the other refusal; land-change keys its two recoveries on those headings. a build failure and then a crash is now a finding, not no verdict: the first diagnostics are the only verdict either run reached and the crash neither confirmed nor cleared them, so telling the lander there is nothing to fix would claim more than the gate knows. a re-run that cannot start cargo now says a re-run happened, leaving the merged-tree remedy to #2066. afk-loop reads the verdict line through land-change instead of keeping a second list of values, the --skip-tests block stops stating the CI net as a fact and names ci/local.sh for when the job does not run, and the claims that the job is only cargo test are narrowed to what ci/test-main.sh runs. red first: a finding carrying the no-verdict words, a build failure and a crash judged as no verdict, and a re-run spawn failure that never said a re-run happened (0 passed, 3 failed). red under mutation: the finding rendered with the retry evidence (2 passed, 1 failed), the no-verdict guard dropped from the first run (1 passed, 2 failed), the re-run note dropped from a spawn failure (2 passed, 1 failed), and the deadline dropped from the new wire pin (0 passed, 1 failed). the ticket premise about a deadline ceiling is corrected: in reqwest blocking a request timeout replaces the client per-wait timeout, so a deadline is not undercut by the transfer budget, and a local server pins that. the smells are taken: the fake spawner stdio choice is an enum, wire post takes a budget as get does, Endpoint get sends in one place, the transport remedy sentence is written once, add_attestation is add_attestations with one record, and CargoTestFailure Other is named Finding. 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 (3946 passed over 122 binaries, 7 ignored) (#2071)
bb8acc52 · 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 - both grant doors weigh the seal before they choose a lane, so an early-releasing sender can no longer file a live embargo key into a receiving position Keyring: #2205 asked for every premise to be re-verified on the tree first and each of them held, a tag-1 frame asking no embargo question at all and a tag-3 grant filing by the frame reveal_at, which is the sender word, so at a clock of 0 against a seal recording Embargoed reveal_at 9_000 both doors left keyring.holds true and escrow.holds false. the framing question is answered where a reader meets it rather than inherited by proximity: which lane a key waits in is decided once, at the door that files it, because a route carrying a key from one position custody to another reads one lane and writes the same one and Escrow::flush promotes but never demotes, so a filing is the answer every position that key later reaches inherits with nothing re-asking, and ADR 0007 states its guarantee over identities rather than over one door, which makes a door that does not ask a gap in it rather than the Escrow scope. one rule in one body, because two doors asking one question in two places is how they come to disagree about it: a grant-borne key is staged until the latest instant any party to the handoff named, so a party added later is another term of the same max rather than another arm. a disagreement withholds rather than refusing or dropping, refusing costing a recipient a grant over a claim they did not write with a remedy that is not theirs to act on, and taking the seal instant alone being strictly weaker, because a grantor own delay over content under no embargo is ADR 0027 timed deposit and a seal contributing 0 would release it on arrival; withholding costs only the wait the seal already imposes on every reader of those bytes, sealed::open embargo gate refusing them at that clock whichever lane the key sits in. the seal is read back from this store and never off the arriving bundle, because the address does not cover vis and put is first-write-wins, so weighing the incoming copy is reading the sender word a second time under another name, pinned on a fixture whose lying copy keeps the address and is therefore a dedup. the two doors get one answer for two reasons and the difference is recorded: tag 3 had a recorded cooperative-defence posture and this applies it to a second party, which is why ADR 0007 takes a #2205 amendment, while tag 1 had no decision at all, existing to bypass the entitlement question and having taken the embargo one with it, which sealed::open first gate separates in four words, time not identity. the cost is measured rather than assumed: one object get per key the door files and zero disk reads where the grant carried the object its key is for, eight keys costing eight gets beside a ninth address the same bundle carried no key for. red under mutation, counts read each time: the seal term dropped from the staging max (654 passed and 3 failed), the tag-1 door reverted to filing into the Keyring (654 passed and 3 failed), the frame term dropped (655 passed and 2 failed), the staging comparison widened to greater-or-equal so an undue key stages (653 passed and 4 failed), the seal weighed off the bundle copy rather than this store (654 passed and 3 failed, the held-seal pin naming the lane), the question moved ahead of the key guard (656 passed and 1 failed, object_gets reading 17 where 8 belongs), and each of the two fixtures inverted as a vacuity control (656 passed and 1 failed, the control firing), each restored to 657 passed and 0 failed. ADR 0012 takes a twelfth amendment recording that this class is a sibling of its own, the disagreement being with a frame rather than a tree entry so no census row moves, and that keyring.holds is still not a bound, a key some door filed before this change being in .loot/keyring still. no migration, no wire or format byte moves and no host behaviour moves, but which lane a grant-borne key is filed into moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4131 passed over 132 binaries, 8 ignored) (#2205)
ec80ad4d · dbf3dbe6…diff - the sync ingest door weighs the seal this store holds rather than the one that arrived beside the key, so a tag-0 bundle spelling a weaker tier at an address this position already owns can no longer file a live embargo's key into the Keyring: #2212 asked for item 1 to be demonstrated before it was repaired and the reproduction read exactly as the ticket claimed, bob holding oid sealed Embargoed 9_000 and a Sync frame carrying a byte-identical object that spells vis Internal beside the real key leaving bob's Keyring holding that key at a clock of 0, which is the state #2205's own new pin forbids reached by changing the frame tag, no plaintext escaping because sealed::open's header gate still refuses. the same read carries grant_ids and that half was demonstrated too, a copy spelling the ANYONE marker over a held Restricted seal getting a key past the entitlement filter #864 built, so every question this door puts to a seal now goes to the copy that will stand at the address. it is asked before the put while the arriving object is still in hand, because first-write-wins makes the held copy the standing one only where the store holds the address at all, so the cost is zero store reads on a fresh address and one on a dedup, which is the read the key verification already owed and now answers the seal's questions with its own; the already-held guard consults both lanes, so the lane the first door chose is the one that stands. the prose is the harder half and the lesson is sharper than do not list members: the sentence that failed was in the correct derived shape, the set of them is Keyring::insert's callers which the compiler enumerates, with a hand-maintained count welded on in the same breath, and the count is the half that was wrong, so every replacement names what decides membership and stops there, at ADR 0007's amendment, escrow.rs, CONTEXT.md, custody.rs twice, ADR 0012's tenth and twelfth amendments, negotiation.rs and the grant-door pin, and escrow.rs's headline stops claiming that no route moves a key between lanes when flush is one and a grant is a new filing at the recipient rather than a carry. secondary items: the stale pin citation and the now-false claim around it, spawn.rs's three false statements about the orphaned child, the unproducible-seal fallback recorded as releasing nothing only at the instant it files, expires_at declined as a term of the staging max with the reason at the code, the demotion refusal naming which of the two recordings fired, the census group sentence that named its members, ADR 0012's push qualifier at the tip with no want, the ingest cost fixture given a publishes-nothing control, workflow.md's three refusals derived from CargoTestFailure and the PRE_LAND constants, a usize subtraction restated as a sum so the sentence beside it can print, and orchestrator.rs's tombstoned pin names declined with the reason. red under mutation, counts read each time: the vis term reverted to the arriving copy (659 passed and 1 failed), the grant_ids term reverted (659 passed and 1 failed), the already-held guard narrowed to the one lane it writes (659 passed and 1 failed), the seal question asked through a second store read (659 passed and 1 failed, object_gets reading 8 where 0 belongs), the lying sync copy made a different object (659 passed and 1 failed, the vacuity control firing), the lying grant ids made to agree (659 passed and 1 failed, the second vacuity control firing), the publishes-nothing control inverted (1353 passed and 1 failed), the refusal made to say both either way (1352 passed and 2 failed) and the ingest cost relation moved by one (1353 passed and 1 failed), each restored to 660 and 1354 passed with 0 failed. no migration, no wire or format byte moves and no host behaviour moves, but which lane a sync-borne key is filed into moves on the client, so this rides the next release and owes no deploy. the workspace suite is green (4140 passed over 132 binaries, 8 ignored) (#2212)
c643aadc · dbf3dbe6…diff - the arc's one self-contradiction is gone and its newest list shape is answered wherever it stands: #2214 asked for every premise to be re-verified on the tree first and items 1-5 all held, and two sites the ticket did not cite held with them plus one list that was already stale — ADR 0007's own #2205 header carried both the hand-maintained variant count and a second copy of the false carry claim, and the impl doc over the store door named the ingest paths where the chokepoint property would have named itself, missing the tag-1 grant door and put_published. the self-contradiction is ADR 0012's twelfth amendment, which #2212's commit message lists as corrected and which it edited one clause later, still saying a crossing between positions carries a key into the lane it is already in while ADR 0007 and escrow.rs both say a grant is a new filing at the recipient that reads the seal and answers for itself; the clause is deleted with the false reason named rather than quietly dropped, because the surviving conclusion has to be seen standing on what an older binary already wrote and on the absence of any route that takes it back out. the sharper lesson is item 4's and it is sharper than do not list members: the sentence that failed was already in the correct derived shape, and a count welded on beside it in the same breath was the half that went stale, so every replacement says what decides membership and stops there — store's call sites are the ingest paths and the compiler enumerates them, file_granted_key's call sites are the Frame variants that carry a grant, and the headline over the grant-door pin stops counting the doors it drives. secondary items: the store door's promise that a garbage key is rejected rather than filed is narrowed to the held-address arm with the fresh arm's literal true named as vacuous where a reader meets it, the carry definition stops being true by construction and says what a carry does to the lane so its own falsifier lands on that axis, the three co-travelling seal facts become a Weighed struct whose third field is named for what both arms make it, embargo_reveal_at delegates its Visibility half to embargo_instant so Embargoed is destructured for an instant in one place and the zero belongs to whoever asks for a number, the demotion refusal spells every bool pair and returns the name from the match so no arm asserts a pair it cannot be reached with, embargo instant becomes the glossary's reveal_at, the 127-character ADR line is rewrapped, and the one-address sync fixture preamble collapses into sync_of_one. red under mutation, counts read each time: the entry arm of the demotion refusal made to say the seal (2 passed and 1 failed), embargo_reveal_at's collapse moved off zero (655 passed and 5 failed) and the fresh arm's unrefuted made false (523 passed and 137 failed), each restored to 3 and 660 passed with 0 failed. no migration, no wire or format byte moves, no host behaviour moves and nothing an operator or a client can observe moves, so this rides the next release and owes no deploy. the workspace suite is green (4140 passed over 132 binaries, 8 ignored) (#2214)
ea968e98 · 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 - the visibility gate goes on reading the whole object and the ticket is refused on a ceiling, because a binary with the read deleted outright still reads one loose object file per recorded path: can_open consults facts that sit beside the ciphertext rather than inside it, so reading less looked free, and the measurement says the read does not belong to this seam at all. a knowingly wrong body answering from the store index and the keyring alone, reading no object bytes, is strictly less work than any header only spelling could do, and over 200 paths of incompressible bytes at 1 KB and 16 KB and 128 KB it moves loot status from 14.13 and 20.57 and 67.91 ms to 14.21 and 20.84 and 66.73 ms and loot surface from 16.64 and 23.11 and 59.09 ms to 16.58 and 23.18 and 56.16 ms, a spread that runs in both directions and peaks at 5 percent on the largest fixture. THE COUNTER IS WHAT MAKES THAT A REFUTATION RATHER THAN A QUIET READING: object_disk_reads reads 200 on every one of those twelve readings, before and after alike, while object_gets falls from 401 to 201 on status and from 600 to 400 on surface, so the span really was deleted and the file reads moved rather than went away. they move to the pass that wants the bytes - same_content behind status and the clobber guard behind surface - which the read memo makes free today and which would pay for the whole tree if this call stopped filling it, and a header only body would add its own opens on top of that. the signal the ticket opened with is therefore not added either: the read counter that exists already settles the question, and a byte counter would have read the same total under the ceiling and a larger one under any header only body. so the code is UNCHANGED and what lands is the reading, recorded on can_open where the next hunt reads it before refiling. one list class defect is corrected beside it: the sentence naming two further sites that could move to this predicate named a merge site that cannot, since the readability question there sits on the theirs side of a cherry pick delta restriction and its own comment says the content must be read to re-seal it, and the sentence now states what decides membership instead of naming members. no mutation proof, because nothing was fixed and no pin was added, and the measurement controls stand in its place: the get counter moves, so the instrument is not blind to the span the ceiling deletes, and restoring the pristine body returned both counters and both timings to the landing band. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4174 passed over 132 binaries, 8 ignored) (#2223)
4ab1afee · dbf3dbe6…diff - the residual #1888 could not settle is settled by counting instead of timing, and the answer is that the clean pair was never in excess: a new measure half counts allocations over the whole of ingest_shared_lineage and splits the call into the eager graph read and the walk and splice, two bands that sum to the region EXACTLY, counter by counter, because the region is those two doors with a map lookup between them - the property no stop instrument can have, and #1888 recorded its own stop reading an early return as costing more than the work it skips in six rounds out of six. at the width the half pins, the pre-#1572 clone spelling put back reads 6488, 12512, 24552 and 96803 allocations at depths 8, 16, 32 and 128 against 4289, 8113, 15754 and 61606 shipped: a removal of 1.37 per manifest entry at every depth in that table and of 33.9 to 36.4 percent of the region, both arms linear with per-entry slopes agreeing to four significant figures, so the same fit carries the depth #1888 measured at to about 281000 removed allocations out of 771000, or 36.5 percent of the times the region reaches the heap. THE TIMED BAND WAS NEVER A CEILING ON THIS CHANGE: the walk and splice the stop instrument left 16.2 ms, a tenth of the region by time, makes 37 percent of it by allocation, so a clean delta of 13.6 percent sits BELOW the share of the work the change removes rather than above it, and nothing is left over to explain. the published magnitude does not move, because a count does not convert to milliseconds and this instrument prefers none of the three timings - what is withdrawn is the reason to distrust the largest of them. no opt-in half is added and Fixtures::gated is untouched, because a land builds release, where the allocation control refuses rather than reporting a false zero, so this reading is a test one taken by hand: its own binary installs the allocator no library may install, opens on require_allocation_counting, and carries an ignored sweep. HUNT-PERF gains the marginal-cost trap it was missing, with the measured instance where a discarded second read reads over 100 percent of the region it is supposed to be a fraction of, the band decomposition that replaces it, and the note that a summing decomposition is still a perturbing one. red under mutation, counts read each time: the clone spelling restored failed the splice-band pin (2 passed and 1 failed, 190 allocations against 73 entries), the pool read moved outside its band failed the sum control in all three (0 passed and 3 failed, 65 against 272), the global allocator removed failed all three on the control itself (0 passed and 3 failed), and a fixture ignoring the declared width failed the scaling pin (2 passed and 1 failed, 0 entries added), each restored to 3 passed. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4177 passed over 133 binaries, 9 ignored) (#1890)
4c6b019a · dbf3dbe6…diff - the attribution the #2223 refutation asserted is now measured and the conclusion stands, because a nested count region says which span the surviving reads belong to: with this body live the open inside vouch_if_on_disk reads 0 files over 200 paths, and with the ceiling in place that same span reads 199 on status and 200 on surface, so the reads move rather than go away and a header only body would add its own opens on top of them. the size axis gains the fourth column the ticket asked for and the effect does not run away with it: at 1 MB objects over a 210 MB store the ceiling leads by about 8.5 ms on either verb, under a fiftieth of the verb, against 14.24 and 20.91 and 69.00 and 455.60 ms for status and 16.67 and 22.92 and 67.37 and 461.02 for surface. THE 3.3x DISCREPANCY IS RECONCILED AND THE GUESS ABOUT IT WAS WRONG: the loose store weighs 213,800 and 3,285,800 and 26,223,400 bytes here, the three figures the ticket body records, to the byte, so both arms ran the same incompressible fixture and compressibility explains nothing; what differs is that the ticket timed the process, which reads 22.7 and 29.8 and 77.6 ms over these same fixtures and meets it at the two smaller sizes, while its largest reading of 226.2 ms reproduces in neither arrangement against seven repetitions spanning 69.00 to 71.96 ms and a first unwarmed reading of 71.13. so the 11x move it was filed on rests on one reading, and the move these fixtures carry over that range is about 3.4x. #2226 gains the depth axis its own cost model lives on, swept without touching MISSING_DEPTH so no fixture and no workload_id moves: at width 200 the walk is linear in depth over 1.16 and 4.66 and 18.49 and 74.04 ms at depths 8 and 32 and 128 and 512, the probe deleted binary reads 1.08 and 4.38 and 17.38 and 68.82, and the probe share does not move across that sweep, which it would not, because probes and manifest entries are both O(changes x paths). that doc now cites #2240, records what the half is, and says the probe is two map lookups rather than one. ONE SWEEP ITEM IS REFUTED RATHER THAN FIXED: the short line in gate.rs is not a rewrap leftover, it is short because the 33 character intra doc link after it does not fit at this column norm, and the same file carries a 44 column line for the same link higher up. the cheapest item had the only seam and it is pinned: the WORK_COUNTERS exclusion table read as the excluded variants and nothing else, which is the definer #2234 sent a reader to, and it is now read back against Work::ALL filtered by gate::COUNTERS. red under mutation, counts read each time: graph_sorts dropped from gate::COUNTERS failed the new pin (0 passed and 1 failed, three variants named against four owed), the PolicyParses row deleted failed it (0 passed and 1 failed, two against three), and the table header renamed so the scan reaches no rows failed the anti vacuity guard (0 passed and 1 failed), each restored to 1 passed and the file to 12 passed. the two ADR 0073 enumerations are deleted rather than corrected, the CONTEXT and gate paragraphs are reflowed with identical word streams, and no code outside a test changes, so this owes no deploy. the workspace suite is green (4178 passed over 133 binaries, 9 ignored) (#2242)
d6bf7f55 · dbf3dbe6…diff - the figure the #1890 verdict rests on is now a column in the table it was missing from, taken the way that table was taken: the pre-#1572 clone spelling put back and the ignored sweep run once per arm, which reads the splice band at 41.3, 39.2, 38.0 and 37.2 percent of its own region on the before binary against 11.2, 6.2, 3.4 and 1.3 shipped, so the 37 percent the conclusion turned on is readable from the tree instead of from a session transcript, and the re-take reproduced every before and after cell already there to within one allocation. THE VERDICT IS NARROWED TO WHAT THE COUNT SHOWS: withdrawing the band ceiling and confirming the clean delta are separate acts and only the first was done, because a count does not convert to milliseconds - what is retired is a contradiction between two readings rather than the spread among the timings, and the published magnitude stays where they left it. two precision defects in the #2242 retraction go with it: the move over that size range is about 4.8x in the call and about 3.4x as a process and the sentence now names which, and rests on one reading is withdrawn because #2223 records no repetition count for its whole-verb rows, so what is known is that the figure does not reproduce and not how it was taken. the sweep doc had its axes inverted against the code under it and now states the property instead, that the shapes fall into runs holding one axis while the other moves, and the list class is answered for the third time in this run by replacing the two named untimed controls with the questions that decide membership. the duplicated in-loop guards are extracted: prove_the_position_has_not_ingested and prove_the_tip_was_spliced are asked by the timing and the count alike, each refusal written once and true of both. the named positive control is refused rather than built, because an in-tree knowingly worse arm means a wrong body in production code, and the before column now published stands in its place. sdk records the node type-stripping floor its .ts import imposes in engines, where npm reads it. red under mutation, counts read each time: the position control inverted failed all three allocation pins (0 passed and 3 failed, naming a position holding 1 change), and the splice control fed a false answer failed all three on its new text (0 passed and 3 failed), each restored to 3 passed. no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4179 passed over 133 binaries, 9 ignored) (#2247)
c8b11725 · dbf3dbe6…diff - the tutorial interrupt red #1916 read as a removal that lied is a tree made again after the sweep: on the linux runner std remove_dir_all answers success only once the root is gone, the captured transcript prints the first prompt ahead of the handler line, and walk, still running on the main thread beside the handler, opened with a create_dir_all over alice that minted the root back before ask beat the heartbeat into it. two delays injected into that sequencing reproduced the red and its alice, heartbeat remnant on this desktop with confirm_gone in place, so that check alone would not have closed it; Sandbox::create now makes alice before the handler is armed, the same two delays pass, and a unit pin drives the sweep, the walk and the beat in that order. what stays open is said at the fix: other writers, the relay thread and the loot subprocesses among them, can still put a path back mid-step. the CI run recorded the two readings and not the sequencing, so the mechanism is established by reading and by the reproduction rather than observed there, and workflow.md stops calling the report and the look one moment. the loot-s3 client keeps its 60 s budget for want of any measured rate to its bucket, but a request that runs out now names it and what it covers, where reqwest said error sending request or error decoding response body with no word of a deadline. the sdk doc stops claiming the native policy: the formula is copied, native allows the send budget between answer chunks where the sdk allows idleMs, the rounding differs, wire.rs now points back at the copy, and the unit test reads the floor from its constant. smaller: the engine.rs verdict compares the band against the band, the measure.rs refusals state a property instead of counting callers, the fixture generator import note sits at the import, and transportFailure takes a role rather than a string. red under mutation, counts read each time: create_dir_all put back in walk (11 passed and 1 failed, holding alice and heartbeat), the s3 timeout naming removed (12 passed and 2 failed), the sdk upload rounded down (6 passed and 1 failed), each restored to 12, 14 and 7 passed. no migration and no wire or format byte moves; the forge and relay deployables word a timed out object store request differently once next deployed, and no code in the tree matches on that text, so this owes no deploy. the workspace suite is green (4192 passed over 133 binaries, 9 ignored) and the sdk suite is green (137 passed over 13 files) (#2254)
bdeb6a0c · 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 - 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 - the review-sweep fix-up over #2280, #2281 and #2282. the closure-walk pin stored x/abc after x/ab, which a random address beginning with c completes, so its stored-once control failed 5 runs in 400; it now uses y/abc and read 400 of 400 green. the spliced-node pin claimed the tree a later save writes back, but that save writes the copy the graph file already holds, since the rewrite inserts what it reads back first and ChangeGraph::insert keeps the first node for an id; it now saves into a store with no graph file, the splice comment says why, and a splice that blanks each spliced manifest goes red on that assertion (0 passed, 1 failed). decided and pinned: an ingest past a key or holder name that is not UTF-8 in a node it does not splice returns Ok and the next rewriting save refuses, kept because the open already defers such a file and stepping every pool node reads 13.8 to 14.4 ms beside a 54.4 to 55.0 ms deferred read of this repo graph file (red when the check scans the whole pool and when the save fallback is dropped, 0 passed and 1 failed each). measured with a tracking allocator over that 85.7 MB file: the rewrite union holds 88.2 MB deferred against 222.5 eager and peaks at 193.9 against 382.0, both freed when the save returns; a pool keeps one copy of the file per read that splices, 85.7 to 342.7 MB over one to four reads against 1.2 to 6.7 eager, recorded at the splice. the move-not-clone property lost its count pin to deferred manifests (a clone costs about two allocations per spliced node, 78 against 65 at the pin shape) and is pinned by the pool ids instead (red under the clone, 0 and 1). the closure walk doc names the holder-name case where it answers and the decoding walk panicked, pinned (red when the ascent check also steps holders, 0 and 1). each mutation restored to green. list-class sentences in HUNT-PERF and the loot-perf fixture rows now name what defines the set of graph file reads, and a rewrap leftover and a stale keys_ascend reference are fixed. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4285 passed over 137 binaries, 11 ignored) (#2287)
a8c863e3 · dbf3dbe6…diff - loot revert --continue and loot cherry-pick --continue now replay the stopped pick instead of closing the stop, on the answers loot resolve gave to that stop and on nothing else: the in-progress record gains the version the verb applies, the conflicts its stop recorded and the answers, resolve writes an answer there only when the conflict it settles is the one the stop recorded at that path, and change_delta_merge takes only the answers its caller hands in, none from an ordinary run, so the repo settled ledger is still never read by a pick (#744). a path whose content moved after it was answered is asked again rather than merged, a path holding the answer bytes at another address keeps it, a replay that stops again keeps the record restore point, and a record written before this closes as it always did. ADR 0080 gains the #1986 amendment deciding the three questions and recording, measured through the binary, that move, squash and apply-patch stop again after a resolve for reasons that are not a ledger and that apply and merge have nothing to replay; CONTEXT.md, the resume module and the usage note say the same. the resume runs to completion through the binary on the home position, on a pinned primary and in a lane. red under mutation, counts read each time: the repo ledger handed to the resume (0 passed, 1 failed in the stale-entry pin), the repo ledger handed to the three-way (0 and 1 in the loot-core pin, 0 and 1 in the re-run pin), a moved path left to the three-way (0 and 1), the address test alone (0 and 1 in loot-core, 0 and 1 through the binary), an answer keyed on the path alone (0 and 1), the record sentence worded by verb (0 and 1), the replay branch emptied (0 and 4), the re-stop taking a new restore point (0 and 1) and the usage note dropping revert (0 and 1), each restored to green. the record gains a trailing section, no format byte moves and there is no migration, so this owes no deploy. the workspace suite is green (4302 passed over 138 test binaries, 12 ignored) (#1986)
8bd8e53c · dbf3dbe6…diff - the review-sweep fix-up over #2287, #2283 and #1986. a resumed pick now asks again about a path its stop asked about when the record holds no answer for it, rather than handing it to the three-way, which merged over the resolution and put back the edit it rejected: change_delta_merge takes the stopped record whole and takes every question on it out of the three-way, keeping an answer only where the line still holds it, so a resolve that recorded no answer (a loot from before #1986, or a failed answer write, which the workspace comment now truthfully says costs a re-ask) re-stops with nothing applied, keeps the restore point, and lands once the question is answered again; refusing --continue was weighed and rejected in the ADR 0080 #2291 amendment, since nothing would be left to resolve. the replay reads its direction from pick::SPECS, which now pairs each spec with it, and a record naming a version under a verb SPECS does not hold refuses instead of replaying as a cherry-pick. the usage note names no verb, so each stopping verb line is true of its own verb, and its test now fails if the note names a verb that replays. a cherry-pick replay to completion is pinned through the binary on the home position, a pinned primary and a lane. false sentences fixed: the in-progress operand reader, the pick stop comment, the resume test doc link, the continue_clause list, the CONTEXT.md in-progress entry and HUNT-PERF on graph file reads; rewrap leftovers in fixture.rs and engine.rs are fixed, and the replaying tests keep their resolution file outside the working tree. red under mutation, counts read each time: questions taken from the answers alone (16 passed and 1 failed in the loot-core pin, 31 and 2 through the binary), an unknown verb replayed as not inverted (20 and 1), the note naming cherry-pick and revert again (19 and 2, the second the census the usage lines no longer matched), each restored to green. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4309 passed over 138 binaries, 12 ignored) (#2291)
812c48c7 · 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 - adr 0004 is now asked of what a repo encodes to send as well as of what it persists: plaintext_equality_census gains a wire half, which keeps per build the payloads its sent function has the fixture repo encode through the public Workspace doors loot bundle and loot push call (the full bundle, the /stow batches a push to an empty host sends, closed at one object a batch so the first-batch delta framing runs, and the /ingest payload around the metadata-only declaration bundle), and takes the same value, shape and repetition readings over them between the equal and distinct worlds; nothing was exported for it, since the ticket premise that a bundle is unreachable from the test crate was wrong. a premise test opens every object the payloads carry with the key riding beside it and holds each world to the plaintexts it records, a moving-bytes control refuses payloads that never change between builds, and three oracles are planted on the wire: the deleted identity_hash itself, an address-only grouping and a per-build-salted digest. the wire is clean: nothing plaintext-derived was found. the repetition reading, shared with the store half, now counts occurrences of repeated windows rather than distinct ones, because the payloads repeat content and a chance collision merged two repeated windows and lowered the count the minimum keeps: at the distinct count the wire half read red in 4 of 25 runs, at the occurrence count 60 of 60 green. red under mutation, counts read each time: the bundle producer appending a domain-separated blake3 of each carried plaintext (census 11 passed and 2 failed, while engine bundle_carries_no_plaintext_equality_oracle reports 1 passed and 0 failed beside it), sent building nothing (8 and 5), the push batches carrying no object (12 and 1), the value reading removed (11 and 2), the shape reading removed (11 and 2), the repetition reading removed (11 and 2), each restored to green. the forge question is answered in the new adr 0004 amendment: /stow and /ingest decode the same Frame and /ingest takes encode_ingest_payload, so a forge input is what the wire half reads, while what a forge or relay keeps of it is its own store and is not asked; the engine pin doc scopes it to the one value it searches for, and CONTEXT.md names the wire half. no migration, no format byte, no wire byte and no published wording moves, so this owes no deploy. the workspace suite is green (4351 passed over 138 binaries, 12 ignored) (#2172)
b98809ca · 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.