Skip to content

docs(compliance): formalize storyboard runner semantics — fixtures, captures, context echo#2593

Merged
bokelley merged 5 commits into
mainfrom
bokelley/storyboard-runner-semantics
Apr 20, 2026
Merged

docs(compliance): formalize storyboard runner semantics — fixtures, captures, context echo#2593
bokelley merged 5 commits into
mainfrom
bokelley/storyboard-runner-semantics

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Closes the three runner-semantics gaps surfaced by the training-agent conformance migration (#2582):

  1. Fixture seeding (Spec: comply_test_controller fixture-seeding scenarios (seed_product, seed_pricing_option, seed_creative, seed_plan, seed_media_buy) #2584): comply_test_controller gains five seed_* scenarios (seed_product, seed_pricing_option, seed_creative, seed_plan, seed_media_buy). Storyboards can now declare prerequisite fixtures by stable ID without implementers guessing which IDs the conformance suite expects.

  2. Declarative fixtures block (Spec: storyboard fixtures block / pattern — replace hardcoded fixture IDs #2585 Pattern A): new fixtures: root block + prerequisites.controller_seeding: true flag. Runner auto-injects a fixtures phase that seeds via the new scenarios in foreign-key order (products → pricing_options → media_buys).

  3. $context.<name> + context_outputs: (Spec: storyboard fixtures block / pattern — replace hardcoded fixture IDs #2585 Pattern B, Spec: context-echo semantics tightening (.* resolution and MUST-echo rules) #2589): specs the existing runner behavior — these mechanisms already work in the runner, but nothing in the spec documented them. Storyboard authors had to grep YAML to learn the substitution syntax.

  4. Context-echo contract tightening (Spec: context-echo semantics tightening (.* resolution and MUST-echo rules) #2589): MUST echo on success AND error, MUST NOT synthesize when caller sent none, MUST NOT mutate. Runners MUST NOT auto-inject synthetic context: — storyboards whose validator checks context.correlation_id MUST declare the block on every sample_request.

What changed

  • static/schemas/source/compliance/comply-test-controller-request.json — 5 new enum values, 5 new allOf branches, new fixture/product_id/pricing_option_id/plan_id params, examples.
  • docs/building/implementation/comply-test-controller.mdx — 5 new scenario sections, seeding-semantics subsection (upsert, foreign-key ordering, sandbox scope, UNKNOWN_SCENARIOnot_applicable).
  • static/compliance/source/universal/storyboard-schema.yaml — new fixtures: root block, prerequisites.controller_seeding, step context_outputs:, context-accumulator + substitution section, context-echo contract section.
  • docs/building/integration/context-sessions.mdx — new "Normative echo contract" subsection with six numbered rules (success echo, error echo, async updates, no synthesis, no mutation, no action).
  • docs/contributing/storyboard-authoring.md — new "Fixtures and cross-step captures" section with Pattern A and Pattern B examples plus when-to-use-which table.

Backwards compatibility

  • comply_test_controller scenario enum is extensible; seed_* adds new values only. Agents that don't implement them return UNKNOWN_SCENARIO, which the runner treats as a coverage gap (grades not_applicable, not failed). Existing agents are unaffected.
  • fixtures: and prerequisites.controller_seeding are optional on storyboards. Existing storyboards that hardcode fixture IDs keep working.
  • context_outputs: and \$context.<name> are specs for already-implemented runner behavior — no agent-side change required.
  • Context-echo tightening: agents that already echo verbatim are unaffected. Agents that synthesize context when the caller didn't send one were relying on undocumented forgiveness; the new rule makes that a conformance failure, as it should have been.

Test plan

  • npm run test:schemas — 483 schemas validate
  • npm run test:examples — 31 example payloads validate against their schemas (includes the 2 new seed_* examples)
  • npm run test:composed — 15 composed schema tests pass
  • npm run test:unit — 627 tests pass
  • npm run typecheck clean
  • npm run build:schemas cleanly builds with the new enum values
  • Review whether any existing storyboard that emits synthetic context on error needs updating before this lands (follow-up)

Closes #2584, #2585, #2589.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Schema Link Check Results

Commit: 2d55efc - fix(docs): correct PricingOption schema link path

⚠️ Warnings (schema not yet released)

These schemas exist in source but haven't been released yet. The links will be broken until the next version is published:

  • https://adcontextprotocol.org/schemas/v3/enums/specialism.json
    • Schema exists in latest (source) but not yet released in v3
    • Action: This link will work after next 3.x release is published

To fix: Either:

  1. Wait for the next release and merge this PR after the release is published
  2. Use latest instead of a version alias if you need the link to work immediately (note: latest is the development version and may change)
  3. Coordinate with maintainers to cut a new release before merging

bokelley added a commit that referenced this pull request Apr 20, 2026
Seven normative gaps surfaced by ad-tech-protocol-expert review of PR #2593:

1. Fixture shape — add non-normative pointer to domain schemas
   (product.json / creative.json / media-buy.json / plan schema) while
   keeping the fixture field additionalProperties: true.
2. Foreign-key DAG — complete the dependency graph. Previous edit only
   called out product→pricing_option and product→media_buy; add
   creative→media_buy, plan→media_buy, product→plan. Update both
   comply-test-controller.mdx and storyboard-schema.yaml.
3. Divergent-fixture tightening — second seed with diverging fixture
   MUST return INVALID_PARAMS, not MAY merge. Storyboards that need to
   mutate fixture state use force_* scenarios. Removes a storyboard
   determinism footgun.
4. Specialism-scoped seed floor — stateful compliance requires sellers
   to implement the seed_* scenarios covering the specialisms they
   certify against. Closes the "implement zero seeds to opt out" loophole
   while preserving UNKNOWN_SCENARIO → not_applicable for honest
   coverage reporting elsewhere.
5. Runner grading codes — enumerate capture_path_not_resolvable (producer),
   unresolved_substitution (consumer), fixture_seed_unsupported
   (not_applicable grading) so downstream tooling can discriminate them.
6. Webhook context echo cross-reference — add a "Echoing the caller's
   context object" subsection to webhooks.mdx pointing at the normative
   echo contract in context-sessions.mdx. Closes the gap where the new
   async-delivery echo rule was only surfaced in context-sessions.
7. Hardcoded-ID deprecation signal — explicitly state that existing
   hardcoded-literal fixtures are valid through 3.x and SHOULD migrate
   to the fixtures: block before 4.0. New storyboards MUST NOT hardcode.
   Also add the open-for-extension enum note (runners MUST accept unknown
   scenarios with UNKNOWN_SCENARIO rather than schema-reject).

No wire-breaking changes. Review feedback only; all docs/schema tests
still clean.
@bokelley

Copy link
Copy Markdown
Contributor Author

Reviewed against the three issues this closes (#2584 seed scenarios, #2585 fixtures/captures, #2589 context-echo). The DAG in the seed-ordering section (line 400) matches what we expected — seed_product before seed_pricing_option, parents before seed_media_buy. The $context.<name> + {{prior_step.<id>.<field>}} substitution syntaxes close #2585's capture pattern cleanly. The fixture/force/simulate separation (line 389 "re-seed MUST fail with INVALID_PARAMS on diverging fixture") is the right invariant to keep storyboards deterministic.

Three findings worth addressing before merge.

Blocker — INVALID_TRANSITION drift from merged #2595

The merged error-taxonomy PR canonicalized the state-machine rejection code as INVALID_STATE_TRANSITION. This PR still uses INVALID_TRANSITION in 7 places in docs/building/implementation/comply-test-controller.mdx:

  • Line 425 (example response)
  • Line 485 (error codes table)
  • Line 535 ("Transition correctly rejected — controller returned INVALID_TRANSITION")
  • Lines 552, 575, 579 (deterministic-mode examples)
  • Line 621 (implementation guidance)

Either: (a) INVALID_TRANSITION is the canonical code and #2595 is wrong, or (b) rename all 7 to INVALID_STATE_TRANSITION. lint-error-codes.cjs (shipped in #2595) should catch this if pointed at MDX docs — if it doesn't, that's a #2588 follow-up.

This is exactly the drift that motivated issue #2588. Merging this PR as-is would make the docs internally inconsistent with the lint that #2595 just shipped.

Nit — list_scenarios doesn't require canonical enum strings

Lines 503–516 describe list_scenarios discovery. The example at 508–513 uses the enum strings from the request schema (comply-test-controller-request.json), but the prose doesn't explicitly require sellers to respond with the canonical names.

Without that requirement, storyboards can't distinguish "seller doesn't support seed_product" from "seller supports it under the name create_test_product". The runner's trial-and-error fallback (line 516) only works if sellers who DO support scenarios use the canonical names.

Add one sentence around line 503:

Sellers that implement list_scenarios MUST respond with scenario names that appear verbatim in the scenario enum of comply-test-controller-request.json. Custom seller-specific scenario names are not part of the compliance contract and storyboard runners will not dispatch to them.

Nit — floor_price vs rate inconsistency in seed_* examples

  • Line 269 (seed_product example): "pricing_model": "cpm", "floor_price": 1.0
  • Line 299 (seed_pricing_option example): "pricing_model": "cpm", "rate": 5.0

Both are CPM pricing options on the same product, but the two examples use different field names for the price. The spec's PricingOption schema uses floor_price for auction-based pricing and fixed_price for fixed pricing; rate isn't a declared CPM field.

If this is a shorthand alias the seeding API accepts: document it as such. If it's a drafting mistake: unify to floor_price (or fixed_price, depending on intent). Exactly the kind of schema drift issue #2584 was filed to prevent — shipping two different field names for the same concept in the canonical doc is the worst case.

Compliments

  • Line 93–98 carve-out for hardcoded literals "through 3.x" with a 4.0 removal deadline: good, though a specific minor (e.g., "warnings begin in 3.3, errors in 4.0") would beat "before 4.0". Not blocking.
  • Line 218–221 simulate_budget_spend normative MUSTs on total_spend / remaining_budget / utilization percentages: exactly the shape our training agent needs to emit. Clean.
  • Line 400 topo-sort requirement on fixtures: block (reject references to unseeded parents with INVALID_PARAMS, don't auto-create): prevents the footgun where a storyboard's ordering bug silently creates parents the seller never validated.

Migration notes for our training agent

Once this ships (and adcp-client PR #669 ships seedComplianceFixtures):

  1. Replace buildCatalog's alias block (product-factory.ts:862–907) with comply_test_controller handlers that implement seed_product, seed_pricing_option, seed_creative, seed_plan, seed_media_buy. Each responds INVALID_PARAMS with the diverging-field detail on re-seed conflicts (per line 389).
  2. Advertise the canonical scenario names on list_scenarios — once the line 503 wording is tightened.
  3. Rename our INVALID_TRANSITION emission to INVALID_STATE_TRANSITION per docs(errors): clarify two-layer error model + add error-code lint #2595 (already done in PR feat(training-agent): storyboard pass — signing composition, review polish, RFC 9728, dual-mode CI #2582 commit e4a8b4e — consistent with the canonical code, not with this PR's 7-place drift).

…aptures, context echo

Closes #2584, #2585, #2589.

1. Adds `seed_*` scenarios to `comply_test_controller` (seed_product,
   seed_pricing_option, seed_creative, seed_plan, seed_media_buy).
   Storyboards can now declare prerequisite fixtures by stable ID
   without implementers guessing which IDs the conformance suite expects.

2. Adds declarative `fixtures:` block and `prerequisites.controller_seeding`
   flag to the storyboard schema. Runner auto-injects a fixtures phase
   that seeds via the new scenarios in foreign-key order.

3. Specs the existing `context_outputs:` + `$context.<name>` mechanism
   (already implemented in the runner, previously undocumented).

4. Tightens the context-echo contract — MUST echo on success AND error,
   MUST NOT synthesize when caller sent none, MUST NOT mutate. Runners
   MUST NOT auto-inject synthetic context.

No wire-breaking changes. The comply_test_controller scenario enum is
extensible; existing agents unaffected until they adopt seed_*.
Seven normative gaps surfaced by ad-tech-protocol-expert review of PR #2593:

1. Fixture shape — add non-normative pointer to domain schemas
   (product.json / creative.json / media-buy.json / plan schema) while
   keeping the fixture field additionalProperties: true.
2. Foreign-key DAG — complete the dependency graph. Previous edit only
   called out product→pricing_option and product→media_buy; add
   creative→media_buy, plan→media_buy, product→plan. Update both
   comply-test-controller.mdx and storyboard-schema.yaml.
3. Divergent-fixture tightening — second seed with diverging fixture
   MUST return INVALID_PARAMS, not MAY merge. Storyboards that need to
   mutate fixture state use force_* scenarios. Removes a storyboard
   determinism footgun.
4. Specialism-scoped seed floor — stateful compliance requires sellers
   to implement the seed_* scenarios covering the specialisms they
   certify against. Closes the "implement zero seeds to opt out" loophole
   while preserving UNKNOWN_SCENARIO → not_applicable for honest
   coverage reporting elsewhere.
5. Runner grading codes — enumerate capture_path_not_resolvable (producer),
   unresolved_substitution (consumer), fixture_seed_unsupported
   (not_applicable grading) so downstream tooling can discriminate them.
6. Webhook context echo cross-reference — add a "Echoing the caller's
   context object" subsection to webhooks.mdx pointing at the normative
   echo contract in context-sessions.mdx. Closes the gap where the new
   async-delivery echo rule was only surfaced in context-sessions.
7. Hardcoded-ID deprecation signal — explicitly state that existing
   hardcoded-literal fixtures are valid through 3.x and SHOULD migrate
   to the fixtures: block before 4.0. New storyboards MUST NOT hardcode.
   Also add the open-for-extension enum note (runners MUST accept unknown
   scenarios with UNKNOWN_SCENARIO rather than schema-reject).

No wire-breaking changes. Review feedback only; all docs/schema tests
still clean.
@bokelley bokelley force-pushed the bokelley/storyboard-runner-semantics branch from 2623007 to 4ca200b Compare April 20, 2026 22:29
- list_scenarios: MUST return canonical enum names verbatim. Custom
  scenario names aren't part of the compliance contract — the runner
  won't dispatch to non-enum scenarios, so listing them serves no
  purpose.
- seed_pricing_option example: unify on floor_price (auction-based CPM,
  matches PricingOption schema) instead of the non-canonical 'rate'
  field. Add cross-reference to the schema so authors know which fields
  are legal.

Review nits from PR author self-review. Not normatively changing any
existing behavior.
@bokelley

Copy link
Copy Markdown
Contributor Author

Clarifying the INVALID_TRANSITIONINVALID_STATE_TRANSITION "blocker" — those 7 usages in comply-test-controller.mdx are correct as written. They reference the controller-specific enum at static/schemas/source/compliance/comply-test-controller-response.json:84, which declares:

"enum": ["INVALID_TRANSITION", "INVALID_STATE", "NOT_FOUND", "UNKNOWN_SCENARIO", "INVALID_PARAMS", "FORBIDDEN", "INTERNAL_ERROR"]

This enum is distinct from the canonical seller-response error-code.json, and the distinction is spec-level intentional (state-machine primitives expose the transition-vs-state difference; seller-level responses collapse it into INVALID_STATE). See the "Controller-specific enum" callout in comply-test-controller.mdx (added by PR #2596, following the same protocol-reviewer feedback).

Three canonical codes live in the spec today:

Code Enum Used by
INVALID_STATE canonical error-code.json seller responses — agent rejects update_media_buy on terminal state
INVALID_TRANSITION comply-test-controller-response.json controller force_* scenarios rejecting an illegal next-state
INVALID_STATE_TRANSITION nowhere drift — only appears in prose; PR #2596 fixes the 5 occurrences in state-machine.yaml

Ground truth:

  • static/schemas/source/enums/error-code.json — contains INVALID_STATE only; no INVALID_STATE_TRANSITION.
  • server/src/training-agent/task-handlers.ts:1994 — emits INVALID_STATE for terminal-state updates (not INVALID_STATE_TRANSITION).
  • server/src/training-agent/comply-test-controller.ts:119, 224 — emits INVALID_TRANSITION / INVALID_STATE depending on controller branch, per the controller enum.
  • static/compliance/source/universal/deterministic-testing.yaml:682, 852 — asserts INVALID_TRANSITION against controller responses (correct per controller enum).

So this PR is consistent with #2595 and #2596. The two nits (list_scenarios MUST-return-canonical + floor_price/rate example unification) are addressed in e9809fa on this branch.

check-schema-links CI caught that /schemas/v3/media-buy/product.json
doesn't exist — the PricingOption schema lives at /schemas/v3/core/pricing-option.json.
Also updated the prose reference in 'Fixture shape' to name the
correct schema paths (core/product.json, core/pricing-option.json,
core/media-buy.json, media-buy/sync-creatives-request.json).
@bokelley bokelley merged commit 82134f1 into main Apr 20, 2026
15 checks passed
@bokelley

Copy link
Copy Markdown
Contributor Author

Correction — retracting my earlier blocker on INVALID_TRANSITION usage in comply-test-controller.mdx. I reviewed against the wrong assumption. Actual enum state:

  • error-code.json (seller canonical): INVALID_STATE — no _TRANSITION suffix.
  • comply-test-controller-response.json (controller canonical, distinct): INVALID_TRANSITION.
  • INVALID_STATE_TRANSITIONnot in any enum or code, only in 5 prose lines of state-machine.yaml that are pure drift.

The 7 INVALID_TRANSITION uses in this PR's comply-test-controller.mdx are correct — that's the controller's canonical rejection code for force_* state transitions, separate from the seller's INVALID_STATE for domain-level invalid states. The two codes exist on purpose.

PR #2596 is the one fixing the actual drift (the state-machine.yaml prose). Apologies for the noise — reading as ship-ready on the error-code axis. Other findings on list_scenarios canonicalization (nit 1) and floor_price vs rate in the seed examples (nit 2) still stand.

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.

Spec: comply_test_controller fixture-seeding scenarios (seed_product, seed_pricing_option, seed_creative, seed_plan, seed_media_buy)

1 participant