Skip to content

feat(media-buy): structure proposal refinement negotiation - #6547

Merged
bokelley merged 12 commits into
mainfrom
address-lucas-comments
Aug 15, 2026
Merged

feat(media-buy): structure proposal refinement negotiation#6547
bokelley merged 12 commits into
mainfrom
address-lucas-comments

Conversation

@bokelley

@bokelley bokelley commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the unreleased instructions field with the shared negotiation noun ask
  • add mechanically verifiable proposal constraints — total_budget, cpm, impressions, and flight — plus product include/omit changes and requested alternative counts
  • return plural draft revisions with keyed unsatisfied constraints and machine-readable partial/unable reason codes, including hold_unavailable and batch_aborted for atomic finalize failures
  • advertise typed proposal-refinement dimensions (total_budget, cpm, impressions, flight, product_changes, alternatives, criteria) separately from free-text negotiation competence; undeclared dimensions MUST fail task-level pre-mutation with a registered error-details shape
  • require parent_proposal_id lineage on every refinement successor and buyer-recomputable terms_digest semantics (RFC 8785 JCS + sha256), with alternative distinctness defined on commercial_terms
  • add normative negotiation-security and hold-governance guidance: ask is untrusted input, deterministic constraint responses form a price oracle worth rate-limiting, holds are seller-gated with seller-chosen expiry, and only commercial_terms is contractual
  • document the relationship between compact refine_proposals and legacy get_products refinement, including the soft-filter vs hard-constraint budget_range inversion adapters must convert

Why

The compact proposal lifecycle introduced by #6115 had only free-text revision semantics. Deterministic buyers could not express hard boundaries or distinguish commercial refusal from unsupported or uninterpreted requests. This implements the structured negotiation contract discussed in #6450 while preserving immutable proposal snapshots and keeping preferences and diversity strategy in free text.

A four-way pre-merge red team (protocol, market fit, adversarial security, implementor DX) then hardened the contract while it is still unreleased: rate and volume are the top negotiated dimensions on real desks and needed typed paths; reason-code selection was ambiguous enough that two conformant sellers would diverge; and the product_selection capability name would have shipped a permanent mismatch with its request field. A parallel scan of IAB Tech Lab's AAMP confirmed no shape changes are needed for future interop — its published negotiation surface types a single buyer_price dimension that maps into this contract as a degenerate case.

Closes #6450.

Validation

  • full repository pre-commit hook
  • 434 server-unit test files passed (6,142 tests; 30 skipped)
  • 370 composed schema validation tests
  • schema registry and reference validation
  • MCP schema projection and 40 KiB context-budget checks
  • documentation navigation, schema-link, example, and compliance-snippet checks
  • TypeScript typecheck
  • changeset protocol-scope validation

@bokelley
bokelley marked this pull request as ready for review August 14, 2026 18:31
Comment thread static/schemas/source/media-buy/proposal-refinement.json Outdated
Comment thread static/schemas/source/media-buy/proposal-refinement.json
Comment thread server/tests/unit/training-agent.test.ts

@aao-secretariat aao-secretariat 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.

Ladon verdict: Escalate to human review

Escalate — gated schema paths require human/CODEOWNERS approval, plus a rename on the protocol surface.

No blocking (critical/high) findings. Schema↔docs coherence, oneOf discriminators, and changeset scope all check out per the reviewer.

