feat(trusted-match): provider-declared TMPX macros + multi-chunk values#5729
Conversation
There was a problem hiding this comment.
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: experimentalis on the schema root (identity-match-response.json:16), sominoris sanctioned. Verified against the experimental contract, not assumed.TmpxMacro$defresolves:#/$defs/TmpxMacrois valid draft-07, defined once, referenced twice (roottmpx_macros+tmpx_providers.*.macros). Same$defspattern already passes CI in five other source schemas.maxItems: 2/minItems: 1cap is uniform across all three array sites (registrationtmpx_macros, responsetmpx_macros,tmpx_providers.*.macros). TheminItems: 1floor backs the normative "omit the provider rather than emit an emptymacros[]" rule in router-architecture.mdx.namepattern^[A-Z][A-Z0-9_]*$andmaxLength(64 name / 1024 value) match between the inline registration constraint and the sharedTmpxMacro.name. 1024 comfortably clears the deleted 255-char GAM budget.- No
oneOfadded — audit walker is untouched, no discriminator regression. - Schema-vs-docs coherence: the reshaped
tmpx_providersrow, the new TmpxMacro sub-table (name/valueboth required), and the Provider Registrationtmpx_macrosrow 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_idframing.provider-registration.json:11still describesprovider_idas "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 registeredtmpx_macroslist and MUST NOT be derived fromprovider_idat runtime. The{TMPX_<provider_id>}rationale is now contradicted by the model this PR establishes — the survivingtmpxfield 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:43asks for ~6 weeks' published notice before a breaking change to an experimental surface.tmpx_providersshipped 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)
- "Mutually exclusive per hop" is prose-only.
specification.mdxstatestmpx_macrosandtmpx_providersare mutually exclusive per hop, but the schema defines both as independent optionals with nonot/oneOf. Unlike the deliberatetmpx/tmpx_providerscoexistence, 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 populatestmpx_macros, router→publisher populatestmpx_providers") or add"not": {"required": ["tmpx_macros","tmpx_providers"]}.static/schemas/source/tmp/identity-match-response.json. - Unenforced "REQUIRED when …".
specification.mdx:345marks registrationtmpx_macros"Conditional / REQUIRED whenidentity_matchis true and the provider emits TMPX," but noif/thenenforces it — even though this file already uses that pattern forcountries/uid_types(provider-registration.json:79-89). "Emits TMPX" isn't a schema-visible predicate, so this is genuinely inexpressible without anemits_tmpxflag; acceptable, just flag the doc asserts an invariant nothing checks.
LGTM. Follow-ups noted below.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
- 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>
There was a problem hiding this comment.
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: experimentalat root (identity-match-response.json:15,provider-registration.json:6). That sanctions an in-3.x breaking reshape with aminorbump —ad-tech-protocol-expertconfirmsminoris correct perexperimental-status.mdx:33-47. Not amajor-without-changeset block. - Caps aligned across all three sites: registration
tmpx_macrosmaxItems 2, response roottmpx_macrosmaxItems 2,tmpx_providers[*].macrosmaxItems 2. Thenamepattern^[A-Z][A-Z0-9_]*$and maxLength 64 are identical in the registration items and$defs/TmpxMacro.name. $defs+#/$defs/TmpxMacroref is a sibling ofproperties/required— member order is irrelevant, and the same draft-07 self-pointer pattern already passes the suite incore/format.json.code-reviewer: clean.- No oneOf touched —
scripts/audit-oneof.mjsgate is not in play.tmpx_providers.additionalPropertiesis 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 registrationtmpx_macrosrow all match the schema shape.
Follow-ups (non-blocking — file as issues)
valuemaxLength 1024 is absent from the doc tables. The schema fixesmaxLength: 1024onTmpxMacro.value, butspecification.mdx:50-53describesvalueonly 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 thevaluerow.- Root
tmpx_macroson a router response is SHOULD-ignore, not MUST-NOT. The schema serves both hops withadditionalPropertiesopen at the root and no mutual-exclusivity guard. A sloppy router that leaks an upstream provider's roottmpx_macrosalongsidetmpx_providersgives 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 carrytmpx_macrosat the root." provider_id's charset rationale is now partly vestigial. With macro names no longer derived fromprovider_id, the only remaining justification for its^[A-Za-z0-9_]+$constraint is logs/metrics and the deprecated{TMPX}fallback.specification.mdx:69keeps the old "so{TMPX_<provider_id>}is a legal token" framing — worth a one-line trim.
Minor nits (non-blocking)
- No dual-shape alias. The experimental policy asks for an alias accepting both old and new forms "where feasible," and
additionalPropertiescouldoneOfthe 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.
- 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>
There was a problem hiding this comment.
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_providerscarriesx-status: experimental(identity-match-response.json:15) and the reshape fromMap<provider_id, string>(#5689, shipped in96a8c1d08) toMap<provider_id, {macros: [TmpxMacro]}>is exactly whatdocs/reference/experimental-status.mdx:31-47permits inside 3.x.minoris correct — the breaking notice rides in the changeset, not the semver bump;majoris reserved for the 4.0 removal oftmpx. 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
oneOfalias is waived against a window with no realistic adopters.ad-tech-protocol-expertconfirmed no test fixtures, examples, or TS consumers carry the v1 string shape. - Schema is structurally sound.
$defs/TmpxMacroresolves via local#/$defs/TmpxMacro(identity-match-response.json:111,129), thetmpx_providers.additionalPropertiesvalue is closed (required:["macros"],additionalProperties:false), andmaxItems:2is consistent across all three sites (response roottmpx_macros, per-providermacros, registrationtmpx_macros). No undiscriminatedoneOfintroduced — the audit walker stays green. - Boundary invariant is the strongest part. Provider→router carries
tmpx_macros[]at root; router→publisher carries the mergedtmpx_providersmap. The "router MUST NOT carrytmpx_macrosat root" rule is correctly identified as un-encodable (one schema, both hops,additionalProperties:true) and relocated to router-conformance prose inrouter-architecture.mdx:161andspecification.mdx. Fail-closed: a provider with no TMPX is omitted from the map, not represented by an emptymacros[]. - Schema-vs-docs coherence. Field tables in
specification.mdxmatch the schema —tmpx_macros, reshapedtmpx_providers, deprecatedtmpx, new TmpxMacro sub-table. Theprovider_iddescription 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 theMap<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 rewritetmp-tmpx-providers.mdto describe the final shape. (ad-tech-protocol-expertflagged 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)
TmpxMacro.namedoc omitsmaxLength. The sub-table inspecification.mdxdocuments the^[A-Z][A-Z0-9_]*$pattern but not the schema'smaxLength: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.
There was a problem hiding this comment.
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:15andprovider-registration.json:6carryx-status: experimental, andtrusted_match.coreis listed indocs/reference/experimental-status.mdx:112. Per that contract (lines 33-39), experimental surfaces MAY have field types changed inside 3.x — so theMap<provider_id,string>→Map<provider_id,{macros:[TmpxMacro]}>reshape does not require amajor. No spec-drift block here. - Schema mechanics.
$defs/TmpxMacroresolves cleanly alongside the rootallOf(same pattern asformat.json).additionalProperties:false,required:["macros"],minItems:1/maxItems:2are all internally consistent.minItems:1correctly forbids an emptymacros[], matching the prose rule "omit the provider rather than send empty." - Pattern parity.
provider-registration.jsontmpx_macros.items.pattern^[A-Z][A-Z0-9_]*$(max 64) exactly matchesTmpxMacro.nameinidentity-match-response.json:147-149.provider_iddeliberately 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_macrosrow inspecification.mdxmatch the schema field-by-field.router-architecture.mdx §Identity Match fan-outis 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 roottmpx_macrosandtmpx_providersdouble-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 fortrusted_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)
- TmpxMacro
namerow omits its maxLength.specification.mdx(TmpxMacro sub-table) documentsvalueas "Length capped at 1024 characters" but thenamerow lists only the pattern — the schema'smaxLength: 64onname(identity-match-response.json:149) isn't in the table. Asymmetric. Add "max length 64" to thenamerow 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.
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>
2a2a3ee to
c5f6178
Compare
bokelley
left a comment
There was a problem hiding this comment.
Approved after rebasing onto main, downgrading the TMPX changeset to patch for the experimental surface, and fixing the stale 3.0 docs nav entry.
There was a problem hiding this comment.
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_macrositems (^[A-Z][A-Z0-9_]*$, maxLength 64, minItems 1 / maxItems 2) exactly matchTmpxMacro.nameinidentity-match-response.json, and both deliberately differ fromprovider_id's^[A-Za-z0-9_]+$(macro names are uppercase-only, no leading digit). No drift. $defs/TmpxMacroplacement (identity-match-response.json$defs block, betweenpropertiesandrequired) is valid draft-07 — member order isn't significant and#/$defs/TmpxMacroresolves by JSON Pointer. Cosmetically off from house style (other source schemas put$defsafterrequired), non-blocking.- No oneOf regression. The reshape uses
additionalProperties: {type: object, required: [macros], additionalProperties: false}+ a$ref, not a union. The onlyanyOfin the touched files is the pre-existing context_match/identity_match constraint in provider-registration, untouched. Nothing foraudit-oneof.mjsto 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 decoupledmacros[].namemodel. The 10 other docs that mention TMPX reference only the singular{TMPX}macro, retained through 3.x. Clean. - Root-
tmpx_macrosleak invariant. "Router MUST NOT carrytmpx_macrosat 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 astrusted_match.verified_identity. - Empty-omission rule is consistent.
minItems: 1on bothtmpx_macrosandtmpx_providers[].macros, plus the prose "a non-emitting provider MUST be omitted rather than represented by an emptymacros[]," agree; the map itself has nominProperties, 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:2ispatch; 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 —minormatches the additive-plus-reshape reality. Changeset categorization, not a wire break. valuemaxLength rationale is muddled. TheTmpxMacro.valuecap 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..mdxwording 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)
docs.jsonnav removal is out of scope but correct. docs.json:126 dropsdist/docs/3.0.19/building/concepts/managing-response-size, unrelated to TMPX. It's a valid cleanup — the tracked 3.0.19 snapshot has nomanaging-response-size.mdx, so the entry was a dangling reference; the livedocs/building/concepts/managing-response-size.mdxis untouched. A one-line note in the PR body would save the next reviewer the reverse-engineering.- PR description schema paths are wrong. The body references
tmp/provider-registration.jsonandtmp/identity-match-response.json; the actual files are understatic/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.
Summary
Follow-up to #5689 — picks up the richer TMPX shape that was reverted before merge so the smaller
tmpx_providersmap win could ship first. The trafficking contract needs to communicate exact macro/value pairs (notprovider_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.Changes
static/schemas/source/tmp/provider-registration.json— addtmpx_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 whenidentity_matchis true and the provider emits TMPX.static/schemas/source/tmp/identity-match-response.json— two new fields and a sharedTmpxMacrodefinition ({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_providersreshaped fromMap<provider_id, string>(v1 from feat(trusted-match): tmpx_providers map + provider_id charset constraint #5689) toMap<provider_id, {macros: [TmpxMacro]}>. Router merges every provider'stmpx_macrosinto this map, preserving ordering and chunking.docs/trusted-match/specification.mdx—tmpx_macros, the reshapedtmpx_providers, and the deprecatedtmpx, with a new TmpxMacro sub-table. Adds a note that the provider-sidetmpx_macrosand router-mergedtmpx_providersare mutually exclusive per hop.tmpx_macrosrow (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).**TMPX macro trafficking**paragraph explains the registration → response → trafficking flow and why macro names MUST come from registration (not runtime-synthesized fromprovider_id).§Inventory-specific behaviorrewritten to walk the per-macro substitution against declared names.docs/trusted-match/router-architecture.mdx— TMPX-collection paragraph under§Identity Match fan-outrewritten to MUST the merge, mandate chunk-order preservation, explicitly forbid synthesizing macro names fromprovider_idat 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:For producers (identity agents), the v1 emitted nothing at the provider→router hop beyond the legacy
tmpxfield; v2 emitstmpx_macros[]with{name, value}pairs drawn from the registered list. Routers move from "copy provider'stmpxinto a map slot" to "copy provider'stmpx_macrosinto the per-providermacros[]."The legacy single-token
tmpxfield stays deprecated (still emitted by routers as a transitional convenience for consumers that haven't migrated; removed in 4.0). Whentmpx_providersandtmpxare both present,tmpx_providersis authoritative; the singular field SHOULD reflect one provider's first-slotvalue.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 passesnpm run lint:schema-links— cleannpm run test:docs-nav— clean for 3.1 / latest (one pre-existing failure underdist/docs/3.0.19/unrelated to this PR)🤖 Generated with Claude Code