diff --git a/.changeset/content-standards-protocol.md b/.changeset/content-standards-protocol.md new file mode 100644 index 00000000..2109a910 --- /dev/null +++ b/.changeset/content-standards-protocol.md @@ -0,0 +1,19 @@ +--- +"adcontextprotocol": minor +--- + +Add Content Standards Protocol for content safety and suitability evaluation. + +Discovery tasks: +- `list_content_features`: Discover available content safety features +- `list_content_standards`: List available standards configurations +- `get_content_standards`: Retrieve content safety policies + +Management tasks: +- `create_content_standards`: Create a new standards configuration +- `update_content_standards`: Update an existing configuration +- `delete_content_standards`: Delete a configuration + +Calibration & Validation tasks: +- `calibrate_content`: Collaborative dialogue to align on policy interpretation +- `validate_content_delivery`: Batch validate delivery records diff --git a/docs.json b/docs.json index ca5b1d84..a4221e99 100644 --- a/docs.json +++ b/docs.json @@ -121,6 +121,27 @@ "pages": [ "docs/governance/brand-standards/index" ] + }, + { + "group": "Content Standards", + "pages": [ + "docs/governance/content-standards/index", + "docs/governance/content-standards/artifacts", + { + "group": "Tasks", + "pages": [ + "docs/governance/content-standards/tasks/list_content_features", + "docs/governance/content-standards/tasks/list_content_standards", + "docs/governance/content-standards/tasks/get_content_standards", + "docs/governance/content-standards/tasks/create_content_standards", + "docs/governance/content-standards/tasks/update_content_standards", + "docs/governance/content-standards/tasks/delete_content_standards", + "docs/governance/content-standards/tasks/calibrate_content", + "docs/governance/content-standards/tasks/get_media_buy_artifacts", + "docs/governance/content-standards/tasks/validate_content_delivery" + ] + } + ] } ] }, diff --git a/docs/governance/content-standards/artifacts.mdx b/docs/governance/content-standards/artifacts.mdx new file mode 100644 index 00000000..7d2f834a --- /dev/null +++ b/docs/governance/content-standards/artifacts.mdx @@ -0,0 +1,304 @@ +--- +title: Artifacts +sidebar_position: 2 +--- + +# Artifacts + +An **artifact** is a unit of content adjacent to an ad placement. When evaluating brand safety and suitability, you're asking: "Is this artifact appropriate for my brand's ads?" + +## What Is an Artifact? + +Artifacts represent the content context where an ad appears: + +- A **news article** on a website +- A **podcast segment** between ad breaks +- A **video chapter** in a YouTube video +- A **social media post** in a feed +- A **scene** in a CTV show +- An **AI-generated image** in a chat conversation + +Artifacts are identified by `property_id` + `artifact_id` - the property defines where the content lives, and the artifact_id is an opaque identifier for that specific piece of content. The artifact_id scheme is flexible - it could be a URL path, a platform-specific ID, or any consistent identifier the property owner uses internally. + +## Structure + +**Schema**: [artifact.json](https://adcontextprotocol.org/schemas/v2/content-standards/artifact.json) + +```json +{ + "property_id": {"type": "domain", "value": "reddit.com"}, + "artifact_id": "r_fitness_post_abc123", + "assets": [ + {"type": "text", "role": "title", "content": "Best protein sources for muscle building", "language": "en"}, + {"type": "text", "role": "paragraph", "content": "Looking for recommendations on high-quality protein sources...", "language": "en"}, + {"type": "image", "url": "https://cdn.reddit.com/fitness-image.jpg", "alt_text": "Person lifting weights"} + ] +} +``` + +### Required Fields + +| Field | Description | +|-------|-------------| +| `property_id` | Where this artifact lives - uses standard identifier types (`domain`, `app_id`, `apple_podcast_id`, etc.) | +| `artifact_id` | Unique identifier within the property - the property owner defines their scheme | +| `assets` | Content in document order - text blocks, images, video, audio | + +### Optional Fields + +| Field | Description | +|-------|-------------| +| `variant_id` | Identifies a specific variant (A/B test, translation, temporal version) | +| `format_id` | Reference to format registry (same as creative formats) | +| `url` | Web URL if the artifact has one | +| `metadata` | Artifact-level metadata (Open Graph, JSON-LD, author info) | +| `published_time` | When the artifact was published | +| `last_update_time` | When the artifact was last modified | + +## Variants + +The same artifact may have multiple variants: + +- **Translations** - English version vs Spanish version +- **A/B tests** - Different headlines being tested +- **Temporal versions** - Content that changed on Wednesday + +Use `variant_id` to distinguish between them: + +```json +// English version +{ + "property_id": {"type": "domain", "value": "nytimes.com"}, + "artifact_id": "article_12345", + "variant_id": "en", + "assets": [ + {"type": "text", "role": "title", "content": "Breaking News Story", "language": "en"} + ] +} + +// Spanish translation +{ + "property_id": {"type": "domain", "value": "nytimes.com"}, + "artifact_id": "article_12345", + "variant_id": "es", + "assets": [ + {"type": "text", "role": "title", "content": "Noticia de Ășltima hora", "language": "es"} + ] +} + +// A/B test variant +{ + "property_id": {"type": "domain", "value": "nytimes.com"}, + "artifact_id": "article_12345", + "variant_id": "headline_test_b", + "assets": [ + {"type": "text", "role": "title", "content": "Alternative Headline Being Tested", "language": "en"} + ] +} +``` + +The combination of `artifact_id` + `variant_id` must be unique within a property. This lets you track which variant a user saw and correlate it with delivery reports. + +## Asset Types + +Assets are the actual content within an artifact. Everything is an asset - titles, paragraphs, images, videos. + +### Text + +```json +{"type": "text", "role": "title", "content": "Article Title", "language": "en"} +{"type": "text", "role": "paragraph", "content": "The article body text...", "language": "en"} +{"type": "text", "role": "description", "content": "A summary of the article", "language": "en"} +{"type": "text", "role": "heading", "content": "Section Header", "heading_level": 2} +{"type": "text", "role": "quote", "content": "A quoted statement"} +``` + +Roles: `title`, `description`, `paragraph`, `heading`, `caption`, `quote`, `list_item` + +Each text asset can have its own `language` tag for mixed-language content. + +### Image + +```json +{ + "type": "image", + "url": "https://cdn.example.com/photo.jpg", + "alt_text": "Description of the image" +} +``` + +### Video + +```json +{ + "type": "video", + "url": "https://cdn.example.com/video.mp4", + "transcript": "Full transcript of the video content...", + "duration_ms": 180000 +} +``` + +### Audio + +```json +{ + "type": "audio", + "url": "https://cdn.example.com/podcast.mp3", + "transcript": "Today we're discussing...", + "duration_ms": 3600000 +} +``` + +## Metadata + +Artifact-level metadata describes the artifact as a whole, not individual assets: + +```json +{ + "metadata": { + "author": "Jane Smith", + "canonical": "https://example.com/article/12345", + "open_graph": { + "og:type": "article", + "og:site_name": "Example News" + }, + "json_ld": [ + { + "@type": "NewsArticle", + "datePublished": "2025-01-15" + } + ] + } +} +``` + +This is separate from assets because it's about the artifact container, not the content itself. + +## Secured Asset Access + +Many assets aren't publicly accessible - AI-generated images, private conversations, paywalled content. The artifact schema supports authenticated access. + +### Pre-Configuration (Recommended) + +For ongoing partnerships, configure access once during onboarding rather than per-request: + +1. **Service account sharing** - Grant the verification agent access to your cloud storage +2. **OAuth client credentials** - Set up machine-to-machine authentication +3. **API key exchange** - Share long-lived API keys during setup + +This happens during the activation phase when the seller first receives content standards from a buyer. + +### Per-Asset Authentication + +When pre-configuration isn't possible, include access credentials with individual assets: + +```json +{ + "type": "image", + "url": "https://cdn.openai.com/secured/img_abc123.png", + "access": { + "method": "bearer_token", + "token": "eyJhbGciOiJIUzI1NiIs..." + } +} +``` + +**Note on token size**: For artifacts with many assets, per-asset tokens can significantly increase payload size. Consider: + +1. **Pre-configured access** - Set up service account access once during onboarding +2. **Shared token reference** - Define tokens at the artifact level and reference by ID +3. **Signed URLs** - Use pre-signed URLs where the URL itself is the credential + +The `url` field is the access URL - it may differ from the artifact's canonical/published URL. For example, a published article at `https://news.example.com/article/123` might have assets served from `https://cdn.example.com/secured/...`. + +### Access Methods + +| Method | Use Case | +|--------|----------| +| `bearer_token` | OAuth2 bearer token in Authorization header | +| `service_account` | GCP/AWS service account credentials | +| `signed_url` | Pre-signed URL with embedded credentials (URL itself is the credential) | + +### Service Account Setup + +For GCP: + +```json +{ + "access": { + "method": "service_account", + "provider": "gcp", + "credentials": { + "type": "service_account", + "project_id": "my-project", + "private_key_id": "...", + "private_key": "-----BEGIN PRIVATE KEY-----\n...", + "client_email": "verification-agent@my-project.iam.gserviceaccount.com" + } + } +} +``` + +For AWS: + +```json +{ + "access": { + "method": "service_account", + "provider": "aws", + "credentials": { + "access_key_id": "AKIAIOSFODNN7EXAMPLE", + "secret_access_key": "...", + "region": "us-east-1" + } + } +} +``` + +### Pre-Signed URLs + +For one-off access without sharing credentials: + +```json +{ + "type": "video", + "url": "https://storage.googleapis.com/bucket/video.mp4?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=...&X-Goog-Signature=...", + "access": { + "method": "signed_url" + } +} +``` + +The URL itself contains the credentials - no additional authentication needed. + +## Property Identifier Types + +The `property_id` uses standard identifier types from the AdCP property schema: + +| Type | Example | Use Case | +|------|---------|----------| +| `domain` | `reddit.com` | Websites | +| `app_id` | `com.spotify.music` | Mobile apps | +| `apple_podcast_id` | `1234567890` | Apple Podcasts | +| `spotify_show_id` | `4rOoJ6Egrf8K2IrywzwOMk` | Spotify podcasts | +| `youtube_channel_id` | `UCddiUEpeqJcYeBxX1IVBKvQ` | YouTube channels | +| `rss_url` | `https://feeds.example.com/podcast.xml` | RSS feeds | + +## Artifact ID Schemes + +The property owner defines their artifact_id scheme. Examples: + +| Property Type | Artifact ID Pattern | Example | +|---------------|---------------------|---------| +| News website | `article_{id}` | `article_12345` | +| Reddit | `r_{subreddit}_{post_id}` | `r_fitness_abc123` | +| Podcast | `episode_{num}_segment_{num}` | `episode_42_segment_2` | +| CTV | `show_{id}_s{season}e{episode}_scene_{num}` | `show_abc_s3e5_scene_12` | +| Social feed | `post_{id}` | `post_xyz789` | + +The verification agent doesn't need to understand the scheme - it's opaque. The property owner uses it to correlate artifacts with their content. + +## Related + +- [Content Standards Overview](/docs/governance/content-standards) - How artifacts fit into the content standards workflow +- [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) - Sending artifacts for calibration diff --git a/docs/governance/content-standards/index.mdx b/docs/governance/content-standards/index.mdx new file mode 100644 index 00000000..feb13b6e --- /dev/null +++ b/docs/governance/content-standards/index.mdx @@ -0,0 +1,247 @@ +--- +title: Overview +sidebar_position: 1 +--- + +# Content Standards Protocol + +The Content Standards Protocol controls *content safety and suitability* - what content contexts are appropriate for a brand's ads. + +## What It Does + +Content Standards agents evaluate placement contexts against brand policies. This enables: + +- **Brand safety** - Is this content safe for *any* brand? (universal thresholds like hate speech, illegal content) +- **Brand suitability** - Is this content appropriate for *my* brand? (brand-specific preferences and tone) +- **Competitive separation** - Avoid appearing near competitor content + +## Key Concepts + +Content standards evaluation involves four key questions that buyers and sellers negotiate: + +1. **What content?** - What [artifacts](/docs/governance/content-standards/artifacts) to evaluate (the ad-adjacent content) +2. **How much adjacency?** - How many artifacts around the ad slot to consider +3. **What sampling rate?** - What percentage of traffic to evaluate +4. **How to calibrate?** - How to align on policy interpretation before runtime + +These parameters are negotiated between buyer and seller during product discovery and media buy creation. + +## Workflow + +```mermaid +sequenceDiagram + participant Brand + participant Buyer as Buyer Agent + participant Seller as Seller Agent + participant Verifier as Verification Agent + + Note over Brand,Verifier: 1. SETUP PHASE + Brand->>Verifier: create_content_standards (policy + calibration examples) + Verifier-->>Brand: standards_id + + Note over Brand,Verifier: 2. ACTIVATION PHASE + Brand->>Buyer: "Buy inventory from Reddit, use standards_id X" + Buyer->>Seller: create_media_buy (includes content_standards reference) + + Seller->>Verifier: calibrate_content (sample artifacts) + Verifier-->>Seller: verdict + explanation + Seller->>Verifier: "What about this edge case?" + Verifier-->>Seller: clarification + Note over Seller: Seller builds local model + + Note over Brand,Verifier: 3. RUNTIME PHASE + loop High-volume decisioning + Note over Seller: Local model evaluates artifacts + end + + Buyer->>Seller: get_media_buy_artifacts (sampled) + Seller-->>Buyer: Content artifacts + Buyer->>Verifier: validate_content_delivery + Verifier-->>Buyer: Validation results +``` + +**Key insight**: Runtime decisioning happens locally at the seller (for scale). Buyers pull content samples from sellers and validate against the verification agent. + +## Adjacency + +How much content around the ad slot should be evaluated? + +| Context | Adjacency Examples | +|---------|-------------------| +| **News article** | The article where the ad appears | +| **Social feed** | 1-2 posts above and below the ad slot | +| **Podcast** | The segment before and after the ad break | +| **CTV** | 1-2 scenes before and after the ad pod | +| **Infinite scroll** | Posts within the visible viewport | + +Adjacency requirements are defined by the seller in their product catalog (`get_products`). The buyer can filter products based on adjacency guarantees: + +```json +{ + "product_id": "reddit_feed_standard", + "content_standards_adjacency_definition": { + "before": 2, + "after": 2, + "unit": "posts" + } +} +``` + +### Adjacency Units + +| Unit | Use Case | +|------|----------| +| `posts` | Social feeds, forums, comment threads | +| `scenes` | CTV, streaming video content | +| `segments` | Podcasts, audio content | +| `seconds` | Time-based adjacency in video/audio | +| `viewports` | Infinite scroll contexts | +| `articles` | News sites, content aggregators | + +Different products may offer different adjacency guarantees at different price points. + +## Sampling Rate + +What percentage of traffic should be evaluated by the verification agent? + +| Rate | Use Case | +|------|----------| +| **100%** | Premium brand safety - every impression validated | +| **10-25%** | Standard monitoring - statistical confidence | +| **1-5%** | Spot checking - drift detection only | + +Sampling rate is negotiated in the media buy: + +```json +{ + "governance": { + "content_standards": { + "agent_url": "https://safety.ias.com/adcp", + "standards_id": "nike_brand_safety", + "sampling_rate": 0.25 + } + } +} +``` + +Higher sampling rates typically cost more but provide stronger guarantees. The seller is responsible for implementing the agreed sampling rate and reporting actual coverage. + +## Policies + +Content Standards uses **natural language prompts** rather than rigid keyword lists: + +```json +{ + "policy": "Sports and fitness content is ideal. Lifestyle content about health is good. Entertainment is generally acceptable. Avoid content about violence, controversial politics, adult themes, or content portraying sedentary lifestyle positively. Block hate speech, illegal activities, or ongoing litigation against our company.", + "calibration": { + "acceptable": [ + { + "property_id": {"type": "domain", "value": "espn.com"}, + "artifact_id": "nba_championship_recap_2024", + "assets": [{"type": "text", "role": "title", "content": "Championship Game Recap"}] + } + ], + "unacceptable": [ + { + "property_id": {"type": "domain", "value": "tabloid.example.com"}, + "artifact_id": "scandal_story_123", + "assets": [{"type": "text", "role": "title", "content": "Celebrity Scandal Exposed"}] + } + ] + } +} +``` + +The policy prompt enables AI-powered verification agents to understand context and nuance. **Calibration** examples provide a training/test set that helps the agent interpret the policy correctly. + +See [Artifacts](/docs/governance/content-standards/artifacts) for details on artifact structure and secured asset access. + +## Scoped Standards + +Buyers typically maintain multiple standards configurations for different contexts - UK TV campaigns have different regulations than US display, and children's brands need stricter safety than adult beverages. + +```json +{ + "standards_id": "coke_uk_tv_zero", + "name": "UK TV - Coca-Cola zero-calorie brands", + "brand_ids": ["coke_zero", "diet_coke"], + "countries_all": ["GB"], + "channels_any": ["ctv", "linear_tv"] +} +``` + +**The buyer selects the appropriate `standards_id` when creating a media buy.** The seller receives a reference to the resolved standards - they don't need to do scope matching themselves. + +## Calibration + +Before running campaigns, sellers calibrate their local models against the verification agent. This is a **dialogue-based process** that may involve human review on either side: + +1. Seller sends sample artifacts to the verification agent +2. Verification agent returns verdicts with detailed explanations +3. Seller asks follow-up questions about edge cases +4. Process repeats until alignment is achieved + +**Human-in-the-loop**: Calibration often involves humans on both sides. A brand safety specialist at the buyer might review edge cases flagged by the verification agent. A content operations team at the seller might curate calibration samples and validate the local model's learning. The protocol supports async workflows where either party can pause for human review before responding. + +```json +// Seller: "Does this pass?" +{ + "artifact": { + "property_id": {"type": "domain", "value": "reddit.com"}, + "artifact_id": "r_news_politics_123", + "assets": [{"type": "text", "role": "title", "content": "Political News Article"}] + } +} + +// Verification agent: "No, because..." +{ + "verdict": "fail", + "explanation": "Political content is excluded by brand policy, even when balanced.", + "policy_alignment": { + "violations": [{ + "policy_text": "Avoid content about controversial politics", + "violation_reason": "Article discusses ongoing political controversy" + }] + } +} +``` + +See [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) for the full task specification. + +## Tasks + +### Discovery + +| Task | Description | +|------|-------------| +| [list_content_features](/docs/governance/content-standards/tasks/list_content_features) | Discover features the agent can evaluate | +| [list_content_standards](/docs/governance/content-standards/tasks/list_content_standards) | List available standards configurations | +| [get_content_standards](/docs/governance/content-standards/tasks/get_content_standards) | Retrieve a specific standards configuration | + +### Management + +| Task | Description | +|------|-------------| +| [create_content_standards](/docs/governance/content-standards/tasks/create_content_standards) | Create a new standards configuration | +| [update_content_standards](/docs/governance/content-standards/tasks/update_content_standards) | Update an existing standards configuration | +| [delete_content_standards](/docs/governance/content-standards/tasks/delete_content_standards) | Delete a standards configuration | + +### Calibration & Validation + +| Task | Description | +|------|-------------| +| [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) | Collaborative dialogue to align on policy interpretation | +| [get_media_buy_artifacts](/docs/governance/content-standards/tasks/get_media_buy_artifacts) | Retrieve content artifacts from a media buy | +| [validate_content_delivery](/docs/governance/content-standards/tasks/validate_content_delivery) | Batch validation of content artifacts | + +## Typical Providers + +- **IAS** - Integral Ad Science +- **DoubleVerify** - Brand safety and verification +- **Scope3** - Sustainability-focused brand safety with prompt-based policies +- **Custom** - Brand-specific implementations + +## Related + +- [Artifacts](/docs/governance/content-standards/artifacts) - What artifacts are and how to structure them +- [Brand Manifest](/docs/creative/brand-manifest) - Static brand identity that can link to standards agents diff --git a/docs/governance/content-standards/tasks/calibrate_content.mdx b/docs/governance/content-standards/tasks/calibrate_content.mdx new file mode 100644 index 00000000..e65a7169 --- /dev/null +++ b/docs/governance/content-standards/tasks/calibrate_content.mdx @@ -0,0 +1,229 @@ +--- +title: calibrate_content +sidebar_position: 7 +--- + +# calibrate_content + +Collaborative calibration task for aligning on content standards interpretation. Used during setup to help sellers understand and internalize a buyer's content policies before campaign execution. + +Unlike high-volume runtime evaluation, calibration is a **dialogue-based process** where parties exchange examples and explanations until aligned. + +## When to Use + +- **Seller onboarding**: When a seller first receives content standards from a buyer +- **Policy clarification**: When a seller needs to understand why specific content passes or fails +- **Model training**: When building a local model to run against the standards +- **Drift detection**: Periodic re-calibration to ensure continued alignment + +## Request + +**Schema**: [calibrate-content-request.json](https://adcontextprotocol.org/schemas/v2/content-standards/calibrate-content-request.json) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `standards_id` | string | Yes | Standards configuration to calibrate against | +| `artifact` | artifact | Yes | Artifact to evaluate | + +### Artifact + +**Schema**: [artifact.json](https://adcontextprotocol.org/schemas/v2/content-standards/artifact.json) + +An artifact represents content context where ad placements occur - identified by `property_id` + `artifact_id` and represented as a collection of assets: + +```json +{ + "property_id": {"type": "domain", "value": "reddit.com"}, + "artifact_id": "r_fitness_abc123", + "assets": [ + {"type": "text", "role": "title", "content": "Best protein sources for muscle building", "language": "en"}, + {"type": "text", "role": "paragraph", "content": "Looking for recommendations on high-quality protein sources...", "language": "en"}, + {"type": "text", "role": "paragraph", "content": "I've been lifting for 6 months and want to optimize my diet.", "language": "en"}, + {"type": "image", "url": "https://cdn.reddit.com/fitness-image.jpg", "alt_text": "Person lifting weights"} + ] +} +``` + +## Response + +**Schema**: [calibrate-content-response.json](https://adcontextprotocol.org/schemas/v2/content-standards/calibrate-content-response.json) + +### Passing Response + +```json +{ + "verdict": "pass", + "explanation": "This content aligns well with the brand's fitness-focused positioning. Health and fitness content is explicitly marked as 'ideal' in the policy. The discussion is constructive and educational.", + "features": [ + { + "feature_id": "brand_safety", + "status": "passed", + "explanation": "No safety concerns. Content is user-generated but constructive fitness discussion." + }, + { + "feature_id": "brand_suitability", + "status": "passed", + "explanation": "Fitness content matches brand's athletic positioning." + } + ] +} +``` + +### Failing Response with Detailed Explanation + +```json +{ + "verdict": "fail", + "explanation": "This content discusses political topics which the policy explicitly excludes. While the article itself is balanced journalism, the brand has requested to avoid all controversial political content regardless of tone.", + "features": [ + { + "feature_id": "brand_safety", + "status": "passed", + "explanation": "No hate speech, illegal content, or explicit material." + }, + { + "feature_id": "brand_suitability", + "status": "failed", + "explanation": "Political content is excluded by brand policy, even when balanced." + } + ] +} +``` + +### Response Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `verdict` | Yes | Overall `pass` or `fail` decision | +| `explanation` | No | Detailed natural language explanation of the decision | +| `features` | No | Per-feature breakdown with explanations | +| `confidence` | No | Model confidence in the verdict (0-1), when available | + +## Dialogue Flow + +Calibration supports back-and-forth dialogue using the protocol's conversation management. The seller sends content, the verification agent responds with an evaluation and explanation, and the seller can respond with questions or try different content - all within the same conversation context. + +### A2A Example + +```javascript +// Seller sends artifact to evaluate +const response1 = await a2a.send({ + message: { + parts: [{ + kind: "data", + data: { + skill: "calibrate_content", + parameters: { + standards_id: "nike_brand_safety", + artifact: { + property_id: { type: "domain", value: "reddit.com" }, + artifact_id: "r_news_politics_123", + assets: [ + { type: "text", role: "title", content: "Political News Article" } + ] + } + } + } + }] + } +}); +// Response: verdict=fail with feature breakdown + +// Seller asks follow-up question about the decision +const response2 = await a2a.send({ + contextId: response1.contextId, + message: { + parts: [{ + kind: "text", + text: "This is factual news, not opinion. Should balanced journalism be excluded?" + }] + } +}); +// Verification agent clarifies that brand policy excludes ALL political content + +// Seller tries different artifact +const response3 = await a2a.send({ + contextId: response1.contextId, + message: { + parts: [{ + kind: "data", + data: { + skill: "calibrate_content", + parameters: { + standards_id: "nike_brand_safety", + artifact: { + property_id: { type: "domain", value: "reddit.com" }, + artifact_id: "r_running_tips_456", + assets: [ + { type: "text", role: "title", content: "Running Tips" } + ] + } + } + } + }] + } +}); +// Response: verdict=pass - now seller understands the boundaries +``` + +### MCP Example + +```javascript +// Initial calibration request +const response1 = await mcp.call('calibrate_content', { + standards_id: "nike_brand_safety", + artifact: { + property_id: { type: "domain", value: "reddit.com" }, + artifact_id: "r_news_politics_123", + assets: [ + { type: "text", role: "title", content: "Political News Article" } + ] + } +}); +// Response includes context_id for conversation continuity + +// Continue dialogue with follow-up question +const response2 = await mcp.call('calibrate_content', { + context_id: response1.context_id, + standards_id: "nike_brand_safety", + artifact: { + property_id: { type: "domain", value: "reddit.com" }, + artifact_id: "r_news_politics_123", + assets: [ + { type: "text", role: "title", content: "Political News Article" } + ] + } +}); +// Include text message in the protocol envelope asking about balanced journalism + +// Try different artifact in same conversation +const response3 = await mcp.call('calibrate_content', { + context_id: response1.context_id, + standards_id: "nike_brand_safety", + artifact: { + property_id: { type: "domain", value: "reddit.com" }, + artifact_id: "r_running_tips_456", + assets: [ + { type: "text", role: "title", content: "Running Tips" } + ] + } +}); +``` + +The key insight is that the dialogue happens at the **protocol layer**, not the task layer. The verification agent maintains conversation context and can respond to follow-up questions, disagreements, or requests for clarification - just like any agent-to-agent conversation. + +## Calibration vs Runtime + +| Aspect | calibrate_content | Runtime (local model) | +|--------|-------------------|----------------------| +| **Purpose** | Alignment & understanding | High-volume decisioning | +| **Volume** | Low (setup/periodic) | High (every impression) | +| **Response** | Verbose explanations | Pass/fail only | +| **Latency** | Seconds acceptable | Milliseconds required | +| **Dialogue** | Multi-turn conversation | Stateless | + +## Related Tasks + +- [get_content_standards](/docs/governance/content-standards/tasks/get_content_standards) - Retrieve the policies being calibrated against +- [list_content_features](/docs/governance/content-standards/tasks/list_content_features) - Discover available features +- [validate_content_delivery](/docs/governance/content-standards/tasks/validate_content_delivery) - Post-campaign delivery validation diff --git a/docs/governance/content-standards/tasks/create_content_standards.mdx b/docs/governance/content-standards/tasks/create_content_standards.mdx new file mode 100644 index 00000000..b62f742f --- /dev/null +++ b/docs/governance/content-standards/tasks/create_content_standards.mdx @@ -0,0 +1,100 @@ +--- +title: create_content_standards +sidebar_position: 5 +--- + +# create_content_standards + +Create a new content standards configuration. + +**Response time**: < 1s + +## Request + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `scope` | object | Yes | Where this standards configuration applies | +| `policy` | string | Yes | Natural language policy prompt | +| `calibration` | object | No | Training set of acceptable/unacceptable artifacts | +| `competitive_separation` | array | No | Competitor brands to avoid | +| `floor` | string | No | Safety floor baseline (`garm_floor` or `custom`) | + +### Example Request + +```json +{ + "scope": { + "brand_ids": ["nike"], + "countries_all": ["GB", "DE", "FR"], + "channels_any": ["display", "video", "ctv"], + "description": "Nike EMEA - all digital channels" + }, + "policy": "Sports and fitness content is ideal. Lifestyle content about health and wellness is good. Entertainment content is generally acceptable. Avoid content about violence, controversial political topics, adult themes, or content that portrays sedentary lifestyle positively.", + "calibration": { + "acceptable": [ + { "type": "domain", "value": "espn.com", "language": "en" }, + { "type": "domain", "value": "healthline.com", "language": "en" } + ], + "unacceptable": [ + { "type": "domain", "value": "tabloid.example.com", "language": "en" } + ] + }, + "competitive_separation": ["adidas", "puma", "under_armour"], + "floor": "garm_floor" +} +``` + +## Response + +### Success Response + +```json +{ + "standards_id": "nike_emea_safety", + "version": "1.0.0", + "effective_date": "2025-01-05T00:00:00Z" +} +``` + +### Error Responses + +**Invalid Scope:** + +```json +{ + "errors": [ + { + "code": "INVALID_SCOPE", + "message": "At least one brand_id is required" + } + ] +} +``` + +**Scope Conflict:** + +```json +{ + "errors": [ + { + "code": "SCOPE_CONFLICT", + "message": "Standards already exist for brand 'nike' in country 'DE' on channel 'display'", + "conflicting_standards_id": "nike_emea_safety" + } + ] +} +``` + +## Scope Conflict Handling + +Multiple standards cannot have overlapping scopes for the same brand/country/channel combination. When creating standards that would conflict: + +1. **Check existing standards** - Use [list_content_standards](/docs/governance/content-standards/tasks/list_content_standards) filtered by your scope +2. **Update rather than create** - If standards already exist, use [update_content_standards](/docs/governance/content-standards/tasks/update_content_standards) +3. **Narrow the scope** - Adjust countries or channels to avoid overlap + +## Related Tasks + +- [list_content_standards](/docs/governance/content-standards/tasks/list_content_standards) - List all configurations +- [update_content_standards](/docs/governance/content-standards/tasks/update_content_standards) - Update a configuration +- [delete_content_standards](/docs/governance/content-standards/tasks/delete_content_standards) - Delete a configuration diff --git a/docs/governance/content-standards/tasks/delete_content_standards.mdx b/docs/governance/content-standards/tasks/delete_content_standards.mdx new file mode 100644 index 00000000..2f38244c --- /dev/null +++ b/docs/governance/content-standards/tasks/delete_content_standards.mdx @@ -0,0 +1,70 @@ +--- +title: delete_content_standards +sidebar_position: 7 +--- + +# delete_content_standards + +Delete a content standards configuration. + +**Response time**: < 500ms + +## Request + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `standards_id` | string | Yes | ID of the standards configuration to delete | + +### Example Request + +```json +{ + "standards_id": "nike_emea_safety" +} +``` + +## Response + +### Success Response + +```json +{ + "deleted": true, + "standards_id": "nike_emea_safety" +} +``` + +### Error Responses + +**Not Found:** + +```json +{ + "errors": [ + { + "code": "STANDARDS_NOT_FOUND", + "message": "No standards found with ID 'invalid_id'" + } + ] +} +``` + +**Standards In Use:** + +```json +{ + "errors": [ + { + "code": "STANDARDS_IN_USE", + "message": "Cannot delete standards 'nike_emea_safety' - currently referenced by active media buys" + } + ] +} +``` + +Standards cannot be deleted while they are referenced by active media buys. Use [list_content_standards](/docs/governance/content-standards/tasks/list_content_standards) to identify usage, or archive standards by setting an expiration date rather than deleting. + +## Related Tasks + +- [list_content_standards](/docs/governance/content-standards/tasks/list_content_standards) - List all configurations +- [create_content_standards](/docs/governance/content-standards/tasks/create_content_standards) - Create a new configuration diff --git a/docs/governance/content-standards/tasks/get_content_standards.mdx b/docs/governance/content-standards/tasks/get_content_standards.mdx new file mode 100644 index 00000000..78479d66 --- /dev/null +++ b/docs/governance/content-standards/tasks/get_content_standards.mdx @@ -0,0 +1,82 @@ +--- +title: get_content_standards +sidebar_position: 2 +--- + +# get_content_standards + +Retrieve content safety policies for a specific standards configuration. + +## Request + +**Schema**: [get-content-standards-request.json](https://adcontextprotocol.org/schemas/v2/content-standards/get-content-standards-request.json) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `standards_id` | string | Yes | Identifier for the standards configuration | + +## Response + +**Schema**: [get-content-standards-response.json](https://adcontextprotocol.org/schemas/v2/content-standards/get-content-standards-response.json) + +### Success Response + +```json +{ + "standards_id": "nike_emea_safety", + "version": "1.2.0", + "effective_date": "2025-01-01T00:00:00Z", + "name": "Nike EMEA - all digital channels", + "brand_ids": ["nike"], + "countries_all": ["GB", "DE", "FR"], + "channels_any": ["display", "video", "ctv"], + "policy": "Sports and fitness content is ideal. Lifestyle content about health and wellness is good. Entertainment content is generally acceptable. Avoid content about violence, controversial political topics, adult themes, or content that portrays sedentary lifestyle positively. Block hate speech, illegal activities, or content disparaging athletes.", + "calibration": { + "acceptable": [ + { "type": "domain", "value": "espn.com", "language": "en" }, + { "type": "domain", "value": "healthline.com", "language": "en" }, + { "type": "text", "value": "Lakers win championship in thrilling overtime finish", "language": "en" } + ], + "unacceptable": [ + { "type": "domain", "value": "tabloid.example.com", "language": "en" }, + { "type": "text", "value": "Political scandal rocks the nation", "language": "en" }, + { "type": "audio_url", "value": "https://cdn.example.com/controversial-podcast.mp3" } + ] + }, + "competitive_separation": ["adidas", "puma", "under_armour", "reebok"], + "floor": "garm_floor" +} +``` + +### Fields + +| Field | Description | +|-------|-------------| +| `standards_id` | Unique identifier for this standards configuration | +| `version` | Version of this configuration | +| `name` | Human-readable name | +| `brand_ids` | Brand identifiers as defined in the Brand Manifest | +| `countries_all` | ISO country codes - standards apply in ALL listed countries | +| `channels_any` | Ad channels - standards apply to ANY of the listed channels | +| `policy` | Natural language policy describing acceptable and unacceptable content contexts | +| `calibration` | Training/test set of content contexts to calibrate policy interpretation | +| `competitive_separation` | Competitor brands to avoid adjacency with | +| `floor` | Safety floor baseline (`garm_floor` = GARM Brand Safety Floor) | + +### Error Response + +```json +{ + "errors": [ + { + "code": "STANDARDS_NOT_FOUND", + "message": "No standards found with ID 'invalid_id'" + } + ] +} +``` + +## Related Tasks + +- [list_content_features](/docs/governance/content-standards/tasks/list_content_features) - Discover available features +- [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) - Collaborative calibration against these standards diff --git a/docs/governance/content-standards/tasks/get_media_buy_artifacts.mdx b/docs/governance/content-standards/tasks/get_media_buy_artifacts.mdx new file mode 100644 index 00000000..7b430da9 --- /dev/null +++ b/docs/governance/content-standards/tasks/get_media_buy_artifacts.mdx @@ -0,0 +1,205 @@ +--- +title: get_media_buy_artifacts +sidebar_position: 8 +--- + +# get_media_buy_artifacts + +Retrieve content artifacts from a media buy for validation. This is separate from `get_media_buy_delivery` which returns performance metrics - artifacts contain the actual content (text, images, video) where ads were placed. + +**Response time**: < 5s (batch of 1,000 artifacts) + +## Data Flow + +```mermaid +sequenceDiagram + participant Buyer as Buyer Agent + participant Seller as Seller Agent + participant Verifier as Verification Agent + + Buyer->>Seller: get_media_buy_artifacts (sampled or full) + Seller-->>Buyer: Artifacts with content + Buyer->>Verifier: validate_content_delivery + Verifier-->>Buyer: Validation results +``` + +The buyer requests artifacts from the seller using the same media buy parameters. The seller returns content samples based on the agreed sampling rate. The buyer then validates these against the verification agent. + +## Request + +**Schema**: [get-media-buy-artifacts-request.json](https://adcontextprotocol.org/schemas/v2/content-standards/get-media-buy-artifacts-request.json) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `media_buy_id` | string | Yes | Media buy to get artifacts from | +| `package_ids` | array | No | Filter to specific packages | +| `sampling` | object | No | Sampling parameters (defaults to media buy agreement) | +| `time_range` | object | No | Filter to specific time period | +| `limit` | integer | No | Maximum artifacts to return (default: 1000) | +| `cursor` | string | No | Pagination cursor for large result sets | + +### Sampling Options + +```json +{ + "sampling": { + "rate": 0.25, + "method": "random" + } +} +``` + +| Method | Description | +|--------|-------------| +| `random` | Random sample across all deliveries | +| `stratified` | Sample proportionally across packages/properties | +| `recent` | Most recent deliveries first | +| `failures_only` | Only artifacts that failed local evaluation | + +## Response + +**Schema**: [get-media-buy-artifacts-response.json](https://adcontextprotocol.org/schemas/v2/content-standards/get-media-buy-artifacts-response.json) + +### Success Response + +```json +{ + "media_buy_id": "mb_nike_reddit_q1", + "artifacts": [ + { + "record_id": "imp_12345", + "timestamp": "2025-01-15T10:30:00Z", + "package_id": "pkg_feed_standard", + "artifact": { + "property_id": {"type": "domain", "value": "reddit.com"}, + "artifact_id": "r_fitness_abc123", + "assets": [ + {"type": "text", "role": "title", "content": "Best protein sources for muscle building", "language": "en"}, + {"type": "text", "role": "paragraph", "content": "Looking for recommendations on high-quality protein sources...", "language": "en"}, + {"type": "image", "url": "https://cdn.reddit.com/fitness-image.jpg", "alt_text": "Person lifting weights"} + ] + }, + "country": "US", + "channel": "social", + "brand_context": {"brand_id": "nike_global", "sku_id": "air_max_2025"}, + "local_verdict": "pass" + }, + { + "record_id": "imp_12346", + "timestamp": "2025-01-15T10:35:00Z", + "package_id": "pkg_feed_standard", + "artifact": { + "property_id": {"type": "domain", "value": "reddit.com"}, + "artifact_id": "r_news_politics_456", + "assets": [ + {"type": "text", "role": "title", "content": "Election Results Analysis", "language": "en"}, + {"type": "text", "role": "paragraph", "content": "The latest polling data shows...", "language": "en"} + ] + }, + "country": "US", + "channel": "social", + "brand_context": {"brand_id": "nike_global", "sku_id": "air_max_2025"}, + "local_verdict": "fail" + } + ], + "sampling_info": { + "total_deliveries": 100000, + "sampled_count": 1000, + "effective_rate": 0.01, + "method": "random" + }, + "pagination": { + "cursor": "eyJvZmZzZXQiOjEwMDB9", + "has_more": true + } +} +``` + +### Response Fields + +| Field | Description | +|-------|-------------| +| `artifacts` | Array of delivery records with full artifact content | +| `artifacts[].country` | ISO 3166-1 alpha-2 country code where delivery occurred | +| `artifacts[].channel` | Channel type (display, video, audio, social) | +| `artifacts[].brand_context` | Brand/SKU information for policy evaluation (schema TBD) | +| `artifacts[].local_verdict` | Seller's local model verdict (pass/fail/unevaluated) | +| `sampling_info` | How the sample was generated | +| `pagination` | Cursor for fetching more results | + +## Use Cases + +### Validate Sample Against Standards + +```python +# Get artifacts from seller +artifacts_response = seller_agent.get_media_buy_artifacts( + media_buy_id="mb_nike_reddit_q1", + sampling={"rate": 0.25, "method": "random"} +) + +# Convert to validation records +records = [ + { + "record_id": a["record_id"], + "timestamp": a["timestamp"], + "media_buy_id": artifacts_response["media_buy_id"], + "artifact": a["artifact"], + "country": a.get("country"), + "channel": a.get("channel"), + "brand_context": a.get("brand_context") + } + for a in artifacts_response["artifacts"] +] + +# Validate against verification agent +validation = verification_agent.validate_content_delivery( + standards_id="nike_brand_safety", + records=records +) + +# Check for drift between local and verified verdicts +for i, result in enumerate(validation["results"]): + local = artifacts_response["artifacts"][i]["local_verdict"] + verified = result["verdict"] + if local != verified: + print(f"Drift detected: {result['record_id']} - local={local}, verified={verified}") +``` + +### Focus on Local Failures + +```python +# Get only artifacts that failed local evaluation +failures = seller_agent.get_media_buy_artifacts( + media_buy_id="mb_nike_reddit_q1", + sampling={"method": "failures_only"}, + limit=100 +) + +# Verify these were correctly flagged +validation = verification_agent.validate_content_delivery( + standards_id="nike_brand_safety", + records=[{"record_id": a["record_id"], "artifact": a["artifact"]} + for a in failures["artifacts"]] +) + +# Check false positive rate +false_positives = sum(1 for r in validation["results"] if r["verdict"] == "pass") +print(f"False positive rate: {false_positives / len(failures['artifacts']):.1%}") +``` + +## Delivery vs Artifacts + +| Aspect | get_media_buy_delivery | get_media_buy_artifacts | +|--------|------------------------|-------------------------| +| **Purpose** | Performance reporting | Content validation | +| **Data size** | Small (metrics) | Large (full content) | +| **Frequency** | Regular reporting | Sampled validation | +| **Contains** | Impressions, clicks, spend | Text, images, video | +| **Consumer** | Buyer for optimization | Verification agent | + +## Related Tasks + +- [validate_content_delivery](/docs/governance/content-standards/tasks/validate_content_delivery) - Validate the artifacts +- [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) - Understand why artifacts pass/fail +- [get_media_buy_delivery](/docs/media-buy/task-reference/get_media_buy_delivery) - Get performance metrics diff --git a/docs/governance/content-standards/tasks/list_content_features.mdx b/docs/governance/content-standards/tasks/list_content_features.mdx new file mode 100644 index 00000000..332af17a --- /dev/null +++ b/docs/governance/content-standards/tasks/list_content_features.mdx @@ -0,0 +1,117 @@ +--- +title: list_content_features +sidebar_position: 1 +--- + +# list_content_features + +Discover what features a Content Standards agent can evaluate. Features define the verification capabilities available. + +**Response time**: < 500ms + +## Request + +**Schema**: [list-content-features-request.json](https://adcontextprotocol.org/schemas/v2/content-standards/list-content-features-request.json) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `standards_id` | string | No | Filter features available for a specific standards configuration | + +## Response + +**Schema**: [list-content-features-response.json](https://adcontextprotocol.org/schemas/v2/content-standards/list-content-features-response.json) + +### Success Response + +```json +{ + "features": [ + { + "feature_id": "brand_safety", + "name": "Brand Safety", + "description": "Overall brand safety classification", + "type": "categorical", + "allowed_values": ["safe", "low_risk", "medium_risk", "high_risk", "blocked"], + "severity": "block" + }, + { + "feature_id": "sentiment", + "name": "Content Sentiment", + "description": "Overall sentiment of the content", + "type": "categorical", + "allowed_values": ["positive", "neutral", "negative"], + "severity": "warn" + }, + { + "feature_id": "sentiment_score", + "name": "Sentiment Score", + "description": "Numerical sentiment score", + "type": "quantitative", + "range": { "min": -1.0, "max": 1.0 }, + "severity": "info" + }, + { + "feature_id": "competitor_adjacency", + "name": "Competitor Adjacency", + "description": "Whether content mentions competitor brands", + "type": "binary", + "severity": "block" + }, + { + "feature_id": "garm_category", + "name": "GARM Category", + "description": "GARM Brand Safety Floor category", + "type": "categorical", + "allowed_values": ["adult_explicit", "arms_ammunition", "crime_harmful", "death_injury", "online_piracy", "hate_speech", "terrorism", "spam", "safe"], + "severity": "block" + }, + { + "feature_id": "news_quality", + "name": "News Quality Score", + "description": "Quality assessment for news content", + "type": "quantitative", + "range": { "min": 0, "max": 100 }, + "severity": "warn" + } + ] +} +``` + +### Feature Types + +| Type | Description | Example | +|------|-------------|---------| +| `binary` | True/false evaluation | `competitor_adjacency` | +| `quantitative` | Numeric value in a range | `sentiment_score` (-1.0 to 1.0) | +| `categorical` | One of allowed values | `brand_safety` (safe, low_risk, etc.) | + +### Severity Levels + +| Severity | Meaning | +|----------|---------| +| `block` | Failed features with this severity should block the placement | +| `warn` | Advisory warning, may not block | +| `info` | Informational, for reporting only | + +### Regional Features + +Some features may only be available in certain regions: + +```json +{ + "feature_id": "imagery_policy", + "name": "Imagery Policy Compliance", + "description": "Compliance with regional imagery policies", + "type": "binary", + "severity": "block", + "coverage": { + "regions": ["EMEA", "APAC"], + "markets": ["SA", "AE", "KW"] + } +} +``` + +## Related Tasks + +- [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) - Uses these features for evaluation +- [validate_content_delivery](/docs/governance/content-standards/tasks/validate_content_delivery) - Batch verification with feature results diff --git a/docs/governance/content-standards/tasks/list_content_standards.mdx b/docs/governance/content-standards/tasks/list_content_standards.mdx new file mode 100644 index 00000000..a136a0f3 --- /dev/null +++ b/docs/governance/content-standards/tasks/list_content_standards.mdx @@ -0,0 +1,73 @@ +--- +title: list_content_standards +sidebar_position: 2 +--- + +# list_content_standards + +List available content standards configurations. + +**Response time**: < 500ms + +## Request + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `brand_ids` | array | No | Filter by brand identifiers | +| `countries` | array | No | Filter by country codes | +| `channels` | array | No | Filter by channels | +| `effective_before` | string | No | Filter to standards effective before this date (ISO 8601) | +| `effective_after` | string | No | Filter to standards effective after this date (ISO 8601) | + +## Response + +Returns an abbreviated list of standards configurations. Use [get_content_standards](/docs/governance/content-standards/tasks/get_content_standards) to retrieve full details including policy text and calibration data. + +### Success Response + +```json +{ + "standards": [ + { + "standards_id": "nike_emea_safety", + "version": "1.2.0", + "effective_date": "2025-01-05T00:00:00Z", + "scope": { + "brand_ids": ["nike"], + "countries_all": ["GB", "DE", "FR"], + "channels_any": ["display", "video", "ctv"], + "description": "Nike EMEA - all digital channels" + } + }, + { + "standards_id": "nike_us_display", + "version": "1.0.0", + "effective_date": "2025-01-10T00:00:00Z", + "scope": { + "brand_ids": ["nike"], + "countries_all": ["US"], + "channels_any": ["display"], + "description": "Nike US - display only" + } + } + ] +} +``` + +### Error Response + +```json +{ + "errors": [ + { + "code": "UNAUTHORIZED", + "message": "Invalid or expired token" + } + ] +} +``` + +## Related Tasks + +- [get_content_standards](/docs/governance/content-standards/tasks/get_content_standards) - Get a specific standards configuration +- [create_content_standards](/docs/governance/content-standards/tasks/create_content_standards) - Create a new configuration diff --git a/docs/governance/content-standards/tasks/update_content_standards.mdx b/docs/governance/content-standards/tasks/update_content_standards.mdx new file mode 100644 index 00000000..25c4c28e --- /dev/null +++ b/docs/governance/content-standards/tasks/update_content_standards.mdx @@ -0,0 +1,72 @@ +--- +title: update_content_standards +sidebar_position: 6 +--- + +# update_content_standards + +Update an existing content standards configuration. Creates a new version. + +**Response time**: < 1s + +## Request + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `standards_id` | string | Yes | ID of the standards configuration to update | +| `scope` | object | No | Updated scope | +| `policy` | string | No | Updated policy prompt | +| `examples` | object | No | Updated training examples | +| `competitive_separation` | array | No | Updated competitors list | +| `floor` | string | No | Updated safety floor | + +### Example Request + +```json +{ + "standards_id": "nike_emea_safety", + "policy": "Sports and fitness content is ideal. Lifestyle content about health and wellness is good. Entertainment content is generally acceptable. Avoid violence, controversial politics, adult themes. Block hate speech and illegal activities.", + "examples": { + "acceptable": [ + { "type": "domain", "value": "espn.com", "language": "en" }, + { "type": "domain", "value": "healthline.com", "language": "en" }, + { "type": "domain", "value": "runnersworld.com", "language": "en" } + ], + "unacceptable": [ + { "type": "domain", "value": "tabloid.example.com", "language": "en" }, + { "type": "domain", "value": "gambling.example.com", "language": "en" } + ] + } +} +``` + +## Response + +### Success Response + +```json +{ + "standards_id": "nike_emea_safety", + "version": "1.3.0", + "effective_date": "2025-01-05T12:00:00Z" +} +``` + +### Error Response + +```json +{ + "errors": [ + { + "code": "STANDARDS_NOT_FOUND", + "message": "No standards found with ID 'invalid_id'" + } + ] +} +``` + +## Related Tasks + +- [get_content_standards](/docs/governance/content-standards/tasks/get_content_standards) - Get current configuration +- [create_content_standards](/docs/governance/content-standards/tasks/create_content_standards) - Create a new configuration +- [delete_content_standards](/docs/governance/content-standards/tasks/delete_content_standards) - Delete a configuration diff --git a/docs/governance/content-standards/tasks/validate_content_delivery.mdx b/docs/governance/content-standards/tasks/validate_content_delivery.mdx new file mode 100644 index 00000000..f2d71e35 --- /dev/null +++ b/docs/governance/content-standards/tasks/validate_content_delivery.mdx @@ -0,0 +1,194 @@ +--- +title: validate_content_delivery +sidebar_position: 4 +--- + +# validate_content_delivery + +Validate delivery records against content safety policies. Designed for batch auditing of where ads were actually delivered. + +**Response time**: < 60s (batch of 10,000 records) + +## Data Flow + +Content artifacts are separate from delivery metrics. Use `get_media_buy_artifacts` to retrieve content for validation: + +```mermaid +sequenceDiagram + participant Buyer as Buyer Agent + participant Seller as Seller Agent + participant Verifier as Verification Agent + + Buyer->>Seller: get_media_buy_artifacts (sampled) + Seller-->>Buyer: Artifacts with content + Buyer->>Verifier: validate_content_delivery + Verifier-->>Buyer: Validation results +``` + +**Why through the buyer?** + +- The **buyer** owns the media buy and knows which `standards_id` applies +- The **buyer** requests artifacts from sellers (separate from performance metrics) +- The **buyer** is accountable for brand safety compliance +- The **verification agent** works on behalf of the buyer + +This keeps responsibilities clear: sellers provide content samples via `get_media_buy_artifacts`, buyers validate samples against the verification agent. + +## Request + +**Schema**: [validate-content-delivery-request.json](https://adcontextprotocol.org/schemas/v2/content-standards/validate-content-delivery-request.json) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `standards_id` | string | Yes | Standards configuration to validate against | +| `records` | array | Yes | Delivery records to validate (max 10,000) | +| `feature_ids` | array | No | Specific features to evaluate (defaults to all) | +| `include_passed` | boolean | No | Include passed records in results (default: true) | + +### Delivery Record + +```json +{ + "record_id": "imp_12345", + "timestamp": "2025-01-15T10:30:00Z", + "media_buy_id": "mb_nike_reddit_q1", + "artifact": { + "property_id": {"type": "domain", "value": "example.com"}, + "artifact_id": "article_12345", + "assets": [ + {"type": "text", "role": "title", "content": "Article Title"} + ] + }, + "country": "US", + "channel": "display", + "brand_context": { + "brand_id": "nike_global", + "sku_id": "air_max_2025" + } +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `record_id` | Yes | Unique identifier for this delivery record | +| `artifact` | Yes | Content artifact where ad was delivered | +| `media_buy_id` | No | Media buy this record belongs to (for multi-buy batches) | +| `timestamp` | No | When the delivery occurred | +| `country` | No | ISO 3166-1 alpha-2 country code for targeting context | +| `channel` | No | Channel type (display, video, audio, social) | +| `brand_context` | No | Brand/SKU information for policy evaluation (schema TBD) | + +## Response + +**Schema**: [validate-content-delivery-response.json](https://adcontextprotocol.org/schemas/v2/content-standards/validate-content-delivery-response.json) + +### Success Response + +```json +{ + "summary": { + "total_records": 1000, + "passed_records": 950, + "failed_records": 50, + "total_features": 5000, + "passed_features": 4750, + "failed_features": 250 + }, + "results": [ + { + "record_id": "imp_12345", + "features": [ + { + "feature_id": "brand_safety", + "status": "passed", + "value": "safe" + }, + { + "feature_id": "competitor_adjacency", + "status": "passed", + "value": false + } + ] + }, + { + "record_id": "imp_12346", + "features": [ + { + "feature_id": "brand_safety", + "status": "failed", + "value": "high_risk", + "message": "Content contains violence" + }, + { + "feature_id": "competitor_adjacency", + "status": "passed", + "value": false + } + ] + } + ] +} +``` + +## Use Cases + +### Post-Campaign Audit + +```python +def audit_campaign_delivery(campaign_id, standards_id, content_standards_agent): + """Audit all delivery records from a campaign.""" + # Fetch delivery records from your ad server + records = fetch_delivery_records(campaign_id) + + # Validate in batches + batch_size = 10000 + all_results = [] + + for i in range(0, len(records), batch_size): + batch = records[i:i + batch_size] + response = content_standards_agent.validate_content_delivery( + standards_id=standards_id, + records=batch + ) + all_results.extend(response["results"]) + + return all_results +``` + +### Real-Time Monitoring Sample + +```python +import random + +def sample_and_validate(records, standards_id, sample_size=1000): + """Validate a random sample for real-time monitoring.""" + sample = random.sample(records, min(sample_size, len(records))) + return content_standards_agent.validate_content_delivery( + standards_id=standards_id, + records=sample + ) +``` + +### Filter for Issues Only + +```python +# Only get failed records to reduce response size +response = content_standards_agent.validate_content_delivery( + standards_id="nike_emea_safety", + records=delivery_records, + include_passed=False # Only return failures +) + +for result in response["results"]: + print(f"Issue with {result['record_id']}") + for feature in result["features"]: + if feature["status"] == "failed": + print(f" - {feature['feature_id']}: {feature['message']}") +``` + +## Related Tasks + +- [get_media_buy_artifacts](/docs/governance/content-standards/tasks/get_media_buy_artifacts) - Get content artifacts from seller +- [calibrate_content](/docs/governance/content-standards/tasks/calibrate_content) - Understand why artifacts pass/fail +- [list_content_features](/docs/governance/content-standards/tasks/list_content_features) - Discover available features +- [get_content_standards](/docs/governance/content-standards/tasks/get_content_standards) - Retrieve the policies diff --git a/static/schemas/source/content-standards/artifact.json b/static/schemas/source/content-standards/artifact.json new file mode 100644 index 00000000..fda9f2af --- /dev/null +++ b/static/schemas/source/content-standards/artifact.json @@ -0,0 +1,309 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/artifact.json", + "title": "Artifact", + "description": "Content artifact for safety and suitability evaluation. An artifact represents content adjacent to an ad placement - a news article, podcast segment, video chapter, or social post. Artifacts are collections of assets (text, images, video, audio) plus metadata and signals.", + "type": "object", + "properties": { + "property_id": { + "type": "object", + "description": "Identifier for the property where this artifact appears", + "properties": { + "type": { + "$ref": "/schemas/enums/identifier-types.json", + "description": "Type of property identifier" + }, + "value": { + "type": "string", + "description": "The identifier value" + } + }, + "required": ["type", "value"], + "additionalProperties": false + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "type": "object", + "description": "Optional reference to a format definition. Uses the same format registry as creative formats.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "Base URL of the agent that defines this format" + }, + "id": { + "type": "string", + "description": "Format identifier within that agent's registry" + } + }, + "required": ["agent_url", "id"], + "additionalProperties": false + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "items": { + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { "type": "string", "const": "text" }, + "role": { + "type": "string", + "enum": ["title", "paragraph", "heading", "caption", "quote", "list_item", "description"], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + } + }, + "required": ["type", "content"] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { "type": "string", "const": "image" }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + } + }, + "required": ["type", "url"] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { "type": "string", "const": "video" }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript" + }, + "transcript_source": { + "type": "string", + "enum": ["original_script", "subtitles", "closed_captions", "dub", "generated"], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + } + }, + "required": ["type", "url"] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { "type": "string", "const": "audio" }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript" + }, + "transcript_source": { + "type": "string", + "enum": ["original_script", "closed_captions", "generated"], + "description": "How the transcript was generated" + } + }, + "required": ["type", "url"] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { "type": "object" } + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_show_id": { + "type": "string", + "description": "Spotify show ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": ["property_id", "artifact_id", "assets"], + "additionalProperties": true, + "$defs": { + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { "type": "string", "const": "bearer_token" }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": ["method", "token"] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { "type": "string", "const": "service_account" }, + "provider": { + "type": "string", + "enum": ["gcp", "aws"], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": ["method", "provider"] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { "type": "string", "const": "signed_url" } + }, + "required": ["method"] + } + ] + } + } +} diff --git a/static/schemas/source/content-standards/calibrate-content-request.json b/static/schemas/source/content-standards/calibrate-content-request.json new file mode 100644 index 00000000..b259e500 --- /dev/null +++ b/static/schemas/source/content-standards/calibrate-content-request.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/calibrate-content-request.json", + "title": "Calibrate Content Request", + "description": "Request parameters for evaluating content during calibration. Multi-turn dialogue is handled at the protocol layer via contextId.", + "type": "object", + "properties": { + "standards_id": { + "type": "string", + "description": "Standards configuration to calibrate against" + }, + "artifact": { + "$ref": "/schemas/content-standards/artifact.json", + "description": "Artifact to evaluate" + } + }, + "required": ["standards_id", "artifact"] +} diff --git a/static/schemas/source/content-standards/calibrate-content-response.json b/static/schemas/source/content-standards/calibrate-content-response.json new file mode 100644 index 00000000..d6e24a81 --- /dev/null +++ b/static/schemas/source/content-standards/calibrate-content-response.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/calibrate-content-response.json", + "title": "Calibrate Content Response", + "description": "Response payload with verdict and detailed explanations for collaborative calibration", + "type": "object", + "oneOf": [ + { + "type": "object", + "description": "Success response with detailed calibration feedback", + "properties": { + "verdict": { + "type": "string", + "enum": ["pass", "fail"], + "description": "Overall pass/fail verdict for the content evaluation" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Model confidence in the verdict (0-1)" + }, + "explanation": { + "type": "string", + "description": "Detailed natural language explanation of the decision" + }, + "features": { + "type": "array", + "description": "Per-feature breakdown with explanations", + "items": { + "type": "object", + "properties": { + "feature_id": { + "type": "string", + "description": "Which feature was evaluated (e.g., brand_safety, brand_suitability, competitor_adjacency)" + }, + "status": { + "type": "string", + "enum": ["passed", "failed", "warning", "unevaluated"], + "description": "Evaluation status for this feature" + }, + "explanation": { + "type": "string", + "description": "Human-readable explanation of why this feature passed or failed" + } + }, + "required": ["feature_id", "status"] + } + }, + "errors": { + "not": {}, + "description": "Field must not be present in success response" + } + }, + "required": ["verdict"] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { "$ref": "/schemas/core/error.json" } + }, + "verdict": { + "not": {}, + "description": "Field must not be present in error response" + } + }, + "required": ["errors"] + } + ] +} diff --git a/static/schemas/source/content-standards/get-content-standards-request.json b/static/schemas/source/content-standards/get-content-standards-request.json new file mode 100644 index 00000000..7790d22e --- /dev/null +++ b/static/schemas/source/content-standards/get-content-standards-request.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/get-content-standards-request.json", + "title": "Get Content Standards Request", + "description": "Request parameters for retrieving content safety policies", + "type": "object", + "properties": { + "standards_id": { + "type": "string", + "description": "Identifier for the standards configuration to retrieve" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["standards_id"] +} diff --git a/static/schemas/source/content-standards/get-content-standards-response.json b/static/schemas/source/content-standards/get-content-standards-response.json new file mode 100644 index 00000000..79439c3b --- /dev/null +++ b/static/schemas/source/content-standards/get-content-standards-response.json @@ -0,0 +1,109 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/get-content-standards-response.json", + "title": "Get Content Standards Response", + "description": "Response payload with content safety policies", + "type": "object", + "oneOf": [ + { + "type": "object", + "description": "Success response", + "properties": { + "standards_id": { + "type": "string", + "description": "Identifier for this standards configuration" + }, + "version": { + "type": "string", + "description": "Version of this standards configuration" + }, + "effective_date": { + "type": "string", + "format": "date-time", + "description": "When this standards version became effective. Null or absent means not yet effective. Multiple standards may be effective simultaneously for different scopes." + }, + "name": { + "type": "string", + "description": "Human-readable name for this standards configuration" + }, + "brand_ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Brand identifiers as defined in the Brand Manifest" + }, + "countries_all": { + "type": "array", + "items": { "type": "string" }, + "description": "ISO 3166-1 alpha-2 country codes - standards apply in ALL listed countries" + }, + "channels_any": { + "type": "array", + "items": { "type": "string" }, + "description": "Advertising channels - standards apply to ANY of the listed channels" + }, + "policy": { + "type": "string", + "description": "Natural language policy describing acceptable and unacceptable content contexts" + }, + "calibration": { + "type": "object", + "description": "Training/test set to calibrate policy interpretation. Note: Large calibration sets may impact response size - consider using pagination or separate retrieval for large artifact collections.", + "properties": { + "acceptable": { + "type": "array", + "items": { "$ref": "/schemas/content-standards/artifact.json" }, + "description": "Artifacts that are acceptable" + }, + "unacceptable": { + "type": "array", + "items": { "$ref": "/schemas/content-standards/artifact.json" }, + "description": "Artifacts that are unacceptable" + } + } + }, + "competitive_separation": { + "type": "array", + "items": { "type": "string" }, + "description": "Competitor brands to avoid adjacency with" + }, + "floor": { + "type": "string", + "enum": ["garm_floor", "custom"], + "description": "Safety floor baseline" + }, + "errors": { + "not": {}, + "description": "Field must not be present in success response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["standards_id", "version"] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { "$ref": "/schemas/core/error.json" } + }, + "standards_id": { + "not": {}, + "description": "Field must not be present in error response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"] + } + ] +} diff --git a/static/schemas/source/content-standards/get-media-buy-artifacts-request.json b/static/schemas/source/content-standards/get-media-buy-artifacts-request.json new file mode 100644 index 00000000..90a33cbb --- /dev/null +++ b/static/schemas/source/content-standards/get-media-buy-artifacts-request.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/get-media-buy-artifacts-request.json", + "title": "Get Media Buy Artifacts Request", + "description": "Request parameters for retrieving content artifacts from a media buy for validation", + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Media buy to get artifacts from" + }, + "package_ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Filter to specific packages within the media buy" + }, + "sampling": { + "type": "object", + "description": "Sampling parameters. Defaults to the sampling rate agreed in the media buy.", + "properties": { + "rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Sampling rate (0-1). 1.0 = all deliveries, 0.25 = 25% sample." + }, + "method": { + "type": "string", + "enum": ["random", "stratified", "recent", "failures_only"], + "description": "How to select the sample" + } + } + }, + "time_range": { + "type": "object", + "description": "Filter to specific time period", + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "Start of time range (inclusive)" + }, + "end": { + "type": "string", + "format": "date-time", + "description": "End of time range (exclusive)" + } + } + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 1000, + "description": "Maximum artifacts to return per request" + }, + "cursor": { + "type": "string", + "description": "Pagination cursor for fetching subsequent pages" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["media_buy_id"] +} diff --git a/static/schemas/source/content-standards/get-media-buy-artifacts-response.json b/static/schemas/source/content-standards/get-media-buy-artifacts-response.json new file mode 100644 index 00000000..fc1035b4 --- /dev/null +++ b/static/schemas/source/content-standards/get-media-buy-artifacts-response.json @@ -0,0 +1,142 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/get-media-buy-artifacts-response.json", + "title": "Get Media Buy Artifacts Response", + "description": "Response containing content artifacts from a media buy for validation", + "type": "object", + "oneOf": [ + { + "type": "object", + "description": "Success response with artifacts", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Media buy these artifacts belong to" + }, + "artifacts": { + "type": "array", + "description": "Delivery records with full artifact content", + "items": { + "type": "object", + "properties": { + "record_id": { + "type": "string", + "description": "Unique identifier for this delivery record" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the delivery occurred" + }, + "package_id": { + "type": "string", + "description": "Which package this delivery belongs to" + }, + "artifact": { + "$ref": "/schemas/content-standards/artifact.json", + "description": "Full artifact with content assets" + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code where delivery occurred" + }, + "channel": { + "type": "string", + "description": "Channel type (e.g., display, video, audio, social)" + }, + "brand_context": { + "type": "object", + "description": "Brand information for policy evaluation. Schema TBD - placeholder for brand identifiers.", + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier" + }, + "sku_id": { + "type": "string", + "description": "Product/SKU identifier if applicable" + } + } + }, + "local_verdict": { + "type": "string", + "enum": ["pass", "fail", "unevaluated"], + "description": "Seller's local model verdict for this artifact" + } + }, + "required": ["record_id", "artifact"] + } + }, + "sampling_info": { + "type": "object", + "description": "Information about how the sample was generated", + "properties": { + "total_deliveries": { + "type": "integer", + "description": "Total deliveries in the time range" + }, + "sampled_count": { + "type": "integer", + "description": "Number of artifacts in this response" + }, + "effective_rate": { + "type": "number", + "description": "Actual sampling rate achieved" + }, + "method": { + "type": "string", + "enum": ["random", "stratified", "recent", "failures_only"], + "description": "Sampling method used" + } + } + }, + "pagination": { + "type": "object", + "description": "Pagination information for large result sets", + "properties": { + "cursor": { + "type": "string", + "description": "Cursor for fetching the next page" + }, + "has_more": { + "type": "boolean", + "description": "Whether more results are available" + } + } + }, + "errors": { + "not": {}, + "description": "Field must not be present in success response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["media_buy_id", "artifacts"] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { "$ref": "/schemas/core/error.json" } + }, + "media_buy_id": { + "not": {}, + "description": "Field must not be present in error response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"] + } + ] +} diff --git a/static/schemas/source/content-standards/list-content-features-request.json b/static/schemas/source/content-standards/list-content-features-request.json new file mode 100644 index 00000000..07d4b67f --- /dev/null +++ b/static/schemas/source/content-standards/list-content-features-request.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/list-content-features-request.json", + "title": "List Content Features Request", + "description": "Request parameters for discovering features the agent can evaluate", + "type": "object", + "properties": { + "standards_id": { + "type": "string", + "description": "Filter features available for a specific standards configuration" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + } +} diff --git a/static/schemas/source/content-standards/list-content-features-response.json b/static/schemas/source/content-standards/list-content-features-response.json new file mode 100644 index 00000000..ca859636 --- /dev/null +++ b/static/schemas/source/content-standards/list-content-features-response.json @@ -0,0 +1,106 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/list-content-features-response.json", + "title": "List Content Features Response", + "description": "Response payload with features and their types", + "type": "object", + "oneOf": [ + { + "type": "object", + "description": "Success response", + "properties": { + "features": { + "type": "array", + "description": "Available features for content evaluation", + "items": { + "type": "object", + "properties": { + "feature_id": { + "type": "string", + "description": "Unique identifier for this feature" + }, + "name": { + "type": "string", + "description": "Human-readable feature name" + }, + "description": { + "type": "string", + "description": "What this feature evaluates" + }, + "type": { + "type": "string", + "enum": ["binary", "quantitative", "categorical"], + "description": "Feature type" + }, + "range": { + "type": "object", + "description": "For quantitative features, the valid range", + "properties": { + "min": { "type": "number" }, + "max": { "type": "number" } + } + }, + "allowed_values": { + "type": "array", + "description": "For categorical features, the allowed values", + "items": { "type": "string" } + }, + "severity": { + "type": "string", + "enum": ["block", "warn", "info"], + "description": "Default severity when this feature fails" + }, + "coverage": { + "type": "object", + "description": "Geographic coverage for regional features", + "properties": { + "regions": { + "type": "array", + "items": { "type": "string" } + }, + "markets": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + "required": ["feature_id", "name", "type"] + } + }, + "errors": { + "not": {}, + "description": "Field must not be present in success response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["features"] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { "$ref": "/schemas/core/error.json" } + }, + "features": { + "not": {}, + "description": "Field must not be present in error response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"] + } + ] +} diff --git a/static/schemas/source/content-standards/validate-content-delivery-request.json b/static/schemas/source/content-standards/validate-content-delivery-request.json new file mode 100644 index 00000000..4e35137b --- /dev/null +++ b/static/schemas/source/content-standards/validate-content-delivery-request.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/validate-content-delivery-request.json", + "title": "Validate Content Delivery Request", + "description": "Request parameters for batch validating delivery records against content safety policies", + "type": "object", + "properties": { + "standards_id": { + "type": "string", + "description": "Standards configuration to validate against" + }, + "records": { + "type": "array", + "description": "Delivery records to validate (max 10,000)", + "maxItems": 10000, + "items": { + "type": "object", + "properties": { + "record_id": { + "type": "string", + "description": "Unique identifier for this delivery record" + }, + "media_buy_id": { + "type": "string", + "description": "Media buy this record belongs to (when batching across multiple buys)" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the delivery occurred" + }, + "artifact": { + "$ref": "/schemas/content-standards/artifact.json", + "description": "Artifact where ad was delivered" + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code where delivery occurred" + }, + "channel": { + "type": "string", + "description": "Channel type (e.g., display, video, audio, social)" + }, + "brand_context": { + "type": "object", + "description": "Brand information for policy evaluation. Schema TBD - placeholder for brand identifiers.", + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier" + }, + "sku_id": { + "type": "string", + "description": "Product/SKU identifier if applicable" + } + } + } + }, + "required": ["record_id", "artifact"] + } + }, + "feature_ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Specific features to evaluate (defaults to all)" + }, + "include_passed": { + "type": "boolean", + "default": true, + "description": "Include passed records in results" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["standards_id", "records"] +} diff --git a/static/schemas/source/content-standards/validate-content-delivery-response.json b/static/schemas/source/content-standards/validate-content-delivery-response.json new file mode 100644 index 00000000..1d1feeb1 --- /dev/null +++ b/static/schemas/source/content-standards/validate-content-delivery-response.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/content-standards/validate-content-delivery-response.json", + "title": "Validate Content Delivery Response", + "description": "Response payload with per-record verdicts and optional feature breakdown", + "type": "object", + "oneOf": [ + { + "type": "object", + "description": "Success response", + "properties": { + "summary": { + "type": "object", + "description": "Summary counts across all records", + "properties": { + "total_records": { "type": "integer" }, + "passed_records": { "type": "integer" }, + "failed_records": { "type": "integer" } + }, + "required": ["total_records", "passed_records", "failed_records"] + }, + "results": { + "type": "array", + "description": "Per-record evaluation results", + "items": { + "type": "object", + "properties": { + "record_id": { + "type": "string", + "description": "Which delivery record was evaluated" + }, + "verdict": { + "type": "string", + "enum": ["pass", "fail"], + "description": "Overall pass/fail verdict for this record" + }, + "features": { + "type": "array", + "description": "Optional feature-level breakdown", + "items": { + "type": "object", + "properties": { + "feature_id": { "type": "string" }, + "status": { + "type": "string", + "enum": ["passed", "failed", "warning", "unevaluated"] + }, + "value": {}, + "message": { "type": "string" }, + "rule_id": { + "type": "string", + "description": "Which rule triggered this result (e.g., GARM category, Scope3 standard)" + } + }, + "required": ["feature_id", "status"] + } + } + }, + "required": ["record_id", "verdict"] + } + }, + "errors": { + "not": {}, + "description": "Field must not be present in success response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["summary", "results"] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { "$ref": "/schemas/core/error.json" } + }, + "summary": { + "not": {}, + "description": "Field must not be present in error response" + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"] + } + ] +} diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json index 635683ac..ef6fb30f 100644 --- a/static/schemas/source/index.json +++ b/static/schemas/source/index.json @@ -571,7 +571,7 @@ } }, "governance": { - "description": "Governance protocol for property governance, brand standards, and compliance", + "description": "Governance protocol for property governance, brand standards, content standards, and compliance", "supporting-schemas": { "property-feature-definition": { "$ref": "/schemas/property/property-feature-definition.json", @@ -604,6 +604,10 @@ "base-property-source": { "$ref": "/schemas/property/base-property-source.json", "description": "A source of properties for a property list - supports publisher+tags, publisher+property_ids, or direct identifiers" + }, + "content-standards-artifact": { + "$ref": "/schemas/content-standards/artifact.json", + "description": "Content artifact for evaluation or calibration - represents content context where ad placements occur, identified by property_id + artifact_id" } }, "tasks": { @@ -666,6 +670,56 @@ "$ref": "/schemas/property/delete-property-list-response.json", "description": "Response payload for delete_property_list task" } + }, + "list-content-features": { + "request": { + "$ref": "/schemas/content-standards/list-content-features-request.json", + "description": "Request parameters for discovering available content safety features" + }, + "response": { + "$ref": "/schemas/content-standards/list-content-features-response.json", + "description": "Response payload with available content features" + } + }, + "get-content-standards": { + "request": { + "$ref": "/schemas/content-standards/get-content-standards-request.json", + "description": "Request parameters for retrieving content safety policies" + }, + "response": { + "$ref": "/schemas/content-standards/get-content-standards-response.json", + "description": "Response payload with content safety policies" + } + }, + "calibrate-content": { + "request": { + "$ref": "/schemas/content-standards/calibrate-content-request.json", + "description": "Request parameters for collaborative calibration dialogue" + }, + "response": { + "$ref": "/schemas/content-standards/calibrate-content-response.json", + "description": "Response payload with detailed explanations for policy alignment" + } + }, + "validate-content-delivery": { + "request": { + "$ref": "/schemas/content-standards/validate-content-delivery-request.json", + "description": "Request parameters for batch validating delivery records" + }, + "response": { + "$ref": "/schemas/content-standards/validate-content-delivery-response.json", + "description": "Response payload with batch validation results" + } + }, + "get-media-buy-artifacts": { + "request": { + "$ref": "/schemas/content-standards/get-media-buy-artifacts-request.json", + "description": "Request parameters for retrieving content artifacts from a media buy" + }, + "response": { + "$ref": "/schemas/content-standards/get-media-buy-artifacts-response.json", + "description": "Response payload with content artifacts for validation" + } } } }, diff --git a/tests/schema-validation.test.cjs b/tests/schema-validation.test.cjs index 2d4b0a71..40727fc8 100644 --- a/tests/schema-validation.test.cjs +++ b/tests/schema-validation.test.cjs @@ -130,33 +130,38 @@ function validateSchemaStructure(schemaPath, schema) { function validateCrossReferences(schemas) { const schemaIds = new Set(schemas.map(([_, schema]) => schema.$id)); const missingRefs = []; - + for (const [schemaPath, schema] of schemas) { // Find all $ref occurrences const refs = JSON.stringify(schema).match(/"\$ref":\s*"([^"]+)"/g) || []; - + for (const refMatch of refs) { const ref = refMatch.match(/"\$ref":\s*"([^"]+)"/)[1]; - + // Skip external references (http://, https://) if (ref.startsWith('http://') || ref.startsWith('https://')) { continue; } - + + // Skip internal references (#/$defs/..., #/properties/..., etc.) + if (ref.startsWith('#/')) { + continue; + } + // Check if referenced schema exists if (!schemaIds.has(ref)) { missingRefs.push({ schema: schemaPath, ref }); } } } - + if (missingRefs.length > 0) { - const errorMsg = missingRefs.map(({ schema, ref }) => + const errorMsg = missingRefs.map(({ schema, ref }) => `${path.basename(schema)} -> ${ref}` ).join(', '); return `Missing referenced schemas: ${errorMsg}`; } - + return true; }