# OpenSpec Change: drive-template-drive-surface - **Change ID**: drive-template-drive-surface - **Capability**: `drive` (extending the existing surface) + cross-cutting consumption of `drive-template` - **Status**: SHIPPED - **Date**: 2026-05-04 - **Follow-up to**: `drive-template-v1` (archived 2026-05-04) --- ## Summary Lands the `@suaveplan/drive` surface additions deferred from `drive-template-v1`: `Drive.hydrateFrom(template, options)`, `Drive.getKeyStatus(path)`, `Drive.getPresence(path)`, the `bus:key:grant` listener that flips encrypted-locked entries to encrypted-unlocked when a valid retroactive grant arrives, and the `FileLocationIndex` projection that backs all three. Adds the four hydration error codes (`DRIVE.NOT_FOUND`, `DRIVE.UNAVAILABLE`, `DRIVE.ALREADY_HYDRATED`, `DRIVE.HYDRATE_ABORTED`), extends `DriveEntry` with optional `keyStatus` / `presence` fields, and ships 15 integration tests across hydrate / get-key-status / get-presence / retroactive-key-grant / file-location-index test files at 100/100/100/100 coverage. This change does NOT touch `@suaveplan/drive-plugin-chunks` (the inline-sig modification, DTP-25, lives in `drive-plugin-chunks-inline-sig` follow-up) and does NOT implement the cache-eviction loop (DTP-13..15 retention LRU / max-age GC) — both ship as separate changes since they require touching sibling packages. --- ## Why `drive-template-v1` shipped the standalone `@suaveplan/drive-template` package (12 of 26 spec requirements package-local). The remaining requirements (DTP-22..24, DTP-21 listener integration) require modifying `@suaveplan/drive` because they extend its public surface. Splitting those into this follow-up change keeps the original drive-template ship reviewable while letting the surface additions go through their own gate cycle. ## What changes - ADDED: `Drive.hydrateFrom(template: DriveTemplate, options?: HydrateOptions): Promise` — 8-step validation pipeline reused from `@suaveplan/drive-template`, populates `FileLocationIndex`, schedules byte fetches per `flags.autoDownload`. - ADDED: `Drive.getKeyStatus(path: string): KeyStatus` — synchronous three-state lookup; throws `DRIVE.NOT_FOUND` for unknown paths. - ADDED: `Drive.getPresence(path: string): ChunkPresence` — chunk-level progress lookup; throws `DRIVE.NOT_FOUND` for unknown paths. - ADDED: `Drive.fileLocationIndex` getter — exposes the index for instrumentation and tests. - ADDED: `FileLocationIndex` class with `set` / `getByPath` / `getByFileId` / `hasPath` / `deleteByPath` / `clear` / `size` / `[Symbol.iterator]`. - ADDED: `KeyStatus`, `ChunkPresence`, `HydrateOptions`, `HydrateResult`, `HydrateProgressEvent` types. - ADDED: `DriveOptions.ourPubkey?: PeerId` so the `key:grant` listener can filter to grants addressed to us. - ADDED: bus-listener registration during `hydrateFrom` for `kind: 'retroactive'` files; flips `keyStatus` from `'encrypted-locked'` to `'encrypted-unlocked'` on validated grants. - ADDED: 4 new `DRIVE_CODES`: `NOT_FOUND`, `UNAVAILABLE`, `ALREADY_HYDRATED`, `HYDRATE_ABORTED`. - MODIFIED: `DriveEntry` — optional `keyStatus?: KeyStatus` and `presence?: ChunkPresence` fields. Strictly additive at the consumer surface. - MODIFIED: `Drive` constructor — new optional `ourPubkey` field threaded into the hydration context. Mid-iteration abort handling: the hydration loop yields between iterations via `await Promise.resolve()` so caller-side aborts scheduled via `queueMicrotask` actually land and trigger rollback. This is a behavior improvement over the synchronous-loop original draft. ## Out of scope (deferred to follow-ups) - DTP-13..15 retention LRU / `{ maxAgeMs }` cache-eviction loop — needs deeper integration with `@suaveplan/drive-plugin-chunks` to know per-file chunk inventory. - DTP-25 `FileLocation.storageHints` inline-sig modification — lives on `@suaveplan/drive-plugin-chunks` (follow-up change `drive-plugin-chunks-inline-sig`). - DTP-13 chunk-arrival accounting (the chunks plugin updates `entry.presence` directly) — needs the chunks plugin to import `@suaveplan/drive`'s `FileLocationIndex` type. - CDN bad-signature mesh fall-back (DTP-18) — needs `drive-transport-https` and `drive-mesh` integration; ships with the transport packages. ## Migration impact None. The API additions are strictly additive and the `DriveEntry` extensions are optional. Existing code that ignores the new fields continues to work unchanged. Consumers that opt in to `Drive.hydrateFrom` MUST also pass `ourPubkey` if they want retroactive key grants to land. ## Verification approach - `tests/hydrate-from.test.ts` — successful hydration, abort cancellation, encrypted-vs-plain counting, preFetch modes (`'none'` / `'auto-download-files'`), already-hydrated rejection, progress events, mid-iteration abort with rollback. - `tests/get-key-status.test.ts` — three-state contract + NOT_FOUND. - `tests/get-presence.test.ts` — progress shape contract + NOT_FOUND. - `tests/retroactive-key-grant.test.ts` — listener wiring, defensive branches (unencrypted file / already-unlocked file / no-ourPubkey), bad-issuer rejection. - `tests/file-location-index.test.ts` — direct unit tests for the index methods. 15 new tests across 5 files at 100/100/100/100 coverage. Includes restoration of the previously-removed `rollback` path with a real test exercising it via `queueMicrotask`-scheduled abort (per AGENTS.md §7a). ## Side effect: AGENTS.md §2b and §7a additions This change folder authored two new sections in `openspec/AGENTS.md`: - **§2b "NEVER ship stubs, placeholders, or work-in-progress code"** — captures the user-driven rule that genesis is an enterprise-grade collection of fully working libraries, not a workspace. Bans stubs, TODOs, tautological tests, half-baked docs, and "Phase 1 / Phase 2" splits where Phase 2 is unwritten. - **§7a "Never delete code that serves a purpose to make tests / coverage pass"** — captures the rule that defensive guards, rollback helpers, and forward-defensive catches must be tested, not deleted. Includes an explicit list of banned anti-patterns and the priority order for what to do instead (write a test → restructure to make the path reachable → annotate as unreachable). Both rules ride along here because the work that surfaced them happened during this change's implementation; future changes will reference them directly.