Changes touching this path
- loot blame: line-level authorship annotation (#389)
e0eb02e0 · dbf3dbe6… - Extract the hunk-attribution engine into a deep `hunks` module
The line-redistribution algorithm behind `loot absorb` (#399) lived as five
free functions (diff_hunks, apply_hunks, attribute_hunk, split/join) plus
LineHunk/HunkTarget in workspace.rs, testable only through the ~200-line
Workspace::absorb via a full Workspace.
Move it to a new `hunks` module — the write-side twin of `blame` (the read-side
line-ancestry engine they share `lcs_match`/`Attr`/`split_lines` with). The
whole line algebra becomes private behind a small bytes-in/bytes-out interface:
`attribute(parent_bytes, working_bytes, owners, index_of) -> (Option<PathHunks>,
Vec<Stay>)` builds a per-path plan, `PathHunks::apply_at(i, base_bytes)` rebuilds
one ancestor's content, and `PathHunks::targets()` reports where hunks landed.
`absorb` never sees a LineHunk or a line vector now — only oids, trees, and
sealed bytes; it keeps just the chain-walk and the sealing/superseding it alone
can do.
The payoff is testability: the splice math gets direct unit tests over crafted
byte inputs (the three migrated line-function tests plus interface-level tests),
instead of only through absorb's full-Workspace integration tests. `blame::
split_lines` is made pub(crate) and reused, deleting the `split_content_lines`
duplicate. AbsorbStay becomes `hunks::Stay`.
Behaviour-preserving: the three absorb integration tests pass unchanged. The
Phase A/B rewrite is outcome-identical (the in-effect emptiness check now lives
inside apply_at).
38525b23 · dbf3dbe6…diff - Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681)
26cfbaa9 · dbf3dbe6…diff - the change graph gets one read view, and the two forwarding bands over it retire (#868)
826c3339 · dbf3dbe6…diff - a verb run from a subdirectory finds the repo instead of advising an init that would nest a second one, path arguments rebase from the cwd the way a tester types them while view's globs stay root-relative, and the resolved root is named on stderr whenever cwd is not it
eca43819 · dbf3dbe6…diff - 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 that names a file in this repo stops carrying its own copy of the #1330 rebase, because every argument slot's kind is now a required field of the table that already declared the leaf's flags and arity, so the argv door rebases from the declaration and a leaf that states nothing does not compile (#1466)
889e5821 · dbf3dbe6…diff - the valued half of the argv door gets a floor: FlagSpec grows value() and message() so a valued flag's payload is stepped over by the spec that declared it rather than found by a bare scan of raw argv, and all 29 readers move onto it along with six more the ticket's list had missed, which is what left loot ferry --dock --git-dir yielding a --git-dir nobody typed while its --json twin was the single case anyone had pinned; and the census meant to stop this class recurring widens from the previous bug's exact spelling to the class itself and gains a positive control, which caught two further hand-rolled scans in help_sub and resolve_tool_path and took the crate from eight offences to zero (#1652)
f86ffb25 · 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 - blame grows six options and the one that mattered was -w, because ignore whitespace when attributing a line reads like a second comparison and this crate has exactly one line matcher: MatchOpts is a parameter of lcs_match_with, lcs_match is defined as that function at the default options rather than as a copy of its body, and the only place two lines are ever compared is match_key inside it, so textdiff, blame and hunks stay byte identical and the whitespace rule could reach diff tomorrow without anything gaining a compare of its own. -L slices a FINISHED annotation rather than narrowing the walk, which is why a range costs exactly the object reads a bare blame costs and the 98 content-address collapse still fires under it, and its rows carry the line numbers of the file rather than of the slice. --since bounds the walk by authored_at, which ADR 0043 makes advisory and never an ordering input, so two rules keep an advisory clock from dropping history silently: a change recording NO clock never cuts the walk, and the first version past the bound is KEPT as a boundary, marked with a caret, so a line older than the bound says the walk stopped instead of being re-attributed to the oldest change the bound happened to keep. --ignore-rev and --ignore-revs-file send a line to the next OLDER version of the path, marked with a tilde, which is a list filter over the versions and not the nearby-line guess git makes there; an unreadable ignore file is REFUSED rather than read as empty, because that failure is silent and inverted, the annotation that comes back blaming exactly the change the operator wrote the file to skip. the ticket got one criterion wrong and it is the porcelain one: it asked for the contract version on that channel like the other machine shapes, but the other machine shapes do not do that, ADR 0023 settled in its 1540 amendment that json carries contract and porcelain stays header-free, and the sentence claiming otherwise is one ADR 0066 records as false by design and guards off the shipped surfaces. so both channels ship and the version rides json, and the spelling is --porcelain rather than -p because sixty verbs answer to one pair of machine selectors. sealed is pinned under every flag rather than once, and --since was the hardest of them: a bound tight enough to stop short of the seal legitimately yields no sealed row at all, so what it must not do is NAME those lines, and it does not (#1516)
b8cb568b · dbf3dbe6…diff - shortlog grows -n, -e and --group, and the mailmap lands as a DISPLAY grouping whose boundary is three structural facts rather than a comment. it lives under .loot, which walk_stat skips at name == DOT BEFORE any ignore rule, so it cannot enter a tree, a change, a bundle or the git mirror and nothing arriving over a wire can write it. the module is pub(crate) in the loot-cli LIBRARY, while verification lives in loot-codec and loot-core and AccessClass and pusher admission live in loot-forge and loot-relayd - every one of those is a crate loot-cli DEPENDS ON, or one that does not depend on it, so the use does not compile there and making it compile means a DEPENDENCY CYCLE. and the type answers key to STRING and never key to key: there is no canonical-key operation and no iterator, so even an in-crate caller, where privacy does not help, cannot obtain the key behind a key. inside the CLI the scope is judged rather than maximised - grant-status, attest, custody and manifest deliberately KEEP the un-mailmapped resolver, because those columns name a key that HOLDS A CAPABILITY and an alias there would be a display that reads like an authorization claim. the rotation criterion takes the DOCS arm and the reason is sharper than the ticket knew: the keypair is ALREADY SWAPPED by the time the report renders, so a rotation that failed because a display file could not be written would report FAILURE OVER A COMPLETED ROTATION. the ritual prints the exact two-line entry at the moment the operator needs it, and a pin feeds that printed text back through the parser so it provably pastes. the file is untracked BY LAYOUT because display names in loot are LOCAL: .loot/peers is local, the ambient identity is local, and blame porcelain author column was frozen as the KEY precisely because two machines reading one repo render different words for one author. a TRACKED mailmap would make some labels repo-wide and leave the rest local, two authorities for one column, and would hand anyone who can land a change the power to relabel an author in everybody else log. one mutation could not be WRITTEN and that is the finding rather than a gap: routing the mailmap into the key column is not EXPRESSIBLE, since the resolver hands back str and String and exposes no key at all while the key columns take a 32 byte array, so a collapse mutation was substituted to prove those assertions are live and the inexpressibility is reported as the structural claim instead of as a green test. one behaviour change is deliberate: the default order flips to NAME order so that -n selects something, because an -n that named the only existing order would assert nothing. and two pre-existing guards caught real drift on the way - the documented-flag census tokenizes on whitespace and brackets only, so a dash-flag followed by a comma or a backtick in PROSE reads as a documented flag, and the verb-grep census needs its row on ONE line, so a wrapped row silently loses the count (#1531)
cfaaad9f · dbf3dbe6…diff - a revset is accepted wherever a selector is, through the door that already existed - and the verb population is DERIVED from the shipped help text rather than from the dispatch table, which closes the two-spellings trap that had already cost two tickets UPSTREAM of this change, since help is pinned set-equal to the table in both directions. a placeholder-name list could undercount a DIFFERENT way, so the whole placeholder vocabulary of the help text is a SECOND set-equality: a verb spelling its change argument some new way now reddens the build instead of dropping silently out of the population. twenty-five selector-taking verbs, TWO of which a hand list would have missed and the derivation found - diff spells from and to, merge spells head, and neither says selector. four are adopted here, exactly the four #1563 named, all through the existing door via one shared consumer: no second parser and no per-verb precedence, because two verbs disagreeing about what a bare word means is worse than neither having revsets. the point-versus-set question is decided PER VERB with its reason rather than made uniform. cat is a point because its product is one path bytes written verbatim with no frame around them, so two versions arrive concatenated and indistinguishable from one file containing both. archive is a point because N trees are either N files, which is a different verb, or one file whose members collide path-for-path. blame is a point because the lineage walk STARTS there and there is no column for a second start. and ignore-rev is a SET USED WHOLE, because the only use of its result is a membership test, so every member is taken. diff is the interesting refusal: a two-member set does NOT collapse into the pair, because the pair is already spelled as the two arguments, so collapsing would give one argument an arity decided by data the operator cannot see - the same expression a comparison on one repo and a refusal on another - and a set has no SIDES, so a topological order would silently come to mean oldest-versus-newest. that is rejected explicitly and recorded rather than overlooked. the shared rule is exactly one, else REFUSE naming the count, and the door never takes a member. sixteen verbs are left, in two groups, both NAMED in the census rather than merely absent: nine where a set is a fan-out over a REWRITE, needing an order, a per-change failure rule, an undo story and a confirmation surface that do not exist - guessing on a destructive verb being the worst place to guess - and seven that are points by nature and left on scope, two of which have a coherent set arm and so are decisions rather than transcriptions. every pin RUNS THE VERB against a real three-change repo, each row in its own fresh copy with declared setup, so a refusal that fires BEFORE the selector resolves cannot pass vacuously - which is the exact failure this ticket exists downstream of, a criterion once pinned only at the flag gate. ten mutations with counts read. the control that matters is the one dropping a placeholder: the VERB census stayed GREEN, because diff line also spells another, while only the vocabulary census fired - which is what makes that second guard load-bearing rather than decorative. and the ignore-rev pin carries its own control, because without it the equality would hold just as well between two no-ops. three prose claims this change made FALSE are fixed, and one existing pin was MOVED rather than deleted, onto a verb the census names as selector-only so the choice cannot go stale silently (#1689)
5e9b9cb6 · dbf3dbe6…diff - every top-level verb whose usage is bounded now declares its arity, so loot burn a.txt b.txt refuses b.txt by name instead of burning a.txt and dropping it: twenty-seven verbs were narrowed, each read handler-first, and none reads more than its widest usage line shows - though a flag that narrows a verb shape is not covered, since the arity is one number per verb, so grant --relay and resolve --tool still drop a surplus word and that is filed as #1934. the census stops naming verbs - who owes an arity is DERIVED from USAGE (no ... on a positional token) with the subcommand families and the OPEN_BUT_TAKES_NONE verbs exempt by reference, and the hand-kept COUNTED list and its per-verb paragraphs are gone, their argument folded into the rule. the one USAGE reader gained a separator arm, since a [-- <path>...] tail read as a bracketed flag would have made grep look bounded, and the ... is read off positionals only so the repeatable ignore-rev flag leaves blame bounded. a new pin derives both sides - expected set and invocation width from USAGE, observed from live refusals - and also asserts the wrong-refusal direction: the refused word must be the junk one and a full documented invocation must pass. five mutations went red with counts read, and no in-repo caller passes an extra token (#1928)
3b9b0f9a · dbf3dbe6…diff - a verb arm now receives the argv door answer instead of the raw slice: VerbRun takes an Admitted value, the rebased tail bound to the spec of the row it was dispatched from, and its readers are the only way in, so argv[0] or argv.first() in an arm fails to compile (both planted, read as E0608 and E0599). spec_of is test-only and its 104 production lookups read through the admitted spec; six of the seven raw-index arms (bundle, grant-status, embargo-status, attest, resolve, completions) read positionals through the door and relay reads its leading word through a named exception; subcommand families resolve their leaf into a new Admitted, and bisect and buoy are handed one ahead of the table. the two named exceptions, leading_word for relay and bisect and pass_through_tail for the bisect run command, plus the one process argv read, are held to a named set of functions by a new census that went red on a planted leading_word read in cmd_bundle, and the source walker consumer header now counts it. refusals and output are unchanged by every suite and by a read of each migrated arm, with one exception found on the way: resolve with a trailing --tool and no command read --tool as the file and now refuses with the usage line, pinned by a new test. CONTEXT.md gains the Admitted argv term; loot-first keeps its own spec_of and DECLARED_READS is untouched (#1929)
d92e95f2 · dbf3dbe6…diff - the two shapes that graduated without a record get their amendments, telemetry status and off carry the contract field, and the comma-joined column gets one home: #1971 carries the ADR 0088 section 4 follow-ups and asks for each premise to be re-verified first, so each was read against the tree — ADR 0023 held no amendment for count-objects (#1523) or telemetry (#1658), telemetry status and off printed no contract field and the string dash where V3 says null, and the Verdict entry in CONTEXT.md still typed 17 of the 68 beside a roster where the dispatch table derives 32 emitting verbs and 51 prose-only ones; the ticket comment correcting item 2 is the later word and is taken as such, telemetry on printing report JSON for the same deliberate reason as show. ADR 0023 gains two dated amendments describing what shipped: the one-metric-per-mark census rows, and the name-keyed telemetry rows with the divergences frozen beside them. status and off lead their JSON with contract now and spell an unresolvable spool null, rendered by status_shape and off_shape, which take values rather than reading the config so all three renderings are assertable; show and on stay exempt because what they print is the report the sender posts, and the exemption is pinned over Report::to_json rather than over a verb, that string being what the endpoint receives. The shared flag column item 3 asks for is verdict::joined_col, the comma join with the dash when empty, and what defines membership is that spelling rather than a roster, so the columns spelled that way are its callers: blame, delta_shape, explain, heads, log, evolog, attestation_shape, refit_shape, seek, tag, both pipeline listings and lanes route through it and no frozen shape pin moved. The cross-shape census is declined with its cost recorded in ADR 0088 section 4, since a census over values needs a pub on each shape that #1553 refuses, one over a spawned binary reads nothing for a verb whose arguments it cannot supply, and its exemption table would be a hand-maintained list asserted as complete. Red under mutation, counts read each time: joined_col joining with a tab (loot-cli 1335 passed and 8 failed), the empty set printing an empty field instead of the dash (1324 passed and 19 failed, and loot-core 647 passed and 1 failed naming lanes_porcelain_rows_are_the_frozen_contract), the contract field taken off status and off (5 passed and 3 failed), the spool null spelled as the string dash again (7 passed and 1 failed), and a contract field added to the report the sender posts (6 passed and 2 failed), each restored. No migration, no wire or format byte moves and no host behaviour moves, but two leaves of client JSON move, so this rides the next release and owes no deploy. The workspace suite is green (4107 passed over 132 binaries, 8 ignored) (#1971)
4ef9c62b · dbf3dbe6…diff - loot blame and loot log --path read the one path they ask of each change through the new GraphView::address_at, which answers off the manifest frames through the new Manifest::address_of where the frames provably are the map (no backslash key, keys strictly ascending in Path order, checked by the keys_ascend step Manifest::addresses uses) and from the decoded map otherwise, so a history walk no longer decodes every manifest on the lineage to read one entry of each, and blame no longer clones each tree; has_path keeps decoding, since its remaining production caller asks many paths of each of a few changes. the new --path-history opt-in half times address_at over every change of a re-opened chain at 200 paths, asking the path that orders last; paired, one loot-perf-gate --features count built per arm with the frame arm never taken as the before, interleaved, the gate reading load idle at 1 to 4 percent (peak 7): 34.87 to 34.97 ms down to 4.46 to 4.53 ms at depth 256, 138.8 to 139.3 down to 18.01 to 18.03 at 1024, and 279.2 to 280.6 down to 35.9 to 36.2 at 2048; counted by the new path_history_allocations test at width 200, the walk allocates 484,352 times at depth 1024 against 0. through the binary, against the landing position binary over a repo 1,025 changes deep and 203 paths wide, min of 15 over two rounds: loot blame 160.4 to 160.6 ms down to 33.7 to 33.8, loot log --path 164.8 to 165.8 down to 43.7 to 44.0, plain loot log 28.9 to 29.2 on both, and every output compared was byte identical with its exit code and stderr: blame plain, --porcelain, --json, with a selector and on an absent path, log --path on flat, nested, dotted and absent paths, and shortlog --path. new pins: over every golden, a file whose Path order is not its byte order, the legacy separator file and the three #2273 shapes, address_of answers what the eager map answers for every key and for probes that are not keys, decoding nothing exactly where the frames are the map, and it answers off the frames past a holder name that is not UTF-8; a spawned blame and log --path find a/b.txt beside a-b.txt and a.txt. red under mutation, counts read each time: the frame arm never taken (0 passed and 1 failed in the codec pin, 1 and 1 in path_history_allocations), the ascent check dropped (0 and 1, on the answer), the counts check dropped (0 and 1, on the control, since Windows reads a backslash as a separator), byte order in the stop (0 and 1 in the codec pin, 0 and 1 in the spawned pin), each restored to green. riding along: HUNT-PERF names the new half. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4289 passed over 137 binaries, 12 ignored) (#2283)
045c7e42 · 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.