Escalation reasons

  • gated_paths: true with review_decision: REVIEW_REQUIRED: this PR modifies/adds 10 files under static/schemas/source/** (including index.json, product-filters.json, product-offer-filters.json, proposal-refinement.json, refine-proposals-response.json, get-adcp-capabilities-response.json). Hard approval gate — human/CODEOWNERS approval required before merge.
  • High-risk modified schema files combined with medium findings (row 5) independently trigger escalation.
  • The diff renames instructionsask on the refine_proposals surface; reviewer notes it is unreleased, but a human should confirm the breaking-class classification and changeset bump before this ships.

Medium findings

  • static/schemas/source/media-buy/proposal-refinement.json:29 — constraints.total_budget has no defined outcome when commercial_terms.total_budget is absent
  • static/schemas/source/media-buy/proposal-refinement.json:45 — alternatives.count is an unbounded response multiplier
  • server/tests/unit/training-agent.test.ts:13663 — batch-wide UNSUPPORTED_FEATURE scope is untested and undocumented

Nits (non-blocking)

  • docs/media-buy/task-reference/refine_proposals.mdx:15 — points at mutable /schemas/latest/ behind a hand-maintained TODO
  • static/schemas/source/media-buy/proposal-budget-constraint.json:1 — duplicates budget-range.json field-for-field; consider $ref

Medium findings

  • static/schemas/source/media-buy/proposal-refinement.json:29 — constraints.total_budget has no defined outcome when commercial_terms.total_budget is absent
  • static/schemas/source/media-buy/proposal-refinement.json:45 — alternatives.count is an unbounded response multiplier
  • server/tests/unit/training-agent.test.ts:13663 — Batch-wide UNSUPPORTED_FEATURE scope is untested and undocumented

Why human review

  • Gated paths: 10 files under static/schemas/source/** changed while review_decision is REVIEW_REQUIRED — human/CODEOWNERS approval required.
  • High-risk modified schema files (proposal-refinement.json, refine-proposals-response.json, get-adcp-capabilities-response.json, index.json, product-filters.json, product-offer-filters.json) carry medium findings.
  • Field rename instructionsask on the refine_proposals protocol surface needs human confirmation of breaking-class classification and changeset bump.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/budget-range.json (added) matches static/schemas/source/**; static/schemas/source/core/product-change-map.json (added) matches static/schemas/source/**; static/schemas/source/core/product-filters.json (modified) matches static/schemas/source/**; static/schemas/source/core/product-offer-filters.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-refinement-reason.json (added) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-budget-constraint.json (added) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-refinement.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-response.json (modified) matches static/schemas/source/**; static/schemas/source/protocol/get-adcp-capabilities-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@aao-secretariat aao-secretariat Bot added the ladon/needs-human-review Ladon has escalated this PR for human review. label Aug 14, 2026
Comment thread static/schemas/source/media-buy/proposal-refinement.json Outdated

@aao-secretariat aao-secretariat 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.

Ladon verdict: Escalate to human review

Escalate — gated schema paths require human/CODEOWNERS approval, plus a breaking-class protocol change.

Why escalate (decision table):

  • Row 2 fires: gated_paths: true (10 files under static/schemas/source/**) AND review_decision is not APPROVED (unknown/none). The hard approval gate is not satisfied — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded.
  • Additionally, the repo's "Breaking-class changes are never auto-approved" hard gate applies: this PR renames/removes fields on the unreleased 3.2 proposal-refinement negotiation surface (e.g. instructionsask). Ratification is a human act and requires escalation unless review_decision is APPROVED.

No critical/high findings surfaced this pass (row 1 does not fire). Schema↔docs coherence holds (reason_code enum, capability supported_dimensions enum, lifecycle_tools gate all match docs), the changeset is present and correctly scoped as minor for the unreleased compact surface, and new response-validator branches are well tested.

Medium findings

  • static/schemas/source/media-buy/proposal-refinement.json:30 — constraints.total_budget asserts a comparison against optional commercial_terms.total_budget with no defined outcome when that field is absent (persists from prior run).

Iteration credit: Two prior mediums were addressed — alternatives.count unbounded multiplier and the untested batch-wide UNSUPPORTED_FEATURE scope. The remaining single medium is a net improvement over the prior pass. The escalation here is driven entirely by the unsatisfied path/breaking-class gates, not by unresolved blocking findings.

Human/CODEOWNERS approval on the gated schema files (and confirmation of the field-rename breaking-class classification) will lift the gate.

Medium findings

  • static/schemas/source/media-buy/proposal-refinement.json:30 — constraints.total_budget has no defined outcome when commercial_terms.total_budget is absent

Why human review

  • Gated paths: 10 files under static/schemas/source/** changed while review_decision is not APPROVED — human/CODEOWNERS approval required.
  • Breaking-class protocol change: field rename instructionsask and removals on the proposal-refinement / refine_proposals surface — ratification is a human act; escalate unless review_decision is APPROVED.
  • Remaining medium: static/schemas/source/media-buy/proposal-refinement.json:30 — constraints.total_budget has no defined outcome when commercial_terms.total_budget is absent.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/budget-range.json (added) matches static/schemas/source/**; static/schemas/source/core/product-change-map.json (added) matches static/schemas/source/**; static/schemas/source/core/product-offer-filters.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-refinement-reason.json (added) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-budget-constraint.json (added) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-refinement.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-response.json (modified) matches static/schemas/source/**; static/schemas/source/protocol/get-adcp-capabilities-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

Comment thread static/schemas/source/media-buy/proposal-refinement.json Outdated

@aao-secretariat aao-secretariat 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.

Ladon verdict: Escalate to human review

Escalate — hard approval gate on static/schemas/source/** is unsatisfied, plus a Breaking-class protocol change requires human ratification.

This PR restructures the (unreleased) refine_proposals surface: instructions/criteria → typed constraints/product_changes/alternatives/ask, adds a proposal_refinement_reason enum and a proposal_refinement capability block. Schema, docs, and server validation move in lockstep; the changeset is a correct minor and no released dist/** artifact is touched. Prior findings #2 (unbounded alternatives) and #3 (untested batch-wide UNSUPPORTED_FEATURE) have been addressed via max_alternatives and new pre-mutation rejection tests — good iteration.

Why this cannot auto-approve:

  • Gated paths (decision row 2): 10 files under static/schemas/source/** are changed and review_decision is not APPROVED (unknown/REVIEW_REQUIRED). This is a deterministic, non-overridable gate — human/CODEOWNERS approval must be recorded first.
  • Breaking-class hard gate: the instructionsask field rename on the refine_proposals protocol surface is a breaking-class wire change; ratification is a human act and is never auto-approved unless review_decision is APPROVED.

Remaining medium finding (does not itself block, surfaced for the human reviewer):

  • static/schemas/source/media-buy/proposal-refinement.json:30 — constraints.total_budget has no defined outcome when commercial_terms.total_budget is absent.

Prior escalation stands: fresh medium finding still present and both hard gates remain unsatisfied.

Medium findings

  • static/schemas/source/media-buy/proposal-refinement.json:30 — constraints.total_budget has no defined outcome when commercial_terms.total_budget is absent

Why human review

  • Gated paths: 10 files under static/schemas/source/** changed while review_decision is not APPROVED (unknown/REVIEW_REQUIRED) — human/CODEOWNERS approval required before merge.
  • Breaking-class protocol change: field rename instructionsask on the refine_proposals surface requires human ratification per the constitution's hard gate; never auto-approved unless review_decision is APPROVED.
  • Open medium finding: proposal-refinement.json:30 — constraints.total_budget has undefined outcome when commercial_terms.total_budget is absent; warrants a human eye alongside the gated schema review.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/budget-range.json (added) matches static/schemas/source/**; static/schemas/source/core/product-change-map.json (added) matches static/schemas/source/**; static/schemas/source/core/product-offer-filters.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-refinement-reason.json (added) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-budget-constraint.json (added) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-refinement.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-response.json (modified) matches static/schemas/source/**; static/schemas/source/protocol/get-adcp-capabilities-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@lukasz-pubx

Copy link
Copy Markdown

Reviewed at b48b2bb90 against merge-base 146f1065, alongside the #6450 discussion.

First — thank you for this. It's a faithful implementation of every position from the RFC discussion, and several parts go beyond what was asked: the reference validator enforces what JSON Schema can't (terms_digest uniqueness across alternatives, unsatisfied_constraints/unsatisfied_product_changes as strict subsets of the request, alternatives_unavailable count coherence), the training-agent tests prove typed-dimension rejection happens before mutation with store state asserted unchanged, and the multi-finalize counter-precedent is explicitly reconciled in the capabilities doc rather than silently diverged from. The response-cardinality fix (proposals[] for revise/partial, singular proposal for finalize) and the keyed constraint object are exactly right. The minor changeset is correct — the renamed instructions field is unreleased (confirmed against the live docs).

Two asks before merge, two smaller notes:

1. criteria participates in the rejection regime but can't be declared in the capability regime. supported_dimensions is closed at ["total_budget", "product_selection", "alternatives"], yet refine_proposals.mdx lists criteria in the same typed-fields table, and this PR's own reference implementation rejects it as a dimension with a name the enum can't express (training-agent.test.ts: details: { unsupported_dimension: 'criteria', supported_dimensions: [] }). Concretely: two sellers both declare all three dimensions; a buyer sends criteria.targeting_overlay (the doc's own second example). One seller accepts it (reading the enum as the complete dimension universe, criteria outside the regime); the other rejects task-level UNSUPPORTED_FEATURE per the reference implementation. No pre-flight signal exists, and a criteria-supporting seller has no way to advertise it. Any of these resolves it: (a) add criteria to the supported_dimensions enum; (b) a normative sentence that criteria discoverability follows the targeting/discovery capability surfaces, naming them; (c) declare criteria baseline-required whenever refine_proposals is in lifecycle_tools.

2. No disposition for alternatives.count exceeding a published max_alternatives. The schema says "Optional maximum alternatives.count the seller accepts" and the doc "publish a pre-flight count ceiling" — but no error code or per-result outcome is named for exceeding it. A buyer sending count: 6 against max_alternatives: 4 gets task-level VALIDATION_ERROR from one conformant seller and 200 + partial/alternatives_unavailable with four drafts from another, breaking SDK retry/fallback strategies written against either. Task-level rejection would be consistent with the PR's own "buyers SHOULD NOT send omitted dimensions" posture, but explicitly blessing partial is also acceptable — it just needs to be one of them, stated.

3. (note) The budget-range / proposal-budget-constraint twin is well-motivated (draft-07 can't compose additionalProperties: false over a $ref) and declared in refine_proposals.mdx prose — but an SDK generator reading only schemas sees two unlinked near-identical definitions. Suggest cross-referencing each schema's description to the other so the declared fork survives schema-only consumption.

4. (note) The dimension-rejection detail payload the training agent emits (details: { unsupported_dimension, supported_dimensions }) is genuinely useful but unspecified — buyers can't rely on anything beyond error.field. Either specify it in the UNSUPPORTED_FEATURE catalog entry for this surface (would let SDKs auto-degrade to supported dimensions) or mark it implementation-specific deliberately.

@bokelley

Copy link
Copy Markdown
Contributor Author

Two interop gaps and two clean-up notes — all legitimate. Synthesis and recommended path for each:


Ask 1 — criteria in the rejection regime but not the capability regime.

Recommending option (a): add "criteria" to the supported_dimensions enum. The training agent already maps criteria to a typed dimension and fires UNSUPPORTED_FEATURE for it — the schema-declared capability surface should match that. Option (b) (infer from discovery surfaces) works architecturally but asks buyers to do a second lookup; option (c) (baseline-required) overprescribes since criteria sub-field coverage varies by discovery capability.

Option (a) — one schema edit:

// static/schemas/source/protocol/get-adcp-capabilities-response.json
  "enum": ["total_budget", "product_selection", "alternatives"]
+ "enum": ["total_budget", "product_selection", "alternatives", "criteria"]

Sellers that support criteria refinement advertise it; those that don't omit it and the pre-flight signal is deterministic.

Option (b) — normative prose, no enum change:

// refine_proposals.mdx — proposal_refinement capability section
+ Criteria support in refinements is not declared in supported_dimensions;
+ a seller's acceptance of criteria fields follows from its get_products
+ targeting capability surface.

Valid, but buyers need two lookups to pre-flight a criteria refinement.

@bokelley — confirm (a) or (b) and I'll push the edit.


Ask 2 — No disposition for alternatives.count exceeding max_alternatives.

Recommending task-level VALIDATION_ERROR: consistent with the "buyers SHOULD NOT" posture when a declared ceiling is explicitly ignored, and keeps partial+alternatives_unavailable unambiguous — that reason code should mean "couldn't produce enough," not "ceiling clamped."

Option A — task-level VALIDATION_ERROR (recommended):

{
  "code": "VALIDATION_ERROR",
  "message": "alternatives.count (6) exceeds seller's max_alternatives (4)",
  "field": "refinements.0.alternatives.count"
}

Option B — bless partial + alternatives_unavailable:

{
  "result": "partial",
  "proposals": ["/* up to max_alternatives drafts */"],
  "reason": "alternatives_unavailable"
}

