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,89 @@
# Pagination streaming
**Package**: `@suaveplan/pagination`
**Status**: PROPOSED
**Type**: feature
## Why
The genesis port of `@suaveplan/pagination` ships every cursor / offset / keyset primitive plus iterator helpers (`createCursorIterator`, `createOffsetIterator`, `collectAll`, `collectUpTo`, `mapPaginated`), but it omits the upstream streaming helper that drives an `AsyncIterable<T>` directly off a `PaginationAdapter`. Upstream's `streaming/stream-pagination.ts` (164 L) ships:
- `paginate(adapter, options)` — drains pages from a `PaginationAdapter<T>`, yielding individual items, fetching the next page only when the current page is exhausted (one-page-in-memory invariant).
- `collectStream(iterable)` — drains a streaming paginator into an array.
- `countStream(iterable)` — counts items without retaining them.
- `takeFromStream(iterable, count)` — bounded take that stops fetching once enough items are collected.
- `filterStream(iterable, predicate)` — lazy filter passthrough.
- `mapStream(iterable, mapper)` — lazy map passthrough.
Genesis already has `MemoryPaginationAdapter` matching the upstream `PaginationAdapter` shape (`fetchPage(offset, pageSize, sort?, filter?)`, `count(filter?)`), so the streaming layer slots in cleanly without altering the adapter contract. The gap costs every consumer that wants to drain a paginator — they currently hand-roll the offset-loop or accept loading the entire dataset via `collectAll(createOffsetIterator(...))` (which holds `pageSize` records at a time but exposes pages, not items). The streaming surface is the canonical one upstream consumers reach for.
This change adds the streaming module, wires `PaginationError` codes for any new error paths (no expected new codes in the happy path), and ships the four operator helpers (`collect`, `count`, `take`, `filter`, `map`) so the surface matches upstream byte-for-byte.
## What changes
- ADDED: `streaming/stream-pagination` module — exports `streamPaginated<T>(adapter: PaginationAdapter<T>, options?: StreamPaginationOptions): AsyncIterable<T>` (renamed from upstream `paginate` to avoid collision with `paginateArray`).
- ADDED: `collectStream`, `countStream`, `takeFromStream`, `filterStream`, `mapStream` operator helpers — match upstream signatures, exported from the same module.
- ADDED: `StreamPaginationOptions` type — `{ pageSize?: number; filter?: FilterConfig; sort?: SortConfig; signal?: AbortSignal }`. Upstream omitted `signal`; genesis adds it because every other iterator in the package honours `AbortSignal` (PG-12).
- ADDED: `streamPaginated` honours `options.signal` — checks `signal.aborted` before each `adapter.fetchPage` call AND between yielding items; throws `PaginationError(PAGINATION.ABORTED)` on cancellation.
- ADDED: `streamPaginated` defaults `pageSize` to 50 (matches upstream constant `DEFAULT_PAGE_SIZE`).
- ADDED: subpath export `./streaming/stream-pagination` in `package.json` for tree-shaking.
- ADDED: `streaming/stream-pagination.md` sibling doc with basic + advanced examples.
## Out of scope
- **Web-Streams `ReadableStream` adapter** — wrapping an `AsyncIterable<T>` as a `ReadableStream<T>` is a separate change once this base iterable lands. It needs `ReadableStreamDefaultController` semantics and a backpressure strategy that's worth its own design pass.
- **Reverse iteration** — descending traversal from the end of the dataset is a separate concern; upstream doesn't ship it either.
- **Cursor / keyset streaming** — `streamPaginated` here is offset-driven (matches upstream). Cursor-streaming is already covered by `createCursorIterator` + `collectAll`; keyset-streaming is separately tracked by an existing PG-09 requirement.
- **Multi-adapter parallel streaming** — fanning out across multiple adapters (e.g. shard-aware streaming) needs an orchestrator; not in scope.
- **Backpressure-aware push streaming** — the iterable here yields on `next()` (pull model). Push-style streaming (with consumer rate-limit) is its own change.
## Source mapping
This change derives from the upstream package at `../suaveplan/packages/universal/pagination/src/streaming/stream-pagination.ts` and the upstream enterprise audit at `../suaveplan/enterprise/pagination.md`.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `streamPaginated(adapter, options)` AsyncIterable | `packages/universal/pagination/src/streaming/stream-pagination.ts` | L17-L78 | Renamed from upstream `paginate` to `streamPaginated` to avoid collision with the existing `paginateArray` in `@suaveplan/pagination`; behavior preserved byte-for-byte (default `pageSize=50`, single-page-in-memory invariant, lazy-init on first `next()`) |
| `collectStream(iterable)` | `packages/universal/pagination/src/streaming/stream-pagination.ts` | L84-L92 | Verbatim |
| `countStream(iterable)` | `packages/universal/pagination/src/streaming/stream-pagination.ts` | L97-L106 | Verbatim |
| `takeFromStream(iterable, count)` | `packages/universal/pagination/src/streaming/stream-pagination.ts` | L112-L122 | Verbatim |
| `filterStream(iterable, predicate)` | `packages/universal/pagination/src/streaming/stream-pagination.ts` | L128-L148 | Verbatim |
| `mapStream(iterable, mapper)` | `packages/universal/pagination/src/streaming/stream-pagination.ts` | L154-L164 | Verbatim |
| `StreamPaginationOptions` (`pageSize`, `filter`, `sort`) | `packages/universal/pagination/src/types/pagination.types.ts` | L166-L170 | Verbatim port of three fields; genesis adds `signal?: AbortSignal` (see "no source line") |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `StreamPaginationOptions.signal?: AbortSignal` | Genesis-wide AbortSignal rule applied across the package (existing PG-12 already mandates `signal` on `createCursorIterator` / `createOffsetIterator` / `collectAll` / `collectUpTo`). The streaming helper inherits the same contract; not adding it would create the only iterator in the package that ignores cancellation. |
| `streamPaginated` throws `PaginationError(PAGINATION.ABORTED)` on signal abort | Same justification — matches PG-12 behavior on the other iterators. |
| `streamPaginated` named (not `paginate`) | The existing genesis surface already exports `paginateArray` from `./offset/offset` and `cursorPaginateArray` from `./cursor/cursor`. A bare `paginate` would clash with consumer expectations (which import name is the "main" one?). `streamPaginated` is unambiguous — verb-prefix matches the symmetry with `createCursorIterator` / `createOffsetIterator`. |
## Verification approach
Each spec delta requirement carries a `**Verified by:**` line. The test files that will exist after this change ships:
- `packages/data/pagination/src/streaming/stream-pagination.test.ts` — drains `MemoryPaginationAdapter` with various `pageSize` settings; one-page-in-memory invariant verified via instrumentation; default `pageSize=50` honoured; `filter` / `sort` forwarded; lazy-init pattern (no fetch before first `next()`)
- `packages/data/pagination/src/streaming/stream-pagination.spec-conformance.test.ts` — `signal.aborted` before first `fetchPage` throws `PaginationError(PAGINATION.ABORTED)` without calling `fetchPage`; abort during in-flight `fetchPage` propagates rejection; abort between item yields throws on next `next()`; `expectTypeOf<StreamPaginationOptions>` pinned
- `packages/data/pagination/src/streaming/stream-pagination.test.ts` (operators) — `collectStream`, `countStream`, `takeFromStream`, `filterStream`, `mapStream` happy paths AND short-circuit / lazy-fetch behaviors (`takeFromStream(iter, 5)` does NOT fetch beyond the page that contains item 5)
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/pagination` to enumerate. Today `grep -rl "@suaveplan/pagination" packages/ --include="package.json"` returns:
- `packages/platform/search/package.json`
- `packages/data/pagination/package.json` (self)
So `@suaveplan/search` is the only in-repo dependent.
- `@suaveplan/pagination` (self) — [x] tests run / [x] docs reviewed / [ ] no action needed
- `@suaveplan/search` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: `streamPaginated` is purely additive — `@suaveplan/search` consumes `cursorPaginateArray` and `createCursorResponse` only; no public-surface change touches its imports.)
If `tests run` is unchecked for any dependent, the change MUST NOT be folded into the canonical spec — finish that work first or split the proposal.
## Migration impact
- **All existing pagination consumers** — no break. `streamPaginated` and the operator helpers are purely additive new exports. No existing surface is renamed, removed, or has its semantics changed.
- **Existing iterator consumers** (`createCursorIterator`, `createOffsetIterator`, `collectAll`, `collectUpTo`, `mapPaginated`) — no break. They continue to expose pages-of-T whereas `streamPaginated` exposes individual T. Both styles co-exist; consumers pick whichever fits their iteration model.

View File

@@ -0,0 +1,106 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: pagination (pagination-streaming)
**Package**: `@suaveplan/pagination`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
>
> Adds PG-14 and PG-15 to the canonical capability spec at `openspec/specs/pagination/spec.md`.
## ADDED Requirements
### Requirement: Streaming pagination iterable over PaginationAdapter
The package SHALL export `streamPaginated<T>(adapter: PaginationAdapter<T>, options?: StreamPaginationOptions): AsyncIterable<T>`. The returned iterable SHALL drain `adapter.fetchPage(offset, pageSize, sort?, filter?)` lazily — fetching the next page only when the current page is exhausted — so memory use is bounded by `pageSize`. `StreamPaginationOptions` SHALL be `{ pageSize?: number; filter?: FilterConfig; sort?: SortConfig; signal?: AbortSignal }`. When `pageSize` is omitted, the iterable SHALL use `DEFAULT_STREAM_PAGE_SIZE` (50). When `signal` is supplied and aborted, `next()` SHALL throw `PaginationError` with code `PAGINATION.ABORTED` without calling `adapter.fetchPage`.
#### Scenario: drains items from MemoryPaginationAdapter
- **WHEN** `streamPaginated(adapter, { pageSize: 10 })` is iterated and `adapter` holds 100 items
- **THEN** the consumer receives all 100 items in order AND `adapter.fetchPage` is called exactly 11 times (10 full pages + one empty/partial page to detect end)
#### Scenario: defaults pageSize to 50 when omitted
- **WHEN** `streamPaginated(adapter)` is iterated with no options
- **THEN** every call to `adapter.fetchPage` is made with `pageSize: 50`
#### Scenario: holds at most one page in memory
- **WHEN** the iterable is mid-drain and instrumentation observes the internal `currentPage` array length
- **THEN** the length is always less than or equal to `pageSize`
#### Scenario: lazy — no fetch before first next()
- **WHEN** `streamPaginated(adapter)` is constructed but `Symbol.asyncIterator` is not invoked
- **THEN** `adapter.fetchPage` has been called zero times
#### Scenario: stops when partial page returned
- **WHEN** the adapter returns a page whose length is less than `pageSize`
- **THEN** the iterable yields the remaining items and then completes (no further `fetchPage` calls)
#### Scenario: forwards filter and sort to adapter.fetchPage
- **WHEN** `streamPaginated(adapter, { pageSize: 10, filter: { status: "active" }, sort: { field: "id", direction: "asc" } })` is iterated
- **THEN** every `adapter.fetchPage` call is made with the same `filter` and `sort` arguments byte-for-byte
#### Scenario: signal already aborted before first next()
- **WHEN** `signal.aborted === true` at iterator construction and `next()` is called
- **THEN** the call throws `PaginationError` with code `PAGINATION.ABORTED` AND `adapter.fetchPage` is NEVER called
#### Scenario: signal aborted between yields
- **WHEN** the consumer aborts the signal between two `next()` calls mid-page
- **THEN** the next `next()` call throws `PaginationError` with code `PAGINATION.ABORTED`
**Verified by:** `packages/data/pagination/src/streaming/stream-pagination.test.ts::"streams items from MemoryPaginationAdapter"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"defaults pageSize to 50 when omitted"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"holds at most one page in memory"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"is lazy — no fetch before first next()"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"stops when partial page returned"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"forwards filter and sort to adapter.fetchPage"` AND `packages/data/pagination/src/streaming/stream-pagination.spec-conformance.test.ts::"throws PAGINATION.ABORTED before first fetchPage when signal already aborted"` AND `packages/data/pagination/src/streaming/stream-pagination.spec-conformance.test.ts::"throws PAGINATION.ABORTED on next() after signal abort between yields"`
---
### Requirement: Streaming operator helpers — collect, count, take, filter, map
The package SHALL export five helper functions over `AsyncIterable<T>` produced by `streamPaginated`:
- `collectStream<T>(iterable: AsyncIterable<T>): Promise<T[]>` — drains the entire iterable into an array.
- `countStream<T>(iterable: AsyncIterable<T>): Promise<number>` — counts items without retaining them.
- `takeFromStream<T>(iterable: AsyncIterable<T>, count: number): Promise<T[]>` — collects the first `count` items, short-circuiting once enough are gathered.
- `filterStream<T>(iterable: AsyncIterable<T>, predicate: (item: T) => boolean): AsyncIterable<T>` — produces a new lazy iterable yielding only items matching `predicate`.
- `mapStream<T, U>(iterable: AsyncIterable<T>, mapper: (item: T) => U): AsyncIterable<U>` — produces a new lazy iterable yielding `mapper(item)` for each input item.
`takeFromStream` SHALL stop pulling items as soon as `count` items have been collected — the underlying iterable's pagination SHALL NOT continue beyond what was needed. `filterStream` and `mapStream` SHALL be lazy — they SHALL NOT pre-fetch or pre-compute beyond what consumers `next()` request. AbortSignal cancellation propagates naturally through the `for await` chain — these helpers do not introduce a separate signal parameter.
#### Scenario: collectStream drains all items
- **WHEN** `collectStream(streamPaginated(adapter))` is awaited against a 100-item adapter
- **THEN** the result is an array of 100 items in order
#### Scenario: countStream returns total without retention
- **WHEN** `countStream(streamPaginated(adapter))` is awaited against a 100-item adapter
- **THEN** the result is `100` AND no array of size 100 is held in memory at any point
#### Scenario: takeFromStream short-circuits at count
- **WHEN** `takeFromStream(streamPaginated(adapter, { pageSize: 10 }), 5)` is awaited against a 100-item adapter
- **THEN** the result is the first 5 items AND `adapter.fetchPage` is called exactly once
#### Scenario: takeFromStream returns empty for count <= 0
- **WHEN** `takeFromStream(iter, 0)` is awaited
- **THEN** the result is `[]` AND `iter` is not advanced at all
#### Scenario: filterStream is lazy
- **WHEN** `filterStream(streamPaginated(adapter), (x) => x.id % 2 === 0)` is iterated and only the first matching item is consumed
- **THEN** the underlying adapter is queried at most enough times to find that one item — not the entire dataset
#### Scenario: mapStream is lazy
- **WHEN** `mapStream(streamPaginated(adapter), (x) => x.id)` is iterated for two items
- **THEN** the mapper is called exactly twice — not once per item in the underlying adapter
**Verified by:** `packages/data/pagination/src/streaming/stream-pagination.test.ts::"collectStream drains all items"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"countStream returns total without retention"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"takeFromStream short-circuits at count"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"takeFromStream returns empty for count <= 0"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"filterStream is lazy and passthrough"` AND `packages/data/pagination/src/streaming/stream-pagination.test.ts::"mapStream is lazy and passthrough"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,102 @@
# Tasks — Pagination streaming
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## Audit Note (2026-05-02)
**Minimal progress** — DEFERRED, do not archive.
- No `packages/data/pagination/src/streaming/` directory exists. Existing dirs: `cursor`, `keyset`, `offset`, `helpers`, `adapters`, `tokens`, `errors`, `otel`, `schemas`, `types`.
- `grep -rln "streamPaginated\|stream-pagination\|collectStream\|countStream\|takeFromStream\|filterStream\|mapStream" packages/data/pagination/src/` returns zero hits.
- `bunx turbo lint typecheck test --filter=@suaveplan/pagination` is GREEN — but the streaming module is not implemented.
- All 57 tasks remain: `streamPaginated<T>` AsyncIterable, five operator helpers (`collectStream`, `countStream`, `takeFromStream`, `filterStream`, `mapStream`), `DEFAULT_STREAM_PAGE_SIZE`, AbortSignal contract, subpath export, sibling `.md`, full test matrix.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/enterprise/pagination.md` end-to-end
- [ ] 1.2 Read `openspec/specs/pagination/spec.md` (already exists — confirm PG-12 AbortSignal contract still aligned)
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
- [ ] 1.6 Draft `specs/pagination/spec.md` from `openspec/templates/spec-delta.md` — note: this is a delta against the existing canonical spec, so add as ADDED Requirements (PG-14, PG-15) numbered after the current PG-13
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
## 2. Implementation — `streaming/stream-pagination`
- [ ] 2.1 Create `packages/data/pagination/src/streaming/stream-pagination.ts` exporting `streamPaginated<T>(adapter, options?): AsyncIterable<T>`
- [ ] 2.2 Default `pageSize` to 50 (constant `DEFAULT_STREAM_PAGE_SIZE`); export the constant from `constants/index.ts`
- [ ] 2.3 Honour the one-page-in-memory invariant — lazy-init on first `next()`; fetch next page only when current page exhausted; mark exhausted when `currentPage.length < pageSize`
- [ ] 2.4 Forward `options.filter` and `options.sort` to `adapter.fetchPage(offset, pageSize, sort, filter)` byte-for-byte with upstream
- [ ] 2.5 Honour `options.signal` — check `signal.aborted` before each `fetchPage` AND before yielding each item; on abort throw `PaginationError(PAGINATION.ABORTED)`
- [ ] 2.6 Add `StreamPaginationOptions` type (`pageSize`, `filter`, `sort`, `signal`) to `types/pagination.types.ts`
- [ ] 2.7 Add `streaming/stream-pagination.md` sibling doc (≥200 words, basic + advanced examples)
## 3. Implementation — operator helpers
- [ ] 3.1 Add `collectStream<T>(iterable): Promise<T[]>` — drains; verify it does NOT short-circuit
- [ ] 3.2 Add `countStream<T>(iterable): Promise<number>` — counts without retaining
- [ ] 3.3 Add `takeFromStream<T>(iterable, count): Promise<T[]>` — short-circuits once `items.length >= count`
- [ ] 3.4 Add `filterStream<T>(iterable, predicate): AsyncIterable<T>` — lazy filter passthrough
- [ ] 3.5 Add `mapStream<T, U>(iterable, mapper): AsyncIterable<U>` — lazy map passthrough
- [ ] 3.6 All five helpers honour `AbortSignal` indirectly via the upstream iterable (no new signal parameter; cancellation propagates through the `for await` chain)
## 4. Subpath exports + barrel
- [ ] 4.1 Add `"./streaming/stream-pagination"` to `packages/data/pagination/package.json` exports map (matching existing per-feature subpath pattern)
- [ ] 4.2 Re-export `streamPaginated`, `collectStream`, `countStream`, `takeFromStream`, `filterStream`, `mapStream`, `StreamPaginationOptions`, `DEFAULT_STREAM_PAGE_SIZE` from `src/index.ts`
## 5. Tests
- [ ] 5.1 `streaming/stream-pagination.test.ts::"streams items from MemoryPaginationAdapter"` — basic round-trip over 100 items with `pageSize=10`
- [ ] 5.2 `streaming/stream-pagination.test.ts::"defaults pageSize to 50 when omitted"` — instrument `fetchPage` and assert called with `pageSize=50`
- [ ] 5.3 `streaming/stream-pagination.test.ts::"holds at most one page in memory"` — instrument iterator state; verify `currentPage` size ≤ `pageSize` at every step
- [ ] 5.4 `streaming/stream-pagination.test.ts::"forwards filter and sort to adapter.fetchPage"` — assert call args
- [ ] 5.5 `streaming/stream-pagination.test.ts::"is lazy — no fetch before first next()"` — instrument; verify zero calls before first `next()`
- [ ] 5.6 `streaming/stream-pagination.test.ts::"stops when partial page returned"` — verify exhausted-on-partial behavior
- [ ] 5.7 `streaming/stream-pagination.spec-conformance.test.ts::"throws PAGINATION.ABORTED before first fetchPage when signal already aborted"` — verify `fetchPage` is NOT called
- [ ] 5.8 `streaming/stream-pagination.spec-conformance.test.ts::"throws PAGINATION.ABORTED on next() after signal abort between yields"` — abort mid-iteration
- [ ] 5.9 `streaming/stream-pagination.spec-conformance.test.ts::"propagates rejection when signal aborts in-flight fetchPage"` — adapter aware of signal rejects naturally
- [ ] 5.10 `streaming/stream-pagination.spec-conformance.test.ts::"pins StreamPaginationOptions shape via expectTypeOf"` — type-level assertion
- [ ] 5.11 `streaming/stream-pagination.test.ts::"collectStream drains all items"` — happy path
- [ ] 5.12 `streaming/stream-pagination.test.ts::"countStream returns total without retention"` — verify result is correct count
- [ ] 5.13 `streaming/stream-pagination.test.ts::"takeFromStream short-circuits at count"` — instrument fetches; verify only enough pages fetched to satisfy take
- [ ] 5.14 `streaming/stream-pagination.test.ts::"filterStream is lazy and passthrough"` — verify items skipped don't pass through
- [ ] 5.15 `streaming/stream-pagination.test.ts::"mapStream is lazy and passthrough"` — verify mapper called once per yielded item
- [ ] 5.16 Every `it(...)` in 5.1–5.15 carries `{ timeout: 5000 }`
## 6. Documentation
- [ ] 6.1 Sibling `.md` for `stream-pagination.ts` (≥200 words, basic + advanced examples)
- [ ] 6.2 Update `packages/data/pagination/README.md` Modules section — add link to streaming module; add Quick-start snippet showing `streamPaginated` + `for await`
- [ ] 6.3 Update `CLAUDE.md` package listing line for `@suaveplan/pagination` to mention streaming
## 7. Pre-archive verification (the 5-gate triple)
- [ ] 7.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
- [ ] 7.2 `bunx turbo lint typecheck test --filter=@suaveplan/pagination` — green
- [ ] 7.3 `bunx turbo test:coverage --filter=@suaveplan/pagination` — 100/100/100/100
- [ ] 7.4 `bun scripts/openspec-conformance.ts @suaveplan/pagination` — exit 0, no `error` findings
- [ ] 7.5 `bunx turbo build --filter=@suaveplan/pagination` — green
- [ ] 7.6 Dependent sweep: `bunx turbo test --filter=...@suaveplan/pagination` (covers `@suaveplan/search`)
## 8. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 8.1 Generics threaded through `streamPaginated<T>`, `mapStream<T, U>`, `filterStream<T>`, etc.
- [ ] 8.2 Zod schemas wired at the `StreamPaginationOptions` boundary if exposed across a network edge (likely no — purely in-memory contract)
- [ ] 8.3 `PAGINATION.ABORTED` thrown by at least one production path (the streaming helper)
- [ ] 8.4 Strict-mode behaviors tested both on and off (signal supplied vs absent)
- [ ] 8.5 OTel instruments emitted (page-fetch counter, page-fetch duration histogram) and asserted with a real in-memory `MeterProvider`
- [ ] 8.6 Lifecycle dedup not applicable here — streaming is single-iterator instances, no shared registry
- [ ] 8.7 No retroactive tick — every box above reflects work done in this change
## 9. Archive
- [ ] 9.1 Fold spec delta into `openspec/specs/pagination/spec.md` — add as PG-14 (streaming pagination contract) and PG-15 (streaming operator helpers)
- [ ] 9.2 Move `openspec/changes/pagination-streaming/` → `openspec/archive/<yyyy-mm-dd>-pagination-streaming/`
- [ ] 9.3 Refresh `MANIFEST.md` if package set or deps changed (no expected change here)