septum 0.1.2 → 0.1.3
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 +2 -0
- package/package.json +1 -1
- package/src/cli/templates/skill.ts +7 -4
- package/src/core/config/loader.ts +21 -8
- package/src/core/database/repositories/dependency.repository.ts +70 -0
- package/src/core/database/repositories/file.repository.ts +7 -0
- package/src/core/database/repository.ts +13 -0
- package/src/core/discovery/topology-detector.ts +71 -3
- package/src/core/ingestion/pipeline.ts +98 -1
- package/src/core/parser/extractors/base.ts +1 -0
- package/src/core/parser/extractors/blade.ts +158 -0
- package/src/core/parser/extractors/laravel-semantic.ts +186 -27
- package/src/core/parser/extractors/registry.ts +21 -0
- package/src/core/parser/tree-sitter.ts +2 -0
- package/src/core/resolver/path-resolver.ts +147 -0
- package/src/core/resolver/vertical-slice-tracer.ts +22 -7
- package/src/mcp/schemas.ts +216 -46
- package/src/mcp/server.ts +68 -24
- package/src/mcp/tools/check-boundary.ts +58 -8
- package/src/mcp/tools/get-domain-catalog.ts +31 -1
- package/src/mcp/tools/locate-symbol.ts +119 -3
- package/src/mcp/tools/trace-vertical-slice.ts +2 -1
- package/src/types/index.ts +4 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, basename } from "node:path";
|
|
1
3
|
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
4
|
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
5
|
+
import type { IngestionPipeline } from "../../core/ingestion/pipeline.ts";
|
|
3
6
|
import {
|
|
4
7
|
SymbolLocator,
|
|
5
8
|
type SymbolLocationResult,
|
|
@@ -8,12 +11,15 @@ import {
|
|
|
8
11
|
export interface LocateSymbolArgs {
|
|
9
12
|
query: string;
|
|
10
13
|
domain?: string;
|
|
14
|
+
workspace_path?: string;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
export function handleLocateSymbol(
|
|
17
|
+
export async function handleLocateSymbol(
|
|
14
18
|
repo: SeptumRepository,
|
|
15
19
|
config: ValidatedSeptumConfig,
|
|
16
|
-
args: LocateSymbolArgs
|
|
20
|
+
args: LocateSymbolArgs,
|
|
21
|
+
pipeline?: IngestionPipeline,
|
|
22
|
+
workspaceRoot?: string
|
|
17
23
|
) {
|
|
18
24
|
if (!args.query) {
|
|
19
25
|
throw new Error(
|
|
@@ -21,8 +27,79 @@ export function handleLocateSymbol(
|
|
|
21
27
|
);
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
const root = workspaceRoot || process.cwd();
|
|
31
|
+
|
|
32
|
+
// 1. Guarded Lazy Ingestion on Empty Catalog / Cold Start
|
|
33
|
+
if (repo.countFiles() === 0 && pipeline) {
|
|
34
|
+
try {
|
|
35
|
+
await pipeline.run(config);
|
|
36
|
+
} catch {
|
|
37
|
+
// Ingestion fallback
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (repo.countFiles() === 0) {
|
|
41
|
+
const diagnostic = {
|
|
42
|
+
status: "NOT_INDEXED",
|
|
43
|
+
found: false,
|
|
44
|
+
query: args.query,
|
|
45
|
+
message: `Workspace '${root}' belum terindeks dalam Septum catalog.`,
|
|
46
|
+
suggested_action: {
|
|
47
|
+
tool: "septum_register_domain",
|
|
48
|
+
params: { name: "core", root: "src", ingest_now: true },
|
|
49
|
+
},
|
|
50
|
+
agent_guidance:
|
|
51
|
+
"Panggil tool 'septum_register_domain(name: \"core\", root: \"src\", ingest_now: true)' untuk mendaftarkan dan mengindeks codebase ini, atau jalankan 'septum init' di terminal workspace.",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: JSON.stringify(diagnostic, null, 2),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
metadata: diagnostic,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
24
66
|
const locator = new SymbolLocator(repo);
|
|
25
|
-
|
|
67
|
+
let result: SymbolLocationResult = locator.locate(args.query, args.domain);
|
|
68
|
+
|
|
69
|
+
// 2. JIT Delta Sync: Check if located file has changed on disk since last ingest
|
|
70
|
+
if (result.found && result.file_path && pipeline) {
|
|
71
|
+
const fullPath = isAbsolute(result.file_path)
|
|
72
|
+
? result.file_path
|
|
73
|
+
: join(root, result.file_path);
|
|
74
|
+
try {
|
|
75
|
+
const reindexed = await pipeline.ingestFile(fullPath, config);
|
|
76
|
+
if (reindexed) {
|
|
77
|
+
result = locator.locate(args.query, args.domain);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Silently fall back to existing result
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Fallback on Cache Miss: Search for candidate source files matching the query terms
|
|
85
|
+
if (!result.found && pipeline) {
|
|
86
|
+
try {
|
|
87
|
+
const cleanTerm = args.query.replace(/[^a-zA-Z0-9_]/g, " ").trim().split(/\s+/)[0] || "";
|
|
88
|
+
if (cleanTerm.length >= 3) {
|
|
89
|
+
const candidateFiles = findCandidateFiles(root, cleanTerm);
|
|
90
|
+
let anyReindexed = false;
|
|
91
|
+
for (const candidate of candidateFiles) {
|
|
92
|
+
const reindexed = await pipeline.ingestFile(candidate, config);
|
|
93
|
+
if (reindexed) anyReindexed = true;
|
|
94
|
+
}
|
|
95
|
+
if (anyReindexed) {
|
|
96
|
+
result = locator.locate(args.query, args.domain);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore candidate scan errors
|
|
101
|
+
}
|
|
102
|
+
}
|
|
26
103
|
|
|
27
104
|
let formattedText = "";
|
|
28
105
|
|
|
@@ -72,3 +149,42 @@ export function handleLocateSymbol(
|
|
|
72
149
|
metadata: result,
|
|
73
150
|
};
|
|
74
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Searches for potential candidate files whose names resemble parts of the query term.
|
|
155
|
+
*/
|
|
156
|
+
function findCandidateFiles(root: string, term: string): string[] {
|
|
157
|
+
const matched: string[] = [];
|
|
158
|
+
const lowerTerm = term.toLowerCase().replace(/_/g, "");
|
|
159
|
+
const searchDirs = ["src", "app", "lib"].map((d) => join(root, d)).filter((d) => existsSync(d));
|
|
160
|
+
|
|
161
|
+
function scan(dir: string, depth: number = 0) {
|
|
162
|
+
if (depth > 4) return;
|
|
163
|
+
try {
|
|
164
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
165
|
+
for (const e of entries) {
|
|
166
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
|
|
167
|
+
const full = join(dir, e.name);
|
|
168
|
+
if (e.isDirectory()) {
|
|
169
|
+
scan(full, depth + 1);
|
|
170
|
+
} else if (e.isFile() && /\.(ts|tsx|js|jsx|php|py|go)$/i.test(e.name)) {
|
|
171
|
+
const lowerName = e.name.toLowerCase().replace(/[-_.]/g, "");
|
|
172
|
+
if (lowerName.includes(lowerTerm) || lowerTerm.includes(lowerName.replace(/\.[^.]+$/, ""))) {
|
|
173
|
+
matched.push(full);
|
|
174
|
+
if (matched.length >= 5) return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Ignore
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const sDir of searchDirs) {
|
|
184
|
+
scan(sDir);
|
|
185
|
+
if (matched.length >= 5) break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return matched;
|
|
189
|
+
}
|
|
190
|
+
|
|
@@ -18,7 +18,8 @@ export function handleTraceVerticalSlice(
|
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const workspaceRoot = (config.settings as Record<string, unknown>)?.workspace_root as string || process.cwd();
|
|
22
|
+
const tracer = new VerticalSliceTracer(repo, workspaceRoot);
|
|
22
23
|
const result: VerticalSliceTraceResponse = tracer.trace(args.query);
|
|
23
24
|
|
|
24
25
|
let formattedText = "";
|
package/src/types/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type ArchetypeKind =
|
|
|
13
13
|
| "request"
|
|
14
14
|
| "resource"
|
|
15
15
|
| "util"
|
|
16
|
+
| "view"
|
|
16
17
|
| "unknown";
|
|
17
18
|
|
|
18
19
|
export type SymbolKind =
|
|
@@ -23,7 +24,9 @@ export type SymbolKind =
|
|
|
23
24
|
| "method"
|
|
24
25
|
| "function"
|
|
25
26
|
| "type"
|
|
26
|
-
| "property"
|
|
27
|
+
| "property"
|
|
28
|
+
| "component"
|
|
29
|
+
| "view";
|
|
27
30
|
|
|
28
31
|
export type Visibility = "public" | "protected" | "private";
|
|
29
32
|
|