Also acceptable but buyers must reconstruct from context whether the shortfall was a ceiling or a supply issue; SDK retry strategies would need to handle both readings of the same reason code.

@bokelley — confirm A or B and I'll add the normative sentence to the refine_proposals.mdx capability doc and to the VALIDATION_ERROR / alternatives_unavailable catalog entries as appropriate.


Note 3 — schema cross-reference.

budget-range.json and proposal-budget-constraint.json are structurally parallel but differ in exactly one way (additionalProperties: true vs false). Adding a one-sentence pointer to each description costs nothing and survives schema-only SDK consumption. Proposed additions:

  • budget-range.json: append "Strict negotiation variant (additionalProperties: false) in proposal-budget-constraint."
  • proposal-budget-constraint.json: append "Extension-tolerant peer schema: budget-range."

Can push this as a follow-up commit — mechanical, no normative impact.


Note 4 — detail payload.

The { unsupported_dimension, supported_dimensions } shape is explicitly constructed in the handler (adcpError call) and asserted by tests — it's already a first-class contract in practice. Recommend promoting it to a normative SHOULD in the UNSUPPORTED_FEATURE catalog entry for this surface, scoped to sellers that declare supported_dimensions. That closes the SDK auto-degrade path Lukasz describes and removes ambiguity about whether buyers can rely on it.

