@1aboveio/skills 1.0.8 → 1.0.10

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 (214) hide show
  1. package/.codex-plugin/plugin.json +13 -0
  2. package/.codex-plugin/resources.json +1827 -0
  3. package/README.md +22 -0
  4. package/package.json +9 -2
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/airflow/release-check.mjs +3 -1
  7. package/skills/app-dev/android-foundation/SKILL.md +1 -1
  8. package/skills/app-dev/android-foundation/release-check.mjs +3 -1
  9. package/skills/app-dev/app-debug/SKILL.md +1 -1
  10. package/skills/app-dev/app-debug/release-check.mjs +3 -1
  11. package/skills/app-dev/better-auth/SKILL.md +1 -1
  12. package/skills/app-dev/better-auth/release-check.mjs +3 -1
  13. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  14. package/skills/app-dev/data-catalog/release-check.mjs +3 -1
  15. package/skills/app-dev/data-quality/SKILL.md +1 -1
  16. package/skills/app-dev/data-quality/release-check.mjs +3 -1
  17. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
  19. package/skills/app-dev/monorepo/SKILL.md +1 -1
  20. package/skills/app-dev/monorepo/release-check.mjs +3 -1
  21. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  22. package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
  23. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  24. package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
  25. package/skills/app-dev/pyspark/SKILL.md +1 -1
  26. package/skills/app-dev/pyspark/release-check.mjs +3 -1
  27. package/skills/app-dev/python-backend/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/release-check.mjs +3 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/shadcn/release-check.mjs +3 -1
  31. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  32. package/skills/app-dev/zod-v4/release-check.mjs +3 -1
  33. package/skills/cicd/cloud-build/SKILL.md +1 -1
  34. package/skills/cicd/cloud-build/release-check.mjs +3 -1
  35. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  36. package/skills/cicd/cloud-debug/release-check.mjs +3 -1
  37. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  38. package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
  39. package/skills/cicd/docker/SKILL.md +1 -1
  40. package/skills/cicd/docker/release-check.mjs +3 -1
  41. package/skills/cicd/github-actions/SKILL.md +1 -1
  42. package/skills/cicd/github-actions/release-check.mjs +3 -1
  43. package/skills/cicd/github-flow/SKILL.md +1 -1
  44. package/skills/cicd/github-flow/release-check.mjs +3 -1
  45. package/skills/cicd/google-cloud/SKILL.md +1 -1
  46. package/skills/cicd/google-cloud/release-check.mjs +3 -1
  47. package/skills/cicd/mergify/SKILL.md +1 -1
  48. package/skills/cicd/mergify/release-check.mjs +3 -1
  49. package/skills/cicd/podman/SKILL.md +1 -1
  50. package/skills/cicd/podman/release-check.mjs +3 -1
  51. package/skills/content/article-review/SKILL.md +1 -1
  52. package/skills/content/article-review/release-check.mjs +3 -1
  53. package/skills/content/article-to-image/SKILL.md +1 -1
  54. package/skills/content/article-to-image/release-check.mjs +3 -1
  55. package/skills/content/flowchart-svg/SKILL.md +1 -1
  56. package/skills/content/flowchart-svg/release-check.mjs +3 -1
  57. package/skills/content/image-gen/SKILL.md +1 -1
  58. package/skills/content/image-gen/release-check.mjs +3 -1
  59. package/skills/content/mermaid/SKILL.md +1 -1
  60. package/skills/content/mermaid/release-check.mjs +3 -1
  61. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  62. package/skills/content/wechat-article-writer/release-check.mjs +3 -1
  63. package/skills/content/wechat-publisher/SKILL.md +1 -1
  64. package/skills/content/wechat-publisher/release-check.mjs +3 -1
  65. package/skills/content/wechat-search/SKILL.md +1 -1
  66. package/skills/content/wechat-search/release-check.mjs +3 -1
  67. package/skills/content/xai-search/SKILL.md +88 -13
  68. package/skills/content/xai-search/release-check.mjs +3 -1
  69. package/skills/content/xai-search/scripts/xai_search.sh +125 -149
  70. package/skills/core/distill-writing/SKILL.md +1 -1
  71. package/skills/core/distill-writing/release-check.mjs +3 -1
  72. package/skills/core/doc-to-md/SKILL.md +1 -1
  73. package/skills/core/doc-to-md/release-check.mjs +3 -1
  74. package/skills/core/editorial-review/SKILL.md +1 -1
  75. package/skills/core/editorial-review/release-check.mjs +3 -1
  76. package/skills/core/house-style/SKILL.md +1 -1
  77. package/skills/core/house-style/release-check.mjs +3 -1
  78. package/skills/core/plain-language/SKILL.md +1 -1
  79. package/skills/core/plain-language/release-check.mjs +3 -1
  80. package/skills/core/polish-cn/SKILL.md +1 -1
  81. package/skills/core/polish-cn/release-check.mjs +3 -1
  82. package/skills/core/reg-parser/SKILL.md +1 -1
  83. package/skills/core/reg-parser/release-check.mjs +3 -1
  84. package/skills/core/terminology/SKILL.md +1 -1
  85. package/skills/core/terminology/release-check.mjs +3 -1
  86. package/skills/core/write-doc/SKILL.md +1 -1
  87. package/skills/core/write-doc/release-check.mjs +3 -1
  88. package/skills/engineering/better-review/SKILL.md +1 -1
  89. package/skills/engineering/better-review/release-check.mjs +3 -1
  90. package/skills/engineering/contributing/SKILL.md +1 -1
  91. package/skills/engineering/contributing/release-check.mjs +3 -1
  92. package/skills/engineering/e2e-test/SKILL.md +1 -1
  93. package/skills/engineering/e2e-test/release-check.mjs +3 -1
  94. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  95. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  96. package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
  97. package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
  98. package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
  99. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  100. package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
  101. package/skills/engineering/golden-path/SKILL.md +1 -1
  102. package/skills/engineering/golden-path/release-check.mjs +3 -1
  103. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  104. package/skills/engineering/harness-runtime/release-check.mjs +3 -1
  105. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  106. package/skills/engineering/resolve-issues/references/preflight.md +7 -0
  107. package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
  108. package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
  109. package/skills/engineering/resolve-issues/release-check.mjs +3 -1
  110. package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
  111. package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
  112. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
  113. package/skills/engineering/resolve-release/SKILL.md +1 -1
  114. package/skills/engineering/resolve-release/release-check.mjs +3 -1
  115. package/skills/engineering/rush-issues/SKILL.md +1 -1
  116. package/skills/engineering/rush-issues/release-check.mjs +3 -1
  117. package/skills/engineering/rush-release/SKILL.md +1 -1
  118. package/skills/engineering/rush-release/release-check.mjs +3 -1
  119. package/skills/engineering/test-quality/SKILL.md +1 -1
  120. package/skills/engineering/test-quality/release-check.mjs +3 -1
  121. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  122. package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
  123. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  124. package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
  125. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  126. package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
  127. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  128. package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
  129. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  130. package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
  131. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  132. package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
  133. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  134. package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
  135. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  136. package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
  137. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
  138. package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
  139. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
  140. package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
  141. package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
  142. package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
  143. package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
  144. package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
  145. package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
  146. package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
  147. package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
  148. package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
  149. package/skills/content/image-gen/scripts/options.test.mjs +0 -245
  150. package/skills/content/xai-search/tests/smoke_test.sh +0 -36
  151. package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
  152. package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
  153. package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
  154. package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
  155. package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
  156. package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
  157. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
  158. package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
  159. package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
  160. package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
  161. package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
  162. package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
  163. package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
  164. package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
  165. package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
  166. package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
  167. package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
  168. package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
  169. package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
  170. package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
  171. package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
  172. package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
  173. package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
  174. package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
  175. package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
  176. package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
  177. package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
  178. package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
  179. package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
  180. package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
  181. package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
  182. package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
  183. package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
  184. package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
  185. package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
  186. package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
  187. package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
  188. package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
  189. package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
  190. package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
  191. package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
  192. package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
  193. package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
  194. package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
  195. package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
  196. package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
  197. package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
  198. package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
  199. package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
  200. package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
  201. package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
  202. package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
  203. package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
  204. package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
  205. package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
  206. package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
  207. package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
  208. package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
  209. package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
  210. package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
  211. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
  212. package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
  213. package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
  214. package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +0 -520
