mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
Merge pull request #599 from proffesor-for-testing/working-july
chore(release): prepare v3.13.5
This commit is contained in:
@@ -1 +1 @@
|
||||
3.32.2
|
||||
3.32.34
|
||||
@@ -1,430 +1,430 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Auto Memory Bridge Hook (ADR-048/049)
|
||||
*
|
||||
* Wires AutoMemoryBridge + LearningBridge + MemoryGraph into Claude Code
|
||||
* session lifecycle. Called by settings.json SessionStart/SessionEnd hooks.
|
||||
*
|
||||
* Usage:
|
||||
* node auto-memory-hook.mjs import # SessionStart: import auto memory files into backend
|
||||
* node auto-memory-hook.mjs sync # SessionEnd: sync insights back to MEMORY.md
|
||||
* node auto-memory-hook.mjs status # Show bridge status
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
|
||||
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
|
||||
|
||||
// Colors
|
||||
const GREEN = '\x1b[0;32m';
|
||||
const CYAN = '\x1b[0;36m';
|
||||
const DIM = '\x1b[2m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
const YELLOW = '\x1b[0;33m';
|
||||
const log = (msg) => console.log(`${CYAN}[AutoMemory] ${msg}${RESET}`);
|
||||
const success = (msg) => console.log(`${GREEN}[AutoMemory] ✓ ${msg}${RESET}`);
|
||||
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
|
||||
|
||||
// #2545: fail LOUD instead of a silent dim skip. When @claude-flow/memory cannot
|
||||
// be resolved, self-learning imports are a no-op — the user must see this and be
|
||||
// told exactly how to fix it (on both stdout, so it shows in the Claude Code hook
|
||||
// transcript, and stderr, per the issue's requested channel).
|
||||
function warnMemoryUnavailable() {
|
||||
const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${PROJECT_ROOT} — self-learning imports are DISABLED.`;
|
||||
const line2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
|
||||
console.log(`${YELLOW}${line1}${RESET}`);
|
||||
console.log(`${YELLOW}${line2}${RESET}`);
|
||||
process.stderr.write(`${line1}\n${line2}\n`);
|
||||
}
|
||||
|
||||
const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
|
||||
|
||||
// ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
|
||||
// Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
|
||||
// (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
|
||||
// of leaving a half-written store or a stale lock behind.
|
||||
let activeBackend = null;
|
||||
let shuttingDown = false;
|
||||
function trackBackend(b) { activeBackend = b; return b; }
|
||||
async function gracefulExit(signal) {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
if (DEBUG) process.stderr.write(`[AutoMemory] received ${signal}, flushing backend before exit\n`);
|
||||
try {
|
||||
if (activeBackend && typeof activeBackend.shutdown === 'function') await activeBackend.shutdown();
|
||||
} catch { /* best effort — never block exit on cleanup */ }
|
||||
process.exit(0);
|
||||
}
|
||||
process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
|
||||
process.on('SIGINT', () => { gracefulExit('SIGINT'); });
|
||||
|
||||
// Ensure data dir
|
||||
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
// ============================================================================
|
||||
// Simple JSON File Backend (implements IMemoryBackend interface)
|
||||
// ============================================================================
|
||||
|
||||
class JsonFileBackend {
|
||||
constructor(filePath) {
|
||||
this.filePath = filePath;
|
||||
this.entries = new Map();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (existsSync(this.filePath)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
|
||||
if (Array.isArray(data)) {
|
||||
for (const entry of data) this.entries.set(entry.id, entry);
|
||||
}
|
||||
} catch { /* start fresh */ }
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() { this._persist(); }
|
||||
async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
|
||||
async get(id) { return this.entries.get(id) ?? null; }
|
||||
async getByKey(key, ns) {
|
||||
for (const e of this.entries.values()) {
|
||||
if (e.key === key && (!ns || e.namespace === ns)) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async update(id, updates) {
|
||||
const e = this.entries.get(id);
|
||||
if (!e) return null;
|
||||
if (updates.metadata) Object.assign(e.metadata, updates.metadata);
|
||||
if (updates.content !== undefined) e.content = updates.content;
|
||||
if (updates.tags) e.tags = updates.tags;
|
||||
e.updatedAt = Date.now();
|
||||
this._persist();
|
||||
return e;
|
||||
}
|
||||
async delete(id) { return this.entries.delete(id); }
|
||||
async query(opts) {
|
||||
let results = [...this.entries.values()];
|
||||
if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
|
||||
if (opts?.type) results = results.filter(e => e.type === opts.type);
|
||||
if (opts?.limit) results = results.slice(0, opts.limit);
|
||||
return results;
|
||||
}
|
||||
async search() { return []; } // No vector search in JSON backend
|
||||
async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
|
||||
async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.entries.delete(id)) n++; } this._persist(); return n; }
|
||||
async count() { return this.entries.size; }
|
||||
async listNamespaces() {
|
||||
const ns = new Set();
|
||||
for (const e of this.entries.values()) ns.add(e.namespace || 'default');
|
||||
return [...ns];
|
||||
}
|
||||
async clearNamespace(ns) {
|
||||
let n = 0;
|
||||
for (const [id, e] of this.entries) {
|
||||
if (e.namespace === ns) { this.entries.delete(id); n++; }
|
||||
}
|
||||
this._persist();
|
||||
return n;
|
||||
}
|
||||
async getStats() {
|
||||
return {
|
||||
totalEntries: this.entries.size,
|
||||
entriesByNamespace: {},
|
||||
entriesByType: { semantic: 0, episodic: 0, procedural: 0, working: 0, cache: 0 },
|
||||
memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
|
||||
};
|
||||
}
|
||||
async healthCheck() {
|
||||
return {
|
||||
status: 'healthy',
|
||||
components: {
|
||||
storage: { status: 'healthy', latency: 0 },
|
||||
index: { status: 'healthy', latency: 0 },
|
||||
cache: { status: 'healthy', latency: 0 },
|
||||
},
|
||||
timestamp: Date.now(), issues: [], recommendations: [],
|
||||
};
|
||||
}
|
||||
|
||||
_persist() {
|
||||
try {
|
||||
writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Resolve memory package path (local dev or npm installed)
|
||||
// ============================================================================
|
||||
|
||||
async function loadMemoryPackage() {
|
||||
// Strategy 0 (#2545): sidecar recorded by `init` / `doctor --fix`. On the
|
||||
// documented `npx ruflo` path @claude-flow/memory (an optionalDependency of
|
||||
// the CLI) lands in the npx cache, which is NOT on the walk-up path from the
|
||||
// project — so init resolves it from the CLI's own context and records the
|
||||
// absolute path here. This is the only strategy that works on that install.
|
||||
try {
|
||||
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
|
||||
if (existsSync(sidecar)) {
|
||||
const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
||||
if (rec?.distPath && existsSync(rec.distPath)) {
|
||||
return await import(`file://${rec.distPath}`);
|
||||
}
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 1: Local dev (built dist)
|
||||
const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
|
||||
if (existsSync(localDist)) {
|
||||
try {
|
||||
return await import(`file://${localDist}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
||||
// Strategy 2: Use createRequire for CJS-style resolution (handles nested node_modules
|
||||
// when installed as a transitive dependency via npx ruflo / npx claude-flow)
|
||||
try {
|
||||
const { createRequire } = await import('module');
|
||||
const require = createRequire(join(PROJECT_ROOT, 'package.json'));
|
||||
return require('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 3: ESM import (works when @claude-flow/memory is a direct dependency)
|
||||
try {
|
||||
return await import('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
|
||||
let searchDir = PROJECT_ROOT;
|
||||
const { parse } = await import('path');
|
||||
while (searchDir !== parse(searchDir).root) {
|
||||
const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
|
||||
if (existsSync(candidate)) {
|
||||
try {
|
||||
return await import(`file://${candidate}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
searchDir = dirname(searchDir);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Read config from .claude-flow/config.yaml
|
||||
// ============================================================================
|
||||
|
||||
function readConfig() {
|
||||
const configPath = join(PROJECT_ROOT, '.claude-flow', 'config.yaml');
|
||||
const defaults = {
|
||||
learningBridge: { enabled: true, sonaMode: 'balanced', confidenceDecayRate: 0.005, accessBoostAmount: 0.03, consolidationThreshold: 10 },
|
||||
memoryGraph: { enabled: true, pageRankDamping: 0.85, maxNodes: 5000, similarityThreshold: 0.8 },
|
||||
agentScopes: { enabled: true, defaultScope: 'project' },
|
||||
};
|
||||
|
||||
if (!existsSync(configPath)) return defaults;
|
||||
|
||||
try {
|
||||
const yaml = readFileSync(configPath, 'utf-8');
|
||||
// Simple YAML parser for the memory section
|
||||
const getBool = (key) => {
|
||||
const match = yaml.match(new RegExp(`${key}:\\s*(true|false)`, 'i'));
|
||||
return match ? match[1] === 'true' : undefined;
|
||||
};
|
||||
|
||||
const lbEnabled = getBool('learningBridge[\\s\\S]*?enabled');
|
||||
if (lbEnabled !== undefined) defaults.learningBridge.enabled = lbEnabled;
|
||||
|
||||
const mgEnabled = getBool('memoryGraph[\\s\\S]*?enabled');
|
||||
if (mgEnabled !== undefined) defaults.memoryGraph.enabled = mgEnabled;
|
||||
|
||||
const asEnabled = getBool('agentScopes[\\s\\S]*?enabled');
|
||||
if (asEnabled !== undefined) defaults.agentScopes.enabled = asEnabled;
|
||||
|
||||
return defaults;
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Commands
|
||||
// ============================================================================
|
||||
|
||||
async function doImport() {
|
||||
log('Importing auto memory files into bridge...');
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
warnMemoryUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
|
||||
await backend.initialize();
|
||||
|
||||
const bridgeConfig = {
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
// Wire learning if enabled and available
|
||||
if (config.learningBridge.enabled && memPkg.LearningBridge) {
|
||||
bridgeConfig.learning = {
|
||||
sonaMode: config.learningBridge.sonaMode,
|
||||
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
|
||||
accessBoostAmount: config.learningBridge.accessBoostAmount,
|
||||
consolidationThreshold: config.learningBridge.consolidationThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
// Wire graph if enabled and available
|
||||
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
|
||||
bridgeConfig.graph = {
|
||||
pageRankDamping: config.memoryGraph.pageRankDamping,
|
||||
maxNodes: config.memoryGraph.maxNodes,
|
||||
similarityThreshold: config.memoryGraph.similarityThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
||||
|
||||
try {
|
||||
const result = await bridge.importFromAutoMemory();
|
||||
success(`Imported ${result.imported} entries (${result.skipped} skipped)`);
|
||||
dim(`├─ Backend entries: ${await backend.count()}`);
|
||||
dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
|
||||
} catch (err) {
|
||||
dim(`Import failed (non-critical): ${err.message}`);
|
||||
}
|
||||
|
||||
await backend.shutdown();
|
||||
}
|
||||
|
||||
async function doSync() {
|
||||
log('Syncing insights to auto memory files...');
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
warnMemoryUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
|
||||
await backend.initialize();
|
||||
|
||||
const entryCount = await backend.count();
|
||||
if (entryCount === 0) {
|
||||
dim('No entries to sync');
|
||||
await backend.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
const bridgeConfig = {
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
if (config.learningBridge.enabled && memPkg.LearningBridge) {
|
||||
bridgeConfig.learning = {
|
||||
sonaMode: config.learningBridge.sonaMode,
|
||||
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
|
||||
consolidationThreshold: config.learningBridge.consolidationThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
|
||||
bridgeConfig.graph = {
|
||||
pageRankDamping: config.memoryGraph.pageRankDamping,
|
||||
maxNodes: config.memoryGraph.maxNodes,
|
||||
};
|
||||
}
|
||||
|
||||
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
||||
|
||||
try {
|
||||
const syncResult = await bridge.syncToAutoMemory();
|
||||
success(`Synced ${syncResult.synced} entries to auto memory`);
|
||||
dim(`├─ Categories updated: ${syncResult.categories?.join(', ') || 'none'}`);
|
||||
dim(`└─ Backend entries: ${entryCount}`);
|
||||
|
||||
// Curate MEMORY.md index with graph-aware ordering
|
||||
await bridge.curateIndex();
|
||||
success('Curated MEMORY.md index');
|
||||
} catch (err) {
|
||||
dim(`Sync failed (non-critical): ${err.message}`);
|
||||
}
|
||||
|
||||
if (bridge.destroy) bridge.destroy();
|
||||
await backend.shutdown();
|
||||
}
|
||||
|
||||
async function doStatus() {
|
||||
const memPkg = await loadMemoryPackage();
|
||||
const config = readConfig();
|
||||
|
||||
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
|
||||
const hasSidecar = existsSync(sidecar);
|
||||
|
||||
console.log('\n=== Auto Memory Bridge Status ===\n');
|
||||
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found — self-learning DISABLED (fix: npm i -D @claude-flow/memory)'}`);
|
||||
console.log(` Resolver: ${hasSidecar ? '✅ .claude-flow/memory-package.json' : '⏸ no sidecar (run: npx ruflo@latest doctor --fix)'}`);
|
||||
console.log(` Store: ${existsSync(STORE_PATH) ? '✅ ' + STORE_PATH : '⏸ Not initialized'}`);
|
||||
console.log(` LearningBridge: ${config.learningBridge.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
|
||||
if (existsSync(STORE_PATH)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
|
||||
console.log(` Entries: ${Array.isArray(data) ? data.length : 0}`);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
const command = process.argv[2] || 'status';
|
||||
|
||||
// Dynamic import() failures can surface as unhandled rejections on a later
|
||||
// microtask even when the awaiting call site already caught them, which would
|
||||
// otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
|
||||
// reason under RUFLO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
|
||||
// (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
if (DEBUG) {
|
||||
const detail = reason && reason.message ? reason.message : String(reason);
|
||||
process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'import': await doImport(); break;
|
||||
case 'sync': await doSync(); break;
|
||||
case 'status': await doStatus(); break;
|
||||
default:
|
||||
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Hooks must never crash Claude Code - fail silently
|
||||
try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
|
||||
}
|
||||
// Force clean exit — process.exitCode alone isn't enough if async errors override it
|
||||
process.exit(0);
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Auto Memory Bridge Hook (ADR-048/049)
|
||||
*
|
||||
* Wires AutoMemoryBridge + LearningBridge + MemoryGraph into Claude Code
|
||||
* session lifecycle. Called by settings.json SessionStart/SessionEnd hooks.
|
||||
*
|
||||
* Usage:
|
||||
* node auto-memory-hook.mjs import # SessionStart: import auto memory files into backend
|
||||
* node auto-memory-hook.mjs sync # SessionEnd: sync insights back to MEMORY.md
|
||||
* node auto-memory-hook.mjs status # Show bridge status
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
|
||||
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
|
||||
|
||||
// Colors
|
||||
const GREEN = '\x1b[0;32m';
|
||||
const CYAN = '\x1b[0;36m';
|
||||
const DIM = '\x1b[2m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
const YELLOW = '\x1b[0;33m';
|
||||
const log = (msg) => console.log(`${CYAN}[AutoMemory] ${msg}${RESET}`);
|
||||
const success = (msg) => console.log(`${GREEN}[AutoMemory] ✓ ${msg}${RESET}`);
|
||||
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
|
||||
|
||||
// #2545: fail LOUD instead of a silent dim skip. When @claude-flow/memory cannot
|
||||
// be resolved, self-learning imports are a no-op — the user must see this and be
|
||||
// told exactly how to fix it (on both stdout, so it shows in the Claude Code hook
|
||||
// transcript, and stderr, per the issue's requested channel).
|
||||
function warnMemoryUnavailable() {
|
||||
const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${PROJECT_ROOT} — self-learning imports are DISABLED.`;
|
||||
const line2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
|
||||
console.log(`${YELLOW}${line1}${RESET}`);
|
||||
console.log(`${YELLOW}${line2}${RESET}`);
|
||||
process.stderr.write(`${line1}\n${line2}\n`);
|
||||
}
|
||||
|
||||
const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
|
||||
|
||||
// ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
|
||||
// Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
|
||||
// (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
|
||||
// of leaving a half-written store or a stale lock behind.
|
||||
let activeBackend = null;
|
||||
let shuttingDown = false;
|
||||
function trackBackend(b) { activeBackend = b; return b; }
|
||||
async function gracefulExit(signal) {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
if (DEBUG) process.stderr.write(`[AutoMemory] received ${signal}, flushing backend before exit\n`);
|
||||
try {
|
||||
if (activeBackend && typeof activeBackend.shutdown === 'function') await activeBackend.shutdown();
|
||||
} catch { /* best effort — never block exit on cleanup */ }
|
||||
process.exit(0);
|
||||
}
|
||||
process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
|
||||
process.on('SIGINT', () => { gracefulExit('SIGINT'); });
|
||||
|
||||
// Ensure data dir
|
||||
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
// ============================================================================
|
||||
// Simple JSON File Backend (implements IMemoryBackend interface)
|
||||
// ============================================================================
|
||||
|
||||
class JsonFileBackend {
|
||||
constructor(filePath) {
|
||||
this.filePath = filePath;
|
||||
this.entries = new Map();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (existsSync(this.filePath)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
|
||||
if (Array.isArray(data)) {
|
||||
for (const entry of data) this.entries.set(entry.id, entry);
|
||||
}
|
||||
} catch { /* start fresh */ }
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() { this._persist(); }
|
||||
async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
|
||||
async get(id) { return this.entries.get(id) ?? null; }
|
||||
async getByKey(key, ns) {
|
||||
for (const e of this.entries.values()) {
|
||||
if (e.key === key && (!ns || e.namespace === ns)) return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async update(id, updates) {
|
||||
const e = this.entries.get(id);
|
||||
if (!e) return null;
|
||||
if (updates.metadata) Object.assign(e.metadata, updates.metadata);
|
||||
if (updates.content !== undefined) e.content = updates.content;
|
||||
if (updates.tags) e.tags = updates.tags;
|
||||
e.updatedAt = Date.now();
|
||||
this._persist();
|
||||
return e;
|
||||
}
|
||||
async delete(id) { return this.entries.delete(id); }
|
||||
async query(opts) {
|
||||
let results = [...this.entries.values()];
|
||||
if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
|
||||
if (opts?.type) results = results.filter(e => e.type === opts.type);
|
||||
if (opts?.limit) results = results.slice(0, opts.limit);
|
||||
return results;
|
||||
}
|
||||
async search() { return []; } // No vector search in JSON backend
|
||||
async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
|
||||
async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.entries.delete(id)) n++; } this._persist(); return n; }
|
||||
async count() { return this.entries.size; }
|
||||
async listNamespaces() {
|
||||
const ns = new Set();
|
||||
for (const e of this.entries.values()) ns.add(e.namespace || 'default');
|
||||
return [...ns];
|
||||
}
|
||||
async clearNamespace(ns) {
|
||||
let n = 0;
|
||||
for (const [id, e] of this.entries) {
|
||||
if (e.namespace === ns) { this.entries.delete(id); n++; }
|
||||
}
|
||||
this._persist();
|
||||
return n;
|
||||
}
|
||||
async getStats() {
|
||||
return {
|
||||
totalEntries: this.entries.size,
|
||||
entriesByNamespace: {},
|
||||
entriesByType: { semantic: 0, episodic: 0, procedural: 0, working: 0, cache: 0 },
|
||||
memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
|
||||
};
|
||||
}
|
||||
async healthCheck() {
|
||||
return {
|
||||
status: 'healthy',
|
||||
components: {
|
||||
storage: { status: 'healthy', latency: 0 },
|
||||
index: { status: 'healthy', latency: 0 },
|
||||
cache: { status: 'healthy', latency: 0 },
|
||||
},
|
||||
timestamp: Date.now(), issues: [], recommendations: [],
|
||||
};
|
||||
}
|
||||
|
||||
_persist() {
|
||||
try {
|
||||
writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
|
||||
} catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Resolve memory package path (local dev or npm installed)
|
||||
// ============================================================================
|
||||
|
||||
async function loadMemoryPackage() {
|
||||
// Strategy 0 (#2545): sidecar recorded by `init` / `doctor --fix`. On the
|
||||
// documented `npx ruflo` path @claude-flow/memory (an optionalDependency of
|
||||
// the CLI) lands in the npx cache, which is NOT on the walk-up path from the
|
||||
// project — so init resolves it from the CLI's own context and records the
|
||||
// absolute path here. This is the only strategy that works on that install.
|
||||
try {
|
||||
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
|
||||
if (existsSync(sidecar)) {
|
||||
const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
||||
if (rec?.distPath && existsSync(rec.distPath)) {
|
||||
return await import(`file://${rec.distPath}`);
|
||||
}
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 1: Local dev (built dist)
|
||||
const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
|
||||
if (existsSync(localDist)) {
|
||||
try {
|
||||
return await import(`file://${localDist}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
||||
// Strategy 2: Use createRequire for CJS-style resolution (handles nested node_modules
|
||||
// when installed as a transitive dependency via npx ruflo / npx claude-flow)
|
||||
try {
|
||||
const { createRequire } = await import('module');
|
||||
const require = createRequire(join(PROJECT_ROOT, 'package.json'));
|
||||
return require('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 3: ESM import (works when @claude-flow/memory is a direct dependency)
|
||||
try {
|
||||
return await import('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
|
||||
let searchDir = PROJECT_ROOT;
|
||||
const { parse } = await import('path');
|
||||
while (searchDir !== parse(searchDir).root) {
|
||||
const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
|
||||
if (existsSync(candidate)) {
|
||||
try {
|
||||
return await import(`file://${candidate}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
searchDir = dirname(searchDir);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Read config from .claude-flow/config.yaml
|
||||
// ============================================================================
|
||||
|
||||
function readConfig() {
|
||||
const configPath = join(PROJECT_ROOT, '.claude-flow', 'config.yaml');
|
||||
const defaults = {
|
||||
learningBridge: { enabled: true, sonaMode: 'balanced', confidenceDecayRate: 0.005, accessBoostAmount: 0.03, consolidationThreshold: 10 },
|
||||
memoryGraph: { enabled: true, pageRankDamping: 0.85, maxNodes: 5000, similarityThreshold: 0.8 },
|
||||
agentScopes: { enabled: true, defaultScope: 'project' },
|
||||
};
|
||||
|
||||
if (!existsSync(configPath)) return defaults;
|
||||
|
||||
try {
|
||||
const yaml = readFileSync(configPath, 'utf-8');
|
||||
// Simple YAML parser for the memory section
|
||||
const getBool = (key) => {
|
||||
const match = yaml.match(new RegExp(`${key}:\\s*(true|false)`, 'i'));
|
||||
return match ? match[1] === 'true' : undefined;
|
||||
};
|
||||
|
||||
const lbEnabled = getBool('learningBridge[\\s\\S]*?enabled');
|
||||
if (lbEnabled !== undefined) defaults.learningBridge.enabled = lbEnabled;
|
||||
|
||||
const mgEnabled = getBool('memoryGraph[\\s\\S]*?enabled');
|
||||
if (mgEnabled !== undefined) defaults.memoryGraph.enabled = mgEnabled;
|
||||
|
||||
const asEnabled = getBool('agentScopes[\\s\\S]*?enabled');
|
||||
if (asEnabled !== undefined) defaults.agentScopes.enabled = asEnabled;
|
||||
|
||||
return defaults;
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Commands
|
||||
// ============================================================================
|
||||
|
||||
async function doImport() {
|
||||
log('Importing auto memory files into bridge...');
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
warnMemoryUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
|
||||
await backend.initialize();
|
||||
|
||||
const bridgeConfig = {
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
// Wire learning if enabled and available
|
||||
if (config.learningBridge.enabled && memPkg.LearningBridge) {
|
||||
bridgeConfig.learning = {
|
||||
sonaMode: config.learningBridge.sonaMode,
|
||||
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
|
||||
accessBoostAmount: config.learningBridge.accessBoostAmount,
|
||||
consolidationThreshold: config.learningBridge.consolidationThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
// Wire graph if enabled and available
|
||||
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
|
||||
bridgeConfig.graph = {
|
||||
pageRankDamping: config.memoryGraph.pageRankDamping,
|
||||
maxNodes: config.memoryGraph.maxNodes,
|
||||
similarityThreshold: config.memoryGraph.similarityThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
||||
|
||||
try {
|
||||
const result = await bridge.importFromAutoMemory();
|
||||
success(`Imported ${result.imported} entries (${result.skipped} skipped)`);
|
||||
dim(`├─ Backend entries: ${await backend.count()}`);
|
||||
dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
|
||||
} catch (err) {
|
||||
dim(`Import failed (non-critical): ${err.message}`);
|
||||
}
|
||||
|
||||
await backend.shutdown();
|
||||
}
|
||||
|
||||
async function doSync() {
|
||||
log('Syncing insights to auto memory files...');
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
warnMemoryUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
|
||||
await backend.initialize();
|
||||
|
||||
const entryCount = await backend.count();
|
||||
if (entryCount === 0) {
|
||||
dim('No entries to sync');
|
||||
await backend.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
const bridgeConfig = {
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
if (config.learningBridge.enabled && memPkg.LearningBridge) {
|
||||
bridgeConfig.learning = {
|
||||
sonaMode: config.learningBridge.sonaMode,
|
||||
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
|
||||
consolidationThreshold: config.learningBridge.consolidationThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
|
||||
bridgeConfig.graph = {
|
||||
pageRankDamping: config.memoryGraph.pageRankDamping,
|
||||
maxNodes: config.memoryGraph.maxNodes,
|
||||
};
|
||||
}
|
||||
|
||||
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
|
||||
|
||||
try {
|
||||
const syncResult = await bridge.syncToAutoMemory();
|
||||
success(`Synced ${syncResult.synced} entries to auto memory`);
|
||||
dim(`├─ Categories updated: ${syncResult.categories?.join(', ') || 'none'}`);
|
||||
dim(`└─ Backend entries: ${entryCount}`);
|
||||
|
||||
// Curate MEMORY.md index with graph-aware ordering
|
||||
await bridge.curateIndex();
|
||||
success('Curated MEMORY.md index');
|
||||
} catch (err) {
|
||||
dim(`Sync failed (non-critical): ${err.message}`);
|
||||
}
|
||||
|
||||
if (bridge.destroy) bridge.destroy();
|
||||
await backend.shutdown();
|
||||
}
|
||||
|
||||
async function doStatus() {
|
||||
const memPkg = await loadMemoryPackage();
|
||||
const config = readConfig();
|
||||
|
||||
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
|
||||
const hasSidecar = existsSync(sidecar);
|
||||
|
||||
console.log('\n=== Auto Memory Bridge Status ===\n');
|
||||
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found — self-learning DISABLED (fix: npm i -D @claude-flow/memory)'}`);
|
||||
console.log(` Resolver: ${hasSidecar ? '✅ .claude-flow/memory-package.json' : '⏸ no sidecar (run: npx ruflo@latest doctor --fix)'}`);
|
||||
console.log(` Store: ${existsSync(STORE_PATH) ? '✅ ' + STORE_PATH : '⏸ Not initialized'}`);
|
||||
console.log(` LearningBridge: ${config.learningBridge.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
|
||||
if (existsSync(STORE_PATH)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
|
||||
console.log(` Entries: ${Array.isArray(data) ? data.length : 0}`);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
const command = process.argv[2] || 'status';
|
||||
|
||||
// Dynamic import() failures can surface as unhandled rejections on a later
|
||||
// microtask even when the awaiting call site already caught them, which would
|
||||
// otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
|
||||
// reason under RUFLO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
|
||||
// (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
if (DEBUG) {
|
||||
const detail = reason && reason.message ? reason.message : String(reason);
|
||||
process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case 'import': await doImport(); break;
|
||||
case 'sync': await doSync(); break;
|
||||
case 'status': await doStatus(); break;
|
||||
default:
|
||||
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Hooks must never crash Claude Code - fail silently
|
||||
try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
|
||||
}
|
||||
// Force clean exit — process.exitCode alone isn't enough if async errors override it
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"manifest": {
|
||||
"version": "3.32.2",
|
||||
"version": "3.32.34",
|
||||
"files": {
|
||||
"auto-memory-hook.mjs": "e3e1033b24704992ddef6b31c7fa9dd7fcd9e1af7935dd77ef73402b916b31e6",
|
||||
"hook-handler.cjs": "f87ec28684bfc5fd0a54c46bd2a53a3fb037defe71d0ea4b8a5cb88a2cd87a3f",
|
||||
"intelligence.cjs": "5a55d979cb7ba5c8c4f27f3b2e6d686fbb1045d180023b803da672a37e05b915",
|
||||
"statusline.cjs": "f6fcaedad7b521248ba57c494eb1a0ce696679fa3f8709f81edf3fdecef369c2"
|
||||
"auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
|
||||
"hook-handler.cjs": "50ea92a72651bdc95634f7588d56a5870963168eef5226f66ed14af3b47c8d9a",
|
||||
"intelligence.cjs": "bd1f8e4b034944aee1df0391dc47ac2e8cc4b3aa542c65407a59d49620bbf76b",
|
||||
"statusline.cjs": "d2a0eac56d1267d8dbed2d70b09feb592299469196ec4b215909f2b052144882"
|
||||
}
|
||||
},
|
||||
"signature": "r7TaWKVLZ6gxssGRM31J6usZrRm1Y5TAZXNmOfeLn+7VzWMrH6pZZPGTfSv6WMW49j8Ko3aamDBtahasWybSAw==",
|
||||
"signature": "i0qilACTabRlI6VORA0QyP0r4EuvJsrvtkucxia3TStgq/la250iesyKjlcHbLV/6fshZiGtPMLq1nmXLf+ZAw==",
|
||||
"algorithm": "ed25519"
|
||||
}
|
||||
|
||||
+565
-565
File diff suppressed because it is too large
Load Diff
+1058
-1058
File diff suppressed because it is too large
Load Diff
+1022
-968
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,54 @@ All notable changes to the Agentic QE project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.13.5] - 2026-08-03
|
||||
|
||||
Quality decisions and generated tests now carry evidence that reflects what AQE
|
||||
actually measured or executed, while initialization and embedding workflows are
|
||||
more reliable across supported environments.
|
||||
|
||||
### Added
|
||||
|
||||
- **Measured quality-gate evidence** ([#596]). CLI and MCP quality evaluation
|
||||
now share canonical thresholds and require fresh, attributed measurements for
|
||||
coverage, passing tests, critical bugs, code smells, security findings,
|
||||
technical debt, and duplication. Missing, malformed, or stale evidence fails
|
||||
closed instead of silently receiving a fabricated passing value.
|
||||
- **Optional status-line installation** ([#591]). `aqe init --no-statusline`
|
||||
skips AQE's status line, removes an existing AQE-owned status line during an
|
||||
upgrade, and preserves a project-authored status line.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Generated tests are trustworthy and executable** ([#577]). Test generation
|
||||
preserves framework imports, validates evidence before reporting success, and
|
||||
no longer treats structurally incomplete output as a passing quality result.
|
||||
- **Node's built-in test runner is honored end to end** ([#592]). Test discovery
|
||||
filters Node test files correctly, execution uses the selected framework, and
|
||||
retry confidence remains bounded.
|
||||
- **Batch embedding semantics are consistent** ([#585]). Mixed batches send only
|
||||
semantic content to the embedding provider, nonsemantic entries receive zero
|
||||
vectors in their original positions, and all-nonsemantic batches avoid
|
||||
provider initialization entirely.
|
||||
- **HNSW initialization works in native ESM** ([#586]). Runtime dependency
|
||||
loading no longer assumes a CommonJS global `require`.
|
||||
- **Learning-engine tests use isolated persistence** ([#588]), preventing shared
|
||||
worker databases from accumulating duplicate genesis data and causing long
|
||||
test runs.
|
||||
|
||||
### Changed
|
||||
|
||||
- Refreshed bundled Claude helpers and development-only Ruflo dependencies,
|
||||
including upstream security and compatibility fixes.
|
||||
|
||||
[#577]: https://github.com/proffesor-for-testing/agentic-qe/issues/577
|
||||
[#585]: https://github.com/proffesor-for-testing/agentic-qe/issues/585
|
||||
[#586]: https://github.com/proffesor-for-testing/agentic-qe/issues/586
|
||||
[#588]: https://github.com/proffesor-for-testing/agentic-qe/issues/588
|
||||
[#591]: https://github.com/proffesor-for-testing/agentic-qe/issues/591
|
||||
[#592]: https://github.com/proffesor-for-testing/agentic-qe/issues/592
|
||||
[#596]: https://github.com/proffesor-for-testing/agentic-qe/issues/596
|
||||
|
||||
## [3.13.4] - 2026-08-02
|
||||
|
||||
Codex users now receive a complete, upgradeable Agentic QE workflow instead of
|
||||
|
||||
@@ -940,7 +940,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"generatedBy": "Agentic QE Fleet",
|
||||
"fleetVersion": "3.13.4",
|
||||
"fleetVersion": "3.13.5",
|
||||
"manifestVersion": "1.4.0",
|
||||
"lastUpdated": "2026-04-13T00:00:00.000Z",
|
||||
"contributors": [
|
||||
|
||||
@@ -234,7 +234,7 @@ aqe security --compliance gdpr,hipaa,soc2 # Compliance checks
|
||||
aqe security --url-validate https://example.com # URL + PII exposure validation
|
||||
|
||||
# Quality gates
|
||||
aqe quality --gate # Evaluate quality gate (pass/fail with exit codes)
|
||||
aqe quality --gate # Measured coverage/tests: exit 0 pass, 1 fail, 2 near threshold
|
||||
|
||||
# Code intelligence
|
||||
aqe code index src/ # Index codebase into knowledge graph
|
||||
@@ -492,5 +492,14 @@ aqe test generate src/
|
||||
aqe coverage src/ --risk --gaps
|
||||
aqe security --sast -t src/
|
||||
aqe quality --gate
|
||||
aqe fleet init --wizard
|
||||
```
|
||||
aqe fleet init --wizard
|
||||
```
|
||||
|
||||
`aqe quality --gate` currently evaluates only measured line coverage and test
|
||||
pass-rate evidence stored by the coverage and test commands. It deliberately
|
||||
does not invent values for security vulnerabilities, critical bugs, code
|
||||
smells, technical debt, or duplication; restoring those measured checks is
|
||||
tracked in [#596](https://github.com/proffesor-for-testing/agentic-qe/issues/596).
|
||||
The command exits `0` when all measured checks pass with more than five
|
||||
percentage points of headroom, `1` when a check fails, and `2` when the gate
|
||||
passes but a check has less than five percentage points of headroom.
|
||||
|
||||
@@ -4,6 +4,7 @@ All Agentic QE release notes organized by version.
|
||||
|
||||
| Version | Date | Highlights |
|
||||
|---------|------|------------|
|
||||
| [v3.13.5](v3.13.5.md) | 2026-08-03 | Measured quality gates and trustworthy cross-framework test execution. |
|
||||
| [v3.13.4](v3.13.4.md) | 2026-08-02 | Reliable Codex fleet setup, upgrades, hooks, and verification. |
|
||||
| [v3.13.3](v3.13.3.md) | 2026-07-29 | Learning capture no longer stops silently under `AQE_DISABLE_WAL` (it now refuses to write in an unsafe journal mode and names what is holding the DB), and QE-Court's shipped panel no longer violates its own `writerIsNeverJuror` rule (#576). |
|
||||
| [v3.13.2](v3.13.2.md) | 2026-07-24 | Stops hook-driven `brain.rvf`/`patterns.rvf` corruption loops by closing native stores after every hook and protecting same-process live locks (#574). |
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# v3.13.5 Release Notes
|
||||
|
||||
**Release Date:** 2026-08-03
|
||||
|
||||
## Highlights
|
||||
|
||||
AQE now bases quality decisions on fresh, attributed measurements and produces
|
||||
more trustworthy, executable tests across Jest, Vitest, and Node's built-in test
|
||||
runner. Initialization and embeddings are also more reliable in real projects.
|
||||
|
||||
## Added
|
||||
|
||||
- Canonical quality evidence for coverage, passing tests, bugs, code smells,
|
||||
security findings, technical debt, and duplication.
|
||||
- Shared CLI and MCP quality thresholds that fail closed when measurements are
|
||||
missing, malformed, or stale.
|
||||
- `aqe init --no-statusline` for installations that do not want AQE's status
|
||||
line, without disturbing a project-authored status line.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Generated tests retain the correct framework imports and must satisfy
|
||||
evidence-backed validation before AQE reports success.
|
||||
- Node test discovery, execution, and retry behavior now honor the selected
|
||||
framework consistently.
|
||||
- Mixed embedding batches preserve output order while excluding nonsemantic
|
||||
content from provider calls; entirely nonsemantic batches make no provider
|
||||
call.
|
||||
- HNSW runtime loading works when AQE is imported as native ESM.
|
||||
- Learning-engine unit tests no longer share persistence across test cases.
|
||||
|
||||
## Changed
|
||||
|
||||
- Refreshed bundled Claude helpers.
|
||||
- Updated development-only Ruflo UI and tooling dependencies, including upstream
|
||||
security fixes.
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
No configuration migration is required. Existing installations can upgrade and
|
||||
refresh their managed assets normally:
|
||||
|
||||
```bash
|
||||
npx agentic-qe init --auto --upgrade
|
||||
```
|
||||
|
||||
Projects that want to remove only AQE's managed status line can run:
|
||||
|
||||
```bash
|
||||
npx agentic-qe init --auto --upgrade --no-statusline
|
||||
```
|
||||
|
||||
Project-authored status-line configuration is preserved.
|
||||
|
||||
Tracking: [#577](https://github.com/proffesor-for-testing/agentic-qe/issues/577),
|
||||
[#585](https://github.com/proffesor-for-testing/agentic-qe/issues/585),
|
||||
[#586](https://github.com/proffesor-for-testing/agentic-qe/issues/586),
|
||||
[#588](https://github.com/proffesor-for-testing/agentic-qe/issues/588),
|
||||
[#591](https://github.com/proffesor-for-testing/agentic-qe/issues/591),
|
||||
[#592](https://github.com/proffesor-for-testing/agentic-qe/issues/592), and
|
||||
[#596](https://github.com/proffesor-for-testing/agentic-qe/issues/596).
|
||||
Generated
+4
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "agentic-qe",
|
||||
"version": "3.13.4",
|
||||
"version": "3.13.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agentic-qe",
|
||||
"version": "3.13.4",
|
||||
"version": "3.13.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -28,7 +28,6 @@
|
||||
"pg": "^8.17.2",
|
||||
"prime-radiant-advanced-wasm": "^0.1.3",
|
||||
"secure-json-parse": "^4.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"uuid": "^14.0.0",
|
||||
"vibium": "^0.1.2",
|
||||
"web-tree-sitter": "~0.26.8",
|
||||
@@ -60,6 +59,7 @@
|
||||
"glob": "^13.0.0",
|
||||
"msw": "^2.12.7",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"engines": {
|
||||
@@ -11248,6 +11248,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agentic-qe",
|
||||
"version": "3.13.4",
|
||||
"version": "3.13.5",
|
||||
"description": "Agentic Quality Engineering V3 - Domain-Driven Design Architecture with 13 Bounded Contexts, O(log n) coverage analysis, ReasoningBank learning, 60 specialized QE agents, mathematical Coherence verification, deep Claude Flow integration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
@@ -71,7 +71,7 @@
|
||||
"test:unit:fast": "vitest run --exclude='**/qe-reasoning-bank.test.ts' --exclude='**/qe-reasoning-bank-feedback-loop.test.ts' --exclude='**/aqe-learning-engine.test.ts' --exclude='**/aqe-learning-engine-patterns.test.ts' tests/unit/adapters tests/unit/shared tests/unit/cli tests/unit/learning tests/unit/kernel tests/unit/workers tests/unit/routing tests/unit/strange-loop tests/unit/sync tests/unit/feedback tests/unit/error-paths tests/unit/early-exit tests/unit/causal-discovery tests/unit/neural-optimizer tests/unit/test-scheduling tests/unit/logging tests/unit/validation tests/unit/memory tests/unit/performance tests/unit/scripts tests/unit/planning",
|
||||
"test:unit:heavy": "vitest run tests/unit/coordination tests/unit/domains tests/unit/integrations tests/unit/optimization tests/unit/init --exclude='**/browser/**' --exclude='**/*.e2e.test.ts' --exclude='**/vibium/**' --exclude='**/browser-swarm-coordinator.test.ts'",
|
||||
"test:unit:mcp": "vitest run tests/unit/mcp --exclude='**/mcp/handlers/domain-handlers.test.ts' --fileParallelism=false",
|
||||
"test:ci": "vitest run --exclude='**/browser/**' --exclude='**/*.e2e.test.ts' --exclude='**/vibium/**' --exclude='**/integration/browser/**' --exclude='**/browser-swarm-coordinator.test.ts' --exclude='**/mcp/handlers/domain-handlers.test.ts'",
|
||||
"test:ci": "vitest run --exclude='**/browser/**' --exclude='**/*.e2e.test.ts' --exclude='**/vibium/**' --exclude='**/integration/browser/**' --exclude='**/browser-swarm-coordinator.test.ts' --exclude='**/mcp/handlers/domain-handlers.test.ts' --exclude='**/fixtures/init-corpus/**'",
|
||||
"test:e2e": "vitest run tests/integration/browser --testTimeout=120000",
|
||||
"test:safe": "NODE_OPTIONS='--max-old-space-size=768 --expose-gc' vitest run --maxForks=1",
|
||||
"test:dev": "npm run test:unit:fast",
|
||||
@@ -183,7 +183,6 @@
|
||||
"pg": "^8.17.2",
|
||||
"prime-radiant-advanced-wasm": "^0.1.3",
|
||||
"secure-json-parse": "^4.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"uuid": "^14.0.0",
|
||||
"vibium": "^0.1.2",
|
||||
"web-tree-sitter": "~0.26.8",
|
||||
@@ -260,6 +259,7 @@
|
||||
"glob": "^13.0.0",
|
||||
"msw": "^2.12.7",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+32
-47
@@ -10,57 +10,42 @@ import chalk from 'chalk';
|
||||
import type { MemoryBackend } from '../../kernel/interfaces.js';
|
||||
import type { CLIContext } from '../handlers/interfaces.js';
|
||||
import { type OutputFormat, type QualityGateResult, writeOutput, toJSON, qualityGateToMarkdown } from '../utils/ci-output.js';
|
||||
import {
|
||||
evaluateQualityEvidence,
|
||||
loadQualityEvidence as loadCanonicalQualityEvidence,
|
||||
type QualityEvidenceValues,
|
||||
} from '../../domains/quality-assessment/quality-evidence.js';
|
||||
|
||||
export async function loadQualityEvidence(memory: MemoryBackend): Promise<{
|
||||
coverage: number;
|
||||
testsPassing: number;
|
||||
}> {
|
||||
const coverage = await memory.get<{ line?: number }>('coverage:latest');
|
||||
const tests = await memory.get<{ passed?: number; failed?: number; skipped?: number }>(
|
||||
'test-run:latest',
|
||||
{ namespace: 'test-execution' }
|
||||
);
|
||||
if (!coverage || !tests) {
|
||||
throw new Error(
|
||||
'No measured quality evidence found in AgentDB. Run coverage and `aqe test execute` before `aqe quality --gate`.'
|
||||
);
|
||||
}
|
||||
const total = (tests.passed ?? 0) + (tests.failed ?? 0) + (tests.skipped ?? 0);
|
||||
if (!Number.isFinite(coverage.line) || total <= 0 || !Number.isFinite(tests.passed)) {
|
||||
throw new Error('Measured quality evidence is malformed or incomplete.');
|
||||
}
|
||||
export async function loadQualityEvidence(memory: MemoryBackend): Promise<QualityEvidenceValues> {
|
||||
return loadCanonicalQualityEvidence(memory);
|
||||
}
|
||||
|
||||
export function evaluateMeasuredQualityEvidence(measured: QualityEvidenceValues): QualityGateResult {
|
||||
const evaluation = evaluateQualityEvidence(measured);
|
||||
return {
|
||||
coverage: coverage.line!,
|
||||
testsPassing: ((tests.passed ?? 0) / total) * 100,
|
||||
passed: evaluation.passed,
|
||||
score: 'N/A',
|
||||
checks: evaluation.checks,
|
||||
recommendations: evaluation.recommendations,
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateMeasuredQualityEvidence(measured: {
|
||||
coverage: number;
|
||||
testsPassing: number;
|
||||
}): QualityGateResult {
|
||||
const checks = [
|
||||
{
|
||||
name: 'coverage',
|
||||
passed: measured.coverage >= 80,
|
||||
value: measured.coverage,
|
||||
threshold: 80,
|
||||
},
|
||||
{
|
||||
name: 'testsPassing',
|
||||
passed: measured.testsPassing >= 95,
|
||||
value: measured.testsPassing,
|
||||
threshold: 95,
|
||||
},
|
||||
];
|
||||
return {
|
||||
passed: checks.every(check => check.passed),
|
||||
score: 'N/A',
|
||||
checks,
|
||||
recommendations: checks
|
||||
.filter(check => !check.passed)
|
||||
.map(check => `${check.name} is below its measured threshold.`),
|
||||
};
|
||||
/**
|
||||
* Published quality command exit contract:
|
||||
* 0 = passed with more than five percentage points of headroom
|
||||
* 1 = one or more measured checks failed
|
||||
* 2 = passed, but at least one measured check has less than five points of headroom
|
||||
*/
|
||||
export function getMeasuredQualityExitCode(result: QualityGateResult): 0 | 1 | 2 {
|
||||
if (!result.passed) return 1;
|
||||
return result.checks.some(check => (
|
||||
(check as typeof check & { direction?: string }).direction !== 'max'
|
||||
&&
|
||||
typeof check.value === 'number'
|
||||
&& typeof check.threshold === 'number'
|
||||
&& check.value >= check.threshold
|
||||
&& check.value < check.threshold + 5
|
||||
)) ? 2 : 0;
|
||||
}
|
||||
|
||||
export function createQualityCommand(
|
||||
@@ -111,7 +96,7 @@ export function createQualityCommand(
|
||||
console.log('');
|
||||
}
|
||||
|
||||
await cleanupAndExit(gateResult.passed ? 0 : 1);
|
||||
await cleanupAndExit(getMeasuredQualityExitCode(gateResult));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import type { CLIContext } from '../handlers/interfaces.js';
|
||||
import { filterTestFilesForFramework, walkSourceFiles } from '../utils/file-discovery.js';
|
||||
import { type OutputFormat, writeOutput, toJSON, toJUnit, testRunToMarkdown, type TestRunSummary } from '../utils/ci-output.js';
|
||||
import { type OutputFormat, writeOutput, toJSON, toJUnit, testRunToMarkdown, type TestRunSummary } from '../utils/ci-output.js';
|
||||
import { writeQualityEvidence } from '../../domains/quality-assessment/quality-evidence.js';
|
||||
|
||||
export function createTestCommand(
|
||||
context: CLIContext,
|
||||
@@ -170,6 +171,12 @@ export function createTestCommand(
|
||||
duration: run.duration,
|
||||
measuredAt: new Date().toISOString(),
|
||||
}, { namespace: 'test-execution', persist: true });
|
||||
await writeQualityEvidence(context.kernel!.memory, {
|
||||
testsPassing: total > 0 ? (run.passed / total) * 100 : 0,
|
||||
}, {
|
||||
measuredAt: new Date().toISOString(),
|
||||
source: 'aqe test execute',
|
||||
});
|
||||
|
||||
if (format === 'json') {
|
||||
writeOutput(toJSON(run), options.output);
|
||||
|
||||
@@ -45,7 +45,8 @@ export class InitHandler implements ICommandHandler {
|
||||
.option('-d, --domains <domains>', 'Comma-separated list of domains to enable', 'all')
|
||||
.option('-m, --max-agents <number>', 'Maximum concurrent agents', '15')
|
||||
.option('--memory <backend>', 'Memory backend (sqlite|agentdb|hybrid|memory). "memory" = database-free, in-memory only.', 'hybrid')
|
||||
.option('--no-database', 'Database-free install: alias for `--memory memory`. Skips the SQLite database phase and runs any MCP server in-memory — nothing is written to .agentic-qe/.')
|
||||
.option('--no-database', 'Database-free install: alias for `--memory memory`. Skips the SQLite database phase and runs any MCP server in-memory — nothing is written to .agentic-qe/.')
|
||||
.option('--no-statusline', 'Do not install AQE\'s Claude Code status line; remove an existing AQE-owned status line while preserving custom ones')
|
||||
.option('--lazy', 'Enable lazy loading of domains')
|
||||
.option('--wizard', 'Run interactive setup wizard')
|
||||
.option('--auto', 'Auto-configure based on project analysis')
|
||||
@@ -145,9 +146,10 @@ export class InitHandler implements ICommandHandler {
|
||||
// only honored by the modular orchestrator. Route there too, or the flag
|
||||
// silently falls through to runStandardInit and installs the full Claude
|
||||
// surface it was meant to suppress.
|
||||
const noClaudeRequested = options.claude === false;
|
||||
|
||||
if (options.modular || platformRequested || databaseFree || noClaudeRequested) {
|
||||
const noClaudeRequested = options.claude === false;
|
||||
const noStatusLineRequested = options.statusline === false;
|
||||
|
||||
if (options.modular || platformRequested || databaseFree || noClaudeRequested || noStatusLineRequested) {
|
||||
console.log(chalk.blue('\n Agentic QE v3 Initialization\n'));
|
||||
await this.runModularInit(options, context);
|
||||
return;
|
||||
@@ -214,8 +216,9 @@ export class InitHandler implements ICommandHandler {
|
||||
withContinueDev: options.withContinuedev,
|
||||
noMcp: options.noMcp && !options.withMcp,
|
||||
noGovernance: options.noGovernance,
|
||||
noClaude,
|
||||
memoryBackend: memoryOnly ? 'memory' : undefined,
|
||||
noClaude,
|
||||
noStatusLine: options.statusline === false,
|
||||
memoryBackend: memoryOnly ? 'memory' : undefined,
|
||||
});
|
||||
|
||||
console.log(chalk.white(' Analyzing project...\n'));
|
||||
@@ -648,8 +651,10 @@ interface InitOptions {
|
||||
withClaudeFlow?: boolean;
|
||||
skipClaudeFlow?: boolean;
|
||||
noGovernance?: boolean;
|
||||
/** commander negatable flag: `--no-claude` sets this to false (#532). */
|
||||
claude?: boolean;
|
||||
/** commander negatable flag: `--no-claude` sets this to false (#532). */
|
||||
claude?: boolean;
|
||||
/** commander negatable flag: `--no-statusline` sets this to false (#591). */
|
||||
statusline?: boolean;
|
||||
modular?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import type { HybridRouter, ChatResponse } from '../../../shared/llm';
|
||||
import { toError } from '../../../shared/error-utils.js';
|
||||
import { safeJsonParse } from '../../../shared/safe-json.js';
|
||||
import { writeQualityEvidence } from '../../quality-assessment/quality-evidence.js';
|
||||
|
||||
// ============================================================================
|
||||
// Service Interface
|
||||
@@ -613,6 +614,10 @@ Provide thoughtful, specific analysis based on the coverage data. Do not include
|
||||
|
||||
// Store latest snapshot
|
||||
await this.memory.set('coverage:latest', summary, { persist: true });
|
||||
await writeQualityEvidence(this.memory, { coverage: summary.line }, {
|
||||
measuredAt: new Date().toISOString(),
|
||||
source: 'coverage-analysis',
|
||||
});
|
||||
|
||||
// Store historical snapshot
|
||||
const timestamp = Date.now();
|
||||
|
||||
@@ -50,6 +50,21 @@ export {
|
||||
type DeploymentAccuracy,
|
||||
} from './services/deployment-advisor';
|
||||
|
||||
export {
|
||||
DEFAULT_QUALITY_EVIDENCE_MAX_AGE_MS,
|
||||
DEFAULT_QUALITY_THRESHOLDS,
|
||||
QUALITY_EVIDENCE_NAMESPACE,
|
||||
QUALITY_METRICS,
|
||||
evaluateQualityEvidence,
|
||||
loadQualityEvidence,
|
||||
writeQualityEvidence,
|
||||
type EvaluatedQualityCheck,
|
||||
type QualityEvidenceRecord,
|
||||
type QualityEvidenceValues,
|
||||
type QualityMetricName,
|
||||
type QualityThreshold,
|
||||
} from './quality-evidence.js';
|
||||
|
||||
// ============================================================================
|
||||
// Coherence-Gated Quality Gates (ADR-030)
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import type { MemoryBackend } from '../../kernel/interfaces.js';
|
||||
|
||||
export const QUALITY_EVIDENCE_NAMESPACE = 'quality-assessment';
|
||||
export const DEFAULT_QUALITY_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const QUALITY_METRICS = [
|
||||
'coverage',
|
||||
'testsPassing',
|
||||
'criticalBugs',
|
||||
'codeSmells',
|
||||
'securityVulnerabilities',
|
||||
'technicalDebt',
|
||||
'duplications',
|
||||
] as const;
|
||||
|
||||
export type QualityMetricName = typeof QUALITY_METRICS[number];
|
||||
export type QualityEvidenceValues = Record<QualityMetricName, number>;
|
||||
|
||||
export interface QualityEvidenceRecord {
|
||||
schemaVersion: 1;
|
||||
metric: QualityMetricName;
|
||||
value: number;
|
||||
measuredAt: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface QualityThreshold {
|
||||
direction: 'min' | 'max';
|
||||
value: number;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export const DEFAULT_QUALITY_THRESHOLDS: Record<QualityMetricName, QualityThreshold> = {
|
||||
coverage: { direction: 'min', value: 80, severity: 'high' },
|
||||
testsPassing: { direction: 'min', value: 95, severity: 'critical' },
|
||||
criticalBugs: { direction: 'max', value: 0, severity: 'critical' },
|
||||
codeSmells: { direction: 'max', value: 20, severity: 'medium' },
|
||||
securityVulnerabilities: { direction: 'max', value: 0, severity: 'critical' },
|
||||
technicalDebt: { direction: 'max', value: 5, severity: 'medium' },
|
||||
duplications: { direction: 'max', value: 5, severity: 'low' },
|
||||
};
|
||||
|
||||
export interface EvaluatedQualityCheck {
|
||||
name: QualityMetricName;
|
||||
passed: boolean;
|
||||
value: number;
|
||||
threshold: number;
|
||||
direction: 'min' | 'max';
|
||||
severity: QualityThreshold['severity'];
|
||||
}
|
||||
|
||||
function evidenceKey(metric: QualityMetricName): string {
|
||||
return `quality-evidence:${metric}:latest`;
|
||||
}
|
||||
|
||||
function parseMeasuredAt(measuredAt: unknown, now: number, maxAgeMs: number): number {
|
||||
if (typeof measuredAt !== 'string') {
|
||||
throw new Error('Measured quality evidence is malformed: measuredAt is required.');
|
||||
}
|
||||
const timestamp = Date.parse(measuredAt);
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
throw new Error('Measured quality evidence is malformed: measuredAt is invalid.');
|
||||
}
|
||||
if (timestamp > now + 60_000) {
|
||||
throw new Error('Measured quality evidence is malformed: measuredAt is in the future.');
|
||||
}
|
||||
if (now - timestamp > maxAgeMs) {
|
||||
throw new Error('Measured quality evidence is stale. Run the relevant quality analyzers again.');
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
function validateRecord(
|
||||
record: unknown,
|
||||
metric: QualityMetricName,
|
||||
now: number,
|
||||
maxAgeMs: number,
|
||||
): asserts record is QualityEvidenceRecord {
|
||||
if (!record || typeof record !== 'object') {
|
||||
throw new Error(`No measured quality evidence found for ${metric}.`);
|
||||
}
|
||||
const candidate = record as Partial<QualityEvidenceRecord>;
|
||||
if (
|
||||
candidate.schemaVersion !== 1
|
||||
|| candidate.metric !== metric
|
||||
|| !Number.isFinite(candidate.value)
|
||||
|| candidate.value! < 0
|
||||
|| typeof candidate.source !== 'string'
|
||||
|| candidate.source.trim().length === 0
|
||||
) {
|
||||
throw new Error(`Measured quality evidence for ${metric} is malformed or incomplete.`);
|
||||
}
|
||||
parseMeasuredAt(candidate.measuredAt, now, maxAgeMs);
|
||||
}
|
||||
|
||||
export async function writeQualityEvidence(
|
||||
memory: MemoryBackend,
|
||||
values: Partial<QualityEvidenceValues>,
|
||||
metadata: { measuredAt: string; source: string },
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
parseMeasuredAt(metadata.measuredAt, now, Number.POSITIVE_INFINITY);
|
||||
if (metadata.source.trim().length === 0) {
|
||||
throw new Error('Quality evidence source is required.');
|
||||
}
|
||||
|
||||
for (const metric of QUALITY_METRICS) {
|
||||
const value = values[metric];
|
||||
if (value === undefined) continue;
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`Quality evidence value for ${metric} must be a non-negative finite number.`);
|
||||
}
|
||||
const record: QualityEvidenceRecord = {
|
||||
schemaVersion: 1,
|
||||
metric,
|
||||
value,
|
||||
measuredAt: metadata.measuredAt,
|
||||
source: metadata.source,
|
||||
};
|
||||
await memory.set(evidenceKey(metric), record, {
|
||||
namespace: QUALITY_EVIDENCE_NAMESPACE,
|
||||
persist: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadQualityEvidence(
|
||||
memory: MemoryBackend,
|
||||
options: { now?: number; maxAgeMs?: number } = {},
|
||||
): Promise<QualityEvidenceValues> {
|
||||
const now = options.now ?? Date.now();
|
||||
const maxAgeMs = options.maxAgeMs ?? DEFAULT_QUALITY_EVIDENCE_MAX_AGE_MS;
|
||||
const values = {} as QualityEvidenceValues;
|
||||
|
||||
for (const metric of QUALITY_METRICS) {
|
||||
const record = await memory.get<QualityEvidenceRecord>(evidenceKey(metric), {
|
||||
namespace: QUALITY_EVIDENCE_NAMESPACE,
|
||||
});
|
||||
validateRecord(record, metric, now, maxAgeMs);
|
||||
values[metric] = record.value;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function evaluateQualityEvidence(
|
||||
values: QualityEvidenceValues,
|
||||
): { passed: boolean; checks: EvaluatedQualityCheck[]; recommendations: string[] } {
|
||||
const checks = QUALITY_METRICS.map((name) => {
|
||||
const threshold = DEFAULT_QUALITY_THRESHOLDS[name];
|
||||
const value = values[name];
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
threshold: threshold.value,
|
||||
direction: threshold.direction,
|
||||
severity: threshold.severity,
|
||||
passed: threshold.direction === 'min' ? value >= threshold.value : value <= threshold.value,
|
||||
};
|
||||
});
|
||||
return {
|
||||
passed: checks.every((check) => check.passed),
|
||||
checks,
|
||||
recommendations: checks
|
||||
.filter((check) => !check.passed)
|
||||
.map((check) => `${check.name} failed its measured ${check.direction} threshold.`),
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { TEST_EXECUTION_CONSTANTS, LLM_ANALYSIS_CONSTANTS } from '../../constant
|
||||
import { toErrorMessage, toError } from '../../../shared/error-utils.js';
|
||||
import { safeJsonParse } from '../../../shared/safe-json.js';
|
||||
import { secureRandom, secureRandomInt } from '../../../shared/utils/crypto-random.js';
|
||||
import { writeQualityEvidence } from '../../quality-assessment/quality-evidence.js';
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
@@ -1296,6 +1297,14 @@ Provide:
|
||||
statement: result.coverage.statement ?? 0,
|
||||
files: result.fileCoverages?.length ?? 0,
|
||||
}, { persist: true });
|
||||
if (Number.isFinite(result.coverage.line)) {
|
||||
await writeQualityEvidence(this.memory, {
|
||||
coverage: result.coverage.line,
|
||||
}, {
|
||||
measuredAt: new Date().toISOString(),
|
||||
source: 'test-execution coverage',
|
||||
});
|
||||
}
|
||||
|
||||
// Store per-file coverage via memory.set() (not storeVector) so that
|
||||
// quality-analyzer's getStoredCoverage() can read it with memory.get()
|
||||
|
||||
@@ -85,17 +85,22 @@ export function getGeneratedTestSyntaxIssues(
|
||||
): TestQualityIssue[] {
|
||||
if (!isJavaScriptFamily(sourceFilePath)) return [];
|
||||
|
||||
const sourceFile = parseGeneratedTest(testCode, sourceFilePath);
|
||||
const diagnostics = (
|
||||
sourceFile as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }
|
||||
).parseDiagnostics ?? [];
|
||||
const diagnostics = ts.transpileModule(testCode, {
|
||||
fileName: sourceFilePath,
|
||||
reportDiagnostics: true,
|
||||
compilerOptions: {
|
||||
jsx: ts.JsxEmit.Preserve,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
target: ts.ScriptTarget.Latest,
|
||||
},
|
||||
}).diagnostics ?? [];
|
||||
|
||||
return diagnostics.map((diagnostic) => {
|
||||
const position = sourceFile.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
||||
const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
||||
return {
|
||||
type: 'syntax-error' as const,
|
||||
severity: 'error' as const,
|
||||
line: position.line + 1,
|
||||
line: position ? position.line + 1 : undefined,
|
||||
description: `Generated test has invalid syntax: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ')}`,
|
||||
suggestion: 'Repair the generated test syntax before accepting or executing it.',
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ export class ModularInitOrchestrator {
|
||||
noGovernance: options.noGovernance,
|
||||
noMcp: options.noMcp,
|
||||
noClaude: options.noClaude,
|
||||
noStatusLine: options.noStatusLine,
|
||||
memoryBackend: options.memoryBackend,
|
||||
},
|
||||
config: {},
|
||||
|
||||
@@ -119,7 +119,9 @@ export class HooksPhase extends BasePhase<HooksResult> {
|
||||
// - statusLine / includeCoAuthoredBy preserved when user-set (#362 follow-up)
|
||||
// - AQE-owned sections deep-merged so user additions survive
|
||||
const v3Sections = generateV3SettingsSections(config, projectRoot);
|
||||
applyV3Sections(settings, v3Sections);
|
||||
applyV3Sections(settings, v3Sections, {
|
||||
statusLine: !context.options.noStatusLine,
|
||||
});
|
||||
|
||||
// Enable MCP servers (deduplicate, replace old 'aqe' with 'agentic-qe')
|
||||
let existingMcp = (settings.enabledMcpjsonServers as string[]) || [];
|
||||
|
||||
@@ -151,7 +151,9 @@ export interface InitOptions {
|
||||
* OpenCode-only install). Opt-in via `--no-claude`; default install is
|
||||
* unchanged. Pairs naturally with `--no-database`.
|
||||
*/
|
||||
noClaude?: boolean;
|
||||
noClaude?: boolean;
|
||||
/** Skip AQE's Claude Code status line and remove a previously AQE-owned one. */
|
||||
noStatusLine?: boolean;
|
||||
/** @deprecated Use default behavior instead — MCP is now enabled by default */
|
||||
withMcp?: boolean;
|
||||
/**
|
||||
|
||||
+19
-12
@@ -198,18 +198,27 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
*
|
||||
* Mutates and returns `settings`.
|
||||
*/
|
||||
export function applyV3Sections(
|
||||
settings: Record<string, unknown>,
|
||||
sections: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
export function applyV3Sections(
|
||||
settings: Record<string, unknown>,
|
||||
sections: Record<string, unknown>,
|
||||
options: { statusLine?: boolean } = {},
|
||||
): Record<string, unknown> {
|
||||
for (const [key, value] of Object.entries(sections)) {
|
||||
if (key === '_aqePermissions') {
|
||||
const existingPerms = (settings.permissions as { allow?: string[]; deny?: string[] }) || {};
|
||||
const existingAllow = existingPerms.allow || [];
|
||||
const merged = [...new Set([...existingAllow, ...(value as string[])])];
|
||||
settings.permissions = { ...existingPerms, allow: merged };
|
||||
} else if (key === 'statusLine') {
|
||||
// Preserve a user's (or another tool's) custom status line.
|
||||
} else if (key === 'statusLine') {
|
||||
if (options.statusLine === false) {
|
||||
// Opting out removes only AQE-owned configuration. A status line from
|
||||
// the user or another tool remains outside AQE's ownership boundary.
|
||||
if (isAqeStatusLine(settings.statusLine)) {
|
||||
delete settings.statusLine;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Preserve a user's (or another tool's) custom status line.
|
||||
if (settings.statusLine === undefined || isAqeStatusLine(settings.statusLine)) {
|
||||
settings.statusLine = value;
|
||||
}
|
||||
@@ -238,12 +247,10 @@ export function generateV3SettingsSections(config: AQEInitConfig, projectRoot?:
|
||||
initialized: new Date().toISOString(),
|
||||
hooksConfigured: true,
|
||||
},
|
||||
statusLine: {
|
||||
type: 'command',
|
||||
command: 'sh -c \'node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/statusline-v3.cjs" 2>/dev/null || echo "▊ Agentic QE v3"\'',
|
||||
refreshMs: 5000,
|
||||
enabled: true,
|
||||
},
|
||||
statusLine: {
|
||||
type: 'command',
|
||||
command: 'sh -c \'node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/statusline-v3.cjs" 2>/dev/null || echo "▊ Agentic QE v3"\'',
|
||||
},
|
||||
// permissions are union-merged in 07-hooks.ts — not set here to avoid overwriting user entries (#362)
|
||||
_aqePermissions: [
|
||||
'Bash(npx agentic-qe:*)',
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
* @module integrations/embeddings/index/HNSWIndex
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import type {
|
||||
IEmbedding,
|
||||
IHNSWConfig,
|
||||
@@ -17,6 +19,8 @@ import type {
|
||||
ISearchOptions,
|
||||
} from '../base/types.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// hnswlib-node is an OPTIONAL dependency (issue #439, ADR-090 amendment).
|
||||
// Top-level static import would crash module load on platforms where the
|
||||
// native binary failed to compile (e.g. Windows without VS Build Tools).
|
||||
|
||||
@@ -346,11 +346,28 @@ export async function computeBatchEmbeddings(
|
||||
const fullConfig = { ...DEFAULT_EMBEDDING_CONFIG, ...config };
|
||||
|
||||
// Check which texts need computation
|
||||
const uncachedTexts: string[] = [];
|
||||
const uncachedIndices: number[] = [];
|
||||
const results: (number[] | null)[] = new Array(texts.length).fill(null);
|
||||
|
||||
// Init BEFORE cache lookup so the cache key namespace is stable.
|
||||
const uncachedTexts: string[] = [];
|
||||
const uncachedIndices: number[] = [];
|
||||
const results: (number[] | null)[] = new Array(texts.length).fill(null);
|
||||
|
||||
// Match computeRealEmbedding(): non-semantic inputs are represented by a
|
||||
// zero vector and must never reach the model or its cache (#585).
|
||||
const semanticIndices: number[] = [];
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
if (isNonSemanticText(texts[i])) {
|
||||
results[i] = new Array(getEmbeddingDimension()).fill(0);
|
||||
} else {
|
||||
semanticIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the single-item API's no-initialization behavior when every
|
||||
// input is non-semantic.
|
||||
if (semanticIndices.length === 0) {
|
||||
return results as number[][];
|
||||
}
|
||||
|
||||
// Init BEFORE cache lookup so the cache key namespace is stable.
|
||||
if (!embeddingModel) {
|
||||
await initializeModel(config);
|
||||
}
|
||||
@@ -358,9 +375,9 @@ export async function computeBatchEmbeddings(
|
||||
throw new Error('Embedding model failed to initialize');
|
||||
}
|
||||
|
||||
if (fullConfig.enableCache) {
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
const cached = embeddingCache.get(cacheKey(texts[i]));
|
||||
if (fullConfig.enableCache) {
|
||||
for (const i of semanticIndices) {
|
||||
const cached = embeddingCache.get(cacheKey(texts[i]));
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||
results[i] = cached.embedding;
|
||||
} else {
|
||||
@@ -368,11 +385,11 @@ export async function computeBatchEmbeddings(
|
||||
uncachedIndices.push(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uncachedTexts.push(...texts);
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
uncachedIndices.push(i);
|
||||
}
|
||||
} else {
|
||||
for (const i of semanticIndices) {
|
||||
uncachedTexts.push(texts[i]);
|
||||
uncachedIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute uncached embeddings
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema } from '../base';
|
||||
import { ToolResult } from '../../types';
|
||||
import { toErrorMessage } from '../../../shared/error-utils.js';
|
||||
import { getMemoryBackend } from '../base.js';
|
||||
import {
|
||||
DEFAULT_QUALITY_THRESHOLDS,
|
||||
loadQualityEvidence,
|
||||
writeQualityEvidence,
|
||||
} from '../../../domains/quality-assessment/quality-evidence.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -20,6 +26,7 @@ export interface QualityEvaluateParams {
|
||||
thresholds?: GateThresholds;
|
||||
includeAdvice?: boolean;
|
||||
riskTolerance?: 'low' | 'medium' | 'high';
|
||||
evidence?: { measuredAt: string; source: string };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -41,6 +48,7 @@ export interface GateThresholds {
|
||||
codeSmells?: { max: number };
|
||||
securityVulnerabilities?: { max: number };
|
||||
technicalDebt?: { max: number };
|
||||
duplications?: { max: number };
|
||||
}
|
||||
|
||||
export interface QualityEvaluateResult {
|
||||
@@ -97,7 +105,7 @@ export class QualityEvaluateTool extends MCPToolBase<QualityEvaluateParams, Qual
|
||||
context: MCPToolContext
|
||||
): Promise<ToolResult<QualityEvaluateResult>> {
|
||||
const {
|
||||
metrics = getDefaultMetrics(),
|
||||
metrics: suppliedMetrics,
|
||||
gateName = 'default',
|
||||
thresholds = getDefaultThresholds(),
|
||||
includeAdvice = true,
|
||||
@@ -105,6 +113,36 @@ export class QualityEvaluateTool extends MCPToolBase<QualityEvaluateParams, Qual
|
||||
} = params;
|
||||
|
||||
try {
|
||||
const memory = await getMemoryBackend(context);
|
||||
if (suppliedMetrics && !params.evidence) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Timestamped evidence provenance is required when supplying quality metrics.',
|
||||
};
|
||||
}
|
||||
if (suppliedMetrics && params.evidence) {
|
||||
await writeQualityEvidence(memory, suppliedMetrics, params.evidence);
|
||||
}
|
||||
const metrics = suppliedMetrics ?? await loadQualityEvidence(memory);
|
||||
const configuredMetrics = [
|
||||
['coverage', thresholds.coverage],
|
||||
['testsPassing', thresholds.testsPassing],
|
||||
['criticalBugs', thresholds.criticalBugs],
|
||||
['codeSmells', thresholds.codeSmells],
|
||||
['securityVulnerabilities', thresholds.securityVulnerabilities],
|
||||
['technicalDebt', thresholds.technicalDebt],
|
||||
['duplications', thresholds.duplications],
|
||||
] as const;
|
||||
const missingMetrics = configuredMetrics
|
||||
.filter(([name, threshold]) => threshold !== undefined && metrics[name] === undefined)
|
||||
.map(([name]) => name);
|
||||
if (missingMetrics.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Measured quality evidence missing configured metrics: ${missingMetrics.join(', ')}.`,
|
||||
};
|
||||
}
|
||||
|
||||
this.emitStream(context, {
|
||||
status: 'evaluating',
|
||||
message: `Evaluating quality gate: ${gateName}`,
|
||||
@@ -187,12 +225,38 @@ export class QualityEvaluateTool extends MCPToolBase<QualityEvaluateParams, Qual
|
||||
});
|
||||
}
|
||||
|
||||
if (thresholds.technicalDebt && metrics.technicalDebt !== undefined) {
|
||||
checks.push({
|
||||
name: 'Technical Debt',
|
||||
passed: metrics.technicalDebt <= thresholds.technicalDebt.max,
|
||||
value: metrics.technicalDebt,
|
||||
threshold: thresholds.technicalDebt.max,
|
||||
severity: 'medium',
|
||||
message: metrics.technicalDebt <= thresholds.technicalDebt.max
|
||||
? `${metrics.technicalDebt}h technical debt within threshold`
|
||||
: `${metrics.technicalDebt}h technical debt exceeds threshold of ${thresholds.technicalDebt.max}h`,
|
||||
});
|
||||
}
|
||||
|
||||
if (thresholds.duplications && metrics.duplications !== undefined) {
|
||||
checks.push({
|
||||
name: 'Duplications',
|
||||
passed: metrics.duplications <= thresholds.duplications.max,
|
||||
value: metrics.duplications,
|
||||
threshold: thresholds.duplications.max,
|
||||
severity: 'low',
|
||||
message: metrics.duplications <= thresholds.duplications.max
|
||||
? `${metrics.duplications}% duplication within threshold`
|
||||
: `${metrics.duplications}% duplication exceeds threshold of ${thresholds.duplications.max}%`,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate overall score and grade
|
||||
const passedChecks = checks.filter(c => c.passed).length;
|
||||
const totalChecks = checks.length;
|
||||
const score = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 100;
|
||||
const grade = calculateGrade(score, checks);
|
||||
const passed = checks.every(c => c.passed || c.severity !== 'critical');
|
||||
const passed = checks.every(c => c.passed);
|
||||
|
||||
// Generate deployment advice
|
||||
const deploymentAdvice: DeploymentAdvice | undefined = includeAdvice
|
||||
@@ -269,6 +333,14 @@ const QUALITY_EVALUATE_SCHEMA: MCPToolSchema = {
|
||||
enum: ['low', 'medium', 'high'],
|
||||
default: 'medium',
|
||||
},
|
||||
evidence: {
|
||||
type: 'object',
|
||||
description: 'Provenance required to persist supplied metrics as canonical measured evidence',
|
||||
properties: {
|
||||
measuredAt: { type: 'string', description: 'ISO-8601 measurement timestamp' },
|
||||
source: { type: 'string', description: 'Scanner or analyzer that produced the metrics' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -276,27 +348,15 @@ const QUALITY_EVALUATE_SCHEMA: MCPToolSchema = {
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function getDefaultMetrics(): QualityMetrics {
|
||||
return {
|
||||
coverage: 80,
|
||||
testsPassing: 95,
|
||||
criticalBugs: 0,
|
||||
codeSmells: 15,
|
||||
securityVulnerabilities: 0,
|
||||
technicalDebt: 4,
|
||||
duplications: 3,
|
||||
complexity: 8,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultThresholds(): GateThresholds {
|
||||
return {
|
||||
coverage: { min: 80 },
|
||||
testsPassing: { min: 95 },
|
||||
criticalBugs: { max: 0 },
|
||||
securityVulnerabilities: { max: 0 },
|
||||
codeSmells: { max: 50 },
|
||||
technicalDebt: { max: 8 },
|
||||
coverage: { min: DEFAULT_QUALITY_THRESHOLDS.coverage.value },
|
||||
testsPassing: { min: DEFAULT_QUALITY_THRESHOLDS.testsPassing.value },
|
||||
criticalBugs: { max: DEFAULT_QUALITY_THRESHOLDS.criticalBugs.value },
|
||||
securityVulnerabilities: { max: DEFAULT_QUALITY_THRESHOLDS.securityVulnerabilities.value },
|
||||
codeSmells: { max: DEFAULT_QUALITY_THRESHOLDS.codeSmells.value },
|
||||
technicalDebt: { max: DEFAULT_QUALITY_THRESHOLDS.technicalDebt.value },
|
||||
duplications: { max: DEFAULT_QUALITY_THRESHOLDS.duplications.value },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema } from '../base';
|
||||
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema, getMemoryBackend } from '../base';
|
||||
import { ToolResult } from '../../types';
|
||||
import { toErrorMessage } from '../../../shared/error-utils.js';
|
||||
import { safeJsonParse } from '../../../shared/safe-json.js';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ALL_SECURITY_PATTERNS,
|
||||
SECRET_PATTERNS,
|
||||
} from '../../../domains/security-compliance/services/scanners/security-patterns.js';
|
||||
import { writeQualityEvidence } from '../../../domains/quality-assessment/quality-evidence.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -231,6 +232,13 @@ export class SecurityScanTool extends MCPToolBase<SecurityScanParams, SecuritySc
|
||||
: severityOrder.length;
|
||||
const passed = worstSeverity > failThreshold;
|
||||
|
||||
await writeQualityEvidence(await getMemoryBackend(context), {
|
||||
securityVulnerabilities: vulnerabilities.length,
|
||||
}, {
|
||||
measuredAt: new Date().toISOString(),
|
||||
source: 'qe/security/scan',
|
||||
});
|
||||
|
||||
this.emitStream(context, {
|
||||
status: 'complete',
|
||||
message: `Scan complete: ${vulnerabilities.length} vulnerabilities found in ${files.length} files`,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* in production, written by sentence-transformers/all-MiniLM-L6-v2-style
|
||||
* models) and verifies:
|
||||
*
|
||||
* 1. Self-query returns id=self with score 1.0 (exact-match recall)
|
||||
* 1. Self-query returns an exact vector match with score 1.0
|
||||
* 2. Top-10 recall vs brute-force ground truth = 100%
|
||||
* 3. Repeated for several random query vectors to rule out one-off luck
|
||||
* 4. Vectors persisted in memory.db deserialize correctly into the backend
|
||||
@@ -183,7 +183,7 @@ describe('NativeHnswBackend — real qe-kernel fixture (#399)', () => {
|
||||
);
|
||||
|
||||
it.runIf(fixture !== null && fixture.length >= 100)(
|
||||
'should hit recall@10 >= 0.9 and top-1 == self on real qe-kernel embeddings',
|
||||
'should hit recall@10 >= 0.9 with an exact top match on real qe-kernel embeddings',
|
||||
() => {
|
||||
const pool = fixture!;
|
||||
const backend = new NativeHnswBackend({
|
||||
@@ -209,13 +209,12 @@ describe('NativeHnswBackend — real qe-kernel fixture (#399)', () => {
|
||||
// them based on entry-point luck. 90-100% recall@10 is the
|
||||
// standard approximate-HNSW guarantee.
|
||||
//
|
||||
// The TOP-1 self-match assertion is the strict bug-detector — it's
|
||||
// mathematically impossible to fail unless HNSW is fundamentally
|
||||
// broken (which is exactly what @ruvector/router 0.1.28 was: it
|
||||
// returned random non-neighbors with recall@10 = 0% to 10%). Anything
|
||||
// above 0.5 recall@10 here would already be a clear pass. We hold
|
||||
// the bar at 0.9 to catch tuning regressions while accepting the
|
||||
// legitimate plateau-region behavior of real embeddings.
|
||||
// The TOP-1 exact-vector assertion is the strict bug-detector. Several
|
||||
// real fixture rows can contain byte-identical embeddings, so requiring
|
||||
// the queried row's numeric id would make the result depend on arbitrary
|
||||
// tie-breaking. @ruvector/router 0.1.28 returned random non-neighbors
|
||||
// with recall@10 = 0% to 10%; requiring an exact top result still catches
|
||||
// that failure while accepting indistinguishable duplicate vectors.
|
||||
const RECALL_FLOOR = 0.9;
|
||||
|
||||
for (const queryIdx of [
|
||||
@@ -234,7 +233,8 @@ describe('NativeHnswBackend — real qe-kernel fixture (#399)', () => {
|
||||
const recallAt10 =
|
||||
[...groundTruthIds].filter((id) => hitIds.has(id)).length / groundTruthIds.size;
|
||||
|
||||
expect(results[0].id).toBe(queryIdx); // self MUST be top-1 (exact-match guarantee)
|
||||
expect(pool[results[0].id].vector).toEqual(queryVector);
|
||||
expect(results[0].score).toBeGreaterThanOrEqual(0.999);
|
||||
expect(recallAt10).toBeGreaterThanOrEqual(RECALL_FLOOR);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const originalRequire = Object.getOwnPropertyDescriptor(globalThis, 'require');
|
||||
|
||||
afterEach(() => {
|
||||
if (originalRequire) {
|
||||
Object.defineProperty(globalThis, 'require', originalRequire);
|
||||
} else {
|
||||
Reflect.deleteProperty(globalThis, 'require');
|
||||
}
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe('HNSWIndex ESM compatibility', () => {
|
||||
it('should initialize when CommonJS require is unavailable globally', async () => {
|
||||
// Arrange: reproduce the published unbundled ESM environment from #586.
|
||||
Reflect.deleteProperty(globalThis, 'require');
|
||||
vi.resetModules();
|
||||
|
||||
// Act
|
||||
const { HNSWEmbeddingIndex } = await import(
|
||||
'../../../src/integrations/embeddings/index/HNSWIndex.js'
|
||||
);
|
||||
const index = new HNSWEmbeddingIndex({ dimension: 4 });
|
||||
index.initializeIndex('test');
|
||||
|
||||
// Assert
|
||||
expect(index.isInitialized('test')).toBe(true);
|
||||
index.clearAll();
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { InitOrchestrator, type InitOrchestratorOptions } from '../../../src/init/init-wizard';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import { InitHandler } from '../../../src/cli/handlers/init-handler';
|
||||
import type { CLIContext } from '../../../src/cli/handlers/interfaces';
|
||||
|
||||
// Mock fs module
|
||||
vi.mock('fs', async () => {
|
||||
@@ -89,6 +92,21 @@ describe('Init Command', () => {
|
||||
});
|
||||
|
||||
describe('Init Options Validation', () => {
|
||||
it('should parse --no-statusline as an explicit opt-out', async () => {
|
||||
const program = new Command();
|
||||
const handler = new InitHandler(async () => undefined as never);
|
||||
const context = {} as CLIContext;
|
||||
const execute = vi.spyOn(handler, 'execute').mockResolvedValue(undefined);
|
||||
handler.register(program, context);
|
||||
|
||||
await program.parseAsync(['node', 'aqe', 'init', '--no-statusline']);
|
||||
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ statusline: false }),
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept valid domain list', () => {
|
||||
const domains = 'test-generation,coverage-analysis,quality-assessment';
|
||||
const domainList = domains.split(',');
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
evaluateMeasuredQualityEvidence,
|
||||
getMeasuredQualityExitCode,
|
||||
loadQualityEvidence,
|
||||
} from '../../../src/cli/commands/quality.js';
|
||||
|
||||
describe('quality command evidence loading', () => {
|
||||
const passing = {
|
||||
coverage: 90,
|
||||
testsPassing: 100,
|
||||
criticalBugs: 0,
|
||||
codeSmells: 10,
|
||||
securityVulnerabilities: 0,
|
||||
technicalDebt: 2,
|
||||
duplications: 3,
|
||||
};
|
||||
|
||||
it('refuses to invent zero metrics when AgentDB has no measured evidence', async () => {
|
||||
const memory = { get: async () => undefined };
|
||||
await expect(loadQualityEvidence(memory as never)).rejects.toThrow(/no measured quality evidence/i);
|
||||
@@ -12,28 +23,35 @@ describe('quality command evidence loading', () => {
|
||||
|
||||
it('loads measured coverage and test pass rate from canonical AgentDB keys', async () => {
|
||||
const memory = {
|
||||
get: async (key: string) => key === 'coverage:latest'
|
||||
? { line: 81.25 }
|
||||
: { passed: 38, failed: 2, skipped: 0 },
|
||||
get: async (key: string) => {
|
||||
const metric = key.split(':')[1] as keyof typeof passing;
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
metric,
|
||||
value: passing[metric],
|
||||
measuredAt: new Date().toISOString(),
|
||||
source: 'test',
|
||||
};
|
||||
},
|
||||
};
|
||||
const evidence = await loadQualityEvidence(memory as never);
|
||||
expect(evidence.coverage).toBe(81.25);
|
||||
expect(evidence.testsPassing).toBe(95);
|
||||
expect(evidence).toEqual(passing);
|
||||
});
|
||||
|
||||
it('reports only measured checks and does not invent security or debt zeros', () => {
|
||||
const result = evaluateMeasuredQualityEvidence({
|
||||
coverage: 81.25,
|
||||
testsPassing: 95,
|
||||
});
|
||||
it('reports all configured checks from measured evidence', () => {
|
||||
const result = evaluateMeasuredQualityEvidence(passing);
|
||||
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.checks.map(check => check.name)).toEqual(['coverage', 'testsPassing']);
|
||||
expect(result.checks).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'securityVulnerabilities' }),
|
||||
expect.objectContaining({ name: 'technicalDebt' }),
|
||||
]),
|
||||
);
|
||||
expect(result.checks.map(check => check.name)).toEqual(Object.keys(passing));
|
||||
});
|
||||
|
||||
it('preserves pass, fail, and near-threshold warning exit codes', () => {
|
||||
const failed = evaluateMeasuredQualityEvidence({ ...passing, coverage: 79.9 });
|
||||
const warning = evaluateMeasuredQualityEvidence({ ...passing, coverage: 82 });
|
||||
const passed = evaluateMeasuredQualityEvidence(passing);
|
||||
|
||||
expect(getMeasuredQualityExitCode(failed)).toBe(1);
|
||||
expect(getMeasuredQualityExitCode(warning)).toBe(2);
|
||||
expect(getMeasuredQualityExitCode(passed)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('test command', () => {
|
||||
const context = {
|
||||
kernel: {
|
||||
getDomainAPIAsync: vi.fn().mockResolvedValue({ runTests }),
|
||||
memory: { set: vi.fn().mockResolvedValue(undefined) },
|
||||
},
|
||||
} as unknown as CLIContext;
|
||||
const command = createTestCommand(
|
||||
@@ -34,5 +35,10 @@ describe('test command', () => {
|
||||
expect(runTests).toHaveBeenCalledWith(expect.objectContaining({
|
||||
framework: 'node',
|
||||
}));
|
||||
expect(context.kernel.memory.set).toHaveBeenCalledWith(
|
||||
'test-run:latest',
|
||||
expect.objectContaining({ passed: 1, failed: 0, skipped: 0 }),
|
||||
{ namespace: 'test-execution', persist: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
DEFAULT_QUALITY_THRESHOLDS,
|
||||
QUALITY_EVIDENCE_NAMESPACE,
|
||||
evaluateQualityEvidence,
|
||||
loadQualityEvidence,
|
||||
writeQualityEvidence,
|
||||
type QualityEvidenceValues,
|
||||
} from '../../../../src/domains/quality-assessment/quality-evidence.js';
|
||||
|
||||
function createMemory() {
|
||||
const values = new Map<string, unknown>();
|
||||
return {
|
||||
values,
|
||||
get: vi.fn(async (key: string, options?: { namespace?: string }) =>
|
||||
values.get(`${options?.namespace ?? 'default'}:${key}`)),
|
||||
set: vi.fn(async (key: string, value: unknown, options?: { namespace?: string }) => {
|
||||
values.set(`${options?.namespace ?? 'default'}:${key}`, value);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const passingValues: QualityEvidenceValues = {
|
||||
coverage: 90,
|
||||
testsPassing: 100,
|
||||
criticalBugs: 0,
|
||||
codeSmells: 10,
|
||||
securityVulnerabilities: 0,
|
||||
technicalDebt: 2,
|
||||
duplications: 3,
|
||||
};
|
||||
|
||||
describe('quality evidence contract', () => {
|
||||
it('should round-trip timestamped evidence through the canonical namespace', async () => {
|
||||
const memory = createMemory();
|
||||
const measuredAt = '2026-08-03T08:00:00.000Z';
|
||||
|
||||
await writeQualityEvidence(memory as never, passingValues, {
|
||||
measuredAt,
|
||||
source: 'quality-analyzer',
|
||||
});
|
||||
const loaded = await loadQualityEvidence(memory as never, {
|
||||
now: Date.parse(measuredAt) + 1_000,
|
||||
});
|
||||
|
||||
expect(loaded).toEqual(passingValues);
|
||||
expect(memory.set).toHaveBeenCalledWith(
|
||||
'quality-evidence:criticalBugs:latest',
|
||||
expect.objectContaining({ measuredAt, source: 'quality-analyzer' }),
|
||||
expect.objectContaining({ namespace: QUALITY_EVIDENCE_NAMESPACE, persist: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail closed when configured evidence is missing', async () => {
|
||||
await expect(loadQualityEvidence(createMemory() as never)).rejects.toThrow(
|
||||
/no measured quality evidence/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail closed when evidence is malformed', async () => {
|
||||
const memory = createMemory();
|
||||
memory.values.set(`${QUALITY_EVIDENCE_NAMESPACE}:quality-evidence:coverage:latest`, {
|
||||
schemaVersion: 1,
|
||||
metric: 'coverage',
|
||||
value: Number.NaN,
|
||||
measuredAt: new Date().toISOString(),
|
||||
source: 'coverage-analysis',
|
||||
});
|
||||
|
||||
await expect(loadQualityEvidence(memory as never)).rejects.toThrow(/malformed/i);
|
||||
});
|
||||
|
||||
it('should fail closed when evidence is stale', async () => {
|
||||
const memory = createMemory();
|
||||
await writeQualityEvidence(memory as never, passingValues, {
|
||||
measuredAt: '2026-08-01T00:00:00.000Z',
|
||||
source: 'quality-analyzer',
|
||||
});
|
||||
|
||||
await expect(loadQualityEvidence(memory as never, {
|
||||
now: Date.parse('2026-08-03T00:00:00.000Z'),
|
||||
maxAgeMs: 60_000,
|
||||
})).rejects.toThrow(/stale/i);
|
||||
});
|
||||
|
||||
it('should evaluate every metric against the shared thresholds', () => {
|
||||
const result = evaluateQualityEvidence({ ...passingValues, technicalDebt: 6 });
|
||||
|
||||
expect(result.passed).toBe(false);
|
||||
expect(result.checks).toHaveLength(7);
|
||||
expect(result.checks.find((check) => check.name === 'technicalDebt')).toEqual(
|
||||
expect.objectContaining({
|
||||
passed: false,
|
||||
threshold: DEFAULT_QUALITY_THRESHOLDS.technicalDebt.value,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,20 @@ describe('TestExecutorService runner command', () => {
|
||||
expect(command.args).not.toContain('--coverage');
|
||||
});
|
||||
|
||||
it('passes a Node worker shard to one native test-runner process', () => {
|
||||
const executor = new TestExecutorService({ memory: {} as never });
|
||||
const command = (
|
||||
executor as unknown as {
|
||||
buildTestCommand(files: string[], framework: string): { command: string; args: string[] };
|
||||
}
|
||||
).buildTestCommand(['one.test.cjs', 'two.test.cjs'], 'node');
|
||||
|
||||
expect(command).toEqual({
|
||||
command: process.execPath,
|
||||
args: ['--test', 'one.test.cjs', 'two.test.cjs'],
|
||||
});
|
||||
});
|
||||
|
||||
it('runs each worker shard as one bounded runner batch', async () => {
|
||||
const executor = new TestExecutorService({ memory: {} as never });
|
||||
const internals = executor as unknown as {
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('ModularInitOrchestrator', () => {
|
||||
expect(orchestrator.getPhases()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should create context with provided options', () => {
|
||||
it('should create context with provided options', () => {
|
||||
const orchestrator = new ModularInitOrchestrator({
|
||||
projectRoot: '/tmp/test',
|
||||
autoMode: true,
|
||||
@@ -95,8 +95,18 @@ describe('ModularInitOrchestrator', () => {
|
||||
expect(ctx.projectRoot).toBe('/tmp/test');
|
||||
expect(ctx.options.autoMode).toBe(true);
|
||||
expect(ctx.options.upgrade).toBe(true);
|
||||
expect(ctx.options.minimal).toBe(true);
|
||||
});
|
||||
expect(ctx.options.minimal).toBe(true);
|
||||
});
|
||||
|
||||
it('should propagate the status line opt-out to init phases', () => {
|
||||
const orchestrator = new ModularInitOrchestrator({
|
||||
projectRoot: '/tmp/test',
|
||||
noStatusLine: true,
|
||||
customPhases: [],
|
||||
});
|
||||
|
||||
expect(orchestrator.getContext().options.noStatusLine).toBe(true);
|
||||
});
|
||||
|
||||
it('should initialize context with empty results map', () => {
|
||||
const orchestrator = new ModularInitOrchestrator({
|
||||
|
||||
@@ -407,10 +407,12 @@ describe('Settings Merge Utilities', () => {
|
||||
agents: { maxConcurrent: 10 },
|
||||
} as any;
|
||||
|
||||
const sections = generateV3SettingsSections(config);
|
||||
|
||||
expect(sections.statusLine).toBeDefined();
|
||||
expect((sections.statusLine as any).enabled).toBe(true);
|
||||
const sections = generateV3SettingsSections(config);
|
||||
|
||||
expect(sections.statusLine).toBeDefined();
|
||||
expect(sections.statusLine).toEqual(expect.objectContaining({ type: 'command' }));
|
||||
expect(sections.statusLine).not.toHaveProperty('enabled');
|
||||
expect(sections.statusLine).not.toHaveProperty('refreshMs');
|
||||
|
||||
expect(sections._aqePermissions).toBeDefined();
|
||||
expect(sections._aqePermissions).toContain('mcp__agentic-qe__*');
|
||||
@@ -454,17 +456,45 @@ describe('Settings Merge Utilities', () => {
|
||||
expect(settings.statusLine).toEqual(userStatusLine);
|
||||
});
|
||||
|
||||
it('should update AQE-owned statusLine and set it when absent', () => {
|
||||
it('should update AQE-owned statusLine and set it when absent', () => {
|
||||
const fresh: Record<string, unknown> = {};
|
||||
applyV3Sections(fresh, generateV3SettingsSections(config));
|
||||
expect((fresh.statusLine as any).command).toContain('statusline-v3.cjs');
|
||||
|
||||
const stale: Record<string, unknown> = {
|
||||
statusLine: { type: 'command', command: 'node old/statusline-v3.cjs', enabled: false },
|
||||
};
|
||||
applyV3Sections(stale, generateV3SettingsSections(config));
|
||||
expect((stale.statusLine as any).enabled).toBe(true); // refreshed
|
||||
});
|
||||
};
|
||||
applyV3Sections(stale, generateV3SettingsSections(config));
|
||||
expect((stale.statusLine as any).command).toContain('statusline-v3.cjs');
|
||||
expect(stale.statusLine).not.toHaveProperty('enabled');
|
||||
});
|
||||
|
||||
it('should omit a statusLine when explicitly disabled', () => {
|
||||
const settings: Record<string, unknown> = {};
|
||||
|
||||
applyV3Sections(settings, generateV3SettingsSections(config), { statusLine: false });
|
||||
|
||||
expect(settings.statusLine).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove an AQE-owned statusLine when explicitly disabled', () => {
|
||||
const settings: Record<string, unknown> = {
|
||||
statusLine: { type: 'command', command: 'node .claude/helpers/statusline-v3.cjs' },
|
||||
};
|
||||
|
||||
applyV3Sections(settings, generateV3SettingsSections(config), { statusLine: false });
|
||||
|
||||
expect(settings.statusLine).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve a custom statusLine when AQE statusLine is disabled', () => {
|
||||
const custom = { type: 'command', command: 'my-prompt --powerline' };
|
||||
const settings: Record<string, unknown> = { statusLine: custom };
|
||||
|
||||
applyV3Sections(settings, generateV3SettingsSections(config), { statusLine: false });
|
||||
|
||||
expect(settings.statusLine).toEqual(custom);
|
||||
});
|
||||
|
||||
it('should respect an explicit includeCoAuthoredBy=false', () => {
|
||||
const settings: Record<string, unknown> = { includeCoAuthoredBy: false };
|
||||
|
||||
@@ -15,6 +15,7 @@ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vites
|
||||
import { setRuVectorFeatureFlags, resetRuVectorFeatureFlags } from '../../../src/integrations/ruvector/feature-flags.js';
|
||||
import { clearEmbeddingCache, resetInitialization } from '../../../src/learning/real-embeddings';
|
||||
import { _resetWitnessChainForTests } from '../../../src/audit/witness-chain';
|
||||
import { resetUnifiedPersistence } from '../../../src/kernel/unified-persistence';
|
||||
|
||||
// Ensure these tests exercise the in-memory PatternStore, not the RVF variant
|
||||
beforeEach(() => { setRuVectorFeatureFlags({ useRVFPatternStore: false }); });
|
||||
@@ -45,11 +46,17 @@ import { createMockMemoryBackend, createMockEventBus } from './_aqe-engine-test-
|
||||
// ============================================================================
|
||||
|
||||
describe('AQELearningEngine', () => {
|
||||
let memory: MemoryBackend;
|
||||
let eventBus: EventBus;
|
||||
let engine: AQELearningEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
const originalMemoryBackend = process.env.AQE_MEMORY_BACKEND;
|
||||
let memory: MemoryBackend;
|
||||
let eventBus: EventBus;
|
||||
let engine: AQELearningEngine;
|
||||
|
||||
beforeEach(async () => {
|
||||
// QEReasoningBank owns a SQLitePatternStore in addition to the injected
|
||||
// MemoryBackend. Keep this unit suite database-free and reset that shared
|
||||
// singleton so patterns cannot accumulate across test cases (#588).
|
||||
process.env.AQE_MEMORY_BACKEND = 'memory';
|
||||
resetUnifiedPersistence();
|
||||
memory = createMockMemoryBackend();
|
||||
eventBus = createMockEventBus();
|
||||
engine = createAQELearningEngine(
|
||||
@@ -63,6 +70,12 @@ describe('AQELearningEngine', () => {
|
||||
afterEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
await engine.dispose();
|
||||
resetUnifiedPersistence();
|
||||
if (originalMemoryBackend === undefined) {
|
||||
delete process.env.AQE_MEMORY_BACKEND;
|
||||
} else {
|
||||
process.env.AQE_MEMORY_BACKEND = originalMemoryBackend;
|
||||
}
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
|
||||
@@ -41,14 +41,20 @@ function fakeEmbedding(text: string, dim = 384): number[] {
|
||||
return v;
|
||||
}
|
||||
|
||||
async function startServer(): Promise<{ url: string; close: () => Promise<void>; calls: number }> {
|
||||
const handle = { url: '', calls: 0, close: async () => {} };
|
||||
async function startServer(): Promise<{
|
||||
url: string;
|
||||
close: () => Promise<void>;
|
||||
calls: number;
|
||||
inputs: string[][];
|
||||
}> {
|
||||
const handle = { url: '', calls: 0, inputs: [] as string[][], close: async () => {} };
|
||||
const server = http.createServer((req, res) => {
|
||||
handle.calls++;
|
||||
let raw = '';
|
||||
req.on('data', (c) => (raw += c));
|
||||
req.on('end', () => {
|
||||
const { input } = JSON.parse(raw) as { input: string[] };
|
||||
handle.inputs.push(input);
|
||||
const data = input.map((t, i) => ({
|
||||
index: i,
|
||||
embedding: fakeEmbedding(t),
|
||||
@@ -107,6 +113,28 @@ describe('real-embeddings.ts — ADR-097 endpoint branch', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('returns zero vectors for non-semantic items in a mixed batch', async () => {
|
||||
const nonSemantic = '{"metrics":{"tasksReceived":1}}';
|
||||
|
||||
const vecs = await computeBatchEmbeddings(['semantic text', nonSemantic], {
|
||||
endpoint: server.url,
|
||||
enableCache: false,
|
||||
});
|
||||
|
||||
expect(vecs[1].every((value) => value === 0)).toBe(true);
|
||||
expect(server.inputs.at(-1)).toEqual(['semantic text']);
|
||||
});
|
||||
|
||||
it('does not initialize the embedder for an entirely non-semantic batch', async () => {
|
||||
const vecs = await computeBatchEmbeddings(
|
||||
['{"metrics":{"tasksReceived":1}}', '1234567890-1234567890-1234567890'],
|
||||
{ endpoint: server.url, enableCache: false }
|
||||
);
|
||||
|
||||
expect(vecs).toHaveLength(2);
|
||||
expect(server.calls).toBe(0);
|
||||
});
|
||||
|
||||
it('returns unit-length vectors', async () => {
|
||||
const vec = await computeRealEmbedding('unit length check', {
|
||||
endpoint: server.url,
|
||||
|
||||
@@ -279,6 +279,16 @@ describe('CoverageGapsTool', () => {
|
||||
|
||||
describe('QualityEvaluateTool', () => {
|
||||
let tool: QualityEvaluateTool;
|
||||
const measuredMetrics = {
|
||||
coverage: 90,
|
||||
testsPassing: 100,
|
||||
criticalBugs: 0,
|
||||
codeSmells: 10,
|
||||
securityVulnerabilities: 0,
|
||||
technicalDebt: 2,
|
||||
duplications: 3,
|
||||
};
|
||||
const evidence = { measuredAt: new Date().toISOString(), source: 'unit-test analyzer' };
|
||||
|
||||
beforeEach(() => {
|
||||
tool = new QualityEvaluateTool();
|
||||
@@ -295,17 +305,40 @@ describe('QualityEvaluateTool', () => {
|
||||
it('should evaluate quality gates', async () => {
|
||||
const result = await tool.invoke({
|
||||
gateName: 'default',
|
||||
metrics: measuredMetrics,
|
||||
evidence,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.passed).toBeDefined();
|
||||
expect(result.data?.checks).toBeDefined();
|
||||
expect(result.data?.checks).toHaveLength(7);
|
||||
expect(result.data?.checks).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Code Smells', threshold: 20 }),
|
||||
expect.objectContaining({ name: 'Technical Debt', threshold: 5 }),
|
||||
expect.objectContaining({ name: 'Duplications', threshold: 5 }),
|
||||
]));
|
||||
});
|
||||
|
||||
it('should fail closed when no metrics or stored evidence are available', async () => {
|
||||
const result = await tool.invoke({ gateName: 'default' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toMatch(/no measured quality evidence/i);
|
||||
});
|
||||
|
||||
it('should reject supplied metrics without timestamped provenance', async () => {
|
||||
const result = await tool.invoke({ metrics: measuredMetrics });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toMatch(/timestamped evidence provenance is required/i);
|
||||
});
|
||||
|
||||
it('should include deployment advice', async () => {
|
||||
const result = await tool.invoke({
|
||||
gateName: 'production',
|
||||
includeDeploymentAdvice: true,
|
||||
metrics: measuredMetrics,
|
||||
evidence,
|
||||
includeAdvice: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user