carlos / platform Public

pricing: plan two round-two fixes

Eight of seventeen round-one findings were fully addressed by the
rewrite; round two found nine partial and eight new problems. All are
now fixed.

The worst was mine: moving the logging call before the healthy
short-circuit also moved it before bucket creation, so first grants
would configure logging on a bucket that does not exist. It now sits in
both paths, and the fake rejects logging on an uncreated bucket so a
test catches the ordering rather than production.

Log retention becomes 45 days, derived rather than chosen: the pass
rebuilds a day until its month closes on day 3 of the next one, so a
1 August log must survive to 4 September. A 30-day expiry would have
let a rebuild silently replace a correct total with a smaller one.

Internal traffic is no longer classified on PLATFORM_EDGE_IPS, which is
public DNS guidance, may be empty, and never sees the private addresses
that gateway endpoints present to S3. Classification is against the VPC
CIDR plus edge addresses with an explicit unclassified category that is
never billed. Requester identity does not work either: a presigned URL
is fetched by an outsider using the signer authorization.

Also: the log bucket was writable by delivery and unreadable by the
collector; billableSum was called but never defined; the durability
term had to replace the request term rather than the request term
simply being deleted; the census key dropped the fleet prefix so two
fleets would collide; a missing request id became a shared dedup
identity; and the production reader was to be installed from a package
that cannot see an unexported field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev a5170bca260f8f849c7bca9122eb16a343076ad9 parent 89a24fe
2 files changed, +170 −19
  • docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md +77 −19
  • docs/superpowers/plans/CODEX-FINDINGS-store-egress-meters-round2.md +93 −0
