septum 0.1.1 → 0.1.3

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.
@@ -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: `Container '${containerQuery}' was not found in any cataloged files. Run 'septum ingest' if recently added.`,
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 score = calculateSimilarity(targetName, bareName);
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.35)
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
- * Standard Levenshtein distance with token substring bonus.
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
- return 0.5 + 0.5 * (minLen / maxLen);
479
+ substringBonus = 0.3 * (minLen / maxLen);
369
480
  }
370
481
 
371
- // Token-based matching (e.g. calculateTotal vs recalculateOrder shares 'calculate')
372
- const tokenize = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[\s_-]+/);
373
- const t1 = tokenize(s1);
374
- const t2 = tokenize(s2);
375
- const sharedTokens = t1.filter((token) => t2.some((t) => t.includes(token) || token.includes(t)));
376
- const tokenBonus = sharedTokens.length > 0 ? 0.3 : 0.0;
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
- matrix[i][j] = Math.min(
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
- return Math.min(1.0, Math.max(0.0, rawSimilarity + tokenBonus));
541
+ const totalScore = rawSimilarity + prefixBonus + substringBonus + tokenBonus;
542
+ return Math.min(1.0, Math.max(0.0, totalScore));
405
543
  }
@@ -5,9 +5,11 @@ import type {
5
5
  } from "../../types/index.ts";
6
6
  import type { SeptumRepository } from "../database/repository.ts";
7
7
  import { CallGraphTracer } from "./call-graph-tracer.ts";
8
+ import { SemanticSliceExtractorRegistry } from "../parser/extractors/registry.ts";
9
+ import { TopologyDetector } from "../discovery/topology-detector.ts";
8
10
 
