Changes touching this path

  • 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…
  • 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
  • SDK slice 3 (#425): typed error model on the write path Map relay/loot failures to typed LootError subclasses so callers branch on `code` instead of string-matching: - push(): a 401 from /stow (loot-net handle_stow -> unwrap_envelope maps a non-allow-listed key to BadSignature/UNAUTHORIZED) now throws AuthError (code "unauthorized") carrying the offending signing pubkey. The SDK signs every envelope in WASM, so a 401 means the key isn't enrolled. No pre-check, auto-enroll, or downgrade (#383) -- attempt and report. Other non-2xx and fetch failures stay TransportError. - Add ConflictError (code "conflict") to errors.ts and export it. It is NOT thrown: /stow is append-only and accepts concurrent forks without rejecting a moved parent (loot-core stow_accumulates_concurrent_forks_without_conflict), and capture-first push re-snapshots heads at push time so it never builds on a stale parent. Clean detection needs an optimistic-concurrency model slice 2's overlay doesn't carry -- deferred, documented, not faked. - AuthError gains an optional `pubkey` (hex) for the allow-list case. Seam 1: push-errors.behavior.test.ts drives a real relay seeded with a foreign key -> AuthError(unauthorized) with pubkey; an unreachable relay -> TransportError. tsc clean; vitest 12/12 (2 new + 10 existing). 565a0638 · 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
  • TS SDK slice 6: physical mode openRepo over the shared LootRepo interface (#428) The second backend: openRepo(path) drives an on-disk .loot/ checkout by shelling out to the installed loot binary, returning the IDENTICAL LootRepo the in-memory mode defines — so calling code is backend-agnostic. The binary owns all crypto/codec; physical mode adds none (no WASM). - CLI (sanctioned tiny machine-output addition, #428): `loot surface --porcelain`/`--json` emit the current readable tree as path+visibility (loot-core `verdict::surface_{porcelain,json}`), so `list()` never scrapes human text. An empty repo is an empty tree, not an error. - sdk/src/physical.ts: openRepo → PhysicalRepo via child_process. list() parses `surface --json`; read() streams the materialized file (a real byte stream) → NotFound on ENOENT; edit/remove write the working copy (capture-first) and record a client-side overlay so status/diff report kinds (added/modified/removed) against a committed baseline captured at open + refreshed on push — loot folds a described change into the current tree, so surface alone can't tell add from modify. describe/push shell out (`describe -m`, `new`); guards map to --allow-demote. Errors map to the shared taxonomy (missing binary → setup error; parent-moved → ConflictError; non-repo → NotFoundError). - Seam: `runReadContract` extracted to sdk/test/read-contract.ts and now runs VERBATIM against BOTH backends (connectRelay and openRepo) — the proof they are interchangeable behind one interface. physical.behavior.test.ts adds a write round-trip + error surface. 32 SDK tests; loot-core/loot-cli green. Deferred (documented): physical private-visibility authoring (a .lootattributes rule) and pull-with-remote behavior — public content covers the AC and the in-memory backend covers private. Refs #428, #422. 9d12a388 · dbf3dbe6…diff
  • Slice 6 review fixes: stream pull, distinct setup error, clean empties (#428) Addresses the code-review findings on 4ca54a2: - pull() now STREAMS the child's stdout via spawn (was buffered via execFile then yielded once) — satisfies the read/pull streaming AC. - Missing/incompatible binary gets its own SetupError (code "setup"), distinct from generic failures and the deferred-private path; an old binary lacking `surface --json` / reading an older format maps to it too. - allowReveal is rejected with a clear error rather than silently dropped (physical slice 6 authors public content; reveal isn't mappable); guard mapping centralized in guardArgs. - Empty-repo machine output no longer string-matches the "nothing to surface" error: new Workspace::surface_tree() returns None on a headless repo, so cmd_surface emits an empty tree cleanly (no prose-scraping). - read()'s ENOENT→NotFound handling deduped into a shared streamFile helper (was repeated in the collector and the iterator). Acknowledged, kept (documented in code): the client-side overlay/baseline for status kinds (loot has no kinded-delta machine output, so capture-first can't "map directly" for status); read streams the materialized file rather than a `loot` stdout (no cat verb); the faithful status/overlay mirror of RelayRepo. Green: loot-cli 243, 32 SDK tests, tsc clean. Refs #428. 3ece1bad · dbf3dbe6…diff
  • SDK: inject transport/runner seams + map binary error codes (#432, #433, #434) Three architecture-review deepenings that make the two LootRepo adapters' decision logic testable without a live relay or the real binary, and replace stderr regex-scraping with the binary's coded error channel. #432 — RelayTransport seam. A narrow dumb-pipe (`post`) + default HttpRelayTransport, injected via `connectRelay(url, id, { transport })`. All interpretation stays adapter-side: response classification (401→AuthError+pubkey, non-2xx/connection-fail→TransportError) is a pure helper, and the push visibility-resolution + GuardError enforcement is extracted to a pure `resolvePushVisibilities`. New relay.unit.test.ts proves error classification, path-scoping, decode, and compose/guard against a fake transport — the WASM core exposes no bundle encoder, so decode/path-scoping replay golden `/fetch` bytes captured from a real relay (test/fixtures, regenerate with gen-relay-fixtures.mjs after a format bump). Relay integration trimmed to the read + write round-trip smokes (read.behavior/write.behavior); push-errors.behavior deleted. #433 — LootRunner seam. `run` (buffered, never throws on non-zero) + `spawn` (streaming) + default SubprocessRunner, injected via `openRepo(path, { runner })`. Physical error-mapping, arg composition, and pull streaming are unit-tested against a fake runner (physical.unit.test.ts); physical integration trimmed to the read + write round-trip smoke. #434 — map binary error codes → LootErrorCode. physical.ts reads `error.code` from the binary's `{"error":{"code","message"}}` under --json and maps it in one place (demotion/mis_seal/seal_wip→guard; unsupported_format/no_repo/unknown_flag →setup; not_found→not-found; else generic); all stderr prose regexes dropped. `run`/`pull` append --json, so the CLI's `new`/`describe` verbs now accept --json (not --porcelain — no consumer) to emit coded failures. loot's engine has no conflict-family slug (it accumulates forks rather than rejecting), so conflict stays deferred/generic — ConflictError remains exported for when a slug lands. e75ba364 · dbf3dbe6…diff
  • SDK/WASM cross-session grant delivery: pull queues, accepting applies (#508) e0bb183e · dbf3dbe6…diff
  • the delta reaches the TypeScript SDK as a SHAPE rather than as prose, and it is the FIRST INSTANCE of #1763 rule rather than a bespoke design: DeltaShape::of is a PROJECTION of the seam and nothing else - the mark is the #306 gutter rather than a new alphabet, the rung is the LineDelta variant, the counts are its own counts and the tally is the disclosure verbatim - so nothing here was hand-designed and the shape cannot drift from what the human rendering shows. three decisions carry the weight. added and deleted are NEVER ZERO where the count is unknown, they are absent, exactly where --stat calls a row uncounted, and ONE function now feeds both channels so the two cannot disagree. a sealed row WITHHOLDS the path, the from-path and the recipient list in both channels, because a path name is CONTENT under #306 - the shape refuses to leak through the encoding what the prose refuses to print. and the machine channel is ONE SHAPE REGARDLESS of --content and --stat, verified byte-identical, because those flags pick WORDS over a delta while this picks an ENCODING of it. the contract number is the shared VERDICT_CONTRACT and not a per-verb one, which ADR 0023 already answered for every shape and #1516 declined explicitly for porcelain, and an SDK test asserts diff and status report the SAME number so a per-verb version would go RED. a defect was caught BEFORE the freeze, which is the only time that is cheap: the first encoder used to_string_lossy, so on Windows a FROZEN contract would have shipped a backslash path beside a human line printing a forward slash - caught by the pin that compares the two RENDERINGS rather than asserting each is non-empty, and its unit pin uses an EMBEDDED backslash rather than a nested path, because a nested-path fixture is vacuous on POSIX. wire names stay snake_case deliberately, since a camelCase mirror would be a second vocabulary for one frozen contract and the only thing it could do is drift. nine mutations with counts read, and the ceiling comes DOWN 57 to 56 - the first entry on that list to PAY rather than be excepted (#1554) ac5700af · dbf3dbe6…diff
  • the TS SDK gains seek(): an agent searches a repo without a clone from JS, over loot seek --json through the existing subprocess runner, typed at both ends, with the question a typed object never a string and the answer the verb own JSON parsed as it prints it, every key the binary writes and none it does not; seekRead(path) is its own call returning the recorded bytes verbatim through the streaming half of the runner, since the buffered half decodes stdout as text and the verb refuses a machine format for a read; both stand alone with a cwd and a binary, and both ride LootCheckout from openRepo with the checkout own runner. a refusal of a listing or a search arrives by class, never by prose, while a read refusal is the sentence on the generic class because the verb takes no machine format for a read, said on the doc and in the README: the verb own slugs and the CLI-shared ones map onto the SDK taxonomy in one function over the one parser of the coded stderr line, now in errors.ts beside the classes and called by the physical adapter too, bad_revision and no_such_remote as NotFoundError, no_identity, no_repo and unknown_flag as SetupError, conflicting_flags, bad_flag_value and read_is_bytes as a new InvalidQuestionError under a new invalid code, and multi_head as MultiHeadError under the same invalid code rather than conflict, which errors.ts reserves for the moved-parent family, carrying the heads parsed off the one line shape the binary prints them in, so the next call pins one with at; the shared slugs are classified once, in the table the physical adapter uses, so bad_flag_value is the invalid class for every verb. the behaviour suite reads loot seek --schema and asserts the SDK tables are the binary own: every flag the verb declares bar --read and the three the SDK spells itself is a field, every verb or shared slug bar error has a class of its own, and the example answer keys are the typed ones. seekArgs is pure and pinned flag by flag without a binary; the behaviour test drives the release binary on a listing of the three seeded paths whose target resolves to an absolute root, a search with several patterns and a count, names only, a clipped line against its whole, a bound, --name, a read of text and of binary bytes, a git target by directory with a null visibility, each refusal class including a missing binary, no identity from outside any repo, a remote name that names nothing and a window over a local target, and the multi-head heads read off the refusal that loot seek --schema renders through the verb own raiser rather than a copied string. the README gains the section and CONTEXT the sentence; the browser LootRepo is untouched, since the WASM core cannot run the binary, and its stateless read stays the map --via api follow-up. nothing in the land gate runs the SDK, so the counts here are the claim: npm run typecheck clean and npm test 114 passed over 11 files in the lane, against release binaries built in the lane; red under mutation, counts read each time: seekArgs dropping --name (3 failed, 8 passed), spelling count as -l (4 failed, 7 passed), forgetting --fresh (3 failed, 8 passed), a multi-head refusal on the generic class (2 failed, 9 passed), parseHeads reading no subject (2 failed, 9 passed), a bad revision as the invalid class (1 failed, 10 passed), seekRead decoding the bytes as text (1 failed, 10 passed), the checkout seeking from the process directory (1 failed, 10 passed), a missing binary on the generic class (1 failed, 10 passed), and a shared slug falling to the generic class (2 failed, 9 passed). no rust, no migration, no wire or format byte moves and no forge or relay byte moves, so this owes no deploy; the workspace suite is untouched by a TypeScript change and was green at the base (3999 passed over 124 binaries, 7 ignored, at b1e5791) (#2116) 7997cffe · dbf3dbe6…diff
  • the browser SDK reads a relay at the heads generation and refuses a host that cannot give it one: connectRelay list() and read() open with a fetch carrying depth one, the heads nodes and nothing older with the whole manifest on each, and the object round is bounded the same way rather than paying the history a second time, since a want is answered by address across the cut, so a read is two bounded rounds where it was one unbounded and one that repeated it, against the 53 MB per call the map measured on this repo; the host is asked once per repo, on GET /info, which the transport seam gains as an optional get so a lane that never asks stays honest, and a host whose answer does not say fetch_depth, or answers with a non-2xx, or not JSON, or through a transport with no get, is read as one that did not say and is refused as a SetupError naming the host and the version line it reported, before any bytes are asked for, unless the caller passed unboundedRead, which accepts the cost on that host and asks a capable one for the bound all the same; a probe that could not reach the host is a TransportError and is asked again next time. heads() names the set the bounded read answered with, on a RelayLootRepo the connect door now returns, and several heads fold as they did, a later head write winning a shared path; push, status and grant ride the same bounded round because the parents a change builds on and the tree it carries are the heads own, which is all they ever read. pinned on a fake transport with the golden bundles, the request bytes bounded on the metadata and the object rounds, the old host refused by name on list and on read with nothing posted, the opt-in against it and not against a capable host, the probe asked once across list and read, the three did-not-say shapes each refused and each opted into, the unreachable probe re-asked, and heads() one; and over a spawned relay with two generations, the request four bytes longer than the unbounded one ending in one, the answer one node whose parent is the base change a whole fetch shows, every round of a read one node, the head named and then two after a fork the second session pushes off a replayed metadata answer, listed as the union, and the field stripped off the real /info refused by name and paid for on opt-in. README states the cost and the option, CONTEXT and ADR 0089 say the browser SDK refuses rather than is to. red under mutation, counts read each time: the depth never sent (21 passed and 5 failed), every host assumed to cut (21 passed and 5 failed), unboundedRead ignored (22 passed and 4 failed), the probe asked on every read (18 passed and 1 failed), a failed probe remembered (17 passed and 2 failed), heads() naming nothing (23 passed and 3 failed), the refusal not naming the host (24 passed and 2 failed), a transport without get reading as capable (18 passed and 1 failed), a 404 on /info reading as capable (18 passed and 1 failed), an error on /info remembered as a refusal (18 passed and 1 failed), an explicit false read as absent (18 passed and 1 failed), a JSON non-object left unclassified (18 passed and 1 failed), the head derivation skipping the parented filter (6 passed and 1 failed), and the object round unbounded (24 passed and 2 failed). no rust, no migration, no wire or format byte moves, and no host behaviour moves, so this owes no deploy beyond the one #2123 already owes the release; the SDK suite is green in the lane (126 passed over 11 files, against release binaries and sdk/wasm built in the lane) and the workspace suite is untouched by a TypeScript change and was green at the base (4017 passed over 126 binaries, 7 ignored, at 5a007435) (#2124) 8eb8e8cd · dbf3dbe6…diff
  • the browser sdk transport stops waiting on a silent host indefinitely, because fetch has no timeout of its own and in a browser nothing stands in for one: HttpRelayTransport now aborts through a watchdog that waits for an answer to begin for its idle budget plus the upload at the uplink floor, then re-arms on each chunk of the body, so an answer that keeps arriving is not cut for its size and one that stops fails once the idle budget passes with no bytes, the same policy #2079 gave the native client, with TRANSFER_IDLE_MS mirroring wire::TRANSFER_IDLE and each side now naming the copy on the other. a budget that runs out throws a TransportError naming which wait it was and how long, and the adapter passes that through instead of relabelling it as an unreachable host, one rule in errors.ts asked by every catch around a transport call. TWO OF THE THREE ITEMS ARE REFUTED RATHER THAN BUILT: the forge blocking clients the ticket named both sit inside cfg(test) modules, in the tree today and at the rev the ticket was filed on, and the forge talks to its object store only through loot_s3::S3Client, which already names its own 60 second timeout; and the upload side header wait was fixed under #2079 by the byte-derived transfer budget, whose refusal already names the answer budget. the frozen-tab limit is written down rather than fixed, since a timer on the page loop measures the page clock and not peer silence, which is #2079 in a browser. red under mutation, counts read each time: the re-arm moved outside the read loop failed the steady-answer pin (6 passed and 1 failed), the answer wait ignoring the bytes failed two (5 passed and 2 failed), the watchdog verdict dropped failed all three budget pins on a bare AbortError (4 passed and 3 failed), and the adapter relabelling every throw failed the pass-through pin (0 passed and 1 failed), each restored. the sdk suite is green against release binaries built in the lane (137 passed over 13 files), no migration, no wire or format byte moves and no host behaviour moves, so this owes no deploy. the workspace suite is green (4179 passed over 133 binaries, 9 ignored) (#2070) f6348ba0 · 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.