septum 0.1.3 → 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 +5 -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/core/database/repositories/environment.repository.ts +207 -0
- package/src/core/database/repositories/symbol.repository.ts +28 -9
- package/src/core/database/repository.ts +6 -1
- package/src/core/database/schema.ts +153 -120
- 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 +1 -0
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/go.ts +5 -1
- package/src/core/parser/extractors/laravel-semantic.ts +320 -4
- package/src/core/parser/extractors/php.ts +5 -1
- package/src/core/parser/extractors/python.ts +3 -1
- 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/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/vertical-slice-tracer.ts +34 -1
- 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 +48 -0
- package/src/mcp/server.ts +62 -2
- package/src/mcp/tools/get-environment-topology.ts +141 -0
- package/src/mcp/tools/get-symbol-hotspots.ts +13 -3
- package/src/mcp/tools/register-domain.ts +1 -1
- package/src/mcp/tools/trace-vertical-slice.ts +9 -64
- package/src/types/index.ts +16 -0
- 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/)
|
|
@@ -63,6 +64,7 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
|
|
|
63
64
|
| `septum query [domain]` | Inspect domain catalog, archetypes, and public signatures |
|
|
64
65
|
| `septum locate <query>` | Locate symbol, class, interface, method, or error source |
|
|
65
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 |
|
|
66
68
|
| `septum feature status\|clear` | Manage active feature lease and editing scope |
|
|
67
69
|
| `septum serve` | Launch stdio MCP server for AI coding agents |
|
|
68
70
|
|
|
@@ -72,12 +74,13 @@ Add Septum to your MCP client (`mcp.json` or `claude_desktop_config.json`):
|
|
|
72
74
|
|
|
73
75
|
All tools expose typed schemas automatically via the Model Context Protocol:
|
|
74
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.
|
|
75
78
|
- **Whole-Project Telescope (`septum_get_domain_catalog`):** Returns macro architecture maps in < 400 tokens.
|
|
76
|
-
- **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*).
|
|
77
80
|
- **Pre-Flight Boundary Guard (`septum_check_boundary`):** Validates proposed edits and imports against bounded-context rules.
|
|
78
81
|
- **Scope Locking (`septum_get_feature_context` / `septum_clear_feature_context`):** Locks active feature scope to eliminate context drift.
|
|
79
82
|
- **Symbol & Blast Radius (`septum_locate_symbol`, `septum_get_symbol`, `septum_get_symbol_impact`):** Resolves call chains, signatures, and refactoring impact.
|
|
80
|
-
- **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.
|
|
81
84
|
- **Dynamic Topology (`septum_register_domain`):** Registers or updates domains and boundary rules on-the-fly.
|
|
82
85
|
|
|
83
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
|
|
|
@@ -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
|
+
}
|
|
@@ -17,12 +17,13 @@ export class SymbolRepository {
|
|
|
17
17
|
if (symbols.length === 0) return;
|
|
18
18
|
|
|
19
19
|
const stmt = this.db.query(
|
|
20
|
-
`INSERT INTO symbols (file_id, name, kind, signature, visibility, line_start, line_end, line_count)
|
|
21
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
20
|
+
`INSERT INTO symbols (file_id, name, kind, signature, visibility, line_start, line_end, line_count, nesting_depth)
|
|
21
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
22
22
|
);
|
|
23
23
|
|
|
24
24
|
for (const sym of symbols) {
|
|
25
25
|
const lineCount = sym.line_count ?? Math.max(1, sym.line_end - sym.line_start + 1);
|
|
26
|
+
const nestingDepth = sym.nesting_depth ?? 1;
|
|
26
27
|
stmt.run(
|
|
27
28
|
fileId,
|
|
28
29
|
sym.name,
|
|
@@ -31,7 +32,8 @@ export class SymbolRepository {
|
|
|
31
32
|
sym.visibility,
|
|
32
33
|
sym.line_start,
|
|
33
34
|
sym.line_end,
|
|
34
|
-
lineCount
|
|
35
|
+
lineCount,
|
|
36
|
+
nestingDepth
|
|
35
37
|
);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -98,6 +100,7 @@ export class SymbolRepository {
|
|
|
98
100
|
line_start: r.line_start,
|
|
99
101
|
line_end: r.line_end,
|
|
100
102
|
line_count: r.line_count ?? (r.line_end - r.line_start + 1),
|
|
103
|
+
nesting_depth: r.nesting_depth ?? 1,
|
|
101
104
|
},
|
|
102
105
|
}));
|
|
103
106
|
}
|
|
@@ -225,12 +228,16 @@ export class SymbolRepository {
|
|
|
225
228
|
|
|
226
229
|
public getHotspotSymbols(options: {
|
|
227
230
|
minLines?: number;
|
|
231
|
+
minNesting?: number;
|
|
228
232
|
kind?: SymbolKind;
|
|
229
233
|
domain?: string;
|
|
230
234
|
limit?: number;
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
sortBy?: "lines" | "nesting" | "risk_score";
|
|
236
|
+
} = {}): Array<SymbolRecord & { file_path: string; domain_name: string; risk_score: number }> {
|
|
237
|
+
const minLines = options.minLines ?? 0;
|
|
238
|
+
const minNesting = options.minNesting ?? 1;
|
|
233
239
|
const limit = options.limit ?? 50;
|
|
240
|
+
const sortBy = options.sortBy ?? "risk_score";
|
|
234
241
|
|
|
235
242
|
let sql = `
|
|
236
243
|
SELECT
|
|
@@ -243,15 +250,16 @@ export class SymbolRepository {
|
|
|
243
250
|
s.line_start,
|
|
244
251
|
s.line_end,
|
|
245
252
|
s.line_count,
|
|
253
|
+
s.nesting_depth,
|
|
246
254
|
f.path AS file_path,
|
|
247
255
|
d.name AS domain_name
|
|
248
256
|
FROM symbols s
|
|
249
257
|
JOIN files f ON s.file_id = f.id
|
|
250
258
|
JOIN domains d ON f.domain_id = d.id
|
|
251
|
-
WHERE s.line_count >= ?
|
|
259
|
+
WHERE s.line_count >= ? AND s.nesting_depth >= ?
|
|
252
260
|
`;
|
|
253
261
|
|
|
254
|
-
const params: any[] = [minLines];
|
|
262
|
+
const params: any[] = [minLines, minNesting];
|
|
255
263
|
|
|
256
264
|
if (options.kind) {
|
|
257
265
|
sql += " AND s.kind = ?";
|
|
@@ -263,10 +271,21 @@ export class SymbolRepository {
|
|
|
263
271
|
params.push(options.domain);
|
|
264
272
|
}
|
|
265
273
|
|
|
266
|
-
|
|
274
|
+
if (sortBy === "nesting") {
|
|
275
|
+
sql += " ORDER BY s.nesting_depth DESC, s.line_count DESC, s.name ASC LIMIT ?";
|
|
276
|
+
} else if (sortBy === "lines") {
|
|
277
|
+
sql += " ORDER BY s.line_count DESC, s.nesting_depth DESC, s.name ASC LIMIT ?";
|
|
278
|
+
} else {
|
|
279
|
+
sql += " ORDER BY (s.line_count * (1.0 + s.nesting_depth * 0.3)) DESC, s.nesting_depth DESC, s.name ASC LIMIT ?";
|
|
280
|
+
}
|
|
267
281
|
params.push(limit);
|
|
268
282
|
|
|
269
|
-
|
|
283
|
+
const rows = this.db.query<any, any[]>(sql).all(...params);
|
|
284
|
+
return rows.map((r) => ({
|
|
285
|
+
...r,
|
|
286
|
+
nesting_depth: r.nesting_depth ?? 1,
|
|
287
|
+
risk_score: Math.round((r.line_count ?? 1) * (1.0 + (r.nesting_depth ?? 1) * 0.3)),
|
|
288
|
+
}));
|
|
270
289
|
}
|
|
271
290
|
|
|
272
291
|
public getSymbolDetails(
|
|
@@ -32,6 +32,7 @@ export type {
|
|
|
32
32
|
} from "../../types/index.ts";
|
|
33
33
|
import { DependencyRepository } from "./repositories/dependency.repository.ts";
|
|
34
34
|
import { DomainRepository } from "./repositories/domain.repository.ts";
|
|
35
|
+
import { EnvironmentRepository } from "./repositories/environment.repository.ts";
|
|
35
36
|
import { FileRepository } from "./repositories/file.repository.ts";
|
|
36
37
|
import { MetaRepository } from "./repositories/meta.repository.ts";
|
|
37
38
|
import { SymbolRepository } from "./repositories/symbol.repository.ts";
|
|
@@ -44,6 +45,7 @@ export class SeptumRepository {
|
|
|
44
45
|
public readonly symbols: SymbolRepository;
|
|
45
46
|
public readonly dependencies: DependencyRepository;
|
|
46
47
|
public readonly meta: MetaRepository;
|
|
48
|
+
public readonly environment: EnvironmentRepository;
|
|
47
49
|
|
|
48
50
|
constructor(private db: Database) {
|
|
49
51
|
this.domains = new DomainRepository(db);
|
|
@@ -51,6 +53,7 @@ export class SeptumRepository {
|
|
|
51
53
|
this.symbols = new SymbolRepository(db);
|
|
52
54
|
this.dependencies = new DependencyRepository(db);
|
|
53
55
|
this.meta = new MetaRepository(db);
|
|
56
|
+
this.environment = new EnvironmentRepository(db);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
public runInTransaction<T>(fn: () => T): T {
|
|
@@ -212,10 +215,12 @@ export class SeptumRepository {
|
|
|
212
215
|
|
|
213
216
|
public getHotspotSymbols(options: {
|
|
214
217
|
minLines?: number;
|
|
218
|
+
minNesting?: number;
|
|
215
219
|
kind?: SymbolKind;
|
|
216
220
|
domain?: string;
|
|
217
221
|
limit?: number;
|
|
218
|
-
|
|
222
|
+
sortBy?: "lines" | "nesting" | "risk_score";
|
|
223
|
+
} = {}): Array<SymbolRecord & { file_path: string; domain_name: string; risk_score: number }> {
|
|
219
224
|
return this.symbols.getHotspotSymbols(options);
|
|
220
225
|
}
|
|
221
226
|
|