B1.3: isStable real threshold (config-driven, replaces tautology)

The legacy stdDev < stdDev*2 was always true. New behaviour: stdDev <=
config.calibration.stabilityThreshold OR stdDev === 0. Default
threshold 0.01 in scaling-units. Schema field + editor UI added. 4
BUG-PRESERVED tests rewritten + 4 new edge tests. 101/101 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 17:29:15 +02:00
parent e6e212a504
commit 497f05d92c
4 changed files with 79 additions and 14 deletions

View File

@@ -34,17 +34,50 @@ test('isStable: constant array → stable with stdDev=0', () => {
assert.strictEqual(r.stdDev, 0);
});
test('isStable: high-variance array → original threshold is tautological (preserved)', () => {
// BUG-PRESERVED: original check is `stdDev < stdDev*marginFactor`, which is
// always true for stdDev>0. Length>=2 ⇒ isStable=true regardless of spread.
// See calibrator stdDev-threshold note. We pin the behaviour here so the
// refactor stays byte-equivalent; a separate behavioural PR can fix the rule.
test('isStable: high-variance array under default threshold → unstable', () => {
// Resolved 2026-05-11: config-driven absolute stabilityThreshold replaces
// the old `stdDev < stdDev*marginFactor` tautology. Default threshold is
// 0.01 (scaling-units); a 0..100 spread blows past it.
const { cal } = makeCalibrator([0, 100, 0, 100], {});
const r = cal.isStable();
assert.strictEqual(r.isStable, false);
assert.ok(r.stdDev > 0);
});
test('isStable: high-variance array with relaxed threshold → stable', () => {
const cfg = { calibration: { stabilityThreshold: 100 } };
const { cal } = makeCalibrator([0, 100, 0, 100], cfg);
const r = cal.isStable();
assert.strictEqual(r.isStable, true);
assert.ok(r.stdDev > 0);
});
test('isStable: zero stdDev (constant) is stable regardless of threshold', () => {
const cfg = { calibration: { stabilityThreshold: 0 } };
const { cal } = makeCalibrator([7, 7, 7, 7], cfg);
const r = cal.isStable();
assert.strictEqual(r.isStable, true);
assert.strictEqual(r.stdDev, 0);
});
test('isStable: stdDev just above threshold → unstable', () => {
const cfg = { calibration: { stabilityThreshold: 0.5 } };
// stdDev of [10, 11] = 0.5; nudge the spread up so stdDev > 0.5.
const { cal } = makeCalibrator([10, 12], cfg);
const r = cal.isStable();
assert.strictEqual(r.isStable, false);
assert.ok(r.stdDev > 0.5);
});
test('isStable: missing config.calibration → falls back to default 0.01', () => {
// stdDev of [10, 10.001] ≈ 0.0005, well under the 0.01 default.
const { cal: stable } = makeCalibrator([10, 10.001], {});
assert.strictEqual(stable.isStable().isStable, true);
// stdDev of [10, 10.1] ≈ 0.05, above the 0.01 default.
const { cal: unstable } = makeCalibrator([10, 10.1], {});
assert.strictEqual(unstable.isStable().isStable, false);
});
test('isStable: < 2 values → unstable', () => {
const { cal } = makeCalibrator([42], {});
const r = cal.isStable();
@@ -101,11 +134,22 @@ test('evaluateRepeatability: insufficient data → null', () => {
assert.strictEqual(r.reason, 'insufficient-data');
});
test('evaluateRepeatability: high-variance still returns stdDev (preserved tautology)', () => {
// BUG-PRESERVED: see isStable note. Original rule treats any length>=2
// buffer as stable, so repeatability returns the raw stdDev even when the
// spread is large.
test('evaluateRepeatability: high-variance under default threshold → null', () => {
// Resolved 2026-05-11: with the real stability check in place, a noisy
// buffer fails isStable() and repeatability reports null with reason.
const cfg = { smoothing: { smoothMethod: 'mean' } };
const { cal, logger } = makeCalibrator([0, 50, 0, 50], cfg);
const r = cal.evaluateRepeatability();
assert.strictEqual(r.repeatability, null);
assert.strictEqual(r.reason, 'unstable');
assert.match(logger.calls.warn[0], /not stable/);
});
test('evaluateRepeatability: high-variance with relaxed threshold → returns stdDev', () => {
const cfg = {
smoothing: { smoothMethod: 'mean' },
calibration: { stabilityThreshold: 100 },
};
const { cal } = makeCalibrator([0, 50, 0, 50], cfg);
const r = cal.evaluateRepeatability();
assert.ok(r.repeatability > 0);