# Capability Spec: drive-uri **Package**: `@suaveplan/drive-uri` > Every requirement carries a `**Verified by:**` line. > > **Note — buffered-read signal threading is revisited in `add-signal-to-storage-like`.** `StorageUriResolver.read()` does not currently thread `AbortSignal` through `adapter.get()`, only through `adapter.createReadStream()` via `open()`. The signal-related requirement below therefore verifies signal propagation through the streaming path. A future cross-cutting change against `@suaveplan/types`' `StorageLike` interface will extend `get/set/exists/...` to accept an optional `AbortSignal`; once it lands, the requirement can be extended to cover the buffered path. ## Requirements ### Requirement: `drive://` plugin parses authority as driveId and delegates to `Drive.read` The `driveUriPlugin(options)` factory SHALL return a `UriPlugin` with `scheme === "drive"`. Its `parse` SHALL extract the URI authority as `driveId` and the remainder as a slash-prefix-trimmed `key`. The returned plugin SHALL forward `key` to the consumer-supplied `Drive` via `Drive.read(key, { signal })` (or `Drive.createReadStream` on `open()`). The plugin SHALL throw `DRIVE_URI.DRIVE_ID_MISMATCH` when the URI's driveId does not equal the `driveId` declared in `options`. The `StorageUriResolver` SHALL surface this throw directly per the pass-through rule in `storage-uri`'s "Adapter error re-wrapping" requirement — `err.code === "DRIVE_URI.DRIVE_ID_MISMATCH"` reaches the caller without a `STORAGE_URI.*` envelope. #### Scenario: URI dispatches to Drive.read - **WHEN** `resolver.read("drive://primary/folder/file.bin")` is called and the resolver holds `driveUriPlugin({ drive, driveId: "primary" })` - **THEN** `drive.read("folder/file.bin", { signal })` is invoked exactly once and its return value is the resolver's result #### Scenario: driveId mismatch surfaces DRIVE_URI.DRIVE_ID_MISMATCH directly - **WHEN** `resolver.read("drive://other/x")` is called and the plugin is configured with `driveId: "primary"` - **THEN** the call rejects with `DriveUriError` having `code === "DRIVE_URI.DRIVE_ID_MISMATCH"`, `uri === "drive://other/x"`, `expected === "primary"`, `actual === "other"` (the resolver passes the `DriveUriError` through unchanged; no `STORAGE_URI.*` envelope is added) #### Scenario: build round-trips driveId and key - **WHEN** `plugin.build("folder/file.bin", { driveId: "primary" })` is called - **THEN** the returned string equals `"drive://primary/folder/file.bin"` **Verified by:** `src/plugins/drive/drive.test.ts::"dispatches a drive:// URI to Drive.read"` AND `src/plugins/drive/drive.test.ts::"throws DRIVE_ID_MISMATCH when URI driveId differs from configured driveId"` AND `src/plugins/drive/drive.test.ts::"build round-trips driveId and key into a drive:// URI"` --- ### Requirement: `drive+mesh://` plugin captures swarmId and delegates to Drive without dispatch-on-swarm The `driveMeshUriPlugin(options)` factory SHALL return a `UriPlugin` with `scheme === "drive+mesh"`. Its `parse` SHALL extract the URI authority as `meta.swarmId` and the remainder as `key`. The plugin SHALL forward `key` to the consumer-supplied `Drive` exactly like the `drive` plugin; it SHALL NOT use `swarmId` to choose a `Drive` or transport — peer routing is the `Drive`'s and its transports' responsibility. The swarmId SHALL be surfaced as the `drive.uri.swarm_id` span attribute on every emitted span. #### Scenario: URI dispatches to the configured Drive regardless of swarmId - **WHEN** `resolver.read("drive+mesh://swarmA/raw/clip.mov")` is called and the resolver holds `driveMeshUriPlugin({ drive: meshDrive })` - **THEN** `meshDrive.read("raw/clip.mov", { signal })` is invoked exactly once #### Scenario: swarmId appears on the emitted span - **WHEN** `resolver.read("drive+mesh://swarmA/raw/clip.mov")` is called - **THEN** the emitted span has attribute `drive.uri.swarm_id === "swarmA"` #### Scenario: build round-trips swarmId and key - **WHEN** `plugin.build("raw/clip.mov", { swarmId: "swarmA" })` is called - **THEN** the returned string equals `"drive+mesh://swarmA/raw/clip.mov"` **Verified by:** `src/plugins/drive-mesh/drive-mesh.test.ts::"dispatches a drive+mesh:// URI to the configured Drive"` AND `src/plugins/drive-mesh/drive-mesh.test.ts::"surfaces swarmId on the emitted span"` AND `src/plugins/drive-mesh/drive-mesh.test.ts::"build round-trips swarmId and key into a drive+mesh:// URI"` --- ### Requirement: `drive+template://` plugin fetches the template, hydrates the Drive, then reads The `driveTemplateUriPlugin(options)` factory SHALL return a `UriPlugin` with `scheme === "drive+template"`. On the first `get` for a given `templateId`, the plugin SHALL invoke `options.templateProvider.fetchTemplate(templateId, signal)`, then `options.drive.hydrateFrom(template, { signal })`, before delegating to `options.drive.read(key, { signal })`. Concurrent reads against the same `templateId` SHALL await a single in-flight hydration. Subsequent reads after a successful hydration SHALL skip both calls. #### Scenario: first read hydrates exactly once - **WHEN** `resolver.read("drive+template://templA/folder/file.bin")` is called for the first time - **THEN** `templateProvider.fetchTemplate("templA", signal)` is invoked exactly once AND `drive.hydrateFrom(template, { signal })` is invoked exactly once AND `drive.read("folder/file.bin", { signal })` is invoked exactly once #### Scenario: concurrent reads against the same templateId share one hydration - **WHEN** two `resolver.read("drive+template://templA/x")` calls are issued before the first hydration resolves - **THEN** `templateProvider.fetchTemplate` is invoked exactly once AND `drive.hydrateFrom` is invoked exactly once AND both `drive.read` calls run after hydration resolves #### Scenario: second read after successful hydration skips fetch and hydrate - **WHEN** a second `resolver.read("drive+template://templA/y")` is issued after the first read returned successfully - **THEN** `templateProvider.fetchTemplate` is NOT invoked again AND `drive.hydrateFrom` is NOT invoked again AND `drive.read("y", { signal })` is invoked **Verified by:** `src/plugins/drive-template/drive-template.test.ts::"hydrates exactly once on the first read"` AND `src/plugins/drive-template/drive-template.test.ts::"shares a single hydration across concurrent reads against the same templateId"` AND `src/plugins/drive-template/drive-template.test.ts::"skips fetch and hydrate on subsequent reads against the same templateId"` --- ### Requirement: `templateProvider` is required for `drive+template://` The `driveTemplateUriPlugin(options)` factory SHALL throw `DRIVE_URI.CAPABILITY_MISSING` synchronously at construction time when `options.templateProvider` is missing or is not a function-typed `fetchTemplate` carrier. The factory MUST NOT defer this check to first use — the plugin would otherwise silently accept registration and fail at the first `read`. #### Scenario: missing templateProvider throws at construction - **WHEN** `driveTemplateUriPlugin({ drive })` is called without a `templateProvider` - **THEN** the call throws `DriveUriError` with `code === "DRIVE_URI.CAPABILITY_MISSING"` and a `reason` field naming `templateProvider` #### Scenario: non-function templateProvider.fetchTemplate is rejected - **WHEN** `driveTemplateUriPlugin({ drive, templateProvider: { fetchTemplate: "not a function" as unknown as TemplateProvider["fetchTemplate"] } })` is called - **THEN** the call throws `DriveUriError` with `code === "DRIVE_URI.CAPABILITY_MISSING"` **Verified by:** `src/plugins/drive-template/drive-template.test.ts::"throws CAPABILITY_MISSING when templateProvider is absent"` AND `src/plugins/drive-template/drive-template.test.ts::"throws CAPABILITY_MISSING when templateProvider.fetchTemplate is not a function"` --- ### Requirement: Template fetch and hydration failures map to distinct DRIVE_URI codes When `options.templateProvider.fetchTemplate` rejects, the plugin SHALL re-wrap the rejection as `DRIVE_URI.TEMPLATE_NOT_AVAILABLE` with the original error preserved on `cause`. When `Drive.hydrateFrom` rejects (any `DTP_*` code), the plugin SHALL re-wrap the rejection as `DRIVE_URI.TEMPLATE_INVALID` with the original on `cause`. The two failures MUST NOT be conflated — operator response differs (network/availability vs. integrity). The `StorageUriResolver` SHALL surface both `DriveUriError`s directly per the pass-through rule. #### Scenario: fetchTemplate rejection becomes TEMPLATE_NOT_AVAILABLE - **WHEN** `resolver.read("drive+template://templA/x")` is called and `templateProvider.fetchTemplate` rejects with `new Error("404")` - **THEN** the call rejects with `DriveUriError` having `code === "DRIVE_URI.TEMPLATE_NOT_AVAILABLE"` AND `cause.message === "404"` #### Scenario: hydrateFrom rejection becomes TEMPLATE_INVALID - **WHEN** `resolver.read("drive+template://templA/x")` is called and `drive.hydrateFrom` rejects with a `SuaveplanError` carrying `code === "DTP.TEMPLATE_EXPIRED"` - **THEN** the call rejects with `DriveUriError` having `code === "DRIVE_URI.TEMPLATE_INVALID"` AND `cause.code === "DTP.TEMPLATE_EXPIRED"` **Verified by:** `src/plugins/drive-template/drive-template.test.ts::"wraps fetchTemplate rejection as TEMPLATE_NOT_AVAILABLE"` AND `src/plugins/drive-template/drive-template.test.ts::"wraps hydrateFrom rejection as TEMPLATE_INVALID"` --- ### Requirement: `DriveStorageLikeAdapter` translates Drive missing-file errors into `undefined` The internal `DriveStorageLikeAdapter` class SHALL implement `StorageLike` and SHALL translate `Drive.read` rejections whose `cause` chain indicates a not-found condition into `undefined` (matching the `StorageLike.get` contract). Every other failure SHALL be re-thrown unchanged so the upstream `StorageUriResolver` can wrap it as `STORAGE_URI.ADAPTER_ERROR` (or pass it through if the failure is itself a `SuaveplanError` subclass). #### Scenario: not-found Drive error returns undefined - **WHEN** `adapter.get("missing.bin")` is called and `Drive.read` rejects with a `DRIVE.IO_FAILED` whose cause is a `NOT_FOUND`-flavoured marker - **THEN** the call resolves to `undefined` #### Scenario: arbitrary Drive failure is re-thrown - **WHEN** `adapter.get("x")` is called and `Drive.read` rejects with `new SuaveplanError("DRIVE.PLUGIN_FAILED", { message: "plugin crashed" })` - **THEN** the call rejects with the same error reference; the adapter does NOT translate it to `undefined` #### Scenario: clear() is unsupported - **WHEN** `adapter.clear()` is called - **THEN** the call rejects with a `SuaveplanError` whose message names `Drive.delete(path)` as the supported alternative **Verified by:** `src/adapter/drive-storage-like.test.ts::"returns undefined for Drive not-found errors"` AND `src/adapter/drive-storage-like.test.ts::"re-throws non-not-found Drive errors unchanged"` AND `src/adapter/drive-storage-like.test.ts::"clear is unsupported and names the alternative"` --- ### Requirement: AbortSignal propagation across every plugin op The adapter shim `createReadStream(key, signal)` SHALL forward the `AbortSignal` argument unchanged to `Drive.createReadStream(key, signal)` so that `StorageUriResolver.open()` threads the caller's signal into the drive's streaming path. A signal that is already aborted at call time SHALL cause the adapter shim to reject with `DRIVE_URI.ABORTED` without invoking the `Drive`. The `drive+template` plugin SHALL forward the signal to `templateProvider.fetchTemplate` and `Drive.hydrateFrom` on the first hydration. `StorageUriResolver`'s own pre-flight abort check (which raises `STORAGE_URI.ABORTED` before any plugin is consulted) is preserved as-is and is not modified by drive-uri. #### Scenario: pre-aborted signal short-circuits the adapter stream - **WHEN** `adapter.createReadStream("x", AbortSignal.abort())` is called on the adapter returned by `driveUriPlugin` - **THEN** the call throws `DriveUriError` with `code === "DRIVE_URI.ABORTED"` AND `Drive.createReadStream` is NOT invoked #### Scenario: live signal reaches Drive.createReadStream unchanged - **WHEN** `adapter.createReadStream("x", liveSignal)` is called - **THEN** the `Drive.createReadStream` invocation receives the same `liveSignal` reference **Verified by:** `src/plugins/drive/drive.test.ts::"short-circuits with DRIVE_URI.ABORTED on a pre-aborted signal"` AND `src/plugins/drive/drive.test.ts::"forwards a live AbortSignal unchanged to Drive.createReadStream"` --- ### Requirement: Streaming reads forward Drive.createReadStream when present The `DriveStorageLikeAdapter` SHALL expose a `createReadStream(key, signal)` method that forwards directly to `Drive.createReadStream(key, signal)`. When `resolver.open("drive://…")` is invoked, the resolver SHALL use this method (no buffered fallback) and the `storage_uri.stream_fallback_total` counter SHALL NOT be incremented. #### Scenario: open returns Drive.createReadStream output - **WHEN** `resolver.open("drive://primary/clip.mov")` is called - **THEN** the returned `ReadableStream` is the same reference returned by `Drive.createReadStream("clip.mov", signal)` AND `storage_uri.stream_fallback_total` does NOT increment #### Scenario: mid-stream abort cancels the Drive stream - **WHEN** `resolver.open("drive://primary/clip.mov", controller.signal)` is called and `controller.abort()` is invoked after the first chunk - **THEN** the underlying `Drive.createReadStream`'s signal observes the abort **Verified by:** `src/plugins/drive/drive.streams.test.ts::"open forwards Drive.createReadStream without buffered fallback"` AND `src/plugins/drive/drive.streams.test.ts::"mid-stream abort propagates to Drive.createReadStream"` --- ### Requirement: Writes via drive:// schemes are rejected The three plugins SHALL reject `set` (write) calls with `DRIVE_URI.WRITE_UNSUPPORTED`. The rejection SHALL carry the original URI and scheme on the error so operators can see misuse rate via `drive_uri.writes_total{scheme, outcome: "error"}`. The `StorageUriResolver` SHALL surface this throw directly per the pass-through rule — `err.code === "DRIVE_URI.WRITE_UNSUPPORTED"` reaches the caller without a `STORAGE_URI.WRITE_UNSUPPORTED` envelope. #### Scenario: write against drive:// rejects with DRIVE_URI.WRITE_UNSUPPORTED - **WHEN** `resolver.write("drive://primary/x", new Uint8Array([1, 2, 3]))` is called - **THEN** the call rejects with `DriveUriError` having `code === "DRIVE_URI.WRITE_UNSUPPORTED"`, `uri === "drive://primary/x"`, `scheme === "drive"` #### Scenario: writes_total counter increments on the rejection - **WHEN** `resolver.write("drive+mesh://swarmA/x", bytes)` is called - **THEN** `drive_uri.writes_total{scheme: "drive+mesh", outcome: "error"}` is incremented by 1 **Verified by:** `src/plugins/drive/drive.test.ts::"rejects writes with DRIVE_URI.WRITE_UNSUPPORTED"` AND `src/otel/otel.test.ts::"increments writes_total on rejected writes"` --- ### Requirement: OpenTelemetry instrumentation Every plugin operation SHALL emit a span on the `@suaveplan/drive-uri` tracer and SHALL record `drive_uri.op_duration_ms{op, scheme, outcome}` on its meter. Errors SHALL increment `drive_uri.errors_total{scheme, code}`. The `drive.uri` span attribute SHALL exclude any URI query string. The `drive+template` plugin SHALL increment `drive_uri.template_hydrations_total{outcome}` exactly once per first-use template; cache-hit reads SHALL NOT increment it. #### Scenario: successful read records duration and reads counter - **WHEN** `resolver.read("drive://primary/x")` succeeds - **THEN** `drive_uri.op_duration_ms{op:"read", scheme:"drive", outcome:"success"}` records one observation AND `drive_uri.reads_total{scheme:"drive", outcome:"success"}` increments by 1 #### Scenario: error increments errors_total with the resolved code - **WHEN** `resolver.read("drive+template://templA/x")` rejects with `TEMPLATE_NOT_AVAILABLE` - **THEN** `drive_uri.errors_total{scheme:"drive+template", code:"DRIVE_URI.TEMPLATE_NOT_AVAILABLE"}` increments by 1 #### Scenario: query strings are stripped from span attributes - **WHEN** `resolver.read("drive+mesh://swarmA/path?peers=abcdef")` is called - **THEN** the emitted span's `drive.uri` attribute equals `"drive+mesh://swarmA/path"` (no `?peers=…`) #### Scenario: template_hydrations_total increments once per first-use template - **WHEN** `resolver.read("drive+template://templA/x")` is called then `resolver.read("drive+template://templA/y")` is called - **THEN** `drive_uri.template_hydrations_total{outcome:"success"}` is exactly 1 **Verified by:** `src/otel/otel.test.ts::"records duration histogram and reads counter on success"` AND `src/otel/otel.test.ts::"increments errors_total with the resolved DRIVE_URI code"` AND `src/otel/otel.test.ts::"strips query strings from the drive.uri span attribute"` AND `src/otel/otel.test.ts::"increments template_hydrations_total exactly once per first-use template"` --- ### Requirement: Conformance harness reuse via `runUriPluginConformance` The package SHALL re-export `runUriPluginConformance` from `@suaveplan/storage-uri` and SHALL include fixture-driven conformance tests for each of the three plugins. The fixtures SHALL cover three valid URI shapes, four invalid URI shapes, and a `build` round-trip for each valid fixture. The package SHALL NOT author its own parallel harness. #### Scenario: drive plugin passes the conformance harness - **WHEN** `runUriPluginConformance(driveUriPlugin({ drive, driveId: "primary" }), driveFixtures)` is invoked - **THEN** every assertion passes without throwing #### Scenario: drive+template plugin passes the conformance harness - **WHEN** `runUriPluginConformance(driveTemplateUriPlugin({ drive, templateProvider }), templateFixtures)` is invoked - **THEN** every assertion passes without throwing **Verified by:** `src/conformance/conformance.test.ts::"drive plugin satisfies runUriPluginConformance"` AND `src/conformance/conformance.test.ts::"drive+template plugin satisfies runUriPluginConformance"` --- ### Requirement: Cross-scheme composition with `storage-uri` A single `StorageUriResolver` MUST accept a mix of `storage-uri`'s plugins and `drive-uri`'s plugins and dispatch each URI to the correct plugin without code-path divergence. Registration of one drive plugin SHALL NOT prevent registration of any number of `storage-uri` plugins, and vice versa. #### Scenario: file:// and drive:// resolve through the same resolver - **WHEN** `createStorageUriResolver([fileUriPlugin({ rootDir }), driveUriPlugin({ drive, driveId: "primary" })])` is constructed and both `resolver.read("file:///tmp/a")` and `resolver.read("drive://primary/b")` are invoked - **THEN** the file:// call routes to the file plugin's adapter AND the drive:// call routes to the drive plugin's adapter; neither call observes the other's state **Verified by:** `tests/integration/compose-with-storage-uri.test.ts::"file:// and drive:// resolve through one StorageUriResolver"`