Skip to content

feat(reporting): one storage decision with versioned migrations - #3555

Open
bokelley wants to merge 168 commits into
mainfrom
bokelley/reporting-subscription-dx-storage
Open

bokelley wants to merge 168 commits into
mainfrom
bokelley/reporting-subscription-dx-storage

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

T8 for #3504 (approved DX plan §2.4 and §6.1 T8, owner decision (a)): the storage facade. It stacks on #3554 (T7) → #3553 → #3552 → #3551 → #3546 → … and the rest of the #3504 stack. Review only feat(reporting): one storage decision with versioned migrations. It targets main because CI runs only for PRs into main.

What a seller writes now

import { createReliableReportingService } from '@adcp/sdk/reporting/service';
import { postgresStorage } from '@adcp/reporting-storage'; // memoryStorage() from '@adcp/sdk/storage' in development

const storage = postgresStorage({ pool, acknowledgeIsolatedDatabase: true });
await storage.checkSchema(); // REPORTING_SCHEMA_BEHIND: run "npx adcp-reporting-storage migrate" (pending: …)
const reporting = createReliableReportingService({
  storage, // was: store
  /* producer config */
  subscriptions: { principal: { agentUrl, notifications, resolvePrincipal }, configure }, // principals is now optional
});
const server = createAdcpServerFromPlatform(reporting.install(platform), { protocol: reporting.principals.protocol });
npx adcp-reporting-storage migrate [--database-url URL] [--schema NAME] [--status | --dry-run]

API

  • @adcp/sdk/storage (new subpath; exports and typesVersions; +8 files in the budget):
    • memoryStorage({ now? }) returns a ReportingStorage: durability, ledger, principals, subscriptions, consumer, checkSchema(), close().
    • Also exports developmentEnvironment(), requireDurable(), isReportingStorage() and DEVELOPMENT_ENVIRONMENTS.
    • The in-memory ledger the SDK tests used is now the shipped memoryStorage().ledger. test/helpers/memory-reporting-ledger-store.js re-exports it, so the existing ledger suites now exercise it.
  • @adcp/reporting-storage:
    • postgresStorage({ pool, acknowledgeIsolatedDatabase?, ledger?, namespace? }) returns a PostgresReportingStorage (adds pool and migrations).
    • Versioned migrations: migrations(), migrate(pool, { dryRun }), migrationStatus, pendingMigrations, appliedMigrations, describeMigrations, MIGRATE_COMMAND.
    • The new bin/adcp-reporting-storage.js provides the adcp-reporting-storage command. The package had no bin before; the command is named after the package.
  • Migrations 0001–0008: adcp_state, reporting_ledger, reporting_ledger_finality_fence (stopped-worker), reporting_row_storage, reporting_ledger_changes, reporting_managed_delivery, reporting_object_writes, reporting_consumer.
    • Each runs in its own transaction under pg_advisory_xact_lock(hashtext('adcp.reporting.schema'), hashtext(current_schema())).
    • Each is recorded in adcp_schema_migrations, the same table and lock Python's python -m adcp migrate uses.
    • Their statements are the existing idempotent constants, so a schema set up by hand migrates cleanly.
    • REPORTING_LEDGER_MIGRATION, REPORTING_LEDGER_FINALITY_WRITER_FENCE_MIGRATION, REPORTING_ROW_STORAGE_MIGRATION, REPORTING_LEDGER_CHANGES_MIGRATION, REPORTING_MANAGED_DELIVERY_MIGRATION and REPORTING_OBJECT_WRITE*_MIGRATION are @deprecated re-exports for one beta.
  • Service:
    • createReliableReportingService({ storage }). store is optional, and passing both, or neither, is refused.
    • Process-local storage outside NODE_ENV=test|development throws REPORTING_STORAGE_NOT_DURABLE.
    • The first runCycle runs checkSchema() once. A failed check is retried.
    • New service.storage and service.principals.
  • Managed block:
    • stateStore defaults to storage.subscriptions.
    • principals is optional: subscriptions.principal (the lifecycle options minus store and reportingSubscriptions) builds the runtime over storage.principals.
  • Production service and consumer: createPostgresReliableReportingProductionService({ storage }) replaces db, defaults the managed block to the storage, and awaits checkSchema(). createReliableReportingConsumerV1({ storage }) uses storage.consumer.
  • Durability is intrinsic:
    • AdcpStateStore.durability is declared by the store: InMemoryStateStore is process-local and PostgresStateStore is durable. createSessionedStore propagates it.
    • The durability option of createPrincipalStateStore and createReportingSubscriptionEngineStateStoreV1 is @deprecated. A value that agrees with the store is accepted. A contradicting value is refused (REPORTING_STORAGE_NOT_DURABLE on the engine store). A custom store that declares nothing keeps the caller's value.

