Files
suaveplan/openspec/archive/2026-05-16-add-drive-uri/proposal.md
2026-05-27 23:29:53 +01:00

14 KiB

Add @suaveplan/drive-uri — drive, drive+mesh, drive+template URI plugins for StorageUriResolver

Package: @suaveplan/drive-uri Status: PROPOSED Type: new-package (genesis-original) Depends on: add-storage-uri (archived) — provides UriPlugin, StorageUriResolver, runUriPluginConformance.

  • Design: ./design.md
  • Spec delta: ./specs/drive-uri/spec.md

Why

@suaveplan/storage-uri collapses seven storage adapters into a single read("scheme://…") surface, but it dispatches at the storage tier only. The drive ecosystem — drive, drive-template, drive-mesh, drive-plugin-chunks, drive-transport-https, drive-webrtc — sits above storage and addresses files by drive identity, swarm partition, or dehydrated template descriptor, not by a flat storage key. A consumer holding a drive+mesh://swarm-abc/raw/clip.mov URI today has no way to reach the bytes without instantiating a Drive, wiring chunksPlugin, a mesh transport, a peer registry, and a trust map at every call site.

drive-uri is the bridge. It exports three UriPlugin factories — driveUriPlugin, driveMeshUriPlugin, driveTemplateUriPlugin — that each wrap a caller-provided Drive instance in a StorageLike-conformant adapter and plug into the existing StorageUriResolver. The resolver itself is unchanged; the consumer simply registers drive plugins alongside their fileUriPlugin / httpsUriPlugin / s3UriPlugin set and the same resolver.read("drive+mesh://…") call returns plaintext bytes via the drive's existing middleware (chunksPlugin reassembles chunks from mesh peers; Drive.hydrateFrom validates the template) — drive-uri does not re-implement any chunk codec, peer discovery, or signature verification logic.

This composition is the central design choice: storage-uri stays drive-agnostic and drive-uri stays storage-agnostic; they meet in the consumer's plugin list. No new resolver class, no umbrella composition helper, no API duplication.

What changes

  • ADDED: driveUriPlugin(options): UriPlugin — scheme drive. Parses drive://<driveId>/<path> and forwards path to the caller-supplied Drive (which the consumer wires with chunksPlugin + a local-only configuration). The driveId is captured in meta.driveId and surfaced on spans, errors, and the rebuilt URI; the plugin enforces that the supplied Drive matches the expected driveId via the options' driveId field.
  • ADDED: driveMeshUriPlugin(options): UriPlugin — scheme drive+mesh. Parses drive+mesh://<swarmId>/<path> and forwards path to a caller-supplied Drive that has been configured with chunksPlugin + a mesh transport (drive-transport-https and/or drive-webrtc) and a populated PeerRegistry. The swarmId is captured in meta.swarmId and used only for instrumentation; the plugin does not dispatch on swarmId — that responsibility stays with the Drive and its transports.
  • ADDED: driveTemplateUriPlugin(options): UriPlugin — scheme drive+template. Parses drive+template://<templateId>/<path> and, before delegating to drive.read(path), fetches the DriveTemplate via an injected templateProvider.fetchTemplate(templateId, signal), hydrates the Drive via drive.hydrateFrom(template, { signal }), and caches the (templateId → hydrated) pair so subsequent reads against the same template skip the round-trip. Hydration errors from Drive.hydrateFrom (any DTP_* code) are re-wrapped as DRIVE_URI.TEMPLATE_INVALID with cause preserved.
  • ADDED: DriveStorageLikeAdapter (internal, exported for testability) — translates Drive.read / Drive.write / Drive.delete / Drive.stat / Drive.createReadStream into the StorageLike shape consumed by StorageUriResolver. The shim maps Drive's "throw on missing" semantics into StorageLike's "get returns undefined" semantics (DRIVE.IO_FAILED with a NOT_FOUND-flavoured cause → undefined; every other failure re-throws so the resolver can wrap it as STORAGE_URI.ADAPTER_ERROR).
  • ADDED: DRIVE_URI_CODES registry via defineCodes("DRIVE_URI", [...] as const) — 7 codes: INVALID_URI, DRIVE_ID_MISMATCH, TEMPLATE_NOT_AVAILABLE, TEMPLATE_INVALID, WRITE_UNSUPPORTED, CAPABILITY_MISSING, ABORTED. The resolver itself emits STORAGE_URI.* codes for routing concerns (UNKNOWN_SCHEME, NOT_FOUND, etc.); DRIVE_URI.* is reserved for drive-specific semantic failures the resolver cannot diagnose.
  • ADDED: DriveUriError extends SuaveplanError<DriveUriErrorCode> with uri, scheme, driveId?, swarmId?, templateId?, path? discriminants. Adapter errors from Drive retain their original DRIVE_* / CHUNKS_* / DRIVE_MESH_* / DTP_* codes via cause.
  • ADDED: OTel surface — drive_uri.op_duration_ms{op, scheme, outcome} histogram; drive_uri.{reads, writes, deletes, errors, template_hydrations}_total{scheme, outcome} counters; spans on @suaveplan/drive-uri tracer with drive.uri, drive.uri.scheme, drive.uri.drive_id?, drive.uri.swarm_id?, drive.uri.template_id? attributes (URI query strings stripped to avoid peer-key leakage). The plugins set those attributes via the parent StorageUriResolver span context — the resolver still emits its own storage_uri.* instruments.
  • ADDED: Conformance — each plugin's parse/build round-trip is verified via runUriPluginConformance (re-exported from @suaveplan/storage-uri); each is also covered by an integration test that wires a real Drive (no mocks per CLAUDE.md rule 2).

