@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.
- package/dist/{chunk-K5FZ47NN.js → chunk-7XCLKPJ3.js} +6 -8
- package/dist/{chunk-K5FZ47NN.js.map → chunk-7XCLKPJ3.js.map} +1 -1
- package/dist/{chunk-A5UIRZU6.js → chunk-A6XEULA4.js} +3 -2
- package/dist/chunk-A6XEULA4.js.map +1 -0
- package/dist/{chunk-F56Y3SHS.js → chunk-E4HJDGCW.js} +7 -9
- package/dist/{chunk-F56Y3SHS.js.map → chunk-E4HJDGCW.js.map} +1 -1
- package/dist/{chunk-IILLSHLM.js → chunk-KAGIAOD7.js} +2583 -84
- package/dist/chunk-KAGIAOD7.js.map +1 -0
- package/dist/{chunk-FHKV6ELT.js → chunk-MJ4GGBTL.js} +11 -13
- package/dist/{chunk-FHKV6ELT.js.map → chunk-MJ4GGBTL.js.map} +1 -1
- package/dist/{chunk-LVQW6WHK.js → chunk-XUM7JEJU.js} +2 -2
- package/dist/{cleanup-TVOX2S2S.js → cleanup-MYSQ44EP.js} +4 -4
- package/dist/cli.js +206 -33
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +60 -49
- package/dist/daemon.js.map +1 -1
- package/dist/dispatcher-SUUX5AX6.js +16 -0
- package/dist/mcp.js +5 -5
- package/dist/{quality-Z7LPMMBC.js → quality-YTQKAEY6.js} +3 -3
- package/dist/{tasks-UOLSPXJQ.js → tasks-GSQUHD4F.js} +6 -3
- package/dist/{usage-CY3V72YN.js → usage-DU4TKVJH.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-A5UIRZU6.js.map +0 -1
- package/dist/chunk-GC5XMBG4.js +0 -551
- package/dist/chunk-GC5XMBG4.js.map +0 -1
- package/dist/chunk-IILLSHLM.js.map +0 -1
- package/dist/chunk-VEPXEHRZ.js +0 -1763
- package/dist/chunk-VEPXEHRZ.js.map +0 -1
- package/dist/dispatcher-UGMU6THT.js +0 -15
- /package/dist/{chunk-LVQW6WHK.js.map → chunk-XUM7JEJU.js.map} +0 -0
- /package/dist/{cleanup-TVOX2S2S.js.map → cleanup-MYSQ44EP.js.map} +0 -0
- /package/dist/{dispatcher-UGMU6THT.js.map → dispatcher-SUUX5AX6.js.map} +0 -0
- /package/dist/{quality-Z7LPMMBC.js.map → quality-YTQKAEY6.js.map} +0 -0
- /package/dist/{tasks-UOLSPXJQ.js.map → tasks-GSQUHD4F.js.map} +0 -0
- /package/dist/{usage-CY3V72YN.js.map → usage-DU4TKVJH.js.map} +0 -0
package/dist/chunk-VEPXEHRZ.js
DELETED
|
@@ -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
|