septum 0.1.2 → 0.1.4

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 (46) hide show
  1. package/README.md +7 -2
  2. package/package.json +1 -1
  3. package/src/cli/commands/slice.ts +4 -46
  4. package/src/cli/commands/topology.ts +119 -0
  5. package/src/cli/index.ts +11 -0
  6. package/src/cli/templates/skill.ts +7 -4
  7. package/src/core/config/loader.ts +21 -8
  8. package/src/core/database/repositories/dependency.repository.ts +70 -0
  9. package/src/core/database/repositories/environment.repository.ts +207 -0
  10. package/src/core/database/repositories/file.repository.ts +7 -0
  11. package/src/core/database/repositories/symbol.repository.ts +28 -9
  12. package/src/core/database/repository.ts +19 -1
  13. package/src/core/database/schema.ts +153 -120
  14. package/src/core/discovery/topology-detector.ts +71 -3
  15. package/src/core/ingestion/pipeline.ts +98 -1
  16. package/src/core/parser/behavioral-analyzer.ts +119 -0
  17. package/src/core/parser/boundary-tracker.ts +160 -0
  18. package/src/core/parser/extractors/base.ts +2 -0
  19. package/src/core/parser/extractors/blade.ts +158 -0
  20. package/src/core/parser/extractors/go.ts +5 -1
  21. package/src/core/parser/extractors/laravel-semantic.ts +506 -31
  22. package/src/core/parser/extractors/php.ts +5 -1
  23. package/src/core/parser/extractors/python.ts +3 -1
  24. package/src/core/parser/extractors/registry.ts +21 -0
  25. package/src/core/parser/extractors/typescript.ts +5 -1
  26. package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
  27. package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
  28. package/src/core/parser/schema/schema-provider-registry.ts +43 -0
  29. package/src/core/parser/schema/schema-provider.interface.ts +33 -0
  30. package/src/core/parser/tree-sitter.ts +2 -0
  31. package/src/core/resolver/path-resolver.ts +147 -0
  32. package/src/core/resolver/vertical-slice-tracer.ts +56 -8
  33. package/src/core/topology/environment-detector.ts +860 -0
  34. package/src/core/topology/types.ts +83 -0
  35. package/src/index.ts +3 -0
  36. package/src/mcp/schemas.ts +264 -46
  37. package/src/mcp/server.ts +130 -26
  38. package/src/mcp/tools/check-boundary.ts +58 -8
  39. package/src/mcp/tools/get-domain-catalog.ts +31 -1
  40. package/src/mcp/tools/get-environment-topology.ts +141 -0
  41. package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
  42. package/src/mcp/tools/locate-symbol.ts +119 -3
  43. package/src/mcp/tools/register-domain.ts +1 -1
  44. package/src/mcp/tools/trace-vertical-slice.ts +11 -65
  45. package/src/types/index.ts +20 -1
  46. package/src/version.ts +3 -3
@@ -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
- const result: SymbolLocationResult = locator.locate(args.query, args.domain);
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
+
@@ -22,7 +22,7 @@ export async function handleRegisterDomain(
22
22
  throw new Error("Missing required arguments: 'name' and 'root' are required.");
23
23
  }
24
24
 
