feat(phase9 #97 D10.e): cursor pagination contract + 18 unit tests#1712
Merged
feat(phase9 #97 D10.e): cursor pagination contract + 18 unit tests#1712
Conversation
added 2 commits
April 26, 2026 03:38
Per design pack §C (canonical post-#1710 SSoT): - aperag/mcp/cursor/codec.py: CursorPayload (sort_key + last_position + invariant_hash + issued_at + ttl_seconds 1h default + server_id + schema_version 1) + base64url JSON encode/decode + is_expired TTL check - aperag/mcp/cursor/invariants.py: compute_invariant_hash sha256 over (sort_key + filters + collection_id + tenant_id + index_id) deterministic across dict ordering - aperag/mcp/cursor/schemas.py: PaginationParams (cursor + limit conint 1..200) + PaginationResult[T] generic (items + next_cursor + total_count) - aperag/mcp/cursor/errors.py: 6 canonical snake_case codes (cursor_invalid / cursor_expired / cursor_filter_mismatch / cursor_tenant_mismatch / cursor_index_changed / cursor_schema_unsupported per §C.3 + #1710 amendment) + CursorError exception + CursorErrorEnvelope wire shape + SILENT_RESET_FORBIDDEN guard - aperag/mcp/cursor/__init__.py: public surface for D10.c read primitive imports tests/unit_test/mcp/test_cursor_contract.py: - 5 codec round-trip / wire format / TTL boundary tests - 2 invariant_hash determinism + binding sensitivity tests - 7 error envelope round-trip tests (parametrized over each canonical code) + SILENT_RESET_FORBIDDEN pin - 4 PaginationParams/PaginationResult shape tests including end-to-end cursor flow Pending D10.c stub head landing for `aperag/service/pagination.py` integration helper + `tests/e2e_http/hurl/<NN>_d10_pagination.hurl` cross-tool e2e — those are Window 1 work.
…_cursor Per Weston msg=cc4a3ab0 二线 CR blocker: decode_cursor() previously surfaced malformed / wrong-schema / expired wire payloads as bare ValueError / KeyError, leaving every D10.c / D10.d caller to re-derive the canonical mapping. That violates the §C.3 explicit-not-silent invariant by construction — any forgotten mapping silently degrades into ValueError → tool error → first-page restart, which is exactly the anti-pattern SILENT_RESET_FORBIDDEN guards against. Fix: - decode_cursor() now raises CursorError directly with the right canonical code: cursor_invalid (malformed wire / base64 / json / missing field), cursor_schema_unsupported (unknown schema_version), cursor_expired (past issued_at + ttl_seconds clock). - _decode_cursor_payload() preserved as a private structural-only decode for tests that need to craft expired / wrong-schema payloads to exercise the canonical error paths. - 3 new canonical-code tests + 1 internal-decode escape hatch test added; old raw-error test deleted (pre-#1710 wire shape no longer reachable through public surface). _payload() fixture's issued_at now defaults to current time so round-trip tests stay green when run far from the fixture's drafting date; tests that need expired / fixed payloads override explicitly. 21/21 tests pass; ruff check + format clean.
This was referenced Apr 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PaginationParams/PaginationResult[T]envelopes (per design pack §C, post-docs(modularization): D10 spec amendment — back-align §G D10.e cursor error codes to §C.3 canonical #1710 SSoT).Write set (per §G D10.e boundary)
aperag/mcp/cursor/__init__.pyaperag/mcp/cursor/codec.pyCursorPayload+encode_cursor/decode_cursor+is_expired(DEFAULT_TTL_SECONDS=3600per §C.4,CURSOR_SCHEMA_VERSION=1)aperag/mcp/cursor/invariants.pycompute_invariant_hashover (sort_key + filters + collection_id + tenant_id + index_id)aperag/mcp/cursor/schemas.pyPaginationParams(cursor + limit conint 1..200) +PaginationResult[T]genericaperag/mcp/cursor/errors.pyCursorError+CursorErrorEnvelopePydantic +SILENT_RESET_FORBIDDEN=Trueanti-pattern guardtests/unit_test/mcp/test_cursor_contract.pyDeferred to follow-up (post-#1711 merge)
Per §G D10.e write-set the lane also includes:
aperag/service/pagination.py— seek-pagination integration helper that wraps cursor encode/decode against D10.c list_collections / list_documents / read_document_chunk ORM queriestests/e2e_http/hurl/<NN>_d10_pagination.hurl— cross-tool e2e flowBoth depend on D10.c's actual primitive implementations landing (currently stub-only on #1711), so they ride a follow-up PR on the same task lane after #1711 merges. Splitting keeps the cursor contract reviewable in isolation without coupling to D10.c implementation churn.
Cross-lane discipline (per architect msg=669db73c)
cursor_invalid/cursor_expired/cursor_filter_mismatch/cursor_tenant_mismatch/cursor_index_changed/cursor_schema_unsupported. No SCREAMING_SNAKE / collapsed-mismatch /CURSOR_FOREIGN/CURSOR_PAGE_OUT_OF_RANGEretained.SILENT_RESET_FORBIDDEN = Truepins the §C.3 anti-pattern: a server that quietly resets to first page on cursor failure is forbidden — the explicit-not-silent contract is loud-fail.Test plan
uv run pytest tests/unit_test/mcp/test_cursor_contract.py -v— 18 passed in 0.72suv run ruff check aperag/mcp/cursor tests/unit_test/mcp/test_cursor_contract.py— cleanuv run ruff format --check— clean🤖 Generated with Claude Code