septum 0.1.6 → 0.1.8

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 (72) hide show
  1. package/README.md +38 -37
  2. package/package.json +6 -5
  3. package/src/cli/commands/check.ts +34 -122
  4. package/src/cli/commands/hook.ts +68 -16
  5. package/src/cli/commands/init.ts +440 -121
  6. package/src/cli/commands/sync.ts +27 -17
  7. package/src/cli/commands/topology.ts +89 -47
  8. package/src/cli/index.ts +7 -68
  9. package/src/cli/scaffold/harness-config.ts +697 -0
  10. package/src/cli/templates/hooks.ts +6 -6
  11. package/src/cli/templates/rules.ts +8 -8
  12. package/src/cli/templates/skill.ts +11 -18
  13. package/src/cli/ui/prompts.ts +122 -0
  14. package/src/core/boundary/evaluator.ts +21 -25
  15. package/src/core/boundary/staged-auditor.ts +50 -0
  16. package/src/core/config/loader.ts +21 -24
  17. package/src/core/database/client.ts +8 -0
  18. package/src/core/database/repositories/dependency.repository.ts +80 -16
  19. package/src/core/database/repositories/domain.repository.ts +86 -33
  20. package/src/core/database/repositories/symbol.repository.ts +147 -65
  21. package/src/core/database/repository.ts +17 -5
  22. package/src/core/database/schema.ts +1 -1
  23. package/src/core/discovery/topology-detector.ts +71 -19
  24. package/src/core/ingestion/pipeline.ts +274 -126
  25. package/src/core/parser/extractors/frontend.ts +1 -0
  26. package/src/core/parser/extractors/go.ts +2 -2
  27. package/src/core/parser/extractors/laravel-semantic.ts +9 -8
  28. package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
  29. package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
  30. package/src/core/parser/extractors/php.ts +2 -2
  31. package/src/core/parser/extractors/python.ts +2 -2
  32. package/src/core/parser/extractors/typescript.ts +2 -2
  33. package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
  34. package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
  35. package/src/core/parser/tree-sitter.ts +6 -1
  36. package/src/core/parser/wasm-assets.ts +17 -0
  37. package/src/core/parser/wasm-loader.ts +3 -0
  38. package/src/core/resolver/call-graph-tracer.ts +4 -3
  39. package/src/core/resolver/module-resolver.ts +1 -10
  40. package/src/core/resolver/path-resolver.ts +57 -4
  41. package/src/core/resolver/vertical-slice-tracer.ts +1 -0
  42. package/src/core/resources/ignore.ts +35 -0
  43. package/src/core/resources/limits.ts +14 -0
  44. package/src/core/resources/source-files.ts +62 -0
  45. package/src/core/resources/work-queue.ts +74 -0
  46. package/src/core/session/session-manager.ts +67 -38
  47. package/src/core/telemetry/telemetry.ts +12 -1
  48. package/src/core/topology/environment-detector.ts +21 -11
  49. package/src/mcp/schemas.ts +381 -8
  50. package/src/mcp/server.ts +326 -529
  51. package/src/mcp/tools/boundary.ts +17 -0
  52. package/src/mcp/tools/check-boundary.ts +97 -43
  53. package/src/mcp/tools/domain.ts +40 -0
  54. package/src/mcp/tools/feature.ts +67 -0
  55. package/src/mcp/tools/get-domain-catalog.ts +15 -7
  56. package/src/mcp/tools/get-environment-topology.ts +2 -2
  57. package/src/mcp/tools/get-feature-context.ts +9 -12
  58. package/src/mcp/tools/get-symbol-impact.ts +5 -2
  59. package/src/mcp/tools/get-symbol.ts +2 -1
  60. package/src/mcp/tools/locate-symbol.ts +28 -5
  61. package/src/mcp/tools/register-domain.ts +25 -9
  62. package/src/mcp/tools/register-feature.ts +40 -21
  63. package/src/mcp/tools/slice.ts +15 -0
  64. package/src/mcp/tools/symbol.ts +82 -0
  65. package/src/mcp/workspace-manager.ts +159 -0
  66. package/src/mcp/workspace-watcher.ts +202 -0
  67. package/src/types/index.ts +21 -1
  68. package/src/types/wasm.d.ts +4 -0
  69. package/src/cli/commands/ingest.ts +0 -29
  70. package/src/cli/commands/locate.ts +0 -61
  71. package/src/cli/commands/query.ts +0 -104
  72. package/src/cli/commands/slice.ts +0 -50
@@ -1,3 +1,4 @@
1
+ import { readOptionalSourceFile } from "../../resources/source-files.ts";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import type { ExecutionChainNode, VerticalSliceRecord } from "../../../types/index.ts";
@@ -62,7 +63,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
62
63
  const parsedRoutes: ParsedRoute[] = [];
