learning-agent 0.2.1 → 0.2.3

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.d.ts CHANGED
@@ -63,6 +63,24 @@ declare const LessonSchema: z.ZodObject<{
63
63
  }>>;
64
64
  deleted: z.ZodOptional<z.ZodBoolean>;
65
65
  retrievalCount: z.ZodOptional<z.ZodNumber>;
66
+ citation: z.ZodOptional<z.ZodObject<{
67
+ file: z.ZodString;
68
+ line: z.ZodOptional<z.ZodNumber>;
69
+ commit: z.ZodOptional<z.ZodString>;
70
+ }, "strip", z.ZodTypeAny, {
71
+ file: string;
72
+ line?: number | undefined;
73
+ commit?: string | undefined;
74
+ }, {
75
+ file: string;
76
+ line?: number | undefined;
77
+ commit?: string | undefined;
78
+ }>>;
79
+ compactionLevel: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
80
+ compactedAt: z.ZodOptional<z.ZodString>;
81
+ lastRetrieved: z.ZodOptional<z.ZodString>;
82
+ invalidatedAt: z.ZodOptional<z.ZodString>;
83
+ invalidationReason: z.ZodOptional<z.ZodString>;
66
84
  }, "strip", z.ZodTypeAny, {
67
85
  type: "quick" | "full";
68
86
  id: string;
@@ -86,6 +104,16 @@ declare const LessonSchema: z.ZodObject<{
86
104
  } | undefined;
87
105
  deleted?: boolean | undefined;
88
106
  retrievalCount?: number | undefined;
107
+ citation?: {
108
+ file: string;
109
+ line?: number | undefined;
110
+ commit?: string | undefined;
111
+ } | undefined;
112
+ compactionLevel?: 0 | 1 | 2 | undefined;
113
+ compactedAt?: string | undefined;
114
+ lastRetrieved?: string | undefined;
115
+ invalidatedAt?: string | undefined;
116
+ invalidationReason?: string | undefined;
89
117
  }, {
90
118
  type: "quick" | "full";
91
119
  id: string;
@@ -109,6 +137,16 @@ declare const LessonSchema: z.ZodObject<{
109
137
  } | undefined;
110
138
  deleted?: boolean | undefined;
111
139
  retrievalCount?: number | undefined;
140
+ citation?: {
141
+ file: string;
142
+ line?: number | undefined;
143
+ commit?: string | undefined;
144
+ } | undefined;
145
+ compactionLevel?: 0 | 1 | 2 | undefined;
146
+ compactedAt?: string | undefined;
147
+ lastRetrieved?: string | undefined;
148
+ invalidatedAt?: string | undefined;
149
+ invalidationReason?: string | undefined;
112
150
  }>;
