Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/pagination-total-count-honesty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
---

Extends `pagination_integrity` with count-honesty assertions that complement the cursor↔has_more invariant. Each page now asserts `query_summary.total_matching = 3`, `query_summary.returned` matches the slice (2 then 1), and `pagination.total_count` equals 3 when volunteered (`field_value_or_absent` so omitting it stays conformant per the schema).

Catches the dishonest pagination class where an agent honors `max_results` and the cursor handshake but lies in the summary numbers — under-reporting `total_count` to hide inventory the same way a dishonest `has_more: false` would, or drifting `total_matching` between pages. Verified by spot-flipping the training agent's `total_count` to the page-local count: page-1 assertion fires with the expected diagnostic.
40 changes: 35 additions & 5 deletions static/compliance/source/universal/pagination-integrity.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ narrative: |
carries a stale cursor onto the terminal page fails the second-page
assertion.

The `total_count` field is intentionally not checked. The schema permits
agents to omit it (some backends cannot compute it cheaply) and asserting
on it would conflate honesty about what's still on the wire with honesty
about an upstream platform's full result set — only the former is
observable from the caller's seat.
The `total_count` field is allowed to be absent (some backends cannot
compute it cheaply per the pagination-response schema) but when an agent
volunteers it, the value MUST match the seeded fixture count of three.
This is the count-honesty half of the invariant: an agent that returns
`total_count: 2` while three creatives are seeded is hiding inventory
the way `has_more: false` would, just on a different field.
`query_summary.total_matching` is required by the response schema and
asserted unconditionally; `query_summary.returned` MUST equal the size
of each page's slice (2 on the continuation page, 1 on the terminal
page) — drift here flags an agent whose summary numbers don't match
what it actually emitted.

agent:
interaction_model: stateful_preloaded
Expand Down Expand Up @@ -164,6 +170,18 @@ phases:
- check: field_present
path: "pagination.cursor"
description: "has_more=true requires a cursor — without one the caller cannot continue"
- check: field_value
path: "query_summary.total_matching"
value: 3
description: "Three fixtures seeded — total_matching MUST reflect the full result set, not just this page"
- check: field_value
path: "query_summary.returned"
value: 2
description: "max_results=2 capped this page at 2 items — query_summary.returned MUST match the slice"
- check: field_value_or_absent
path: "pagination.total_count"
allowed_values: [3]
description: "If volunteered, total_count MUST equal the seeded set size — under-reporting hides inventory the same way a dishonest has_more does"

- check: field_present
path: "context"
Expand Down Expand Up @@ -223,6 +241,18 @@ phases:
path: "pagination.cursor"
allowed_values: [null]
description: "Terminal page MUST omit cursor (null also accepted for clients that explicitly clear the field)"
- check: field_value
path: "query_summary.total_matching"
value: 3
description: "total_matching MUST stay stable across pages — drift between pages is a query-summary bug"
- check: field_value
path: "query_summary.returned"
value: 1
description: "One creative left after the first page returned two — query_summary.returned MUST match the slice"
- check: field_value_or_absent
path: "pagination.total_count"
allowed_values: [3]
description: "total_count (when volunteered) MUST stay stable across pages and equal the seeded set size"

- check: field_present
path: "context"
Expand Down
Loading