diff --git a/.agents/wg/constitution.md b/.agents/wg/constitution.md index d5dfdb7725d..361fe0d5a48 100644 --- a/.agents/wg/constitution.md +++ b/.agents/wg/constitution.md @@ -78,6 +78,11 @@ 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 + (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 de89fa3c67d..3b84ee642cd 100644 --- a/LADON.md +++ b/LADON.md @@ -78,6 +78,31 @@ 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: + +- **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 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 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..8fa92ee6478 --- /dev/null +++ b/governance/decisions/DR-0032-sdks-translate-between-releases.md @@ -0,0 +1,50 @@ +--- +id: DR-0032 +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", "#8433", "DR-0024", "DR-0034", "DR-0035", "DR-0036", "adcontextprotocol/adcp-client#3372", "adcontextprotocol/adcp-client#3376"] +dissent: none recorded +--- + +> **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 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). + +**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: + +- **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. + +**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 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: + +- 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. + +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, 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. + +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 + +- **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 new file mode 100644 index 00000000000..918744f4d0b --- /dev/null +++ b/governance/decisions/DR-0033-every-minor-change-has-a-downgrade-path.md @@ -0,0 +1,40 @@ +--- +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", "DR-0024", "DR-0032", "DR-0036"] +dissent: none recorded +--- + +> **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 + +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. + +The statement takes exactly one of these forms: + +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. + +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. 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 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.