diff --git a/docs/research/alknet-filesystem/poc-summary.md b/docs/research/alknet-filesystem/poc-summary.md new file mode 100644 index 0000000..dcee426 --- /dev/null +++ b/docs/research/alknet-filesystem/poc-summary.md @@ -0,0 +1,363 @@ +# alknet-filesystem: POC Research Summary + +**Status:** Research complete on the two highest-leverage unknowns (path-tree layer + write path); the approach is viable enough to spec. Remaining unknowns are implementation-scope, not feasibility. +**Date:** 2026-06-20 +**Scope:** Captures what the POC proved, what unknowns it closed, what remains open, and the architectural direction it establishes. Source material for the eventual `alknet-filesystem` crate spec. + +--- + +## Executive Summary + +A POC (`alknet-filesystem-poc`, `/workspace/alknet-filesystem-poc`) was completed that resolves the two largest sources of feasibility uncertainty around building a content-addressed, branch-aware, mountable filesystem from three orthogonal layers: SQLite (path tree + application file format), iroh-blobs (content-addressed blob store), and honker (durable pub/sub + queue + locks inside the same SQLite file). + +The POC was built in two iterations: + +1. **Path-tree layer** (Tier 1) — proved that a SQLite-backed path tree over an iroh-blobs `MemStore` gives Fossil-style branching with free content dedup, honker notify-on-commit inside the same transaction as path-tree mutations, and free multi-tenant isolation via a `bucket_id` column. 8 tests. +2. **Write path** — proved that "branch on write, merge on close" reconciles the fundamental mismatch between content-addressed storage (BLAKE3 must hash the complete file) and filesystem write semantics (chunks arrive incrementally, possibly out of order). A concurrent reader sees the old version until `close()` commits atomically; crash/abort leaves the old version intact. 7 tests. + +**15/15 tests pass.** All deps are published crates.io versions (no workspace path deps), so the POC is portable. + +The three layers compose cleanly for both the read path *and* the write path. The remaining unknowns — FsStore/redb vs SQLite, actual SFTP wiring, network distribution, GC/tag management — are implementation details rather than architectural risks. + +--- + +## Background: The Insight + +The POC originated from a correction on X: SQLite is not just a database, it's a legitimate [application file format](https://sqlite.org/appfileformat.html). The key observations from that link: + +- **BLOBs < ~100KB are faster inline in SQLite than as filesystem files.** This inverts the usual "databases are slow for big binaries" assumption at exactly the granularity iroh-blobs already cares about (16 KiB chunk groups). +- **Atomic transactions** over path-tree metadata, independent of content blobs. +- **The schema is the documentation.** An SQL schema defines the file format more concisely than a custom format spec. + +iroh-blobs had already discovered this independently: its `FsStore` uses a hybrid approach (`DESIGN.md`, `/workspace/iroh-blobs/DESIGN.md`) with redb (an embedded KV DB, SQLite-shaped philosophy) for small blobs and filesystem files for large blobs, with four metadata tables (`blobs`, `tags`, `inline_data`, `inline_outboard` — see `src/store/fs/meta/tables.rs`). The path-tree layer is the missing piece iroh-blobs deliberately doesn't provide: iroh answers *"given a BLAKE3 hash, where are the bytes?"* but not *"given a path, which hash does it resolve to?"* + +The architectural insight: **SQLite for path edges, iroh for content, honker for coordination.** Each layer does what it's best at, and the boundaries are clean. + +--- + +## The Three Layers + +### Layer 1: iroh-blobs — content-addressed blob storage + +**Crate:** `iroh-blobs` 0.103 from crates.io, `MemStore` backend (no redb, no fsync rabbit hole — deliberate POC scope choice). +**Source reviewed:** `/workspace/iroh-blobs` (v0.100 local checkout) + published 0.103 source in cargo cache. + +iroh-blobs provides content-addressed blob storage with BLAKE3 verified streaming. A blob is a sequence of bytes of arbitrary size, identified by its 32-byte BLAKE3 hash. Content is dedup'd by definition: same hash = same bytes. This is the layer that gives us the "many agents / many forks share most content" property — content is shared across branches because it lives under its hash, not under its path. + +Key API surface used by the POC: +- `MemStore::new()` — in-memory store, no persistence +- `store.blobs().add_slice(bytes).with_tag().await` → `TagInfo` (returns the hash + creates a persistent named tag protecting the blob from GC) +- `store.blobs().reader(hash)` → `BlobReader` (implements `AsyncRead + AsyncSeek`) +- `store.blobs().has(hash)` → `bool` + +**One sharp edge worth recording:** `Hash::new(buf)` *computes* a BLAKE3 hash of the input; `Hash::from_bytes(bytes)` *wraps* 32 raw bytes as a `Hash`. Round-tripping a hash through hex requires `from_bytes`, not `new`. This cost ~20 minutes of debugging and is worth documenting for anyone touching the iroh API. + +### Layer 2: SQLite — path tree + application file format + +**Crate:** `rusqlite` 0.39 (bundled), pinned to match `honker-core`'s rusqlite version. +**Source:** `src/schema.rs`, `src/fs.rs` + +SQLite stores the path tree: a mapping from `(bucket, branch, path)` to an irob `Link` (64-hex BLAKE3 hash). The schema has four core tables: + +- `buckets` — top-level isolation unit (multi-tenant). One row per tenant. +- `branches` — Fossil-style named snapshots with parent pointers. A branch starts empty and inherits everything from its parent. +- `paths` — one row per `(bucket, branch, path)` entry that has been *overridden or created* on this branch (only the delta from the parent). `link` is the BLAKE3 hash; `size` is cached so `fstat` is a single indexed lookup. +- `tombstones` — deletion markers. A path may exist on a parent branch but be deleted on a child; the tombstone stops the chain walk from inheriting. + +Plus two write-path tables (see Write Path below): +- `write_sessions` — active write sessions (one per open file handle) +- `write_chunks` — chunks for active write sessions (one row per `write()` call, stored inline as BLOBs) + +The core read operation is a recursive CTE that walks the branch parent chain: + +```sql +WITH RECURSIVE chain(branch_id, parent_id, depth) AS ( + SELECT b.id, b.parent_id, 0 FROM branches b + JOIN buckets bk ON bk.id = b.bucket_id + WHERE bk.name = ?1 AND b.name = ?2 + UNION ALL + SELECT b.id, b.parent_id, c.depth + 1 FROM chain c + JOIN branches b ON b.id = c.parent_id +) +SELECT p.kind, p.link, p.symlink_to, p.size + FROM chain c JOIN paths p ON p.branch_id = c.branch_id + LEFT JOIN tombstones t ON t.branch_id IN (SELECT branch_id FROM chain WHERE depth <= c.depth) + AND t.path = p.path + WHERE p.path = ?3 AND t.id IS NULL + ORDER BY c.depth ASC LIMIT 1 +``` + +This returns the *first* matching path row that isn't tombstoned on a closer branch. The `ORDER BY c.depth ASC LIMIT 1` correctly picks the closest branch's override. Even on the POC's naive schema, resolves are sub-millisecond in-memory. + +### Layer 3: honker — durable pub/sub + queue + locks inside the SQLite file + +**Crate:** `honker-core` 0.2.4 (not `honker` — see below), `bundled-sqlite` feature. +**Source:** `src/schema.rs` (bootstrap), `src/watch.rs` (listener) + +honker provides pub/sub, durable queues, named locks, rate limits, and a scheduler — all as SQL functions registered on *your own* rusqlite connection. The key integration point: + +```rust +honker_core::apply_default_pragmas(conn)?; // WAL, synchronous=NORMAL, ... +honker_core::attach_notify(conn)?; // _honker_notifications table + notify() SQL function +honker_core::attach_honker_functions(conn)?; // enqueue, claim, lock_acquire, stream_publish, cron, ... +honker_core::bootstrap_honker_schema(conn)?; // queue/stream/scheduler tables +``` + +This registers `notify(channel, payload)` as a SQL scalar function on the same connection that owns the path-tree tables. The critical property: calling `SELECT notify(...)` inside the same transaction as a path-tree mutation means the event is atomic with the data change. A watcher wakes on commit, not on poll. A rolled-back mutation produces no event. This is the transactional-outbox pattern, built in. + +**Why `honker-core` not `honker`:** The `honker` crate opens its *own* SQLite connection and manages its own database handle. To get the SQL functions on *your* connection — the whole point of the transactional-outbox property — you need `honker-core`, which exposes `attach_honker_functions(conn)` for any rusqlite connection. The `honker` crate is the ergonomic Rust wrapper for the "honker manages everything" use case; `honker-core` is the foundation for the "I own the connection" use case. The POC is the latter. + +honker is single-machine, file-backed (explicitly: "two servers writing the same .db over NFS is not a Honker deployment strategy"). That's fine for the local-VFS layer; the distributed part is iroh's job at the blob layer. The split is clean: honker coordinates local state + local workers; iroh coordinates cross-node content. They don't compete. + +--- + +## POC Iteration 1: Path-Tree Layer (Tier 1) + +**Scope:** SQLite path-tree + iroh-blobs MemStore + honker notify, no SFTP, no FsStore, no network. The goal was to validate the SQLite+iroh seam and the branching model cheaply. + +### What it proved + +**1. SQLite is a workable path-tree layer over content-addressed blobs.** Path rows store BLAKE3 hashes; bytes live in iroh. `put`/`get` roundtrips cleanly. Atomic path-tree mutations (`rename`, `mkdir`, `unlink`) compose with iroh blob add/delete. `rename` is O(1) on path edges regardless of file size — content stays in iroh under its hash, not under its path. This is the property git also gets right. *(Test: `put_get_roundtrip`, `rename_is_o1_on_path_edges`)* + +**2. Fossil-style branching gives free content sharing across forks.** A child branch inherits parent files via the recursive-CTE chain walk. Same content hashes to the same link (free dedup via content addressing). Writes on a child are invisible on the parent. Tombstones hide parent files on child branches. *(Tests: `branch_inherits_parent_content`, `branch_modifications_do_not_leak_to_parent`, `tombstone_hides_parent_file_on_child`, `content_is_deduped_across_branches`)* + +**3. honker notify-on-commit works inside the same transaction.** `notify()` is called inside the path-tree mutation; watcher receives the event on commit. *(Test: `watch_fires_on_commit`)* + +**4. Buckets (multi-tenancy) are free.** A `bucket_id` column on every row = isolation. Alpha files don't leak into Beta. Auth is an adapter problem (which connection sees which buckets). *(Test: `multi_bucket_isolation`)* + +### The `share-check` demo + +Running `cargo run -- share-check` demonstrates the content-sharing property end-to-end: + +``` +== content sharing across branches == +main shared.txt = "hello from main\n" +agent-a shared.txt = "hello from main\n" ← inherited from parent, same hash +agent-a agent-a.txt = "agent a working here\n" +agent-a main-only.txt resolved = false ← tombstoned on agent-a + +== dir listing on agent-a (parent chain walk) == + agent-a.txt kind=File link=f7a5aa575a40… + shared.txt kind=File link=b9d5a428d102… +``` + +`shared.txt` is byte-identical across `main` and `agent-a` (content shared by hash). `main-only.txt` is hidden on `agent-a` by the tombstone. `agent-a.txt` is only on the branch. + +--- + +## POC Iteration 2: Write Path — "Branch on Write, Merge on Close" + +**Scope:** Chunked writes (SFTP-style `open → write at offset × N → close`), crash/abort semantics, concurrent-reader isolation. The deal-breaker question: can content-addressed storage serve as the backend for a mountable filesystem's write path, or is the BLAKE3-must-hash-the-whole-thing constraint a fundamental mismatch? + +### The problem + +BLAKE3 must hash the *complete* file to produce the content address. But filesystem writes arrive as chunks — SFTP writes in ~32KB chunks, possibly pipelined and out of order. You can't hash until you have all the bytes. Where do partial writes live? What does a concurrent reader see? What happens on crash? + +### The solution: "branch on write, merge on close" + +A write session *is* a short-lived branch: + +- **`open(path, WRITE)`** creates a temp child branch of the target branch. Inserts a `write_sessions` row. +- **`write(offset, chunk)`** inserts a row in `write_chunks` — one transaction per chunk, crash-safe. Chunks may arrive out of order (SFTP pipelines writes). Chunk-sized BLOBs (~32KB) are SQLite's sweet spot per the appfileformat paper's "BLOBs < 100KB faster inline" finding. The offset is the unique key — two writes to the same offset overwrite (last-write-wins within a session, matching POSIX overlapping-write semantics). +- **Reads on the *target* branch during the session** see the *old* version via the parent chain walk. This is POSIX "concurrent readers see old version until close commits", for free, from the branching model — no separate snapshot mechanism needed. +- **`close()`** assembles chunks in offset order → BLAKE3 hash → iroh `add_bytes` → updates the path row on the target branch → `notify()` → marks session closed. All the SQLite parts in one transaction. The new version becomes visible atomically. +- **`abort()` or crash:** the session row stays `open`/`aborted`, the target branch is untouched, the old version is still readable. Orphaned sessions can be found and cleaned up later. + +### What it proved + +| Test | What it proves | +|---|---| +| `chunked_write_close_produces_correct_hash` | Assembling chunks + BLAKE3 hashing produces the same hash as a whole-file write. Content addressing works through the chunk boundary. | +| `chunked_write_out_of_order` | SFTP-style pipelined writes (offset 6 before offset 0) assemble correctly. The offset is the key, not arrival order. | +| `concurrent_reader_sees_old_version_during_write` | **The key POSIX property.** During a write session, reads return the old version. After `close()`, reads return the new version. For free, from the branching model. | +| `abort_leaves_old_version_untouched` | Simulated crash/abort: old version survives, chunks discarded. | +| `abort_on_new_file_leaves_no_trace` | Aborting a write to a new path doesn't create a phantom entry. | +| `large_chunked_file_writes_correctly` | 1MB file in 32KB chunks assembles, hashes, and reads back correctly. Matches whole-file hash. | +| `multiple_concurrent_write_sessions_same_path` | Two sessions on the same path don't corrupt each other's chunks (session-scoped). Last close wins. | + +### The bug that was found and fixed + +The initial `chunk_idx = offset / 32768` computation was wrong — two writes at offsets 0 and 6 (both < 32768) collided on `chunk_idx = 0`, causing the `INSERT OR REPLACE` to overwrite the first chunk with the second. The out-of-order test caught this immediately. Fix: use the offset itself as the unique key. This is the kind of thing the POC exists to catch — a spec written without the POC would have shipped this bug. + +### Why SQLite + honker wins for the write path specifically + +The write path is where the SQLite-vs-redb-vs-filesystem decision matters most, and SQLite wins for three reasons that all show up here: + +1. **Chunk-sized BLOBs are SQLite's sweet spot.** SFTP writes in ~32KB chunks. The appfileformat page's "BLOBs < 100KB are faster inline in SQLite than as filesystem files" finding is *exactly* this case. redb could do it too, but now you have two databases for one transaction. + +2. **The chunks, the path tree, and the honker notification are one transaction.** No dual-write between "where chunks live" and "where the path tree lives." No fsync ordering problem (the thing iroh's DESIGN.md spent the most words on — "files are hard," the bitfield/data/outboard write-ordering problem). One WAL, one commit boundary. + +3. **honker coordinates the session.** Named locks on the path prevent concurrent writers from stomping each other (not yet wired in the POC — see Open Unknowns). The write session can be a honker job — enqueue on open, track progress, close completes the job, notify fires on merge. Crash leaves an orphaned job you can see and clean up. + +--- + +## Architectural Direction (Established by the POC) + +### The stack + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SFTP / SSH (russh-sftp Handler trait) — not yet wired │ +│ open / read / write / close / readdir / rename / unlink / ... │ +│ maps 1:1 to PathTree + WriteSession API │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ PathTree (SQLite) │ +│ buckets, branches, paths, tombstones │ +│ recursive-CTE chain walk for branch-aware resolve │ +│ WriteSession: branch-on-write, merge-on-close │ +│ honker notify() inside every mutation txn │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌──────────────┴──────────────┐ + ▼ ▼ +┌─────────────────────────────┐ ┌────────────────────────────────┐ +│ iroh-blobs (content store) │ │ honker (in the SQLite file) │ +│ BLAKE3 content addressing │ │ notify/listen (watch) │ +│ MemStore (POC) / │ │ durable queues (sync/replicate)│ +│ FsStore (production, redb) │ │ named locks (writer coord) │ +│ Tags + GC (mark-sweep) │ │ scheduler (maintenance cron) │ +└─────────────────────────────┘ └────────────────────────────────┘ +``` + +### Layer separation + +| Concern | Layer | Why | +|---|---|---| +| Path → content hash mapping | SQLite path tree | Hierarchical, indexed, transactional, schema-as-doc | +| Content bytes | iroh-blobs | Content-addressed, dedup, verified streaming, network transfer | +| Path-tree mutations | SQLite txn | Atomic, crash-safe, single WAL | +| Filesystem events (watch/inotify) | honker notify() | Atomic with the mutation, wake-on-commit not poll | +| Background sync/replication kicks | honker queues | Transactional outbox: enqueue in same txn as mutation | +| Writer coordination | honker named locks | Prevent concurrent writers on same path | +| Branching / forking | branches table + chain walk | Fossil-style, content shared by hash, only path edges diverge | +| Multi-tenancy | bucket_id column | Free — just a where-clause, auth is an adapter problem | +| Chunked writes | write_sessions + write_chunks | Branch-on-write, merge-on-close, crash-safe per-chunk | + +### SFTP mapping (conceptual, not yet wired) + +The russh-sftp `Handler` trait (`/workspace/russh-sftp/src/server/handler.rs`) is a near 1:1 mirror of POSIX FS syscalls translated to SFTP packets. Each maps onto SQL operations against the path-tree tables: + +| SFTP op | PathTree operation | +|---|---| +| `open(path, O_RDONLY)` | `resolve(bucket, branch, path)` → `Link` → iroh `BlobReader` | +| `open(path, O_WRONLY)` | `WriteSession::open(bucket, branch, path)` | +| `write(handle, offset, data)` | `WriteSession::write_chunk(offset, data)` | +| `close(handle)` | `WriteSession::close()` → hash → merge → notify | +| `readdir(path)` | `list_dir(bucket, branch, path)` | +| `stat`/`lstat`/`fstat` | `resolve(...)` → entry size/kind (indexed lookup, cheaper than real FS fstat) | +| `rename(from, to)` | `PathTree::rename(...)` — O(1) on edges, content stays in iroh | +| `remove(path)` | `PathTree::unlink(...)` — tombstone, content GC'd later | +| `mkdir`/`rmdir` | `PathTree::mkdir`/`unlink` with `kind=dir` | +| `symlink`/`readlink` | `PathTree::symlink`/`resolve` with `kind=symlink` | +| `extended` | SFTP escape hatch — could expose iroh-specific ops (get ticket, resolve to hash) | + +The russh-sftp client's `File` already implements `AsyncRead + AsyncSeek + AsyncWrite` with pipelined writes (`write_nowait` + ack window), and the iroh `BlobReader` already supports range reads. The two trait surfaces line up. `SeekFrom::End` (round-trip-costly in real SFTP — calls `fstat`) becomes a single indexed SQLite lookup, so it's actually *cheaper* than a real FS fstat. + +### Multi-tenancy / buckets + +The "bucket" concept maps to S3's bucket format (from the rudolfs reference, `/workspace/@alkdev/alknet/docs/research/references/gitlfs/rudolfs-reference.md`). In rudolfs, `StorageKey = (Namespace, Oid)` where `Namespace = (org, project)` — tenant isolation by URL path. In the POC, `bucket_id` on every row achieves the same isolation with a single where-clause. Auth from the SSH/SFTP connection determines which buckets are visible — it's an adapter problem, not a storage problem. + +The rudolfs caching layer (LRU + disk cache → permanent storage, with `fanout()` to stream to both client and cache simultaneously) is a useful pattern for the production version: a local iroh-blobs `FsStore` as cache, a remote iroh node as permanent storage. The decorator composition pattern (`Verify ↔ Encrypted ↔ Cached ↔ Retrying(Disk → S3)`) translates directly: `Verify` becomes BLAKE3 verification (built into iroh), `Cached` becomes the local iroh store, `S3` becomes the remote iroh node. + +--- + +## Open Unknowns (For Future POCs) + +These are the unknowns that remain after the POC. None are feasibility blockers (the basic mechanics work); they are scope/work-quantity questions that affect spec sizing. + +### 1. FsStore (redb + filesystem) vs SQLite — the two-database question (scoping, not feasibility) + +The POC used `MemStore` deliberately — no redb, no fsync rabbit hole, no partial-file lifecycle. The production version needs `FsStore` for persistence of large blobs. The open question: is having *both* redb (iroh's metadata) and SQLite (our path tree) in one process a problem? + +Two embedded databases means two WAL files, two fsync paths, two crash-recovery stories. The likely answer is "fine, they serve different purposes" — redb stores blob metadata + inline data, SQLite stores path trees + write chunks + honker tables. But it needs validation. + +The alternative — forking iroh-blobs to use SQLite instead of redb — is a big maintenance commitment. The POC's write_chunks table proves SQLite can handle chunk-sized inline BLOBs at iroh's granularity, so the swap is mechanically possible. But it should only be done if the two-database coexistence proves problematic, not for aesthetics. A scoping probe would run `FsStore` alongside the SQLite path tree and measure: double-fsync overhead, WAL contention, operational confusion. + +### 2. Incomplete blobs in a distributed context (design, not feasibility) + +The "many agents" scenario has a second incomplete-blob problem: agent B tries to read a file whose hash is in the path tree (inherited from parent) but whose *content* hasn't been downloaded to B's local store yet. What does the read return? Does it block? Does it trigger a fetch? Does it return an error? + +iroh's `BlobReader` errors on missing chunks — but a filesystem caller expects either data or `ENOENT`, not "try again later." This is the seam between "path tree says it exists" and "blob store has the bytes." A design probe would model the fetch-on-read path: resolve → miss → async fetch from a peer that has it → block or return `EIO` temporarily. The honker queue is the coordination mechanism for background fetching. + +### 3. SFTP wiring (mechanical, not design) + +The `Handler` trait maps 1:1 to the `PathTree` + `WriteSession` API (see SFTP mapping table above). Wiring it is straightforward but non-trivial: handle management (open files, open directories), error code mapping (SQLite errors → SFTP status codes), `fsync@openssh.com` extension negotiation, and the `extended` channel for iroh-specific ops. A POC would implement the `Handler` trait and test with an actual `sshfs` mount. + +### 4. honker named locks for writer coordination (mechanical) + +The POC's `multiple_concurrent_write_sessions_same_path` test shows that two sessions on the same path don't corrupt each other's chunks (session-scoped), and last-close wins. But a real filesystem needs explicit locking: `honker_lock_acquire('path:::', writer_id, timeout)` to prevent concurrent writers from stomping each other. The lock is a SQL function already registered on the connection — it just needs to be called in `WriteSession::open`. Not wired in the POC; straightforward to add. + +### 5. GC and tag management (design) + +iroh-blobs uses tags + mark-sweep GC: blobs are protected from deletion by tags (persistent or temp), and GC walks all tags to find reachable hashes, then sweeps everything else. The path tree needs to manage tags: when a path row points to a `Link`, that blob needs a persistent tag so it survives GC. When a path is tombstoned (unlinked), the tag can be removed and the blob becomes eligible for GC. The mapping from path rows to tags is the design question. A POC would wire `tags.create()` / `tags.delete()` into `upsert_path` / `unlink` and verify GC reclaims orphaned content. + +### 6. Branch chain depth and performance (perf, deferred) + +The recursive CTE walks the full parent chain on every resolve. For shallow chains (2-3 levels, as in "main → agent-a → working-session") this is sub-millisecond. For deep chains (many nested forks, or long-lived agent workspaces with many snapshot branches), performance could degrade. A materialized view or a "resolved paths" cache table (updated on commit) would solve this if it becomes an issue. Worth a perf probe with realistic branch depths before spec. + +### 7. Snapshot / commit semantics (design) + +The POC has branches but no explicit "commit" or "snapshot" operation — a branch is just a name, and writes to it are immediate. A real filesystem (especially one backing git) needs snapshot points: "this branch was at this state at this time." The `branches` table has `created_at` but no snapshot history. The design question: is a snapshot a new branch (Fossil's model), or is it a recorded point-in-time within a branch (git's model)? Fossil's model maps more naturally to the existing recursive CTE. + +--- + +## Test Coverage + +``` +running 15 tests +test abort_leaves_old_version_untouched ... ok +test abort_on_new_file_leaves_no_trace ... ok +test branch_inherits_parent_content ... ok +test branch_modifications_do_not_leak_to_parent ... ok +test chunked_write_close_produces_correct_hash ... ok +test chunked_write_out_of_order ... ok +test concurrent_reader_sees_old_version_during_write ... ok +test content_is_deduped_across_branches ... ok +test large_chunked_file_writes_correctly ... ok +test multi_bucket_isolation ... ok +test multiple_concurrent_write_sessions_same_path ... ok +test put_get_roundtrip ... ok +test rename_is_o1_on_path_edges ... ok +test tombstone_hides_parent_file_on_child ... ok +test watch_fires_on_commit ... ok +``` + +--- + +## POC Structure + +``` +src/ + main.rs # CLI: mkfs, ls, put, get, rm, mv, branch, watch, share-check + schema.rs # SQLite schema (paths, branches, tombstones, write_sessions, + # write_chunks) + honker bootstrap + fs.rs # PathTree: resolve, put_file, mkdir, symlink, unlink, rename, + # list_dir, read_file, open_write + # recursive-CTE chain walk for branch-aware resolve + blob_bridge.rs # iroh MemStore adapter: put_bytes → Link, get_bytes → bytes + branch.rs # Branch listing / creation + write_session.rs # Chunked write: open, write_chunk, close, abort + # branch-on-write, merge-on-close + watch.rs # honker notify/listen wrapper for fs events + lib.rs # re-exports +tests/ + integration.rs # 15 tests: 8 path-tree + 7 write-path +``` + +--- + +## References + +- POC: `/workspace/alknet-filesystem-poc` — `Cargo.toml`, `src/`, `tests/integration.rs` +- SQLite appfileformat: https://sqlite.org/appfileformat.html +- iroh-blobs source (v0.100): `/workspace/iroh-blobs` — `DESIGN.md` (blob store tradeoffs, hybrid approach, files-are-hard), `src/store/fs/` (FsStore, redb tables, EntryState), `src/api/blobs.rs` (Blobs API) +- iroh-blobs published (v0.103): cargo cache — `store/mem.rs` (MemStore), `api/blobs.rs` (AddProgress, BlobReader) +- honker: https://honker.dev/docs/ — `honker-core` API: `attach_honker_functions`, `attach_notify`, `bootstrap_honker_schema`, `apply_default_pragmas` +- honker source: `~/.cargo/registry/src/*/honker-core-0.2.4/src/` — `lib.rs`, `honker_ops.rs` +- russh-sftp source: `/workspace/russh-sftp/src/` — `server/handler.rs` (Handler trait), `client/fs/file.rs` (pipelined AsyncWrite), `client/fs/dir.rs` +- rudolfs reference: `/workspace/@alkdev/alknet/docs/research/references/gitlfs/rudolfs-reference.md` — decorator pattern, LRU cache, namespace/bucket isolation, fanout streaming +- iroh-blobs research docs: `/workspace/@alkdev/alknet/docs/research/references/iroh/iroh-blobs/` — overview, storage, key types, transfer protocol +- russh-sftp research docs: `/workspace/@alkdev/alknet/docs/research/references/ssh/russh-sftp/` — overview, client API, server API, wire protocol \ No newline at end of file