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.
- package/LICENSE +21 -21
- package/README.md +105 -105
- package/bin/septum.ts +4 -4
- package/package.json +63 -63
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +216 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +213 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +104 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +50 -48
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +220 -210
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +50 -38
- package/src/cli/templates/skill.ts +65 -69
- package/src/core/boundary/evaluator.ts +429 -210
- package/src/core/config/loader.ts +117 -97
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -44
- package/src/core/database/repositories/dependency.repository.ts +552 -349
- package/src/core/database/repositories/domain.repository.ts +275 -198
- package/src/core/database/repositories/file.repository.ts +117 -106
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +482 -443
- package/src/core/database/repository.ts +281 -260
- package/src/core/database/schema.ts +154 -153
- package/src/core/discovery/topology-detector.ts +486 -486
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +606 -535
- package/src/core/parser/boundary-tracker.ts +433 -288
- package/src/core/parser/extractors/base.ts +62 -62
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/frontend.ts +147 -194
- package/src/core/parser/extractors/go.ts +408 -180
- package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
- package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
- package/src/core/parser/extractors/php.ts +415 -153
- package/src/core/parser/extractors/python.ts +460 -228
- package/src/core/parser/extractors/registry.ts +70 -70
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +668 -181
- package/src/core/parser/tree-sitter.ts +41 -41
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +698 -668
- package/src/core/resolver/module-resolver.ts +479 -342
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/symbol-locator.ts +543 -543
- package/src/core/resolver/vertical-slice-tracer.ts +323 -285
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/index.ts +21 -21
- package/src/mcp/schemas.ts +695 -286
- package/src/mcp/server.ts +927 -716
- package/src/mcp/tools/check-boundary.ts +307 -152
- package/src/mcp/tools/get-domain-catalog.ts +155 -121
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +227 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
- package/src/mcp/tools/get-symbol-impact.ts +28 -26
- package/src/mcp/tools/get-symbol.ts +65 -63
- package/src/mcp/tools/locate-symbol.ts +169 -190
- package/src/mcp/tools/register-domain.ts +59 -64
- package/src/mcp/tools/register-feature.ts +91 -0
- package/src/mcp/tools/trace-vertical-slice.ts +74 -48
- package/src/types/index.ts +366 -315
- package/src/version.ts +3 -3
|
@@ -1,190 +1,169 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join
|
|
3
|
-
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
4
|
-
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
5
|
-
import type { IngestionPipeline } from "../../core/ingestion/pipeline.ts";
|
|
6
|
-
import {
|
|
7
|
-
SymbolLocator,
|
|
8
|
-
type SymbolLocationResult,
|
|
9
|
-
} from "../../core/resolver/symbol-locator.ts";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
4
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
5
|
+
import type { IngestionPipeline } from "../../core/ingestion/pipeline.ts";
|
|
6
|
+
import {
|
|
7
|
+
SymbolLocator,
|
|
8
|
+
type SymbolLocationResult,
|
|
9
|
+
} from "../../core/resolver/symbol-locator.ts";
|
|
10
|
+
import type { LocateSymbolArgs } from "../schemas.ts";
|
|
11
|
+
|
|
12
|
+
export type { LocateSymbolArgs } from "../schemas.ts";
|
|
13
|
+
|
|
14
|
+
export async function handleLocateSymbol(
|
|
15
|
+
repo: SeptumRepository,
|
|
16
|
+
config: ValidatedSeptumConfig,
|
|
17
|
+
args: Partial<LocateSymbolArgs>,
|
|
18
|
+
pipeline?: IngestionPipeline,
|
|
19
|
+
workspaceRoot?: string
|
|
20
|
+
) {
|
|
21
|
+
const query = (args.query || args.symbol || "").toString().trim();
|
|
22
|
+
if (!query) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Missing required argument: 'query' (e.g. 'OrderController::calculateTotal' or error log snippet)"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const root = workspaceRoot || args.workspace_path || process.cwd();
|
|
29
|
+
const limit = args.limit ?? 10;
|
|
30
|
+
|
|
31
|
+
// 1. Guarded Lazy Ingestion on Empty Catalog / Cold Start
|
|
32
|
+
if (repo.countFiles() === 0 && pipeline) {
|
|
33
|
+
try {
|
|
34
|
+
await pipeline.run(config);
|
|
35
|
+
} catch {
|
|
36
|
+
// Ingestion fallback
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (repo.countFiles() === 0) {
|
|
40
|
+
const diagnostic = {
|
|
41
|
+
status: "NOT_INDEXED",
|
|
42
|
+
found: false,
|
|
43
|
+
query,
|
|
44
|
+
message: `Workspace '${root}' belum terindeks dalam Septum catalog.`,
|
|
45
|
+
suggested_action: {
|
|
46
|
+
tool: "septum_register_domain",
|
|
47
|
+
params: { name: "core", root: "src", ingest_now: true },
|
|
48
|
+
},
|
|
49
|
+
agent_guidance:
|
|
50
|
+
"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.",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: JSON.stringify(diagnostic, null, 2),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
metadata: diagnostic,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const locator = new SymbolLocator(repo);
|
|
66
|
+
let result: SymbolLocationResult = (locator.locate as any)(query, args.domain, limit);
|
|
67
|
+
|
|
68
|
+
// 2. JIT Delta Sync: Check if located file has changed on disk since last ingest
|
|
69
|
+
if (result.found && result.file_path && pipeline) {
|
|
70
|
+
const fullPath = isAbsolute(result.file_path)
|
|
71
|
+
? result.file_path
|
|
72
|
+
: join(root, result.file_path);
|
|
73
|
+
try {
|
|
74
|
+
const reindexed = await pipeline.ingestFile(fullPath, config);
|
|
75
|
+
if (reindexed) {
|
|
76
|
+
result = (locator.locate as any)(query, args.domain, limit);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Silently fall back to existing result
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Fallback on Cache Miss: Search for candidate source files matching the query terms
|
|
84
|
+
if (!result.found && pipeline) {
|
|
85
|
+
try {
|
|
86
|
+
const cleanTerm = query.replace(/[^a-zA-Z0-9_]/g, " ").trim().split(/\s+/)[0] || "";
|
|
87
|
+
if (cleanTerm.length >= 3) {
|
|
88
|
+
const candidateFiles = findCandidateFiles(root, cleanTerm);
|
|
89
|
+
let anyReindexed = false;
|
|
90
|
+
for (const candidate of candidateFiles) {
|
|
91
|
+
const reindexed = await pipeline.ingestFile(candidate, config);
|
|
92
|
+
if (reindexed) anyReindexed = true;
|
|
93
|
+
}
|
|
94
|
+
if (anyReindexed) {
|
|
95
|
+
result = (locator.locate as any)(query, args.domain, limit);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore candidate scan errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const suggestions = (result.suggestions || []).slice(0, limit);
|
|
104
|
+
const summary = result.found
|
|
105
|
+
? `[FOUND] ${result.exact_symbol?.name || query} in ${result.file_path} (lines ${result.exact_symbol?.line_start}-${result.exact_symbol?.line_end})`
|
|
106
|
+
: `[NOT FOUND] Symbol '${query}' not found. ${suggestions.length > 0 ? `Top suggestions: ${suggestions.map((s) => s.name).slice(0, 3).join(", ")}` : "No close suggestions."}`;
|
|
107
|
+
|
|
108
|
+
const payload = {
|
|
109
|
+
status: result.found ? "success" : "not_found",
|
|
110
|
+
found: result.found,
|
|
111
|
+
query,
|
|
112
|
+
file_path: result.file_path || null,
|
|
113
|
+
container_name: result.container_name || null,
|
|
114
|
+
exact_symbol: result.exact_symbol || null,
|
|
115
|
+
sibling_methods: result.sibling_methods || [],
|
|
116
|
+
suggestions,
|
|
117
|
+
summary,
|
|
118
|
+
message: result.message || summary,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text" as const,
|
|
125
|
+
text: JSON.stringify(payload, null, 2),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
metadata: payload,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Searches for potential candidate files whose names resemble parts of the query term.
|
|
134
|
+
*/
|
|
135
|
+
function findCandidateFiles(root: string, term: string): string[] {
|
|
136
|
+
const matched: string[] = [];
|
|
137
|
+
const lowerTerm = term.toLowerCase().replace(/_/g, "");
|
|
138
|
+
const searchDirs = ["src", "app", "lib"].map((d) => join(root, d)).filter((d) => existsSync(d));
|
|
139
|
+
|
|
140
|
+
function scan(dir: string, depth: number = 0) {
|
|
141
|
+
if (depth > 4) return;
|
|
142
|
+
try {
|
|
143
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
|
|
146
|
+
const full = join(dir, e.name);
|
|
147
|
+
if (e.isDirectory()) {
|
|
148
|
+
scan(full, depth + 1);
|
|
149
|
+
} else if (e.isFile() && /\.(ts|tsx|js|jsx|php|py|go)$/i.test(e.name)) {
|
|
150
|
+
const lowerName = e.name.toLowerCase().replace(/[-_.]/g, "");
|
|
151
|
+
if (lowerName.includes(lowerTerm) || lowerTerm.includes(lowerName.replace(/\.[^.]+$/, ""))) {
|
|
152
|
+
matched.push(full);
|
|
153
|
+
if (matched.length >= 5) return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const sDir of searchDirs) {
|
|
163
|
+
scan(sDir);
|
|
164
|
+
if (matched.length >= 5) break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return matched;
|
|
168
|
+
}
|
|
169
|
+
|
|
@@ -1,64 +1,59 @@
|
|
|
1
|
-
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
-
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
-
import { IngestionPipeline, type IngestionMetrics } from "../../core/ingestion/pipeline.ts";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
1
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import { IngestionPipeline, type IngestionMetrics } from "../../core/ingestion/pipeline.ts";
|
|
4
|
+
import type { RegisterDomainArgs } from "../schemas.ts";
|
|
5
|
+
|
|
6
|
+
export type { RegisterDomainArgs } from "../schemas.ts";
|
|
7
|
+
|
|
8
|
+
export async function handleRegisterDomain(
|
|
9
|
+
repo: SeptumRepository,
|
|
10
|
+
config: ValidatedSeptumConfig,
|
|
11
|
+
args: Partial<RegisterDomainArgs>,
|
|
12
|
+
workspaceRoot: string = process.cwd()
|
|
13
|
+
) {
|
|
14
|
+
const domainName = (args.domain || args.name || "").trim();
|
|
15
|
+
if (!domainName || !args.root) {
|
|
16
|
+
throw new Error("Missing required arguments: 'domain' (or 'name') and 'root' are required.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const domainConfig = {
|
|
20
|
+
root: args.root,
|
|
21
|
+
description: args.description || `Domain: ${domainName}`,
|
|
22
|
+
allowed_dependencies: args.allowed_dependencies || [],
|
|
23
|
+
forbidden_dependencies: args.forbidden_dependencies || [],
|
|
24
|
+
archetypes: args.archetypes || {},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const domainId = repo.domains.upsertDomain(domainName, domainConfig);
|
|
28
|
+
|
|
29
|
+
config.domains[domainName] = domainConfig;
|
|
30
|
+
|
|
31
|
+
let ingestionMetrics: IngestionMetrics | null = null;
|
|
32
|
+
if (args.ingest_now) {
|
|
33
|
+
const pipeline = new IngestionPipeline(repo, workspaceRoot);
|
|
34
|
+
ingestionMetrics = await pipeline.run(config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text" as const,
|
|
41
|
+
text: JSON.stringify(
|
|
42
|
+
{
|
|
43
|
+
status: "success",
|
|
44
|
+
message: `Domain '${domainName}' successfully registered in SQLite SSOT.${
|
|
45
|
+
args.ingest_now ? " Initial ingestion completed." : ""
|
|
46
|
+
}`,
|
|
47
|
+
domain_id: domainId,
|
|
48
|
+
domain: domainName,
|
|
49
|
+
name: domainName,
|
|
50
|
+
config: domainConfig,
|
|
51
|
+
...(ingestionMetrics ? { ingestion_metrics: ingestionMetrics } : {}),
|
|
52
|
+
},
|
|
53
|
+
null,
|
|
54
|
+
2
|
|
55
|
+
),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
2
|
+
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import { SessionManager } from "../../core/session/session-manager.ts";
|
|
4
|
+
import type { RegisterFeatureArgs } from "../schemas.ts";
|
|
5
|
+
|
|
6
|
+
export type { RegisterFeatureArgs } from "../schemas.ts";
|
|
7
|
+
|
|
8
|
+
export async function handleRegisterFeature(
|
|
9
|
+
repo: SeptumRepository,
|
|
10
|
+
config: ValidatedSeptumConfig,
|
|
11
|
+
args: Partial<RegisterFeatureArgs>,
|
|
12
|
+
workspaceRoot: string = process.cwd()
|
|
13
|
+
) {
|
|
14
|
+
const featureKey = (args.feature_key || args.feature || "").toString().trim();
|
|
15
|
+
const domain = (args.domain || args.domain_name || "").toString().trim();
|
|
16
|
+
|
|
17
|
+
if (!featureKey || !domain) {
|
|
18
|
+
throw new Error("Missing required arguments: 'feature_key' and 'domain' are required.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rawTouchpoints = args.allowed_touchpoints ?? [];
|
|
22
|
+
const allowedTouchpoints = Array.isArray(rawTouchpoints)
|
|
23
|
+
? rawTouchpoints
|
|
24
|
+
: typeof rawTouchpoints === "string"
|
|
25
|
+
? [rawTouchpoints]
|
|
26
|
+
: [];
|
|
27
|
+
|
|
28
|
+
const rawReuse = args.reuse_symbols;
|
|
29
|
+
const reuseSymbols: string[] = Array.isArray(rawReuse)
|
|
30
|
+
? rawReuse
|
|
31
|
+
: typeof rawReuse === "string"
|
|
32
|
+
? [rawReuse]
|
|
33
|
+
: [];
|
|
34
|
+
|
|
35
|
+
const featureConfig = {
|
|
36
|
+
domain,
|
|
37
|
+
description: args.description || `Feature: ${featureKey}`,
|
|
38
|
+
allowed_touchpoints: allowedTouchpoints,
|
|
39
|
+
reuse_symbols: reuseSymbols,
|
|
40
|
+
input_contract: args.input_contract || {},
|
|
41
|
+
output_contract: args.output_contract || {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (!config.features) {
|
|
45
|
+
config.features = {};
|
|
46
|
+
}
|
|
47
|
+
config.features[featureKey] = featureConfig;
|
|
48
|
+
|
|
49
|
+
if (typeof (repo as any)?.upsertFeature === "function") {
|
|
50
|
+
(repo as any).upsertFeature(featureKey, featureConfig);
|
|
51
|
+
} else if (typeof repo?.setMeta === "function") {
|
|
52
|
+
try {
|
|
53
|
+
const existingRaw = repo.getMeta("features_json");
|
|
54
|
+
const featuresMap = existingRaw ? JSON.parse(existingRaw) : {};
|
|
55
|
+
featuresMap[featureKey] = featureConfig;
|
|
56
|
+
repo.setMeta("features_json", JSON.stringify(featuresMap));
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore meta persistence errors on mock repositories
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Record Active Feature Session Lease (.septum/session.json)
|
|
63
|
+
SessionManager.saveActiveSession(workspaceRoot, {
|
|
64
|
+
feature_key: featureKey,
|
|
65
|
+
domain,
|
|
66
|
+
touchpoints: featureConfig.allowed_touchpoints,
|
|
67
|
+
locked_at: new Date().toISOString(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text" as const,
|
|
74
|
+
text: JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
status: "success",
|
|
77
|
+
message: `Feature '${featureKey}' successfully registered in SQLite SSOT and session lease established.`,
|
|
78
|
+
feature_key: featureKey,
|
|
79
|
+
feature: featureKey,
|
|
80
|
+
domain,
|
|
81
|
+
touchpoints: featureConfig.allowed_touchpoints,
|
|
82
|
+
reuse_symbols: featureConfig.reuse_symbols,
|
|
83
|
+
},
|
|
84
|
+
null,
|
|
85
|
+
2
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|