Repository navigation
fix(money): zero-vs-absent conflation in providers/azure and pkg/config,pkg/scorer [go part] #62
Description
Split from LeanerCloud/cloud-commitments-cli#1638 (cross-component issue, monorepo split)
The shared root cause
A value that is absent is represented by the zero value of its type --
0,'',false, SQLNULLcollapsed by an aggregate -- and is then consumed as if the user, the provider, or the database had asserted that value. In every case below the code cannot answer "did nobody supply this, or did somebody supply zero?", and it resolves the ambiguity in the direction that produces a confident-looking number.The fix pattern
- Go: represent absent as
*float64/*bool, never a value type with a magic zero. Where a pointer is impractical, return(T, error)or(T, bool).- Never coerce a missing number to 0 on a money path. A fabricated
$0is indistinguishable from a real one and reads as authoritative.Instances (this repo)
extFloatreturns0for both a missing key and a parse failure on the Azure Advisor savings path
- Where:
providers/azure/recommendations.go:442(rec.EstimatedSavings = extFloat(ext, "annualSavingsAmount") / 12),extFloatat:455-462.- What:
extFloatreturns0when the key is absent, when the pointer is nil, and whenstrconv.ParseFloatfails. Note the deliberate contrast with its two siblings:extString(:449-454) andextInt(:464-471) both take afallbackparameter and preserve the caller's existing value;extFloatalone hardcodes0and has no fallback parameter at all.- Failure scenario: an Advisor payload returns
annualSavingsAmount: "1,234.00"(a thousands separator, whichParseFloatrejects). The recommendation is stored withEstimatedSavings = 0, ranked below every other recommendation, and hidden by any savings-threshold filter. A real saving is presented as worthless rather than flagged as unparsable, and no error is logged.- Fix:
extFloatreturns(float64, error)or*float64; the caller drops the recommendation with a log rather than shipping a fabricated 0. This instance also appears in the currency-blindness sweep filed alongside this one (Advisor'ssavingsCurrencyis likewise never read); a single fix at this call site closes both.- Related:
providers/azure/internal/recommendations/converter.go:308has anamountValuePtrwritten specifically to preserve the absent signal, next to anamountValuethat drops it -- same file, same inconsistency, worth checking alongside theextFloatfix.
pkg/configflipsDryRuntofalsewhenever the YAML file omitsdry_run
- Where:
pkg/config/load.go:116-119(applyYAMLBase), withyamlConfig.DryRundeclared as a plainbooland the safe default set inLoad.- What:
if yc.DryRun != cfg.DryRun { cfg.DryRun = yc.DryRun }is an unconditional assignment written to look conditional. BecauseyamlConfig.DryRunis a valuebool, an absentdry_run:key unmarshals tofalseand is indistinguishable from an explicitdry_run: false. The default isDryRun: true, documented as "no purchases without explicit opt-out", so merely having a config file present inverts the safety default. The sibling field on the very next lines (:120-122,if yc.AutoApprove { … }) uses the correct absent-means-keep-default shape, which is what shows the asymmetry is unintentional.- Failure scenario: an operator writes a
cudly.yamlcontaining onlyenabled_clouds: [aws].LoadresolvesDryRun=false. Every subsequent invocation is a real-purchase run with no flag having been passed, and the resolved config never surfaces that the safety default was overridden.- Mitigating fact, verified:
pkg/configcurrently has no importer outside itself, so this cannot fire today. It is a loaded gun for whoever wires the layered loader into a consumer, and the cost of fixing it now is two*boolfields.- Fix: make
yamlConfig.DryRun/AutoApprove*booland apply only when non-nil -- the same absent-versus-explicitly-false distinctionpkg/common/types.goalready uses forRecurringMonthlyCost.Findings from the 2026-09-02 codebase audit
A06-027 (low) -- this repo's half
The mirror direction of the ladder-plan zero-fabrication (platform half filed separately): all three numeric filters in
pkg/scorer/scorer.go:79,:83and:87are guarded by> 0, so a configuredMinSavingsPctof0disables the filter rather than setting the floor at zero, and a recommendation withSavingsPercentage = -12lands inresult.Passed. Findings A06-027 and A09-028.Verification requirement
Each fix needs a test that feeds a real zero and a genuine absence through the same path and asserts they produce different outcomes. A test that only exercises populated values passes with every one of these defects present, which is why they survived to now: the existing suites cover the populated path everywhere.
Cross-references
- fix(exchange): make PurchaseHistoryRecord.MonthlyCost a *float64 to distinguish absent from zero cloud-commitments-cli#1081 --
fix(exchange): make PurchaseHistoryRecord.MonthlyCost a *float64 to distinguish absent from zero. Already open, already this exact pattern. - The platform-repo half of this same audit sweep (frontend/, internal/auth, internal/database/postgres/migrations, internal/api, internal/server) is filed separately as a sibling issue in LeanerCloud/cloud-commitments-platform, since those files no longer live in this repo after the split.
Acceptance criteria
-
providers/azure/recommendations.goextFloatreturns(float64, error)or*float64instead of collapsing missing-key and parse-failure to0; caller drops/logs unparsable recommendations instead of shippingEstimatedSavings = 0. -
providers/azure/internal/recommendations/converter.go:308amountValue/amountValuePtrinconsistency reviewed alongside theextFloatfix. -
pkg/config/load.goDryRunandAutoApprovebecome*boolinyamlConfig, applied only when non-nil, so an absentdry_run:key keeps the safe default. -
pkg/scorer/scorer.go:79,83,87numeric filters distinguish "MinSavingsPct configured as 0" from "MinSavingsPct not configured", per A06-027. - Each fix ships a test that exercises a real zero and a genuine absence through the same path and asserts they diverge.
Activity
fix(azure): drop Advisor recommendations with absent or unparsable sa…
fix(azure)!: read authoritative VM reservation inventory
cristim commentedon Oct 17, 2026
Checked against main: partly fixed (extFloat returns an error; pkg/config DryRun is *bool). Remaining:
scorer.Config.MinSavingsPctis a plain float with a > 0 guard (pkg/scorer/scorer.go~:17, :88), so an explicit 0 floor reads as unset and a negative-savings rec passes; and the Azure converter's amountValue returns 0 for an absent Amount. Fix: an additiveMinSavingsPctSetfrom the *float64 in load.go plus nil-aware cost fields, about 100 lines. Open PR #461 edits scorer.go, so this is blocked on #461.cristim commentedon Oct 19, 2026
Claiming: the remaining scorer.Config.MinSavingsPct unset-vs-explicit-zero instance; the blocking PRs have merged [claude-cc-libs]
fix(scorer): fail closed on a NaN or Inf MinSavingsPct (#62)
fix(scorer): keep the original negative min_savings_pct error text (#62)
fix(scorer): distinguish an explicit 0% savings floor from unset (#516)
cristim commentedon Oct 19, 2026
Claiming: the remaining Azure converter amountValue absent-vs-zero instance (the scorer part landed in #516) [claude-cc-libs]
fix(azure): stop reading absent Modern amounts through a magic-zero h…