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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -105
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -63
  5. package/src/cli/commands/check.ts +161 -161
  6. package/src/cli/commands/hook.ts +216 -188
  7. package/src/cli/commands/ingest.ts +29 -29
  8. package/src/cli/commands/init.ts +213 -211
  9. package/src/cli/commands/locate.ts +61 -61
  10. package/src/cli/commands/query.ts +104 -64
  11. package/src/cli/commands/serve.ts +5 -5
  12. package/src/cli/commands/slice.ts +50 -48
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +220 -210
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +50 -38
  17. package/src/cli/templates/skill.ts +65 -69
  18. package/src/core/boundary/evaluator.ts +429 -210
  19. package/src/core/config/loader.ts +117 -97
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -44
  22. package/src/core/database/repositories/dependency.repository.ts +552 -349
  23. package/src/core/database/repositories/domain.repository.ts +275 -198
  24. package/src/core/database/repositories/file.repository.ts +117 -106
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +482 -443
  27. package/src/core/database/repository.ts +281 -260
  28. package/src/core/database/schema.ts +154 -153
  29. package/src/core/discovery/topology-detector.ts +486 -486
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +606 -535
  32. package/src/core/parser/boundary-tracker.ts +433 -288
  33. package/src/core/parser/extractors/base.ts +62 -62
  34. package/src/core/parser/extractors/blade.ts +158 -158
  35. package/src/core/parser/extractors/frontend.ts +147 -194
  36. package/src/core/parser/extractors/go.ts +408 -180
  37. package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
  38. package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
  39. package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
  40. package/src/core/parser/extractors/php.ts +415 -153
  41. package/src/core/parser/extractors/python.ts +460 -228
  42. package/src/core/parser/extractors/registry.ts +70 -70
  43. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  44. package/src/core/parser/extractors/typescript.ts +668 -181
  45. package/src/core/parser/tree-sitter.ts +41 -41
  46. package/src/core/parser/wasm-loader.ts +271 -0
  47. package/src/core/resolver/call-graph-tracer.ts +698 -668
  48. package/src/core/resolver/module-resolver.ts +479 -342
  49. package/src/core/resolver/path-resolver.ts +147 -147
  50. package/src/core/resolver/symbol-locator.ts +543 -543
  51. package/src/core/resolver/vertical-slice-tracer.ts +323 -285
  52. package/src/core/session/session-manager.ts +56 -56
  53. package/src/core/telemetry/telemetry.ts +86 -86
  54. package/src/core/topology/environment-detector.ts +109 -6
  55. package/src/index.ts +21 -21
  56. package/src/mcp/schemas.ts +695 -286
  57. package/src/mcp/server.ts +927 -716
  58. package/src/mcp/tools/check-boundary.ts +307 -152
  59. package/src/mcp/tools/get-domain-catalog.ts +155 -121
  60. package/src/mcp/tools/get-environment-topology.ts +3 -6
  61. package/src/mcp/tools/get-feature-context.ts +227 -116
  62. package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
  63. package/src/mcp/tools/get-symbol-impact.ts +28 -26
  64. package/src/mcp/tools/get-symbol.ts +65 -63
  65. package/src/mcp/tools/locate-symbol.ts +169 -190
  66. package/src/mcp/tools/register-domain.ts +59 -64
  67. package/src/mcp/tools/register-feature.ts +91 -0
  68. package/src/mcp/tools/trace-vertical-slice.ts +74 -48
  69. package/src/types/index.ts +366 -315
  70. package/src/version.ts +3 -3
