Skip to content

fix(plans): a plan target_coverage of 0 is stored and shown as 80, and the dashboard treats 0 as unset #523

Closed

Description

@cristim

Summary

A plan target_coverage of 0 is stored and shown as 80, and the dashboard treats a coverage of 0 as unset. Since #510 0 is a valid stored coverage.

Location (origin/main 496d9d7)

  • internal/api/types.go:778 PlanRequest.TargetCoverage is int with omitempty, so 0 is dropped on marshal.
  • internal/api/types.go:865-868 buildServiceConfig turns 0 into 80.
  • internal/api/handler_dashboard.go:370-376 resolveCoverageByAccountKey drops entries with coverage 0, so scaledSavings shows full savings for accounts configured at 0%.
  • internal/api/handler_dashboard.go:386-394 resolveTargetCoverage returns 80 when the value is not greater than 0.
  • internal/purchase/manager.go:120 sets defaults.Coverage, but nothing reads Manager.defaults.

Failure scenario

An operator sets a plan target coverage to 0. The plan reads back as 80, and the dashboard still reports full savings for an account configured at 0%.

Decision needed

What the dashboard should show for 0% coverage (zero savings vs "not configured"); needs an owner decision.

Context

Related: #135 (closed), #345.

Acceptance

A plan created with target_coverage 0 reads back 0, and the dashboard behavior for 0 matches the owner decision, with tests.

Activity

  1. cristim commented on Oct 27, 2026

    @cristim
    MemberAuthor

    Claiming the plan half (claude-cc-platform). A plan target_coverage of 0 will be kept instead of saved as 80, an omitted value still defaults to 80, and the plan form will refuse an empty coverage field. The dashboard half, whether 0% shows as zero savings or as not configured, stays open for the owner decision, so the PR will say "Part of #523".

  2. cristim commented on Oct 27, 2026

    @cristim
    MemberAuthor

    Scope update for the plan half (PR #1533), decided by the coordinator for the owner as the safe default. The owner can override it.

    • Plans: target_coverage must be 1..100. An explicit 0 on a plan now gets a 400 with a clear message instead of being silently saved as 80, and an omitted value still defaults to 80.
      • Why: the plan executor rejects a 0% target as invalid input (planstep.validateInputs, internal/planstep/resolve.go:289, pinned by TestResolveFailsClosed). A stored 0% plan would fail on every tick without writing a failed row or notifying anyone, and a 0% plan can never buy anything anyway.
      • The alternatives were to keep 0 storable and have the executor record one failed step and notify, or to treat a 0% plan as a no-op. Either can replace this if the owner prefers.
    • So this issue's "a plan created with target_coverage 0 reads back 0" acceptance now applies only to non-plan uses of coverage, where 0 stays valid. Examples are the global and per-service coverage settings (fix(frontend/settings): keep a stored default_coverage of 0 on load #510).
    • Still open for the owner: the dashboard half. Should a 0% coverage show zero savings or "not configured" (resolveCoverageByAccountKey / resolveTargetCoverage)?
  3. cristim commented on Nov 12, 2026

    @cristim
    MemberAuthor

    Claimed by claude-platform-2.

  4. cristim commented on Nov 12, 2026

    @cristim
    MemberAuthor

    Releasing my claim (claude-platform-2): the issue's own 'Decision needed' (what the dashboard should show for 0% coverage: zero savings or not configured) is an owner decision with no recorded default, and the fix changes API serialisation (PlanRequest.TargetCoverage omitempty) and the dashboard semantics together. Not starting without it.

  5. cristim commented on Nov 12, 2026

    @cristim
    MemberAuthor

    Status check by claude-platform on origin/main: the plan half is in place. PR #1533 (merged) makes validateTargetCoverage refuse an explicit 0 with a 400 (internal/api/types.go:837-841), and an omitted value still defaults to 80. The remaining half is the dashboard's 0% display (zero savings or not configured), which the issue marks as an owner decision with no recorded default. Left open for that decision; no code change.

  6. cristim commented on Nov 12, 2026

    @cristim
    MemberAuthor

    Decided by claude-platform-2 under the owner rule (record, override on the issue if you disagree): see the decision comment below when it lands; claim re-taken for the recommended option.

  7. cristim commented on Nov 12, 2026

    @cristim
    MemberAuthor

    Decided by claude-platform-2 under the owner rule: a target_coverage of 0 is stored and shown as 0 (an omitted field still defaults to 80), and a plan service at 0% is refused before any purchase with a visible reason instead of buying. Landed on main in three commits: 6830b9a (planstep refuses a coverage-0 service with ErrUnsupportedPlan, recorded as a failed step; scheduled-ramp creation returns 409), 7d867ad (API accepts 0..100, create/update/response keep 0, form uses ?? and min 0, OpenAPI), 60975c8 (dashboard treats a configured 0% as zero savings; a global row with 0 or no row stays unset). Review: two adversarial passes by the same reviewer at tree bd0c8408 (before rebase onto main), no path buys on a 0% plan, 7 mutants killed. Local verification on the rebased head: build, vet (plain and integration), golangci-lint 0 issues, go test for api/config/planstep/purchase/mocks, jest 16 suites green. Main CI watch is running. Follow-up: add a config-level table test for ResolveConfiguredCoverageForRecs (override 0 over global 0 or 80, override without coverage, no row, global 80).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions