Changes touching this path
- verbs/: seam scaffold + the repo family (init/clone/config) (#661)
28849eaa · dbf3dbe6… - a view step refuses over work no change records, and a primary finalize says so on stderr (#436)
a3248ede · dbf3dbe6…diff - what a peer declares in a negotiation is a type now, so the head list that strands a pull cannot be passed (#799)
#40's fault-injection harness found a peer holding 3 of 6 files whose head had
advanced to the sender's tip, with the negotiation reporting up to date. The
ticket read that as "an interrupted pull cannot resume." It does resume:
`pull_via_interrupted_fetch_resumes` has been proving that since #217, and
`pull_via` declares `negotiation_have()` at all three of its call sites.
The harness resumed by declaring `bob.repo.heads()` instead. That is the
defect — not the sync path, but the fact that `have` accepted any `Vec<Oid>`
and the obvious thing to reach for was the wrong one. `Repo::heads`' own doc
said "what a peer passes as have", so the trait was teaching it. `loot clone`
was doing it too, correct only because a freshly inited repo has no heads.
What a peer may declare is now `Have`, and `negotiation_have()` is the only
thing that makes one — the #217 filter is the check the type certifies.
`Have::nothing()` is the one other constructor, for transports probing a relay
and clients with no repo to ask yet; declaring less can only over-send, and
over-declaring is the strandable direction. Threaded through the
receiver-declares half only: SyncTransport, loot_net::{offer,fetch,pull} and
the forge's signed twins.
The sender side keeps `&[Oid]` deliberately. `have` means one thing in both
directions — what the recipient of the bundle holds — so there is no second
concept to name; what differs is provenance, and a type cannot carry a
guarantee across a network. Wrapping it would advertise a check that does not
happen. That reasoning lives on `Have`'s doc, which is the only place it is
written.
Candidate 1 from the ticket — refuse to advance the head over an incomplete
closure — is not built, because ADR 0024 already priced and rejected it under
"cross-batch atomicity is deliberately not provided". A confirmation note says
so there, so the next reader does not re-derive it. The new
`tests/sync_resume.rs` asserts the head advance rather than treating it as the
failure, and covers the fault the suite genuinely missed: a bundle that is
short but valid, where every batch succeeds, the pull returns Ok, and the
operator is told it worked while holding half the files. The next pull heals
it, which is what is pinned.
Acceptance criterion 3 ("am I up to date" must answer no while any object is
absent) is deliberately not built: `closure_complete` is unsatisfiable on the
forge path, where a reader legitimately never receives ciphertext it holds no
grant for, so the warning would fire forever on every forge repo with
restricted content. That is #803.
`an_interrupted_transfer_resumes_to_convergence` runs with its ignore deleted.
271dd5cf · dbf3dbe6…diff - the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803)
f15d576e · dbf3dbe6…diff - custody at rest seals to your own key behind an optional passphrase, unlock is a session file or an env var, and no non-interactive caller starts prompting (#1138)
c8ffee3b · dbf3dbe6…diff - loot init's refusal leads with the command to run instead of describing two mechanisms, because it is the first thing a stranger types after installing (#1158)
19cdb435 · dbf3dbe6…diff - loot clone probes the host and speaks the signed forge client, so a forge URL reaches the door instead of dying at the transport, and the sealed grants arrive with it (#1162)
98f5b662 · dbf3dbe6…diff - a refused clone removes only what it created, so the 403 every stranger earns stops blocking its own retry, and the refusal pin dispatches every command both arms name (#1209)
7e399a76 · dbf3dbe6…diff - the unlock session holds the seed sealed under this repo's own ciphertext instead of the passphrase you typed, so a synced config directory leaks a window and not a secret you reuse elsewhere, and the passphrase is zeroized from the moment loot owns it (#1173)
1943d2c7 · dbf3dbe6…diff - loot id phrase cuts the key for the recovery door loot id recover already opened, issuing the 24 words for any identity ever minted with no derivation change, and refusing outright when stdout is not a terminal because those words are the private key
8bd665a5 · 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 - loot config grows a second scope and the local one is untracked because of where it sits rather than because a rule says so: .loot/config.local is skipped by the tree walk at name == DOT, ahead of every ignore call, which is a stronger guarantee than the obvious alternative - a root-level file plus a .lootignore line could not have held, since .lootignore is itself tracked and a fresh loot init writes none, so the first status in every new repo would capture a per-operator default identity as internal, and the dialect has no negation to walk it back with. verified rather than argued: the same content under .loot/ is invisible to status while at the repo root it lands as internal on the very next command. precedence is one function and the merged listing is defined through it, so the file a row names and the value that --get prints cannot disagree; --get exits non-zero on an unset key, pinned through the shipped binary because a library Result proves nothing about what a shell branches on, and a flag-shaped --get value is refused rather than looked up, since the door hands a valued flag whatever follows it. bare set and unset write GLOBAL, inverting git deliberately: loot config set identity is the route init own refusal advertises as one name across every repo on this machine. the settings are read where they matter - remote is consulted by resolve_remote, so a bare push, pull, grants, pull-grants or propose now contacts what the repo names, with origin still the answer when nothing says otherwise. two of the ticket premises were wrong. identity cannot do what it asks: a repo identity is already repo-local in .loot/identity, a config key that overrode it would manufacture the exact name-to-key drift doctor checks for, and loot init can never read a local answer at all because the file lives inside the .loot it is about to create - so the live case is clone run from inside a repo, which is what the pin asserts against a repo configuring nothing as its control. and the local file is not a store artifact: ON_MALFORMED has nothing to choose between, because the key = value dialect cannot produce an undecodable value - the real hazard was an unreadable file reading as absent and falling through to the other scope, which Layer::load now refuses outright. one argv finding rode along: config is the first family whose flags may precede its subcommand word, and args[0] was wrong in both directions, so the leaf now resolves from the first token that is neither a flag nor a flag value (#1507)
2c12d0f2 · 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 - the no-false-absence guard 1527 landed was BYPASSABLE, and the bypass was neither of the two escape routes its doc ruled out: buoy and bisect return from main before the notice single call site, so assemble measured the frontier and threw it away unprinted, and a plain loot buoy on a shallow position exits 2 with no buoy - a REFUSAL-SHAPED false absence, which the comment four lines up calls the shape an operator most needs the line for. the repair is SHAPE rather than a third bullet: main is now a two statement wrapper and the whole former body is dispatch(), which is the only expression in main that produces an exit code, so every exit code the binary emits is that call return value and a return anywhere inside it - any depth, any verb, ahead of the table or behind it - returns INTO the notice. a new early return is not a new escape route, it is the SAME route, and because nothing is enumerated nothing has to be maintained. that matters because the old doc argued unbypassability by ENUMERATING escape routes and closing each, which reads as exhaustive, is not, and failed on the first route nobody listed - the same shape as a hand written count beside a derived number, which this run watched go stale three times. the residual is STATED rather than enumerated away: a process that never returns through main, process::exit or abort or panic, which is a property of process TERMINATION and not of the verb table, so nothing a verb does BY RETURNING can miss the notice. a census pinning exactly one raw exit was considered and refused, because that walker own consumer count is itself a derived-and-pasted census and adding a consumer churns it. the failed enumeration is kept as HISTORY, marked as the thing that failed, with the shape argument replacing it, and a THIRD site carrying the same claim was found in CONTEXT.md glossary which the ticket never named. FRONTIER_WIDTH stops being a process global read by verbs that hold the authority: both callers now ask their own workspace, and the public spelling no longer compiles from outside the module, which is the narrowing made structural rather than promised. three pins that asserted nothing are gone. the re-encoding privacy pin is DELETED because no byte seam is reachable - the transport is a TYPED seam, so the test re-encoded its own recorded id vectors and asserted the codec is linear in their length, true for every possible input - and its doc now records what it did against what it claimed, with the criterion standing on the other two pins, which carry positive controls. the partition pin is read out of the RENDERED sentence now rather than from literal triples the constructor never saw. two mutations make the case by experiment: moving the notice back to the end of dispatch reddens the new pin while log and show stay GREEN, and re-adding the old partition body under a scratch name PASSES, so vacuity is demonstrated rather than argued. an eighth pin was written and then deleted, because a mutation proved the existing assertion already sits on the live case (#1828)
b5eb05df · dbf3dbe6…diff - loot init quickstart block names the identity just created in its restricted= example, as the tip line above it already did, where it printed restricted=alice whatever the identity was called, so pasting it as told would seal .env away from its author and the lockout gate refused it: render::lootattributes_quickstart takes the identity name and init passes its own. pinned by init_quickstart_restricts_to_the_identity_the_tip_names, which inits as bee and holds the two lines to the same rule. red with each piece undone, counts read each time, each restored green: the block before the fix (0 passed and 1 failed), init passing alice to the block (0 and 1), the tip naming alice (0 and 1). the pages that write restricted=alice beside loot init --identity alice are right and unchanged, as are the captured runs under docs/evidence. no format constant, codec byte or migration moves, and init has no machine output. the workspace suite is green (4434 passed over 139 binaries, 13 ignored) (#2278)
ce011329 · 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.