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.
Files changed (65) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +100 -100
  3. package/bin/septum.ts +4 -4
  4. package/package.json +63 -62
  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 +90 -90
  13. package/src/cli/commands/sync.ts +31 -31
  14. package/src/cli/index.ts +199 -178
  15. package/src/cli/templates/hooks.ts +75 -75
  16. package/src/cli/templates/rules.ts +38 -38
  17. package/src/cli/templates/skill.ts +66 -66
  18. package/src/core/boundary/evaluator.ts +210 -210
  19. package/src/core/config/loader.ts +84 -84
  20. package/src/core/config/schema.ts +39 -39
  21. package/src/core/database/client.ts +44 -132
  22. package/src/core/database/repositories/dependency.repository.ts +279 -279
  23. package/src/core/database/repositories/domain.repository.ts +198 -198
  24. package/src/core/database/repositories/file.repository.ts +99 -99
  25. package/src/core/database/repositories/meta.repository.ts +33 -33
  26. package/src/core/database/repositories/symbol.repository.ts +424 -424
  27. package/src/core/database/repository.ts +242 -242
  28. package/src/core/database/schema.ts +120 -0
  29. package/src/core/discovery/topology-detector.ts +418 -418
  30. package/src/core/ingestion/hasher.ts +5 -5
  31. package/src/core/ingestion/pipeline.ts +438 -438
  32. package/src/core/parser/boundary-tracker.ts +128 -128
  33. package/src/core/parser/extractors/base.ts +60 -60
  34. package/src/core/parser/extractors/frontend.ts +194 -194
  35. package/src/core/parser/extractors/go.ts +176 -176
  36. package/src/core/parser/extractors/laravel-semantic.ts +426 -426
  37. package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -199
  38. package/src/core/parser/extractors/nestjs-semantic.ts +119 -119
  39. package/src/core/parser/extractors/php.ts +149 -149
  40. package/src/core/parser/extractors/python.ts +226 -226
  41. package/src/core/parser/extractors/registry.ts +49 -49
  42. package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
  43. package/src/core/parser/extractors/typescript.ts +177 -177
  44. package/src/core/parser/tree-sitter.ts +39 -39
  45. package/src/core/resolver/call-graph-tracer.ts +668 -668
  46. package/src/core/resolver/module-resolver.ts +342 -323
  47. package/src/core/resolver/symbol-locator.ts +543 -405
  48. package/src/core/resolver/vertical-slice-tracer.ts +237 -236
  49. package/src/core/session/session-manager.ts +56 -56
  50. package/src/core/telemetry/telemetry.ts +86 -86
  51. package/src/index.ts +18 -18
  52. package/src/mcp/schemas.ts +68 -68
  53. package/src/mcp/server.ts +612 -602
  54. package/src/mcp/tools/check-boundary.ts +102 -102
  55. package/src/mcp/tools/get-domain-catalog.ts +91 -91
  56. package/src/mcp/tools/get-feature-context.ts +116 -116
  57. package/src/mcp/tools/get-symbol-hotspots.ts +49 -49
  58. package/src/mcp/tools/get-symbol-impact.ts +26 -26
  59. package/src/mcp/tools/get-symbol.ts +63 -63
  60. package/src/mcp/tools/locate-symbol.ts +74 -74
  61. package/src/mcp/tools/register-domain.ts +64 -64
  62. package/src/mcp/tools/trace-vertical-slice.ts +102 -102
  63. package/src/types/index.ts +296 -296
  64. package/src/version.ts +3 -0
  65. package/src/core/database/schema.sql +0 -92
