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,56 +1,56 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { ActiveFeatureSession } from "../../types/index.ts";
|
|
4
|
-
|
|
5
|
-
export class SessionManager {
|
|
6
|
-
private static readonly SESSION_FILE = ".septum/session.json";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Save the active feature session to .septum/session.json
|
|
10
|
-
*/
|
|
11
|
-
public static saveActiveSession(workspaceRoot: string, session: ActiveFeatureSession): void {
|
|
12
|
-
const septumDir = join(workspaceRoot, ".septum");
|
|
13
|
-
if (!existsSync(septumDir)) {
|
|
14
|
-
mkdirSync(septumDir, { recursive: true });
|
|
15
|
-
}
|
|
16
|
-
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
17
|
-
writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Get the active feature session, or null if no session exists or file is invalid
|
|
22
|
-
*/
|
|
23
|
-
public static getActiveSession(workspaceRoot: string): ActiveFeatureSession | null {
|
|
24
|
-
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
25
|
-
if (!existsSync(sessionPath)) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const raw = readFileSync(sessionPath, "utf-8");
|
|
31
|
-
const parsed = JSON.parse(raw) as ActiveFeatureSession;
|
|
32
|
-
if (parsed && typeof parsed.feature_key === "string" && Array.isArray(parsed.touchpoints)) {
|
|
33
|
-
return parsed;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Clear the active feature session
|
|
43
|
-
*/
|
|
44
|
-
public static clearActiveSession(workspaceRoot: string): boolean {
|
|
45
|
-
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
46
|
-
if (existsSync(sessionPath)) {
|
|
47
|
-
try {
|
|
48
|
-
rmSync(sessionPath, { force: true });
|
|
49
|
-
return true;
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ActiveFeatureSession } from "../../types/index.ts";
|
|
4
|
+
|
|
5
|
+
export class SessionManager {
|
|
6
|
+
private static readonly SESSION_FILE = ".septum/session.json";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Save the active feature session to .septum/session.json
|
|
10
|
+
*/
|
|
11
|
+
public static saveActiveSession(workspaceRoot: string, session: ActiveFeatureSession): void {
|
|
12
|
+
const septumDir = join(workspaceRoot, ".septum");
|
|
13
|
+
if (!existsSync(septumDir)) {
|
|
14
|
+
mkdirSync(septumDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
17
|
+
writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the active feature session, or null if no session exists or file is invalid
|
|
22
|
+
*/
|
|
23
|
+
public static getActiveSession(workspaceRoot: string): ActiveFeatureSession | null {
|
|
24
|
+
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
25
|
+
if (!existsSync(sessionPath)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const raw = readFileSync(sessionPath, "utf-8");
|
|
31
|
+
const parsed = JSON.parse(raw) as ActiveFeatureSession;
|
|
32
|
+
if (parsed && typeof parsed.feature_key === "string" && Array.isArray(parsed.touchpoints)) {
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Clear the active feature session
|
|
43
|
+
*/
|
|
44
|
+
public static clearActiveSession(workspaceRoot: string): boolean {
|
|
45
|
+
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
46
|
+
if (existsSync(sessionPath)) {
|
|
47
|
+
try {
|
|
48
|
+
rmSync(sessionPath, { force: true });
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface SeptumToolCallEvent {
|
|
5
|
-
timestamp?: string;
|
|
6
|
-
event: "tool_call_completed" | "tool_call_failed";
|
|
7
|
-
tool: string;
|
|
8
|
-
duration_ms: number;
|
|
9
|
-
input: Record<string, any>;
|
|
10
|
-
metrics?: {
|
|
11
|
-
bytes_out?: number;
|
|
12
|
-
lines_out?: number;
|
|
13
|
-
nodes_returned?: number;
|
|
14
|
-
};
|
|
15
|
-
session?: {
|
|
16
|
-
feature_key?: string | null;
|
|
17
|
-
domain?: string | null;
|
|
18
|
-
} | null;
|
|
19
|
-
status: "success" | "error";
|
|
20
|
-
error?: {
|
|
21
|
-
code?: string;
|
|
22
|
-
message: string;
|
|
23
|
-
stack?: string;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class SeptumTelemetry {
|
|
28
|
-
private static readonly MAX_LOG_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
|
29
|
-
private static readonly SLOW_EXECUTION_THRESHOLD_MS = 250;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolves the log file path inside the active workspace's .septum directory.
|
|
33
|
-
*/
|
|
34
|
-
public static getLogPath(workspaceRoot: string = process.cwd()): string {
|
|
35
|
-
return join(workspaceRoot, ".septum", "septum.log");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Records a tool call event to both stderr (dual-sink) and .septum/septum.log.
|
|
40
|
-
*/
|
|
41
|
-
public static recordToolCall(
|
|
42
|
-
event: SeptumToolCallEvent,
|
|
43
|
-
workspaceRoot: string = process.cwd()
|
|
44
|
-
): void {
|
|
45
|
-
const payload: SeptumToolCallEvent = {
|
|
46
|
-
timestamp: event.timestamp || new Date().toISOString(),
|
|
47
|
-
...event,
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const jsonLine = JSON.stringify(payload);
|
|
51
|
-
|
|
52
|
-
// Sink 1: Live stderr stream for operator monitoring (slow queries & failures)
|
|
53
|
-
if (payload.status === "error") {
|
|
54
|
-
console.error(`[Septum Telemetry ERROR] ${jsonLine}`);
|
|
55
|
-
} else if (payload.duration_ms >= this.SLOW_EXECUTION_THRESHOLD_MS) {
|
|
56
|
-
console.error(`[Septum Telemetry SLOW_WARN] ${jsonLine}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Sink 2: Append-only persistent file recorder in .septum/septum.log
|
|
60
|
-
try {
|
|
61
|
-
const logPath = this.getLogPath(workspaceRoot);
|
|
62
|
-
const logDir = dirname(logPath);
|
|
63
|
-
|
|
64
|
-
if (!existsSync(logDir)) {
|
|
65
|
-
mkdirSync(logDir, { recursive: true });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check for rotation if file exceeds 2MB
|
|
69
|
-
if (existsSync(logPath)) {
|
|
70
|
-
try {
|
|
71
|
-
const stats = statSync(logPath);
|
|
72
|
-
if (stats.size >= this.MAX_LOG_SIZE_BYTES) {
|
|
73
|
-
const rotatedPath = `${logPath}.1`;
|
|
74
|
-
renameSync(logPath, rotatedPath);
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
// If stat/rotation fails, proceed to append without crashing
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
appendFileSync(logPath, `${jsonLine}\n`, "utf8");
|
|
82
|
-
} catch {
|
|
83
|
-
// Telemetry file writes must never crash the main application process
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SeptumToolCallEvent {
|
|
5
|
+
timestamp?: string;
|
|
6
|
+
event: "tool_call_completed" | "tool_call_failed";
|
|
7
|
+
tool: string;
|
|
8
|
+
duration_ms: number;
|
|
9
|
+
input: Record<string, any>;
|
|
10
|
+
metrics?: {
|
|
11
|
+
bytes_out?: number;
|
|
12
|
+
lines_out?: number;
|
|
13
|
+
nodes_returned?: number;
|
|
14
|
+
};
|
|
15
|
+
session?: {
|
|
16
|
+
feature_key?: string | null;
|
|
17
|
+
domain?: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
status: "success" | "error";
|
|
20
|
+
error?: {
|
|
21
|
+
code?: string;
|
|
22
|
+
message: string;
|
|
23
|
+
stack?: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SeptumTelemetry {
|
|
28
|
+
private static readonly MAX_LOG_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
|
|
29
|
+
private static readonly SLOW_EXECUTION_THRESHOLD_MS = 250;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the log file path inside the active workspace's .septum directory.
|
|
33
|
+
*/
|
|
34
|
+
public static getLogPath(workspaceRoot: string = process.cwd()): string {
|
|
35
|
+
return join(workspaceRoot, ".septum", "septum.log");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Records a tool call event to both stderr (dual-sink) and .septum/septum.log.
|
|
40
|
+
*/
|
|
41
|
+
public static recordToolCall(
|
|
42
|
+
event: SeptumToolCallEvent,
|
|
43
|
+
workspaceRoot: string = process.cwd()
|
|
44
|
+
): void {
|
|
45
|
+
const payload: SeptumToolCallEvent = {
|
|
46
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
47
|
+
...event,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const jsonLine = JSON.stringify(payload);
|
|
51
|
+
|
|
52
|
+
// Sink 1: Live stderr stream for operator monitoring (slow queries & failures)
|
|
53
|
+
if (payload.status === "error") {
|
|
54
|
+
console.error(`[Septum Telemetry ERROR] ${jsonLine}`);
|
|
55
|
+
} else if (payload.duration_ms >= this.SLOW_EXECUTION_THRESHOLD_MS) {
|
|
56
|
+
console.error(`[Septum Telemetry SLOW_WARN] ${jsonLine}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sink 2: Append-only persistent file recorder in .septum/septum.log
|
|
60
|
+
try {
|
|
61
|
+
const logPath = this.getLogPath(workspaceRoot);
|
|
62
|
+
const logDir = dirname(logPath);
|
|
63
|
+
|
|
64
|
+
if (!existsSync(logDir)) {
|
|
65
|
+
mkdirSync(logDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for rotation if file exceeds 2MB
|
|
69
|
+
if (existsSync(logPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const stats = statSync(logPath);
|
|
72
|
+
if (stats.size >= this.MAX_LOG_SIZE_BYTES) {
|
|
73
|
+
const rotatedPath = `${logPath}.1`;
|
|
74
|
+
renameSync(logPath, rotatedPath);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// If stat/rotation fails, proceed to append without crashing
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
appendFileSync(logPath, `${jsonLine}\n`, "utf8");
|
|
82
|
+
} catch {
|
|
83
|
+
// Telemetry file writes must never crash the main application process
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -556,22 +556,120 @@ export class EnvironmentDetector {
|
|
|
556
556
|
const realEnv = this.parseEnvFileSafe(path.join(root, ".env"));
|
|
557
557
|
const mergedEnv = new Map([...envMap, ...realEnv]);
|
|
558
558
|
const detectedStorage: string[] = [];
|
|
559
|
+
const detectedFrom = new Set<string>();
|
|
559
560
|
|
|
560
561
|
const dbConn = mergedEnv.get("DB_CONNECTION")?.toLowerCase() || "";
|
|
561
562
|
const dbPort = mergedEnv.get("DB_PORT") || "";
|
|
562
563
|
|
|
563
|
-
if (dbConn === "mysql" || dbPort === "3306" || [...mergedEnv.keys()].some(k => k.startsWith("MYSQL_"))) {
|
|
564
|
+
if (dbConn === "mysql" || dbPort === "3306" || [...mergedEnv.keys()].some((k) => k.startsWith("MYSQL_"))) {
|
|
564
565
|
detectedStorage.push("MySQL / MariaDB");
|
|
565
|
-
|
|
566
|
+
detectedFrom.add(".env");
|
|
567
|
+
} else if (
|
|
568
|
+
dbConn === "pgsql" ||
|
|
569
|
+
dbPort === "5432" ||
|
|
570
|
+
[...mergedEnv.keys()].some((k) => k.startsWith("POSTGRES_"))
|
|
571
|
+
) {
|
|
566
572
|
detectedStorage.push("PostgreSQL");
|
|
573
|
+
detectedFrom.add(".env");
|
|
567
574
|
} else if (dbConn === "sqlite" || fs.existsSync(path.join(root, "database", "database.sqlite"))) {
|
|
568
575
|
detectedStorage.push("SQLite (File-based)");
|
|
576
|
+
detectedFrom.add(dbConn === "sqlite" ? ".env" : "database/database.sqlite");
|
|
569
577
|
}
|
|
570
578
|
|
|
571
579
|
const queueConn = mergedEnv.get("QUEUE_CONNECTION")?.toLowerCase() || "";
|
|
572
580
|
const redisHost = mergedEnv.get("REDIS_HOST");
|
|
573
581
|
if (queueConn === "redis" || redisHost || mergedEnv.has("REDIS_CLIENT")) {
|
|
574
582
|
detectedStorage.push("Redis (Cache & Queues)");
|
|
583
|
+
detectedFrom.add(".env");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Inspect package.json dependencies & devDependencies
|
|
587
|
+
const packageJson = this.readJsonSafe(path.join(root, "package.json"));
|
|
588
|
+
const allPkgDeps: Record<string, string> = {
|
|
589
|
+
...(packageJson?.dependencies || {}),
|
|
590
|
+
...(packageJson?.devDependencies || {}),
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const addStorageUnique = (label: string, source: string): void => {
|
|
594
|
+
if (!detectedStorage.some((s) => s.toLowerCase().startsWith(label.split(" ")[0].toLowerCase()))) {
|
|
595
|
+
detectedStorage.push(label);
|
|
596
|
+
}
|
|
597
|
+
detectedFrom.add(source);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
if (allPkgDeps["mysql2"] || allPkgDeps["mysql"]) {
|
|
601
|
+
addStorageUnique("MySQL / MariaDB", "package.json");
|
|
602
|
+
}
|
|
603
|
+
if (allPkgDeps["pg"] || allPkgDeps["postgres"]) {
|
|
604
|
+
addStorageUnique("PostgreSQL", "package.json");
|
|
605
|
+
}
|
|
606
|
+
if (allPkgDeps["better-sqlite3"] || allPkgDeps["sqlite3"] || allPkgDeps["@libsql/client"]) {
|
|
607
|
+
addStorageUnique("SQLite (Embedded / Local WAL)", "package.json");
|
|
608
|
+
}
|
|
609
|
+
if (allPkgDeps["redis"] || allPkgDeps["ioredis"]) {
|
|
610
|
+
addStorageUnique("Redis (Cache & Queues)", "package.json");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Inspect go.mod for Go database drivers
|
|
614
|
+
const goMod = this.readFileSafe(path.join(root, "go.mod"));
|
|
615
|
+
if (goMod) {
|
|
616
|
+
if (goMod.includes("go-sql-driver/mysql") || goMod.includes("driver/mysql")) {
|
|
617
|
+
addStorageUnique("MySQL / MariaDB", "go.mod");
|
|
618
|
+
}
|
|
619
|
+
if (goMod.includes("lib/pq") || goMod.includes("jackc/pgx") || goMod.includes("driver/postgres")) {
|
|
620
|
+
addStorageUnique("PostgreSQL", "go.mod");
|
|
621
|
+
}
|
|
622
|
+
if (
|
|
623
|
+
goMod.includes("mattn/go-sqlite3") ||
|
|
624
|
+
goMod.includes("modernc.org/sqlite") ||
|
|
625
|
+
goMod.includes("driver/sqlite")
|
|
626
|
+
) {
|
|
627
|
+
addStorageUnique("SQLite (Embedded / Local WAL)", "go.mod");
|
|
628
|
+
}
|
|
629
|
+
if (goMod.includes("go-redis")) {
|
|
630
|
+
addStorageUnique("Redis (Cache & Queues)", "go.mod");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Inspect .septum/septum.db or bun:sqlite usage in source
|
|
635
|
+
const hasSeptumDb = fs.existsSync(path.join(root, ".septum", "septum.db"));
|
|
636
|
+
const hasBunSqliteConn =
|
|
637
|
+
fs.existsSync(path.join(root, "src", "core", "database", "connection.ts")) &&
|
|
638
|
+
this.readFileSafe(path.join(root, "src", "core", "database", "connection.ts")).includes("bun:sqlite");
|
|
639
|
+
|
|
640
|
+
if (hasSeptumDb || hasBunSqliteConn) {
|
|
641
|
+
addStorageUnique(
|
|
642
|
+
"SQLite (Embedded / Local WAL)",
|
|
643
|
+
hasBunSqliteConn ? "src/core/database/connection.ts (bun:sqlite)" : ".septum/septum.db"
|
|
644
|
+
);
|
|
645
|
+
} else if (allPkgDeps["@types/bun"] || packageJson?.engines?.bun) {
|
|
646
|
+
const entryFiles = [
|
|
647
|
+
path.join(root, "src", "index.ts"),
|
|
648
|
+
path.join(root, "src", "db.ts"),
|
|
649
|
+
path.join(root, "src", "database.ts"),
|
|
650
|
+
];
|
|
651
|
+
for (const ef of entryFiles) {
|
|
652
|
+
if (fs.existsSync(ef) && this.readFileSafe(ef).includes("bun:sqlite")) {
|
|
653
|
+
addStorageUnique("SQLite (Embedded / Local WAL)", path.relative(root, ef));
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (
|
|
660
|
+
detectedStorage.length === 0 &&
|
|
661
|
+
(allPkgDeps["@prisma/client"] || allPkgDeps["drizzle-orm"])
|
|
662
|
+
) {
|
|
663
|
+
const prismaSchema = this.readFileSafe(path.join(root, "prisma", "schema.prisma"));
|
|
664
|
+
if (prismaSchema.includes('"sqlite"')) {
|
|
665
|
+
addStorageUnique("SQLite (Embedded / Local WAL)", "prisma/schema.prisma");
|
|
666
|
+
} else if (prismaSchema.includes('"postgresql"')) {
|
|
667
|
+
addStorageUnique("PostgreSQL", "prisma/schema.prisma");
|
|
668
|
+
} else if (prismaSchema.includes('"mysql"')) {
|
|
669
|
+
addStorageUnique("MySQL / MariaDB", "prisma/schema.prisma");
|
|
670
|
+
} else {
|
|
671
|
+
addStorageUnique("SQL Database (Prisma / Drizzle ORM)", "package.json");
|
|
672
|
+
}
|
|
575
673
|
}
|
|
576
674
|
|
|
577
675
|
if (detectedStorage.length > 0) {
|
|
@@ -580,7 +678,7 @@ export class EnvironmentDetector {
|
|
|
580
678
|
platform: detectedStorage.join(" + "),
|
|
581
679
|
canonical_tag: "managed_db",
|
|
582
680
|
status: "DETECTED",
|
|
583
|
-
detected_from:
|
|
681
|
+
detected_from: Array.from(detectedFrom),
|
|
584
682
|
constraints: [
|
|
585
683
|
"Database connection pool limits and socket timeouts must be configured.",
|
|
586
684
|
"Pada Shared Hosting: Akses MySQL biasanya terikat ke 'localhost' / loopback dengan batas koneksi maksimal.",
|
|
@@ -713,12 +811,17 @@ export class EnvironmentDetector {
|
|
|
713
811
|
}
|
|
714
812
|
|
|
715
813
|
// Distributed Interplay 5: Database Migration Concurrency Invariant
|
|
716
|
-
const
|
|
717
|
-
|
|
814
|
+
const hostLayerNodes = nodes.filter((n) => n.layer === "host");
|
|
815
|
+
const backendRuntimeNodes = nodes.filter((n) => n.layer === "runtime" && n.role !== "frontend_spa");
|
|
816
|
+
if (hostLayerNodes.length > 1 || backendRuntimeNodes.length > 1) {
|
|
817
|
+
const involvedComputeIds = [
|
|
818
|
+
...(hostLayerNodes.length > 1 ? hostLayerNodes.map((n) => n.id) : []),
|
|
819
|
+
...(backendRuntimeNodes.length > 1 ? backendRuntimeNodes.map((n) => n.id) : []),
|
|
820
|
+
];
|
|
718
821
|
interplays.push({
|
|
719
822
|
title: "Distributed Database Migration Concurrency Invariant",
|
|
720
823
|
involved_layers: ["host", "storage"],
|
|
721
|
-
involved_node_ids:
|
|
824
|
+
involved_node_ids: involvedComputeIds,
|
|
722
825
|
risk_or_rule: "Terdapat lebih dari 1 instance host/compute (multi-VPS / multi-node) yang terhubung ke database bersama. Menjalankan migrasi database serentak saat deployment memicu deadlock tabel atau migration race condition.",
|
|
723
826
|
required_action: "Pastikan script migrasi (seperti 'artisan migrate --isolated') hanya dijalankan oleh satu designated deploy pipeline runner sebelum node lain menerima traffic.",
|
|
724
827
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
export * from "./types/index.ts";
|
|
2
|
-
export * from "./core/config/schema.ts";
|
|
3
|
-
export * from "./core/config/loader.ts";
|
|
4
|
-
export * from "./core/database/client.ts";
|
|
5
|
-
export * from "./core/database/repository.ts";
|
|
6
|
-
export * from "./core/parser/extractors/base.ts";
|
|
7
|
-
export * from "./core/parser/tree-sitter.ts";
|
|
8
|
-
export * from "./core/ingestion/pipeline.ts";
|
|
9
|
-
export * from "./core/boundary/evaluator.ts";
|
|
10
|
-
export * from "./core/resolver/call-graph-tracer.ts";
|
|
11
|
-
export * from "./core/resolver/vertical-slice-tracer.ts";
|
|
12
|
-
export * from "./core/topology/types.ts";
|
|
13
|
-
export * from "./core/topology/environment-detector.ts";
|
|
14
|
-
export * from "./mcp/tools/get-environment-topology.ts";
|
|
15
|
-
export * from "./mcp/server.ts";
|
|
16
|
-
export * from "./cli/index.ts";
|
|
17
|
-
|
|
18
|
-
import { runCLI } from "./cli/index.ts";
|
|
19
|
-
if (import.meta.main) {
|
|
20
|
-
runCLI(process.argv);
|
|
21
|
-
}
|
|
1
|
+
export * from "./types/index.ts";
|
|
2
|
+
export * from "./core/config/schema.ts";
|
|
3
|
+
export * from "./core/config/loader.ts";
|
|
4
|
+
export * from "./core/database/client.ts";
|
|
5
|
+
export * from "./core/database/repository.ts";
|
|
6
|
+
export * from "./core/parser/extractors/base.ts";
|
|
7
|
+
export * from "./core/parser/tree-sitter.ts";
|
|
8
|
+
export * from "./core/ingestion/pipeline.ts";
|
|
9
|
+
export * from "./core/boundary/evaluator.ts";
|
|
10
|
+
export * from "./core/resolver/call-graph-tracer.ts";
|
|
11
|
+
export * from "./core/resolver/vertical-slice-tracer.ts";
|
|
12
|
+
export * from "./core/topology/types.ts";
|
|
13
|
+
export * from "./core/topology/environment-detector.ts";
|
|
14
|
+
export * from "./mcp/tools/get-environment-topology.ts";
|
|
15
|
+
export * from "./mcp/server.ts";
|
|
16
|
+
export * from "./cli/index.ts";
|
|
17
|
+
|
|
18
|
+
import { runCLI } from "./cli/index.ts";
|
|
19
|
+
if (import.meta.main) {
|
|
20
|
+
runCLI(process.argv);
|
|
21
|
+
}
|