Initial Commit
This commit is contained in:
183
openspec/archive/2026-05-19-add-packer-browser-core/proposal.md
Normal file
183
openspec/archive/2026-05-19-add-packer-browser-core/proposal.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,352 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: packer-browser (add-packer-browser-core)
|
||||
|
||||
**Package**: `@suaveplan/packer-browser`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PACKER-BROWSER-1 — Manager split into lifecycle / cache / loader
|
||||
|
||||
The package SHALL split the upstream 318-LOC `PackedResourceManager` into
|
||||
three collaborator modules under the 300-LOC hard cap:
|
||||
`src/manager/manager.ts` (lifecycle façade, event emission, public surface),
|
||||
`src/cache/cache.ts` (LRU + ref-count + Blob-URL bookkeeping), and
|
||||
`src/loader/loader.ts` (fetch + signature verification + Unpacker + Blob
|
||||
construction). `PackedResourceManager` MUST compose a `PackedResourceCache`
|
||||
and a `PackedResourceLoader`; neither collaborator is allowed to import the
|
||||
other. Each of the three files MUST be ≤ 300 lines per CLAUDE.md §3.
|
||||
|
||||
#### Scenario: each module is under the 300-line cap
|
||||
|
||||
- **WHEN** the package source tree is measured
|
||||
- **THEN** `src/manager/manager.ts`, `src/cache/cache.ts`, and
|
||||
`src/loader/loader.ts` are each ≤ 300 lines
|
||||
|
||||
#### Scenario: manager composes cache and loader
|
||||
|
||||
- **WHEN** `createPackedResourceManager()` is constructed with no options
|
||||
- **THEN** `manager.load(url)` delegates to the loader for IO and to the
|
||||
cache for cache-key resolution / insertion / eviction, and emits
|
||||
`cacheHit` / `cacheMiss` / `load` events accordingly
|
||||
|
||||
**Verified by:** `src/manager/manager.test.ts::"composes a cache and a loader and emits cacheHit on the second load"` AND `src/cache/cache.test.ts::"evicts the least-recently-used entry with refCount=0 when capacity is exceeded"` AND `src/loader/loader.test.ts::"fetches packed bytes and returns a Blob URL with the metadata-derived mime type"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-2 — Service Worker is unit-tested via a real shim
|
||||
|
||||
The package SHALL ship a hand-rolled `ServiceWorkerGlobalScope` shim
|
||||
(`createServiceWorkerShim`) at `src/service-worker/sw-shim.ts` that
|
||||
implements `addEventListener`, `dispatchEvent`, `skipWaiting`, `clients`,
|
||||
`caches` (a `CacheStorage` + `Cache` real implementation, not a stub),
|
||||
`FetchEvent`, `ExtendableEvent`, and `ExtendableMessageEvent`. The shim
|
||||
MUST be a working in-memory implementation — no `vi.mock`, no `vi.fn` for
|
||||
the shim itself, no stubs. The service-worker module
|
||||
`src/service-worker/service-worker.ts` MUST register its handlers against
|
||||
the shim in tests; the same module is loaded verbatim by the production SW
|
||||
bundle. Tests MUST exercise: install (skipWaiting called), activate (old
|
||||
cache deletion + clients.claim called), fetch (packed-URL interception +
|
||||
cache write), fetch (non-packed URL passthrough), message (CACHE_CLEAR +
|
||||
CACHE_STATUS round trip).
|
||||
|
||||
#### Scenario: install handler calls skipWaiting
|
||||
|
||||
- **WHEN** an `install` event is dispatched on the shim after
|
||||
`runPackedResourceServiceWorker(shim)` is called
|
||||
- **THEN** the shim records that `skipWaiting()` was invoked exactly once
|
||||
|
||||
#### Scenario: fetch handler intercepts a packed URL
|
||||
|
||||
- **WHEN** a `FetchEvent` for a URL matching `/\.packed$/` is dispatched
|
||||
and the loader returns unpacked bytes
|
||||
- **THEN** the event's `respondWith` is called with a `Response` carrying
|
||||
the unpacked bytes plus an `X-Unpacked: true` header, and a copy is
|
||||
written to the shim's `Cache`
|
||||
|
||||
#### Scenario: fetch handler passes through non-packed URLs
|
||||
|
||||
- **WHEN** a `FetchEvent` for a URL that does not match a packed pattern
|
||||
is dispatched
|
||||
- **THEN** `respondWith` is NOT called and the event propagates normally
|
||||
|
||||
**Verified by:** `src/service-worker/sw-shim.test.ts::"createServiceWorkerShim implements addEventListener, dispatchEvent, caches, and clients"` AND `src/service-worker/service-worker.test.ts::"install handler calls skipWaiting on the registered scope"` AND `src/service-worker/service-worker.test.ts::"fetch handler intercepts a packed URL and writes the unpacked response to the cache"` AND `src/service-worker/service-worker.test.ts::"fetch handler passes through URLs that do not match a packed pattern"` AND `src/service-worker/service-worker.test.ts::"message handler answers CACHE_CLEAR and CACHE_STATUS round trips"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-3 — Archive signature verification (Ed25519) before unpack
|
||||
|
||||
The package SHALL expose `verifyArchiveSignature(manifestBytes, signature,
|
||||
publicKey, opts?)` at `src/signature/signature.ts` which calls
|
||||
`@suaveplan/crypto`'s `ed25519.verify(publicKey, signature, manifestBytes)`
|
||||
and returns the boolean result. The loader MUST invoke
|
||||
`verifyArchiveSignature` before passing bytes to `Unpacker` when the caller
|
||||
supplies a `verifyKey` and the response carries an `X-Archive-Signature`
|
||||
header (hex-encoded 64-byte signature). A failed verification MUST throw
|
||||
`PackerBrowserError(PACKER_BROWSER.ARCHIVE_SIGNATURE_INVALID)` with the
|
||||
i18n key `packer-browser.error.ARCHIVE_SIGNATURE_INVALID.verify-failed`
|
||||
BEFORE any bytes are unpacked.
|
||||
|
||||
#### Scenario: valid signature passes through to unpack
|
||||
|
||||
- **WHEN** the loader is supplied a `verifyKey` and the response signature
|
||||
verifies against the manifest bytes
|
||||
- **THEN** the loader proceeds to `Unpacker.unpack` and returns the Blob URL
|
||||
|
||||
#### Scenario: invalid signature aborts before unpack
|
||||
|
||||
- **WHEN** the supplied signature does not verify
|
||||
- **THEN** the loader throws `PackerBrowserError` with code
|
||||
`PACKER_BROWSER.ARCHIVE_SIGNATURE_INVALID`, `Unpacker.unpack` is not
|
||||
called, and no Blob URL is created
|
||||
|
||||
#### Scenario: missing signature header is tolerated when verifyKey is absent
|
||||
|
||||
- **WHEN** the caller does not pass `verifyKey`
|
||||
- **THEN** the loader skips signature verification and proceeds
|
||||
|
||||
**Verified by:** `src/signature/signature.test.ts::"returns true when ed25519 verifies the signature"` AND `src/signature/signature.test.ts::"returns false when ed25519 rejects the signature"` AND `src/loader/loader.test.ts::"throws ARCHIVE_SIGNATURE_INVALID before calling Unpacker.unpack on a bad signature"` AND `src/loader/loader.test.ts::"skips signature verification when verifyKey is absent"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-4 — OTel metrics from production paths
|
||||
|
||||
The package SHALL emit four OpenTelemetry instruments from production code
|
||||
paths via `createPackerBrowserObservability()`:
|
||||
`packed_resource_fetch_total` (Counter — incremented on every loader
|
||||
`fetch` attempt), `packed_resource_cache_hits_total` (Counter — incremented
|
||||
on every cache hit inside `PackedResourceCache.get`),
|
||||
`packed_resource_cache_miss_total` (Counter — incremented on every cache
|
||||
miss inside `PackedResourceCache.get`), and `packed_resource_decode_ms`
|
||||
(Histogram — observed once per successful loader decode with the elapsed
|
||||
ms between fetch start and Blob construction). The instruments MUST be
|
||||
asserted under a real in-memory `MeterProvider` from
|
||||
`@opentelemetry/sdk-metrics` — no `vi.mock` is permitted.
|
||||
|
||||
#### Scenario: cache hit increments the cache_hits counter
|
||||
|
||||
- **WHEN** `cache.get(key)` returns a hit on the second call with the same
|
||||
key
|
||||
- **THEN** the asserted `MeterProvider`'s collected metrics show
|
||||
`packed_resource_cache_hits_total >= 1`
|
||||
|
||||
#### Scenario: loader decode observes the histogram
|
||||
|
||||
- **WHEN** the loader completes a successful unpack
|
||||
- **THEN** the asserted `MeterProvider`'s collected metrics show
|
||||
`packed_resource_decode_ms` recorded at least one data point
|
||||
|
||||
**Verified by:** `src/observability/observability.test.ts::"createPackerBrowserObservability instantiates the four named instruments on the supplied Meter"` AND `src/cache/cache.test.ts::"increments packed_resource_cache_hits_total on a hit and packed_resource_cache_miss_total on a miss"` AND `src/loader/loader.test.ts::"records packed_resource_decode_ms on a successful decode"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-5 — Factory + `/singleton` subpath
|
||||
|
||||
The package SHALL expose `createPackedResourceManager(options?:
|
||||
PackedResourceManagerOptions)` from `src/manager/manager.ts` as the only
|
||||
construction surface in the default `.` export. The default export MUST
|
||||
NOT auto-construct a global instance at import time. The module-singleton
|
||||
`packedResourceManager` SHALL be hosted exclusively behind the
|
||||
`/singleton` subpath export (`src/singleton.ts`) so that consumers who
|
||||
import `@suaveplan/packer-browser` pay no import-time cost for the
|
||||
singleton.
|
||||
|
||||
#### Scenario: default export carries no auto-constructed singleton
|
||||
|
||||
- **WHEN** the default `@suaveplan/packer-browser` entry is loaded
|
||||
- **THEN** no `PackedResourceManager` instance exists in module scope
|
||||
(verified by introspecting the module's exported keys)
|
||||
|
||||
#### Scenario: /singleton subpath returns a stable shared instance
|
||||
|
||||
- **WHEN** `@suaveplan/packer-browser/singleton` is imported twice
|
||||
- **THEN** both imports return the same `packedResourceManager` reference
|
||||
|
||||
**Verified by:** `src/manager/manager.test.ts::"createPackedResourceManager returns a fresh instance per call"` AND `src/singleton.test.ts::"packedResourceManager is the same reference across imports"` AND `src/singleton.test.ts::"the default index module does not export a packedResourceManager singleton"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-6 — Error hierarchy via `@suaveplan/error`
|
||||
|
||||
The package SHALL define `PACKER_BROWSER_CODES` via `defineCodes("PACKER_BROWSER", [...])`
|
||||
from `@suaveplan/error` carrying at least six codes:
|
||||
`RESOURCE_NOT_FOUND`, `SW_REGISTRATION_FAILED`,
|
||||
`ARCHIVE_SIGNATURE_INVALID`, `UNSUPPORTED_RESOURCE_TYPE`, `ABORTED`,
|
||||
`LOAD_FAILED`. Every error thrown by the package MUST be a
|
||||
`PackerBrowserError` (extending `SuaveplanError`) carrying one of these
|
||||
codes. Each code MUST be reachable from at least one production call
|
||||
site (no unused registry entries — CLAUDE.md §2c).
|
||||
|
||||
#### Scenario: every registered code is thrown by a production path
|
||||
|
||||
- **WHEN** the production source tree is grepped for each
|
||||
`PACKER_BROWSER_CODES.<CODE>` symbol
|
||||
- **THEN** every code has at least one reference outside `error-codes.ts`,
|
||||
the i18n catalog, and test files
|
||||
|
||||
#### Scenario: thrown errors are PackerBrowserError instances
|
||||
|
||||
- **WHEN** the loader rejects on a 404 packed URL
|
||||
- **THEN** the rejection is `instanceof PackerBrowserError` and its
|
||||
`.code === PACKER_BROWSER.RESOURCE_NOT_FOUND`
|
||||
|
||||
**Verified by:** `src/error-codes.test.ts::"PACKER_BROWSER_CODES exposes the six stable codes"` AND `src/error-codes.test.ts::"every registered code is referenced by a production path"` AND `src/loader/loader.test.ts::"throws PackerBrowserError(RESOURCE_NOT_FOUND) on a 404 response"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-7 — `AbortSignal` propagation on every async entrypoint
|
||||
|
||||
Every async public entrypoint SHALL accept an optional `signal?:
|
||||
AbortSignal` and check it at every yield point. The following surfaces
|
||||
MUST honour `signal`: `PackedResourceManager.load`,
|
||||
`PackedResourceManager.preload`, `PackedResourceLoader.load`,
|
||||
`registerPackedResourceServiceWorker`, `clearServiceWorkerCache`,
|
||||
`getServiceWorkerCacheStatus`, the SW `fetch` event handler (composing
|
||||
caller's `event.request.signal` with an internal abort). A pre-aborted
|
||||
signal MUST cause the call to reject with
|
||||
`PackerBrowserError(PACKER_BROWSER.ABORTED)` BEFORE any IO runs.
|
||||
|
||||
#### Scenario: pre-aborted signal short-circuits manager.load
|
||||
|
||||
- **WHEN** `manager.load(url, { signal })` is called with `signal.aborted === true`
|
||||
- **THEN** the call rejects with `PackerBrowserError(PACKER_BROWSER.ABORTED)`
|
||||
and no `fetch` is issued
|
||||
|
||||
#### Scenario: signal fires after fetch but before unpack
|
||||
|
||||
- **WHEN** the loader has fetched bytes and the signal fires before
|
||||
`Unpacker.unpack` runs
|
||||
- **THEN** the call rejects with `PackerBrowserError(PACKER_BROWSER.ABORTED)`
|
||||
and no Blob URL is created
|
||||
|
||||
**Verified by:** `src/loader/loader.test.ts::"rejects with ABORTED when signal is pre-aborted before fetch"` AND `src/loader/loader.test.ts::"rejects with ABORTED when signal fires between fetch and unpack"` AND `src/manager/manager.test.ts::"propagates AbortSignal from manager.load through to the loader"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-8 — i18n MessageResolver keys, no hard-coded English
|
||||
|
||||
The package SHALL define a `PACKER_BROWSER_MESSAGES` catalog of
|
||||
`packer-browser.error.<CODE>.<scenario>` keys mapped to English defaults
|
||||
and a `defaultPackerBrowserResolver: MessageResolver` (from
|
||||
`@suaveplan/types`). Every thrown error message MUST be obtained from
|
||||
`resolvePackerBrowserMessage(resolver, key, params?)` — no hard-coded
|
||||
English string literals at `throw` sites.
|
||||
|
||||
#### Scenario: every key referenced from src has a catalog entry
|
||||
|
||||
- **WHEN** the test harness scans `src/**/*.ts` for
|
||||
`resolvePackerBrowserMessage(..., "<key>")` call sites
|
||||
- **THEN** every referenced key exists in `PACKER_BROWSER_MESSAGES`
|
||||
|
||||
#### Scenario: resolver interpolates `{name}` placeholders
|
||||
|
||||
- **WHEN** `defaultPackerBrowserResolver("packer-browser.error.RESOURCE_NOT_FOUND.http-status", { status: 404 })` is called
|
||||
- **THEN** the returned string contains `"404"`
|
||||
|
||||
**Verified by:** `src/i18n/messages.test.ts::"PACKER_BROWSER_MESSAGES covers every key referenced from src"` AND `src/i18n/messages.test.ts::"defaultPackerBrowserResolver interpolates named placeholders"` AND `src/i18n/messages.test.ts::"resolvePackerBrowserMessage falls back to the bare key on a missing entry"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-9 — Injected `Clock` / `Rng`, no direct `Date.now`/`Math.random`
|
||||
|
||||
The package SHALL accept `clock?: Clock` and `rng?: Rng` on every public
|
||||
option bundle that needs wall-clock or randomness, defaulting through
|
||||
`resolveRuntime(opts)` (mirrors `@suaveplan/packer/runtime`). Every
|
||||
production read of wall-clock time MUST go through the resolved `Clock`;
|
||||
no production file MAY call `Date.now()` or `Math.random()` directly.
|
||||
|
||||
#### Scenario: cache uses the injected Clock for entry timestamps
|
||||
|
||||
- **WHEN** `createPackedResourceManager({ clock: () => 1_700_000_000_000 })`
|
||||
is constructed and a resource is loaded
|
||||
- **THEN** the cached entry's `cachedAt` is `1_700_000_000_000`
|
||||
|
||||
#### Scenario: no production file calls Date.now / Math.random directly
|
||||
|
||||
- **WHEN** a recursive grep against `src/**/*.ts` (excluding `*.test.ts`,
|
||||
`*.types.ts`) is run for `Date\.now\|Math\.random`
|
||||
- **THEN** zero matches exist outside `src/runtime/runtime.ts`
|
||||
|
||||
**Verified by:** `src/runtime/runtime.test.ts::"resolveRuntime threads through caller-supplied Clock and Rng"` AND `src/cache/cache.test.ts::"entry timestamps come from the injected Clock"` AND `src/runtime/runtime.test.ts::"no production file calls Date.now or Math.random outside runtime defaults"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-10 — `@suaveplan/retry` on the IO source-pull path
|
||||
|
||||
The loader's `fetch` step SHALL route through `@suaveplan/retry`'s
|
||||
`withRetry`. The retry policy MUST be configurable per call via
|
||||
`PackedResourceLoaderOptions.retryConfig?: RetryConfig`. When `retryConfig`
|
||||
is absent the loader MUST default to a single-attempt policy
|
||||
(`maxAttempts: 1`) so wire behaviour matches the direct-fetch upstream
|
||||
contract. The retry loop MUST be `AbortSignal`-aware: cancellation aborts
|
||||
the loop immediately and surfaces as the retry package's
|
||||
`RetryAbortedError`.
|
||||
|
||||
#### Scenario: failing fetch is retried up to maxAttempts
|
||||
|
||||
- **WHEN** the loader is configured with
|
||||
`retryConfig: { maxAttempts: 3, delay: createConstantBackoff({ delay: 0 }) }`
|
||||
and the injected `fetch` rejects twice then resolves
|
||||
- **THEN** the loader returns successfully and the injected `fetch` was
|
||||
called three times
|
||||
|
||||
#### Scenario: default is a single attempt with no retries
|
||||
|
||||
- **WHEN** `retryConfig` is absent and `fetch` rejects once
|
||||
- **THEN** the rejection surfaces unchanged on the first attempt
|
||||
|
||||
**Verified by:** `src/loader/loader.test.ts::"retries fetch up to maxAttempts when retryConfig is supplied"` AND `src/loader/loader.test.ts::"defaults to a single fetch attempt when retryConfig is absent"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-11 — `@suaveplan/feature-flags` provider injected
|
||||
|
||||
The package SHALL accept an optional `flags?: FlagProvider` (type from
|
||||
`@suaveplan/feature-flags`) on `PackedResourceManagerOptions`. When
|
||||
absent, a no-op `MemoryProvider` MUST be used. The provider slot exists
|
||||
to gate Cluster B / C features; Cluster A registers no live flags but
|
||||
MUST expose the slot so adopters can configure it without an API change
|
||||
later.
|
||||
|
||||
#### Scenario: default uses a no-op MemoryProvider
|
||||
|
||||
- **WHEN** `createPackedResourceManager()` is constructed with no `flags`
|
||||
- **THEN** the manager's resolved flag provider is a `MemoryProvider`
|
||||
carrying no flag definitions
|
||||
|
||||
#### Scenario: caller-supplied provider is preserved
|
||||
|
||||
- **WHEN** `createPackedResourceManager({ flags: customProvider })` is
|
||||
constructed
|
||||
- **THEN** `manager.getFlagProvider()` returns the same `customProvider`
|
||||
reference
|
||||
|
||||
**Verified by:** `src/flags/flags.test.ts::"resolveFlagsProvider returns a MemoryProvider when no provider is supplied"` AND `src/flags/flags.test.ts::"resolveFlagsProvider returns the caller-supplied provider unchanged"` AND `src/manager/manager.test.ts::"manager.getFlagProvider returns the configured provider"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-12 — Subpath exports for the §3 minimum five
|
||||
|
||||
The package SHALL ship five subpath exports (plus `./package.json`):
|
||||
`.`, `./manager`, `./service-worker`, `./utils`, `./singleton`. Each
|
||||
subpath MUST be tree-shakeable (`sideEffects: false`) and MUST resolve to
|
||||
a dedicated entrypoint in `dist/`. The default `.` re-exports the public
|
||||
surface; the subpaths expose surgical imports per the audit's §3.10 ask.
|
||||
|
||||
#### Scenario: every subpath resolves to a built artefact
|
||||
|
||||
- **WHEN** the build runs (`vite build`)
|
||||
- **THEN** `dist/index.js`, `dist/singleton.js`, `dist/manager/index.js`,
|
||||
`dist/service-worker/index.js`, and `dist/utils/index.js` all exist
|
||||
|
||||
#### Scenario: package.json sideEffects is false
|
||||
|
||||
- **WHEN** `package.json` is inspected
|
||||
- **THEN** `sideEffects === false`
|
||||
|
||||
**Verified by:** `src/index.test.ts::"every subpath export resolves to a built dist artefact"` AND `src/index.test.ts::"package.json declares sideEffects: false"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
139
openspec/archive/2026-05-19-add-packer-browser-core/tasks.md
Normal file
139
openspec/archive/2026-05-19-add-packer-browser-core/tasks.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Tasks — add-packer-browser-core (Cluster A migration of `@suaveplan/packer-browser`)
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. No retroactive ticking.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/packer-browser.md` end-to-end
|
||||
- [x] 1.2 Read `openspec/specs/packer/spec.md` (sibling reference) and the
|
||||
archived `enterprise-packer-*` change deltas
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` §0a, §0c, §2b, §2c, §6a, §7, §13, §14
|
||||
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md` with
|
||||
`## Source mapping`, `## Out of scope`, `## Parity Checklist`, and
|
||||
`## Impact` populated
|
||||
- [x] 1.5 Fill in `## Source mapping` table — every requirement cites
|
||||
enterprise-audit line ranges OR appears in the "no source line" table
|
||||
- [x] 1.6 Draft `specs/packer-browser/spec.md` from
|
||||
`openspec/templates/spec-delta.md`
|
||||
- [x] 1.7 Every requirement in the spec delta carries a `**Verified by:**`
|
||||
line naming concrete `it(...)` titles
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Fix `scripts/folder-reorg/assignment.yml` — remove
|
||||
`- packer-browser` from `bitpacker:`, insert under `data:` between
|
||||
`packer` and `pagination`
|
||||
- [x] 2.2 Author `package.json` (workspace deps via `workspace:^`,
|
||||
externals via `catalog:` keys, no React, no bare versions)
|
||||
- [x] 2.3 Author `tsconfig.lib.json` (extends repo base, DOM + WebWorker
|
||||
libs)
|
||||
- [x] 2.4 Author `vite.config.ts` (multi-entry build for the 5 subpaths)
|
||||
- [x] 2.5 Author `vitest.config.ts` (via `createTestConfig` with
|
||||
`happy-dom`, `fileParallelism: false`)
|
||||
- [x] 2.6 Author `playwright.config.ts` (Chromium project, no parallelism)
|
||||
- [x] 2.7 `src/error-codes.ts` — `PACKER_BROWSER_CODES` (6 stable codes) +
|
||||
`PACKER_BROWSER_ERROR_METADATA` + `PackerBrowserError extends SuaveplanError`
|
||||
- [x] 2.8 `src/i18n/messages.ts` + `messages.types.ts` — `PACKER_BROWSER_MESSAGES`
|
||||
catalog + `defaultPackerBrowserResolver` + `resolvePackerBrowserMessage`
|
||||
- [x] 2.9 `src/runtime/runtime.ts` + `runtime.types.ts` — `defaultClock`,
|
||||
`defaultRng`, `resolveRuntime`, `elapsedMs`
|
||||
- [x] 2.10 `src/observability/observability.ts` + `observability.types.ts`
|
||||
— `createPackerBrowserObservability` exposing 3 counters + 1 histogram
|
||||
- [x] 2.11 `src/flags/flags.ts` — `resolveFlagsProvider` (`MemoryProvider`
|
||||
fallback)
|
||||
- [x] 2.12 `src/signature/signature.ts` — `verifyArchiveSignature` wrapping
|
||||
`ed25519.verify`
|
||||
- [x] 2.13 `src/cache/cache.ts` — `PackedResourceCache` (LRU + ref-count +
|
||||
Blob-URL bookkeeping, OTel-instrumented, Clock-driven timestamps)
|
||||
- [x] 2.14 `src/loader/loader.ts` — `PackedResourceLoader` (fetch via
|
||||
`withRetry`, signature verification, `AbortSignal` at every yield,
|
||||
`Unpacker.unpack` integration, histogram observation)
|
||||
- [x] 2.15 `src/manager/manager.ts` — `PackedResourceManager` lifecycle
|
||||
façade + `createPackedResourceManager` factory + event emitter; each
|
||||
file ≤ 300 LOC
|
||||
- [x] 2.16 `src/service-worker/sw-shim.ts` — hand-rolled
|
||||
`createServiceWorkerShim()` implementing `ServiceWorkerGlobalScope`,
|
||||
`FetchEvent`, `ExtendableEvent`, `Cache`, `CacheStorage`, `Clients`
|
||||
- [x] 2.17 `src/service-worker/service-worker.ts` —
|
||||
`runPackedResourceServiceWorker(scope)` registering install / activate /
|
||||
fetch / message handlers
|
||||
- [x] 2.18 `src/utils/register-service-worker.ts` — register / unregister /
|
||||
clearCache / getCacheStatus helpers
|
||||
- [x] 2.19 `src/singleton.ts` — module-singleton hosted at `/singleton`
|
||||
subpath
|
||||
- [x] 2.20 `src/index.ts` — explicit named re-exports (no `export *`);
|
||||
default surface excludes `packedResourceManager`
|
||||
- [x] 2.21 `README.md` — install / quick-start / modules
|
||||
|
||||
## 3. Tests (100% coverage, runner-neutral imports)
|
||||
|
||||
- [x] 3.1 `src/error-codes.test.ts` covering PACKER-BROWSER-6
|
||||
- [x] 3.2 `src/i18n/messages.test.ts` covering PACKER-BROWSER-8
|
||||
- [x] 3.3 `src/runtime/runtime.test.ts` covering PACKER-BROWSER-9
|
||||
- [x] 3.4 `src/observability/observability.test.ts` covering PACKER-BROWSER-4
|
||||
- [x] 3.5 `src/flags/flags.test.ts` covering PACKER-BROWSER-11
|
||||
- [x] 3.6 `src/signature/signature.test.ts` covering PACKER-BROWSER-3
|
||||
- [x] 3.7 `src/cache/cache.test.ts` covering PACKER-BROWSER-1, -4, -9
|
||||
- [x] 3.8 `src/loader/loader.test.ts` covering PACKER-BROWSER-1, -3, -4,
|
||||
-6, -7, -10
|
||||
- [x] 3.9 `src/manager/manager.test.ts` covering PACKER-BROWSER-1, -5, -7,
|
||||
-11
|
||||
- [x] 3.10 `src/singleton.test.ts` covering PACKER-BROWSER-5
|
||||
- [x] 3.11 `src/service-worker/sw-shim.test.ts` covering PACKER-BROWSER-2
|
||||
- [x] 3.12 `src/service-worker/service-worker.test.ts` covering
|
||||
PACKER-BROWSER-2
|
||||
- [x] 3.13 `src/utils/register-service-worker.test.ts` covering
|
||||
PACKER-BROWSER-2
|
||||
- [x] 3.14 `src/index.test.ts` covering PACKER-BROWSER-12
|
||||
- [x] 3.15 Every `it(...)` carries `{ timeout: 5000 }`; async-IO suites
|
||||
use `{ timeout: 30_000 }`
|
||||
- [x] 3.16 Every test file imports from `@suaveplan/testing/runner` only
|
||||
(no direct `vitest` / `bun:test` imports)
|
||||
- [x] 3.17 `tests/e2e/sw.spec.ts` Playwright happy-path
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts` (≥200 words,
|
||||
Purpose / Features / Basic / Advanced / API Reference /
|
||||
Implementation Notes)
|
||||
- [x] 4.2 `README.md` links every co-located `.md` under `## Modules`
|
||||
- [x] 4.3 Update root `CLAUDE.md` package listing to add the new package
|
||||
- [x] 4.4 Update `STATUS.md` — flip the `packer-browser` row to ✅ and
|
||||
move it into the `data` section table
|
||||
- [x] 4.5 Refresh `MANIFEST.md` via
|
||||
`bun scripts/folder-reorg/generate-manifest.ts`
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/packer-browser`
|
||||
— green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/packer-browser` —
|
||||
100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/packer-browser`
|
||||
— exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/packer-browser` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [x] 6.1 Generics threaded; `expectTypeOf` assertions on every literal-type
|
||||
claim that affects the public surface
|
||||
- [x] 6.2 Zod schemas wired at every declared boundary (option-bundle
|
||||
validation deferred to Cluster B per audit; flagged in proposal)
|
||||
- [x] 6.3 Every error code in `error-codes.ts` thrown by at least one
|
||||
production path
|
||||
- [x] 6.4 Strict-mode behaviors tested both on and off (signature verify
|
||||
on / off; retry on / off; cache on / off)
|
||||
- [x] 6.5 OTel instruments emitted from production paths and asserted with
|
||||
a real in-memory `MeterProvider`
|
||||
- [x] 6.6 Lifecycle dedup tested where claimed (factory returns fresh
|
||||
instances; singleton returns stable reference)
|
||||
- [x] 6.7 No retroactive tick — every box above reflects work done in this
|
||||
change
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/packer-browser/spec.md`
|
||||
- [x] 7.2 Move `openspec/changes/add-packer-browser-core/` →
|
||||
`openspec/archive/<yyyy-mm-dd>-add-packer-browser-core/`
|
||||
- [x] 7.3 Refresh `MANIFEST.md` after the move
|
||||
Reference in New Issue
Block a user