Skip to content

feat(reporting): plan and execute subscription obligations in the service - #3518

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

bokelley merged 77 commits into
mainfrom
bokelley/reporting-subscription-ledger-service

Conversation

@bokelley

@bokelley bokelley commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fifth of the series for #3504, and the second of two stacked PRs that write subscription obligations into the reporting ledger. It stacks on #3517 (which stacks on #3509), so it contains every earlier commit. Review only feat(reporting): plan and execute subscription obligations in the service (209ded8). It targets main because CI runs only for PRs into main.

What ships

Engine

  • frozenOwedSets(principal, { accountId? }) returns, per (account, subscription):

    • the frozen horizon through
    • the frozen obligations, grouped with byte-sorted media_buy_ids
    • every retained version's configuration

    It never returns the provisional periods owedPeriods computes past through, because ledger obligations are immutable.

  • listPrincipals(tenantId) wraps the engine store.

Service

In createReliableReportingService({ reportingSubscriptions }), runCycle first plans each listed principal's frozen owed sets, on the cycle's account. It then plans account configurations as before, and the worker executes both.

  • Each (version, offering) generation goes through the same trusted path as installConfiguration. That path is now one shared prepareGeneration:

    • the delivery-offering checks
    • resolveSource, resolveCurrency and resolveCoverage
    • the reserved-key and credential checks on the source scope
    • the frozen schedule identity

    For a generation from a subscription version and its resolved offering:

    • The subscription version supplies identity, feed_purpose and required_finality.
    • The resolved offering supplies the report definition and schedule.
    • Owed-set generations skip only assertSupportedScheduleSemantics and the keyless-predecessor reuse.
  • Each obligation freezes the host-authorized constituents of exactly its media buys. If a media buy the engine says is owed has no authorized constituent, that subscription fails closed.

  • owedThrough is min(engine through, cycle now).

  • Option changes:

    • New and required when subscriptions are advertised:
      • listSubscriptionPrincipals(), for example engine.listPrincipals(tenantId) across your tenants
      • resolveSubscriptionConfiguration({ principal, account, subscription, offering, sourceOffering }), which returns requested metrics and dimensions, source settings, and the finalityPolicy or canonicalization an official or billing subscription needs
    • Optional: resolveSubscriptionAccount(accountId), which defaults to { id, ctx_metadata: {} }, and onPlanningError.
  • Failures are isolated per subscription. One subscription never stops another or the account's own configurations. Without onPlanningError, runCycle rejects with an AggregateError after the rest of the cycle has run.

  • resolvePrincipal now also scopes reads and status ingest. get_reporting_status, get_media_buy_delivery and sync_reporting_status use it through feat(reporting): hold subscription obligations in the reporting ledger #3517's resolveSubscriptionPrincipal. Without subscriptions no scope is passed, and every handler is exactly the pre-3.3 one.

  • Docs: docs/guides/REPORTING-LEDGER.md gains a subscription section.

Tests

