id: schema_validation
version: "1.0.0"
title: "Schema compliance and temporal validation"
category: schema_validation
summary: "Validates that agent responses conform to AdCP schemas and that temporal constraints are enforced."
track: core
required_tools:
  - get_products

narrative: |
  Every AdCP agent must return responses that match the published JSON schemas. Fields defined
  in the spec — product_id, delivery_type, pricing_options — must be present and correctly typed.
  Agents must also enforce temporal constraints: flight dates must be logically consistent
  (end after start) and start dates should not be in the past.

  This storyboard checks schema compliance on get_products responses and enforces temporal
  constraints on create_media_buy — reversed dates must always be rejected, and past
  start_time must be either rejected or accepted-with-adjustment (never silently accepted).
  These are foundational requirements that every agent must satisfy regardless of platform
  type.

agent:
  interaction_model: media_buy_seller
  capabilities:
    - sells_media
  examples:
    - "Any AdCP seller agent"

caller:
  role: buyer_agent
  example: "Compliance test harness"

prerequisites:
  description: |
    The caller needs a brand identity for product discovery requests. The runner
    also seeds the product and pricing option referenced by the temporal
    create_media_buy requests so date validation is isolated from catalog
    availability.
  test_kit: "test-kits/acme-outdoor.yaml"
  controller_seeding: true

fixtures:
  products:
    - product_id: "test-product"
      delivery_type: "non_guaranteed"
      channels: ["display"]
      format_options:
        - format_option_id: "display_300x250"
          format_kind: "image"
          params:
            width: 300
            height: 250
  pricing_options:
    - product_id: "test-product"
      pricing_option_id: "test-pricing"
      pricing_model: "cpm"
      currency: "USD"
      floor_price: 1.0

