feat(compliance): pagination cursor↔has_more invariant lint and storyboard#3095
Merged
Conversation
…board Adds a build-time lint that scans schema examples and storyboard fixtures for the two violation classes the prose contract on pagination-response.json doesn't enforce: has_more=true without cursor, and has_more=false with a stale cursor. Wired into build-compliance so CI gates on it. Adds a universal storyboard (pagination_integrity) that seeds three creatives, walks list_creatives with max_results=2, and asserts the invariant on a continuation page (has_more=true, cursor present, captured) followed by a terminal page (has_more=false, cursor absent or null). Fixes the training agent's list_creatives to honor pagination.max_results (default 50, capped at 100 per the request schema) and emit an opaque base64url offset cursor when the page is non-terminal. Malformed cursors now surface INVALID_REQUEST instead of silently restarting from offset 0. Previously every response carried has_more: false regardless of input — the exact dishonest shape the new storyboard catches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
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>
This was referenced Apr 25, 2026
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>
This was referenced Apr 25, 2026
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
… codec Addresses three blockers from the independent expert review on #3110: 1. **Idempotency_key.** The `sync_three_accounts` setup step on the new storyboard omitted `idempotency_key`, failing the build-time storyboard-idempotency lint. Adds the conventional `$generate:uuid_v4#<storyboard>_<phase>_<step>` alias. 2. **Cursor codec duplication.** Triage shipped private `encodeAccountCursor` / `decodeAccountCursor` in account-handlers.ts with format `offset:N`. PR #3095 (now merged) generalized the codec to a kind-prefixed pair. Moves the shared codec to `server/src/training-agent/pagination.ts` (avoids the task-handlers ↔ account-handlers circular import the in-place re-export would create) and replaces the local helpers with `encodeOffsetCursor('accounts', n)` / `decodeOffsetCursor('accounts', c)`. list_creatives and get_signals handlers move to the shared module with no behavior change. 3. **Bootstrap-pattern deviation note.** The seed-DAG documented at storyboard-schema.yaml does not include accounts; this storyboard uses `sync_accounts` as setup rather than `controller_seeding: true`. Adds explicit narrative explaining why and pointing at a future `seed_account` controller scenario. Also fixes: - **Wire-shape leak.** `accountStateToWire()` was passing `account.brand` through verbatim, including a `name` operational hint that brand-ref.json forbids (additionalProperties: false). Strips to declared fields only (`domain`, optional `brand_id`); display name and advertiser fields are still derived from the operational hint but never round-trip on the wire. Compliance fixture pool's brand entries cleaned up to match. - **Framework registration.** Triage added `handleListAccounts` to `HANDLER_MAP` but never registered the tool through the framework-server's `accounts:` block, so the SDK never advertised `list_accounts` and the storyboard's pagination steps were skipped (no-such-tool). Wires it alongside `sync_accounts`. - **Doc-parity.** Adds the new storyboard to both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx` per the lint that gates the build. Verified: negative-test flips `has_more` to false on the agent — first-page assertion fires with `Expected true, got false`. Reverted, clean run restored. 4/4 storyboard steps pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
…rmance (#3110) * feat(training-agent): add list_accounts handler with pagination conformance Closes #3106 https://claude.ai/code/session_01N6Bgto9hbbN5pepe9uhgxp * fix(training-agent): wire list_accounts handler + reuse shared cursor codec Addresses three blockers from the independent expert review on #3110: 1. **Idempotency_key.** The `sync_three_accounts` setup step on the new storyboard omitted `idempotency_key`, failing the build-time storyboard-idempotency lint. Adds the conventional `$generate:uuid_v4#<storyboard>_<phase>_<step>` alias. 2. **Cursor codec duplication.** Triage shipped private `encodeAccountCursor` / `decodeAccountCursor` in account-handlers.ts with format `offset:N`. PR #3095 (now merged) generalized the codec to a kind-prefixed pair. Moves the shared codec to `server/src/training-agent/pagination.ts` (avoids the task-handlers ↔ account-handlers circular import the in-place re-export would create) and replaces the local helpers with `encodeOffsetCursor('accounts', n)` / `decodeOffsetCursor('accounts', c)`. list_creatives and get_signals handlers move to the shared module with no behavior change. 3. **Bootstrap-pattern deviation note.** The seed-DAG documented at storyboard-schema.yaml does not include accounts; this storyboard uses `sync_accounts` as setup rather than `controller_seeding: true`. Adds explicit narrative explaining why and pointing at a future `seed_account` controller scenario. Also fixes: - **Wire-shape leak.** `accountStateToWire()` was passing `account.brand` through verbatim, including a `name` operational hint that brand-ref.json forbids (additionalProperties: false). Strips to declared fields only (`domain`, optional `brand_id`); display name and advertiser fields are still derived from the operational hint but never round-trip on the wire. Compliance fixture pool's brand entries cleaned up to match. - **Framework registration.** Triage added `handleListAccounts` to `HANDLER_MAP` but never registered the tool through the framework-server's `accounts:` block, so the SDK never advertised `list_accounts` and the storyboard's pagination steps were skipped (no-such-tool). Wires it alongside `sync_accounts`. - **Doc-parity.** Adds the new storyboard to both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx` per the lint that gates the build. Verified: negative-test flips `has_more` to false on the agent — first-page assertion fires with `Expected true, got false`. Reverted, clean run restored. 4/4 storyboard steps pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(training-agent): bump expected tool count to 49 for list_accounts --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Apr 25, 2026
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>
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
scripts/lint-pagination-invariant.cjs) scans schemaexamples[]and storyboardsample_request/sample_responseforhas_more=truewithoutcursorandhas_more=falsewith stalecursor. Wired intobuild-complianceso CI gates on it.pagination_integrity) seeds three creatives, walkslist_creativeswithmax_results=2, asserts the invariant on a continuation page (has_more=true, cursor present, captured) followed by a terminal page (has_more=false, cursor absent or null).list_creativesto honorpagination.max_results(default 50, capped at 100 per the request schema) and emit an opaque base64url offset cursor when the page is non-terminal. Malformed cursors surfaceINVALID_REQUESTrather than silently restarting at offset 0.Why
Brian flagged that "most adapters' pagination returns
has_more: falsewithout populatingtotal_count— works but isn't fully honest if the platform paginates server-side." The schema permits omittingtotal_count, but the prose contract onpagination-response.json(cursor "Only present when has_more is true") was unenforced. Two complementary guards close the gap: a static lint on authored examples, and a runtime storyboard that walks pagination on a real agent.The training agent fix was discovered when running the storyboard against it: every list_creatives response carried
has_more: falseregardless ofmax_results. That was the exact dishonest shape the storyboard catches; ref-implementation should not teach it.Reviewed by
Three subagents reviewed before push. Findings addressed:
decodeCreativeCursornow returnsINVALID_REQUESTon malformed input (was: silent reset to 0); removed silent lower-bound clamp onmax_results; added WeakSet cycle guard inwalkPaginationObjects.<unnamed>fallback rendering, plus a self-referential cycle test for the walker (16 tests total, all passing).cursor: nullat runtime perrunner-output-contract.yaml's absent-as-null reporting).Out of scope (deferred)
total_countmonotonicity check (total_count >= returned_so_far) — protocol reviewer flagged as a free honesty check that doesn't require upstream visibility. Worth a follow-up PR; not bundling here to keep the surface small.Test plan
npm run build— full schema/compliance/tarball build (CI step)npm run test:server-unit— 2151 tests, 1 LLM flake unrelated to changes (passes on retry)npm run test:unit— 820/820 passnpm run test:schemas && test:json-schema && test:extension-schemas && test:composed— all cleannpm run check:registry && test:platform-agnostic— both cleannpm run test:hmac-vectors && test:hmac-signer-conformance— both cleannpm run test:pagination-invariant— 16/16 passpagination_integritystoryboard against training agent: 6/6 steps clean🤖 Generated with Claude Code