Initial Commit
This commit is contained in:
112
openspec/archive/2026-04-28-drive-port/proposal.md
Normal file
112
openspec/archive/2026-04-28-drive-port/proposal.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# OpenSpec Change: drive-port
|
||||
|
||||
- **Change ID**: drive-port
|
||||
- **Package**: `@suaveplan/drive` (new)
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-28
|
||||
|
||||
## Summary
|
||||
|
||||
Port `@suaveplan/drive` from `../suaveplan/packages/universal/drive` into genesis as a **file-VFS-only** core. Drive in genesis exposes paths and bytes — `read`, `write`, `delete`, `stat`, `list`, `createReadStream`, `createWriteStream` — backed by `AbstractStorage` from `@suaveplan/storage`. Chunking, P2P mesh sync, transports, and `transfer()`-style cross-drive copy live in **separate packages** that consume drive via its plugin API. There is no `StorageAdapter` shim — drive sits directly on `AbstractStorage`. Constructor accepts a storage instance directly; `Drive.initialize()` calls `storage.connect()` for storages drive owns; `Drive.dispose()` closes only those it opened.
|
||||
|
||||
## Why this is file-only
|
||||
|
||||
The previous draft of this proposal carried chunks, `transfer()` / manager, and capability-aware streaming all in one package. Discussion this session split those into:
|
||||
|
||||
| Concern | Package | Layer |
|
||||
| --- | --- | --- |
|
||||
| File VFS (paths, bytes, metadata, plugin slot) | `@suaveplan/drive` | this change |
|
||||
| Random-access offset writes on storage | `@suaveplan/storage` (`setPartial`) | `storage-set-partial` |
|
||||
| Chunk format, manifest, packer, sig-verify gate | `@suaveplan/drive-plugin-chunks` | `drive-plugin-chunks-port` |
|
||||
| Peer index, availability gossip, parallel pull | `@suaveplan/drive-mesh` | `drive-mesh-port` |
|
||||
| Wire — HTTPS / WebRTC / libp2p / SMB-as-storage | `@suaveplan/drive-transport-*` | per transport |
|
||||
|
||||
Drive proper does **not** know about chunks, peers, signatures, or wires. It is a thin file abstraction over storage with a plugin slot.
|
||||
|
||||
## Current state
|
||||
|
||||
- Source `@suaveplan/drive` v1.1.1 (`../suaveplan/packages/universal/drive`): ~7 300 LOC, 127 files, ships only `MemoryStorage`, has its own incompatible `StorageAdapter` interface.
|
||||
- Genesis `AbstractStorage` (`packages/storage/storage/src/abstract/abstract.ts:56`) already declares `getPartial`, `getItemSize`, `createReadStream`, `createWriteStream` as abstract — the `MIGRATION.md` §6.3 storage-bridge work is already done. No `StorageProviderAdapter` shim is created.
|
||||
|
||||
## What changes
|
||||
|
||||
### 1. Storage contract — `DriveStorage` is structural
|
||||
|
||||
`Drive` consumes `AbstractStorage` directly. `DriveStorage` is a structural view that adds an `id: string`, optional `capabilities`, and optional `connect`/`dispose`/`isConnected` lifecycle hooks. See `storage-adapter-sketch.md` in this folder for the full TypeScript sketch.
|
||||
|
||||
### 2. Constructor overloads
|
||||
|
||||
```ts
|
||||
new Drive() // empty, attach storages later
|
||||
new Drive(storage) // bare AbstractStorage, registered as id "default"
|
||||
new Drive(storage, options) // storage + options
|
||||
new Drive({ defaultStorage, plugins, ... }) // options-only form
|
||||
```
|
||||
|
||||
`new Drive(storage)` is sugar for `new Drive({ defaultStorage: storage })` — observably equivalent.
|
||||
|
||||
### 3. Lifecycle ownership
|
||||
|
||||
`Drive.initialize()` walks `this.storages` and calls `storage.connect?.()` only when `storage.isConnected?.() !== true`. Storages drive itself connects are tracked in `connectedByMe: WeakSet<DriveStorage>`. `Drive.dispose()` closes only those — pre-connected storages passed in by the caller are left open. Both `initialize` and `dispose` are idempotent.
|
||||
|
||||
### 4. Plugin slot
|
||||
|
||||
```ts
|
||||
interface DrivePlugin<THandle = void> {
|
||||
readonly name: string;
|
||||
install(drive: Drive): THandle | Promise<THandle>;
|
||||
dispose?(): void | Promise<void>;
|
||||
}
|
||||
|
||||
const chunks = drive.use(chunksPlugin({ packer, serverPubKey }));
|
||||
// drive.* — file-level
|
||||
// chunks.* — chunk-level (via the plugin's returned handle)
|
||||
```
|
||||
|
||||
`drive.use(plugin)` returns the plugin's typed handle. Drive's own type surface stays file-level; plugins extend functionality through their handles, not through module augmentation.
|
||||
|
||||
Plugins also register middleware on drive's file ops (`read`, `write`, `delete`, `stat`, `list`) via `drive.useMiddleware(...)`. Middleware execute outer-first on request, outer-last on response; `plugin.dispose()` runs in reverse-registration order on `Drive.dispose()`.
|
||||
|
||||
### 5. Generics
|
||||
|
||||
```ts
|
||||
class Drive<TStorageMap extends Record<string, DriveStorage> = Record<string, DriveStorage>> {
|
||||
use<K extends string, S extends DriveStorage>(id: K, storage: S):
|
||||
Drive<TStorageMap & Record<K, S>>;
|
||||
getStorage<K extends keyof TStorageMap & string>(id: K): TStorageMap[K];
|
||||
}
|
||||
```
|
||||
|
||||
Storage IDs are typed end-to-end. Calls with an unknown id are a TypeScript error.
|
||||
|
||||
### 6. Module-augmentation factories
|
||||
|
||||
`StorageFactory.register('file', FileSystem)` (already in `@suaveplan/storage`) is the single registration point. `Drive.fromConfig({ storage: { type: 'file', config: { ... } } })` resolves through `StorageFactory`. Drive does not maintain a separate adapter registry.
|
||||
|
||||
### 7. Error codes
|
||||
|
||||
`DRIVE_CODES = defineCodes("DRIVE", [NOT_INITIALIZED, DISPOSED, STORAGE_CONNECT_FAILED, STORAGE_NOT_FOUND, IO_FAILED, INVALID_PATH, PLUGIN_FAILED] as const)`. `DriveError extends SuaveplanError<DriveErrorCode>`. Frozen registry.
|
||||
|
||||
### 8. Error handling boundaries
|
||||
|
||||
- `storage.connect()` errors → `DriveError(DRIVE.STORAGE_CONNECT_FAILED, { cause })`
|
||||
- ops on disposed drive → `DriveError(DRIVE.DISPOSED)`
|
||||
- ops before `initialize()` → `DriveError(DRIVE.NOT_INITIALIZED)`
|
||||
- `getStorage("nope")` → `DriveError(DRIVE.STORAGE_NOT_FOUND)`
|
||||
- plugin install/dispose throw → `DriveError(DRIVE.PLUGIN_FAILED, { cause })`
|
||||
|
||||
## Impact
|
||||
|
||||
- New package `packages/drive/drive/` at `0.1.0`.
|
||||
- New OpenSpec capability `drive` at `openspec/specs/drive/spec.md`.
|
||||
- No changes to `@suaveplan/storage` (the `setPartial` addition is `storage-set-partial`, a separate change).
|
||||
|
||||
## Out of scope (covered by sibling changes)
|
||||
|
||||
- Chunk format, manifest, packer integration → `drive-plugin-chunks-port`
|
||||
- Sparse-file storage layout → requires `storage-set-partial`
|
||||
- Mesh peer index / parallel pull → `drive-mesh-port`
|
||||
- Wire transports → `drive-transport-https-port`, `drive-transport-webrtc-port`
|
||||
- `transfer()` / `DriveManager` cross-drive copy → deferred; mesh subsumes most of it
|
||||
- `@suaveplan/drive-sharing` port → separate change
|
||||
- `@suaveplan/vault` orchestration layer → separate change after drive lands
|
||||
57
openspec/archive/2026-04-28-drive-port/specs/drive/spec.md
Normal file
57
openspec/archive/2026-04-28-drive-port/specs/drive/spec.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Drive — Spec Delta (drive-port)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### DRIVE-01 — Drive consumes `AbstractStorage` directly
|
||||
|
||||
`Drive` MUST accept any concrete `AbstractStorage` from `@suaveplan/storage` as its storage substrate. There MUST NOT be a separate `StorageAdapter` shim. Drive MUST narrow capabilities through a `DriveStorage` view that extends `AbstractStorage` with `id: string` and optional `capabilities?: { partialReads?, nativeStreams?, atomicWrites?, contentAddressable?, partialWrites?, requiresLifecycle? }` plus optional `connect?()`, `dispose?()`, `isConnected?()` lifecycle hooks.
|
||||
|
||||
**Verified by:** `src/drive/drive.spec-conformance.test.ts` (`DRIVE-01`).
|
||||
|
||||
### DRIVE-02 — `initialize()` is idempotent and ownership-tracking
|
||||
|
||||
`Drive.initialize()` MUST be idempotent. For each registered storage, drive MUST call `storage.connect?.()` ONLY when `storage.isConnected?.() !== true`, and MUST record those storages in an internal `connectedByMe: WeakSet<DriveStorage>`. Connection failures MUST surface as `DriveError(DRIVE.STORAGE_CONNECT_FAILED, { cause })`.
|
||||
|
||||
**Verified by:** `src/drive/drive.spec-conformance.test.ts` (`DRIVE-02`).
|
||||
|
||||
### DRIVE-03 — `dispose()` is idempotent and ownership-respecting
|
||||
|
||||
`Drive.dispose()` MUST be idempotent. Drive MUST only close storages in `connectedByMe`; storages pre-connected by the caller MUST be left open. After `dispose()`, the drive transitions to a terminal disposed state.
|
||||
|
||||
**Verified by:** `src/drive/drive.spec-conformance.test.ts` (`DRIVE-03`).
|
||||
|
||||
### DRIVE-04 — Operations after `dispose()` throw
|
||||
|
||||
Any file operation invoked on a disposed drive (`read`, `write`, `delete`, `stat`, `list`, `createReadStream`, `createWriteStream`) MUST throw `DriveError(DRIVE.DISPOSED)`. Operations invoked before `initialize()` MUST throw `DriveError(DRIVE.NOT_INITIALIZED)`.
|
||||
|
||||
**Verified by:** `src/drive/drive.spec-conformance.test.ts` (`DRIVE-04`).
|
||||
|
||||
### DRIVE-05 — Error-code registry is namespaced and frozen
|
||||
|
||||
`DRIVE_CODES` MUST be declared via `defineCodes("DRIVE", [...])` from `@suaveplan/error`. `Object.isFrozen(DRIVE_CODES)` MUST be `true`. Every code value MUST start with `DRIVE.`. `DriveError` MUST extend `SuaveplanError<DriveErrorCode>`.
|
||||
|
||||
**Verified by:** `src/error-codes.spec-conformance.test.ts` (`DRIVE-05`).
|
||||
|
||||
### DRIVE-06 — Plugin middleware order
|
||||
|
||||
`drive.use(plugin)` and `drive.useMiddleware(mw)` MUST install in registration order. Middleware chains for `read`, `write`, `delete`, `stat`, `list` MUST execute outer-first on the request path and outer-last on the response path. `plugin.dispose()` MUST be called in reverse-registration order on `Drive.dispose()`. Plugin install/dispose throws MUST surface as `DriveError(DRIVE.PLUGIN_FAILED, { cause })`.
|
||||
|
||||
**Verified by:** `src/plugins/registry.spec-conformance.test.ts` (`DRIVE-06`).
|
||||
|
||||
### DRIVE-07 — Capability fallbacks for streams
|
||||
|
||||
`drive.createReadStream(path)` MUST use `storage.createReadStream` when `storage.capabilities?.nativeStreams === true`, otherwise MUST fall back to the inherited `AbstractStorage.defaultCreateReadStream`. Same rule for `createWriteStream`.
|
||||
|
||||
**Verified by:** `src/file-operations/streams.spec-conformance.test.ts` (`DRIVE-07`).
|
||||
|
||||
### DRIVE-08 — Generic storage typing
|
||||
|
||||
`Drive<TStorageMap>` MUST narrow `TStorageMap` through `drive.use<K, S>(id, storage)` so that `drive.getStorage(id)` returns `TStorageMap[K]` exactly (no widening to the base `DriveStorage`). Calls with an unknown id MUST be a TypeScript error.
|
||||
|
||||
**Verified by:** `src/drive/drive.spec-conformance.test.ts` (`DRIVE-08`, type-level via `expectTypeOf`).
|
||||
|
||||
### DRIVE-09 — Constructor overloads
|
||||
|
||||
`new Drive()`, `new Drive(storage)`, `new Drive(storage, options)`, and `new Drive({ defaultStorage, plugins, ... })` MUST all be valid. The bare-storage form MUST be observably equivalent to passing `{ defaultStorage: storage }` and MUST register the storage under id `"default"`.
|
||||
|
||||
**Verified by:** `src/drive/drive.spec-conformance.test.ts` (`DRIVE-09`).
|
||||
197
openspec/archive/2026-04-28-drive-port/storage-adapter-sketch.md
Normal file
197
openspec/archive/2026-04-28-drive-port/storage-adapter-sketch.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Unified `DriveStorage` — design sketch
|
||||
|
||||
Reference for `drive-port`. Drive does **not** define its own `StorageAdapter` class; it consumes `AbstractStorage` from `@suaveplan/storage` directly and layers a thin capability protocol on top.
|
||||
|
||||
## 1. The base contract is already there
|
||||
|
||||
`packages/storage/storage/src/abstract/abstract.ts:56` declares the abstract surface drive needs:
|
||||
|
||||
```ts
|
||||
abstract class AbstractStorage implements StorageLike {
|
||||
abstract get(key, signal?): Promise<Uint8Array | undefined>;
|
||||
abstract set(key, value, opts?, signal?): Promise<void>;
|
||||
abstract delete(key, signal?): Promise<boolean>;
|
||||
abstract exists(key, signal?): Promise<boolean>;
|
||||
abstract list(opts?, signal?): Promise<StorageListResult>;
|
||||
abstract clear(signal?): Promise<void>;
|
||||
abstract getWithMetadata(key, signal?): Promise<StorageResult<Uint8Array> | null>;
|
||||
abstract getMetadata(key, signal?): Promise<StorageMetadata | null>;
|
||||
abstract getSize(signal?): Promise<number>;
|
||||
abstract getPartial(key, offset, length, signal?): Promise<Uint8Array | null>;
|
||||
abstract getItemSize(key, signal?): Promise<number | null>;
|
||||
abstract createReadStream(key, signal?): ReadableStream<Uint8Array>;
|
||||
abstract createWriteStream(key, signal?): WritableStream<Uint8Array>;
|
||||
}
|
||||
```
|
||||
|
||||
Every method drive's source `StorageAdapter` provided (`read`, `write`, `readPartial`, `size`, streams) is already present here. The `MIGRATION.md` §6.3 enhancement work is a no-op in genesis.
|
||||
|
||||
## 2. The `DriveStorage` view
|
||||
|
||||
```ts
|
||||
// @suaveplan/drive/src/types/drive-storage.types.ts
|
||||
import type { AbstractStorage } from "@suaveplan/storage/abstract";
|
||||
|
||||
export interface StorageCapabilities {
|
||||
/** `getPartial` is a native byte-range read (S3, OPFS). */
|
||||
partialReads?: boolean;
|
||||
/** `createReadStream`/`createWriteStream` are native (no buffer-and-chunk emulation). */
|
||||
nativeStreams?: boolean;
|
||||
/** `set` is atomic (write-or-rollback, no partial visibility). */
|
||||
atomicWrites?: boolean;
|
||||
/** Keys are content-derived hashes (chunk maps can be cross-mounted without re-read). */
|
||||
contentAddressable?: boolean;
|
||||
/** `connect` MUST be called before use; `dispose` releases handles. */
|
||||
requiresLifecycle?: boolean;
|
||||
}
|
||||
|
||||
export interface DriveStorage extends AbstractStorage {
|
||||
/** Stable identifier used by `Drive.use(id, storage)` and `getStorage(id)`. */
|
||||
readonly id: string;
|
||||
/** Optional self-description; absent flags = treat as `false`. */
|
||||
readonly capabilities?: StorageCapabilities;
|
||||
/** Optional lifecycle hooks (drive only invokes them when defined and `isConnected?.() !== true`). */
|
||||
connect?(): Promise<void>;
|
||||
dispose?(): Promise<void>;
|
||||
isConnected?(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
`DriveStorage` is **structural** — any `AbstractStorage` subclass with an `id` field satisfies it; capabilities and lifecycle hooks are duck-typed and probed at runtime.
|
||||
|
||||
## 3. How drive uses capabilities
|
||||
|
||||
```ts
|
||||
async readPartial(path: string, offset: number, length: number, signal?: AbortSignal) {
|
||||
const storage = this.resolve(path);
|
||||
if (storage.capabilities?.partialReads) {
|
||||
const slice = await storage.getPartial(path, offset, length, signal);
|
||||
if (slice !== null) return slice;
|
||||
}
|
||||
const full = await storage.get(path, signal);
|
||||
if (full === undefined) throw new DriveError(DRIVE_CODES.IO_FAILED, `not found: ${path}`);
|
||||
return full.slice(offset, offset + length);
|
||||
}
|
||||
```
|
||||
|
||||
The capability flag is the contract; the inherited `defaultCreate*Stream` from `AbstractStorage` already provides the emulation fallback, so drive doesn't reimplement it.
|
||||
|
||||
## 4. Lifecycle in `Drive`
|
||||
|
||||
```ts
|
||||
class Drive<TStorageMap extends Record<string, DriveStorage>> {
|
||||
readonly #storages = new Map<string, DriveStorage>();
|
||||
readonly #connectedByMe = new WeakSet<DriveStorage>();
|
||||
#state: "fresh" | "ready" | "disposed" = "fresh";
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.#state === "ready") return;
|
||||
if (this.#state === "disposed") throw new DriveError(DRIVE_CODES.DISPOSED);
|
||||
for (const storage of this.#storages.values()) {
|
||||
if (storage.isConnected?.() === true) continue;
|
||||
if (typeof storage.connect !== "function") continue;
|
||||
try {
|
||||
await storage.connect();
|
||||
this.#connectedByMe.add(storage);
|
||||
} catch (cause) {
|
||||
throw new DriveError(
|
||||
DRIVE_CODES.STORAGE_CONNECT_FAILED,
|
||||
`failed to connect storage "${storage.id}"`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
}
|
||||
this.#state = "ready";
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
if (this.#state === "disposed") return;
|
||||
for (const storage of this.#storages.values()) {
|
||||
if (!this.#connectedByMe.has(storage)) continue;
|
||||
await storage.dispose?.();
|
||||
}
|
||||
this.#state = "disposed";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ownership rule:** drive only closes what drive opened. Pre-connected storages passed in by the caller are left open on `dispose()`. This is the property `connectedByMe: WeakSet<DriveStorage>` enforces.
|
||||
|
||||
## 5. Generic narrowing
|
||||
|
||||
```ts
|
||||
class Drive<TStorageMap extends Record<string, DriveStorage> = {}> {
|
||||
use<K extends string, S extends DriveStorage>(
|
||||
id: K,
|
||||
storage: S,
|
||||
): Drive<TStorageMap & Record<K, S>> {
|
||||
this.#storages.set(id, storage);
|
||||
return this as unknown as Drive<TStorageMap & Record<K, S>>;
|
||||
}
|
||||
|
||||
getStorage<K extends keyof TStorageMap & string>(id: K): TStorageMap[K] {
|
||||
const storage = this.#storages.get(id);
|
||||
if (!storage) throw new DriveError(DRIVE_CODES.STORAGE_NOT_FOUND, id);
|
||||
return storage as TStorageMap[K];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```ts
|
||||
const drive = new Drive()
|
||||
.use("local", new MemoryStorage({ id: "local" }))
|
||||
.use("remote", new S3Storage({ id: "remote", bucket: "vault" }));
|
||||
|
||||
const local: MemoryStorage = drive.getStorage("local"); // narrowed
|
||||
drive.getStorage("nope"); // TS error: argument not assignable to keyof TStorageMap
|
||||
```
|
||||
|
||||
## 6. Constructor overloads
|
||||
|
||||
```ts
|
||||
function Drive(): Drive;
|
||||
function Drive(storage: DriveStorage): Drive<{ default: DriveStorage }>;
|
||||
function Drive(storage: DriveStorage, options: DriveOptions): Drive<{ default: DriveStorage }>;
|
||||
function Drive(options: DriveOptions): Drive;
|
||||
```
|
||||
|
||||
The bare-storage form is sugar:
|
||||
|
||||
```ts
|
||||
new Drive(storage) // equivalent to new Drive({ defaultStorage: storage })
|
||||
```
|
||||
|
||||
Both register the storage under id `"default"` so `drive.getStorage("default")` works in either path.
|
||||
|
||||
## 7. Plugin middleware
|
||||
|
||||
```ts
|
||||
export interface DrivePlugin {
|
||||
readonly name: string;
|
||||
install(drive: Drive): void;
|
||||
dispose?(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface DriveMiddleware {
|
||||
read?(ctx: ReadCtx, next: () => Promise<Uint8Array>): Promise<Uint8Array>;
|
||||
write?(ctx: WriteCtx, next: () => Promise<void>): Promise<void>;
|
||||
delete?(ctx: DeleteCtx, next: () => Promise<boolean>): Promise<boolean>;
|
||||
stat?(ctx: StatCtx, next: () => Promise<StorageMetadata | null>): Promise<StorageMetadata | null>;
|
||||
list?(ctx: ListCtx, next: () => Promise<StorageListResult>): Promise<StorageListResult>;
|
||||
}
|
||||
```
|
||||
|
||||
`drive.use(plugin)` registers in order; middleware execution is outer-first on the request path, outer-last on the response path; `plugin.dispose()` runs in reverse-registration order during `Drive.dispose()`.
|
||||
|
||||
## 8. What this replaces
|
||||
|
||||
| Source-side artefact | Replaced by |
|
||||
| --- | --- |
|
||||
| `drive/src/storage/adapter.ts` (StorageAdapter base class) | Direct `AbstractStorage` consumption + structural `DriveStorage` interface |
|
||||
| `drive/src/storage/memory.ts` (drive-internal MemoryStorage) | `MemoryStorage` from `@suaveplan/storage` |
|
||||
| `storageRegistry` inside drive | `StorageFactory` from `@suaveplan/storage` (single registration point) |
|
||||
| `MIGRATION.md` §6.4 `StorageProviderAdapter` | Not needed — `AbstractStorage` already exposes the four methods |
|
||||
| Constructor flags (`enableCow`, `enableDedup`, ...) | Plugins via `drive.use(cowPlugin())` |
|
||||
| Hard-wired `MemoryStorage` default | No default — caller must pass a storage or use `Drive.fromConfig({ storage: { type, config } })` |
|
||||
51
openspec/archive/2026-04-28-drive-port/tasks.md
Normal file
51
openspec/archive/2026-04-28-drive-port/tasks.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Tasks: drive-port
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/drive/drive/` with `package.json` (`@suaveplan/drive@0.0.0`), `tsconfig.json` extending `@suaveplan/typescript-config/library.json`, `vitest.config.ts` via `createTestConfig()`, root `README.md`.
|
||||
- [x] 1.2 Workspace deps: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/storage`, `@suaveplan/streams`, `@opentelemetry/api`, `zod`. All `workspace:^`.
|
||||
- [x] 1.3 Subpath exports: `.`, `./types`, `./errors`. (No `./manager`, `./plugins/*` — those live in sibling packages.)
|
||||
|
||||
## 2. Types and contract
|
||||
|
||||
- [x] 2.1 `src/types/drive.types.ts` — `DriveOptions`, `DriveStorage` (extends `AbstractStorage`), `StorageCapabilities`, `DrivePlugin<THandle>`, `DriveMiddleware`.
|
||||
- [x] 2.2 `src/error-codes.ts` — `defineCodes("DRIVE", [...])` with codes from proposal §7. Sibling `DriveError` extends `SuaveplanError<DriveErrorCode>`.
|
||||
- [x] 2.3 `src/schemas/` — Zod boundary schemas for `DriveOptions`. Marker `// schema: boundary-only`.
|
||||
|
||||
## 3. `Drive` class
|
||||
|
||||
- [x] 3.1 `src/drive/drive.ts` — overloaded constructor `()`, `(storage)`, `(storage, options)`, `(options)`. Generics `<TStorageMap>`. Internal `connectedByMe: WeakSet<DriveStorage>`, `state: "fresh" | "ready" | "disposed"`.
|
||||
- [x] 3.2 `Drive.initialize()` — idempotent; for each storage, call `connect?.()` only when `isConnected?.() !== true`; record in `connectedByMe`; surface failures as `DriveError(DRIVE.STORAGE_CONNECT_FAILED, { cause })`.
|
||||
- [x] 3.3 `Drive.dispose()` — idempotent; close only storages in `connectedByMe`; transition to terminal disposed state.
|
||||
- [x] 3.4 `drive.use(id, storage)` — generic narrowing.
|
||||
- [x] 3.5 `drive.getStorage(id)` — typed lookup; `DRIVE.STORAGE_NOT_FOUND` if absent.
|
||||
- [x] 3.6 `Drive.fromConfig(config)` — resolves storage via `StorageFactory` from `@suaveplan/storage`.
|
||||
|
||||
## 4. File operations
|
||||
|
||||
- [x] 4.1 `src/file-operations/read.ts` / `write.ts` / `delete.ts` / `stat.ts` / `list.ts` — each runs through the middleware chain.
|
||||
- [x] 4.2 `src/file-operations/streams.ts` — `createReadStream(path)` / `createWriteStream(path)`; prefer storage's native streams when `capabilities.nativeStreams === true`, otherwise inherit `AbstractStorage.defaultCreate{Read,Write}Stream`.
|
||||
|
||||
## 5. Plugin slot
|
||||
|
||||
- [x] 5.1 `src/plugins/registry.ts` — install/dispose lifecycle, middleware chain (outer-first request, outer-last response).
|
||||
- [x] 5.2 `drive.use(plugin)` returns the plugin's typed handle.
|
||||
- [x] 5.3 `drive.useMiddleware(middleware)` — registers middleware separately from plugin install.
|
||||
|
||||
## 6. Spec
|
||||
|
||||
- [x] 6.1 Create `openspec/specs/drive/spec.md` with `DRIVE-01`…`DRIVE-09`, each with `**Verified by:**` line.
|
||||
|
||||
## 7. Conformance tests
|
||||
|
||||
- [x] 7.1 `src/drive/drive.spec-conformance.test.ts` — DRIVE-01, DRIVE-02, DRIVE-03, DRIVE-04, DRIVE-08, DRIVE-09.
|
||||
- [x] 7.2 `src/error-codes.spec-conformance.test.ts` — DRIVE-05.
|
||||
- [x] 7.3 `src/plugins/registry.spec-conformance.test.ts` — DRIVE-06.
|
||||
- [x] 7.4 `src/file-operations/streams.spec-conformance.test.ts` — DRIVE-07.
|
||||
|
||||
## 8. Verify
|
||||
|
||||
- [x] 8.1 `bunx turbo lint typecheck test --filter=@suaveplan/drive` green.
|
||||
- [x] 8.2 `bunx turbo test:coverage --filter=@suaveplan/drive` 100/100/100/100.
|
||||
- [x] 8.3 `bun scripts/openspec-conformance.ts @suaveplan/drive` PASS.
|
||||
- [x] 8.4 Sibling `.md` exists for every non-test, non-types, non-barrel `.ts` in `src/`.
|
||||
Reference in New Issue
Block a user