From 76a373437aef2f618732116e30b6fccbcf0ade98 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 26 Oct 2026 23:38:31 +0000 Subject: [PATCH 1/2] docs(governance): propose DR-0032 and DR-0033 (parked pending #8501) Split out of #8410. DR-0032 (SDKs translate between minor releases; mutations refuse lossy conversion; reduced account keys stay one-to-one) and DR-0033 (every minor-release change states its pathway back), with the LADON.md and constitution rules that enforce DR-0033. Both stay proposed: WG Session C (2026-10-26) parked them until #8501 is settled. Co-Authored-By: Claude Opus 5.5 (1M context) --- .agents/wg/constitution.md | 6 +++ LADON.md | 21 ++++++++ ...DR-0032-sdks-translate-between-releases.md | 51 +++++++++++++++++++ ...every-minor-change-has-a-downgrade-path.md | 39 ++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 governance/decisions/DR-0032-sdks-translate-between-releases.md create mode 100644 governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md diff --git a/.agents/wg/constitution.md b/.agents/wg/constitution.md index d5dfdb7725d..75230b43e69 100644 --- a/.agents/wg/constitution.md +++ b/.agents/wg/constitution.md @@ -78,6 +78,12 @@ Every desk enforces these regardless of surface: policy-wide decision, never a per-variant edit ([DR-0009]). - Graded conformance assertions require a normative basis — a spec MUST or a schema `required[]` field, never scenario prose ([DR-0001]). +- Minor releases are non-breaking and backward compatible: every change + targeting a minor states its pathway to the previous minor of the same major + (droppable, moves, refused, or identity-bearing), on 3.x now and on 4.x from + 4.1 ([DR-0033]). SDKs translate between + releases and refuse lossy mutations rather than sending a different meaning + ([DR-0032]). - When the schema-literal reading and shipping SDK behavior diverge on wire shape, the SDK is canon; codify, don't migrate ([DR-0008]). Wire shape only — not semantic contracts. diff --git a/LADON.md b/LADON.md index de89fa3c67d..dd05a89f1fd 100644 --- a/LADON.md +++ b/LADON.md @@ -78,6 +78,27 @@ algorithms) change. A clarification is patch-eligible only when the prior spec was demonstrably ambiguous *and* any conformant 3.1.0 implementation would already satisfy the new MUST. "It's just docs" does not apply to normative docs. +### Mandatory: every minor-release change has a pathway back + +Per DR-0033, a PR that changes `static/schemas/source/**` or normative docs for +a minor release (3.x now, and each later major's own minors from 4.1 onward) +must state, for each new or changed field, enum value, task, union variant, +requiredness or semantic rule, how it maps to the previous minor release of the +same major: + +- droppable (and, for a mutating request, whether dropping changes what the + seller commits to); +- moves (with the mapping); +- refused (with the error); or +- identity-bearing (Breaking, with the reduction and migration rule from + DR-0032). + +Look in the PR body, the changeset, or the docs migration page. A minor-release +change with no stated pathway is `high`. A stated pathway with no cross-version +conformance case and no tracked follow-up is `medium`. A mutation-side field +declared "droppable" whose loss would change spend, targeting, caps or account +identity is `high`, because it is a refusal mislabeled as droppable. + ### Mandatory: cite governance precedent When a PR decides a question of protocol policy or convention rather than diff --git a/governance/decisions/DR-0032-sdks-translate-between-releases.md b/governance/decisions/DR-0032-sdks-translate-between-releases.md new file mode 100644 index 00000000000..86e1d66eb25 --- /dev/null +++ b/governance/decisions/DR-0032-sdks-translate-between-releases.md @@ -0,0 +1,51 @@ +--- +id: DR-0032 +title: SDKs translate between minor releases of a major; mutations refuse lossy conversion; reduced account keys stay one-to-one +class: normative +status: proposed +date: 2026-10-22 +refs: ["#8373", "#8501", "#8502", "adcontextprotocol/adcp-client#3370", "adcontextprotocol/adcp-client#3372", "DR-0024"] +dissent: none recorded +--- + +> **Parked (WG Session C, 2026-10-26).** Not ratified. This waits on #8501 (closed schemas gaining fields in minor releases), following an objection from @KonstantinMirin. The WG has since adopted the rule that a minor release never adds or removes a property on a closed schema, and #8502 enforces it in CI. That narrows how much translation SDKs need, so this record will be revised against the outcome of #8501 before it is put up for ratification again. + +## Decision + +**Application code speaks one release.** An AdCP SDK lets an application build requests and read responses in the newest release the SDK supports. The SDK negotiates the highest release both sides support, per counterpart and per call, and converts between that release and the application's, one consecutive minor at a time within the major (3.2 ↔ 3.1 ↔ 3.0, chained; likewise 4.1 ↔ 4.0). This holds for client SDKs (convert requests down, responses up) and server frameworks (convert requests up, responses down, as [DR-0024] requires of responses). + +**Every loss is reported.** A conversion that removes or rewrites a field reports it where the application can see it, with the field's JSON path and the two releases. + +**Reads drop; mutations refuse.** +- On a read, a field the older release cannot express is removed and reported. +- On a mutation, the SDK removes only fields that do not change what the counterpart commits to: labels and hints (a buy's `name`, `pacing`, creative rotation hints). If any other field would be lost (a cap, a targeting exclusion, a bidding mode, any field of an update), the SDK refuses the call before sending, with a typed `UNSUPPORTED_FEATURE`-class error. +- A field with an exact older equivalent is moved, not dropped. Moves are defined by the shared conformance cases (see below), never by one SDK. + +**A task the older release does not define** is refused before sending with a typed error, unless the counterpart lists the tool. + +**Account keys.** When the older release's account natural key cannot express fields of the buyer's key (3.2's `operator_unit`, `currency`, `timezone` and `brand.countries`), the SDK may send the older, reduced key only when the reduction is one-to-one: +- It records which reduced key each buyer key reached the seller with. +- It refuses a second buyer key that would reach the same seller account, and refuses a request that sends that reduced key unchanged on behalf of another buyer key. +- It never merges two buyer accounts into one seller account. + +**When the seller upgrades**, a reduced account keeps being addressed by its reduced key (or by `account_id`) until the buyer migrates it explicitly: +- An `operator_unit`-only reduction is rekeyed in place with a `sync_accounts` settings update (`account`, `operator_identity`, `revision`). +- An account reduced by `currency`, `timezone` or `brand.countries` cannot change those on an existing account, so the buyer opens a new account under the full key and decides which key keeps the history. +- `account_id` addressing is the recommended steady state for accounts first opened across a version gap. + +**Shared conformance cases.** Every consecutive minor pair has cross-version cases in the compliance bundle. They cover each direction for both client and server: input, expected output, expected drops, or expected refusal. Every SDK runs them. + +## Rationale + +Without this, each buyer and seller rebuilds the same translation, inconsistently: pinning outbound clients to old releases, reducing account keys by hand, and branching handlers on the served release. Dropping fields from a mutation is the dangerous case. A buy sent without its budget cap or exclusion still succeeds, and spends differently from what was asked. Refusing is the only conversion that cannot misspend. + +Sharing a reduced account key looks harmless but merges billing terms, spend, reporting and read access across buyer accounts, for example two buying desks. Encoding the dropped fields into `brand_id` invents brand identities the house never published. A one-to-one reduction keeps account counts honest without either cost, and fails closed exactly where the older release cannot tell keys apart. + +## Implications + +- Updates the "SDK consequences" section of `specs/version-negotiation.md`: auto-downshift is the default, and it is reported rather than silent. +- Supersedes nothing. [DR-0024]'s rule that responses conform to the served release is how the server side of this decision is graded. Its unpinned-request clause is addressed separately in DR-0034. +- The conformance cases are a follow-up deliverable. Until they exist, SDKs document their moves and refusals. +- Not settled here: a wire signal telling the counterpart that a conversion withheld something. Logs and callbacks on the converting side suffice for now. + +[DR-0024]: ./DR-0024-responses-conform-to-served-release.md diff --git a/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md b/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md new file mode 100644 index 00000000000..55a5c0c87fa --- /dev/null +++ b/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md @@ -0,0 +1,39 @@ +--- +id: DR-0033 +title: Every minor-release change states its pathway to the previous minor of the same major +class: normative +status: proposed +date: 2026-10-22 +refs: ["#8373", "#8501", "#8502", "DR-0032", "DR-0024"] +dissent: none recorded +--- + +> **Parked (WG Session C, 2026-10-26).** Not ratified. This waits on #8501 (closed schemas gaining fields in minor releases), following an objection from @KonstantinMirin. The WG has since adopted the rule that a minor release never adds or removes a property on a closed schema, and #8502 enforces it in CI. That narrows how much translation SDKs need, so this record will be revised against the outcome of #8501 before it is put up for ratification again. + +## Decision + +Every Normative or Breaking change targeting a minor release states, in the PR that introduces it, how it maps to the previous minor release of the same major: 3.3 to 3.2, and later 4.1 to 4.0. A minor release is non-breaking *and* backward compatible: an SDK on the new minor must still be able to reach a counterpart on any earlier minor of its major. That covers each new or changed field, enum value, task, union variant, requiredness and semantic rule. The statement takes exactly one of these forms: + +1. **Droppable.** Older counterparts never see it, and nothing else changes. For a request field on a mutating task, say also whether dropping it changes what the seller commits to; if it does, the downgrade is a refusal (form 3). +2. **Moves.** It has an exact equivalent in the previous release (a renamed field, a restructured block, a narrower enum). Give the mapping, both directions where both exist. +3. **Refused.** The previous release cannot express it, and an SDK must refuse rather than send a different meaning. Name the error. +4. **Identity-bearing.** It changes account identity, resource identity or idempotency keys. Treat it as Breaking, and state the reduction and migration rule (see [DR-0032]). + +New tasks keep using the manifest's `added_in` and `legacy_fallback`. The pathway also covers the reverse direction: how a server reads the previous release's payload in the new shape. + +Each stated pathway ships with its cross-version conformance case, or with a tracked follow-up when the conformance harness for that pair is not ready. + +Reviewers (Ladon and the expert seats) treat a minor-release schema or normative-doc change with no stated pathway as a `high` finding. + +## Rationale + +SDKs can only translate what the spec has defined how to translate. When a change ships without a pathway, every SDK invents one, and they disagree on exactly the calls that move money. Asking at design time costs one paragraph; reverse-engineering it after release cost the 3.2 cycle an account-identity break ([DR-0032]) and a set of response shapes older validators reject ([DR-0024]). + +## Implications + +- Applies to every change targeting a minor release from adoption onward, on 3.x now and on every later major's own minor cycle (4.1 maps to 4.0, 4.2 to 4.1). Existing 3.2 and 3.3 changes are backfilled through the conformance cases in [DR-0032]. +- A change whose only honest pathway is "refused" is still allowed. The record makes the cost visible, so reviewers can weigh the additive alternative the constitution prefers. +- A major release (4.0) is the one place breaking changes land, and it is out of scope: cross-major bridging stays `VERSION_UNSUPPORTED` by design. Its own minors are in scope from 4.1 onward. + +[DR-0024]: ./DR-0024-responses-conform-to-served-release.md +[DR-0032]: ./DR-0032-sdks-translate-between-releases.md From 0a25302d0f0fdd73540f8784b080d663c1ab93f3 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 2 Nov 2026 10:57:00 +0000 Subject: [PATCH 2/2] docs(governance): revise DR-0032 and DR-0033 against DR-0036 #8501 settled on DR-0036 (a minor adds only to open schemas). Translation now addresses the silent-ignore hazard: newer fields go only to peers that negotiated them, reads drop and report, mutations refuse a lost field that changes the commitment, moves follow DR-0033 pathway statements, closed and digest-bound objects are never reshaped, and the 3.2 account-key change reduces one-to-one. DR-0033's pathway forms become ignorable, gated, moves or a DR-0036 correction; identity changes wait for a major. LADON.md and the constitution invariant follow. Co-Authored-By: Claude Opus 5.5 --- .agents/wg/constitution.md | 7 ++- LADON.md | 22 ++++---- ...DR-0032-sdks-translate-between-releases.md | 53 +++++++++---------- ...every-minor-change-has-a-downgrade-path.md | 33 ++++++------ 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/.agents/wg/constitution.md b/.agents/wg/constitution.md index 75230b43e69..361fe0d5a48 100644 --- a/.agents/wg/constitution.md +++ b/.agents/wg/constitution.md @@ -80,10 +80,9 @@ Every desk enforces these regardless of surface: schema `required[]` field, never scenario prose ([DR-0001]). - Minor releases are non-breaking and backward compatible: every change targeting a minor states its pathway to the previous minor of the same major - (droppable, moves, refused, or identity-bearing), on 3.x now and on 4.x from - 4.1 ([DR-0033]). SDKs translate between - releases and refuse lossy mutations rather than sending a different meaning - ([DR-0032]). + (ignorable, gated, moves, or a DR-0036 correction), on 3.x now and on 4.x + from 4.1 ([DR-0033]). SDKs translate between releases and refuse a mutation + whose lost field would change what the seller commits to ([DR-0032]). - When the schema-literal reading and shipping SDK behavior diverge on wire shape, the SDK is canon; codify, don't migrate ([DR-0008]). Wire shape only — not semantic contracts. diff --git a/LADON.md b/LADON.md index dd05a89f1fd..3b84ee642cd 100644 --- a/LADON.md +++ b/LADON.md @@ -86,18 +86,22 @@ must state, for each new or changed field, enum value, task, union variant, requiredness or semantic rule, how it maps to the previous minor release of the same major: -- droppable (and, for a mutating request, whether dropping changes what the - seller commits to); -- moves (with the mapping); -- refused (with the error); or -- identity-bearing (Breaking, with the reduction and migration rule from - DR-0032). +- **ignorable**: on an open schema, and a peer on the previous minor can + ignore it without committing to anything different; +- **gated**: on an open schema, but a peer that ignores it would commit to + something different, with the refusal SDKs apply on a mutation and any + capability flag senders check; +- **moves**: an exact equivalent in the previous minor, with the mapping; or +- **correction**: a DR-0036 correction, with the spec text it restores. Look in the PR body, the changeset, or the docs migration page. A minor-release change with no stated pathway is `high`. A stated pathway with no cross-version -conformance case and no tracked follow-up is `medium`. A mutation-side field -declared "droppable" whose loss would change spend, targeting, caps or account -identity is `high`, because it is a refusal mislabeled as droppable. +conformance case and no tracked follow-up is `medium`. A field declared +"ignorable" whose loss would change spend, targeting, caps, bidding, creative +activation, measurement or account settings is `high`, because it is a gated +change mislabeled as ignorable. An addition to a closed schema that DR-0036 +doesn't list as a correction, or a change to account or resource identity, is +`high` in a minor: it waits for the next major. ### Mandatory: cite governance precedent diff --git a/governance/decisions/DR-0032-sdks-translate-between-releases.md b/governance/decisions/DR-0032-sdks-translate-between-releases.md index 86e1d66eb25..8fa92ee6478 100644 --- a/governance/decisions/DR-0032-sdks-translate-between-releases.md +++ b/governance/decisions/DR-0032-sdks-translate-between-releases.md @@ -1,51 +1,50 @@ --- id: DR-0032 -title: SDKs translate between minor releases of a major; mutations refuse lossy conversion; reduced account keys stay one-to-one +title: SDKs negotiate a release per counterpart and translate between the minors of a major; a mutation that would lose meaning is refused class: normative status: proposed date: 2026-10-22 -refs: ["#8373", "#8501", "#8502", "adcontextprotocol/adcp-client#3370", "adcontextprotocol/adcp-client#3372", "DR-0024"] +refs: ["#8373", "#8501", "#8433", "DR-0024", "DR-0034", "DR-0035", "DR-0036", "adcontextprotocol/adcp-client#3372", "adcontextprotocol/adcp-client#3376"] dissent: none recorded --- -> **Parked (WG Session C, 2026-10-26).** Not ratified. This waits on #8501 (closed schemas gaining fields in minor releases), following an objection from @KonstantinMirin. The WG has since adopted the rule that a minor release never adds or removes a property on a closed schema, and #8502 enforces it in CI. That narrows how much translation SDKs need, so this record will be revised against the outcome of #8501 before it is put up for ratification again. +> **Revised 2026-11-02 against DR-0036.** First proposed on 2026-10-22 and parked at WG Session C pending #8501. #8501 settled on DR-0036: a minor adds only to open schemas. This revision narrows the record to what translation still has to do under that rule. ## Decision -**Application code speaks one release.** An AdCP SDK lets an application build requests and read responses in the newest release the SDK supports. The SDK negotiates the highest release both sides support, per counterpart and per call, and converts between that release and the application's, one consecutive minor at a time within the major (3.2 ↔ 3.1 ↔ 3.0, chained; likewise 4.1 ↔ 4.0). This holds for client SDKs (convert requests down, responses up) and server frameworks (convert requests up, responses down, as [DR-0024] requires of responses). +**Application code speaks one release.** An AdCP SDK lets an application build requests and read responses in the newest release the SDK supports. The SDK picks the release to speak with each counterpart, per call: the highest release both sides support, from the counterpart's `supported_versions`. A prerelease matches only itself. A server serves a request that names only the major per DR-0034. The SDK converts between that release and the application's one consecutive minor at a time within the major (3.3 ↔ 3.2 ↔ 3.1, chained; likewise 4.1 ↔ 4.0). -**Every loss is reported.** A conversion that removes or rewrites a field reports it where the application can see it, with the field's JSON path and the two releases. +**What translation covers.** Under DR-0036, a minor adds only to open schemas, so an older peer's validator accepts a newer field and then ignores it. The hazard is no longer a rejection but a silent ignore: a seller that ignores a cap or an exclusion spends differently from what was asked. Translation handles that hazard: -**Reads drop; mutations refuse.** -- On a read, a field the older release cannot express is removed and reported. -- On a mutation, the SDK removes only fields that do not change what the counterpart commits to: labels and hints (a buy's `name`, `pacing`, creative rotation hints). If any other field would be lost (a cap, a targeting exclusion, a bidding mode, any field of an update), the SDK refuses the call before sending, with a typed `UNSUPPORTED_FEATURE`-class error. -- A field with an exact older equivalent is moved, not dropped. Moves are defined by the shared conformance cases (see below), never by one SDK. +- **Newer fields go only to peers that negotiated the newer release.** A client removes them from a request before sending it to an older seller. A server removes them from a response served at an older release, as DR-0024 requires and DR-0036 states for the reopened proposal flow. +- **Moves.** Where a change's pathway statement (DR-0033) gives an exact equivalent in the older release, the SDK moves the value instead of removing it, in both directions. +- **Reads drop.** On a read, a removed field is reported and the call proceeds. The result may be broader than asked; the application sees why. +- **Mutations refuse.** On a mutating request, a field may be removed only if losing it leaves what the counterpart commits to unchanged: a label or a hint. If removing a field would change spend, targeting, caps, bidding, creative activation, measurement or account identity, the SDK refuses the call before sending, with a typed `UNSUPPORTED_FEATURE`-class error that names the field and the release that introduced it. +- **Every change is reported** where the application can see it, with the field's JSON path and the two releases. +- **Up-conversion never removes.** A client converting an older response up, and a server converting an older request up, apply moves only. +- **Tasks the negotiated release does not define** are refused before sending, unless the counterpart lists the tool. -**A task the older release does not define** is refused before sending with a typed error, unless the counterpart lists the tool. +**Closed and digest-bound objects are never reshaped.** A closed schema changes within a major only by a DR-0036 correction, which accepts what the spec text already required and so needs no translation. An object that a digest signs, such as `commercial-terms` under `terms_digest`, is never projected: if it cannot be represented in the served release, the resource is refused with `VERSION_UNSUPPORTED` rather than stripped. -**Account keys.** When the older release's account natural key cannot express fields of the buyer's key (3.2's `operator_unit`, `currency`, `timezone` and `brand.countries`), the SDK may send the older, reduced key only when the reduction is one-to-one: -- It records which reduced key each buyer key reached the seller with. -- It refuses a second buyer key that would reach the same seller account, and refuses a request that sends that reduced key unchanged on behalf of another buyer key. -- It never merges two buyer accounts into one seller account. +**Account keys changed before DR-0036 reduce one-to-one.** 3.2 added `operator_unit`, `currency` and `timezone` to the account natural key, and `countries` to the brand inside it, on schemas 3.1 published closed. That change shipped and stays until 4.0 (DR-0036 records earlier cases instead of undoing them). For a peer on 3.1 or earlier, the SDK may send the older, reduced key only while the reduction is one-to-one: -**When the seller upgrades**, a reduced account keeps being addressed by its reduced key (or by `account_id`) until the buyer migrates it explicitly: -- An `operator_unit`-only reduction is rekeyed in place with a `sync_accounts` settings update (`account`, `operator_identity`, `revision`). -- An account reduced by `currency`, `timezone` or `brand.countries` cannot change those on an existing account, so the buyer opens a new account under the full key and decides which key keeps the history. -- `account_id` addressing is the recommended steady state for accounts first opened across a version gap. +- It records which reduced key each buyer key reached the seller with, and a key sent unreduced claims itself. +- It refuses, before sending, any second buyer key that would reach the same seller account, in either order, and checks every key in a request before recording any. +- After the seller upgrades, the reduced key stays in use until the buyer migrates the account. An `operator_unit`-only reduction is rekeyed in place with a `sync_accounts` settings update. A reduction by `currency`, `timezone` or `brand.countries` needs a new account under the full key, because those can't change on an existing account. `account_id` addressing is the recommended steady state for accounts opened across a version gap. -**Shared conformance cases.** Every consecutive minor pair has cross-version cases in the compliance bundle. They cover each direction for both client and server: input, expected output, expected drops, or expected refusal. Every SDK runs them. +Reducing one-to-one is the default. An SDK may offer refusing instead. Future changes to account identity can't happen in a minor under DR-0036; they wait for a major. + +**Shared conformance cases.** Each consecutive minor pair has cross-version cases in the compliance bundle (#8433), covering client and server in each direction, with an exact expected output, the reported changes, or the expected refusal. Every SDK runs them. ## Rationale -Without this, each buyer and seller rebuilds the same translation, inconsistently: pinning outbound clients to old releases, reducing account keys by hand, and branching handlers on the served release. Dropping fields from a mutation is the dangerous case. A buy sent without its budget cap or exclusion still succeeds, and spends differently from what was asked. Refusing is the only conversion that cannot misspend. +Without this, every buyer and seller rebuilds the same translation inconsistently: pinning outbound clients to old releases, reducing account keys by hand, and branching handlers on the served release. DR-0036 removes the worst failure, older validators rejecting newer payloads, but it turns the remaining one into a quieter one. An older peer now ignores what it doesn't know. On a read that is harmless and visible. On a mutation it means the seller commits to something other than what the buyer asked, so refusing is the only conversion that can't misspend. -Sharing a reduced account key looks harmless but merges billing terms, spend, reporting and read access across buyer accounts, for example two buying desks. Encoding the dropped fields into `brand_id` invents brand identities the house never published. A one-to-one reduction keeps account counts honest without either cost, and fails closed exactly where the older release cannot tell keys apart. +A one-to-one reduction keeps account counts honest without merging two buyer accounts into one seller account or inventing brand identities, and it fails closed exactly where the older release can't tell keys apart. ## Implications -- Updates the "SDK consequences" section of `specs/version-negotiation.md`: auto-downshift is the default, and it is reported rather than silent. -- Supersedes nothing. [DR-0024]'s rule that responses conform to the served release is how the server side of this decision is graded. Its unpinned-request clause is addressed separately in DR-0034. -- The conformance cases are a follow-up deliverable. Until they exist, SDKs document their moves and refusals. -- Not settled here: a wire signal telling the counterpart that a conversion withheld something. Logs and callbacks on the converting side suffice for now. - -[DR-0024]: ./DR-0024-responses-conform-to-served-release.md +- **SDK consequences in `specs/version-negotiation.md` are updated** when this is ratified: auto-downshift is the default and is reported, not silent. +- **Reference implementation:** `@adcp/sdk` (adcontextprotocol/adcp-client#3372, #3376). The Python SDK shipped its translation in adcontextprotocol/adcp-client-python#1559. Go and Java track it in adcontextprotocol/adcp-go#588 and adcontextprotocol/adcp-sdk-java#47. +- **The conformance cases** in #8433 cover 3.2 ↔ 3.1 and the settled 3.3 ↔ 3.2 surfaces. 3.3 ↔ 3.2 is completed when 3.3 reaches GA and its closed-schema dispositions land. +- **Not settled here:** a wire signal telling the counterpart that a conversion withheld something. Reports on the converting side suffice for now. diff --git a/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md b/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md index 55a5c0c87fa..918744f4d0b 100644 --- a/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md +++ b/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md @@ -4,36 +4,37 @@ title: Every minor-release change states its pathway to the previous minor of th class: normative status: proposed date: 2026-10-22 -refs: ["#8373", "#8501", "#8502", "DR-0032", "DR-0024"] +refs: ["#8373", "#8501", "DR-0024", "DR-0032", "DR-0036"] dissent: none recorded --- -> **Parked (WG Session C, 2026-10-26).** Not ratified. This waits on #8501 (closed schemas gaining fields in minor releases), following an objection from @KonstantinMirin. The WG has since adopted the rule that a minor release never adds or removes a property on a closed schema, and #8502 enforces it in CI. That narrows how much translation SDKs need, so this record will be revised against the outcome of #8501 before it is put up for ratification again. +> **Revised 2026-11-02 against DR-0036.** First proposed on 2026-10-22 and parked at WG Session C pending #8501. DR-0036 now forbids additions to closed schemas within a major, so the "identity-bearing" form is gone: such a change waits for a major. ## Decision -Every Normative or Breaking change targeting a minor release states, in the PR that introduces it, how it maps to the previous minor release of the same major: 3.3 to 3.2, and later 4.1 to 4.0. A minor release is non-breaking *and* backward compatible: an SDK on the new minor must still be able to reach a counterpart on any earlier minor of its major. That covers each new or changed field, enum value, task, union variant, requiredness and semantic rule. The statement takes exactly one of these forms: +A minor release is non-breaking **and** backward compatible: an SDK on the new minor must still be able to reach a counterpart on any earlier minor of its major. So every Normative or Breaking change targeting a minor states, in the PR that introduces it, how it maps to the previous minor of the same major: 3.3 to 3.2 now, and 4.1 to 4.0 later. That covers each new or changed field, enum value, task, union variant, requiredness and semantic rule. -1. **Droppable.** Older counterparts never see it, and nothing else changes. For a request field on a mutating task, say also whether dropping it changes what the seller commits to; if it does, the downgrade is a refusal (form 3). -2. **Moves.** It has an exact equivalent in the previous release (a renamed field, a restructured block, a narrower enum). Give the mapping, both directions where both exist. -3. **Refused.** The previous release cannot express it, and an SDK must refuse rather than send a different meaning. Name the error. -4. **Identity-bearing.** It changes account identity, resource identity or idempotency keys. Treat it as Breaking, and state the reduction and migration rule (see [DR-0032]). +The statement takes exactly one of these forms: -New tasks keep using the manifest's `added_in` and `legacy_fallback`. The pathway also covers the reverse direction: how a server reads the previous release's payload in the new shape. +1. **Ignorable.** The change is on an open schema, and a peer on the previous minor can ignore it without committing to anything different: a label, a hint, a description, or a read filter whose absence only broadens results. SDKs drop it for older peers and report the drop (DR-0032). +2. **Gated.** The change is on an open schema, and a peer that ignores it would commit to something different: it limits or changes spend, targeting, caps, bidding, creative activation, measurement or account settings. Name the refusal SDKs apply on a mutation, and the capability flag, if any, a sender checks before sending it. +3. **Moves.** It has an exact equivalent in the previous minor: a renamed field, a restructured block, a narrower enum. Give the mapping in both directions where both exist. +4. **Correction.** It is a DR-0036 correction to a closed schema. Name the spec text it restores. A correction accepts only what that text already required, so it needs no translation. -Each stated pathway ships with its cross-version conformance case, or with a tracked follow-up when the conformance harness for that pair is not ready. +New tasks keep using the manifest's `added_in` and `legacy_fallback`; SDKs refuse a task the counterpart's release doesn't define (DR-0032). + +**Not allowed in a minor:** additions to or removals from a closed schema other than a recorded correction (DR-0036), and changes to account or resource identity or to idempotency keys. Those wait for the next major. + +Each stated pathway ships with its cross-version conformance case, or with a tracked follow-up when the case for that pair isn't ready. Reviewers (Ladon and the expert seats) treat a minor-release schema or normative-doc change with no stated pathway as a `high` finding. ## Rationale -SDKs can only translate what the spec has defined how to translate. When a change ships without a pathway, every SDK invents one, and they disagree on exactly the calls that move money. Asking at design time costs one paragraph; reverse-engineering it after release cost the 3.2 cycle an account-identity break ([DR-0032]) and a set of response shapes older validators reject ([DR-0024]). +SDKs can only translate what the spec has defined how to translate. When a change ships without a pathway, every SDK invents one, and they disagree on exactly the calls that move money. DR-0036 makes an older peer ignore new fields rather than reject them, which makes the gated case the one that matters: without a statement, nobody knows whether a silently ignored field changes what was bought. Stating the pathway at design time costs one paragraph. Reverse-engineering it after release cost the 3.2 cycle an account-identity break (DR-0032) and the 3.3 cycle a triage of 57 fields (#8501). ## Implications -- Applies to every change targeting a minor release from adoption onward, on 3.x now and on every later major's own minor cycle (4.1 maps to 4.0, 4.2 to 4.1). Existing 3.2 and 3.3 changes are backfilled through the conformance cases in [DR-0032]. -- A change whose only honest pathway is "refused" is still allowed. The record makes the cost visible, so reviewers can weigh the additive alternative the constitution prefers. -- A major release (4.0) is the one place breaking changes land, and it is out of scope: cross-major bridging stays `VERSION_UNSUPPORTED` by design. Its own minors are in scope from 4.1 onward. - -[DR-0024]: ./DR-0024-responses-conform-to-served-release.md -[DR-0032]: ./DR-0032-sdks-translate-between-releases.md +- Applies to every change targeting a minor release from adoption onward, on 3.x now and on each later major's own minors from 4.1. A major release (4.0) is where breaking changes land, and cross-major bridging stays `VERSION_UNSUPPORTED`. +- The 3.3 dispositions from #8501 (moves, deferrals, corrections) serve as the pathway statements for 3.3's closed-schema changes. The remaining 3.3 changes state their pathway before GA. +- A change whose only honest pathway is "gated" is still allowed. The statement makes the cost visible, so reviewers can weigh an ignorable design the constitution prefers.