Skip to content

feat(reporting): hold subscription obligations in the reporting ledger - #3517

Merged
bokelley merged 65 commits into
mainfrom
bokelley/reporting-subscription-ledger
Oct 31, 2026
Merged

bokelley merged 65 commits into
mainfrom
bokelley/reporting-subscription-ledger

Conversation

@bokelley

@bokelley bokelley commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fourth of the series for #3504, and the first of two stacked PRs that write subscription obligations into the reporting ledger. It stacks on #3509, so it contains the commits of #3505, #3508 and #3509. Review only feat(reporting): hold subscription obligations in the reporting ledger (a74b552). It targets main because CI runs only for PRs into main. The follow-up PR wires the service to the engine.

The engine already computes the seller's owed set, but nothing wrote it into the ledger, so the producer never executed subscription obligations. This PR makes the ledger and the PostgreSQL store hold them, and adds a planner API.

What ships

Identity

There is one ledger generation per (account, owning principal, subscription_id, subscription_version, resolved offering_id).

  • delivery_config_id is the subscription_id and delivery_config_version is the subscription_version, as get-reporting-status-response.json subscription_coverage requires.
  • ReportingLedgerConfigurationV1.subscription is an immutable block in the fingerprint: { tenantId, principalId, subscriptionId, offeringId, ownership: 'owed_set' }. Obligations copy it, so ownership can be checked from the obligation alone.
  • A subscription generation's configurationId digests the owner and offering as well. Account generations keep exactly today's id and fingerprint (tested), and so do their rows.
  • installConfiguration refuses a subscription block; subscription generations go only through the planner.

Planner API

producer.planSubscriptionObligations({ account_id, principal, subscriptionId, configurations, obligations, owedThrough, now? }):

  1. Installs each (version, offering) generation once, idempotently. A changed input throws ReportingConfigurationImmutableError.
  2. Writes each obligation through store.putObligation. Each one carries:
    • its own period
    • media_buy_ids frozen at its scope_resolved_at (the period end)
    • the constituents of exactly those buys
    • periodOrdinal = floor(period_start_ms / 1000)
    • expected_at = period end + the offering's delivery_sla
  3. Advances owedSet.through on every generation this subscription owns on the account, after the obligations are durable.

Obligations are never grid-filled: planObligations skips owed-set generations. Retired periods are skipped.

Owed-set completeness

owedSet is mutable and outside the fingerprint, the same way historyStart is. It holds:

  • through: the completeness horizon
  • from, until and mediaBuyIds: a monotonic summary of the written obligations, used for relevance and media_buy_ids scope matching

The generation's own mediaBuyIds/constituents are empty and never read. evaluateReportingLedgerCoverageV1 has an owed-set branch:

  • The obligations present are the owed set by construction, so a gap never raises a continuity error. This holds in both the memory store and PostgreSQL createSnapshot.
  • A scope whose end is after through returns owedSetOpen. The handler then reports scope_closed: false and keeps coverage_complete: true, because an owed set still being frozen is not missing history (coverage_complete: false would force HISTORY_UNAVAILABLE).

The other grid paths use only existing obligations for an owed set:

  • next_expected_at projects nothing from the grid.
  • The successor and history-overlap rules ignore owed sets.
  • In status ingest, a status without reporting_obligation_id matches an existing owed obligation by ordinal (new optional findObligationByOrdinal, falling back to listObligations), never a bare grid slot.
  • The frozen-calendar proof skips owed sets.

Visibility

ReportingLedgerSnapshotQueryV1.subscription_principal scopes snapshots to account generations plus the caller's own subscription generations and obligations. With no principal, no subscription generation is visible. It is part of the snapshot query, so cursors and changes checkpoints are bound to it.

  • createReportingStatusHandler, createReportingDeliveryHandler and createSyncReportingStatusHandler take resolveSubscriptionPrincipal.
  • The status handler fails closed if a store returns another principal's generation.
  • get_media_buy_delivery answers another principal's revision with REFERENCE_NOT_FOUND and never falls through to the legacy reader.
  • Status ingest treats another principal's generation as unknown.
  • offering_id is emitted on subscription obligations to callers served 3.3. It is in 3.3.0-beta.1's reporting-obligation.json.

