Initial Commit
This commit is contained in:
123
openspec/changes/complete-drive-uri/specs/drive/spec.md
Normal file
123
openspec/changes/complete-drive-uri/specs/drive/spec.md
Normal file
@@ -0,0 +1,123 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: drive (drive)
|
||||
|
||||
**Package**: `@suaveplan/drive`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line at authoring time. The named tests are planned; they MUST exist and pass verbatim before this delta is folded into `openspec/specs/drive/spec.md`.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `Drive.createReadStream` SHALL accept a range-options bag overload
|
||||
|
||||
`Drive.createReadStream(path, signalOrOptions?)` SHALL be overloaded to accept either a bare `AbortSignal` (existing form, preserved) or a `RangeReadOptions` bag with optional `offset`, `length`, and `signal`. The implementation MUST runtime-discriminate via `instanceof AbortSignal`. When the options form is passed, the drive MUST forward `{ offset, length, signal }` through the plugin middleware chain via the middleware context's `range` field.
|
||||
|
||||
#### Scenario: bare signal returns the whole-file stream unchanged
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", abortSignal)` is called on a 200 KiB file
|
||||
- **THEN** the returned stream emits the entire 200 KiB payload and respects `abortSignal` for cancellation
|
||||
|
||||
#### Scenario: options form emits exactly the requested slice
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", { offset: 1024, length: 4096 })` is called
|
||||
- **THEN** the returned stream emits exactly bytes `[1024, 5120)` of the source
|
||||
|
||||
#### Scenario: zero-length range yields an immediately-closed stream
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", { offset: 12_345, length: 0 })` is called
|
||||
- **THEN** the returned stream closes immediately with zero enqueued bytes
|
||||
|
||||
**Verified by:** `src/drive/drive.test.ts::"createReadStream(path, signal) preserves whole-file behaviour"` AND `src/drive/drive.test.ts::"createReadStream(path, { offset, length }) emits exactly the requested slice"` AND `src/drive/drive.test.ts::"createReadStream(path, { length: 0 }) returns an immediately closed stream"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Invalid range options SHALL throw `DRIVE.RANGE_INVALID` synchronously
|
||||
|
||||
`Drive.createReadStream(path, options)` SHALL validate `options.offset` and `options.length` synchronously BEFORE constructing the `ReadableStream`. Either field, when present, MUST satisfy `Number.isInteger(x) && x >= 0 && Number.isFinite(x)`. On violation, the method MUST throw `DriveError(DRIVE.RANGE_INVALID, …)` with `context.field` naming the offending field.
|
||||
|
||||
#### Scenario: negative offset throws synchronously
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", { offset: -1 })` is called
|
||||
- **THEN** the call throws `DriveError` with `code === "DRIVE.RANGE_INVALID"` AND `context.field === "offset"` AND no `ReadableStream` is created
|
||||
|
||||
#### Scenario: non-integer length throws synchronously
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", { length: 1.5 })` is called
|
||||
- **THEN** the call throws `DriveError` with `code === "DRIVE.RANGE_INVALID"` AND `context.field === "length"`
|
||||
|
||||
#### Scenario: NaN or Infinity rejected
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", { offset: Number.NaN })` OR `drive.createReadStream("file.bin", { length: Number.POSITIVE_INFINITY })` is called
|
||||
- **THEN** each call throws `DriveError(DRIVE.RANGE_INVALID, …)` synchronously
|
||||
|
||||
**Verified by:** `src/drive/drive.test.ts::"createReadStream throws RANGE_INVALID for negative offset"` AND `src/drive/drive.test.ts::"createReadStream throws RANGE_INVALID for non-integer length"` AND `src/drive/drive.test.ts::"createReadStream throws RANGE_INVALID for NaN or Infinity"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Out-of-bounds range SHALL surface `DRIVE.RANGE_OUT_OF_BOUNDS`
|
||||
|
||||
When the underlying source exposes a known size at stream-start (chunks-plugin manifest, storage `stat`, or a `nativeRange`-capable storage that reports `size`), `Drive.createReadStream` MUST raise `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` synchronously for `offset > size` or `offset + length > size`. When size is unknown at start-time, the stream MUST accept the request, emit available bytes, and surface `DRIVE.RANGE_OUT_OF_BOUNDS` via `controller.error` on close if the byte count emitted is less than the requested `length`. The error MUST carry the originating `CHUNKS.RANGE_OUT_OF_BOUNDS` (when applicable) as `cause`.
|
||||
|
||||
#### Scenario: offset past EOF rejected synchronously when size is known
|
||||
|
||||
- **WHEN** the file is 1024 bytes and `drive.createReadStream("file.bin", { offset: 2048 })` is called via a chunks-plugin-mediated path
|
||||
- **THEN** the call throws `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` synchronously with `cause` carrying `CHUNKS.RANGE_OUT_OF_BOUNDS`
|
||||
|
||||
#### Scenario: offset+length past EOF rejected synchronously when size is known
|
||||
|
||||
- **WHEN** the file is 1024 bytes and `drive.createReadStream("file.bin", { offset: 900, length: 500 })` is called via the chunks plugin
|
||||
- **THEN** the call throws `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` synchronously
|
||||
|
||||
#### Scenario: short-fall on stream end when size was unknown at start
|
||||
|
||||
- **WHEN** the storage cannot report size and `drive.createReadStream("file.bin", { offset: 0, length: 10_000 })` is called and the source closes after 4000 bytes
|
||||
- **THEN** the stream errors with `DriveError(DRIVE.RANGE_OUT_OF_BOUNDS, …)` AND `context.emittedBytes === 4000`
|
||||
|
||||
**Verified by:** `src/drive/drive.test.ts::"createReadStream throws RANGE_OUT_OF_BOUNDS when offset exceeds known size"` AND `src/drive/drive.test.ts::"createReadStream throws RANGE_OUT_OF_BOUNDS when offset+length exceeds known size"` AND `src/file-operations/streams.range.test.ts::"stream errors with RANGE_OUT_OF_BOUNDS on short-fall when size is unknown"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `nativeRange` storage capability SHALL be honoured
|
||||
|
||||
`DriveStorage` SHALL accept an optional `capabilities.nativeRange?: boolean` flag. When `true`, `Drive.createReadStream(path, { offset, length, signal })` MUST forward `{ offset, length }` to `storage.createReadStream` directly. When `false` (or absent), the drive MUST wrap the storage stream in a slicing transform that drops bytes before `offset` and stops emitting after `length` bytes. Both paths MUST produce identical observable output.
|
||||
|
||||
#### Scenario: storage with nativeRange receives offset/length
|
||||
|
||||
- **WHEN** `storage.capabilities.nativeRange === true` and `drive.createReadStream("file.bin", { offset: 1024, length: 4096 })` is invoked
|
||||
- **THEN** `storage.createReadStream` is called exactly once with `{ offset: 1024, length: 4096, signal }` and no slicing transform is interposed
|
||||
|
||||
#### Scenario: storage without nativeRange is wrapped in a slicing transform
|
||||
|
||||
- **WHEN** `storage.capabilities?.nativeRange` is `undefined` or `false` and the same call is made
|
||||
- **THEN** `storage.createReadStream` is called with `{ signal }` only AND the returned stream is wrapped so the consumer observes exactly bytes `[1024, 5120)`
|
||||
|
||||
**Verified by:** `src/file-operations/streams.range.test.ts::"nativeRange storage receives offset and length directly"` AND `src/file-operations/streams.range.test.ts::"non-nativeRange storage is wrapped in a slicing transform"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Range cancellation SHALL propagate via `AbortSignal`
|
||||
|
||||
`Drive.createReadStream(path, options)` MUST propagate `options.signal` (or the bare signal in the back-compat form) through the chunks-plugin / mesh / storage pull chain. On abort, in-flight chunk fetches MUST be cancelled, the stream MUST close with `controller.error(AbortError)`, and (over `drive+mesh://`) the mesh transport MUST emit a `cd:error` envelope with `code: "ABORTED"`.
|
||||
|
||||
#### Scenario: mid-stream abort cancels chunk fetches
|
||||
|
||||
- **WHEN** `drive.createReadStream("file.bin", { offset: 0, length: 1_048_576, signal })` is called and `signal.abort()` is invoked after the first chunk emits
|
||||
- **THEN** the stream closes with an `AbortError`, no further chunks are fetched, and the chunks-plugin loop exits at the next `throwIfAborted` checkpoint
|
||||
|
||||
**Verified by:** `src/file-operations/streams.range.test.ts::"abort mid-range stream cancels in-flight chunk fetches"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `DRIVE_CODES` SHALL include `RANGE_INVALID` and `RANGE_OUT_OF_BOUNDS`
|
||||
|
||||
`DRIVE_CODES` SHALL include `"RANGE_INVALID"` and `"RANGE_OUT_OF_BOUNDS"` declared via the existing `defineCodes("DRIVE", [...] as const)` call. `Object.isFrozen(DRIVE_CODES)` MUST remain `true`. Every code value MUST start with `DRIVE.`.
|
||||
|
||||
#### Scenario: registry exposes the new codes
|
||||
|
||||
- **WHEN** the test imports `DRIVE_CODES` from `@suaveplan/drive`
|
||||
- **THEN** `DRIVE_CODES.RANGE_INVALID === "DRIVE.RANGE_INVALID"` AND `DRIVE_CODES.RANGE_OUT_OF_BOUNDS === "DRIVE.RANGE_OUT_OF_BOUNDS"` AND `Object.isFrozen(DRIVE_CODES) === true`
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts::"DRIVE_CODES exposes RANGE_INVALID and RANGE_OUT_OF_BOUNDS"` AND `src/error-codes.spec-conformance.test.ts::"Object.isFrozen(DRIVE_CODES) is true"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user