Out of scope

  • Writes via drive*:// URIs. Initial release rejects write/remove with DRIVE_URI.WRITE_UNSUPPORTED. Reason: write semantics for shared/mesh files (chunk re-upload, gossip re-advertise, template re-issue, retroactive key grants) are non-trivial; they belong in a follow-up add-drive-uri-writes change once read paths are proven in production.
  • A new DriveUriResolver class. The three plugins satisfy the existing UriPlugin contract from @suaveplan/storage-uri; consumers register them into a StorageUriResolver next to their storage plugins. Adding a separate resolver would duplicate dispatch logic without changing observable behaviour.
  • A composeUriResolvers helper. Equivalent to "register more plugins into one resolver." Out of scope unless a future use case requires two resolvers with conflicting state.
  • Range reads on drive+mesh://. drive.createReadStream already streams chunk-by-chunk via the chunks plugin; byte-range slicing on top requires a future Drive.createReadStream({ offset, length }) extension — tracked separately.
  • Auto-discovery of peers across swarms. Each drive+mesh:// URI carries its swarmId in the authority; cross-swarm peer routing is the application's job, not the resolver's.
  • A DriveLike interface in @suaveplan/types. Adding it requires ≥2 consumers per AGENTS.md §11; drive-uri is the only consumer today. If a second appears, hoist then.
  • Reimplementing chunk codec / template validation / peer discovery. All three live in their owning packages and are exercised through the Drive reference the consumer provides. drive-uri is a routing layer, not a drive substitute.

Source mapping

This is a genesis-original package; there is no upstream in ../suaveplan/. All requirements derive from:

  • The UriPlugin / StorageUriResolver / runUriPluginConformance surface in @suaveplan/storage-uri (openspec/specs/storage-uri/spec.md, folded post-archival).
  • The Drive class in @suaveplan/drive — exact signatures used: Drive.read(path: string, options?: ReadOptions): Promise<Uint8Array>, Drive.createReadStream(path: string, signal?: AbortSignal): ReadableStream<Uint8Array>, Drive.write(path, data, options?), Drive.delete(path, signal?), Drive.stat(path, signal?), Drive.hydrateFrom(template, options?).
  • The DriveTemplate schema from @suaveplan/drive-template — fetched via the consumer-supplied templateProvider; never decoded or validated inside drive-uri (validation happens inside Drive.hydrateFrom via the existing 8-step pipeline).

Requirements with no source line

Requirement Justification
Three distinct schemes rather than one drive:// with a discriminator segment Each scheme has different authority semantics (driveId / swarmId / templateId), different consumer-side wiring (local-only Drive vs. mesh-configured Drive vs. mesh-configured Drive + templateProvider), and different failure modes. Collapsing them would force every consumer to wire every dependency.
templateId → hydrated cache lives in drive-uri, not Drive The cache is a URI-resolver concern: it prevents repeated templateProvider.fetchTemplate calls when the same template is referenced by many URIs. Drive.hydrateFrom is naturally idempotent on (driveId, nonce) via TemplateNonceLru, but it does not memoise the template object itself — drive-uri does, scoped to its lifetime.
driveId mismatch is DRIVE_URI.DRIVE_ID_MISMATCH, not a Drive-tier error The check happens in the plugin before any Drive call, so the failure has no Drive origin to wrap. Surfacing it as a drive-uri code keeps the error code namespace honest.
Query strings dropped from span attributes A future iteration may carry peer hints or seeds in the query string; redacting now establishes the contract before any sensitive value is leaked.
Adapter shim translates Drive's throw-on-missing into StorageLike's undefined-on-missing StorageUriResolver.read already maps undefined → STORAGE_URI.NOT_FOUND; pushing the translation up into Drive would change Drive's contract for every consumer. Keeping it in the shim is one-line and local.
No composeUriResolvers helper despite the earlier proposal mentioning one A single StorageUriResolver with mixed storage + drive plugins gives the same composition without introducing a second resolver class or a fall-through dispatch. Simpler is better.
DRIVE_URI.CAPABILITY_MISSING is the lazy-check code A consumer who never resolves drive+template:// should not have to wire a templateProvider. The plugin only requires the dependency on first use; absence at first use raises this code (not at construction).

