# 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`. `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 { readonly name: string; install(drive: Drive): THandle | Promise; dispose?(): void | Promise; } 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 = Record> { use(id: K, storage: S): Drive>; getStorage(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`. 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