Skip to content

Steps calibration: manual sampleDays counts unusable points and trace misstates the source #84

Description

@bhelm

Double-verified finding

Two independent read-only audits reproduced this on exact fork integration base 032983d2bed9bf50dfcfc4d06ef707f3818cdc26. Current upstream main still contains the same behavior.

Both platform twins define a usable calibration day as:

  • motion >= 1.0
  • steps > 0

That filter drives the visible stepsCal point lines and StepsEstimateEngine.status. But calibrate(..., manualOverride: > 0) / calibrate(..., manualOverride > 0) returns before applying it and sets sampleDays to every supplied point. The trace then prints that unfiltered count and also describes a user-set coefficient as a motion-weighted median.

Exact reproduction

Call the public calibration trace with these points and manualOverride = 9.5:

  1. (motion: 0.5, steps: 500) — below the motion floor
  2. (motion: 10, steps: 0) — no positive phone reference
  3. (motion: 10, steps: 1000) — usable

Both platforms emit exactly one voting point:

stepsCal point motion=10.0 phoneRef=1000 ratio=100.0 (steps/motion votes weighted by motion)

but the fit line says:

stepsCal fit k=9.5 sampleDays=3 confidence=1.0 manual=true (k = motion-weighted median of steps/motion)

For the same input, status(...) reports a manual calibration with sampleDays=1.

Severity and reachability

P2 — diagnostic/provenance integrity. The coefficient, confidence, and estimated step values are not changed. The incorrect count is nevertheless persisted by the production orchestration and exported in the Steps diagnostic trace.

The path is production-reachable on both platforms: manual overrides are wired into the 60-day calibration pass, and calibration points admit any positive motion before the stricter motion >= 1.0 usability gate. The manual UI currently hides the count, limiting immediate user-visible impact.

Red tests

Add mirrored Swift and Kotlin tests with the three-point fixture above. Before the fix they must fail by proving:

  • calibrate(... manualOverride: 9.5).sampleDays == 1
  • status(... manualOverride: 9.5).sampleDays == 1
  • exactly one stepsCal point line is emitted
  • the exact fit line contains sampleDays=1
  • the manual fit line says (user-set k) and does not claim a weighted-median fit

Keep an all-usable manual fixture to prove the ordinary count remains unchanged.

Acceptance

  • One canonical usability predicate determines manual sampleDays everywhere.
  • The returned calibration, status, persisted profile metadata, and trace agree on the usable-day count.
  • A manual coefficient is described as user-set, never as a fitted weighted median.
  • Manual coefficient precedence, confidence 1.0, estimates, and the automatic calibration path remain unchanged.
  • Mirrored platform tests and the Steps differential trace rollout pass.

Routing

This is a production/helper fix, so implement it as a narrow upstream PR against ryanbr/noop; do not carry a fork-only production patch. After the maintainer merges it, sync upstream into drift/meta through the fork's merge workflow.

Final duplicate search across open/closed fork and upstream issues/PRs found no exact duplicate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions