Skip to content

feat(trusted-match): provider-declared TMPX macros + multi-chunk values#5729

Merged
bokelley merged 5 commits into
mainfrom
ohalushchak-exadel/tmp-tmpx-macros
Jun 28, 2026
Merged

feat(trusted-match): provider-declared TMPX macros + multi-chunk values#5729
bokelley merged 5 commits into
mainfrom
ohalushchak-exadel/tmp-tmpx-macros

Conversation

@ohalushchak-exadel

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #5689 — picks up the richer TMPX shape that was reverted before merge so the smaller tmpx_providers map win could ship first. The trafficking contract needs to communicate exact macro/value pairs (not provider_id → opaque token strings), because ad-server line items are configured against stable, provider-namespaced names ahead of time. The wire shape also needs to be ready for multi-chunk values when an opaque TMPX exceeds one macro slot.

⚠️ Breaking change to an experimental surface. tmpx_providers was just introduced in #5689 as Map<provider_id, string>. This PR reshapes it to Map<provider_id, {macros: [TmpxMacro]}>. Sanctioned by x-status: experimental on identity-match-response.json — see the experimental contract. Window between the two PRs is small, surface is experimental, but anyone who adopted the v1 shape needs to migrate.

Changes

  • static/schemas/source/tmp/provider-registration.json — add tmpx_macros: ordered list of stable ad-server macro names the provider's TMPX response fills. Pattern-validated (^[A-Z][A-Z0-9_]*$), capped at 2 entries in v1; cap MAY rise without a shape change. Required when identity_match is true and the provider emits TMPX.

  • static/schemas/source/tmp/identity-match-response.json — two new fields and a shared TmpxMacro definition ({name, value} — provider-namespaced name + opaque URL-safe value):

    • tmpx_macros[] (provider-emitted): ordered macro/value pairs the identity agent emits, names drawn from its registered list.
    • tmpx_providers reshaped from Map<provider_id, string> (v1 from feat(trusted-match): tmpx_providers map + provider_id charset constraint #5689) to Map<provider_id, {macros: [TmpxMacro]}>. Router merges every provider's tmpx_macros into this map, preserving ordering and chunking.
  • docs/trusted-match/specification.mdx

    • IdentityMatchResponse field table updated to list tmpx_macros, the reshaped tmpx_providers, and the deprecated tmpx, with a new TmpxMacro sub-table. Adds a note that the provider-side tmpx_macros and router-merged tmpx_providers are mutually exclusive per hop.
    • Provider Registration field table gains the tmpx_macros row (matching the schema this PR actually changes — addresses the cross-wire flagged in feat(trusted-match): tmpx_providers map + provider_id charset constraint #5689's bot review).
    • New **TMPX macro trafficking** paragraph explains the registration → response → trafficking flow and why macro names MUST come from registration (not runtime-synthesized from provider_id).
    • §Inventory-specific behavior rewritten to walk the per-macro substitution against declared names.
  • docs/trusted-match/router-architecture.mdx — TMPX-collection paragraph under §Identity Match fan-out rewritten to MUST the merge, mandate chunk-order preservation, explicitly forbid synthesizing macro names from provider_id at runtime, and explicitly forbid collapsing per-provider values into a single string.

  • Changeset — minor bump (adds new fields + reshapes an experimental field).

Migration from #5689's v1 shape

For a consumer that reads tmpx_providers["pinnacle_id"] and expects a string:

- const token = response.tmpx_providers[providerID];
+ const token = response.tmpx_providers[providerID].macros[0].value;

For producers (identity agents), the v1 emitted nothing at the provider→router hop beyond the legacy tmpx field; v2 emits tmpx_macros[] with {name, value} pairs drawn from the registered list. Routers move from "copy provider's tmpx into a map slot" to "copy provider's tmpx_macros into the per-provider macros[]."

The legacy single-token tmpx field stays deprecated (still emitted by routers as a transitional convenience for consumers that haven't migrated; removed in 4.0). When tmpx_providers and tmpx are both present, tmpx_providers is authoritative; the singular field SHOULD reflect one provider's first-slot value.

Examples use the fictional brand set

Per .agents/playbook.md, all worked examples use fictional brands: Pinnacle (PIN_TMPX_1, PIN_TMPX_2) and Nova (NOVA_TMPX_1).

Test plan

  • npm run test:schemas — full schema validation suite passes
  • npm run lint:schema-links — clean
  • npm run test:docs-nav — clean for 3.1 / latest (one pre-existing failure under dist/docs/3.0.19/ unrelated to this PR)
  • Pre-commit (unit / server unit / typecheck) passes locally
  • Spec preview renders the updated tables correctly
  • CI green on the PR

🤖 Generated with Claude Code

aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 26, 2026

@aao-release-bot aao-release-bot 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.

Clean fast-follow to #5689. Reshapes tmpx_providers into provider-namespaced macro/value pairs — the right shape, because ad-server line items are configured against stable registered names ahead of time, not runtime-synthesized from provider_id.

The breaking reshape of tmpx_providers (Map<provider_id, string>Map<provider_id, {macros: [TmpxMacro]}>) lands under a minor changeset, and that is correct: identity-match-response.json:16 carries x-status: experimental, and docs/reference/experimental-status.mdx:33-40 explicitly permits type changes to experimental fields inside 3.x. This is not a breaking-wire-change-without-major block — the escape valve is real and correctly invoked. A migration note ships in the PR body.

ad-tech-protocol-expert: sound-with-caveats. code-reviewer: clean. No MUST FIX category.

Things I checked

  • x-status: experimental is on the schema root (identity-match-response.json:16), so minor is sanctioned. Verified against the experimental contract, not assumed.
  • TmpxMacro $def resolves: #/$defs/TmpxMacro is valid draft-07, defined once, referenced twice (root tmpx_macros + tmpx_providers.*.macros). Same $defs pattern already passes CI in five other source schemas.
  • maxItems: 2 / minItems: 1 cap is uniform across all three array sites (registration tmpx_macros, response tmpx_macros, tmpx_providers.*.macros). The minItems: 1 floor backs the normative "omit the provider rather than emit an empty macros[]" rule in router-architecture.mdx.
  • name pattern ^[A-Z][A-Z0-9_]*$ and maxLength (64 name / 1024 value) match between the inline registration constraint and the shared TmpxMacro.name. 1024 comfortably clears the deleted 255-char GAM budget.
  • No oneOf added — audit walker is untouched, no discriminator regression.
  • Schema-vs-docs coherence: the reshaped tmpx_providers row, the new TmpxMacro sub-table (name/value both required), and the Provider Registration tmpx_macros row all match the schema. The registration-table row also closes the cross-wire flagged in #5689's bot review.
  • Changeset present, descriptively named, minor — matches additive fields + an experimental reshape.

Follow-ups (non-blocking — file as issues)

  • Stale provider_id framing. provider-registration.json:11 still describes provider_id as "the trailing token in the per-provider TMPX macro name {TMPX_<provider_id>}," and justifies the charset constraint #5689 added on exactly that basis. This PR's whole thesis is that macro names come from the registered tmpx_macros list and MUST NOT be derived from provider_id at runtime. The {TMPX_<provider_id>} rationale is now contradicted by the model this PR establishes — the surviving tmpx field carries the same old framing. The charset constraint can stay (it's harmless), but the prose justifying it is dead. Worth a cleanup pass so implementers don't read two macro-naming models in one spec.
  • Experimental notice window. experimental-status.mdx:43 asks for ~6 weeks' published notice before a breaking change to an experimental surface. tmpx_providers shipped in #5689 days ago, so a same-week reshape can't satisfy that literally — though since nobody could have adopted a field that didn't exist 6 weeks ago, the practical risk is near zero. Either way the contract is process-level; note it in release notes rather than block.

Minor nits (non-blocking)

  1. "Mutually exclusive per hop" is prose-only. specification.mdx states tmpx_macros and tmpx_providers are mutually exclusive per hop, but the schema defines both as independent optionals with no not/oneOf. Unlike the deliberate tmpx/tmpx_providers coexistence, this one is a normative claim the schema doesn't enforce. The same schema serves both hops, so it can't be expressed without splitting — soften the wording to descriptive ("by convention, provider→router populates tmpx_macros, router→publisher populates tmpx_providers") or add "not": {"required": ["tmpx_macros","tmpx_providers"]}. static/schemas/source/tmp/identity-match-response.json.
  2. Unenforced "REQUIRED when …". specification.mdx:345 marks registration tmpx_macros "Conditional / REQUIRED when identity_match is true and the provider emits TMPX," but no if/then enforces it — even though this file already uses that pattern for countries/uid_types (provider-registration.json:79-89). "Emits TMPX" isn't a schema-visible predicate, so this is genuinely inexpressible without an emits_tmpx flag; acceptable, just flag the doc asserts an invariant nothing checks.

LGTM. Follow-ups noted below.

@mintlify

mintlify Bot commented Jun 26, 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 26, 2026, 5:42 PM

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

ohalushchak-exadel added a commit that referenced this pull request Jun 26, 2026
- Stale `provider_id` framing (carried over from #5689): the field's
  description still justified its charset constraint by "trailing
  token in the per-provider TMPX macro name `{TMPX_<provider_id>}`,"
  which is the model this PR moves away from. Rewrite the prose in
  both provider-registration.json and the Provider Registration table
  to point at `tmpx_providers` keying and the registered `tmpx_macros`
  names, and explicitly state macro names MUST NOT be derived from
  `provider_id` at runtime. Keep the charset constraint — it's
  harmless and still useful for logs/metrics/legacy `{TMPX}`
  substitutions.

- Schema-vs-prose drift on mutual exclusivity: spec said `tmpx_macros`
  and `tmpx_providers` are "mutually exclusive per hop," but the
  schema defines both as independent optionals with no `not`/`oneOf`
  (the same schema serves both hops, so the exclusivity can't be
  expressed without splitting). Soften the wording to descriptive
  ("by convention, provider->router populates `tmpx_macros`,
  router->publisher populates `tmpx_providers`") and add a SHOULD on
  the consumer side instead of a normative MUST.

- Unenforced "REQUIRED when ...": the registration `tmpx_macros` row
  asserted "REQUIRED when `identity_match` is true and the provider
  emits TMPX," but "emits TMPX" isn't a schema-visible predicate, so
  no `if`/`then` could enforce it. Drop the "Conditional / REQUIRED
  when ..." framing, restate as a normative invariant in prose
  ("a provider that emits TMPX MUST also register this list"), and
  explicitly call out that the schema cannot enforce this.

- Experimental notice window: experimental-status.mdx asks for ~6
  weeks' published notice before a breaking change to an experimental
  surface. `tmpx_providers` shipped days ago, so the literal window
  can't apply; note the situation in the changeset rather than block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 26, 2026

@aao-release-bot aao-release-bot 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.

Clean reshape of the experimental TMPX surface. Right call to communicate exact {name, value} macro pairs instead of provider_id → opaque token — trafficking is configured against registered slot names ahead of time, so deriving macro names from provider_id at runtime was always the wrong shape.

Things I checked

  • Both schemas carry x-status: experimental at root (identity-match-response.json:15, provider-registration.json:6). That sanctions an in-3.x breaking reshape with a minor bump — ad-tech-protocol-expert confirms minor is correct per experimental-status.mdx:33-47. Not a major-without-changeset block.
  • Caps aligned across all three sites: registration tmpx_macros maxItems 2, response root tmpx_macros maxItems 2, tmpx_providers[*].macros maxItems 2. The name pattern ^[A-Z][A-Z0-9_]*$ and maxLength 64 are identical in the registration items and $defs/TmpxMacro.name.
  • $defs + #/$defs/TmpxMacro ref is a sibling of properties/required — member order is irrelevant, and the same draft-07 self-pointer pattern already passes the suite in core/format.json. code-reviewer: clean.
  • No oneOf touchedscripts/audit-oneof.mjs gate is not in play. tmpx_providers.additionalProperties is a closed object (required:["macros"], additionalProperties:false), not a new undiscriminated union.
  • Changeset present (.changeset/tmp-tmpx-macros.md, minor) with a migration sketch and an honest accounting of why the 6-week notice can't literally apply to a field that shipped days ago.
  • Schema-vs-docs: the IdentityMatchResponse table (Map<string, { macros: List<TmpxMacro> }>), the TmpxMacro sub-table, and the registration tmpx_macros row all match the schema shape.

Follow-ups (non-blocking — file as issues)

  • value maxLength 1024 is absent from the doc tables. The schema fixes maxLength: 1024 on TmpxMacro.value, but specification.mdx:50-53 describes value only as an "opaque URL-safe wire string" with no cap. A publisher sizing GAM key-value slots reads the table, not the JSON. Add the cap to the value row.
  • Root tmpx_macros on a router response is SHOULD-ignore, not MUST-NOT. The schema serves both hops with additionalProperties open at the root and no mutual-exclusivity guard. A sloppy router that leaks an upstream provider's root tmpx_macros alongside tmpx_providers gives the publisher no schema signal for which to read — double-firing the same value into one slot corrupts the per-provider accounting this PR exists to protect. Harden the narrative to "a router response MUST NOT carry tmpx_macros at the root."
  • provider_id's charset rationale is now partly vestigial. With macro names no longer derived from provider_id, the only remaining justification for its ^[A-Za-z0-9_]+$ constraint is logs/metrics and the deprecated {TMPX} fallback. specification.mdx:69 keeps the old "so {TMPX_<provider_id>} is a legal token" framing — worth a one-line trim.

Minor nits (non-blocking)

  1. No dual-shape alias. The experimental policy asks for an alias accepting both old and new forms "where feasible," and additionalProperties could oneOf the legacy string and the new object during transition. Defensible to skip given the v1 shape is hours old, but call the waiver out in release notes rather than leaving it in the changeset body.

The cross-schema rule "emits TMPX ⇒ MUST register tmpx_macros" is correctly documented as router-conformance rather than pretended into the schema — draft-07 can't express a cross-document conditional, and the honest split matches house style.

LGTM. Follow-ups noted below. Ship once CI confirms npm run test:schemas green.

ohalushchak-exadel added a commit that referenced this pull request Jun 26, 2026
- TmpxMacro `value` doc row gains the 1024-char maxLength the schema
  fixes — sizing guidance now matches between schema and prose.

- Root `tmpx_macros` on a router response is hardened from
  SHOULD-ignore to MUST-NOT. The schema can't enforce it (one schema
  serves both hops), so the rule lives as a router-conformance
  invariant in both specification.mdx (the IdentityMatchResponse
  narrative) and router-architecture.mdx (the TMPX-collection
  paragraph). Closes the failure mode where a leaked upstream
  `tmpx_macros` next to `tmpx_providers` would give the publisher no
  schema signal for which to read and risk double-firing the same
  value into one slot.

- Vestigial `provider_id` charset rationale trimmed. The
  `{TMPX_<provider_id>}` justification no longer applies (macro names
  come from the registered `tmpx_macros` list, not from `provider_id`),
  so the description now points at the surviving justification —
  log/metric/dashboard safety — without restating the dead model.
  Same trim in provider-registration.json and the Provider
  Registration table.

- Changeset gains a dual-shape-alias waiver: the experimental policy
  asks for `oneOf` aliasing of the old + new shapes "where feasible";
  skipped here because the v1 shape is hours old and the dual-shape
  carrying cost (validator complexity, ambiguous consumer paths,
  perpetual deprecation tail) outweighs the migration cost. The
  deprecated singular `tmpx` field already covers the belt-and-
  suspenders path for any caller that did read v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aao-release-bot[bot]
aao-release-bot Bot previously approved these changes Jun 26, 2026

@aao-release-bot aao-release-bot 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.

Clean experimental reshape. Right call to model the trafficking contract as declared {name, value} pairs instead of provider_id→opaque-token — ad-server line items are configured against stable macro names ahead of time, so synthesizing names from provider_id at runtime was always the wrong shape.

Things I checked

  • Experimental contract sanctions the break. tmpx_providers carries x-status: experimental (identity-match-response.json:15) and the reshape from Map<provider_id, string> (#5689, shipped in 96a8c1d08) to Map<provider_id, {macros: [TmpxMacro]}> is exactly what docs/reference/experimental-status.mdx:31-47 permits inside 3.x. minor is correct — the breaking notice rides in the changeset, not the semver bump; major is reserved for the 4.0 removal of tmpx. Not a block.
  • 6-week-notice + alias waivers are reasoned, not hand-waved. Field is days old, so the literal 6-week window can't bind; the dual-shape oneOf alias is waived against a window with no realistic adopters. ad-tech-protocol-expert confirmed no test fixtures, examples, or TS consumers carry the v1 string shape.
  • Schema is structurally sound. $defs/TmpxMacro resolves via local #/$defs/TmpxMacro (identity-match-response.json:111,129), the tmpx_providers.additionalProperties value is closed (required:["macros"], additionalProperties:false), and maxItems:2 is consistent across all three sites (response root tmpx_macros, per-provider macros, registration tmpx_macros). No undiscriminated oneOf introduced — the audit walker stays green.
  • Boundary invariant is the strongest part. Provider→router carries tmpx_macros[] at root; router→publisher carries the merged tmpx_providers map. The "router MUST NOT carry tmpx_macros at root" rule is correctly identified as un-encodable (one schema, both hops, additionalProperties:true) and relocated to router-conformance prose in router-architecture.mdx:161 and specification.mdx. Fail-closed: a provider with no TMPX is omitted from the map, not represented by an empty macros[].
  • Schema-vs-docs coherence. Field tables in specification.mdx match the schema — tmpx_macros, reshaped tmpx_providers, deprecated tmpx, new TmpxMacro sub-table. The provider_id description was updated to drop the stale {TMPX_<provider_id>} synthesized-macro model on both schema and docs. No surviving drift in names, types, or required/optional.
  • No dist regeneration owed. static/schemas/source/** only; dist/schemas/<version>/ is release-commit territory.

Follow-ups (non-blocking — file as issues)

  • Two pending changesets now describe the same field two different ways. .changeset/tmp-tmpx-providers.md (from #5689, still unreleased — not yet in CHANGELOG) describes the Map<provider_id, string> shape, the {TMPX_<provider_id>} synthesized-macro model, and lists "provider-declared macro names plus multi-chunk... as a follow-up." This PR ships that follow-up and deletes that model. At the next version bump both aggregate into one CHANGELOG entry that contradicts itself. Reconcile before release: fold both into one changeset, or rewrite tmp-tmpx-providers.md to describe the final shape. (ad-tech-protocol-expert flagged this; it's a release-narrative issue, not wire drift.)
  • Test plan has two unchecked boxes — "Spec preview renders the updated tables correctly" and "CI green on the PR." Neither is the primary wire change (the schema reshape is validated by test:schemas, which is checked), so non-blocking — but confirm the rendered TmpxMacro sub-table before merge since it's hand-authored MDX.

Minor nits (non-blocking)

  1. TmpxMacro.name doc omits maxLength. The sub-table in specification.mdx documents the ^[A-Z][A-Z0-9_]*$ pattern but not the schema's maxLength:64 (identity-match-response.json:149). Add it for parity.

Approving on the strength of the experimental-contract grounding plus the clean provider→router→publisher boundary modeling. Reconcile the two changesets before the version cut.

@aao-release-bot aao-release-bot 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.

Clean reshape of an experimental surface, and the migration story is honest. One thing to resolve before this merges: the changeset and the PR description disagree on the bump level, and the file is on the wrong side of that disagreement.

Things I checked

  • The break is sanctioned. Both static/schemas/source/tmp/identity-match-response.json:15 and provider-registration.json:6 carry x-status: experimental, and trusted_match.core is listed in docs/reference/experimental-status.mdx:112. Per that contract (lines 33-39), experimental surfaces MAY have field types changed inside 3.x — so the Map<provider_id,string>Map<provider_id,{macros:[TmpxMacro]}> reshape does not require a major. No spec-drift block here.
  • Schema mechanics. $defs/TmpxMacro resolves cleanly alongside the root allOf (same pattern as format.json). additionalProperties:false, required:["macros"], minItems:1/maxItems:2 are all internally consistent. minItems:1 correctly forbids an empty macros[], matching the prose rule "omit the provider rather than send empty."
  • Pattern parity. provider-registration.json tmpx_macros.items.pattern ^[A-Z][A-Z0-9_]*$ (max 64) exactly matches TmpxMacro.name in identity-match-response.json:147-149. provider_id deliberately keeps the looser ^[A-Za-z0-9_]+$ — correct, since under v2 it's no longer the macro name.
  • Wire/docs coherence. The IdentityMatchResponse table, the new TmpxMacro sub-table, and the provider-registration tmpx_macros row in specification.mdx match the schema field-by-field. router-architecture.mdx §Identity Match fan-out is rewritten consistently. The 2-chunk cap (maxItems:2) is stated identically in all three places, and raising it later is genuinely non-breaking.

What flips this to approve

Changeset level: patch should be minor. .changeset/tmp-tmpx-macros.md:8 reads patch, but docs/reference/versioning.mdx:164 classifies "a new optional field" as Minor, and lines 190-192 state "Patches never change schema — no new fields, no renamed fields, no new enum values." This PR adds tmpx_macros on two schemas, reshapes tmpx_providers, and mints a TmpxMacro $def. x-status: experimental relaxes the stability contract (what may change + notice window); it does not lower the semver classification of the release carrying the change — there's no carve-out for that in the experimental doc or .changeset/config.json. Precedent agrees: #5689, which shipped the very field being reshaped, was cut as minor. And the PR body's "Changes" section literally claims "Changeset — minor bump." The file contradicts the author's own stated intent. ad-tech-protocol-expert: the patch level is the one unsound element — fix to minor.

Follow-ups (non-blocking — file as issues)

  • Dual-hop unenforceable invariant. One schema serves both provider→router and router→publisher hops, with a MUST-drop-tmpx_macros-at-root rule that the schema cannot enforce and no test backs. Both reviewers flagged this as the weakest design point — a publisher that sees both root tmpx_macros and tmpx_providers double-fires the same value. Acceptable for an experimental surface, but it should not survive into a stable one. Recommend tracking "split the dual-hop schema" as a graduation blocker for trusted_match.core.
  • #5689's deferral gate. #5689 explicitly deferred provider-declared macro names + multi-chunk as "gated on routers actually hitting size-budget truncation in production." Worth confirming that production-truncation signal now exists, since #5689 made it the gate for exactly this change.

Minor nits (non-blocking)

  1. TmpxMacro name row omits its maxLength. specification.mdx (TmpxMacro sub-table) documents value as "Length capped at 1024 characters" but the name row lists only the pattern — the schema's maxLength: 64 on name (identity-match-response.json:149) isn't in the table. Asymmetric. Add "max length 64" to the name row for table/schema parity.

Flip to approve once .changeset/tmp-tmpx-macros.md reads minor — matching versioning.mdx, the #5689 precedent, and this PR's own description.

ohalushchak-exadel and others added 5 commits June 28, 2026 20:09
Picks up the redesign that was reverted from #5689 ahead of merge to
ship the smaller "tmpx_providers map" win first. The trafficking
contract has to communicate exact macro/value pairs, not provider_id
→ opaque string, because the ad-server line items (GAM key-values,
VAST URL macros, DOOH play-log fields) are configured against stable,
provider-namespaced names ahead of time — synthesizing macro names
from provider_id at runtime breaks that operational model. The wire
shape also needs to be ready for multi-chunk TMPX values when an
opaque token exceeds one macro slot.

Schema:

- `tmp/provider-registration.json` adds `tmpx_macros`: ordered list
  of stable ad-server macro names this provider's TMPX response fills.
  Pattern `^[A-Z][A-Z0-9_]*$`, capped at 2 entries in v1; the cap MAY
  rise without a shape change. Required when identity_match is true
  and the provider emits TMPX.

- `tmp/identity-match-response.json` adds two fields and a shared
  `TmpxMacro` definition (`{name, value}` — provider-namespaced name,
  opaque URL-safe value):
  * `tmpx_macros[]` (provider-emitted): ordered macro/value pairs the
    identity agent emits, names drawn from its registered list.
  * `tmpx_providers` reshaped from `Map<provider_id, string>` to
    `Map<provider_id, {macros: [TmpxMacro]}>`. Router merges every
    provider's `tmpx_macros` into this map, preserving ordering and
    chunking.

BREAKING CHANGE TO AN EXPERIMENTAL SURFACE (sanctioned by
`x-status: experimental` on identity-match-response.json):
`tmpx_providers` was introduced in #5689 as a Map<provider_id, string>
(opaque token per provider) and is reshaped here. Consumers that
adopted the v1 shape between #5689 and this change MUST migrate to
read each provider's `macros[*].value` rather than a single string.
The window between the two PRs is small and the surface is
experimental, so the migration cost is bounded.

The legacy single-token `tmpx` field stays deprecated; routers MAY
populate it with one provider's first-slot value for back-compat,
with `tmpx_providers` authoritative when both are present. Removed
in 4.0.

Spec narrative updated to match: IdentityMatchResponse and Provider
Registration field tables surface the new fields and the reshape;
the IdentityMatchResponse table also clarifies that the provider-side
`tmpx_macros` and the router-merged `tmpx_providers` are mutually
exclusive per hop. New `**TMPX macro trafficking**` paragraph + a new
`TmpxMacro` sub-table walk the registration → response → trafficking
flow. `§Inventory-specific behavior` rewritten to point at the
declared macro names (not provider_id-derived). `router-architecture.mdx`
TMPX-collection paragraph rewritten to MUST the merge, preserve chunk
order, forbid synthesizing macro names from provider_id, and forbid
collapsing per-provider values into a single string.

All worked examples use the fictional brand set per `.agents/playbook.md`
(Pinnacle → PIN_TMPX_1, Nova → NOVA_TMPX_1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Stale `provider_id` framing (carried over from #5689): the field's
  description still justified its charset constraint by "trailing
  token in the per-provider TMPX macro name `{TMPX_<provider_id>}`,"
  which is the model this PR moves away from. Rewrite the prose in
  both provider-registration.json and the Provider Registration table
  to point at `tmpx_providers` keying and the registered `tmpx_macros`
  names, and explicitly state macro names MUST NOT be derived from
  `provider_id` at runtime. Keep the charset constraint — it's
  harmless and still useful for logs/metrics/legacy `{TMPX}`
  substitutions.

- Schema-vs-prose drift on mutual exclusivity: spec said `tmpx_macros`
  and `tmpx_providers` are "mutually exclusive per hop," but the
  schema defines both as independent optionals with no `not`/`oneOf`
  (the same schema serves both hops, so the exclusivity can't be
  expressed without splitting). Soften the wording to descriptive
  ("by convention, provider->router populates `tmpx_macros`,
  router->publisher populates `tmpx_providers`") and add a SHOULD on
  the consumer side instead of a normative MUST.

- Unenforced "REQUIRED when ...": the registration `tmpx_macros` row
  asserted "REQUIRED when `identity_match` is true and the provider
  emits TMPX," but "emits TMPX" isn't a schema-visible predicate, so
  no `if`/`then` could enforce it. Drop the "Conditional / REQUIRED
  when ..." framing, restate as a normative invariant in prose
  ("a provider that emits TMPX MUST also register this list"), and
  explicitly call out that the schema cannot enforce this.

- Experimental notice window: experimental-status.mdx asks for ~6
  weeks' published notice before a breaking change to an experimental
  surface. `tmpx_providers` shipped days ago, so the literal window
  can't apply; note the situation in the changeset rather than block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TmpxMacro `value` doc row gains the 1024-char maxLength the schema
  fixes — sizing guidance now matches between schema and prose.

- Root `tmpx_macros` on a router response is hardened from
  SHOULD-ignore to MUST-NOT. The schema can't enforce it (one schema
  serves both hops), so the rule lives as a router-conformance
  invariant in both specification.mdx (the IdentityMatchResponse
  narrative) and router-architecture.mdx (the TMPX-collection
  paragraph). Closes the failure mode where a leaked upstream
  `tmpx_macros` next to `tmpx_providers` would give the publisher no
  schema signal for which to read and risk double-firing the same
  value into one slot.

- Vestigial `provider_id` charset rationale trimmed. The
  `{TMPX_<provider_id>}` justification no longer applies (macro names
  come from the registered `tmpx_macros` list, not from `provider_id`),
  so the description now points at the surviving justification —
  log/metric/dashboard safety — without restating the dead model.
  Same trim in provider-registration.json and the Provider
  Registration table.

- Changeset gains a dual-shape-alias waiver: the experimental policy
  asks for `oneOf` aliasing of the old + new shapes "where feasible";
  skipped here because the v1 shape is hours old and the dual-shape
  carrying cost (validator complexity, ambiguous consumer paths,
  perpetual deprecation tail) outweighs the migration cost. The
  deprecated singular `tmpx` field already covers the belt-and-
  suspenders path for any caller that did read v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the ohalushchak-exadel/tmp-tmpx-macros branch from 2a2a3ee to c5f6178 Compare June 28, 2026 19:46

@bokelley bokelley 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.

Approved after rebasing onto main, downgrading the TMPX changeset to patch for the experimental surface, and fixing the stale 3.0 docs nav entry.

@aao-release-bot aao-release-bot 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.

Sound reshape of an experimental surface. The {name, value} macro-pair model is the right shape: ad-server line items are configured against stable registered names ahead of serve time, so emitting the name alongside the value — and decoupling it from provider_id synthesis (#5689's {TMPX_<provider_id>}) — is what makes the trafficking pre-configurable instead of runtime-guessed.

The breaking reshape of tmpx_providers (Map<provider_id, string>Map<provider_id, {macros: [TmpxMacro]}>) does not require a major changeset. docs/reference/experimental-status.mdx:33-39 explicitly permits type changes to experimental surfaces inside 3.x; both schemas carry x-status: experimental and trusted_match.core is on the canonical experimental list. patch is not a "breaking-without-major" violation here. The notice/alias waiver in the changeset is defensible — you cannot give six weeks' notice for a field that's existed for days, so the rule is vacuously satisfied, and the migration note is present.

Things I checked

  • Cross-schema constraint parity. provider-registration tmpx_macros items (^[A-Z][A-Z0-9_]*$, maxLength 64, minItems 1 / maxItems 2) exactly match TmpxMacro.name in identity-match-response.json, and both deliberately differ from provider_id's ^[A-Za-z0-9_]+$ (macro names are uppercase-only, no leading digit). No drift.
  • $defs/TmpxMacro placement (identity-match-response.json $defs block, between properties and required) is valid draft-07 — member order isn't significant and #/$defs/TmpxMacro resolves by JSON Pointer. Cosmetically off from house style (other source schemas put $defs after required), non-blocking.
  • No oneOf regression. The reshape uses additionalProperties: {type: object, required: [macros], additionalProperties: false} + a $ref, not a union. The only anyOf in the touched files is the pre-existing context_match/identity_match constraint in provider-registration, untouched. Nothing for audit-oneof.mjs to catch.
  • Stale-reference sweep. All six pre-PR {TMPX_<provider_id>} references (router-architecture.mdx:161; specification.mdx:211, 216, 328, 703, 705) fall entirely inside replaced hunks; the new text consistently uses the decoupled macros[].name model. The 10 other docs that mention TMPX reference only the singular {TMPX} macro, retained through 3.x. Clean.
  • Root-tmpx_macros leak invariant. "Router MUST NOT carry tmpx_macros at the root, schema can't enforce it because the same schema serves both hops" is a real but acceptable single-schema-two-hops gap, correctly demoted to a router-conformance MUST and honestly flagged as unenforceable in both the spec and router-architecture.mdx. Same pattern as trusted_match.verified_identity.
  • Empty-omission rule is consistent. minItems: 1 on both tmpx_macros and tmpx_providers[].macros, plus the prose "a non-emitting provider MUST be omitted rather than represented by an empty macros[]," agree; the map itself has no minProperties, so an empty map (nobody emitted) stays valid. Coherent.

Follow-ups (non-blocking — file as issues)

  • Changeset severity is internally inconsistent. .changeset/tmp-tmpx-macros.md:2 is patch; the changeset body and the PR description both say "minor bump." Under the experimental contract the bump level is essentially decorative for this surface, but pick one — minor matches the additive-plus-reshape reality. Changeset categorization, not a wire break.
  • value maxLength rationale is muddled. The TmpxMacro.value cap is 1024 in both schema and the spec table — they agree, so no schema/docs divergence. But the table prose justifies 1024 as "comfortably above the 255-char GAM key-value limit," which a single 1024-char value can't satisfy in a GAM slot — that's what the 2-chunk model is for. Tighten the prose to say whether 1024 is a per-chunk or per-assembled-token bound. .mdx wording only.
  • Spec-preview render box unchecked. Test plan leaves "[ ] Spec preview renders the updated tables correctly" unchecked. npm run test:schemas (checked) covers the wire correctness; the new TmpxMacro sub-table and reshaped rows are the secondary presentation surface — worth an eyeball before this goes to a published release.

Minor nits (non-blocking)

  1. docs.json nav removal is out of scope but correct. docs.json:126 drops dist/docs/3.0.19/building/concepts/managing-response-size, unrelated to TMPX. It's a valid cleanup — the tracked 3.0.19 snapshot has no managing-response-size.mdx, so the entry was a dangling reference; the live docs/building/concepts/managing-response-size.mdx is untouched. A one-line note in the PR body would save the next reviewer the reverse-engineering.
  2. PR description schema paths are wrong. The body references tmp/provider-registration.json and tmp/identity-match-response.json; the actual files are under static/schemas/source/trusted-match/. Doesn't affect the diff.

code-reviewer: no blockers. ad-tech-protocol-expert: sound-with-caveats — experimental sanction confirmed, no oneOf regression, wire model is the right shape.

LGTM. Follow-ups noted below.

@bokelley bokelley merged commit 7c237fc into main Jun 28, 2026
32 checks passed
@bokelley bokelley deleted the ohalushchak-exadel/tmp-tmpx-macros branch June 28, 2026 20:00
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