Initial Commit
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# enhance-packer-browser-platform — Cluster C §4 platform enhancements
|
||||
|
||||
**Package**: `@suaveplan/packer-browser`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature
|
||||
**Spec delta**: ./specs/packer-browser/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
Cluster A (`2026-05-19-add-packer-browser-core`) shipped the production
|
||||
core (PACKER-BROWSER-1..12). Cluster B
|
||||
(`2026-05-19-enhance-packer-browser-streaming`) shipped the four streaming
|
||||
items (PACKER-BROWSER-13..16). The upstream audit
|
||||
(`../suaveplan/enterprise/packer-browser.md` §4) enumerates ten feature-rich
|
||||
enhancements; six remain after Clusters A + B: adaptive cache sizing,
|
||||
preload-hint generation, multi-origin SW helper, BackgroundFetch
|
||||
integration, diagnostics dev overlay, and the final subpath split + full
|
||||
Playwright E2E sweep. Cluster C closes all six so the package reaches
|
||||
parity with the upstream §4 list and the audit's §6 acceptance criteria.
|
||||
|
||||
## What changes
|
||||
|
||||
Pure additions. Cluster A/B public surface (PACKER-BROWSER-1..16) remains
|
||||
intact. Cluster C adds new exports, three new subpath entrypoints
|
||||
(`/diagnostics`, `/prefetch`, `/preload`), one new error code, and two new
|
||||
OTel counters; no removals.
|
||||
|
||||
- ADDED: `createAdaptiveCacheSizer(opts)` — recurring `navigator.storage.estimate()`
|
||||
poller that mutates the manager's cache ceiling. AbortSignal-aware, Clock-injected.
|
||||
- ADDED: `PackedResourceManagerOptions.adaptiveCache` — `{ enabled?, fraction?, intervalMs?, signal? }`.
|
||||
- ADDED: `PackedResourceCache.setMaxCacheSize(n)` — runtime ceiling mutator used by the adaptive sizer.
|
||||
- ADDED: `generatePreloadHints(manifest, opts)` and `injectPreloadHints(doc, hints)` at the new `/preload` subpath.
|
||||
- ADDED: `registerCrossOriginPackedResourceServiceWorker(opts)` at the existing `/utils` subpath; rejects with `PACKER_BROWSER.UNAUTHORIZED_ORIGIN` on mismatch.
|
||||
- ADDED: `PACKER_BROWSER_CODES.UNAUTHORIZED_ORIGIN` — new error code (8th total).
|
||||
- ADDED: `prefetchPackedArchive(archiveUrl, opts)` at the new `/prefetch` subpath using `BackgroundFetchManager`.
|
||||
- ADDED: `handleBackgroundFetchSuccess(event, opts)` SW helper.
|
||||
- ADDED: `PackerBrowserCounters.prefetchSuccessTotal` + `prefetchFailureTotal` — two new OTel counters.
|
||||
- ADDED: `createDiagnosticsOverlay(manager, opts)` at the new `/diagnostics` subpath.
|
||||
- ADDED: Subpath exports `./preload`, `./prefetch`, `./diagnostics` (total subpaths now 8 incl. `./package.json`).
|
||||
- ADDED: Playwright E2E specs covering SW intercept, range 206, encrypted archive end-to-end, content-addressed URL invalidation, preload injection, diagnostics mount/unmount.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- React companion package (`@suaveplan/react-packer`) — distinct package, separate change.
|
||||
- Mutation testing harness — covered by separate cross-cutting work.
|
||||
- Multi-browser Playwright matrix (Firefox + WebKit projects). Cluster C
|
||||
ships the spec coverage on the chromium project; multi-browser execution
|
||||
awaits a `playwright install` step which is documented but not run by
|
||||
default in the genesis CI.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/packer-browser.md`.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| PACKER-BROWSER-17 — Adaptive cache sizing | L71-L71 | §4.3 adaptive cache via `navigator.storage.estimate()`; flag-gated, Clock-injected, AbortSignal-aware. |
|
||||
| PACKER-BROWSER-18 — Preload-hint generation | L73-L73 | §4.5 `<link rel="preload">` on SW install; pure helper + DOM injector. |
|
||||
| PACKER-BROWSER-19 — Multi-origin SW helper | L75-L75 | §4.7 CDN-origin SW; new `UNAUTHORIZED_ORIGIN` code; wraps Cluster A's `registerPackedResourceServiceWorker`. |
|
||||
| PACKER-BROWSER-20 — BackgroundFetch integration | L76-L76 | §4.8 prefetch via `BackgroundFetchManager`; new `prefetchSuccessTotal`/`prefetchFailureTotal` counters. |
|
||||
| PACKER-BROWSER-21 — Diagnostics dev overlay | L77-L77 | §4.9 live overlay; subscribe-only, no manager mutation, real DOM. |
|
||||
| PACKER-BROWSER-22 — Final subpath split + Playwright E2E | L78-L78 / §4.10, §5.8-5.9 | Adds `/preload`, `/prefetch`, `/diagnostics` subpaths and ships the E2E suite. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/adaptive-cache/adaptive-cache.test.ts` — adaptive sizer + cache ceiling mutation.
|
||||
- `src/preload/preload.test.ts` — `generatePreloadHints` + `injectPreloadHints` against happy-dom.
|
||||
- `src/utils/cross-origin.test.ts` — origin allowlist + `UNAUTHORIZED_ORIGIN` error.
|
||||
- `src/prefetch/prefetch.test.ts` — `prefetchPackedArchive` + SW success handler against a `BackgroundFetchManager` shim.
|
||||
- `src/diagnostics/diagnostics.test.ts` — overlay mount/update/unmount + null-document fallback.
|
||||
- `src/error-codes.test.ts` — extended to assert `UNAUTHORIZED_ORIGIN` registration + production usage.
|
||||
- `src/observability/observability.test.ts` — extended to cover prefetch counters via real `MeterProvider`.
|
||||
- `tests/e2e/sw.spec.ts` + new specs in `tests/e2e/` — Playwright sweep.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. Cluster A/B public surface unchanged. Subpath additions are
|
||||
backwards-compatible.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/packer-browser` to enumerate. The
|
||||
package currently has no in-monorepo dependents on the `genesis` branch.
|
||||
|
||||
- No dependents — verified by `grep -rln "@suaveplan/packer-browser" /home/paul/web/genesis/packages/ /home/paul/web/genesis/apps/`.
|
||||
@@ -0,0 +1,266 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: packer-browser (enhance-packer-browser-platform)
|
||||
|
||||
**Package**: `@suaveplan/packer-browser`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring
|
||||
> time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PACKER-BROWSER-17 — Adaptive cache sizing from `navigator.storage.estimate()`
|
||||
|
||||
The package SHALL expose `createAdaptiveCacheSizer(opts)` at
|
||||
`src/adaptive-cache/adaptive-cache.ts`. The sizer SHALL, on construction
|
||||
and on a recurring interval (default 60_000 ms, driven by the injected
|
||||
`Clock`'s elapsed sampling), invoke an injected `storageEstimate` function
|
||||
(defaults to `() => navigator.storage.estimate()` when present) and call
|
||||
`cache.setMaxCacheSize(Math.max(1, Math.floor(quota * fraction)))` with the
|
||||
configured `fraction` (default `0.25`). The sizer MUST honour an injected
|
||||
`AbortSignal`: a pre-aborted signal causes the constructor to return a
|
||||
disposer without scheduling any work; aborting at any later point cancels
|
||||
the timer and rejects in-flight estimate awaits. When
|
||||
`navigator.storage.estimate` is unavailable AND no `storageEstimate` is
|
||||
injected, the sizer MUST fall back to the cache's static configured
|
||||
ceiling and emit no further updates. The
|
||||
`PackedResourceCache` SHALL expose `setMaxCacheSize(n: number): void` that
|
||||
updates the ceiling and runs `evictLRU()` until `size() <= n`. The
|
||||
manager SHALL accept `PackedResourceManagerOptions.adaptiveCache:
|
||||
AdaptiveCacheOptions` and SHALL register the
|
||||
`packer-browser.cache.adaptive` flag (default `true`) on the injected
|
||||
`FlagProvider`; when the flag is `false` the sizer MUST NOT be created.
|
||||
|
||||
#### Scenario: sizer applies the configured fraction of the estimated quota
|
||||
|
||||
- **WHEN** `createAdaptiveCacheSizer({ cache, fraction: 0.25, storageEstimate: async () => ({ quota: 4000, usage: 0 }) })` is invoked
|
||||
- **THEN** `cache.maxSize` is updated to `1000` after the initial estimate resolves
|
||||
|
||||
#### Scenario: missing estimate API falls back to the static ceiling
|
||||
|
||||
- **WHEN** the sizer is constructed with neither a `storageEstimate` injection nor a global `navigator.storage.estimate`
|
||||
- **THEN** the cache ceiling is unchanged and the sizer's `disposer()` is a safe no-op
|
||||
|
||||
#### Scenario: AbortSignal cancellation terminates the recurring updater
|
||||
|
||||
- **WHEN** the sizer is constructed with `signal: aborter.signal`, then `aborter.abort()` is called
|
||||
- **THEN** subsequent ticks of the recurring updater never invoke `storageEstimate` again
|
||||
|
||||
#### Scenario: setMaxCacheSize evicts excess entries
|
||||
|
||||
- **WHEN** the cache contains 5 unpinned entries and `cache.setMaxCacheSize(3)` is called
|
||||
- **THEN** the two least-recently-cached entries are evicted (revoked) and `cache.size()` is `3`
|
||||
|
||||
#### Scenario: flag-gated off skips construction
|
||||
|
||||
- **WHEN** the manager is constructed with `adaptiveCache: { enabled: false }`
|
||||
- **THEN** no sizer is created and `cache.maxSize` remains the static configured ceiling
|
||||
|
||||
**Verified by:** `src/adaptive-cache/adaptive-cache.test.ts::"applies the configured fraction of the estimated quota to the cache ceiling"` AND `src/adaptive-cache/adaptive-cache.test.ts::"falls back to the static ceiling when navigator.storage.estimate is unavailable"` AND `src/adaptive-cache/adaptive-cache.test.ts::"AbortSignal cancellation terminates the recurring updater"` AND `src/adaptive-cache/adaptive-cache.test.ts::"disposer returned from the sizer stops further estimate calls"` AND `src/cache/cache.test.ts::"setMaxCacheSize evicts unpinned entries until size meets the new ceiling"` AND `src/manager/manager.test.ts::"adaptiveCache.enabled=false skips sizer construction"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-18 — `<link rel="preload">` generation on SW install
|
||||
|
||||
The package SHALL expose `generatePreloadHints(manifest, opts?):
|
||||
readonly PreloadHintLink[]` at `src/preload/preload.ts` returning a pure
|
||||
list of `{ url, as, crossOrigin? }` records derived from the manifest's
|
||||
`preload: true` entries OR the top-N entries by `priority` (descending,
|
||||
ties broken by lexicographic url) when `opts.topN` is supplied. The
|
||||
package SHALL expose `injectPreloadHints(doc, hints): readonly
|
||||
HTMLLinkElement[]` which appends `<link rel="preload">` elements to
|
||||
`doc.head` when `doc` carries a `head`; in non-DOM contexts the helper
|
||||
MUST return an empty array without throwing. Every emitted hint MUST
|
||||
count toward `PackerBrowserCounters.fetchTotal` so dashboards account
|
||||
for preload traffic. The helpers MUST be re-exported from the
|
||||
new `/preload` subpath AND the existing root `.` entrypoint.
|
||||
|
||||
#### Scenario: hints derived from `preload: true` manifest entries
|
||||
|
||||
- **WHEN** `generatePreloadHints({ entries: [{ url: "/a", preload: true, as: "image" }, { url: "/b", preload: false, as: "font" }] })` is called
|
||||
- **THEN** the result contains exactly one hint with `{ url: "/a", as: "image" }`
|
||||
|
||||
#### Scenario: top-N by priority overrides the preload flag
|
||||
|
||||
- **WHEN** `generatePreloadHints({ entries: [{ url: "/a", priority: 1 }, { url: "/b", priority: 5 }, { url: "/c", priority: 3 }] }, { topN: 2 })` is called
|
||||
- **THEN** the result is `[{ url: "/b", ... }, { url: "/c", ... }]` in that order
|
||||
|
||||
#### Scenario: injectPreloadHints is feature-detected
|
||||
|
||||
- **WHEN** `injectPreloadHints(undefined, hints)` is invoked
|
||||
- **THEN** the call returns `[]` without throwing
|
||||
|
||||
#### Scenario: injection appends link elements to document head
|
||||
|
||||
- **WHEN** `injectPreloadHints(document, hints)` is invoked under happy-dom with two hints
|
||||
- **THEN** `document.head.querySelectorAll('link[rel="preload"]').length` is `2`
|
||||
|
||||
#### Scenario: each emitted hint increments fetchTotal
|
||||
|
||||
- **WHEN** `generatePreloadHints(manifest, { observability })` emits two hints
|
||||
- **THEN** the observability bundle's `fetchTotal` counter has been incremented twice
|
||||
|
||||
**Verified by:** `src/preload/preload.test.ts::"derives hints from manifest entries marked preload: true"` AND `src/preload/preload.test.ts::"top-N selection orders by descending priority"` AND `src/preload/preload.test.ts::"injectPreloadHints returns an empty array when the document is undefined"` AND `src/preload/preload.test.ts::"injectPreloadHints appends one link element per hint to document.head"` AND `src/preload/preload.test.ts::"emits one fetchTotal counter increment per generated hint when observability is supplied"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-19 — Multi-origin Service Worker helper
|
||||
|
||||
The package SHALL expose
|
||||
`registerCrossOriginPackedResourceServiceWorker(opts)` at
|
||||
`src/utils/cross-origin.ts` accepting `{ swUrl: string, scope: string,
|
||||
allowedOrigins: readonly string[], container?:
|
||||
ServiceWorkerContainerLike, messageResolver? }`. The helper SHALL parse
|
||||
`swUrl` and reject with
|
||||
`PackerBrowserError(PACKER_BROWSER.UNAUTHORIZED_ORIGIN)` BEFORE any
|
||||
`register()` call when the parsed origin is not present in
|
||||
`allowedOrigins`. On success the helper SHALL delegate to
|
||||
`registerPackedResourceServiceWorker` and then invoke
|
||||
`container.getRegistrations()` (mirroring `clients.matchAll({
|
||||
includeUncontrolled: true })` semantics by listing every registration
|
||||
regardless of activation state) so callers can verify the cross-origin
|
||||
SW is reachable. The new `PACKER_BROWSER.UNAUTHORIZED_ORIGIN` code SHALL
|
||||
be added to `PACKER_BROWSER_CODES` and reachable from this helper's
|
||||
production path.
|
||||
|
||||
#### Scenario: SW URL whose origin is not in the allowlist is rejected
|
||||
|
||||
- **WHEN** `registerCrossOriginPackedResourceServiceWorker({ swUrl: "https://attacker.com/sw.js", scope: "/", allowedOrigins: ["https://cdn.example.com"] })` is called
|
||||
- **THEN** the promise rejects with `PackerBrowserError(PACKER_BROWSER.UNAUTHORIZED_ORIGIN)`
|
||||
|
||||
#### Scenario: SW URL whose origin is in the allowlist registers
|
||||
|
||||
- **WHEN** the helper is called with `swUrl: "https://cdn.example.com/sw.js"` and `allowedOrigins: ["https://cdn.example.com"]`
|
||||
- **THEN** the underlying container's `register` is invoked exactly once with `("https://cdn.example.com/sw.js", { scope })`
|
||||
|
||||
#### Scenario: registration list is queried after success
|
||||
|
||||
- **WHEN** registration succeeds against the allowlisted origin
|
||||
- **THEN** the container's `getRegistrations()` is awaited and its returned array is exposed on the helper's resolved value
|
||||
|
||||
**Verified by:** `src/utils/cross-origin.test.ts::"rejects with UNAUTHORIZED_ORIGIN when swUrl origin is not allowlisted"` AND `src/utils/cross-origin.test.ts::"delegates to registerPackedResourceServiceWorker when origin is allowlisted"` AND `src/utils/cross-origin.test.ts::"returns the container's registration list after successful registration"` AND `src/error-codes.test.ts::"UNAUTHORIZED_ORIGIN is referenced by a production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-20 — BackgroundFetch API integration
|
||||
|
||||
The package SHALL expose `prefetchPackedArchive(archiveUrl, opts):
|
||||
Promise<BackgroundFetchRegistrationLike | null>` at
|
||||
`src/prefetch/prefetch.ts`. When `opts.backgroundFetch` (or
|
||||
`navigator.serviceWorker.ready.backgroundFetch` resolved from a real
|
||||
`ServiceWorkerRegistration` in production) is available, the helper
|
||||
SHALL invoke `backgroundFetch.fetch(id, [archiveUrl], opts.options)` and
|
||||
resolve with the registration. When the API is missing, the helper
|
||||
SHALL resolve with `null`. The package SHALL expose
|
||||
`handleBackgroundFetchSuccess(event, deps)` for the SW's
|
||||
`backgroundfetchsuccess` listener: on success, the helper SHALL drain
|
||||
the event's records into the configured cache (default `packed-resources-v1`)
|
||||
and increment `PackerBrowserCounters.prefetchSuccessTotal`. On failure
|
||||
or `backgroundfetchfailure` invocations the helper SHALL increment
|
||||
`PackerBrowserCounters.prefetchFailureTotal`. The two new counters
|
||||
(`packed_resource_prefetch_success_total` and
|
||||
`packed_resource_prefetch_failure_total`) MUST be created on the
|
||||
observability bundle and asserted under a real in-memory
|
||||
`MeterProvider`.
|
||||
|
||||
#### Scenario: missing API returns null
|
||||
|
||||
- **WHEN** `prefetchPackedArchive("/x.packed", { backgroundFetch: undefined })` is called
|
||||
- **THEN** the promise resolves to `null` and no counters are incremented
|
||||
|
||||
#### Scenario: present API delegates to BackgroundFetchManager.fetch
|
||||
|
||||
- **WHEN** the helper is called with a shim `BackgroundFetchManager` whose `fetch` resolves
|
||||
- **THEN** the shim's `fetch` is invoked exactly once with the configured id, the archive URL array, and the options bundle, and the helper's resolved value is the shim's returned registration
|
||||
|
||||
#### Scenario: success handler drains records into the cache and increments the success counter
|
||||
|
||||
- **WHEN** `handleBackgroundFetchSuccess(event, { caches, cacheName: "test", observability })` is invoked against a shim event whose `matchAll` returns one record
|
||||
- **THEN** the configured cache contains the record's response and `prefetchSuccessTotal` is incremented exactly once
|
||||
|
||||
#### Scenario: failure handler increments the failure counter
|
||||
|
||||
- **WHEN** `handleBackgroundFetchFailure(event, { observability })` is invoked
|
||||
- **THEN** `prefetchFailureTotal` is incremented exactly once
|
||||
|
||||
**Verified by:** `src/prefetch/prefetch.test.ts::"resolves to null when BackgroundFetchManager is unavailable"` AND `src/prefetch/prefetch.test.ts::"delegates to BackgroundFetchManager.fetch when the API is available"` AND `src/prefetch/prefetch.test.ts::"success handler drains records into the cache and increments prefetchSuccessTotal"` AND `src/prefetch/prefetch.test.ts::"failure handler increments prefetchFailureTotal"` AND `src/observability/observability.test.ts::"prefetchSuccessTotal and prefetchFailureTotal are emitted under a real MeterProvider"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-21 — Diagnostics dev overlay
|
||||
|
||||
The package SHALL expose `createDiagnosticsOverlay(manager, opts?): {
|
||||
mount(parent?: ParentNode): () => void }` at
|
||||
`src/diagnostics/diagnostics.ts`. The factory SHALL subscribe to the
|
||||
manager's `TinyEmitter` events (`load`, `error`, `cacheHit`, `cacheMiss`,
|
||||
`cacheEvict`), maintain an in-memory ring of the last 50 events, and
|
||||
render a single `<div>` whose child nodes report cache size, hit-ratio,
|
||||
miss-ratio, in-flight count, and the event ring. The factory MUST NOT
|
||||
mutate the manager (subscribe-only). The mount method MUST return a
|
||||
disposer that removes the rendered element and unsubscribes every
|
||||
listener. When `document` is `undefined`, `mount` MUST return a no-op
|
||||
disposer without throwing.
|
||||
|
||||
#### Scenario: overlay renders cache stats from the manager
|
||||
|
||||
- **WHEN** the manager's cache contains two entries and the overlay is mounted onto a happy-dom `<body>`
|
||||
- **THEN** the rendered element's text contains the substring `cache.size=2`
|
||||
|
||||
#### Scenario: event ring records the last 50 events
|
||||
|
||||
- **WHEN** the manager emits 60 `cacheHit` events
|
||||
- **THEN** the overlay's event-ring snapshot contains exactly 50 entries, all of type `cacheHit`
|
||||
|
||||
#### Scenario: dispose removes the element and unsubscribes
|
||||
|
||||
- **WHEN** `mount` returns a disposer and the disposer is invoked
|
||||
- **THEN** the overlay's element is no longer attached to `parent`, and subsequent manager `emit` calls leave the snapshot unchanged
|
||||
|
||||
#### Scenario: no-document environment returns a safe disposer
|
||||
|
||||
- **WHEN** `mount` is called against an overlay constructed without `document`
|
||||
- **THEN** the disposer is a callable no-op and no errors are thrown
|
||||
|
||||
#### Scenario: overlay does not mutate the manager
|
||||
|
||||
- **WHEN** the overlay is mounted and `manager.getCacheStats()` is recorded before and after `mount` + `dispose`
|
||||
- **THEN** both snapshots are structurally equal
|
||||
|
||||
**Verified by:** `src/diagnostics/diagnostics.test.ts::"renders cache.size derived from the manager"` AND `src/diagnostics/diagnostics.test.ts::"event ring retains only the last 50 events"` AND `src/diagnostics/diagnostics.test.ts::"disposer removes the element and unsubscribes every listener"` AND `src/diagnostics/diagnostics.test.ts::"returns a safe no-op disposer when document is absent"` AND `src/diagnostics/diagnostics.test.ts::"mount and dispose do not mutate the manager's cache state"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-BROWSER-22 — Final subpath split + Playwright E2E coverage
|
||||
|
||||
The package SHALL ship three additional subpath exports on top of
|
||||
PACKER-BROWSER-12's five: `./preload`, `./prefetch`, `./diagnostics`.
|
||||
Each new subpath MUST be tree-shakeable
|
||||
(`"sideEffects": false`) and MUST resolve to a built artefact under
|
||||
`dist/`. The package SHALL ship Playwright spec files under `tests/e2e/`
|
||||
covering the following scenarios: SW install + packed URL intercept,
|
||||
range-request 206 response, encrypted archive end-to-end, content-
|
||||
addressed URL invalidation on SW activate, preload-hint DOM injection,
|
||||
and diagnostics overlay mount/unmount. The Playwright config SHALL
|
||||
validate without browser binaries installed; running the suite is gated
|
||||
on `bunx playwright install chromium` being executed in the runtime
|
||||
environment.
|
||||
|
||||
#### Scenario: every new subpath resolves to a built dist artefact
|
||||
|
||||
- **WHEN** the build runs and `package.json.exports` is inspected
|
||||
- **THEN** `./preload`, `./prefetch`, `./diagnostics` are present and each `import.default` value points at `./dist/<feature>/index.js`
|
||||
|
||||
#### Scenario: subpaths preserve tree-shake invariant
|
||||
|
||||
- **WHEN** `package.json` is inspected
|
||||
- **THEN** `sideEffects === false`
|
||||
|
||||
#### Scenario: Playwright config is valid and the E2E spec files exist
|
||||
|
||||
- **WHEN** the test harness scans `tests/e2e/`
|
||||
- **THEN** files exist for SW intercept, range 206, encrypted archive, content-addressed invalidation, preload injection, and diagnostics mount/unmount
|
||||
|
||||
**Verified by:** `src/index.test.ts::"every subpath export resolves to a built dist artefact (cluster C)"` AND `src/index.test.ts::"package.json declares sideEffects: false (cluster C)"` AND `src/index.test.ts::"every Playwright E2E spec file exists under tests/e2e/"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,84 @@
|
||||
# Tasks — enhance-packer-browser-platform
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/packer-browser.md` §4 items 3, 5, 7, 8, 9, 10
|
||||
- [x] 1.2 Read `openspec/specs/packer-browser/spec.md` (PACKER-BROWSER-1..16)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md` with Source mapping + Affected dependents
|
||||
- [x] 1.5 Draft `specs/packer-browser/spec.md` carrying PACKER-BROWSER-17..22
|
||||
- [x] 1.6 Every requirement carries a `**Verified by:**` line at draft time
|
||||
|
||||
## 2. Implementation — C-1 adaptive cache sizing
|
||||
|
||||
- [ ] 2.1 Add `setMaxCacheSize(n: number)` to `PackedResourceCache`; mutating the ceiling triggers `evictLRU()` while size > ceiling.
|
||||
- [ ] 2.2 Implement `src/adaptive-cache/adaptive-cache.ts` exposing `createAdaptiveCacheSizer(opts)`.
|
||||
- [ ] 2.3 Wire `adaptiveCache` into `PackedResourceManagerOptions` + manager constructor; gate via `packer-browser.cache.adaptive` flag (default ON).
|
||||
- [ ] 2.4 Register the new flag in `registerDefaultFlags`.
|
||||
|
||||
## 3. Implementation — C-2 preload hint generation
|
||||
|
||||
- [ ] 3.1 Implement `src/preload/preload.ts` exposing `generatePreloadHints(manifest, opts)` and `injectPreloadHints(doc, hints)`.
|
||||
- [ ] 3.2 Re-export from `/utils` (legacy) and add new `/preload` subpath.
|
||||
- [ ] 3.3 Wire OTel counter increments on every hint emitted.
|
||||
|
||||
## 4. Implementation — C-3 multi-origin SW helper
|
||||
|
||||
- [ ] 4.1 Add `UNAUTHORIZED_ORIGIN` to `PACKER_BROWSER_CODES`.
|
||||
- [ ] 4.2 Implement `src/utils/cross-origin.ts` exposing `registerCrossOriginPackedResourceServiceWorker(opts)`.
|
||||
- [ ] 4.3 Re-export from `/utils` subpath barrel + root.
|
||||
|
||||
## 5. Implementation — C-4 BackgroundFetch integration
|
||||
|
||||
- [ ] 5.1 Add `prefetchSuccessTotal` + `prefetchFailureTotal` counters to observability bundle.
|
||||
- [ ] 5.2 Implement `src/prefetch/prefetch.ts` exposing `prefetchPackedArchive(archiveUrl, opts)` and `handleBackgroundFetchSuccess(event, opts)`.
|
||||
- [ ] 5.3 Add new `/prefetch` subpath export.
|
||||
|
||||
## 6. Implementation — C-5 diagnostics dev overlay
|
||||
|
||||
- [ ] 6.1 Implement `src/diagnostics/diagnostics.ts` exposing `createDiagnosticsOverlay(manager, opts)`.
|
||||
- [ ] 6.2 Add new `/diagnostics` subpath export.
|
||||
- [ ] 6.3 Subscribe to manager's `TinyEmitter` events; assert no manager mutation.
|
||||
|
||||
## 7. Implementation — C-6 final subpath split + Playwright E2E
|
||||
|
||||
- [ ] 7.1 Update `package.json` `exports`: add `./preload`, `./prefetch`, `./diagnostics`. Update `vite.config.ts` lib entries.
|
||||
- [ ] 7.2 Write Playwright spec files in `tests/e2e/` covering SW intercept, range 206, encrypted archive, content-addressed invalidation, preload injection, diagnostics mount/unmount.
|
||||
- [ ] 7.3 Document `bunx playwright install chromium` in README "Test plan" footer.
|
||||
|
||||
## 8. Tests
|
||||
|
||||
- [ ] 8.1 Unit tests per feature covering every scenario named in the spec delta
|
||||
- [ ] 8.2 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async)
|
||||
- [ ] 8.3 100/100/100/100 coverage on touched files
|
||||
|
||||
## 9. Documentation
|
||||
|
||||
- [ ] 9.1 Sibling `.md` (≥200 words, basic + advanced examples) for every new implementation file
|
||||
- [ ] 9.2 README links every co-located `.md` under `## Modules`
|
||||
|
||||
## 10. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 10.1 `bun run check` — Biome 0 errors / 0 warnings
|
||||
- [ ] 10.2 `bunx turbo lint typecheck test --filter=@suaveplan/packer-browser` — green
|
||||
- [ ] 10.3 `bunx turbo test:coverage --filter=@suaveplan/packer-browser` — 100/100/100/100
|
||||
- [ ] 10.4 `bun scripts/openspec-conformance.ts @suaveplan/packer-browser` — exit 0
|
||||
- [ ] 10.5 `bunx turbo build --filter=@suaveplan/packer-browser` — green
|
||||
|
||||
## 11. Pre-archive 9-row checklist (AGENTS.md Section 13)
|
||||
|
||||
- [ ] 11.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim
|
||||
- [ ] 11.2 Zod schemas wired at every declared boundary (n/a for this change — no wire surface)
|
||||
- [ ] 11.3 Every error code in `error-codes.ts` thrown by at least one production path (incl. new `UNAUTHORIZED_ORIGIN`)
|
||||
- [ ] 11.4 Strict-mode behaviors tested both on and off (n/a — no strict-mode toggle introduced)
|
||||
- [ ] 11.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider` (incl. new prefetch counters)
|
||||
- [ ] 11.6 Lifecycle dedup tested where claimed (adaptive sizer disposer)
|
||||
- [ ] 11.7 `AbortSignal` propagation tested on every async surface
|
||||
- [ ] 11.8 Subpath exports resolve to built artefacts; `sideEffects: false` preserved
|
||||
- [ ] 11.9 No retroactive tick
|
||||
|
||||
## 12. Archive
|
||||
|
||||
- [ ] 12.1 Fold spec delta into `openspec/specs/packer-browser/spec.md` (append PACKER-BROWSER-17..22; preserve 1..16)
|
||||
- [ ] 12.2 Move `openspec/changes/enhance-packer-browser-platform/` → `openspec/archive/<yyyy-mm-dd>-enhance-packer-browser-platform/`
|
||||
- [ ] 12.3 Refresh `MANIFEST.md` via `bun scripts/folder-reorg/generate-manifest.ts`
|
||||
Reference in New Issue
Block a user