1162 lines
35 KiB
JavaScript
1162 lines
35 KiB
JavaScript
/**
|
|
* RuVector Intelligence Layer v2 for Claude Code
|
|
*
|
|
* Enhanced with:
|
|
* 1. Native HNSW rebuild on startup (150x faster search)
|
|
* 2. Hyperbolic distance for hierarchical embeddings
|
|
* 3. Confidence Calibration (track predicted vs actual)
|
|
* 4. A/B Testing (holdout group comparison)
|
|
* 5. Feedback Loop (learn from followed/ignored suggestions)
|
|
* 6. Active Learning (identify uncertain states)
|
|
* 7. Pattern Decay (time-weighted trajectories)
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { createHash } from 'crypto';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DATA_DIR = join(__dirname, 'data');
|
|
const MEMORY_FILE = join(DATA_DIR, 'memory.json');
|
|
const TRAJECTORIES_FILE = join(DATA_DIR, 'trajectories.json');
|
|
const PATTERNS_FILE = join(DATA_DIR, 'patterns.json');
|
|
const CALIBRATION_FILE = join(DATA_DIR, 'calibration.json');
|
|
const FEEDBACK_FILE = join(DATA_DIR, 'feedback.json');
|
|
const ERROR_PATTERNS_FILE = join(DATA_DIR, 'error-patterns.json');
|
|
const SEQUENCES_FILE = join(DATA_DIR, 'sequences.json');
|
|
|
|
// Ensure data directory exists
|
|
if (!existsSync(DATA_DIR)) {
|
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
// Try to load @ruvector/core VectorDB
|
|
let VectorDB = null;
|
|
let ruvectorAvailable = false;
|
|
|
|
try {
|
|
const ruvector = await import('@ruvector/core');
|
|
VectorDB = ruvector.VectorDB;
|
|
ruvectorAvailable = true;
|
|
console.error('✅ @ruvector/core loaded - using native HNSW vector search');
|
|
} catch (e) {
|
|
console.error('⚠️ @ruvector/core not available, using fallback cosine similarity');
|
|
}
|
|
|
|
// Try to load attention WASM for hyperbolic distance
|
|
let attentionWasm = null;
|
|
try {
|
|
attentionWasm = await import('../../crates/ruvector-attention-wasm/pkg/ruvector_attention_wasm.js');
|
|
console.error('✅ Hyperbolic attention WASM loaded');
|
|
} catch (e) {
|
|
// Hyperbolic not available - use fallback
|
|
}
|
|
|
|
/**
|
|
* Hyperbolic distance in Poincaré ball model
|
|
* Better for hierarchical/tree-like data (crates, packages, file paths)
|
|
*/
|
|
function poincareDistance(u, v, c = 1.0) {
|
|
const EPS = 1e-7;
|
|
const sqrtC = Math.sqrt(c);
|
|
|
|
let normDiffSq = 0, normUSq = 0, normVSq = 0;
|
|
for (let i = 0; i < u.length; i++) {
|
|
const diff = u[i] - (v[i] || 0);
|
|
normDiffSq += diff * diff;
|
|
normUSq += u[i] * u[i];
|
|
normVSq += (v[i] || 0) * (v[i] || 0);
|
|
}
|
|
|
|
const lambdaU = 1.0 - c * normUSq;
|
|
const lambdaV = 1.0 - c * normVSq;
|
|
const numerator = 2.0 * c * normDiffSq;
|
|
const denominator = Math.max(EPS, lambdaU * lambdaV);
|
|
|
|
const arg = Math.max(1.0, 1.0 + numerator / denominator);
|
|
return (1.0 / sqrtC) * Math.acosh(arg);
|
|
}
|
|
|
|
/**
|
|
* Text to embedding with hierarchical awareness
|
|
*/
|
|
function textToEmbedding(text, dims = 128) {
|
|
const embedding = new Float32Array(dims).fill(0);
|
|
const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ');
|
|
const words = normalized.split(/\s+/).filter(w => w.length > 1);
|
|
|
|
const wordFreq = {};
|
|
for (const word of words) {
|
|
wordFreq[word] = (wordFreq[word] || 0) + 1;
|
|
}
|
|
|
|
for (const [word, freq] of Object.entries(wordFreq)) {
|
|
const hash = createHash('sha256').update(word).digest();
|
|
const idfWeight = 1 / Math.log(1 + freq);
|
|
for (let i = 0; i < dims; i++) {
|
|
const byteIdx = i % hash.length;
|
|
const val = ((hash[byteIdx] & 0xFF) / 127.5) - 1;
|
|
embedding[i] += val * idfWeight;
|
|
}
|
|
}
|
|
|
|
// L2 normalize
|
|
const magnitude = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
|
|
if (magnitude > 0) {
|
|
for (let i = 0; i < dims; i++) embedding[i] /= magnitude;
|
|
}
|
|
|
|
// Scale down to fit in Poincaré ball (|x| < 1)
|
|
const maxNorm = 0.95;
|
|
for (let i = 0; i < dims; i++) embedding[i] *= maxNorm;
|
|
|
|
return Array.from(embedding);
|
|
}
|
|
|
|
/**
|
|
* Cosine similarity (fallback)
|
|
*/
|
|
function cosineSimilarity(a, b) {
|
|
let dot = 0, magA = 0, magB = 0;
|
|
for (let i = 0; i < a.length; i++) {
|
|
dot += a[i] * (b[i] || 0);
|
|
magA += a[i] * a[i];
|
|
magB += (b[i] || 0) * (b[i] || 0);
|
|
}
|
|
return dot / (Math.sqrt(magA) * Math.sqrt(magB) || 1);
|
|
}
|
|
|
|
/**
|
|
* Vector Memory with Native HNSW + Hyperbolic distance option
|
|
*/
|
|
class VectorMemory {
|
|
constructor(options = {}) {
|
|
this.useHyperbolic = options.hyperbolic ?? true;
|
|
this.curvature = options.curvature ?? 1.0;
|
|
this.db = null;
|
|
this.memories = this.loadMemories();
|
|
this.dimensions = 128;
|
|
}
|
|
|
|
loadMemories() {
|
|
if (existsSync(MEMORY_FILE)) {
|
|
try { return JSON.parse(readFileSync(MEMORY_FILE, 'utf-8')); }
|
|
catch { return []; }
|
|
}
|
|
return [];
|
|
}
|
|
|
|
saveMemories() {
|
|
writeFileSync(MEMORY_FILE, JSON.stringify(this.memories, null, 2));
|
|
}
|
|
|
|
async init() {
|
|
if (ruvectorAvailable && VectorDB && !this.db) {
|
|
try {
|
|
this.db = new VectorDB({
|
|
dimensions: this.dimensions,
|
|
distanceMetric: 'Cosine', // Native HNSW uses cosine, we post-process with hyperbolic
|
|
hnswConfig: { m: 16, efConstruction: 200, efSearch: 100, maxElements: 50000 }
|
|
});
|
|
|
|
// Rebuild index from stored memories
|
|
let rebuilt = 0;
|
|
for (const mem of this.memories) {
|
|
if (mem.embedding) {
|
|
await this.db.insert({ id: mem.id, vector: new Float32Array(mem.embedding) });
|
|
rebuilt++;
|
|
}
|
|
}
|
|
console.error(`📊 VectorDB rebuilt with ${rebuilt} memories (HNSW index ready)`);
|
|
} catch (e) {
|
|
console.error('VectorDB init failed:', e.message);
|
|
this.db = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
async store(type, content, metadata = {}) {
|
|
const id = `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
const embedding = textToEmbedding(content, this.dimensions);
|
|
|
|
const memory = {
|
|
id, type, content, embedding,
|
|
metadata: { ...metadata, timestamp: new Date().toISOString() }
|
|
};
|
|
|
|
this.memories.push(memory);
|
|
if (this.db) {
|
|
try { await this.db.insert({ id, vector: new Float32Array(embedding) }); }
|
|
catch (e) { /* fallback works */ }
|
|
}
|
|
|
|
this.saveMemories();
|
|
return id;
|
|
}
|
|
|
|
async search(query, limit = 5) {
|
|
const queryEmbedding = textToEmbedding(query, this.dimensions);
|
|
|
|
// Use native HNSW for candidate retrieval
|
|
let candidates = this.memories;
|
|
if (this.db) {
|
|
try {
|
|
const results = await this.db.search({
|
|
vector: new Float32Array(queryEmbedding),
|
|
k: Math.min(limit * 3, 50) // Get more candidates for reranking
|
|
});
|
|
candidates = results.map(r => this.memories.find(m => m.id === r.id)).filter(Boolean);
|
|
} catch (e) { /* use all memories */ }
|
|
}
|
|
|
|
// Rerank with hyperbolic distance if enabled
|
|
const scored = candidates.map(mem => {
|
|
let score;
|
|
if (this.useHyperbolic && mem.embedding) {
|
|
const dist = poincareDistance(queryEmbedding, mem.embedding, this.curvature);
|
|
score = 1 / (1 + dist); // Convert distance to similarity
|
|
} else {
|
|
score = cosineSimilarity(queryEmbedding, mem.embedding || []);
|
|
}
|
|
return { ...mem, score };
|
|
});
|
|
|
|
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
}
|
|
|
|
getStats() {
|
|
const typeCount = {};
|
|
for (const mem of this.memories) {
|
|
typeCount[mem.type] = (typeCount[mem.type] || 0) + 1;
|
|
}
|
|
return {
|
|
total: this.memories.length,
|
|
byType: typeCount,
|
|
usingNativeHNSW: !!this.db,
|
|
usingHyperbolic: this.useHyperbolic
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calibration Tracker - measures if confidence matches reality
|
|
*/
|
|
class CalibrationTracker {
|
|
constructor() {
|
|
this.data = this.load();
|
|
}
|
|
|
|
load() {
|
|
if (existsSync(CALIBRATION_FILE)) {
|
|
try { return JSON.parse(readFileSync(CALIBRATION_FILE, 'utf-8')); }
|
|
catch { return { buckets: {}, predictions: [] }; }
|
|
}
|
|
return { buckets: {}, predictions: [] };
|
|
}
|
|
|
|
save() {
|
|
writeFileSync(CALIBRATION_FILE, JSON.stringify(this.data, null, 2));
|
|
}
|
|
|
|
record(predicted, actual, confidence) {
|
|
const correct = predicted === actual;
|
|
const bucket = Math.floor(confidence * 10) / 10; // 0.0, 0.1, ..., 0.9
|
|
|
|
if (!this.data.buckets[bucket]) {
|
|
this.data.buckets[bucket] = { total: 0, correct: 0 };
|
|
}
|
|
this.data.buckets[bucket].total++;
|
|
if (correct) this.data.buckets[bucket].correct++;
|
|
|
|
this.data.predictions.push({
|
|
predicted, actual, correct, confidence,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Keep last 500 predictions
|
|
if (this.data.predictions.length > 500) {
|
|
this.data.predictions = this.data.predictions.slice(-500);
|
|
}
|
|
|
|
this.save();
|
|
return correct;
|
|
}
|
|
|
|
getCalibrationError() {
|
|
let totalError = 0, count = 0;
|
|
for (const [bucket, { total, correct }] of Object.entries(this.data.buckets)) {
|
|
if (total >= 5) {
|
|
const expectedRate = parseFloat(bucket) + 0.05;
|
|
const actualRate = correct / total;
|
|
totalError += Math.abs(expectedRate - actualRate);
|
|
count++;
|
|
}
|
|
}
|
|
return count > 0 ? totalError / count : 0;
|
|
}
|
|
|
|
getStats() {
|
|
const stats = {};
|
|
for (const [bucket, { total, correct }] of Object.entries(this.data.buckets)) {
|
|
stats[bucket] = {
|
|
total,
|
|
accuracy: (correct / total).toFixed(3),
|
|
expected: (parseFloat(bucket) + 0.05).toFixed(2)
|
|
};
|
|
}
|
|
return { buckets: stats, calibrationError: this.getCalibrationError().toFixed(3) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Feedback Loop - track if suggestions were followed
|
|
*/
|
|
class FeedbackLoop {
|
|
constructor() {
|
|
this.data = this.load();
|
|
}
|
|
|
|
load() {
|
|
if (existsSync(FEEDBACK_FILE)) {
|
|
try { return JSON.parse(readFileSync(FEEDBACK_FILE, 'utf-8')); }
|
|
catch { return { suggestions: [], followRates: {} }; }
|
|
}
|
|
return { suggestions: [], followRates: {} };
|
|
}
|
|
|
|
save() {
|
|
writeFileSync(FEEDBACK_FILE, JSON.stringify(this.data, null, 2));
|
|
}
|
|
|
|
recordSuggestion(suggestionId, suggested, confidence) {
|
|
this.data.suggestions.push({
|
|
id: suggestionId,
|
|
suggested,
|
|
confidence,
|
|
followed: null,
|
|
outcome: null,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
this.save();
|
|
return suggestionId;
|
|
}
|
|
|
|
recordOutcome(suggestionId, actualUsed, success) {
|
|
const suggestion = this.data.suggestions.find(s => s.id === suggestionId);
|
|
if (suggestion) {
|
|
suggestion.followed = suggestion.suggested === actualUsed;
|
|
suggestion.outcome = success;
|
|
|
|
// Update follow rates
|
|
const key = suggestion.suggested;
|
|
if (!this.data.followRates[key]) {
|
|
this.data.followRates[key] = { total: 0, followed: 0, followedSuccess: 0, ignoredSuccess: 0 };
|
|
}
|
|
const r = this.data.followRates[key];
|
|
r.total++;
|
|
if (suggestion.followed) {
|
|
r.followed++;
|
|
if (success) r.followedSuccess++;
|
|
} else {
|
|
if (success) r.ignoredSuccess++;
|
|
}
|
|
|
|
this.save();
|
|
}
|
|
}
|
|
|
|
getAdviceValue() {
|
|
const result = {};
|
|
for (const [key, r] of Object.entries(this.data.followRates)) {
|
|
if (r.total >= 5) {
|
|
const followRate = r.followed / r.total;
|
|
const followedSuccessRate = r.followed > 0 ? r.followedSuccess / r.followed : 0;
|
|
const ignoredSuccessRate = (r.total - r.followed) > 0
|
|
? r.ignoredSuccess / (r.total - r.followed) : 0;
|
|
|
|
result[key] = {
|
|
followRate: followRate.toFixed(3),
|
|
followedSuccessRate: followedSuccessRate.toFixed(3),
|
|
ignoredSuccessRate: ignoredSuccessRate.toFixed(3),
|
|
adviceValue: (followedSuccessRate - ignoredSuccessRate).toFixed(3)
|
|
};
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ReasoningBank with A/B Testing, Decay, and Active Learning
|
|
*/
|
|
class ReasoningBank {
|
|
constructor() {
|
|
this.trajectories = this.loadTrajectories();
|
|
this.qTable = this.loadPatterns();
|
|
this.alpha = 0.1;
|
|
this.gamma = 0.9;
|
|
this.epsilon = 0.1;
|
|
// A/B testing: Use environment override, or persistent session-based assignment
|
|
// INTELLIGENCE_MODE=treatment forces learning mode (for development/testing)
|
|
// INTELLIGENCE_MODE=control forces control mode (for baseline comparison)
|
|
this.abTestGroup = process.env.INTELLIGENCE_MODE ||
|
|
(this.getSessionId() % 100 < 5 ? 'control' : 'treatment'); // 5% holdout
|
|
this.decayHalfLife = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
|
|
}
|
|
|
|
loadTrajectories() {
|
|
if (existsSync(TRAJECTORIES_FILE)) {
|
|
try { return JSON.parse(readFileSync(TRAJECTORIES_FILE, 'utf-8')); }
|
|
catch { return []; }
|
|
}
|
|
return [];
|
|
}
|
|
|
|
loadPatterns() {
|
|
if (existsSync(PATTERNS_FILE)) {
|
|
try { return JSON.parse(readFileSync(PATTERNS_FILE, 'utf-8')); }
|
|
catch { return {}; }
|
|
}
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Get persistent session ID for consistent A/B assignment
|
|
* Uses process PID + startup time hash for session-stable assignment
|
|
*/
|
|
getSessionId() {
|
|
// Combine PID with a time bucket (hourly) for session-stable but varied assignment
|
|
const hourBucket = Math.floor(Date.now() / (60 * 60 * 1000));
|
|
return (process.pid || 0) + hourBucket;
|
|
}
|
|
|
|
save() {
|
|
writeFileSync(TRAJECTORIES_FILE, JSON.stringify(this.trajectories.slice(-1000), null, 2));
|
|
writeFileSync(PATTERNS_FILE, JSON.stringify(this.qTable, null, 2));
|
|
}
|
|
|
|
stateKey(state) {
|
|
// Preserve hyphens in crate names (e.g., ruvector-core, micro-hnsw-wasm)
|
|
return state.toLowerCase().replace(/[^a-z0-9-]+/g, '_').slice(0, 80);
|
|
}
|
|
|
|
/**
|
|
* Calculate decay weight based on trajectory age
|
|
*/
|
|
getDecayWeight(timestamp) {
|
|
const age = Date.now() - new Date(timestamp).getTime();
|
|
return Math.pow(0.5, age / this.decayHalfLife);
|
|
}
|
|
|
|
/**
|
|
* Record trajectory with time-weighted learning
|
|
*/
|
|
recordTrajectory(state, action, outcome, reward) {
|
|
const stateKey = this.stateKey(state);
|
|
const trajectory = {
|
|
id: `traj-${Date.now()}`,
|
|
state: stateKey,
|
|
action, outcome, reward,
|
|
timestamp: new Date().toISOString(),
|
|
abGroup: this.abTestGroup
|
|
};
|
|
this.trajectories.push(trajectory);
|
|
|
|
// Time-weighted Q-learning with decay
|
|
if (!this.qTable[stateKey]) this.qTable[stateKey] = { _meta: { lastUpdate: null, updateCount: 0 } };
|
|
|
|
const meta = this.qTable[stateKey]._meta || { lastUpdate: null, updateCount: 0 };
|
|
const decayWeight = meta.lastUpdate ? this.getDecayWeight(meta.lastUpdate) : 1.0;
|
|
|
|
// Decayed current Q + new update
|
|
const currentQ = (this.qTable[stateKey][action] || 0) * decayWeight;
|
|
const updateCount = (meta.updateCount || 0) + 1;
|
|
const adaptiveLR = Math.max(0.01, this.alpha / Math.sqrt(updateCount));
|
|
|
|
this.qTable[stateKey][action] = Math.min(0.8, Math.max(-0.5,
|
|
currentQ + adaptiveLR * (reward - currentQ)
|
|
));
|
|
|
|
this.qTable[stateKey]._meta = {
|
|
lastUpdate: new Date().toISOString(),
|
|
updateCount
|
|
};
|
|
|
|
this.save();
|
|
return trajectory.id;
|
|
}
|
|
|
|
/**
|
|
* Get best action with A/B testing and active learning
|
|
*/
|
|
getBestAction(state, availableActions) {
|
|
const stateKey = this.stateKey(state);
|
|
const qValues = this.qTable[stateKey] || {};
|
|
|
|
// A/B Testing: Control group gets random actions
|
|
if (this.abTestGroup === 'control') {
|
|
const action = availableActions[Math.floor(Math.random() * availableActions.length)];
|
|
return { action, confidence: 0, reason: 'control-group', qValues, abGroup: 'control' };
|
|
}
|
|
|
|
// Exploration with probability ε
|
|
if (Math.random() < this.epsilon) {
|
|
const action = availableActions[Math.floor(Math.random() * availableActions.length)];
|
|
return { action, confidence: 0, reason: 'exploration', qValues, abGroup: 'treatment' };
|
|
}
|
|
|
|
// Exploitation
|
|
let bestAction = availableActions[0];
|
|
let bestQ = -Infinity;
|
|
let secondBestQ = -Infinity;
|
|
|
|
for (const action of availableActions) {
|
|
const q = qValues[action] || 0;
|
|
if (q > bestQ) {
|
|
secondBestQ = bestQ;
|
|
bestQ = q;
|
|
bestAction = action;
|
|
} else if (q > secondBestQ) {
|
|
secondBestQ = q;
|
|
}
|
|
}
|
|
|
|
const confidence = 1 / (1 + Math.exp(-bestQ * 2));
|
|
|
|
// Active Learning: flag uncertain states
|
|
const uncertainty = bestQ - secondBestQ;
|
|
const isUncertain = uncertainty < 0.1 && bestQ < 0.5;
|
|
|
|
return {
|
|
action: bestAction,
|
|
confidence: bestQ > 0 ? confidence : 0,
|
|
reason: bestQ > 0 ? 'learned-preference' : 'no-data',
|
|
qValues,
|
|
abGroup: 'treatment',
|
|
isUncertain,
|
|
uncertaintyGap: uncertainty.toFixed(3)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get uncertain states for active learning
|
|
*/
|
|
getUncertainStates(threshold = 0.1) {
|
|
const uncertain = [];
|
|
for (const [state, actions] of Object.entries(this.qTable)) {
|
|
if (state === '_meta') continue;
|
|
|
|
const qVals = Object.entries(actions)
|
|
.filter(([k]) => k !== '_meta')
|
|
.map(([, v]) => v)
|
|
.sort((a, b) => b - a);
|
|
|
|
if (qVals.length >= 2) {
|
|
const gap = qVals[0] - qVals[1];
|
|
if (gap < threshold && qVals[0] < 0.5) {
|
|
uncertain.push({ state, gap, topQ: qVals[0] });
|
|
}
|
|
}
|
|
}
|
|
return uncertain.sort((a, b) => a.gap - b.gap).slice(0, 10);
|
|
}
|
|
|
|
getTopPatterns(limit = 10) {
|
|
const patterns = [];
|
|
for (const [state, actions] of Object.entries(this.qTable)) {
|
|
const sorted = Object.entries(actions)
|
|
.filter(([k]) => k !== '_meta')
|
|
.sort((a, b) => b[1] - a[1]);
|
|
if (sorted.length > 0) {
|
|
patterns.push({
|
|
state,
|
|
bestAction: sorted[0][0],
|
|
qValue: sorted[0][1].toFixed(3),
|
|
alternatives: sorted.slice(1, 3).map(([a, q]) => `${a}:${q.toFixed(2)}`)
|
|
});
|
|
}
|
|
}
|
|
return patterns.sort((a, b) => parseFloat(b.qValue) - parseFloat(a.qValue)).slice(0, limit);
|
|
}
|
|
|
|
getABStats() {
|
|
const treatment = this.trajectories.filter(t => t.abGroup === 'treatment');
|
|
const control = this.trajectories.filter(t => t.abGroup === 'control');
|
|
|
|
const treatmentSuccess = treatment.filter(t => t.reward > 0).length;
|
|
const controlSuccess = control.filter(t => t.reward > 0).length;
|
|
|
|
return {
|
|
treatment: { total: treatment.length, successRate: treatment.length > 0 ? (treatmentSuccess / treatment.length).toFixed(3) : 'N/A' },
|
|
control: { total: control.length, successRate: control.length > 0 ? (controlSuccess / control.length).toFixed(3) : 'N/A' },
|
|
lift: treatment.length > 10 && control.length > 10
|
|
? ((treatmentSuccess / treatment.length) - (controlSuccess / control.length)).toFixed(3)
|
|
: 'insufficient-data'
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error Pattern Tracker - learns from specific error types
|
|
*/
|
|
class ErrorPatternTracker {
|
|
constructor() {
|
|
this.data = this.load();
|
|
}
|
|
|
|
load() {
|
|
if (existsSync(ERROR_PATTERNS_FILE)) {
|
|
try { return JSON.parse(readFileSync(ERROR_PATTERNS_FILE, 'utf-8')); }
|
|
catch { return { patterns: {}, fixes: {}, recentErrors: [] }; }
|
|
}
|
|
return { patterns: {}, fixes: {}, recentErrors: [] };
|
|
}
|
|
|
|
save() {
|
|
writeFileSync(ERROR_PATTERNS_FILE, JSON.stringify(this.data, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Parse error output to extract error codes and types
|
|
*/
|
|
parseError(stderr) {
|
|
const errors = [];
|
|
|
|
// Rust error codes (E0308, E0433, etc.)
|
|
const rustErrors = stderr.match(/error\[E\d{4}\]/g) || [];
|
|
for (const e of rustErrors) {
|
|
const code = e.match(/E\d{4}/)[0];
|
|
errors.push({ type: 'rust', code, category: this.categorizeRustError(code) });
|
|
}
|
|
|
|
// TypeScript errors (TS2304, TS2322, etc.)
|
|
const tsErrors = stderr.match(/TS\d{4}/g) || [];
|
|
for (const code of tsErrors) {
|
|
errors.push({ type: 'typescript', code, category: this.categorizeTsError(code) });
|
|
}
|
|
|
|
// npm/node errors
|
|
if (stderr.includes('ENOENT')) errors.push({ type: 'npm', code: 'ENOENT', category: 'file-not-found' });
|
|
if (stderr.includes('EACCES')) errors.push({ type: 'npm', code: 'EACCES', category: 'permission' });
|
|
if (stderr.includes('MODULE_NOT_FOUND')) errors.push({ type: 'node', code: 'MODULE_NOT_FOUND', category: 'missing-module' });
|
|
|
|
return errors;
|
|
}
|
|
|
|
categorizeRustError(code) {
|
|
const categories = {
|
|
'E0308': 'type-mismatch',
|
|
'E0433': 'missing-import',
|
|
'E0412': 'undefined-type',
|
|
'E0425': 'undefined-value',
|
|
'E0599': 'missing-method',
|
|
'E0277': 'trait-not-implemented',
|
|
'E0382': 'use-after-move',
|
|
'E0502': 'borrow-conflict',
|
|
'E0507': 'cannot-move-out',
|
|
'E0515': 'return-local-reference'
|
|
};
|
|
return categories[code] || 'other';
|
|
}
|
|
|
|
categorizeTsError(code) {
|
|
const categories = {
|
|
'TS2304': 'undefined-name',
|
|
'TS2322': 'type-mismatch',
|
|
'TS2339': 'missing-property',
|
|
'TS2345': 'argument-type',
|
|
'TS2769': 'overload-mismatch'
|
|
};
|
|
return categories[code] || 'other';
|
|
}
|
|
|
|
/**
|
|
* Record an error occurrence
|
|
*/
|
|
recordError(command, stderr, file = null, crate = null) {
|
|
const errors = this.parseError(stderr);
|
|
const timestamp = new Date().toISOString();
|
|
|
|
for (const error of errors) {
|
|
const key = `${error.type}:${error.code}`;
|
|
if (!this.data.patterns[key]) {
|
|
this.data.patterns[key] = { count: 0, category: error.category, contexts: [], lastSeen: null };
|
|
}
|
|
this.data.patterns[key].count++;
|
|
this.data.patterns[key].lastSeen = timestamp;
|
|
if (crate && !this.data.patterns[key].contexts.includes(crate)) {
|
|
this.data.patterns[key].contexts.push(crate);
|
|
}
|
|
}
|
|
|
|
// Store recent errors for sequence detection
|
|
if (errors.length > 0) {
|
|
this.data.recentErrors.push({ errors, command, file, crate, timestamp });
|
|
if (this.data.recentErrors.length > 100) {
|
|
this.data.recentErrors = this.data.recentErrors.slice(-100);
|
|
}
|
|
}
|
|
|
|
this.save();
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Record a successful fix for an error pattern
|
|
*/
|
|
recordFix(errorCode, fixDescription) {
|
|
if (!this.data.fixes[errorCode]) {
|
|
this.data.fixes[errorCode] = [];
|
|
}
|
|
this.data.fixes[errorCode].push({
|
|
fix: fixDescription,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
// Keep last 5 fixes per error
|
|
if (this.data.fixes[errorCode].length > 5) {
|
|
this.data.fixes[errorCode] = this.data.fixes[errorCode].slice(-5);
|
|
}
|
|
this.save();
|
|
}
|
|
|
|
/**
|
|
* Suggest fixes for an error code
|
|
*/
|
|
suggestFix(errorCode) {
|
|
const fixes = this.data.fixes[errorCode] || [];
|
|
const pattern = this.data.patterns[errorCode];
|
|
|
|
return {
|
|
errorCode,
|
|
category: pattern?.category || 'unknown',
|
|
occurrences: pattern?.count || 0,
|
|
commonContexts: pattern?.contexts?.slice(0, 3) || [],
|
|
recentFixes: fixes.slice(-3).map(f => f.fix)
|
|
};
|
|
}
|
|
|
|
getStats() {
|
|
const totalErrors = Object.values(this.data.patterns).reduce((s, p) => s + p.count, 0);
|
|
const topErrors = Object.entries(this.data.patterns)
|
|
.sort((a, b) => b[1].count - a[1].count)
|
|
.slice(0, 5)
|
|
.map(([code, p]) => ({ code, count: p.count, category: p.category }));
|
|
|
|
return { totalErrors, topErrors, fixesRecorded: Object.keys(this.data.fixes).length };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* File Sequence Tracker - learns which files are often edited together
|
|
*/
|
|
class SequenceTracker {
|
|
constructor() {
|
|
this.data = this.load();
|
|
this.sessionEdits = []; // Track edits in current session
|
|
}
|
|
|
|
load() {
|
|
if (existsSync(SEQUENCES_FILE)) {
|
|
try { return JSON.parse(readFileSync(SEQUENCES_FILE, 'utf-8')); }
|
|
catch { return { sequences: {}, coedits: {}, testPairs: {} }; }
|
|
}
|
|
return { sequences: {}, coedits: {}, testPairs: {} };
|
|
}
|
|
|
|
save() {
|
|
writeFileSync(SEQUENCES_FILE, JSON.stringify(this.data, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Record a file edit and learn sequences
|
|
*/
|
|
recordEdit(file) {
|
|
const timestamp = Date.now();
|
|
const normalizedFile = this.normalizePath(file);
|
|
|
|
// Check for sequence from previous edit
|
|
if (this.sessionEdits.length > 0) {
|
|
const lastEdit = this.sessionEdits[this.sessionEdits.length - 1];
|
|
const timeDiff = timestamp - lastEdit.timestamp;
|
|
|
|
// If edited within 5 minutes, consider it a sequence
|
|
if (timeDiff < 5 * 60 * 1000) {
|
|
this.recordSequence(lastEdit.file, normalizedFile);
|
|
}
|
|
}
|
|
|
|
// Detect test file pairing
|
|
this.detectTestPair(normalizedFile);
|
|
|
|
this.sessionEdits.push({ file: normalizedFile, timestamp });
|
|
|
|
// Keep session to last 20 edits
|
|
if (this.sessionEdits.length > 20) {
|
|
this.sessionEdits = this.sessionEdits.slice(-20);
|
|
}
|
|
|
|
this.save();
|
|
}
|
|
|
|
normalizePath(file) {
|
|
// Normalize to relative path from crates/ or src/
|
|
const match = file.match(/(crates\/[^/]+\/.*|src\/.*|tests\/.*)/);
|
|
return match ? match[1] : file.split('/').slice(-3).join('/');
|
|
}
|
|
|
|
recordSequence(from, to) {
|
|
if (from === to) return;
|
|
|
|
if (!this.data.sequences[from]) {
|
|
this.data.sequences[from] = {};
|
|
}
|
|
if (!this.data.sequences[from][to]) {
|
|
this.data.sequences[from][to] = { count: 0, lastSeen: null };
|
|
}
|
|
this.data.sequences[from][to].count++;
|
|
this.data.sequences[from][to].lastSeen = new Date().toISOString();
|
|
|
|
// Also record as co-edit (bidirectional)
|
|
const pairKey = [from, to].sort().join('|');
|
|
if (!this.data.coedits[pairKey]) {
|
|
this.data.coedits[pairKey] = { count: 0, files: [from, to] };
|
|
}
|
|
this.data.coedits[pairKey].count++;
|
|
}
|
|
|
|
detectTestPair(file) {
|
|
// Match source file to test file patterns
|
|
let testFile = null;
|
|
let sourceFile = null;
|
|
|
|
if (file.includes('/tests/') || file.includes('.test.') || file.includes('_test.')) {
|
|
testFile = file;
|
|
// Try to find corresponding source
|
|
sourceFile = file
|
|
.replace('/tests/', '/src/')
|
|
.replace('.test.', '.')
|
|
.replace('_test.', '.');
|
|
} else if (file.includes('/src/')) {
|
|
sourceFile = file;
|
|
// Construct potential test file paths
|
|
const ext = file.split('.').pop();
|
|
testFile = file
|
|
.replace('/src/', '/tests/')
|
|
.replace(`.${ext}`, `.test.${ext}`);
|
|
}
|
|
|
|
if (testFile && sourceFile) {
|
|
const pairKey = [sourceFile, testFile].sort().join('|');
|
|
if (!this.data.testPairs[pairKey]) {
|
|
this.data.testPairs[pairKey] = { source: sourceFile, test: testFile, editCount: 0 };
|
|
}
|
|
this.data.testPairs[pairKey].editCount++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Suggest next files based on current file
|
|
*/
|
|
suggestNextFiles(currentFile, limit = 3) {
|
|
const normalized = this.normalizePath(currentFile);
|
|
const sequences = this.data.sequences[normalized] || {};
|
|
|
|
const suggestions = Object.entries(sequences)
|
|
.sort((a, b) => b[1].count - a[1].count)
|
|
.slice(0, limit)
|
|
.map(([file, data]) => ({
|
|
file,
|
|
probability: Math.min(0.9, data.count / 10),
|
|
timesSequenced: data.count
|
|
}));
|
|
|
|
// Also check for test file suggestion
|
|
const testSuggestion = this.suggestTestFile(currentFile);
|
|
if (testSuggestion && !suggestions.find(s => s.file === testSuggestion.file)) {
|
|
suggestions.push(testSuggestion);
|
|
}
|
|
|
|
return suggestions.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Suggest test file for a source file
|
|
*/
|
|
suggestTestFile(sourceFile) {
|
|
const normalized = this.normalizePath(sourceFile);
|
|
|
|
// Find matching test pair
|
|
for (const [, pair] of Object.entries(this.data.testPairs)) {
|
|
if (pair.source === normalized || normalized.includes(pair.source)) {
|
|
return {
|
|
file: pair.test,
|
|
type: 'test-file',
|
|
probability: 0.8,
|
|
reason: 'Corresponding test file'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Generate test file path if not found
|
|
if (sourceFile.includes('/src/') && !sourceFile.includes('test')) {
|
|
const ext = sourceFile.split('.').pop();
|
|
const testPath = sourceFile
|
|
.replace('/src/', '/tests/')
|
|
.replace(`.${ext}`, ext === 'rs' ? `_test.${ext}` : `.test.${ext}`);
|
|
return {
|
|
file: this.normalizePath(testPath),
|
|
type: 'suggested-test',
|
|
probability: 0.5,
|
|
reason: 'Suggested test location'
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Suggest running tests after editing source files
|
|
*/
|
|
shouldSuggestTests(file) {
|
|
const normalized = this.normalizePath(file);
|
|
|
|
// Always suggest tests for Rust source files
|
|
if (file.endsWith('.rs') && file.includes('/src/') && !file.includes('test')) {
|
|
const crateMatch = file.match(/crates\/([^/]+)/);
|
|
const crate = crateMatch ? crateMatch[1] : null;
|
|
return {
|
|
suggest: true,
|
|
command: crate ? `cargo test -p ${crate}` : 'cargo test',
|
|
reason: 'Source file modified'
|
|
};
|
|
}
|
|
|
|
// Suggest tests for TypeScript source files
|
|
if ((file.endsWith('.ts') || file.endsWith('.tsx')) && !file.includes('.test.')) {
|
|
return {
|
|
suggest: true,
|
|
command: 'npm test',
|
|
reason: 'TypeScript source modified'
|
|
};
|
|
}
|
|
|
|
return { suggest: false };
|
|
}
|
|
|
|
getStats() {
|
|
return {
|
|
totalSequences: Object.keys(this.data.sequences).length,
|
|
totalCoedits: Object.keys(this.data.coedits).length,
|
|
testPairs: Object.keys(this.data.testPairs).length,
|
|
sessionEdits: this.sessionEdits.length
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Neural Router with enhanced intelligence
|
|
*/
|
|
class NeuralRouter {
|
|
constructor(memory, reasoning, calibration, feedback) {
|
|
this.memory = memory;
|
|
this.reasoning = reasoning;
|
|
this.calibration = calibration;
|
|
this.feedback = feedback;
|
|
}
|
|
|
|
async route(task, context = {}) {
|
|
const { fileType, crate, operation = 'edit' } = context;
|
|
// Use underscore format to match pretrained Q-table
|
|
const state = `${operation}_${fileType || 'file'}_in_${crate || 'project'}`;
|
|
const agents = this.getAgentsForContext(fileType, crate);
|
|
|
|
const suggestion = this.reasoning.getBestAction(state, agents);
|
|
const similar = await this.memory.search(task, 3);
|
|
|
|
let finalAgent = suggestion.action;
|
|
let finalConf = suggestion.confidence;
|
|
|
|
if (similar.length > 0 && similar[0].score > 0.7) {
|
|
const pastAgent = similar[0].metadata?.agent;
|
|
if (pastAgent && agents.includes(pastAgent)) {
|
|
finalAgent = pastAgent;
|
|
finalConf = Math.min(1, finalConf + 0.2);
|
|
}
|
|
}
|
|
|
|
// Record for feedback tracking
|
|
const suggestionId = `sug-${Date.now()}`;
|
|
this.feedback.recordSuggestion(suggestionId, finalAgent, finalConf);
|
|
|
|
return {
|
|
recommended: finalAgent,
|
|
confidence: finalConf,
|
|
reason: this.buildReason(finalAgent, suggestion.reason, similar),
|
|
alternatives: agents.filter(a => a !== finalAgent).slice(0, 3),
|
|
context: { state, agents, similar: similar.slice(0, 2) },
|
|
suggestionId,
|
|
abGroup: suggestion.abGroup,
|
|
isUncertain: suggestion.isUncertain
|
|
};
|
|
}
|
|
|
|
getAgentsForContext(fileType, crate) {
|
|
const base = ['coder', 'reviewer', 'tester'];
|
|
|
|
const typeMap = {
|
|
'rs': ['rust-developer', 'code-analyzer'],
|
|
'ts': ['typescript-developer', 'backend-dev'],
|
|
'js': ['javascript-developer', 'backend-dev'],
|
|
'md': ['technical-writer'],
|
|
'json': ['config-specialist'],
|
|
'py': ['python-developer'],
|
|
'css': ['frontend-developer'],
|
|
'html': ['frontend-developer'],
|
|
'tsx': ['frontend-developer'],
|
|
'yml': ['devops-engineer'],
|
|
'yaml': ['devops-engineer'],
|
|
'sql': ['database-expert'],
|
|
'sh': ['system-admin']
|
|
};
|
|
|
|
if (typeMap[fileType]) base.push(...typeMap[fileType]);
|
|
|
|
// Crate-specific specializations
|
|
if (fileType === 'rs') {
|
|
if (crate?.includes('wasm') || crate === 'rvlite') base.push('production-validator');
|
|
if (crate?.includes('gnn') || crate?.includes('attention') || crate === 'sona') base.push('ml-developer');
|
|
if (crate?.includes('postgres')) base.push('backend-dev', 'system-architect');
|
|
if (crate?.includes('mincut') || crate?.includes('graph')) base.push('system-architect');
|
|
}
|
|
|
|
return [...new Set(base)];
|
|
}
|
|
|
|
buildReason(agent, qReason, similar) {
|
|
const parts = [];
|
|
if (qReason === 'learned-preference') parts.push('learned from past success');
|
|
if (similar.length > 0 && similar[0].score > 0.6) parts.push('similar past task succeeded');
|
|
if (parts.length === 0) parts.push('default selection');
|
|
return `${agent}: ${parts.join(', ')}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main Intelligence API v2
|
|
*/
|
|
class RuVectorIntelligence {
|
|
constructor(options = {}) {
|
|
this.memory = new VectorMemory({ hyperbolic: options.hyperbolic ?? true });
|
|
this.reasoning = new ReasoningBank();
|
|
this.calibration = new CalibrationTracker();
|
|
this.feedback = new FeedbackLoop();
|
|
this.errorPatterns = new ErrorPatternTracker();
|
|
this.sequences = new SequenceTracker();
|
|
this.router = new NeuralRouter(this.memory, this.reasoning, this.calibration, this.feedback);
|
|
this.initialized = false;
|
|
}
|
|
|
|
async init() {
|
|
if (!this.initialized) {
|
|
await this.memory.init();
|
|
this.initialized = true;
|
|
}
|
|
}
|
|
|
|
async remember(type, content, metadata = {}) {
|
|
await this.init();
|
|
return this.memory.store(type, content, metadata);
|
|
}
|
|
|
|
async recall(query, limit = 5) {
|
|
await this.init();
|
|
return this.memory.search(query, limit);
|
|
}
|
|
|
|
learn(state, action, outcome, reward) {
|
|
return this.reasoning.recordTrajectory(state, action, outcome, reward);
|
|
}
|
|
|
|
suggest(state, actions) {
|
|
return this.reasoning.getBestAction(state, actions);
|
|
}
|
|
|
|
async route(task, context = {}) {
|
|
await this.init();
|
|
return this.router.route(task, context);
|
|
}
|
|
|
|
recordCalibration(predicted, actual, confidence) {
|
|
return this.calibration.record(predicted, actual, confidence);
|
|
}
|
|
|
|
recordFeedback(suggestionId, actualUsed, success) {
|
|
this.feedback.recordOutcome(suggestionId, actualUsed, success);
|
|
}
|
|
|
|
// === New v3 Features ===
|
|
|
|
/**
|
|
* Record an error from command output
|
|
*/
|
|
recordError(command, stderr, file = null, crate = null) {
|
|
return this.errorPatterns.recordError(command, stderr, file, crate);
|
|
}
|
|
|
|
/**
|
|
* Record a fix for an error pattern
|
|
*/
|
|
recordFix(errorCode, fixDescription) {
|
|
this.errorPatterns.recordFix(errorCode, fixDescription);
|
|
}
|
|
|
|
/**
|
|
* Get suggested fixes for an error
|
|
*/
|
|
suggestFix(errorCode) {
|
|
return this.errorPatterns.suggestFix(errorCode);
|
|
}
|
|
|
|
/**
|
|
* Record a file edit for sequence learning
|
|
*/
|
|
recordFileEdit(file) {
|
|
this.sequences.recordEdit(file);
|
|
}
|
|
|
|
/**
|
|
* Suggest next files based on current file
|
|
*/
|
|
suggestNextFiles(file, limit = 3) {
|
|
return this.sequences.suggestNextFiles(file, limit);
|
|
}
|
|
|
|
/**
|
|
* Check if tests should be suggested after editing a file
|
|
*/
|
|
shouldSuggestTests(file) {
|
|
return this.sequences.shouldSuggestTests(file);
|
|
}
|
|
|
|
stats() {
|
|
return {
|
|
memory: this.memory.getStats(),
|
|
trajectories: this.reasoning.trajectories.length,
|
|
patterns: Object.keys(this.reasoning.qTable).length,
|
|
topPatterns: this.reasoning.getTopPatterns(5),
|
|
calibration: this.calibration.getStats(),
|
|
abTest: this.reasoning.getABStats(),
|
|
adviceValue: this.feedback.getAdviceValue(),
|
|
uncertainStates: this.reasoning.getUncertainStates(0.15),
|
|
// v3 stats
|
|
errorPatterns: this.errorPatterns.getStats(),
|
|
sequences: this.sequences.getStats(),
|
|
ruvectorNative: ruvectorAvailable
|
|
};
|
|
}
|
|
}
|
|
|
|
export { RuVectorIntelligence, VectorMemory, ReasoningBank, NeuralRouter, CalibrationTracker, FeedbackLoop, ErrorPatternTracker, SequenceTracker };
|
|
export default RuVectorIntelligence;
|