Skip to content

feat(decisioning): SignalsPlatform + AudiencePlatform Protocols (breadth sprint Batch 1)#332

Merged
bokelley merged 2 commits intomainfrom
bokelley/decisioning-signals-audience-protocols
May 1, 2026
Merged

feat(decisioning): SignalsPlatform + AudiencePlatform Protocols (breadth sprint Batch 1)#332
bokelley merged 2 commits intomainfrom
bokelley/decisioning-signals-audience-protocols

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 1, 2026

Summary

Breadth sprint Batch 1 per the parity audit. Ports two of the eight missing specialism Protocols from the JS reference at `src/lib/server/decisioning/specialisms/{signals,audiences}.ts`.

New:

  • `SignalsPlatform` — covers `signal-marketplace` (third-party data brokers) + `signal-owned` (first-party data). Methods: `get_signals` + `activate_signal`. Both sync at wire level (no Submitted arm); long-running activations drive lifecycle via `ctx.publish_status_change`.
  • `AudiencePlatform` — covers `audience-sync`. Methods: `sync_audiences` (wire-required) + `poll_audience_statuses` (adopter-internal helper).
  • `REQUIRED_METHODS_PER_SPECIALISM` entries for all three slugs.
  • Public exports — `SalesPlatform`, `SignalsPlatform`, `AudiencePlatform` now on `adcp.decisioning.all` (closes a drift bug; `SalesPlatform` was referenced in the quickstart docstring but never publicly re-exported).

Tests (13 new):

  • `runtime_checkable` conformance per Protocol.
  • `validate_platform` required-method enforcement per slug.
  • Public-export drift guard against `adcp.decisioning.all`.
  • Cross-specialism composition (sales-* + signal-* together).
  • `audience-sync` minimal-implementation passes (only `sync_audiences` required).

One existing test updated: `test_validate_platform_warns_on_unenforced_spec_specialism` switched its canonical "unenforced spec slug" example from `signal-marketplace` (now enforced) to `creative-ad-server` (still pending until Batch 2).

Test plan

  • `pytest tests/test_decisioning_specialisms.py` — 13 passed
  • `pytest tests/` — 2221 passed (up from 2208)
  • `ruff check` clean
  • `mypy src/adcp/decisioning/` clean

Remaining breadth-sprint queue

Per the parity audit, remaining specialism Protocols for Batch 2+:

  • `CreativeBuilderPlatform` + `CreativeAdServerPlatform` (F13/F16/F17)
  • `CampaignGovernancePlatform` (governance-spend-authority, governance-delivery-monitor, governance-aware-seller)
  • `BrandRightsPlatform`, `ContentStandardsPlatform`
  • `PropertyListsPlatform`, `CollectionListsPlatform`

Release plan

Accumulates into the held release-please PR #328 alongside foundation (#316), codemod ergonomics (#329), parity rename + Tier 1 docs (#330), and F12 auto-emit (#331). All five ship together in 4.4.0 once salesagent validates.

🤖 Generated with Claude Code

bokelley and others added 2 commits April 30, 2026 20:46
…dth sprint Batch 1)

First batch of the breadth-sprint per the parity audit (8 missing
specialism Protocols). Ports two from JS reference at
``src/lib/server/decisioning/specialisms/{signals,audiences}.ts``.

New Protocols:

* ``SignalsPlatform`` (src/adcp/decisioning/specialisms/signals.py) —
  covers ``signal-marketplace`` (third-party data brokers like
  LiveRamp, Oracle Data Cloud) AND ``signal-owned`` (first-party data
  providers like publisher first-party data, retailer customer-graph).
  Two methods: ``get_signals`` (sync catalog discovery) and
  ``activate_signal`` (provisioning onto destination platforms).
  Activation is sync at the wire level — no Submitted arm. Long-running
  activation pipelines (identity-graph match: 5-30 min) return the
  success-arm shape with ``deployments`` rows in ``pending`` state and
  drive lifecycle via ``ctx.publish_status_change``.

* ``AudiencePlatform`` (src/adcp/decisioning/specialisms/audience.py) —
  covers ``audience-sync``. Two methods: ``sync_audiences`` (wire-required;
  push first-party CRM audiences with delta upsert) and
  ``poll_audience_statuses`` (adopter-internal; batch state read for
  cross-platform orchestration). Match-rate computation runs in the
  adopter's background; per-audience terminal state via
  ``publish_status_change``.