If you'd prefer to mark it explicitly implementation-specific instead, a one-sentence prose note in refine_proposals.mdx under the UNSUPPORTED_FEATURE reference does that.

Either way, can push this commit alongside note 3.


Holding on asks 1 and 2 pending your call — those involve normative schema/doc edits that should be your decision. Notes 3 and 4 are ready to push on your go-ahead.


Generated by Claude Code

@aao-secretariat aao-secretariat 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.

Ladon verdict: Escalate to human review

Escalate — gated schema paths require human/CODEOWNERS approval and a breaking-class protocol change is present.

The developer addressed the prior blocking medium finding: proposal-refinement.json now defines the outcome when commercial_terms.total_budget is absent (partial/unable + constraint_unsatisfiable + unsatisfied_constraints:['total_budget'], never revised), enforced in source-schema.ts via proposalSatisfiesBudgetConstraint with full test coverage. The reviewer surfaced no new Critical/High/Medium findings in the delta, and schema↔docs coherence, the max_alternatives VALIDATION_ERROR contract, and the additive wire change (minor changeset) all check out. This is a genuine net improvement over the prior run.

However, two hard gates still stand and neither is satisfied:

  1. Gated paths (row 2): 10 files under static/schemas/source/** are changed and review_decision is not APPROVED (unknown/none). This is a deterministic, non-overridable approval gate — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded.

  2. Breaking-class protocol change (prose hard gate): the instructionsask field rename on the refine_proposals surface is a Breaking-class change per the constitution. Ratification is a human act and is never auto-approved unless review_decision is APPROVED.

Both gates lift the moment a human/CODEOWNERS approval is recorded on the PR. No blocking code-level findings remain; the sole obstacle to merge is the required human ratification.

Why human review

  • Gated paths: 10 files under static/schemas/source/** changed while review_decision is not APPROVED (unknown/none) — human/CODEOWNERS approval required before merge.
  • Breaking-class protocol change: field rename instructionsask on the refine_proposals surface requires human ratification per the constitution's hard gate; never auto-approved unless review_decision is APPROVED.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/budget-range.json (added) matches static/schemas/source/**; static/schemas/source/core/product-change-map.json (added) matches static/schemas/source/**; static/schemas/source/core/product-offer-filters.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-refinement-reason.json (added) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-budget-constraint.json (added) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-refinement.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-response.json (modified) matches static/schemas/source/**; static/schemas/source/protocol/get-adcp-capabilities-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@aao-secretariat aao-secretariat 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.

Ladon verdict: Escalate to human review

Escalate — gated-path approval gate unsatisfied and breaking-class protocol change pending human ratification.

This PR adds hard upper bounds (maximum: 10 on alternatives.count / max_alternatives, maxItems: 25 on refinements) to the refine_proposals surface, with schema↔docs coherence maintained across refine_proposals.mdx and get_adcp_capabilities.mdx and new tests covering schema-level rejection plus server-side atomic batch rejection. The reviewer surfaced no critical/high/medium findings — the diff is mechanically clean.

However, the decision cannot auto-approve:

Escalation reasons

  • Gated paths: 10 files under static/schemas/source/** changed while review_decision is not APPROVED (unknown/none). Human/CODEOWNERS approval is required before merge (decision rule row 2 — hard, non-overridable gate).
  • Breaking-class protocol change carried forward from the prior review: the instructionsask field rename on the refine_proposals surface is a breaking-class change per the constitution's hard gate, never auto-approved unless review_decision is APPROVED.

The prior decision was escalate for the same gated-path/breaking-class reasons; those conditions persist in this run (files still touch the gated surface, review_decision still not APPROVED). Once a human/CODEOWNERS approval is recorded, this clean diff can fall through to approval.

Why human review

  • Gated paths: 10 files under static/schemas/source/** changed while review_decision is not APPROVED (unknown/none) — human/CODEOWNERS approval required before merge.
  • Breaking-class protocol change: field rename instructions → ask on the refine_proposals surface requires human ratification per the constitution's hard gate; never auto-approved unless review_decision is APPROVED.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/budget-range.json (added) matches static/schemas/source/**; static/schemas/source/core/product-change-map.json (added) matches static/schemas/source/**; static/schemas/source/core/product-offer-filters.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-refinement-reason.json (added) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-budget-constraint.json (added) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-refinement.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-request.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-response.json (modified) matches static/schemas/source/**; static/schemas/source/protocol/get-adcp-capabilities-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'unknown', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@bokelley

Copy link
Copy Markdown
Contributor Author

All review feedback is addressed in 165f898454 and 42550c1893: criteria is capability-advertisable; published alternative ceilings produce task-level VALIDATION_ERROR; the paired budget schemas cross-reference each other; unsupported-dimension recovery details are specified; budget constraints fail closed; alternative and batch cardinality are bounded; and mixed batches are proven to reject task-wide without mutation. The three inline review threads are resolved. Validation includes 630 training-agent tests, 366 composed-schema cases, 795 schema files, typecheck, MCP projection, targeting-aware discovery, docs navigation, and compliance checks.

Red-team and market-scan follow-ups on refine_proposals:

- Typed cpm, impressions, and flight hard constraints, capability-gated
  like total_budget; unsatisfied_constraints opens to free string keys
- Rename capability dimension product_selection to product_changes so
  dimensions match their request fields
- constraint_unsatisfiable precedence over other reason codes; typed
  failures never masquerade as commercially_declined
- hold_unavailable and batch_aborted reason codes plus double-finalize
  INVALID_STATE rule and seller hold-policy guidance
- Undeclared-dimension rejection upgraded to MUST with an
  error-details/unsupported-refinement-dimension.json details shape
- Every refinement successor carries parent_proposal_id equal to its
  source; response schema and validator enforce lineage
- Validator recomputes terms_digest from JCS-canonicalized
  commercial_terms and checks alternative distinctness on terms,
  not digest strings
- Normative text: partial drafts satisfy every unlisted constraint,
  only commercial_terms is contractual, ask is untrusted input,
  price-probe friction guidance, legacy budget_range soft-vs-hard
  facade warning, response example with lineage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bokelley

Copy link
Copy Markdown
Contributor Author

Ran a four-way red team (protocol, market fit, adversarial security, implementor DX) plus a market scan against IAB Tech Lab's AAMP before finalizing. efe48f522e addresses everything that had to land while the wire contract is still unreleased:

Contract-breaking-if-released-later (schema):

  • Capability dimension product_selectionproduct_changes so every dimension matches its request field — a codegen trap once released.
  • unsatisfied_constraints items opened from a closed ["total_budget"] enum to self-describing constraint keys, so future dimensions don't need a four-surface enum bump.
  • New typed hard constraints cpm (rate ceiling), impressions (volume floor), and flight (window bounds) — rate and volume are the Welcome to adcp Discussions! #1/Remove targeting_template and add pluggable product catalog system #2 negotiated dimensions on real desks and were prose-only. All three are mechanically verifiable against commercial_terms, same as total_budget.
  • Refinement successors now require parent_proposal_id = source proposal, so negotiation lineage is reconstructible from proposals alone (audit trail without response archaeology).

Semantics that two conformant sellers would have implemented differently:

  • constraint_unsatisfiable takes precedence over every other reason code; typed failures never use commercially_declined (that's for ask-level refusals). Fixes a real wedge where a budget violation + alternatives shortfall had two competing MUSTs for one scalar.
  • Undeclared-dimension rejection upgraded SHOULD → MUST (task-level, pre-mutation) — the task doc, capability doc, and reference implementation now agree, and storyboard grading has a MUST to assert. Details shape registered as error-details/unsupported-refinement-dimension.json.
  • Finalize failure vocabulary: hold_unavailable + batch_aborted reason codes, an explicit double-finalize INVALID_STATE rule, and seller hold-policy guidance (holds are seller-gated; concurrent-hold caps allowed; expires_at is seller-chosen).
  • New normative invariant: every draft on a partial satisfies every constraint absent from unsatisfied_constraints, whatever the reason_code — this is the sentence the 'buyers don't parse prose' pitch actually rests on.

Trust and abuse (doc-normative):

  • terms_digest is now stated as buyer-recomputable (RFC 8785 JCS + sha256), and the validator recomputes it and checks alternative distinctness on canonicalized terms, not digest strings — a seller can no longer fabricate distinct digests over identical terms.
  • Only commercial_terms is contractual; ask/reason/suggestions prose never binds. (The procurement question every agency legal team asks first.)
  • New Negotiation security section: ask is untrusted input that must be fenced from pricing authority, and deterministic constraint responses form a floor-price oracle sellers should rate-limit.
  • Façade warning: legacy budget_range (soft filter) and compact total_budget (hard constraint) share a shape with inverted semantics — adapters must convert, not copy.

AAMP scan result: AAMP 2.3's published negotiation surface types exactly one dimension (buyer_price in integer micros, serial rounds) with no hold primitive; AdCP is strictly more expressive and AAMP maps into refine_proposals as a degenerate single-constraint case. Nothing there argues for shape changes. Full local suite green: 6,142 server tests, 370 composed-schema tests, typecheck, docs gates.

🤖 Generated with Claude Code

@aao-secretariat aao-secretariat 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.

Ladon verdict: Escalate to human review

Escalate — gated paths require human/CODEOWNERS approval, and a breaking-class protocol change is in scope.

This PR extends the refine_proposals surface with new hard constraints (cpm/impressions/flight), negotiation lineage via a required parent_proposal_id, a buyer-verifiable terms_digest, and new reason codes. Schema↔docs coherence, changeset scope/type, oneOf discriminators, and reference validators all check out — the reviewer found no blocking or Medium findings. That's a clean diff, but it does not clear the hard gates.

Escalation reasons

  • Gated paths: 13 files under static/schemas/source/** changed while review_decision is REVIEW_REQUIRED (not APPROVED). This is a deterministic hard approval gate — human/CODEOWNERS approval is required before merge regardless of how clean the diff is. Affected files include static/schemas/source/media-buy/refine-proposals-request.json, refine-proposals-response.json, proposal-refinement.json, core/canonical-proposal.json, enums/error-code.json, and the new added schemas.
  • Breaking-class protocol change: introducing a required parent_proposal_id (negotiation lineage) and new required commercial-terms structure on the refine_proposals surface is a breaking-class change under the constitution's decision classes. Per the hard gate, breaking-class changes are never auto-approved — ratification is a human act — unless review_decision is APPROVED.

Prior decision was also escalate for the same gates; those conditions still hold (row 2). No blocking findings otherwise. Once a human/CODEOWNERS APPROVED review is recorded, this can fall through to the normal table.

Why human review

  • Gated paths: 13 files under static/schemas/source/** changed while review_decision is REVIEW_REQUIRED (not APPROVED) — human/CODEOWNERS approval required before merge.
  • Breaking-class protocol change on the refine_proposals surface (required parent_proposal_id negotiation lineage + new required commercial-terms structure) — never auto-approved unless review_decision is APPROVED per the constitution's hard gate.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/core/budget-range.json (added) matches static/schemas/source/**; static/schemas/source/core/canonical-proposal.json (modified) matches static/schemas/source/**; static/schemas/source/core/product-change-map.json (added) matches static/schemas/source/**; static/schemas/source/core/product-offer-filters.json (modified) matches static/schemas/source/**; static/schemas/source/enums/error-code.json (modified) matches static/schemas/source/**; static/schemas/source/enums/proposal-refinement-reason.json (added) matches static/schemas/source/**; static/schemas/source/error-details/unsupported-refinement-dimension.json (added) matches static/schemas/source/**; static/schemas/source/index.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-budget-constraint.json (added) matches static/schemas/source/**; static/schemas/source/media-buy/proposal-refinement.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-request.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/refine-proposals-response.json (modified) matches static/schemas/source/**; static/schemas/source/protocol/get-adcp-capabilities-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'REVIEW_REQUIRED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@bokelley
bokelley enabled auto-merge (squash) August 15, 2026 15:32
@lukasz-pubx

Copy link
Copy Markdown

Re-reviewed at efe48f52 (delta: 165f898, 42550c1, efe48f52). All four findings from my earlier comment are resolved in the tree, not just claimed:

  1. criteria is now in supported_dimensions, and the dimension names match their request fields (product_selectionproduct_changes) — the right rename to make while the surface is unreleased.
  2. max_alternatives violations are now a task-level MUST (VALIDATION_ERROR, no silent clamping), with the protocol ceiling of 10.
  3. The budget twins cross-reference each other in both schema descriptions, so the declared fork survives schema-only consumption.
  4. The rejection details shape is registered (error-details/unsupported-refinement-dimension.json) and referenced normatively — and the SHOULD→MUST upgrade on undeclared-dimension rejection also removes the conformant-divergence residue.

The hardening delta holds up on inspection: cpm/impressions/flight state their failure dispositions honestly (mixed or auction pricing → cpm unsatisfied; asap start → unverifiable → unsatisfied), parent_proposal_id is required in all three proposal-bearing outcome branches, batches are bounded, and the unsatisfied_constraints open-key trade (subset rule in prose + validator rather than a closed schema enum) is a reasonable forward-compat call. No new interop-grade issues found. Thanks for the fast, thorough turnaround — from my side this is ready.

@bokelley
bokelley disabled auto-merge August 15, 2026 18:51
@bokelley
bokelley merged commit fd610b5 into main Aug 15, 2026
48 checks passed
@bokelley
bokelley deleted the address-lucas-comments branch August 15, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ladon/needs-human-review Ladon has escalated this PR for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: define structured negotiation requests for refine_proposals

2 participants