Skip to content

fix(reporting-storage): upgrade 0.1.0b4/0.2.0b0 ledgers in place and pin the adcp storage contract (#1763) - #1787

Merged
bokelley merged 5 commits into
nextfrom
claude/issue-1763-storage-migrations
Nov 2, 2026
Merged

bokelley merged 5 commits into
nextfrom
claude/issue-1763-storage-migrations

Conversation

@bokelley

@bokelley bokelley commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Why

adcp==10.0.0b4 with the only published adcp-reporting-storage (0.2.0b0) installs cleanly (adcp>=9.0.0,<11) and fails every PostgreSQL Reliable Reporting turn with column "offering_id" does not exist (#1763). ESA has a production ledger created by 0.1.0b4/0.2.0b0 that cannot be recreated.

Drift audit

Column lists adcp reads the ledger with (adcp.reporting.ledger._record_rows), by release:

adcp _OBLIGATION_COLUMNS _CONFIGURATION_COLUMNS _STATUS_COLUMNS
9.4.0, 10.0.0b2, 10.0.0b3 23 columns (lived in storage) 22
10.0.0b4, b5, next +offering_id 19 incl. offering_id +offering_id

That is the only column-list drift. Storage on next already ships matching DDL and a migration framework (0001–0005, unreleased); 0.1.0b4 and 0.2.0b0 are source-identical. The SDK 9 line has no drift (storage on main == 0.1.0b4; _record_rows unchanged since).

What this PR does

  • create_schema() applies and records the versioned migrations inside its bootstrap transaction, under the reporting schema advisory lock, so the store= startup path (what ESA runs) and python -m adcp migrate converge. A 0.1.0b4/0.2.0b0 ledger is upgraded in place: offering_id added with '' for every existing account-level row, generation key and period index widened, managed-delivery FKs re-pointed, versions recorded in adcp_schema_migrations. Re-runnable; concurrent boots serialize; no evidence row rewritten or dropped.
  • Storage contract number on both sides: adcp_reporting_storage.STORAGE_CONTRACT = 2, checked when a PgReportingLedgerStore is built. A mismatched adcp is refused at startup with REPORTING_SCHEMA_BEHIND, naming both installed versions, both contracts and the release to install. adcp 10.0.0b4/b5 (no declaration yet) are classified by their column lists; the adcp-side declaration and check come in a follow-up PR.
  • Floor adcp>=10.0.0b4,<11, the first adcp release of contract 2, so the incompatible pair no longer resolves.

Test evidence (real PostgreSQL 16, actual published wheels)

New tests/conformance/reporting/test_reporting_storage_published_upgrade_pg.py seeds ledgers with adcp==9.3.1 + adcp-reporting-storage==0.1.0b4 and adcp==10.0.0b2 + 0.2.0b0 (generations, obligations, revisions, rows), then with this checkout:

  • reproduces the b4 failure on the published 0.2.0b0 (UndefinedColumn: offering_id);
  • create_schema() and migrate() each upgrade in place: every pre-upgrade row survives byte-for-byte plus offering_id='', all records read back, versions 0001–0005 recorded, re-run is a no-op;
  • the upgraded catalog (columns, constraints, indexes, triggers) is identical to a freshly migrated schema;
  • a ReliableReportingService(store=...) restart rebinds both stored generations with no initialization errors and closes the next period with a new snapshot revision;
  • four concurrent create_schema()/migrate() boots serialize and record each version once;
  • adcp==10.0.0b4 + this storage (the pair a seller upgrades to) upgrades the 0.2.0b0 ledger and reads everything.
9 passed, 1 skipped in 127.37s

Plus make lint, make typecheck-all, and the existing storage facade / migration / PG ledger tests pass locally.

Operator note

The upgrade is a stopped-worker upgrade (0.2.0b0 workers' configuration writes name the four-column key): stop workers of the earlier release, start the new release (or run python -m adcp migrate), done.

🤖 Generated with Claude Code


Open workspace in Conductor

@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 fix for #1763 with no blocking or medium findings.

What I checked:

  • No findings from the reviewer (critical/high/medium all absent).
  • high_risk: false; gated_paths: false; no no-auto-approve team match; no prior decision.
  • The change moves create_schema() to apply versioned migrations via the new apply_pending inside its bootstrap transaction, and adds a storage-contract integer that fail-closes an incompatible adcp/storage pairing at construction rather than on first ledger read. The _apply extraction is behavior-equivalent to the old _migrate loop.
  • Concurrency, in-place upgrade, and evidence-preservation are covered by real-PostgreSQL conformance tests against the published wheels.
  • No adcp.* public-surface change, so SDK semver rules do not apply; the floor/contract break is documented 0.x storage-package policy (this is a separate adcp-reporting-storage package, not the adcp namespace).

With no findings and no escalation trigger, rows 1–8 do not fire; the PR falls through to row 9 → approve.

bokelley added a commit that referenced this pull request Nov 1, 2026
…bed contracts

The release configuration test expected the companion gate to run only for
adcp publishes; it now runs for both packages after the build, with a
PostgreSQL service. The smoke classification test stubs both declaration
modules instead of assuming the workspace pair agrees, which it does not
until the storage declaration (#1787) lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Nov 1, 2026
…ontract at startup

adcp 10.0.0b4 added offering_id to the ledger column lists that
adcp-reporting-storage reads against its DDL; the published storage release
(0.2.0b0) lacked the column, installed cleanly beside 10.0.0b4, and failed
every reporting turn with UndefinedColumn, logged only as a configuration
failure (#1763).

adcp now names the ledger layout it reads with one integer,
adcp.reporting.ledger.storage_contract.REPORTING_STORAGE_CONTRACT (2), and
checks the installed ledger store against it at service startup
(ReliableReportingService._initialize, before any schema work) and when
postgres_storage() is built. A store declares its contract with a
storage_contract attribute (adcp-reporting-storage 0.2.0b1+, #1787); a store
from that distribution without one predates the declaration and is
contract 1; other stores are not subject to the check. A mismatch raises
ReliableReportingConfigurationError with REPORTING_SCHEMA_BEHIND, naming
both installed versions, both contracts, the release to install and the
migration to run, so the pair fails once and loudly instead of every turn.

tests/test_reporting_storage_contract.py pins a fingerprint of the shared
column lists to the contract number, so changing _record_rows without
bumping the contract (and the storage DDL with it) fails the build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve.

Clean pass with no blocking or Medium findings. The reviewer verified the core migration-ownership logic: apply_pending(within=...) walks migrations in version order with correct break-not-skip semantics (0004 subscriptions/ stops the walk, leaving 0004/0005 for python -m adcp migrate), and the within posix paths exactly match the -- include: directives of 0001–0003. The conformance test correctly moves check_schema() after migrate() (fail-closed on behind-schema) and asserts the upgraded-vs-fresh catalog invariant. Public-surface changes are additive and snapshot-tracked: STORAGE_CONTRACT/require_adcp_contract exports, contract.py resolving to the public adcp.reporting export, and a consistent adcp>=10.0.0b4,<11 storage floor with matching test.

Checked against repo gates: no public-API break requiring a semver signal, no hand-edits to generated code, no import-layering or forward-compat regressions, no disabled tests or blanket type:ignores.

Decision table: no critical/high findings (row 1 n/a); gated_paths false and review_decision APPROVED (row 2 n/a); high_risk false (rows 3/5 n/a); no medium findings (rows 4/8 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 added a commit that referenced this pull request Nov 2, 2026
…ks with (#1763) (#1788)

* ci(release): gate publishes on a companion the built distribution works with

scripts/check_companion_releases.py refused an adcp release only when no
published adcp-reporting-storage *range* accepted it. adcp 10.0.0b4 changed
the ledger column lists while the published storage (adcp>=9.0.0,<11) still
accepted it, so the pair installed cleanly and failed every reporting turn
(#1763). Range compatibility is not contract compatibility.

The gate now proves the pair works. With --dist, it installs the wheel just
built together with the published companion into a private environment and
runs scripts/companion_smoke.py there, which compares the storage contract
each side declares (adcp.reporting.ledger.storage_contract versus
adcp_reporting_storage.contract; releases predating the declaration are
classified by their column lists) and, with ADCP_PG_TEST_URL, creates the
ledger schema and round-trips a configuration, obligation and revision
through the installed stores on PostgreSQL, the calls that failed on the
incompatible pair. Storage publishes are gated too, in the other direction:
the built storage wheel is smoked with the oldest and the newest published
adcp its own requirement accepts, so a floor that admits an adcp of another
contract is refused.

release-publish.yml builds first, runs the gate for both packages against a
PostgreSQL service, then uploads. Verified locally against PyPI and
PostgreSQL 16: the adcp wheel built from next is refused against the
published 0.2.0b0 (contract 2 versus 1), the storage wheel from #1787
(floor 10.0.0b4) passes with 10.0.0b4, and the published 0.1.0b4 passes with
adcp 9.0.0 and 9.3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(release): assert the gate smokes both packages and classify stubbed contracts

The release configuration test expected the companion gate to run only for
adcp publishes; it now runs for both packages after the build, with a
PostgreSQL service. The smoke classification test stubs both declaration
modules instead of assuming the workspace pair agrees, which it does not
until the storage declaration (#1787) lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(release): classify the smoke's undeclared adcp from patched column lists

The test asserted this checkout's column lists carry offering_id, which is
true on next and false on main; both classifications are now driven by
patched lists so the test is branch-agnostic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Nov 2, 2026
…ontract at startup (#1789)

adcp 10.0.0b4 added offering_id to the ledger column lists that
adcp-reporting-storage reads against its DDL; the published storage release
(0.2.0b0) lacked the column, installed cleanly beside 10.0.0b4, and failed
every reporting turn with UndefinedColumn, logged only as a configuration
failure (#1763).

adcp now names the ledger layout it reads with one integer,
adcp.reporting.ledger.storage_contract.REPORTING_STORAGE_CONTRACT (2), and
checks the installed ledger store against it at service startup
(ReliableReportingService._initialize, before any schema work) and when
postgres_storage() is built. A store declares its contract with a
storage_contract attribute (adcp-reporting-storage 0.2.0b1+, #1787); a store
from that distribution without one predates the declaration and is
contract 1; other stores are not subject to the check. A mismatch raises
ReliableReportingConfigurationError with REPORTING_SCHEMA_BEHIND, naming
both installed versions, both contracts, the release to install and the
migration to run, so the pair fails once and loudly instead of every turn.

tests/test_reporting_storage_contract.py pins a fingerprint of the shared
column lists to the contract number, so changing _record_rows without
bumping the contract (and the storage DDL with it) fails the build.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@aao-secretariat

Copy link
Copy Markdown

Ladon cannot review this PR until merge conflicts are resolved.

bokelley and others added 3 commits November 2, 2026 09:57
…pin the adcp storage contract

adcp 10.0.0b4 added offering_id to the ledger column lists in
adcp.reporting.ledger._record_rows, which adcp-reporting-storage reads
against its own DDL. The only published storage release, 0.2.0b0, does not
create the column, declares adcp>=9.0.0,<11, and so installs cleanly beside
10.0.0b4 and fails every reporting turn with UndefinedColumn (#1763).

Storage side of the fix:

- create_schema() now applies the versioned migrations (0001-0005) that are
  not yet recorded in adcp_schema_migrations inside its own transaction,
  under the reporting schema advisory lock, before its optional DDL. A
  ledger created by 0.1.0b4 or 0.2.0b0 is upgraded in place through either
  entry point: offering_id is added with '' for every existing account-level
  generation, obligation and status, the generation key and period index
  are widened, the managed-delivery foreign keys are re-pointed, and the
  applied schema versions are recorded. Re-running is a no-op; concurrent
  boots serialize. No evidence row is rewritten or dropped.
- A storage contract number is declared on both sides
  (adcp_reporting_storage.STORAGE_CONTRACT = 2) and checked when a
  PostgreSQL ledger store is built: an adcp of another contract is refused
  at startup with REPORTING_SCHEMA_BEHIND naming both versions and the
  release to install, instead of failing on the first read. adcp releases
  without a declaration (10.0.0b4, b5) are classified by their column lists.
- The dependency floor moves to adcp>=10.0.0b4,<11, the first release of
  contract 2, so the incompatible pair no longer resolves.

tests/conformance/reporting/test_reporting_storage_published_upgrade_pg.py
seeds ledgers with the actual published wheel pairs (adcp 9.3.1 +
storage 0.1.0b4, adcp 10.0.0b2 + storage 0.2.0b0), reproduces the b4
failure on 0.2.0b0, and proves both upgrade paths keep every evidence row,
read everything back, rebind the stored generations on restart, close the
next period, record the version, re-run as a no-op, serialize concurrent
boots, and produce a catalog identical to a fresh migration.

Closes #1763 (storage side); the adcp-side startup check and the release
gate follow in their own PRs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tions

create_schema applies and records the migrations made of the DDL this
store owns (0001-0003) and stops there: 0004 (subscription state) and 0005
(inline storage) create reporting_* tables of other stores, and the A/B
catalog-identity suites assert that an older binary's installed object set
is exactly its own manifest. Those two stay pending for python -m adcp
migrate, which postgres_storage requires anyway.

Also: the contract module imports ReliableReportingConfigurationError
through the public adcp.reporting name, the public API snapshot records
STORAGE_CONTRACT and require_adcp_contract, and the release configuration
test pins next's storage floor to the first adcp of its storage contract.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ontract at startup (#1789)

adcp 10.0.0b4 added offering_id to the ledger column lists that
adcp-reporting-storage reads against its DDL; the published storage release
(0.2.0b0) lacked the column, installed cleanly beside 10.0.0b4, and failed
every reporting turn with UndefinedColumn, logged only as a configuration
failure (#1763).

adcp now names the ledger layout it reads with one integer,
adcp.reporting.ledger.storage_contract.REPORTING_STORAGE_CONTRACT (2), and
checks the installed ledger store against it at service startup
(ReliableReportingService._initialize, before any schema work) and when
postgres_storage() is built. A store declares its contract with a
storage_contract attribute (adcp-reporting-storage 0.2.0b1+, #1787); a store
from that distribution without one predates the declaration and is
contract 1; other stores are not subject to the check. A mismatch raises
ReliableReportingConfigurationError with REPORTING_SCHEMA_BEHIND, naming
both installed versions, both contracts, the release to install and the
migration to run, so the pair fails once and loudly instead of every turn.

tests/test_reporting_storage_contract.py pins a fingerprint of the shared
column lists to the contract number, so changing _record_rows without
bumping the contract (and the storage DDL with it) fails the build.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@bokelley
bokelley force-pushed the claude/issue-1763-storage-migrations branch from 4e11f3d to c596855 Compare November 2, 2026 10:00

@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 findings.

This PR pins a storage contract (version 2) between adcp and adcp-reporting-storage to close #1763 and upgrades the 0.1.0b4/0.2.0b0 ledgers in place. The reviewer confirmed:

  • New public symbols (STORAGE_CONTRACT, require_adcp_contract, the storage_contract module) are additive with a matching public_api_snapshot.json entry — no semver break.
  • The fail-closed contract guard is symmetric (adcp→store require_storage_contract at service _initialize/postgres_storage; store→adcp require_adcp_contract at PgReportingLedgerStore.__init__), skips in-memory/custom stores, and refuses both older and newer contracts (_Newer/_Older tested).
  • Migration apply path (apply_pending/_apply) holds the reporting schema lock, is re-runnable, bounded to owned migrations 0001-0003, and serializes concurrent boots (proven, count==5); test subprocesses carry explicit timeouts.
  • Largest-file delta (test_reporting_storage_published_upgrade_pg.py) and _published_ledger_seed.py both reviewed and sound; the #1763 repro is asserted.
  • Test plan honest: 9 passed / 1 skipped on real PostgreSQL with actual published wheels.

Gate checks: no critical/high/medium findings (rows 1, 4–6, 8 do not fire). gated_paths false and review_decision APPROVED (row 2 n/a). high_risk false (rows 3, 5 n/a). No no-auto-approve team match (row 7 n/a). Prior decision was approve. Falls through to row 9 → approve.

… notifications-enabled ledger

next added migration 0006 (status checkpoint offerings) after this branch
was cut; the published-wheel upgrade test now derives the versions it
expects from the shipped set instead of listing them, and seeds a
notifications-enabled 0.2.0b0 ledger (outbox and status stores) to prove the
in-place upgrade and migrate record every migration, 0006 included, with
the evidence unchanged.

Co-Authored-By: Claude Fable 5.1 <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.

The delta since the prior clean pass is docs wording and test infrastructure only. Docstrings/comments in pg.py and the migration/README docs update "0004–0005" to "0004 onward" to name the new migration 0006 (status checkpoint offerings), which lives outside this diff. Tests replace hardcoded ["0004","0005"] version lists with _shipped()/_rest() derived from the actual migrations() manifest, and add a notifications-enabled upgrade test covering the reporting_status_checkpoints/events tables. Helpers are internally consistent and order-preserving.

No product-code logic, public API, wire shape, or credential handling changed in the delta. No critical/high/medium findings surfaced, and no prior blockers to re-surface (prior decision was also approve).

Checks against decision table:

  • No critical/high/medium findings (rows 1, 4, 5, 6, 8 don't fire).
  • gated_paths false; review_decision APPROVED (row 2 n/a).
  • high_risk false (rows 3, 5 n/a).
  • No no-auto-approve team match (row 7 n/a).

Falls through to row 9 → approve.

bokelley added a commit that referenced this pull request Nov 2, 2026
…ks with, on main (#1790)

* ci(release): gate publishes on a companion the built distribution works with

scripts/check_companion_releases.py refused an adcp release only when no
published adcp-reporting-storage *range* accepted it. adcp 10.0.0b4 changed
the ledger column lists while the published storage (adcp>=9.0.0,<11) still
accepted it, so the pair installed cleanly and failed every reporting turn
(#1763). Range compatibility is not contract compatibility.

The gate now proves the pair works. With --dist, it installs the wheel just
built together with the published companion into a private environment and
runs scripts/companion_smoke.py there, which compares the storage contract
each side declares (adcp.reporting.ledger.storage_contract versus
adcp_reporting_storage.contract; releases predating the declaration are
classified by their column lists) and, with ADCP_PG_TEST_URL, creates the
ledger schema and round-trips a configuration, obligation and revision
through the installed stores on PostgreSQL, the calls that failed on the
incompatible pair. Storage publishes are gated too, in the other direction:
the built storage wheel is smoked with the oldest and the newest published
adcp its own requirement accepts, so a floor that admits an adcp of another
contract is refused.

release-publish.yml builds first, runs the gate for both packages against a
PostgreSQL service, then uploads. Verified locally against PyPI and
PostgreSQL 16: the adcp wheel built from next is refused against the
published 0.2.0b0 (contract 2 versus 1), the storage wheel from #1787
(floor 10.0.0b4) passes with 10.0.0b4, and the published 0.1.0b4 passes with
adcp 9.0.0 and 9.3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7b40f34)

* test(release): assert the gate smokes both packages and classify stubbed contracts

The release configuration test expected the companion gate to run only for
adcp publishes; it now runs for both packages after the build, with a
PostgreSQL service. The smoke classification test stubs both declaration
modules instead of assuming the workspace pair agrees, which it does not
until the storage declaration (#1787) lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 876faa5)

* test(release): classify the smoke's undeclared adcp from patched column lists

The test asserted this checkout's column lists carry offering_id, which is
true on next and false on main; both classifications are now driven by
patched lists so the test is branch-agnostic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <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 delta since prior approve.

The only change since the prior approve (head 1522644) is a single comment reflow in packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/pg.py (L702-713): the "stay pending for python -m adcp migrate" comment rewrapped across lines with zero semantic change. No code, control flow, or wire surface touched.

Checked:

  • No critical/high/medium findings in this run.
  • high_risk: false; no deletions in sensitive paths.
  • gated_paths: false and review_decision: APPROVED — no path gate holding.
  • Author is not on any no-auto-approve team.
  • Public-surface / semver, forward-compat union, import-layering, generated-code, ctx_metadata credential, and CI-gate concerns all N/A for a comment reflow.

Decision table: rows 1–8 do not fire (no findings, no gate, no team match, <3 mediums). Falls through to row 9 → approve.

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