Required-method coverage in ``REQUIRED_METHODS_PER_SPECIALISM``:

* ``signal-marketplace``, ``signal-owned`` — both gate on
  ``{get_signals, activate_signal}`` (shared Protocol).
* ``audience-sync`` — gates only on ``sync_audiences`` since
  ``poll_audience_statuses`` is adopter-internal.

Public re-exports added at ``adcp.decisioning.__all__``:
``SalesPlatform``, ``SignalsPlatform``, ``AudiencePlatform``. Closes
a small drift bug — ``SalesPlatform`` was referenced in the
quickstart docstring but never actually re-exported through the
public surface.

Test coverage in ``tests/test_decisioning_specialisms.py`` (13 new
tests):

* ``runtime_checkable`` conformance per Protocol.
* ``validate_platform`` required-method enforcement.
* Public-export pinning (drift-guard against ``adcp.decisioning.__all__``).
* Cross-specialism composition (claiming sales-* + signal-* together
  satisfies both Protocols).
* ``audience-sync`` minimal-implementation passes (only
  ``sync_audiences`` required).

One existing test updated:
``test_validate_platform_warns_on_unenforced_spec_specialism``
switched its canonical "spec-recognized but unenforced" example from
``signal-marketplace`` (now enforced) to ``creative-ad-server`` (still
pending until Batch 2 ships Creative Protocols).

Remaining specialism Protocols (creative-*, governance-*,
brand-rights, content-standards, property-lists, collection-lists)
are queued for subsequent breadth-sprint PRs.

2221 tests pass (up from 2208).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… brittle test

Two converging expert-review findings:

P1 (audience.py:45,53): two string-literal globals were port artifacts
from the JS reference that never compiled to anything meaningful in
Python:

  Audience = "SyncAudiencesAudience"
  SyncAudiencesRow = "SyncAudiencesSuccessResponse.audiences[number]"

The first is a string constant masquerading as a forward-ref but
unimported; the second is TypeScript indexed-access syntax
(``T['audiences'][number]``) which has no Python meaning and would
raise NameError if ``typing.get_type_hints`` ever resolved it.
Replaced with a comment block pointing adopters at the canonical
``adcp.types.SyncAudiencesAudience`` /
``adcp.types.SyncAudiencesSuccessResponse`` imports.

P2 (test_decisioning_specialisms.py:284): the smoke check pinned on
``hasattr(SalesPlatform, '_is_protocol')`` — a private CPython typing
internal that's brittle against typing-module changes. Replaced with
an ``isinstance`` check against a minimal-but-complete shim that
exercises all 9 SalesPlatform methods. Same invariant via a durable
public assertion.

Punt list (P2/P3 reviewer findings) deferred to follow-up:

* JS exports ``Audience``, ``SyncAudiencesRow``, ``AudienceStatus``
  type aliases that Python doesn't re-export. Adopters import directly
  from ``adcp.types`` today; not blocking.
* Cross-language adopter-shape divergence on ``sync_audiences`` (JS
  returns rows, Python returns full response). Pick one in a follow-up
  RFC; both produce the same wire output.

