Initial Commit
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Enterprise hardening — `@suaveplan/packer` resilience cluster
|
||||
|
||||
**Package**: `@suaveplan/packer`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature
|
||||
**Design**: (inline below — small scope)
|
||||
**Spec delta**: ./specs/packer/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
Cluster #1 (`enterprise-packer-integrity`, archived 2026-05-19) landed BLAKE3
|
||||
content-addressable digests, the bundle-level Merkle root, mandatory
|
||||
integrity verification on unpack, full OpenTelemetry instrumentation, and
|
||||
i18n-keyed error messages. The `enterprise/packer.md` audit (L43, L46–L47)
|
||||
calls out three remaining resilience gaps that block the ENTERPRISE label:
|
||||
|
||||
1. No retry primitives on IO-backed sources (`ReadableStream.read()` calls
|
||||
throw straight up the stack on transient errors).
|
||||
2. No idempotency layer — repeat packs over identical inputs re-run the full
|
||||
gzip + AEAD + ed25519 pipeline every time.
|
||||
3. The whole-file `pack` / `unpack` paths do not accept `AbortSignal`; only
|
||||
the chunked path honours cancellation today.
|
||||
4. `Date.now()` and `crypto.getRandomValues()` are called at production code
|
||||
paths in `src/`, violating the audit's "no `Math.random()` / `Date.now()`
|
||||
at module scope; injectable clock and RNG" line.
|
||||
|
||||
This is cluster #2 of a 3-cluster enterprise hardening of `@suaveplan/packer`
|
||||
(integrity → resilience → extensibility). Cluster #3 is deliberately out of
|
||||
scope here — see "Out of scope" below.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `src/retry/retryable-read.ts` — `retryRead(reader, opts)` helper that
|
||||
wraps `ReadableStream` reads in `@suaveplan/retry`'s `withRetry`, honours
|
||||
`AbortSignal`, and returns the same `{ value, done }` shape. Defaults:
|
||||
`createExponentialBackoff({ initial: 25, base: 2, max: 1000, jitter: "equal" })`
|
||||
+ `maxAttempts: 4`.
|
||||
- ADDED: `RetryPolicy` option bundle on `PackerOptions`, `PackChunkedOptions`,
|
||||
and `UnpackChunkedOptions` (`retryConfig?`, `signal?`). Threaded through to
|
||||
`normalizeSource` and the abstract `collect` path so every `reader.read()`
|
||||
in production code is retry-aware.
|
||||
- ADDED: `src/idempotency/idempotency.ts` — `PackIdempotencyGuard` that
|
||||
computes a content-derived key (`blake3(payload) || compressionLevel ||
|
||||
encryptionAlgo || signed`-bit) and short-circuits to a cached `PackedData`
|
||||
when the same input is packed again. Backed by an injectable
|
||||
`IdempotencyStoreAdapter` (default `MemoryIdempotencyStore`).
|
||||
- ADDED: `PackerOptions.idempotency?: { store, ttlMs?, enabled? }`.
|
||||
`Packer.pack` consults the guard before any work; a hit returns the cached
|
||||
bundle bytes. Misses populate the store after the pack completes.
|
||||
- ADDED: `signal?: AbortSignal` on `UnpackOptions`. `Unpacker.unpack` now
|
||||
checks `signal.aborted` at every yield point (before integrity verify,
|
||||
before signature verify, before AEAD decrypt, before gunzip, before
|
||||
checksum verify, before the final emit).
|
||||
- ADDED: `PackOptions` interface (`{ filename?, signal? }`) and an additional
|
||||
`Packer.pack(data, opts: PackOptions)` overload. The original
|
||||
`Packer.pack(data, filename?)` overload remains for back-compat.
|
||||
- ADDED: `signal?: AbortSignal` on `PackChunkedOptions` — already honoured on
|
||||
the unpacker; now enforced on the chunked packer between every emitted
|
||||
chunk and the source pull loop.
|
||||
- ADDED: `Clock` and `Rng` injection on `PackerOptions`, `PackChunkedOptions`,
|
||||
`UnpackChunkedOptions`. Defaults wire to real platform implementations
|
||||
(`() => Date.now()` for `Clock`, a wrapper around `crypto.getRandomValues`
|
||||
for `Rng`). Every `Date.now()` / `crypto.getRandomValues()` /
|
||||
`performance.now()` call in production `src/` is replaced with a call
|
||||
through the injected `Clock` / `Rng`.
|
||||
- MODIFIED: `AbstractPacker.buildMetadata` reads `timestamp` from the
|
||||
resolved `Clock` instead of `Date.now()`.
|
||||
- MODIFIED: `packer/packer.ts` reads nonce bytes via the resolved `Rng`
|
||||
instead of calling `crypto.getRandomValues` directly.
|
||||
- MODIFIED: every `nowMs()` helper across `packer/`, `unpacker/`,
|
||||
`chunked/packer/`, `chunked/unpacker/` now reads through the resolved
|
||||
`monotonicClock` (defaults to `performance.now()` for backward parity).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- `@suaveplan/schema-registry` integration → cluster #3.
|
||||
- `@suaveplan/feature-flags` for format-version rollouts → cluster #3.
|
||||
- Plugin / extension interface → cluster #3.
|
||||
- `fast-check` property tests for round-trip invariants → cluster #3.
|
||||
- mitata benchmarks → cluster #3.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/packer.md`.
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| PACKER-22 (`@suaveplan/retry` on IO) | L47 | Verbatim — "**retry** primitives for IO-backed sources". |
|
||||
| PACKER-23 (idempotency cache) | L47 | Verbatim — "**Idempotency** — repeat packs over the same inputs are a no-op". |
|
||||
| PACKER-24 (whole-file AbortSignal) | L46 | Verbatim — "**Backpressure & cancellation** — `AbortSignal` everywhere async". |
|
||||
| PACKER-25 (injected `Clock` + `Rng`) | L43 | Verbatim — "no `Math.random()` / `Date.now()` at module scope; injectable clock and RNG". |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
None — every requirement in this cluster maps directly to an audit bullet.
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/retry/retryable-read.test.ts` + `.spec-conformance.test.ts` — pins
|
||||
`retryRead` against an injectable failing reader, asserts the retry policy
|
||||
is consulted, asserts `AbortSignal` cancels mid-backoff.
|
||||
- `src/idempotency/idempotency.test.ts` + `.spec-conformance.test.ts` — pins
|
||||
identical-input dedup, cache-miss population, custom-store injection.
|
||||
- `src/packer/packer.spec-conformance.test.ts` (extended) — pins `pack`
|
||||
abort, idempotency hit, `Clock`-driven timestamp, `Rng`-driven nonce.
|
||||
- `src/unpacker/unpacker.spec-conformance.test.ts` (extended) — pins
|
||||
pre-flight abort, mid-pipeline abort.
|
||||
- `src/chunked/packer/packer.spec-conformance.test.ts` (extended) — pins
|
||||
signal-driven mid-stream abort.
|
||||
|
||||
## Migration impact
|
||||
|
||||
Public-surface additions only. No existing signature changes shape — the new
|
||||
`Packer.pack(data, opts: PackOptions)` overload sits beside the existing
|
||||
`Packer.pack(data, filename?)`. The `Clock` / `Rng` / `idempotency` /
|
||||
`retryConfig` / `signal` options are all optional with platform defaults.
|
||||
Two consumer packages (`@suaveplan/drive-plugin-chunks`,
|
||||
`@suaveplan/drive-webrtc`) are audited and the sweep is part of the gates;
|
||||
no existing call site changes shape.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/packer` to enumerate.
|
||||
|
||||
- `@suaveplan/drive-plugin-chunks` — [x] tests run / [x] docs reviewed / [ ] no action needed
|
||||
- `@suaveplan/drive-webrtc` — [x] tests run / [x] docs reviewed / [ ] no action needed
|
||||
|
||||
If `tests run` is unchecked for any dependent, the change MUST NOT be folded
|
||||
into the canonical spec.
|
||||
|
||||
## Migration
|
||||
|
||||
No behavioral change for callers that did not pass the new options. Existing
|
||||
calls keep their previous semantics:
|
||||
|
||||
- **No `signal` passed**: pack / unpack behaviour is identical to cluster #1.
|
||||
- **No `idempotency` passed**: no cache layer is consulted.
|
||||
- **No `retryConfig` passed**: a single read is attempted (no retries) for
|
||||
parity with cluster #1 — the default policy is `maxAttempts: 1` so the
|
||||
retry layer is a no-op unless opted in.
|
||||
- **No `clock` / `rng` passed**: defaults call `Date.now()` /
|
||||
`crypto.getRandomValues` exactly as before, so wire output is identical.
|
||||
@@ -0,0 +1,193 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: packer (enterprise-packer-resilience)
|
||||
|
||||
**Package**: `@suaveplan/packer`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PACKER-22 — `@suaveplan/retry` on IO-backed source reads
|
||||
|
||||
The package SHALL route every IO-backed source pull (every `reader.read()`
|
||||
call against a `ReadableStream` consumed by `normalizeSource` or the
|
||||
abstract `collect` helper) through `@suaveplan/retry`'s `withRetry`. The
|
||||
retry policy MUST be configurable per call via a `retryConfig?: RetryConfig`
|
||||
option on `PackerOptions`, `PackChunkedOptions`, and
|
||||
`UnpackChunkedOptions`. When `retryConfig` is absent the package MUST
|
||||
default to a single-attempt policy (`maxAttempts: 1`) so wire behaviour
|
||||
matches cluster #1. When `retryConfig` is supplied the retry loop MUST
|
||||
honour the caller's `AbortSignal`: cancellation MUST abort the retry loop
|
||||
immediately (no further attempts, no further sleeps) and surface as a
|
||||
`RetryAbortedError`.
|
||||
|
||||
#### Scenario: failing reader is retried up to maxAttempts
|
||||
|
||||
- **WHEN** `normalizeSource` consumes a `ReadableStream` whose `read()`
|
||||
rejects the first two times and resolves on the third, with
|
||||
`retryConfig: { maxAttempts: 3, delay: createConstantBackoff({ delay: 0 }) }`
|
||||
- **THEN** the iterable yields the eventually-read bytes and the consumer
|
||||
observes three total read attempts
|
||||
|
||||
#### Scenario: AbortSignal cancels the retry loop mid-backoff
|
||||
|
||||
- **WHEN** the retry loop is sleeping between attempts and the caller's
|
||||
`AbortSignal` fires
|
||||
- **THEN** the retry rejects with `RetryAbortedError` and no further
|
||||
`reader.read()` is invoked
|
||||
|
||||
#### Scenario: default policy is a single attempt
|
||||
|
||||
- **WHEN** `retryConfig` is not supplied and a `read()` rejects
|
||||
- **THEN** the rejection surfaces unchanged and the iterable terminates
|
||||
with the original error (no retries)
|
||||
|
||||
**Verified by:** `src/retry/retryable-read.test.ts::"retries up to maxAttempts on a failing reader"` AND `src/retry/retryable-read.test.ts::"cancels the retry loop when AbortSignal fires mid-backoff"` AND `src/retry/retryable-read.spec-conformance.test.ts::"default policy is a single attempt with no retries"` AND `src/retry/retryable-read.spec-conformance.test.ts::"retryRead surfaces RetryAbortedError when signal fires before first read"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-23 — Idempotent repeat-pack short-circuit
|
||||
|
||||
The package SHALL expose an injectable idempotency cache on
|
||||
`PackerOptions.idempotency` (`{ store, ttlMs?, enabled? }`). When enabled,
|
||||
`Packer.pack(data, opts?)` MUST compute a content-derived dedup key from
|
||||
`blake3(data)` plus the packer's pipeline parameters
|
||||
(`compressionLevel`, `encryption.algorithm`, `signing` enabled bit), look
|
||||
the key up in the supplied `IdempotencyStoreAdapter`, and on hit return
|
||||
the previously-built `PackedData` without re-running the gzip / AEAD /
|
||||
sign / integrity pipeline. On miss the pipeline MUST run as before and
|
||||
the resulting `PackedData` MUST be written back to the store under the
|
||||
same key with the configured TTL (default `86_400_000` ms / 24 h).
|
||||
Idempotency MUST be opt-in: callers that do not pass `idempotency` see
|
||||
the legacy non-cached behaviour.
|
||||
|
||||
#### Scenario: identical pack inputs short-circuit on the second call
|
||||
|
||||
- **WHEN** `Packer.pack(data)` is called twice with identical `data`,
|
||||
identical options, and a `MemoryIdempotencyStore` injected through
|
||||
`idempotency.store`
|
||||
- **THEN** the second call returns a `PackedData` whose `data` bytes and
|
||||
`metadata.integrity` are byte-equal to the first call's result, and
|
||||
the underlying pipeline (gzip / AEAD / sign) ran exactly once
|
||||
|
||||
#### Scenario: cache miss populates the store
|
||||
|
||||
- **WHEN** the first call's dedup key is absent from the store
|
||||
- **THEN** the store's `set(key, record)` is invoked exactly once after
|
||||
the pack completes, with the resulting `PackedData` carried on
|
||||
`record.response`
|
||||
|
||||
#### Scenario: idempotency disabled by default
|
||||
|
||||
- **WHEN** `Packer.pack(data)` is called with no `idempotency` option
|
||||
- **THEN** no store interaction occurs and the pipeline runs end-to-end
|
||||
on every call
|
||||
|
||||
**Verified by:** `src/idempotency/idempotency.test.ts::"second pack with identical input short-circuits to cached PackedData"` AND `src/idempotency/idempotency.test.ts::"cache miss populates the store with the resulting PackedData"` AND `src/idempotency/idempotency.spec-conformance.test.ts::"idempotency is disabled by default"` AND `src/idempotency/idempotency.spec-conformance.test.ts::"custom IdempotencyStoreAdapter is consulted exactly once per pack"`
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: PACKER-24 — Whole-file `AbortSignal` propagation
|
||||
|
||||
The package SHALL accept an `AbortSignal` on every async public
|
||||
entrypoint and MUST check the signal at every yield point. `Unpacker.unpack`
|
||||
accepts `UnpackOptions.signal?` and MUST check it before integrity
|
||||
verify, before signature verify, before AEAD decrypt, before gunzip, and
|
||||
before the final emit. `Packer.pack` accepts an additional overload
|
||||
`pack(data, opts: PackOptions)` where `PackOptions.signal?` is checked
|
||||
before compression, before encryption, before signing, before integrity
|
||||
compute, and before the final return. `packChunks` accepts
|
||||
`PackChunkedOptions.signal?` and MUST check it on every loop iteration
|
||||
before yielding the next packed chunk. Aborted operations MUST throw
|
||||
`PackerError(PACKER.ABORTED)` with the i18n catalog message
|
||||
`packer.error.ABORTED.<entrypoint>`. The original
|
||||
`Packer.pack(data, filename?)` overload MUST remain a valid call shape
|
||||
for back-compat.
|
||||
|
||||
#### Scenario: Unpacker.unpack aborts before integrity verify
|
||||
|
||||
- **WHEN** `Unpacker.unpack(packed, { signal })` is called with a signal
|
||||
that is already aborted
|
||||
- **THEN** the call rejects with `PackerError(PACKER.ABORTED)` and no
|
||||
integrity recomputation runs
|
||||
|
||||
#### Scenario: Unpacker.unpack aborts between pipeline steps
|
||||
|
||||
- **WHEN** the signal fires after integrity verify but before AEAD decrypt
|
||||
- **THEN** the call rejects with `PackerError(PACKER.ABORTED)` and no
|
||||
decrypted plaintext is materialised
|
||||
|
||||
#### Scenario: Packer.pack(data, opts) honours opts.signal
|
||||
|
||||
- **WHEN** `Packer.pack(data, { signal })` is called with a signal that
|
||||
is already aborted
|
||||
- **THEN** the call rejects with `PackerError(PACKER.ABORTED)` before
|
||||
any compression / encryption / signing / integrity work runs
|
||||
|
||||
#### Scenario: packChunks aborts mid-stream
|
||||
|
||||
- **WHEN** `packChunks(source, { ..., signal })` is iterated and the
|
||||
signal fires after the second emitted chunk
|
||||
- **THEN** the iterator rejects with `PackerError(PACKER.ABORTED)`
|
||||
before yielding any further chunks
|
||||
|
||||
#### Scenario: legacy filename overload remains valid
|
||||
|
||||
- **WHEN** `Packer.pack(data, "logo.png")` is called
|
||||
- **THEN** the call succeeds, `metadata.mimeType` is `"image/png"`, and
|
||||
no signal-related behaviour kicks in
|
||||
|
||||
**Verified by:** `src/unpacker/unpacker.spec-conformance.test.ts::"rejects with PACKER.ABORTED when signal is pre-aborted"` AND `src/unpacker/unpacker.spec-conformance.test.ts::"rejects with PACKER.ABORTED when signal fires between pipeline steps"` AND `src/packer/packer.spec-conformance.test.ts::"rejects with PACKER.ABORTED when opts.signal is pre-aborted"` AND `src/chunked/packer/packer.spec-conformance.test.ts::"aborts mid-stream when signal fires after the second chunk"` AND `src/packer/packer.spec-conformance.test.ts::"legacy pack(data, filename) overload still surfaces mimeType"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PACKER-25 — Injected `Clock` and `Rng` everywhere
|
||||
|
||||
The package SHALL accept `clock?: Clock` and `rng?: Rng` (types from
|
||||
`@suaveplan/types`) on every public option bundle (`PackerOptions`,
|
||||
`PackChunkedOptions`, `UnpackChunkedOptions`). Every production
|
||||
read of wall-clock time MUST go through the resolved `Clock` (no direct
|
||||
`Date.now()`); every production source of randomness MUST go through
|
||||
the resolved `Rng` (no direct `Math.random()` /
|
||||
`crypto.getRandomValues`); every elapsed-time measurement MUST go
|
||||
through an injected `monotonicClock?: MonotonicClock` (defaults to a
|
||||
wrapper around `performance.now()`). The defaults MUST produce
|
||||
byte-identical wire output to the cluster #1 behaviour. Determinism:
|
||||
two `Packer.pack` calls with identical inputs, identical options, and a
|
||||
seeded `Rng` MUST produce byte-identical `PackedData.data` and
|
||||
identical `metadata.integrity` values.
|
||||
|
||||
#### Scenario: timestamp comes from the injected Clock
|
||||
|
||||
- **WHEN** `Packer.pack(data)` is called with a clock returning a fixed
|
||||
value `1_700_000_000_000`
|
||||
- **THEN** `result.metadata.timestamp === 1_700_000_000_000`
|
||||
|
||||
#### Scenario: AEAD nonce comes from the injected Rng
|
||||
|
||||
- **WHEN** `Packer.pack(data, opts?)` is called with `encryption` set and
|
||||
an `Rng` whose `nextBytes(n)` returns a fixed byte pattern
|
||||
- **THEN** `result.metadata.nonce` equals the byte pattern truncated to
|
||||
the nonce length
|
||||
|
||||
#### Scenario: same inputs + same seed produce byte-identical output
|
||||
|
||||
- **WHEN** two `Packer.pack(data)` calls run with the same seeded `Rng`
|
||||
and the same fixed `Clock`
|
||||
- **THEN** their `data` and `metadata.integrity.entryDigest` are
|
||||
byte-equal
|
||||
|
||||
#### Scenario: no production file calls Date.now / Math.random / crypto.getRandomValues directly
|
||||
|
||||
- **WHEN** a recursive grep against `src/**/*.ts` (excluding `*.test.ts`,
|
||||
`*.types.ts`, `*.schemas.ts`) is run for `Date.now\|Math.random\|crypto.getRandomValues`
|
||||
- **THEN** zero matches exist outside the default-Clock / default-Rng
|
||||
factories in `src/runtime/runtime.ts`
|
||||
|
||||
**Verified by:** `src/packer/packer.spec-conformance.test.ts::"timestamp is sourced from the injected Clock"` AND `src/packer/packer.spec-conformance.test.ts::"AEAD nonce is sourced from the injected Rng"` AND `src/packer/packer.spec-conformance.test.ts::"two packs with identical inputs and seeded Rng produce byte-identical output"` AND `src/runtime/runtime.spec-conformance.test.ts::"no production file calls Date.now / Math.random / crypto.getRandomValues directly"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,95 @@
|
||||
# Tasks — Enterprise hardening (resilience cluster) for `@suaveplan/packer`
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/packer.md` end-to-end
|
||||
- [x] 1.2 Read `openspec/specs/packer/spec.md`
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
|
||||
- [x] 1.5 Fill in `## Source mapping` table — every requirement cites a line range
|
||||
- [x] 1.6 Draft `specs/packer/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 — Runtime defaults (R-4 foundation)
|
||||
|
||||
- [ ] 2.1 New `src/runtime/runtime.ts` — `defaultClock`, `defaultMonotonicClock`, `defaultRng`, `resolveRuntime(opts)` returning `{ clock, monotonicClock, rng }`.
|
||||
- [ ] 2.2 Sibling `.md`, `.types.ts`, `index.ts` barrel.
|
||||
- [ ] 2.3 Extend `PackerOptions`, `PackChunkedOptions`, `UnpackChunkedOptions` with `clock?: Clock`, `monotonicClock?: MonotonicClock`, `rng?: Rng`.
|
||||
|
||||
## 3. Implementation — Retry (R-1)
|
||||
|
||||
- [ ] 3.1 Add `@suaveplan/retry` to `packer/package.json` deps (workspace:^).
|
||||
- [ ] 3.2 New `src/retry/retryable-read.ts` — `retryRead(reader, opts)` wrapping `reader.read()` in `withRetry`.
|
||||
- [ ] 3.3 Extend `normalizeSource` to accept `retryConfig?` and route ReadableStream reads through `retryRead`.
|
||||
- [ ] 3.4 Extend `AbstractPacker.packStream`'s `collect` helper to accept and propagate `retryConfig?` to `normalizeSource`.
|
||||
- [ ] 3.5 Thread `retryConfig?` and `signal?` through `PackerOptions`, `PackChunkedOptions`, `UnpackChunkedOptions`.
|
||||
|
||||
## 4. Implementation — Idempotency (R-2)
|
||||
|
||||
- [ ] 4.1 Add `@suaveplan/idempotency` to `packer/package.json` deps (workspace:^).
|
||||
- [ ] 4.2 New `src/idempotency/idempotency.ts` — `PackIdempotencyGuard` consuming `IdempotencyStoreAdapter`, computing content-derived dedup key.
|
||||
- [ ] 4.3 Sibling `.md`, `.types.ts`, `index.ts` barrel.
|
||||
- [ ] 4.4 Add `idempotency?: { store, ttlMs?, enabled? }` to `PackerOptions`.
|
||||
- [ ] 4.5 `Packer.pack` short-circuits on cache hit before any pipeline work; populates store on miss.
|
||||
|
||||
## 5. Implementation — Whole-file AbortSignal (R-3)
|
||||
|
||||
- [ ] 5.1 Add `signal?: AbortSignal` to `UnpackOptions`.
|
||||
- [ ] 5.2 Add `PackOptions` interface (`{ filename?, signal? }`) on `types/packer.types.ts`.
|
||||
- [ ] 5.3 Overload `Packer.pack(data, filenameOrOpts?, opts?)` accepting either filename string or `PackOptions`.
|
||||
- [ ] 5.4 `Unpacker.unpack` checks `signal.aborted` at every yield point (5 places).
|
||||
- [ ] 5.5 `Packer.pack` checks `signal.aborted` at every yield point (5 places).
|
||||
- [ ] 5.6 `packChunks` checks `signal.aborted` between emitted chunks.
|
||||
- [ ] 5.7 New i18n catalog keys `packer.error.ABORTED.pack`, `packer.error.ABORTED.pack-chunks`.
|
||||
|
||||
## 6. Implementation — Clock / Rng audit-and-replace (R-4)
|
||||
|
||||
- [ ] 6.1 `AbstractPacker.buildMetadata` reads `timestamp` from `this.options.clock`.
|
||||
- [ ] 6.2 `packer/packer.ts` `randomBytes` replaced with `this.options.rng.nextBytes(n)`.
|
||||
- [ ] 6.3 All `nowMs()` helpers across the package route through `this.options.monotonicClock` (or resolved per-call for chunked async fns).
|
||||
- [ ] 6.4 Grep verify: zero `Date.now\|Math.random\|crypto.getRandomValues\|performance.now` outside `src/runtime/runtime.ts`.
|
||||
|
||||
## 7. Tests
|
||||
|
||||
- [ ] 7.1 `src/runtime/runtime.test.ts` + `.spec-conformance.test.ts` — pins defaults; grep audit.
|
||||
- [ ] 7.2 `src/retry/retryable-read.test.ts` + `.spec-conformance.test.ts` — pins retry attempts, abort mid-backoff, default single-attempt.
|
||||
- [ ] 7.3 `src/idempotency/idempotency.test.ts` + `.spec-conformance.test.ts` — pins hit / miss / disabled / custom store.
|
||||
- [ ] 7.4 `src/packer/packer.spec-conformance.test.ts` extended — abort, idempotency, clock, rng, determinism, legacy overload.
|
||||
- [ ] 7.5 `src/unpacker/unpacker.spec-conformance.test.ts` extended — pre-flight abort, mid-pipeline abort.
|
||||
- [ ] 7.6 `src/chunked/packer/packer.spec-conformance.test.ts` extended — mid-stream abort.
|
||||
- [ ] 7.7 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for async I/O).
|
||||
|
||||
## 8. Documentation
|
||||
|
||||
- [ ] 8.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples).
|
||||
- [ ] 8.2 Update `README.md` Modules list with `retry/`, `idempotency/`, `runtime/`.
|
||||
|
||||
## 9. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 9.1 `bun run check` — Biome 0/0/0.
|
||||
- [ ] 9.2 `bunx turbo lint typecheck test --filter=@suaveplan/packer` — green.
|
||||
- [ ] 9.3 `bunx turbo test:coverage --filter=@suaveplan/packer` — 100/100/100/100.
|
||||
- [ ] 9.4 `bun scripts/openspec-conformance.ts @suaveplan/packer` — exit 0.
|
||||
- [ ] 9.5 `bunx turbo build --filter=@suaveplan/packer` — green.
|
||||
|
||||
## 10. Consumer sweep
|
||||
|
||||
- [ ] 10.1 `bunx turbo lint typecheck test --filter=@suaveplan/drive-plugin-chunks --filter=@suaveplan/drive-webrtc` — green.
|
||||
|
||||
## 11. Pre-archive 9-row checklist
|
||||
|
||||
- [ ] 11.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim.
|
||||
- [ ] 11.2 Zod schemas wired at every declared boundary.
|
||||
- [ ] 11.3 Every error code in `error-codes.ts` thrown by at least one production path.
|
||||
- [ ] 11.4 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider` (NO `vi.mock`).
|
||||
- [ ] 11.5 No `export *` in feature barrels.
|
||||
- [ ] 11.6 No direct `vitest` / `bun:test` imports.
|
||||
- [ ] 11.7 No retroactive tick — every box above reflects work done in this change.
|
||||
- [ ] 11.8 Sibling `.md` for every new `.ts`.
|
||||
- [ ] 11.9 Items R-1, R-2, R-3, R-4 each have ≥1 production code path and ≥1 test pinning every named scenario.
|
||||
|
||||
## 12. Archive
|
||||
|
||||
- [ ] 12.1 Fold `specs/packer/spec.md` into `openspec/specs/packer/spec.md`.
|
||||
- [ ] 12.2 Move `openspec/changes/enterprise-packer-resilience/` → `openspec/archive/<yyyy-mm-dd>-enterprise-packer-resilience/`.
|
||||
- [ ] 12.3 Refresh `MANIFEST.md`.
|
||||
Reference in New Issue
Block a user