@hominis/fireforge 0.49.2 → 0.51.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 (400) hide show
  1. package/CHANGELOG.md +87 -1
  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 +41 -0
  10. package/dist/src/commands/check-report.js +91 -0
  11. package/dist/src/commands/check-run.d.ts +33 -0
  12. package/dist/src/commands/check-run.js +112 -0
  13. package/dist/src/commands/check-steps.d.ts +75 -0
  14. package/dist/src/commands/check-steps.js +200 -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/declarations-report.d.ts +43 -0
  19. package/dist/src/commands/declarations-report.js +76 -0
  20. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  21. package/dist/src/commands/doctor-source-pin.js +4 -1
  22. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  23. package/dist/src/commands/export-adopt-check.js +98 -0
  24. package/dist/src/commands/export-flow.js +29 -14
  25. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  26. package/dist/src/commands/export-placement-gate.js +8 -0
  27. package/dist/src/commands/export-shared.d.ts +2 -1
  28. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  29. package/dist/src/commands/export-staged-registration.js +91 -0
  30. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  31. package/dist/src/commands/forward-import-remedy.js +63 -0
  32. package/dist/src/commands/furnace/apply.js +3 -3
  33. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  34. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  35. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  36. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  37. package/dist/src/commands/furnace/create.js +2 -2
  38. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  39. package/dist/src/commands/furnace/deploy-named.js +107 -0
  40. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  41. package/dist/src/commands/furnace/deploy.js +50 -86
  42. package/dist/src/commands/furnace/diff.js +26 -3
  43. package/dist/src/commands/furnace/index.js +66 -41
  44. package/dist/src/commands/furnace/init.js +13 -6
  45. package/dist/src/commands/furnace/override.js +4 -2
  46. package/dist/src/commands/furnace/preview.js +4 -3
  47. package/dist/src/commands/furnace/refresh.js +2 -2
  48. package/dist/src/commands/furnace/remove.js +4 -3
  49. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  50. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  51. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  52. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  53. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  54. package/dist/src/commands/furnace/rename-registrations.js +98 -0
  55. package/dist/src/commands/furnace/rename.d.ts +2 -1
  56. package/dist/src/commands/furnace/rename.js +28 -66
  57. package/dist/src/commands/furnace/scan.d.ts +9 -4
  58. package/dist/src/commands/furnace/scan.js +11 -9
  59. package/dist/src/commands/furnace/sync.js +3 -2
  60. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  61. package/dist/src/commands/furnace/validate-report.js +91 -0
  62. package/dist/src/commands/furnace/validate.d.ts +1 -1
  63. package/dist/src/commands/furnace/validate.js +151 -113
  64. package/dist/src/commands/furnace/validation-output.js +14 -3
  65. package/dist/src/commands/lint-per-patch.js +36 -61
  66. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  67. package/dist/src/commands/lint-report.d.ts +8 -4
  68. package/dist/src/commands/lint-report.js +20 -5
  69. package/dist/src/commands/lint.js +15 -34
  70. package/dist/src/commands/manifest.js +4 -0
  71. package/dist/src/commands/park.d.ts +22 -0
  72. package/dist/src/commands/park.js +232 -0
  73. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  74. package/dist/src/commands/patch/delete.d.ts +1 -1
  75. package/dist/src/commands/patch/delete.js +24 -104
  76. package/dist/src/commands/patch/index.d.ts +3 -3
  77. package/dist/src/commands/patch/index.js +10 -4
  78. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  79. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  80. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  81. package/dist/src/commands/patch/lint-ignore.js +78 -31
  82. package/dist/src/commands/patch/move-files-create.js +34 -18
  83. package/dist/src/commands/patch/move-files-into.js +15 -7
  84. package/dist/src/commands/patch/move-files.js +6 -2
  85. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  86. package/dist/src/commands/patch/removal-created-files.js +83 -0
  87. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  88. package/dist/src/commands/patch/removal-dependents.js +133 -0
  89. package/dist/src/commands/patch/rename.js +20 -1
  90. package/dist/src/commands/patch/reserve.d.ts +33 -0
  91. package/dist/src/commands/patch/reserve.js +152 -0
  92. package/dist/src/commands/patch/retire.d.ts +34 -0
  93. package/dist/src/commands/patch/retire.js +133 -0
  94. package/dist/src/commands/patch/split-plan.d.ts +17 -8
  95. package/dist/src/commands/patch/split-plan.js +60 -28
  96. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  97. package/dist/src/commands/patch/split-staged.js +202 -0
  98. package/dist/src/commands/patch/split.d.ts +5 -0
  99. package/dist/src/commands/patch/split.js +62 -17
  100. package/dist/src/commands/patch/stage.d.ts +34 -0
  101. package/dist/src/commands/patch/stage.js +153 -0
  102. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  103. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  104. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  105. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  106. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  107. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  108. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  109. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  110. package/dist/src/commands/patch/staged-dependency.js +54 -139
  111. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  112. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  113. package/dist/src/commands/re-export-adjacent.d.ts +50 -2
  114. package/dist/src/commands/re-export-adjacent.js +160 -40
  115. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  116. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  117. package/dist/src/commands/re-export-drift.d.ts +36 -8
  118. package/dist/src/commands/re-export-drift.js +73 -49
  119. package/dist/src/commands/re-export-files.js +11 -8
  120. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  121. package/dist/src/commands/re-export-hunks.js +220 -0
  122. package/dist/src/commands/re-export-lint.d.ts +2 -1
  123. package/dist/src/commands/re-export-lint.js +23 -10
  124. package/dist/src/commands/re-export-options.js +74 -6
  125. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  126. package/dist/src/commands/re-export-preflight.js +160 -0
  127. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  128. package/dist/src/commands/re-export-purity-scope.js +131 -0
  129. package/dist/src/commands/re-export-register.js +56 -17
  130. package/dist/src/commands/re-export-removed.d.ts +13 -2
  131. package/dist/src/commands/re-export-removed.js +57 -12
  132. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  133. package/dist/src/commands/re-export-scan-owner.js +116 -0
  134. package/dist/src/commands/re-export-scan.d.ts +19 -9
  135. package/dist/src/commands/re-export-scan.js +60 -24
  136. package/dist/src/commands/re-export-single.js +122 -46
  137. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  138. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  139. package/dist/src/commands/re-export-summary.d.ts +28 -0
  140. package/dist/src/commands/re-export-summary.js +36 -0
  141. package/dist/src/commands/re-export.js +71 -57
  142. package/dist/src/commands/rebase/index.js +17 -17
  143. package/dist/src/commands/run.js +16 -2
  144. package/dist/src/commands/status-check.d.ts +55 -16
  145. package/dist/src/commands/status-check.js +92 -37
  146. package/dist/src/commands/status-json.d.ts +22 -9
  147. package/dist/src/commands/status-json.js +47 -32
  148. package/dist/src/commands/status-output.js +3 -1
  149. package/dist/src/commands/status-scope.d.ts +37 -0
  150. package/dist/src/commands/status-scope.js +54 -0
  151. package/dist/src/commands/status.js +66 -23
  152. package/dist/src/commands/test-browser-preflight.js +19 -3
  153. package/dist/src/commands/test-build-furnace.d.ts +22 -0
  154. package/dist/src/commands/test-build-furnace.js +54 -0
  155. package/dist/src/commands/test-build-phase.js +64 -41
  156. package/dist/src/commands/test-checkjs.js +10 -2
  157. package/dist/src/commands/test-diagnose.js +7 -0
  158. package/dist/src/commands/test-dispatch.d.ts +39 -0
  159. package/dist/src/commands/test-dispatch.js +211 -0
  160. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  161. package/dist/src/commands/test-furnace-reach.js +132 -0
  162. package/dist/src/commands/test-modes.d.ts +16 -1
  163. package/dist/src/commands/test-modes.js +24 -1
  164. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  165. package/dist/src/commands/test-multi-verdict.js +111 -0
  166. package/dist/src/commands/test-orchestrate.d.ts +52 -0
  167. package/dist/src/commands/test-orchestrate.js +240 -0
  168. package/dist/src/commands/test-perf-scope.d.ts +50 -0
  169. package/dist/src/commands/test-perf-scope.js +138 -0
  170. package/dist/src/commands/test-prelock.d.ts +51 -0
  171. package/dist/src/commands/test-prelock.js +273 -0
  172. package/dist/src/commands/test-register.js +69 -10
  173. package/dist/src/commands/test-results.d.ts +62 -0
  174. package/dist/src/commands/test-results.js +62 -0
  175. package/dist/src/commands/test-run.d.ts +13 -55
  176. package/dist/src/commands/test-run.js +74 -99
  177. package/dist/src/commands/test-shards.d.ts +110 -0
  178. package/dist/src/commands/test-shards.js +255 -0
  179. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  180. package/dist/src/commands/test-stale-gate.js +60 -28
  181. package/dist/src/commands/test-verdict.d.ts +11 -1
  182. package/dist/src/commands/test-verdict.js +28 -1
  183. package/dist/src/commands/test.d.ts +6 -1
  184. package/dist/src/commands/test.js +120 -124
  185. package/dist/src/commands/token-lint.d.ts +14 -0
  186. package/dist/src/commands/token-lint.js +151 -0
  187. package/dist/src/commands/token.js +10 -0
  188. package/dist/src/commands/verify-scope.d.ts +28 -0
  189. package/dist/src/commands/verify-scope.js +67 -0
  190. package/dist/src/commands/verify.d.ts +10 -2
  191. package/dist/src/commands/verify.js +67 -36
  192. package/dist/src/core/build-baseline-types.d.ts +46 -0
  193. package/dist/src/core/build-baseline-validate.js +19 -0
  194. package/dist/src/core/build-baseline.d.ts +5 -1
  195. package/dist/src/core/build-baseline.js +3 -1
  196. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  197. package/dist/src/core/build-overwrite-guard.js +84 -4
  198. package/dist/src/core/build-prepare.d.ts +13 -0
  199. package/dist/src/core/build-prepare.js +42 -10
  200. package/dist/src/core/build-run-record.d.ts +74 -0
  201. package/dist/src/core/build-run-record.js +136 -0
  202. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  203. package/dist/src/core/checkjs-support-heads.js +104 -0
  204. package/dist/src/core/config-paths.d.ts +2 -2
  205. package/dist/src/core/config-paths.js +10 -0
  206. package/dist/src/core/config-validate-check.d.ts +8 -0
  207. package/dist/src/core/config-validate-check.js +219 -0
  208. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  209. package/dist/src/core/config-validate-file-size.js +91 -2
  210. package/dist/src/core/config-validate-history.d.ts +11 -0
  211. package/dist/src/core/config-validate-history.js +38 -0
  212. package/dist/src/core/config-validate-patch-policy.js +100 -0
  213. package/dist/src/core/config-validate-test-toolchains.js +21 -0
  214. package/dist/src/core/config-validate-token-scales.d.ts +9 -0
  215. package/dist/src/core/config-validate-token-scales.js +92 -0
  216. package/dist/src/core/config-validate.js +20 -5
  217. package/dist/src/core/config.d.ts +5 -0
  218. package/dist/src/core/config.js +16 -0
  219. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  220. package/dist/src/core/coverage-dispatch.js +44 -0
  221. package/dist/src/core/coverage-extend.d.ts +70 -45
  222. package/dist/src/core/coverage-extend.js +199 -144
  223. package/dist/src/core/coverage-records.d.ts +108 -0
  224. package/dist/src/core/coverage-records.js +384 -0
  225. package/dist/src/core/destructive.d.ts +7 -4
  226. package/dist/src/core/destructive.js +10 -10
  227. package/dist/src/core/engine-session-lock.d.ts +16 -1
  228. package/dist/src/core/engine-session-lock.js +38 -10
  229. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  230. package/dist/src/core/export-moved-code-shape.js +135 -44
  231. package/dist/src/core/file-lock-queue.d.ts +67 -0
  232. package/dist/src/core/file-lock-queue.js +221 -0
  233. package/dist/src/core/file-lock-shared.d.ts +57 -0
  234. package/dist/src/core/file-lock-shared.js +117 -0
  235. package/dist/src/core/file-lock.d.ts +13 -25
  236. package/dist/src/core/file-lock.js +163 -140
  237. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  238. package/dist/src/core/furnace-apply-output.js +31 -1
  239. package/dist/src/core/furnace-apply.js +22 -12
  240. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  241. package/dist/src/core/furnace-composed-source.js +167 -0
  242. package/dist/src/core/furnace-config-validate-exceptions.d.ts +16 -0
  243. package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
  244. package/dist/src/core/furnace-config-validate.js +14 -0
  245. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  246. package/dist/src/core/furnace-css-fragments.js +26 -0
  247. package/dist/src/core/furnace-operation.d.ts +3 -3
  248. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  249. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  250. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  251. package/dist/src/core/furnace-registration-remove.js +53 -17
  252. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  253. package/dist/src/core/furnace-registration-rename.js +77 -0
  254. package/dist/src/core/furnace-registration.d.ts +14 -4
  255. package/dist/src/core/furnace-registration.js +44 -10
  256. package/dist/src/core/furnace-staleness.d.ts +7 -0
  257. package/dist/src/core/furnace-staleness.js +34 -24
  258. package/dist/src/core/furnace-validate-accessibility.d.ts +8 -4
  259. package/dist/src/core/furnace-validate-accessibility.js +71 -28
  260. package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
  261. package/dist/src/core/furnace-validate-exceptions.js +80 -0
  262. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  263. package/dist/src/core/furnace-validate-icon-button.js +78 -0
  264. package/dist/src/core/furnace-validate.js +1 -1
  265. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  266. package/dist/src/core/harness-port-fallback.js +96 -0
  267. package/dist/src/core/history-log-file.d.ts +61 -0
  268. package/dist/src/core/history-log-file.js +164 -0
  269. package/dist/src/core/history-log.d.ts +2 -1
  270. package/dist/src/core/history-log.js +2 -1
  271. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  272. package/dist/src/core/json-preserving-edit.js +199 -0
  273. package/dist/src/core/line-prefixer.d.ts +18 -0
  274. package/dist/src/core/line-prefixer.js +34 -0
  275. package/dist/src/core/mach.d.ts +7 -0
  276. package/dist/src/core/mach.js +32 -10
  277. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  278. package/dist/src/core/module-resolution-preflight.js +24 -6
  279. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  280. package/dist/src/core/negative-control-compiled.js +73 -0
  281. package/dist/src/core/negative-control-diff.d.ts +51 -0
  282. package/dist/src/core/negative-control-diff.js +179 -0
  283. package/dist/src/core/negative-control.d.ts +150 -0
  284. package/dist/src/core/negative-control.js +399 -0
  285. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  286. package/dist/src/core/objdir-install-manifests.js +226 -0
  287. package/dist/src/core/park-store.d.ts +63 -0
  288. package/dist/src/core/park-store.js +216 -0
  289. package/dist/src/core/patch-export-update.d.ts +6 -0
  290. package/dist/src/core/patch-export-update.js +13 -3
  291. package/dist/src/core/patch-export.d.ts +3 -2
  292. package/dist/src/core/patch-export.js +22 -16
  293. package/dist/src/core/patch-files.js +26 -8
  294. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
  295. package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
  296. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  297. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  298. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  299. package/dist/src/core/patch-lint-checkjs.js +20 -13
  300. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  301. package/dist/src/core/patch-lint-code-lines.js +105 -0
  302. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  303. package/dist/src/core/patch-lint-cross.js +17 -4
  304. package/dist/src/core/patch-lint-css.d.ts +8 -2
  305. package/dist/src/core/patch-lint-css.js +24 -14
  306. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  307. package/dist/src/core/patch-lint-file-size.js +22 -11
  308. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +23 -0
  309. package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
  310. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  311. package/dist/src/core/patch-lint-ignore.d.ts +196 -0
  312. package/dist/src/core/patch-lint-ignore.js +429 -0
  313. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  314. package/dist/src/core/patch-lint-ownership.js +26 -0
  315. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  316. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  317. package/dist/src/core/patch-lint-size.d.ts +117 -0
  318. package/dist/src/core/patch-lint-size.js +331 -0
  319. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  320. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  321. package/dist/src/core/patch-lint-token-scale.d.ts +57 -0
  322. package/dist/src/core/patch-lint-token-scale.js +286 -0
  323. package/dist/src/core/patch-lint.d.ts +11 -83
  324. package/dist/src/core/patch-lint.js +66 -288
  325. package/dist/src/core/patch-lock.js +7 -6
  326. package/dist/src/core/patch-manifest-consistency.js +7 -6
  327. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  328. package/dist/src/core/patch-manifest-io.js +27 -9
  329. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  330. package/dist/src/core/patch-manifest-key-order.js +61 -0
  331. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  332. package/dist/src/core/patch-manifest-stage.js +96 -0
  333. package/dist/src/core/patch-manifest-validate.js +61 -3
  334. package/dist/src/core/patch-order-legal.d.ts +26 -0
  335. package/dist/src/core/patch-order-legal.js +88 -0
  336. package/dist/src/core/patch-policy-content.d.ts +40 -0
  337. package/dist/src/core/patch-policy-content.js +216 -0
  338. package/dist/src/core/patch-policy.d.ts +22 -5
  339. package/dist/src/core/patch-policy.js +26 -12
  340. package/dist/src/core/patch-retired.d.ts +57 -0
  341. package/dist/src/core/patch-retired.js +92 -0
  342. package/dist/src/core/patch-subset.d.ts +16 -0
  343. package/dist/src/core/patch-subset.js +66 -0
  344. package/dist/src/core/preflight-wait.d.ts +20 -0
  345. package/dist/src/core/preflight-wait.js +38 -0
  346. package/dist/src/core/rebase-from-version.d.ts +46 -0
  347. package/dist/src/core/rebase-from-version.js +71 -0
  348. package/dist/src/core/run-log.d.ts +26 -0
  349. package/dist/src/core/run-log.js +106 -4
  350. package/dist/src/core/run-profile.d.ts +7 -0
  351. package/dist/src/core/run-profile.js +7 -1
  352. package/dist/src/core/smoke-patterns.d.ts +5 -0
  353. package/dist/src/core/smoke-patterns.js +13 -0
  354. package/dist/src/core/test-harness-crash.d.ts +19 -1
  355. package/dist/src/core/test-harness-crash.js +8 -9
  356. package/dist/src/core/test-harness-signature.d.ts +33 -0
  357. package/dist/src/core/test-harness-signature.js +23 -1
  358. package/dist/src/core/test-raw-log.d.ts +66 -0
  359. package/dist/src/core/test-raw-log.js +122 -0
  360. package/dist/src/core/test-run-completeness.d.ts +90 -0
  361. package/dist/src/core/test-run-completeness.js +184 -0
  362. package/dist/src/core/test-stale-check.d.ts +27 -1
  363. package/dist/src/core/test-stale-check.js +39 -49
  364. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  365. package/dist/src/core/test-stale-exempt.js +70 -0
  366. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  367. package/dist/src/core/test-xpcshell-retry.js +2 -1
  368. package/dist/src/core/token-lint.d.ts +53 -0
  369. package/dist/src/core/token-lint.js +131 -0
  370. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  371. package/dist/src/core/token-sibling-sheets.js +119 -0
  372. package/dist/src/core/tree-guard.js +5 -0
  373. package/dist/src/core/typecheck-shim.d.ts +6 -2
  374. package/dist/src/core/typecheck-shim.js +205 -4
  375. package/dist/src/core/typecheck.js +3 -1
  376. package/dist/src/index.d.ts +14 -3
  377. package/dist/src/index.js +6 -1
  378. package/dist/src/types/commands/index.d.ts +2 -2
  379. package/dist/src/types/commands/options.d.ts +212 -24
  380. package/dist/src/types/commands/patches.d.ts +118 -1
  381. package/dist/src/types/config.d.ts +264 -0
  382. package/dist/src/types/config.js +9 -0
  383. package/dist/src/types/furnace.d.ts +63 -1
  384. package/dist/src/types/furnace.js +7 -0
  385. package/dist/src/types/index.d.ts +1 -1
  386. package/dist/src/utils/options.d.ts +36 -7
  387. package/dist/src/utils/options.js +94 -8
  388. package/dist/src/utils/path-glob.d.ts +15 -0
  389. package/dist/src/utils/path-glob.js +47 -0
  390. package/dist/src/utils/regex.d.ts +15 -3
  391. package/dist/src/utils/regex.js +37 -16
  392. package/docs/configuration.md +369 -11
  393. package/docs/exit-codes.md +18 -1
  394. package/docs/furnace.md +139 -3
  395. package/docs/lifecycle-invariants.md +65 -30
  396. package/docs/machine-output.md +120 -12
  397. package/docs/patch-workflow.md +458 -22
  398. package/docs/run-logs.md +20 -1
  399. package/docs/testing.md +346 -21
  400. package/package.json +3 -3