9
11
  export class VerticalSliceTracer {
10
- constructor(private repo: SeptumRepository) {}
12
+ constructor(private repo: SeptumRepository, private projectRoot: string = process.cwd()) {}
11
13
 
12
14
  /**
13
15
  * Trace an end-to-end vertical slice from route/action/intent query.
@@ -25,16 +27,29 @@ export class VerticalSliceTracer {
25
27
  const lowerTrimmed = trimmed.toLowerCase();
26
28
  let allSlices = this.repo.getAllVerticalSlices();
27
29
 
28
- // Lazy fallback: If no slices cataloged, attempt heuristic call-graph tracing
29
- if (allSlices.length === 0) {
30
+ // Lazy on-demand fallback: If no slices cataloged, extract framework routes or trace call-graph
31
+ const hasCatalogData = typeof (this.repo as any).getAllDomains === "function" && this.repo.getAllDomains().length > 0;
32
+ if (allSlices.length === 0 && hasCatalogData) {
30
33
  try {
31
- const tracer = new CallGraphTracer(this.repo);
32
- const traced = tracer.traceAllSlices();
33
- if (traced.length > 0) {
34
- for (const s of traced) {
34
+ const topology = TopologyDetector.detect(this.projectRoot);
35
+ const semanticSlices = SemanticSliceExtractorRegistry.extractAllSlicesSync(
36
+ this.projectRoot,
37
+ topology.framework
38
+ );
39
+ if (semanticSlices.length > 0) {
40
+ for (const s of semanticSlices) {
35
41
  this.repo.upsertVerticalSlice(s);
36
42
  }
37
43
  allSlices = this.repo.getAllVerticalSlices();
44
+ } else {
45
+ const tracer = new CallGraphTracer(this.repo, this.projectRoot);
46
+ const traced = tracer.traceAllSlices();
47
+ if (traced.length > 0) {
48
+ for (const s of traced) {
49
+ this.repo.upsertVerticalSlice(s);
50
+ }
51
+ allSlices = this.repo.getAllVerticalSlices();
52
+ }
38
53
  }
39
54
  } catch (err: unknown) {
40
55
  const msg = err instanceof Error ? err.message : String(err);
@@ -1,68 +1,238 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const GetDomainCatalogSchema = z.object({
4
- domain: z.string().optional(),
5
- archetype: z.string().optional(),
6
- });
3
+ function withAliases(raw: unknown, aliases: Record<string, string>): unknown {
4
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
5
+ const obj = { ...(raw as Record<string, unknown>) };
6
+ for (const [alias, target] of Object.entries(aliases)) {
7
+ if (obj[alias] !== undefined && obj[target] === undefined) {
8
+ obj[target] = obj[alias];
9
+ }
10
+ }
11
+ return obj;
12
+ }
13
+
14
+ function normalizeBoundaryArgs(raw: unknown): unknown {
15
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
16
+ const obj = { ...(raw as Record<string, unknown>) };
17
+ const aliases: Record<string, string> = {
18
+ filePath: "file_path",
19
+ file: "file_path",
20
+ path: "file_path",
21
+ filePaths: "file_paths",
22
+ files: "file_paths",
23
+ paths: "file_paths",
24
+ proposedImports: "proposed_imports",
25
+ imports: "proposed_imports",
26
+ featureKey: "feature_key",
27
+ feature: "feature_key",
28
+ };
29
+ for (const [alias, target] of Object.entries(aliases)) {
30
+ if (obj[alias] !== undefined && obj[target] === undefined) {
31
+ obj[target] = obj[alias];
32
+ }
33
+ }
34
+ if (Array.isArray(obj.file_path) && !obj.file_paths) {
35
+ obj.file_paths = obj.file_path;
36
+ delete obj.file_path;
37
+ }
38
+ if (typeof obj.file_paths === "string" && !obj.file_path) {
39
+ obj.file_path = obj.file_paths;
40
+ delete obj.file_paths;
41
+ }
42
+ return obj;
43
+ }
44
+
45
+ export const GetDomainCatalogSchema = z.preprocess(
46
+ (args) =>
47
+ withAliases(args, {
48
+ domain_name: "domain",
49
+ domainName: "domain",
50
+ name: "domain",
51
+ archetype_filter: "archetype",
52
+ archetypeFilter: "archetype",
53
+ type: "archetype",
54
+ workspace: "workspace_path",
55
+ workspacePath: "workspace_path",
56
+ project_root: "workspace_path",
57
+ target_path: "workspace_path",
58
+ path: "workspace_path",
59
+ }),
60
+ z.object({
61
+ domain: z.string().optional(),
62
+ archetype: z.string().optional(),
63
+ workspace_path: z.string().optional(),
64
+ })
65
+ );
7
66
  export type GetDomainCatalogArgs = z.infer<typeof GetDomainCatalogSchema>;
8
67
 
9
- export const GetFeatureContextSchema = z.object({
10
- feature: z.string().min(1, "feature is required"),
11
- });
68
+ export const GetFeatureContextSchema = z.preprocess(
69
+ (args) =>
70
+ withAliases(args, {
71
+ feature_key: "feature",
72
+ featureKey: "feature",
73
+ name: "feature",
74
+ workspace: "workspace_path",
75
+ workspacePath: "workspace_path",
76
+ }),
77
+ z.object({
78
+ feature: z.string().min(1, "feature is required"),
79
+ workspace_path: z.string().optional(),
80
+ })
81
+ );
12
82
  export type GetFeatureContextArgs = z.infer<typeof GetFeatureContextSchema>;
13
83
 
14
- export const LocateSymbolSchema = z.object({
15
- query: z.string().min(1, "query is required"),
16
- domain: z.string().optional(),
17
- limit: z.number().int().positive().optional().default(10),
18
- });
84
+ export const LocateSymbolSchema = z.preprocess(
85
+ (args) =>
86
+ withAliases(args, {
87
+ symbol: "query",
88
+ name: "query",
89
+ symbol_name: "query",
90
+ symbolName: "query",
91
+ target: "query",
92
+ domain_name: "domain",
93
+ domainName: "domain",
94
+ max: "limit",
95
+ workspace: "workspace_path",
96
+ workspacePath: "workspace_path",
97
+ project_root: "workspace_path",
98
+ target_path: "workspace_path",
99
+ path: "workspace_path",
100
+ }),
101
+ z.object({
102
+ query: z.string().min(1, "query is required"),
103
+ domain: z.string().optional(),
104
+ limit: z.number().int().positive().optional().default(10),
105
+ workspace_path: z.string().optional(),
106
+ })
107
+ );
19
108
  export type LocateSymbolArgs = z.infer<typeof LocateSymbolSchema>;
20
109
 
21
- export const GetSymbolSchema = z.object({
22
- symbol: z.string().min(1, "symbol is required"),
23
- domain: z.string().optional(),
24
- include_dependencies: z.boolean().optional().default(true),
25
- });
110
+ export const GetSymbolSchema = z.preprocess(
111
+ (args) =>
112
+ withAliases(args, {
113
+ query: "symbol",
114
+ name: "symbol",
115
+ symbol_name: "symbol",
116
+ symbolName: "symbol",
117
+ target: "symbol",
118
+ domain_name: "domain",
119
+ domainName: "domain",
120
+ includeDependencies: "include_dependencies",
121
+ include_deps: "include_dependencies",
122
+ includeDeps: "include_dependencies",
123
+ dependencies: "include_dependencies",
124
+ deps: "include_dependencies",
125
+ }),
126
+ z.object({
127
+ symbol: z.string().min(1, "symbol is required"),
128
+ domain: z.string().optional(),
129
+ include_dependencies: z.boolean().optional().default(true),
130
+ })
131
+ );
26
132
  export type GetSymbolArgs = z.infer<typeof GetSymbolSchema>;
27
133
 
28
- export const GetSymbolImpactSchema = z.object({
29
- symbol: z.string().min(1, "symbol is required"),
30
- });
134
+ export const GetSymbolImpactSchema = z.preprocess(
135
+ (args) =>
136
+ withAliases(args, {
137
+ query: "symbol",
138
+ name: "symbol",
139
+ symbol_name: "symbol",
140
+ symbolName: "symbol",
141
+ file_path: "symbol",
142
+ filePath: "symbol",
143
+ file: "symbol",
144
+ path: "symbol",
145
+ target: "symbol",
146
+ }),
147
+ z.object({
148
+ symbol: z.string().min(1, "symbol is required"),
149
+ })
150
+ );
31
151
  export type GetSymbolImpactArgs = z.infer<typeof GetSymbolImpactSchema>;
32
152
 
33
- export const TraceVerticalSliceSchema = z.object({
34
- query: z.string().min(1, "query is required"),
35
- max_depth: z.number().int().positive().optional().default(5),
36
- });
153
+ export const TraceVerticalSliceSchema = z.preprocess(
154
+ (args) =>
155
+ withAliases(args, {
156
+ route: "query",
157
+ uri: "query",
158
+ route_uri: "query",
159
+ routeUri: "query",
160
+ symbol: "query",
161
+ name: "query",
162
+ feature: "query",
163
+ feature_key: "query",
164
+ featureKey: "query",
165
+ maxDepth: "max_depth",
166
+ depth: "max_depth",
167
+ }),
168
+ z.object({
169
+ query: z.string().min(1, "query is required"),
170
+ max_depth: z.number().int().positive().optional().default(5),
171
+ })
172
+ );
37
173
  export type TraceVerticalSliceArgs = z.infer<typeof TraceVerticalSliceSchema>;
38
174
 
39
- export const CheckBoundarySchema = z.object({
40
- file_path: z.string().optional(),
41
- file_paths: z.array(z.string()).optional(),
42
- proposed_imports: z.array(z.string()).default([]),
43
- feature_key: z.string().optional(),
44
- });
175
+ export const CheckBoundarySchema = z.preprocess(
176
+ normalizeBoundaryArgs,
177
+ z.object({
178
+ file_path: z.string().optional(),
179
+ file_paths: z.array(z.string()).optional(),
180
+ proposed_imports: z.array(z.string()).default([]),
181
+ feature_key: z.string().optional(),
182
+ })
183
+ );
45
184
  export type CheckBoundaryArgs = z.infer<typeof CheckBoundarySchema>;
46
185
 
47
- export const GetSymbolHotspotsSchema = z.object({
48
- domain: z.string().optional(),
49
- limit: z.number().int().positive().optional().default(10),
50
- });
186
+ export const GetSymbolHotspotsSchema = z.preprocess(
187
+ (args) =>
188
+ withAliases(args, {
189
+ domain_name: "domain",
190
+ domainName: "domain",
191
+ max: "limit",
192
+ }),
193
+ z.object({
194
+ domain: z.string().optional(),
195
+ limit: z.number().int().positive().optional().default(10),
196
+ })
197
+ );
51
198
  export type GetSymbolHotspotsArgs = z.infer<typeof GetSymbolHotspotsSchema>;
52
199
 
53
- export const RegisterDomainSchema = z.object({
54
- name: z.string().min(1, "Domain name is required"),
55
- root: z.string().min(1, "Domain root path is required"),
56
- description: z.string().optional(),
57
- allowed_dependencies: z.array(z.string()).optional(),
58
- forbidden_dependencies: z.array(z.string()).optional(),
59
- archetypes: z.record(z.string()).optional(),
60
- ingest_now: z.boolean().optional().default(false),
61
- });
200
+ export const RegisterDomainSchema = z.preprocess(
201
+ (args) =>
202
+ withAliases(args, {
203
+ domain: "name",
204
+ domain_name: "name",
205
+ domainName: "name",
206
+ root_path: "root",
207
+ rootPath: "root",
208
+ path: "root",
209
+ allowedDependencies: "allowed_dependencies",
210
+ allowed: "allowed_dependencies",
211
+ forbiddenDependencies: "forbidden_dependencies",
212
+ forbidden: "forbidden_dependencies",
213
+ archetype: "archetypes",
214
+ ingestNow: "ingest_now",
215
+ }),
216
+ z.object({
217
+ name: z.string().min(1, "Domain name is required"),
218
+ root: z.string().min(1, "Domain root path is required"),
219
+ description: z.string().optional(),
220
+ allowed_dependencies: z.array(z.string()).optional(),
221
+ forbidden_dependencies: z.array(z.string()).optional(),
222
+ archetypes: z.record(z.string()).optional(),
223
+ ingest_now: z.boolean().optional().default(false),
224
+ })
225
+ );
62
226
  export type RegisterDomainArgs = z.infer<typeof RegisterDomainSchema>;
63
227
 
64
- export const AutoDiscoverDomainsSchema = z.object({
65
- persist: z.boolean().optional().default(true),
66
- });
228
+ export const AutoDiscoverDomainsSchema = z.preprocess(
229
+ (args) =>
230
+ withAliases(args, {
231
+ save: "persist",
232
+ }),
233
+ z.object({
234
+ persist: z.boolean().optional().default(true),
235
+ })
236
+ );
67
237
  export type AutoDiscoverDomainsArgs = z.infer<typeof AutoDiscoverDomainsSchema>;
68
238