Skip to content

feat(creative): declare tracker execution contracts - #6786

Merged
bokelley merged 51 commits into
mainfrom
tracker-execution-contract
Aug 25, 2026
Merged

feat(creative): declare tracker execution contracts#6786
bokelley merged 51 commits into
mainfrom
tracker-execution-contract

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • add a format-option-scoped AdCP 3.2 production contract for first-class pixel_tracker, vast_tracker, and daast_tracker assets
  • distinguish complete unsupported sets from incomplete/undeclared behavior, with exact event, actor, path, target, offset, and standards-version matching
  • pin the effective product/placement contract, exact VAST/DAAST execution version, and RFC 8785 digests in immutable package format snapshots
  • keep seller production authority out of creative-agent capability and legacy projection surfaces
  • compose tracker initiation separately from feat(creative): define delivery representations and macro resolution #6767 macro processing and representation selection

Preview observation sessions and runtime evidence are intentionally excluded and remain AdCP 3.3 work in #6782. URL-slot tracker selectors are the independent 3.3 extension in #6784.

Stack

This PR is stacked on #6767 because tracker compatibility participates in representation-set selection. Retarget to main after #6767 merges.

Validation

  • npm run build:schemas — 106 bundled schemas, zero failures
  • npm run test:schemas
  • npm run test:json-schema
  • npm run test:compliance-snippets
  • npm run test:error-code-drift
  • npm run test:oneof-discriminators
  • npm run test:schema-links
  • node --test tests/tracker-execution-contracts.test.cjs tests/tracker-execution-package-integration.test.cjs tests/tracking-event-enums.test.cjs tests/creative-delivery-contracts.test.cjs tests/docs-nav-validation.test.cjs
  • node scripts/check-changeset-protocol-scope.cjs origin/vast-creative-delivery-rules
  • ad-tech, protocol, and code whole-diff review

Closes #6207
Related: #6775, #6783, #6782, #6784, #6767

Comment thread static/schemas/source/core/assets/vast-tracker-asset.json Outdated
Comment thread static/schemas/source/core/canonical-format-option.json

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Request changes

Request changes — 1 blocking finding.

Blocking findings

  • static/schemas/source/core/assets/vast-tracker-asset.json:34 — GA-published vast_tracker/daast_tracker assets are narrowed against 3.1.19 (offset ignored→forbidden, new event/target matrix) via the new constraints files. This is a breaking wire change shipped under a minor changeset, which violates the repo's changeset-scope rule (breaking wire changes require major, not minor/patch). Either scope the changeset as major or avoid tightening validation on the already-published assets.

Medium findings

  • static/schemas/source/core/canonical-format-option.json:45 — canonical-format-option adds a publisher_domain→format_option_id requirement (accept→reject), a validation tightening worth confirming is intended and non-breaking for existing payloads.

