6.4 KiB
6.4 KiB
OpenSpec Change: pagination-spec-conformance
- Change ID: pagination-spec-conformance
- Package:
@suaveplan/pagination - Status: PROPOSED
- Date: 2026-04-26
Summary
Bring openspec/specs/pagination/spec.md into agreement with the shipped implementation, add **Verified by:** lines to every requirement, and rectify the retroactively ticked historical hazard so bun scripts/openspec-conformance.ts @suaveplan/pagination exits 0. The implementation is solid (100/100/100/100 coverage, property-based tests, OTel instrumentation, AbortSignal throughout); the original spec was authored ahead of implementation and several MUST clauses drifted from the chosen behaviour. This change reconciles wording with reality and pins three previously-implicit scenarios in a new cursor-pagination.spec-conformance.test.ts file. No public-API changes; no breaking changes for consumers.
Current state — drift inventory
| Spec ID | Drift | Resolution |
|---|---|---|
| PG-01 | Spec says malformed cursor MUST throw INVALID_CURSOR; impl returns null from decodeCursor so callers can branch without catching. validateCursor returns false. |
Reword: malformed cursor returns null from decode and false from validate; throws are reserved for caller-driven error paths. |
| PG-02 | Spec says normalizeCursorRequest MUST throw INVALID_LIMIT; impl clamps limit to [1, maxLimit] and floors fractional values. |
Reword: normalisation clamps + floors; structured throws are reserved for cursor decode failures inside fetchAfter/fetchBefore when the caller provides an aborted signal. |
| PG-03 | Spec calls calculateCursorMeta(opts) with pageInfo/edges; impl is (edges, hasMore, direction?, totalCount?). |
Reword to match impl signature; semantics (forward/backward hasNextPage/hasPreviousPage) preserved. |
| PG-04 | Spec signature fetchAfter(adapter, cursor, opts); impl is fetchAfter(adapter, cursor, opts, getSortValue) (extractor required because adapters are field-agnostic). |
Reword: extractor is part of the signature; pre- and post-fetch abort checks throw PAGINATION.ABORTED. |
| PG-05 | Spec says throw on page<1 / limit≤0; impl clamps + floors with sensible defaults from PaginationDefaults. |
Reword: clamping is the contract; throws are not part of normalisation. pageToOffset/offsetToPage round-trip property is asserted by tests/property.test.ts. |
| PG-06 | Spec calls paginateArray(array, opts) and references currentPage/nextPage/previousPage meta fields; impl is paginateArray(items, page, limit) and meta is { page, limit, total, totalPages, hasNext, hasPrevious }. |
Reword to match impl; out-of-bounds page returns empty data with correct meta — pinned by new spec-conformance test. |
| PG-07 | Spec says mergeOffsetResponses returns a full OffsetPaginatedResponse<T>; impl returns { data, total } (the merged total is taken from the first response — the merged window has no single page/limit). |
Reword: merge returns { data: T[]; total: number }; consumers wrap with createOffsetResponse if they need the full envelope. |
| PG-08 | Matches impl. | Annotate. Stability is pinned by new spec-conformance test. |
| PG-09 | Spec calls keysetPaginate(array, opts); impl is keysetPaginate(items, lastKeys, opts) (lastKeys is an explicit parameter, not nested in opts, because null first-page is the common case). |
Reword to match impl. |
| PG-10 | Spec says decodePageToken MUST throw INVALID_TOKEN on bad structure; impl returns null (consistent with cursor / keyset decode paths). hashFilters is order-independent (sorted-key concatenation). |
Reword: invalid → null; INVALID_TOKEN is reserved for the schema-layer PageTokenPayloadSchema.parse boundary. |
| PG-11 | Spec says encodePageToken accepts clock?: Clock; impl exposes clock? on CursorPaginationOptions for adapter-backed fetchers but the encode path is timestamp-free (tokens carry value/sort/filter only). |
Reword: injectable Clock lives on CursorPaginationOptions and adapter helpers; tokens themselves carry no timestamp so no clock is needed inside encodePageToken. |
| PG-12 | Matches impl. | Annotate. |
| PG-13 | Spec lists count, find, findOne on PaginationAdapter<T>; impl exposes count + fetchPage(offset, limit, sort?, filter?) (the two methods every cursor/offset/keyset path actually needs). |
Reword to match impl. findOne is removed from the interface; consumers compose it from fetchPage(0, 1, ...) if they need it. |
| PG-14 | Matches impl, but every schema is consumer-facing; flag as // schema: boundary-only to suppress conformance-script warnings. |
Annotate; add boundary-only marker to pagination.schemas.ts. |
| PG-15 | Matches impl. | Annotate. |
| PG-16 | Matches impl (pagination.request.total, pagination.page.size, pagination.encode_decode.duration_ms). |
Annotate. |
| PG-17 | Matches impl (tests/property.test.ts). |
Annotate. |
| PG-18 | Spec says three separate bench files; impl ships a single consolidated pagination.bench.ts covering all three round-trips. |
Reword: consolidated benchmark file is acceptable; baseline numbers belong in README. |
| PG-19 | Matches impl. | Annotate. |
| PG-20 | Matches impl. | Annotate. |
What changes
- Rewrite each requirement body in
openspec/changes/pagination-spec-conformance/specs/pagination/spec.mdunder## MODIFIED Requirements, mirroring the impl. - Append a
**Verified by:**line to every requirement (PG-01..PG-20). - Add
src/cursor/cursor-pagination.spec-conformance.test.tswith four pinned scenarios:- PG-01 cursor encode/decode preserves value type (string vs number).
- PG-08 keyset cursor stability (idempotent encode + decode∘encode∘decode fixed point).
- PG-06
paginateArraynever throws for out-of-bounds pages. - PG-03
Connection<T>/Edge<T>generics are threaded throughcreateConnection/createEdges(type-level viaexpectTypeOf).
- Add
// schema: boundary-onlymarkers tosrc/schemas/pagination.schemas.tsso the conformance script suppresses the schemas-at-boundary warnings (every schema is consumer-facing). - Re-run
bun scripts/openspec-conformance.ts @suaveplan/paginationand confirm exit 0.
Impact
- Public API unchanged.
- One new test file (~110 lines).
openspec/specs/pagination/spec.mdrequirement bodies reworded to match impl; 20**Verified by:**lines added.- One marker comment added to the schemas file.