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.
Files changed (38) hide show
  1. package/.claude/skills/skills-manifest.json +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/dist/cli/bundle.js +1187 -508
  4. package/dist/cli/commands/prove.d.ts +60 -0
  5. package/dist/cli/commands/prove.js +167 -0
  6. package/dist/cli/index.js +2 -0
  7. package/dist/domains/test-generation/pattern-injection/edge-case-injector.d.ts +6 -0
  8. package/dist/domains/test-generation/pattern-injection/edge-case-injector.js +30 -0
  9. package/dist/kernel/unified-memory-schemas.d.ts +2 -2
  10. package/dist/kernel/unified-memory-schemas.js +26 -1
  11. package/dist/kernel/unified-memory.js +32 -0
  12. package/dist/learning/aqe-learning-engine.js +2 -1
  13. package/dist/learning/daily-log.d.ts +43 -0
  14. package/dist/learning/daily-log.js +91 -0
  15. package/dist/learning/experience-capture.d.ts +42 -0
  16. package/dist/learning/experience-capture.js +94 -4
  17. package/dist/learning/index.d.ts +4 -0
  18. package/dist/learning/index.js +8 -0
  19. package/dist/learning/opd-remediation.d.ts +55 -0
  20. package/dist/learning/opd-remediation.js +130 -0
  21. package/dist/learning/pattern-lifecycle.d.ts +12 -1
  22. package/dist/learning/pattern-lifecycle.js +18 -2
  23. package/dist/learning/pattern-store.d.ts +12 -4
  24. package/dist/learning/pattern-store.js +178 -19
  25. package/dist/learning/qe-hooks.d.ts +1 -0
  26. package/dist/learning/qe-hooks.js +30 -0
  27. package/dist/learning/qe-patterns.d.ts +6 -0
  28. package/dist/learning/qe-patterns.js +10 -1
  29. package/dist/learning/sqlite-persistence.d.ts +40 -0
  30. package/dist/learning/sqlite-persistence.js +228 -1
  31. package/dist/mcp/bundle.js +647 -20
  32. package/dist/mcp/handlers/core-handlers.d.ts +5 -0
  33. package/dist/mcp/handlers/core-handlers.js +11 -0
  34. package/dist/mcp/index.d.ts +1 -0
  35. package/dist/mcp/index.js +2 -0
  36. package/dist/mcp/tool-scoping.d.ts +36 -0
  37. package/dist/mcp/tool-scoping.js +129 -0
  38. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  import { v4 as uuidv4 } from 'uuid';
9
9
  import { ok, err } from '../shared/types/index.js';
10
10
  import { toErrorMessage, toError } from '../shared/error-utils.js';
11
- import { calculateQualityScore, shouldPromotePattern, validateQEPattern, mapQEDomainToAQE, } from './qe-patterns.js';
11
+ import { calculateQualityScore, shouldPromotePattern, validateQEPattern, mapQEDomainToAQE, PROMOTION_THRESHOLD, } from './qe-patterns.js';
12
12
  /**
13
13
  * Default pattern store configuration
14
14
  */
@@ -21,7 +21,7 @@ export const DEFAULT_PATTERN_STORE_CONFIG = {
21
21
  efSearch: 100,
22
22
  maxElements: 50000,
23
23
  },
24
- promotionThreshold: 3,
24
+ promotionThreshold: PROMOTION_THRESHOLD,
25
25
  minConfidence: 0.3,
26
26
  maxPatternsPerDomain: 5000,
27
27
  autoCleanup: true,
