septum 0.1.4 → 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 (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -105
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +216 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +213 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +104 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +50 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +220 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +50 -38
  17. package/src/cli/templates/skill.ts +65 -69
  18. package/src/core/boundary/evaluator.ts +429 -210
  19. package/src/core/config/loader.ts +117 -97
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -44
  22. package/src/core/database/repositories/dependency.repository.ts +552 -349
  23. package/src/core/database/repositories/domain.repository.ts +275 -198
  24. package/src/core/database/repositories/file.repository.ts +117 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +482 -443
  27. package/src/core/database/repository.ts +281 -260
  28. package/src/core/database/schema.ts +154 -153
  29. package/src/core/discovery/topology-detector.ts +486 -486
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +606 -535
  32. package/src/core/parser/boundary-tracker.ts +433 -288
  33. package/src/core/parser/extractors/base.ts +62 -62
  34. package/src/core/parser/extractors/blade.ts +158 -158
  35. package/src/core/parser/extractors/frontend.ts +147 -194
  36. package/src/core/parser/extractors/go.ts +408 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
  40. package/src/core/parser/extractors/php.ts +415 -153
  41. package/src/core/parser/extractors/python.ts +460 -228
  42. package/src/core/parser/extractors/registry.ts +70 -70
  43. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  44. package/src/core/parser/extractors/typescript.ts +668 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/parser/wasm-loader.ts +271 -0
  47. package/src/core/resolver/call-graph-tracer.ts +698 -668
  48. package/src/core/resolver/module-resolver.ts +479 -342
  49. package/src/core/resolver/path-resolver.ts +147 -147
  50. package/src/core/resolver/symbol-locator.ts +543 -543
  51. package/src/core/resolver/vertical-slice-tracer.ts +323 -285
  52. package/src/core/session/session-manager.ts +56 -56
  53. package/src/core/telemetry/telemetry.ts +86 -86
  54. package/src/core/topology/environment-detector.ts +109 -6
  55. package/src/index.ts +21 -21
  56. package/src/mcp/schemas.ts +695 -286
  57. package/src/mcp/server.ts +927 -716
  58. package/src/mcp/tools/check-boundary.ts +307 -152
  59. package/src/mcp/tools/get-domain-catalog.ts +155 -121
  60. package/src/mcp/tools/get-environment-topology.ts +3 -6
  61. package/src/mcp/tools/get-feature-context.ts +227 -116
  62. package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
  63. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  64. package/src/mcp/tools/get-symbol.ts +65 -63
  65. package/src/mcp/tools/locate-symbol.ts +169 -190
  66. package/src/mcp/tools/register-domain.ts +59 -64
  67. package/src/mcp/tools/register-feature.ts +91 -0
  68. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  69. package/src/types/index.ts +366 -315
  70. package/src/version.ts +3 -3
@@ -1,41 +1,41 @@
1
- import type { ParsedFileAST } from "../../types/index.ts";
2
- import type { CodeExtractor } from "./extractors/base.ts";
3
- import { BladeExtractor } from "./extractors/blade.ts";
4
- import { FrontendExtractor } from "./extractors/frontend.ts";
5
- import { GoExtractor } from "./extractors/go.ts";
6
- import { PHPExtractor } from "./extractors/php.ts";
7
- import { PythonExtractor } from "./extractors/python.ts";
8
- import { TypeScriptExtractor } from "./extractors/typescript.ts";
9
-
10
- export class ASTParserEngine {
11
- private extractors: CodeExtractor[];
12
-
13
- constructor() {
14
- this.extractors = [
15
- new BladeExtractor(),
16
- new FrontendExtractor(),
17
- new TypeScriptExtractor(),
18
- new PHPExtractor(),
19
- new PythonExtractor(),
20
- new GoExtractor(),
21
- ];
22
- }
23
-
24
- public async parseFile(filePath: string, content: string): Promise<ParsedFileAST> {
25
- for (const extractor of this.extractors) {
26
- if (extractor.canHandle(filePath)) {
27
- return extractor.extract(filePath, content);
28
- }
29
- }
30
-
31
- // Default fallback extractor for unknown file types
32
- return {
33
- symbols: [],
34
- dependencies: [],
35
- };
36
- }
37
-
38
- public registerExtractor(extractor: CodeExtractor): void {
39
- this.extractors.unshift(extractor);
40
- }
41
- }
1
+ import type { ParsedFileAST } from "../../types/index.ts";
2
+ import type { CodeExtractor } from "./extractors/base.ts";
3
+ import { BladeExtractor } from "./extractors/blade.ts";
4
+ import { FrontendExtractor } from "./extractors/frontend.ts";
5
+ import { GoExtractor } from "./extractors/go.ts";
6
+ import { PHPExtractor } from "./extractors/php.ts";
7
+ import { PythonExtractor } from "./extractors/python.ts";
8
+ import { TypeScriptExtractor } from "./extractors/typescript.ts";
9
+
10
+ export class ASTParserEngine {
11
+ private extractors: CodeExtractor[];
12
+
13
+ constructor() {
14
+ this.extractors = [
15
+ new BladeExtractor(),
16
+ new FrontendExtractor(),
17
+ new TypeScriptExtractor(),
18
+ new PHPExtractor(),
19
+ new PythonExtractor(),
20
+ new GoExtractor(),
21
+ ];
22
+ }
23
+
24
+ public async parseFile(filePath: string, content: string): Promise<ParsedFileAST> {
25
+ for (const extractor of this.extractors) {
26
+ if (extractor.canHandle(filePath)) {
27
+ return extractor.extract(filePath, content);
28
+ }
29
+ }
30
+
31
+ // Default fallback extractor for unknown file types
32
+ return {
33
+ symbols: [],
34
+ dependencies: [],
35
+ };
36
+ }
37
+
38
+ public registerExtractor(extractor: CodeExtractor): void {
39
+ this.extractors.unshift(extractor);
40
+ }
41
+ }
@@ -0,0 +1,271 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import Parser from "web-tree-sitter";
5
+
6
+ const GRAMMAR_BY_KEY: Record<string, string> = {
7
+ // Extensions
8
+ ".ts": "tree-sitter-typescript.wasm",
9
+ ".mts": "tree-sitter-typescript.wasm",
10
+ ".cts": "tree-sitter-typescript.wasm",
11
+ ".tsx": "tree-sitter-tsx.wasm",
12
+ ".js": "tree-sitter-javascript.wasm",
13
+ ".mjs": "tree-sitter-javascript.wasm",
14
+ ".cjs": "tree-sitter-javascript.wasm",
15
+ ".jsx": "tree-sitter-javascript.wasm",
16
+ ".php": "tree-sitter-php.wasm",
17
+ ".py": "tree-sitter-python.wasm",
18
+ ".go": "tree-sitter-go.wasm",
19
+ ".rs": "tree-sitter-rust.wasm",
20
+ ".java": "tree-sitter-java.wasm",
21
+ ".cs": "tree-sitter-c_sharp.wasm",
22
+ ".rb": "tree-sitter-ruby.wasm",
23
+ // Language names
24
+ ts: "tree-sitter-typescript.wasm",
25
+ typescript: "tree-sitter-typescript.wasm",
26
+ tsx: "tree-sitter-tsx.wasm",
27
+ js: "tree-sitter-javascript.wasm",
28
+ jsx: "tree-sitter-javascript.wasm",
29
+ javascript: "tree-sitter-javascript.wasm",
30
+ php: "tree-sitter-php.wasm",
31
+ py: "tree-sitter-python.wasm",
32
+ python: "tree-sitter-python.wasm",
33
+ go: "tree-sitter-go.wasm",
34
+ golang: "tree-sitter-go.wasm",
35
+ rs: "tree-sitter-rust.wasm",
36
+ rust: "tree-sitter-rust.wasm",
37
+ java: "tree-sitter-java.wasm",
38
+ cs: "tree-sitter-c_sharp.wasm",
39
+ csharp: "tree-sitter-c_sharp.wasm",
40
+ c_sharp: "tree-sitter-c_sharp.wasm",
41
+ rb: "tree-sitter-ruby.wasm",
42
+ ruby: "tree-sitter-ruby.wasm",
43
+ };
44
+
45
+ let initPromise: Promise<boolean> | null = null;
46
+ let sharedParser: Parser | null = null;
47
+ const languageCache = new Map<string, Parser.Language>();
48
+ const languageLoadingPromises = new Map<string, Promise<Parser.Language | null>>();
49
+
50
+ function getModuleDir(): string {
51
+ if (typeof import.meta.dir === "string") {
52
+ return import.meta.dir;
53
+ }
54
+ try {
55
+ return path.dirname(fileURLToPath(import.meta.url));
56
+ } catch {
57
+ return process.cwd();
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Deterministic 4-tier WASM path resolver for both source execution and compiled binaries.
63
+ */
64
+ export function resolveWasmPath(fileName: string, isCoreRuntime: boolean = false): string | null {
65
+ const candidates: string[] = [];
66
+ const relativeNodeModulesPath = isCoreRuntime
67
+ ? path.join("node_modules", "web-tree-sitter", fileName)
68
+ : path.join("node_modules", "tree-sitter-wasms", "out", fileName);
69
+
70
+ // Tier 1: Explicit environment variable override
71
+ const envDir = process.env.SEPTUM_WASM_DIR;
72
+ if (envDir) {
73
+ candidates.push(path.join(envDir, fileName));
74
+ candidates.push(path.join(envDir, "out", fileName));
75
+ }
76
+
77
+ // Tier 2: Walk up from import.meta.dir to package root
78
+ let currentDir = getModuleDir();
79
+ for (let depth = 0; depth < 7; depth++) {
80
+ candidates.push(path.join(currentDir, relativeNodeModulesPath));
81
+ candidates.push(path.join(currentDir, "wasm", fileName));
82
+ const parent = path.dirname(currentDir);
83
+ if (parent === currentDir) break;
84
+ currentDir = parent;
85
+ }
86
+
87
+ // Tier 3: Relative to process.execPath (for `bun build --compile` standalone binaries)
88
+ if (process.execPath) {
89
+ const execDir = path.dirname(process.execPath);
90
+ candidates.push(path.join(execDir, "wasm", fileName));
91
+ candidates.push(path.join(execDir, fileName));
92
+ candidates.push(path.join(execDir, "..", "wasm", fileName));
93
+ candidates.push(path.join(execDir, relativeNodeModulesPath));
94
+ candidates.push(path.join(execDir, "..", relativeNodeModulesPath));
95
+ }
96
+
97
+ // Tier 4: Fallback to process.cwd()/node_modules/...
98
+ const cwd = process.cwd();
99
+ candidates.push(path.join(cwd, relativeNodeModulesPath));
100
+ candidates.push(path.join(cwd, "wasm", fileName));
101
+
102
+ for (const candidate of candidates) {
103
+ try {
104
+ if (fs.existsSync(candidate)) {
105
+ return candidate;
106
+ }
107
+ } catch {
108
+ // Ignore permission or stat errors on intermediate paths
109
+ }
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Resolves a language key or file path/extension to its WASM grammar filename.
117
+ */
118
+ export function resolveGrammarFileName(langKeyOrPath: string): string | null {
119
+ const normalized = langKeyOrPath.trim().toLowerCase();
120
+ if (GRAMMAR_BY_KEY[normalized]) {
121
+ return GRAMMAR_BY_KEY[normalized];
122
+ }
123
+ if (normalized.endsWith(".wasm")) {
124
+ return normalized;
125
+ }
126
+ const ext = path.extname(normalized);
127
+ if (ext && GRAMMAR_BY_KEY[ext]) {
128
+ return GRAMMAR_BY_KEY[ext];
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Initializes the core web-tree-sitter runtime once as a singleton.
135
+ */
136
+ export async function ensureTreeSitterInitialized(): Promise<boolean> {
137
+ if (initPromise) {
138
+ return initPromise;
139
+ }
140
+
141
+ initPromise = (async () => {
142
+ try {
143
+ const coreWasmPath = resolveWasmPath("tree-sitter.wasm", true);
144
+ if (!coreWasmPath) {
145
+ return false;
146
+ }
147
+ await Parser.init({
148
+ locateFile: () => coreWasmPath,
149
+ });
150
+ if (!sharedParser) {
151
+ sharedParser = new Parser();
152
+ }
153
+ return true;
154
+ } catch {
155
+ initPromise = null;
156
+ return false;
157
+ }
158
+ })();
159
+
160
+ return initPromise;
161
+ }
162
+
163
+ /**
164
+ * Loads and caches a Tree-Sitter Language grammar from its WASM file.
165
+ */
166
+ export async function loadTreeSitterLanguage(langKeyOrPath: string): Promise<Parser.Language | null> {
167
+ const wasmFileName = resolveGrammarFileName(langKeyOrPath);
168
+ if (!wasmFileName) {
169
+ return null;
170
+ }
171
+
172
+ const cached = languageCache.get(wasmFileName);
173
+ if (cached) {
174
+ return cached;
175
+ }
176
+
177
+ const inFlight = languageLoadingPromises.get(wasmFileName);
178
+ if (inFlight) {
179
+ return inFlight;
180
+ }
181
+
182
+ const loadPromise = (async () => {
183
+ try {
184
+ const initialized = await ensureTreeSitterInitialized();
185
+ if (!initialized) {
186
+ return null;
187
+ }
188
+
189
+ const wasmPath = resolveWasmPath(wasmFileName, false);
190
+ if (!wasmPath) {
191
+ return null;
192
+ }
193
+
194
+ const wasmBytes = fs.readFileSync(wasmPath);
195
+ const lang = await Parser.Language.load(wasmBytes);
196
+ languageCache.set(wasmFileName, lang);
197
+ return lang;
198
+ } catch {
199
+ return null;
200
+ } finally {
201
+ languageLoadingPromises.delete(wasmFileName);
202
+ }
203
+ })();
204
+
205
+ languageLoadingPromises.set(wasmFileName, loadPromise);
206
+ return loadPromise;
207
+ }
208
+
209
+ /**
210
+ * Parses source content using web-tree-sitter and executes a visitor over the root SyntaxNode.
211
+ * Guarantees 0 WASM memory leaks by always invoking `tree.delete()` inside `finally`.
212
+ */
213
+ export async function parseWithTreeSitter<T>(
214
+ langKey: string,
215
+ content: string,
216
+ visitor: (rootNode: Parser.SyntaxNode) => T
217
+ ): Promise<T | null> {
218
+ const language = await loadTreeSitterLanguage(langKey);
219
+ if (!language || !sharedParser) {
220
+ return null;
221
+ }
222
+
223
+ let tree: Parser.Tree | null = null;
224
+ try {
225
+ sharedParser.setLanguage(language);
226
+ tree = sharedParser.parse(content);
227
+ if (!tree) {
228
+ return null;
229
+ }
230
+ return visitor(tree.rootNode);
231
+ } catch {
232
+ return null;
233
+ } finally {
234
+ if (tree) {
235
+ tree.delete();
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Computes maximum control-flow nesting depth directly from a Tree-Sitter function/method AST node.
242
+ * Base depth for a function/method body is 1.
243
+ */
244
+ export function calculateAstControlFlowNesting(
245
+ rootNode: Parser.SyntaxNode,
246
+ controlNodeTypes: ReadonlySet<string>,
247
+ boundaryNodeTypes?: ReadonlySet<string>
248
+ ): number {
249
+ let maxDepth = 1;
250
+
251
+ function walk(node: Parser.SyntaxNode, currentDepth: number, isRoot: boolean): void {
252
+ if (!isRoot && boundaryNodeTypes && boundaryNodeTypes.has(node.type)) {
253
+ return;
254
+ }
255
+
256
+ let nextDepth = currentDepth;
257
+ if (!isRoot && controlNodeTypes.has(node.type)) {
258
+ nextDepth = currentDepth + 1;
259
+ if (nextDepth > maxDepth) {
260
+ maxDepth = nextDepth;
261
+ }
262
+ }
263
+
264
+ for (const child of node.namedChildren) {
265
+ walk(child, nextDepth, false);
266
+ }
267
+ }
268
+
269
+ walk(rootNode, 1, true);
270
+ return maxDepth;
271
+ }