diff --git a/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md b/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md
index 07d996650..b85fba70f 100644
--- a/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md
+++ b/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md
@@ -438,7 +438,13 @@ resource "aws_s3_bucket_lifecycle_configuration" "logs" {
id = "expire-raw-logs"
status = "Enabled"
filter {}
- expiration { days = 30 }
+ # 45 days is DERIVED, not chosen. The pass rebuilds a day's totals from
+ # the surviving log objects until that month's statement closes, and
+ # plan one closes a month on day 3 of the next one — so a 1 August log
+ # must still exist on 4 September, 34 days later. An expiry inside that
+ # window would let a rebuild silently REPLACE a correct total with a
+ # smaller one. Any change to the closure grace must move this too.
+ expiration { days = 45 }
}
}
@@ -473,9 +479,17 @@ resource "aws_s3_bucket_logging" "data" {
}
```
-Add `log_bucket_name` and `account_id` variables and a `log_bucket` output. The operator's console role also needs `s3:PutBucketLogging` on tenant buckets — add it wherever the store-provisioning grant lives, and check first:
+Add `log_bucket_name` and `account_id` variables and a `log_bucket` output.
-Run: `grep -rn 's3:CreateBucket\|s3:PutBucketPolicy' infra/ | head`
+**Three separate permissions, and only one of them is in the policy above.** Delivery permission is not read permission:
+
+1. **Log delivery** writes to the bucket. That is the policy above. Tighten its scope beyond `aws:SourceAccount`: add an `aws:SourceArn` condition naming the source buckets, and restrict `resources` to the two prefixes actually used rather than `/*`.
+2. **The console** must *read* the logs. The host policy grants object reads and listing on the deployment and data buckets only (`infra/modules/region-host/main.tf:121`), so the collector gets `AccessDenied` as written. Add the log bucket ARN there with `s3:ListBucket` on the bucket and `s3:GetObject` on `storelogs/*` and `datalogs/*`.
+3. **The console** must call `s3:PutBucketLogging` on tenant buckets, which nothing grants today.
+
+Find where the store-provisioning grant lives before adding to it:
+
+Run: `grep -rn 's3:CreateBucket\|s3:PutBucketPolicy\|s3:GetObject' infra/modules/region-host/main.tf | head`
**This is a manual apply and not part of the Go gate.** Until it is applied, every day reads as `Unswept`, which is the correct reading rather than a silent zero.
@@ -597,17 +611,31 @@ func (s *stubS3) PutBucketLogging(_ context.Context, in *awss3.PutBucketLoggingI
Add `LogBucket string` to `Deployment` (walk.go:53-58), documented as "the access-log destination; empty disables logging". Populate it in `storeBackend` (`cmd/console/main.go:535`) from `CARLOS_LOG_BUCKET` — **the pass's own config value is a different variable and setting only that leaves every provisioning call with logging disabled.**
-In `Grant`, put the call **immediately after `EnsureUser` and before the healthy short-circuit at walk.go:201**, with the prefix helper beside `MintedBucket`:
+**`Grant` needs the call in two places, not one.** The healthy path returns at `walk.go:201` without reaching `EnsureBucket` at `:205`, so a call placed only after creation never reaches an existing store — but a call placed only before creation configures logging on a bucket that does not exist yet, and the **first** grant fails. Both, guarded:
```go
- // Before the healthy short-circuit, deliberately: an existing store is
- // exactly the case that needs logging turned on, and the short-circuit
- // returns at :201 without ever reaching EnsureBucket.
+// logStore is the one place the logging call is written; Grant reaches it
+// twice, because the two paths arrive with the bucket in different states.
+func logStore(ctx context.Context, p Provisioner, d Deployment, bucket, acct, app string) error {
if err := p.EnsureBucketLogging(ctx, bucket, d.LogBucket, StoreLogPrefix(acct, app)); err != nil {
- return nil, err
+ return fmt.Errorf("objstore: enable logging on %s: %w", bucket, err)
}
+ return nil
+}
```
+Call it **inside the healthy short-circuit, just before `return rec, nil` at `:201`** — the bucket demonstrably exists on that path — and again **immediately after `EnsureBucket` succeeds at `:205`**, for the provisioning path.
+
+Make `MemProvisioner.EnsureBucketLogging` **reject a bucket it has never created**, so the ordering error fails a test rather than only production:
+
+```go
+ if p.Buckets[name] == nil {
+ return fmt.Errorf("objstore: mem: no such bucket %q", name)
+ }
+```
+
+and add a test that a **first** grant succeeds and configures logging, not only that a healthy re-grant does.
+
```go
// StoreLogPrefix is where a tenant bucket's access logs land. One prefix per
// app, so the day pass reads exactly one tenant's logs.
@@ -665,8 +693,14 @@ Four rules this pass follows, each answering a way the log stream lies:
1. **The day comes from the record, not the object name.** Log objects are named by delivery time. A record whose `Time` is not `date` is ignored by this run and picked up by the run for its own day.
2. **Deduplicate on request id.** AWS may deliver a record more than once. The pass holds a set per day; if it exceeds a cap the day is `Unswept` rather than possibly-double-counted.
-3. **Internal fetches are not egress.** A GET whose `RemoteIP` is one of the platform's own addresses is an app pulling its own object to serve through the edge, where the edge meter already counts the external leg. Same-region S3-to-EC2 transfer is free. Counted separately as `BytesInternal`, priced at zero.
-4. **An empty read is not a measured zero.** Absence of logs is absence of evidence.
+3. **Internal fetches are not egress, and "internal" is a network fact, not a guess.** An app pulling its own object to serve through the edge is free in-region transfer that the edge meter already counts on its external leg. **`PLATFORM_EDGE_IPS` cannot make this call**: it is public DNS guidance, it may legally be empty (`cmd/console/main.go:684`), the console already documents that it can omit live edges (`internal/console/server.go:771`), and traffic through a VPC gateway endpoint reaches S3 from a *private* address that will never appear in it. Classify against the **VPC CIDR** (the relay module already outputs it) plus the edge public addresses, and keep a third category:
+ - `BytesInternal` — matched a known internal range. Priced at zero.
+ - `BytesOut` — matched a known external address. Billed.
+ - **`BytesUnclassified`** — matched neither. Never billed, always surfaced, and if it exceeds a small fraction of the day the app is `Unswept`. An address we cannot place is not permission to invent a number.
+
+ Note what does *not* work: classifying on the requester's IAM identity. A presigned URL is fetched by an external downloader using the signer's authorization, so "the requester is the app's own user" is true of exactly the downloads that should be billed.
+4. **An empty read is not a measured zero.** Absence of logs is absence of evidence. A tenant with logging configured but no log object yet is indistinguishable from one that served nothing, so both are `Unswept`.
+5. **A record with no request id cannot be deduplicated.** AWS writes `-` for unavailable fields, and treating that as an identity collapses every such record into one. Count them, never dedup them, and mark the day `Unswept` if they exceed a small fraction of it.
- [ ] **Step 1: Day shape and meter**
@@ -745,7 +779,7 @@ The seam is a *lister plus reader*, because a day's logs are many objects:
type logObjects func(ctx context.Context, bucket, prefix string) ([][]byte, error)
```
-Add a `logObjects` field to `Server`, defaulting in `cmd/console/main.go` to an implementation that paginates `ListObjectsV2` over `bucket/prefix` and `GetObject`s each key, with a bounded total size. `objstore.S3ClientFor(ctx, region, endpoint)` (aws.go:95) builds the client; `sizeProvisionedStore` (`economicspass.go:992-1017`) is the paginating precedent to copy.
+Add a `logObjects` field to `Server`. **It is unexported, and `cmd/console/main.go` is a different package, so it cannot assign it** — the production default is installed *inside* `internal/console`, exactly as `sizeStore` does (`economicspass.go:992`): the field is nil by default and the pass falls back to a package-level real implementation that paginates `ListObjectsV2` over `bucket/prefix` and `GetObject`s each key, with a bounded total size. Only the config values (`LogBucket`, region, endpoint) come from `main.go`. `objstore.S3ClientFor(ctx, region, endpoint)` (aws.go:95) builds the client; `sizeProvisionedStore` (`economicspass.go:992-1017`) is the paginating precedent to copy.
**Discover tenants from the log prefixes, not from live records** — a store torn down yesterday has its `store.json` deleted (`walk.go:585`) but its logs still exist, and the pass must still bill the day it served. List `storelogs/` and walk down two levels for account and app, mirroring `storagePass`'s `ListDirs` idiom (`economicspass.go:148-163`).
@@ -823,16 +857,20 @@ The fleet prefix must come from configuration, since nothing in the console know
- [ ] **Step 2: Survive a deleted route**
-Reuse plan one's census, which already records every name a route answers to. In `censusPass`'s route loop:
+Reuse plan one's census, which already records every name a route answers to. **Record the full replica key, prefix included** — an unprefixed path would make two fleets replicating `/data/app.db` share one census entry, which is exactly the collision the two-fleet test below exists to catch, and `LitestreamConfig` keeps both components in the URL for the same reason (`internal/ops/litestream.go:140-141`). In `censusPass`'s route loop:
```go
- if rt.DBPath != "" {
- units[strings.TrimPrefix(filepath.Clean(rt.DBPath), "/")] =
- usage.CensusUnit{Account: acct, App: rt.App}
+ if rt.DBPath != "" && fleetPrefix != "" {
+ key := path.Join(fleetPrefix, strings.TrimPrefix(filepath.Clean(rt.DBPath), "/"))
+ units[key] = usage.CensusUnit{Account: acct, App: rt.App}
}
```
-The census key is the **unprefixed** path, so the pass strips the fleet prefix from a log key before consulting it, then falls back to `replicaOwner` on the live registry — the same asymmetry plan one established: a deleted route is in the census, a newly created one is in the registry.
+`censusPass` therefore needs the fleet prefix too, from the same configuration `durabilityPass` reads. The pass looks up the **whole** log key, walking its prefixes down to the recorded entry, then falls back to `replicaOwner` on the live registry — the same asymmetry plan one established: a deleted route is in the census, a newly created one is in the registry.
+
+**If a deployment ever runs more than one fleet prefix, a single `CARLOS_LITESTREAM_PREFIX` is not enough** and the config becomes a list. Check whether that is already true before building for one:
+
+Run: `grep -rn 'litestream' --include=*.sh --include=*.tf . | grep -i prefix | head`
- [ ] **Step 3: The pass**
@@ -893,13 +931,33 @@ Seed it in `DefaultScenario` at the same S3 PUT anchor `RequestsPer1kUSD` uses t
`MeterRequests1k` is **gone** from both the cost and the `billableCost` terms.
-**Charge the shared allowance once.** Store egress and edge egress share one inclusion, so:
+**Charge the shared allowance once.** Store egress and edge egress share one inclusion. The existing `billable` helper (`compute.go:85`) takes a single meter, so define its sibling rather than calling a function that does not exist:
+
+```go
+// billableSum is billable() over the combined value of two meters that
+// share one allowance — edge and store egress are one inclusion, so
+// charging each against the full allowance would give away twice as much.
+func billableSum(meters map[string]float64, a, b string, allowance float64) float64 {
+ total := meters[a] + meters[b]
+ if total <= allowance {
+ return 0
+ }
+ return total - allowance
+}
+```
+
+Worked case to assert: 0.75 GB edge plus 0.75 GB store against a 1 GB allowance is **0.5 GB billable**, not 0.
+
+Use that single `bEgress` in `billableCost` — do **not** add store egress again alongside it. In the same term, **replace** the request-cost line rather than deleting it:
```go
- bEgress := billableSum(meters, usage.MeterEgressGB, usage.MeterStoreEgressGB, ft.EgressGB)
+ billableCost := allocatedUSD*computeFrac +
+ bEgress*rc.EgressGBUSD +
+ meters[usage.MeterDurabilityWrites1k]*rc.DurabilityPer1kUSD +
+ bStorage*rc.StorageGBMonthUSD
```
-and use that single `bEgress` in `billableCost` — do **not** add store egress again alongside it. Update the `clipped` check to compare against the combined total, not `egress_gb` alone.
+Deleting `meters[usage.MeterRequests1k]*rc.RequestsPer1kUSD` at `compute.go:125` without putting durability in its place drops replication out of cost-plus revenue entirely. Update the `clipped` check to compare against the combined egress total, not `egress_gb` alone.
- [ ] **Step 3: Both JavaScript calculations, same commit**
diff --git a/docs/superpowers/plans/CODEX-FINDINGS-store-egress-meters-round2.md b/docs/superpowers/plans/CODEX-FINDINGS-store-egress-meters-round2.md
new file mode 100644
index 000000000..0f51f3ebd
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-FINDINGS-store-egress-meters-round2.md
@@ -0,0 +1,93 @@
+## 1. Disposition table
+
+**Not executable as written at `89a24fea`.** The rewrite corrects substantial parts of round one, including the parser’s offsets and Go syntax, but introduces provisioning and collection failures.
+
+References to “plan” below mean [the rewritten plan](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md).
+
+| # | Round-one finding | Disposition | Assessment |
+|---|---|---|---|
+| 1 | Parser offsets | **ADDRESSED** | Plan:218–228 now uses the correct positions, and the fixture distinguishes bytes sent from object size. |
+| 2 | Invented Go interfaces/signatures | **ADDRESSED** | Task 2 now matches `awsProvisioner`, `MemProvisioner`, `s3API`, `stubS3`, and `Grant`; the removed box task needs no further disposition. |
+| 3 | Missing production reader/configuration | **PARTIAL** | Plan:598 and :748 prescribe both pieces, but initialization crosses an unexported field boundary and the reader’s permissions remain missing. |
+| 4 | Self-logging feedback loop | **ADDRESSED** | Task 2 creates a separate destination without enabling logging on it. |
+| 5 | Missing delivery permission | **PARTIAL** | The service policy permits delivery, but its account-wide source scope and unrestricted destination prefixes omit the requested source/prefix isolation. |
+| 6 | Missing fleet prefix | **PARTIAL** | Live lookup includes the prefix, but the rewritten census discards it and cannot preserve distinct ownership for identical paths across fleets. |
+| 7 | Event day and late arrivals | **PARTIAL** | Timestamp filtering and replacement are prescribed, but no task wires a catch-up date loop into `economicsPass`, which currently calls neither new pass at [economicspass.go:58](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/console/economicspass.go:58). |
+| 8 | Partial logs presented as complete | **PARTIAL** | Plan:762 still advances completeness after a nonempty, parseable sample, and prefix-only discovery cannot identify stores whose logging has never produced an object. |
+| 9 | Duplicate delivery | **PARTIAL** | Rebuilding a day with a request-ID set handles observed duplicates, but missing request IDs have no refusal rule. |
+| 10 | Unsafe box-side retry | **ADDRESSED** | Explicitly deferred with the replay and date-loss explanation and analytics precedent; no objection to that disposition. |
+| 11 | Internal traffic charged twice | **PARTIAL** | Separate internal totals are prescribed, but the chosen address list does not establish the transfer classification. |
+| 12 | Healthy stores never acquire logging | **PARTIAL** | Healthy grants now reach logging, but moving the call unconditionally ahead of bucket creation breaks first grants. |
+| 13 | Unescaped interior quotes | **ADDRESSED** | Candidate scanning replaces first-quote parsing; the supplied interior-quote fixture succeeds and the unterminated fixture is rejected. |
+| 14 | Deleted stores disappear from discovery | **ADDRESSED** | Discovery uses retained account/app log prefixes instead of surviving `store.json` records. |
+| 15 | Pricing consumers and allowance | **PARTIAL** | The combined allowance and three consumers are named, but `billableSum` is undefined and the replacement durability term in `billableCost` is not explicitly supplied. |
+| 16 | Integer truncation/proration | **ADDRESSED** | Both folds convert before division and explicitly forbid a days-in-month divisor. |
+| 17 | Versioning defeats expiration | **ADDRESSED** | The separate, unversioned bucket makes expiration delete raw objects; its interaction with recomputation is a new problem below. |
+
+## 2. New findings
+
+The exact parser block passes `gofmt`’s syntax parser. `for off := 0; ; {` is valid Go: every examined quote advances `off`, including before `continue`, and reaching the final quote leaves a valid empty slice for the terminating search. Tracing the supplied fixtures yields one valid candidate for both GET variants and none for the unterminated URI. `splitPrefix` correctly removes the timestamp brackets while retaining the timezone. The HCL block also passes `tofu fmt -check -`. These were syntax checks and fixture tracing, not a Go test run or Terraform deployment validation.
+
+1. **[P1] First grants fail because logging precedes bucket creation.**
+
+ [Plan:606](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:606) calls `PutBucketLogging` immediately after `EnsureUser`. The actual bucket creation occurs later, at [walk.go:205](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/objstore/walk.go:205). With logging configured, a newly minted bucket therefore receives a logging request before it exists, and the grant returns an error before reaching creation.
+
+ Configure logging before returning on the healthy path, and after `EnsureBucket` on the provisioning path. Make the fake reject logging requests for nonexistent buckets so the test catches this ordering error.
+
+2. **[P1] The new bucket is writable by log delivery but unreadable by the prescribed collector.**
+
+ [Plan:476](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:476) adds only the operator’s `PutBucketLogging` permission. The existing host policy grants object reads and bucket listing only on the deployment and data buckets at [region-host/main.tf:121](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/infra/modules/region-host/main.tf:121). Neither includes the new log bucket.
+
+ The production reader consequently gets `AccessDenied` under that role. Wire the log bucket ARN into the consuming infrastructure and grant the collector `s3:ListBucket` plus `s3:GetObject` on its log prefixes. Delivery permission does not grant reader permission.
+
+3. **[P1] Thirty-day expiration can erase inputs while their month is still being recomputed.**
+
+ [Plan:441](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:441) expires raw objects after 30 days; [plan:756](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:756) replaces daily totals from the surviving objects until statement closure. Plan one closes August only after September 3, using its [period calculation at :934](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-08-invoice-grade-statements.md:934).
+
+ Early-August logs can therefore disappear before the final August sweep. A rebuild can replace previously observed usage with a smaller total; some surviving late-delivered records can even leave that smaller total apparently swept. Unversioned expiration really deletes these inputs. [AWS expiration behavior](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-expire-general-considerations.html).
+
+ Retain inputs through the reconciliation window, or persist replay-safe ingestion state before deleting them. A daily sum alone cannot support both deletion and reconstruction.
+
+4. **[P1] `PLATFORM_EDGE_IPS` does not identify all internal S3 traffic.**
+
+ [Plan:752](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:752) treats membership in that set as the classification boundary. Its actual purpose is public DNS guidance, and it may legally be empty: [main.go:684](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/cmd/console/main.go:684). The console also explicitly recognizes that this configured list can omit live edges at [server.go:771](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/console/server.go:771).
+
+ S3 sees private source addresses through gateway endpoints, so a same-region app fetch can miss the public-IP set and be charged again alongside edge delivery. NAT introduces another address mismatch. [AWS gateway endpoint behavior](https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html).
+
+ **Presigned URLs do not make remote-IP classification intrinsically wrong:** the external downloader makes the request using the signer’s authorization. Replacing the test with “requester is the app’s IAM user” would wrongly exempt those downloads. [AWS presigned URL behavior](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html). Define verified network/region classification, including an unknown category, rather than equating public edge addresses with all free transfer.
+
+5. **[P1] The prescribed production-reader initialization cannot access its field.**
+
+ [Plan:748](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:748) says to add lowercase `logObjects` to `console.Server` and initialize it from `cmd/console/main.go`. That caller is outside package `console`; it constructs the server through [main.go:265](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/cmd/console/main.go:265). It cannot assign an unexported field.
+
+ Initialize the production fallback inside `internal/console`, following [sizeStore at economicspass.go:992](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/console/economicspass.go:992), or provide an exported configuration/constructor hook.
+
+6. **[P1] `billableSum` is an undefined function.**
+
+ The call at [plan:899](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:899) has no definition in the repository or either plan. The existing helper at [compute.go:85](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/economics/compute.go:85) accepts one meter.
+
+ Define the helper or inline `max(0, edgeGB + storeGB - allowance)`. The intended arithmetic is correct: 0.75 GB edge plus 0.75 GB store against a 1 GB allowance leaves **0.5 GB billable**. Also explicitly replace the request-cost term at [compute.go:125](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/economics/compute.go:125) with the durability-cost term; merely deleting it leaves durability out of cost-plus revenue.
+
+7. **[P1] The new census representation erases the fleet distinction its test requires.**
+
+ [Plan:830](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:830) stores ownership under an unprefixed database path, then :835 strips the fleet prefix before lookup. Two owners of `/data/app.db` under different fleet prefixes therefore have the same census key.
+
+ The real generator preserves both components in the replica URL at [litestream.go:140](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/internal/ops/litestream.go:140). A single `CARLOS_LITESTREAM_PREFIX` plus an unprefixed ownership map cannot satisfy the two-fleet test at plan:845. Preserve bucket/fleet/path ownership in the historical mapping and validate that scope before lookup.
+
+8. **[P2] An unavailable request ID becomes a shared deduplication identity.**
+
+ [Plan:332](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:332) accepts `RequestID: "-"`; the pass then deduplicates on that string. AWS permits unavailable fields to contain `-`. Distinct otherwise parseable records with that value collapse into one request. [AWS log format](https://docs.aws.amazon.com/AmazonS3/latest/userguide/LogFormat.html).
+
+ Require a usable identity for counted records and mark the affected day uncertain otherwise. Conversely, the warning at plan:933 about duplicates delivered on different days is misleading: rebuilding the **event day** from all retained objects deduplicates both copies together. Delivery midnight does not itself require persisted IDs.
+
+9. **[P2] Terraform does not order logging enablement after its delivery policy.**
+
+ [Plan:469](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:469) references the destination bucket but not `aws_s3_bucket_policy.logs`. Terraform may create the policy and enable logging concurrently.
+
+ Add `depends_on = [aws_s3_bucket_policy.logs]`. The HCL is syntactically valid, and the policy is sufficient for basic delivery once installed; its missing source/prefix restrictions are the remaining round-one scope issue. [AWS delivery permissions](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html).
+
+10. **[P2] Empty `LogBucket` does not disable existing logging as documented.**
+
+ The AWS implementation at [plan:540](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/pricing-plans-spec/docs/superpowers/plans/2026-09-10-store-egress-and-durability-meters.md:540) and fake at :571 simply return. Existing logging configuration remains active.
+
+ Either describe empty configuration as “leave logging unmanaged,” or implement disabling with an empty `BucketLoggingStatus` and clear the fake’s recorded state. [AWS disable operation](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLogging.html).
\ No newline at end of file