@lbroth/rothunter 1.0.0-rc.1

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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/dist/adapters/llm.d.ts +68 -0
  4. package/dist/adapters/llm.d.ts.map +1 -0
  5. package/dist/adapters/llm.js +189 -0
  6. package/dist/adapters/llm.js.map +1 -0
  7. package/dist/config.d.ts +37 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +81 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/detector-registry.d.ts +32 -0
  12. package/dist/detector-registry.d.ts.map +1 -0
  13. package/dist/detector-registry.js +74 -0
  14. package/dist/detector-registry.js.map +1 -0
  15. package/dist/detectors/api-race.d.ts +6 -0
  16. package/dist/detectors/api-race.d.ts.map +1 -0
  17. package/dist/detectors/api-race.js +222 -0
  18. package/dist/detectors/api-race.js.map +1 -0
  19. package/dist/detectors/bad-config.d.ts +6 -0
  20. package/dist/detectors/bad-config.d.ts.map +1 -0
  21. package/dist/detectors/bad-config.js +529 -0
  22. package/dist/detectors/bad-config.js.map +1 -0
  23. package/dist/detectors/console-log-prod.d.ts +6 -0
  24. package/dist/detectors/console-log-prod.d.ts.map +1 -0
  25. package/dist/detectors/console-log-prod.js +72 -0
  26. package/dist/detectors/console-log-prod.js.map +1 -0
  27. package/dist/detectors/dead-api.d.ts +10 -0
  28. package/dist/detectors/dead-api.d.ts.map +1 -0
  29. package/dist/detectors/dead-api.js +115 -0
  30. package/dist/detectors/dead-api.js.map +1 -0
  31. package/dist/detectors/dead-export.d.ts +12 -0
  32. package/dist/detectors/dead-export.d.ts.map +1 -0
  33. package/dist/detectors/dead-export.js +140 -0
  34. package/dist/detectors/dead-export.js.map +1 -0
  35. package/dist/detectors/dead-handler.d.ts +12 -0
  36. package/dist/detectors/dead-handler.d.ts.map +1 -0
  37. package/dist/detectors/dead-handler.js +40 -0
  38. package/dist/detectors/dead-handler.js.map +1 -0
  39. package/dist/detectors/dead-module.d.ts +14 -0
  40. package/dist/detectors/dead-module.d.ts.map +1 -0
  41. package/dist/detectors/dead-module.js +50 -0
  42. package/dist/detectors/dead-module.js.map +1 -0
  43. package/dist/detectors/deep-nesting.d.ts +12 -0
  44. package/dist/detectors/deep-nesting.d.ts.map +1 -0
  45. package/dist/detectors/deep-nesting.js +133 -0
  46. package/dist/detectors/deep-nesting.js.map +1 -0
  47. package/dist/detectors/duplicate-function.d.ts +9 -0
  48. package/dist/detectors/duplicate-function.d.ts.map +1 -0
  49. package/dist/detectors/duplicate-function.js +199 -0
  50. package/dist/detectors/duplicate-function.js.map +1 -0
  51. package/dist/detectors/duplicate-type.d.ts +9 -0
  52. package/dist/detectors/duplicate-type.d.ts.map +1 -0
  53. package/dist/detectors/duplicate-type.js +166 -0
  54. package/dist/detectors/duplicate-type.js.map +1 -0
  55. package/dist/detectors/hot-hub-file.d.ts +11 -0
  56. package/dist/detectors/hot-hub-file.d.ts.map +1 -0
  57. package/dist/detectors/hot-hub-file.js +42 -0
  58. package/dist/detectors/hot-hub-file.js.map +1 -0
  59. package/dist/detectors/long-file.d.ts +12 -0
  60. package/dist/detectors/long-file.d.ts.map +1 -0
  61. package/dist/detectors/long-file.js +82 -0
  62. package/dist/detectors/long-file.js.map +1 -0
  63. package/dist/detectors/long-function.d.ts +12 -0
  64. package/dist/detectors/long-function.d.ts.map +1 -0
  65. package/dist/detectors/long-function.js +45 -0
  66. package/dist/detectors/long-function.js.map +1 -0
  67. package/dist/detectors/magic-numbers.d.ts +10 -0
  68. package/dist/detectors/magic-numbers.d.ts.map +1 -0
  69. package/dist/detectors/magic-numbers.js +332 -0
  70. package/dist/detectors/magic-numbers.js.map +1 -0
  71. package/dist/detectors/mutable-globals.d.ts +6 -0
  72. package/dist/detectors/mutable-globals.d.ts.map +1 -0
  73. package/dist/detectors/mutable-globals.js +95 -0
  74. package/dist/detectors/mutable-globals.js.map +1 -0
  75. package/dist/detectors/mutation.d.ts +11 -0
  76. package/dist/detectors/mutation.d.ts.map +1 -0
  77. package/dist/detectors/mutation.js +397 -0
  78. package/dist/detectors/mutation.js.map +1 -0
  79. package/dist/detectors/public-any.d.ts +6 -0
  80. package/dist/detectors/public-any.d.ts.map +1 -0
  81. package/dist/detectors/public-any.js +52 -0
  82. package/dist/detectors/public-any.js.map +1 -0
  83. package/dist/detectors/race-condition.d.ts +6 -0
  84. package/dist/detectors/race-condition.d.ts.map +1 -0
  85. package/dist/detectors/race-condition.js +608 -0
  86. package/dist/detectors/race-condition.js.map +1 -0
  87. package/dist/detectors/shared-db-write.d.ts +6 -0
  88. package/dist/detectors/shared-db-write.d.ts.map +1 -0
  89. package/dist/detectors/shared-db-write.js +656 -0
  90. package/dist/detectors/shared-db-write.js.map +1 -0
  91. package/dist/detectors/silent-catch.d.ts +6 -0
  92. package/dist/detectors/silent-catch.d.ts.map +1 -0
  93. package/dist/detectors/silent-catch.js +167 -0
  94. package/dist/detectors/silent-catch.js.map +1 -0
  95. package/dist/detectors/similar-functions.d.ts +15 -0
  96. package/dist/detectors/similar-functions.d.ts.map +1 -0
  97. package/dist/detectors/similar-functions.js +334 -0
  98. package/dist/detectors/similar-functions.js.map +1 -0
  99. package/dist/detectors/skip-tests.d.ts +6 -0
  100. package/dist/detectors/skip-tests.d.ts.map +1 -0
  101. package/dist/detectors/skip-tests.js +69 -0
  102. package/dist/detectors/skip-tests.js.map +1 -0
  103. package/dist/detectors/todo-comments.d.ts +29 -0
  104. package/dist/detectors/todo-comments.d.ts.map +1 -0
  105. package/dist/detectors/todo-comments.js +154 -0
  106. package/dist/detectors/todo-comments.js.map +1 -0
  107. package/dist/detectors/unused-deps.d.ts +8 -0
  108. package/dist/detectors/unused-deps.d.ts.map +1 -0
  109. package/dist/detectors/unused-deps.js +115 -0
  110. package/dist/detectors/unused-deps.js.map +1 -0
  111. package/dist/extraction/api-race-confirmer.d.ts +31 -0
  112. package/dist/extraction/api-race-confirmer.d.ts.map +1 -0
  113. package/dist/extraction/api-race-confirmer.js +110 -0
  114. package/dist/extraction/api-race-confirmer.js.map +1 -0
  115. package/dist/extraction/llm-confirmer.d.ts +25 -0
  116. package/dist/extraction/llm-confirmer.d.ts.map +1 -0
  117. package/dist/extraction/llm-confirmer.js +118 -0
  118. package/dist/extraction/llm-confirmer.js.map +1 -0
  119. package/dist/extraction/mutation-confirmer.d.ts +30 -0
  120. package/dist/extraction/mutation-confirmer.d.ts.map +1 -0
  121. package/dist/extraction/mutation-confirmer.js +73 -0
  122. package/dist/extraction/mutation-confirmer.js.map +1 -0
  123. package/dist/extraction/prompt-chunking.d.ts +37 -0
  124. package/dist/extraction/prompt-chunking.d.ts.map +1 -0
  125. package/dist/extraction/prompt-chunking.js +61 -0
  126. package/dist/extraction/prompt-chunking.js.map +1 -0
  127. package/dist/extraction/race-confirmer.d.ts +28 -0
  128. package/dist/extraction/race-confirmer.d.ts.map +1 -0
  129. package/dist/extraction/race-confirmer.js +68 -0
  130. package/dist/extraction/race-confirmer.js.map +1 -0
  131. package/dist/extraction/shared-db-write-confirmer.d.ts +31 -0
  132. package/dist/extraction/shared-db-write-confirmer.d.ts.map +1 -0
  133. package/dist/extraction/shared-db-write-confirmer.js +141 -0
  134. package/dist/extraction/shared-db-write-confirmer.js.map +1 -0
  135. package/dist/extraction/triage-confirmer.d.ts +59 -0
  136. package/dist/extraction/triage-confirmer.d.ts.map +1 -0
  137. package/dist/extraction/triage-confirmer.js +104 -0
  138. package/dist/extraction/triage-confirmer.js.map +1 -0
  139. package/dist/graph/cfg.d.ts +45 -0
  140. package/dist/graph/cfg.d.ts.map +1 -0
  141. package/dist/graph/cfg.js +198 -0
  142. package/dist/graph/cfg.js.map +1 -0
  143. package/dist/graph/decorator-entries.d.ts +2 -0
  144. package/dist/graph/decorator-entries.d.ts.map +1 -0
  145. package/dist/graph/decorator-entries.js +89 -0
  146. package/dist/graph/decorator-entries.js.map +1 -0
  147. package/dist/graph/entry-points.d.ts +12 -0
  148. package/dist/graph/entry-points.d.ts.map +1 -0
  149. package/dist/graph/entry-points.js +282 -0
  150. package/dist/graph/entry-points.js.map +1 -0
  151. package/dist/graph/handler-conventions.d.ts +2 -0
  152. package/dist/graph/handler-conventions.d.ts.map +1 -0
  153. package/dist/graph/handler-conventions.js +26 -0
  154. package/dist/graph/handler-conventions.js.map +1 -0
  155. package/dist/graph/iac-entries.d.ts +2 -0
  156. package/dist/graph/iac-entries.d.ts.map +1 -0
  157. package/dist/graph/iac-entries.js +123 -0
  158. package/dist/graph/iac-entries.js.map +1 -0
  159. package/dist/graph/import-graph.d.ts +48 -0
  160. package/dist/graph/import-graph.d.ts.map +1 -0
  161. package/dist/graph/import-graph.js +86 -0
  162. package/dist/graph/import-graph.js.map +1 -0
  163. package/dist/graph/monorepo-detect.d.ts +3 -0
  164. package/dist/graph/monorepo-detect.d.ts.map +1 -0
  165. package/dist/graph/monorepo-detect.js +166 -0
  166. package/dist/graph/monorepo-detect.js.map +1 -0
  167. package/dist/graph/tsconfig-paths.d.ts +23 -0
  168. package/dist/graph/tsconfig-paths.d.ts.map +1 -0
  169. package/dist/graph/tsconfig-paths.js +217 -0
  170. package/dist/graph/tsconfig-paths.js.map +1 -0
  171. package/dist/multi-workspace-scanner.d.ts +13 -0
  172. package/dist/multi-workspace-scanner.d.ts.map +1 -0
  173. package/dist/multi-workspace-scanner.js +130 -0
  174. package/dist/multi-workspace-scanner.js.map +1 -0
  175. package/dist/normalizers/type-normalizer.d.ts +16 -0
  176. package/dist/normalizers/type-normalizer.d.ts.map +1 -0
  177. package/dist/normalizers/type-normalizer.js +189 -0
  178. package/dist/normalizers/type-normalizer.js.map +1 -0
  179. package/dist/parsers/typescript-parser.d.ts +57 -0
  180. package/dist/parsers/typescript-parser.d.ts.map +1 -0
  181. package/dist/parsers/typescript-parser.js +502 -0
  182. package/dist/parsers/typescript-parser.js.map +1 -0
  183. package/dist/reporter/json-reporter.d.ts +12 -0
  184. package/dist/reporter/json-reporter.d.ts.map +1 -0
  185. package/dist/reporter/json-reporter.js +28 -0
  186. package/dist/reporter/json-reporter.js.map +1 -0
  187. package/dist/reporter/markdown-reporter.d.ts +11 -0
  188. package/dist/reporter/markdown-reporter.d.ts.map +1 -0
  189. package/dist/reporter/markdown-reporter.js +77 -0
  190. package/dist/reporter/markdown-reporter.js.map +1 -0
  191. package/dist/rothunter.d.ts +125 -0
  192. package/dist/rothunter.d.ts.map +1 -0
  193. package/dist/rothunter.js +1038 -0
  194. package/dist/rothunter.js.map +1 -0
  195. package/dist/server/false-positives.d.ts +34 -0
  196. package/dist/server/false-positives.d.ts.map +1 -0
  197. package/dist/server/false-positives.js +85 -0
  198. package/dist/server/false-positives.js.map +1 -0
  199. package/dist/server/index.d.ts +2 -0
  200. package/dist/server/index.d.ts.map +1 -0
  201. package/dist/server/index.js +1529 -0
  202. package/dist/server/index.js.map +1 -0
  203. package/dist/server/marked-to-fix.d.ts +16 -0
  204. package/dist/server/marked-to-fix.d.ts.map +1 -0
  205. package/dist/server/marked-to-fix.js +36 -0
  206. package/dist/server/marked-to-fix.js.map +1 -0
  207. package/dist/server/scan-store.d.ts +147 -0
  208. package/dist/server/scan-store.d.ts.map +1 -0
  209. package/dist/server/scan-store.js +291 -0
  210. package/dist/server/scan-store.js.map +1 -0
  211. package/dist/server/settings-store.d.ts +28 -0
  212. package/dist/server/settings-store.d.ts.map +1 -0
  213. package/dist/server/settings-store.js +46 -0
  214. package/dist/server/settings-store.js.map +1 -0
  215. package/dist/server/workspace-store.d.ts +39 -0
  216. package/dist/server/workspace-store.d.ts.map +1 -0
  217. package/dist/server/workspace-store.js +108 -0
  218. package/dist/server/workspace-store.js.map +1 -0
  219. package/dist/types/detector-input.d.ts +37 -0
  220. package/dist/types/detector-input.d.ts.map +1 -0
  221. package/dist/types/detector-input.js +2 -0
  222. package/dist/types/detector-input.js.map +1 -0
  223. package/dist/types.d.ts +110 -0
  224. package/dist/types.d.ts.map +1 -0
  225. package/dist/types.js +2 -0
  226. package/dist/types.js.map +1 -0
  227. package/dist/utils/clustering.d.ts +14 -0
  228. package/dist/utils/clustering.d.ts.map +1 -0
  229. package/dist/utils/clustering.js +56 -0
  230. package/dist/utils/clustering.js.map +1 -0
  231. package/dist/utils/gitignore.d.ts +32 -0
  232. package/dist/utils/gitignore.d.ts.map +1 -0
  233. package/dist/utils/gitignore.js +122 -0
  234. package/dist/utils/gitignore.js.map +1 -0
  235. package/dist/utils/hash.d.ts +11 -0
  236. package/dist/utils/hash.d.ts.map +1 -0
  237. package/dist/utils/hash.js +14 -0
  238. package/dist/utils/hash.js.map +1 -0
  239. package/dist/utils/ignore-annotation.d.ts +28 -0
  240. package/dist/utils/ignore-annotation.d.ts.map +1 -0
  241. package/dist/utils/ignore-annotation.js +46 -0
  242. package/dist/utils/ignore-annotation.js.map +1 -0
  243. package/dist/utils/llm-json.d.ts +2 -0
  244. package/dist/utils/llm-json.d.ts.map +1 -0
  245. package/dist/utils/llm-json.js +53 -0
  246. package/dist/utils/llm-json.js.map +1 -0
  247. package/dist/utils/logger.d.ts +3 -0
  248. package/dist/utils/logger.d.ts.map +1 -0
  249. package/dist/utils/logger.js +4 -0
  250. package/dist/utils/logger.js.map +1 -0
  251. package/dist/utils/project-conventions.d.ts +2 -0
  252. package/dist/utils/project-conventions.d.ts.map +1 -0
  253. package/dist/utils/project-conventions.js +108 -0
  254. package/dist/utils/project-conventions.js.map +1 -0
  255. package/dist/utils/regex.d.ts +9 -0
  256. package/dist/utils/regex.d.ts.map +1 -0
  257. package/dist/utils/regex.js +11 -0
  258. package/dist/utils/regex.js.map +1 -0
  259. package/dist/utils/snippet.d.ts +20 -0
  260. package/dist/utils/snippet.d.ts.map +1 -0
  261. package/dist/utils/snippet.js +28 -0
  262. package/dist/utils/snippet.js.map +1 -0
  263. package/dist/utils/source-reader.d.ts +19 -0
  264. package/dist/utils/source-reader.d.ts.map +1 -0
  265. package/dist/utils/source-reader.js +32 -0
  266. package/dist/utils/source-reader.js.map +1 -0
  267. package/logo.png +0 -0
  268. package/package.json +92 -0
  269. package/scripts/start-llm.mjs +161 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rothunter contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="RotHunter" width="160" />
