@hominis/fireforge 0.49.1 → 0.50.0

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 (336) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +28 -0
  10. package/dist/src/commands/check-report.js +78 -0
  11. package/dist/src/commands/check-run.d.ts +28 -0
  12. package/dist/src/commands/check-run.js +104 -0
  13. package/dist/src/commands/check-steps.d.ts +67 -0
  14. package/dist/src/commands/check-steps.js +175 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  19. package/dist/src/commands/doctor-source-pin.js +4 -1
  20. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  21. package/dist/src/commands/export-adopt-check.js +98 -0
  22. package/dist/src/commands/export-flow.js +29 -14
  23. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  24. package/dist/src/commands/export-shared.d.ts +2 -1
  25. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  26. package/dist/src/commands/export-staged-registration.js +91 -0
  27. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  28. package/dist/src/commands/forward-import-remedy.js +63 -0
  29. package/dist/src/commands/furnace/apply.js +3 -3
  30. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  32. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  33. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  34. package/dist/src/commands/furnace/create-templates.d.ts +8 -3
  35. package/dist/src/commands/furnace/create-templates.js +8 -23
  36. package/dist/src/commands/furnace/create.js +2 -2
  37. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  38. package/dist/src/commands/furnace/deploy-named.js +107 -0
  39. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  40. package/dist/src/commands/furnace/deploy.js +50 -86
  41. package/dist/src/commands/furnace/diff.js +26 -3
  42. package/dist/src/commands/furnace/index.js +66 -41
  43. package/dist/src/commands/furnace/init.js +13 -6
  44. package/dist/src/commands/furnace/override.js +4 -2
  45. package/dist/src/commands/furnace/preview.js +4 -3
  46. package/dist/src/commands/furnace/refresh.js +2 -2
  47. package/dist/src/commands/furnace/remove.js +4 -3
  48. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  49. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  50. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  51. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  52. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  53. package/dist/src/commands/furnace/rename-registrations.js +93 -0
  54. package/dist/src/commands/furnace/rename.d.ts +2 -1
  55. package/dist/src/commands/furnace/rename.js +28 -66
  56. package/dist/src/commands/furnace/scan.d.ts +9 -4
  57. package/dist/src/commands/furnace/scan.js +11 -9
  58. package/dist/src/commands/furnace/sync.js +3 -2
  59. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  60. package/dist/src/commands/furnace/validate-report.js +91 -0
  61. package/dist/src/commands/furnace/validate.d.ts +1 -1
  62. package/dist/src/commands/furnace/validate.js +127 -114
  63. package/dist/src/commands/furnace/validation-output.js +14 -3
  64. package/dist/src/commands/lint-per-patch.js +31 -60
  65. package/dist/src/commands/lint-report.d.ts +8 -4
  66. package/dist/src/commands/lint-report.js +15 -5
  67. package/dist/src/commands/lint.js +15 -34
  68. package/dist/src/commands/manifest.js +2 -0
  69. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  70. package/dist/src/commands/patch/delete.d.ts +1 -1
  71. package/dist/src/commands/patch/delete.js +13 -98
  72. package/dist/src/commands/patch/index.d.ts +1 -1
  73. package/dist/src/commands/patch/index.js +4 -2
  74. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  75. package/dist/src/commands/patch/lint-ignore.js +65 -28
  76. package/dist/src/commands/patch/move-files-create.js +26 -17
  77. package/dist/src/commands/patch/move-files-into.js +5 -3
  78. package/dist/src/commands/patch/move-files.js +3 -1
  79. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  80. package/dist/src/commands/patch/removal-dependents.js +133 -0
  81. package/dist/src/commands/patch/rename.js +7 -0
  82. package/dist/src/commands/patch/retire.d.ts +34 -0
  83. package/dist/src/commands/patch/retire.js +128 -0
  84. package/dist/src/commands/patch/split-plan.d.ts +7 -7
  85. package/dist/src/commands/patch/split-plan.js +45 -28
  86. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  87. package/dist/src/commands/patch/split-staged.js +202 -0
  88. package/dist/src/commands/patch/split.d.ts +5 -0
  89. package/dist/src/commands/patch/split.js +51 -15
  90. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  91. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  92. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  93. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  94. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  95. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  96. package/dist/src/commands/patch/staged-dependency.js +14 -41
  97. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  98. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  99. package/dist/src/commands/re-export-adjacent.d.ts +45 -2
  100. package/dist/src/commands/re-export-adjacent.js +156 -37
  101. package/dist/src/commands/re-export-drift.d.ts +36 -8
  102. package/dist/src/commands/re-export-drift.js +73 -49
  103. package/dist/src/commands/re-export-files.js +11 -8
  104. package/dist/src/commands/re-export-lint.d.ts +2 -1
  105. package/dist/src/commands/re-export-lint.js +23 -10
  106. package/dist/src/commands/re-export-options.js +45 -4
  107. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  108. package/dist/src/commands/re-export-preflight.js +160 -0
  109. package/dist/src/commands/re-export-register.js +42 -17
  110. package/dist/src/commands/re-export-removed.d.ts +12 -1
  111. package/dist/src/commands/re-export-removed.js +41 -11
  112. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  113. package/dist/src/commands/re-export-scan-owner.js +116 -0
  114. package/dist/src/commands/re-export-scan.d.ts +17 -9
  115. package/dist/src/commands/re-export-scan.js +56 -23
  116. package/dist/src/commands/re-export-single.js +22 -6
  117. package/dist/src/commands/re-export-summary.d.ts +28 -0
  118. package/dist/src/commands/re-export-summary.js +36 -0
  119. package/dist/src/commands/re-export.js +37 -24
  120. package/dist/src/commands/rebase/index.js +17 -17
  121. package/dist/src/commands/run.js +16 -2
  122. package/dist/src/commands/status-check.d.ts +55 -16
  123. package/dist/src/commands/status-check.js +92 -37
  124. package/dist/src/commands/status-json.d.ts +22 -9
  125. package/dist/src/commands/status-json.js +47 -32
  126. package/dist/src/commands/status-output.js +3 -1
  127. package/dist/src/commands/status-scope.d.ts +37 -0
  128. package/dist/src/commands/status-scope.js +54 -0
  129. package/dist/src/commands/status.js +66 -23
  130. package/dist/src/commands/test-browser-preflight.js +19 -3
  131. package/dist/src/commands/test-build-furnace.d.ts +21 -0
  132. package/dist/src/commands/test-build-furnace.js +48 -0
  133. package/dist/src/commands/test-build-phase.js +47 -41
  134. package/dist/src/commands/test-checkjs.js +7 -0
  135. package/dist/src/commands/test-diagnose.js +7 -0
  136. package/dist/src/commands/test-modes.d.ts +16 -1
  137. package/dist/src/commands/test-modes.js +24 -1
  138. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  139. package/dist/src/commands/test-multi-verdict.js +111 -0
  140. package/dist/src/commands/test-orchestrate.d.ts +43 -0
  141. package/dist/src/commands/test-orchestrate.js +222 -0
  142. package/dist/src/commands/test-perf-scope.d.ts +29 -0
  143. package/dist/src/commands/test-perf-scope.js +83 -0
  144. package/dist/src/commands/test-prelock.d.ts +51 -0
  145. package/dist/src/commands/test-prelock.js +177 -0
  146. package/dist/src/commands/test-register.js +54 -10
  147. package/dist/src/commands/test-run.js +4 -1
  148. package/dist/src/commands/test-stale-gate.js +57 -27
  149. package/dist/src/commands/test-verdict.d.ts +10 -1
  150. package/dist/src/commands/test-verdict.js +28 -1
  151. package/dist/src/commands/test.d.ts +6 -1
  152. package/dist/src/commands/test.js +66 -60
  153. package/dist/src/commands/token-lint.d.ts +14 -0
  154. package/dist/src/commands/token-lint.js +111 -0
  155. package/dist/src/commands/token.js +10 -0
  156. package/dist/src/commands/verify-scope.d.ts +28 -0
  157. package/dist/src/commands/verify-scope.js +67 -0
  158. package/dist/src/commands/verify.d.ts +10 -2
  159. package/dist/src/commands/verify.js +59 -35
  160. package/dist/src/core/build-baseline-types.d.ts +46 -0
  161. package/dist/src/core/build-baseline-validate.js +19 -0
  162. package/dist/src/core/build-baseline.d.ts +5 -1
  163. package/dist/src/core/build-baseline.js +3 -1
  164. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  165. package/dist/src/core/build-overwrite-guard.js +84 -4
  166. package/dist/src/core/build-prepare.d.ts +7 -0
  167. package/dist/src/core/build-prepare.js +14 -8
  168. package/dist/src/core/build-run-record.d.ts +74 -0
  169. package/dist/src/core/build-run-record.js +136 -0
  170. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  171. package/dist/src/core/checkjs-support-heads.js +104 -0
  172. package/dist/src/core/config-paths.d.ts +2 -2
  173. package/dist/src/core/config-paths.js +8 -0
  174. package/dist/src/core/config-validate-check.d.ts +8 -0
  175. package/dist/src/core/config-validate-check.js +143 -0
  176. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  177. package/dist/src/core/config-validate-file-size.js +91 -2
  178. package/dist/src/core/config-validate-history.d.ts +11 -0
  179. package/dist/src/core/config-validate-history.js +38 -0
  180. package/dist/src/core/config-validate-patch-policy.js +100 -0
  181. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  182. package/dist/src/core/config-validate-token-scales.d.ts +6 -0
  183. package/dist/src/core/config-validate-token-scales.js +50 -0
  184. package/dist/src/core/config-validate.js +14 -5
  185. package/dist/src/core/config.d.ts +5 -0
  186. package/dist/src/core/config.js +16 -0
  187. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  188. package/dist/src/core/coverage-dispatch.js +44 -0
  189. package/dist/src/core/coverage-extend.d.ts +70 -45
  190. package/dist/src/core/coverage-extend.js +199 -144
  191. package/dist/src/core/coverage-records.d.ts +97 -0
  192. package/dist/src/core/coverage-records.js +350 -0
  193. package/dist/src/core/destructive.d.ts +7 -4
  194. package/dist/src/core/destructive.js +10 -10
  195. package/dist/src/core/engine-session-lock.d.ts +10 -1
  196. package/dist/src/core/engine-session-lock.js +28 -8
  197. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  198. package/dist/src/core/export-moved-code-shape.js +135 -44
  199. package/dist/src/core/file-lock-queue.d.ts +67 -0
  200. package/dist/src/core/file-lock-queue.js +221 -0
  201. package/dist/src/core/file-lock.d.ts +4 -25
  202. package/dist/src/core/file-lock.js +105 -138
  203. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  204. package/dist/src/core/furnace-apply-output.js +31 -1
  205. package/dist/src/core/furnace-apply.js +12 -2
  206. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  207. package/dist/src/core/furnace-composed-source.js +167 -0
  208. package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
  209. package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
  210. package/dist/src/core/furnace-config-validate.js +8 -0
  211. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  212. package/dist/src/core/furnace-css-fragments.js +26 -0
  213. package/dist/src/core/furnace-operation.d.ts +3 -3
  214. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  215. package/dist/src/core/furnace-registration-remove.js +53 -17
  216. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  217. package/dist/src/core/furnace-registration-rename.js +77 -0
  218. package/dist/src/core/furnace-registration.d.ts +14 -4
  219. package/dist/src/core/furnace-registration.js +44 -10
  220. package/dist/src/core/furnace-staleness.d.ts +7 -0
  221. package/dist/src/core/furnace-staleness.js +34 -24
  222. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  223. package/dist/src/core/furnace-validate-accessibility.js +10 -1
  224. package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
  225. package/dist/src/core/furnace-validate-exceptions.js +43 -0
  226. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  227. package/dist/src/core/furnace-validate-icon-button.js +49 -0
  228. package/dist/src/core/furnace-validate.js +1 -1
  229. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  230. package/dist/src/core/harness-port-fallback.js +96 -0
  231. package/dist/src/core/history-log-file.d.ts +61 -0
  232. package/dist/src/core/history-log-file.js +157 -0
  233. package/dist/src/core/history-log.d.ts +2 -1
  234. package/dist/src/core/history-log.js +2 -1
  235. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  236. package/dist/src/core/json-preserving-edit.js +199 -0
  237. package/dist/src/core/mach.js +8 -1
  238. package/dist/src/core/negative-control-diff.d.ts +39 -0
  239. package/dist/src/core/negative-control-diff.js +136 -0
  240. package/dist/src/core/negative-control.d.ts +138 -0
  241. package/dist/src/core/negative-control.js +356 -0
  242. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  243. package/dist/src/core/objdir-install-manifests.js +226 -0
  244. package/dist/src/core/patch-export-update.d.ts +2 -0
  245. package/dist/src/core/patch-export-update.js +1 -0
  246. package/dist/src/core/patch-export.js +3 -4
  247. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
  248. package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
  249. package/dist/src/core/patch-lint-checkjs.js +8 -11
  250. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  251. package/dist/src/core/patch-lint-code-lines.js +105 -0
  252. package/dist/src/core/patch-lint-cross.js +12 -4
  253. package/dist/src/core/patch-lint-css.d.ts +8 -2
  254. package/dist/src/core/patch-lint-css.js +10 -4
  255. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  256. package/dist/src/core/patch-lint-file-size.js +22 -11
  257. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
  258. package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
  259. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  260. package/dist/src/core/patch-lint-ignore.d.ts +152 -0
  261. package/dist/src/core/patch-lint-ignore.js +299 -0
  262. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  263. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  264. package/dist/src/core/patch-lint-size.d.ts +117 -0
  265. package/dist/src/core/patch-lint-size.js +323 -0
  266. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  267. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  268. package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
  269. package/dist/src/core/patch-lint-token-scale.js +109 -0
  270. package/dist/src/core/patch-lint.d.ts +11 -83
  271. package/dist/src/core/patch-lint.js +64 -286
  272. package/dist/src/core/patch-lock.js +7 -6
  273. package/dist/src/core/patch-manifest-consistency.js +7 -6
  274. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  275. package/dist/src/core/patch-manifest-io.js +19 -8
  276. package/dist/src/core/patch-manifest-validate.js +34 -2
  277. package/dist/src/core/patch-order-legal.d.ts +25 -0
  278. package/dist/src/core/patch-order-legal.js +83 -0
  279. package/dist/src/core/patch-policy-content.d.ts +40 -0
  280. package/dist/src/core/patch-policy-content.js +216 -0
  281. package/dist/src/core/patch-policy.d.ts +16 -3
  282. package/dist/src/core/patch-policy.js +16 -9
  283. package/dist/src/core/patch-retired.d.ts +35 -0
  284. package/dist/src/core/patch-retired.js +54 -0
  285. package/dist/src/core/patch-subset.d.ts +16 -0
  286. package/dist/src/core/patch-subset.js +66 -0
  287. package/dist/src/core/preflight-wait.d.ts +20 -0
  288. package/dist/src/core/preflight-wait.js +38 -0
  289. package/dist/src/core/rebase-from-version.d.ts +46 -0
  290. package/dist/src/core/rebase-from-version.js +71 -0
  291. package/dist/src/core/run-log.d.ts +20 -0
  292. package/dist/src/core/run-log.js +89 -3
  293. package/dist/src/core/run-profile.d.ts +7 -0
  294. package/dist/src/core/run-profile.js +7 -1
  295. package/dist/src/core/smoke-patterns.d.ts +5 -0
  296. package/dist/src/core/smoke-patterns.js +13 -0
  297. package/dist/src/core/test-harness-crash.d.ts +18 -1
  298. package/dist/src/core/test-harness-crash.js +6 -4
  299. package/dist/src/core/test-harness-signature.d.ts +17 -0
  300. package/dist/src/core/test-harness-signature.js +4 -0
  301. package/dist/src/core/test-run-completeness.d.ts +57 -0
  302. package/dist/src/core/test-run-completeness.js +144 -0
  303. package/dist/src/core/test-stale-check.d.ts +27 -1
  304. package/dist/src/core/test-stale-check.js +39 -49
  305. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  306. package/dist/src/core/test-stale-exempt.js +70 -0
  307. package/dist/src/core/token-lint.d.ts +50 -0
  308. package/dist/src/core/token-lint.js +126 -0
  309. package/dist/src/core/tree-guard.js +3 -0
  310. package/dist/src/core/typecheck-shim.d.ts +1 -1
  311. package/dist/src/core/typecheck-shim.js +90 -2
  312. package/dist/src/index.d.ts +8 -3
  313. package/dist/src/index.js +3 -1
  314. package/dist/src/types/commands/index.d.ts +2 -2
  315. package/dist/src/types/commands/options.d.ts +148 -21
  316. package/dist/src/types/commands/patches.d.ts +75 -1
  317. package/dist/src/types/config.d.ts +192 -0
  318. package/dist/src/types/config.js +9 -0
  319. package/dist/src/types/furnace.d.ts +43 -1
  320. package/dist/src/types/furnace.js +7 -0
  321. package/dist/src/types/index.d.ts +1 -1
  322. package/dist/src/utils/options.d.ts +34 -6
  323. package/dist/src/utils/options.js +86 -5
  324. package/dist/src/utils/path-glob.d.ts +15 -0
  325. package/dist/src/utils/path-glob.js +47 -0
  326. package/dist/src/utils/regex.d.ts +15 -3
  327. package/dist/src/utils/regex.js +37 -16
  328. package/docs/configuration.md +282 -9
  329. package/docs/exit-codes.md +18 -1
  330. package/docs/furnace.md +97 -3
  331. package/docs/lifecycle-invariants.md +52 -19
  332. package/docs/machine-output.md +74 -12
  333. package/docs/patch-workflow.md +297 -22
  334. package/docs/run-logs.md +20 -1
  335. package/docs/testing.md +276 -20
  336. package/package.json +2 -2
