# Design: complete-drive Non-normative companion to `proposal.md` + `specs/drive/spec.md`. Architecture, file layout, algorithms, trade-offs. ## Today's surface (genesis HEAD) `Drive` + `DriveError` + `DRIVE_CODES` + hydration helpers (`hydrateFrom`, `getKeyStatus`, `getPresence`, `KEY_GRANT_TOPIC`) + `FileLocationIndex` + 13 exported types. Eleven `DRIVE_*` codes. Middleware for read/write/delete/stat/list. Streams via positional `signal`. Plugin slot supports install + dispose; no unload. ## Target surface Same shape, widened consistently: ```ts class Drive { // unchanged initialize(): Promise dispose(): Promise use(id: K, storage: S): Drive<...> getStorage(id: K): TStorageMap[K] usePlugin(plugin: DrivePlugin): Promise useMiddleware(mw: DriveMiddleware): void // widened read(path: string, opts?: ReadOptions): Promise write(path: string, data: Uint8Array, opts?: WriteOptions): Promise delete(path: string, opts?: DeleteOptions): Promise stat(path: string, opts?: StatOptions): Promise list(prefix?: string, opts?: ListOptions): Promise createReadStream(path: string, opts?: ReadStreamOptions): ReadableStream createWriteStream(path: string, opts?: WriteStreamOptions): WritableStream // new copy(src: string, dst: string, opts?: CopyOptions): Promise move(src: string, dst: string, opts?: MoveOptions): Promise scan(prefix?: string, opts?: ScanOptions): AsyncIterable watch(path: string, opts?: WatchOptions): DriveWatchHandle usage(): Promise setQuota(quota: DriveQuota): void // plugin lifecycle unloadPlugin(name: string): Promise listPlugins(): readonly DrivePluginInfo[] findPlugin(name: string): DrivePluginInfo | undefined // sharing seam share(path: string, audience: ShareAudience): Promise revoke(handle: ShareHandle): Promise listShares(path?: string): Promise // unchanged hydration hydrateFrom(t: DriveTemplate, opts?: HydrateOptions): Promise getKeyStatus(path: string): KeyStatus getPresence(path: string): ChunkPresence } ``` ## File layout (delta) ```text src/ drive/ (unchanged: drive.ts + hydration helpers) error-codes.ts (+6 codes) file-operations/ read.ts | write.ts | delete.ts | stat.ts | list.ts | streams.ts (existing, signatures widened) copy.ts (NEW) move.ts (NEW) watch.ts (NEW) scan.ts (NEW — auto-pagination helper) observability/ usage.ts (NEW) quota.ts (NEW) spans.ts (NEW — `withSpan('drive.', attrs, fn)`) plugins/ registry.ts (+unload/list/find, name-keyed map) types/ drive.types.ts (new interfaces: DeleteOptions, StatOptions, ReadStreamOptions, WriteStreamOptions, CopyOptions, MoveOptions, ScanOptions, WatchOptions, DriveWatchHandle, DriveChangeEvent, DriveListPage, DriveUsage, DriveQuota, DrivePluginInfo, DriveShareLike, ShareAudience, ShareHandle) ``` All new files ship with sibling `.md` + co-located `.test.ts`. Each feature gets a subpath export in `package.json` per AGENTS §6. ## Algorithms ### `copy(src, dst)` 1. `ensureReady()`; validate both paths. 2. Resolve src + dst storages (may differ). 3. If both storages are the same and `storage.copy?` exists → call it. 4. Otherwise stream src → dst via `createReadStream(src).pipeTo(createWriteStream(dst))`. 5. Wrap in `runMiddleware("copy", ...)`; on any throw, surface as `DriveError(DRIVE.COPY_FAILED, { cause })`. 6. Emit `drive:copy:after` with `{ src, dst, bytes }` on the bus. ### `move(src, dst)` 1. If `storage.move?` and same storage → call it (atomic rename). 2. Else: `copy(src, dst)`; on success `delete(src)`; on delete failure attempt rollback `delete(dst)` and throw `DriveError(DRIVE.MOVE_FAILED)`. ### `watch(path)` `EventBusLike.subscribe("drive:change", filter: e => e.path === path || e.path.startsWith(path + "/"))`. Returns `{ events: ReadableStream → AsyncIterable, unsubscribe }`. When no bus is injected, drive falls back to a private `LightweightFanout` (≤256 listeners) so the API remains usable in zero-config tests. ### `usage()` - Per storage: prefer native `storage.usage?()`. - Fallback: stream `scan()` and sum `stat.size` with backpressure via `AbortSignal` and 5s cache. - Returns `{ byStorage: Record; total: { bytes, files } }`. ### Quota enforcement - Soft cap → emit `drive:quota:warning` (one-shot per cap crossing; reset on usage dropping >5%). - Hard cap → throw `DriveError(DRIVE.QUOTA_EXCEEDED)` from `write`/`copy`/`move` before the underlying storage call. ### Sharing seam `DriveShareLike` lives in drive (not drive-sharing), so drive owns the contract and `drive-sharing` adapts to it — exactly the inversion required to keep drive's dependency footprint sharing-free. ## Trade-offs - **Why options bundle over keeping positional `signal?`** — every other op already uses an options bundle. Consistent surface > one-cycle source churn. Overload preserves source compatibility. - **Why ship `scan` as well as `list`** — `list` is a single page (mirrors storage capability). `scan` is the consumer-friendly auto-paginating iterable. Both are cheap to maintain. - **Why not pull in `drive-sharing` as a peer** — keeps drive a true leaf in the sharing graph; downstream apps that don't share never load the share code. - **Why expose `DriveShareLike` from drive, not from `@suaveplan/types`** — drive owns the *semantics* of paths and audiences; types should remain pure data. When `complete-types-drive-promotion` lands, only the *data* portions (`ShareHandle`, `ShareAudience`) move; the interface stays here. ## Cross-package contract notes - The middleware contract is **observed**, not enforced — adding `copy`/`move`/`watch` slots is purely additive; existing plugins that only implement `read`/`write` keep working unchanged. - `EventBusLike` is the only DI surface required by `watch` / quota / hydrate progress. Plugins that want change events subscribe through the same bus.