All files / calibrate forecast-guided.ts

100% Statements 34/34
100% Branches 15/15
100% Functions 6/6
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60                  1x 9x 9x   9x 219x 219x 219x 219x   219x 217x 2x   219x     219x 219x     219x   219x 219x 9x     1x 27x 27x   27x 252x 252x 252x   27x 33x 15x 15x 33x 27x   264x 264x 264x        
/**
 * ForecastGuidedCalibrator — default calibration mode.
 *
 * Measures how far the actual value deviates from the prediction interval.
 * Residual > 1 means the value is outside the interval.
 * jointConfidence amplifies TRCF confidence when the residual confirms the anomaly.
 */
import type { ICalibrator, DetectionResult, ForecastResult, DataPoint, CalibrationResult, CalibrationMode } from '../types.js'
 
export class ForecastGuidedCalibrator implements ICalibrator {
  readonly mode: CalibrationMode = 'forecast-guided'
  private hitRateTracker = new HitRateTracker()
 
  calibrate(detection: DetectionResult, forecast: ForecastResult, currentPoint: DataPoint): CalibrationResult {
    const predicted = forecast.predicted[0] ?? currentPoint.value
    const q10 = forecast.q10[0] ?? predicted
    const q90 = forecast.q90[0] ?? predicted
    const spread = q90 - q10
 
    const residual = spread > 1e-10
      ? Math.abs(currentPoint.value - predicted) / (spread / 2)
      : Math.abs(currentPoint.value - predicted)
 
    const intervalBreached = currentPoint.value < q10 || currentPoint.value > q90
 
    // TRCF confidence amplified by residual — residual > 3 saturates the bonus
    const residualBonus = Math.min(residual / 3, 1)
    const jointConfidence = clamp(detection.confidence * (1 + residualBonus * 0.5), 0, 1)
 
    // Track hit rate for future calibration rounds
    this.hitRateTracker.record(jointConfidence > 0.7, jointConfidence)
 
    return { mode: 'forecast-guided', residual, jointConfidence, intervalBreached }
  }
}
 
/** Rolling hit-rate tracker for calibration feedback. */
class HitRateTracker {
  private history: { hit: boolean; score: number }[] = []
  private maxSize = 100
 
  record(isHit: boolean, score: number): void {
    this.history.push({ hit: isHit, score })
    if (this.history.length > this.maxSize) this.history.shift()
  }
 
  getHitRate(): number {
    if (this.history.length === 0) return 0.5
    const hits = this.history.filter((h) => h.hit).length
    return hits / this.history.length
  }
}
 
function clamp(v: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, v))
}
 
// Re-export for other calibrators
export { HitRateTracker, clamp }