@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,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain graph traversal queries using recursive CTEs and native SQLite.
|
|
3
|
+
*
|
|
4
|
+
* Implements BFS traversal, typed neighbor lookup, 360-degree context view,
|
|
5
|
+
* and aggregate statistics for the brain_page_nodes / brain_page_edges graph
|
|
6
|
+
* populated by T528 + T530.
|
|
7
|
+
*
|
|
8
|
+
* Uses getBrainNativeDb() (DatabaseSync) for recursive CTEs that Drizzle's
|
|
9
|
+
* ORM layer cannot express directly.
|
|
10
|
+
*
|
|
11
|
+
* @task T535
|
|
12
|
+
* @epic T523
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { BrainEdgeType, BrainPageEdgeRow, BrainPageNodeRow } from '../store/brain-schema.js';
|
|
16
|
+
import { getBrainDb, getBrainNativeDb } from '../store/brain-sqlite.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Return types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** A node returned from a BFS traversal, annotated with its discovery depth. */
|
|
23
|
+
export interface TraceNode extends BrainPageNodeRow {
|
|
24
|
+
/** Distance from the seed node (0 = seed itself). */
|
|
25
|
+
depth: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A neighbor node with the edge that connects it to the queried node. */
|
|
29
|
+
export interface RelatedNode {
|
|
30
|
+
/** The neighbour node. */
|
|
31
|
+
node: BrainPageNodeRow;
|
|
32
|
+
/** Edge relationship type. */
|
|
33
|
+
edgeType: BrainEdgeType;
|
|
34
|
+
/** Direction: 'out' (queried node is from_id) or 'in' (queried node is to_id). */
|
|
35
|
+
direction: 'out' | 'in';
|
|
36
|
+
/** Edge weight/confidence 0.0–1.0. */
|
|
37
|
+
weight: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Full context for a single node: itself, incoming edges, outgoing edges, and neighbour nodes. */
|
|
41
|
+
export interface NodeContext {
|
|
42
|
+
/** The node itself. */
|
|
43
|
+
node: BrainPageNodeRow;
|
|
44
|
+
/** Edges where this node is the target (in-edges). */
|
|
45
|
+
inEdges: BrainPageEdgeRow[];
|
|
46
|
+
/** Edges where this node is the source (out-edges). */
|
|
47
|
+
outEdges: BrainPageEdgeRow[];
|
|
48
|
+
/** All neighbour nodes reachable via in- or out-edges, deduplicated. */
|
|
49
|
+
neighbors: RelatedNode[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Aggregate statistics for the graph. */
|
|
53
|
+
export interface GraphStats {
|
|
54
|
+
/** Node counts grouped by node_type. */
|
|
55
|
+
nodesByType: Array<{ nodeType: string; count: number }>;
|
|
56
|
+
/** Edge counts grouped by edge_type. */
|
|
57
|
+
edgesByType: Array<{ edgeType: string; count: number }>;
|
|
58
|
+
/** Total node count. */
|
|
59
|
+
totalNodes: number;
|
|
60
|
+
/** Total edge count. */
|
|
61
|
+
totalEdges: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Internal raw-row types (native db returns plain objects)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
interface RawNode {
|
|
69
|
+
id: string;
|
|
70
|
+
node_type: string;
|
|
71
|
+
label: string;
|
|
72
|
+
quality_score: number;
|
|
73
|
+
content_hash: string | null;
|
|
74
|
+
last_activity_at: string;
|
|
75
|
+
metadata_json: string | null;
|
|
76
|
+
created_at: string;
|
|
77
|
+
updated_at: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface RawEdge {
|
|
81
|
+
from_id: string;
|
|
82
|
+
to_id: string;
|
|
83
|
+
edge_type: string;
|
|
84
|
+
weight: number;
|
|
85
|
+
provenance: string | null;
|
|
86
|
+
created_at: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Map a snake_case raw row to a camelCase BrainPageNodeRow. */
|
|
90
|
+
function mapNode(raw: RawNode): BrainPageNodeRow {
|
|
91
|
+
return {
|
|
92
|
+
id: raw.id,
|
|
93
|
+
nodeType: raw.node_type as BrainPageNodeRow['nodeType'],
|
|
94
|
+
label: raw.label,
|
|
95
|
+
qualityScore: raw.quality_score,
|
|
96
|
+
contentHash: raw.content_hash,
|
|
97
|
+
lastActivityAt: raw.last_activity_at,
|
|
98
|
+
metadataJson: raw.metadata_json,
|
|
99
|
+
createdAt: raw.created_at,
|
|
100
|
+
updatedAt: raw.updated_at,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Map a snake_case raw row to a camelCase BrainPageEdgeRow. */
|
|
105
|
+
function mapEdge(raw: RawEdge): BrainPageEdgeRow {
|
|
106
|
+
return {
|
|
107
|
+
fromId: raw.from_id,
|
|
108
|
+
toId: raw.to_id,
|
|
109
|
+
edgeType: raw.edge_type as BrainEdgeType,
|
|
110
|
+
weight: raw.weight,
|
|
111
|
+
provenance: raw.provenance,
|
|
112
|
+
createdAt: raw.created_at,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// traceBrainGraph — BFS traversal via recursive CTE
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Traverse the brain knowledge graph using BFS from a seed node.
|
|
122
|
+
*
|
|
123
|
+
* Uses a recursive CTE that follows edges in both directions. Nodes are
|
|
124
|
+
* returned in ascending depth order, then descending quality_score. The seed
|
|
125
|
+
* node itself is returned at depth 0.
|
|
126
|
+
*
|
|
127
|
+
* @param projectRoot - Absolute path to the project root (locates brain.db)
|
|
128
|
+
* @param nodeId - Starting node ID (format: '<type>:<source-id>')
|
|
129
|
+
* @param maxDepth - Maximum traversal depth (default 3)
|
|
130
|
+
* @returns Array of nodes annotated with their discovery depth
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const nodes = await traceBrainGraph('/project', 'decision:D-abc123', 2);
|
|
135
|
+
* for (const n of nodes) console.log(n.depth, n.id, n.label);
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export async function traceBrainGraph(
|
|
139
|
+
projectRoot: string,
|
|
140
|
+
nodeId: string,
|
|
141
|
+
maxDepth = 3,
|
|
142
|
+
): Promise<TraceNode[]> {
|
|
143
|
+
await getBrainDb(projectRoot);
|
|
144
|
+
const nativeDb = getBrainNativeDb();
|
|
145
|
+
|
|
146
|
+
if (!nativeDb) return [];
|
|
147
|
+
|
|
148
|
+
// Check whether the seed node exists
|
|
149
|
+
const seedCheck = nativeDb
|
|
150
|
+
.prepare('SELECT 1 FROM brain_page_nodes WHERE id = ?')
|
|
151
|
+
.get(nodeId) as unknown as { 1: number } | undefined;
|
|
152
|
+
if (!seedCheck) return [];
|
|
153
|
+
|
|
154
|
+
// Recursive CTE — bidirectional BFS with cycle detection via path string.
|
|
155
|
+
// The path is '|'-delimited node IDs. We check membership with a LIKE guard.
|
|
156
|
+
const rows = nativeDb
|
|
157
|
+
.prepare(
|
|
158
|
+
`
|
|
159
|
+
WITH RECURSIVE connected(id, depth, path) AS (
|
|
160
|
+
SELECT id, 0, id
|
|
161
|
+
FROM brain_page_nodes
|
|
162
|
+
WHERE id = ?
|
|
163
|
+
|
|
164
|
+
UNION ALL
|
|
165
|
+
|
|
166
|
+
SELECT
|
|
167
|
+
CASE WHEN e.from_id = c.id THEN e.to_id ELSE e.from_id END AS next_id,
|
|
168
|
+
c.depth + 1,
|
|
169
|
+
c.path || '|' || CASE WHEN e.from_id = c.id THEN e.to_id ELSE e.from_id END
|
|
170
|
+
FROM brain_page_edges e
|
|
171
|
+
JOIN connected c ON (e.from_id = c.id OR e.to_id = c.id)
|
|
172
|
+
WHERE c.depth < ?
|
|
173
|
+
AND ('|' || c.path || '|') NOT LIKE (
|
|
174
|
+
'%|' || CASE WHEN e.from_id = c.id THEN e.to_id ELSE e.from_id END || '|%'
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
SELECT DISTINCT n.id, n.node_type, n.label, n.quality_score,
|
|
178
|
+
n.content_hash, n.last_activity_at, n.metadata_json,
|
|
179
|
+
n.created_at, n.updated_at,
|
|
180
|
+
MIN(c.depth) AS depth
|
|
181
|
+
FROM connected c
|
|
182
|
+
JOIN brain_page_nodes n ON n.id = c.id
|
|
183
|
+
GROUP BY n.id
|
|
184
|
+
ORDER BY depth ASC, n.quality_score DESC
|
|
185
|
+
`,
|
|
186
|
+
)
|
|
187
|
+
.all(nodeId, maxDepth) as unknown as Array<RawNode & { depth: number }>;
|
|
188
|
+
|
|
189
|
+
return rows.map((r) => ({ ...mapNode(r), depth: r.depth }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// relatedBrainNodes — 1-hop neighbours with edge metadata
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Return the immediate (1-hop) neighbours of a node, including edge metadata.
|
|
198
|
+
*
|
|
199
|
+
* Follows edges in both directions. Results are sorted by edge weight
|
|
200
|
+
* descending, then quality_score descending.
|
|
201
|
+
*
|
|
202
|
+
* @param projectRoot - Absolute path to the project root
|
|
203
|
+
* @param nodeId - Node to inspect (format: '<type>:<source-id>')
|
|
204
|
+
* @param edgeType - Optional edge type filter (e.g. 'applies_to')
|
|
205
|
+
* @returns Array of neighbour nodes with edge relationship info
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const related = await relatedBrainNodes('/project', 'decision:D-abc123', 'applies_to');
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export async function relatedBrainNodes(
|
|
213
|
+
projectRoot: string,
|
|
214
|
+
nodeId: string,
|
|
215
|
+
edgeType?: string,
|
|
216
|
+
): Promise<RelatedNode[]> {
|
|
217
|
+
await getBrainDb(projectRoot);
|
|
218
|
+
const nativeDb = getBrainNativeDb();
|
|
219
|
+
|
|
220
|
+
if (!nativeDb) return [];
|
|
221
|
+
|
|
222
|
+
const edgeFilter = edgeType ? 'AND e.edge_type = ?' : '';
|
|
223
|
+
|
|
224
|
+
// Build params array for the union query
|
|
225
|
+
// Each half needs: nodeId [, edgeType]
|
|
226
|
+
const queryParams: string[] = edgeType ? [nodeId, edgeType, nodeId, edgeType] : [nodeId, nodeId];
|
|
227
|
+
|
|
228
|
+
// Two unions: outgoing (this node is from_id) and incoming (this node is to_id)
|
|
229
|
+
const rows = nativeDb
|
|
230
|
+
.prepare(
|
|
231
|
+
`
|
|
232
|
+
SELECT n.id, n.node_type, n.label, n.quality_score,
|
|
233
|
+
n.content_hash, n.last_activity_at, n.metadata_json,
|
|
234
|
+
n.created_at, n.updated_at,
|
|
235
|
+
e.edge_type, e.weight, 'out' AS direction
|
|
236
|
+
FROM brain_page_edges e
|
|
237
|
+
JOIN brain_page_nodes n ON n.id = e.to_id
|
|
238
|
+
WHERE e.from_id = ? ${edgeFilter}
|
|
239
|
+
|
|
240
|
+
UNION
|
|
241
|
+
|
|
242
|
+
SELECT n.id, n.node_type, n.label, n.quality_score,
|
|
243
|
+
n.content_hash, n.last_activity_at, n.metadata_json,
|
|
244
|
+
n.created_at, n.updated_at,
|
|
245
|
+
e.edge_type, e.weight, 'in' AS direction
|
|
246
|
+
FROM brain_page_edges e
|
|
247
|
+
JOIN brain_page_nodes n ON n.id = e.from_id
|
|
248
|
+
WHERE e.to_id = ? ${edgeFilter}
|
|
249
|
+
|
|
250
|
+
ORDER BY weight DESC, quality_score DESC
|
|
251
|
+
`,
|
|
252
|
+
)
|
|
253
|
+
.all(...queryParams) as unknown as Array<
|
|
254
|
+
RawNode & { edge_type: string; weight: number; direction: string }
|
|
255
|
+
>;
|
|
256
|
+
|
|
257
|
+
return rows.map((r) => ({
|
|
258
|
+
node: mapNode(r),
|
|
259
|
+
edgeType: r.edge_type as BrainEdgeType,
|
|
260
|
+
direction: r.direction as 'out' | 'in',
|
|
261
|
+
weight: r.weight,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// contextBrainNode — 360-degree view of a single node
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Return a 360-degree view of a single graph node.
|
|
271
|
+
*
|
|
272
|
+
* Provides the node itself, all incoming edges, all outgoing edges, and the
|
|
273
|
+
* full set of neighbour nodes with their edge relationships.
|
|
274
|
+
*
|
|
275
|
+
* @param projectRoot - Absolute path to the project root
|
|
276
|
+
* @param nodeId - Node to inspect (format: '<type>:<source-id>')
|
|
277
|
+
* @returns Full context record, or null if the node does not exist
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```typescript
|
|
281
|
+
* const ctx = await contextBrainNode('/project', 'decision:D-abc123');
|
|
282
|
+
* if (ctx) {
|
|
283
|
+
* console.log(ctx.node.label, ctx.outEdges.length, 'outgoing edges');
|
|
284
|
+
* }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export async function contextBrainNode(
|
|
288
|
+
projectRoot: string,
|
|
289
|
+
nodeId: string,
|
|
290
|
+
): Promise<NodeContext | null> {
|
|
291
|
+
await getBrainDb(projectRoot);
|
|
292
|
+
const nativeDb = getBrainNativeDb();
|
|
293
|
+
|
|
294
|
+
if (!nativeDb) return null;
|
|
295
|
+
|
|
296
|
+
const rawNode = nativeDb
|
|
297
|
+
.prepare(
|
|
298
|
+
`SELECT id, node_type, label, quality_score, content_hash,
|
|
299
|
+
last_activity_at, metadata_json, created_at, updated_at
|
|
300
|
+
FROM brain_page_nodes WHERE id = ?`,
|
|
301
|
+
)
|
|
302
|
+
.get(nodeId) as unknown as RawNode | undefined;
|
|
303
|
+
|
|
304
|
+
if (!rawNode) return null;
|
|
305
|
+
|
|
306
|
+
const node = mapNode(rawNode);
|
|
307
|
+
|
|
308
|
+
const rawOutEdges = nativeDb
|
|
309
|
+
.prepare(
|
|
310
|
+
`SELECT from_id, to_id, edge_type, weight, provenance, created_at
|
|
311
|
+
FROM brain_page_edges WHERE from_id = ? ORDER BY weight DESC`,
|
|
312
|
+
)
|
|
313
|
+
.all(nodeId) as unknown as RawEdge[];
|
|
314
|
+
|
|
315
|
+
const rawInEdges = nativeDb
|
|
316
|
+
.prepare(
|
|
317
|
+
`SELECT from_id, to_id, edge_type, weight, provenance, created_at
|
|
318
|
+
FROM brain_page_edges WHERE to_id = ? ORDER BY weight DESC`,
|
|
319
|
+
)
|
|
320
|
+
.all(nodeId) as unknown as RawEdge[];
|
|
321
|
+
|
|
322
|
+
const outEdges = rawOutEdges.map(mapEdge);
|
|
323
|
+
const inEdges = rawInEdges.map(mapEdge);
|
|
324
|
+
|
|
325
|
+
// Collect unique neighbour IDs from both directions
|
|
326
|
+
const seenIds = new Set<string>([nodeId]);
|
|
327
|
+
const neighbors: RelatedNode[] = [];
|
|
328
|
+
|
|
329
|
+
for (const e of rawOutEdges) {
|
|
330
|
+
if (!seenIds.has(e.to_id)) {
|
|
331
|
+
seenIds.add(e.to_id);
|
|
332
|
+
const rawNeighbour = nativeDb
|
|
333
|
+
.prepare(
|
|
334
|
+
`SELECT id, node_type, label, quality_score, content_hash,
|
|
335
|
+
last_activity_at, metadata_json, created_at, updated_at
|
|
336
|
+
FROM brain_page_nodes WHERE id = ?`,
|
|
337
|
+
)
|
|
338
|
+
.get(e.to_id) as unknown as RawNode | undefined;
|
|
339
|
+
if (rawNeighbour) {
|
|
340
|
+
neighbors.push({
|
|
341
|
+
node: mapNode(rawNeighbour),
|
|
342
|
+
edgeType: e.edge_type as BrainEdgeType,
|
|
343
|
+
direction: 'out',
|
|
344
|
+
weight: e.weight,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const e of rawInEdges) {
|
|
351
|
+
if (!seenIds.has(e.from_id)) {
|
|
352
|
+
seenIds.add(e.from_id);
|
|
353
|
+
const rawNeighbour = nativeDb
|
|
354
|
+
.prepare(
|
|
355
|
+
`SELECT id, node_type, label, quality_score, content_hash,
|
|
356
|
+
last_activity_at, metadata_json, created_at, updated_at
|
|
357
|
+
FROM brain_page_nodes WHERE id = ?`,
|
|
358
|
+
)
|
|
359
|
+
.get(e.from_id) as unknown as RawNode | undefined;
|
|
360
|
+
if (rawNeighbour) {
|
|
361
|
+
neighbors.push({
|
|
362
|
+
node: mapNode(rawNeighbour),
|
|
363
|
+
edgeType: e.edge_type as BrainEdgeType,
|
|
364
|
+
direction: 'in',
|
|
365
|
+
weight: e.weight,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Sort neighbours by weight descending
|
|
372
|
+
neighbors.sort((a, b) => b.weight - a.weight);
|
|
373
|
+
|
|
374
|
+
return { node, inEdges, outEdges, neighbors };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// graphStats — aggregate counts by type
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Return aggregate counts for brain_page_nodes and brain_page_edges by type.
|
|
383
|
+
*
|
|
384
|
+
* @param projectRoot - Absolute path to the project root
|
|
385
|
+
* @returns Counts by node type and edge type, plus totals
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* const stats = await graphStats('/project');
|
|
390
|
+
* console.log(stats.totalNodes, stats.totalEdges);
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
export async function graphStats(projectRoot: string): Promise<GraphStats> {
|
|
394
|
+
await getBrainDb(projectRoot);
|
|
395
|
+
const nativeDb = getBrainNativeDb();
|
|
396
|
+
|
|
397
|
+
if (!nativeDb) {
|
|
398
|
+
return { nodesByType: [], edgesByType: [], totalNodes: 0, totalEdges: 0 };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const nodeRows = nativeDb
|
|
402
|
+
.prepare(
|
|
403
|
+
`SELECT node_type, COUNT(*) AS count
|
|
404
|
+
FROM brain_page_nodes GROUP BY node_type ORDER BY count DESC`,
|
|
405
|
+
)
|
|
406
|
+
.all() as unknown as Array<{ node_type: string; count: number }>;
|
|
407
|
+
|
|
408
|
+
const edgeRows = nativeDb
|
|
409
|
+
.prepare(
|
|
410
|
+
`SELECT edge_type, COUNT(*) AS count
|
|
411
|
+
FROM brain_page_edges GROUP BY edge_type ORDER BY count DESC`,
|
|
412
|
+
)
|
|
413
|
+
.all() as unknown as Array<{ edge_type: string; count: number }>;
|
|
414
|
+
|
|
415
|
+
const totalNodes = nodeRows.reduce((s, r) => s + r.count, 0);
|
|
416
|
+
const totalEdges = edgeRows.reduce((s, r) => s + r.count, 0);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
nodesByType: nodeRows.map((r) => ({ nodeType: r.node_type, count: r.count })),
|
|
420
|
+
edgesByType: edgeRows.map((r) => ({ edgeType: r.edge_type, count: r.count })),
|
|
421
|
+
totalNodes,
|
|
422
|
+
totalEdges,
|
|
423
|
+
};
|
|
424
|
+
}
|
package/src/memory/learnings.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
14
|
import { getBrainAccessor } from '../store/brain-accessor.js';
|
|
15
|
+
import { upsertGraphNode } from './graph-auto-populate.js';
|
|
16
|
+
import { computeLearningQuality } from './quality-scoring.js';
|
|
15
17
|
|
|
16
18
|
/** Parameters for storing a new learning. */
|
|
17
19
|
export interface StoreLearningParams {
|
|
@@ -44,10 +46,10 @@ function generateLearningId(): string {
|
|
|
44
46
|
* @task T4769, T5241
|
|
45
47
|
*/
|
|
46
48
|
export async function storeLearning(projectRoot: string, params: StoreLearningParams) {
|
|
47
|
-
if (!params.insight
|
|
49
|
+
if (!params.insight?.trim()) {
|
|
48
50
|
throw new Error('Insight text is required');
|
|
49
51
|
}
|
|
50
|
-
if (!params.source
|
|
52
|
+
if (!params.source?.trim()) {
|
|
51
53
|
throw new Error('Source is required');
|
|
52
54
|
}
|
|
53
55
|
if (params.confidence < 0 || params.confidence > 1) {
|
|
@@ -55,18 +57,50 @@ export async function storeLearning(projectRoot: string, params: StoreLearningPa
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const accessor = await getBrainAccessor(projectRoot);
|
|
58
|
-
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
59
60
|
|
|
60
|
-
// Check for duplicate insight
|
|
61
|
+
// Check for duplicate insight by normalized text
|
|
61
62
|
const existingLearnings = await accessor.findLearnings();
|
|
63
|
+
const normalizedInput = params.insight.trim().toLowerCase();
|
|
62
64
|
const duplicate = existingLearnings.find(
|
|
63
|
-
(e) => e.insight.toLowerCase() ===
|
|
65
|
+
(e) => e.insight.trim().toLowerCase() === normalizedInput,
|
|
64
66
|
);
|
|
65
67
|
|
|
66
68
|
if (duplicate) {
|
|
67
|
-
//
|
|
69
|
+
// Take the higher confidence value and update the timestamp
|
|
70
|
+
const maxConfidence = Math.max(duplicate.confidence, params.confidence);
|
|
71
|
+
await accessor.updateLearning(duplicate.id, {
|
|
72
|
+
confidence: maxConfidence,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const updated = await accessor.getLearning(duplicate.id);
|
|
76
|
+
|
|
77
|
+
// Refresh graph node for the updated learning (best-effort, T537).
|
|
78
|
+
upsertGraphNode(
|
|
79
|
+
projectRoot,
|
|
80
|
+
`learning:${duplicate.id}`,
|
|
81
|
+
'learning',
|
|
82
|
+
duplicate.insight.substring(0, 200),
|
|
83
|
+
duplicate.qualityScore ?? 0.5,
|
|
84
|
+
duplicate.insight + (duplicate.application ?? ''),
|
|
85
|
+
{ source: duplicate.source, confidence: maxConfidence, actionable: duplicate.actionable },
|
|
86
|
+
).catch(() => {
|
|
87
|
+
/* best-effort */
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...updated!,
|
|
92
|
+
applicableTypes: JSON.parse(updated!.applicableTypesJson || '[]'),
|
|
93
|
+
};
|
|
68
94
|
}
|
|
69
95
|
|
|
96
|
+
// Compute quality score from confidence, actionability, and content richness.
|
|
97
|
+
const qualityScore = computeLearningQuality({
|
|
98
|
+
confidence: params.confidence,
|
|
99
|
+
actionable: params.actionable ?? false,
|
|
100
|
+
insight: params.insight.trim(),
|
|
101
|
+
application: params.application ?? null,
|
|
102
|
+
});
|
|
103
|
+
|
|
70
104
|
// Create new entry
|
|
71
105
|
const entry = {
|
|
72
106
|
id: generateLearningId(),
|
|
@@ -76,10 +110,24 @@ export async function storeLearning(projectRoot: string, params: StoreLearningPa
|
|
|
76
110
|
actionable: params.actionable ?? false,
|
|
77
111
|
application: params.application ?? null,
|
|
78
112
|
applicableTypesJson: params.applicableTypes ? JSON.stringify(params.applicableTypes) : '[]',
|
|
79
|
-
|
|
113
|
+
qualityScore,
|
|
80
114
|
};
|
|
81
115
|
|
|
82
116
|
const saved = await accessor.addLearning(entry);
|
|
117
|
+
|
|
118
|
+
// Auto-populate graph node for the new learning (best-effort, T537).
|
|
119
|
+
upsertGraphNode(
|
|
120
|
+
projectRoot,
|
|
121
|
+
`learning:${saved.id}`,
|
|
122
|
+
'learning',
|
|
123
|
+
saved.insight.substring(0, 200),
|
|
124
|
+
qualityScore,
|
|
125
|
+
saved.insight + (saved.application ?? ''),
|
|
126
|
+
{ source: saved.source, confidence: saved.confidence, actionable: saved.actionable },
|
|
127
|
+
).catch(() => {
|
|
128
|
+
/* best-effort */
|
|
129
|
+
});
|
|
130
|
+
|
|
83
131
|
return {
|
|
84
132
|
...saved,
|
|
85
133
|
applicableTypes: JSON.parse(saved.applicableTypesJson || '[]'),
|
package/src/memory/patterns.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { randomBytes } from 'node:crypto';
|
|
14
14
|
import { getBrainAccessor } from '../store/brain-accessor.js';
|
|
15
|
+
import { upsertGraphNode } from './graph-auto-populate.js';
|
|
16
|
+
import { computePatternQuality } from './quality-scoring.js';
|
|
15
17
|
|
|
16
18
|
/** Pattern types from ADR-009. */
|
|
17
19
|
export type PatternType = 'workflow' | 'blocker' | 'success' | 'failure' | 'optimization';
|
|
@@ -53,34 +55,70 @@ function generatePatternId(): string {
|
|
|
53
55
|
* @task T4768, T5241
|
|
54
56
|
*/
|
|
55
57
|
export async function storePattern(projectRoot: string, params: StorePatternParams) {
|
|
56
|
-
if (!params.pattern
|
|
58
|
+
if (!params.pattern?.trim()) {
|
|
57
59
|
throw new Error('Pattern description is required');
|
|
58
60
|
}
|
|
59
|
-
if (!params.context
|
|
61
|
+
if (!params.context?.trim()) {
|
|
60
62
|
throw new Error('Pattern context is required');
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const accessor = await getBrainAccessor(projectRoot);
|
|
64
66
|
|
|
65
|
-
//
|
|
66
|
-
// Currently we just match on type and exact text
|
|
67
|
+
// Search for duplicate pattern by normalized text within same type
|
|
67
68
|
const existingPatterns = await accessor.findPatterns({ type: params.type });
|
|
69
|
+
const normalizedInput = params.pattern.trim().toLowerCase();
|
|
68
70
|
const duplicate = existingPatterns.find(
|
|
69
|
-
(e) => e.pattern.toLowerCase() ===
|
|
71
|
+
(e) => e.pattern.trim().toLowerCase() === normalizedInput,
|
|
70
72
|
);
|
|
71
73
|
|
|
72
74
|
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
73
75
|
|
|
74
76
|
if (duplicate) {
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// Merge examples arrays (union of task IDs)
|
|
78
|
+
const existingExamples: string[] = JSON.parse(duplicate.examplesJson || '[]');
|
|
79
|
+
const newExamples: string[] = params.examples ?? [];
|
|
80
|
+
const mergedExamples = Array.from(new Set([...existingExamples, ...newExamples]));
|
|
81
|
+
|
|
82
|
+
await accessor.updatePattern(duplicate.id, {
|
|
83
|
+
frequency: duplicate.frequency + 1,
|
|
84
|
+
extractedAt: now,
|
|
85
|
+
examplesJson: JSON.stringify(mergedExamples),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const updated = await accessor.getPattern(duplicate.id);
|
|
89
|
+
|
|
90
|
+
// Refresh graph node for the updated (incremented) pattern (best-effort, T537).
|
|
91
|
+
upsertGraphNode(
|
|
92
|
+
projectRoot,
|
|
93
|
+
`pattern:${duplicate.id}`,
|
|
94
|
+
'pattern',
|
|
95
|
+
duplicate.pattern.substring(0, 200),
|
|
96
|
+
duplicate.qualityScore ?? 0.5,
|
|
97
|
+
duplicate.pattern + duplicate.context,
|
|
98
|
+
{
|
|
99
|
+
type: duplicate.type,
|
|
100
|
+
impact: duplicate.impact ?? undefined,
|
|
101
|
+
frequency: duplicate.frequency + 1,
|
|
102
|
+
},
|
|
103
|
+
).catch(() => {
|
|
104
|
+
/* best-effort */
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...updated!,
|
|
109
|
+
examples: mergedExamples,
|
|
110
|
+
};
|
|
82
111
|
}
|
|
83
112
|
|
|
113
|
+
// Compute quality score based on type, content richness, and examples.
|
|
114
|
+
const examplesJson = params.examples ? JSON.stringify(params.examples) : '[]';
|
|
115
|
+
const qualityScore = computePatternQuality({
|
|
116
|
+
type: params.type,
|
|
117
|
+
pattern: params.pattern.trim(),
|
|
118
|
+
context: params.context.trim(),
|
|
119
|
+
examples_json: examplesJson,
|
|
120
|
+
});
|
|
121
|
+
|
|
84
122
|
// Create new entry
|
|
85
123
|
const entry = {
|
|
86
124
|
id: generatePatternId(),
|
|
@@ -92,11 +130,26 @@ export async function storePattern(projectRoot: string, params: StorePatternPara
|
|
|
92
130
|
impact: params.impact ?? null,
|
|
93
131
|
antiPattern: params.antiPattern ?? null,
|
|
94
132
|
mitigation: params.mitigation ?? null,
|
|
95
|
-
examplesJson
|
|
133
|
+
examplesJson,
|
|
96
134
|
extractedAt: now,
|
|
135
|
+
qualityScore,
|
|
97
136
|
};
|
|
98
137
|
|
|
99
138
|
const saved = await accessor.addPattern(entry);
|
|
139
|
+
|
|
140
|
+
// Auto-populate graph node for the new pattern (best-effort, T537).
|
|
141
|
+
upsertGraphNode(
|
|
142
|
+
projectRoot,
|
|
143
|
+
`pattern:${saved.id}`,
|
|
144
|
+
'pattern',
|
|
145
|
+
saved.pattern.substring(0, 200),
|
|
146
|
+
qualityScore,
|
|
147
|
+
saved.pattern + saved.context,
|
|
148
|
+
{ type: saved.type, impact: saved.impact ?? undefined },
|
|
149
|
+
).catch(() => {
|
|
150
|
+
/* best-effort */
|
|
151
|
+
});
|
|
152
|
+
|
|
100
153
|
return {
|
|
101
154
|
...saved,
|
|
102
155
|
examples: JSON.parse(saved.examplesJson || '[]'),
|