Skip to content

Adopt the call-budget guard from #152, and fix the under-chunked read it caught - #166

Merged
PawSpaceIND merged 1 commit into
mainfrom
claude/pawspace-e2e-review-gg5i72
Aug 13, 2026
Merged

Adopt the call-budget guard from #152, and fix the under-chunked read it caught#166
PawSpaceIND merged 1 commit into
mainfrom
claude/pawspace-e2e-review-gg5i72

Conversation

@PawSpaceIND

Copy link
Copy Markdown
Owner

Another account's #152 fixed the same Customer 360 N+1 that #160 fixed here, so its code is superseded. Its test is not — porting it found a real defect in the merged fix.

300 customers 500 customers Verdict
chunk 50 — what #160 shipped 51 calls 82 calls fails both
chunk 80 — what #152 used 34 calls 58 calls passes
budget asserted by the test ≤ 50 ≤ 70

#160 removed the 8-queries-per-customer loop but sized the chunk a third too small, so main was making 82 D1 calls where 58 would do. ID_CHUNK is now 80, still inside D1's ~100 bound-parameter ceiling. Verified by reverting to 50 and watching the test fail with got 51 D1 calls.

Why the guard this repo already had would not have caught it

Eight narrow queries per customer are each perfectly legal. Only a count of calls catches a fan-out.

Guard Catches
bind cap one statement that is too wide
call budget many statements that are individually fine

#152's test also proves something #160 never did: it pins the preserved semantics field by field — default-address-first ordering, newest-first booking timeline, the per-customer LIMIT 50 on coupons, the food-order merge, consent defaults, duplicate detection. The refactor claimed byte-identical output; now there's evidence.

Shared harness

tests/helpers/d1-harness.mjs collects both guards, so suites stop each writing a more forgiving shim than the real database:

  • D1_MAX_BOUND_PARAMS, previously stranded in one suite
  • a call counter where batch() costs one subrequest, as real D1 charges it — otherwise a budget would punish batching, the exact thing we want code to do
  • assertWithinBudget asserts a floor as well as a ceiling, so a budget can't pass because the work silently did nothing

tests/unit-economics.test.mjs now imports the shared cap rather than defining its own.

Coordination

Verification

  • npm test1269/1269 pass (was 1265)
  • npm run lint — 23 errors / 81 warnings, unchanged from baseline
  • npx tsc --noEmit — clean
  • Guard confirmed to bite: fails at chunk 50, passes at 80

🤖 Generated with Claude Code

https://claude.ai/code/session_011Hon8zmG1pwVY6zwxtT5th


Generated by Claude Code

… it caught

Another account's PR #152 fixed the same Customer 360 N+1 that #160 fixed here, so
its code is superseded. Its TEST is not: porting it found a real defect in the
merged fix.

  chunk 50 (what #160 shipped)   300 customers: 51 calls   500: 82 calls   FAILS
  chunk 80 (what #152 used)      300 customers: 34 calls   500: 58 calls   passes
  budget asserted by the test    300 customers: <= 50      500: <= 70

#160 removed the 8-queries-per-customer loop but sized the chunk a third too
small, so main was making 82 D1 calls where 58 would do. ID_CHUNK is now 80,
still inside D1's ~100 bound-parameter ceiling. Verified by reverting to 50 and
watching the test fail with "got 51 D1 calls".

The bind-parameter cap this repo already enforced would never have caught either
defect. Eight narrow queries per customer are each perfectly legal; only a count
of calls catches a fan-out. The two guards catch different things:

  bind cap     -> ONE statement that is too wide
  call budget  -> MANY statements that are individually fine

That test also proves something #160 never did: it pins the preserved semantics
field by field - default-address-first ordering, newest-first booking timeline,
the per-customer LIMIT 50 on coupons, the food-order merge, consent defaults and
duplicate detection. The refactor claimed byte-identical output and now has the
evidence.

tests/helpers/d1-harness.mjs collects both guards so every suite models the same
database instead of each writing a more forgiving shim:

- D1_MAX_BOUND_PARAMS, previously stranded in one suite
- a call counter where batch() costs ONE subrequest as real D1 charges it -
  otherwise a budget would punish batching, the exact thing we want code to do
- assertWithinBudget asserts a FLOOR as well as a ceiling, so a budget cannot
  pass because the work silently did nothing

tests/unit-economics.test.mjs now imports the shared cap rather than defining it.

Credit where due: the framing is theirs - assert the property that would actually
break, not the shape of the code.

Tests 1265 -> 1269. Lint unchanged at 23 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Hon8zmG1pwVY6zwxtT5th
@PawSpaceIND
PawSpaceIND merged commit 565ca58 into main Aug 13, 2026
18 checks passed
PawSpaceIND pushed a commit that referenced this pull request Aug 13, 2026
main fixed two instances of this class while this branch was open - the
Customer 360 fan-out (#160, corrected to a chunk of 80 by #166) and the
per-booking reads in unit economics (#165, at a chunk of 50). Both declared
their own constant in their own module, which is how the same bug came back at
a different size in a different place, and neither touched the read that is
actually wrong on staging: /team/analytics still counts 331 bookings of GMV
next to Rs 0 collected.

Both now go through lib/d1-chunked-in, so there is one helper and one size (80,
the number #166 measured: a chunk of 50 cost 82 D1 calls for 500 customers
where 58 would do). A test fails if any module declares its own chunk constant
again.

tests/crm-stack-hardening.test.mjs loads lib/customer-360.ts directly without
the extensionless-import fallback the other real-execution suites install, so
it broke the moment that module imported a sibling. It installs the same hook
now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CE4fKzrWRZetcYKh8CaU3b
PawSpaceIND added a commit that referenced this pull request Aug 13, 2026
…158)

/team/analytics reported 331 bookings and GMV of Rs 3,24,472 next to "Collected Rs 0" on every
service line, because the payments read for those bookings asked D1 for 331 bound parameters against
a cap near 100, and the failure was swallowed into a confident zero. Nine IN-list reads over unbounded
sets now go through one shared helper, lib/d1-chunked-in.ts, at the chunk size #166 measured.

Also, from review: the chunking is not free. A table-existence guard inside a chunked read is charged
once per chunk, which cost 1,012 D1 subrequests over 5,000 bookings against a Worker limit near 1,000
- the fix would have re-broken the same screen one order of magnitude further up. That guard is now
memoised per request. An "ORDER BY ... LIMIT 200" inside chunking applies per chunk, so the payroll
and incentive approval trails showed the wrong 200 events in block order; both are reapplied to the
whole answer. tests/chunked-in-semantics.test.mjs pins all three properties.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants