Changes touching this path
- day 1 logs: drive-log entry + evidence section A row
cc57f391 · dbf3dbe6… - catch up: GB1 loot ferry lands (PR #114)
6816f50d · dbf3dbe6…diff - normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text)
e58fdda6 · dbf3dbe6…diff - 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 - 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 - ferry ingest: compose the full parent tree or refuse; roll back an aborted pass's ingest (#307)
cc115440 · dbf3dbe6…diff - ferry: floor a projected commit date past its git parents (#626)
940772ac · 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 - the mirror's tree may carry paths loot has no record of, and ADR 0028 never recorded the public-delta (#645)
c612e0e3 · dbf3dbe6…diff - a no-op ingest still mints its change: the spine needs a mark per mirrored sha (#842)
316935ec · dbf3dbe6…diff - the docs gate themselves: a land refuses a link that resolves to nothing, five rotted ADR cross-links are fixed, every amended ADR warns in its status, and the artifact sweep retires with its producer
586e3260 · dbf3dbe6…diff - loot ferry parses both policy files once per commit instead of once per changed path, and the instrument had to be built before the fix because PolicyLoads sat on Attributes::load while both ferry doors call parse, so the counter read a structural zero over this whole path and would have read zero after the fix too - a 0-valued counter being indistinguishable from one watching code that does not run. The tally moves to Attributes::parse and Ignore::parse_recorded rather than being routed through a counted door, because the fix itself removes those door calls: a counter on them would read zero afterwards, which is the same blindness relocated. That changes what the counter MEANS, from policy file reads to policy re-derivations, so all three existing pins were re-read rather than adjusted until green - status moves 2/3 to 3/4 with the +1 being its single Ignore::load now counted, and the point is that the GROWTH half passed untouched (narrow equals wide) while only the constant moved, so the test is re-pinned and not re-decided; the tier-exclusion test is still green because the in-process tier links loot-core and never loot-cli, so ADR 0073's exclusion needs no re-taking; and lane_new_sweep's note that policy_loads is one per read_tree_at is repointed to two, since read_tree_at calls both loads. The instrument was proved non-vacuous against the UNFIXED code first, at 18, 66 and 258 parses over four commits of two, eight and thirty-two changed paths, which is exactly 2 plus 2 times commits times width; after, it is 2 plus 2 times commits, so 258 falls to 10 at width 32 and stays 10 as width grows. The most useful thing learned here is a red proof that inverts an assumption: blinding the instrument by putting the tally back on load makes the counter read a constant 1 everywhere, so the constancy pin passes AND the parses-greater-than-zero guard passes, and only the GROWTH assertion catches it - a positive-value pin does not protect against a blinded instrument, which is what ADR 0072's controls bullet credited it with, and that bullet is corrected rather than left standing. This ticket's own wall clock does not reproduce and is corrected rather than repeated: the removed re-parse is 4.63 microseconds per path in release against this repo's real policy files, not 22.0, so a full-history ferry is about 1.9 seconds rather than 8,819 milliseconds - 22.0 is close to the debug reading of 31.53, so the hunt appears to have measured a debug build, and a figure taken under a different build is not a smaller version of the same number. ignored_under is deleted rather than kept as a pure forward once the parse is hoisted, and ADR 0028 is amended because it argued its delete-arm decision partly on an Ignore::parse per deleted path, a cost that no longer exists - the decision stands on the attribution argument, which was load-bearing anyway. seal_under takes a parsed Attributes and narrows pub to pub(crate) since Attributes is crate-private, and its doc said it keeps the bridge from re-parsing the policy twice per path, which was true about the wrong unit: it halved a cost that should never have been per-path. One honest regression is recorded rather than hidden: a deletions-only commit now costs 2 parses where it cost 0, because the hoist is unconditional (#1704)
1822132f · dbf3dbe6…diff - the ferry layer gets its own #288 pin, and the decision is recorded: a_keyless_reconcile_merge_does_not_resurrect_files_deleted_on_the_spine replays the deleted-on-the-spine incident in a keyless repo, so the reconcile takes the merge arm through merge_tips rather than the carry, and asserts the reconcile minted a two-parent merge so the pin cannot slide onto the carry unnoticed. restoring the pre-9763144 ancestry-union tree_at turns it red (53 passed, 1 failed in the ferry module) while the carry pin stays green and both loot-core pins go red as the positive control. the carry pin stays green there because its replay feeds the node manifest against a polluted base and carried tree, and the converge DeleteWins arm drops the stale path: with that arm also made to re-adopt theirs the carry pin goes red too (52 passed, 2 failed), and with the arm alone broken the ferry module stays green (54 passed) while four loot-core tests go red, among them delete_vs_untouched_ours_deleted_theirs_kept_deletion_wins, the carry replay input shape. carry_line doc records that reason at the seam, ADR 0028 gains a #1790 amendment, and the carry and keyless test headers drop the claims the new test made stale. no behaviour changed, only a test, comments and the ADR, and the workspace suite is green (#1790)
588d7a2b · 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 - ADR 0098 records the decisions on #2411: the web writes tickets as signed changes the browser builds, seals and pushes, one per action and stow-first, and the forge becomes a second writer to main for ticket-only changes, those whose every path is under tickets/ and whose signer is a key on the owner account. the forge refuses an ingest that would drop a head it does not descend from; a land folds ticket-only forge changes in before the gate and fails closed on any other forge-only change; a web write racing a land is folded by the sync as a ticket-only merge with no gate, pinned by a test that no gate input reads tickets/; the CLI reads web tickets through a read-only forge overlay; a web change subject is content-free (ticket <id>: <verb>); and tickets never reach git main or GitHub. docs/specs/loot-tickets.md gains dated amendments: created-at on c/ and r/, event position keyed on the space-stripped path so a move keeps order, loot tickets --state replacing --closed, no new --id, the BOM strip on --body-file -, the web write path, criterion 2 met in dependency waves, and the map #2422 slicing. ADR 0028 gains the ticket-free projection and ADR 0063 the gate-less ticket-only sync fold. CONTEXT.md amends Ticket and gains Second writer, Ticket-only change and Stow-first publish. the doc link, line-ending, hygiene corpus and runbook suites are green (22 passed), and the link gate went red under a broken link in the new ADR (1 failed, 2 passed) and was restored. docs only, no code moves (#2423)
2669a01a · 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.