@rigour-labs/core 6.8.0 → 6.8.1-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 (326) hide show
  1. package/dist/inference/cloud-provider.js +12 -2
  2. package/package.json +11 -8
  3. package/dist/brief/briefing.test.d.ts +0 -1
  4. package/dist/brief/briefing.test.js +0 -107
  5. package/dist/context/automatic-index-cache.test.d.ts +0 -1
  6. package/dist/context/automatic-index-cache.test.js +0 -30
  7. package/dist/context/automatic-index.test.d.ts +0 -1
  8. package/dist/context/automatic-index.test.js +0 -45
  9. package/dist/context/cache-engine.test.d.ts +0 -1
  10. package/dist/context/cache-engine.test.js +0 -99
  11. package/dist/context/dependency-graph.test.d.ts +0 -1
  12. package/dist/context/dependency-graph.test.js +0 -13
  13. package/dist/context/index-status.test.d.ts +0 -1
  14. package/dist/context/index-status.test.js +0 -26
  15. package/dist/context.test.d.ts +0 -1
  16. package/dist/context.test.js +0 -228
  17. package/dist/deep/agent-review.test.d.ts +0 -1
  18. package/dist/deep/agent-review.test.js +0 -53
  19. package/dist/deep/code-context.test.d.ts +0 -1
  20. package/dist/deep/code-context.test.js +0 -48
  21. package/dist/deep/code-pass.test.d.ts +0 -1
  22. package/dist/deep/code-pass.test.js +0 -112
  23. package/dist/deep/code-review-prompt.test.d.ts +0 -1
  24. package/dist/deep/code-review-prompt.test.js +0 -20
  25. package/dist/deep/code-verifier.test.d.ts +0 -1
  26. package/dist/deep/code-verifier.test.js +0 -54
  27. package/dist/deep/diff-tests/calls.test.d.ts +0 -1
  28. package/dist/deep/diff-tests/calls.test.js +0 -33
  29. package/dist/deep/diff-tests/run.test.d.ts +0 -1
  30. package/dist/deep/diff-tests/run.test.js +0 -58
  31. package/dist/deep/fact-extractor.test.d.ts +0 -1
  32. package/dist/deep/fact-extractor.test.js +0 -581
  33. package/dist/deep/parse-findings.test.d.ts +0 -1
  34. package/dist/deep/parse-findings.test.js +0 -25
  35. package/dist/deep/pr-review.test.d.ts +0 -1
  36. package/dist/deep/pr-review.test.js +0 -80
  37. package/dist/deep/prompts.test.d.ts +0 -1
  38. package/dist/deep/prompts.test.js +0 -235
  39. package/dist/deep/reference-pack.test.d.ts +0 -1
  40. package/dist/deep/reference-pack.test.js +0 -46
  41. package/dist/deep/related-changes.test.d.ts +0 -1
  42. package/dist/deep/related-changes.test.js +0 -33
  43. package/dist/deep/review-context-export.test.d.ts +0 -1
  44. package/dist/deep/review-context-export.test.js +0 -32
  45. package/dist/deep/review-tools.test.d.ts +0 -1
  46. package/dist/deep/review-tools.test.js +0 -39
  47. package/dist/deep/risk.test.d.ts +0 -1
  48. package/dist/deep/risk.test.js +0 -101
  49. package/dist/deep/verifier.test.d.ts +0 -1
  50. package/dist/deep/verifier.test.js +0 -635
  51. package/dist/discovery.test.d.ts +0 -1
  52. package/dist/discovery.test.js +0 -93
  53. package/dist/environment.test.d.ts +0 -1
  54. package/dist/environment.test.js +0 -94
  55. package/dist/firewall/firewall.test.d.ts +0 -1
  56. package/dist/firewall/firewall.test.js +0 -117
  57. package/dist/firewall/trust-boundaries.test.d.ts +0 -1
  58. package/dist/firewall/trust-boundaries.test.js +0 -64
  59. package/dist/firewall/trusted-control.test.d.ts +0 -1
  60. package/dist/firewall/trusted-control.test.js +0 -188
  61. package/dist/gates/agent-team.test.d.ts +0 -1
  62. package/dist/gates/agent-team.test.js +0 -113
  63. package/dist/gates/ast.test.d.ts +0 -1
  64. package/dist/gates/ast.test.js +0 -112
  65. package/dist/gates/checkpoint.test.d.ts +0 -1
  66. package/dist/gates/checkpoint.test.js +0 -105
  67. package/dist/gates/content.test.d.ts +0 -1
  68. package/dist/gates/content.test.js +0 -73
  69. package/dist/gates/coverage.test.d.ts +0 -1
  70. package/dist/gates/coverage.test.js +0 -53
  71. package/dist/gates/dedupe-failures.test.d.ts +0 -1
  72. package/dist/gates/dedupe-failures.test.js +0 -12
  73. package/dist/gates/deep-analysis.test.d.ts +0 -1
  74. package/dist/gates/deep-analysis.test.js +0 -86
  75. package/dist/gates/deep-intent.test.d.ts +0 -1
  76. package/dist/gates/deep-intent.test.js +0 -51
  77. package/dist/gates/deep-timeout.test.d.ts +0 -1
  78. package/dist/gates/deep-timeout.test.js +0 -10
  79. package/dist/gates/deprecated-apis.test.d.ts +0 -1
  80. package/dist/gates/deprecated-apis.test.js +0 -318
  81. package/dist/gates/deprecated-dependencies.test.d.ts +0 -1
  82. package/dist/gates/deprecated-dependencies.test.js +0 -55
  83. package/dist/gates/frontend-secret-exposure.test.d.ts +0 -1
  84. package/dist/gates/frontend-secret-exposure.test.js +0 -148
  85. package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +0 -1
  86. package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +0 -27
  87. package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +0 -1
  88. package/dist/gates/hallucinated-imports/js-resolver-types.test.js +0 -14
  89. package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +0 -1
  90. package/dist/gates/hallucinated-imports-sveltekit.test.js +0 -132
  91. package/dist/gates/hallucinated-imports.test.d.ts +0 -1
  92. package/dist/gates/hallucinated-imports.test.js +0 -1206
  93. package/dist/gates/js-style-context.test.d.ts +0 -1
  94. package/dist/gates/js-style-context.test.js +0 -35
  95. package/dist/gates/logic-drift.test.d.ts +0 -1
  96. package/dist/gates/logic-drift.test.js +0 -52
  97. package/dist/gates/phantom-apis.test.d.ts +0 -1
  98. package/dist/gates/phantom-apis.test.js +0 -396
  99. package/dist/gates/promise-safety.test.d.ts +0 -1
  100. package/dist/gates/promise-safety.test.js +0 -34
  101. package/dist/gates/runner.test.d.ts +0 -1
  102. package/dist/gates/runner.test.js +0 -77
  103. package/dist/gates/scoped-gates.test.d.ts +0 -1
  104. package/dist/gates/scoped-gates.test.js +0 -52
  105. package/dist/gates/security-patterns-owasp.test.d.ts +0 -1
  106. package/dist/gates/security-patterns-owasp.test.js +0 -186
  107. package/dist/gates/security-patterns.test.d.ts +0 -1
  108. package/dist/gates/security-patterns.test.js +0 -194
  109. package/dist/gates/semantic-bugs.test.d.ts +0 -1
  110. package/dist/gates/semantic-bugs.test.js +0 -76
  111. package/dist/gates/side-effect-analysis.test.d.ts +0 -1
  112. package/dist/gates/side-effect-analysis.test.js +0 -162
  113. package/dist/gates/style-drift.test.d.ts +0 -1
  114. package/dist/gates/style-drift.test.js +0 -26
  115. package/dist/gates/test-quality.test.d.ts +0 -1
  116. package/dist/gates/test-quality.test.js +0 -325
  117. package/dist/gates/trusted-reviews.test.d.ts +0 -1
  118. package/dist/gates/trusted-reviews.test.js +0 -16
  119. package/dist/gates/unindexed-reads/queries.test.d.ts +0 -1
  120. package/dist/gates/unindexed-reads/queries.test.js +0 -54
  121. package/dist/gates/unindexed-reads/schema.test.d.ts +0 -1
  122. package/dist/gates/unindexed-reads/schema.test.js +0 -81
  123. package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +0 -1
  124. package/dist/gates/unindexed-reads/unindexed-reads.test.js +0 -90
  125. package/dist/hooks/checker.test.d.ts +0 -1
  126. package/dist/hooks/checker.test.js +0 -159
  127. package/dist/hooks/dlp-confidence.test.d.ts +0 -1
  128. package/dist/hooks/dlp-confidence.test.js +0 -51
  129. package/dist/hooks/dlp-feedback.test.d.ts +0 -1
  130. package/dist/hooks/dlp-feedback.test.js +0 -131
  131. package/dist/hooks/input-validator.test.d.ts +0 -1
  132. package/dist/hooks/input-validator.test.js +0 -329
  133. package/dist/hooks/templates.test.d.ts +0 -1
  134. package/dist/hooks/templates.test.js +0 -27
  135. package/dist/inference/brain-placeholder.test.d.ts +0 -1
  136. package/dist/inference/brain-placeholder.test.js +0 -28
  137. package/dist/inference/cloud-provider.test.d.ts +0 -1
  138. package/dist/inference/cloud-provider.test.js +0 -139
  139. package/dist/inference/executable.test.d.ts +0 -1
  140. package/dist/inference/executable.test.js +0 -41
  141. package/dist/inference/http-download.test.d.ts +0 -1
  142. package/dist/inference/http-download.test.js +0 -109
  143. package/dist/inference/llama-engine-checksum.test.d.ts +0 -1
  144. package/dist/inference/llama-engine-checksum.test.js +0 -27
  145. package/dist/inference/llama-engine.test.d.ts +0 -1
  146. package/dist/inference/llama-engine.test.js +0 -51
  147. package/dist/inference/llama-process.test.d.ts +0 -1
  148. package/dist/inference/llama-process.test.js +0 -61
  149. package/dist/inference/local-model.test.d.ts +0 -1
  150. package/dist/inference/local-model.test.js +0 -23
  151. package/dist/inference/model-download.test.d.ts +0 -1
  152. package/dist/inference/model-download.test.js +0 -125
  153. package/dist/inference/model-manager.test.d.ts +0 -1
  154. package/dist/inference/model-manager.test.js +0 -24
  155. package/dist/inference/types.test.d.ts +0 -1
  156. package/dist/inference/types.test.js +0 -19
  157. package/dist/memory/recall.test.d.ts +0 -1
  158. package/dist/memory/recall.test.js +0 -36
  159. package/dist/pattern-index/indexer.test.d.ts +0 -6
  160. package/dist/pattern-index/indexer.test.js +0 -197
  161. package/dist/pattern-index/matcher.test.d.ts +0 -6
  162. package/dist/pattern-index/matcher.test.js +0 -238
  163. package/dist/pattern-index/pattern-reuse.test.d.ts +0 -1
  164. package/dist/pattern-index/pattern-reuse.test.js +0 -76
  165. package/dist/pattern-index/semantic-runtime.test.d.ts +0 -1
  166. package/dist/pattern-index/semantic-runtime.test.js +0 -31
  167. package/dist/pattern-index/staleness.test.d.ts +0 -6
  168. package/dist/pattern-index/staleness.test.js +0 -211
  169. package/dist/review/agent-fixes.test.d.ts +0 -1
  170. package/dist/review/agent-fixes.test.js +0 -32
  171. package/dist/review/backtest-init.test.d.ts +0 -1
  172. package/dist/review/backtest-init.test.js +0 -136
  173. package/dist/review/backtest-judges.test.d.ts +0 -1
  174. package/dist/review/backtest-judges.test.js +0 -32
  175. package/dist/review/backtest-last.test.d.ts +0 -1
  176. package/dist/review/backtest-last.test.js +0 -109
  177. package/dist/review/backtest.test.d.ts +0 -1
  178. package/dist/review/backtest.test.js +0 -183
  179. package/dist/review/baseline.test.d.ts +0 -1
  180. package/dist/review/baseline.test.js +0 -22
  181. package/dist/review/branch-checks.test.d.ts +0 -1
  182. package/dist/review/branch-checks.test.js +0 -48
  183. package/dist/review/check-outcomes.test.d.ts +0 -1
  184. package/dist/review/check-outcomes.test.js +0 -56
  185. package/dist/review/code-patterns.test.d.ts +0 -1
  186. package/dist/review/code-patterns.test.js +0 -142
  187. package/dist/review/dead-code.test.d.ts +0 -1
  188. package/dist/review/dead-code.test.js +0 -154
  189. package/dist/review/deep-runs.test.d.ts +0 -1
  190. package/dist/review/deep-runs.test.js +0 -23
  191. package/dist/review/effectiveness.test.d.ts +0 -1
  192. package/dist/review/effectiveness.test.js +0 -42
  193. package/dist/review/fix-scope.test.d.ts +0 -1
  194. package/dist/review/fix-scope.test.js +0 -104
  195. package/dist/review/generated-files.test.d.ts +0 -1
  196. package/dist/review/generated-files.test.js +0 -25
  197. package/dist/review/migration-order.test.d.ts +0 -1
  198. package/dist/review/migration-order.test.js +0 -62
  199. package/dist/review/quiet.test.d.ts +0 -1
  200. package/dist/review/quiet.test.js +0 -38
  201. package/dist/review/receipt.test.d.ts +0 -1
  202. package/dist/review/receipt.test.js +0 -60
  203. package/dist/review/review-task.test.d.ts +0 -1
  204. package/dist/review/review-task.test.js +0 -89
  205. package/dist/review/review.test.d.ts +0 -1
  206. package/dist/review/review.test.js +0 -203
  207. package/dist/review/reviewer/adapters.test.d.ts +0 -1
  208. package/dist/review/reviewer/adapters.test.js +0 -100
  209. package/dist/review/reviewer/api-judge.test.d.ts +0 -1
  210. package/dist/review/reviewer/api-judge.test.js +0 -108
  211. package/dist/review/reviewer/background.test.d.ts +0 -1
  212. package/dist/review/reviewer/background.test.js +0 -118
  213. package/dist/review/reviewer/context.test.d.ts +0 -1
  214. package/dist/review/reviewer/context.test.js +0 -45
  215. package/dist/review/reviewer/exec.test.d.ts +0 -1
  216. package/dist/review/reviewer/exec.test.js +0 -59
  217. package/dist/review/reviewer/inputs.test.d.ts +0 -1
  218. package/dist/review/reviewer/inputs.test.js +0 -52
  219. package/dist/review/reviewer/panel.test.d.ts +0 -1
  220. package/dist/review/reviewer/panel.test.js +0 -110
  221. package/dist/review/reviewer/record.test.d.ts +0 -1
  222. package/dist/review/reviewer/record.test.js +0 -40
  223. package/dist/review/reviewer/rule-writer.test.d.ts +0 -1
  224. package/dist/review/reviewer/rule-writer.test.js +0 -52
  225. package/dist/review/reviewer/settings.test.d.ts +0 -1
  226. package/dist/review/reviewer/settings.test.js +0 -57
  227. package/dist/review/reviewer/usage.test.d.ts +0 -1
  228. package/dist/review/reviewer/usage.test.js +0 -14
  229. package/dist/review/reviewer.test.d.ts +0 -1
  230. package/dist/review/reviewer.test.js +0 -792
  231. package/dist/review/stories.test.d.ts +0 -1
  232. package/dist/review/stories.test.js +0 -58
  233. package/dist/review/toolchain.test.d.ts +0 -1
  234. package/dist/review/toolchain.test.js +0 -108
  235. package/dist/review/typed/redundancy.test.d.ts +0 -1
  236. package/dist/review/typed/redundancy.test.js +0 -315
  237. package/dist/review/typed/schema-nullability.test.d.ts +0 -1
  238. package/dist/review/typed/schema-nullability.test.js +0 -63
  239. package/dist/review-learning/human-edits.test.d.ts +0 -1
  240. package/dist/review-learning/human-edits.test.js +0 -47
  241. package/dist/review-learning/repo-rules.test.d.ts +0 -1
  242. package/dist/review-learning/repo-rules.test.js +0 -93
  243. package/dist/review-learning/review-learning.test.d.ts +0 -1
  244. package/dist/review-learning/review-learning.test.js +0 -249
  245. package/dist/safety.test.d.ts +0 -1
  246. package/dist/safety.test.js +0 -42
  247. package/dist/semantic/benchmark.test.d.ts +0 -1
  248. package/dist/semantic/benchmark.test.js +0 -22
  249. package/dist/semantic/intent/intent.test.d.ts +0 -1
  250. package/dist/semantic/intent/intent.test.js +0 -46
  251. package/dist/semantic/learn/learn.test.d.ts +0 -1
  252. package/dist/semantic/learn/learn.test.js +0 -67
  253. package/dist/semantic/origins.test.d.ts +0 -1
  254. package/dist/semantic/origins.test.js +0 -77
  255. package/dist/semantic/project-facts.test.d.ts +0 -1
  256. package/dist/semantic/project-facts.test.js +0 -45
  257. package/dist/semantic/sites/call-sites.test.d.ts +0 -1
  258. package/dist/semantic/sites/call-sites.test.js +0 -46
  259. package/dist/services/adaptive-thresholds.test.d.ts +0 -1
  260. package/dist/services/adaptive-thresholds.test.js +0 -53
  261. package/dist/services/agent-history.test.d.ts +0 -1
  262. package/dist/services/agent-history.test.js +0 -69
  263. package/dist/services/context-scope-summary.test.d.ts +0 -1
  264. package/dist/services/context-scope-summary.test.js +0 -17
  265. package/dist/services/context-telemetry-service.test.d.ts +0 -1
  266. package/dist/services/context-telemetry-service.test.js +0 -182
  267. package/dist/services/cursor-usage-sync.test.d.ts +0 -1
  268. package/dist/services/cursor-usage-sync.test.js +0 -173
  269. package/dist/services/engineering-knowledge-graph.test.d.ts +0 -1
  270. package/dist/services/engineering-knowledge-graph.test.js +0 -76
  271. package/dist/services/model-pricing.test.d.ts +0 -1
  272. package/dist/services/model-pricing.test.js +0 -44
  273. package/dist/services/observed-savings.test.d.ts +0 -1
  274. package/dist/services/observed-savings.test.js +0 -37
  275. package/dist/services/score-history.test.d.ts +0 -1
  276. package/dist/services/score-history.test.js +0 -61
  277. package/dist/smoke.test.d.ts +0 -1
  278. package/dist/smoke.test.js +0 -17
  279. package/dist/storage/cache-cleanup.test.d.ts +0 -1
  280. package/dist/storage/cache-cleanup.test.js +0 -54
  281. package/dist/storage/context-telemetry.test.d.ts +0 -1
  282. package/dist/storage/context-telemetry.test.js +0 -80
  283. package/dist/storage/db.test.d.ts +0 -1
  284. package/dist/storage/db.test.js +0 -46
  285. package/dist/storage/fix-lessons.test.d.ts +0 -1
  286. package/dist/storage/fix-lessons.test.js +0 -61
  287. package/dist/storage/lessons.test.d.ts +0 -1
  288. package/dist/storage/lessons.test.js +0 -81
  289. package/dist/storage/local-encryption.test.d.ts +0 -1
  290. package/dist/storage/local-encryption.test.js +0 -34
  291. package/dist/storage/local-memory.test.d.ts +0 -1
  292. package/dist/storage/local-memory.test.js +0 -55
  293. package/dist/storage/share-memory.test.d.ts +0 -1
  294. package/dist/storage/share-memory.test.js +0 -34
  295. package/dist/storage/team-diagnostics.test.d.ts +0 -1
  296. package/dist/storage/team-diagnostics.test.js +0 -51
  297. package/dist/storage/team-scope.test.d.ts +0 -1
  298. package/dist/storage/team-scope.test.js +0 -22
  299. package/dist/storage/team-store.test.d.ts +0 -1
  300. package/dist/storage/team-store.test.js +0 -11
  301. package/dist/storage/team-sync-scope.test.d.ts +0 -1
  302. package/dist/storage/team-sync-scope.test.js +0 -100
  303. package/dist/storage/team-vector-store.test.d.ts +0 -1
  304. package/dist/storage/team-vector-store.test.js +0 -56
  305. package/dist/storage/telemetry-scope.test.d.ts +0 -1
  306. package/dist/storage/telemetry-scope.test.js +0 -38
  307. package/dist/task/thread.test.d.ts +0 -1
  308. package/dist/task/thread.test.js +0 -133
  309. package/dist/telemetry/telemetry.test.d.ts +0 -1
  310. package/dist/telemetry/telemetry.test.js +0 -64
  311. package/dist/types/index.test.d.ts +0 -1
  312. package/dist/types/index.test.js +0 -33
  313. package/dist/utils/command-line.test.d.ts +0 -1
  314. package/dist/utils/command-line.test.js +0 -8
  315. package/dist/utils/diff-removed.test.d.ts +0 -1
  316. package/dist/utils/diff-removed.test.js +0 -29
  317. package/dist/utils/diff.test.d.ts +0 -1
  318. package/dist/utils/diff.test.js +0 -38
  319. package/dist/utils/glob-paths.test.d.ts +0 -1
  320. package/dist/utils/glob-paths.test.js +0 -40
  321. package/dist/utils/profile.test.d.ts +0 -1
  322. package/dist/utils/profile.test.js +0 -68
  323. package/dist/utils/scanner.test.d.ts +0 -1
  324. package/dist/utils/scanner.test.js +0 -48
  325. package/dist/utils/scope.test.d.ts +0 -1
  326. package/dist/utils/scope.test.js +0 -38
