feat(media-buy): external audience sources on sync_audiences (#6540) - #6857
feat(media-buy): external audience sources on sync_audiences (#6540)#6857bokelley wants to merge 2 commits into
Conversation
Runtime leg of the audience-activation surface, experimental under media_buy.audience_activation. Audiences carry either inline member deltas or a source reference (core/audience-source.json): dataset (grantee-identified share the seller reads) or platform_segment (binds a vendor-distributed segment). Response echoes the source with access_status and columns_read; counts anchor to last_synced_at. Transport fixed at creation; loss of source access never changes audience status; access expiry is not deletion. New SOURCE_ACCESS_FAILED error code with error.field-keyed recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| "type": "string", | ||
| "format": "date-time", | ||
| "description": "ISO 8601 timestamp of when the most recent sync operation was accepted by the platform. Useful for agents reasoning about audience freshness. Omitted if the seller does not track this." | ||
| "description": "ISO 8601 timestamp of when the most recent sync operation was accepted by the platform. Useful for agents reasoning about audience freshness. Omitted if the seller does not track this. REQUIRED when source is present: for externally sourced audiences every count field is as-of this timestamp — it is the buyer's only anchor for what a count means, since two reads can differ because membership changed, match rate changed, or the seller re-read the source." |
There was a problem hiding this comment.
Medium: Description says last_synced_at is "REQUIRED when source is present," but the if/then two hunks down only requires it when status is ready/too_small — and the docs lifecycle rule scopes it the same way ("Required on sourced audiences whenever counts are populated (ready/too_small)"). A sourced audience in processing has source present with nothing synced yet, and the test at tests/audience-external-sources.test.cjs:612 correctly asserts it validates without last_synced_at. The normative field text is stricter than both the enforcement and the docs. Tighten it: "REQUIRED once counts populate (status ready/too_small)."
There was a problem hiding this comment.
Ladon verdict: Escalate to human review
Escalate — gated schema paths require human/CODEOWNERS approval.
This PR adds an experimental runtime leg of audience activation (#6540): a new discriminated core/audience-source.json, a source request field with schema-enforced mutual exclusion vs add/remove, a response source echo, and a new SOURCE_ACCESS_FAILED error code. Schema, changeset scope (minor, protocol-scoped), oneOf discrimination, and schema↔docs coherence largely hold. No critical or high findings were reported, so the diff itself is not blocking.
However, this PR modifies/adds files under static/schemas/source/**, which are under a hard, deterministic approval gate (gated_paths: true), and review_decision is REVIEW_REQUIRED — not APPROVED. Per decision rule row 2, the outcome must be escalate until a real human/CODEOWNERS approval satisfies the gate.
Medium findings (non-blocking, worth a look during human review):
- static/schemas/source/media-buy/sync-audiences-response.json:116 —
last_synced_atdescription overstates its requirement relative to its own if/then and the docs (normative-text mismatch).
Escalation reasons:
- Gated schema paths touched (e.g.
static/schemas/source/core/audience-source.json,static/schemas/source/media-buy/sync-audiences-request.json,sync-audiences-response.json,enums/error-code.json) andreview_decisionisREVIEW_REQUIRED. Human/CODEOWNERS approval required before merge.
Medium findings
- static/schemas/source/media-buy/sync-audiences-response.json:116 — last_synced_at description overstates requirement vs its own if/then and the docs
Why human review
- Gated paths touched under static/schemas/source/** (audience-source.json added; sync-audiences-request/response.json, error-code.json, audience-activation-method.json modified) and review_decision is REVIEW_REQUIRED — human/CODEOWNERS approval required before this can merge.
- This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/audience-activation-method.json (modified) matches
static/schemas/source/**; static/schemas/source/core/audience-source.json (added) matchesstatic/schemas/source/**; static/schemas/source/enums/error-code.json (modified) matchesstatic/schemas/source/**; static/schemas/source/media-buy/sync-audiences-request.json (modified) matchesstatic/schemas/source/**; static/schemas/source/media-buy/sync-audiences-response.json (modified) matchesstatic/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.
… schema-graph growth New 3.2 wire code needs a held-for-next-minor entry in the error-code drift registry, and core/audience-source.json adds one definition to the media-buy tool graph (583→584 instances, 152→153 unique) — pins updated to acknowledge the deliberate growth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Escalate to human review
Escalate — gated schema paths require human/CODEOWNERS approval.
This PR modifies/adds files under static/schemas/source/** (audience-source.json added; sync-audiences-request/response.json, error-code.json, audience-activation-method.json modified), which sit behind a hard, non-overridable approval gate. review_decision is not APPROVED (unknown/none), so per decision rule row 2 the gate is not satisfied and the outcome must be escalate.
No blocking (critical/high) findings surfaced in this run. The prior medium finding on sync-audiences-response.json:116 is outside the current delta and not re-raised. The delta since the prior pass — a new SOURCE_ACCESS_FAILED disposition (held-for-next-minor, target 3.2) plus definition-count bumps (583→584, 152→153) for the single new audience-source definition — is internally consistent and lint-valid. This is a net-clean iteration, but the gated-paths gate still stands until a human/CODEOWNERS approval is recorded.
Escalation reasons:
- Gated paths touched under
static/schemas/source/**andreview_decisionis notAPPROVED— human/CODEOWNERS review required before merge.
Why human review
- Touches gated paths under static/schemas/source/** (audience-source.json added; sync-audiences-request.json, sync-audiences-response.json, error-code.json, audience-activation-method.json modified) and review_decision is not APPROVED — human/CODEOWNERS approval required before this can merge (decision rule row 2, hard gate).
- This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/audience-activation-method.json (modified) matches
static/schemas/source/**; static/schemas/source/core/audience-source.json (added) matchesstatic/schemas/source/**; static/schemas/source/enums/error-code.json (modified) matchesstatic/schemas/source/**; static/schemas/source/media-buy/sync-audiences-request.json (modified) matchesstatic/schemas/source/**; static/schemas/source/media-buy/sync-audiences-response.json (modified) matchesstatic/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.
Implements #6540 — the runtime leg of the audience-activation surface (#4324 / #6549), experimental under the same
media_buy.audience_activationfeature id. WG approved the audience work for 3.2 as experimental.What this adds
An audience on
sync_audiencescarries either inline member deltas or a source reference — a pointer to buyer data the seller ingests over a declared activation rail. Data never transits AdCP.core/audience-source.json(new, experimental):dataset(grantee-identified share the seller reads — Snowflake Secure Data Sharing, Databricks D2D, BigQuery authorized views; optional declarativeaccess_expires_at) |platform_segment(binds a vendor-distributed segment to anaudience_id;segment_refis the vendor's ID as issued to the buyer, seller owns the ingest-side mapping).sourceon the audience object; schema-enforced mutual exclusion withadd/remove.sourceecho (never credentials) withaccess_status(active/unavailable) andcolumns_read;last_synced_atschema-required exactly when counts populate (ready/too_small) so every count has an anchor — first-read-pending (processing) and failed-first-ingest stay legal.SOURCE_ACCESS_FAILEDon both error-code surfaces,error.field-keyed recovery: bad reference (fix request) vs. missing grant (grant + retry) vs. vendor outage (backoff).sync_audiences(flows, canonical column contract, refresh vs. replication lag, latency band); present-tense updates to every "until RFC: external audience sources — pass-through references on sync_audiences #6540 lands" placeholder; canonical experimental-list row extended.tests/audience-external-sources.test.cjs(12) wired into the chain.Normative rules from the RFC review threads
CONFLICT(error.field: audience_id); convert by delete-and-recreate.last_synced_atfreezes, and failed re-reads MUST NOT emitsuspended(reserved for consent/policy causes, which remain orthogonal). Health reports viasource.access_status.delete: true.UNSUPPORTED_FEATURE; credential material →CREDENTIAL_IN_ARGS; grantee-identified flows only.Validation
Schema build, 12/12 new tests, example validation, error-code lint (both enum surfaces), oneOf audit, and compliance links all green on a tree rebased onto the merged #6549.
Closes #6540.
🤖 Generated with Claude Code