phases:
  - id: capability_discovery
    title: "Capability discovery"
    narrative: |
      The buyer calls get_adcp_capabilities to confirm the agent supports media buying before validating response schemas.

    steps:
      - id: get_capabilities
        title: "Check agent capabilities"
        narrative: |
          Verify that the agent declares the expected protocol support before
          proceeding with domain-specific operations.
        task: get_adcp_capabilities
        schema_ref: "protocol/get-adcp-capabilities-request.json"
        response_schema_ref: "protocol/get-adcp-capabilities-response.json"
        doc_ref: "/protocol/get_adcp_capabilities"
        comply_scenario: capability_discovery
        stateful: false
        expected: |
          Return capabilities declaring media_buy in supported_protocols, confirming the agent sells media.
        sample_request:
          context:
            correlation_id: "schema_validation--get_capabilities"
        validations:
          - check: response_schema
            description: "Response matches get-adcp-capabilities-response.json schema"
          - check: field_present
            path: "supported_protocols"
            description: "Agent declares supported protocols"

          - check: field_present
            path: "context"
            description: "Response echoes back the context object"
          - check: field_value
            path: "context.correlation_id"
            value: "schema_validation--get_capabilities"
            description: "Context correlation_id returned unchanged"
  - id: schema_compliance
    title: "Response schema compliance"
    narrative: |
      The buyer sends a standard get_products request and validates that the response
      matches the published schema. Each product must have the required fields defined
      in the AdCP spec.

    steps:
      - id: get_products_schema
        title: "Validate get_products response schema"
        narrative: |
          Send a brief and validate the response structure. Every product in the response
          must conform to the get-products-response.json schema, with required v3 fields
          present.
        task: get_products
        schema_ref: "media-buy/get-products-request.json"
        response_schema_ref: "media-buy/get-products-response.json"
        doc_ref: "/media-buy/task-reference/get_products"
        comply_scenario: schema_compliance
        stateful: false
        expected: |
          Return products matching the brief. Each product must have all eight required fields:
          - product_id: unique identifier for the product
          - name: human-readable product name
          - description: detailed description of the product and its inventory
          - publisher_properties: array (minItems: 1) of publisher property selectors
          - format_options: non-empty array of canonical ProductFormatDeclaration objects
          - delivery_type: "guaranteed" or "non_guaranteed"
          - pricing_options: array (minItems: 1) — each with pricing_option_id, pricing_model, and currency
          - reporting_capabilities: fully-formed object with required sub-fields:
              available_reporting_frequencies, expected_delay_minutes, timezone,
              supports_webhooks, available_metrics, date_range_support

          Responses missing any of these fields fail schema validation regardless of
          what other fields are present.

        sample_request:
          idempotency_key: "$generate:uuid_v4#schema_validation_schema_compliance_get_products_schema"
          buying_mode: "brief"
          brief: "Return a small representative sample of available advertising products"
          pagination:
            max_results: 5
          account:
            brand:
              domain: "acmeoutdoor.example"
            operator: "pinnacle-agency.example"

          context:
            correlation_id: "schema_validation--get_products_schema"
        validations:
          - check: response_schema
            description: "Response matches get-products-response.json schema"
          - check: field_present
            path: "products"
            description: "Response contains a products array"
          - check: field_present
            path: "products[0].product_id"
            description: "Each product has a product_id"
          - check: field_present
            path: "products[0].delivery_type"
            description: "Each product declares guaranteed or non_guaranteed delivery"

          - check: field_present
            path: "context"
            description: "Response echoes back the context object"
          - check: field_value
            path: "context.correlation_id"
            value: "schema_validation--get_products_schema"
            description: "Context correlation_id returned unchanged"
      - id: pricing_options_present
        title: "Validate pricing options structure"
        narrative: |
          Verify that products include pricing_options with the required fields:
          pricing_option_id and pricing_model. These are required for buyers to
          construct valid create_media_buy requests.
        task: get_products
        schema_ref: "media-buy/get-products-request.json"
        response_schema_ref: "media-buy/get-products-response.json"
        doc_ref: "/media-buy/task-reference/get_products"
        comply_scenario: schema_compliance
        stateful: false
        expected: |
          Products include pricing_options with:
          - pricing_option_id: unique identifier for the pricing option
          - pricing_model: CPM, CPC, flat_rate, etc.

        sample_request:
          idempotency_key: "$generate:uuid_v4#schema_validation_schema_compliance_pricing_options_present"
          buying_mode: "brief"
          brief: "Premium display inventory with pricing details"
          pagination:
            max_results: 5
          account:
            brand:
              domain: "acmeoutdoor.example"
            operator: "pinnacle-agency.example"

          context:
            correlation_id: "schema_validation--pricing_options_present"
        validations:
          - check: field_present
            path: "products[0].pricing_options[0].pricing_option_id"
            description: "Pricing options have a pricing_option_id"
          - check: field_present
            path: "products[0].pricing_options[0].pricing_model"
            description: "Pricing options declare a pricing_model"

          - check: field_present
            path: "context"
            description: "Response echoes back the context object"
          - check: field_value
            path: "context.correlation_id"
            value: "schema_validation--pricing_options_present"
            description: "Context correlation_id returned unchanged"
  - id: format_option_reconciliation
    title: "Format option and pricing option reconciliation"
    narrative: |
      Identifiers returned by get_products must be accepted back by the same agent in
      subsequent calls. Two common reconciliation failures:

      1. Format options: the seller returns canonical declarations that their own
         validation rejects when the buyer echoes the selected format_kind and option
         reference back in sync_creatives.
      2. Pricing option IDs: the seller returns pricing_option_id values in products
         that create_media_buy rejects when the buyer references them.

      These validations confirm structural correctness — that required fields exist and
      are well-formed. Full roundtrip verification (echoing a product's canonical format into
      sync_creatives and confirming acceptance) requires a stateful multi-step test,
      which is covered by the media_buy_seller storyboard's creative_sync phase.

    steps:
      - id: get_products_for_formats
        title: "Get products and verify identifiers"
        narrative: |
          Request products and verify that each product includes format_options and
          pricing_options with the required fields. These identifiers will be echoed
          back in create_media_buy and sync_creatives — if they are malformed or
          missing, the buyer cannot complete the purchase.
        task: get_products
        schema_ref: "media-buy/get-products-request.json"
        response_schema_ref: "media-buy/get-products-response.json"
        doc_ref: "/media-buy/task-reference/get_products"
        comply_scenario: schema_compliance
        stateful: false
        expected: |
          Return products with format_options and pricing_options. Each format option must have:
          - format_option_id: stable option identifier
          - format_kind: canonical creative discriminator
          - params: canonical constraints such as dimensions or duration

          Each pricing_option must have:
          - pricing_option_id: unique identifier echoed in create_media_buy
          - pricing_model: CPM, CPC, flat_rate, etc.

          These canonical selectors must be accepted unchanged in subsequent requests.

        sample_request:
          idempotency_key: "$generate:uuid_v4#schema_validation_format_id_reconciliation_get_products_for_formats"
          buying_mode: "brief"
          brief: "Return one representative product with creative format requirements"
          pagination:
            max_results: 1
          account:
            brand:
              domain: "acmeoutdoor.example"
            operator: "pinnacle-agency.example"
          context:
            correlation_id: "schema_validation--get_products_for_formats"

        validations:
          - check: response_schema
            description: "Response matches get-products-response.json schema"
          - check: field_present
            path: "products[0].format_options"
            description: "Products include canonical format_options"
          - check: field_present
            path: "products[0].format_options[0].format_option_id"
            description: "Format options include a stable option identifier"
          - check: field_present
            path: "products[0].format_options[0].format_kind"
            description: "Format options carry a canonical format declaration"
          - check: field_present
            path: "products[0].pricing_options[0].pricing_option_id"
            description: "Pricing options include pricing_option_id — must be accepted in create_media_buy"
          - check: field_present
            path: "products[0].pricing_options[0].pricing_model"
            description: "Pricing options include pricing_model"
          - check: field_present
            path: "context"
            description: "Response echoes back the context object"
          - check: field_value
            path: "context.correlation_id"
            value: "schema_validation--get_products_for_formats"
            description: "Context correlation_id returned unchanged"

  - id: temporal_validation
    title: "Temporal constraint enforcement"
    narrative: |
      The buyer sends media buy requests with invalid temporal constraints. The agent
      must reject reversed dates (end before start) and past start dates with a
      structured INVALID_REQUEST response.

    steps:
      - id: reversed_dates
        title: "Reject reversed flight dates"
        narrative: |
          Send a create_media_buy request where end_time is before start_time. The agent
          must reject this with an INVALID_REQUEST error.
        task: create_media_buy
        requires_tool: create_media_buy
        schema_ref: "media-buy/create-media-buy-request.json"
        response_schema_ref: "media-buy/create-media-buy-response.json"
        doc_ref: "/media-buy/task-reference/create_media_buy"
        comply_scenario: temporal_validation
        expect_error: true
        negative_path: payload_well_formed
        stateful: false
        expected: |
          Reject the request with:
          - Error code: INVALID_REQUEST
          - Message indicating the date constraint violation

        sample_request:
          brand:
            domain: "acmeoutdoor.example"
          account:
            brand:
              domain: "acmeoutdoor.example"
            operator: "pinnacle-agency.example"
          idempotency_key: "$generate:uuid_v4#schema-validation-reversed-dates"
          start_time: "2099-12-31T00:00:00Z"
          end_time: "2099-01-01T00:00:00Z"
          packages:
            - product_id: "test-product"
              budget: 10000
              pricing_option_id: "test-pricing"

          context:
            correlation_id: "schema_validation--reversed_dates"
        validations:
          - check: field_present
            path: "adcp_error"
            description: "Response contains an error for reversed dates"

          - check: field_present
            path: "context"
            description: "Response echoes back the context object"
          - check: field_value
            path: "context.correlation_id"
            value: "schema_validation--reversed_dates"
            description: "Context correlation_id returned unchanged"
  - id: past_start_rejection
    title: "Past start date rejection"
    narrative: |
      A buyer must not be able to create a new media buy whose flight starts in
      the past. The agent rejects this schema-valid but temporally invalid
      request with INVALID_REQUEST.

    steps:
      - id: create_buy_past_start_reject
        title: "Reject past-start create_media_buy"
        narrative: |
          Send a create_media_buy whose start_time is years in the past. An
          agent rejects this with INVALID_REQUEST. The request carries a valid
          `idempotency_key` so the rejection is unambiguously about the
          temporal constraint — without a key the agent could also
          legitimately reject for missing-key (per universal/idempotency.yaml),
          and the storyboard couldn't tell which rule fired.
        task: create_media_buy
        requires_tool: create_media_buy
        schema_ref: "media-buy/create-media-buy-request.json"
        response_schema_ref: "media-buy/create-media-buy-response.json"
        doc_ref: "/media-buy/task-reference/create_media_buy"
        comply_scenario: temporal_validation
        expect_error: true
        negative_path: payload_well_formed
        stateful: false
        expected: |
          Reject the request with:
          - Error code: INVALID_REQUEST
          - Message indicating the past start_time violation

        sample_request:
          brand:
            domain: "acmeoutdoor.example"
          account:
            brand:
              domain: "acmeoutdoor.example"
            operator: "pinnacle-agency.example"
          start_time: "2020-01-01T00:00:00Z"
          end_time: "2099-12-31T23:59:59Z"
          packages:
            - product_id: "test-product"
              budget: 10000
              pricing_option_id: "test-pricing"

          idempotency_key: "$generate:uuid_v4#schema_validation_past_start_reject"
          context:
            correlation_id: "schema_validation--past_start_date_reject"
        validations:
          - check: error_code
            value: "INVALID_REQUEST"
            description: "Past start_time rejected with INVALID_REQUEST"

          - check: field_present
            path: "context"
            description: "Response echoes back the context object"
          - check: field_value
            path: "context.correlation_id"
            value: "schema_validation--past_start_date_reject"
            description: "Context correlation_id returned unchanged"