63
64
 
64
65
  for (const routeFile of routeFiles) {
65
- const content = fs.readFileSync(routeFile, "utf-8");
66
+ const content = readOptionalSourceFile(routeFile, "utf-8");
66
67
  parsedRoutes.push(...this.parseRoutesContent(content));
67
68
  }
68
69
 
@@ -97,7 +98,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
97
98
 
98
99
  let behavioralInfo: import("../behavioral-analyzer.ts").MethodBehavioralSummary | null = null;
99
100
  if (controllerInfo.filePath && fs.existsSync(controllerInfo.filePath)) {
100
- const controllerCode = fs.readFileSync(controllerInfo.filePath, "utf-8");
101
+ const controllerCode = readOptionalSourceFile(controllerInfo.filePath, "utf-8");
101
102
  const actionBody = this.extractActionBody(controllerCode, route.actionName);
102
103
  behavioralInfo = BehavioralAnalyzer.analyzeMethod(actionBody);
103
104
 
@@ -105,7 +106,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
105
106
  const requestClassName = this.detectFormRequestClass(controllerCode, route.actionName);
106
107
  if (requestClassName) {
107
108
  const reqFile = this.findClassFile("Requests", requestClassName);
108
- const rules = reqFile && fs.existsSync(reqFile) ? this.extractValidationRules(fs.readFileSync(reqFile, "utf-8")) : {};
109
+ const rules = reqFile && fs.existsSync(reqFile) ? this.extractValidationRules(readOptionalSourceFile(reqFile, "utf-8")) : {};
109
110
  formRequestInfo = {
110
111
  className: requestClassName,
111
112
  filePath: reqFile ? path.relative(this.projectRoot, reqFile) : undefined,
@@ -117,7 +118,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
117
118
  const modelClassName = this.detectModelClass(controllerCode, route.actionName, actionBody);
118
119
  if (modelClassName) {
119
120
  const modFile = this.findClassFile("Models", modelClassName);
120
- const modMetadata = modFile && fs.existsSync(modFile) ? this.extractModelMetadata(fs.readFileSync(modFile, "utf-8")) : { fillable: [], casts: {} };
121
+ const modMetadata = modFile && fs.existsSync(modFile) ? this.extractModelMetadata(readOptionalSourceFile(modFile, "utf-8")) : { fillable: [], casts: {} };
121
122
  modelInfo = {
122
123
  className: modelClassName,
123
124
  filePath: modFile ? path.relative(this.projectRoot, modFile) : undefined,
@@ -509,7 +510,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
509
510
 
510
511
  for (const c of candidates) {
511
512
  if (fs.existsSync(c)) {
512
- const lines = fs.readFileSync(c, "utf-8").split("\n");
513
+ const lines = readOptionalSourceFile(c, "utf-8").split("\n");
513
514
  let targetLine = 1;
514
515
  for (let i = 0; i < lines.length; i++) {
515
516
  if (new RegExp(`function\\s+${actionName}\\s*\\(`, "i").test(lines[i])) {
@@ -814,7 +815,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
814
815
 
815
816
  for (const file of jobFiles) {
816
817
  const fullPath = path.join(jobsDir, file);
817
- const content = fs.readFileSync(fullPath, "utf-8");
818
+ const content = readOptionalSourceFile(fullPath, "utf-8");
818
819
  const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
819
820
  if (!classMatch) continue;
820
821
 
@@ -890,7 +891,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
890
891
 
891
892
  for (const file of files) {
892
893
  const fullPath = path.join(commandsDir, file);
893
- const content = fs.readFileSync(fullPath, "utf-8");
894
+ const content = readOptionalSourceFile(fullPath, "utf-8");
894
895
  const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
895
896
  if (!classMatch) continue;
896
897
 
@@ -969,7 +970,7 @@ export class LaravelSemanticExtractor implements SemanticSliceExtractor {
969
970
 
970
971
  for (const file of files) {
971
972
  const fullPath = path.join(listenersDir, file);
972
- const content = fs.readFileSync(fullPath, "utf-8");
973
+ const content = readOptionalSourceFile(fullPath, "utf-8");
973
974
  const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
974
975
  if (!classMatch) continue;
975
976
 
@@ -1,5 +1,7 @@
1
+ import { readOptionalSourceFile } from "../../resources/source-files.ts";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
4
+ import { blankCommentsAndStrings } from "../boundary-tracker.ts";
3
5
  import type { SemanticSliceExtractor, VerticalSliceCandidate } from "./semantic-extractor.interface.ts";
4
6
 
5
7
  export class McpCliSemanticExtractor implements SemanticSliceExtractor {
@@ -18,7 +20,7 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
18
20
  const pkgPath = path.join(projectRoot, "package.json");
19
21
  if (fs.existsSync(pkgPath)) {
20
22
  try {
21
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
23
+ const pkg = JSON.parse(readOptionalSourceFile(pkgPath, "utf-8"));
22
24
  if (pkg.bin || pkg.dependencies?.["@modelcontextprotocol/sdk"]) {
23
25
  return true;
24
26
  }
@@ -44,11 +46,13 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
44
46
  for (const file of toolFiles) {
45
47
  const fullPath = path.join(mcpToolsDir, file);
46
48
  const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, "/");
47
- const content = fs.readFileSync(fullPath, "utf-8");
49
+ const content = readOptionalSourceFile(fullPath, "utf-8");
48
50
  const baseName = path.basename(file, path.extname(file));
49
51
  const snakeBase = baseName.replace(/-/g, "_");
50
- const toolIdentifier =
51
- registeredTools.get(snakeBase) || `${toolPrefix}_${snakeBase}`;
52
+
53
+ // MCP-07: Only create ingress slices for whitelisted public MCP tools
54
+ const toolIdentifier = registeredTools.get(snakeBase) || registeredTools.get(`${toolPrefix}_${snakeBase}`);
55
+ if (!toolIdentifier) continue;
52
56
 
53
57
  // Detect handler function (both sync and async) and its 1-indexed line number
54
58
  const handlerMatch = /export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/.exec(content);
@@ -143,7 +147,7 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
143
147
  for (const file of commandFiles) {
144
148
  const fullPath = path.join(cliCommandsDir, file);
145
149
  const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, "/");
146
- const content = fs.readFileSync(fullPath, "utf-8");
150
+ const content = readOptionalSourceFile(fullPath, "utf-8");
147
151
  const commandName = path.basename(file, path.extname(file));
148
152
 
149
153
  const handlerMatch = /export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/.exec(content);
@@ -235,7 +239,7 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
235
239
  const pkgPath = path.join(projectRoot, "package.json");
236
240
  if (fs.existsSync(pkgPath)) {
237
241
  try {
238
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
242
+ const pkg = JSON.parse(readOptionalSourceFile(pkgPath, "utf-8"));
239
243
  const rawName = typeof pkg.name === "string" ? pkg.name.replace(/^@[^/]+\//, "") : "mcp";
240
244
  const toolPrefix = rawName.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase() || "mcp";
241
245
  let cliBinName = rawName;
@@ -257,17 +261,53 @@ export class McpCliSemanticExtractor implements SemanticSliceExtractor {
257
261
  if (!fs.existsSync(serverPath)) return map;
258
262
 
259
263
  try {
260
- const content = fs.readFileSync(serverPath, "utf-8");
261
- const nameRegex = /name:\s*['"]([a-zA-Z0-9_-]+)['"]/g;
262
- let match: RegExpExecArray | null;
263
- while ((match = nameRegex.exec(content)) !== null) {
264
- const fullToolName = match[1];
265
- const normalized = fullToolName.replace(/-/g, "_");
266
- map.set(normalized, fullToolName);
267
- const withoutPrefix = normalized.replace(/^[a-z0-9]+_/, "");
268
- if (withoutPrefix) {
269
- map.set(withoutPrefix, fullToolName);
264
+ const content = readOptionalSourceFile(serverPath, "utf-8");
265
+ const masked = blankCommentsAndStrings(content);
266
+ const scan = (start: number, namesInArray: boolean) => {
267
+ let depth = 0;
268
+ const names: string[] = [];
269
+ const property = /\b([A-Za-z_]\w*)\s*:\s*(['"])?/y;
270
+ for (let i = start; i < masked.length; i++) {
271
+ const ch = masked[i];
272
+ if ("[{(".includes(ch)) depth++;
273
+ else if ("]})".includes(ch)) {
274
+ if (--depth === 0) break;
275
+ }
276
+ if (depth !== (namesInArray ? 2 : 1) || (i > 0 && /\w/.test(masked[i - 1]))) continue;
277
+ property.lastIndex = i;
278
+ const match = property.exec(masked);
279
+ if (!match) continue;
280
+ if (!namesInArray) names.push(match[1]);
281
+ else if (match[1] === "name" && match[2]) {
282
+ const quoteStart = i + match[0].lastIndexOf(match[2]);
283
+ const quoteEnd = masked.indexOf(match[2], quoteStart + 1);
284
+ const name = content.slice(quoteStart + 1, quoteEnd);
285
+ if (/^[A-Za-z0-9_-]+$/.test(name)) names.push(name);
286
+ }
287
+ }
288
+ // Unbalanced declarations are not evidence of public registration.
289
+ if (depth !== 0) return;
290
+ for (const fullToolName of names) {
291
+ const normalized = fullToolName.replace(/-/g, "_");
292
+ map.set(normalized, fullToolName);
293
+ const withoutPrefix = normalized.replace(/^[a-z0-9]+_/, "");
294
+ if (withoutPrefix) map.set(withoutPrefix, fullToolName);
295
+ }
296
+ };
297
+ const listCall = /setRequestHandler\s*\(\s*ListToolsRequestSchema\b/.exec(masked);
298
+ if (listCall) {
299
+ const callStart = masked.indexOf("(", listCall.index);
300
+ let depth = 1;
301
+ let end = callStart + 1;
302
+ for (; end < masked.length && depth; end++) {
303
+ if (masked[end] === "(") depth++;
304
+ else if (masked[end] === ")") depth--;
270
305
  }
306
+ const tools = /\btools\s*:\s*\[/.exec(masked.slice(callStart, end));
307
+ if (tools && depth === 0) scan(callStart + tools.index + tools[0].length - 1, true);
308
+ } else {
309
+ const schemas = /\bTOOL_SCHEMAS\s*=\s*\{/.exec(masked);
310
+ if (schemas) scan(schemas.index + schemas[0].length - 1, false);
271
311
  }
272
312
  } catch {}
273
313
  return map;
@@ -1,3 +1,4 @@
1
+ import { readOptionalSourceFile } from "../../resources/source-files.ts";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import type { SemanticSliceExtractor, VerticalSliceCandidate } from "./semantic-extractor.interface.ts";
@@ -14,7 +15,7 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
14
15
  const pkgPath = path.join(projectRoot, "package.json");
15
16
  if (fs.existsSync(pkgPath)) {
16
17
  try {
17
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
18
+ const pkg = JSON.parse(readOptionalSourceFile(pkgPath, "utf-8"));
18
19
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
19
20
  if (deps["@nestjs/core"]) return true;
20
21
  } catch {}
@@ -32,7 +33,7 @@ export class NestJsSemanticExtractor implements SemanticSliceExtractor {
32
33
 
33
34
  for (const file of controllerFiles) {
34
35
  const relPath = path.relative(projectRoot, file).replace(/\\/g, "/");
35
- const content = fs.readFileSync(file, "utf-8");
36
+ const content = readOptionalSourceFile(file, "utf-8");
36
37
 
37
38
  // Extract @Controller('prefix')
38
39
  const controllerMatch = /@Controller\s*\(\s*['"]?([^'")]*)['"]?\s*\)[\s\S]*?export\s+class\s+([A-Za-z0-9_]+)/.exec(
@@ -44,10 +44,10 @@ export class PHPExtractor implements CodeExtractor {
44
44
  this.extractFromAst(rootNode, content)
45
45
  );
46
46
  if (astResult) {
47
- return astResult;
47
+ return { ...astResult, status: "parsed" };
48
48
  }
49
49
 
50
- return this.extractFallback(content);
50
+ return { ...this.extractFallback(content), status: "degraded" };
51
51
  }
52
52
 
53
53
  private isExternalNamespace(cleaned: string): boolean {
@@ -79,10 +79,10 @@ export class PythonExtractor implements CodeExtractor {
79
79
  this.extractFromAst(rootNode, content)
80
80
  );
81
81
  if (astResult) {
82
- return astResult;
82
+ return { ...astResult, status: "parsed" };
83
83
  }
84
84
 
85
- return this.extractFallback(content);
85
+ return { ...this.extractFallback(content), status: "degraded" };
86
86
  }
87
87
 
88
88
  private extractFallback(content: string): ParsedFileAST {
@@ -46,10 +46,10 @@ export class TypeScriptExtractor implements CodeExtractor {
46
46
  );
47
47
 
48
48
  if (treeSitterResult) {
49
- return treeSitterResult;
49
+ return { ...treeSitterResult, status: "parsed" };
50
50
  }
51
51
 
52
- return this.extractFallback(content);
52
+ return { ...this.extractFallback(content), status: "degraded" };
53
53
  }
54
54
 
55
55
  private resolveLanguageKey(filePath: string): string {
@@ -1,3 +1,4 @@
1
+ import { readOptionalSourceFile } from "../../../resources/source-files.ts";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import type {
@@ -35,7 +36,7 @@ export class LaravelMigrationSchemaProvider implements StackSchemaProvider {
35
36
  for (const migFile of migrationFiles) {
36
37
  const fullPath = path.join(migrationsDir, migFile);
37
38
  try {
38
- const content = fs.readFileSync(fullPath, "utf-8");
39
+ const content = readOptionalSourceFile(fullPath, "utf-8");
39
40
  this.extractColumnsFromMigration(content, tableName, fieldsMap);
40
41
  } catch {
41
42
  // Skip unreadable files
@@ -66,7 +67,7 @@ export class LaravelMigrationSchemaProvider implements StackSchemaProvider {
66
67
  : path.join(projectRoot, entityFilePath);
67
68
  if (fs.existsSync(fullEntityPath)) {
68
69
  try {
69
- const content = fs.readFileSync(fullEntityPath, "utf-8");
70
+ const content = readOptionalSourceFile(fullEntityPath, "utf-8");
70
71
  const tableMatch = /protected\s+\$table\s*=\s*['"]([^'"]+)['"]/.exec(content);
71
72
  if (tableMatch) {
72
73
  return tableMatch[1];
@@ -120,7 +121,7 @@ export class LaravelMigrationSchemaProvider implements StackSchemaProvider {
120
121
  if (matched.length === 0) {
121
122
  for (const f of files) {
122
123
  try {
123
- const content = fs.readFileSync(path.join(migrationsDir, f), "utf-8");
124
+ const content = readOptionalSourceFile(path.join(migrationsDir, f), "utf-8");
124
125
  if (
125
126
  content.includes(`Schema::create('${tableName}'`) ||
126
127
  content.includes(`Schema::create("${tableName}"`) ||
@@ -1,3 +1,4 @@
1
+ import { readOptionalSourceFile } from "../../../resources/source-files.ts";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import type {
@@ -38,7 +39,7 @@ export class PrismaSchemaProvider implements StackSchemaProvider {
38
39
  if (!schemaPath) return null;
39
40
 
40
41
  try {
41
- const content = fs.readFileSync(schemaPath, "utf-8");
42
+ const content = readOptionalSourceFile(schemaPath, "utf-8");
42
43
  const modelRegex = new RegExp(`model\\s+${entitySymbol}\\s*\\{([^}]+)\\}`, "m");
43
44
  const match = modelRegex.exec(content);
44
45
  if (!match) return null;
@@ -8,6 +8,9 @@ import { PythonExtractor } from "./extractors/python.ts";
8
8
  import { TypeScriptExtractor } from "./extractors/typescript.ts";
9
9
 
10
10
  export class ASTParserEngine {
11
+ public static isSourceFile(filePath: string): boolean {
12
+ return /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|php|py|go|rs|java|cs|rb|vue|astro|svelte)$/i.test(filePath);
13
+ }
11
14
  private extractors: CodeExtractor[];
12
15
 
13
16
  constructor() {
@@ -24,12 +27,14 @@ export class ASTParserEngine {
24
27
  public async parseFile(filePath: string, content: string): Promise<ParsedFileAST> {
25
28
  for (const extractor of this.extractors) {
26
29
  if (extractor.canHandle(filePath)) {
27
- return extractor.extract(filePath, content);
30
+ const result = await extractor.extract(filePath, content);
31
+ return { status: "parsed", ...result };
28
32
  }
29
33
  }
30
34
 
31
35
  // Default fallback extractor for unknown file types
32
36
  return {
37
+ status: "unsupported",
33
38
  symbols: [],
34
39
  dependencies: [],
35
40
  };
@@ -0,0 +1,17 @@
1
+ import runtime from "web-tree-sitter/tree-sitter.wasm" with { type: "file" };
2
+ import typescript from "tree-sitter-wasms/out/tree-sitter-typescript.wasm" with { type: "file" };
3
+ import tsx from "tree-sitter-wasms/out/tree-sitter-tsx.wasm" with { type: "file" };
4
+ import javascript from "tree-sitter-wasms/out/tree-sitter-javascript.wasm" with { type: "file" };
5
+ import php from "tree-sitter-wasms/out/tree-sitter-php.wasm" with { type: "file" };
6
+ import python from "tree-sitter-wasms/out/tree-sitter-python.wasm" with { type: "file" };
7
+ import go from "tree-sitter-wasms/out/tree-sitter-go.wasm" with { type: "file" };
8
+
9
+ export const WASM_ASSETS: Record<string, string> = {
10
+ "tree-sitter.wasm": runtime,
11
+ "tree-sitter-typescript.wasm": typescript,
12
+ "tree-sitter-tsx.wasm": tsx,
13
+ "tree-sitter-javascript.wasm": javascript,
14
+ "tree-sitter-php.wasm": php,
15
+ "tree-sitter-python.wasm": python,
16
+ "tree-sitter-go.wasm": go,
17
+ };
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import Parser from "web-tree-sitter";
5
+ import { WASM_ASSETS } from "./wasm-assets.ts";
5
6
 
6
7
  const GRAMMAR_BY_KEY: Record<string, string> = {
7
8
  // Extensions
@@ -74,6 +75,8 @@ export function resolveWasmPath(fileName: string, isCoreRuntime: boolean = false
74
75
  candidates.push(path.join(envDir, "out", fileName));
75
76
  }
76
77
 
78
+ if (WASM_ASSETS[fileName]) candidates.push(WASM_ASSETS[fileName]);
79
+
77
80
  // Tier 2: Walk up from import.meta.dir to package root
78
81
  let currentDir = getModuleDir();
79
82
  for (let depth = 0; depth < 7; depth++) {
@@ -1,3 +1,4 @@
1
+ import { readOptionalSourceFile } from "../resources/source-files.ts";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { dirname, join, normalize, relative, resolve } from "node:path";
3
4
  import type {
@@ -86,7 +87,7 @@ export class CallGraphTracer {
86
87
  if (!existsSync(fullPath)) continue;
87
88
 
88
89
  try {
89
- const content = readFileSync(fullPath, "utf-8");
90
+ const content = readOptionalSourceFile(fullPath, "utf-8");
90
91
  const lines = content.split("\n");
91
92
 
92
93
  for (let i = 0; i < lines.length; i++) {
@@ -414,7 +415,7 @@ export class CallGraphTracer {
414
415
  fileSymbols[0];
415
416
 
416
417
  const symbolDisplay = mainSymbol
417
- ? `${mainSymbol.name}${mainSymbol.kind === "class" ? "::execute" : ""}`
418
+ ? mainSymbol.name
418
419
  : resolvedFile.path.split("/").pop()?.replace(/\.[^.]+$/, "") || "Module";
419
420
 
420
421
  // Prevent adding duplicate stages if already present, or add as refinement
@@ -428,7 +429,7 @@ export class CallGraphTracer {
428
429
  symbol: symbolDisplay,
429
430
  file: resolvedFile.path,
430
431
  line: mainSymbol?.line_start || 1,
431
- description: `${stage.toUpperCase()}: ${symbolDisplay}`,
432
+ description: `${stage.toUpperCase()} (dependency): ${symbolDisplay}`,
432
433
  });
433
434
  }
434
435
 
@@ -97,16 +97,7 @@ export class ModuleResolver {
97
97
  filePath: string,
98
98
  config: ValidatedSeptumConfig
99
99
  ): string | null {
100
- const absPath = this.toAbsolutePath(filePath);
101
-
102
- for (const [domainName, domainCfg] of Object.entries(config.domains)) {
103
- const domainAbsRoot = this.toAbsolutePath(domainCfg.root);
104
- if (this.isSubPathOrSame(absPath, domainAbsRoot)) {
105
- return domainName;
106
- }
107
- }
108
-
109
- return null;
100
+ return this.matchDomainByPath(filePath, config);
110
101
  }
111
102
 
112
103
  /**
@@ -1,5 +1,5 @@
1
- import { existsSync, statSync } from "node:fs";
2
- import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
1
+ import { existsSync, statSync, realpathSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
3
3
 
4
4
  export const PROJECT_ROOT_MARKERS = [
5
5
  "package.json",
@@ -26,6 +26,7 @@ export function canonicalizePath(targetPath: string): string {
26
26
  return targetPath;
27
27
  }
28
28
  let abs = normalize(resolve(targetPath));
29
+ try { if (existsSync(abs)) abs = realpathSync(abs); } catch { /* Keep lexical paths for missing or inaccessible files. */ }
29
30
  if (process.platform === "win32") {
30
31
  abs = abs.replace(/^[a-zA-Z]:/, (m) => m.toUpperCase());
31
32
  }
@@ -104,7 +105,7 @@ export function findProjectRoot(fromPath: string): string {
104
105
  // Filesystem traversal error fallback
105
106
  }
106
107
 
107
- return canonicalizePath(dirname(resolve(fromPath)));
108
+ return canonicalizePath(existsSync(fromPath) && statSync(fromPath).isDirectory() ? fromPath : dirname(resolve(fromPath)));
108
109
  }
109
110
 
110
111
  /**
@@ -117,7 +118,8 @@ export function findProjectRoot(fromPath: string): string {
117
118
  export function resolveWorkspaceRoot(pathHint?: string): string {
118
119
  if (pathHint && typeof pathHint === "string" && pathHint.trim()) {
119
120
  const trimmed = pathHint.trim().replace(/^['"]|['"]$/g, "");
120
- const candidate = findProjectRoot(trimmed);
121
+ const absHint = resolve(trimmed);
122
+ const candidate = existsSync(absHint) && statSync(absHint).isDirectory() ? canonicalizePath(absHint) : findProjectRoot(trimmed);
121
123
  if (candidate && existsSync(candidate) && !isAppHostDirectory(candidate)) {
122
124
  setLastKnownProjectRoot(candidate);
123
125
  return candidate;
@@ -145,3 +147,54 @@ export function resolveWorkspaceRoot(pathHint?: string): string {
145
147
  // Fallback if inside app host directory: keep looking in lastKnown or return cwd
146
148
  return canonicalizePath(cwd);
147
149
  }
150
+
151
+ /**
152
+ * Verifies that candidatePath is strictly contained within workspaceRoot.
153
+ * Resolves symlinks (realpath) if the path exists, and checks ancestor realpath if not yet created.
154
+ */
155
+ export function isPathContained(candidatePath: string, workspaceRoot: string): boolean {
156
+ if (!candidatePath || !workspaceRoot) return false;
157
+ try {
158
+ const canonicalWorkspace = canonicalizePath(workspaceRoot);
159
+ const resolvedTarget = isAbsolute(candidatePath) ? normalize(candidatePath) : resolve(canonicalWorkspace, candidatePath);
160
+
161
+ // Lexical containment
162
+ const relLexical = relative(canonicalWorkspace, resolvedTarget).replace(/\\/g, "/");
163
+ if (relLexical === ".." || relLexical.startsWith("../") || isAbsolute(relLexical)) {
164
+ return false;
165
+ }
166
+
167
+ // Realpath / symlink containment
168
+ if (existsSync(resolvedTarget)) {
169
+ const realTarget = realpathSync(resolvedTarget);
170
+ const relReal = relative(canonicalWorkspace, realTarget).replace(/\\/g, "/");
171
+ if (relReal === ".." || relReal.startsWith("../") || isAbsolute(relReal)) {
172
+ return false;
173
+ }
174
+ } else {
175
+ let cur = dirname(resolvedTarget);
176
+ while (!existsSync(cur)) {
177
+ const parent = dirname(cur);
178
+ if (parent === cur) break;
179
+ cur = parent;
180
+ }
181
+ if (existsSync(cur)) {
182
+ const realCur = realpathSync(cur);
183
+ const relCur = relative(canonicalWorkspace, realCur).replace(/\\/g, "/");
184
+ if (relCur === ".." || relCur.startsWith("../") || isAbsolute(relCur)) {
185
+ return false;
186
+ }
187
+ }
188
+ }
189
+
190
+ return true;
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ export function assertPathContained(candidatePath: string, workspaceRoot: string, contextDescription = "Path"): void {
197
+ if (!isPathContained(candidatePath, workspaceRoot)) {
198
+ throw new Error(`${contextDescription} '${candidatePath}' escapes workspace boundary '${workspaceRoot}'.`);
199
+ }
200
+ }
@@ -81,6 +81,7 @@ export class VerticalSliceTracer {
81
81
  if (exact) {
82
82
  return this.formatSuccessResponse(trimmed, exact, "exact", undefined, effectiveMaxDepth);
83
83
  }
84
+ return { query: trimmed, found: false, max_depth_applied: effectiveMaxDepth, message: `No vertical slice found matching query '${trimmed}'.` };
84
85
  }
85
86
 
86
87
  // 2. URI-only match: "/orders" or "orders/{id}"
@@ -0,0 +1,35 @@
1
+ export function shouldIgnoreSourcePath(filePath: string, ignorePatterns: string[]): boolean {
2
+ if (filePath.replace(/\\/g, "/").split("/").some(segment => ["node_modules", "vendor", ".git", ".septum", ".strata", ".codegraph", "dist", "build", ".output", ".nuxt", ".next", ".cache", "coverage", ".turbo"].includes(segment))) return true;
3
+ const normPath = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
4
+ for (const pattern of ignorePatterns) {
5
+ const normPattern = pattern.replace(/\\/g, "/").replace(/^\.\//, "").trim();
6
+ if (!normPattern) continue;
7
+
8
+ if (normPattern === "*" || normPattern === "**") {
9
+ return true;
10
+ }
11
+
12
+ if (normPath === normPattern) return true;
13
+
14
+ if (normPattern.endsWith("/**")) {
15
+ const prefix = normPattern.slice(0, -3);
16
+ if (normPath === prefix || normPath.startsWith(prefix + "/")) return true;
17
+ } else if (normPattern.endsWith("/*")) {
18
+ const prefix = normPattern.slice(0, -2);
19
+ if (normPath === prefix || normPath.startsWith(prefix + "/")) return true;
20
+ } else if (normPath.startsWith(normPattern + "/")) {
21
+ return true;
22
+ }
23
+
24
+ if (normPattern.startsWith("*.") && normPath.endsWith(normPattern.slice(1))) {
25
+ return true;
26
+ }
27
+
28
+ const segments = normPath.split("/");
29
+ if (segments.includes(normPattern)) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+
@@ -0,0 +1,14 @@
1
+ export const RESOURCE_LIMITS = Object.freeze({
2
+ workspaces: 4,
3
+ workspaceIdleMs: 10 * 60_000,
4
+ watches: 2048,
5
+ pendingPaths: 1000,
6
+ debounceMs: 300,
7
+ pollMs: 5000,
8
+ verifyMs: 60_000,
9
+ sourceBytes: 2 * 1024 * 1024,
10
+ batchFiles: 50,
11
+ batchBytes: 1024 * 1024,
12
+ maxQueuePending: 50,
13
+ shutdownMs: 5000,
14
+ });
@@ -0,0 +1,62 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { closeSync, fstatSync, openSync, readSync, statSync } from 'node:fs';
3
+ import { relative } from 'node:path';
4
+ import { computeContentHash } from "../ingestion/hasher.ts";
5
+ import { RESOURCE_LIMITS } from './limits.ts';
6
+
7
+ export interface SkippedSourceFile { path: string; reason: 'file_too_large' }
8
+ export class SourceFileLimitError extends Error {
9
+ constructor(public readonly filePath: string) { super(`Source file exceeds ${RESOURCE_LIMITS.sourceBytes} bytes: ${filePath}`); }
10
+ }
11
+ export interface SourceReadFingerprint { path: string; hash: string; size: number; mtime: number }
12
+ const reads = new AsyncLocalStorage<{skipped: Map<string, SkippedSourceFile>; inputs: Map<string, SourceReadFingerprint>}>();
13
+
14
+ // Read at most the admission limit, even if the file grows after stat.
15
+ export function readSourceFile(filePath: string, _encoding?: string): string {
16
+ const fd = openSync(filePath, 'r');
17
+ try {
18
+ const size = fstatSync(fd).size;
19
+ if (size > RESOURCE_LIMITS.sourceBytes) throw new SourceFileLimitError(filePath);
20
+ let buffer = Buffer.allocUnsafe(Math.min(Math.max(size + 1, 4096), RESOURCE_LIMITS.sourceBytes + 1));
21
+ let count = 0;
22
+ while (true) {
23
+ const read = readSync(fd, buffer, count, buffer.length - count, null);
24
+ if (!read) return buffer.toString('utf8', 0, count);
25
+ count += read;
26
+ if (count > RESOURCE_LIMITS.sourceBytes) throw new SourceFileLimitError(filePath);
27
+ if (count === buffer.length) {
28
+ const expanded = Buffer.allocUnsafe(Math.min(buffer.length * 2, RESOURCE_LIMITS.sourceBytes + 1));
29
+ buffer.copy(expanded, 0, 0, count);
30
+ buffer = expanded;
31
+ }
32
+ }
33
+ } finally { closeSync(fd); }
34
+ }
35
+
36
+ export function readOptionalSourceFile(filePath: string, encoding?: string): string {
37
+ try {
38
+ const content = readSourceFile(filePath, encoding);
39
+ const collector = reads.getStore();
40
+ if (collector) {
41
+ const stat = statSync(filePath);
42
+ collector.inputs.set(filePath, {path:filePath,hash:computeContentHash(content),size:stat.size,mtime:stat.mtimeMs});
43
+ }
44
+ return content;
45
+ }
46
+ catch (error) {
47
+ if (!(error instanceof SourceFileLimitError)) throw error;
48
+ const collector = reads.getStore();
49
+ if (!collector) throw error;
50
+ collector.skipped.set(filePath, { path: filePath, reason: 'file_too_large' });
51
+ const stat = statSync(filePath);
52
+ collector.inputs.set(filePath, {path:filePath,hash:'file_too_large',size:stat.size,mtime:stat.mtimeMs});
53
+ return '';
54
+ }
55
+ }
56
+
57
+ export async function collectSourceSkips<T>(root: string, read: () => Promise<T>): Promise<{ value: T; skipped: SkippedSourceFile[]; inputs: SourceReadFingerprint[] }> {
58
+ const collector = {skipped: new Map<string, SkippedSourceFile>(), inputs: new Map<string, SourceReadFingerprint>()};
59
+ const value = await reads.run(collector, read);
60
+ const rel = (file: {path:string}) => relative(root, file.path).replace(/\\/g, '/');
61
+ return {value, skipped:[...collector.skipped.values()].map(file => ({...file,path:rel(file)})), inputs:[...collector.inputs.values()].map(file => ({...file,path:rel(file)}))};
62
+ }