claude-brain 0.17.13 → 0.22.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/VERSION +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +80 -104
- package/src/cli/auto-setup.ts +1 -9
- package/src/cli/bin.ts +23 -2
- package/src/cli/commands/export.ts +130 -0
- package/src/cli/commands/reindex.ts +107 -0
- package/src/cli/commands/serve.ts +54 -0
- package/src/cli/commands/status.ts +158 -0
- package/src/code-intelligence/indexer.ts +315 -0
- package/src/code-intelligence/linker.ts +178 -0
- package/src/code-intelligence/parser.ts +484 -0
- package/src/code-intelligence/query.ts +291 -0
- package/src/code-intelligence/schema.ts +83 -0
- package/src/code-intelligence/types.ts +95 -0
- package/src/config/defaults.ts +3 -3
- package/src/config/loader.ts +6 -0
- package/src/config/schema.ts +28 -2
- package/src/health/index.ts +5 -2
- package/src/hooks/brain-hook.ts +4 -1
- package/src/hooks/context-hook.ts +69 -10
- package/src/hooks/installer.ts +4 -7
- package/src/intelligence/cross-project/index.ts +1 -7
- package/src/intelligence/prediction/index.ts +1 -7
- package/src/intelligence/reasoning/index.ts +1 -7
- package/src/memory/compression.ts +105 -0
- package/src/memory/fts5-search.ts +456 -0
- package/src/memory/index.ts +342 -38
- package/src/memory/migrations/add-fts5.ts +98 -0
- package/src/memory/pruning.ts +60 -0
- package/src/routing/intent-classifier.ts +58 -1
- package/src/routing/response-filter.ts +128 -0
- package/src/routing/router.ts +457 -54
- package/src/server/http-api.ts +319 -1
- package/src/server/providers/resources.ts +1 -42
- package/src/server/services.ts +113 -12
- package/src/server/web-viewer.ts +1115 -0
- package/src/setup/index.ts +12 -22
- package/src/tools/schemas.ts +1 -1
- package/src/intelligence/cross-project/affinity.ts +0 -159
- package/src/intelligence/cross-project/transfer.ts +0 -201
- package/src/intelligence/prediction/context-anticipator.ts +0 -198
- package/src/intelligence/prediction/decision-predictor.ts +0 -184
- package/src/intelligence/reasoning/counterfactual.ts +0 -248
- package/src/intelligence/reasoning/synthesizer.ts +0 -167
- package/src/setup/wizard.ts +0 -459
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FTS5 Search Service — Phase 26
|
|
3
|
+
* Full-text search over the observations table using SQLite FTS5.
|
|
4
|
+
* Replaces ChromaDB as the primary search backend.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from 'crypto'
|
|
8
|
+
import type { Database } from 'bun:sqlite'
|
|
9
|
+
import type { Logger } from 'pino'
|
|
10
|
+
|
|
11
|
+
export type ObservationCategory = 'decision' | 'pattern' | 'correction' | 'insight' | 'preference'
|
|
12
|
+
|
|
13
|
+
export interface NewObservation {
|
|
14
|
+
project: string
|
|
15
|
+
category: ObservationCategory
|
|
16
|
+
content: string
|
|
17
|
+
reasoning?: string
|
|
18
|
+
context?: string
|
|
19
|
+
confidence?: number
|
|
20
|
+
source?: string
|
|
21
|
+
tags?: string[]
|
|
22
|
+
file_paths?: string[]
|
|
23
|
+
symbols?: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ObservationResult {
|
|
27
|
+
id: string
|
|
28
|
+
project: string
|
|
29
|
+
category: ObservationCategory
|
|
30
|
+
content: string
|
|
31
|
+
reasoning: string | null
|
|
32
|
+
context: string | null
|
|
33
|
+
confidence: number
|
|
34
|
+
source: string
|
|
35
|
+
tags: string[]
|
|
36
|
+
file_paths: string[]
|
|
37
|
+
symbols: string[]
|
|
38
|
+
access_count: number
|
|
39
|
+
last_accessed: string | null
|
|
40
|
+
created_at: string
|
|
41
|
+
updated_at: string
|
|
42
|
+
archived: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ScoredResult extends ObservationResult {
|
|
46
|
+
score: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DuplicateResult {
|
|
50
|
+
id: string
|
|
51
|
+
content: string
|
|
52
|
+
score: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class FTS5Search {
|
|
56
|
+
private db: Database
|
|
57
|
+
private logger: Logger
|
|
58
|
+
|
|
59
|
+
constructor(db: Database, logger: Logger) {
|
|
60
|
+
this.db = db
|
|
61
|
+
this.logger = logger.child({ component: 'fts5-search' })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Search observations via FTS5 full-text search.
|
|
66
|
+
*/
|
|
67
|
+
search(query: string, project?: string, limit: number = 10): ObservationResult[] {
|
|
68
|
+
if (!query.trim()) return []
|
|
69
|
+
|
|
70
|
+
const ftsQuery = this.buildFTSQuery(query)
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
let sql: string
|
|
74
|
+
const params: any[] = []
|
|
75
|
+
|
|
76
|
+
if (project) {
|
|
77
|
+
sql = `
|
|
78
|
+
SELECT o.*, rank
|
|
79
|
+
FROM observations o
|
|
80
|
+
JOIN observations_fts fts ON o.rowid = fts.rowid
|
|
81
|
+
WHERE observations_fts MATCH ? AND o.project = ? AND o.archived = 0
|
|
82
|
+
ORDER BY rank
|
|
83
|
+
LIMIT ?
|
|
84
|
+
`
|
|
85
|
+
params.push(ftsQuery, project, limit)
|
|
86
|
+
} else {
|
|
87
|
+
sql = `
|
|
88
|
+
SELECT o.*, rank
|
|
89
|
+
FROM observations o
|
|
90
|
+
JOIN observations_fts fts ON o.rowid = fts.rowid
|
|
91
|
+
WHERE observations_fts MATCH ? AND o.archived = 0
|
|
92
|
+
ORDER BY rank
|
|
93
|
+
LIMIT ?
|
|
94
|
+
`
|
|
95
|
+
params.push(ftsQuery, limit)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rows = this.db.prepare(sql).all(...params) as any[]
|
|
99
|
+
return rows.map(row => this.rowToResult(row))
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.logger.warn({ error, query, ftsQuery }, 'FTS5 search failed, trying fallback')
|
|
102
|
+
return this.fallbackSearch(query, project, limit)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Search with BM25 ranking and confidence scoring.
|
|
108
|
+
* Returns results with a normalized score between 0 and 1.
|
|
109
|
+
*/
|
|
110
|
+
searchWithConfidence(query: string, project?: string, limit: number = 10): ScoredResult[] {
|
|
111
|
+
if (!query.trim()) return []
|
|
112
|
+
|
|
113
|
+
const ftsQuery = this.buildFTSQuery(query)
|
|
114
|
+
const queryLower = query.toLowerCase()
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
let sql: string
|
|
118
|
+
const params: any[] = []
|
|
119
|
+
|
|
120
|
+
if (project) {
|
|
121
|
+
sql = `
|
|
122
|
+
SELECT o.*, bm25(observations_fts) as bm25_score
|
|
123
|
+
FROM observations o
|
|
124
|
+
JOIN observations_fts fts ON o.rowid = fts.rowid
|
|
125
|
+
WHERE observations_fts MATCH ? AND o.project = ? AND o.archived = 0
|
|
126
|
+
ORDER BY bm25_score
|
|
127
|
+
LIMIT ?
|
|
128
|
+
`
|
|
129
|
+
params.push(ftsQuery, project, limit)
|
|
130
|
+
} else {
|
|
131
|
+
sql = `
|
|
132
|
+
SELECT o.*, bm25(observations_fts) as bm25_score
|
|
133
|
+
FROM observations o
|
|
134
|
+
JOIN observations_fts fts ON o.rowid = fts.rowid
|
|
135
|
+
WHERE observations_fts MATCH ? AND o.archived = 0
|
|
136
|
+
ORDER BY bm25_score
|
|
137
|
+
LIMIT ?
|
|
138
|
+
`
|
|
139
|
+
params.push(ftsQuery, limit)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rows = this.db.prepare(sql).all(...params) as any[]
|
|
143
|
+
|
|
144
|
+
return rows.map(row => {
|
|
145
|
+
const result = this.rowToResult(row)
|
|
146
|
+
const bm25 = Math.abs(row.bm25_score as number)
|
|
147
|
+
|
|
148
|
+
// Compute confidence score
|
|
149
|
+
let score = this.normalizeBM25(bm25)
|
|
150
|
+
|
|
151
|
+
// Boost for exact content match
|
|
152
|
+
if (result.content.toLowerCase().includes(queryLower)) {
|
|
153
|
+
score = Math.min(1.0, score + 0.15)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Boost for tag match
|
|
157
|
+
if (result.tags.some(t => queryLower.includes(t.toLowerCase()))) {
|
|
158
|
+
score = Math.min(1.0, score + 0.1)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Boost for project match
|
|
162
|
+
if (project && result.project === project) {
|
|
163
|
+
score = Math.min(1.0, score + 0.05)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { ...result, score }
|
|
167
|
+
}).sort((a, b) => b.score - a.score)
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.logger.warn({ error, query, ftsQuery }, 'FTS5 confidence search failed, trying fallback')
|
|
170
|
+
return this.fallbackSearch(query, project, limit).map(r => ({ ...r, score: 0.5 }))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Store a new observation. Returns the generated ID.
|
|
176
|
+
*/
|
|
177
|
+
store(observation: NewObservation): string {
|
|
178
|
+
const id = randomUUID()
|
|
179
|
+
const now = new Date().toISOString()
|
|
180
|
+
|
|
181
|
+
const stmt = this.db.prepare(`
|
|
182
|
+
INSERT INTO observations (id, project, category, content, reasoning, context, confidence, source, tags, file_paths, symbols, access_count, last_accessed, created_at, updated_at, archived)
|
|
183
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, 0)
|
|
184
|
+
`)
|
|
185
|
+
|
|
186
|
+
stmt.run(
|
|
187
|
+
id,
|
|
188
|
+
observation.project,
|
|
189
|
+
observation.category,
|
|
190
|
+
observation.content,
|
|
191
|
+
observation.reasoning || null,
|
|
192
|
+
observation.context || null,
|
|
193
|
+
observation.confidence ?? 0.8,
|
|
194
|
+
observation.source || 'explicit',
|
|
195
|
+
observation.tags ? JSON.stringify(observation.tags) : null,
|
|
196
|
+
observation.file_paths ? JSON.stringify(observation.file_paths) : null,
|
|
197
|
+
observation.symbols ? JSON.stringify(observation.symbols) : null,
|
|
198
|
+
now,
|
|
199
|
+
now
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
this.logger.debug({ id, category: observation.category, project: observation.project }, 'Observation stored')
|
|
203
|
+
return id
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check for duplicates via FTS5 text similarity.
|
|
208
|
+
* Returns the best matching duplicate if above threshold, null otherwise.
|
|
209
|
+
*/
|
|
210
|
+
searchForDuplicates(content: string, project: string, threshold: number = 0.85): DuplicateResult | null {
|
|
211
|
+
const ftsQuery = this.buildFTSQuery(content)
|
|
212
|
+
if (!ftsQuery.trim()) return null
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const rows = this.db.prepare(`
|
|
216
|
+
SELECT o.id, o.content, bm25(observations_fts) as bm25_score
|
|
217
|
+
FROM observations o
|
|
218
|
+
JOIN observations_fts fts ON o.rowid = fts.rowid
|
|
219
|
+
WHERE observations_fts MATCH ? AND o.project = ? AND o.archived = 0
|
|
220
|
+
ORDER BY bm25_score
|
|
221
|
+
LIMIT 3
|
|
222
|
+
`).all(ftsQuery, project) as any[]
|
|
223
|
+
|
|
224
|
+
if (rows.length === 0) return null
|
|
225
|
+
|
|
226
|
+
// Use word overlap as a proxy for semantic similarity
|
|
227
|
+
for (const row of rows) {
|
|
228
|
+
const similarity = this.wordOverlap(content, row.content as string)
|
|
229
|
+
if (similarity >= threshold) {
|
|
230
|
+
return {
|
|
231
|
+
id: row.id as string,
|
|
232
|
+
content: row.content as string,
|
|
233
|
+
score: similarity
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.logger.warn({ error }, 'Duplicate check failed')
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Fetch all observations for a project, optionally filtered by category.
|
|
247
|
+
*/
|
|
248
|
+
fetchAll(project: string, category?: ObservationCategory): ObservationResult[] {
|
|
249
|
+
let sql: string
|
|
250
|
+
const params: any[] = [project]
|
|
251
|
+
|
|
252
|
+
if (category) {
|
|
253
|
+
sql = `SELECT * FROM observations WHERE project = ? AND category = ? AND archived = 0 ORDER BY created_at DESC`
|
|
254
|
+
params.push(category)
|
|
255
|
+
} else {
|
|
256
|
+
sql = `SELECT * FROM observations WHERE project = ? AND archived = 0 ORDER BY created_at DESC`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const rows = this.db.prepare(sql).all(...params) as any[]
|
|
260
|
+
return rows.map(row => this.rowToResult(row))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Update an observation's fields.
|
|
265
|
+
*/
|
|
266
|
+
update(id: string, updates: Partial<NewObservation>): void {
|
|
267
|
+
const fields: string[] = []
|
|
268
|
+
const values: any[] = []
|
|
269
|
+
|
|
270
|
+
if (updates.content !== undefined) { fields.push('content = ?'); values.push(updates.content) }
|
|
271
|
+
if (updates.reasoning !== undefined) { fields.push('reasoning = ?'); values.push(updates.reasoning) }
|
|
272
|
+
if (updates.context !== undefined) { fields.push('context = ?'); values.push(updates.context) }
|
|
273
|
+
if (updates.confidence !== undefined) { fields.push('confidence = ?'); values.push(updates.confidence) }
|
|
274
|
+
if (updates.source !== undefined) { fields.push('source = ?'); values.push(updates.source) }
|
|
275
|
+
if (updates.tags !== undefined) { fields.push('tags = ?'); values.push(JSON.stringify(updates.tags)) }
|
|
276
|
+
if (updates.file_paths !== undefined) { fields.push('file_paths = ?'); values.push(JSON.stringify(updates.file_paths)) }
|
|
277
|
+
if (updates.symbols !== undefined) { fields.push('symbols = ?'); values.push(JSON.stringify(updates.symbols)) }
|
|
278
|
+
|
|
279
|
+
if (fields.length === 0) return
|
|
280
|
+
|
|
281
|
+
fields.push('updated_at = ?')
|
|
282
|
+
values.push(new Date().toISOString())
|
|
283
|
+
values.push(id)
|
|
284
|
+
|
|
285
|
+
this.db.prepare(`UPDATE observations SET ${fields.join(', ')} WHERE id = ?`).run(...values)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Delete an observation by ID.
|
|
290
|
+
*/
|
|
291
|
+
delete(id: string): void {
|
|
292
|
+
this.db.prepare('DELETE FROM observations WHERE id = ?').run(id)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Record an access to bump access_count and last_accessed.
|
|
297
|
+
*/
|
|
298
|
+
recordAccess(id: string): void {
|
|
299
|
+
this.db.prepare(`
|
|
300
|
+
UPDATE observations SET access_count = access_count + 1, last_accessed = ? WHERE id = ?
|
|
301
|
+
`).run(new Date().toISOString(), id)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get a single observation by ID.
|
|
306
|
+
*/
|
|
307
|
+
getById(id: string): ObservationResult | null {
|
|
308
|
+
const row = this.db.prepare('SELECT * FROM observations WHERE id = ?').get(id) as any
|
|
309
|
+
if (!row) return null
|
|
310
|
+
return this.rowToResult(row)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Phase 27: Fetch observations in a date range for a project.
|
|
315
|
+
* Ordered by created_at descending (most recent first).
|
|
316
|
+
*/
|
|
317
|
+
fetchByTimeRange(project: string, start: Date, end: Date, limit: number = 50): ObservationResult[] {
|
|
318
|
+
const startStr = start.toISOString()
|
|
319
|
+
const endStr = end.toISOString()
|
|
320
|
+
|
|
321
|
+
const rows = this.db.prepare(`
|
|
322
|
+
SELECT * FROM observations
|
|
323
|
+
WHERE project = ? AND archived = 0
|
|
324
|
+
AND created_at >= ? AND created_at <= ?
|
|
325
|
+
ORDER BY created_at DESC
|
|
326
|
+
LIMIT ?
|
|
327
|
+
`).all(project, startStr, endStr, limit) as any[]
|
|
328
|
+
|
|
329
|
+
return rows.map(row => this.rowToResult(row))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Phase 27: Increment access count and update last_accessed timestamp.
|
|
334
|
+
* Alias for recordAccess for API consistency.
|
|
335
|
+
*/
|
|
336
|
+
incrementAccess(id: string): void {
|
|
337
|
+
this.recordAccess(id)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- Private helpers ---
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Build an FTS5 query from a natural language query.
|
|
344
|
+
* Tokenizes words and joins with OR for broad matching.
|
|
345
|
+
*/
|
|
346
|
+
private buildFTSQuery(query: string): string {
|
|
347
|
+
// Remove special FTS5 characters that could cause syntax errors
|
|
348
|
+
const cleaned = query.replace(/[":*^~(){}[\]]/g, ' ').trim()
|
|
349
|
+
if (!cleaned) return ''
|
|
350
|
+
|
|
351
|
+
// Split into words, filter short/stop words
|
|
352
|
+
const words = cleaned.split(/\s+/).filter(w => w.length >= 2)
|
|
353
|
+
if (words.length === 0) return ''
|
|
354
|
+
|
|
355
|
+
// Join with OR for broad matching
|
|
356
|
+
return words.map(w => `"${w}"`).join(' OR ')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Normalize BM25 score to 0-1 range.
|
|
361
|
+
* BM25 returns negative scores in SQLite (lower = better match).
|
|
362
|
+
* Typical range: -20 (excellent) to 0 (poor).
|
|
363
|
+
*/
|
|
364
|
+
private normalizeBM25(score: number): number {
|
|
365
|
+
// Map typical BM25 range to 0.5-0.85
|
|
366
|
+
const normalized = Math.min(1, Math.max(0, score / 20))
|
|
367
|
+
return 0.5 + normalized * 0.35
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Compute word overlap between two strings as a similarity proxy.
|
|
372
|
+
* Returns a value between 0 and 1.
|
|
373
|
+
*/
|
|
374
|
+
private wordOverlap(a: string, b: string): number {
|
|
375
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length >= 3))
|
|
376
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length >= 3))
|
|
377
|
+
|
|
378
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0
|
|
379
|
+
|
|
380
|
+
let overlap = 0
|
|
381
|
+
for (const word of wordsA) {
|
|
382
|
+
if (wordsB.has(word)) overlap++
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Jaccard similarity
|
|
386
|
+
const union = new Set([...wordsA, ...wordsB]).size
|
|
387
|
+
return overlap / union
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Fallback LIKE-based search when FTS5 query syntax fails.
|
|
392
|
+
*/
|
|
393
|
+
private fallbackSearch(query: string, project?: string, limit: number = 10): ObservationResult[] {
|
|
394
|
+
const pattern = `%${query}%`
|
|
395
|
+
|
|
396
|
+
let sql: string
|
|
397
|
+
const params: any[] = []
|
|
398
|
+
|
|
399
|
+
if (project) {
|
|
400
|
+
sql = `
|
|
401
|
+
SELECT * FROM observations
|
|
402
|
+
WHERE (content LIKE ? OR reasoning LIKE ? OR context LIKE ? OR tags LIKE ?)
|
|
403
|
+
AND project = ? AND archived = 0
|
|
404
|
+
ORDER BY created_at DESC
|
|
405
|
+
LIMIT ?
|
|
406
|
+
`
|
|
407
|
+
params.push(pattern, pattern, pattern, pattern, project, limit)
|
|
408
|
+
} else {
|
|
409
|
+
sql = `
|
|
410
|
+
SELECT * FROM observations
|
|
411
|
+
WHERE (content LIKE ? OR reasoning LIKE ? OR context LIKE ? OR tags LIKE ?)
|
|
412
|
+
AND archived = 0
|
|
413
|
+
ORDER BY created_at DESC
|
|
414
|
+
LIMIT ?
|
|
415
|
+
`
|
|
416
|
+
params.push(pattern, pattern, pattern, pattern, limit)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const rows = this.db.prepare(sql).all(...params) as any[]
|
|
420
|
+
return rows.map(row => this.rowToResult(row))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Convert a raw database row to an ObservationResult.
|
|
425
|
+
*/
|
|
426
|
+
private rowToResult(row: any): ObservationResult {
|
|
427
|
+
return {
|
|
428
|
+
id: row.id,
|
|
429
|
+
project: row.project,
|
|
430
|
+
category: row.category,
|
|
431
|
+
content: row.content,
|
|
432
|
+
reasoning: row.reasoning || null,
|
|
433
|
+
context: row.context || null,
|
|
434
|
+
confidence: row.confidence ?? 0.8,
|
|
435
|
+
source: row.source || 'explicit',
|
|
436
|
+
tags: row.tags ? this.parseJsonArray(row.tags) : [],
|
|
437
|
+
file_paths: row.file_paths ? this.parseJsonArray(row.file_paths) : [],
|
|
438
|
+
symbols: row.symbols ? this.parseJsonArray(row.symbols) : [],
|
|
439
|
+
access_count: row.access_count ?? 0,
|
|
440
|
+
last_accessed: row.last_accessed || null,
|
|
441
|
+
created_at: row.created_at,
|
|
442
|
+
updated_at: row.updated_at,
|
|
443
|
+
archived: row.archived === 1
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private parseJsonArray(value: string): string[] {
|
|
448
|
+
try {
|
|
449
|
+
const parsed = JSON.parse(value)
|
|
450
|
+
return Array.isArray(parsed) ? parsed : []
|
|
451
|
+
} catch {
|
|
452
|
+
// Handle comma-separated strings as fallback
|
|
453
|
+
return value.split(',').map(s => s.trim()).filter(Boolean)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|