13 tests pass; mypy + ruff clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit c66ff72 into main May 1, 2026
12 checks passed
bokelley added a commit that referenced this pull request May 1, 2026
…ionLists Protocols (breadth sprint Batch 4 — FINAL) (#335)

Final batch of the breadth-sprint per the parity audit. Ports the
remaining four specialism Protocols from JS reference. With this
PR, every spec specialism slug except ``governance-aware-seller``
has a Protocol class + REQUIRED_METHODS coverage.

New Protocols:

* ``BrandRightsPlatform`` — covers ``brand-rights``. Identity
  discovery + licensing. Required (3, sync): ``get_brand_identity``,
  ``get_rights``, ``acquire_rights``. ``acquire_rights`` returns a
  3-arm discriminated success union (acquired / pending / rejected)
  — rejection-as-data for spec-defined GRANT rejection, AdcpError
  only for buyer-fixable REQUEST rejection. Async outcomes for the
  ``pending`` arm flow via ``push_notification_config`` webhook
  (NOT a polling tool).

* ``ContentStandardsPlatform`` — covers ``content-standards``.
  Brand safety policies, content adjacency rules, per-creative
  compliance verification. Required (6): list/get/create/update,
  ``calibrate_content``, ``validate_content_delivery``. Optional
  (2, analyzer reads): ``get_media_buy_artifacts``,
  ``get_creative_features``.

* ``PropertyListsPlatform`` + ``CollectionListsPlatform``
  (specialisms/lists.py) — covers ``property-lists`` and
  ``collection-lists``. Parallel CRUD shapes (5 methods each, all
  required) with token-issuance semantics: ``create_*`` returns a
  per-seller-scoped ``fetch_token``, ``delete_*`` revokes it.
  Compromise-driven revocation MUST trigger the delete path.

Required-method coverage in ``REQUIRED_METHODS_PER_SPECIALISM``:
``brand-rights`` (3), ``content-standards`` (6),
``property-lists`` (5), ``collection-lists`` (5).

Public re-exports added at ``adcp.decisioning.__all__``:
``BrandRightsPlatform``, ``ContentStandardsPlatform``,
``PropertyListsPlatform``, ``CollectionListsPlatform``.

Test coverage in ``tests/test_decisioning_specialisms.py`` (13 new
tests, 43 total in the file):

* ``runtime_checkable`` conformance per Protocol.
* ``validate_platform`` enforcement per slug — including a
  security-relevant test that ``property-lists`` REQUIRES
  ``delete_property_list`` (revocation path) so adopters can't ship
  list-publishing without revocation primitives.
* Contract pins per slug.
* **Breadth-sprint completeness pin**:
  ``test_every_spec_slug_except_governance_aware_seller_is_enforced``
  asserts that ``SPEC_SPECIALISM_ENUM - REQUIRED_METHODS.keys()``
  yields exactly ``{governance-aware-seller, signed-requests}`` —
  the two slugs unenforced by design (composition claim and
  deprecated-moved-to-universal respectively).

One existing dispatch test updated:
``test_validate_platform_warns_on_unenforced_spec_specialism``
swapped its canonical "spec-recognized but unenforced" example
from ``brand-rights`` (now enforced) to ``governance-aware-seller``
(the only remaining unenforced spec slug — by design).

**Breadth sprint COMPLETE.** All 8 missing specialism Protocols
from the parity audit are now ported. 9 PRs total accumulating in
the held release PR #328:

* #316 foundation
* #329 codemod ergonomics
* #330 parity rename + Tier 1 docs
* #331 F12 auto-emit
* #332 Signals + Audience (Batch 1)
* #333 Creative Builder + AdServer (Batch 2)
* #334 Campaign Governance (Batch 3)
* #335 Brand + Content + Lists (Batch 4 — this PR)

Ready for salesagent validation against editable install before
tagging 4.4.0.

2252 tests pass (up from 2239).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request May 1, 2026
* fix(decisioning): handler shims for every non-sales wire tool

The Emma DX smoke test (AudioStack creative-generative agent)
caught a load-bearing gap: the breadth-sprint shipped 10 Protocol
classes + REQUIRED_METHODS coverage at the static layer, but
``PlatformHandler`` only had 9 sales-* shims. Every non-sales wire
tool (``build_creative``, ``get_signals``, ``check_governance``,
``get_brand_identity``, ``list_content_standards``,
``create_property_list``, ``create_collection_list``, ...) was 404
at the wire even though capabilities advertised the slug,
``validate_platform`` reported green, and the wire-tool registry at
``mcp_tools.py`` already mapped each tool name to its Request type.

A creative-generative adopter wrapping AudioStack (Emma-style
real-world test) could implement the Protocol shape correctly,
``serve()`` started, ``tools/list`` returned the advertised set —
but ``tools/call name="build_creative"`` got
``Unknown tool: build_creative``. Silent buyer-facing failure with
the framework reporting green at boot.

Fix: 31 new shims following the existing sales-* template, one per
wire tool the breadth-sprint Protocols cover.

Per-Protocol-family advertised tool sets, kept as separate
frozensets for readability and future selective filtering. The
``advertised_tools`` ClassVar is the union of all 9 sets (40 tools
total).

New runtime gate: ``_OPTIONAL_PLATFORM_METHODS`` +
``_require_platform_method`` helper. For methods marked optional on
the per-specialism Protocol (``preview_creative`` on
CreativeBuilderPlatform, ``get_media_buy_artifacts`` /
``get_creative_features`` on ContentStandardsPlatform), the shim
pre-checks ``hasattr(platform, method_name)`` and surfaces
``AdcpError(code='UNSUPPORTED_FEATURE')`` to buyers when the adopter
chose not to wire the method. Without this, AttributeError would
get wrapped to ``INTERNAL_ERROR`` — adopter contract violation, not
buyer-fixable. Required methods are caught at server boot by
``validate_platform``; the optional set complements that gate at
runtime.

Account-resolution: every new shim uses
``getattr(params, "account", None)`` — most non-sales tools don't
carry ``account`` on the wire (catalog reads, governance plans,
brand-rights queries). The ``AccountStore`` impl handles the no-ref
case per its resolution mode (``'derived'`` tolerates None;
``'implicit'`` raises ``AUTH_INVALID``).

Arg-projection: ``sync_audiences`` arg-projects
``audiences=params.audiences`` to match the JS-side adopter
ergonomic (the AudiencePlatform method signature is
``sync_audiences(audiences, ctx)`` per PR #332).

Test coverage in ``tests/test_decisioning_handler_shims.py`` (42
new tests):

* ``advertised_tools`` covers every spec wire tool (drift guard
  against accidentally shipping unrouted advertised tools).
* Parametrized: every advertised non-sales tool has a shim method
  on PlatformHandler.
* End-to-end shim → ``_invoke_platform_method`` → platform method
  for one tool per Protocol family.
* ``sync_audiences`` arg-projection regression test.
* ``UNSUPPORTED_FEATURE`` surface for optional methods.
* AudioStack DX regression test — pinned by name so the Emma fix
  doesn't silently regress.

2308 tests pass (up from 2266).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(decisioning): expert P0/P1 fixes on handler shims

P0:
- F12 auto-emit on 12 webhook-eligible shims (activate_signal,
  get_signals, get_creative_delivery, sync_audiences, get_brand_identity,
  get_rights, acquire_rights, all 5 property_list ops). Mirrors the
  sales-* shim pattern. Fires only when (a) request schema allows
  push_notification_config and the buyer registered a URL, and
  (b) method_name is in SPEC_WEBHOOK_TASK_TYPES.
- build_creative bare-manifest / list / envelope projection helper
  (_project_build_creative) — JS-side projectBuildCreativeReturn parity.
  The CreativeBuilderPlatform.build_creative Protocol declares 3 return
  arms (single CreativeManifest, list, fully-shaped success envelope);
  the wire envelope has only 2 success arms ({creative_manifest} vs
  {creative_manifests}). Bare-manifest/list returns now project to wire
  shape instead of failing oneOf validation.
- sync_audiences list arm projection (_project_sync_audiences) — wire
  envelope is {audiences: [...]} but Protocol allows list-of-rows
  ergonomic. Mirrors JS-side wrapping.
- build_creative gated via _require_platform_method so a sales-only
  adopter routing here surfaces UNSUPPORTED_FEATURE instead of leaking
  AttributeError -> INTERNAL_ERROR.

P1:
- update_rights shim (BrandRightsPlatform mutation: extend term, change
  scope, revoke). Added to _BRAND_RIGHTS_ADVERTISED_TOOLS.
- acquire_rights docstring corrected from 3-arm to 4-arm
  (acquired/pending/rejected/error).
- Governance/brand-rights/content-standards shims switched from
  getattr(params, "account", None) to explicit None for the schemas
  that declare additionalProperties:false on the request type — the
  account field is forbidden, getattr was a foot-gun for future drift.
  get_media_buy_artifacts and get_creative_features keep getattr since
  their schemas DO carry account.

Tests:
- _project_build_creative and _project_sync_audiences arm coverage.
- F12 auto-emit fires on get_signals, acquire_rights,
  get_creative_delivery, sync_audiences (with projected envelope).
- update_rights routes through the shim.
- update_rights does NOT auto-emit (not in SPEC_WEBHOOK_TASK_TYPES).
- property_list ops do NOT auto-emit (wire schema forbids
  push_notification_config); regression-pin so a future schema change
  surfaces here.
- build_creative UNSUPPORTED_FEATURE when platform doesn't implement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant