Skip to content

fix(ladder): floor tranche amounts to the stored scale instead of rounding them up - #1199

Merged
IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-1182
Oct 25, 2026
Merged

IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-1182

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Root cause

Item 1 of #1182: tranche amounts are float64 stored in ladder_tranches.amount_usd_hr NUMERIC(20,6). I checked them against the go precision contract at the version go.mod pins (pkg/ladder/capability.go PurchaseLayer doc from go#438, and providers/aws/internal/spcommitment.Normalize from go#403). That contract says to floor the commitment, never round it up, refuse anything outside [0.001, 1000000] USD/hr, and truncate the shortest decimal to 5 places.

Any amount whose decimal fits in 6 places already round-tripped exactly. pgx sends the float's shortest decimal and reads it back with ParseFloat. That covers every 5-decimal contract value, the range ends, and values float64 can't represent exactly such as 0.1, 0.29 and 999999.99999. Two seams could still round a planned amount up:

  1. buildTrancheDBRows converted the exact planned *big.Rat to the nearest float64. The contract says "callers converting a *big.Rat amount to float64 must not round it up". For example, 0.12345999999999999 became 0.12346.

  2. On insert, Postgres rounded the float's decimal half-up to 6 places:

    • 0.0009995 was stored as 0.001000. The plan is below the 0.001 minimum, but the stored amount would be accepted.
    • 999999.9999999 was stored as 1000000.000000, so the submitted amount becomes 1000000 instead of 999999.99999.
    • 0.1234599 was stored as 0.123460, so the submitted amount becomes 0.12346 instead of 0.12345.

    Planner amounts are routinely finer than 6 places (1/3, ramp halvings), so this was a real path, not just a theoretical one.

