From 4e983dc13455cf8e2d7e119cc3f2e09d18143236 Mon Sep 17 00:00:00 2001 From: ATC964 <82515918+atc964@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:16:06 -0400 Subject: [PATCH 1/2] feat: conform name bounds to OpenDirect 2.1, add negotiation sender identity, add CUR curated-package deal type - Product.name max length 128 -> 100, Organization.name and Account.name max length 128 -> 120, matching the OpenDirect 2.1 spec. Bounds already at spec (Creative.name 255, lifecycle name fields at 200) are untouched. - NegotiationMessage gains optional agent_url: the sender's registered A2A agent URL, for registry trust-tier verification. Optional for backward compatibility. - DealType gains CUR: curated package deal, floor-priced and bid-based, bundling curated inventory under one deal ID via an SSP-native curation product (Index Inventory/Auction Packages, PubMatic Auction Packages, Magnite Curate). Distinct from PA (private auction, the general private-marketplace tier). Named CUR rather than reusing the industry term "PMP" on the wire, since bare "PMP" is ambiguous and commonly refers to the PA tier instead; seller-internal "PMP curated deals" terminology maps to CUR here. The docstring spells out the disambiguation. Regenerated spec/jsonschema and spec/fixtures golden vectors from the models via spec/generate_schemas.py and spec/jsonschema/protocol/generate.py, plus python -m iab_agentic_primitives.conformance --regenerate. --- CHANGELOG.md | 25 ++++ CONFORMANCE.md | 5 +- .../protocol/NegotiationMessage.golden.json | 5 + spec/jsonschema/Account.json | 3 +- spec/jsonschema/Agent.json | 2 +- spec/jsonschema/Deal.json | 5 +- spec/jsonschema/Organization.json | 3 +- spec/jsonschema/Product.json | 10 +- spec/jsonschema/Proposal.json | 5 +- spec/jsonschema/Quote.json | 5 +- .../protocol/DealBookingResponse.json | 5 +- .../protocol/NegotiationMessage.json | 13 ++ .../protocol/ProductListResponse.json | 10 +- spec/jsonschema/protocol/QuoteRequest.json | 7 +- spec/jsonschema/protocol/QuoteResponse.json | 5 +- .../conformance/vectors.py | 8 +- .../primitives/identity.py | 9 +- .../primitives/inventory.py | 5 +- .../primitives/pricing.py | 17 ++- .../protocol/negotiation.py | 6 + src/iab_agentic_primitives/protocol/quotes.py | 4 +- tests/test_enum_unification.py | 33 ++++- tests/test_name_bounds.py | 118 ++++++++++++++++++ tests/test_protocol.py | 21 ++++ 24 files changed, 289 insertions(+), 40 deletions(-) create mode 100644 tests/test_name_bounds.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cbff7f4..0da3c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ major version bump. ## [Unreleased] +### Changed + +- **Breaking:** name-length bounds conformed to the OpenDirect 2.1 spec + (the IAB direct-buying API standard): `Product.name` max length 128 -> + 100; `Organization.name` and `Account.name` max length 128 -> 120. + Bounds already matching spec (`Creative.name` 255, the lifecycle `name` + fields at 200) are unchanged. Breaking for any integrator currently + sending names in the 101-128 (Product) or 121-128 (Organization/ + Account) range. + +### Added + +- `NegotiationMessage.agent_url` (optional): the sender's registered A2A + agent URL, for registry trust-tier verification. Optional for backward + compatibility with callers that predate this field. +- `DealType.CUR`: curated package deal (floor-priced, bid-based, curated + inventory bundled under one deal ID via an SSP-native curation + product — e.g. Index Inventory/Auction Packages, PubMatic Auction + Packages, Magnite Curate). Distinct from `PA` (private auction, the + general private-marketplace tier); named `CUR` rather than "PMP" + because bare "PMP" is ambiguous in industry usage and commonly refers + to the `PA` tier instead. Seller-internal "PMP curated deals" map to + `CUR` on the wire. See the `DealType` docstring for the full + disambiguation. + ## [0.5.1] - 2026-08-10 First tagged release published on GitHub. diff --git a/CONFORMANCE.md b/CONFORMANCE.md index 516336b..bdb92b2 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -10,8 +10,9 @@ generator). Acronyms used below (defined on first use): IAB = Interactive Advertising Bureau; JSON = JavaScript Object Notation; FD = flagged decision (the -remediation plan's decision register); PG / PD / PA = Programmatic -Guaranteed / Preferred Deal / Private Auction; GRP = gross rating point; +remediation plan's decision register); PG / PD / PA / CUR = Programmatic +Guaranteed / Preferred Deal / Private Auction / curated package deal; +GRP = gross rating point; CPM / CPP = cost per mille / cost per point; A2A = Agent-to-Agent protocol; JSON-RPC = JSON Remote Procedure Call; AAMP = the IAB Tech Lab agent discovery and trust registry; AdCOM = Advertising Common Object diff --git a/spec/fixtures/protocol/NegotiationMessage.golden.json b/spec/fixtures/protocol/NegotiationMessage.golden.json index e4c4af1..97a61a8 100644 --- a/spec/fixtures/protocol/NegotiationMessage.golden.json +++ b/spec/fixtures/protocol/NegotiationMessage.golden.json @@ -6,6 +6,7 @@ { "data": { "action": "counter", + "agent_url": null, "buyer_identity": { "advertiser_id": null, "advertiser_industry": null, @@ -37,6 +38,7 @@ { "data": { "action": "accept", + "agent_url": null, "buyer_identity": null, "buyer_price": { "amount_micros": 22000000, @@ -56,6 +58,7 @@ { "data": { "action": "reject", + "agent_url": null, "buyer_identity": null, "buyer_price": null, "idempotency_key": "00000000-0000-4000-8000-000000000012", @@ -72,6 +75,7 @@ { "data": { "action": "counter", + "agent_url": null, "buyer_identity": { "advertiser_id": null, "advertiser_industry": null, @@ -104,6 +108,7 @@ "description": "FD-13: unknown + x_ fields on a negotiation message are ignored", "expected": { "action": "counter", + "agent_url": null, "buyer_identity": { "advertiser_id": null, "advertiser_industry": null, diff --git a/spec/jsonschema/Account.json b/spec/jsonschema/Account.json index 2ecde23..2bed0e9 100644 --- a/spec/jsonschema/Account.json +++ b/spec/jsonschema/Account.json @@ -51,7 +51,7 @@ "name": { "anyOf": [ { - "maxLength": 128, + "maxLength": 120, "type": "string" }, { @@ -59,6 +59,7 @@ } ], "default": null, + "description": "OpenDirect 2.1 identity name bound (120).", "title": "Name" }, "seller_organization_id": { diff --git a/spec/jsonschema/Agent.json b/spec/jsonschema/Agent.json index 2f8be98..321210d 100644 --- a/spec/jsonschema/Agent.json +++ b/spec/jsonschema/Agent.json @@ -226,7 +226,7 @@ "type": "array" }, "supported_deal_types": { - "description": "DealType wire values the agent supports: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction).", + "description": "DealType wire values the agent supports: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal).", "items": { "type": "string" }, diff --git a/spec/jsonschema/Deal.json b/spec/jsonschema/Deal.json index 5632047..e5e60e3 100644 --- a/spec/jsonschema/Deal.json +++ b/spec/jsonschema/Deal.json @@ -168,11 +168,12 @@ "type": "string" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/Organization.json b/spec/jsonschema/Organization.json index ebc37b1..1145586 100644 --- a/spec/jsonschema/Organization.json +++ b/spec/jsonschema/Organization.json @@ -67,7 +67,8 @@ "title": "Ext" }, "name": { - "maxLength": 128, + "description": "OpenDirect 2.1 identity name bound (120).", + "maxLength": 120, "title": "Name", "type": "string" }, diff --git a/spec/jsonschema/Product.json b/spec/jsonschema/Product.json index ebf719b..803028d 100644 --- a/spec/jsonschema/Product.json +++ b/spec/jsonschema/Product.json @@ -39,7 +39,7 @@ "default": null }, "supported_deal_types": { - "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction).", + "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal).", "items": { "$ref": "#/$defs/DealType" }, @@ -58,11 +58,12 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" @@ -258,7 +259,8 @@ "title": "Ext" }, "name": { - "maxLength": 128, + "description": "OpenDirect 2.1 Product name bound (100).", + "maxLength": 100, "title": "Name", "type": "string" }, diff --git a/spec/jsonschema/Proposal.json b/spec/jsonschema/Proposal.json index a2291b3..8437fda 100644 --- a/spec/jsonschema/Proposal.json +++ b/spec/jsonschema/Proposal.json @@ -12,11 +12,12 @@ "type": "string" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/Quote.json b/spec/jsonschema/Quote.json index 0c054b5..e3cb80f 100644 --- a/spec/jsonschema/Quote.json +++ b/spec/jsonschema/Quote.json @@ -149,11 +149,12 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/protocol/DealBookingResponse.json b/spec/jsonschema/protocol/DealBookingResponse.json index 0f24b28..9dc9d12 100644 --- a/spec/jsonschema/protocol/DealBookingResponse.json +++ b/spec/jsonschema/protocol/DealBookingResponse.json @@ -323,11 +323,12 @@ "type": "string" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/protocol/NegotiationMessage.json b/spec/jsonschema/protocol/NegotiationMessage.json index ba3ddfc..b52d233 100644 --- a/spec/jsonschema/protocol/NegotiationMessage.json +++ b/spec/jsonschema/protocol/NegotiationMessage.json @@ -183,6 +183,19 @@ "$ref": "#/$defs/NegotiationAction", "description": "REQUIRED move discriminator: 'accept', 'counter', 'reject' (walk-away), or 'final_offer'. No default \u2014 the action-less bare-price payload that caused the historical 422 does not validate." }, + "agent_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A2A endpoint of the sending agent, for registry trust verification (see AgentTrustVerification). Optional for backward compatibility with callers that predate this field.", + "title": "Agent Url" + }, "buyer_identity": { "anyOf": [ { diff --git a/spec/jsonschema/protocol/ProductListResponse.json b/spec/jsonschema/protocol/ProductListResponse.json index 90bd864..5b1c077 100644 --- a/spec/jsonschema/protocol/ProductListResponse.json +++ b/spec/jsonschema/protocol/ProductListResponse.json @@ -39,7 +39,7 @@ "default": null }, "supported_deal_types": { - "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction).", + "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal).", "items": { "$ref": "#/$defs/DealType" }, @@ -58,11 +58,12 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" @@ -258,7 +259,8 @@ "title": "Ext" }, "name": { - "maxLength": 128, + "description": "OpenDirect 2.1 Product name bound (100).", + "maxLength": 100, "title": "Name", "type": "string" }, diff --git a/spec/jsonschema/protocol/QuoteRequest.json b/spec/jsonschema/protocol/QuoteRequest.json index 28e0209..d1c2d21 100644 --- a/spec/jsonschema/protocol/QuoteRequest.json +++ b/spec/jsonschema/protocol/QuoteRequest.json @@ -238,11 +238,12 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" @@ -452,7 +453,7 @@ }, "deal_type": { "$ref": "#/$defs/DealType", - "description": "'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction). Typed \u2014 the retired long-form strings are not valid wire values." + "description": "'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal). Typed \u2014 the retired long-form strings are not valid wire values." }, "flight_end": { "anyOf": [ diff --git a/spec/jsonschema/protocol/QuoteResponse.json b/spec/jsonschema/protocol/QuoteResponse.json index ac0b051..674fb49 100644 --- a/spec/jsonschema/protocol/QuoteResponse.json +++ b/spec/jsonschema/protocol/QuoteResponse.json @@ -149,11 +149,12 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA" + "PA", + "CUR" ], "title": "DealType", "type": "string" diff --git a/src/iab_agentic_primitives/conformance/vectors.py b/src/iab_agentic_primitives/conformance/vectors.py index 8a3681f..6d55e13 100644 --- a/src/iab_agentic_primitives/conformance/vectors.py +++ b/src/iab_agentic_primitives/conformance/vectors.py @@ -23,10 +23,10 @@ Determinism: every timestamp and identifier is pinned — regeneration is byte-stable, so ``--regenerate`` never produces noise diffs. -Acronyms: FD = flagged decision; PG/PD/PA = Programmatic Guaranteed / -Preferred Deal / Private Auction; GRP = gross rating point; CPM/CPP = -cost per mille / cost per point; GPP = Global Privacy Platform; -TCF = Transparency & Consent Framework. +Acronyms: FD = flagged decision; PG/PD/PA/CUR = Programmatic Guaranteed / +Preferred Deal / Private Auction / curated package deal; GRP = gross +rating point; CPM/CPP = cost per mille / cost per point; GPP = Global +Privacy Platform; TCF = Transparency & Consent Framework. """ from __future__ import annotations diff --git a/src/iab_agentic_primitives/primitives/identity.py b/src/iab_agentic_primitives/primitives/identity.py index a33e4d6..1633b5d 100644 --- a/src/iab_agentic_primitives/primitives/identity.py +++ b/src/iab_agentic_primitives/primitives/identity.py @@ -56,7 +56,7 @@ class Organization(WireModel): """ organization_id: str = Field(description="Registry-issued organization identifier.") - name: str = Field(max_length=128) + name: str = Field(max_length=120, description="OpenDirect 2.1 identity name bound (120).") role: OrganizationRole status: OrganizationStatus = OrganizationStatus.ACTIVE address: str | None = None @@ -96,7 +96,9 @@ class Account(WireModel): default=None, description="Optional advertiser this account transacts for (buyer-supplied).", ) - name: str | None = Field(default=None, max_length=128) + name: str | None = Field( + default=None, max_length=120, description="OpenDirect 2.1 identity name bound (120)." + ) status: AccountStatus = AccountStatus.ACTIVE ext: dict[str, Any] | None = None @@ -240,7 +242,8 @@ class Agent(WireModel): default_factory=list, description=( "DealType wire values the agent supports: 'PG' (Programmatic " - "Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction)." + "Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), " + "'CUR' (curated package deal)." ), ) trust_status: TrustStatus = Field( diff --git a/src/iab_agentic_primitives/primitives/inventory.py b/src/iab_agentic_primitives/primitives/inventory.py index 6fb489b..6f31494 100644 --- a/src/iab_agentic_primitives/primitives/inventory.py +++ b/src/iab_agentic_primitives/primitives/inventory.py @@ -37,7 +37,8 @@ class CommercialTerms(WireModel): supported_deal_types: list[DealType] = Field( default_factory=list, description="Supported deal types: 'PG' (Programmatic Guaranteed), " - "'PD' (Preferred Deal), 'PA' (Private Auction).", + "'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated " + "package deal).", ) supported_pricing_models: list[PricingModel] = Field(default_factory=list) minimum_deal_value: Money | None = None @@ -65,7 +66,7 @@ class Product(WireModel): seller_organization_id: str = Field( description="Registry-issued id of the owning seller organization." ) - name: str = Field(max_length=128) + name: str = Field(max_length=100, description="OpenDirect 2.1 Product name bound (100).") description: str | None = None base_price: Money | None = Field( default=None, diff --git a/src/iab_agentic_primitives/primitives/pricing.py b/src/iab_agentic_primitives/primitives/pricing.py index 41fc7e2..42f81cb 100644 --- a/src/iab_agentic_primitives/primitives/pricing.py +++ b/src/iab_agentic_primitives/primitives/pricing.py @@ -26,7 +26,21 @@ class DealType(str, Enum): - ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions - ``PD`` = Preferred Deal: fixed price, non-guaranteed first look - - ``PA`` = Private Auction: auction with floor price, invited buyers + - ``PA`` = Private Auction: auction with floor price, invited buyers. + This is the general private-marketplace tier — bare "PMP" in + industry usage most often means this. + - ``CUR`` = curated package deal: a floor-priced, bid-based deal that + bundles curated inventory (single- or multi-publisher) under one + deal ID via an SSP-native (supply-side platform) curation product — + e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction + Packages, Magnite Curate. Mechanically closest to ``PA`` (floor + + competitive bid) but a commercially distinct, separately sold + product, so it is kept as a sibling value rather than folded into + ``PA``. Named ``CUR`` rather than "PMP" specifically because + bare "PMP" is ambiguous in industry usage — it is also the term + commonly used for the ``PA`` private-auction tier — so this value + deliberately avoids reusing that string on the wire. Seller-internal + "PMP curated deals" terminology maps to ``CUR`` here, not to ``PA``. Mapping from the seller repo's retired long-form encoding (``models/core.py``): ``programmaticguaranteed`` -> ``PG``, @@ -37,6 +51,7 @@ class DealType(str, Enum): PROGRAMMATIC_GUARANTEED = "PG" PREFERRED_DEAL = "PD" PRIVATE_AUCTION = "PA" + CURATED_PACKAGE = "CUR" # --------------------------------------------------------------------------- diff --git a/src/iab_agentic_primitives/protocol/negotiation.py b/src/iab_agentic_primitives/protocol/negotiation.py index 4f1a79d..7e7d968 100644 --- a/src/iab_agentic_primitives/protocol/negotiation.py +++ b/src/iab_agentic_primitives/protocol/negotiation.py @@ -112,6 +112,12 @@ class NegotiationMessage(IdempotentRequest): "omitted on 'reject'.", ) buyer_identity: BuyerIdentity | None = None + agent_url: str | None = Field( + default=None, + description="A2A endpoint of the sending agent, for registry trust " + "verification (see AgentTrustVerification). Optional for backward " + "compatibility with callers that predate this field.", + ) rationale: str = Field(default="", description="Optional human-readable rationale.") @model_validator(mode="after") diff --git a/src/iab_agentic_primitives/protocol/quotes.py b/src/iab_agentic_primitives/protocol/quotes.py index 0eba83f..2045f7e 100644 --- a/src/iab_agentic_primitives/protocol/quotes.py +++ b/src/iab_agentic_primitives/protocol/quotes.py @@ -54,8 +54,8 @@ class QuoteRequest(IdempotentRequest): product_id: str = Field(description="Seller-issued product to quote.") deal_type: DealType = Field( description="'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), " - "'PA' (Private Auction). Typed — the retired long-form strings are " - "not valid wire values." + "'PA' (Private Auction), 'CUR' (curated package deal). Typed — the " + "retired long-form strings are not valid wire values." ) impressions: int | None = Field( default=None, ge=0, description="Requested volume; required for PG." diff --git a/tests/test_enum_unification.py b/tests/test_enum_unification.py index c08bedc..2b3ed47 100644 --- a/tests/test_enum_unification.py +++ b/tests/test_enum_unification.py @@ -1,6 +1,7 @@ """Enum-unification tests: one vocabulary per concept, wire values canonical. -PG = Programmatic Guaranteed, PD = Preferred Deal, PA = Private Auction. +PG = Programmatic Guaranteed, PD = Preferred Deal, PA = Private Auction, +CUR = curated package deal. """ import pytest @@ -22,7 +23,7 @@ def test_dealtype_wire_values() -> None: assert DealType("PG") is DealType.PROGRAMMATIC_GUARANTEED assert DealType("PD") is DealType.PREFERRED_DEAL assert DealType("PA") is DealType.PRIVATE_AUCTION - assert {m.value for m in DealType} == {"PG", "PD", "PA"} + assert {m.value for m in DealType} == {"PG", "PD", "PA", "CUR"} @pytest.mark.parametrize( @@ -47,6 +48,34 @@ def test_dealtype_serializes_to_wire_value() -> None: assert '"deal_type":"PG"' in quote.model_dump_json() +def test_dealtype_cur_roundtrips() -> None: + """CUR (curated package deal) serializes/deserializes like any other member.""" + from iab_agentic_primitives.primitives import ProductRef, Quote, QuotePricing, QuoteTerms + + assert DealType("CUR") is DealType.CURATED_PACKAGE + + quote = Quote( + quote_id="q-2", + deal_type=DealType.CURATED_PACKAGE, + product=ProductRef(product_id="p-1", name="x"), + pricing=QuotePricing(), + terms=QuoteTerms(), + ) + dumped = quote.model_dump_json() + assert '"deal_type":"CUR"' in dumped + assert Quote.model_validate_json(dumped).deal_type is DealType.CURATED_PACKAGE + + +def test_dealtype_cur_documented_distinctly_from_pa() -> None: + """The docstring must disambiguate CUR (curated package) from PA (private auction), + and explain why the wire value is CUR rather than the industry's ambiguous "PMP".""" + doc = DealType.__doc__ or "" + assert "CUR" in doc + assert "curated" in doc.lower() + assert "ambiguous" in doc.lower() + assert "PMP" in doc # still referenced, as the term the ambiguity comes from + + # --------------------------------------------------------------------------- # DealStatus: ONE unioned vocabulary; retired aliases absent # --------------------------------------------------------------------------- diff --git a/tests/test_name_bounds.py b/tests/test_name_bounds.py new file mode 100644 index 0000000..7dd1738 --- /dev/null +++ b/tests/test_name_bounds.py @@ -0,0 +1,118 @@ +"""Name-length bounds conformed to the OpenDirect 2.1 spec. + +OpenDirect = the IAB (Interactive Advertising Bureau) direct-buying API +standard. ``Product.name`` (and the equivalent identity-adjacent names on +``Organization``/``Account``) previously used a uniform 128-char bound +inherited from neither source repo's actual spec grounding; this brings +them in line with OpenDirect 2.1: Product name 100, Organization/Account +name 120. Bounds that already matched spec (``Creative.name`` 255, +the lifecycle ``name`` fields at 200) are deliberately untouched. +""" + +from datetime import date + +import pytest +from pydantic import ValidationError + +from iab_agentic_primitives.primitives import ( + Account, + Creative, + Money, + Organization, + OrganizationRole, + Product, +) +from iab_agentic_primitives.primitives.lifecycle import Line, Order + +# --------------------------------------------------------------------------- +# Product.name: 128 -> 100 +# --------------------------------------------------------------------------- + + +def _product(name: str) -> Product: + return Product(product_id="p-1", seller_organization_id="org-1", name=name) + + +def test_product_name_100_chars_accepted() -> None: + assert _product("x" * 100).name == "x" * 100 + + +def test_product_name_101_chars_rejected() -> None: + with pytest.raises(ValidationError): + _product("x" * 101) + + +# --------------------------------------------------------------------------- +# Organization.name: 128 -> 120 +# --------------------------------------------------------------------------- + + +def _organization(name: str) -> Organization: + return Organization(organization_id="org-1", name=name, role=OrganizationRole.SELLER) + + +def test_organization_name_120_chars_accepted() -> None: + assert _organization("x" * 120).name == "x" * 120 + + +def test_organization_name_121_chars_rejected() -> None: + with pytest.raises(ValidationError): + _organization("x" * 121) + + +# --------------------------------------------------------------------------- +# Account.name: 128 -> 120 +# --------------------------------------------------------------------------- + + +def _account(name: str) -> Account: + return Account( + account_id="acct-1", + buyer_organization_id="org-buyer", + seller_organization_id="org-seller", + name=name, + ) + + +def test_account_name_120_chars_accepted() -> None: + assert _account("x" * 120).name == "x" * 120 + + +def test_account_name_121_chars_rejected() -> None: + with pytest.raises(ValidationError): + _account("x" * 121) + + +# --------------------------------------------------------------------------- +# Untouched bounds: creative 255, lifecycle 200 — regression guards only +# --------------------------------------------------------------------------- + + +def test_creative_name_255_chars_still_accepted() -> None: + assert Creative(creative_id="cr-1", name="x" * 255).name == "x" * 255 + with pytest.raises(ValidationError): + Creative(creative_id="cr-2", name="x" * 256) + + +def test_lifecycle_name_200_chars_still_accepted() -> None: + order_kwargs = dict( + account_id="acct-1", + budget=Money.from_decimal_str("1000.00"), + start_date=date(2026, 1, 1), + end_date=date(2026, 12, 31), + ) + assert Order(order_id="o-1", name="x" * 200, **order_kwargs).name == "x" * 200 + with pytest.raises(ValidationError): + Order(order_id="o-2", name="x" * 201, **order_kwargs) + + line_kwargs = dict( + order_id="o-1", + product_id="p-1", + start_date=date(2026, 1, 1), + end_date=date(2026, 12, 31), + rate=Money.from_decimal_str("10.00"), + quantity=1000, + ) + assert Line(line_id="l-1", name="x" * 200, **line_kwargs).name == "x" * 200 + with pytest.raises(ValidationError): + Line(line_id="l-2", name="x" * 201, **line_kwargs) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index c3e81ea..fb1dcdc 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -295,6 +295,27 @@ def test_buyer_counter_validates_against_the_sellers_model() -> None: assert received.buyer_price == Money.from_decimal_str("21.00") +def test_agent_url_is_optional_and_roundtrips() -> None: + """agent_url (sender identity, for tier verification) is optional for + backward compatibility, and round-trips when supplied.""" + without_url = build_negotiation_message() + assert without_url.agent_url is None + reparsed = NegotiationMessage.model_validate_json(without_url.model_dump_json()) + assert reparsed.agent_url is None + + with_url = NegotiationMessage( + idempotency_key="idem-neg-2", + action=NegotiationAction.COUNTER, + negotiation_id="neg-001", + buyer_price=Money.from_decimal_str("21.00"), + agent_url="https://buyer.example.com/agents/acme", + ) + dumped = with_url.model_dump_json() + assert '"agent_url":"https://buyer.example.com/agents/acme"' in dumped + reparsed_with_url = NegotiationMessage.model_validate_json(dumped) + assert reparsed_with_url.agent_url == "https://buyer.example.com/agents/acme" + + def test_legacy_bare_price_payload_is_rejected() -> None: """The buyer's historical ``{"price": 22.0}`` body cannot validate: no action, no typed buyer_price, no negotiation context.""" From a4a523828ebbb55b3474f064133669ba446bcff8 Mon Sep 17 00:00:00 2001 From: ATC964 <82515918+atc964@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:17:37 -0400 Subject: [PATCH 2/2] feat: represent curated packages via an IAB-aligned Curation object, not a deal type - DealType reverts to PG/PD/PA only. Its docstring now states explicitly that curated packages are NOT a fourth deal type: pricing mechanic and packaging structure are orthogonal, and a curated deal can accompany any DealType (typically PA-mechanics, but can be PG). Both CUR and the industry term "PMP" were considered as wire values for a fourth deal type and rejected, in favor of aligning with the published IAB Deals API v1.0 model, which keeps the two concerns separate. - Add Curation (and CurationFeeType) to primitives.pricing, modeling the IAB Deals API v1.0 Curation object (deal-api spec, deal1.0.md): curator (canonical domain of the packaging entity), curator_deal_id (the curator's own deal id, spec cdealid), curation_fee_type (spec curfeetype: 0=undisclosed, 1=percent of spend, 2=flat fee, 3=CPM, 4=no fee), and an ext slot. All fields optional, matching the published spec's own all-optional Curation object. - Deal gains an optional curation field. DealBookingResponse wraps Deal directly, so it picks up curation automatically with no separate field. Regenerated spec/jsonschema and spec/fixtures golden vectors via spec/generate_schemas.py and spec/jsonschema/protocol/generate.py plus python -m iab_agentic_primitives.conformance --regenerate, and refreshed STANDARDS_GAP_REPORT.md (the iab-deals-api-1.0 standards entry now notes Curation is modeled, still unverified against the published examples). --- CHANGELOG.md | 20 ++-- CONFORMANCE.md | 5 +- STANDARDS_GAP_REPORT.md | 2 +- spec/fixtures/primitives/Deal.golden.json | 6 + .../protocol/DealBookingResponse.golden.json | 2 + spec/jsonschema/Agent.json | 2 +- spec/jsonschema/Deal.json | 88 +++++++++++++- spec/jsonschema/Product.json | 7 +- spec/jsonschema/Proposal.json | 5 +- spec/jsonschema/Quote.json | 5 +- .../protocol/DealBookingResponse.json | 88 +++++++++++++- .../protocol/ProductListResponse.json | 7 +- spec/jsonschema/protocol/QuoteRequest.json | 7 +- spec/jsonschema/protocol/QuoteResponse.json | 5 +- .../conformance/standards.py | 6 +- .../conformance/vectors.py | 8 +- .../primitives/__init__.py | 6 +- .../primitives/identity.py | 3 +- .../primitives/inventory.py | 3 +- .../primitives/lifecycle.py | 7 ++ .../primitives/pricing.py | 87 ++++++++++++-- src/iab_agentic_primitives/protocol/quotes.py | 4 +- tests/test_curation.py | 113 ++++++++++++++++++ tests/test_enum_unification.py | 37 ++---- 24 files changed, 431 insertions(+), 92 deletions(-) create mode 100644 tests/test_curation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da3c8a..2b93642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,14 +21,18 @@ major version bump. - `NegotiationMessage.agent_url` (optional): the sender's registered A2A agent URL, for registry trust-tier verification. Optional for backward compatibility with callers that predate this field. -- `DealType.CUR`: curated package deal (floor-priced, bid-based, curated - inventory bundled under one deal ID via an SSP-native curation - product — e.g. Index Inventory/Auction Packages, PubMatic Auction - Packages, Magnite Curate). Distinct from `PA` (private auction, the - general private-marketplace tier); named `CUR` rather than "PMP" - because bare "PMP" is ambiguous in industry usage and commonly refers - to the `PA` tier instead. Seller-internal "PMP curated deals" map to - `CUR` on the wire. See the `DealType` docstring for the full +- `Deal.curation` (optional): an IAB Deals API v1.0 `Curation` object + (`deal-api` spec, `deal1.0.md`) — `curator` (canonical domain of the + packaging entity), `curator_deal_id` (the curator's own deal id, spec + `cdealid`), `curation_fee_type` (spec `curfeetype`; 0=undisclosed, + 1=percent of spend, 2=flat fee, 3=CPM, 4=no fee), and an `ext` slot. + Curated packages (Index Inventory/Auction Packages, PubMatic Auction + Packages, Magnite Curate, and the like) are represented by this object + riding alongside any `DealType`, not by a new `DealType` value: + pricing mechanic and packaging structure are orthogonal per the + published spec. Two candidate `DealType` wire values, `CUR` and the + industry term "PMP", were considered and rejected in favor of this + spec-aligned object; see the `DealType` docstring for the full disambiguation. ## [0.5.1] - 2026-08-10 diff --git a/CONFORMANCE.md b/CONFORMANCE.md index bdb92b2..516336b 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -10,9 +10,8 @@ generator). Acronyms used below (defined on first use): IAB = Interactive Advertising Bureau; JSON = JavaScript Object Notation; FD = flagged decision (the -remediation plan's decision register); PG / PD / PA / CUR = Programmatic -Guaranteed / Preferred Deal / Private Auction / curated package deal; -GRP = gross rating point; +remediation plan's decision register); PG / PD / PA = Programmatic +Guaranteed / Preferred Deal / Private Auction; GRP = gross rating point; CPM / CPP = cost per mille / cost per point; A2A = Agent-to-Agent protocol; JSON-RPC = JSON Remote Procedure Call; AAMP = the IAB Tech Lab agent discovery and trust registry; AdCOM = Advertising Common Object diff --git a/STANDARDS_GAP_REPORT.md b/STANDARDS_GAP_REPORT.md index 725c45e..aea7b29 100644 --- a/STANDARDS_GAP_REPORT.md +++ b/STANDARDS_GAP_REPORT.md @@ -62,7 +62,7 @@ What is **not** verified is external-spec *fidelity*. The contract borrows names - **Status:** UNVERIFIED - **Registry id:** `iab-deals-api-1.0` -- **Unverified / missing:** No vectors derived from the published Deals API v1.0 examples/schemas. Internal vectors cover our reconciled quote->book envelopes only. +- **Unverified / missing:** No vectors derived from the published Deals API v1.0 examples/schemas. Internal vectors cover our reconciled quote->book envelopes only. The Curation object (deal-api spec, deal1.0.md) is now modeled as Deal.curation, but against this repo's own transcription of the field table, not against a hash-pinned copy of the publication or its example payloads. - **To close this gap:** Obtain the published IAB Deals API v1.0 schemas and example payloads and add vectors derived from them; assert our reconciled quote->book envelopes validate against the published request/response shapes. #### IAB Tech Lab AAMP agent discovery and trust registry diff --git a/spec/fixtures/primitives/Deal.golden.json b/spec/fixtures/primitives/Deal.golden.json index 92dfa6c..89c3ba0 100644 --- a/spec/fixtures/primitives/Deal.golden.json +++ b/spec/fixtures/primitives/Deal.golden.json @@ -26,6 +26,7 @@ "verified_at": "2026-07-01T12:00:00Z" }, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-001", "deal_type": "PG", "expires_at": null, @@ -107,6 +108,7 @@ "buyer_tier": "agency", "consent_context": null, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-ltv-002", "deal_type": "PG", "expires_at": null, @@ -190,6 +192,7 @@ "buyer_tier": "public", "consent_context": null, "created_at": "2026-07-02T09:30:00Z", + "curation": null, "deal_id": "deal-003", "deal_type": "PD", "expires_at": null, @@ -257,6 +260,7 @@ "verified_at": "2026-07-01T12:00:00Z" }, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-001", "deal_type": "PG", "expires_at": null, @@ -356,6 +360,7 @@ "verified_at": "2026-07-01T12:00:00Z" }, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-001", "deal_type": "PG", "expires_at": null, @@ -453,6 +458,7 @@ "verified_at": "2026-07-01T12:00:00Z" }, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-001", "deal_type": "PG", "expires_at": null, diff --git a/spec/fixtures/protocol/DealBookingResponse.golden.json b/spec/fixtures/protocol/DealBookingResponse.golden.json index 00351d6..cc819db 100644 --- a/spec/fixtures/protocol/DealBookingResponse.golden.json +++ b/spec/fixtures/protocol/DealBookingResponse.golden.json @@ -29,6 +29,7 @@ "verified_at": "2026-07-01T12:00:00Z" }, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-001", "deal_type": "PG", "expires_at": null, @@ -141,6 +142,7 @@ "verified_at": "2026-07-01T12:00:00Z" }, "created_at": "2026-07-01T12:00:00Z", + "curation": null, "deal_id": "deal-001", "deal_type": "PG", "expires_at": null, diff --git a/spec/jsonschema/Agent.json b/spec/jsonschema/Agent.json index 321210d..2f8be98 100644 --- a/spec/jsonschema/Agent.json +++ b/spec/jsonschema/Agent.json @@ -226,7 +226,7 @@ "type": "array" }, "supported_deal_types": { - "description": "DealType wire values the agent supports: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal).", + "description": "DealType wire values the agent supports: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction).", "items": { "type": "string" }, diff --git a/spec/jsonschema/Deal.json b/spec/jsonschema/Deal.json index e5e60e3..6484634 100644 --- a/spec/jsonschema/Deal.json +++ b/spec/jsonschema/Deal.json @@ -148,6 +148,77 @@ "title": "ConsentContext", "type": "object" }, + "Curation": { + "description": "IAB Deals API v1.0 ``Curation`` object (``deal-api`` spec,\n``deal1.0.md``): identifies the entity that packaged inventory,\ntechnology, and/or data into a deal.\n\nOrthogonal to :class:`DealType` (see its docstring) \u2014 a curated deal\nstill has a pricing mechanic (``PG``/``PD``/``PA``); this object\nlayers packaging provenance on top and can accompany any of them.\nEvery field is optional per the published spec, matching the\nspec's own all-optional ``Curation`` object.", + "properties": { + "curation_fee_type": { + "anyOf": [ + { + "$ref": "#/$defs/CurationFeeType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Fee-structure classification for curation services (spec ``curfeetype``); see :class:`CurationFeeType`." + }, + "curator": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Canonical domain of the business entity that did the packaging of inventory, technology and/or data (spec ``curator``).", + "title": "Curator" + }, + "curator_deal_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Deal id in the curator's own namespace (spec ``cdealid``).", + "title": "Curator Deal Id" + }, + "ext": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Extension slot (spec placeholder for deal-specific ext).", + "title": "Ext" + } + }, + "title": "Curation", + "type": "object" + }, + "CurationFeeType": { + "description": "Deals API v1.0 ``Curation.curfeetype``: how the curator's fee is\nstructured. Communicates fee STRUCTURE only, never the fee amount\nitself (per the published spec).\n\n- ``0`` = undisclosed\n- ``1`` = percentage of spend\n- ``2`` = flat fee\n- ``3`` = CPM (cost per mille)\n- ``4`` = no fee", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "title": "CurationFeeType", + "type": "integer" + }, "DealStatus": { "description": "ONE deal status vocabulary, unioned from the four competing sets.\n\nSources: buyer ``DealResponse.status`` (proposed/active/rejected/\nexpired/completed), seller ``DealBookingStatus`` (proposed/active/\nexpired/cancelled), buyer ``BuyerDealStatus`` (quoted/negotiating/\naccepted/booking/booked/delivering/... + linear TV extensions).\n\nRecorded aliases (retired values, NOT valid on the wire):\n``delivering`` -> ``active``; ``booking`` -> ``booked``;\n``quoted`` -> represented by QuoteStatus, not a deal state;\n``partially_canceled`` -> ``partially_cancelled``.", "enum": [ @@ -168,12 +239,11 @@ "type": "string" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" @@ -739,6 +809,18 @@ "title": "Created At", "type": "string" }, + "curation": { + "anyOf": [ + { + "$ref": "#/$defs/Curation" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IAB Deals API v1.0 Curation object, present when this deal is a curated package (see the DealType docstring: curation is orthogonal to deal_type, not a value of it)." + }, "deal_id": { "description": "Seller-issued deal identifier.", "title": "Deal Id", diff --git a/spec/jsonschema/Product.json b/spec/jsonschema/Product.json index 803028d..d4d061a 100644 --- a/spec/jsonschema/Product.json +++ b/spec/jsonschema/Product.json @@ -39,7 +39,7 @@ "default": null }, "supported_deal_types": { - "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal).", + "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction).", "items": { "$ref": "#/$defs/DealType" }, @@ -58,12 +58,11 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/Proposal.json b/spec/jsonschema/Proposal.json index 8437fda..e07cb09 100644 --- a/spec/jsonschema/Proposal.json +++ b/spec/jsonschema/Proposal.json @@ -12,12 +12,11 @@ "type": "string" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/Quote.json b/spec/jsonschema/Quote.json index e3cb80f..89a767f 100644 --- a/spec/jsonschema/Quote.json +++ b/spec/jsonschema/Quote.json @@ -149,12 +149,11 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/protocol/DealBookingResponse.json b/spec/jsonschema/protocol/DealBookingResponse.json index 9dc9d12..8925895 100644 --- a/spec/jsonschema/protocol/DealBookingResponse.json +++ b/spec/jsonschema/protocol/DealBookingResponse.json @@ -148,6 +148,77 @@ "title": "ConsentContext", "type": "object" }, + "Curation": { + "description": "IAB Deals API v1.0 ``Curation`` object (``deal-api`` spec,\n``deal1.0.md``): identifies the entity that packaged inventory,\ntechnology, and/or data into a deal.\n\nOrthogonal to :class:`DealType` (see its docstring) \u2014 a curated deal\nstill has a pricing mechanic (``PG``/``PD``/``PA``); this object\nlayers packaging provenance on top and can accompany any of them.\nEvery field is optional per the published spec, matching the\nspec's own all-optional ``Curation`` object.", + "properties": { + "curation_fee_type": { + "anyOf": [ + { + "$ref": "#/$defs/CurationFeeType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Fee-structure classification for curation services (spec ``curfeetype``); see :class:`CurationFeeType`." + }, + "curator": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Canonical domain of the business entity that did the packaging of inventory, technology and/or data (spec ``curator``).", + "title": "Curator" + }, + "curator_deal_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Deal id in the curator's own namespace (spec ``cdealid``).", + "title": "Curator Deal Id" + }, + "ext": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Extension slot (spec placeholder for deal-specific ext).", + "title": "Ext" + } + }, + "title": "Curation", + "type": "object" + }, + "CurationFeeType": { + "description": "Deals API v1.0 ``Curation.curfeetype``: how the curator's fee is\nstructured. Communicates fee STRUCTURE only, never the fee amount\nitself (per the published spec).\n\n- ``0`` = undisclosed\n- ``1`` = percentage of spend\n- ``2`` = flat fee\n- ``3`` = CPM (cost per mille)\n- ``4`` = no fee", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "title": "CurationFeeType", + "type": "integer" + }, "Deal": { "description": "A confirmed deal booked from a quote (Deals API v1.0 book phase).\n\nID minting: ``deal_id`` is seller-issued.", "properties": { @@ -179,6 +250,18 @@ "title": "Created At", "type": "string" }, + "curation": { + "anyOf": [ + { + "$ref": "#/$defs/Curation" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IAB Deals API v1.0 Curation object, present when this deal is a curated package (see the DealType docstring: curation is orthogonal to deal_type, not a value of it)." + }, "deal_id": { "description": "Seller-issued deal identifier.", "title": "Deal Id", @@ -323,12 +406,11 @@ "type": "string" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/protocol/ProductListResponse.json b/spec/jsonschema/protocol/ProductListResponse.json index 5b1c077..8c1c426 100644 --- a/spec/jsonschema/protocol/ProductListResponse.json +++ b/spec/jsonschema/protocol/ProductListResponse.json @@ -39,7 +39,7 @@ "default": null }, "supported_deal_types": { - "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal).", + "description": "Supported deal types: 'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction).", "items": { "$ref": "#/$defs/DealType" }, @@ -58,12 +58,11 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" diff --git a/spec/jsonschema/protocol/QuoteRequest.json b/spec/jsonschema/protocol/QuoteRequest.json index d1c2d21..ef936c6 100644 --- a/spec/jsonschema/protocol/QuoteRequest.json +++ b/spec/jsonschema/protocol/QuoteRequest.json @@ -238,12 +238,11 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" @@ -453,7 +452,7 @@ }, "deal_type": { "$ref": "#/$defs/DealType", - "description": "'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated package deal). Typed \u2014 the retired long-form strings are not valid wire values." + "description": "'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction). Typed \u2014 the retired long-form strings are not valid wire values." }, "flight_end": { "anyOf": [ diff --git a/spec/jsonschema/protocol/QuoteResponse.json b/spec/jsonschema/protocol/QuoteResponse.json index 674fb49..1f589c0 100644 --- a/spec/jsonschema/protocol/QuoteResponse.json +++ b/spec/jsonschema/protocol/QuoteResponse.json @@ -149,12 +149,11 @@ "type": "object" }, "DealType": { - "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n- ``CUR`` = curated package deal: a floor-priced, bid-based deal that\n bundles curated inventory (single- or multi-publisher) under one\n deal ID via an SSP-native (supply-side platform) curation product \u2014\n e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction\n Packages, Magnite Curate. Mechanically closest to ``PA`` (floor +\n competitive bid) but a commercially distinct, separately sold\n product, so it is kept as a sibling value rather than folded into\n ``PA``. Named ``CUR`` rather than \"PMP\" specifically because\n bare \"PMP\" is ambiguous in industry usage \u2014 it is also the term\n commonly used for the ``PA`` private-auction tier \u2014 so this value\n deliberately avoids reusing that string on the wire. Seller-internal\n \"PMP curated deals\" terminology maps to ``CUR`` here, not to ``PA``.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", + "description": "Programmatic deal types. The short wire encoding is canonical.\n\n- ``PG`` = Programmatic Guaranteed: fixed price, guaranteed impressions\n- ``PD`` = Preferred Deal: fixed price, non-guaranteed first look\n- ``PA`` = Private Auction: auction with floor price, invited buyers.\n This is the general private-marketplace tier \u2014 bare \"PMP\" in\n industry usage most often means this.\n\nCurated packages are NOT a fourth deal type. A curated deal (Index\nExchange Inventory/Auction Packages, PubMatic Auction Packages,\nMagnite Curate, and the like) is represented by the optional\n:class:`Curation` object riding alongside a deal, not by a distinct\n``DealType`` value \u2014 pricing mechanic and packaging structure are\northogonal, and the ``Curation`` object can accompany any\n``DealType`` (curated deals are typically ``PA``-mechanics \u2014 floor +\ncompetitive bid \u2014 but a curated deal can equally be ``PG``). Two\nwire-value candidates for a fourth deal type, ``CUR`` and the\nindustry term \"PMP\", were considered and rejected during design: both\nwould have collapsed a packaging/provenance concept into the\npricing-mechanic enum, which the published IAB Deals API v1.0\n``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately\nkeeps separate. See :class:`Curation` for the wire shape; seller-\ninternal \"PMP curated deals\" terminology maps to a deal carrying a\npopulated ``Curation`` object, not to a dedicated ``DealType`` value.\n\nMapping from the seller repo's retired long-form encoding\n(``models/core.py``): ``programmaticguaranteed`` -> ``PG``,\n``preferreddeal`` -> ``PD``, ``privateauction`` -> ``PA``. The\nlong-form strings are NOT valid wire values.", "enum": [ "PG", "PD", - "PA", - "CUR" + "PA" ], "title": "DealType", "type": "string" diff --git a/src/iab_agentic_primitives/conformance/standards.py b/src/iab_agentic_primitives/conformance/standards.py index cd9e67c..b23621d 100644 --- a/src/iab_agentic_primitives/conformance/standards.py +++ b/src/iab_agentic_primitives/conformance/standards.py @@ -98,7 +98,11 @@ missing=( "No vectors derived from the published Deals API v1.0 " "examples/schemas. Internal vectors cover our reconciled " - "quote->book envelopes only." + "quote->book envelopes only. The Curation object (deal-api " + "spec, deal1.0.md) is now modeled as Deal.curation, but " + "against this repo's own transcription of the field table, " + "not against a hash-pinned copy of the publication or its " + "example payloads." ), ), StandardEntry( diff --git a/src/iab_agentic_primitives/conformance/vectors.py b/src/iab_agentic_primitives/conformance/vectors.py index 6d55e13..8a3681f 100644 --- a/src/iab_agentic_primitives/conformance/vectors.py +++ b/src/iab_agentic_primitives/conformance/vectors.py @@ -23,10 +23,10 @@ Determinism: every timestamp and identifier is pinned — regeneration is byte-stable, so ``--regenerate`` never produces noise diffs. -Acronyms: FD = flagged decision; PG/PD/PA/CUR = Programmatic Guaranteed / -Preferred Deal / Private Auction / curated package deal; GRP = gross -rating point; CPM/CPP = cost per mille / cost per point; GPP = Global -Privacy Platform; TCF = Transparency & Consent Framework. +Acronyms: FD = flagged decision; PG/PD/PA = Programmatic Guaranteed / +Preferred Deal / Private Auction; GRP = gross rating point; CPM/CPP = +cost per mille / cost per point; GPP = Global Privacy Platform; +TCF = Transparency & Consent Framework. """ from __future__ import annotations diff --git a/src/iab_agentic_primitives/primitives/__init__.py b/src/iab_agentic_primitives/primitives/__init__.py index 45748ea..3ea59e2 100644 --- a/src/iab_agentic_primitives/primitives/__init__.py +++ b/src/iab_agentic_primitives/primitives/__init__.py @@ -13,7 +13,7 @@ - ``identity`` -- Organization, Account, Agent, BuyerIdentity, ConsentContext - ``inventory`` -- Product, MediaKit, Package -- ``pricing`` -- DealType, pricing enums, linear TV params, RateCard, Quote +- ``pricing`` -- DealType, Curation, pricing enums, linear TV params, RateCard, Quote - ``lifecycle`` -- Proposal, Negotiation, Deal, Order, Line, ChangeRequest, Session - ``creative`` -- Creative, CreativeApproval, Assignment - ``supply_chain`` -- SupplyChain, SupplyChainNode, SellersJsonEntry @@ -103,6 +103,8 @@ ) from .pricing import ( CancellationTerms, + Curation, + CurationFeeType, DealType, LinearTVParams, LinearTVQuoteDetails, @@ -198,6 +200,8 @@ "Product", # pricing "CancellationTerms", + "Curation", + "CurationFeeType", "DealType", "LinearTVParams", "LinearTVQuoteDetails", diff --git a/src/iab_agentic_primitives/primitives/identity.py b/src/iab_agentic_primitives/primitives/identity.py index 1633b5d..8364c07 100644 --- a/src/iab_agentic_primitives/primitives/identity.py +++ b/src/iab_agentic_primitives/primitives/identity.py @@ -242,8 +242,7 @@ class Agent(WireModel): default_factory=list, description=( "DealType wire values the agent supports: 'PG' (Programmatic " - "Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction), " - "'CUR' (curated package deal)." + "Guaranteed), 'PD' (Preferred Deal), 'PA' (Private Auction)." ), ) trust_status: TrustStatus = Field( diff --git a/src/iab_agentic_primitives/primitives/inventory.py b/src/iab_agentic_primitives/primitives/inventory.py index 6f31494..757bfbd 100644 --- a/src/iab_agentic_primitives/primitives/inventory.py +++ b/src/iab_agentic_primitives/primitives/inventory.py @@ -37,8 +37,7 @@ class CommercialTerms(WireModel): supported_deal_types: list[DealType] = Field( default_factory=list, description="Supported deal types: 'PG' (Programmatic Guaranteed), " - "'PD' (Preferred Deal), 'PA' (Private Auction), 'CUR' (curated " - "package deal).", + "'PD' (Preferred Deal), 'PA' (Private Auction).", ) supported_pricing_models: list[PricingModel] = Field(default_factory=list) minimum_deal_value: Money | None = None diff --git a/src/iab_agentic_primitives/primitives/lifecycle.py b/src/iab_agentic_primitives/primitives/lifecycle.py index 010f9e9..c19f09b 100644 --- a/src/iab_agentic_primitives/primitives/lifecycle.py +++ b/src/iab_agentic_primitives/primitives/lifecycle.py @@ -21,6 +21,7 @@ from ._util import Money, WireModel, utc_now from .identity import AccessTier, BuyerIdentity, ConsentContext from .pricing import ( + Curation, DealType, LinearTVQuoteDetails, MediaType, @@ -335,6 +336,12 @@ class Deal(WireModel): description="OpenRTB supply chain (schain) for transparency (EP-10.3); " "optional so pre-schain deals still validate.", ) + curation: Curation | None = Field( + default=None, + description="IAB Deals API v1.0 Curation object, present when this deal " + "is a curated package (see the DealType docstring: curation is " + "orthogonal to deal_type, not a value of it).", + ) # --------------------------------------------------------------------------- diff --git a/src/iab_agentic_primitives/primitives/pricing.py b/src/iab_agentic_primitives/primitives/pricing.py index 42f81cb..049b37b 100644 --- a/src/iab_agentic_primitives/primitives/pricing.py +++ b/src/iab_agentic_primitives/primitives/pricing.py @@ -29,18 +29,23 @@ class DealType(str, Enum): - ``PA`` = Private Auction: auction with floor price, invited buyers. This is the general private-marketplace tier — bare "PMP" in industry usage most often means this. - - ``CUR`` = curated package deal: a floor-priced, bid-based deal that - bundles curated inventory (single- or multi-publisher) under one - deal ID via an SSP-native (supply-side platform) curation product — - e.g. Index Exchange Inventory/Auction Packages, PubMatic Auction - Packages, Magnite Curate. Mechanically closest to ``PA`` (floor + - competitive bid) but a commercially distinct, separately sold - product, so it is kept as a sibling value rather than folded into - ``PA``. Named ``CUR`` rather than "PMP" specifically because - bare "PMP" is ambiguous in industry usage — it is also the term - commonly used for the ``PA`` private-auction tier — so this value - deliberately avoids reusing that string on the wire. Seller-internal - "PMP curated deals" terminology maps to ``CUR`` here, not to ``PA``. + + Curated packages are NOT a fourth deal type. A curated deal (Index + Exchange Inventory/Auction Packages, PubMatic Auction Packages, + Magnite Curate, and the like) is represented by the optional + :class:`Curation` object riding alongside a deal, not by a distinct + ``DealType`` value — pricing mechanic and packaging structure are + orthogonal, and the ``Curation`` object can accompany any + ``DealType`` (curated deals are typically ``PA``-mechanics — floor + + competitive bid — but a curated deal can equally be ``PG``). Two + wire-value candidates for a fourth deal type, ``CUR`` and the + industry term "PMP", were considered and rejected during design: both + would have collapsed a packaging/provenance concept into the + pricing-mechanic enum, which the published IAB Deals API v1.0 + ``Curation`` object (``deal-api`` spec, ``deal1.0.md``) deliberately + keeps separate. See :class:`Curation` for the wire shape; seller- + internal "PMP curated deals" terminology maps to a deal carrying a + populated ``Curation`` object, not to a dedicated ``DealType`` value. Mapping from the seller repo's retired long-form encoding (``models/core.py``): ``programmaticguaranteed`` -> ``PG``, @@ -51,7 +56,61 @@ class DealType(str, Enum): PROGRAMMATIC_GUARANTEED = "PG" PREFERRED_DEAL = "PD" PRIVATE_AUCTION = "PA" - CURATED_PACKAGE = "CUR" + + +# --------------------------------------------------------------------------- +# Curation — IAB Deals API v1.0 Curation object (deal-api spec, deal1.0.md) +# --------------------------------------------------------------------------- + + +class CurationFeeType(int, Enum): + """Deals API v1.0 ``Curation.curfeetype``: how the curator's fee is + structured. Communicates fee STRUCTURE only, never the fee amount + itself (per the published spec). + + - ``0`` = undisclosed + - ``1`` = percentage of spend + - ``2`` = flat fee + - ``3`` = CPM (cost per mille) + - ``4`` = no fee + """ + + UNDISCLOSED = 0 + PERCENT_OF_SPEND = 1 + FLAT_FEE = 2 + CPM = 3 + NO_FEE = 4 + + +class Curation(WireModel): + """IAB Deals API v1.0 ``Curation`` object (``deal-api`` spec, + ``deal1.0.md``): identifies the entity that packaged inventory, + technology, and/or data into a deal. + + Orthogonal to :class:`DealType` (see its docstring) — a curated deal + still has a pricing mechanic (``PG``/``PD``/``PA``); this object + layers packaging provenance on top and can accompany any of them. + Every field is optional per the published spec, matching the + spec's own all-optional ``Curation`` object. + """ + + curator: str | None = Field( + default=None, + description="Canonical domain of the business entity that did the " + "packaging of inventory, technology and/or data (spec ``curator``).", + ) + curator_deal_id: str | None = Field( + default=None, + description="Deal id in the curator's own namespace (spec ``cdealid``).", + ) + curation_fee_type: CurationFeeType | None = Field( + default=None, + description="Fee-structure classification for curation services " + "(spec ``curfeetype``); see :class:`CurationFeeType`.", + ) + ext: dict[str, Any] | None = Field( + default=None, description="Extension slot (spec placeholder for deal-specific ext)." + ) # --------------------------------------------------------------------------- @@ -384,6 +443,8 @@ class Quote(WireModel): __all__ = [ "CancellationTerms", + "Curation", + "CurationFeeType", "DealType", "LinearTVParams", "LinearTVQuoteDetails", diff --git a/src/iab_agentic_primitives/protocol/quotes.py b/src/iab_agentic_primitives/protocol/quotes.py index 2045f7e..289ecd0 100644 --- a/src/iab_agentic_primitives/protocol/quotes.py +++ b/src/iab_agentic_primitives/protocol/quotes.py @@ -54,8 +54,8 @@ class QuoteRequest(IdempotentRequest): product_id: str = Field(description="Seller-issued product to quote.") deal_type: DealType = Field( description="'PG' (Programmatic Guaranteed), 'PD' (Preferred Deal), " - "'PA' (Private Auction), 'CUR' (curated package deal). Typed — the " - "retired long-form strings are not valid wire values." + "'PA' (Private Auction). Typed — the retired long-form strings " + "are not valid wire values." ) impressions: int | None = Field( default=None, ge=0, description="Requested volume; required for PG." diff --git a/tests/test_curation.py b/tests/test_curation.py new file mode 100644 index 0000000..ef3c8aa --- /dev/null +++ b/tests/test_curation.py @@ -0,0 +1,113 @@ +"""Tests for the Curation object (IAB Deals API v1.0, deal-api spec, deal1.0.md). + +Curated packages ride on a Deal via the optional ``curation`` field, not via +a dedicated DealType value — see DealType's docstring and +tests/test_enum_unification.py::test_dealtype_docstring_disclaims_curation_as_a_deal_type +for the disambiguation this repo settled on (CUR and "PMP" were both +considered and rejected as DealType values). +""" + +import pytest +from pydantic import ValidationError + +from iab_agentic_primitives.primitives import ( + Curation, + CurationFeeType, + Deal, + DealType, + ProductRef, + QuotePricing, + QuoteTerms, +) + + +def _deal(**kwargs) -> Deal: + return Deal( + deal_id="deal-1", + deal_type=DealType.PRIVATE_AUCTION, + product=ProductRef(product_id="p-1", name="x"), + pricing=QuotePricing(), + terms=QuoteTerms(), + **kwargs, + ) + + +# --------------------------------------------------------------------------- +# Optional-absent default: a Deal with no curation info still validates +# --------------------------------------------------------------------------- + + +def test_deal_curation_defaults_to_none() -> None: + deal = _deal() + assert deal.curation is None + assert '"curation":null' in deal.model_dump_json() + + +# --------------------------------------------------------------------------- +# Round-trip: a populated Curation object survives serialize/deserialize +# --------------------------------------------------------------------------- + + +def test_curation_roundtrips_on_a_deal() -> None: + curation = Curation( + curator="curator.example.com", + curator_deal_id="curator-deal-42", + curation_fee_type=CurationFeeType.PERCENT_OF_SPEND, + ) + deal = _deal(curation=curation) + + dumped = deal.model_dump_json() + assert '"curator":"curator.example.com"' in dumped + assert '"curator_deal_id":"curator-deal-42"' in dumped + assert '"curation_fee_type":1' in dumped + + reparsed = Deal.model_validate_json(dumped) + assert reparsed.curation == curation + assert reparsed.curation.curator == "curator.example.com" + assert reparsed.curation.curator_deal_id == "curator-deal-42" + assert reparsed.curation.curation_fee_type is CurationFeeType.PERCENT_OF_SPEND + + +def test_curation_fields_are_individually_optional() -> None: + """Every Curation field is optional per the published spec (all fields + in the Deals API v1.0 Curation object table are marked Optional).""" + curation = Curation() + assert curation.curator is None + assert curation.curator_deal_id is None + assert curation.curation_fee_type is None + assert curation.ext is None + + +def test_curation_can_accompany_any_deal_type() -> None: + """Curation is orthogonal to DealType: it can ride with PG, not just PA.""" + curation = Curation(curator="curator.example.com", curator_deal_id="cd-1") + deal = Deal( + deal_id="deal-2", + deal_type=DealType.PROGRAMMATIC_GUARANTEED, + product=ProductRef(product_id="p-1", name="x"), + pricing=QuotePricing(), + terms=QuoteTerms(), + curation=curation, + ) + assert deal.deal_type is DealType.PROGRAMMATIC_GUARANTEED + assert deal.curation == curation + + +# --------------------------------------------------------------------------- +# curation_fee_type bounds: only the spec's 0-4 enum values are valid +# --------------------------------------------------------------------------- + + +def test_curation_fee_type_wire_values() -> None: + assert CurationFeeType.UNDISCLOSED == 0 + assert CurationFeeType.PERCENT_OF_SPEND == 1 + assert CurationFeeType.FLAT_FEE == 2 + assert CurationFeeType.CPM == 3 + assert CurationFeeType.NO_FEE == 4 + assert {m.value for m in CurationFeeType} == {0, 1, 2, 3, 4} + + +@pytest.mark.parametrize("out_of_bounds", [-1, 5, 100]) +def test_curation_fee_type_rejects_out_of_bounds_values(out_of_bounds: int) -> None: + with pytest.raises(ValidationError): + Curation(curation_fee_type=out_of_bounds) diff --git a/tests/test_enum_unification.py b/tests/test_enum_unification.py index 2b3ed47..905852d 100644 --- a/tests/test_enum_unification.py +++ b/tests/test_enum_unification.py @@ -1,7 +1,6 @@ """Enum-unification tests: one vocabulary per concept, wire values canonical. -PG = Programmatic Guaranteed, PD = Preferred Deal, PA = Private Auction, -CUR = curated package deal. +PG = Programmatic Guaranteed, PD = Preferred Deal, PA = Private Auction. """ import pytest @@ -23,7 +22,7 @@ def test_dealtype_wire_values() -> None: assert DealType("PG") is DealType.PROGRAMMATIC_GUARANTEED assert DealType("PD") is DealType.PREFERRED_DEAL assert DealType("PA") is DealType.PRIVATE_AUCTION - assert {m.value for m in DealType} == {"PG", "PD", "PA", "CUR"} + assert {m.value for m in DealType} == {"PG", "PD", "PA"} @pytest.mark.parametrize( @@ -48,32 +47,16 @@ def test_dealtype_serializes_to_wire_value() -> None: assert '"deal_type":"PG"' in quote.model_dump_json() -def test_dealtype_cur_roundtrips() -> None: - """CUR (curated package deal) serializes/deserializes like any other member.""" - from iab_agentic_primitives.primitives import ProductRef, Quote, QuotePricing, QuoteTerms - - assert DealType("CUR") is DealType.CURATED_PACKAGE - - quote = Quote( - quote_id="q-2", - deal_type=DealType.CURATED_PACKAGE, - product=ProductRef(product_id="p-1", name="x"), - pricing=QuotePricing(), - terms=QuoteTerms(), - ) - dumped = quote.model_dump_json() - assert '"deal_type":"CUR"' in dumped - assert Quote.model_validate_json(dumped).deal_type is DealType.CURATED_PACKAGE - - -def test_dealtype_cur_documented_distinctly_from_pa() -> None: - """The docstring must disambiguate CUR (curated package) from PA (private auction), - and explain why the wire value is CUR rather than the industry's ambiguous "PMP".""" +def test_dealtype_docstring_disclaims_curation_as_a_deal_type() -> None: + """Curated packages are represented by the Curation object, not a DealType + value; the docstring must say so explicitly and record that CUR/PMP were + considered and rejected as fourth deal-type wire values.""" doc = DealType.__doc__ or "" + assert "not" in doc.lower() and "deal type" in doc.lower() + assert "Curation" in doc assert "CUR" in doc - assert "curated" in doc.lower() - assert "ambiguous" in doc.lower() - assert "PMP" in doc # still referenced, as the term the ambiguity comes from + assert "PMP" in doc + assert "orthogonal" in doc.lower() # ---------------------------------------------------------------------------