Deviations from the plan

  • Paths (owner decision (a)).
    • postgresStorage lives in @adcp/reporting-storage, not @adcp/sdk/storage/postgres.
    • The command is npx adcp-reporting-storage migrate, not npx adcp migrate, because the SDK may never import the storage package.
    • The DX guide in the spec checkout is read-only. The SDK docs get the new paths in T10.
  • Migrations are versioned entries, not .sql files. Each entry's SQL is the existing constant, so every statement lives in one place, the same reason Python uses -- include:.
  • No startup hook on a request-only host. The service checks the schema before its first cycle. A host that serves requests before running a cycle should await storage.checkSchema() at boot. The production service awaits it at construction.
  • Managed delivery for memory storage. Managed delivery's tables live in the Postgres storage's schema and migrations. The production service builds the managed store over storage.pool, because it needs the service's notification port and retention. memoryStorage() has no managed delivery, because TS has no in-memory managed-delivery store.
  • Production notification tables. The production service's notification outbox and activity tables have configurable names, so they stay on setup.migrations / applyMigrations.
  • Not ported from Python. Python's staging/seals source stores have no TS counterpart.
  • New for buyers. The buyer consumer state (consumer) is new relative to Python's ReportingStorage.
  • subscriptions.principal is TS-specific. TS principal runtimes also need notifications and agentUrl, which Python's PrincipalService(store) does not, so the block carries the lifecycle options.

Tests

  • test/lib/reporting-storage-facade.test.js (11) covers:
    • the subpath;
    • memoryStorage shape;
    • the production refusal;
    • lease-fenced memory consumer;
    • intrinsic and deprecated durability;
    • storage vs store;
    • the schema check run once and retried;
    • subscriptions.principal rules;
    • the consumer taking storage.
  • reporting-subscription-managed.test.js gains a storage world. The service builds the runtime, and principal records, engine state and obligations land in the storage.
  • packages/reporting-storage/test/reporting-storage-migrations.test.js covers order, the behind-schema refusal (it never writes), and the CLI's arguments, --status and --dry-run.
  • New PostgreSQL suite. packages/reporting-storage/test/reporting-storage-facade-pg.test.js covers:
    • the CLI migrating a fresh schema;
    • --status and --dry-run;
    • a re-run reporting "up to date";
    • every store round-tripping;
    • concurrent runners applying each version once;
    • a schema set up from the deprecated constants migrating with rows kept.

Package weight

  • SDK: 26,587,751 packed bytes, under the inherited 26,610,000 cap, which is unchanged.
  • Storage package: 445,755 packed bytes. Its file budget is 125 → 140 (+13: three modules and the bin), with a comment.

Checks

npm run typecheck, the storage typecheck, lint:boundaries, check:package-size, Prettier, 964 tests across reporting, principal, persistent-notification and state-store, and the packages/reporting-storage suite (PostgreSQL suites run in CI).

Refs #3504

🤖 Generated with Claude Code

bokelley and others added 30 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>
Deliver principal-level reporting subscription obligations (#3504) to the
owner's destination through managed delivery.

- Destination resolution: the Core service calls a new
  reportingSubscriptions.bindManagedGeneration hook (through the planner's
  bindGeneration) before a managed generation's first obligation. The
  production service supplies createReportingSubscriptionManagedBindingV1:
  version destination_id -> the owner's current destination generation
  (delivery-eligible, pattern and transport matching the frozen
  destination_method) -> a per-account authorization of that
  destination_ref (ensureDestinationAuthorization) -> an immutable binding
  that freezes path_template.
- Bindings are keyed by their Core configuration: the managed migration drops
  the published (account, delivery_config_id, delivery_config_version) key,
  found by its columns.
- path_template is expanded per obligation and passed to adapters as
  delivery_path; object-store names place it inside the collision-free
  generation prefix. Subscription revision rows carry account_id inside the
  committed digest.
- The managed worker re-checks the owner's account authority before
  delivering and before settling; a lost grant holds the materialization
  without spending an attempt, so the obligation stays owed.
- Status subscribers and notification activity reach only the owner of a
  subscription obligation. Production reads are scoped to the caller's
  principal and carry subscription_coverage.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	scripts/check-package-size.mjs
…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 15 commits October 30, 2026 22:33
…elley/reporting-subscription-dx-errors

# Conflicts:
#	src/lib/reporting/subscriptions/engine.ts
When the offerings a condition covers have different period boundaries,
the engine now writes one excluded[] entry per offering with offering_id
and that offering's cutover instants (capacity, excluded_by_buyer,
action_required, overridden, and offering_unsupported after a resolution
change). Before, it wrote one account-wide entry cut at the latest end. A
per-offering entry closes at its own offering's cutover. A release whose
excluded[] has no offering_id gets them folded back into one entry.

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

