carlos / platform Public

Multi-currency: the tenth review's two reports

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 28dc2629bd235b39d215e8c6d27a079fdd00c002 parent 2548750
2 files changed, +200 −0
  • docs/superpowers/specs/CODEX-FINDINGS-multi-currency-review10.md +94 −0
  • docs/superpowers/specs/OPUS-REVIEW-multi-currency-review10.md +106 −0

CI passed — run details

diff --git a/docs/superpowers/specs/CODEX-FINDINGS-multi-currency-review10.md b/docs/superpowers/specs/CODEX-FINDINGS-multi-currency-review10.md
new file mode 100644
index 000000000..84cc2c90f
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-multi-currency-review10.md
@@ -0,0 +1,94 @@
+| Ninth-review item | Closed at `faf1bd25`? | Would its regression test pass without the fix? |
+|---|---|---|
+| **Opus P2-1 — startup clears legitimate history gaps** | **Yes.** The clearing sweep is gone: `internal/console/billingquantityaudit.go:42`. Summary updates preserve an existing gap at `internal/console/billingmembership.go:462`. | **No.** The new restart assertions at `billingmembership_test.go:719` and `billingquantityaudit_test.go:191`, and the retained-gap case at `billingcurrency_test.go:3117`, distinguish the unsafe sweep. |
+| **Opus P3-1 — open sessions with changed purchase metadata are offered again** | **Yes for the reported plan/band defect:** `internal/console/billingservice.go:247`. Other inconsistencies remain below. | **No for plan and band**, tested separately at `billingcurrency_test.go:418`. **Yes for the terms-version case:** the existing `priceTermsFromMetadata` comparison already rejected that change. |
+| **Opus’s claims** | **Partly corrected.** Provisioning-test wording, trigger ordering and inclusion-mutation wording are corrected. The rollback-healing claim and inaccurate reviewer agreement remain. | Prose has no behavioral regression test; details below. |
+| **Astra 1 — startup erases missing-history holds** | **Yes**, by the same removal and preservation rules as Opus P2-1. | **No**, for the restart regressions above. |
+| **Astra’s claims** | **Partly corrected.** The singular review reader still omits `reference`; historical agreement, blanket coverage and site-provenance claims remain overstated. | The identified weak assertions still pass without the property they purportedly establish; details below. |
+
+These counterfactuals are source judgments, not executed mutation tests. Platform references below are at **`faf1bd25`**, compared against **`12e70d95`**; site references are at **`9427c22`**. The requested `faf1bd25..faf1bd25` delta is empty. No files were modified, and no tests or network commands were run.
+
+On severity: both ninth reports correctly rated the gap-clearing defect **P2**. For their carried-forward disagreement, **Opus’s P3 reading fits the metadata-only refusal defect**: it requires an authenticated provider-side metadata alteration, with no demonstrated buyer-controlled path. **Astra’s P2 reading fits the second-read ownership defect**: processing A’s intent could act on B, although inconsistent authenticated responses were required. The current code closes that account substitution.
+
+**Findings, most severe first**
+
+1. **P3 — Initial subscription reconciliation can bypass the completed-session refusal review.**
+
+ **Where:** `internal/console/billingservice.go:793`, `:798`, `:882`; compare the stricter binder at `:970`. Subscription webhooks enter reconciliation directly at `internal/console/billinghandlers.go:270`.
+
+ **Scenario:** A valid GBP Personal checkout is bound. Someone with provider access changes only the Checkout Session’s `metadata.plan` to Business, leaving its ownership fields, actual GBP currency and subscription metadata unchanged. It completes, and `customer.subscription.created` arrives before `checkout.session.completed`.
+
+ Reconciliation validates the subscription against the intent, then checks the session’s ownership and actual currency. It never compares the session’s plan, band, terms or metadata price terms with the intent. It activates the subscription and deletes the intent through `internal/console/billingstore.go:746`.
+
+ The completion binder would have filed a refusal review for that session. After activation, however, its already-recorded-subscription return at `billingservice.go:954` bypasses that review too. Delivery order therefore decides whether this mismatch receives the required review. No currency conversion is demonstrated.
+
+ **Smallest acceptable fix:** Apply the same purchase-identity comparison during initial reconciliation, after ownership and before recording state. An owned completed mismatch must file the intent-keyed review and leave the intent standing.
+
+ **Regression needed:** Give session and subscription separate metadata, alter only the session, and deliver the subscription webhook first. The current fake shares `f.meta` between both objects (`internal/console/billingservice_test.go:174`, `:263`), which conceals this case.
+
+2. **P3 — Resume and listing validation still accept an account-metadata contradiction that the binder rejects.**
+
+ **Where:** `internal/console/billingservice.go:243`, `:262`, `:1215`; rejection occurs at `:938` or `:946`.
+
+ **Scenario:** A’s open session retains A’s intent ID, customer and `client_reference_id`, but its `metadata.account_sqid` becomes B’s. Its purchase fields remain correct.
+
+ Both `ownSession` and `intentSession` accept it. Interactive resume can offer its URL, and listing recovery can retain it. Once paid, binding rejects the account contradiction before reaching the refusal-review branch. This is another provider-side inconsistency, not a demonstrated buyer exploit or cross-account activation.
+
+ **Smallest acceptable fix:** Require `metadata.account_sqid == acct.Sqid` in the session validation used before offering or retaining a checkout. Keep the binder’s independent checks. Add separate resume and listing cases that alter only this field.
+
+**Results on the remaining requested paths**
+
+- **A — GBP end to end.** `?country=IN&currency=gbp` produces band D and Personal **£7.70**, independently of currency’s effect on allowances (`billingpage.go:265`). The handler checks effective currency and the signed quote (`billinghandlers.go:81`, `:102`), and checkout stores the book and GBP (`billingservice.go:526`). `CreateCheckout` sends GBP and disables Adaptive Pricing (`internal/billingstripe/client.go:622`). Outside finding 1, subscription currency and item currency are checked before account and assignment writes (`billingservice.go:752`; `billingstore.go:599`, `:725`). History, monthly allowance and version-2 calculation preserve GBP (`billinginvoicestore.go:530`; `internal/plans/month.go:173`; `billingcalculation.go:174`). Invoice creation, recovery, line verification and hashes use that calculation’s currency (`billinginvoice.go:859`, `:886`, `:901`, `:964`, `:1024`, `:1116`). Quantity operations select and verify the stored currency’s price (`billingquantity.go:1016`, `:1105`). I found no alternate-currency computation or conversion in this chain.
+
+- **B/M/Q/U — One form and quote integrity.** The signature uses a separately labelled key derived from the billing secret and covers session token, account, book, country, band, currency, choice flag and member count (`billingpage.go:446`). Disclosed form values cannot sign another tuple. Reuse of an identical quote is allowed; another session, account, book, band, count or rotated token invalidates it. All plans are displayed, quantity follows the authenticated member count and plan minimum, and postal affects tax subsequently shown at Stripe, not the console’s pretax quote. Membership changes conflict at `billingservice.go:417`, with another membership-locked check at `billingstore.go:348`.
+
+ Changed country/currency and forged `quoted_*` fields redirect to a quote. An old tab’s valid quote remains valid for the same inputs; cookies from another tab cannot alter its POST. Empty, malformed or repeated **currency fields** receive 400 on both doors after the preceding authentication checks (`billinghandlers.go:176`). An absent field uses the country default and must still match the signed quote; legacy mode uses EUR. The quote endpoint authenticates session, CSRF and membership, then constructs an encoded account-local redirect; it performs no billing mutation (`billinghandlers.go:137`).
+
+- **C/O — Cookies and held currencies.** Present-empty clears, absent falls back, invalid changes nothing (`billingcurrency.go:43`). Consequently, an invalid parameter leaves an older cookie available on a later URL that omits that parameter; explicit clearing removes it. Neither cookie participates in checkout’s POST decision. A previous purchase, including legacy EUR, overrides URL and cookie suggestions and offers one currency (`billingpage.go:301`). Checkout refuses another currency even after cancellation (`billingservice.go:513`).
+
+- **D/L/R/W/Y/AA — Ownership.** Both creation sites check refused responses against the intent before expiry. Listing, resume and the pass enforce ownership; the pass supplies its expected intent to the binder’s second read (`billingservice.go:644`, `:1173`, `:1207`; `billingpass.go:195`, `:200`, `:259`). The expected-intent binder checks its account before deriving one from response metadata. The webhook binder instead derives an account and checks its standing intent. Expiry proofs are independently rechecked transactionally (`billingstore.go:442`, `:465`). The binder’s already-recorded-subscription return makes no binding mutation, but contributes to finding 1.
+
+- **S/V/X/Z/BB/CC — Reviews, retirement and quantity holds.** Filing requires the exact intent still to exist; repeated observations fill an empty reference without adding an occurrence (`billingstore.go:969`, `:988`). Resolution selects and deletes exactly that intent, records its session ID and closes only its outstanding quantity events (`billingstore.go:922`, `:940`, `:943`, `:947`). The operator listing/API carries `reference` (`billingoperator.go:61`, `:179`, `:198`). `quantityOutcomesSQL` is the sole admission list for the processing guard, startup audit and obligation check (`billingquantityaudit.go:61`); changed guard text triggers replacement. No remaining serving or migration statement clears an existing history gap. Pre-history and clock-reversal holds survive restart.
+
+- **E/H/N/T — Barrier and rollout.** The trigger’s overlap is exactly the paid-span selection: active, non-Free, with positive intersection between the month and `[effective_at, min(next effective_at, period_end))` (`billingstore.go:208`; `internal/plans/month.go:146`). Same-second supersession gives the earlier revision zero duration. Free/inactive rows contribute no paid span but still terminate predecessors. The book-assignment writer copies the subscription’s paid-through `period_end`; renewal or quantity dating changes `effective_at`, not that meaning (`billingstore.go:613`, `:650`, `:725`).
+
+ I found neither an allowed legacy freeze over a span this build prices from the book nor an excessive refusal over a valid legacy-only month. Creation precedes retirement of the old trigger. The retirement transaction refuses a missing replacement, leaves the old trigger intact on failure and is idempotent (`billingstore.go:797`). Both can temporarily exist; that fails closed. An older branch build can recreate the obsolete name, so the rollback restriction remains necessary. Startup migration supplies missing tables and columns before this binary serves.
+
+- **F/G/J/K — Figures, catalogs and public page.** Console inclusions divide `PriceFor`’s pooled allowance by the actual charged quantity (`billingpage.go:408`, `:427`). Personal’s unavailable row uses its valid one-member quantity. Every current scaled per-member figure is whole in base units and exactly representable within three displayed decimals; increasing team quantity introduces no rounding discrepancy. Band A’s rendered sentences are asserted verbatim (`billingcurrency_test.go:2627`).
+
+ Book validation checks complete prices, rounding, EUR compatibility, positive rates/floors, duplicate keys and arithmetic overflow (`internal/plans/pricebook.go:149`, `:231`). Configuration requires the selected catalog, permits complete optional catalogs and rejects reused price IDs; live configuration refuses an unapproved book (`cmd/console/billing.go:106`, `:134`). Provisioning checks cover pagination, at least 36 configured keys and the own-only currency-option rule (`stripe-provision.sh:393`, `:424`, `:434`).
+
+ The public export contains commercial data, not customer or credential data. The pinned site’s export is **byte-identical** to the platform’s committed export. The site formats exact rates, checks scaled inclusion exactness and approved numeric strings, and derives countries and their defaults from the export (`copy-review/pricing/build.mjs:48`, `:114`, `:134`, `:144`, `:160`). Its first-line marker is present; draft allowance requires exactly `"1"`, and ordinary build invokes the check. Signup links always contain both parameters; currency is populated only for an explicit choice (`src/site.js:75`). A locale-only suggestion can differ from the console’s subsequent default, as the owner’s rule expressly requires; it is never forwarded as a choice.
+
+**Claims the code contradicts**
+
+| Claim | Pinned-source result and smallest correction |
+|---|---|
+| Rollback-and-forward heals the obsolete-outcome gap: `DISPOSITIONS-multi-currency.md:70` | False after removing the sweep. Remove the healing assurance and explicitly say the gap remains held. The review-eight historical entry at `:199` should identify that behavior as superseded. |
+| Review seven’s reviewers agreed on the first two findings as P2: dispositions `:176` | Its own `:181` records Opus P3 versus Astra P2 for the second read. Correct the agreement sentence. |
+| Both eighth reports closed every seventh finding: dispositions `:194` | Astra’s eighth report explicitly marked reference handling partly closed. Likewise, the ninth-round summary at `:212` overstates closure of the eighth-round claims. |
+| Every recorded purchase passes `intentSession`: `billingservice.go:238`; design `:152` | Reconciliation uses its own weaker checks. Finding 1 requires code correction, not merely softer wording. |
+| `intentSession` previously failed to judge terms version: dispositions `:217` | Terms version was already validated through `priceTermsFromMetadata` and compared through `PriceTerms`. The new direct terms comparison is redundant; the plan/band comparisons are the substantive fix. |
+| The review reader carries `reference`: dispositions `:200` | The listing/API does. Singular `billingReview` still selects no reference (`billingstore.go:786`). Add that column to its read or narrow the claim. No additional operator-API defect follows. |
+| Review-four fixes uniformly have distinguishing tests: dispositions `:108` | Too broad. Provisioning tests inspect shell text (`internal/plans/provision_test.go:27`). Successful migration does not distinguish unsafe ordering (`billingcurrency_test.go:2493`). Repeated resolution returns before DELETE, so it does not test the DELETE’s intent predicate (`billingstore.go:879`; `billingcurrency_test.go:2957`). |
+| The post-resolution webhook test replays the refused completion | Its fake has already been changed to an open replacement session (`billingcurrency_test.go:2939`, `:2964`). Preserve separate old/new session objects to establish the late-completion claim. |
+| The site test proves export provenance | `test/pricing-pricebook.test.mjs:32` checks schema, currencies and a source-note pattern, not export equality. Current bytes match independently; add an actual provenance/equality check or rename the test. |
+
+Review three’s surviving default-as-explicit mutation is correctly qualified for ordinary accepted forms. Review four’s eight historical mutation runs cannot be certified from source. Removing an inclusion assertion over already-exact data still passes; changing the data to violate exactness is a different, meaningful mutation.
+
+The added browser helper has no shared cookie jar (`billingcurrency_test.go:1773`). Fixtures use wall-clock time for sessions and webhook freshness, but I found no concrete calendar-date, ordering or temporary-root failure in the inspected additions.
+
+**What must be verified against Stripe test mode before enablement**
+
+1. **Catalog and verifier:** Read all 36 expanded prices; establish exact amounts, currency-option shapes, metadata, recurrence and tax behavior. Exercise more than 100 objects and a failed later page; verification must stop on failure.
+2. **Checkout matrix:** Complete USD/EUR/GBP purchases across IE, GB, US, IN and JP, including IN/GBP. Confirm unit price × quantity, session/subscription/item currency, disabled Adaptive Pricing and no conversion.
+3. **Independent metadata and webhook ordering:** Establish which Checkout Session metadata Stripe permits changing independently of subscription metadata. Exercise both completion-first and subscription-first delivery; mismatches must produce one review and no activation.
+4. **Quantity and renewal:** Exercise both proration policies and renewal in every currency. Verify price identity, charged quantity, proration dates and assignment dates; run the quantity-fence rehearsal before enabling that worker.
+5. **Overages:** Exercise every meter, rounding boundary and currency-specific floor through creation, recovery, finalization and payment, including canceled subscriptions. Confirm line currency, subtotal and tax.
+6. **Recovery:** Lose responses around checkout and invoice operations; replay and recover by listing within and beyond retention windows. Confirm stable identities, legacy request compatibility and no duplicate charges.
+7. **Refusal and resolution:** Exercise open-session expiry, paid refusals, concurrent pass/webhook observations, visible references, exact-intent retirement and late delivery of the actual retired completion. Then open a new purchase and invoice its next month.
+8. **Customer constraints and discounts:** Establish currency restrictions during and after subscriptions, with outstanding invoices, and with percentage, fixed-amount and house discounts.
+
+Separately, rehearse migration and repeated startup against database copies containing pre-history and clock-reversal holds; Stripe cannot verify their preservation.
+
+**Verdict: accept with fixes.**
\ No newline at end of file
diff --git a/docs/superpowers/specs/OPUS-REVIEW-multi-currency-review10.md b/docs/superpowers/specs/OPUS-REVIEW-multi-currency-review10.md
new file mode 100644
index 000000000..502c0bc63
--- /dev/null
+++ b/docs/superpowers/specs/OPUS-REVIEW-multi-currency-review10.md
@@ -0,0 +1,106 @@
+# Tenth review of `feat/carloku-multi-currency-20261001` at `faf1bd25` (base `12e70d95`)
+
+**Verdict: accept with fixes.** Both ninth-round findings are closed in the code. Nothing new touches money. There are two P3 fixes: one line in reconcile, and the roll note.
+
+**How I worked.** I read only with `git show faf1bd25:`, modified nothing and ran no tests. The requested delta `faf1bd25..faf1bd25` is empty, so I read `f8a23ef3..faf1bd25` as the ninth-round fixes. That delta changes code in two places: `billingquantityaudit.go` drops the heal sweep, and `billingservice.go` adds three lines to `intentSession`. The rest is tests and the dispositions. `internal/plans`, `cmd`, `billingpage.go`, `billinghandlers.go`, the trigger code and the site are unchanged since round eight. The site's `copy-review/pricing/pricebook.json` at `9427c22` is byte-identical to `internal/plans/public-pricebook.json` at `faf1bd25` (sha256 `d91d5a48…`). I re-read in full only what changed or interacts with it: the binder, reconcile, every resume and listing path, the pass, the review store and the quantity audit.
+
+## Ninth-review findings
+
+| Finding | Closed? | Would its test pass without its fix? |
+|---|---|---|
+| **Opus P2-1 / Astra 1 (P2)**: the heal sweep cleared gaps that were set on purpose | **Closed.** The clearing `UPDATE` is gone (`billingquantityaudit.go:46-53`). Nothing else writes `history_gap=0`: see CC. | **No, for all three tests.** The pre-history test (`billingmembership_test.go:717-724`) and the clock test (`billingquantityaudit_test.go:190-196`) both process their events as `included` with `processed_at ≥ occurred` (fixture `billingmembership_test.go:457`). The old `NOT EXISTS` would therefore be true, and the heal would clear both gaps. `TestAGapFromAnObsoleteOutcomeListStaysHeld` (`billingcurrency_test.go:3111-3151`) also fails under the heal: the gap goes to 0, and `held` goes false, because its only event is processed with an admitted outcome. |
+| **Opus P3-1**: `intentSession` did not judge plan, band or terms | **Closed.** The new comparison is at `billingservice.go:247-249`. Every offer and keep path goes through `intentSession` (DD). | **Partly.** The `plan` and `band` iterations fail without the fix. The `terms_version` iteration **passes without it**: `priceTerms()` already carries `TermsVersion` (`billingstore.go:106-107`), and `Validate` refuses unknown terms (`pricebook.go:507`). The new terms clause is redundant but harmless. |
+| **Opus claims** (six) | **Five corrected.** The roll-note half of claim 5 is still in the text (P3-2 below). Claim 6 (`billingmembership.go:144`, "for good") is true again now that the heal is gone. | n/a |
+| **Astra claims** | **Not dispositioned.** The review-nine table lists only Opus's claims. Of Astra's: the `audit.go:46` comment is gone with the code; "generated checks", "dropped first", the inclusion mutation and "the one P2" are fixed. Line 194 still says both eighth reports found every seventh-round finding closed. The singular `billingReview` (`billingstore.go:784-789`) still omits `reference`, but its only callers are the quantity reviews (`billingquantity.go:1234,1295`), so it is not a defect. Site provenance is unchanged. | n/a |
+
+**Where the reviewers disagreed.**
+- **The heal sweep.** Both rated it P2, and P2 was right. Clearing the gap releases `quantityObligationTx` (`billingquantityaudit.go:64-67`), and that check gates both freezing and retirement. The verdicts differed (Astra reject, Opus accept with fixes). The fix turned out to be deleting one statement, so accept with fixes was the proportionate reading.
+- **Opus P3-1.** Astra did not report it. P3 is right: only the provider can change the metadata.
+- **The round-eight terms dispute**, which Astra re-argued at P2. From the code it is P3:
+ - It needs an authenticated provider to change metadata the console set.
+ - The money stays the intent's either way. Reconcile verifies the subscription's price ID, quantity and currency against the intent (`billingservice.go:739-773`), which is also why P3-1 below is benign.
+
+## Findings
+
+### P3-1. Reconcile records from a session whose plan, band or terms the binder refused
+- **Where:** `billingservice.go:793-799`. Reconcile judges the bound session with `ownSession` + `FixedCurrency` only, not `intentSession`.
+- **Scenario:**
+ 1. The intent is bound at creation (`saveBillingCheckout`).
+ 2. The provider returns the completed session with `metadata.plan` or `band` altered. The subscription's own metadata is intact.
+ 3. The pass, or the `checkout.session.completed` webhook, reaches the binder (`:967-980`). It files `billing_checkout_currency_refused` and refuses.
+ 4. Then `customer.subscription.created` (`billinghandlers.go:270-271`) runs reconcile.
+ - The intent matches the subscription (`:767`), and the session passes `ownSession` and `FixedCurrency`.
+ - So the purchase is applied and the intent deleted (`billingstore.go:746`).
+- **Result:**
+ - An open review says "nothing is recorded" over a purchase that was recorded.
+ - Its resolution finds no intent and retires nothing (`billingstore.go:921-948`).
+ - The money is correct, because the subscription's price and quantity are the intent's.
+ - It contradicts the `intentSession` comment (`:238-242`, "every session the console … records a purchase from passes through here") and design line 152.
+- **Fix:** at `:793`, use `b.intentSession(session, intent, acct)` in place of `ownSession`, and keep the subscription and customer clauses.
+
+### P3-2. The roll note still promises a heal that no longer exists
+- **Where:** `DISPOSITIONS-multi-currency.md:70`: "this binary's sweep clears such a mark again on the next start, so a roll back and forward heals, but the account is held in between."
+- **Scenario:**
+ 1. An operator trusts the note and rolls back past `3e941f00` after a refusal was resolved.
+ 2. The older sweep marks the account (`billingquantityaudit.go:42`).
+ 3. Rolling forward no longer clears it, and no operator door exists. That account's months never freeze and it cannot retire.
+- **Fix:** reword the note. The prohibition is absolute, and a mark it causes holds the account until the (unbuilt) operator door exists.
+
+## Added sections, and what interacts with this round's changes
+
+- **CC.** No statement can clear a gap:
+ - **The summary upsert only ORs.** It reads the stored gap, ORs in `clockReversed` and writes it back (`billingmembership.go:462-467,487-492`).
+ - **The sweep only sets.** Its one statement sets `history_gap=1` (`billingquantityaudit.go:42`).
+ - **No row can be dropped.** There is no `DELETE` of `billing_quantity_changes` and no table rebuild.
+ - **The pre-history default applies once.** `DEFAULT 1` applies only when the column is first added (`:144-147`); later starts leave it alone.
+ - **Both holds survive restarts.** The pre-history and clock holds survive every start. Both tests now show it, and each would fail with the heal back in.
+- **DD.** Every path that offers or keeps a session now runs `intentSession`, which covers plan, band, terms, book and currency:
+ - **The two resume reads:** previous intent (`:559`) and current intent (`:614`).
+ - **The fresh create (`:660`).**
+ - **The interactive replay (`:1180`).**
+ - **The listing (`:1215`).**
+ - **The pass's open branch** (`billingpass.go:214`).
+
+ Two things are still not judged on a session:
+ - **The bound-session read in reconcile:** P3-1.
+ - **The session's line items and discount.** The session read is not expanded, so its price and quantity are never compared. They are console-set and cannot change after creation, and reconcile checks the subscription item's price and quantity.
+
+ Base `12e70d95` already required `plan`, `band` and `terms_version` in session metadata (`billingservice.go:611` there), so legacy sessions carry them and the new check refuses no legacy session.
+- **D / L / R / W / Y / AA / S / V / Z / BB.** The binder still checks ownership first, before the purchase identity (`:938`, `:967`). `fileCheckoutRefusedReview` still requires the exact intent to stand. Resolution still deletes by account plus intent id. The only new interaction is P3-1.
+- **X.** `quantityOutcomesSQL` is still the one list. With the heal gone, it has three users: the guard, the marking sweep and the obligation check.
+- **A, B, C, E–K, M–Q, T, U.** No code changed this round. The round-eight and round-nine traces stand, including the GBP purchase end to end with no conversion path. The site's book matches the platform export byte for byte.
+
+## Claims the code contradicts
+
+1. `DISPOSITIONS:70`, the roll note's heal sentence (P3-2).
+2. `billingservice.go:238-242` and design line 152 say every session the console records a purchase from passes the one intent check. Reconcile does not (P3-1).
+3. `DISPOSITIONS:216`: "intentSession compares plan, band and terms version too", with its test as the reproduction. The terms clause was already enforced through `priceTerms()`, so that iteration of the test passes without the fix.
+4. `DISPOSITIONS:218`: "six stale sentences … corrected". Opus's claim 5 (the roll note) is not.
+5. `DISPOSITIONS:194`: "Both found every seventh-round finding closed". Astra marked reference handling partly closed. This is unchanged and not dispositioned.
+6. `DISPOSITIONS:199,204` still present the heal as the fix for round eight's P3-1, with no note that review nine removed it. The review-nine row does record the removal, so this is a reader trap, not a falsehood.
+
+## Before enabling: Stripe test mode
+
+The ninth reports' lists stand:
+- catalog and paging;
+- the currency × country matrix, including IN with GBP;
+- the refusal shape;
+- subscription currency and metadata;
+- the customer currency lock;
+- overage invoices per currency;
+- proration and renewal;
+- lost-response replay and listing;
+- expiring a completed session;
+- a replay after a resolution;
+- promotion codes;
+- a refusal resolved, with its id in `billing_retired_intents` and in `GET /api/billing/reviews` while open;
+- a concurrent webhook and pass giving one review;
+- the rollback rehearsal.
+
+Changed or added:
+
+1. **Metadata immutability (item 15).** Confirm that `metadata` and `subscription_data.metadata` (`client.go:644-645`) cannot change after creation or diverge from each other. If they cannot, P3-1 and the plan/band checks are belt and braces.
+2. **Line items.** Read a session with `expand[]=line_items` and confirm the price and quantity are the ones sent. Nothing in the console checks them on the session.
+3. **Restart (item 16, local, not Stripe).** Restart against a copy of a database holding a clock-rollback hold and a pre-history summary. At `faf1bd25` the gap should survive; at `f8a23ef3` it did not.
+
+**Verdict: accept with fixes.** Make reconcile's session check `intentSession` (P3-1), correct the roll note (P3-2), and fix the stale disposition lines. Nothing found this round affects money.