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.
Files changed (72) hide show
  1. package/README.md +38 -37
  2. package/package.json +6 -5
  3. package/src/cli/commands/check.ts +34 -122
  4. package/src/cli/commands/hook.ts +68 -16
  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 +7 -68
  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/evaluator.ts +21 -25
  15. package/src/core/boundary/staged-auditor.ts +50 -0
  16. package/src/core/config/loader.ts +21 -24
  17. package/src/core/database/client.ts +8 -0
  18. package/src/core/database/repositories/dependency.repository.ts +80 -16
  19. package/src/core/database/repositories/domain.repository.ts +86 -33
  20. package/src/core/database/repositories/symbol.repository.ts +147 -65
  21. package/src/core/database/repository.ts +17 -5
  22. package/src/core/database/schema.ts +1 -1
  23. package/src/core/discovery/topology-detector.ts +71 -19
  24. package/src/core/ingestion/pipeline.ts +274 -126
  25. package/src/core/parser/extractors/frontend.ts +1 -0
  26. package/src/core/parser/extractors/go.ts +2 -2
  27. package/src/core/parser/extractors/laravel-semantic.ts +9 -8
  28. package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
  29. package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
  30. package/src/core/parser/extractors/php.ts +2 -2
  31. package/src/core/parser/extractors/python.ts +2 -2
  32. package/src/core/parser/extractors/typescript.ts +2 -2
  33. package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
  34. package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
  35. package/src/core/parser/tree-sitter.ts +6 -1
  36. package/src/core/parser/wasm-assets.ts +17 -0
  37. package/src/core/parser/wasm-loader.ts +3 -0
  38. package/src/core/resolver/call-graph-tracer.ts +4 -3
  39. package/src/core/resolver/module-resolver.ts +1 -10
  40. package/src/core/resolver/path-resolver.ts +57 -4
  41. package/src/core/resolver/vertical-slice-tracer.ts +1 -0
  42. package/src/core/resources/ignore.ts +35 -0
  43. package/src/core/resources/limits.ts +14 -0
  44. package/src/core/resources/source-files.ts +62 -0
  45. package/src/core/resources/work-queue.ts +74 -0
  46. package/src/core/session/session-manager.ts +67 -38
  47. package/src/core/telemetry/telemetry.ts +12 -1
  48. package/src/core/topology/environment-detector.ts +21 -11
  49. package/src/mcp/schemas.ts +381 -8
  50. package/src/mcp/server.ts +326 -529
  51. package/src/mcp/tools/boundary.ts +17 -0
  52. package/src/mcp/tools/check-boundary.ts +97 -43
  53. package/src/mcp/tools/domain.ts +40 -0
  54. package/src/mcp/tools/feature.ts +67 -0
  55. package/src/mcp/tools/get-domain-catalog.ts +15 -7
  56. package/src/mcp/tools/get-environment-topology.ts +2 -2
  57. package/src/mcp/tools/get-feature-context.ts +9 -12
  58. package/src/mcp/tools/get-symbol-impact.ts +5 -2
  59. package/src/mcp/tools/get-symbol.ts +2 -1
  60. package/src/mcp/tools/locate-symbol.ts +28 -5
  61. package/src/mcp/tools/register-domain.ts +25 -9
  62. package/src/mcp/tools/register-feature.ts +40 -21
  63. package/src/mcp/tools/slice.ts +15 -0
  64. package/src/mcp/tools/symbol.ts +82 -0
  65. package/src/mcp/workspace-manager.ts +159 -0
  66. package/src/mcp/workspace-watcher.ts +202 -0
  67. package/src/types/index.ts +21 -1
  68. package/src/types/wasm.d.ts +4 -0
  69. package/src/cli/commands/ingest.ts +0 -29
  70. package/src/cli/commands/locate.ts +0 -61
  71. package/src/cli/commands/query.ts +0 -104
  72. 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,4 +1,4 @@
1
- import { posix } from "node:path";
1
+ import { posix, relative, resolve, isAbsolute } from "node:path";
2
2
  import type { BoundaryViolation } from "../../types/index.ts";
3
3
  import type { ValidatedSeptumConfig } from "../config/schema.ts";