createReliableReportingService({ subscriptions: SubscriptionOptions }) now
builds the coverage engine, installs the reporting_subscriptions section on
the principal runtime, fills in capabilities.principal on install, reconciles
a principal after each sync_principal write that submits subscriptions, and
sweeps every subscribed principal at the start of runCycle.

The roster and buys are derived from the platform's accounts.list and
sales.getMediaBuys under the principal's identity, with overrides named as
in the Python SDK's SubscriptionOptions. An unreadable roster is coded
SUBSCRIPTION_ROSTER_UNAVAILABLE, and reconcile and planning failures are
returned in runCycle's subscriptionPlanningFailures instead of an
AggregateError. The production service accepts the managed block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the managed subscriptions block the service serves the journal's
authorization.authorized_at on list_accounts and sync_accounts to callers
holding subscriptions, and the derived roster reads the same value. A host
value is adopted on first observation, an omitted one is stamped, a value
that moves an open chain earlier is refused and logged as
authorized_at_regressed, and the chain rule decides whether a re-grant
continues the chain. service.grants.changed pushes a grant change for an
exact instant, and resolveRevocationInstant becomes optional.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
client.reporting (ReportingClient) adds subscribe, unsubscribe,
replaceSubscriptions, listSubscriptions and preview: patch-shaped writes
over the replace-shaped sync_principal wire, with auto-versioning, a
destination merge, pre-dispatch checks (scope, destination, path_template,
SUBSCRIPTION_ID_CLASH, SUBSCRIPTION_OPERATOR_SCOPE_REQUIRED,
SUBSCRIPTION_SELECTOR_UNMATCHED), CONFLICT retries with fresh keys and a
verified readback. A raw client.syncPrincipal that would deactivate a
subscription or revoke a destination by omission is refused before sending
unless allowDeactivation or allowRevocation is passed. Names follow the
Python SDK's ReportingClient.

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

The facade loads offering selection on demand and carries its own readback
diff, so client/core gains only the facade module itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…entity in the auth snapshot

- The derived roster and buy reads take the engine's signal and check it
  before each read and page, so a cancelled cycle stops promptly.
- The authInfo snapshot kept for service-context reads is an allowlist
  (clientId, scopes, resource, expiresAt), so no adopter field carrying a
  credential reaches the durable engine store.

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

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

- A projection whose journal write fails still serves the platform's values,
  but now warns and reports the failure to planningErrorHandler instead of
  failing silently.
- New engine test: a pushed re-grant inside retention continues the chain,
  one after retention starts a new chain, and a listing observed after
  retention does too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A seller that rewrites a submitted field, or drops the subscription from
its readback, fails with SUBSCRIPTION_READBACK_MISMATCH.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
memoryStorage() (new @adcp/sdk/storage subpath) and postgresStorage({ pool })
(@adcp/reporting-storage) return one ReportingStorage covering the reporting
ledger, principal records, reporting-subscription engine state and the buyer
consumer's checkpoints and leases. Process-local storage is refused outside
NODE_ENV=test|development with REPORTING_STORAGE_NOT_DURABLE.

The storage package ships versioned migrations recorded in
adcp_schema_migrations and the `npx adcp-reporting-storage migrate
[--status | --dry-run]` command; checkSchema() refuses a schema that is behind
with REPORTING_SCHEMA_BEHIND, and the service runs it before its first cycle.
The migration constants stay as deprecated re-exports.

The service, the production service and the buyer consumer take storage.
subscriptions.principals is optional with storage: subscriptions.principal
builds the runtime over storage.principals (service.principals). Durability is
declared by the state store; the durability options are deprecated, accepted
when they agree and refused when they contradict the store. The in-memory
ledger the tests used now ships behind memoryStorage().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/lib/reporting/consumer-runtime.ts Resolved
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 31, 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 — one medium finding, no blocking issues.

