septum 0.1.7 → 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 +36 -55
- package/package.json +3 -2
- package/src/cli/commands/check.ts +2 -0
- package/src/cli/commands/hook.ts +59 -12
- 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 +5 -67
- 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/staged-auditor.ts +5 -2
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +77 -13
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +128 -117
- 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 +228 -94
- 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/schema/providers/laravel-migration-provider.ts +4 -3
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/path-resolver.ts +57 -4
- 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 +19 -6
- 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 +321 -535
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +83 -11
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +9 -6
- 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 +3 -3
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +34 -17
- 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 +1 -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
|
@@ -6,7 +6,7 @@ export const SEPTUM_HOOKS_CONFIG = {
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "sh -c '
|
|
9
|
+
"command": "sh -c 'ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"; if [ -f \"$ROOT/.gemini/hooks/septum-pre-write.sh\" ]; then exec \"$ROOT/.gemini/hooks/septum-pre-write.sh\"; elif [ -f \"$ROOT/.agents/hooks/septum-pre-write.sh\" ]; then exec \"$ROOT/.agents/hooks/septum-pre-write.sh\"; else exit 0; fi'",
|
|
10
10
|
"timeout": 10,
|
|
11
11
|
},
|
|
12
12
|
],
|
|
@@ -18,7 +18,7 @@ export const SEPTUM_HOOKS_CONFIG = {
|
|
|
18
18
|
"hooks": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
|
-
"command": "sh -c '
|
|
21
|
+
"command": "sh -c 'ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"; if [ -f \"$ROOT/.gemini/hooks/septum-post-write.sh\" ]; then exec \"$ROOT/.gemini/hooks/septum-post-write.sh\"; elif [ -f \"$ROOT/.agents/hooks/septum-post-write.sh\" ]; then exec \"$ROOT/.agents/hooks/septum-post-write.sh\"; else exit 0; fi'",
|
|
22
22
|
"timeout": 15,
|
|
23
23
|
},
|
|
24
24
|
],
|
|
@@ -38,13 +38,13 @@ fi
|
|
|
38
38
|
|
|
39
39
|
# Delegate directly to Septum's active 3-gate hook engine
|
|
40
40
|
if command -v bun >/dev/null 2>&1 && [ -f "$ROOT_DIR/bin/septum.ts" ]; then
|
|
41
|
-
exec bun run "$ROOT_DIR/bin/septum.ts" hook pre-write
|
|
41
|
+
exec bun run "$ROOT_DIR/bin/septum.ts" hook pre-write "$@"
|
|
42
42
|
elif command -v septum >/dev/null 2>&1; then
|
|
43
|
-
exec septum hook pre-write
|
|
43
|
+
exec septum hook pre-write "$@"
|
|
44
44
|
elif [ -f "$ROOT_DIR/node_modules/.bin/septum" ]; then
|
|
45
|
-
exec "$ROOT_DIR/node_modules/.bin/septum" hook pre-write
|
|
45
|
+
exec "$ROOT_DIR/node_modules/.bin/septum" hook pre-write "$@"
|
|
46
46
|
elif command -v bunx >/dev/null 2>&1; then
|
|
47
|
-
exec bunx septum hook pre-write
|
|
47
|
+
exec bunx septum hook pre-write "$@"
|
|
48
48
|
fi
|
|
49
49
|
|
|
50
50
|
exit 0
|
|
@@ -8,14 +8,14 @@ export const SEPTUM_RULES_TEMPLATE = `# Septum Bounded-Context, Anti-Duplication
|
|
|
8
8
|
|
|
9
9
|
Sebelum membuat class, fungsi, method, tipe, atau variabel/konstanta baru di file backend:
|
|
10
10
|
1. **Periksa Peta Domain & Koneksi File (Compact Mode < 400 Token):**
|
|
11
|
-
Panggil \`
|
|
11
|
+
Panggil \`septum_domain({ action: "catalog", domain: "<nama_domain>", detail_level: "compact" })\` (atau tanpa \`domain\` untuk *Macro Semantic Map*).
|
|
12
12
|
* Perhatikan daftar simbol eksisting (\`class\`, \`interface\`, \`struct\`, \`function\`, \`type\`, \`constant\`, \`variable\`) beserta \`line_span\`-nya.
|
|
13
13
|
* Perhatikan \`connections.imports_from\` (file mana saja yang diimpor oleh file tersebut) dan \`connections.imported_by_count\` (berapa file lain yang bergantung padanya).
|
|
14
|
-
* Jika membutuhkan daftar signature method lengkap pada satu file, panggil \`
|
|
14
|
+
* Jika membutuhkan daftar signature method lengkap pada satu file, panggil \`septum_symbol({ action: "inspect", query: "<NamaSimbol>" })\`.
|
|
15
15
|
2. **Cari Simbol Lintas-Domain Sebelum Membuat Baru:**
|
|
16
|
-
Panggil \`
|
|
16
|
+
Panggil \`septum_symbol({ action: "locate", query: "<kata_kunci>" })\` untuk memastikan fungsi/helper/class yang ingin dibuat belum ada di codebase.
|
|
17
17
|
3. **Kunci Scope pada Fitur Aktif:**
|
|
18
|
-
Jika mengerjakan fitur terdaftar, panggil \`
|
|
18
|
+
Jika mengerjakan fitur terdaftar, panggil \`septum_feature({ action: "get", feature_key: "<nama_fitur>" })\`:
|
|
19
19
|
* **Patuhi \`allowed_touchpoints\`:** Jangan memodifikasi file di luar daftar touchpoint fitur aktif.
|
|
20
20
|
* **Gunakan ulang \`reuse_symbols\`:** Wajib mengimpor dan memakai kembali simbol yang terdaftar di \`reuse_symbols\`.
|
|
21
21
|
|
|
@@ -26,11 +26,11 @@ Sebelum membuat class, fungsi, method, tipe, atau variabel/konstanta baru di fil
|
|
|
26
26
|
1. **Cek Dampak Simbol (\`inbound_callers\`):**
|
|
27
27
|
Sebelum mengubah parameter/return signature atau menghapus class/fungsi/method publik, wajib panggil:
|
|
28
28
|
\`\`\`json
|
|
29
|
-
|
|
29
|
+
septum_symbol({ "action": "impact", "query": "NamaClassAtauFungsi" })
|
|
30
30
|
\`\`\`
|
|
31
31
|
Periksa seluruh \`inbound_callers\` (termasuk *named imports* lintas file) agar tidak memutus kontrak file pemanggil.
|
|
32
32
|
2. **Telusuri Vertical Slice (Default Depth 5, Max Depth 8):**
|
|
33
|
-
Gunakan \`
|
|
33
|
+
Gunakan \`septum_slice({ query: "<route|command|handler>", max_depth: 5 })\` (naikkan hingga \`max_depth: 8\` untuk alur Event/Job/Queue berlapis) guna memahami rantai eksekusi dari Ingress hingga Repository/Entity.
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -38,13 +38,13 @@ Sebelum membuat class, fungsi, method, tipe, atau variabel/konstanta baru di fil
|
|
|
38
38
|
|
|
39
39
|
1. **Sebelum Menulis Import Lintas-Domain (\`write_to_file\` / \`replace_file_content\`):**
|
|
40
40
|
\`\`\`json
|
|
41
|
-
|
|
41
|
+
septum_boundary({
|
|
42
42
|
"file_path": "path/ke/file.ts",
|
|
43
43
|
"proposed_imports": ["../other-domain/service"],
|
|
44
44
|
"feature_key": "opsional_feature_key"
|
|
45
45
|
})
|
|
46
46
|
\`\`\`
|
|
47
47
|
2. **Audit Menyeluruh & Deteksi Siklus (\`circular_dependency\`):**
|
|
48
|
-
Jalankan \`
|
|
48
|
+
Jalankan \`septum_boundary({ scope: "staged" })\` atau \`septum_boundary({ scope: "all" })\` setelah menyelesaikan perubahan multi-file. Jika status \`rejected\` (\`forbidden_dependency\`, \`disallowed_dependency\`, \`touchpoint_violation\`, atau \`circular_dependency\`), batalkan/perbaiki arsitektur import sebelum melakukan commit.
|
|
49
49
|
`;
|
|
50
50
|
|
|
@@ -20,22 +20,15 @@ description: Framework-agnostic codebase discovery, multi-language AST symbol &
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
## 2. Referensi Cepat
|
|
23
|
+
## 2. Referensi Cepat 5 Unified Tools MCP Septum (Standardized \`snake_case\` Contracts)
|
|
24
24
|
|
|
25
25
|
| Tool MCP | Parameter Utama (\`snake_case\`) | Fungsi & Kapan Digunakan |
|
|
26
26
|
|---|---|---|
|
|
27
|
-
| **\`
|
|
28
|
-
| **\`
|
|
29
|
-
| **\`
|
|
30
|
-
| **\`
|
|
31
|
-
| **\`
|
|
32
|
-
| **\`septum_check_boundary\`** | \`{ file_path?, file_paths?, proposed_imports?, feature_key?, scope?: "file"\\|"all"\\|"staged"\\|"domain", domain? }\` | **Pre-Write:** memvalidasi \`proposed_imports\` pada \`file_path\`. **Macro Audit:** panggil dengan \`{ scope: "all" }\` atau \`{ scope: "staged" }\` untuk mengaudit seluruh batas domain & *circular dependencies*. |
|
|
33
|
-
| **\`septum_get_symbol_hotspots\`** | \`{ domain?, min_lines?: 30, min_nesting?, kind?, limit?: 30, sort_by?: "risk_score" }\` | Mengidentifikasi fungsi/class paling kompleks berdasarkan \`risk_score\` (\`line_count\` $\\times$ \`nesting_depth\`). |
|
|
34
|
-
| **\`septum_get_feature_context\`** | \`{ feature_key? }\` | Mengambil konteks fitur aktif (\`allowed_touchpoints\`, \`reuse_symbols\`, input/output contract). |
|
|
35
|
-
| **\`septum_clear_feature_context\`** | \`{}\` | Melepaskan kunci sesi fitur aktif di \`.septum/session.json\`. |
|
|
36
|
-
| **\`septum_register_domain\`** | \`{ domain, root, description?, allowed_dependencies?, forbidden_dependencies?, archetypes?, ingest_now?: true }\` | Mendaftarkan atau memperbarui bounded-context domain langsung ke SQLite SSOT (\`.septum/septum.db\`). |
|
|
37
|
-
| **\`septum_register_feature\`** | \`{ feature_key, domain, description?, allowed_touchpoints?, reuse_symbols?, input_contract?, output_contract? }\` | Mendaftarkan kontrak fitur baru ke SQLite SSOT dan mengaktifkan sesi *touchpoint lock*. |
|
|
38
|
-
| **\`septum_get_environment_topology\`** | \`{ resolve?, resolveNode?, refresh? }\` | Memetakan 6-layer topologi infrastruktur (\`EDGE\`, \`GATEWAY\`, \`HOST\`, \`RUNTIME\`, \`STORAGE\`, \`TELEMETRY\`) beserta invarian lintas-layer. |
|
|
27
|
+
| **\`septum_domain\`** | \`{ action?: "catalog"\\|"register"\\|"topology", domain?, detail_level?: "compact"\\|"full", archetype_filter?, file_filter?, root?, allowed_dependencies?, forbidden_dependencies?, ingest_now?, resolve?, resolve_node? }\` | Mengelola arsitektur makro, topologi domain, pendaftaran bounded-context, dan plane infrastruktur 6-layer. Default \`action: "catalog"\` (< 400 token). |
|
|
28
|
+
| **\`septum_symbol\`** | \`{ action?: "locate"\\|"inspect"\\|"impact"\\|"hotspots", query?, symbol?, domain?, include_dependencies?: true, min_lines?: 30, min_nesting?, sort_by?: "risk_score", limit?: 10 }\` | Unified Symbol Engine: mencari simbol (\`locate\`), inspeksi detail kontrak & signature (\`inspect\`), hitung blast radius & pemanggil (\`impact\`), atau isolasi fungsi kompleks & SRP smell (\`hotspots\`). |
|
|
29
|
+
| **\`septum_slice\`** | \`{ query, max_depth?: 5 }\` | Menelusuri alur eksekusi end-to-end (\`Ingress ➔ Validation ➔ Controller ➔ Service ➔ Repository ➔ Entity\`). \`max_depth\` default \`5\`, rentang \`1..8\` (gunakan \`6..8\` untuk deep trace Event/Job/Queue). |
|
|
30
|
+
| **\`septum_boundary\`** | \`{ scope?: "file"\\|"all"\\|"staged"\\|"domain", file_path?, file_paths?, proposed_imports?, feature_key?, domain? }\` | Pre-flight boundary check memvalidasi \`proposed_imports\` sebelum menulis file, serta audit repo-wide atau git-staged untuk mencegah pelanggaran batas domain dan *circular dependency*. |
|
|
31
|
+
| **\`septum_feature\`** | \`{ action?: "get"\\|"lock"\\|"release", feature_key?, domain?, allowed_touchpoints?, reuse_symbols?, force? }\` | Mengelola konteks fitur: ambil detail fitur (\`get\`), kunci sesi touchpoint di \`.septum/session.json\` (\`lock\`), atau lepaskan kunci sesi (\`release\`). |
|
|
39
32
|
|
|
40
33
|
---
|
|
41
34
|
|
|
@@ -47,8 +40,8 @@ description: Framework-agnostic codebase discovery, multi-language AST symbol &
|
|
|
47
40
|
Gunakan alur 4 langkah ini saat merancang domain baru, melakukan *re-mapping* arsitektur, atau mengunci kontrak fitur:
|
|
48
41
|
|
|
49
42
|
### Langkah 1: Topological & Connectivity Scan (Maksimal 2 Tool Call)
|
|
50
|
-
1. Panggil \`
|
|
51
|
-
2. Panggil \`
|
|
43
|
+
1. Panggil \`septum_domain({ action: "catalog" })\` untuk memeriksa *Telescope View* dan daftar domain yang terdaftar.
|
|
44
|
+
2. Panggil \`septum_domain({ action: "catalog", domain: "<target>", detail_level: "compact" })\` untuk memetakan file, simbol yang sudah ada, dan koneksi \`imports_from\` / \`imported_by_count\`.
|
|
52
45
|
|
|
53
46
|
### Langkah 2: Perumusan Kontrak Domain & Anti-Duplikasi Simbol
|
|
54
47
|
* **\`domain\` & \`root\`:** Nama domain dan direktori kanonikalnya.
|
|
@@ -56,10 +49,10 @@ Gunakan alur 4 langkah ini saat merancang domain baru, melakukan *re-mapping* ar
|
|
|
56
49
|
* **\`reuse_symbols\`:** Daftar class, service, repository, atau konstanta eksisting yang **wajib digunakan kembali** agar agen tidak membuat kode duplikat.
|
|
57
50
|
|
|
58
51
|
### Langkah 3: Registrasi Deterministik ke SQLite SSOT
|
|
59
|
-
1. Gunakan \`
|
|
60
|
-
2. Gunakan \`
|
|
52
|
+
1. Gunakan \`septum_domain({ action: "register", domain, root, allowed_dependencies, forbidden_dependencies, ingest_now: true })\` untuk menyimpan batas domain.
|
|
53
|
+
2. Gunakan \`septum_feature({ action: "lock", feature_key, domain, allowed_touchpoints, reuse_symbols })\` untuk mengunci fokus pengerjaan fitur.
|
|
61
54
|
|
|
62
55
|
### Langkah 4: Verifikasi Boundary & Siklus Dependensi
|
|
63
|
-
Jalankan \`
|
|
56
|
+
Jalankan \`septum_boundary({ scope: "all" })\` (atau CLI \`septum check --staged\`) untuk memastikan 0 pelanggaran batas domain dan 0 *circular dependency*.
|
|
64
57
|
`;
|
|
65
58
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import type { HarnessOption } from "../scaffold/harness-config.ts";
|
|
3
|
+
|
|
4
|
+
const RESET = "\x1b[0m";
|
|
5
|
+
const BOLD = "\x1b[1m";
|
|
6
|
+
const DIM = "\x1b[2m";
|
|
7
|
+
const CYAN = "\x1b[36m";
|
|
8
|
+
const GREEN = "\x1b[32m";
|
|
9
|
+
const YELLOW = "\x1b[33m";
|
|
10
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
11
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
12
|
+
|
|
13
|
+
export async function promptSelectHarnesses(options: HarnessOption[]): Promise<string[]> {
|
|
14
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
15
|
+
// Non-interactive fallback (CI/scripts/piped): return default selections
|
|
16
|
+
return options.filter((o) => o.selectedByDefault).map((o) => o.id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selected = new Set<string>(
|
|
20
|
+
options.filter((o) => o.selectedByDefault).map((o) => o.id)
|
|
21
|
+
);
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
|
|
24
|
+
process.stdout.write(HIDE_CURSOR);
|
|
25
|
+
|
|
26
|
+
function render(firstTime: boolean = false) {
|
|
27
|
+
if (!firstTime) {
|
|
28
|
+
// Move up by number of options + header lines
|
|
29
|
+
readline.moveCursor(process.stdout, 0, -(options.length + 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
readline.clearLine(process.stdout, 0);
|
|
33
|
+
console.log(
|
|
34
|
+
`${CYAN}◇${RESET} ${BOLD}Select AI Agent Harnesses for this project:${RESET} ${DIM}(Space: toggle, Enter: confirm, 'a': all)${RESET}`
|
|
35
|
+
);
|
|
36
|
+
readline.clearLine(process.stdout, 0);
|
|
37
|
+
console.log(`${CYAN}│${RESET}`);
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < options.length; i++) {
|
|
40
|
+
const opt = options[i];
|
|
41
|
+
const isCurrent = i === cursor;
|
|
42
|
+
const isChecked = selected.has(opt.id);
|
|
43
|
+
|
|
44
|
+
const pointer = isCurrent ? `${CYAN}❯${RESET}` : " ";
|
|
45
|
+
const box = isChecked ? `${GREEN}[●]${RESET}` : `${DIM}[○]${RESET}`;
|
|
46
|
+
const name = isCurrent ? `${BOLD}${opt.name}${RESET}` : opt.name;
|
|
47
|
+
const file = `${DIM}(${opt.targetFile})${RESET}`;
|
|
48
|
+
|
|
49
|
+
readline.clearLine(process.stdout, 0);
|
|
50
|
+
console.log(`${CYAN}│${RESET} ${pointer} ${box} ${name.padEnd(26)} ${file}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
render(true);
|
|
55
|
+
|
|
56
|
+
return new Promise<string[]>((resolve) => {
|
|
57
|
+
readline.emitKeypressEvents(process.stdin);
|
|
58
|
+
const wasRaw = process.stdin.isRaw;
|
|
59
|
+
process.stdin.setRawMode(true);
|
|
60
|
+
process.stdin.resume();
|
|
61
|
+
|
|
62
|
+
const onKeypress = (str: string, key: readline.Key) => {
|
|
63
|
+
if (key.ctrl && key.name === "c") {
|
|
64
|
+
cleanup();
|
|
65
|
+
process.exit(130);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (key.name === "up" || key.name === "k") {
|
|
69
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
70
|
+
render();
|
|
71
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
72
|
+
cursor = (cursor + 1) % options.length;
|
|
73
|
+
render();
|
|
74
|
+
} else if (key.name === "space") {
|
|
75
|
+
const id = options[cursor].id;
|
|
76
|
+
if (selected.has(id)) {
|
|
77
|
+
selected.delete(id);
|
|
78
|
+
} else {
|
|
79
|
+
selected.add(id);
|
|
80
|
+
}
|
|
81
|
+
render();
|
|
82
|
+
} else if (str === "a" || str === "A") {
|
|
83
|
+
if (selected.size === options.length) {
|
|
84
|
+
selected.clear();
|
|
85
|
+
} else {
|
|
86
|
+
for (const opt of options) selected.add(opt.id);
|
|
87
|
+
}
|
|
88
|
+
render();
|
|
89
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
90
|
+
cleanup();
|
|
91
|
+
// Clear prompt display and print finalized selection summary
|
|
92
|
+
readline.moveCursor(process.stdout, 0, -(options.length + 2));
|
|
93
|
+
for (let i = 0; i < options.length + 2; i++) {
|
|
94
|
+
readline.clearLine(process.stdout, 0);
|
|
95
|
+
readline.moveCursor(process.stdout, 0, 1);
|
|
96
|
+
}
|
|
97
|
+
readline.moveCursor(process.stdout, 0, -(options.length + 2));
|
|
98
|
+
|
|
99
|
+
const names = options
|
|
100
|
+
.filter((o) => selected.has(o.id))
|
|
101
|
+
.map((o) => o.name)
|
|
102
|
+
.join(", ");
|
|
103
|
+
console.log(
|
|
104
|
+
`${GREEN}✓${RESET} ${BOLD}Configured AI Harnesses:${RESET} ${names || `${YELLOW}None (skipped)${RESET}`}`
|
|
105
|
+
);
|
|
106
|
+
console.log(`${CYAN}│${RESET}`);
|
|
107
|
+
|
|
108
|
+
resolve(Array.from(selected));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function cleanup() {
|
|
113
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
114
|
+
process.stdin.setRawMode(wasRaw);
|
|
115
|
+
process.stdin.pause();
|
|
116
|
+
process.stdout.write(SHOW_CURSOR);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
process.stdin.on("keypress", onKeypress);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RESOURCE_LIMITS } from "../resources/limits.ts";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import type { BoundaryViolation } from "../../types/index.ts";
|
|
@@ -28,8 +29,10 @@ export async function auditStagedFiles(
|
|
|
28
29
|
for (const file of files) {
|
|
29
30
|
if (featureKey) violations.push(...evaluator.checkFeatureTouchpoints(featureKey, file, config));
|
|
30
31
|
if (!ASTParserEngine.isSourceFile(file) || !evaluator.resolveSourceDomain(file, config)) continue;
|
|
32
|
+
const size = Number(execFileSync("git", ["cat-file", "-s", `:${file}`], {cwd:workspaceRoot,encoding:"utf8",timeout:5000,maxBuffer:1024}));
|
|
33
|
+
if (!Number.isFinite(size) || size > RESOURCE_LIMITS.sourceBytes) throw new Error(`Staged source exceeds resource limit: '${file}'.`);
|
|
31
34
|
const content = execFileSync("git", ["show", `:${file}`], {
|
|
32
|
-
cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer:
|
|
35
|
+
cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: RESOURCE_LIMITS.sourceBytes + 1,
|
|
33
36
|
});
|
|
34
37
|
const parsed = await parser.parseFile(resolve(workspaceRoot, file), content);
|
|
35
38
|
if (parsed.status === "unsupported" || parsed.status === "degraded") {
|
|
@@ -43,5 +46,5 @@ export async function auditStagedFiles(
|
|
|
43
46
|
const staged = new Set([...files, ...deleted]);
|
|
44
47
|
const baseline = (repo?.getAllDependenciesWithDomains() || []).filter(dependency => !staged.has(dependency.source_file));
|
|
45
48
|
violations.push(...evaluator.detectCircularDependencies(config, [...baseline, ...stagedDependencies], staged));
|
|
46
|
-
return { files, violations };
|
|
49
|
+
return { files: Array.from(staged), violations };
|
|
47
50
|
}
|
|
@@ -41,4 +41,12 @@ export class SeptumDatabase {
|
|
|
41
41
|
public transaction<T>(fn: () => T): T {
|
|
42
42
|
return this.db.transaction(fn)();
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
public checkpoint(): void {
|
|
46
|
+
try {
|
|
47
|
+
this.db.run("PRAGMA wal_checkpoint(PASSIVE);");
|
|
48
|
+
} catch {
|
|
49
|
+
// Ignore if locked or busy
|
|
50
|
+
}
|
|
51
|
+
}
|
|
44
52
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { dirname, join, posix } from "node:path";
|
|
2
|
+
import { dirname, join, posix, resolve } from "node:path";
|
|
3
3
|
import type { Database } from "bun:sqlite";
|
|
4
4
|
import type {
|
|
5
5
|
ExtractedDependency,
|
|
@@ -249,7 +249,10 @@ export class DependencyRepository {
|
|
|
249
249
|
);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
public getSymbolImpact(
|
|
252
|
+
public getSymbolImpact(
|
|
253
|
+
symbolQuery: string,
|
|
254
|
+
options: { domain?: string; workspaceRoot?: string } = {}
|
|
255
|
+
): GetSymbolImpactResponse {
|
|
253
256
|
const trimmed = symbolQuery.trim();
|
|
254
257
|
if (!trimmed) {
|
|
255
258
|
return {
|
|
@@ -277,15 +280,75 @@ export class DependencyRepository {
|
|
|
277
280
|
targetMethod = parts[1];
|
|
278
281
|
}
|
|
279
282
|
|
|
280
|
-
// 1. Look up target symbol in symbols joined with files
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
283
|
+
// 1. Look up target symbol in symbols joined with files and optionally domains
|
|
284
|
+
let targetSymRows: Array<{ name: string; file_path: string }>;
|
|
285
|
+
if (targetMethod) {
|
|
286
|
+
let methodSql = `
|
|
287
|
+
SELECT s.name, s.signature, f.path AS file_path
|
|
288
|
+
FROM symbols s
|
|
289
|
+
JOIN files f ON s.file_id = f.id
|
|
290
|
+
`;
|
|
291
|
+
const methodParams: any[] = [];
|
|
292
|
+
if (options.domain) {
|
|
293
|
+
methodSql += ` JOIN domains dom ON f.domain_id = dom.id WHERE LOWER(dom.name) = LOWER(?) AND `;
|
|
294
|
+
methodParams.push(options.domain);
|
|
295
|
+
} else {
|
|
296
|
+
methodSql += ` WHERE `;
|
|
297
|
+
}
|
|
298
|
+
methodSql += ` (s.name = ? OR s.name = ? OR (s.name = ? AND s.kind = 'method')) `;
|
|
299
|
+
methodParams.push(trimmed, `${targetContainer}::${targetMethod}`, targetMethod);
|
|
300
|
+
targetSymRows = this.db.query<{ name: string; signature: string; file_path: string }, any[]>(methodSql).all(...methodParams)
|
|
301
|
+
.filter(row => {
|
|
302
|
+
if (row.name !== targetMethod) return true;
|
|
303
|
+
// Go indexes bare method names; the receiver in its signature proves identity.
|
|
304
|
+
const receiver = row.signature.match(/^func\s*\(\s*(?:[A-Za-z_]\w*\s+)?\*?([A-Za-z_]\w*)(?:\[[^\]]+\])?\s*\)/);
|
|
305
|
+
return receiver?.[1] === targetContainer;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (targetSymRows.length === 0) {
|
|
309
|
+
return {
|
|
310
|
+
target_symbol: trimmed,
|
|
311
|
+
found_in_index: false,
|
|
312
|
+
warning: `Symbol '${trimmed}' was not found in Septum index${options.domain ? ` for domain '${options.domain}'` : ""}. Method does not exist.`,
|
|
313
|
+
impact_summary: {
|
|
314
|
+
total_dependents: 0,
|
|
315
|
+
risk_level: "unknown",
|
|
316
|
+
affected_files_count: 0,
|
|
317
|
+
},
|
|
318
|
+
inbound_callers: [],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
let containerSql = `
|
|
323
|
+
SELECT s.name, f.path AS file_path
|
|
324
|
+
FROM symbols s
|
|
325
|
+
JOIN files f ON s.file_id = f.id
|
|
326
|
+
`;
|
|
327
|
+
const containerParams: any[] = [];
|
|
328
|
+
if (options.domain) {
|
|
329
|
+
containerSql += ` JOIN domains dom ON f.domain_id = dom.id WHERE LOWER(dom.name) = LOWER(?) AND `;
|
|
330
|
+
containerParams.push(options.domain);
|
|
331
|
+
} else {
|
|
332
|
+
containerSql += ` WHERE `;
|
|
333
|
+
}
|
|
334
|
+
containerSql += ` (s.name = ? OR s.name = ? OR s.name LIKE ? OR s.name LIKE ?) `;
|
|
335
|
+
containerParams.push(trimmed, targetContainer, `${targetContainer}::%`, `%::${targetContainer}`);
|
|
336
|
+
targetSymRows = this.db.query<{ name: string; file_path: string }, any[]>(containerSql).all(...containerParams);
|
|
337
|
+
|
|
338
|
+
if (targetSymRows.length === 0) {
|
|
339
|
+
return {
|
|
340
|
+
target_symbol: trimmed,
|
|
341
|
+
found_in_index: false,
|
|
342
|
+
warning: `Symbol '${trimmed}' was not found in Septum index${options.domain ? ` for domain '${options.domain}'` : ""}.`,
|
|
343
|
+
impact_summary: {
|
|
344
|
+
total_dependents: 0,
|
|
345
|
+
risk_level: "unknown",
|
|
346
|
+
affected_files_count: 0,
|
|
347
|
+
},
|
|
348
|
+
inbound_callers: [],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
289
352
|
|
|
290
353
|
const definingFiles = new Set<string>(
|
|
291
354
|
targetSymRows.map((r) => r.file_path.replace(/\\/g, "/"))
|
|
@@ -415,8 +478,9 @@ export class DependencyRepository {
|
|
|
415
478
|
if (targetMethod) {
|
|
416
479
|
for (const filePath of uniqueCallerFiles) {
|
|
417
480
|
try {
|
|
418
|
-
|
|
419
|
-
|
|
481
|
+
const fullPath = options.workspaceRoot ? resolve(options.workspaceRoot, filePath) : filePath;
|
|
482
|
+
if (fs.existsSync(fullPath)) {
|
|
483
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
420
484
|
const lines = content.split(/\r?\n/);
|
|
421
485
|
for (let i = 0; i < lines.length; i++) {
|
|
422
486
|
const line = lines[i];
|
|
@@ -21,50 +21,103 @@ export class DomainRepository {
|
|
|
21
21
|
constructor(private db: Database) {}
|
|
22
22
|
|
|
23
23
|
public upsertDomain(name: string, config: DomainConfig): number {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return this.db.transaction(() => {
|
|
25
|
+
const normalizedName = name.trim();
|
|
26
|
+
let existing = this.getDomainByName(normalizedName);
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// MCP-13: If duplicates exist with different casing, consolidate into canonical primary row
|
|
29
|
+
if (existing) {
|
|
30
|
+
const allMatching = this.db
|
|
31
|
+
.query<DomainRecord, [string]>("SELECT * FROM domains WHERE LOWER(name) = LOWER(?) ORDER BY id ASC")
|
|
32
|
+
.all(normalizedName);
|
|
33
|
+
if (allMatching.length > 1) {
|
|
34
|
+
const primary = allMatching[0];
|
|
35
|
+
const fields = [
|
|
36
|
+
["allowed_dependencies", "allowed_deps_json"],
|
|
37
|
+
["forbidden_dependencies", "forbidden_deps_json"],
|
|
38
|
+
["archetypes", "archetypes_json"],
|
|
39
|
+
] as const;
|
|
40
|
+
for (const [field, column] of fields) {
|
|
41
|
+
if (config[field] !== undefined) continue;
|
|
42
|
+
const values = allMatching.map(row => {
|
|
43
|
+
const parsed = JSON.parse(row[column]);
|
|
44
|
+
return JSON.stringify(Array.isArray(parsed) ? [...parsed].sort() : Object.entries(parsed).sort());
|
|
45
|
+
});
|
|
46
|
+
if (new Set(values).size > 1) {
|
|
47
|
+
throw new Error(`Conflicting legacy domain '${normalizedName}': provide '${field}' explicitly before consolidating case variants.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (let i = 1; i < allMatching.length; i++) {
|
|
51
|
+
const duplicate = allMatching[i];
|
|
52
|
+
this.db.query("UPDATE files SET domain_id = ? WHERE domain_id = ?").run(primary.id, duplicate.id);
|
|
53
|
+
this.db.query("UPDATE vertical_slices SET domain_id = ? WHERE domain_id = ?").run(primary.id, duplicate.id);
|
|
54
|
+
this.db.query("DELETE FROM domains WHERE id = ?").run(duplicate.id);
|
|
55
|
+
}
|
|
56
|
+
existing = primary;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
31
59
|
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
const targetName = existing ? existing.name : normalizedName;
|
|
61
|
+
|
|
62
|
+
// MCP-12: Distinguish absent (undefined -> preserve) from explicit empty ([] -> clear)
|
|
63
|
+
let allowed: string[];
|
|
64
|
+
if (config.allowed_dependencies !== undefined) {
|
|
65
|
+
allowed = config.allowed_dependencies;
|
|
66
|
+
} else if (existing?.allowed_deps_json) {
|
|
34
67
|
try {
|
|
35
68
|
allowed = JSON.parse(existing.allowed_deps_json);
|
|
36
|
-
} catch {
|
|
69
|
+
} catch {
|
|
70
|
+
allowed = [];
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
allowed = [];
|
|
37
74
|
}
|
|
38
|
-
|
|
75
|
+
|
|
76
|
+
let forbidden: string[];
|
|
77
|
+
if (config.forbidden_dependencies !== undefined) {
|
|
78
|
+
forbidden = config.forbidden_dependencies;
|
|
79
|
+
} else if (existing?.forbidden_deps_json) {
|
|
39
80
|
try {
|
|
40
81
|
forbidden = JSON.parse(existing.forbidden_deps_json);
|
|
41
|
-
} catch {
|
|
82
|
+
} catch {
|
|
83
|
+
forbidden = [];
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
forbidden = [];
|
|
42
87
|
}
|
|
43
|
-
|
|
88
|
+
|
|
89
|
+
let archetypes: Record<string, string>;
|
|
90
|
+
if (config.archetypes !== undefined) {
|
|
91
|
+
archetypes = config.archetypes;
|
|
92
|
+
} else if (existing?.archetypes_json) {
|
|
44
93
|
try {
|
|
45
94
|
archetypes = JSON.parse(existing.archetypes_json);
|
|
46
|
-
} catch {
|
|
95
|
+
} catch {
|
|
96
|
+
archetypes = {};
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
archetypes = {};
|
|
47
100
|
}
|
|
48
|
-
}
|
|
49
101
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
const allowedJson = JSON.stringify(allowed);
|
|
103
|
+
const forbiddenJson = JSON.stringify(forbidden);
|
|
104
|
+
const archetypesJson = JSON.stringify(archetypes);
|
|
105
|
+
|
|
106
|
+
const query = `
|
|
107
|
+
INSERT INTO domains (name, root_path, allowed_deps_json, forbidden_deps_json, archetypes_json)
|
|
108
|
+
VALUES (?, ?, ?, ?, ?)
|
|
109
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
110
|
+
root_path = excluded.root_path,
|
|
111
|
+
allowed_deps_json = excluded.allowed_deps_json,
|
|
112
|
+
forbidden_deps_json = excluded.forbidden_deps_json,
|
|
113
|
+
archetypes_json = excluded.archetypes_json,
|
|
114
|
+
updated_at = CURRENT_TIMESTAMP
|
|
115
|
+
`;
|
|
116
|
+
this.db.query(query).run(targetName, config.root, allowedJson, forbiddenJson, archetypesJson);
|
|
117
|
+
|
|
118
|
+
const record = this.getDomainByName(targetName);
|
|
119
|
+
return record?.id ?? 0;
|
|
120
|
+
}).immediate();
|
|
68
121
|
}
|
|
69
122
|
|
|
70
123
|
public getDomainByName(name: string): DomainRecord | null {
|
|
@@ -268,7 +321,7 @@ export class DomainRepository {
|
|
|
268
321
|
files: catalogFiles,
|
|
269
322
|
agent_hint:
|
|
270
323
|
detailLevel === "compact"
|
|
271
|
-
? "Compact view (<400 tokens). Use
|
|
324
|
+
? "Compact view (<400 tokens). Use septum_symbol({ action: 'inspect', query: symbol }) for exact method signatures, or septum_domain({ action: 'catalog', domain, detail_level: 'full', file_filter: '<path>' }) for full member lists."
|
|
272
325
|
: undefined,
|
|
273
326
|
};
|
|
274
327
|
}
|