@@ -9,7 +9,7 @@
9
9
  * (`re-export-adjacent.ts`) warns about unmanaged neighbour files but is
10
10
  * structurally blind to drift inside owned files. This module compares
11
11
  * the old body against the refreshed body per file and reports the
12
- * payload lines that are about to enter the body. Under
12
+ * payload lines that are about to enter or leave the body. Under
13
13
  * `--refuse-foreign-drift` the patch is skipped and the run exits
14
14
  * non-zero, mirroring `--refuse-adjacent-unmanaged`.
15
15
  *
@@ -74,8 +74,15 @@ function multisetExcess(next, previous) {
74
74
  }
75
75
  return excess;
76
76
  }
77
- function summarizeForeignHunks(payload, foreignAdded, foreignRemoved) {
77
+ /**
78
+ * Summarizes the hunks of `payload` that carry the budgeted `+`/`-` lines.
79
+ * `describe` renders the per-hunk counts, so the old body's dropped lines
80
+ * read in the same engine-edit direction as the new body's absorbed ones.
81
+ */
82
+ function summarizeForeignHunks(payload, foreignAdded, foreignRemoved, describe) {
78
83
  const summaries = [];
84
+ if (payload === undefined)
85
+ return summaries;
79
86
  const addedBudget = new Map(foreignAdded);
80
87
  const removedBudget = new Map(foreignRemoved);
81
88
  for (const section of payload.sections) {
@@ -101,7 +108,7 @@ function summarizeForeignHunks(payload, foreignAdded, foreignRemoved) {
101
108
  }
102
109
  }
103
110
  if (added > 0 || removed > 0) {
104
- summaries.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@ (+${added}/-${removed})`);
111
+ summaries.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@ ${describe(added, removed)}`);
105
112
  }
106
113
  }
