Close round seventeen: a size is recorded only for the same store that was read before sizing
Round seventeen read the diff since round sixteen only. Opus accepted; Astra accepted with fixes (one P2). It is the first round with no rejection, and it is not a review of the whole branch. - The storage pass compares the store's whole identity after sizing (objstore.SameStore), not its bucket name: a store torn down and made again under the same name during the listing is not measured. - A prefix that is not an account's app has no store record and is no longer read as a store that could not be measured. The round's reports and dispositions are added. Billing is off; nothing attests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
8 files changed,
+233
−9
docs/superpowers/specs/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md+14 −0docs/superpowers/specs/CODEX-FINDINGS-carloku-paid-launch-overnight-round17.md+50 −0docs/superpowers/specs/OPUS-REVIEW-carloku-paid-launch-overnight-round17.md+85 −0implementation/carloku-paid-launch/AGENT_HANDOFF.md+1 −1implementation/carloku-paid-launch/OUTSTANDING.md+1 −1internal/console/economicspass.go+17 −7internal/console/economicspass_test.go+54 −0internal/objstore/walk.go+11 −0
CI passed — run details
diff --git a/docs/superpowers/specs/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md b/docs/superpowers/specs/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md| index 600774c3b..9d8de0106 100644 |
| --- a/docs/superpowers/specs/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md |
| +++ b/docs/superpowers/specs/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md |
| @@ -486,3 +486,17 @@ Both found every round fifteen finding closed. Astra: one P1, two P2. Opus: one |
| | Opus N4: the writer's handling of the mark's earlier place had no test | Accepted. | `ownership_test.go` | `TestTheMarkIsReadFromWhereAnEarlierBuildWroteIt` (now also writes a record with the earlier field set and reads the raw result) | |
| Mutation checks in a scratch copy, each failing its test: every unswept entry holding the day for everyone; the per-account count not kept; an entry with no account not holding the day; the re-read after sizing ignoring a teardown; the writer leaving the mark in its earlier place. |
| + |
| +## Round seventeen (reviewed `86a1849d`, the diff since round sixteen only; Astra accepted with fixes, Opus accepted) |
| + |
| +Reports: `CODEX-FINDINGS-carloku-paid-launch-overnight-round17.md`, `OPUS-REVIEW-carloku-paid-launch-overnight-round17.md`. |
| + |
| +The first round with no rejection. It was a review of the diff `abc9f781..86a1849d`, not of the branch. Both found every round sixteen finding closed. Astra: one P2. Opus: three P3 and one wording correction. |
| + |
| +| Finding | Disposition | Where | Reproduction | |
| +| --- | --- | --- | --- | |
| +| Astra F1, Opus P3-2: the re-read after sizing judged "the same store" by the bucket name and the absence of a teardown, so a store torn down and made again under the same name during the listing, or taken back, passed | Accepted. The re-read compares the estate the walks compare (`objstore.SameStore`: bucket, key, grant instant, grants counted, teardown state, take-backs). A key rotated during sizing also discards the size, which errs toward holding. | `console/economicspass.go` `storagePass`; `objstore/walk.go` `SameStore` | `TestStoragePassDiscardsASizeTakenAcrossAReplacementOfTheStore` (made again under the same name; record deleted), `TestStoragePassDiscardsASizeTakenAcrossATeardown` | |
| +| Opus P3-1: a prefix that is not an account's app is read as a store that could not be measured, and holds storage for everyone every day it exists | Accepted. The pass does not look for a store record under a name no record can have. No test was added for it. | `console/economicspass.go` `storagePass` | none | |
| +| Opus P3-3 and Astra, claims: "if a teardown began or the store is no longer the same" said more than the check did | Made true by the first row. | none | none | |
| + |
| +Mutation check in a scratch copy: the comparison reverted to the bucket name fails the new test. |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-carloku-paid-launch-overnight-round17.md b/docs/superpowers/specs/CODEX-FINDINGS-carloku-paid-launch-overnight-round17.md| new file mode 100644 |
| index 000000000..6b1979064 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-carloku-paid-launch-overnight-round17.md |
| @@ -0,0 +1,50 @@ |
| +# Round 17 review: `86a1849d` |
| + |
| +Reviewed against base `aed42815`, concentrating on `abc9f781..86a1849d`. No files changed, tests run, network commands, or Stripe/AWS CLI calls. Test counterfactuals below are based on inspection. |
| + |
| +Locations refer to `86a1849d`. **C** = `internal/console`; **O** = `internal/objstore`; **U** = `internal/usage`; **D** = `implementation/carloku-paid-launch`; **S** = `docs/superpowers/specs`. |
| + |
| +## 1. Round 16 dispositions |
| + |
| +| Finding | Disposition | Fix and regression test | Would the test pass without the fix? | |
| +|---|---|---|---| |
| +| **Astra F1 / Opus N1:** one unswept store holds storage for every account | **Closed for statements rebuilt by this code.** Exclusions are deduplicated per account per day; an unidentified account still holds everyone. Existing closed statements remain immutable. | `C/economicspass.go:887`, `:947`, `:1681`, `:1707`. `TestUnsweptStorageHoldsOnlyItsOwnAccount`, `C/economicspass_test.go:1572`; existing `TestStatementsPassUnsweptStorageIsNotComplete`, `:872`. | **No** for the new test: restoring the global hold fails the unrelated account’s assertion. **Yes** for the existing test when the entire change is reverted: it checks only the affected account. | |
| +| **Astra F2:** sizing admitted before teardown records the remaining account’s bytes | **Partly closed.** The re-read catches a teardown block that remains, disappearance, unreadability, and a changed bucket name. It does not establish that the same store survived sizing; see F1 below. | `C/economicspass.go:227`. `TestStoragePassDiscardsASizeTakenAcrossATeardown`, `C/economicspass_test.go:1625`. | **No.** Without the teardown check, the fixture records 31 GiB. The test leaves the teardown block present; it does not exercise recreation or successful take-back. | |
| +| **Astra F3 / Opus N3:** an admitted queue drain returns metadata after retained teardown | **Accepted and left open**, as 19f. No production caller or enqueue call was found. | Documentation: `D/OUTSTANDING.md:70`; disposition: `S/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md:485`. Unchanged mechanism: `O/queue.go:276`, `:314`, `:420`, `:467`. | No fix or regression test claimed. | |
| +| **Opus N2:** live shared buckets are sized fully for each store | **Accepted and left open**, as 19i. | `D/OUTSTANDING.md:68`; disposition `S/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md:484`. Unchanged whole-bucket sizing: `C/economicspass.go:223`, `:1913`. | No fix or regression test claimed. | |
| +| **Opus N4:** writer normalization of the legacy delivery mark is untested | **Closed.** The added case explicitly sets the legacy field after reading and inspects raw persisted JSON. | Writer: `O/objstore.go:277`. Test: `TestTheMarkIsReadFromWhereAnEarlierBuildWroteIt`, `O/ownership_test.go:3352`; added case `:3401`, assertion `:3417`. | **No.** Removing writer normalization leaves the nested mark and omits the required top-level mark. | |
| + |
| +The platform account uses the same adjusted statement construction as other accounts (`C/economicspass.go:1670`). `BillingSnapshot` checks that statement’s own source counts through `MeterComplete` (`U/statement.go:176`, `:210`). The economics view consumes modelling totals and `CoveredDays`, not the shared storage completeness count (`C/economicsview.go:418`, `:630`, `:653`); this diff introduces no shared-count substitution there. |
| + |
| +## 2. New findings |
| + |
| +### F1. P2 — Recreation under the same bucket name defeats the post-sizing check |
| + |
| +**Where:** `C/economicspass.go:201`, `:227`, `:237`. Supporting lifecycle code: `O/walk.go:606`, `:608`, `:1316`, `:1330`. |
| + |
| +**Scenario:** |
| + |
| +1. Account A solely holds bucket X. `storagePass` reads A’s record and enters sizing; pause before its first bucket listing. |
| +2. A completes teardown. An operator grants X to B, which writes objects. |
| +3. Resume the listing so it obtains B’s bytes, then pause before the post-sizing record read. |
| +4. B completes teardown. A declares and provisions a new, empty store under X. |
| +5. Resume the re-read. A’s record exists, has `Provisioned.Bucket == X`, and has no teardown block. The check accepts B’s measured size and writes it under A with `StoreUnswept=false`. |
| + |
| +These are sequential owners; no simultaneously shared bucket is required. This therefore exceeds the documented 19i limitation. |
| + |
| +The first record’s generation is discarded. Neither the new grant instant, key, nor other estate identity is compared. The walks already distinguish replacement stores with the same name (`O/walk.go:967`, `:1014`, `:1330`), but the new storage check does not. |
| + |
| +The incorrect measurement enters a create-only storage day (`C/economicspass.go:243`; `U/statement.go:121`) and folds into A’s storage total (`C/economicspass.go:1062`). Because it is not marked unswept, the new account exclusion mechanism cannot hold it. Billing remains off; this establishes incorrect persisted usage, not a current charge. |
| + |
| +**Smallest acceptable fix:** Preserve the original record generation and require it to match after sizing, alongside the existing checks; discard and hold on a change. Add a regression that replaces the store under the same bucket name during sizing. A successful take-back that clears the teardown block also needs coverage. |
| + |
| +## 3. Claims the code contradicts |
| + |
| +| Location | Claim | What the code establishes | |
| +|---|---|---| |
| +| `D/AGENT_HANDOFF.md:35`, newly added parenthesis | “a size taken across the start of a teardown is discarded” | Only if the final read still exposes teardown, disappearance, unreadability, or a different bucket name. Recreation or take-back can erase those indicators: F1. | |
| +| `S/CODEX-DISPOSITIONS-carloku-paid-launch-overnight.md:483` | If teardown began or the store is no longer the same, the size is discarded. | The check compares the bucket name and current state, not store identity or whether teardown occurred during sizing. | |
| + |
| +The changed descriptions of the per-account hold, persistent held days, shared-bucket limitation, dormant queue race, and legacy-mark regression are supported. The claimed mutation executions were not independently rerun. |
| + |
| +**Verdict: accept with fixes.** |
| \ No newline at end of file |
diff --git a/docs/superpowers/specs/OPUS-REVIEW-carloku-paid-launch-overnight-round17.md b/docs/superpowers/specs/OPUS-REVIEW-carloku-paid-launch-overnight-round17.md| new file mode 100644 |
| index 000000000..6e5d17e83 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/OPUS-REVIEW-carloku-paid-launch-overnight-round17.md |
| @@ -0,0 +1,85 @@ |
| +# Opus production and authority review, round 17: Carloku paid launch, `aed42815..86a1849d` |
| + |
| +**Verdict: accept.** Every round 16 finding is either closed or accepted and left open, as the dispositions say. The storage hold is now per account and matches the fold that attributes the bytes. The re-read after sizing closes Astra's F2 interleaving. I found no new P1 or P2. There are three P3 findings and one overstated claim. |
| + |
| +I read everything with `git show 86a1849d:<path>`, changed nothing, ran no tests and made no network calls. Every answer to "would the test pass without the fix?" comes from reading the test against the code. |
| + |
| +Abbreviations: **C** = `internal/console`, **O** = `internal/objstore`, **D** = `implementation/carloku-paid-launch`, **S** = `docs/superpowers/specs`. |
| + |
| +## Production authority |
| + |
| +- **Nothing attests.** `git diff abc9f781 86a1849d` touches only `C/economicspass.go`, its test, `O/ownership_test.go` and documents. `SetAttestation` and `SetAllClientsInstrumented` still have no caller outside tests. |
| +- **The quantity worker cannot reach the provider with either switch unset.** These files are unchanged since my round 9 to 16 readings: `billingquantity.go`, `billingservice.go`, `internal/billingstripe/client.go`, `cmd/console/billing.go`, `walklock.go`, `fence.go`, `aws.go`, `walk.go`. The worker still needs the proration policy and `QuantityFence == "stripe-sandbox-verified"`, and `cmd/console` rejects any other non-empty value. Those readings stand: |
| + - The walk lease and the signing-time fence have been observed on AWS only (19d). |
| + - The conditional delete is unverified on a real backend (19c). |
| + - `TestSandboxQuantityFence` has not been run, and `D/AGENT_HANDOFF.md` item 8 still says so. |
| +- **Billing.** Still off. This round's change only narrows a hold, from every account to the account that owns the unmeasured store. That account's zero is still never complete (`usage/statement.go:176`), so `BillingSnapshot` refuses it. Nothing here can bill a customer wrongly. |
| +- **Leaks.** The new code adds one log line (`C/economicspass.go:232`). It names the account and app sqids, the same identifiers the neighbouring lines already log, and no secret or bucket content. |
| +- **Data.** Nothing in the diff writes to a store record, a bucket or a ledger. The pass's new `objstore.Get` is read-only. |
| + |
| +## 1. Disposition table (round 16) |
| + |
| +| Finding | Status | Fix | Test; fails without the fix? | |
| +|---|---|---|---| |
| +| **Astra F1 / Opus N1**: one unswept store holds storage for every account | **Closed.** Per-day `storageHeld` is filled from the storage-day key's account (`C/economicspass.go:877-896`). Only if the day still counts for storage does `StorageExcluded[acct]` go up (`:947-951`). `accountSourceDays` takes it off that account's `SourceStorage` (`:1707-1722`), and `statementsForMonth` passes it per account (`:1681`). A key whose account fails `release.ValidAccount` still sets `storageOK = false` for everyone (`:888-891`). That matches `foldStorageAndCensus`, which charges such a key's bytes to `OpsAccount` (`:1041-1061`). The hold's account and the fold's account are the same split of the same key, so the zero lands on the account that is held. | `economicspass.go` | `TestUnsweptStorageHoldsOnlyItsOwnAccount` (`C/economicspass_test.go:1572`): **fails** under each of three mutations. With the old fleet-wide hold, the other account reads 0 and the test wants 1. Without the subtraction, the held account reads 1 and the test wants 0. If an invalid key is treated per account, the other account reads 1 and the test wants 0. `TestStatementsPassUnsweptStorageIsNotComplete` (`:872`) still pins the account's own hold. | |
| +| **Astra F2**: a pass admitted before a retained teardown records the remaining account's bytes | **Closed for the reported shape.** After a successful size, the record is read again. The size is discarded and the store marked unswept in four cases: the read errors, the record is gone, it carries a teardown, or `Provisioned` is gone or names another bucket (`C/economicspass.go:227-235`). A teardown that lands after the re-read began after the listing ended, so the bytes listed were not listed across it. | `economicspass.go` | `TestStoragePassDiscardsASizeTakenAcrossATeardown` (`C/economicspass_test.go:1625`): **fails** (`StoreBytes = 31 GiB`, not unswept). It covers only the teardown branch. Record-deleted and bucket-changed are untested (P3-2). | |
| +| **Astra F3 / Opus N3**: the queue drain checks permission once per batch | **Accepted and left open** as 19f. The added sentence matches the code: one `queuePermitted` per batch, plus the fallback check. Nothing calls `DrainApp` and nothing enqueues. | `D/OUTSTANDING.md` 19f | none | |
| +| **Opus N2**: a live shared bucket is sized in full for each store | **Accepted and left open** as 19i. The text matches `sizeProvisionedStore` (`C/economicspass.go:1905-1937`): the whole bucket, no prefix, the console's ambient credentials. Nothing makes the hold that 19i requires automatic. Storage on a shared bucket stays safe only while storage billing is off. | `D/OUTSTANDING.md` 19i | none | |
| +| **Opus N4**: `Put`'s migration of the mark has no test | **Closed.** | `O/objstore.go` (unchanged) | `TestTheMarkIsReadFromWhereAnEarlierBuildWroteIt`, new block at `O/ownership_test.go:3399-3419`: **fails**. It sets `Grant.LegacyDelivering`, writes through `Put`, then asserts on the raw JSON: a top-level `delivering` and none under `grant`. Without the normalisation, the mark serialises under `grant` and there is no top-level field. | |
| + |
| +## 2. New findings, most severe first |
| + |
| +### P3-1. An invalid-account key still holds storage for everyone, though no store record can exist under it |
| + |
| +**Where:** `C/economicspass.go:202-210` and `:888-891`; `O/objstore.go:240-242`. |
| + |
| +**Scenario:** |
| +1. A legacy unqualified prefix exists, such as `apps/<app>/releases/`. The fold's own comment (`:1050-1058`) says these have been seen. `storagePass` lists it as account `<app>`. |
| +2. `objstore.Get` refuses the invalid account before reading anything, so `gerr != nil` and the entry is written `StoreUnswept`. |
| +3. `sweepUsage` cannot resolve the key's account, so it holds storage for every account. It does this every day the prefix exists. |
| + |
| +The new per-account rule therefore gives no relief while such a prefix exists. This predates the branch: it was fleet-wide before, too. It errs toward holding and does not make 19a or 19i worse. But a refusal by name validation is not a store that could not be measured: no build writes a store record under an invalid account. |
| + |
| +**Smallest fix:** in `storagePass`, skip `objstore.Get` (no store, not unswept) when `!release.ValidAccount(acct) || !release.ValidApp(app)`. The fold already charges the release bytes to `OpsAccount`. Alternatively, leave it and note it under 19a or item 18. |
| + |
| +### P3-2. The re-read's notion of "the same store" is weaker than the walks', and only one of its branches is tested |
| + |
| +**Where:** `C/economicspass.go:227-228`. The first read discards its etag at `:201` (`rec, _, gerr`). |
| + |
| +**Scenario:** during a long listing, A's retained teardown begins on shared bucket X. An operator then grants A's take-back, which clears the teardown on the same bucket name. The re-read sees `Teardown == nil` and the same bucket, so it accepts a size that spanned a period when X was B's alone. A and B now share X live, so the size is wrong only in the way 19i already covers. This is not a new mis-attribution, but nothing beyond the bucket name checks that the estate is the same. |
| + |
| +**Smallest fix:** compare the estate fields the walks compare: grant instant, key ID and take-back count. Then add one case each to the test for record deleted and bucket changed. Comparing etags would also catch this, at the cost of holding a day across a benign write. |
| + |
| +### P3-3. A disposition says more about the re-read than the check does |
| + |
| +See §3. Correct the wording only. |
| + |
| +### Asked and not found |
| + |
| +- **An account whose statement reads storage complete while one of its stores was not measured.** None found. |
| + - Each `StoreUnswept` entry either holds its own key's account or holds everyone. |
| + - `StorageExcluded` is counted only on days `SourceDays[SourceStorage]` counts, so the subtraction is exact and cannot go below the real count. |
| + - A held account is always in the statement universe: the fold touches its key, so `meters[acct]` exists. |
| + - A partially written day cannot occur: `CreateDayObject` runs once, after every app. |
| + - Another view or fold that reads the shared count where the account's own count is right: |
| + - The platform's own statement is account `bab`. It goes through the same `accountSourceDays` (`:1681`) and is held by both an unswept `bab/...` entry and an invalid-account entry. |
| + - The billing snapshot reads `st.MeterComplete`, which reads per-account `SourceDays`. |
| + - The economics view (`C/economicsview.go:414-654`) reads only `CoveredDays` and `PeriodDays`, which are for modelling and were never per account. It does not decide whether a meter is complete. |
| +- **A size recorded for a store whose teardown began during the listing.** None found. A teardown block written at any point before the re-read is seen there. One written after it began after the listing finished. |
| + |
| +## 3. Claims the code contradicts |
| + |
| +| Location | Claim | What the code supports | |
| +|---|---|---| |
| +| `S/CODEX-DISPOSITIONS…` round 16, Astra F2 row | "if a teardown began or the store is no longer the same, the size is discarded" | "The same" is judged by the record existing, carrying no teardown, and naming the same provisioned bucket (`C/economicspass.go:227-228`). It is not judged by the estate the walks compare (P3-2). Suggested wording: "…or the record is gone or names another bucket…". | |
| + |
| +These were checked and are accurate: |
| +- `D/AGENT_HANDOFF.md` item 5 ("holds it, for its own account only… a size taken across the start of a teardown is discarded"). A store record always has a valid account, so "only" holds. |
| +- `D/OUTSTANDING.md` 19 (round counts and outcomes), 19a ("a day written as held stays held for that account"; the day object is create-only), 19f (the drain sentence), 19i. |
| +- The round 16 dispositions' remaining rows, and the claim that nothing attests or reaches Stripe. |
| + |
| +None of these is made worse by this round: 6, 6a, 7, 9, and 19a to 19i. |
| + |
| +## 4. Verdict |
| + |
| +**Accept.** Nothing attests, and the quantity worker cannot reach the provider with either switch unset. Nothing can bill wrongly, leak an identifier or secret, or destroy data. Where the evidence is ambiguous, the code holds. The three P3s and the one wording fix can ride the next commit. |
diff --git a/implementation/carloku-paid-launch/AGENT_HANDOFF.md b/implementation/carloku-paid-launch/AGENT_HANDOFF.md| index 76ded750d..36ed7d179 100644 |
| --- a/implementation/carloku-paid-launch/AGENT_HANDOFF.md |
| +++ b/implementation/carloku-paid-launch/AGENT_HANDOFF.md |
| @@ -32,7 +32,7 @@ When a review is approved, apply each string byte for byte: the site by replacin |
| 2. Console GET `/a/{sqid}/billing` (`billingpage.go`, `templates/billing.html`): current plan, owner-only chooser posting to the existing checkout door, `?country=` price preview computed from the plans terms, error and pending banners, portal button once a customer exists, read-only for members, byte-identical 404 for non-members, closed state when billing is off. `plans.Countries()` exposes the embedded country list. |
| 3. Durability ledger: the epoch read race is fixed (epoch and created-at are immutable after recover) with a `-race` regression. An unmetered replication fallback on a box whose ledger opened now files an escape (`DurabilityLedger.Escaped`): the box-day is incomplete and the owner is excluded for the run. A box whose ledger failed to open has no publisher and files nothing. The console fold asks each box's published day to prove the union of that box's durability, metrics and edge windows; a box with no ledger directory, no day file, or a ledger whose coverage starts after the box began serving holds the day for everyone. There is no tolerance. Durability days are counted per account: a box-day attested with an exclusion set is complete for every account the box did not exclude. The move worker's checkpoint is recorded against the route's owner. Nothing attests: attestation is false everywhere, in this code and in production. The scoped attestation (path A) was built, reviewed in round six, found unable to prove an exclusive writer, and removed from this branch. It is kept on `feat/carloku-scoped-attestation-20261001` with its findings open. Replication-write completeness is a launch blocker, not a waived requirement. |
| 4. `internal/awsbilling`: Cost and Usage Report reader (versioned deliveries only), newest-delivery selection, digests, charged S3 transfer classifier. It holds on a charged usage line it does not count and on any non-usage line (fee, refund, credit, discount) with a cost on a transfer usage type. Every transfer usage type the flagship account wrote from July to September 2026 is pinned in a test. |
| -5. `internal/objstore/ownership.go`: append-only bucket ownership ledger, opened at grant, closed at teardown. It resolves an owner only for a ledger that claims completeness, and only for hours that begin at or after the instant it began vouching, so a store's partial first hour never resolves (a ledger that began vouching exactly on the hour answers for that hour). Completeness needs a grant block recorded as minting, nothing delivered to the app yet, a probe showing the bucket does not exist, and an inventory showing no principal's policy names the bucket (`GranteeProber`), and is dated when both probes had answered. The AWS provisioner cannot list principals yet, so on AWS no ledger claims completeness, new stores included, and store transfer holds. A minting grant is refused while another app holds the name by ledger interval or by store record, and for a bucket that already exists and it never held. Grant, teardown and both rotation verbs are walks. A walk that spends backend authority takes a lease (`walklock.go`) on the bucket name, or on a hash of it when the name is one this build would refuse, or on a hash of the app when the record names no bucket: an object taken by compare-and-swap, bound to the invocation, thirty minutes long, standing until it runs out if the walk failed or any of its requests needed a second attempt. The record is read again under the lease and must still be the same estate, not only the same name: teardown and grant compare the grant instant, the key, how many grants have reached the provider, whether a teardown had begun, whether it noted the bucket deleted and how many take-backs it has seen; rotation compares the same estate and requires that no teardown has begun. Every grant is counted on the record before any call that changes anything at the provider, the user and its policy included (two read-only probes, of the bucket and of who can reach it, come first), so a healthy store granted again writes its record once. A grant marks the record before it changes the user's keys, and a rotation just before it delivers the new key (`Record.Delivering`); each takes the mark off when it records the result. A rotation that fails before delivery leaves no mark and is resumed by running it again, with the app's key kept (unless the write of the mark itself was lost in transit and landed, in which case the mark stands, which errs toward the repair). One whose delivery fails keeps the mark, because a failed write may still land. While the mark stands a grant run again walks in full and a rotation is refused (`ErrGrantUnfinished`). The mark does not save the key the app was holding: the repairing grant reaps and replaces it. A record from a build before the mark existed carries none, which means not recorded, not finished; the one earlier build of this branch that kept the mark inside the grant block is read. An older grant block that names no bucket is made to name it in the write that counts the walk. A record's bucket is the one it is provisioned on, else the one its grant block names: the arguments choose a bucket only for a store that has none, so a grant never moves a store to another name. A record whose grant block and provisioned block name different buckets (an earlier build could write one), or whose teardown note is about a bucket that is not its own, is refused by the record's one reader and one writer (`objstore.Get` and `objstore.Put`; the check is `objstore.Coherent`; `ErrTwoBuckets`, `ErrUnfinishedTeardown`), so no door, pass or walk can act on one or write one. The holder inventory reads such a record anyway (`objstore.Inspect`) and counts it as a holder of every bucket it names, whatever the ledger shows for that app. An operator repairs such a record by hand; there is no verb for it. A take-back over a bucket that a store of another account holds is refused unless an operator grants it; a store whose ledger interval is closed does not count as holding it; a scanning door never changes a store whose teardown has begun, its re-grant (`objstore.Regrant`) refuses one under the walk's own lease and never makes a first store, the scan pass does not sweep or queue one, a sweep already running does not publish after one begins, the scan status shows no coverage for one, and the daily storage pass holds it, for its own account only, instead of sizing its bucket (a size taken across the start of a teardown is discarded). A failed walk's lease stands until it runs out, with one exception: the same app re-running the same kind of walk may take it at once. A declaration-only teardown and the app purge take no lease; they delete the store record only as the generation they read. The user belongs to the app and the lease to the name, so a late request of a failed teardown can still remove the policy of a store granted afterwards under another name (round eight, Opus N2, open). Every request of a walk, to S3, to IAM and to the deployment bucket that holds the records and ledgers, is signed with the instant its lease was taken (`internal/sigfence`), so AWS refuses a request a paused process sends after its lease could be taken over. The record and its scan sidecar are deleted as the generation that was read. The signing-time rule was checked read-only against S3 and IAM with a list and two gets (`TestFenceAgainstAWS`); no delete was sent. A declaration-only teardown takes no lease (there is no bucket name) and relies on the conditional delete alone. A teardown writes a block on its own record before it touches anything and decides by compare-and-swap on the ledger (for a bucket name this build would refuse there is no ledger, and it decides from the store records alone): if nobody else holds the bucket it marks the bucket as being destroyed; otherwise it leaves the bucket and its scanner, revokes its own credentials, and records that the scanner is owed. A grant by the same app over its own unfinished teardown takes the store back, runs in full, and voids the teardown's note that the bucket was deleted. A teardown that leaves a bucket to another account keeps its record and returns `ErrBucketRetained` (discussion 191). It can finish once that bucket has been destroyed, when someone runs it again; nothing runs it again by itself. For a bucket name this build would refuse there is no ledger: a store whose teardown has begun is not counted as a holder there, so the last of two sharers destroys the bucket. `ErrBucketRetained` has no member copy yet; it needs Paul's review. A randomized simulation (`walk_sim_test.go`) checks the invariants over sequential walks with provider and store faults, including failures after the effect, and walks frozen past their lease while another takes the name. It does not generate a second bucket name, rotations, stores with no ledger interval or disagreeing clocks; those have direct tests. |
| +5. `internal/objstore/ownership.go`: append-only bucket ownership ledger, opened at grant, closed at teardown. It resolves an owner only for a ledger that claims completeness, and only for hours that begin at or after the instant it began vouching, so a store's partial first hour never resolves (a ledger that began vouching exactly on the hour answers for that hour). Completeness needs a grant block recorded as minting, nothing delivered to the app yet, a probe showing the bucket does not exist, and an inventory showing no principal's policy names the bucket (`GranteeProber`), and is dated when both probes had answered. The AWS provisioner cannot list principals yet, so on AWS no ledger claims completeness, new stores included, and store transfer holds. A minting grant is refused while another app holds the name by ledger interval or by store record, and for a bucket that already exists and it never held. Grant, teardown and both rotation verbs are walks. A walk that spends backend authority takes a lease (`walklock.go`) on the bucket name, or on a hash of it when the name is one this build would refuse, or on a hash of the app when the record names no bucket: an object taken by compare-and-swap, bound to the invocation, thirty minutes long, standing until it runs out if the walk failed or any of its requests needed a second attempt. The record is read again under the lease and must still be the same estate, not only the same name: teardown and grant compare the grant instant, the key, how many grants have reached the provider, whether a teardown had begun, whether it noted the bucket deleted and how many take-backs it has seen; rotation compares the same estate and requires that no teardown has begun. Every grant is counted on the record before any call that changes anything at the provider, the user and its policy included (two read-only probes, of the bucket and of who can reach it, come first), so a healthy store granted again writes its record once. A grant marks the record before it changes the user's keys, and a rotation just before it delivers the new key (`Record.Delivering`); each takes the mark off when it records the result. A rotation that fails before delivery leaves no mark and is resumed by running it again, with the app's key kept (unless the write of the mark itself was lost in transit and landed, in which case the mark stands, which errs toward the repair). One whose delivery fails keeps the mark, because a failed write may still land. While the mark stands a grant run again walks in full and a rotation is refused (`ErrGrantUnfinished`). The mark does not save the key the app was holding: the repairing grant reaps and replaces it. A record from a build before the mark existed carries none, which means not recorded, not finished; the one earlier build of this branch that kept the mark inside the grant block is read. An older grant block that names no bucket is made to name it in the write that counts the walk. A record's bucket is the one it is provisioned on, else the one its grant block names: the arguments choose a bucket only for a store that has none, so a grant never moves a store to another name. A record whose grant block and provisioned block name different buckets (an earlier build could write one), or whose teardown note is about a bucket that is not its own, is refused by the record's one reader and one writer (`objstore.Get` and `objstore.Put`; the check is `objstore.Coherent`; `ErrTwoBuckets`, `ErrUnfinishedTeardown`), so no door, pass or walk can act on one or write one. The holder inventory reads such a record anyway (`objstore.Inspect`) and counts it as a holder of every bucket it names, whatever the ledger shows for that app. An operator repairs such a record by hand; there is no verb for it. A take-back over a bucket that a store of another account holds is refused unless an operator grants it; a store whose ledger interval is closed does not count as holding it; a scanning door never changes a store whose teardown has begun, its re-grant (`objstore.Regrant`) refuses one under the walk's own lease and never makes a first store, the scan pass does not sweep or queue one, a sweep already running does not publish after one begins, the scan status shows no coverage for one, and the daily storage pass holds it, for its own account only, instead of sizing its bucket (a size is discarded if the record read after sizing is not the same store, by the estate the walks compare). A failed walk's lease stands until it runs out, with one exception: the same app re-running the same kind of walk may take it at once. A declaration-only teardown and the app purge take no lease; they delete the store record only as the generation they read. The user belongs to the app and the lease to the name, so a late request of a failed teardown can still remove the policy of a store granted afterwards under another name (round eight, Opus N2, open). Every request of a walk, to S3, to IAM and to the deployment bucket that holds the records and ledgers, is signed with the instant its lease was taken (`internal/sigfence`), so AWS refuses a request a paused process sends after its lease could be taken over. The record and its scan sidecar are deleted as the generation that was read. The signing-time rule was checked read-only against S3 and IAM with a list and two gets (`TestFenceAgainstAWS`); no delete was sent. A declaration-only teardown takes no lease (there is no bucket name) and relies on the conditional delete alone. A teardown writes a block on its own record before it touches anything and decides by compare-and-swap on the ledger (for a bucket name this build would refuse there is no ledger, and it decides from the store records alone): if nobody else holds the bucket it marks the bucket as being destroyed; otherwise it leaves the bucket and its scanner, revokes its own credentials, and records that the scanner is owed. A grant by the same app over its own unfinished teardown takes the store back, runs in full, and voids the teardown's note that the bucket was deleted. A teardown that leaves a bucket to another account keeps its record and returns `ErrBucketRetained` (discussion 191). It can finish once that bucket has been destroyed, when someone runs it again; nothing runs it again by itself. For a bucket name this build would refuse there is no ledger: a store whose teardown has begun is not counted as a holder there, so the last of two sharers destroys the bucket. `ErrBucketRetained` has no member copy yet; it needs Paul's review. A randomized simulation (`walk_sim_test.go`) checks the invariants over sequential walks with provider and store faults, including failures after the effect, and walks frozen past their lease while another takes the name. It does not generate a second bucket name, rotations, stores with no ledger interval or disagreeing clocks; those have direct tests. |
| 6. Statement folds: `console/durabilityfold.go` (`durability_writes_1k`, policy in `usage.BillableReplicationWrites`) and `console/storeegressfold.go` (`store_egress_gb`). `usage.MeterSource` names a source for both meters. Month evidence pins ownership ledgers by their month projection and knows the durability box directory. Statements carry the five billed meters by UTC day (`Statement.Days`), summing to the monthly totals; the other meters are not recorded by day. With a transfer report configured, the statements catch-up walks a 36-month window and sweeps every month that has an open statement, is missing a statement an account is owed (the platform's included), or is a gap between metered months. An account's statement that will not parse, or that cannot be read three passes running, is reported once per process and the month is classified without it. The platform's own statement is different: while it cannot be read the month stays unknown, for as long as that lasts. A month seen with no statement is remembered for six hours, a closed statement for the life of the process. |
| 7. Operator API doors (`billingoperator.go`): list and resolve billing review holds, list and set the house marker. No CLI verb yet. |
| 8. Team quantity worker (`billingquantity.go`) behind two switches, both unset: `CONSOLE_BILLING_PRORATION` (`immediate` or `next_invoice`, Paul's decision) and `CONSOLE_BILLING_QUANTITY_FENCE=stripe-sandbox-verified` (set only after `TestSandboxQuantityFence` has passed against a Stripe test-mode account; it has not been run). With either unset it makes no provider call. An operation is recorded by a compare-and-set on the billing row's revision, subscription, period, quantity and status, on the target and newest pending addition recomputed from the database, and on its own instant lying inside the period and not before the addition it charges for. The request dates its proration at that instant (`proration_date`). Sending and retiring exclude each other through a send lease on the operation's row, owned by one sender's token; an operation dated ahead of the observing clock is left alone. Every key ends in the account's operation serial. An operation nobody can show ran is acknowledged only after its idempotency key has been tried at the provider with a metadata-only request (the fence), or after a person has resolved the review it was held with: a resolved review stands in for the fence, on that person's word and with no provider check. If the fence request took the key, the operation never ran and may be retired, on a provider read taken after the fence when the subscription is current. If the key is held by the operation's own request, that request began and its outcome is not known: the operation is recorded as landed if the provider shows its target, and otherwise held with a review (`billing_quantity_operation_unsettled`) and never replaced until a person resolves it. A fence the provider refuses is held the same way. The fence rests on Stripe's documented key rules, is modelled by a fake in the tests, is not verified against Stripe, and lasts only as long as Stripe remembers a key (documented as at least 24 hours): `docs/superpowers/specs/2026-10-01-stripe-idempotency-evidence.md`. A quantity that goes down inside a period files `billing_quantity_lowered`. An operation is confirmed only by the provider's success answer to its own request. A confirmed landing takes effect at the operation's instant, so a seat's charge and its allowance start together; a quantity that merely matches an unconfirmed operation's target is recorded as `landed_unverified`, dated when observed, with a review. If the provider refuses a request and names `proration_date`, the operation's key is fenced at once and it is retired on the next read; if it does not name it, the operation waits out the 22-hour replay limit. None of the provider's side is confirmed: test mode is not authorized. Only additions of the subscription's current period can raise it; older ones are closed uncharged with a review; additions and operations of an ended subscription are closed. It does not yet lower the quantity at renewal. |
diff --git a/implementation/carloku-paid-launch/OUTSTANDING.md b/implementation/carloku-paid-launch/OUTSTANDING.md| index f0dc1461c..94c146eaa 100644 |
| --- a/implementation/carloku-paid-launch/OUTSTANDING.md |
| +++ b/implementation/carloku-paid-launch/OUTSTANDING.md |
| @@ -63,7 +63,7 @@ Each is concrete. None is waived. |
| ### Process |
| -19. **No review round has passed.** Astra rejected rounds one to sixteen; Opus accepted each with fixes. Every round has closed the shapes the round before reported and found neighbouring ones, in the store walks since round seven. Some concern records no walk of this build writes; others need only this build's ordinary walks (a failed rotation, a retained teardown, a scanning door racing a teardown), and two were faults in the previous round's own fixes. Round eleven found one P1 and two P2 in the store walks (a grant could move an older record to another bucket name; a waiting rotation ignored a grant that ran; the user and its policy were written before the grant was counted). Its fixes are on the launch branch (`1395f7bf`). Round twelve read them: every round-eleven finding is closed for its reported shape, Astra rejected again (one P1, two P2) and Opus accepted with fixes (one P2, three P3). The new findings were about records no walk of this build writes (a teardown note for another bucket, a record naming two buckets) being acted on by teardown, rotation and the scanning doors; a rotation started after a grant that failed at its last write; and a take-back that returned access to a bucket another account holds, through the member's own door. Round thirteen read those fixes: closed for their reported shapes, Astra rejected again (two P1, four P2) and Opus accepted with fixes (one P2, five P3). The new findings were paths that read such a record and acted without the check (the holder inventory, the storage pass, the declare door), a rotation's own failed last write, and a retained record blocking another account's take-back. The check now sits in the record's reader and writer. Round fourteen read those: closed for their reported shapes; Astra rejected (two P1, two P2), Opus accepted with fixes (two P2, two P3). Round fifteen read those: all closed; Astra rejected (one P1, one P2), Opus accepted with fixes (one P2, two P3), all about readers of a bucket that a retained teardown left to another account (a sweep in flight, the scan status, the storage pass). Round sixteen read those: all closed, and one of them had made a single store under teardown hold storage for every account. Astra rejected (one P1, two P2), Opus accepted with fixes (one P1, one P2, two P3). The storage hold is now per account; two findings are left open as 19f and 19i. Those fixes are on the launch branch and unconfirmed until the next round reads them. Each round's fixes are on the launch branch, gated locally, and unconfirmed until the next round reads them. |
| +19. **No review of the whole branch has passed.** Astra rejected rounds one to sixteen; Opus accepted each with fixes. Round seventeen read only the diff since round sixteen: Opus accepted, Astra accepted with fixes (one P2, fixed since and unconfirmed). That is not a pass of the branch: the last whole-branch reading was round sixteen's, and it was rejected. Every round has closed the shapes the round before reported and found neighbouring ones, in the store walks since round seven. Some concern records no walk of this build writes; others need only this build's ordinary walks (a failed rotation, a retained teardown, a scanning door racing a teardown), and two were faults in the previous round's own fixes. Round eleven found one P1 and two P2 in the store walks (a grant could move an older record to another bucket name; a waiting rotation ignored a grant that ran; the user and its policy were written before the grant was counted). Its fixes are on the launch branch (`1395f7bf`). Round twelve read them: every round-eleven finding is closed for its reported shape, Astra rejected again (one P1, two P2) and Opus accepted with fixes (one P2, three P3). The new findings were about records no walk of this build writes (a teardown note for another bucket, a record naming two buckets) being acted on by teardown, rotation and the scanning doors; a rotation started after a grant that failed at its last write; and a take-back that returned access to a bucket another account holds, through the member's own door. Round thirteen read those fixes: closed for their reported shapes, Astra rejected again (two P1, four P2) and Opus accepted with fixes (one P2, five P3). The new findings were paths that read such a record and acted without the check (the holder inventory, the storage pass, the declare door), a rotation's own failed last write, and a retained record blocking another account's take-back. The check now sits in the record's reader and writer. Round fourteen read those: closed for their reported shapes; Astra rejected (two P1, two P2), Opus accepted with fixes (two P2, two P3). Round fifteen read those: all closed; Astra rejected (one P1, one P2), Opus accepted with fixes (one P2, two P3), all about readers of a bucket that a retained teardown left to another account (a sweep in flight, the scan status, the storage pass). Round sixteen read those: all closed, and one of them had made a single store under teardown hold storage for every account. Astra rejected (one P1, two P2), Opus accepted with fixes (one P1, one P2, two P3). The storage hold is now per account; two findings are left open as 19f and 19i. Those fixes are on the launch branch and unconfirmed until the next round reads them. Each round's fixes are on the launch branch, gated locally, and unconfirmed until the next round reads them. |
| 19g. **Refusals whose doors say the wrong thing, and records only a person can repair.** A record that names two buckets (`ErrTwoBuckets`) or whose teardown note is about another bucket (`ErrUnfinishedTeardown`) is refused by the record's one reader and one writer, so nothing acts on it and nothing writes it; the settings page shows such a store as unreadable. No walk of this build writes either; whether an earlier build left such records in production has not been checked. There is no operator verb to repair one. The doors that meet these refusals answer with sentences written for other failures: the grant and provision doors say to run the grant again, the destroy door and the purge say to try again, the scanning doors say to try again or to re-run the grant. The same goes for a rotation refused because a grant must be run again first (`ErrGrantUnfinished`) and for a member's take-back refused because another account holds the bucket (`ErrBucketHeld`). Correct copy for each needs approval (A). |
| 19i. **A live bucket shared by two stores is sized in full for each of them.** The daily storage pass lists the whole bucket for every store record that names it, so two accounts on one operator-adopted bucket would each carry all of its bytes. It predates this branch. Storage on a shared bucket must hold, not bill, until the pass uses the holder inventory and sizes only a sole holder. Not built. |
| 19h. **A rotation whose delivery fails is repaired by a grant, which swaps the app's key with no overlap.** A failed delivery may still land, so the record stays marked, the next rotation is refused, and the grant run again replaces whichever key the app holds. Hosts hold a dead key until they adopt the new config. Before this branch such a rotation was resumed by running it again, which kept the overlap when the delivery had not landed and broke the app when it had. A repair that waits out the signing window and then reads which key was delivered would keep the overlap where the window is enforced (observed on AWS only, 19d); it is not built. |
diff --git a/internal/console/economicspass.go b/internal/console/economicspass.go| index 33af140da..263e84e21 100644 |
| --- a/internal/console/economicspass.go |
| +++ b/internal/console/economicspass.go |
| @@ -198,7 +198,15 @@ func (s *Server) storagePass(ctx context.Context, date string) error { |
| for _, ks := range sizes { |
| st.BucketBytes += ks.Size |
| } |
| - rec, _, gerr := objstore.Get(ctx, s.cfg.Releases, acct, app) |
| + // A prefix that is not an account's app (a legacy unqualified |
| + // one, say) cannot have a store record: no build writes one |
| + // under an invalid name. That is "no store", not a store that |
| + // could not be measured. |
| + var rec *objstore.Record |
| + var gerr error |
| + if release.ValidAccount(acct) && release.ValidApp(app) { |
| + rec, _, gerr = objstore.Get(ctx, s.cfg.Releases, acct, app) |
| + } |
| if gerr != nil { |
| // Absent is (nil, "", nil) — objstore.Get's own idiom — so a |
| // non-nil error here is a REAL failure (transient bucket |
| @@ -224,13 +232,15 @@ func (s *Server) storagePass(ctx context.Context, date string) error { |
| if serr != nil { |
| log.Printf("console: storage pass: store %s/%s: %v", acct, app, serr) |
| st.StoreUnswept = true |
| - } else if after, _, aerr := objstore.Get(ctx, s.cfg.Releases, acct, app); aerr != nil || after == nil || |
| - after.Teardown != nil || after.Provisioned == nil || after.Provisioned.Bucket != rec.Provisioned.Bucket { |
| + } else if after, _, aerr := objstore.Get(ctx, s.cfg.Releases, acct, app); aerr != nil || |
| + after == nil || after.Teardown != nil || !objstore.SameStore(rec, after) { |
| // Sizing lists the whole bucket and takes time. If the |
| - // store's teardown began meanwhile (or the record can |
| - // no longer be read as the same store), what was |
| - // listed may be another account's objects: the size is |
| - // discarded and the day held for this account. |
| + // record read afterwards is not the same store (a |
| + // teardown began; the store was torn down and made |
| + // again, or taken back, under the same bucket name; |
| + // its key was rotated; it is gone or unreadable), what |
| + // was listed may be another account's objects: the |
| + // size is discarded and the day held for this account. |
| log.Printf("console: storage pass: store %s/%s changed while it was being sized; held", acct, app) |
| st.StoreUnswept = true |
| } else { |
diff --git a/internal/console/economicspass_test.go b/internal/console/economicspass_test.go| index 35ae63487..a55698b0a 100644 |
| --- a/internal/console/economicspass_test.go |
| +++ b/internal/console/economicspass_test.go |
| @@ -1660,3 +1660,57 @@ func TestStoragePassDiscardsASizeTakenAcrossATeardown(t *testing.T) { |
| t.Fatalf("day entry %+v; want the size discarded and the store unswept", app) |
| } |
| } |
| + |
| +// The store is torn down and made again under the same bucket name while it |
| +// is being sized (or its record is deleted): the record read afterwards |
| +// names the same bucket and carries no teardown, and is not the same store. |
| +// The size is discarded and the day held. |
| +func TestStoragePassDiscardsASizeTakenAcrossAReplacementOfTheStore(t *testing.T) { |
| + at := time.Date(2026, 8, 10, 9, 0, 0, 0, time.UTC) |
| + for name, during := range map[string]func(t *testing.T, fx *consoleFixture){ |
| + "made again under the same name": func(t *testing.T, fx *consoleFixture) { |
| + again := &objstore.Record{ |
| + Grant: &objstore.GrantBlock{Backend: "aws", Bucket: "carlos-store-x", Source: objstore.GrantSourceOperator, At: at.Add(time.Hour), By: "op"}, |
| + Provisioned: &objstore.Provisioned{Bucket: "carlos-store-x", Region: "eu-west-1", IAMUser: "u", KeyID: "K2", At: at.Add(time.Hour)}, |
| + } |
| + putRawStore(t, fx, fixtureAcct, fixtureApp, again) |
| + }, |
| + "record deleted": func(t *testing.T, fx *consoleFixture) { |
| + _, etag, err := fx.rel.Get(context.Background(), objstore.Key(fixtureAcct, fixtureApp)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := release.DeleteIfMatch(context.Background(), fx.rel, objstore.Key(fixtureAcct, fixtureApp), etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, |
| + } { |
| + fx := newConsoleFixture(t) |
| + ctx := context.Background() |
| + rec := &objstore.Record{ |
| + Grant: &objstore.GrantBlock{Backend: "aws", Bucket: "carlos-store-x", Source: objstore.GrantSourceOperator, At: at, By: "op"}, |
| + Provisioned: &objstore.Provisioned{Bucket: "carlos-store-x", Region: "eu-west-1", IAMUser: "u", KeyID: "K1", At: at}, |
| + } |
| + if _, err := objstore.Put(ctx, fx.rel, fixtureAcct, fixtureApp, rec, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + fx.srv.storeSizer = func(context.Context, *objstore.Provisioned) (int64, error) { |
| + during(t, fx) |
| + return 31 << 30, nil |
| + } |
| + if err := fx.srv.storagePass(ctx, "2026-08-12"); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, _, err := fx.rel.Get(ctx, usage.StorageDayKey("2026-08-12")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var day usage.StorageDay |
| + if err := json.Unmarshal(raw, &day); err != nil { |
| + t.Fatal(err) |
| + } |
| + if app := day.Apps[fixtureAcct+"/"+fixtureApp]; !app.StoreUnswept || app.StoreBytes != 0 { |
| + t.Errorf("%s: day entry %+v; want the size discarded and the store unswept", name, app) |
| + } |
| + } |
| +} |
diff --git a/internal/objstore/walk.go b/internal/objstore/walk.go| index 0624448e2..7866a968b 100644 |
| --- a/internal/objstore/walk.go |
| +++ b/internal/objstore/walk.go |
| @@ -1614,3 +1614,14 @@ func Coherent(rec *Record, acct, app string) error { |
| } |
| return nil |
| } |
| + |
| +// SameStore reports whether two reads of a store's record describe the same |
| +// store: the estate the walks compare (bucket, key, grant instant, the |
| +// count of grants that reached the provider, whether a teardown has begun, |
| +// what it noted, how many take-backs it has seen). A store torn down and |
| +// made again under the same bucket name is not the same store, and neither |
| +// is one taken back in between. For a reader that measures something slow |
| +// between two reads and must not record it against a store that changed. |
| +func SameStore(a, b *Record) bool { |
| + return a != nil && b != nil && estateOf(a) == estateOf(b) |
| +} |