Changes touching this path
- Candidate 5: extract the CLI report DTOs into workspace::reports
The plain-data report values Workspace verbs return for the CLI to render —
WorkingRow, EditReport, DeltaClass, PathDelta, ConflictSide, ConflictView,
AdoptReport, AdoptCatchupReport, PullReport, StepReport, BurnReport — were a
~210-line contiguous block in the 8,000-line workspace.rs, so a verb-output
tweak meant editing the god-file even when no engine logic changed. Lift them
into a workspace::reports child module and re-export (`pub use reports::{…}`)
so workspace::EditReport etc. stay stable for main.rs and render.rs — zero
external churn. Pure data relocation, no behaviour change; loot-cli lib (223),
bin (55), emit_snapshot (3), and cli_smoke (8) suites all green.
With the lane DTOs (f1bcaa6) this covers candidate 5's DTO-locality goal. A
handful of report DTOs (HistoryRow/HistoryView/GraphHistory/BuoyResolution,
IngestAct, SnapshotOpts) stay in workspace.rs for now — they are separated by
impl-carrying types (Remotes, Snapshotted), not a clean block, and the win is
already banked.
Candidate 5's prose-into-Shape half is deliberately not done: emit.rs keeps
human rendering out of emit/verdict by design (R5/#181, needs the peer
registry) — that is an ADR-level call, not a mechanical extraction.
ea9d65b8 · dbf3dbe6… - Pass-2 candidate 2: give the pure extracted modules their own test surface
Pass 1's extractions created clean interfaces but left their tests behind in
the god-files ("the interface is the test surface" unmet). Add #[cfg(test)]
modules to the pure/cheap ones:
- policy.rs: the glob dialect (`*` stops at `/`, `**` crosses), Attributes
first-match-wins + public_by_fallthrough (the mis-seal consent test),
is_catchall, is_secret_name, Ignore subtree pruning + policy-file protection,
parse_visibility. This security-relevant dialect was previously exercised
only transitively through workspace.rs.
- reports.rs: DeltaClass::gutter (the frozen #306 +/M/- mapping) and
WorkingRow::version_hex.
- kv.rs already shipped with its own tests (candidate 5).
Surfaced a real finding while writing the secret-name test: SECRET_NAMES's
`.env*` is anchored at the basename start, so the suffix style `prod.env` is
NOT caught (only `.env`, `.env.local`, …). Left as-is and documented in the
test — widening the set is a security-policy call, not a test-backfill.
Stateful modules (negotiation, lanes) stay covered through the engine.rs /
workspace.rs suites: isolated tests there need the same repo/workspace
fixtures those suites already build. loot-cli lib green (237).
9551c8d8 · dbf3dbe6…diff - Build the seal-WIP guard (#418): refuse a bare sync verb that seals live described WIP
Graduated from #356's "Prevent + hint" resolution (map #354). After ADR 0039
(#362) made review a pure projection, the only way to strand a described
working change as a PR-less signed line is a *deliberate* bare sync verb.
Guard that path at its source.
- New typed refusal RepoError::SealWip { subject, verb } in loot-core, a sibling
of MisSeal/Demotion, on the ADR 0030/0038 guard+override pattern. Overridable
with --seal-wip on both verbs.
- ferry seal path: thread seal_wip through ferry::run -> reconcile_onto; the
Merge-with-wip arm (the ONLY place a bare ferry seals — land pre-finalizes,
adopt folds through fold_line_in) refuses unless overridden. reconcile_onto
now returns the sealed subject so the caller reports the seal from the seam
that decided it, not a post-state heuristic.
- adopt seal path: thread seal_wip through adopt_harbor; refuse before
fold_line_in when a described line diverged. The clean fast-forward and
redundant-capture drop return earlier, so a no-op catch-up never trips.
- Fires ONLY on a described line the sync would actually fold: an un-described
one stays the #275 refusal; a break-glass ferry / no-op sync (no live
described WIP) is untouched.
- On override the verb prints the follow-up-round recovery recipe
(SEAL_WIP_RECOVERY, one shared seam); the review "nothing to review" and land
"not in the pr-map" paths print it too when sealed_unlanded_anchor() detects a
signed line ahead of mirror main with no PR.
- tag's ferry passes seal_wip=false — it is not a finalizer, so it refuses
rather than silently seal live WIP while cutting a release.
- docs/agents/concurrent.md §"One seal path remains" rewritten to describe the
guard; CONTEXT.md gains the "Seal-WIP guard" vocabulary entry.
Tests: guard at both seams (refuse without / seal with --seal-wip), un-described
stays #275, no-wip catch-up untripped, full ferry::run break-glass + no-op pass
untripped, sealed_unlanded_anchor signal. Full suite green (loot-core 337,
loot-cli 243, loot-first 69); the describe_contention CPU-load flake passes
in isolation.
42409371 · dbf3dbe6…diff - loot cherry-pick + revert: apply/invert a change's delta to the current line (#392, #393)
0db6666c · dbf3dbe6…diff - loot duplicate: copy a change with a new change-id (#398)
36c1463b · 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 - 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 - loot diff --content: unified content diffs from inside a lane (#536)
7b01b038 · dbf3dbe6…diff - a view step refuses over work no change records, and a primary finalize says so on stderr (#436)
a3248ede · dbf3dbe6…diff - a path publish tier rides the delta itself, so diff and status stop disagreeing about what is published (#786)
c98b2dad · dbf3dbe6…diff - a forge advertises its retention window on /info, and burn prints one honest bound per host it disclosed to (#814)
652f4023 · dbf3dbe6…diff - the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803)
f15d576e · dbf3dbe6…diff - a no-remote repo converges its diverged heads with loot converge, and the diverged hint names the verb the repo can actually run (#1183)
2d9a2d31 · dbf3dbe6…diff - loot converge stops buying a second whole-tree read for the note it prints, because the implicit capture one seam below has already read that tree, and the series added to catch exactly this learns to fail: its fixture had neither the co-located checkout nor the standing fork the branch needs, so it could not reach the cost it was watching (#1401)
12c1207a · dbf3dbe6…diff - a second renderer stops being able to disagree with the first about which paths changed, because "what changed between two points" is now one seam taking (from, to, pathspec) whose endpoints may be a recorded change or the live working tree, and the six-rung content ladder and the in-the-clear plaintext count moved into it — so a renderer formats a number it is handed instead of tallying its own, and status and diff stop computing the same delta twice (#1541)
289ddd4e · dbf3dbe6…diff - loot diff and loot cat stop giving two answers about one embargoed path, because the four-rung absence ladder cat.rs already had moved into the delta seam and both renderers now match on it instead of each re-deriving burned-vs-sealed — and beside it an option group stops claiming the positionals of a leaf that never declared an arity, which is what would have turned `loot diff HEAD~1 HEAD` into a pathspec of its own selectors (#1551)
2248a1f4 · 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 - ADR 0079 settles what a visibility tier MEANS for a live delta row, and the answer is the shipped one for a reason narrower than the ticket argued: a live side is read at the tier the disk assigns it, because visibility describes SEALED content and loot gates on a key rather than on a tier - reading a live row at its recorded tier would make a declared tier withhold something all by itself for the first time anywhere in loot, and the first bytes it would withhold are the author's own from the author. The ticket's framing overstates what actually ships, and that was measured rather than repeated: with the embargoed path FINALIZED so the anchor holds it, a bare loot diff --content REFUSES at 0 paths rendered in the clear with the plaintext sitting unsealed on disk, so the real rule is that the record wins wherever it holds a version and the disk answers only for the rest - and the rest is a path added since the anchor. The two positions were far closer than the ticket says. The losing position is recorded in its own terms rather than strawmanned, because #536's discipline is about what leaves in a transcript and not about what the operator could otherwise reach, and an agent piping into a PR body does capture what the verb used to refuse; it lost because it has no well-formed answer for the row this ticket is actually about - a path with no recorded tier must fall back to .lootattributes, which is the disk's tier the long way round - and because the refusal is a BAD signal for that job, firing on paths the operator wrote thirty seconds ago while staying silent on the restricted paths #536 always meant to print. The residual is named with no mitigation claimed, since nothing stops the PR-body case and no mechanism at this seam could, the same bytes being one cat away. Its concern nevertheless landed as two real fixes that nothing covered. Unreadable::of_row asked the OLD side about prev_visibility, which is always absent over a live endpoint, so it was really asking about the disk: one path at one instant answered embargoed from cat, content not readable here from --content and sealed from --stat, which is verbatim the #1551 defect, and two of those three pointed the reader at loot grant for something ADR 0007 lets nobody grant. Reverting that term fails the new agreement pin while #1491's own pin stays GREEN, which is the demonstration that the old pin never covered it. And the in-the-clear tally under-reported: Tier::disclosed_by read one side, so dropping a restricted rule and editing the path - the ordinary declarative un-restrict, and the case the function's own doc names - printed recorded restricted plaintext under 2 paths rendered in the clear naming no tier. It now reads 1 restricted, filled through a REQUIRED constructor argument rather than by relaxing prev_visibility, which would print a was-to-now transition for a policy edit nothing recorded and reopen the #1005 phantom; over a both-recorded delta the tiers are equal and max is idempotent, so the tally can only gain a note and never lose one. A pin that closed on embargoed greater-or-equal to one is tightened to full set equality on the disclosure, because a nonzero check is not a guard. One label changed and no gate did: content not readable here becomes the embargo sentence, which AC2 cannot be satisfied without, and no byte of content moves in either direction. A row may now read internal in its header and embargoed in its body, which is correct under this ADR and asserted deliberately - the header is the disk's tier and the body is why the record's side would not open, and moving the header would be the losing position arriving one field at a time (#1574)
daaf69d3 · 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 - loot merge <head> is converge restricted to one head rather than a second fold: merge_head and converge_heads both reach the graph through one private fold_onto, so the three-way content merge is the ADR 0044 seam unchanged, the conflict record is the one loot conflicts and loot resolve already read, the merge node carries a minted mechanical subject naming the head it folded, and loot undo steps it back through an op recorded off the merge node rather than off an outcome map that can be empty. What it deliberately does not do is retire anything: converge drops superseded heads on the way past, and a retirement is a change to a head the operator did not name, which is the whole of what separates this verb from the one it restricts. A flat head is refused rather than silently skipped, because naming a divergent co-version does not make divergence content-mergeable. Two premises of the ticket were wrong. The un-described-change refusal does not apply as it does to the other merges: that refusal belongs to adopt, raised inside fold_line_in, which captures and signs its own merge parent, while converge never reaches it at all because capture-first defers on any working change one branch earlier - so this verb inherits the deferral, and the refusal is unreachable by construction rather than reimplemented. And loot heads resolves no selector, it only lists, so the spelling merge takes comes from loot abandon --head, which routes through resolve_selector. A third premise is narrower: conflicts are recorded and nothing proceeds, but a single named head has no loop to stop, so that criterion is merge_tips behaviour unchanged rather than anything this change makes true. One inherited shape is named rather than altered - a deferred merge --porcelain prints nothing and exits 0, exactly as a deferred converge does, because a deferral rides the human field while the machine contract is verdict rows (#1505)
6107561a · dbf3dbe6…diff - loot fetch lands as the first half of pull by call rather than by imitation: the negotiate-fetch-apply loop comes out of pull_via_declaring as Workspace::fetch_via, and pull now runs capture-first, that call, then the fold, so the two verbs cannot drift about what arriving means - the same extraction merge made of fold_onto. the custody routing comes out of pull_routed the same way, so a locked fetch takes the exact fork pull takes, relay unsigned and forge refused by name, and fetch becomes the second caller of the sealing door, which the custody gate now enumerates by name instead of counting to one. safe on a dirty tree is structural rather than guarded, because capture-first sits above the extracted half, and it is pinned by byte-comparing status porcelain across the call with a modified tracked file in the fixture - the live version id is what moves, and a capture-first mutation reddens it while the path list stays identical. no re-fetch is measured rather than asserted: the test relay now records the raw wants count per round, a later pull is required to ask for zero addresses, and the non-zero ask the fetch itself made is the positive control - an ingest-free mutation makes that number read 2. what integrating would involve is read off the head partition converge executes, through a shared converge_partition, so fast-forward means no merge node rather than one head. two premises of the ticket are wrong. a subsequent converge or pull completes the GRAPH integration but not the working tree when the repo was strictly behind, because converge_heads returns before materializing on a single head and the auto-surface of pull fires only on a non-empty apply, so the content lands in the graph and never on disk while the pull prints nothing new - the same un-materialized state a no-surface pull and a locked pull leave, and fetch now prints that and names loot rehome first. and --remote is the first valued flag of six verbs rather than of this one, so loot fetch --remote --json resolved a remote named --json; the refusal went into remote_sel, where push, pull, propose, grants and pull-grants all reach it (#1514)
577ae796 · 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 move lands as the sixth member of the REFIT FAMILY rather than as machinery of its own, and the whole re-anchor is ONE DELTA over tree entries: the shift, the new parent tree against the old parent, applied to every node of the moved line - so node N tree becomes old_parent then delta N, which is why DESCENDANTS NEED NO SEPARATE RULE and why the clash set is decidable from trees BEFORE anything is minted. the naming decision is the ticket own proposal taken rather than a third option invented on taste: loot move <selector> --onto <selector>, with no collision to find - loot has no mv, and a rename is something a capture RECORDS since v12 - and it is recorded in CONTEXT.md as a new Move entry that states the decision against the word rebase, which the glossary deliberately avoids. AC6 follows an EXISTING mechanism rather than a new one: split moves an entry whole and duplicate reuses entries entry-for-entry under ADR 0004, so move reuses that shape, and the structural proof is that there is NO put_sealed and NO ObjectStore::get on the move path at all - a re-seal would require acquiring a key the code never asks for. its pin carries a POSITIVE CONTROL, open_object failing before AND after, because without one an entry comparing equal proves nothing. the clash arm is the squash clash copied in shape - record conflicts, persist, op, mint nothing - and move joins STOPPING so it carries the abort, continue and force trio. two things are reported rather than papered over: the ticket parent line calls this the start of the jj rebase -r item but AC2 asks for the OPPOSITE of -r, descendants travelling WITH the change rather than re-parenting onto the old parent, and the AC as WRITTEN is what shipped. and the working change is itself a descendant, so it travels, re-recorded as a superseding version but deliberately UNSIGNED, because signing it would mint permanent history from work nobody named - which also means a clean move reports two descendants where the operator sees one, and that is pinned as true rather than hidden. eleven pins, each reddened by a named mutation with the count read rather than the word ok (#1517)
7ff4ef4d · dbf3dbe6…diff - the branch and reorder forms land as FLAGS ON MOVE rather than as verbs of their own, and the branch form is a SELECTOR SUBSTITUTION rather than a second planner: branch_root walks the first-parent line and stops at the first node the destination already descends from, the change just below it becomes the source, and the ordinary plan_move decides everything else - so the branch form CANNOT DRIFT from the subtree form refusals, clash set or sealed-carry rule, because there is one planner under both. reorder is DELTA COMPOSITION: the moved manifest becomes anchor then lower-over-upper, the overtaken one anchor then lower then upper, which equals its old tree exactly when the two touch disjoint paths - which is WHY descendants are re-parented with their manifests UNTOUCHED - and the intersection of the two deltas is the clash set, decided from trees before anything is minted. a DEFECT IN WHAT LANDED AN HOUR AGO is fixed here rather than filed: loot move <finalized> --onto @ was NOT refused, because @ on a different fork passes every #1517 guard - not self, not a descendant, not the current parent, not a merge - so it would have minted SIGNED HISTORY PARENTED ON THE UNSIGNED WORKING CHANGE, a version no push, bundle or fetch carries. the repair is stated as a rule over the parent-to-child EDGES a restructure creates, asked of the DRAFT POINTER and never the signature, because a keyless repo signs nothing - so both forms and any seventh Refit inherit the refusal instead of each enumerating its own cases. AC1 was already satisfied by #1517 and is CITED rather than re-pinned, verified against move_onto live-descendant closure rather than taken on trust. non-adjacent insertion is REFUSED BY NAME rather than half-built, because inserting at an arbitrary point re-parents the line it lands in front of, which is the -r shape #1836 holds. fourteen pins, each reddened by a named mutation with the count read - including two draft pins that go red together, which is the census proving the rule sits on the edge rather than on the verb. and two prose censuses caught the USAGE text itself, where a -b inside a jj rebase citation tokenized as an undeclared flag (#1518)
066564a1 · dbf3dbe6…diff - the draft rule stops being a claim about planners and becomes a property of the ONE SEAM EVERY REFIT CROSSES: refuse_building_on_the_draft is asked unconditionally as the first statement of apply_refit, over a new minted_edges whose match is WILDCARD-FREE, one arm per variant, each declaring the pre-existing parents it would record on. the proof is a MUTATION THAT DOES NOT COMPILE - adding an eighth variant fails with non-exhaustive patterns AT minted_edges, so a seventh Refit cannot forget the rule the way a census or a call-site list would have let it. the two planner calls STAY and are documented as ORDERING ONLY, naming an illegal destination before a clash set is computed, so forgetting one costs a MESSAGE rather than the rule. and duplicate is ruled a DEFECT rather than a named exception, because the artifact it shipped was genuinely broken: the copy is SIGNED so it travels, and it named as parent a version resolve_live_version excludes BY CONSTRUCTION - loot show on that parent exits 1 with no live version matching - with latent harm besides, since any later describe, squash or abandon of the draft makes it permanent rather than repairable by a later loot new. #398 SURVIVES CONTACT and its hard constraint was in a COMMENT its resolution never restates: duplicate must not reproduce a change at its own parent, because a content-addressed version id would collide, which is why the jj default of the source own parents is unreachable here and why #398 reached for the draft at all - the anchor satisfies that constraint just as well and is a version a receiver can resolve, so nothing in #398 required the draft specifically. the planner is no longer TOLD which version the draft is, its view loses that field entirely, which is the narrowing made structural rather than promised. two user-visible changes are stated rather than slipped in: a duplicate with work in progress now lands BESIDE the draft on the finalized anchor rather than on top of it, and --after @ now REFUSES. seven mutations, six red with the counts read and one a compile error, and the two halves are separable - blanking the declaration reddens both pins while neutralising the seam call reddens only one (#1841)
56bc591f · dbf3dbe6…diff - the catch-up stops leaving its own consequence to a runbook the reader may never have loaded: adopt now prints the git step IT JUST CREATED, and only when it actually moved the tree and the checkout is measurably behind - naming the paths it wrote, which is exactly the set needing the proof. the ancestry is judged on the LOOT GRAPH through the mark map rather than by asking git, because git cannot answer it: the checkout has not fetched the landed commit. there are two arms by position, since which git command is correct depends on where HEAD is, and neither arm ever recommends git before a catch-up, because git-first WEDGES the primary and loot-side-first is and stays the order. and the land closing note is narrowed so its final word about the primary cannot read as being about the checkout: it now says which thing the fast-forward moves, says HEAD does not move with the dock, and names the verb that says the rest. the finding that changed the procedure is bigger than the ticket: the skill had the FETCH AFTER THE PROOFS, and a land pushes main FROM THE MIRROR, while the only fetches a land runs in the checkout are its pre-land drift and ADR guards - so at the exact moment an operator needs this procedure their origin/main is stale by that very land, every landed path reads as differing, and the freshly landed perf point does not resolve at all. followed literally the skill produced the precise wrong conclusion it exists to prevent, so the fetch is hoisted with the reason recorded, and the printed block fetches first by construction. twelve mutations with counts read, of which two are worth the reading. the tenth is a CONTROL: the skill quote wraps across two indented lines, so with the whitespace flattening removed the citation pin reddens over LAYOUT rather than over words, which is what makes the flattening load-bearing rather than decorative. and the fifth is the one to learn from - the lane guard test PASSED WITH THE GUARD DELETED, because the lane was silent for an unrelated reason, landed main having been recorded on the primary so the lane lineage-filtered graph had never seen it and the ancestry answered false. a pass that survives deleting the thing under test is not a pass, so the arrangement was rebuilt with landed main recorded THROUGH the lane and two explicit preconditions proving the lane reaches the checkout and can answer the ancestry at all - after which the same mutation reddens. what is NOT done is said rather than implied: a bare ferry still says nothing about git, its catch-up being a different path, so the note claims only the dock for it; the block deliberately stays silent on a repeat adopt, because its claim is that THIS run wrote over your tree and a no-op cannot make that claim; and none of this was reproduced live (#1679)
c8ebe232 · dbf3dbe6…diff - the condition that could not be false is gone, and so is the pair that made it possible: merged stops being a FIELD set by hand at three construction sites and becomes a DERIVED method over the one bit that decides it, so the two cannot disagree - unrepresentable rather than guarded. eighteen reads follow it, and eight of those were conjunctions that are now TAUTOLOGIES, reduced to one term rather than left standing to read as two conditions. the test was rebuilt by moving a DIFFERENT AXIS, because a second catch-up can never reach the block at all: the fixture now puts a real second commit on the checkout main, so the DOCK is still behind and the catch-up genuinely runs, writes the tree and reaches the block, while the CHECKOUT is level and the block declines - and deleting the predicate reddens two tests. the DISCRIMINATION is the part worth reading: under that same mutation the OLD test stays GREEN, which is precisely the defect this ticket describes, demonstrated rather than asserted. the printed recipe stops handing the operator a command that ERRORS. the tree delta now returns what was WRITTEN and what was REMOVED apart, and a removal gets a tree read for its proof, because there is no file to hash, and a checkout for its repair - placed BEFORE the merge, since restoring the file is what lets the merge run at all. the plumbing needed a pin of its own, and that is proved rather than assumed: folding removals back into the written set reddens the workspace pin while the rendering pin stays GREEN. the off-main arm gains its PROOF and withholds only its REMEDY, and says that it withholds it and why, because the byte comparison does not turn on where HEAD is while the repair does - with that limit stated in the same list the module other limits live in, rather than left for a reader to discover. the section this run added about careful sentences gets its own grammar fixed, a stray fragment from an earlier draft removed and a clause given its missing object - and NO count added, with a number that was being carried forward replaced by the property, consecutive sweeps each turning one up. the only-shorthand claim is narrowed at all three sites to the width of the check that backs it, which is a claim about ONE declaration and nothing wider. and the tenth copy of the short-hash helper becomes one, with the unguarded universal above it replaced by what it actually is and an explicit note of what it does not cover (#1911)
53ede622 · dbf3dbe6…diff - the Refit family reports which version each re-anchored change became, and move, squash and absorb emit that mapping through one machine shape instead of counting it. the executor already built the old-version to rebuilt-version remap to thread each line, and each arm reported its length as re-anchored N descendant(s) or intervening change(s): the column the #1517 census named as missing from three verbs at once and declined to freeze for one of them. the reports carry the list now as Reanchored rows (from, to, the durable change id both carry, and carried for a change rewritten only because it sat above one the verb was aimed at), and the human lines print the same bytes derived from that list rather than a count kept beside it - measured through the spawned v0.4.20 primary binary against the lane build over nine fixtures across move --onto, move --before, squash and absorb (the clean arms, the move and squash stops with their aborts, the absorb stay and a mixed absorb) with minted ids masked: the transcripts are identical. the shape is spelled once in loot_cli::refit_shape over the executor seam, by the rule that filed delta_shape beside its seam: R rows for the pairs in the order minted, C rows for the paths a stop recorded, and the A and S rows only absorb fills for where each hunk went and which stayed with its reason, following ADR 0088 at every rule, with a path the frame cannot carry withheld under unprintable through the predicate delta_shape already decides with, and change null rather than a dash string on a keyless repo. the handle is read off the rebuilt version because rebuild_node drops a superseded draft from the graph, which the first cut of the pins found by reading None off the original id. squash and move declare the machine pair beside the resume trio, absorb takes OUT, the usage lines advertise it, PROSE_ONLY loses the three and the ceiling comes down 56 to 53 as the #1517 entry paid, ADR 0066 tiers move 25 to 28 and 55 to 52 with the #1517 amendment marked paid rather than left stale, ADR 0023 gains the amendment recording the value, the rows, the flag members and the JSON fields, and CONTEXT.md the Refit entry. the pins resolve both ends against the graph in-process for every report and through a separate loot evolog process for every R row off the spawned binary, where a carried working draft is asked about as @ because the hex door excludes the working change by design, and the superseded end refusing by name is asserted as the reason the mapping exists. red first: the move arm marking every row aimed-at reddens the two workspace pins and the two spawned move pins (42 passed, 2 failed; 5 passed, 2 failed), the squash arm marking its intervening rows aimed-at reddens the squash workspace pin and both spawned squash pins (43 passed, 1 failed; 5 passed, 2 failed), and the porcelain R row printing from in the to column reddens the encoder pin and the two spawned resolve pins (43 passed, 1 failed; 5 passed, 2 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 over the final tree (loot-cli 1789 passed over 52 binaries, 3 ignored; the remaining crates 2094 passed over 69 binaries, 4 ignored) (#1837)
1be80495 · dbf3dbe6…diff - sweep 1 fix-up over #1826, #1837 and #1839: a live change only a superseded head reaches is listed in the fork view, in a third section of its own, rather than in no section at all. LogGraph::retain_heads leaves such a change with an empty reachable_from, and history() partitioned the fork on exactly one head and more than one, so after a move --onto the ancestor the old tip alone reaches was printed nowhere - reproduced first through the spawned binary with two live lines (0 passed, 1 failed), the fork and the non-empty superseded head both holding as controls. GraphHistory gains unreached, the renderer prints it under behind a superseded tip after the shared ancestry, drawn_rows leads with it because such a change can only be a child of a lane or shared row, the path filter, retain_versions and all_rows walk it, and both machine shapes carry the row with no new column. HEAD on an untracked position with no working change now counts the heads Liveness::is_superseded rejects and answers the sole counted head by name, because Position::anchor answers the first graph head, which can be the superseded one; measured at the other two sites the ticket named, propose already reads is_live and IntegrationPreview deliberately counts the whole partition with retire for the superseded ones, so the liveness doc now points at the callers of the predicate rather than listing two readers. the move --onto usage line gets its space back; refit_shape spells its path columns through treepath::store_key with a backslash pin, and the #2033 census records the tree-path axis it is blind to by design; the shallow clause has one spelling, workspace::parents_not_held, read by the notice and the push refusal, with the pin the suite lacked; carried is carried_count, Reanchored.change is change_id, the squash and absorb op-log lines say re-anchored, frontier_beyond_the_host is private, the one-flag-member sentences drop one, ADR 0089 names both readers of .loot/shallow and both frontier spellings, and the one raw exit ADR 0089 rests on is held to one by a census in tutorial.rs. own > 0 for the move --onto stale head is pinned off the loot heads row. red first, six mutations in one round over disjoint pins: the unreached filter blinded, drawn_rows reordered, the HEAD filter blinded, store_key undone and a second raw exit added redden five pins with the sync shallow control green (1 passed, 5 failed) and the spawned fork pin (2 passed, 1 failed); the noun-verb swap survived tests/shallow (10 passed, 0 failed), which is why the clause has a pin now (0 passed, 1 failed under the swap). ADR 0066 keeps its gated 28 and points at the gate instead of at tickets. 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 (3895 passed over 122 binaries, 7 ignored) (#2046)
0b55a20f · dbf3dbe6…diff - a fetch gains a depth the host honours: the request carries a trailing depth after its wants, written only above zero so a request at zero is byte for byte what every client sent before, and an old host decoder returns after the wants and never sees it, which is what makes it safe to send to any host; the relay and the forge confine the change lane to the nodes within that many generations of their live heads, one being the heads and nothing older, intersected with the delta past have and, on the forge, after the entitlement gate, so a caller refused metadata in a full bundle is refused it at any depth and a node the caller holds is never re-sent, while a want is answered by address whatever the depth, the lane walking the cut changes as it walks the held ones; the seeds are live heads and not childless ids, the forge reading the ref declared set a client computed with its retire applied and the engine excluding any version a node names as a predecessor, since on a host that ingests amends a superseded sibling stays childless and would otherwise seed a generation of its own, 213 such tips against 31 real on this repo; /info advertises it as fetch_depth, false when absent, so a caller that needs the bound refuses a host without it and a caller that can afford the fallback proceeds. the receiver still cuts and that stays the rule, ADR 0089 amended rather than overturned: pull_metadata_via and the depth round of a bounded pull ask the host for the depth they will keep, FromTips as its n and a deepen as zero since the frontier is this position own, and applies IngestDepth to whatever arrives, so a host that predates the field sends the history and the position is the same one generation deep, which the test relay pins both ways by playing a host that honours the depth and one that does not. the WASM core encodes the same bytes, frozen in the parity suite against the native vector at depth one. decided by a grill of six questions on 2026-09-20 and recorded on the ticket and in the ADR: bounded per invocation regardless of persistence, a depth on the existing fetch rather than a listing endpoint, counted from the host heads, search kept on the receiving side over bodies fetched by address, the host cut an optimisation the client never depends on, and the SDK and the cache cap to follow. this is the wire half of the browser SDK stopping its 53 MB stateless read and of the serverless runner per-job pull; measured against the live relay in the landing comment once the hosts are deployed. pinned on the same shapes at each host so the two walks are held to one answer, a chain at depths one, two and zero and past a have, a fork whose two heads are both seeds, a superseded sibling that never is, and depth zero byte-identical to the one-pass walk this walk replaced, the forge with a reader the gate refuses getting nothing at any depth; in loot-net on the field decoded both ways with an old body as zero, an odd remainder refused, a pre-field info as false and one frozen vector both encoders pin; over the wire on a spawned relay and a spawned forge each answering one node of two at depth one and advertising the cut, with a want riding across the cut; and in the CLI cache refresh asking depth one. a clone at a depth asks the host too, so its declined count is 0 under a host that cut, since nothing behind the cut arrives to be declined, and the frontier width is what says the position is bounded, which the shallow suite now pins against a host that honours the depth and one that predates it; the count stays exact on a deepen, which asks the host for no cut because the frontier it deepens from is this position own. red under mutation, counts read each time: the engine ignoring the depth (0 passed and 1 failed), the engine cut counting one generation too many (0 passed and 1 failed), the depth never written (1 passed and 1 failed), the depth never read (1 passed and 1 failed), the relay not advertising the cut (1 passed and 1 failed), the relay handler dropping the depth (0 passed and 1 failed), the forge ignoring the depth (0 passed and 1 failed), the forge cutting before the gate (0 passed and 1 failed), the forge not advertising the cut (0 passed and 1 failed), the cache refresh asking no depth (0 passed and 1 failed), the receiver stopping its own cut when the host cuts (0 passed and 1 failed), the wasm framing never writing the depth (0 passed and 1 failed), the forge handler dropping the depth (0 passed and 1 failed), the engine seeding from a superseded version (0 passed and 1 failed), the forge seeding from every childless id declared or not (0 passed and 1 failed), an odd remainder read as no depth (1 passed and 1 failed), and the wants lane skipping the cut changes (0 passed and 1 failed). no migration and no format major move: a trailing field the old side never reads is a minor move, and /info default false is the whole compatibility story; the relay and the forge owe a deploy, which the release cut carries. the workspace suite is green (4017 passed over 126 binaries, 7 ignored) (#2123)
e3ddfdce · 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 - loot show, the deletion rows of loot status and a surface listing name the published tier off the marker the object carries rather than off the .lootattributes on disk, so a published object whose rule was deleted no longer prints internal and a plain one given a rule since no longer prints published: the delta seam reads each row marker in Workspace::rows_of, off the side that carries the row, and keeps the rule for a live side (ADR 0079 section 1) and for a seal it cannot read, while the clobber guards take ResolvedDelta::classes and pay no read; surface, rehome and the pull auto-surface list through the new Workspace::with_sealed_publish_status. the status machine listing is the working tree on disk and keeps the rule. measured over a 400-path tree with the counters, every added read is a memo hit: object_gets moves by one per row with a recorded side (show of a half-edited change 1602 to 1802, status with a third deleted 804 to 938, surface --json 802 to 1203) and object_disk_reads is flat on each (601, 401, 401), a clean status is flat on both, and a hyperfine A/B over 2000 paths reads within noise (show 98.2 vs 97.3 ms, status 93.1 vs 92.6, surface --porcelain 165.4 vs 168.3, status with deletions 130.8 vs 129.8, load 0 to 6 percent). surface_object_reads counts the listing marker read apart from the opens, and the restore pin reads the seals its capture wrote. both directions pinned red first (0 passed and 2 failed with the fix undone), each through the verb: show and status deletion rows, and surface over trees ferried from git commits that delete and add a published rule. red with each piece undone, counts read each time, each restored green: the delta seam on the rule (1 passed and 1 failed), marker or rule, the overstating repair (1 and 1), the new side asked for a deletion (1 and 1), the listing on the rule (1 and 1), the listing as marker or rule (1 and 1), the guard paying the reads (0 and 2 in surface_object_reads). the published values in diff and surface machine output change for such rows and no key or column moves (ADR 0023). no format, wire or migration byte moves, so this owes no deploy. the workspace suite is green (4527 passed over 140 binaries, 13 ignored) (#2352)
0c27bd77 · 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.