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

124 lines
8.6 KiB
Markdown

<!-- 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 -->