@cleocode/core 2026.4.29 → 2026.4.31
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/bootstrap.d.ts +35 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/code/index.d.ts +8 -4
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/parser.d.ts +22 -9
- package/dist/code/parser.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts +11 -4
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +6 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3859 -3008
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +10 -7
- package/dist/internal.d.ts.map +1 -1
- package/dist/lib/tree-sitter-languages.d.ts +11 -7
- package/dist/lib/tree-sitter-languages.d.ts.map +1 -1
- package/dist/memory/auto-extract.d.ts +27 -15
- package/dist/memory/auto-extract.d.ts.map +1 -1
- package/dist/memory/brain-backfill.d.ts +59 -0
- package/dist/memory/brain-backfill.d.ts.map +1 -0
- package/dist/memory/brain-purge.d.ts +51 -0
- package/dist/memory/brain-purge.d.ts.map +1 -0
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/decisions.d.ts.map +1 -1
- package/dist/memory/engine-compat.d.ts +71 -0
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/graph-auto-populate.d.ts +65 -0
- package/dist/memory/graph-auto-populate.d.ts.map +1 -0
- package/dist/memory/graph-queries.d.ts +127 -0
- package/dist/memory/graph-queries.d.ts.map +1 -0
- package/dist/memory/learnings.d.ts +2 -0
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/patterns.d.ts +2 -0
- package/dist/memory/patterns.d.ts.map +1 -1
- package/dist/memory/quality-scoring.d.ts +90 -0
- package/dist/memory/quality-scoring.d.ts.map +1 -0
- package/dist/sessions/session-memory-bridge.d.ts +16 -10
- package/dist/sessions/session-memory-bridge.d.ts.map +1 -1
- package/dist/store/brain-accessor.d.ts +7 -0
- package/dist/store/brain-accessor.d.ts.map +1 -1
- package/dist/store/brain-schema.d.ts +185 -11
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/nexus-schema.d.ts +480 -2
- package/dist/store/nexus-schema.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +9 -9
- package/dist/store/validation-schemas.d.ts +44 -28
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/system/dependencies.d.ts +43 -0
- package/dist/system/dependencies.d.ts.map +1 -0
- package/dist/system/health.d.ts +3 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/package.json +19 -19
- package/src/bootstrap.ts +124 -0
- package/src/code/index.ts +20 -4
- package/src/code/parser.ts +310 -110
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +19 -45
- package/src/hooks/handlers/__tests__/session-hooks.test.ts +42 -54
- package/src/hooks/handlers/session-hooks.ts +11 -33
- package/src/index.ts +14 -0
- package/src/internal.ts +37 -7
- package/src/lib/tree-sitter-languages.ts +11 -7
- package/src/memory/__tests__/auto-extract.test.ts +20 -82
- package/src/memory/__tests__/embedding-pipeline.test.ts +389 -0
- package/src/memory/auto-extract.ts +34 -120
- package/src/memory/brain-backfill.ts +471 -0
- package/src/memory/brain-purge.ts +315 -0
- package/src/memory/brain-retrieval.ts +43 -2
- package/src/memory/brain-search.ts +23 -6
- package/src/memory/decisions.ts +76 -3
- package/src/memory/engine-compat.ts +168 -0
- package/src/memory/graph-auto-populate.ts +173 -0
- package/src/memory/graph-queries.ts +424 -0
- package/src/memory/learnings.ts +55 -7
- package/src/memory/patterns.ts +66 -13
- package/src/memory/quality-scoring.ts +173 -0
- package/src/sessions/__tests__/session-memory-bridge.test.ts +27 -49
- package/src/sessions/session-memory-bridge.ts +19 -47
- package/src/store/__tests__/brain-accessor-pageindex.test.ts +93 -22
- package/src/store/brain-accessor.ts +48 -2
- package/src/store/brain-schema.ts +165 -13
- package/src/store/brain-sqlite.ts +35 -0
- package/src/store/nexus-schema.ts +257 -3
- package/src/system/dependencies.ts +534 -0
- package/src/system/health.ts +126 -22
- package/src/tasks/complete.ts +40 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain.db noise purge utility — removes 2927 noise entries leaving ~57 signal entries.
|
|
3
|
+
*
|
|
4
|
+
* Purge rules:
|
|
5
|
+
* - Patterns: keep newest per unique pattern text (dedup), delete duplicates
|
|
6
|
+
* - Learnings: delete ALL (all are auto-generated task completion noise)
|
|
7
|
+
* - Decisions: delete ALL except D-mntpeeer (the one real architectural decision)
|
|
8
|
+
* - Observations: delete task-start/task-complete/session-note/test/junk noise
|
|
9
|
+
*
|
|
10
|
+
* Safety: requires backup to exist before calling. Never touches tasks.db.
|
|
11
|
+
*
|
|
12
|
+
* @task T524
|
|
13
|
+
* @epic T523
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { inArray, ne, sql } from 'drizzle-orm';
|
|
17
|
+
import {
|
|
18
|
+
brainDecisions,
|
|
19
|
+
brainLearnings,
|
|
20
|
+
brainObservations,
|
|
21
|
+
brainPatterns,
|
|
22
|
+
} from '../store/brain-schema.js';
|
|
23
|
+
import { getBrainDb, getBrainNativeDb } from '../store/brain-sqlite.js';
|
|
24
|
+
import { ensureFts5Tables, rebuildFts5Index } from './brain-search.js';
|
|
25
|
+
|
|
26
|
+
/** Result counts from a purge run. */
|
|
27
|
+
export interface PurgeResult {
|
|
28
|
+
/** Number of pattern rows deleted. */
|
|
29
|
+
patternsDeleted: number;
|
|
30
|
+
/** Number of learning rows deleted. */
|
|
31
|
+
learningsDeleted: number;
|
|
32
|
+
/** Number of decision rows deleted. */
|
|
33
|
+
decisionsDeleted: number;
|
|
34
|
+
/** Number of observation rows deleted. */
|
|
35
|
+
observationsDeleted: number;
|
|
36
|
+
/** Counts after purge. */
|
|
37
|
+
after: {
|
|
38
|
+
patterns: number;
|
|
39
|
+
learnings: number;
|
|
40
|
+
decisions: number;
|
|
41
|
+
observations: number;
|
|
42
|
+
};
|
|
43
|
+
/** FTS5 indexes rebuilt. */
|
|
44
|
+
fts5Rebuilt: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Count rows in a table using native DB PRAGMA (avoids Drizzle type issues).
|
|
49
|
+
* Falls back to 0 on error.
|
|
50
|
+
*/
|
|
51
|
+
function countRowsNative(tableName: string): number {
|
|
52
|
+
const nativeDb = getBrainNativeDb();
|
|
53
|
+
if (!nativeDb) return 0;
|
|
54
|
+
try {
|
|
55
|
+
const row = nativeDb.prepare(`SELECT COUNT(*) AS cnt FROM ${tableName}`).get() as
|
|
56
|
+
| { cnt: number }
|
|
57
|
+
| undefined;
|
|
58
|
+
return row?.cnt ?? 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute the brain.db noise purge.
|
|
66
|
+
*
|
|
67
|
+
* Deletes ~2927 noise entries across four tables, leaving ~57 signal entries.
|
|
68
|
+
* The one real architectural decision (D-mntpeeer) is explicitly preserved.
|
|
69
|
+
*
|
|
70
|
+
* Rules applied in order:
|
|
71
|
+
* 1. Patterns: deduplicate by pattern text — keep newest per unique text, delete older dupes
|
|
72
|
+
* 2. Learnings: delete all (100% noise — auto-generated "Completed:" and dependency notices)
|
|
73
|
+
* 3. Decisions: delete all except D-mntpeeer
|
|
74
|
+
* 4. Observations: delete task-start, task-complete, session-note, and test/junk entries
|
|
75
|
+
*
|
|
76
|
+
* @param projectRoot - Absolute path to the project root (e.g. /mnt/projects/cleocode)
|
|
77
|
+
* @returns PurgeResult with before/after counts and FTS5 status
|
|
78
|
+
*/
|
|
79
|
+
export async function purgeBrainNoise(projectRoot: string): Promise<PurgeResult> {
|
|
80
|
+
const db = await getBrainDb(projectRoot);
|
|
81
|
+
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// Pre-purge counts
|
|
84
|
+
// =========================================================================
|
|
85
|
+
|
|
86
|
+
const beforePatterns = countRowsNative('brain_patterns');
|
|
87
|
+
const beforeLearnings = countRowsNative('brain_learnings');
|
|
88
|
+
const beforeDecisions = countRowsNative('brain_decisions');
|
|
89
|
+
const beforeObservations = countRowsNative('brain_observations');
|
|
90
|
+
|
|
91
|
+
console.log('Pre-purge counts:');
|
|
92
|
+
console.log(` Patterns: ${beforePatterns}`);
|
|
93
|
+
console.log(` Learnings: ${beforeLearnings}`);
|
|
94
|
+
console.log(` Decisions: ${beforeDecisions}`);
|
|
95
|
+
console.log(` Observations: ${beforeObservations}`);
|
|
96
|
+
|
|
97
|
+
// =========================================================================
|
|
98
|
+
// SAFETY: Confirm D-mntpeeer exists before any destructive operation
|
|
99
|
+
// =========================================================================
|
|
100
|
+
|
|
101
|
+
const realDecision = await db.select().from(brainDecisions).where(sql`id = 'D-mntpeeer'`);
|
|
102
|
+
|
|
103
|
+
if (realDecision.length === 0) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'SAFETY ABORT: D-mntpeeer not found in brain_decisions. Backup and restore required.',
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log('Safety check passed: D-mntpeeer confirmed present');
|
|
110
|
+
|
|
111
|
+
// =========================================================================
|
|
112
|
+
// Rule 1: Pattern deduplication
|
|
113
|
+
// Strategy: for each unique pattern text, find the MAX(extracted_at) row,
|
|
114
|
+
// delete all other rows with the same text.
|
|
115
|
+
// =========================================================================
|
|
116
|
+
|
|
117
|
+
// Get all patterns to find duplicates in TS (safer than raw SQL subquery)
|
|
118
|
+
const allPatterns = await db.select().from(brainPatterns);
|
|
119
|
+
|
|
120
|
+
// Group by normalized pattern text
|
|
121
|
+
const patternGroups = new Map<string, typeof allPatterns>();
|
|
122
|
+
for (const p of allPatterns) {
|
|
123
|
+
const key = p.pattern.trim().toLowerCase();
|
|
124
|
+
const group = patternGroups.get(key) ?? [];
|
|
125
|
+
group.push(p);
|
|
126
|
+
patternGroups.set(key, group);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Collect IDs to delete: for each group with >1 entry, keep newest (max extractedAt), delete rest
|
|
130
|
+
const patternIdsToDelete: string[] = [];
|
|
131
|
+
for (const [, group] of patternGroups) {
|
|
132
|
+
if (group.length <= 1) continue;
|
|
133
|
+
// Sort by extractedAt desc, keep first, delete the rest
|
|
134
|
+
group.sort((a, b) => (b.extractedAt > a.extractedAt ? 1 : -1));
|
|
135
|
+
const toDelete = group.slice(1).map((p) => p.id);
|
|
136
|
+
patternIdsToDelete.push(...toDelete);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let patternsDeleted = 0;
|
|
140
|
+
if (patternIdsToDelete.length > 0) {
|
|
141
|
+
// Delete in batches of 500 to avoid SQLite parameter limits
|
|
142
|
+
const BATCH = 500;
|
|
143
|
+
for (let i = 0; i < patternIdsToDelete.length; i += BATCH) {
|
|
144
|
+
const batch = patternIdsToDelete.slice(i, i + BATCH);
|
|
145
|
+
await db.delete(brainPatterns).where(inArray(brainPatterns.id, batch));
|
|
146
|
+
patternsDeleted += batch.length;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`Patterns deleted (dedup): ${patternsDeleted}`);
|
|
151
|
+
|
|
152
|
+
// =========================================================================
|
|
153
|
+
// Rule 2: Delete ALL learnings (100% noise)
|
|
154
|
+
// All learnings are auto-generated "Completed: T..." or dependency notices
|
|
155
|
+
// =========================================================================
|
|
156
|
+
|
|
157
|
+
// Count before delete using run
|
|
158
|
+
const learningsRows = await db.select().from(brainLearnings);
|
|
159
|
+
const learningsDeleted = learningsRows.length;
|
|
160
|
+
|
|
161
|
+
if (learningsDeleted > 0) {
|
|
162
|
+
// Delete in batches using IDs
|
|
163
|
+
const ids = learningsRows.map((r) => r.id);
|
|
164
|
+
const BATCH = 500;
|
|
165
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
166
|
+
const batch = ids.slice(i, i + BATCH);
|
|
167
|
+
await db.delete(brainLearnings).where(inArray(brainLearnings.id, batch));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`Learnings deleted (all): ${learningsDeleted}`);
|
|
172
|
+
|
|
173
|
+
// =========================================================================
|
|
174
|
+
// Rule 3: Delete ALL decisions EXCEPT D-mntpeeer
|
|
175
|
+
// =========================================================================
|
|
176
|
+
|
|
177
|
+
const decisionsToDelete = await db
|
|
178
|
+
.select()
|
|
179
|
+
.from(brainDecisions)
|
|
180
|
+
.where(ne(brainDecisions.id, 'D-mntpeeer'));
|
|
181
|
+
|
|
182
|
+
const decisionsDeleted = decisionsToDelete.length;
|
|
183
|
+
if (decisionsDeleted > 0) {
|
|
184
|
+
const ids = decisionsToDelete.map((r) => r.id);
|
|
185
|
+
await db.delete(brainDecisions).where(inArray(brainDecisions.id, ids));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`Decisions deleted (all except D-mntpeeer): ${decisionsDeleted}`);
|
|
189
|
+
|
|
190
|
+
// =========================================================================
|
|
191
|
+
// Rule 4: Delete noise observations
|
|
192
|
+
// Keep: real release notes, real codebase analysis, real session handoffs
|
|
193
|
+
// Delete: task-start, task-complete, session-note, test/junk entries
|
|
194
|
+
// =========================================================================
|
|
195
|
+
|
|
196
|
+
// Collect all observations to classify them
|
|
197
|
+
const allObservations = await db.select().from(brainObservations);
|
|
198
|
+
|
|
199
|
+
const obsIdsToDelete: string[] = [];
|
|
200
|
+
|
|
201
|
+
for (const obs of allObservations) {
|
|
202
|
+
const title = obs.title ?? '';
|
|
203
|
+
const narrative = (obs.narrative ?? '').toLowerCase();
|
|
204
|
+
const titleLower = title.toLowerCase();
|
|
205
|
+
|
|
206
|
+
// Rule 4a: Task lifecycle noise
|
|
207
|
+
if (
|
|
208
|
+
title.startsWith('Task start: T') ||
|
|
209
|
+
title.startsWith('Task complete: T') ||
|
|
210
|
+
title.startsWith('Task depended on') ||
|
|
211
|
+
title.startsWith('Task T') // catch "Task T527 depended on..." etc.
|
|
212
|
+
) {
|
|
213
|
+
obsIdsToDelete.push(obs.id);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Rule 4b: Session notes (all are noise — real handoffs are in sticky notes)
|
|
218
|
+
if (title.startsWith('Session note:')) {
|
|
219
|
+
obsIdsToDelete.push(obs.id);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Rule 4c: Test/audit/probe/junk observations
|
|
224
|
+
const testKeywords = [
|
|
225
|
+
'audit test',
|
|
226
|
+
'audit probe',
|
|
227
|
+
'probe observation',
|
|
228
|
+
'dup test',
|
|
229
|
+
'provider test',
|
|
230
|
+
'brain regression',
|
|
231
|
+
'brain validation',
|
|
232
|
+
'release test',
|
|
233
|
+
'functional validation',
|
|
234
|
+
'test title',
|
|
235
|
+
'test decision',
|
|
236
|
+
'test learning',
|
|
237
|
+
'test pattern',
|
|
238
|
+
'test observation',
|
|
239
|
+
'sticky note', // audit test sticky note
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const isTestNoise = testKeywords.some(
|
|
243
|
+
(kw) => titleLower.includes(kw) || narrative.includes(kw),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (isTestNoise) {
|
|
247
|
+
obsIdsToDelete.push(obs.id);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Rule 4d: Auto-detection codebase map noise (keep real ones, delete duplicates)
|
|
251
|
+
// "Codebase Stack Analysis" and "Codebase Integrations" — keep one of each,
|
|
252
|
+
// but the task asks us to keep ~27 so we apply conservative rules.
|
|
253
|
+
// Only delete if title exactly matches auto-generated repeated patterns.
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let observationsDeleted = 0;
|
|
257
|
+
if (obsIdsToDelete.length > 0) {
|
|
258
|
+
const BATCH = 500;
|
|
259
|
+
for (let i = 0; i < obsIdsToDelete.length; i += BATCH) {
|
|
260
|
+
const batch = obsIdsToDelete.slice(i, i + BATCH);
|
|
261
|
+
await db.delete(brainObservations).where(inArray(brainObservations.id, batch));
|
|
262
|
+
observationsDeleted += batch.length;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log(`Observations deleted: ${observationsDeleted}`);
|
|
267
|
+
|
|
268
|
+
// =========================================================================
|
|
269
|
+
// Post-purge counts
|
|
270
|
+
// =========================================================================
|
|
271
|
+
|
|
272
|
+
const afterPatterns = countRowsNative('brain_patterns');
|
|
273
|
+
const afterLearnings = countRowsNative('brain_learnings');
|
|
274
|
+
const afterDecisions = countRowsNative('brain_decisions');
|
|
275
|
+
const afterObservations = countRowsNative('brain_observations');
|
|
276
|
+
|
|
277
|
+
console.log('\nPost-purge counts:');
|
|
278
|
+
console.log(` Patterns: ${afterPatterns} (deleted ${beforePatterns - afterPatterns})`);
|
|
279
|
+
console.log(` Learnings: ${afterLearnings} (deleted ${beforeLearnings - afterLearnings})`);
|
|
280
|
+
console.log(` Decisions: ${afterDecisions} (deleted ${beforeDecisions - afterDecisions})`);
|
|
281
|
+
console.log(
|
|
282
|
+
` Observations: ${afterObservations} (deleted ${beforeObservations - afterObservations})`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// =========================================================================
|
|
286
|
+
// Rebuild FTS5 indexes after bulk deletes
|
|
287
|
+
// =========================================================================
|
|
288
|
+
|
|
289
|
+
let fts5Rebuilt = false;
|
|
290
|
+
const nativeDb = getBrainNativeDb();
|
|
291
|
+
if (nativeDb) {
|
|
292
|
+
ensureFts5Tables(nativeDb);
|
|
293
|
+
try {
|
|
294
|
+
rebuildFts5Index(nativeDb);
|
|
295
|
+
fts5Rebuilt = true;
|
|
296
|
+
console.log('FTS5 indexes rebuilt successfully');
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.warn('FTS5 rebuild failed (non-fatal):', err);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
patternsDeleted: beforePatterns - afterPatterns,
|
|
304
|
+
learningsDeleted: beforeLearnings - afterLearnings,
|
|
305
|
+
decisionsDeleted: beforeDecisions - afterDecisions,
|
|
306
|
+
observationsDeleted: beforeObservations - afterObservations,
|
|
307
|
+
after: {
|
|
308
|
+
patterns: afterPatterns,
|
|
309
|
+
learnings: afterLearnings,
|
|
310
|
+
decisions: afterDecisions,
|
|
311
|
+
observations: afterObservations,
|
|
312
|
+
},
|
|
313
|
+
fts5Rebuilt,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -35,6 +35,8 @@ import type {
|
|
|
35
35
|
BrainTimelineNeighborRow,
|
|
36
36
|
} from './brain-row-types.js';
|
|
37
37
|
import { searchBrain } from './brain-search.js';
|
|
38
|
+
import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
|
|
39
|
+
import { computeObservationQuality } from './quality-scoring.js';
|
|
38
40
|
|
|
39
41
|
// ============================================================================
|
|
40
42
|
// Types
|
|
@@ -155,7 +157,7 @@ export async function searchBrainCompact(
|
|
|
155
157
|
): Promise<SearchBrainCompactResult> {
|
|
156
158
|
const { query, limit, tables, dateStart, dateEnd, agent } = params;
|
|
157
159
|
|
|
158
|
-
if (!query
|
|
160
|
+
if (!query?.trim()) {
|
|
159
161
|
return { results: [], total: 0, tokensEstimated: 0 };
|
|
160
162
|
}
|
|
161
163
|
|
|
@@ -551,7 +553,7 @@ export async function observeBrain(
|
|
|
551
553
|
agent,
|
|
552
554
|
} = params;
|
|
553
555
|
|
|
554
|
-
if (!text
|
|
556
|
+
if (!text?.trim()) {
|
|
555
557
|
throw new Error('Observation text is required');
|
|
556
558
|
}
|
|
557
559
|
|
|
@@ -601,6 +603,9 @@ export async function observeBrain(
|
|
|
601
603
|
}
|
|
602
604
|
}
|
|
603
605
|
|
|
606
|
+
// Compute quality score from text richness and title length.
|
|
607
|
+
const qualityScore = computeObservationQuality({ text, title });
|
|
608
|
+
|
|
604
609
|
const id = `O-${Date.now().toString(36)}-${(observeSeq++ % 1000).toString(36)}`;
|
|
605
610
|
const accessor = await getBrainAccessor(projectRoot);
|
|
606
611
|
|
|
@@ -614,6 +619,7 @@ export async function observeBrain(
|
|
|
614
619
|
sourceSessionId: validSessionId,
|
|
615
620
|
sourceType: sourceType ?? 'agent',
|
|
616
621
|
agent: agent ?? null,
|
|
622
|
+
qualityScore,
|
|
617
623
|
createdAt: now,
|
|
618
624
|
});
|
|
619
625
|
|
|
@@ -655,6 +661,41 @@ export async function observeBrain(
|
|
|
655
661
|
});
|
|
656
662
|
}
|
|
657
663
|
|
|
664
|
+
// Auto-populate graph node + edges for this observation (best-effort, T537).
|
|
665
|
+
try {
|
|
666
|
+
await upsertGraphNode(
|
|
667
|
+
projectRoot,
|
|
668
|
+
`observation:${row.id}`,
|
|
669
|
+
'observation',
|
|
670
|
+
row.title.substring(0, 200),
|
|
671
|
+
row.qualityScore ?? 0.5,
|
|
672
|
+
row.narrative ?? row.title,
|
|
673
|
+
{ sourceType: row.sourceType, agent: row.agent ?? undefined },
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
// Link observation → session when the observation has a session context.
|
|
677
|
+
if (validSessionId) {
|
|
678
|
+
await upsertGraphNode(
|
|
679
|
+
projectRoot,
|
|
680
|
+
`session:${validSessionId}`,
|
|
681
|
+
'session',
|
|
682
|
+
validSessionId,
|
|
683
|
+
0.8,
|
|
684
|
+
'',
|
|
685
|
+
);
|
|
686
|
+
await addGraphEdge(
|
|
687
|
+
projectRoot,
|
|
688
|
+
`observation:${row.id}`,
|
|
689
|
+
`session:${validSessionId}`,
|
|
690
|
+
'produced_by',
|
|
691
|
+
1.0,
|
|
692
|
+
'auto:observe',
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
/* Graph population is best-effort — never block the primary return */
|
|
697
|
+
}
|
|
698
|
+
|
|
658
699
|
return {
|
|
659
700
|
id: row.id,
|
|
660
701
|
type: row.type,
|
|
@@ -20,6 +20,7 @@ import { typedAll } from '../store/typed-query.js';
|
|
|
20
20
|
import type { BrainSearchHit } from './brain-row-types.js';
|
|
21
21
|
import type { SimilarityResult } from './brain-similarity.js';
|
|
22
22
|
import { searchSimilar } from './brain-similarity.js';
|
|
23
|
+
import { QUALITY_SCORE_THRESHOLD } from './quality-scoring.js';
|
|
23
24
|
|
|
24
25
|
/** Search result with BM25 rank. */
|
|
25
26
|
export interface BrainSearchResult {
|
|
@@ -281,7 +282,7 @@ export async function searchBrain(
|
|
|
281
282
|
query: string,
|
|
282
283
|
options?: BrainSearchOptions,
|
|
283
284
|
): Promise<BrainSearchResult> {
|
|
284
|
-
if (!query
|
|
285
|
+
if (!query?.trim()) {
|
|
285
286
|
return { decisions: [], patterns: [], learnings: [], observations: [] };
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -339,10 +340,12 @@ function searchWithFts5(
|
|
|
339
340
|
FROM brain_decisions_fts fts
|
|
340
341
|
JOIN brain_decisions d ON d.rowid = fts.rowid
|
|
341
342
|
WHERE brain_decisions_fts MATCH ?
|
|
343
|
+
AND (d.quality_score IS NULL OR d.quality_score >= ?)
|
|
342
344
|
ORDER BY bm25(brain_decisions_fts)
|
|
343
345
|
LIMIT ?
|
|
344
346
|
`),
|
|
345
347
|
safeQuery,
|
|
348
|
+
QUALITY_SCORE_THRESHOLD,
|
|
346
349
|
limit,
|
|
347
350
|
);
|
|
348
351
|
result.decisions = rows;
|
|
@@ -360,10 +363,12 @@ function searchWithFts5(
|
|
|
360
363
|
FROM brain_patterns_fts fts
|
|
361
364
|
JOIN brain_patterns p ON p.rowid = fts.rowid
|
|
362
365
|
WHERE brain_patterns_fts MATCH ?
|
|
366
|
+
AND (p.quality_score IS NULL OR p.quality_score >= ?)
|
|
363
367
|
ORDER BY bm25(brain_patterns_fts)
|
|
364
368
|
LIMIT ?
|
|
365
369
|
`),
|
|
366
370
|
safeQuery,
|
|
371
|
+
QUALITY_SCORE_THRESHOLD,
|
|
367
372
|
limit,
|
|
368
373
|
);
|
|
369
374
|
result.patterns = rows;
|
|
@@ -380,10 +385,12 @@ function searchWithFts5(
|
|
|
380
385
|
FROM brain_learnings_fts fts
|
|
381
386
|
JOIN brain_learnings l ON l.rowid = fts.rowid
|
|
382
387
|
WHERE brain_learnings_fts MATCH ?
|
|
388
|
+
AND (l.quality_score IS NULL OR l.quality_score >= ?)
|
|
383
389
|
ORDER BY bm25(brain_learnings_fts)
|
|
384
390
|
LIMIT ?
|
|
385
391
|
`),
|
|
386
392
|
safeQuery,
|
|
393
|
+
QUALITY_SCORE_THRESHOLD,
|
|
387
394
|
limit,
|
|
388
395
|
);
|
|
389
396
|
result.learnings = rows;
|
|
@@ -400,10 +407,12 @@ function searchWithFts5(
|
|
|
400
407
|
FROM brain_observations_fts fts
|
|
401
408
|
JOIN brain_observations o ON o.rowid = fts.rowid
|
|
402
409
|
WHERE brain_observations_fts MATCH ?
|
|
410
|
+
AND (o.quality_score IS NULL OR o.quality_score >= ?)
|
|
403
411
|
ORDER BY bm25(brain_observations_fts)
|
|
404
412
|
LIMIT ?
|
|
405
413
|
`),
|
|
406
414
|
safeQuery,
|
|
415
|
+
QUALITY_SCORE_THRESHOLD,
|
|
407
416
|
limit,
|
|
408
417
|
);
|
|
409
418
|
result.observations = rows;
|
|
@@ -459,12 +468,14 @@ function likeSearchDecisions(
|
|
|
459
468
|
return typedAll<BrainDecisionRow>(
|
|
460
469
|
nativeDb.prepare(`
|
|
461
470
|
SELECT * FROM brain_decisions
|
|
462
|
-
WHERE decision LIKE ? OR rationale LIKE ?
|
|
471
|
+
WHERE (decision LIKE ? OR rationale LIKE ?)
|
|
472
|
+
AND (quality_score IS NULL OR quality_score >= ?)
|
|
463
473
|
ORDER BY created_at DESC
|
|
464
474
|
LIMIT ?
|
|
465
475
|
`),
|
|
466
476
|
likePattern,
|
|
467
477
|
likePattern,
|
|
478
|
+
QUALITY_SCORE_THRESHOLD,
|
|
468
479
|
limit,
|
|
469
480
|
);
|
|
470
481
|
}
|
|
@@ -478,12 +489,14 @@ function likeSearchPatterns(
|
|
|
478
489
|
return typedAll<BrainPatternRow>(
|
|
479
490
|
nativeDb.prepare(`
|
|
480
491
|
SELECT * FROM brain_patterns
|
|
481
|
-
WHERE pattern LIKE ? OR context LIKE ?
|
|
492
|
+
WHERE (pattern LIKE ? OR context LIKE ?)
|
|
493
|
+
AND (quality_score IS NULL OR quality_score >= ?)
|
|
482
494
|
ORDER BY frequency DESC
|
|
483
495
|
LIMIT ?
|
|
484
496
|
`),
|
|
485
497
|
likePattern,
|
|
486
498
|
likePattern,
|
|
499
|
+
QUALITY_SCORE_THRESHOLD,
|
|
487
500
|
limit,
|
|
488
501
|
);
|
|
489
502
|
}
|
|
@@ -497,12 +510,14 @@ function likeSearchLearnings(
|
|
|
497
510
|
return typedAll<BrainLearningRow>(
|
|
498
511
|
nativeDb.prepare(`
|
|
499
512
|
SELECT * FROM brain_learnings
|
|
500
|
-
WHERE insight LIKE ? OR source LIKE ?
|
|
513
|
+
WHERE (insight LIKE ? OR source LIKE ?)
|
|
514
|
+
AND (quality_score IS NULL OR quality_score >= ?)
|
|
501
515
|
ORDER BY confidence DESC
|
|
502
516
|
LIMIT ?
|
|
503
517
|
`),
|
|
504
518
|
likePattern,
|
|
505
519
|
likePattern,
|
|
520
|
+
QUALITY_SCORE_THRESHOLD,
|
|
506
521
|
limit,
|
|
507
522
|
);
|
|
508
523
|
}
|
|
@@ -516,12 +531,14 @@ function likeSearchObservations(
|
|
|
516
531
|
return typedAll<BrainObservationRow>(
|
|
517
532
|
nativeDb.prepare(`
|
|
518
533
|
SELECT * FROM brain_observations
|
|
519
|
-
WHERE title LIKE ? OR narrative LIKE ?
|
|
534
|
+
WHERE (title LIKE ? OR narrative LIKE ?)
|
|
535
|
+
AND (quality_score IS NULL OR quality_score >= ?)
|
|
520
536
|
ORDER BY created_at DESC
|
|
521
537
|
LIMIT ?
|
|
522
538
|
`),
|
|
523
539
|
likePattern,
|
|
524
540
|
likePattern,
|
|
541
|
+
QUALITY_SCORE_THRESHOLD,
|
|
525
542
|
limit,
|
|
526
543
|
);
|
|
527
544
|
}
|
|
@@ -591,7 +608,7 @@ export async function hybridSearch(
|
|
|
591
608
|
projectRoot: string,
|
|
592
609
|
options?: HybridSearchOptions,
|
|
593
610
|
): Promise<HybridResult[]> {
|
|
594
|
-
if (!query
|
|
611
|
+
if (!query?.trim()) return [];
|
|
595
612
|
|
|
596
613
|
const maxResults = options?.limit ?? 10;
|
|
597
614
|
let ftsWeight = options?.ftsWeight ?? 0.5;
|
package/src/memory/decisions.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { getBrainAccessor } from '../store/brain-accessor.js';
|
|
|
13
13
|
import type { BrainDecisionRow, NewBrainDecisionRow } from '../store/brain-schema.js';
|
|
14
14
|
import { taskExistsInTasksDb } from '../store/cross-db-cleanup.js';
|
|
15
15
|
import { getDb } from '../store/sqlite.js';
|
|
16
|
+
import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
|
|
17
|
+
import { computeDecisionQuality } from './quality-scoring.js';
|
|
16
18
|
|
|
17
19
|
/** Parameters for storing a new decision. */
|
|
18
20
|
export interface StoreDecisionParams {
|
|
@@ -79,10 +81,10 @@ export async function storeDecision(
|
|
|
79
81
|
projectRoot: string,
|
|
80
82
|
params: StoreDecisionParams,
|
|
81
83
|
): Promise<BrainDecisionRow> {
|
|
82
|
-
if (!params.decision
|
|
84
|
+
if (!params.decision?.trim()) {
|
|
83
85
|
throw new Error('Decision text is required');
|
|
84
86
|
}
|
|
85
|
-
if (!params.rationale
|
|
87
|
+
if (!params.rationale?.trim()) {
|
|
86
88
|
throw new Error('Rationale is required');
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -107,6 +109,25 @@ export async function storeDecision(
|
|
|
107
109
|
updatedAt: now,
|
|
108
110
|
});
|
|
109
111
|
const updated = await accessor.getDecision(duplicate.id);
|
|
112
|
+
|
|
113
|
+
// Refresh the graph node for the updated decision (best-effort).
|
|
114
|
+
const updatedQuality = computeDecisionQuality({
|
|
115
|
+
confidence: params.confidence,
|
|
116
|
+
rationale: params.rationale.trim(),
|
|
117
|
+
contextTaskId: params.contextTaskId ?? null,
|
|
118
|
+
});
|
|
119
|
+
upsertGraphNode(
|
|
120
|
+
projectRoot,
|
|
121
|
+
`decision:${duplicate.id}`,
|
|
122
|
+
'decision',
|
|
123
|
+
params.decision.trim().substring(0, 200),
|
|
124
|
+
updatedQuality,
|
|
125
|
+
params.decision.trim() + params.rationale.trim(),
|
|
126
|
+
{ type: params.type, confidence: params.confidence },
|
|
127
|
+
).catch(() => {
|
|
128
|
+
/* best-effort */
|
|
129
|
+
});
|
|
130
|
+
|
|
110
131
|
return updated!;
|
|
111
132
|
}
|
|
112
133
|
|
|
@@ -126,6 +147,13 @@ export async function storeDecision(
|
|
|
126
147
|
}
|
|
127
148
|
}
|
|
128
149
|
|
|
150
|
+
// Compute quality score from confidence level, rationale richness, and task linkage.
|
|
151
|
+
const qualityScore = computeDecisionQuality({
|
|
152
|
+
confidence: params.confidence,
|
|
153
|
+
rationale: params.rationale.trim(),
|
|
154
|
+
contextTaskId: validTaskId ?? null,
|
|
155
|
+
});
|
|
156
|
+
|
|
129
157
|
const row: NewBrainDecisionRow = {
|
|
130
158
|
id,
|
|
131
159
|
type: params.type,
|
|
@@ -137,9 +165,54 @@ export async function storeDecision(
|
|
|
137
165
|
contextEpicId: validEpicId,
|
|
138
166
|
contextTaskId: validTaskId,
|
|
139
167
|
contextPhase: params.contextPhase,
|
|
168
|
+
qualityScore,
|
|
140
169
|
};
|
|
141
170
|
|
|
142
|
-
|
|
171
|
+
const saved = await accessor.addDecision(row);
|
|
172
|
+
|
|
173
|
+
// Auto-populate graph node + edges for the new decision (best-effort, T537).
|
|
174
|
+
// All graph writes run fire-and-forget so they never block the return.
|
|
175
|
+
try {
|
|
176
|
+
await upsertGraphNode(
|
|
177
|
+
projectRoot,
|
|
178
|
+
`decision:${saved.id}`,
|
|
179
|
+
'decision',
|
|
180
|
+
saved.decision.substring(0, 200),
|
|
181
|
+
qualityScore,
|
|
182
|
+
saved.decision + saved.rationale,
|
|
183
|
+
{ type: saved.type, confidence: saved.confidence },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Link decision → task when a task context is present.
|
|
187
|
+
if (validTaskId) {
|
|
188
|
+
await upsertGraphNode(projectRoot, `task:${validTaskId}`, 'task', validTaskId, 1.0, '');
|
|
189
|
+
await addGraphEdge(
|
|
190
|
+
projectRoot,
|
|
191
|
+
`decision:${saved.id}`,
|
|
192
|
+
`task:${validTaskId}`,
|
|
193
|
+
'applies_to',
|
|
194
|
+
1.0,
|
|
195
|
+
'auto:store-decision',
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Link decision → epic when an epic context is present.
|
|
200
|
+
if (validEpicId) {
|
|
201
|
+
await upsertGraphNode(projectRoot, `epic:${validEpicId}`, 'epic', validEpicId, 1.0, '');
|
|
202
|
+
await addGraphEdge(
|
|
203
|
+
projectRoot,
|
|
204
|
+
`decision:${saved.id}`,
|
|
205
|
+
`epic:${validEpicId}`,
|
|
206
|
+
'applies_to',
|
|
207
|
+
1.0,
|
|
208
|
+
'auto:store-decision',
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
/* Graph population is best-effort — never block the primary return */
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return saved;
|
|
143
216
|
}
|
|
144
217
|
|
|
145
218
|
/**
|