septum 0.1.5 → 0.1.6

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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/hook.ts +36 -8
  3. package/src/cli/commands/init.ts +6 -4
  4. package/src/cli/commands/query.ts +45 -5
  5. package/src/cli/commands/slice.ts +4 -2
  6. package/src/cli/index.ts +15 -5
  7. package/src/cli/templates/hooks.ts +6 -6
  8. package/src/cli/templates/rules.ts +35 -23
  9. package/src/cli/templates/skill.ts +47 -51
  10. package/src/core/boundary/evaluator.ts +198 -1
  11. package/src/core/config/loader.ts +20 -0
  12. package/src/core/database/repositories/dependency.repository.ts +249 -63
  13. package/src/core/database/repositories/domain.repository.ts +96 -39
  14. package/src/core/database/repositories/file.repository.ts +20 -19
  15. package/src/core/database/repositories/symbol.repository.ts +44 -5
  16. package/src/core/database/repository.ts +19 -2
  17. package/src/core/database/schema.ts +3 -2
  18. package/src/core/ingestion/pipeline.ts +47 -25
  19. package/src/core/parser/boundary-tracker.ts +145 -0
  20. package/src/core/parser/extractors/frontend.ts +43 -90
  21. package/src/core/parser/extractors/go.ts +200 -21
  22. package/src/core/parser/extractors/laravel-semantic.ts +18 -12
  23. package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
  24. package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
  25. package/src/core/parser/extractors/php.ts +293 -31
  26. package/src/core/parser/extractors/python.ts +242 -10
  27. package/src/core/parser/extractors/typescript.ts +604 -117
  28. package/src/core/parser/wasm-loader.ts +271 -0
  29. package/src/core/resolver/call-graph-tracer.ts +49 -17
  30. package/src/core/resolver/module-resolver.ts +87 -16
  31. package/src/core/resolver/vertical-slice-tracer.ts +33 -11
  32. package/src/core/topology/environment-detector.ts +109 -6
  33. package/src/mcp/schemas.ts +146 -29
  34. package/src/mcp/server.ts +93 -41
  35. package/src/mcp/tools/check-boundary.ts +176 -40
  36. package/src/mcp/tools/get-domain-catalog.ts +24 -14
  37. package/src/mcp/tools/get-environment-topology.ts +3 -6
  38. package/src/mcp/tools/get-feature-context.ts +35 -20
  39. package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
  40. package/src/mcp/tools/locate-symbol.ts +12 -14
  41. package/src/mcp/tools/register-domain.ts +3 -12
  42. package/src/mcp/tools/register-feature.ts +22 -31
  43. package/src/mcp/tools/trace-vertical-slice.ts +8 -8
  44. package/src/types/index.ts +36 -5
@@ -286,3 +286,148 @@ export function calculatePythonNesting(
286
286
 
287
287
  return maxDepth;
288
288
  }
