feat(compliance): get_signals pagination cursor integrity#3109
Merged
Conversation
This was referenced Apr 25, 2026
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
…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>
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>
…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>
…storyboards tables
3011a9f to
c2aa797
Compare
This was referenced Apr 25, 2026
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
…obal pool, lint registration, doc parity Triage's #3114 implementation had four gaps the build/run surface revealed: 1. **TypeScript compile error.** `Array.from(seeded.values()) as ReturnType<typeof getFormats>` was a same-shape cast across non-overlapping types (TrainingFormat has required `name`/`description`/`renders`/`assets`; the seeded entry is `Record<string, unknown>`). Cast through `unknown` to match the contract that storyboards seed complete fixtures. 2. **Contradiction-lint registration.** `creative_formats` was added as a fixture category in storyboard-schema.yaml but missed in `scripts/lint-storyboard-contradictions.cjs`'s `FIXTURE_CATEGORY_PRIMARY_ID` map, failing the build with `unknown fixture category "creative_formats"`. Add `creative_formats: 'format_id'` alongside the other categories. 3. **Doc-parity.** Same lint that bit #3109/#3110 — adds rows for `pagination_integrity_creative_formats` to both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx`. 4. **Seed pool scope (the load-bearing fix).** The original implementation kept the seeded formats in `session.complyExtensions.seededCreativeFormats`, mirroring the per-session shape used by `seed_creative` / `seed_media_buy`. But `list_creative_formats` is a global catalog read with no tenant identity in its request schema — the seed call (which carries identity) and the listing call (which does not) land in different sessions, and the listing falls through to the static 37-format catalog, failing the `total_count: 2` assertion. Move the seed pool to a process-global Map (`SEEDED_CREATIVE_FORMATS`) — the test controller is sandbox-only and process-scoped anyway, so global scope is correct here. Other `seed_*` scenarios stay session-scoped because the listing calls they pair with carry identity. Mirror into session state so any test reading `complyExtensions.seededCreativeFormats` still observes it. Verified: 3/3 pagination-integrity storyboards (list_creatives, get_signals, list_creative_formats) pass clean against the training agent — 14/14 steps. Negative-test on `list_creative_formats` (flip `has_more` to false) trips the page-1 assertion as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
…obal pool, lint registration, doc parity Triage's #3114 implementation had four gaps the build/run surface revealed: 1. **TypeScript compile error.** `Array.from(seeded.values()) as ReturnType<typeof getFormats>` was a same-shape cast across non-overlapping types (TrainingFormat has required `name`/`description`/`renders`/`assets`; the seeded entry is `Record<string, unknown>`). Cast through `unknown` to match the contract that storyboards seed complete fixtures. 2. **Contradiction-lint registration.** `creative_formats` was added as a fixture category in storyboard-schema.yaml but missed in `scripts/lint-storyboard-contradictions.cjs`'s `FIXTURE_CATEGORY_PRIMARY_ID` map, failing the build with `unknown fixture category "creative_formats"`. Add `creative_formats: 'format_id'` alongside the other categories. 3. **Doc-parity.** Same lint that bit #3109/#3110 — adds rows for `pagination_integrity_creative_formats` to both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx`. 4. **Seed pool scope (the load-bearing fix).** The original implementation kept the seeded formats in `session.complyExtensions.seededCreativeFormats`, mirroring the per-session shape used by `seed_creative` / `seed_media_buy`. But `list_creative_formats` is a global catalog read with no tenant identity in its request schema — the seed call (which carries identity) and the listing call (which does not) land in different sessions, and the listing falls through to the static 37-format catalog, failing the `total_count: 2` assertion. Move the seed pool to a process-global Map (`SEEDED_CREATIVE_FORMATS`) — the test controller is sandbox-only and process-scoped anyway, so global scope is correct here. Other `seed_*` scenarios stay session-scoped because the listing calls they pair with carry identity. Mirror into session state so any test reading `complyExtensions.seededCreativeFormats` still observes it. Verified: 3/3 pagination-integrity storyboards (list_creatives, get_signals, list_creative_formats) pass clean against the training agent — 14/14 steps. Negative-test on `list_creative_formats` (flip `has_more` to false) trips the page-1 assertion as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
…gination (#3114) * feat(compliance): add seed_creative_format + list_creative_formats pagination (#3108) Adds `seed_creative_format` to `comply_test_controller` so the compliance harness can pre-populate a deterministic, size-controlled set of creative formats for pagination-integrity storyboards. **Schema changes:** `seed_creative_format` added to the `scenario` enum in both `comply-test-controller-request.json` and `comply-test-controller-response.json`. Request schema gains `params.format_id` (string, required for the new scenario) and a matching allOf conditional. Both files are updated atomically. **Implementation:** `seed_creative_format` is handled in `handleComplyTestController` before the SDK dispatcher (avoids UNKNOWN_SCENARIO). Idempotency enforced inline (same-fixture replay succeeds; different-fixture replay returns INVALID_STATE, matching the SEED_CACHE contract of other seed_* scenarios). `agent_url` stamped at write time so stored entries are schema-valid without a read-time patch. **Handler:** `handleListCreativeFormats` is now session-aware. When seeded formats are present they replace the static catalog, giving storyboards a knowable total_count. Cursor-based pagination added (reusing the offset-encoded cursor helpers from `handleListCreatives`). Static-catalog path is unchanged. **Storyboard:** `pagination-integrity-creative-formats.yaml` seeds 2 formats, walks pages at `max_results=1`, asserts `has_more`/`cursor`/`total_count` invariants. No `query_summary` assertions (field absent from response schema). Non-breaking: additive enum value + optional param. Existing `list_creative_formats` callers receive pagination fields in addition to `formats`; pagination block is additive per `additionalProperties: true` on the response schema. https://claude.ai/code/session_01VVBirzqi8AzidsW646iypJ * fix(compliance): resolve pre-PR review blockers on seed_creative_format - comply-test-controller-response.json: add SeedSuccess oneOf branch so seed_* responses pass schema validation (plain { success: true } failed all five existing branches which require scenarios/previous_state/ simulated/forced/error as discriminating fields) - comply-test-controller.ts: add message field to both success return paths (idempotent replay + first seed) to match other seed_* scenarios - task-handlers.ts: use `args` instead of `req` in sessionKeyFromArgs call to avoid an unsound cast (args is ToolArgs which is the expected shape; req carries ListCreativeFormatsRequest extras not needed for session key derivation) https://claude.ai/code/session_01VVBirzqi8AzidsW646iypJ * fix(compliance): unblock seed_creative_format pagination — process-global pool, lint registration, doc parity Triage's #3114 implementation had four gaps the build/run surface revealed: 1. **TypeScript compile error.** `Array.from(seeded.values()) as ReturnType<typeof getFormats>` was a same-shape cast across non-overlapping types (TrainingFormat has required `name`/`description`/`renders`/`assets`; the seeded entry is `Record<string, unknown>`). Cast through `unknown` to match the contract that storyboards seed complete fixtures. 2. **Contradiction-lint registration.** `creative_formats` was added as a fixture category in storyboard-schema.yaml but missed in `scripts/lint-storyboard-contradictions.cjs`'s `FIXTURE_CATEGORY_PRIMARY_ID` map, failing the build with `unknown fixture category "creative_formats"`. Add `creative_formats: 'format_id'` alongside the other categories. 3. **Doc-parity.** Same lint that bit #3109/#3110 — adds rows for `pagination_integrity_creative_formats` to both `docs/building/conformance.mdx` and `docs/building/compliance-catalog.mdx`. 4. **Seed pool scope (the load-bearing fix).** The original implementation kept the seeded formats in `session.complyExtensions.seededCreativeFormats`, mirroring the per-session shape used by `seed_creative` / `seed_media_buy`. But `list_creative_formats` is a global catalog read with no tenant identity in its request schema — the seed call (which carries identity) and the listing call (which does not) land in different sessions, and the listing falls through to the static 37-format catalog, failing the `total_count: 2` assertion. Move the seed pool to a process-global Map (`SEEDED_CREATIVE_FORMATS`) — the test controller is sandbox-only and process-scoped anyway, so global scope is correct here. Other `seed_*` scenarios stay session-scoped because the listing calls they pair with carry identity. Mirror into session state so any test reading `complyExtensions.seededCreativeFormats` still observes it. Verified: 3/3 pagination-integrity storyboards (list_creatives, get_signals, list_creative_formats) pass clean against the training agent — 14/14 steps. Negative-test on `list_creative_formats` (flip `has_more` to false) trips the page-1 assertion as expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <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>
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
Third storyboard in the rolling pagination conformance series — extends the cursor↔has_more invariant from
list_creatives(#3095/#3100) ontoget_signals.static/compliance/source/universal/get-signals-pagination-integrity.yamlsendssignal_spec: "audience"withpagination.max_results: 1. Page 1 assertshas_more=truewith a cursor (broad-query against any non-trivial signals catalog must be non-terminal). Page 2 follows the captured cursor and asserts schema conformance only — catalog size varies, can't pin terminal state.handleGetSignalsnow honorspagination.max_results/pagination.cursorand emits a proper pagination block. Previously capped atMAX_SIGNAL_RESULTS=10internally and emitted no pagination — exactly the dishonest shape this storyboard catches.kind-prefixed pair so a list_creatives cursor can't decode to a meaningful offset on a different endpoint. Old wrappers preserved; list_creatives behavior unchanged.Why softer page-2 assertions than #3095
The seeded
list_creativesstoryboard pins counts viaseed_creative(3 fixtures).get_signalshas noseed_signalscenario today, so this storyboard uses a broad query and asserts only what's portable: page 1 must be non-terminal under a broad query against any non-trivial catalog. The static lint (scripts/lint-pagination-invariant.cjs) covers cursor invariants on every sample fixture, andpagination_integrity.yamlexercises the full multi-page round-trip on the seedable side.Reviewed by
max_resultsandpagination.max_resultswith no documented winner. Filed upstream as Spec: pin precedence for pagination.max_results vs top-level max_results on get_signals #3113. Storyboard implements pagination-wins; an agent that read the spec the other way could legitimately fail it. Resolution lives upstream.max_results(pagination.max_results gets the schema's documented 100 cap).Verified
Negative-test by flipping the agent back to
has_more: false:Reverted — clean run restored.
Series status
encodeOffsetCursor)seed_creative_formatmax_resultsTest plan
npm run build:complianceclean (pagination-invariant lint + 7 storyboard lints)npm run test:pagination-invariant— 16/16 passnpm run typecheck— cleanget_signalsunit tests — 20/20 passpagination_integrity(list_creatives) still passes — 6/6 stepsget_signals_pagination_integrityagainst training agent — 3/3 steps cleanhas_more: false🤖 Generated with Claude Code