Repository navigation
fix(plans): allocate marked ramps by exact step count - #907
Conversation
|
Warning Review limit reached
This review includes 7 billable files and costs up to $1.75.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 22 minutes for your next included review. View limit details
Comment |
|
@coderabbitai review |
|
7d9c807 to
03144c3
Compare
cristim
left a comment
There was a problem hiding this comment.
Independent review of 03144c3: no actionable findings
Base used: 36d385e, which is the current head of #906 (fix/631-m2-scheduling). Diff reviewed: 36d385eb...03144c39 (345 additions, 35 deletions, 7 files). Stack is consistent.
Findings (all NON-BLOCKING)
- Nit,
internal/planstep/allocation_test.go: thestep > TotalStepsclamp inequalCountShare(min(step, r.count),allocation.go:38) has no test. A mutation probe replacing it withsteppasses the whole planstep suite. Production code is correct (the clamp keeps the target at 100% of base if TotalSteps is lowered mid-ramp). Suggest one resolver case with step 5, count 3, expecting target-met or no over-buy. - Nit, marked Savings Plans do not telescope: each step is sized as floor-to-cent(hourly x coverage / (100 x count)) from the share at step 1 (
resolve.goequalCountShare(coverage, 1)), so overcountmanual steps the cents lost to each floor add up (hourly 1.234, count 3: 3 x 0.41 = 1.23 vs 1.234; the loss is bounded by about one cent per step). This matches the plan's stated contract ("manual count3/97 exact cent floor") and automatic multi-step SP stays refused, so it is not a defect. It is worth saying in the PR body that "exact" applies to counts, and that SP shares are exact per step with a cent floor.
What I attacked
- Count arithmetic: target = floor(base x coverage x min(step,count) / (100 x count)) in
big.Rat(allocation.go:35-39,resolve.gotarget). The cumulative floor telescopes, so the last step equals floor(base x coverage/100) exactly. Verified by test: base 97, count 3 gives deltas [32,32,33]; count 97 gives 1 per step; base 100, percent 33 legacy still gives 99 (unchanged). - Decimals:
decimalRatioparses the shortest round-trip text, so 33.3 is exactly 333/10, not the float expansion. NaN, Inf and negative are rejected. Exponent forms (1e20) parse.floorAllocationrejects values beyond int range. - Zero or one step:
ValidateAllocationModerequires 1..MaxTotalSteps, and immediate requires exactly 1.ResolvecallsPrecheck(resolve.go:95) before any use, sobig.NewRat(.., 100*count)cannot see count 0 (no division panic). Step < 1 is rejected already. Immediate with count 1 and stored percent 33 or 0 buys the full 100%. - Mid-ramp count change: baseline is frozen and the own-ledger committed count is subtracted, so lowering or raising TotalSteps after steps ran can only produce gap <= 0 (no buy) or a smaller catch-up, never a double buy. Partly executed ramp: the test with committed 17 and shown 2 clamps to shown; committed 32 at step 2 gives ErrTargetMet. Negative baseline, shown or committed counts error out in marked mode.
- Legacy path: empty mode keeps
math.Floor(base*(coverage*cum)/10000)and the oldvalidateInputsbounds; immediate PercentPerStep != 100 is still refused when mode is empty. - SP representation:
exactSPHourlyfloors exact cents and checks both the shortest-JSON text and%.2fequal those cents, else errors (no silent wrap). - Tests at this SHA:
go build ./...ok;go test -race ./internal/planstep/... ./internal/config/...872 passed;go test -tags integration ./internal/purchase -run TestEqualCountAttachpassed against native Postgres (24.8s). - Fail-before: copied
allocation_test.goonto the parent tip; TestEqualCountResolve, TestEqualCountSavingsPlan (all 6 cases), TestEqualCountImmediateOmittedPercent, TestEqualCountRejectsBeforeReads and TestEqualCountInvalidQuantities all fail there. - Mutation probes (4, each reverted): float-cumulative target instead of rational (killed by 3 tests); ceil instead of floor (killed); SP share at step=count instead of 1 (killed); remove
min(step,count)clamp (SURVIVED, finding 1). - Not tested by me: the 97-step native attach covers only the first step (the 97 steps all run through the resolver unit test); provider completion is simulated, as the PR states.
Gate status (origin/main Review gate)
- Independent review: this comment, at the SHA above.
- Findings resolved: no actionable findings; two nits above.
- CI on SHA: not applicable until retarget (base is a child branch, not main);
statusCheckRolluphas 1 entry only. - CodeRabbit: not applicable until retarget; no review threads exist.
- Local real-path evidence: PR body states fixture-backed Resolve plus native Postgres, provider completion simulated, not a real-account scenario. I reproduced the planstep and native attach runs above.
- Labels: match #631 (effort/xl, impact/many, priority/p1, severity/high, triaged, type/feat, urgency/this-sprint). The PR has no closing issue (Refs #631).
- Size: 345 additions, under the 400 cap for additions;
resolve.goandallocation.gostay under 500 lines.
Size marked count targets and Savings Plans shares from exact ratios. Preserve legacy percent sizing and reject cent loss at serialization.
03144c3 to
cf12d41
Compare
|
Rebased onto main after #906 merged (squash 4e1fb1b); retargeted to main. Pure rebase, Empty range-diff, so the independent verdict on 03144c3 carries to cf12d41 (owner #631 rule). Gate still needs CI green on cf12d41. |
|
Gate record for cf12d41 (origin/main CLAUDE.md "Review gate"):
Merging at cf12d41 with --match-head-commit. #908 will be retargeted to main and rebased next. |
Delta of 686b9f02 over #907: the planstep and config code is identical on main. With the retention runtime (#905) a completed protected ramp has an untrusted baseline, so the exact-count test must assert the refusal instead of reading the baseline; without this TestEqualCountAttachThree fails on main plus #905. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(purchase): refuse unverified ramp sizing and adoption Mint fresh ramp lineage at creation and freeze it atomically. Preserve authorized quantities while refusing new ramp sizing or notification adoption from unverifiable histories. * test(plans): adapt exact-count allocation proof to the retention ledger Delta of 686b9f02 over #907: the planstep and config code is identical on main. With the retention runtime (#905) a completed protected ramp has an untrusted baseline, so the exact-count test must assert the refusal instead of reading the baseline; without this TestEqualCountAttachThree fails on main plus #905. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(purchase): fail a refused ramp step durably and release its suppressions When the retention ledger refuses to size a step whose root execution row already carries recommendations, fail the row like the other refusals (Error and NoSubmission recorded, later ticks skip quietly) instead of returning the raw sentinel, which logged an error every tick. The pinned failure now expects the row's real recommendation state, and a won failure releases the row's suppressions in the same transaction as the stamp, since the step provably bought nothing. The CAS still commits before the stamp; that pre-existing gap is recorded in known_issues/36 and #1052. Refs #631 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plans): let never-started legacy ramps size again Migration 000127 marks a protected multi-step ramp 'new' again when its retention ledger is 'legacy' but it is still at step 0, has no frozen baseline and has no surviving purchase execution rows. Such plans then freeze a baseline at their first step like any new plan. The ledger guard trigger is disabled only around that update, inside the migration transaction. Mid-ramp legacy plans are left for a follow-up change. The untrusted-ledger refusal now says what is wrong instead of naming a reconcile action that does not exist. Refs #631 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
What
Allocate marked (managed) ramps by exact step count: size count targets and Savings Plans shares from exact ratios, keep legacy percent sizing for unmarked ramps, and reject cent loss at serialization.
Why
M2 managed-ramp chain for #631: float cumulative percent sizing drifts for uneven step counts (e.g. 3 steps of a base total), so marked ramps need exact rational allocation.
Local verification
go build ./...andgo test -race ./internal/config/... ./internal/purchase/...on this tip (macOS).Resolvewith fake recommendation reads and native PostgreSQL; provider completion simulated. Parent-fail and deletion probes were run in scratch copies. Not a real-account scenario.Stack (M2 chain, merge in order)
Refs #631
🤖 Generated with Claude Code