4
4
  import { ConfigLoader } from "../config/loader.ts";
@@ -105,7 +105,8 @@ export class BoundaryEvaluator {
105
105
  target: string;
106
106
  statement: string;
107
107
  is_external: boolean;
108
- }>
108
+ }>,
109
+ relevantFiles?: ReadonlySet<string>
109
110
  ): BoundaryViolation[] {
110
111
  const violations: BoundaryViolation[] = [];
111
112
  const allDependencies = preloadedDeps ?? this.repo.getAllDependenciesWithDomains();
@@ -158,6 +159,7 @@ export class BoundaryEvaluator {
158
159
  };
159
160
 
160
161
  type EdgeMeta = {
162
+ files: Set<string>;
161
163
  file: string;
162
164
  line: number;
163
165
  source_domain: string;
@@ -182,6 +184,7 @@ export class BoundaryEvaluator {
182
184
  }
183
185
  if (!outMap.has(targetDomain)) {
184
186
  outMap.set(targetDomain, {
187
+ files: new Set([dep.source_file]),
185
188
  file: dep.source_file,
186
189
  line: dep.line_number,
187
190
  source_domain: dep.source_domain,
@@ -189,6 +192,7 @@ export class BoundaryEvaluator {
189
192
  target: dep.target,
190
193
  });
191
194
  }
195
+ outMap.get(targetDomain)!.files.add(dep.source_file);
192
196
  }
193
197
 
194
198
  // 2. File-to-file edge
@@ -201,6 +205,7 @@ export class BoundaryEvaluator {
201
205
  }
202
206
  if (!outMap.has(targetFile)) {
203
207
  outMap.set(targetFile, {
208
+ files: new Set([dep.source_file]),
204
209
  file: dep.source_file,
205
210
  line: dep.line_number,
206
211
  source_domain: dep.source_domain,
@@ -245,6 +250,8 @@ export class BoundaryEvaluator {
245
250
  if (cycleStartIndex !== -1) {
246
251
  const cycleNodes = stack.slice(cycleStartIndex);
247
252
  const sig = canonicalizeCycle(cycleNodes);
253
+ const cycleEdges = cycleNodes.map((source, index) => graph.get(source)?.get(cycleNodes[(index + 1) % cycleNodes.length]));
254
+ if (relevantFiles && !cycleEdges.some(edge => edge && [...edge.files].some(file => relevantFiles.has(file)))) continue;
248
255
  if (!reportedCycles.has(sig)) {
249
256
  reportedCycles.add(sig);
250
257
  const firstNode = cycleNodes[0];
@@ -378,39 +385,28 @@ export class BoundaryEvaluator {
378
385
  return violations;
379
386
  }
380
387
 
381
- const normalizedTarget = targetFilePath.replace(/\\/g, "/").replace(/^\.\//, "");
388
+ const root = this.resolver.getProjectRoot();
389
+ const normalizedTarget = relative(root, resolve(root, targetFilePath.replace(/\\/g, "/"))).replace(/\\/g, "/");
382
390
  const allowedTouchpoints = featureConfig.allowed_touchpoints.map((tp) =>
383
- tp.replace(/\\/g, "/").replace(/^\.\//, "")
391
+ isAbsolute(tp) ? relative(root, tp).replace(/\\/g, "/") : posix.normalize(tp.replace(/\\/g, "/"))
384
392
  );
385
393
 
386
394
  const domainCfg = config.domains[featureConfig.domain];
387
395
  const domainRoot = domainCfg?.root ? domainCfg.root.replace(/\\/g, "/").replace(/^\.\//, "") : "";
388
396
 
389
- const isAllowed = allowedTouchpoints.some((pattern) => {
397
+ const matches = (pattern: string): boolean => {
390
398
  if (pattern.endsWith("/**")) {
391
- const prefix = pattern.slice(0, -3);
392
- return normalizedTarget.startsWith(prefix);
399
+ return normalizedTarget.startsWith(pattern.slice(0, -3) + "/");
393
400
  }
394
401
  if (pattern.endsWith("/*")) {
395
- const prefix = pattern.slice(0, -2);
396
- return normalizedTarget.startsWith(prefix);
402
+ const prefix = pattern.slice(0, -2) + "/";
403
+ return normalizedTarget.startsWith(prefix) && !normalizedTarget.slice(prefix.length).includes("/");
397
404
  }
398
- if (normalizedTarget === pattern) return true;
399
-
400
- if (
401
- domainRoot &&
402
- (normalizedTarget === `${domainRoot}/${pattern}` ||
403
- normalizedTarget.startsWith(`${domainRoot}/${pattern}`))
404
- ) {
405
- return true;
406
- }
407
-
408
- if (pattern.includes("/") && normalizedTarget.endsWith("/" + pattern)) {
409
- return true;
410
- }
411
-
412
- return false;
413
- });
405
+ return normalizedTarget === pattern;
406
+ };
407
+ const isAllowed = allowedTouchpoints.some(pattern =>
408
+ matches(pattern) || Boolean(domainRoot && matches(posix.normalize(`${domainRoot}/${pattern}`)))
409
+ );
414
410
 
415
411
  if (!isAllowed) {
416
412
  violations.push({
@@ -0,0 +1,50 @@
1
+ import { RESOURCE_LIMITS } from "../resources/limits.ts";
2
+ import { execFileSync } from "node:child_process";
3
+ import { resolve } from "node:path";
4
+ import type { BoundaryViolation } from "../../types/index.ts";
5
+ import type { ValidatedSeptumConfig } from "../config/schema.ts";
6
+ import { ASTParserEngine } from "../parser/tree-sitter.ts";
7
+ import type { SeptumRepository } from "../database/repository.ts";
8
+ import type { BoundaryEvaluator } from "./evaluator.ts";
9
+
10
+ export async function auditStagedFiles(
11
+ evaluator: BoundaryEvaluator,
12
+ config: ValidatedSeptumConfig,
13
+ workspaceRoot: string,
14
+ featureKey?: string,
15
+ repo?: SeptumRepository
16
+ ): Promise<{ files: string[]; violations: BoundaryViolation[] }> {
17
+ const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"], {
18
+ cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: 10 * 1024 * 1024,
19
+ });
20
+ const files = output.split("\0").filter(Boolean);
21
+ const deletedOutput = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=D", "-z"], {
22
+ cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: 10 * 1024 * 1024,
23
+ });
24
+ const deleted = deletedOutput.split("\0").filter(Boolean);
25
+ const violations: BoundaryViolation[] = [];
26
+ if (featureKey) for (const file of deleted) violations.push(...evaluator.checkFeatureTouchpoints(featureKey, file, config));
27
+ const stagedDependencies: ReturnType<SeptumRepository["getAllDependenciesWithDomains"]> = [];
28
+ const parser = new ASTParserEngine();
29
+ for (const file of files) {
30
+ if (featureKey) violations.push(...evaluator.checkFeatureTouchpoints(featureKey, file, config));
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}'.`);
34
+ const content = execFileSync("git", ["show", `:${file}`], {
35
+ cwd: workspaceRoot, encoding: "utf-8", timeout: 5000, maxBuffer: RESOURCE_LIMITS.sourceBytes + 1,
36
+ });
37
+ const parsed = await parser.parseFile(resolve(workspaceRoot, file), content);
38
+ if (parsed.status === "unsupported" || parsed.status === "degraded") {
39
+ throw new Error(`Cannot reliably parse staged file '${file}': ${parsed.status}.`);
40
+ }
41
+ for (const dependency of parsed.dependencies) {
42
+ stagedDependencies.push({ source_file: file, source_domain: evaluator.resolveSourceDomain(file, config)!, target: dependency.target, statement: dependency.statement, line_number: dependency.line_number, is_external: dependency.is_external });
43
+ violations.push(...evaluator.checkProposedChanges(file, [dependency.target], config).map(violation => ({ ...violation, line: dependency.line_number })));
44
+ }
45
+ }
46
+ const staged = new Set([...files, ...deleted]);
47
+ const baseline = (repo?.getAllDependenciesWithDomains() || []).filter(dependency => !staged.has(dependency.source_file));
48
+ violations.push(...evaluator.detectCircularDependencies(config, [...baseline, ...stagedDependencies], staged));
49
+ return { files: Array.from(staged), violations };
50
+ }
@@ -65,20 +65,23 @@ export class ConfigLoader {
65
65
  public static loadFromRepository(repo: SeptumRepository): ValidatedSeptumConfig {
66
66
  const config = ConfigLoader.getDefaultConfig();
67
67
  if (repo && typeof repo.getAllDomains === "function") {
68
- try {
69
- const rows = repo.getAllDomains();
70
- for (const r of rows) {
71
- config.domains[r.name] = {
72
- root: r.root_path,
73
- allowed_dependencies: JSON.parse(r.allowed_deps_json || "[]"),
74
- forbidden_dependencies: JSON.parse(r.forbidden_deps_json || "[]"),
75
- archetypes: JSON.parse(r.archetypes_json || "{}"),
76
- };
77
- }
78
- } catch {
79
- // Fallback gracefully to default config
68
+ const rows = repo.getAllDomains();
69
+ for (const row of rows) {
70
+ config.domains[row.name] = {
71
+ root: row.root_path,
72
+ allowed_dependencies: JSON.parse(row.allowed_deps_json || "[]"),
73
+ forbidden_dependencies: JSON.parse(row.forbidden_deps_json || "[]"),
74
+ archetypes: JSON.parse(row.archetypes_json || "{}"),
75
+ };
80
76
  }
81
77
  }
78
+
79
+ if (typeof repo?.getMeta === "function") {
80
+ const features = repo.getMeta("features_json");
81
+ if (features) config.features = JSON.parse(features);
82
+ const settings = repo.getMeta("settings_json");
83
+ if (settings) Object.assign(config.settings, JSON.parse(settings));
84
+ }
82
85
  return config;
83
86
  }
84
87
 
@@ -95,20 +98,14 @@ export class ConfigLoader {
95
98
  )
96
99
  .get("domains");
97
100
 
98
- if (tables) {
99
- const rows = db.query<any, []>("SELECT * FROM domains").all();
100
- for (const r of rows) {
101
- config.domains[r.name] = {
102
- root: r.root_path,
103
- allowed_dependencies: JSON.parse(r.allowed_deps_json || "[]"),
104
- forbidden_dependencies: JSON.parse(r.forbidden_deps_json || "[]"),
105
- archetypes: JSON.parse(r.archetypes_json || "{}"),
106
- };
107
- }
101
+ try {
102
+ if (tables) Object.assign(config, ConfigLoader.loadFromRepository(new SeptumRepository(db)));
103
+ config.settings.db_path = dbPath;
104
+ } finally {
105
+ db.close();
108
106
  }
109
- db.close();
110
107
  } catch {
111
- // Fallback gracefully to default config
108
+ throw new Error(`Cannot load Septum configuration from '${dbPath}'.`);
112
109
  }
113
110
  }
114
111
 
@@ -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 {
@@ -258,7 +261,7 @@ export class DependencyRepository {
258
261
  warning: "Empty symbol query provided.",
259
262
  impact_summary: {
260
263
  total_dependents: 0,
261
- risk_level: "low",
264
+ risk_level: "unknown",
262
265
  affected_files_count: 0,
263
266
  },
264
267
  inbound_callers: [],
@@ -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];
@@ -463,7 +527,7 @@ export class DependencyRepository {
463
527
  const foundInIndex = targetSymRows.length > 0 || depRows.length > 0;
464
528
  const warning = foundInIndex
465
529
  ? undefined
466
- : `Symbol '${trimmed}' was not found in Septum index (neither in declared symbols nor tracked dependencies). Risk assessment defaults to low (0 callers) due to unindexed status.`;
530
+ : `Symbol '${trimmed}' was not found in Septum index (neither in declared symbols nor tracked dependencies). Risk cannot be assessed without index evidence.`;
467
531
 
468
532
  return {
469
533
  target_symbol: trimmed,
@@ -471,7 +535,7 @@ export class DependencyRepository {
471
535
  warning,
472
536
  impact_summary: {
473
537
  total_dependents: inboundCallers.length,
474
- risk_level: riskLevel,
538
+ risk_level: foundInIndex ? riskLevel : "unknown",
475
539
  affected_files_count: affectedFiles.size,
476
540
  },
477
541
  inbound_callers: inboundCallers,