@rigour-labs/core 6.7.10 → 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 (346) hide show
  1. package/dist/brief/briefing.d.ts +59 -0
  2. package/dist/brief/briefing.js +126 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +2 -0
  5. package/dist/inference/cloud-provider.js +12 -2
  6. package/dist/review/backtest-init.d.ts +8 -5
  7. package/dist/review/backtest-init.js +27 -9
  8. package/dist/review/backtest-last.js +3 -1
  9. package/dist/review/reviewer/adapters.d.ts +4 -0
  10. package/dist/review/reviewer/adapters.js +23 -0
  11. package/dist/review/reviewer/inputs.d.ts +4 -1
  12. package/dist/review/reviewer/inputs.js +36 -2
  13. package/dist/review/reviewer/record.d.ts +5 -0
  14. package/dist/review/reviewer/record.js +3 -3
  15. package/dist/review/reviewer/verdict.d.ts +14 -0
  16. package/dist/review/reviewer/verdict.js +46 -6
  17. package/dist/review/reviewer.d.ts +2 -0
  18. package/dist/review/reviewer.js +24 -5
  19. package/dist/review-learning/repo-rules.d.ts +14 -2
  20. package/dist/review-learning/repo-rules.js +59 -9
  21. package/dist/task/thread.d.ts +47 -0
  22. package/dist/task/thread.js +234 -0
  23. package/dist/templates/universal-config.js +4 -0
  24. package/dist/types/index.d.ts +21 -0
  25. package/dist/types/index.js +7 -0
  26. package/package.json +11 -8
  27. package/dist/context/automatic-index-cache.test.d.ts +0 -1
  28. package/dist/context/automatic-index-cache.test.js +0 -30
  29. package/dist/context/automatic-index.test.d.ts +0 -1
  30. package/dist/context/automatic-index.test.js +0 -45
  31. package/dist/context/cache-engine.test.d.ts +0 -1
  32. package/dist/context/cache-engine.test.js +0 -99
  33. package/dist/context/dependency-graph.test.d.ts +0 -1
  34. package/dist/context/dependency-graph.test.js +0 -13
  35. package/dist/context/index-status.test.d.ts +0 -1
  36. package/dist/context/index-status.test.js +0 -26
  37. package/dist/context.test.d.ts +0 -1
  38. package/dist/context.test.js +0 -228
  39. package/dist/deep/agent-review.test.d.ts +0 -1
  40. package/dist/deep/agent-review.test.js +0 -53
  41. package/dist/deep/code-context.test.d.ts +0 -1
  42. package/dist/deep/code-context.test.js +0 -48
  43. package/dist/deep/code-pass.test.d.ts +0 -1
  44. package/dist/deep/code-pass.test.js +0 -112
  45. package/dist/deep/code-review-prompt.test.d.ts +0 -1
  46. package/dist/deep/code-review-prompt.test.js +0 -20
  47. package/dist/deep/code-verifier.test.d.ts +0 -1
  48. package/dist/deep/code-verifier.test.js +0 -54
  49. package/dist/deep/diff-tests/calls.test.d.ts +0 -1
  50. package/dist/deep/diff-tests/calls.test.js +0 -33
  51. package/dist/deep/diff-tests/run.test.d.ts +0 -1
  52. package/dist/deep/diff-tests/run.test.js +0 -58
  53. package/dist/deep/fact-extractor.test.d.ts +0 -1
  54. package/dist/deep/fact-extractor.test.js +0 -581
  55. package/dist/deep/parse-findings.test.d.ts +0 -1
  56. package/dist/deep/parse-findings.test.js +0 -25
  57. package/dist/deep/pr-review.test.d.ts +0 -1
  58. package/dist/deep/pr-review.test.js +0 -80
  59. package/dist/deep/prompts.test.d.ts +0 -1
  60. package/dist/deep/prompts.test.js +0 -235
  61. package/dist/deep/reference-pack.test.d.ts +0 -1
  62. package/dist/deep/reference-pack.test.js +0 -46
  63. package/dist/deep/related-changes.test.d.ts +0 -1
  64. package/dist/deep/related-changes.test.js +0 -33
  65. package/dist/deep/review-context-export.test.d.ts +0 -1
  66. package/dist/deep/review-context-export.test.js +0 -32
  67. package/dist/deep/review-tools.test.d.ts +0 -1
  68. package/dist/deep/review-tools.test.js +0 -39
  69. package/dist/deep/risk.test.d.ts +0 -1
  70. package/dist/deep/risk.test.js +0 -101
  71. package/dist/deep/verifier.test.d.ts +0 -1
  72. package/dist/deep/verifier.test.js +0 -635
  73. package/dist/discovery.test.d.ts +0 -1
  74. package/dist/discovery.test.js +0 -93
  75. package/dist/environment.test.d.ts +0 -1
  76. package/dist/environment.test.js +0 -94
  77. package/dist/firewall/firewall.test.d.ts +0 -1
  78. package/dist/firewall/firewall.test.js +0 -117
  79. package/dist/firewall/trust-boundaries.test.d.ts +0 -1
  80. package/dist/firewall/trust-boundaries.test.js +0 -64
  81. package/dist/firewall/trusted-control.test.d.ts +0 -1
  82. package/dist/firewall/trusted-control.test.js +0 -188
  83. package/dist/gates/agent-team.test.d.ts +0 -1
  84. package/dist/gates/agent-team.test.js +0 -113
  85. package/dist/gates/ast.test.d.ts +0 -1
  86. package/dist/gates/ast.test.js +0 -112
  87. package/dist/gates/checkpoint.test.d.ts +0 -1
  88. package/dist/gates/checkpoint.test.js +0 -105
  89. package/dist/gates/content.test.d.ts +0 -1
  90. package/dist/gates/content.test.js +0 -73
  91. package/dist/gates/coverage.test.d.ts +0 -1
  92. package/dist/gates/coverage.test.js +0 -53
  93. package/dist/gates/dedupe-failures.test.d.ts +0 -1
  94. package/dist/gates/dedupe-failures.test.js +0 -12
  95. package/dist/gates/deep-analysis.test.d.ts +0 -1
  96. package/dist/gates/deep-analysis.test.js +0 -86
  97. package/dist/gates/deep-intent.test.d.ts +0 -1
  98. package/dist/gates/deep-intent.test.js +0 -51
  99. package/dist/gates/deep-timeout.test.d.ts +0 -1
  100. package/dist/gates/deep-timeout.test.js +0 -10
  101. package/dist/gates/deprecated-apis.test.d.ts +0 -1
  102. package/dist/gates/deprecated-apis.test.js +0 -318
  103. package/dist/gates/deprecated-dependencies.test.d.ts +0 -1
  104. package/dist/gates/deprecated-dependencies.test.js +0 -55
  105. package/dist/gates/frontend-secret-exposure.test.d.ts +0 -1
  106. package/dist/gates/frontend-secret-exposure.test.js +0 -148
  107. package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +0 -1
  108. package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +0 -27
  109. package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +0 -1
  110. package/dist/gates/hallucinated-imports/js-resolver-types.test.js +0 -14
  111. package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +0 -1
  112. package/dist/gates/hallucinated-imports-sveltekit.test.js +0 -132
  113. package/dist/gates/hallucinated-imports.test.d.ts +0 -1
  114. package/dist/gates/hallucinated-imports.test.js +0 -1206
  115. package/dist/gates/js-style-context.test.d.ts +0 -1
  116. package/dist/gates/js-style-context.test.js +0 -35
  117. package/dist/gates/logic-drift.test.d.ts +0 -1
  118. package/dist/gates/logic-drift.test.js +0 -52
  119. package/dist/gates/phantom-apis.test.d.ts +0 -1
  120. package/dist/gates/phantom-apis.test.js +0 -396
  121. package/dist/gates/promise-safety.test.d.ts +0 -1
  122. package/dist/gates/promise-safety.test.js +0 -34
  123. package/dist/gates/runner.test.d.ts +0 -1
  124. package/dist/gates/runner.test.js +0 -77
  125. package/dist/gates/scoped-gates.test.d.ts +0 -1
  126. package/dist/gates/scoped-gates.test.js +0 -52
  127. package/dist/gates/security-patterns-owasp.test.d.ts +0 -1
  128. package/dist/gates/security-patterns-owasp.test.js +0 -186
  129. package/dist/gates/security-patterns.test.d.ts +0 -1
  130. package/dist/gates/security-patterns.test.js +0 -194
  131. package/dist/gates/semantic-bugs.test.d.ts +0 -1
  132. package/dist/gates/semantic-bugs.test.js +0 -76
  133. package/dist/gates/side-effect-analysis.test.d.ts +0 -1
  134. package/dist/gates/side-effect-analysis.test.js +0 -162
  135. package/dist/gates/style-drift.test.d.ts +0 -1
  136. package/dist/gates/style-drift.test.js +0 -26
  137. package/dist/gates/test-quality.test.d.ts +0 -1
  138. package/dist/gates/test-quality.test.js +0 -325
  139. package/dist/gates/trusted-reviews.test.d.ts +0 -1
  140. package/dist/gates/trusted-reviews.test.js +0 -16
  141. package/dist/gates/unindexed-reads/queries.test.d.ts +0 -1
  142. package/dist/gates/unindexed-reads/queries.test.js +0 -54
  143. package/dist/gates/unindexed-reads/schema.test.d.ts +0 -1
  144. package/dist/gates/unindexed-reads/schema.test.js +0 -81
  145. package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +0 -1
  146. package/dist/gates/unindexed-reads/unindexed-reads.test.js +0 -90
  147. package/dist/hooks/checker.test.d.ts +0 -1
  148. package/dist/hooks/checker.test.js +0 -159
  149. package/dist/hooks/dlp-confidence.test.d.ts +0 -1
  150. package/dist/hooks/dlp-confidence.test.js +0 -51
  151. package/dist/hooks/dlp-feedback.test.d.ts +0 -1
  152. package/dist/hooks/dlp-feedback.test.js +0 -131
  153. package/dist/hooks/input-validator.test.d.ts +0 -1
  154. package/dist/hooks/input-validator.test.js +0 -329
  155. package/dist/hooks/templates.test.d.ts +0 -1
  156. package/dist/hooks/templates.test.js +0 -27
  157. package/dist/inference/brain-placeholder.test.d.ts +0 -1
  158. package/dist/inference/brain-placeholder.test.js +0 -28
  159. package/dist/inference/cloud-provider.test.d.ts +0 -1
  160. package/dist/inference/cloud-provider.test.js +0 -139
  161. package/dist/inference/executable.test.d.ts +0 -1
  162. package/dist/inference/executable.test.js +0 -41
  163. package/dist/inference/http-download.test.d.ts +0 -1
  164. package/dist/inference/http-download.test.js +0 -109
  165. package/dist/inference/llama-engine-checksum.test.d.ts +0 -1
  166. package/dist/inference/llama-engine-checksum.test.js +0 -27
  167. package/dist/inference/llama-engine.test.d.ts +0 -1
  168. package/dist/inference/llama-engine.test.js +0 -51
  169. package/dist/inference/llama-process.test.d.ts +0 -1
  170. package/dist/inference/llama-process.test.js +0 -61
  171. package/dist/inference/local-model.test.d.ts +0 -1
  172. package/dist/inference/local-model.test.js +0 -23
  173. package/dist/inference/model-download.test.d.ts +0 -1
  174. package/dist/inference/model-download.test.js +0 -125
  175. package/dist/inference/model-manager.test.d.ts +0 -1
  176. package/dist/inference/model-manager.test.js +0 -24
  177. package/dist/inference/types.test.d.ts +0 -1
  178. package/dist/inference/types.test.js +0 -19
  179. package/dist/memory/recall.test.d.ts +0 -1
  180. package/dist/memory/recall.test.js +0 -36
  181. package/dist/pattern-index/indexer.test.d.ts +0 -6
  182. package/dist/pattern-index/indexer.test.js +0 -197
  183. package/dist/pattern-index/matcher.test.d.ts +0 -6
  184. package/dist/pattern-index/matcher.test.js +0 -238
  185. package/dist/pattern-index/pattern-reuse.test.d.ts +0 -1
  186. package/dist/pattern-index/pattern-reuse.test.js +0 -76
  187. package/dist/pattern-index/semantic-runtime.test.d.ts +0 -1
  188. package/dist/pattern-index/semantic-runtime.test.js +0 -31
  189. package/dist/pattern-index/staleness.test.d.ts +0 -6
  190. package/dist/pattern-index/staleness.test.js +0 -211
  191. package/dist/review/agent-fixes.test.d.ts +0 -1
  192. package/dist/review/agent-fixes.test.js +0 -32
  193. package/dist/review/backtest-init.test.d.ts +0 -1
  194. package/dist/review/backtest-init.test.js +0 -98
  195. package/dist/review/backtest-judges.test.d.ts +0 -1
  196. package/dist/review/backtest-judges.test.js +0 -32
  197. package/dist/review/backtest-last.test.d.ts +0 -1
  198. package/dist/review/backtest-last.test.js +0 -109
  199. package/dist/review/backtest.test.d.ts +0 -1
  200. package/dist/review/backtest.test.js +0 -183
  201. package/dist/review/baseline.test.d.ts +0 -1
  202. package/dist/review/baseline.test.js +0 -22
  203. package/dist/review/branch-checks.test.d.ts +0 -1
  204. package/dist/review/branch-checks.test.js +0 -48
  205. package/dist/review/check-outcomes.test.d.ts +0 -1
  206. package/dist/review/check-outcomes.test.js +0 -56
  207. package/dist/review/code-patterns.test.d.ts +0 -1
  208. package/dist/review/code-patterns.test.js +0 -142
  209. package/dist/review/dead-code.test.d.ts +0 -1
  210. package/dist/review/dead-code.test.js +0 -154
  211. package/dist/review/deep-runs.test.d.ts +0 -1
  212. package/dist/review/deep-runs.test.js +0 -23
  213. package/dist/review/effectiveness.test.d.ts +0 -1
  214. package/dist/review/effectiveness.test.js +0 -42
  215. package/dist/review/fix-scope.test.d.ts +0 -1
  216. package/dist/review/fix-scope.test.js +0 -104
  217. package/dist/review/generated-files.test.d.ts +0 -1
  218. package/dist/review/generated-files.test.js +0 -25
  219. package/dist/review/migration-order.test.d.ts +0 -1
  220. package/dist/review/migration-order.test.js +0 -62
  221. package/dist/review/quiet.test.d.ts +0 -1
  222. package/dist/review/quiet.test.js +0 -38
  223. package/dist/review/receipt.test.d.ts +0 -1
  224. package/dist/review/receipt.test.js +0 -60
  225. package/dist/review/review-task.test.d.ts +0 -1
  226. package/dist/review/review-task.test.js +0 -89
  227. package/dist/review/review.test.d.ts +0 -1
  228. package/dist/review/review.test.js +0 -203
  229. package/dist/review/reviewer/adapters.test.d.ts +0 -1
  230. package/dist/review/reviewer/adapters.test.js +0 -60
  231. package/dist/review/reviewer/api-judge.test.d.ts +0 -1
  232. package/dist/review/reviewer/api-judge.test.js +0 -108
  233. package/dist/review/reviewer/background.test.d.ts +0 -1
  234. package/dist/review/reviewer/background.test.js +0 -118
  235. package/dist/review/reviewer/context.test.d.ts +0 -1
  236. package/dist/review/reviewer/context.test.js +0 -45
  237. package/dist/review/reviewer/exec.test.d.ts +0 -1
  238. package/dist/review/reviewer/exec.test.js +0 -59
  239. package/dist/review/reviewer/inputs.test.d.ts +0 -1
  240. package/dist/review/reviewer/inputs.test.js +0 -26
  241. package/dist/review/reviewer/panel.test.d.ts +0 -1
  242. package/dist/review/reviewer/panel.test.js +0 -110
  243. package/dist/review/reviewer/record.test.d.ts +0 -1
  244. package/dist/review/reviewer/record.test.js +0 -32
  245. package/dist/review/reviewer/rule-writer.test.d.ts +0 -1
  246. package/dist/review/reviewer/rule-writer.test.js +0 -52
  247. package/dist/review/reviewer/settings.test.d.ts +0 -1
  248. package/dist/review/reviewer/settings.test.js +0 -57
  249. package/dist/review/reviewer/usage.test.d.ts +0 -1
  250. package/dist/review/reviewer/usage.test.js +0 -14
  251. package/dist/review/reviewer.test.d.ts +0 -1
  252. package/dist/review/reviewer.test.js +0 -705
  253. package/dist/review/stories.test.d.ts +0 -1
  254. package/dist/review/stories.test.js +0 -58
  255. package/dist/review/toolchain.test.d.ts +0 -1
  256. package/dist/review/toolchain.test.js +0 -108
  257. package/dist/review/typed/redundancy.test.d.ts +0 -1
  258. package/dist/review/typed/redundancy.test.js +0 -315
  259. package/dist/review/typed/schema-nullability.test.d.ts +0 -1
  260. package/dist/review/typed/schema-nullability.test.js +0 -63
  261. package/dist/review-learning/human-edits.test.d.ts +0 -1
  262. package/dist/review-learning/human-edits.test.js +0 -47
  263. package/dist/review-learning/repo-rules.test.d.ts +0 -1
  264. package/dist/review-learning/repo-rules.test.js +0 -55
  265. package/dist/review-learning/review-learning.test.d.ts +0 -1
  266. package/dist/review-learning/review-learning.test.js +0 -249
  267. package/dist/safety.test.d.ts +0 -1
  268. package/dist/safety.test.js +0 -42
  269. package/dist/semantic/benchmark.test.d.ts +0 -1
  270. package/dist/semantic/benchmark.test.js +0 -22
  271. package/dist/semantic/intent/intent.test.d.ts +0 -1
  272. package/dist/semantic/intent/intent.test.js +0 -46
  273. package/dist/semantic/learn/learn.test.d.ts +0 -1
  274. package/dist/semantic/learn/learn.test.js +0 -67
  275. package/dist/semantic/origins.test.d.ts +0 -1
  276. package/dist/semantic/origins.test.js +0 -77
  277. package/dist/semantic/project-facts.test.d.ts +0 -1
  278. package/dist/semantic/project-facts.test.js +0 -45
  279. package/dist/semantic/sites/call-sites.test.d.ts +0 -1
  280. package/dist/semantic/sites/call-sites.test.js +0 -46
  281. package/dist/services/adaptive-thresholds.test.d.ts +0 -1
  282. package/dist/services/adaptive-thresholds.test.js +0 -53
  283. package/dist/services/agent-history.test.d.ts +0 -1
  284. package/dist/services/agent-history.test.js +0 -69
  285. package/dist/services/context-scope-summary.test.d.ts +0 -1
  286. package/dist/services/context-scope-summary.test.js +0 -17
  287. package/dist/services/context-telemetry-service.test.d.ts +0 -1
  288. package/dist/services/context-telemetry-service.test.js +0 -182
  289. package/dist/services/cursor-usage-sync.test.d.ts +0 -1
  290. package/dist/services/cursor-usage-sync.test.js +0 -173
  291. package/dist/services/engineering-knowledge-graph.test.d.ts +0 -1
  292. package/dist/services/engineering-knowledge-graph.test.js +0 -76
  293. package/dist/services/model-pricing.test.d.ts +0 -1
  294. package/dist/services/model-pricing.test.js +0 -44
  295. package/dist/services/observed-savings.test.d.ts +0 -1
  296. package/dist/services/observed-savings.test.js +0 -37
  297. package/dist/services/score-history.test.d.ts +0 -1
  298. package/dist/services/score-history.test.js +0 -61
  299. package/dist/smoke.test.d.ts +0 -1
  300. package/dist/smoke.test.js +0 -17
  301. package/dist/storage/cache-cleanup.test.d.ts +0 -1
  302. package/dist/storage/cache-cleanup.test.js +0 -54
  303. package/dist/storage/context-telemetry.test.d.ts +0 -1
  304. package/dist/storage/context-telemetry.test.js +0 -80
  305. package/dist/storage/db.test.d.ts +0 -1
  306. package/dist/storage/db.test.js +0 -46
  307. package/dist/storage/fix-lessons.test.d.ts +0 -1
  308. package/dist/storage/fix-lessons.test.js +0 -61
  309. package/dist/storage/lessons.test.d.ts +0 -1
  310. package/dist/storage/lessons.test.js +0 -81
  311. package/dist/storage/local-encryption.test.d.ts +0 -1
  312. package/dist/storage/local-encryption.test.js +0 -34
  313. package/dist/storage/local-memory.test.d.ts +0 -1
  314. package/dist/storage/local-memory.test.js +0 -55
  315. package/dist/storage/share-memory.test.d.ts +0 -1
  316. package/dist/storage/share-memory.test.js +0 -34
  317. package/dist/storage/team-diagnostics.test.d.ts +0 -1
  318. package/dist/storage/team-diagnostics.test.js +0 -51
  319. package/dist/storage/team-scope.test.d.ts +0 -1
  320. package/dist/storage/team-scope.test.js +0 -22
  321. package/dist/storage/team-store.test.d.ts +0 -1
  322. package/dist/storage/team-store.test.js +0 -11
  323. package/dist/storage/team-sync-scope.test.d.ts +0 -1
  324. package/dist/storage/team-sync-scope.test.js +0 -100
  325. package/dist/storage/team-vector-store.test.d.ts +0 -1
  326. package/dist/storage/team-vector-store.test.js +0 -56
  327. package/dist/storage/telemetry-scope.test.d.ts +0 -1
  328. package/dist/storage/telemetry-scope.test.js +0 -38
  329. package/dist/telemetry/telemetry.test.d.ts +0 -1
  330. package/dist/telemetry/telemetry.test.js +0 -64
  331. package/dist/types/index.test.d.ts +0 -1
  332. package/dist/types/index.test.js +0 -33
  333. package/dist/utils/command-line.test.d.ts +0 -1
  334. package/dist/utils/command-line.test.js +0 -8
  335. package/dist/utils/diff-removed.test.d.ts +0 -1
  336. package/dist/utils/diff-removed.test.js +0 -29
  337. package/dist/utils/diff.test.d.ts +0 -1
  338. package/dist/utils/diff.test.js +0 -38
  339. package/dist/utils/glob-paths.test.d.ts +0 -1
  340. package/dist/utils/glob-paths.test.js +0 -40
  341. package/dist/utils/profile.test.d.ts +0 -1
  342. package/dist/utils/profile.test.js +0 -68
  343. package/dist/utils/scanner.test.d.ts +0 -1
  344. package/dist/utils/scanner.test.js +0 -48
  345. package/dist/utils/scope.test.d.ts +0 -1
  346. package/dist/utils/scope.test.js +0 -38