Fix (no migration)

  • config.LadderAmountFloat floors the exact planned amount to the column's 6 places. Flooring at 6 and then truncating at 5 gives the same result as truncating the exact amount at 5. An amount below a minimum also stays below it. buildTrancheDBRows now uses it.
  • A positive planned amount below 0.000001 USD/hr floors to 0. buildTrancheDBRows drops that tranche with a log line instead of writing a $0 scheduled row; such an amount is far below the provider's 0.001 minimum and could never be bought, and refusing it would fail the same run on every rerun. A zero or negative planned amount (which BuildTranches never emits) is refused, failing the run.
  • The display plan (immediateTrancheAmounts) sums the same floored amounts, so plan_json never shows more than the persisted rows.
  • insertLadderTranchesTx writes an exact decimal ($5::numeric) through ladderAmountText. That helper refuses a non-finite amount, or one with more than 6 decimals that the column would round, so every stored amount reads back as the same float64. Planner rows are already floored, so they never hit the refusal.
  • Six decimals already exceed the provider's five, so the column didn't need widening and no migration was added. (Note for numbering: 000138 is taken by fix(auth): reject sessions issued before the user's last password change #1197.)

Item 2 (refuse tranches without a CommitmentRate): left for #118

There is still no tranche executor on main. ClaimDueLadderTranches exists but has no production caller. It deliberately passes legacy NULL-basis rows and on_demand_equivalent rows (both without a rate) to the callback, and TestLadderTrancheProvenance_InsertClaimAndReconcile pins that behavior. Whether the executor should fail the batch, skip those rows, or cancel them with a status_reason is a design choice for #118, so I didn't add a guard here.

Design constraint for #118: the executor must refuse any claimed row whose CommitmentRate is nil rather than defaulting the rate to 1. New planner rows always carry amount_basis='commitment' plus a rate. insertLadderTranchesTx and the ladder_tranche_amount_provenance CHECK already reject a commitment-basis row without a rate.

Verification

Run on macOS against real Postgres (testcontainers, postgres image) via the existing integration harness. Tests ran with AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null because this machine's default AWS profile is invalid. All evidence is unit tests plus local real-Postgres integration tests; nothing calls a real cloud API.

New tests:

  • internal/config/store_postgres_ladder_precision_integration_test.go TestLadderTrancheAmount_RoundTripsProviderPrecisionContract. It stores contract edges (0.001, 0.000999, 0.00101, 0.1, 0.29, 0.12345, 1.00001, 123456.78901, 999999.99999, 999999.999999, 1000000, 1000000.000001, 99999999.999999) and floored planner rats (1/3, 0.12345999999999999, 0.1234567, 0.0009995, 999999.9999999, 1000000). It reads them back through ClaimDueLadderTranches (the float scan an executor will use) and checks three things:

    • the float64 bits match;
    • amount_usd_hr::text holds the exact decimal;
    • the contract's refusal and 5-decimal wire (a mirror of spcommitment.Normalize) are the same for the stored amount as for the written one, or for the exact plan in the planner cases.

    Floats the column would round (0.1234599, 0.0009995, 999999.9999999, 1/3, NaN) must be refused, with the batch rolled back.

  • internal/config/ladder_provenance_test.go: unit tests for LadderAmountFloat and ladderAmountText.

  • internal/server/handler_ladder_test.go TestBuildTrancheDBRows_FloorsAmountToStoredScale, TestBuildTrancheDBRows_DropsAmountFlooringToZero, TestBuildTrancheDBRows_NonPositiveAmountErrors and TestImmediateTrancheAmounts_SumsStoredAmounts.

  • TestLadderB1_DailyRerunNeverExceedsTarget (existing test, tightened): live commitment must now be <= target on every one of the 40 days. Before, the bound was target+1e-6, which tolerated the half-up overshoot. The final fixed point may now sit up to 1e-6 per ramp step below the target, never above it. That is far under the provider's 1e-5 precision.

Fail-before / pass-after (mutation runs against the pre-fix code, with the new tests kept):

# planner seam reverted (nearest r.Float64()), store as on main
--- FAIL: TestLadderTrancheAmount_RoundTripsProviderPrecisionContract
    expected: 0x412e847ffffffca5  actual: 0x412e848000000000
    stored 999999.9999999 read back as 1e+06
# store seam reverted only
--- FAIL: TestLadderTrancheAmount_RoundTripsProviderPrecisionContract
    An error is expected but got nil.  0.1234599
# buildTrancheDBRows reverted
--- FAIL: TestBuildTrancheDBRows_FloorsAmountToStoredScale
    expected: 0.123459  actual: 0.12345999999999999
# handler + store as on main
--- FAIL: TestLadderB1_DailyRerunNeverExceedsTarget
    "10.000001" is not less than or equal to "10"   day 7 over-plans: live=10.000001

After the fix, all of these pass:

go build ./...
go vet ./internal/config/ ./internal/server/ && go vet -tags integration ./internal/config/
go test ./internal/config/ ./internal/server/ ./internal/mocks/ -count=1
go test -tags integration ./internal/config/ -run Ladder -count=1 -p 1          # ok 96s
go test -tags integration ./internal/server/ -run Ladder -count=1               # ok
go test -tags integration ./internal/purchase/ -run 'Ladder|RampStep' -count=1  # ok
go test -tags integration ./internal/api/ -run Ladder -count=1                  # ok
golangci-lint v2.10.1 run --allow-parallel-runners [--build-tags integration] ./internal/config/... ./internal/server/...  # 0 issues
gocyclo -over 10 -ignore "_test\.go" <changed non-test files>                   # silent
bash scripts/gosec-hook.sh <changed files>                                      # exit 0
git-secrets --register-aws --scan (isolated temp repo, all tracked files) + scripts/test-git-secrets-allowlist.sh  # clean / ok

Concurrency: this only changes how one row's value is encoded inside the existing insert statement. No new locking, reads or write ordering, so there is no new race to test.

Out of scope / notes

  • The tolerance has one edge that no float64 boundary can avoid. A plan within one float64 ulp above 1000000 (about 1e-10) converts to exactly 1000000 and would be accepted. The go contract already documents this as the float64 precision seam in PurchaseLayer.
  • ladder_runs.total_hourly_commit (a dashboard total, the float sum of the rows) is still rounded half-up by its own NUMERIC(20,6). It is not a purchase input, so I left it alone.
  • Branch is up to date with origin/main (merged, no conflicts).
  • Not decided here: whether a positive amount below the provider minimum (0.001 USD/hr) should be refused at plan time rather than at purchase. Today it is stored and the provider refuses it at purchase.

Maintainer: please mirror the issue's triage labels onto this PR: severity/medium, triaged, priority/p3, urgency/this-quarter, impact/few, effort/s, type/bug.

Refs #1182

🤖 Generated with Claude Code

…nding them up

Tranche amounts are float64 stored in NUMERIC(20,6). Two seams could round a
planned commitment up, against the go precision contract (pkg/ladder
LadderCapability.PurchaseLayer, go#354/LeanerCloud#403/LeanerCloud#438: floor, never round up; refuse
outside [0.001, 1000000] USD/hr; truncate to five decimals):

- buildTrancheDBRows converted the exact *big.Rat to the nearest float64.
- Postgres rounded the float's decimal half-up to six places on insert, so
  0.0009995 was stored as the 0.001 minimum, 999999.9999999 as 1000000 and
  0.1234599 as 0.123460 (five-decimal wire 0.12346 instead of 0.12345).

The planner now floors to six decimals (config.LadderAmountFloat), and the
store writes an exact decimal and refuses an amount the column would round
(ladderAmountText), so every stored amount reads back as the same float64.
No migration: six decimals already exceed the provider's five.

TestLadderB1_DailyRerunNeverExceedsTarget now requires live <= target on
every day (half-up rounding over-planned to 10.000001 on day 7) and allows
the floored fixed point to sit up to one micro-dollar per ramp step below it.

Refs LeanerCloud#1182

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 7 billable files and costs up to $1.75.

Or wait 8 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 1c5787a and a48b124.

Files selected for processing: 7
  • internal/config/ladder_provenance.go
  • internal/config/ladder_provenance_test.go
  • internal/config/store_postgres_ladder.go
  • internal/config/store_postgres_ladder_precision_integration_test.go
  • internal/server/handler_ladder.go
  • internal/server/handler_ladder_immediate_integration_test.go
  • internal/server/handler_ladder_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@IAmJSD IAmJSD added severity/medium Moderate harm triaged Item has been triaged priority/p3 Polish / idea / may never ship urgency/this-quarter Within the quarter impact/few Limited audience effort/s Hours type/bug Defect labels Oct 20, 2026
@cristim

cristim commented Oct 21, 2026

Copy link
Copy Markdown
Member

Independent review 2 (claude-fleet, cold): APPROVE f1bdefa

Findings (none blocking):

  • [non-blocking] internal/config/store_postgres_ladder.go:300-315 - The $5::numeric text encoding is not detectable by any test.
    • Mutation M3 restored the float64 parameter but kept the ladderAmountText refusal, and every test still passed.
    • pgx v5 encodes a float64 into numeric through its shortest decimal, so once the more-than-6-decimal refusal is in place, both encodings store the same value.
    • Storing 12345678.123456, 1234567890.123456 and 99999999.999999 gave exact values with and without the mutation.
    • The refusal is the real fix. The text path is harmless and defensible, but untested and arguably redundant.
  • [non-blocking] internal/server/handler_ladder.go:407 (immediateTrancheAmounts) - The display plan's immediate amounts still use the exact rationals, while the persisted rows are now floored.
    • So plan_json can show up to 1e-6 per tranche more than the rows netting reads. That slightly contradicts the comment that the plan "never authorizes more than the rows netting sees".
    • The plan is display only, so no purchase is affected.
  • [non-blocking, theoretical] internal/config/ladder_provenance.go:192 (LadderAmountFloat doc) - "The stored decimal equals the floored value" holds only below about 1e9 USD/hr, as the doc says.
    • Above about 8.6e9, the float64 ulp exceeds 1e-6, and the shortest decimal could round up.
    • That is far beyond the provider's 1e6 cap.

Against #1182 item 1: both seams are fixed.

  • The nearest-float conversion in buildTrancheDBRows (handler_ladder.go:723) is replaced by a floor.
  • Postgres half-up rounding at insert (store_postgres_ladder.go:300) can no longer round up, because of the new refusal.
  • big.Int.Div is Euclidean, so it floors for a positive denominator.
  • The test mirror spCommitmentWire matches the go repo's spcommitment Normalize at the pinned aws version: range [0.001, 1e6], then the shortest decimal truncated to 5 places.
  • Item 2 is deferred to fix(ladder): tranche status is never updated, so in-flight netting grows and planning silently stops #118 with a stated reason. That is acceptable, since ClaimDueLadderTranches has no production caller.

Attacked:

  • Blast radius:
    • The only production producer of LadderTrancheDB.AmountUSDHr is buildTrancheDBRows, which is now floored, so the refusal cannot reject planner rows.
    • The amend path writes a string already limited to 6 decimals (ladder_amend.go:15).
    • checkLadderRunTotal is unaffected or slightly improved.
  • Mutation probes on a scratch copy, each restored and confirmed identical with cmp:
    • M1, LadderAmountFloat changed to the nearest float: TestLadderAmountFloat and TestBuildTrancheDBRows_FloorsAmountToStoredScale FAIL.
    • M2, the 6-decimal refusal removed: TestLadderAmountText_Refuses and the integration TestLadderTrancheAmount_RoundTrips FAIL.
    • M3, float64 sent instead of text: survives (the first finding above).
    • M4, the handler back to r.Float64(): TestBuildTrancheDBRows_FloorsAmount and the integration TestLadderB1_DailyRerunNeverExceedsTarget FAIL.
  • Fail-before, on base plus the new tests plus a shim that reproduces base behaviour. All five FAIL:
    • TestLadderAmountFloat: expected 0.333333, got 0.3333333333333333.
    • TestLadderAmountText_Refuses: expected an error, got nil.
    • TestBuildTrancheDBRows_FloorsAmount: expected 0.123459, got 0.12345999999999999.
    • TestLadderTrancheAmount_RoundTrips: 1/3 reads back as 0.333333.
    • TestLadderB1_DailyRerun: on day 7, live=10.000001 > 10.

Verified locally, with no cloud credentials, under the heavy lock, with testcontainers Postgres:

  • Head:
    • go build ./... and go vet on config and server, plain and with -tags integration: OK.
    • gofmt -l on the 7 changed files: clean.
    • go test -count=1 ./internal/config/ ./internal/server/ ./internal/mocks/: ok.
    • Integration runs, all ok:
      • -run Ladder on config and server;
      • -run 'Ladder|RampStep' on purchase;
      • -run Ladder on api.
  • Merged tree:
    • merge-tree with origin/main 7eac792 is clean.
    • The touched files did not change on main, but the config package did, so I exported the merged tree.
    • Build, vet, unit tests on config, server and mocks, and integration -run Ladder on config and server: all ok.
  • No migration.

Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged.

@cristim

cristim commented Oct 21, 2026

Copy link
Copy Markdown
Member

VERDICT: REQUEST_CHANGES f1bdefa

Rebase check: git merge-tree against origin/main 7eac792 is clean (tree 86ad8000). The merged tree builds, and go vet passes with and without -tags integration for ./internal/config/ and ./internal/server/. Both sides pin go pkg v0.0.0-20261018003623-541e05e5fd29.
Largest commit: f1bdefa, 269 changed lines (cap 400). OK.

Findings

  • medium internal/server/handler_ladder.go:723 (via internal/config/ladder_provenance.go:193) - flooring persists a $0 tranche without any error. A positive planned amount below 1e-6 floors to 0. ladderAmountText(0) returns "0", and the row is inserted as a scheduled 0.000000 tranche. The comment at handler_ladder.go:718 says this function surfaces errors "instead of writing a $0 row". The go planner only skips exact zeros (pkg/ladder/ladder.go:187, processAllocation), and PlannedAction.Validate rejects amounts <= 0, so a 0 here comes from this seam. Reproduced on the merged tree: buildTrancheDBRows with AmountUSDPerHour "1/10000000" and "9/10000000" returns AmountUSDHr=0 and err=nil. The 1e-6 case returns 1e-06. On main, amounts in [5e-7, 1e-6) were stored as 0.000001; this PR now stores them as 0, which widens the zero band. Fix: refuse a tranche that floors to 0 (fail loud), or drop it explicitly with a log line, and add a unit test either way. Also decide whether a positive amount below the provider minimum of 0.001 should be refused at plan time instead of at purchase.
  • nit internal/config/ladder_provenance.go:197 - the comment "at most 15 significant digits below 1e9" is correct. Above roughly 1e9 USD/hr, though, the nearest float of the floored decimal can round up, and nothing refuses that. It is unreachable under the 1e6 contract cap, so a doc nit only.

Checked and correct

  • Every amount_usd_hr write was checked. Planner inserts go through insertLadderTranchesTx (SaveLadderTranches and insertLadderRunWithTranchesTx), and the only LadderTrancheDB construction with an amount is buildTrancheDBRows. Amendments (store_postgres_ladder_amend.go:105) already write a validated decimal string limited to 6 places.
  • The floor is correct: big.Int.Div is Euclidean, and a Rat denominator is always positive. A 6-place floor followed by 5-place truncation equals the 5-place truncation of the exact value. This matches spcommitment.Normalize (providers/aws aeb8a6f7ff51, commitment.go:19-47) and the PurchaseLayer contract at capability.go:62-66.

Attacked

  • Merged tree runs:
    • go test ./internal/config/ ./internal/server/: ok
    • -tags integration ./internal/config/ -run Ladder -p 1: ok (182s)
    • -tags integration ./internal/server/ -run Ladder: ok
  • 40-day rerun probe (real Postgres): no zero rows, final live=9.999998, never above 10.
  • Mutation probes, each restored afterwards:
    • nearest Float64 in buildTrancheDBRows: FloorsAmountToStoredScale FAILS
    • scale 5: three tests FAIL
    • half-up instead of floor: FAILS
    • drop the >6-decimal refusal: FAILS
    • NaN accepted: FAILS
    • store seam sends the float instead of text (integration): RoundTrips FAILS ("error expected")

IAmJSD and others added 2 commits October 21, 2026 08:19
Address the LeanerCloud#1199 review. A positive planned amount below 0.000001 USD/hr
floors to 0 at the six-decimal tranche column; buildTrancheDBRows now drops
that tranche with a log line instead of persisting a $0 scheduled row, and
refuses a zero or negative planned amount outright. immediateTrancheAmounts
sums the same floored amounts, so plan_json never shows more than the rows
netting reads. Document that the floored float can round up only above
about 1e9 USD/hr.

Refs LeanerCloud#1182

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the reviews of f1bdefa. New head: 606ecaf. This merges origin/main (c8bdd46, clean) and adds one fix commit.

VERDICT: REQUEST_CHANGES

  • medium, $0 tranche from flooring: fixed.
    • New storedTrancheAmount (internal/server/handler_ladder.go:691) floors the planned amount with the new config.LadderAmountFloor.
    • A positive amount that floors to 0 is dropped in buildTrancheDBRows with a log line (handler_ladder.go:748-753) and is no longer written as a scheduled 0.000000 row.
    • A zero or negative planned amount fails loud (handler_ladder.go:696).
    • I chose drop over refuse because refusing would fail the same run on every rerun. A sub-1e-6 amount can come from a custom ramp's tiny step fraction, and it is far below the 0.001 minimum, so it could never be bought anyway.
    • New tests in handler_ladder_test.go:
      • TestBuildTrancheDBRows_DropsAmountFlooringToZero (1157): 1/10000000 and 9/10000000 are dropped, and 1/1000000 is kept as 1e-06.
      • TestBuildTrancheDBRows_NonPositiveAmountErrors (1179).
    • Mutation probes, each restored and checked with cmp, against the new tests:
      • persisting the zero floor: DropsAmount and SumsStoredAmounts FAIL.
      • removing the non-positive check: NonPositive and SumsStoredAmounts FAIL.
  • Refuse amounts below 0.001 at plan time: left open, owner decision. Today such an amount is stored and the provider refuses it at purchase. I noted this in the PR body.
  • nit, doc above 1e9: fixed. The LadderAmountFloat doc (internal/config/ladder_provenance.go:203-208) now says that above about 1e9 USD/hr the nearest float can round the floored decimal up by less than one ulp, that nothing refuses it, and that this is unreachable under the 1e6 cap.

Independent review 2: APPROVE

  • $5::numeric text encoding is untested: left as is. You're right that once the more-than-6-decimal refusal is in place, pgx's shortest-decimal float encoding stores the same value. I kept the text path because it makes the written decimal explicit and doesn't depend on pgx's float-to-numeric encoding. The refusal is the tested guard.
  • Display plan shows exact rationals while the rows are floored: fixed. immediateTrancheAmounts (handler_ladder.go:709) now sums the floored amounts through storedTrancheAmount and skips dropped tranches. So plan_json never shows more than the rows, and the comment at handler_ladder.go:405 is now accurate.
    • New test: TestImmediateTrancheAmounts_SumsStoredAmounts (handler_ladder_test.go:1193). It checks that two immediate 1/3 tranches sum to 0.666666, and that a layer whose only immediate tranche floors to 0 has no entry.
    • Mutation probe: going back to exact rationals makes it FAIL.
  • LadderAmountFloat doc above ~8.6e9: fixed by the same doc change as above.

PR body: describes the drop/refuse behavior and the new tests, notes the 0.001 plan-time question as open, and replaces the stale "behind main" note.

Verification on the merged tree (no cloud credentials, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null, testcontainers Postgres):

  • go build ./...: ok.
  • go vet ./internal/config/ ./internal/server/, plain and with -tags integration: ok.
  • gofmt -l: clean.
  • go test -count=1, run one package at a time: ok for ./internal/config/, ./internal/server/ and ./internal/mocks/.
  • Integration runs, all ok:
    • -tags integration ./internal/config/ -run Ladder -p 1;
    • ./internal/server/ -run Ladder;
    • ./internal/purchase/ -run 'Ladder|RampStep';
    • ./internal/api/ -run Ladder.
  • golangci-lint v2.10.1 --allow-parallel-runners on config and server, plain and with --build-tags integration: 0 issues.
  • gocyclo -over 10 on the changed non-test files: silent.
  • scripts/gosec-hook.sh on the changed files: exit 0.
  • git-secrets --register-aws --scan in an isolated temp repo: clean.
  • scripts/test-git-secrets-allowlist.sh: 137 PASS.

@cristim

cristim commented Oct 25, 2026

Copy link
Copy Markdown
Member

VERDICT: APPROVE 606ecaf

Delta f1bdefa..606ecaf (one commit):

  • config.LadderAmountFloor (internal/config/ladder_provenance.go) is split out of LadderAmountFloat and returns the exact floored *big.Rat.
  • New storedTrancheAmount (internal/server/handler_ladder.go:691) refuses an amount that is malformed or <= 0. A positive amount that floors to 0 at 6 decimals returns stored=false.
  • buildTrancheDBRows (handler_ladder.go:739) drops a tranche with stored=false and logs a line. It never writes a $0 row.
  • immediateTrancheAmounts (handler_ladder.go:709) sums the same floored amounts, so the display plan never exceeds the rows. When every immediate tranche of a layer floors to 0, that layer gets no entry and no purchase action (handler_ladder.go:581).
  • 3 new unit tests.

Prior blocker ($0 tranche when the planned amount is < 1e-6): FIXED. The tranche is dropped with a log line. 1e-6 itself is kept. Tested by TestBuildTrancheDBRows_DropsAmountFlooringToZero.

Money path, every tranche amount write:

  • Planner insert: handler floor -> ladderAmountText (refuses NaN/Inf and >6 decimals) -> $5::numeric.
  • Amend UPDATE (store_postgres_ladder_amend.go:105): input already validated by ladderAmountPattern plus Sign() > 0 (ladder_amend.go:33-35).
  • No other production writer.
  • Upstream BuildTranches (go pkg ladder.go processAllocation) already skips zero steps, so the new non-positive refusal cannot fire in normal runs and cannot fail them.

Findings (all nit, non-blocking):

  • nit internal/config/store_postgres_ladder.go:300 - the store has no guard on amount <= 0 and there is no DB CHECK. The $0 guard lives only in the handler. Optional defense in depth: have ladderAmountText refuse amounts <= 0.
  • nit handler_ladder.go:412 - p.tranches counts the tranches the planner produced, including dropped ones, so the run counter can exceed the rows written.
  • nit - a sub-1e-6 residual gap can re-plan each run and be dropped again, so one log line repeats daily per config. Harmless.

Attacked / results:

  • merge-tree with origin/main 503eb2e: clean (debec295). Ran on the merged tree (go 1.26.10): build, vet, and unit tests for config and server, 1478 passed. -tags integration -run Ladder for config and server: ok. RoundTripsProviderPrecisionContract and ImmediateStep* PASS on testcontainers Postgres.
  • Mutation: persist a stored=false tranche -> DropsAmountFlooringToZero FAIL.
  • Mutation: remove the Sign() <= 0 refusal -> NonPositiveAmountErrors and SumsStoredAmounts FAIL.
  • Mutation: return the unfloored rat -> FloorsAmountToStoredScale and SumsStoredAmounts FAIL.
  • CI on 606ecaf (run of 2026-10-21, old base c8bdd46, go 1.26.9): Build Docker Image, Security Scanning and CI Success are red because of GO-2026-6809 in stdlib (fixed in 1.26.10). That comes from the old base, not the PR. Main 503eb2e is green. A rebase or re-run is needed for green CI on the merged SHA.

@IAmJSD CI on this head is red only from the old merge base (govulncheck on go1.26.9, fixed on main by #1236). Please merge or rebase onto current main so CI runs on a fresh merge ref.

@cristim

cristim commented Oct 25, 2026

Copy link
Copy Markdown
Member

Independent review 2 (claude-fleet, cold), re-review: APPROVE 606ecaf

Identity: #1199 is fix/issue-1182 (the ladder flooring PR). f1bdefa is its commit, and the earlier approval belongs here. #1099 is a different branch (fix/issue-84).

Delta: f1bdefa..606ecaf. Merge 735f967 (main c8bdd46) has an empty remerge-diff. Commit 606ecaf touches ladder_provenance.go, handler_ladder.go and tests. The head merges cleanly with current main 2ded648.

Findings:

Attacked:

  • Can a drop lose a real purchase? BuildTranches already skips exact zeros and clamps the last step to the remainder (pkg ladder.go processAllocation). A sub-1e-6 tranche therefore comes only from a remainder or a sliver of the ramp fraction, at least 1000x below the 0.001 USD/hr provider minimum. The r.Sign() <= 0 refusal cannot fire from BuildTranches output, so it fails loud only on a broken tranche.
  • Total never exceeds the target: each row is floored, and drops only remove amount. TotalHourlyCommit is still summed from the persisted rows (handler_ladder.go:470). Probe Q4 (persist unfloored) makes TestLadderB1_DailyRerunNeverExceedsTarget FAIL.
  • Display matches the rows: immediateTrancheAmounts and buildTrancheDBRows share storedTrancheAmount. assembleLadderPlan skips a nil layer, so no zero-amount PlannedAction reaches Validate.
  • The drop log line carries the run ID, tranche ID, layer and RatString. Nothing user-controlled or sensitive.
  • Probes on the merged export, each restored and cmp-checked. All KILLED:
    • Q1: persist floored zeros.
    • Q2: drop exactly 1e-6.
    • Q3: no sign check.
    • Q4: store unfloored amounts (unit and integration).
    • Q5: display keeps dropped tranches.
    • Q6: round half-up in LadderAmountFloor.
  • Fail-before: 606ecaf's test file on 735f967 FAILS DropsAmountFlooringToZero (4 rows, not 2), NonPositiveAmountErrors and SumsStoredAmounts (2/3, not 0.666666).

Verified locally (go1.26.10, merged export, heavy lock):

Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged.

Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristim

cristim commented Oct 25, 2026

Copy link
Copy Markdown
Member

Independent review 2 (claude-fleet, cold), re-review: APPROVE a48b124

This is a delta from my APPROVE at 606ecaf. The only new commit is a merge of origin/main (1c5787a):

  • git log --first-parent 606ecafe..a48b124b shows just that merge.
  • git show --remerge-diff is empty, so there are no hand edits, and the PR's own change is identical to what I reviewed.
  • git merge-tree --write-tree origin/main a48b124b is clean.

Verified locally on a git archive export of a48b124, with GOTOOLCHAIN=go1.26.10 and no cloud credentials, under the heavy lock:

  • go build ./...: ok.
  • go vet on config and server, plain and with -tags integration: ok.
  • go test -count=1 ./internal/config/ ./internal/server/ ./internal/mocks/: ok. That includes the Ladder, Tranche and BuildTrancheDBRows tests.
  • go test -tags integration -run 'Ladder|Tranche' ./internal/config/ ./internal/server/ (testcontainers Postgres): ok.

Merge order with #1238: #1238 is not on main, so TestLadderTrancheAmount_RoundTripsProviderPrecisionContract still passes here. Whichever of #1199/#1238 lands second must switch that test's lifecycleTranche fixture to claimableTranche. On a combined tree, the test claims 0 of 19 rows.

My earlier findings still apply. LadderAmountFloat has no production caller.

Same reviewer as the earlier review, re-reviewing the new head. Review only: nothing was pushed, and the GitHub review state is unchanged.

@cristim

cristim commented Oct 25, 2026

Copy link
Copy Markdown
Member

VERDICT: APPROVE a48b124
Delta re-review after the refresh onto main (previously approved at 606ecaf).

Findings: no blocking findings.

Attacked:

  • Merge cleanliness: git show --remerge-diff a48b124b is empty (no conflict resolution, no hand edits in the merge).
  • PR diff unchanged: compared git diff c8bdd460..606ecafe (old merge base) with git diff 1c5787a0..a48b124b and git diff origin/main...a48b124b, index lines stripped. Byte-identical except two hunk-header offsets (-3 lines) in handler_ladder_immediate_integration_test.go, caused by main's #702894d2 switching ladderB1Conn to testhelpers.RequireMigratedDatabase. The PR's hunk there (TestLadderB1_DailyRerunNeverExceedsTarget) is unaffected.
  • Main changes since the old base that touch the PR's area: store_postgres_ladder_resolve.go (checked_at comparison, unrelated), ladder test harness moves to a shared Postgres server per package, pg_locks database filter in the lock test, frontend ladder work. No new writer of ladder_tranches.amount_usd_hr: the only INSERT is still insertLadderTranchesTx (now through ladderAmountText) and the amend UPDATE (store_postgres_ladder_amend.go:105) is pre-existing and out of scope. buildTrancheDBRows still has one caller (handler_ladder.go runLadderPlan path).
  • Clean merge onto current origin/main 8f186a8: git merge-tree --write-tree origin/main a48b124b clean (tree eedbab8d). All tests below ran on that merged tree.
  • Unit: go test ./internal/config/ ./internal/purchase/ ./internal/server/ all ok.
  • Integration (testcontainers): go test -tags integration -run Ladder ./internal/config/ ./internal/server/ ok; with -v, TestLadderTrancheAmount_RoundTripsProviderPrecisionContract and all five TestLadderB1_* tests PASS (not skipped).
  • Mutation probes on the merged tree (each restored, files diffed against the tree afterwards):
    1. LadderAmountFloat uses nearest float (amount.Float64()) instead of the floor: TestLadderAmountFloat_FloorsToStoredScaleWithoutRoundingUp FAILS.
    2. ladderAmountText scale guard disabled (> 99): TestLadderAmountText_RefusesAmountsTheColumnWouldRound FAILS.
    3. storedTrancheAmount always reports stored=true: TestBuildTrancheDBRows_DropsAmountFlooringToZero / TestImmediateTrancheAmounts_SumsStoredAmounts FAIL.
  • CI on a48b124: 35 pass, 1 skipping.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at a48b124baa593218d7130f8178307d0fd2b0e8c6 merged onto main (26004a6d), together with #974, #1106, #1151, #1193, #1199, #1212, #1217, #1218, #1219, #1221, #1223, #1224, #1225, #1226, #1227, #1256, #1262, #1266, #1272, #1275, #1283, #1286, #1293, #1299, #1300, #1313 in that order (combined tree d3d95c83, Linux, Go 1.26.10): go build ./..., go vet with and without -tags integration, the full go test -race -short -count=1 ./..., and go test -race -tags integration -count=1 on every changed package, against Docker Postgres. All pass. CI Success and pre-commit are green on this head, with 2 approving independent review(s) on it. Nothing was run against any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 25, 2026 19:37
@IAmJSD
IAmJSD merged commit c1d4a4a into LeanerCloud:main Oct 25, 2026
36 checks passed
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on a48b124b, green CI, and the combined-tree verification above. Squash-merged at exactly a48b124baa593218d7130f8178307d0fd2b0e8c6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p3 Polish / idea / may never ship severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants