Files
2026-05-27 23:29:53 +01:00

19 KiB

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 DriveUriErrors 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"