@@ -1,587 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { execFileSync, spawnSync } from 'node:child_process'
4
- import fs from 'node:fs'
5
- import os from 'node:os'
6
- import path from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { decideScope, numstatLines, priorProblems, parseCliArgs, RereviewScopeError, changedSymbols, headingSlug, sweepRecheck, patchImpactLines, skipRecord, ownLinesUnchanged } from './rereview-scope.mjs'
9
-
10
- const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'rereview-scope.mjs')
11
- const DONE = { spec: 'done', correctness: 'done', tests: 'done', standards: 'done' }
12
-
13
- // A repo with main (a.txt, b.txt) and a feature branch changing both, reviewed at `reviewed`.
14
- function fixture(t) {
15
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rereview-scope-'))
16
- const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'rereview-prior-'))
17
- t.after(() => { fs.rmSync(dir, { recursive: true, force: true }); fs.rmSync(outside, { recursive: true, force: true }) })
18
- const git = (...args) => execFileSync('git', args, { cwd: dir, encoding: 'utf8' }).trim()
19
- const write = (file, text) => fs.writeFileSync(path.join(dir, file), text)
20
- const commit = (msg) => { git('add', '-A'); git('commit', '-qm', msg); return git('rev-parse', 'HEAD') }
21
- git('init', '-q', '-b', 'main')
22
- git('config', 'user.email', 't@example.com')
23
- git('config', 'user.name', 'T')
24
- write('a.txt', 'a1\na2\na3\na4\n'); write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8\n'); write('c.txt', 'c1\n')
25
- const base = commit('base')
26
- git('checkout', '-qb', 'feature')
27
- write('a.txt', 'A1\nA2\nA3\na4\n'); write('b.txt', 'B1\nB2\nB3\nb4\nb5\nb6\nb7\nb8\n')
28
- const reviewed = commit('feature')
29
- const context = spawnSync('node', [path.join(path.dirname(SCRIPT), 'pr-context.mjs'), '--base', base, '--head', reviewed], { cwd: dir, encoding: 'utf8' })
30
- assert.equal(context.status, 0, context.stderr)
31
- const patchFingerprint = JSON.parse(context.stdout).patchFingerprint
32
- const prior = (extra = {}) => {
33
- const file = path.join(outside, 'prior.json')
34
- fs.writeFileSync(file, JSON.stringify({ version: 1, round: 1, head: reviewed, mergeBase: base, patchFingerprint, passes: DONE, commands: ['node --test'], findings: [{ id: 'R1', severity: 'P1', summary: 's' }, { id: 'R2', severity: 'P3', summary: 's', followUpUrl: 'https://github.com/o/r/issues/9' }], ...extra }))
35
- return file
36
- }
37
- const run = (priorFile, base = 'main') => {
38
- const r = spawnSync('node', [SCRIPT, '--prior', priorFile, '--base', base], { cwd: dir, encoding: 'utf8' })
39
- return { status: r.status, out: r.status === 0 ? JSON.parse(r.stdout) : null, stderr: r.stderr }
40
- }
41
- return { dir, git, write, commit, base, reviewed, prior, run }
42
- }
43
-
44
- test('a small fix to one PR file is a delta review: that file in full, the other carried', (t) => {
45
- const f = fixture(t)
46
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
47
- const { status, out } = f.run(f.prior())
48
- assert.equal(status, 0)
49
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
50
- assert.deepEqual(out.touchedFiles, ['a.txt'])
51
- assert.deepEqual(out.carriedFiles, ['b.txt'])
52
- assert.deepEqual(out.openFindings.map((f) => f.id), ['R1'])
53
- assert.match(out.fixDiffCommand, /^git diff [0-9a-f]{40} [0-9a-f]{40} -- a\.txt$/)
54
- })
55
-
56
- test('a rebase whose base change touches no PR file stays a delta', (t) => {
57
- const f = fixture(t)
58
- f.git('checkout', '-q', 'main'); f.write('c.txt', 'c2\n'); f.commit('base moves elsewhere')
59
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
60
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
61
- const { out } = f.run(f.prior())
62
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
63
- assert.deepEqual(out.touchedFiles, ['a.txt'])
64
- })
65
-
66
- test('main moving under a PR file without a conflict, while a P1 is open, reviews only the merge diff', (t) => {
67
- const f = fixture(t)
68
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8-base\n'); f.commit('base moves under b')
69
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
70
- const { out } = f.run(f.prior())
71
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
72
- assert.deepEqual(out.mergeFiles, ['b.txt'])
73
- assert.deepEqual(out.touchedFiles, ['b.txt'])
74
- assert.equal(out.fixLines, 0)
75
- assert.match(out.mergeDiffCommand, /^git diff [0-9a-f]{40} [0-9a-f]{40} -- b\.txt$/)
76
- assert.match(out.fixDiff, /\+b8-base/)
77
- })
78
-
79
- test('a small resolution in a file main moved under receives an integration delta', (t) => {
80
- const f = fixture(t)
81
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8-base\n'); f.commit('base moves under b')
82
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
83
- f.write('b.txt', 'B1\nB2-resolved\nB3\nb4\nb5\nb6\nb7\nb8-base\n'); f.commit('resolve')
84
- const { out } = f.run(f.prior())
85
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
86
- assert.deepEqual(out.integrationFiles, ['b.txt'])
87
- assert.deepEqual(out.touchedFiles, ['b.txt'])
88
- assert.deepEqual(out.carriedFiles, ['a.txt'])
89
- assert.equal(out.fixLines, 0)
90
- assert.equal(out.integrationLines, 2)
91
- assert.match(out.fixDiff, /\+B2-resolved/)
92
- assert.match(out.mergeDiffCommand, /-- b\.txt$/)
93
- })
94
-
95
- test('imported main lines do not inflate a small integration change', (t) => {
96
- const f = fixture(t)
97
- const imported = Array.from({ length: 100 }, (_, i) => `base-${i}`).join('\n') + '\n'
98
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8\n' + imported); f.commit('large base addition')
99
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
100
- f.write('b.txt', 'B1\nB2-resolved\nB3\nb4\nb5\nb6\nb7\nb8\n' + imported); f.commit('small resolution')
101
- const { out } = f.run(f.prior())
102
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
103
- assert.equal(out.integrationLines, 2)
104
- assert.equal(out.fixLinesSinceFull, 2)
105
- assert.match(out.fixDiff, /\+base-99/)
106
- })
107
-
108
- test('broad integration changes still require a full review', (t) => {
109
- const f = fixture(t)
110
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8-base\n'); f.commit('base moves under b')
111
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
112
- f.write('b.txt', 'Y1\nY2\nY3\ny4\nb5\nb6\nb7\nb8-base\n'); f.commit('broad resolution')
113
- const { out } = f.run(f.prior())
114
- assert.equal(out.tier, 'full')
115
- assert.ok(out.reasons.some((r) => r.includes('more than 50%')), out.reasons.join('; '))
116
- })
117
-
118
- test('moving a PR edit between functions over a changed base counts as integration impact', (t) => {
119
- const f = fixture(t)
120
- const source = (publicEnabled, adminEnabled, tail = '') =>
121
- `def public():\n public_only = 1\n enabled = ${publicEnabled}\n return enabled\n\ndef admin():\n admin_only = 2\n enabled = ${adminEnabled}\n return enabled\n${tail}`
122
- f.git('checkout', '-q', 'main'); f.write('logic.py', source('False', 'False')); f.commit('function base')
123
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
124
- // Isolate the PR to a single two-line edit so moving it changes its entire scope.
125
- f.write('a.txt', 'a1\na2\na3\na4\n'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8\n')
126
- f.write('logic.py', source('True', 'False'))
127
- const head = f.commit('enable public')
128
- const prior = f.prior({ head, mergeBase: f.git('merge-base', 'main', 'HEAD'), findings: [] })
129
- f.git('checkout', '-q', 'main'); f.write('logic.py', source('False', 'False', '# imported main comment\n')); f.commit('move base')
130
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
131
- f.write('logic.py', source('False', 'True', '# imported main comment\n')); f.commit('move edit to admin')
132
- const { out } = f.run(prior)
133
- assert.equal(out.samePatch, false)
134
- assert.equal(out.prLines, 2)
135
- assert.ok(out.integrationLines > out.prLines / 2)
136
- assert.equal(out.tier, 'full')
137
- })
138
-
139
- test('an actual rebase conflict can stay delta after a bounded resolution', (t) => {
140
- const f = fixture(t)
141
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2-base\nb3\nb4\nb5\nb6\nb7\nb8\n'); f.commit('competing base edit')
142
- f.git('checkout', '-q', 'feature')
143
- const rebase = spawnSync('git', ['rebase', 'main'], { cwd: f.dir, encoding: 'utf8' })
144
- assert.equal(rebase.status, 1)
145
- f.write('b.txt', 'B1\nB2-with-base\nB3\nb4\nb5\nb6\nb7\nb8\n')
146
- f.git('add', 'b.txt'); f.git('-c', 'core.editor=true', 'rebase', '--continue')
147
- const { out } = f.run(f.prior())
148
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
149
- assert.deepEqual(out.integrationFiles, ['b.txt'])
150
- assert.equal(out.integrationLines, 4)
151
- assert.match(out.fixDiff, /\+B2-with-base/)
152
- })
153
-
154
- test('a PR file removed from the current net diff remains in review scope', (t) => {
155
- const f = fixture(t)
156
- f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8\n'); f.commit('remove reviewed change')
157
- const { out } = f.run(f.prior())
158
- assert.equal(out.tier, 'full')
159
- assert.ok(out.touchedFiles.includes('b.txt'))
160
- assert.ok(out.reasons.some((r) => r.includes('more than 50%')))
161
- })
162
-
163
- test('unbounded integration impact fails closed, while reordered patch lines count as changes', () => {
164
- const patch = (lines) => `diff --git a/x b/x\n--- a/x\n+++ b/x\n@@ -1 +1 @@\n${lines.join('\n')}\n`
165
- assert.equal(patchImpactLines(patch(['-old', '+one', '+two']), patch(['-old', '+two', '+one'])), 2)
166
- assert.equal(patchImpactLines(patch(['-old', '+new']), patch(['-old', '+new']).replace('@@ -1 +1 @@', '@@ -20 +20 @@')), 4)
167
- assert.equal(patchImpactLines('Binary files a/x and b/x differ', patch(['+new'])), null)
168
- const large = Array.from({ length: 2001 }, (_, i) => `+line-${i}`)
169
- assert.equal(patchImpactLines(patch(large), patch([...large].reverse())), null)
170
- const out = decideScope({ prior: { round: 1, head: 'h', mergeBase: 'm', passes: DONE, commands: ['node --test'] }, round: 2, prFiles: ['x'], touchedFiles: ['x'], baseMovedFiles: ['x'], conflictFiles: ['x'], fixLines: 0, integrationLines: null, prLines: 10000, priorHeadReachable: true })
171
- assert.equal(out.tier, 'full')
172
- assert.ok(out.reasons.some((r) => r.includes('cannot be bounded')))
173
- })
174
-
175
- test('integration anchors count partial relocations but normalize imported base offsets', () => {
176
- const header = 'diff --git a/x b/x\n--- a/x\n+++ b/x\n'
177
- const hunk = (line, from, to) => `@@ -${line} +${line} @@\n-${from}\n+${to}\n`
178
- const before = header + hunk(1, 'old', 'new') + hunk(10, 'value', 'one')
179
- const relocated = header + hunk(2, 'old', 'new') + hunk(10, 'value', 'two')
180
- assert.equal(patchImpactLines(before, relocated), 6)
181
- const rebased = header + hunk(4, 'old', 'new') + hunk(13, 'value', 'two')
182
- const baseDiff = header + '@@ -0,0 +1,3 @@\n+base1\n+base2\n+base3\n'
183
- assert.equal(patchImpactLines(before, rebased, baseDiff), 2)
184
- const afterDeletion = header + hunk(1, 'old', 'new') + hunk(8, 'value', 'two')
185
- const deletedBase = header + '@@ -5,2 +4,0 @@\n-base1\n-base2\n'
186
- assert.equal(patchImpactLines(before, afterDeletion, deletedBase), 2)
187
- })
188
-
189
- test('own lines stay unchanged only at the same places: kept-both conflicts yes, relocations and base rewrites no', () => {
190
- const header = 'diff --git a/x b/x\n--- a/x\n+++ b/x\n'
191
- const reviewed = header + '@@ -4,0 +5 @@ a4\n+mine\n'
192
- const sibling = header + '@@ -4,0 +5 @@ a4\n+sibling\n'
193
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -5,0 +6 @@ sibling\n+mine\n', sibling), true, 'after the base\'s insertion')
194
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -4,0 +5 @@ a4\n+mine\n', sibling), true, 'before it')
195
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -6,0 +7 @@ a6\n+mine\n', sibling), false, 'moved one line further')
196
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -5,0 +6 @@ sibling\n+mine2\n', sibling), false, 'its own line changed')
197
- const rewrite = header + '@@ -5 +5 @@\n-sibling\n+mine\n'
198
- assert.equal(ownLinesUnchanged(reviewed, rewrite, sibling), false, 'replacing a base line is a change')
199
- const shifted = header + '@@ -1,0 +1,2 @@\n+top1\n+top2\n'
200
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -6,0 +7 @@ a4\n+mine\n', shifted), true, 'base lines above only shift it')
201
- assert.equal(ownLinesUnchanged('Binary files a/x and b/x differ', reviewed), false)
202
- const block = header + '@@ -4,0 +5,3 @@ a4\n+if (admin) {\n+ grant()\n+}\n'
203
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -5,0 +6 @@ if (admin) {\n+mine\n', block), false, 'inside the base block, not at its edge')
204
- assert.equal(ownLinesUnchanged(reviewed, header + '@@ -7,0 +8 @@ }\n+mine\n', block), true, 'after the whole block')
205
- const pair = header + '@@ -4,0 +5,2 @@ a4\n+lock\n+unlock\n'
206
- const split = header + '@@ -4,0 +5 @@ a4\n+lock\n@@ -5,0 +7 @@ sibling\n+unlock\n'
207
- assert.equal(ownLinesUnchanged(pair, split, sibling), false, 'a reviewed hunk split around base lines')
208
- const strict = header + '@@ -0,0 +1 @@\n+use strict\n'
209
- const imports = header + '@@ -0,0 +1,2 @@\n+import a\n+import b\n'
210
- assert.equal(ownLinesUnchanged(strict, header + '@@ -1,0 +2 @@ import a\n+use strict\n', imports), false, 'between the base\'s new lines at the file start')
211
- assert.equal(ownLinesUnchanged(strict, strict, imports), true, 'still before them')
212
- })
213
-
214
- test('an equivalent rebase after a passing review skips the reviewer, and the verdict CLI re-derives the skip', (t) => {
215
- const f = fixture(t)
216
- const prior = f.prior({ unit: '1201', round: 7, fixRound: 1, findings: [], commands: ['node --test'] })
217
- f.git('checkout', '-q', 'main'); f.write('c.txt', 'c2\n'); f.commit('unrelated base update')
218
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
219
- const reviews = fs.mkdtempSync(path.join(os.tmpdir(), 'rereview-skip-'))
220
- t.after(() => fs.rmSync(reviews, { recursive: true, force: true }))
221
- const file = path.join(reviews, 'review-r8.json')
222
- const scope = spawnSync('node', [SCRIPT, '--prior', prior, '--base', 'main', '--write-skip', file], { cwd: f.dir, encoding: 'utf8' })
223
- assert.equal(scope.status, 0, scope.stderr)
224
- const out = JSON.parse(scope.stdout)
225
- assert.equal(out.tier, 'skip', out.reasons.join('; '))
226
- assert.equal(out.samePatch, true)
227
- assert.equal(out.round, 8)
228
- assert.equal(out.fixRound, 1)
229
- assert.deepEqual(out.touchedFiles, [])
230
- assert.deepEqual(out.baseMovedFiles, ['c.txt'])
231
- assert.equal(out.skipRecord, file)
232
- const record = JSON.parse(fs.readFileSync(file, 'utf8'))
233
- assert.equal(record.tier, 'skip')
234
- assert.equal(record.carriedFrom, f.reviewed)
235
- assert.equal(record.head, out.head)
236
- assert.deepEqual(record.commands, [], 'a skip runs no checks and claims none')
237
- assert.deepEqual(record.findings, [])
238
-
239
- const verdict = path.resolve(path.dirname(SCRIPT), '../../resolve-issues/scripts/review-verdict.mjs')
240
- const run = (...args) => spawnSync('node', [verdict, '--review', file, '--prior', prior, ...args], { cwd: f.dir, encoding: 'utf8' })
241
- const result = run('--head', out.head, '--base', 'main')
242
- assert.equal(result.status, 0, result.stderr)
243
- assert.equal(JSON.parse(result.stdout).reviewedHead, out.head)
244
- assert.equal(JSON.parse(result.stdout).tier, 'skip')
245
- assert.equal(JSON.parse(result.stdout).fixRound, 1)
246
- assert.equal(run('--head', out.head).status, 2, 'a skip record is never taken on its word: --base is required')
247
- const bound = run('--head', f.reviewed, '--base', 'main')
248
- assert.equal(bound.status, 0, bound.stderr)
249
- assert.equal(JSON.parse(bound.stdout).verdict, 'review-again', 'a skip record is bound to its HEAD')
250
-
251
- // A skip record forged over a real change fails the rerun: the reviewer must run.
252
- f.write('a.txt', 'A1\nA2-changed\nA3\na4\n'); const changed = f.commit('real change')
253
- fs.writeFileSync(file, JSON.stringify({ ...record, head: changed }))
254
- const forged = run('--head', changed, '--base', 'main')
255
- assert.equal(forged.status, 0, forged.stderr)
256
- assert.equal(JSON.parse(forged.stdout).verdict, 'review-again')
257
- assert.match(JSON.parse(forged.stdout).reason, /picks "delta"/, forged.stdout)
258
-
259
- // The skip record is a valid prior: the next fix gets a delta against it.
260
- fs.writeFileSync(file, JSON.stringify(record))
261
- const next = f.run(file)
262
- assert.equal(next.out.tier, 'delta', next.out.reasons.join('; '))
263
- assert.equal(next.out.round, 9)
264
- assert.deepEqual(next.out.touchedFiles, ['a.txt'])
265
- })
266
-
267
- test('a skip needs a passing prior; a base moving under a PR file still skips while the PR\'s own lines are unchanged', (t) => {
268
- const f = fixture(t)
269
- f.git('commit', '-q', '--amend', '-m', 'reworded')
270
- assert.equal(f.run(f.prior()).out.tier, 'delta', 'an open P1 is never carried: it is fixed')
271
- assert.equal(f.run(f.prior({ findings: [] })).out.tier, 'skip')
272
- assert.equal(f.run(f.prior({ findings: undefined })).out.tier, 'delta', 'a prior with no findings array proves no pass')
273
- const passedAt = f.git('rev-parse', 'HEAD')
274
- assert.equal(f.run(f.prior({ findings: [], head: passedAt })).out.tier, 'skip', 'same HEAD: nothing to review')
275
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8-base\n'); f.commit('base moves under b')
276
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
277
- const { out } = f.run(f.prior({ findings: [], head: passedAt }))
278
- assert.equal(out.tier, 'skip', 'the unit\'s own changes are the reviewed ones; CI on the new HEAD proves the fit')
279
- assert.deepEqual(out.mergeFiles, ['b.txt'])
280
- assert.throws(() => skipRecord({ head: 'h' }, f.run(f.prior()).out), RereviewScopeError)
281
- })
282
-
283
- test('a rebase conflict resolved by keeping both sides skips; one that rewrites a PR line is reviewed', (t) => {
284
- // wiki-kb #12: six of ten extra review rounds only re-checked sibling registrations a rebase
285
- // added beside the unit's own, unchanged lines.
286
- const f = fixture(t)
287
- f.git('checkout', '-q', 'main'); f.write('a.txt', 'a1\na2\na3\na4\nsibling\n'); f.commit('sibling registration')
288
- f.git('checkout', '-q', 'feature')
289
- f.write('a.txt', 'A1\nA2\nA3\na4\nmine\n'); const reviewed = f.commit('own registration')
290
- const prior = f.prior({ findings: [], head: reviewed })
291
- const rebase = spawnSync('git', ['rebase', 'main'], { cwd: f.dir, encoding: 'utf8' })
292
- assert.equal(rebase.status, 1, 'both sides appended at the same place')
293
- f.write('a.txt', 'A1\nA2\nA3\na4\nsibling\nmine\n')
294
- f.git('add', 'a.txt'); f.git('-c', 'core.editor=true', 'rebase', '--continue')
295
- assert.equal(f.run(prior).out.tier, 'skip')
296
- f.write('a.txt', 'A1\nA2\nA3\na4\nsibling\nmine-and-sibling\n'); f.commit('resolution rewrites the unit\'s line')
297
- const { out } = f.run(prior)
298
- assert.equal(out.tier, 'delta')
299
- assert.ok(out.touchedFiles.includes('a.txt'))
300
- })
301
-
302
- test('a completed full retry replaces incomplete coverage without consuming a fix cycle', (t) => {
303
- const f = fixture(t)
304
- const prior = f.prior({ unit: '1201', fixRound: 1, complete: false, commands: [], findings: [{ id: 'R1', severity: 'P1', summary: 'unconfirmed blocker' }], passes: { ...DONE, tests: 'partial' } })
305
- const { out } = f.run(prior)
306
- assert.equal(out.tier, 'full')
307
- assert.equal(out.round, 2)
308
- assert.equal(out.fixRound, 1)
309
- const file = path.join(f.dir, 'review.json')
310
- fs.writeFileSync(file, JSON.stringify({ ...out, version: 1, unit: '1201', complete: true, passes: DONE, commands: ['git diff --check main...HEAD'], findings: [] }))
311
- const verdict = path.resolve(path.dirname(SCRIPT), '../../resolve-issues/scripts/review-verdict.mjs')
312
- const result = spawnSync('node', [verdict, '--review', file, '--head', out.head, '--prior', prior], { cwd: f.dir, encoding: 'utf8' })
313
- assert.equal(result.status, 0, result.stderr)
314
- assert.equal(JSON.parse(result.stdout).fixRound, 1)
315
- })
316
-
317
- test('missing check evidence or follow-ups alone force full retry and preserve the fix count', (t) => {
318
- const f = fixture(t)
319
- const verdict = path.resolve(path.dirname(SCRIPT), '../../resolve-issues/scripts/review-verdict.mjs')
320
- for (const extra of [{ commands: [] }, { findings: [{ id: 'R1', severity: 'P1', summary: 'blocker' }, { id: 'R2', severity: 'P2', summary: 'deferred' }] }]) {
321
- const prior = f.prior({ unit: '1201', round: 3, fixRound: 3, commands: ['node --test'], ...extra })
322
- const { out } = f.run(prior)
323
- assert.equal(out.tier, 'full')
324
- assert.equal(out.fixRound, 3)
325
- // The verdict's stall guard reads every earlier round, so name the files <unit>-r<N>.json.
326
- const rounds = path.dirname(prior)
327
- for (const round of [1, 2]) fs.writeFileSync(path.join(rounds, `1201-r${round}.json`), JSON.stringify({ version: 1, unit: '1201', round, head: out.head, passes: DONE, commands: ['node --test'], findings: [] }))
328
- const priorFile = path.join(rounds, '1201-r3.json')
329
- fs.copyFileSync(prior, priorFile)
330
- const file = path.join(rounds, '1201-r4.json')
331
- fs.writeFileSync(file, JSON.stringify({ ...out, version: 1, unit: '1201', complete: true, passes: DONE, commands: ['git diff --check main...HEAD'], findings: [{ id: 'R1', severity: 'P1', summary: 'confirmed blocker', trigger: 'A2 reverts on reload', rule: 'a.txt line 2 stays fixed', paths: [{ path: 'a.txt:2', status: 'open' }] }] }))
332
- const result = spawnSync('node', [verdict, '--review', file, '--head', out.head, '--prior', priorFile], { cwd: f.dir, encoding: 'utf8' })
333
- assert.equal(result.status, 0, result.stderr)
334
- assert.equal(JSON.parse(result.stdout).verdict, 'fix')
335
- assert.equal(JSON.parse(result.stdout).action, 'fix')
336
- assert.equal(JSON.parse(result.stdout).fixRound, 3)
337
- }
338
- })
339
-
340
- test('scope advances the fix counter only after blocking findings', (t) => {
341
- const f = fixture(t)
342
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
343
- assert.equal(f.run(f.prior({ round: 7, fixRound: 2 })).out.fixRound, 3)
344
- assert.equal(f.run(f.prior({ round: 7, fixRound: 2, findings: [] })).out.fixRound, 2)
345
- assert.equal(f.run(f.prior({ round: 7, fixRound: 0 })).out.tier, 'full')
346
- })
347
-
348
- test('whitespace-changing code is never classified as an equivalent patch', (t) => {
349
- const f = fixture(t)
350
- f.git('checkout', '-q', 'main')
351
- f.write('logic.py', 'def value():\n if True:\n return 1\n return 2\n'); f.commit('python base')
352
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
353
- f.write('logic.py', 'def value():\n if False:\n return 1\n return 2\n')
354
- const head = f.commit('python feature')
355
- const prior = f.prior({ head, mergeBase: f.git('merge-base', 'main', 'HEAD'), findings: [] })
356
- f.write('logic.py', 'def value():\n if False:\n return 1\n return 2\n'); f.commit('changed indentation')
357
- const { out } = f.run(prior)
358
- assert.equal(out.samePatch, false)
359
- assert.ok(out.touchedFiles.includes('logic.py'))
360
- })
361
-
362
- test('a binary content change cannot carry an equivalent-patch approval', (t) => {
363
- const f = fixture(t)
364
- f.git('checkout', '-q', 'main'); f.write('image.bin', Buffer.from([0, 1, 2, 3])); f.commit('binary base')
365
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
366
- f.write('image.bin', Buffer.from([0, 4, 2, 3]))
367
- const head = f.commit('binary feature')
368
- const prior = f.prior({ head, mergeBase: f.git('merge-base', 'main', 'HEAD'), findings: [] })
369
- f.write('image.bin', Buffer.from([0, 5, 2, 3])); f.commit('changed binary')
370
- const { out } = f.run(prior)
371
- assert.equal(out.samePatch, false)
372
- assert.ok(out.touchedFiles.includes('image.bin'))
373
- })
374
-
375
- test('moving an identical edit to a different function never carries approval', (t) => {
376
- const f = fixture(t)
377
- const source = (publicEnabled, adminEnabled) => ['public', 'admin'].map((name, i) =>
378
- `def ${name}():\n one = 1\n two = 2\n three = 3\n enabled = ${i === 0 ? publicEnabled : adminEnabled}\n four = 4\n five = 5\n return enabled\n`).join('\n')
379
- f.git('checkout', '-q', 'main'); f.write('logic.py', source('False', 'False')); f.commit('repeated context base')
380
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
381
- f.write('logic.py', source('True', 'False'))
382
- const head = f.commit('reviewed public change')
383
- const mergeBase = f.git('merge-base', 'main', 'HEAD')
384
- const prior = f.prior({ head, mergeBase, findings: [] })
385
- const original = f.run(prior).out
386
- f.write('logic.py', source('False', 'True')); f.commit('move change to admin')
387
- const { out } = f.run(prior)
388
- assert.equal(out.patchId, original.patchId, 'this is a real patch-id collision')
389
- assert.notEqual(out.patchFingerprint, original.patchFingerprint)
390
- assert.equal(out.samePatch, false)
391
- assert.ok(out.touchedFiles.includes('logic.py'))
392
- assert.ok(out.fixLines > 0)
393
- const gone = f.prior({ head: '0'.repeat(40), mergeBase, patchId: original.patchId, patchFingerprint: original.patchFingerprint, findings: [] })
394
- assert.equal(f.run(gone).out.tier, 'full')
395
- })
396
-
397
- test('an unknown previous merge-base forces full', (t) => {
398
- const f = fixture(t)
399
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
400
- const { out } = f.run(f.prior({ mergeBase: '1'.repeat(40) }))
401
- assert.equal(out.tier, 'full')
402
- assert.ok(out.reasons.some((r) => r.includes('merge-base')), out.reasons.join('; '))
403
- })
404
-
405
- test('fixes that rewrite more than half the PR force full', (t) => {
406
- const f = fixture(t)
407
- f.write('a.txt', 'X1\nX2\nX3\nx4\n'); f.write('b.txt', 'Y1\nY2\nY3\ny4\nb5\nb6\nb7\nb8\n'); f.commit('rewrite')
408
- const { out } = f.run(f.prior())
409
- assert.equal(out.tier, 'full')
410
- assert.ok(out.reasons.some((r) => r.includes('more than 50%')), out.reasons.join('; '))
411
- })
412
-
413
- test('a late round after a delta stays a delta for a small fix: round number and prior scope never force full', (t) => {
414
- const f = fixture(t)
415
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
416
- const { out } = f.run(f.prior({ round: 4, scope: 'delta', fixLinesSinceFull: 2 }))
417
- assert.equal(out.round, 5)
418
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
419
- assert.equal(out.fixLinesSinceFull, 4)
420
- })
421
-
422
- test('small fixes that add up to more than half the PR since the last full review force full', (t) => {
423
- const f = fixture(t)
424
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
425
- // The PR is 12 changed lines; 5 earlier delta lines plus this round's 2 is over half.
426
- const { out } = f.run(f.prior({ round: 3, scope: 'delta', fixLinesSinceFull: 5 }))
427
- assert.equal(out.tier, 'full')
428
- assert.ok(out.reasons.some((r) => r.includes('since the last full review changed 7 of 12')), out.reasons.join('; '))
429
- assert.equal(out.fixLinesSinceFull, 0)
430
- })
431
-
432
- test('a content-equivalent rebase reuses the review even when the reviewed HEAD is gone', (t) => {
433
- const f = fixture(t)
434
- const reviewedPatch = execFileSync('git', ['patch-id', '--verbatim'], { cwd: f.dir, encoding: 'utf8', input: f.git('diff', `${f.base}...${f.reviewed}`) + '\n' }).trim().split(/\s+/)[0]
435
- f.git('checkout', '-q', 'main'); f.write('c.txt', 'c2\n'); f.commit('base moves elsewhere')
436
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
437
- const gone = '0'.repeat(40)
438
- const { out } = f.run(f.prior({ head: gone, patchId: reviewedPatch }))
439
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
440
- assert.equal(out.samePatch, true)
441
- assert.equal(out.patchId, reviewedPatch)
442
- assert.deepEqual(out.touchedFiles, [])
443
- assert.deepEqual(out.carriedFiles, ['a.txt', 'b.txt'])
444
- assert.equal(out.fixDiff, '')
445
- assert.deepEqual(out.openFindings.map((x) => x.id), ['R1'])
446
- const legacy = f.prior({ head: gone, patchId: reviewedPatch, patchFingerprint: undefined })
447
- assert.equal(f.run(legacy).out.tier, 'full', 'patch-id alone cannot prove edit locations')
448
- })
449
-
450
- test('a rebase that changes the PR patch with the reviewed HEAD gone forces full', (t) => {
451
- const f = fixture(t)
452
- const reviewedPatch = execFileSync('git', ['patch-id', '--verbatim'], { cwd: f.dir, encoding: 'utf8', input: f.git('diff', `${f.base}...${f.reviewed}`) + '\n' }).trim().split(/\s+/)[0]
453
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
454
- const { out } = f.run(f.prior({ head: '0'.repeat(40), patchId: reviewedPatch }))
455
- assert.equal(out.tier, 'full')
456
- assert.equal(out.samePatch, false)
457
- })
458
-
459
- test('a content-equivalent rebase over a base that moved under a PR file, with the reviewed HEAD gone, reviews the merge diff', (t) => {
460
- const f = fixture(t)
461
- const reviewedPatch = execFileSync('git', ['patch-id', '--verbatim'], { cwd: f.dir, encoding: 'utf8', input: f.git('diff', `${f.base}...${f.reviewed}`) + '\n' }).trim().split(/\s+/)[0]
462
- f.git('checkout', '-q', 'main'); f.write('b.txt', 'b1\nb2\nb3\nb4\nb5\nb6\nb7\nb8-base\n'); f.commit('base moves under b')
463
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
464
- const { out } = f.run(f.prior({ head: '0'.repeat(40), patchId: reviewedPatch }))
465
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
466
- assert.equal(out.samePatch, true)
467
- assert.deepEqual(out.touchedFiles, ['b.txt'])
468
- assert.match(out.fixDiff, /\+b8-base/)
469
- })
470
-
471
- test('a metadata-only commit (same tree, new message) is a delta with nothing to review', (t) => {
472
- const f = fixture(t)
473
- f.git('commit', '-q', '--amend', '-m', 'feature, reworded')
474
- const { out } = f.run(f.prior())
475
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
476
- assert.deepEqual(out.touchedFiles, [])
477
- assert.equal(out.fixDiff, '')
478
- })
479
-
480
- test('a previous round that did not cover the whole diff forces full', (t) => {
481
- const f = fixture(t)
482
- f.write('a.txt', 'A1\nA2-fixed\nA3\na4\n'); f.commit('fix')
483
- for (const extra of [{ complete: false }, { oversize: true }, { passes: { ...DONE, correctness: 'partial' } }, { passes: undefined }, { tier: 'delta' }, { scope: 'delta' }, { tier: 'skip' }, { tier: 'partial' }, { scope: 'partial' }]) {
484
- assert.equal(f.run(f.prior(extra)).out.tier, 'full', JSON.stringify(extra))
485
- }
486
- assert.equal(f.run(f.prior({ passes: { ...DONE, spec: 'skipped: no spec found' } })).out.tier, 'delta')
487
- })
488
-
489
- test('an unknown previous HEAD forces full, and an unreadable prior is a usage error', (t) => {
490
- const f = fixture(t)
491
- const { out } = f.run(f.prior({ head: '0'.repeat(40) }))
492
- assert.equal(out.tier, 'full')
493
- assert.deepEqual(out.carriedFiles, [])
494
- assert.equal(f.run(path.join(f.dir, 'missing.json')).status, 2)
495
- })
496
-
497
- test('priorProblems names every gap', () => {
498
- assert.deepEqual(priorProblems({ round: 1, head: 'h', mergeBase: 'm', passes: DONE, commands: ['node --test'] }), [])
499
- assert.equal(priorProblems({}).length, 3 + 2)
500
- })
501
-
502
- test('carry-forward refuses unfinished skips and unresolved incidents through the scope CLI', (t) => {
503
- const f = fixture(t)
504
- f.git('commit', '-q', '--amend', '-m', 'metadata-only change')
505
- const incomplete = [
506
- ...['skipped: timeout', 'skipped: not applicable: sampled due to token budget',
507
- 'skipped: not applicable: budget exhausted', 'skipped: not applicable: not reviewed'].map((status) =>
508
- ({ passes: { ...DONE, correctness: status } })),
509
- ...[{ status: 'open' }, {}, 'resolved'].map((executionIncident) => ({ executionIncident })),
510
- { commands: [] },
511
- { commands: undefined },
512
- ]
513
- for (const extra of incomplete) {
514
- const { status, out } = f.run(f.prior({ findings: [], ...extra }))
515
- assert.equal(status, 0)
516
- assert.equal(out.samePatch, true)
517
- assert.equal(out.tier, 'full', JSON.stringify(extra))
518
- assert.ok(out.reasons.some((reason) => /unfinished|executionIncident|no checks/.test(reason)))
519
- }
520
- for (const status of ['skipped: no spec found', 'skipped: not applicable: prose-only change']) {
521
- assert.equal(f.run(f.prior({ findings: [], passes: { ...DONE, spec: status }, executionIncident: { status: 'resolved' } })).out.tier, 'skip')
522
- }
523
- })
524
-
525
- test('decideScope keeps PR order and splits touched from carried', () => {
526
- const d = decideScope({ prior: { round: 1, head: 'h', mergeBase: 'm', passes: DONE, commands: ['node --test'] }, round: 2, prFiles: ['x', 'y', 'z'], touchedFiles: ['z', 'x'], baseMovedFiles: ['other'], fixLines: 5, prLines: 100, priorHeadReachable: true })
527
- assert.equal(d.tier, 'delta')
528
- assert.deepEqual(d.touchedFiles, ['x', 'z'])
529
- assert.deepEqual(d.carriedFiles, ['y'])
530
- })
531
-
532
- test('numstatLines counts binary files as one line', () => {
533
- assert.equal(numstatLines('3\t2\ta.txt\n-\t-\timg.png\n'), 6)
534
- })
535
-
536
- test('parseCliArgs requires --prior and --base', () => {
537
- assert.throws(() => parseCliArgs(['--prior', 'p.json']), RereviewScopeError)
538
- assert.throws(() => parseCliArgs(['--prior']), RereviewScopeError)
539
- assert.deepEqual(parseCliArgs(['--prior', 'p', '--base', 'main']), { prior: 'p', base: 'main', head: 'HEAD' })
540
- assert.equal(parseCliArgs(['--prior', 'p', '--base', 'main', '--write-skip', 'r2.json']).writeSkip, 'r2.json')
541
- })
542
-
543
- test('a delta hands over its setup: changed names, their call sites in carried files, and a fix-only scan', (t) => {
544
- const f = fixture(t)
545
- f.git('checkout', '-q', 'main')
546
- f.write('lib.js', 'export function parseAmount(s) { return Number(s) }\nexport const LIMIT = 10\n')
547
- f.write('caller.js', "import { parseAmount } from './lib.js'\nexport const total = parseAmount('5')\n")
548
- f.commit('code base')
549
- f.git('checkout', '-q', 'feature'); f.git('rebase', '-q', 'main')
550
- f.write('lib.js', 'export function parseAmount(s) { return Number(s.trim()) }\nexport const LIMIT = 10\n')
551
- f.write('caller.js', "import { parseAmount } from './lib.js'\nexport const total = parseAmount(' 5 ')\n")
552
- const reviewed = f.commit('feature code')
553
- const prior = f.prior({ head: reviewed, mergeBase: f.git('merge-base', 'main', 'HEAD'),
554
- sweeps: [{ rule: 'amounts are trimmed', points: [{ location: 'lib.js:1', enforced: true }, { location: 'caller.js:2', enforced: true }] }] })
555
- f.write('lib.js', 'export function parseAmount(s) {\n return Number(String(s).trim())\n}\nexport const LIMIT = 10\n')
556
- f.commit('fix')
557
- const { out } = f.run(prior)
558
- assert.equal(out.tier, 'delta', out.reasons.join('; '))
559
- assert.deepEqual(out.touchedFiles, ['lib.js'])
560
- assert.deepEqual(out.changedSymbols.names, ['parseAmount'])
561
- assert.deepEqual(out.callSites.sites.map((s) => `${s.file}:${s.line}`), ['caller.js:1', 'caller.js:2'])
562
- assert.deepEqual(out.sweepRecheck.map((p) => p.location), ['lib.js:1'])
563
- assert.ok(Array.isArray(out.fixScan.leads) && Array.isArray(out.fixScan.findings))
564
- assert.match(out.fixDiff, /^\+ return Number\(String\(s\)\.trim\(\)\)$/m)
565
- // --head selects the reviewed commit even when the checkout has moved on
566
- f.write('caller.js', "import { parseAmount } from './lib.js'\n"); f.commit('later commit drops a call site')
567
- const r = spawnSync('node', [SCRIPT, '--prior', prior, '--base', 'main', '--head', 'HEAD~1'], { cwd: f.dir, encoding: 'utf8' })
568
- assert.deepEqual(JSON.parse(r.stdout).callSites.sites.map((s) => `${s.file}:${s.line}`), ['caller.js:1', 'caller.js:2'])
569
- })
570
-
571
- test('changedSymbols reads added and removed lines only, and Markdown headings as anchors', () => {
572
- const diff = [
573
- '--- a/src/a.ts', '+++ b/src/a.ts',
574
- '+export async function loadUser(id) {', '-const staleCache = new Map()', ' function contextOnly() {}',
575
- '+ if (x) {', '+def python_helper(arg):', '+ const localOnly = 1',
576
- '--- a/src/a.test.ts', '+++ b/src/a.test.ts', '+const result = run()', '+function helperInTest() {}',
577
- '--- a/docs/x.md', '+++ b/docs/x.md', '+## Stacked PRs (`stack` mode)', '+plain prose with function words',
578
- ].join('\n')
579
- assert.deepEqual(changedSymbols(diff), { names: ['loadUser', 'python_helper', 'staleCache'], anchors: ['#stacked-prs-stack-mode'] })
580
- assert.equal(headingSlug('Fix loop'), 'fix-loop')
581
- })
582
-
583
- test('sweepRecheck keeps only points in touched files', () => {
584
- const prior = { sweeps: [{ rule: 'r', points: [{ location: 'a.js:3', enforced: false, finding: 'R1' }, { location: 'b.md:10-12', enforced: true }] }] }
585
- assert.deepEqual(sweepRecheck(prior, ['b.md']), [{ rule: 'r', location: 'b.md:10-12', enforced: true }])
586
- assert.deepEqual(sweepRecheck({}, ['a.js']), [])
587
- })