test/lib/reporting-subscription-service.test.js:

  • Option off, byte for byte. A deterministic transcript covers install, a cycle, the summary and periods views, sync_reporting_status, get_media_buy_delivery, and the ledger records behind them. It is compared against test/fixtures/reporting-subscriptions/ledger-option-off-golden.json, captured by building and running the same transcript on feat(reporting): buyer runtime and durable storage for reporting subscriptions (#3504, 3/3) #3509's head (800adfc). The only normalization replaces real-clock install and creation stamps.
  • End to end. Two principals hold the same analytics-daily subscription on one account with different buys. The engine reconciles, and one cycle plans and executes 5 obligations through the inline source. Each source request carries that period's frozen media buys, including a period with two buys. A second cycle is idempotent. Visibility:
    • Principal A sees its 4 periods with offering_id, one generation, and health complete.
    • B sees its 1 period.
    • A principal without a subscription sees nothing.
    • B cannot read A's revision by id.
  • Failure isolation. One principal's policy hook throws: the other principal's obligations are still planned and executed, and the cycle rejects with an AggregateError.
  • Construction. The service refuses to construct without the required hooks.

reliable-reporting-service.test.js passes the new required options.

Deviations from the brief

  • resolveSubscriptionConfiguration is a required host hook. The SDK has no code path that maps a sync_accounts configuration onto install input; the host does that today. sourceSettings (version, digest, snapshot ref) and the official finality policy are seller policy that cannot be derived from the subscription or the offering.
  • The service still refuses non-utc/source_timezone and non-whole-day offerings at construction. Lifting that gate would advertise offerings that account configurations cannot install. So in this service, subscription periods are whole-day utc/source_timezone, even though the ledger API (feat(reporting): hold subscription obligations in the reporting ledger #3517) accepts any engine period. Relaxing the construction gate for subscription-only offerings is a follow-up.
  • A resolved offering with managed delivery is refused per subscription. That means a method, or a reconciliation_mode other than delivery_only. Managed delivery is out of scope here.

Left for the managed-delivery PR

  • Destination. A subscription generation carries subscription.{tenantId, principalId, subscriptionId} and its version. Resolve the destination from that version's destination.destination_id through the principal runtime (listReportingSubscriptions), and expand path_template with expandReportingSubscriptionPathTemplate.
  • Managed bindings. Binding install looks generations up by (account, delivery_config_id, delivery_config_version) (managed/postgres.ts), and adcp_reporting_managed_bindings is UNIQUE (account_id, delivery_config_id, delivery_config_version). Both need the configuration id or subscription_key.
  • Lifecycle notifications. subscribers receive status transitions of every principal's subscription obligations on an account. Notification fanout should be scoped to the owner.
  • Receipts after lost authority. engine.acceptsReceipt still has to be applied in receipt and status ingest.

Local checks

  • typecheck, lint:boundaries, check:package-size and Prettier pass.
  • 566 tests pass across the ledger, service, status-ingest, calendar, consumer-status and subscription suites, including the engine, coverage, consumer and principal suites.

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 21 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>
…vice

createReliableReportingService({ reportingSubscriptions }) now writes the
engine's frozen owed sets into the ledger every cycle, before account
configurations, and the worker executes them (#3504).

- engine.frozenOwedSets(principal, { accountId? }) returns each
  subscription's frozen obligations, horizon and version configurations;
  engine.listPrincipals(tenantId) lists principals from the engine store.
- Generations go through the same trusted path as installConfiguration
  (resolveSource, resolveCurrency, resolveCoverage), with seller policy from
  the new resolveSubscriptionConfiguration hook. Each obligation freezes the
  authorized constituents of exactly the media buys it owes.
- New options: listSubscriptionPrincipals (required),
  resolveSubscriptionConfiguration (required), resolveSubscriptionAccount and
  onPlanningError. Planning failures are isolated per subscription.
- resolvePrincipal now also scopes get_reporting_status,
  get_media_buy_delivery and sync_reporting_status to the caller's own
  subscription generations.
- Without reportingSubscriptions the service is byte-for-byte unchanged,
  proven by a golden transcript captured from the service before the change.

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

A source-calendar P1D generation freezes the runtime's tzdb and ICU versions
and the install day into its identity, so the golden differed on CI runners.
The transcript now uses a UTC-aligned offering; the golden is recaptured from
#3509's head (800adfc) and still matches byte for byte.

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 — no blocking or medium findings.

Reviewed the reporting-subscription ledger/service work (focus commit 209ded8 plus the load-bearing paths it builds on). Verified fail-closed behavior where it matters: owed-buy coverage check, owedThrough capped at the cycle clock, per-subscription failure isolation with abort propagating outside the try, and multi-tenant visibility gated by subscription_principal with handler-level defense-in-depth. The postgres migration is catalog-guarded and builds the widened unique index before dropping the legacy constraint — additive/idempotent. Five changesets correctly cover the opt-in additive surface; new 3.3 wire fields are emitted only when the served bundle defines them, satisfying the DR-0033 pathway-back rule.

Decision trace: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); high_risk false, no deletions (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 with gated_paths false that does not block auto-approve.

# 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, no blocking or medium findings.

Checked against the decision table top-down:

  • Rows 1 (critical/high) — no findings of any severity.
  • Row 2 (gated_paths) — gated_paths is false; does not fire despite review_decision=REVIEW_REQUIRED.
  • Row 3/5 (high_risk) — high_risk is false.
  • Row 4 (medium data-loss/schema/infra) — no medium findings.
  • Row 6 (sticky escalate) — prior decision was approve, not escalate.
  • Row 7 (no-auto-approve team) — no team match.
  • Row 8 (≥3 medium) — zero medium findings.
  • Row 9 — falls through to approve.

Reviewer verified the load-bearing concerns for this SDK: acceptEnvelopeOnClosedResponseRoot is stricter than the removed relaxResponseRoot (closed roots stay additionalProperties:false, body typos still rejected, composing bundles untouched); collectWholesaleProductPages loop is bounded with abort checks and idempotency; per-page invariant failures propagate correctly; witness-not-translator preserved (payload returned verbatim, no fabrication); removed/renamed exports are @internal so changeset impact is minor and appropriate.

No wire-fidelity, changeset-vs-impact, or doc-link concerns surfaced.

…bscription-coverage

# Conflicts:
#	scripts/check-package-size.mjs
bokelley and others added 3 commits October 30, 2026 19:25
…/reporting-subscription-ledger

# Conflicts:
#	scripts/check-package-size.mjs
…-beta.2

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 — clean pass, no blocking findings.

Checked: AdCP 3.3.0-beta.1 → beta.2 adoption (version/generated/docs bumps), typed RequestValidationError / isPreDispatchError markers across errors/index.ts, SingleAgentClient.ts, TaskExecutor.ts, client-hooks.ts, schema-errors.ts and the storyboard runner, and the ESM/CJS schema-loader binding for envelope-status checks (envelope-status-compat.ts + schema-loader.ts bindEnvelopeSchemaLoader). Verified RequestValidationError inherits code 'VALIDATION_ERROR' with dispatched:false; FeatureUnsupported/ProtocolFeatureUnsupported inherit the marker. The largest delta file creative-delivery.ts reconcileLegacyAssignmentRefs is the documented, tested per-axis shared-ref design, not a defect. Three changesets present and correctly scoped (minor for beta.2 adoption + typed error, patch for the legacy-ref regression fix) — satisfies changeset-vs-wire-impact.

Decision path: no critical/high/medium findings (table row 1/4/5/8 not triggered). gated_paths is false (row 2 n/a despite REVIEW_REQUIRED). high_risk false (rows 3/5 n/a). Prior decision was approve (row 6 n/a). No no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.

@aao-secretariat

Copy link
Copy Markdown

Ladon cannot review this PR until merge conflicts are resolved.

bokelley and others added 5 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 — subsequent pass, clean diff, no findings.

This is a follow-up pass on prior-approved PR #3518. The delta is small (4 files) and CI/test-only: an empty-frontmatter CI changeset, comment-only annotations on the packed-tarball budget in check-package-size.mjs (the 26_520_000 cap unchanged), a pre-dispatch-errors test swapping a retired 3.3.0-beta.1 literal for ADCP_VERSION (seller caps remain 3.3-beta.0, feature-refusal assertion still holds), and a single-line string collapse in reporting-subscription-coverage. No src/lib or bin code touched; changeset-vs-wire and witness-not-translator rules have no new surface.

Decision table walk-through:

  • Rows 1 (critical/high): no findings — skip.
  • Row 2 (gated_paths + not APPROVED): gated_paths is false — skip.
  • Row 3 (deleted high-risk): high_risk false — skip.
  • Row 4/5 (medium data-loss/schema/infra or modified high-risk+medium): no medium findings — skip.
  • Row 6 (sticky escalate): prior decision was approve — skip.
  • Row 7 (no-auto-approve team): no team match — skip.
  • Row 8 (≥3 medium): zero medium findings — skip.
  • Row 9: approve.

Note: review_decision is REVIEW_REQUIRED, but gated_paths is false, so row 2 does not apply, and REVIEW_REQUIRED alone is not an approve blocker (the protected-branch rule is deprecated). Prior approve stands.

bokelley and others added 7 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>
…eporting-subscription-ledger-service

# Conflicts:
#	src/lib/reporting/subscriptions/engine.ts
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 delta pass, no blocking or medium findings.

What I checked:

  • No critical/high/medium findings in this run.
  • gated_paths: false → row 2 does not apply even though review_decision is REVIEW_REQUIRED.
  • high_risk: false, no deletions.
  • No no-auto-approve team match.
  • Prior decision was approve; this is a consistent follow-up.

Change summary (all fail-closed and tested):

  • calendar.ts refuses sub-daily billing_cycle alignment (adcp#8622) before it would silently align to local midnight — correct witness behavior.
  • engine.ts replaces withheld.at(-1) with latestPeriodEnd() for unordered frozen periods; refreshes open action_required pending_action via JCS canonicalize with principal.changed firing.
  • server/principal/reporting-subscriptions.ts narrows the flag reset from !== true to === false, so an omitted replace_account_configs no longer drops a committed retry or re-runs the destructive hook.

Changesets present. No decision-table trigger fired (rows 1–8 all clear) → falls through to row 9.

@bokelley
bokelley merged commit 7b8eaf0 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-service 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