113
151
  declare const TombstoneSchema: z.ZodObject<{
114
152
  id: z.ZodString;
@@ -184,61 +222,40 @@ declare function appendLesson(repoRoot: string, lesson: Lesson): Promise<void>;
184
222
  declare function readLessons(repoRoot: string, options?: ReadLessonsOptions): Promise<ReadLessonsResult>;
185
223
 
186
224
  /**
187
- * SQLite storage layer with FTS5 for full-text search
188
- *
189
- * Rebuildable index - not the source of truth.
190
- * Stored in .claude/.cache (gitignored).
225
+ * SQLite database connection management.
191
226
  */
192
227
 
193
228
  /** Relative path to database file from repo root */
194
229
  declare const DB_PATH = ".claude/.cache/lessons.sqlite";
195
230
  /**
196
- * Close the database connection and release resources.
197
- *
198
- * **Resource lifecycle:**
199
- * - The database is opened lazily on first call to `openDb()` or any function that uses it
200
- * (e.g., `searchKeyword`, `rebuildIndex`, `syncIfNeeded`, `getCachedEmbedding`)
201
- * - Once opened, the connection remains active until `closeDb()` is called
202
- * - After closing, subsequent database operations will reopen the connection
203
- *
204
- * **When to call:**
205
- * - At the end of CLI commands to ensure clean process exit
206
- * - When transitioning between repositories in long-running processes
207
- * - Before process exit in graceful shutdown handlers
208
- *
209
- * **Best practices for long-running processes:**
210
- * - In single-operation scripts: call before exit
211
- * - In daemon/server processes: call in shutdown handler
212
- * - Not necessary to call between operations in the same repository
213
- *
214
- * @example
215
- * ```typescript
216
- * // CLI command pattern
217
- * try {
218
- * await searchKeyword(repoRoot, 'typescript', 10);
219
- * // ... process results
220
- * } finally {
221
- * closeDb();
222
- * }
223
- *
224
- * // Graceful shutdown pattern
225
- * process.on('SIGTERM', () => {
226
- * closeDb();
227
- * process.exit(0);
228
- * });
229
- * ```
231
+ * Close the SQLite database connection.
230
232
  */
231
233
  declare function closeDb(): void;
234
+
232
235
  /**
233
- * Rebuild the SQLite index from the JSONL source of truth.
234
- * Preserves embeddings where content hash is unchanged.
235
- * Updates the last sync mtime after successful rebuild.
236
+ * SQLite index synchronization with JSONL source of truth.
237
+ */
238
+
239
+ /**
240
+ * Rebuild the SQLite index from JSONL source of truth.
241
+ * Gracefully degrades: no-op with warning if SQLite unavailable.
242
+ * Preserves cached embeddings when lesson content hasn't changed.
243
+ * @param repoRoot - Absolute path to repository root
236
244
  */
237
245
  declare function rebuildIndex(repoRoot: string): Promise<void>;
246
+
238
247
  /**
239
- * Search lessons using FTS5 keyword search.
240
- * Returns matching lessons up to the specified limit.
241
- * Increments retrieval count for all returned lessons.
248
+ * SQLite search operations using FTS5 full-text search.
249
+ */
250
+
251
+ /**
252
+ * Search lessons using FTS5 full-text search.
253
+ * Does NOT degrade gracefully: throws error if SQLite unavailable.
254
+ * @param repoRoot - Absolute path to repository root
255
+ * @param query - FTS5 query string
256
+ * @param limit - Maximum number of results
257
+ * @returns Matching lessons
258
+ * @throws Error if SQLite unavailable (FTS5 required)
242
259
  */
243
260
  declare function searchKeyword(repoRoot: string, query: string, limit: number): Promise<Lesson[]>;
244
261
 
@@ -775,6 +792,10 @@ declare function formatLessonsCheck(lessons: ScoredLesson[]): string;
775
792
  * @see {@link unloadEmbedding} for embedding model cleanup
776
793
  * @module learning-agent
777
794
  */
778
- declare const VERSION = "0.1.0";
795
+ /**
796
+ * Package version - must match package.json.
797
+ * Update this when releasing a new version.
798
+ */
799
+ declare const VERSION = "0.2.3";
779
800
 
780
801
  export { type ActionabilityResult, type Context, type CorrectionSignal, DB_PATH, type DetectedCorrection, type DetectedSelfCorrection, type DetectedTestFailure, type EditEntry, type EditHistory, LESSONS_PATH, type Lesson, LessonSchema, type LessonType, LessonTypeSchema, MODEL_FILENAME, MODEL_URI, type NoveltyOptions, type NoveltyResult, type ParseError, type PlanRetrievalResult, type ProposeResult, type RankedLesson, type ReadLessonsOptions, type ReadLessonsResult, type ScoredLesson, type SearchVectorOptions, type Severity, type Source, type SpecificityResult, type TestResult, type Tombstone, 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 };
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import { mkdir, appendFile, readFile } from 'fs/promises';
2
2
  import { join, dirname } from 'path';
3
3
  import { createHash } from 'crypto';
4
4
  import { z } from 'zod';
5
+ import { createRequire } from 'module';
5
6
  import { existsSync, mkdirSync, statSync } from 'fs';
6
- import Database from 'better-sqlite3';
7
7
  import { resolveModelFile, getLlama } from 'node-llama-cpp';
8
8
  import { homedir } from 'os';
9
9
 
@@ -22,7 +22,23 @@ var PatternSchema = z.object({
22
22
  bad: z.string(),
23
23
  good: z.string()
24
24
  });
25
+ var CitationSchema = z.object({
26
+ file: z.string().min(1),
27
+ // Source file path (required, non-empty)
28
+ line: z.number().int().positive().optional(),
29
+ // Line number (optional, must be positive)
30
+ commit: z.string().optional()
31
+ // Git commit hash (optional)
32
+ });
25
33
  var SeveritySchema = z.enum(["high", "medium", "low"]);
34
+ var CompactionLevelSchema = z.union([
35
+ z.literal(0),
36
+ // Active
37
+ z.literal(1),
38
+ // Flagged (>90 days)
39
+ z.literal(2)
40
+ // Archived
41
+ ]);
26
42
  var LessonTypeSchema = z.enum(["quick", "full"]);
27
43
  var LessonSchema = z.object({
28
44
  // Core identity (required)
@@ -46,7 +62,20 @@ var LessonSchema = z.object({
46
62
  pattern: PatternSchema.optional(),
47
63
  // Lifecycle fields (optional)
48
64
  deleted: z.boolean().optional(),
49
- retrievalCount: z.number().optional()
65
+ retrievalCount: z.number().optional(),
66
+ // Provenance tracking (optional)
67
+ citation: CitationSchema.optional(),
68
+ // Age-based validity fields (optional)
69
+ compactionLevel: CompactionLevelSchema.optional(),
70
+ // 0=active, 1=flagged, 2=archived
71
+ compactedAt: z.string().optional(),
72
+ // ISO8601 when compaction happened
73
+ lastRetrieved: z.string().optional(),
74
+ // ISO8601 last retrieval time
75
+ // Invalidation fields (optional - for marking lessons as wrong)
76
+ invalidatedAt: z.string().optional(),
77
+ // ISO8601
78
+ invalidationReason: z.string().optional()
50
79
  });
51
80
  var TombstoneSchema = z.object({
52
81
  id: z.string(),
@@ -129,9 +158,45 @@ async function readLessons(repoRoot, options = {}) {
129
158
  }
130
159
  return { lessons: Array.from(lessons.values()), skippedCount };
131
160
  }
132
- var DB_PATH = ".claude/.cache/lessons.sqlite";
161
+ var require2 = createRequire(import.meta.url);
162
+ var sqliteAvailable = null;
163
+ var sqliteWarningLogged = false;
164
+ var DatabaseConstructor = null;
165
+ function isSqliteAvailable() {
166
+ if (sqliteAvailable !== null) {
167
+ return sqliteAvailable;
168
+ }
169
+ try {
170
+ const module = require2("better-sqlite3");
171
+ const Constructor = module.default || module;
172
+ const testDb = new Constructor(":memory:");
173
+ testDb.close();
174
+ DatabaseConstructor = Constructor;
175
+ sqliteAvailable = true;
176
+ } catch {
177
+ sqliteAvailable = false;
178
+ if (!sqliteWarningLogged) {
179
+ console.warn("SQLite unavailable, running in JSONL-only mode");
180
+ sqliteWarningLogged = true;
181
+ }
182
+ }
183
+ return sqliteAvailable;
184
+ }
185
+ function logDegradationWarning() {
186
+ if (!sqliteAvailable && !sqliteWarningLogged) {
187
+ console.warn("SQLite unavailable, running in JSONL-only mode");
188
+ sqliteWarningLogged = true;
189
+ }
190
+ }
191
+ function getDatabaseConstructor() {
192
+ if (!isSqliteAvailable()) {
193
+ return null;
194
+ }
195
+ return DatabaseConstructor;
196
+ }
197
+
198
+ // src/storage/sqlite/schema.ts
133
199
  var SCHEMA_SQL = `
134
- -- Main lessons table
135
200
  CREATE TABLE IF NOT EXISTS lessons (
136
201
  id TEXT PRIMARY KEY,
137
202
  type TEXT NOT NULL,
@@ -150,32 +215,31 @@ var SCHEMA_SQL = `
150
215
  retrieval_count INTEGER NOT NULL DEFAULT 0,
151
216
  last_retrieved TEXT,
152
217
  embedding BLOB,
153
- content_hash TEXT
218
+ content_hash TEXT,
219
+ invalidated_at TEXT,
220
+ invalidation_reason TEXT,
221
+ citation_file TEXT,
222
+ citation_line INTEGER,
223
+ citation_commit TEXT,
224
+ compaction_level INTEGER DEFAULT 0,
225
+ compacted_at TEXT
154
226
  );
155
227
 
156
- -- FTS5 virtual table for full-text search
157
228
  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'
229
+ id, trigger, insight, tags,
230
+ content='lessons', content_rowid='rowid'
164
231
  );
165
232
 
166
- -- Trigger to sync FTS on INSERT
167
233
  CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
168
234
  INSERT INTO lessons_fts(rowid, id, trigger, insight, tags)
169
235
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
170
236
  END;
171
237
 
172
- -- Trigger to sync FTS on DELETE
173
238
  CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
174
239
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
175
240
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
176
241
  END;
177
242
 
178
- -- Trigger to sync FTS on UPDATE
179
243
  CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
180
244
  INSERT INTO lessons_fts(lessons_fts, rowid, id, trigger, insight, tags)
181
245
  VALUES ('delete', old.rowid, old.id, old.trigger, old.insight, old.tags);
@@ -183,12 +247,10 @@ var SCHEMA_SQL = `
183
247
  VALUES (new.rowid, new.id, new.trigger, new.insight, new.tags);
184
248
  END;
185
249
 
186
- -- Index for common queries
187
250
  CREATE INDEX IF NOT EXISTS idx_lessons_created ON lessons(created);
188
251
  CREATE INDEX IF NOT EXISTS idx_lessons_confirmed ON lessons(confirmed);
189
252
  CREATE INDEX IF NOT EXISTS idx_lessons_severity ON lessons(severity);
190
253
 
191
- -- Metadata table for sync tracking
192
254
  CREATE TABLE IF NOT EXISTS metadata (
193
255
  key TEXT PRIMARY KEY,
194
256
  value TEXT NOT NULL
@@ -197,17 +259,35 @@ var SCHEMA_SQL = `
197
259
  function createSchema(database) {
198
260
  database.exec(SCHEMA_SQL);
199
261
  }
262
+
263
+ // src/storage/sqlite/connection.ts
264
+ var DB_PATH = ".claude/.cache/lessons.sqlite";
200
265
  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");
266
+ var dbIsInMemory = false;
267
+ function openDb(repoRoot, options = {}) {
268
+ if (!isSqliteAvailable()) {
269
+ return null;
270
+ }
271
+ const { inMemory = false } = options;
272
+ if (db) {
273
+ if (inMemory !== dbIsInMemory) {
274
+ closeDb();
275
+ } else {
276
+ return db;
277
+ }
278
+ }
279
+ const Database = getDatabaseConstructor();
280
+ if (inMemory) {
281
+ db = new Database(":memory:");
282
+ dbIsInMemory = true;
283
+ } else {
284
+ const dbPath = join(repoRoot, DB_PATH);
285
+ const dir = dirname(dbPath);
286
+ mkdirSync(dir, { recursive: true });
287
+ db = new Database(dbPath);
288
+ dbIsInMemory = false;
289
+ db.pragma("journal_mode = WAL");
290
+ }
211
291
  createSchema(db);
212
292
  return db;
213
293
  }
@@ -215,10 +295,18 @@ function closeDb() {
215
295
  if (db) {
216
296
  db.close();
217
297
  db = null;
298
+ dbIsInMemory = false;
218
299
  }
219
300
  }
301
+ function contentHash(trigger, insight) {
302
+ return createHash("sha256").update(`${trigger} ${insight}`).digest("hex");
303
+ }
220
304
  function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
221
305
  const database = openDb(repoRoot);
306
+ if (!database) {
307
+ logDegradationWarning();
308
+ return null;
309
+ }
222
310
  const row = database.prepare("SELECT embedding, content_hash FROM lessons WHERE id = ?").get(lessonId);
223
311
  if (!row || !row.embedding || !row.content_hash) {
224
312
  return null;
@@ -235,38 +323,14 @@ function getCachedEmbedding(repoRoot, lessonId, expectedHash) {
235
323
  }
236
324
  function setCachedEmbedding(repoRoot, lessonId, embedding, hash) {
237
325
  const database = openDb(repoRoot);
326
+ if (!database) {
327
+ logDegradationWarning();
328
+ return;
329
+ }
238
330
  const float32 = embedding instanceof Float32Array ? embedding : new Float32Array(embedding);
239
331
  const buffer = Buffer.from(float32.buffer, float32.byteOffset, float32.byteLength);
240
332
  database.prepare("UPDATE lessons SET embedding = ?, content_hash = ? WHERE id = ?").run(buffer, hash, lessonId);
241
333
  }
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
334
  function collectCachedEmbeddings(database) {
271
335
  const cache = /* @__PURE__ */ new Map();
272
336
  const rows = database.prepare("SELECT id, embedding, content_hash FROM lessons WHERE embedding IS NOT NULL").all();
@@ -278,8 +342,8 @@ function collectCachedEmbeddings(database) {
278
342
  return cache;
279
343
  }
280
344
  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)
345
+ INSERT INTO lessons (id, type, trigger, insight, evidence, severity, tags, source, context, supersedes, related, created, confirmed, deleted, retrieval_count, last_retrieved, embedding, content_hash, invalidated_at, invalidation_reason, citation_file, citation_line, citation_commit, compaction_level, compacted_at)
346
+ VALUES (@id, @type, @trigger, @insight, @evidence, @severity, @tags, @source, @context, @supersedes, @related, @created, @confirmed, @deleted, @retrieval_count, @last_retrieved, @embedding, @content_hash, @invalidated_at, @invalidation_reason, @citation_file, @citation_line, @citation_commit, @compaction_level, @compacted_at)
283
347
  `;
284
348
  function getJsonlMtime(repoRoot) {
285
349
  const jsonlPath = join(repoRoot, LESSONS_PATH);
@@ -299,6 +363,10 @@ function setLastSyncMtime(database, mtime) {
299
363
  }
300
364
  async function rebuildIndex(repoRoot) {
301
365
  const database = openDb(repoRoot);
366
+ if (!database) {
367
+ logDegradationWarning();
368
+ return;
369
+ }
302
370
  const { lessons } = await readLessons(repoRoot);
303
371
  const cachedEmbeddings = collectCachedEmbeddings(database);
304
372
  database.exec("DELETE FROM lessons");
@@ -331,10 +399,16 @@ async function rebuildIndex(repoRoot) {
331
399
  confirmed: lesson.confirmed ? 1 : 0,
332
400
  deleted: lesson.deleted ? 1 : 0,
333
401
  retrieval_count: lesson.retrievalCount ?? 0,
334
- last_retrieved: null,
335
- // Reset on rebuild since we're rebuilding from source
402
+ last_retrieved: lesson.lastRetrieved ?? null,
336
403
  embedding: hasValidCache ? cached.embedding : null,
337
- content_hash: hasValidCache ? cached.contentHash : null
404
+ content_hash: hasValidCache ? cached.contentHash : null,
405
+ invalidated_at: lesson.invalidatedAt ?? null,
406
+ invalidation_reason: lesson.invalidationReason ?? null,
407
+ citation_file: lesson.citation?.file ?? null,
408
+ citation_line: lesson.citation?.line ?? null,
409
+ citation_commit: lesson.citation?.commit ?? null,
410
+ compaction_level: lesson.compactionLevel ?? 0,
411
+ compacted_at: lesson.compactedAt ?? null
338
412
  });
339
413
  }
340
414
  });
@@ -345,12 +419,17 @@ async function rebuildIndex(repoRoot) {
345
419
  }
346
420
  }
347
421
  async function syncIfNeeded(repoRoot, options = {}) {
422
+ if (!isSqliteAvailable()) {
423
+ logDegradationWarning();
424
+ return false;
425
+ }
348
426
  const { force = false } = options;
349
427
  const jsonlMtime = getJsonlMtime(repoRoot);
350
428
  if (jsonlMtime === null && !force) {
351
429
  return false;
352
430
  }
353
431
  const database = openDb(repoRoot);
432
+ if (!database) return false;
354
433
  const lastSyncMtime = getLastSyncMtime(database);
355
434
  const needsRebuild = force || lastSyncMtime === null || jsonlMtime !== null && jsonlMtime > lastSyncMtime;
356
435
  if (needsRebuild) {
@@ -359,27 +438,49 @@ async function syncIfNeeded(repoRoot, options = {}) {
359
438
  }
360
439
  return false;
361
440
  }
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));
441
+
442
+ // src/storage/sqlite/search.ts
443
+ function rowToLesson(row) {
444
+ const lesson = {
445
+ id: row.id,
446
+ type: row.type,
447
+ trigger: row.trigger,
448
+ insight: row.insight,
449
+ tags: row.tags ? row.tags.split(",").filter(Boolean) : [],
450
+ source: row.source,
451
+ context: JSON.parse(row.context),
452
+ supersedes: JSON.parse(row.supersedes),
453
+ related: JSON.parse(row.related),
454
+ created: row.created,
455
+ confirmed: row.confirmed === 1
456
+ };
457
+ if (row.evidence !== null) lesson.evidence = row.evidence;
458
+ if (row.severity !== null) lesson.severity = row.severity;
459
+ if (row.deleted === 1) lesson.deleted = true;
460
+ if (row.retrieval_count > 0) lesson.retrievalCount = row.retrieval_count;
461
+ if (row.invalidated_at !== null) lesson.invalidatedAt = row.invalidated_at;
462
+ if (row.invalidation_reason !== null) lesson.invalidationReason = row.invalidation_reason;
463
+ if (row.citation_file !== null) {
464
+ lesson.citation = {
465
+ file: row.citation_file,
466
+ ...row.citation_line !== null && { line: row.citation_line },
467
+ ...row.citation_commit !== null && { commit: row.citation_commit }
468
+ };
377
469
  }
378
- return rows.map(rowToLesson);
470
+ if (row.compaction_level !== null && row.compaction_level !== 0) {
471
+ lesson.compactionLevel = row.compaction_level;
472
+ }
473
+ if (row.compacted_at !== null) lesson.compactedAt = row.compacted_at;
474
+ if (row.last_retrieved !== null) lesson.lastRetrieved = row.last_retrieved;
475
+ return lesson;
379
476
  }
380
477
  function incrementRetrievalCount(repoRoot, lessonIds) {
381
478
  if (lessonIds.length === 0) return;
382
479
  const database = openDb(repoRoot);
480
+ if (!database) {
481
+ logDegradationWarning();
482
+ return;
483
+ }
383
484
  const now = (/* @__PURE__ */ new Date()).toISOString();
384
485
  const update = database.prepare(`
385
486
  UPDATE lessons
@@ -394,6 +495,38 @@ function incrementRetrievalCount(repoRoot, lessonIds) {
394
495
  });
395
496
  updateMany(lessonIds);
396
497
  }
498
+ async function searchKeyword(repoRoot, query, limit) {
499
+ const database = openDb(repoRoot);
500
+ if (!database) {
501
+ throw new Error(
502
+ "Keyword search requires SQLite (FTS5 required). Install native build tools or use vector search instead."
503
+ );
504
+ }
505
+ const countResult = database.prepare("SELECT COUNT(*) as cnt FROM lessons").get();
506
+ if (countResult.cnt === 0) return [];
507
+ const rows = database.prepare(
508
+ `
509
+ SELECT l.*
510
+ FROM lessons l
511
+ JOIN lessons_fts fts ON l.rowid = fts.rowid
512
+ WHERE lessons_fts MATCH ?
513
+ AND l.invalidated_at IS NULL
514
+ LIMIT ?
515
+ `
516
+ ).all(query, limit);
517
+ if (rows.length > 0) {
518
+ incrementRetrievalCount(repoRoot, rows.map((r) => r.id));
519
+ }
520
+ return rows.map(rowToLesson);
521
+ }
522
+
523
+ // src/utils.ts
524
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
525
+ function getLessonAgeDays(lesson) {
526
+ const created = new Date(lesson.created).getTime();
527
+ const now = Date.now();
528
+ return Math.floor((now - created) / MS_PER_DAY);
529
+ }
397
530
  var MODEL_URI = "hf:ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf";
398
531
  var MODEL_FILENAME = "hf_ggml-org_embeddinggemma-300M-qat-Q4_0.gguf";
399
532
  var DEFAULT_MODEL_DIR = join(homedir(), ".node-llama-cpp", "models");
@@ -462,6 +595,7 @@ async function searchVector(repoRoot, query, options) {
462
595
  const queryVector = await embedText(query);
463
596
  const scored = [];
464
597
  for (const lesson of lessons) {
598
+ if (lesson.invalidatedAt) continue;
465
599
  const lessonText = `${lesson.trigger} ${lesson.insight}`;
466
600
  const hash = contentHash(lesson.trigger, lesson.insight);
467
601
  let lessonVector = getCachedEmbedding(repoRoot, lesson.id, hash);
@@ -496,10 +630,7 @@ function severityBoost(lesson) {
496
630
  }
497
631
  }
498
632
  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));
