@hawon/nexus 0.1.0 → 0.3.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/README.md +60 -38
- package/dist/cli/index.js +76 -145
- package/dist/index.js +15 -26
- package/dist/mcp/server.js +61 -32
- package/package.json +2 -1
- package/scripts/auto-skill.sh +54 -0
- package/scripts/auto-sync.sh +11 -0
- package/scripts/benchmark.ts +444 -0
- package/scripts/scan-tool-result.sh +46 -0
- package/src/cli/index.ts +79 -172
- package/src/index.ts +17 -29
- package/src/mcp/server.ts +67 -41
- package/src/memory-engine/index.ts +4 -6
- package/src/memory-engine/nexus-memory.test.ts +437 -0
- package/src/memory-engine/nexus-memory.ts +631 -0
- package/src/memory-engine/semantic.ts +380 -0
- package/src/parser/parse.ts +1 -21
- package/src/promptguard/advanced-rules.ts +129 -12
- package/src/promptguard/entropy.ts +21 -2
- package/src/promptguard/evolution/auto-update.ts +16 -6
- package/src/promptguard/multilingual-rules.ts +68 -0
- package/src/promptguard/rules.ts +87 -2
- package/src/promptguard/scanner.test.ts +262 -0
- package/src/promptguard/scanner.ts +1 -1
- package/src/promptguard/semantic.ts +19 -4
- package/src/promptguard/token-analysis.ts +17 -5
- package/src/review/analyzer.test.ts +279 -0
- package/src/review/analyzer.ts +112 -28
- package/src/shared/stop-words.ts +21 -0
- package/src/skills/index.ts +11 -27
- package/src/skills/memory-skill-engine.ts +1044 -0
- package/src/testing/health-check.ts +19 -2
- package/src/cost/index.ts +0 -3
- package/src/cost/tracker.ts +0 -290
- package/src/cost/types.ts +0 -34
- package/src/memory-engine/compressor.ts +0 -97
- package/src/memory-engine/context-window.ts +0 -113
- package/src/memory-engine/store.ts +0 -371
- package/src/memory-engine/types.ts +0 -32
- package/src/skills/context-engine.ts +0 -863
- package/src/skills/extractor.ts +0 -224
- package/src/skills/global-context.ts +0 -726
- package/src/skills/library.ts +0 -189
- package/src/skills/pattern-engine.ts +0 -712
- package/src/skills/render-evolved.ts +0 -160
- package/src/skills/skill-reconciler.ts +0 -703
- package/src/skills/smart-extractor.ts +0 -843
- package/src/skills/types.ts +0 -18
- package/src/skills/wisdom-extractor.ts +0 -737
- package/src/superdev-evolution/index.ts +0 -3
- package/src/superdev-evolution/skill-manager.ts +0 -266
- package/src/superdev-evolution/types.ts +0 -20
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pattern Evolution Engine
|
|
3
|
-
*
|
|
4
|
-
* A sophisticated algorithm that:
|
|
5
|
-
* 1. Extracts task-completion patterns from sessions
|
|
6
|
-
* 2. Captures surrounding context (why this task was needed)
|
|
7
|
-
* 3. Finds similar past patterns across all sessions
|
|
8
|
-
* 4. Analyzes WHY approaches changed between similar tasks
|
|
9
|
-
* 5. Builds an evolution graph of how skills matured over time
|
|
10
|
-
* 6. Generates refined skills incorporating full evolution history
|
|
11
|
-
*
|
|
12
|
-
* The core insight: a skill isn't just "what Claude did" —
|
|
13
|
-
* it's WHY the approach was chosen given the context,
|
|
14
|
-
* and HOW it evolved from earlier attempts.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
|
-
import type { ParsedSession, ParsedMessage, ToolCall } from "../parser/types.js";
|
|
19
|
-
|
|
20
|
-
// ─── Types ───────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
export type ContextWindow = {
|
|
23
|
-
/** Messages leading up to the task (motivation, constraints, prior failures). */
|
|
24
|
-
before: ParsedMessage[];
|
|
25
|
-
/** The core task: user request + Claude execution. */
|
|
26
|
-
core: TaskSequence;
|
|
27
|
-
/** Messages after: user feedback, corrections, follow-ups. */
|
|
28
|
-
after: ParsedMessage[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type TaskSequence = {
|
|
32
|
-
/** User's original request. */
|
|
33
|
-
request: ParsedMessage;
|
|
34
|
-
/** Claude's response(s) with tool calls. */
|
|
35
|
-
responses: ParsedMessage[];
|
|
36
|
-
/** All tool calls made. */
|
|
37
|
-
toolCalls: ToolCall[];
|
|
38
|
-
/** Files touched. */
|
|
39
|
-
files: string[];
|
|
40
|
-
/** Was the outcome successful? (inferred from follow-up). */
|
|
41
|
-
outcome: "success" | "failure" | "partial" | "unknown";
|
|
42
|
-
/** User's feedback if any. */
|
|
43
|
-
feedback?: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export type PatternFingerprint = {
|
|
47
|
-
/** Unique hash of the pattern. */
|
|
48
|
-
id: string;
|
|
49
|
-
/** Action verb + object (e.g., "fix lint-errors"). */
|
|
50
|
-
action: string;
|
|
51
|
-
/** Normalized keywords from the request. */
|
|
52
|
-
keywords: string[];
|
|
53
|
-
/** Tool sequence signature (e.g., "Bash→Edit→Bash"). */
|
|
54
|
-
toolSignature: string;
|
|
55
|
-
/** Session this came from. */
|
|
56
|
-
sessionId: string;
|
|
57
|
-
/** Timestamp. */
|
|
58
|
-
timestamp: string;
|
|
59
|
-
/** Semantic vector (bag-of-words TF weights). */
|
|
60
|
-
vector: Map<string, number>;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export type PatternMatch = {
|
|
64
|
-
/** The pattern being compared to. */
|
|
65
|
-
pattern: PatternFingerprint;
|
|
66
|
-
/** Context window of this pattern. */
|
|
67
|
-
context: ContextWindow;
|
|
68
|
-
/** Similarity score 0-1. */
|
|
69
|
-
similarity: number;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export type DriftAnalysis = {
|
|
73
|
-
/** What changed between the two approaches. */
|
|
74
|
-
changes: DriftChange[];
|
|
75
|
-
/** Why it changed (inferred). */
|
|
76
|
-
reason: DriftReason;
|
|
77
|
-
/** Confidence in the analysis. */
|
|
78
|
-
confidence: number;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export type DriftChange = {
|
|
82
|
-
aspect: "tools" | "approach" | "files" | "order" | "scope";
|
|
83
|
-
description: string;
|
|
84
|
-
old: string;
|
|
85
|
-
new: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type DriftReason =
|
|
89
|
-
| "user_correction" // User explicitly said "no, do it this way"
|
|
90
|
-
| "failure_recovery" // Previous approach failed, this is the fix
|
|
91
|
-
| "optimization" // Same result, better method
|
|
92
|
-
| "scope_change" // Task expanded or narrowed
|
|
93
|
-
| "context_dependent" // Different context required different approach
|
|
94
|
-
| "unknown";
|
|
95
|
-
|
|
96
|
-
export type EvolvedSkill = {
|
|
97
|
-
id: string;
|
|
98
|
-
name: string;
|
|
99
|
-
description: string;
|
|
100
|
-
/** Current best approach (latest successful). */
|
|
101
|
-
currentApproach: {
|
|
102
|
-
steps: string[];
|
|
103
|
-
tools: string[];
|
|
104
|
-
files: string[];
|
|
105
|
-
};
|
|
106
|
-
/** Full evolution history. */
|
|
107
|
-
evolution: EvolutionEntry[];
|
|
108
|
-
/** Contexts where this skill applies. */
|
|
109
|
-
applicableContexts: string[];
|
|
110
|
-
/** Contexts where this skill should NOT be used. */
|
|
111
|
-
antiPatterns: string[];
|
|
112
|
-
/** Confidence (higher = more validated). */
|
|
113
|
-
confidence: number;
|
|
114
|
-
/** How many times this pattern appeared. */
|
|
115
|
-
occurrences: number;
|
|
116
|
-
/** When first seen / last seen. */
|
|
117
|
-
firstSeen: string;
|
|
118
|
-
lastSeen: string;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
export type EvolutionEntry = {
|
|
122
|
-
timestamp: string;
|
|
123
|
-
sessionId: string;
|
|
124
|
-
approach: string[];
|
|
125
|
-
outcome: "success" | "failure" | "partial" | "unknown";
|
|
126
|
-
drift?: DriftAnalysis;
|
|
127
|
-
contextSummary: string;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// ─── Stop words for keyword extraction ───────────────────────────
|
|
131
|
-
|
|
132
|
-
const STOP_WORDS = new Set([
|
|
133
|
-
"the", "a", "an", "is", "are", "was", "were", "be", "been", "have", "has",
|
|
134
|
-
"had", "do", "does", "did", "will", "would", "could", "should", "can",
|
|
135
|
-
"i", "me", "my", "we", "you", "your", "he", "she", "it", "they", "them",
|
|
136
|
-
"this", "that", "these", "those", "what", "which", "who", "how", "when",
|
|
137
|
-
"and", "but", "or", "not", "no", "so", "if", "then", "for", "of", "to",
|
|
138
|
-
"in", "on", "at", "by", "with", "from", "as", "into", "about", "up",
|
|
139
|
-
"there", "here", "all", "each", "both", "few", "more", "some", "any",
|
|
140
|
-
"just", "also", "than", "too", "very", "only", "now", "well",
|
|
141
|
-
"please", "thanks", "ok", "okay", "sure", "yes",
|
|
142
|
-
]);
|
|
143
|
-
|
|
144
|
-
// ─── Core Algorithm ──────────────────────────────────────────────
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Step 1: Extract task sequences with context windows from a session.
|
|
148
|
-
*/
|
|
149
|
-
export function extractContextualPatterns(session: ParsedSession): ContextWindow[] {
|
|
150
|
-
const messages = session.messages;
|
|
151
|
-
const windows: ContextWindow[] = [];
|
|
152
|
-
|
|
153
|
-
for (let i = 0; i < messages.length; i++) {
|
|
154
|
-
const msg = messages[i];
|
|
155
|
-
if (msg.role !== "user") continue;
|
|
156
|
-
|
|
157
|
-
// Look for a task: user message followed by assistant with tool calls
|
|
158
|
-
const responses: ParsedMessage[] = [];
|
|
159
|
-
const toolCalls: ToolCall[] = [];
|
|
160
|
-
const files: string[] = [];
|
|
161
|
-
let j = i + 1;
|
|
162
|
-
|
|
163
|
-
while (j < messages.length && messages[j].role === "assistant") {
|
|
164
|
-
const resp = messages[j];
|
|
165
|
-
responses.push(resp);
|
|
166
|
-
if (resp.toolCalls) {
|
|
167
|
-
for (const tc of resp.toolCalls) {
|
|
168
|
-
toolCalls.push(tc);
|
|
169
|
-
const fp = tc.input["file_path"] ?? tc.input["path"];
|
|
170
|
-
if (typeof fp === "string") files.push(fp);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
j++;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Skip if no tool calls (just a chat, not a task)
|
|
177
|
-
if (toolCalls.length === 0) {
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Gather context: up to 3 messages before
|
|
182
|
-
const beforeStart = Math.max(0, i - 3);
|
|
183
|
-
const before = messages.slice(beforeStart, i);
|
|
184
|
-
|
|
185
|
-
// Gather after: next user message and up to 2 more
|
|
186
|
-
const afterMessages: ParsedMessage[] = [];
|
|
187
|
-
let k = j;
|
|
188
|
-
let afterCount = 0;
|
|
189
|
-
while (k < messages.length && afterCount < 3) {
|
|
190
|
-
afterMessages.push(messages[k]);
|
|
191
|
-
afterCount++;
|
|
192
|
-
k++;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Infer outcome from follow-up
|
|
196
|
-
const outcome = inferOutcome(msg, afterMessages);
|
|
197
|
-
const feedback = extractFeedback(afterMessages);
|
|
198
|
-
|
|
199
|
-
const core: TaskSequence = {
|
|
200
|
-
request: msg,
|
|
201
|
-
responses,
|
|
202
|
-
toolCalls,
|
|
203
|
-
files: [...new Set(files)],
|
|
204
|
-
outcome,
|
|
205
|
-
feedback,
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
windows.push({ before, core, after: afterMessages });
|
|
209
|
-
|
|
210
|
-
// Skip to end of this sequence
|
|
211
|
-
i = j - 1;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return windows;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Step 2: Generate a fingerprint for a pattern (for similarity matching).
|
|
219
|
-
*/
|
|
220
|
-
export function fingerprintPattern(
|
|
221
|
-
window: ContextWindow,
|
|
222
|
-
sessionId: string,
|
|
223
|
-
): PatternFingerprint {
|
|
224
|
-
const request = window.core.request.content;
|
|
225
|
-
const keywords = extractKeywords(request);
|
|
226
|
-
const action = extractAction(request);
|
|
227
|
-
const toolSig = window.core.toolCalls.map((tc) => tc.name).join("→");
|
|
228
|
-
const vector = buildTermVector(request);
|
|
229
|
-
|
|
230
|
-
const hashInput = `${action}:${toolSig}:${keywords.slice(0, 5).join(",")}`;
|
|
231
|
-
const id = createHash("sha256").update(hashInput).digest("hex").slice(0, 12);
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
id,
|
|
235
|
-
action,
|
|
236
|
-
keywords,
|
|
237
|
-
toolSignature: toolSig,
|
|
238
|
-
sessionId,
|
|
239
|
-
timestamp: window.core.request.timestamp,
|
|
240
|
-
vector,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Step 3: Find similar patterns across all sessions.
|
|
246
|
-
*/
|
|
247
|
-
export function findSimilarPatterns(
|
|
248
|
-
target: PatternFingerprint,
|
|
249
|
-
allPatterns: { fingerprint: PatternFingerprint; context: ContextWindow }[],
|
|
250
|
-
threshold = 0.3,
|
|
251
|
-
): PatternMatch[] {
|
|
252
|
-
const matches: PatternMatch[] = [];
|
|
253
|
-
|
|
254
|
-
for (const { fingerprint, context } of allPatterns) {
|
|
255
|
-
if (fingerprint.id === target.id) continue; // skip self
|
|
256
|
-
|
|
257
|
-
const similarity = computePatternSimilarity(target, fingerprint);
|
|
258
|
-
if (similarity >= threshold) {
|
|
259
|
-
matches.push({ pattern: fingerprint, context, similarity });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return matches.sort((a, b) => b.similarity - a.similarity);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Step 4: Analyze drift between two similar patterns.
|
|
268
|
-
*/
|
|
269
|
-
export function analyzeDrift(
|
|
270
|
-
older: ContextWindow,
|
|
271
|
-
newer: ContextWindow,
|
|
272
|
-
): DriftAnalysis {
|
|
273
|
-
const changes: DriftChange[] = [];
|
|
274
|
-
|
|
275
|
-
// Tool changes
|
|
276
|
-
const oldTools = older.core.toolCalls.map((tc) => tc.name);
|
|
277
|
-
const newTools = newer.core.toolCalls.map((tc) => tc.name);
|
|
278
|
-
const oldToolSig = oldTools.join("→");
|
|
279
|
-
const newToolSig = newTools.join("→");
|
|
280
|
-
|
|
281
|
-
if (oldToolSig !== newToolSig) {
|
|
282
|
-
changes.push({
|
|
283
|
-
aspect: "tools",
|
|
284
|
-
description: "Tool sequence changed",
|
|
285
|
-
old: oldToolSig,
|
|
286
|
-
new: newToolSig,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// File scope changes
|
|
291
|
-
const oldFiles = new Set(older.core.files);
|
|
292
|
-
const newFiles = new Set(newer.core.files);
|
|
293
|
-
const addedFiles = [...newFiles].filter((f) => !oldFiles.has(f));
|
|
294
|
-
const removedFiles = [...oldFiles].filter((f) => !newFiles.has(f));
|
|
295
|
-
|
|
296
|
-
if (addedFiles.length > 0 || removedFiles.length > 0) {
|
|
297
|
-
changes.push({
|
|
298
|
-
aspect: "files",
|
|
299
|
-
description: `Files changed: +${addedFiles.length} -${removedFiles.length}`,
|
|
300
|
-
old: [...oldFiles].join(", "),
|
|
301
|
-
new: [...newFiles].join(", "),
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Scope change (tool count difference)
|
|
306
|
-
const toolCountDiff = newTools.length - oldTools.length;
|
|
307
|
-
if (Math.abs(toolCountDiff) >= 3) {
|
|
308
|
-
changes.push({
|
|
309
|
-
aspect: "scope",
|
|
310
|
-
description: toolCountDiff > 0
|
|
311
|
-
? `Scope expanded (${oldTools.length}→${newTools.length} tool calls)`
|
|
312
|
-
: `Scope narrowed (${oldTools.length}→${newTools.length} tool calls)`,
|
|
313
|
-
old: String(oldTools.length),
|
|
314
|
-
new: String(newTools.length),
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Order changes (same tools, different sequence)
|
|
319
|
-
const oldToolSet = new Set(oldTools);
|
|
320
|
-
const newToolSet = new Set(newTools);
|
|
321
|
-
const sameTools = [...oldToolSet].every((t) => newToolSet.has(t)) &&
|
|
322
|
-
[...newToolSet].every((t) => oldToolSet.has(t));
|
|
323
|
-
if (sameTools && oldToolSig !== newToolSig) {
|
|
324
|
-
changes.push({
|
|
325
|
-
aspect: "order",
|
|
326
|
-
description: "Same tools used in different order",
|
|
327
|
-
old: oldToolSig,
|
|
328
|
-
new: newToolSig,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Determine reason
|
|
333
|
-
const reason = inferDriftReason(older, newer, changes);
|
|
334
|
-
|
|
335
|
-
// Confidence based on evidence
|
|
336
|
-
const confidence = Math.min(1, 0.3 + changes.length * 0.15 +
|
|
337
|
-
(reason !== "unknown" ? 0.2 : 0));
|
|
338
|
-
|
|
339
|
-
return { changes, reason, confidence };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Step 5: Build an evolved skill from a pattern and its history.
|
|
344
|
-
*/
|
|
345
|
-
export function buildEvolvedSkill(
|
|
346
|
-
primary: { fingerprint: PatternFingerprint; context: ContextWindow },
|
|
347
|
-
history: PatternMatch[],
|
|
348
|
-
): EvolvedSkill {
|
|
349
|
-
// Sort all occurrences chronologically
|
|
350
|
-
const allOccurrences = [
|
|
351
|
-
{ fp: primary.fingerprint, ctx: primary.context },
|
|
352
|
-
...history.map((m) => ({ fp: m.pattern, ctx: m.context })),
|
|
353
|
-
].sort((a, b) => a.fp.timestamp.localeCompare(b.fp.timestamp));
|
|
354
|
-
|
|
355
|
-
// Build evolution timeline
|
|
356
|
-
const evolution: EvolutionEntry[] = [];
|
|
357
|
-
for (let i = 0; i < allOccurrences.length; i++) {
|
|
358
|
-
const { fp, ctx } = allOccurrences[i];
|
|
359
|
-
const steps = ctx.core.toolCalls.map((tc) => {
|
|
360
|
-
const fileArg = tc.input["file_path"] ?? tc.input["path"] ?? tc.input["command"] ?? "";
|
|
361
|
-
return `${tc.name}: ${String(fileArg).slice(0, 80)}`;
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
const entry: EvolutionEntry = {
|
|
365
|
-
timestamp: fp.timestamp,
|
|
366
|
-
sessionId: fp.sessionId,
|
|
367
|
-
approach: steps,
|
|
368
|
-
outcome: ctx.core.outcome,
|
|
369
|
-
contextSummary: summarizeContext(ctx),
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
// Analyze drift from previous occurrence
|
|
373
|
-
if (i > 0) {
|
|
374
|
-
entry.drift = analyzeDrift(allOccurrences[i - 1].ctx, ctx);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
evolution.push(entry);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Extract anti-patterns from failures
|
|
381
|
-
const antiPatterns: string[] = [];
|
|
382
|
-
for (const entry of evolution) {
|
|
383
|
-
if (entry.outcome === "failure" && entry.drift) {
|
|
384
|
-
for (const change of entry.drift.changes) {
|
|
385
|
-
antiPatterns.push(`Avoid: ${change.old} (changed to ${change.new})`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Extract applicable contexts from successful entries
|
|
391
|
-
const applicableContexts = evolution
|
|
392
|
-
.filter((e) => e.outcome === "success" || e.outcome === "unknown")
|
|
393
|
-
.map((e) => e.contextSummary)
|
|
394
|
-
.filter((s) => s.length > 0);
|
|
395
|
-
|
|
396
|
-
// Current best approach = latest successful
|
|
397
|
-
const latestSuccess = [...evolution]
|
|
398
|
-
.reverse()
|
|
399
|
-
.find((e) => e.outcome === "success" || e.outcome === "unknown");
|
|
400
|
-
|
|
401
|
-
const currentApproach = latestSuccess
|
|
402
|
-
? {
|
|
403
|
-
steps: latestSuccess.approach,
|
|
404
|
-
tools: [...new Set(latestSuccess.approach.map((s) => s.split(":")[0]))],
|
|
405
|
-
files: primary.context.core.files,
|
|
406
|
-
}
|
|
407
|
-
: {
|
|
408
|
-
steps: evolution[evolution.length - 1]?.approach ?? [],
|
|
409
|
-
tools: primary.fingerprint.toolSignature.split("→"),
|
|
410
|
-
files: primary.context.core.files,
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
// Confidence: more occurrences + more successes = higher
|
|
414
|
-
const successCount = evolution.filter((e) => e.outcome === "success").length;
|
|
415
|
-
const confidence = Math.min(1,
|
|
416
|
-
0.2 +
|
|
417
|
-
(allOccurrences.length * 0.1) +
|
|
418
|
-
(successCount * 0.15) +
|
|
419
|
-
(evolution.some((e) => e.drift) ? 0.1 : 0),
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
return {
|
|
423
|
-
id: primary.fingerprint.id,
|
|
424
|
-
name: primary.fingerprint.action,
|
|
425
|
-
description: buildDescription(primary, history),
|
|
426
|
-
currentApproach,
|
|
427
|
-
evolution,
|
|
428
|
-
applicableContexts: [...new Set(applicableContexts)].slice(0, 5),
|
|
429
|
-
antiPatterns: [...new Set(antiPatterns)].slice(0, 5),
|
|
430
|
-
confidence,
|
|
431
|
-
occurrences: allOccurrences.length,
|
|
432
|
-
firstSeen: allOccurrences[0].fp.timestamp,
|
|
433
|
-
lastSeen: allOccurrences[allOccurrences.length - 1].fp.timestamp,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Step 6: Run the full pipeline across multiple sessions.
|
|
439
|
-
*/
|
|
440
|
-
export function analyzePatternEvolution(sessions: ParsedSession[]): EvolvedSkill[] {
|
|
441
|
-
// Phase 1: Extract all contextual patterns
|
|
442
|
-
const allPatterns: { fingerprint: PatternFingerprint; context: ContextWindow }[] = [];
|
|
443
|
-
|
|
444
|
-
for (const session of sessions) {
|
|
445
|
-
const windows = extractContextualPatterns(session);
|
|
446
|
-
for (const window of windows) {
|
|
447
|
-
const fingerprint = fingerprintPattern(window, session.sessionId);
|
|
448
|
-
allPatterns.push({ fingerprint, context: window });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (allPatterns.length === 0) return [];
|
|
453
|
-
|
|
454
|
-
// Phase 2: Cluster similar patterns
|
|
455
|
-
const processed = new Set<string>();
|
|
456
|
-
const evolvedSkills: EvolvedSkill[] = [];
|
|
457
|
-
|
|
458
|
-
for (const pattern of allPatterns) {
|
|
459
|
-
if (processed.has(pattern.fingerprint.id)) continue;
|
|
460
|
-
|
|
461
|
-
const similar = findSimilarPatterns(pattern.fingerprint, allPatterns, 0.25);
|
|
462
|
-
|
|
463
|
-
// Mark all as processed
|
|
464
|
-
processed.add(pattern.fingerprint.id);
|
|
465
|
-
for (const match of similar) {
|
|
466
|
-
processed.add(match.pattern.id);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Only create skills for patterns that appeared 2+ times or had tool calls
|
|
470
|
-
if (similar.length > 0 || pattern.context.core.toolCalls.length >= 3) {
|
|
471
|
-
const skill = buildEvolvedSkill(pattern, similar);
|
|
472
|
-
evolvedSkills.push(skill);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Sort by confidence * occurrences
|
|
477
|
-
return evolvedSkills
|
|
478
|
-
.sort((a, b) => (b.confidence * b.occurrences) - (a.confidence * a.occurrences));
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ─── Helper Functions ────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
function extractKeywords(text: string): string[] {
|
|
484
|
-
return text
|
|
485
|
-
.toLowerCase()
|
|
486
|
-
.replace(/[^a-z가-힣0-9\s-]/g, " ")
|
|
487
|
-
.split(/\s+/)
|
|
488
|
-
.filter((w) => w.length > 2 && !STOP_WORDS.has(w))
|
|
489
|
-
.filter((w) => !/^[0-9]+$/.test(w));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function extractAction(text: string): string {
|
|
493
|
-
const words = text.toLowerCase().split(/\s+/).slice(0, 10);
|
|
494
|
-
|
|
495
|
-
const verbs = [
|
|
496
|
-
"fix", "add", "create", "build", "update", "remove", "delete", "refactor",
|
|
497
|
-
"implement", "write", "setup", "configure", "install", "deploy", "test",
|
|
498
|
-
"debug", "analyze", "review", "optimize", "migrate", "convert", "check",
|
|
499
|
-
"scan", "validate", "generate", "export", "import", "push", "commit",
|
|
500
|
-
];
|
|
501
|
-
const verb = words.find((w) => verbs.includes(w)) ?? words[0] ?? "task";
|
|
502
|
-
|
|
503
|
-
// Find object: first noun-like word after the verb
|
|
504
|
-
const verbIdx = words.indexOf(verb);
|
|
505
|
-
const objectWords = words.slice(verbIdx + 1).filter((w) => !STOP_WORDS.has(w));
|
|
506
|
-
const object = objectWords[0] ?? "unknown";
|
|
507
|
-
|
|
508
|
-
return `${verb}-${object}`;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function buildTermVector(text: string): Map<string, number> {
|
|
512
|
-
const words = extractKeywords(text);
|
|
513
|
-
const tf = new Map<string, number>();
|
|
514
|
-
for (const word of words) {
|
|
515
|
-
tf.set(word, (tf.get(word) ?? 0) + 1);
|
|
516
|
-
}
|
|
517
|
-
// Normalize
|
|
518
|
-
const max = Math.max(...tf.values(), 1);
|
|
519
|
-
for (const [term, count] of tf) {
|
|
520
|
-
tf.set(term, count / max);
|
|
521
|
-
}
|
|
522
|
-
return tf;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function computePatternSimilarity(
|
|
526
|
-
a: PatternFingerprint,
|
|
527
|
-
b: PatternFingerprint,
|
|
528
|
-
): number {
|
|
529
|
-
// Weighted combination of:
|
|
530
|
-
// 1. Keyword overlap (Jaccard) — 40%
|
|
531
|
-
// 2. Tool signature similarity — 30%
|
|
532
|
-
// 3. Term vector cosine similarity — 30%
|
|
533
|
-
|
|
534
|
-
// Keyword Jaccard
|
|
535
|
-
const aSet = new Set(a.keywords);
|
|
536
|
-
const bSet = new Set(b.keywords);
|
|
537
|
-
const intersection = [...aSet].filter((k) => bSet.has(k)).length;
|
|
538
|
-
const union = new Set([...aSet, ...bSet]).size;
|
|
539
|
-
const jaccard = union > 0 ? intersection / union : 0;
|
|
540
|
-
|
|
541
|
-
// Tool signature: longest common subsequence ratio
|
|
542
|
-
const toolSim = lcsRatio(a.toolSignature.split("→"), b.toolSignature.split("→"));
|
|
543
|
-
|
|
544
|
-
// Cosine similarity of term vectors
|
|
545
|
-
const cosine = cosineSimilarity(a.vector, b.vector);
|
|
546
|
-
|
|
547
|
-
return jaccard * 0.4 + toolSim * 0.3 + cosine * 0.3;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function lcsRatio(a: string[], b: string[]): number {
|
|
551
|
-
if (a.length === 0 || b.length === 0) return 0;
|
|
552
|
-
|
|
553
|
-
const dp: number[][] = Array.from({ length: a.length + 1 }, () =>
|
|
554
|
-
new Array(b.length + 1).fill(0),
|
|
555
|
-
);
|
|
556
|
-
|
|
557
|
-
for (let i = 1; i <= a.length; i++) {
|
|
558
|
-
for (let j = 1; j <= b.length; j++) {
|
|
559
|
-
if (a[i - 1] === b[j - 1]) {
|
|
560
|
-
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
561
|
-
} else {
|
|
562
|
-
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const lcsLen = dp[a.length][b.length];
|
|
568
|
-
return (2 * lcsLen) / (a.length + b.length);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function cosineSimilarity(a: Map<string, number>, b: Map<string, number>): number {
|
|
572
|
-
let dot = 0;
|
|
573
|
-
let normA = 0;
|
|
574
|
-
let normB = 0;
|
|
575
|
-
|
|
576
|
-
for (const [term, weight] of a) {
|
|
577
|
-
normA += weight * weight;
|
|
578
|
-
if (b.has(term)) {
|
|
579
|
-
dot += weight * (b.get(term) ?? 0);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
for (const [, weight] of b) {
|
|
583
|
-
normB += weight * weight;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
587
|
-
return denom > 0 ? dot / denom : 0;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function inferOutcome(
|
|
591
|
-
_request: ParsedMessage,
|
|
592
|
-
afterMessages: ParsedMessage[],
|
|
593
|
-
): "success" | "failure" | "partial" | "unknown" {
|
|
594
|
-
if (afterMessages.length === 0) return "unknown";
|
|
595
|
-
|
|
596
|
-
const nextUser = afterMessages.find((m) => m.role === "user");
|
|
597
|
-
if (!nextUser) return "unknown";
|
|
598
|
-
|
|
599
|
-
const text = nextUser.content.toLowerCase();
|
|
600
|
-
|
|
601
|
-
// Failure signals
|
|
602
|
-
const failureSignals = [
|
|
603
|
-
"안돼", "안 돼", "틀렸", "잘못", "wrong", "error", "fail", "doesn't work",
|
|
604
|
-
"not working", "broken", "bug", "fix this", "try again", "다시",
|
|
605
|
-
"아닌데", "그게 아니라",
|
|
606
|
-
];
|
|
607
|
-
if (failureSignals.some((s) => text.includes(s))) return "failure";
|
|
608
|
-
|
|
609
|
-
// Success signals
|
|
610
|
-
const successSignals = [
|
|
611
|
-
"좋아", "완벽", "됐어", "됐다", "고마워", "감사", "good", "perfect",
|
|
612
|
-
"great", "thanks", "works", "nice", "잘됐", "오케이", "ㅇㅇ", "ㄱㄱ",
|
|
613
|
-
"next", "다음", "이제",
|
|
614
|
-
];
|
|
615
|
-
if (successSignals.some((s) => text.includes(s))) return "success";
|
|
616
|
-
|
|
617
|
-
// If user just continues with a new task, probably success
|
|
618
|
-
if (!text.includes("?") && text.length < 100) return "success";
|
|
619
|
-
|
|
620
|
-
return "unknown";
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function extractFeedback(afterMessages: ParsedMessage[]): string | undefined {
|
|
624
|
-
const nextUser = afterMessages.find((m) => m.role === "user");
|
|
625
|
-
if (!nextUser) return undefined;
|
|
626
|
-
|
|
627
|
-
const text = nextUser.content;
|
|
628
|
-
// Only return as feedback if it's short (likely a reaction, not a new task)
|
|
629
|
-
if (text.length > 200) return undefined;
|
|
630
|
-
return text;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function inferDriftReason(
|
|
634
|
-
older: ContextWindow,
|
|
635
|
-
newer: ContextWindow,
|
|
636
|
-
changes: DriftChange[],
|
|
637
|
-
): DriftReason {
|
|
638
|
-
// Check if older failed and newer is a recovery
|
|
639
|
-
if (older.core.outcome === "failure") return "failure_recovery";
|
|
640
|
-
|
|
641
|
-
// Check if user explicitly corrected
|
|
642
|
-
if (older.core.feedback) {
|
|
643
|
-
const fb = older.core.feedback.toLowerCase();
|
|
644
|
-
const correctionWords = [
|
|
645
|
-
"no", "wrong", "not", "don't", "instead", "아니", "말고", "다른",
|
|
646
|
-
"그게 아니라", "이렇게", "대신",
|
|
647
|
-
];
|
|
648
|
-
if (correctionWords.some((w) => fb.includes(w))) return "user_correction";
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Check scope change
|
|
652
|
-
if (changes.some((c) => c.aspect === "scope")) return "scope_change";
|
|
653
|
-
|
|
654
|
-
// Check if same tools but different order (optimization)
|
|
655
|
-
if (changes.length === 1 && changes[0].aspect === "order") return "optimization";
|
|
656
|
-
|
|
657
|
-
// Check if context is very different
|
|
658
|
-
const olderCtx = summarizeContext(older).toLowerCase();
|
|
659
|
-
const newerCtx = summarizeContext(newer).toLowerCase();
|
|
660
|
-
const contextOverlap = computeTextOverlap(olderCtx, newerCtx);
|
|
661
|
-
if (contextOverlap < 0.3) return "context_dependent";
|
|
662
|
-
|
|
663
|
-
return "unknown";
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function computeTextOverlap(a: string, b: string): number {
|
|
667
|
-
const aWords = new Set(a.split(/\s+/));
|
|
668
|
-
const bWords = new Set(b.split(/\s+/));
|
|
669
|
-
const intersection = [...aWords].filter((w) => bWords.has(w)).length;
|
|
670
|
-
const union = new Set([...aWords, ...bWords]).size;
|
|
671
|
-
return union > 0 ? intersection / union : 0;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function summarizeContext(ctx: ContextWindow): string {
|
|
675
|
-
const parts: string[] = [];
|
|
676
|
-
|
|
677
|
-
// What came before (motivation)
|
|
678
|
-
for (const msg of ctx.before) {
|
|
679
|
-
if (msg.role === "user" && msg.content.length < 200) {
|
|
680
|
-
parts.push(msg.content.slice(0, 100));
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// The request itself
|
|
685
|
-
parts.push(ctx.core.request.content.slice(0, 150));
|
|
686
|
-
|
|
687
|
-
return parts.join(" | ").slice(0, 300);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function buildDescription(
|
|
691
|
-
primary: { fingerprint: PatternFingerprint; context: ContextWindow },
|
|
692
|
-
history: PatternMatch[],
|
|
693
|
-
): string {
|
|
694
|
-
const action = primary.fingerprint.action;
|
|
695
|
-
const occurrences = history.length + 1;
|
|
696
|
-
const tools = primary.fingerprint.toolSignature;
|
|
697
|
-
|
|
698
|
-
let desc = `${action} (seen ${occurrences}x, tools: ${tools})`;
|
|
699
|
-
|
|
700
|
-
if (history.length > 0) {
|
|
701
|
-
const latestDrift = history[history.length - 1];
|
|
702
|
-
const drift = analyzeDrift(
|
|
703
|
-
history.length > 1 ? history[history.length - 2].context : primary.context,
|
|
704
|
-
latestDrift.context,
|
|
705
|
-
);
|
|
706
|
-
if (drift.reason !== "unknown") {
|
|
707
|
-
desc += `. Approach evolved due to ${drift.reason.replace(/_/g, " ")}`;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return desc;
|
|
712
|
-
}
|