@@ -1,55 +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 { rulesForDiff, rulesSection, splitRules } from './repo-rules.js';
6
- const AGENTS = `# Conventions
7
-
8
- @SHARED.md
9
-
10
- - **Migrations are append-only.** Add new files under \`migrations/\`; never edit applied ones.
11
- - Every outbound send goes through \`deliverOrder()\` in \`src/lib/delivery.ts\`, which checks the permit.
12
- - Keep components small.
13
-
14
- Prefer \`fetchWithTimeout\` over a bare call when talking to partner APIs; a hung request blocks the queue.
15
- `;
16
- let repo;
17
- beforeEach(() => {
18
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'repo-rules-'));
19
- fs.writeFileSync(path.join(repo, 'AGENTS.md'), AGENTS);
20
- fs.mkdirSync(path.join(repo, '.cursor/rules'), { recursive: true });
21
- fs.writeFileSync(path.join(repo, '.cursor/rules/ui.mdc'), 'Never set `fullWidth` on `<Button>`; use `maxWidth` caps instead of stretching.\n');
22
- });
23
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
24
- const diff = (file, added) => `+++ b/${file}\n@@ -1,0 +1,1 @@\n+${added}\n`;
25
- describe('repository rules', () => {
26
- it('splits a rules file into rules with the paths and specific identifiers they name', () => {
27
- const rules = splitRules('AGENTS.md', AGENTS);
28
- expect(rules.map(r => r.text.slice(0, 30))).toEqual(['**Migrations are append-only.*', 'Every outbound send goes throu', 'Prefer `fetchWithTimeout` over']);
29
- expect(rules[1]).toMatchObject({ paths: ['src/lib/delivery.ts'], symbols: ['deliverOrder'] });
30
- expect(rules[0].paths).toEqual(['migrations/']);
31
- expect(rules.map(r => r.requirement)).toEqual([true, true, false]); // "never", "every"; "prefer" is guidance
32
- // A section that only describes an exception, with no imperative, is guidance; one that ends in an imperative is a requirement.
33
- const [exceptionOnly, withImperative] = splitRules('AGENTS.md', '- **One narrow exception:** the queue table is still literally named `study_jobs`, not renamed with the feature.\n\n- **One narrow exception:** the queue table is still literally named `study_jobs`. Import the `JOBS_TABLE` constant; do not inline the raw table name again.\n');
34
- expect([exceptionOnly.requirement, withImperative.requirement]).toEqual([false, true]);
35
- expect(rules[0].id).toMatch(/^[0-9a-f]{10}$/);
36
- expect(splitRules('AGENTS.md', AGENTS)[0].id).toBe(rules[0].id); // stable across runs
37
- });
38
- it('keeps a rule\'s "Why" and "How to apply" paragraphs with it, and ranks a rule naming the change above one that only shares its words', () => {
39
- const text = '- **Use the design system.** Every control comes from `src/lib/ui`.\n\n**Why:** one source of styling.\n\n**How to apply:** import from `$lib/ui`, never a raw `<button>`.\n\n- Bound both ends of every time window a scheduled job reads.\n\n- Name the index a new query relies on in the migrations.\n';
40
- const rules = splitRules('AGENTS.md', text);
41
- expect(rules.map(r => r.text.slice(0, 24))).toEqual(['**Use the design system.', 'Bound both ends of every', 'Name the index a new que']);
42
- expect(rules[0].text).toContain('**How to apply:**');
43
- fs.writeFileSync(path.join(repo, 'AGENTS.md'), text);
44
- const change = diff('src/lib/ui/Button.svelte', 'const timeWindow = scheduledJob.readsRows();');
45
- expect(rulesForDiff(repo, change, true, 10).map(r => r.text.slice(0, 12))).toEqual(['**Use the de', 'Bound both e']); // the path hit first, then shared words; the index rule shares nothing
46
- expect(rulesForDiff(repo, change, true, 1)).toHaveLength(1);
47
- });
48
- it('shows only the rules that name what the change touches, and nothing when disabled', () => {
49
- expect(rulesForDiff(repo, diff('migrations/2026_add.sql', 'alter table x;'), true).map(r => r.text.slice(0, 20))).toEqual(['**Migrations are app']);
50
- expect(rulesForDiff(repo, diff('src/jobs/send.ts', 'await deliverOrder(order);'), true).map(r => r.text.slice(0, 20))).toEqual(['Every outbound send ']);
51
- expect(rulesForDiff(repo, diff('src/ui/List.tsx', 'const x = 1;'), true)).toEqual([]);
52
- expect(rulesForDiff(repo, diff('migrations/2026_add.sql', 'x'), false)).toEqual([]);
53
- expect(rulesSection(rulesForDiff(repo, diff('src/a.ts', 'await fetchWithTimeout(url);'), true))).toContain('[AGENTS.md] Prefer `fetchWithTimeout`');
54
- });
55
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,249 +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 { gitIn, withActedOn } from './acted-on.js';
7
- import { learnFromReviews } from './learn-from-reviews.js';
8
- import { outcomeFor } from './outcomes.js';
9
- import { rulesFromReviews } from './rules-from-reviews.js';
10
- import { describeLesson, lessonView } from './team-lessons.js';
11
- import { decideLesson, isSpecific, lessonState, lessonText, lessonsFromReview, matchLessons, mergeLessons, readLessons, writeLessons } from './lessons.js';
12
- import { lessonsForDiff, lessonsSection } from './team-lessons.js';
13
- let repo;
14
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
15
- const write = (body) => fs.writeFileSync(path.join(repo, 'src/orders.ts'), body);
16
- const commit = (message) => { git('add', '-A'); git('commit', '-qm', message); return git('rev-parse', 'HEAD'); };
17
- const V1 = 'export async function save(db, order) {\n await db.insert(order);\n return order.id;\n}\n';
18
- const V2 = 'export async function save(db, order) {\n await db.upsert(order, { onConflict: "id" });\n return order.id;\n}\n';
19
- beforeEach(() => {
20
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'review-learning-'));
21
- git('init', '-q', '-b', 'main');
22
- git('config', 'user.email', 't@example.com');
23
- git('config', 'user.name', 't');
24
- git('config', 'commit.gpgsign', 'false');
25
- fs.mkdirSync(path.join(repo, 'src'));
26
- });
27
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
28
- const comment = (id, sha, start, body, prNumber = 7) => ({ id, prNumber, path: 'src/orders.ts', start, end: start, commit: sha, body, author: 'reviewer' });
29
- describe('acted on', () => {
30
- it('marks a comment whose lines changed before merge, and one nobody acted on, keeping both', () => {
31
- write(V1);
32
- const reviewed = commit('pr head');
33
- write(V2);
34
- const merged = commit('address review');
35
- const marked = withActedOn(gitIn(repo), { number: 7, mergeSha: merged, mergedAt: '', comments: [
36
- comment('a', reviewed, 2, '**Use an upsert keyed on `id` so a retry does not duplicate the order.**'),
37
- comment('b', reviewed, 3, 'Nit: return the whole order?'),
38
- ], reviews: [] });
39
- expect(marked.map(c => [c.id, c.actedOn])).toEqual([['a', true], ['b', false]]);
40
- });
41
- });
42
- describe('lessons', () => {
43
- it('never makes a lesson of a bare path or a review bot\'s line-range scaffolding', () => {
44
- expect(lessonText('src/routes/learner/+layout.ts')).toBe('');
45
- expect(lessonText('`src/routes/learner/api/[planId]/+server.ts`')).toBe('');
46
- expect(lessonText('In src/a.ts around lines 10-12: Check the lock before the first read.')).toBe('Check the lock before the first read.');
47
- expect(lessonText('Around line 124-141: Add a test for the failed save.')).toBe('Add a test for the failed save.');
48
- expect(lessonsFromReview({ id: '1', prNumber: 3, commit: 'c', author: 'rabbit[bot]', source: 'bot', body: '- `src/a/b.ts`\n- Bound both ends of the time window.' }, ['src/a/b.ts']).map(l => l.text))
49
- .toEqual(['Bound both ends of the time window.']);
50
- });
51
- it('keeps the point of a review comment and drops tool output and markup', () => {
52
- const body = '_⚠️ Potential issue_ | _🟠 Major_\n\n**Use an upsert keyed on `id`.**\n\n<details>\n<summary>🏁 Script executed</summary>\nrg -n insert\n</details>';
53
- expect(lessonText(body)).toBe('Use an upsert keyed on `id`.');
54
- expect(lessonText('Callers retry on timeout, so this insert duplicates orders. Please use upsert.')).toBe('Callers retry on timeout, so this insert duplicates orders.');
55
- expect(lessonText('### Medium Severity\n\nThe retry loop never resets `attempt`, so a second failure gives up at once.')).toBe('The retry loop never resets `attempt`, so a second failure gives up at once.');
56
- });
57
- it('makes a candidate a lesson only on evidence, and keeps every piece of it', () => {
58
- const point = (pr, prAuthor, source = 'person', reviewer = `reviewer-of-${pr}`) => ({ kind: 'point', pr, comment: `c${pr}${prAuthor}`, author: reviewer, source, prAuthor });
59
- const lesson = (...evidence) => ({ id: 'x', text: 'Use upsert', file: 'src/orders.ts', symbols: [], state: 'candidate', evidence, createdAt: '', updatedAt: '' });
60
- expect(lessonState(lesson(point(1, 'ann')))).toEqual({ state: 'candidate' });
61
- expect(lessonState(lesson(point(1, 'ann'), point(2, 'ann')))).toEqual({ state: 'candidate' }); // two PRs, one author: weak, not enough
62
- expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob', 'bot')))).toEqual({ state: 'verified', promotedBy: 'recurrence' }); // a bot's point counts the same
63
- const said = (e, text) => ({ ...e, text });
64
- expect(lessonState(lesson(said(point(1, 'ann', 'bot', 'helper[bot]'), 'Around line 60-103: update the callers.'), said(point(2, 'bob', 'bot', 'helper[bot]'), 'Around line 12-14: update the loader.')))).toEqual({ state: 'candidate' }); // a bot rewording its own point is one source
65
- expect(lessonState(lesson(said(point(1, 'ann', 'bot', 'helper[bot]'), 'Consider more tests.'), said(point(2, 'bob', 'bot', 'helper[bot]'), 'Consider more tests.')))).toEqual({ state: 'candidate' }); // one bot's template on every PR is one source
66
- expect(lessonState(lesson(said(point(1, 'ann', 'person', 'lead'), 'Regenerate the API client after changing the schema.'), said(point(2, 'bob', 'person', 'lead'), 'The generated client is stale again.')))).toEqual({ state: 'verified', promotedBy: 'recurrence' }); // a senior re-raising it in their own words
67
- expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob'), { kind: 'norule', pr: 1, comment: 'n', author: '' }))).toEqual({ state: 'candidate' }); // no rule in it: never promoted again
68
- const outcome = { kind: 'outcome', pr: 1, comment: 'o', author: '', detail: 'fixed later by abc' };
69
- const counter = { kind: 'counter', pr: 1, comment: 'k', author: '', detail: 'unchanged 40 days' };
70
- expect(lessonState(lesson(point(1, 'ann', 'bot'), outcome))).toEqual({ state: 'verified', promotedBy: 'outcome' });
71
- expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob'), counter))).toEqual({ state: 'candidate' }); // counter-evidence holds recurrence back
72
- const accepted = { kind: 'accepted', pr: 1, comment: 'y', author: 'lead@x' };
73
- const rejected = { kind: 'rejected', pr: 1, comment: 'n', author: 'lead@x' };
74
- expect(lessonState(lesson(point(1, 'ann'), outcome, rejected))).toEqual({ state: 'rejected' }); // a person's decision wins
75
- expect(lessonState(lesson(point(1, 'ann'), rejected, accepted))).toEqual({ state: 'verified', promotedBy: 'person' }); // the latest decision
76
- expect(lessonState({ ...lesson({ pr: 1, comment: 'old', author: 'r' }), state: 'verified' })).toEqual({ state: 'verified', promotedBy: 'legacy' }); // a record from before evidence keeps its state
77
- });
78
- it('records a person accepting or rejecting a lesson as evidence, with who and why', () => {
79
- writeLessons(repo, [{ id: 'x', text: 'Use upsert', file: 'src/orders.ts', symbols: [], state: 'candidate', evidence: [{ kind: 'point', pr: 1, comment: 'c', author: 'r' }], createdAt: '', updatedAt: '' }]);
80
- expect(decideLesson(repo, 'x', 'rejected', 'lead@x', 'we accept duplicates here')).toMatchObject({ state: 'rejected' });
81
- expect(readLessons(repo)[0].evidence.at(-1)).toMatchObject({ kind: 'rejected', author: 'lead@x', detail: 'we accept duplicates here' });
82
- expect(matchLessons(readLessons(repo), { files: ['src/orders.ts'], symbols: new Set() }, { includeCandidates: true })).toEqual([]); // never served as a lesson
83
- expect(decideLesson(repo, 'nope', 'accepted', 'lead@x')).toBeUndefined();
84
- });
85
- it('matches lessons by file, symbols and directory, verified only unless asked', () => {
86
- const base = { symbols: ['insert'], evidence: [{ pr: 1, comment: 'c', author: 'r' }], createdAt: '', updatedAt: '' };
87
- const lessons = [
88
- { ...base, id: '1', text: 'same file', file: 'src/orders.ts', state: 'verified' },
89
- { ...base, id: '2', text: 'other dir, two shared symbols', file: 'lib/x.ts', state: 'verified', symbols: ['insertOrder', 'orderId'] },
90
- { ...base, id: '6', text: 'a lesson about a runbook', file: 'docs/runbook.md', state: 'verified', symbols: ['insertOrder', 'orderId'] },
91
- { ...base, id: '5', text: 'other dir, one generic shared symbol', file: 'lib/z.ts', state: 'verified' },
92
- { ...base, id: '3', text: 'candidate', file: 'src/orders.ts', state: 'candidate' },
93
- { ...base, id: '4', text: 'unrelated', file: 'lib/y.ts', state: 'verified', symbols: ['other'] },
94
- ];
95
- const change = { files: ['src/orders.ts'], symbols: new Set(['insert', 'insertOrder', 'orderId']) };
96
- expect(matchLessons(lessons, change).map(l => l.id)).toEqual(['2', '1']); // two specific shared names outrank a same-file lesson with only generic ones
97
- expect(matchLessons(lessons, change, { includeCandidates: true }).map(l => l.id)).toEqual(['2', '1', '3']);
98
- // A lesson learned only from the pull request under review is its own reviews, already in front of the judge.
99
- const own = { ...base, id: '7', text: 'from this very pull request', file: 'src/orders.ts', state: 'verified', evidence: [{ pr: 42, comment: 'c', author: 'r' }] };
100
- const also = { ...own, id: '8', text: 'from this and another', evidence: [{ pr: 42, comment: 'c', author: 'r' }, { pr: 3, comment: 'd', author: 'r' }] };
101
- expect(matchLessons([...lessons, own, also], change, { excludePr: 42 }).map(l => l.id).sort()).toEqual(['1', '2', '8']); // '7' is left out; '8' has evidence elsewhere too
102
- // One file's many lessons never crowd out another file's only one.
103
- const many = Array.from({ length: 6 }, (_, i) => ({ ...base, id: `m${i}`, text: `orders lesson ${i}`, file: 'src/orders.ts', state: 'verified', symbols: ['insertOrder', 'orderId'] }));
104
- const lone = { ...base, id: 'lone', text: 'the only lesson about the manifest', file: 'src/manifest.sha', state: 'verified' };
105
- const served = matchLessons([...many, lone], { files: ['src/orders.ts', 'src/manifest.sha'], symbols: new Set(['insertOrder', 'orderId']) }, { limit: 4, perFile: 3 });
106
- expect(served.map(l => l.id)).toEqual(['m0', 'm1', 'm2', 'lone']);
107
- });
108
- });
109
- describe('learning from one pull request as it goes', () => {
110
- it('reads its reviews up to a time, and counts a point acted on by the head as of then', async () => {
111
- write(V1);
112
- const reviewed = commit('pr head');
113
- write(V2);
114
- const fixed = commit('address review');
115
- const api = 'https://api.github.com/repos/acme/app';
116
- const pages = {
117
- [`${api}/pulls/42`]: { number: 42, user: { login: 'dev' }, head: { sha: fixed } },
118
- [`${api}/pulls/42/commits?per_page=100&page=1`]: [
119
- { sha: reviewed, commit: { committer: { date: '2026-09-01T00:00:00Z' } } },
120
- { sha: fixed, commit: { committer: { date: '2026-09-02T00:00:00Z' } } },
121
- ],
122
- [`${api}/pulls/42/comments?per_page=100&page=1`]: [],
123
- [`${api}/pulls/42/reviews?per_page=100&page=1`]: [
124
- { id: 1, commit_id: reviewed, submitted_at: '2026-09-01T12:00:00Z', user: { login: 'priya' }, body: '- In src/orders.ts make the insert idempotent on retry.' },
125
- { id: 2, commit_id: fixed, submitted_at: '2026-09-06T00:00:00Z', user: { login: 'priya' }, body: '- A later point the learner must not see yet.' },
126
- ],
127
- };
128
- const fetchImpl = async (url) => ({ ok: url in pages, status: url in pages ? 200 : 404, json: async () => pages[url] });
129
- const early = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, pr: 42, until: '2026-09-01T18:00:00Z' });
130
- expect(early).toMatchObject({ prs: 1, reviewBodies: 1, total: 1 }); // the later review is not seen yet
131
- expect(readLessons(repo)[0]).toMatchObject({ file: 'src/orders.ts', text: 'In src/orders.ts make the insert idempotent on retry.', evidence: [{ pr: 42, author: 'priya', actedOn: false }] });
132
- fs.rmSync(path.join(repo, '.rigour', 'review-lessons.json'));
133
- await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, pr: 42, until: '2026-09-03T00:00:00Z' });
134
- expect(readLessons(repo)[0].evidence[0]).toMatchObject({ actedOn: true }); // the fix landed before this cut
135
- });
136
- });
137
- describe('outcome evidence from git', () => {
138
- it('finds a later fix to the lines a point named, counts lines left alone against it, and never looks past the cut', () => {
139
- write('export function total(order) {\n return order.items.length;\n}\n');
140
- commit('base');
141
- git('checkout', '-qb', 'feature');
142
- write('export function total(order) {\n const n = order.items.length;\n return n;\n}\n');
143
- const reviewed = commit('pr head');
144
- git('checkout', '-q', 'main');
145
- git('merge', '-q', '--no-ff', '--no-edit', 'feature');
146
- const merge = git('rev-parse', 'HEAD');
147
- write('// header\nexport function total(order) {\n const n = order.items?.length ?? 0;\n return n;\n}\n');
148
- execFileSync('git', ['-C', repo, 'commit', '-qam', 'fix: total crashes on an order with no items'], { env: { ...process.env, GIT_COMMITTER_DATE: '2026-09-20T00:00:00Z' } });
149
- const point = (start, actedOn = false) => ({ id: `p${start}`, text: 'Guard a missing items list.', file: 'src/orders.ts', symbols: [], state: 'candidate', at: { commit: reviewed, start, end: start }, evidence: [{ kind: 'point', pr: 7, comment: `c${start}`, author: 'bot', source: 'bot', actedOn }], createdAt: '', updatedAt: '' });
150
- const pr = { number: 7, mergeSha: merge, mergedAt: '2026-09-01T00:00:00Z' };
151
- const later = outcomeFor(gitIn(repo), point(2), pr, { mainRef: 'main' });
152
- expect(later).toMatchObject({ kind: 'outcome', detail: expect.stringContaining('fix: total crashes on an order with no items') });
153
- expect(outcomeFor(gitIn(repo), point(2), pr, { mainRef: 'main', until: '2026-09-10T00:00:00Z' })).toBeUndefined(); // before the fix, within the window: no verdict yet
154
- expect(outcomeFor(gitIn(repo), point(3), pr, { mainRef: 'main' })).toMatchObject({ kind: 'counter' }); // `return n;` shipped and stayed, past the window
155
- expect(outcomeFor(gitIn(repo), point(2, true), pr, { mainRef: 'main' })).toBeUndefined(); // acted on in the PR: no outcome to read
156
- });
157
- });
158
- describe('team standards', () => {
159
- it('verifies a standard when the same point recurs in another author\'s PR, and serves it only to a change it is about', () => {
160
- const standard = (pr, text) => ({ id: `s${pr}`, text, file: '', symbols: [], state: 'candidate', evidence: [{ kind: 'point', pr, comment: `r${pr}`, author: `reviewer${pr}`, prAuthor: `dev${pr}` }], createdAt: '', updatedAt: '' });
161
- const merged = mergeLessons([], [standard(1, 'Bound both ends of every time window a job reads.'), standard(2, 'Bound both ends of the time window this job reads.')]);
162
- expect(merged.lessons).toHaveLength(1);
163
- expect(merged.lessons[0].state).toBe('verified');
164
- const unrelated = mergeLessons(merged.lessons, [standard(3, 'Log the request id on every error.')]);
165
- expect(unrelated.lessons).toHaveLength(2);
166
- expect(matchLessons(unrelated.lessons, { files: ['src/jobs/scan.ts'], symbols: new Set(['timeWindowStart', 'readRows']) }).map(l => l.text)).toEqual(['Bound both ends of every time window a job reads.']);
167
- expect(matchLessons(unrelated.lessons, { files: ['src/ui/Dialog.svelte'], symbols: new Set(['onKeydown']) })).toEqual([]); // nothing in common: not served
168
- });
169
- });
170
- describe('turning reviews into rules', () => {
171
- const point = (id, text, file = '') => ({ id, text, file, symbols: [], state: 'candidate', evidence: [{ pr: 7, comment: `c-${id}`, author: 'priya' }], createdAt: '', updatedAt: '' });
172
- it('keeps the rule behind a point, drops a status note, keeps an unanswered point as said, and keeps the words beside the rule', async () => {
173
- const prompts = [];
174
- const write = async (prompt) => {
175
- prompts.push(prompt);
176
- return 'Here you go:\n{"rules":[{"id":"a","rule":"Apply discounts before computing an invoice total.","file":null},{"id":"b","rule":null,"file":null}]}';
177
- };
178
- const points = [point('a', 'The invoice total is computed before the discount is applied.'), point('b', 'npm run check: 0 errors, lint clean.'), point('c', 'A retry posts the payment twice.')];
179
- const out = await rulesFromReviews(points, write);
180
- expect(out).toMatchObject({ rules: 1, dropped: 1, unanswered: 1 });
181
- expect(out.lessons.map(l => [l.text, l.evidence.at(-1)?.kind])).toEqual([
182
- ['Apply discounts before computing an invoice total.', undefined], ['npm run check: 0 errors, lint clean.', 'norule'], ['A retry posts the payment twice.', undefined],
183
- ]); // the no-rule point is kept and marked, never sent again
184
- expect(out.lessons[0].evidence[0].said).toBe('The invoice total is computed before the discount is applied.');
185
- expect(prompts[0]).toContain('"said": "npm run check: 0 errors, lint clean."');
186
- expect(describeLesson(lessonView({ ...out.lessons[0], evidence: [...out.lessons[0].evidence, { pr: 9, comment: 'x', author: 'sam' }] })))
187
- .toBe('team standard: Apply discounts before computing an invoice total. (in their words: "The invoice total is computed before the discount is applied.") (acted on in PR #7, #9)');
188
- });
189
- it('never loses a point when the model cannot run, and keeps a file only when the point was about it', async () => {
190
- const out = await rulesFromReviews([point('a', 'In src/run.ts the flag is read last.', 'src/run.ts')], async () => undefined);
191
- expect(out.lessons.map(l => [l.text, l.file])).toEqual([['In src/run.ts the flag is read last.', 'src/run.ts']]);
192
- const moved = await rulesFromReviews([point('a', 'A point about one place.', 'src/a.ts')], async () => '{"rules":[{"id":"a","rule":"A rule the model placed in another file.","file":"src/b.ts"}]}');
193
- expect(moved.lessons[0].file).toBe('');
194
- });
195
- });
196
- describe('isSpecific', () => {
197
- it('counts identifiers that name something, not common words', () => {
198
- expect(['orderId', 'max_rows', 'renderTransactionalMessage'].map(isSpecific)).toEqual([true, true, true]);
199
- expect(['data', 'error', 'route', 'insert'].map(isSpecific)).toEqual([false, false, false, false]);
200
- });
201
- });
202
- describe('learnFromReviews', () => {
203
- it('reads merged PRs from GitHub, learns from acted-on comments, and shows them for the next change', async () => {
204
- write(V1);
205
- const reviewed = commit('pr head');
206
- write(V2);
207
- const merged = commit('address review');
208
- const api = 'https://api.github.com/repos/acme/app';
209
- const pages = {
210
- [`${api}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=1`]: [
211
- { number: 7, merged_at: '2026-09-01T00:00:00Z', merge_commit_sha: merged, user: { login: 'dev' } },
212
- { number: 8, merged_at: null, merge_commit_sha: null },
213
- ],
214
- [`${api}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=2`]: [],
215
- [`${api}/pulls/7/comments?per_page=100`]: [
216
- { id: 11, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: '**Use an upsert keyed on `id` so retries do not duplicate orders.**', user: { login: 'priya' } },
217
- { id: 12, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: 'agreed', in_reply_to_id: 11, user: { login: 'sam' } },
218
- { id: 13, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: '**Rename the variable to orderRow for clarity.**', user: { login: 'review-helper[bot]', type: 'Bot' } },
219
- { id: 14, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: '**Self note: tidy this later.**', user: { login: 'dev' } },
220
- ],
221
- [`${api}/pulls/7/reviews?per_page=100`]: [
222
- { id: 21, commit_id: reviewed, user: { login: 'priya' }, body: '## Review\n\n- Bound both ends of every time window a scheduled job reads.\n- In src/orders.ts the insert needs the idempotency key.\nThanks!' },
223
- { id: 22, commit_id: reviewed, user: { login: 'review-helper[bot]', type: 'Bot' }, body: '- Consider adding more unit tests for orders.' },
224
- { id: 23, commit_id: reviewed, user: { login: 'dev' }, body: '- Addressed every point in the next commit.' },
225
- ],
226
- };
227
- const fetchImpl = async (url) => ({ ok: url in pages, status: url in pages ? 200 : 404, json: async () => pages[url] });
228
- const result = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl });
229
- // Every point is a candidate, the bot's too; the author's own comments are not review points.
230
- expect(result).toMatchObject({ prs: 1, comments: 2, actedOn: 2, reviewBodies: 2, candidates: { person: 3, bot: 2 }, verified: 0 });
231
- const lessons = readLessons(repo);
232
- expect(lessons.every(l => l.state === 'candidate')).toBe(true); // nothing is a lesson without evidence
233
- const sources = new Set(lessons.flatMap(l => l.evidence.map(e => `${e.author}/${e.source}/${e.prAuthor}`)));
234
- expect([...sources].sort()).toEqual(['priya/person/dev', 'review-helper[bot]/bot/dev']);
235
- expect(lessons.map(l => l.text)).toEqual(expect.arrayContaining(['Bound both ends of every time window a scheduled job reads.', 'Consider adding more unit tests for orders.']));
236
- const diff = '+++ b/src/orders.ts\n@@ -1,0 +1,1 @@\n+ await db.insert(other);\n';
237
- expect(lessonsForDiff(repo, diff, 'verified')).toEqual([]); // candidates are not served by default
238
- expect(lessonsSection(lessonsForDiff(repo, diff, 'all'))).toContain('src/orders.ts: Use an upsert keyed on `id` so retries do not duplicate orders.');
239
- const again = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, writeRules: async () => { throw new Error('nothing promoted: no rule is written'); } });
240
- expect(again).toMatchObject({ added: 0, total: lessons.length });
241
- const upsert = lessons.find(l => l.text.startsWith('Use an upsert'));
242
- decideLesson(repo, upsert.id, 'accepted', 'lead@acme');
243
- const ruled = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, writeRules: async (prompt) => JSON.stringify({ rules: [...prompt.matchAll(/"id": "(\w+)"/g)].map(m => ({ id: m[1], rule: 'Make every write idempotent on retry.', file: 'src/orders.ts' })) }) });
244
- expect(ruled).toMatchObject({ rules: 1, notRules: 0, promoted: { person: 1 } }); // written only for the promoted lesson
245
- expect(readLessons(repo).find(l => l.state === 'verified')).toMatchObject({ text: 'Make every write idempotent on retry.', promotedBy: 'person' });
246
- const audit = fs.readFileSync(path.join(repo, '.rigour', 'review-rules-log.jsonl'), 'utf8').trim().split('\n').map(l => JSON.parse(l));
247
- expect(audit.map(a => [a.said, a.rule])).toEqual([[upsert.text, 'Make every write idempotent on retry.']]);
248
- });
249
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,42 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { FileGuardGate } from './gates/safety.js';
3
- import { execa } from 'execa';
4
- vi.mock('execa');
5
- describe('FileGuardGate', () => {
6
- const config = {
7
- safety: {
8
- protected_paths: ['docs/'],
9
- max_files_changed_per_cycle: 10
10
- }
11
- };
12
- it('should flag modified (M) protected files', async () => {
13
- const gate = new FileGuardGate(config);
14
- vi.mocked(execa).mockResolvedValueOnce({ stdout: ' M docs/SPEC.md\n' });
15
- const failures = await gate.run({ cwd: '/test', record: {} });
16
- expect(failures).toHaveLength(1);
17
- expect(failures[0].title).toContain("Protected file 'docs/SPEC.md' was modified.");
18
- });
19
- it('should flag added (A) protected files', async () => {
20
- const gate = new FileGuardGate(config);
21
- vi.mocked(execa).mockResolvedValueOnce({ stdout: 'A docs/NEW.md\n' });
22
- const failures = await gate.run({ cwd: '/test', record: {} });
23
- expect(failures).toHaveLength(1);
24
- expect(failures[0].title).toContain("Protected file 'docs/NEW.md' was modified.");
25
- });
26
- it('should NOT flag untracked (??) protected files', async () => {
27
- const gate = new FileGuardGate(config);
28
- vi.mocked(execa).mockResolvedValueOnce({ stdout: '?? docs/UNTRAKED.md\n' });
29
- const failures = await gate.run({ cwd: '/test', record: {} });
30
- expect(failures).toHaveLength(0);
31
- });
32
- it('should correctly handle multiple mixed statuses', async () => {
33
- const gate = new FileGuardGate(config);
34
- vi.mocked(execa).mockResolvedValueOnce({
35
- stdout: ' M docs/MODIFIED.md\n?? docs/NEW_UNTRACKED.md\n D docs/DELETED.md\n'
36
- });
37
- const failures = await gate.run({ cwd: '/test', record: {} });
38
- expect(failures).toHaveLength(2);
39
- expect(failures.map(f => f.title)).toContain("Protected file 'docs/MODIFIED.md' was modified.");
40
- expect(failures.map(f => f.title)).toContain("Protected file 'docs/DELETED.md' was modified.");
41
- });
42
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,22 +0,0 @@
1
- import path from 'path';
2
- import { describe, expect, it } from 'vitest';
3
- import { CANDIDATE_RULES, ECOSYSTEM_RULES } from './rules/ecosystem/index.js';
4
- import { loadCases, runCase } from './benchmark.js';
5
- const ROOT = path.resolve(__dirname, '../../../../benchmarks/semantic');
6
- describe('semantic benchmark', () => {
7
- const cases = loadCases(ROOT);
8
- it('has cases for every built-in rule, including negatives', () => {
9
- const rules = new Set(cases.map(c => c.rule));
10
- expect(rules).toEqual(new Set([
11
- 'in-memory-aggregation', 'credential-redirect', 'degraded-response-cached',
12
- ...[...ECOSYSTEM_RULES, ...CANDIDATE_RULES].map(rule => rule.id), null,
13
- ]));
14
- });
15
- for (const bench of cases) {
16
- it(`${bench.name}: ${bench.note}`, () => {
17
- const result = runCase(ROOT, bench);
18
- expect(result.falsePositives).toEqual([]);
19
- expect(result.caught).toBe(true);
20
- });
21
- }
22
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,46 +0,0 @@
1
- import path from 'path';
2
- import { describe, expect, it } from 'vitest';
3
- import { loadIntentCases, runIntentCase, scoreIntent } from './benchmark.js';
4
- import { verdictOf } from './intent-check.js';
5
- const ROOT = path.resolve(__dirname, '../../../../../benchmarks/intent');
6
- /** Answers from the labels: an oracle, optionally contradicting itself on one read. */
7
- function labelledModel(labels, contradict) {
8
- return {
9
- async ask(prompt) {
10
- const label = Object.keys(labels).find(name => prompt.includes(`Question: If \`${name}\``) || prompt.includes(`result of \`${name}\``));
11
- if (!label)
12
- return null;
13
- const optional = labels[label] === 'optional';
14
- const firstQuestion = prompt.includes('Question: If');
15
- if (label === contradict)
16
- return 'yes';
17
- return firstQuestion === optional ? 'yes' : 'no';
18
- },
19
- };
20
- }
21
- describe('verdictOf', () => {
22
- it('counts only consistent flipped answers', () => {
23
- expect(verdictOf(['yes', 'no'])).toBe('optional');
24
- expect(verdictOf(['no', 'yes'])).toBe('required');
25
- expect(verdictOf(['yes', 'yes'])).toBe('unknown');
26
- expect(verdictOf(['no', 'no'])).toBe('unknown');
27
- expect(verdictOf([null, 'no'])).toBe('unknown');
28
- });
29
- });
30
- describe('intent benchmark with a labelled oracle', () => {
31
- const cases = loadIntentCases(ROOT);
32
- it('finds every labelled site, skips the fixed one, and scores the oracle perfectly', async () => {
33
- const results = await Promise.all(cases.map(bench => runIntentCase(ROOT, bench, labelledModel(bench.labels))));
34
- const score = scoreIntent(results);
35
- expect(score.finderErrors).toEqual([]);
36
- expect(score.falsePositives).toBe(0);
37
- expect(score.falseNegatives).toBe(0);
38
- expect(score.rawCorrect).toBe(score.rawTotal);
39
- });
40
- it('drops a read whose answers contradict each other instead of guessing', async () => {
41
- const bench = cases.find(c => c.name === 'dashboard');
42
- const result = await runIntentCase(ROOT, bench, labelledModel(bench.labels, 'getRecommendedCourses'));
43
- expect(result.verdicts.find(v => v.read.label === 'getRecommendedCourses')?.verdict).toBe('unknown');
44
- expect(result.findings).toBe(0);
45
- });
46
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,67 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import ts from 'typescript';
5
- import { afterEach, describe, expect, it } from 'vitest';
6
- import { fileURLToPath } from 'url';
7
- import { forEachNode } from '../ast.js';
8
- import { loadLearnCases, runLearnCase } from './benchmark.js';
9
- import { guardsOf, namesIn, propertiesNamed } from './conditions.js';
10
- import { functionKeyOf, isInvocation } from './identity.js';
11
- import { loadLearnedRules, saveLearnedRule } from './store.js';
12
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../benchmarks/learn');
13
- describe('learn benchmark', () => {
14
- for (const bench of loadLearnCases(ROOT)) {
15
- it(`${bench.name}: ${bench.note}`, async () => {
16
- const result = await runLearnCase(ROOT, bench);
17
- expect(result.problems).toEqual([]);
18
- });
19
- }
20
- });
21
- function parse(text) {
22
- return ts.createSourceFile('x.ts', text, ts.ScriptTarget.Latest, true);
23
- }
24
- function firstCall(sf) {
25
- let call;
26
- forEachNode(sf, (n) => { if (!call && isInvocation(n))
27
- call = n; });
28
- return call;
29
- }
30
- describe('guardsOf', () => {
31
- it('collects the spread condition and the value condition', () => {
32
- const sf = parse("h({ ...(ok ? { 'cache-control': fresh ? 'max-age=1' : 'no-store' } : {}) });");
33
- const [prop] = propertiesNamed(sf, 'cache-control');
34
- expect(namesIn(guardsOf(prop, firstCall(sf)))).toEqual(['fresh', 'ok']);
35
- });
36
- it('lists property names before receivers', () => {
37
- const sf = parse("h({ c: status === 200 && body.stepsAvailable !== false ? 'a' : 'b' });");
38
- const [prop] = propertiesNamed(sf, 'c');
39
- expect(namesIn(guardsOf(prop, firstCall(sf)))[0]).toBe('stepsAvailable');
40
- });
41
- });
42
- describe('functionKeyOf', () => {
43
- it('names nested arrows, methods and the module level', () => {
44
- const sf = parse('class A { run() { const inner = () => f(); } }\ng();\n');
45
- const keys = [];
46
- forEachNode(sf, (n) => { if (isInvocation(n))
47
- keys.push(functionKeyOf(n)); });
48
- expect(keys).toEqual(['A.run>inner', '<module>']);
49
- });
50
- });
51
- describe('learned rule store', () => {
52
- let dir;
53
- afterEach(() => { if (dir)
54
- fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
55
- it('round-trips a rule and skips files that are not rules', () => {
56
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'learned-'));
57
- const rule = {
58
- id: 'require-option-redirect-abcd1234', version: 1,
59
- pattern: { template: 'require-option', callee: { level: 'name', key: 'fetch' }, argIndex: 1, property: 'redirect' },
60
- message: 'm', severity: 'medium', source: { file: 'http.ts', function: 'post' },
61
- validation: { firesBefore: 1, firesAfter: 0, maxHits: 3, repoHits: [] },
62
- };
63
- saveLearnedRule(dir, rule);
64
- fs.writeFileSync(path.join(dir, '.rigour', 'rules', 'broken.json'), '{"id": 1}');
65
- expect(loadLearnedRules(dir)).toEqual([rule]);
66
- });
67
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,77 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import ts from 'typescript';
5
- import { afterEach, describe, expect, it } from 'vitest';
6
- import { forEachNode, symbolOf } from './ast.js';
7
- import { originsOf } from './origins.js';
8
- import { programBatches } from './program.js';
9
- let dir;
10
- afterEach(() => { if (dir)
11
- fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
12
- /** Program over `files`; returns the checker and a finder for `probe(<expr>)` arguments in main.ts. */
13
- function setup(files) {
14
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'origins-'));
15
- for (const [name, body] of Object.entries(files))
16
- fs.writeFileSync(path.join(dir, name), body);
17
- const [program] = [...programBatches([path.join(dir, 'main.ts')], { strict: true, noEmit: true })];
18
- const checker = program.getTypeChecker();
19
- const main = program.getSourceFile(path.join(dir, 'main.ts'));
20
- const probe = () => {
21
- let found;
22
- forEachNode(main, (n) => {
23
- if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === 'probe')
24
- found = n.arguments[0];
25
- });
26
- return found;
27
- };
28
- const kinds = () => originsOf(checker, probe()).map(o => o.kind === 'call' ? `call:${o.node.expression.getText()}` : o.kind === 'param' ? `param:${o.index}` : o.kind);
29
- return { kinds };
30
- }
31
- const PRELUDE = 'declare function probe(v: unknown): void;\ndeclare function a(): number;\ndeclare function b(): number;\n';
32
- describe('originsOf', () => {
33
- it('follows const initializers and let reassignments', () => {
34
- const { kinds } = setup({ 'main.ts': `${PRELUDE}let x = a();\nif (Math.random()) x = b();\nconst y = x;\nprobe(y);\n` });
35
- expect(kinds().sort()).toEqual(['call:a', 'call:b']);
36
- });
37
- it('maps an element of Promise.all([...]) to the matching call', async () => {
38
- const { kinds } = setup({ 'main.ts': `${PRELUDE}async function f() {\n const [first, second] = await Promise.all([a(), b()]);\n probe(second);\n return first;\n}\n` });
39
- expect(kinds()).toEqual(['call:b']);
40
- });
41
- it('reads a destructured property from an object literal', () => {
42
- const { kinds } = setup({ 'main.ts': `${PRELUDE}const obj = { left: a(), right: b() };\nconst { right } = obj;\nprobe(right);\n` });
43
- expect(kinds()).toEqual(['call:b']);
44
- });
45
- it('keeps both sides of ?? and of a conditional', () => {
46
- const { kinds } = setup({ 'main.ts': `${PRELUDE}const v = Math.random() > 0.5 ? a() : (b() ?? a());\nprobe(v);\n` });
47
- expect(kinds().sort()).toEqual(['call:a', 'call:a', 'call:b']);
48
- });
49
- it('reports a parameter with its index', () => {
50
- const { kinds } = setup({ 'main.ts': `${PRELUDE}function f(first: number, second: number) {\n probe(second);\n return first;\n}\n` });
51
- expect(kinds()).toEqual(['param:1']);
52
- });
53
- it('follows imports across files to the declaration', () => {
54
- const { kinds } = setup({
55
- 'lib.ts': 'export const shared = Math.max(1, 2);\n',
56
- 'main.ts': `${PRELUDE}import { shared } from './lib';\nprobe(shared);\n`,
57
- });
58
- expect(kinds()).toEqual(['call:Math.max']);
59
- });
60
- it('stays unknown for an import that does not resolve', () => {
61
- const { kinds } = setup({ 'main.ts': `${PRELUDE}import { missing } from './nowhere';\nprobe(missing);\n` });
62
- expect(kinds()).toEqual(['unknown']);
63
- });
64
- });
65
- describe('symbolOf', () => {
66
- it('resolves the name in a shorthand property to the variable it reads', () => {
67
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'origins-'));
68
- fs.writeFileSync(path.join(dir, 'main.ts'), "const headers = { 'x-token': 't' };\nexport const init = { headers };\n");
69
- const [program] = [...programBatches([path.join(dir, 'main.ts')], { strict: true, noEmit: true })];
70
- const main = program.getSourceFile(path.join(dir, 'main.ts'));
71
- let shorthand;
72
- forEachNode(main, (n) => { if (ts.isShorthandPropertyAssignment(n))
73
- shorthand = n; });
74
- const decl = symbolOf(program.getTypeChecker(), shorthand.name)?.valueDeclaration;
75
- expect(decl && ts.isVariableDeclaration(decl) && decl.name.getText()).toBe('headers');
76
- });
77
- });
@@ -1 +0,0 @@
1
- export {};