633
+ const ageDays = getLessonAgeDays(lesson);
503
634
  return ageDays <= RECENCY_THRESHOLD_DAYS ? RECENCY_BOOST : 1;
504
635
  }
505
636
  function confirmationBoost(lesson) {
@@ -690,7 +821,7 @@ function isFullLesson(lesson) {
690
821
  async function loadSessionLessons(repoRoot, limit = DEFAULT_LIMIT2) {
691
822
  const { lessons: allLessons } = await readLessons(repoRoot);
692
823
  const highSeverityLessons = allLessons.filter(
693
- (lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed
824
+ (lesson) => isFullLesson(lesson) && lesson.severity === "high" && lesson.confirmed && !lesson.invalidatedAt
694
825
  );
695
826
  highSeverityLessons.sort((a, b) => {
696
827
  const dateA = new Date(a.created).getTime();
@@ -710,7 +841,7 @@ async function retrieveForPlan(repoRoot, planText, limit = DEFAULT_LIMIT3) {
710
841
  return { lessons: topLessons, message };
711
842
  }
712
843
  function formatLessonsCheck(lessons) {
713
- const header = "\u{1F4DA} Lessons Check\n" + "\u2500".repeat(40);
844
+ const header = "Lessons Check\n" + "\u2500".repeat(40);
714
845
  if (lessons.length === 0) {
715
846
  return `${header}
716
847
  No relevant lessons found for this plan.`;
@@ -725,7 +856,7 @@ ${lessonLines.join("\n")}`;
725
856
  }
726
857
 
727
858
  // src/index.ts
728
- var VERSION = "0.1.0";
859
+ var VERSION = "0.2.3";
729
860
 
730
861
  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
862
  //# sourceMappingURL=index.js.map