Store contract and PostgreSQL

  • New optional ReportingLedgerStore.recordOwedSetProgress (monotonic merge under the account fence) and findObligationByOrdinal. The producer refuses to plan into a store without recordOwedSetProgress.
  • REPORTING_LEDGER_MIGRATION changes the generation key:
    • It adds subscription_key TEXT NOT NULL DEFAULT '' under a catalog guard. The key is reportingLedgerSubscriptionKeyV1: a digest of owner and offering, empty for account generations.
    • It creates the unique index adcp_reporting_configurations_generation (account_id, delivery_config_id, delivery_config_version, subscription_key) before dropping the published three-column constraint.
    • A guarded DO $$ block finds that constraint in pg_constraint by its columns, not by name.
    • The migration is idempotent and additive, and existing rows keep their uniqueness. A fresh schema creates the table without the old constraint.
  • The version-regression check and history-overlap reads are scoped to subscription_key = ''.
  • Snapshot configurations and obligations filter on data->'subscription'.
  • The consumer-status snapshot query uses EXISTS instead of a join, since several visible generations can share an (id, version). For account generations the result is the same, because the join matched at most one row.
  • The memory test store mirrors all of this.

Tests

  • test/lib/reporting-subscription-ledger.test.js (memory store):
    • sparse obligations with no continuity error and no grid fill
    • owedThrough completeness: scope open, then closed, and never regressing
    • several offerings in one version
    • two principals with the same subscription_id: generations, snapshots, revision reads, and a leaky-store refusal
    • frozen media_buy_ids per period, and media-buy scope matching
    • status ingest that accepts only the caller's owed periods
    • unsafe plans refused
    • account generation ids and fingerprints unchanged
  • packages/reporting-storage/test/reporting-subscription-ledger-pg.test.js, in the reporting-storage parallel group that CI runs with REPORTING_LEDGER_PG_URL:
    • the migration on a fresh schema and on the published layout with an existing row, twice each
    • the dropped constraint and the widened key
    • two principals and an account generation sharing (account, id, version)
    • owed-set progress and findObligationByOrdinal
    • per-principal snapshots and the status handler

Spec ambiguities

  • scope.delivery_config_generations items are (delivery_config_id, delivery_config_version, feed_purpose) with uniqueItems. Every offering one subscription version resolves shares the selector's feed_purpose, so per-offering generations collapse into one wire entry. Obligations tell them apart by offering_id.
  • The consumer-status chain identity (delivery_config_id, delivery_config_version, report_definition_id, period) cannot tell apart two offerings of one version that share a report definition and a period. Ingest accepts the status against any matching visible generation.
  • next_expected_at in a complete summary "MUST" name the next scheduled period start. An owed set's future periods depend on buys the ledger cannot see, so owed-set generations contribute none.
  • The spec says nothing about how a seller signals that an owed set is not yet frozen through a scope's end. I use scope_closed: false and leave coverage_complete true.

Deviations from the brief

  • Visibility filters on JSON, with no owner column. It uses data->'subscription' rather than adding a column beside subscription_key.
  • The plan input names the account, owner and subscription_id explicitly. That way a cycle with no new obligations still advances the horizon of existing generations.
  • owedSet also carries from, until and mediaBuyIds. Relevance and media_buy_ids scope checks need them. All of them only widen.
  • No version-regression check for subscription generations. An older version's offering can be frozen after a newer version's generation exists.
  • Grid schedule validation is skipped for owed-set generations. The ledger accepts any engine period (P1M, account_timezone, sub-daily) and stores the offering's schedule identity verbatim.

