septum 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -37
- package/package.json +6 -5
- package/src/cli/commands/check.ts +34 -122
- package/src/cli/commands/hook.ts +68 -16
- package/src/cli/commands/init.ts +440 -121
- package/src/cli/commands/sync.ts +27 -17
- package/src/cli/commands/topology.ts +89 -47
- package/src/cli/index.ts +7 -68
- package/src/cli/scaffold/harness-config.ts +697 -0
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +8 -8
- package/src/cli/templates/skill.ts +11 -18
- package/src/cli/ui/prompts.ts +122 -0
- package/src/core/boundary/evaluator.ts +21 -25
- package/src/core/boundary/staged-auditor.ts +50 -0
- package/src/core/config/loader.ts +21 -24
- package/src/core/database/client.ts +8 -0
- package/src/core/database/repositories/dependency.repository.ts +80 -16
- package/src/core/database/repositories/domain.repository.ts +86 -33
- package/src/core/database/repositories/symbol.repository.ts +147 -65
- package/src/core/database/repository.ts +17 -5
- package/src/core/database/schema.ts +1 -1
- package/src/core/discovery/topology-detector.ts +71 -19
- package/src/core/ingestion/pipeline.ts +274 -126
- package/src/core/parser/extractors/frontend.ts +1 -0
- package/src/core/parser/extractors/go.ts +2 -2
- package/src/core/parser/extractors/laravel-semantic.ts +9 -8
- package/src/core/parser/extractors/mcp-cli-semantic.ts +56 -16
- package/src/core/parser/extractors/nestjs-semantic.ts +3 -2
- package/src/core/parser/extractors/php.ts +2 -2
- package/src/core/parser/extractors/python.ts +2 -2
- package/src/core/parser/extractors/typescript.ts +2 -2
- package/src/core/parser/schema/providers/laravel-migration-provider.ts +4 -3
- package/src/core/parser/schema/providers/prisma-schema-provider.ts +2 -1
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +3 -0
- package/src/core/resolver/call-graph-tracer.ts +4 -3
- package/src/core/resolver/module-resolver.ts +1 -10
- package/src/core/resolver/path-resolver.ts +57 -4
- package/src/core/resolver/vertical-slice-tracer.ts +1 -0
- package/src/core/resources/ignore.ts +35 -0
- package/src/core/resources/limits.ts +14 -0
- package/src/core/resources/source-files.ts +62 -0
- package/src/core/resources/work-queue.ts +74 -0
- package/src/core/session/session-manager.ts +67 -38
- package/src/core/telemetry/telemetry.ts +12 -1
- package/src/core/topology/environment-detector.ts +21 -11
- package/src/mcp/schemas.ts +381 -8
- package/src/mcp/server.ts +326 -529
- package/src/mcp/tools/boundary.ts +17 -0
- package/src/mcp/tools/check-boundary.ts +97 -43
- package/src/mcp/tools/domain.ts +40 -0
- package/src/mcp/tools/feature.ts +67 -0
- package/src/mcp/tools/get-domain-catalog.ts +15 -7
- package/src/mcp/tools/get-environment-topology.ts +2 -2
- package/src/mcp/tools/get-feature-context.ts +9 -12
- package/src/mcp/tools/get-symbol-impact.ts +5 -2
- package/src/mcp/tools/get-symbol.ts +2 -1
- package/src/mcp/tools/locate-symbol.ts +28 -5
- package/src/mcp/tools/register-domain.ts +25 -9
- package/src/mcp/tools/register-feature.ts +40 -21
- package/src/mcp/tools/slice.ts +15 -0
- package/src/mcp/tools/symbol.ts +82 -0
- package/src/mcp/workspace-manager.ts +159 -0
- package/src/mcp/workspace-watcher.ts +202 -0
- package/src/types/index.ts +21 -1
- package/src/types/wasm.d.ts +4 -0
- package/src/cli/commands/ingest.ts +0 -29
- package/src/cli/commands/locate.ts +0 -61
- package/src/cli/commands/query.ts +0 -104
- package/src/cli/commands/slice.ts +0 -50
|
@@ -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,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
|
|
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
|
|
397
|
+
const matches = (pattern: string): boolean => {
|
|
390
398
|
if (pattern.endsWith("/**")) {
|
|
391
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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(
|
|
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: "
|
|
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
|
|
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];
|
|
@@ -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
|
|
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,
|