@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
JavaScript
/**
* 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