3
+ </p>
4
+
5
+ <h1 align="center">RotHunter</h1>
6
+
7
+ <p align="center">
8
+ Self-hosted code-hygiene engine for TypeScript / JavaScript codebases.<br/>
9
+ Deterministic detectors + local LLM verdicts + dashboard.
10
+ </p>
11
+
12
+ ## What it catches
13
+
14
+ 24 detectors out of the box:
15
+
16
+ - Duplicate types / functions, dead modules / exports / handlers / api
17
+ - Race-condition / mutation / shared-db-write / api-race
18
+ - TSConfig / ESLint / Biome anti-patterns
19
+ - Silent catch, skip-tests (`.skip` / `.only`), TODO / FIXME / HACK comments
20
+ - Long files / functions, deep nesting
21
+ - Public `any`, mutable globals, unused deps, hot-hub files
22
+ - Similar functions (fuzzy clusters with canonical-pick + npm-package suggestion)
23
+
24
+ **Cross-service race detection** — `shared-db-write` and `api-race` run cross-workspace in monorepo mode, catching DB-column writes and write-endpoint calls that span service / repository boundaries. See [`docs/RACE-DETECTION.md`](./docs/RACE-DETECTION.md) for a walkthrough with three concrete scenarios.
25
+
26
+ Full detector list with severities + tunables: [`docs/DETECTORS.md`](./docs/DETECTORS.md).
27
+
28
+ ### Coverage by mode
29
+
30
+ | Mode | Detectors |
31
+ |---|---|
32
+ | Single-workspace | All 24 |
33
+ | Multi-workspace (cross-repo via `rothunter.config.json`) | 9 cross-repo always-on (duplicate-type, duplicate-function, dead-module, dead-export, dead-api, long-function, deep-nesting, public-any, hot-hub-file) + the remaining 15 looped per workspace with workspace-namespaced fingerprints |
34
+
35
+ ## Quick start
36
+
37
+ ```bash
38
+ npm install
39
+ npm run dev:full # auto-picks the fastest LLM backend + server + UI
40
+ # → server on :3000, UI on :5173, LLM on :8080
41
+ ```
42
+
43
+ `dev:full` runs `scripts/start-llm.mjs` which auto-detects the best
44
+ available LLM backend on your host and starts it alongside the server +
45
+ UI. Selection order:
46
+
47
+ | # | Backend | When picked | Notes |
48
+ |---|---------|-------------|-------|
49
+ | 1 | **llama.cpp native** (`llama-server`) | `llama-server` on PATH | Uses Metal on macOS / CUDA on Linux when the binary was built with GPU support. |
50
+ | 2 | **Docker** (`docker compose up rothunter-llm`) | Docker Desktop available | Slower on macOS (no Metal inside the Linux VM) but works on any platform. |
51
+
52
+ Force a specific backend:
53
+
54
+ ```bash
55
+ ROTHUNTER_LLM_BACKEND=llamacpp npm run dev:full # or docker
56
+ ROTHUNTER_LLM_MODEL=bartowski/Qwen2.5-Coder-7B-Instruct-GGUF npm run dev:full
57
+ ```
58
+
59
+ ### Install an LLM backend
60
+
61
+ Pick ONE — `dev:full` picks the first available.
62
+
63
+ ```bash
64
+ # Native llama.cpp (recommended — uses Metal on macOS, CUDA on Linux)
65
+ brew install llama.cpp # macOS
66
+ # Linux: see https://github.com/ggml-org/llama.cpp
67
+
68
+ # Cross-platform sandbox: Docker (slower on macOS — no GPU passthrough)
69
+ docker --version
70
+ ```
71
+
72
+ ### Run pieces individually
73
+
74
+ ```bash
75
+ npm run dev # server + UI only (assumes the LLM is already up)
76
+ npm run llm # auto-detected LLM only
77
+ npm run docker # full docker-compose stack (server + llama.cpp sidecar)
78
+ ```
79
+
80
+ ### Or point at a remote LLM
81
+
82
+ If you already have an OpenAI-compatible endpoint (vLLM, OpenRouter, LM
83
+ Studio, an on-prem cluster …) skip the auto-launch entirely:
84
+
85
+ ```bash
86
+ export ROTHUNTER_LLM_BASE_URL=https://my-llm.internal/v1
87
+ export ROTHUNTER_LLM_MODEL=Qwen2.5-Coder-14B-Instruct
88
+ export ROTHUNTER_LLM_API_KEY=... # if the endpoint needs auth
89
+ export ROTHUNTER_LLM_CONCURRENCY=4 # 1 = sequential, raise for vLLM / llama.cpp --parallel N
90
+ npm run dev
91
+ ```
92
+
93
+ ## Layout
94
+
95
+ ```
96
+ src/
97
+ detectors/ — 24 deterministic detectors
98
+ extraction/ — LLM confirmers
99
+ parsers/ — ts-morph symbol + import graph
100
+ graph/ — import-graph + entry-point resolution
101
+ server/ — Fastify HTTP API + SSE scan stream
102
+ ui/ — React / Vite / Tailwind dashboard
103
+ docker/ — compose + Dockerfile
104
+ ```
105
+
106
+ ## Configuration
107
+
108
+ Every knob is an environment variable; see [`.env.example`](./.env.example) for the full list with defaults. The most common:
109
+
110
+ | Variable | Default | Purpose |
111
+ |---|---|---|
112
+ | `ROTHUNTER_PORT` | `3000` | HTTP API port |
113
+ | `ROTHUNTER_HOST` | `127.0.0.1` | Bind address — `0.0.0.0` exposes the API to LAN |
114
+ | `ROTHUNTER_FS_ROOTS` | `$HOME` (+ `/workspace` in docker) | Colon-separated allow-roots for workspace switches |
115
+ | `ROTHUNTER_LLM_BASE_URL` | `http://127.0.0.1:8080/v1` | OpenAI-compatible LLM endpoint |
116
+ | `ROTHUNTER_LLM_MODEL` | `bartowski/Qwen2.5-Coder-14B-Instruct-GGUF` | HF repo id |
117
+ | `ROTHUNTER_LLM_CONCURRENCY` | `min(8, cores / 2)` | Parallel verdict requests |
118
+
119
+ ## Security model
120
+
121
+ The HTTP API has no authentication — it relies on the loopback bind. Treat rothunter as a single-tenant developer tool, not a hosted service.
122
+
123
+ - Server binds `127.0.0.1` by default. Setting `ROTHUNTER_HOST=0.0.0.0` exposes the API to anyone on the network.
124
+ - The server can only read / write paths under `ROTHUNTER_FS_ROOTS`. Workspace switches outside this set return HTTP 403.
125
+ - LLM confirmers send code excerpts (±8 lines around the finding plus the enclosing signature) to the configured endpoint. Default is a loopback `llama.cpp` instance — nothing leaves the host. Verify the data-retention policy before pointing at a remote endpoint.
126
+
127
+ Full threat model + vulnerability reporting in [`SECURITY.md`](./SECURITY.md).
128
+
129
+ ## Roadmap
130
+
131
+ See [`ROADMAP.md`](./ROADMAP.md) for planned detectors (TypeScript misuse:
132
+ `any-leak` / `god-type` / `everything-optional` / `wide-string-type` /
133
+ `boolean-trap`) and other queued improvements.
134
+
135
+ ## Contributing
136
+
137
+ PRs welcome. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the detector-author checklist and quality bar.
138
+
139
+ ## License
140
+
141
+ MIT — see [`LICENSE`](./LICENSE).
@@ -0,0 +1,68 @@
1
+ export interface ChatMessage {
2
+ role: 'system' | 'user' | 'assistant';
3
+ content: string;
4
+ }
5
+ export interface ChatOptions {
6
+ temperature?: number;
7
+ json?: boolean;
8
+ timeoutMs?: number;
9
+ maxTokens?: number;
10
+ }
11
+ export interface LlmClientOptions {
12
+ baseUrl?: string;
13
+ model?: string;
14
+ apiKey?: string;
15
+ defaultTimeoutMs?: number;
16
+ }
17
+ export declare class LlmClient {
18
+ private readonly baseUrl;
19
+ private readonly modelOverride;
20
+ private readonly apiKey?;
21
+ private readonly defaultTimeoutMs;
22
+ /**
23
+ * Resolved model id used in actual requests. Populated either from
24
+ * the explicit override (env / constructor) or via a one-shot probe
25
+ * of `GET /v1/models` against the configured base URL. The probe
26
+ * keeps `npm run dev:full` working when the operator's env override
27
+ * doesn't match what the backend actually has loaded.
28
+ */
29
+ private resolvedModel;
30
+ /** Tag of the backend we resolved against — exposed for the settings UI. */
31
+ resolvedBackend: 'llamacpp' | 'remote' | 'unknown';
32
+ /**
33
+ * Pure constructor — never reads process.env. Pass explicit opts
34
+ * (typical production wiring is via `createDefaultLlmClient()`, which
35
+ * collects env defaults). Keeping the constructor env-free makes the
36
+ * class trivially mockable + test-isolated.
37
+ */
38
+ constructor(opts?: LlmClientOptions);
39
+ /**
40
+ * One-shot model discovery against `/v1/models`. Picks a sensible
41
+ * default when the operator hasn't set `ROTHUNTER_LLM_MODEL` or when
42
+ * the operator's override is no longer loaded on the backend.
43
+ * Idempotent — repeat calls reuse `resolvedModel`.
44
+ */
45
+ private resolveModel;
46
+ chat(messages: ChatMessage[], options?: ChatOptions): Promise<string>;
47
+ /**
48
+ * Issues a tiny completion to ensure the model is loaded into memory
49
+ * and the first forward pass is compiled. Uses a short timeout
50
+ * (5 s) so the orchestrator can detect "no LLM available" quickly
51
+ * and skip the confirmation pass instead of spending the full
52
+ * verdict timeout on every finding.
53
+ *
54
+ * Returns `true` on success, `false` on any failure — let the caller
55
+ * decide whether to continue, retry, or fall back to deterministic
56
+ * verdicts only.
57
+ */
58
+ warmup(): Promise<boolean>;
59
+ }
60
+ /**
61
+ * Build an LlmClient from `ROTHUNTER_LLM_*` env vars (with the standard
62
+ * fallback chain for the API key: ROTHUNTER_LLM_API_KEY →
63
+ * OPENROUTER_API_KEY → OPENAI_API_KEY). All production call sites route
64
+ * through here so env coupling lives in one place. Tests construct
65
+ * `LlmClient` directly with explicit opts to bypass env reads.
66
+ */
67
+ export declare function createDefaultLlmClient(): LlmClient;
68
+ //# sourceMappingURL=llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../src/adapters/llm.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAcD,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C;;;;;;OAMG;IACH,OAAO,CAAC,aAAa,CAAqB;IAC1C,4EAA4E;IACrE,eAAe,EAAE,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAa;IAEtE;;;;;OAKG;gBACS,IAAI,GAAE,gBAAqB;IAQvC;;;;;OAKG;YACW,YAAY;IA2BpB,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IA0C/E;;;;;;;;;;OAUG;IACG,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;CAWjC;AAkBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,SAAS,CA2BlD"}
@@ -0,0 +1,189 @@
1
+ // OpenAI-compat /v1/chat/completions client. Works with llama-server,
2
+ // vLLM, OpenRouter, LM Studio, and any other backend that speaks the
3
+ // OpenAI chat-completion schema.
4
+ //
5
+ // Env: ROTHUNTER_LLM_BASE_URL / _MODEL / _API_KEY / _TIMEOUT_MS.
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ const DEFAULT_BASE_URL = 'http://127.0.0.1:8080/v1';
10
+ // Qwen2.5-Coder-14B Q4_K_M (~8GB) hits 8/8 on the golden eval — the
11
+ // smallest local model we evaluated that gets every borderline pair
12
+ // right.
13
+ const DEFAULT_MODEL = 'bartowski/Qwen2.5-Coder-14B-Instruct-GGUF';
14
+ const DEFAULT_TIMEOUT_MS = 120_000;
15
+ export class LlmClient {
16
+ baseUrl;
17
+ modelOverride;
18
+ apiKey;
19
+ defaultTimeoutMs;
20
+ /**
21
+ * Resolved model id used in actual requests. Populated either from
22
+ * the explicit override (env / constructor) or via a one-shot probe
23
+ * of `GET /v1/models` against the configured base URL. The probe
24
+ * keeps `npm run dev:full` working when the operator's env override
25
+ * doesn't match what the backend actually has loaded.
26
+ */
27
+ resolvedModel;
28
+ /** Tag of the backend we resolved against — exposed for the settings UI. */
29
+ resolvedBackend = 'unknown';
30
+ /**
31
+ * Pure constructor — never reads process.env. Pass explicit opts
32
+ * (typical production wiring is via `createDefaultLlmClient()`, which
33
+ * collects env defaults). Keeping the constructor env-free makes the
34
+ * class trivially mockable + test-isolated.
35
+ */
36
+ constructor(opts = {}) {
37
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
38
+ this.modelOverride = opts.model;
39
+ this.resolvedModel = opts.model;
40
+ this.apiKey = opts.apiKey;
41
+ this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
42
+ }
43
+ /**
44
+ * One-shot model discovery against `/v1/models`. Picks a sensible
45
+ * default when the operator hasn't set `ROTHUNTER_LLM_MODEL` or when
46
+ * the operator's override is no longer loaded on the backend.
47
+ * Idempotent — repeat calls reuse `resolvedModel`.
48
+ */
49
+ async resolveModel() {
50
+ if (this.resolvedModel && this.resolvedBackend !== 'unknown')
51
+ return this.resolvedModel;
52
+ try {
53
+ const res = await fetch(`${this.baseUrl}/models`, {
54
+ signal: AbortSignal.timeout(5000),
55
+ headers: this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : undefined,
56
+ });
57
+ if (!res.ok)
58
+ return this.resolvedModel;
59
+ const data = (await res.json());
60
+ const ids = (data.data ?? []).map((m) => m.id).filter((id) => typeof id === 'string');
61
+ if (ids.length === 0)
62
+ return this.resolvedModel;
63
+ const looksLocal = /127\.0\.0\.1|localhost|0\.0\.0\.0|host\.docker\.internal/.test(this.baseUrl);
64
+ this.resolvedBackend = looksLocal ? 'llamacpp' : 'remote';
65
+ // Honour the operator override when set + still reachable;
66
+ // otherwise fall back to the first reported id.
67
+ if (this.modelOverride && ids.includes(this.modelOverride)) {
68
+ this.resolvedModel = this.modelOverride;
69
+ }
70
+ else {
71
+ this.resolvedModel = ids[0];
72
+ }
73
+ return this.resolvedModel;
74
+ }
75
+ catch {
76
+ return this.resolvedModel;
77
+ }
78
+ }
79
+ async chat(messages, options = {}) {
80
+ const controller = new AbortController();
81
+ const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
82
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
83
+ const model = (await this.resolveModel()) ?? this.modelOverride ?? DEFAULT_MODEL;
84
+ const body = {
85
+ model,
86
+ messages,
87
+ stream: false,
88
+ temperature: options.temperature ?? 0,
89
+ // Hard cap by default. Without this the server happily generates
90
+ // thousands of tokens and a single request can monopolize the
91
+ // worker for minutes. 512 is enough for verdict JSON even when
92
+ // the backend is a reasoning-tuned model emitting a <think>
93
+ // block before the structured answer.
94
+ max_tokens: options.maxTokens ?? 512,
95
+ };
96
+ const headers = { 'content-type': 'application/json' };
97
+ if (this.apiKey)
98
+ headers.authorization = `Bearer ${this.apiKey}`;
99
+ try {
100
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
101
+ method: 'POST',
102
+ headers,
103
+ body: JSON.stringify(body),
104
+ signal: controller.signal,
105
+ });
106
+ if (!res.ok) {
107
+ const text = await res.text().catch(() => '');
108
+ throw new Error(`LLM HTTP ${res.status}: ${text.slice(0, 200)}`);
109
+ }
110
+ const data = (await res.json());
111
+ const err = typeof data.error === 'string' ? data.error : data.error?.message;
112
+ if (err)
113
+ throw new Error(`LLM error: ${err}`);
114
+ return data.choices?.[0]?.message?.content ?? '';
115
+ }
116
+ finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
120
+ /**
121
+ * Issues a tiny completion to ensure the model is loaded into memory
122
+ * and the first forward pass is compiled. Uses a short timeout
123
+ * (5 s) so the orchestrator can detect "no LLM available" quickly
124
+ * and skip the confirmation pass instead of spending the full
125
+ * verdict timeout on every finding.
126
+ *
127
+ * Returns `true` on success, `false` on any failure — let the caller
128
+ * decide whether to continue, retry, or fall back to deterministic
129
+ * verdicts only.
130
+ */
131
+ async warmup() {
132
+ try {
133
+ await this.chat([{ role: 'user', content: 'ok' }], { temperature: 0, maxTokens: 1, timeoutMs: 5_000 });
134
+ return true;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Read the marker file written by `scripts/start-llm.mjs` so the server
143
+ * picks up the model + port that script actually launched, without
144
+ * needing the operator to keep ROTHUNTER_LLM_MODEL in sync by hand.
145
+ * Returns null when no marker exists.
146
+ */
147
+ function readLlmMarker() {
148
+ try {
149
+ const p = path.join(os.homedir(), '.rothunter', 'llm-active.json');
150
+ if (!fs.existsSync(p))
151
+ return null;
152
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ /**
159
+ * Build an LlmClient from `ROTHUNTER_LLM_*` env vars (with the standard
160
+ * fallback chain for the API key: ROTHUNTER_LLM_API_KEY →
161
+ * OPENROUTER_API_KEY → OPENAI_API_KEY). All production call sites route
162
+ * through here so env coupling lives in one place. Tests construct
163
+ * `LlmClient` directly with explicit opts to bypass env reads.
164
+ */
165
+ export function createDefaultLlmClient() {
166
+ const envTimeout = Number(process.env.ROTHUNTER_LLM_TIMEOUT_MS);
167
+ // Marker beats the hard-coded default but loses to an explicit env
168
+ // override. Operator wins, automation second, default last.
169
+ const marker = readLlmMarker();
170
+ // Marker file lives under $HOME, so it could be doctored by another
171
+ // process running as the same user. Validate `port` is an integer
172
+ // in the legal TCP-port range before interpolating into a URL so a
173
+ // hostile marker can't redirect the engine at an arbitrary host.
174
+ const markerPort = marker?.port && /^\d{1,5}$/.test(marker.port) && Number(marker.port) > 0 && Number(marker.port) <= 65_535
175
+ ? marker.port
176
+ : undefined;
177
+ const baseUrl = process.env.ROTHUNTER_LLM_BASE_URL ??
178
+ (markerPort ? `http://127.0.0.1:${markerPort}/v1` : undefined);
179
+ const model = process.env.ROTHUNTER_LLM_MODEL ?? marker?.model;
180
+ return new LlmClient({
181
+ baseUrl,
182
+ model,
183
+ apiKey: process.env.ROTHUNTER_LLM_API_KEY ??
184
+ process.env.OPENROUTER_API_KEY ??
185
+ process.env.OPENAI_API_KEY,
186
+ defaultTimeoutMs: Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : undefined,
187
+ });
188
+ }
189
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/adapters/llm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * RotHunter configuration — one file per linked-workspaces group.
3
+ *
4
+ * Discovered at scan time by `loadRotHunterConfig(workspaceRoot)`. Looks for
5
+ * `rothunter.config.json` in the workspace root, then in `.rothunter/config.json`.
6
+ * Returns null if no config is present (single-workspace mode).
7
+ *
8
+ * Schema:
9
+ * {
10
+ * "workspaces": [
11
+ * { "path": ".", "name": "backend", "package": "@org/backend" },
12
+ * { "path": "../frontend", "name": "frontend", "package": "@org/frontend" }
13
+ * ]
14
+ * }
15
+ *
16
+ * - `path` is relative to the config file's directory (or absolute).
17
+ * - `name` is a stable logical identifier used as a path prefix in
18
+ * findings + fingerprints.
19
+ * - `package` is optional. When set, a bare import specifier matching
20
+ * that package name (e.g. `import { X } from '@org/backend'`) is
21
+ * resolved into that workspace's source tree.
22
+ */
23
+ export interface WorkspaceConfig {
24
+ /** Absolute path on disk. */
25
+ rootAbs: string;
26
+ /** Logical name used as a workspace ID + finding prefix. */
27
+ name: string;
28
+ /** Package name (npm-style) for bare-specifier resolution into this workspace. */
29
+ packageName?: string;
30
+ }
31
+ export interface RotHunterConfig {
32
+ workspaces: WorkspaceConfig[];
33
+ /** Absolute path of the config file that produced this object. */
34
+ configPath: string;
35
+ }
36
+ export declare function loadRotHunterConfig(workspaceRoot: string): RotHunterConfig | null;
37
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;CACpB;AA0BD,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAkBjF"}
package/dist/config.js ADDED
@@ -0,0 +1,81 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { discoverMonorepoWorkspaces } from './graph/monorepo-detect.js';
5
+ const CONFIG_CANDIDATES = ['rothunter.config.json', '.rothunter/config.json'];
6
+ // Shape mirrors src/schemas/rothunter.config.schema.json — that JSON
7
+ // Schema powers IDE autocomplete + inline validation, this zod schema
8
+ // powers runtime validation with line-accurate error messages. Keep the
9
+ // two in sync.
10
+ const RawConfigSchema = z
11
+ .object({
12
+ $schema: z.string().url().optional(),
13
+ workspaces: z
14
+ .array(z
15
+ .object({
16
+ path: z.string().min(1, 'path is required'),
17
+ name: z.string().min(1, 'name is required'),
18
+ package: z.string().min(1).optional(),
19
+ })
20
+ .strict())
21
+ .min(1, 'workspaces must be a non-empty array'),
22
+ })
23
+ .strict();
24
+ export function loadRotHunterConfig(workspaceRoot) {
25
+ for (const candidate of CONFIG_CANDIDATES) {
26
+ const abs = path.join(workspaceRoot, candidate);
27
+ if (!fs.existsSync(abs))
28
+ continue;
29
+ return parseConfig(abs);
30
+ }
31
+ // Fall back to monorepo auto-detection. Reads package.json#workspaces
32
+ // (npm / yarn / yarn-berry / bun / Turbo / Lerna), pnpm-workspace.yaml,
33
+ // and nx.json. If any sibling packages exist we ingest them automatically
34
+ // so users don't have to write a rothunter.config.json by hand.
35
+ const auto = discoverMonorepoWorkspaces(workspaceRoot);
36
+ if (auto && auto.length > 0) {
37
+ return {
38
+ workspaces: auto,
39
+ configPath: `${workspaceRoot}/[auto-detected]`,
40
+ };
41
+ }
42
+ return null;
43
+ }
44
+ function parseConfig(configPath) {
45
+ let raw;
46
+ try {
47
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
48
+ }
49
+ catch (err) {
50
+ throw new Error(`${configPath}: invalid JSON — ${err.message}`, { cause: err });
51
+ }
52
+ const parsed = RawConfigSchema.safeParse(raw);
53
+ if (!parsed.success) {
54
+ const issues = parsed.error.issues
55
+ .map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`)
56
+ .join('\n');
57
+ throw new Error(`${configPath}: schema validation failed:\n${issues}`);
58
+ }
59
+ const data = parsed.data;
60
+ const configDir = path.dirname(configPath);
61
+ const workspaces = data.workspaces.map((entry, i) => {
62
+ const rootAbs = path.isAbsolute(entry.path) ? entry.path : path.resolve(configDir, entry.path);
63
+ if (!fs.existsSync(rootAbs)) {
64
+ throw new Error(`${configPath}: workspaces[${i}].path "${entry.path}" does not exist on disk.`);
65
+ }
66
+ return {
67
+ rootAbs,
68
+ name: entry.name,
69
+ packageName: entry.package,
70
+ };
71
+ });
72
+ const names = new Set();
73
+ for (const w of workspaces) {
74
+ if (names.has(w.name)) {
75
+ throw new Error(`${configPath}: duplicate workspace name "${w.name}".`);
76
+ }
77
+ names.add(w.name);
78
+ }
79
+ return { workspaces, configPath };
80
+ }
81
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAuCxE,MAAM,iBAAiB,GAAG,CAAC,uBAAuB,EAAE,wBAAwB,CAAC,CAAC;AAE9E,qEAAqE;AACrE,sEAAsE;AACtE,wEAAwE;AACxE,eAAe;AACf,MAAM,eAAe,GAAG,CAAC;KACtB,MAAM,CAAC;IACN,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACpC,UAAU,EAAE,CAAC;SACV,KAAK,CACJ,CAAC;SACE,MAAM,CAAC;QACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC;QAC3C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC;QAC3C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;KACtC,CAAC;SACD,MAAM,EAAE,CACZ;SACA,GAAG,CAAC,CAAC,EAAE,sCAAsC,CAAC;CAClD,CAAC;KACD,MAAM,EAAE,CAAC;AAGZ,MAAM,UAAU,mBAAmB,CAAC,aAAqB;IACvD,KAAK,MAAM,SAAS,IAAI,iBAAiB,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QAChD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAClC,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IACD,sEAAsE;IACtE,wEAAwE;IACxE,0EAA0E;IAC1E,gEAAgE;IAChE,MAAM,IAAI,GAAG,0BAA0B,CAAC,aAAa,CAAC,CAAC;IACvD,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,UAAU,EAAE,IAAI;YAChB,UAAU,EAAE,GAAG,aAAa,kBAAkB;SAC/C,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,UAAkB;IACrC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,oBAAqB,GAAa,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAC/B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;aAC/D,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,gCAAgC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,IAAI,GAAc,MAAM,CAAC,IAAI,CAAC;IACpC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAE3C,MAAM,UAAU,GAAsB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QACrE,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/F,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,gBAAgB,CAAC,WAAW,KAAK,CAAC,IAAI,2BAA2B,CAAC,CAAC;QAClG,CAAC;QACD,OAAO;YACL,OAAO;YACP,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,OAAO;SAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,+BAA+B,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;QAC1E,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Single source of truth for detector IDs.
3
+ *
4
+ * Anything that lists every detector (the server's settings UI, the CLI
5
+ * `--detectors` flag validation, future detector documentation) imports
6
+ * from here. Previously the server/index.ts hardcoded a parallel list
7
+ * that drifted when new detectors were wired into rothunter.ts.
8
+ *
9
+ * To register a new detector:
10
+ * 1. Add its id below.
11
+ * 2. Import + invoke it from rothunter.ts.
12
+ * 3. (Optional) Tag it as `singleWorkspaceOnly` if it can't run when
13
+ * multi-workspace-scanner is active — see the `MULTI_WORKSPACE_*`
14
+ * sets below for the current split.
15
+ */
16
+ export declare const DETECTOR_IDS: readonly ["duplicate-type", "duplicate-function", "dead-module", "dead-export", "dead-api", "long-function", "deep-nesting", "public-any", "hot-hub-file", "dead-handler", "mutation", "race-condition", "shared-db-write", "api-race", "bad-config", "silent-catch", "skip-tests", "long-file", "console-log-prod", "magic-numbers", "mutable-globals", "unused-deps", "similar-functions", "todo-comments"];
17
+ export type DetectorId = (typeof DETECTOR_IDS)[number];
18
+ /**
19
+ * Detectors that need real workspace-root-relative file paths or git
20
+ * access on a single workspace. Multi-workspace mode skips these because
21
+ * multi-workspace-scanner prefixes paths with the workspace name (see
22
+ * the warn-log in rothunter.ts run()).
23
+ */
24
+ export declare const MULTI_WORKSPACE_SKIPPED: Set<"duplicate-type" | "duplicate-function" | "dead-module" | "dead-export" | "dead-api" | "long-function" | "deep-nesting" | "public-any" | "hot-hub-file" | "dead-handler" | "mutation" | "race-condition" | "shared-db-write" | "api-race" | "bad-config" | "silent-catch" | "skip-tests" | "long-file" | "console-log-prod" | "magic-numbers" | "mutable-globals" | "unused-deps" | "similar-functions" | "todo-comments">;
25
+ /**
26
+ * Cross-repo detector: only runs in multi-workspace mode (its whole
27
+ * purpose is to flag exported symbols that nobody imports across
28
+ * workspace boundaries — in single-workspace mode dead-export already
29
+ * covers that ground).
30
+ */
31
+ export declare const MULTI_WORKSPACE_ONLY: Set<"duplicate-type" | "duplicate-function" | "dead-module" | "dead-export" | "dead-api" | "long-function" | "deep-nesting" | "public-any" | "hot-hub-file" | "dead-handler" | "mutation" | "race-condition" | "shared-db-write" | "api-race" | "bad-config" | "silent-catch" | "skip-tests" | "long-file" | "console-log-prod" | "magic-numbers" | "mutable-globals" | "unused-deps" | "similar-functions" | "todo-comments">;
32
+ //# sourceMappingURL=detector-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector-registry.d.ts","sourceRoot":"","sources":["../src/detector-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,YAAY,+YA2Bf,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,gaAgBlC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,gaAAoC,CAAC"}