Initial Commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: drive-uri (fix-storage-uri-error-passthrough)
|
||||
|
||||
**Package**: `@suaveplan/drive-uri`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line at authoring time. This delta MODIFIES five scenarios across three requirements from `add-drive-uri` (still in `openspec/changes/`, not yet archived). The `**Note —** revisited in `fix-storage-uri-error-passthrough`...` annotation at the top of the drive-uri `add-drive-uri` delta is also retired by this change. (The companion `**Note —** buffered-read signal threading` annotation stays, since `add-signal-to-storage-like` has not landed.)
|
||||
|
||||
## MODIFIED 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 (see `fix-storage-uri-error-passthrough` spec delta for storage-uri).
|
||||
|
||||
#### 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 per the storage-uri pass-through rule; 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: 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: 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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,65 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: storage-uri (fix-storage-uri-error-passthrough)
|
||||
|
||||
**Package**: `@suaveplan/storage-uri`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line at authoring time. This delta MODIFIES two requirements from `add-storage-uri` (still in `openspec/changes/`, not yet archived).
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Adapter error re-wrapping
|
||||
|
||||
Errors thrown by the underlying adapter SHALL be inspected before re-wrap. Any throwable that is an instance of `SuaveplanError` (the runtime carrier for `defineCodes`-based error namespaces) SHALL be re-thrown unchanged — the resolver MUST NOT wrap a namespaced error inside its own envelope. Throwables that are NOT `SuaveplanError` instances (plain `Error`, foreign objects, strings) SHALL be wrapped in `StorageUriError` with `code === "STORAGE_URI.ADAPTER_ERROR"`, the original preserved as `cause`, and `uri` + `scheme` discriminants populated.
|
||||
|
||||
This includes `StorageUriError` itself (which `extends SuaveplanError`) — the pre-existing pass-through is preserved as a special case of the new rule.
|
||||
|
||||
#### Scenario: arbitrary non-Suaveplan throwable is re-wrapped as ADAPTER_ERROR
|
||||
|
||||
- **WHEN** `read("file:///a")` is called and `adapter.get` throws `new Error("disk fault")`
|
||||
- **THEN** the rejection is a `StorageUriError` with `code === "STORAGE_URI.ADAPTER_ERROR"`, `cause.message === "disk fault"`, `uri === "file:///a"`, and `scheme === "file"`
|
||||
|
||||
#### Scenario: SuaveplanError subclass passes through with its original code
|
||||
|
||||
- **WHEN** `read("drive://primary/x")` is called and `adapter.get` throws `new DriveUriError("DRIVE_ID_MISMATCH", { uri, expected, actual })` (a `SuaveplanError` subclass with `code === "DRIVE_URI.DRIVE_ID_MISMATCH"`)
|
||||
- **THEN** the rejection is that same `DriveUriError` instance, `err.code === "DRIVE_URI.DRIVE_ID_MISMATCH"`, and the resolver does NOT wrap it inside a `StorageUriError`
|
||||
|
||||
#### Scenario: existing StorageUriError is re-thrown unchanged
|
||||
|
||||
- **WHEN** the adapter (via a nested resolver) throws `new StorageUriError("NOT_FOUND", ...)`
|
||||
- **THEN** the same instance is propagated; no re-wrap occurs
|
||||
|
||||
**Verified by:** `src/resolver/resolver.test.ts::"wraps non-SuaveplanError throwables as ADAPTER_ERROR"` AND `src/resolver/resolver.test.ts::"passes through thrown SuaveplanError subclasses unchanged"` AND `src/resolver/resolver.test.ts::"re-throws StorageUriError unchanged"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Streaming writes with buffered fallback
|
||||
|
||||
The `write(uri, body, signal?)` method SHALL accept `Uint8Array` or `ReadableStream<Uint8Array>` for `body`. When the underlying adapter exposes `createWriteStream`, the resolver SHALL forward it. When the adapter exposes only `set`, the resolver SHALL drain the stream into a single `Uint8Array`, call `set` once, and SHALL increment `storage_uri.buffered_write_total{scheme}`.
|
||||
|
||||
When `adapter.set` throws, the resolver SHALL apply the same pass-through rule as `Adapter error re-wrapping` (above): `SuaveplanError` subclasses SHALL pass through unchanged with their original code; non-`SuaveplanError` throwables SHALL be wrapped as `StorageUriError` with `code === "STORAGE_URI.WRITE_UNSUPPORTED"` and the original preserved as `cause`.
|
||||
|
||||
#### Scenario: Uint8Array body is forwarded directly
|
||||
|
||||
- **WHEN** `write("file:///a.bin", new Uint8Array([1,2,3]))` is called
|
||||
- **THEN** `adapter.set` is invoked exactly once with the exact byte array
|
||||
|
||||
#### Scenario: stream body is drained into a single set call
|
||||
|
||||
- **WHEN** `write("file:///a.bin", stream)` is called where `stream` yields three chunks
|
||||
- **THEN** `adapter.set` is invoked exactly once with the concatenated bytes; `buffered_write_total{scheme:"file"}` is incremented by 1
|
||||
|
||||
#### Scenario: non-Suaveplan throw on set wraps as WRITE_UNSUPPORTED
|
||||
|
||||
- **WHEN** `write("memory://x/y", bytes)` is called against a plugin whose `adapter.set` throws `new Error("read only mount")`
|
||||
- **THEN** the call rejects with `StorageUriError` having `code === "STORAGE_URI.WRITE_UNSUPPORTED"` and `cause.message === "read only mount"`
|
||||
|
||||
#### Scenario: SuaveplanError on set passes through with its original code
|
||||
|
||||
- **WHEN** `write("drive://primary/x", bytes)` is called against a plugin whose `adapter.set` throws `new DriveUriError("WRITE_UNSUPPORTED", { uri, scheme: "drive" })`
|
||||
- **THEN** the rejection is that same `DriveUriError` instance, `err.code === "DRIVE_URI.WRITE_UNSUPPORTED"`, and the resolver does NOT re-wrap it as `STORAGE_URI.WRITE_UNSUPPORTED`
|
||||
|
||||
**Verified by:** `src/resolver/resolver.writes.test.ts::"forwards Uint8Array body directly to adapter.set"` AND `src/resolver/resolver.writes.test.ts::"drains ReadableStream into single set call"` AND `src/resolver/resolver.writes.test.ts::"wraps non-Suaveplan throw on set as WRITE_UNSUPPORTED"` AND `src/resolver/resolver.writes.test.ts::"passes through thrown SuaveplanError subclasses on write"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user