@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
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Per-path packaging-coverage anchors for scoped `fireforge test --build`.
|
|
4
|
+
*
|
|
5
|
+
* A scoped build used to carry one claim anchored to the whole engine, so
|
|
6
|
+
* any edit outside the new scope (a peer's, on a shared engine, nearly
|
|
7
|
+
* always) replaced it. Each staged request path now carries its own anchor
|
|
8
|
+
* restricted to its own staging inputs, and a claim is dropped only when
|
|
9
|
+
* one of those inputs changed.
|
|
10
|
+
*
|
|
11
|
+
* The staging inputs of a request path are its manifest directory (the
|
|
12
|
+
* nearest directory at or above it that holds a test manifest) plus every
|
|
13
|
+
* path a manifest there names outside that directory (`support-files`,
|
|
14
|
+
* `head`, `!/`-rooted shared fixtures). Without the full mozbuild graph
|
|
15
|
+
* that is the closest sound boundary: an edit outside it cannot change
|
|
16
|
+
* what the harness stages for that path.
|
|
17
|
+
*/
|
|
18
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
19
|
+
import { join, posix } from 'node:path';
|
|
20
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
21
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
22
|
+
import { verbose } from '../utils/logger.js';
|
|
23
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
24
|
+
import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
|
|
25
|
+
import { getHead, isMissingHeadError } from './git.js';
|
|
26
|
+
import { git } from './git-base.js';
|
|
27
|
+
import { getUntrackedFiles } from './git-status.js';
|
|
28
|
+
/**
|
|
29
|
+
* Basenames of the test manifests whose entries decide what a packaged test
|
|
30
|
+
* runtime stages.
|
|
31
|
+
*/
|
|
32
|
+
const TEST_MANIFEST_BASENAMES = new Set([
|
|
33
|
+
'xpcshell.toml',
|
|
34
|
+
'xpcshell.ini',
|
|
35
|
+
'browser.toml',
|
|
36
|
+
'browser.ini',
|
|
37
|
+
'mochitest.toml',
|
|
38
|
+
'mochitest.ini',
|
|
39
|
+
'chrome.toml',
|
|
40
|
+
'chrome.ini',
|
|
41
|
+
'a11y.toml',
|
|
42
|
+
'a11y.ini',
|
|
43
|
+
]);
|
|
44
|
+
/** Directory entries a directory request's manifest walk visits at most. */
|
|
45
|
+
const MAX_WALK_ENTRIES = 5000;
|
|
46
|
+
/**
|
|
47
|
+
* Hex-encoded SHA-256 of an engine-relative file, matching the digest the
|
|
48
|
+
* baseline writer produces. A missing file hashes to
|
|
49
|
+
* {@link DELETED_FILE_FINGERPRINT}. `undefined` on any other IO error,
|
|
50
|
+
* which every caller treats as "cannot prove unchanged".
|
|
51
|
+
* @param engineDir - Path to the engine directory
|
|
52
|
+
* @param relPath - Engine-relative POSIX path
|
|
53
|
+
*/
|
|
54
|
+
export async function hashEngineFile(engineDir, relPath) {
|
|
55
|
+
try {
|
|
56
|
+
return sha256Hex(await readFile(join(engineDir, relPath)));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
60
|
+
return DELETED_FILE_FINGERPRINT;
|
|
61
|
+
verbose(`Coverage anchor: could not hash ${relPath} — ${toError(error).message}`);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** True for a test manifest path (by basename). */
|
|
66
|
+
export function isTestManifestPath(path) {
|
|
67
|
+
return TEST_MANIFEST_BASENAMES.has(posix.basename(path));
|
|
68
|
+
}
|
|
69
|
+
/** Normalizes a request path: forward slashes, no `./`, no trailing slash. */
|
|
70
|
+
export function normalizeRequestPath(path) {
|
|
71
|
+
const normalized = posix.normalize(normalizePathSlashes(path.trim())).replace(/\/+$/, '');
|
|
72
|
+
return normalized === '.' ? '' : normalized;
|
|
73
|
+
}
|
|
74
|
+
/** True when `file` is `prefix` or sits beneath it. The empty prefix is the whole tree. */
|
|
75
|
+
export function isUnderPath(file, prefix) {
|
|
76
|
+
return prefix === '' || file === prefix || file.startsWith(`${prefix}/`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads HEAD and the dirty set. Throws on a git failure other than an
|
|
80
|
+
* unborn HEAD: callers treat that as "nothing can be proven unchanged".
|
|
81
|
+
* @param engineDir - Path to the engine directory
|
|
82
|
+
*/
|
|
83
|
+
export async function readEngineContentState(engineDir) {
|
|
84
|
+
let head;
|
|
85
|
+
try {
|
|
86
|
+
head = await getHead(engineDir);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (!isMissingHeadError(error))
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const dirty = new Set();
|
|
93
|
+
if (head !== undefined) {
|
|
94
|
+
for (const line of (await git(['diff', '--name-only', 'HEAD'], engineDir)).split('\n')) {
|
|
95
|
+
if (line.trim())
|
|
96
|
+
dirty.add(line.trim());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const file of await getUntrackedFiles(engineDir))
|
|
100
|
+
dirty.add(file);
|
|
101
|
+
return { engineDir, head, dirty, hashes: new Map(), diffs: new Map() };
|
|
102
|
+
}
|
|
103
|
+
function liveHash(state, path) {
|
|
104
|
+
let pending = state.hashes.get(path);
|
|
105
|
+
if (pending === undefined) {
|
|
106
|
+
pending = hashEngineFile(state.engineDir, path);
|
|
107
|
+
state.hashes.set(path, pending);
|
|
108
|
+
}
|
|
109
|
+
return pending;
|
|
110
|
+
}
|
|
111
|
+
/** Paths committed between `from` and HEAD, or undefined when `from` cannot be diffed. */
|
|
112
|
+
function committedSince(state, from) {
|
|
113
|
+
let pending = state.diffs.get(from);
|
|
114
|
+
if (pending === undefined) {
|
|
115
|
+
pending = git(['diff', '--name-only', `${from}..HEAD`], state.engineDir).then((output) => new Set(output
|
|
116
|
+
.split('\n')
|
|
117
|
+
.map((line) => line.trim())
|
|
118
|
+
.filter(Boolean)), (error) => {
|
|
119
|
+
verbose(`Coverage anchor: cannot diff ${from}..HEAD — ${toError(error).message}`);
|
|
120
|
+
return undefined;
|
|
121
|
+
});
|
|
122
|
+
state.diffs.set(from, pending);
|
|
123
|
+
}
|
|
124
|
+
return pending;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Paths whose content may differ from the anchor, restricted to `inputs`
|
|
128
|
+
* (undefined: the whole tree) and excluding `rebuilt` paths. Undefined when
|
|
129
|
+
* the anchor cannot be compared at all (no usable HEAD on either side).
|
|
130
|
+
*/
|
|
131
|
+
async function candidatePaths(state, anchor, inputs, rebuilt) {
|
|
132
|
+
if (state.head === undefined || anchor.engineHeadSha === '')
|
|
133
|
+
return undefined;
|
|
134
|
+
const inScope = (file) => (inputs === undefined || inputs.some((input) => isUnderPath(file, input))) &&
|
|
135
|
+
!rebuilt.some((path) => isUnderPath(file, path));
|
|
136
|
+
const candidates = new Set();
|
|
137
|
+
if (state.head !== anchor.engineHeadSha) {
|
|
138
|
+
const committed = await committedSince(state, anchor.engineHeadSha);
|
|
139
|
+
if (committed === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
for (const file of committed)
|
|
142
|
+
if (inScope(file))
|
|
143
|
+
candidates.add(file);
|
|
144
|
+
}
|
|
145
|
+
for (const file of state.dirty)
|
|
146
|
+
if (inScope(file))
|
|
147
|
+
candidates.add(file);
|
|
148
|
+
for (const file of Object.keys(anchor.fingerprints))
|
|
149
|
+
if (inScope(file))
|
|
150
|
+
candidates.add(file);
|
|
151
|
+
return candidates;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Inputs whose live content differs from what the anchor staged, sorted.
|
|
155
|
+
* Undefined when the anchor cannot be compared, which every caller treats as
|
|
156
|
+
* "changed".
|
|
157
|
+
*
|
|
158
|
+
* @param state - Engine state from {@link readEngineContentState}
|
|
159
|
+
* @param anchor - The claim's anchor
|
|
160
|
+
* @param inputs - Staging inputs to compare, or undefined for the whole tree
|
|
161
|
+
* @param rebuilt - Request paths the current build stages afresh
|
|
162
|
+
*/
|
|
163
|
+
export async function findChangedInputs(state, anchor, inputs, rebuilt = []) {
|
|
164
|
+
const candidates = await candidatePaths(state, anchor, inputs, rebuilt);
|
|
165
|
+
if (candidates === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
const changed = [];
|
|
168
|
+
for (const file of candidates) {
|
|
169
|
+
const recorded = anchor.fingerprints[file];
|
|
170
|
+
// Not recorded: the staged copy is the anchor HEAD's, and the file is
|
|
171
|
+
// a candidate only because it was committed or dirtied since.
|
|
172
|
+
if (recorded === undefined || (await liveHash(state, file)) !== recorded)
|
|
173
|
+
changed.push(file);
|
|
174
|
+
}
|
|
175
|
+
return changed.sort();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The anchor after the current build restaged `rebuilt`: recorded content
|
|
179
|
+
* for those paths becomes their live content. Undefined when that content
|
|
180
|
+
* cannot be read or the anchor cannot be compared.
|
|
181
|
+
*/
|
|
182
|
+
export async function refreshAnchor(state, anchor, rebuilt) {
|
|
183
|
+
const everything = await candidatePaths(state, anchor, undefined, []);
|
|
184
|
+
if (everything === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
const restaged = (file) => rebuilt.some((path) => isUnderPath(file, path));
|
|
187
|
+
const fingerprints = {};
|
|
188
|
+
for (const [file, hash] of Object.entries(anchor.fingerprints)) {
|
|
189
|
+
if (!restaged(file))
|
|
190
|
+
fingerprints[file] = hash;
|
|
191
|
+
}
|
|
192
|
+
for (const file of everything) {
|
|
193
|
+
// A restaged file equal to the anchor HEAD's content needs no entry.
|
|
194
|
+
if (!restaged(file) || (state.head === anchor.engineHeadSha && !state.dirty.has(file))) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const live = await liveHash(state, file);
|
|
198
|
+
if (live === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
fingerprints[file] = live;
|
|
201
|
+
}
|
|
202
|
+
return { ...anchor, fingerprints };
|
|
203
|
+
}
|
|
204
|
+
/** Nearest directory at or above `dir` that holds a test manifest, or undefined. */
|
|
205
|
+
async function findManifestDirectory(engineDir, dir) {
|
|
206
|
+
for (let current = dir;; current = posix.dirname(current)) {
|
|
207
|
+
const base = current === '.' ? '' : current;
|
|
208
|
+
try {
|
|
209
|
+
const entries = await readdir(join(engineDir, base));
|
|
210
|
+
if (entries.some((entry) => TEST_MANIFEST_BASENAMES.has(entry)))
|
|
211
|
+
return base;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// A missing directory has no manifest. Keep walking up.
|
|
215
|
+
}
|
|
216
|
+
if (base === '')
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The request paths a scoped build restages, widened by the test manifests
|
|
222
|
+
* of every requested file: packaging one file of a manifest re-processes
|
|
223
|
+
* the manifest itself, so a manifest that gained an entry for that file is
|
|
224
|
+
* restaged too, and must not count as a changed input of the manifest's
|
|
225
|
+
* other files afterwards.
|
|
226
|
+
*
|
|
227
|
+
* @param engineDir - Path to the engine directory
|
|
228
|
+
* @param requested - Normalized request paths of the build
|
|
229
|
+
*/
|
|
230
|
+
export async function widenWithRestagedManifests(engineDir, requested) {
|
|
231
|
+
const widened = new Set(requested);
|
|
232
|
+
for (const path of requested) {
|
|
233
|
+
if (!TEST_FILE_PATTERN.test(posix.basename(path)))
|
|
234
|
+
continue;
|
|
235
|
+
const dir = await findManifestDirectory(engineDir, posix.dirname(path));
|
|
236
|
+
if (dir === undefined)
|
|
237
|
+
continue;
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = await readdir(join(engineDir, dir));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (TEST_MANIFEST_BASENAMES.has(entry))
|
|
247
|
+
widened.add(dir === '' ? entry : `${dir}/${entry}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [...widened];
|
|
251
|
+
}
|
|
252
|
+
/** A test implementation file (`browser_*.js`, `test_*.js`, …), not a directory. */
|
|
253
|
+
const TEST_FILE_PATTERN = /^(?:browser|test)_.*\.m?js$/;
|
|
254
|
+
/** Test manifests at or beneath `dir`, bounded by {@link MAX_WALK_ENTRIES}. */
|
|
255
|
+
async function findManifestsBeneath(engineDir, dir) {
|
|
256
|
+
const manifests = [];
|
|
257
|
+
const pending = [dir];
|
|
258
|
+
let visited = 0;
|
|
259
|
+
while (pending.length > 0 && visited < MAX_WALK_ENTRIES) {
|
|
260
|
+
const current = pending.pop() ?? '';
|
|
261
|
+
let entries;
|
|
262
|
+
try {
|
|
263
|
+
entries = await readdir(join(engineDir, current), { withFileTypes: true });
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
visited += 1;
|
|
270
|
+
const path = current ? `${current}/${entry.name}` : entry.name;
|
|
271
|
+
if (entry.isDirectory())
|
|
272
|
+
pending.push(path);
|
|
273
|
+
else if (TEST_MANIFEST_BASENAMES.has(entry.name))
|
|
274
|
+
manifests.push(path);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return manifests;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Engine-relative paths a manifest names outside its own directory. Bare
|
|
281
|
+
* names stay inside it. `!/` and `/` prefixes are engine-root relative, as
|
|
282
|
+
* in mozbuild manifests. A glob keeps the directory part before its first
|
|
283
|
+
* wildcard segment.
|
|
284
|
+
*/
|
|
285
|
+
export function manifestExternalReferences(manifestPath, text) {
|
|
286
|
+
const dir = posix.dirname(manifestPath);
|
|
287
|
+
const refs = [];
|
|
288
|
+
for (const token of text.match(/[\w.!~@+\-/*?{}]+/g) ?? []) {
|
|
289
|
+
if (!token.includes('/') || token.includes('//'))
|
|
290
|
+
continue;
|
|
291
|
+
const rooted = token.startsWith('!/')
|
|
292
|
+
? token.slice(2)
|
|
293
|
+
: token.startsWith('/')
|
|
294
|
+
? token.slice(1)
|
|
295
|
+
: undefined;
|
|
296
|
+
const resolved = rooted === undefined ? posix.join(dir, token) : posix.normalize(rooted);
|
|
297
|
+
const segments = resolved.split('/');
|
|
298
|
+
const wildcard = segments.findIndex((segment) => /[*?{}]/.test(segment));
|
|
299
|
+
const path = normalizeRequestPath(wildcard >= 0 ? segments.slice(0, wildcard).join('/') : resolved);
|
|
300
|
+
if (path === '' || path.startsWith('..') || isUnderPath(path, dir))
|
|
301
|
+
continue;
|
|
302
|
+
refs.push(path);
|
|
303
|
+
}
|
|
304
|
+
return refs;
|
|
305
|
+
}
|
|
306
|
+
/** Drops entries that sit beneath another entry, then sorts. */
|
|
307
|
+
function collapsePaths(paths) {
|
|
308
|
+
const unique = [...new Set(paths)].sort();
|
|
309
|
+
return unique.filter((path) => !unique.some((other) => other !== path && isUnderPath(path, other)));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Staging inputs of one request path. See the module comment.
|
|
313
|
+
* @param engineDir - Path to the engine directory
|
|
314
|
+
* @param requestPath - Engine-relative request path (file or directory)
|
|
315
|
+
*/
|
|
316
|
+
export async function collectStagingInputs(engineDir, requestPath) {
|
|
317
|
+
const path = normalizeRequestPath(requestPath);
|
|
318
|
+
let isDirectory = false;
|
|
319
|
+
try {
|
|
320
|
+
isDirectory = (await stat(join(engineDir, path))).isDirectory();
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// A missing request path is staged by nothing. Its directory still decides.
|
|
324
|
+
}
|
|
325
|
+
const start = isDirectory ? path : posix.dirname(path);
|
|
326
|
+
const base = await findManifestDirectory(engineDir, start === '.' ? '' : start);
|
|
327
|
+
const fallback = start === '.' || start === '' ? path : start;
|
|
328
|
+
const root = base === undefined || base === '' ? fallback : base;
|
|
329
|
+
const manifests = new Set();
|
|
330
|
+
for (const entry of await readdir(join(engineDir, root)).catch(() => [])) {
|
|
331
|
+
if (TEST_MANIFEST_BASENAMES.has(entry))
|
|
332
|
+
manifests.add(`${root}/${entry}`);
|
|
333
|
+
}
|
|
334
|
+
if (isDirectory) {
|
|
335
|
+
for (const manifest of await findManifestsBeneath(engineDir, path))
|
|
336
|
+
manifests.add(manifest);
|
|
337
|
+
}
|
|
338
|
+
const inputs = [root, path];
|
|
339
|
+
for (const manifest of manifests) {
|
|
340
|
+
try {
|
|
341
|
+
const text = await readFile(join(engineDir, manifest), 'utf8');
|
|
342
|
+
inputs.push(...manifestExternalReferences(manifest, text));
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
verbose(`Coverage anchor: cannot read ${manifest} — ${toError(error).message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return collapsePaths(inputs);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Records the anchor of a request path this build staged. Undefined when
|
|
352
|
+
* HEAD is unborn or an input's content cannot be read: the path is still
|
|
353
|
+
* claimed, and later retention falls back to the baseline-wide anchor.
|
|
354
|
+
*
|
|
355
|
+
* @param state - Engine state after the build
|
|
356
|
+
* @param requestPath - Engine-relative request path
|
|
357
|
+
* @param mozconfigHash - Hash of the mozconfig the build configured with
|
|
358
|
+
*/
|
|
359
|
+
export async function createCoverageRecord(state, requestPath, mozconfigHash) {
|
|
360
|
+
if (state.head === undefined)
|
|
361
|
+
return undefined;
|
|
362
|
+
const inputs = await collectStagingInputs(state.engineDir, requestPath);
|
|
363
|
+
return narrowAnchor(state, { engineHeadSha: state.head, mozconfigHash, fingerprints: {} }, inputs, [...state.dirty]);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Restricts an anchor to `inputs`. `extraDirty` adds paths whose live content
|
|
367
|
+
* becomes the recorded one (the dirty set of a record being created).
|
|
368
|
+
*/
|
|
369
|
+
export async function narrowAnchor(state, anchor, inputs, extraDirty = []) {
|
|
370
|
+
const fingerprints = {};
|
|
371
|
+
for (const [file, hash] of Object.entries(anchor.fingerprints)) {
|
|
372
|
+
if (inputs.some((input) => isUnderPath(file, input)))
|
|
373
|
+
fingerprints[file] = hash;
|
|
374
|
+
}
|
|
375
|
+
for (const file of extraDirty) {
|
|
376
|
+
if (!inputs.some((input) => isUnderPath(file, input)))
|
|
377
|
+
continue;
|
|
378
|
+
const live = await liveHash(state, file);
|
|
379
|
+
if (live === undefined)
|
|
380
|
+
return undefined;
|
|
381
|
+
fingerprints[file] = live;
|
|
382
|
+
}
|
|
383
|
+
return { ...anchor, fingerprints, inputs };
|
|
384
|
+
}
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* forward-import that later-patch lint would then block). Without a single
|
|
12
12
|
* helper, every new destructive command would re-implement this and drift.
|
|
13
13
|
*/
|
|
14
|
-
/** Filename of the audit log, relative to the patches directory. */
|
|
15
|
-
export declare const HISTORY_LOG_FILENAME = ".fireforge-history.jsonl";
|
|
16
14
|
/**
|
|
17
15
|
* A structural conflict that must block the operation even under `--force`.
|
|
18
16
|
*
|
|
@@ -128,12 +126,17 @@ export declare function assertConfirmationAvailable(operation: string, options:
|
|
|
128
126
|
*/
|
|
129
127
|
export declare function confirmDestructive(input: DestructiveOpInput): Promise<DestructiveOpResult>;
|
|
130
128
|
/**
|
|
131
|
-
* Appends a single JSONL record to
|
|
129
|
+
* Appends a single JSONL record to the project's patch history log
|
|
130
|
+
* (`.fireforge/history.jsonl` unless `fireforge.json#history.path` says
|
|
131
|
+
* otherwise; a log at the pre-0.50 `patches/.fireforge-history.jsonl` is
|
|
132
|
+
* moved there first).
|
|
132
133
|
*
|
|
133
134
|
* Call order matters: append only after the mutation succeeds, never
|
|
134
135
|
* pre-emptively, so rolled-back failures do not leave ghost entries. The log
|
|
135
136
|
* is append-only and advisory: no code path reads it back. It exists so
|
|
136
|
-
* operators have a post-hoc audit trail when something goes wrong.
|
|
137
|
+
* operators have a post-hoc audit trail when something goes wrong. Every
|
|
138
|
+
* record carries the writing invocation's id (and `FIREFORGE_SESSION_ID`
|
|
139
|
+
* when set), so a session sharing the checkout can pick out its own lines.
|
|
137
140
|
*
|
|
138
141
|
* @param patchesDir - Path to the patches directory
|
|
139
142
|
* @param entry - Serializable history record
|
|
@@ -12,14 +12,10 @@
|
|
|
12
12
|
* forward-import that later-patch lint would then block). Without a single
|
|
13
13
|
* helper, every new destructive command would re-implement this and drift.
|
|
14
14
|
*/
|
|
15
|
-
import { appendFile } from 'node:fs/promises';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
15
|
import { confirm } from '@clack/prompts';
|
|
18
16
|
import { CancellationError, InvalidArgumentError } from '../errors/base.js';
|
|
19
|
-
import { ensureDir } from '../utils/fs.js';
|
|
20
17
|
import { info, isCancel, warn } from '../utils/logger.js';
|
|
21
|
-
|
|
22
|
-
export const HISTORY_LOG_FILENAME = '.fireforge-history.jsonl';
|
|
18
|
+
import { appendHistoryLine, historyRecordIdentity } from './history-log-file.js';
|
|
23
19
|
/**
|
|
24
20
|
* Prints the change summary lines at warn-severity so the prompt stands out.
|
|
25
21
|
*/
|
|
@@ -161,22 +157,26 @@ export async function confirmDestructive(input) {
|
|
|
161
157
|
return 'proceed';
|
|
162
158
|
}
|
|
163
159
|
/**
|
|
164
|
-
* Appends a single JSONL record to
|
|
160
|
+
* Appends a single JSONL record to the project's patch history log
|
|
161
|
+
* (`.fireforge/history.jsonl` unless `fireforge.json#history.path` says
|
|
162
|
+
* otherwise; a log at the pre-0.50 `patches/.fireforge-history.jsonl` is
|
|
163
|
+
* moved there first).
|
|
165
164
|
*
|
|
166
165
|
* Call order matters: append only after the mutation succeeds, never
|
|
167
166
|
* pre-emptively, so rolled-back failures do not leave ghost entries. The log
|
|
168
167
|
* is append-only and advisory: no code path reads it back. It exists so
|
|
169
|
-
* operators have a post-hoc audit trail when something goes wrong.
|
|
168
|
+
* operators have a post-hoc audit trail when something goes wrong. Every
|
|
169
|
+
* record carries the writing invocation's id (and `FIREFORGE_SESSION_ID`
|
|
170
|
+
* when set), so a session sharing the checkout can pick out its own lines.
|
|
170
171
|
*
|
|
171
172
|
* @param patchesDir - Path to the patches directory
|
|
172
173
|
* @param entry - Serializable history record
|
|
173
174
|
*/
|
|
174
175
|
export async function appendHistory(patchesDir, entry) {
|
|
175
|
-
await ensureDir(patchesDir);
|
|
176
176
|
const record = {
|
|
177
177
|
ts: new Date().toISOString(),
|
|
178
|
+
...historyRecordIdentity(),
|
|
178
179
|
...entry,
|
|
179
180
|
};
|
|
180
|
-
|
|
181
|
-
await appendFile(join(patchesDir, HISTORY_LOG_FILENAME), line, 'utf-8');
|
|
181
|
+
await appendHistoryLine(patchesDir, JSON.stringify(record));
|
|
182
182
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LockMode } from './file-lock.js';
|
|
1
2
|
import { type LockStatusSnapshot } from './file-lock.js';
|
|
2
3
|
export interface EngineSessionLockOptions {
|
|
3
4
|
/**
|
|
@@ -7,6 +8,11 @@ export interface EngineSessionLockOptions {
|
|
|
7
8
|
* fail-fast behaviour.
|
|
8
9
|
*/
|
|
9
10
|
waitLockSeconds?: number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* `shared` lets build-less test runs overlap each other; see
|
|
13
|
+
* `file-lock-shared.ts`. Every other caller holds the lock exclusively.
|
|
14
|
+
*/
|
|
15
|
+
mode?: LockMode | undefined;
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* Formats the notice printed when the wait budget is extended because the
|
|
@@ -59,6 +65,15 @@ export declare function isIndexLockError(error: unknown): boolean;
|
|
|
59
65
|
export declare function isUnavailableGenerationToken(token: string): boolean;
|
|
60
66
|
/** Strips the marker prefix off an unavailable token for display. */
|
|
61
67
|
export declare function unavailableGenerationReason(token: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Engine-relative paths whose working-tree status differs between two
|
|
70
|
+
* generation tokens (both sides of a rename), and whether HEAD moved. An
|
|
71
|
+
* untracked directory keeps git's trailing `/`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function generationDeltaPaths(before: string, after: string): {
|
|
74
|
+
headMoved: boolean;
|
|
75
|
+
paths: string[];
|
|
76
|
+
};
|
|
62
77
|
/**
|
|
63
78
|
* Describes what moved between two generation tokens, in operator terms.
|
|
64
79
|
*
|
|
@@ -101,4 +116,4 @@ export declare function describeEngineGenerationDelta(before: string, after: str
|
|
|
101
116
|
* demonstrably had something to measure. The reverse means there was never a
|
|
102
117
|
* baseline to compare against. Neither verdict can be trusted, so both throw.
|
|
103
118
|
*/
|
|
104
|
-
export declare function assertEngineGenerationUnchanged(engineDir: string, before: string): Promise<void>;
|
|
119
|
+
export declare function assertEngineGenerationUnchanged(engineDir: string, before: string, tolerateDelta?: (before: string, after: string) => Promise<boolean>): Promise<void>;
|
|
@@ -3,8 +3,9 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { InconclusiveVerdictError } from '../errors/base.js';
|
|
4
4
|
import { toError } from '../utils/errors.js';
|
|
5
5
|
import { info, warn } from '../utils/logger.js';
|
|
6
|
+
import { MAX_WAIT_LOCK_SECONDS } from '../utils/options.js';
|
|
6
7
|
import { sleep } from '../utils/sleep.js';
|
|
7
|
-
import { readLockStatus, withFileLock } from './file-lock.js';
|
|
8
|
+
import { formatLockQueuePosition, readLockStatus, withFileLock, } from './file-lock.js';
|
|
8
9
|
import { git } from './git-base.js';
|
|
9
10
|
const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
|
|
10
11
|
/**
|
|
@@ -14,7 +15,7 @@ const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
|
|
|
14
15
|
* never gets one longer than the largest wait they could have asked for.
|
|
15
16
|
*/
|
|
16
17
|
const WAIT_EXTENSION_FACTOR = 4;
|
|
17
|
-
const MAX_WAIT_LOCK_MS =
|
|
18
|
+
const MAX_WAIT_LOCK_MS = MAX_WAIT_LOCK_SECONDS * 1000;
|
|
18
19
|
/**
|
|
19
20
|
* Formats the periodic operator-facing waiting line printed while `--wait-lock`
|
|
20
21
|
* polls a contended engine session lock. The holder identification comes from
|
|
@@ -24,12 +25,9 @@ const MAX_WAIT_LOCK_MS = 3600 * 1000;
|
|
|
24
25
|
function formatWaitProgressLine(waitedMs, timeoutMs, holder, queue) {
|
|
25
26
|
const progress = `${Math.round(waitedMs / 1000)}s of up to ${Math.round(timeoutMs / 1000)}s`;
|
|
26
27
|
// Queue position: under several concurrent sessions the wait
|
|
27
|
-
// line alone left operators inferring their place from `ps`.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: queue.ahead === 0
|
|
31
|
-
? ` You are next in a queue of ${queue.depth}.`
|
|
32
|
-
: ` ${queue.ahead} ahead of you (queue of ${queue.depth}).`;
|
|
28
|
+
// line alone left operators inferring their place from `ps`. It names the
|
|
29
|
+
// ordering rule, because the count is only useful if the grant honours it.
|
|
30
|
+
const position = formatLockQueuePosition(queue);
|
|
33
31
|
if (holder === undefined) {
|
|
34
32
|
return `Waiting for the FireForge engine lock — ${progress}.${position}`;
|
|
35
33
|
}
|
|
@@ -69,6 +67,7 @@ export async function withEngineSessionLock(projectRoot, command, operation, opt
|
|
|
69
67
|
}
|
|
70
68
|
const { waitLockSeconds } = options;
|
|
71
69
|
return withFileLock(join(projectRoot, ENGINE_SESSION_LOCK_PATH), operation, {
|
|
70
|
+
...(options.mode === 'shared' ? { mode: 'shared' } : {}),
|
|
72
71
|
timeoutMs: waitLockSeconds !== undefined ? waitLockSeconds * 1000 : 1000,
|
|
73
72
|
...(waitLockSeconds !== undefined
|
|
74
73
|
? {
|
|
@@ -124,8 +123,13 @@ export async function readEngineSessionLockStatus(projectRoot) {
|
|
|
124
123
|
* Pure, so the wording is unit-testable without a lock on disk.
|
|
125
124
|
*/
|
|
126
125
|
export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
|
|
126
|
+
const shared = snapshot.sharedHolders !== undefined && snapshot.sharedHolders > 0
|
|
127
|
+
? `Shared holders: ${snapshot.sharedHolders} build-less test run(s) under a shared lock.`
|
|
128
|
+
: undefined;
|
|
127
129
|
if (!snapshot.held) {
|
|
128
|
-
return
|
|
130
|
+
return shared === undefined
|
|
131
|
+
? ['Engine session lock: free (no FireForge engine-mutating command is running).']
|
|
132
|
+
: ['Engine session lock: no exclusive holder.', shared];
|
|
129
133
|
}
|
|
130
134
|
const elapsed = snapshot.heldForMs === undefined
|
|
131
135
|
? 'unknown duration'
|
|
@@ -138,6 +142,7 @@ export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
|
|
|
138
142
|
const lines = [
|
|
139
143
|
`Engine session lock: ${identity}, for ${elapsed}.`,
|
|
140
144
|
`Queue depth: ${snapshot.queueDepth} waiter(s).`,
|
|
145
|
+
...(shared === undefined ? [] : [shared]),
|
|
141
146
|
];
|
|
142
147
|
// Liveness answers "does the process exist". It does not answer "is it
|
|
143
148
|
// getting anywhere". A holder alive for minutes having used a fraction of
|
|
@@ -254,6 +259,25 @@ function parsePorcelainRecords(body) {
|
|
|
254
259
|
}
|
|
255
260
|
return records;
|
|
256
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Engine-relative paths whose working-tree status differs between two
|
|
264
|
+
* generation tokens (both sides of a rename), and whether HEAD moved. An
|
|
265
|
+
* untracked directory keeps git's trailing `/`.
|
|
266
|
+
*/
|
|
267
|
+
export function generationDeltaPaths(before, after) {
|
|
268
|
+
const from = splitGenerationToken(before);
|
|
269
|
+
const to = splitGenerationToken(after);
|
|
270
|
+
const beforeRecords = new Set(parsePorcelainRecords(from.body));
|
|
271
|
+
const afterRecords = new Set(parsePorcelainRecords(to.body));
|
|
272
|
+
const paths = new Set();
|
|
273
|
+
for (const record of [...afterRecords, ...beforeRecords]) {
|
|
274
|
+
if (beforeRecords.has(record) && afterRecords.has(record))
|
|
275
|
+
continue;
|
|
276
|
+
for (const path of record.slice(3).split(' -> '))
|
|
277
|
+
paths.add(path);
|
|
278
|
+
}
|
|
279
|
+
return { headMoved: from.head !== to.head, paths: [...paths].sort() };
|
|
280
|
+
}
|
|
257
281
|
/**
|
|
258
282
|
* Describes what moved between two generation tokens, in operator terms.
|
|
259
283
|
*
|
|
@@ -317,7 +341,7 @@ export function describeEngineGenerationDelta(before, after) {
|
|
|
317
341
|
* demonstrably had something to measure. The reverse means there was never a
|
|
318
342
|
* baseline to compare against. Neither verdict can be trusted, so both throw.
|
|
319
343
|
*/
|
|
320
|
-
export async function assertEngineGenerationUnchanged(engineDir, before) {
|
|
344
|
+
export async function assertEngineGenerationUnchanged(engineDir, before, tolerateDelta) {
|
|
321
345
|
const after = await snapshotEngineGeneration(engineDir);
|
|
322
346
|
const beforeUnavailable = isUnavailableGenerationToken(before);
|
|
323
347
|
const afterUnavailable = isUnavailableGenerationToken(after);
|
|
@@ -339,6 +363,10 @@ export async function assertEngineGenerationUnchanged(engineDir, before) {
|
|
|
339
363
|
}
|
|
340
364
|
if (after === before)
|
|
341
365
|
return;
|
|
366
|
+
// A caller that can prove the delta never reached the runtime under test
|
|
367
|
+
// (a perf sample, see `test-perf-scope.ts`) keeps its verdict.
|
|
368
|
+
if (tolerateDelta !== undefined && (await tolerateDelta(before, after)))
|
|
369
|
+
return;
|
|
342
370
|
const delta = describeEngineGenerationDelta(before, after);
|
|
343
371
|
throw new InconclusiveVerdictError('engine/ changed while `fireforge test` was running, so this test verdict is invalid/inconclusive.\n\n' +
|
|
344
372
|
(delta.length > 0 ? `What moved:\n${delta.map((line) => ` ${line}`).join('\n')}\n\n` : '') +
|