learning-agent 0.1.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/index.js ADDED
@@ -0,0 +1,732 @@
1
+ import { mkdir, appendFile, readFile } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { z } from 'zod';
5
+ import { existsSync, mkdirSync, statSync } from 'fs';
6
+ import Database from 'better-sqlite3';
7
+ import { resolveModelFile, getLlama } from 'node-llama-cpp';
8
+ import { homedir } from 'os';
9
+
10
+ // src/storage/jsonl.ts
11
+ var SourceSchema = z.enum([
12
+ "user_correction",
13
+ "self_correction",
14
+ "test_failure",
15
+ "manual"
16
+ ]);
17
+ var ContextSchema = z.object({
18
+ tool: z.string(),
19
+ intent: z.string()
20
+ });
21
+ var PatternSchema = z.object({
22
+ bad: z.string(),
23
+ good: z.string()
24
+ });
25
+ var SeveritySchema = z.enum(["high", "medium", "low"]);
26
+ var LessonTypeSchema = z.enum(["quick", "full"]);
27
+ var LessonSchema = z.object({
28
+ // Core identity (required)
29
+ id: z.string(),
30
+ type: LessonTypeSchema,
31
+ trigger: z.string(),
32
+ insight: z.string(),
33
+ // Metadata (required)
34
+ tags: z.array(z.string()),
35
+ source: SourceSchema,
36
+ context: ContextSchema,
37
+ created: z.string(),
38
+ // ISO8601
39
+ confirmed: z.boolean(),
40
+ // Relationships (required, can be empty arrays)
41
+ supersedes: z.array(z.string()),
42
+ related: z.array(z.string()),
43
+ // Extended fields (optional - typically present for 'full' type)
44
+ evidence: z.string().optional(),
45
+ severity: SeveritySchema.optional(),
46
+ pattern: PatternSchema.optional(),
47
+ // Lifecycle fields (optional)
48
+ deleted: z.boolean().optional(),
49
+ retrievalCount: z.number().optional()
50
+ });
51
+ var TombstoneSchema = z.object({
52
+ id: z.string(),
53
+ deleted: z.literal(true),
54
+ deletedAt: z.string()
55
+ // ISO8601
56
+ });
57
+ function generateId(insight) {
58
+ const hash = createHash("sha256").update(insight).digest("hex");
59
+ return `L${hash.slice(0, 8)}`;
60
+ }
61
+
62
+ // src/storage/jsonl.ts
63
+ var LESSONS_PATH = ".claude/lessons/index.jsonl";
64
+ async function appendLesson(repoRoot, lesson) {
65
+ const filePath = join(repoRoot, LESSONS_PATH);
66
+ await mkdir(dirname(filePath), { recursive: true });
67
+ const line = JSON.stringify(lesson) + "\n";
68
+ await appendFile(filePath, line, "utf-8");
69
+ }
70
+ function parseJsonLine(line, lineNumber, strict, onParseError) {
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(line);
74
+ } catch (err) {
75
+ const parseError = {
76
+ line: lineNumber,
77
+ message: `Invalid JSON: ${err.message}`,
78
+ cause: err
79
+ };
80
+ if (strict) {
81
+ throw new Error(`Parse error on line ${lineNumber}: ${parseError.message}`);
82
+ }
83
+ onParseError?.(parseError);
84
+ return null;
85
+ }
86
+ const result = LessonSchema.safeParse(parsed);
87
+ if (!result.success) {
88
+ const parseError = {
89
+ line: lineNumber,
90
+ message: `Schema validation failed: ${result.error.message}`,
91
+ cause: result.error
92
+ };
93
+ if (strict) {
94
+ throw new Error(`Parse error on line ${lineNumber}: ${parseError.message}`);
95
+ }
96
+ onParseError?.(parseError);
97
+ return null;
98
+ }
99
+ return result.data;
100
+ }
101
+ async function readLessons(repoRoot, options = {}) {
102
+ const { strict = false, onParseError } = options;
103
+ const filePath = join(repoRoot, LESSONS_PATH);
104
+ let content;
105
+ try {
106
+ content = await readFile(filePath, "utf-8");
107
+ } catch (err) {
108
+ if (err.code === "ENOENT") {
109
+ return { lessons: [], skippedCount: 0 };
110
+ }
111
+ throw err;
112
+ }
113
+ const lessons = /* @__PURE__ */ new Map();
114
+ let skippedCount = 0;
115
+ const lines = content.split("\n");
116
+ for (let i = 0; i < lines.length; i++) {
117
+ const trimmed = lines[i].trim();
118
+ if (!trimmed) continue;
119
+ const lesson = parseJsonLine(trimmed, i + 1, strict, onParseError);
120
+ if (!lesson) {
121
+ skippedCount++;
122
+ continue;
123
+ }
124
+ if (lesson.deleted) {
125
+ lessons.delete(lesson.id);
126
+ } else {
127
+ lessons.set(lesson.id, lesson);
128
+ }
129
+ }
130
+ return { lessons: Array.from(lessons.values()), skippedCount };
131
+ }
132
+ var DB_PATH = ".claude/.cache/lessons.sqlite";
133
+ var SCHEMA_SQL = `
134
+ -- Main lessons table
135
+ CREATE TABLE IF NOT EXISTS lessons (
136
+ id TEXT PRIMARY KEY,
137
+ type TEXT NOT NULL,
138
+ trigger TEXT NOT NULL,
139
+ insight TEXT NOT NULL,
140
+ evidence TEXT,
141
+ severity TEXT,
142
+ tags TEXT NOT NULL DEFAULT '',
143
+ source TEXT NOT NULL,
144
+ context TEXT NOT NULL DEFAULT '{}',
145
+ supersedes TEXT NOT NULL DEFAULT '[]',
146
+ related TEXT NOT NULL DEFAULT '[]',
147
+ created TEXT NOT NULL,
148
+ confirmed INTEGER NOT NULL DEFAULT 0,
149
+ deleted INTEGER NOT NULL DEFAULT 0,
150
+ retrieval_count INTEGER NOT NULL DEFAULT 0,
151
+ last_retrieved TEXT,
152
+ embedding BLOB,
153
+ content_hash TEXT
154
+ );
155
+
156
+ -- FTS5 virtual table for full-text search
157
+ CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
158
+ id,
159
+ trigger,
160
+ insight,
161
+ tags,
162
+ content='lessons',
163
+ content_rowid='rowid'
164
+ );
165
+
166
+ -- Trigger to sync FTS on INSERT
167
+ CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
168
+ INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
169
+ VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
170
+ END;
171
+
172
+ -- Trigger to sync FTS on DELETE
173
+ CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
174
+ INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
175
+ VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
176
+ END;
177
+
178
+ -- Trigger to sync FTS on UPDATE
179
+ CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
180
+ INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
181
+ VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
182
+ INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
183
+ VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
184
+ END;
185
+
186
+ -- Index for common queries
187
+ CREATE INDEX IF NOT EXISTS idx_lessons_created ON lessons(created);
188
+ CREATE INDEX IF NOT EXISTS idx_lessons_confirmed ON lessons(confirmed);
189
+ CREATE INDEX IF NOT EXISTS idx_lessons_severity ON lessons(severity);
190
+
191
+ -- Metadata table for sync tracking
192
+ CREATE TABLE IF NOT EXISTS metadata (
193
+ key TEXT PRIMARY KEY,
194
+ value TEXT NOT NULL
195
+ );
196
+ `;
197
+ function createSchema(database) {
198
+ database.exec(SCHEMA_SQL);
199
+ }
200
+ var db = null;
201
+ function contentHash(trigger, insight) {
202
+ return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
203
+ }
204
+ function openDb(repoRoot) {
205
+ if (db) return db;
206
+ const dbPath = join(repoRoot, DB_PATH);
207
+ const dir = dirname(dbPath);
208
+ mkdirSync(dir, { recursive: true });
209
+ db = new Database(dbPath);
210
+ db.pragma("journal_mode = WAL");
211
+ createSchema(db);
212
+ return db;
213
+ }
214
+ function closeDb() {
215
+ if (db) {
216
+ db.close();
217
+ db = null;
218
+ }
219
+ }
220
+ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
221
+ const database = openDb(repoRoot);
222
+ const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
223
+ if (!row || !row.embedding || !row.content_hash) {
224
+ return null;
225
+ }
226
+ if (expectedHash && row.content_hash !== expectedHash) {
227
+ return null;
228
+ }
229
+ const float32 = new Float32Array(
230
+ row.embedding.buffer,
231
+ row.embedding.byteOffset,
232
+ row.embedding.byteLength / 4
233
+ );
234
+ return Array.from(float32);
235
+ }
236
+ function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
237
+ const database = openDb(repoRoot);
238
+ const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
239
+ const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
240
+ database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
241
+ }
242
+ function rowToLesson(row) {
243
+ const lesson = {
244
+ id: row.id,
245
+ type: row.type,
246
+ trigger: row.trigger,
247
+ insight: row.insight,
248
+ tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
249
+ source: row.source,
250
+ context: JSON.parse(row.context),
251
+ supersedes: JSON.parse(row.supersedes),
252
+ related: JSON.parse(row.related),
253
+ created: row.created,
254
+ confirmed: row.confirmed === 1
255
+ };
256
+ if (row.evidence !== null) {
257
+ lesson.evidence = row.evidence;
258
+ }
259
+ if (row.severity !== null) {
260
+ lesson.severity = row.severity;
261
+ }
262
+ if (row.deleted === 1) {
263
+ lesson.deleted = true;
264
+ }
265
+ if (row.retrieval_count > 0) {
266
+ lesson.retrievalCount = row.retrieval_count;
267
+ }
268
+ return lesson;
269
+ }
270
+ function collectCachedEmbeddings(database) {
271
+ const cache = /* @__PURE__ */ new Map();
272
+ const rows = database.prepare("SELECT id, embedding, content_hash FROM lessons WHERE embedding IS NOT NULL").all();
273
+ for (const row of rows) {
274
+ if (row.embedding && row.content_hash) {
275
+ cache.set(row.id, { embedding: row.embedding, contentHash: row.content_hash });
276
+ }
277
+ }
278
+ return cache;
279
+ }
280
+ var INSERT_LESSON_SQL = `
281
+ INSERT INTO lessons (id, type, trigger, insight, evidence, severity, tags, source, context, supersedes, related, created, confirmed, deleted, retrieval_count, last_retrieved, embedding, content_hash)
282
+ VALUES (@id, @type, @trigger, @insight, @evidence, @severity, @tags, @source, @context, @supersedes, @related, @created, @confirmed, @deleted, @retrieval_count, @last_retrieved, @embedding, @content_hash)
283
+ `;
284
+ function getJsonlMtime(repoRoot) {
285
+ const jsonlPath = join(repoRoot, LESSONS_PATH);
286
+ try {
287
+ const stat = statSync(jsonlPath);
288
+ return stat.mtimeMs;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+ function getLastSyncMtime(database) {
294
+ const row = database.prepare("SELECT value FROM metadata WHERE key = ?").get("last_sync_mtime");
295
+ return row ? parseFloat(row.value) : null;
296
+ }
297
+ function setLastSyncMtime(database, mtime) {
298
+ database.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)").run("last_sync_mtime", mtime.toString());
299
+ }
300
+ async function rebuildIndex(repoRoot) {
301
+ const database = openDb(repoRoot);
302
+ const { lessons } = await readLessons(repoRoot);
303
+ const cachedEmbeddings = collectCachedEmbeddings(database);
304
+ database.exec("DELETE FROM lessons");
305
+ if (lessons.length === 0) {
306
+ const mtime2 = getJsonlMtime(repoRoot);
307
+ if (mtime2 !== null) {
308
+ setLastSyncMtime(database, mtime2);
309
+ }
310
+ return;
311
+ }
312
+ const insert = database.prepare(INSERT_LESSON_SQL);
313
+ const insertMany = database.transaction((items) => {
314
+ for (const lesson of items) {
315
+ const newHash = contentHash(lesson.trigger, lesson.insight);
316
+ const cached = cachedEmbeddings.get(lesson.id);
317
+ const hasValidCache = cached && cached.contentHash === newHash;
318
+ insert.run({
319
+ id: lesson.id,
320
+ type: lesson.type,
321
+ trigger: lesson.trigger,
322
+ insight: lesson.insight,
323
+ evidence: lesson.evidence ?? null,
324
+ severity: lesson.severity ?? null,
325
+ tags: lesson.tags.join(","),
326
+ source: lesson.source,
327
+ context: JSON.stringify(lesson.context),
328
+ supersedes: JSON.stringify(lesson.supersedes),
329
+ related: JSON.stringify(lesson.related),
330
+ created: lesson.created,
331
+ confirmed: lesson.confirmed ? 1 : 0,
332
+ deleted: lesson.deleted ? 1 : 0,
333
+ retrieval_count: lesson.retrievalCount ?? 0,
334
+ last_retrieved: null,
335
+ // Reset on rebuild since we're rebuilding from source
336
+ embedding: hasValidCache ? cached.embedding : null,
337
+ content_hash: hasValidCache ? cached.contentHash : null
338
+ });
339
+ }
340
+ });
341
+ insertMany(lessons);
342
+ const mtime = getJsonlMtime(repoRoot);
343
+ if (mtime !== null) {
344
+ setLastSyncMtime(database, mtime);
345
+ }
346
+ }
347
+ async function syncIfNeeded(repoRoot, options = {}) {
348
+ const { force = false } = options;
349
+ const jsonlMtime = getJsonlMtime(repoRoot);
350
+ if (jsonlMtime === null && !force) {
351
+ return false;
352
+ }
353
+ const database = openDb(repoRoot);
354
+ const lastSyncMtime = getLastSyncMtime(database);
355
+ const needsRebuild = force || lastSyncMtime === null || jsonlMtime !== null && jsonlMtime > lastSyncMtime;
356
+ if (needsRebuild) {
357
+ await rebuildIndex(repoRoot);
358
+ return true;
359
+ }
360
+ return false;
361
+ }
362
+ async function searchKeyword(repoRoot, query, limit) {
363
+ const database = openDb(repoRoot);
364
+ const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
365
+ if (countResult.cnt === 0) return [];
366
+ const rows = database.prepare(
367
+ `
368
+ SELECT l.*
369
+ FROM lessons l
370
+ JOIN lessons_fts fts ON l.rowid = fts.rowid
371
+ WHERE lessons_fts MATCH ?
372
+ LIMIT ?
373
+ `
374
+ ).all(query, limit);
375
+ if (rows.length > 0) {
376
+ incrementRetrievalCount(repoRoot, rows.map((r) => r.id));
377
+ }
378
+ return rows.map(rowToLesson);
379
+ }
380
+ function incrementRetrievalCount(repoRoot, lessonIds) {
381
+ if (lessonIds.length === 0) return;
382
+ const database = openDb(repoRoot);
383
+ const now = (/* @__PURE__ */ new Date()).toISOString();
384
+ const update = database.prepare(`
385
+ UPDATE lessons
386
+ SET retrieval_count = retrieval_count + 1,
387
+ last_retrieved = ?
388
+ WHERE id = ?
389
+ `);
390
+ const updateMany = database.transaction((ids) => {
391
+ for (const id of ids) {
392
+ update.run(now, id);
393
+ }
394
+ });
395
+ updateMany(lessonIds);
396
+ }
397
+ var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
398
+ var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
399
+ var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
400
+ function isModelAvailable() {
401
+ return existsSync(join(DEFAULT_MODEL_DIR, MODEL_FILENAME));
402
+ }
403
+ async function resolveModel(options = {}) {
404
+ const { cli = true } = options;
405
+ return resolveModelFile(MODEL_URI, { cli });
406
+ }
407
+
408
+ // src/embeddings/nomic.ts
409
+ var embeddingContext = null;
410
+ async function getEmbedding() {
411
+ if (embeddingContext) return embeddingContext;
412
+ const modelPath = await resolveModel({ cli: true });
413
+ const llama = await getLlama();
414
+ const model = await llama.loadModel({ modelPath });
415
+ embeddingContext = await model.createEmbeddingContext();
416
+ return embeddingContext;
417
+ }
418
+ function unloadEmbedding() {
419
+ if (embeddingContext) {
420
+ embeddingContext.dispose();
421
+ embeddingContext = null;
422
+ }
423
+ }
424
+ async function embedText(text) {
425
+ const ctx = await getEmbedding();
426
+ const result = await ctx.getEmbeddingFor(text);
427
+ return Array.from(result.vector);
428
+ }
429
+ async function embedTexts(texts) {
430
+ if (texts.length === 0) return [];
431
+ const ctx = await getEmbedding();
432
+ const results = [];
433
+ for (const text of texts) {
434
+ const result = await ctx.getEmbeddingFor(text);
435
+ results.push(Array.from(result.vector));
436
+ }
437
+ return results;
438
+ }
439
+
440
+ // src/search/vector.ts
441
+ function cosineSimilarity(a, b) {
442
+ if (a.length !== b.length) {
443
+ throw new Error("Vectors must have same length");
444
+ }
445
+ let dotProduct = 0;
446
+ let normA = 0;
447
+ let normB = 0;
448
+ for (let i = 0; i < a.length; i++) {
449
+ dotProduct += a[i] * b[i];
450
+ normA += a[i] * a[i];
451
+ normB += b[i] * b[i];
452
+ }
453
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
454
+ if (magnitude === 0) return 0;
455
+ return dotProduct / magnitude;
456
+ }
457
+ var DEFAULT_LIMIT = 10;
458
+ async function searchVector(repoRoot, query, options) {
459
+ const limit = options?.limit ?? DEFAULT_LIMIT;
460
+ const { lessons } = await readLessons(repoRoot);
461
+ if (lessons.length === 0) return [];
462
+ const queryVector = await embedText(query);
463
+ const scored = [];
464
+ for (const lesson of lessons) {
465
+ const lessonText = `${lesson.trigger} ${lesson.insight}`;
466
+ const hash = contentHash(lesson.trigger, lesson.insight);
467
+ let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
468
+ if (!lessonVector) {
469
+ lessonVector = await embedText(lessonText);
470
+ setCachedEmbedding(repoRoot, lesson.id, lessonVector, hash);
471
+ }
472
+ const score = cosineSimilarity(queryVector, lessonVector);
473
+ scored.push({ lesson, score });
474
+ }
475
+ scored.sort((a, b) => b.score - a.score);
476
+ return scored.slice(0, limit);
477
+ }
478
+
479
+ // src/search/ranking.ts
480
+ var RECENCY_THRESHOLD_DAYS = 30;
481
+ var HIGH_SEVERITY_BOOST = 1.5;
482
+ var MEDIUM_SEVERITY_BOOST = 1;
483
+ var LOW_SEVERITY_BOOST = 0.8;
484
+ var RECENCY_BOOST = 1.2;
485
+ var CONFIRMATION_BOOST = 1.3;
486
+ function severityBoost(lesson) {
487
+ switch (lesson.severity) {
488
+ case "high":
489
+ return HIGH_SEVERITY_BOOST;
490
+ case "medium":
491
+ return MEDIUM_SEVERITY_BOOST;
492
+ case "low":
493
+ return LOW_SEVERITY_BOOST;
494
+ default:
495
+ return MEDIUM_SEVERITY_BOOST;
496
+ }
497
+ }
498
+ function recencyBoost(lesson) {
499
+ const created = new Date(lesson.created);
500
+ const now = /* @__PURE__ */ new Date();
501
+ const ageMs = now.getTime() - created.getTime();
502
+ const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
503
+ return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
504
+ }
505
+ function confirmationBoost(lesson) {
506
+ return lesson.confirmed ? CONFIRMATION_BOOST : 1;
507
+ }
508
+ function calculateScore(lesson, vectorSimilarity) {
509
+ return vectorSimilarity * severityBoost(lesson) * recencyBoost(lesson) * confirmationBoost(lesson);
510
+ }
511
+ function rankLessons(lessons) {
512
+ return lessons.map((scored) => ({
513
+ ...scored,
514
+ finalScore: calculateScore(scored.lesson, scored.score)
515
+ })).sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
516
+ }
517
+
518
+ // src/capture/quality.ts
519
+ var DEFAULT_SIMILARITY_THRESHOLD = 0.8;
520
+ async function isNovel(repoRoot, insight, options = {}) {
521
+ const threshold = options.threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
522
+ await syncIfNeeded(repoRoot);
523
+ const words = insight.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
524
+ if (words.length === 0) {
525
+ return { novel: true };
526
+ }
527
+ const searchQuery = words.join(" OR ");
528
+ const results = await searchKeyword(repoRoot, searchQuery, 10);
529
+ if (results.length === 0) {
530
+ return { novel: true };
531
+ }
532
+ const insightWords = new Set(insight.toLowerCase().split(/\s+/));
533
+ for (const lesson of results) {
534
+ const lessonWords = new Set(lesson.insight.toLowerCase().split(/\s+/));
535
+ const intersection = [...insightWords].filter((w) => lessonWords.has(w)).length;
536
+ const union = (/* @__PURE__ */ new Set([...insightWords, ...lessonWords])).size;
537
+ const similarity = union > 0 ? intersection / union : 0;
538
+ if (similarity >= threshold) {
539
+ return {
540
+ novel: false,
541
+ reason: `Found similar existing lesson: "${lesson.insight.slice(0, 50)}..."`,
542
+ existingId: lesson.id
543
+ };
544
+ }
545
+ if (lesson.insight.toLowerCase() === insight.toLowerCase()) {
546
+ return {
547
+ novel: false,
548
+ reason: `Exact duplicate found`,
549
+ existingId: lesson.id
550
+ };
551
+ }
552
+ }
553
+ return { novel: true };
554
+ }
555
+ var MIN_WORD_COUNT = 4;
556
+ var VAGUE_PATTERNS = [
557
+ /\bwrite better\b/i,
558
+ /\bbe careful\b/i,
559
+ /\bremember to\b/i,
560
+ /\bmake sure\b/i,
561
+ /\btry to\b/i,
562
+ /\bdouble check\b/i
563
+ ];
564
+ var GENERIC_IMPERATIVE_PATTERN = /^(always|never)\s+\w+(\s+\w+){0,2}$/i;
565
+ function isSpecific(insight) {
566
+ const words = insight.trim().split(/\s+/).filter((w) => w.length > 0);
567
+ if (words.length < MIN_WORD_COUNT) {
568
+ return { specific: false, reason: "Insight is too short to be actionable" };
569
+ }
570
+ for (const pattern of VAGUE_PATTERNS) {
571
+ if (pattern.test(insight)) {
572
+ return { specific: false, reason: "Insight matches a vague pattern" };
573
+ }
574
+ }
575
+ if (GENERIC_IMPERATIVE_PATTERN.test(insight)) {
576
+ return { specific: false, reason: "Insight matches a vague pattern" };
577
+ }
578
+ return { specific: true };
579
+ }
580
+ var ACTION_PATTERNS = [
581
+ /\buse\s+.+\s+instead\s+of\b/i,
582
+ // "use X instead of Y"
583
+ /\bprefer\s+.+\s+(over|to)\b/i,
584
+ // "prefer X over Y" or "prefer X to Y"
585
+ /\balways\s+.+\s+when\b/i,
586
+ // "always X when Y"
587
+ /\bnever\s+.+\s+without\b/i,
588
+ // "never X without Y"
589
+ /\bavoid\s+(using\s+)?\w+/i,
590
+ // "avoid X" or "avoid using X"
591
+ /\bcheck\s+.+\s+before\b/i,
592
+ // "check X before Y"
593
+ /^(run|use|add|remove|install|update|configure|set|enable|disable)\s+/i
594
+ // Imperative commands at start
595
+ ];
596
+ function isActionable(insight) {
597
+ for (const pattern of ACTION_PATTERNS) {
598
+ if (pattern.test(insight)) {
599
+ return { actionable: true };
600
+ }
601
+ }
602
+ return { actionable: false, reason: "Insight lacks clear action guidance" };
603
+ }
604
+ async function shouldPropose(repoRoot, insight) {
605
+ const specificResult = isSpecific(insight);
606
+ if (!specificResult.specific) {
607
+ return { shouldPropose: false, reason: specificResult.reason };
608
+ }
609
+ const actionableResult = isActionable(insight);
610
+ if (!actionableResult.actionable) {
611
+ return { shouldPropose: false, reason: actionableResult.reason };
612
+ }
613
+ const noveltyResult = await isNovel(repoRoot, insight);
614
+ if (!noveltyResult.novel) {
615
+ return { shouldPropose: false, reason: noveltyResult.reason };
616
+ }
617
+ return { shouldPropose: true };
618
+ }
619
+
620
+ // src/capture/triggers.ts
621
+ var USER_CORRECTION_PATTERNS = [
622
+ /\bno\b[,.]?\s/i,
623
+ // "no, ..." or "no ..."
624
+ /\bwrong\b/i,
625
+ // "wrong"
626
+ /\bactually\b/i,
627
+ // "actually..."
628
+ /\bnot that\b/i,
629
+ // "not that"
630
+ /\bi meant\b/i
631
+ // "I meant"
632
+ ];
633
+ function detectUserCorrection(signals) {
634
+ const { messages, context } = signals;
635
+ if (messages.length < 2) {
636
+ return null;
637
+ }
638
+ for (let i = 1; i < messages.length; i++) {
639
+ const message = messages[i];
640
+ if (!message) continue;
641
+ for (const pattern of USER_CORRECTION_PATTERNS) {
642
+ if (pattern.test(message)) {
643
+ return {
644
+ trigger: `User correction during ${context.intent}`,
645
+ correctionMessage: message,
646
+ context
647
+ };
648
+ }
649
+ }
650
+ }
651
+ return null;
652
+ }
653
+ function detectSelfCorrection(history) {
654
+ const { edits } = history;
655
+ if (edits.length < 3) {
656
+ return null;
657
+ }
658
+ for (let i = 0; i <= edits.length - 3; i++) {
659
+ const first = edits[i];
660
+ const second = edits[i + 1];
661
+ const third = edits[i + 2];
662
+ if (!first || !second || !third) continue;
663
+ if (first.file === second.file && second.file === third.file && first.success && !second.success && third.success) {
664
+ return {
665
+ file: first.file,
666
+ trigger: `Self-correction on ${first.file}`
667
+ };
668
+ }
669
+ }
670
+ return null;
671
+ }
672
+ function detectTestFailure(testResult) {
673
+ if (testResult.passed) {
674
+ return null;
675
+ }
676
+ const lines = testResult.output.split("\n").filter((line) => line.trim().length > 0);
677
+ const errorLine = lines.find((line) => /error|fail|assert/i.test(line)) ?? lines[0] ?? "";
678
+ return {
679
+ testFile: testResult.testFile,
680
+ errorOutput: testResult.output,
681
+ trigger: `Test failure in ${testResult.testFile}: ${errorLine.slice(0, 100)}`
682
+ };
683
+ }
684
+
685
+ // src/retrieval/session.ts
686
+ var DEFAULT_LIMIT2 = 5;
687
+ function isFullLesson(lesson) {
688
+ return lesson.type === "full" && lesson.severity !== void 0;
689
+ }
690
+ async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
691
+ const { lessons: allLessons } = await readLessons(repoRoot);
692
+ const highSeverityLessons = allLessons.filter(
693
+ (lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
694
+ );
695
+ highSeverityLessons.sort((a, b) => {
696
+ const dateA = new Date(a.created).getTime();
697
+ const dateB = new Date(b.created).getTime();
698
+ return dateB - dateA;
699
+ });
700
+ return highSeverityLessons.slice(0, limit);
701
+ }
702
+
703
+ // src/retrieval/plan.ts
704
+ var DEFAULT_LIMIT3 = 5;
705
+ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
706
+ const scored = await searchVector(repoRoot, planText, { limit: limit * 2 });
707
+ const ranked = rankLessons(scored);
708
+ const topLessons = ranked.slice(0, limit);
709
+ const message = formatLessonsCheck(topLessons);
710
+ return { lessons: topLessons, message };
711
+ }
712
+ function formatLessonsCheck(lessons) {
713
+ const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
714
+ if (lessons.length === 0) {
715
+ return `${header}
716
+ No relevant lessons found for this plan.`;
717
+ }
718
+ const lessonLines = lessons.map((l, i) => {
719
+ const bullet = `${i + 1}.`;
720
+ const insight = l.lesson.insight;
721
+ return `${bullet} ${insight}`;
722
+ });
723
+ return `${header}
724
+ ${lessonLines.join("\n")}`;
725
+ }
726
+
727
+ // src/index.ts
728
+ var VERSION = "0.1.0";
729
+
730
+ export { DB_PATH, LESSONS_PATH, LessonSchema, LessonTypeSchema, MODEL_FILENAME, MODEL_URI, TombstoneSchema, VERSION, appendLesson, calculateScore, closeDb, confirmationBoost, cosineSimilarity, detectSelfCorrection, detectTestFailure, detectUserCorrection, embedText, embedTexts, formatLessonsCheck, generateId, getEmbedding, isActionable, isModelAvailable, isNovel, isSpecific, loadSessionLessons, rankLessons, readLessons, rebuildIndex, recencyBoost, resolveModel, retrieveForPlan, searchKeyword, searchVector, severityBoost, shouldPropose, unloadEmbedding };
731
+ //# sourceMappingURL=index.js.map
732
+ //# sourceMappingURL=index.js.map