septum 0.1.3 → 0.1.5

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