{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../src/core/routing/drift.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,MAAM,cAAc,GACvB,SAAS,GACT,MAAM,GACN,SAAS,GACT,iBAAiB,GACjB,WAAW,GACX,mBAAmB,GACnB,WAAW,GACX,kBAAkB,CAAC;AAEtB,QAAA,MAAM,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,WAAW,CASvD,CAAC;AAEF,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,IAAI,WAAW,CAQ9C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAKpH;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EACnC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAC/B,WAAW,CA6Db;AAED,OAAO,EAAE,cAAc,IAAI,oBAAoB,EAAE,CAAC","sourcesContent":["/**\n * Deterministic bounded drift detection.\n *\n * Detectors are deterministic and bounded; drift never automatically changes\n * the active policy. Drift generates evaluation recommendations. Safety drift\n * is high priority. Minimum sample counts are enforced and no unsupported\n * statistical certainty is claimed.\n */\n\nimport { appendDriftSample, appendEvent, readDriftSamples } from \"./store.js\";\nimport type { DriftConfig, DriftResult } from \"./types.js\";\n\nexport type DriftDimension =\n\t| \"quality\"\n\t| \"cost\"\n\t| \"latency\"\n\t| \"failure_cluster\"\n\t| \"retrieval\"\n\t| \"task_distribution\"\n\t| \"flakiness\"\n\t| \"policy_selection\";\n\nconst DEFAULT_CONFIG: Record<DriftDimension, DriftConfig> = {\n\tquality: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.15, enabled: true },\n\tcost: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.25, enabled: true },\n\tlatency: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.3, enabled: true },\n\tfailure_cluster: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.2, enabled: true },\n\tretrieval: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.2, enabled: true },\n\ttask_distribution: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.25, enabled: true },\n\tflakiness: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.15, enabled: true },\n\tpolicy_selection: { method: \"fixed_threshold\", windowSize: 30, minSampleCount: 10, threshold: 0.2, enabled: true },\n};\n\nexport interface DriftHealth {\n\tok: boolean;\n\treasons: string[];\n}\n\n/** Check drift detector health (read-only). */\nexport function checkDriftHealth(): DriftHealth {\n\tconst reasons: string[] = [];\n\tfor (const [dim, cfg] of Object.entries(DEFAULT_CONFIG)) {\n\t\tif (!cfg.enabled) continue;\n\t\tif (cfg.windowSize < cfg.minSampleCount) reasons.push(`${dim}: window < min samples`);\n\t\tif (cfg.threshold <= 0) reasons.push(`${dim}: threshold must be > 0`);\n\t}\n\treturn { ok: reasons.length === 0, reasons };\n}\n\n/**\n * Record a sample for a dimension and detect drift over the trailing window.\n * Compares the last half of the window against the first half using a simple\n * deterministic mean-delta detector (documented as such).\n */\nexport function detectDrift(dimension: DriftDimension, value: number, config: Partial<DriftConfig> = {}): DriftResult {\n\tconst cfg: DriftConfig = { ...DEFAULT_CONFIG[dimension], ...config };\n\tappendDriftSample(dimension, value);\n\tconst samples = readDriftSamples(dimension).slice(-cfg.windowSize);\n\treturn computeDrift(dimension, samples, cfg);\n}\n\n/**\n * Pure deterministic drift computation over an explicit sample window. This is\n * the unit of testing: it never reads persistent storage, so results are fully\n * reproducible and do not depend on previously recorded samples.\n */\nexport function computeDrift(\n\tdimension: DriftDimension,\n\tsamples: { t: number; v: number }[],\n\tconfig: Partial<DriftConfig> = {},\n): DriftResult {\n\tconst cfg: DriftConfig = { ...DEFAULT_CONFIG[dimension], ...config };\n\tconst sampleCount = samples.length;\n\tconst observedAt = new Date().toISOString();\n\n\tif (sampleCount < cfg.minSampleCount) {\n\t\treturn {\n\t\t\tdetectorId: `detector-${dimension}`,\n\t\t\tmethod: cfg.method,\n\t\t\tdimension,\n\t\t\tsampleWindow: cfg.windowSize,\n\t\t\tminSampleCount: cfg.minSampleCount,\n\t\t\tsampleCount,\n\t\t\tdriftDetected: false,\n\t\t\tmeasure: 0,\n\t\t\tthreshold: cfg.threshold,\n\t\t\tseverity: \"low\",\n\t\t\tobservedAt,\n\t\t\trecommendation: [\"insufficient samples; collecting more\"],\n\t\t};\n\t}\n\n\tconst half = Math.floor(sampleCount / 2);\n\tconst early = samples.slice(0, half);\n\tconst late = samples.slice(half);\n\tconst mean = (arr: { v: number }[]): number => (arr.length ? arr.reduce((s, x) => s + x.v, 0) / arr.length : 0);\n\tconst earlyMean = mean(early);\n\tconst lateMean = mean(late);\n\tconst measure = Math.abs(lateMean - earlyMean);\n\n\tconst driftDetected = measure > cfg.threshold;\n\n\t// Severity: safety/quality drift is higher priority.\n\tlet severity: DriftResult[\"severity\"] = \"low\";\n\tif (driftDetected)\n\t\tseverity = dimension === \"quality\" ? \"high\" : dimension === \"cost\" || dimension === \"latency\" ? \"medium\" : \"low\";\n\n\tconst recommendation = driftDetected\n\t\t? [\"re-evaluate routing policy\", \"schedule evaluation pack re-run for this dimension\"]\n\t\t: [];\n\n\tif (driftDetected) {\n\t\tappendEvent({\n\t\t\ttype: \"ROUTING_DRIFT_DETECTED\",\n\t\t\tpayload: { dimension, driftDetected, observedAt },\n\t\t});\n\t}\n\treturn {\n\t\tdetectorId: `detector-${dimension}`,\n\t\tmethod: cfg.method,\n\t\tdimension,\n\t\tsampleWindow: cfg.windowSize,\n\t\tminSampleCount: cfg.minSampleCount,\n\t\tsampleCount,\n\t\tdriftDetected,\n\t\tmeasure: Number(measure.toFixed(4)),\n\t\tthreshold: cfg.threshold,\n\t\tseverity,\n\t\tobservedAt,\n\t\trecommendation,\n\t};\n}\n\nexport { DEFAULT_CONFIG as DRIFT_DEFAULT_CONFIG };\n"]}