Skip to content

feat(compliance): get_signals pagination cursor integrity#3109

Merged
bokelley merged 3 commits into
mainfrom
bokelley/signals-formats-pagination
Apr 25, 2026
Merged

feat(compliance): get_signals pagination cursor integrity#3109
bokelley merged 3 commits into
mainfrom
bokelley/signals-formats-pagination

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Apr 25, 2026

Summary

Third storyboard in the rolling pagination conformance series — extends the cursor↔has_more invariant from list_creatives (#3095/#3100) onto get_signals.

  • Storyboard: static/compliance/source/universal/get-signals-pagination-integrity.yaml sends signal_spec: "audience" with pagination.max_results: 1. Page 1 asserts has_more=true with 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.
  • Training agent fix: handleGetSignals now honors pagination.max_results / pagination.cursor and emits a proper pagination block. Previously capped at MAX_SIGNAL_RESULTS=10 internally and emitted no pagination — exactly the dishonest shape this storyboard catches.
  • Cursor codec: generalized from feat(compliance): pagination cursor↔has_more invariant lint and storyboard #3095 into a 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_creatives storyboard pins counts via seed_creative (3 fixtures). get_signals has no seed_signal scenario 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, and pagination_integrity.yaml exercises the full multi-page round-trip on the seedable side.

Reviewed by

  • ad-tech-protocol-expert: sound-with-caveats. Two flags addressed:
    1. Field precedence: the request schema declares both top-level max_results and pagination.max_results with 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.
    2. kind-prefixed cursor: correct defensive practice; keep.
  • code-reviewer: no blockers. One concrete flag addressed in 3011a9fb — preserve the legacy 50-cap on top-level max_results (pagination.max_results gets the schema's documented 100 cap).

Verified

Negative-test by flipping the agent back to has_more: false:

× first_page: A broad query against any non-trivial signal catalog must yield more than one match — first page is non-terminal — Expected true, got false

Reverted — clean run restored.

Series status

Test plan

  • npm run build:compliance clean (pagination-invariant lint + 7 storyboard lints)
  • npm run test:pagination-invariant — 16/16 pass
  • npm run typecheck — clean
  • get_signals unit tests — 20/20 pass
  • pagination_integrity (list_creatives) still passes — 6/6 steps
  • get_signals_pagination_integrity against training agent — 3/3 steps clean
  • Pre-commit unit suite — clean
  • Negative test: storyboard fires when agent reverts to has_more: false

🤖 Generated with Claude Code

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>
bokelley and others added 3 commits April 25, 2026 08:24
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>
@bokelley bokelley force-pushed the bokelley/signals-formats-pagination branch from 3011a9f to c2aa797 Compare April 25, 2026 12:27
@bokelley bokelley merged commit 9525717 into main Apr 25, 2026
19 checks passed
@bokelley bokelley deleted the bokelley/signals-formats-pagination branch April 25, 2026 12:34
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>
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