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.
Files changed (46) hide show
  1. package/README.md +7 -2
  2. package/package.json +1 -1
  3. package/src/cli/commands/slice.ts +4 -46
  4. package/src/cli/commands/topology.ts +119 -0
  5. package/src/cli/index.ts +11 -0
  6. package/src/cli/templates/skill.ts +7 -4
  7. package/src/core/config/loader.ts +21 -8
  8. package/src/core/database/repositories/dependency.repository.ts +70 -0
  9. package/src/core/database/repositories/environment.repository.ts +207 -0
  10. package/src/core/database/repositories/file.repository.ts +7 -0
  11. package/src/core/database/repositories/symbol.repository.ts +28 -9
  12. package/src/core/database/repository.ts +19 -1
  13. package/src/core/database/schema.ts +153 -120
  14. package/src/core/discovery/topology-detector.ts +71 -3
  15. package/src/core/ingestion/pipeline.ts +98 -1
  16. package/src/core/parser/behavioral-analyzer.ts +119 -0
  17. package/src/core/parser/boundary-tracker.ts +160 -0
  18. package/src/core/parser/extractors/base.ts +2 -0
  19. package/src/core/parser/extractors/blade.ts +158 -0
  20. package/src/core/parser/extractors/go.ts +5 -1
  21. package/src/core/parser/extractors/laravel-semantic.ts +506 -31
  22. package/src/core/parser/extractors/php.ts +5 -1
  23. package/src/core/parser/extractors/python.ts +3 -1
  24. package/src/core/parser/extractors/registry.ts +21 -0
  25. package/src/core/parser/extractors/typescript.ts +5 -1
  26. package/src/core/parser/schema/providers/laravel-migration-provider.ts +215 -0
  27. package/src/core/parser/schema/providers/prisma-schema-provider.ts +89 -0
  28. package/src/core/parser/schema/schema-provider-registry.ts +43 -0
  29. package/src/core/parser/schema/schema-provider.interface.ts +33 -0
  30. package/src/core/parser/tree-sitter.ts +2 -0
  31. package/src/core/resolver/path-resolver.ts +147 -0
  32. package/src/core/resolver/vertical-slice-tracer.ts +56 -8
  33. package/src/core/topology/environment-detector.ts +860 -0
  34. package/src/core/topology/types.ts +83 -0
  35. package/src/index.ts +3 -0
  36. package/src/mcp/schemas.ts +264 -46
  37. package/src/mcp/server.ts +130 -26
  38. package/src/mcp/tools/check-boundary.ts +58 -8
  39. package/src/mcp/tools/get-domain-catalog.ts +31 -1
  40. package/src/mcp/tools/get-environment-topology.ts +141 -0
  41. package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
  42. package/src/mcp/tools/locate-symbol.ts +119 -3
  43. package/src/mcp/tools/register-domain.ts +1 -1
  44. package/src/mcp/tools/trace-vertical-slice.ts +11 -65
  45. package/src/types/index.ts +20 -1
  46. 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
+ [![Version: 0.1.4](https://img.shields.io/badge/Version-0.1.4-blue)](package.json)
5
6
  [![Runtime: Bun](https://img.shields.io/badge/Runtime-Bun-black?logo=bun)](https://bun.sh)
6
7
  [![Protocol: Model Context Protocol](https://img.shields.io/badge/Protocol-MCP-green)](https://modelcontextprotocol.io/)
7
8
  [![Database: SQLite WAL](https://img.shields.io/badge/Database-SQLite%20(bun:sqlite)-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*) across frameworks.
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-of-code.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "septum",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Deterministic Bounded-Context, Structural File Catalog & Anti-Poisoning Partition for AI Agents",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -20,57 +20,15 @@ export async function handleSliceCommand(
20
20
  return;
21
21
  }
22
22
 
23
- if (result.found && result.slice) {
24
- const s = result.slice;
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(`\n • Alternative Matches:`);
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
- Gunakan skill ini saat:
29
- 1. Menghubungkan Septum pertama kali ke repositori baru atau codebase yang belum terpetakan.
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 meminta inisialisasi cerdas berbasis pemahaman AI pada struktur proyek.
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(dbPath: string = ".septum/septum.db"): ValidatedSeptumConfig {
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(dbPath)) {
16
- const dbConfig = ConfigLoader.loadFromDatabaseOrDefaults(dbPath);
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
- const septumDb = new SeptumDatabase(dbPath);
24
- const repo = new SeptumRepository(septumDb.raw);
25
- TopologyDetector.discoverAndPersist(repo, process.cwd());
26
- septumDb.close();
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(dbPath);
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,