@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
@@ -0,0 +1,122 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import ignore from 'ignore';
4
+ /**
5
+ * Load every `.gitignore` reachable from `workspaceRoot` AND the
6
+ * workspace-root `.rothunterignore` (gitignore-syntax extension, only
7
+ * for rothunter). Returns a unified matcher.
8
+ *
9
+ * The matcher answers "is this workspace-relative path ignored?" — used
10
+ * by the parser + by any detector that walks the filesystem on its own.
11
+ *
12
+ * Rules:
13
+ * - Top-level `.gitignore` always loads (when present).
14
+ * - Nested `.gitignore`s load too, prefixed with their relative dir so
15
+ * a pattern like `dist/` inside `packages/foo/.gitignore` matches
16
+ * `packages/foo/dist/...` and not the root `dist/`.
17
+ * - `.rothunterignore` at the workspace root loads with the same
18
+ * syntax — use this for files that ARE in git but should be hidden
19
+ * from rothunter (fixtures, vendored SDKs, generated test data).
20
+ * - `node_modules/` and `.git/` are always ignored regardless of
21
+ * what the operator's files say — the cost of scanning them is
22
+ * prohibitive on every repo and never produces useful findings.
23
+ *
24
+ * Always returns a non-null matcher; the always-on rules (`node_modules`
25
+ * + `.git`) ensure callers get sensible defaults even when no ignore
26
+ * file exists.
27
+ */
28
+ export function loadGitignore(workspaceRoot) {
29
+ const ig = ignore();
30
+ // Always-on baseline. node_modules is the single biggest perf win;
31
+ // .git is opaque to detectors anyway. These are baked in so that a
32
+ // workspace without any .gitignore still scans something sensible.
33
+ ig.add(['.git', 'node_modules']);
34
+ const scan = (dir, relPrefix) => {
35
+ const gitignorePath = path.join(dir, '.gitignore');
36
+ if (fs.existsSync(gitignorePath)) {
37
+ try {
38
+ const raw = fs.readFileSync(gitignorePath, 'utf-8');
39
+ const patterns = raw
40
+ .split('\n')
41
+ .map((l) => l.trimEnd())
42
+ .filter((l) => l.length > 0 && !l.startsWith('#'));
43
+ if (relPrefix === '') {
44
+ ig.add(patterns);
45
+ }
46
+ else {
47
+ // Nested .gitignore — scope every pattern to the containing dir
48
+ // unless the pattern is anchored (starts with `/`) or negated
49
+ // (`!`), in which case the `ignore` library's standard semantics
50
+ // around relative roots take over.
51
+ const scoped = patterns.map((p) => {
52
+ const neg = p.startsWith('!');
53
+ const body = neg ? p.slice(1) : p;
54
+ const anchored = body.startsWith('/');
55
+ const cleanBody = anchored ? body.slice(1) : body;
56
+ const scopedBody = path.posix.join(relPrefix, cleanBody);
57
+ return (neg ? '!' : '') + scopedBody;
58
+ });
59
+ ig.add(scoped);
60
+ }
61
+ }
62
+ catch {
63
+ // Unreadable — silently skip. Better to scan more files than to
64
+ // crash the whole pipeline on a permissions error.
65
+ }
66
+ }
67
+ // Recurse into immediate sub-directories. Cap depth to avoid
68
+ // pathological monorepos eating O(n) syscalls per scan; the gitignore
69
+ // files we care about live within a handful of levels of the root.
70
+ let entries;
71
+ try {
72
+ entries = fs.readdirSync(dir, { withFileTypes: true });
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ for (const entry of entries) {
78
+ if (!entry.isDirectory())
79
+ continue;
80
+ if (entry.name === '.git' || entry.name === 'node_modules')
81
+ continue;
82
+ if (entry.name.startsWith('.') && entry.name !== '.rothunter')
83
+ continue;
84
+ const childDir = path.join(dir, entry.name);
85
+ const childRel = relPrefix === '' ? entry.name : path.posix.join(relPrefix, entry.name);
86
+ // Already-ignored directories don't need their nested .gitignores
87
+ // — git doesn't recurse past an ignored dir, neither should we.
88
+ if (ig.ignores(childRel + '/'))
89
+ continue;
90
+ scan(childDir, childRel);
91
+ }
92
+ };
93
+ scan(workspaceRoot, '');
94
+ // Layer `.rothunterignore` on top — rothunter-only path patterns
95
+ // using identical gitignore syntax. Used to hide fixtures, vendored
96
+ // code, and generated artifacts from scans without touching the
97
+ // operator's git rules.
98
+ const rhPath = path.join(workspaceRoot, '.rothunterignore');
99
+ if (fs.existsSync(rhPath)) {
100
+ try {
101
+ const raw = fs.readFileSync(rhPath, 'utf-8');
102
+ const patterns = raw
103
+ .split('\n')
104
+ .map((l) => l.trimEnd())
105
+ .filter((l) => l.length > 0 && !l.startsWith('#'));
106
+ if (patterns.length > 0)
107
+ ig.add(patterns);
108
+ }
109
+ catch {
110
+ // Unreadable — silently skip.
111
+ }
112
+ }
113
+ return ig;
114
+ }
115
+ /**
116
+ * Filter a workspace-relative path list through the gitignore matcher,
117
+ * preserving order.
118
+ */
119
+ export function filterGitignored(matcher, files) {
120
+ return files.filter((f) => !matcher.ignores(f.replace(/\\/g, '/')));
121
+ }
122
+ //# sourceMappingURL=gitignore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitignore.js","sourceRoot":"","sources":["../../src/utils/gitignore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Stable, short SHA-256 prefix used as a fingerprint suffix across
3
+ * every detector. Centralised here so the algorithm + truncation
4
+ * length live in ONE place — bumping from 16 to 24 hex chars only
5
+ * requires this file, not every detector.
6
+ *
7
+ * 16 hex chars = 64 bits of entropy → negligible collision risk for
8
+ * scan-sized populations (thousands of findings per scan).
9
+ */
10
+ export declare function stableHash(input: string, length?: number): string;
11
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAK,GAAG,MAAM,CAE7D"}
@@ -0,0 +1,14 @@
1
+ import * as crypto from 'node:crypto';
2
+ /**
3
+ * Stable, short SHA-256 prefix used as a fingerprint suffix across
4
+ * every detector. Centralised here so the algorithm + truncation
5
+ * length live in ONE place — bumping from 16 to 24 hex chars only
6
+ * requires this file, not every detector.
7
+ *
8
+ * 16 hex chars = 64 bits of entropy → negligible collision risk for
9
+ * scan-sized populations (thousands of findings per scan).
10
+ */
11
+ export function stableHash(input, length = 16) {
12
+ return crypto.createHash('sha256').update(input).digest('hex').slice(0, length);
13
+ }
14
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,MAAM,GAAG,EAAE;IACnD,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAClF,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * In-source suppression pragma — operators (and agents under operator
3
+ * supervision) can add a `// rothunter:ignore-<detectorId>` comment
4
+ * above a flagged line to silence the detector for that specific
5
+ * location. `// rothunter:ignore-all` silences every detector.
6
+ *
7
+ * Persistent + co-located with the code being suppressed — survives
8
+ * re-scans automatically. The convention mirrors
9
+ * `// eslint-disable-next-line <rule>`.
10
+ *
11
+ * Required shape (single line above target):
12
+ *
13
+ * // rothunter:ignore-<detectorId>
14
+ * // reason: <one short sentence explaining why this is intentional>
15
+ *
16
+ * Detectors call `hasIgnoreAnnotation(rawSource, line, detectorId)`
17
+ * BEFORE emitting a finding. The matcher walks the previous 5
18
+ * non-blank lines looking for the pragma — captures both styles:
19
+ *
20
+ * // rothunter:ignore-silent-catch ← matches detectorId
21
+ * // rothunter:ignore-all ← global ignore
22
+ *
23
+ * Strict matching: only line-comment `//` syntax (not block comments).
24
+ * Block comments confuse the regex on multi-line files; agents should
25
+ * stick to single-line line comments.
26
+ */
27
+ export declare function hasIgnoreAnnotation(rawSource: string, targetLine: number, detectorId: string): boolean;
28
+ //# sourceMappingURL=ignore-annotation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ignore-annotation.d.ts","sourceRoot":"","sources":["../../src/utils/ignore-annotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAeT"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * In-source suppression pragma — operators (and agents under operator
3
+ * supervision) can add a `// rothunter:ignore-<detectorId>` comment
4
+ * above a flagged line to silence the detector for that specific
5
+ * location. `// rothunter:ignore-all` silences every detector.
6
+ *
7
+ * Persistent + co-located with the code being suppressed — survives
8
+ * re-scans automatically. The convention mirrors
9
+ * `// eslint-disable-next-line <rule>`.
10
+ *
11
+ * Required shape (single line above target):
12
+ *
13
+ * // rothunter:ignore-<detectorId>
14
+ * // reason: <one short sentence explaining why this is intentional>
15
+ *
16
+ * Detectors call `hasIgnoreAnnotation(rawSource, line, detectorId)`
17
+ * BEFORE emitting a finding. The matcher walks the previous 5
18
+ * non-blank lines looking for the pragma — captures both styles:
19
+ *
20
+ * // rothunter:ignore-silent-catch ← matches detectorId
21
+ * // rothunter:ignore-all ← global ignore
22
+ *
23
+ * Strict matching: only line-comment `//` syntax (not block comments).
24
+ * Block comments confuse the regex on multi-line files; agents should
25
+ * stick to single-line line comments.
26
+ */
27
+ export function hasIgnoreAnnotation(rawSource, targetLine, detectorId) {
28
+ if (!rawSource || targetLine < 1)
29
+ return false;
30
+ const lines = rawSource.split(/\r?\n/);
31
+ const start = Math.max(0, targetLine - 1 - 5);
32
+ const end = Math.min(lines.length, targetLine - 1);
33
+ const allTag = 'rothunter:ignore-all';
34
+ const detTag = `rothunter:ignore-${detectorId}`;
35
+ for (let i = start; i < end; i++) {
36
+ const line = lines[i] ?? '';
37
+ if (!line.includes('rothunter:ignore'))
38
+ continue;
39
+ // Cheap substring matches — avoid false positives from
40
+ // `rothunter:ignore-races-something` matching `ignore-race`.
41
+ if (line.includes(allTag) || line.includes(detTag))
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ //# sourceMappingURL=ignore-annotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ignore-annotation.js","sourceRoot":"","sources":["../../src/utils/ignore-annotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,UAAU,mBAAmB,CACjC,SAAiB,EACjB,UAAkB,EAClB,UAAkB;IAElB,IAAI,CAAC,SAAS,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,sBAAsB,CAAC;IACtC,MAAM,MAAM,GAAG,oBAAoB,UAAU,EAAE,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC;YAAE,SAAS;QACjD,uDAAuD;QACvD,6DAA6D;QAC7D,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function parseLlmJsonResponse(raw: string): unknown;
2
+ //# sourceMappingURL=llm-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-json.d.ts","sourceRoot":"","sources":["../../src/utils/llm-json.ts"],"names":[],"mappings":"AAAA,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWzD"}
@@ -0,0 +1,53 @@
1
+ export function parseLlmJsonResponse(raw) {
2
+ const cleaned = stripCodeFence(raw).trim();
3
+ if (!cleaned)
4
+ throw new Error('Empty LLM response');
5
+ try {
6
+ return JSON.parse(cleaned);
7
+ }
8
+ catch {
9
+ const extracted = extractFirstJsonObject(cleaned);
10
+ if (extracted)
11
+ return JSON.parse(extracted);
12
+ throw new Error(`LLM response is not valid JSON: ${cleaned.slice(0, 200)}`);
13
+ }
14
+ }
15
+ function stripCodeFence(s) {
16
+ const fence = /^```(?:json)?\s*([\s\S]*?)\s*```$/i;
17
+ const m = s.trim().match(fence);
18
+ return m?.[1] ?? s;
19
+ }
20
+ function extractFirstJsonObject(s) {
21
+ const start = s.indexOf('{');
22
+ if (start < 0)
23
+ return null;
24
+ let depth = 0;
25
+ let inString = false;
26
+ let escape = false;
27
+ for (let i = start; i < s.length; i++) {
28
+ const ch = s[i];
29
+ if (escape) {
30
+ escape = false;
31
+ continue;
32
+ }
33
+ if (ch === '\\') {
34
+ escape = true;
35
+ continue;
36
+ }
37
+ if (ch === '"') {
38
+ inString = !inString;
39
+ continue;
40
+ }
41
+ if (inString)
42
+ continue;
43
+ if (ch === '{')
44
+ depth++;
45
+ else if (ch === '}') {
46
+ depth--;
47
+ if (depth === 0)
48
+ return s.slice(start, i + 1);
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ //# sourceMappingURL=llm-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-json.js","sourceRoot":"","sources":["../../src/utils/llm-json.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAEpD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,SAAS,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAClD,IAAI,SAAS;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,mCAAmC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,CAAS;IAC/B,MAAM,KAAK,GAAG,oCAAoC,CAAC;IACnD,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,sBAAsB,CAAC,CAAS;IACvC,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChB,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,GAAG,KAAK,CAAC;YACf,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,MAAM,GAAG,IAAI,CAAC;YACd,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,GAAG,CAAC,QAAQ,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,QAAQ;YAAE,SAAS;QACvB,IAAI,EAAE,KAAK,GAAG;YAAE,KAAK,EAAE,CAAC;aACnB,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACpB,KAAK,EAAE,CAAC;YACR,IAAI,KAAK,KAAK,CAAC;gBAAE,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,3 @@
1
+ import pino from 'pino';
2
+ export declare const logger: pino.Logger<never, boolean>;
3
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAIxB,eAAO,MAAM,MAAM,6BAAkB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import pino from 'pino';
2
+ const level = process.env.ROTHUNTER_LOG_LEVEL ?? process.env.LOG_LEVEL ?? 'info';
3
+ export const logger = pino({ level });
4
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM,CAAC;AAEjF,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function readProjectConventions(workspaceRoot: string, evidenceFile?: string): string | undefined;
2
+ //# sourceMappingURL=project-conventions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-conventions.d.ts","sourceRoot":"","sources":["../../src/utils/project-conventions.ts"],"names":[],"mappings":"AAqDA,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,GAAG,SAAS,CAyCpB"}
@@ -0,0 +1,108 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ /**
4
+ * Read every project-conventions file in scope and return their
5
+ * concatenated body truncated to a prompt budget. Walks upward from
6
+ * the evidence file's directory so nested package rules layer over
7
+ * the workspace default. Cached per-path so a 100-finding scan reads
8
+ * each file once.
9
+ *
10
+ * Why this exists: detectors flag patterns ("duplicate-function",
11
+ * "long-function", "long-file", …) that are intentional in some
12
+ * codebases (Commander.js idiom, linear request handlers, recognizer
13
+ * tables). Encoding every project rule into the detector is impossible
14
+ * — but the project usually writes those rules in a conventions file
15
+ * (CLAUDE.md, AGENTS.md, .cursorrules, copilot-instructions.md, …).
16
+ * Feeding them into the LLM verdict makes triage project-aware without
17
+ * touching detector logic.
18
+ *
19
+ * Returns `undefined` when no recognised conventions file is found.
20
+ */
21
+ const cache = new Map();
22
+ const MAX_LEN = 6000;
23
+ /**
24
+ * Filenames (relative to each directory we visit) that count as
25
+ * "project conventions". Ordered roughly by community adoption.
26
+ * Tools-specific files (`.cursorrules`, `.windsurfrules`) are kept in
27
+ * the same list because rules written for one agent usually apply to
28
+ * any agent — they describe project shape, not tool quirks.
29
+ */
30
+ const CONVENTION_FILENAMES = [
31
+ 'CLAUDE.md',
32
+ 'AGENTS.md',
33
+ 'AGENT.md',
34
+ 'GEMINI.md',
35
+ 'CODEX.md',
36
+ '.codex.md',
37
+ 'COPILOT.md',
38
+ 'AI.md',
39
+ 'AI_GUIDELINES.md',
40
+ 'AI_RULES.md',
41
+ '.cursorrules',
42
+ '.cursor/rules',
43
+ '.windsurfrules',
44
+ '.github/copilot-instructions.md',
45
+ '.continue/rules.md',
46
+ 'CONVENTIONS.md',
47
+ 'CODESTYLE.md',
48
+ 'STYLEGUIDE.md',
49
+ 'CONTRIBUTING.md',
50
+ ];
51
+ export function readProjectConventions(workspaceRoot, evidenceFile) {
52
+ const dirs = [];
53
+ // Walk upward from the evidence file's directory to the workspace
54
+ // root, deepest first. Within each directory we read every matching
55
+ // conventions file. Nested rules appear before workspace defaults
56
+ // in the joined output so the LLM sees them as the more specific
57
+ // override.
58
+ if (evidenceFile) {
59
+ const wsAbs = path.resolve(workspaceRoot);
60
+ let dir = path.dirname(path.resolve(workspaceRoot, evidenceFile));
61
+ while (true) {
62
+ dirs.push(dir);
63
+ if (path.resolve(dir) === wsAbs)
64
+ break;
65
+ const parent = path.dirname(dir);
66
+ if (parent === dir)
67
+ break;
68
+ dir = parent;
69
+ }
70
+ }
71
+ else {
72
+ dirs.push(workspaceRoot);
73
+ }
74
+ const parts = [];
75
+ let budget = MAX_LEN;
76
+ for (const dir of dirs) {
77
+ for (const name of CONVENTION_FILENAMES) {
78
+ if (budget <= 0)
79
+ break;
80
+ const candidate = path.join(dir, name);
81
+ let body = cache.get(candidate);
82
+ if (body === undefined && !cache.has(candidate)) {
83
+ body = readFileSafe(candidate);
84
+ cache.set(candidate, body);
85
+ }
86
+ if (!body)
87
+ continue;
88
+ const wsRel = path.relative(workspaceRoot, candidate) || name;
89
+ const slice = body.length > budget ? body.slice(0, budget) + '\n…(truncated)' : body;
90
+ parts.push(`# ${wsRel}\n${slice}`);
91
+ budget -= slice.length + wsRel.length + 4;
92
+ }
93
+ if (budget <= 0)
94
+ break;
95
+ }
96
+ if (parts.length === 0)
97
+ return undefined;
98
+ return parts.join('\n\n---\n\n');
99
+ }
100
+ function readFileSafe(p) {
101
+ try {
102
+ return fs.readFileSync(p, 'utf-8');
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ //# sourceMappingURL=project-conventions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-conventions.js","sourceRoot":"","sources":["../../src/utils/project-conventions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Escape every regex metacharacter in `s` so it can be embedded as a
3
+ * literal inside a new RegExp. Centralised because four detectors
4
+ * shipped their own copy of the same one-liner — when a future bug
5
+ * (e.g. forgetting to escape `-` inside character classes) needs a
6
+ * fix, doing it here propagates everywhere.
7
+ */
8
+ export declare function escapeForRegex(s: string): string;
9
+ //# sourceMappingURL=regex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex.d.ts","sourceRoot":"","sources":["../../src/utils/regex.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Escape every regex metacharacter in `s` so it can be embedded as a
3
+ * literal inside a new RegExp. Centralised because four detectors
4
+ * shipped their own copy of the same one-liner — when a future bug
5
+ * (e.g. forgetting to escape `-` inside character classes) needs a
6
+ * fix, doing it here propagates everywhere.
7
+ */
8
+ export function escapeForRegex(s) {
9
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ }
11
+ //# sourceMappingURL=regex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex.js","sourceRoot":"","sources":["../../src/utils/regex.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Snippet trimmers shared across the detectors. The cluster confirmers
3
+ * (api-race, shared-db-write, race-condition, …) embed `enclosingSource`
4
+ * blobs in each evidence note — those blobs eat the LLM prompt budget
5
+ * quickly when they're long real-world functions. We collapse + cap them
6
+ * here so every detector applies the same prompt-shape.
7
+ */
8
+ /**
9
+ * Collapse whitespace + truncate a single-line snippet. Used for
10
+ * preview strings in finding titles + evidence rows.
11
+ */
12
+ export declare function trimSnippet(text: string, maxChars?: number): string;
13
+ /**
14
+ * Preserve the first/last lines of a long function body, ellipsise the
15
+ * middle. Used for the LLM prompt's `enclosingSource` block — gives the
16
+ * model enough context to see the signature + final return without
17
+ * blowing the token budget on a 200-line method.
18
+ */
19
+ export declare function trimEnclosingSource(full: string, headLines?: number): string;
20
+ //# sourceMappingURL=snippet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.d.ts","sourceRoot":"","sources":["../../src/utils/snippet.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,MAAM,CAGhE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAK,GAAG,MAAM,CAIxE"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Snippet trimmers shared across the detectors. The cluster confirmers
3
+ * (api-race, shared-db-write, race-condition, …) embed `enclosingSource`
4
+ * blobs in each evidence note — those blobs eat the LLM prompt budget
5
+ * quickly when they're long real-world functions. We collapse + cap them
6
+ * here so every detector applies the same prompt-shape.
7
+ */
8
+ /**
9
+ * Collapse whitespace + truncate a single-line snippet. Used for
10
+ * preview strings in finding titles + evidence rows.
11
+ */
12
+ export function trimSnippet(text, maxChars = 160) {
13
+ const collapsed = text.replace(/\s+/g, ' ').trim();
14
+ return collapsed.length > maxChars ? collapsed.slice(0, maxChars - 3) + '...' : collapsed;
15
+ }
16
+ /**
17
+ * Preserve the first/last lines of a long function body, ellipsise the
18
+ * middle. Used for the LLM prompt's `enclosingSource` block — gives the
19
+ * model enough context to see the signature + final return without
20
+ * blowing the token budget on a 200-line method.
21
+ */
22
+ export function trimEnclosingSource(full, headLines = 40) {
23
+ const lines = full.split(/\r?\n/);
24
+ if (lines.length <= headLines + 2)
25
+ return full;
26
+ return [...lines.slice(0, headLines), ' // ...', lines[lines.length - 1] ?? ''].join('\n');
27
+ }
28
+ //# sourceMappingURL=snippet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippet.js","sourceRoot":"","sources":["../../src/utils/snippet.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,QAAQ,GAAG,GAAG;IACtD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACnD,OAAO,SAAS,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,SAAS,GAAG,EAAE;IAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,KAAK,CAAC,MAAM,IAAI,SAAS,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC9F,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Project } from 'ts-morph';
2
+ /**
3
+ * Build a fast workspace-relative source reader. File-walking detectors
4
+ * (magic-numbers, console-log-prod, silent-catch, skip-tests, bad-config,
5
+ * long-file, mutable-globals, …) used to readFileSync every
6
+ * candidate file independently — the orchestrator already parsed those
7
+ * same files into a shared ts-morph Project, so the disk I/O was pure
8
+ * duplication. When a Project is passed, this reader serves text from
9
+ * ts-morph's in-memory SourceFile cache; otherwise it falls back to
10
+ * direct readFileSync so detectors keep working in their own tests
11
+ * (where there is no shared Project to inject).
12
+ *
13
+ * Returns `null` on read failure (missing file, encoding error, etc.) —
14
+ * callers should `continue` past missing files, same as the previous
15
+ * readFileSync/try-catch shape.
16
+ */
17
+ export type SourceReader = (rel: string) => string | null;
18
+ export declare function makeSourceReader(workspaceRoot: string, project?: Project): SourceReader;
19
+ //# sourceMappingURL=source-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-reader.d.ts","sourceRoot":"","sources":["../../src/utils/source-reader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAE1D,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,YAAY,CAWvF"}
@@ -0,0 +1,32 @@
1
+ import * as path from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ export function makeSourceReader(workspaceRoot, project) {
4
+ if (project) {
5
+ const byRel = new Map();
6
+ for (const sf of project.getSourceFiles()) {
7
+ const rel = path.relative(workspaceRoot, sf.getFilePath());
8
+ // Use POSIX separator so reads work the same across platforms.
9
+ byRel.set(rel.split(path.sep).join('/'), sf.getFullText());
10
+ }
11
+ return (rel) => byRel.get(rel.split(path.sep).join('/')) ?? readFromDisk(workspaceRoot, rel);
12
+ }
13
+ return (rel) => readFromDisk(workspaceRoot, rel);
14
+ }
15
+ function readFromDisk(workspaceRoot, rel) {
16
+ try {
17
+ const root = path.resolve(workspaceRoot);
18
+ const resolved = path.resolve(root, rel);
19
+ // Refuse anything that escapes the workspace root via `..` /
20
+ // absolute / symlink before it reaches `readFileSync`. Callers
21
+ // are internal today, but a defensive guard here keeps any
22
+ // future detector or test fixture from punching through.
23
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) {
24
+ return null;
25
+ }
26
+ return readFileSync(resolved, 'utf-8');
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ //# sourceMappingURL=source-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-reader.js","sourceRoot":"","sources":["../../src/utils/source-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoBvC,MAAM,UAAU,gBAAgB,CAAC,aAAqB,EAAE,OAAiB;IACvE,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QACxC,KAAK,MAAM,EAAE,IAAI,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;YAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YAC3D,+DAA+D;YAC/D,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,YAAY,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAC/F,CAAC;IACD,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,aAAqB,EAAE,GAAW;IACtD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACzC,6DAA6D;QAC7D,+DAA+D;QAC/D,2DAA2D;QAC3D,yDAAyD;QACzD,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/D,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
package/logo.png ADDED
Binary file