septum 0.1.6 → 0.1.7
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/README.md +23 -3
- package/package.json +5 -5
- package/src/cli/commands/check.ts +32 -122
- package/src/cli/commands/hook.ts +9 -4
- package/src/cli/index.ts +2 -1
- package/src/core/boundary/evaluator.ts +21 -25
- package/src/core/boundary/staged-auditor.ts +47 -0
- package/src/core/config/loader.ts +21 -24
- package/src/core/database/repositories/dependency.repository.ts +3 -3
- package/src/core/database/repositories/symbol.repository.ts +71 -0
- package/src/core/ingestion/pipeline.ts +50 -36
- package/src/core/parser/extractors/frontend.ts +1 -0
- package/src/core/parser/extractors/go.ts +2 -2
- package/src/core/parser/extractors/php.ts +2 -2
- package/src/core/parser/extractors/python.ts +2 -2
- package/src/core/parser/extractors/typescript.ts +2 -2
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +3 -0
- package/src/core/resolver/module-resolver.ts +1 -10
- package/src/core/resolver/vertical-slice-tracer.ts +1 -0
- package/src/core/session/session-manager.ts +55 -39
- package/src/mcp/server.ts +99 -88
- package/src/mcp/tools/check-boundary.ts +15 -33
- package/src/mcp/tools/get-domain-catalog.ts +6 -1
- package/src/mcp/tools/locate-symbol.ts +25 -2
- package/src/mcp/tools/register-feature.ts +9 -7
- package/src/types/index.ts +20 -1
- package/src/types/wasm.d.ts +4 -0
package/src/mcp/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, watch, type FSWatcher } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, statSync, watch, type FSWatcher } from "node:fs";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import {
|
|
@@ -12,6 +12,7 @@ import { ConfigLoader } from "../core/config/loader.ts";
|
|
|
12
12
|
import { SeptumDatabase } from "../core/database/client.ts";
|
|
13
13
|
import { SeptumRepository } from "../core/database/repository.ts";
|
|
14
14
|
import { IngestionPipeline } from "../core/ingestion/pipeline.ts";
|
|
15
|
+
import { ASTParserEngine } from "../core/parser/tree-sitter.ts";
|
|
15
16
|
import { SessionManager } from "../core/session/session-manager.ts";
|
|
16
17
|
import { SeptumTelemetry } from "../core/telemetry/telemetry.ts";
|
|
17
18
|
import { handleCheckBoundary } from "./tools/check-boundary.ts";
|
|
@@ -52,6 +53,8 @@ interface WorkspaceContext {
|
|
|
52
53
|
repo: SeptumRepository;
|
|
53
54
|
evaluator: BoundaryEvaluator;
|
|
54
55
|
pipeline: IngestionPipeline;
|
|
56
|
+
ready: Promise<void>;
|
|
57
|
+
watcher: { flush: () => Promise<void>; close: () => void };
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
const workspaceContextCache = new Map<string, WorkspaceContext>();
|
|
@@ -127,6 +130,87 @@ export function extractPathHint(args: unknown): string | undefined {
|
|
|
127
130
|
return undefined;
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
function createWorkspaceWatcher(root: string, repo: SeptumRepository, pipeline: IngestionPipeline, ready: Promise<void>) {
|
|
134
|
+
const pending = new Set<string>();
|
|
135
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
136
|
+
let flushing: Promise<void> | null = null;
|
|
137
|
+
let closed = false;
|
|
138
|
+
let needsFullSync = false;
|
|
139
|
+
const ignored = new Set(["node_modules", "vendor", ".git", ".septum", ".strata", "dist", "build", ".output", ".nuxt", ".next", ".cache", "coverage", ".turbo"]);
|
|
140
|
+
let watcher: FSWatcher | null = null;
|
|
141
|
+
let polling: ReturnType<typeof setInterval> | null = null;
|
|
142
|
+
|
|
143
|
+
const flush = async (): Promise<void> => {
|
|
144
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
145
|
+
if (flushing) return flushing;
|
|
146
|
+
flushing = (async () => {
|
|
147
|
+
await ready;
|
|
148
|
+
if (!closed && (!watcher || needsFullSync || repo.getMeta("sync_error"))) {
|
|
149
|
+
needsFullSync = false;
|
|
150
|
+
await pipeline.run(ConfigLoader.load(undefined, root), { verifyContent: true });
|
|
151
|
+
repo.setMeta("sync_error", "");
|
|
152
|
+
}
|
|
153
|
+
while (!closed && pending.size > 0) {
|
|
154
|
+
const paths = [...pending];
|
|
155
|
+
pending.clear();
|
|
156
|
+
const config = ConfigLoader.load(undefined, root);
|
|
157
|
+
for (const filename of paths) {
|
|
158
|
+
const target = resolve(root, filename);
|
|
159
|
+
const relPath = relative(root, target).replace(/\\/g, "/");
|
|
160
|
+
if (!existsSync(target)) {
|
|
161
|
+
repo.runInTransaction(() => { repo.deleteFileByPath(relPath); repo.clearVerticalSlices();
|
|
162
|
+
const unsupported = JSON.parse(repo.getMeta("unsupported_files") || "[]") as string[];
|
|
163
|
+
repo.setMeta("unsupported_files", JSON.stringify(unsupported.filter(path => path !== relPath))); });
|
|
164
|
+
} else {
|
|
165
|
+
await pipeline.ingestFile(target, config);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
170
|
+
try { await flushing; } finally { flushing = null; }
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const usePolling = (error: unknown) => {
|
|
174
|
+
watcher?.close();
|
|
175
|
+
watcher = null;
|
|
176
|
+
repo.setMeta("watcher_mode", "polling");
|
|
177
|
+
console.error(`[Septum Watcher Warning] Using polling: ${String(error)}`);
|
|
178
|
+
if (!polling) polling = setInterval(() => {
|
|
179
|
+
void flush().catch(failure => repo.setMeta("sync_error", String(failure)));
|
|
180
|
+
}, 1000);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
watcher = watch(root, { recursive: true }, (_event, filename) => {
|
|
185
|
+
if (!filename || closed) return;
|
|
186
|
+
const norm = filename.replace(/\\/g, "/");
|
|
187
|
+
if (norm.split("/").some(segment => ignored.has(segment))) return;
|
|
188
|
+
const target = resolve(root, norm);
|
|
189
|
+
let directory = false;
|
|
190
|
+
try { directory = statSync(target).isDirectory(); } catch { /* The path may have been removed before the event arrives. */ }
|
|
191
|
+
if (directory || (!ASTParserEngine.isSourceFile(norm) && (repo.getAllFiles().some(file => file.path.startsWith(norm + "/")) || (JSON.parse(repo.getMeta("unsupported_files") || "[]") as string[]).some(file => file.startsWith(norm + "/"))))) {
|
|
192
|
+
needsFullSync = true;
|
|
193
|
+
} else if (ASTParserEngine.isSourceFile(norm)) {
|
|
194
|
+
pending.add(norm);
|
|
195
|
+
} else {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (timer) clearTimeout(timer);
|
|
199
|
+
timer = setTimeout(() => {
|
|
200
|
+
void flush().catch(error => {
|
|
201
|
+
repo.setMeta("sync_error", String(error));
|
|
202
|
+
console.error(`[Septum Watcher Warning] ${String(error)}`);
|
|
203
|
+
});
|
|
204
|
+
}, 300);
|
|
205
|
+
});
|
|
206
|
+
repo.setMeta("watcher_mode", "native");
|
|
207
|
+
watcher.on("error", usePolling);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
usePolling(error);
|
|
210
|
+
}
|
|
211
|
+
return { flush, close: () => { closed = true; if (timer) clearTimeout(timer); if (polling) clearInterval(polling); watcher?.close(); } };
|
|
212
|
+
}
|
|
213
|
+
|
|
130
214
|
function getWorkspaceContext(pathHint?: string): WorkspaceContext {
|
|
131
215
|
const root = resolveWorkspaceRoot(pathHint);
|
|
132
216
|
let ctx = workspaceContextCache.get(root);
|
|
@@ -142,13 +226,16 @@ function getWorkspaceContext(pathHint?: string): WorkspaceContext {
|
|
|
142
226
|
const evaluator = new BoundaryEvaluator(repo, new ModuleResolver(root, repo));
|
|
143
227
|
const pipeline = new IngestionPipeline(repo, root);
|
|
144
228
|
|
|
145
|
-
|
|
229
|
+
const ready = pipeline.run(config, { verifyContent: true }).then(() => {});
|
|
230
|
+
const watcher = createWorkspaceWatcher(root, repo, pipeline, ready);
|
|
231
|
+
ctx = { root, config, db, repo, evaluator, pipeline, ready, watcher };
|
|
146
232
|
workspaceContextCache.set(root, ctx);
|
|
147
233
|
return ctx;
|
|
148
234
|
}
|
|
149
235
|
|
|
150
236
|
export async function runMCPServer(): Promise<void> {
|
|
151
237
|
const initialCtx = getWorkspaceContext();
|
|
238
|
+
await initialCtx.ready;
|
|
152
239
|
|
|
153
240
|
const server = new Server(
|
|
154
241
|
{
|
|
@@ -571,6 +658,11 @@ export async function runMCPServer(): Promise<void> {
|
|
|
571
658
|
const startTime = performance.now();
|
|
572
659
|
const pathHint = extractPathHint(args as Record<string, any>);
|
|
573
660
|
const ctx = getWorkspaceContext(pathHint);
|
|
661
|
+
await ctx.ready;
|
|
662
|
+
await ctx.watcher.flush();
|
|
663
|
+
if (ctx.repo.getMeta("sync_error")) throw new Error(`Workspace index synchronization failed: ${ctx.repo.getMeta("sync_error")}`);
|
|
664
|
+
ctx.config = ConfigLoader.load(undefined, ctx.root);
|
|
665
|
+
ctx.evaluator.getResolver().setRepository(ctx.repo);
|
|
574
666
|
|
|
575
667
|
const activeSession = SessionManager.getActiveSession(ctx.root);
|
|
576
668
|
const sessionPayload = activeSession
|
|
@@ -772,7 +864,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
772
864
|
metrics: { bytes_out: bytesOut, lines_out: linesOut },
|
|
773
865
|
session: sessionPayload,
|
|
774
866
|
status: (response as { isError?: boolean } | undefined)?.isError ? "error" : "success",
|
|
775
|
-
});
|
|
867
|
+
}, ctx.root);
|
|
776
868
|
|
|
777
869
|
return response;
|
|
778
870
|
} catch (err) {
|
|
@@ -790,7 +882,7 @@ export async function runMCPServer(): Promise<void> {
|
|
|
790
882
|
message: errorInstance.message,
|
|
791
883
|
stack: errorInstance.stack,
|
|
792
884
|
},
|
|
793
|
-
});
|
|
885
|
+
}, ctx.root);
|
|
794
886
|
|
|
795
887
|
return {
|
|
796
888
|
isError: true,
|
|
@@ -806,84 +898,10 @@ export async function runMCPServer(): Promise<void> {
|
|
|
806
898
|
|
|
807
899
|
const transport = new StdioServerTransport();
|
|
808
900
|
|
|
809
|
-
// In-process transparent background watcher for auto-syncing codebase changes
|
|
810
|
-
let fsWatcher: FSWatcher | null = null;
|
|
811
|
-
let debounceTimer: NodeJS.Timeout | null = null;
|
|
812
|
-
const SUPPORTED_EXTENSIONS = new Set([".ts", ".js", ".tsx", ".jsx", ".php", ".py", ".go"]);
|
|
813
|
-
const IGNORED_SEGMENTS = new Set([
|
|
814
|
-
"node_modules",
|
|
815
|
-
".git",
|
|
816
|
-
".septum",
|
|
817
|
-
".strata",
|
|
818
|
-
"dist",
|
|
819
|
-
".output",
|
|
820
|
-
".nuxt",
|
|
821
|
-
".next",
|
|
822
|
-
".cache",
|
|
823
|
-
"coverage",
|
|
824
|
-
".turbo",
|
|
825
|
-
]);
|
|
826
|
-
|
|
827
|
-
try {
|
|
828
|
-
const bgPipeline = initialCtx.pipeline;
|
|
829
|
-
const watchRoot = initialCtx.root;
|
|
830
|
-
|
|
831
|
-
fsWatcher = watch(watchRoot, { recursive: true }, (_event, filename) => {
|
|
832
|
-
if (!filename) return;
|
|
833
|
-
|
|
834
|
-
const norm = filename.replace(/\\/g, "/");
|
|
835
|
-
const parts = norm.split("/");
|
|
836
|
-
if (parts.some((p) => IGNORED_SEGMENTS.has(p))) return;
|
|
837
|
-
|
|
838
|
-
const ext = extname(norm).toLowerCase();
|
|
839
|
-
if (!SUPPORTED_EXTENSIONS.has(ext)) return;
|
|
840
|
-
|
|
841
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
842
|
-
debounceTimer = setTimeout(async () => {
|
|
843
|
-
try {
|
|
844
|
-
const freshConfig = ConfigLoader.load(undefined, watchRoot);
|
|
845
|
-
const targetPath = resolve(watchRoot, filename);
|
|
846
|
-
const relPath = relative(watchRoot, targetPath).replace(/\\/g, "/");
|
|
847
|
-
if (!existsSync(targetPath)) {
|
|
848
|
-
if (typeof (initialCtx.repo as any).deleteFileByPath === "function") {
|
|
849
|
-
(initialCtx.repo as any).deleteFileByPath(relPath);
|
|
850
|
-
} else if (typeof initialCtx.repo.deleteFile === "function") {
|
|
851
|
-
initialCtx.repo.deleteFile(relPath);
|
|
852
|
-
}
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
const updated = await bgPipeline.ingestFile(targetPath, freshConfig);
|
|
856
|
-
if (updated) {
|
|
857
|
-
console.error(
|
|
858
|
-
`[Septum Watcher] Auto-synced modified file '${filename}' via incremental delta sync.`
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
} catch (err: unknown) {
|
|
862
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
863
|
-
console.error(`[Septum Watcher Warning] Background auto-sync failed: ${msg}`);
|
|
864
|
-
}
|
|
865
|
-
}, 300);
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
fsWatcher.on("error", (err: Error) => {
|
|
869
|
-
console.error(`[Septum Watcher] File watcher error: ${err.message}`);
|
|
870
|
-
});
|
|
871
|
-
} catch (err: unknown) {
|
|
872
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
873
|
-
console.error(`[Septum Watcher Warning] Could not start filesystem watcher: ${msg}`);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
901
|
const cleanup = () => {
|
|
877
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
878
|
-
if (fsWatcher) {
|
|
879
|
-
try {
|
|
880
|
-
fsWatcher.close();
|
|
881
|
-
} catch {
|
|
882
|
-
// Ignore watcher close errors during shutdown
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
902
|
for (const ctx of workspaceContextCache.values()) {
|
|
886
903
|
try {
|
|
904
|
+
ctx.watcher.close();
|
|
887
905
|
ctx.db.close();
|
|
888
906
|
} catch (err) {
|
|
889
907
|
// Ignore close error during shutdown
|
|
@@ -896,16 +914,9 @@ export async function runMCPServer(): Promise<void> {
|
|
|
896
914
|
process.on("SIGINT", cleanup);
|
|
897
915
|
process.on("SIGTERM", cleanup);
|
|
898
916
|
process.on("beforeExit", () => {
|
|
899
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
900
|
-
if (fsWatcher) {
|
|
901
|
-
try {
|
|
902
|
-
fsWatcher.close();
|
|
903
|
-
} catch {
|
|
904
|
-
// Ignore watcher close errors
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
917
|
for (const ctx of workspaceContextCache.values()) {
|
|
908
918
|
try {
|
|
919
|
+
ctx.watcher.close();
|
|
909
920
|
ctx.db.close();
|
|
910
921
|
} catch {
|
|
911
922
|
// Ignore close error
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { auditStagedFiles } from "../../core/boundary/staged-auditor.ts";
|
|
2
3
|
import type { BoundaryEvaluator } from "../../core/boundary/evaluator.ts";
|
|
3
4
|
import type { ValidatedSeptumConfig } from "../../core/config/schema.ts";
|
|
4
5
|
import type { SeptumRepository } from "../../core/database/repository.ts";
|
|
@@ -7,7 +8,7 @@ import { normalizeBoundaryArgs, type CheckBoundaryArgs } from "../schemas.ts";
|
|
|
7
8
|
|
|
8
9
|
export type { CheckBoundaryArgs } from "../schemas.ts";
|
|
9
10
|
|
|
10
|
-
export function handleCheckBoundary(
|
|
11
|
+
export async function handleCheckBoundary(
|
|
11
12
|
evaluator: BoundaryEvaluator,
|
|
12
13
|
config: ValidatedSeptumConfig,
|
|
13
14
|
rawArgs: CheckBoundaryArgs & Record<string, unknown>,
|
|
@@ -19,6 +20,17 @@ export function handleCheckBoundary(
|
|
|
19
20
|
const proposedImports = args.proposed_imports ?? [];
|
|
20
21
|
const featureKey = args.feature_key;
|
|
21
22
|
|
|
23
|
+
const unsupported = JSON.parse(repo?.getMeta("unsupported_files") || "[]") as string[];
|
|
24
|
+
const requested = [...(Array.isArray(args.file_path) ? args.file_path : args.file_path ? [args.file_path] : []), ...(args.file_paths || [])]
|
|
25
|
+
.map(file => relative(root, resolve(root, file)).replace(/\\/g, "/"));
|
|
26
|
+
const relevantUnsupported = unsupported.filter(file =>
|
|
27
|
+
(!args.domain || evaluator.resolveSourceDomain(file, config) === args.domain) &&
|
|
28
|
+
(requested.length === 0 || requested.includes(file))
|
|
29
|
+
);
|
|
30
|
+
if (relevantUnsupported.length > 0 && args.scope !== "staged" && !args.staged) {
|
|
31
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ status: "unknown", unsupported_files: relevantUnsupported, message: "Boundary audit is incomplete because source files have no supported parser." }) }] };
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const filesToCheck: string[] = [];
|
|
23
35
|
if (args.file_paths && Array.isArray(args.file_paths) && args.file_paths.length > 0) {
|
|
24
36
|
filesToCheck.push(...args.file_paths);
|
|
@@ -36,20 +48,7 @@ export function handleCheckBoundary(
|
|
|
36
48
|
args.scope ?? (args.staged ? "staged" : args.domain ? "domain" : "all");
|
|
37
49
|
|
|
38
50
|
if (effectiveScope === "staged") {
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const gitOut = execSync("git diff --name-only --cached", {
|
|
42
|
-
cwd: root,
|
|
43
|
-
encoding: "utf-8",
|
|
44
|
-
timeout: 5000,
|
|
45
|
-
});
|
|
46
|
-
stagedFiles = gitOut
|
|
47
|
-
.split("\n")
|
|
48
|
-
.map((f) => f.trim())
|
|
49
|
-
.filter(Boolean);
|
|
50
|
-
} catch {
|
|
51
|
-
stagedFiles = [];
|
|
52
|
-
}
|
|
51
|
+
const { files: stagedFiles, violations: stagedViolations } = await auditStagedFiles(evaluator, config, root, featureKey, repo);
|
|
53
52
|
|
|
54
53
|
if (stagedFiles.length === 0) {
|
|
55
54
|
return {
|
|
@@ -75,23 +74,6 @@ export function handleCheckBoundary(
|
|
|
75
74
|
};
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
const codebaseViolations = evaluator.auditCodebase(config);
|
|
79
|
-
const stagedNormalized = new Set(
|
|
80
|
-
stagedFiles.map((f) => f.replace(/\\/g, "/").replace(/^\.\//, ""))
|
|
81
|
-
);
|
|
82
|
-
const stagedViolations: BoundaryViolation[] = [];
|
|
83
|
-
|
|
84
|
-
for (const filePath of stagedFiles) {
|
|
85
|
-
const evalViolations = evaluator.evaluate(filePath, proposedImports, featureKey, config);
|
|
86
|
-
stagedViolations.push(...evalViolations);
|
|
87
|
-
}
|
|
88
|
-
for (const v of codebaseViolations) {
|
|
89
|
-
const normFile = v.file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
90
|
-
if (stagedNormalized.has(normFile)) {
|
|
91
|
-
stagedViolations.push(v);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
77
|
const filteredViolations = args.domain
|
|
96
78
|
? stagedViolations.filter(
|
|
97
79
|
(v) =>
|
|
@@ -15,6 +15,10 @@ export function handleGetDomainCatalog(
|
|
|
15
15
|
) {
|
|
16
16
|
const targetRoot = args.workspace_path || workspaceRoot || (repo as any)?.projectRoot || process.cwd();
|
|
17
17
|
const domainName = args.domain ?? args.name;
|
|
18
|
+
const indexStatus = {
|
|
19
|
+
unsupported_files: JSON.parse(repo.getMeta("unsupported_files") || "[]"),
|
|
20
|
+
watcher_mode: repo.getMeta("watcher_mode") || "manual",
|
|
21
|
+
};
|
|
18
22
|
|
|
19
23
|
if (!domainName) {
|
|
20
24
|
const allDomains = repo.getAllDomains();
|
|
@@ -55,6 +59,7 @@ export function handleGetDomainCatalog(
|
|
|
55
59
|
: repo.getAllVerticalSlices().length;
|
|
56
60
|
|
|
57
61
|
const macroMap = {
|
|
62
|
+
index_status: indexStatus,
|
|
58
63
|
telescope_view: {
|
|
59
64
|
project_type: projectType,
|
|
60
65
|
framework,
|
|
@@ -129,7 +134,7 @@ export function handleGetDomainCatalog(
|
|
|
129
134
|
content: [
|
|
130
135
|
{
|
|
131
136
|
type: "text" as const,
|
|
132
|
-
text: JSON.stringify(catalog, null, 2),
|
|
137
|
+
text: JSON.stringify({ ...catalog, index_status: indexStatus }, null, 2),
|
|
133
138
|
},
|
|
134
139
|
],
|
|
135
140
|
};
|
|
@@ -100,18 +100,41 @@ export async function handleLocateSymbol(
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
const exactLineCount = result.exact_symbol
|
|
104
|
+
? result.exact_symbol.line_end - result.exact_symbol.line_start + 1
|
|
105
|
+
: 0;
|
|
106
|
+
const complexitySmell =
|
|
107
|
+
exactLineCount > 300
|
|
108
|
+
? {
|
|
109
|
+
is_high_complexity: true,
|
|
110
|
+
threshold: 300,
|
|
111
|
+
total_lines: exactLineCount,
|
|
112
|
+
warning: `Symbol '${result.exact_symbol?.name}' spans ${exactLineCount} lines (exceeds 300-line threshold). Candidate for decomposition/refactoring.`,
|
|
113
|
+
}
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
103
116
|
const suggestions = (result.suggestions || []).slice(0, limit);
|
|
104
|
-
|
|
117
|
+
let summary = result.found
|
|
105
118
|
? `[FOUND] ${result.exact_symbol?.name || query} in ${result.file_path} (lines ${result.exact_symbol?.line_start}-${result.exact_symbol?.line_end})`
|
|
106
119
|
: `[NOT FOUND] Symbol '${query}' not found. ${suggestions.length > 0 ? `Top suggestions: ${suggestions.map((s) => s.name).slice(0, 3).join(", ")}` : "No close suggestions."}`;
|
|
107
120
|
|
|
121
|
+
if (complexitySmell) {
|
|
122
|
+
summary += ` | ⚠️ High-Complexity Smell: spans ${exactLineCount} lines (> 300L)`;
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
const payload = {
|
|
109
126
|
status: result.found ? "success" : "not_found",
|
|
110
127
|
found: result.found,
|
|
111
128
|
query,
|
|
112
129
|
file_path: result.file_path || null,
|
|
113
130
|
container_name: result.container_name || null,
|
|
114
|
-
exact_symbol: result.exact_symbol
|
|
131
|
+
exact_symbol: result.exact_symbol
|
|
132
|
+
? {
|
|
133
|
+
...result.exact_symbol,
|
|
134
|
+
total_lines: exactLineCount,
|
|
135
|
+
}
|
|
136
|
+
: null,
|
|
137
|
+
complexity_smell: complexitySmell,
|
|
115
138
|
sibling_methods: result.sibling_methods || [],
|
|
116
139
|
suggestions,
|
|
117
140
|
summary,
|
|
@@ -41,6 +41,14 @@ export async function handleRegisterFeature(
|
|
|
41
41
|
output_contract: args.output_contract || {},
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
// Record Active Feature Session Lease (.septum/session.json)
|
|
45
|
+
SessionManager.saveActiveSession(workspaceRoot, {
|
|
46
|
+
feature_key: featureKey,
|
|
47
|
+
domain,
|
|
48
|
+
touchpoints: featureConfig.allowed_touchpoints,
|
|
49
|
+
locked_at: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
|
|
44
52
|
if (!config.features) {
|
|
45
53
|
config.features = {};
|
|
46
54
|
}
|
|
@@ -59,13 +67,7 @@ export async function handleRegisterFeature(
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
SessionManager.saveActiveSession(workspaceRoot, {
|
|
64
|
-
feature_key: featureKey,
|
|
65
|
-
domain,
|
|
66
|
-
touchpoints: featureConfig.allowed_touchpoints,
|
|
67
|
-
locked_at: new Date().toISOString(),
|
|
68
|
-
});
|
|
70
|
+
|
|
69
71
|
|
|
70
72
|
return {
|
|
71
73
|
content: [
|
package/src/types/index.ts
CHANGED
|
@@ -129,6 +129,7 @@ export interface FileRecord {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export interface ActiveFeatureSession {
|
|
132
|
+
owner_id?: string;
|
|
132
133
|
feature_key: string;
|
|
133
134
|
domain: string;
|
|
134
135
|
touchpoints: string[];
|
|
@@ -177,6 +178,7 @@ export interface ExtractedDependency {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
export interface ParsedFileAST {
|
|
181
|
+
status?: "parsed" | "unsupported" | "degraded";
|
|
180
182
|
symbols: ExtractedSymbol[];
|
|
181
183
|
dependencies: ExtractedDependency[];
|
|
182
184
|
}
|
|
@@ -324,6 +326,21 @@ export interface SymbolExactRange {
|
|
|
324
326
|
total_lines: number;
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
export interface ResolvedOutboundCall {
|
|
330
|
+
symbol: string;
|
|
331
|
+
target_file?: string;
|
|
332
|
+
target_range?: SymbolExactRange;
|
|
333
|
+
domain?: string;
|
|
334
|
+
is_resolved: boolean;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface SymbolComplexitySmell {
|
|
338
|
+
is_high_complexity: boolean;
|
|
339
|
+
threshold: number;
|
|
340
|
+
total_lines: number;
|
|
341
|
+
warning: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
327
344
|
export interface GetSymbolDetailsResponse {
|
|
328
345
|
symbol: string;
|
|
329
346
|
kind: SymbolKind | string;
|
|
@@ -334,6 +351,8 @@ export interface GetSymbolDetailsResponse {
|
|
|
334
351
|
visibility?: Visibility | string;
|
|
335
352
|
dependencies_injected: string[];
|
|
336
353
|
outbound_calls: string[];
|
|
354
|
+
resolved_outbound_calls?: ResolvedOutboundCall[];
|
|
355
|
+
complexity_smell?: SymbolComplexitySmell;
|
|
337
356
|
sibling_methods?: string[];
|
|
338
357
|
connected_files?: FileConnectionsSummary;
|
|
339
358
|
}
|
|
@@ -358,7 +377,7 @@ export interface GetSymbolImpactResponse {
|
|
|
358
377
|
warning?: string;
|
|
359
378
|
impact_summary: {
|
|
360
379
|
total_dependents: number;
|
|
361
|
-
risk_level: "low" | "medium" | "high";
|
|
380
|
+
risk_level: "low" | "medium" | "high" | "unknown";
|
|
362
381
|
affected_files_count: number;
|
|
363
382
|
};
|
|
364
383
|
inbound_callers: InboundCaller[];
|