septum 0.1.6 → 0.1.8
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 +38 -37
- package/package.json +6 -5
- package/src/cli/commands/check.ts +34 -122
- package/src/cli/commands/hook.ts +68 -16
- package/src/cli/commands/init.ts +440 -121
- package/src/cli/commands/sync.ts +27 -17
- package/src/cli/commands/topology.ts +89 -47
- package/src/cli/index.ts +7 -68
- package/src/cli/scaffold/harness-config.ts +697 -0
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +8 -8
- package/src/cli/templates/skill.ts +11 -18
- package/src/cli/ui/prompts.ts +122 -0
- package/src/core/boundary/evaluator.ts +21 -25
- package/src/core/boundary/staged-auditor.ts +50 -0
- package/src/core/config/loader.ts +21 -24
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +80 -16
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +147 -65
- package/src/core/database/repository.ts +17 -5
- package/src/core/database/schema.ts +1 -1
- package/src/core/discovery/topology-detector.ts +71 -19
- package/src/core/ingestion/pipeline.ts +274 -126
- package/src/core/parser/extractors/frontend.ts +1 -0
- package/src/core/parser/extractors/go.ts +2 -2
- package/src/core/parser/extractors/laravel-semantic.ts +9 -8
- package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
- package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
- package/src/core/parser/extractors/php.ts +2 -2
- package/src/core/parser/extractors/python.ts +2 -2
- package/src/core/parser/extractors/typescript.ts +2 -2
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +3 -0
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/module-resolver.ts +1 -10
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resolver/vertical-slice-tracer.ts +1 -0
- package/src/core/resources/ignore.ts +35 -0
- package/src/core/resources/limits.ts +14 -0
- package/src/core/resources/source-files.ts +62 -0
- package/src/core/resources/work-queue.ts +74 -0
- package/src/core/session/session-manager.ts +67 -38
- package/src/core/telemetry/telemetry.ts +12 -1
- package/src/core/topology/environment-detector.ts +21 -11
- package/src/mcp/schemas.ts +381 -8
- package/src/mcp/server.ts +326 -529
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +97 -43
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +15 -7
- package/src/mcp/tools/get-environment-topology.ts +2 -2
- package/src/mcp/tools/get-feature-context.ts +9 -12
- package/src/mcp/tools/get-symbol-impact.ts +5 -2
- package/src/mcp/tools/get-symbol.ts +2 -1
- package/src/mcp/tools/locate-symbol.ts +28 -5
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +40 -21
- package/src/mcp/tools/slice.ts +15 -0
- package/src/mcp/tools/symbol.ts +82 -0
- package/src/mcp/workspace-manager.ts +159 -0
- package/src/mcp/workspace-watcher.ts +202 -0
- package/src/types/index.ts +21 -1
- package/src/types/wasm.d.ts +4 -0
- package/src/cli/commands/ingest.ts +0 -29
- package/src/cli/commands/locate.ts +0 -61
- package/src/cli/commands/query.ts +0 -104
- package/src/cli/commands/slice.ts +0 -50
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { RESOURCE_LIMITS } from "./limits.ts";
|
|
2
|
+
|
|
3
|
+
/** One mutation/parse lane per server; failures do not poison later work. */
|
|
4
|
+
export class SerialWorkQueue {
|
|
5
|
+
private tail: Promise<void> = Promise.resolve();
|
|
6
|
+
private pending = 0;
|
|
7
|
+
private readonly maxPending: number;
|
|
8
|
+
|
|
9
|
+
constructor(maxPending = RESOURCE_LIMITS.maxQueuePending) {
|
|
10
|
+
this.maxPending = maxPending;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get pendingCount(): number {
|
|
14
|
+
return this.pending;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
run<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
18
|
+
if (signal?.aborted) {
|
|
19
|
+
return Promise.reject(signal.reason ?? new Error("Request was cancelled before queueing"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (this.pending >= this.maxPending) {
|
|
23
|
+
const busyError = new Error(`Server is busy: request admission limit (${this.maxPending}) exceeded. Retry after current tasks finish.`);
|
|
24
|
+
(busyError as any).code = "SERVER_BUSY";
|
|
25
|
+
return Promise.reject(busyError);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.pending++;
|
|
29
|
+
|
|
30
|
+
return new Promise<T>((resolve, reject) => {
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
|
|
33
|
+
const abortHandler = () => {
|
|
34
|
+
cancelled = true;
|
|
35
|
+
reject(signal?.reason ?? new Error("Request was cancelled while queued"));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (signal) {
|
|
39
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const execute = async () => {
|
|
43
|
+
if (signal) {
|
|
44
|
+
signal.removeEventListener("abort", abortHandler);
|
|
45
|
+
}
|
|
46
|
+
if (cancelled || signal?.aborted) {
|
|
47
|
+
if (!cancelled) {
|
|
48
|
+
reject(signal?.reason ?? new Error("Request was cancelled before execution"));
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const res = await work();
|
|
55
|
+
resolve(res);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
reject(err);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const next = this.tail.then(execute, execute);
|
|
62
|
+
this.tail = next.finally(() => {
|
|
63
|
+
this.pending--;
|
|
64
|
+
if (this.pending === 0) {
|
|
65
|
+
this.tail = Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
drain(): Promise<void> {
|
|
72
|
+
return this.tail;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,56 +1,85 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { SeptumRepository } from "../database/repository.ts";
|
|
3
5
|
import type { ActiveFeatureSession } from "../../types/index.ts";
|
|
6
|
+
import { SeptumDatabase } from "../database/client.ts";
|
|
4
7
|
|
|
5
8
|
export class SessionManager {
|
|
6
9
|
private static readonly SESSION_FILE = ".septum/session.json";
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
11
|
+
public static getOwnerId(): string {
|
|
12
|
+
return process.env.SEPTUM_SESSION_ID || `process:${process.pid}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private static mutate<T>(workspaceRoot: string, fn: () => T, repo?: SeptumRepository): T {
|
|
16
|
+
if (repo) return repo.runInTransaction(fn, true);
|
|
17
|
+
const db = new SeptumDatabase(join(workspaceRoot, ".septum/septum.db"));
|
|
18
|
+
try {
|
|
19
|
+
// Serialize ownership checks with writes across CLI and MCP processes.
|
|
20
|
+
return db.raw.transaction(fn).immediate();
|
|
21
|
+
} finally {
|
|
22
|
+
db.close();
|
|
15
23
|
}
|
|
16
|
-
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
17
|
-
writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8");
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
public static saveActiveSession(
|
|
27
|
+
workspaceRoot: string,
|
|
28
|
+
session: ActiveFeatureSession,
|
|
29
|
+
ownerId: string = SessionManager.getOwnerId(),
|
|
30
|
+
persistence?: { repo: SeptumRepository; save: () => void }
|
|
31
|
+
): void {
|
|
32
|
+
SessionManager.mutate(workspaceRoot, () => {
|
|
33
|
+
const current = SessionManager.getActiveSession(workspaceRoot);
|
|
34
|
+
if (current && current.owner_id !== ownerId) {
|
|
35
|
+
throw new Error(`Workspace already has an active feature session owned by '${current.owner_id || "legacy-session"}'. Release it with its owner, or explicitly run 'septum feature clear --force'.`);
|
|
36
|
+
}
|
|
37
|
+
const septumDir = join(workspaceRoot, ".septum");
|
|
38
|
+
mkdirSync(septumDir, { recursive: true });
|
|
39
|
+
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
40
|
+
const temporaryPath = `${sessionPath}.${randomUUID()}.tmp`;
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(temporaryPath, JSON.stringify({ ...session, owner_id: ownerId }, null, 2), "utf-8");
|
|
43
|
+
// Metadata and ownership use one write lock; a failed rename rolls back SQLite.
|
|
44
|
+
persistence?.save();
|
|
45
|
+
renameSync(temporaryPath, sessionPath);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(temporaryPath, { force: true });
|
|
48
|
+
}
|
|
49
|
+
}, persistence?.repo);
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
public static getActiveSession(workspaceRoot: string): ActiveFeatureSession | null {
|
|
24
53
|
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
25
|
-
if (!existsSync(sessionPath))
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
54
|
+
if (!existsSync(sessionPath)) return null;
|
|
55
|
+
let parsed: any;
|
|
29
56
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return parsed;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
57
|
+
parsed = JSON.parse(readFileSync(sessionPath, "utf-8")) as ActiveFeatureSession;
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
throw new Error(`Corrupted active feature session JSON in '${sessionPath}': ${e.message}. Release it explicitly with 'septum feature clear --force' or 'septum_feature({action: "release", force: true})'.`);
|
|
38
60
|
}
|
|
61
|
+
if (parsed && typeof parsed.feature_key === "string" && parsed.feature_key.trim() &&
|
|
62
|
+
typeof parsed.domain === "string" && parsed.domain.trim() &&
|
|
63
|
+
Array.isArray(parsed.touchpoints) && parsed.touchpoints.every((tp: unknown) => typeof tp === "string") &&
|
|
64
|
+
(parsed.owner_id === undefined || typeof parsed.owner_id === "string")) return parsed;
|
|
65
|
+
throw new Error(`Invalid active feature session in '${sessionPath}'. Release it explicitly with 'septum feature clear --force'.`);
|
|
39
66
|
}
|
|
40
67
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (existsSync(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
public static clearActiveSession(
|
|
69
|
+
workspaceRoot: string,
|
|
70
|
+
ownerId: string = SessionManager.getOwnerId(),
|
|
71
|
+
force: boolean = false
|
|
72
|
+
): boolean {
|
|
73
|
+
if (!existsSync(join(workspaceRoot, SessionManager.SESSION_FILE))) return false;
|
|
74
|
+
return SessionManager.mutate(workspaceRoot, () => {
|
|
75
|
+
const sessionPath = join(workspaceRoot, SessionManager.SESSION_FILE);
|
|
76
|
+
if (!existsSync(sessionPath)) return false;
|
|
77
|
+
if (!force) {
|
|
78
|
+
const current = SessionManager.getActiveSession(workspaceRoot);
|
|
79
|
+
if (current?.owner_id !== ownerId) throw new Error("Only the active session owner can release it. Use 'septum feature clear --force' for explicit recovery.");
|
|
52
80
|
}
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
rmSync(sessionPath, { force: true });
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
55
84
|
}
|
|
56
85
|
}
|
|
@@ -56,13 +56,24 @@ export class SeptumTelemetry {
|
|
|
56
56
|
console.error(`[Septum Telemetry SLOW_WARN] ${jsonLine}`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
this.appendLog(jsonLine, workspaceRoot);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static recordMemorySample(metrics: Record<string, number>, workspaceRoot: string = process.cwd()): void {
|
|
63
|
+
const jsonLine = JSON.stringify({timestamp: new Date().toISOString(), event: "memory_sample", metrics});
|
|
64
|
+
console.error(`[Septum Memory] ${jsonLine}`);
|
|
65
|
+
this.appendLog(jsonLine, workspaceRoot);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static appendLog(jsonLine: string, workspaceRoot: string): void {
|
|
59
69
|
// Sink 2: Append-only persistent file recorder in .septum/septum.log
|
|
60
70
|
try {
|
|
61
71
|
const logPath = this.getLogPath(workspaceRoot);
|
|
62
72
|
const logDir = dirname(logPath);
|
|
63
73
|
|
|
74
|
+
// SV-03: Do not auto-scaffold .septum directory for telemetry if workspace has not been initialized
|
|
64
75
|
if (!existsSync(logDir)) {
|
|
65
|
-
|
|
76
|
+
return;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
// Check for rotation if file exceeds 2MB
|
|
@@ -18,8 +18,8 @@ export class EnvironmentDetector {
|
|
|
18
18
|
): EnvironmentTopology {
|
|
19
19
|
// 1. Fetch any previously verified layers and nodes from SQLite SSOT
|
|
20
20
|
const savedLayers: Partial<Record<EnvironmentLayer, EnvironmentLayerInfo>> =
|
|
21
|
-
repo
|
|
22
|
-
const savedNodes = repo
|
|
21
|
+
repo ? repo.environment.getAllLayers() : {};
|
|
22
|
+
const savedNodes = repo ? repo.environment.getAllNodes() : [];
|
|
23
23
|
const savedNodeMap = new Map(savedNodes.map((n) => [n.id, n]));
|
|
24
24
|
|
|
25
25
|
// 2. Perform root file-based heuristic inspection for each of the 6 layers
|
|
@@ -31,19 +31,28 @@ export class EnvironmentDetector {
|
|
|
31
31
|
const detectedTelemetry = this.inspectTelemetryLayer(workspaceRoot);
|
|
32
32
|
|
|
33
33
|
// 3. Merge layers: Database verified layers take precedence
|
|
34
|
+
const resolveLayer = (layerKey: EnvironmentLayer, detected: EnvironmentLayerInfo): EnvironmentLayerInfo => {
|
|
35
|
+
const saved = savedLayers[layerKey];
|
|
36
|
+
if (saved && saved.status === "VERIFIED") {
|
|
37
|
+
return saved;
|
|
38
|
+
}
|
|
39
|
+
return detected;
|
|
40
|
+
};
|
|
41
|
+
|
|
34
42
|
const layers: Record<EnvironmentLayer, EnvironmentLayerInfo> = {
|
|
35
|
-
edge:
|
|
36
|
-
gateway:
|
|
37
|
-
host:
|
|
38
|
-
runtime:
|
|
39
|
-
storage:
|
|
40
|
-
telemetry:
|
|
43
|
+
edge: resolveLayer("edge", detectedEdge),
|
|
44
|
+
gateway: resolveLayer("gateway", detectedGateway),
|
|
45
|
+
host: resolveLayer("host", detectedHost),
|
|
46
|
+
runtime: resolveLayer("runtime", detectedRuntime),
|
|
47
|
+
storage: resolveLayer("storage", detectedStorage),
|
|
48
|
+
telemetry: resolveLayer("telemetry", detectedTelemetry),
|
|
41
49
|
};
|
|
42
50
|
|
|
43
|
-
// 4. Save layers to SQLite if not already stored
|
|
51
|
+
// 4. Save layers to SQLite if not already stored (refresh only non-VERIFIED layers)
|
|
44
52
|
if (repo) {
|
|
45
53
|
for (const layerKey of Object.keys(layers) as EnvironmentLayer[]) {
|
|
46
|
-
|
|
54
|
+
const saved = savedLayers[layerKey];
|
|
55
|
+
if (!saved || (forceRefresh && saved.status !== "VERIFIED")) {
|
|
47
56
|
repo.environment.saveLayer(layers[layerKey]);
|
|
48
57
|
}
|
|
49
58
|
}
|
|
@@ -231,7 +240,8 @@ export class EnvironmentDetector {
|
|
|
231
240
|
// 7. Save detected nodes to SQLite SSOT
|
|
232
241
|
if (repo) {
|
|
233
242
|
for (const node of finalNodes) {
|
|
234
|
-
|
|
243
|
+
const saved = savedNodeMap.get(node.id);
|
|
244
|
+
if (!saved || (forceRefresh && saved.status !== "VERIFIED")) {
|
|
235
245
|
repo.environment.saveNode(node);
|
|
236
246
|
}
|
|
237
247
|
}
|