Skip to content

feat(reporting): mirror the TypeScript SDK's advanced path, engine errors and readback name - #1751

Open
bokelley wants to merge 3 commits into
claude/issue-1695-owed-namesfrom
claude/issue-1695-ts-parity
Open

bokelley wants to merge 3 commits into
claude/issue-1695-owed-namesfrom
claude/issue-1695-ts-parity

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

The cross-SDK parity slice, stacked on #1746. It mirrors what the TypeScript DX slices adcp-client#3542–#3546 add, in snake_case. Review only feat(reporting): mirror the TypeScript SDK's advanced path, engine errors and readback name.

TypeScript Python Where it landed
windDownGrant(current?), REPORTING_WIND_DOWN_TASKS wind_down_grant(current=None), REPORTING_WIND_DOWN_TASKS (keep authorized_at; same shared vectors) #1740
ReportingSubscriptionError with the 7 vector codes; the 5 refusals carry dispatched: false same #1736, #1737, #1738
CoverageInputError with detail, including COVERAGE_BUDGET_EXCEEDED same #1738, #1744
PrincipalRef { tenantId, principalId } PrincipalRef(tenant_id, principal_id) #1739
owedSubscriptionPeriodsV1, Owed* types owed_subscription_periods, OwedSubscriptionPeriod (deprecated aliases kept) #1746
engine.listSubscriptionPrincipals engine.list_subscription_principals (deprecated alias kept) #1746
service option subscriptions with SubscriptionOptions ReliableReportingService(subscriptions=SubscriptionOptions(...)); passing it with reporting_subscriptions= is refused #1739
@adcp/sdk/reporting/advanced adcp.reporting.advanced this PR
ReportingSubscriptionEngineError (SUBSCRIPTION_STATE_CONFLICT, _STATE_CORRUPT, _ROSTER_INCOMPLETE, _HOST_FACT_INVALID) ReportingSubscriptionEngineError with code and recovery this PR
PlatformConfigError.configField ReliableReportingConfigurationError.config_field this PR
recordSubscriptionReadback PrincipalService.record_subscription_readback (update_subscription_status warns and delegates) this PR

This PR

  • adcp.reporting.advanced re-exports the building blocks, grouped as in #3542:

    • the engine and its stores;
    • the coverage function;
    • calendar and selection;
    • the capability builder;
    • principal-section validation;
    • the service planner hooks.

    ReportingSubscriptionServiceOptions leaves the adcp.reporting facade. It was added there earlier in this stack and never released. The facade keeps SubscriptionOptions and PrincipalRef.

  • ReportingSubscriptionEngineError(code, message) carries recovery (transient for conflict and roster, terminal for corrupt and host fact) and dispatched=None.

    • ReportingSubscriptionStateConflictError becomes its subclass.
    • An unreadable state document raises ReportingSubscriptionStateCorruptError, which is still a ValueError.
    • The roster walk is capped at 10,000 pages and refuses an unfinished walk instead of using it.
    • Host hooks that return the wrong shape fail with SUBSCRIPTION_HOST_FACT_INVALID.
  • config_field is set on every seller configuration code the SDK raises:

    • SubscriptionOptions.accounts;
    • capability_offerings;
    • storage;
    • the python -m adcp migrate command.