Row 1 of the decision table fires on the high finding, producing request-changes. Note: this PR touches gated paths under static/schemas/source/** with review_decision unknown/not APPROVED, and the reviewer flags a breaking-class change — both of which independently require human/CODEOWNERS review before merge. The schema modeling itself (discriminated tracker-execution-selector oneOf, immutable PackageFormatSnapshot with RFC 8785 digests, authority-stripping on projection surfaces) is clean and well tested; the blocker is the changeset-type/wire-compatibility mismatch on the GA-published tracker assets.

Blocking findings

  • static/schemas/source/core/assets/vast-tracker-asset.json:34 — vast_tracker/daast_tracker GA assets narrowed (offset ignored→forbidden, new event/target matrix): breaking wire change vs 3.1.19 shipped under a minor changeset; requires major changeset or reverted tightening

Medium findings

  • static/schemas/source/core/canonical-format-option.json:45 — canonical-format-option adds publisher_domain->format_option_id requirement (accept->reject)

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Escalate to human review

Escalate — gated paths not yet satisfied by human/CODEOWNERS review.

This is a subsequent pass. The prior request-changes blocking finding (vast/daast tracker assets narrowed against 3.1 GA under a minor changeset) has been resolved: the delta drops the whole-file constraints allOf and keeps only a local progress→offset requirement, restoring legacy-asset permissiveness, and new tests prove legacy assets/packages validate. The prior medium (canonical-format-option.json) file is out of the current delta and correctly skipped. No new blocking or medium findings this run.

However, this PR touches many files under static/schemas/source/**, which is a deterministic hard approval gate (gated_paths: true), and review_decision is CHANGES_REQUESTED — not APPROVED. Per decision rule row 2, the outcome must be escalate until a human/CODEOWNERS approval lifts the gate. No critical/high findings exist, so this is not request-changes; it is held for required human review only.

Escalation reasons:

  • Gated paths touched (protocol schema source under static/schemas/source/**) and review_decision is CHANGES_REQUESTED, not APPROVED. Human/CODEOWNERS approval is required before merge.

Why human review

  • Gated paths under static/schemas/source/** are modified/added and review_decision is CHANGES_REQUESTED (not APPROVED) — human/CODEOWNERS approval required to lift the hard approval gate (decision rule row 2).
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/assets/daast-tracker-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/assets/pixel-tracker-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/assets/vast-tracker-asset.json (modified) matches static/schemas/source/**; static/schemas/source/core/canonical-format-option.json (modified) matches static/schemas/source/**; static/schemas/source/core/daast-tracker-constraints.json (added) matches static/schemas/source/**; static/schemas/source/core/format.json (modified) matches static/schemas/source/**; static/schemas/source/core/package-format-snapshot.json (added) matches static/schemas/source/**; static/schemas/source/core/package.json (modified) matches static/schemas/source/**; static/schemas/source/core/product-format-declaration.json (modified) matches static/schemas/source/**; static/schemas/source/core/representation-destination.json (modified) matches static/schemas/source/**; static/schemas/source/core/representation-rejection.json (modified) matches static/schemas/source/**; static/schemas/source/core/representation-selection.json (modified) matches static/schemas/source/**; static/schemas/source/core/requirements/daast-asset-requirements.json (modified) matches static/schemas/source/**; static/schemas/source/core/tracker-execution-contract.json (added) matches static/schemas/source/**; static/schemas/source/core/tracker-execution-selector.json (added) matches static/schemas/source/**; static/schemas/source/core/transformer.json (modified) matches static/schemas/source/**; static/schemas/source/core/vast-tracker-constraints.json (added) matches static/schemas/source/**; static/schemas/source/core/x-entity-types.json (modified) matches static/schemas/source/**; static/schemas/source/enums/pixel-tracking-event.json (added) matches static/schemas/source/**; static/schemas/source/enums/tracker-execution-actor.json (added) matches static/schemas/source/**; static/schemas/source/enums/tracker-firing-path.json (added) matches static/schemas/source/**; static/schemas/source/formats/canonical/audio_daast.json (modified) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/get-media-buys-response.json (modified) matches static/schemas/source/**; static

…(truncated)…

@bokelley

Copy link
Copy Markdown
Contributor Author

Ready for fresh CODEOWNER review on the repaired stack. The head now descends from current #6781 and preserves all parent representation/macro guarantees. Follow-up fixes require matching first-class format slots, enforce complete-parent narrowing over atomic version/actor/path tuples without treating incomplete parents as ceilings, and keep DAAST close legacy-compatible while excluding it from standards selectors. Focused combined schema/compliance tests are green. @ohalushchak-exadel @bmilekic, please review when available.

… tracker-execution-contract

# Conflicts:
#	static/schemas/source/core/canonical-format-option.json
#	static/schemas/source/core/product-format-declaration.json
@bokelley
bokelley requested a review from pkras August 25, 2026 05:58
@bokelley

Copy link
Copy Markdown
Contributor Author

Pushed the latest parent integration and CI/resource fix at a049687a99.

  • preserves both root-level macro_resolution_capabilities and tracker_execution_contract
  • keeps macro processing metadata out of canonical creative params
  • restores the exact GA VAST/DAAST asset validation envelope while retaining strict matrices for new tracker selectors
  • removes avoidable schema-graph expansion; measured MCP contexts are 398.08 KiB (media-buy, 400 KiB budget) and 405.39 KiB (creative, scoped 410 KiB budget)
  • full schema suite passed: 34 schema checks, 160 Node tests, 10 premium-display tests, and 6 geo tests
  • focused combined suite passed 54/54; two independent protocol/ad-tech reviews found no blocker/high issues

Fresh human/CODEOWNERS review is requested for the exact pushed head. #6781 remains the dependency and should merge first.

@pkras pkras left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. closeLinear is missing VAST 4.0 (blocker or needs a note).
    In vast-tracker-constraints.json, closeLinear allows ["3.0","4.1","4.2","4.3"] but not 4.0, even though every neighboring event covers 4.0–4.3 and closeLinear exists in 4.0. As written it reads like a typo, and a seller building a 4.0 tag can’t declare it. Please either add 4.0 or add a $comment explaining why the gap is intentional.

  2. The “preservation” rule is described but not enforced.
    The docs/spec say a child contract preserves parent commitments “unless narrower inventory genuinely excludes them,” but the actual matching only checks that the child is a subset of the parent — nothing enforces preservation, and “genuinely excludes” has no concrete definition. As-is, one seller could trim the list freely and another couldn’t, and both would pass. Can we either define this precisely or drop the sentence so implementations don’t diverge?

  3. Fix the IAB citations (doc-only, but worth doing here).
    Two provenance claims are off: audible_video_complete is labeled “IAB type 500,” but in OpenRTB Native 1.2 the 500+ range is exchange-specific, not a defined event; and viewable_video_50 is described as requiring “audio on,” which type 4 doesn’t (audio-on is what distinguishes the audible metric). The enum values are fine — just the descriptions.

Smaller items:

The per-version VAST event rules are almost entirely untested — only loaded+4.0 is exercised. A wrong enum in any of the other branches would ship silently; a positive/negative vector per rule would close that.
Minor doc contradiction: package.json says pending items carry the digest unconditionally, while get-media-buys-response.json says “when present” — ID-less snapshots have no digest, so “when present” is the correct phrasing in both.
Heads-up that the new if publisher_domain → require format_option_id rule in canonical-format-option.json is an additional constraint beyond trackers; worth confirming no existing publisher_domain-only options would break under a minor bump.

Base automatically changed from creative-revision-identity to main August 25, 2026 10:42
…tract

# Conflicts:
#	docs/contributing/x-entity-annotation.md
#	docs/creative/canonical-formats.mdx
#	docs/creative/representation-sets.mdx
#	docs/creative/universal-macros.mdx
#	package.json
#	static/schemas/source/core/canonical-format-option.json
#	static/schemas/source/core/product-format-declaration.json
#	static/schemas/source/core/representation-destination.json
#	static/schemas/source/core/representation-rejection.json
#	static/schemas/source/core/representation-selection.json
#	static/schemas/source/core/x-entity-types.json
#	tests/mcp-schema-projection.test.cjs
#	tests/tracking-event-enums.test.cjs
@bokelley
bokelley requested a review from pkras August 25, 2026 11:31
@bokelley

Copy link
Copy Markdown
Contributor Author

Retargeted to main after #6781 merged and pushed clean ancestry reconciliation at f85d9e80ac.

Verification on the exact combined tree:

  • main is an ancestor; PR diff is limited to the intended 45 tracker-contract files
  • schema build: 106/106 bundles
  • focused MCP/tracker/package/representation/revision/event suite: 55/55
  • MCP contexts: media-buy 398 KiB (400 KiB budget), creative 405 KiB (410 KiB budget)
  • confidential-name scan and diff checks clean
  • local pre-push matrix passed schema/compliance authority, /signals, and required exact sales lifecycle gates; two unrelated existing creative storyboards (creative/native_in_feed, creative/policy_backed_rejections) hit the workstation's 120s isolation timeout, so the remaining local tenant pass was stopped rather than changing unrelated coverage. Required GitHub CI is running on the exact head.

All review threads remain resolved. Fresh CODEOWNERS approval is requested for this head.

@bokelley
bokelley enabled auto-merge (squash) August 25, 2026 11:31
@bokelley

Copy link
Copy Markdown
Contributor Author

CI follow-up: current main added a conformance-freshness gate that correctly found three uncatalogued versioned vector sets. Head 865848d6c1 catalogs creative-representation-resolution, macro-processing, and tracker-execution-contracts. The exact failing command (npm run test:doc-compliance-drift) now passes 21/21 including release instrumentation; docs navigation passes 43/43. Exact-head CI has restarted.

@bokelley

Copy link
Copy Markdown
Contributor Author

Final readiness update on f4df138c15:

  • Merged current main and resolved the vector-catalog conflict by retaining main’s macro/representation descriptions plus this PR’s tracker-contract catalog entry.
  • Split creative-operation format discovery from seller production authority: creative.supported_formats[].format preserves canonical shape and creative-route macro capabilities, while rejecting tracker contracts/digests.
  • Fixed the creative storyboard stack overflow by hoisting the repeated canonical AssetVariant union in generated bundles. The build_creative response bundle fell from ~21.3 MB to ~2.5 MB; the marker is stripped and validation is wire-equivalent.
  • Existing GA VAST/DAAST compatibility repairs remain intact. All review threads are resolved.

Local verification: 106/106 bundles; schema suite green (879 schemas, 34 schema checks, 160 contract tests, premium/geo suites); MCP projection 27/27; capability/hoist/tracker focused suites green; typecheck green; full /creative storyboard matrix 50/50 clean (209 passed, 0 failed). Three independent final expert reviews found no blocker/high issues. Auto-merge remains enabled pending exact-head CI and required human approval.

@bokelley

Copy link
Copy Markdown
Contributor Author

Exact-head hosted CI is now fully green (56 passed, 0 failed), with all review threads resolved and no new actionable comments. Auto-merge is enabled; the only remaining gate is the requested human/CODEOWNERS approval.

@bokelley

Copy link
Copy Markdown
Contributor Author

Addressed the latest human review in f4cad71b59.

  1. closeLinear / VAST 4.0: kept 4.0 excluded and documented the intentional gap. IAB's VAST 4.1 release notes say closeLinear was added back from VAST 3.0; the official 4.0 schema omits it. The test now exhaustively checks every supported VAST version, including 3.0 pass, 4.0 fail, and 4.1+ pass.
  2. Layer preservation: removed the undefined “genuinely excludes” rule. Derivation is now executable: complete parents require complete tuple-subset children; incomplete parents may add or omit tuples for narrower scope, remain incomplete, and omission stays undeclared.
  3. IAB provenance: corrected Native 1.2 semantics. Types 1–4 are the standardized mappings; type 4 has no audio requirement; 500+ is exchange-specific. audible_video_complete is explicitly AdCP-defined rather than mislabeled as IAB type 500.
  4. Version coverage: added exhaustive positive/negative coverage across all versions for skip/progress, fullscreen, exitFullscreen, 4.x interaction events, loaded, interactiveStart, and closeLinear.
  5. Package digest wording: both package schemas and verifier metadata now require an identical product_snapshot_digest only when present.
  6. Publisher identity: confirmed intentional and retained. The compact declaration is 3.2 prerelease-only, matches the existing Product {publisher_domain, format_option_id} identity rule, and has direct positive/negative regression coverage.

Verification on the pushed head: full schema suite green (879 schemas, 34 schema checks, 160 contract tests, premium 10/10, geo 6/6); focused tracker/package/compatibility tests green; compliance freshness and docs navigation 43/43 green. Two independent expert re-reviews found no remaining blocker/high issue.

Primary references: IAB VAST 4.1 release notes, IAB OpenRTB Native 1.2.

@bokelley

Copy link
Copy Markdown
Contributor Author

Exact-head CI is green after a targeted rerun of the one transient creative_lifecycle.preview_display fetch failure. The affected shard and /sales aggregate now pass without changing thresholds or code. All checks pass, all review threads remain resolved, and auto-merge is enabled pending the requested human/CODEOWNERS approval.

@lukasz-pubx

Copy link
Copy Markdown

Question: independent seller-side impression counting — where does the spec see it living?

This PR formalizes execution of buyer-supplied trackers, and deliberately excludes reporting credit ("This promises construction and handoff of the outbound action—not network delivery, HTTP success, attribution, or reporting credit"). I'd like to understand the intended path for the mirror-image case:

Consider a seller agent that wants log-level impression records of its own — to attest delivery on get_media_buy_delivery, reconcile against billing, or support billing_measurement variance resolution — but that operates under two real-world constraints:

  1. No guaranteed on-page presence. The agent represents publishers whose pages don't run any JS the agent controls, so client-side observation (GPT listeners, wrapper analytics) can't be assumed.
  2. No log-level access to the ad server. Many publisher ad-server arrangements expose only aggregate reporting (e.g., no Data Transfer entitlement), so the agent can't derive impression-level records from the ad server it fronts.

Under those constraints, the only mechanism that travels with the ad is a seller-injected tracker attached at trafficking/assembly time (a tracking URL set on the trafficked creative, or an <Impression> element added during VAST assembly). Three questions:

To be clear, none of this blocks this PR — the production contract for buyer trackers stands on its own. I'm trying to establish whether independent seller-side measurement is (a) intentionally protocol-invisible, (b) a 3.3 candidate, or (c) worth a fresh RFC.

@bokelley

Copy link
Copy Markdown
Contributor Author

Good framing — each question points at a real gap. My read on all three, leaning on what the protocol actually specifies vs. what it intentionally leaves to implementation:

1. Seller-injected trackers: out-of-spec, intentionally
This PR defines the buyer-to-seller production contract: the buyer declares trackers, the seller executes them. Seller-side impression counting through trafficking-time injection — a tracking URL attached to the creative, an <Impression> element added during VAST assembly — sits entirely within the seller's production pipeline. AdCP specifies the wire between agents, not how an agent instruments its own ad server. So the pattern is currently (a) intentionally protocol-invisible, not an oversight. Whether to acknowledge it normatively is a separate product question and a genuine 3.3 candidate, but the 3.2 scope boundary here is correct.

2. Seller-injected tracker disclosure: worth filing for 3.3
This is a real gap, and it sharpens once #6782 lands preview observation. A buyer observing a delivered payload will see beacons that appear in no declared manifest, with no way to distinguish "seller measurement infrastructure" from "manifest tampering." The primitives — how a seller declares production additions, what a readback surface should surface — aren't in scope here and weren't in #6207's original tracker-execution framing. This warrants its own issue, and connecting it to #6782's observation surface when filing would be the right framing. Would suggest (b) a 3.3 candidate rather than a current-spec gap.

3. Impression-level join key: re-file for 3.3
#6207 proposed impression_id: { minted_by, available_in_delivery } as a distinct substrate from the tracker execution contract. This PR closes #6207 on the tracker-contract side — the join-key/delivery-data plane was never in scope here and isn't covered by #6782 (preview observation) or #6784 (URL-slot selectors). It should be re-filed as its own 3.3 issue. Without it, seller-side impression reconciliation against get_media_buy_delivery is explicitly unspecified — which is consistent with where 3.2 lands, but the gap is real and should be tracked rather than lost.

Summary: (a) intentionally invisible now, (b) and (c) are both 3.3 candidates worth separate issues. Nothing changes the 3.2 scope here.


Triaged by Claude Code. Session: https://claude.ai/code/${CLAUDE_CODE_REMOTE_SESSION_ID}


Generated by Claude Code

@lukasz-pubx

Copy link
Copy Markdown

Thanks — that matches our read, and the follow-up is filed as #6878 (Measurement: declare seller-initiated trackers and impression-ID joinability). Mapping it to your three answers:

It also pre-empts the DR-0005 conformance-path objection that hit #6207's substituted_macros_in_trackers[] — happy to adjust scope there if the WG prefers the severable pieces split out.

@bokelley
bokelley disabled auto-merge August 25, 2026 19:08
@bokelley
bokelley merged commit ad899c2 into main Aug 25, 2026
80 of 81 checks passed
@bokelley
bokelley deleted the tracker-execution-contract branch August 25, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ladon/needs-human-review Ladon has escalated this PR for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: Measurement capability declaration (tracking_capabilities, attribution methodologies, TMP exposure data)

4 participants