@@ -1,236 +1,237 @@
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
-
9
- export class VerticalSliceTracer {
10
- constructor(private repo: SeptumRepository) {}
11
-
12
- /**
13
- * Trace an end-to-end vertical slice from route/action/intent query.
14
- * Examples of queries:
15
- * - "POST /orders/{id}/status"
16
- * - "/orders/{id}/status"
17
- * - "OrderController@updateStatus"
18
- * - "OrderController::updateStatus"
19
- * - "orders.update_status"
20
- * - "createOrder"
21
- * - "Ubah status order di dashboard"
22
- */
23
- public trace(rawQuery: string): VerticalSliceTraceResponse {
24
- const trimmed = rawQuery.trim();
25
- let allSlices = this.repo.getAllVerticalSlices();
26
-
27
- // Lazy fallback: If no slices cataloged, attempt heuristic call-graph tracing
28
- if (allSlices.length === 0) {
29
- try {
30
- const tracer = new CallGraphTracer(this.repo);
31
- const traced = tracer.traceAllSlices();
32
- if (traced.length > 0) {
33
- for (const s of traced) {
34
- this.repo.upsertVerticalSlice(s);
35
- }
36
- allSlices = this.repo.getAllVerticalSlices();
37
- }
38
- } catch (err: unknown) {
39
- const msg = err instanceof Error ? err.message : String(err);
40
- console.error(`[Septum Slice Warning] Failed on-demand trace of vertical slices: ${msg}`);
41
- }
42
- }
43
-
44
- if (allSlices.length === 0) {
45
- return {
46
- query: trimmed,
47
- found: false,
48
- message: "No vertical slices cataloged. Run 'septum ingest' to extract framework routes and MVC slices.",
49
- };
50
- }
51
-
52
- // 1. Exact HTTP Method + URI match: "POST /orders/{id}/status" or "POST /api/orders"
53
- const methodUriMatch = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s]+)$/i);
54
- if (methodUriMatch) {
55
- const httpMethod = methodUriMatch[1].toUpperCase();
56
- const uri = methodUriMatch[2].toLowerCase();
57
- const exact = allSlices.find(
58
- (s) =>
59
- s.http_method.toUpperCase() === httpMethod &&
60
- (s.route_uri.toLowerCase() === uri || this.urisMatch(s.route_uri, uri))
61
- );
62
- if (exact) {
63
- return this.formatSuccessResponse(trimmed, exact, "exact");
64
- }
65
- }
66
-
67
- // 2. URI-only match: "/orders" or "orders/{id}"
68
- const uriMatches = allSlices.filter((s) => s.route_uri.toLowerCase() === lowerTrimmed);
69
- if (uriMatches.length > 0) {
70
- if (uriMatches.length === 1) {
71
- return this.formatSuccessResponse(trimmed, uriMatches[0], "exact");
72
- }
73
- // Multiple matches (e.g. GET /orders and POST /orders) -> return primary with alternatives
74
- return this.formatSuccessResponse(trimmed, uriMatches[0], "exact", uriMatches.slice(1));
75
- }
76
-
77
- // 3. Named Route match: "orders.status.update" or "admin.users.index"
78
- const nameMatch = allSlices.find(
79
- (s) => s.route_name && s.route_name.toLowerCase() === lowerTrimmed
80
- );
81
- if (nameMatch) {
82
- return this.formatSuccessResponse(trimmed, nameMatch, "exact");
83
- }
84
-
85
- // 4. Controller@Action match: "OrderController@updateStatus" or "OrderController"
86
- const controllerActionMatch = trimmed.match(/^([A-Za-z0-9_]+)(?:@([A-Za-z0-9_]+))?$/);
87
- if (controllerActionMatch) {
88
- const [, ctrl, act] = controllerActionMatch;
89
- const lowerCtrl = ctrl.toLowerCase();
90
- const lowerAct = act ? act.toLowerCase() : undefined;
91
-
92
- const exact = allSlices.find((s) => {
93
- const ctrlMatch = s.controller_class.toLowerCase() === lowerCtrl || s.controller_class.toLowerCase().endsWith(lowerCtrl);
94
- if (!ctrlMatch) return false;
95
- if (lowerAct) return s.action_name.toLowerCase() === lowerAct;
96
- return true;
97
- });
98
- if (exact) {
99
- return this.formatSuccessResponse(trimmed, exact, "exact");
100
- }
101
- }
102
-
103
- // 5. Symbol Match in Execution Chain or Action Name (e.g. "createOrder", "CreateOrderUseCase", "OrderController")
104
- const symbolMatch = allSlices.find((s) => {
105
- if (s.action_name.toLowerCase() === lowerTrimmed) return true;
106
- if (s.controller_class.toLowerCase() === lowerTrimmed) return true;
107
- if (s.execution_chain_json) {
108
- try {
109
- const chain: ExecutionChainNode[] = JSON.parse(s.execution_chain_json);
110
- return chain.some(
111
- (node) =>
112
- node.symbol.toLowerCase() === lowerTrimmed ||
113
- node.symbol.toLowerCase().includes(lowerTrimmed) ||
114
- (node.file && node.file.toLowerCase().includes(lowerTrimmed))
115
- );
116
- } catch {
117
- return false;
118
- }
119
- }
120
- return false;
121
- });
122
- if (symbolMatch) {
123
- return this.formatSuccessResponse(trimmed, symbolMatch, "inferred");
124
- }
125
-
126
- // 6. Semantic / Token Scoring for Natural Language (e.g. "Ubah status order di dashboard")
127
- const tokens = this.tokenize(trimmed);
128
- const scoredSlices = allSlices
129
- .map((slice) => {
130
- let score = 0;
131
- const textCorpus = [
132
- slice.route_uri,
133
- slice.route_name || "",
134
- slice.controller_class,
135
- slice.action_name,
136
- slice.execution_chain_json || "",
137
- ].join(" ").toLowerCase();
138
-
139
- for (const token of tokens) {
140
- if (textCorpus.includes(token)) {
141
- score += 1;
142
- if (slice.route_uri.toLowerCase().includes(token)) score += 2;
143
- if (slice.action_name.toLowerCase().includes(token)) score += 2;
144
- }
145
- }
146
- return { slice, score };
147
- })
148
- .filter((item) => item.score > 0)
149
- .sort((a, b) => b.score - a.score);
150
-
151
- if (scoredSlices.length > 0) {
152
- const primary = scoredSlices[0].slice;
153
- const alternatives = scoredSlices.slice(1, 4).map((item) => item.slice);
154
- return this.formatSuccessResponse(trimmed, primary, "inferred", alternatives);
155
- }
156
-
157
- return {
158
- query: trimmed,
159
- found: false,
160
- alternatives: allSlices.slice(0, 5).map((s) => ({
161
- method: s.http_method,
162
- uri: s.route_uri,
163
- controller: s.controller_class,
164
- action: s.action_name,
165
- })),
166
- message: `No vertical slice found matching query '${trimmed}'.`,
167
- };
168
- }
169
-
170
- private urisMatch(uriA: string, uriB: string): boolean {
171
- const normA = uriA.toLowerCase().replace(/\{[^}]+\}/g, ":param");
172
- const normB = uriB.toLowerCase().replace(/\{[^}]+\}/g, ":param").replace(/:[a-zA-Z0-9_]+/g, ":param");
173
- return normA === normB;
174
- }
175
-
176
- private tokenize(str: string): string[] {
177
- return str
178
- .replace(/([a-z])([A-Z])/g, "$1 $2")
179
- .toLowerCase()
180
- .replace(/[^a-z0-9_]+/g, " ")
181
- .split(/\s+/)
182
- .filter((t) => t.length > 2 && !["ubah", "di", "dan", "yang", "the", "for", "and"].includes(t));
183
- }
184
-
185
- private formatSuccessResponse(
186
- query: string,
187
- record: VerticalSliceRecord,
188
- confidence: "exact" | "inferred" = "exact",
189
- alternatives?: VerticalSliceRecord[]
190
- ): VerticalSliceTraceResponse {
191
- let chain: ExecutionChainNode[] = [];
192
- if (record.execution_chain_json) {
193
- try {
194
- chain = JSON.parse(record.execution_chain_json);
195
- } catch (err: unknown) {
196
- const msg = err instanceof Error ? err.message : String(err);
197
- console.error(`[Septum Slice Error] Corrupted execution chain JSON for slice ${record.id}: ${msg}`);
198
- }
199
- }
200
-
201
- let message = `Vertical slice resolved (${confidence}): ${record.http_method} ${record.route_uri} -> ${record.controller_class}@${record.action_name}`;
202
- if (chain.length > 0) {
203
- const stageLines = chain.map((node) => {
204
- const stageLabel = `[${node.stage.toUpperCase()}]`.padEnd(14, " ");
205
- const loc = node.file ? ` (${node.file}${node.line ? `:${node.line}` : ""})` : "";
206
- return `${stageLabel} ${node.symbol}${loc}`;
207
- });
208
- message = `=== VERTICAL SLICE TRACE (${confidence.toUpperCase()}) ===\n` + stageLines.join("\n");
209
- }
210
-
211
- return {
212
- query,
213
- found: true,
214
- confidence,
215
- is_exact_match: confidence === "exact",
216
- architecture_style: record.architecture_style,
217
- entrypoint: {
218
- method: record.http_method,
219
- uri: record.route_uri,
220
- name: record.route_name,
221
- controller: record.controller_class,
222
- action: record.action_name,
223
- file: record.controller_file,
224
- line: record.controller_line,
225
- },
226
- chain,
227
- alternatives: alternatives?.map((a) => ({
228
- method: a.http_method,
229
- uri: a.route_uri,
230
- controller: a.controller_class,
231
- action: a.action_name,
232
- })),
233
- message,
234
- };
235
- }
236
- }
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
+
9
+ export class VerticalSliceTracer {
10
+ constructor(private repo: SeptumRepository) {}
11
+
12
+ /**
13
+ * Trace an end-to-end vertical slice from route/action/intent query.
14
+ * Examples of queries:
15
+ * - "POST /orders/{id}/status"
16
+ * - "/orders/{id}/status"
17
+ * - "OrderController@updateStatus"
18
+ * - "OrderController::updateStatus"
19
+ * - "orders.update_status"
20
+ * - "createOrder"
21
+ * - "Ubah status order di dashboard"
22
+ */
23
+ public trace(rawQuery: string): VerticalSliceTraceResponse {
24
+ const trimmed = rawQuery.trim();
25
+ const lowerTrimmed = trimmed.toLowerCase();
26
+ let allSlices = this.repo.getAllVerticalSlices();
27
+
28
+ // Lazy fallback: If no slices cataloged, attempt heuristic call-graph tracing
29
+ if (allSlices.length === 0) {
30
+ try {
31
+ const tracer = new CallGraphTracer(this.repo);
32
+ const traced = tracer.traceAllSlices();
33
+ if (traced.length > 0) {
34
+ for (const s of traced) {
35
+ this.repo.upsertVerticalSlice(s);
36
+ }
37
+ allSlices = this.repo.getAllVerticalSlices();
38
+ }
39
+ } catch (err: unknown) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ console.error(`[Septum Slice Warning] Failed on-demand trace of vertical slices: ${msg}`);
42
+ }
43
+ }
44
+
45
+ if (allSlices.length === 0) {
46
+ return {
47
+ query: trimmed,
48
+ found: false,
49
+ message: "No vertical slices cataloged. Run 'septum ingest' to extract framework routes and MVC slices.",
50
+ };
51
+ }
52
+
53
+ // 1. Exact HTTP Method + URI match: "POST /orders/{id}/status" or "POST /api/orders"
54
+ const methodUriMatch = trimmed.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s]+)$/i);
55
+ if (methodUriMatch) {
56
+ const httpMethod = methodUriMatch[1].toUpperCase();
57
+ const uri = methodUriMatch[2].toLowerCase();
58
+ const exact = allSlices.find(
59
+ (s) =>
60
+ s.http_method.toUpperCase() === httpMethod &&
61
+ (s.route_uri.toLowerCase() === uri || this.urisMatch(s.route_uri, uri))
62
+ );
63
+ if (exact) {
64
+ return this.formatSuccessResponse(trimmed, exact, "exact");
65
+ }
66
+ }
67
+
68
+ // 2. URI-only match: "/orders" or "orders/{id}"
69
+ const uriMatches = allSlices.filter((s) => s.route_uri.toLowerCase() === lowerTrimmed);
70
+ if (uriMatches.length > 0) {
71
+ if (uriMatches.length === 1) {
72
+ return this.formatSuccessResponse(trimmed, uriMatches[0], "exact");
73
+ }
74
+ // Multiple matches (e.g. GET /orders and POST /orders) -> return primary with alternatives
75
+ return this.formatSuccessResponse(trimmed, uriMatches[0], "exact", uriMatches.slice(1));
76
+ }
77
+
78
+ // 3. Named Route match: "orders.status.update" or "admin.users.index"
79
+ const nameMatch = allSlices.find(
80
+ (s) => s.route_name && s.route_name.toLowerCase() === lowerTrimmed
81
+ );
82
+ if (nameMatch) {
83
+ return this.formatSuccessResponse(trimmed, nameMatch, "exact");
84
+ }
85
+
86
+ // 4. Controller@Action match: "OrderController@updateStatus" or "OrderController"
87
+ const controllerActionMatch = trimmed.match(/^([A-Za-z0-9_]+)(?:@([A-Za-z0-9_]+))?$/);
88
+ if (controllerActionMatch) {
89
+ const [, ctrl, act] = controllerActionMatch;
90
+ const lowerCtrl = ctrl.toLowerCase();
91
+ const lowerAct = act ? act.toLowerCase() : undefined;
92
+
93
+ const exact = allSlices.find((s) => {
94
+ const ctrlMatch = s.controller_class.toLowerCase() === lowerCtrl || s.controller_class.toLowerCase().endsWith(lowerCtrl);
95
+ if (!ctrlMatch) return false;
96
+ if (lowerAct) return s.action_name.toLowerCase() === lowerAct;
97
+ return true;
98
+ });
99
+ if (exact) {
100
+ return this.formatSuccessResponse(trimmed, exact, "exact");
101
+ }
102
+ }
103
+
104
+ // 5. Symbol Match in Execution Chain or Action Name (e.g. "createOrder", "CreateOrderUseCase", "OrderController")
105
+ const symbolMatch = allSlices.find((s) => {
106
+ if (s.action_name.toLowerCase() === lowerTrimmed) return true;
107
+ if (s.controller_class.toLowerCase() === lowerTrimmed) return true;
108
+ if (s.execution_chain_json) {
109
+ try {
110
+ const chain: ExecutionChainNode[] = JSON.parse(s.execution_chain_json);
111
+ return chain.some(
112
+ (node) =>
113
+ node.symbol.toLowerCase() === lowerTrimmed ||
114
+ node.symbol.toLowerCase().includes(lowerTrimmed) ||
115
+ (node.file && node.file.toLowerCase().includes(lowerTrimmed))
116
+ );
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+ return false;
122
+ });
123
+ if (symbolMatch) {
124
+ return this.formatSuccessResponse(trimmed, symbolMatch, "inferred");
125
+ }
126
+
127
+ // 6. Semantic / Token Scoring for Natural Language (e.g. "Ubah status order di dashboard")
128
+ const tokens = this.tokenize(trimmed);
129
+ const scoredSlices = allSlices
130
+ .map((slice) => {
131
+ let score = 0;
132
+ const textCorpus = [
133
+ slice.route_uri,
134
+ slice.route_name || "",
135
+ slice.controller_class,
136
+ slice.action_name,
137
+ slice.execution_chain_json || "",
138
+ ].join(" ").toLowerCase();
139
+
140
+ for (const token of tokens) {
141
+ if (textCorpus.includes(token)) {
142
+ score += 1;
143
+ if (slice.route_uri.toLowerCase().includes(token)) score += 2;
144
+ if (slice.action_name.toLowerCase().includes(token)) score += 2;
145
+ }
146
+ }
147
+ return { slice, score };
148
+ })
149
+ .filter((item) => item.score > 0)
150
+ .sort((a, b) => b.score - a.score);
151
+
152
+ if (scoredSlices.length > 0) {
153
+ const primary = scoredSlices[0].slice;
154
+ const alternatives = scoredSlices.slice(1, 4).map((item) => item.slice);
155
+ return this.formatSuccessResponse(trimmed, primary, "inferred", alternatives);
156
+ }
157
+
158
+ return {
159
+ query: trimmed,
160
+ found: false,
161
+ alternatives: allSlices.slice(0, 5).map((s) => ({
162
+ method: s.http_method,
163
+ uri: s.route_uri,
164
+ controller: s.controller_class,
165
+ action: s.action_name,
166
+ })),
167
+ message: `No vertical slice found matching query '${trimmed}'.`,
168
+ };
169
+ }
170
+
171
+ private urisMatch(uriA: string, uriB: string): boolean {
172
+ const normA = uriA.toLowerCase().replace(/\{[^}]+\}/g, ":param");
173
+ const normB = uriB.toLowerCase().replace(/\{[^}]+\}/g, ":param").replace(/:[a-zA-Z0-9_]+/g, ":param");
174
+ return normA === normB;
175
+ }
176
+
177
+ private tokenize(str: string): string[] {
178
+ return str
179
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
180
+ .toLowerCase()
181
+ .replace(/[^a-z0-9_]+/g, " ")
182
+ .split(/\s+/)
183
+ .filter((t) => t.length > 2 && !["ubah", "di", "dan", "yang", "the", "for", "and"].includes(t));
184
+ }
185
+
186
+ private formatSuccessResponse(
187
+ query: string,
188
+ record: VerticalSliceRecord,
189
+ confidence: "exact" | "inferred" = "exact",
190
+ alternatives?: VerticalSliceRecord[]
191
+ ): VerticalSliceTraceResponse {
192
+ let chain: ExecutionChainNode[] = [];
193
+ if (record.execution_chain_json) {
194
+ try {
195
+ chain = JSON.parse(record.execution_chain_json);
196
+ } catch (err: unknown) {
197
+ const msg = err instanceof Error ? err.message : String(err);
198
+ console.error(`[Septum Slice Error] Corrupted execution chain JSON for slice ${record.id}: ${msg}`);
199
+ }
200
+ }
201
+
202
+ let message = `Vertical slice resolved (${confidence}): ${record.http_method} ${record.route_uri} -> ${record.controller_class}@${record.action_name}`;
203
+ if (chain.length > 0) {
204
+ const stageLines = chain.map((node) => {
205
+ const stageLabel = `[${node.stage.toUpperCase()}]`.padEnd(14, " ");
206
+ const loc = node.file ? ` (${node.file}${node.line ? `:${node.line}` : ""})` : "";
207
+ return `${stageLabel} ${node.symbol}${loc}`;
208
+ });
209
+ message = `=== VERTICAL SLICE TRACE (${confidence.toUpperCase()}) ===\n` + stageLines.join("\n");
210
+ }
211
+
212
+ return {
213
+ query,
214
+ found: true,
215
+ confidence,
216
+ is_exact_match: confidence === "exact",
217
+ architecture_style: record.architecture_style,
218
+ entrypoint: {
219
+ method: record.http_method,
220
+ uri: record.route_uri,
221
+ name: record.route_name,
222
+ controller: record.controller_class,
223
+ action: record.action_name,
224
+ file: record.controller_file,
225
+ line: record.controller_line,
226
+ },
227
+ chain,
228
+ alternatives: alternatives?.map((a) => ({
229
+ method: a.http_method,
230
+ uri: a.route_uri,
231
+ controller: a.controller_class,
232
+ action: a.action_name,
233
+ })),
234
+ message,
235
+ };
236
+ }
237
+ }
@@ -1,56 +1,56 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import type { ActiveFeatureSession } from "../../types/index.ts";
4
-
5
- export class SessionManager {
6
- private static readonly SESSION_FILE = ".septum/session.json";
7
-
8
- /**
9
- * Save the active feature session to .septum/session.json
10
- */
11
- public static saveActiveSession(workspaceRoot: string, session: ActiveFeatureSession): void {
12
- const septumDir = join(workspaceRoot, ".septum");
13
- if (!existsSync(septumDir)) {
14
- mkdirSync(septumDir, { recursive: true });
15
- }
16
- const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
17
- writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
18
- }
19
-
20
- /**
21
- * Get the active feature session, or null if no session exists or file is invalid
22
- */
23
- public static getActiveSession(workspaceRoot: string): ActiveFeatureSession | null {
24
- const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
25
- if (!existsSync(sessionPath)) {
26
- return null;
27
- }
28
-
29
- try {
30
- const raw = readFileSync(sessionPath, "utf-8");
31
- const parsed = JSON.parse(raw) as ActiveFeatureSession;
32
- if (parsed && typeof parsed.feature_key === "string" && Array.isArray(parsed.touchpoints)) {
33
- return parsed;
34
- }
35
- return null;
36
- } catch {
37
- return null;
38
- }
39
- }
40
-
41
- /**
42
- * Clear the active feature session
43
- */
44
- public static clearActiveSession(workspaceRoot: string): boolean {
45
- const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
46
- if (existsSync(sessionPath)) {
47
- try {
48
- rmSync(sessionPath, { force: true });
49
- return true;
50
- } catch {
51
- return false;
52
- }
53
- }
54
- return false;
55
- }
56
- }
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ActiveFeatureSession } from "../../types/index.ts";
4
+
5
+ export class SessionManager {
6
+ private static readonly SESSION_FILE = ".septum/session.json";
7
+
8
+ /**
9
+ * Save the active feature session to .septum/session.json
10
+ */
11
+ public static saveActiveSession(workspaceRoot: string, session: ActiveFeatureSession): void {
12
+ const septumDir = join(workspaceRoot, ".septum");
13
+ if (!existsSync(septumDir)) {
14
+ mkdirSync(septumDir, { recursive: true });
15
+ }
16
+ const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
17
+ writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
18
+ }
19
+
20
+ /**
21
+ * Get the active feature session, or null if no session exists or file is invalid
22
+ */
23
+ public static getActiveSession(workspaceRoot: string): ActiveFeatureSession | null {
24
+ const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
25
+ if (!existsSync(sessionPath)) {
26
+ return null;
27
+ }
28
+
29
+ try {
30
+ const raw = readFileSync(sessionPath, "utf-8");
31
+ const parsed = JSON.parse(raw) as ActiveFeatureSession;
32
+ if (parsed && typeof parsed.feature_key === "string" && Array.isArray(parsed.touchpoints)) {
33
+ return parsed;
34
+ }
35
+ return null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Clear the active feature session
43
+ */
44
+ public static clearActiveSession(workspaceRoot: string): boolean {
45
+ const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
46
+ if (existsSync(sessionPath)) {
47
+ try {
48
+ rmSync(sessionPath, { force: true });
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+ }