septum 0.1.2 → 0.1.4
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 +7 -2
- package/package.json +1 -1
- package/src/cli/commands/slice.ts +4 -46
- package/src/cli/commands/topology.ts +119 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/templates/skill.ts +7 -4
- package/src/core/config/loader.ts +21 -8
- package/src/core/database/repositories/dependency.repository.ts +70 -0
- package/src/core/database/repositories/environment.repository.ts +207 -0
- package/src/core/database/repositories/file.repository.ts +7 -0
- package/src/core/database/repositories/symbol.repository.ts +28 -9
- package/src/core/database/repository.ts +19 -1
- package/src/core/database/schema.ts +153 -120
- package/src/core/discovery/topology-detector.ts +71 -3
- package/src/core/ingestion/pipeline.ts +98 -1
- package/src/core/parser/behavioral-analyzer.ts +119 -0
- package/src/core/parser/boundary-tracker.ts +160 -0
- package/src/core/parser/extractors/base.ts +2 -0
- package/src/core/parser/extractors/blade.ts +158 -0
- package/src/core/parser/extractors/go.ts +5 -1
- package/src/core/parser/extractors/laravel-semantic.ts +506 -31
- package/src/core/parser/extractors/php.ts +5 -1
- package/src/core/parser/extractors/python.ts +3 -1
- package/src/core/parser/extractors/registry.ts +21 -0
- package/src/core/parser/extractors/typescript.ts +5 -1
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
- package/src/core/parser/schema/schema-provider-registry.ts +43 -0
- package/src/core/parser/schema/schema-provider.interface.ts +33 -0
- package/src/core/parser/tree-sitter.ts +2 -0
- package/src/core/resolver/path-resolver.ts +147 -0
- package/src/core/resolver/vertical-slice-tracer.ts +56 -8
- package/src/core/topology/environment-detector.ts +860 -0
- package/src/core/topology/types.ts +83 -0
- package/src/index.ts +3 -0
- package/src/mcp/schemas.ts +264 -46
- package/src/mcp/server.ts +130 -26
- package/src/mcp/tools/check-boundary.ts +58 -8
- package/src/mcp/tools/get-domain-catalog.ts +31 -1
- package/src/mcp/tools/get-environment-topology.ts +141 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
- package/src/mcp/tools/locate-symbol.ts +119 -3
- package/src/mcp/tools/register-domain.ts +1 -1
- package/src/mcp/tools/trace-vertical-slice.ts +11 -65
- package/src/types/index.ts +20 -1
- package/src/version.ts +3 -3
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Deterministic Bounded-Context & Anti-Poisoning Architecture Guard for AI Coding Agents.**
|
|
4
4
|
|
|
5
|
+
[](package.json)
|
|
5
6
|
[](https://bun.sh)
|
|
6
7
|
[](https://modelcontextprotocol.io/)
|
|
7
8
|
[-003B57?logo=sqlite)](https://sqlite.org/)
|
|
@@ -61,6 +62,9 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
|
|
|
61
62
|
| `septum sync` | Fast incremental delta sync (< 15ms) for modified files |
|
|
62
63
|
| `septum check --strict` | CI/CD gate: verify boundary integrity & fail on violations |
|
|
63
64
|
| `septum query [domain]` | Inspect domain catalog, archetypes, and public signatures |
|
|
65
|
+
| `septum locate <query>` | Locate symbol, class, interface, method, or error source |
|
|
66
|
+
| `septum slice <route\|intent>` | Trace end-to-end vertical execution slice for route/intent |
|
|
67
|
+
| `septum topology [action]` | Inspect 6-layer environment topology plane & distributed nodes |
|
|
64
68
|
| `septum feature status\|clear` | Manage active feature lease and editing scope |
|
|
65
69
|
| `septum serve` | Launch stdio MCP server for AI coding agents |
|
|
66
70
|
|
|
@@ -70,12 +74,13 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
|
|
|
70
74
|
|
|
71
75
|
All tools expose typed schemas automatically via the Model Context Protocol:
|
|
72
76
|
|
|
77
|
+
- **Self-Aware Environment Topology (`septum_get_environment_topology`):** Maps and verifies the 6-layer production architecture (Edge/WAF, Gateway/Web Server, Host, Runtime, Storage/Cache, Telemetry) and distributed entities with cross-layer invariants.
|
|
73
78
|
- **Whole-Project Telescope (`septum_get_domain_catalog`):** Returns macro architecture maps in < 400 tokens.
|
|
74
|
-
- **Vertical Slice Tracer (`septum_trace_vertical_slice`):** Traces execution chains (*Ingress ➔ Validation ➔ Orchestration ➔ Domain ➔ Egress*)
|
|
79
|
+
- **Omni-Trigger Vertical Slice Tracer (`septum_trace_vertical_slice`):** Traces execution chains across HTTP routes, queue jobs, console commands, and event listeners (*Ingress ➔ Validation ➔ Orchestration ➔ Domain ➔ Egress*).
|
|
75
80
|
- **Pre-Flight Boundary Guard (`septum_check_boundary`):** Validates proposed edits and imports against bounded-context rules.
|
|
76
81
|
- **Scope Locking (`septum_get_feature_context` / `septum_clear_feature_context`):** Locks active feature scope to eliminate context drift.
|
|
77
82
|
- **Symbol & Blast Radius (`septum_locate_symbol`, `septum_get_symbol`, `septum_get_symbol_impact`):** Resolves call chains, signatures, and refactoring impact.
|
|
78
|
-
- **God Function Detector (`septum_get_symbol_hotspots`):** Surfaces oversized functions/methods by physical lines
|
|
83
|
+
- **God Function & Nesting Detector (`septum_get_symbol_hotspots`):** Surfaces oversized functions/methods ranked by physical lines, cyclomatic nesting depth, and composite risk score.
|
|
79
84
|
- **Dynamic Topology (`septum_register_domain`):** Registers or updates domains and boundary rules on-the-fly.
|
|
80
85
|
|
|
81
86
|
---
|
package/package.json
CHANGED
|
@@ -20,57 +20,15 @@ export async function handleSliceCommand(
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
if (result.found
|
|
24
|
-
|
|
25
|
-
console.log(`\n=================================================`);
|
|
26
|
-
console.log(` VERTICAL SLICE: ${s.route.method} ${s.route.uri}`);
|
|
27
|
-
console.log(`=================================================`);
|
|
28
|
-
if (s.route.name) {
|
|
29
|
-
console.log(` • Route Name: ${s.route.name}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (s.request) {
|
|
33
|
-
console.log(`\n • Request Validation:`);
|
|
34
|
-
console.log(` Class: ${s.request.class} (${s.request.file || "inline"})`);
|
|
35
|
-
const rules = Object.keys(s.request.rules);
|
|
36
|
-
if (rules.length > 0) {
|
|
37
|
-
console.log(` Rules:`);
|
|
38
|
-
for (const r of rules) {
|
|
39
|
-
console.log(` - ${r}: ${s.request.rules[r]}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
console.log(`\n • Controller Action:`);
|
|
45
|
-
console.log(` Class: ${s.controller.class}@${s.controller.action}`);
|
|
46
|
-
console.log(` Target: ${s.controller.file || "unknown"}:${s.controller.line}`);
|
|
47
|
-
|
|
48
|
-
if (s.model) {
|
|
49
|
-
console.log(`\n • Model:`);
|
|
50
|
-
console.log(` Class: ${s.model.class} (${s.model.file || "unknown"})`);
|
|
51
|
-
if (s.model.fillable && s.model.fillable.length > 0) {
|
|
52
|
-
console.log(` Fillable: [${s.model.fillable.join(", ")}]`);
|
|
53
|
-
}
|
|
54
|
-
if (s.model.casts && Object.keys(s.model.casts).length > 0) {
|
|
55
|
-
console.log(` Casts: ${JSON.stringify(s.model.casts)}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (s.frontend) {
|
|
60
|
-
console.log(`\n • Frontend Target:`);
|
|
61
|
-
console.log(` Page: ${s.frontend.target}`);
|
|
62
|
-
if (s.frontend.props.length > 0) {
|
|
63
|
-
console.log(` Props: [${s.frontend.props.join(", ")}]`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
23
|
+
if (result.found) {
|
|
24
|
+
console.log(`\n${result.message}\n`);
|
|
67
25
|
if (result.alternatives && result.alternatives.length > 0) {
|
|
68
|
-
console.log(
|
|
26
|
+
console.log(` • Alternative Matches:`);
|
|
69
27
|
for (const alt of result.alternatives) {
|
|
70
28
|
console.log(` - ${alt.method} ${alt.uri} -> ${alt.controller}@${alt.action}`);
|
|
71
29
|
}
|
|
30
|
+
console.log(``);
|
|
72
31
|
}
|
|
73
|
-
console.log(`\n=================================================\n`);
|
|
74
32
|
} else {
|
|
75
33
|
console.log(`\n❌ Vertical Slice Not Found:`);
|
|
76
34
|
console.log(` Query: ${result.query}`);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { SeptumDatabase } from "../../core/database/client.ts";
|
|
2
|
+
import { SeptumRepository } from "../../core/database/repository.ts";
|
|
3
|
+
import { EnvironmentDetector } from "../../core/topology/environment-detector.ts";
|
|
4
|
+
import type { EnvironmentLayer, TopologyNode } from "../../core/topology/types.ts";
|
|
5
|
+
|
|
6
|
+
export async function handleTopologyCommand(
|
|
7
|
+
subcommand?: string,
|
|
8
|
+
arg1?: string,
|
|
9
|
+
arg2?: string,
|
|
10
|
+
arg3?: string,
|
|
11
|
+
arg4?: string
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const db = new SeptumDatabase();
|
|
15
|
+
const repo = new SeptumRepository(db.raw);
|
|
16
|
+
|
|
17
|
+
if (subcommand === "resolve" && arg1 && arg2) {
|
|
18
|
+
const layer = arg1.toLowerCase() as EnvironmentLayer;
|
|
19
|
+
const platform = arg2;
|
|
20
|
+
// Fix: Preserve existing constraints instead of overwriting with empty array
|
|
21
|
+
const existing = repo.environment.getLayer(layer);
|
|
22
|
+
repo.environment.saveLayer({
|
|
23
|
+
layer,
|
|
24
|
+
platform,
|
|
25
|
+
canonical_tag: EnvironmentDetector.matchCanonicalTag(platform),
|
|
26
|
+
status: "VERIFIED",
|
|
27
|
+
detected_from: ["CLI Manual Resolution"],
|
|
28
|
+
constraints: existing?.constraints && existing.constraints.length > 0 ? existing.constraints : [
|
|
29
|
+
`Configured manually as '${platform}'.`
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
console.log(`[Septum Topology] Layer '${layer}' successfully verified as '${platform}' in SQLite SSOT.\n`);
|
|
33
|
+
} else if (subcommand === "add-node" && arg1 && arg2 && arg3) {
|
|
34
|
+
const nodeId = arg1;
|
|
35
|
+
const layer = arg2.toLowerCase() as EnvironmentLayer;
|
|
36
|
+
const platform = arg3;
|
|
37
|
+
const domainOrIp = arg4;
|
|
38
|
+
|
|
39
|
+
const node: TopologyNode = {
|
|
40
|
+
id: nodeId,
|
|
41
|
+
layer,
|
|
42
|
+
name: nodeId,
|
|
43
|
+
platform,
|
|
44
|
+
canonical_tag: EnvironmentDetector.matchCanonicalTag(platform),
|
|
45
|
+
domain_or_ip: domainOrIp,
|
|
46
|
+
status: "VERIFIED",
|
|
47
|
+
detected_from: ["CLI Manual Node Resolution"],
|
|
48
|
+
constraints: [`Node added manually with platform '${platform}'.`],
|
|
49
|
+
};
|
|
50
|
+
repo.environment.saveNode(node);
|
|
51
|
+
console.log(`[Septum Topology] Node '${nodeId}' (${layer.toUpperCase()}) added to distributed topology graph.\n`);
|
|
52
|
+
} else if (subcommand === "remove-node" && arg1) {
|
|
53
|
+
repo.environment.deleteNode(arg1);
|
|
54
|
+
console.log(`[Septum Topology] Node '${arg1}' removed from distributed topology graph.\n`);
|
|
55
|
+
} else if (subcommand === "clear") {
|
|
56
|
+
repo.environment.clearAll();
|
|
57
|
+
console.log("[Septum Topology] Cleared environment topology and nodes in SQLite SSOT.\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const topology = EnvironmentDetector.detect(cwd, repo);
|
|
61
|
+
|
|
62
|
+
console.log("================================================================================");
|
|
63
|
+
console.log("🌐 SEPTUM SELF-AWARE ENVIRONMENT TOPOLOGY PLANE");
|
|
64
|
+
console.log("================================================================================");
|
|
65
|
+
console.log(`Overall Status: ${topology.overall_status}`);
|
|
66
|
+
console.log(`Topology Mode: ${topology.is_distributed ? "DISTRIBUTED MULTI-NODE GRAPH" : "MONOLITHIC SINGLE-NODE"}`);
|
|
67
|
+
console.log(`Summary: ${topology.architectural_summary}\n`);
|
|
68
|
+
|
|
69
|
+
if (topology.nodes.length > 0) {
|
|
70
|
+
console.log("--- DISTRIBUTED TOPOLOGY NODES ---");
|
|
71
|
+
for (const node of topology.nodes) {
|
|
72
|
+
const statusIcon = node.status === "VERIFIED" ? "✓ [VERIFIED]" : node.status === "DETECTED" ? "• [DETECTED]" : "? [AMBIGUOUS]";
|
|
73
|
+
const domainStr = node.domain_or_ip ? ` @ ${node.domain_or_ip}` : "";
|
|
74
|
+
const roleStr = node.role ? ` [Role: ${node.role}]` : "";
|
|
75
|
+
console.log(` ${statusIcon} ${node.id.padEnd(20)} (${node.layer.toUpperCase()}): ${node.platform}${domainStr}${roleStr}`);
|
|
76
|
+
if (node.connected_to && node.connected_to.length > 0) {
|
|
77
|
+
console.log(` ➔ Connected to: ${node.connected_to.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
if (node.constraints && node.constraints.length > 0) {
|
|
80
|
+
for (const c of node.constraints) {
|
|
81
|
+
console.log(` - ${c}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log("--- 6-LAYER PRODUCTION SUMMARY PLANE ---");
|
|
89
|
+
for (const [layerKey, info] of Object.entries(topology.layers)) {
|
|
90
|
+
const statusIcon = info.status === "VERIFIED" ? "✓ [VERIFIED]" : info.status === "DETECTED" ? "• [DETECTED]" : "? [AMBIGUOUS]";
|
|
91
|
+
console.log(` ${statusIcon} ${layerKey.toUpperCase().padEnd(10)}: ${info.platform || "Unresolved"}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (topology.interplays.length > 0) {
|
|
95
|
+
console.log("\n--- CROSS-LAYER & DISTRIBUTED INVARIANTS ---");
|
|
96
|
+
for (const interplay of topology.interplays) {
|
|
97
|
+
console.log(` ⚡ ${interplay.title} (${interplay.involved_layers.join(" ➔ ")})`);
|
|
98
|
+
console.log(` Rule: ${interplay.risk_or_rule}`);
|
|
99
|
+
console.log(` Action: ${interplay.required_action}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (topology.pending_inquiries.length > 0) {
|
|
104
|
+
console.log("\n--- PENDING ARCHITECTURAL INQUIRIES ---");
|
|
105
|
+
console.log("Action Required: Please clarify these ambiguous layers/nodes for your AI Agent:\n");
|
|
106
|
+
for (const inquiry of topology.pending_inquiries) {
|
|
107
|
+
console.log(` ❓ ${inquiry}`);
|
|
108
|
+
}
|
|
109
|
+
console.log("\nTip: To resolve an inquiry or add distributed nodes, run:");
|
|
110
|
+
console.log(" septum topology resolve <layer> \"<Platform Name>\"");
|
|
111
|
+
console.log(" septum topology add-node <id> <layer> \"<Platform Name>\" [domain_or_ip]");
|
|
112
|
+
console.log("Example:");
|
|
113
|
+
console.log(" septum topology add-node host:vps-worker host \"Ubuntu 24.04 (Dedicated Worker)\" 10.0.0.6");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log("================================================================================\n");
|
|
117
|
+
|
|
118
|
+
db.close();
|
|
119
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { handleCheckCommand } from "./commands/check.ts";
|
|
|
5
5
|
import { handleHookCommand } from "./commands/hook.ts";
|
|
6
6
|
import { handleIngestCommand } from "./commands/ingest.ts";
|
|
7
7
|
import { handleInitCommand } from "./commands/init.ts";
|
|
8
|
+
import { handleTopologyCommand } from "./commands/topology.ts";
|
|
8
9
|
import { handleLocateCommand } from "./commands/locate.ts";
|
|
9
10
|
import { handleQueryCommand } from "./commands/query.ts";
|
|
10
11
|
import { handleServeCommand } from "./commands/serve.ts";
|
|
@@ -112,6 +113,12 @@ export async function runCLI(argv: string[]): Promise<void> {
|
|
|
112
113
|
break;
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
case "topology":
|
|
117
|
+
case "env": {
|
|
118
|
+
await handleTopologyCommand(positionals[1], positionals[2], positionals[3], positionals[4], positionals[5]);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
115
122
|
case "feature": {
|
|
116
123
|
const sub = positionals[1];
|
|
117
124
|
if (sub === "clear") {
|
|
@@ -188,6 +195,10 @@ COMMANDS:
|
|
|
188
195
|
feature [status|clear] View or release active feature context session lock
|
|
189
196
|
hook [subhook] Internal pre-tool guardrail hook (e.g. pre-write)
|
|
190
197
|
|
|
198
|
+
Environment & Topology:
|
|
199
|
+
topology [action] Inspect 6-layer environment topology & distributed nodes
|
|
200
|
+
Subcommands: resolve <layer> <platform>, add-node, remove-node, clear
|
|
201
|
+
|
|
191
202
|
Daemon & Protocol:
|
|
192
203
|
serve Start Model Context Protocol (MCP) server over stdio
|
|
193
204
|
|
|
@@ -25,11 +25,14 @@ Skill ini berakar langsung pada riset rekayasa perangkat lunak terkemuka:
|
|
|
25
25
|
|
|
26
26
|
## 2. Kapan Wajib Menggunakan (Trigger Moments)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
> [!NOTE] Zero-Config Default (\`septum init\`)
|
|
29
|
+
> Untuk mayoritas proyek (library kecil/menengah, layered monolith standar, MVC, CLI, MCP tools), pengguna atau agen **cukup menjalankan \`septum init\`** di terminal. Perintah \`septum init\` secara default berjalan 100% otomatis tanpa wawancara manual (zero-config heuristic auto-detect & auto-ingest).
|
|
30
|
+
|
|
31
|
+
Gunakan alur 4 langkah skill ini HANYA saat:
|
|
32
|
+
1. Memetakan domain bisnis kustom / modular DDD enterprise yang kompleks di mana batasan dependency non-standar perlu disepakati bersama Tech Lead.
|
|
30
33
|
2. Memetakan ulang domain bisnis setelah terjadi refactoring arsitektur besar (*re-mapping*).
|
|
31
|
-
3. Mengonfigurasi fitur baru (\`features\` session) untuk mengunci daftar file yang boleh dimodifikasi (*allowed_touchpoints*) dan simbol yang wajib dipakai ulang (*reuse_symbols*).
|
|
32
|
-
4. Pengguna
|
|
34
|
+
3. Mengonfigurasi sesi fitur baru (\`features\` session) untuk mengunci daftar file yang boleh dimodifikasi (*allowed_touchpoints*) dan simbol yang wajib dipakai ulang (*reuse_symbols*).
|
|
35
|
+
4. Pengguna secara eksplisit meminta konsultasi desain domain boundary interaktif.
|
|
33
36
|
|
|
34
37
|
---
|
|
35
38
|
|
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
2
3
|
import { Database } from "bun:sqlite";
|
|
3
4
|
import type { ValidatedSeptumConfig } from "./schema.ts";
|
|
4
5
|
import { TopologyDetector } from "../discovery/topology-detector.ts";
|
|
5
6
|
import { SeptumDatabase } from "../database/client.ts";
|
|
6
7
|
import { SeptumRepository } from "../database/repository.ts";
|
|
8
|
+
import { resolveWorkspaceRoot, isAppHostDirectory } from "../resolver/path-resolver.ts";
|
|
7
9
|
|
|
8
10
|
export class ConfigLoader {
|
|
9
11
|
/**
|
|
10
12
|
* Loads configuration with SQLite as the Single Source of Truth (SSOT).
|
|
11
13
|
* Automatically executes Zero-Config TopologyDetector if the database has no domains.
|
|
12
14
|
*/
|
|
13
|
-
public static load(
|
|
15
|
+
public static load(
|
|
16
|
+
dbPath?: string,
|
|
17
|
+
projectRoot?: string
|
|
18
|
+
): ValidatedSeptumConfig {
|
|
19
|
+
const root = resolveWorkspaceRoot(projectRoot);
|
|
20
|
+
const resolvedDbPath = dbPath
|
|
21
|
+
? (isAbsolute(dbPath) ? dbPath : join(root, dbPath))
|
|
22
|
+
: join(root, ".septum", "septum.db");
|
|
23
|
+
|
|
14
24
|
// 1. If database exists and has registered domains, load directly from SQLite SSOT
|
|
15
|
-
if (existsSync(
|
|
16
|
-
const dbConfig = ConfigLoader.loadFromDatabaseOrDefaults(
|
|
25
|
+
if (existsSync(resolvedDbPath)) {
|
|
26
|
+
const dbConfig = ConfigLoader.loadFromDatabaseOrDefaults(resolvedDbPath);
|
|
17
27
|
if (Object.keys(dbConfig.domains).length > 0) {
|
|
18
28
|
return dbConfig;
|
|
19
29
|
}
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
// 2. Zero-Config Auto-Discovery: Discover project topology and persist to SQLite
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
// Guard against running auto-discovery inside MCP host / IDE installation folders
|
|
34
|
+
if (!isAppHostDirectory(root)) {
|
|
35
|
+
const septumDb = new SeptumDatabase(resolvedDbPath);
|
|
36
|
+
const repo = new SeptumRepository(septumDb.raw);
|
|
37
|
+
TopologyDetector.discoverAndPersist(repo, root);
|
|
38
|
+
septumDb.close();
|
|
39
|
+
}
|
|
27
40
|
|
|
28
|
-
return ConfigLoader.loadFromDatabaseOrDefaults(
|
|
41
|
+
return ConfigLoader.loadFromDatabaseOrDefaults(resolvedDbPath);
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
public static getDefaultConfig(): ValidatedSeptumConfig {
|
|
@@ -276,4 +276,74 @@ export class DependencyRepository {
|
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
public getFileInboundImpact(filePath: string): {
|
|
280
|
+
direct_dependents_count: number;
|
|
281
|
+
top_consumers: string[];
|
|
282
|
+
symbols_count: number;
|
|
283
|
+
risk_level: "low" | "medium" | "high";
|
|
284
|
+
} {
|
|
285
|
+
const cleanPath = filePath.replace(/\\/g, "/");
|
|
286
|
+
const baseName = cleanPath.split("/").pop()?.replace(/\.[^.]+$/, "") || "";
|
|
287
|
+
|
|
288
|
+
const symbols = this.db
|
|
289
|
+
.query<{ name: string }, [string, string]>(
|
|
290
|
+
`SELECT s.name
|
|
291
|
+
FROM symbols s
|
|
292
|
+
JOIN files f ON s.file_id = f.id
|
|
293
|
+
WHERE f.path = ? OR f.path LIKE ?`
|
|
294
|
+
)
|
|
295
|
+
.all(cleanPath, `%${cleanPath}`);
|
|
296
|
+
|
|
297
|
+
const matchedFiles = new Set<string>();
|
|
298
|
+
|
|
299
|
+
if (baseName.length >= 3) {
|
|
300
|
+
const depRows = this.db
|
|
301
|
+
.query<{ file: string }, [string, string, string]>(
|
|
302
|
+
`SELECT DISTINCT f.path AS file
|
|
303
|
+
FROM dependencies d
|
|
304
|
+
JOIN files f ON d.source_file_id = f.id
|
|
305
|
+
WHERE (d.target_symbol_or_path LIKE ? OR d.target_symbol_or_path LIKE ?)
|
|
306
|
+
AND f.path NOT LIKE ?`
|
|
307
|
+
)
|
|
308
|
+
.all(`%${cleanPath}%`, `%${baseName}%`, `%${cleanPath}`);
|
|
309
|
+
|
|
310
|
+
for (const row of depRows) {
|
|
311
|
+
matchedFiles.add(row.file);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const sym of symbols.slice(0, 8)) {
|
|
316
|
+
const symName = sym.name.split("::").pop() || sym.name;
|
|
317
|
+
if (symName.length >= 4) {
|
|
318
|
+
const symDeps = this.db
|
|
319
|
+
.query<{ file: string }, [string, string]>(
|
|
320
|
+
`SELECT DISTINCT f.path AS file
|
|
321
|
+
FROM dependencies d
|
|
322
|
+
JOIN files f ON d.source_file_id = f.id
|
|
323
|
+
WHERE d.target_symbol_or_path LIKE ?
|
|
324
|
+
AND f.path NOT LIKE ?
|
|
325
|
+
LIMIT 10`
|
|
326
|
+
)
|
|
327
|
+
.all(`%${symName}%`, `%${cleanPath}`);
|
|
328
|
+
for (const r of symDeps) {
|
|
329
|
+
matchedFiles.add(r.file);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const consumerList = Array.from(matchedFiles);
|
|
335
|
+
let riskLevel: "low" | "medium" | "high" = "low";
|
|
336
|
+
if (consumerList.length >= 5) {
|
|
337
|
+
riskLevel = "high";
|
|
338
|
+
} else if (consumerList.length >= 2) {
|
|
339
|
+
riskLevel = "medium";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
direct_dependents_count: consumerList.length,
|
|
344
|
+
top_consumers: consumerList.slice(0, 5),
|
|
345
|
+
symbols_count: symbols.length,
|
|
346
|
+
risk_level: riskLevel,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
279
349
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { CanonicalPlatformTag, EnvironmentLayer, EnvironmentLayerInfo, TopologyNode } from "../../topology/types.ts";
|
|
3
|
+
|
|
4
|
+
export class EnvironmentRepository {
|
|
5
|
+
constructor(private db: Database) {}
|
|
6
|
+
|
|
7
|
+
public getLayer(layer: EnvironmentLayer): EnvironmentLayerInfo | null {
|
|
8
|
+
const row = this.db
|
|
9
|
+
.query<{
|
|
10
|
+
layer: string;
|
|
11
|
+
platform: string | null;
|
|
12
|
+
status: string;
|
|
13
|
+
detected_from_json: string;
|
|
14
|
+
constraints_json: string;
|
|
15
|
+
unresolved_inquiry: string | null;
|
|
16
|
+
}, [string]>("SELECT * FROM environment_topology WHERE layer = ?")
|
|
17
|
+
.get(layer);
|
|
18
|
+
|
|
19
|
+
if (!row) return null;
|
|
20
|
+
return {
|
|
21
|
+
layer: row.layer as EnvironmentLayer,
|
|
22
|
+
platform: row.platform ?? undefined,
|
|
23
|
+
status: row.status as any,
|
|
24
|
+
detected_from: JSON.parse(row.detected_from_json || "[]"),
|
|
25
|
+
constraints: JSON.parse(row.constraints_json || "[]"),
|
|
26
|
+
unresolved_inquiry: row.unresolved_inquiry ?? undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public getAllLayers(): Record<EnvironmentLayer, EnvironmentLayerInfo> {
|
|
31
|
+
const rows = this.db
|
|
32
|
+
.query<{
|
|
33
|
+
layer: string;
|
|
34
|
+
platform: string | null;
|
|
35
|
+
status: string;
|
|
36
|
+
detected_from_json: string;
|
|
37
|
+
constraints_json: string;
|
|
38
|
+
unresolved_inquiry: string | null;
|
|
39
|
+
}, []>("SELECT * FROM environment_topology")
|
|
40
|
+
.all();
|
|
41
|
+
|
|
42
|
+
const result: Partial<Record<EnvironmentLayer, EnvironmentLayerInfo>> = {};
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
result[row.layer as EnvironmentLayer] = {
|
|
45
|
+
layer: row.layer as EnvironmentLayer,
|
|
46
|
+
platform: row.platform ?? undefined,
|
|
47
|
+
status: row.status as any,
|
|
48
|
+
detected_from: JSON.parse(row.detected_from_json || "[]"),
|
|
49
|
+
constraints: JSON.parse(row.constraints_json || "[]"),
|
|
50
|
+
unresolved_inquiry: row.unresolved_inquiry ?? undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return result as Record<EnvironmentLayer, EnvironmentLayerInfo>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public saveLayer(info: EnvironmentLayerInfo): void {
|
|
57
|
+
this.db
|
|
58
|
+
.query(
|
|
59
|
+
`INSERT INTO environment_topology (
|
|
60
|
+
layer, platform, status, detected_from_json, constraints_json, unresolved_inquiry, updated_at
|
|
61
|
+
) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
62
|
+
ON CONFLICT(layer) DO UPDATE SET
|
|
63
|
+
platform = excluded.platform,
|
|
64
|
+
status = excluded.status,
|
|
65
|
+
detected_from_json = excluded.detected_from_json,
|
|
66
|
+
constraints_json = excluded.constraints_json,
|
|
67
|
+
unresolved_inquiry = excluded.unresolved_inquiry,
|
|
68
|
+
updated_at = CURRENT_TIMESTAMP`
|
|
69
|
+
)
|
|
70
|
+
.run(
|
|
71
|
+
info.layer,
|
|
72
|
+
info.platform ?? null,
|
|
73
|
+
info.status,
|
|
74
|
+
JSON.stringify(info.detected_from ?? []),
|
|
75
|
+
JSON.stringify(info.constraints ?? []),
|
|
76
|
+
info.unresolved_inquiry ?? null
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public clearAll(): void {
|
|
81
|
+
this.db.run("DELETE FROM environment_topology");
|
|
82
|
+
this.db.run("DELETE FROM environment_nodes");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Node CRUD operations for Multi-Node Topology
|
|
86
|
+
public getNode(id: string): TopologyNode | null {
|
|
87
|
+
const row = this.db
|
|
88
|
+
.query<{
|
|
89
|
+
id: string;
|
|
90
|
+
layer: string;
|
|
91
|
+
name: string;
|
|
92
|
+
platform: string;
|
|
93
|
+
canonical_tag: string;
|
|
94
|
+
domain_or_ip: string | null;
|
|
95
|
+
role: string | null;
|
|
96
|
+
status: string;
|
|
97
|
+
detected_from_json: string;
|
|
98
|
+
constraints_json: string;
|
|
99
|
+
connected_to_json: string;
|
|
100
|
+
unresolved_inquiry: string | null;
|
|
101
|
+
}, [string]>("SELECT * FROM environment_nodes WHERE id = ?")
|
|
102
|
+
.get(id);
|
|
103
|
+
|
|
104
|
+
if (!row) return null;
|
|
105
|
+
return this.mapNodeRow(row);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public getAllNodes(): TopologyNode[] {
|
|
109
|
+
const rows = this.db
|
|
110
|
+
.query<{
|
|
111
|
+
id: string;
|
|
112
|
+
layer: string;
|
|
113
|
+
name: string;
|
|
114
|
+
platform: string;
|
|
115
|
+
canonical_tag: string;
|
|
116
|
+
domain_or_ip: string | null;
|
|
117
|
+
role: string | null;
|
|
118
|
+
status: string;
|
|
119
|
+
detected_from_json: string;
|
|
120
|
+
constraints_json: string;
|
|
121
|
+
connected_to_json: string;
|
|
122
|
+
unresolved_inquiry: string | null;
|
|
123
|
+
}, []>("SELECT * FROM environment_nodes ORDER BY layer, id")
|
|
124
|
+
.all();
|
|
125
|
+
|
|
126
|
+
return rows.map((r) => this.mapNodeRow(r));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public getNodesByLayer(layer: EnvironmentLayer): TopologyNode[] {
|
|
130
|
+
const rows = this.db
|
|
131
|
+
.query<{
|
|
132
|
+
id: string;
|
|
133
|
+
layer: string;
|
|
134
|
+
name: string;
|
|
135
|
+
platform: string;
|
|
136
|
+
canonical_tag: string;
|
|
137
|
+
domain_or_ip: string | null;
|
|
138
|
+
role: string | null;
|
|
139
|
+
status: string;
|
|
140
|
+
detected_from_json: string;
|
|
141
|
+
constraints_json: string;
|
|
142
|
+
connected_to_json: string;
|
|
143
|
+
unresolved_inquiry: string | null;
|
|
144
|
+
}, [string]>("SELECT * FROM environment_nodes WHERE layer = ? ORDER BY id")
|
|
145
|
+
.all(layer);
|
|
146
|
+
|
|
147
|
+
return rows.map((r) => this.mapNodeRow(r));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public saveNode(node: TopologyNode): void {
|
|
151
|
+
this.db
|
|
152
|
+
.query(
|
|
153
|
+
`INSERT INTO environment_nodes (
|
|
154
|
+
id, layer, name, platform, canonical_tag, domain_or_ip, role, status,
|
|
155
|
+
detected_from_json, constraints_json, connected_to_json, unresolved_inquiry, updated_at
|
|
156
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
157
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
158
|
+
layer = excluded.layer,
|
|
159
|
+
name = excluded.name,
|
|
160
|
+
platform = excluded.platform,
|
|
161
|
+
canonical_tag = excluded.canonical_tag,
|
|
162
|
+
domain_or_ip = excluded.domain_or_ip,
|
|
163
|
+
role = excluded.role,
|
|
164
|
+
status = excluded.status,
|
|
165
|
+
detected_from_json = excluded.detected_from_json,
|
|
166
|
+
constraints_json = excluded.constraints_json,
|
|
167
|
+
connected_to_json = excluded.connected_to_json,
|
|
168
|
+
unresolved_inquiry = excluded.unresolved_inquiry,
|
|
169
|
+
updated_at = CURRENT_TIMESTAMP`
|
|
170
|
+
)
|
|
171
|
+
.run(
|
|
172
|
+
node.id,
|
|
173
|
+
node.layer,
|
|
174
|
+
node.name,
|
|
175
|
+
node.platform,
|
|
176
|
+
node.canonical_tag || "unspecified",
|
|
177
|
+
node.domain_or_ip ?? null,
|
|
178
|
+
node.role ?? null,
|
|
179
|
+
node.status,
|
|
180
|
+
JSON.stringify(node.detected_from ?? []),
|
|
181
|
+
JSON.stringify(node.constraints ?? []),
|
|
182
|
+
JSON.stringify(node.connected_to ?? []),
|
|
183
|
+
node.unresolved_inquiry ?? null
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public deleteNode(id: string): void {
|
|
188
|
+
this.db.query("DELETE FROM environment_nodes WHERE id = ?").run(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private mapNodeRow(row: any): TopologyNode {
|
|
192
|
+
return {
|
|
193
|
+
id: row.id,
|
|
194
|
+
layer: row.layer as EnvironmentLayer,
|
|
195
|
+
name: row.name,
|
|
196
|
+
platform: row.platform,
|
|
197
|
+
canonical_tag: (row.canonical_tag as CanonicalPlatformTag) || "unspecified",
|
|
198
|
+
domain_or_ip: row.domain_or_ip ?? undefined,
|
|
199
|
+
role: row.role ?? undefined,
|
|
200
|
+
status: row.status as any,
|
|
201
|
+
detected_from: JSON.parse(row.detected_from_json || "[]"),
|
|
202
|
+
constraints: JSON.parse(row.constraints_json || "[]"),
|
|
203
|
+
connected_to: JSON.parse(row.connected_to_json || "[]"),
|
|
204
|
+
unresolved_inquiry: row.unresolved_inquiry ?? undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -22,6 +22,13 @@ export class FileRepository {
|
|
|
22
22
|
.all(domainId);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
public countFiles(): number {
|
|
26
|
+
const row = this.db
|
|
27
|
+
.query<{ count: number }, []>("SELECT COUNT(*) as count FROM files")
|
|
28
|
+
.get();
|
|
29
|
+
return row?.count ?? 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
public upsertFile(
|
|
26
33
|
domainId: number,
|
|
27
34
|
path: string,
|