25
- const domainConfig: DomainConfig = {
25
+ const domainConfig = {
26
26
  root: args.root,
27
27
  description: args.description || `Domain: ${args.name}`,
28
28
  allowed_dependencies: args.allowed_dependencies || [],
@@ -18,75 +18,21 @@ export function handleTraceVerticalSlice(
18
18
  );
19
19
  }
20
20
 
21
- const tracer = new VerticalSliceTracer(repo);
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
- let formattedText = "";
25
+ let formattedText = result.message;
25
26
 
26
- if (result.found && result.slice) {
27
- const s = result.slice;
28
- formattedText = `=== VERTICAL SLICE TRACE ===\n`;
29
- formattedText += `Query: ${result.query}\n`;
30
- formattedText += `Status: RESOLVED\n\n`;
31
-
32
- formattedText += `• Route:\n`;
33
- formattedText += ` Method: ${s.route.method}\n`;
34
- formattedText += ` URI: ${s.route.uri}\n`;
35
- if (s.route.name) {
36
- formattedText += ` Name: ${s.route.name}\n`;
37
- }
38
-
39
- if (s.request) {
40
- formattedText += `\n• Request / Validation:\n`;
41
- formattedText += ` Class: ${s.request.class} (${s.request.file || "inline"})\n`;
42
- const ruleKeys = Object.keys(s.request.rules);
43
- if (ruleKeys.length > 0) {
44
- formattedText += ` Rules:\n`;
45
- for (const k of ruleKeys) {
46
- formattedText += ` - ${k} => ${s.request.rules[k]}\n`;
47
- }
48
- }
49
- }
50
-
51
- formattedText += `\n• Controller Action:\n`;
52
- formattedText += ` Class: ${s.controller.class}@${s.controller.action}\n`;
53
- formattedText += ` File: ${s.controller.file || "unknown"}:${s.controller.line}\n`;
54
-
55
- if (s.model) {
56
- formattedText += `\n• Model:\n`;
57
- formattedText += ` Class: ${s.model.class} (${s.model.file || "unknown"})\n`;
58
- if (s.model.fillable && s.model.fillable.length > 0) {
59
- formattedText += ` Fillable: [${s.model.fillable.join(", ")}]\n`;
60
- }
61
- if (s.model.casts && Object.keys(s.model.casts).length > 0) {
62
- formattedText += ` Casts: ${JSON.stringify(s.model.casts)}\n`;
63
- }
27
+ if (result.found && result.alternatives && result.alternatives.length > 0) {
28
+ formattedText += `\n\n• Other Matching Slices:\n`;
29
+ for (const alt of result.alternatives) {
30
+ formattedText += ` - ${alt.method} ${alt.uri} -> ${alt.controller}@${alt.action}\n`;
64
31
  }
65
-
66
- if (s.frontend) {
67
- formattedText += `\n• Frontend Target:\n`;
68
- formattedText += ` Page: ${s.frontend.target}\n`;
69
- if (s.frontend.props.length > 0) {
70
- formattedText += ` Props: [${s.frontend.props.join(", ")}]\n`;
71
- }
72
- }
73
-
74
- if (result.alternatives && result.alternatives.length > 0) {
75
- formattedText += `\n• Other Matching Slices:\n`;
76
- for (const alt of result.alternatives) {
77
- formattedText += ` - ${alt.method} ${alt.uri} -> ${alt.controller}@${alt.action}\n`;
78
- }
79
- }
80
- } else {
81
- formattedText = `=== VERTICAL SLICE NOT FOUND ===\n`;
82
- formattedText += `Query: ${result.query}\n`;
83
- formattedText += `Message: ${result.message}\n`;
84
-
85
- if (result.alternatives && result.alternatives.length > 0) {
86
- formattedText += `\nAvailable Slices:\n`;
87
- for (const alt of result.alternatives) {
88
- formattedText += ` - ${alt.method} ${alt.uri} -> ${alt.controller}@${alt.action}\n`;
89
- }
32
+ } else if (!result.found && result.alternatives && result.alternatives.length > 0) {
33
+ formattedText += `\n\nAvailable Slices:\n`;
34
+ for (const alt of result.alternatives) {
35
+ formattedText += ` - ${alt.method} ${alt.uri} -> ${alt.controller}@${alt.action}\n`;
90
36
  }
91
37
  }
92
38
 
@@ -12,7 +12,9 @@ export type ArchetypeKind =
12
12
  | "middleware"
13
13
  | "request"
14
14
  | "resource"
15
+ | "route"
15
16
  | "util"
17
+ | "view"
16
18
  | "unknown";
17
19
 
18
20
  export type SymbolKind =
@@ -23,7 +25,9 @@ export type SymbolKind =
23
25
  | "method"
24
26
  | "function"
25
27
  | "type"
26
- | "property";
28
+ | "property"
29
+ | "component"
30
+ | "view";
27
31
 
28
32
  export type Visibility = "public" | "protected" | "private";
29
33
 
@@ -120,6 +124,7 @@ export interface SymbolRecord {
120
124
  line_start: number;
121
125
  line_end: number;
122
126
  line_count: number;
127
+ nesting_depth: number;
123
128
  }
124
129
 
125
130
  export interface DependencyRecord {
@@ -139,6 +144,7 @@ export interface ExtractedSymbol {
139
144
  line_start: number;
140
145
  line_end: number;
141
146
  line_count: number;
147
+ nesting_depth?: number;
142
148
  }
143
149
 
144
150
  export interface ExtractedDependency {
@@ -190,12 +196,17 @@ export interface DomainCatalogResponse {
190
196
  export interface ExecutionChainNode {
191
197
  stage:
192
198
  | "ingress"
199
+ | "guard"
193
200
  | "validation"
194
201
  | "controller"
195
202
  | "orchestrator"
196
203
  | "use_case"
197
204
  | "domain"
198
205
  | "entity"
206
+ | "side_effect"
207
+ | "job"
208
+ | "event"
209
+ | "transaction"
199
210
  | "egress"
200
211
  | "repository"
201
212
  | string;
@@ -203,6 +214,14 @@ export interface ExecutionChainNode {
203
214
  file?: string;
204
215
  line?: number;
205
216
  description?: string;
217
+ table?: string;
218
+ schema_file?: string;
219
+ columns_summary?: string;
220
+ guards?: string[];
221
+ mutations?: string[];
222
+ emits?: string[];
223
+ dispatches?: string[];
224
+ transactions?: boolean;
206
225
  }
207
226
 
208
227
  export interface VerticalSliceRecord {
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
- import packageJson from "../package.json" with { type: "json" };
2
-
3
- export const SEPTUM_VERSION: string = packageJson.version;
1
+ import packageJson from "../package.json" with { type: "json" };
2
+
3
+ export const SEPTUM_VERSION: string = packageJson.version;