diff --git a/.changeset/odd-bushes-poke.md b/.changeset/odd-bushes-poke.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/odd-bushes-poke.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs/storyboards/brand_rights.yaml b/docs/storyboards/brand_rights.yaml new file mode 100644 index 0000000000..ea2c432065 --- /dev/null +++ b/docs/storyboards/brand_rights.yaml @@ -0,0 +1,225 @@ +id: brand_rights +version: "1.0.0" +title: "Brand identity and rights licensing" +category: brand_rights +summary: "Brand agent that serves identity assets and licenses rights for AI-generated content." + +narrative: | + You run a brand agent — a system that holds brand identity data (logos, colors, fonts, + tone of voice) and licenses rights for AI-generated content. A buyer agent connects to + discover your brand identity, browse available rights, acquire licenses, manage them over + time, and submit generated creatives for brand approval. + + Brand agents are the bridge between brand owners and generative AI. When a DSP or + creative platform wants to generate an ad for your brand, they call your brand agent + to get the identity guidelines, license the right to generate content, and then submit + the result for approval before it goes live. + + This storyboard covers the full brand rights lifecycle: discovering the brand, browsing + rights, acquiring a license, managing it, and approving generated content. + +agent: + interaction_model: brand_rights_holder + capabilities: + - brand_identity + - rights_licensing + examples: + - "Rights management platforms" + - "Talent agencies" + - "Brand licensing services" + - "Enterprise brand portals" + +caller: + role: buyer_agent + example: "Pinnacle Agency (creative buyer)" + +prerequisites: + description: | + The caller needs brand context for identity discovery and campaign parameters for + rights acquisition. The test kit provides a sample brand with visual identity assets. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: identity_discovery + title: "Brand identity discovery" + narrative: | + The buyer retrieves the brand's public identity — logos, colors, fonts, tone of + voice, and visual guidelines. This is the foundation for any generative content: + the buyer needs to know what the brand looks like and sounds like before generating + anything. + + steps: + - id: get_brand_identity + title: "Retrieve brand identity" + narrative: | + The buyer calls get_brand_identity to retrieve the brand's visual and verbal + identity. Public-tier access returns basic assets. Authorized access (after + account linking) provides high-resolution assets, voice configs, and detailed + tone guidelines. + task: get_brand_identity + schema_ref: "brand/get-brand-identity-request.json" + response_schema_ref: "brand/get-brand-identity-response.json" + doc_ref: "/brand-protocol/tasks/get_brand_identity" + stateful: false + expected: | + Return brand identity data: + - Logos at multiple resolutions + - Brand colors (primary, secondary, accent) + - Typography (fonts, weights, sizes) + - Tone of voice guidelines + - Visual style guidelines + + sample_request: + brand: + domain: "acmeoutdoor.com" + + validations: + - check: response_schema + description: "Response matches get-brand-identity-response.json schema" + + - id: rights_search + title: "Browse available rights" + narrative: | + The buyer discovers what rights are available for licensing. Rights define what + generative content can be created — image generation, video synthesis, voice + cloning, copy writing — and at what terms. + + steps: + - id: get_rights + title: "Discover available rights" + narrative: | + The buyer calls get_rights to see what content generation rights the brand + offers. Each right specifies the type of content, pricing, duration, and + any constraints (impression caps, geo restrictions, etc.). + task: get_rights + schema_ref: "brand/get-rights-request.json" + response_schema_ref: "brand/get-rights-response.json" + doc_ref: "/brand-protocol/tasks/get_rights" + stateful: false + expected: | + Return available rights with pricing: + - Right types (image_generation, video_synthesis, copy_writing, etc.) + - Pricing options per right + - Duration and renewal terms + - Usage constraints (impression caps, geo restrictions) + + sample_request: + brand: + domain: "acmeoutdoor.com" + + validations: + - check: response_schema + description: "Response matches get-rights-response.json schema" + + - id: rights_acquisition + title: "Acquire a rights license" + narrative: | + The buyer selects a right and acquires a license. This is the contractual commitment — + the buyer pays for the right to generate content of a specific type for a defined + period. The brand agent issues generation credentials. + + steps: + - id: acquire_rights + title: "Purchase a rights license" + narrative: | + The buyer acquires a specific right by selecting a pricing option and providing + campaign context. The brand agent validates the request, processes payment, + and returns a rights grant with generation credentials. + task: acquire_rights + schema_ref: "brand/acquire-rights-request.json" + response_schema_ref: "brand/acquire-rights-response.json" + doc_ref: "/brand-protocol/tasks/acquire_rights" + stateful: true + expected: | + Return the acquired rights grant: + - rights_grant_id: unique identifier + - status: active + - Generation credentials + - Expiration date and usage limits + - Terms and constraints + + sample_request: + brand: + domain: "acmeoutdoor.com" + right_type: "image_generation" + pricing_option_id: "standard_monthly" + campaign: + name: "Acme Outdoor Summer 2026" + start_date: "2026-04-01" + end_date: "2026-06-30" + + validations: + - check: response_schema + description: "Response matches acquire-rights-response.json schema" + + - id: rights_management + title: "Manage rights" + narrative: | + The buyer modifies an existing rights grant — extending the end date, adjusting + impression caps, or pausing generation while keeping the license active. + + steps: + - id: update_rights + title: "Modify an existing rights grant" + narrative: | + The buyer updates an active rights grant. Changes may include extending the + duration, increasing impression caps, or pausing/resuming generation. The + brand agent re-issues credentials if necessary. + task: update_rights + schema_ref: "brand/update-rights-request.json" + response_schema_ref: "brand/update-rights-response.json" + doc_ref: "/brand-protocol/tasks/update_rights" + stateful: true + expected: | + Return the updated rights grant: + - Updated expiration or caps + - New generation credentials if changed + - Status confirmation + + sample_request: + rights_grant_id: "rg_acme_summer_2026" + updates: + end_date: "2026-09-30" + impression_cap: 5000000 + + validations: + - check: response_schema + description: "Response matches update-rights-response.json schema" + + - id: creative_approval + title: "Creative approval" + narrative: | + After generating content using the licensed rights, the buyer submits the creative + for brand approval. The brand agent reviews the generated content against brand + guidelines and either approves, requests changes, or rejects it. + + steps: + - id: creative_approval + title: "Submit generated creative for brand approval" + narrative: | + The buyer submits a generated creative for the brand's review. The brand agent + evaluates it against identity guidelines, tone of voice, and any contractual + constraints from the rights grant. + task: creative_approval + schema_ref: "brand/creative-approval-request.json" + response_schema_ref: "brand/creative-approval-response.json" + doc_ref: "/brand-protocol/walkthrough-rights-licensing" + stateful: true + expected: | + Return an approval decision: + - decision: approved, changes_requested, or rejected + - Feedback on brand compliance + - Specific issues if changes requested or rejected + + sample_request: + rights_grant_id: "rg_acme_summer_2026" + creative: + creative_id: "gen_trail_pro_display" + format: "display_300x250" + assets: + - asset_type: "image" + url: "https://cdn.pinnacle-agency.example/gen-trail-pro-300x250.png" + + validations: + - check: response_schema + description: "Response matches creative-approval-response.json schema" diff --git a/docs/storyboards/campaign_governance_conditions.yaml b/docs/storyboards/campaign_governance_conditions.yaml new file mode 100644 index 0000000000..8dc749a3e6 --- /dev/null +++ b/docs/storyboards/campaign_governance_conditions.yaml @@ -0,0 +1,183 @@ +id: campaign_governance_conditions +version: "1.0.0" +title: "Campaign governance — conditional approval" +category: campaign_governance_conditions +summary: "Governance agent approves a media buy with conditions. Buyer re-checks after meeting the conditions." + +narrative: | + The buyer's governance agent evaluates a media buy that falls within spending authority but + triggers policy conditions — for example, the buy targets a channel that requires weekly + reporting, or the creative format requires brand safety review. + + The governance agent returns approved_with_conditions, attaching conditions the buyer must + honor during the campaign. The buyer can proceed with the media buy by passing the + governance context, but the conditions are binding. + + This storyboard tests the middle path between outright approval and denial: the buy is + allowed, but with strings attached. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform with governance support" + - "SSP that respects governance checks" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, and a governance agent URL. + The governance plan defines policy conditions that trigger on specific buy parameters. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: plan_registration + title: "Register governance plan with policy conditions" + narrative: | + The buyer registers a governance plan that allows the agent full spending authority + but attaches policy conditions for specific channels or formats. Buys that trigger + these policies are approved with conditions rather than denied. + + steps: + - id: sync_plans + title: "Register a governance plan with policy conditions" + narrative: | + The buyer's governance agent registers a plan with full spending authority but + custom policies that require weekly reporting for CTV buys and brand safety + review for user-generated content placements. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: campaign_governance_conditions + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - authority_level: agent_full + - custom_policies: policy conditions registered + + sample_request: + plans: + - plan_name: "Acme Outdoor conditional governance" + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + authority_level: "agent_full" + custom_policies: + - "CTV buys require weekly delivery reporting" + - "UGC placements require brand safety review before go-live" + + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: governance_check_conditions + title: "Governance check — approved with conditions" + narrative: | + The buyer proposes a media buy that includes CTV inventory. The governance agent + approves the buy but attaches the weekly reporting condition from the plan. The + buyer receives a governance context that encodes these conditions. + + steps: + - id: check_governance_conditions + title: "Pre-buy governance check (approved with conditions)" + narrative: | + The buyer calls check_governance with a media buy that includes CTV products. + The governance agent approves the buy but attaches conditions: weekly delivery + reporting is required for the CTV line items. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: campaign_governance_conditions + stateful: true + expected: | + Return an approved governance decision with conditions: + - decision: approved + - conditions: array of requirements the buyer must honor + - e.g., "Weekly delivery reporting required for CTV line items" + - governance_context: token the buyer passes to create_media_buy + - findings: may include should-severity findings noting the conditions + + sample_request: + plan_id: "gov_acme_conditional" + binding: + type: "media_buy" + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + total_budget: 40000 + packages: + - product_id: "sports_ctv_q2" + budget: 25000 + - product_id: "lifestyle_display_q2" + budget: 15000 + + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "decision" + description: "Response contains a governance decision" + - check: field_present + path: "conditions" + description: "Approval includes conditions" + - check: field_present + path: "governance_context" + description: "Response includes governance context for the media buy" + + - id: create_buy_with_conditions + title: "Create media buy with governance conditions" + narrative: | + The buyer creates the media buy, passing the governance context from the conditional + approval. The seller validates the governance approval and confirms the buy. The + conditions from the governance check are now binding for the campaign duration. + + steps: + - id: create_media_buy + title: "Create a media buy with conditional governance approval" + narrative: | + The buyer creates the media buy with the governance_context token from the + conditional approval. The seller confirms the buy. The buyer is now bound by + the conditions (weekly reporting for CTV line items). + task: 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: campaign_governance_conditions + stateful: true + expected: | + Confirm the media buy with governance approval: + - media_buy_id: your platform's identifier + - status: confirmed or active + - governance_context: echoed back confirming governance was validated + - packages: confirmed line items + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + brand: + domain: "acmeoutdoor.com" + governance_context: "gov_ctx_acme_conditional_approved" + start_time: "2026-04-01T00:00:00Z" + end_time: "2026-06-30T23:59:59Z" + packages: + - product_id: "sports_ctv_q2" + budget: 25000 + pricing_option_id: "cpm_guaranteed" + - product_id: "lifestyle_display_q2" + budget: 15000 + pricing_option_id: "cpm_standard" + + validations: + - check: response_schema + description: "Response matches create-media-buy-response.json schema" diff --git a/docs/storyboards/campaign_governance_delivery.yaml b/docs/storyboards/campaign_governance_delivery.yaml new file mode 100644 index 0000000000..bcabd9b741 --- /dev/null +++ b/docs/storyboards/campaign_governance_delivery.yaml @@ -0,0 +1,228 @@ +id: campaign_governance_delivery +version: "1.0.0" +title: "Campaign governance — delivery monitoring with drift detection" +category: campaign_governance_delivery +summary: "Governance agent monitors delivery, detects budget drift past thresholds, and triggers re-evaluation." + +narrative: | + After a media buy is confirmed with governance approval, the governance agent monitors + delivery. When spend drifts past a reallocation threshold — for example, one line item + is overspending while another is underspending — the governance agent triggers a delivery + phase re-evaluation. + + This storyboard covers the delivery monitoring governance flow: initial approval, + delivery metrics showing drift, governance re-check triggered by drift, and either + re-approval with adjusted conditions or a pause recommendation. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform with governance and delivery reporting" + - "Retail media network with pacing data" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, a governance agent URL, + and an active media buy with delivery data. The governance plan defines a + reallocation threshold that triggers re-evaluation. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: plan_registration + title: "Register governance plan with delivery monitoring" + narrative: | + The buyer registers a governance plan that includes delivery monitoring thresholds. + When spend drift exceeds the reallocation threshold, the governance agent triggers + a delivery-phase re-evaluation. + + steps: + - id: sync_plans + title: "Register a governance plan with reallocation threshold" + narrative: | + The buyer's governance agent registers a plan with full spending authority and + a 20% reallocation threshold. If any line item's spend drifts more than 20% + from the planned budget allocation, the governance agent re-evaluates. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: campaign_governance_delivery + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - authority_level: agent_full + - reallocation_threshold: 0.20 (20% drift triggers re-evaluation) + + sample_request: + plans: + - plan_name: "Acme Outdoor delivery governance" + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + authority_level: "agent_full" + reallocation_threshold: 0.20 + conditions: + - "Re-evaluate governance if any line item drifts >20% from plan" + + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: initial_approval + title: "Initial governance check — approved" + narrative: | + The buyer proposes a media buy within authority. The governance agent approves + the buy with the delivery monitoring conditions active. + + steps: + - id: check_governance_approved + title: "Pre-buy governance check (approved)" + narrative: | + The buyer calls check_governance with a media buy within authority. The governance + agent approves and notes that delivery monitoring is active with the 20% drift + threshold. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: campaign_governance_delivery + stateful: true + expected: | + Return an approved governance decision: + - decision: approved + - governance_context: token for the media buy + - monitoring: delivery phase governance is active + + sample_request: + plan_id: "gov_acme_delivery" + binding: + type: "media_buy" + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + total_budget: 40000 + packages: + - product_id: "sports_ctv_q2" + budget: 20000 + - product_id: "outdoor_video_q2" + budget: 20000 + + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "decision" + description: "Response contains a governance decision" + + - id: delivery_monitoring + title: "Delivery metrics show budget drift" + narrative: | + The campaign is running. The buyer checks delivery and finds that one line item + is overspending while the other is underspending. The CTV line item has consumed + 70% of its budget at the halfway point while the video line item is at 30%. + + steps: + - id: get_delivery + title: "Check delivery metrics" + narrative: | + The buyer requests delivery data and finds budget drift. One line item is + significantly overpacing while the other is underpacing. + task: get_media_buy_delivery + schema_ref: "media-buy/get-media-buy-delivery-request.json" + response_schema_ref: "media-buy/get-media-buy-delivery-response.json" + doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: campaign_governance_delivery + stateful: true + expected: | + Return delivery metrics showing drift: + - Per-package delivery with impressions, spend, and pacing + - One package overpacing (>60% spend at 50% flight) + - One package underpacing (<40% spend at 50% flight) + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + media_buy_ids: + - "mb_acme_q2_2026" + include_package_daily_breakdown: true + + validations: + - check: response_schema + description: "Response matches get-media-buy-delivery-response.json schema" + - check: field_present + path: "media_buys" + description: "Response contains media buy delivery data" + + - id: drift_recheck + title: "Governance re-check — drift detected" + narrative: | + The buyer's governance agent detects that budget drift has exceeded the 20% + reallocation threshold. It triggers a delivery-phase governance check with the + current delivery data as evidence. The governance agent re-evaluates and returns + a decision — either re-approved with updated conditions or a recommendation to + pause and rebalance. + + steps: + - id: check_governance_drift + title: "Delivery-phase governance re-check (drift exceeded)" + narrative: | + The buyer calls check_governance with governance_phase: delivery and attaches + the current delivery metrics as evidence. The governance agent evaluates the + drift against the reallocation threshold and returns a decision. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: campaign_governance_delivery + stateful: true + expected: | + Return a governance decision about the delivery drift: + - decision: approved (with rebalancing conditions) or denied (pause recommended) + - findings: should-severity findings noting the drift amounts + - severity: should + - code: BUDGET_DRIFT_EXCEEDED + - message: explains which line items drifted and by how much + - conditions: if approved, conditions for rebalancing (e.g., "Reallocate $5K from CTV to video") + + sample_request: + plan_id: "gov_acme_delivery" + governance_phase: "delivery" + governance_context: "gov_ctx_acme_delivery_approved" + binding: + type: "media_buy" + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + media_buy_id: "mb_acme_q2_2026" + delivery_evidence: + packages: + - product_id: "sports_ctv_q2" + budget: 20000 + spend_to_date: 14000 + flight_progress: 0.50 + - product_id: "outdoor_video_q2" + budget: 20000 + spend_to_date: 6000 + flight_progress: 0.50 + + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "decision" + description: "Response contains a governance decision" + - check: field_present + path: "findings" + description: "Response contains findings about the drift" diff --git a/docs/storyboards/campaign_governance_denied.yaml b/docs/storyboards/campaign_governance_denied.yaml new file mode 100644 index 0000000000..d9f856bd1d --- /dev/null +++ b/docs/storyboards/campaign_governance_denied.yaml @@ -0,0 +1,131 @@ +id: campaign_governance_denied +version: "1.0.0" +title: "Campaign governance — denied" +category: campaign_governance_denied +summary: "Governance agent denies a media buy that exceeds the agent's spending authority. No human escalation — the buy is blocked." + +narrative: | + The buyer's governance agent registers a plan with strict spending authority. The buyer + then proposes a media buy that exceeds the agent's per-transaction threshold. The + governance agent denies the buy outright with a must-severity finding. + + Unlike the escalation storyboard, there is no human override here. The denial is final. + The buyer must reduce the buy amount or restructure into smaller transactions that fall + within the agent's authority. This tests the hard-stop governance path. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - governance_aware + examples: + - "Publisher platform with governance support" + - "Retail media network" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, and a governance agent URL. + The governance plan must define a per-transaction threshold below the intended buy amount. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: plan_registration + title: "Register governance plan" + narrative: | + The buyer registers a governance plan with strict spending authority. The plan sets + an agent_limited authority level with a $10K per-transaction threshold. Any buy above + this amount is denied without escalation. + + steps: + - id: sync_plans + title: "Register a governance plan with strict authority" + narrative: | + The buyer's governance agent registers a plan with a $10K per-transaction limit + and no escalation path. Buys that exceed this threshold are denied outright. + task: sync_plans + schema_ref: "governance/sync-plans-request.json" + response_schema_ref: "governance/sync-plans-response.json" + doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: campaign_governance_denied + stateful: true + expected: | + Acknowledge the governance plan: + - plan_id: identifier for this governance plan + - authority_level: agent_limited + - per_transaction_threshold: $10K spending limit + + sample_request: + plans: + - plan_name: "Acme Outdoor strict governance" + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + authority_level: "agent_limited" + per_transaction_threshold: 10000 + escalation_path: "none" + + validations: + - check: response_schema + description: "Response matches sync-plans-response.json schema" + + - id: governance_check + title: "Governance check — denied" + narrative: | + The buyer proposes a $50K media buy against the $10K threshold. The governance agent + evaluates the binding and returns a denied decision with a must-severity finding + explaining the spending authority violation. No escalation instructions are provided + because the plan has no escalation path. + + steps: + - id: check_governance_denied + title: "Pre-buy governance check (denied, no escalation)" + narrative: | + The buyer calls check_governance with a $50K media buy binding. The governance + agent denies the buy because the total exceeds the $10K per-transaction authority. + The response includes a must-severity finding with the violation details. + task: check_governance + schema_ref: "governance/check-governance-request.json" + response_schema_ref: "governance/check-governance-response.json" + doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: campaign_governance_denied + stateful: true + expected: | + Return a denied governance decision: + - decision: denied + - findings: array with at least one must-severity finding + - severity: must + - code: SPENDING_AUTHORITY_EXCEEDED + - message: explains the threshold and how much the buy exceeds it + - No escalation instructions (plan has no escalation path) + + sample_request: + plan_id: "gov_acme_strict" + binding: + type: "media_buy" + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + total_budget: 50000 + packages: + - product_id: "sports_ctv_q2" + budget: 30000 + - product_id: "outdoor_video_q2" + budget: 20000 + + validations: + - check: response_schema + description: "Response matches check-governance-response.json schema" + - check: field_present + path: "decision" + description: "Response contains a governance decision" + - check: field_value + path: "decision" + description: "Decision is denied" + - check: field_present + path: "findings" + description: "Response contains findings explaining the denial" diff --git a/docs/storyboards/capability_discovery.yaml b/docs/storyboards/capability_discovery.yaml new file mode 100644 index 0000000000..aad1cf631e --- /dev/null +++ b/docs/storyboards/capability_discovery.yaml @@ -0,0 +1,105 @@ +id: capability_discovery +version: "1.0.0" +title: "Capability discovery" +category: capability_discovery +summary: "Buyer calls get_adcp_capabilities to discover what an agent supports before making any buying or creative decisions." + +narrative: | + Before doing anything else, a buyer agent calls get_adcp_capabilities to learn what your + platform supports. This is the first call in any new relationship — the buyer needs to know + your protocol version, supported domains (media buy, creative, governance, signals, SI), + pricing models, targeting capabilities, and creative specs. + + The response shapes every subsequent call. If you declare media_buy support, the buyer knows + it can send briefs and create media buys. If you declare creative support, the buyer knows + it can sync or build creatives. The buyer uses this to decide which storyboards apply to your + agent and what flows to attempt. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + examples: + - "Any AdCP-compliant agent" + - "Publisher platforms" + - "Creative ad servers" + - "DSPs" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + No prerequisites. This is the very first call a buyer makes to any agent. + The test kit provides brand context but it is not required for this storyboard. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: protocol_discovery + title: "Protocol and capability discovery" + narrative: | + The buyer calls get_adcp_capabilities to learn what your agent supports. This call + takes no required arguments — it is a read-only introspection of the agent's + capabilities. The buyer may optionally filter by protocol domain or major version. + + steps: + - id: get_capabilities + title: "Discover agent capabilities" + narrative: | + The buyer calls get_adcp_capabilities with no arguments. Your agent returns its + full capability declaration: protocol version, supported domains, pricing models, + targeting options, creative specs, and any extensions. + + This response is the source of truth for what the buyer can do with your agent. + Every field the buyer reads here determines which tasks it will call next. + 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 your agent's full capability declaration: + - adcp.major_versions: which protocol versions you support (e.g., [3]) + - supported_protocols: which domains are available (media_buy, creative, governance, signals, sponsored_intelligence) + - media_buy: pricing models, delivery types, targeting, reporting features + - creative: library support, generation, transformation capabilities + - governance: property and creative features the governance agent can evaluate + - account: auth model, billing support, sandbox mode + + sample_request: {} + + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" + - check: field_present + path: "adcp.major_versions" + description: "Agent declares supported protocol versions" + - check: field_present + path: "supported_protocols" + description: "Agent declares which protocol domains it supports" + + - id: get_capabilities_filtered + title: "Discover capabilities filtered by protocol" + narrative: | + The buyer calls get_adcp_capabilities again, this time filtering for a specific + protocol domain. This is useful when the buyer already knows what it wants and + only needs details for one domain (e.g., just media_buy capabilities). + 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 filtered to the requested protocol domain. The response + should include the same structure but only the requested domain details. + + sample_request: + protocols: + - "media_buy" + + validations: + - check: response_schema + description: "Response matches get-adcp-capabilities-response.json schema" diff --git a/docs/storyboards/content_standards.yaml b/docs/storyboards/content_standards.yaml new file mode 100644 index 0000000000..7a86bc0a00 --- /dev/null +++ b/docs/storyboards/content_standards.yaml @@ -0,0 +1,248 @@ +id: content_standards +version: "1.0.0" +title: "Content standards" +category: content_standards +summary: "Define creative quality rules, calibrate content against them, and validate that delivered ads met the standards." + +narrative: | + You run a governance agent that manages content standards — rules that define what + creative content is acceptable for a brand or campaign. Buyers create standards that + specify quality requirements, safety constraints, and brand compliance rules. Your + agent stores these standards, calibrates sample content against them, and validates + that delivered creatives met the requirements. + + Content standards complement property governance. Property lists control where ads appear; + content standards control what the ads look like. Together they form a complete brand + safety framework. + + This storyboard covers the full content standards lifecycle: defining standards, querying + them, calibrating content before launch, and validating delivery after the fact. + +agent: + interaction_model: governance_agent + capabilities: + - content_standards + - creative_quality + examples: + - "Creative quality platforms" + - "Brand safety services" + - "Ad verification platforms" + - "GARM-aligned quality tools" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and creative quality requirements. The test kit + provides a sample brand with visual assets for calibration. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: create_standards + title: "Define content standards" + narrative: | + The buyer creates content standards that define what creative content is acceptable. + Standards include quality requirements, safety constraints, and brand-specific rules. + + steps: + - id: create_content_standards + title: "Create content standards" + narrative: | + The buyer defines a set of content standards. These rules will be used to + evaluate creatives before and after delivery. + task: create_content_standards + schema_ref: "content-standards/create-content-standards-request.json" + response_schema_ref: "content-standards/create-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/create_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the created content standards: + - standards_id: platform-assigned identifier + - Rules registered with severity levels + - Status: active + + sample_request: + brand: + domain: "acmeoutdoor.com" + name: "Acme Outdoor creative standards" + rules: + - category: "brand_safety" + description: "No violent or controversial imagery" + severity: "must" + - category: "quality" + description: "Minimum 72 DPI for display assets" + severity: "should" + - category: "brand_compliance" + description: "Brand colors must match palette within 5% tolerance" + severity: "must" + + validations: + - check: response_schema + description: "Response matches create-content-standards-response.json schema" + + - id: list_and_get + title: "Query content standards" + narrative: | + The buyer lists all content standards for the account and retrieves a specific + standard to inspect its rules. + + steps: + - id: list_content_standards + title: "List all content standards" + narrative: | + The buyer lists all content standards for the brand. The response includes + standard metadata without full rule details. + task: list_content_standards + schema_ref: "content-standards/list-content-standards-request.json" + response_schema_ref: "content-standards/list-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/list_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return content standard summaries: + - Array of standards with standards_id, name, rule_count + - Status of each standard set + + sample_request: + brand: + domain: "acmeoutdoor.com" + + validations: + - check: response_schema + description: "Response matches list-content-standards-response.json schema" + + - id: get_content_standards + title: "Get a specific content standard" + narrative: | + The buyer retrieves the full details of a specific content standard, including + all rules and their severity levels. + task: get_content_standards + schema_ref: "content-standards/get-content-standards-request.json" + response_schema_ref: "content-standards/get-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/get_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the full content standard: + - standards_id, name + - All rules with categories, descriptions, and severity + + sample_request: + standards_id: "cs_acme_creative_001" + + validations: + - check: response_schema + description: "Response matches get-content-standards-response.json schema" + + - id: update_standards + title: "Update content standards" + narrative: | + The buyer modifies existing content standards — adding rules, adjusting severity, + or removing obsolete constraints. + + steps: + - id: update_content_standards + title: "Update content standards" + narrative: | + The buyer updates an existing content standard with new or modified rules. + task: update_content_standards + schema_ref: "content-standards/update-content-standards-request.json" + response_schema_ref: "content-standards/update-content-standards-response.json" + doc_ref: "/governance/content-standards/tasks/update_content_standards" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return the updated content standard: + - Updated rule set + - Confirmation of changes applied + + sample_request: + standards_id: "cs_acme_creative_001" + add_rules: + - category: "accessibility" + description: "All images must have alt text" + severity: "should" + + validations: + - check: response_schema + description: "Response matches update-content-standards-response.json schema" + + - id: calibration + title: "Calibrate content" + narrative: | + Before launching a campaign, the buyer calibrates sample creatives against the + content standards. This is a pre-flight check — does the creative meet the rules + before it goes live? + + steps: + - id: calibrate_content + title: "Calibrate content against standards" + narrative: | + The buyer submits sample creative content for evaluation against the content + standards. The governance agent checks each rule and returns a calibration + report indicating pass/fail per rule. + task: calibrate_content + schema_ref: "content-standards/calibrate-content-request.json" + response_schema_ref: "content-standards/calibrate-content-response.json" + doc_ref: "/governance/content-standards/tasks/calibrate_content" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return a calibration report: + - overall_pass: boolean + - Per-rule results with pass/fail and evidence + - Remediation guidance for failed rules + + sample_request: + standards_id: "cs_acme_creative_001" + content: + creative_id: "display_trail_pro_300x250" + format: "display_300x250" + assets: + - asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + + validations: + - check: response_schema + description: "Response matches calibrate-content-response.json schema" + + - id: delivery_validation + title: "Validate delivered content" + narrative: | + After the campaign runs, the buyer validates that the delivered creatives met the + content standards. The governance agent checks actual delivered content against + the rules and flags any violations. + + steps: + - id: validate_content_delivery + title: "Validate content delivery compliance" + narrative: | + The buyer submits delivery data with creative references. The governance agent + evaluates the delivered content against the content standards and returns + a compliance report. + task: validate_content_delivery + schema_ref: "content-standards/validate-content-delivery-request.json" + response_schema_ref: "content-standards/validate-content-delivery-response.json" + doc_ref: "/governance/content-standards/tasks/validate_content_delivery" + comply_scenario: governance_content_standards + stateful: true + expected: | + Return validation results: + - compliant: boolean overall status + - Per-creative compliance status + - Violations with rule references and evidence + + sample_request: + standards_id: "cs_acme_creative_001" + delivery: + - creative_id: "display_trail_pro_300x250" + impressions: 150000 + - creative_id: "video_30s_trail_pro" + impressions: 75000 + + validations: + - check: response_schema + description: "Response matches validate-content-delivery-response.json schema" diff --git a/docs/storyboards/creative_lifecycle.yaml b/docs/storyboards/creative_lifecycle.yaml new file mode 100644 index 0000000000..b836435f30 --- /dev/null +++ b/docs/storyboards/creative_lifecycle.yaml @@ -0,0 +1,281 @@ +id: creative_lifecycle +version: "1.0.0" +title: "Creative lifecycle" +category: creative_lifecycle +summary: "Full creative lifecycle on a stateful platform: sync multiple creatives, list with filtering, build and preview across formats, observe status transitions." + +narrative: | + You run a creative platform with a persistent library — an ad server, creative management + platform, or publisher that accepts and stores creative assets. A buyer agent pushes + multiple creatives in different formats, queries the library, builds serving tags, previews + renderings, and monitors creative status as assets move through your review pipeline. + + This storyboard covers the complete creative lifecycle from the buyer's perspective: + uploading assets, browsing the library, building deliverables across formats, and + observing status transitions as creatives move from pending_review through to accepted. + + The individual creative storyboards (template, ad server, sales agent) cover specific + interaction models. This storyboard tests the full lifecycle across multiple creatives + and formats on a single platform. + +agent: + interaction_model: stateful_preloaded + capabilities: + - has_creative_library + - supports_transformation + examples: + - "Innovid" + - "Flashtalking" + - "CM360" + - "Creative management platforms" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs creative assets in multiple formats (display, video, native) and + a brand identity. The test kit provides sample assets at standard ad dimensions. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: discover_formats + title: "Discover accepted formats" + narrative: | + Before pushing any creatives, the buyer discovers what formats your platform accepts. + This determines which assets to prepare and what dimensions and specs to target. + + steps: + - id: list_formats + title: "List creative formats" + narrative: | + The buyer calls list_creative_formats to discover what your platform accepts. + The response defines format specs: dimensions, asset requirements, mime types, + and any platform-specific constraints. + task: list_creative_formats + schema_ref: "creative/list-creative-formats-request.json" + response_schema_ref: "creative/list-creative-formats-response.json" + doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle + stateful: false + expected: | + Return all creative formats your platform accepts: + - format_id with your agent_url and unique id + - Asset requirements (dimensions, file sizes, mime types) + - Render dimensions + - At least two formats (e.g., display and video) + + sample_request: {} + + validations: + - check: response_schema + description: "Response matches list-creative-formats-response.json schema" + - check: field_present + path: "formats" + description: "Response contains formats array" + + - id: sync_multiple + title: "Sync multiple creatives" + narrative: | + The buyer pushes three creatives in different formats to your platform: a display + banner, a video spot, and a native card. Your platform validates each creative + against its format specs and returns per-creative status. + + steps: + - id: sync_creatives + title: "Push three creatives in different formats" + narrative: | + The buyer syncs three creatives simultaneously: a 300x250 display banner, a 30s + video spot, and a native content card. Your platform validates each against its + format specs and returns per-creative action and status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Accept and validate all three creatives: + - Per-creative action: created + - Per-creative status: accepted or pending_review + - Validation results for each creative + - Platform-assigned IDs if applicable + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + creatives: + - creative_id: "display_trail_pro_300x250" + name: "Trail Pro 3000 - Display 300x250" + format_id: + agent_url: "https://your-platform.example.com" + id: "display_300x250" + assets: + - asset_id: "image" + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-300x250.png" + mime_type: "image/png" + - creative_id: "video_30s_trail_pro" + name: "Trail Pro 3000 - 30s Video" + format_id: + agent_url: "https://your-platform.example.com" + id: "video_30s" + assets: + - asset_id: "video" + asset_type: "video" + url: "https://cdn.pinnacle-agency.example/trail-pro-30s.mp4" + mime_type: "video/mp4" + - creative_id: "native_trail_pro" + name: "Trail Pro 3000 - Native Card" + format_id: + agent_url: "https://your-platform.example.com" + id: "native_content" + assets: + - asset_id: "image" + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-native.png" + mime_type: "image/png" + - asset_id: "headline" + asset_type: "text" + content: "Trail Pro 3000 — Built for the Summit" + + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + - check: field_present + path: "results" + description: "Response contains per-creative results" + + - id: list_and_filter + title: "List creatives with filtering" + narrative: | + The buyer queries the creative library to see their synced creatives. First a broad + list, then filtered by format. This verifies the library correctly stores and indexes + the pushed creatives. + + steps: + - id: list_all + title: "List all creatives in library" + narrative: | + The buyer calls list_creatives with no filters to see all creatives in the + library for their account. The response includes the three creatives synced + in the previous phase. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Return creatives in the library: + - creatives array containing the synced items + - Each creative includes: creative_id, name, format_id, status + - At least three creatives from the sync phase + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains creatives array" + + - id: list_filtered + title: "List creatives filtered by format" + narrative: | + The buyer lists creatives filtered to a specific format (display only). The + response should only include creatives matching that format. + task: list_creatives + schema_ref: "creative/list-creatives-request.json" + response_schema_ref: "creative/list-creatives-response.json" + doc_ref: "/creative/task-reference/list_creatives" + comply_scenario: creative_lifecycle + stateful: true + expected: | + Return only creatives matching the format filter: + - creatives array filtered to display format + - Should include display_trail_pro_300x250 but not video or native + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + filters: + format_ids: + - agent_url: "https://your-platform.example.com" + id: "display_300x250" + + validations: + - check: response_schema + description: "Response matches list-creatives-response.json schema" + - check: field_present + path: "creatives" + description: "Response contains filtered creatives" + + - id: build_and_preview + title: "Build and preview across formats" + narrative: | + The buyer builds serving tags and previews renderings for the synced creatives. + This tests multi-format output: a display tag, a VAST tag for video, and a + native rendering preview. + + steps: + - id: preview_display + title: "Preview the display creative" + narrative: | + The buyer calls preview_creative for the display banner to see how it renders + in the platform's environment before going live. + task: preview_creative + schema_ref: "creative/preview-creative-request.json" + response_schema_ref: "creative/preview-creative-response.json" + doc_ref: "/creative/task-reference/preview_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a preview of the display creative: + - preview_url: rendered preview the buyer can inspect + - render_dimensions: matches the 300x250 format + - status: preview available + + sample_request: + creative_id: "display_trail_pro_300x250" + + validations: + - check: response_schema + description: "Response matches preview-creative-response.json schema" + + - id: build_video_tag + title: "Build a VAST tag for the video creative" + narrative: | + The buyer builds a serving tag for the video creative. The platform produces + a VAST-compatible tag that the buyer can traffic to ad servers. + task: build_creative + schema_ref: "creative/build-creative-request.json" + response_schema_ref: "creative/build-creative-response.json" + doc_ref: "/creative/task-reference/build_creative" + comply_scenario: creative_flow + stateful: true + expected: | + Return a built serving tag for the video creative: + - tag: VAST-compatible serving tag or URL + - format: matches the video format + - creative_id: matches the requested creative + + sample_request: + creative_id: "video_30s_trail_pro" + output_format: + agent_url: "https://your-platform.example.com" + id: "vast_30s" + + validations: + - check: response_schema + description: "Response matches build-creative-response.json schema" diff --git a/docs/storyboards/media_buy_catalog_creative.yaml b/docs/storyboards/media_buy_catalog_creative.yaml index c80dca58de..ac4f435eb6 100644 --- a/docs/storyboards/media_buy_catalog_creative.yaml +++ b/docs/storyboards/media_buy_catalog_creative.yaml @@ -106,6 +106,7 @@ phases: schema_ref: "creative/list-creative-formats-request.json" response_schema_ref: "creative/list-creative-formats-response.json" doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle stateful: false expected: | Return creative formats that accept catalog assets. Look for formats with @@ -213,6 +214,7 @@ phases: 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: full_sales_flow stateful: false expected: | Return products that support catalog-driven creative. Products should indicate @@ -249,6 +251,7 @@ phases: 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: create_media_buy stateful: true expected: | Create the media buy with catalog-driven packages. Return: @@ -432,6 +435,7 @@ phases: schema_ref: "media-buy/get-media-buy-delivery-request.json" response_schema_ref: "media-buy/get-media-buy-delivery-response.json" doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow stateful: true expected: | Return delivery metrics including impressions, clicks, spend, and diff --git a/docs/storyboards/media_buy_governance_escalation.yaml b/docs/storyboards/media_buy_governance_escalation.yaml index 8befa8053f..c10c5e90ff 100644 --- a/docs/storyboards/media_buy_governance_escalation.yaml +++ b/docs/storyboards/media_buy_governance_escalation.yaml @@ -130,6 +130,7 @@ phases: schema_ref: "governance/sync-plans-request.json" response_schema_ref: "governance/sync-plans-response.json" doc_ref: "/governance/campaign/tasks/sync_plans" + comply_scenario: campaign_governance stateful: true expected: | Acknowledge the governance plan: @@ -172,6 +173,7 @@ phases: 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: full_sales_flow stateful: false expected: | Return products matching the brief with pricing that totals above $20K: @@ -217,6 +219,7 @@ phases: schema_ref: "governance/check-governance-request.json" response_schema_ref: "governance/check-governance-response.json" doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: campaign_governance_denied stateful: true expected: | Return a denied governance decision: @@ -275,6 +278,7 @@ phases: schema_ref: "governance/check-governance-request.json" response_schema_ref: "governance/check-governance-response.json" doc_ref: "/governance/campaign/tasks/check_governance" + comply_scenario: campaign_governance_conditions stateful: true expected: | Return an approved governance decision with conditions: @@ -337,6 +341,7 @@ phases: 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: create_media_buy stateful: true expected: | Confirm the media buy with governance approval: @@ -391,6 +396,7 @@ phases: schema_ref: "governance/report-plan-outcome-request.json" response_schema_ref: "governance/report-plan-outcome-response.json" doc_ref: "/governance/campaign/tasks/report_plan_outcome" + comply_scenario: campaign_governance stateful: true expected: | Acknowledge the outcome report: @@ -436,6 +442,7 @@ phases: schema_ref: "governance/get-plan-audit-logs-request.json" response_schema_ref: "governance/get-plan-audit-logs-response.json" doc_ref: "/governance/campaign/tasks/get_plan_audit_logs" + comply_scenario: campaign_governance stateful: false expected: | Return the complete audit trail for the governance plan: diff --git a/docs/storyboards/media_buy_guaranteed_approval.yaml b/docs/storyboards/media_buy_guaranteed_approval.yaml index 3ea72f4ae1..22c38194e8 100644 --- a/docs/storyboards/media_buy_guaranteed_approval.yaml +++ b/docs/storyboards/media_buy_guaranteed_approval.yaml @@ -106,6 +106,7 @@ phases: 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: full_sales_flow stateful: false expected: | Return guaranteed products matching the brief. Each product should include: @@ -164,6 +165,7 @@ phases: 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: create_media_buy stateful: true expected: | Return the media buy in submitted status: @@ -221,6 +223,7 @@ phases: schema_ref: "media-buy/get-media-buys-request.json" response_schema_ref: "media-buy/get-media-buys-response.json" doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle stateful: true expected: | Return the media buy in pending_approval status: @@ -271,6 +274,7 @@ phases: schema_ref: "media-buy/get-media-buys-request.json" response_schema_ref: "media-buy/get-media-buys-response.json" doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle stateful: true expected: | Return the media buy in confirmed or active status: @@ -315,6 +319,7 @@ phases: schema_ref: "creative/sync-creatives-request.json" response_schema_ref: "creative/sync-creatives-response.json" doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync stateful: true expected: | Accept and validate creatives: @@ -363,6 +368,7 @@ phases: schema_ref: "media-buy/get-media-buy-delivery-request.json" response_schema_ref: "media-buy/get-media-buy-delivery-response.json" doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow stateful: true expected: | Return delivery metrics for the guaranteed media buy: diff --git a/docs/storyboards/media_buy_non_guaranteed.yaml b/docs/storyboards/media_buy_non_guaranteed.yaml index 5dbe93490d..1a77706b59 100644 --- a/docs/storyboards/media_buy_non_guaranteed.yaml +++ b/docs/storyboards/media_buy_non_guaranteed.yaml @@ -58,6 +58,7 @@ phases: 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: full_sales_flow stateful: false expected: | Return non-guaranteed products matching the brief. Each product should include: @@ -114,6 +115,7 @@ phases: 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: create_media_buy stateful: true expected: | Return the media buy in completed status: @@ -170,6 +172,7 @@ phases: schema_ref: "media-buy/get-media-buys-request.json" response_schema_ref: "media-buy/get-media-buys-response.json" doc_ref: "/media-buy/task-reference/get_media_buys" + comply_scenario: media_buy_lifecycle stateful: true expected: | Return the media buy with pacing data: @@ -211,6 +214,7 @@ phases: schema_ref: "media-buy/update-media-buy-request.json" response_schema_ref: "media-buy/update-media-buy-response.json" doc_ref: "/media-buy/task-reference/update_media_buy" + comply_scenario: media_buy_lifecycle stateful: true expected: | Apply the updates and return the modified media buy: @@ -254,6 +258,7 @@ phases: schema_ref: "media-buy/get-media-buy-delivery-request.json" response_schema_ref: "media-buy/get-media-buy-delivery-response.json" doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow stateful: true expected: | Return delivery metrics for the non-guaranteed media buy: diff --git a/docs/storyboards/media_buy_proposal_mode.yaml b/docs/storyboards/media_buy_proposal_mode.yaml index edbe7b504b..336c2f983b 100644 --- a/docs/storyboards/media_buy_proposal_mode.yaml +++ b/docs/storyboards/media_buy_proposal_mode.yaml @@ -101,6 +101,7 @@ phases: 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: full_sales_flow stateful: false expected: | Return products and proposals matching the brief: @@ -155,6 +156,7 @@ phases: 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: full_sales_flow stateful: true expected: | Return the refined proposal: @@ -208,6 +210,7 @@ phases: 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: create_media_buy stateful: true expected: | Convert the proposal into a confirmed media buy: @@ -251,6 +254,7 @@ phases: schema_ref: "creative/list-creative-formats-request.json" response_schema_ref: "creative/list-creative-formats-response.json" doc_ref: "/creative/task-reference/list_creative_formats" + comply_scenario: creative_lifecycle stateful: false expected: | Return creative formats your platform accepts. Each format should define: @@ -277,6 +281,7 @@ phases: schema_ref: "creative/sync-creatives-request.json" response_schema_ref: "creative/sync-creatives-response.json" doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync stateful: true expected: | Accept and validate creatives: @@ -335,6 +340,7 @@ phases: schema_ref: "media-buy/get-media-buy-delivery-request.json" response_schema_ref: "media-buy/get-media-buy-delivery-response.json" doc_ref: "/media-buy/task-reference/get_media_buy_delivery" + comply_scenario: reporting_flow stateful: true expected: | Return delivery metrics for the media buy: diff --git a/docs/storyboards/media_buy_seller.yaml b/docs/storyboards/media_buy_seller.yaml index 1edfb80abf..0cf72fe47e 100644 --- a/docs/storyboards/media_buy_seller.yaml +++ b/docs/storyboards/media_buy_seller.yaml @@ -69,7 +69,7 @@ phases: schema_ref: "account/sync-accounts-request.json" response_schema_ref: "account/sync-accounts-response.json" doc_ref: "/accounts/tasks/sync_accounts" - comply_scenario: account_setup + # No TestScenario exists for account setup stateful: true expected: | Return the account with: @@ -125,7 +125,7 @@ phases: schema_ref: "account/sync-governance-request.json" response_schema_ref: "account/sync-governance-response.json" doc_ref: "/accounts/tasks/sync_governance" - comply_scenario: governance_setup + # No TestScenario exists for governance registration stateful: true expected: | Acknowledge the governance agents. Your platform should: @@ -177,7 +177,7 @@ phases: 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: media_buy_flow + comply_scenario: full_sales_flow stateful: false expected: | Return products matching the brief. Each product should include: @@ -239,7 +239,7 @@ phases: 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: media_buy_flow + comply_scenario: full_sales_flow stateful: true expected: | Return updated products reflecting the refinements. The response should: @@ -307,7 +307,7 @@ phases: 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: media_buy_flow + comply_scenario: create_media_buy stateful: true expected: | Process the media buy request and return one of: @@ -374,7 +374,7 @@ phases: schema_ref: "media-buy/get-media-buys-request.json" response_schema_ref: "media-buy/get-media-buys-response.json" doc_ref: "/media-buy/task-reference/get_media_buys" - comply_scenario: media_buy_flow + comply_scenario: media_buy_lifecycle stateful: true expected: | Return the current state of the media buy: @@ -424,7 +424,7 @@ phases: schema_ref: "creative/list-creative-formats-request.json" response_schema_ref: "creative/list-creative-formats-response.json" doc_ref: "/creative/task-reference/list_creative_formats" - comply_scenario: creative_sync + comply_scenario: creative_lifecycle stateful: false expected: | Return creative formats your platform accepts. Each format should define: @@ -518,7 +518,7 @@ phases: schema_ref: "media-buy/get-media-buy-delivery-request.json" response_schema_ref: "media-buy/get-media-buy-delivery-response.json" doc_ref: "/media-buy/task-reference/get_media_buy_delivery" - comply_scenario: media_buy_flow + comply_scenario: reporting_flow stateful: true expected: | Return delivery metrics for the media buy: diff --git a/docs/storyboards/property_governance.yaml b/docs/storyboards/property_governance.yaml new file mode 100644 index 0000000000..584e63aebc --- /dev/null +++ b/docs/storyboards/property_governance.yaml @@ -0,0 +1,236 @@ +id: property_governance +version: "1.0.0" +title: "Property governance" +category: property_governance +summary: "Manage brand safety property lists — create inclusion/exclusion lists, query and update them, validate delivery compliance." + +narrative: | + You run a governance agent that manages property lists for brand safety. Buyers create + inclusion and exclusion lists that define where their ads can and cannot appear. Your + agent stores these lists, lets buyers query and update them, and validates that actual + ad delivery complied with the property constraints. + + Property governance is how brands control their environment. An inclusion list says + "only show my ads on these properties." An exclusion list says "never show my ads here." + The validation step checks after the fact: did the seller actually respect the lists? + + This storyboard covers the full property list lifecycle: creating lists, querying them, + updating and deleting, and validating that delivery matched the constraints. + +agent: + interaction_model: governance_agent + capabilities: + - property_governance + - brand_safety + examples: + - "IAS" + - "DoubleVerify" + - "GARM-aligned platforms" + - "Brand safety services" + +caller: + role: buyer_agent + example: "Pinnacle Agency (buyer)" + +prerequisites: + description: | + The caller needs a brand identity and property domain knowledge. The test kit + provides a sample brand with campaign context. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: create_list + title: "Create property lists" + narrative: | + The buyer creates inclusion and exclusion property lists for the campaign. These + define the safe and unsafe environments for the brand's ads. + + steps: + - id: create_inclusion_list + title: "Create an inclusion list" + narrative: | + The buyer creates an inclusion list specifying which properties (domains, + apps, channels) are approved for ad placement. + task: create_property_list + schema_ref: "property/create-property-list-request.json" + response_schema_ref: "property/create-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return the created property list: + - list_id: platform-assigned identifier + - list_type: inclusion + - Properties registered + - Status: active + + sample_request: + brand: + domain: "acmeoutdoor.com" + list_type: "inclusion" + name: "Acme Outdoor approved properties" + properties: + - domain: "outdoormagazine.example.com" + - domain: "hikingtrails.example.com" + - domain: "campinggear.example.com" + + validations: + - check: response_schema + description: "Response matches create-property-list-response.json schema" + + - id: list_and_get + title: "Query property lists" + narrative: | + The buyer lists all property lists for the account and retrieves a specific list + to inspect its contents. + + steps: + - id: list_property_lists + title: "List all property lists" + narrative: | + The buyer lists all property lists for the brand. The response includes list + metadata (name, type, property count) without full property details. + task: list_property_lists + schema_ref: "property/list-property-lists-request.json" + response_schema_ref: "property/list-property-lists-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: property_list_filters + stateful: true + expected: | + Return property list summaries: + - Array of lists with list_id, name, list_type, property_count + - Includes both inclusion and exclusion lists + + sample_request: + brand: + domain: "acmeoutdoor.com" + + validations: + - check: response_schema + description: "Response matches list-property-lists-response.json schema" + + - id: get_property_list + title: "Get a specific property list" + narrative: | + The buyer retrieves the full details of a specific property list, including + all properties in the list. + task: get_property_list + schema_ref: "property/get-property-list-request.json" + response_schema_ref: "property/get-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return the full property list: + - list_id, name, list_type + - All properties in the list with their details + + sample_request: + list_id: "pl_acme_inclusion_001" + + validations: + - check: response_schema + description: "Response matches get-property-list-response.json schema" + + - id: update_list + title: "Update property lists" + narrative: | + The buyer modifies an existing property list — adding or removing properties + as brand safety requirements evolve. + + steps: + - id: update_property_list + title: "Update a property list" + narrative: | + The buyer adds new properties to or removes properties from an existing list. + task: update_property_list + schema_ref: "property/update-property-list-request.json" + response_schema_ref: "property/update-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return the updated property list: + - Updated property count + - Confirmation of additions and removals + + sample_request: + list_id: "pl_acme_inclusion_001" + add: + - domain: "mountaineering.example.com" + remove: + - domain: "campinggear.example.com" + + validations: + - check: response_schema + description: "Response matches update-property-list-response.json schema" + + - id: delete_list + title: "Delete a property list" + narrative: | + The buyer removes a property list that is no longer needed. Active media buys + referencing the list should be notified. + + steps: + - id: delete_property_list + title: "Delete a property list" + narrative: | + The buyer deletes a property list. The governance agent removes the list and + returns confirmation. + task: delete_property_list + schema_ref: "property/delete-property-list-request.json" + response_schema_ref: "property/delete-property-list-response.json" + doc_ref: "/governance/property/tasks/property_lists" + comply_scenario: governance_property_lists + stateful: true + expected: | + Confirm deletion: + - list_id: the deleted list + - status: deleted + + sample_request: + list_id: "pl_acme_inclusion_001" + + validations: + - check: response_schema + description: "Response matches delete-property-list-response.json schema" + + - id: delivery_validation + title: "Validate delivery compliance" + narrative: | + After ads have been delivered, the buyer validates that the seller respected the + property lists. The governance agent checks actual delivery data against the + inclusion/exclusion constraints. + + steps: + - id: validate_property_delivery + title: "Validate property compliance" + narrative: | + The buyer submits delivery data and the governance agent checks whether all + placements complied with the property lists. Non-compliant placements are + flagged with details. + task: validate_property_delivery + schema_ref: "property/validate-property-delivery-request.json" + response_schema_ref: "property/validate-property-delivery-response.json" + doc_ref: "/governance/property/tasks/validate_property_delivery" + comply_scenario: governance_property_lists + stateful: true + expected: | + Return validation results: + - compliant: boolean overall status + - Per-placement compliance status + - Violations with details (property, list, severity) + + sample_request: + brand: + domain: "acmeoutdoor.com" + list_id: "pl_acme_inclusion_001" + delivery: + - property: "outdoormagazine.example.com" + impressions: 50000 + - property: "randomsite.example.com" + impressions: 200 + + validations: + - check: response_schema + description: "Response matches validate-property-delivery-response.json schema" diff --git a/docs/storyboards/schema.yaml b/docs/storyboards/schema.yaml index 0e899635e9..dc9627339f 100644 --- a/docs/storyboards/schema.yaml +++ b/docs/storyboards/schema.yaml @@ -13,12 +13,12 @@ # id: string (unique identifier, e.g., "creative_template") # version: string (semver, e.g., "1.0.0") # title: string (human-readable title) -# category: enum (creative_template | creative_ad_server | creative_sales_agent | creative_generative | media_buy_seller | media_buy_guaranteed_approval | media_buy_non_guaranteed | media_buy_proposal_mode | media_buy_governance_escalation | media_buy_catalog_creative | signal_marketplace | signal_owned) +# category: enum (capability_discovery | creative_template | creative_ad_server | creative_sales_agent | creative_generative | creative_lifecycle | media_buy_seller | media_buy_guaranteed_approval | media_buy_non_guaranteed | media_buy_proposal_mode | media_buy_governance_escalation | media_buy_catalog_creative | campaign_governance_denied | campaign_governance_conditions | campaign_governance_delivery | signal_marketplace | signal_owned | social_platform | si_session | brand_rights | property_governance | content_standards) # summary: string (one-line description for listings) # narrative: string (paragraph explaining the overall flow) # # agent: -# interaction_model: enum (stateless_transform | stateful_preloaded | stateful_push | stateless_generate | media_buy_seller | marketplace_catalog | owned_signals) +# interaction_model: enum (stateless_transform | stateful_preloaded | stateful_push | stateless_generate | media_buy_seller | marketplace_catalog | owned_signals | si_platform | brand_rights_holder | governance_agent) # capabilities: string[] (AdCP capability flags: supports_transformation, has_creative_library, supports_generation, sells_media, accepts_briefs, supports_guaranteed, supports_non_guaranteed, catalog_signals) # examples: string[] (real-world examples: "Celtra", "Innovid") # diff --git a/docs/storyboards/si_session.yaml b/docs/storyboards/si_session.yaml new file mode 100644 index 0000000000..14227fddef --- /dev/null +++ b/docs/storyboards/si_session.yaml @@ -0,0 +1,163 @@ +id: si_session +version: "1.0.0" +title: "Sponsored intelligence session" +category: si_session +summary: "Conversational ad session on an AI platform — discover offerings, initiate a session, exchange messages, and terminate." + +narrative: | + You run an AI platform that supports sponsored intelligence — conversational ad experiences + embedded in AI-powered search, chat, or assistant products. A buyer agent connects to + discover what SI offerings are available, initiate a session, send messages within the + conversation, and cleanly terminate when done. + + Sponsored intelligence is fundamentally different from display or video advertising. The + ad experience is conversational — the user asks a question, the AI responds, and sponsored + content is woven into the response in a way that is transparent and relevant. + + This storyboard covers the SI session lifecycle from the buyer's perspective: discovering + what the platform offers, starting a conversation, exchanging messages, and ending the + session. + +agent: + interaction_model: si_platform + capabilities: + - sponsored_intelligence + examples: + - "Perplexity" + - "ChatGPT Search" + - "Arc Browser" + - "AI assistants with ad support" + +caller: + role: buyer_agent + example: "Nova Motors (advertiser)" + +prerequisites: + description: | + The caller needs brand context and campaign parameters for SI. The test kit provides + a sample brand (Nova Motors) with signal definitions suitable for conversational + ad experiences. + test_kit: "test-kits/nova-motors.yaml" + +phases: + - id: offering_discovery + title: "Discover SI offerings" + narrative: | + Before initiating any session, the buyer discovers what sponsored intelligence + offerings the platform has available. This determines what kinds of conversational + experiences can be sponsored and at what pricing. + + steps: + - id: si_get_offering + title: "Get available SI offerings" + narrative: | + The buyer calls si_get_offering to learn what conversational ad experiences + the platform supports. The response describes available offerings with pricing, + targeting options, and format specifications. + task: si_get_offering + schema_ref: "sponsored-intelligence/si-get-offering-request.json" + response_schema_ref: "sponsored-intelligence/si-get-offering-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_get_offering" + comply_scenario: si_availability + stateful: false + expected: | + Return available SI offerings: + - Offering descriptions with pricing + - Supported conversation types + - Targeting and context options + - Format specifications for sponsored content + + sample_request: {} + + validations: + - check: response_schema + description: "Response matches si-get-offering-response.json schema" + + - id: session_lifecycle + title: "Session lifecycle" + narrative: | + The buyer initiates a session, exchanges messages within it, and terminates + cleanly. Each session represents a single conversational ad experience — the + buyer provides context and the platform weaves sponsored content into the + conversation. + + steps: + - id: si_initiate_session + title: "Start a conversation session" + narrative: | + The buyer initiates a new SI session with campaign context. The platform + creates a session and returns a session ID that the buyer uses for subsequent + messages. + task: si_initiate_session + schema_ref: "sponsored-intelligence/si-initiate-session-request.json" + response_schema_ref: "sponsored-intelligence/si-initiate-session-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_initiate_session" + comply_scenario: si_session_lifecycle + stateful: true + expected: | + Return a new session: + - session_id: platform-assigned session identifier + - status: active + - Initial context acknowledgment + - Available interaction modes + + sample_request: + brand: + domain: "novamotors.example.com" + campaign_context: "Nova EV launch — targeting environmentally conscious drivers interested in electric vehicles" + + validations: + - check: response_schema + description: "Response matches si-initiate-session-response.json schema" + + - id: si_send_message + title: "Exchange messages" + narrative: | + The buyer sends a message within the active session. The platform processes + the message and returns a response that may include sponsored content woven + into the conversational experience. + task: si_send_message + schema_ref: "sponsored-intelligence/si-send-message-request.json" + response_schema_ref: "sponsored-intelligence/si-send-message-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_send_message" + comply_scenario: si_session_lifecycle + stateful: true + expected: | + Process the message and return a response: + - Message acknowledgment + - Response content (may include sponsored elements) + - Session state (active, waiting, etc.) + + sample_request: + session_id: "si_nova_ev_001" + message: "What are the best electric vehicles for long road trips?" + + validations: + - check: response_schema + description: "Response matches si-send-message-response.json schema" + + - id: si_terminate_session + title: "End the session" + narrative: | + The buyer terminates the SI session. The platform records session metrics + and returns a summary of the conversation including any sponsored content + that was delivered. + task: si_terminate_session + schema_ref: "sponsored-intelligence/si-terminate-session-request.json" + response_schema_ref: "sponsored-intelligence/si-terminate-session-response.json" + doc_ref: "/sponsored-intelligence/tasks/si_terminate_session" + comply_scenario: si_handoff + stateful: true + expected: | + Terminate the session and return a summary: + - session_id: confirms which session was terminated + - status: terminated + - Session metrics (duration, messages exchanged) + - Sponsored content delivery summary + + sample_request: + session_id: "si_nova_ev_001" + + validations: + - check: response_schema + description: "Response matches si-terminate-session-response.json schema" diff --git a/docs/storyboards/signal_marketplace.yaml b/docs/storyboards/signal_marketplace.yaml index db4c70ca16..668b1c07c3 100644 --- a/docs/storyboards/signal_marketplace.yaml +++ b/docs/storyboards/signal_marketplace.yaml @@ -65,6 +65,7 @@ phases: schema_ref: "signals/get-signals-request.json" response_schema_ref: "signals/get-signals-response.json" doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow stateful: false expected: | Return matching signals from multiple data providers. Each signal must include: @@ -104,6 +105,7 @@ phases: schema_ref: "signals/get-signals-request.json" response_schema_ref: "signals/get-signals-response.json" doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow stateful: false expected: | Return the exact signals requested, with full metadata and pricing. @@ -150,6 +152,7 @@ phases: schema_ref: "signals/get-signals-request.json" response_schema_ref: "signals/get-signals-response.json" doc_ref: "/signals/data-providers" + comply_scenario: signals_flow stateful: false expected: | Return the requested signal with verifiable provenance metadata: @@ -194,6 +197,7 @@ phases: schema_ref: "signals/activate-signal-request.json" response_schema_ref: "signals/activate-signal-response.json" doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow stateful: true expected: | Return a deployment with: @@ -245,6 +249,7 @@ phases: schema_ref: "signals/activate-signal-request.json" response_schema_ref: "signals/activate-signal-response.json" doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow stateful: true expected: | Return a deployment with: diff --git a/docs/storyboards/signal_owned.yaml b/docs/storyboards/signal_owned.yaml index b09b752775..57e0eb1b5d 100644 --- a/docs/storyboards/signal_owned.yaml +++ b/docs/storyboards/signal_owned.yaml @@ -62,6 +62,7 @@ phases: schema_ref: "signals/get-signals-request.json" response_schema_ref: "signals/get-signals-response.json" doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow stateful: false expected: | Return matching signals from your proprietary data. Each signal must include: @@ -98,6 +99,7 @@ phases: schema_ref: "signals/get-signals-request.json" response_schema_ref: "signals/get-signals-response.json" doc_ref: "/signals/tasks/get_signals" + comply_scenario: signals_flow stateful: false expected: | Return only signals matching the filter criteria. If no signals match, @@ -134,6 +136,7 @@ phases: schema_ref: "signals/activate-signal-request.json" response_schema_ref: "signals/activate-signal-response.json" doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow stateful: true expected: | Return a deployment with: @@ -177,6 +180,7 @@ phases: schema_ref: "signals/activate-signal-request.json" response_schema_ref: "signals/activate-signal-response.json" doc_ref: "/signals/tasks/activate_signal" + comply_scenario: signals_flow stateful: true expected: | Return a deployment with: diff --git a/docs/storyboards/social_platform.yaml b/docs/storyboards/social_platform.yaml new file mode 100644 index 0000000000..4ba215a1cc --- /dev/null +++ b/docs/storyboards/social_platform.yaml @@ -0,0 +1,271 @@ +id: social_platform +version: "1.0.0" +title: "Social platform" +category: social_platform +summary: "Social media platform that accepts audience segments, native creatives, and conversion events from buyer agents." + +narrative: | + You run a social media platform — Snap, Meta, TikTok, Pinterest, or any walled garden that + sells advertising through audience-based targeting and native creative formats. A buyer agent + connects to set up an account, push audience segments, sync native creatives, track conversion + events, and monitor spend. + + Unlike open-web media buys, social platforms require the buyer to push assets into the + platform's environment. Audiences are activated via sync_audiences, creatives are pushed via + sync_creatives in platform-native formats, and conversion events flow back via log_event. + + This storyboard covers the social platform integration from the buyer's perspective: + account setup, audience activation, native creative push, event tracking, and financial + monitoring. + +agent: + interaction_model: media_buy_seller + capabilities: + - sells_media + - accepts_briefs + - supports_non_guaranteed + examples: + - "Snap" + - "Meta" + - "TikTok" + - "Pinterest" + +caller: + role: buyer_agent + example: "Scope3 (DSP)" + +prerequisites: + description: | + The caller needs a brand identity, operator credentials, audience segment definitions, + and native creative assets. The test kit provides a sample brand with creative assets + suitable for social formats. + test_kit: "test-kits/acme-outdoor.yaml" + +phases: + - id: account_setup + title: "Account setup" + narrative: | + The buyer establishes an account with the social platform and verifies its status. + Social platforms often require advertiser verification before accepting ad spend. + + steps: + - id: sync_accounts + title: "Register advertiser account" + narrative: | + The buyer registers their brand and operator with the social platform. The platform + provisions an advertiser account and returns its status. Social platforms may require + identity verification before the account goes active. + task: sync_accounts + schema_ref: "account/sync-accounts-request.json" + response_schema_ref: "account/sync-accounts-response.json" + doc_ref: "/accounts/tasks/sync_accounts" + stateful: true + expected: | + Return the advertiser account with: + - account_id: platform's identifier + - status: active or pending_approval (if verification required) + - account_scope: how the platform scopes this relationship + + sample_request: + accounts: + - brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + billing: "operator" + + validations: + - check: response_schema + description: "Response matches sync-accounts-response.json schema" + - check: field_present + path: "accounts[0].account_id" + description: "Account has a platform-assigned ID" + + - id: list_accounts + title: "Verify account status" + narrative: | + The buyer checks which accounts exist on the platform and their current status. + This confirms the account is active and shows any pending setup requirements. + task: list_accounts + schema_ref: "account/list-accounts-request.json" + response_schema_ref: "account/list-accounts-response.json" + doc_ref: "/accounts/tasks/list_accounts" + stateful: true + expected: | + Return accounts matching the query: + - accounts array with status, account_id, brand, operator + - Active accounts ready for ad operations + - Pending accounts with setup URLs if verification needed + + sample_request: + brand: + domain: "acmeoutdoor.com" + + validations: + - check: response_schema + description: "Response matches list-accounts-response.json schema" + - check: field_present + path: "accounts" + description: "Response contains accounts array" + + - id: audience_sync + title: "Audience activation" + narrative: | + The buyer pushes audience segments to the platform. Social platforms use these segments + for targeting — the buyer defines who to reach, and the platform matches against its + user base. + + steps: + - id: sync_audiences + title: "Push audience segments" + narrative: | + The buyer syncs audience segment definitions to the platform. Each segment includes + targeting criteria that the platform evaluates against its user graph. The platform + returns match rates and segment status. + task: sync_audiences + schema_ref: "media-buy/sync-audiences-request.json" + response_schema_ref: "media-buy/sync-audiences-response.json" + doc_ref: "/media-buy/task-reference/sync_audiences" + comply_scenario: sync_audiences + stateful: true + expected: | + Accept and process audience segments: + - Per-segment status: active, processing, or rejected + - Match rate estimates where available + - Platform-assigned segment IDs + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + audiences: + - audience_id: "outdoor_enthusiasts_25_54" + name: "Outdoor enthusiasts 25-54" + description: "Adults 25-54 interested in hiking, camping, and outdoor gear" + + validations: + - check: response_schema + description: "Response matches sync-audiences-response.json schema" + + - id: creative_push + title: "Native creative sync" + narrative: | + The buyer pushes native creative assets to the platform. Social platforms render ads + in their native format — the buyer provides assets (images, headlines, descriptions) + and the platform assembles them into the native ad unit. + + steps: + - id: sync_creatives + title: "Push native creative assets" + narrative: | + The buyer syncs creative assets for native ad formats. The platform validates + each creative against its format requirements and returns per-creative status. + task: sync_creatives + schema_ref: "creative/sync-creatives-request.json" + response_schema_ref: "creative/sync-creatives-response.json" + doc_ref: "/creative/task-reference/sync_creatives" + comply_scenario: creative_sync + stateful: true + expected: | + Accept and validate native creatives: + - Per-creative action: created or updated + - Per-creative status: accepted, pending_review, or rejected + - Validation errors for rejected creatives + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + creatives: + - creative_id: "native_trail_pro" + name: "Trail Pro 3000 - Native" + format_id: + agent_url: "https://social-platform.example.com" + id: "native_feed" + assets: + - asset_id: "image" + asset_type: "image" + url: "https://cdn.pinnacle-agency.example/trail-pro-native.png" + mime_type: "image/png" + - asset_id: "headline" + asset_type: "text" + content: "Trail Pro 3000 — Built for the Summit" + + validations: + - check: response_schema + description: "Response matches sync-creatives-response.json schema" + + - id: event_logging + title: "Conversion event tracking" + narrative: | + The buyer sends conversion events back to the platform for measurement and optimization. + Events include purchases, signups, and other post-click actions that the platform uses + to optimize delivery and report on campaign performance. + + steps: + - id: log_event + title: "Send conversion events" + narrative: | + The buyer logs conversion events that occurred after ad exposure. The platform + records these events for attribution, reporting, and delivery optimization. + task: log_event + schema_ref: "media-buy/log-event-request.json" + response_schema_ref: "media-buy/log-event-response.json" + doc_ref: "/media-buy/task-reference/log_event" + stateful: true + expected: | + Acknowledge the events: + - Per-event status: accepted or rejected + - Event IDs for deduplication + - Attribution window validation + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + events: + - event_type: "purchase" + event_id: "evt_trail_pro_001" + timestamp: "2026-04-05T14:30:00Z" + value: 149.99 + currency: "USD" + + validations: + - check: response_schema + description: "Response matches log-event-response.json schema" + + - id: financials + title: "Account financials" + narrative: | + The buyer checks account financials — spending, balance, and payment status. This is + essential for budget monitoring across multiple social platforms. + + steps: + - id: get_account_financials + title: "Check account spending and balance" + narrative: | + The buyer retrieves financial information for the advertiser account. The platform + returns current spend, remaining balance, and payment status. + task: get_account_financials + schema_ref: "account/get-account-financials-request.json" + response_schema_ref: "account/get-account-financials-response.json" + doc_ref: "/accounts/tasks/get_account_financials" + stateful: true + expected: | + Return account financial data: + - Current spend to date + - Remaining balance or credit + - Payment status and terms + - Budget utilization metrics + + sample_request: + account: + brand: + domain: "acmeoutdoor.com" + operator: "pinnacle-agency.com" + + validations: + - check: response_schema + description: "Response matches get-account-financials-response.json schema" diff --git a/package-lock.json b/package-lock.json index 95b898e212..4c67198fbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "adcontextprotocol", "version": "3.0.0-rc.3", "dependencies": { - "@adcp/client": "^4.22.0", + "@adcp/client": "^4.22.1", "@anthropic-ai/sdk": "^0.82.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@google/generative-ai": "^0.24.1", @@ -115,9 +115,9 @@ } }, "node_modules/@adcp/client": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@adcp/client/-/client-4.22.0.tgz", - "integrity": "sha512-Dkky5u2BNkNrwio7TMLqGfQE1QtI2S4iNngSwVRvF3hVRIZ5mdIhdkzq8dzn3VS+0ePT6372Wce8d5CHj+68Zw==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/@adcp/client/-/client-4.22.1.tgz", + "integrity": "sha512-bUd5do/V2meMVkRdh4/P9bAXbHynTzaUbSgILAxwjsjjBr/odWQU+qzx/uTXYHrApG5WZi8Ep/WAOiEbtDbcMg==", "license": "Apache-2.0", "dependencies": { "yaml": "^2.7.1" diff --git a/package.json b/package.json index 01ce3cae55..7454503a35 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "check:images": "bash scripts/check-image-quality.sh" }, "dependencies": { - "@adcp/client": "^4.22.0", + "@adcp/client": "^4.22.1", "@anthropic-ai/sdk": "^0.82.0", "@asteasolutions/zod-to-openapi": "^8.5.0", "@google/generative-ai": "^0.24.1", diff --git a/server/src/addie/jobs/compliance-heartbeat.ts b/server/src/addie/jobs/compliance-heartbeat.ts index 7eaf41169e..b9bd532514 100644 --- a/server/src/addie/jobs/compliance-heartbeat.ts +++ b/server/src/addie/jobs/compliance-heartbeat.ts @@ -5,7 +5,7 @@ * Updates compliance status and triggers notifications on status transitions. */ -import { comply, type ComplyOptions } from '../services/compliance-testing.js'; +import { comply, getPlatformStoryboards, type ComplyOptions, type PlatformType } from '../services/compliance-testing.js'; import { ComplianceDatabase, type TrackSummaryEntry, type OverallRunStatus, type LifecycleStage } from '../../db/compliance-db.js'; import { query } from '../../db/client.js'; import { notifyComplianceChange } from '../../notifications/compliance.js'; @@ -56,11 +56,18 @@ export async function runComplianceHeartbeatJob(options: HeartbeatOptions = {}): // These are credentials the owner saved when connecting through Addie. const auth = await complianceDb.resolveOwnerAuth(agent.agent_url); + // Use storyboard-based routing when the agent has a registered platform type + const metadata = await complianceDb.getRegistryMetadata(agent.agent_url); + const storyboards = metadata?.platform_type + ? getPlatformStoryboards(metadata.platform_type as PlatformType) + : undefined; + const complyOptions: ComplyOptions = { test_session_id: `heartbeat-${Date.now()}`, dry_run: true, timeout_ms: 60_000, auth, + ...(storyboards && { storyboards }), userAgent: AAO_UA_COMPLIANCE, }; diff --git a/server/src/addie/mcp/member-tools.ts b/server/src/addie/mcp/member-tools.ts index 7bd7e19da9..cbb3bd1e2d 100644 --- a/server/src/addie/mcp/member-tools.ts +++ b/server/src/addie/mcp/member-tools.ts @@ -25,6 +25,7 @@ import { SAMPLE_BRIEFS, getAllPlatformTypes, getPlatformProfile, + getPlatformStoryboards, type ComplyOptions, type ComplianceTrack, type PlatformType, @@ -2990,6 +2991,14 @@ export function createMemberToolHandlers( } complyOptions.platform_type = effectivePlatformType; + // When no explicit tracks are provided, use storyboard-based routing + if (!tracks) { + const recommendedStoryboards = getPlatformStoryboards(effectivePlatformType); + if (recommendedStoryboards) { + complyOptions.storyboards = recommendedStoryboards; + } + } + try { const result = await comply(resolved.resolvedUrl, complyOptions); diff --git a/server/src/addie/services/compliance-testing.ts b/server/src/addie/services/compliance-testing.ts index cdab0aa9e6..ba67f55399 100644 --- a/server/src/addie/services/compliance-testing.ts +++ b/server/src/addie/services/compliance-testing.ts @@ -17,6 +17,7 @@ export type ComplianceTrack = | 'creative' | 'reporting' | 'governance' + | 'campaign_governance' | 'signals' | 'si' | 'audiences'; @@ -40,8 +41,12 @@ export type PlatformType = export interface ComplyOptions extends TestOptions { tracks?: ComplianceTrack[]; + /** Run the scenarios defined by these storyboard IDs. Takes priority over tracks. */ + storyboards?: string[]; platform_type?: PlatformType; timeout_ms?: number; + /** Limit to specific scenarios directly. Bypasses both storyboard and track selection. */ + scenarios?: TestScenario[]; } export interface PlatformProfile { @@ -111,6 +116,7 @@ const TRACK_LABELS: Record = { creative: 'Creative workflows', reporting: 'Reporting', governance: 'Governance', + campaign_governance: 'Campaign governance', signals: 'Signals', si: 'Sponsored intelligence', audiences: 'Audience sync', @@ -127,9 +133,16 @@ export const TRACK_SCENARIOS: Record = { 'validation', 'temporal_validation', ], - creative: ['creative_sync', 'creative_inline', 'creative_flow'], + creative: ['creative_sync', 'creative_inline', 'creative_flow', 'creative_lifecycle'], reporting: ['deterministic_delivery'], governance: ['governance_property_lists', 'governance_content_standards', 'property_list_filters'], + campaign_governance: [ + 'campaign_governance', + 'campaign_governance_denied', + 'campaign_governance_conditions', + 'campaign_governance_delivery', + 'seller_governance_context', + ], signals: ['signals_flow'], si: ['si_session_lifecycle', 'si_availability', 'si_handoff'], audiences: ['sync_audiences'], @@ -256,6 +269,33 @@ const PLATFORM_PROFILES: Record = { }, }; +/** + * Maps each platform type to the storyboards that define its expected behavior. + * This is the primary routing mechanism: a platform type selects storyboards, + * storyboards extract to scenarios, scenarios run via testAllScenarios(). + */ +export const PLATFORM_STORYBOARDS: Record = { + display_ad_server: ['capability_discovery', 'media_buy_seller'], + video_ad_server: ['capability_discovery', 'media_buy_seller'], + social_platform: ['capability_discovery', 'social_platform'], + pmax_platform: ['capability_discovery', 'media_buy_seller', 'creative_lifecycle'], + dsp: ['capability_discovery', 'media_buy_seller'], + retail_media: ['capability_discovery', 'media_buy_seller', 'media_buy_catalog_creative'], + search_platform: ['capability_discovery', 'media_buy_seller'], + audio_platform: ['capability_discovery', 'media_buy_seller'], + creative_transformer: ['capability_discovery', 'creative_template'], + creative_library: ['capability_discovery', 'creative_lifecycle'], + creative_ad_server: ['capability_discovery', 'creative_ad_server'], + si_platform: ['capability_discovery', 'si_session'], + ai_ad_network: ['capability_discovery', 'media_buy_seller', 'creative_lifecycle'], + ai_platform: ['capability_discovery', 'creative_template'], + generative_dsp: ['capability_discovery', 'media_buy_seller', 'creative_lifecycle'], +}; + +export function getPlatformStoryboards(platformType: PlatformType): string[] | undefined { + return PLATFORM_STORYBOARDS[platformType]; +} + export function getBriefsByVertical(vertical: string): SampleBrief[] { const normalized = vertical.trim().toLowerCase(); return SAMPLE_BRIEFS.filter((brief) => brief.vertical === normalized); @@ -409,14 +449,70 @@ function buildPlatformCoherence( }; } +const ALL_KNOWN_SCENARIOS = new Set( + (Object.values(TRACK_SCENARIOS) as TestScenario[][]).flat(), +); + +/** + * Filter an array of scenario name strings to only those that exist in TRACK_SCENARIOS. + * Logs a warning for any unknown scenario names. + */ +export function filterToKnownScenarios(candidates: string[]): TestScenario[] { + return candidates.filter((s) => ALL_KNOWN_SCENARIOS.has(s)) as TestScenario[]; +} + +/** + * Reverse-map a set of scenarios to the tracks that contain them. + */ +function tracksForScenarios(scenarios: TestScenario[]): ComplianceTrack[] { + const scenarioSet = new Set(scenarios); + const tracks: ComplianceTrack[] = []; + for (const [track, trackScenarios] of Object.entries(TRACK_SCENARIOS)) { + if (trackScenarios.some((s) => scenarioSet.has(s))) { + tracks.push(track as ComplianceTrack); + } + } + return tracks; +} + +/** + * Resolve storyboard IDs to a merged, deduped list of known scenarios. + * Uses dynamic import to avoid circular dependency with storyboards.ts. + */ +async function resolveStoryboardScenarios(storyboardIds: string[]): Promise { + const { getStoryboard, extractScenariosFromStoryboard } = await import('../../services/storyboards.js'); + const allScenarios = new Set(); + for (const id of storyboardIds) { + const sb = getStoryboard(id); + if (sb) { + for (const s of extractScenariosFromStoryboard(sb)) { + allScenarios.add(s); + } + } + } + return filterToKnownScenarios([...allScenarios]); +} + export async function comply(agentUrl: string, options: ComplyOptions = {}): Promise { - const requestedTracks = options.tracks?.length - ? options.tracks - : (Object.keys(TRACK_SCENARIOS) as ComplianceTrack[]); + // Priority: scenarios > storyboards > tracks > default (all tracks) + let scenarioList: TestScenario[]; + if (options.scenarios?.length) { + scenarioList = options.scenarios; + } else if (options.storyboards?.length) { + scenarioList = await resolveStoryboardScenarios(options.storyboards); + } else { + scenarioList = buildScenarioList(options.tracks); + } + + const requestedTracks = (options.scenarios?.length || options.storyboards?.length) + ? tracksForScenarios(scenarioList) + : options.tracks?.length + ? options.tracks + : (Object.keys(TRACK_SCENARIOS) as ComplianceTrack[]); const suite = await testAllScenarios(agentUrl, { ...options, - scenarios: buildScenarioList(requestedTracks), + scenarios: scenarioList, }); const trackResults = buildTrackResults(requestedTracks, suite.results); diff --git a/server/src/routes/registry-api.ts b/server/src/routes/registry-api.ts index 4179480d27..187001fd2a 100644 --- a/server/src/routes/registry-api.ts +++ b/server/src/routes/registry-api.ts @@ -15,8 +15,8 @@ import { MemberDatabase } from "../db/member-db.js"; import { query } from "../db/client.js"; import * as manifestRefsDb from "../db/manifest-refs-db.js"; import { bulkResolveRateLimiter, brandCreationRateLimiter, storyboardEvalRateLimiter } from "../middleware/rate-limit.js"; -import { listStoryboards, getStoryboard, getTestKitForStoryboard } from "../services/storyboards.js"; -import { comply } from "../addie/services/compliance-testing.js"; +import { listStoryboards, getStoryboard, getTestKitForStoryboard, extractScenariosFromStoryboard } from "../services/storyboards.js"; +import { comply, filterToKnownScenarios } from "../addie/services/compliance-testing.js"; import { PUBLIC_TEST_AGENT } from "../config/test-agent.js"; import * as policiesDb from "../db/policies-db.js"; import { createLogger } from "../logger.js"; @@ -2595,10 +2595,12 @@ export function createRegistryApiRouter(config: RegistryApiConfig): Router { // Resolve agent auth const auth = await complianceDb.resolveOwnerAuth(agentUrl); - // Run comply with the storyboard's referenced scenarios + // Only run scenarios this storyboard references, not the full suite + const storyboardScenarios = filterToKnownScenarios(extractScenariosFromStoryboard(storyboard)); const complyResult = await comply(agentUrl, { dry_run: true, timeout_ms: 90_000, + ...(storyboardScenarios.length > 0 && { scenarios: storyboardScenarios }), ...(auth && { auth }), }); @@ -2713,18 +2715,21 @@ export function createRegistryApiRouter(config: RegistryApiConfig): Router { return res.status(404).json({ error: "Storyboard not found" }); } - // Run comply against both the user's agent and the reference test agent + // Only run scenarios this storyboard references, not the full suite const auth = await complianceDb.resolveOwnerAuth(agentUrl); + const compareScenarios = filterToKnownScenarios(extractScenariosFromStoryboard(storyboard)); const [userResult, referenceResult] = await Promise.all([ comply(agentUrl, { dry_run: true, timeout_ms: 90_000, + ...(compareScenarios.length > 0 && { scenarios: compareScenarios }), ...(auth && { auth }), }), comply(PUBLIC_TEST_AGENT.url, { dry_run: true, timeout_ms: 90_000, + ...(compareScenarios.length > 0 && { scenarios: compareScenarios }), auth: { type: "bearer", token: PUBLIC_TEST_AGENT.token }, }), ]); diff --git a/server/src/services/storyboards.ts b/server/src/services/storyboards.ts index 0d2d28ee9c..118b789f9d 100644 --- a/server/src/services/storyboards.ts +++ b/server/src/services/storyboards.ts @@ -216,6 +216,22 @@ export function getTestKitForStoryboard(storyboardId: string): TestKit | undefin return testKits.get(kitId); } +/** + * Extract unique comply_scenario values from a storyboard. + * Used to limit comply() to only the scenarios a storyboard references. + */ +export function extractScenariosFromStoryboard(storyboard: Storyboard): string[] { + const scenarios = new Set(); + for (const phase of storyboard.phases) { + for (const step of phase.steps) { + if (step.comply_scenario) { + scenarios.add(step.comply_scenario); + } + } + } + return [...scenarios]; +} + /** * Reload storyboards from disk. Useful for development. */ diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts index 2943e51f98..126556c663 100644 --- a/server/src/training-agent/task-handlers.ts +++ b/server/src/training-agent/task-handlers.ts @@ -1799,7 +1799,7 @@ function handleGetAdcpCapabilities(_args: ToolArgs, _ctx: TrainingContext): Reco const channels = [...new Set(PUBLISHERS.flatMap(p => p.channels))].sort(); return { adcp: { major_versions: [3] }, - supported_protocols: ['media_buy', 'creative', 'governance', 'signals', 'brand'], + supported_protocols: ['media_buy', 'creative', 'governance', 'signals', 'brand', 'compliance'], protocol_version: '3.0', tasks, media_buy: { diff --git a/server/tests/manual/storyboard-validation.ts b/server/tests/manual/storyboard-validation.ts new file mode 100644 index 0000000000..fc3d265cb9 --- /dev/null +++ b/server/tests/manual/storyboard-validation.ts @@ -0,0 +1,201 @@ +/** + * Storyboard validation against real agents. + * + * Usage: + * npx tsx server/tests/manual/storyboard-validation.ts + * npx tsx server/tests/manual/storyboard-validation.ts --storyboard media_buy_seller + * npx tsx server/tests/manual/storyboard-validation.ts --agent https://some-agent.example/mcp + */ + +import { + comply, + filterToKnownScenarios, + type ComplyResult, +} from '../../src/addie/services/compliance-testing.js'; +import { + listStoryboards, + getStoryboard, + extractScenariosFromStoryboard, +} from '../../src/services/storyboards.js'; + +const TEST_AGENT_URL = process.env.TEST_AGENT_URL || 'https://test-agent.adcontextprotocol.org/mcp'; +const TEST_AGENT_TOKEN = process.env.TEST_AGENT_TOKEN || '1v8tAhASaUYYp4odoQ1PnMpdqNaMiTrCRqYo9OJp6IQ'; + +const args = process.argv.slice(2); +const storyboardFilter = args.includes('--storyboard') ? args[args.indexOf('--storyboard') + 1] : undefined; +const agentUrl = args.includes('--agent') ? args[args.indexOf('--agent') + 1] : TEST_AGENT_URL; + +interface TrackDetail { + track: string; + status: string; + scenarios: { scenario: string; passed: boolean; error?: string }[]; +} + +interface StoryboardResult { + id: string; + title: string; + scenarios_extracted: string[]; + scenarios_known: string[]; + tracks_tested: string[]; + track_details: TrackDetail[]; + tracks_passed: number; + tracks_failed: number; + tracks_partial: number; + tracks_skipped: number; + duration_ms: number; + observations: string[]; + error?: string; +} + +async function runStoryboard(storyboardId: string): Promise { + const sb = getStoryboard(storyboardId); + if (!sb) { + return { + id: storyboardId, title: '(not found)', scenarios_extracted: [], + scenarios_known: [], tracks_tested: [], tracks_passed: 0, + tracks_failed: 0, tracks_partial: 0, tracks_skipped: 0, + duration_ms: 0, error: 'Storyboard not found', + }; + } + + const extracted = extractScenariosFromStoryboard(sb); + const known = filterToKnownScenarios(extracted); + + if (known.length === 0) { + return { + id: storyboardId, title: sb.title, + scenarios_extracted: extracted, scenarios_known: [], + tracks_tested: [], track_details: [], tracks_passed: 0, tracks_failed: 0, + tracks_partial: 0, tracks_skipped: 0, duration_ms: 0, observations: [], + error: 'No known scenarios — storyboard documents the flow but has no test coverage yet', + }; + } + + try { + const result: ComplyResult = await comply(agentUrl, { + scenarios: known, + dry_run: true, + timeout_ms: 90_000, + auth: { type: 'bearer', token: TEST_AGENT_TOKEN }, + }); + + const trackDetails: TrackDetail[] = result.tracks.map(t => ({ + track: t.track, + status: t.status, + scenarios: t.scenarios.map(s => ({ + scenario: s.scenario, + passed: s.overall_passed, + ...(s.steps?.some(st => !st.passed) && { + error: s.steps.filter(st => !st.passed).map(st => `${st.step}: ${st.error || 'failed'}`).join('; '), + }), + })), + })); + + return { + id: storyboardId, + title: sb.title, + scenarios_extracted: extracted, + scenarios_known: known, + tracks_tested: result.tracks.map(t => t.track), + track_details: trackDetails, + tracks_passed: result.summary.tracks_passed, + tracks_failed: result.summary.tracks_failed, + tracks_partial: result.summary.tracks_partial, + tracks_skipped: result.summary.tracks_skipped, + duration_ms: result.total_duration_ms, + observations: result.observations.map(o => `[${o.severity}] ${o.category}: ${o.message}`), + }; + } catch (err) { + return { + id: storyboardId, title: sb.title, + scenarios_extracted: extracted, scenarios_known: known, + tracks_tested: [], track_details: [], tracks_passed: 0, tracks_failed: 0, + tracks_partial: 0, tracks_skipped: 0, duration_ms: 0, observations: [], + error: err instanceof Error ? err.message : String(err), + }; + } +} + +async function main() { + console.log(`\nAgent: ${agentUrl}`); + console.log(`Filter: ${storyboardFilter || '(all storyboards)'}\n`); + + const all = listStoryboards(); + const storyboardIds = storyboardFilter + ? [storyboardFilter] + : all.map(s => s.id); + + const results: StoryboardResult[] = []; + + for (const id of storyboardIds) { + process.stdout.write(` ${id}... `); + const result = await runStoryboard(id); + results.push(result); + + if (result.error) { + console.log(`⚠ ${result.error}`); + } else if (result.tracks_failed === 0 && result.tracks_partial === 0) { + console.log(`✓ ${result.tracks_passed} tracks passed (${result.duration_ms}ms)`); + } else { + console.log(`✗ ${result.tracks_passed}P/${result.tracks_failed}F/${result.tracks_partial}partial (${result.duration_ms}ms)`); + } + } + + // Summary table + console.log('\n--- Summary ---\n'); + console.log('Storyboard | Scenarios | Result | Duration'); + console.log('------------------------------------|-----------|------------------|----------'); + for (const r of results) { + const name = r.id.padEnd(35); + const scenarios = `${r.scenarios_known.length}/${r.scenarios_extracted.length}`.padEnd(9); + let status: string; + if (r.error) { + status = `⚠ ${r.error.slice(0, 16)}`; + } else if (r.tracks_failed === 0 && r.tracks_partial === 0) { + status = `✓ ${r.tracks_passed} passed`; + } else { + status = `✗ ${r.tracks_passed}P/${r.tracks_failed}F/${r.tracks_partial}pt`; + } + const dur = r.error ? '-' : `${r.duration_ms}ms`; + console.log(`${name} | ${scenarios} | ${status.padEnd(16)} | ${dur}`); + } + + // Totals + const withTests = results.filter(r => !r.error); + const allPassed = withTests.filter(r => r.tracks_failed === 0 && r.tracks_partial === 0); + const noTests = results.filter(r => r.error); + console.log(`\nTestable: ${withTests.length}/${results.length} | All passed: ${allPassed.length}/${withTests.length} | No test coverage: ${noTests.length}`); + + // Detail section for failures and interesting cases + const interesting = results.filter(r => r.tracks_failed > 0 || r.tracks_partial > 0 || r.observations.length > 0); + if (interesting.length > 0) { + console.log('\n--- Detail (failures and observations) ---\n'); + for (const r of interesting) { + console.log(`### ${r.id}`); + for (const td of r.track_details) { + console.log(` Track: ${td.track} → ${td.status}`); + for (const s of td.scenarios) { + console.log(` ${s.passed ? '✓' : '✗'} ${s.scenario}${s.error ? ` — ${s.error}` : ''}`); + } + } + for (const obs of r.observations) { + console.log(` ${obs}`); + } + console.log(); + } + } + + // Detail for "0 passed, 0 failed" (all skipped) + const allSkipped = withTests.filter(r => r.tracks_passed === 0 && r.tracks_failed === 0 && r.tracks_partial === 0); + if (allSkipped.length > 0) { + console.log('\n--- All tracks skipped (scenario ran but no track results) ---\n'); + for (const r of allSkipped) { + console.log(` ${r.id}: scenarios=${r.scenarios_known.join(',')} tracks=${r.track_details.map(t => `${t.track}:${t.status}`).join(',') || 'none'}`); + } + } +} + +main().catch(err => { + console.error('Fatal:', err); + process.exit(1); +}); diff --git a/server/tests/unit/compliance-track-scenarios.test.ts b/server/tests/unit/compliance-track-scenarios.test.ts index c1fc00e1c5..df55b25110 100644 --- a/server/tests/unit/compliance-track-scenarios.test.ts +++ b/server/tests/unit/compliance-track-scenarios.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { TRACK_SCENARIOS, buildScenarioList } from '../../src/addie/services/compliance-testing.js'; +import { TRACK_SCENARIOS, buildScenarioList, PLATFORM_STORYBOARDS } from '../../src/addie/services/compliance-testing.js'; +import { getStoryboard } from '../../src/services/storyboards.js'; describe('TRACK_SCENARIOS', () => { it('maps reporting track to deterministic_delivery', () => { @@ -31,3 +32,37 @@ describe('buildScenarioList', () => { } }); }); + +describe('PLATFORM_STORYBOARDS', () => { + it('every platform type maps to at least one storyboard', () => { + for (const [, storyboards] of Object.entries(PLATFORM_STORYBOARDS)) { + expect(storyboards.length).toBeGreaterThan(0); + } + }); + + it('every mapped storyboard ID is a valid storyboard', () => { + const allIds = new Set(); + for (const storyboards of Object.values(PLATFORM_STORYBOARDS)) { + for (const id of storyboards) { + allIds.add(id); + } + } + for (const id of allIds) { + expect(getStoryboard(id)).toBeDefined(); + } + }); + + it('si_platform maps to si_session', () => { + expect(PLATFORM_STORYBOARDS.si_platform).toContain('si_session'); + }); + + it('social_platform maps to social_platform storyboard', () => { + expect(PLATFORM_STORYBOARDS.social_platform).toContain('social_platform'); + }); + + it('every platform type includes capability_discovery', () => { + for (const [, storyboards] of Object.entries(PLATFORM_STORYBOARDS)) { + expect(storyboards).toContain('capability_discovery'); + } + }); +}); diff --git a/server/tests/unit/storyboards.test.ts b/server/tests/unit/storyboards.test.ts index b5d4dac999..077da6cb83 100644 --- a/server/tests/unit/storyboards.test.ts +++ b/server/tests/unit/storyboards.test.ts @@ -4,6 +4,7 @@ import { getStoryboard, getTestKit, getTestKitForStoryboard, + extractScenariosFromStoryboard, type Storyboard, type StoryboardSummary, } from '../../src/services/storyboards.js'; @@ -11,20 +12,30 @@ import { describe('listStoryboards', () => { it('returns all storyboards when no category filter', () => { const results = listStoryboards(); - expect(results.length).toBeGreaterThanOrEqual(11); + expect(results.length).toBeGreaterThanOrEqual(21); const ids = results.map((s) => s.id); + expect(ids).toContain('capability_discovery'); expect(ids).toContain('creative_template'); expect(ids).toContain('creative_ad_server'); expect(ids).toContain('creative_sales_agent'); + expect(ids).toContain('creative_lifecycle'); expect(ids).toContain('media_buy_seller'); expect(ids).toContain('media_buy_guaranteed_approval'); expect(ids).toContain('media_buy_non_guaranteed'); expect(ids).toContain('media_buy_proposal_mode'); expect(ids).toContain('media_buy_governance_escalation'); expect(ids).toContain('media_buy_catalog_creative'); + expect(ids).toContain('campaign_governance_denied'); + expect(ids).toContain('campaign_governance_conditions'); + expect(ids).toContain('campaign_governance_delivery'); expect(ids).toContain('signal_marketplace'); expect(ids).toContain('signal_owned'); + expect(ids).toContain('social_platform'); + expect(ids).toContain('si_session'); + expect(ids).toContain('brand_rights'); + expect(ids).toContain('property_governance'); + expect(ids).toContain('content_standards'); }); it('each summary has required fields', () => { @@ -146,7 +157,7 @@ describe('getStoryboard', () => { it('schema_ref paths point to known schema directories', () => { const storyboards = listStoryboards(); - const validPrefixes = ['creative/', 'media-buy/', 'account/', 'governance/', 'signals/']; + const validPrefixes = ['creative/', 'media-buy/', 'account/', 'governance/', 'signals/', 'protocol/', 'sponsored-intelligence/', 'brand/', 'property/', 'content-standards/']; for (const summary of storyboards) { const sb = getStoryboard(summary.id)!; for (const phase of sb.phases) { @@ -417,3 +428,225 @@ describe('storyboard interaction models', () => { } }); }); + +describe('extractScenariosFromStoryboard', () => { + it('extracts deduped scenarios from media_buy_seller', () => { + const sb = getStoryboard('media_buy_seller')!; + const scenarios = extractScenariosFromStoryboard(sb); + expect(scenarios).toContain('full_sales_flow'); + expect(scenarios).toContain('create_media_buy'); + expect(scenarios).toContain('media_buy_lifecycle'); + expect(scenarios).toContain('reporting_flow'); + expect(scenarios).toContain('creative_lifecycle'); + expect(scenarios).toContain('creative_sync'); + // Should be deduped + const duplicates = scenarios.filter((s, i) => scenarios.indexOf(s) !== i); + expect(duplicates).toEqual([]); + }); + + it('returns empty array for storyboard with no comply_scenario', () => { + const fakeSb = { + id: 'test', + version: '1.0.0', + title: 'test', + category: 'test', + summary: 'test', + narrative: 'test', + agent: { interaction_model: 'test', capabilities: [], examples: [] }, + caller: { role: 'test', example: 'test' }, + phases: [{ + id: 'p1', + title: 'test', + narrative: 'test', + steps: [{ + id: 's1', + title: 'test', + narrative: 'test', + task: 'test', + schema_ref: 'test', + doc_ref: 'test', + stateful: false, + expected: 'test', + }], + }], + } as unknown as import('../../src/services/storyboards.js').Storyboard; + expect(extractScenariosFromStoryboard(fakeSb)).toEqual([]); + }); +}); + +describe('capability_discovery storyboard', () => { + it('has protocol_discovery phase with get_adcp_capabilities task', () => { + const sb = getStoryboard('capability_discovery')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('media_buy_seller'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('get_adcp_capabilities'); + }); + + it('references protocol schema paths', () => { + const sb = getStoryboard('capability_discovery')!; + const refs = sb.phases.flatMap((p) => p.steps.map((s) => s.schema_ref)); + expect(refs.some((r) => r.startsWith('protocol/'))).toBe(true); + }); +}); + +describe('campaign governance storyboards', () => { + it('denied storyboard covers plan registration and denial', () => { + const sb = getStoryboard('campaign_governance_denied')!; + expect(sb).toBeDefined(); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('sync_plans'); + expect(tasks).toContain('check_governance'); + }); + + it('conditions storyboard covers conditional approval and media buy creation', () => { + const sb = getStoryboard('campaign_governance_conditions')!; + expect(sb).toBeDefined(); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('sync_plans'); + expect(tasks).toContain('check_governance'); + expect(tasks).toContain('create_media_buy'); + }); + + it('delivery storyboard covers monitoring and drift re-check', () => { + const sb = getStoryboard('campaign_governance_delivery')!; + expect(sb).toBeDefined(); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('sync_plans'); + expect(tasks).toContain('check_governance'); + expect(tasks).toContain('get_media_buy_delivery'); + }); + + it('all governance storyboards resolve acme_outdoor test kit', () => { + for (const id of ['campaign_governance_denied', 'campaign_governance_conditions', 'campaign_governance_delivery']) { + const kit = getTestKitForStoryboard(id); + expect(kit).toBeDefined(); + expect(kit!.id).toBe('acme_outdoor'); + } + }); +}); + +describe('creative_lifecycle storyboard', () => { + it('covers sync, list, build, and preview tasks', () => { + const sb = getStoryboard('creative_lifecycle')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('stateful_preloaded'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('list_creative_formats'); + expect(tasks).toContain('sync_creatives'); + expect(tasks).toContain('list_creatives'); + expect(tasks).toContain('preview_creative'); + expect(tasks).toContain('build_creative'); + }); + + it('has phases covering the full lifecycle', () => { + const sb = getStoryboard('creative_lifecycle')!; + const phaseIds = sb.phases.map((p) => p.id); + expect(phaseIds).toContain('discover_formats'); + expect(phaseIds).toContain('sync_multiple'); + expect(phaseIds).toContain('list_and_filter'); + expect(phaseIds).toContain('build_and_preview'); + }); +}); + +describe('social_platform storyboard', () => { + it('covers account setup, audiences, creatives, events, and financials', () => { + const sb = getStoryboard('social_platform')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('media_buy_seller'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('sync_accounts'); + expect(tasks).toContain('list_accounts'); + expect(tasks).toContain('sync_audiences'); + expect(tasks).toContain('sync_creatives'); + expect(tasks).toContain('log_event'); + expect(tasks).toContain('get_account_financials'); + }); + + it('resolves acme_outdoor test kit', () => { + const kit = getTestKitForStoryboard('social_platform'); + expect(kit).toBeDefined(); + expect(kit!.id).toBe('acme_outdoor'); + }); +}); + +describe('si_session storyboard', () => { + it('covers the full SI session lifecycle', () => { + const sb = getStoryboard('si_session')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('si_platform'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('si_get_offering'); + expect(tasks).toContain('si_initiate_session'); + expect(tasks).toContain('si_send_message'); + expect(tasks).toContain('si_terminate_session'); + }); + + it('resolves nova_motors test kit', () => { + const kit = getTestKitForStoryboard('si_session'); + expect(kit).toBeDefined(); + expect(kit!.id).toBe('nova_motors'); + }); +}); + +describe('brand_rights storyboard', () => { + it('covers brand identity discovery and rights lifecycle', () => { + const sb = getStoryboard('brand_rights')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('brand_rights_holder'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('get_brand_identity'); + expect(tasks).toContain('get_rights'); + expect(tasks).toContain('acquire_rights'); + expect(tasks).toContain('update_rights'); + expect(tasks).toContain('creative_approval'); + }); + + it('resolves acme_outdoor test kit', () => { + const kit = getTestKitForStoryboard('brand_rights'); + expect(kit).toBeDefined(); + expect(kit!.id).toBe('acme_outdoor'); + }); +}); + +describe('property_governance storyboard', () => { + it('covers property list CRUD and delivery validation', () => { + const sb = getStoryboard('property_governance')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('governance_agent'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('create_property_list'); + expect(tasks).toContain('list_property_lists'); + expect(tasks).toContain('get_property_list'); + expect(tasks).toContain('update_property_list'); + expect(tasks).toContain('delete_property_list'); + expect(tasks).toContain('validate_property_delivery'); + }); + + it('resolves acme_outdoor test kit', () => { + const kit = getTestKitForStoryboard('property_governance'); + expect(kit).toBeDefined(); + expect(kit!.id).toBe('acme_outdoor'); + }); +}); + +describe('content_standards storyboard', () => { + it('covers content standards CRUD, calibration, and delivery validation', () => { + const sb = getStoryboard('content_standards')!; + expect(sb).toBeDefined(); + expect(sb.agent.interaction_model).toBe('governance_agent'); + const tasks = sb.phases.flatMap((p) => p.steps.map((s) => s.task)); + expect(tasks).toContain('create_content_standards'); + expect(tasks).toContain('list_content_standards'); + expect(tasks).toContain('get_content_standards'); + expect(tasks).toContain('update_content_standards'); + expect(tasks).toContain('calibrate_content'); + expect(tasks).toContain('validate_content_delivery'); + }); + + it('resolves acme_outdoor test kit', () => { + const kit = getTestKitForStoryboard('content_standards'); + expect(kit).toBeDefined(); + expect(kit!.id).toBe('acme_outdoor'); + }); +}); diff --git a/server/tests/unit/training-agent.test.ts b/server/tests/unit/training-agent.test.ts index 24a3aab3a6..f16237f2ed 100644 --- a/server/tests/unit/training-agent.test.ts +++ b/server/tests/unit/training-agent.test.ts @@ -3464,7 +3464,7 @@ describe('get_adcp_capabilities handler', () => { expect(result.adcp).toEqual({ major_versions: [3] }); expect(result.protocol_version).toBe('3.0'); - expect(result.supported_protocols).toEqual(['media_buy', 'creative', 'governance', 'signals', 'brand']); + expect(result.supported_protocols).toEqual(['media_buy', 'creative', 'governance', 'signals', 'brand', 'compliance']); }); it('lists protocol tasks without get_adcp_capabilities itself', async () => {