@hominis/fireforge 0.44.4 → 0.46.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 (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -18,8 +18,8 @@
18
18
  import { join } from 'node:path';
19
19
  import { Command } from 'commander';
20
20
  import { getProjectPaths, loadConfig } from '../../core/config.js';
21
- import { appendHistory, assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
22
- import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
21
+ import { assertConfirmationAvailable, confirmDestructive } from '../../core/destructive.js';
22
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
23
23
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
24
24
  import { buildPatchQueueContext } from '../../core/patch-lint.js';
25
25
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
@@ -30,7 +30,8 @@ import { toError } from '../../utils/errors.js';
30
30
  import { readText, removeFile, writeText } from '../../utils/fs.js';
31
31
  import { info, intro, outro, success, warn } from '../../utils/logger.js';
32
32
  import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
33
- import { normalizePatchDisplayName } from '../../utils/validation.js';
33
+ import { normalizePatchDisplayName, parsePositiveIntegerFlag } from '../../utils/validation.js';
34
+ import { proceedAfterDecision } from '../destructive-decision.js';
34
35
  import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
35
36
  import { runPatchLint } from '../export-shared.js';
36
37
  import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, mergeStagedForwardImports, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
@@ -68,9 +69,9 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
68
69
  await renumberPatchesInManifest(patchesDir, plan.placement.renameMap);
69
70
  renumberApplied = true;
70
71
  }
71
- await writeText(newPatchPath, normalizePatchArtifact(plan.movedDiff));
72
+ await writeText(newPatchPath, plan.movedDiff);
72
73
  newPatchWritten = true;
73
- await writeText(sourcePathAfter, normalizePatchArtifact(plan.remainingDiff));
74
+ await writeText(sourcePathAfter, plan.remainingDiff);
74
75
  sourceRewritten = true;
75
76
  const fresh = await loadPatchesManifest(patchesDir);
76
77
  if (!fresh)
@@ -91,28 +92,23 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
91
92
  updatedPatches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
92
93
  const updated = validatePatchesManifest({ ...fresh, patches: updatedPatches });
93
94
  await savePatchesManifest(patchesDir, updated);
94
- try {
95
- await appendHistory(patchesDir, {
96
- operation: 'patch-split',
97
- args: {
98
- source: effectiveSourceFilename,
99
- newFilename: plan.placement.newFilename,
100
- order: plan.placement.insertionOrder,
101
- files: plan.movedFiles,
102
- ownerRewrites: plan.ownerRewrites,
103
- renames: [...plan.placement.renameMap.entries()].map(([from, entry]) => ({
104
- from,
105
- to: entry.newFilename,
106
- })),
107
- },
108
- ...(options.yes === true ? { yes: true } : {}),
109
- ...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
110
- result: 'ok',
111
- });
112
- }
113
- catch (historyError) {
114
- warn(`History log append failed after patch split committed: ${toError(historyError).message}`);
115
- }
95
+ await appendHistoryBestEffort(patchesDir, {
96
+ operation: 'patch-split',
97
+ args: {
98
+ source: effectiveSourceFilename,
99
+ newFilename: plan.placement.newFilename,
100
+ order: plan.placement.insertionOrder,
101
+ files: plan.movedFiles,
102
+ ownerRewrites: plan.ownerRewrites,
103
+ renames: [...plan.placement.renameMap.entries()].map(([from, entry]) => ({
104
+ from,
105
+ to: entry.newFilename,
106
+ })),
107
+ },
108
+ ...(options.yes === true ? { yes: true } : {}),
109
+ ...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
110
+ result: 'ok',
111
+ }, `patch split committed`);
116
112
  }
117
113
  catch (error) {
118
114
  // Reverse-order rollback; each step warns on its own failure so the
@@ -226,8 +222,26 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
226
222
  // instead of linting each projected body blind.
227
223
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
228
224
  const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
229
- await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
230
- await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
225
+ await runPatchLint({
226
+ engineDir: paths.engine,
227
+ filesAffected: remainingFiles,
228
+ diffContent: remainingDiff,
229
+ config,
230
+ skipLint: options.skipLint,
231
+ patchQueueCtx,
232
+ ignoreChecks,
233
+ patchTier: source.tier,
234
+ });
235
+ await runPatchLint({
236
+ engineDir: paths.engine,
237
+ filesAffected: movedFiles,
238
+ diffContent: movedDiff,
239
+ config,
240
+ skipLint: options.skipLint,
241
+ patchQueueCtx,
242
+ ignoreChecks,
243
+ patchTier: source.tier,
244
+ });
231
245
  const { conflicts, stagedDependencyAdditions } = runProjectedSplitLint(plan, patchQueueCtx);
232
246
  plan.stagedDependencyAdditions = stagedDependencyAdditions;
233
247
  const newMetadata = buildNewPatchMetadata(plan, config);
@@ -246,14 +260,8 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
246
260
  unsafeOverride: options.forceUnsafe === true,
247
261
  conflicts,
248
262
  });