@@ -1,203 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { GateRunner } from '../gates/runner.js';
7
- import { ConfigSchema } from '../types/index.js';
8
- import { parseDiff } from '../utils/diff.js';
9
- import { splitByChangedLines } from './changed-lines.js';
10
- import { anchorInChangedFunction, changedFunctionSpans } from './changed-function-spans.js';
11
- import { diffFromGit } from './git-diff.js';
12
- import { reviewChange, toReviewFinding } from './review.js';
13
- import { findingKey } from './quiet.js';
14
- /** These tests are about other checks; a new unreferenced fixture file is not their subject. */
15
- const NO_DEAD_CODE = { unused_exports: { enabled: false }, orphan_files: { enabled: false } };
16
- const LEAKY = [
17
- 'export async function notify(endpoint: string, signature: string) {',
18
- " return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
19
- '}',
20
- '',
21
- ].join('\n');
22
- function failure(file, line) {
23
- return { id: 'x', title: 'X', details: 'd', files: file ? [file] : [], ...(line !== undefined ? { line } : {}) };
24
- }
25
- describe('splitByChangedLines', () => {
26
- it('separates changed-line, file-level, unlocated and outside findings', () => {
27
- const changed = { 'a.ts': new Set([3]) };
28
- const split = splitByChangedLines([failure('a.ts', 3), failure('a.ts', 9), failure('a.ts'), failure('b.ts'), failure(undefined)], changed);
29
- expect(split.findings.map(f => f.line)).toEqual([3]);
30
- expect(split.fileFindings).toHaveLength(1);
31
- expect(split.unlocated).toBe(1);
32
- expect(split.outside).toBe(2);
33
- });
34
- it('keeps a deep finding inside a changed function, anchored on the nearest changed line', () => {
35
- const deep = (line) => ({ ...failure('a.ts', line), provenance: 'deep-analysis' });
36
- const changed = { 'a.ts': new Set([12, 14]) };
37
- const spans = { 'a.ts': [[10, 20]] };
38
- const split = splitByChangedLines([deep(11), deep(30), failure('a.ts', 11)], changed, spans);
39
- expect(split.findings).toEqual([expect.objectContaining({ line: 11, anchorLine: 12 })]);
40
- expect(split.contextFindings.map(f => f.line)).toEqual([30]); // elsewhere in a changed file: shown, not blocking
41
- expect(split.outside).toBe(1); // a rule finding on an unchanged line is pre-existing, as before
42
- });
43
- it('counts a multi-line finding as changed when lines inside it were deleted', () => {
44
- // fetch(url, { <- line 4, unchanged
45
- // redirect: ... <- deleted, so the removal sits before new line 5
46
- // }) <- line 5
47
- const call = { ...failure('a.ts', 4), endLine: 5 };
48
- const outsideCall = { ...failure('a.ts', 9), endLine: 10 };
49
- const removed = { 'a.ts': [{ line: 5, text: [" redirect: 'manual',"] }] };
50
- const split = splitByChangedLines([call, outsideCall, failure('a.ts', 4)], {}, {}, removed);
51
- expect(split.findings).toEqual([call]);
52
- expect(split.outside).toBe(2);
53
- });
54
- });
55
- describe('changedFunctionSpans', () => {
56
- it('spans the outermost function around changed lines, and anchors to the nearest changed line in it', () => {
57
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'spans-'));
58
- try {
59
- fs.writeFileSync(path.join(dir, 'a.ts'), 'const x = 1;\nexport function f(a: number) {\n if (a > 0) {\n return a;\n }\n return 0;\n}\n');
60
- const spans = changedFunctionSpans(dir, { 'a.ts': new Set([4]), 'missing.ts': new Set([1]), 'notes.md': new Set([1]) });
61
- expect(spans).toEqual({ 'a.ts': [[2, 7]] });
62
- expect(anchorInChangedFunction(6, new Set([4]), spans['a.ts'])).toBe(4);
63
- expect(anchorInChangedFunction(1, new Set([4]), spans['a.ts'])).toBeUndefined();
64
- }
65
- finally {
66
- fs.rmSync(dir, { recursive: true, force: true });
67
- }
68
- });
69
- });
70
- describe('git-backed review', () => {
71
- let repo;
72
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
73
- const write = (rel, body) => {
74
- fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
75
- fs.writeFileSync(path.join(repo, rel), body);
76
- };
77
- beforeEach(() => {
78
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'review-core-'));
79
- git('init', '-q', '-b', 'main');
80
- git('config', 'user.email', 't@example.com');
81
- git('config', 'user.name', 't');
82
- git('config', 'commit.gpgsign', 'false');
83
- });
84
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
85
- it('works in a repository with no commits yet', () => {
86
- write('src/new.ts', 'export const a = 1;\n');
87
- expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/new.ts': new Set([1]) });
88
- });
89
- it('takes uncommitted edits and untracked files from the working tree', () => {
90
- write('src/a.ts', 'export const a = 1;\nexport const b = 2;\n');
91
- git('add', '-A');
92
- git('commit', '-qm', 'init');
93
- write('src/a.ts', 'export const a = 1;\nexport const b = 3;\n');
94
- write('src/new.ts', 'export const c = 1;\nexport const d = 2;\n');
95
- expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/a.ts': new Set([2]), 'src/new.ts': new Set([1, 2]) });
96
- });
97
- it('takes a branch against its merge-base, committed and uncommitted', () => {
98
- write('src/a.ts', 'export const a = 1;\n');
99
- git('add', '-A');
100
- git('commit', '-qm', 'init');
101
- git('checkout', '-q', '-b', 'feature');
102
- write('src/b.ts', 'export const b = 1;\n');
103
- git('add', '-A');
104
- git('commit', '-qm', 'feature');
105
- write('src/a.ts', 'export const a = 1;\nexport const z = 2;\n');
106
- expect(parseDiff(diffFromGit(repo, { mode: 'base', base: 'main' }))).toEqual({ 'src/a.ts': new Set([2]), 'src/b.ts': new Set([1]) });
107
- });
108
- it('fails a change that introduces a bug and ignores the same bug in untouched code', async () => {
109
- write('src/old.ts', LEAKY);
110
- git('add', '-A');
111
- git('commit', '-qm', 'init');
112
- write('src/notify.ts', LEAKY);
113
- const config = ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: true } } });
114
- const result = await reviewChange({ cwd: repo, config });
115
- const semantic = result.findings.filter(f => f.id === 'semantic-bugs');
116
- expect(result.status).toBe('FAIL');
117
- expect(semantic.map(f => [f.files?.[0], f.line])).toEqual([['src/notify.ts', 2]]);
118
- expect(toReviewFinding(semantic[0])).toMatchObject({ id: 'semantic-bugs', file: 'src/notify.ts', line: 2, severity: 'high' });
119
- });
120
- it("names every check it ran in the summary, the review's own beside the gates", async () => {
121
- write('src/a.ts', 'export const a = 1;\n');
122
- git('add', '-A');
123
- git('commit', '-qm', 'init');
124
- write('src/b.ts', 'export const unused = 2;\n');
125
- git('add', '-A');
126
- const config = ConfigSchema.parse({ gates: { migration_order: { enabled: false } } });
127
- const { report } = await reviewChange({ cwd: repo, config });
128
- expect(report?.summary).toMatchObject({ 'unused-exports': 'FAIL', 'migration-order': 'SKIP', 'query-patterns': 'PASS', 'change-sweep': 'PASS' });
129
- expect(report?.summary).not.toHaveProperty('redundancy'); // typed checks were not asked for
130
- });
131
- it('reports what the change introduced, not what the touched code already had', async () => {
132
- const branchy = (name, extra = '') => [
133
- `export function ${name}(x: number) {`,
134
- ...Array.from({ length: 12 }, (_, i) => ` if (x === ${i}) return ${i};`),
135
- extra,
136
- ' return -1;',
137
- '}',
138
- '',
139
- ].join('\n');
140
- write('src/old.ts', branchy('legacy'));
141
- git('add', '-A');
142
- git('commit', '-qm', 'init');
143
- write('src/old.ts', branchy('legacy', ' if (x === 99) return 99;'));
144
- write('src/fresh.ts', branchy('fresh'));
145
- const config = ConfigSchema.parse({ version: 1 });
146
- const result = await reviewChange({ cwd: repo, config });
147
- const complexity = [...result.findings, ...result.fileFindings, ...result.advisory].filter(f => f.id === 'AST_COMPLEXITY');
148
- expect(complexity.map(f => f.files?.[0])).toEqual(['src/fresh.ts']);
149
- expect(result.preexisting).toBeGreaterThanOrEqual(1);
150
- const all = await reviewChange({ cwd: repo, config: ConfigSchema.parse({ version: 1, review: { show_preexisting: true } }) });
151
- const shown = [...all.findings, ...all.fileFindings, ...all.advisory].filter(f => f.id === 'AST_COMPLEXITY');
152
- expect(shown.map(f => f.files?.[0]).sort()).toEqual(['src/fresh.ts', 'src/old.ts']);
153
- expect(all.preexisting).toBe(0);
154
- }, 30_000); // two whole reviews, each running the rules on the change and on the base tree
155
- it('lets a change dismiss its own finding only when the review trusts the working tree', async () => {
156
- write('src/old.ts', 'export const a = 1;\n');
157
- write('.gitignore', '.rigour/*\n!.rigour/dismissed.json\n');
158
- git('add', '-A');
159
- git('commit', '-qm', 'init');
160
- git('checkout', '-q', '-b', 'pr');
161
- write('src/notify.ts', LEAKY);
162
- const config = ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: true }, ...NO_DEAD_CODE } });
163
- const key = findingKey((await reviewChange({ cwd: repo, config })).findings[0]);
164
- write('.rigour/dismissed.json', JSON.stringify({ version: 1, entries: [{ key, reason: 'trust me', at: 'now' }] }));
165
- git('add', '-A');
166
- git('commit', '-qm', 'pr');
167
- const trusting = await reviewChange({ cwd: repo, config, source: { mode: 'base', base: 'main' } });
168
- expect([trusting.status, trusting.dismissed]).toEqual(['PASS', 1]);
169
- expect(trusting.controlFilesChanged).toEqual(['.rigour/dismissed.json']);
170
- const independent = await reviewChange({ cwd: repo, config, source: { mode: 'base', base: 'main' }, trustedRef: 'main' });
171
- expect([independent.status, independent.dismissed]).toEqual(['FAIL', 0]);
172
- });
173
- it('runs every gate in a git worktree, where .git is a file', async () => {
174
- write('src/a.ts', 'export const a = 1;\n');
175
- git('add', '-A');
176
- git('commit', '-qm', 'init');
177
- const worktree = path.join(repo, 'wt');
178
- git('worktree', 'add', '-q', worktree);
179
- fs.writeFileSync(path.join(worktree, 'src/a.ts'), 'export const a = 1;\nexport const b = 2;\n');
180
- const config = ConfigSchema.parse({ version: 1, ignore: ['.git/**', 'wt/**'] });
181
- const result = await reviewChange({ cwd: worktree, config });
182
- expect(result.gateErrors).toEqual([]);
183
- expect(result.status).not.toBe('ERROR');
184
- });
185
- it('is ERROR when a proven gate crashed, and only lists a crashed heuristic gate', async () => {
186
- write('src/a.ts', 'export const a = 1;\n');
187
- const config = ConfigSchema.parse({ version: 1, gates: NO_DEAD_CODE });
188
- const crash = (summary) => vi.spyOn(GateRunner.prototype, 'run').mockResolvedValueOnce({ status: 'FAIL', summary, failures: [], stats: { duration_ms: 0 } });
189
- crash({ 'hallucinated-imports': 'ERROR' });
190
- const proven = await reviewChange({ cwd: repo, config });
191
- expect([proven.status, proven.gateErrors]).toEqual(['ERROR', ['hallucinated-imports']]);
192
- crash({ 'style-drift': 'ERROR', 'semantic-bugs': 'PASS' });
193
- const heuristic = await reviewChange({ cwd: repo, config });
194
- expect([heuristic.status, heuristic.gateErrors]).toEqual(['PASS', ['style-drift']]);
195
- });
196
- it('passes when nothing changed', async () => {
197
- write('src/a.ts', 'export const a = 1;\n');
198
- git('add', '-A');
199
- git('commit', '-qm', 'init');
200
- const result = await reviewChange({ cwd: repo, config: ConfigSchema.parse({ version: 1 }) });
201
- expect(result).toMatchObject({ status: 'PASS', findings: [], report: null });
202
- });
203
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,100 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { describe, expect, it } from 'vitest';
5
- import { ADAPTERS, apiVendor, selectReviewers } from './adapters.js';
6
- /** A real `codex exec --json` run (codex-cli 0.160.1), the warning's path shortened: the shape the adapter must read. */
7
- const CODEX = [
8
- '{"type":"thread.started","thread_id":"01a11477-f9fd-7472-8d31-5863de4cbbdb"}',
9
- '{"type":"item.completed","item":{"id":"item_0","type":"error","message":"Codex is ignoring 1 unrecognized configuration setting. Check for typos or deprecated settings.\\n user (~/.codex/config.toml)"}}',
10
- '{"type":"turn.started"}',
11
- '{"type":"item.completed","item":{"id":"item_2","type":"agent_message","text":"{\\"answers\\":[{\\"id\\":\\"a1\\",\\"call\\":\\"unsure\\",\\"evidence\\":\\"none\\"}]}"}}',
12
- '{"type":"turn.completed","usage":{"input_tokens":26565,"cached_input_tokens":13184,"cache_write_input_tokens":0,"output_tokens":24,"reasoning_output_tokens":0}}',
13
- ].join('\n');
14
- describe('reading an agent CLI\'s answer', () => {
15
- it('takes Codex\'s last agent message, never a warning, and its tokens', () => {
16
- expect(ADAPTERS.codex.answer(CODEX)).toEqual({ text: '{"answers":[{"id":"a1","call":"unsure","evidence":"none"}]}', tokens: { input: 26565, output: 24 } });
17
- });
18
- it('takes Claude Code\'s result, its cost and its tokens', () => {
19
- const out = JSON.stringify({ result: '{"prior_points":[]}', total_cost_usd: 0.42, usage: { input_tokens: 10, cache_read_input_tokens: 900, cache_creation_input_tokens: 90, output_tokens: 300 } });
20
- expect(ADAPTERS.claude.answer(out)).toEqual({ text: '{"prior_points":[]}', costUsd: 0.42, tokens: { input: 1000, output: 300 } });
21
- });
22
- it('falls back to the raw output when it is not what the CLI usually prints', () => {
23
- expect(ADAPTERS.claude.answer('not json')).toEqual({ text: 'not json' });
24
- expect(ADAPTERS.codex.answer('plain text')).toEqual({ text: 'plain text' });
25
- });
26
- it("reads Claude Code's event stream: the answer and cost from its result, each turn's usage once, and what every tool call read", () => {
27
- const events = [
28
- { type: 'system', subtype: 'init' },
29
- { type: 'assistant', message: { id: 'm1', usage: { input_tokens: 10, cache_read_input_tokens: 0, cache_creation_input_tokens: 4000, output_tokens: 50 }, content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/work/full.diff' } }] } },
30
- { type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't1', content: 'x'.repeat(1200) }] } },
31
- { type: 'assistant', message: { id: 'm2', usage: { input_tokens: 5, cache_read_input_tokens: 4000, cache_creation_input_tokens: 300, output_tokens: 20 }, content: [{ type: 'text', text: 'checking' }] } },
32
- { type: 'assistant', message: { id: 'm2', usage: { input_tokens: 5, cache_read_input_tokens: 4000, cache_creation_input_tokens: 300, output_tokens: 20 }, content: [{ type: 'tool_use', id: 't2', name: 'Grep', input: { pattern: 'preloadAll' } }] } },
33
- { type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't2', content: [{ type: 'text', text: 'src/a.ts:3' }] }] } },
34
- { type: 'result', result: '{"prior_points":[]}', total_cost_usd: 0.31, usage: { input_tokens: 15, cache_read_input_tokens: 4000, cache_creation_input_tokens: 4300, output_tokens: 70 } },
35
- ];
36
- const answer = ADAPTERS.claude.answer(events.map(e => JSON.stringify(e)).join('\n'));
37
- expect(answer).toMatchObject({ text: '{"prior_points":[]}', costUsd: 0.31, tokens: { input: 8315, output: 70 } });
38
- expect(answer.trace).toEqual({
39
- turns: 2, // m2 arrived as two events: one turn, counted once
40
- usage: { input: 15, cacheRead: 4000, cacheWrite: 4300, output: 70 },
41
- calls: [
42
- { turn: 1, tool: 'Read', target: '/work/full.diff', resultChars: 1200 },
43
- { turn: 2, tool: 'Grep', target: 'preloadAll', resultChars: 10 },
44
- ],
45
- });
46
- expect(ADAPTERS.claude.args('p', undefined)).toEqual(expect.arrayContaining(['--output-format', 'stream-json', '--verbose']));
47
- });
48
- });
49
- describe('a judge reached through an API', () => {
50
- it('is told apart by its model\'s maker, so cross and full modes pair it with a different vendor', () => {
51
- expect(apiVendor({ model: 'anthropic/claude-sonnet-4.5' })).toBe('anthropic');
52
- expect(apiVendor({ model: 'gpt-5' })).toBe('openai');
53
- expect(apiVendor({ model: 'google/gemini-2.5-pro' })).toBe('google');
54
- expect(apiVendor({ model: 'qwen3-coder' })).toBe('other');
55
- expect(apiVendor({ model: 'qwen3-coder', vendor: 'openai' })).toBe('openai');
56
- const vendorOf = (name) => (name === 'api' ? 'openai' : ADAPTERS[name].vendor);
57
- expect(selectReviewers(['claude', 'api'], 'cross', new Set(['anthropic']), new Set(['claude', 'api']), 2, vendorOf)).toEqual(['api']); // the author's vendor is skipped
58
- expect(selectReviewers(['claude', 'api'], 'full', new Set(), new Set(['claude', 'api']), 2, vendorOf)).toEqual(['claude', 'api']);
59
- expect(ADAPTERS.api.answer(JSON.stringify({ result: '{"prior_points":[]}', usage: { input: 10, cacheRead: 5, cacheWrite: 0, output: 3 }, cost_usd: 0.02, trace: { turns: 2, usage: {}, calls: [] } }))).toMatchObject({ text: '{"prior_points":[]}', costUsd: 0.02, tokens: { input: 15, output: 3 }, trace: { turns: 2 } });
60
- expect(ADAPTERS.codex.args('p', undefined, { reasoning: 'medium' })).toContain('model_reasoning_effort=medium');
61
- expect(ADAPTERS.codex.args('p', undefined)).toContain('model_reasoning_effort=high');
62
- });
63
- });
64
- describe('what a judge knows besides the repository', () => {
65
- it('launches claude with no memory file, auto-memory, user settings, hooks or MCP servers of its own', () => {
66
- expect(ADAPTERS.claude.env).toEqual({ CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1', CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1' });
67
- const args = ADAPTERS.claude.args('p', undefined);
68
- expect(args.slice(args.indexOf('--setting-sources'), args.indexOf('--setting-sources') + 2)).toEqual(['--setting-sources', 'project']);
69
- expect(args).toEqual(expect.arrayContaining(['--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}']));
70
- expect(JSON.parse(args[args.indexOf('--settings') + 1])).toMatchObject({ hooks: {} });
71
- for (const v of ['2.1.285 (Claude Code)', '2.1.300', '2.2.0', '3.0.0'])
72
- expect(ADAPTERS.claude.outsideRepo('/home/x', v)).toBeUndefined();
73
- expect(ADAPTERS.claude.outsideRepo('/home/x', '2.1.284 (Claude Code)')).toBe('claude 2.1.284: memory isolation unverified (needs 2.1.285 or later)');
74
- expect(ADAPTERS.claude.outsideRepo('/home/x', '2.0.999')).toContain('unverified');
75
- expect(ADAPTERS.claude.outsideRepo('/home/x', '1.9.400')).toContain('unverified');
76
- expect(ADAPTERS.claude.outsideRepo('/home/x', undefined)).toBe('claude (version unknown): memory isolation unverified (needs 2.1.285 or later)');
77
- });
78
- it("says when codex will also read a person's own Codex config or instructions, and from where", () => {
79
- const home = fs.mkdtempSync(path.join(os.tmpdir(), 'judge-home-'));
80
- const saved = process.env.CODEX_HOME;
81
- delete process.env.CODEX_HOME;
82
- try {
83
- expect(ADAPTERS.codex.outsideRepo(home, undefined)).toBeUndefined();
84
- fs.mkdirSync(path.join(home, '.codex'));
85
- fs.writeFileSync(path.join(home, '.codex', 'AGENTS.md'), 'Always answer in French.\n');
86
- fs.writeFileSync(path.join(home, '.codex', 'config.toml'), 'personality = "pragmatic"\n');
87
- expect(ADAPTERS.codex.outsideRepo(home, undefined)).toBe(`codex also reads ${path.join(home, '.codex', 'config.toml')} and ${path.join(home, '.codex', 'AGENTS.md')}`);
88
- process.env.CODEX_HOME = path.join(home, 'elsewhere');
89
- expect(ADAPTERS.codex.outsideRepo(home, undefined)).toBeUndefined(); // CODEX_HOME wins over the home folder
90
- expect(ADAPTERS.cursor.outsideRepo(home, undefined)).toBe('cursor also reads your own Cursor rules and settings');
91
- }
92
- finally {
93
- if (saved === undefined)
94
- delete process.env.CODEX_HOME;
95
- else
96
- process.env.CODEX_HOME = saved;
97
- fs.rmSync(home, { recursive: true, force: true });
98
- }
99
- });
100
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,108 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
- import { runApiJudge } from './api-judge.js';
7
- let repo;
8
- let work;
9
- beforeEach(() => {
10
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'api-judge-'));
11
- work = fs.mkdtempSync(path.join(os.tmpdir(), 'api-judge-inputs-'));
12
- execFileSync('git', ['-C', repo, 'init', '-q']);
13
- fs.mkdirSync(path.join(repo, 'src'));
14
- fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export function job() {\n return 1;\n}\n');
15
- execFileSync('git', ['-C', repo, 'add', '-A']);
16
- execFileSync('git', ['-C', repo, '-c', 'user.email=t@x', '-c', 'user.name=t', 'commit', '-qm', 'init']);
17
- fs.writeFileSync(path.join(work, 'full.diff'), '+++ b/src/job.ts\n');
18
- });
19
- afterEach(() => { for (const d of [repo, work])
20
- fs.rmSync(d, { recursive: true, force: true }); });
21
- /** A model that plays a scripted conversation: each entry is what it answers to the next request. */
22
- function model(turns) {
23
- const seen = [];
24
- let i = 0;
25
- const fetchImpl = (async (_url, init) => {
26
- const body = JSON.parse(init.body);
27
- seen.push(body);
28
- const turn = turns[i++] ?? { text: 'done' };
29
- if (turn.status)
30
- return new Response('nope', { status: turn.status });
31
- const message = turn.tools
32
- ? { role: 'assistant', content: null, tool_calls: turn.tools.map((t, k) => ({ id: `c${i}-${k}`, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.args) } })) }
33
- : { role: 'assistant', content: turn.text };
34
- return new Response(JSON.stringify({ choices: [{ message }], usage: turn.usage ?? { prompt_tokens: 100, completion_tokens: 10, prompt_tokens_details: { cached_tokens: 60 } } }), { status: 200, headers: { 'content-type': 'application/json' } });
35
- });
36
- return { fetchImpl, seen };
37
- }
38
- const options = (fetchImpl, extra = {}) => ({ url: 'https://example.test/v1', model: 'some-model', key: 'k', maxTurns: 10, timeoutMs: 30_000, cwd: repo, roots: [repo, work], fetchImpl, ...extra });
39
- describe('the API judge', () => {
40
- it('runs the tools the model asks for inside the checkout and the inputs, then returns its answer with the usage and the trace', async () => {
41
- const { fetchImpl, seen } = model([
42
- { tools: [{ name: 'read_file', args: { path: path.join(work, 'full.diff') } }, { name: 'read_file', args: { path: 'src/job.ts', start_line: 2, end_line: 2 } }] },
43
- { tools: [{ name: 'search', args: { pattern: 'return' } }, { name: 'git', args: { args: ['log', '-1', '--format=%s'] } }, { name: 'list_dir', args: { path: 'src' } }] },
44
- { text: '{"prior_points":[]}', usage: { prompt_tokens: 50, completion_tokens: 5, cost: 0.01 } },
45
- ]);
46
- const run = await runApiJudge('review this', options(fetchImpl));
47
- expect(run.exitCode).toBe(0);
48
- const answer = JSON.parse(run.stdout);
49
- expect(answer.result).toBe('{"prior_points":[]}');
50
- expect(answer.usage).toEqual({ input: 130, cacheRead: 120, cacheWrite: 0, output: 25 });
51
- expect(answer.cost_usd).toBe(0.01);
52
- expect(answer.trace.turns).toBe(3);
53
- expect(answer.trace.calls.map(c => c.tool)).toEqual(['Read', 'Read', 'Grep', 'Bash', 'Glob']);
54
- const toolResults = (request) => request.messages.filter((m) => m.role === 'tool').map((m) => m.content);
55
- expect(toolResults(seen[1])).toEqual(['1: +++ b/src/job.ts\n2: ', '2: return 1;']); // turn 1's reads, as the model got them
56
- expect(toolResults(seen[2]).slice(2)).toEqual(['src/job.ts:2: return 1;', 'init', 'job.ts']); // turn 2's search, git and listing
57
- expect(seen[0].messages[0].role).toBe('system');
58
- expect(seen[0].tools.map((t) => t.function.name)).toEqual(['read_file', 'search', 'list_dir', 'git']);
59
- });
60
- it('refuses to read outside the roots, a symlink out included, and refuses git that writes or points elsewhere', async () => {
61
- fs.symlinkSync(os.homedir(), path.join(repo, 'out'));
62
- const { fetchImpl, seen } = model([
63
- { tools: [{ name: 'read_file', args: { path: '/etc/hosts' } }, { name: 'read_file', args: { path: 'out/.bashrc' } }, { name: 'git', args: { args: ['log', '--output=/tmp/x'] } }, { name: 'git', args: { args: ['push'] } }, { name: 'nope', args: {} }] },
64
- { text: 'ok' },
65
- ]);
66
- await runApiJudge('p', options(fetchImpl));
67
- const results = seen[1].messages.filter((m) => m.role === 'tool').map((m) => m.content);
68
- expect(results).toEqual([
69
- 'refused: outside the repository and the review inputs', 'refused: outside the repository and the review inputs',
70
- 'refused: only read-only git commands, without options that write or point elsewhere', 'refused: only read-only git commands, without options that write or point elsewhere',
71
- 'refused: no tool named nope',
72
- ]);
73
- });
74
- it('fails closed on a turn cap, an HTTP error and a timeout, saying why', async () => {
75
- const loop = model(Array.from({ length: 5 }, () => ({ tools: [{ name: 'list_dir', args: { path: '.' } }] })));
76
- expect(await runApiJudge('p', options(loop.fetchImpl, { maxTurns: 3 }))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('no answer within 3 turns') });
77
- expect(await runApiJudge('p', options(model([{ status: 429 }]).fetchImpl))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('HTTP 429') });
78
- const slow = (async (_u, init) => new Promise((_r, reject) => init.signal.addEventListener('abort', () => reject(new Error('aborted')))));
79
- expect(await runApiJudge('p', options(slow, { timeoutMs: 50 }))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('request failed') });
80
- });
81
- it('fails closed on an empty final message, saying why, and echoes back only what the API needs to continue', async () => {
82
- const empty = (async () => new Response(JSON.stringify({ choices: [{ message: { role: 'assistant', content: '', reasoning: 'thinking…' }, finish_reason: 'length' }], usage: {} }), { status: 200 }));
83
- expect(await runApiJudge('p', options(empty))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('an empty answer on turn 1 (finish_reason length)') });
84
- const providerError = (async () => new Response(JSON.stringify({ choices: [{ message: { role: 'assistant', content: '' }, finish_reason: 'error', error: { message: 'Provider returned error', code: 502 } }], usage: {} }), { status: 200 }));
85
- expect(await runApiJudge('p', options(providerError))).toMatchObject({ exitCode: 1, stderr: expect.stringContaining('the API reported an error: {"message":"Provider returned error","code":502}') });
86
- const { fetchImpl, seen } = model([{ tools: [{ name: 'list_dir', args: { path: '.' } }] }, { text: 'ok' }]);
87
- await runApiJudge('p', options(fetchImpl));
88
- const echoed = seen[1].messages.find((m) => m.role === 'assistant');
89
- expect(Object.keys(echoed).sort()).toEqual(['content', 'role', 'tool_calls']); // no reasoning prose sent back
90
- expect(seen[0].max_tokens).toBe(32000);
91
- });
92
- it('gives the judge its inputs inline, smallest first, the diff cut with a note when the budget runs out', async () => {
93
- const { fetchImpl, seen } = model([{ text: 'ok' }]);
94
- const inputs = [{ path: '/w/full.diff', text: 'x'.repeat(300_000) }, { path: '/w/pr-description.md', text: 'the description' }, { path: '/w/previous-reviews.md', text: 'the reviews' }];
95
- await runApiJudge('review this', options(fetchImpl, { inputs }));
96
- const first = seen[0].messages[1].content;
97
- expect(first.startsWith('review this\n\nThe inputs named above, inline')).toBe(true);
98
- expect(first.indexOf('### /w/previous-reviews.md')).toBeLessThan(first.indexOf('### /w/full.diff')); // smallest first
99
- expect(first).toContain('…[cut here: read /w/full.diff with read_file for the rest]');
100
- expect(first.length).toBeLessThan(241_000);
101
- });
102
- it('passes the reasoning effort when asked', async () => {
103
- const { fetchImpl, seen } = model([{ text: 'ok' }]);
104
- await runApiJudge('p', options(fetchImpl, { reasoning: 'low' }));
105
- expect(seen[0].reasoning_effort).toBe('low');
106
- expect(seen[0].model).toBe('some-model');
107
- });
108
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,118 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
5
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
- import { ConfigSchema } from '../../types/index.js';
7
- import { backgroundReview, reviewStatus, startBackgroundReview } from './background.js';
8
- import { VerdictStore } from './store.js';
9
- let repo;
10
- let bin;
11
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
12
- const config = ConfigSchema.parse({ version: 1, review: { reviewer: { enabled: true } } });
13
- const originalPath = process.env.PATH;
14
- let PR = { number: 7, state: 'OPEN', isDraft: false, author: { login: 'author' }, body: 'desc' };
15
- const VERDICT = JSON.stringify({ prior_points: [], redundant: [], reads: [], scans: [], merge_impact: [], findings: [{ class: 'correctness', file: 'src/job.ts', line: 1, issue: 'never locks', quote: 'export const job = 1;', consequence: 'a second run reads stale rows' }], carried: [], resolved_previous: [] });
16
- /** Real git; a pull request on the branch; a claude that answers one finding. */
17
- const exec = async (command, args, options) => {
18
- if (command === 'git') {
19
- try {
20
- return { exitCode: 0, stdout: execFileSync('git', args, { cwd: options.cwd, encoding: 'utf8' }), stderr: '' };
21
- }
22
- catch (error) {
23
- return { exitCode: 1, stdout: '', stderr: String(error.message) };
24
- }
25
- }
26
- if (command === 'gh') {
27
- if (args[0] === 'pr')
28
- return { exitCode: 0, stdout: JSON.stringify(PR), stderr: '' };
29
- return { exitCode: 0, stdout: '[]', stderr: '' };
30
- }
31
- if (args[0] === '--version')
32
- return { exitCode: 0, stdout: '1.0.0\n', stderr: '' };
33
- return { exitCode: 0, stdout: JSON.stringify({ result: VERDICT }), stderr: '' };
34
- };
35
- beforeEach(() => {
36
- bin = fs.mkdtempSync(path.join(os.tmpdir(), 'bin-'));
37
- fs.writeFileSync(path.join(bin, process.platform === 'win32' ? 'claude.cmd' : 'claude'), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
38
- process.env.PATH = [bin, originalPath ?? ''].join(path.delimiter);
39
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'background-'));
40
- git('init', '-q', '-b', 'main');
41
- git('config', 'user.email', 't@example.com');
42
- git('config', 'user.name', 't');
43
- git('config', 'commit.gpgsign', 'false');
44
- fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
45
- git('add', '-A');
46
- git('commit', '-qm', 'init');
47
- git('checkout', '-qb', 'feature');
48
- fs.mkdirSync(path.join(repo, 'src'));
49
- fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export const job = 1;\n');
50
- git('add', '-A');
51
- git('commit', '-qm', 'job');
52
- });
53
- afterEach(() => {
54
- process.env.PATH = originalPath;
55
- try {
56
- git('worktree', 'prune');
57
- }
58
- catch { /* gone */ }
59
- for (const dir of [repo, bin])
60
- fs.rmSync(dir, { recursive: true, force: true });
61
- });
62
- describe('the background review', () => {
63
- it('reviews the pushed commit in a worktree of its own, records the verdict under the branch, and leaves the checkout alone', async () => {
64
- const head = git('rev-parse', 'HEAD');
65
- fs.writeFileSync(path.join(repo, 'src/job.ts'), 'export const job = 2; // edited after the push\n'); // must not be what the reviewer reads
66
- const lines = [];
67
- const result = await backgroundReview(repo, { head, branch: 'feature', base: 'main' }, config, exec, line => lines.push(line));
68
- expect(result).toMatchObject({ outcome: 'findings', pr: 7 });
69
- expect(result.items).toEqual([expect.objectContaining({ file: 'src/job.ts', issue: 'never locks' })]);
70
- expect(lines.at(-1)).toContain('never locks');
71
- expect(fs.readFileSync(path.join(repo, 'src/job.ts'), 'utf8')).toContain('edited after the push');
72
- const store = (await VerdictStore.open(repo, exec));
73
- expect(store.branchState('feature')).toMatchObject({ head, mode: 'full' });
74
- expect(fs.existsSync(store.worktreeDir(head))).toBe(false); // removed when done
75
- const status = await reviewStatus(repo, 'feature', exec);
76
- expect(status).toMatchObject({ branch: 'feature', last: { head, mode: 'full', open: [expect.objectContaining({ issue: 'never locks' })] } });
77
- expect(status?.running).toBeUndefined();
78
- });
79
- it('asks whether a review is due before checking anything out, and the status says why it was skipped', async () => {
80
- PR = { ...PR, isDraft: true };
81
- try {
82
- const head = git('rev-parse', 'HEAD');
83
- const result = await backgroundReview(repo, { head, branch: 'feature', base: 'main' }, config, exec, () => undefined);
84
- expect(result).toMatchObject({ outcome: 'skipped', reason: expect.stringContaining('is a draft') });
85
- const store = (await VerdictStore.open(repo, exec));
86
- expect(fs.existsSync(store.worktreeDir(head))).toBe(false); // never checked out
87
- expect((await reviewStatus(repo, 'feature', exec))?.attempt).toMatchObject({ head, outcome: 'skipped', reason: expect.stringContaining('is a draft') });
88
- }
89
- finally {
90
- PR = { ...PR, isDraft: false };
91
- }
92
- });
93
- it('starts detached with a pid the status reports, and a newer start for the branch ends the older one', async () => {
94
- const head = git('rev-parse', 'HEAD');
95
- const sleeper = [process.execPath, '-e', 'setTimeout(() => {}, 20000)'];
96
- const log = await startBackgroundReview(repo, { head, branch: 'feature', base: 'main' }, sleeper, exec);
97
- expect(log).toMatch(/feature\.log$/);
98
- const first = (await reviewStatus(repo, 'feature', exec))?.running;
99
- expect(first).toMatchObject({ head });
100
- expect(alive(first.pid)).toBe(true);
101
- await startBackgroundReview(repo, { head: 'b'.repeat(40), branch: 'feature', base: 'main' }, sleeper, exec);
102
- const second = (await reviewStatus(repo, 'feature', exec))?.running;
103
- expect(second?.pid).not.toBe(first.pid);
104
- for (let waited = 0; alive(first.pid) && waited < 3000; waited += 50)
105
- await new Promise(resolve => setTimeout(resolve, 50)); // under load the signal lands later
106
- expect(alive(first.pid)).toBe(false);
107
- process.kill(second.pid);
108
- });
109
- });
110
- function alive(pid) {
111
- try {
112
- process.kill(pid, 0);
113
- return true;
114
- }
115
- catch {
116
- return false;
117
- }
118
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,45 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { describe, expect, it } from 'vitest';
5
- import { buildContext, dismissedAs } from './context.js';
6
- const dismissal = { id: 'abcdef0123', file: 'src/x.ts', line: 40, class: 'correctness', issue: 'cache key built from user id may collide', reason: 'ids are unique per tenant', at: '2026-10-06' };
7
- const finding = (over) => ({ id: 'ffff000011', kind: 'finding', class: 'correctness', file: 'src/x.ts', line: 40, issue: 'cache key built from the user id may collide', consequence: 'two users share an entry', ...over });
8
- describe('a dismissal', () => {
9
- it('covers the same finding re-worded nearby', () => {
10
- expect(dismissedAs(finding({}), [dismissal])).toBe(dismissal);
11
- expect(dismissedAs(finding({ line: 42, issue: 'the cache key built from user id may collide' }), [dismissal])).toBe(dismissal);
12
- expect(dismissedAs(finding({ id: 'abcdef0123', issue: 'anything at all' }), [dismissal])).toBe(dismissal); // its own id
13
- });
14
- it('never hides a different bug, even on the same line and in the same class', () => {
15
- expect(dismissedAs(finding({ issue: 'cache entry never invalidated when the user changes' }), [dismissal])).toBeUndefined();
16
- expect(dismissedAs(finding({ line: 42, issue: 'cache entry never invalidated when user id changes' }), [dismissal])).toBeUndefined();
17
- expect(dismissedAs(finding({ line: 80 }), [dismissal])).toBeUndefined(); // far away
18
- expect(dismissedAs(finding({ class: 'production-cost' }), [dismissal])).toBeUndefined();
19
- expect(dismissedAs({ ...finding({}), kind: 'prior' }, [dismissal])).toBeUndefined(); // a human's point is never dismissed this way
20
- });
21
- });
22
- describe('what the judges are told the team knows', () => {
23
- it('shows verified lessons by default, candidates too with all, none when off, each with the words it came from', () => {
24
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'context-'));
25
- try {
26
- const lesson = { file: 'src/a.ts', symbols: ['save'], createdAt: '', updatedAt: '' };
27
- fs.mkdirSync(path.join(repo, '.rigour'));
28
- fs.writeFileSync(path.join(repo, '.rigour', 'review-lessons.json'), JSON.stringify({ version: 1, lessons: [
29
- { ...lesson, id: 'v', text: 'Make every save idempotent on retry.', state: 'verified', evidence: [{ pr: 3, comment: 'a', author: 'p', said: 'A retry here saves twice.' }, { pr: 4, comment: 'b', author: 'p' }] },
30
- { ...lesson, id: 'c', text: 'Log the save id on failure.', state: 'candidate', evidence: [{ pr: 5, comment: 'c', author: 'p' }] },
31
- ] }));
32
- const diff = 'diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n@@ -0,0 +1,1 @@\n+export const save = 1;\n';
33
- const told = (lessons) => buildContext({ cwd: repo, stateRoot: repo, dismissals: [], diff, router: undefined, lessons, previousPanel: undefined, touched: new Set(), docs: [], checks: [] }).text;
34
- expect(told()).toContain('src/a.ts: Make every save idempotent on retry. (in their words: "A retry here saves twice.") (acted on in PR #3, #4)');
35
- expect(told()).not.toContain('Log the save id');
36
- expect(told('all')).toContain('Log the save id on failure.');
37
- expect(told('off')).not.toContain('idempotent');
38
- fs.writeFileSync(path.join(repo, 'AGENTS.md'), '- Every call to `save` in `src/a.ts` must be idempotent on retry.\n');
39
- expect(told()).toMatch(/## Rules this repository wrote for itself[^\n]*\n- \[[0-9a-f]{10}\] \(AGENTS\.md, requirement\) Every call to `save` in `src\/a\.ts` must be idempotent on retry\./);
40
- }
41
- finally {
42
- fs.rmSync(repo, { recursive: true, force: true });
43
- }
44
- });
45
- });
@@ -1 +0,0 @@
1
- export {};