@@ -1,285 +1,323 @@
1
- import type {
2
- ExecutionChainNode,
3
- VerticalSliceRecord,
4
- VerticalSliceTraceResponse,
5
- } from "../../types/index.ts";
6
- import type { SeptumRepository } from "../database/repository.ts";
7
- import { CallGraphTracer } from "./call-graph-tracer.ts";
8
- import { SemanticSliceExtractorRegistry } from "../parser/extractors/registry.ts";
9
- import { TopologyDetector } from "../discovery/topology-detector.ts";
10
-
11
- export class VerticalSliceTracer {
12
- constructor(private repo: SeptumRepository, private projectRoot: string = process.cwd()) {}
13
-
14
- /**
15
- * Trace an end-to-end vertical slice from route/action/intent query.
16
- * Examples of queries:
17
- * - "POST /orders/{id}/status"
18
- * - "/orders/{id}/status"
19
- * - "OrderController@updateStatus"
20
- * - "OrderController::updateStatus"
21
- * - "orders.update_status"
22
- * - "createOrder"
23
- * - "Ubah status order di dashboard"
24
- */
25
- public trace(rawQuery: string): VerticalSliceTraceResponse {
26
- const trimmed = rawQuery.trim();
27
- const lowerTrimmed = trimmed.toLowerCase();
28
- let allSlices = this.repo.getAllVerticalSlices();
29
-
30
- // Lazy on-demand fallback: If no slices cataloged, extract framework routes or trace call-graph
31
- const hasCatalogData = typeof (this.repo as any).getAllDomains === "function" && this.repo.getAllDomains().length > 0;
32
- if (allSlices.length === 0 && hasCatalogData) {
33
- try {
34
- const topology = TopologyDetector.detect(this.projectRoot);
35
- const semanticSlices = SemanticSliceExtractorRegistry.extractAllSlicesSync(
36
- this.projectRoot,
37
- topology.framework
38
- );
39
- if (semanticSlices.length > 0) {
40
- for (const s of semanticSlices) {
41
- this.repo.upsertVerticalSlice(s);
42
- }
43
- allSlices = this.repo.getAllVerticalSlices();
44
- } else {
45
- const tracer = new CallGraphTracer(this.repo, this.projectRoot);
46
- const traced = tracer.traceAllSlices();
47
- if (traced.length > 0) {
48
- for (const s of traced) {
49
- this.repo.upsertVerticalSlice(s);
50
- }
51
- allSlices = this.repo.getAllVerticalSlices();
52
- }
53
- }
54
- } catch (err: unknown) {
55
- const msg = err instanceof Error ? err.message : String(err);
56
- console.error(`[Septum Slice Warning] Failed on-demand trace of vertical slices: ${msg}`);
57
- }
58
- }
59
-
60
- if (allSlices.length === 0) {
61
- return {
62
- query: trimmed,
63
- found: false,
64
- message: "No vertical slices cataloged. Run 'septum ingest' to extract framework routes and MVC slices.",
65
- };
66
- }
67
-
68
- // 1. Exact HTTP Method + URI match: "POST /orders/{id}/status" or "POST /api/orders"
69
- const methodUriMatch = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s]+)$/i);
70
- if (methodUriMatch) {
71
- const httpMethod = methodUriMatch[1].toUpperCase();
72
- const uri = methodUriMatch[2].toLowerCase();
73
- const exact = allSlices.find(
74
- (s) =>
75
- s.http_method.toUpperCase() === httpMethod &&
76
- (s.route_uri.toLowerCase() === uri || this.urisMatch(s.route_uri, uri))
77
- );
78
- if (exact) {
79
- return this.formatSuccessResponse(trimmed, exact, "exact");
80
- }
81
- }
82
-
83
- // 2. URI-only match: "/orders" or "orders/{id}"
84
- const uriMatches = allSlices.filter((s) => s.route_uri.toLowerCase() === lowerTrimmed);
85
- if (uriMatches.length > 0) {
86
- if (uriMatches.length === 1) {
87
- return this.formatSuccessResponse(trimmed, uriMatches[0], "exact");
88
- }
89
- // Multiple matches (e.g. GET /orders and POST /orders) -> return primary with alternatives
90
- return this.formatSuccessResponse(trimmed, uriMatches[0], "exact", uriMatches.slice(1));
91
- }
92
-
93
- // 3. Named Route match: "orders.status.update" or "admin.users.index"
94
- const nameMatch = allSlices.find(
95
- (s) => s.route_name && s.route_name.toLowerCase() === lowerTrimmed
96
- );
97
- if (nameMatch) {
98
- return this.formatSuccessResponse(trimmed, nameMatch, "exact");
99
- }
100
-
101
- // 3b. Omni-Trigger Match: "JOB ProcessOrderJob", "job:ProcessOrderJob", "CLI order:cleanup", "EVENT OrderPlacedListener"
102
- const omniTriggerMatch = trimmed.match(/^(JOB|CLI|EVENT)\s+([A-Za-z0-9_:\-]+)$/i);
103
- if (omniTriggerMatch) {
104
- const triggerKind = omniTriggerMatch[1].toUpperCase();
105
- const targetName = omniTriggerMatch[2].toLowerCase();
106
- const exact = allSlices.find(
107
- (s) =>
108
- s.http_method.toUpperCase() === triggerKind &&
109
- (s.controller_class.toLowerCase() === targetName ||
110
- s.route_uri.toLowerCase() === targetName ||
111
- s.route_uri.toLowerCase() === `${triggerKind.toLowerCase()}:${targetName}`)
112
- );
113
- if (exact) {
114
- return this.formatSuccessResponse(trimmed, exact, "exact");
115
- }
116
- }
117
-
118
- // 4. Controller@Action match: "OrderController@updateStatus" or "OrderController"
119
- const controllerActionMatch = trimmed.match(/^([A-Za-z0-9_]+)(?:@([A-Za-z0-9_]+))?$/);
120
- if (controllerActionMatch) {
121
- const [, ctrl, act] = controllerActionMatch;
122
- const lowerCtrl = ctrl.toLowerCase();
123
- const lowerAct = act ? act.toLowerCase() : undefined;
124
-
125
- const exact = allSlices.find((s) => {
126
- const ctrlMatch = s.controller_class.toLowerCase() === lowerCtrl || s.controller_class.toLowerCase().endsWith(lowerCtrl);
127
- if (!ctrlMatch) return false;
128
- if (lowerAct) return s.action_name.toLowerCase() === lowerAct;
129
- return true;
130
- });
131
- if (exact) {
132
- return this.formatSuccessResponse(trimmed, exact, "exact");
133
- }
134
- }
135
-
136
- // 5. Symbol Match in Execution Chain or Action Name (e.g. "createOrder", "CreateOrderUseCase", "OrderController")
137
- const symbolMatch = allSlices.find((s) => {
138
- if (s.action_name.toLowerCase() === lowerTrimmed) return true;
139
- if (s.controller_class.toLowerCase() === lowerTrimmed) return true;
140
- if (s.execution_chain_json) {
141
- try {
142
- const chain: ExecutionChainNode[] = JSON.parse(s.execution_chain_json);
143
- return chain.some(
144
- (node) =>
145
- node.symbol.toLowerCase() === lowerTrimmed ||
146
- node.symbol.toLowerCase().includes(lowerTrimmed) ||
147
- (node.file && node.file.toLowerCase().includes(lowerTrimmed))
148
- );
149
- } catch {
150
- return false;
151
- }
152
- }
153
- return false;
154
- });
155
- if (symbolMatch) {
156
- return this.formatSuccessResponse(trimmed, symbolMatch, "inferred");
157
- }
158
-
159
- // 6. Semantic / Token Scoring for Natural Language (e.g. "Ubah status order di dashboard")
160
- const tokens = this.tokenize(trimmed);
161
- const scoredSlices = allSlices
162
- .map((slice) => {
163
- let score = 0;
164
- const textCorpus = [
165
- slice.route_uri,
166
- slice.route_name || "",
167
- slice.controller_class,
168
- slice.action_name,
169
- slice.execution_chain_json || "",
170
- ].join(" ").toLowerCase();
171
-
172
- for (const token of tokens) {
173
- if (textCorpus.includes(token)) {
174
- score += 1;
175
- if (slice.route_uri.toLowerCase().includes(token)) score += 2;
176
- if (slice.action_name.toLowerCase().includes(token)) score += 2;
177
- }
178
- }
179
- return { slice, score };
180
- })
181
- .filter((item) => item.score > 0)
182
- .sort((a, b) => b.score - a.score);
183
-
184
- if (scoredSlices.length > 0) {
185
- const primary = scoredSlices[0].slice;
186
- const alternatives = scoredSlices.slice(1, 4).map((item) => item.slice);
187
- return this.formatSuccessResponse(trimmed, primary, "inferred", alternatives);
188
- }
189
-
190
- return {
191
- query: trimmed,
192
- found: false,
193
- alternatives: allSlices.slice(0, 5).map((s) => ({
194
- method: s.http_method,
195
- uri: s.route_uri,
196
- controller: s.controller_class,
197
- action: s.action_name,
198
- })),
199
- message: `No vertical slice found matching query '${trimmed}'.`,
200
- };
201
- }
202
-
203
- private urisMatch(uriA: string, uriB: string): boolean {
204
- const normA = uriA.toLowerCase().replace(/\{[^}]+\}/g, ":param");
205
- const normB = uriB.toLowerCase().replace(/\{[^}]+\}/g, ":param").replace(/:[a-zA-Z0-9_]+/g, ":param");
206
- return normA === normB;
207
- }
208
-
209
- private tokenize(str: string): string[] {
210
- return str
211
- .replace(/([a-z])([A-Z])/g, "$1 $2")
212
- .toLowerCase()
213
- .replace(/[^a-z0-9_]+/g, " ")
214
- .split(/\s+/)
215
- .filter((t) => t.length > 2 && !["ubah", "di", "dan", "yang", "the", "for", "and"].includes(t));
216
- }
217
-
218
- private formatSuccessResponse(
219
- query: string,
220
- record: VerticalSliceRecord,
221
- confidence: "exact" | "inferred" = "exact",
222
- alternatives?: VerticalSliceRecord[]
223
- ): VerticalSliceTraceResponse {
224
- let chain: ExecutionChainNode[] = [];
225
- if (record.execution_chain_json) {
226
- try {
227
- chain = JSON.parse(record.execution_chain_json);
228
- } catch (err: unknown) {
229
- const msg = err instanceof Error ? err.message : String(err);
230
- console.error(`[Septum Slice Error] Corrupted execution chain JSON for slice ${record.id}: ${msg}`);
231
- }
232
- }
233
-
234
- let message = `Vertical slice resolved (${confidence}): ${record.http_method} ${record.route_uri} -> ${record.controller_class}@${record.action_name}`;
235
- if (chain.length > 0) {
236
- const stageLines = chain.map((node) => {
237
- const stageLabel = `[${node.stage.toUpperCase()}]`.padEnd(14, " ");
238
- const loc = node.file ? ` (${node.file}${node.line ? `:${node.line}` : ""})` : "";
239
- let extra = "";
240
- if (node.table) {
241
- extra += ` [table: ${node.table}]`;
242
- }
243
- if (node.columns_summary) {
244
- extra += ` {${node.columns_summary}}`;
245
- }
246
- if (node.guards && node.guards.length > 0 && node.stage !== "guard") {
247
- extra += ` [guards: ${node.guards.join(", ")}]`;
248
- }
249
- if (node.mutations && node.mutations.length > 0 && node.stage !== "side_effect") {
250
- extra += ` [mutates: ${node.mutations.join(", ")}]`;
251
- }
252
- if (node.transactions && node.stage !== "transaction") {
253
- extra += ` [tx: active]`;
254
- }
255
- return `${stageLabel} ${node.symbol}${loc}${extra}`;
256
- });
257
- message = `=== VERTICAL SLICE TRACE (${confidence.toUpperCase()}) ===\n` + stageLines.join("\n");
258
- }
259
-
260
- return {
261
- query,
262
- found: true,
263
- confidence,
264
- is_exact_match: confidence === "exact",
265
- architecture_style: record.architecture_style,
266
- entrypoint: {
267
- method: record.http_method,
268
- uri: record.route_uri,
269
- name: record.route_name,
270
- controller: record.controller_class,
271
- action: record.action_name,
272
- file: record.controller_file,
273
- line: record.controller_line,
274
- },
275
- chain,
276
- alternatives: alternatives?.map((a) => ({
277
- method: a.http_method,
278
- uri: a.route_uri,
279
- controller: a.controller_class,
280
- action: a.action_name,
281
- })),
282
- message,
283
- };
284
- }
285
- }
1
+ import type {
2
+ ExecutionChainNode,
3
+ VerticalSliceRecord,
4
+ VerticalSliceTraceResponse,
5
+ } from "../../types/index.ts";
6
+ import type { SeptumRepository } from "../database/repository.ts";
7
+ import { CallGraphTracer } from "./call-graph-tracer.ts";
8
+ import { SemanticSliceExtractorRegistry } from "../parser/extractors/registry.ts";
9
+ import { TopologyDetector } from "../discovery/topology-detector.ts";
10
+
11
+ export class VerticalSliceTracer {
12
+ constructor(private repo: SeptumRepository, private projectRoot: string = process.cwd()) {}
13
+
14
+ /**
15
+ * Trace an end-to-end vertical slice from route/action/intent query.
16
+ * Examples of queries:
17
+ * - "POST /orders/{id}/status"
18
+ * - "/orders/{id}/status"
19
+ * - "OrderController@updateStatus"
20
+ * - "OrderController::updateStatus"
21
+ * - "orders.update_status"
22
+ * - "createOrder"
23
+ * - "Ubah status order di dashboard"
24
+ */
25
+ public trace(rawQuery: string, maxDepth: number = 5): VerticalSliceTraceResponse {
26
+ const effectiveMaxDepth = Math.min(Math.max(Math.trunc(maxDepth || 5), 1), 8);
27
+ const trimmed = rawQuery.trim();
28
+ const lowerTrimmed = trimmed.toLowerCase();
29
+ let allSlices = this.repo.getAllVerticalSlices();
30
+
31
+ // Lazy on-demand fallback: If no slices cataloged, extract framework routes or trace call-graph
32
+ const topology = TopologyDetector.detect(this.projectRoot);
33
+ const isSupportedFramework = ["laravel", "express", "nestjs", "fastify"].includes(topology.framework);
34
+ const hasCatalogData = typeof (this.repo as any).getAllDomains === "function" && this.repo.getAllDomains().length > 0;
35
+ if (allSlices.length === 0 && (hasCatalogData || isSupportedFramework)) {
36
+ try {
37
+ const semanticSlices = SemanticSliceExtractorRegistry.extractAllSlicesSync(
38
+ this.projectRoot,
39
+ topology.framework
40
+ );
41
+ if (semanticSlices.length > 0) {
42
+ for (const s of semanticSlices) {
43
+ this.repo.upsertVerticalSlice(s);
44
+ }
45
+ allSlices = this.repo.getAllVerticalSlices();
46
+ } else {
47
+ const tracer = new CallGraphTracer(this.repo, this.projectRoot);
48
+ const traced = tracer.traceAllSlices(effectiveMaxDepth);
49
+ if (traced.length > 0) {
50
+ for (const s of traced) {
51
+ this.repo.upsertVerticalSlice(s);
52
+ }
53
+ allSlices = this.repo.getAllVerticalSlices();
54
+ }
55
+ }
56
+ } catch (err: unknown) {
57
+ const msg = err instanceof Error ? err.message : String(err);
58
+ console.error(`[Septum Slice Warning] Failed on-demand trace of vertical slices: ${msg}`);
59
+ }
60
+ }
61
+
62
+ if (allSlices.length === 0) {
63
+ return {
64
+ query: trimmed,
65
+ found: false,
66
+ max_depth_applied: effectiveMaxDepth,
67
+ message: "No vertical slices cataloged. Run 'septum ingest' to extract framework routes and MVC slices.",
68
+ };
69
+ }
70
+
71
+ // 1. Exact HTTP Method + URI match: "POST /orders/{id}/status" or "POST /api/orders"
72
+ const methodUriMatch = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s]+)$/i);
73
+ if (methodUriMatch) {
74
+ const httpMethod = methodUriMatch[1].toUpperCase();
75
+ const uri = methodUriMatch[2].toLowerCase();
76
+ const exact = allSlices.find(
77
+ (s) =>
78
+ s.http_method.toUpperCase() === httpMethod &&
79
+ (s.route_uri.toLowerCase() === uri || this.urisMatch(s.route_uri, uri))
80
+ );
81
+ if (exact) {
82
+ return this.formatSuccessResponse(trimmed, exact, "exact", undefined, effectiveMaxDepth);
83
+ }
84
+ }
85
+
86
+ // 2. URI-only match: "/orders" or "orders/{id}"
87
+ const uriMatches = allSlices.filter((s) => s.route_uri.toLowerCase() === lowerTrimmed);
88
+ if (uriMatches.length > 0) {
89
+ if (uriMatches.length === 1) {
90
+ return this.formatSuccessResponse(trimmed, uriMatches[0], "exact", undefined, effectiveMaxDepth);
91
+ }
92
+ // Multiple matches (e.g. GET /orders and POST /orders) -> return primary with alternatives
93
+ return this.formatSuccessResponse(trimmed, uriMatches[0], "exact", uriMatches.slice(1), effectiveMaxDepth);
94
+ }
95
+
96
+ // 3. Named Route match: "orders.status.update" or "admin.users.index"
97
+ const nameMatch = allSlices.find(
98
+ (s) => s.route_name && s.route_name.toLowerCase() === lowerTrimmed
99
+ );
100
+ if (nameMatch) {
101
+ return this.formatSuccessResponse(trimmed, nameMatch, "exact", undefined, effectiveMaxDepth);
102
+ }
103
+
104
+ // 3b. Omni-Trigger Match: "JOB ProcessOrderJob", "job:ProcessOrderJob", "CLI order:cleanup", "EVENT OrderPlacedListener"
105
+ const omniTriggerMatch = trimmed.match(/^(JOB|CLI|EVENT)\s+([A-Za-z0-9_:\-]+)$/i);
106
+ if (omniTriggerMatch) {
107
+ const triggerKind = omniTriggerMatch[1].toUpperCase();
108
+ const targetName = omniTriggerMatch[2].toLowerCase();
109
+ const exact = allSlices.find(
110
+ (s) =>
111
+ s.http_method.toUpperCase() === triggerKind &&
112
+ (s.controller_class.toLowerCase() === targetName ||
113
+ s.route_uri.toLowerCase() === targetName ||
114
+ s.route_uri.toLowerCase() === `${triggerKind.toLowerCase()}:${targetName}`)
115
+ );
116
+ if (exact) {
117
+ return this.formatSuccessResponse(trimmed, exact, "exact", undefined, effectiveMaxDepth);
118
+ }
119
+ }
120
+
121
+ // 4. Controller@Action match: "OrderController@updateStatus" or "OrderController"
122
+ const controllerActionMatch = trimmed.match(/^([A-Za-z0-9_\\/.]+?)(?:(?:@|::)([A-Za-z0-9_]+))?$/);
123
+ if (controllerActionMatch) {
124
+ const [, rawCtrl, act] = controllerActionMatch;
125
+ const ctrl = rawCtrl.split(/[\\/]/).pop()?.replace(/\.(php|ts|js)$/i, "") || rawCtrl;
126
+ const lowerCtrl = ctrl.toLowerCase();
127
+ const lowerAct = act ? act.toLowerCase() : undefined;
128
+
129
+ const exact = allSlices.find((s) => {
130
+ const sCtrlLower = s.controller_class.toLowerCase();
131
+ const ctrlMatch =
132
+ sCtrlLower === lowerCtrl ||
133
+ sCtrlLower.endsWith(`\\${lowerCtrl}`) ||
134
+ sCtrlLower.endsWith(`/${lowerCtrl}`);
135
+ if (!ctrlMatch) return false;
136
+ if (lowerAct) return s.action_name.toLowerCase() === lowerAct;
137
+ if (!act && !lowerCtrl.endsWith("controller") && sCtrlLower !== lowerCtrl) {
138
+ return false;
139
+ }
140
+ return true;
141
+ });
142
+ if (exact) {
143
+ return this.formatSuccessResponse(trimmed, exact, "exact", undefined, effectiveMaxDepth);
144
+ }
145
+ }
146
+
147
+ // 5. Symbol Match in Execution Chain or Action Name (e.g. "createOrder", "CreateOrderUseCase", "OrderController")
148
+ const symbolMatch = allSlices.find((s) => {
149
+ if (s.action_name.toLowerCase() === lowerTrimmed) return true;
150
+ if (s.controller_class.toLowerCase() === lowerTrimmed) return true;
151
+ if (s.execution_chain_json) {
152
+ try {
153
+ const chain: ExecutionChainNode[] = JSON.parse(s.execution_chain_json);
154
+ return chain.some(
155
+ (node) =>
156
+ node.symbol.toLowerCase() === lowerTrimmed ||
157
+ node.symbol.toLowerCase().includes(lowerTrimmed) ||
158
+ (node.file && node.file.toLowerCase().includes(lowerTrimmed))
159
+ );
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+ return false;
165
+ });
166
+ if (symbolMatch) {
167
+ return this.formatSuccessResponse(trimmed, symbolMatch, "inferred", undefined, effectiveMaxDepth);
168
+ }
169
+
170
+ // 6. Semantic / Token Scoring for Natural Language (e.g. "Ubah status order di dashboard")
171
+ const tokens = this.tokenize(trimmed);
172
+ const scoredSlices = allSlices
173
+ .map((slice) => {
174
+ let score = 0;
175
+ const textCorpus = [
176
+ slice.route_uri,
177
+ slice.route_name || "",
178
+ slice.controller_class,
179
+ slice.action_name,
180
+ slice.execution_chain_json || "",
181
+ ].join(" ").toLowerCase();
182
+
183
+ for (const token of tokens) {
184
+ if (textCorpus.includes(token)) {
185
+ score += 1;
186
+ if (slice.route_uri.toLowerCase().includes(token)) score += 2;
187
+ if (slice.action_name.toLowerCase().includes(token)) score += 2;
188
+ }
189
+ }
190
+ return { slice, score };
191
+ })
192
+ .filter((item) => item.score > 0)
193
+ .sort((a, b) => b.score - a.score);
194
+
195
+ if (scoredSlices.length > 0) {
196
+ const primary = scoredSlices[0].slice;
197
+ const alternatives = scoredSlices.slice(1, 4).map((item) => item.slice);
198
+ return this.formatSuccessResponse(trimmed, primary, "inferred", alternatives, effectiveMaxDepth);
199
+ }
200
+
201
+ return {
202
+ query: trimmed,
203
+ found: false,
204
+ max_depth_applied: effectiveMaxDepth,
205
+ alternatives: allSlices.slice(0, 5).map((s) => ({
206
+ method: s.http_method,
207
+ uri: s.route_uri,
208
+ controller: s.controller_class,
209
+ action: s.action_name,
210
+ })),
211
+ message: `No vertical slice found matching query '${trimmed}'.`,
212
+ };
213
+ }
214
+
215
+ private urisMatch(uriA: string, uriB: string): boolean {
216
+ const normA = uriA
217
+ .toLowerCase()
218
+ .replace(/\{[^}]+\}/g, ":param")
219
+ .replace(/:[a-zA-Z0-9_]+/g, ":param");
220
+ const normB = uriB
221
+ .toLowerCase()
222
+ .replace(/\{[^}]+\}/g, ":param")
223
+ .replace(/:[a-zA-Z0-9_]+/g, ":param");
224
+ return normA === normB;
225
+ }
226
+
227
+ private tokenize(str: string): string[] {
228
+ return str
229
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
230
+ .toLowerCase()
231
+ .replace(/[^a-z0-9_]+/g, " ")
232
+ .split(/\s+/)
233
+ .filter((t) => t.length > 2 && !["ubah", "di", "dan", "yang", "the", "for", "and"].includes(t));
234
+ }
235
+
236
+ private formatSuccessResponse(
237
+ query: string,
238
+ record: VerticalSliceRecord,
239
+ confidence: "exact" | "inferred" = "exact",
240
+ alternatives?: VerticalSliceRecord[],
241
+ effectiveMaxDepth: number = 5
242
+ ): VerticalSliceTraceResponse {
243
+ let chain: ExecutionChainNode[] = [];
244
+ if (record.execution_chain_json) {
245
+ try {
246
+ chain = JSON.parse(record.execution_chain_json);
247
+ } catch (err: unknown) {
248
+ const msg = err instanceof Error ? err.message : String(err);
249
+ console.error(`[Septum Slice Error] Corrupted execution chain JSON for slice ${record.id}: ${msg}`);
250
+ }
251
+ }
252
+
253
+ if (
254
+ chain.length < effectiveMaxDepth &&
255
+ record.controller_file &&
256
+ typeof (this.repo as any).getAllFiles === "function"
257
+ ) {
258
+ try {
259
+ const callGraphTracer = new CallGraphTracer(this.repo, this.projectRoot);
260
+ chain = callGraphTracer.expandChainFromFile(
261
+ record.controller_file,
262
+ chain,
263
+ effectiveMaxDepth
264
+ );
265
+ } catch {
266
+ // Ignore expansion errors on mock/partial repositories
267
+ }
268
+ }
269
+
270
+ let message = `Vertical slice resolved (${confidence}): ${record.http_method} ${record.route_uri} -> ${record.controller_class}@${record.action_name}`;
271
+ if (chain.length > 0) {
272
+ const stageLines = chain.map((node) => {
273
+ const stageLabel = `[${node.stage.toUpperCase()}]`.padEnd(14, " ");
274
+ const loc = node.file ? ` (${node.file}${node.line ? `:${node.line}` : ""})` : "";
275
+ let extra = "";
276
+ if (node.table) {
277
+ extra += ` [table: ${node.table}]`;
278
+ }
279
+ if (node.columns_summary) {
280
+ extra += ` {${node.columns_summary}}`;
281
+ }
282
+ if (node.guards && node.guards.length > 0 && node.stage !== "guard") {
283
+ extra += ` [guards: ${node.guards.join(", ")}]`;
284
+ }
285
+ if (node.mutations && node.mutations.length > 0 && node.stage !== "side_effect") {
286
+ extra += ` [mutates: ${node.mutations.join(", ")}]`;
287
+ }
288
+ if (node.transactions && node.stage !== "transaction") {
289
+ extra += ` [tx: active]`;
290
+ }
291
+ return `${stageLabel} ${node.symbol}${loc}${extra}`;
292
+ });
293
+ message = `=== VERTICAL SLICE TRACE (${confidence.toUpperCase()}) ===\n` + stageLines.join("\n");
294
+ }
295
+
296
+ return {
297
+ query,
298
+ found: true,
299
+ confidence,
300
+ is_exact_match: confidence === "exact",
301
+ architecture_style: record.architecture_style,
302
+ max_depth_applied: effectiveMaxDepth,
303
+ entrypoint: {
304
+ method: record.http_method,
305
+ uri: record.route_uri,
306
+ name: record.route_name,
307
+ controller: record.controller_class,
308
+ action: record.action_name,
309
+ file: record.controller_file,
310
+ line: record.controller_line,
311
+ },
312
+ chain,
313
+ slice: chain,
314
+ alternatives: alternatives?.map((a) => ({
315
+ method: a.http_method,
316
+ uri: a.route_uri,
317
+ controller: a.controller_class,
318
+ action: a.action_name,
319
+ })),
320
+ message,
321
+ };
322
+ }
323
+ }