@edihasaj/recall 0.5.7 → 0.6.0

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 (35) hide show
  1. package/dist/{chunk-K5FZ47NN.js → chunk-7XCLKPJ3.js} +6 -8
  2. package/dist/{chunk-K5FZ47NN.js.map → chunk-7XCLKPJ3.js.map} +1 -1
  3. package/dist/{chunk-A5UIRZU6.js → chunk-A6XEULA4.js} +3 -2
  4. package/dist/chunk-A6XEULA4.js.map +1 -0
  5. package/dist/{chunk-F56Y3SHS.js → chunk-E4HJDGCW.js} +7 -9
  6. package/dist/{chunk-F56Y3SHS.js.map → chunk-E4HJDGCW.js.map} +1 -1
  7. package/dist/{chunk-IILLSHLM.js → chunk-KAGIAOD7.js} +2583 -84
  8. package/dist/chunk-KAGIAOD7.js.map +1 -0
  9. package/dist/{chunk-FHKV6ELT.js → chunk-MJ4GGBTL.js} +11 -13
  10. package/dist/{chunk-FHKV6ELT.js.map → chunk-MJ4GGBTL.js.map} +1 -1
  11. package/dist/{chunk-LVQW6WHK.js → chunk-XUM7JEJU.js} +2 -2
  12. package/dist/{cleanup-TVOX2S2S.js → cleanup-MYSQ44EP.js} +4 -4
  13. package/dist/cli.js +206 -33
  14. package/dist/cli.js.map +1 -1
  15. package/dist/daemon.js +60 -49
  16. package/dist/daemon.js.map +1 -1
  17. package/dist/dispatcher-SUUX5AX6.js +16 -0
  18. package/dist/mcp.js +5 -5
  19. package/dist/{quality-Z7LPMMBC.js → quality-YTQKAEY6.js} +3 -3
  20. package/dist/{tasks-UOLSPXJQ.js → tasks-GSQUHD4F.js} +6 -3
  21. package/dist/{usage-CY3V72YN.js → usage-DU4TKVJH.js} +2 -2
  22. package/package.json +1 -1
  23. package/dist/chunk-A5UIRZU6.js.map +0 -1
  24. package/dist/chunk-GC5XMBG4.js +0 -551
  25. package/dist/chunk-GC5XMBG4.js.map +0 -1
  26. package/dist/chunk-IILLSHLM.js.map +0 -1
  27. package/dist/chunk-VEPXEHRZ.js +0 -1763
  28. package/dist/chunk-VEPXEHRZ.js.map +0 -1
  29. package/dist/dispatcher-UGMU6THT.js +0 -15
  30. /package/dist/{chunk-LVQW6WHK.js.map → chunk-XUM7JEJU.js.map} +0 -0
  31. /package/dist/{cleanup-TVOX2S2S.js.map → cleanup-MYSQ44EP.js.map} +0 -0
  32. /package/dist/{dispatcher-UGMU6THT.js.map → dispatcher-SUUX5AX6.js.map} +0 -0
  33. /package/dist/{quality-Z7LPMMBC.js.map → quality-YTQKAEY6.js.map} +0 -0
  34. /package/dist/{tasks-UOLSPXJQ.js.map → tasks-GSQUHD4F.js.map} +0 -0
  35. /package/dist/{usage-CY3V72YN.js.map → usage-DU4TKVJH.js.map} +0 -0
