septum 0.1.0
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 -0
- package/README.md +100 -0
- package/bin/septum.ts +4 -0
- package/package.json +62 -0
- package/src/cli/commands/check.ts +161 -0
- package/src/cli/commands/hook.ts +188 -0
- package/src/cli/commands/ingest.ts +29 -0
- package/src/cli/commands/init.ts +211 -0
- package/src/cli/commands/locate.ts +61 -0
- package/src/cli/commands/query.ts +64 -0
- package/src/cli/commands/serve.ts +5 -0
- package/src/cli/commands/slice.ts +90 -0
- package/src/cli/commands/sync.ts +31 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/templates/hooks.ts +75 -0
- package/src/cli/templates/rules.ts +38 -0
- package/src/cli/templates/skill.ts +66 -0
- package/src/core/boundary/evaluator.ts +210 -0
- package/src/core/config/loader.ts +84 -0
- package/src/core/config/schema.ts +39 -0
- package/src/core/database/client.ts +132 -0
- package/src/core/database/repositories/dependency.repository.ts +279 -0
- package/src/core/database/repositories/domain.repository.ts +198 -0
- package/src/core/database/repositories/file.repository.ts +99 -0
- package/src/core/database/repositories/meta.repository.ts +33 -0
- package/src/core/database/repositories/symbol.repository.ts +424 -0
- package/src/core/database/repository.ts +242 -0
- package/src/core/database/schema.sql +92 -0
- package/src/core/discovery/topology-detector.ts +418 -0
- package/src/core/ingestion/hasher.ts +5 -0
- package/src/core/ingestion/pipeline.ts +438 -0
- package/src/core/parser/boundary-tracker.ts +128 -0
- package/src/core/parser/extractors/base.ts +60 -0
- package/src/core/parser/extractors/frontend.ts +194 -0
- package/src/core/parser/extractors/go.ts +176 -0
- package/src/core/parser/extractors/laravel-semantic.ts +426 -0
- package/src/core/parser/extractors/mcp-cli-semantic.ts +199 -0
- package/src/core/parser/extractors/nestjs-semantic.ts +119 -0
- package/src/core/parser/extractors/php.ts +149 -0
- package/src/core/parser/extractors/python.ts +226 -0
- package/src/core/parser/extractors/registry.ts +49 -0
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -0
- package/src/core/parser/extractors/typescript.ts +177 -0
- package/src/core/parser/tree-sitter.ts +39 -0
- package/src/core/resolver/call-graph-tracer.ts +668 -0
- package/src/core/resolver/module-resolver.ts +323 -0
- package/src/core/resolver/symbol-locator.ts +405 -0
- package/src/core/resolver/vertical-slice-tracer.ts +236 -0
- package/src/core/session/session-manager.ts +56 -0
- package/src/core/telemetry/telemetry.ts +86 -0
- package/src/index.ts +18 -0
- package/src/mcp/schemas.ts +68 -0
- package/src/mcp/server.ts +602 -0
- package/src/mcp/tools/check-boundary.ts +102 -0
- package/src/mcp/tools/get-domain-catalog.ts +91 -0
- package/src/mcp/tools/get-feature-context.ts +116 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +49 -0
- package/src/mcp/tools/get-symbol-impact.ts +26 -0
- package/src/mcp/tools/get-symbol.ts +63 -0
- package/src/mcp/tools/locate-symbol.ts +74 -0
- package/src/mcp/tools/register-domain.ts +64 -0
- package/src/mcp/tools/trace-vertical-slice.ts +102 -0
- package/src/types/index.ts +296 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ValidatedSeptumConfig } from "../config/schema.ts";
|
|
4
|
+
import type { SeptumRepository } from "../database/repository.ts";
|
|
5
|
+
|
|
6
|
+
export interface PathAliasRule {
|
|
7
|
+
prefix: string;
|
|
8
|
+
targets: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Psr4Rule {
|
|
12
|
+
prefix: string;
|
|
13
|
+
baseDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ModuleResolver {
|
|
17
|
+
private projectRoot: string;
|
|
18
|
+
private repo?: SeptumRepository;
|
|
19
|
+
private tsPaths: PathAliasRule[] = [];
|
|
20
|
+
private tsBaseUrl: string = ".";
|
|
21
|
+
private psr4Rules: Psr4Rule[] = [];
|
|
22
|
+
private configsLoaded = false;
|
|
23
|
+
|
|
24
|
+
constructor(projectRoot: string = process.cwd(), repo?: SeptumRepository) {
|
|
25
|
+
this.projectRoot = path.resolve(projectRoot);
|
|
26
|
+
this.repo = repo;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public setRepository(repo: SeptumRepository): void {
|
|
30
|
+
this.repo = repo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Lazily loads project configuration files (tsconfig.json, composer.json)
|
|
35
|
+
*/
|
|
36
|
+
public ensureConfigLoaded(): void {
|
|
37
|
+
if (this.configsLoaded) return;
|
|
38
|
+
this.loadTsConfig();
|
|
39
|
+
this.loadComposerJson();
|
|
40
|
+
this.configsLoaded = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the domain that owns the given source file.
|
|
45
|
+
*/
|
|
46
|
+
public resolveSourceDomain(
|
|
47
|
+
filePath: string,
|
|
48
|
+
config: ValidatedSeptumConfig
|
|
49
|
+
): string | null {
|
|
50
|
+
const absPath = this.toAbsolutePath(filePath);
|
|
51
|
+
|
|
52
|
+
for (const [domainName, domainCfg] of Object.entries(config.domains)) {
|
|
53
|
+
const domainAbsRoot = this.toAbsolutePath(domainCfg.root);
|
|
54
|
+
if (this.isSubPathOrSame(absPath, domainAbsRoot)) {
|
|
55
|
+
return domainName;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Deterministically resolves an imported module/target to its target domain.
|
|
64
|
+
*/
|
|
65
|
+
public resolveTargetDomain(
|
|
66
|
+
targetStatement: string,
|
|
67
|
+
sourceFilePath: string | undefined,
|
|
68
|
+
config: ValidatedSeptumConfig
|
|
69
|
+
): string | null {
|
|
70
|
+
this.ensureConfigLoaded();
|
|
71
|
+
|
|
72
|
+
const normalizedTarget = targetStatement.trim().replace(/^['"]|['"]$/g, "");
|
|
73
|
+
if (!normalizedTarget) return null;
|
|
74
|
+
|
|
75
|
+
// 1. Relative imports: ./ or ../ or Python dot relative . or ..
|
|
76
|
+
if (normalizedTarget.startsWith("./") || normalizedTarget.startsWith("../")) {
|
|
77
|
+
if (sourceFilePath) {
|
|
78
|
+
const sourceAbs = this.toAbsolutePath(sourceFilePath);
|
|
79
|
+
const sourceDir = path.dirname(sourceAbs);
|
|
80
|
+
const resolvedCandidate = path.resolve(sourceDir, normalizedTarget);
|
|
81
|
+
const domainFromPath = this.matchDomainByPath(resolvedCandidate, config);
|
|
82
|
+
if (domainFromPath) return domainFromPath;
|
|
83
|
+
}
|
|
84
|
+
} else if (normalizedTarget.startsWith(".")) {
|
|
85
|
+
// Python style relative import (e.g. .service, ..billing.service)
|
|
86
|
+
if (sourceFilePath) {
|
|
87
|
+
const sourceAbs = this.toAbsolutePath(sourceFilePath);
|
|
88
|
+
const sourceDir = path.dirname(sourceAbs);
|
|
89
|
+
const dotMatch = /^\.+/.exec(normalizedTarget);
|
|
90
|
+
if (dotMatch) {
|
|
91
|
+
const dotCount = dotMatch[0].length;
|
|
92
|
+
const remainder = normalizedTarget.slice(dotCount).replace(/\./g, path.sep);
|
|
93
|
+
const upSteps = "../".repeat(Math.max(0, dotCount - 1));
|
|
94
|
+
const resolvedCandidate = path.resolve(sourceDir, upSteps, remainder);
|
|
95
|
+
const domainFromPath = this.matchDomainByPath(resolvedCandidate, config);
|
|
96
|
+
if (domainFromPath) return domainFromPath;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Workspace Package Name Resolution (e.g. @repo/shared-types, @repo/design-system)
|
|
102
|
+
const domainFromPackage = this.matchDomainByPackageName(normalizedTarget, config);
|
|
103
|
+
if (domainFromPackage) return domainFromPackage;
|
|
104
|
+
|
|
105
|
+
// 3. Python Absolute Dot-notation: domains.billing.service or app.domains.billing
|
|
106
|
+
if (normalizedTarget.includes(".") && !normalizedTarget.includes("/")) {
|
|
107
|
+
const asPath = normalizedTarget.replace(/\./g, path.sep);
|
|
108
|
+
const candidatePath = path.resolve(this.projectRoot, asPath);
|
|
109
|
+
const domainFromPython = this.matchDomainByPath(candidatePath, config);
|
|
110
|
+
if (domainFromPython) return domainFromPython;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. TypeScript Path Aliases (tsconfig.json paths)
|
|
114
|
+
const tsResolved = this.resolveTsPathAlias(normalizedTarget);
|
|
115
|
+
if (tsResolved) {
|
|
116
|
+
const domainFromTs = this.matchDomainByPath(tsResolved, config);
|
|
117
|
+
if (domainFromTs) return domainFromTs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4. PHP PSR-4 Namespaces (composer.json autoload)
|
|
121
|
+
const phpResolved = this.resolvePsr4Namespace(normalizedTarget);
|
|
122
|
+
if (phpResolved) {
|
|
123
|
+
const domainFromPhp = this.matchDomainByPath(phpResolved, config);
|
|
124
|
+
if (domainFromPhp) return domainFromPhp;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 4. Database-driven direct file lookup
|
|
128
|
+
if (this.repo) {
|
|
129
|
+
const fileRecord = this.repo.getFileWithDomain(normalizedTarget);
|
|
130
|
+
if (fileRecord) {
|
|
131
|
+
return fileRecord.domain_name;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. Database-driven symbol lookup
|
|
135
|
+
const symbolName = this.extractSymbolName(normalizedTarget);
|
|
136
|
+
if (symbolName) {
|
|
137
|
+
const symbolRecord = this.repo.findSymbolWithDomain(symbolName);
|
|
138
|
+
if (symbolRecord) {
|
|
139
|
+
return symbolRecord.domain_name;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 6. Root path segment matching (fallback for explicit domain root references)
|
|
145
|
+
const normalizedSlash = normalizedTarget.replace(/\\/g, "/").toLowerCase();
|
|
146
|
+
for (const [domainName, domainCfg] of Object.entries(config.domains)) {
|
|
147
|
+
const domainRootSlash = domainCfg.root.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
148
|
+
const lowerDomain = domainName.toLowerCase();
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
normalizedSlash.includes(`/${domainRootSlash}/`) ||
|
|
152
|
+
normalizedSlash.startsWith(`${domainRootSlash}/`) ||
|
|
153
|
+
normalizedSlash === domainRootSlash ||
|
|
154
|
+
normalizedSlash.includes(`/${lowerDomain}/`) ||
|
|
155
|
+
normalizedSlash.endsWith(`/${lowerDomain}`)
|
|
156
|
+
) {
|
|
157
|
+
return domainName;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private matchDomainByPath(targetPath: string, config: ValidatedSeptumConfig): string | null {
|
|
165
|
+
const absTargetPath = this.toAbsolutePath(targetPath);
|
|
166
|
+
|
|
167
|
+
const sortedDomains = Object.entries(config.domains)
|
|
168
|
+
.map(([domainName, domainCfg]) => ({
|
|
169
|
+
domainName,
|
|
170
|
+
domainCfg,
|
|
171
|
+
absRoot: this.toAbsolutePath(domainCfg.root),
|
|
172
|
+
}))
|
|
173
|
+
.sort((a, b) => b.absRoot.length - a.absRoot.length);
|
|
174
|
+
|
|
175
|
+
for (const { domainName, absRoot } of sortedDomains) {
|
|
176
|
+
if (this.isSubPathOrSame(absTargetPath, absRoot)) {
|
|
177
|
+
return domainName;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private matchDomainByPackageName(target: string, config: ValidatedSeptumConfig): string | null {
|
|
185
|
+
for (const [domainName, domainCfg] of Object.entries(config.domains)) {
|
|
186
|
+
const pkgJsonPath = path.resolve(this.projectRoot, domainCfg.root, "package.json");
|
|
187
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const raw = fs.readFileSync(pkgJsonPath, "utf8");
|
|
190
|
+
const pkg = JSON.parse(raw);
|
|
191
|
+
if (pkg.name && typeof pkg.name === "string") {
|
|
192
|
+
if (target === pkg.name || target.startsWith(`${pkg.name}/`)) {
|
|
193
|
+
return domainName;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Ignore unreadable or invalid package.json
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private isSubPathOrSame(targetPath: string, rootPath: string): boolean {
|
|
205
|
+
const normalizedTarget = path.normalize(targetPath);
|
|
206
|
+
const normalizedRoot = path.normalize(rootPath);
|
|
207
|
+
|
|
208
|
+
if (normalizedTarget === normalizedRoot) return true;
|
|
209
|
+
const rootWithSep = normalizedRoot.endsWith(path.sep)
|
|
210
|
+
? normalizedRoot
|
|
211
|
+
: normalizedRoot + path.sep;
|
|
212
|
+
|
|
213
|
+
return normalizedTarget.startsWith(rootWithSep);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private toAbsolutePath(filePath: string): string {
|
|
217
|
+
if (path.isAbsolute(filePath)) {
|
|
218
|
+
return path.normalize(filePath);
|
|
219
|
+
}
|
|
220
|
+
return path.resolve(this.projectRoot, filePath);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private extractSymbolName(target: string): string | null {
|
|
224
|
+
// PHP namespace or TS qualified name: Foo\Bar\Baz or Foo/Bar/Baz or bare Baz
|
|
225
|
+
const cleaned = target.replace(/\\/g, "/");
|
|
226
|
+
const parts = cleaned.split("/").filter(Boolean);
|
|
227
|
+
const lastPart = parts[parts.length - 1];
|
|
228
|
+
if (lastPart && /^[A-Z][a-zA-Z0-9_]*$/.test(lastPart)) {
|
|
229
|
+
return lastPart;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private resolveTsPathAlias(target: string): string | null {
|
|
235
|
+
for (const rule of this.tsPaths) {
|
|
236
|
+
if (rule.prefix.endsWith("*")) {
|
|
237
|
+
const basePrefix = rule.prefix.slice(0, -1);
|
|
238
|
+
if (target.startsWith(basePrefix)) {
|
|
239
|
+
const suffix = target.slice(basePrefix.length);
|
|
240
|
+
for (const targetPattern of rule.targets) {
|
|
241
|
+
const candidate = targetPattern.replace("*", suffix);
|
|
242
|
+
const baseDir = path.resolve(this.projectRoot, this.tsBaseUrl);
|
|
243
|
+
return path.resolve(baseDir, candidate);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (target === rule.prefix) {
|
|
247
|
+
for (const targetPattern of rule.targets) {
|
|
248
|
+
const baseDir = path.resolve(this.projectRoot, this.tsBaseUrl);
|
|
249
|
+
return path.resolve(baseDir, targetPattern);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private resolvePsr4Namespace(target: string): string | null {
|
|
257
|
+
// PHP PSR-4: App\Domain\Billing\Service -> app/Domain/Billing/Service.php
|
|
258
|
+
const normalizedTarget = target.replace(/\//g, "\\");
|
|
259
|
+
|
|
260
|
+
for (const rule of this.psr4Rules) {
|
|
261
|
+
if (normalizedTarget.startsWith(rule.prefix)) {
|
|
262
|
+
const subNamespace = normalizedTarget.slice(rule.prefix.length);
|
|
263
|
+
const subPath = subNamespace.replace(/\\/g, path.sep);
|
|
264
|
+
const resolved = path.resolve(this.projectRoot, rule.baseDir, subPath);
|
|
265
|
+
return resolved;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private loadTsConfig(): void {
|
|
272
|
+
const tsconfigPath = path.join(this.projectRoot, "tsconfig.json");
|
|
273
|
+
if (!fs.existsSync(tsconfigPath)) return;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const raw = fs.readFileSync(tsconfigPath, "utf-8");
|
|
277
|
+
// Strip comments (simple JSON with comments regex)
|
|
278
|
+
const cleanJson = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
|
279
|
+
const parsed = JSON.parse(cleanJson);
|
|
280
|
+
const compilerOptions = parsed.compilerOptions ?? {};
|
|
281
|
+
|
|
282
|
+
this.tsBaseUrl = compilerOptions.baseUrl ?? ".";
|
|
283
|
+
|
|
284
|
+
if (compilerOptions.paths && typeof compilerOptions.paths === "object") {
|
|
285
|
+
for (const [key, val] of Object.entries(compilerOptions.paths)) {
|
|
286
|
+
const targets = Array.isArray(val) ? (val as string[]) : [String(val)];
|
|
287
|
+
this.tsPaths.push({
|
|
288
|
+
prefix: key,
|
|
289
|
+
targets,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Graceful fallback if tsconfig is malformed
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private loadComposerJson(): void {
|
|
299
|
+
const composerPath = path.join(this.projectRoot, "composer.json");
|
|
300
|
+
if (!fs.existsSync(composerPath)) return;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const raw = fs.readFileSync(composerPath, "utf-8");
|
|
304
|
+
const parsed = JSON.parse(raw);
|
|
305
|
+
|
|
306
|
+
const extractPsr4 = (section: Record<string, unknown> | undefined) => {
|
|
307
|
+
if (section && typeof section["psr-4"] === "object" && section["psr-4"] !== null) {
|
|
308
|
+
for (const [ns, dir] of Object.entries(section["psr-4"] as Record<string, string>)) {
|
|
309
|
+
this.psr4Rules.push({
|
|
310
|
+
prefix: ns,
|
|
311
|
+
baseDir: String(dir),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
extractPsr4(parsed.autoload);
|
|
318
|
+
extractPsr4(parsed["autoload-dev"]);
|
|
319
|
+
} catch {
|
|
320
|
+
// Graceful fallback if composer.json is malformed
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|