107
114
  }
@@ -113,47 +120,71 @@ function countExcess(excess) {
113
120
  total += count;
114
121
  return total;
115
122
  }
123
+ const EMPTY_PAYLOAD_LINES = new Map();
116
124
  /**
117
125
  * Pure comparison: per owned file, which `+`/`-` payload lines the
118
- * refreshed body carries that the old body did not. Files not in
119
- * `previousFilesAffected` are skipped, since those are intentional
120
- * adoptions (`--scan`) rather than drift.
126
+ * refreshed body carries that the old body did not, and which the old body
127
+ * carried that the refreshed one dropped. Both directions are drift: a
128
+ * concurrent session deleting a line the patch added changes the patch as
129
+ * surely as one adding a line, and a belt that only saw absorbed lines let
130
+ * such a deletion through silently. Files not in `previousFilesAffected`
131
+ * are skipped, since those are intentional adoptions (`--scan`) or
132
+ * retirements (`--expect-removed`) rather than drift.
121
133
  */
122
134
  export function computeForeignDrift(oldBody, newBody, previousFilesAffected) {
123
135
  const owned = new Set(previousFilesAffected);
124
136
  const oldByFile = collectPayloadByFile(oldBody);
125
137
  const newByFile = collectPayloadByFile(newBody);
126
138
  const drift = [];
127
- for (const [file, next] of newByFile) {
139
+ // New-body order first, then files only the old body carried.
140
+ const files = new Set([...newByFile.keys(), ...oldByFile.keys()]);
141
+ for (const file of files) {
128
142
  if (!owned.has(file))
129
143
  continue;
144
+ const next = newByFile.get(file);
130
145
  const previous = oldByFile.get(file);
131
- if (next.isBinary || previous?.isBinary === true) {
132
- const binaryChanged = next.binaryNewHash === undefined ||
133
- previous?.binaryNewHash === undefined ||
134
- !(next.binaryNewHash.startsWith(previous.binaryNewHash) ||
135
- previous.binaryNewHash.startsWith(next.binaryNewHash));
136
- if (binaryChanged) {
137
- drift.push({ file, addedLines: 0, removedLines: 0, hunkSummaries: [], binaryChanged });
146
+ if (next?.isBinary === true || previous?.isBinary === true) {
147
+ if (binaryContentChanged(next, previous)) {
148
+ drift.push({
149
+ file,
150
+ addedLines: 0,
151
+ removedLines: 0,
152
+ hunkSummaries: [],
153
+ binaryChanged: true,
154
+ });
138
155
  }
139
156
  continue;
140
157
  }
141
- const foreignAdded = multisetExcess(next.added, previous?.added ?? new Map());
142
- const foreignRemoved = multisetExcess(next.removed, previous?.removed ?? new Map());
143
- const addedLines = countExcess(foreignAdded);
144
- const removedLines = countExcess(foreignRemoved);
158
+ const absorbedAdded = multisetExcess(next?.added ?? EMPTY_PAYLOAD_LINES, previous?.added ?? EMPTY_PAYLOAD_LINES);
159
+ const absorbedRemoved = multisetExcess(next?.removed ?? EMPTY_PAYLOAD_LINES, previous?.removed ?? EMPTY_PAYLOAD_LINES);
160
+ const droppedAdded = multisetExcess(previous?.added ?? EMPTY_PAYLOAD_LINES, next?.added ?? EMPTY_PAYLOAD_LINES);
161
+ const droppedRemoved = multisetExcess(previous?.removed ?? EMPTY_PAYLOAD_LINES, next?.removed ?? EMPTY_PAYLOAD_LINES);
162
+ const addedLines = countExcess(absorbedAdded) + countExcess(droppedRemoved);
163
+ const removedLines = countExcess(absorbedRemoved) + countExcess(droppedAdded);
145
164
  if (addedLines === 0 && removedLines === 0)
146
165
  continue;
147
166
  drift.push({
148
167
  file,
149
168
  addedLines,
150
169
  removedLines,
151
- hunkSummaries: summarizeForeignHunks(next, foreignAdded, foreignRemoved).slice(0, MAX_HUNK_SUMMARIES_PER_FILE),
170
+ hunkSummaries: [
171
+ ...summarizeForeignHunks(next, absorbedAdded, absorbedRemoved, (plus, minus) => `(+${plus}/-${minus})`),
172
+ // A dropped `+` line is one the engine lost; a dropped `-` line is an
173
+ // upstream line the engine has back.
174
+ ...summarizeForeignHunks(previous, droppedAdded, droppedRemoved, (plus, minus) => `in the old body, dropped (+${minus}/-${plus})`),
175
+ ].slice(0, MAX_HUNK_SUMMARIES_PER_FILE),
152
176
  binaryChanged: false,
153
177
  });
154
178
  }
155
179
  return drift;
156
180
  }
181
+ /** True when a binary section appeared, vanished, or its new-side blob hash moved. */
182
+ function binaryContentChanged(next, previous) {
183
+ if (next?.binaryNewHash === undefined || previous?.binaryNewHash === undefined)
184
+ return true;
185
+ return !(next.binaryNewHash.startsWith(previous.binaryNewHash) ||
186
+ previous.binaryNewHash.startsWith(next.binaryNewHash));
187
+ }
157
188
  /**
158
189
  * Reads the old body, prints the always-on drift preview (including under
159
190
  * dry-run and `--scan`), and, when `ctx.refuseForeignDrift` is set,
@@ -161,6 +192,10 @@ export function computeForeignDrift(oldBody, newBody, previousFilesAffected) {
161
192
  */
162
193
  export async function reportForeignDrift(args) {
163
194
  const { patch, patchesDir, engineDir, newDiffContent, ctx } = args;
195
+ for (const file of args.adoptedFiles ?? []) {
196
+ if (ctx.expectedDriftFiles.has(file))
197
+ ctx.expectedSeen.add(file);
198
+ }
164
199
  const oldPath = join(patchesDir, patch.filename);
165
200
  let oldBody;
166
201
  try {
@@ -172,7 +207,7 @@ export async function reportForeignDrift(args) {
172
207
  catch (error) {
173
208
  return refuseUnreadableBaseline(ctx, patch.filename, `old body unreadable (${toError(error).message})`);
174
209
  }
175
- const drift = computeForeignDrift(oldBody, newDiffContent, patch.filesAffected);
210
+ const drift = computeForeignDrift(oldBody, newDiffContent, args.ownedFiles ?? patch.filesAffected);
176
211
  ctx.evaluationRuns += 1;
177
212
  if (drift.length === 0)
178
213
  return false;
@@ -184,7 +219,7 @@ export async function reportForeignDrift(args) {
184
219
  const recentlyEdited = await findFilesEditedSinceLastExport(engineDir, oldPath, drift.map((d) => d.file));
185
220
  const totalLines = drift.reduce((sum, d) => sum + d.addedLines + d.removedLines, 0);
186
221
  const lineNoun = totalLines === 1 ? 'line' : 'lines';
187
- warn(`${patch.filename}: refreshed body absorbs ${totalLines} ${lineNoun} not present in the old patch body (${drift.length} file(s)):`);
222
+ warn(`${patch.filename}: refreshed body changes ${totalLines} ${lineNoun} against the old patch body (${drift.length} file(s)):`);
188
223
  for (const entry of drift.slice(0, MAX_FILES_REPORTED)) {
189
224
  const expectedTag = ctx.expectedDriftFiles.has(entry.file) ? ' (expected via --expect)' : '';
190
225
  const authorshipTag = recentlyEdited.has(entry.file)
@@ -194,7 +229,7 @@ export async function reportForeignDrift(args) {
194
229
  info(` ${entry.file}: binary content changed${expectedTag}${authorshipTag}`);
195
230
  continue;
196
231
  }
197
- info(` ${entry.file}: +${entry.addedLines}/-${entry.removedLines} newly captured line(s)${expectedTag}${authorshipTag}`);
232
+ info(` ${entry.file}: +${entry.addedLines} added / -${entry.removedLines} removed line(s)${expectedTag}${authorshipTag}`);
198
233
  for (const summary of entry.hunkSummaries) {
199
234
  info(` ${summary}`);
200
235
  }
@@ -274,31 +309,24 @@ function refuseUnreadableBaseline(ctx, patchFilename, detail) {
274
309
  /**
275
310
  * States, verbatim in the notice, what the `--expect` whitelist is compared
276
311
  * against and when. This notice is the only feedback an operator gets that
277
- * the drift belt looked at the files they named, and a downstream report
278
- * showed the previous copy actively misleading: it warned "showed no drift"
279
- * over an edit that was captured correctly, and none of the causes it named
280
- * held, so the operator's only recourse was `grep -c` on the written body.
281
- *
282
- * The measurement, stated exactly:
312
+ * the drift belt looked at the files they named, so it has to describe the
313
+ * measurement exactly:
283
314
  *
284
315
  * - Compared: for each file already in the patch's `filesAffected`, the
285
316
  * `+`/`-` payload lines the refreshed body carries that the old patch
286
- * body did not (`computeForeignDrift`). It is a content comparison, and
287
- * it is one-directional: excess in the new body only.
317
+ * body did not, and the ones the old body carried that the refreshed body
318
+ * dropped (`computeForeignDrift`). It is a content comparison in both
319
+ * directions, so a deletion-only edit is drift too.
288
320
  * - When: per patch, on the body about to be written, before the write.
289
321
  * Never against post-write state.
290
322
  *
291
- * The one-directional half is what the earlier copy hid, and it is the
292
- * cause the report's first shape actually hit: deleting a line from an
293
- * engine file makes the refreshed body drop that file's `+` line. Nothing
294
- * is absorbed, so there is no drift to see. The belt exists to catch a
295
- * concurrent session's edits being silently swallowed into a body, not
296
- * every difference between two bodies. Stating that is the fix. Widening
297
- * the comparison would turn every ordinary line removal into a refusal.
323
+ * An earlier copy compared one direction only, so a correctly captured
324
+ * deletion read as an `--expect` that matched nothing, and a peer's
325
+ * deletion in a file outside `--expect` passed the belt silently.
298
326
  */
299
327
  const EXPECT_MEASUREMENT_NOTE = '--expect is evaluated against CONTENT, before the write: for each file already in the ' +
300
- "patch's filesAffected, the payload lines the refreshed body carries that the old patch " +
301
- 'body did not. It never looks at post-write state.';
328
+ "patch's filesAffected, the payload lines the refreshed body adds or drops against the old " +
329
+ 'patch body. It never looks at post-write state.';
302
330
  /**
303
331
  * Names `--expect` paths that never drifted this run. A typo'd
304
332
  * `--expect` path silently degrades the flag back to refusing the slice it
@@ -318,17 +346,13 @@ export function warnUnseenExpectedDrift(driftCtx) {
318
346
  `${unseen.join(', ')}.\n` +
319
347
  `${EXPECT_MEASUREMENT_NOTE}\n` +
320
348
  'Causes, in rough order of likelihood:\n' +
321
- ' 1. Your edit only REMOVED lines from the body (you deleted a line from the engine ' +
322
- 'file). The comparison is one-directional — it reports lines ABSORBED into the body — ' +
323
- 'so a correctly captured deletion is not drift and never will be. Confirm the capture ' +
324
- 'with "grep -c" on the written patch body.\n' +
325
- " 2. A typo in the path, or a path not in the patch's filesAffected (files adopted " +
326
- 'this run via --scan/--scan-file are excluded from the comparison by design).\n' +
327
- ' 3. Adoption already captured it: a "re-export <p> --scan --scan-file <f>" run also ' +
349
+ " 1. A typo in the path, or a path in none of the selected patches' filesAffected and " +
350
+ 'not adopted by this run.\n' +
351
+ ' 2. Adoption already captured it: a "re-export <p> --scan --scan-file <f>" run also ' +
328
352
  "captures that patch's other drifted files, so a scan-less re-export afterwards " +
329
- 'legitimately sees nothing left to absorb. Informational — a chain that treats this ' +
353
+ 'legitimately sees nothing left to change. Informational — a chain that treats this ' +
330
354
  'warning as an error will false-red on it.\n' +
331
- ' 4. The slice was already captured by an earlier export in this session.\n' +
332
- ' 5. The engine content already matches the patch body — a concurrent session may have ' +
355
+ ' 3. The slice was already captured by an earlier export in this session.\n' +
356
+ ' 4. The engine content already matches the patch body — a concurrent session may have ' +
333
357
  'exported the same change, so the drift converged before this run looked.');
334
358
  }
@@ -7,6 +7,7 @@ import { listTrackedInHead } from '../core/git-file-ops.js';
7
7
  import { extractAffectedFiles } from '../core/patch-apply.js';
8
8
  import { updatePatchAndMetadata } from '../core/patch-export.js';
9
9
  import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
10
+ import { buildLintIgnoreChecks, mergeLintIgnoreIds } from '../core/patch-lint-ignore.js';
10
11
  import { computeProjectedLintRegressions } from '../core/patch-lint-projection.js';
11
12
  import { loadPatchesManifest } from '../core/patch-manifest.js';
12
13
  import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
@@ -25,10 +26,8 @@ import { projectEntryBody } from './patch/entry-projection.js';
25
26
  * so the caller can drop the field rather than write an empty array.
26
27
  */
27
28
  function resolveEffectiveTierAndLintIgnore(target, options) {
28
- const existingIgnoreSet = new Set(target.lintIgnore ?? []);
29
29
  const flagIgnoreSet = new Set(options.lintIgnore ?? []);
30
- const mergedIgnoreSet = new Set([...existingIgnoreSet, ...flagIgnoreSet]);
31
- const effectiveLintIgnore = mergedIgnoreSet.size > 0 ? [...mergedIgnoreSet] : undefined;
30
+ const effectiveLintIgnore = mergeLintIgnoreIds(target.lintIgnore, [...flagIgnoreSet]);
32
31
  const effectiveTier = options.tier ?? target.tier;
33
32
  return { effectiveTier, effectiveLintIgnore, flagIgnoreSet };
34
33
  }
@@ -39,8 +38,10 @@ function resolveEffectiveTierAndLintIgnore(target, options) {
39
38
  * shrink. Pre-existing cross-patch errors are surfaced as a non-blocking
40
39
  * warning so the user does not walk away thinking the queue is clean.
41
40
  */
42
- async function runProjectedCrossPatchLint(patchesDir, targetFilename, projectedDiff) {
43
- const baseCtx = await buildPatchQueueContext(patchesDir);
41
+ async function runProjectedCrossPatchLint(patchesDir, targetFilename, projectedDiff, config) {
42
+ // The config threads `patchLint.forwardRegistration` through, so a forward
43
+ // jar.mn registration is judged the way `lint --per-patch` judges it.
44
+ const baseCtx = await buildPatchQueueContext(patchesDir, config);
44
45
  const projection = projectEntryBody(projectedDiff);
45
46
  const projectedEntries = baseCtx.entries.map((entry) => {
46
47
  if (entry.filename !== targetFilename)
@@ -48,7 +49,7 @@ async function runProjectedCrossPatchLint(patchesDir, targetFilename, projectedD
48
49
  return { ...entry, ...projection };
49
50
  });
50
51
  const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
51
- const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
52
+ const projectedIssues = lintPatchQueue({ ...baseCtx, entries: projectedEntries }).filter((i) => i.severity === 'error');
52
53
  const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
53
54
  if (baselineIssues.length > 0 && regressions.length === 0) {
54
55
  warn(`Note: projected queue still has ${baselineIssues.length} pre-existing ` +
@@ -223,7 +224,7 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
223
224
  // `--tier` and `--lint-ignore` participate too, with append/union
224
225
  // semantics on the lint-ignore list.
225
226
  const { effectiveTier, effectiveLintIgnore, flagIgnoreSet } = resolveEffectiveTierAndLintIgnore(target, options);
226
- const ignoreChecks = effectiveLintIgnore ? new Set(effectiveLintIgnore) : undefined;
227
+ const ignoreChecks = buildLintIgnoreChecks(effectiveLintIgnore);
227
228
  const patchQueueCtx = (await pathExists(paths.patches))
228
229
  ? await buildPatchQueueContext(paths.patches)
229
230
  : undefined;
@@ -237,7 +238,7 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
237
238
  ignoreChecks,
238
239
  patchTier: effectiveTier,
239
240
  });
240
- const conflicts = await runProjectedCrossPatchLint(paths.patches, target.filename, projectedDiff);
241
+ const conflicts = await runProjectedCrossPatchLint(paths.patches, target.filename, projectedDiff, config);
241
242
  const filesUpdates = buildFilesModeMetadataUpdates(actualProjectedFiles, options, effectiveLintIgnore, flagIgnoreSet);
242
243
  const manifest = await loadPatchesManifest(paths.patches);
243
244
  if (manifest) {
@@ -246,6 +247,7 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
246
247
  manifest: buildProjectedManifest(manifest, manifest.patches.map((entry) => entry.filename === target.filename ? { ...entry, ...filesUpdates } : entry)),
247
248
  command: 're-export --files',
248
249
  forceUnsafe: options.forceUnsafe === true,
250
+ scope: [target.filename],
249
251
  });
250
252
  }
251
253
  const decision = await confirmFilesModeProjection({
@@ -297,6 +299,7 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
297
299
  config,
298
300
  command: 're-export --files',
299
301
  forceUnsafe: options.forceUnsafe === true,
302
+ scope: [target.filename],
300
303
  },
301
304
  });
302
305
  success(`Re-exported ${target.filename}`);
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { type PatchQueueContext } from '../core/patch-lint.js';
23
23
  import { type PerPatchLintCacheFile } from '../core/patch-lint-cache.js';
24
+ import { type LintIgnoreChecks } from '../core/patch-lint-ignore.js';
24
25
  import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
25
26
  import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
26
27
  import { type PerRunCheckJs } from './lint-per-run-checkjs.js';
@@ -92,7 +93,7 @@ export declare function lintReExportedPatch(args: {
92
93
  existingFiles: string[];
93
94
  diffContent: string;
94
95
  skipLint?: boolean;
95
- ignoreChecks?: ReadonlySet<string>;
96
+ ignoreChecks?: LintIgnoreChecks;
96
97
  patchTier?: 'branding';
97
98
  }): Promise<ReExportLintResult | null>;
98
99
  /**
@@ -20,18 +20,20 @@
20
20
  * (`refreshQueueCtxEntry`), so later iterations of an `--all` run lint
21
21
  * against the just-refreshed body instead of the stale one.
22
22
  */
23
- import { buildPatchQueueContext, countNonBinaryDiffLines, detectNewFilesInDiff, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
23
+ import { buildPatchQueueContext, detectNewFilesInDiff, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, measurePatchSize, resolvePatchSizeTier, } from '../core/patch-lint.js';
24
24
  import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, getPerPatchLintCacheHeadSha, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/patch-lint-cache.js';
25
25
  import { invalidateNewFileCreatorsCache } from '../core/patch-lint-cross.js';
26
+ import { lintIgnoreCacheTokens } from '../core/patch-lint-ignore.js';
26
27
  import { resolvePatchOwnedSysMjs } from '../core/patch-lint-ownership.js';
27
28
  import { computeProjectedLintRegressions } from '../core/patch-lint-projection.js';
28
29
  import { GeneralError } from '../errors/base.js';
29
30
  import { pathExists } from '../utils/fs.js';
30
31
  import { info, warn } from '../utils/logger.js';
31
32
  import { reportPatchLintOutcome } from './export-shared.js';
33
+ import { formatForwardImportRemedy } from './forward-import-remedy.js';
32
34
  import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
33
35
  import { projectEntryBody } from './patch/entry-projection.js';
34
- import { formatForwardImportRemedy } from './re-export-scan.js';
36
+ import { categoryOf, manifestFromQueue } from './re-export-scan.js';
35
37
  /**
36
38
  * Builds the once-per-invocation lint context.
37
39
  *
@@ -135,7 +137,7 @@ export async function lintReExportedPatch(args) {
135
137
  queueContext: patchQueueCtx,
136
138
  ...(lintCtx.engineHeadSha === undefined ? {} : { engineHeadSha: lintCtx.engineHeadSha }),
137
139
  });
138
- const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, args.ignoreChecks);
140
+ const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, lintIgnoreCacheTokens(args.ignoreChecks));
139
141
  if (cached) {
140
142
  lintCtx.reusedCacheEntries++;
141
143
  reportPatchLintOutcome([...globalIssues, ...cached.issues], args.skipLint);
@@ -167,8 +169,10 @@ export async function lintReExportedPatch(args) {
167
169
  else if (patchQueueCtx) {
168
170
  lintOptions.checkJsReportScope = resolvePatchOwnedSysMjs(detectNewFilesInDiff(diffContent));
169
171
  }
172
+ const patchSize = await measurePatchSize(paths.engine, diffContent, existingFiles, config);
170
173
  const issues = await lintExportedPatch(paths.engine, existingFiles, diffContent, config, {
171
174
  ...lintOptions,
175
+ patchSizeMeasurement: patchSize,
172
176
  ...(patchQueueCtx ? { patchQueueCtx } : {}),
173
177
  ...(args.ignoreChecks ? { ignoreChecks: args.ignoreChecks } : {}),
174
178
  ...(args.patchTier ? { patchTier: args.patchTier } : {}),
@@ -177,8 +181,8 @@ export async function lintReExportedPatch(args) {
177
181
  return {
178
182
  issues,
179
183
  suppressed: suppressedIssues,
180
- lineCount: countNonBinaryDiffLines(diffContent).textLines,
181
- lintIgnore: [...(args.ignoreChecks ?? [])],
184
+ lineCount: patchSize.lines,
185
+ lintIgnore: lintIgnoreCacheTokens(args.ignoreChecks),
182
186
  };
183
187
  }
184
188
  /**
@@ -236,10 +240,11 @@ export function assertRefreshedBodyHasNoNewQueueErrors(args) {
236
240
  const projectedEntries = ctx.entries.map((entry) => entry.filename === patchFilename ? { ...entry, ...projection } : entry);
237
241
  const isError = (issue) => issue.severity === 'error';
238
242
  const baseline = lintPatchQueue(ctx).filter(isError);
239
- const projected = lintPatchQueue({
240
- entries: projectedEntries,
241
- ...(ctx.patchPolicy ? { patchPolicy: ctx.patchPolicy } : {}),
242
- }).filter(isError);
243
+ // The whole context carries over (patchPolicy and the
244
+ // `patchLint.forwardRegistration` scope): dropping the scope here let a
245
+ // forward jar.mn registration pass this gate and surface only in the
246
+ // later queue audit, separately from the forward import it packages.
247
+ const projected = lintPatchQueue({ ...ctx, entries: projectedEntries }).filter(isError);
243
248
  const regressions = computeProjectedLintRegressions(baseline, projected);
244
249
  if (regressions.length === 0) {
245
250
  if (baseline.length > 0 && !lintCtx.queueBaselineWarned) {
@@ -253,7 +258,15 @@ export function assertRefreshedBodyHasNoNewQueueErrors(args) {
253
258
  const hasForwardImport = regressions.some((issue) => issue.check === 'forward-import');
254
259
  const message = `Refusing to re-export ${patchFilename} because the refreshed body introduces ` +
255
260
  `${regressions.length} new cross-patch lint error${regressions.length === 1 ? '' : 's'}:\n${details}` +
256
- (hasForwardImport ? `\n${formatForwardImportRemedy('re-running the re-export')}` : '');
261
+ (hasForwardImport
262
+ ? `\n${formatForwardImportRemedy({
263
+ reRunHint: 're-running the re-export',
264
+ issues: regressions,
265
+ manifest: manifestFromQueue(ctx),
266
+ config: lintCtx.config,
267
+ ...categoryOf(ctx, patchFilename),
268
+ })}`
269
+ : '');
257
270
  if (forceUnsafe) {
258
271
  warn(message);
259
272
  warn('Proceeding because --force-unsafe was provided.');
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { InvalidArgumentError } from '../errors/base.js';
3
3
  import { info } from '../utils/logger.js';
4
+ import { validateScanFileOwners } from './re-export-scan-owner.js';
4
5
  /**
5
6
  * Heuristic for `--files` positional folding: an extra positional that
6
7
  * contains a path separator and is not a `.patch` filename is an engine
@@ -62,6 +63,39 @@ function validateWhitelistFlagDependencies(options) {
62
63
  options.refuseAdjacentUnmanaged !== true) {
63
64
  throw new InvalidArgumentError('--expect-unmanaged names approved exceptions to --refuse-adjacent-unmanaged and has no effect without it. Pass --refuse-adjacent-unmanaged, or drop --expect-unmanaged.', '--expect-unmanaged');
64
65
  }
66
+ for (const [flag, values] of [
67
+ ['--expect-new', options.expectNew],
68
+ ['--adjacent-ignore', options.adjacentIgnore],
69
+ ]) {
70
+ if (values !== undefined && values.length > 0 && options.refuseAdjacentUnmanaged !== true) {
71
+ throw new InvalidArgumentError(`${flag} relaxes --refuse-adjacent-unmanaged and has no effect without it. Pass --refuse-adjacent-unmanaged, or drop ${flag}.`, flag);
72
+ }
73
+ }
74
+ if (options.adjacentIgnore?.some((entry) => entry.includes('**'))) {
75
+ throw new InvalidArgumentError('--adjacent-ignore takes a path or a glob whose * stays within one path segment; ** is refused, because a subtree carve-out is how a reviewed exception becomes a blanket one.', '--adjacent-ignore');
76
+ }
77
+ }
78
+ /**
79
+ * Refuses a path-valued flag whose value starts with `-`. Commander hands a
80
+ * required option value whatever follows it, so a list that ended early
81
+ * (`--expect-unmanaged --dry-run`) silently turned the next flag into a path
82
+ * and the dry run into a write. No engine path starts with a dash.
83
+ */
84
+ function validatePathFlagValues(options) {
85
+ for (const [flag, values] of [
86
+ ['--expect', options.expect],
87
+ ['--expect-unmanaged', options.expectUnmanaged],
88
+ ['--expect-new', options.expectNew],
89
+ ['--expect-removed', options.expectRemoved],
90
+ ['--adjacent-ignore', options.adjacentIgnore],
91
+ ['--scan-file', options.scanFiles],
92
+ ]) {
93
+ const swallowed = (values ?? []).filter((value) => value.trim().startsWith('-'));
94
+ if (swallowed.length > 0) {
95
+ throw new InvalidArgumentError(`${flag} was given ${swallowed.map((value) => `"${value}"`).join(', ')}, which is a flag, not an engine path: ` +
96
+ `the ${flag} before it had no value, so it took the next argument. Give every ${flag} a path.`, flag);
97
+ }
98
+ }
65
99
  }
66
100
  function validateExpectedRemovalUsage(patches, options) {
67
101
  if (options.expectRemoved?.length &&
@@ -71,6 +105,9 @@ function validateExpectedRemovalUsage(patches, options) {
71
105
  }
72
106
  /** Validates mutually exclusive `re-export` targeting and metadata options. */
73
107
  export function validateReExportOptionCombinations(patches, options) {
108
+ // First: a swallowed flag would otherwise surface as whichever
109
+ // combination error it happens to trip, naming the wrong problem.
110
+ validatePathFlagValues(options);
74
111
  validateExpectedRemovalUsage(patches, options);
75
112
  if (options.files !== undefined) {
76
113
  if (options.all || options.scan) {
@@ -91,9 +128,10 @@ export function validateReExportOptionCombinations(patches, options) {
91
128
  if (options.scanFilesManifest !== undefined) {
92
129
  throw new InvalidArgumentError('--scan-file cannot be combined with --scan-files.', '--scan-file');
93
130
  }
94
- if (options.all || patches.length !== 1) {
95
- throw new InvalidArgumentError('--scan-file operates on exactly one target patch. Pass a single patch identifier.', '--scan-file');
131
+ if (options.all) {
132
+ throw new InvalidArgumentError('--scan-file cannot be combined with --all. Pass the target patch identifiers.', '--scan-file');
96
133
  }
134
+ validateScanFileOwners(patches, options.scanFiles);
97
135
  }
98
136
  if (options.scanFilesManifest !== undefined) {
99
137
  if (!options.scan)
@@ -105,8 +143,11 @@ export function validateReExportOptionCombinations(patches, options) {
105
143
  if (options.refuseAdjacentUnmanaged === true && (options.scan || options.files !== undefined)) {
106
144
  throw new InvalidArgumentError('--refuse-adjacent-unmanaged applies to the scan-less path only and cannot be combined with --scan or --files (those set filesAffected explicitly).', '--refuse-adjacent-unmanaged');
107
145
  }
108
- if (options.refuseForeignDrift === true && (options.scan || options.files !== undefined)) {
109
- throw new InvalidArgumentError('--refuse-foreign-drift applies to the scan-less path only and cannot be combined with --scan or --files (those explicitly capture the current engine state).', '--refuse-foreign-drift');
146
+ // Beside --scan the belt covers the files each patch already owns; the
147
+ // files a scan adopts are the run's stated intent. --files replaces the
148
+ // file list outright, so there is no owned set left to guard.
149
+ if (options.refuseForeignDrift === true && options.files !== undefined) {
150
+ throw new InvalidArgumentError('--refuse-foreign-drift cannot be combined with --files (it sets filesAffected explicitly and captures the current engine state).', '--refuse-foreign-drift');
110
151
  }
111
152
  validateWhitelistFlagDependencies(options);
112
153
  const usingTierFlag = options.tier !== undefined;
@@ -0,0 +1,23 @@
1
+ import type { ReExportOptions } from '../types/commands/index.js';
2
+ /**
3
+ * Reads a path list file: one engine-relative path per line, blank lines
4
+ * and `#` comment lines ignored. Backs `--expect-from`, which exists because
5
+ * a list held in one shell variable reaches the CLI as one argument under
6
+ * zsh, and splitting it by hand left every path after the first as a
7
+ * positional patch name.
8
+ *
9
+ * @param file - The list file, relative to the working directory
10
+ * @param flag - Flag name for error messages
11
+ */
12
+ export declare function readPathListFile(file: string, flag: string): Promise<string[]>;
13
+ /**
14
+ * Runs every lock-free argument check: empty or unknown patch names
15
+ * (naming an engine file passed by mistake), option combinations, path
16
+ * flag values that are flags, and path flag values that name no changed
17
+ * file. Throws the refusal the locked run would have thrown, only sooner.
18
+ *
19
+ * @param projectRoot - Project root
20
+ * @param patches - Positional patch arguments as given
21
+ * @param options - Options as the command layer builds them
22
+ */
23
+ export declare function preflightReExportArguments(projectRoot: string, patches: readonly string[], options: ReExportOptions): Promise<void>;