# Design — `add-drive-uri-writes` Non-normative. The normative contract is `./specs/drive-uri/spec.md`. ## Per-scheme operation matrix | Operation | `drive://` | `drive+mesh://` | `drive+template://` | |---|---|---|---| | `set(key, bytes)` (resolver `write`) | `Drive.write(key, bytes, { signal })` → `void` | `Drive.write(key, bytes, { signal })` → `void`; mesh advertise NOT triggered | throw `DRIVE_URI.TEMPLATE_READ_ONLY` | | `delete(key)` (resolver `remove`) | `Drive.delete(key, signal)` → `boolean` | `Drive.delete(key, signal)` → `boolean`; mesh "no-longer-available" advert NOT triggered | throw `DRIVE_URI.TEMPLATE_READ_ONLY` | | `clear()` | throw `DRIVE_URI.WRITE_UNSUPPORTED` (Drive-wide wipe footgun) | same | same | | `createReadStream` | unchanged | unchanged | unchanged | | `get` / `exists` / `list` / `stat` | unchanged | unchanged | unchanged | `clear()` retains `DRIVE_URI.WRITE_UNSUPPORTED` to preserve the existing error contract for that footgun call site. The new `TEMPLATE_READ_ONLY` code is specific to template-backed mutation attempts — operators differentiate "this drive forbids wholesale wipes" (a config-time concern) from "this URI scheme is intrinsically read-only" (an architectural constraint). ## Error code transition | Old behaviour | New behaviour | Code | |---|---|---| | `set` on `drive://` → throw `WRITE_UNSUPPORTED` | succeeds | (success) | | `set` on `drive+mesh://` → throw `WRITE_UNSUPPORTED` | succeeds | (success) | | `set` on `drive+template://` → throw `WRITE_UNSUPPORTED` | throw `TEMPLATE_READ_ONLY` | `DRIVE_URI.TEMPLATE_READ_ONLY` | | `delete` on `drive+template://` → forwarded to underlying `Drive.delete` (silent footgun: would mutate the hydrated cache, breaking the dehydrated semantics) | throw `TEMPLATE_READ_ONLY` | `DRIVE_URI.TEMPLATE_READ_ONLY` | | `clear()` on any scheme → throw `WRITE_UNSUPPORTED` | unchanged | `DRIVE_URI.WRITE_UNSUPPORTED` | `DRIVE_URI.WRITE_UNSUPPORTED` is intentionally retained — consumers who already asserted on it for the `clear()` path continue to work; we narrow its meaning rather than churn the constant. ## Mesh write — propagation timing and observability `drive+mesh://` writes complete when local persist is durable. Specifically: 1. `Drive.write(path, bytes, { signal })` returns when the installed `chunksPlugin` middleware has resolved (encrypted, chunked, persisted to the configured storage). 2. Mesh re-advertise (`mesh.advertise(fileId, bitset, totalChunks)`) is NOT triggered by the URI surface. This matches direct-`Drive` behaviour and avoids surprising consumers who manage `GenerationTracker` / `AdvertiseThrottle` lifecycles themselves. 3. The plugin emits a span event `drive_uri.mesh_write_local_only` on every successful `drive+mesh://` write, with attributes `{ scheme: "drive+mesh", path }`. Operators can grep the trace to confirm the contract holds in their environment. 4. Consumers wanting gossip propagation invoke `mesh.advertise(fileId, ...)` after the write resolves. Propagation thereafter follows the existing `GossipEngine` timing (best-effort, backoff-throttled, observable via the mesh package's own events). This deliberate choice preserves separation of concerns: the URI surface is a routing layer; mesh participation is a Drive-tier policy. ## Key-rotation impact (none, by inheritance) A write may produce data encrypted under a NEW `KeySlotPublic` version. The URI surface does no key derivation, no slot resolution, and no grant signing — those live in `chunksPlugin` and in `Drive.hydrateFrom`'s `key:grant` subscription. The read path validates retroactive grants today; nothing in the write path needs to change for new key versions to be honoured on subsequent reads. No new wiring required at the URI tier. ## AbortSignal threading - `resolver.write(uri, body, signal?)` passes `signal` to `InstrumentedDriveAdapter.set(key, value, signal)`. - `InstrumentedDriveAdapter.set` updates its signature to accept `AbortSignal` (currently typed `set(key, value)` per `StorageLike`; we add the third arg as an optional propagated to `Drive.write({ signal })`). Pre-aborted signals short-circuit to `DRIVE_URI.ABORTED` before any work. - `resolver.remove(uri, signal?)` already threads `signal` to `adapter.delete(key)`; we widen the override to accept and forward `signal` to `Drive.delete(path, signal)`. `StorageLike` itself does not specify `signal` on `set`/`delete`. `DriveStorageLikeAdapter` adds the parameter as a tolerated extension; the resolver already passes it via the streaming-write surface (`createWriteStream(key, body, signal)`), so the precedent is established. ## File-layout impact New / edited files (under `packages/drive/drive-uri/src/`): ``` adapter/ drive-storage-like.ts # widen set/delete to accept signal; forward through drive-storage-like.md # doc update — note new TEMPLATE_READ_ONLY case error-codes/ error-codes.ts # add TEMPLATE_READ_ONLY to defineCodes error-codes.md # update; describe narrowed WRITE_UNSUPPORTED role plugins/ drive/ drive.streams.ts # remove unconditional WRITE_UNSUPPORTED; per-scheme dispatch drive.streams.md # doc update drive-template/ drive-template.ts # override set/delete → TEMPLATE_READ_ONLY drive-template.md # doc update otel/ otel.ts # add `mesh_write_local_only` span-event constant otel.md # doc update README.md # update Features / Modules sections tests/integration/ drive-roundtrip-write.test.ts # NEW — real Drive, write+read cycle drive-mesh-roundtrip-write.test.ts # NEW — mesh-configured Drive, assert no implicit advertise template-write-rejection.test.ts # NEW — TEMPLATE_READ_ONLY round-trip ``` All new code stays inside the existing 300-line / 500-line file budgets; the changed `drive.streams.ts` currently sits at 148 lines and the per-scheme branching adds ~30 lines. ## Trade-offs considered - **Per-scheme `set` dispatch via `if (scheme === "drive+template")` vs. polymorphic adapter subclass.** Chose the inline branch — only one scheme has the constraint and a subclass would duplicate the entire instrumented method body. If a fourth scheme appears, revisit. - **Auto-advertise on mesh writes.** Rejected: violates the direct-`Drive` contract, would surprise consumers who run `GenerationTracker` themselves, and forces every mesh-write call site through an advertise budget. The span event gives operators visibility without enforcing policy. - **Reusing `WRITE_UNSUPPORTED` for template rejections.** Rejected: it conflates a temporary state (the deferred scope this change closes) with a permanent architectural constraint (templates are signed). Distinct codes let consumers branch correctly. - **Removing `WRITE_UNSUPPORTED` entirely.** Rejected: it would churn the public surface (consumers may already assert on it for the `clear()` path) and §2c forbids deleting a registered code without a real reason. Narrowing the role is the right move. ## Cross-reference This change is the second consumer of structural `DriveLike` (after `add-drive-range-reads`), strengthening the case for `hoist-drivelike-to-types`. Independent landing — `drive-uri` imports `DriveLike` from `@suaveplan/drive` today and will move to `@suaveplan/types` once `hoist-drivelike-to-types` archives.