@cleocode/core 2026.4.30 → 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
package/src/code/parser.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tree-sitter AST parser — query execution engine
|
|
2
|
+
* Tree-sitter AST parser — native Node bindings query execution engine.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Uses the tree-sitter native Node module to parse source files in-process,
|
|
5
|
+
* executing S-expression query patterns directly against the AST without
|
|
6
|
+
* spawning a subprocess or writing temp files.
|
|
7
7
|
*
|
|
8
|
-
* @task
|
|
8
|
+
* @task T509
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { join, relative } from 'node:path';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { relative } from 'node:path';
|
|
15
14
|
import type {
|
|
16
15
|
BatchParseResult,
|
|
17
16
|
CodeSymbol,
|
|
@@ -21,51 +20,187 @@ import type {
|
|
|
21
20
|
import { detectLanguage, type TreeSitterLanguage } from '../lib/tree-sitter-languages.js';
|
|
22
21
|
|
|
23
22
|
// ---------------------------------------------------------------------------
|
|
24
|
-
//
|
|
23
|
+
// Native module loading (CommonJS interop via createRequire)
|
|
25
24
|
// ---------------------------------------------------------------------------
|
|
26
25
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
26
|
+
/** ESM-safe require for loading native tree-sitter addons. */
|
|
27
|
+
const _require = createRequire(import.meta.url);
|
|
29
28
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Load a native module via require, returning null on failure.
|
|
31
|
+
*
|
|
32
|
+
* @param id - Module specifier (e.g. "tree-sitter" or "tree-sitter-rust")
|
|
33
|
+
*/
|
|
34
|
+
function tryRequire(id: string): unknown {
|
|
33
35
|
try {
|
|
34
|
-
|
|
35
|
-
// Verify the binary actually runs (broken symlinks pass existsSync)
|
|
36
|
-
execFileSync(bin, ['--version'], { timeout: 5000, stdio: 'pipe' });
|
|
37
|
-
_treeSitterAvailable = true;
|
|
36
|
+
return _require(id) as unknown;
|
|
38
37
|
} catch {
|
|
39
|
-
|
|
38
|
+
return null;
|
|
40
39
|
}
|
|
41
|
-
return _treeSitterAvailable;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Parser singleton — lazy-loaded on first use
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** The Parser constructor from the tree-sitter native module. */
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
type ParserConstructor = new () => NativeParser;
|
|
49
|
+
|
|
50
|
+
/** Minimal shape of the native tree-sitter Parser instance. */
|
|
51
|
+
interface NativeParser {
|
|
52
|
+
setLanguage(lang: unknown): void;
|
|
53
|
+
parse(source: string): NativeTree;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Minimal shape of the parsed Tree. */
|
|
57
|
+
interface NativeTree {
|
|
58
|
+
rootNode: SyntaxNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Minimal shape of a tree-sitter SyntaxNode. */
|
|
62
|
+
interface SyntaxNode {
|
|
63
|
+
type: string;
|
|
64
|
+
text: string;
|
|
65
|
+
startPosition: { row: number; column: number };
|
|
66
|
+
endPosition: { row: number; column: number };
|
|
67
|
+
children: SyntaxNode[];
|
|
68
|
+
namedChildren: SyntaxNode[];
|
|
69
|
+
childForFieldName(fieldName: string): SyntaxNode | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Minimal shape of a Query capture result. */
|
|
73
|
+
interface QueryCapture {
|
|
74
|
+
name: string;
|
|
75
|
+
node: SyntaxNode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Minimal shape of the native Query class. */
|
|
79
|
+
interface NativeQueryConstructor {
|
|
80
|
+
new (language: unknown, pattern: string): NativeQuery;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Minimal shape of a constructed Query instance. */
|
|
84
|
+
interface NativeQuery {
|
|
85
|
+
captures(node: SyntaxNode): QueryCapture[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The native Parser constructor (null if module unavailable). */
|
|
89
|
+
let _ParserClass: ParserConstructor | null = null;
|
|
90
|
+
|
|
91
|
+
/** The native Query constructor (null if module unavailable). */
|
|
92
|
+
let _QueryClass: NativeQueryConstructor | null = null;
|
|
93
|
+
|
|
94
|
+
/** Singleton parser instance, reused across calls. */
|
|
95
|
+
let _parserInstance: NativeParser | null = null;
|
|
96
|
+
|
|
97
|
+
/** Whether availability has been probed. */
|
|
98
|
+
let _availabilityChecked = false;
|
|
99
|
+
|
|
100
|
+
/** Whether the native tree-sitter module loaded successfully. */
|
|
101
|
+
let _available = false;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Probe and cache tree-sitter native module availability.
|
|
105
|
+
*
|
|
106
|
+
* Loads the tree-sitter native module once and caches the result.
|
|
107
|
+
* Subsequent calls return the cached value with no I/O.
|
|
108
|
+
*
|
|
109
|
+
* @returns True if the native module loaded successfully
|
|
110
|
+
*/
|
|
111
|
+
export function isTreeSitterAvailable(): boolean {
|
|
112
|
+
if (_availabilityChecked) return _available;
|
|
113
|
+
_availabilityChecked = true;
|
|
114
|
+
|
|
115
|
+
const mod = tryRequire('tree-sitter');
|
|
116
|
+
if (mod === null) {
|
|
117
|
+
_available = false;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// tree-sitter exports the Parser class directly
|
|
122
|
+
_ParserClass = mod as ParserConstructor;
|
|
123
|
+
|
|
124
|
+
// The Query class lives on the Parser namespace
|
|
125
|
+
const parserWithQuery = mod as { Query?: NativeQueryConstructor };
|
|
126
|
+
_QueryClass = parserWithQuery.Query ?? null;
|
|
127
|
+
|
|
128
|
+
_available = _ParserClass !== null && _QueryClass !== null;
|
|
129
|
+
return _available;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get (or create) the singleton Parser instance.
|
|
134
|
+
*
|
|
135
|
+
* @throws {Error} If tree-sitter native module is not available
|
|
136
|
+
*/
|
|
137
|
+
function getParser(): NativeParser {
|
|
138
|
+
if (_parserInstance) return _parserInstance;
|
|
139
|
+
if (!isTreeSitterAvailable() || _ParserClass === null) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
'tree-sitter native module not available. ' +
|
|
142
|
+
'Run: pnpm install (tree-sitter is a bundled dependency)',
|
|
60
143
|
);
|
|
61
144
|
}
|
|
62
|
-
|
|
63
|
-
|
|
145
|
+
_parserInstance = new _ParserClass();
|
|
146
|
+
return _parserInstance;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Grammar registry — language identifier → loaded grammar object
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/** Cache of loaded grammar objects, keyed by language identifier. */
|
|
154
|
+
const _grammarCache = new Map<string, unknown>();
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Describe how each grammar package exports its language object.
|
|
158
|
+
*
|
|
159
|
+
* Most packages export a single object via `module.exports = language`.
|
|
160
|
+
* tree-sitter-typescript exports `{ typescript, tsx }`.
|
|
161
|
+
* tree-sitter-php (not used here) exports `{ php, php_only }`.
|
|
162
|
+
*/
|
|
163
|
+
interface GrammarSpec {
|
|
164
|
+
/** npm package name. */
|
|
165
|
+
pkg: string;
|
|
166
|
+
/** Property to read from the module exports (undefined = use exports directly). */
|
|
167
|
+
prop?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const GRAMMAR_SPECS: Record<string, GrammarSpec> = {
|
|
171
|
+
typescript: { pkg: 'tree-sitter-typescript', prop: 'typescript' },
|
|
172
|
+
tsx: { pkg: 'tree-sitter-typescript', prop: 'tsx' },
|
|
173
|
+
javascript: { pkg: 'tree-sitter-javascript' },
|
|
174
|
+
python: { pkg: 'tree-sitter-python' },
|
|
175
|
+
go: { pkg: 'tree-sitter-go' },
|
|
176
|
+
rust: { pkg: 'tree-sitter-rust' },
|
|
177
|
+
java: { pkg: 'tree-sitter-java' },
|
|
178
|
+
c: { pkg: 'tree-sitter-c' },
|
|
179
|
+
cpp: { pkg: 'tree-sitter-cpp' },
|
|
180
|
+
ruby: { pkg: 'tree-sitter-ruby' },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Load and cache a grammar for the given language key.
|
|
185
|
+
*
|
|
186
|
+
* @param langKey - Language key matching a {@link GRAMMAR_SPECS} entry
|
|
187
|
+
* @returns The grammar object, or null if the package is not installed
|
|
188
|
+
*/
|
|
189
|
+
function loadGrammar(langKey: string): unknown {
|
|
190
|
+
if (_grammarCache.has(langKey)) return _grammarCache.get(langKey) ?? null;
|
|
191
|
+
|
|
192
|
+
const spec = GRAMMAR_SPECS[langKey];
|
|
193
|
+
if (!spec) return null;
|
|
194
|
+
|
|
195
|
+
const mod = tryRequire(spec.pkg);
|
|
196
|
+
if (mod === null) {
|
|
197
|
+
_grammarCache.set(langKey, null);
|
|
198
|
+
return null;
|
|
64
199
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
200
|
+
|
|
201
|
+
const grammar = spec.prop ? (mod as Record<string, unknown>)[spec.prop] : mod;
|
|
202
|
+
_grammarCache.set(langKey, grammar ?? null);
|
|
203
|
+
return grammar ?? null;
|
|
69
204
|
}
|
|
70
205
|
|
|
71
206
|
// ---------------------------------------------------------------------------
|
|
@@ -177,55 +312,95 @@ function captureToKind(capture: string): CodeSymbolKind {
|
|
|
177
312
|
}
|
|
178
313
|
|
|
179
314
|
// ---------------------------------------------------------------------------
|
|
180
|
-
//
|
|
315
|
+
// Query cache — avoid re-compiling patterns for repeated parses
|
|
181
316
|
// ---------------------------------------------------------------------------
|
|
182
317
|
|
|
318
|
+
/** Cache of compiled Query objects, keyed by `<langKey>:<queryKey>`. */
|
|
319
|
+
const _queryCache = new Map<string, NativeQuery>();
|
|
320
|
+
|
|
183
321
|
/**
|
|
184
|
-
*
|
|
322
|
+
* Get or compile a tree-sitter Query for the given language and pattern.
|
|
185
323
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* capture: 0 - name, start: (5, 9), end: (5, 20), text: `parseFile`
|
|
191
|
-
* capture: 1 - definition.function, start: (5, 0), end: (15, 1)
|
|
192
|
-
* ```
|
|
324
|
+
* @param grammar - The loaded grammar object (language)
|
|
325
|
+
* @param langKey - Key used for cache lookup
|
|
326
|
+
* @param pattern - S-expression query pattern string
|
|
327
|
+
* @returns Compiled Query instance, or null on compilation failure
|
|
193
328
|
*/
|
|
194
|
-
function
|
|
329
|
+
function getQuery(grammar: unknown, langKey: string, pattern: string): NativeQuery | null {
|
|
330
|
+
const cacheKey = langKey;
|
|
331
|
+
if (_queryCache.has(cacheKey)) return _queryCache.get(cacheKey) ?? null;
|
|
332
|
+
|
|
333
|
+
if (_QueryClass === null) return null;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const query = new _QueryClass(grammar, pattern);
|
|
337
|
+
_queryCache.set(cacheKey, query);
|
|
338
|
+
return query;
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Capture processing — convert Query captures to CodeSymbol objects
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Convert tree-sitter query captures into CodeSymbol objects.
|
|
350
|
+
*
|
|
351
|
+
* The query patterns use two capture groups per match:
|
|
352
|
+
* - `@definition.<kind>` — the enclosing declaration node with line range
|
|
353
|
+
* - `@name` — the identifier node containing the symbol's text
|
|
354
|
+
*
|
|
355
|
+
* tree-sitter's Query.captures() returns captures in document order within
|
|
356
|
+
* each match. Because definition nodes enclose the name node, the definition
|
|
357
|
+
* capture appears first, then the name capture for the same match.
|
|
358
|
+
*
|
|
359
|
+
* We pair consecutive `definition.*` + `name` captures to build symbols.
|
|
360
|
+
*
|
|
361
|
+
* @param captures - Raw captures from Query.captures()
|
|
362
|
+
* @param filePath - Relative file path for the symbol record
|
|
363
|
+
* @param language - Language identifier for the symbol record
|
|
364
|
+
* @returns Extracted CodeSymbol objects
|
|
365
|
+
*/
|
|
366
|
+
function captureToSymbols(
|
|
367
|
+
captures: QueryCapture[],
|
|
368
|
+
filePath: string,
|
|
369
|
+
language: string,
|
|
370
|
+
): CodeSymbol[] {
|
|
195
371
|
const symbols: CodeSymbol[] = [];
|
|
196
|
-
const lines = output.split('\n');
|
|
197
372
|
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
373
|
+
let i = 0;
|
|
374
|
+
while (i < captures.length) {
|
|
375
|
+
const cap = captures[i]!;
|
|
201
376
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
377
|
+
if (cap.name.startsWith('definition.')) {
|
|
378
|
+
const kindSuffix = cap.name.slice('definition.'.length);
|
|
379
|
+
const kind = captureToKind(kindSuffix);
|
|
380
|
+
// tree-sitter rows are 0-based; convert to 1-based line numbers
|
|
381
|
+
const startLine = cap.node.startPosition.row + 1;
|
|
382
|
+
const endLine = cap.node.endPosition.row + 1;
|
|
210
383
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
384
|
+
// The matching @name capture should immediately follow
|
|
385
|
+
const nameCap = captures[i + 1];
|
|
386
|
+
if (nameCap && nameCap.name === 'name') {
|
|
387
|
+
const nameText = nameCap.node.text;
|
|
388
|
+
if (nameText) {
|
|
389
|
+
symbols.push({
|
|
390
|
+
name: nameText,
|
|
391
|
+
kind,
|
|
392
|
+
startLine,
|
|
393
|
+
endLine,
|
|
394
|
+
filePath,
|
|
395
|
+
language,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
i += 2;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
228
401
|
}
|
|
402
|
+
|
|
403
|
+
i++;
|
|
229
404
|
}
|
|
230
405
|
|
|
231
406
|
return symbols;
|
|
@@ -236,7 +411,12 @@ function parseQueryOutput(output: string, filePath: string, language: string): C
|
|
|
236
411
|
// ---------------------------------------------------------------------------
|
|
237
412
|
|
|
238
413
|
/**
|
|
239
|
-
* Parse a single file and extract code symbols
|
|
414
|
+
* Parse a single file and extract code symbols using the native tree-sitter
|
|
415
|
+
* Node module.
|
|
416
|
+
*
|
|
417
|
+
* Loads the grammar for the detected language, sets it on the shared parser
|
|
418
|
+
* singleton, runs the compiled Query against the AST, and converts captures
|
|
419
|
+
* into CodeSymbol objects.
|
|
240
420
|
*
|
|
241
421
|
* @param filePath - Absolute or relative path to source file
|
|
242
422
|
* @param projectRoot - Project root for relative path computation
|
|
@@ -256,6 +436,18 @@ export function parseFile(filePath: string, projectRoot?: string): ParseResult {
|
|
|
256
436
|
};
|
|
257
437
|
}
|
|
258
438
|
|
|
439
|
+
if (!isTreeSitterAvailable()) {
|
|
440
|
+
return {
|
|
441
|
+
filePath: relPath,
|
|
442
|
+
language,
|
|
443
|
+
symbols: [],
|
|
444
|
+
errors: [
|
|
445
|
+
'tree-sitter native module not available. ' +
|
|
446
|
+
'Run: pnpm install (tree-sitter is a bundled dependency)',
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
259
451
|
const queryKey = queryKeyForLanguage(language);
|
|
260
452
|
const pattern = QUERY_PATTERNS[queryKey];
|
|
261
453
|
if (!pattern) {
|
|
@@ -267,34 +459,54 @@ export function parseFile(filePath: string, projectRoot?: string): ParseResult {
|
|
|
267
459
|
};
|
|
268
460
|
}
|
|
269
461
|
|
|
270
|
-
const
|
|
271
|
-
|
|
462
|
+
const grammar = loadGrammar(language);
|
|
463
|
+
if (!grammar) {
|
|
464
|
+
return {
|
|
465
|
+
filePath: relPath,
|
|
466
|
+
language,
|
|
467
|
+
symbols: [],
|
|
468
|
+
errors: [`Grammar package not installed for ${language}`],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let source: string;
|
|
473
|
+
try {
|
|
474
|
+
source = readFileSync(filePath, 'utf-8');
|
|
475
|
+
} catch (err) {
|
|
476
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
477
|
+
return { filePath: relPath, language, symbols: [], errors: [`Failed to read file: ${msg}`] };
|
|
478
|
+
}
|
|
272
479
|
|
|
273
480
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
481
|
+
const parser = getParser();
|
|
482
|
+
parser.setLanguage(grammar);
|
|
483
|
+
const tree = parser.parse(source);
|
|
276
484
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
485
|
+
const query = getQuery(grammar, queryKey, pattern);
|
|
486
|
+
if (!query) {
|
|
487
|
+
return {
|
|
488
|
+
filePath: relPath,
|
|
489
|
+
language,
|
|
490
|
+
symbols: [],
|
|
491
|
+
errors: [`Failed to compile query for ${language}`],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
282
494
|
|
|
283
|
-
const
|
|
495
|
+
const captures = query.captures(tree.rootNode);
|
|
496
|
+
const symbols = captureToSymbols(captures, relPath, language);
|
|
284
497
|
return { filePath: relPath, language, symbols, errors: [] };
|
|
285
498
|
} catch (err) {
|
|
286
499
|
const msg = err instanceof Error ? err.message : String(err);
|
|
287
500
|
return { filePath: relPath, language, symbols: [], errors: [msg] };
|
|
288
|
-
} finally {
|
|
289
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
290
501
|
}
|
|
291
502
|
}
|
|
292
503
|
|
|
293
504
|
/**
|
|
294
505
|
* Batch-parse multiple files, grouping by language for efficiency.
|
|
295
506
|
*
|
|
296
|
-
* Files
|
|
297
|
-
*
|
|
507
|
+
* Files are parsed individually; the parser singleton is reused across all
|
|
508
|
+
* files. Grammar loading is cached so each grammar is loaded at most once
|
|
509
|
+
* per process.
|
|
298
510
|
*
|
|
299
511
|
* @param filePaths - Array of file paths to parse
|
|
300
512
|
* @param projectRoot - Project root for relative path computation
|
|
@@ -305,25 +517,13 @@ export function batchParse(filePaths: string[], projectRoot?: string): BatchPars
|
|
|
305
517
|
const results: ParseResult[] = [];
|
|
306
518
|
const skipped: string[] = [];
|
|
307
519
|
|
|
308
|
-
// Group files by language
|
|
309
|
-
const byLanguage = new Map<string, string[]>();
|
|
310
520
|
for (const fp of filePaths) {
|
|
311
521
|
const lang = detectLanguage(fp);
|
|
312
522
|
if (!lang) {
|
|
313
523
|
skipped.push(relative(root, fp));
|
|
314
524
|
continue;
|
|
315
525
|
}
|
|
316
|
-
|
|
317
|
-
const group = byLanguage.get(key) ?? [];
|
|
318
|
-
group.push(fp);
|
|
319
|
-
byLanguage.set(key, group);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Parse each group
|
|
323
|
-
for (const [_queryKey, files] of byLanguage) {
|
|
324
|
-
for (const fp of files) {
|
|
325
|
-
results.push(parseFile(fp, root));
|
|
326
|
-
}
|
|
526
|
+
results.push(parseFile(fp, root));
|
|
327
527
|
}
|
|
328
528
|
|
|
329
529
|
const totalSymbols = results.reduce((sum, r) => sum + r.symbols.length, 0);
|
|
@@ -30,6 +30,11 @@ vi.mock('../memory-bridge-refresh.js', () => ({
|
|
|
30
30
|
maybeRefreshMemoryBridge: vi.fn(),
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
|
+
// T527: Mock session-grade to prevent real DB access in handleSessionEnd
|
|
34
|
+
vi.mock('../../../sessions/session-grade.js', () => ({
|
|
35
|
+
gradeSession: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
}));
|
|
37
|
+
|
|
33
38
|
// ---------------------------------------------------------------------------
|
|
34
39
|
// Handler imports — after mock setup
|
|
35
40
|
// ---------------------------------------------------------------------------
|
|
@@ -99,10 +104,10 @@ describe('hook automation E2E', () => {
|
|
|
99
104
|
});
|
|
100
105
|
|
|
101
106
|
// -------------------------------------------------------------------------
|
|
102
|
-
// 1. SessionStart
|
|
107
|
+
// 1. SessionStart — refreshes memory bridge (no observeBrain write per T527)
|
|
103
108
|
// -------------------------------------------------------------------------
|
|
104
109
|
describe('SessionStart', () => {
|
|
105
|
-
it('
|
|
110
|
+
it('does not write a brain observation on session start (T527)', async () => {
|
|
106
111
|
await handleSessionStart(PROJECT_ROOT, {
|
|
107
112
|
timestamp: TIMESTAMP,
|
|
108
113
|
sessionId: 'ses-e2e-1',
|
|
@@ -111,16 +116,8 @@ describe('hook automation E2E', () => {
|
|
|
111
116
|
agent: 'claude-sonnet',
|
|
112
117
|
});
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
expect(observeBrainMock).
|
|
116
|
-
PROJECT_ROOT,
|
|
117
|
-
expect.objectContaining({
|
|
118
|
-
title: 'Session start: E2E Session',
|
|
119
|
-
type: 'discovery',
|
|
120
|
-
sourceSessionId: 'ses-e2e-1',
|
|
121
|
-
sourceType: 'agent',
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
119
|
+
// T527: session start no longer writes a duplicate brain observation
|
|
120
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
124
121
|
});
|
|
125
122
|
|
|
126
123
|
it('triggers memory bridge refresh after session start', async () => {
|
|
@@ -136,10 +133,10 @@ describe('hook automation E2E', () => {
|
|
|
136
133
|
});
|
|
137
134
|
|
|
138
135
|
// -------------------------------------------------------------------------
|
|
139
|
-
// 2. SessionEnd
|
|
136
|
+
// 2. SessionEnd — refreshes memory bridge (no observeBrain write per T527)
|
|
140
137
|
// -------------------------------------------------------------------------
|
|
141
138
|
describe('SessionEnd', () => {
|
|
142
|
-
it('
|
|
139
|
+
it('does not write a brain observation on session end (T527)', async () => {
|
|
143
140
|
await handleSessionEnd(PROJECT_ROOT, {
|
|
144
141
|
timestamp: TIMESTAMP,
|
|
145
142
|
sessionId: 'ses-e2e-2',
|
|
@@ -147,29 +144,8 @@ describe('hook automation E2E', () => {
|
|
|
147
144
|
tasksCompleted: ['T166', 'T168'],
|
|
148
145
|
});
|
|
149
146
|
|
|
150
|
-
|
|
151
|
-
expect(observeBrainMock).
|
|
152
|
-
PROJECT_ROOT,
|
|
153
|
-
expect.objectContaining({
|
|
154
|
-
title: 'Session end: ses-e2e-2',
|
|
155
|
-
type: 'change',
|
|
156
|
-
sourceSessionId: 'ses-e2e-2',
|
|
157
|
-
sourceType: 'agent',
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('includes task list in session end observation text', async () => {
|
|
163
|
-
await handleSessionEnd(PROJECT_ROOT, {
|
|
164
|
-
timestamp: TIMESTAMP,
|
|
165
|
-
sessionId: 'ses-e2e-tasks',
|
|
166
|
-
duration: 600,
|
|
167
|
-
tasksCompleted: ['T100', 'T101'],
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
171
|
-
expect(callText).toContain('T100');
|
|
172
|
-
expect(callText).toContain('T101');
|
|
147
|
+
// T527: session end no longer writes a duplicate brain observation
|
|
148
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
173
149
|
});
|
|
174
150
|
|
|
175
151
|
it('triggers memory bridge refresh after session end', async () => {
|
|
@@ -536,8 +512,8 @@ describe('hook automation E2E', () => {
|
|
|
536
512
|
// 11. Dedup: PostToolUse doesn't double-capture what session-hooks handles
|
|
537
513
|
// -------------------------------------------------------------------------
|
|
538
514
|
describe('dedup (no double-capture)', () => {
|
|
539
|
-
it('PostToolUse
|
|
540
|
-
// Simulate a
|
|
515
|
+
it('PostToolUse fires brain observation; SessionEnd does not (T527)', async () => {
|
|
516
|
+
// Simulate a task completing then a session ending
|
|
541
517
|
await handleToolComplete(PROJECT_ROOT, {
|
|
542
518
|
timestamp: TIMESTAMP,
|
|
543
519
|
taskId: 'T168',
|
|
@@ -545,6 +521,8 @@ describe('hook automation E2E', () => {
|
|
|
545
521
|
status: 'done',
|
|
546
522
|
});
|
|
547
523
|
|
|
524
|
+
// PostToolUse (handleToolComplete) should have fired once
|
|
525
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
548
526
|
observeBrainMock.mockClear();
|
|
549
527
|
|
|
550
528
|
await handleSessionEnd(PROJECT_ROOT, {
|
|
@@ -554,12 +532,8 @@ describe('hook automation E2E', () => {
|
|
|
554
532
|
tasksCompleted: ['T168'],
|
|
555
533
|
});
|
|
556
534
|
|
|
557
|
-
//
|
|
558
|
-
expect(observeBrainMock).
|
|
559
|
-
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
560
|
-
PROJECT_ROOT,
|
|
561
|
-
expect.objectContaining({ title: 'Session end: ses-dedup' }),
|
|
562
|
-
);
|
|
535
|
+
// T527: session end no longer writes a duplicate brain observation
|
|
536
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
563
537
|
});
|
|
564
538
|
|
|
565
539
|
it('work-capture does not fire when captureWork is disabled (default)', async () => {
|