All files adapter.ts

100% Statements 45/45
100% Branches 26/26
100% Functions 5/5
100% Lines 45/45

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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86                                                      1x 21x 21x 21x   21x 21x 21x   21x   21x 17x   13x 13x 17x 17x 13x 17x 17x 17x 17x 17x 17x 17x 17x   21x 17x 16x 16x 16x 14x 14x 14x 14x 14x 14x 17x 4x 4x 2x 2x     2x 2x 2x 2x 17x 21x              
/**
 * TimesfmWebAdapter — browser-native IForecaster for @agentix-e/timesfm-web.
 *
 * Standalone module — zero dependencies on @agentix-e/anomaly-detector-core.
 * This ensures the adapter loads in browser contexts where Node.js modules
 * (node:fs, node:module) are unavailable.
 *
 * Uses dynamic import() for optional dependency loading.
 * Falls back to descriptive error when @agentix-e/timesfm-web is not installed.
 */
 
// ── Inline types (subset of @agentix-e/anomaly-detector-core) ──
 
/** A single data point. */
export interface DataPoint { value: number; timestamp: number; dimensions?: Record<string, number>; metadata?: Record<string, unknown> }
 
/** Forecast result. */
export interface ForecastResult { predicted: number[]; q10: number[]; q90: number[]; horizon: number; modelName: string; predictedAt: number; metadata?: Record<string, unknown> }
 
/** Forecaster backend type. */
export type ForecasterType = 'anofox' | 'timesfm' | 'custom'
 
/** Forecaster interface. */
export interface IForecaster { forecast(context: DataPoint[], horizon: number): Promise<ForecastResult>; readonly modelName: string; readonly type: ForecasterType }
 
// ── Implementation ──
 
export class TimesfmWebAdapter implements IForecaster {
  readonly type: ForecasterType = 'timesfm'
  private config: TimesfmWebConfig
  private engine: unknown = null
 
  constructor(config?: TimesfmWebConfig) {
    this.config = { model: 'timesfm-2.5-200m', contextWindow: 1024, horizon: 64, ...config }
  }
 
  get modelName(): string { return 'TimesFM-' + this.config.model }
 
  async forecast(context: DataPoint[], horizon: number): Promise<ForecastResult> {
    const engine = await this.ensureEngine()
    // @ts-expect-error optional dependency type is unknown
    const result = await (engine as Record<string, unknown>).forecast(
      context.map(p => p.value),
      { contextLength: this.config.contextWindow, horizon: horizon ?? this.config.horizon }
    ) as { pointForecast?: Float64Array; quantileForecast?: number[][] }
    return {
      predicted: result.pointForecast ? Array.from(result.pointForecast) : [],
      q10: result.quantileForecast?.[2] ? Array.from(result.quantileForecast[2]) : [],
      q90: result.quantileForecast?.[8] ? Array.from(result.quantileForecast[8]) : [],
      horizon: horizon ?? this.config.horizon,
      modelName: this.modelName,
      predictedAt: Date.now(),
    }
  }
 
  private async ensureEngine(): Promise<unknown> {
    if (this.engine) return this.engine
    try {
      const pkgName = '@agentix-e/timesfm-web'
      const mod = await import(pkgName) as Record<string, unknown>
      this.engine = new (mod.TimesFMWebEngine as new (cfg: Record<string, unknown>) => unknown)({
        modelName: this.config.model,
        maxContext: this.config.contextWindow,
        maxHorizon: this.config.horizon,
      })
      return this.engine
    } catch (err) {
      const msg = (err as Error).message
      if (msg.includes('Cannot find') || msg.includes('ERR_MODULE_NOT_FOUND')) {
        throw new Error(
          'TimesFM Web is not installed. Install it:\n' +
          '  npm install @agentix-e/timesfm-web\n' +
          'Or use anofox-forecast (default).'
        )
      }
      throw err
    }
  }
}
 
export interface TimesfmWebConfig {
  model?: string
  contextWindow?: number
  horizon?: number
}