Skip to content

fix(engine): applyContributorCalibration can return NaN and bypasses its own cold-start guard on a NaN sampleSize #6627

Closed

Description

@JSONbored

Context

packages/loopover-engine/src/signals/contributor-calibration.ts's applyContributorCalibration(baselineReadinessScore, calibration) adjusts a baseline readiness score by a contributor's own predict-vs-real calibration history. Its own field-level doc comment on ContributorCalibrationSignal.agreementRate states: "Clamped to [0, 1] before use, so a malformed upstream aggregate can never push the adjustment past its own clamp" — i.e. the function's documented contract is that it degrades gracefully for any malformed calibration input and always returns a valid [0, 100] score (or the unchanged baseline).

The cold-start guard on line 56 is if (!calibration || calibration.sampleSize < MIN_CALIBRATION_SAMPLES) return baselineReadinessScore;. This relies on JavaScript's < comparison, which is always false for NaN. A calibration.sampleSize of NaN therefore bypasses the cold-start guard entirely instead of being treated as "insufficient history."

The local clamp(value, min, max) helper on line 18 (return Math.min(max, Math.max(min, value));) also has no Number.isFinite guard, unlike its sibling normalizers elsewhere in this package (e.g. governor/reputation-throttle.ts's clampFraction, which is Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : fallback, or governor/rate-limit.ts/governor/budget-cap.ts's finiteNonNegativeInt/finiteNonNegative, both of which explicitly fall back to a safe default for non-finite input). Math.max(min, NaN) and Math.min(max, NaN) both evaluate to NaN, so a NaN agreementRate (or a NaN produced anywhere downstream) propagates straight through clamp() unclamped.

Verified empirically (node -e, using the exact logic from the file):

  • applyContributorCalibration(70, { sampleSize: NaN, agreementRate: 0.9 }) returns 78 — the cold-start guard is bypassed and a full calibration adjustment is applied from a malformed sample count instead of returning the unchanged baseline of 70.
  • applyContributorCalibration(70, { sampleSize: 20, agreementRate: NaN }) returns NaN — a non-numeric readiness score reaches the caller, violating the documented [0, 100]-clamped contract.

The existing test suite (test/unit/predicted-gate.test.ts, describe("applyContributorCalibration (#2349)")) has explicit "CLAMP BOUNDARY" tests for out-of-range finite values (agreementRate: 1.7, agreementRate: -0.4) but no test exercises a non-finite (NaN) sampleSize or agreementRate.

Requirements

  • packages/loopover-engine/src/signals/contributor-calibration.ts's clamp(value, min, max) helper must treat a non-finite value (checked via Number.isFinite) as min (i.e. the lowest end of the clamp range) rather than propagating NaN, mirroring the fail-toward-the-safe-end pattern already used by this package's other numeric normalizers (governor/reputation-throttle.ts's clampFraction, governor/rate-limit.ts's finiteNonNegativeInt).
  • applyContributorCalibration's cold-start guard must treat a non-finite calibration.sampleSize as "insufficient history" (i.e. return baselineReadinessScore unchanged), not fall through to computing an adjustment. Change the guard's condition so calibration.sampleSize < MIN_CALIBRATION_SAMPLES is only trusted when Number.isFinite(calibration.sampleSize) is true; a non-finite sampleSize must take the same "cold start" branch.
  • After this fix, applyContributorCalibration(70, { sampleSize: NaN, agreementRate: 0.9 }) must return 70 (the unchanged baseline), and applyContributorCalibration(70, { sampleSize: 20, agreementRate: NaN }) must return a finite number in [0, 100] (not NaN).
  • Do not change the function's behavior for any already-tested finite input (the existing "CLAMP BOUNDARY" tests in test/unit/predicted-gate.test.ts must continue to pass unchanged).

Deliverables

  • clamp() in packages/loopover-engine/src/signals/contributor-calibration.ts guards against non-finite input.
  • applyContributorCalibration's cold-start guard treats a non-finite sampleSize as insufficient history.
  • New unit tests in test/unit/predicted-gate.test.ts (alongside the existing applyContributorCalibration (#2349) describe block) covering: NaN sampleSize with a finite agreementRate, and a finite sampleSize with a NaN agreementRate.

Test Coverage Requirements

This repo's Codecov patch gate is 99%+ for src/**/packages/**. Both new branches (non-finite sampleSize, non-finite agreementRate) must be covered by new assertions in test/unit/predicted-gate.test.ts.

Expected Outcome

applyContributorCalibration can never return NaN, and a malformed/non-finite sampleSize is treated as insufficient calibration history (baseline returned unchanged) rather than silently applying a calibration adjustment — matching the function's own documented "malformed upstream aggregate can never push the adjustment past its own clamp" contract.

Links & Resources

  • packages/loopover-engine/src/signals/contributor-calibration.ts
  • test/unit/predicted-gate.test.ts (describe("applyContributorCalibration (#2349)"), starting around line 556)
  • Precedent for the fail-toward-safe-end normalization pattern: packages/loopover-engine/src/governor/reputation-throttle.ts's clampFraction, packages/loopover-engine/src/governor/rate-limit.ts's finiteNonNegativeInt

Activity

  1. added
    help wantedExtra attention is needed
    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.
    on Jul 16, 2026
  2. linear-code commented on Jul 16, 2026

    @linear-code
  3. ghost closed this as completedin #6651on Jul 16, 2026
  4. added a commit that references this issue on Jul 16, 2026
    d4b5a81
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions