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.
Files changed (59) hide show
  1. package/README.md +36 -55
  2. package/package.json +3 -2
  3. package/src/cli/commands/check.ts +2 -0
  4. package/src/cli/commands/hook.ts +59 -12
  5. package/src/cli/commands/init.ts +440 -121
  6. package/src/cli/commands/sync.ts +27 -17
  7. package/src/cli/commands/topology.ts +89 -47
  8. package/src/cli/index.ts +5 -67
  9. package/src/cli/scaffold/harness-config.ts +697 -0
  10. package/src/cli/templates/hooks.ts +6 -6
  11. package/src/cli/templates/rules.ts +8 -8
  12. package/src/cli/templates/skill.ts +11 -18
  13. package/src/cli/ui/prompts.ts +122 -0
  14. package/src/core/boundary/staged-auditor.ts +5 -2
  15. package/src/core/database/client.ts +8 -0
  16. package/src/core/database/repositories/dependency.repository.ts +77 -13
  17. package/src/core/database/repositories/domain.repository.ts +86 -33
  18. package/src/core/database/repositories/symbol.repository.ts +128 -117
  19. package/src/core/database/repository.ts +17 -5
  20. package/src/core/database/schema.ts +1 -1
  21. package/src/core/discovery/topology-detector.ts +71 -19
  22. package/src/core/ingestion/pipeline.ts +228 -94
  23. package/src/core/parser/extractors/laravel-semantic.ts +9 -8
  24. package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
  25. package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
  26. package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
  27. package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
  28. package/src/core/resolver/call-graph-tracer.ts +4 -3
  29. package/src/core/resolver/path-resolver.ts +57 -4
  30. package/src/core/resources/ignore.ts +35 -0
  31. package/src/core/resources/limits.ts +14 -0
  32. package/src/core/resources/source-files.ts +62 -0
  33. package/src/core/resources/work-queue.ts +74 -0
  34. package/src/core/session/session-manager.ts +19 -6
  35. package/src/core/telemetry/telemetry.ts +12 -1
  36. package/src/core/topology/environment-detector.ts +21 -11
  37. package/src/mcp/schemas.ts +381 -8
  38. package/src/mcp/server.ts +321 -535
  39. package/src/mcp/tools/boundary.ts +17 -0
  40. package/src/mcp/tools/check-boundary.ts +83 -11
  41. package/src/mcp/tools/domain.ts +40 -0
  42. package/src/mcp/tools/feature.ts +67 -0
  43. package/src/mcp/tools/get-domain-catalog.ts +9 -6
  44. package/src/mcp/tools/get-environment-topology.ts +2 -2
  45. package/src/mcp/tools/get-feature-context.ts +9 -12
  46. package/src/mcp/tools/get-symbol-impact.ts +5 -2
  47. package/src/mcp/tools/get-symbol.ts +2 -1
  48. package/src/mcp/tools/locate-symbol.ts +3 -3
  49. package/src/mcp/tools/register-domain.ts +25 -9
  50. package/src/mcp/tools/register-feature.ts +34 -17
  51. package/src/mcp/tools/slice.ts +15 -0
  52. package/src/mcp/tools/symbol.ts +82 -0
  53. package/src/mcp/workspace-manager.ts +159 -0
  54. package/src/mcp/workspace-watcher.ts +202 -0
  55. package/src/types/index.ts +1 -0
  56. package/src/cli/commands/ingest.ts +0 -29
  57. package/src/cli/commands/locate.ts +0 -61
  58. package/src/cli/commands/query.ts +0 -104
  59. 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 'HOOK=\"$(git rev-parse --show-toplevel 2>/dev/null)/.agents/hooks/septum-pre-write.sh\"; if [ -f \"$HOOK\" ]; then exec \"$HOOK\"; else exit 0; fi'",
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 'HOOK=\"$(git rev-parse --show-toplevel 2>/dev/null)/.agents/hooks/septum-post-write.sh\"; if [ -f \"$HOOK\" ]; then exec \"$HOOK\"; else exit 0; fi'",
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 \`septum_get_domain_catalog({ domain: "<nama_domain>", detail_level: "compact" })\` (atau tanpa \`domain\` untuk *Macro Semantic Map*).
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 \`septum_get_symbol({ symbol: "<NamaSimbol>" })\` atau \`septum_get_domain_catalog({ domain: "<nama_domain>", detail_level: "full", file_filter: "<nama_file>" })\`.
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 \`septum_locate_symbol({ query: "<kata_kunci>" })\` untuk memastikan fungsi/helper/class yang ingin dibuat belum ada di codebase.
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 \`septum_get_feature_context({ feature_key: "<nama_fitur>" })\`:
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
- septum_get_symbol_impact({ "symbol": "NamaClassAtauFungsi" })
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 \`septum_trace_vertical_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.
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
- septum_check_boundary({
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 \`septum_check_boundary({ scope: "staged" })\` atau \`septum_check_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.
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 12 Tool MCP Septum (Standardized \`snake_case\` Contracts)
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
- | **\`septum_get_domain_catalog\`** | \`{ domain?, detail_level?: "compact"\\|"full", archetype_filter?, file_filter? }\` | Tanpa \`domain\`: menampilkan *Macro Semantic Map* seluruh proyek. Dengan \`domain\`: menampilkan daftar file, \`connections\` (\`imports_from\`, \`imported_by_count\`), dan simbol di domain tersebut (\`compact\` default < 400 token). |
28
- | **\`septum_locate_symbol\`** | \`{ query, domain?, limit?: 10 }\` | Mencari lokasi file dan baris (\`line_start\`-\`line_end\`) dari class, fungsi, method (\`Class::method\`), struct, atau konstanta. |
29
- | **\`septum_get_symbol\`** | \`{ symbol, domain?, include_dependencies?: true }\` | Mengekstrak detail kontrak simbol: signature, \`exact_range\`, \`dependencies_injected\`, \`outbound_calls\`, \`sibling_methods\`, dan \`connected_files\`. |
30
- | **\`septum_get_symbol_impact\`** | \`{ symbol }\` | Menghitung *blast radius* (\`inbound_callers\`, \`total_dependents\`, \`risk_level\`) termasuk pelacakan *named imports* lintas file sebelum mengubah signature. |
31
- | **\`septum_trace_vertical_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). |
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 \`septum_get_domain_catalog({})\` untuk memeriksa *Telescope View* dan daftar domain yang terdaftar.
51
- 2. Panggil \`septum_get_domain_catalog({ domain: "<target>", detail_level: "compact" })\` untuk memetakan file, simbol yang sudah ada, dan koneksi \`imports_from\` / \`imported_by_count\`.
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 \`septum_register_domain({ domain, root, allowed_dependencies, forbidden_dependencies, ingest_now: true })\` untuk menyimpan batas domain.
60
- 2. Gunakan \`septum_register_feature({ feature_key, domain, allowed_touchpoints, reuse_symbols })\` untuk mengunci fokus pengerjaan fitur.
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 \`septum_check_boundary({ scope: "all" })\` (atau CLI \`septum check --staged\`) untuk memastikan 0 pelanggaran batas domain dan 0 *circular dependency*.
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: 10 * 1024 * 1024,
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(symbolQuery: string): GetSymbolImpactResponse {
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 to find its defining file_path & stem
281
- const targetSymRows = this.db
282
- .query<{ name: string; file_path: string }, [string, string, string, string]>(
283
- `SELECT s.name, f.path AS file_path
284
- FROM symbols s
285
- JOIN files f ON s.file_id = f.id
286
- WHERE s.name = ? OR s.name = ? OR s.name LIKE ? OR s.name LIKE ?`
287
- )
288
- .all(trimmed, targetContainer, `${targetContainer}::%`, `%::${targetContainer}`);
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
- if (fs.existsSync(filePath)) {
419
- const content = fs.readFileSync(filePath, "utf-8");
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
- const normalizedName = name.trim();
25
- const existing = this.getDomainByName(normalizedName);
24
+ return this.db.transaction(() => {
25
+ const normalizedName = name.trim();
26
+ let existing = this.getDomainByName(normalizedName);
26
27
 
27
- // Merge dependencies and archetypes if existing and incoming is empty
28
- let allowed = config.allowed_dependencies ?? [];
29
- let forbidden = config.forbidden_dependencies ?? [];
30
- let archetypes = config.archetypes ?? {};
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
- if (existing) {
33
- if (allowed.length === 0 && existing.allowed_deps_json && existing.allowed_deps_json !== "[]") {
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
- if (forbidden.length === 0 && existing.forbidden_deps_json && existing.forbidden_deps_json !== "[]") {
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
- if (Object.keys(archetypes).length === 0 && existing.archetypes_json && existing.archetypes_json !== "{}") {
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
- const allowedJson = JSON.stringify(allowed);
51
- const forbiddenJson = JSON.stringify(forbidden);
52
- const archetypesJson = JSON.stringify(archetypes);
53
-
54
- const query = `
55
- INSERT INTO domains (name, root_path, allowed_deps_json, forbidden_deps_json, archetypes_json)
56
- VALUES (?, ?, ?, ?, ?)
57
- ON CONFLICT(name) DO UPDATE SET
58
- root_path = excluded.root_path,
59
- allowed_deps_json = excluded.allowed_deps_json,
60
- forbidden_deps_json = excluded.forbidden_deps_json,
61
- archetypes_json = excluded.archetypes_json,
62
- updated_at = CURRENT_TIMESTAMP
63
- `;
64
- this.db.query(query).run(normalizedName, config.root, allowedJson, forbiddenJson, archetypesJson);
65
-
66
- const record = this.getDomainByName(normalizedName);
67
- return record?.id ?? 0;
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 septum_get_symbol({ symbol }) for exact method signatures, or septum_get_domain_catalog({ domain, detail_level: 'full', file_filter: '<path>' }) for full member lists."
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
  }