agentic-qe 3.7.14 → 3.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/skills-manifest.json +1 -1
- package/CHANGELOG.md +27 -0
- package/dist/cli/bundle.js +1187 -508
- package/dist/cli/commands/prove.d.ts +60 -0
- package/dist/cli/commands/prove.js +167 -0
- package/dist/cli/index.js +2 -0
- package/dist/domains/test-generation/pattern-injection/edge-case-injector.d.ts +6 -0
- package/dist/domains/test-generation/pattern-injection/edge-case-injector.js +30 -0
- package/dist/kernel/unified-memory-schemas.d.ts +2 -2
- package/dist/kernel/unified-memory-schemas.js +26 -1
- package/dist/kernel/unified-memory.js +32 -0
- package/dist/learning/aqe-learning-engine.js +2 -1
- package/dist/learning/daily-log.d.ts +43 -0
- package/dist/learning/daily-log.js +91 -0
- package/dist/learning/experience-capture.d.ts +42 -0
- package/dist/learning/experience-capture.js +94 -4
- package/dist/learning/index.d.ts +4 -0
- package/dist/learning/index.js +8 -0
- package/dist/learning/opd-remediation.d.ts +55 -0
- package/dist/learning/opd-remediation.js +130 -0
- package/dist/learning/pattern-lifecycle.d.ts +12 -1
- package/dist/learning/pattern-lifecycle.js +18 -2
- package/dist/learning/pattern-store.d.ts +12 -4
- package/dist/learning/pattern-store.js +178 -19
- package/dist/learning/qe-hooks.d.ts +1 -0
- package/dist/learning/qe-hooks.js +30 -0
- package/dist/learning/qe-patterns.d.ts +6 -0
- package/dist/learning/qe-patterns.js +10 -1
- package/dist/learning/sqlite-persistence.d.ts +40 -0
- package/dist/learning/sqlite-persistence.js +228 -1
- package/dist/mcp/bundle.js +647 -20
- package/dist/mcp/handlers/core-handlers.d.ts +5 -0
- package/dist/mcp/handlers/core-handlers.js +11 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tool-scoping.d.ts +36 -0
- package/dist/mcp/tool-scoping.js +129 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
16
16
|
import { safeJsonParse } from '../shared/safe-json.js';
|
|
17
17
|
import { toErrorMessage } from '../shared/error-utils.js';
|
|
18
18
|
import { getUnifiedMemory } from '../kernel/unified-memory.js';
|
|
19
|
+
import { computeBatchEmbeddings, getEmbeddingDimension } from './real-embeddings.js';
|
|
19
20
|
export const DEFAULT_SQLITE_CONFIG = {
|
|
20
21
|
// LEGACY: Ignored when useUnified=true (the default). All data goes to memory.db
|
|
21
22
|
dbPath: '.agentic-qe/memory.db',
|
|
@@ -25,6 +26,32 @@ export const DEFAULT_SQLITE_CONFIG = {
|
|
|
25
26
|
foreignKeys: true,
|
|
26
27
|
useUnified: true, // ADR-046: Use unified storage (memory.db) by default
|
|
27
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Hash-based embedding generation (no ONNX dependency).
|
|
31
|
+
* Deterministic: same text always produces the same embedding.
|
|
32
|
+
* FALLBACK ONLY — real embeddings use all-MiniLM-L6-v2 via computeBatchEmbeddings().
|
|
33
|
+
* Kept for environments where ONNX/@xenova/transformers is unavailable.
|
|
34
|
+
*/
|
|
35
|
+
export function hashEmbedding(text, dimension = 384) {
|
|
36
|
+
const embedding = new Array(dimension).fill(0);
|
|
37
|
+
const normalized = text.toLowerCase().trim();
|
|
38
|
+
// Multiple hash passes for better distribution
|
|
39
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
40
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
41
|
+
const charCode = normalized.charCodeAt(i);
|
|
42
|
+
const idx = (charCode * (i + 1) * (pass + 1)) % dimension;
|
|
43
|
+
embedding[idx] += Math.sin(charCode * (pass + 1)) / (i + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Normalize to unit vector
|
|
47
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
48
|
+
if (magnitude > 0) {
|
|
49
|
+
for (let i = 0; i < dimension; i++) {
|
|
50
|
+
embedding[i] /= magnitude;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return embedding;
|
|
54
|
+
}
|
|
28
55
|
/**
|
|
29
56
|
* SQLite-based pattern persistence
|
|
30
57
|
*/
|
|
@@ -160,6 +187,31 @@ export class SQLitePatternStore {
|
|
|
160
187
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_patterns_unique_name_domain_type
|
|
161
188
|
ON qe_patterns(name, qe_domain, pattern_type);
|
|
162
189
|
|
|
190
|
+
-- FTS5 full-text search index for hybrid vector/text search
|
|
191
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS qe_patterns_fts USING fts5(
|
|
192
|
+
name, description, pattern_type, qe_domain,
|
|
193
|
+
content='qe_patterns',
|
|
194
|
+
content_rowid='rowid'
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
-- FTS5 triggers to keep index in sync
|
|
198
|
+
CREATE TRIGGER IF NOT EXISTS qe_patterns_fts_insert AFTER INSERT ON qe_patterns BEGIN
|
|
199
|
+
INSERT INTO qe_patterns_fts(rowid, name, description, pattern_type, qe_domain)
|
|
200
|
+
VALUES (new.rowid, new.name, new.description, new.pattern_type, new.qe_domain);
|
|
201
|
+
END;
|
|
202
|
+
|
|
203
|
+
CREATE TRIGGER IF NOT EXISTS qe_patterns_fts_delete AFTER DELETE ON qe_patterns BEGIN
|
|
204
|
+
INSERT INTO qe_patterns_fts(qe_patterns_fts, rowid, name, description, pattern_type, qe_domain)
|
|
205
|
+
VALUES ('delete', old.rowid, old.name, old.description, old.pattern_type, old.qe_domain);
|
|
206
|
+
END;
|
|
207
|
+
|
|
208
|
+
CREATE TRIGGER IF NOT EXISTS qe_patterns_fts_update AFTER UPDATE ON qe_patterns BEGIN
|
|
209
|
+
INSERT INTO qe_patterns_fts(qe_patterns_fts, rowid, name, description, pattern_type, qe_domain)
|
|
210
|
+
VALUES ('delete', old.rowid, old.name, old.description, old.pattern_type, old.qe_domain);
|
|
211
|
+
INSERT INTO qe_patterns_fts(rowid, name, description, pattern_type, qe_domain)
|
|
212
|
+
VALUES (new.rowid, new.name, new.description, new.pattern_type, new.qe_domain);
|
|
213
|
+
END;
|
|
214
|
+
|
|
163
215
|
-- Indexes for performance
|
|
164
216
|
CREATE INDEX IF NOT EXISTS idx_patterns_domain ON qe_patterns(qe_domain);
|
|
165
217
|
CREATE INDEX IF NOT EXISTS idx_patterns_type ON qe_patterns(pattern_type);
|
|
@@ -228,11 +280,26 @@ export class SQLitePatternStore {
|
|
|
228
280
|
prepareStatements() {
|
|
229
281
|
if (!this.db)
|
|
230
282
|
throw new Error('Database not initialized');
|
|
283
|
+
// Two insert statements to handle both conflict types:
|
|
284
|
+
// 1. Primary key conflict (same id) — update metadata only, preserve stats
|
|
285
|
+
// 2. Unique index conflict (same name+domain+type, different id) — update metadata only
|
|
231
286
|
this.prepared.set('insertPattern', this.db.prepare(`
|
|
232
|
-
INSERT
|
|
287
|
+
INSERT INTO qe_patterns (
|
|
233
288
|
id, pattern_type, qe_domain, domain, name, description,
|
|
234
289
|
confidence, tier, template_json, context_json
|
|
235
290
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
291
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
292
|
+
confidence = excluded.confidence,
|
|
293
|
+
tier = excluded.tier,
|
|
294
|
+
template_json = excluded.template_json,
|
|
295
|
+
context_json = excluded.context_json,
|
|
296
|
+
updated_at = datetime('now')
|
|
297
|
+
ON CONFLICT(name, qe_domain, pattern_type) DO UPDATE SET
|
|
298
|
+
confidence = excluded.confidence,
|
|
299
|
+
tier = excluded.tier,
|
|
300
|
+
template_json = excluded.template_json,
|
|
301
|
+
context_json = excluded.context_json,
|
|
302
|
+
updated_at = datetime('now')
|
|
236
303
|
`));
|
|
237
304
|
this.prepared.set('insertEmbedding', this.db.prepare(`
|
|
238
305
|
INSERT OR REPLACE INTO qe_pattern_embeddings (pattern_id, embedding, dimension, model)
|
|
@@ -338,6 +405,67 @@ export class SQLitePatternStore {
|
|
|
338
405
|
}
|
|
339
406
|
return rows.map(row => this.rowToPattern(row));
|
|
340
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* FTS5 full-text search for patterns.
|
|
410
|
+
* Returns pattern IDs with BM25 relevance scores.
|
|
411
|
+
*/
|
|
412
|
+
searchFTS(query, limit = 20) {
|
|
413
|
+
if (!this.db)
|
|
414
|
+
throw new Error('Database not initialized');
|
|
415
|
+
if (!query.trim())
|
|
416
|
+
return [];
|
|
417
|
+
// Sanitize: wrap in double quotes to force literal phrase match,
|
|
418
|
+
// escaping any internal double quotes to prevent FTS5 syntax injection
|
|
419
|
+
const sanitized = '"' + query.replace(/"/g, '""') + '"';
|
|
420
|
+
try {
|
|
421
|
+
const rows = this.db.prepare(`
|
|
422
|
+
SELECT p.id, rank AS fts_score
|
|
423
|
+
FROM qe_patterns_fts fts
|
|
424
|
+
JOIN qe_patterns p ON p.rowid = fts.rowid
|
|
425
|
+
WHERE qe_patterns_fts MATCH ?
|
|
426
|
+
ORDER BY rank
|
|
427
|
+
LIMIT ?
|
|
428
|
+
`).all(sanitized, limit);
|
|
429
|
+
// FTS5 rank is negative (lower = better), normalize to 0..1
|
|
430
|
+
// Use Math.max(maxScore, 1.0) to prevent single-result inflation:
|
|
431
|
+
// without this, a single FTS5 result always normalizes to 1.0
|
|
432
|
+
const maxAbsScore = Math.max(...rows.map(r => Math.abs(r.fts_score)), 1.0);
|
|
433
|
+
return rows.map(r => ({
|
|
434
|
+
id: r.id,
|
|
435
|
+
ftsScore: Math.abs(r.fts_score) / maxAbsScore,
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// FTS5 table may not exist yet (unified DB migrated before schema update)
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Ghost pattern check: find patterns in SQLite that have no embeddings.
|
|
445
|
+
* Used by aqe_health to detect data integrity issues.
|
|
446
|
+
*/
|
|
447
|
+
getGhostPatternCount() {
|
|
448
|
+
if (!this.db)
|
|
449
|
+
throw new Error('Database not initialized');
|
|
450
|
+
const total = this.db.prepare(`SELECT COUNT(*) as c FROM qe_patterns WHERE id NOT LIKE 'bench-%'`).get().c;
|
|
451
|
+
const ghostCount = this.db.prepare(`
|
|
452
|
+
SELECT COUNT(*) as c FROM qe_patterns p
|
|
453
|
+
WHERE p.id NOT LIKE 'bench-%'
|
|
454
|
+
AND p.id NOT IN (SELECT pattern_id FROM qe_pattern_embeddings)
|
|
455
|
+
`).get().c;
|
|
456
|
+
// Sample up to 10 ghost IDs for diagnostics
|
|
457
|
+
const sampleGhosts = this.db.prepare(`
|
|
458
|
+
SELECT p.id FROM qe_patterns p
|
|
459
|
+
WHERE p.id NOT LIKE 'bench-%'
|
|
460
|
+
AND p.id NOT IN (SELECT pattern_id FROM qe_pattern_embeddings)
|
|
461
|
+
LIMIT 10
|
|
462
|
+
`).all();
|
|
463
|
+
return {
|
|
464
|
+
total,
|
|
465
|
+
withoutEmbeddings: ghostCount,
|
|
466
|
+
sampleGhostIds: sampleGhosts.map(g => g.id),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
341
469
|
/**
|
|
342
470
|
* Get all embeddings for HNSW indexing
|
|
343
471
|
*/
|
|
@@ -671,6 +799,105 @@ export class SQLitePatternStore {
|
|
|
671
799
|
};
|
|
672
800
|
}
|
|
673
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Backfill embeddings for patterns that don't have them.
|
|
804
|
+
* Uses real all-MiniLM-L6-v2 transformer embeddings via @xenova/transformers.
|
|
805
|
+
* Falls back to hash-based embeddings if ONNX is unavailable.
|
|
806
|
+
* Skips bench/test patterns (id LIKE 'bench-%').
|
|
807
|
+
*
|
|
808
|
+
* @param batchSize - Patterns per inference batch (default: 32, matches model batch size)
|
|
809
|
+
* @returns Stats about the backfill operation
|
|
810
|
+
*/
|
|
811
|
+
async backfillEmbeddings(batchSize = 32) {
|
|
812
|
+
if (!this.db)
|
|
813
|
+
throw new Error('Database not initialized');
|
|
814
|
+
const dimension = getEmbeddingDimension(); // 384
|
|
815
|
+
// Find patterns without embeddings (skip bench patterns)
|
|
816
|
+
const patternsWithout = this.db.prepare(`
|
|
817
|
+
SELECT p.id, p.name, p.description, p.pattern_type, p.qe_domain
|
|
818
|
+
FROM qe_patterns p
|
|
819
|
+
WHERE p.id NOT IN (SELECT pattern_id FROM qe_pattern_embeddings)
|
|
820
|
+
AND p.id NOT LIKE 'bench-%'
|
|
821
|
+
ORDER BY p.quality_score DESC
|
|
822
|
+
`).all();
|
|
823
|
+
const alreadyHad = this.db.prepare(`
|
|
824
|
+
SELECT COUNT(*) as c FROM qe_pattern_embeddings
|
|
825
|
+
`).get();
|
|
826
|
+
if (patternsWithout.length === 0) {
|
|
827
|
+
console.log(`[SQLitePatternStore] Backfill: all patterns already have embeddings (${alreadyHad.c} total)`);
|
|
828
|
+
return { processed: 0, skipped: 0, errors: 0, alreadyHad: alreadyHad.c, method: 'transformer' };
|
|
829
|
+
}
|
|
830
|
+
console.log(`[SQLitePatternStore] Backfill: ${patternsWithout.length} patterns need embeddings (${alreadyHad.c} already have)`);
|
|
831
|
+
const insertEmbedding = this.prepared.get('insertEmbedding');
|
|
832
|
+
if (!insertEmbedding) {
|
|
833
|
+
throw new Error('Prepared statements not ready');
|
|
834
|
+
}
|
|
835
|
+
let processed = 0;
|
|
836
|
+
let skipped = 0;
|
|
837
|
+
let errors = 0;
|
|
838
|
+
let method = 'transformer';
|
|
839
|
+
// Process in batches: compute embeddings async, then insert in sync transaction
|
|
840
|
+
for (let i = 0; i < patternsWithout.length; i += batchSize) {
|
|
841
|
+
const batch = patternsWithout.slice(i, i + batchSize);
|
|
842
|
+
// Build text for each pattern in the batch
|
|
843
|
+
const textsWithIds = [];
|
|
844
|
+
for (const pattern of batch) {
|
|
845
|
+
const text = [
|
|
846
|
+
pattern.name,
|
|
847
|
+
pattern.description,
|
|
848
|
+
pattern.pattern_type,
|
|
849
|
+
pattern.qe_domain,
|
|
850
|
+
].filter(Boolean).join(' ');
|
|
851
|
+
if (!text.trim()) {
|
|
852
|
+
skipped++;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
textsWithIds.push({ id: pattern.id, text });
|
|
856
|
+
}
|
|
857
|
+
if (textsWithIds.length === 0)
|
|
858
|
+
continue;
|
|
859
|
+
// Compute real embeddings via all-MiniLM-L6-v2 (async ONNX inference)
|
|
860
|
+
let embeddings;
|
|
861
|
+
try {
|
|
862
|
+
embeddings = await computeBatchEmbeddings(textsWithIds.map(t => t.text));
|
|
863
|
+
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
// Transformer unavailable — fall back to hash embeddings for remaining patterns
|
|
866
|
+
console.warn(`[SQLitePatternStore] Transformer unavailable, falling back to hash embeddings: ${toErrorMessage(e)}`);
|
|
867
|
+
method = 'hash-fallback';
|
|
868
|
+
embeddings = textsWithIds.map(t => hashEmbedding(t.text, dimension));
|
|
869
|
+
}
|
|
870
|
+
// Insert computed embeddings in a sync transaction
|
|
871
|
+
const embeddingTag = method === 'transformer' ? 'transformer-backfill' : 'hash-backfill';
|
|
872
|
+
const insertBatch = this.db.transaction(() => {
|
|
873
|
+
for (let j = 0; j < textsWithIds.length; j++) {
|
|
874
|
+
try {
|
|
875
|
+
const embedding = embeddings[j];
|
|
876
|
+
if (!embedding || embedding.length !== dimension) {
|
|
877
|
+
errors++;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
881
|
+
insertEmbedding.run(textsWithIds[j].id, buffer, dimension, embeddingTag);
|
|
882
|
+
processed++;
|
|
883
|
+
}
|
|
884
|
+
catch (e) {
|
|
885
|
+
errors++;
|
|
886
|
+
if (errors <= 3) {
|
|
887
|
+
console.warn(`[SQLitePatternStore] Backfill error for ${textsWithIds[j].id}:`, toErrorMessage(e));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
insertBatch();
|
|
893
|
+
const progress = Math.min(i + batchSize, patternsWithout.length);
|
|
894
|
+
if (progress % 100 === 0 || progress >= patternsWithout.length) {
|
|
895
|
+
console.log(`[SQLitePatternStore] Backfill progress: ${progress}/${patternsWithout.length}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
console.log(`[SQLitePatternStore] Backfill complete (${method}): ${processed} processed, ${skipped} skipped, ${errors} errors`);
|
|
899
|
+
return { processed, skipped, errors, alreadyHad: alreadyHad.c, method };
|
|
900
|
+
}
|
|
674
901
|
/**
|
|
675
902
|
* Close the database
|
|
676
903
|
*/
|