@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 +0,0 @@
1
- export {};
@@ -1,58 +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 { recordReviewOutcome } from './agent-fixes.js';
6
- import { readAgentEvents } from './effectiveness.js';
7
- import { recordLessonsServed, recordPrCatches } from './learning-events.js';
8
- import { compactDiff, readStories } from './stories.js';
9
- let cwd;
10
- beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'stories-')); fs.mkdirSync(path.join(cwd, 'src')); });
11
- afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
12
- const finding = (id, title) => ({ id, title, details: `${title}.`, severity: 'high', files: ['src/a.ts'], line: 2 });
13
- describe('compactDiff', () => {
14
- it('keeps only the changed lines and two lines of context', () => {
15
- const before = ['a', 'b', 'c', 'old', 'd', 'e', 'f'].join('\n');
16
- const after = ['a', 'b', 'c', 'new', 'd', 'e', 'f'].join('\n');
17
- expect(compactDiff(before, after)).toEqual([' b', ' c', '-old', '+new', ' d', ' e']);
18
- });
19
- });
20
- describe('stories from fixes', () => {
21
- it('credits a fix to the stage that first reported it, with the code change', () => {
22
- fs.writeFileSync(path.join(cwd, 'src/a.ts'), "fetch(url, { headers });\n");
23
- recordReviewOutcome(cwd, [finding('semantic-bugs', 'Credential header follows redirects')], ['src/a.ts'], 'edit');
24
- fs.writeFileSync(path.join(cwd, 'src/a.ts'), "fetch(url, { headers, redirect: 'error' });\n");
25
- recordReviewOutcome(cwd, [], ['src/a.ts'], 'stop');
26
- const [story] = readStories(cwd);
27
- expect(story).toMatchObject({ stage: 'edit', file: 'src/a.ts', rule: 'semantic-bugs', title: 'Credential header follows redirects' });
28
- expect(story.diff).toEqual(['-fetch(url, { headers });', "+fetch(url, { headers, redirect: 'error' });", ' ']);
29
- });
30
- it('writes no story for a finding someone dismissed, even after its file changed', async () => {
31
- const { dismissFinding, findingKey } = await import('./quiet.js');
32
- const f = finding('semantic-bugs', 'Looks risky');
33
- fs.writeFileSync(path.join(cwd, 'src/a.ts'), 'x\n');
34
- recordReviewOutcome(cwd, [f], ['src/a.ts'], 'review');
35
- dismissFinding(cwd, findingKey(f), 'intended');
36
- fs.writeFileSync(path.join(cwd, 'src/a.ts'), 'y\n');
37
- recordReviewOutcome(cwd, [], ['src/a.ts'], 'review');
38
- expect(readStories(cwd)).toEqual([]);
39
- });
40
- it('writes no story while the finding is still open', () => {
41
- fs.writeFileSync(path.join(cwd, 'src/a.ts'), 'x\n');
42
- recordReviewOutcome(cwd, [finding('semantic-bugs', 'X')], ['src/a.ts'], 'review');
43
- expect(readStories(cwd)).toEqual([]);
44
- });
45
- });
46
- describe('learning events', () => {
47
- it('records lessons served and PR catches, and nothing when there are none', () => {
48
- recordLessonsServed(cwd, 'recall', []);
49
- recordPrCatches(cwd, []);
50
- expect(readAgentEvents(cwd)).toEqual([]);
51
- recordLessonsServed(cwd, 'review', ['Never follow redirects with credentials', 'Never follow redirects with credentials']);
52
- recordPrCatches(cwd, [finding('semantic-bugs', 'Unbounded query')]);
53
- expect(readAgentEvents(cwd)).toEqual([
54
- expect.objectContaining({ type: 'lessons_served', via: 'review', lessons: ['Never follow redirects with credentials'] }),
55
- expect.objectContaining({ type: 'pr_catches', findings: [{ rule: 'semantic-bugs', title: 'Unbounded query', file: 'src/a.ts' }] }),
56
- ]);
57
- });
58
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,108 +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 { ConfigSchema } from '../types/index.js';
6
- import { runToolchain } from './toolchain.js';
7
- let dir;
8
- const config = (commands = {}) => ConfigSchema.parse({ version: 1, commands });
9
- /** A fake installed tool: node logic plus the sh and .cmd shims npm would create. */
10
- function tool(name, logic) {
11
- const bin = path.join(dir, 'node_modules', '.bin');
12
- fs.mkdirSync(bin, { recursive: true });
13
- fs.writeFileSync(path.join(bin, `${name}.js`), `const fs = require('fs'); const args = process.argv.slice(2);\nfs.appendFileSync(${JSON.stringify(path.join(dir, 'calls.log'))}, ${JSON.stringify(name)} + ' ' + args.join(' ') + '\\n');\n${logic}\n`);
14
- fs.writeFileSync(path.join(bin, name), `#!/bin/sh\nexec node "$(dirname "$0")/${name}.js" "$@"\n`, { mode: 0o755 });
15
- fs.writeFileSync(path.join(bin, `${name}.cmd`), `@node "%~dp0\\${name}.js" %*\r\n`);
16
- }
17
- const calls = () => (fs.existsSync(path.join(dir, 'calls.log')) ? fs.readFileSync(path.join(dir, 'calls.log'), 'utf8') : '');
18
- beforeEach(() => {
19
- dir = fs.mkdtempSync(path.join(os.tmpdir(), 'toolchain-'));
20
- fs.mkdirSync(path.join(dir, 'src'));
21
- fs.writeFileSync(path.join(dir, 'src/a.ts'), 'export const a = 1;\n');
22
- fs.writeFileSync(path.join(dir, 'src/bad.ts'), 'BAD\n');
23
- });
24
- afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
25
- describe('runToolchain', () => {
26
- it('reports every tool the project did not install as skipped, never downloading one', async () => {
27
- const results = await runToolchain(dir, ['src/a.ts'], config());
28
- expect(results.map(r => [r.tool, r.status])).toEqual([['format', 'skipped'], ['lint', 'skipped'], ['overlay', 'skipped'], ['typecheck', 'skipped'], ['test', 'skipped'], ['knip', 'skipped']]);
29
- });
30
- it('fails, not skips, a tool the project declares but has not installed: a checkout without its dependencies proves nothing', async () => {
31
- fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^9', typescript: '^5' } }));
32
- const results = await runToolchain(dir, ['src/a.ts'], config());
33
- expect(results.map(r => [r.tool, r.status])).toEqual([['format', 'skipped'], ['lint', 'fail'], ['overlay', 'skipped'], ['typecheck', 'fail'], ['test', 'skipped'], ['knip', 'skipped']]);
34
- expect(results.find(r => r.tool === 'lint')).toMatchObject({ command: 'eslint is in package.json but not installed', output: expect.stringContaining('install the dependencies') });
35
- });
36
- it('runs type-checked rules the project does not enable, blocking on changed lines only, with the project\'s own eslint and config', async () => {
37
- fs.writeFileSync(path.join(dir, 'eslint.config.js'), 'export default [];\n');
38
- fs.mkdirSync(path.join(dir, 'node_modules/typescript-eslint'), { recursive: true });
39
- fs.writeFileSync(path.join(dir, 'node_modules/typescript-eslint/package.json'), '{"name":"typescript-eslint","main":"index.js"}');
40
- fs.writeFileSync(path.join(dir, 'node_modules/typescript-eslint/index.js'), 'module.exports = {};');
41
- const report = JSON.stringify([{ filePath: path.join(dir, 'src/a.ts'), messages: [
42
- { line: 2, column: 5, ruleId: '@typescript-eslint/no-unnecessary-condition', message: 'Unnecessary conditional, value is always truthy.' },
43
- { line: 9, column: 1, ruleId: '@typescript-eslint/no-floating-promises', message: 'Promises must be awaited.' },
44
- ] }]);
45
- tool('eslint', `if (args[0] === '--version') { console.log('v9.0.0'); process.exit(0); }\nif (args.includes('--format')) { console.log(${JSON.stringify(report)}); process.exit(1); }\nprocess.exit(0);`);
46
- const results = await runToolchain(dir, ['src/a.ts'], config(), { 'src/a.ts': new Set([1, 2, 3]) });
47
- const overlay = results.find(r => r.tool === 'overlay');
48
- expect(overlay).toMatchObject({ status: 'fail', lines: ['src/a.ts:2:5 @typescript-eslint/no-unnecessary-condition: Unnecessary conditional, value is always truthy.'] });
49
- expect(overlay.command).toContain('1 on changed lines, 1 pre-existing (not blocking)');
50
- const call = calls().split('\n').find(line => line.includes('--format json'));
51
- expect(call).toMatch(/eslint --config \S+eslint\.overlay\.config\.mjs --no-warn-ignored --format json src\/a\.ts/);
52
- expect(fs.readdirSync(dir)).not.toContain('eslint.overlay.config.mjs'); // never written into the repository
53
- // Nothing on a changed line: pass, backlog counted.
54
- const clean = await runToolchain(dir, ['src/a.ts'], config(), { 'src/a.ts': new Set([7]) });
55
- expect(clean.find(r => r.tool === 'overlay')).toMatchObject({ status: 'pass', command: expect.stringContaining('0 on changed lines, 2 pre-existing') });
56
- });
57
- it('runs knip when the project installs it, and reports only what is in the changed files', async () => {
58
- const report = JSON.stringify({ files: [path.join(dir, 'src/dead.ts'), 'src/elsewhere.ts'], issues: [
59
- { file: 'src/a.ts', exports: [{ name: 'spare', line: 3, col: 1 }], types: [{ name: 'Spare', line: 4, col: 1 }] },
60
- { file: 'src/other.ts', exports: [{ name: 'old', line: 1, col: 1 }], types: [] },
61
- ] });
62
- tool('knip', `console.log(${JSON.stringify(report)}); process.exit(1);`);
63
- fs.writeFileSync(path.join(dir, 'src/dead.ts'), 'export const dead = 1;\n');
64
- const results = await runToolchain(dir, ['src/a.ts', 'src/dead.ts'], config());
65
- expect(results.find(r => r.tool === 'knip')).toMatchObject({ status: 'fail', lines: ['src/dead.ts: nothing imports or runs this file', 'src/a.ts:3: unused export spare', 'src/a.ts:4: unused export Spare'] });
66
- expect(calls()).toContain('knip --production --no-progress --reporter json');
67
- });
68
- it('runs the formatter on changed files only and reports what it said', async () => {
69
- tool('prettier', "const bad = args.filter(a => a.endsWith('.ts') && fs.readFileSync(a, 'utf8').includes('BAD'));\nif (bad.length) { console.log('[warn] ' + bad.join(' ')); process.exit(1); }");
70
- const [format] = await runToolchain(dir, ['src/a.ts', 'src/bad.ts', 'src/gone.ts'], config());
71
- expect(format).toMatchObject({ tool: 'format', status: 'fail' });
72
- expect(format.output).toContain('src/bad.ts');
73
- expect(calls()).toContain('prettier --check --ignore-unknown src/a.ts src/bad.ts');
74
- expect(calls()).not.toContain('gone.ts');
75
- });
76
- it('quiets ESLint 9 about ignored files, and runs a tool set under commands: as that command, whose failure fails the push', async () => {
77
- tool('eslint', "if (args[0] === '--version') { console.log('v9.1.0'); process.exit(0); }");
78
- tool('prettier', 'process.exit(0);');
79
- const results = await runToolchain(dir, ['src/a.ts'], config({ format: 'node -e "process.exit(4)"', test: 'node -e "process.exit(0)"' }));
80
- expect(results.find(r => r.tool === 'lint')).toMatchObject({ status: 'pass' });
81
- expect(calls()).toContain('eslint --max-warnings=0 --no-warn-ignored src/a.ts');
82
- expect(results.find(r => r.tool === 'format')).toMatchObject({ status: 'fail', command: expect.stringContaining('node -e') });
83
- expect(calls()).not.toContain('prettier'); // the team's command replaces the detected tool
84
- expect(results.find(r => r.tool === 'test')).toMatchObject({ status: 'pass' });
85
- });
86
- it("uses the project's own check script for types, and never hands a tool Rigour's own files", async () => {
87
- fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { check: 'node -e "process.exit(3)"' } }));
88
- tool('prettier', 'process.exit(0);');
89
- const results = await runToolchain(dir, ['src/a.ts', '.rigour/scan-cache.json', 'rigour-report.json'], config());
90
- expect(results.find(r => r.tool === 'typecheck')).toMatchObject({ status: 'fail', command: expect.stringContaining('npm') });
91
- expect(calls()).toContain('prettier --check --ignore-unknown src/a.ts\n');
92
- });
93
- it('finds svelte-check by its binary, generating SvelteKit types first, with no svelte.config file', async () => {
94
- tool('svelte-kit', 'process.exit(0);');
95
- tool('svelte-check', 'process.exit(0);');
96
- fs.writeFileSync(path.join(dir, 'tsconfig.json'), '{}');
97
- const typecheck = (await runToolchain(dir, ['src/a.ts'], config())).find(r => r.tool === 'typecheck');
98
- expect(typecheck).toMatchObject({ status: 'pass', command: expect.stringContaining('svelte-check') });
99
- expect(calls().indexOf('svelte-kit sync')).toBeLessThan(calls().indexOf('svelte-check'));
100
- });
101
- it('runs a test file that failed in the parallel run again alone, so a timing flake passes', async () => {
102
- tool('vitest', "if (args[0] === 'related') { console.log(' FAIL src/a.test.ts > flaky'); process.exit(1); }\nprocess.exit(0);");
103
- const test = (await runToolchain(dir, ['src/a.ts'], config())).find(r => r.tool === 'test');
104
- expect(test.status).toBe('pass');
105
- expect(test.command).toContain('flaky files passed alone: src/a.test.ts');
106
- expect(calls()).toContain('vitest run src/a.test.ts');
107
- });
108
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,315 +0,0 @@
1
- import { execFileSync } from 'child_process';
2
- import fs from 'fs';
3
- import { createRequire } from 'module';
4
- import os from 'os';
5
- import path from 'path';
6
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
- import { ConfigSchema } from '../../types/index.js';
8
- import { reviewChange } from '../review.js';
9
- let repo;
10
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
11
- const write = (rel, body) => {
12
- fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
13
- fs.writeFileSync(path.join(repo, rel), body);
14
- };
15
- const config = (redundancy = {}) => ConfigSchema.parse({ version: 1, gates: { redundancy, unused_exports: { enabled: false }, orphan_files: { enabled: false } } });
16
- const typescriptDir = path.dirname(createRequire(import.meta.url).resolve('typescript/package.json'));
17
- const DB = `export interface Row { id: string; set_id: string | null; updated_at: string }
18
- export class Query<T> {
19
- not(_column: string, _operator: string, _value: unknown): Query<T> { return this; }
20
- gte(_column: string, _value: unknown): Query<T> { return this; }
21
- returns<R>(): Promise<R> { return Promise.resolve([] as unknown as R); }
22
- }
23
- export function from(_table: string): Query<unknown> { return new Query(); }
24
- `;
25
- beforeEach(() => {
26
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'typed-'));
27
- git('init', '-q', '-b', 'main');
28
- git('config', 'user.email', 't@example.com');
29
- git('config', 'user.name', 't');
30
- git('config', 'commit.gpgsign', 'false');
31
- write('package.json', '{"name":"fixture","devDependencies":{"typescript":"*"}}\n');
32
- write('tsconfig.json', '{"compilerOptions":{"strict":true,"module":"esnext","target":"es2022","moduleResolution":"bundler","skipLibCheck":true},"include":["src"]}\n');
33
- write('.gitignore', 'node_modules\n');
34
- fs.mkdirSync(path.join(repo, 'node_modules'));
35
- fs.symlinkSync(typescriptDir, path.join(repo, 'node_modules/typescript'), 'junction'); // the project's own compiler, never downloaded
36
- write('src/db.ts', DB);
37
- write('src/main.ts', "export const main = 1;\n");
38
- git('add', '-A');
39
- git('commit', '-qm', 'base');
40
- git('checkout', '-qb', 'feature');
41
- });
42
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
43
- const review = (cfg = config()) => reviewChange({ cwd: repo, config: cfg, source: { mode: 'base', base: 'main' }, typed: true });
44
- describe('what a change made redundant', () => {
45
- it('reports a null filter beside a range on the same column, and the nullable row type the query filters non-null', async () => {
46
- write('src/scan.ts', `import { from, type Row } from './db';
47
- export async function scan(since: string): Promise<Row[]> {
48
- return from('t').not('set_id', 'is', null).gte('set_id', since).returns<Row[]>();
49
- }
50
- `);
51
- git('add', '-A');
52
- git('commit', '-qm', 'scan');
53
- const result = await review();
54
- const found = result.findings.map(f => [f.id, f.files?.[0], f.line]);
55
- expect(found).toContainEqual(['duplicate-null-filter', 'src/scan.ts', 3]);
56
- expect(found).toContainEqual(['nullable-filtered-column', 'src/scan.ts', 3]); // anchored on the query the change wrote
57
- expect(result.findings.find(f => f.id === 'nullable-filtered-column')?.details).toContain('declared nullable at src/db.ts:1');
58
- expect(result.findings.find(f => f.id === 'duplicate-null-filter')?.details).toContain("`.gte('set_id', …)` at src/scan.ts:3");
59
- }, 60_000);
60
- it('reports an optional member every host supplies and a property written but never read; a spread host or a read elsewhere clears them', async () => {
61
- write('src/types.ts', 'export interface Candidate { userId?: string; title: string; extra: string }\n');
62
- write('src/a.ts', "import type { Candidate } from './types';\nexport const a: Candidate = { userId: 'u1', title: 'one', extra: 'x' };\n");
63
- write('src/b.ts', "import type { Candidate } from './types';\nexport const b: Candidate = { userId: 'u2', title: 'two', extra: 'y' };\n");
64
- write('src/use.ts', "import { a } from './a';\nimport { b } from './b';\nexport const titles = [a.title, b.title];\n");
65
- git('add', '-A');
66
- git('commit', '-qm', 'candidates');
67
- const result = await review();
68
- const found = result.findings.map(f => [f.id, f.files?.[0]]);
69
- expect(found).toContainEqual(['optional-always-supplied', 'src/types.ts']);
70
- expect(found).toContainEqual(['write-only-property', 'src/types.ts']);
71
- const writeOnly = result.findings.filter(f => f.id === 'write-only-property').map(f => f.details.match(/`([^`]+)`/)[1]);
72
- expect(writeOnly).toEqual(['Candidate.userId', 'Candidate.extra']); // userId is supplied everywhere and read nowhere: both findings hold
73
- expect(found.map(([id]) => id)).not.toContain('typed-checks-unavailable');
74
- // Declared as a wire contract: another service reads it, so nothing on it is write-only.
75
- const wired = await review(config({ wire_contracts: ['src/types.ts'] }));
76
- expect(wired.findings.map(f => f.id)).not.toContain('write-only-property');
77
- // A value that leaves only through serialisation is a hint for the reviewer, not a block.
78
- write('src/send.ts', "import { a } from './a';\nexport const body = JSON.stringify(a);\n");
79
- git('add', '-A');
80
- git('commit', '-qm', 'send');
81
- const sent = await review();
82
- expect(sent.findings.map(f => f.id)).not.toContain('write-only-property');
83
- expect(sent.hints).toEqual([expect.stringContaining('Candidate.userId is set by 2 host(s)'), expect.stringContaining('write-only-property src/types.ts:1: Candidate.extra')]);
84
- expect(sent.hints[1]).toContain('leaves only through JSON.stringify() at src/send.ts:2');
85
- }, 60_000);
86
- it('in a library, an exported type has consumers the program cannot see: its members are hints, a local type still blocks', async () => {
87
- write('tsconfig.json', '{"compilerOptions":{"strict":true,"declaration":true,"module":"esnext","target":"es2022","moduleResolution":"bundler","skipLibCheck":true},"include":["src"]}\n');
88
- write('src/types.ts', 'export interface Result { items?: string[]; cached: boolean }\ninterface Local { note: string }\nexport const local: Local = { note: "n" };\n');
89
- write('src/a.ts', "import type { Result } from './types';\nexport const a: Result = { items: [], cached: true };\n");
90
- write('src/b.ts', "import type { Result } from './types';\nexport const b: Result = { items: [], cached: false };\n");
91
- git('add', '-A');
92
- git('commit', '-qm', 'library');
93
- const result = await review();
94
- expect(result.findings.map(f => [f.id, f.details.match(/`([^`]+)`/)[1]])).toEqual([['write-only-property', 'Local.note']]);
95
- expect(result.hints).toEqual([
96
- expect.stringMatching(/^optional-always-supplied src\/types\.ts:1: `Result\.items` is optional .* consumers outside this program may read it\)$/),
97
- expect.stringMatching(/^write-only-property src\/types\.ts:1: `Result\.items` is set by 2 host\(s\)/),
98
- expect.stringMatching(/^write-only-property src\/types\.ts:1: `Result\.cached` is set by 2 host\(s\)/),
99
- ]);
100
- }, 60_000);
101
- it('hints at a function that scans a collection, called once per item of another', async () => {
102
- write('src/later.ts', 'export function hasLater(starts: string[], at: string): boolean {\n return starts.some(s => s > at);\n}\nexport function plan(rows: string[], starts: string[]): boolean[] {\n return rows.map(r => hasLater(starts, r));\n}\n');
103
- git('add', '-A');
104
- git('commit', '-qm', 'later');
105
- const result = await review();
106
- expect(result.hints).toEqual([expect.stringMatching(/^nested-scan src\/later\.ts:5: hasLater\(\) scans its `starts` argument and is called inside \.map\(\) over rows/)]);
107
- expect(result.findings).toEqual([]);
108
- }, 60_000);
109
- it('says nothing for a project with no tsconfig, and blocks a TypeScript project whose program cannot be built', async () => {
110
- write('src/x.ts', 'export const x = 1;\n');
111
- git('add', '-A');
112
- git('commit', '-qm', 'x');
113
- fs.unlinkSync(path.join(repo, 'tsconfig.json'));
114
- expect((await review()).findings).toEqual([]);
115
- write('tsconfig.json', '{"extends":"./.generated/tsconfig.json"}\n');
116
- const broken = await review();
117
- expect(broken.status).toBe('ERROR'); // a check that could not run is a crashed gate, never a pass
118
- expect(broken.gateErrors).toEqual(['typed-checks-unavailable']);
119
- expect(broken.typedError).toMatch(/\.generated\/tsconfig\.json.*install the dependencies/s);
120
- }, 60_000);
121
- });
122
- describe('a null guard on a column the query makes non-null', () => {
123
- const SESSION = `import { from } from './db';
124
- export interface Session { id: string; ended_at: string | null; label: string | null }
125
- export async function endedSessions(): Promise<Session[]> {
126
- return from('sessions').not('ended_at', 'is', null).returns<Session[]>();
127
- }
128
- `;
129
- const CONSUMER = `import { endedSessions } from './sessions';
130
- export async function summary(): Promise<string[]> {
131
- const rows = await endedSessions();
132
- return rows.map(row => {
133
- if (row.ended_at == null) return 'open';
134
- const ended = row.ended_at ?? 'never';
135
- if (!row.label) return ended;
136
- return row.label;
137
- });
138
- }
139
- `;
140
- const guards = async () => (await review()).advisory.filter(f => f.id === 'dead-null-guard').map(f => [f.files?.[0], f.line]);
141
- it('names each guard the filter made dead, and leaves a guard on a column no query filters alone', async () => {
142
- write('src/sessions.ts', SESSION);
143
- write('src/summary.ts', CONSUMER);
144
- git('add', '-A');
145
- git('commit', '-qm', 'sessions');
146
- expect(await guards()).toEqual([['src/summary.ts', 5], ['src/summary.ts', 6]]);
147
- });
148
- it("names a truthiness test on a filtered string column with the empty-string case, and leaves a number's alone", async () => {
149
- write('src/sessions.ts', `import { from } from './db';
150
- export interface Visit { id: string; page: string | null; seconds: number | null }
151
- export async function visits(): Promise<Visit[]> {
152
- return from('visits').not('page', 'is', null).not('seconds', 'is', null).returns<Visit[]>();
153
- }
154
- export async function pages(): Promise<string[]> {
155
- return (await visits()).filter(v => !!v.page && !v.seconds).map(v => v.page ?? '');
156
- }
157
- `);
158
- git('add', '-A');
159
- git('commit', '-qm', 'visits');
160
- const result = (await review()).advisory.filter(f => f.id === 'dead-null-guard');
161
- expect(result.map(f => f.line)).toEqual([7, 7]); // \`!v.page\` and \`v.page ?? ''\`, not \`!v.seconds\`
162
- expect(result.some(f => /empty string/.test(f.details ?? ''))).toBe(true);
163
- });
164
- it('follows a query typed through a generic pager and an intersection, and ignores the pager\'s own type parameter', async () => {
165
- write('src/pager.ts', `import { from, type Query } from './db';
166
- export async function readAll<Row>(page: () => Query<unknown>): Promise<Row[]> {
167
- await page();
168
- return from('audit').returns<Row[]>(); // the helper's own query, typed by its parameter: not a row type
169
- }
170
- `);
171
- write('src/sessions.ts', `import { from } from './db';
172
- import { readAll } from './pager';
173
- export interface Session { id: string; ended_at: string | null }
174
- export async function endedSessions(): Promise<Session[]> {
175
- return readAll<Session & Record<string, unknown>>(() => from('sessions').not('ended_at', 'is', null));
176
- }
177
- export async function summary(): Promise<string[]> {
178
- return (await endedSessions()).map(row => row.ended_at ?? 'never');
179
- }
180
- `);
181
- git('add', '-A');
182
- git('commit', '-qm', 'pager');
183
- expect(await guards()).toEqual([['src/sessions.ts', 8]]);
184
- });
185
- it('says nothing when another query returns the same rows unfiltered', async () => {
186
- write('src/sessions.ts', `${SESSION}export async function allSessions(): Promise<Session[]> {
187
- return from('sessions').returns<Session[]>();
188
- }
189
- `);
190
- write('src/summary.ts', CONSUMER);
191
- git('add', '-A');
192
- git('commit', '-qm', 'sessions');
193
- expect(await guards()).toEqual([]);
194
- });
195
- it('says nothing when code builds such a row with the column null', async () => {
196
- write('src/sessions.ts', `${SESSION}export const draft: Session = { id: 'new', ended_at: null, label: null };
197
- `);
198
- write('src/summary.ts', CONSUMER);
199
- git('add', '-A');
200
- git('commit', '-qm', 'sessions');
201
- expect(await guards()).toEqual([]);
202
- });
203
- });
204
- describe('an input production never varies', () => {
205
- const found = async (id) => (await review()).advisory.filter(f => f.id === id).map(f => [f.files?.[0], f.line]);
206
- const commit = () => { git('add', '-A'); git('commit', '-qm', 'change'); };
207
- it('names a member every production object sets to the same value while code branches on it; tests do not count', async () => {
208
- write('src/jobs.ts', `export interface Job { id: string; retries: number }
209
- export function next(job: Job): string { return job.retries > 0 ? 'retry' : 'run'; }
210
- export const nightly: Job = { id: 'nightly', retries: 0 };
211
- export const hourly: Job = { id: 'hourly', retries: 0 };
212
- `);
213
- write('src/jobs.test.ts', `import { next } from './jobs';
214
- export const flaky = next({ id: 'flaky', retries: 3 });
215
- `);
216
- commit();
217
- expect(await found('constant-member')).toEqual([['src/jobs.ts', 1]]);
218
- });
219
- it('says nothing when production sets the member differently', async () => {
220
- write('src/jobs.ts', `export interface Job { id: string; retries: number }
221
- export function next(job: Job): string { return job.retries > 0 ? 'retry' : 'run'; }
222
- export const nightly: Job = { id: 'nightly', retries: 0 };
223
- export const hourly: Job = { id: 'hourly', retries: 2 };
224
- `);
225
- commit();
226
- expect(await found('constant-member')).toEqual([]);
227
- });
228
- it('names a parameter every production call passes the same literal while the body branches on it', async () => {
229
- write('src/format.ts', `export function label(text: string, upper: boolean): string { return upper ? text.toUpperCase() : text; }
230
- export const a = label('one', false);
231
- export const b = label('two', false);
232
- export function wrap(text: string, client: { send(t: string): string }): string { return client.send(text); }
233
- `);
234
- write('src/send.ts', `import { wrap } from './format';
235
- const client = { send: (t: string) => t };
236
- export const x = wrap('a', client);
237
- export const y = wrap('b', client); // the same variable name is not the same literal
238
- `);
239
- write('src/format.test.ts', `import { label } from './format';
240
- export const loud = label('three', true);
241
- `);
242
- commit();
243
- expect(await found('constant-argument')).toEqual([['src/format.ts', 1]]);
244
- });
245
- it('says nothing when production calls vary, or the function is passed around as a value', async () => {
246
- write('src/format.ts', `export function label(text: string, upper: boolean): string { return upper ? text.toUpperCase() : text; }
247
- export const a = label('one', false);
248
- export const b = label('two', true);
249
- export function shout(text: string, loud: boolean): string { return loud ? text + '!' : text; }
250
- export const c = shout('x', false);
251
- export const d = shout('y', false);
252
- export const handlers = [shout];
253
- `);
254
- commit();
255
- expect(await found('constant-argument')).toEqual([]);
256
- });
257
- });
258
- describe('a nullable row type for a NOT NULL column, from migrations in another repository', () => {
259
- const SUPABASE = `export class Query<T> {
260
- select(_columns: string): Query<T> { return this; }
261
- returns<R>(): Promise<R> { return Promise.resolve([] as unknown as R); }
262
- }
263
- export const db = {
264
- schema(_name: string) { return db; },
265
- from(_table: string): Query<unknown> { return new Query(); },
266
- };
267
- `;
268
- let migrations;
269
- beforeEach(() => {
270
- migrations = fs.mkdtempSync(path.join(os.tmpdir(), 'other-repo-migrations-'));
271
- fs.writeFileSync(path.join(migrations, '20240101_sets.sql'), 'create table study_set (id uuid primary key, owner_id uuid not null, title text, archived_at timestamptz not null);');
272
- write('src/sb.ts', SUPABASE);
273
- git('add', '-A');
274
- git('commit', '-qm', 'client');
275
- });
276
- afterEach(() => { fs.rmSync(migrations, { recursive: true, force: true }); });
277
- it('notes the nullable member the change declared, never blocking, and skips nullable columns and aliases', async () => {
278
- write('src/sets.ts', `import { db } from './sb';
279
- export interface SetRow { id: string; owner_id: string | null; title: string | null; done: string | null }
280
- export async function sets(): Promise<SetRow[]> {
281
- return db.from('study_set').select('id, owner_id, title, done:archived_at').returns<SetRow[]>();
282
- }
283
- `);
284
- git('add', '-A');
285
- git('commit', '-qm', 'sets');
286
- const result = await review(config({ schema_migrations: [migrations] }));
287
- expect(result.findings.filter(f => f.id === 'nullable-not-null-column')).toEqual([]);
288
- const notes = result.advisory.filter(f => f.id === 'nullable-not-null-column');
289
- expect(notes.map(f => [f.files?.[0], f.line])).toEqual([['src/sets.ts', 2]]);
290
- expect(notes[0].details).toContain('`owner_id` is declared nullable at src/sets.ts:2 but `study_set.owner_id` is NOT NULL');
291
- });
292
- it('anchors on the new query when the row type is older, and reads the schema the query names', async () => {
293
- fs.writeFileSync(path.join(migrations, '20240202_app.sql'), 'create table app.study_set (id uuid, owner_id uuid);');
294
- git('checkout', '-q', 'main'); // the row type is older than the branch
295
- write('src/rows.ts', 'export interface SetRow { id: string; owner_id: string | null }\n');
296
- git('add', '-A');
297
- git('commit', '-qm', 'rows');
298
- git('checkout', '-q', 'feature');
299
- git('merge', '-q', '--no-edit', 'main');
300
- write('src/sets.ts', `import { db } from './sb';
301
- import type { SetRow } from './rows';
302
- export async function publicSets(): Promise<SetRow[]> {
303
- return db
304
- .from('study_set').select('*').returns<SetRow[]>();
305
- }
306
- export async function appSets(): Promise<SetRow[]> {
307
- return db.schema('app').from('study_set').select('*').returns<SetRow[]>();
308
- }
309
- `);
310
- git('add', '-A');
311
- git('commit', '-qm', 'sets');
312
- const result = await review(config({ schema_migrations: [migrations] }));
313
- expect(result.advisory.filter(f => f.id === 'nullable-not-null-column').map(f => [f.files?.[0], f.line])).toEqual([['src/sets.ts', 5]]);
314
- });
315
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,63 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import { describe, expect, it } from 'vitest';
5
- import { loadSchemaNullability, tableKey } from './schema-nullability.js';
6
- /** The migrations as numbered files in a folder of their own, replayed. */
7
- const replay = (...migrations) => {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'migrations-'));
9
- try {
10
- migrations.forEach((sql, i) => fs.writeFileSync(path.join(dir, `${String(i).padStart(3, '0')}_step.sql`), sql));
11
- return loadSchemaNullability(dir, ['.']);
12
- }
13
- finally {
14
- fs.rmSync(dir, { recursive: true, force: true });
15
- }
16
- };
17
- const columns = (schema, table) => Object.fromEntries(schema.get(tableKey(table)) ?? []);
18
- describe('NOT NULL columns from migrations', () => {
19
- it('reads column definitions, primary keys and quoted names, ignoring strings, comments and CHECK text', () => {
20
- const schema = replay(`
21
- -- a comment; with a semicolon
22
- create table if not exists public.study_set (
23
- id uuid primary key default gen_random_uuid(),
24
- "OwnerId" uuid not null references auth.users(id),
25
- title text default 'not null' ,
26
- archived_at timestamptz check (archived_at is not null or title is null),
27
- kind text NOT NULL,
28
- /* block; comment */
29
- constraint study_set_title_unique unique (title)
30
- );`);
31
- expect(columns(schema, 'study_set')).toEqual({ id: true, OwnerId: true, title: false, archived_at: false, kind: true });
32
- });
33
- it('follows ALTER TABLE in file order: add, set and drop NOT NULL, rename, drop column, rename table', () => {
34
- const schema = replay('create table app.card (id bigint, set_id bigint, body text);', `alter table only app.card add column if not exists due_at timestamptz not null default now(),
35
- alter column set_id set not null, add constraint card_pk primary key (id);`, 'alter table app.card alter column body set not null; alter table app.card alter column body drop not null;', 'alter table app.card rename column due_at to next_due_at; alter table app.card drop column if exists body;', 'alter table app.card rename to flashcard;');
36
- expect(schema.has(tableKey('app.card'))).toBe(false);
37
- expect(columns(schema, 'app.flashcard')).toEqual({ id: true, set_id: true, next_due_at: true });
38
- });
39
- it('keeps a function body whole and treats tables it cannot read as unknown', () => {
40
- const schema = replay(`create function touch() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$;
41
- create table copy as select * from other;
42
- create table part partition of parent for values in (1);
43
- create table gone (id int not null); drop table if exists gone cascade;`);
44
- expect(schema.get(tableKey('copy'))).toBeNull();
45
- expect(schema.get(tableKey('part'))).toBeNull();
46
- expect(schema.has(tableKey('gone'))).toBe(false);
47
- });
48
- it('loads every folder named, in file-name order across them, skipping one that does not exist', () => {
49
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'schema-'));
50
- try {
51
- fs.mkdirSync(path.join(root, 'a'));
52
- fs.mkdirSync(path.join(root, 'b'));
53
- fs.writeFileSync(path.join(root, 'b', '20240101_create.sql'), 'create table t (id int, note text);');
54
- fs.writeFileSync(path.join(root, 'a', '20240202_tighten.sql'), 'alter table t alter column note set not null;');
55
- fs.writeFileSync(path.join(root, 'a', 'README.md'), 'not sql');
56
- const schema = loadSchemaNullability(root, ['a', path.join(root, 'b'), 'missing']);
57
- expect(columns(schema, 't')).toEqual({ id: false, note: true });
58
- }
59
- finally {
60
- fs.rmSync(root, { recursive: true, force: true });
61
- }
62
- });
63
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,47 +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 { captureHumanEdits, recordAgentWrites } from './human-edits.js';
7
- import { readLessons } from './lessons.js';
8
- let repo;
9
- const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
10
- const write = (body) => fs.writeFileSync(path.join(repo, 'src/job.ts'), body);
11
- beforeEach(() => {
12
- repo = fs.mkdtempSync(path.join(os.tmpdir(), 'human-edits-'));
13
- git('init', '-q', '-b', 'main');
14
- git('config', 'user.email', 't@example.com');
15
- git('config', 'user.name', 't');
16
- git('config', 'commit.gpgsign', 'false');
17
- fs.mkdirSync(path.join(repo, 'src'));
18
- write('export function job(rows) {\n return rows.map(r => r.id);\n}\n');
19
- git('add', '-A');
20
- git('commit', '-qm', 'base');
21
- });
22
- afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
23
- describe('a person editing what the agent wrote', () => {
24
- it('becomes a lesson with correction evidence, taken once', () => {
25
- write('export function job(rows) {\n return rows.slice(0, 10000).map(r => r.id);\n}\n'); // the agent's write
26
- recordAgentWrites(repo, ['src/job.ts']);
27
- write('export function job(rows, after) {\n return rows.filter(r => r.id > after).slice(0, 500).map(r => r.id);\n}\n'); // the person's fix
28
- expect(captureHumanEdits(repo, 'lead@acme')).toBe(1);
29
- const [lesson] = readLessons(repo);
30
- expect(lesson).toMatchObject({ file: 'src/job.ts', state: 'verified', promotedBy: 'correction' });
31
- expect(lesson.text).toContain('a person changed what the agent wrote');
32
- expect(lesson.text).toContain('+ return rows.filter(r => r.id > after).slice(0, 500).map(r => r.id);');
33
- expect(lesson.evidence[0]).toMatchObject({ kind: 'correction', author: 'lead@acme' });
34
- expect(captureHumanEdits(repo, 'lead@acme')).toBe(0); // taken once
35
- });
36
- it('is not a correction when only whitespace moved, or the change came from git', () => {
37
- recordAgentWrites(repo, ['src/job.ts']);
38
- write('export function job(rows) {\n return rows.map(r => r.id);\n}\n'); // a formatter
39
- expect(captureHumanEdits(repo)).toBe(0);
40
- recordAgentWrites(repo, ['src/job.ts']);
41
- git('checkout', '-q', '-b', 'other');
42
- write('export const job = (rows) => rows.map(r => r.id);\n');
43
- git('commit', '-qam', 'someone else\'s branch');
44
- expect(captureHumanEdits(repo)).toBe(0); // a checkout brought it, not a person correcting the agent
45
- expect(readLessons(repo)).toEqual([]);
46
- });
47
- });
@@ -1 +0,0 @@
1
- export {};