mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
fa28ce6bfa
Adds 4 new capabilities with production wiring, 148 tests, and performance baselines: - R1: HDC Pattern Fingerprinting — 10K-bit binary hypervectors with O(1) XOR binding, wired into PatternStore search as Hamming-distance pre-filter - R2: CUSUM Drift Detection — two-sided statistical change-point detection across all 4 coherence gate types (retrieve/write/learn/act), auto-enabled via feature flag - R3: Delta Event Sourcing — pattern version history with rollback via SQLite pattern_deltas table, wired into PatternStore store/recordUsage paths - EWC++ Activation — three-loop protocol (instantAdapt→recordOutcome→backgroundConsolidate) wired in quality-assessment, test-generation, and contract-testing coordinators with double-flag guard (useSONAThreeLoop && useEwcPlusPlusRegularization) Extracts coherence-gate.ts (930 lines) into 4 modules under 500-line limit. All feature flags default true (opt-out) per amended ADR-087 policy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
248 lines
9.2 KiB
TypeScript
248 lines
9.2 KiB
TypeScript
/**
|
|
* EWC++ Activation Tests
|
|
*
|
|
* Tests for:
|
|
* - Fisher Information Matrix round-trip persistence
|
|
* - Multi-domain EWC loss behavior
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
SONAThreeLoopEngine,
|
|
type ThreeLoopConfig,
|
|
} from '../../../../src/integrations/ruvector/sona-three-loop';
|
|
|
|
// ============================================================================
|
|
// Test Helpers
|
|
// ============================================================================
|
|
|
|
function createTestConfig(overrides: Partial<ThreeLoopConfig> = {}): Partial<ThreeLoopConfig> {
|
|
return {
|
|
dimension: 16,
|
|
microLoraLr: 0.01,
|
|
consolidationInterval: 5,
|
|
ewcLambda: 100.0,
|
|
taskBoundaryZScoreThreshold: 2.0,
|
|
fisherDecay: 0.9,
|
|
fisherSampleSize: 50,
|
|
importanceThreshold: 0.001,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create features clustered around a specific pattern.
|
|
* E.g., domainIdx=0 -> [1, 0, 0, ...], domainIdx=1 -> [0, 1, 0, ...],
|
|
* with small noise for variation.
|
|
*/
|
|
function createDomainFeatures(dim: number, domainIdx: number, noise: number = 0.05): number[] {
|
|
const features: number[] = new Array(dim).fill(0);
|
|
// Set the primary dimension for this "domain"
|
|
if (domainIdx < dim) {
|
|
features[domainIdx] = 1.0;
|
|
}
|
|
// Add small noise for realistic variation
|
|
for (let i = 0; i < dim; i++) {
|
|
features[i] += (Math.random() - 0.5) * noise;
|
|
}
|
|
return features;
|
|
}
|
|
|
|
/**
|
|
* Create deterministic features (seed-based, no Math.random).
|
|
*/
|
|
function createSeededFeatures(dim: number, seed: number): number[] {
|
|
const features: number[] = [];
|
|
let s = seed;
|
|
for (let i = 0; i < dim; i++) {
|
|
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
features.push((s / 0x7fffffff) * 2 - 1);
|
|
}
|
|
return features;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Fisher Round-Trip Persistence
|
|
// ============================================================================
|
|
|
|
describe('Fisher round-trip persistence', () => {
|
|
it('should persist and restore Fisher state correctly', () => {
|
|
const config = createTestConfig({ dimension: 16 });
|
|
const engine = new SONAThreeLoopEngine(config);
|
|
|
|
// Run several instantAdapt + recordOutcome cycles with diverse rewards
|
|
for (let i = 0; i < 15; i++) {
|
|
const features = createSeededFeatures(16, i * 7 + 1);
|
|
engine.instantAdapt(features);
|
|
// Use diverse rewards to produce non-trivial gradient variance
|
|
const reward = i % 3 === 0 ? 1.0 : i % 3 === 1 ? -0.5 : 0.3;
|
|
engine.recordOutcome(reward);
|
|
}
|
|
|
|
// Consolidate to update Fisher diagonal
|
|
const consolidation = engine.backgroundConsolidate();
|
|
expect(consolidation.consolidated).toBe(true);
|
|
|
|
// Capture Fisher state from original engine
|
|
const originalFisher = engine.getFisherDiagonal();
|
|
const originalOptimal = engine.getOptimalParams();
|
|
const originalMetrics = engine.getEWCMetrics();
|
|
|
|
// Create a brand-new engine instance
|
|
const newEngine = new SONAThreeLoopEngine(config);
|
|
|
|
// Verify the new engine starts with zero Fisher
|
|
const freshMetrics = newEngine.getEWCMetrics();
|
|
expect(freshMetrics.fisherTrace).toBe(0);
|
|
|
|
// Load the Fisher state from the original engine
|
|
newEngine.loadFisher(originalFisher, originalOptimal);
|
|
|
|
// Verify the restored Fisher matches the original
|
|
const restoredMetrics = newEngine.getEWCMetrics();
|
|
expect(restoredMetrics.fisherTrace).toBeCloseTo(originalMetrics.fisherTrace, 5);
|
|
|
|
const restoredFisher = newEngine.getFisherDiagonal();
|
|
expect(restoredFisher.length).toBe(originalFisher.length);
|
|
for (let i = 0; i < restoredFisher.length; i++) {
|
|
expect(restoredFisher[i]).toBeCloseTo(originalFisher[i], 5);
|
|
}
|
|
});
|
|
|
|
it('should have non-zero Fisher trace after consolidation', () => {
|
|
const config = createTestConfig({
|
|
dimension: 16,
|
|
// Consolidate after every adapt/record pair
|
|
consolidationInterval: 1,
|
|
// Low z-score threshold to ensure task boundary fires
|
|
taskBoundaryZScoreThreshold: 0.5,
|
|
});
|
|
const engine = new SONAThreeLoopEngine(config);
|
|
|
|
// Phase 1: Build stable gradient history (need 5+ calls to detectTaskBoundary)
|
|
// Each consolidation calls detectTaskBoundary once, building history
|
|
for (let i = 0; i < 6; i++) {
|
|
engine.instantAdapt(createSeededFeatures(16, i + 100));
|
|
engine.recordOutcome(0.5); // uniform reward -> stable gradient magnitudes
|
|
engine.backgroundConsolidate(); // Each call adds one entry to gradient history
|
|
}
|
|
|
|
// Phase 2: Shock the gradient to trigger task boundary
|
|
// Now gradientHistory has 6 entries; this 7th call will compute z-score
|
|
engine.instantAdapt(createSeededFeatures(16, 999));
|
|
engine.recordOutcome(50.0); // extreme reward shock
|
|
engine.backgroundConsolidate(); // Should detect task boundary now
|
|
|
|
const metrics = engine.getEWCMetrics();
|
|
// Fisher trace should be non-zero after task boundary triggers updateFisher
|
|
expect(metrics.fisherTrace).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Multi-Domain EWC Loss
|
|
// ============================================================================
|
|
|
|
describe('Multi-domain EWC loss', () => {
|
|
it('should show EWC loss > 0 when training on new domain after consolidation', () => {
|
|
const dim = 16;
|
|
const config = createTestConfig({
|
|
dimension: dim,
|
|
// Consolidate after every pair to build gradient history
|
|
consolidationInterval: 1,
|
|
ewcLambda: 100.0,
|
|
taskBoundaryZScoreThreshold: 0.5,
|
|
});
|
|
const engine = new SONAThreeLoopEngine(config);
|
|
|
|
// -------- Domain A: build gradient history, then trigger task boundary --------
|
|
// Build stable baseline (6 entries in detectTaskBoundary history)
|
|
for (let i = 0; i < 6; i++) {
|
|
engine.instantAdapt(createDomainFeatures(dim, 0, 0.01));
|
|
engine.recordOutcome(0.5);
|
|
engine.backgroundConsolidate();
|
|
}
|
|
// Shock to trigger task boundary -> updateFisher captures Domain A Fisher
|
|
engine.instantAdapt(createDomainFeatures(dim, 0, 0.01));
|
|
engine.recordOutcome(50.0);
|
|
engine.backgroundConsolidate();
|
|
|
|
// Verify Fisher is now non-zero
|
|
const fisherAfterA = engine.getFisherDiagonal();
|
|
const traceA = fisherAfterA.reduce((sum, v) => sum + v, 0);
|
|
expect(traceA).toBeGreaterThan(0);
|
|
|
|
// -------- Domain B: different feature direction --------
|
|
// Train with different features; each consolidation applies EWC regularization
|
|
for (let i = 0; i < 6; i++) {
|
|
engine.instantAdapt(createDomainFeatures(dim, 1, 0.01));
|
|
engine.recordOutcome(0.3);
|
|
engine.backgroundConsolidate();
|
|
}
|
|
// Shock again to trigger second task boundary
|
|
engine.instantAdapt(createDomainFeatures(dim, 1, 0.01));
|
|
engine.recordOutcome(40.0);
|
|
const consolidationB = engine.backgroundConsolidate();
|
|
expect(consolidationB.consolidated).toBe(true);
|
|
|
|
// After Domain B training, EWC loss should be positive because
|
|
// the parameters diverged from Domain A's optimal point.
|
|
// ewcLossBefore captures the loss at the start of consolidation,
|
|
// before regularization pulls weights back toward optimal.
|
|
// ewcLossAfter captures the loss after regularization correction.
|
|
// At least the "before" loss should be positive if parameters moved.
|
|
const metricsAfterB = engine.getEWCMetrics();
|
|
const ewcLossAfterB = metricsAfterB.regularizationLoss;
|
|
const domainBLossBefore = consolidationB.ewcLossBefore;
|
|
const domainBLossAfter = consolidationB.ewcLossAfter;
|
|
|
|
expect(
|
|
ewcLossAfterB > 0 || domainBLossBefore > 0 || domainBLossAfter > 0
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should preserve Domain A knowledge via Fisher weighting', () => {
|
|
const dim = 16;
|
|
const config = createTestConfig({
|
|
dimension: dim,
|
|
consolidationInterval: 1,
|
|
ewcLambda: 1000.0,
|
|
taskBoundaryZScoreThreshold: 0.5,
|
|
});
|
|
const engine = new SONAThreeLoopEngine(config);
|
|
|
|
// Domain A: build gradient history, then trigger task boundary
|
|
for (let i = 0; i < 6; i++) {
|
|
engine.instantAdapt(createDomainFeatures(dim, 0, 0.01));
|
|
engine.recordOutcome(0.5);
|
|
engine.backgroundConsolidate();
|
|
}
|
|
engine.instantAdapt(createDomainFeatures(dim, 0, 0.01));
|
|
engine.recordOutcome(50.0);
|
|
engine.backgroundConsolidate();
|
|
|
|
const fisherAfterA = engine.getFisherDiagonal();
|
|
const fisherTraceA = fisherAfterA.reduce((sum, v) => sum + v, 0);
|
|
|
|
// Fisher trace should be positive after Domain A (task boundary triggered updateFisher)
|
|
expect(fisherTraceA).toBeGreaterThan(0);
|
|
|
|
// Domain B: different direction, trigger another task boundary
|
|
for (let i = 0; i < 6; i++) {
|
|
engine.instantAdapt(createDomainFeatures(dim, 1, 0.01));
|
|
engine.recordOutcome(0.3);
|
|
engine.backgroundConsolidate();
|
|
}
|
|
engine.instantAdapt(createDomainFeatures(dim, 1, 0.01));
|
|
engine.recordOutcome(40.0);
|
|
engine.backgroundConsolidate();
|
|
|
|
// Fisher trace should have been blended (EWC++ online update)
|
|
const fisherAfterB = engine.getFisherDiagonal();
|
|
const fisherTraceB = fisherAfterB.reduce((sum, v) => sum + v, 0);
|
|
|
|
// Fisher trace should still be positive (blended via EWC++ decay, not overwritten)
|
|
expect(fisherTraceB).toBeGreaterThan(0);
|
|
});
|
|
});
|