@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.
Files changed (52) hide show
  1. package/README.md +60 -38
  2. package/dist/cli/index.js +76 -145
  3. package/dist/index.js +15 -26
  4. package/dist/mcp/server.js +61 -32
  5. package/package.json +2 -1
  6. package/scripts/auto-skill.sh +54 -0
  7. package/scripts/auto-sync.sh +11 -0
  8. package/scripts/benchmark.ts +444 -0
  9. package/scripts/scan-tool-result.sh +46 -0
  10. package/src/cli/index.ts +79 -172
  11. package/src/index.ts +17 -29
  12. package/src/mcp/server.ts +67 -41
  13. package/src/memory-engine/index.ts +4 -6
  14. package/src/memory-engine/nexus-memory.test.ts +437 -0
  15. package/src/memory-engine/nexus-memory.ts +631 -0
  16. package/src/memory-engine/semantic.ts +380 -0
  17. package/src/parser/parse.ts +1 -21
  18. package/src/promptguard/advanced-rules.ts +129 -12
  19. package/src/promptguard/entropy.ts +21 -2
  20. package/src/promptguard/evolution/auto-update.ts +16 -6
  21. package/src/promptguard/multilingual-rules.ts +68 -0
  22. package/src/promptguard/rules.ts +87 -2
  23. package/src/promptguard/scanner.test.ts +262 -0
  24. package/src/promptguard/scanner.ts +1 -1
  25. package/src/promptguard/semantic.ts +19 -4
  26. package/src/promptguard/token-analysis.ts +17 -5
  27. package/src/review/analyzer.test.ts +279 -0
  28. package/src/review/analyzer.ts +112 -28
  29. package/src/shared/stop-words.ts +21 -0
  30. package/src/skills/index.ts +11 -27
  31. package/src/skills/memory-skill-engine.ts +1044 -0
  32. package/src/testing/health-check.ts +19 -2
  33. package/src/cost/index.ts +0 -3
  34. package/src/cost/tracker.ts +0 -290
  35. package/src/cost/types.ts +0 -34
  36. package/src/memory-engine/compressor.ts +0 -97
  37. package/src/memory-engine/context-window.ts +0 -113
  38. package/src/memory-engine/store.ts +0 -371
  39. package/src/memory-engine/types.ts +0 -32
  40. package/src/skills/context-engine.ts +0 -863
  41. package/src/skills/extractor.ts +0 -224
  42. package/src/skills/global-context.ts +0 -726
  43. package/src/skills/library.ts +0 -189
  44. package/src/skills/pattern-engine.ts +0 -712
  45. package/src/skills/render-evolved.ts +0 -160
  46. package/src/skills/skill-reconciler.ts +0 -703
  47. package/src/skills/smart-extractor.ts +0 -843
  48. package/src/skills/types.ts +0 -18
  49. package/src/skills/wisdom-extractor.ts +0 -737
  50. package/src/superdev-evolution/index.ts +0 -3
  51. package/src/superdev-evolution/skill-manager.ts +0 -266
  52. 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
- }