Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
# Design — `@suaveplan/react-packer`
## Architecture
`@suaveplan/react-packer` is a React 19 companion that wraps a
`PackedResourceManager` from `@suaveplan/packer-browser` in a small set of
hooks plus a React context.
```text
Consumer app
│
▼
<PackedResourceProvider value={manager} resolver={resolver}>
│
▼
usePackedResource(packedUrl) ─┐
usePackedImage(packedUrl) │
usePackedAudio(packedUrl) ├─► manager.load(url, opts)
usePackedVideo(packedUrl) │ │
usePackedFont(packedUrl, family) ─┘ ▼
PackedResourceLoader (fetch + verify + unpack)
PackedResourceCache (LRU + Blob URLs)
```
The provider is optional. If no provider is mounted in the tree, the
hooks resolve the singleton via dynamic import of
`@suaveplan/packer-browser/singleton`. This keeps the default zero-config
import cost zero — singleton construction only runs in trees that don't
inject a manager. Adopters that care about tree-shaking, multiple
managers, or explicit lifecycle (test rigs, SSR) mount a provider and
never touch the singleton.
## Public surface
```ts
// hooks/use-packed-resource
export interface UsePackedResourceOptions {
cache?: boolean;
verifyChecksum?: boolean;
verifyKey?: Uint8Array;
signal?: AbortSignal;
allowedMimePrefixes?: ReadonlyArray<string>;
}
export interface UsePackedResourceReturn {
url: string | null;
blob: Blob | null;
loading: boolean;
error: ReactPackerError | null;
status: "idle" | "loading" | "success" | "error";
reload: () => void;
}
export function usePackedResource(
packedUrl: string | null | undefined,
options?: UsePackedResourceOptions
): UsePackedResourceReturn;
// hooks/use-packed-image — default { cache: true, allowedMimePrefixes: ["image/"] }
export function usePackedImage(
packedUrl: string | null | undefined,
options?: UsePackedResourceOptions
): UsePackedResourceReturn;
// hooks/use-packed-audio — default { cache: true, allowedMimePrefixes: ["audio/"] }
// hooks/use-packed-video — default { cache: false, allowedMimePrefixes: ["video/"] }
// hooks/use-packed-font — default { cache: true, allowedMimePrefixes: ["font/", "application/font", "application/octet-stream"] }
export interface UsePackedFontReturn extends UsePackedResourceReturn {
fontLoaded: boolean;
}
export function usePackedFont(
packedUrl: string | null | undefined,
fontFamily?: string,
options?: UsePackedResourceOptions
): UsePackedFontReturn;
// provider
export interface PackedResourceProviderValue {
manager: PackedResourceManager;
messageResolver?: MessageResolver;
}
export function PackedResourceProvider(props: {
value: PackedResourceProviderValue;
children?: ReactNode;
}): ReactElement;
export function usePackedResourceManager(): PackedResourceManager;
```
## Error model
A single error code registry is registered with `@suaveplan/error`:
| Code | Raised when |
|---|---|
| `REACT_PACKER_LOAD_FAILED` | `manager.load(...)` threw and the hook is reporting it. The original error is preserved on `.cause`. |
| `REACT_PACKER_FONT_LOAD_FAILED` | `document.fonts.load(...)` rejected. Hook surfaces `fontLoaded = false` and the wrapped error on `result.error`. |
| `REACT_PACKER_ABORTED` | A pre-aborted signal was passed to a load; rejected before fetching. |
`ReactPackerError extends SuaveplanError` and carries a brand symbol;
`ReactPackerError.is(value)` discriminates safely across realms.
## File layout
```text
src/
index.ts # barrel — explicit named re-exports
error-codes/
error-codes.ts
error-codes.md
error-codes.test.ts
index.ts
types/
react-packer.types.ts
react-packer.types.md
index.ts
provider/
packed-resource-provider/
packed-resource-provider.tsx
packed-resource-provider.md
packed-resource-provider.test.tsx
packed-resource-provider.stories.tsx
index.ts
hooks/
use-packed-resource/
use-packed-resource.ts
use-packed-resource.md
use-packed-resource.test.tsx
use-packed-resource.stories.tsx
index.ts
use-packed-image/
use-packed-image.ts
use-packed-image.md
use-packed-image.test.tsx
use-packed-image.stories.tsx
index.ts
use-packed-audio/
use-packed-audio.ts
use-packed-audio.md
use-packed-audio.test.tsx
use-packed-audio.stories.tsx
index.ts
use-packed-video/
use-packed-video.ts
use-packed-video.md
use-packed-video.test.tsx
use-packed-video.stories.tsx
index.ts
use-packed-font/
use-packed-font.ts
use-packed-font.md
use-packed-font.test.tsx
use-packed-font.stories.tsx
index.ts
```
## Lifecycle semantics
1. Mount with `packedUrl !== null` → `status = "loading"`, hook calls
`manager.load(url, opts)` with an internal `AbortController` whose
signal merges with any externally-passed `opts.signal`.
2. Success → `status = "success"`, `url` + `blob` populated, manager
`release` is registered as cleanup so unmount drops the cache ref.
3. Failure → `status = "error"`, `error` is a `ReactPackerError` wrapping
the underlying error on `.cause`.
4. Re-render with different `packedUrl` → previous controller aborts,
previous URL is released, new load starts.
5. Unmount → controller aborts, the URL is released, no `setState` runs
after unmount (mounted-ref guard).
6. `reload()` increments an internal counter that re-runs the effect.
## Testing strategy
- Real `PackedResourceManager` constructed per-test via
`createPackedResourceManager({ fetch })`.
- `fetch` is a tiny closure returning `new Response(Uint8Array, { headers })`.
- No `vi.mock`/`vi.spyOn` against `@suaveplan/packer-browser` — the
manager runs its real loader + cache pipeline against the injected
`fetch`. This is exactly the pattern used by
`packer-browser/src/loader/loader.test.ts` and is what CLAUDE.md
rule 2 mandates.
- Coverage runs via `provider: "istanbul"` with 100/100/100/100
thresholds set in `vitest.config.ts`.

