@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.
- package/CHANGELOG.md +87 -1
- package/README.md +1 -1
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build-prelock.d.ts +20 -0
- package/dist/src/commands/build-prelock.js +50 -0
- package/dist/src/commands/build.d.ts +3 -1
- package/dist/src/commands/build.js +13 -6
- package/dist/src/commands/check-report.d.ts +41 -0
- package/dist/src/commands/check-report.js +91 -0
- package/dist/src/commands/check-run.d.ts +33 -0
- package/dist/src/commands/check-run.js +112 -0
- package/dist/src/commands/check-steps.d.ts +75 -0
- package/dist/src/commands/check-steps.js +200 -0
- package/dist/src/commands/check.d.ts +32 -0
- package/dist/src/commands/check.js +160 -0
- package/dist/src/commands/config.js +5 -6
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/doctor-engine-eslint.js +6 -1
- package/dist/src/commands/doctor-source-pin.js +4 -1
- package/dist/src/commands/export-adopt-check.d.ts +38 -0
- package/dist/src/commands/export-adopt-check.js +98 -0
- package/dist/src/commands/export-flow.js +29 -14
- package/dist/src/commands/export-placement-gate.d.ts +6 -2
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-staged-registration.d.ts +46 -0
- package/dist/src/commands/export-staged-registration.js +91 -0
- package/dist/src/commands/forward-import-remedy.d.ts +32 -0
- package/dist/src/commands/forward-import-remedy.js +63 -0
- package/dist/src/commands/furnace/apply.js +3 -3
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
- package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc.js +2 -2
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
- package/dist/src/commands/furnace/deploy-named.js +107 -0
- package/dist/src/commands/furnace/deploy.d.ts +3 -2
- package/dist/src/commands/furnace/deploy.js +50 -86
- package/dist/src/commands/furnace/diff.js +26 -3
- package/dist/src/commands/furnace/index.js +66 -41
- package/dist/src/commands/furnace/init.js +13 -6
- package/dist/src/commands/furnace/override.js +4 -2
- package/dist/src/commands/furnace/preview.js +4 -3
- package/dist/src/commands/furnace/refresh.js +2 -2
- package/dist/src/commands/furnace/remove.js +4 -3
- package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
- package/dist/src/commands/furnace/rename-dry-run.js +127 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
- package/dist/src/commands/furnace/rename-helpers.js +15 -1
- package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
- package/dist/src/commands/furnace/rename-registrations.js +98 -0
- package/dist/src/commands/furnace/rename.d.ts +2 -1
- package/dist/src/commands/furnace/rename.js +28 -66
- package/dist/src/commands/furnace/scan.d.ts +9 -4
- package/dist/src/commands/furnace/scan.js +11 -9
- package/dist/src/commands/furnace/sync.js +3 -2
- package/dist/src/commands/furnace/validate-report.d.ts +45 -0
- package/dist/src/commands/furnace/validate-report.js +91 -0
- package/dist/src/commands/furnace/validate.d.ts +1 -1
- package/dist/src/commands/furnace/validate.js +151 -113
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +36 -61
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +20 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +4 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete-gap-notice.js +3 -1
- package/dist/src/commands/patch/delete.d.ts +1 -1
- package/dist/src/commands/patch/delete.js +24 -104
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +10 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +78 -31
- package/dist/src/commands/patch/move-files-create.js +34 -18
- package/dist/src/commands/patch/move-files-into.js +15 -7
- package/dist/src/commands/patch/move-files.js +6 -2
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
- package/dist/src/commands/patch/removal-dependents.js +133 -0
- package/dist/src/commands/patch/rename.js +20 -1
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +133 -0
- package/dist/src/commands/patch/split-plan.d.ts +17 -8
- package/dist/src/commands/patch/split-plan.js +60 -28
- package/dist/src/commands/patch/split-staged.d.ts +108 -0
- package/dist/src/commands/patch/split-staged.js +202 -0
- package/dist/src/commands/patch/split.d.ts +5 -0
- package/dist/src/commands/patch/split.js +62 -17
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
- package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
- package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
- package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
- package/dist/src/commands/patch/staged-dependency-from.js +108 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +54 -139
- package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
- package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
- package/dist/src/commands/re-export-adjacent.d.ts +50 -2
- package/dist/src/commands/re-export-adjacent.js +160 -40
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-drift.d.ts +36 -8
- package/dist/src/commands/re-export-drift.js +73 -49
- package/dist/src/commands/re-export-files.js +11 -8
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-lint.d.ts +2 -1
- package/dist/src/commands/re-export-lint.js +23 -10
- package/dist/src/commands/re-export-options.js +74 -6
- package/dist/src/commands/re-export-preflight.d.ts +23 -0
- package/dist/src/commands/re-export-preflight.js +160 -0
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +56 -17
- package/dist/src/commands/re-export-removed.d.ts +13 -2
- package/dist/src/commands/re-export-removed.js +57 -12
- package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
- package/dist/src/commands/re-export-scan-owner.js +116 -0
- package/dist/src/commands/re-export-scan.d.ts +19 -9
- package/dist/src/commands/re-export-scan.js +60 -24
- package/dist/src/commands/re-export-single.js +122 -46
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export-summary.d.ts +28 -0
- package/dist/src/commands/re-export-summary.js +36 -0
- package/dist/src/commands/re-export.js +71 -57
- package/dist/src/commands/rebase/index.js +17 -17
- package/dist/src/commands/run.js +16 -2
- package/dist/src/commands/status-check.d.ts +55 -16
- package/dist/src/commands/status-check.js +92 -37
- package/dist/src/commands/status-json.d.ts +22 -9
- package/dist/src/commands/status-json.js +47 -32
- package/dist/src/commands/status-output.js +3 -1
- package/dist/src/commands/status-scope.d.ts +37 -0
- package/dist/src/commands/status-scope.js +54 -0
- package/dist/src/commands/status.js +66 -23
- package/dist/src/commands/test-browser-preflight.js +19 -3
- package/dist/src/commands/test-build-furnace.d.ts +22 -0
- package/dist/src/commands/test-build-furnace.js +54 -0
- package/dist/src/commands/test-build-phase.js +64 -41
- package/dist/src/commands/test-checkjs.js +10 -2
- package/dist/src/commands/test-diagnose.js +7 -0
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-modes.d.ts +16 -1
- package/dist/src/commands/test-modes.js +24 -1
- package/dist/src/commands/test-multi-verdict.d.ts +61 -0
- package/dist/src/commands/test-multi-verdict.js +111 -0
- package/dist/src/commands/test-orchestrate.d.ts +52 -0
- package/dist/src/commands/test-orchestrate.js +240 -0
- package/dist/src/commands/test-perf-scope.d.ts +50 -0
- package/dist/src/commands/test-perf-scope.js +138 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +273 -0
- package/dist/src/commands/test-register.js +69 -10
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +74 -99
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +60 -28
- package/dist/src/commands/test-verdict.d.ts +11 -1
- package/dist/src/commands/test-verdict.js +28 -1
- package/dist/src/commands/test.d.ts +6 -1
- package/dist/src/commands/test.js +120 -124
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +151 -0
- package/dist/src/commands/token.js +10 -0
- package/dist/src/commands/verify-scope.d.ts +28 -0
- package/dist/src/commands/verify-scope.js +67 -0
- package/dist/src/commands/verify.d.ts +10 -2
- package/dist/src/commands/verify.js +67 -36
- package/dist/src/core/build-baseline-types.d.ts +46 -0
- package/dist/src/core/build-baseline-validate.js +19 -0
- package/dist/src/core/build-baseline.d.ts +5 -1
- package/dist/src/core/build-baseline.js +3 -1
- package/dist/src/core/build-overwrite-guard.d.ts +50 -0
- package/dist/src/core/build-overwrite-guard.js +84 -4
- package/dist/src/core/build-prepare.d.ts +13 -0
- package/dist/src/core/build-prepare.js +42 -10
- package/dist/src/core/build-run-record.d.ts +74 -0
- package/dist/src/core/build-run-record.js +136 -0
- package/dist/src/core/checkjs-support-heads.d.ts +41 -0
- package/dist/src/core/checkjs-support-heads.js +104 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +10 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +219 -0
- package/dist/src/core/config-validate-file-size.d.ts +9 -6
- package/dist/src/core/config-validate-file-size.js +91 -2
- package/dist/src/core/config-validate-history.d.ts +11 -0
- package/dist/src/core/config-validate-history.js +38 -0
- package/dist/src/core/config-validate-patch-policy.js +100 -0
- package/dist/src/core/config-validate-test-toolchains.js +21 -0
- package/dist/src/core/config-validate-token-scales.d.ts +9 -0
- package/dist/src/core/config-validate-token-scales.js +92 -0
- package/dist/src/core/config-validate.js +20 -5
- package/dist/src/core/config.d.ts +5 -0
- package/dist/src/core/config.js +16 -0
- package/dist/src/core/coverage-dispatch.d.ts +24 -0
- package/dist/src/core/coverage-dispatch.js +44 -0
- package/dist/src/core/coverage-extend.d.ts +70 -45
- package/dist/src/core/coverage-extend.js +199 -144
- package/dist/src/core/coverage-records.d.ts +108 -0
- package/dist/src/core/coverage-records.js +384 -0
- package/dist/src/core/destructive.d.ts +7 -4
- package/dist/src/core/destructive.js +10 -10
- package/dist/src/core/engine-session-lock.d.ts +16 -1
- package/dist/src/core/engine-session-lock.js +38 -10
- package/dist/src/core/export-moved-code-shape.d.ts +49 -11
- package/dist/src/core/export-moved-code-shape.js +135 -44
- package/dist/src/core/file-lock-queue.d.ts +67 -0
- package/dist/src/core/file-lock-queue.js +221 -0
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +13 -25
- package/dist/src/core/file-lock.js +163 -140
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +31 -1
- package/dist/src/core/furnace-apply.js +22 -12
- package/dist/src/core/furnace-composed-source.d.ts +76 -0
- package/dist/src/core/furnace-composed-source.js +167 -0
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +16 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
- package/dist/src/core/furnace-config-validate.js +14 -0
- package/dist/src/core/furnace-css-fragments.d.ts +10 -0
- package/dist/src/core/furnace-css-fragments.js +26 -0
- package/dist/src/core/furnace-operation.d.ts +3 -3
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-registration-remove.d.ts +22 -1
- package/dist/src/core/furnace-registration-remove.js +53 -17
- package/dist/src/core/furnace-registration-rename.d.ts +26 -0
- package/dist/src/core/furnace-registration-rename.js +77 -0
- package/dist/src/core/furnace-registration.d.ts +14 -4
- package/dist/src/core/furnace-registration.js +44 -10
- package/dist/src/core/furnace-staleness.d.ts +7 -0
- package/dist/src/core/furnace-staleness.js +34 -24
- package/dist/src/core/furnace-validate-accessibility.d.ts +8 -4
- package/dist/src/core/furnace-validate-accessibility.js +71 -28
- package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
- package/dist/src/core/furnace-validate-exceptions.js +80 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +78 -0
- package/dist/src/core/furnace-validate.js +1 -1
- package/dist/src/core/harness-port-fallback.d.ts +60 -0
- package/dist/src/core/harness-port-fallback.js +96 -0
- package/dist/src/core/history-log-file.d.ts +61 -0
- package/dist/src/core/history-log-file.js +164 -0
- package/dist/src/core/history-log.d.ts +2 -1
- package/dist/src/core/history-log.js +2 -1
- package/dist/src/core/json-preserving-edit.d.ts +22 -0
- package/dist/src/core/json-preserving-edit.js +199 -0
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +32 -10
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +51 -0
- package/dist/src/core/negative-control-diff.js +179 -0
- package/dist/src/core/negative-control.d.ts +150 -0
- package/dist/src/core/negative-control.js +399 -0
- package/dist/src/core/objdir-install-manifests.d.ts +73 -0
- package/dist/src/core/objdir-install-manifests.js +226 -0
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +6 -0
- package/dist/src/core/patch-export-update.js +13 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +22 -16
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +20 -13
- package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
- package/dist/src/core/patch-lint-code-lines.js +105 -0
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +17 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +24 -14
- package/dist/src/core/patch-lint-file-size.d.ts +12 -5
- package/dist/src/core/patch-lint-file-size.js +22 -11
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +23 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +196 -0
- package/dist/src/core/patch-lint-ignore.js +429 -0
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
- package/dist/src/core/patch-lint-size-thresholds.js +110 -0
- package/dist/src/core/patch-lint-size.d.ts +117 -0
- package/dist/src/core/patch-lint-size.js +331 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
- package/dist/src/core/patch-lint-staged-registration.js +20 -0
- package/dist/src/core/patch-lint-token-scale.d.ts +57 -0
- package/dist/src/core/patch-lint-token-scale.js +286 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +66 -288
- package/dist/src/core/patch-lock.js +7 -6
- package/dist/src/core/patch-manifest-consistency.js +7 -6
- package/dist/src/core/patch-manifest-io.d.ts +6 -3
- package/dist/src/core/patch-manifest-io.js +27 -9
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +61 -3
- package/dist/src/core/patch-order-legal.d.ts +26 -0
- package/dist/src/core/patch-order-legal.js +88 -0
- package/dist/src/core/patch-policy-content.d.ts +40 -0
- package/dist/src/core/patch-policy-content.js +216 -0
- package/dist/src/core/patch-policy.d.ts +22 -5
- package/dist/src/core/patch-policy.js +26 -12
- package/dist/src/core/patch-retired.d.ts +57 -0
- package/dist/src/core/patch-retired.js +92 -0
- package/dist/src/core/patch-subset.d.ts +16 -0
- package/dist/src/core/patch-subset.js +66 -0
- package/dist/src/core/preflight-wait.d.ts +20 -0
- package/dist/src/core/preflight-wait.js +38 -0
- package/dist/src/core/rebase-from-version.d.ts +46 -0
- package/dist/src/core/rebase-from-version.js +71 -0
- package/dist/src/core/run-log.d.ts +26 -0
- package/dist/src/core/run-log.js +106 -4
- package/dist/src/core/run-profile.d.ts +7 -0
- package/dist/src/core/run-profile.js +7 -1
- package/dist/src/core/smoke-patterns.d.ts +5 -0
- package/dist/src/core/smoke-patterns.js +13 -0
- package/dist/src/core/test-harness-crash.d.ts +19 -1
- package/dist/src/core/test-harness-crash.js +8 -9
- package/dist/src/core/test-harness-signature.d.ts +33 -0
- package/dist/src/core/test-harness-signature.js +23 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +90 -0
- package/dist/src/core/test-run-completeness.js +184 -0
- package/dist/src/core/test-stale-check.d.ts +27 -1
- package/dist/src/core/test-stale-check.js +39 -49
- package/dist/src/core/test-stale-exempt.d.ts +40 -0
- package/dist/src/core/test-stale-exempt.js +70 -0
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +53 -0
- package/dist/src/core/token-lint.js +131 -0
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +5 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -2
- package/dist/src/core/typecheck-shim.js +205 -4
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +14 -3
- package/dist/src/index.js +6 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +212 -24
- package/dist/src/types/commands/patches.d.ts +118 -1
- package/dist/src/types/config.d.ts +264 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +63 -1
- package/dist/src/types/furnace.js +7 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +36 -7
- package/dist/src/utils/options.js +94 -8
- package/dist/src/utils/path-glob.d.ts +15 -0
- package/dist/src/utils/path-glob.js +47 -0
- package/dist/src/utils/regex.d.ts +15 -3
- package/dist/src/utils/regex.js +37 -16
- package/docs/configuration.md +369 -11
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +139 -3
- package/docs/lifecycle-invariants.md +65 -30
- package/docs/machine-output.md +120 -12
- package/docs/patch-workflow.md +458 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +346 -21
- 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
|
|
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
|
|
40
|
-
* diff's new files would add, the fingerprint of code moved out of
|
|
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
|
|
53
|
-
*
|
|
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
|
|
56
|
-
* @
|
|
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
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
57
|
-
|
|
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
|
|
68
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
197
|
+
* @returns Detail lines
|
|
104
198
|
*/
|
|
105
|
-
|
|
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[]>;
|