@@ -1,1763 +0,0 @@
1
- import {
2
- CONFIDENCE,
3
- appendEvidence,
4
- countDistinctCorrectionSessions,
5
- createMemory,
6
- demoteMemory,
7
- enqueueVerifyCapture,
8
- findSemanticDuplicates,
9
- findSimilarRejectedExemplar,
10
- getMemory,
11
- getMemoryFeedback,
12
- incrementMemoryRepetition,
13
- listMemories,
14
- loadEmbeddingConfigFromEnv,
15
- memoryDedupeKey,
16
- promoteMemory,
17
- queryMemories,
18
- recordAudit,
19
- recordAuditWithSnapshot,
20
- updateMemoryCaptureContext
21
- } from "./chunk-IILLSHLM.js";
22
- import {
23
- contradictions,
24
- feedbackEvents,
25
- implicitSignals,
26
- maintenanceCleanupLog,
27
- memories,
28
- memoryInjections
29
- } from "./chunk-A5UIRZU6.js";
30
-
31
- // src/maintenance/cleanup.ts
32
- import { and as and2, eq as eq4, gte, inArray, sql } from "drizzle-orm";
33
- import { randomUUID as randomUUID2 } from "crypto";
34
-
35
- // src/contradictions/detector.ts
36
- import { eq, and } from "drizzle-orm";
37
- import { randomUUID } from "crypto";
38
- function detectContradictions(db, repo) {
39
- const mems = queryMemories(db, { repo }).filter(
40
- (m) => m.status === "active" || m.status === "candidate"
41
- );
42
- const found = [];
43
- const seen = /* @__PURE__ */ new Set();
44
- for (let i = 0; i < mems.length; i++) {
45
- for (let j = i + 1; j < mems.length; j++) {
46
- const a = mems[i];
47
- const b = mems[j];
48
- const pairKey = [a.id, b.id].sort().join(":");
49
- if (seen.has(pairKey)) continue;
50
- const contradiction = checkContradiction(a, b);
51
- if (contradiction) {
52
- seen.add(pairKey);
53
- const existing = db.select().from(contradictions).where(
54
- and(
55
- eq(contradictions.memory_a_id, a.id),
56
- eq(contradictions.memory_b_id, b.id)
57
- )
58
- ).get();
59
- if (!existing) {
60
- const id = randomUUID();
61
- const now = (/* @__PURE__ */ new Date()).toISOString();
62
- db.insert(contradictions).values({
63
- id,
64
- memory_a_id: a.id,
65
- memory_b_id: b.id,
66
- contradiction_type: contradiction.type,
67
- severity: contradiction.severity,
68
- description: contradiction.description,
69
- resolved: false,
70
- detected_at: now
71
- }).run();
72
- recordAudit(db, a.id, "contradiction_detected", "system", contradiction.description);
73
- recordAudit(db, b.id, "contradiction_detected", "system", contradiction.description);
74
- found.push({
75
- id,
76
- memory_a_id: a.id,
77
- memory_b_id: b.id,
78
- contradiction_type: contradiction.type,
79
- severity: contradiction.severity,
80
- description: contradiction.description,
81
- resolved: false,
82
- resolution: null,
83
- detected_at: now,
84
- resolved_at: null
85
- });
86
- }
87
- }
88
- }
89
- }
90
- return found;
91
- }
92
- function checkContradiction(a, b) {
93
- if (!scopesOverlap(a, b)) return null;
94
- const negation = checkDirectNegation(a, b);
95
- if (negation) return negation;
96
- const conflict = checkConflictingRules(a, b);
97
- if (conflict) return conflict;
98
- if (a.supersedes === b.id || b.supersedes === a.id) {
99
- return {
100
- type: "superseded",
101
- severity: "medium",
102
- description: `One memory supersedes the other`
103
- };
104
- }
105
- return null;
106
- }
107
- var NEGATION_PAIRS = [
108
- [/\balways\b/i, /\bnever\b/i],
109
- [/\bdo\b/i, /\bdo not\b|don't\b/i],
110
- [/\buse\b/i, /\bdo not use\b|don't use\b|never use\b/i],
111
- [/\brequired\b/i, /\bforbidden\b|prohibited\b/i],
112
- [/\benable\b/i, /\bdisable\b/i]
113
- ];
114
- function checkDirectNegation(a, b) {
115
- for (const [pos, neg] of NEGATION_PAIRS) {
116
- const aPos = pos.test(a.text) && !neg.test(a.text);
117
- const aNeg = neg.test(a.text) && !pos.test(a.text);
118
- const bPos = pos.test(b.text) && !neg.test(b.text);
119
- const bNeg = neg.test(b.text) && !pos.test(b.text);
120
- if (aPos && bNeg || aNeg && bPos) {
121
- const subjectA = extractSubject(a.text);
122
- const subjectB = extractSubject(b.text);
123
- if (subjectA && subjectB && wordOverlap(subjectA, subjectB) > 0.5) {
124
- return {
125
- type: "direct_negation",
126
- severity: "high",
127
- description: `"${a.text}" contradicts "${b.text}"`
128
- };
129
- }
130
- }
131
- }
132
- return null;
133
- }
134
- function checkConflictingRules(a, b) {
135
- if (a.type !== b.type) return null;
136
- if (a.type !== "rule" && a.type !== "command") return null;
137
- const useA = a.text.match(/\buse\s+(\S+)/i);
138
- const useB = b.text.match(/\buse\s+(\S+)/i);
139
- if (useA && useB) {
140
- const toolA = useA[1].toLowerCase().replace(/[,.:;]/g, "");
141
- const toolB = useB[1].toLowerCase().replace(/[,.:;]/g, "");
142
- if (toolA !== toolB) {
143
- const contextA = extractContext(a.text);
144
- const contextB = extractContext(b.text);
145
- if (contextA && contextB && wordOverlap(contextA, contextB) > 0.3) {
146
- return {
147
- type: "conflicting_rules",
148
- severity: "medium",
149
- description: `"use ${toolA}" vs "use ${toolB}" in similar context`
150
- };
151
- }
152
- }
153
- }
154
- const sim = wordOverlap(a.text, b.text);
155
- if (sim > 0.6 && sim < 0.95 && a.text !== b.text) {
156
- return {
157
- type: "scope_overlap",
158
- severity: "low",
159
- description: `Very similar memories (${(sim * 100).toFixed(0)}% overlap): "${a.text.slice(0, 50)}" vs "${b.text.slice(0, 50)}"`
160
- };
161
- }
162
- return null;
163
- }
164
- function resolveContradiction(db, contradictionId, keepMemoryId, actor, resolution) {
165
- const row = db.select().from(contradictions).where(eq(contradictions.id, contradictionId)).get();
166
- if (!row) return false;
167
- const now = (/* @__PURE__ */ new Date()).toISOString();
168
- const demoteId = row.memory_a_id === keepMemoryId ? row.memory_b_id : row.memory_a_id;
169
- demoteMemory(db, demoteId, `contradiction resolved: keep ${keepMemoryId.slice(0, 8)}`);
170
- db.update(contradictions).set({
171
- resolved: true,
172
- resolution: resolution ?? `Kept ${keepMemoryId.slice(0, 8)}, demoted ${demoteId.slice(0, 8)}`,
173
- resolved_at: now
174
- }).where(eq(contradictions.id, contradictionId)).run();
175
- recordAudit(db, keepMemoryId, "contradiction_resolved", actor, resolution ?? null);
176
- recordAudit(db, demoteId, "contradiction_resolved", actor, `demoted in favor of ${keepMemoryId.slice(0, 8)}`);
177
- return true;
178
- }
179
- function autoResolveContradictions(db, repo) {
180
- const unresolved = db.select().from(contradictions).where(eq(contradictions.resolved, false)).all();
181
- let resolved = 0;
182
- for (const c of unresolved) {
183
- const a = getMemory(db, c.memory_a_id);
184
- const b = getMemory(db, c.memory_b_id);
185
- if (!a || !b) continue;
186
- if (repo && a.repo !== repo && b.repo !== repo) continue;
187
- if (c.severity === "low") continue;
188
- if (Math.abs(a.confidence - b.confidence) < 0.15) continue;
189
- const keepId = a.confidence >= b.confidence ? a.id : b.id;
190
- resolveContradiction(db, c.id, keepId, "auto-resolver", "Auto-resolved: higher confidence wins");
191
- resolved++;
192
- }
193
- return resolved;
194
- }
195
- function listContradictions(db, options = {}) {
196
- if (options.resolved !== void 0) {
197
- return db.select().from(contradictions).where(eq(contradictions.resolved, options.resolved)).all();
198
- }
199
- return db.select().from(contradictions).all();
200
- }
201
- function scopesOverlap(a, b) {
202
- if (a.scope === "global" || b.scope === "global") return true;
203
- if (a.scope === "team" || b.scope === "team") return true;
204
- if (a.scope === "repo" && b.scope === "repo") {
205
- return !a.repo || !b.repo || a.repo === b.repo;
206
- }
207
- if (a.scope === "path" && b.scope === "path") {
208
- if (!a.path_scope || !b.path_scope) return true;
209
- return a.path_scope.startsWith(b.path_scope.replace("/**", "")) || b.path_scope.startsWith(a.path_scope.replace("/**", ""));
210
- }
211
- if (a.scope === "repo" && b.scope === "path" || a.scope === "path" && b.scope === "repo") {
212
- return !a.repo || !b.repo || a.repo === b.repo;
213
- }
214
- return false;
215
- }
216
- function extractSubject(text) {
217
- return text.replace(/\b(always|never|must|do not|don't|use|do|run|call|import)\b/gi, "").trim().toLowerCase();
218
- }
219
- function extractContext(text) {
220
- return text.replace(/\buse\s+\S+/i, "").trim().toLowerCase();
221
- }
222
- function wordOverlap(a, b) {
223
- const wordsA = new Set(a.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
224
- const wordsB = new Set(b.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
225
- if (wordsA.size === 0 || wordsB.size === 0) return 0;
226
- const intersection = [...wordsA].filter((w) => wordsB.has(w));
227
- const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
228
- return intersection.length / union.size;
229
- }
230
-
231
- // src/repo/quality.ts
232
- import { eq as eq3 } from "drizzle-orm";
233
-
234
- // src/health/scoring.ts
235
- import { eq as eq2 } from "drizzle-orm";
236
- var WEIGHTS = {
237
- confidence: 0.4,
238
- freshness: 0.25,
239
- follow_rate: 0.2,
240
- signal_ratio: 0.15
241
- };
242
- function computeHealthScore(db, memoryId) {
243
- const mem = getMemory(db, memoryId);
244
- if (!mem) return null;
245
- const confidence = mem.confidence;
246
- const freshness = computeFreshness(mem);
247
- const followRate = computeFollowRate(db, memoryId);
248
- const signalRatio = computeSignalRatio(db, memoryId);
249
- const score = WEIGHTS.confidence * confidence + WEIGHTS.freshness * freshness + WEIGHTS.follow_rate * followRate + WEIGHTS.signal_ratio * signalRatio;
250
- return {
251
- memory_id: memoryId,
252
- score: clamp(score),
253
- confidence_component: confidence,
254
- freshness_component: freshness,
255
- follow_rate_component: followRate,
256
- signal_ratio_component: signalRatio,
257
- computed_at: (/* @__PURE__ */ new Date()).toISOString()
258
- };
259
- }
260
- function computeAllHealthScores(db, repo) {
261
- const mems = repo ? queryMemories(db, { repo }) : listMemories(db);
262
- const scores = [];
263
- for (const mem of mems) {
264
- if (mem.status === "rejected") continue;
265
- const score = computeHealthScore(db, mem.id);
266
- if (score) scores.push(score);
267
- }
268
- return scores.sort((a, b) => b.score - a.score);
269
- }
270
- function computeFreshness(mem) {
271
- const now = Date.now();
272
- const referenceDate = mem.last_validated_at ?? mem.last_injected_at ?? mem.updated_at;
273
- const age = now - new Date(referenceDate).getTime();
274
- const dayMs = 864e5;
275
- const halfLife = 30 * dayMs;
276
- const freshness = Math.pow(0.5, age / halfLife);
277
- return clamp(freshness);
278
- }
279
- function computeFollowRate(db, memoryId) {
280
- const feedback = db.select().from(feedbackEvents).where(eq2(feedbackEvents.memory_id, memoryId)).all();
281
- if (feedback.length === 0) return 0.5;
282
- const followed = feedback.filter((f) => f.outcome === "followed").length;
283
- return followed / feedback.length;
284
- }
285
- function computeSignalRatio(db, memoryId) {
286
- const signals = db.select().from(implicitSignals).where(eq2(implicitSignals.memory_id, memoryId)).all();
287
- if (signals.length === 0) return 0.5;
288
- const positive = signals.filter(
289
- (s) => ["test_pass", "file_unchanged", "task_accepted"].includes(s.signal_type)
290
- ).length;
291
- return positive / signals.length;
292
- }
293
- function formatHealthReport(scores) {
294
- if (scores.length === 0) return "No memories to score.";
295
- const lines = [
296
- "# Memory Health Report",
297
- "",
298
- `Total: ${scores.length} memories scored`,
299
- "",
300
- "| Score | Conf | Fresh | Follow | Signal | ID |",
301
- "|-------|------|-------|--------|--------|----------|"
302
- ];
303
- for (const s of scores.slice(0, 30)) {
304
- lines.push(
305
- `| ${pct(s.score)} | ${pct(s.confidence_component)} | ${pct(s.freshness_component)} | ${pct(s.follow_rate_component)} | ${pct(s.signal_ratio_component)} | ${s.memory_id.slice(0, 8)} |`
306
- );
307
- }
308
- const avg = scores.reduce((sum, s) => sum + s.score, 0) / scores.length;
309
- const unhealthy = scores.filter((s) => s.score < 0.3).length;
310
- const healthy = scores.filter((s) => s.score >= 0.6).length;
311
- lines.push("");
312
- lines.push(`Avg score: ${pct(avg)}`);
313
- lines.push(`Healthy (\u22650.6): ${healthy} | Unhealthy (<0.3): ${unhealthy}`);
314
- return lines.join("\n");
315
- }
316
- function clamp(n) {
317
- return Math.max(0, Math.min(1, n));
318
- }
319
- function pct(n) {
320
- return (n * 100).toFixed(0).padStart(3) + "%";
321
- }
322
-
323
- // src/repo/quality.ts
324
- function getRepoQualityProfile(db, repo) {
325
- if (!repo) {
326
- return defaultProfile();
327
- }
328
- const memories4 = queryMemories(db, { repo }).filter((m) => m.status !== "rejected");
329
- const active = memories4.filter((m) => m.status === "active");
330
- const activeCount = active.length;
331
- const totalCount = memories4.length;
332
- const health = computeAllHealthScores(db, repo);
333
- const avgHealth = health.length > 0 ? health.reduce((sum, item) => sum + item.score, 0) / health.length : 0;
334
- const totalInjections = memories4.reduce((sum, m) => sum + m.injection_count, 0);
335
- const totalOverrides = memories4.reduce((sum, m) => sum + m.override_count, 0);
336
- const overrideRate = totalInjections > 0 ? totalOverrides / totalInjections : 0;
337
- const ids = new Set(memories4.map((m) => m.id));
338
- const unresolved = db.select().from(contradictions).where(eq3(contradictions.resolved, false)).all().filter((item) => ids.has(item.memory_a_id) || ids.has(item.memory_b_id));
339
- const contradictionRate = activeCount > 0 ? unresolved.length / activeCount : 0;
340
- const stage = classifyStage(activeCount);
341
- const pressure = clamp2(activeCount / 50);
342
- const score = clamp2(
343
- avgHealth * 0.5 + (1 - clamp2(overrideRate)) * 0.25 + (1 - clamp2(contradictionRate)) * 0.15 + (1 - pressure) * 0.1
344
- );
345
- let repeatSessionsRequired = stage === "cold" ? 2 : stage === "growing" ? 3 : 4;
346
- if (score >= 0.75 && repeatSessionsRequired > 2) {
347
- repeatSessionsRequired -= 1;
348
- } else if (score < 0.45) {
349
- repeatSessionsRequired += 1;
350
- }
351
- let compileConfidenceThreshold = stage === "cold" ? CONFIDENCE.ACTIVE_MIN : stage === "growing" ? 0.68 : 0.72;
352
- if (score < 0.45) {
353
- compileConfidenceThreshold += 0.05;
354
- } else if (score > 0.8) {
355
- compileConfidenceThreshold -= 0.03;
356
- }
357
- let dedupSimilarityThreshold = stage === "cold" ? 0.85 : stage === "growing" ? 0.8 : 0.75;
358
- if (score < 0.45) {
359
- dedupSimilarityThreshold -= 0.05;
360
- }
361
- return {
362
- repo,
363
- stage,
364
- score,
365
- total_count: totalCount,
366
- active_count: activeCount,
367
- avg_health: avgHealth,
368
- override_rate: clamp2(overrideRate),
369
- contradiction_rate: clamp2(contradictionRate),
370
- repeat_sessions_required: repeatSessionsRequired,
371
- compile_confidence_threshold: clamp2(
372
- compileConfidenceThreshold,
373
- CONFIDENCE.ACTIVE_MIN,
374
- 0.82
375
- ),
376
- dedup_similarity_threshold: clamp2(dedupSimilarityThreshold, 0.65, 0.9)
377
- };
378
- }
379
- function seedCandidateConfidence(baseConfidence, profile) {
380
- const maturityPenalty = profile.stage === "cold" ? 0 : profile.stage === "growing" ? 0.03 : 0.05;
381
- const qualityPenalty = profile.score < 0.45 ? 0.03 : 0;
382
- return clamp2(
383
- baseConfidence - maturityPenalty - qualityPenalty,
384
- CONFIDENCE.TRANSIENT_MAX + 0.05,
385
- CONFIDENCE.ACTIVE_MIN - 0.01
386
- );
387
- }
388
- function seedScannedConfidence(baseConfidence, profile) {
389
- const maturityPenalty = profile.stage === "cold" ? 0 : profile.stage === "growing" ? 0.02 : 0.05;
390
- const qualityPenalty = profile.score < 0.45 ? 0.03 : 0;
391
- return clamp2(
392
- baseConfidence - maturityPenalty - qualityPenalty,
393
- 0.5,
394
- 0.85
395
- );
396
- }
397
- function classifyStage(activeCount) {
398
- if (activeCount < 10) return "cold";
399
- if (activeCount < 50) return "growing";
400
- return "mature";
401
- }
402
- function defaultProfile() {
403
- return {
404
- stage: "cold",
405
- score: 0.35,
406
- total_count: 0,
407
- active_count: 0,
408
- avg_health: 0,
409
- override_rate: 0,
410
- contradiction_rate: 0,
411
- repeat_sessions_required: 2,
412
- compile_confidence_threshold: CONFIDENCE.ACTIVE_MIN,
413
- dedup_similarity_threshold: 0.85
414
- };
415
- }
416
- function clamp2(n, min = 0, max = 1) {
417
- return Math.max(min, Math.min(max, n));
418
- }
419
-
420
- // src/capture/scope.ts
421
- import { execSync } from "child_process";
422
- import { dirname, extname, basename } from "path";
423
- var SCOPE_MARKERS = [
424
- {
425
- pattern: /\b(in this file|this file only|just this file)\b/i,
426
- scope: "path",
427
- reason: "explicit file scope marker"
428
- },
429
- {
430
- pattern: /\b(in this directory|in this folder|this dir)\b/i,
431
- scope: "path",
432
- reason: "explicit directory scope marker"
433
- },
434
- {
435
- pattern: /\b(in this repo|for this repo|repo-wide|across the repo|this project)\b/i,
436
- scope: "repo",
437
- reason: "explicit repo scope marker"
438
- },
439
- {
440
- pattern: /\b(team-wide|company-wide|org-wide|for the team|for the org|across the team)\b/i,
441
- scope: "team",
442
- reason: "explicit team/org scope marker"
443
- },
444
- {
445
- pattern: /\b(for me always|always for me|agent-wide|regardless of project|across all my repos|in all my work|for all (?:my )?projects|everywhere|all repos)\b/i,
446
- scope: "global",
447
- reason: "explicit global/cross-repo scope marker"
448
- },
449
- {
450
- pattern: /\b(just for now|this time|for this task|temporarily)\b/i,
451
- scope: "session",
452
- reason: "explicit session scope marker"
453
- }
454
- ];
455
- var FRAMEWORK_INDICATORS = [
456
- /\b(typescript|javascript|python|rust|go|java|swift|ruby)\b/i,
457
- /\b(react|vue|angular|svelte|next\.?js|express|fastify|django|flask|rails)\b/i,
458
- /\b(eslint|prettier|biome|ruff|clippy|rubocop)\b/i,
459
- /\b(jest|vitest|pytest|cargo test|go test)\b/i,
460
- /\b(npm|yarn|pnpm|bun|pip|uv|poetry|cargo|go mod)\b/i
461
- ];
462
- var FILE_TYPE_SCOPES = {
463
- // Config files → repo scope
464
- ".json": "repo",
465
- ".yaml": "repo",
466
- ".yml": "repo",
467
- ".toml": "repo",
468
- ".ini": "repo",
469
- // Source files → path scope
470
- ".ts": "path",
471
- ".tsx": "path",
472
- ".js": "path",
473
- ".jsx": "path",
474
- ".py": "path",
475
- ".rs": "path",
476
- ".go": "path",
477
- ".swift": "path",
478
- ".java": "path",
479
- ".rb": "path",
480
- // Test files → path scope
481
- ".test.ts": "path",
482
- ".spec.ts": "path",
483
- ".test.js": "path",
484
- ".spec.js": "path"
485
- };
486
- function inferScope(correctionText, contextPath, repoPath, context = {}) {
487
- const markerHaystack = `${context.original_text ?? ""} ${correctionText}`;
488
- for (const marker of SCOPE_MARKERS) {
489
- if (marker.pattern.test(markerHaystack)) {
490
- return {
491
- scope: marker.scope,
492
- path_scope: marker.scope === "path" && contextPath ? inferPathScope(contextPath) : null,
493
- confidence_modifier: 0.1,
494
- // boost for explicit markers
495
- reason: marker.reason
496
- };
497
- }
498
- }
499
- for (const indicator of FRAMEWORK_INDICATORS) {
500
- if (indicator.test(correctionText)) {
501
- return {
502
- scope: "repo",
503
- path_scope: null,
504
- confidence_modifier: 0.05,
505
- reason: "language/framework reference implies repo scope"
506
- };
507
- }
508
- }
509
- if (contextPath) {
510
- const ext = extname(contextPath);
511
- const base = basename(contextPath);
512
- if (base.includes(".test.") || base.includes(".spec.") || contextPath.includes("__tests__") || contextPath.includes("/test/")) {
513
- return {
514
- scope: "path",
515
- path_scope: inferPathScope(contextPath),
516
- confidence_modifier: 0,
517
- reason: "test file context \u2192 path scope"
518
- };
519
- }
520
- if (FILE_TYPE_SCOPES[ext] === "repo" || base === "package.json" || base === "tsconfig.json" || base === "Makefile") {
521
- return {
522
- scope: "repo",
523
- path_scope: null,
524
- confidence_modifier: 0.05,
525
- reason: "config file context \u2192 repo scope"
526
- };
527
- }
528
- if (FILE_TYPE_SCOPES[ext] === "path") {
529
- return {
530
- scope: "path",
531
- path_scope: inferPathScope(contextPath),
532
- confidence_modifier: 0,
533
- reason: "source file context \u2192 directory scope"
534
- };
535
- }
536
- }
537
- const toolScope = inferFromRecentTools(context.recent_tool_calls);
538
- if (toolScope) {
539
- return toolScope;
540
- }
541
- const assistantScope = inferFromAssistantTurn(context.prev_assistant_turn);
542
- if (assistantScope) {
543
- return assistantScope;
544
- }
545
- if (hasSpecificFileReference(correctionText)) {
546
- return {
547
- scope: "path",
548
- path_scope: extractPathFromText(correctionText),
549
- confidence_modifier: 0,
550
- reason: "specific file/path reference in correction"
551
- };
552
- }
553
- if (contextPath && repoPath) {
554
- const ownerScope = inferFromGitOwnership(contextPath, repoPath);
555
- if (ownerScope) return ownerScope;
556
- }
557
- return {
558
- scope: "repo",
559
- path_scope: null,
560
- confidence_modifier: 0,
561
- reason: "default: no specific scope signals detected"
562
- };
563
- }
564
- function inferPathScope(filePath) {
565
- const dir = dirname(filePath);
566
- return `${dir}/**`;
567
- }
568
- function hasSpecificFileReference(text) {
569
- return /\b[\w-]+\.(ts|js|py|rs|go|swift|java|rb|tsx|jsx|vue|svelte)\b/.test(text) || /\b(src|lib|app|components|utils|test|spec)\//.test(text);
570
- }
571
- function extractPathFromText(text) {
572
- const pathMatch = text.match(
573
- /\b((?:src|lib|app|components|utils|test|spec)\/[\w/.-]+)/
574
- );
575
- if (pathMatch) return `${pathMatch[1]}**`;
576
- const dirMatch = text.match(
577
- /\b((?:src|lib|app|components|utils|test|spec)\/[\w/-]*)/
578
- );
579
- if (dirMatch) return `${dirMatch[1]}/**`;
580
- return null;
581
- }
582
- var SOURCE_AWARE_TOOLS = /* @__PURE__ */ new Set([
583
- "Read",
584
- "Edit",
585
- "Write",
586
- "MultiEdit",
587
- "NotebookEdit",
588
- "NotebookRead",
589
- "Grep",
590
- "Glob"
591
- ]);
592
- function isPathInsideRepo(path) {
593
- if (path.startsWith("/")) return false;
594
- if (/^(?:Applications|app|usr|opt|System|Library|private|var|tmp)\//.test(path)) return false;
595
- return true;
596
- }
597
- function inferFromRecentTools(toolCalls) {
598
- if (!toolCalls || toolCalls.length === 0) return null;
599
- for (const toolCall of toolCalls) {
600
- if (toolCall.path && SOURCE_AWARE_TOOLS.has(toolCall.name) && isPathInsideRepo(toolCall.path)) {
601
- return {
602
- scope: "path",
603
- path_scope: inferPathScope(toolCall.path),
604
- confidence_modifier: 0.05,
605
- reason: `recent tool path context: ${toolCall.path}`
606
- };
607
- }
608
- const inferredPath = extractPathFromText(toolCall.input_summary ?? "");
609
- if (inferredPath) {
610
- return {
611
- scope: "path",
612
- path_scope: inferredPath,
613
- confidence_modifier: 0.05,
614
- reason: "recent tool summary implies path scope"
615
- };
616
- }
617
- const summary = toolCall.input_summary ?? "";
618
- if (/\b(pytest|vitest|jest|cargo test|go test)\b/i.test(summary)) {
619
- return {
620
- scope: "repo",
621
- path_scope: null,
622
- confidence_modifier: 0.04,
623
- reason: "recent test/tool pattern implies repo scope"
624
- };
625
- }
626
- }
627
- return null;
628
- }
629
- function inferFromAssistantTurn(assistantTurn) {
630
- if (!assistantTurn) return null;
631
- const inferredPath = extractPathFromText(assistantTurn);
632
- if (inferredPath) {
633
- return {
634
- scope: "path",
635
- path_scope: inferredPath,
636
- confidence_modifier: 0.05,
637
- reason: "previous assistant turn referenced a path"
638
- };
639
- }
640
- for (const indicator of FRAMEWORK_INDICATORS) {
641
- if (indicator.test(assistantTurn)) {
642
- return {
643
- scope: "repo",
644
- path_scope: null,
645
- confidence_modifier: 0.03,
646
- reason: "previous assistant turn referenced repo-level tooling/framework"
647
- };
648
- }
649
- }
650
- return null;
651
- }
652
- function inferFromGitOwnership(filePath, repoPath) {
653
- try {
654
- const codeowners = execSync(
655
- `cat .github/CODEOWNERS 2>/dev/null || cat CODEOWNERS 2>/dev/null || echo ""`,
656
- { cwd: repoPath, encoding: "utf-8" }
657
- ).trim();
658
- if (codeowners) {
659
- const dir = dirname(filePath);
660
- for (const line of codeowners.split("\n")) {
661
- if (line.startsWith("#") || !line.trim()) continue;
662
- const parts = line.trim().split(/\s+/);
663
- if (parts.length >= 2 && filePath.includes(parts[0].replace("*", ""))) {
664
- return {
665
- scope: "path",
666
- path_scope: `${parts[0]}`,
667
- confidence_modifier: 0.05,
668
- reason: `CODEOWNERS match: ${parts[0]} \u2192 ${parts.slice(1).join(", ")}`
669
- };
670
- }
671
- }
672
- }
673
- } catch {
674
- }
675
- return null;
676
- }
677
-
678
- // src/capture/correction.ts
679
- var NEGATION_REPLACEMENT = /\b(?:not|don't|do not|never|stop)\s+(?:use|do|run|call|import)\s+(.+?)[\s,;.]+(?:use|do|run|call|import|instead)\s+(.+)/i;
680
- var EXPLICIT_RULE = /\b(always|never|must|required|forbidden|don't ever)\b\s+(.+)/i;
681
- var WHEN_DO_RULE = /\b(?:whenever|each time|every time|when(?:ever)?)\s+(?:i|you|we)\s+(say|use|ask|mention|do|run)\s+(.+?)[,.]?\s+(?:we|you|i|please|always|just)?\s*(do|run|use|please|add|make|update|commit|push|backup|back up|sync|verify|check|ensure)\s+(.+)/i;
682
- var REVIEW_FEEDBACK = /\b(?:review|reviewer|PR feedback|code review)\s+(?:said|says|asked|wants|requires|flagged)\s+(.+)/i;
683
- var SOFT_PREFERENCE = /\b(?:we|I|the team|this repo)\s+(?:prefer|usually use|tend to use|lean on|default to|use)\s+(.+?)(?:\s+(?:instead of|not|over)\s+(.+))?$/i;
684
- var SOFT_DECISION = /\b(?:let's|lets|let us|we should|we'll|we will|we can|use)\s+(?:use|keep|follow|stick with|go with)\s+(.+?)(?:\s+(?:instead of|over)\s+(.+))?(?:[.!]|$)/i;
685
- var CONFIG_BACKED_DECISION = /\b(?:editorconfig|prettier|eslint|tsconfig|package\.json|ci|workflow|this repo)\b.*\b(?:says|uses|wants|defaults to|is configured for)\s+(.+)/i;
686
- var QUESTION_ONLY = /^\s*(?:should|could|would|can|do)\b.*\?\s*$/i;
687
- var DESCRIPTIVE_MODAL_RE = /\b(?:i|you|we|they|those|that|which|who)(?:\s+\w+){0,2}\s+(?:always|never|must|don't|do not|prefer|required|forbidden)\b/i;
688
- var DESTRUCTIVE_VERB_RE = /\b(?:remove|delete|drop|wipe|clear|purge|erase|nuke|truncate|reset|destroy)\b/i;
689
- var HIGH_RISK_TARGET_RE = /\b(?:plugin|plugins|setting|settings|config|configs|configuration|file|files|folder|folders|directory|directories|memor(?:y|ies)|database|db|repo|repos|repository|branch|branches|commit|commits|history|backup|backups|secret|secrets|credential|credentials|key|keys|token|tokens)\b/i;
690
- function isDestructiveRisky(text) {
691
- return DESTRUCTIVE_VERB_RE.test(text) && HIGH_RISK_TARGET_RE.test(text);
692
- }
693
- var TRIGGER_TEMPLATE_RE = /^\s*when(?:ever)?\s+(?:the\s+)?user\s+(?:says|asks|writes|types|mentions|uses|requests)\b/i;
694
- function isTriggerTemplateRule(text) {
695
- return TRIGGER_TEMPLATE_RE.test(text);
696
- }
697
- function isHighRiskRule(text) {
698
- return isDestructiveRisky(text) || isTriggerTemplateRule(text);
699
- }
700
- function detectCorrections(text) {
701
- const normalizedText = text.trim();
702
- if (QUESTION_ONLY.test(normalizedText)) return [];
703
- if (looksLikePastedTranscript(normalizedText)) return [];
704
- const matches = [];
705
- const segments = correctionCandidateSegments(normalizedText);
706
- for (const segment of segments) {
707
- const descriptive = DESCRIPTIVE_MODAL_RE.exec(segment);
708
- if (descriptive && descriptive.index > 0) continue;
709
- const whenDo = segment.match(WHEN_DO_RULE);
710
- if (whenDo) {
711
- const trigger = stripTrailingPunctuation(whenDo[2]);
712
- const action = stripTrailingPunctuation(`${whenDo[3]} ${whenDo[4]}`);
713
- matches.push({
714
- type: "rule",
715
- text: `When user ${whenDo[1].toLowerCase()}s "${trigger}", ${action}.`,
716
- confidence: 0.5,
717
- original: segment
718
- });
719
- continue;
720
- }
721
- const negMatch = segment.match(NEGATION_REPLACEMENT);
722
- if (negMatch) {
723
- matches.push({
724
- type: "rule",
725
- text: `Do not use ${negMatch[1].trim()}. Use ${negMatch[2].trim()} instead.`,
726
- confidence: 0.45,
727
- original: segment
728
- });
729
- continue;
730
- }
731
- const reviewMatch = segment.match(REVIEW_FEEDBACK);
732
- if (reviewMatch) {
733
- matches.push({
734
- type: "review_pattern",
735
- text: reviewMatch[1].trim(),
736
- confidence: 0.55
737
- // stronger — review feedback
738
- });
739
- continue;
740
- }
741
- const ruleMatch = segment.match(EXPLICIT_RULE);
742
- if (ruleMatch) {
743
- matches.push({
744
- type: "rule",
745
- text: `${ruleMatch[1]} ${ruleMatch[2].trim()}`,
746
- confidence: 0.5
747
- });
748
- continue;
749
- }
750
- const decisionMatch = segment.match(SOFT_DECISION);
751
- if (decisionMatch && isDurableDecision(segment, decisionMatch[1], decisionMatch[2])) {
752
- const decision = decisionMatch[2] ? `Prefer ${decisionMatch[1].trim()} over ${decisionMatch[2].trim()}` : `Use ${stripTrailingPunctuation(decisionMatch[1])}`;
753
- matches.push({
754
- type: "decision",
755
- text: ensureSentence(decision),
756
- confidence: 0.38
757
- });
758
- continue;
759
- }
760
- const prefMatch = segment.match(SOFT_PREFERENCE);
761
- if (prefMatch && isDurableDecision(segment, prefMatch[1], prefMatch[2])) {
762
- const pref = prefMatch[2] ? `Prefer ${prefMatch[1].trim()} over ${prefMatch[2].trim()}` : `Prefer ${stripTrailingPunctuation(prefMatch[1])}`;
763
- matches.push({
764
- type: "decision",
765
- text: ensureSentence(pref),
766
- confidence: 0.36
767
- });
768
- continue;
769
- }
770
- const configMatch = segment.match(CONFIG_BACKED_DECISION);
771
- if (configMatch) {
772
- matches.push({
773
- type: "decision",
774
- text: ensureSentence(`Follow configured repo convention: ${stripTrailingPunctuation(configMatch[1])}`),
775
- confidence: 0.42
776
- });
777
- }
778
- }
779
- return matches;
780
- }
781
- function stripTrailingPunctuation(text) {
782
- return text.trim().replace(/[.?!,:;]+$/, "");
783
- }
784
- function ensureSentence(text) {
785
- const cleaned = stripTrailingPunctuation(text);
786
- return cleaned.endsWith(".") ? cleaned : `${cleaned}.`;
787
- }
788
- var TRANSCRIPT_MARKERS = [
789
- "\u203B recap:",
790
- "\u273B",
791
- "\u23FA",
792
- "\u23BF",
793
- "\u276F",
794
- "Bash(",
795
- "Hook activity",
796
- "Top reused memories",
797
- "RECENT INJECTIONS",
798
- "BREAKDOWN BY TYPE",
799
- "sqlite3"
800
- ];
801
- var DURABLE_DECISION_HINT = /\b(repo|repository|project|default|defaults|convention(?:al|s)?|configured|config|editorconfig|prettier|eslint|tsconfig|package\.json|ci|workflow|style|pattern|architecture|runtime|database|sqlite|pnpm|yarn|npm|uv|pytest|vitest)\b/i;
802
- var TRANSCRIPT_LINE_RE = /^(?:[⏺⎿❯✻※]|(?:Bash|Edit|Write|Read|Grep|Glob|Task|TodoWrite)\(|\s*(?:│|├|┌|└|─)|\s*…|\s*={3,})/u;
803
- function looksLikePastedTranscript(text) {
804
- if (text.length < 1200) return false;
805
- const markerCount = TRANSCRIPT_MARKERS.reduce(
806
- (total, marker) => total + (text.includes(marker) ? 1 : 0),
807
- 0
808
- );
809
- return markerCount >= 2;
810
- }
811
- function correctionCandidateSegments(text) {
812
- const lines = text.split(/\r?\n/);
813
- const singleLine = lines.length === 1;
814
- const segments = singleLine ? [text] : lines.map((line) => line.trim()).filter(Boolean);
815
- return segments.map(stripListPrefix).filter((line) => line.length >= 8 && line.length <= 500).filter((line) => !TRANSCRIPT_LINE_RE.test(line)).filter((line) => !line.startsWith("```"));
816
- }
817
- function stripListPrefix(text) {
818
- return text.replace(/^\s*(?:[-*]|\d+[.)])\s+/, "").trim();
819
- }
820
- function isDurableDecision(segment, first, second) {
821
- const haystack = `${segment} ${first} ${second ?? ""}`;
822
- if (/\b(?:instead of|over)\b/i.test(haystack)) return true;
823
- return DURABLE_DECISION_HINT.test(haystack);
824
- }
825
- async function processCorrection(db, text, ctx) {
826
- const corrections = detectCorrections(text);
827
- if (corrections.length === 0) return [];
828
- const profile = getRepoQualityProfile(db, ctx.repo);
829
- const ids = [];
830
- const captureContext = buildCaptureContext(ctx);
831
- for (const correction of corrections) {
832
- if (correction.type !== "review_pattern") {
833
- const reasons = qualityReasons(correction.text);
834
- if (reasons.length > 0) continue;
835
- }
836
- if (await isSimilarToRejectedFragmentSemantic(db, correction.text)) continue;
837
- const evidence = correction.type === "review_pattern" ? {
838
- type: "review_feedback",
839
- reported_by_user: true,
840
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
841
- context: text
842
- } : {
843
- type: "session_correction",
844
- session: ctx.sessionId,
845
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
846
- context: text
847
- };
848
- const duplicate = await findDuplicateMemory(
849
- db,
850
- ctx.repo,
851
- correction.type,
852
- correction.text,
853
- profile.dedup_similarity_threshold
854
- );
855
- if (duplicate) {
856
- const before = getMemory(db, duplicate.id);
857
- appendEvidence(db, duplicate.id, evidence);
858
- if (captureContext) {
859
- updateMemoryCaptureContext(db, duplicate.id, captureContext);
860
- }
861
- if (before && !before.evidence.some((entry) => entry.type === "session_correction" && entry.session === ctx.sessionId)) {
862
- incrementMemoryRepetition(db, duplicate.id);
863
- }
864
- const updated = getMemory(db, duplicate.id);
865
- if (updated && updated.status !== "active" && !isHighRiskRule(updated.text) && countDistinctCorrectionSessions(updated) >= profile.repeat_sessions_required) {
866
- promoteMemory(db, duplicate.id, "repeat_correction");
867
- const after = getMemory(db, duplicate.id);
868
- recordAuditWithSnapshot(
869
- db,
870
- duplicate.id,
871
- "promoted",
872
- "system",
873
- `repetition:${after?.repetition_count ?? updated.repetition_count}`,
874
- before ?? null,
875
- after ?? null
876
- );
877
- }
878
- ids.push(duplicate.id);
879
- continue;
880
- }
881
- const inferredScope = inferScope(
882
- correction.text,
883
- ctx.path,
884
- void 0,
885
- {
886
- prev_assistant_turn: ctx.prev_assistant_turn,
887
- recent_tool_calls: ctx.recent_tool_calls,
888
- original_text: text
889
- }
890
- );
891
- const input = {
892
- type: correction.type,
893
- text: correction.text,
894
- scope: inferredScope.scope,
895
- path_scope: inferredScope.path_scope,
896
- repo: ctx.repo ?? null,
897
- source: correction.type === "review_pattern" ? "user_reported_review" : "user_correction",
898
- confidence: seedCandidateConfidence(
899
- Math.min(1, correction.confidence + inferredScope.confidence_modifier),
900
- profile
901
- ),
902
- evidence: [evidence],
903
- capture_context: captureContext
904
- };
905
- const id = createMemory(db, input);
906
- maybePromoteGroupCandidate(db, id);
907
- enqueueVerifyCapture(db, {
908
- id,
909
- text: input.text,
910
- scope: input.scope,
911
- path_scope: input.path_scope ?? null,
912
- repo: input.repo ?? null,
913
- capture_context: captureContext ?? null
914
- });
915
- ids.push(id);
916
- }
917
- return ids;
918
- }
919
- function maybePromoteGroupCandidate(db, candidateId) {
920
- const candidate = getMemory(db, candidateId);
921
- if (!candidate || candidate.status !== "candidate") return;
922
- if (isHighRiskRule(candidate.text)) return;
923
- const followedCount = queryMemories(db, {
924
- repo: candidate.repo ?? void 0,
925
- type: candidate.type,
926
- scope: candidate.scope
927
- }).filter((memory) => memory.id !== candidate.id).reduce((total, memory) => total + getMemoryFeedback(db, memory.id).filter((entry) => entry.outcome === "followed").length, 0);
928
- if (followedCount < 3) return;
929
- const before = candidate;
930
- promoteMemory(db, candidate.id, "repeat_correction");
931
- const after = getMemory(db, candidate.id);
932
- recordAuditWithSnapshot(
933
- db,
934
- candidate.id,
935
- "promoted",
936
- "system",
937
- `repetition:group_followed:${followedCount}`,
938
- before,
939
- after ?? null
940
- );
941
- }
942
- function buildCaptureContext(ctx) {
943
- const recentToolCalls = (ctx.recent_tool_calls ?? []).slice(-5).map((toolCall) => ({
944
- name: toolCall.name,
945
- path: toolCall.path ?? extractContextPath(toolCall.input_summary),
946
- exit_code: toolCall.exit_code
947
- }));
948
- const hasContext = Boolean(ctx.prev_assistant_turn) || recentToolCalls.length > 0 || Boolean(ctx.repo) || Boolean(ctx.path) || Boolean(ctx.agent);
949
- if (!hasContext) return null;
950
- return {
951
- prev_assistant_text: ctx.prev_assistant_turn,
952
- recent_tool_calls: recentToolCalls,
953
- repo: ctx.repo ?? null,
954
- path: ctx.path ?? null,
955
- agent: ctx.agent
956
- };
957
- }
958
- function extractContextPath(text) {
959
- if (!text) return void 0;
960
- const match = text.match(
961
- /\b((?:src|lib|app|components|utils|test|spec)\/[\w./-]+|[\w./-]+\.(?:ts|tsx|js|jsx|py|rs|go|swift|java|rb|json|toml|ya?ml))\b/
962
- );
963
- return match?.[1];
964
- }
965
- async function processReviewFeedback(db, feedback, ctx) {
966
- const profile = getRepoQualityProfile(db, ctx.repo);
967
- const evidence = {
968
- type: "review_feedback",
969
- reported_by_user: true,
970
- reviewer: ctx.reviewer,
971
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
972
- context: feedback
973
- };
974
- const corrections = detectCorrections(feedback);
975
- if (corrections.length > 0) {
976
- const ids = [];
977
- for (const correction of corrections) {
978
- const duplicate = await findDuplicateMemory(
979
- db,
980
- ctx.repo,
981
- correction.type,
982
- correction.text,
983
- profile.dedup_similarity_threshold
984
- );
985
- if (duplicate) {
986
- appendEvidence(db, duplicate.id, evidence);
987
- const updated = getMemory(db, duplicate.id);
988
- if (updated && updated.status !== "active" && countDistinctCorrectionSessions(updated) >= Math.max(1, profile.repeat_sessions_required - 1)) {
989
- promoteMemory(db, duplicate.id, "review_feedback");
990
- }
991
- ids.push(duplicate.id);
992
- continue;
993
- }
994
- const id2 = createMemory(db, {
995
- type: correction.type,
996
- text: correction.text,
997
- scope: ctx.path ? "path" : "repo",
998
- path_scope: ctx.path ?? null,
999
- repo: ctx.repo ?? null,
1000
- source: "user_reported_review",
1001
- confidence: seedCandidateConfidence(correction.confidence + 0.1, profile),
1002
- evidence: [evidence]
1003
- });
1004
- ids.push(id2);
1005
- }
1006
- return ids;
1007
- }
1008
- const id = createMemory(db, {
1009
- type: "review_pattern",
1010
- text: feedback,
1011
- scope: ctx.path ? "path" : "repo",
1012
- path_scope: ctx.path ?? null,
1013
- repo: ctx.repo ?? null,
1014
- source: "user_reported_review",
1015
- confidence: seedCandidateConfidence(0.4, profile),
1016
- evidence: [evidence]
1017
- });
1018
- return [id];
1019
- }
1020
- function textSimilarity(a, b) {
1021
- const wordsA = new Set(a.toLowerCase().split(/\s+/));
1022
- const wordsB = new Set(b.toLowerCase().split(/\s+/));
1023
- const intersection = [...wordsA].filter((w) => wordsB.has(w));
1024
- const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
1025
- return intersection.length / union.size;
1026
- }
1027
- var REJECTED_EXEMPLAR_THRESHOLD = 0.7;
1028
- var REJECTED_EXEMPLAR_SEMANTIC_THRESHOLD = 0.85;
1029
- function isSimilarToRejectedFragment(db, text, threshold = REJECTED_EXEMPLAR_THRESHOLD) {
1030
- const rejected = queryMemories(db, { status: "rejected" }).filter((m) => m.source === "user_correction" || m.source === "user_reported_review");
1031
- for (const exemplar of rejected) {
1032
- if (textSimilarity(text, exemplar.text) >= threshold) return true;
1033
- }
1034
- return false;
1035
- }
1036
- async function isSimilarToRejectedFragmentSemantic(db, text, options = {}) {
1037
- const lexicalT = options.lexicalThreshold ?? REJECTED_EXEMPLAR_THRESHOLD;
1038
- const semanticT = options.semanticThreshold ?? REJECTED_EXEMPLAR_SEMANTIC_THRESHOLD;
1039
- if (isSimilarToRejectedFragment(db, text, lexicalT)) return true;
1040
- const config = loadEmbeddingConfigFromEnv();
1041
- if (!config) return false;
1042
- const match = await findSimilarRejectedExemplar(db, text, config, semanticT);
1043
- return match != null;
1044
- }
1045
- async function findDuplicateMemory(db, repo, type, text, threshold) {
1046
- if (!repo) return void 0;
1047
- const existing = queryMemories(db, { repo }).filter((m) => m.status !== "rejected" && m.type === type);
1048
- let best;
1049
- let bestScore = 0;
1050
- for (const memory of existing) {
1051
- const score = textSimilarity(memory.text, text);
1052
- if (score >= threshold && score > bestScore) {
1053
- best = memory;
1054
- bestScore = score;
1055
- }
1056
- }
1057
- if (best) return best;
1058
- const config = loadEmbeddingConfigFromEnv();
1059
- if (!config) return void 0;
1060
- const semantic = await findSemanticDuplicates(
1061
- db,
1062
- text,
1063
- config,
1064
- threshold,
1065
- { repo, type, limit: 1 }
1066
- );
1067
- return semantic[0] ? getMemory(db, semantic[0].id) : void 0;
1068
- }
1069
-
1070
- // src/maintenance/cleanup.ts
1071
- var SUPPRESS_INJECTION_FLOOR = 50;
1072
- var GLOBALIZE_REPO_FLOOR = 3;
1073
- var DEFAULT_ACTOR = "maintenance:cleanup";
1074
- function runDeterministicCleanup(db, opts = { dryRun: true }) {
1075
- const runId = randomUUID2();
1076
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1077
- const plan = [];
1078
- if (!opts.only || opts.only === "dedupe_exact_merge") {
1079
- plan.push(...planDedupeExact(db));
1080
- }
1081
- if (!opts.only || opts.only === "reject_fragment_candidate") {
1082
- plan.push(...planRejectFragments(db));
1083
- }
1084
- if (!opts.only || opts.only === "promote_repeat_correction") {
1085
- plan.push(...planPromoteRepeats(db));
1086
- }
1087
- if (!opts.only || opts.only === "suppress_unproductive_command") {
1088
- plan.push(...planSuppressCommands(db));
1089
- }
1090
- if (!opts.only || opts.only === "globalize_cross_repo") {
1091
- plan.push(...planGlobalizeCrossRepo(db));
1092
- }
1093
- const counts = summarize(plan);
1094
- if (!opts.dryRun) {
1095
- for (const item of plan) {
1096
- switch (item.kind) {
1097
- case "dedupe_exact_merge":
1098
- applyDedupeExact(db, runId, item);
1099
- break;
1100
- case "reject_fragment_candidate":
1101
- applyRejectFragment(db, runId, item);
1102
- break;
1103
- case "promote_repeat_correction":
1104
- applyPromoteRepeat(db, runId, item);
1105
- break;
1106
- case "suppress_unproductive_command":
1107
- applySuppressCommand(db, runId, item);
1108
- break;
1109
- case "globalize_cross_repo":
1110
- applyGlobalizeCrossRepo(db, runId, item);
1111
- break;
1112
- }
1113
- }
1114
- }
1115
- return {
1116
- run_id: runId,
1117
- dry_run: opts.dryRun,
1118
- started_at: startedAt,
1119
- finished_at: (/* @__PURE__ */ new Date()).toISOString(),
1120
- counts,
1121
- plan
1122
- };
1123
- }
1124
- function summarize(plan) {
1125
- let dedupeClusters = 0;
1126
- let dedupeLosers = 0;
1127
- let fragmentRejections = 0;
1128
- let repeatPromotions = 0;
1129
- let commandSuppressions = 0;
1130
- let globalizations = 0;
1131
- let globalizeLosers = 0;
1132
- for (const p of plan) {
1133
- if (p.kind === "dedupe_exact_merge") {
1134
- dedupeClusters += 1;
1135
- dedupeLosers += p.loser_ids.length;
1136
- } else if (p.kind === "reject_fragment_candidate") {
1137
- fragmentRejections += 1;
1138
- } else if (p.kind === "promote_repeat_correction") {
1139
- repeatPromotions += 1;
1140
- } else if (p.kind === "suppress_unproductive_command") {
1141
- commandSuppressions += 1;
1142
- } else {
1143
- globalizations += 1;
1144
- globalizeLosers += p.loser_ids.length;
1145
- }
1146
- }
1147
- return {
1148
- dedupe_clusters: dedupeClusters,
1149
- dedupe_losers: dedupeLosers,
1150
- fragment_rejections: fragmentRejections,
1151
- repeat_promotions: repeatPromotions,
1152
- command_suppressions: commandSuppressions,
1153
- globalizations,
1154
- globalize_losers: globalizeLosers
1155
- };
1156
- }
1157
- function normalizeText(text) {
1158
- return text.toLowerCase().replace(/\s+/g, " ").replace(/[\s.;:,!?`]+$/g, "").trim();
1159
- }
1160
- function scopeKey(row) {
1161
- return [row.type, row.scope, row.repo ?? "", row.path_scope ?? "", row.norm].join("\0");
1162
- }
1163
- function planDedupeExact(db) {
1164
- const rows = db.select({
1165
- id: memories.id,
1166
- type: memories.type,
1167
- text: memories.text,
1168
- scope: memories.scope,
1169
- repo: memories.repo,
1170
- path_scope: memories.path_scope,
1171
- status: memories.status,
1172
- injection_count: memories.injection_count,
1173
- confidence: memories.confidence,
1174
- created_at: memories.created_at
1175
- }).from(memories).where(inArray(memories.status, ["active", "candidate"])).all();
1176
- const groups = /* @__PURE__ */ new Map();
1177
- for (const row of rows) {
1178
- const norm = normalizeText(row.text);
1179
- if (!norm) continue;
1180
- const key = scopeKey({ ...row, norm });
1181
- const list = groups.get(key) ?? [];
1182
- list.push(row);
1183
- groups.set(key, list);
1184
- }
1185
- const plans = [];
1186
- for (const [key, list] of groups) {
1187
- if (list.length < 2) continue;
1188
- const sorted = [...list].sort((a, b) => {
1189
- const statusRank = (s) => s === "active" ? 0 : 1;
1190
- const dStatus = statusRank(a.status) - statusRank(b.status);
1191
- if (dStatus !== 0) return dStatus;
1192
- if (a.injection_count !== b.injection_count) return b.injection_count - a.injection_count;
1193
- if (a.confidence !== b.confidence) return b.confidence - a.confidence;
1194
- return a.created_at.localeCompare(b.created_at);
1195
- });
1196
- const winner = sorted[0];
1197
- const losers = sorted.slice(1);
1198
- const total = list.reduce((acc, r) => acc + r.injection_count, 0);
1199
- plans.push({
1200
- kind: "dedupe_exact_merge",
1201
- winner_id: winner.id,
1202
- winner_text: winner.text,
1203
- loser_ids: losers.map((l) => l.id),
1204
- scope_key: key,
1205
- total_injection_count: total
1206
- });
1207
- }
1208
- return plans;
1209
- }
1210
- function applyDedupeExact(db, runId, plan) {
1211
- const winner = getMemory(db, plan.winner_id);
1212
- if (!winner) return;
1213
- const losers = plan.loser_ids.map((id) => getMemory(db, id)).filter((m) => m != null);
1214
- if (losers.length === 0) return;
1215
- const sumCounts = losers.reduce(
1216
- (acc, l) => ({
1217
- injection: acc.injection + l.injection_count,
1218
- override: acc.override + l.override_count,
1219
- repetition: acc.repetition + l.repetition_count
1220
- }),
1221
- { injection: 0, override: 0, repetition: 0 }
1222
- );
1223
- const now = (/* @__PURE__ */ new Date()).toISOString();
1224
- for (const loser of losers) {
1225
- db.update(feedbackEvents).set({ memory_id: winner.id }).where(eq4(feedbackEvents.memory_id, loser.id)).run();
1226
- const loserInj = db.select().from(memoryInjections).where(eq4(memoryInjections.memory_id, loser.id)).all();
1227
- for (const inj of loserInj) {
1228
- const collision = db.select({ id: memoryInjections.id }).from(memoryInjections).where(and2(eq4(memoryInjections.memory_id, winner.id), eq4(memoryInjections.session_id, inj.session_id))).get();
1229
- if (collision) {
1230
- db.delete(memoryInjections).where(eq4(memoryInjections.id, inj.id)).run();
1231
- } else {
1232
- db.update(memoryInjections).set({ memory_id: winner.id }).where(eq4(memoryInjections.id, inj.id)).run();
1233
- }
1234
- }
1235
- }
1236
- db.update(memories).set({
1237
- injection_count: winner.injection_count + sumCounts.injection,
1238
- override_count: winner.override_count + sumCounts.override,
1239
- repetition_count: winner.repetition_count + sumCounts.repetition,
1240
- updated_at: now
1241
- }).where(eq4(memories.id, winner.id)).run();
1242
- for (const loser of losers) {
1243
- db.update(memories).set({ status: "rejected", supersedes: winner.id, dedupe_key: null, updated_at: now }).where(eq4(memories.id, loser.id)).run();
1244
- const after = getMemory(db, loser.id);
1245
- recordAuditWithSnapshot(
1246
- db,
1247
- loser.id,
1248
- "rejected",
1249
- DEFAULT_ACTOR,
1250
- `dedupe_exact:merged_into:${winner.id}:run:${runId}`,
1251
- loser,
1252
- after ?? null
1253
- );
1254
- db.insert(maintenanceCleanupLog).values({
1255
- id: randomUUID2(),
1256
- run_id: runId,
1257
- action: "dedupe_exact_merge",
1258
- memory_id: loser.id,
1259
- related_memory_id: winner.id,
1260
- before_snapshot: loser,
1261
- after_snapshot: after,
1262
- details: { scope_key: plan.scope_key, transferred_injection_count: loser.injection_count },
1263
- reverted: false,
1264
- reverted_at: null,
1265
- created_at: now
1266
- }).run();
1267
- }
1268
- }
1269
- var VERB_HINTS = [
1270
- "is",
1271
- "are",
1272
- "use",
1273
- "uses",
1274
- "used",
1275
- "run",
1276
- "runs",
1277
- "ran",
1278
- "must",
1279
- "never",
1280
- "always",
1281
- "do",
1282
- "don't",
1283
- "do not",
1284
- "should",
1285
- "avoid",
1286
- "prefer",
1287
- "keep",
1288
- "set",
1289
- "add",
1290
- "remove",
1291
- "skip",
1292
- "replace",
1293
- "fix",
1294
- "ensure",
1295
- "require",
1296
- "require ",
1297
- "make",
1298
- "build",
1299
- "test",
1300
- "deploy",
1301
- "install",
1302
- "import",
1303
- "export",
1304
- "commit",
1305
- "push",
1306
- "call",
1307
- "wrap",
1308
- "split",
1309
- "merge",
1310
- "store",
1311
- "load",
1312
- "save",
1313
- "ignore",
1314
- "accept",
1315
- "reject"
1316
- ];
1317
- var BARE_MODAL_RE = /^\s*(must|never|always|do not|don't|required|prefer|should)\b[^\w]*(stay|do|stop|go|reply|reply\?)?\s*$/i;
1318
- var TRAILING_QUESTION_RE = /\?\s*$/;
1319
- var DANGLING_CONNECTOR_RE = /\b(?:and|or|but|with|without|to|from|for|of|as|because|instead|over|the|a|an|on|in|at|by)\s*$/i;
1320
- var TRAILING_DOUBLE_DOT_RE = /\.{2,}\s*$/;
1321
- var RULE_FILLER_PREFIX_RE = /^\s*(?:always|never|must|don't|do not|prefer|required)\s+(?:just|now|uh|um|so|like|maybe|kinda|sort\s+of)\b/i;
1322
- var MAX_RULE_LENGTH = 300;
1323
- function planRejectFragments(db) {
1324
- const rows = db.select({
1325
- id: memories.id,
1326
- text: memories.text,
1327
- source: memories.source,
1328
- status: memories.status
1329
- }).from(memories).where(and2(eq4(memories.status, "candidate"), eq4(memories.source, "user_correction"))).all();
1330
- const out = [];
1331
- for (const row of rows) {
1332
- const reasons = qualityReasons(row.text);
1333
- if (reasons.length > 0) {
1334
- out.push({
1335
- kind: "reject_fragment_candidate",
1336
- memory_id: row.id,
1337
- text: row.text,
1338
- reasons
1339
- });
1340
- }
1341
- }
1342
- return out;
1343
- }
1344
- function qualityReasons(rawText) {
1345
- const text = rawText.trim();
1346
- const reasons = [];
1347
- if (text.length < 14) reasons.push("too_short");
1348
- if (text.length > MAX_RULE_LENGTH) reasons.push("too_long");
1349
- if (TRAILING_QUESTION_RE.test(text)) reasons.push("trailing_question");
1350
- if (BARE_MODAL_RE.test(text)) reasons.push("bare_modal");
1351
- if (TRAILING_DOUBLE_DOT_RE.test(text)) reasons.push("trailing_double_dot");
1352
- if (DANGLING_CONNECTOR_RE.test(text)) reasons.push("dangling_connector");
1353
- if (RULE_FILLER_PREFIX_RE.test(text)) reasons.push("filler_prefix");
1354
- const words = text.toLowerCase().replace(/[^\w' ]+/g, " ").split(/\s+/).filter(Boolean);
1355
- const hasVerb = words.some((w) => VERB_HINTS.includes(w));
1356
- if (!hasVerb) reasons.push("no_verb");
1357
- return reasons;
1358
- }
1359
- function applyRejectFragment(db, runId, plan) {
1360
- const before = getMemory(db, plan.memory_id);
1361
- if (!before || before.status !== "candidate") return;
1362
- const now = (/* @__PURE__ */ new Date()).toISOString();
1363
- db.update(memories).set({ status: "rejected", dedupe_key: null, updated_at: now }).where(eq4(memories.id, plan.memory_id)).run();
1364
- const after = getMemory(db, plan.memory_id);
1365
- recordAuditWithSnapshot(
1366
- db,
1367
- plan.memory_id,
1368
- "rejected",
1369
- DEFAULT_ACTOR,
1370
- `cleanup_fragment:${plan.reasons.join(",")}:run:${runId}`,
1371
- before,
1372
- after ?? null
1373
- );
1374
- db.insert(maintenanceCleanupLog).values({
1375
- id: randomUUID2(),
1376
- run_id: runId,
1377
- action: "reject_fragment_candidate",
1378
- memory_id: plan.memory_id,
1379
- related_memory_id: null,
1380
- before_snapshot: before,
1381
- after_snapshot: after,
1382
- details: { reasons: plan.reasons },
1383
- reverted: false,
1384
- reverted_at: null,
1385
- created_at: now
1386
- }).run();
1387
- }
1388
- function planPromoteRepeats(db) {
1389
- const rows = db.select({
1390
- id: memories.id,
1391
- text: memories.text,
1392
- repetition_count: memories.repetition_count,
1393
- source: memories.source,
1394
- status: memories.status
1395
- }).from(memories).where(and2(eq4(memories.status, "candidate"), eq4(memories.source, "user_correction"))).all();
1396
- const out = [];
1397
- for (const row of rows) {
1398
- const text = row.text.trim();
1399
- if (qualityReasons(text).length > 0) continue;
1400
- if (isDestructiveRisky(text)) continue;
1401
- if (row.repetition_count >= 2) {
1402
- out.push({ kind: "promote_repeat_correction", memory_id: row.id, text, matched_pattern: "repetition" });
1403
- }
1404
- }
1405
- return out;
1406
- }
1407
- function applyPromoteRepeat(db, runId, plan) {
1408
- const before = getMemory(db, plan.memory_id);
1409
- if (!before || before.status !== "candidate") return;
1410
- const now = (/* @__PURE__ */ new Date()).toISOString();
1411
- db.update(memories).set({ status: "active", confidence: Math.max(before.confidence, 0.7), updated_at: now, last_validated_at: now }).where(eq4(memories.id, plan.memory_id)).run();
1412
- const after = getMemory(db, plan.memory_id);
1413
- recordAuditWithSnapshot(
1414
- db,
1415
- plan.memory_id,
1416
- "promoted",
1417
- DEFAULT_ACTOR,
1418
- `cleanup_promote:${plan.matched_pattern}:run:${runId}`,
1419
- before,
1420
- after ?? null
1421
- );
1422
- db.insert(maintenanceCleanupLog).values({
1423
- id: randomUUID2(),
1424
- run_id: runId,
1425
- action: "promote_repeat_correction",
1426
- memory_id: plan.memory_id,
1427
- related_memory_id: null,
1428
- before_snapshot: before,
1429
- after_snapshot: after,
1430
- details: { matched_pattern: plan.matched_pattern },
1431
- reverted: false,
1432
- reverted_at: null,
1433
- created_at: now
1434
- }).run();
1435
- }
1436
- function planSuppressCommands(db) {
1437
- const candidates = db.select({
1438
- id: memories.id,
1439
- text: memories.text,
1440
- injection_count: memories.injection_count
1441
- }).from(memories).where(and2(
1442
- eq4(memories.status, "active"),
1443
- eq4(memories.type, "command"),
1444
- eq4(memories.auto_inject, true),
1445
- gte(memories.injection_count, SUPPRESS_INJECTION_FLOOR)
1446
- )).all();
1447
- const out = [];
1448
- for (const row of candidates) {
1449
- const followedRow = db.select({ n: sql`count(*)` }).from(feedbackEvents).where(and2(
1450
- eq4(feedbackEvents.memory_id, row.id),
1451
- eq4(feedbackEvents.outcome, "followed")
1452
- )).get();
1453
- const followed = followedRow?.n ?? 0;
1454
- if (followed > 0) continue;
1455
- out.push({
1456
- kind: "suppress_unproductive_command",
1457
- memory_id: row.id,
1458
- text: row.text,
1459
- injection_count: row.injection_count,
1460
- followed_count: followed
1461
- });
1462
- }
1463
- return out;
1464
- }
1465
- function applySuppressCommand(db, runId, plan) {
1466
- const before = getMemory(db, plan.memory_id);
1467
- if (!before || !before.auto_inject) return;
1468
- const now = (/* @__PURE__ */ new Date()).toISOString();
1469
- db.update(memories).set({ auto_inject: false, updated_at: now }).where(eq4(memories.id, plan.memory_id)).run();
1470
- const after = getMemory(db, plan.memory_id);
1471
- recordAuditWithSnapshot(
1472
- db,
1473
- plan.memory_id,
1474
- "demoted",
1475
- DEFAULT_ACTOR,
1476
- `cleanup_suppress_command:inj=${plan.injection_count},followed=0:run:${runId}`,
1477
- before,
1478
- after ?? null
1479
- );
1480
- db.insert(maintenanceCleanupLog).values({
1481
- id: randomUUID2(),
1482
- run_id: runId,
1483
- action: "suppress_unproductive_command",
1484
- memory_id: plan.memory_id,
1485
- related_memory_id: null,
1486
- before_snapshot: before,
1487
- after_snapshot: after,
1488
- details: {
1489
- injection_count: plan.injection_count,
1490
- followed_count: plan.followed_count
1491
- },
1492
- reverted: false,
1493
- reverted_at: null,
1494
- created_at: now
1495
- }).run();
1496
- }
1497
- function planGlobalizeCrossRepo(db) {
1498
- const rows = db.select({
1499
- id: memories.id,
1500
- type: memories.type,
1501
- text: memories.text,
1502
- scope: memories.scope,
1503
- repo: memories.repo,
1504
- injection_count: memories.injection_count,
1505
- confidence: memories.confidence,
1506
- created_at: memories.created_at
1507
- }).from(memories).where(and2(eq4(memories.status, "active"), inArray(memories.type, ["command", "rule"]))).all();
1508
- const groups = /* @__PURE__ */ new Map();
1509
- for (const row of rows) {
1510
- if (row.scope === "global") continue;
1511
- if (!row.repo) continue;
1512
- const norm = normalizeText(row.text);
1513
- if (!norm) continue;
1514
- const key = `${row.type}::${norm}`;
1515
- const list = groups.get(key) ?? [];
1516
- list.push(row);
1517
- groups.set(key, list);
1518
- }
1519
- const allActive = queryMemories(db, { status: "active" }).filter((m) => m.type === "rule" || m.type === "command");
1520
- const plans = [];
1521
- for (const list of groups.values()) {
1522
- const repos = new Set(list.map((r) => r.repo));
1523
- if (repos.size < GLOBALIZE_REPO_FLOOR) continue;
1524
- const sorted = [...list].sort((a, b) => {
1525
- if (a.injection_count !== b.injection_count) return b.injection_count - a.injection_count;
1526
- if (a.confidence !== b.confidence) return b.confidence - a.confidence;
1527
- return a.created_at.localeCompare(b.created_at);
1528
- });
1529
- const winner = sorted[0];
1530
- const winnerMemory = getMemory(db, winner.id);
1531
- if (!winnerMemory) continue;
1532
- const clusterIds = new Set(list.map((r) => r.id));
1533
- const conflict = allActive.find((other) => {
1534
- if (clusterIds.has(other.id)) return false;
1535
- if (!other.repo || repos.has(other.repo)) return false;
1536
- const winnerAsGlobal = { ...winnerMemory, scope: "global" };
1537
- return checkContradiction(winnerAsGlobal, other) != null;
1538
- });
1539
- if (conflict) continue;
1540
- const losers = sorted.slice(1);
1541
- const total = list.reduce((acc, r) => acc + r.injection_count, 0);
1542
- plans.push({
1543
- kind: "globalize_cross_repo",
1544
- winner_id: winner.id,
1545
- winner_text: winner.text,
1546
- loser_ids: losers.map((l) => l.id),
1547
- repos: [...repos],
1548
- total_injection_count: total
1549
- });
1550
- }
1551
- return plans;
1552
- }
1553
- function applyGlobalizeCrossRepo(db, runId, plan) {
1554
- const winner = getMemory(db, plan.winner_id);
1555
- if (!winner) return;
1556
- const now = (/* @__PURE__ */ new Date()).toISOString();
1557
- const globalDedupeKey = memoryDedupeKey({ ...winner, scope: "global", repo: null });
1558
- const globalDedupeCollision = db.select({ id: memories.id }).from(memories).where(eq4(memories.dedupe_key, globalDedupeKey)).get();
1559
- db.update(memories).set({
1560
- scope: "global",
1561
- repo: null,
1562
- dedupe_key: globalDedupeCollision && globalDedupeCollision.id !== winner.id ? null : globalDedupeKey,
1563
- updated_at: now
1564
- }).where(eq4(memories.id, winner.id)).run();
1565
- const after = getMemory(db, winner.id);
1566
- recordAuditWithSnapshot(
1567
- db,
1568
- winner.id,
1569
- "edited",
1570
- DEFAULT_ACTOR,
1571
- `cleanup_globalize:winner:run:${runId}`,
1572
- winner,
1573
- after ?? null
1574
- );
1575
- db.insert(maintenanceCleanupLog).values({
1576
- id: randomUUID2(),
1577
- run_id: runId,
1578
- action: "globalize_cross_repo",
1579
- memory_id: winner.id,
1580
- related_memory_id: null,
1581
- before_snapshot: winner,
1582
- after_snapshot: after,
1583
- details: { role: "winner", repos: plan.repos },
1584
- reverted: false,
1585
- reverted_at: null,
1586
- created_at: now
1587
- }).run();
1588
- for (const loserId of plan.loser_ids) {
1589
- const loser = getMemory(db, loserId);
1590
- if (!loser || loser.status === "rejected") continue;
1591
- db.update(memories).set({ status: "rejected", supersedes: winner.id, dedupe_key: null, updated_at: now }).where(eq4(memories.id, loserId)).run();
1592
- const afterLoser = getMemory(db, loserId);
1593
- recordAuditWithSnapshot(
1594
- db,
1595
- loserId,
1596
- "rejected",
1597
- DEFAULT_ACTOR,
1598
- `cleanup_globalize:loser:winner=${winner.id}:run:${runId}`,
1599
- loser,
1600
- afterLoser ?? null
1601
- );
1602
- db.insert(maintenanceCleanupLog).values({
1603
- id: randomUUID2(),
1604
- run_id: runId,
1605
- action: "globalize_cross_repo",
1606
- memory_id: loserId,
1607
- related_memory_id: winner.id,
1608
- before_snapshot: loser,
1609
- after_snapshot: afterLoser,
1610
- details: { role: "loser", repo: loser.repo },
1611
- reverted: false,
1612
- reverted_at: null,
1613
- created_at: now
1614
- }).run();
1615
- }
1616
- }
1617
- function revertCleanupRun(db, runId) {
1618
- const rows = db.select().from(maintenanceCleanupLog).where(eq4(maintenanceCleanupLog.run_id, runId)).all();
1619
- if (rows.length === 0) {
1620
- return { run_id: runId, reverted: 0, skipped: 0, reasons: { not_found: 1 } };
1621
- }
1622
- const reasons = {};
1623
- let reverted = 0;
1624
- let skipped = 0;
1625
- const now = (/* @__PURE__ */ new Date()).toISOString();
1626
- for (const row of rows) {
1627
- if (row.reverted) {
1628
- skipped += 1;
1629
- reasons.already_reverted = (reasons.already_reverted ?? 0) + 1;
1630
- continue;
1631
- }
1632
- const before = row.before_snapshot;
1633
- if (!before || !before.status) {
1634
- skipped += 1;
1635
- reasons.no_snapshot = (reasons.no_snapshot ?? 0) + 1;
1636
- continue;
1637
- }
1638
- const current = getMemory(db, row.memory_id);
1639
- if (!current) {
1640
- skipped += 1;
1641
- reasons.memory_missing = (reasons.memory_missing ?? 0) + 1;
1642
- continue;
1643
- }
1644
- db.update(memories).set({
1645
- status: before.status,
1646
- text: before.text ?? current.text,
1647
- scope: before.scope ?? current.scope,
1648
- path_scope: before.path_scope ?? current.path_scope,
1649
- repo: before.repo !== void 0 ? before.repo : current.repo,
1650
- confidence: before.confidence ?? current.confidence,
1651
- injection_count: before.injection_count ?? current.injection_count,
1652
- override_count: before.override_count ?? current.override_count,
1653
- repetition_count: before.repetition_count ?? current.repetition_count,
1654
- supersedes: before.supersedes ?? null,
1655
- auto_inject: before.auto_inject ?? current.auto_inject,
1656
- updated_at: now
1657
- }).where(eq4(memories.id, row.memory_id)).run();
1658
- const after = getMemory(db, row.memory_id);
1659
- recordAuditWithSnapshot(
1660
- db,
1661
- row.memory_id,
1662
- "rolled_back",
1663
- DEFAULT_ACTOR,
1664
- `cleanup_revert:run:${runId}:log:${row.id}`,
1665
- current,
1666
- after ?? null
1667
- );
1668
- db.update(maintenanceCleanupLog).set({ reverted: true, reverted_at: now }).where(eq4(maintenanceCleanupLog.id, row.id)).run();
1669
- reverted += 1;
1670
- }
1671
- return { run_id: runId, reverted, skipped, reasons };
1672
- }
1673
- function listCleanupRuns(db, limit = 10) {
1674
- const rows = db.select().from(maintenanceCleanupLog).all();
1675
- const byRun = /* @__PURE__ */ new Map();
1676
- for (const row of rows) {
1677
- let entry = byRun.get(row.run_id);
1678
- if (!entry) {
1679
- entry = {
1680
- run_id: row.run_id,
1681
- started_at: row.created_at,
1682
- finished_at: row.created_at,
1683
- total: 0,
1684
- by_action: {},
1685
- reverted: 0
1686
- };
1687
- byRun.set(row.run_id, entry);
1688
- }
1689
- entry.total += 1;
1690
- entry.by_action[row.action] = (entry.by_action[row.action] ?? 0) + 1;
1691
- if (row.reverted) entry.reverted += 1;
1692
- if (row.created_at < entry.started_at) entry.started_at = row.created_at;
1693
- if (row.created_at > entry.finished_at) entry.finished_at = row.created_at;
1694
- }
1695
- return [...byRun.values()].sort((a, b) => b.finished_at.localeCompare(a.finished_at)).slice(0, limit);
1696
- }
1697
- function formatCleanupReport(report) {
1698
- const lines = [];
1699
- lines.push(`Cleanup ${report.dry_run ? "DRY-RUN" : "APPLY"} run=${report.run_id.slice(0, 8)}`);
1700
- lines.push(` dedupe_clusters: ${report.counts.dedupe_clusters}`);
1701
- lines.push(` dedupe_losers: ${report.counts.dedupe_losers}`);
1702
- lines.push(` fragment_rejections: ${report.counts.fragment_rejections}`);
1703
- lines.push(` repeat_promotions: ${report.counts.repeat_promotions}`);
1704
- lines.push(` command_suppressions: ${report.counts.command_suppressions}`);
1705
- lines.push(` globalizations: ${report.counts.globalizations} (losers=${report.counts.globalize_losers})`);
1706
- if (report.plan.length === 0) {
1707
- lines.push(" (no actions)");
1708
- return lines.join("\n");
1709
- }
1710
- lines.push("");
1711
- for (const item of report.plan) {
1712
- if (item.kind === "dedupe_exact_merge") {
1713
- lines.push(` merge: keep ${item.winner_id.slice(0, 8)} drop ${item.loser_ids.length} inj=${item.total_injection_count}`);
1714
- lines.push(` "${truncate(item.winner_text, 80)}"`);
1715
- } else if (item.kind === "reject_fragment_candidate") {
1716
- lines.push(` reject: ${item.memory_id.slice(0, 8)} reasons=${item.reasons.join(",")}`);
1717
- lines.push(` "${truncate(item.text, 80)}"`);
1718
- } else if (item.kind === "promote_repeat_correction") {
1719
- lines.push(` promote: ${item.memory_id.slice(0, 8)} via=${item.matched_pattern}`);
1720
- lines.push(` "${truncate(item.text, 80)}"`);
1721
- } else if (item.kind === "suppress_unproductive_command") {
1722
- lines.push(` suppress: ${item.memory_id.slice(0, 8)} inj=${item.injection_count} followed=${item.followed_count}`);
1723
- lines.push(` "${truncate(item.text, 80)}"`);
1724
- } else {
1725
- lines.push(` globalize: keep ${item.winner_id.slice(0, 8)} drop ${item.loser_ids.length} repos=[${item.repos.join(",")}]`);
1726
- lines.push(` "${truncate(item.winner_text, 80)}"`);
1727
- }
1728
- }
1729
- return lines.join("\n");
1730
- }
1731
- function truncate(s, n) {
1732
- if (s.length <= n) return s;
1733
- return s.slice(0, n - 1) + "\u2026";
1734
- }
1735
-
1736
- export {
1737
- computeHealthScore,
1738
- computeAllHealthScores,
1739
- formatHealthReport,
1740
- getRepoQualityProfile,
1741
- seedScannedConfidence,
1742
- inferScope,
1743
- detectContradictions,
1744
- resolveContradiction,
1745
- autoResolveContradictions,
1746
- listContradictions,
1747
- runDeterministicCleanup,
1748
- planDedupeExact,
1749
- planRejectFragments,
1750
- qualityReasons,
1751
- planPromoteRepeats,
1752
- planSuppressCommands,
1753
- planGlobalizeCrossRepo,
1754
- revertCleanupRun,
1755
- listCleanupRuns,
1756
- formatCleanupReport,
1757
- isTriggerTemplateRule,
1758
- isHighRiskRule,
1759
- detectCorrections,
1760
- processCorrection,
1761
- processReviewFeedback
1762
- };
1763
- //# sourceMappingURL=chunk-VEPXEHRZ.js.map