View File

@@ -0,0 +1,128 @@
# Add `@suaveplan/react-packer` — React 19 companion for `@suaveplan/packer-browser`
**Package**: `@suaveplan/react-packer`
**Status**: PROPOSED
**Type**: new-package
- **Design**: ./design.md
- **Spec delta**: ./specs/react-packer/spec.md
## Why
`@suaveplan/packer-browser` (genesis Cluster A) ships the manager, cache,
loader, signature verification, and Service-Worker interceptor with **no
React runtime dep**. Upstream `packer-browser@1.0.17` carried the React
hooks (`usePackedResource`, `usePackedImage`, `usePackedAudio`,
`usePackedVideo`, `usePackedFont`) inside the same package, forcing every
non-React consumer (server bundles, R3F apps that do their own loading,
Web-Worker scripts) to ship React via transitive dependency. Upstream
`enterprise/packer-browser.md` §3.3 specifically calls for splitting the
hooks into a React companion package; that split has been deferred to
`@suaveplan/react-packer`, this change.
This package provides the hooks at genesis quality — `AbortSignal` on
every action, `MessageResolver` plumbed through, `PackedResourceProvider`
React context for explicit dependency injection of a
`PackedResourceManager`, sensible fallback to the
`@suaveplan/packer-browser/singleton` for adopters that want zero-config,
and 100/100/100/100 coverage against a real (non-mock) manager backed by
real `Response` + `Blob` fixtures.
## What changes
This is a brand-new package — every export is additive. No changes to
`@suaveplan/packer-browser`'s surface.
- ADDED: `usePackedResource(packedUrl, opts?)` — generic hook returning
`{ url, blob, loading, error, status, reload }`.
- ADDED: `usePackedImage(packedUrl, opts?)` — image-tuned defaults (cache
on, `allowedMimePrefixes = ["image/"]`).
- ADDED: `usePackedAudio(packedUrl, opts?)` — audio-tuned defaults.
- ADDED: `usePackedVideo(packedUrl, opts?)` — video-tuned defaults (cache
off — videos are large).
- ADDED: `usePackedFont(packedUrl, fontFamily?, opts?)` — injects a
`@font-face` rule and waits for `document.fonts.load`. Returns the
generic shape plus `fontLoaded: boolean`.
- ADDED: `PackedResourceProvider` — React context that supplies a
`PackedResourceManager` and (optionally) a `MessageResolver` to every
hook in its subtree.
- ADDED: `usePackedResourceManager()` — internal hook that resolves the
manager from context with a documented fallback to
`@suaveplan/packer-browser/singleton`.
- ADDED: `REACT_PACKER_CODES` + `ReactPackerError` — stable error code
registry (`LOAD_FAILED`, `FONT_LOAD_FAILED`, `ABORTED`).
## Out of scope
- Service-Worker registration UI — adopters use
`@suaveplan/packer-browser`'s lifecycle helpers directly.
- A `<PackedImage>` / `<PackedVideo>` / `<PackedAudio>` React component
wrapper. The hooks are the public surface; consumers compose their own
presentational components. This keeps the package focused on the carve
upstream `enterprise/packer-browser.md` §3.3 explicitly asked for.
## Source mapping
This change derives from upstream
`../suaveplan/packages/browser/packer-browser/src/hooks/usePackedResource.ts`
and `../suaveplan/enterprise/packer-browser.md` §3.3 ("Move React hooks
into a new React companion package"). The genesis rewrite preserves the
upstream public symbol names and per-variant defaults while bringing
every requirement to enterprise quality (no module singleton, real
`AbortSignal`, real `MessageResolver`, real error hierarchy).
| Requirement | Source lines | Notes |
|---|---|---|
| REACT-PACKER-1 (`usePackedResource`) | upstream `usePackedResource.ts` L22-L87 | Rewritten to consume injected manager; adds AbortSignal + error-code wrapping. |
| REACT-PACKER-2 (`usePackedImage`) | upstream L89-L101 | Same defaults (cache on); adds `allowedMimePrefixes`. |
| REACT-PACKER-3 (`usePackedAudio`) | upstream L103-L115 | Same defaults (cache on). |
| REACT-PACKER-4 (`usePackedVideo`) | upstream L117-L129 | Same defaults (cache off). |
| REACT-PACKER-5 (`usePackedFont`) | upstream L131-L198 | Preserves `@font-face` injection + cleanup. |
| REACT-PACKER-6 (`PackedResourceProvider`) | enterprise/packer-browser.md L42-L51 ("Move React hooks…") | Adds explicit DI surface that upstream did not have (module singleton only). |
| REACT-PACKER-7 (error hierarchy) | enterprise/packer-browser.md §63 | Standard genesis bar; upstream raised plain `Error`. |
| REACT-PACKER-8 (singleton fallback) | enterprise/packer-browser.md L51 | Adopters who omit `<PackedResourceProvider>` fall back to `@suaveplan/packer-browser/singleton`. |
### Requirements with no source line
None — every requirement either ports an upstream behaviour or implements
an enterprise gap explicitly named in `enterprise/packer-browser.md`.
## Verification approach
Every requirement carries a `**Verified by:**` line pointing at a concrete
`it(...)` title in the planned test files.
- `src/hooks/use-packed-resource/use-packed-resource.test.tsx` — covers
REACT-PACKER-1 (loading, success, error, reload, abort on unmount,
abort on URL change, signal propagation).
- `src/hooks/use-packed-image/use-packed-image.test.tsx` — covers
REACT-PACKER-2 (`allowedMimePrefixes` enforcement, image defaults).
- `src/hooks/use-packed-audio/use-packed-audio.test.tsx` — covers
REACT-PACKER-3 (audio defaults).
- `src/hooks/use-packed-video/use-packed-video.test.tsx` — covers
REACT-PACKER-4 (video defaults including cache off).
- `src/hooks/use-packed-font/use-packed-font.test.tsx` — covers
REACT-PACKER-5 (`@font-face` injection, cleanup, `fontLoaded` flag,
`document.fonts.load` failure path).
- `src/provider/packed-resource-provider/packed-resource-provider.test.tsx`
— covers REACT-PACKER-6 + REACT-PACKER-8 (context wiring, singleton
fallback when no provider, `MessageResolver` propagation).
- `src/error-codes/error-codes.test.ts` — covers REACT-PACKER-7 (code
registry, `ReactPackerError.is(...)` brand check).
## Migration impact
None — additive only. `@suaveplan/packer-browser` is not modified.
Consumers that previously imported hooks from upstream `packer-browser`
(none on genesis — packer-browser shipped without hooks per its own
proposal) migrate by adding `@suaveplan/react-packer` as a dep and
importing from there.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/react-packer` to enumerate.
This is a new package with zero existing dependents on genesis:
- No dependents (verified by `grep -rln "@suaveplan/react-packer" packages/ apps/ 2>/dev/null` returning no matches at proposal time).

View File

@@ -0,0 +1,159 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: react-packer (add-react-packer)
**Package**: `@suaveplan/react-packer`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: REACT-PACKER-1 — `usePackedResource` lifecycle
The package SHALL export `usePackedResource(packedUrl, options?)`, a React hook that resolves a `PackedResourceManager` from context (or the singleton fallback), invokes `manager.load(url, opts)`, and returns `{ url, loading, error, status, reload }`. The hook MUST honour `AbortSignal`: it merges any caller-supplied signal with its own internal `AbortController`, aborts on unmount, aborts on `packedUrl` change, and aborts on `reload()`. On a `null`/`undefined` `packedUrl` the hook MUST settle on `status = "idle"` without invoking the manager.
#### Scenario: nullable packed URL produces idle state
- **WHEN** the hook is mounted with `packedUrl = null`
- **THEN** `status === "idle"`, `url === null`, `loading === false`, and `manager.load` is never called
#### Scenario: success transitions loading → success
- **WHEN** the manager resolves a real Blob URL for the given packed URL
- **THEN** the hook transitions `loading` (true) → `success`, exposes the Blob `url`, and clears any prior `error`
#### Scenario: pre-aborted signal short-circuits with REACT_PACKER_ABORTED
- **WHEN** the caller passes a pre-aborted `AbortSignal`
- **THEN** the hook surfaces `status = "error"` with `error.code === "REACT_PACKER_ABORTED"` and never calls the manager
#### Scenario: unmount releases the cached resource
- **WHEN** a successful load completes and the component unmounts
- **THEN** `manager.release(url, opts.verifyKey)` runs exactly once and no `setState` warning fires
**Verified by:** `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"is idle when packedUrl is null"` AND `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"transitions loading then success on a real manager"` AND `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"surfaces REACT_PACKER_ABORTED on a pre-aborted signal"` AND `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"releases the cached entry on unmount"` AND `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"reload() re-runs the loader and resets state"` AND `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"changing packedUrl aborts the prior in-flight load"` AND `src/hooks/use-packed-resource/use-packed-resource.test.tsx::"wraps a manager failure as REACT_PACKER_LOAD_FAILED"`
---
### Requirement: REACT-PACKER-2 — `usePackedImage` with image-tuned defaults
The package SHALL export `usePackedImage(packedUrl, options?)` which delegates to `usePackedResource` with `cache: true` and `allowedMimePrefixes: ["image/"]` unless the caller overrides them. The hook MUST preserve the full `UsePackedResourceReturn` shape.
#### Scenario: image hook applies image MIME allow-list
- **WHEN** the hook is mounted and the underlying manager call inspects options
- **THEN** the call includes `allowedMimePrefixes: ["image/"]` and `cache: true`
#### Scenario: caller override wins over defaults
- **WHEN** the caller passes `{ cache: false }`
- **THEN** the manager call receives `cache: false`
**Verified by:** `src/hooks/use-packed-image/use-packed-image.test.tsx::"loads an image-tuned packed resource"` AND `src/hooks/use-packed-image/use-packed-image.test.tsx::"caller can override the cache default"`
---
### Requirement: REACT-PACKER-3 — `usePackedAudio` with audio-tuned defaults
The package SHALL export `usePackedAudio(packedUrl, options?)` which delegates to `usePackedResource` with `cache: true` and `allowedMimePrefixes: ["audio/"]` unless overridden.
#### Scenario: audio hook applies audio MIME allow-list
- **WHEN** the hook resolves a packed URL
- **THEN** the manager receives `allowedMimePrefixes: ["audio/"]` and `cache: true`
**Verified by:** `src/hooks/use-packed-audio/use-packed-audio.test.tsx::"loads an audio-tuned packed resource"` AND `src/hooks/use-packed-audio/use-packed-audio.test.tsx::"forwards caller-supplied verifyKey to the manager"`
---
### Requirement: REACT-PACKER-4 — `usePackedVideo` with video-tuned defaults
The package SHALL export `usePackedVideo(packedUrl, options?)` which delegates to `usePackedResource` with `cache: false` (videos are large and dominate cache budgets) and `allowedMimePrefixes: ["video/"]` unless overridden.
#### Scenario: video hook opts out of caching by default
- **WHEN** the hook resolves a packed URL with no caller options
- **THEN** the manager receives `cache: false` and `allowedMimePrefixes: ["video/"]`
**Verified by:** `src/hooks/use-packed-video/use-packed-video.test.tsx::"loads a video-tuned packed resource without caching"` AND `src/hooks/use-packed-video/use-packed-video.test.tsx::"caller can opt back into caching"`
---
### Requirement: REACT-PACKER-5 — `usePackedFont` injects @font-face
The package SHALL export `usePackedFont(packedUrl, fontFamily?, options?)` which delegates to `usePackedResource` with font-tuned defaults, injects a `@font-face` stylesheet (`<style id="packed-font-<family>">`) into `document.head`, waits for `document.fonts.load("1em <family>")`, and exposes `fontLoaded: boolean` alongside the standard return. Unmount MUST remove the injected `<style>` element. When `document.fonts.load` rejects, the hook MUST surface `error.code === "REACT_PACKER_FONT_LOAD_FAILED"` and leave `fontLoaded = false`.
#### Scenario: successful font load injects @font-face and flips fontLoaded
- **WHEN** the manager resolves the font Blob URL and `document.fonts.load` resolves
- **THEN** a `<style id="packed-font-<family>">` element exists in `document.head`, the rule's `src` URL matches the loaded Blob URL, and `fontLoaded === true`
#### Scenario: font-family omitted skips injection but still loads the bytes
- **WHEN** the hook is mounted with `fontFamily = undefined`
- **THEN** no `<style>` element is injected and `fontLoaded === false`, but the underlying Blob `url` is populated
#### Scenario: document.fonts.load rejection surfaces FONT_LOAD_FAILED
- **WHEN** `document.fonts.load` rejects with an `Error`
- **THEN** `error.code === "REACT_PACKER_FONT_LOAD_FAILED"` and `fontLoaded === false`
#### Scenario: unmount removes the injected style element
- **WHEN** a font hook unmounts after a successful injection
- **THEN** `document.getElementById("packed-font-<family>")` returns `null`
**Verified by:** `src/hooks/use-packed-font/use-packed-font.test.tsx::"injects @font-face on successful load"` AND `src/hooks/use-packed-font/use-packed-font.test.tsx::"omitting fontFamily skips style injection"` AND `src/hooks/use-packed-font/use-packed-font.test.tsx::"wraps fonts.load rejection as REACT_PACKER_FONT_LOAD_FAILED"` AND `src/hooks/use-packed-font/use-packed-font.test.tsx::"removes the injected style element on unmount"`
---
### Requirement: REACT-PACKER-6 — `PackedResourceProvider` context
The package SHALL export `PackedResourceProvider`, a React context provider that supplies a `PackedResourceManager` and optional `MessageResolver` to every hook in its subtree. Hooks MUST resolve their manager via `useContext(PackedResourceContext)` first and consult the optional resolver when constructing `ReactPackerError` instances.
#### Scenario: hook uses the provider's manager
- **WHEN** a hook is rendered inside `<PackedResourceProvider value={{ manager }}>`
- **THEN** that hook calls `manager.load(...)` rather than the singleton
#### Scenario: provider supplies a MessageResolver
- **WHEN** the provider's value supplies a custom `messageResolver` and a load fails
- **THEN** the resulting `ReactPackerError.messageKey` is the key the resolver returned
**Verified by:** `src/provider/packed-resource-provider/packed-resource-provider.test.tsx::"hook consumes the provider-supplied manager"` AND `src/provider/packed-resource-provider/packed-resource-provider.test.tsx::"messageResolver is threaded into raised errors"`
---
### Requirement: REACT-PACKER-7 — Stable error hierarchy
The package SHALL export `REACT_PACKER_CODES` (a `defineCodes(...)` registry) and `ReactPackerError extends SuaveplanError`. The registry MUST include `LOAD_FAILED`, `FONT_LOAD_FAILED`, and `ABORTED`. `ReactPackerError.is(value)` MUST return `true` only for instances carrying the brand symbol, even across realms.
#### Scenario: every registered code is reachable from a production path
- **WHEN** the consumer triggers a load failure, font load rejection, or pre-aborted signal
- **THEN** each respective `REACT_PACKER_*` code is raised by exactly one hook code path and surfaces on `result.error.code`
#### Scenario: `is(value)` is a safe brand check
- **WHEN** an arbitrary `unknown` value is passed to `ReactPackerError.is`
- **THEN** the result is `true` iff the value is a real `ReactPackerError` instance with the brand symbol set
**Verified by:** `src/error-codes/error-codes.test.ts::"registers LOAD_FAILED, FONT_LOAD_FAILED, ABORTED"` AND `src/error-codes/error-codes.test.ts::"is(value) returns true only for branded instances"` AND `src/error-codes/error-codes.test.ts::"preserves cause through the SuaveplanError chain"`
---
### Requirement: REACT-PACKER-8 — Singleton fallback when no provider is mounted
The package SHALL resolve a working `PackedResourceManager` even when no `<PackedResourceProvider>` is mounted. In that case it MUST defer to `@suaveplan/packer-browser/singleton` so adopters who do not need DI get a zero-config experience.
#### Scenario: hook outside a provider uses the singleton
- **WHEN** a hook is rendered with no surrounding provider
- **THEN** `usePackedResourceManager()` returns the module-singleton instance from `@suaveplan/packer-browser/singleton`
**Verified by:** `src/provider/packed-resource-provider/packed-resource-provider.test.tsx::"falls back to the singleton when no provider is mounted"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,75 @@
# Tasks — Add `@suaveplan/react-packer`
> Every checkbox MUST reflect work actually done in this change.
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/packages/browser/packer-browser/src/hooks/usePackedResource.ts`
- [x] 1.2 Read `../suaveplan/enterprise/packer-browser.md` §3.3
- [x] 1.3 Read `openspec/AGENTS.md` §§0c, 2b, 6a, 7, 13, 14
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [x] 1.5 Source-mapping table filled in (every requirement cites upstream lines OR an enterprise-md gap)
- [x] 1.6 Draft `specs/react-packer/spec.md` from `openspec/templates/spec-delta.md`
- [x] 1.7 Every requirement carries a `**Verified by:**` line with concrete planned `it(...)` titles
## 2. Implementation
- [x] 2.1 Scaffold `packages/data/react-packer/` (package.json, tsconfig, vite, vitest, playwright)
- [x] 2.2 Add `react-packer` to `scripts/folder-reorg/assignment.yml` (data block, alphabetical between `pagination` and `serialize`)
- [x] 2.3 `src/error-codes/error-codes.ts` — `REACT_PACKER_CODES` registry + `ReactPackerError`
- [x] 2.4 `src/types/react-packer.types.ts` — `UsePackedResourceOptions`, `UsePackedResourceReturn`, `UsePackedFontReturn`, `PackedResourceProviderValue`
- [x] 2.5 `src/provider/packed-resource-provider/packed-resource-provider.tsx` — context + `usePackedResourceManager`
- [x] 2.6 `src/hooks/use-packed-resource/use-packed-resource.ts` — generic hook with AbortController, mounted-ref, reload counter
- [x] 2.7 `src/hooks/use-packed-image/use-packed-image.ts`
- [x] 2.8 `src/hooks/use-packed-audio/use-packed-audio.ts`
- [x] 2.9 `src/hooks/use-packed-video/use-packed-video.ts`
- [x] 2.10 `src/hooks/use-packed-font/use-packed-font.ts` — adds `@font-face` injection + `fontLoaded`
- [x] 2.11 `src/index.ts` barrel — explicit named re-exports only
- [x] 2.12 `src/<feature>/index.ts` feature barrels — explicit named re-exports (§6a)
## 3. Tests
- [x] 3.1 `src/error-codes/error-codes.test.ts`
- [x] 3.2 `src/provider/packed-resource-provider/packed-resource-provider.test.tsx`
- [x] 3.3 `src/hooks/use-packed-resource/use-packed-resource.test.tsx`
- [x] 3.4 `src/hooks/use-packed-image/use-packed-image.test.tsx`
- [x] 3.5 `src/hooks/use-packed-audio/use-packed-audio.test.tsx`
- [x] 3.6 `src/hooks/use-packed-video/use-packed-video.test.tsx`
- [x] 3.7 `src/hooks/use-packed-font/use-packed-font.test.tsx`
- [x] 3.8 Every `it(...)` carries `{ timeout: 5000 }` (30000 where genuinely async)
- [x] 3.9 Test files import from `@suaveplan/testing/runner` only (rule 11 / §7)
- [x] 3.10 No mocks — real `PackedResourceManager` + real `Response`+`Blob` fixtures
## 4. Documentation
- [x] 4.1 Sibling `.md` per implementation file (≥200 words, basic + advanced + API + impl notes)
- [x] 4.2 Stories `.stories.tsx` per hook + provider
- [x] 4.3 `README.md` at package root linking every co-located `.md`
- [x] 4.4 Update `CLAUDE.md` package listing under data block alongside `packer-browser`
- [x] 4.5 Update `STATUS.md` with new row
## 5. Pre-archive verification (the 5-gate triple)
- [ ] 5.1 `bun run check` — Biome 0 errors / 0 warnings
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/react-packer` — green
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/react-packer` — 100/100/100/100
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/react-packer` — exit 0
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/react-packer` — green
## 6. Pre-archive 9-row checklist (AGENTS.md §13)
- [ ] 6.1 Generics threaded where appropriate; no inline shapes (rule 11)
- [ ] 6.2 No Zod boundaries declared (no untrusted inputs cross the hook surface — manager handles its own boundaries)
- [ ] 6.3 Every error code in `REACT_PACKER_CODES` thrown by at least one production path
- [ ] 6.4 `AbortSignal` propagation tested (pre-aborted + abort-on-unmount + abort-on-url-change)
- [ ] 6.5 OTel traces pass through unchanged (the manager owns them; hooks do not add counters per scope)
- [ ] 6.6 Lifecycle dedup tested — mounted-ref guard prevents `setState` after unmount
- [ ] 6.7 Singleton fallback resolves only when no provider is mounted
- [ ] 6.8 No retroactive tick — every box above reflects work in this change
- [ ] 6.9 Consumer sweep clean (no existing genesis consumers of `@suaveplan/react-packer`)
## 7. Archive
- [ ] 7.1 Fold spec delta into `openspec/specs/react-packer/spec.md` (NEW capability)
- [ ] 7.2 Move `openspec/changes/add-react-packer/` → `openspec/archive/<yyyy-mm-dd>-add-react-packer/`
- [ ] 7.3 Refresh `MANIFEST.md` via `bun scripts/folder-reorg/generate-manifest.ts`