@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,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The one key order every `patches.json` writer uses for a patch row and
|
|
4
|
+
* for the manifest itself. Writers used to disagree: `export` spread
|
|
5
|
+
* `sourceProduct` before `sourceVersion` while a full-manifest save put it
|
|
6
|
+
* after `filesAffected`, so a no-op rewrite left a key-order-only hunk in
|
|
7
|
+
* the manifest.
|
|
8
|
+
*/
|
|
9
|
+
/** Patch-row keys in the order of the `PatchMetadata` interface. */
|
|
10
|
+
const PATCH_ROW_KEY_ORDER = [
|
|
11
|
+
'filename',
|
|
12
|
+
'order',
|
|
13
|
+
'category',
|
|
14
|
+
'name',
|
|
15
|
+
'description',
|
|
16
|
+
'createdAt',
|
|
17
|
+
'sourceEsrVersion',
|
|
18
|
+
'sourceProduct',
|
|
19
|
+
'sourceVersion',
|
|
20
|
+
'filesAffected',
|
|
21
|
+
'lintIgnore',
|
|
22
|
+
'tier',
|
|
23
|
+
'stagedDependencies',
|
|
24
|
+
];
|
|
25
|
+
/** Manifest keys in the order of the `PatchesManifest` interface. */
|
|
26
|
+
const MANIFEST_KEY_ORDER = ['version', 'patches', 'retired', 'reserved'];
|
|
27
|
+
function reorder(value, order) {
|
|
28
|
+
const source = value;
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const key of order) {
|
|
31
|
+
if (Object.hasOwn(source, key))
|
|
32
|
+
out[key] = source[key];
|
|
33
|
+
}
|
|
34
|
+
// Keys this build does not know keep their relative order after the
|
|
35
|
+
// known ones, so a newer writer's fields survive an older rewrite.
|
|
36
|
+
for (const key of Object.keys(source)) {
|
|
37
|
+
if (!Object.hasOwn(out, key))
|
|
38
|
+
out[key] = source[key];
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns a copy of a patch row with its keys in canonical order.
|
|
44
|
+
*
|
|
45
|
+
* @param row - A `PatchMetadata` row, typed or raw
|
|
46
|
+
* @returns The same fields in canonical key order
|
|
47
|
+
*/
|
|
48
|
+
export function canonicalizePatchRow(row) {
|
|
49
|
+
return reorder(row, PATCH_ROW_KEY_ORDER);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns a copy of a manifest with its top-level keys and every row in
|
|
53
|
+
* canonical order.
|
|
54
|
+
*
|
|
55
|
+
* @param manifest - A `PatchesManifest`, typed or raw
|
|
56
|
+
* @returns The manifest in canonical key order
|
|
57
|
+
*/
|
|
58
|
+
export function canonicalizeManifest(manifest) {
|
|
59
|
+
const ordered = reorder(manifest, MANIFEST_KEY_ORDER);
|
|
60
|
+
return { ...ordered, patches: manifest.patches.map((row) => canonicalizePatchRow(row)) };
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manifest `patch stage` writes into the git index: HEAD's
|
|
3
|
+
* `patches.json` with only the named patches' rows (and their retired and
|
|
4
|
+
* reserved slots) taken from the working copy.
|
|
5
|
+
*
|
|
6
|
+
* Sessions sharing one checkout each touch their own rows of the shared
|
|
7
|
+
* manifest. Staging the whole file commits every session's rows under one
|
|
8
|
+
* message; staging hunks with `git apply --cached --unidiff-zero` lands a
|
|
9
|
+
* kept insertion at its new-side line number once a dropped hunk above it
|
|
10
|
+
* shifted the file, and committed invalid JSON. Merging rows from the
|
|
11
|
+
* parsed manifests has neither failure: the result is built from structure,
|
|
12
|
+
* then validated before anything is staged.
|
|
13
|
+
*/
|
|
14
|
+
import type { PatchesManifest } from '../types/commands/index.js';
|
|
15
|
+
/** What staging does to one named patch's row. */
|
|
16
|
+
export type StagedRowChange = 'added' | 'updated' | 'removed' | 'unchanged';
|
|
17
|
+
/** The staged manifest and the per-patch row changes it carries. */
|
|
18
|
+
export interface StagedManifest {
|
|
19
|
+
manifest: PatchesManifest;
|
|
20
|
+
changes: Array<{
|
|
21
|
+
filename: string;
|
|
22
|
+
change: StagedRowChange;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Merges the named rows of `working` into `head`.
|
|
27
|
+
*
|
|
28
|
+
* Rows: every unnamed row comes from HEAD; a named row comes from the
|
|
29
|
+
* working copy, or leaves when the working copy no longer has it.
|
|
30
|
+
* Retired slots: a slot whose `filename` is named follows the working
|
|
31
|
+
* copy, the rest follow HEAD. Reserved slots follow HEAD. A slot whose
|
|
32
|
+
* order a staged row now fills is dropped, as every manifest write does.
|
|
33
|
+
*
|
|
34
|
+
* @param head - HEAD's manifest, or null when HEAD has none
|
|
35
|
+
* @param working - The working-copy manifest
|
|
36
|
+
* @param filenames - Patch filenames whose rows to take from the working copy
|
|
37
|
+
* @returns The validated manifest and the row changes
|
|
38
|
+
* @throws Error when two rows would hold one order, or the merge does not
|
|
39
|
+
* validate
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeStagedManifest(head: PatchesManifest | null, working: PatchesManifest, filenames: ReadonlySet<string>): StagedManifest;
|
|
42
|
+
/**
|
|
43
|
+
* Serializes a staged manifest exactly as the manifest writers do, so the
|
|
44
|
+
* staged blob and a later full save of the same content are byte-identical.
|
|
45
|
+
*/
|
|
46
|
+
export declare function serializeStagedManifest(manifest: PatchesManifest): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The manifest `patch stage` writes into the git index: HEAD's
|
|
4
|
+
* `patches.json` with only the named patches' rows (and their retired and
|
|
5
|
+
* reserved slots) taken from the working copy.
|
|
6
|
+
*
|
|
7
|
+
* Sessions sharing one checkout each touch their own rows of the shared
|
|
8
|
+
* manifest. Staging the whole file commits every session's rows under one
|
|
9
|
+
* message; staging hunks with `git apply --cached --unidiff-zero` lands a
|
|
10
|
+
* kept insertion at its new-side line number once a dropped hunk above it
|
|
11
|
+
* shifted the file, and committed invalid JSON. Merging rows from the
|
|
12
|
+
* parsed manifests has neither failure: the result is built from structure,
|
|
13
|
+
* then validated before anything is staged.
|
|
14
|
+
*/
|
|
15
|
+
import { toError } from '../utils/errors.js';
|
|
16
|
+
import { canonicalizeManifest } from './patch-manifest-key-order.js';
|
|
17
|
+
import { validatePatchesManifest } from './patch-manifest-validate.js';
|
|
18
|
+
import { carryReservedSlots, carryRetiredSlots } from './patch-retired.js';
|
|
19
|
+
/**
|
|
20
|
+
* Merges the named rows of `working` into `head`.
|
|
21
|
+
*
|
|
22
|
+
* Rows: every unnamed row comes from HEAD; a named row comes from the
|
|
23
|
+
* working copy, or leaves when the working copy no longer has it.
|
|
24
|
+
* Retired slots: a slot whose `filename` is named follows the working
|
|
25
|
+
* copy, the rest follow HEAD. Reserved slots follow HEAD. A slot whose
|
|
26
|
+
* order a staged row now fills is dropped, as every manifest write does.
|
|
27
|
+
*
|
|
28
|
+
* @param head - HEAD's manifest, or null when HEAD has none
|
|
29
|
+
* @param working - The working-copy manifest
|
|
30
|
+
* @param filenames - Patch filenames whose rows to take from the working copy
|
|
31
|
+
* @returns The validated manifest and the row changes
|
|
32
|
+
* @throws Error when two rows would hold one order, or the merge does not
|
|
33
|
+
* validate
|
|
34
|
+
*/
|
|
35
|
+
export function mergeStagedManifest(head, working, filenames) {
|
|
36
|
+
const headRows = head?.patches ?? [];
|
|
37
|
+
const changes = [...filenames].sort().map((filename) => {
|
|
38
|
+
const before = headRows.find((row) => row.filename === filename);
|
|
39
|
+
const after = working.patches.find((row) => row.filename === filename);
|
|
40
|
+
const change = before === undefined
|
|
41
|
+
? 'added'
|
|
42
|
+
: after === undefined
|
|
43
|
+
? 'removed'
|
|
44
|
+
: JSON.stringify(canonicalRow(before)) === JSON.stringify(canonicalRow(after))
|
|
45
|
+
? 'unchanged'
|
|
46
|
+
: 'updated';
|
|
47
|
+
return { filename, change };
|
|
48
|
+
});
|
|
49
|
+
const rows = [
|
|
50
|
+
...headRows.filter((row) => !filenames.has(row.filename)),
|
|
51
|
+
...working.patches.filter((row) => filenames.has(row.filename)),
|
|
52
|
+
].sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
53
|
+
assertNoCollision(rows);
|
|
54
|
+
const retiredSource = [
|
|
55
|
+
...(head?.retired ?? []).filter((slot) => !filenames.has(slot.filename)),
|
|
56
|
+
...(working.retired ?? []).filter((slot) => filenames.has(slot.filename)),
|
|
57
|
+
];
|
|
58
|
+
const retired = carryRetiredSlots({ retired: retiredSource }, rows);
|
|
59
|
+
const reserved = carryReservedSlots(head, rows);
|
|
60
|
+
const merged = {
|
|
61
|
+
version: 1,
|
|
62
|
+
patches: rows,
|
|
63
|
+
...(retired !== undefined ? { retired } : {}),
|
|
64
|
+
...(reserved !== undefined ? { reserved } : {}),
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
validatePatchesManifest(JSON.parse(JSON.stringify(merged)));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`the staged manifest would not validate: ${toError(error).message}`, {
|
|
71
|
+
cause: error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { manifest: canonicalizeManifest(merged), changes };
|
|
75
|
+
}
|
|
76
|
+
function canonicalRow(row) {
|
|
77
|
+
return canonicalizeManifest({ patches: [row] }).patches[0];
|
|
78
|
+
}
|
|
79
|
+
function assertNoCollision(rows) {
|
|
80
|
+
const seen = new Map();
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const holder = seen.get(row.order);
|
|
83
|
+
if (holder !== undefined) {
|
|
84
|
+
throw new Error(`order ${String(row.order)} would be held by both ${holder} and ${row.filename}; ` +
|
|
85
|
+
'stage the patch that moved off that order as well');
|
|
86
|
+
}
|
|
87
|
+
seen.set(row.order, row.filename);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Serializes a staged manifest exactly as the manifest writers do, so the
|
|
92
|
+
* staged blob and a later full save of the same content are byte-identical.
|
|
93
|
+
*/
|
|
94
|
+
export function serializeStagedManifest(manifest) {
|
|
95
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
96
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { parseObject } from '../utils/parse.js';
|
|
6
6
|
import { isArray, isObject, isValidFirefoxProduct, isValidFirefoxVersion, PATCH_CATEGORIES, } from '../utils/validation.js';
|
|
7
|
+
import { parseLintIgnoreEntries } from './patch-lint-ignore.js';
|
|
8
|
+
import { canonicalizePatchRow } from './patch-manifest-key-order.js';
|
|
7
9
|
import { checkDocumentVersion, describeNewerDocument } from './state-file.js';
|
|
8
10
|
function parseForwardImports(data, label) {
|
|
9
11
|
if (!isArray(data)) {
|
|
@@ -99,7 +101,11 @@ function validatePatchMetadata(data, index) {
|
|
|
99
101
|
// `lintIgnore` to suppress a rule is quietly re-tripped the next time the
|
|
100
102
|
// manifest validates. Preserve both `lintIgnore` and `tier` here. Future
|
|
101
103
|
// optional fields follow the same template.
|
|
102
|
-
|
|
104
|
+
// Entries are bare check IDs or `{ rule, max, reason }` ceilings.
|
|
105
|
+
const rawLintIgnore = rec.raw('lintIgnore');
|
|
106
|
+
const lintIgnore = rawLintIgnore === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: parseLintIgnoreEntries(rawLintIgnore, `patches[${index}].lintIgnore`);
|
|
103
109
|
const rawTier = rec.raw('tier');
|
|
104
110
|
let tier;
|
|
105
111
|
if (rawTier !== undefined) {
|
|
@@ -129,7 +135,54 @@ function validatePatchMetadata(data, index) {
|
|
|
129
135
|
if (rawStagedDependencies !== undefined) {
|
|
130
136
|
result.stagedDependencies = parseStagedDependencies(rawStagedDependencies, `patches[${index}].stagedDependencies`);
|
|
131
137
|
}
|
|
132
|
-
return result;
|
|
138
|
+
return canonicalizePatchRow(result);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validates the `retired` placeholders `patch retire` leaves behind. Kept on
|
|
142
|
+
* the manifest so every write that round-trips a loaded manifest carries
|
|
143
|
+
* them; dropping them would silently reopen the gaps they were retired to
|
|
144
|
+
* close.
|
|
145
|
+
*/
|
|
146
|
+
function parseRetiredSlots(data) {
|
|
147
|
+
if (!isArray(data)) {
|
|
148
|
+
throw new Error('patches.json field "retired" must be an array');
|
|
149
|
+
}
|
|
150
|
+
return data.map((entry, index) => {
|
|
151
|
+
const rec = parseObject(entry, `retired[${index}]`);
|
|
152
|
+
const slot = {
|
|
153
|
+
order: rec.nonNegativeInteger('order'),
|
|
154
|
+
category: rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)'),
|
|
155
|
+
filename: rec.string('filename'),
|
|
156
|
+
retiredAt: rec.string('retiredAt'),
|
|
157
|
+
};
|
|
158
|
+
const reason = rec.optionalString('reason');
|
|
159
|
+
if (reason !== undefined)
|
|
160
|
+
slot.reason = reason;
|
|
161
|
+
return slot;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validates the `reserved` placeholders `patch reserve` writes. Same
|
|
166
|
+
* survival rule as {@link parseRetiredSlots}: dropping one on a round trip
|
|
167
|
+
* would reopen the gap a peer reserved the order to close.
|
|
168
|
+
*/
|
|
169
|
+
function parseReservedSlots(data) {
|
|
170
|
+
if (!isArray(data)) {
|
|
171
|
+
throw new Error('patches.json field "reserved" must be an array');
|
|
172
|
+
}
|
|
173
|
+
return data.map((entry, index) => {
|
|
174
|
+
const rec = parseObject(entry, `reserved[${index}]`);
|
|
175
|
+
const slot = {
|
|
176
|
+
order: rec.nonNegativeInteger('order'),
|
|
177
|
+
category: rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)'),
|
|
178
|
+
owner: rec.validatedString('owner', (value) => value.trim() !== '', 'a non-empty label'),
|
|
179
|
+
reservedAt: rec.string('reservedAt'),
|
|
180
|
+
};
|
|
181
|
+
const reason = rec.optionalString('reason');
|
|
182
|
+
if (reason !== undefined)
|
|
183
|
+
slot.reason = reason;
|
|
184
|
+
return slot;
|
|
185
|
+
});
|
|
133
186
|
}
|
|
134
187
|
/** Manifest schema version this build writes and understands. */
|
|
135
188
|
const PATCHES_MANIFEST_VERSION = 1;
|
|
@@ -156,10 +209,15 @@ export function validatePatchesManifest(data) {
|
|
|
156
209
|
if (!isArray(data['patches'])) {
|
|
157
210
|
throw new Error('patches.json field "patches" must be an array');
|
|
158
211
|
}
|
|
159
|
-
|
|
212
|
+
const manifest = {
|
|
160
213
|
version: 1,
|
|
161
214
|
patches: data['patches'].map((patch, index) => validatePatchMetadata(patch, index)),
|
|
162
215
|
};
|
|
216
|
+
if (data['retired'] !== undefined)
|
|
217
|
+
manifest.retired = parseRetiredSlots(data['retired']);
|
|
218
|
+
if (data['reserved'] !== undefined)
|
|
219
|
+
manifest.reserved = parseReservedSlots(data['reserved']);
|
|
220
|
+
return manifest;
|
|
163
221
|
}
|
|
164
222
|
/**
|
|
165
223
|
* Infers patch metadata from a filename pattern.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The earliest order a new patch can take after a given order, under the
|
|
3
|
+
* queue's occupancy and `patchPolicy`.
|
|
4
|
+
*
|
|
5
|
+
* A refusal that says "export it as its own later patch" leaves the
|
|
6
|
+
* operator to find a number that is free, inside the category's range, and
|
|
7
|
+
* (with `allowGaps: false`) does not open a gap. Each wrong guess costs a
|
|
8
|
+
* full export attempt. This answers it once, with the same policy
|
|
9
|
+
* evaluation the export itself will run.
|
|
10
|
+
*/
|
|
11
|
+
import type { PatchesManifest } from '../types/commands/index.js';
|
|
12
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
13
|
+
/**
|
|
14
|
+
* Smallest order strictly greater than `after` that no patch occupies and
|
|
15
|
+
* that adds no order-related `patchPolicy` finding (collision, category
|
|
16
|
+
* range, reserved range, numeric gap) for a `category` patch. A retired
|
|
17
|
+
* slot counts as free, since a patch may fill it; a reservation does not,
|
|
18
|
+
* since it is held for its owner's export. Returns null when no such
|
|
19
|
+
* order exists inside the category's configured ranges.
|
|
20
|
+
*
|
|
21
|
+
* @param config - Project config (only `patchPolicy` is read)
|
|
22
|
+
* @param manifest - Current manifest
|
|
23
|
+
* @param category - Category the new patch would carry
|
|
24
|
+
* @param after - The order the new patch must come after
|
|
25
|
+
*/
|
|
26
|
+
export declare function earliestLegalOrderAfter(config: Pick<FireForgeConfig, 'patchPolicy'>, manifest: PatchesManifest, category: string, after: number): number | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The earliest order a new patch can take after a given order, under the
|
|
4
|
+
* queue's occupancy and `patchPolicy`.
|
|
5
|
+
*
|
|
6
|
+
* A refusal that says "export it as its own later patch" leaves the
|
|
7
|
+
* operator to find a number that is free, inside the category's range, and
|
|
8
|
+
* (with `allowGaps: false`) does not open a gap. Each wrong guess costs a
|
|
9
|
+
* full export attempt. This answers it once, with the same policy
|
|
10
|
+
* evaluation the export itself will run.
|
|
11
|
+
*/
|
|
12
|
+
import { formatPatchOrder } from './patch-parse.js';
|
|
13
|
+
import { evaluatePatchPolicy } from './patch-policy.js';
|
|
14
|
+
import { reservedOrderSet } from './patch-retired.js';
|
|
15
|
+
/** Policy codes an order choice alone can trip. */
|
|
16
|
+
const ORDER_CODES = new Set([
|
|
17
|
+
'order-collision',
|
|
18
|
+
'category-range',
|
|
19
|
+
'reserved-range',
|
|
20
|
+
'numeric-gap',
|
|
21
|
+
]);
|
|
22
|
+
/** Upper bound on the search when no policy range bounds it. */
|
|
23
|
+
const UNBOUNDED_SEARCH = 1000;
|
|
24
|
+
const GAP_ORDERS_PATTERN = /numeric gap\(s\): (?<orders>[^.]+)\./;
|
|
25
|
+
/**
|
|
26
|
+
* Order-related findings as comparable keys. A `numeric-gap` finding
|
|
27
|
+
* becomes one key per missing order, so filling one gap of several reads as
|
|
28
|
+
* removing a key rather than as a new, differently worded finding.
|
|
29
|
+
*/
|
|
30
|
+
function orderIssueKeys(config, manifest) {
|
|
31
|
+
const keys = new Set();
|
|
32
|
+
for (const issue of evaluatePatchPolicy(config, manifest)) {
|
|
33
|
+
if (!ORDER_CODES.has(issue.code))
|
|
34
|
+
continue;
|
|
35
|
+
const gaps = issue.code === 'numeric-gap' ? GAP_ORDERS_PATTERN.exec(issue.message) : null;
|
|
36
|
+
if (gaps?.groups?.['orders'] === undefined) {
|
|
37
|
+
keys.add(`${issue.code}|${issue.message}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
for (const order of gaps.groups['orders'].split(',')) {
|
|
41
|
+
keys.add(`numeric-gap|${issue.filename}|${order.trim()}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return keys;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Smallest order strictly greater than `after` that no patch occupies and
|
|
48
|
+
* that adds no order-related `patchPolicy` finding (collision, category
|
|
49
|
+
* range, reserved range, numeric gap) for a `category` patch. A retired
|
|
50
|
+
* slot counts as free, since a patch may fill it; a reservation does not,
|
|
51
|
+
* since it is held for its owner's export. Returns null when no such
|
|
52
|
+
* order exists inside the category's configured ranges.
|
|
53
|
+
*
|
|
54
|
+
* @param config - Project config (only `patchPolicy` is read)
|
|
55
|
+
* @param manifest - Current manifest
|
|
56
|
+
* @param category - Category the new patch would carry
|
|
57
|
+
* @param after - The order the new patch must come after
|
|
58
|
+
*/
|
|
59
|
+
export function earliestLegalOrderAfter(config, manifest, category, after) {
|
|
60
|
+
const occupied = new Set([
|
|
61
|
+
...manifest.patches.map((patch) => patch.order),
|
|
62
|
+
...reservedOrderSet(manifest),
|
|
63
|
+
]);
|
|
64
|
+
const ranges = config.patchPolicy?.ranges.filter((range) => range.category === category) ?? [];
|
|
65
|
+
const limit = ranges.length > 0 ? Math.max(...ranges.map((range) => range.to)) : after + UNBOUNDED_SEARCH;
|
|
66
|
+
const baseline = config.patchPolicy ? orderIssueKeys(config, manifest) : new Set();
|
|
67
|
+
for (let order = after + 1; order <= limit; order++) {
|
|
68
|
+
if (occupied.has(order))
|
|
69
|
+
continue;
|
|
70
|
+
if (!config.patchPolicy)
|
|
71
|
+
return order;
|
|
72
|
+
const probe = {
|
|
73
|
+
filename: `${formatPatchOrder(order)}-${category}-probe.patch`,
|
|
74
|
+
order,
|
|
75
|
+
category,
|
|
76
|
+
name: 'probe',
|
|
77
|
+
description: 'probe',
|
|
78
|
+
createdAt: '',
|
|
79
|
+
sourceEsrVersion: '',
|
|
80
|
+
filesAffected: [],
|
|
81
|
+
};
|
|
82
|
+
const projected = { ...manifest, patches: [...manifest.patches, probe] };
|
|
83
|
+
const introduced = [...orderIssueKeys(config, projected)].filter((key) => !baseline.has(key));
|
|
84
|
+
if (introduced.length === 0)
|
|
85
|
+
return order;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-only patch policy: the `patchPolicy` rules that need the
|
|
3
|
+
* patch bodies, not just the manifest.
|
|
4
|
+
*
|
|
5
|
+
* - `upstreamAllowlist`: which upstream files (files no patch in the queue
|
|
6
|
+
* creates) a non-reserved patch may modify.
|
|
7
|
+
* - `addOnly` allowlist entries: for shared registration files, a patch may
|
|
8
|
+
* only add lines, and each non-blank added line must carry the marker.
|
|
9
|
+
* - `scopeRules`: limit a class of patch (a category, or a filename pattern
|
|
10
|
+
* such as `-tests\.patch$`) to the files its globs name.
|
|
11
|
+
* - `verifyDocumentPaths`: every reserved allowlist entry's `adr` /
|
|
12
|
+
* `documentation` path must exist.
|
|
13
|
+
*
|
|
14
|
+
* Every rule is opt-in, and none can be evaluated from `patches.json`
|
|
15
|
+
* alone, which is why they live apart from `patch-policy.ts`: that module
|
|
16
|
+
* answers synchronously over a (possibly projected) manifest, this one reads
|
|
17
|
+
* patch bodies and the filesystem. `verify` and `lint --per-patch` run both.
|
|
18
|
+
*/
|
|
19
|
+
import type { PatchesManifest } from '../types/commands/index.js';
|
|
20
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
21
|
+
import { type PatchQueueEntry } from './patch-lint-cross.js';
|
|
22
|
+
import type { PatchPolicyIssue } from './patch-policy.js';
|
|
23
|
+
/** True when the policy sets any rule this module evaluates. */
|
|
24
|
+
export declare function hasPatchPolicyContentRules(config: FireForgeConfig): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Evaluates the body-dependent `patchPolicy` rules over a patch queue.
|
|
27
|
+
*
|
|
28
|
+
* @param config - Project configuration
|
|
29
|
+
* @param projectRoot - Project root, for the document-path check
|
|
30
|
+
* @param manifest - Current manifest (category and order per patch)
|
|
31
|
+
* @param entries - Patch queue entries with their bodies
|
|
32
|
+
* @returns Findings, in the same shape {@link evaluatePatchPolicy} returns
|
|
33
|
+
*/
|
|
34
|
+
export declare function evaluatePatchPolicyContent(config: FireForgeConfig, projectRoot: string, manifest: PatchesManifest, entries: readonly PatchQueueEntry[]): Promise<PatchPolicyIssue[]>;
|
|
35
|
+
/**
|
|
36
|
+
* {@link evaluatePatchPolicyContent} for a caller without a queue context
|
|
37
|
+
* in hand (`verify`): reads the patch bodies only when a body-dependent
|
|
38
|
+
* rule is configured.
|
|
39
|
+
*/
|
|
40
|
+
export declare function evaluatePatchPolicyContentForQueue(config: FireForgeConfig, projectRoot: string, patchesDir: string, manifest: PatchesManifest): Promise<PatchPolicyIssue[]>;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Registration-only patch policy: the `patchPolicy` rules that need the
|
|
4
|
+
* patch bodies, not just the manifest.
|
|
5
|
+
*
|
|
6
|
+
* - `upstreamAllowlist`: which upstream files (files no patch in the queue
|
|
7
|
+
* creates) a non-reserved patch may modify.
|
|
8
|
+
* - `addOnly` allowlist entries: for shared registration files, a patch may
|
|
9
|
+
* only add lines, and each non-blank added line must carry the marker.
|
|
10
|
+
* - `scopeRules`: limit a class of patch (a category, or a filename pattern
|
|
11
|
+
* such as `-tests\.patch$`) to the files its globs name.
|
|
12
|
+
* - `verifyDocumentPaths`: every reserved allowlist entry's `adr` /
|
|
13
|
+
* `documentation` path must exist.
|
|
14
|
+
*
|
|
15
|
+
* Every rule is opt-in, and none can be evaluated from `patches.json`
|
|
16
|
+
* alone, which is why they live apart from `patch-policy.ts`: that module
|
|
17
|
+
* answers synchronously over a (possibly projected) manifest, this one reads
|
|
18
|
+
* patch bodies and the filesystem. `verify` and `lint --per-patch` run both.
|
|
19
|
+
*/
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { pathExists } from '../utils/fs.js';
|
|
22
|
+
import { matchesAnyGlob } from '../utils/path-glob.js';
|
|
23
|
+
import { buildPatchQueueContext } from './patch-lint-cross.js';
|
|
24
|
+
import { parseDiffSections } from './patch-parse.js';
|
|
25
|
+
/** True when the policy sets any rule this module evaluates. */
|
|
26
|
+
export function hasPatchPolicyContentRules(config) {
|
|
27
|
+
const cfg = config.patchPolicy;
|
|
28
|
+
return (cfg !== undefined &&
|
|
29
|
+
(cfg.upstreamAllowlist !== undefined ||
|
|
30
|
+
cfg.scopeRules !== undefined ||
|
|
31
|
+
cfg.verifyDocumentPaths === true));
|
|
32
|
+
}
|
|
33
|
+
function normalizeAllowEntry(entry) {
|
|
34
|
+
return typeof entry === 'string' ? { path: entry } : entry;
|
|
35
|
+
}
|
|
36
|
+
function isReservedOrder(cfg, order) {
|
|
37
|
+
return (cfg.reservedRanges ?? []).some((range) => order >= range.from && order <= range.to);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The upstream files a patch body edits, keyed by path: every section that
|
|
41
|
+
* does not create its file, minus paths some patch in the queue creates. A
|
|
42
|
+
* rename counts against both of its paths.
|
|
43
|
+
*/
|
|
44
|
+
function collectUpstreamEdits(diff, queueCreated) {
|
|
45
|
+
const edits = new Map();
|
|
46
|
+
for (const section of parseDiffSections(diff)) {
|
|
47
|
+
if (section.isNewFile)
|
|
48
|
+
continue;
|
|
49
|
+
const paths = new Set([section.sourcePath, section.targetPath]);
|
|
50
|
+
for (const path of paths) {
|
|
51
|
+
if (queueCreated.has(path))
|
|
52
|
+
continue;
|
|
53
|
+
const edit = edits.get(path) ?? { added: [], removed: [], deleted: false, binary: false };
|
|
54
|
+
edit.deleted ||= section.isDeletedFile;
|
|
55
|
+
edit.binary ||= section.isBinary;
|
|
56
|
+
for (const hunk of section.hunks) {
|
|
57
|
+
for (const line of hunk.lines) {
|
|
58
|
+
if (line.startsWith('+'))
|
|
59
|
+
edit.added.push(line.slice(1));
|
|
60
|
+
else if (line.startsWith('-'))
|
|
61
|
+
edit.removed.push(line.slice(1));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
edits.set(path, edit);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return edits;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Why an add-only file's edit breaks the mode, or undefined when it holds.
|
|
71
|
+
* Blank lines are free in both directions: they carry no registration.
|
|
72
|
+
*/
|
|
73
|
+
function addOnlyViolation(edit, marker) {
|
|
74
|
+
if (edit.deleted)
|
|
75
|
+
return 'deletes the file';
|
|
76
|
+
if (edit.binary)
|
|
77
|
+
return 'carries a binary change';
|
|
78
|
+
const removed = edit.removed.filter((line) => line.trim() !== '');
|
|
79
|
+
if (removed.length > 0) {
|
|
80
|
+
return `removes ${String(removed.length)} line(s), first: ${JSON.stringify(removed[0])}`;
|
|
81
|
+
}
|
|
82
|
+
const needle = marker.toLowerCase();
|
|
83
|
+
const unmarked = edit.added.filter((line) => line.trim() !== '' && !line.toLowerCase().includes(needle));
|
|
84
|
+
if (unmarked.length > 0) {
|
|
85
|
+
return (`adds ${String(unmarked.length)} line(s) without the "${marker}" marker, ` +
|
|
86
|
+
`first: ${JSON.stringify(unmarked[0])}`);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
function evaluateUpstreamEdits(cfg, filename, edits, severity) {
|
|
91
|
+
const allowlist = (cfg.upstreamAllowlist ?? []).map(normalizeAllowEntry);
|
|
92
|
+
const issues = [];
|
|
93
|
+
const outside = [];
|
|
94
|
+
for (const [path, edit] of [...edits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
95
|
+
const matches = allowlist.filter((entry) => matchesAnyGlob(path, [entry.path]));
|
|
96
|
+
if (matches.length === 0) {
|
|
97
|
+
outside.push(path);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Strictest wins: any add-only match makes the file add-only.
|
|
101
|
+
const addOnly = matches.find((entry) => entry.addOnly === true);
|
|
102
|
+
if (!addOnly)
|
|
103
|
+
continue;
|
|
104
|
+
const marker = addOnly.marker ?? cfg.addOnlyMarker ?? '';
|
|
105
|
+
const violation = addOnlyViolation(edit, marker);
|
|
106
|
+
if (violation !== undefined) {
|
|
107
|
+
issues.push({
|
|
108
|
+
code: 'add-only',
|
|
109
|
+
filename,
|
|
110
|
+
severity,
|
|
111
|
+
message: `${filename} edits ${path}, which patchPolicy.upstreamAllowlist marks add-only ` +
|
|
112
|
+
`(added lines carrying "${marker}"), but ${violation}.`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (outside.length > 0) {
|
|
117
|
+
issues.push({
|
|
118
|
+
code: 'upstream-allowlist',
|
|
119
|
+
filename,
|
|
120
|
+
severity,
|
|
121
|
+
message: `${filename} modifies upstream file(s) outside patchPolicy.upstreamAllowlist: ` +
|
|
122
|
+
`${outside.join(', ')}. Move the change into a fork-owned file, or add a reviewed allowlist entry.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return issues;
|
|
126
|
+
}
|
|
127
|
+
function evaluateScopeRules(cfg, filename, category, files, severity) {
|
|
128
|
+
const issues = [];
|
|
129
|
+
for (const [index, rule] of (cfg.scopeRules ?? []).entries()) {
|
|
130
|
+
if (rule.category !== undefined && rule.category !== category)
|
|
131
|
+
continue;
|
|
132
|
+
if (rule.filenamePattern !== undefined && !new RegExp(rule.filenamePattern).test(filename)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const outside = files.filter((file) => !matchesAnyGlob(file, rule.files));
|
|
136
|
+
if (outside.length === 0)
|
|
137
|
+
continue;
|
|
138
|
+
issues.push({
|
|
139
|
+
code: 'scope',
|
|
140
|
+
filename,
|
|
141
|
+
severity,
|
|
142
|
+
message: `${filename} is scoped by patchPolicy.scopeRules[${String(index)}] to ` +
|
|
143
|
+
`${rule.files.join(', ')}, but touches: ${outside.join(', ')}. ` +
|
|
144
|
+
'Move those files to a patch of the class that owns them.',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return issues;
|
|
148
|
+
}
|
|
149
|
+
async function evaluateDocumentPaths(cfg, projectRoot, severity) {
|
|
150
|
+
if (cfg.verifyDocumentPaths !== true)
|
|
151
|
+
return [];
|
|
152
|
+
const issues = [];
|
|
153
|
+
for (const range of cfg.reservedRanges ?? []) {
|
|
154
|
+
for (const allowed of range.allowed) {
|
|
155
|
+
for (const [field, path] of [
|
|
156
|
+
['adr', allowed.adr],
|
|
157
|
+
['documentation', allowed.documentation],
|
|
158
|
+
]) {
|
|
159
|
+
if (path === undefined || (await pathExists(join(projectRoot, path))))
|
|
160
|
+
continue;
|
|
161
|
+
issues.push({
|
|
162
|
+
code: 'reserved-document-missing',
|
|
163
|
+
filename: allowed.filename,
|
|
164
|
+
severity,
|
|
165
|
+
message: `${allowed.filename}'s reserved-range allowlist entry names ${field} "${path}", ` +
|
|
166
|
+
'which does not exist. Restore the document or correct the path.',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return issues;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Evaluates the body-dependent `patchPolicy` rules over a patch queue.
|
|
175
|
+
*
|
|
176
|
+
* @param config - Project configuration
|
|
177
|
+
* @param projectRoot - Project root, for the document-path check
|
|
178
|
+
* @param manifest - Current manifest (category and order per patch)
|
|
179
|
+
* @param entries - Patch queue entries with their bodies
|
|
180
|
+
* @returns Findings, in the same shape {@link evaluatePatchPolicy} returns
|
|
181
|
+
*/
|
|
182
|
+
export async function evaluatePatchPolicyContent(config, projectRoot, manifest, entries) {
|
|
183
|
+
const cfg = config.patchPolicy;
|
|
184
|
+
if (!cfg || !hasPatchPolicyContentRules(config))
|
|
185
|
+
return [];
|
|
186
|
+
const severity = cfg.mutationMode === 'warn' ? 'warning' : 'error';
|
|
187
|
+
const queueCreated = new Set();
|
|
188
|
+
for (const entry of entries)
|
|
189
|
+
for (const file of entry.createdFiles)
|
|
190
|
+
queueCreated.add(file);
|
|
191
|
+
const bodies = new Map(entries.map((entry) => [entry.filename, entry.diff]));
|
|
192
|
+
const issues = [];
|
|
193
|
+
for (const patch of manifest.patches) {
|
|
194
|
+
const diff = bodies.get(patch.filename);
|
|
195
|
+
if (cfg.upstreamAllowlist !== undefined && diff !== undefined) {
|
|
196
|
+
if (!isReservedOrder(cfg, patch.order)) {
|
|
197
|
+
const edits = collectUpstreamEdits(diff, queueCreated);
|
|
198
|
+
issues.push(...evaluateUpstreamEdits(cfg, patch.filename, edits, severity));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
issues.push(...evaluateScopeRules(cfg, patch.filename, patch.category, patch.filesAffected, severity));
|
|
202
|
+
}
|
|
203
|
+
issues.push(...(await evaluateDocumentPaths(cfg, projectRoot, severity)));
|
|
204
|
+
return issues;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* {@link evaluatePatchPolicyContent} for a caller without a queue context
|
|
208
|
+
* in hand (`verify`): reads the patch bodies only when a body-dependent
|
|
209
|
+
* rule is configured.
|
|
210
|
+
*/
|
|
211
|
+
export async function evaluatePatchPolicyContentForQueue(config, projectRoot, patchesDir, manifest) {
|
|
212
|
+
if (!hasPatchPolicyContentRules(config))
|
|
213
|
+
return [];
|
|
214
|
+
const { entries } = await buildPatchQueueContext(patchesDir);
|
|
215
|
+
return evaluatePatchPolicyContent(config, projectRoot, manifest, entries);
|
|
216
|
+
}
|