The reviewer isolated the in-scope T8 commit (f8368e7, "one storage decision with versioned migrations") and verified the load-bearing paths: migrate() concurrency (per-migration tx under pg_advisory_xact_lock with a post-lock double-check, resumable on partial failure), the CLI --schema injection guard (identifier validated before -c search_path), checkSchema being non-mutating, resolveStoreDurability agree/contradict/absent logic, the withStorage/withStorageSubscriptions one-of refusals, and the memory-ledger.ts move preserving structuredClone and immutability semantics. Changeset is present and correctly minor for both experimental packages; package-size budgets match.

Medium findings

  • src/lib/reporting/consumer-runtime.ts:340 — Buyer consumer accepts process-local storage (memoryStorage()) in production without a durability refusal, asymmetric with the seller's requireDurable gate and the PR's own "refused in production" invariant.

Only 1 medium finding, no critical/high, no gated paths, high_risk false, no team gate. Falls through to row 9. The medium is worth a human eye but does not block.

Decision rules walkthrough: Row 1 (critical/high) — no. Row 2 (gated_paths true & not approved) — gated_paths is false, skip. Row 3/5 (high_risk) — false. Row 4 (medium data-loss/schema/infra) — the finding category is a durability/refusal gap, not tagged schema/infra; no escalation. Row 6 — no prior decision. Row 7 — no team gate. Row 8 — fewer than 3 mediums. Row 9 — approve.

Medium findings

  • src/lib/reporting/consumer-runtime.ts:340 — Buyer consumer accepts process-local storage in production without refusal

…de suite

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

Delta since the prior approve is a single test-only change in packages/reporting-storage/test/reporting-storage-facade-pg.test.js: an ownerToken literal renamed 'owner-a' → 'owner-token-a'. No behavior change, test-file only (no changeset required), and the value is opaque to lease logic.

No current findings. The prior Medium (src/lib/reporting/consumer-runtime.ts:340) is non-blocking and its file is absent from this delta, so it does not carry forward. No Critical/High findings exist.

Decision table: no critical/high (row 1 n/a), gated_paths false (row 2 n/a), no deletions (row 3 n/a), no medium data-loss/schema/infra (row 4 n/a), high_risk false (row 5 n/a), prior decision was approve not escalate (row 6 n/a), no team gate (row 7 n/a), fewer than 3 mediums (row 8 n/a) → row 9 approve.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 31, 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 — test-only delta since prior approve, no findings.

The only change since the prior approve (head f8368e7) is a test-only edit in packages/reporting-storage/test/reporting-storage-facade-pg.test.js: ownerToken renamed owner-a → owner-token-a in a single workLeases.claim call. The value is never asserted against and flows straight into claim with the returned lease reused, so there is no behavior or correctness impact.

The prior Medium finding (src/lib/reporting/consumer-runtime.ts:340) is out of scope this pass — that file is absent from the current delta, so it does not carry forward.

No critical/high/medium findings in this run. No gated paths, no high-risk flag, no author team gate. Falls through to row 9.

…roduction

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 31, 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 findings.

This delta (2 files / 55 lines) adds an optional durability to the consumer storage option and refuses process-local outside NODE_ENV=test|development with REPORTING_STORAGE_NOT_DURABLE — a fail-closed guard symmetric with the already-approved seller refusal, and tested under NODE_ENV=production (reporting-storage-facade.test.js:48-53). The surface is @experimental with no wire change, so DR-0033 does not apply.

One non-blocking Low observation (not surfaced inline): the service uses the shared requireDurable() helper (service/index.ts:499) while the consumer inlines its own test/development guard — currently identical behavior but a potential drift risk if DEVELOPMENT_ENVIRONMENTS ever changes. Consider consolidating on the shared helper.

Prior pass (approve) had no blockers; none re-surface. No critical/high/medium findings. No author-team gate, gated_paths is false, high_risk is false. Decision table falls through to row 9.

…le, as the seller does

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 fail-closed hardening of the reliable-reporting consumer.

The delta (2 files, 52 lines) makes storage.durability required and flips the production guard from === 'process-local' to !== 'durable', so any storage that does not explicitly declare itself durable is now refused outside test/development — mirroring the seller side. The input.storage !== undefined short-circuit preserves the prior no-throw path when storage is absent, and a new test covers the undeclared-durability branch using the same production-env arrangement as the adjacent postgresStorage assertion. Changesets are present.

No critical/high/medium findings in this run. Prior approve stands and this is a net-neutral/positive iteration. No author team gate, gated_paths is false, and high_risk is false — 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