@rigour-labs/core 6.8.0 → 6.8.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,211 +0,0 @@
1
- /**
2
- * Staleness Detector Tests
3
- *
4
- * Tests for the staleness detection system.
5
- */
6
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
- import * as fs from 'fs/promises';
8
- import * as path from 'path';
9
- import * as os from 'os';
10
- import { StalenessDetector, checkCodeStaleness } from './staleness.js';
11
- describe('StalenessDetector', () => {
12
- let testDir;
13
- beforeEach(async () => {
14
- testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-staleness-'));
15
- });
16
- afterEach(async () => {
17
- await fs.rm(testDir, { recursive: true, force: true });
18
- });
19
- describe('React deprecations', () => {
20
- it('should detect componentWillMount', async () => {
21
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { react: '^18.0.0' } }));
22
- const detector = new StalenessDetector(testDir);
23
- const result = await detector.checkStaleness(`
24
- class MyComponent extends React.Component {
25
- componentWillMount() {
26
- console.log('mounting');
27
- }
28
- }
29
- `);
30
- expect(result.status).toBe('DEPRECATED');
31
- expect(result.issues.some(i => i.pattern.includes('componentWillMount'))).toBe(true);
32
- expect(result.issues[0].replacement).toContain('useEffect');
33
- });
34
- it('should detect ReactDOM.render', async () => {
35
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' } }));
36
- const detector = new StalenessDetector(testDir);
37
- const result = await detector.checkStaleness(`
38
- ReactDOM.render(<App />, document.getElementById('root'));
39
- `);
40
- expect(result.status).toBe('DEPRECATED');
41
- expect(result.issues.some(i => i.pattern.includes('ReactDOM.render'))).toBe(true);
42
- expect(result.issues[0].replacement).toContain('createRoot');
43
- });
44
- it('should not flag React deprecations if React is not installed', async () => {
45
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { express: '^4.0.0' } }));
46
- const detector = new StalenessDetector(testDir);
47
- const result = await detector.checkStaleness(`
48
- componentWillMount() {}
49
- `);
50
- // Should not flag React-specific patterns if React isn't a dependency
51
- expect(result.issues.filter(i => i.pattern.includes('componentWillMount'))).toHaveLength(0);
52
- });
53
- });
54
- describe('Package deprecations', () => {
55
- it('should detect moment.js usage', async () => {
56
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
57
- const detector = new StalenessDetector(testDir);
58
- const result = await detector.checkStaleness(`
59
- import moment from 'moment';
60
- const date = moment().format('YYYY-MM-DD');
61
- `);
62
- expect(result.status).toBe('STALE');
63
- expect(result.issues.some(i => i.replacement.includes('date-fns'))).toBe(true);
64
- });
65
- it('should detect request library usage', async () => {
66
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
67
- const detector = new StalenessDetector(testDir);
68
- const result = await detector.checkStaleness(`
69
- const request = require('request');
70
- request.get('https://example.com');
71
- `);
72
- expect(result.status).toBe('DEPRECATED');
73
- expect(result.issues.some(i => i.replacement.includes('fetch'))).toBe(true);
74
- });
75
- });
76
- describe('JavaScript deprecations', () => {
77
- it('should detect var keyword usage', async () => {
78
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
79
- const detector = new StalenessDetector(testDir);
80
- const result = await detector.checkStaleness(`
81
- var name = 'test';
82
- var count = 0;
83
- `);
84
- expect(result.issues.some(i => i.replacement.includes('const') || i.replacement.includes('let'))).toBe(true);
85
- });
86
- });
87
- describe('Redux deprecations', () => {
88
- it('should detect createStore usage in modern Redux projects', async () => {
89
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { redux: '^4.2.0' } }));
90
- const detector = new StalenessDetector(testDir);
91
- const result = await detector.checkStaleness(`
92
- import { createStore } from 'redux';
93
- const store = createStore(reducer);
94
- `);
95
- expect(result.status).toBe('STALE');
96
- expect(result.issues.some(i => i.replacement.includes('configureStore'))).toBe(true);
97
- });
98
- });
99
- describe('Node.js deprecations', () => {
100
- it('should detect Buffer constructor', async () => {
101
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
102
- const detector = new StalenessDetector(testDir);
103
- const result = await detector.checkStaleness(`
104
- const buf = new Buffer('hello');
105
- `);
106
- expect(result.status).toBe('DEPRECATED');
107
- expect(result.issues.some(i => i.replacement.includes('Buffer.from'))).toBe(true);
108
- });
109
- });
110
- describe('Next.js deprecations', () => {
111
- it('should detect getInitialProps in Next 13+ projects', async () => {
112
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { next: '^13.0.0' } }));
113
- const detector = new StalenessDetector(testDir);
114
- const result = await detector.checkStaleness(`
115
- MyPage.getInitialProps = async (ctx) => {
116
- return { data: {} };
117
- };
118
- `);
119
- expect(result.issues.some(i => i.pattern.includes('getInitialProps'))).toBe(true);
120
- });
121
- });
122
- describe('TypeScript best practices', () => {
123
- it('should warn about enum usage', async () => {
124
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
125
- const detector = new StalenessDetector(testDir);
126
- const result = await detector.checkStaleness(`
127
- enum Status {
128
- Active = 'active',
129
- Inactive = 'inactive'
130
- }
131
- `);
132
- expect(result.issues.some(i => i.severity === 'info' && i.pattern.includes('enum'))).toBe(true);
133
- });
134
- });
135
- describe('Project context', () => {
136
- it('should return project versions in context', async () => {
137
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({
138
- dependencies: {
139
- react: '^18.2.0',
140
- next: '^14.0.0'
141
- },
142
- devDependencies: {
143
- typescript: '^5.0.0'
144
- }
145
- }));
146
- const detector = new StalenessDetector(testDir);
147
- const result = await detector.checkStaleness('const x = 1;');
148
- expect(result.projectContext.react).toBeDefined();
149
- expect(result.projectContext.next).toBeDefined();
150
- expect(result.projectContext.typescript).toBeDefined();
151
- });
152
- });
153
- describe('Custom deprecations', () => {
154
- it('should allow adding custom deprecation rules', async () => {
155
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
156
- const detector = new StalenessDetector(testDir);
157
- detector.addDeprecation({
158
- pattern: 'legacyFunction',
159
- deprecatedIn: '1.0.0',
160
- replacement: 'newFunction()',
161
- severity: 'error',
162
- reason: 'Custom deprecation'
163
- });
164
- const result = await detector.checkStaleness(`
165
- legacyFunction();
166
- `);
167
- expect(result.issues.some(i => i.replacement === 'newFunction()')).toBe(true);
168
- });
169
- });
170
- describe('Overall status', () => {
171
- it('should return FRESH when no issues found', async () => {
172
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
173
- const detector = new StalenessDetector(testDir);
174
- const result = await detector.checkStaleness(`
175
- const name = 'test';
176
- const greet = () => console.log('Hello');
177
- `);
178
- expect(result.status).toBe('FRESH');
179
- });
180
- it('should return STALE for warnings', async () => {
181
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { redux: '^4.2.0' } }));
182
- const detector = new StalenessDetector(testDir);
183
- const result = await detector.checkStaleness(`
184
- createStore(reducer);
185
- `);
186
- expect(result.status).toBe('STALE');
187
- });
188
- it('should return DEPRECATED for errors', async () => {
189
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
190
- const detector = new StalenessDetector(testDir);
191
- const result = await detector.checkStaleness(`
192
- new Buffer('hello');
193
- `);
194
- expect(result.status).toBe('DEPRECATED');
195
- });
196
- });
197
- });
198
- describe('checkCodeStaleness', () => {
199
- let testDir;
200
- beforeEach(async () => {
201
- testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-staleness-'));
202
- await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
203
- });
204
- afterEach(async () => {
205
- await fs.rm(testDir, { recursive: true, force: true });
206
- });
207
- it('should be a quick helper for staleness checking', async () => {
208
- const result = await checkCodeStaleness(testDir, 'const x = 1;');
209
- expect(result.status).toBe('FRESH');
210
- });
211
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,32 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
- import { listResolvedFixes, openFindingCount, recordReviewOutcome } from './agent-fixes.js';
6
- const finding = (file) => ({ id: 'semantic-bugs', title: 't', details: 'd', files: [file], line: 2 });
7
- describe('recordReviewOutcome', () => {
8
- let cwd;
9
- beforeEach(() => {
10
- cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-fixes-'));
11
- fs.writeFileSync(path.join(cwd, 'a.ts'), 'before\n');
12
- });
13
- afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
14
- it('stores the before and after once a reviewed file no longer has the finding', () => {
15
- expect(recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts'])).toMatchObject({ opened: 1, resolved: 0 });
16
- fs.writeFileSync(path.join(cwd, 'a.ts'), 'after\n');
17
- expect(recordReviewOutcome(cwd, [], ['a.ts'])).toMatchObject({ opened: 0, resolved: 1 });
18
- const [fix] = listResolvedFixes(cwd);
19
- expect(fix).toMatchObject({ file: 'a.ts', rule: 'semantic-bugs', before: 'before\n', after: 'after\n' });
20
- expect(openFindingCount(cwd)).toBe(0);
21
- });
22
- it('keeps a finding open when its file was not part of the review (committed, not fixed)', () => {
23
- recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts']);
24
- expect(recordReviewOutcome(cwd, [], ['other.ts'])).toMatchObject({ opened: 0, resolved: 0 });
25
- expect(openFindingCount(cwd)).toBe(1);
26
- });
27
- it('does not count a finding that vanished without an edit as a fix', () => {
28
- recordReviewOutcome(cwd, [finding('a.ts')], ['a.ts']);
29
- expect(recordReviewOutcome(cwd, [], ['a.ts'])).toMatchObject({ opened: 0, resolved: 0 });
30
- expect(listResolvedFixes(cwd)).toEqual([]);
31
- });
32
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,136 +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 { ledgerProblems, loadLedger, LEDGER_PATH, LedgerSchema } from './backtest.js';
8
- import { mergedPrs, scaffoldLedger } from './backtest-init.js';
9
- let repo;
10
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
11
- const config = ConfigSchema.parse({ version: 1 });
12
- const REVIEWS = [
13
- { id: 1, user: { login: 'ci-bot', type: 'Bot' }, body: 'automated', state: 'COMMENTED', submitted_at: '2026-04-12T10:00:00Z', commit_id: 'c1c1c1c1c1' },
14
- { id: 2, user: { login: 'senior', type: 'User' }, body: '## Review\n\n- A retry posts the invoice twice.\n- **Quadratic** scan in `latestPayment`.\nlooks good otherwise', state: 'CHANGES_REQUESTED', submitted_at: '2026-04-12T12:00:00Z', commit_id: 'c2c2c2c2c2' },
15
- ];
16
- const INLINE = [
17
- { path: 'src/lib/server/invoices.ts', line: 40, body: 'The export reads every line item for ids.\n\nNarrow it.', created_at: '2026-04-12T12:00:00Z' },
18
- { path: 'src/checkout/total.ts', line: null, original_line: null, body: 'Drops the currency', created_at: '2026-04-12T12:00:00Z' },
19
- ];
20
- function fakeExec(calls) {
21
- return async (command, args, options) => {
22
- calls.push([command, ...args]);
23
- if (command === 'git') {
24
- try {
25
- return { exitCode: 0, stdout: execFileSync('git', args, { cwd: options.cwd, encoding: 'utf8', stdio: 'pipe' }), stderr: '' };
26
- }
27
- catch (error) {
28
- return { exitCode: error.status ?? 1, stdout: '', stderr: String(error.stderr ?? '') };
29
- }
30
- }
31
- if (args[0] === 'auth')
32
- return { exitCode: 0, stdout: 'token\n', stderr: '' };
33
- if (args[0] === 'pr')
34
- return { exitCode: 0, stdout: 'author\n', stderr: '' };
35
- if (args[1].endsWith('/reviews'))
36
- return { exitCode: 0, stdout: JSON.stringify(REVIEWS), stderr: '' };
37
- return { exitCode: 0, stdout: JSON.stringify(INLINE), stderr: '' };
38
- };
39
- }
40
- beforeEach(() => {
41
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'backtest-init-'));
42
- git('init', '-q', '-b', 'main');
43
- git('config', 'user.email', 't@example.com');
44
- git('config', 'user.name', 't');
45
- git('config', 'commit.gpgsign', 'false');
46
- fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
47
- git('add', '-A');
48
- execFileSync('git', ['-C', repo, 'commit', '-qm', 'main as of the review'], { env: { ...process.env, GIT_COMMITTER_DATE: '2026-04-12T09:00:00Z', GIT_AUTHOR_DATE: '2026-04-12T09:00:00Z' } });
49
- });
50
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
51
- describe('rigour backtest init', () => {
52
- it('takes the base from where the reviewed commit left main, not from main as of the review', async () => {
53
- const forked = git('rev-parse', 'HEAD');
54
- git('checkout', '-q', '-b', 'feature');
55
- fs.writeFileSync(path.join(repo, 'b.ts'), 'export const b = 1;\n');
56
- git('add', '-A');
57
- execFileSync('git', ['-C', repo, 'commit', '-qm', 'the branch'], { env: { ...process.env, GIT_COMMITTER_DATE: '2026-04-12T09:30:00Z', GIT_AUTHOR_DATE: '2026-04-12T09:30:00Z' } });
58
- const reviewed = git('rev-parse', 'HEAD');
59
- git('checkout', '-q', 'main');
60
- fs.writeFileSync(path.join(repo, 'c.ts'), 'export const c = 1;\n');
61
- git('add', '-A');
62
- execFileSync('git', ['-C', repo, 'commit', '-qm', 'main moves on before the review'], { env: { ...process.env, GIT_COMMITTER_DATE: '2026-04-12T11:00:00Z', GIT_AUTHOR_DATE: '2026-04-12T11:00:00Z' } });
63
- const realCommit = (calls) => async (command, args, options) => {
64
- const answer = await fakeExec(calls)(command, args, options);
65
- return command === 'gh' && args[1]?.endsWith('/reviews') ? { ...answer, stdout: JSON.stringify(REVIEWS.map(r => ({ ...r, commit_id: reviewed }))) } : answer;
66
- };
67
- const { rounds } = await scaffoldLedger(repo, 212, config, realCommit([]));
68
- expect(rounds[0].base).toBe(forked);
69
- });
70
- it('writes one round per human review: inline comments become file-and-window points, body bullets need a pattern', async () => {
71
- const calls = [];
72
- const mainAtReview = git('rev-parse', 'HEAD');
73
- const { rounds, incomplete } = await scaffoldLedger(repo, 212, config, fakeExec(calls));
74
- expect(rounds).toHaveLength(1);
75
- expect(rounds[0]).toMatchObject({ id: 'pr212-r1', commit: 'c2c2c2c2c2', base: mainAtReview, reviewed_at: '2026-04-12T12:00:00Z', pr: 212 });
76
- expect(rounds[0].points).toEqual([
77
- { id: 'R1-1', point: 'The export reads every line item for ids.', file: 'src/lib/server/invoices\\.ts', lines: [30, 50] },
78
- { id: 'R1-2', point: 'Drops the currency', file: 'src/checkout/total\\.ts', needs: 'a line window or a text pattern (the comment has no line)' },
79
- { id: 'R1-B1', point: 'A retry posts the invoice twice.', file: '', needs: 'a file pattern and a text pattern (this point was made in the review body, not on a line)' },
80
- { id: 'R1-B2', point: 'Quadratic scan in latestPayment.', file: '', needs: 'a file pattern and a text pattern (this point was made in the review body, not on a line)' },
81
- ]);
82
- expect(incomplete).toBe(3);
83
- expect(calls.find(c => c[0] === 'gh' && c[1] === 'pr')).toContain('212');
84
- const written = LedgerSchema.parse(JSON.parse(fs.readFileSync(path.join(repo, LEDGER_PATH), 'utf8')));
85
- expect(ledgerProblems(written)).toHaveLength(3);
86
- expect(() => loadLedger(repo)).toThrow('rows a person still has to complete');
87
- });
88
- it('keeps rounds already in the ledger and replaces its own', async () => {
89
- fs.mkdirSync(path.join(repo, '.rigour'));
90
- fs.writeFileSync(path.join(repo, LEDGER_PATH), JSON.stringify({ rounds: [
91
- { id: 'pr212-r1', commit: 'old0000', base: 'main', points: [], must_not_flag: [] },
92
- { id: 'pr7-r1', commit: 'other00', base: 'main', points: [{ id: 'P', point: 'p', file: 'x', text: 'x' }], must_not_flag: [] },
93
- ] }));
94
- await scaffoldLedger(repo, 212, config, fakeExec([]));
95
- const written = LedgerSchema.parse(JSON.parse(fs.readFileSync(path.join(repo, LEDGER_PATH), 'utf8')));
96
- expect(written.rounds.map(r => [r.id, r.commit])).toEqual([['pr7-r1', 'other00'], ['pr212-r1', 'c2c2c2c2c2']]);
97
- });
98
- });
99
- describe('the last N merged pull requests', () => {
100
- const run = async (pages) => {
101
- const limits = [];
102
- const exec = async (command, args, options) => {
103
- if (command === 'gh' && args[0] === 'auth')
104
- return { exitCode: 0, stdout: 'token\n', stderr: '' };
105
- if (command === 'gh' && args[0] === 'pr') {
106
- expect(args).toEqual(expect.arrayContaining(['--search', 'sort:updated-desc', '--json', 'number,mergedAt,updatedAt']));
107
- const limit = Number(args[args.indexOf('--limit') + 1]);
108
- limits.push(limit);
109
- return { exitCode: 0, stdout: JSON.stringify(pages(limit).slice(0, limit)), stderr: '' };
110
- }
111
- return fakeExec([])(command, args, options);
112
- };
113
- return { ...(await mergedPrs(repo, 2, config, exec)), limits };
114
- };
115
- const pr = (number, mergedAt, updatedAt = mergedAt) => ({ number, mergedAt: `2026-04-${mergedAt}T00:00:00Z`, updatedAt: `2026-04-${updatedAt}T00:00:00Z` });
116
- it('keeps the N most recently merged: a long-lived pull request merged last is in, one merged first is out', async () => {
117
- const { prs, incomplete, limits } = await run(() => [pr(3, '12'), pr(6, '10'), pr(5, '05'), pr(7, '01')]);
118
- expect(prs.map(p => p.number)).toEqual([3, 6]);
119
- expect(incomplete).toBe(false);
120
- expect(limits).toEqual([8]); // fewer listed than asked for: that is everything
121
- });
122
- it('lists more when old pull requests touched after merge crowd the window, until the result is provably complete', async () => {
123
- // Eight old pull requests a bot touched on the 20th, then the two real latest merges.
124
- const all = [...Array.from({ length: 8 }, (_, i) => pr(100 + i, '02', '20')), pr(3, '12'), pr(6, '10'), pr(5, '05')];
125
- const { prs, incomplete, limits } = await run(() => all);
126
- expect(limits).toEqual([8, 16]); // the first page held only crowding bots' touches: its oldest update (the 20th) is after the merges kept
127
- expect(prs.map(p => p.number)).toEqual([3, 6]);
128
- expect(incomplete).toBe(false);
129
- });
130
- it('stops at the cap and says the result may be incomplete', async () => {
131
- const crowd = Array.from({ length: 200 }, (_, i) => pr(1000 + i, '01', '28'));
132
- const { incomplete, limits } = await run(() => crowd);
133
- expect(limits).toEqual([8, 16, 32, 64]);
134
- expect(incomplete).toBe(true);
135
- });
136
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,32 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { formatJudges, judgedFrom } from './backtest-judges.js';
3
- const base = { outcome: 'findings', items: [], unverified: [], resolved: [], answerInReply: [], notes: [], advisory: [], disputed: [], dropped: [], dismissed: [], reviewers: ['claude', 'codex'], cached: false };
4
- describe('measuring each judge', () => {
5
- it('computes Cohen\'s kappa between two judges: 1 in full agreement, 0 at chance, and says when they share blind spots', () => {
6
- const round = (claude, codex) => ({ judges: { judges: ['claude', 'codex'], caught: { claude, codex }, points: ['p1', 'p2', 'p3', 'p4'], runs: 2 }, points: [] });
7
- expect(formatJudges([round(['p1', 'p3'], ['p1', 'p3'])])).toContain('kappa 1.00 over 4 point(s) (they share blind spots: a second judge adds little)');
8
- expect(formatJudges([round(['p1', 'p2'], ['p1', 'p3'])])).toContain('kappa 0.00 over 4 point(s)');
9
- expect(formatJudges([round(['p1'], ['p2'])])).toMatch(/kappa -0\.\d\d over 4/);
10
- });
11
- it('takes each judge\'s own item from the panel, including what it raised in its own words', () => {
12
- const lock = { id: 'a', kind: 'finding', class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock' };
13
- const judged = judgedFrom({ ...base, costUsd: 2, runs: 3, panel: [
14
- { item: lock, judges: ['claude', 'codex'], calls: { claude: 'raised', codex: 'raised' }, status: 'confirmed', members: [{ judge: 'claude', item: lock }, { judge: 'codex', item: { ...lock, id: 'b', line: 3, issue: 'lock taken late' } }] },
15
- ] });
16
- expect(judged).toEqual({ judges: ['claude', 'codex'], costUsd: 2, runs: 3, raised: [
17
- { judge: 'claude', file: 'src/job.ts', line: 2, text: 'returns before the lock' },
18
- { judge: 'codex', file: 'src/job.ts', line: 3, text: 'lock taken late' },
19
- ] });
20
- expect(judgedFrom({ ...base, reviewers: ['claude'] })).toBeUndefined();
21
- });
22
- it('reports each judge, what only it found, each pair\'s agreement and the cost of a caught point', () => {
23
- const round = (caught) => ({ judges: { judges: ['claude', 'codex'], caught, points: ['p1', 'p2', 'p3', 'p4'], runs: 3, costUsd: 3 }, points: ['p1', 'p2', 'p3', 'p4'].map(id => ({ id, caught: Object.values(caught).some(ids => ids.includes(id)) })) });
24
- const report = formatJudges([round({ claude: ['p1', 'p2'], codex: ['p1', 'p3'] }), round({ claude: ['p1'], codex: ['p1'] })]);
25
- expect(report).toContain('Judges (2 round(s), 8 human point(s))');
26
- expect(report).toContain('claude: raised 3/8; 1 that no other judge raised');
27
- expect(report).toContain('codex: raised 3/8; 1 that no other judge raised');
28
- expect(report).toMatch(/claude and codex: kappa 0\.\d\d over 8 point\(s\)/);
29
- expect(report).toContain('6 agent run(s), $6.00 recorded, $1.50 per caught point');
30
- expect(formatJudges([{ points: [] }])).toBe('');
31
- });
32
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,109 +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 { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH } from './backtest-last.js';
8
- import { roundsForPr } from './backtest-init.js';
9
- const config = ConfigSchema.parse({ version: 1 });
10
- const item = (file, line, blocking = true, gate = 'reviewer:claude') => ({ gate, file, line, text: `${file} scan has no upper bound`, blocking });
11
- const result = (round, items, caught = 0, costUsd) => ({ round, head: 'h', base: 'b', points: Array.from({ length: caught }, (_, i) => ({ id: `p${i}`, point: 'p', caught: true, noted: false })), falseBlocks: [], items, durationMs: 1000, ...(costUsd !== undefined ? { costUsd } : {}) });
12
- describe('the front door report', () => {
13
- const ledger = { rounds: [
14
- { id: 'pr7-r1', commit: 'c', base: 'b', reviewed_at: '2026-04-12T12:00:00Z', pr: 7, points: [{ id: 'R1-1', point: 'drops the currency', file: 'src/b\\.ts', lines: [1, 11] }], must_not_flag: [] },
15
- { id: 'pr7-r2', commit: 'c', base: 'b', reviewed_at: '2026-04-12T13:00:00Z', pr: 7, points: [{ id: 'R2-1', point: 'the export reads every line item', file: 'src/a\\.ts', lines: [1, 11] }], must_not_flag: [] },
16
- { id: 'pr7-r3', commit: 'c', base: 'b', reviewed_at: '2026-04-12T14:00:00Z', pr: 7, points: [{ id: 'R3-1', point: 'still unbounded', file: 'src/a\\.ts', lines: [1, 11] }], must_not_flag: [] },
17
- { id: 'pr7-approved', commit: 'c', base: 'b', reviewed_at: '2026-04-12T15:00:00Z', pr: 7, points: [], must_not_flag: [] },
18
- { id: 'pr8-approved', commit: 'c', base: 'b', reviewed_at: '2026-04-13T15:00:00Z', pr: 8, points: [], must_not_flag: [] },
19
- ] };
20
- it('counts points raised later that an earlier round blocked, with how many rounds earlier, and every block on an approved head', () => {
21
- const results = [result('pr7-r1', [item('src/a.ts', 3)]), result('pr7-r2', [item('src/a.ts', 3)], 1, 1.5), result('pr7-r3', [], 0, 0.5), result('pr7-approved', [item('src/a.ts', 3)]), result('pr8-approved', [item('src/z.ts', 9, false)])];
22
- const r = scoreLast(ledger, results);
23
- expect(r).toMatchObject({ prs: 2, rounds: 5, approvedHeads: 2, pointsLater: 2, sameRound: 1, costUsd: 2 });
24
- expect(r.early).toEqual([
25
- { pr: 7, round: 'pr7-r1', point: 'the export reads every line item', by: 'reviewer:claude src/a.ts:3', roundsEarlier: 1 },
26
- { pr: 7, round: 'pr7-r1', point: 'still unbounded', by: 'reviewer:claude src/a.ts:3', roundsEarlier: 2 }, // the earliest round that blocked it counts
27
- ]);
28
- expect(r.blocksOnApproved).toEqual([{ pr: 7, round: 'pr7-approved', item: item('src/a.ts', 3) }]); // an advisory item on pr8 is not a block
29
- const text = formatLast({ ...r, skipped: ['pr9-r1: commit abcdef012 is not reachable (force-pushed away)'] });
30
- expect(text.split('\n')[0]).toBe('1 blocking item(s) on 2 approved head(s): every one is a block the team would have overridden.'); // the worse number first
31
- expect(text).toContain('2 of 2 point(s) people raised in a later round were already blocked, 1.5 round(s) earlier on average.');
32
- expect(text).toContain('skipped pr9-r1: commit abcdef012');
33
- const clean = formatLast({ ...scoreLast(ledger, results.map(x => (x.round.endsWith('-approved') ? { ...x, items: [] } : x))), skipped: [] });
34
- expect(clean.split('\n')[0]).toContain('point(s) people raised in a later round were already blocked'); // nothing on approved heads: the catches lead
35
- expect(clean).toContain('0 blocking items on 2 approved head(s).');
36
- });
37
- });
38
- describe('rigour backtest --last', () => {
39
- let repo;
40
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
41
- const commit = (message, date) => { git('add', '-A'); execFileSync('git', ['-C', repo, 'commit', '-qm', message], { env: { ...process.env, GIT_COMMITTER_DATE: date, GIT_AUTHOR_DATE: date } }); return git('rev-parse', 'HEAD'); };
42
- let reviewed;
43
- const fake = (calls) => async (command, args, options) => {
44
- calls.push([command, ...args]);
45
- if (command === 'git') {
46
- if (args[0] === 'fetch')
47
- return { exitCode: 0, stdout: '', stderr: '' }; // the head is already here
48
- try {
49
- return { exitCode: 0, stdout: execFileSync('git', args, { cwd: options.cwd, encoding: 'utf8', stdio: 'pipe' }), stderr: '' };
50
- }
51
- catch (error) {
52
- return { exitCode: error.status ?? 1, stdout: '', stderr: String(error.stderr ?? '') };
53
- }
54
- }
55
- if (args[0] === 'auth')
56
- return { exitCode: 0, stdout: 'token\n', stderr: '' };
57
- if (args[0] === 'pr' && args[1] === 'list')
58
- return { exitCode: 0, stdout: JSON.stringify([{ number: 7, mergedAt: '2026-04-12T16:00:00Z' }, { number: 6, mergedAt: '2026-04-11T16:00:00Z' }]), stderr: '' };
59
- if (args[0] === 'pr')
60
- return { exitCode: 0, stdout: 'author\n', stderr: '' };
61
- const pr = /pulls\/(\d+)\//.exec(args[1])?.[1];
62
- if (args[1].endsWith('/reviews')) {
63
- if (pr === '6')
64
- return { exitCode: 0, stdout: JSON.stringify([{ id: 60, user: { login: 'author', type: 'User' }, body: 'self note', state: 'COMMENTED', submitted_at: '2026-04-11T12:00:00Z', commit_id: reviewed }]), stderr: '' };
65
- return { exitCode: 0, stdout: JSON.stringify([
66
- { id: 1, user: { login: 'senior', type: 'User' }, body: 'first pass', state: 'CHANGES_REQUESTED', submitted_at: '2026-04-12T12:00:00Z', commit_id: reviewed },
67
- { id: 2, user: { login: 'senior', type: 'User' }, body: 'second pass', state: 'CHANGES_REQUESTED', submitted_at: '2026-04-12T13:00:00Z', commit_id: reviewed },
68
- { id: 3, user: { login: 'senior', type: 'User' }, body: '', state: 'APPROVED', submitted_at: '2026-04-12T14:00:00Z', commit_id: reviewed },
69
- ]), stderr: '' };
70
- }
71
- if (args[1].includes('/reviews/1/'))
72
- return { exitCode: 0, stdout: JSON.stringify([{ path: 'b.ts', line: 1, body: 'Drops the currency', created_at: '2026-04-12T12:00:00Z' }]), stderr: '' };
73
- if (args[1].includes('/reviews/2/'))
74
- return { exitCode: 0, stdout: JSON.stringify([{ path: 'a.ts', line: 1, body: 'The export reads every line item.', created_at: '2026-04-12T13:00:00Z' }]), stderr: '' };
75
- return { exitCode: 0, stdout: '[]', stderr: '' };
76
- };
77
- beforeEach(() => {
78
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'backtest-last-'));
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
- fs.writeFileSync(path.join(repo, 'a.ts'), 'export const a = 1;\n');
84
- commit('main', '2026-04-12T09:00:00Z');
85
- git('checkout', '-q', '-b', 'feature');
86
- fs.writeFileSync(path.join(repo, 'b.ts'), 'export const b = 1;\n');
87
- reviewed = commit('the branch', '2026-04-12T09:30:00Z');
88
- });
89
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
90
- it('builds the ledger from the last merged pull requests, the approved head included, runs it and leads with the two numbers', async () => {
91
- const calls = [];
92
- const report = await backtestLast(repo, config, { last: 2, exec: fake(calls), collect: async () => ({ items: [item('a.ts', 1)], costUsd: 0.25 }) });
93
- expect(report).toMatchObject({ prs: 1, rounds: 3, approvedHeads: 1, pointsLater: 1, sameRound: 1, costUsd: 0.75 });
94
- expect(report.early).toEqual([{ pr: 7, round: 'pr7-r1', point: 'The export reads every line item.', by: 'reviewer:claude a.ts:1', roundsEarlier: 1 }]);
95
- expect(report.blocksOnApproved.map(b => b.round)).toEqual(['pr7-approved']);
96
- expect(report.skipped).toEqual(['pull request 6: pull request 6 has no review by a person yet']); // the author's own note is not a review
97
- expect(calls.filter(c => c[0] === 'git' && c[1] === 'fetch').map(c => c.at(-1))).toEqual(['pull/7/head', 'pull/6/head']);
98
- const written = JSON.parse(fs.readFileSync(path.join(repo, LAST_LEDGER_PATH), 'utf8'));
99
- expect(written.rounds.map(r => [r.id, r.points.length])).toEqual([['pr7-r1', 1], ['pr7-r2', 1], ['pr7-approved', 0]]);
100
- expect(formatLast(report).split('\n')[0]).toContain('1 blocking item(s) on 1 approved head(s)');
101
- });
102
- it('gives the scaffolder the approved head only when asked, with no points', async () => {
103
- const calls = [];
104
- const without = await roundsForPr(repo, 7, config, fake(calls));
105
- expect(without.approved).toBeUndefined();
106
- const withHead = await roundsForPr(repo, 7, config, fake(calls), { approvedHead: true });
107
- expect(withHead.approved).toMatchObject({ id: 'pr7-approved', commit: reviewed, reviewed_at: '2026-04-12T14:00:00Z', points: [] });
108
- });
109
- });
@@ -1 +0,0 @@
1
- export {};