septum 0.1.0 → 0.1.2
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 +100 -100
- package/bin/septum.ts +4 -4
- package/package.json +63 -62
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +188 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +211 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +64 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +90 -90
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +199 -178
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +38 -38
- package/src/cli/templates/skill.ts +66 -66
- package/src/core/boundary/evaluator.ts +210 -210
- package/src/core/config/loader.ts +84 -84
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -132
- package/src/core/database/repositories/dependency.repository.ts +279 -279
- package/src/core/database/repositories/domain.repository.ts +198 -198
- package/src/core/database/repositories/file.repository.ts +99 -99
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +424 -424
- package/src/core/database/repository.ts +242 -242
- package/src/core/database/schema.ts +120 -0
- package/src/core/discovery/topology-detector.ts +418 -418
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +438 -438
- package/src/core/parser/boundary-tracker.ts +128 -128
- package/src/core/parser/extractors/base.ts +60 -60
- package/src/core/parser/extractors/frontend.ts +194 -194
- package/src/core/parser/extractors/go.ts +176 -176
- package/src/core/parser/extractors/laravel-semantic.ts +426 -426
- package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
- package/src/core/parser/extractors/php.ts +149 -149
- package/src/core/parser/extractors/python.ts +226 -226
- package/src/core/parser/extractors/registry.ts +49 -49
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +177 -177
- package/src/core/parser/tree-sitter.ts +39 -39
- package/src/core/resolver/call-graph-tracer.ts +668 -668
- package/src/core/resolver/module-resolver.ts +342 -323
- package/src/core/resolver/symbol-locator.ts +543 -405
- package/src/core/resolver/vertical-slice-tracer.ts +237 -236
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/index.ts +18 -18
- package/src/mcp/schemas.ts +68 -68
- package/src/mcp/server.ts +612 -602
- package/src/mcp/tools/check-boundary.ts +102 -102
- package/src/mcp/tools/get-domain-catalog.ts +91 -91
- package/src/mcp/tools/get-feature-context.ts +116 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +49 -49
- package/src/mcp/tools/get-symbol-impact.ts +26 -26
- package/src/mcp/tools/get-symbol.ts +63 -63
- package/src/mcp/tools/locate-symbol.ts +74 -74
- package/src/mcp/tools/register-domain.ts +64 -64
- package/src/mcp/tools/trace-vertical-slice.ts +102 -102
- package/src/types/index.ts +296 -296
- package/src/version.ts +3 -0
- package/src/core/database/schema.sql +0 -92
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility to find the end line (1-based index) of a code block.
|
|
3
|
-
* Handles brace matching with string and comment awareness for C-like languages (TS, JS, PHP, Go).
|
|
4
|
-
*/
|
|
5
|
-
export function findBraceBlockEnd(lines: string[], startLineIndex: number): number {
|
|
6
|
-
let depth = 0;
|
|
7
|
-
let hasOpened = false;
|
|
8
|
-
let inMultiLineComment = false;
|
|
9
|
-
|
|
10
|
-
for (let i = startLineIndex; i < lines.length; i++) {
|
|
11
|
-
const line = lines[i];
|
|
12
|
-
let inSingleQuote = false;
|
|
13
|
-
let inDoubleQuote = false;
|
|
14
|
-
let inTemplateString = false;
|
|
15
|
-
|
|
16
|
-
for (let j = 0; j < line.length; j++) {
|
|
17
|
-
const char = line[j];
|
|
18
|
-
const prevChar = j > 0 ? line[j - 1] : "";
|
|
19
|
-
const nextChar = j + 1 < line.length ? line[j + 1] : "";
|
|
20
|
-
|
|
21
|
-
// Multi-line comment boundary
|
|
22
|
-
if (inMultiLineComment) {
|
|
23
|
-
if (char === "*" && nextChar === "/") {
|
|
24
|
-
inMultiLineComment = false;
|
|
25
|
-
j++; // skip '/'
|
|
26
|
-
}
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Check start of multi-line comment /*
|
|
31
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
32
|
-
inMultiLineComment = true;
|
|
33
|
-
j++;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check single-line comment //
|
|
38
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
39
|
-
break; // ignore rest of line
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// String quote handling (with escape character check)
|
|
43
|
-
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
44
|
-
inSingleQuote = !inSingleQuote;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
48
|
-
inDoubleQuote = !inDoubleQuote;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
52
|
-
inTemplateString = !inTemplateString;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Brace tracking
|
|
61
|
-
if (char === "{") {
|
|
62
|
-
depth++;
|
|
63
|
-
hasOpened = true;
|
|
64
|
-
} else if (char === "}") {
|
|
65
|
-
if (hasOpened) {
|
|
66
|
-
depth--;
|
|
67
|
-
if (depth === 0) {
|
|
68
|
-
return i + 1; // 1-based line number
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Safety guard: If we haven't seen an opening brace within 40 lines or hit a declaration-only statement
|
|
75
|
-
if (!hasOpened) {
|
|
76
|
-
const trimmed = line.trim();
|
|
77
|
-
if (trimmed.endsWith(";") && i > startLineIndex) {
|
|
78
|
-
return i + 1;
|
|
79
|
-
}
|
|
80
|
-
if (i - startLineIndex > 40) {
|
|
81
|
-
return startLineIndex + 1;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Fallback: If braces were unclosed or not found, return start line
|
|
87
|
-
return startLineIndex + 1;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Utility to find the end line (1-based index) of an indentation-based block (Python).
|
|
92
|
-
*/
|
|
93
|
-
export function findPythonBlockEnd(lines: string[], startLineIndex: number): number {
|
|
94
|
-
if (startLineIndex >= lines.length) return startLineIndex + 1;
|
|
95
|
-
|
|
96
|
-
const startLine = lines[startLineIndex];
|
|
97
|
-
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
98
|
-
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
99
|
-
|
|
100
|
-
let lastContentLine = startLineIndex + 1;
|
|
101
|
-
let bodyStarted = false;
|
|
102
|
-
|
|
103
|
-
for (let i = startLineIndex + 1; i < lines.length; i++) {
|
|
104
|
-
const line = lines[i];
|
|
105
|
-
const trimmed = line.trim();
|
|
106
|
-
|
|
107
|
-
// Skip empty lines or pure comment lines
|
|
108
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const currentIndentMatch = line.match(/^(\s*)/);
|
|
113
|
-
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
114
|
-
|
|
115
|
-
if (currentIndent > baseIndent) {
|
|
116
|
-
bodyStarted = true;
|
|
117
|
-
lastContentLine = i + 1;
|
|
118
|
-
} else {
|
|
119
|
-
// Returned to same or lesser indentation -> block ended
|
|
120
|
-
if (bodyStarted) {
|
|
121
|
-
return lastContentLine;
|
|
122
|
-
}
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return lastContentLine;
|
|
128
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Utility to find the end line (1-based index) of a code block.
|
|
3
|
+
* Handles brace matching with string and comment awareness for C-like languages (TS, JS, PHP, Go).
|
|
4
|
+
*/
|
|
5
|
+
export function findBraceBlockEnd(lines: string[], startLineIndex: number): number {
|
|
6
|
+
let depth = 0;
|
|
7
|
+
let hasOpened = false;
|
|
8
|
+
let inMultiLineComment = false;
|
|
9
|
+
|
|
10
|
+
for (let i = startLineIndex; i < lines.length; i++) {
|
|
11
|
+
const line = lines[i];
|
|
12
|
+
let inSingleQuote = false;
|
|
13
|
+
let inDoubleQuote = false;
|
|
14
|
+
let inTemplateString = false;
|
|
15
|
+
|
|
16
|
+
for (let j = 0; j < line.length; j++) {
|
|
17
|
+
const char = line[j];
|
|
18
|
+
const prevChar = j > 0 ? line[j - 1] : "";
|
|
19
|
+
const nextChar = j + 1 < line.length ? line[j + 1] : "";
|
|
20
|
+
|
|
21
|
+
// Multi-line comment boundary
|
|
22
|
+
if (inMultiLineComment) {
|
|
23
|
+
if (char === "*" && nextChar === "/") {
|
|
24
|
+
inMultiLineComment = false;
|
|
25
|
+
j++; // skip '/'
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check start of multi-line comment /*
|
|
31
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
32
|
+
inMultiLineComment = true;
|
|
33
|
+
j++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check single-line comment //
|
|
38
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
39
|
+
break; // ignore rest of line
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// String quote handling (with escape character check)
|
|
43
|
+
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
44
|
+
inSingleQuote = !inSingleQuote;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
48
|
+
inDoubleQuote = !inDoubleQuote;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
52
|
+
inTemplateString = !inTemplateString;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Brace tracking
|
|
61
|
+
if (char === "{") {
|
|
62
|
+
depth++;
|
|
63
|
+
hasOpened = true;
|
|
64
|
+
} else if (char === "}") {
|
|
65
|
+
if (hasOpened) {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0) {
|
|
68
|
+
return i + 1; // 1-based line number
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Safety guard: If we haven't seen an opening brace within 40 lines or hit a declaration-only statement
|
|
75
|
+
if (!hasOpened) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (trimmed.endsWith(";") && i > startLineIndex) {
|
|
78
|
+
return i + 1;
|
|
79
|
+
}
|
|
80
|
+
if (i - startLineIndex > 40) {
|
|
81
|
+
return startLineIndex + 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback: If braces were unclosed or not found, return start line
|
|
87
|
+
return startLineIndex + 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Utility to find the end line (1-based index) of an indentation-based block (Python).
|
|
92
|
+
*/
|
|
93
|
+
export function findPythonBlockEnd(lines: string[], startLineIndex: number): number {
|
|
94
|
+
if (startLineIndex >= lines.length) return startLineIndex + 1;
|
|
95
|
+
|
|
96
|
+
const startLine = lines[startLineIndex];
|
|
97
|
+
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
98
|
+
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
99
|
+
|
|
100
|
+
let lastContentLine = startLineIndex + 1;
|
|
101
|
+
let bodyStarted = false;
|
|
102
|
+
|
|
103
|
+
for (let i = startLineIndex + 1; i < lines.length; i++) {
|
|
104
|
+
const line = lines[i];
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
|
|
107
|
+
// Skip empty lines or pure comment lines
|
|
108
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const currentIndentMatch = line.match(/^(\s*)/);
|
|
113
|
+
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
114
|
+
|
|
115
|
+
if (currentIndent > baseIndent) {
|
|
116
|
+
bodyStarted = true;
|
|
117
|
+
lastContentLine = i + 1;
|
|
118
|
+
} else {
|
|
119
|
+
// Returned to same or lesser indentation -> block ended
|
|
120
|
+
if (bodyStarted) {
|
|
121
|
+
return lastContentLine;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lastContentLine;
|
|
128
|
+
}
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import type { ArchetypeKind, ParsedFileAST } from "../../../types/index.ts";
|
|
2
|
-
|
|
3
|
-
export interface CodeExtractor {
|
|
4
|
-
canHandle(filePath: string): boolean;
|
|
5
|
-
extract(filePath: string, content: string): Promise<ParsedFileAST>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function detectArchetype(filePath: string, archetypesMap: Record<string, string>): ArchetypeKind {
|
|
9
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
10
|
-
|
|
11
|
-
// Check user-configured archetype glob patterns
|
|
12
|
-
for (const [archetype, pattern] of Object.entries(archetypesMap)) {
|
|
13
|
-
if (matchesSimpleGlob(normalized, pattern)) {
|
|
14
|
-
return archetype as ArchetypeKind;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Built-in convention heuristics
|
|
19
|
-
const lower = normalized.toLowerCase();
|
|
20
|
-
if (lower.includes("controller")) return "controller";
|
|
21
|
-
if (lower.includes("service")) return "service";
|
|
22
|
-
if (lower.includes("repository") || lower.includes("repo")) return "repository";
|
|
23
|
-
if (lower.includes("model") || lower.includes("entities") || lower.includes("entity")) return "model";
|
|
24
|
-
if (lower.includes("action")) return "action";
|
|
25
|
-
if (lower.includes("job") || lower.includes("queue")) return "job";
|
|
26
|
-
if (lower.includes("event")) return "event";
|
|
27
|
-
if (lower.includes("listener")) return "listener";
|
|
28
|
-
if (lower.includes("middleware")) return "middleware";
|
|
29
|
-
if (lower.includes("request")) return "request";
|
|
30
|
-
if (lower.includes("resource")) return "resource";
|
|
31
|
-
if (lower.includes("util") || lower.includes("helper")) return "util";
|
|
32
|
-
|
|
33
|
-
return "unknown";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function matchesSimpleGlob(path: string, pattern: string): boolean {
|
|
37
|
-
const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
38
|
-
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
39
|
-
|
|
40
|
-
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
41
|
-
const regexStr = escaped
|
|
42
|
-
.replace(/\*\*\//g, "(?:.*\\/)?")
|
|
43
|
-
.replace(/\*\*/g, ".*")
|
|
44
|
-
.replace(/\*/g, "[^/]*")
|
|
45
|
-
.replace(/\?/g, "[^/]");
|
|
46
|
-
|
|
47
|
-
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
48
|
-
if (regex.test(normalizedPath)) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// If pattern does not contain directory separators, allow matching against the basename
|
|
53
|
-
if (!normalizedPattern.includes("/")) {
|
|
54
|
-
const base = normalizedPath.split("/").pop() ?? "";
|
|
55
|
-
return regex.test(base);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
1
|
+
import type { ArchetypeKind, ParsedFileAST } from "../../../types/index.ts";
|
|
2
|
+
|
|
3
|
+
export interface CodeExtractor {
|
|
4
|
+
canHandle(filePath: string): boolean;
|
|
5
|
+
extract(filePath: string, content: string): Promise<ParsedFileAST>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function detectArchetype(filePath: string, archetypesMap: Record<string, string>): ArchetypeKind {
|
|
9
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
10
|
+
|
|
11
|
+
// Check user-configured archetype glob patterns
|
|
12
|
+
for (const [archetype, pattern] of Object.entries(archetypesMap)) {
|
|
13
|
+
if (matchesSimpleGlob(normalized, pattern)) {
|
|
14
|
+
return archetype as ArchetypeKind;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Built-in convention heuristics
|
|
19
|
+
const lower = normalized.toLowerCase();
|
|
20
|
+
if (lower.includes("controller")) return "controller";
|
|
21
|
+
if (lower.includes("service")) return "service";
|
|
22
|
+
if (lower.includes("repository") || lower.includes("repo")) return "repository";
|
|
23
|
+
if (lower.includes("model") || lower.includes("entities") || lower.includes("entity")) return "model";
|
|
24
|
+
if (lower.includes("action")) return "action";
|
|
25
|
+
if (lower.includes("job") || lower.includes("queue")) return "job";
|
|
26
|
+
if (lower.includes("event")) return "event";
|
|
27
|
+
if (lower.includes("listener")) return "listener";
|
|
28
|
+
if (lower.includes("middleware")) return "middleware";
|
|
29
|
+
if (lower.includes("request")) return "request";
|
|
30
|
+
if (lower.includes("resource")) return "resource";
|
|
31
|
+
if (lower.includes("util") || lower.includes("helper")) return "util";
|
|
32
|
+
|
|
33
|
+
return "unknown";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function matchesSimpleGlob(path: string, pattern: string): boolean {
|
|
37
|
+
const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
38
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
39
|
+
|
|
40
|
+
const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
const regexStr = escaped
|
|
42
|
+
.replace(/\*\*\//g, "(?:.*\\/)?")
|
|
43
|
+
.replace(/\*\*/g, ".*")
|
|
44
|
+
.replace(/\*/g, "[^/]*")
|
|
45
|
+
.replace(/\?/g, "[^/]");
|
|
46
|
+
|
|
47
|
+
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
48
|
+
if (regex.test(normalizedPath)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If pattern does not contain directory separators, allow matching against the basename
|
|
53
|
+
if (!normalizedPattern.includes("/")) {
|
|
54
|
+
const base = normalizedPath.split("/").pop() ?? "";
|
|
55
|
+
return regex.test(base);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|