Changes touching this path

  • Extract loot-codec + wasm core; de-risk the in-memory SDK boundary (#423) Slice 1 (the tracer bullet) is a multi-session build; this lands its riskiest integration — the Rust→WASM crypto/codec boundary — proven end-to-end, per the #381 "verify spikes before building" mandate. Spike result (ADR 0040): zstd-sys will not build for wasm32 (needs clang for its C), while aes-gcm/blake3/ed25519/getrandom-js do. So: - Extract `loot-codec`: a no-fs, wasm-buildable crate holding the byte format, sync-bundle codec, sealed content (AES-GCM + blake3), and the leaf types (Oid/Visibility/RepoError/ChangeNode). loot-core depends on it and re-exports every item at its original path — a pure relocation, proven by the unchanged, still-green workspace (767 tests pass). - zstd is an optional loot-codec feature (default on for the native host; off for wasm). A new zstd-free `sealed::decrypt` single-sources the AES-GCM step so native `open` and the wasm core share decrypt code; public content is inflated host-side in JS. - `loot-wasm`: wasm-bindgen exports (bundle decode, decrypt, blake3 address) over a pure native-testable `core`, plus the minimal diskless identity (generate/fromSeed/publicKey) on ed25519 directly — not loot-identity, whose OpenSSH/passphrase machinery is native-only (#383). - Golden-parity harness (loot-wasm/tests/parity.rs) runs identical assertions natively and under `wasm-pack test --node`, freezing native-computed vectors; both green. Remaining for slice 1 (next sessions): the TS package, fetch transport + client-side path-scoping, host-side zstd, and the live-relay behavior suite. Refs #423, #421. ADR 0040. 491cf780 · dbf3dbe6…
  • Harden the golden-parity harness: freeze native wire bytes, kill drift (#423) Code review flagged the parity harness (Seam 2) on both axes: it re-encoded the Frame and re-encrypted inside the test, so under `wasm-pack test --node` the same wasm build both produced and consumed the bytes — a symmetric miscompilation would cancel and pass. The native/wasm assertion blocks had also drifted (wasm checked a subset). - Feed the system-under-test FROZEN bytes produced by the native build: a native-encoded Frame bundle and a native AES-GCM ciphertext (plus the existing blake3 address). The wasm side only *decodes*/decrypts them, so nothing it encodes is fed back into it — a codec/decrypt miscompilation now fails the parity check. - The native run regenerates those bytes and hard-asserts they still match the frozen constants, so a deliberate wire-format change trips loudly with a copy-paste replacement. - Both entry points now call ONE shared assertion block (check_address / check_decrypt / check_bundle) over values resolved through each build's own surface, so native and wasm can no longer diverge. Green natively and under `wasm-pack test --node`. Refs #423. ced4e5da · dbf3dbe6…diff
  • TS SDK slice 1: in-memory public read against a real relay (#423) Completes the slice-1 tracer bullet on top of the landed WASM core: an in-memory agent connects to a relay and reads a public file byte-for-byte with no .loot/ on disk. The single passing read() exercises the whole stack — WASM codec + fetch transport + client-side path-scoping + AES decrypt + host-side zstd inflate. - loot-wasm: add encode_fetch_request so the /fetch wire framing (version marker + have/wants) stays single-sourced with the binary; frozen-vector parity added (native + wasm-pack test --node). - sdk/ (@millerbyte/loot-sdk, ESM): connectRelay(url, identity) -> LootRepo with list() and streaming read()/.bytes(). Transport speaks the relay HTTP wire via fetch(); loot-net never crosses. Client-side path-scoping (#380): a metadata fetch resolves path->oid, a scoped fetch pulls just that object's bytes (both have=[], since the relay gathers objects+keys from the changes not in `have`). zstd inflate is host-side (fzstd) — the wasm core has no zstd. Typed errors (LootError/TransportError/NotFoundError/AuthError, #382). - Seam 1: a vitest suite drives the real `loot serve` relay, seeded via the real `loot` CLI (init -> author -> new -> push); 4 tests green (list, byte- for-byte read, streaming, NotFound). The wasm pkg is built by `npm run build:wasm` (gitignored, regenerated from crates/loot-wasm). Deferred to later slices: write path, private/grant read, physical backend. Refs #423, #421. db75b0b7 · dbf3dbe6…diff
  • TS SDK slice 2: author & push a signed public change (#424) The write half of the in-memory loop: a pre-registered key edits files and pushes a signed full-tree change to the relay, read back to confirm. All composition (change-id fold, dual signatures, bundle encode, /stow envelope) stays in Rust/WASM (#381); TS owns only the capture-first overlay. Prefactor: - Move the change-id fold into loot-codec (change_id module: compute_change_id_raw / change_signing_message / mint_change_id / canonical_predecessors); loot-core's compute_change_id(&Change) delegates. Behavior-preserving (loot-core 296 tests unchanged). - loot-codec: seal_uncompressed (the wasm author path can't run zstd). - loot-identity: public Identity::from_seed(&[u8;32]) (the flagged #383/#424 ctor, native side). loot-wasm: - Identity.sign + wrapEnvelope ([0x01][pubkey][sig][bundle], sig over bundle). - ChangeBuilder: carry(unchanged) / put(edited, seals uncompressed public) / finish() -> {envelope, changeId, versionId} — folds, signs the finalize message, encodes the Sync frame, wraps the envelope, all in Rust. - Golden parity (Seam 2) extended: frozen pubkey, signature, envelope bytes, and change-id fold — green natively and under wasm-pack test --node. sdk/: - Capture-first overlay: edit/remove mutate an in-RAM overlay that IS the pending change; describe names it; status/diff report it; push composes via ChangeBuilder (carry unchanged paths, put edited, skip removed) and POSTs the envelope to /stow, returning the durable change-id. - Seam 1: a write behavior suite drives a real relay whose allow-list holds the SDK key; the SDK authors the first change and reads it back through a fresh connection (4 tests). Full SDK suite: 9 green. Slice 2 authors PUBLIC content stored UNCOMPRESSED (valid + readable; zstd's C won't build for wasm, and fzstd is decompress-only). Deferred: unauthorized error mapping (slice 3), private/grant writes (slice 4). Refs #424, #421. 2defc2f5 · dbf3dbe6…diff
  • Slice 2 review fixes: dedup envelope framing, rename, precondition errors (#424) Addresses the code-review findings on 8e70ef8: - loot-wasm: extract `envelope_bytes` — the `/stow` framing is now written once and shared by `Identity::wrap_envelope` and `ChangeBuilder::finish` (was duplicated). Parity vectors unchanged (byte-identical), confirming the dedup is behavior-preserving. - Rename `set_parent`/`setParent` -> `add_parent`/`addParent`: it appends (push loops over merge heads), so "add" matches the accumulate semantics. - sdk push() preconditions (no describe / nothing pending) now throw a plain Error, not `LootError("conflict", …)` — "conflict" means a real same-path bounce, not a caller usage bug. - Seam 1: assert the landed change carries its describe message on the wire (AC#5's "the message is visible"), since the v1 surface has no `log` verb. - Honest comment on carry's visibility collapse (public-only in slice 2; grant-id preservation is slice 4). Green: native + `wasm-pack test --node` parity; 10 SDK tests. Refs #424. 285b6cb5 · dbf3dbe6…diff
  • @ TS SDK slice 5: streaming pull of new changes (#427) Add `pull(): AsyncIterable<Uint8Array>` to `RelayRepo`: POST the sessions 6b3e1b47 · dbf3dbe6…diff
  • Slice 4 (#426): author private (sealed) content — visibility + guards Adds the authorship/sealing side of private content to the in-memory TS SDK, plus same-session read-back. Cross-session grant delivery stays deferred (#383). ECIES to wasm (single-sourced, no drift): move the key_seal composition (ECDH over X25519 + ChaCha20-Poly1305, the "loot grant key wrap 2024" KDF, ed25519→ x25519 derivation, 80-byte wire format) into the wasm-buildable loot-codec. loot-identity::key_seal now delegates to it, preserving its IdentityError surface so loot-cli/loot-net callers are untouched. loot-wasm's Identity gains x25519PublicKey / sealKeyToSelf / unsealKey. ChangeBuilder::put now seals Restricted content: the content key is ECIES-wrapped to the author (never rides in the bundle), so the relay stores only ciphertext. AuthoredChange exposes the private grants (oid → wrapped key) for the SDK's RAM keyring; read() unwraps via the wasm identity to read own content back. SDK guard model: edit(path, bytes, { visibility }) inherits the path's current visibility (new path → public); describe/push take { allowDemote, allowReveal } and a visibility change without the matching guard is refused (GuardError). list()/read() report each entry's visibility. Seams: parity.rs freezes the deterministic x25519 pubkey + a native ECIES wrapped-key vector and asserts native AND wasm unseal it (round-trip + wrong- identity refusal). private.behavior.test.ts drives sealing-stores-ciphertext, same-session read-back, keyless-reader refusal, and the guard cases. ac8b8ef5 · dbf3dbe6…diff
  • conflicts map stores base OID + loot resolve --tool for external 3-way merge (#400, #401) a0711731 · dbf3dbe6…diff
  • Rename the Public visibility tier to Internal (ADR 0041 §2, #480) 6969626d · 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
  • SDK/WASM cross-session grant delivery: pull queues, accepting applies (#508) e0bb183e · dbf3dbe6…diff
  • Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681) 26cfbaa9 · dbf3dbe6…diff
  • a change records when it was authored, so a projected commit stops reading '6 years ago' on GitHub (ADR 0043) loot changes carried no timestamp, so the git bridge fabricated one: BASE_EPOCH + generation, one second per ancestor depth from a 2020 epoch. All 526 commits sat inside seven minutes of September 2020. Earlier repairs (#626's floor, the missing-generation refusal) fixed ORDERING and never touched the absolute date, which is why this kept coming back. ADR 0028 inherited the no-timestamp constraint rather than choosing it. The real reason is upstream: a version id is blake3 over authored content, and a clock inside that hash gives two peers different ids for identical content, destroying the dedup and convergence of ADR 0001/0004. So authored_at rides the label seam ADR 0029 already cut for change_id -- covered by the finalize signature (no relay can restamp it) but never folded into the version id. The wasm golden vectors prove the separation held: FROZEN_VERSION_ID, FROZEN_OBJ_ADDR and FROZEN_SIGN are byte-identical, and only the version marker and one presence byte moved. Advisory, and never an ordering input: a self-reported clock is a claim, not evidence (ADR 0025). in_order/ids_topo, buoy and path_touch.ordinal are all untouched, the forge indexes nothing on it, and the projection floors it past every git parent so ancestry holds whoever's clock is wrong. Format v11: additive for readers (a v<=10 change decodes as None and an absent timestamp adds nothing to the signed message, so every existing signature still verifies), breaking for writers, so loot-cli and loot-forge go to 0.4.0 in lockstep. Forge migration 0004 stores it as bigint, not timestamptz -- signed data must round-trip bit-exact or pullers reject the change. Only new work gets real dates. Every commit on main predates v11, main is push-fast-forward-only, and backfilling would fabricate the very claim this replaces. Perf-Baseline: reset the change body grew one presence byte, plus eight where a timestamp is present, so bundle_bytes/store_bytes/wire_bytes step once at the v11 boundary eaa56d99 · dbf3dbe6…diff
  • a repo open stops materializing half a million manifest entries no verb asked for: ChangeNode.tree becomes a Manifest holding either the decoded map or a proved-decodable byte range into one shared Arc, so topology decodes eagerly because reachability needs it while a change's path manifest decodes only when something reads it - which is where the cost actually was, since framing the 37.9 MB graph without building the maps takes 6.8 ms against 507 ms to build them, and that 507 ms was the bulk of the 1050 ms every verb paid, including loot whoami whose whole job is to print 93 bytes out of id.pub. Deref carries the ~270 existing node.tree read sites unchanged and Deferred is module-private, so a deferred manifest cannot exist over bytes Manifest::walk has not already refused with the same framing the eager pass uses, which is what earns an infallible &TreeManifest return rather than a Result; and an unmaterialized manifest is pinned never to read as an empty one, the rule read_graph_required already states for an absent graph file. Verbs also declare what they read now: RepoNeed rides beside CustodyNeed through the already-pub(crate) open_at_clocked_needing, so no new public door is added and the sealing door custody_lock pins by string and call-site count is untouched, with whoami, conflicts and manifest narrowing to POSITION_ONLY, defaulting to EVERYTHING everywhere else, and a withheld graph or store aborting loudly rather than reading as empty. whoami 555 to 42 ms against a 40 ms control, conflicts 557 to 41, manifest 557 to 42, log -n 5 626 to 144, status 704 to 227, lanes 4069 to 717, peak RSS for whoami 148 to 7.1 MB, and twelve verbs byte-identical against main including 414 KB of loot log. Three of this ticket's premises were wrong and are corrected rather than worked around: the keyring it named as the second cost measures 1.1 ms and is left alone, read_graph_outline did not fit because it discards the parents reachability needs, and the object-store half it called the boolean that costs a gigabyte had already landed in #1545 - while the stat cache stays unbuilt, which is the whole of what status and lanes still spend (#1536) b923abb1 · dbf3dbe6…diff
  • 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
  • a bundle stops carrying the address it can derive from the bytes beside it, and the bump is 12 to 13 rather than the 11 to 12 the ticket names, because #1539 already took 11 to 12 on main and has never shipped - every tag through v0.4.20-deploy.2 still reads 11. Reusing 12 for a second layout was rejected on measurement rather than taste: read_version can only compare numbers, so two incompatible layouts under one major is exactly the misparse the marker exists to prevent, and pinning the constant at 12 while writing the v13 layout turns bundle-codec tests red with the decoder taking the legacy arm on bytes it wrote itself. No count is given for those reds on purpose - the experiment was run twice and answered differently the second time, which is the whole reason this repo does not write a number it has not just measured. ADR 0019's own amendment says every layout change bumps the major, and two majors between releases has precedent - v8, v9 and v10 all first shipped in v0.2.0 - so a major is not a release label. The encoder stops writing the 32-byte address and the decoder derives blake3 of nonce and ciphertext for every major; the v12-and-earlier declaration is parsed for cursor correctness and DROPPED rather than checked, which is the treatment v5 gives the v4 escrow section and v10 the v9 purge lane, and dropping is the stronger choice because a checked claim still has to decide what to do on disagreement while a discarded one cannot reach any outcome, so the compat lane cannot carry the attack either. The grant_ids lever is taken but NOT as the ticket frames it: the list really is a pure function of vis plus the world marker, and rebuilding it on decode would still be wrong, because that synthesises an authorization token from an invariant nothing enforces on a hand-built SealedObject - so it becomes a two-bit flag byte carrying exactly what the pre-v13 wire could carry, with unknown bits REFUSED rather than masked, since a silently narrowed holder list is how an object stops being published with nobody told. bundle_bytes falls 45,757 to 37,857, minus 17.27 percent, and the figure reconciles to the byte: 200 addresses at 32, plus 180 Internal objects at 8, plus 20 Restricted at 3. The ticket's own baseline of 45,753 was stale by the four bytes #1539's rename count added, and its estimate for the grant half comes in at 3.28 percent rather than 3.5 because the fixture makes every tenth file Restricted, whose holder list #521 had already reduced to a bare zero count; the address half lands at 13.99 against the ticket's 14.0. This is a safety change and not only a size one, so it is pinned on both sides: a hand-built v12 frame declaring one address over bytes that hash elsewhere must arrive at its true hash, led by a positive control because refused and impossible look identical from outside and only one is the claim, and a byte-flip sweep over a real v13 bundle - deliberately not a round trip, which passes whether or not the address is derived - asserting every decode that survives yields a key equal to the hash of the bytes read beside it. Restoring the declared address executes the attack and prints it: one bit flipped at offset 11 and the object separates from its content, ac4373d5 against ad4373d5. A golden is added for the object lane because a field REORDER passes the width test and fails only the golden, and that lane had no frozen bytes at any version. The forge's shipped-under-the-wrong-address refusal is DELETED rather than left standing, which is the riskiest edit here: decoded objs has exactly one constructor and its keys are now obj.address() by construction, so the branch is unreachable from any input, and a refusal nobody can show is doing anything would read to the next maintainer as the thing keeping mislabelled bytes out of the blob store - what actually keeps them out is one layer down and pinned there. VERDICT_CONTRACT moves with the major and no machine-output column does, which is stated in the version history so a consumer pinning contract 12 knows to move and knows the columns are identical (#1384) Perf-Baseline: reset FORMAT_MAJOR 12 to 13 orphans every recorded point; bundle_bytes 45757 to 37857 is the intended -7900 from #1384 1c22e8ad · dbf3dbe6…diff
  • packed object storage lands its READ half, and the decision that could not wait for the writer is what a pack is ALLOWED TO GROUP: membership is the address first byte, order is the address order, and the reader REFUSES both violations rather than the packer promising them. a pack adds structure over addresses and that structure sits OUTSIDE the ciphertext, so a packer grouping by grant list or visibility tier would make the pack boundary BE the set of objects a reader cannot open - an authorization-set disclosure produced by a storage layout, which no encryption below it helps. the rule is safe structurally rather than by good behaviour: addresses are blake3 over nonce and ciphertext (ADR 0004), so bucketing by addr[0] is provably uncorrelated with custody and a packer CANNOT group everything one identity can open. the stronger rule, no loose object in a packed bucket, is rejected as unimplementable, since one object arriving after a repack would make every store refused; the residual packed-or-loose bit is an arrival-order fact mtime already discloses. FORMAT_MAJOR goes 13 to 14 at the READER and not at the writer, because the number should mean written by a binary that can read packs, and deferring it to 1525 leaves a window where some binaries read packs and some do not and all report 13. what the bump buys is a named refusal instead of a silent lie: pack/ is not a 64 character hex name, so a v13 walk SKIPS it and reports every packed object MISSING, referenced by change and by path, where v14 answers UnsupportedFormat found 14 supported 13. v14 is the first major in this history that moves no wire layout at all, measured rather than argued - the two frozen parity vectors moved by their marker byte and every other frozen vector beside them is byte identical. two findings beyond the criteria. packing breaks ADR 0038 burn SILENTLY: destroy_objects_loose deletes a file, cannot reach a span in a pack, and would not fail either, the already-destroyed arm swallowing it while the caller records a tombstone over LIVE ciphertext - it now refuses, and the guard asks the DISK rather than the live index, because the index version was vacuously green with burn having removed the address before the save runs. and a packed read must re-hash where a loose read need not, since a wrong offset yields a DIFFERENT VALID object under the wrong address rather than garbage. the membership pin was vacuous on its first writing too, filtering with the same function the code bucketed with, so it asserted only that the two agreed (#1524) eb753b9f · 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
  • the browser SDK head derivation skips a superseded version: the WASM core ChangeView, the JSON WasmBundle.changesJson renders, gains predecessors as hex ids, an empty list when a change supersedes none, and snapshot() in sdk/src/repo.ts drops any id some returned change names as a predecessor before folding the heads, the engine within_depth_of_heads rule, so heads() and the parents a push builds on no longer name a superseded version under unboundedRead against a host that sends one. the golden relay bundles are regenerated with the first change pushed, amended and pushed again, so the meta bundle holds a superseded version nothing names as a parent; the amend is a loot squash rather than the loot edit the ticket named, because an edit finalize records the superseded version as a parent as well as a predecessor, measured, which the old derivation already skipped. pinned in relay.unit.test.ts: the golden holds that shape, and heads() names the amend alone while list() does not show draft.md, the path only the superseded version holds; the shared parity check_bundle pins the empty list. red first against the old golden (20 passed and 2 failed). red under mutation, counts read each time: the predecessor filter dropped from snapshot (20 passed and 2 failed), the core rendering no predecessors (19 passed and 3 failed), the field left out of the JSON (parity 0 passed and 1 failed), each restored to green. no format byte and no migration moves and no host changes, so this owes no deploy. the SDK suite is green in the lane (139 passed over 13 files), the site gate is green (678 passed and 62 skipped over 62 files, 62 surfaces) with no ceiling moved, and the workspace suite is green (4323 passed over 137 binaries, 12 ignored) (#2137) a9fd3357 · 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.