Skip to content

feat(compliance): macro-identifier substitution storyboard (parked: Live Integration RFC)#5646

Draft
BaiyuScope3 wants to merge 1 commit into
mainfrom
BaiyuScope3/universal-macro-compliance
Draft

feat(compliance): macro-identifier substitution storyboard (parked: Live Integration RFC)#5646
BaiyuScope3 wants to merge 1 commit into
mainfrom
BaiyuScope3/universal-macro-compliance

Conversation

@BaiyuScope3

@BaiyuScope3 BaiyuScope3 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Parked / draft — do not merge yet.

This storyboard (macro-identifier-substitution.yaml) verifies a seller substitutes {MEDIA_BUY_ID} / {PACKAGE_ID} in creative tracking URLs. It was built against the expect_universal_macro_substituted assertion, which has since been pulled from @adcp/sdk in adcontextprotocol/adcp-client#2263.

Reason (per @bokelley's review on #2263): the assertion observed a build_creative preview, but those identifiers resolve at serve time and aren't serialized on the wire, so the check skip-dominates for conformant sellers. The substitution we actually want to certify is a decisioning roundtrip — a Live Integration check (RFC incoming). The macro-alignment logic from the SDK assertion is reusable on the collector side of that check.

Next step: rework this storyboard against the Live Integration surface once that RFC lands. Keeping it open as a tracking placeholder.

Related:

🤖 Generated with Claude Code

@mintlify

mintlify Bot commented Jun 19, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
adcp 🟢 Ready View Preview Jun 19, 2026, 8:39 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@bokelley

Copy link
Copy Markdown
Contributor

Took a careful pass over this + adcontextprotocol/adcp-client#2263 against the schemas. The helper and the guide are solid — the open question is the storyboard, and I think it's pointed at a surface that can't actually observe the behavior. Recommend a split.

Keep here: the SDK helper + guide

universal_macro_translation is exactly the right producer-side tool — it's what a seller calls at serve time to substitute and encode macros into the pixel. Its correctness is best enforced by unit tests against a shared golden fixture, not a storyboard, because the call is internal and never serialized on the wire (fixture note below). Two cheap guide additions:

  • State the value/native rule, don't just show it. Only build-time IDs ({MEDIA_BUY_ID}, {PACKAGE_ID}, {CREATIVE_ID}) may be value; every impression-time macro — privacy/consent ({GDPR_CONSENT}, {US_PRIVACY}, {GPP_STRING}), device, geo, {CACHEBUSTER}, {TIMESTAMP} — MUST be native. A value mapping for a consent macro freezes one user's consent string into every impression.
  • native is inserted raw → note the trust boundary (must come from trusted ad-server config, not buyer input).

Pull from here: macro-identifier-substitution.yaml + expect_universal_macro_substituted

Two reasons it can't do the job as a static (Wire Conformance) check:

  1. Wrong surface. build_creative is where macros are placed into the manifest to flow downstream — not where they resolve. The spec is explicit that {MEDIA_BUY_ID}/{PACKAGE_ID} are resolved by the sales agent at serve time (universal-macros.mdx:514-519; and the macro_values field itself: "Macros not provided here remain as {MACRO} placeholders for the sales agent to resolve at serve time"). A conformant build_creative output still carries the braces — so the current "expected" is actually the non-conformant state.
  2. Not wire-observable anyway. Nothing serializes the resolved tracker: create_media_buy returns IDs, sync_creatives requires only creative_id+action per item, get_*_delivery is aggregate metrics. The preview path is opt-in and skip-dominated (preview is in no required[]; default output_format is url = no inline HTML; and source_path: /creative_manifest/preview_html doesn't exist — preview_html lives at preview.previews[].renders[].preview_html).

Where the real check belongs

Verifying substitution means observing an actual impression — a decisioning roundtrip — which is a Live Integration concern, not Wire Conformance. It extends the live-sandbox concept to the serve surface: the harness owns an impression collector, drives a synthetic impression through the seller's sandbox, and asserts on the fired pixel. I'm drafting that as a separate RFC; it's a real infra/program decision (test-owned collectors + a sandbox impression-trigger), not a storyboard tweak.

Shared golden fixture (applies to #2263 too)

The helper is JS-only today; the Python SDK ships the full seller side with no equivalent and no unreserved encoder (filed: adcontextprotocol/adcp-client-python#956). Since conformance only ever exercises alphanumeric IDs (identical under any encoder), JS↔Python encoder drift would be invisible. A language-neutral test-vector file both SDKs validate against fixes that — I have a draft vector set (reserved-char encode / native-raw / drop-unmapped / no-re-expansion) and can PR it.

Net: ship helper + guide; pull the storyboard + assertion; the decisioning-roundtrip check comes as its own Live Integration RFC. Happy to pair on the split.

@bokelley

Copy link
Copy Markdown
Contributor

Checked both claims against the schemas/spec independently — both hold.

Wrong surface. build_creative's own macro_values field description (build_creative.mdx) says it plainly: "Macros not provided here remain as {MACRO} placeholders for the sales agent to resolve at serve time." The storyboard's build_with_macros step never sets macro_values, so a fully spec-conformant agent returns the braces, not the ids — the storyboard's "expected" is the non-conformant response.

Not wire-observable. source_path: "/creative_manifest/preview_html" doesn't exist on any schema — preview_html only appears nested under preview.previews[].renders[] in preview-render.json, gated behind include_preview: true + preview_output_format: "html" (default "url"). The storyboard's sample request sets neither, so even an agent that does substitute correctly and does support preview would skip-neutral on this assertion, not pass it.

Net: agreed on the split. Live Integration RFC is the right home for an impression-observable check.

I don't have push access to this branch (only claude/* branches), so rather than sit on it, here's the diff to apply — static/compliance/source/universal/macro-identifier-substitution.yaml and .changeset/macro-identifier-substitution.md removed outright (the latter per check-changeset-protocol-scope.cjs: once the storyboard's gone, nothing in this PR's diff against main is protocol-scoped anymore, so the gate wants the changeset file dropped, not kept empty — confirmed by running it locally), plus your two guide additions:

diff --git a/docs/creative/universal-macros.mdx b/docs/creative/universal-macros.mdx
index 15e93663..bd828751 100644
--- a/docs/creative/universal-macros.mdx
+++ b/docs/creative/universal-macros.mdx
@@ -569,6 +569,15 @@ Behavior:
   omit a tracker the agent can't fill than to emit a broken one.
 - **Already-minted parameters pass through untouched** — `pkg_id=123456` (no macro) is
   left exactly as-is.
+- **Map only build-time AdCP identifiers (`{MEDIA_BUY_ID}`, `{PACKAGE_ID}`,
+  `{CREATIVE_ID}`) as `value`** — every impression-time macro (privacy/consent macros
+  like `{GDPR_CONSENT}`, `{US_PRIVACY}`, `{GPP_STRING}`, plus device, geo,
+  `{CACHEBUSTER}`, and `{TIMESTAMP}`) needs `native` instead, since the real value
+  differs per impression. Mapping a consent macro to a fixed `value` would freeze one
+  user's consent string into every impression it's served.
+- **`native` strings are inserted without encoding or validation** — only point them at
+  ad-server config tokens you control, never at buyer-supplied input, since `native`
+  bypasses the RFC-3986 encoding that protects `value` entries from injection.

(Verified the build-time-ID vs. impression-time-macro split against the existing catalog tables in the same doc — "Common Macros" vs. "Privacy & Compliance"/"Device & Environment" — so this isn't introducing a new classification, just stating the rule the catalog already implies.)

npm run build:compliance and npm run typecheck both pass clean with the storyboard removed. Golden-fixture/Python-encoder (adcp-client-python#956) and the Live Integration RFC are your own follow-ups — nothing further needed here from this pass.

🤖 Generated with Claude Code


Generated by Claude Code

@bokelley

Copy link
Copy Markdown
Contributor

Applied the fix as three follow-up commits on this branch:

  1. 1940697 — dropped the now-orphaned .changeset/macro-identifier-substitution.md.
  2. 2f0bc68 — removed static/compliance/source/universal/macro-identifier-substitution.yaml. Confirmed independently: its expect_universal_macro_substituted step reads preview_html from /creative_manifest/preview_html, but that's not where build_creative's response exposes it (see build_creative.mdx and the preview-render.json schema) — which matches the CI signal of every "Storyboards (current /*)" job failing while "3.0-compat" passed.
  3. 3802f7f — added the two guide bullets to docs/creative/universal-macros.mdx under "Implementing translation with the SDK": map only build-time identifiers ({MEDIA_BUY_ID}, {PACKAGE_ID}, {CREATIVE_ID}) as value, everything impression-time as native, and a note that native is inserted unencoded so it should only point at ad-server tokens, never buyer-supplied input.

No protocol-scoped change ships once the storyboard is gone, so no changeset is needed — verified via check-changeset-protocol-scope.cjs.

Verified locally before pushing: npm run build:compliance, full typecheck + test suite, the storyboard matrix build on both current and 3.0-compat tracks, and the Mintlify broken-links check all pass clean. CI is re-running against the latest commit now; the @adcp/sdk dependency bump (adcontextprotocol/adcp-client#2263) remains the other open item before this can leave draft.


Generated by Claude Code

…red)

Seller-scoped storyboard asserting {MEDIA_BUY_ID}/{PACKAGE_ID} substitution in
creative tracking URLs. Parked pending the Live Integration check — the driving
assertion was pulled from @adcp/sdk (adcontextprotocol/adcp-client#2263) because a
build_creative preview can't certify serve-time substitution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@BaiyuScope3 BaiyuScope3 force-pushed the BaiyuScope3/universal-macro-compliance branch from 3802f7f to e664a1f Compare June 22, 2026 21:30
@BaiyuScope3 BaiyuScope3 changed the title feat(compliance): universal-macro substitution check + SDK helper guide feat(compliance): media-buy identifier substitution storyboard (parked — pending Live Integration check) Jun 22, 2026
@BaiyuScope3 BaiyuScope3 changed the title feat(compliance): media-buy identifier substitution storyboard (parked — pending Live Integration check) feat(compliance): macro-identifier substitution storyboard (parked: Live Integration RFC) Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants