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,198 +1,275 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite";
|
|
2
|
-
import type {
|
|
3
|
-
DomainCatalogResponse,
|
|
4
|
-
DomainConfig,
|
|
5
|
-
DomainRecord,
|
|
6
|
-
FileRecord,
|
|
7
|
-
SymbolKind,
|
|
8
|
-
SymbolRecord,
|
|
9
|
-
} from "../../../types/index.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
|
-
const
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type {
|
|
3
|
+
DomainCatalogResponse,
|
|
4
|
+
DomainConfig,
|
|
5
|
+
DomainRecord,
|
|
6
|
+
FileRecord,
|
|
7
|
+
SymbolKind,
|
|
8
|
+
SymbolRecord,
|
|
9
|
+
} from "../../../types/index.ts";
|
|
10
|
+
import { DependencyRepository } from "./dependency.repository.ts";
|
|
11
|
+
|
|
12
|
+
const CONTAINER_KINDS = new Set<SymbolKind>([
|
|
13
|
+
"class",
|
|
14
|
+
"interface",
|
|
15
|
+
"trait",
|
|
16
|
+
"struct",
|
|
17
|
+
"enum",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export class DomainRepository {
|
|
21
|
+
constructor(private db: Database) {}
|
|
22
|
+
|
|
23
|
+
public upsertDomain(name: string, config: DomainConfig): number {
|
|
24
|
+
const normalizedName = name.trim();
|
|
25
|
+
const existing = this.getDomainByName(normalizedName);
|
|
26
|
+
|
|
27
|
+
// Merge dependencies and archetypes if existing and incoming is empty
|
|
28
|
+
let allowed = config.allowed_dependencies ?? [];
|
|
29
|
+
let forbidden = config.forbidden_dependencies ?? [];
|
|
30
|
+
let archetypes = config.archetypes ?? {};
|
|
31
|
+
|
|
32
|
+
if (existing) {
|
|
33
|
+
if (allowed.length === 0 && existing.allowed_deps_json && existing.allowed_deps_json !== "[]") {
|
|
34
|
+
try {
|
|
35
|
+
allowed = JSON.parse(existing.allowed_deps_json);
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
if (forbidden.length === 0 && existing.forbidden_deps_json && existing.forbidden_deps_json !== "[]") {
|
|
39
|
+
try {
|
|
40
|
+
forbidden = JSON.parse(existing.forbidden_deps_json);
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
if (Object.keys(archetypes).length === 0 && existing.archetypes_json && existing.archetypes_json !== "{}") {
|
|
44
|
+
try {
|
|
45
|
+
archetypes = JSON.parse(existing.archetypes_json);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const allowedJson = JSON.stringify(allowed);
|
|
51
|
+
const forbiddenJson = JSON.stringify(forbidden);
|
|
52
|
+
const archetypesJson = JSON.stringify(archetypes);
|
|
53
|
+
|
|
54
|
+
const query = `
|
|
55
|
+
INSERT INTO domains (name, root_path, allowed_deps_json, forbidden_deps_json, archetypes_json)
|
|
56
|
+
VALUES (?, ?, ?, ?, ?)
|
|
57
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
58
|
+
root_path = excluded.root_path,
|
|
59
|
+
allowed_deps_json = excluded.allowed_deps_json,
|
|
60
|
+
forbidden_deps_json = excluded.forbidden_deps_json,
|
|
61
|
+
archetypes_json = excluded.archetypes_json,
|
|
62
|
+
updated_at = CURRENT_TIMESTAMP
|
|
63
|
+
`;
|
|
64
|
+
this.db.query(query).run(normalizedName, config.root, allowedJson, forbiddenJson, archetypesJson);
|
|
65
|
+
|
|
66
|
+
const record = this.getDomainByName(normalizedName);
|
|
67
|
+
return record?.id ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getDomainByName(name: string): DomainRecord | null {
|
|
71
|
+
return this.db
|
|
72
|
+
.query<DomainRecord, [string]>("SELECT * FROM domains WHERE LOWER(name) = LOWER(?)")
|
|
73
|
+
.get(name.trim());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public getAllDomains(): DomainRecord[] {
|
|
77
|
+
return this.db.query<DomainRecord, []>("SELECT * FROM domains ORDER BY name ASC").all();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getDomainCatalog(
|
|
81
|
+
domainName: string,
|
|
82
|
+
archetypeFilter?: string,
|
|
83
|
+
options?: { detailLevel?: "compact" | "full"; fileFilter?: string }
|
|
84
|
+
): DomainCatalogResponse | null {
|
|
85
|
+
const domain = this.getDomainByName(domainName);
|
|
86
|
+
if (!domain) return null;
|
|
87
|
+
|
|
88
|
+
const detailLevel = options?.detailLevel ?? "compact";
|
|
89
|
+
const fileFilter = options?.fileFilter?.trim();
|
|
90
|
+
|
|
91
|
+
const allowedDeps: string[] = JSON.parse(domain.allowed_deps_json || "[]");
|
|
92
|
+
const forbiddenDeps: string[] = JSON.parse(domain.forbidden_deps_json || "[]");
|
|
93
|
+
|
|
94
|
+
let filesQuery = "SELECT * FROM files WHERE domain_id = ?";
|
|
95
|
+
const params: [number, string?] = [domain.id];
|
|
96
|
+
|
|
97
|
+
if (archetypeFilter) {
|
|
98
|
+
filesQuery += " AND archetype = ?";
|
|
99
|
+
params.push(archetypeFilter);
|
|
100
|
+
}
|
|
101
|
+
filesQuery += " ORDER BY path ASC";
|
|
102
|
+
|
|
103
|
+
const allDomainFiles = this.db.query<FileRecord, any>(filesQuery).all(...params);
|
|
104
|
+
const totalFiles = allDomainFiles.length;
|
|
105
|
+
|
|
106
|
+
const files = fileFilter
|
|
107
|
+
? allDomainFiles.filter((f) =>
|
|
108
|
+
f.path.replace(/\\/g, "/").toLowerCase().includes(fileFilter.replace(/\\/g, "/").toLowerCase())
|
|
109
|
+
)
|
|
110
|
+
: allDomainFiles;
|
|
111
|
+
|
|
112
|
+
const catalogFiles: DomainCatalogResponse["files"] = [];
|
|
113
|
+
|
|
114
|
+
// Batch retrieve symbols for all files in one query to eliminate N+1 roundtrips
|
|
115
|
+
const symbolsByFileId = new Map<number, SymbolRecord[]>();
|
|
116
|
+
for (const f of files) {
|
|
117
|
+
symbolsByFileId.set(f.id, []);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (files.length > 0) {
|
|
121
|
+
const allDomainSymbols = this.db
|
|
122
|
+
.query<SymbolRecord, [number]>(
|
|
123
|
+
`SELECT s.* FROM symbols s
|
|
124
|
+
JOIN files f ON s.file_id = f.id
|
|
125
|
+
WHERE f.domain_id = ?
|
|
126
|
+
ORDER BY s.file_id ASC, s.line_start ASC`
|
|
127
|
+
)
|
|
128
|
+
.all(domain.id);
|
|
129
|
+
|
|
130
|
+
for (const sym of allDomainSymbols) {
|
|
131
|
+
const list = symbolsByFileId.get(sym.file_id);
|
|
132
|
+
if (list) {
|
|
133
|
+
list.push(sym);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Batch compute file connections for the domain (zero N+1)
|
|
139
|
+
const depRepo = new DependencyRepository(this.db);
|
|
140
|
+
const connectionsByPath = depRepo.getDomainFileConnections(domain.id);
|
|
141
|
+
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
const symbols = symbolsByFileId.get(file.id) ?? [];
|
|
144
|
+
|
|
145
|
+
const symbolMap = new Map<
|
|
146
|
+
string,
|
|
147
|
+
{
|
|
148
|
+
name: string;
|
|
149
|
+
kind: SymbolKind;
|
|
150
|
+
lineSpan: string;
|
|
151
|
+
properties: string[];
|
|
152
|
+
methods: string[];
|
|
153
|
+
signatures: string[];
|
|
154
|
+
}
|
|
155
|
+
>();
|
|
156
|
+
|
|
157
|
+
for (const sym of symbols) {
|
|
158
|
+
const lineSpan = `${sym.line_start}-${sym.line_end}`;
|
|
159
|
+
if (CONTAINER_KINDS.has(sym.kind)) {
|
|
160
|
+
if (!symbolMap.has(sym.name)) {
|
|
161
|
+
symbolMap.set(sym.name, {
|
|
162
|
+
name: sym.name,
|
|
163
|
+
kind: sym.kind,
|
|
164
|
+
lineSpan,
|
|
165
|
+
properties: [],
|
|
166
|
+
methods: [],
|
|
167
|
+
signatures: [sym.signature],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} else if (sym.kind === "method" || sym.kind === "property") {
|
|
171
|
+
let targetContainer:
|
|
172
|
+
| {
|
|
173
|
+
name: string;
|
|
174
|
+
kind: SymbolKind;
|
|
175
|
+
lineSpan: string;
|
|
176
|
+
properties: string[];
|
|
177
|
+
methods: string[];
|
|
178
|
+
signatures: string[];
|
|
179
|
+
}
|
|
180
|
+
| undefined;
|
|
181
|
+
|
|
182
|
+
if (sym.name.includes("::")) {
|
|
183
|
+
const containerName = sym.name.split("::")[0];
|
|
184
|
+
const candidate = symbolMap.get(containerName);
|
|
185
|
+
if (candidate && CONTAINER_KINDS.has(candidate.kind)) {
|
|
186
|
+
targetContainer = candidate;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (targetContainer) {
|
|
191
|
+
if (sym.kind === "method") {
|
|
192
|
+
targetContainer.methods.push(sym.signature);
|
|
193
|
+
} else {
|
|
194
|
+
targetContainer.properties.push(sym.signature);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
symbolMap.set(sym.name, {
|
|
198
|
+
name: sym.name,
|
|
199
|
+
kind: sym.kind,
|
|
200
|
+
lineSpan,
|
|
201
|
+
properties: sym.kind === "property" ? [sym.signature] : [],
|
|
202
|
+
methods: [],
|
|
203
|
+
signatures: [sym.signature],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
if (
|
|
208
|
+
detailLevel === "compact" &&
|
|
209
|
+
(sym.kind === "variable" || sym.kind === "constant") &&
|
|
210
|
+
sym.visibility === "private"
|
|
211
|
+
) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
symbolMap.set(sym.name, {
|
|
215
|
+
name: sym.name,
|
|
216
|
+
kind: sym.kind,
|
|
217
|
+
lineSpan,
|
|
218
|
+
properties: [],
|
|
219
|
+
methods: [],
|
|
220
|
+
signatures: [sym.signature],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rawConn = connectionsByPath.get(file.path);
|
|
226
|
+
const fileConnections = {
|
|
227
|
+
imports_from: rawConn?.imports_from ?? [],
|
|
228
|
+
imported_by_count: rawConn?.imported_by_count ?? 0,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
catalogFiles.push({
|
|
232
|
+
path: file.path,
|
|
233
|
+
archetype: file.archetype,
|
|
234
|
+
line_count: file.line_count,
|
|
235
|
+
connections: fileConnections,
|
|
236
|
+
symbols: Array.from(symbolMap.values()).map((s) => {
|
|
237
|
+
if (detailLevel === "compact") {
|
|
238
|
+
return {
|
|
239
|
+
name: s.name,
|
|
240
|
+
kind: s.kind,
|
|
241
|
+
line_span: s.lineSpan,
|
|
242
|
+
methods_count: s.methods.length > 0 ? s.methods.length : undefined,
|
|
243
|
+
properties_count: s.properties.length > 0 ? s.properties.length : undefined,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
name: s.name,
|
|
248
|
+
kind: s.kind,
|
|
249
|
+
line_span: s.lineSpan,
|
|
250
|
+
methods_count: s.methods.length > 0 ? s.methods.length : undefined,
|
|
251
|
+
properties_count: s.properties.length > 0 ? s.properties.length : undefined,
|
|
252
|
+
properties: s.properties.length > 0 ? s.properties : undefined,
|
|
253
|
+
methods: s.methods.length > 0 ? s.methods : undefined,
|
|
254
|
+
signatures: s.signatures.length > 0 ? s.signatures : undefined,
|
|
255
|
+
};
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
domain: domain.name,
|
|
262
|
+
root: domain.root_path,
|
|
263
|
+
detail_level: detailLevel,
|
|
264
|
+
total_files: totalFiles,
|
|
265
|
+
showing_files: catalogFiles.length,
|
|
266
|
+
allowed_dependencies: allowedDeps,
|
|
267
|
+
forbidden_dependencies: forbiddenDeps,
|
|
268
|
+
files: catalogFiles,
|
|
269
|
+
agent_hint:
|
|
270
|
+
detailLevel === "compact"
|
|
271
|
+
? "Compact view (<400 tokens). Use septum_get_symbol({ symbol }) for exact method signatures, or septum_get_domain_catalog({ domain, detail_level: 'full', file_filter: '<path>' }) for full member lists."
|
|
272
|
+
: undefined,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|