Local checks

  • typecheck, typecheck:reporting-storage, lint:boundaries, check:package-size and Prettier pass. The packed budget is +60 KB with a comment; no module files were added.
  • The ledger, service, status-ingest and subscription suites pass.
  • The new PostgreSQL suite, reporting-history-pg and most of reporting-status-ingest-pg pass against PGlite locally. CI runs them on PostgreSQL 16.

Update: main's beta release (#3521) changed scripts/check-package-size.mjs, which conflicted with the stack. I merged origin/main into bokelley/reporting-subscription-ledger and carried that merge up through the stacked branches. The only conflict was the file-count budget, and I resolved it by keeping both sides' entries. The merge commits change nothing else.

Refs #3504

🤖 Generated with Claude Code

bokelley and others added 19 commits October 29, 2026 09:30
Add the pure expectedSubscriptionPeriods rule for AdCP 3.3 principal-level
reporting subscriptions (experimental media_buy.reporting_subscriptions),
the normative period_generation calendar it uses, selector resolution
(exact, finest, all with byte-order ties), scope membership, and a builder
for the 3.3 registration capabilities (configuration_tasks, the deprecated
configuration_task, and the subscriptions block).

createAdcpServerFromPlatform adds media_buy.reporting_subscriptions to
experimental_features only beside a subscriptions block.

installConfiguration now throws a typed ReportingConfigurationImmutableError
with the unchanged message.

The cross-SDK parity table is
test/fixtures/reporting-subscriptions/coverage-cases.json.

Refs #3504

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

Advertising media_buy.reporting_subscriptions also requires
adcp.principal.supported_sections to list reporting_subscriptions, which the
SDK does not emit yet. Keep the capability builder pure in this slice and add
the feature id together with the principal block in the runtime slice, so an
adopter can never produce a 3.3-invalid capabilities response.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adopt the rules adcontextprotocol/adcp#8585 settled:

- Selection reads each offering's published standard_dimensions and
  unmapped_dimensions, and fails closed on an offering without them.
- period_timezone: a utc offering matches only UTC or Etc/UTC; a
  source_timezone offering only its own published zone (never an
  account-resolved one).
- The coverage function takes the readback's inactive_intervals in wire
  shape, per-offering timezone_cutovers with previous_timezone
  (account.timezone is now the current zone), and per-offering gaps.
- The parity table fixes the billing-cycle case's scope (both keys, which
  maxProperties 1 forbids), adds cases for the settled rules, and its
  subscriptions are now schema-validated.

The new wire fields are hand-modeled in subscriptions/wire.ts until
3.3.0-beta.2 ships.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- An empty flight (stop at or before start) overlaps nothing.
- A cutover carries the outgoing stretch's own owing start into E, so two
  cutovers inside one period never owe a period twice.
