UNPKG

@claude-flow/embeddings

Version:

V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking

138 lines • 5.75 kB
/** * ADR-121 Phase 23 — Trend visualization + drift detection (BEYOND SOTA). * * Phase 22's regression check catches single-step drops well — if a * commit drops recall@5 by 15%, CI fails. But it misses the **death * by a thousand cuts** failure mode: every commit drops 1%, every * check passes the 10% threshold, but cumulative drift after 30 * commits is 30%. * * This module ships two pieces: * 1. `renderSparkline(values)` — zero-dep ASCII sparkline using * the standard 8-level block-element ramp. Turns a sequence of * metric values into a one-line trend visualization for CI * output, PR comments, and human dashboards. * 2. `detectDrift(values, options)` — rolling-window drift check. * Compares the mean of the EARLIEST window to the mean of the * LATEST window; reports the relative change. Designed to fire * when cumulative drift exceeds the threshold even if every * single-step delta was below the per-step threshold. * * Composable with Phase 22's `checkRegression`: production CI typically * wants BOTH — single-step regression detection (catches obvious * breaks) AND drift detection (catches slow rot). */ /** * Render a sequence of numeric values as an ASCII sparkline using * the 8-level block-element ramp: ▁▂▃▄▅▆▇█ * * - Maps min(values) → ▁ and max(values) → █, linearly interpolated. * - When all values are identical, renders a flat line of the * middle character (▄) — visually distinguishable from "no data". * - Returns the empty string for an empty input. */ export function renderSparkline(values) { if (!Array.isArray(values) || values.length === 0) return ''; const ramp = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; const filtered = values.filter(v => Number.isFinite(v)); if (filtered.length === 0) return '?'.repeat(values.length); let min = Infinity; let max = -Infinity; for (const v of filtered) { if (v < min) min = v; if (v > max) max = v; } if (min === max) return ramp[3].repeat(values.length); const span = max - min; return values .map(v => { if (!Number.isFinite(v)) return '?'; const norm = (v - min) / span; const idx = Math.min(ramp.length - 1, Math.max(0, Math.floor(norm * ramp.length))); return ramp[idx]; }) .join(''); } /** * Compute drift between the earliest and latest windows of a numeric * series. Returns `skipped: true` when the series is too short to * compute distinct windows. */ export function detectDrift(values, options = {}) { const windowSize = options.windowSize ?? 3; const threshold = options.threshold ?? 0.15; const direction = options.direction ?? 'higher'; const finite = values.filter(v => Number.isFinite(v)); // Need at least 2 * windowSize samples for distinct early + late windows. if (finite.length < 2 * windowSize) { return { sampleCount: finite.length, earlyMean: 0, lateMean: 0, delta: 0, drift: 0, passed: true, reason: `skipped — only ${finite.length} samples, need ≥${2 * windowSize} for windowSize=${windowSize}`, skipped: true, }; } const earlySlice = finite.slice(0, windowSize); const lateSlice = finite.slice(-windowSize); const earlyMean = earlySlice.reduce((s, v) => s + v, 0) / earlySlice.length; const lateMean = lateSlice.reduce((s, v) => s + v, 0) / lateSlice.length; const delta = lateMean - earlyMean; const drift = earlyMean === 0 ? (lateMean === 0 ? 0 : Infinity) : delta / Math.abs(earlyMean); let passed; let reason; if (direction === 'higher') { if (drift >= -threshold) { passed = true; const sign = drift >= 0 ? '+' : ''; reason = `no significant drift — late mean ${lateMean.toFixed(4)} ${sign}${(drift * 100).toFixed(1)}% vs early mean ${earlyMean.toFixed(4)} (within ${(threshold * 100).toFixed(0)}% drift tolerance for higher-is-better)`; } else { passed = false; reason = `DRIFT DETECTED — late mean ${lateMean.toFixed(4)} dropped ${Math.abs(drift * 100).toFixed(1)}% from early mean ${earlyMean.toFixed(4)} (exceeds ${(threshold * 100).toFixed(0)}% drift threshold over ${finite.length} samples)`; } } else { if (drift <= threshold) { passed = true; const sign = drift >= 0 ? '+' : ''; reason = `no significant drift — late mean ${lateMean.toFixed(4)} ${sign}${(drift * 100).toFixed(1)}% vs early mean ${earlyMean.toFixed(4)} (within ${(threshold * 100).toFixed(0)}% drift tolerance for lower-is-better)`; } else { passed = false; reason = `DRIFT DETECTED — late mean ${lateMean.toFixed(4)} grew ${(drift * 100).toFixed(1)}% from early mean ${earlyMean.toFixed(4)} (exceeds ${(threshold * 100).toFixed(0)}% drift threshold over ${finite.length} samples)`; } } return { sampleCount: finite.length, earlyMean, lateMean, delta, drift, passed, reason, skipped: false, }; } export function summarizeBenchmarkTrend(values, benchmark, metricPath, options = {}) { return { benchmark, metricPath, sparkline: renderSparkline(values), sampleCount: values.length, latestValue: values.length > 0 ? values[values.length - 1] : null, drift: detectDrift(values, options), }; } //# sourceMappingURL=ledger-trends.js.map