septum 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +100 -100
- package/bin/septum.ts +4 -4
- package/package.json +63 -62
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +188 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +211 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +64 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +90 -90
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +199 -178
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +38 -38
- package/src/cli/templates/skill.ts +66 -66
- package/src/core/boundary/evaluator.ts +210 -210
- package/src/core/config/loader.ts +84 -84
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -132
- package/src/core/database/repositories/dependency.repository.ts +279 -279
- package/src/core/database/repositories/domain.repository.ts +198 -198
- package/src/core/database/repositories/file.repository.ts +99 -99
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +424 -424
- package/src/core/database/repository.ts +242 -242
- package/src/core/database/schema.ts +120 -0
- package/src/core/discovery/topology-detector.ts +418 -418
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +438 -438
- package/src/core/parser/boundary-tracker.ts +128 -128
- package/src/core/parser/extractors/base.ts +60 -60
- package/src/core/parser/extractors/frontend.ts +194 -194
- package/src/core/parser/extractors/go.ts +176 -176
- package/src/core/parser/extractors/laravel-semantic.ts +426 -426
- package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
- package/src/core/parser/extractors/php.ts +149 -149
- package/src/core/parser/extractors/python.ts +226 -226
- package/src/core/parser/extractors/registry.ts +49 -49
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +177 -177
- package/src/core/parser/tree-sitter.ts +39 -39
- package/src/core/resolver/call-graph-tracer.ts +668 -668
- package/src/core/resolver/module-resolver.ts +342 -323
- package/src/core/resolver/symbol-locator.ts +543 -405
- package/src/core/resolver/vertical-slice-tracer.ts +237 -236
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/index.ts +18 -18
- package/src/mcp/schemas.ts +68 -68
- package/src/mcp/server.ts +612 -602
- package/src/mcp/tools/check-boundary.ts +102 -102
- package/src/mcp/tools/get-domain-catalog.ts +91 -91
- package/src/mcp/tools/get-feature-context.ts +116 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +49 -49
- package/src/mcp/tools/get-symbol-impact.ts +26 -26
- package/src/mcp/tools/get-symbol.ts +63 -63
- package/src/mcp/tools/locate-symbol.ts +74 -74
- package/src/mcp/tools/register-domain.ts +64 -64
- package/src/mcp/tools/trace-vertical-slice.ts +102 -102
- package/src/types/index.ts +296 -296
- package/src/version.ts +3 -0
- package/src/core/database/schema.sql +0 -92
|
@@ -1,405 +1,543 @@
|
|
|
1
|
-
import type { FileRecord, SymbolKind, SymbolRecord } from "../database/repository.ts";
|
|
2
|
-
import type { SeptumRepository } from "../database/repository.ts";
|
|
3
|
-
|
|
4
|
-
export interface ParsedSymbolQuery {
|
|
5
|
-
raw: string;
|
|
6
|
-
container?: string; // e.g. "OrderController" or "App\\Http\\Controllers\\OrderController"
|
|
7
|
-
member?: string; // e.g. "calculateTotal"
|
|
8
|
-
kind?: SymbolKind;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface SuggestionMatch {
|
|
12
|
-
name: string;
|
|
13
|
-
kind: string;
|
|
14
|
-
signature: string;
|
|
15
|
-
line_start: number;
|
|
16
|
-
line_end: number;
|
|
17
|
-
similarity_score: number; // 0.0 - 1.0
|
|
18
|
-
file_path?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface SymbolLocationResult {
|
|
22
|
-
query: string;
|
|
23
|
-
parsed: ParsedSymbolQuery;
|
|
24
|
-
found: boolean;
|
|
25
|
-
file_path?: string;
|
|
26
|
-
container_name?: string;
|
|
27
|
-
exact_symbol?: {
|
|
28
|
-
name: string;
|
|
29
|
-
kind: string;
|
|
30
|
-
signature: string;
|
|
31
|
-
visibility: string;
|
|
32
|
-
line_start: number;
|
|
33
|
-
line_end: number;
|
|
34
|
-
};
|
|
35
|
-
sibling_methods?: string[];
|
|
36
|
-
suggestions: SuggestionMatch[];
|
|
37
|
-
message: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class SymbolLocator {
|
|
41
|
-
constructor(private repo: SeptumRepository) {}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Parse error log strings, method call syntax, or symbol names into container & member.
|
|
45
|
-
* Examples:
|
|
46
|
-
* - "Method OrderController::calculateTotal() does not exist"
|
|
47
|
-
* - "Call to undefined method App\\Http\\Controllers\\OrderController::calculateTotal()"
|
|
48
|
-
* - "OrderController->calculateTotal()"
|
|
49
|
-
* - "OrderController.calculateTotal"
|
|
50
|
-
* - "OrderController"
|
|
51
|
-
* - "calculateTotal"
|
|
52
|
-
*/
|
|
53
|
-
public parseQuery(rawQuery: string): ParsedSymbolQuery {
|
|
54
|
-
const trimmed = rawQuery.trim();
|
|
55
|
-
|
|
56
|
-
// 1. Error string matching: "Method X::y() does not exist" or "Call to undefined method X::y()"
|
|
57
|
-
const methodNotExistMatch = trimmed.match(/(?:Method|method)\s+([A-Za-z0-9_\\\/\.]+)::([A-Za-z0-9_]+)\s*\(\)?/i);
|
|
58
|
-
if (methodNotExistMatch) {
|
|
59
|
-
return {
|
|
60
|
-
raw: trimmed,
|
|
61
|
-
container: methodNotExistMatch[1],
|
|
62
|
-
member: methodNotExistMatch[2],
|
|
63
|
-
kind: "method",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 2. Python / JS error: "'X' object has no attribute 'y'"
|
|
68
|
-
const pyAttrMatch = trimmed.match(/['"]?([A-Za-z0-9_]+)['"]?\s+object has no attribute\s+['"]?([A-Za-z0-9_]+)['"]?/i);
|
|
69
|
-
if (pyAttrMatch) {
|
|
70
|
-
return {
|
|
71
|
-
raw: trimmed,
|
|
72
|
-
container: pyAttrMatch[1],
|
|
73
|
-
member: pyAttrMatch[2],
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 3. Class::member or Class->member or Class.member
|
|
78
|
-
const staticCallMatch = trimmed.match(/^([A-Za-z0-9_\\\/\.]+)::([A-Za-z0-9_]+)(?:\(\))?$/);
|
|
79
|
-
if (staticCallMatch) {
|
|
80
|
-
return {
|
|
81
|
-
raw: trimmed,
|
|
82
|
-
container: staticCallMatch[1],
|
|
83
|
-
member: staticCallMatch[2],
|
|
84
|
-
kind: "method",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const arrowCallMatch = trimmed.match(/^([A-Za-z0-9_\\\/\.]+)->([A-Za-z0-9_]+)(?:\(\))?$/);
|
|
89
|
-
if (arrowCallMatch) {
|
|
90
|
-
return {
|
|
91
|
-
raw: trimmed,
|
|
92
|
-
container: arrowCallMatch[1],
|
|
93
|
-
member: arrowCallMatch[2],
|
|
94
|
-
kind: "method",
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const dotCallMatch = trimmed.match(/^([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)(?:\(\))?$/);
|
|
99
|
-
if (dotCallMatch) {
|
|
100
|
-
return {
|
|
101
|
-
raw: trimmed,
|
|
102
|
-
container: dotCallMatch[1],
|
|
103
|
-
member: dotCallMatch[2],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 4. Function call: "calculateTotal()"
|
|
108
|
-
const funcMatch = trimmed.match(/^([A-Za-z0-9_]+)\(\)$/);
|
|
109
|
-
if (funcMatch) {
|
|
110
|
-
return {
|
|
111
|
-
raw: trimmed,
|
|
112
|
-
member: funcMatch[1],
|
|
113
|
-
kind: "function",
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 5. Bare word: could be class or function
|
|
118
|
-
return {
|
|
119
|
-
raw: trimmed,
|
|
120
|
-
member: trimmed,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Locate the symbol or diagnostic resolution in the database.
|
|
126
|
-
*/
|
|
127
|
-
public locate(rawQuery: string, domainFilter?: string): SymbolLocationResult {
|
|
128
|
-
const parsed = this.parseQuery(rawQuery);
|
|
129
|
-
|
|
130
|
-
// If a container (Class / Interface) is specified
|
|
131
|
-
if (parsed.container) {
|
|
132
|
-
return this.locateWithContainer(parsed, domainFilter);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// If only member or bare name is provided
|
|
136
|
-
return this.locateBareMember(parsed, domainFilter);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private locateWithContainer(
|
|
140
|
-
parsed: ParsedSymbolQuery,
|
|
141
|
-
domainFilter?: string
|
|
142
|
-
): SymbolLocationResult {
|
|
143
|
-
const containerQuery = parsed.container!;
|
|
144
|
-
const simpleContainerName = containerQuery.split(/\\|\//).pop() || containerQuery;
|
|
145
|
-
const memberName = parsed.member;
|
|
146
|
-
|
|
147
|
-
// Search for container in symbols table or files table
|
|
148
|
-
const containers = this.repo.findContainers(simpleContainerName);
|
|
149
|
-
|
|
150
|
-
if (containers.length === 0) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
1
|
+
import type { FileRecord, SymbolKind, SymbolRecord } from "../database/repository.ts";
|
|
2
|
+
import type { SeptumRepository } from "../database/repository.ts";
|
|
3
|
+
|
|
4
|
+
export interface ParsedSymbolQuery {
|
|
5
|
+
raw: string;
|
|
6
|
+
container?: string; // e.g. "OrderController" or "App\\Http\\Controllers\\OrderController"
|
|
7
|
+
member?: string; // e.g. "calculateTotal"
|
|
8
|
+
kind?: SymbolKind;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SuggestionMatch {
|
|
12
|
+
name: string;
|
|
13
|
+
kind: string;
|
|
14
|
+
signature: string;
|
|
15
|
+
line_start: number;
|
|
16
|
+
line_end: number;
|
|
17
|
+
similarity_score: number; // 0.0 - 1.0
|
|
18
|
+
file_path?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SymbolLocationResult {
|
|
22
|
+
query: string;
|
|
23
|
+
parsed: ParsedSymbolQuery;
|
|
24
|
+
found: boolean;
|
|
25
|
+
file_path?: string;
|
|
26
|
+
container_name?: string;
|
|
27
|
+
exact_symbol?: {
|
|
28
|
+
name: string;
|
|
29
|
+
kind: string;
|
|
30
|
+
signature: string;
|
|
31
|
+
visibility: string;
|
|
32
|
+
line_start: number;
|
|
33
|
+
line_end: number;
|
|
34
|
+
};
|
|
35
|
+
sibling_methods?: string[];
|
|
36
|
+
suggestions: SuggestionMatch[];
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SymbolLocator {
|
|
41
|
+
constructor(private repo: SeptumRepository) {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse error log strings, method call syntax, or symbol names into container & member.
|
|
45
|
+
* Examples:
|
|
46
|
+
* - "Method OrderController::calculateTotal() does not exist"
|
|
47
|
+
* - "Call to undefined method App\\Http\\Controllers\\OrderController::calculateTotal()"
|
|
48
|
+
* - "OrderController->calculateTotal()"
|
|
49
|
+
* - "OrderController.calculateTotal"
|
|
50
|
+
* - "OrderController"
|
|
51
|
+
* - "calculateTotal"
|
|
52
|
+
*/
|
|
53
|
+
public parseQuery(rawQuery: string): ParsedSymbolQuery {
|
|
54
|
+
const trimmed = rawQuery.trim();
|
|
55
|
+
|
|
56
|
+
// 1. Error string matching: "Method X::y() does not exist" or "Call to undefined method X::y()"
|
|
57
|
+
const methodNotExistMatch = trimmed.match(/(?:Method|method)\s+([A-Za-z0-9_\\\/\.]+)::([A-Za-z0-9_]+)\s*\(\)?/i);
|
|
58
|
+
if (methodNotExistMatch) {
|
|
59
|
+
return {
|
|
60
|
+
raw: trimmed,
|
|
61
|
+
container: methodNotExistMatch[1],
|
|
62
|
+
member: methodNotExistMatch[2],
|
|
63
|
+
kind: "method",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Python / JS error: "'X' object has no attribute 'y'"
|
|
68
|
+
const pyAttrMatch = trimmed.match(/['"]?([A-Za-z0-9_]+)['"]?\s+object has no attribute\s+['"]?([A-Za-z0-9_]+)['"]?/i);
|
|
69
|
+
if (pyAttrMatch) {
|
|
70
|
+
return {
|
|
71
|
+
raw: trimmed,
|
|
72
|
+
container: pyAttrMatch[1],
|
|
73
|
+
member: pyAttrMatch[2],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Class::member or Class->member or Class.member
|
|
78
|
+
const staticCallMatch = trimmed.match(/^([A-Za-z0-9_\\\/\.]+)::([A-Za-z0-9_]+)(?:\(\))?$/);
|
|
79
|
+
if (staticCallMatch) {
|
|
80
|
+
return {
|
|
81
|
+
raw: trimmed,
|
|
82
|
+
container: staticCallMatch[1],
|
|
83
|
+
member: staticCallMatch[2],
|
|
84
|
+
kind: "method",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const arrowCallMatch = trimmed.match(/^([A-Za-z0-9_\\\/\.]+)->([A-Za-z0-9_]+)(?:\(\))?$/);
|
|
89
|
+
if (arrowCallMatch) {
|
|
90
|
+
return {
|
|
91
|
+
raw: trimmed,
|
|
92
|
+
container: arrowCallMatch[1],
|
|
93
|
+
member: arrowCallMatch[2],
|
|
94
|
+
kind: "method",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const dotCallMatch = trimmed.match(/^([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)(?:\(\))?$/);
|
|
99
|
+
if (dotCallMatch) {
|
|
100
|
+
return {
|
|
101
|
+
raw: trimmed,
|
|
102
|
+
container: dotCallMatch[1],
|
|
103
|
+
member: dotCallMatch[2],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 4. Function call: "calculateTotal()"
|
|
108
|
+
const funcMatch = trimmed.match(/^([A-Za-z0-9_]+)\(\)$/);
|
|
109
|
+
if (funcMatch) {
|
|
110
|
+
return {
|
|
111
|
+
raw: trimmed,
|
|
112
|
+
member: funcMatch[1],
|
|
113
|
+
kind: "function",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 5. Bare word: could be class or function
|
|
118
|
+
return {
|
|
119
|
+
raw: trimmed,
|
|
120
|
+
member: trimmed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Locate the symbol or diagnostic resolution in the database.
|
|
126
|
+
*/
|
|
127
|
+
public locate(rawQuery: string, domainFilter?: string): SymbolLocationResult {
|
|
128
|
+
const parsed = this.parseQuery(rawQuery);
|
|
129
|
+
|
|
130
|
+
// If a container (Class / Interface) is specified
|
|
131
|
+
if (parsed.container) {
|
|
132
|
+
return this.locateWithContainer(parsed, domainFilter);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If only member or bare name is provided
|
|
136
|
+
return this.locateBareMember(parsed, domainFilter);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private locateWithContainer(
|
|
140
|
+
parsed: ParsedSymbolQuery,
|
|
141
|
+
domainFilter?: string
|
|
142
|
+
): SymbolLocationResult {
|
|
143
|
+
const containerQuery = parsed.container!;
|
|
144
|
+
const simpleContainerName = containerQuery.split(/\\|\//).pop() || containerQuery;
|
|
145
|
+
const memberName = parsed.member;
|
|
146
|
+
|
|
147
|
+
// Search for container in symbols table or files table
|
|
148
|
+
const containers = this.repo.findContainers(simpleContainerName);
|
|
149
|
+
|
|
150
|
+
if (containers.length === 0) {
|
|
151
|
+
// Container not found directly: search for fuzzy container matches across the catalog
|
|
152
|
+
const allSymbols = this.repo.getAllSymbolsWithFiles();
|
|
153
|
+
const containerCandidates = allSymbols.filter((s) =>
|
|
154
|
+
["class", "interface", "trait", "enum", "struct"].includes(s.kind)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Score containers by similarity
|
|
158
|
+
const scoredContainers = containerCandidates
|
|
159
|
+
.map((s) => {
|
|
160
|
+
const bare = s.name.includes("::")
|
|
161
|
+
? s.name.split("::")[0]
|
|
162
|
+
: s.name.split(/\\|\//).pop() || s.name;
|
|
163
|
+
const score = calculateSimilarity(simpleContainerName, bare);
|
|
164
|
+
return {
|
|
165
|
+
name: s.name,
|
|
166
|
+
bare,
|
|
167
|
+
kind: s.kind,
|
|
168
|
+
signature: s.signature,
|
|
169
|
+
line_start: s.line_start,
|
|
170
|
+
line_end: s.line_end,
|
|
171
|
+
file_path: s.file_path,
|
|
172
|
+
similarity_score: Math.round(score * 100) / 100,
|
|
173
|
+
};
|
|
174
|
+
})
|
|
175
|
+
.filter((c) => c.similarity_score >= 0.3)
|
|
176
|
+
.sort((a, b) => b.similarity_score - a.similarity_score);
|
|
177
|
+
|
|
178
|
+
// Deduplicate by name + file_path
|
|
179
|
+
const seenContainers = new Set<string>();
|
|
180
|
+
const uniqueContainers = scoredContainers.filter((c) => {
|
|
181
|
+
const key = `${c.name}@${c.file_path}`;
|
|
182
|
+
if (seenContainers.has(key)) return false;
|
|
183
|
+
seenContainers.add(key);
|
|
184
|
+
return true;
|
|
185
|
+
}).slice(0, 5);
|
|
186
|
+
|
|
187
|
+
// If a member was requested, prioritize methods within top fuzzy candidate containers
|
|
188
|
+
let memberSuggestions: SuggestionMatch[] = [];
|
|
189
|
+
if (memberName) {
|
|
190
|
+
const topCandidateFilePaths = new Set(uniqueContainers.map((c) => c.file_path));
|
|
191
|
+
|
|
192
|
+
const candidateMethods = allSymbols.filter(
|
|
193
|
+
(s) =>
|
|
194
|
+
(s.kind === "method" || s.kind === "function") &&
|
|
195
|
+
topCandidateFilePaths.has(s.file_path)
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const methodsToScore =
|
|
199
|
+
candidateMethods.length > 0
|
|
200
|
+
? candidateMethods
|
|
201
|
+
: allSymbols
|
|
202
|
+
.filter(
|
|
203
|
+
(s) =>
|
|
204
|
+
(s.kind === "method" || s.kind === "function") &&
|
|
205
|
+
Math.abs(s.name.length - memberName.length) <= 8
|
|
206
|
+
)
|
|
207
|
+
.slice(0, 100);
|
|
208
|
+
|
|
209
|
+
memberSuggestions = methodsToScore
|
|
210
|
+
.map((s) => {
|
|
211
|
+
const bare = s.name.includes("::") ? s.name.split("::")[1] : s.name;
|
|
212
|
+
const score = calculateSimilarity(memberName, bare);
|
|
213
|
+
return {
|
|
214
|
+
name: s.name,
|
|
215
|
+
kind: s.kind,
|
|
216
|
+
signature: `${s.signature} (${s.file_path}:${s.line_start})`,
|
|
217
|
+
line_start: s.line_start,
|
|
218
|
+
line_end: s.line_end,
|
|
219
|
+
similarity_score: Math.round(score * 100) / 100,
|
|
220
|
+
file_path: s.file_path,
|
|
221
|
+
};
|
|
222
|
+
})
|
|
223
|
+
.filter((s) => s.similarity_score >= 0.35)
|
|
224
|
+
.sort((a, b) => b.similarity_score - a.similarity_score)
|
|
225
|
+
.slice(0, 5);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const suggestions: SuggestionMatch[] = uniqueContainers.map((c) => ({
|
|
229
|
+
name: c.name,
|
|
230
|
+
kind: c.kind,
|
|
231
|
+
signature: `${c.signature || c.kind} (${c.file_path}:${c.line_start})`,
|
|
232
|
+
line_start: c.line_start,
|
|
233
|
+
line_end: c.line_end,
|
|
234
|
+
similarity_score: c.similarity_score,
|
|
235
|
+
file_path: c.file_path,
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
// Combine container suggestions with member suggestions if available
|
|
239
|
+
const combinedSuggestions = [...suggestions, ...memberSuggestions]
|
|
240
|
+
.sort((a, b) => b.similarity_score - a.similarity_score)
|
|
241
|
+
.slice(0, 7);
|
|
242
|
+
|
|
243
|
+
const topSuggestion = combinedSuggestions.length > 0 ? combinedSuggestions[0].name : null;
|
|
244
|
+
const hint = topSuggestion
|
|
245
|
+
? ` Container '${containerQuery}' was not found. Did you mean '${topSuggestion}'?`
|
|
246
|
+
: ` Container '${containerQuery}' was not found in cataloged files.`;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
query: parsed.raw,
|
|
250
|
+
parsed,
|
|
251
|
+
found: false,
|
|
252
|
+
suggestions: combinedSuggestions,
|
|
253
|
+
message: `${hint} Run 'septum ingest' if this file was recently added.`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Pick primary container matching
|
|
258
|
+
const primary = containers[0];
|
|
259
|
+
const fileSymbols = this.repo.getSymbolsByFileId(primary.file.id);
|
|
260
|
+
|
|
261
|
+
// Extract methods in this container / file
|
|
262
|
+
const methodSymbols = fileSymbols.filter(
|
|
263
|
+
(s) => s.kind === "method" || s.kind === "function"
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const siblingMethodNames = Array.from(
|
|
267
|
+
new Set(
|
|
268
|
+
methodSymbols.map((m) => {
|
|
269
|
+
if (m.name.includes("::")) {
|
|
270
|
+
return m.name.split("::")[1];
|
|
271
|
+
}
|
|
272
|
+
return m.name;
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (!memberName) {
|
|
278
|
+
// User only asked for the container
|
|
279
|
+
return {
|
|
280
|
+
query: parsed.raw,
|
|
281
|
+
parsed,
|
|
282
|
+
found: true,
|
|
283
|
+
file_path: primary.file.path,
|
|
284
|
+
container_name: simpleContainerName,
|
|
285
|
+
exact_symbol: primary.symbol
|
|
286
|
+
? {
|
|
287
|
+
name: primary.symbol.name,
|
|
288
|
+
kind: primary.symbol.kind,
|
|
289
|
+
signature: primary.symbol.signature,
|
|
290
|
+
visibility: primary.symbol.visibility,
|
|
291
|
+
line_start: primary.symbol.line_start,
|
|
292
|
+
line_end: primary.symbol.line_end,
|
|
293
|
+
}
|
|
294
|
+
: undefined,
|
|
295
|
+
sibling_methods: siblingMethodNames,
|
|
296
|
+
suggestions: [],
|
|
297
|
+
message: `Found container '${simpleContainerName}' in ${primary.file.path} with ${siblingMethodNames.length} methods.`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Search for exact member match
|
|
302
|
+
const exactMatch = methodSymbols.find((s) => {
|
|
303
|
+
const bareName = s.name.includes("::") ? s.name.split("::")[1] : s.name;
|
|
304
|
+
return bareName.toLowerCase() === memberName.toLowerCase();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (exactMatch) {
|
|
308
|
+
return {
|
|
309
|
+
query: parsed.raw,
|
|
310
|
+
parsed,
|
|
311
|
+
found: true,
|
|
312
|
+
file_path: primary.file.path,
|
|
313
|
+
container_name: simpleContainerName,
|
|
314
|
+
exact_symbol: {
|
|
315
|
+
name: exactMatch.name,
|
|
316
|
+
kind: exactMatch.kind,
|
|
317
|
+
signature: exactMatch.signature,
|
|
318
|
+
visibility: exactMatch.visibility,
|
|
319
|
+
line_start: exactMatch.line_start,
|
|
320
|
+
line_end: exactMatch.line_end,
|
|
321
|
+
},
|
|
322
|
+
sibling_methods: siblingMethodNames,
|
|
323
|
+
suggestions: [],
|
|
324
|
+
message: `Exact method '${memberName}' found in ${primary.file.path} at line ${exactMatch.line_start}.`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Member NOT found in container: calculate fuzzy suggestions
|
|
329
|
+
const suggestions: SuggestionMatch[] = methodSymbols
|
|
330
|
+
.map((s) => {
|
|
331
|
+
const bareName = s.name.includes("::") ? s.name.split("::")[1] : s.name;
|
|
332
|
+
const score = calculateSimilarity(memberName, bareName);
|
|
333
|
+
return {
|
|
334
|
+
name: bareName,
|
|
335
|
+
kind: s.kind,
|
|
336
|
+
signature: s.signature,
|
|
337
|
+
line_start: s.line_start,
|
|
338
|
+
line_end: s.line_end,
|
|
339
|
+
similarity_score: Math.round(score * 100) / 100,
|
|
340
|
+
};
|
|
341
|
+
})
|
|
342
|
+
.filter((s) => s.similarity_score >= 0.25)
|
|
343
|
+
.sort((a, b) => b.similarity_score - a.similarity_score)
|
|
344
|
+
.slice(0, 5);
|
|
345
|
+
|
|
346
|
+
const topSuggestion = suggestions.length > 0 ? suggestions[0].name : null;
|
|
347
|
+
const hint = topSuggestion
|
|
348
|
+
? ` Method '${memberName}' does not exist! Did you mean '${topSuggestion}'?`
|
|
349
|
+
: ` Method '${memberName}' does not exist.`;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
query: parsed.raw,
|
|
353
|
+
parsed,
|
|
354
|
+
found: false,
|
|
355
|
+
file_path: primary.file.path,
|
|
356
|
+
container_name: simpleContainerName,
|
|
357
|
+
sibling_methods: siblingMethodNames,
|
|
358
|
+
suggestions,
|
|
359
|
+
message: `File: ${primary.file.path}.${hint} Available methods: [${siblingMethodNames.join(", ")}].`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private locateBareMember(
|
|
364
|
+
parsed: ParsedSymbolQuery,
|
|
365
|
+
domainFilter?: string
|
|
366
|
+
): SymbolLocationResult {
|
|
367
|
+
const targetName = parsed.member!;
|
|
368
|
+
const directMatches = this.repo.findSymbolsByName(targetName);
|
|
369
|
+
|
|
370
|
+
if (directMatches.length > 0) {
|
|
371
|
+
const first = directMatches[0];
|
|
372
|
+
return {
|
|
373
|
+
query: parsed.raw,
|
|
374
|
+
parsed,
|
|
375
|
+
found: true,
|
|
376
|
+
file_path: first.file_path,
|
|
377
|
+
container_name: first.name.includes("::") ? first.name.split("::")[0] : undefined,
|
|
378
|
+
exact_symbol: {
|
|
379
|
+
name: first.name,
|
|
380
|
+
kind: first.kind,
|
|
381
|
+
signature: first.signature,
|
|
382
|
+
visibility: first.visibility,
|
|
383
|
+
line_start: first.line_start,
|
|
384
|
+
line_end: first.line_end,
|
|
385
|
+
},
|
|
386
|
+
suggestions: [],
|
|
387
|
+
message: `Found exact symbol '${first.name}' in ${first.file_path}:${first.line_start}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// If bare name is a class name
|
|
392
|
+
const containers = this.repo.findContainers(targetName);
|
|
393
|
+
if (containers.length > 0) {
|
|
394
|
+
const primary = containers[0];
|
|
395
|
+
const fileSymbols = this.repo.getSymbolsByFileId(primary.file.id);
|
|
396
|
+
const siblingMethods = fileSymbols
|
|
397
|
+
.filter((s) => s.kind === "method" || s.kind === "function")
|
|
398
|
+
.map((s) => (s.name.includes("::") ? s.name.split("::")[1] : s.name));
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
query: parsed.raw,
|
|
402
|
+
parsed,
|
|
403
|
+
found: true,
|
|
404
|
+
file_path: primary.file.path,
|
|
405
|
+
container_name: targetName,
|
|
406
|
+
exact_symbol: primary.symbol
|
|
407
|
+
? {
|
|
408
|
+
name: primary.symbol.name,
|
|
409
|
+
kind: primary.symbol.kind,
|
|
410
|
+
signature: primary.symbol.signature,
|
|
411
|
+
visibility: primary.symbol.visibility,
|
|
412
|
+
line_start: primary.symbol.line_start,
|
|
413
|
+
line_end: primary.symbol.line_end,
|
|
414
|
+
}
|
|
415
|
+
: undefined,
|
|
416
|
+
sibling_methods: Array.from(new Set(siblingMethods)),
|
|
417
|
+
suggestions: [],
|
|
418
|
+
message: `Found container '${targetName}' in ${primary.file.path}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Nothing found directly: Search all symbols for fuzzy suggestions
|
|
423
|
+
const allSymbols = this.repo.getAllSymbolsWithFiles();
|
|
424
|
+
const suggestions: SuggestionMatch[] = allSymbols
|
|
425
|
+
.map((s) => {
|
|
426
|
+
const bareName = s.name.includes("::") ? s.name.split("::")[1] : s.name;
|
|
427
|
+
const scoreBare = calculateSimilarity(targetName, bareName);
|
|
428
|
+
const scoreFull = calculateSimilarity(targetName, s.name);
|
|
429
|
+
const score = Math.max(scoreBare, scoreFull);
|
|
430
|
+
return {
|
|
431
|
+
name: s.name,
|
|
432
|
+
kind: s.kind,
|
|
433
|
+
signature: `${s.signature} (${s.file_path}:${s.line_start})`,
|
|
434
|
+
line_start: s.line_start,
|
|
435
|
+
line_end: s.line_end,
|
|
436
|
+
similarity_score: Math.round(score * 100) / 100,
|
|
437
|
+
file_path: s.file_path,
|
|
438
|
+
};
|
|
439
|
+
})
|
|
440
|
+
.filter((s) => s.similarity_score >= 0.3)
|
|
441
|
+
.sort((a, b) => b.similarity_score - a.similarity_score)
|
|
442
|
+
.slice(0, 5);
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
query: parsed.raw,
|
|
446
|
+
parsed,
|
|
447
|
+
found: false,
|
|
448
|
+
suggestions,
|
|
449
|
+
message: `Symbol '${targetName}' not found. ${suggestions.length > 0 ? `Did you mean '${suggestions[0].name}'?` : ""}`,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Enhanced similarity matching:
|
|
456
|
+
* Combines Damerau-Levenshtein distance (handling typos & transpositions),
|
|
457
|
+
* prefix/suffix matching, substring inclusion, and token Jaccard similarity.
|
|
458
|
+
*/
|
|
459
|
+
export function calculateSimilarity(source: string, target: string): number {
|
|
460
|
+
const s1 = source.toLowerCase();
|
|
461
|
+
const s2 = target.toLowerCase();
|
|
462
|
+
|
|
463
|
+
if (s1 === s2) return 1.0;
|
|
464
|
+
if (!s1 || !s2) return 0.0;
|
|
465
|
+
|
|
466
|
+
// Prefix bonus: highly relevant for autocomplete-style / partial searches
|
|
467
|
+
let prefixBonus = 0;
|
|
468
|
+
if (s2.startsWith(s1) || s1.startsWith(s2)) {
|
|
469
|
+
const minLen = Math.min(s1.length, s2.length);
|
|
470
|
+
const maxLen = Math.max(s1.length, s2.length);
|
|
471
|
+
prefixBonus = 0.25 * (minLen / maxLen);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Substring inclusion bonus
|
|
475
|
+
let substringBonus = 0;
|
|
476
|
+
if (s2.includes(s1) || s1.includes(s2)) {
|
|
477
|
+
const minLen = Math.min(s1.length, s2.length);
|
|
478
|
+
const maxLen = Math.max(s1.length, s2.length);
|
|
479
|
+
substringBonus = 0.3 * (minLen / maxLen);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Token-based matching (CamelCase, snake_case, kebab-case, namespaces)
|
|
483
|
+
const tokenize = (str: string) =>
|
|
484
|
+
str
|
|
485
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
486
|
+
.toLowerCase()
|
|
487
|
+
.split(/[\s_\-\.\:\/\\]+/)
|
|
488
|
+
.filter((t) => t.length > 1);
|
|
489
|
+
|
|
490
|
+
const t1 = tokenize(source);
|
|
491
|
+
const t2 = tokenize(target);
|
|
492
|
+
let tokenBonus = 0;
|
|
493
|
+
if (t1.length > 0 && t2.length > 0) {
|
|
494
|
+
const sharedTokens = t1.filter((token) =>
|
|
495
|
+
t2.some((t) => t.includes(token) || token.includes(t))
|
|
496
|
+
);
|
|
497
|
+
const allUniqueTokens = new Set([...t1, ...t2]);
|
|
498
|
+
const jaccard = sharedTokens.length / (allUniqueTokens.size || 1);
|
|
499
|
+
tokenBonus = jaccard * 0.35;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Damerau-Levenshtein Matrix
|
|
503
|
+
const len1 = s1.length;
|
|
504
|
+
const len2 = s2.length;
|
|
505
|
+
const matrix: number[][] = [];
|
|
506
|
+
|
|
507
|
+
for (let i = 0; i <= len1; i++) {
|
|
508
|
+
matrix[i] = [i];
|
|
509
|
+
}
|
|
510
|
+
for (let j = 0; j <= len2; j++) {
|
|
511
|
+
matrix[0][j] = j;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (let i = 1; i <= len1; i++) {
|
|
515
|
+
for (let j = 1; j <= len2; j++) {
|
|
516
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
517
|
+
let minCost = Math.min(
|
|
518
|
+
matrix[i - 1][j] + 1, // deletion
|
|
519
|
+
matrix[i][j - 1] + 1, // insertion
|
|
520
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
// Transposition check (Damerau)
|
|
524
|
+
if (
|
|
525
|
+
i > 1 &&
|
|
526
|
+
j > 1 &&
|
|
527
|
+
s1[i - 1] === s2[j - 2] &&
|
|
528
|
+
s1[i - 2] === s2[j - 1]
|
|
529
|
+
) {
|
|
530
|
+
minCost = Math.min(minCost, matrix[i - 2][j - 2] + 1);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
matrix[i][j] = minCost;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const distance = matrix[len1][len2];
|
|
538
|
+
const maxLen = Math.max(len1, len2);
|
|
539
|
+
const rawSimilarity = 1.0 - distance / maxLen;
|
|
540
|
+
|
|
541
|
+
const totalScore = rawSimilarity + prefixBonus + substringBonus + tokenBonus;
|
|
542
|
+
return Math.min(1.0, Math.max(0.0, totalScore));
|
|
543
|
+
}
|