Changes touching this path

  • loot-core testkit: extracted engine modules own their tests again (#660) 3412c046 · dbf3dbe6…
  • 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 repo open stops materializing half a million manifest entries no verb asked for: ChangeNode.tree becomes a Manifest holding either the decoded map or a proved-decodable byte range into one shared Arc, so topology decodes eagerly because reachability needs it while a change's path manifest decodes only when something reads it - which is where the cost actually was, since framing the 37.9 MB graph without building the maps takes 6.8 ms against 507 ms to build them, and that 507 ms was the bulk of the 1050 ms every verb paid, including loot whoami whose whole job is to print 93 bytes out of id.pub. Deref carries the ~270 existing node.tree read sites unchanged and Deferred is module-private, so a deferred manifest cannot exist over bytes Manifest::walk has not already refused with the same framing the eager pass uses, which is what earns an infallible &TreeManifest return rather than a Result; and an unmaterialized manifest is pinned never to read as an empty one, the rule read_graph_required already states for an absent graph file. Verbs also declare what they read now: RepoNeed rides beside CustodyNeed through the already-pub(crate) open_at_clocked_needing, so no new public door is added and the sealing door custody_lock pins by string and call-site count is untouched, with whoami, conflicts and manifest narrowing to POSITION_ONLY, defaulting to EVERYTHING everywhere else, and a withheld graph or store aborting loudly rather than reading as empty. whoami 555 to 42 ms against a 40 ms control, conflicts 557 to 41, manifest 557 to 42, log -n 5 626 to 144, status 704 to 227, lanes 4069 to 717, peak RSS for whoami 148 to 7.1 MB, and twelve verbs byte-identical against main including 414 KB of loot log. Three of this ticket's premises were wrong and are corrected rather than worked around: the keyring it named as the second cost measures 1.1 ms and is left alone, read_graph_outline did not fit because it discards the parents reachability needs, and the object-store half it called the boolean that costs a gigabyte had already landed in #1545 - while the stat cache stays unbuilt, which is the whole of what status and lanes still spend (#1536) b923abb1 · dbf3dbe6…diff
  • the loot-core tests stop sharing one repo and the shared store stops growing, and the design was already written one line above the bug: tmp said the temp root every fixture repo lives under while returning the root itself, so returning a fresh directory beneath a per-process root makes that sentence true as written with no doc edit and none of the 161 call sites changed. uniqueness is per call rather than per process, because the sites run on the harness's parallel threads inside one process, which is the half a pid does not touch and the half the isolation gradient had already separated - lib alone green, workspace red. a third hole neither ticket named is the larger one: 48 sites spelled DagRepo::init with the bare temp dir longhand, bypassing the helper entirely and invisible to both tickets, so the real set is 51 statements and not the sixty a three-line window had suggested - established by reading whole statements, since that window reads past short ones and stops short inside this crate's many multi-line format calls. the evidence is a paired experiment rather than a green run: two concurrent processes reddened object_reads five times in five, asymmetrically with the earlier starter losing every time, while the shared-store half needed six concurrent runs to reproduce at all and then failed 28 of 36 across four test names, two of them never recorded before, which is the fifth and sixth name for a ticket whose own point is that the name is not stable. after, the same experiments are ten of ten and thirty-six of thirty-six green, and a paired count on an idle machine has the unfixed arm adding 44 objects and rewriting graph, keyring and heads while the fixed arm moves nothing, byte-identical with mtimes unchanged to the millisecond. a census derives the property from source with no allowlist and was proved red four ways, including one site from the third hole that any list written from the two tickets would have passed, and including the census blinded, which reported ok on an empty set and was caught only by its own non-vacuity guard (#1667, #1688) 87c3aacb · dbf3dbe6…diff
  • a move becomes a recorded fact instead of a later guess, and the premise this ticket rested on was false: #98's object reuse is keyed by PATH, so a moved path is absent from the outgoing tree at its new key, falls through to put_sealed and gets a fresh address - the same object under a different key was not a fact waiting to be read off the tree, it had to be made true by extending the reuse across the move, and everything else follows from that. The rule is that a move is recorded only when the path's sealed object survives it, so a move whose content also changed in the same capture window shares no object, records nothing and is two rows, while a move and then an edit across two captures of one change composes and reads as one row saying the content also changed. Empty content and any ambiguity are refused for the same reason the whole design exists: zero bytes equal all zero bytes, and choosing between two vanished twins would make a signed fact depend on iteration order. On the boundary the render side compares nothing at all - resolve reads keys and never an address, a visibility or a byte, pinned by running it twice over trees that agree on every key and differ on every address at three unopenable tiers, with a positive control beside it so agreement is not evidence it answers nothing. The capture side compares once: it opens a vanished object with this identity's own keys, fail-closed so an unopenable object is never read or hashed, and pairs only against an addition at an identical visibility and publication tier, with the digest living for one call, never an address, never stored and never on the wire. What a relay newly learns is that two keys held equal ciphertext, which recording a move states in plaintext anyway since tree paths are plaintext at every tier, so it is inherent to the feature rather than the ADR 0004 oracle - written into that ADR rather than left in a message. Renames ride the label seam change_id and authored_at already use: covered by the finalize signature so a relay cannot rewrite a move, never folded into the version id so two peers reaching one tree by different routes still agree on its address, which is why every existing id and signature is unchanged and a v11 store reads with no move recorded. That was tested rather than argued, against a real store built by the shipped binary, where the old move still renders as a delete and an add in the same repo the new one renders as a rename, because inferring the old one would be the equality oracle arriving through the compatibility door. Three further things were wrong and are corrected rather than worked around: ADR 0019 says an additive change bumps the minor and has never described this project, since the minor is still zero and all five additive changes took the major; plan_moves own doc comment claimed the tier is checked before the digest, three lines above code that does the lookup first, when the property actually holds by the stronger route that nothing unopenable enters the index at all; and the empty-content refusal was written on both sides of the pairing, where each made the other unprovable and removing either left the test green - a duplicated guard is not belt and braces, it is two guards neither of which can be shown to be doing anything (#1539) 3c7e029b · dbf3dbe6…diff
  • 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

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.