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/CHANGELOG.md +92 -1
- package/README.md +109 -81
- package/dist/cli.js +2026 -1466
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +66 -45
- package/dist/index.js +214 -83
- package/dist/index.js.map +1 -1
- package/package.json +21 -11
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
|
|
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
|
|
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
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
*
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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.
|
|
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
|