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.
- package/LICENSE +21 -21
- package/README.md +105 -105
- package/bin/septum.ts +4 -4
- package/package.json +63 -63
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +216 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +213 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +104 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +50 -48
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +220 -210
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +50 -38
- package/src/cli/templates/skill.ts +65 -69
- package/src/core/boundary/evaluator.ts +429 -210
- package/src/core/config/loader.ts +117 -97
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -44
- package/src/core/database/repositories/dependency.repository.ts +552 -349
- package/src/core/database/repositories/domain.repository.ts +275 -198
- package/src/core/database/repositories/file.repository.ts +117 -106
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +482 -443
- package/src/core/database/repository.ts +281 -260
- package/src/core/database/schema.ts +154 -153
- package/src/core/discovery/topology-detector.ts +486 -486
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +606 -535
- package/src/core/parser/boundary-tracker.ts +433 -288
- package/src/core/parser/extractors/base.ts +62 -62
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/frontend.ts +147 -194
- package/src/core/parser/extractors/go.ts +408 -180
- package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
- package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
- package/src/core/parser/extractors/php.ts +415 -153
- package/src/core/parser/extractors/python.ts +460 -228
- package/src/core/parser/extractors/registry.ts +70 -70
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +668 -181
- package/src/core/parser/tree-sitter.ts +41 -41
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +698 -668
- package/src/core/resolver/module-resolver.ts +479 -342
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/symbol-locator.ts +543 -543
- package/src/core/resolver/vertical-slice-tracer.ts +323 -285
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/index.ts +21 -21
- package/src/mcp/schemas.ts +695 -286
- package/src/mcp/server.ts +927 -716
- package/src/mcp/tools/check-boundary.ts +307 -152
- package/src/mcp/tools/get-domain-catalog.ts +155 -121
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +227 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
- package/src/mcp/tools/get-symbol-impact.ts +28 -26
- package/src/mcp/tools/get-symbol.ts +65 -63
- package/src/mcp/tools/locate-symbol.ts +169 -190
- package/src/mcp/tools/register-domain.ts +59 -64
- package/src/mcp/tools/register-feature.ts +91 -0
- package/src/mcp/tools/trace-vertical-slice.ts +74 -48
- package/src/types/index.ts +366 -315
- 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
|
+
}
|