249
- if (decision === 'dry-run') {
250
- outro('Dry run complete — no changes made');
251
- return;
252
- }
253
- if (decision === 'declined') {
254
- outro('Split cancelled');
263
+ if (!proceedAfterDecision(decision, 'Split cancelled'))
255
264
  return;
256
- }
257
265
  await commitPatchSplit(paths.patches, plan, newMetadata, options, config);
258
266
  success(`Split ${source.filename}: ${placement.newFilename} now owns ${plan.movedFiles.length} file(s)`);
259
267
  if (plan.ownerRewrites.length > 0) {
@@ -273,13 +281,7 @@ export function registerPatchSplit(parent, context) {
273
281
  .requiredOption('--name <name>', 'Name for the new patch')
274
282
  .option('--category <category>', "Category for the new patch (default: the source patch's)")
275
283
  .option('--description <desc>', 'Description for the new patch')
276
- .option('--order <n>', 'Exact sparse order for the new patch', commanderArgParser((raw) => {
277
- const n = Number.parseInt(raw, 10);
278
- if (!Number.isInteger(n) || n <= 0) {
279
- throw new InvalidArgumentError(`--order must be a positive integer, got "${raw}".`, '--order');
280
- }
281
- return n;
282
- }))
284
+ .option('--order <n>', 'Exact sparse order for the new patch', commanderArgParser((raw) => parsePositiveIntegerFlag('--order', raw)))
283
285
  .option('--before <patch>', 'Place the new patch before this patch')
284
286
  .option('--after <patch>', 'Place the new patch after this patch (default: the source patch)')
285
287
  .option('--dry-run', 'Show what would happen without writing')
@@ -4,11 +4,10 @@
4
4
  * PatchMetadata.stagedDependencies without rewriting the .patch body.
5
5
  */
6
6
  import { Command } from 'commander';
7
- import { appendHistory } from '../../core/destructive.js';
7
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
8
8
  import { mutatePatchMetadata } from '../../core/patch-export.js';
9
9
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
10
- import { toError } from '../../utils/errors.js';
11
- import { info, intro, outro, warn } from '../../utils/logger.js';
10
+ import { info, intro, outro } from '../../utils/logger.js';
12
11
  import { addWaitLockOption, resolveWaitLockSeconds } from '../../utils/options.js';
13
12
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
14
13
  import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
@@ -113,7 +112,7 @@ function resolveImportRemovalTarget(options, staged) {
113
112
  const list = candidates
114
113
  .map((entry) => ` - ${dependencyLabel('import', importView(entry))}`)
115
114
  .join('\n');
116
- throw new GeneralError(`--remove matches ${String(candidates.length)} staged forward-imports on this patch; ` +
115
+ throw new GeneralError(`--remove matches ${candidates.length} staged forward-imports on this patch; ` +
117
116
  `pass --creates to pick one:\n${list}`);
118
117
  }
119
118
  return { file, specifier, creates: '(no match)' };
@@ -142,7 +141,7 @@ function resolveRegistrationRemovalTarget(options, staged) {
142
141
  const list = candidates
143
142
  .map((entry) => ` - ${dependencyLabel('registration', registrationView(entry))}`)
144
143
  .join('\n');
145
- throw new GeneralError(`--remove matches ${String(candidates.length)} staged registrations on this patch; ` +
144
+ throw new GeneralError(`--remove matches ${candidates.length} staged registrations on this patch; ` +
146
145
  `pass --creates to pick one:\n${list}`);
147
146
  }
148
147
  return { file, line, creates: '(no match)' };
@@ -280,23 +279,18 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
280
279
  const before = result.before.stagedDependencies;
281
280
  const after = result.after.stagedDependencies;
282
281
  info(`${targetPatch.filename}: ${describeStagedDependencyChange(countStagedEntries(before), countStagedEntries(after), mode, kind, target)}.`);
283
- try {
284
- await appendHistory(paths.patches, {
285
- operation: 'patch-staged-dependency',
286
- args: {
287
- filename: targetPatch.filename,
288
- mode,
289
- kind,
290
- before: before ?? {},
291
- after: after ?? {},
292
- },
293
- ...(options.yes === true ? { yes: true } : {}),
294
- result: 'ok',
295
- });
296
- }
297
- catch (historyError) {
298
- warn(`History log append failed after patch staged-dependency committed (${targetPatch.filename}): ${toError(historyError).message}`);
299
- }
282
+ await appendHistoryBestEffort(paths.patches, {
283
+ operation: 'patch-staged-dependency',
284
+ args: {
285
+ filename: targetPatch.filename,
286
+ mode,
287
+ kind,
288
+ before: before ?? {},
289
+ after: after ?? {},
290
+ },
291
+ ...(options.yes === true ? { yes: true } : {}),
292
+ result: 'ok',
293
+ }, `patch staged-dependency committed (${targetPatch.filename})`);
300
294
  outro('Patch staged-dependency complete');
301
295
  }
302
296
  /**
@@ -15,12 +15,13 @@
15
15
  * `--clear` must be supplied per invocation.
16
16
  */
17
17
  import { Command, Option } from 'commander';
18
- import { appendHistory, confirmDestructive } from '../../core/destructive.js';
18
+ import { confirmDestructive } from '../../core/destructive.js';
19
+ import { appendHistoryBestEffort } from '../../core/history-log.js';
19
20
  import { updatePatchMetadata } from '../../core/patch-export.js';
20
21
  import { InvalidArgumentError } from '../../errors/base.js';
21
- import { toError } from '../../utils/errors.js';
22
- import { info, intro, outro, warn } from '../../utils/logger.js';
22
+ import { info, intro, outro } from '../../utils/logger.js';
23
23
  import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
24
+ import { proceedAfterDecision } from '../destructive-decision.js';
24
25
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
25
26
  /**
26
27
  * Runs the `patch tier` command: updates `PatchMetadata.tier` on the
@@ -71,14 +72,8 @@ export async function patchTierCommand(projectRoot, identifier, options = {}) {
71
72
  yes: options.yes === true,
72
73
  dryRun: isDryRun,
73
74
  });
74
- if (decision === 'dry-run') {
75
- outro('Dry run complete — no changes made');
75
+ if (!proceedAfterDecision(decision, 'Cancelled — no changes made'))
76
76
  return;
77
- }
78
- if (decision === 'declined') {
79
- outro('Cancelled — no changes made');
80
- return;
81
- }
82
77
  // Single write under the patch directory lock (delegated inside
83
78
  // updatePatchMetadata). Setting routes through `updates`; clearing
84
79
  // routes through `unsetFields` so TypeScript's exact optional types
@@ -92,21 +87,16 @@ export async function patchTierCommand(projectRoot, identifier, options = {}) {
92
87
  else {
93
88
  await updatePatchMetadata(paths.patches, target.filename, {}, ['tier']);
94
89
  }
95
- try {
96
- await appendHistory(paths.patches, {
97
- operation: 'patch-tier',
98
- args: {
99
- filename: target.filename,
100
- ...(before !== undefined ? { before } : {}),
101
- ...(after !== undefined ? { after } : {}),
102
- },
103
- ...(options.yes === true ? { yes: true } : {}),
104
- result: 'ok',
105
- });
106
- }
107
- catch (historyError) {
108
- warn(`History log append failed after patch tier committed (${target.filename}): ${toError(historyError).message}`);
109
- }
90
+ await appendHistoryBestEffort(paths.patches, {
91
+ operation: 'patch-tier',
92
+ args: {
93
+ filename: target.filename,
94
+ ...(before !== undefined ? { before } : {}),
95
+ ...(after !== undefined ? { after } : {}),
96
+ },
97
+ ...(options.yes === true ? { yes: true } : {}),
98
+ result: 'ok',
99
+ }, `patch tier committed (${target.filename})`);
110
100
  info(`${target.filename}: ${action}.`);
111
101
  outro('Patch tier complete');
112
102
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Structural equality for patch rename maps, shared by the placement-mode
3
+ * export preview/commit check (`export-flow.ts`) and `patch reorder`'s
4
+ * under-lock drift check. Both previously carried a byte-identical copy of
5
+ * the sorted-entries comparison; one helper keeps the two commit gates from
6
+ * drifting.
7
+ */
8
+ import type { PatchRenameEntry } from '../core/patch-manifest.js';
9
+ /**
10
+ * Rename-map entries ordered by target order, so previews, history entries
11
+ * and equality checks all walk the queue in its final on-disk order.
12
+ */
13
+ export declare function getSortedRenameEntries(renameMap: Map<string, PatchRenameEntry>): Array<[string, PatchRenameEntry]>;
14
+ /**
15
+ * Compares two rename maps entry by entry after sorting both by target
16
+ * order. Equal when every (current filename, new filename, new order)
17
+ * triple matches positionally.
18
+ */
19
+ export declare function renameMapsEqual(left: Map<string, PatchRenameEntry>, right: Map<string, PatchRenameEntry>): boolean;
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Structural equality for patch rename maps, shared by the placement-mode
4
+ * export preview/commit check (`export-flow.ts`) and `patch reorder`'s
5
+ * under-lock drift check. Both previously carried a byte-identical copy of
6
+ * the sorted-entries comparison; one helper keeps the two commit gates from
7
+ * drifting.
8
+ */
9
+ /**
10
+ * Rename-map entries ordered by target order, so previews, history entries
11
+ * and equality checks all walk the queue in its final on-disk order.
12
+ */
13
+ export function getSortedRenameEntries(renameMap) {
14
+ return Array.from(renameMap.entries()).sort((a, b) => a[1].newOrder - b[1].newOrder);
15
+ }
16
+ /**
17
+ * Compares two rename maps entry by entry after sorting both by target
18
+ * order. Equal when every (current filename, new filename, new order)
19
+ * triple matches positionally.
20
+ */
21
+ export function renameMapsEqual(left, right) {
22
+ const leftEntries = getSortedRenameEntries(left);
23
+ const rightEntries = getSortedRenameEntries(right);
24
+ if (leftEntries.length !== rightEntries.length) {
25
+ return false;
26
+ }
27
+ return leftEntries.every(([leftFilename, leftEntry], index) => {
28
+ const rightTuple = rightEntries[index];
29
+ if (!rightTuple)
30
+ return false;
31
+ const [rightFilename, rightEntry] = rightTuple;
32
+ return (leftFilename === rightFilename &&
33
+ leftEntry.newFilename === rightEntry.newFilename &&
34
+ leftEntry.newOrder === rightEntry.newOrder);
35
+ });
36
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Clears `pendingResolution` from `state.json` transactionally.
3
+ *
4
+ * Five commands (`doctor --clear-resolution`, `resolve`, `rebase --continue`,
5
+ * `rebase --abort`, and the rebase patch loop) had each copied the same
6
+ * spread-then-`delete` updater. Routing the clear through `updateState` rather
7
+ * than a load/modify/save pair is the load-bearing part: the updater runs
8
+ * inside the state-file lock against the freshest on-disk state, so a
9
+ * concurrent write to an unrelated key (`buildMode`, `baseCommit`) is not
10
+ * clobbered by a stale reload. Copies of that reasoning drift; one does not.
11
+ *
12
+ * A no-op when nothing is pending, so callers need no guard of their own.
13
+ * @param projectRoot - Project root holding `.fireforge/state.json`.
14
+ */
15
+ export declare function clearPendingResolution(projectRoot: string): Promise<void>;
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { updateState } from '../core/config.js';
3
+ /**
4
+ * Clears `pendingResolution` from `state.json` transactionally.
5
+ *
6
+ * Five commands (`doctor --clear-resolution`, `resolve`, `rebase --continue`,
7
+ * `rebase --abort`, and the rebase patch loop) had each copied the same
8
+ * spread-then-`delete` updater. Routing the clear through `updateState` rather
9
+ * than a load/modify/save pair is the load-bearing part: the updater runs
10
+ * inside the state-file lock against the freshest on-disk state, so a
11
+ * concurrent write to an unrelated key (`buildMode`, `baseCommit`) is not
12
+ * clobbered by a stale reload. Copies of that reasoning drift; one does not.
13
+ *
14
+ * A no-op when nothing is pending, so callers need no guard of their own.
15
+ * @param projectRoot - Project root holding `.fireforge/state.json`.
16
+ */
17
+ export async function clearPendingResolution(projectRoot) {
18
+ await updateState(projectRoot, (current) => {
19
+ if (!current.pendingResolution)
20
+ return current;
21
+ const next = { ...current };
22
+ delete next.pendingResolution;
23
+ return next;
24
+ });
25
+ }
@@ -1,5 +1,5 @@
1
- import type { getProjectPaths } from '../core/config.js';
2
1
  import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
2
+ import type { ProjectPaths } from '../types/config.js';
3
3
  /**
4
4
  * Shared per-run context for the adjacency advisory: the classification
5
5
  * inputs computed once by `reExportCommand`, plus the collection of
@@ -10,13 +10,43 @@ export interface AdjacentUnmanagedContext {
10
10
  binaryName: string;
11
11
  furnacePrefixes: ReadonlySet<string>;
12
12
  refuseAdjacentUnmanaged: boolean;
13
+ /**
14
+ * Engine-relative paths named by `--expect-unmanaged`: reviewed, recorded
15
+ * exceptions that are reported but never refuse. Normalized once by the
16
+ * caller so the comparison matches git's engine-relative output.
17
+ */
18
+ approvedUnmanaged: ReadonlySet<string>;
19
+ /** Approved paths actually met this run, so unused ones can be surfaced. */
20
+ approvedSeen: Set<string>;
21
+ /**
22
+ * Offenders collected across the loop. `files` stays the bare paths that
23
+ * downstream consumers already read; `anchored` carries the same entries
24
+ * with the owned directory that made each one adjacent, so the run-level
25
+ * refusal can name the anchor without re-deriving it.
26
+ */
13
27
  refusals: {
14
28
  patchFilename: string;
15
29
  files: string[];
30
+ anchored: string[];
16
31
  }[];
17
32
  }
18
33
  /** Returns the subset of `files` that no longer exist under `engineDir`. */
19
34
  export declare function findMissingFiles(engineDir: string, files: readonly string[]): Promise<string[]>;
35
+ /**
36
+ * One unmanaged file and the owned directory that made it adjacent.
37
+ *
38
+ * The anchor is what an unattended run could not previously recover: the
39
+ * notice named the offending file and the patch, but not WHICH of the
40
+ * patch's owned locations the file sits beside, so triaging a refusal on a
41
+ * patch that owns files in several directories meant re-deriving the
42
+ * adjacency by hand from the manifest.
43
+ */
44
+ export interface AdjacentUnmanagedFile {
45
+ /** Engine-relative path of the unmanaged file. */
46
+ file: string;
47
+ /** Directory holding at least one file this patch owns. */
48
+ anchorDir: string;
49
+ }
20
50
  /**
21
51
  * Emits the scan-less advisory for one patch: missing manifest files and
22
52
  * unmanaged files adjacent to the patch's ownership. When
@@ -27,7 +57,7 @@ export declare function findMissingFiles(engineDir: string, files: readonly stri
27
57
  */
28
58
  export declare function reportAdjacentUnmanagedFiles(args: {
29
59
  patch: PatchMetadata;
30
- paths: ReturnType<typeof getProjectPaths>;
60
+ paths: ProjectPaths;
31
61
  manifest: PatchesManifest;
32
62
  currentFilesAffected: readonly string[];
33
63
  ctx: AdjacentUnmanagedContext;
@@ -47,7 +47,9 @@ async function findAdjacentUnmanagedFiles(args) {
47
47
  const parentDirs = [...new Set(currentFilesAffected.map((file) => dirname(file)))];
48
48
  const currentSet = new Set(currentFilesAffected);
49
49
  const claimedByOthers = getClaimedFiles(manifest, patchFilename);
50
- const candidates = new Set();
50
+ // First anchor wins: the dirs are scanned in ownership order and a file
51
+ // lives in exactly one of them, so the map is a dedupe, not a choice.
52
+ const candidates = new Map();
51
53
  for (const dir of parentDirs) {
52
54
  const [modifiedFiles, untrackedFiles] = await Promise.all([
53
55
  getModifiedFilesInDir(engineDir, dir),
@@ -59,10 +61,37 @@ async function findAdjacentUnmanagedFiles(args) {
59
61
  continue;
60
62
  if (isToolManagedPath(file, untrackedSet.has(file), ctx))
61
63
  continue;
62
- candidates.add(file);
64
+ if (!candidates.has(file))
65
+ candidates.set(file, dir);
63
66
  }
64
67
  }
65
- return [...candidates].sort();
68
+ return [...candidates.entries()]
69
+ .map(([file, anchorDir]) => ({ file, anchorDir }))
70
+ .sort((a, b) => a.file.localeCompare(b.file));
71
+ }
72
+ /**
73
+ * Splits the candidates into the ones that refuse and the ones a
74
+ * `--expect-unmanaged` carve-out admits. Approved paths are recorded as seen
75
+ * so an unused carve-out can be reported, and are RETURNED rather than
76
+ * dropped: an exception nobody can see is how a carve-out quietly widens.
77
+ */
78
+ function partitionApproved(files, ctx) {
79
+ const refusing = [];
80
+ const approved = [];
81
+ for (const entry of files) {
82
+ if (ctx.approvedUnmanaged.has(entry.file)) {
83
+ ctx.approvedSeen.add(entry.file);
84
+ approved.push(entry);
85
+ }
86
+ else {
87
+ refusing.push(entry);
88
+ }
89
+ }
90
+ return { refusing, approved };
91
+ }
92
+ /** Renders `<file> (beside engine/<dir>)` for a notice line. */
93
+ function describeAnchored(entry) {
94
+ return `${entry.file} (beside engine/${entry.anchorDir})`;
66
95
  }
67
96
  /**
68
97
  * Emits the scan-less advisory for one patch: missing manifest files and
@@ -83,23 +112,35 @@ export async function reportAdjacentUnmanagedFiles(args) {
83
112
  ` Re-run with --scan to reconcile filesAffected with the current worktree, ` +
84
113
  `or pass --files <paths> to set the list explicitly.`);
85
114
  }
86
- const unmanagedFiles = await findAdjacentUnmanagedFiles({
115
+ const allUnmanaged = await findAdjacentUnmanagedFiles({
87
116
  currentFilesAffected,
88
117
  engineDir: paths.engine,
89
118
  manifest,
90
119
  patchFilename: patch.filename,
91
120
  ctx,
92
121
  });
122
+ const { refusing: unmanagedFiles, approved } = partitionApproved(allUnmanaged, ctx);
123
+ if (approved.length > 0) {
124
+ info(`${patch.filename}: ${approved.length} adjacent unmanaged file(s) admitted by ` +
125
+ `--expect-unmanaged (${approved.map(describeAnchored).join(', ')}) — reported, not refused.`);
126
+ }
93
127
  if (unmanagedFiles.length === 0)
94
128
  return false;
95
- const firstFew = unmanagedFiles.slice(0, 3).join(', ');
96
- const more = unmanagedFiles.length > 3 ? `, +${String(unmanagedFiles.length - 3)} more` : '';
97
- warn(`${patch.filename}: found ${String(unmanagedFiles.length)} unmanaged file(s) adjacent to this patch's ownership (${firstFew}${more}); plain re-export keeps filesAffected unchanged — use --scan-file to include reviewed files.`);
98
- for (const file of unmanagedFiles) {
99
- info(` ${file} — fireforge re-export ${patch.filename} --scan --scan-file ${file}`);
129
+ const firstFew = unmanagedFiles.slice(0, 3).map(describeAnchored).join(', ');
130
+ const more = unmanagedFiles.length > 3 ? `, +${unmanagedFiles.length - 3} more` : '';
131
+ warn(`${patch.filename}: found ${unmanagedFiles.length} unmanaged file(s) adjacent to this patch's ownership (${firstFew}${more}); ` +
132
+ `adjacent means the file sits in a directory this patch already owns a file in. ` +
133
+ `Plain re-export keeps filesAffected unchanged — use --scan-file to include reviewed files.`);
134
+ for (const entry of unmanagedFiles) {
135
+ info(` ${entry.file} — beside engine/${entry.anchorDir} — ` +
136
+ `fireforge re-export ${patch.filename} --scan --scan-file ${entry.file}`);
100
137
  }
101
138
  if (ctx.refuseAdjacentUnmanaged) {
102
- ctx.refusals.push({ patchFilename: patch.filename, files: unmanagedFiles });
139
+ ctx.refusals.push({
140
+ patchFilename: patch.filename,
141
+ files: unmanagedFiles.map((entry) => entry.file),
142
+ anchored: unmanagedFiles.map(describeAnchored),
143
+ });
103
144
  return true;
104
145
  }
105
146
  return false;
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { createHash } from 'node:crypto';
3
2
  import { readdir, readFile } from 'node:fs/promises';
4
3
  import { join } from 'node:path';
5
4
  import { isUnavailableGenerationToken, snapshotEngineGeneration, unavailableGenerationReason, } from '../core/engine-session-lock.js';
@@ -10,12 +9,11 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
10
9
  import { mapWithConcurrency } from '../utils/concurrency.js';
11
10
  import { getNodeErrorCode, toError } from '../utils/errors.js';
12
11
  import { readText } from '../utils/fs.js';
12
+ import { sha256Hex } from '../utils/hash.js';
13
+ import { isObject } from '../utils/validation.js';
13
14
  import { normalizeEngineRelativeInput } from './re-export-scan.js';
14
15
  /** Concurrency bound for patch-body hashing (matches the classify/lint pools). */
15
16
  const PATCH_HASH_CONCURRENCY = 8;
16
- function isRecord(value) {
17
- return typeof value === 'object' && value !== null && !Array.isArray(value);
18
- }
19
17
  function parseScanFilesManifest(raw, manifestPath) {
20
18
  let parsed;
21
19
  try {
@@ -24,12 +22,12 @@ function parseScanFilesManifest(raw, manifestPath) {
24
22
  catch (error) {
25
23
  throw new InvalidArgumentError(`--scan-files manifest is not valid JSON (${manifestPath}): ${toError(error).message}`, '--scan-files');
26
24
  }
27
- if (!isRecord(parsed) || !Array.isArray(parsed['assignments'])) {
25
+ if (!isObject(parsed) || !Array.isArray(parsed['assignments'])) {
28
26
  throw new InvalidArgumentError('--scan-files manifest must contain an assignments array.', '--scan-files');
29
27
  }
30
28
  const assignments = [];
31
29
  for (const [index, assignment] of parsed['assignments'].entries()) {
32
- if (!isRecord(assignment)) {
30
+ if (!isObject(assignment)) {
33
31
  throw new InvalidArgumentError(`--scan-files assignments[${index}] must be an object.`, '--scan-files');
34
32
  }
35
33
  const { patch, files } = assignment;
@@ -99,7 +97,7 @@ async function fingerprintDryRunState(engineDir, patchesDir) {
99
97
  throw new GeneralError(`[dry-run] cannot fingerprint the engine working tree for the purity guard: ` +
100
98
  unavailableGenerationReason(generation));
101
99
  }
102
- fingerprint.set('the engine working tree', createHash('sha256').update(generation).digest('hex'));
100
+ fingerprint.set('the engine working tree', sha256Hex(generation));
103
101
  let names = [];
104
102
  try {
105
103
  const entries = await readdir(patchesDir, { withFileTypes: true });
@@ -122,9 +120,7 @@ async function fingerprintDryRunState(engineDir, patchesDir) {
122
120
  const results = await mapWithConcurrency(names, PATCH_HASH_CONCURRENCY, async (name) => {
123
121
  try {
124
122
  return {
125
- hash: createHash('sha256')
126
- .update(await readFile(join(patchesDir, name)))
127
- .digest('hex'),
123
+ hash: sha256Hex(await readFile(join(patchesDir, name))),
128
124
  };
129
125
  }
130
126
  catch (error) {
@@ -72,3 +72,11 @@ export declare function reportForeignDrift(args: {
72
72
  newDiffContent: string;
73
73
  ctx: ForeignDriftContext;
74
74
  }): Promise<boolean>;
75
+ /**
76
+ * Names `--expect` paths that never drifted this run. A typo'd
77
+ * `--expect` path silently degrades the flag back to refusing the slice it
78
+ * was meant to admit, so surface the mismatch — but only as a warning: an
79
+ * expected file legitimately shows no drift in several shapes, all named
80
+ * below.
81
+ */
82
+ export declare function warnUnseenExpectedDrift(driftCtx: ForeignDriftContext): void;