289
+
290
+ /**
291
+ * Replaces the contents of comments and string/template literals with spaces while
292
+ * preserving exact character offsets, outer quote delimiters, and line breaks.
293
+ */
294
+ export function blankCommentsAndStrings(
295
+ content: string,
296
+ options?: { hashComments?: boolean }
297
+ ): string {
298
+ const hashComments = options?.hashComments ?? false;
299
+ const out = content.split("");
300
+ const len = out.length;
301
+
302
+ let i = 0;
303
+ let inSingle = false;
304
+ let inDouble = false;
305
+ let inTemplate = false;
306
+ let inLineComment = false;
307
+ let inBlockComment = false;
308
+
309
+ while (i < len) {
310
+ const ch = out[i];
311
+ const next = i + 1 < len ? out[i + 1] : "";
312
+
313
+ if (inLineComment) {
314
+ if (ch === "\n") {
315
+ inLineComment = false;
316
+ } else if (ch !== "\r") {
317
+ out[i] = " ";
318
+ }
319
+ i++;
320
+ continue;
321
+ }
322
+
323
+ if (inBlockComment) {
324
+ if (ch === "*" && next === "/") {
325
+ out[i] = " ";
326
+ out[i + 1] = " ";
327
+ inBlockComment = false;
328
+ i += 2;
329
+ continue;
330
+ }
331
+ if (ch !== "\n" && ch !== "\r") {
332
+ out[i] = " ";
333
+ }
334
+ i++;
335
+ continue;
336
+ }
337
+
338
+ if (inSingle) {
339
+ if (ch === "\\" && i + 1 < len) {
340
+ if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
341
+ if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
342
+ i += 2;
343
+ continue;
344
+ }
345
+ if (ch === "'") {
346
+ inSingle = false;
347
+ } else if (ch !== "\n" && ch !== "\r") {
348
+ out[i] = " ";
349
+ }
350
+ i++;
351
+ continue;
352
+ }
353
+
354
+ if (inDouble) {
355
+ if (ch === "\\" && i + 1 < len) {
356
+ if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
357
+ if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
358
+ i += 2;
359
+ continue;
360
+ }
361
+ if (ch === '"') {
362
+ inDouble = false;
363
+ } else if (ch !== "\n" && ch !== "\r") {
364
+ out[i] = " ";
365
+ }
366
+ i++;
367
+ continue;
368
+ }
369
+
370
+ if (inTemplate) {
371
+ if (ch === "\\" && i + 1 < len) {
372
+ if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
373
+ if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
374
+ i += 2;
375
+ continue;
376
+ }
377
+ if (ch === "`") {
378
+ inTemplate = false;
379
+ } else if (ch !== "\n" && ch !== "\r") {
380
+ out[i] = " ";
381
+ }
382
+ i++;
383
+ continue;
384
+ }
385
+
386
+ // Normal code state
387
+ if (ch === "/" && next === "/") {
388
+ out[i] = " ";
389
+ out[i + 1] = " ";
390
+ inLineComment = true;
391
+ i += 2;
392
+ continue;
393
+ }
394
+
395
+ if (ch === "/" && next === "*") {
396
+ out[i] = " ";
397
+ out[i + 1] = " ";
398
+ inBlockComment = true;
399
+ i += 2;
400
+ continue;
401
+ }
402
+
403
+ if (hashComments && ch === "#") {
404
+ out[i] = " ";
405
+ inLineComment = true;
406
+ i++;
407
+ continue;
408
+ }
409
+
410
+ if (ch === "'") {
411
+ inSingle = true;
412
+ i++;
413
+ continue;
414
+ }
415
+
416
+ if (ch === '"') {
417
+ inDouble = true;
418
+ i++;
419
+ continue;
420
+ }
421
+
422
+ if (ch === "`") {
423
+ inTemplate = true;
424
+ i++;
425
+ continue;
426
+ }
427
+
428
+ i++;
429
+ }
430
+
431
+ return out.join("");
432
+ }
433
+
@@ -4,8 +4,11 @@ import type {
4
4
  ParsedFileAST,
5
5
  } from "../../../types/index.ts";
6
6
  import type { CodeExtractor } from "./base.ts";
7
+ import { TypeScriptExtractor } from "./typescript.ts";
7
8
 