@@ -55,6 +55,7 @@ export class PatternStore {
55
55
  cleanupTimer;
56
56
  // Optional SQLite persistence delegate for delete/promote
57
57
  sqliteStore = null;
58
+ loadingPromise = null;
58
59
  // In-memory caches for fast access
59
60
  patternCache = new Map();
60
61
  domainIndex = new Map();
@@ -75,12 +76,23 @@ export class PatternStore {
75
76
  this.config = { ...DEFAULT_PATTERN_STORE_CONFIG, ...config };
76
77
  }
77
78
  /**
78
- * Set SQLite persistence delegate for delete/promote operations.
79
- * When set, PatternStore will forward these operations to SQLite
80
- * in addition to updating the in-memory cache.
79
+ * Set SQLite persistence delegate and load patterns into memory.
80
+ *
81
+ * When set, PatternStore will:
82
+ * 1. Load existing patterns from SQLite into the in-memory cache
83
+ * 2. Forward create/delete/promote operations to SQLite for persistence
84
+ * 3. Persist embeddings alongside patterns on store()
81
85
  */
82
86
  setSqliteStore(store) {
83
87
  this.sqliteStore = store;
88
+ // Load patterns from SQLite if we're already initialized
89
+ // (setSqliteStore is called after initialize() in QEReasoningBank)
90
+ // Store promise so concurrent store/search calls can await it
91
+ if (this.initialized) {
92
+ this.loadingPromise = this.loadPatterns().catch((e) => console.warn('[PatternStore] Failed to load patterns after setSqliteStore:', e)).finally(() => {
93
+ this.loadingPromise = null;
94
+ });
95
+ }
84
96
  }
85
97
  /**
86
98
  * Initialize the pattern store
@@ -161,6 +173,47 @@ export class PatternStore {
161
173
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('HNSW init timeout')), timeoutMs));
162
174
  await Promise.race([initPromise, timeoutPromise]);
163
175
  this.hnswAvailable = this.hnswIndex.isNativeAvailable();
176
+ // Load existing embeddings from SQLite into HNSW index (capped to prevent timeout)
177
+ if (this.sqliteStore) {
178
+ try {
179
+ const embeddings = this.sqliteStore.getAllEmbeddings();
180
+ const maxBootstrap = this.config.hnsw.maxElements;
181
+ let loaded = 0;
182
+ for (const { patternId, embedding } of embeddings) {
183
+ if (loaded >= maxBootstrap)
184
+ break;
185
+ if (!embedding || embedding.length !== this.config.embeddingDimension)
186
+ continue;
187
+ const pattern = this.patternCache.get(patternId);
188
+ if (!pattern)
189
+ continue;
190
+ try {
191
+ await this.hnswIndex.insert(patternId, embedding, {
192
+ filePath: pattern.patternType,
193
+ lineCoverage: pattern.confidence * 100,
194
+ branchCoverage: pattern.qualityScore * 100,
195
+ functionCoverage: 0,
196
+ statementCoverage: 0,
197
+ uncoveredLineCount: 0,
198
+ uncoveredBranchCount: 0,
199
+ riskScore: 1 - pattern.confidence,
200
+ lastUpdated: Date.now(),
201
+ totalLines: 0,
202
+ });
203
+ loaded++;
204
+ }
205
+ catch {
206
+ // Duplicate or invalid — skip
207
+ }
208
+ }
209
+ if (loaded > 0) {
210
+ console.log(`[PatternStore] Loaded ${loaded} embeddings from SQLite into HNSW`);
211
+ }
212
+ }
213
+ catch (error) {
214
+ console.warn('[PatternStore] Failed to load SQLite embeddings into HNSW:', toErrorMessage(error));
215
+ }
216
+ }
164
217
  console.log(`[PatternStore] HNSW lazy-initialized (native: ${this.hnswAvailable})`);
165
218
  }
166
219
  catch (error) {
@@ -170,13 +223,28 @@ export class PatternStore {
170
223
  }
171
224
  }
172
225
  /**
173
- * Load existing patterns from memory with timeout protection
226
+ * Load existing patterns from SQLite into in-memory cache.
227
+ *
228
+ * Previously this was a no-op after Issue #258 removed kv_store duplication,
229
+ * but that left 15,634 SQLite patterns invisible to search on every restart.
230
+ * Now properly loads from SQLitePatternStore when wired.
174
231
  */
175
232
  async loadPatterns() {
176
- // Patterns are loaded from qe_patterns table by SQLitePatternStore.
177
- // PatternStore's in-memory cache is populated via indexPattern() calls
178
- // from the ReasoningBank when it loads from the relational store.
179
- // Previously this loaded from kv_store, which duplicated storage (Issue #258).
233
+ if (!this.sqliteStore) {
234
+ return; // SQLite not wired yet will be loaded after setSqliteStore()
235
+ }
236
+ try {
237
+ const patterns = this.sqliteStore.getPatterns({ limit: 50000 });
238
+ for (const pattern of patterns) {
239
+ this.indexPattern(pattern);
240
+ }
241
+ if (patterns.length > 0) {
242
+ console.log(`[PatternStore] Loaded ${patterns.length} patterns from SQLite into memory cache`);
243
+ }
244
+ }
245
+ catch (error) {
246
+ console.warn('[PatternStore] Failed to load patterns from SQLite:', toErrorMessage(error));
247
+ }
180
248
  }
181
249
  /**
182
250
  * Index a pattern in local caches
@@ -193,8 +261,13 @@ export class PatternStore {
193
261
  this.typeIndex.set(pattern.patternType, new Set());
194
262
  }
195
263
  this.typeIndex.get(pattern.patternType).add(pattern.id);
196
- // Tier index
197
- this.tierIndex.get(pattern.tier).add(pattern.id);
264
+ // Tier index (defensive: coerce unexpected tier values to 'short-term')
265
+ const tier = (pattern.tier === 'long-term') ? 'long-term' : 'short-term';
266
+ if (pattern.tier !== tier) {
267
+ // Pattern has invalid tier from SQLite — store corrected copy in cache
268
+ pattern.tier = tier;
269
+ }
270
+ this.tierIndex.get(tier).add(pattern.id);
198
271
  }
199
272
  /**
200
273
  * Remove pattern from local indices
@@ -212,6 +285,9 @@ export class PatternStore {
212
285
  if (!this.initialized) {
213
286
  await this.initialize();
214
287
  }
288
+ if (this.loadingPromise) {
289
+ await this.loadingPromise;
290
+ }
215
291
  // Validate pattern
216
292
  const validation = validateQEPattern(pattern);
217
293
  if (!validation.valid) {
@@ -227,11 +303,17 @@ export class PatternStore {
227
303
  // Run cleanup for this domain
228
304
  await this.cleanupDomain(pattern.qeDomain);
229
305
  }
230
- // Patterns are persisted to qe_patterns table by SQLitePatternStore.
231
- // PatternStore only maintains in-memory cache + HNSW index for fast search.
232
- // Previously this wrote to kv_store, causing 229MB bloat (Issue #258).
233
- // Index locally
306
+ // Index in memory cache
234
307
  this.indexPattern(pattern);
308
+ // Persist to SQLite (pattern + embedding atomically)
309
+ if (this.sqliteStore) {
310
+ try {
311
+ this.sqliteStore.storePattern(pattern, pattern.embedding);
312
+ }
313
+ catch (error) {
314
+ console.warn(`[PatternStore] SQLite persist failed for ${pattern.id}:`, toErrorMessage(error));
315
+ }
316
+ }
235
317
  // Add to HNSW if embedding is available (lazy-load HNSW only when needed)
236
318
  if (pattern.embedding) {
237
319
  const hnsw = await this.ensureHNSW();
@@ -327,6 +409,9 @@ export class PatternStore {
327
409
  if (!this.initialized) {
328
410
  await this.initialize();
329
411
  }
412
+ if (this.loadingPromise) {
413
+ await this.loadingPromise;
414
+ }
330
415
  return this.patternCache.get(id) ?? null;
331
416
  }
332
417
  /**
@@ -336,6 +421,9 @@ export class PatternStore {
336
421
  if (!this.initialized) {
337
422
  await this.initialize();
338
423
  }
424
+ if (this.loadingPromise) {
425
+ await this.loadingPromise;
426
+ }
339
427
  const startTime = performance.now();
340
428
  const limit = options.limit || 10;
341
429
  const results = [];
@@ -362,10 +450,69 @@ export class PatternStore {
362
450
  }
363
451
  }
364
452
  }
453
+ // FTS5 hybrid search: blend BM25 text relevance with vector similarity
454
+ // 75% vector score + 25% FTS5 score for patterns found by both
455
+ if (typeof query === 'string' && query.trim() && this.sqliteStore) {
456
+ try {
457
+ const ftsResults = this.sqliteStore.searchFTS(query, limit * 2);
458
+ if (ftsResults.length > 0) {
459
+ const ftsScoreMap = new Map(ftsResults.map(r => [r.id, r.ftsScore]));
460
+ const existingIds = new Set(results.map(r => r.pattern.id));
461
+ // Boost existing vector results that also match FTS5
462
+ for (const result of results) {
463
+ const ftsScore = ftsScoreMap.get(result.pattern.id);
464
+ if (ftsScore !== undefined) {
465
+ result.score = 0.75 * result.score + 0.25 * ftsScore;
466
+ }
467
+ }
468
+ // Add FTS5-only results not already in vector results
469
+ for (const ftsResult of ftsResults) {
470
+ if (existingIds.has(ftsResult.id))
471
+ continue;
472
+ const pattern = await this.get(ftsResult.id);
473
+ if (pattern && this.matchesFilters(pattern, options)) {
474
+ const reuseInfo = this.calculateReuseInfo(pattern, ftsResult.ftsScore);
475
+ results.push({
476
+ pattern,
477
+ score: 0.5 * ftsResult.ftsScore, // FTS-only: exact keyword match is valuable
478
+ matchType: 'exact',
479
+ similarity: ftsResult.ftsScore,
480
+ canReuse: reuseInfo.canReuse,
481
+ estimatedTokenSavings: reuseInfo.estimatedTokenSavings,
482
+ reuseConfidence: reuseInfo.reuseConfidence,
483
+ });
484
+ }
485
+ }
486
+ }
487
+ }
488
+ catch {
489
+ // FTS5 unavailable, continue with text fallback
490
+ }
491
+ }
365
492
  // Text search fallback or additional
366
493
  if (typeof query === 'string' || results.length < limit) {
367
494
  const textResults = await this.searchByText(typeof query === 'string' ? query : '', options, limit - results.length);
368
- results.push(...textResults);
495
+ // Deduplicate: only add text results not already present
496
+ const existingIds = new Set(results.map(r => r.pattern.id));
497
+ for (const tr of textResults) {
498
+ if (!existingIds.has(tr.pattern.id)) {
499
+ results.push(tr);
500
+ }
501
+ }
502
+ }
503
+ // Apply temporal decay: boost recent patterns, penalize stale ones
504
+ // Half-life of 30 days — patterns used recently score higher
505
+ const TEMPORAL_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
506
+ const now = Date.now();
507
+ for (const result of results) {
508
+ const lastUsed = result.pattern.lastUsedAt?.getTime() ?? result.pattern.createdAt.getTime();
509
+ const ageMs = now - lastUsed;
510
+ const decayFactor = Math.pow(0.5, ageMs / TEMPORAL_HALF_LIFE_MS);
511
+ // Only boost patterns that have been used — new untested patterns get neutral score
512
+ const effectiveDecay = result.pattern.usageCount > 0 ? decayFactor : 0.5;
513
+ // Multiplicative decay: preserves relative ordering from search scoring
514
+ // while penalizing stale patterns (decayFactor is 0-1)
515
+ result.score = result.score * (0.7 + 0.3 * effectiveDecay);
369
516
  }
370
517
  // Sort by score and limit
371
518
  results.sort((a, b) => b.score - a.score);
@@ -555,6 +702,15 @@ export class PatternStore {
555
702
  qualityScore,
556
703
  lastUsedAt: now,
557
704
  };
705
+ // Persist usage to SQLite
706
+ if (this.sqliteStore) {
707
+ try {
708
+ this.sqliteStore.recordUsage(id, success);
709
+ }
710
+ catch (error) {
711
+ console.warn(`[PatternStore] SQLite recordUsage failed for ${id}:`, toErrorMessage(error));
712
+ }
713
+ }
558
714
  // Check for promotion (ADR-052: shouldPromotePattern returns PromotionCheck object)
559
715
  const promotionCheck = shouldPromotePattern(updated);
560
716
  const shouldPromote = promotionCheck.meetsUsageCriteria &&
@@ -564,7 +720,7 @@ export class PatternStore {
564
720
  await this.promote(id);
565
721
  }
566
722
  else {
567
- // Update cache only - persistence handled by SQLitePatternStore
723
+ // Update in-memory cache
568
724
  this.patternCache.set(id, updated);
569
725
  }
570
726
  return ok(undefined);
@@ -699,7 +855,10 @@ export class PatternStore {
699
855
  }
700
856
  // Check for removal (short-term, old, low quality)
701
857
  if (pattern.tier === 'short-term') {
702
- const ageMs = Date.now() - pattern.createdAt.getTime();
858
+ const createdTime = pattern.createdAt instanceof Date
859
+ ? pattern.createdAt.getTime()
860
+ : new Date(pattern.createdAt).getTime();
861
+ const ageMs = Date.now() - createdTime;
703
862
  const isOld = ageMs > 7 * 24 * 60 * 60 * 1000; // 7 days
704
863
  const isLowQuality = pattern.qualityScore < 0.2;
705
864
  const isUnused = pattern.usageCount === 0 && ageMs > 24 * 60 * 60 * 1000; // 1 day
@@ -25,6 +25,7 @@ export declare const QE_HOOK_EVENTS: {
25
25
  readonly PatternLearned: "qe:pattern-learned";
26
26
  readonly PatternApplied: "qe:pattern-applied";
27
27
  readonly PatternPromoted: "qe:pattern-promoted";
28
+ readonly PreCompaction: "qe:pre-compaction";
28
29
  };
29
30
  export type QEHookEvent = (typeof QE_HOOK_EVENTS)[keyof typeof QE_HOOK_EVENTS];
30
31
  /**
@@ -34,6 +34,8 @@ export const QE_HOOK_EVENTS = {
34
34
  PatternLearned: 'qe:pattern-learned',
35
35
  PatternApplied: 'qe:pattern-applied',
36
36
  PatternPromoted: 'qe:pattern-promoted',
37
+ // Session lifecycle
38
+ PreCompaction: 'qe:pre-compaction',
37
39
  };
38
40
  // ============================================================================
39
41
  // QE Hook Handlers
@@ -434,6 +436,34 @@ export function createQEHookHandlers(reasoningBank) {
434
436
  data: { patternId, newTier },
435
437
  };
436
438
  },
439
+ // ========================================================================
440
+ // Session Lifecycle Hooks
441
+ // ========================================================================
442
+ [QE_HOOK_EVENTS.PreCompaction]: async (ctx) => {
443
+ const stats = { experiencesFlushed: 0, patternsPromoted: 0 };
444
+ // Flush pending experiences before context compaction
445
+ if (ctx.data?.experienceCaptureService) {
446
+ const service = ctx.data.experienceCaptureService;
447
+ const pendingCount = service.getPendingCount?.() ?? 0;
448
+ if (pendingCount > 0) {
449
+ const flushed = await service.flushPending?.();
450
+ stats.experiencesFlushed = flushed ?? pendingCount;
451
+ }
452
+ }
453
+ // Promote eligible patterns before compaction
454
+ if (ctx.data?.patternLifecycleManager) {
455
+ const manager = ctx.data.patternLifecycleManager;
456
+ const promotionResult = manager.runPromotionSweep?.();
457
+ if (promotionResult) {
458
+ stats.patternsPromoted = promotionResult.promoted ?? 0;
459
+ }
460
+ }
461
+ console.log('[QEHooks] Pre-compaction flush:', stats);
462
+ return {
463
+ success: true,
464
+ data: stats,
465
+ };
466
+ },
437
467
  };
438
468
  }
439
469
  // ============================================================================
@@ -195,6 +195,12 @@ export declare function calculateQualityScore(pattern: {
195
195
  /**
196
196
  * Pattern promotion check result
197
197
  */
198
+ /**
199
+ * Shared promotion threshold: minimum successful uses before a short-term
200
+ * pattern can be promoted to long-term. Used by pattern-store, pattern-lifecycle,
201
+ * experience-capture, and shouldPromotePattern().
202
+ */
203
+ export declare const PROMOTION_THRESHOLD = 3;
198
204
  export interface PromotionCheck {
199
205
  meetsUsageCriteria: boolean;
200
206
  meetsQualityCriteria: boolean;
@@ -72,6 +72,15 @@ export function calculateQualityScore(pattern) {
72
72
  const usageScore = Math.min(pattern.usageCount / 100, 1);
73
73
  return (pattern.confidence * 0.3 + usageScore * 0.2 + pattern.successRate * 0.5);
74
74
  }
75
+ /**
76
+ * Pattern promotion check result
77
+ */
78
+ /**
79
+ * Shared promotion threshold: minimum successful uses before a short-term
80
+ * pattern can be promoted to long-term. Used by pattern-store, pattern-lifecycle,
81
+ * experience-capture, and shouldPromotePattern().
82
+ */
83
+ export const PROMOTION_THRESHOLD = 3;
75
84
  /**
76
85
  * Check if pattern should be promoted to long-term storage
77
86
  * Requires 3+ successful uses as per ADR-021
@@ -82,7 +91,7 @@ export function calculateQualityScore(pattern) {
82
91
  * @param coherenceThreshold - Threshold for coherence violation (default: 0.4)
83
92
  */
84
93
  export function shouldPromotePattern(pattern, coherenceEnergy, coherenceThreshold = 0.4) {
85
- const meetsUsageCriteria = pattern.tier === 'short-term' && pattern.successfulUses >= 3;
94
+ const meetsUsageCriteria = pattern.tier === 'short-term' && pattern.successfulUses >= PROMOTION_THRESHOLD;
86
95
  const meetsQualityCriteria = pattern.successRate >= 0.7 && pattern.confidence >= 0.6;
87
96
  // NEW: Coherence criteria - only block if coherence energy is provided and exceeds threshold
88
97
  const meetsCoherenceCriteria = coherenceEnergy === undefined || coherenceEnergy < coherenceThreshold;
@@ -36,6 +36,13 @@ export interface SQLitePersistenceConfig {
36
36
  useUnified: boolean;
37
37
  }
38
38
  export declare const DEFAULT_SQLITE_CONFIG: SQLitePersistenceConfig;
39
+ /**
40
+ * Hash-based embedding generation (no ONNX dependency).
41
+ * Deterministic: same text always produces the same embedding.
42
+ * FALLBACK ONLY — real embeddings use all-MiniLM-L6-v2 via computeBatchEmbeddings().
43
+ * Kept for environments where ONNX/@xenova/transformers is unavailable.
44
+ */
45
+ export declare function hashEmbedding(text: string, dimension?: number): number[];
39
46
  /**
40
47
  * SQLite-based pattern persistence
41
48
  */
@@ -78,6 +85,23 @@ export declare class SQLitePatternStore {
78
85
  domain?: QEDomain;
79
86
  limit?: number;
80
87
  }): QEPattern[];
88
+ /**
89
+ * FTS5 full-text search for patterns.
90
+ * Returns pattern IDs with BM25 relevance scores.
91
+ */
92
+ searchFTS(query: string, limit?: number): Array<{
93
+ id: string;
94
+ ftsScore: number;
95
+ }>;
96
+ /**
97
+ * Ghost pattern check: find patterns in SQLite that have no embeddings.
98
+ * Used by aqe_health to detect data integrity issues.
99
+ */
100
+ getGhostPatternCount(): {
101
+ total: number;
102
+ withoutEmbeddings: number;
103
+ sampleGhostIds: string[];
104
+ };
81
105
  /**
82
106
  * Get all embeddings for HNSW indexing
83
107
  */
@@ -159,6 +183,22 @@ export declare class SQLitePatternStore {
159
183
  avgSimilarity: number;
160
184
  }>;
161
185
  };
186
+ /**
187
+ * Backfill embeddings for patterns that don't have them.
188
+ * Uses real all-MiniLM-L6-v2 transformer embeddings via @xenova/transformers.
189
+ * Falls back to hash-based embeddings if ONNX is unavailable.
190
+ * Skips bench/test patterns (id LIKE 'bench-%').
191
+ *
192
+ * @param batchSize - Patterns per inference batch (default: 32, matches model batch size)
193
+ * @returns Stats about the backfill operation
194
+ */
195
+ backfillEmbeddings(batchSize?: number): Promise<{
196
+ processed: number;
197
+ skipped: number;
198
+ errors: number;
199
+ alreadyHad: number;
200
+ method: 'transformer' | 'hash-fallback';
201
+ }>;
162
202
  /**
163
203
  * Close the database
164
204
  */