- Sub-daily periods under a local alignment restart at each local midnight
  on wall-clock multiples (adcp#8588); calendars gain next() and the walk
  iterates by instant with an iteration budget and skip-ahead.
- A billing anchor in a DST fold is interval zero itself.
- A wire authorized_at equal to the chain start continues the chain.
- The start rule lives in one function for the pending owner decision.
- Schema validation accepts unknown reporting-dimension values.
- The parity table grows to 79 cases with obligations on every case and
  non-wire inputs labelled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The start bound is the first boundary at or after the later of the
version's accepted_at and the account's authorized_at; a buy is owed every
period that ends after its confirmed_at and overlaps its flight, so the
period in which the buy is confirmed is owed in full. The rule stays in
firstOwedBoundary. Parity table: the mid-period confirmed_at and hourly
cases change, plus every case whose first period contains confirmed_at;
new cases for an ASAP buy confirmed mid-day and a monthly billing buy
confirmed on the 12th.

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

Opt-in support for the AdCP 3.3 sync_principal reporting_subscriptions
section (experimental media_buy.reporting_subscriptions):

- createPrincipalLifecycle({ reportingSubscriptions }) validates, versions
  and reads back subscriptions (core/reporting-subscription-state.json),
  honours dry_run and expected_configuration_version, applies
  replace_account_configs once per version through a host hook, derives
  action_required from the destination, and emits principal.changed with
  reason reporting_subscription_changed. It also exposes the adcp.principal
  capability block.
- createReportingSubscriptionEngineV1 turns three host hooks into grant
  history, max_accounts slots, precedence, exceptions at cutover instants,
  timezone cutovers, re-grant gaps, accounts_unauthorized, the receipt
  roster and the post-loss receipt window, and computes owed periods with
  the same expectedSubscriptionPeriods rule buyers run.
- createReliableReportingService({ reportingSubscriptions }) advertises
  configuration_tasks and the subscriptions block and adds
  subscription_coverage to summary and periods views.
- createAdcpServerFromPlatform passes capabilities.principal and
  capability_changes through, adds protocol.principal and
  media_buy.reporting_subscriptions to experimental_features, refuses
  subscriptions without the reporting_subscriptions section, and strips
  3.3-only fields for callers pinned below 3.3.

Without the options every response is byte-for-byte unchanged; a golden
transcript captured from the pre-change runtime proves it.

Refs #3504

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

- readFacts refuses a roster that does not finish within the page cap, as it
  already did for a repeating cursor: a partial roster would read as
  revocations and fabricate accounts_unauthorized entries.
- Callers pinned below 3.2 no longer see protocol.principal once the
  adcp.principal block is removed for them.
- subscription_coverage is added only for callers served 3.3 or later;
  earlier get_reporting_status response shapes are closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The placeholder regex backtracked polynomially on runs of '{' (CodeQL).
Tokenize braces in a single pass instead, which also rejects nested and
unbalanced braces explicitly.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The principal readback lists inactive_intervals; the engine feeds them
  to the coverage function in wire shape.
- Timezone cutovers carry previous_timezone and gaps are recorded per
  offering_id when covered offerings' boundaries differ.
- authorized_at is the start of the authorization chain: a re-grant inside
  retention keeps it, so the engine stamps the re-grant when authority is
  observed again.
- The service requires standard_dimensions on every offering when
  subscriptions are advertised.
- Responses served at 3.3.0-beta.1 omit the fields its closed schemas do
  not define; they reach callers served 3.3.0-beta.2 or later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts, sharded engine state

Addresses the expert review of the subscription runtime:

- Owed periods are frozen as they elapse and never recomputed; pruning past
  status_retention_days never re-owes or drops an elapsed period. A product,
  package or offering change applies from the E of its observation, and
  periods of that buy starting earlier keep resolving from the buy as it was.
- Change instants come from source data (granted_at, timezone_changed_at,
  resolveRevocationInstant, the overriding configuration's active_until).
- The engine store is sharded per principal and per account; the receipt
  roster is tenant-scoped; an unobserved revocation followed by a new chain
  opens a gap.
- replace_account_configs runs after commit and is retried on failure; a
  destination transport change needs a version; inactive_intervals over 64
  is rejected; shared path_templates that can expand to the same path are
  rejected; the delivery-config clash check is required unless opted out;
  coverage recording rejects oversized lists and no longer bumps
  configuration_version.
- Post-beta.1 fields are gated on the served bundle's schema, and an unknown
  served version emits none of them. prior_versions[] carries configuration
  (adcp#8588).

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

A small max_accounts against a large roster gives every account past the
cap a capacity exception, which can exceed the readback's 1000-entry
limit. The reconcile used to throw for the whole principal on every pass.
Now that subscription keeps its last published lists, every other
subscription still updates, and the pass reports it in readback_overflow
and through onReadbackOverflow.

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

- syncReportingSubscriptionsV1 writes and reads back the
  reporting_subscriptions section through the guarded sync_principal
  lifecycle.
- reportingSubscriptionVersionRecordsV1 rebuilds coverage version records
  from the readback plus the buyer's own per-version configurations, and
  subscriptionExpectedReportingPeriodsV1 maps owed obligations onto the
  consumer runtime's ExpectedReportingPeriod.
- The consumer runtime takes a per-account subscriptionExpectations hook
  fed with the summary view's subscription_coverage, and refreshes on an
  authenticated principal.changed through handleAuthenticatedPrincipalChanged.
- createReportingSubscriptionEngineStateStoreV1 persists engine state as one
  CAS document per principal on any AdcpStateStore; PostgresStateStore makes
  it durable with no further migration. A PostgreSQL restart test joins the
  CI backend-contracts step.

Refs #3504

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

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

reportingSubscriptionVersionRecordsV1 no longer takes buyer-recorded
inactive intervals: the readback's inactive_intervals (adcp#8585) pass to
expectedSubscriptionPeriods unchanged. Test offerings publish
standard_dimensions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
listPrincipals feeds the receipt roster, so a list cut short at the page
cap, or a repeating cursor, now throws instead of returning a partial list,
matching the authorized-account walk.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The engine state store follows the engine's sharded interface: one CAS
  document per principal and per (principal, account), with listKeys and a
  tenant-scoped listPrincipals.
- Subscription expectations get the summary's ledger_as_of (minus an
  optional grace) as now, and the hook is documented as requiring a seller
  that materializes subscription obligations.
- Expectation templates must supply coverage; the old default could never
  match an obligation reporting packages or partial coverage.
- principal.changed redeliveries are dropped by idempotency_key, and a
  burst during a run joins one trailing run.
- syncReportingSubscriptionsV1 refuses a deactivation by omission unless
  allowDeactivation, checked against every configuration read it
  replaces, and verifies the stored configuration field by field.
- reportingSubscriptionVersionRecordsV1 falls back to the readback's
  configuration and prior_versions[].configuration (adcp#8588).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owed-set generations for AdCP 3.3 principal-level reporting subscriptions
(#3504): one generation per (account, owning principal, subscription_id,
subscription_version, resolved offering), with an immutable `subscription`
block and a mutable `owedSet` horizon outside the fingerprint.

- producer.planSubscriptionObligations installs each generation once,
  writes every frozen owed obligation with its own period, media buys and
  constituents (periodOrdinal = start second), and then advances
  owedSet.through on every generation the subscription owns.
- Coverage treats an owed set as complete by construction: gaps are not
  owed, so there is no continuity error, and the scope stays open until
  owedSet.through reaches its end.
- get_reporting_status, get_media_buy_delivery and sync_reporting_status
  scope subscription generations to the caller's principal
  (resolveSubscriptionPrincipal, snapshot `subscription_principal`).
- Status ingest accepts a status without reporting_obligation_id against an
  owed set only for an existing owed obligation.
- PostgreSQL: additive `subscription_key` column and a unique index on
  (account_id, delivery_config_id, delivery_config_version,
  subscription_key) replacing the three-column constraint, found and
  dropped by its columns. Mirrored in the memory test store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 29, 2026

@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 — AdCP 3.3 principal-level reporting subscriptions (experimental, opt-in) across 52 files, no blocking findings.

Checked:

  • No Critical/High/Medium findings reported.
  • Witness-not-translator honored: 3.3-only wire fields (reporting-subscription-release.ts, create-adcp-server L6619-6656, handler offering_id gate) emitted only when the served bundle defines them, not fabricated; handler re-checks visibility (L1671-1681).
  • Multi-tenant isolation enforced in SQL via EXISTS keyed on subscription tenant/principal (postgres.ts L389-401, L432-451); migration is additive/idempotent, preserving existing (account,id,version) uniqueness.
  • engine.ts fails closed on partial rosters and freezes each elapsed period exactly once (no re-owing); coverage.ts owed-period walk is bounded; producer.ts widens owed-set before obligations land then advances through once durable.
  • path_template guarded against traversal / shared-destination collision.
  • Changesets correct (4 minor, incl. @adcp/reporting-storage for the migration); CI wires the pg test.

Decision table: no critical/high (row 1 n/a); gated_paths false (row 2 n/a); high_risk false (rows 3/5 n/a); no medium findings (rows 4/8 n/a); no prior decision (row 6 n/a); no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve. Note: review_decision is REVIEW_REQUIRED but gated_paths is false, so row 2 does not apply.

# Conflicts:
#	scripts/check-package-size.mjs
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@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 subsequent pass over the beta.6/beta.7 delta.

What I checked:

  • Task-envelope status preservation: relaxResponseRoot replaced by per-release acceptEnvelopeOnClosedResponseRoot + omitsEnvelopeStatusForRelease. Release-accurate (3.2.x closed arms omit status, 3.3 composes/requires it), witness-preserving (seller payload untouched; status mutation is validation-view only via prepareResponseForSchemaValidation), and backed by the response-envelope-status-gating suite.
  • Opt-in collect_product_pages wholesale walker: merged logical view explicitly labelled separate from retained physical pages; per-page schema/invariant grading via onCollectedProductPage; fresh unkeyed reads enforced at loader and dispatch.
  • Two new default-on discovery invariants (pricing-currency, required-metric): cite normative ProductFilters basis; report unproven cases as indeterminate rather than fabricating requirements — consistent with witness-not-translator.
  • Changesets present and correctly typed (minor/minor/patch).

No Critical/High/Medium findings. Prior pass was also clean (approve). high_risk: false, no gated paths, no team gate. review_decision is REVIEW_REQUIRED but gated_paths is false, so row 2 does not fire. None of rows 1–8 apply → row 9 approve.

bokelley and others added 4 commits October 30, 2026 04:05
…bscription-coverage

# Conflicts:
#	scripts/check-package-size.mjs
…dules after merging main

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

# Conflicts:
#	scripts/check-package-size.mjs
…er merging main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bokelley and others added 6 commits October 30, 2026 19:04
…ndle

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

Main adopted 3.3.0-beta.2, which no longer ships the beta.1 bundle and
defines the fields settled after beta.1 (standard_dimensions,
inactive_intervals, prior_versions[].configuration, previous_timezone,
per-offering entries), so the release gate now emits them to beta.2
callers. inactive in coverage stays gated until beta.3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s beta.2 adoption

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

# Conflicts:
#	scripts/check-package-size.mjs
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@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 on the AdCP 3.3.0-beta.1→beta.2 adoption.

Checked:

  • Regenerated type/wire artifacts + version-string bumps (generated/trivial).
  • 3.3.0-beta.2 commercial-terms semantic overrides and the signals SIGNAL_NOT_FOUND→REFERENCE_NOT_FOUND convention, consistent across docstring/example/skill.
  • Typed RequestValidationError/isPreDispatchError marker (code='VALIDATION_ERROR' inherited from ValidationError; dispatched:false at every pre-send gate; storyboard runner's caughtError marker check in scope).
  • ESM envelope-status loader binding (#3529 — no circular static import; cache cleared on loader swap).
  • Largest non-generated file src/lib/v2/projection/creative-delivery.ts: reconcileLegacyAssignmentRefs per-axis merge with independent-set conflict detection and fail-closed on unparseable size/duration constraints is sound; packageSizeAcceptor stated-axis short-circuit is deliberate and locally documented.
  • Changesets present and typed correctly for the beta pre-mode train.

No critical/high/medium findings. high_risk false, gated_paths false, no author-team gate. Prior decision was approve; delta remains clean. Row 9 → approve.

Note: review_decision is REVIEW_REQUIRED, but since gated_paths is false, row 2 does not apply.

bokelley and others added 4 commits October 30, 2026 19:50
@aao-secretariat

Copy link
Copy Markdown

Ladon cannot review this PR until merge conflicts are resolved.

bokelley and others added 4 commits October 30, 2026 20:20
…bscription-coverage

Brings in #3548. The packed-byte cap keeps the higher of main's and this branch's.

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

# Conflicts:
#	scripts/check-package-size.mjs
…reporting-subscription-consumer

# Conflicts:
#	scripts/check-package-size.mjs
…/reporting-subscription-ledger

# Conflicts:
#	scripts/check-package-size.mjs
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@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 subsequent pass, no blocking or medium findings.

This is a follow-up pass to a prior approve. The delta is 67 lines across 4 files, entirely CI/test-only:

  • scripts/check-package-size.mjs: tarball budget bump with a measured rationale (26,369,089 bytes after #3541 vs 26,520,000 cap).
  • A documentary empty-frontmatter changeset.
  • A line-collapse in reporting-subscription-coverage.test.js.
  • pre-dispatch-errors.test.js: swaps a retired hardcoded 3.3.0-beta.1 pin for ADCP_VERSION. The release/feature-gate test still asserts ProtocolFeatureUnsupportedError with dispatched:false against a seller advertising only 3.3-beta.0, so coverage holds and is forward-safe.

No src/lib or bin changes, so no changeset is required. Decision-table check: no critical/high/medium findings (row 1, 4-6, 8 don't fire); gated_paths is false (row 2 n/a); high_risk is false (rows 3/5 n/a); no no-auto-approve team match (row 7 n/a); prior decision was approve (row 6 n/a). Falls through to row 9 → approve.

bokelley and others added 6 commits October 30, 2026 22:24
A sub-daily billing_cycle was routed to the local sub-daily calendar,
which aligns to local midnight and ignores period_anchor, so every
boundary was off by the anchor's time of day. adcontextprotocol/adcp#8622
makes a sub-daily billing_cycle invalid; refuse it.

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

A later write that merely omitted replace_account_configs cleared both the
applied marker and a committed-but-unconfirmed replacement, so a failed host
hook was silently abandoned. An omitted flag now leaves both untouched; an
explicit false clears them, so a later true applies again.

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

- An open action_required exception now takes the host's current
  pending_action (url, message, reason) when it changes, keeps its from, and
  notifies principal.changed. It used to keep the first action forever.
- accounts_unauthorized last_owed_period_end takes the latest frozen
  period_end. Frozen periods are not stored in end order, so the last entry
  could under-state the window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 30, 2026

@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 — subsequent pass on delta (229 lines, 6 files); prior pass was clean and approved.

What I checked:

  • Decision table top-down: no critical/high findings (row 1 n/a); gated_paths is false (row 2 n/a despite review_decision REVIEW_REQUIRED — row 2 only fires when gated_paths is true); high_risk is false (rows 3/5 n/a); no medium findings so rows 4/8 n/a; prior decision was approve so sticky row 6 n/a; no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.
  • Reviewer surfaced no findings. Three defensive fixes reviewed as sound and tested: calendar.ts fail-closed refusal of sub-daily billing_cycle (adcp#8622); engine.ts latestPeriodEnd now scans for max period_end instead of trusting last-appended; in-place refresh of open action_required exclusion's pending_action with canonicalize ?? null guard; server replace_account_configs reset narrowed from !== true to === false so an omitted flag no longer wipes a failed-hook pending retry.
  • Changesets present (4 reporting-subscription changeset files) covering the src/lib/** changes — changeset-vs-wire-impact satisfied.
  • No witness-not-translator concerns; changes are defensive/fail-closed, not fabrication.

No blocking or medium findings. Clean net-positive iteration on an already-clean PR.

@bokelley
bokelley merged commit 0187eb3 into main Oct 31, 2026
45 checks passed
@bokelley
bokelley dismissed aao-secretariat[bot]’s stale review October 31, 2026 14:18

The merge-base changed after approval.

@bokelley
bokelley deleted the bokelley/reporting-subscription-ledger branch October 31, 2026 14:18
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