8
9
  export class FrontendExtractor implements CodeExtractor {
10
+ private readonly tsExtractor = new TypeScriptExtractor();
11
+
9
12
  public canHandle(filePath: string): boolean {
10
13
  return /\.(vue|astro|svelte)$/i.test(filePath);
11
14
  }
@@ -19,22 +22,21 @@ export class FrontendExtractor implements CodeExtractor {
19
22
  const isSvelte = /\.svelte$/i.test(filePath);
20
23
 
21
24
  if (isVue) {
22
- this.extractVue(content, symbols, dependencies);
25
+ await this.extractVue(content, symbols, dependencies);
23
26
  } else if (isAstro) {
24
- this.extractAstro(content, symbols, dependencies);
27
+ await this.extractAstro(content, symbols, dependencies);
25
28
  } else if (isSvelte) {
26
- this.extractSvelte(content, symbols, dependencies);
29
+ await this.extractSvelte(content, symbols, dependencies);
27
30
  }
28
31
 
29
32
  return { symbols, dependencies };
30
33
  }
31
34
 
32
- private extractVue(
35
+ private async extractVue(
33
36
  content: string,
34
37
  symbols: ExtractedSymbol[],
35
38
  dependencies: ExtractedDependency[]
36
- ): void {
37
- // Match <script> and <script setup> blocks
39
+ ): Promise<void> {
38
40
  const scriptRegex = /<script(\s+[^>]*?)?>([\s\S]*?)<\/script>/gi;
39
41
  let match: RegExpExecArray | null;
40
42
 
@@ -44,64 +46,61 @@ export class FrontendExtractor implements CodeExtractor {
44
46
  const scriptBody = match[2];
45
47
  const isSetup = /\bsetup\b/i.test(attributes);
46
48
 
47
- // Calculate start line of the script body
48
- const beforeMatch = content.slice(0, match.index);
49
49
  const tagOpenMatch = fullMatch.match(/<script(\s+[^>]*?)?>/i);
50
50
  const tagOpenLen = tagOpenMatch ? tagOpenMatch[0].length : 0;
51
51
  const scriptStartOffset = match.index + tagOpenLen;
52
52
  const startLine = content.slice(0, scriptStartOffset).split("\n").length;
53
53
 
54
- // Extract symbols and dependencies from this script block
55
- this.extractScriptContent(scriptBody, startLine, symbols, dependencies);
54
+ await this.extractScriptContent(scriptBody, startLine, symbols, dependencies);
56
55
 
57
56
  if (isSetup) {
57
+ const scriptLines = scriptBody.split("\n").length;
58
58
  symbols.push({
59
59
  name: "<script setup>",
60
60
  kind: "method",
61
61
  signature: "script setup",
62
62
  visibility: "public",
63
63
  line_start: startLine,
64
- line_end: startLine + scriptBody.split("\n").length,
65
- line_count: scriptBody.split("\n").length,
64
+ line_end: startLine + scriptLines - 1,
65
+ line_count: Math.max(1, scriptLines),
66
66
  });
67
67
  }
68
68
  }
69
69
 
70
- // Vue defineProps / defineEmits detection
71
70
  const definePropsMatch = content.match(/defineProps\s*<([^>]+)>/);
72
71
  if (definePropsMatch) {
73
72
  const line = content.slice(0, content.indexOf(definePropsMatch[0])).split("\n").length;
73
+ const propLines = definePropsMatch[0].split("\n").length;
74
74
  symbols.push({
75
75
  name: "Props",
76
76
  kind: "interface",
77
77
  signature: `defineProps<${definePropsMatch[1]}>`,
78
78
  visibility: "public",
79
79
  line_start: line,
80
- line_end: line,
81
- line_count: 1,
80
+ line_end: line + propLines - 1,
81
+ line_count: Math.max(1, propLines),
82
82
  });
83
83
  }
84
84
  }
85
85
 
86
- private extractAstro(
86
+ private async extractAstro(
87
87
  content: string,
88
88
  symbols: ExtractedSymbol[],
89
89
  dependencies: ExtractedDependency[]
90
- ): void {
91
- // Astro frontmatter is between the first --- and the second ---
90
+ ): Promise<void> {
92
91
  const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
93
92
  if (frontmatterMatch) {
94
93
  const scriptBody = frontmatterMatch[1];
95
- const startLine = 2; // Line 1 is '---'
96
- this.extractScriptContent(scriptBody, startLine, symbols, dependencies);
94
+ const startLine = 2;
95
+ await this.extractScriptContent(scriptBody, startLine, symbols, dependencies);
97
96
  }
98
97
  }
99
98
 
100
- private extractSvelte(
99
+ private async extractSvelte(
101
100
  content: string,
102
101
  symbols: ExtractedSymbol[],
103
102
  dependencies: ExtractedDependency[]
104
- ): void {
103
+ ): Promise<void> {
105
104
  const scriptRegex = /<script(\s+[^>]*?)?>([\s\S]*?)<\/script>/gi;
106
105
  let match: RegExpExecArray | null;
107
106
 
@@ -113,82 +112,36 @@ export class FrontendExtractor implements CodeExtractor {
113
112
  const scriptStartOffset = match.index + tagOpenLen;
114
113
  const startLine = content.slice(0, scriptStartOffset).split("\n").length;
115
114
 
116
- this.extractScriptContent(scriptBody, startLine, symbols, dependencies);
115
+ await this.extractScriptContent(scriptBody, startLine, symbols, dependencies);
117
116
  }
118
117
  }
119
118
 
120
- private extractScriptContent(
119
+ private async extractScriptContent(
121
120
  scriptContent: string,
122
- lineOffset: number,
121
+ startLine: number,
123
122
  symbols: ExtractedSymbol[],
124
123
  dependencies: ExtractedDependency[]
125
- ): void {
126
- const lines = scriptContent.split("\n");
127
-
128
- // 1. Extract Imports
129
- const importRegex = /(?:import\s+(?:(?:[\w*\s{},]+)\s+from\s+)?['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g;
130
- for (let i = 0; i < lines.length; i++) {
131
- const line = lines[i];
132
- let match: RegExpExecArray | null;
133
- while ((match = importRegex.exec(line)) !== null) {
134
- const target = match[1] || match[2];
135
- if (target) {
136
- const isExternal = !target.startsWith(".") && !target.startsWith("/");
137
- dependencies.push({
138
- target,
139
- statement: line.trim(),
140
- line_number: lineOffset + i,
141
- is_external: isExternal,
142
- });
143
- }
144
- }
124
+ ): Promise<void> {
125
+ const parsed = await this.tsExtractor.extract("component-script.ts", scriptContent);
126
+ const delta = Math.max(0, startLine - 1);
127
+
128
+ for (const dep of parsed.dependencies) {
129
+ dependencies.push({
130
+ ...dep,
131
+ line_number: dep.line_number + delta,
132
+ });
145
133
  }
146
134
 
147
- // 2. Extract Functions / Methods / Classes / Components
148
- const funcRegex = /(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([a-zA-Z0-9_]+)/g;
149
- const constFuncRegex = /(?:export\s+)?const\s+([a-zA-Z0-9_]+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z0-9_]+)\s*=>/g;
150
- const classRegex = /(?:export\s+)?class\s+([a-zA-Z0-9_]+)/g;
151
-
152
- for (let i = 0; i < lines.length; i++) {
153
- const line = lines[i];
154
- const currentLineNum = lineOffset + i;
155
-
156
- let m: RegExpExecArray | null;
157
- while ((m = funcRegex.exec(line)) !== null) {
158
- symbols.push({
159
- name: m[1],
160
- kind: "function",
161
- signature: line.trim(),
162
- visibility: line.includes("export") ? "public" : "private",
163
- line_start: currentLineNum,
164
- line_end: currentLineNum,
165
- line_count: 1,
166
- });
167
- }
168
-
169
- while ((m = constFuncRegex.exec(line)) !== null) {
170
- symbols.push({
171
- name: m[1],
172
- kind: "function",
173
- signature: line.trim(),
174
- visibility: line.includes("export") ? "public" : "private",
175
- line_start: currentLineNum,
176
- line_end: currentLineNum,
177
- line_count: 1,
178
- });
179
- }
180
-
181
- while ((m = classRegex.exec(line)) !== null) {
182
- symbols.push({
183
- name: m[1],
184
- kind: "class",
185
- signature: line.trim(),
186
- visibility: line.includes("export") ? "public" : "private",
187
- line_start: currentLineNum,
188
- line_end: currentLineNum,
189
- line_count: 1,
190
- });
191
- }
135
+ for (const sym of parsed.symbols) {
136
+ const shiftedStart = sym.line_start + delta;
137
+ const shiftedEnd = Math.max(shiftedStart, sym.line_end + delta);
138
+ symbols.push({
139
+ ...sym,
140
+ line_start: shiftedStart,
141
+ line_end: shiftedEnd,
142
+ line_count: Math.max(1, shiftedEnd - shiftedStart + 1),
143
+ });
192
144
  }
193
145
  }
194
146
  }
147
+