septum 0.1.1 → 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/package.json
CHANGED
|
@@ -201,23 +201,42 @@ export class ModuleResolver {
|
|
|
201
201
|
return null;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
public toCanonicalPath(filePath: string): string {
|
|
205
|
+
let resolved = path.isAbsolute(filePath)
|
|
206
|
+
? path.normalize(filePath)
|
|
207
|
+
: path.resolve(this.projectRoot, filePath);
|
|
208
|
+
|
|
209
|
+
if (process.platform === "win32") {
|
|
210
|
+
// Normalize drive letter to uppercase (e.g. c:\ -> C:\)
|
|
211
|
+
resolved = resolved.replace(/^[a-zA-Z]:/, (m) => m.toUpperCase());
|
|
212
|
+
}
|
|
213
|
+
return resolved;
|
|
214
|
+
}
|
|
215
|
+
|
|
204
216
|
private isSubPathOrSame(targetPath: string, rootPath: string): boolean {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
217
|
+
const canonicalTarget = this.toCanonicalPath(targetPath);
|
|
218
|
+
const canonicalRoot = this.toCanonicalPath(rootPath);
|
|
219
|
+
|
|
220
|
+
if (process.platform === "win32") {
|
|
221
|
+
const lowerTarget = canonicalTarget.toLowerCase();
|
|
222
|
+
const lowerRoot = canonicalRoot.toLowerCase();
|
|
223
|
+
if (lowerTarget === lowerRoot) return true;
|
|
224
|
+
const rootWithSep = lowerRoot.endsWith(path.sep)
|
|
225
|
+
? lowerRoot
|
|
226
|
+
: lowerRoot + path.sep;
|
|
227
|
+
return lowerTarget.startsWith(rootWithSep);
|
|
228
|
+
}
|
|
207
229
|
|
|
208
|
-
if (
|
|
209
|
-
const rootWithSep =
|
|
210
|
-
?
|
|
211
|
-
:
|
|
230
|
+
if (canonicalTarget === canonicalRoot) return true;
|
|
231
|
+
const rootWithSep = canonicalRoot.endsWith(path.sep)
|
|
232
|
+
? canonicalRoot
|
|
233
|
+
: canonicalRoot + path.sep;
|
|
212
234
|
|
|
213
|
-
return
|
|
235
|
+
return canonicalTarget.startsWith(rootWithSep);
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
private toAbsolutePath(filePath: string): string {
|
|
217
|
-
|
|
218
|
-
return path.normalize(filePath);
|
|
219
|
-
}
|
|
220
|
-
return path.resolve(this.projectRoot, filePath);
|
|
239
|
+
return this.toCanonicalPath(filePath);
|
|
221
240
|
}
|
|
222
241
|
|
|
223
242
|
private extractSymbolName(target: string): string | null {
|
|
@@ -148,12 +148,109 @@ export class SymbolLocator {
|
|
|
148
148
|
const containers = this.repo.findContainers(simpleContainerName);
|
|
149
149
|
|
|
150
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
|
+
|
|
151
248
|
return {
|
|
152
249
|
query: parsed.raw,
|
|
153
250
|
parsed,
|
|
154
251
|
found: false,
|
|
155
|
-
suggestions:
|
|
156
|
-
message:
|
|
252
|
+
suggestions: combinedSuggestions,
|
|
253
|
+
message: `${hint} Run 'septum ingest' if this file was recently added.`,
|
|
157
254
|
};
|
|
158
255
|
}
|
|
159
256
|
|
|
@@ -327,7 +424,9 @@ export class SymbolLocator {
|
|
|
327
424
|
const suggestions: SuggestionMatch[] = allSymbols
|
|
328
425
|
.map((s) => {
|
|
329
426
|
const bareName = s.name.includes("::") ? s.name.split("::")[1] : s.name;
|
|
330
|
-
const
|
|
427
|
+
const scoreBare = calculateSimilarity(targetName, bareName);
|
|
428
|
+
const scoreFull = calculateSimilarity(targetName, s.name);
|
|
429
|
+
const score = Math.max(scoreBare, scoreFull);
|
|
331
430
|
return {
|
|
332
431
|
name: s.name,
|
|
333
432
|
kind: s.kind,
|
|
@@ -335,9 +434,10 @@ export class SymbolLocator {
|
|
|
335
434
|
line_start: s.line_start,
|
|
336
435
|
line_end: s.line_end,
|
|
337
436
|
similarity_score: Math.round(score * 100) / 100,
|
|
437
|
+
file_path: s.file_path,
|
|
338
438
|
};
|
|
339
439
|
})
|
|
340
|
-
.filter((s) => s.similarity_score >= 0.
|
|
440
|
+
.filter((s) => s.similarity_score >= 0.3)
|
|
341
441
|
.sort((a, b) => b.similarity_score - a.similarity_score)
|
|
342
442
|
.slice(0, 5);
|
|
343
443
|
|
|
@@ -352,7 +452,9 @@ export class SymbolLocator {
|
|
|
352
452
|
}
|
|
353
453
|
|
|
354
454
|
/**
|
|
355
|
-
*
|
|
455
|
+
* Enhanced similarity matching:
|
|
456
|
+
* Combines Damerau-Levenshtein distance (handling typos & transpositions),
|
|
457
|
+
* prefix/suffix matching, substring inclusion, and token Jaccard similarity.
|
|
356
458
|
*/
|
|
357
459
|
export function calculateSimilarity(source: string, target: string): number {
|
|
358
460
|
const s1 = source.toLowerCase();
|
|
@@ -361,20 +463,43 @@ export function calculateSimilarity(source: string, target: string): number {
|
|
|
361
463
|
if (s1 === s2) return 1.0;
|
|
362
464
|
if (!s1 || !s2) return 0.0;
|
|
363
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
|
+
|
|
364
474
|
// Substring inclusion bonus
|
|
475
|
+
let substringBonus = 0;
|
|
365
476
|
if (s2.includes(s1) || s1.includes(s2)) {
|
|
366
477
|
const minLen = Math.min(s1.length, s2.length);
|
|
367
478
|
const maxLen = Math.max(s1.length, s2.length);
|
|
368
|
-
|
|
479
|
+
substringBonus = 0.3 * (minLen / maxLen);
|
|
369
480
|
}
|
|
370
481
|
|
|
371
|
-
// Token-based matching (
|
|
372
|
-
const tokenize = (str: string) =>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
377
501
|
|
|
502
|
+
// Damerau-Levenshtein Matrix
|
|
378
503
|
const len1 = s1.length;
|
|
379
504
|
const len2 = s2.length;
|
|
380
505
|
const matrix: number[][] = [];
|
|
@@ -389,11 +514,23 @@ export function calculateSimilarity(source: string, target: string): number {
|
|
|
389
514
|
for (let i = 1; i <= len1; i++) {
|
|
390
515
|
for (let j = 1; j <= len2; j++) {
|
|
391
516
|
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
392
|
-
|
|
517
|
+
let minCost = Math.min(
|
|
393
518
|
matrix[i - 1][j] + 1, // deletion
|
|
394
519
|
matrix[i][j - 1] + 1, // insertion
|
|
395
520
|
matrix[i - 1][j - 1] + cost // substitution
|
|
396
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;
|
|
397
534
|
}
|
|
398
535
|
}
|
|
399
536
|
|
|
@@ -401,5 +538,6 @@ export function calculateSimilarity(source: string, target: string): number {
|
|
|
401
538
|
const maxLen = Math.max(len1, len2);
|
|
402
539
|
const rawSimilarity = 1.0 - distance / maxLen;
|
|
403
540
|
|
|
404
|
-
|
|
541
|
+
const totalScore = rawSimilarity + prefixBonus + substringBonus + tokenBonus;
|
|
542
|
+
return Math.min(1.0, Math.max(0.0, totalScore));
|
|
405
543
|
}
|