septum 0.1.5 → 0.1.7
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/README.md +23 -3
- package/package.json +5 -5
- package/src/cli/commands/check.ts +32 -122
- package/src/cli/commands/hook.ts +45 -12
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/query.ts +45 -5
- package/src/cli/commands/slice.ts +4 -2
- package/src/cli/index.ts +17 -6
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +35 -23
- package/src/cli/templates/skill.ts +47 -51
- package/src/core/boundary/evaluator.ts +217 -24
- package/src/core/boundary/staged-auditor.ts +47 -0
- package/src/core/config/loader.ts +29 -12
- package/src/core/database/repositories/dependency.repository.ts +252 -66
- package/src/core/database/repositories/domain.repository.ts +96 -39
- package/src/core/database/repositories/file.repository.ts +20 -19
- package/src/core/database/repositories/symbol.repository.ts +115 -5
- package/src/core/database/repository.ts +19 -2
- package/src/core/database/schema.ts +3 -2
- package/src/core/ingestion/pipeline.ts +96 -60
- package/src/core/parser/boundary-tracker.ts +145 -0
- package/src/core/parser/extractors/frontend.ts +44 -90
- package/src/core/parser/extractors/go.ts +200 -21
- package/src/core/parser/extractors/laravel-semantic.ts +18 -12
- package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
- package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
- package/src/core/parser/extractors/php.ts +293 -31
- package/src/core/parser/extractors/python.ts +242 -10
- package/src/core/parser/extractors/typescript.ts +604 -117
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +274 -0
- package/src/core/resolver/call-graph-tracer.ts +49 -17
- package/src/core/resolver/module-resolver.ts +88 -26
- package/src/core/resolver/vertical-slice-tracer.ts +34 -11
- package/src/core/session/session-manager.ts +55 -39
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/mcp/schemas.ts +146 -29
- package/src/mcp/server.ts +177 -114
- package/src/mcp/tools/check-boundary.ts +159 -41
- package/src/mcp/tools/get-domain-catalog.ts +30 -15
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +35 -20
- package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
- package/src/mcp/tools/locate-symbol.ts +37 -16
- package/src/mcp/tools/register-domain.ts +3 -12
- package/src/mcp/tools/register-feature.ts +25 -32
- package/src/mcp/tools/trace-vertical-slice.ts +8 -8
- package/src/types/index.ts +56 -6
- package/src/types/wasm.d.ts +4 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtractedDependency, ExtractedSymbol, ParsedFileAST, Visibility } from "../../../types/index.ts";
|
|
2
|
-
import { findPythonBlockEnd, calculatePythonNesting } from "../boundary-tracker.ts";
|
|
3
2
|
import type { CodeExtractor } from "./base.ts";
|
|
4
3
|
|
|
5
4
|
const PYTHON_STDLIB_AND_EXTERNALS = new Set([
|
|
@@ -47,59 +46,104 @@ const PYTHON_STDLIB_AND_EXTERNALS = new Set([
|
|
|
47
46
|
"redis",
|
|
48
47
|
]);
|
|
49
48
|
|
|
49
|
+
import type Parser from "web-tree-sitter";
|
|
50
|
+
import {
|
|
51
|
+
blankCommentsAndStrings,
|
|
52
|
+
calculatePythonNesting,
|
|
53
|
+
findPythonBlockEnd,
|
|
54
|
+
} from "../boundary-tracker.ts";
|
|
55
|
+
import { calculateAstControlFlowNesting, parseWithTreeSitter } from "../wasm-loader.ts";
|
|
56
|
+
|
|
57
|
+
const PYTHON_CONTROL_FLOW_NODES = new Set([
|
|
58
|
+
"if_statement",
|
|
59
|
+
"for_statement",
|
|
60
|
+
"while_statement",
|
|
61
|
+
"try_statement",
|
|
62
|
+
"with_statement",
|
|
63
|
+
"match_statement",
|
|
64
|
+
"conditional_expression",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const PYTHON_FUNCTION_BOUNDARY_NODES = new Set([
|
|
68
|
+
"function_definition",
|
|
69
|
+
"lambda",
|
|
70
|
+
]);
|
|
71
|
+
|
|
50
72
|
export class PythonExtractor implements CodeExtractor {
|
|
51
73
|
public canHandle(filePath: string): boolean {
|
|
52
74
|
return /\.py$/i.test(filePath);
|
|
53
75
|
}
|
|
54
76
|
|
|
55
|
-
public async extract(
|
|
77
|
+
public async extract(_filePath: string, content: string): Promise<ParsedFileAST> {
|
|
78
|
+
const astResult = await parseWithTreeSitter("python", content, (rootNode) =>
|
|
79
|
+
this.extractFromAst(rootNode, content)
|
|
80
|
+
);
|
|
81
|
+
if (astResult) {
|
|
82
|
+
return { ...astResult, status: "parsed" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { ...this.extractFallback(content), status: "degraded" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private extractFallback(content: string): ParsedFileAST {
|
|
56
89
|
const symbols: ExtractedSymbol[] = [];
|
|
57
90
|
const dependencies: ExtractedDependency[] = [];
|
|
58
91
|
|
|
59
92
|
const lines = content.split("\n");
|
|
93
|
+
const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split("\n");
|
|
60
94
|
|
|
61
95
|
// 1. Extract Python Imports
|
|
62
96
|
for (let i = 0; i < lines.length; i++) {
|
|
63
97
|
const line = lines[i];
|
|
64
|
-
const trimmed =
|
|
98
|
+
const trimmed = cleanLines[i].trim();
|
|
99
|
+
const rawTrimmed = line.trim();
|
|
65
100
|
const lineNum = i + 1;
|
|
66
101
|
|
|
67
|
-
|
|
68
|
-
if (trimmed.startsWith("#")) continue;
|
|
102
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
69
103
|
|
|
70
104
|
// Pattern A: from <module> import <symbols>
|
|
71
105
|
const fromMatch = /^from\s+([.\w]+)\s+import\s+([*\w\s,()]+)/.exec(trimmed);
|
|
72
106
|
if (fromMatch) {
|
|
73
107
|
const modulePath = fromMatch[1];
|
|
108
|
+
const importedList = fromMatch[2]
|
|
109
|
+
.replace(/[()]/g, " ")
|
|
110
|
+
.split(",")
|
|
111
|
+
.map((s) => s.trim().split(/\s+as\s+/)[0].trim())
|
|
112
|
+
.filter((s) => Boolean(s) && s !== "*");
|
|
74
113
|
const isRelative = modulePath.startsWith(".");
|
|
75
114
|
const topPackage = isRelative ? "" : modulePath.split(".")[0];
|
|
76
115
|
const isExternal = !isRelative && PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
|
|
77
116
|
|
|
78
117
|
dependencies.push({
|
|
79
118
|
target: modulePath,
|
|
80
|
-
statement:
|
|
119
|
+
statement: rawTrimmed,
|
|
81
120
|
line_number: lineNum,
|
|
82
121
|
is_external: isExternal,
|
|
122
|
+
...(importedList.length > 0 ? { imported_symbols: importedList } : {}),
|
|
83
123
|
});
|
|
84
124
|
continue;
|
|
85
125
|
}
|
|
86
126
|
|
|
87
127
|
// Pattern B: import <module> [as <alias>]
|
|
88
|
-
const importMatch = /^import\s+([.\w]+)(?:\s+as\s
|
|
128
|
+
const importMatch = /^import\s+([.\w]+)(?:\s+as\s+(\w+))?/.exec(trimmed);
|
|
89
129
|
if (importMatch) {
|
|
90
130
|
const modulePath = importMatch[1];
|
|
131
|
+
const alias = importMatch[2];
|
|
91
132
|
const topPackage = modulePath.split(".")[0];
|
|
92
133
|
const isExternal = PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
|
|
134
|
+
const shortName = alias || modulePath.split(".").pop() || modulePath;
|
|
93
135
|
|
|
94
136
|
dependencies.push({
|
|
95
137
|
target: modulePath,
|
|
96
|
-
statement:
|
|
138
|
+
statement: rawTrimmed,
|
|
97
139
|
line_number: lineNum,
|
|
98
140
|
is_external: isExternal,
|
|
141
|
+
imported_symbols: [shortName],
|
|
99
142
|
});
|
|
100
143
|
}
|
|
101
144
|
}
|
|
102
145
|
|
|
146
|
+
|
|
103
147
|
// 2. Extract Python Classes and Functions
|
|
104
148
|
let currentClass: string | null = null;
|
|
105
149
|
let currentClassIndent = -1;
|
|
@@ -205,8 +249,8 @@ export class PythonExtractor implements CodeExtractor {
|
|
|
205
249
|
? "protected"
|
|
206
250
|
: "public";
|
|
207
251
|
|
|
208
|
-
const endLine = findPythonBlockEnd(
|
|
209
|
-
const nestingDepth = calculatePythonNesting(
|
|
252
|
+
const endLine = findPythonBlockEnd(cleanLines, i);
|
|
253
|
+
const nestingDepth = calculatePythonNesting(cleanLines, i, endLine - 1);
|
|
210
254
|
symbols.push({
|
|
211
255
|
name: isMethod ? `${currentClass}::${funcName}` : funcName,
|
|
212
256
|
kind: isMethod ? "method" : "function",
|
|
@@ -225,4 +269,192 @@ export class PythonExtractor implements CodeExtractor {
|
|
|
225
269
|
dependencies,
|
|
226
270
|
};
|
|
227
271
|
}
|
|
272
|
+
|
|
273
|
+
private extractFromAst(rootNode: Parser.SyntaxNode, content: string): ParsedFileAST {
|
|
274
|
+
const symbols: ExtractedSymbol[] = [];
|
|
275
|
+
const dependencies: ExtractedDependency[] = [];
|
|
276
|
+
const lines = content.split("\n");
|
|
277
|
+
|
|
278
|
+
const getVisibility = (name: string): Visibility =>
|
|
279
|
+
name.startsWith("__") ? "private" : name.startsWith("_") ? "protected" : "public";
|
|
280
|
+
|
|
281
|
+
const processNode = (node: Parser.SyntaxNode, currentClass: string | null): void => {
|
|
282
|
+
if (node.type === "import_from_statement") {
|
|
283
|
+
const modNode =
|
|
284
|
+
node.childForFieldName("module_name") ??
|
|
285
|
+
node.namedChildren.find((c) => c.type === "dotted_name" || c.type === "relative_import");
|
|
286
|
+
if (modNode) {
|
|
287
|
+
const modulePath = modNode.text.trim();
|
|
288
|
+
const isRelative = modulePath.startsWith(".");
|
|
289
|
+
const topPackage = isRelative ? "" : modulePath.split(".")[0];
|
|
290
|
+
const isExternal = !isRelative && PYTHON_STDLIB_AND_EXTERNALS.has(topPackage);
|
|
291
|
+
const importedSymbols: string[] = [];
|
|
292
|
+
|
|
293
|
+
for (const child of node.namedChildren) {
|
|
294
|
+
if (child.equals(modNode)) continue;
|
|
295
|
+
if (child.type === "dotted_name") {
|
|
296
|
+
importedSymbols.push(child.text.trim());
|
|
297
|
+
} else if (child.type === "aliased_import") {
|
|
298
|
+
const orig = child.childForFieldName("name") ?? child.namedChildren[0];
|
|
299
|
+
const alias = child.childForFieldName("alias") ?? child.namedChildren[1];
|
|
300
|
+
if (orig?.text) importedSymbols.push(orig.text.trim());
|
|
301
|
+
if (alias?.text) importedSymbols.push(alias.text.trim());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
dependencies.push({
|
|
306
|
+
target: modulePath,
|
|
307
|
+
statement: (lines[node.startPosition.row] ?? node.text).trim(),
|
|
308
|
+
line_number: node.startPosition.row + 1,
|
|
309
|
+
is_external: isExternal,
|
|
310
|
+
...(importedSymbols.length > 0 ? { imported_symbols: Array.from(new Set(importedSymbols)) } : {}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (node.type === "import_statement") {
|
|
317
|
+
for (const child of node.namedChildren) {
|
|
318
|
+
if (child.type === "dotted_name") {
|
|
319
|
+
const modulePath = child.text.trim();
|
|
320
|
+
const topPackage = modulePath.split(".")[0];
|
|
321
|
+
const shortName = modulePath.split(".").pop() || modulePath;
|
|
322
|
+
dependencies.push({
|
|
323
|
+
target: modulePath,
|
|
324
|
+
statement: (lines[node.startPosition.row] ?? node.text).trim(),
|
|
325
|
+
line_number: node.startPosition.row + 1,
|
|
326
|
+
is_external: PYTHON_STDLIB_AND_EXTERNALS.has(topPackage),
|
|
327
|
+
imported_symbols: [shortName],
|
|
328
|
+
});
|
|
329
|
+
} else if (child.type === "aliased_import") {
|
|
330
|
+
const orig = child.childForFieldName("name") ?? child.namedChildren[0];
|
|
331
|
+
const alias = child.childForFieldName("alias") ?? child.namedChildren[1];
|
|
332
|
+
if (orig) {
|
|
333
|
+
const modulePath = orig.text.trim();
|
|
334
|
+
const topPackage = modulePath.split(".")[0];
|
|
335
|
+
const shortName = alias?.text?.trim() || modulePath.split(".").pop() || modulePath;
|
|
336
|
+
dependencies.push({
|
|
337
|
+
target: modulePath,
|
|
338
|
+
statement: (lines[node.startPosition.row] ?? node.text).trim(),
|
|
339
|
+
line_number: node.startPosition.row + 1,
|
|
340
|
+
is_external: PYTHON_STDLIB_AND_EXTERNALS.has(topPackage),
|
|
341
|
+
imported_symbols: [shortName],
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (node.type === "decorated_definition") {
|
|
350
|
+
const defNode =
|
|
351
|
+
node.childForFieldName("definition") ??
|
|
352
|
+
node.namedChildren.find((c) => c.type === "class_definition" || c.type === "function_definition");
|
|
353
|
+
if (defNode) {
|
|
354
|
+
processNode(defNode, currentClass);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (node.type === "class_definition") {
|
|
360
|
+
const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "identifier");
|
|
361
|
+
if (!nameNode) return;
|
|
362
|
+
|
|
363
|
+
const className = nameNode.text;
|
|
364
|
+
const lineStart = node.startPosition.row + 1;
|
|
365
|
+
const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
|
|
366
|
+
const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/:$/, "").trim();
|
|
367
|
+
|
|
368
|
+
symbols.push({
|
|
369
|
+
name: className,
|
|
370
|
+
kind: "class",
|
|
371
|
+
signature: headerLine || `class ${className}`,
|
|
372
|
+
visibility: getVisibility(className),
|
|
373
|
+
line_start: lineStart,
|
|
374
|
+
line_end: lineEnd,
|
|
375
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const bodyNode = node.childForFieldName("body") ?? node.namedChildren.find((c) => c.type === "block");
|
|
379
|
+
if (bodyNode) {
|
|
380
|
+
for (const stmt of bodyNode.namedChildren) {
|
|
381
|
+
processNode(stmt, className);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (node.type === "function_definition") {
|
|
388
|
+
const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "identifier");
|
|
389
|
+
if (!nameNode) return;
|
|
390
|
+
|
|
391
|
+
const funcName = nameNode.text;
|
|
392
|
+
const isMethod = currentClass !== null;
|
|
393
|
+
const lineStart = node.startPosition.row + 1;
|
|
394
|
+
const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
|
|
395
|
+
const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/:$/, "").trim();
|
|
396
|
+
const nestingDepth = calculateAstControlFlowNesting(
|
|
397
|
+
node,
|
|
398
|
+
PYTHON_CONTROL_FLOW_NODES,
|
|
399
|
+
PYTHON_FUNCTION_BOUNDARY_NODES
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
symbols.push({
|
|
403
|
+
name: isMethod ? `${currentClass}::${funcName}` : funcName,
|
|
404
|
+
kind: isMethod ? "method" : "function",
|
|
405
|
+
signature: headerLine || `def ${funcName}()`,
|
|
406
|
+
visibility: getVisibility(funcName),
|
|
407
|
+
line_start: lineStart,
|
|
408
|
+
line_end: lineEnd,
|
|
409
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
410
|
+
nesting_depth: nestingDepth,
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (node.type === "expression_statement") {
|
|
416
|
+
const assignNode =
|
|
417
|
+
node.namedChildren.find((c) => c.type === "assignment") ??
|
|
418
|
+
(node.firstNamedChild?.type === "assignment" ? node.firstNamedChild : null);
|
|
419
|
+
if (assignNode) {
|
|
420
|
+
const leftNode = assignNode.childForFieldName("left") ?? assignNode.namedChildren[0];
|
|
421
|
+
if (leftNode && leftNode.type === "identifier") {
|
|
422
|
+
const varName = leftNode.text.trim();
|
|
423
|
+
const lineStart = assignNode.startPosition.row + 1;
|
|
424
|
+
const lineEnd = Math.max(lineStart, assignNode.endPosition.row + 1);
|
|
425
|
+
const rawLine = (lines[assignNode.startPosition.row] ?? assignNode.text).trim();
|
|
426
|
+
|
|
427
|
+
if (currentClass !== null) {
|
|
428
|
+
symbols.push({
|
|
429
|
+
name: `${currentClass}::${varName}`,
|
|
430
|
+
kind: "property",
|
|
431
|
+
signature: rawLine,
|
|
432
|
+
visibility: getVisibility(varName),
|
|
433
|
+
line_start: lineStart,
|
|
434
|
+
line_end: lineEnd,
|
|
435
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
436
|
+
});
|
|
437
|
+
} else if (/^[A-Z][A-Z0-9_]*$/.test(varName)) {
|
|
438
|
+
symbols.push({
|
|
439
|
+
name: varName,
|
|
440
|
+
kind: "property",
|
|
441
|
+
signature: rawLine,
|
|
442
|
+
visibility: "public",
|
|
443
|
+
line_start: lineStart,
|
|
444
|
+
line_end: lineEnd,
|
|
445
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
for (const child of rootNode.namedChildren) {
|
|
454
|
+
processNode(child, null);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { symbols, dependencies };
|
|
458
|
+
}
|
|
228
459
|
}
|
|
460
|
+
|