Skip to content

feat(compliance): pagination total_count honesty assertions#3100

Merged
bokelley merged 1 commit into
mainfrom
bokelley/total-count-monotonic
Apr 25, 2026
Merged

feat(compliance): pagination total_count honesty assertions#3100
bokelley merged 1 commit into
mainfrom
bokelley/total-count-monotonic

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Extends pagination_integrity with three runtime assertions per page:

  • query_summary.total_matching = 3 (always — schema requires the field)
  • query_summary.returned matches the slice (2 on continuation, 1 on terminal)
  • pagination.total_count = 3 when volunteered (field_value_or_absent preserves the schema's optional stance)

Why

Closes the deferred follow-up from #3095. The protocol reviewer flagged that the cursor↔has_more storyboard tested wire-level honesty but didn't exercise count-level honesty. An agent can honor max_results and the cursor handshake while still lying in the summary — under-reporting total_count to hide inventory the same way a dishonest has_more: false would, or drifting total_matching across pages.

These assertions are pure internal-consistency checks: they don't require upstream visibility (the storyboard knows the seeded set is 3) so they catch the dishonesty without conflating wire honesty with platform honesty. total_count stays optional via field_value_or_absent allowed_values: [3] — agents whose backends can't compute it cheaply remain conformant; agents that volunteer it must tell the truth.

Verified

Spot-checked the assertion fires correctly by flipping the training agent's total_count from totalMatching (the truthful 3) to pageCreatives.length (the dishonest page-local 2). Page-1 assertion fired with diagnostic:

× first_page: If volunteered, total_count MUST equal the seeded set size — under-reporting hides inventory the same way a dishonest has_more does — Expected absent or one of [3], got 2

Reverted the agent — clean run again.

Test plan

  • npm run build:compliance — pagination-invariant lint and 7 storyboard lints clean
  • npm run test:pagination-invariant — 16/16 tests pass
  • npm run typecheck — clean
  • Pre-commit unit suite — 820/820 tests pass
  • pagination_integrity against training agent: 6/6 steps clean
  • Negative test: storyboard fires when training agent under-reports total_count
  • All 8 creative storyboards still pass (61/62 steps, 1 expected skip in branch_set)

🤖 Generated with Claude Code

Extends `pagination_integrity` with three runtime assertions per page:
- `query_summary.total_matching = 3` (always — schema requires the field)
- `query_summary.returned` matches the slice (2 on continuation, 1 on terminal)
- `pagination.total_count = 3` when volunteered (field_value_or_absent
  preserves the schema's optional stance)

Catches the dishonest pagination class where an agent honors max_results
and the cursor handshake but under-reports total_count to hide inventory
the same way a dishonest has_more: false would. Verified by flipping the
training agent's total_count to page-local count — page 1 assertion
fires with the expected diagnostic.

Closes the deferred follow-up flagged by the protocol reviewer on #3095.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 6099ff6 into main Apr 25, 2026
14 checks passed
@bokelley bokelley deleted the bokelley/total-count-monotonic branch April 25, 2026 10:05
bokelley added a commit that referenced this pull request Apr 25, 2026
Adds get_signals_pagination_integrity universal storyboard mirroring the
cursor↔has_more invariant that gates list_creatives (#3095/#3100) onto
the signals marketplace. Page 1 asserts has_more=true with a cursor
under a broad query; page 2 follows the cursor and asserts schema
conformance.

Fixes the training agent's handleGetSignals to honor pagination.max_results
/ pagination.cursor (was: capped at MAX_SIGNAL_RESULTS=10 internally and
emitted no pagination block — exactly the dishonest shape this storyboard
catches). Reads pagination.max_results in preference to legacy top-level
max_results for forward compatibility.

Generalizes the offset cursor codec from #3095 into a kind-prefixed pair
so cursors can't be replayed across endpoints. encodeCreativeCursor /
decodeCreativeCursor preserved as wrappers; list_creatives behavior
unchanged.

Negative-test verified: flipping the agent back to has_more: false fires
the page-1 assertion with the expected diagnostic.

Closes the second target in the rolling pagination conformance series.
list_creative_formats deferred to a separate issue (#3108) pending a
seed_creative_format scenario; list_accounts deferred to #3106 pending
the missing handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 25, 2026
* feat(compliance): get_signals pagination cursor integrity

Adds get_signals_pagination_integrity universal storyboard mirroring the
cursor↔has_more invariant that gates list_creatives (#3095/#3100) onto
the signals marketplace. Page 1 asserts has_more=true with a cursor
under a broad query; page 2 follows the cursor and asserts schema
conformance.

Fixes the training agent's handleGetSignals to honor pagination.max_results
/ pagination.cursor (was: capped at MAX_SIGNAL_RESULTS=10 internally and
emitted no pagination block — exactly the dishonest shape this storyboard
catches). Reads pagination.max_results in preference to legacy top-level
max_results for forward compatibility.

Generalizes the offset cursor codec from #3095 into a kind-prefixed pair
so cursors can't be replayed across endpoints. encodeCreativeCursor /
decodeCreativeCursor preserved as wrappers; list_creatives behavior
unchanged.

Negative-test verified: flipping the agent back to has_more: false fires
the page-1 assertion with the expected diagnostic.

Closes the second target in the rolling pagination conformance series.
list_creative_formats deferred to a separate issue (#3108) pending a
seed_creative_format scenario; list_accounts deferred to #3106 pending
the missing handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(training-agent): preserve legacy 50-cap on top-level get_signals max_results

Code-reviewer flagged on #3109: the cap on top-level `max_results` shifted
from 50 (prior behavior) to 100 (new code's uniform cap) when I unified
the read path. No spec basis for either cap on the top-level field, but
silently widening it is the wrong direction for a behavioral preserve.

Restructure: pagination.max_results gets the schema's documented 100 cap,
top-level max_results keeps the historical 50 cap. The two forms
diverge intentionally — pagination is the standard envelope, top-level
is the predecessor. Spec ambiguity on which form wins when both are
present is tracked at #3113.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(conformance): add get_signals_pagination_integrity to universal-storyboards tables

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 25, 2026
…, list_collection_lists, list_property_lists (#3147)

* feat(training-agent): add cursor pagination to governance list handlers (#3112)

Adds cursor-based pagination to list_content_standards, list_collection_lists,
and list_property_lists — the fifth batch in the rolling pagination conformance
series (#3095, #3100, #3109, #3110).

https://claude.ai/code/session_018AkoeaWmnrsnbXBtK8FLD2

* fix(compliance): scope identity + idempotency_key + valid channel on governance pagination storyboards

Triage's #3147 hit two build-time lints CI surfaced:

1. **Storyboard scoping** — 15 sample_request blocks (3 storyboards × 5 steps
   each, minus capability_discovery) omitted brand/account identity. The
   create_* and list_* tasks for governance lists are tenant-scoped per
   `lint-storyboard-scoping`. Adds the canonical
   `account: { brand: { domain: 'acmeoutdoor.example' }, operator: 'pinnacle-agency.example' }`
   to all 15.

2. **Idempotency_key on mutating setup steps** — 9 create_* sample_requests
   (3 per storyboard) on `create_collection_list`/`create_content_standards`/
   `create_property_list` omitted `idempotency_key`, which their request
   schemas mark as required. Adds `$generate:uuid_v4#<storyboard>_setup_<step>`
   per the established convention.

3. **Invalid channel value** — `create_standards_2` used `channels_any: ["video"]`
   which isn't in the channels enum. Replace with `["olv"]` (the standardized
   value for online video advertising outside CTV per
   `static/schemas/source/enums/channels.json`).

Verified: 8/8 pagination storyboards pass against the training agent
(list_creatives, get_signals, list_creative_formats, list_accounts,
get_media_buys, content_standards, collection_lists, property_lists)
— 41/41 steps clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant