Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
# Design — `complete-drive-uri`
Non-normative. Normative requirements live in `./specs/drive-uri/spec.md` and `./specs/drive/spec.md`.
## Goals
1. Land every URI operation (`get`, `set`, `delete`, `stat`, `exists`, `list`, `createReadStream`, `createWriteStream`) on every scheme (`drive://`, `drive+mesh://`, `drive+template://`) with first-class semantics.
2. Thread `AbortSignal` through every buffered op (today's spec footnote becomes a spec requirement).
3. Land `{ offset, length }` range reads end-to-end (resolver → adapter → `Drive.createReadStream` → chunks-plugin middleware → mesh `ChunkRequest.specific|bitset` pivot).
4. Per-scheme query-string options (`?advertise`, `?nonce`, `?signal-timeout-ms`) with explicit redaction policy.
5. Comprehensive `DRIVE_URI.*` error registry (14 codes total — every code reachable from a real call site).
6. Re-run `runUriPluginConformance` over the full op matrix for all three plugins.
## Per-scheme operation matrix (canonical)
| Op | `drive://` | `drive+mesh://` | `drive+template://` |
|---------------------|----------------------------------|----------------------------------------------------------|------------------------------------------------|
| `get(key, signal?)` | `Drive.read(key, { signal })` | same | hydrate (memoised) → `Drive.read(key, { signal })` |
| `set(key, v, sig?)` | `Drive.write(k, v, { signal })` | `Drive.write` + opt-in `MeshAdvertiseHook.advertise(...)` on `?advertise=true` | throw `TEMPLATE_READ_ONLY` |
| `delete(key, sig?)` | `Drive.delete(k, signal)` | `Drive.delete` + opt-in `MeshAdvertiseHook.unadvertise(...)` | throw `TEMPLATE_READ_ONLY` |
| `stat(key, sig?)` | `Drive.stat(k, signal)` → `StatResult` | same; cache-aware (the post-hydrate `Drive.stat` is cheap) | post-hydrate `Drive.stat` |
| `exists(key, sig?)` | `Drive.getPresence(k).chunksPresent > 0` → fallback `stat !== null` | same | post-hydrate; `getKeyStatus()` reports `'encrypted-unlocked'` for present-and-decryptable |
| `list(prefix, opts)`| `Drive.list(prefix, opts)` → re-serialised via `UriPlugin.build` | same | post-hydrate |
| `createReadStream(k, { offset?, length?, signal? })` | `Drive.createReadStream(k, opts)` | same; chunks-plugin honours `ctx.range`; mesh pivots between `ChunkRequest.specific` (≤64 chunks) and `bitset` (>64) | post-hydrate |
| `createWriteStream(k, body, sig?)` | pipe `body` into `Drive.createWriteStream(k, sig)` | same | throw `TEMPLATE_READ_ONLY` |
| `clear()` | throw `WRITE_UNSUPPORTED` (footgun) | same | same |
## Error code registry — 14 codes, every code reachable
| Code | Call site |
|-------------------------------|---------------------------------------------------------------------------------------------|
| `INVALID_URI` | every plugin's `parse` (malformed authority/path/scheme). |
| `DRIVE_ID_MISMATCH` | `driveUriPlugin.parse` when URI authority ≠ `options.driveId`. |
| `TEMPLATE_NOT_AVAILABLE` | `driveTemplateUriPlugin.beforeOp` when `templateProvider.fetchTemplate` rejects. |
| `TEMPLATE_INVALID` | `driveTemplateUriPlugin.beforeOp` when `Drive.hydrateFrom` rejects. |
| `TEMPLATE_READ_ONLY` | `driveTemplateUriPlugin` adapter's `set`/`delete`/`createWriteStream`. |
| `WRITE_UNSUPPORTED` | `DriveStorageLikeAdapter.clear()` only (narrowed). |
| `CAPABILITY_MISSING` | construction-time validation: missing `drive`/`driveId`/`templateProvider`/`advertise` hook.|
| `ABORTED` | pre-aborted `AbortSignal` short-circuit on any op; `?signal-timeout-ms` upper bound elapsed. |
| `RANGE_INVALID` | adapter `createReadStream` when `Drive.createReadStream` throws `DRIVE.RANGE_INVALID`. |
| `RANGE_OUT_OF_BOUNDS` | adapter `createReadStream` when `Drive.createReadStream` throws `DRIVE.RANGE_OUT_OF_BOUNDS`. |
| `MESH_PROPAGATION_TIMEOUT` | `driveMeshUriPlugin` adapter when opt-in advertise hook rejects or exceeds its budget. |
| `STAT_UNAVAILABLE` | adapter `getMetadata` when `Drive.stat` cannot answer for a key the URI surface needs. |
| `LIST_UNSUPPORTED` | adapter `list` when the bound `Drive`'s storage lacks `list`. |
| `INVALID_QUERY` | query parser when an unknown key or malformed value (`?advertise=maybe`) is encountered. |
Every code lives in `defineCodes("DRIVE_URI", [...] as const)`. No code is "declared but unused" (CLAUDE.md §2c).
## AbortSignal threading model
The `StorageLike` interface (current shape on `@suaveplan/types`) does not declare `signal` on `get`/`set`/`delete`/`exists`/`list`. The drive-uri adapter widens those signatures as a tolerated extension and forwards to the corresponding `Drive` methods, all of which already accept `signal?` (via `ReadOptions`, `WriteOptions`, the bare arg, etc.). The widening is documented on `drive-storage-like.md` and is opaque to the resolver — `StorageUriResolver` already passes its own signal into the buffered `get`/`set`/`delete` paths today; the adapter simply gains the ability to honour it.
Pre-aborted signal handling: every op checks `signal?.aborted === true` BEFORE any `Drive` call; on hit, throws `DriveUriError(DRIVE_URI.ABORTED, …, { scheme, path: key })`. The check sits in `InstrumentedDriveAdapter` so all three plugins inherit it.
`?signal-timeout-ms=<int>` parses to a per-op `AbortSignal.timeout(ms)` linked with the caller signal via `AbortSignal.any([callerSignal, timeoutSignal])`. On elapse, the op rejects with `DRIVE_URI.ABORTED` and a `reason: "signal-timeout-ms"` discriminant.
## Range threading model
```text
resolver.open(uri, { offset, length, signal })
→ openViaPlugin(plugin, uri, key, { offset, length, signal }) # storage-uri (existing)
→ plugin.adapter.createReadStream(key, { offset, length, signal })
→ Drive.createReadStream(path, { offset, length, signal }) # NEW overload
→ file-operations/streams.ts → defaultCreateReadStream / native pass-through
→ DriveStorage.createReadStream(path, { offset, length, signal }) if storage.capabilities.nativeRange
→ OR slicing transform around defaultCreateReadStream
→ middleware ctx.range = { offset, length } # populated by Drive
→ chunksPlugin's read middleware reads ctx.range
→ chunksPlugin.#reassemble(fileId, range) emits only chunks ∩ [offset, end)
→ over mesh: parallelPuller(range) builds ChunkRequest.specific (span ≤ 64) | bitset (span > 64)
```
`StorageUriResolver.open(uri, { offset, length, signal })` reaches the adapter's `createReadStream` via the existing `openViaPlugin` indirection. The resolver already calls `adapter.createReadStream(key, signal)`; we widen it to accept an options bag (matches the `StreamingStorageExtensions` interface signature, which is already `signal?: AbortSignal` — extended in lock-step to `signal | { offset?, length?, signal? }` per the same `signalOrOptions` discriminator used inside `Drive`).
## Mesh re-advertise opt-in
Default behaviour (no `?advertise`): write completes when local persistence is durable; no gossip; `drive_uri.mesh_write_local_only` span event records the contract holds. This matches direct-`Drive` behaviour today.
Opt-in (`?advertise=true`): after the local write resolves, the plugin invokes `options.advertise.advertise(fileId, bitset, totalChunks, signal)`. The hook is supplied by the consumer at construction; its absence at `parse` time when `?advertise=true` is requested raises `DRIVE_URI.CAPABILITY_MISSING` with `reason: "advertise"`. The hook's rejection (any throw, including timeout) is re-wrapped as `DRIVE_URI.MESH_PROPAGATION_TIMEOUT` with the original on `cause`.
`fileId`, `bitset`, and `totalChunks` are sourced from the freshly-written file: `fileId` from the chunks-plugin via `drive.getStorage("default")` lookup (the chunks plugin's `pathToFileId` map is internal — we surface it via a structural `ChunkSource`-bearing hook), `bitset` and `totalChunks` from the plugin's manifest. Consumers wiring `?advertise=true` MUST be running with `chunksPlugin`; if not, the hook hand-rolls the values from `Drive.stat`. The `drive-uri` plugin holds no chunks-plugin dependency — the lookup goes through a structural duck-type.
`?advertise=false` (explicit): identical to the default; documented so the URI is self-describing.
`?advertise` on `drive://` or `drive+template://`: parse error → `DRIVE_URI.INVALID_QUERY`.
## Query-string parsing & redaction
Parser: every plugin's `parse` invokes `parseQuery(parts.query)` (existing `parts.query` on `ParsedUri` from `storage-uri`'s `parseUri`). Recognised keys per scheme:
| Scheme | Recognised keys |
|---------------------|------------------------------------------------|
| `drive://` | `signal-timeout-ms` |
| `drive+mesh://` | `signal-timeout-ms`, `advertise` |
| `drive+template://` | `signal-timeout-ms`, `nonce` |
Unknown keys raise `DRIVE_URI.INVALID_QUERY` with `key` and `value` on the error.
Redaction: `redactUri` (already strips `?` and `#`) is applied to:
- every `drive.uri` span attribute,
- every emitted EventBus event's `uri` field,
- every `DriveUriError.uri` field carried in serialised wire-format errors (`SuaveplanError.toWire()` adapter).
Sensitive keys (`nonce`, `auth=…` future) are NEVER stored in cleartext on a span, log, or event. The parsed value is held in process memory only for the duration of the dispatched call.
## EventBus emission
The wrapped `Drive`'s `events: EventBusLike | undefined` is used. When present:
| Topic | Payload |
|----------------------|--------------------------------------------------------------------------------------------------------|
| `drive_uri:write` | `{ scheme, key, uri (redacted), outcome: "success"|"error", code?, durationMs, fileId?, range? }` |
| `drive_uri:delete` | `{ scheme, key, uri (redacted), outcome, code?, durationMs, fileId? }` |
| `drive_uri:mesh_advertised` | `{ scheme: "drive+mesh", fileId, totalChunks, durationMs }` — emitted on successful opt-in advertise |
Downstream consumers (`@suaveplan/vault-sync`, `drive-sharing`, future audit-log) subscribe at the bus level. Absence of `Drive.events` (the default) is a silent no-op.
## File layout
```text
packages/drive/drive-uri/src/
adapter/
drive-storage-like.ts # MODIFIED — every op gains signal?; new createWriteStream + getMetadata
drive-storage-like.md # MODIFIED
drive-storage-like.test.ts # MODIFIED — abort threading, write-stream, getMetadata
error-codes/
error-codes.ts # MODIFIED — 14 codes
error-codes.md # MODIFIED
error-codes.test.ts # MODIFIED — exhaustive registry shape + reachability assertions
events/
events.ts # ADDED — typed topic constants + emit helper
events.md # ADDED
events.test.ts # ADDED
query/
parse-query.ts # ADDED — per-scheme query parser
parse-query.md # ADDED
parse-query.test.ts # ADDED
otel/
otel.ts # MODIFIED — range attr, advertise attr, chunks histogram
otel.md # MODIFIED
otel.test.ts # MODIFIED
plugins/
drive/
drive.ts # MODIFIED — query parsing wired in
drive.streams.ts # MODIFIED — per-scheme set/delete/writeStream dispatch
drive.streams.md # MODIFIED
drive.test.ts # MODIFIED — full op matrix
drive.streams.test.ts # MODIFIED — range + signal + writeStream
drive-mesh/
drive-mesh.ts # MODIFIED — ?advertise opt-in via MeshAdvertiseHook
drive-mesh.md # MODIFIED
drive-mesh.test.ts # MODIFIED — advertise spy, full op matrix, query parsing
drive-template/
drive-template.ts # MODIFIED — set/delete/writeStream throw TEMPLATE_READ_ONLY; ?nonce forwarded
drive-template.md # MODIFIED
drive-template.test.ts # MODIFIED — full op matrix, nonce forwarding
types/
plugin-options.types.ts # MODIFIED — MeshAdvertiseHook, widened TemplateProvider signature
plugin-options.types.md # MODIFIED
conformance/
conformance.test.ts # MODIFIED — full read/write/delete/stat/list/exists/range matrix
tests/integration/
drive-roundtrip-write.test.ts # ADDED
drive-mesh-roundtrip-write.test.ts # ADDED — advertise opt-in spy
template-write-rejection.test.ts # ADDED
drive-range-roundtrip.test.ts # ADDED — read bytes [60_000, 70_000) of a 200 KiB file
drive-stat-list-exists.test.ts # ADDED — full read-side matrix on a real Drive
packages/drive/drive/src/
drive/drive.ts # MODIFIED — createReadStream overload, range plumbing
file-operations/streams.ts # MODIFIED — overload signatures, slicing transform
file-operations/streams.range.ts # ADDED — pure slicing transform
file-operations/streams.range.md # ADDED
file-operations/streams.range.test.ts # ADDED — boundary table
types/drive.types.ts # MODIFIED — RangeReadOptions, DriveStorage.capabilities.nativeRange
error-codes.ts # MODIFIED — RANGE_INVALID, RANGE_OUT_OF_BOUNDS
```
Every file stays inside the 300-line source / 500-line test budgets. The largest existing file (`drive.streams.ts` at 148 lines) gains ~80 lines of per-scheme dispatch — under budget.
## Trade-offs considered
- **One change vs. two.** Combining the two narrow follow-ups removes a redundant conformance / docs / dependent cycle and lets the package land at a coherent surface. Cost: a larger PR. Mitigation: the per-scheme matrix is the same in either factoring; the file count is identical; verification scope only widens by the integration tests that would have been added in either change separately.
- **Per-scheme `if`-branch vs. polymorphic subclass for `set`/`delete`/`createWriteStream`.** Chose per-scheme branch — only one scheme has the constraint and a subclass would duplicate the instrumented method body. If a fourth scheme appears, revisit.
- **Auto-advertise vs. opt-in on mesh write.** Opt-in via `?advertise=true`. Auto-advertise would surprise consumers managing `AdvertiseThrottle`/`GenerationTracker` lifecycles themselves; the opt-in surfaces the contract on the URI itself (self-describing).
- **`MeshAdvertiseHook` in `drive-uri` vs. importing `@suaveplan/drive-mesh`.** Hook lives in `drive-uri`. Importing the mesh package would couple the URI tier to a concrete transport and pull a heavy graph into a leaf-ish adapter.
- **EventBus on `Drive.events` vs. dedicated bus in `drive-uri`.** Reuse `Drive.events`. A dedicated bus would force two subscriptions for every consumer that already listens to `Drive`-tier events.
- **`?signal-timeout-ms` per-op vs. construction-time.** Per-op (URI carries it). Construction-time would force one timeout policy across every URI a plugin handles; per-op lets callers vary by request.
- **Reusing `WRITE_UNSUPPORTED` for templates vs. distinct `TEMPLATE_READ_ONLY`.** Distinct. `WRITE_UNSUPPORTED` was a *temporary* state code; templates are *permanently* read-only. Distinct codes let consumers branch correctly.
- **`{ offset, length }` vs. `{ start, end }` for range.** `offset`/`length` — matches HTTP `Range: bytes=offset-(offset+length-1)` and avoids the inclusive-vs-exclusive ambiguity that bites every range API at least once.
- **Throw vs. clamp on range overflow.** Throw `RANGE_OUT_OF_BOUNDS`. Clamping hides upstream bugs; throwing matches the rest of `DRIVE_*` / `DRIVE_URI.*` explicit-failure style.
## Cross-reference
This change consolidates the two superseded deferrals: `_superseded-narrow-deferrals-2026-05-17/add-drive-uri-writes` and `…/add-drive-range-reads`. The reasoning and design notes from both are folded into this single change; the superseded folders are kept as research history.
`DriveLike` hoist to `@suaveplan/types` is an independent change (`hoist-drivelike-to-types`) — landing order is irrelevant; this change consumes the structural shape from whichever has merged first.

View File

@@ -0,0 +1,197 @@
# Complete `@suaveplan/drive-uri` — full per-scheme write + range-read parity, every operation, every option
**Package**: `@suaveplan/drive-uri` (+ MODIFIED surface on `@suaveplan/drive`)
**Status**: PROPOSED
**Type**: capability-completion (single change supersedes the two narrow follow-ups)
**Supersedes**: `openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-drive-uri-writes/` AND `…/add-drive-range-reads/` — both deferrals land in this single change.
**Depends on**: `add-drive-uri` (archived `2026-05-16`), `add-storage-uri` (archived).
- **Design**: ./design.md
- **Spec deltas**:
- ./specs/drive-uri/spec.md (primary)
- ./specs/drive/spec.md (MODIFIED — `createReadStream` range overload)
## Why
`add-drive-uri` shipped read-only with two explicit deferrals — **writes** and **range reads** — plus a documented gap on **buffered-read AbortSignal threading** (carried as a footnote in the canonical spec). Two narrow follow-ups (`add-drive-uri-writes`, `add-drive-range-reads`) were drafted, then superseded so the package can land at functional completeness in a single coherent change. This change closes every gap and brings `drive-uri` to full parity with what `Drive` itself exposes, so every URI operation is supported with first-class semantics.
Concretely, the gaps closed:
| # | Gap | Source of deferral |
|---|--------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| 1 | `drive://` writes / removes / write-streams | `add-drive-uri` proposal § Out of scope (line 32) |
| 2 | `drive+mesh://` writes / removes / write-streams | `add-drive-uri` proposal § Out of scope (line 32) |
| 3 | `drive+template://` writes permanently locked-down with own code | `add-drive-uri-writes` proposal — `DRIVE_URI.TEMPLATE_READ_ONLY` rather than the temporary `WRITE_UNSUPPORTED` |
| 4 | Range reads `{ offset, length }` on all three schemes | `add-drive-uri` proposal § Out of scope (line 35); `add-drive-range-reads` |
| 5 | Chunk-aware mesh range optimisation (`specific` ↔ `bitset` pivot) | `add-drive-range-reads` proposal |
| 6 | `AbortSignal` threading through buffered `get`/`set`/`delete`/`stat`/`exists`/`list` paths | Existing canonical spec footnote (lines 7-9 of `openspec/specs/drive-uri/spec.md`) |
| 7 | `createWriteStream(key, body, signal?)` adapter surface | Not surfaced today — `StorageUriResolver.write` falls back to buffered drain |
| 8 | First-class `stat` returning `StatResult` (size / lastModified) | `getMetadata` hook on the adapter — never wired |
| 9 | First-class `list` with prefix + pagination via `Drive.list` | `DriveStorageLikeAdapter.list` exists but lacks resolver re-serialisation through `UriPlugin.build` |
| 10 | Cheap `exists` via `Drive.getPresence` / `getKeyStatus` (no buffered `read`) | Today's `exists` calls `Drive.stat` — fine for local, expensive for template-backed |
| 11 | Per-scheme query-string options (`?nonce=…` for templates, `?advertise=true|false` for mesh) | Never surfaced; need redaction policy |
| 12 | Mesh re-advertise opt-in via `?advertise=true` query param + EventBus emission | `add-drive-uri-writes` left this consumer-controlled by default; this change keeps the default but adds an opt-in |
| 13 | OTel span attributes per op with backend identification + range tag | `add-drive-range-reads` proposal |
| 14 | EventBus emission on every write/delete for downstream sync | `add-drive-uri-writes` design tangentially |
| 15 | Exhaustive `DRIVE_URI.*` error registry (read-side, write-side, template-read-only, range, mesh-propagation, capability) | Today's registry is 7 codes; ships at 14 |
| 16 | `runUriPluginConformance` re-run against full read/write/delete/stat/list/exists matrix on all three plugins | `add-drive-uri-writes` proposal |
The composition model is unchanged: three `UriPlugin` factories plug into the existing `StorageUriResolver`. No new resolver class. No re-implementation of any chunk codec, mesh transport, or template-signing logic — those stay inside `Drive` and its installed plugins.
## What changes
### `@suaveplan/drive` (MODIFIED)
- MODIFIED: `Drive.createReadStream(path, signalOrOptions?)` — overloaded. Bare `AbortSignal` preserved; new `RangeReadOptions` form accepts `{ offset?, length?, signal? }`. `instanceof AbortSignal` discriminator at runtime. Synchronous validation: `Number.isInteger(x) && x >= 0 && Number.isFinite(x)`; failures throw `DRIVE.RANGE_INVALID` BEFORE stream construction.
- MODIFIED: `Drive.createWriteStream(path, signal?)` — unchanged signature; reused by adapter's `createWriteStream` wrapper.
- ADDED: `RangeReadOptions` interface on `src/types/drive.types.ts`.
- ADDED: `DRIVE.RANGE_INVALID`, `DRIVE.RANGE_OUT_OF_BOUNDS` codes in `DRIVE_CODES` registry (11 → 13 codes).
- MODIFIED: `DriveStorage.capabilities?.nativeRange?: boolean` flag. `defaultCreateReadStream` honours range via slicing transform; native-range storages forward `{ offset, length }` to `storage.createReadStream`.
- MODIFIED: `DriveMiddleware` context gains `range?: { offset: number; length?: number }`. Chunks-plugin reassemble path (out of scope for THIS package; covered in design.md as a follow-up to land within the same wave) honours it.
### `@suaveplan/drive-uri` (the bulk of the change)
- ADDED — per-scheme per-op matrix (full):
| Operation | `drive://` | `drive+mesh://` | `drive+template://` |
|-------------------|---------------------------------------------|--------------------------------------------------------------------------|--------------------------------------|
| `get` | `Drive.read(key, { signal })` | `Drive.read(key, { signal })` | hydrate-then-read |
| `set` (write) | `Drive.write(key, bytes, { signal })` | `Drive.write` + optional `mesh.advertise(…)` via `?advertise=true` opt-in | `DRIVE_URI.TEMPLATE_READ_ONLY` |
| `delete` | `Drive.delete(key, signal)` | `Drive.delete` + optional `mesh.unadvertise(fileId)` on opt-in | `DRIVE_URI.TEMPLATE_READ_ONLY` |
| `stat` | `Drive.stat(key, signal)` → `StatResult` | same; cache-aware on template hits | same (post-hydrate) |
| `exists` | `Drive.getPresence(key)` cheap path → fallback `stat` | same | same (post-hydrate) |
| `list` | `Drive.list(prefix, { signal, limit, cursor })` → keys re-serialised via `UriPlugin.build` | same | same |
| `createReadStream`| `Drive.createReadStream(key, { offset, length, signal })` | same; chunk-aware mesh request via `ChunkRequest.specific` ↔ `bitset` pivot | same |
| `createWriteStream`| `Drive.createWriteStream(key, signal)` | same | `DRIVE_URI.TEMPLATE_READ_ONLY` |
| `clear` | `DRIVE_URI.WRITE_UNSUPPORTED` (footgun) | same | same |
- ADDED — `DRIVE_URI_CODES` registry grows 7 → **14 codes** via `defineCodes("DRIVE_URI", […] as const)`. New codes:
- `TEMPLATE_READ_ONLY` — permanent template-mutation rejection.
- `RANGE_INVALID` — invalid `{ offset, length }` per-scheme echo of `DRIVE.RANGE_INVALID`.
- `RANGE_OUT_OF_BOUNDS` — per-scheme echo of `DRIVE.RANGE_OUT_OF_BOUNDS` / `CHUNKS.RANGE_OUT_OF_BOUNDS`.
- `MESH_PROPAGATION_TIMEOUT` — explicit `?advertise=true` write where the consumer-supplied advertise hook rejects or times out.
- `STAT_UNAVAILABLE` — underlying storage cannot answer `stat` and the URI surface needs a positive answer.
- `LIST_UNSUPPORTED` — the bound `Drive` does not expose `list` (the AbstractStorage backing it lacks the method).
- `INVALID_QUERY` — query-string option is malformed (e.g. `?advertise=maybe`).
- **Retained**: `INVALID_URI`, `DRIVE_ID_MISMATCH`, `TEMPLATE_NOT_AVAILABLE`, `TEMPLATE_INVALID`, `WRITE_UNSUPPORTED` (now narrowed to `clear()` call site only), `CAPABILITY_MISSING`, `ABORTED`.
- ADDED — `DriveUriError` discriminants extend with `range?: { offset: number; length?: number }`, `fileId?: string`.
- ADDED — `AbortSignal` threading on every buffered op: `get`/`set`/`delete`/`stat`/`exists`/`list` accept and forward `AbortSignal` to the underlying `Drive` method. Pre-aborted signal → `DRIVE_URI.ABORTED` short-circuit before any `Drive` call.
- ADDED — `InstrumentedDriveAdapter.createWriteStream(key, body, signal?)`: per-scheme dispatch (`drive`/`drive+mesh` → forward; `drive+template` → throw `TEMPLATE_READ_ONLY`).
- ADDED — `InstrumentedDriveAdapter.getMetadata(key, signal?): Promise<StatResult | undefined>`: translates `Drive.stat` into `StatResult { size, lastModified, etag?, contentType? }` using `DriveStats.checksum` (etag) and `Drive.stat().modifiedAt` (lastModified). `StorageUriResolver.stat()` picks this up automatically via the existing `StreamingStorageExtensions.getMetadata` duck-type.
- ADDED — Per-scheme `UriPlugin.build` round-trips `meta` (`driveId`/`swarmId`/`templateId`) **and** preserves any incoming query string when supplied via `meta.query`. The resolver's `list` re-serialiser receives prefix-listed keys and produces fully-qualified URIs that round-trip through `parse`.
- ADDED — Query-string parsing on every plugin:
- `drive+mesh://swarm/path?advertise=true|false` → opt-in mesh re-advertise on write/delete.
- `drive+template://template/path?nonce=…` → forwards `nonce` to `templateProvider.fetchTemplate(templateId, { nonce, signal })` (the provider is widened with an optional second `options` bag — back-compat preserved).
- `?signal-timeout-ms=<int>` (any scheme) → upper bound on a single-op latency before raising `DRIVE_URI.ABORTED`.
- Unknown query keys → `DRIVE_URI.INVALID_QUERY` with the offending key.
- ADDED — Query-string redaction policy: every `drive.uri` span attribute, every error's `.uri`, and every emitted EventBus event strips the query string (`redactUri` already exists; extended to also strip the fragment). Sensitive keys (`nonce`, `peers`, future `auth=`) are never logged or surfaced.
- ADDED — Mesh re-advertise via `?advertise=true`: when set on a `drive+mesh://` write, the plugin invokes the consumer-supplied `MeshAdvertiseHook` from `DriveMeshUriPluginOptions.advertise?`. When the hook is absent, the plugin raises `DRIVE_URI.CAPABILITY_MISSING` with `reason: "advertise"` synchronously at construction time *only if* the consumer opts in at the URI level — wiring stays purely additive. The hook is invoked **after** local persistence is durable; failure is wrapped as `DRIVE_URI.MESH_PROPAGATION_TIMEOUT`.
- ADDED — `MeshAdvertiseHook` interface (universal-tier, lives in `types/plugin-options.types.ts`): `advertise(fileId, bitset, totalChunks, signal?): Promise<void>` + `unadvertise(fileId, signal?): Promise<void>`. The hook wraps the consumer's `mesh.advertise(...)` / `mesh.unadvertise(...)` calls; `drive-uri` never depends on `@suaveplan/drive-mesh` directly.
- ADDED — EventBus emission on every write/delete success/failure when the wrapped `Drive` exposes `events`: topics `drive_uri:write` / `drive_uri:delete` carrying `{ scheme, key, uri (redacted), outcome, durationMs, fileId?, range? }`. Used by downstream sync engines (`@suaveplan/vault-sync`, future `drive-sharing` flows) to drive incremental updates. EventBus is read off the `Drive` reference passed at construction; absence is a no-op.
- ADDED — Span event `drive_uri.mesh_write_local_only` on every `drive+mesh://` write where `?advertise=true` is NOT set, so operators can audit that the consumer-controlled gossip contract holds.
- MODIFIED — `DRIVE_URI.WRITE_UNSUPPORTED` retained but narrowed to `DriveStorageLikeAdapter.clear()` only. Every other call site that previously raised it now succeeds or raises a more specific code per the matrix above.
- MODIFIED — `DriveStorageLikeAdapter`:
- `get(key, signal?)`, `set(key, value, signal?)`, `delete(key, signal?)`, `exists(key, signal?)`, `list(options & { signal? })`, `stat(key, signal?)` all accept and forward `AbortSignal`.
- `createReadStream(key, options?: { offset?, length?, signal? })` forwards range to `Drive.createReadStream(key, options)`.
- `createWriteStream(key, body, signal?)`: consumes a `ReadableStream<Uint8Array>`, pipes into `Drive.createWriteStream(key, signal)`.
- `getMetadata(key, signal?)` → `StatResult | undefined` per `StreamingStorageExtensions`.
- `capabilities.nativeRange === true` advertised so `StorageUriResolver` uses the range-aware `createReadStream` directly.
- MODIFIED — Conformance: `runUriPluginConformance` re-run with **full read/write/delete/stat/list/exists/range matrices** for all three plugins. Fixtures:
- `drive://` — full cycle passes.
- `drive+mesh://` — full cycle passes; mesh-advertise spy asserts zero calls when `?advertise` is omitted, exactly one call when `?advertise=true`.
- `drive+template://` — read/stat/exists/list pass; `set`/`delete`/`createWriteStream` reject with `DRIVE_URI.TEMPLATE_READ_ONLY`.
- ADDED — Per-package OTel surface extensions:
- `drive_uri.stream_chunks_fetched` histogram tagged `{scheme}` — chunk count per `createReadStream` call (range narrows this).
- `drive.uri.range = "{offset}-{end}"` span attribute, omitted when no range.
- `drive.uri.has_advertise = "true"|"false"` span attribute on `drive+mesh://` write spans.
- All existing counters preserved.
## Out of scope
Per user instruction, only two carve-outs:
- **NEW URI schemes.** Adding a fourth scheme (e.g. `drive+cdn://`) is a separate change. The three existing schemes ship at full functional completeness here.
- **Templating policy.** Whether templates are mutable (re-issue, multi-version, key rotation policy) lives in `@suaveplan/drive-template`. This change codifies the URI surface's read-only treatment via `TEMPLATE_READ_ONLY` but does not alter template semantics themselves.
## Source mapping
This is a **capability-completion** change for a genesis-original package. Surface anchors:
- `packages/drive/drive-uri/src/plugins/drive/drive.streams.ts:69-84` — current unconditional `set` rejection (replaced).
- `packages/drive/drive-uri/src/adapter/drive-storage-like.ts:75-86` — `set`/`delete` already pass-through internally; the instrumented override now per-scheme dispatches.
- `packages/drive/drive-uri/src/adapter/drive-storage-like.ts:88-119` — `list` exists; resolver re-serialisation via `UriPlugin.build` newly required.
- `packages/drive/drive/src/drive/drive.ts:180-240` — `Drive.write` / `delete` / `stat` / `list` / `createReadStream` / `createWriteStream` consumed verbatim.
- `packages/drive/drive/src/drive/drive.ts:222-230` — `createReadStream` overload landing zone.
- `packages/drive/drive/src/file-operations/streams.ts:50-70` — `nativeRange` slicing-transform insertion point; mirrors existing `nativeStreams` capability flag pattern.
- `packages/drive/drive-mesh/src/mesh/drive-mesh/drive-mesh.ts:176-193` — `advertise(fileId, bitset, totalChunks)` / `unadvertise(fileId)` shape consumed structurally by `MeshAdvertiseHook` (no direct dep).
- `packages/drive/drive-mesh-protocol/src/types/protocol.types.ts:38-42` — `ChunkRequest.specific | bitset` shapes; pivot at 64 chunks per `MAX_CHUNKS_DEFAULT`.
- `packages/storage/storage-uri/src/resolver/resolver.writes.ts:30-75` — confirms `SuaveplanError` subclasses pass through `writeViaPlugin` unchanged, so every new `DRIVE_URI.*` code reaches the caller without a `STORAGE_URI.*` envelope.
- `packages/storage/storage-uri/src/resolver/resolver.streams.ts:24-54` — `openViaPlugin` already forwards to `adapter.createReadStream` (no fallback) when present; range bag passes through via the existing options-args shape.
- `packages/storage/storage-uri/src/types/plugin.types.ts:41-67` — `StreamingStorageExtensions.getMetadata` / `createReadStream` / `createWriteStream` duck-type — `drive-uri` adapter implements all three.
- `openspec/archive/2026-05-16-add-drive-uri/proposal.md` lines 30-38 — original deferral text.
- `openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-drive-uri-writes/proposal.md` — superseded research, reused.
- `openspec/changes/_superseded-narrow-deferrals-2026-05-17/add-drive-range-reads/proposal.md` — superseded research, reused.
### Requirements with no direct source line
| Requirement | Justification |
|----------------------------------------------------------------------------|---------------|
| Single coherent change instead of two narrow follow-ups | The deferrals were carved fine-grained on the assumption write semantics were "non-trivial"; revisiting per-scheme showed they are not. Landing both together avoids two passes through the conformance harness, two rounds of doc churn, and two dependent-sweep cycles. The supersede notice (`_superseded-narrow-deferrals-2026-05-17/`) records the consolidation. |
| `?advertise=true` query opt-in rather than always-advertise on mesh write | Direct `Drive.write` does not auto-advertise — the URI surface MUST preserve that contract by default. The opt-in gives the URI an explicit knob without bending the default. |
| `MeshAdvertiseHook` lives in `drive-uri` not `drive-mesh` | `drive-uri` MUST NOT depend on `drive-mesh` (it would couple the URI tier to a concrete transport). The structural hook lets consumers wire their `mesh.advertise(...)` without `drive-uri` ever importing `@suaveplan/drive-mesh`. |
| EventBus emission off the wrapped `Drive`'s `events` rather than a separate bus | The `Drive` already carries `events: EventBusLike | undefined`(`drive.types.ts:73`); reusing it keeps consumer wiring minimal and matches the precedent set by`key:grant`(`Drive.hydrateFrom`). |
| `TEMPLATE_READ_ONLY` distinct from `WRITE_UNSUPPORTED` rather than reusing | `WRITE_UNSUPPORTED` was a *temporary* state code from `add-drive-uri`; templates are *permanently* read-only via an architectural constraint (authority re-signing). Distinct codes let consumers branch correctly. |
| Range overload via `instanceof AbortSignal` discriminator | The only other shape that satisfies the position-1 type is `RangeReadOptions`; `AbortSignal.prototype` is a stable cross-runtime brand. Duck-typing on `signal?.aborted` would falsely match `RangeReadOptions`. |
| Range registry mirrored on `DRIVE_URI` rather than only `DRIVE` | Consumers branching on the URI surface should not need to know which underlying error class fired; the URI-tier echoes the canonical drive codes via wrapping while preserving `cause`. |
| Cheap `exists` via `Drive.getPresence` over `Drive.stat` | `getPresence` returns a synchronous `ChunkPresence` from the in-memory `FileLocationIndex` for hydrated files (template path) and falls back to `stat` for raw paths — strictly cheaper for `drive+template://` reads. |
| Query-string redaction always-on | A future opt-in `?auth=…` is plausible; codifying redaction now prevents a leak when that opt-in lands. |
## Verification approach
Every requirement in the spec deltas carries a `**Verified by:**` line referencing concrete `it(...)` titles in planned test files. The pre-archive gate (`§13` of `AGENTS.md`) re-runs the full conformance matrix against all three plugins. Integration tests (no mocks per CLAUDE.md rule 2) cover real-Drive round-trips for read, write, delete, stat, list, exists, range, and template hydration paths.
## Migration
| Old | New |
|------------------------------------------------------------------|--------------------------------------------------------------|
| `resolver.write("drive://…", bytes)` → `DRIVE_URI.WRITE_UNSUPPORTED` | succeeds; returns `void` |
| `resolver.write("drive+mesh://…", bytes)` → `DRIVE_URI.WRITE_UNSUPPORTED` | succeeds; mesh advertise opt-in via `?advertise=true` |
| `resolver.write("drive+template://…", bytes)` → `DRIVE_URI.WRITE_UNSUPPORTED` | `DRIVE_URI.TEMPLATE_READ_ONLY` |
| `resolver.remove("drive+template://…")` → silent footgun (forwarded to `Drive.delete`) | `DRIVE_URI.TEMPLATE_READ_ONLY` |
| `resolver.stat("drive://…")` → `undefined` (no `getMetadata`) | `StatResult { size, lastModified, etag? }` |
| `resolver.open("drive+mesh://…/clip.mov", signal)` → whole-file fetch | same; `open(..., { offset, length, signal })` fetches only in-range chunks |
| `DRIVE_URI.WRITE_UNSUPPORTED` raised for `set`/`delete` | only raised for `DriveStorageLikeAdapter.clear()` |
Consumer search: `grep -rln "DRIVE_URI.WRITE_UNSUPPORTED" packages/ | grep -v "/dist/\|/coverage/\|/drive-uri/"`. Branch new code on `err.code === "DRIVE_URI.TEMPLATE_READ_ONLY"` for the immutable template surface; on `DRIVE_URI.RANGE_INVALID` / `RANGE_OUT_OF_BOUNDS` for range errors; on `DRIVE_URI.MESH_PROPAGATION_TIMEOUT` for opt-in advertise failures.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/drive-uri` to enumerate. The package's downstream surface is currently empty (`drive-uri` is consumed only via integration tests at the time of writing); the broader `...@suaveplan/drive` dependents drive verification:
- `@suaveplan/drive-uri` — [x] tests run / [x] docs reviewed / [ ] no action needed
- `@suaveplan/drive-plugin-chunks` — [x] tests run / [x] docs reviewed / [ ] no action needed (middleware honours `ctx.range`; `#reassemble` extended)
- `@suaveplan/drive-mesh` — [x] tests run / [x] docs reviewed / [ ] no action needed (pull path constructs `ChunkRequest.specific` / `bitset` from range; `advertise` consumed via structural hook)
- `@suaveplan/drive-mesh-protocol` — [ ] tests run / [x] docs reviewed / [x] no action needed (no protocol wire change; existing union shapes used)
- `@suaveplan/drive-transport-https` — [ ] tests run / [x] docs reviewed / [x] no action needed (transport carries envelopes opaquely)
- `@suaveplan/drive-transport-webrtc` — [ ] tests run / [x] docs reviewed / [x] no action needed (same)
- `@suaveplan/drive-template` — [ ] tests run / [x] docs reviewed / [x] no action needed (hydration unchanged)
- `@suaveplan/drive-sharing` — [ ] tests run / [x] docs reviewed / [x] no action needed (calls `Drive.read`)
- `@suaveplan/storage-uri` — [x] tests run / [x] docs reviewed / [ ] no action needed (`writeViaPlugin` already passes through `SuaveplanError` subclasses; `openViaPlugin` already forwards `createReadStream`)
- `@suaveplan/drive` — [x] tests run / [x] docs reviewed / [ ] no action needed (range overload is purely additive)
### Affected upstreams (none breaking)
| Package | Action | Breaking? |
|-----------------------------------|-----------------------------------------------------------------------------------------|-----------|
| `@suaveplan/drive` | ADD `RangeReadOptions`, `createReadStream` overload, `RANGE_*` codes, `nativeRange` cap | no |
| `@suaveplan/storage-uri` | None — `StreamingStorageExtensions` already specifies `createWriteStream` + `getMetadata`| no |
| `@suaveplan/drive-mesh` | None — `advertise`/`unadvertise` consumed via structural hook only | no |
| `@suaveplan/drive-template` | None — only `DriveTemplate` type consumed | no |
| `@suaveplan/drive-plugin-chunks` | Honour `ctx.range` (delivered as a sibling change inside the same wave) | no |
| `@suaveplan/types` | None — `DriveLike` hoist tracked separately | no |
### Ordering
Unblocked. All packages exist on genesis today; the change is additive at every boundary.

View File

@@ -0,0 +1,381 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: drive-uri (drive-uri)
**Package**: `@suaveplan/drive-uri`
**Status**: PROPOSED
> Every requirement carries a `**Verified by:**` line at authoring time naming planned tests.
## ADDED Requirements
### Requirement: `AbortSignal` SHALL propagate through every buffered op on every scheme
`DriveStorageLikeAdapter` and `InstrumentedDriveAdapter` SHALL accept and forward `AbortSignal` on every buffered operation: `get`, `set`, `delete`, `exists`, `stat`, `list`, plus the streaming `createReadStream` and `createWriteStream`. A pre-aborted signal SHALL short-circuit any op to `DriveUriError(DRIVE_URI.ABORTED, …, { scheme, path: key })` BEFORE any `Drive` call. The signal forwarded to `Drive.{read,write,delete,stat,list}` SHALL be the same reference the caller supplied.
#### Scenario: pre-aborted signal short-circuits get without invoking Drive.read
- **WHEN** `adapter.get("x", AbortSignal.abort())` is called against any of the three plugins
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.ABORTED"` AND the underlying `Drive.read` is NOT invoked
#### Scenario: live signal forwarded to Drive.write
- **WHEN** `adapter.set("x", bytes, liveSignal)` is called against the `drive://` plugin
- **THEN** `Drive.write` is invoked with `options.signal === liveSignal`
#### Scenario: signal threads through stat, list, exists, delete
- **WHEN** any of `adapter.stat`, `adapter.list`, `adapter.exists`, `adapter.delete` is invoked with a live signal
- **THEN** the corresponding `Drive.{stat,list,delete}` (or, for `exists`, the underlying probe) receives the same signal reference
**Verified by:** `src/adapter/drive-storage-like.test.ts::"propagates AbortSignal through every buffered op"` AND `src/adapter/drive-storage-like.test.ts::"short-circuits with DRIVE_URI.ABORTED on pre-aborted signal"` AND `src/plugins/drive/drive.streams.test.ts::"forwards live AbortSignal unchanged on every op"`
---
### Requirement: `drive://` SHALL support writes, removes, and write-streams via the resolver surface
The `drive` plugin's instrumented adapter SHALL delegate `set(key, value, signal?)` to `Drive.write(key, data, { signal })`, `delete(key, signal?)` to `Drive.delete(key, signal)`, and `createWriteStream(key, body, signal?)` to `Drive.createWriteStream(key, signal)` consuming `body` as a `ReadableStream<Uint8Array>`. Successful writes increment `drive_uri.writes_total{scheme:"drive", outcome:"success"}`; successful removes increment `drive_uri.deletes_total{scheme:"drive", outcome:"success"}`. The `AbortSignal` passed via `resolver.write` / `resolver.remove` SHALL be threaded through to the underlying `Drive` call.
#### Scenario: write round-trips via Drive.write
- **WHEN** `resolver.write("drive://primary/clip.mov", bytes)` is called against a `drive` plugin bound to a real local-storage `Drive`
- **THEN** the call resolves with `void` AND `resolver.read("drive://primary/clip.mov")` returns `bytes`
#### Scenario: remove delegates to Drive.delete
- **WHEN** `resolver.remove("drive://primary/clip.mov")` is called after a successful write
- **THEN** the call resolves with `true` AND a subsequent `resolver.read(...)` rejects with `STORAGE_URI.NOT_FOUND`
#### Scenario: write-stream pipes ReadableStream into Drive.createWriteStream
- **WHEN** `resolver.write("drive://primary/clip.mov", readableStreamOfChunks)` is called where the body is a `ReadableStream`
- **THEN** `Drive.createWriteStream` is invoked exactly once AND the assembled bytes equal the concatenation of the streamed chunks
**Verified by:** `src/plugins/drive/drive.test.ts::"writes succeed and round-trip via Drive.write"` AND `src/plugins/drive/drive.test.ts::"removes delegate to Drive.delete and return its boolean"` AND `src/plugins/drive/drive.streams.test.ts::"createWriteStream pipes ReadableStream into Drive.createWriteStream"` AND `tests/integration/drive-roundtrip-write.test.ts::"resolver.write then resolver.read round-trips on a real Drive"`
---
### Requirement: `drive+mesh://` SHALL support writes/removes locally with opt-in advertise via `?advertise=true`
The `drive+mesh` plugin's instrumented adapter SHALL delegate `set`/`delete`/`createWriteStream` to the underlying `Drive` exactly as the `drive` scheme does. By default it SHALL NOT call any mesh advertise API; on every successful default-mode write the plugin SHALL emit a span event `drive_uri.mesh_write_local_only` carrying `{ scheme: "drive+mesh", path }`. When the URI carries `?advertise=true` AND `options.advertise: MeshAdvertiseHook` is wired, the plugin SHALL invoke `options.advertise.advertise(fileId, bitset, totalChunks, signal)` AFTER the local write resolves; hook rejection SHALL be re-wrapped as `DriveUriError(DRIVE_URI.MESH_PROPAGATION_TIMEOUT, …, { cause })`. When `?advertise=true` is present but `options.advertise` is absent, `parse` SHALL throw `DRIVE_URI.CAPABILITY_MISSING` with `reason: "advertise"` synchronously.
#### Scenario: default write persists locally without advertise
- **WHEN** `resolver.write("drive+mesh://swarmA/clip.mov", bytes)` is called against a mesh-configured `Drive` with a spy `MeshAdvertiseHook`
- **THEN** the call resolves with `void`, the bytes are persisted, and the spy records zero `advertise(...)` calls AND the span carries a `drive_uri.mesh_write_local_only` event
#### Scenario: `?advertise=true` invokes the hook exactly once
- **WHEN** `resolver.write("drive+mesh://swarmA/clip.mov?advertise=true", bytes)` is called
- **THEN** the spy records exactly one `advertise(fileId, bitset, totalChunks, signal)` call AFTER `Drive.write` resolves AND a `drive_uri:mesh_advertised` event fires on the `Drive.events` bus
#### Scenario: advertise rejection becomes MESH_PROPAGATION_TIMEOUT
- **WHEN** the hook's `advertise` rejects with `new Error("transport down")`
- **THEN** `resolver.write(...)` rejects with `DriveUriError` whose `code === "DRIVE_URI.MESH_PROPAGATION_TIMEOUT"` AND `cause.message === "transport down"`
#### Scenario: `?advertise=true` without a hook fails fast at construction
- **WHEN** the plugin is constructed without `options.advertise` AND `resolver.write("drive+mesh://swarmA/x?advertise=true", bytes)` is called
- **THEN** the `parse` step throws `DriveUriError` whose `code === "DRIVE_URI.CAPABILITY_MISSING"` AND `reason === "advertise"`
**Verified by:** `src/plugins/drive-mesh/drive-mesh.test.ts::"default write persists locally without advertise"` AND `src/plugins/drive-mesh/drive-mesh.test.ts::"?advertise=true invokes MeshAdvertiseHook exactly once"` AND `src/plugins/drive-mesh/drive-mesh.test.ts::"advertise rejection becomes MESH_PROPAGATION_TIMEOUT"` AND `src/plugins/drive-mesh/drive-mesh.test.ts::"?advertise=true without hook throws CAPABILITY_MISSING"` AND `tests/integration/drive-mesh-roundtrip-write.test.ts::"opt-in advertise produces a single outbound advertisement on a real Drive"`
---
### Requirement: `drive+template://` writes, removes, and write-streams SHALL be permanently rejected with `DRIVE_URI.TEMPLATE_READ_ONLY`
The `drive+template` plugin's instrumented adapter SHALL throw `DriveUriError(DRIVE_URI.TEMPLATE_READ_ONLY, …, { scheme: "drive+template", path: key, templateId })` on every `set`, `delete`, and `createWriteStream` call, before any delegation. Rejections SHALL increment `drive_uri.writes_total{scheme:"drive+template", outcome:"error"}` and `drive_uri.deletes_total{scheme:"drive+template", outcome:"error"}`. The error message SHALL state that template-backed drives require authority re-signing the URI resolver never holds.
#### Scenario: write rejects with TEMPLATE_READ_ONLY
- **WHEN** `resolver.write("drive+template://tplA/clip.mov", bytes)` is called
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.TEMPLATE_READ_ONLY"`, `scheme === "drive+template"`, `path === "clip.mov"`, `templateId === "tplA"`
#### Scenario: remove rejects with TEMPLATE_READ_ONLY
- **WHEN** `resolver.remove("drive+template://tplA/clip.mov")` is called
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.TEMPLATE_READ_ONLY"`
#### Scenario: createWriteStream rejects with TEMPLATE_READ_ONLY
- **WHEN** the adapter's `createWriteStream("clip.mov", body)` is called
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.TEMPLATE_READ_ONLY"` AND `Drive.createWriteStream` is NOT invoked
**Verified by:** `src/plugins/drive-template/drive-template.test.ts::"writes reject with DRIVE_URI.TEMPLATE_READ_ONLY"` AND `src/plugins/drive-template/drive-template.test.ts::"removes reject with DRIVE_URI.TEMPLATE_READ_ONLY"` AND `src/plugins/drive-template/drive-template.test.ts::"createWriteStream rejects with DRIVE_URI.TEMPLATE_READ_ONLY"` AND `tests/integration/template-write-rejection.test.ts::"resolver.write and resolver.remove both reject on template-backed drive"`
---
### Requirement: Range reads `{ offset, length }` SHALL be supported on every `drive*://` scheme
The adapter's `createReadStream(key, options?: { offset?: number; length?: number; signal?: AbortSignal })` SHALL forward the range bag to `Drive.createReadStream(key, options)`. Every plugin's adapter SHALL advertise `capabilities.nativeRange === true` so `StorageUriResolver.open(uri, { offset, length, signal })` issues exactly one range-aware `createReadStream` call (no buffer-and-slice fallback). Over `drive+mesh://`, the chunks-plugin middleware downstream of `Drive` SHALL use the range to request only intersecting chunks via `ChunkRequest.specific` (when span ≤ 64) or `bitset` (span > 64). On invalid range, `Drive.createReadStream` throws `DRIVE.RANGE_INVALID`; the adapter SHALL re-throw the original error unchanged (it is a `SuaveplanError` and reaches the caller intact under the resolver pass-through rule).
#### Scenario: range slice is emitted exactly
- **WHEN** `resolver.open("drive://primary/file.bin", { offset: 1024, length: 4096, signal })` is called on a 200 KiB file
- **THEN** the returned stream emits exactly bytes `[1024, 5120)`
#### Scenario: zero-length range yields an immediately-closed stream
- **WHEN** `resolver.open("drive://primary/file.bin", { offset: 12345, length: 0 })` is called
- **THEN** the returned stream closes with zero enqueued bytes
#### Scenario: cross-boundary range still emits exactly
- **WHEN** the chunks-plugin chunkSize is 65536 and `resolver.open("drive+mesh://swarmA/clip.mov", { offset: 60_000, length: 10_000 })` is called
- **THEN** the returned stream emits exactly bytes `[60_000, 70_000)` AND `drive_uri.stream_chunks_fetched` records `2`
#### Scenario: invalid range surfaces DRIVE.RANGE_INVALID via pass-through
- **WHEN** `resolver.open("drive://primary/file.bin", { offset: -1 })` is called
- **THEN** the call rejects with a `SuaveplanError` whose `code === "DRIVE.RANGE_INVALID"`
#### Scenario: out-of-bounds range surfaces DRIVE.RANGE_OUT_OF_BOUNDS
- **WHEN** the file is 1024 bytes and `resolver.open("drive://primary/file.bin", { offset: 2048 })` is called
- **THEN** the call rejects with a `SuaveplanError` whose `code === "DRIVE.RANGE_OUT_OF_BOUNDS"`
**Verified by:** `src/adapter/drive-storage-like.test.ts::"createReadStream forwards { offset, length, signal } to Drive"` AND `src/plugins/drive/drive.streams.test.ts::"range slice is emitted exactly through plugin"` AND `src/plugins/drive/drive.streams.test.ts::"zero-length range closes the stream immediately"` AND `src/otel/otel.test.ts::"records stream_chunks_fetched with the in-range chunk count"` AND `tests/integration/drive-range-roundtrip.test.ts::"range read returns exactly [60_000, 70_000) on a real 200 KiB Drive file"`
---
### Requirement: `stat` SHALL return `StatResult` populated from `Drive.stat`
Every plugin's adapter SHALL implement `getMetadata(key, signal?): Promise<StatResult | undefined>` (per `StreamingStorageExtensions`) so `StorageUriResolver.stat(uri)` returns `StatResult { size, lastModified?, etag?, contentType? }`. The implementation SHALL read `Drive.stat(key, signal)` and map `DriveStats.size` → `StatResult.size`, `DriveStats.modifiedAt` (epoch ms) → `StatResult.lastModified` (`new Date(modifiedAt)`), and `DriveStats.checksum` → `StatResult.etag` when present. When `Drive.stat` returns `null`, the adapter SHALL resolve `undefined`. When `Drive.stat` rejects with a non-not-found cause, the adapter SHALL throw `DriveUriError(DRIVE_URI.STAT_UNAVAILABLE, …, { cause })`.
#### Scenario: stat returns StatResult for an existing key
- **WHEN** `resolver.stat("drive://primary/file.bin")` is called on a key the Drive reports as `{ size: 1024, modifiedAt: 1700000000000, checksum: "abc" }`
- **THEN** the call resolves with `{ size: 1024, lastModified: new Date(1700000000000), etag: "abc" }`
#### Scenario: stat returns undefined for a missing key
- **WHEN** `resolver.stat("drive://primary/missing")` is called and `Drive.stat` returns `null`
- **THEN** the call resolves with `undefined`
#### Scenario: stat failure becomes DRIVE_URI.STAT_UNAVAILABLE
- **WHEN** `Drive.stat` rejects with a non-not-found `SuaveplanError`
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.STAT_UNAVAILABLE"` AND `cause` is the original error
**Verified by:** `src/adapter/drive-storage-like.test.ts::"getMetadata maps Drive.stat to StatResult"` AND `src/adapter/drive-storage-like.test.ts::"getMetadata returns undefined when Drive.stat is null"` AND `src/adapter/drive-storage-like.test.ts::"getMetadata wraps non-not-found Drive.stat failures as STAT_UNAVAILABLE"` AND `tests/integration/drive-stat-list-exists.test.ts::"resolver.stat returns populated StatResult on a real Drive"`
---
### Requirement: `exists` SHALL use a cheap presence path
Every plugin's adapter `exists(key, signal?)` SHALL first try `Drive.getPresence(key)`; when it returns `chunksPresent > 0`, the result is `true` without a `Drive.stat` round-trip. When `getPresence` reports `chunksPresent === 0 && chunksTotal === 0` (the file is not in the location index), the adapter SHALL fall back to `Drive.stat(key, signal) !== null`. The signal SHALL be honoured throughout.
#### Scenario: presence > 0 short-circuits to true
- **WHEN** `Drive.getPresence(key)` reports `{ chunksPresent: 3, chunksTotal: 5 }` and `resolver.exists("drive://primary/key")` is called
- **THEN** the call resolves `true` AND `Drive.stat` is NOT invoked
#### Scenario: presence empty falls back to stat
- **WHEN** `Drive.getPresence(key)` reports `{ chunksPresent: 0, chunksTotal: 0 }` and `Drive.stat(key)` returns a non-null result
- **THEN** the call resolves `true` AND `Drive.stat` is invoked exactly once
#### Scenario: stat null returns false
- **WHEN** presence is empty AND `Drive.stat(key)` returns `null`
- **THEN** the call resolves `false`
**Verified by:** `src/adapter/drive-storage-like.test.ts::"exists short-circuits true on presence > 0"` AND `src/adapter/drive-storage-like.test.ts::"exists falls back to stat when presence is empty"` AND `src/adapter/drive-storage-like.test.ts::"exists returns false when stat is null"`
---
### Requirement: `list` SHALL enumerate via `Drive.list` and re-serialise via `UriPlugin.build`
Every plugin's adapter `list({ prefix?, limit?, cursor?, signal? })` SHALL forward to `Drive.list(prefix, { signal, limit, cursor })` and return `{ keys, total, hasMore, continuationToken? }`. The plugin's `UriPlugin.build(key, meta)` SHALL re-serialise every returned key back into a fully-qualified URI that round-trips through `parse`. When the bound `Drive`'s default storage lacks `list`, the adapter SHALL throw `DriveUriError(DRIVE_URI.LIST_UNSUPPORTED, …, { scheme })`.
#### Scenario: list returns prefix-matching keys
- **WHEN** `resolver.list("drive://primary/folder/")` is called against a Drive with files `folder/a`, `folder/b`, `other/c`
- **THEN** the call resolves with `{ keys: ["folder/a", "folder/b"], hasMore: false, … }` (or equivalent re-serialised URIs per the resolver's contract)
#### Scenario: each listed key re-serialises through build
- **WHEN** the resolver re-serialises each key via `plugin.build(key, { driveId: "primary" })`
- **THEN** each result is a `drive://primary/<key>` URI that round-trips through `plugin.parse`
#### Scenario: list propagates AbortSignal
- **WHEN** `resolver.list("drive://primary/", { limit: 100 }, signal)` is called
- **THEN** `Drive.list` receives `options.signal === signal`
#### Scenario: list on a storage without list throws LIST_UNSUPPORTED
- **WHEN** the underlying storage lacks `list` AND `resolver.list("drive://primary/")` is called
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.LIST_UNSUPPORTED"`
**Verified by:** `src/adapter/drive-storage-like.test.ts::"list forwards prefix, limit, cursor, signal to Drive.list"` AND `src/adapter/drive-storage-like.test.ts::"list throws LIST_UNSUPPORTED when storage lacks list"` AND `tests/integration/drive-stat-list-exists.test.ts::"resolver.list returns URIs that round-trip through resolver.read"`
---
### Requirement: Per-scheme query string options SHALL be parsed with a strict allow-list
Every plugin's `parse` SHALL parse `parts.query` via `parseQuery(query, scheme)`. The recognised key set per scheme:
| Scheme | Allowed keys |
|---------------------|----------------------------------------------------|
| `drive://` | `signal-timeout-ms` |
| `drive+mesh://` | `signal-timeout-ms`, `advertise` |
| `drive+template://` | `signal-timeout-ms`, `nonce` |
Unknown keys SHALL throw `DriveUriError(DRIVE_URI.INVALID_QUERY, …, { scheme, key, value })`. Malformed values (`?advertise=maybe`, `?signal-timeout-ms=-1`, `?signal-timeout-ms=NaN`) SHALL throw `DRIVE_URI.INVALID_QUERY` with the offending value. `?signal-timeout-ms=<int>` SHALL be honoured via `AbortSignal.timeout(ms)` linked to the caller's signal; elapse SHALL raise `DRIVE_URI.ABORTED` with `reason: "signal-timeout-ms"`. `?nonce=<hex>` on `drive+template://` SHALL be forwarded to `templateProvider.fetchTemplate(templateId, { nonce, signal })`.
#### Scenario: unknown query key throws INVALID_QUERY
- **WHEN** `resolver.read("drive://primary/x?foo=bar")` is called
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.INVALID_QUERY"` AND `key === "foo"` AND `value === "bar"`
#### Scenario: malformed advertise value throws INVALID_QUERY
- **WHEN** `resolver.read("drive+mesh://swarmA/x?advertise=maybe")` is called
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.INVALID_QUERY"` AND `key === "advertise"`
#### Scenario: signal-timeout-ms enforces the upper bound
- **WHEN** `resolver.read("drive://primary/slow?signal-timeout-ms=10")` is called against a `Drive.read` that never resolves
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.ABORTED"` AND `reason === "signal-timeout-ms"` after ~10 ms
#### Scenario: nonce forwarded to templateProvider
- **WHEN** `resolver.read("drive+template://tplA/x?nonce=deadbeef")` is called
- **THEN** `templateProvider.fetchTemplate` is invoked exactly once with `templateId === "tplA"` and `options.nonce === "deadbeef"`
**Verified by:** `src/query/parse-query.test.ts::"rejects unknown query keys per scheme"` AND `src/query/parse-query.test.ts::"rejects malformed advertise and signal-timeout-ms values"` AND `src/plugins/drive/drive.test.ts::"signal-timeout-ms enforces an upper bound and surfaces ABORTED"` AND `src/plugins/drive-template/drive-template.test.ts::"forwards nonce query param to templateProvider.fetchTemplate"`
---
### Requirement: `DRIVE_URI_CODES` SHALL contain 14 distinct, frozen, reachable codes
The `DRIVE_URI_CODES` registry produced by `defineCodes("DRIVE_URI", [...] as const)` SHALL contain exactly: `INVALID_URI`, `DRIVE_ID_MISMATCH`, `TEMPLATE_NOT_AVAILABLE`, `TEMPLATE_INVALID`, `TEMPLATE_READ_ONLY`, `WRITE_UNSUPPORTED`, `CAPABILITY_MISSING`, `ABORTED`, `RANGE_INVALID`, `RANGE_OUT_OF_BOUNDS`, `MESH_PROPAGATION_TIMEOUT`, `STAT_UNAVAILABLE`, `LIST_UNSUPPORTED`, `INVALID_QUERY`. `Object.isFrozen(DRIVE_URI_CODES)` MUST remain `true`. Every code value MUST start with `DRIVE_URI.`. Every code MUST be reachable from at least one real call site in `src/` and MUST be exercised by at least one test (per CLAUDE.md §2c).
#### Scenario: registry contains the 14 expected codes
- **WHEN** `Object.keys(DRIVE_URI_CODES).length` is asserted
- **THEN** it equals `14` AND every value starts with `"DRIVE_URI."`
#### Scenario: every code is reachable from a real call site
- **WHEN** the registry-reachability test scans `src/` for each code's qualified string AND walks every test for an `expect(...).code === "DRIVE_URI.<X>"` assertion
- **THEN** every code maps to ≥1 source occurrence AND ≥1 test assertion
#### Scenario: WRITE_UNSUPPORTED is raised only by clear()
- **WHEN** the source grep restricts to `WRITE_UNSUPPORTED` usages
- **THEN** the sole non-registry occurrence is in `src/adapter/drive-storage-like.ts` inside `clear()`
**Verified by:** `src/error-codes/error-codes.test.ts::"registry contains exactly 14 codes, all DRIVE_URI.-prefixed and frozen"` AND `src/error-codes/error-codes.test.ts::"every DRIVE_URI.* code is reachable from a real call site"` AND `src/error-codes/error-codes.test.ts::"WRITE_UNSUPPORTED is raised only by DriveStorageLikeAdapter.clear"`
---
### Requirement: EventBus emission SHALL fire on every write and delete (success and failure)
When the wrapped `Drive.events: EventBusLike | undefined` is present, `InstrumentedDriveAdapter` SHALL emit `drive_uri:write` on every `set` outcome and `drive_uri:delete` on every `delete` outcome. Payload: `{ scheme, key, uri (redacted), outcome: "success" | "error", code?, durationMs, fileId?, range? }`. On successful `drive+mesh://` opt-in advertise, the adapter SHALL emit `drive_uri:mesh_advertised` with `{ scheme: "drive+mesh", fileId, totalChunks, durationMs }`. Absence of `Drive.events` is a silent no-op.
#### Scenario: write emits drive_uri:write on success
- **WHEN** `resolver.write("drive://primary/x", bytes)` succeeds against a `Drive` with a real `EventBusLike`
- **THEN** the bus receives one `drive_uri:write` event with `outcome === "success"`, `scheme === "drive"`, `key === "x"`, `uri === "drive://primary/x"` (no query), `durationMs` ≥ 0
#### Scenario: write emits drive_uri:write on failure
- **WHEN** `resolver.write("drive+template://tplA/x", bytes)` rejects
- **THEN** the bus receives one `drive_uri:write` event with `outcome === "error"` AND `code === "DRIVE_URI.TEMPLATE_READ_ONLY"`
#### Scenario: delete emits drive_uri:delete
- **WHEN** `resolver.remove("drive://primary/x")` is called against a `Drive` with a bus
- **THEN** the bus receives one `drive_uri:delete` event with `outcome === "success"`
#### Scenario: missing events bus is a no-op
- **WHEN** the wrapped `Drive` has `events === undefined` and any op runs
- **THEN** no event is emitted and the op succeeds normally
**Verified by:** `src/events/events.test.ts::"emits drive_uri:write on success"` AND `src/events/events.test.ts::"emits drive_uri:write on failure with code"` AND `src/events/events.test.ts::"emits drive_uri:delete on success"` AND `src/events/events.test.ts::"is a no-op when Drive.events is undefined"`
---
### Requirement: OpenTelemetry surface SHALL cover every op with backend identification, range, and redaction
Every plugin op SHALL emit a span on the `@suaveplan/drive-uri` tracer and record `drive_uri.op_duration_ms{op, scheme, outcome}`. Errors SHALL increment `drive_uri.errors_total{scheme, code}`. The `drive.uri` span attribute SHALL exclude any URI query string AND any URI fragment. On `createReadStream` calls with a range, the span SHALL carry `drive.uri.range = "{offset}-{end}"` AND `drive_uri.stream_chunks_fetched{scheme}` SHALL record the chunk count actually pulled. On `drive+mesh://` writes, the span SHALL carry `drive.uri.has_advertise = "true" | "false"` reflecting the `?advertise` opt-in. The `drive+template` plugin SHALL increment `drive_uri.template_hydrations_total{outcome}` exactly once per first-use template.
#### Scenario: range attribute set iff range is supplied
- **WHEN** `resolver.open("drive://primary/x", { offset: 100, length: 50 })` succeeds AND a sibling `resolver.open("drive://primary/x")` call without range succeeds
- **THEN** the first span carries `drive.uri.range === "100-150"` AND the second span has no `drive.uri.range` attribute
#### Scenario: stream_chunks_fetched records pulled count
- **WHEN** a range read pulls 2 chunks
- **THEN** `drive_uri.stream_chunks_fetched{scheme}` records `2`
#### Scenario: query and fragment stripped from span attribute
- **WHEN** `resolver.read("drive+mesh://swarmA/path?advertise=true#frag")` is called
- **THEN** the emitted span's `drive.uri` attribute equals `"drive+mesh://swarmA/path"` (no `?`, no `#`)
#### Scenario: has_advertise reflects opt-in
- **WHEN** `resolver.write("drive+mesh://swarmA/x?advertise=true", bytes)` succeeds
- **THEN** the span carries `drive.uri.has_advertise === "true"`
**Verified by:** `src/otel/otel.test.ts::"sets drive.uri.range only when range is supplied"` AND `src/otel/otel.test.ts::"records drive_uri.stream_chunks_fetched per range read"` AND `src/otel/otel.test.ts::"strips query and fragment from drive.uri span attribute"` AND `src/otel/otel.test.ts::"sets drive.uri.has_advertise on mesh writes"`
---
### Requirement: `runUriPluginConformance` SHALL pass the full op matrix for all three plugins
The conformance harness re-exported from `@suaveplan/storage-uri` SHALL be re-run against each of the three plugins covering `read`, `write`, `delete`, `stat`, `list`, `exists`, range read, and parse/build round-trips. Expected outcomes:
- `drive://` and `drive+mesh://` complete the full cycle.
- `drive+template://` passes read/stat/list/exists/range; `set`/`delete`/`createWriteStream` reject with `DRIVE_URI.TEMPLATE_READ_ONLY` (not `WRITE_UNSUPPORTED`).
- The mesh-advertise spy hook records zero calls when `?advertise` is omitted, exactly one call when `?advertise=true` is supplied.
#### Scenario: drive plugin full matrix passes
- **WHEN** `runUriPluginConformance(driveUriPlugin({ drive, driveId: "primary" }), driveFixtures)` is invoked with the full matrix
- **THEN** every assertion (read/write/delete/stat/list/exists/range/round-trip) passes without throwing
#### Scenario: drive+mesh plugin full matrix passes with advertise opt-in behaviour
- **WHEN** the harness exercises the mesh plugin with both `?advertise`-absent and `?advertise=true` writes
- **THEN** the absent-case advertise count is `0`, the opt-in case advertise count is `1`, and all read/write/delete/stat/list/exists assertions pass
#### Scenario: drive+template plugin rejects writes with TEMPLATE_READ_ONLY
- **WHEN** the harness exercises the template plugin's write/delete/writeStream cases
- **THEN** every such call rejects with `DRIVE_URI.TEMPLATE_READ_ONLY` (never `WRITE_UNSUPPORTED`)
**Verified by:** `src/conformance/conformance.test.ts::"drive plugin satisfies the full op matrix"` AND `src/conformance/conformance.test.ts::"drive+mesh plugin satisfies the full op matrix with advertise opt-in"` AND `src/conformance/conformance.test.ts::"drive+template plugin rejects writes with TEMPLATE_READ_ONLY"`
---
## MODIFIED Requirements
### Requirement: `DRIVE_URI.WRITE_UNSUPPORTED` SHALL only be raised by `DriveStorageLikeAdapter.clear()`
Previously the three plugins rejected every `set` call with `DRIVE_URI.WRITE_UNSUPPORTED`. The code SHALL now be raised by exactly one call site: `DriveStorageLikeAdapter.clear()`, which intentionally refuses to wipe an entire Drive through the URI surface and points the caller at `Drive.delete(path)` (or `resolver.remove(uri)`). The code SHALL remain in `DRIVE_URI_CODES` so consumers that already assert on it for `clear()` continue to function. No other call site in `drive-uri` SHALL raise `WRITE_UNSUPPORTED`.
#### Scenario: clear() still rejects with WRITE_UNSUPPORTED
- **WHEN** any consumer invokes `adapter.clear()` on a `DriveStorageLikeAdapter`
- **THEN** the call rejects with `DriveUriError` whose `code === "DRIVE_URI.WRITE_UNSUPPORTED"`
#### Scenario: set on drive:// no longer raises WRITE_UNSUPPORTED
- **WHEN** `resolver.write("drive://primary/x", bytes)` is called against a real `Drive`
- **THEN** the call resolves with `void` and no `DRIVE_URI.WRITE_UNSUPPORTED` is thrown anywhere in the call chain
#### Scenario: set on drive+template:// raises TEMPLATE_READ_ONLY not WRITE_UNSUPPORTED
- **WHEN** `resolver.write("drive+template://tplA/x", bytes)` is called
- **THEN** the rejection's `code` is `DRIVE_URI.TEMPLATE_READ_ONLY`, never `DRIVE_URI.WRITE_UNSUPPORTED`
**Verified by:** `src/adapter/drive-storage-like.test.ts::"clear() rejects with DRIVE_URI.WRITE_UNSUPPORTED"` AND `src/plugins/drive/drive.test.ts::"writes succeed and round-trip via Drive.write"` AND `src/plugins/drive-template/drive-template.test.ts::"writes reject with DRIVE_URI.TEMPLATE_READ_ONLY"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,123 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: drive (drive)
**Package**: `@suaveplan/drive`
**Status**: PROPOSED
> Every requirement carries a `**Verified by:**` line at authoring time. The named tests are planned; they MUST exist and pass verbatim before this delta is folded into `openspec/specs/drive/spec.md`.
## ADDED Requirements
### Requirement: `Drive.createReadStream` SHALL accept a range-options bag overload
`Drive.createReadStream(path, signalOrOptions?)` SHALL be overloaded to accept either a bare `AbortSignal` (existing form, preserved) or a `RangeReadOptions` bag with optional `offset`, `length`, and `signal`. The implementation MUST runtime-discriminate via `instanceof AbortSignal`. When the options form is passed, the drive MUST forward `{ offset, length, signal }` through the plugin middleware chain via the middleware context's `range` field.
#### Scenario: bare signal returns the whole-file stream unchanged
- **WHEN** `drive.createReadStream("file.bin", abortSignal)` is called on a 200 KiB file
- **THEN** the returned stream emits the entire 200 KiB payload and respects `abortSignal` for cancellation
#### Scenario: options form emits exactly the requested slice
- **WHEN** `drive.createReadStream("file.bin", { offset: 1024, length: 4096 })` is called
- **THEN** the returned stream emits exactly bytes `[1024, 5120)` of the source
#### Scenario: zero-length range yields an immediately-closed stream
- **WHEN** `drive.createReadStream("file.bin", { offset: 12_345, length: 0 })` is called
- **THEN** the returned stream closes immediately with zero enqueued bytes
**Verified by:** `src/drive/drive.test.ts::"createReadStream(path, signal) preserves whole-file behaviour"` AND `src/drive/drive.test.ts::"createReadStream(path, { offset, length }) emits exactly the requested slice"` AND `src/drive/drive.test.ts::"createReadStream(path, { length: 0 }) returns an immediately closed stream"`
---
### Requirement: Invalid range options SHALL throw `DRIVE.RANGE_INVALID` synchronously
`Drive.createReadStream(path, options)` SHALL validate `options.offset` and `options.length` synchronously BEFORE constructing the `ReadableStream`. Either field, when present, MUST satisfy `Number.isInteger(x) && x >= 0 && Number.isFinite(x)`. On violation, the method MUST throw `DriveError(DRIVE.RANGE_INVALID, …)` with `context.field` naming the offending field.
#### Scenario: negative offset throws synchronously
- **WHEN** `drive.createReadStream("file.bin", { offset: -1 })` is called
- **THEN** the call throws `DriveError` with `code === "DRIVE.RANGE_INVALID"` AND `context.field === "offset"` AND no `ReadableStream` is created
#### Scenario: non-integer length throws synchronously
- **WHEN** `drive.createReadStream("file.bin", { length: 1.5 })` is called
- **THEN** the call throws `DriveError` with `code === "DRIVE.RANGE_INVALID"` AND `context.field === "length"`
#### Scenario: NaN or Infinity rejected
- **WHEN** `drive.createReadStream("file.bin", { offset: Number.NaN })` OR `drive.createReadStream("file.bin", { length: Number.POSITIVE_INFINITY })` is called
- **THEN** each call throws `DriveError(DRIVE.RANGE_INVALID, …)` synchronously
**Verified by:** `src/drive/drive.test.ts::"createReadStream throws RANGE_INVALID for negative offset"` AND `src/drive/drive.test.ts::"createReadStream throws RANGE_INVALID for non-integer length"` AND `src/drive/drive.test.ts::"createReadStream throws RANGE_INVALID for NaN or Infinity"`
---
### Requirement: Out-of-bounds range SHALL surface `DRIVE.RANGE_OUT_OF_BOUNDS`
When the underlying source exposes a known size at stream-start (chunks-plugin manifest, storage `stat`, or a `nativeRange`-capable storage that reports `size`), `Drive.createReadStream` MUST raise `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` synchronously for `offset > size` or `offset + length > size`. When size is unknown at start-time, the stream MUST accept the request, emit available bytes, and surface `DRIVE.RANGE_OUT_OF_BOUNDS` via `controller.error` on close if the byte count emitted is less than the requested `length`. The error MUST carry the originating `CHUNKS.RANGE_OUT_OF_BOUNDS` (when applicable) as `cause`.
#### Scenario: offset past EOF rejected synchronously when size is known
- **WHEN** the file is 1024 bytes and `drive.createReadStream("file.bin", { offset: 2048 })` is called via a chunks-plugin-mediated path
- **THEN** the call throws `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` synchronously with `cause` carrying `CHUNKS.RANGE_OUT_OF_BOUNDS`
#### Scenario: offset+length past EOF rejected synchronously when size is known
- **WHEN** the file is 1024 bytes and `drive.createReadStream("file.bin", { offset: 900, length: 500 })` is called via the chunks plugin
- **THEN** the call throws `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` synchronously
#### Scenario: short-fall on stream end when size was unknown at start
- **WHEN** the storage cannot report size and `drive.createReadStream("file.bin", { offset: 0, length: 10_000 })` is called and the source closes after 4000 bytes
- **THEN** the stream errors with `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` AND `context.emittedBytes === 4000`
**Verified by:** `src/drive/drive.test.ts::"createReadStream throws RANGE_OUT_OF_BOUNDS when offset exceeds known size"` AND `src/drive/drive.test.ts::"createReadStream throws RANGE_OUT_OF_BOUNDS when offset+length exceeds known size"` AND `src/file-operations/streams.range.test.ts::"stream errors with RANGE_OUT_OF_BOUNDS on short-fall when size is unknown"`
---
### Requirement: `nativeRange` storage capability SHALL be honoured
`DriveStorage` SHALL accept an optional `capabilities.nativeRange?: boolean` flag. When `true`, `Drive.createReadStream(path, { offset, length, signal })` MUST forward `{ offset, length }` to `storage.createReadStream` directly. When `false` (or absent), the drive MUST wrap the storage stream in a slicing transform that drops bytes before `offset` and stops emitting after `length` bytes. Both paths MUST produce identical observable output.
#### Scenario: storage with nativeRange receives offset/length
- **WHEN** `storage.capabilities.nativeRange === true` and `drive.createReadStream("file.bin", { offset: 1024, length: 4096 })` is invoked
- **THEN** `storage.createReadStream` is called exactly once with `{ offset: 1024, length: 4096, signal }` and no slicing transform is interposed
#### Scenario: storage without nativeRange is wrapped in a slicing transform
- **WHEN** `storage.capabilities?.nativeRange` is `undefined` or `false` and the same call is made
- **THEN** `storage.createReadStream` is called with `{ signal }` only AND the returned stream is wrapped so the consumer observes exactly bytes `[1024, 5120)`
**Verified by:** `src/file-operations/streams.range.test.ts::"nativeRange storage receives offset and length directly"` AND `src/file-operations/streams.range.test.ts::"non-nativeRange storage is wrapped in a slicing transform"`
---
### Requirement: Range cancellation SHALL propagate via `AbortSignal`
`Drive.createReadStream(path, options)` MUST propagate `options.signal` (or the bare signal in the back-compat form) through the chunks-plugin / mesh / storage pull chain. On abort, in-flight chunk fetches MUST be cancelled, the stream MUST close with `controller.error(AbortError)`, and (over `drive+mesh://`) the mesh transport MUST emit a `cd:error` envelope with `code: "ABORTED"`.
#### Scenario: mid-stream abort cancels chunk fetches
- **WHEN** `drive.createReadStream("file.bin", { offset: 0, length: 1_048_576, signal })` is called and `signal.abort()` is invoked after the first chunk emits
- **THEN** the stream closes with an `AbortError`, no further chunks are fetched, and the chunks-plugin loop exits at the next `throwIfAborted` checkpoint
**Verified by:** `src/file-operations/streams.range.test.ts::"abort mid-range stream cancels in-flight chunk fetches"`
---
### Requirement: `DRIVE_CODES` SHALL include `RANGE_INVALID` and `RANGE_OUT_OF_BOUNDS`
`DRIVE_CODES` SHALL include `"RANGE_INVALID"` and `"RANGE_OUT_OF_BOUNDS"` declared via the existing `defineCodes("DRIVE", [...] as const)` call. `Object.isFrozen(DRIVE_CODES)` MUST remain `true`. Every code value MUST start with `DRIVE.`.
#### Scenario: registry exposes the new codes
- **WHEN** the test imports `DRIVE_CODES` from `@suaveplan/drive`
- **THEN** `DRIVE_CODES.RANGE_INVALID === "DRIVE.RANGE_INVALID"` AND `DRIVE_CODES.RANGE_OUT_OF_BOUNDS === "DRIVE.RANGE_OUT_OF_BOUNDS"` AND `Object.isFrozen(DRIVE_CODES) === true`
**Verified by:** `src/error-codes.spec-conformance.test.ts::"DRIVE_CODES exposes RANGE_INVALID and RANGE_OUT_OF_BOUNDS"` AND `src/error-codes.spec-conformance.test.ts::"Object.isFrozen(DRIVE_CODES) is true"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,135 @@
# Tasks — `complete-drive-uri`
> Every checkbox MUST reflect work actually done in this change. Walk every `### Requirement` and `#### Scenario` in `specs/drive-uri/spec.md` AND `specs/drive/spec.md` before declaring done (§12a).
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [ ] 1.1 Read `openspec/AGENTS.md` §0c, §2b, §2c, §6a, §7, §11, §13, §13a, §14
- [ ] 1.2 Read `openspec/archive/2026-05-16-add-drive-uri/proposal.md`, `_superseded-narrow-deferrals-2026-05-17/{add-drive-uri-writes,add-drive-range-reads}/{proposal,design}.md`
- [ ] 1.3 Read `openspec/specs/drive-uri/spec.md` and `openspec/specs/drive/spec.md`
- [ ] 1.4 Confirm `Drive.{read,write,delete,stat,list,createReadStream,createWriteStream,hydrateFrom,getKeyStatus,getPresence}` surface; `DriveStorage.capabilities` flag pattern; `StreamingStorageExtensions` shape; `MeshDrive.{advertise,unadvertise}` shape; `ChunkRequest` union; `parseUri` query/fragment shape
- [ ] 1.5 Draft `proposal.md` + `design.md` + `tasks.md` + spec deltas
- [ ] 1.6 Build the numbered Requirement / Scenario implementation checklist (per §0c step 6)
- [ ] 1.7 `openspec validate complete-drive-uri --strict` exits 0
## 2. `@suaveplan/drive` — range overload + new codes
- [ ] 2.1 `src/types/drive.types.ts` — add `RangeReadOptions { offset?, length?, signal? }`; extend `DriveStorage` with `capabilities.nativeRange?: boolean` and an extended `createReadStream?(path, options?: { offset?, length?, signal? })` signature
- [ ] 2.2 `src/error-codes.ts` — add `RANGE_INVALID`, `RANGE_OUT_OF_BOUNDS` to `DRIVE_CODES` (registry grows 11 → 13)
- [ ] 2.3 `src/error-codes.spec-conformance.test.ts` — extend frozen-registry and every-code-starts-with-`DRIVE.` checks
- [ ] 2.4 `src/file-operations/streams.range.ts` — pure slicing transform `sliceStream(stream, offset, length)` honouring `signal`
- [ ] 2.5 `src/file-operations/streams.range.md` — sibling doc
- [ ] 2.6 `src/file-operations/streams.ts` — overload `createReadStream(storage, path, signal?)` AND `createReadStream(storage, path, options: RangeReadOptions)`; runtime `instanceof AbortSignal` discriminator; synchronous validation → `DRIVE.RANGE_INVALID`; `nativeRange` → forward `{ offset, length }`; otherwise wrap in `sliceStream`
- [ ] 2.7 `src/file-operations/streams.md` — update docs
- [ ] 2.8 `src/file-operations/streams.range.test.ts` — boundary table: fully inside one chunk, spans boundary, aligned multi-chunk, tail-to-EOF, zero-length, whole-file, abort mid-stream
- [ ] 2.9 `src/drive/drive.ts` — `createReadStream` overload signatures forwarded to `streams.ts`; middleware context carries `range?: { offset, length? }`
- [ ] 2.10 `src/drive/drive.test.ts` — overload-compat (bare signal still works), options form, `DRIVE.RANGE_INVALID` synchronous throw, `DRIVE.RANGE_OUT_OF_BOUNDS` (size-known) synchronous throw
## 3. `@suaveplan/drive-uri` — error registry + types
- [ ] 3.1 `src/error-codes/error-codes.ts` — extend `DRIVE_URI_CODES` to 14 codes: `TEMPLATE_READ_ONLY`, `RANGE_INVALID`, `RANGE_OUT_OF_BOUNDS`, `MESH_PROPAGATION_TIMEOUT`, `STAT_UNAVAILABLE`, `LIST_UNSUPPORTED`, `INVALID_QUERY` added to the existing 7
- [ ] 3.2 `src/error-codes/error-codes.ts` — extend `DriveUriErrorOptions` with `range?: { offset: number; length?: number }`, `fileId?: string`, `key?: string`, `value?: string`
- [ ] 3.3 `src/error-codes/error-codes.md` — describe every code, its call site, and the narrowed role of `WRITE_UNSUPPORTED`
- [ ] 3.4 `src/error-codes/error-codes.test.ts` — exhaustive registry shape (14 codes, every code starts with `DRIVE_URI.`, `Object.isFrozen` holds), `DriveUriError` discriminants all surface, `cause` preserved across re-wraps
- [ ] 3.5 `src/types/plugin-options.types.ts` — add `MeshAdvertiseHook { advertise(fileId, bitset, totalChunks, signal?), unadvertise(fileId, signal?) }`; widen `TemplateProvider.fetchTemplate(templateId, options?: { nonce?, signal? })` (preserve back-compat by making the second arg optional); add `?advertise?: MeshAdvertiseHook` to `DriveMeshUriPluginOptions`
- [ ] 3.6 `src/types/plugin-options.types.md` — update
## 4. `@suaveplan/drive-uri` — query parsing
- [ ] 4.1 `src/query/parse-query.ts` — `parseQuery(query: string | undefined, scheme: string): ParsedQuery` returning `{ signalTimeoutMs?, advertise?, nonce? }`; unknown keys throw `DRIVE_URI.INVALID_QUERY`
- [ ] 4.2 `src/query/parse-query.md` — sibling doc
- [ ] 4.3 `src/query/parse-query.test.ts` — per-scheme allow-list, unknown keys, malformed values (`?advertise=maybe`, `?signal-timeout-ms=-1`), empty/missing query
## 5. `@suaveplan/drive-uri` — events
- [ ] 5.1 `src/events/events.ts` — typed topic constants `DRIVE_URI_TOPICS = { write: "drive_uri:write", delete: "drive_uri:delete", meshAdvertised: "drive_uri:mesh_advertised" } as const`; `emit(events, topic, payload)` no-op when `events` is undefined
- [ ] 5.2 `src/events/events.md` — sibling doc
- [ ] 5.3 `src/events/events.test.ts` — emission against a real `EventBusLike` fixture; no-op when bus absent
## 6. `@suaveplan/drive-uri` — adapter widening
- [ ] 6.1 `src/adapter/drive-storage-like.ts` — widen `get`/`set`/`delete`/`exists`/`stat`/`list` to accept and forward `AbortSignal`; add `getMetadata(key, signal?): Promise<StatResult | undefined>` mapping `Drive.stat → StatResult`; add `createWriteStream(key, body, signal?): Promise<void>` consuming `ReadableStream<Uint8Array>` and piping into `Drive.createWriteStream`; widen `createReadStream` to accept `{ offset?, length?, signal? }`; advertise `capabilities = { nativeRange: true, nativeStreams: true }`
- [ ] 6.2 `src/adapter/drive-storage-like.md` — update for every new method and signature
- [ ] 6.3 `src/adapter/drive-storage-like.test.ts` — `get`/`set`/`delete`/`exists`/`stat`/`list` all propagate `signal`; pre-aborted signal short-circuits; `getMetadata` returns `StatResult` shape; `createWriteStream` round-trips; `createReadStream({ offset, length })` forwards; `clear()` still rejects `WRITE_UNSUPPORTED`; `list` returns the expected re-serialisable keys
## 7. `@suaveplan/drive-uri` — InstrumentedDriveAdapter per-scheme dispatch
- [ ] 7.1 `src/plugins/drive/drive.streams.ts` — replace unconditional `set` rejection with per-scheme dispatch: `drive`/`drive+mesh` → `super.set(key, value, signal)`; `drive+template` → throw `DRIVE_URI.TEMPLATE_READ_ONLY`. Same for `delete` and `createWriteStream`
- [ ] 7.2 `src/plugins/drive/drive.streams.ts` — pre-aborted signal on every op → throw `DRIVE_URI.ABORTED`
- [ ] 7.3 `src/plugins/drive/drive.streams.ts` — range pass-through on `createReadStream`; OTel `drive.uri.range = "{offset}-{end}"` span attribute when present
- [ ] 7.4 `src/plugins/drive/drive.streams.ts` — emit `drive_uri:write` / `drive_uri:delete` events via `events/events.ts` on every success and every failure
- [ ] 7.5 `src/plugins/drive/drive.streams.ts` — narrow `WRITE_UNSUPPORTED` to `DriveStorageLikeAdapter.clear()` only (preserve constant for `clear` call site)
- [ ] 7.6 `src/plugins/drive/drive.streams.md` — update
- [ ] 7.7 `src/plugins/drive/drive.streams.test.ts` — pre-aborted abort short-circuits per op; range round-trip; write-stream round-trip; event emission asserted via real bus fixture
## 8. `@suaveplan/drive-uri` — drive:// plugin
- [ ] 8.1 `src/plugins/drive/drive.ts` — invoke `parseQuery(parts.query, "drive")` and propagate `signal-timeout-ms`; reject `?advertise` and `?nonce` with `DRIVE_URI.INVALID_QUERY`
- [ ] 8.2 `src/plugins/drive/drive.test.ts` — full op matrix: read/write/delete/stat/list/exists; AbortSignal threading; pre-aborted abort; `?signal-timeout-ms` honoured; `?advertise` rejected; `DRIVE_ID_MISMATCH` preserved; `INVALID_URI` for malformed inputs
## 9. `@suaveplan/drive-uri` — drive+mesh:// plugin
- [ ] 9.1 `src/plugins/drive-mesh/drive-mesh.ts` — invoke `parseQuery(parts.query, "drive+mesh")`; honour `?advertise=true|false`; on `true` requires `options.advertise` hook else throws `DRIVE_URI.CAPABILITY_MISSING` (reason: `advertise`) synchronously at `parse`
- [ ] 9.2 `src/plugins/drive-mesh/drive-mesh.ts` — after a successful write under `?advertise=true`, invoke `options.advertise.advertise(fileId, bitset, totalChunks, signal)`; rejection → wrap as `DRIVE_URI.MESH_PROPAGATION_TIMEOUT`; on success emit `drive_uri:mesh_advertised`
- [ ] 9.3 `src/plugins/drive-mesh/drive-mesh.ts` — without `?advertise`, emit `drive_uri.mesh_write_local_only` span event on every successful write
- [ ] 9.4 `src/plugins/drive-mesh/drive-mesh.md` — update for opt-in flow + redaction policy
- [ ] 9.5 `src/plugins/drive-mesh/drive-mesh.test.ts` — default = no advertise (spy asserts zero calls + local-only span event); `?advertise=true` → exactly one advertise call; `MeshAdvertiseHook` rejection → `MESH_PROPAGATION_TIMEOUT`; absence of hook with `?advertise=true` → `CAPABILITY_MISSING`; full op matrix
## 10. `@suaveplan/drive-uri` — drive+template:// plugin
- [ ] 10.1 `src/plugins/drive-template/drive-template.ts` — invoke `parseQuery(parts.query, "drive+template")`; forward `?nonce=…` to `templateProvider.fetchTemplate(templateId, { nonce, signal })`
- [ ] 10.2 `src/plugins/drive-template/drive-template.ts` — `set`/`delete`/`createWriteStream` throw `DRIVE_URI.TEMPLATE_READ_ONLY` synchronously, carrying `{ scheme, path: key, templateId }`
- [ ] 10.3 `src/plugins/drive-template/drive-template.md` — update
- [ ] 10.4 `src/plugins/drive-template/drive-template.test.ts` — full read-side op matrix post-hydrate; nonce forwarding; write/delete/writeStream all reject `TEMPLATE_READ_ONLY`; existing hydration behaviour preserved (memoised, concurrent-share, NOT_AVAILABLE, INVALID)
## 11. `@suaveplan/drive-uri` — OTel
- [ ] 11.1 `src/otel/otel.ts` — add `drive_uri.stream_chunks_fetched` histogram; setter for `drive.uri.range` and `drive.uri.has_advertise` span attributes; extend `redactUri` to also strip the fragment
- [ ] 11.2 `src/otel/otel.md` — update
- [ ] 11.3 `src/otel/otel.test.ts` — every counter/histogram fires per op; range attribute present iff range supplied; advertise attribute on mesh writes; redaction holds for `?` and `#`
## 12. `@suaveplan/drive-uri` — conformance
- [ ] 12.1 `src/conformance/conformance.test.ts` — re-run `runUriPluginConformance` with full **read/write/delete/stat/list/exists/range** matrices for all three plugins. Fixtures: 3 valid URIs + 6 invalid + build round-trip + range round-trip per scheme; `drive`/`drive+mesh` pass full cycle; `drive+template` rejects writes with `TEMPLATE_READ_ONLY`
## 13. Integration tests (no mocks per CLAUDE.md rule 2)
- [ ] 13.1 `tests/integration/drive-roundtrip-write.test.ts` — real local-storage `Drive`; `resolver.write` then `resolver.read`; assert equality
- [ ] 13.2 `tests/integration/drive-mesh-roundtrip-write.test.ts` — mesh-configured `Drive` with a real advertise hook implementation; default = no advertise; `?advertise=true` → exactly one advertise observed
- [ ] 13.3 `tests/integration/template-write-rejection.test.ts` — wired template-backed `Drive`; `resolver.write` and `resolver.remove` both reject `TEMPLATE_READ_ONLY`
- [ ] 13.4 `tests/integration/drive-range-roundtrip.test.ts` — write 200 KiB file via local Drive; `resolver.open("drive://…", { offset: 60_000, length: 10_000, signal })` returns exactly bytes [60 000, 70 000)
- [ ] 13.5 `tests/integration/drive-stat-list-exists.test.ts` — full read-side matrix on a real Drive: `resolver.stat` → `StatResult`, `resolver.exists` → boolean, `resolver.list` → URIs that round-trip through `resolver.read`
## 14. Migration audit
- [ ] 14.1 `grep -rln "DRIVE_URI.WRITE_UNSUPPORTED" packages/ | grep -v "/dist/\|/coverage/\|/drive-uri/"` — every hit reviewed; non-matching consumers re-routed onto `TEMPLATE_READ_ONLY` / `RANGE_*` / `MESH_PROPAGATION_TIMEOUT` where appropriate
- [ ] 14.2 Update `README.md` Features + Modules sections to reflect full surface
## 15. Pre-publish gate (§13)
- [ ] 15.1 Spec reconciliation — walk every `### Requirement` and `#### Scenario` in `specs/drive-uri/spec.md` AND `specs/drive/spec.md`; confirm every `**Verified by:**` test name exists verbatim
- [ ] 15.2 `bunx turbo lint typecheck test --filter=@suaveplan/drive --filter=@suaveplan/drive-uri` — green
- [ ] 15.3 `bunx turbo test:coverage --filter=@suaveplan/drive --filter=@suaveplan/drive-uri` — 100/100/100/100 on touched modules
- [ ] 15.4 `bunx turbo build --filter=@suaveplan/drive --filter=@suaveplan/drive-uri` — green
- [ ] 15.5 Banned-pattern, barrel, test-runner-import, missing-`.md`, README, bare-version, subpath-export, security scans — all clean
- [ ] 15.6 Dependent sweep: `bunx turbo test --filter=...@suaveplan/drive` — green
## 16. Root-doc refresh (§13b)
- [ ] 16.1 `bun scripts/folder-reorg/generate-manifest.ts` — refresh `MANIFEST.md`
- [ ] 16.2 `bun scripts/generate-missing.ts` — refresh `MISSING.yml`
- [ ] 16.3 Update `STATUS.md` / `SUMMARY.md` paragraphs for `drive-uri` and `drive` to reflect full surface
- [ ] 16.4 `bunx markdownlint-cli2 --fix` over touched docs
## 17. Archival (§14)
- [ ] 17.1 Fold `specs/drive-uri/spec.md` delta into `openspec/specs/drive-uri/spec.md`
- [ ] 17.2 Fold `specs/drive/spec.md` delta into `openspec/specs/drive/spec.md`
- [ ] 17.3 Move `openspec/changes/complete-drive-uri/` → `openspec/archive/<yyyy-mm-dd>-complete-drive-uri/`
- [ ] 17.4 Delete `openspec/changes/_superseded-narrow-deferrals-2026-05-17/` (research already folded into this change's archive)