Where Python does not match, and why

  • PrincipalLifecycleError (code, recovery, dispatched, applied → appliedResponse). Python has no counterpart to rename. TS throws it from its principal lifecycle helper, and #3544 itself notes "Python has no principal lifecycle helper". Python's seller PrincipalService returns failed responses instead of raising. The buyer's PrincipalConfigurationError (in adcp.principal) is a different helper's error, with no applied field.
  • PlatformConfigError's default code PLATFORM_CONFIG_INVALID. It is TS-only and not in error-codes.json. Python keeps code=None for uncoded configuration errors and sets a code only for the four spec codes.
  • groupOwedSubscriptionPeriodsV1 and toExpectedReportingPeriodsV1. They have no Python equivalent because none is needed. owed_subscription_periods already returns one row per (version, offering, period), with its media buys grouped, and OwedSubscriptionPeriod already is an ExpectedReportingPeriod, so no conversion exists to rename.
  • The building blocks are still exported from adcp.reporting.subscriptions. TS made reporting/advanced their only home. In Python, adcp.reporting.subscriptions and adcp.reporting.service_subscriptions are the defining modules, and the earlier PRs in this stack and the docs import from them. Deprecating every name there would emit warnings across the suite for no behavioral gain. adcp.reporting.advanced is the documented home; the old paths can be deprecated in a later beta if wanted.
  • ReportingSubscriptionServiceOptions.list_principals keeps its field name (see feat(reporting): use owed and list_subscription_principals, as the TypeScript SDK does #1746).

Tests

  • tests/test_reporting_subscription_ts_parity.py covers:
    • the advanced exports and the slimmer facade;
    • engine error codes and recovery;
    • the roster page cap;
    • a wrong-shape host fact;
    • the readback rename;
    • config_field.
  • The subscription, principal, service, storage-boundary, PostgreSQL production and migration, and public-API suites pass, as do ruff and mypy.

Refs #1695

🤖 Generated with Claude Code

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — cross-SDK parity slice with one non-blocking medium.

What I checked:

  • Public-surface semver: removing ReportingSubscriptionServiceOptions from the facade is non-breaking — it was added in adaba82a4 within this same unreleased stack (no tag contains it) and remains importable via adcp.reporting.advanced and .service_subscriptions, so the feat: prefix holds.
  • The update_subscription_status → record_subscription_readback rename ships with a deprecated delegating alias, preserving backward compat.
  • ReportingSubscriptionStateCorruptError stays a ValueError, preserving existing catch semantics.
  • The 10k-page fail-closed roster cap replaces an unbounded while True — correct direction.
  • high_risk is true only due to a (modified) src/adcp/server/principal.py; no medium-or-higher finding lands on that file, so the modification is presumed safe (row 5 does not fire).

Medium findings:

  • src/adcp/reporting/subscriptions/engine.py:1178 — _account_buys wrong-shape guard raises a raw TypeError on a None/non-iterable host return instead of the typed SUBSCRIPTION_HOST_FACT_INVALID, and the branch is untested.

Decision: Single medium finding, category is not data-loss/schema/infra, high_risk is (modified)-only with no medium on the modified file, no gated paths, no team gate. Rows 1–8 do not fire; falls through to row 9 → approve. Worth tightening the typed error on engine.py:1178 in a follow-up, but it does not block.

Medium findings

  • src/adcp/reporting/subscriptions/engine.py:1178 — _account_buys wrong-shape guard misses None/non-iterable returns

@bokelley
bokelley force-pushed the claude/issue-1695-owed-names branch from 0f89d2a to 105089f Compare October 30, 2026 23:23
@bokelley
bokelley force-pushed the claude/issue-1695-ts-parity branch from 6bc162d to 5b116d5 Compare October 30, 2026 23:23
@bokelley
bokelley force-pushed the claude/issue-1695-owed-names branch from 105089f to 268789b Compare October 30, 2026 23:23
…rors and readback name

Parity with adcp-client#3542, #3544 and #3546:

- adcp.reporting.advanced is the documented home of the low-level building blocks, as @adcp/sdk/reporting/advanced is: the engine and its stores, the coverage function, calendar and selection, the capability builder, principal-section validation and the service planner hooks. adcp.reporting.subscriptions and service_subscriptions keep defining and exporting them. ReportingSubscriptionServiceOptions leaves the adcp.reporting facade (it was added there earlier in this stack, never released); the facade keeps SubscriptionOptions and PrincipalRef.
- ReportingSubscriptionEngineError carries code and recovery for the engine's runtime failures, as TypeScript names them:
  - SUBSCRIPTION_STATE_CONFLICT: ReportingSubscriptionStateConflictError is now a subclass.
  - SUBSCRIPTION_STATE_CORRUPT: an unreadable state document; still a ValueError.
  - SUBSCRIPTION_ROSTER_INCOMPLETE: the roster walk is capped at 10,000 pages, and an unfinished one is refused, never used partially.
  - SUBSCRIPTION_HOST_FACT_INVALID: a host hook returned the wrong shape.
- ReliableReportingConfigurationError gains config_field (TypeScript's configField), set on the seller configuration codes the SDK raises.
- PrincipalService.update_subscription_status is renamed record_subscription_readback (TypeScript's recordSubscriptionReadback); the old name warns and delegates.

Refs #1695

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bokelley
bokelley force-pushed the claude/issue-1695-ts-parity branch from 5b116d5 to f5c119d Compare October 30, 2026 23:23

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — TS-parity slice for the reporting subscriptions surface.

What I checked:

  • Semver signal: clean. The removed facade symbol was never released, and the record_subscription_readback rename keeps a deprecated delegator, so no breaking public-API change ships under a non-breaking prefix.
  • High-risk flag (src/adcp/server/principal.py modified) — reviewer surfaced no medium-or-higher concern on that file, so the modification is presumed contract-preserving.
  • gated_paths: false and review_decision: APPROVED — no gate obstacles.
  • No no-auto-approve team match.

Medium findings (1 — non-blocking):

  • src/adcp/reporting/subscriptions/engine.py:1178 — _account_buys wrong-shape guard misses None/non-iterable host returns; raises raw TypeError instead of typed SUBSCRIPTION_HOST_FACT_INVALID (inconsistent with _list_accounts sibling).

This is a single medium finding carried over from the prior approve. Decision table: no critical/high (row 1 n/a), gated_paths false (row 2 n/a), no (deleted) high-risk reason (row 3 n/a), the lone medium is not data-loss/schema/infra category (row 4 n/a), high-risk (modified) + medium would trigger row 5 — however this medium is on engine.py, not on the high-risk-matched principal.py file, so row 5 does not fire. Prior decision was approve, not escalate (row 6 n/a). No team gate (row 7 n/a). Fewer than three mediums (row 8 n/a). Falls through to row 9 → approve.

Medium findings

  • src/adcp/reporting/subscriptions/engine.py:1178 — _account_buys wrong-shape guard misses None/non-iterable host returns

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — cross-SDK parity slice with 1 medium finding, no blocking issues.

What I checked:

  • ReportingSubscriptionServiceOptions removal from the adcp.reporting facade confirmed non-breaking (never released earlier in this stack per PR body), so feat: prefix is correct — no semver signal violation.
  • ReportingSubscriptionStateCorruptError's (EngineError, ValueError) MRO is valid and preserves ValueError catch compatibility.
  • record_subscription_readback rename ships with a deprecating shim.
  • New public export advanced re-export module added.

Medium findings:

  • src/adcp/reporting/subscriptions/engine.py:1178 — _account_buys wrong-shape guard misses None/non-iterable returns (raises bare TypeError instead of typed SUBSCRIPTION_HOST_FACT_INVALID; path untested).

Decision path: No critical/high findings (row 1 n/a). gated_paths is false (row 2 n/a). high_risk is true but the only high-risk match (src/adcp/server/principal.py) is (modified) with no medium finding on it, and the sole medium is on engine.py — not a high-risk-path file, so row 5 does not fire. No deletions (row 3). The single medium is category code-quality/edge-case, not data-loss/schema/infra (row 4 n/a). Prior decision was approve, not escalate (row 6 n/a). No no-auto-approve team match (row 7 n/a). Only 1 medium finding (row 8 requires ≥3). Falls through to row 9 → approve.

Note: the engine.py:1178 medium carried over from the prior approve run and remains unresolved, but a single medium is below the blocking/comment threshold.

Medium findings

  • src/adcp/reporting/subscriptions/engine.py:1178 — _account_buys wrong-shape guard misses None/non-iterable returns

…f the wrong shape

Review follow-ups:

- An authorized_at regression is counted and logged after the engine-state write, not inside the change, which reruns on a compare-and-swap conflict. Under contention one regression used to count and log once per attempt.
- account_buys returning None, a non-sequence or a string now fails with SUBSCRIPTION_HOST_FACT_INVALID instead of a raw TypeError.
- The managed path's page-cap comment covers buys only; the engine caps the roster walk.

Refs #1695

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

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — follow-up pass clean.

This is a subsequent review on a prior approve decision. The one prior medium finding (_account_buys wrong-shape guard at engine.py:1178) has been resolved: engine.py:1193 now rejects non-Sequence returns before iterating, with test coverage for None/int/str/wrong-element. The regression-counting refactor additionally fixes a real double-count bug by moving counting/logging out of the _mutate CAS-retry closure into _report_regressions (called once post-write), validated by test_a_regression_is_counted_once_under_write_conflicts.

No current blocking or medium findings. high_risk is true only because src/adcp/server/principal.py is modified, but with no medium-or-higher concern on that file, the modification is presumed safe. gated_paths is false and review_decision is APPROVED. No no-auto-approve team match.

Decision table: rows 1–8 do not fire (no critical/high, gated_paths false, no deletions, no medium findings, no team gate, prior decision was approve not escalate). Falls through to row 9 → approve.

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

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ladon verdict: Approve

Approve — clean pass, no blocking or medium findings.

Checked: this PR mirrors the TS SDK parity slice — adds adcp.reporting.advanced, the ReportingSubscriptionEngineError code/recovery hierarchy, config_field on seller configuration errors, and renames update_subscription_status to record_subscription_readback. The rename keeps a delegating DeprecationWarning wrapper, so there is no public-surface break; ReportingSubscriptionServiceOptions leaving the facade is an unreleased-stack change tracked by the public_api_snapshot.json fixture — feat: is the correct semver signal (no feat!:/BREAKING CHANGE: required). Operational readiness is sound: the roster walk is bounded (10k pages) and fail-closes on an unfinished walk, host facts are shape-validated (SUBSCRIPTION_HOST_FACT_INVALID), and the regression-counter refactor counts once under write conflicts (covered by the _ConflictingStore test).

Decision table: high_risk is true, but the only matching reason is src/adcp/server/principal.py (modified) with no medium-or-higher finding on it, so row 5 does not fire. gated_paths is false and review_decision is APPROVED. Prior decision was approve. No findings at any tier → falls through to row 9.

This branch has not been deployed

No deployments
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