Verification approach

Every requirement in the spec delta carries a **Verified by:** line referencing concrete it(...) titles in these planned test files:

  • src/plugins/drive/drive.test.ts — parse drive://<driveId>/<path>, dispatch to Drive.read(path), DRIVE_ID_MISMATCH when the URI's driveId differs from the bound Drive's configured id, INVALID_URI for malformed inputs, AbortSignal propagation.
  • src/plugins/drive/drive.streams.test.ts — createReadStream forwarding, mid-stream abort cancels the underlying Drive.createReadStream.
  • src/plugins/drive-mesh/drive-mesh.test.ts — parse drive+mesh://<swarmId>/<path>, dispatch to a mesh-configured Drive, swarmId captured in meta.swarmId, span attribute populated, error wrapping for DRIVE_MESH.* codes.
  • src/plugins/drive-template/drive-template.test.ts — parse drive+template://<templateId>/<path>, templateProvider.fetchTemplate invoked exactly once per templateId (memoisation), drive.hydrateFrom called before first read, TEMPLATE_NOT_AVAILABLE when provider rejects, TEMPLATE_INVALID when hydrateFrom throws any DTP_* code, CAPABILITY_MISSING when no provider is wired.
  • src/adapter/drive-storage-like.test.ts — get returns undefined on DRIVE.IO_FAILED-with-NOT_FOUND cause, re-throws on every other failure, exists is HEAD-flavoured (does not buffer file bytes), stat populates size/lastModified from Drive.stat.
  • src/error-codes/error-codes.test.ts — exhaustive DRIVE_URI_CODES registry, DriveUriError discriminant fields, cause preserved across re-wraps.
  • src/otel/otel.test.ts — template_hydrations_total increments per first-use template, query string stripped from drive.uri attribute, per-scheme error counters populated.
  • src/conformance/conformance.test.ts — runUriPluginConformance (re-exported) passes for all three plugins with the planned URI fixture sets.
  • tests/integration/drive-roundtrip.test.ts — writes a file via Drive.write(path, bytes) on a real local-storage Drive, reads it via resolver.read("drive://.../path") through StorageUriResolver, asserts equality.
  • tests/integration/template-roundtrip.test.ts — issues a DriveTemplate, dehydrates, exposes the template via an in-process templateProvider, resolves drive+template://.../path through a fully wired Drive + chunks plugin pair, asserts plaintext matches source.
  • tests/integration/compose-with-storage-uri.test.ts — single StorageUriResolver registered with fileUriPlugin + driveUriPlugin; resolves file:///tmp/foo and drive://.../foo through the same dispatcher without code-path divergence.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/drive-uri to enumerate. The package is new — no dependents currently. After adoption, the obvious upstream beneficiaries are enterprise-media, audio/video pipelines that consume URI-addressed sources, and drive-sharing flows that surface shareable URIs.

  • No dependents (verified by bunx turbo ls --filter=...@suaveplan/drive-uri returning the package itself).

Affected upstreams (none modified)

Package Action Breaking?
@suaveplan/storage-uri None — consumed via UriPlugin + runUriPluginConformance export; new package registers plugins into the existing resolver no
@suaveplan/drive None — Drive.read / createReadStream / write / delete / stat / hydrateFrom consumed as-is no
@suaveplan/drive-template None — only the DriveTemplate type is consumed (passed through to Drive.hydrateFrom) no
@suaveplan/drive-mesh None — only via the consumer-supplied Drive reference no
@suaveplan/drive-plugin-chunks None — only via the consumer-supplied Drive reference no
@suaveplan/drive-transport-https / drive-webrtc None — wired into Drive by the consumer no

drive-uri MUST NOT silently grow any of these packages' contracts. If a needed capability turns out to be missing during implementation, capture it as a separate proposal against the relevant upstream package — never bend the contract here.

Ordering

This change is unblocked: add-storage-uri was archived in commit f41fe490 (2026-05-16). All other dependencies (drive, drive-mesh, drive-template, drive-plugin-chunks) ship today with the consumed surface intact.