agentic-qe 3.7.14 → 3.7.16
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/helpers/brain-checkpoint.cjs +11 -0
- package/.claude/skills/skills-manifest.json +1 -1
- package/CHANGELOG.md +49 -0
- package/dist/cli/bundle.js +1260 -528
- package/dist/cli/commands/prove.d.ts +60 -0
- package/dist/cli/commands/prove.js +167 -0
- package/dist/cli/handlers/brain-handler.js +2 -1
- package/dist/cli/index.js +2 -0
- package/dist/domains/test-generation/coordinator.js +6 -4
- 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/feedback/feedback-loop.d.ts +5 -0
- package/dist/feedback/feedback-loop.js +12 -0
- package/dist/feedback/index.d.ts +1 -1
- package/dist/feedback/index.js +1 -1
- package/dist/kernel/hnsw-adapter.d.ts +3 -0
- package/dist/kernel/hnsw-adapter.js +11 -1
- package/dist/kernel/unified-memory-schemas.d.ts +3 -3
- package/dist/kernel/unified-memory-schemas.js +28 -1
- package/dist/kernel/unified-memory.js +57 -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-middleware.js +24 -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 +43 -0
- package/dist/learning/sqlite-persistence.js +237 -1
- package/dist/learning/token-tracker.js +4 -0
- package/dist/mcp/bundle.js +836 -48
- package/dist/mcp/handlers/core-handlers.d.ts +5 -0
- package/dist/mcp/handlers/core-handlers.js +11 -0
- package/dist/mcp/handlers/handler-factory.js +92 -11
- 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/dist/routing/routing-feedback.d.ts +5 -0
- package/dist/routing/routing-feedback.js +29 -3
- package/dist/sync/pull-agent.js +2 -1
- package/dist/test-scheduling/pipeline.d.ts +7 -0
- package/dist/test-scheduling/pipeline.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPD (Observe-Plan-Decide) Remediation Hints
|
|
3
|
+
*
|
|
4
|
+
* When patterns have negative rewards (failures, flakiness), generate
|
|
5
|
+
* actionable hints: "bad because X, fix by Y"
|
|
6
|
+
*
|
|
7
|
+
* Categories:
|
|
8
|
+
* - flaky: intermittent failures (20-80% fail rate, 3+ executions)
|
|
9
|
+
* - false-positive: consistently broken (80%+ fail rate)
|
|
10
|
+
* - outdated: was working, now fails (recent regression)
|
|
11
|
+
* - wrong-scope: vague description + high failure rate
|
|
12
|
+
* - missing-context: recurring keywords in failure feedback
|
|
13
|
+
*/
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Hint Generation
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Generate remediation hints for a failed pattern based on its execution history.
|
|
19
|
+
*
|
|
20
|
+
* Analyzes execution records to classify the failure mode and produce
|
|
21
|
+
* actionable suggestions for fixing the pattern.
|
|
22
|
+
*/
|
|
23
|
+
export function generateRemediationHints(pattern, executionHistory, config) {
|
|
24
|
+
const hints = [];
|
|
25
|
+
const maxHints = config?.maxHintsPerPattern ?? 3;
|
|
26
|
+
const totalCount = executionHistory.length;
|
|
27
|
+
if (totalCount === 0)
|
|
28
|
+
return hints;
|
|
29
|
+
const failCount = executionHistory.filter((e) => !e.success).length;
|
|
30
|
+
const failRate = failCount / totalCount;
|
|
31
|
+
// Category 1: Flaky (intermittent failures — 20-80% fail rate, 3+ runs)
|
|
32
|
+
if (failRate > 0.2 && failRate < 0.8 && totalCount >= 3) {
|
|
33
|
+
hints.push({
|
|
34
|
+
patternId: pattern.id,
|
|
35
|
+
observation: `Pattern "${pattern.name}" fails ${(failRate * 100).toFixed(0)}% of the time (${failCount}/${totalCount} executions)`,
|
|
36
|
+
diagnosis: 'Intermittent failures suggest timing dependencies, external service flakiness, or non-deterministic behavior',
|
|
37
|
+
suggestion: 'Add retry logic, mock external dependencies, or add explicit waits for async operations',
|
|
38
|
+
confidence: Math.min(0.9, 0.5 + totalCount * 0.05),
|
|
39
|
+
category: 'flaky',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Category 2: False positive (always/almost always fails)
|
|
43
|
+
if (failRate >= 0.8 && totalCount >= 2) {
|
|
44
|
+
hints.push({
|
|
45
|
+
patternId: pattern.id,
|
|
46
|
+
observation: `Pattern "${pattern.name}" fails ${(failRate * 100).toFixed(0)}% of executions — effectively broken`,
|
|
47
|
+
diagnosis: 'Consistent failures indicate the pattern logic is incorrect or the target code changed',
|
|
48
|
+
suggestion: 'Review the pattern against current code. Consider quarantining and creating a replacement pattern.',
|
|
49
|
+
confidence: 0.85,
|
|
50
|
+
category: 'false-positive',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Category 3: Outdated (was working, now failing — regression)
|
|
54
|
+
if (totalCount >= 5) {
|
|
55
|
+
const recentFails = executionHistory.slice(-3).filter((e) => !e.success).length;
|
|
56
|
+
const earlySuccesses = executionHistory
|
|
57
|
+
.slice(0, Math.max(1, totalCount - 3))
|
|
58
|
+
.filter((e) => e.success).length;
|
|
59
|
+
if (recentFails >= 2 && earlySuccesses >= 2) {
|
|
60
|
+
hints.push({
|
|
61
|
+
patternId: pattern.id,
|
|
62
|
+
observation: `Pattern "${pattern.name}" worked previously but now fails consistently`,
|
|
63
|
+
diagnosis: 'Recent code changes likely broke compatibility with this pattern',
|
|
64
|
+
suggestion: 'Update pattern to match current code structure. Check git log for recent changes to affected files.',
|
|
65
|
+
confidence: 0.8,
|
|
66
|
+
category: 'outdated',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Category 4: Wrong scope (too broad — vague description + high failure)
|
|
71
|
+
if (pattern.description && pattern.description.length < 20 && failRate > 0.3) {
|
|
72
|
+
hints.push({
|
|
73
|
+
patternId: pattern.id,
|
|
74
|
+
observation: `Pattern "${pattern.name}" has a vague description and high failure rate`,
|
|
75
|
+
diagnosis: 'Pattern may be too broadly scoped — matching contexts where it does not apply',
|
|
76
|
+
suggestion: 'Narrow the pattern scope by adding specific tags, domain constraints, or more detailed matching criteria',
|
|
77
|
+
confidence: 0.6,
|
|
78
|
+
category: 'wrong-scope',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Category 5: Missing context (recurring keywords in failure feedback)
|
|
82
|
+
const feedbackMessages = executionHistory
|
|
83
|
+
.filter((e) => !e.success && e.feedback)
|
|
84
|
+
.map((e) => e.feedback)
|
|
85
|
+
.slice(-3);
|
|
86
|
+
if (feedbackMessages.length > 0) {
|
|
87
|
+
const commonWords = findCommonKeywords(feedbackMessages);
|
|
88
|
+
if (commonWords.length > 0) {
|
|
89
|
+
hints.push({
|
|
90
|
+
patternId: pattern.id,
|
|
91
|
+
observation: `Failure feedback contains recurring themes: ${commonWords.join(', ')}`,
|
|
92
|
+
diagnosis: `Common failure keywords suggest a systematic issue: ${commonWords.slice(0, 3).join(', ')}`,
|
|
93
|
+
suggestion: `Address the recurring "${commonWords[0]}" issue in the pattern logic or preconditions`,
|
|
94
|
+
confidence: 0.65,
|
|
95
|
+
category: 'missing-context',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return hints.slice(0, maxHints);
|
|
100
|
+
}
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Keyword Extraction (Internal)
|
|
103
|
+
// ============================================================================
|
|
104
|
+
const STOP_WORDS = new Set([
|
|
105
|
+
'the', 'a', 'an', 'is', 'was', 'in', 'to', 'for', 'of',
|
|
106
|
+
'and', 'or', 'not', 'with', 'test', 'error',
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* Find common keywords across failure feedback messages.
|
|
110
|
+
* Returns words that appear in at least 2 distinct messages, sorted by frequency.
|
|
111
|
+
*/
|
|
112
|
+
export function findCommonKeywords(feedbacks) {
|
|
113
|
+
const wordCounts = new Map();
|
|
114
|
+
for (const feedback of feedbacks) {
|
|
115
|
+
const words = feedback
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.split(/\W+/)
|
|
118
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
119
|
+
const uniqueWords = new Set(words);
|
|
120
|
+
for (const word of uniqueWords) {
|
|
121
|
+
wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Array.from(wordCounts.entries())
|
|
125
|
+
.filter(([, count]) => count >= 2)
|
|
126
|
+
.sort((a, b) => b[1] - a[1])
|
|
127
|
+
.map(([word]) => word)
|
|
128
|
+
.slice(0, 5);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=opd-remediation.js.map
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Confidence decay over time
|
|
10
10
|
*/
|
|
11
11
|
import type { Database as DatabaseType } from 'better-sqlite3';
|
|
12
|
-
import type
|
|
12
|
+
import { type QEDomain, type QEPatternType } from './qe-patterns.js';
|
|
13
13
|
import { AsymmetricLearningEngine, type AsymmetricLearningConfig } from './asymmetric-learning.js';
|
|
14
14
|
import type { WitnessChain } from '../audit/witness-chain.js';
|
|
15
15
|
/**
|
|
@@ -32,6 +32,8 @@ export interface PatternLifecycleConfig {
|
|
|
32
32
|
minActiveConfidence: number;
|
|
33
33
|
/** Maximum age in days before automatic deprecation review */
|
|
34
34
|
maxAgeForActivePatterns: number;
|
|
35
|
+
/** Days of activity window required for pattern promotion (default: 30) */
|
|
36
|
+
promotionActivityWindowDays: number;
|
|
35
37
|
/** ADR-061: Asymmetric learning config */
|
|
36
38
|
asymmetricLearning: Partial<AsymmetricLearningConfig>;
|
|
37
39
|
}
|
|
@@ -75,6 +77,7 @@ export interface PromotionCheckResult {
|
|
|
75
77
|
meetsRewardThreshold: boolean;
|
|
76
78
|
meetsOccurrenceThreshold: boolean;
|
|
77
79
|
meetsSuccessRateThreshold: boolean;
|
|
80
|
+
meetsActivityWindow: boolean;
|
|
78
81
|
currentReward: number;
|
|
79
82
|
currentOccurrences: number;
|
|
80
83
|
currentSuccessRate: number;
|
|
@@ -165,6 +168,14 @@ export declare class PatternLifecycleManager {
|
|
|
165
168
|
promoted: number;
|
|
166
169
|
checked: number;
|
|
167
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Run a promotion sweep — convenience alias for pre-compaction hook.
|
|
173
|
+
* Iterates short-term patterns, checks promotion criteria, promotes eligible ones.
|
|
174
|
+
*/
|
|
175
|
+
runPromotionSweep(): {
|
|
176
|
+
promoted: number;
|
|
177
|
+
checked: number;
|
|
178
|
+
};
|
|
168
179
|
/**
|
|
169
180
|
* Check if a pattern should be deprecated
|
|
170
181
|
*/
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Quality feedback loops
|
|
9
9
|
* - Confidence decay over time
|
|
10
10
|
*/
|
|
11
|
+
import { PROMOTION_THRESHOLD } from './qe-patterns.js';
|
|
11
12
|
import { AsymmetricLearningEngine } from './asymmetric-learning.js';
|
|
12
13
|
import { safeJsonParse } from '../shared/safe-json.js';
|
|
13
14
|
import { LoggerFactory } from '../logging/index.js';
|
|
@@ -17,13 +18,14 @@ const logger = LoggerFactory.create('pattern-lifecycle');
|
|
|
17
18
|
*/
|
|
18
19
|
export const DEFAULT_LIFECYCLE_CONFIG = {
|
|
19
20
|
promotionRewardThreshold: 0.7,
|
|
20
|
-
promotionMinOccurrences:
|
|
21
|
+
promotionMinOccurrences: PROMOTION_THRESHOLD,
|
|
21
22
|
promotionMinSuccessRate: 0.7,
|
|
22
23
|
deprecationFailureThreshold: 3,
|
|
23
24
|
staleDaysThreshold: 30,
|
|
24
25
|
confidenceDecayRate: 0.01, // 1% per day
|
|
25
26
|
minActiveConfidence: 0.3,
|
|
26
27
|
maxAgeForActivePatterns: 90,
|
|
28
|
+
promotionActivityWindowDays: 30,
|
|
27
29
|
asymmetricLearning: {},
|
|
28
30
|
};
|
|
29
31
|
// ============================================================================
|
|
@@ -306,6 +308,7 @@ Pattern extracted from ${exp.count} successful experiences.`;
|
|
|
306
308
|
meetsRewardThreshold: false,
|
|
307
309
|
meetsOccurrenceThreshold: false,
|
|
308
310
|
meetsSuccessRateThreshold: false,
|
|
311
|
+
meetsActivityWindow: false,
|
|
309
312
|
currentReward: 0,
|
|
310
313
|
currentOccurrences: 0,
|
|
311
314
|
currentSuccessRate: 0,
|
|
@@ -315,11 +318,17 @@ Pattern extracted from ${exp.count} successful experiences.`;
|
|
|
315
318
|
const meetsReward = avgReward >= this.config.promotionRewardThreshold;
|
|
316
319
|
const meetsOccurrences = pattern.usageCount >= this.config.promotionMinOccurrences;
|
|
317
320
|
const meetsSuccessRate = pattern.successRate >= this.config.promotionMinSuccessRate;
|
|
321
|
+
// Temporal window: require activity within configured window to prevent
|
|
322
|
+
// promoting stale patterns that haven't been validated recently
|
|
323
|
+
const PROMOTION_ACTIVITY_WINDOW_MS = this.config.promotionActivityWindowDays * 24 * 60 * 60 * 1000;
|
|
324
|
+
const lastActivity = pattern.lastUsedAt?.getTime() ?? pattern.createdAt.getTime();
|
|
325
|
+
const meetsActivityWindow = (Date.now() - lastActivity) < PROMOTION_ACTIVITY_WINDOW_MS;
|
|
318
326
|
return {
|
|
319
|
-
shouldPromote: pattern.tier === 'short-term' && meetsReward && meetsOccurrences && meetsSuccessRate,
|
|
327
|
+
shouldPromote: pattern.tier === 'short-term' && meetsReward && meetsOccurrences && meetsSuccessRate && meetsActivityWindow,
|
|
320
328
|
meetsRewardThreshold: meetsReward,
|
|
321
329
|
meetsOccurrenceThreshold: meetsOccurrences,
|
|
322
330
|
meetsSuccessRateThreshold: meetsSuccessRate,
|
|
331
|
+
meetsActivityWindow,
|
|
323
332
|
currentReward: avgReward,
|
|
324
333
|
currentOccurrences: pattern.usageCount,
|
|
325
334
|
currentSuccessRate: pattern.successRate,
|
|
@@ -360,6 +369,13 @@ Pattern extracted from ${exp.count} successful experiences.`;
|
|
|
360
369
|
}
|
|
361
370
|
return { promoted, checked: shortTermPatterns.length };
|
|
362
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Run a promotion sweep — convenience alias for pre-compaction hook.
|
|
374
|
+
* Iterates short-term patterns, checks promotion criteria, promotes eligible ones.
|
|
375
|
+
*/
|
|
376
|
+
runPromotionSweep() {
|
|
377
|
+
return this.promoteEligiblePatterns();
|
|
378
|
+
}
|
|
363
379
|
// ============================================================================
|
|
364
380
|
// Pattern Deprecation
|
|
365
381
|
// ============================================================================
|
|
@@ -183,6 +183,7 @@ export declare class PatternStore implements IPatternStore {
|
|
|
183
183
|
private initialized;
|
|
184
184
|
private cleanupTimer?;
|
|
185
185
|
private sqliteStore;
|
|
186
|
+
private loadingPromise;
|
|
186
187
|
private patternCache;
|
|
187
188
|
private domainIndex;
|
|
188
189
|
private typeIndex;
|
|
@@ -193,9 +194,12 @@ export declare class PatternStore implements IPatternStore {
|
|
|
193
194
|
private stats;
|
|
194
195
|
constructor(memory: MemoryBackend, config?: Partial<PatternStoreConfig>);
|
|
195
196
|
/**
|
|
196
|
-
* Set SQLite persistence delegate
|
|
197
|
-
*
|
|
198
|
-
*
|
|
197
|
+
* Set SQLite persistence delegate and load patterns into memory.
|
|
198
|
+
*
|
|
199
|
+
* When set, PatternStore will:
|
|
200
|
+
* 1. Load existing patterns from SQLite into the in-memory cache
|
|
201
|
+
* 2. Forward create/delete/promote operations to SQLite for persistence
|
|
202
|
+
* 3. Persist embeddings alongside patterns on store()
|
|
199
203
|
*/
|
|
200
204
|
setSqliteStore(store: import('./sqlite-persistence.js').SQLitePatternStore): void;
|
|
201
205
|
/**
|
|
@@ -220,7 +224,11 @@ export declare class PatternStore implements IPatternStore {
|
|
|
220
224
|
*/
|
|
221
225
|
private initializeHNSWInternal;
|
|
222
226
|
/**
|
|
223
|
-
* Load existing patterns from
|
|
227
|
+
* Load existing patterns from SQLite into in-memory cache.
|
|
228
|
+
*
|
|
229
|
+
* Previously this was a no-op after Issue #258 removed kv_store duplication,
|
|
230
|
+
* but that left 15,634 SQLite patterns invisible to search on every restart.
|
|
231
|
+
* Now properly loads from SQLitePatternStore when wired.
|
|
224
232
|
*/
|
|
225
233
|
private loadPatterns;
|
|
226
234
|
/**
|
|
@@ -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:
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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 >=
|
|
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;
|