Files
suaveplan/openspec/archive/2026-05-19-add-packer-browser-core/proposal.md
2026-05-27 23:29:53 +01:00

184 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# add-packer-browser-core — Cluster A migration of `@suaveplan/packer-browser`
**Package**: `@suaveplan/packer-browser`
**Status**: PROPOSED
**Type**: new-package
**Design**: not applicable (Cluster A core; no separate `design.md` — architecture is captured inline in `tasks.md` and the spec delta).
**Spec delta**: ./specs/packer-browser/spec.md
## Why
`@suaveplan/packer-browser` is the browser-tier runtime for whole-file packed
archives produced by `@suaveplan/packer`. The upstream form (audit:
`../suaveplan/enterprise/packer-browser.md` lines 1–115) carries a
production-grade `PackedResourceManager` (LRU + ref-count + Blob-URL
bookkeeping), a Service Worker interceptor, and lifecycle helpers — but
violates four genesis quality bars: (1) `PackedResourceManager.ts` is 318 LOC
(over the 300-line hard cap), (2) the Service Worker is untested, (3) the
package ships React hooks as a runtime dependency, and (4) it has no archive
signature verification, no OTel metrics, no injected `Clock`/`Rng`, no
`MessageResolver`-keyed errors, no `AbortSignal` propagation, and no
`@suaveplan/retry` on the IO source-pull path. Cluster A lands the
production-quality core that fixes every cross-cutting gap and the §3 audit
items; Clusters B and C land §4 enhancements (progressive streaming,
range-request SW, encrypted archives, versioned URLs, BackgroundFetch,
diagnostics overlay).
The package is **NEW on genesis** — `STATUS.md` marks it as pending under
the `bitpacker` section (line 129). This change also relocates it under
`data:` in `scripts/folder-reorg/assignment.yml` between `packer` and
`pagination`, mirroring `@suaveplan/packer`'s home (the upstream `bitpacker`
grouping is historical and no longer matches its dep graph).
## What changes
This change adds a new package; every requirement is `## ADDED`. The public
surface is:
- ADDED: `PackedResourceManager` (class) — lifecycle façade over the cache
+ loader; emits typed `EventEmitter` events for `load` / `cacheHit` /
`cacheMiss` / `cacheEvict` / `error`.
- ADDED: `createPackedResourceManager(options?)` — explicit factory; the
default `.` export does NOT construct a singleton at import time.
- ADDED: `packedResourceManager` — module-singleton, hosted behind the
`/singleton` subpath export so the default surface stays import-cost-free.
- ADDED: `PackedResourceCache` + `PackedResourceLoader` — the two
collaborators behind the manager. Available via `/manager` subpath for
consumers that need surgical imports.
- ADDED: `runPackedResourceServiceWorker(scope)` + handlers for `install` /
`activate` / `fetch` / `message`, exposed via `/service-worker` subpath
along with the hand-rolled `ServiceWorkerGlobalScope` shim
(`createServiceWorkerShim`) used by the unit-test harness.
- ADDED: `registerPackedResourceServiceWorker` /
`unregisterPackedResourceServiceWorker` / `clearServiceWorkerCache` /
`getServiceWorkerCacheStatus`, via `/utils` subpath.
- ADDED: `verifyArchiveSignature(manifestBytes, signature, publicKey)` —
Ed25519 verification gate run before any packed resource is unpacked.
- ADDED: `PACKER_BROWSER_CODES` + `PackerBrowserError` (extends
`SuaveplanError`) — ≥6 stable codes:
`RESOURCE_NOT_FOUND`, `SW_REGISTRATION_FAILED`,
`ARCHIVE_SIGNATURE_INVALID`, `UNSUPPORTED_RESOURCE_TYPE`, `ABORTED`,
`LOAD_FAILED`.
- ADDED: `createPackerBrowserObservability` + `PackerBrowserObservability`
— OTel meter/tracer bundle exposing
`packed_resource_fetch_total`, `packed_resource_cache_hits_total`,
`packed_resource_cache_miss_total` counters and
`packed_resource_decode_ms` histogram.
- ADDED: `PACKER_BROWSER_MESSAGES` + `defaultPackerBrowserResolver` —
English i18n catalog keyed `packer-browser.error.<CODE>.<scenario>`.
- ADDED: `resolveRuntime` + `defaultClock` / `defaultRng` —
injectable platform primitives (mirrors `@suaveplan/packer/runtime`).
## Out of scope
- React hooks (`usePackedResource`, `usePackedAudio`, `usePackedFont`,
`usePackedImage`, `usePackedVideo`) — these belong in a separate
`@suaveplan/react-packer` companion (assignment task #5 in the upstream
enterprise audit). Adding React as a runtime dep would force the
browser-tier package onto every consumer; the audit explicitly recommends
the split.
- Progressive streaming unpack (§4.1) — Cluster B.
- Range-request SW support (§4.2) — Cluster B.
- Adaptive cache sizing via `navigator.storage.estimate()` (§4.3) —
Cluster B.
- Encrypted-archive pluggable keyring (§4.4) — Cluster B. (Basic AEAD
decryption via the existing `@suaveplan/crypto`/`@suaveplan/packer`
surface is supported on the loader; the **pluggable keyring** is what
Cluster B adds.)
- `<link rel="preload">` generation (§4.5) — Cluster C.
- Content-addressed versioned URLs + invalidation (§4.6) — Cluster C.
- Multi-origin SW helper (§4.7) — Cluster C.
- BackgroundFetch API integration (§4.8) — Cluster C.
- Diagnostics dev overlay (§4.9) — Cluster C.
- Subpath splits beyond `.`, `/singleton`, `/manager`, `/service-worker`,
`/utils` (§4.10) — Cluster C.
## Source mapping
This change derives from `../suaveplan/enterprise/packer-browser.md`.
| Requirement | Source lines | Notes |
|---|---|---|
| PACKER-BROWSER-1 — Manager split (lifecycle / cache / loader) | L20-L22, L35-L36, L45-L45 | Splits the 318-LOC `PackedResourceManager.ts` into `manager.ts`, `cache.ts`, `loader.ts`, each ≤ 300 LOC. |
| PACKER-BROWSER-2 — Service Worker is unit-tested via a real SW-scope shim | L29-L30, L40-L41, L46-L46 | Hand-rolled `createServiceWorkerShim()`; no `vi.mock`. |
| PACKER-BROWSER-3 — Archive signature verification (Ed25519) before unpack | L38-L39, L48-L48 | Run via `@suaveplan/crypto` `ed25519.verify` before the loader hands bytes to `Unpacker`. |
| PACKER-BROWSER-4 — OTel metrics emitted from production paths | L37-L38, L49-L49 | Counters + histogram on the loader/cache; asserted via real in-memory `MeterProvider`. |
| PACKER-BROWSER-5 — Factory + singleton split (`/singleton` subpath) | L36-L37, L51-L51 | `createPackedResourceManager(opts)` plus a `./singleton` module-side export. |
| PACKER-BROWSER-6 — Error hierarchy via `@suaveplan/error` | L62-L63 | `PACKER_BROWSER_CODES` ≥ 6 codes; `PackerBrowserError extends SuaveplanError`. |
| PACKER-BROWSER-7 — `AbortSignal` propagation on every async entrypoint | (cross-cutting; mirrors PACKER-24) | All async public APIs accept `signal?` and check at every yield point. |
| PACKER-BROWSER-8 — i18n MessageResolver keys (no hard-coded English) | (cross-cutting; mirrors PACKER-25 i18n cluster) | `PACKER_BROWSER_MESSAGES` catalog + `defaultPackerBrowserResolver`. |
| PACKER-BROWSER-9 — Injected `Clock`/`Rng` (no `Date.now`/`Math.random` outside runtime defaults) | (cross-cutting; mirrors PACKER-25) | `resolveRuntime` + `defaultClock`/`defaultRng`. |
| PACKER-BROWSER-10 — `@suaveplan/retry` on IO source-pull path | (cross-cutting; mirrors PACKER-22) | Loader's `fetch` step routes through `withRetry` with single-attempt default. |
| PACKER-BROWSER-11 — `@suaveplan/feature-flags` provider injected | (cross-cutting; reserved for Cluster B/C) | No-op default; flags slot present so Cluster B can opt in without API churn. |
| PACKER-BROWSER-12 — Subpath exports `./manager`, `./service-worker`, `./utils`, `./singleton` | L78-L78, L90-L91 | Tree-shakeable surface for the §3 minimum. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| PACKER-BROWSER-7 / -8 / -9 / -10 / -11 | Cross-cutting genesis baselines (CLAUDE.md §0a parity rules — every migrated package must adopt the runtime/observability/i18n/retry/flag scaffolding regardless of whether the audit explicitly called it out). |
## Verification approach
Every requirement carries one or more `**Verified by:**` entries pointing at
concrete `it(...)` titles in the listed test files. Tests are co-located with
implementation (`src/<feature>/<feature>.test.ts`).
- `src/manager/manager.test.ts` — covers PACKER-BROWSER-1, -5, -7, -11.
- `src/cache/cache.test.ts` — covers PACKER-BROWSER-1, -4, -9.
- `src/loader/loader.test.ts` — covers PACKER-BROWSER-1, -3, -4, -7, -10.
- `src/service-worker/service-worker.test.ts` — covers PACKER-BROWSER-2.
- `src/service-worker/sw-shim.test.ts` — covers PACKER-BROWSER-2.
- `src/signature/signature.test.ts` — covers PACKER-BROWSER-3.
- `src/error-codes.test.ts` — covers PACKER-BROWSER-6.
- `src/i18n/messages.test.ts` — covers PACKER-BROWSER-8.
- `src/runtime/runtime.test.ts` — covers PACKER-BROWSER-9.
- `src/observability/observability.test.ts` — covers PACKER-BROWSER-4.
- `src/utils/register-service-worker.test.ts` — covers PACKER-BROWSER-2.
- `src/singleton.test.ts` — covers PACKER-BROWSER-5.
- `src/flags/flags.test.ts` — covers PACKER-BROWSER-11.
- `tests/e2e/sw.spec.ts` — Playwright sweep proving SW registration +
packed URL interception in a real Chromium tab.
## Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `PackedResourceManager` class (LRU, refCount, Blob bookkeeping) | ✅ | ⬆️ | Migrated; split into manager + cache + loader; now factory-constructed. |
| Module-singleton `packedResourceManager` | ✅ | ⬆️ | Migrated to `/singleton` subpath; default `.` export no longer auto-constructs. |
| Service Worker interceptor (install / activate / fetch / message) | ✅ | ⬆️ | Migrated; now unit-tested via hand-rolled `ServiceWorkerGlobalScope` shim. |
| Lifecycle helpers (register/unregister/clearCache/getCacheStatus) | ✅ | ✅ | Migrated 1:1 under `/utils`. |
| React hooks (`usePackedResource*`) | ✅ | 🚫 | Out of scope per audit recommendation: relocated to companion `@suaveplan/react-packer` package (assignment.md task #5). Excluding React from the runtime surface is the explicit audit ask. |
| Archive signature verification | 🚫 | ⬆️ | Added: `verifyArchiveSignature` via `@suaveplan/crypto`'s `ed25519.verify`. Audit §3 ask. |
| OTel metrics (fetch/hit/miss/decode) | 🚫 | ⬆️ | Added: real `MeterProvider`-asserted counters + histogram. Audit §3 ask. |
| `MessageResolver`-keyed errors | 🚫 | ⬆️ | Added: `PACKER_BROWSER_MESSAGES` catalog. Cross-cutting genesis baseline. |
| Injected `Clock` / `Rng` | 🚫 | ⬆️ | Added: `resolveRuntime` mirrors packer's pattern. |
| `AbortSignal` on every async entrypoint | 🚫 | ⬆️ | Added: load / preload / verify / SW fetch all accept and check `signal`. |
| `@suaveplan/retry` on IO source-pull | 🚫 | ⬆️ | Added: loader routes `fetch` through `withRetry`. |
| `@suaveplan/feature-flags` provider | 🚫 | ⬆️ | Added: no-op default; reserved for Cluster B/C feature gates. |
| Progressive streaming unpack | 🚫 | 🚫 | Cluster B (audit §4.1). |
| Range-request SW support | 🚫 | 🚫 | Cluster B (audit §4.2). |
| Adaptive cache sizing | 🚫 | 🚫 | Cluster B (audit §4.3). |
| Encrypted-archive pluggable keyring | 🚫 | 🚫 | Cluster B (audit §4.4). |
| `<link rel="preload">` hints | 🚫 | 🚫 | Cluster C (audit §4.5). |
| Versioned content-addressed URLs | 🚫 | 🚫 | Cluster C (audit §4.6). |
| Multi-origin SW helper | 🚫 | 🚫 | Cluster C (audit §4.7). |
| BackgroundFetch integration | 🚫 | 🚫 | Cluster C (audit §4.8). |
| Diagnostics dev overlay | 🚫 | 🚫 | Cluster C (audit §4.9). |
| Subpath splits beyond minimum five | 🚫 | 🚫 | Cluster C (audit §4.10). |
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/packer-browser` to enumerate. The
package is **NEW on genesis** with zero existing consumers:
- No dependents (verified by `grep -rln "@suaveplan/packer-browser" packages/ apps/`).
### Migration
Not applicable — no genesis consumer code exists to migrate. Future React
consumers should target the planned `@suaveplan/react-packer` companion
package (out of scope here).