19 KiB
Capability Spec Delta: drive-uri (add-drive-uri)
Package: @suaveplan/drive-uri
Status: PROPOSED
Every requirement carries a
**Verified by:**line at authoring time.
Note — buffered-read signal threading is revisited in
add-signal-to-storage-like.StorageUriResolver.read()does not currently threadAbortSignalthroughadapter.get(), only throughadapter.createReadStream()viaopen(). Requirement R7 below therefore verifies signal propagation through the streaming path. A future cross-cutting change against@suaveplan/types'StorageLikeinterface will extendget/set/exists/...to accept an optionalAbortSignal; once it lands, R7 can be extended to cover the buffered path as well.
ADDED 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.
Scenario: URI dispatches to Drive.read
- WHEN
resolver.read("drive://primary/folder/file.bin")is called and the resolver holdsdriveUriPlugin({ 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
- WHEN
resolver.read("drive://other/x")is called and the plugin is configured withdriveId: "primary" - THEN the call rejects with an error whose
causeis aDriveUriErrorhavingcode === "DRIVE_URI.DRIVE_ID_MISMATCH",uri === "drive://other/x",expected === "primary",actual === "other"(theStorageUriResolverwraps the plugin'sparsethrow asSTORAGE_URI.ADAPTER_ERRORper its own contract, preserving the original oncause).plugin.parse(uri)called outside the resolver throws theDriveUriErrordirectly.
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 holdsdriveMeshUriPlugin({ 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 ANDdrive.hydrateFrom(template, { signal })is invoked exactly once ANDdrive.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.fetchTemplateis invoked exactly once ANDdrive.hydrateFromis invoked exactly once AND bothdrive.readcalls 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.fetchTemplateis NOT invoked again ANDdrive.hydrateFromis NOT invoked again ANDdrive.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 atemplateProvider - THEN the call throws
DriveUriErrorwithcode === "DRIVE_URI.CAPABILITY_MISSING"and areasonfield namingtemplateProvider
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
DriveUriErrorwithcode === "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).
Note: when
resolver.read(...)raises an async failure produced by the drive-template adapter (aDriveUriError), theStorageUriResolverwraps it asSTORAGE_URI.ADAPTER_ERRORper its own spec, preserving the originalDriveUriErroroncause. The scenarios below therefore assert on(err as { cause }).cause— that is the original drive-uri throwable.
Scenario: fetchTemplate rejection becomes TEMPLATE_NOT_AVAILABLE
- WHEN
resolver.read("drive+template://templA/x")is called andtemplateProvider.fetchTemplaterejects withnew Error("404") - THEN the outer call rejects with an error whose
causeis aDriveUriErrorhavingcode === "DRIVE_URI.TEMPLATE_NOT_AVAILABLE"ANDcause.cause.message === "404"
Scenario: hydrateFrom rejection becomes TEMPLATE_INVALID
- WHEN
resolver.read("drive+template://templA/x")is called anddrive.hydrateFromrejects with aSuaveplanErrorcarryingcode === "DTP.TEMPLATE_EXPIRED" - THEN the outer call rejects with an error whose
causeis aDriveUriErrorhavingcode === "DRIVE_URI.TEMPLATE_INVALID"ANDcause.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.
Scenario: not-found Drive error returns undefined
- WHEN
adapter.get("missing.bin")is called andDrive.readrejects with aDRIVE.IO_FAILEDwhose cause is aNOT_FOUND-flavoured marker - THEN the call resolves to
undefined
Scenario: arbitrary Drive failure is re-thrown
- WHEN
adapter.get("x")is called andDrive.readrejects withnew 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
SuaveplanErrorwhose message namesDrive.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 bydriveUriPlugin - THEN the call throws
DriveUriErrorwithcode === "DRIVE_URI.ABORTED"ANDDrive.createReadStreamis NOT invoked
Scenario: live signal reaches Drive.createReadStream unchanged
- WHEN
adapter.createReadStream("x", liveSignal)is called - THEN the
Drive.createReadStreaminvocation receives the sameliveSignalreference
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
ReadableStreamis the same reference returned byDrive.createReadStream("clip.mov", signal)ANDstorage_uri.stream_fallback_totaldoes NOT increment
Scenario: mid-stream abort cancels the Drive stream
- WHEN
resolver.open("drive://primary/clip.mov", controller.signal)is called andcontroller.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"}.
Scenario: write against drive:// rejects with DRIVE_URI.WRITE_UNSUPPORTED on the cause chain
- WHEN
resolver.write("drive://primary/x", new Uint8Array([1, 2, 3]))is called - THEN the call rejects with an error whose outer
code === "STORAGE_URI.WRITE_UNSUPPORTED"(the resolver's normalised write-failure code) AND whosecause.code === "DRIVE_URI.WRITE_UNSUPPORTED"(the original drive-uri rejection) AND whosecausecarriesuri === "drive://primary/x",scheme === "drive"
Scenario: writes_total counter increments on the rejection
- WHEN
adapter.set("x", bytes)is called on the adapter returned bydriveMeshUriPlugin - THEN the call rejects with
code === "DRIVE_URI.WRITE_UNSUPPORTED"ANDdrive_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 ANDdrive_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 withTEMPLATE_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.uriattribute 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 thenresolver.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 bothresolver.read("file:///tmp/a")andresolver.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"