@@ -21,14 +21,25 @@
21
21
  * carry), so the refusal can name the sequence instead of leaving the
22
22
  * operator to derive it.
23
23
  */
24
+ /** One run of consecutive lines a new file shares with an existing patch. */
25
+ export interface MovedCodeBlock {
26
+ /** New-file path in the pending diff. */
27
+ file: string;
28
+ /** File in the source patch that carries the same run. */
29
+ sourceFile: string;
30
+ /** The matched substantive lines, trimmed, in order. */
31
+ lines: string[];
32
+ }
24
33
  /** One existing patch that already carries lines the new files would add. */
25
34
  export interface MovedCodeOverlap {
26
35
  /** Filename of the patch that still contains the moved lines. */
27
36
  sourcePatch: string;
28
37
  /** New-file paths in the pending diff whose lines it overlaps. */
29
38
  files: string[];
30
- /** Count of distinct non-trivial lines shared. */
39
+ /** Count of substantive lines inside the matched blocks. */
31
40
  sharedLines: number;
41
+ /** The matched blocks, so the claim can be checked line by line. */
42
+ blocks: MovedCodeBlock[];
32
43
  }
33
44
  /** Minimal queue view this detector needs. */
34
45
  export interface MovedCodeQueueEntry {
@@ -36,9 +47,9 @@ export interface MovedCodeQueueEntry {
36
47
  diff: string;
37
48
  }
38
49
  /**
39
- * Finds existing patches that still carry substantive lines the pending
40
- * diff's new files would add, the fingerprint of code moved out of an
41
- * existing patch into a new file.
50
+ * Finds existing patches that still carry a block of substantive lines the
51
+ * pending diff's new files would add, the fingerprint of code moved out of
52
+ * an existing patch into a new file.
42
53
  *
43
54
  * Pure. Overlaps are returned strongest-first so the message names the
44
55
  * most likely source patch.
@@ -48,13 +59,40 @@ export interface MovedCodeQueueEntry {
48
59
  * @returns Overlaps, or an empty list when the shape is not present
49
60
  */
50
61
  export declare function detectMovedCodeOverlaps(pendingDiff: string, entries: readonly MovedCodeQueueEntry[]): MovedCodeOverlap[];
62
+ /** What the placement projection learned about the pending export. */
63
+ export interface MovedCodeAdviceInput {
64
+ /** Result of {@link detectMovedCodeOverlaps}. */
65
+ overlaps: readonly MovedCodeOverlap[];
66
+ /** The filename the pending export would take. */
67
+ newPatchName: string;
68
+ /** The order the pending export would take. */
69
+ insertionOrder: number;
70
+ /**
71
+ * New system modules in the pending diff that no patch registers
72
+ * (`unregistered-system-module` findings on the new patch).
73
+ */
74
+ unregisteredModules?: readonly string[];
75
+ /**
76
+ * True when every error in the exported content is a missing
77
+ * registration: registering is then the whole fix, and adopt-then-split
78
+ * is not offered at all.
79
+ */
80
+ registrationResolvesAll?: boolean;
81
+ /**
82
+ * Why adopting the new files into the strongest overlap's source patch
83
+ * would itself be refused (forward imports, size limit). Empty when the
84
+ * adopt is clean.
85
+ */
86
+ adoptBlockers?: readonly string[];
87
+ }
51
88
  /**
52
- * Renders the adopt-then-split remedy for the strongest overlap, as lines
53
- * appended to the refusal's detail list.
89
+ * Renders the remedy lines appended to a placement refusal: "register
90
+ * first" when new modules lack their moz.build registration, then the
91
+ * adopt-then-split sequence for a real moved-code block, but only when the
92
+ * adopt itself would pass. Each claim names its evidence (the matched
93
+ * lines, the blocking findings) so it can be checked.
54
94
  *
55
- * @param overlaps - Result of {@link detectMovedCodeOverlaps}
56
- * @param newPatchName - The filename the pending export would take
57
- * @param insertionOrder - The order the pending export would take
58
- * @returns Detail lines, or an empty list when the shape is not present
95
+ * @param input - See {@link MovedCodeAdviceInput}
96
+ * @returns Detail lines, or an empty list when neither shape is present
59
97
  */
60
- export declare function formatAdoptThenSplitRemedy(overlaps: readonly MovedCodeOverlap[], newPatchName: string, insertionOrder: number): string[];
98
+ export declare function formatMovedCodeAdvice(input: MovedCodeAdviceInput): string[];
@@ -38,12 +38,67 @@ function isSubstantiveLine(line) {
38
38
  return false;
39
39
  return true;
40
40
  }
41
- /** Minimum shared substantive lines before the shape is claimed. */
42
- const MOVED_CODE_LINE_THRESHOLD = 3;
43
41
  /**
44
- * Finds existing patches that still carry substantive lines the pending
45
- * diff's new files would add, the fingerprint of code moved out of an
46
- * existing patch into a new file.
42
+ * Minimum run of consecutive substantive lines, in the same order in both
43
+ * the new file and the source patch, before a block counts as moved code.
44
+ * Matching scattered single lines instead let a large unrelated patch
45
+ * "share" a dozen common statements with any new module and sent operators
46
+ * down an adopt-then-split sequence through a patch that only carried a
47
+ * `<script>` line for the file.
48
+ */
49
+ const MOVED_CODE_BLOCK_THRESHOLD = 3;
50
+ function substantiveSequences(diff) {
51
+ const sequences = new Map();
52
+ for (const [file, lines] of extractAddedLinesPerFile(diff)) {
53
+ const substantive = lines.map((line) => line.trim()).filter(isSubstantiveLine);
54
+ if (substantive.length > 0)
55
+ sequences.set(file, substantive);
56
+ }
57
+ return sequences;
58
+ }
59
+ /**
60
+ * Longest-first, non-overlapping runs of `pending` that appear contiguously
61
+ * in one of `sources`' files. Anchored on exact line matches, so the work
62
+ * is bounded by the number of shared lines, not the product of the sizes.
63
+ */
64
+ function findBlocks(file, pending, sources) {
65
+ const positions = new Map();
66
+ for (const [sourceFile, lines] of sources) {
67
+ lines.forEach((line, index) => {
68
+ const list = positions.get(line) ?? [];
69
+ list.push([sourceFile, index]);
70
+ positions.set(line, list);
71
+ });
72
+ }
73
+ const blocks = [];
74
+ let i = 0;
75
+ while (i < pending.length) {
76
+ let best;
77
+ for (const [sourceFile, start] of positions.get(pending[i] ?? '') ?? []) {
78
+ const source = sources.get(sourceFile) ?? [];
79
+ let length = 0;
80
+ while (i + length < pending.length &&
81
+ start + length < source.length &&
82
+ pending[i + length] === source[start + length]) {
83
+ length += 1;
84
+ }
85
+ if (best === undefined || length > best.length)
86
+ best = { sourceFile, length };
87
+ }
88
+ if (best !== undefined && best.length >= MOVED_CODE_BLOCK_THRESHOLD) {
89
+ blocks.push({ file, sourceFile: best.sourceFile, lines: pending.slice(i, i + best.length) });
90
+ i += best.length;
91
+ }
92
+ else {
93
+ i += 1;
94
+ }
95
+ }
96
+ return blocks;
97
+ }
98
+ /**
99
+ * Finds existing patches that still carry a block of substantive lines the
100
+ * pending diff's new files would add, the fingerprint of code moved out of
101
+ * an existing patch into a new file.
47
102
  *
48
103
  * Pure. Overlaps are returned strongest-first so the message names the
49
104
  * most likely source patch.
@@ -53,59 +108,95 @@ const MOVED_CODE_LINE_THRESHOLD = 3;
53
108
  * @returns Overlaps, or an empty list when the shape is not present
54
109
  */
55
110
  export function detectMovedCodeOverlaps(pendingDiff, entries) {
56
- const pendingByFile = extractAddedLinesPerFile(pendingDiff);
57
- const pendingLines = new Map();
58
- for (const [file, lines] of pendingByFile) {
59
- const substantive = new Set(lines.map((line) => line.trim()).filter(isSubstantiveLine));
60
- if (substantive.size > 0)
61
- pendingLines.set(file, substantive);
62
- }
63
- if (pendingLines.size === 0)
111
+ const pending = substantiveSequences(pendingDiff);
112
+ if (pending.size === 0)
64
113
  return [];
65
114
  const overlaps = [];
66
115
  for (const entry of entries) {
67
- const existing = new Set();
68
- for (const lines of extractAddedLinesPerFile(entry.diff).values()) {
69
- for (const line of lines) {
70
- const trimmed = line.trim();
71
- if (isSubstantiveLine(trimmed))
72
- existing.add(trimmed);
73
- }
74
- }
75
- if (existing.size === 0)
116
+ const sources = substantiveSequences(entry.diff);
117
+ if (sources.size === 0)
76
118
  continue;
77
- const files = [];
78
- const shared = new Set();
79
- for (const [file, lines] of pendingLines) {
80
- let fileShared = 0;
81
- for (const line of lines) {
82
- if (!existing.has(line))
83
- continue;
84
- shared.add(line);
85
- fileShared += 1;
86
- }
87
- if (fileShared > 0)
88
- files.push(file);
89
- }
90
- if (shared.size >= MOVED_CODE_LINE_THRESHOLD && files.length > 0) {
91
- overlaps.push({ sourcePatch: entry.filename, files: files.sort(), sharedLines: shared.size });
92
- }
119
+ const blocks = [...pending].flatMap(([file, lines]) => findBlocks(file, lines, sources));
120
+ if (blocks.length === 0)
121
+ continue;
122
+ overlaps.push({
123
+ sourcePatch: entry.filename,
124
+ files: [...new Set(blocks.map((block) => block.file))].sort(),
125
+ sharedLines: blocks.reduce((sum, block) => sum + block.lines.length, 0),
126
+ blocks,
127
+ });
93
128
  }
94
129
  return overlaps.sort((a, b) => b.sharedLines - a.sharedLines || a.sourcePatch.localeCompare(b.sourcePatch));
95
130
  }
131
+ /** Lines shown per matched block, and blocks shown per overlap. */
132
+ const SHOWN_BLOCK_LINES = 4;
133
+ const SHOWN_BLOCKS = 3;
134
+ /** Renders the matched blocks of one overlap, capped, as detail lines. */
135
+ function formatMatchedBlocks(overlap) {
136
+ const lines = [];
137
+ for (const block of overlap.blocks.slice(0, SHOWN_BLOCKS)) {
138
+ lines.push(` ${block.file} ← ${overlap.sourcePatch} (${block.sourceFile}), ${block.lines.length} line(s):`);
139
+ for (const line of block.lines.slice(0, SHOWN_BLOCK_LINES))
140
+ lines.push(` | ${line}`);
141
+ if (block.lines.length > SHOWN_BLOCK_LINES) {
142
+ lines.push(` | … ${block.lines.length - SHOWN_BLOCK_LINES} more`);
143
+ }
144
+ }
145
+ if (overlap.blocks.length > SHOWN_BLOCKS) {
146
+ lines.push(` … ${overlap.blocks.length - SHOWN_BLOCKS} more block(s)`);
147
+ }
148
+ return lines;
149
+ }
150
+ /**
151
+ * Renders the remedy lines appended to a placement refusal: "register
152
+ * first" when new modules lack their moz.build registration, then the
153
+ * adopt-then-split sequence for a real moved-code block, but only when the
154
+ * adopt itself would pass. Each claim names its evidence (the matched
155
+ * lines, the blocking findings) so it can be checked.
156
+ *
157
+ * @param input - See {@link MovedCodeAdviceInput}
158
+ * @returns Detail lines, or an empty list when neither shape is present
159
+ */
160
+ export function formatMovedCodeAdvice(input) {
161
+ const lines = [];
162
+ const unregistered = input.unregisteredModules ?? [];
163
+ if (unregistered.length > 0) {
164
+ lines.push('', `Register first: ${unregistered.join(', ')} ${unregistered.length === 1 ? 'is a new system module' : 'are new system modules'} ` +
165
+ 'no patch registers in moz.build. Add the moz.build line in the patch that owns that ' +
166
+ 'moz.build; when its line is already in the working tree but unexported, declare it as a ' +
167
+ 'staged registration on that patch, re-run this export, then re-export that patch.' +
168
+ (input.registrationResolvesAll === true
169
+ ? ' The exported content has no other cross-patch error, so once registered the new ' +
170
+ 'files need no adoption into another patch.'
171
+ : ''));
172
+ }
173
+ const strongest = input.overlaps[0];
174
+ if (strongest === undefined || input.registrationResolvesAll === true)
175
+ return lines;
176
+ const blockers = input.adoptBlockers ?? [];
177
+ const claim = `${strongest.sharedLines} substantive line(s) the new file(s) would add, in ` +
178
+ `${strongest.blocks.length} matching block(s), are still carried by ${strongest.sourcePatch}:`;
179
+ if (blockers.length > 0) {
180
+ lines.push('', claim, ...formatMatchedBlocks(strongest), `Adopting the new files into ${strongest.sourcePatch} first (adopt-then-split) would be refused too, so it is not offered:`, ...blockers.map((blocker) => ` - ${blocker}`));
181
+ return lines;
182
+ }
183
+ const adopt = formatAdoptThenSplitRemedy(strongest, input.newPatchName, input.insertionOrder);
184
+ if (unregistered.length > 0) {
185
+ adopt.splice(1, 0, 'Only if the export still fails once those modules are registered:');
186
+ }
187
+ lines.push(...adopt, `Matched lines: ${claim}`, ...formatMatchedBlocks(strongest));
188
+ return lines;
189
+ }
96
190
  /**
97
191
  * Renders the adopt-then-split remedy for the strongest overlap, as lines
98
192
  * appended to the refusal's detail list.
99
193
  *
100
- * @param overlaps - Result of {@link detectMovedCodeOverlaps}
194
+ * @param strongest - The strongest overlap {@link detectMovedCodeOverlaps} found
101
195
  * @param newPatchName - The filename the pending export would take
102
196
  * @param insertionOrder - The order the pending export would take
103
- * @returns Detail lines, or an empty list when the shape is not present
197
+ * @returns Detail lines
104
198
  */
105
- export function formatAdoptThenSplitRemedy(overlaps, newPatchName, insertionOrder) {
106
- const strongest = overlaps[0];
107
- if (strongest === undefined)
108
- return [];
199
+ function formatAdoptThenSplitRemedy(strongest, newPatchName, insertionOrder) {
109
200
  const fileArgs = strongest.files.map((file) => `--scan-file ${file}`).join(' ');
110
201
  const moveArgs = strongest.files.map((file) => `--file ${file}`).join(' ');
111
202
  // `patch move-files --create` takes the new patch's name, not a
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Live queue state around a contended lock: how many are queued and how many
3
+ * are ahead of you. Waiters are granted the lock in arrival order, so `ahead`
4
+ * is the number of grants that happen before yours.
5
+ */
6
+ export interface LockQueueState {
7
+ /** Live waiters currently queued for the lock, including this process. */
8
+ depth: number;
9
+ /** Live waiters that arrived before this process. */
10
+ ahead: number;
11
+ }
12
+ /**
13
+ * A ticket not refreshed for this long is skipped when the queue is read.
14
+ * Six heartbeats of slack: long enough that a loaded host never skips a
15
+ * waiter that is merely slow, short enough that a stopped waiter does not
16
+ * hold the head of the queue for more than half a minute.
17
+ */
18
+ export declare const WAITER_STALE_MS = 30000;
19
+ /** Sibling directory holding the waiter tickets of a lock. */
20
+ export declare function lockWaitersDir(lockPath: string): string;
21
+ /**
22
+ * This process's view of itself in the queue: its wait start, and its ticket
23
+ * when it holds one. Without a ticket the position falls back to comparing
24
+ * wait starts, which is what a waiter whose registration failed can know.
25
+ */
26
+ export interface QueueSelf {
27
+ startedAt: number;
28
+ ticket?: string | undefined;
29
+ }
30
+ /**
31
+ * Reads the live waiter queue around `lockPath`.
32
+ *
33
+ * Advisory and fail-open: an unreadable registry reports an empty queue
34
+ * rather than failing whatever asked. The caller's own ticket always counts
35
+ * as live, since this process knows it is waiting.
36
+ *
37
+ * @param lockPath - The lock directory whose queue to inspect
38
+ * @param self - This waiter, to compute `ahead`
39
+ */
40
+ export declare function readLockQueue(lockPath: string, self?: QueueSelf): Promise<LockQueueState>;
41
+ /** A waiter's place in the queue, from joining it to leaving it. */
42
+ export interface WaiterRegistration {
43
+ /** Ticket name, or `undefined` when registration failed. */
44
+ ticket: string | undefined;
45
+ /** Refreshes the ticket when a heartbeat is due. Never throws. */
46
+ heartbeat: () => Promise<void>;
47
+ /** Leaves the queue. Always safe to call. */
48
+ deregister: () => Promise<void>;
49
+ }
50
+ /**
51
+ * Takes a ticket in the lock's waiter queue.
52
+ *
53
+ * Never fails the acquisition: a registry that cannot be written yields a
54
+ * registration without a ticket, and the waiter falls back to contending
55
+ * for the lock without queue order, which is how every waiter behaved
56
+ * before tickets decided the grant.
57
+ *
58
+ * @param lockPath - The contended lock directory
59
+ * @param startedAt - When this waiter started waiting (its arrival time)
60
+ */
61
+ export declare function registerWaiter(lockPath: string, startedAt: number): Promise<WaiterRegistration>;
62
+ /**
63
+ * Renders the queue position for a wait line, with the ordering rule, so the
64
+ * reader knows the count is a promise and not a snapshot. Empty when nothing
65
+ * is queued.
66
+ */
67
+ export declare function formatLockQueuePosition(queue: LockQueueState | undefined): string;
@@ -0,0 +1,221 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Waiter tickets for `withFileLock`: who is queued for a contended lock, in
4
+ * what order, and whose turn it is.
5
+ *
6
+ * The queue started as a display: waiters advertised themselves so the wait
7
+ * line could say "You are next", but the grant itself was a free-for-all of
8
+ * `mkdir` polls. A waiter whose poll interval had backed off to 2 s lost
9
+ * every free moment to later arrivals polling at 100 ms, so a run told it
10
+ * was next for twenty minutes watched three later runs take the lock in
11
+ * turn. The tickets now decide the grant: only the head of the queue may
12
+ * attempt the lock, so the position printed is the position honoured.
13
+ *
14
+ * A ticket is a file in `<lock>.waiters/` named
15
+ * `<startedAtMs>-<pid>-<uuid>`. Arrival order is the wait start, with the
16
+ * whole name as a tie-break so every reader sorts the queue identically.
17
+ * Because a ticket now blocks everyone behind it, a ticket whose owner is
18
+ * gone must not hold the queue: a dead PID (or a reused one, where the
19
+ * start-tick check can tell) drops out and its ticket is removed, and a
20
+ * ticket its owner has stopped refreshing drops out without being removed,
21
+ * which covers a stopped (^Z) waiter and a reused PID on platforms with no
22
+ * start tick.
23
+ */
24
+ import { randomUUID } from 'node:crypto';
25
+ import { readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
26
+ import { join } from 'node:path';
27
+ import { toError } from '../utils/errors.js';
28
+ import { ensureDir } from '../utils/fs.js';
29
+ import { verbose } from '../utils/logger.js';
30
+ import { isLockOwnerAlive, readProcessStartTick } from './file-lock-owner.js';
31
+ /**
32
+ * How often a waiter refreshes its ticket. Every wait loop iteration
33
+ * refreshes once this much time has passed, and no caller sleeps longer
34
+ * than a few seconds between iterations.
35
+ */
36
+ const WAITER_HEARTBEAT_MS = 5_000;
37
+ /**
38
+ * A ticket not refreshed for this long is skipped when the queue is read.
39
+ * Six heartbeats of slack: long enough that a loaded host never skips a
40
+ * waiter that is merely slow, short enough that a stopped waiter does not
41
+ * hold the head of the queue for more than half a minute.
42
+ */
43
+ export const WAITER_STALE_MS = 30_000;
44
+ /** Prefix of the ticket line carrying the waiter's process start tick. */
45
+ const START_TICK_PREFIX = 'start-tick=';
46
+ /** Sibling directory holding the waiter tickets of a lock. */
47
+ export function lockWaitersDir(lockPath) {
48
+ return `${lockPath}.waiters`;
49
+ }
50
+ /** Parses a ticket name: `<startedAtMs>-<pid>-<uuid>`. */
51
+ function parseTicketName(name) {
52
+ const match = /^(\d+)-(\d+)-/.exec(name);
53
+ const startedAt = match?.[1];
54
+ const pid = match?.[2];
55
+ if (startedAt === undefined || pid === undefined)
56
+ return undefined;
57
+ return { name, startedAt: Number.parseInt(startedAt, 10), pid: Number.parseInt(pid, 10) };
58
+ }
59
+ /** Arrival order: wait start first, then the name, so every reader agrees. */
60
+ function compareTickets(a, b) {
61
+ if (a.startedAt !== b.startedAt)
62
+ return a.startedAt - b.startedAt;
63
+ if (a.name === b.name)
64
+ return 0;
65
+ return a.name < b.name ? -1 : 1;
66
+ }
67
+ /** Reads the start tick a ticket recorded, when its writer could record one. */
68
+ async function readTicketStartTick(path) {
69
+ const content = await readFile(path, 'utf-8');
70
+ const line = content.split('\n').find((entry) => entry.startsWith(START_TICK_PREFIX));
71
+ if (line === undefined)
72
+ return undefined;
73
+ const tick = Number.parseInt(line.slice(START_TICK_PREFIX.length), 10);
74
+ return Number.isFinite(tick) ? tick : undefined;
75
+ }
76
+ /**
77
+ * Whether a ticket still holds a place in the queue. A ticket whose owner is
78
+ * provably gone is removed so the directory does not accumulate them; one
79
+ * that has merely stopped being refreshed is skipped but left alone, since
80
+ * its owner may resume and refresh it.
81
+ */
82
+ async function isTicketLive(dir, ticket, now) {
83
+ const path = join(dir, ticket.name);
84
+ let startTick;
85
+ let mtimeMs;
86
+ try {
87
+ startTick = await readTicketStartTick(path);
88
+ mtimeMs = (await stat(path)).mtimeMs;
89
+ }
90
+ catch {
91
+ // Deregistered between the listing and this read.
92
+ return false;
93
+ }
94
+ if (!isLockOwnerAlive({ pid: ticket.pid, startTick })) {
95
+ await rm(path, { force: true }).catch((error) => {
96
+ verbose(`Could not remove dead lock waiter ticket ${path}: ${toError(error).message}`);
97
+ });
98
+ return false;
99
+ }
100
+ return now - mtimeMs <= WAITER_STALE_MS;
101
+ }
102
+ /**
103
+ * Reads the live waiter queue around `lockPath`.
104
+ *
105
+ * Advisory and fail-open: an unreadable registry reports an empty queue
106
+ * rather than failing whatever asked. The caller's own ticket always counts
107
+ * as live, since this process knows it is waiting.
108
+ *
109
+ * @param lockPath - The lock directory whose queue to inspect
110
+ * @param self - This waiter, to compute `ahead`
111
+ */
112
+ export async function readLockQueue(lockPath, self) {
113
+ const dir = lockWaitersDir(lockPath);
114
+ let names;
115
+ try {
116
+ names = await readdir(dir);
117
+ }
118
+ catch {
119
+ return { depth: 0, ahead: 0 };
120
+ }
121
+ const now = Date.now();
122
+ const tickets = names
123
+ .map(parseTicketName)
124
+ .filter((ticket) => ticket !== undefined);
125
+ const liveFlags = await Promise.all(tickets.map((ticket) => ticket.name === self?.ticket ? Promise.resolve(true) : isTicketLive(dir, ticket, now)));
126
+ const live = tickets.filter((_, index) => liveFlags[index] === true);
127
+ if (self === undefined)
128
+ return { depth: live.length, ahead: 0 };
129
+ if (self.ticket === undefined) {
130
+ return {
131
+ depth: live.length,
132
+ ahead: live.filter((ticket) => ticket.startedAt < self.startedAt).length,
133
+ };
134
+ }
135
+ const own = parseTicketName(self.ticket);
136
+ // Our ticket can be missing from the listing only if something removed it;
137
+ // we still count ourselves, and the next heartbeat recreates it.
138
+ const listed = live.some((ticket) => ticket.name === self.ticket);
139
+ return {
140
+ depth: live.length + (listed ? 0 : 1),
141
+ ahead: own === undefined
142
+ ? 0
143
+ : live.filter((ticket) => ticket.name !== own.name && compareTickets(ticket, own) < 0)
144
+ .length,
145
+ };
146
+ }
147
+ /**
148
+ * Takes a ticket in the lock's waiter queue.
149
+ *
150
+ * Never fails the acquisition: a registry that cannot be written yields a
151
+ * registration without a ticket, and the waiter falls back to contending
152
+ * for the lock without queue order, which is how every waiter behaved
153
+ * before tickets decided the grant.
154
+ *
155
+ * @param lockPath - The contended lock directory
156
+ * @param startedAt - When this waiter started waiting (its arrival time)
157
+ */
158
+ export async function registerWaiter(lockPath, startedAt) {
159
+ const dir = lockWaitersDir(lockPath);
160
+ const ticket = `${startedAt}-${process.pid}-${randomUUID()}`;
161
+ const file = join(dir, ticket);
162
+ const startTick = readProcessStartTick(process.pid);
163
+ const content = `${process.pid}\n${startTick !== undefined ? `${START_TICK_PREFIX}${startTick}\n` : ''}`;
164
+ try {
165
+ await ensureDir(dir);
166
+ await writeFile(file, content, 'utf-8');
167
+ }
168
+ catch (error) {
169
+ verbose(`Could not register lock waiter for ${lockPath}: ${toError(error).message}`);
170
+ return {
171
+ ticket: undefined,
172
+ heartbeat: async () => {
173
+ /* nothing to refresh */
174
+ },
175
+ deregister: async () => {
176
+ /* nothing to remove */
177
+ },
178
+ };
179
+ }
180
+ let lastBeatAt = Date.now();
181
+ return {
182
+ ticket,
183
+ heartbeat: async () => {
184
+ if (Date.now() - lastBeatAt < WAITER_HEARTBEAT_MS)
185
+ return;
186
+ lastBeatAt = Date.now();
187
+ try {
188
+ // A rewrite, not a utimes: it also recreates a ticket that was
189
+ // removed while this process was stopped long enough to look dead.
190
+ await ensureDir(dir);
191
+ await writeFile(file, content, 'utf-8');
192
+ }
193
+ catch (error) {
194
+ verbose(`Could not refresh lock waiter ticket ${file}: ${toError(error).message}`);
195
+ }
196
+ },
197
+ deregister: async () => {
198
+ try {
199
+ await rm(file, { force: true });
200
+ }
201
+ catch (error) {
202
+ // A leftover ticket of a process about to exit drops out on its own
203
+ // (dead PID), so this never needs to fail the caller.
204
+ verbose(`Could not remove lock waiter ticket ${file}: ${toError(error).message}`);
205
+ }
206
+ },
207
+ };
208
+ }
209
+ /**
210
+ * Renders the queue position for a wait line, with the ordering rule, so the
211
+ * reader knows the count is a promise and not a snapshot. Empty when nothing
212
+ * is queued.
213
+ */
214
+ export function formatLockQueuePosition(queue) {
215
+ if (queue === undefined || queue.depth === 0)
216
+ return '';
217
+ if (queue.ahead === 0) {
218
+ return ` You are next in a queue of ${queue.depth} (granted in arrival order).`;
219
+ }
220
+ return ` ${queue.ahead} ahead of you (queue of ${queue.depth}, granted in arrival order).`;
221
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared holds on a FireForge directory lock.
3
+ *
4
+ * Every lock used to be exclusive, so two build-less `fireforge test` runs
5
+ * on one objdir, each with its own profile and (under auto ports) its own
6
+ * ports, still waited for each other: the gate's browser tiers ran strictly
7
+ * serially on a 14-core host. A shared hold lets such runs overlap while
8
+ * anything that writes (a build, a re-export, a test run that builds)
9
+ * still holds the lock alone.
10
+ *
11
+ * The scheme reuses the exclusive lock as a turnstile, so arrival order and
12
+ * stale-holder reaping come from the machinery that already exists:
13
+ *
14
+ * - A shared holder takes the exclusive lock in the ordinary way, records
15
+ * itself under `<lock>.shared/<uuid>/` (an owner record like the lock's
16
+ * own), and releases the exclusive lock at once.
17
+ * - An exclusive holder takes the exclusive lock, then waits until no live
18
+ * shared holder is left. While it waits it already holds the turnstile,
19
+ * so no new shared holder can slip in ahead of it: a writer queued
20
+ * behind readers is served once they finish, never starved by newcomers.
21
+ *
22
+ * A shared holder whose process died is reaped by the same PID-first
23
+ * liveness check the exclusive lock uses.
24
+ */
25
+ /** How a caller holds a lock. */
26
+ export type LockMode = 'exclusive' | 'shared';
27
+ /** The directory holding a lock's shared-holder records. */
28
+ export declare function sharedHoldersDir(lockPath: string): string;
29
+ /**
30
+ * Counts the live shared holders of a lock, reaping any whose process is
31
+ * gone. A missing directory means none.
32
+ *
33
+ * @param lockPath - The lock directory
34
+ * @param options - `reap: false` only counts (status reads)
35
+ */
36
+ export declare function countLiveSharedHolders(lockPath: string, options?: {
37
+ reap?: boolean;
38
+ }): Promise<number>;
39
+ /** A registered shared hold. */
40
+ export interface SharedHold {
41
+ /** Removes the hold. Never throws. */
42
+ release(): Promise<void>;
43
+ }
44
+ /**
45
+ * Records a shared hold. Call while holding the lock's exclusive turnstile.
46
+ *
47
+ * @param lockPath - The lock directory
48
+ * @param metadata - Owner metadata lines (`command=…`)
49
+ */
50
+ export declare function registerSharedHold(lockPath: string, metadata: readonly string[] | undefined): Promise<SharedHold>;
51
+ /**
52
+ * Removes every shared hold this process registered, for the signal path.
53
+ * Never throws.
54
+ *
55
+ * @returns The holder paths removed
56
+ */
57
+ export declare function releaseSharedHoldsForSignal(): Promise<string[]>;