Changes touching this path
- S4: operation log + undo (#146)
Implement ADR 0031: an append-only, repo-wide, local-only operation log
(`.loot/ops`) backing `loot undo` / `loot op log` / `loot op restore`, the
safety net that makes ADR 0030's implicit auto-snapshot safe to trust.
Core (`loot_core::oplog`):
- `Operation` captures the resulting **view** — change-graph heads, each dock's
working/tip pointers, the conflicts set, and the ambient-dock pointer — as raw
pointer-file bytes, so restore is a pure pointer reset that never touches the
object store or the append-only graph (nothing is ever deleted).
- `record` appends one op per view-changing command; `undo` steps the view back
one op and appends a compensating op (the log grows on undo, so redo lands);
`op restore <n>` jumps to any op. A 1-based ordinal is the durable ref; a
`pos` field walks the history back one step at a time.
- Barriers: `push`/`grant`/`maroon`/`pull-grants` are recorded non-undoable;
undo refuses to cross one and names the real remedy (keyring/manifest are
one-way state a view reset cannot retract).
CLI:
- `loot undo`, `loot op log`, `loot op restore <n>` verbs + USAGE.
- One `record_op` per view-changing command; read-only verbs record nothing.
loot has no standalone snapshot op — S2 made `status` read-only, so every
capture rides a mutating verb and that verb is the one op.
- undo/restore reload from the restored files and re-materialize the ambient
dock (writing the restored tree, pruning what the step removed).
The oplog never enters a bundle (bundle serializes changes/objects/keys, never
reads `.loot/ops`) — asserted by test. Tests cover append-on-undo, walk-back,
barrier refusal, absent-pointer round-trip, and full CLI undo/redo. clippy clean.
ce2f96a7 · dbf3dbe6… - Merge pull request #162 from Connor-Miller/s4-oplog-undo
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
4204625fdiff - S3: divergent change — marker + loot abandon (#147)
Surface and collapse a divergent change per ADR 0029/0030 — the last slice of
the jj-ergonomics trio (map #142). A divergent change is one durable change id
carrying more than one live version id (two writers rewriting one change id); it
is data, not an error, and is detected per change id, not by head-counting.
- Engine: `divergent_change_ids(abandoned)` scans every node (a divergence can
sit under a single graph head, e.g. as merge parents, with identical trees) and
returns change ids with >1 live version; `versions_of_change`; `abandon_head`
drops a version from the live heads. `record_carrying` is now public — the
amend primitive that makes divergence exist (and constructs it in tests).
- `log`/`status` render a trailing `!` on a divergent change id and list each
version (shared `change_col` helper). A log whose only multi-head reason is one
divergent change stays the flat listing — routed by distinct change *lines*,
not head count, so the "run `loot apply` to converge" branch never mis-claims a
divergence apply cannot collapse (Act 3 reproduces flat, as in the prototype).
- `loot abandon <version-id>` drops a version, leaving the other live version(s)
under the change id. Nothing is deleted — the version stops being a live head
and joins a local-only `.loot/abandoned` set the live view filters. It refuses
a non-divergent change (never hides a change's sole version), and is one
undoable operation: the oplog View now also captures `.loot/abandoned`, so
`loot undo` brings the version back. `resolve`/`dock merge` are untouched.
Tests: engine detection + abandon; workspace abandon→collapse→undo and the
non-divergent refusal; store abandoned round-trip; `change_col` `!` rendering.
Live-verified Act 3 (flat `!` listing → abandon → undo restores). clippy clean.
43d5cd72 · dbf3dbe6…diff - Merge pull request #168 from Connor-Miller/s3-divergent-abandon
Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com>
dd573561diff - Catch up to git main 809ddfe: #260 smoke default, #271 arm64 build, loot diff (#1), ADR 0034 dock retirement (#253)
587684dd · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - loot bisect: binary-search history to find a regression (#390)
bec6bfc5 · dbf3dbe6…diff - loot-core durable decoders cap the reservation, never the count (#620)
537c4219 · dbf3dbe6…diff - a view step refuses over work no change records, and a primary finalize says so on stderr (#436)
a3248ede · dbf3dbe6…diff - a resolution records which side it answered, so catching up after resolving no longer re-raises the path (#744); ADR 0039 says what the one-commit promise actually guarantees (#653)
b837289e · dbf3dbe6…diff - every persisted artifact crosses the store as its own type, and raw paths retire to the four recorded owners (#863)
Land A of #863 (decision #851): RepoStore gains a typed read/write pair per
artifact — identity, graph, keyring/escrow and their shared halves, manifest,
purges, attestations, burn log, conflicts, resolved — each owning the file,
the atomic replace, and the absent-file semantics, with the codec staying
where it lives. The engine's save_to/load_from/refresh_hold/gc/verify raw
sites, the op log's capture/restore and ops file, and the bisect session all
migrate through; the ops file and view restore gain the temp+rename atomicity
they lacked, and the op-log capture now waits out the Windows replace window
instead of recording a live file as absent. Absent is now NotFound alone
everywhere but `resolved` (best-effort by contract): a torn read fails loudly
rather than reading as an empty artifact. The migrated getters drop to
pub(crate); `objects/`, the keypair/peers, `git-mirror/`, and doctor keep raw
access as the recorded second owners.
0edf769d · dbf3dbe6…diff - the store grows the inventory: classification as data, an exhaustive save walk, and a View derived from the captured rows (#863)
Land B of #863 (decision #851), closing the ticket. One static row per
artifact in loot_core::store carries what the getters' doc comments said in
prose: ownership class (ADR 0034's three), undo membership (captured at a
pinned wire ordinal, or excluded with the reason as data — `lost` stays
shared AND excluded, because a view reset must not un-acknowledge accepted
loss), and the artifact's place in the walk. The engine's save_to persists
the shared surface by one exhaustive dispatch over the table, so adding an
artifact without deciding its persist story fails to compile; the op-log
View's entries derive from the captured rows in ordinal order, with ordinals
below the pinned inline count encoding inside each op and every later
ordinal riding its own trailing section — the discipline bisect (#390) and
resolved (#744) established by hand, now produced by the ordinals
themselves, so old op logs decode unchanged and a new captured row is one
inventory row whose existence IS its undo coverage. Structural tests pin the
table: dense unique ordinals, capture implies position ownership, and every
row's path agrees with its class (keyring/escrow project, ADR 0047).
d05a0c46 · dbf3dbe6…diff - the store stops threading a dock nothing can ever produce through 43 signatures, because a store instance is already the position ADR 0034 retired named docks in favor of, and the sixteen path getters no caller outside loot-core reaches stop being public while the one main reservation that guards a git ref name stays (#1413)
23ab3abd · dbf3dbe6…diff
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.