@hominis/fireforge 0.49.1 → 0.50.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 +41 -0
- 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 +28 -0
- package/dist/src/commands/check-report.js +78 -0
- package/dist/src/commands/check-run.d.ts +28 -0
- package/dist/src/commands/check-run.js +104 -0
- package/dist/src/commands/check-steps.d.ts +67 -0
- package/dist/src/commands/check-steps.js +175 -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/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-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-templates.d.ts +8 -3
- package/dist/src/commands/furnace/create-templates.js +8 -23
- 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 +93 -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 +127 -114
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +31 -60
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +15 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +2 -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 +13 -98
- package/dist/src/commands/patch/index.d.ts +1 -1
- package/dist/src/commands/patch/index.js +4 -2
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +65 -28
- package/dist/src/commands/patch/move-files-create.js +26 -17
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +3 -1
- 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 +7 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +128 -0
- package/dist/src/commands/patch/split-plan.d.ts +7 -7
- package/dist/src/commands/patch/split-plan.js +45 -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 +51 -15
- 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.js +14 -41
- 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 +45 -2
- package/dist/src/commands/re-export-adjacent.js +156 -37
- 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-lint.d.ts +2 -1
- package/dist/src/commands/re-export-lint.js +23 -10
- package/dist/src/commands/re-export-options.js +45 -4
- 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-register.js +42 -17
- package/dist/src/commands/re-export-removed.d.ts +12 -1
- package/dist/src/commands/re-export-removed.js +41 -11
- 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 +17 -9
- package/dist/src/commands/re-export-scan.js +56 -23
- package/dist/src/commands/re-export-single.js +22 -6
- 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 +37 -24
- 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 +21 -0
- package/dist/src/commands/test-build-furnace.js +48 -0
- package/dist/src/commands/test-build-phase.js +47 -41
- package/dist/src/commands/test-checkjs.js +7 -0
- package/dist/src/commands/test-diagnose.js +7 -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 +43 -0
- package/dist/src/commands/test-orchestrate.js +222 -0
- package/dist/src/commands/test-perf-scope.d.ts +29 -0
- package/dist/src/commands/test-perf-scope.js +83 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +177 -0
- package/dist/src/commands/test-register.js +54 -10
- package/dist/src/commands/test-run.js +4 -1
- package/dist/src/commands/test-stale-gate.js +57 -27
- package/dist/src/commands/test-verdict.d.ts +10 -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 +66 -60
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +111 -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 +59 -35
- 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 +7 -0
- package/dist/src/core/build-prepare.js +14 -8
- 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 +8 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +143 -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 +7 -0
- package/dist/src/core/config-validate-token-scales.d.ts +6 -0
- package/dist/src/core/config-validate-token-scales.js +50 -0
- package/dist/src/core/config-validate.js +14 -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 +97 -0
- package/dist/src/core/coverage-records.js +350 -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 +10 -1
- package/dist/src/core/engine-session-lock.js +28 -8
- 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.d.ts +4 -25
- package/dist/src/core/file-lock.js +105 -138
- 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 +12 -2
- 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 +9 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
- package/dist/src/core/furnace-config-validate.js +8 -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-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 +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +10 -1
- package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
- package/dist/src/core/furnace-validate-exceptions.js +43 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +49 -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 +157 -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/mach.js +8 -1
- package/dist/src/core/negative-control-diff.d.ts +39 -0
- package/dist/src/core/negative-control-diff.js +136 -0
- package/dist/src/core/negative-control.d.ts +138 -0
- package/dist/src/core/negative-control.js +356 -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/patch-export-update.d.ts +2 -0
- package/dist/src/core/patch-export-update.js +1 -0
- package/dist/src/core/patch-export.js +3 -4
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -11
- 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.js +12 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +10 -4
- 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 +18 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +152 -0
- package/dist/src/core/patch-lint-ignore.js +299 -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 +323 -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 +39 -0
- package/dist/src/core/patch-lint-token-scale.js +109 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +64 -286
- 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 +19 -8
- package/dist/src/core/patch-manifest-validate.js +34 -2
- package/dist/src/core/patch-order-legal.d.ts +25 -0
- package/dist/src/core/patch-order-legal.js +83 -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 +16 -3
- package/dist/src/core/patch-policy.js +16 -9
- package/dist/src/core/patch-retired.d.ts +35 -0
- package/dist/src/core/patch-retired.js +54 -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 +20 -0
- package/dist/src/core/run-log.js +89 -3
- 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 +18 -1
- package/dist/src/core/test-harness-crash.js +6 -4
- package/dist/src/core/test-harness-signature.d.ts +17 -0
- package/dist/src/core/test-harness-signature.js +4 -0
- package/dist/src/core/test-run-completeness.d.ts +57 -0
- package/dist/src/core/test-run-completeness.js +144 -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/token-lint.d.ts +50 -0
- package/dist/src/core/token-lint.js +126 -0
- package/dist/src/core/tree-guard.js +3 -0
- package/dist/src/core/typecheck-shim.d.ts +1 -1
- package/dist/src/core/typecheck-shim.js +90 -2
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +3 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +148 -21
- package/dist/src/types/commands/patches.d.ts +75 -1
- package/dist/src/types/config.d.ts +192 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +43 -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 +34 -6
- package/dist/src/utils/options.js +86 -5
- 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 +282 -9
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +97 -3
- package/docs/lifecycle-invariants.md +52 -19
- package/docs/machine-output.md +74 -12
- package/docs/patch-workflow.md +297 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +276 -20
- package/package.json +2 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The step plan of `fireforge check`: which commands run, with which
|
|
4
|
+
* arguments, in which order constraints, and how each one's output is
|
|
5
|
+
* condensed into its table row.
|
|
6
|
+
*
|
|
7
|
+
* Every built-in step is the real command run as a child `fireforge`
|
|
8
|
+
* process, not an in-process call. That is what keeps the gate's output
|
|
9
|
+
* sane (each command owns its stdout, its machine mode and its error
|
|
10
|
+
* boundary) and what avoids double locking: `check` itself takes no lock,
|
|
11
|
+
* so each child takes exactly the locks its command takes standalone.
|
|
12
|
+
* None of the built-in steps takes the engine session lock, which is why
|
|
13
|
+
* they can run side by side.
|
|
14
|
+
*/
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { GeneralError } from '../errors/base.js';
|
|
17
|
+
/**
|
|
18
|
+
* How to re-run this CLI as a child: the same Node, the same loader flags
|
|
19
|
+
* (minus debugger flags, whose port the parent already holds), the same
|
|
20
|
+
* entry script.
|
|
21
|
+
*/
|
|
22
|
+
function fireforgeInvocation() {
|
|
23
|
+
const entry = process.argv[1];
|
|
24
|
+
if (entry === undefined) {
|
|
25
|
+
throw new GeneralError('Cannot resolve the fireforge CLI entry point for check steps.');
|
|
26
|
+
}
|
|
27
|
+
const loaderFlags = process.execArgv.filter((flag) => !flag.startsWith('--inspect'));
|
|
28
|
+
return { command: process.execPath, prefix: [...loaderFlags, entry] };
|
|
29
|
+
}
|
|
30
|
+
/** Strips clack's gutter glyphs so a captured line reads as plain text. */
|
|
31
|
+
function plainLine(line) {
|
|
32
|
+
return line.replace(/^[\s│■●◆◇▲└┌|]+/u, '').trim();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The generic detail: nothing on success, and on failure the last line
|
|
36
|
+
* the step printed (its refusal, usually), cut to fit a table cell.
|
|
37
|
+
*/
|
|
38
|
+
export function lastOutputLine(combined, exitCode) {
|
|
39
|
+
if (exitCode === 0)
|
|
40
|
+
return '';
|
|
41
|
+
const lines = combined
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.map(plainLine)
|
|
44
|
+
.filter((line) => line.length > 0);
|
|
45
|
+
const last = lines.at(-1) ?? '';
|
|
46
|
+
return last.length > 120 ? `${last.slice(0, 117)}...` : last;
|
|
47
|
+
}
|
|
48
|
+
function parseJsonDocument(stdout) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(stdout);
|
|
51
|
+
return typeof parsed === 'object' && parsed !== null
|
|
52
|
+
? parsed
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Status detail from its `--json --summary` document: the failing classes. */
|
|
60
|
+
export function summarizeStatus(output) {
|
|
61
|
+
const doc = parseJsonDocument(output.stdout);
|
|
62
|
+
if (doc?.summary === undefined)
|
|
63
|
+
return lastOutputLine(output.combined, output.exitCode);
|
|
64
|
+
const offenders = doc.check?.offenders ?? [];
|
|
65
|
+
if (offenders.length > 0) {
|
|
66
|
+
return offenders.map((entry) => `${entry.count} ${entry.classification}`).join(', ');
|
|
67
|
+
}
|
|
68
|
+
return `${doc.summary.total ?? 0} changed file(s), none failing`;
|
|
69
|
+
}
|
|
70
|
+
/** Furnace detail from its `--json` document: counts after exceptions. */
|
|
71
|
+
export function summarizeFurnace(output) {
|
|
72
|
+
const doc = parseJsonDocument(output.stdout);
|
|
73
|
+
if (doc?.summary === undefined)
|
|
74
|
+
return lastOutputLine(output.combined, output.exitCode);
|
|
75
|
+
const { errors, warnings, excepted, staleExceptions } = doc.summary;
|
|
76
|
+
const extra = [
|
|
77
|
+
excepted > 0 ? `${excepted} excepted` : '',
|
|
78
|
+
staleExceptions > 0 ? `${staleExceptions} stale exception(s)` : '',
|
|
79
|
+
].filter((part) => part.length > 0);
|
|
80
|
+
return [`${errors} error(s), ${warnings} warning(s)`, ...extra].join(', ');
|
|
81
|
+
}
|
|
82
|
+
/** Token-coverage detail: the coverage percentage it reports. */
|
|
83
|
+
export function summarizeTokenCoverage(output) {
|
|
84
|
+
const match = /Token coverage: (\d+)%/.exec(output.combined);
|
|
85
|
+
if (match?.[1] !== undefined)
|
|
86
|
+
return `coverage ${match[1]}%`;
|
|
87
|
+
if (output.combined.includes('No modified CSS files'))
|
|
88
|
+
return 'no modified CSS';
|
|
89
|
+
return lastOutputLine(output.combined, output.exitCode);
|
|
90
|
+
}
|
|
91
|
+
function generic(output) {
|
|
92
|
+
return lastOutputLine(output.combined, output.exitCode);
|
|
93
|
+
}
|
|
94
|
+
/** The status step's `--check` flags from `check.status`. */
|
|
95
|
+
export function statusPolicyArgs(policy) {
|
|
96
|
+
const args = [];
|
|
97
|
+
if (policy?.failOn !== undefined)
|
|
98
|
+
args.push('--fail-on', policy.failOn.join(','));
|
|
99
|
+
if (policy?.allow !== undefined)
|
|
100
|
+
args.push('--allow', policy.allow.join(','));
|
|
101
|
+
if (policy?.failOnUnknown === true)
|
|
102
|
+
args.push('--fail-on-unknown');
|
|
103
|
+
return args;
|
|
104
|
+
}
|
|
105
|
+
function builtinStep(invocation, projectRoot, name, args, summarize, extra = {}) {
|
|
106
|
+
return {
|
|
107
|
+
name,
|
|
108
|
+
kind: 'builtin',
|
|
109
|
+
command: invocation.command,
|
|
110
|
+
args: [...invocation.prefix, ...args],
|
|
111
|
+
display: `fireforge ${args.join(' ')}`,
|
|
112
|
+
cwd: projectRoot,
|
|
113
|
+
after: extra.after ?? [],
|
|
114
|
+
skipReason: extra.skipReason,
|
|
115
|
+
summarize,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Runs a project command line through the platform shell. */
|
|
119
|
+
function shellInvocation(commandLine) {
|
|
120
|
+
return process.platform === 'win32'
|
|
121
|
+
? { command: 'cmd.exe', args: ['/d', '/s', '/c', commandLine] }
|
|
122
|
+
: { command: '/bin/sh', args: ['-c', commandLine] };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Builds the full plan: the six built-in steps (with the ones this project
|
|
126
|
+
* cannot run marked skipped) followed by `check.extraSteps[]`.
|
|
127
|
+
*
|
|
128
|
+
* `furnace-validate` waits for `typecheck`, the one built-in step that
|
|
129
|
+
* writes: it regenerates a stale Furnace jsconfig, whose drift
|
|
130
|
+
* `furnace validate` reports. Run side by side, the two would race on that
|
|
131
|
+
* file and the verdict would depend on which won.
|
|
132
|
+
*/
|
|
133
|
+
export function buildCheckPlan(input) {
|
|
134
|
+
const { projectRoot, config } = input;
|
|
135
|
+
const invocation = fireforgeInvocation();
|
|
136
|
+
const check = config.check ?? {};
|
|
137
|
+
const patchArgs = input.patches !== undefined ? ['--patches', input.patches.join(',')] : [];
|
|
138
|
+
const lintWarnings = check.maxWarnings?.lint !== undefined ? ['--max-warnings', String(check.maxWarnings.lint)] : [];
|
|
139
|
+
const furnaceWarnings = check.maxWarnings?.furnace !== undefined
|
|
140
|
+
? ['--max-warnings', String(check.maxWarnings.furnace)]
|
|
141
|
+
: [];
|
|
142
|
+
const plan = [
|
|
143
|
+
builtinStep(invocation, projectRoot, 'status', ['status', '--json', '--summary', '--check', ...statusPolicyArgs(check.status), ...patchArgs], summarizeStatus),
|
|
144
|
+
builtinStep(invocation, projectRoot, 'verify', ['verify', ...patchArgs], generic),
|
|
145
|
+
builtinStep(invocation, projectRoot, 'lint', ['lint', '--per-patch', '--notices', 'summary', ...lintWarnings, ...patchArgs], generic),
|
|
146
|
+
builtinStep(invocation, projectRoot, 'typecheck', ['typecheck'], generic, {
|
|
147
|
+
skipReason: config.typecheck === undefined ? 'no "typecheck" block in fireforge.json' : undefined,
|
|
148
|
+
}),
|
|
149
|
+
builtinStep(invocation, projectRoot, 'furnace-validate', ['furnace', 'validate', '--json', ...furnaceWarnings], summarizeFurnace, {
|
|
150
|
+
after: ['typecheck'],
|
|
151
|
+
skipReason: input.furnaceConfigured ? undefined : 'no furnace.json',
|
|
152
|
+
}),
|
|
153
|
+
builtinStep(invocation, projectRoot, 'token-coverage', ['token', 'coverage'], summarizeTokenCoverage),
|
|
154
|
+
];
|
|
155
|
+
for (const step of check.extraSteps ?? []) {
|
|
156
|
+
const shell = shellInvocation(step.command);
|
|
157
|
+
plan.push({
|
|
158
|
+
name: step.name,
|
|
159
|
+
kind: 'extra',
|
|
160
|
+
command: shell.command,
|
|
161
|
+
args: shell.args,
|
|
162
|
+
display: step.command,
|
|
163
|
+
cwd: step.cwd !== undefined ? join(projectRoot, step.cwd) : projectRoot,
|
|
164
|
+
timeoutMs: step.timeout !== undefined ? step.timeout * 1000 : undefined,
|
|
165
|
+
after: [],
|
|
166
|
+
summarize: generic,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
for (const name of check.skip ?? []) {
|
|
170
|
+
const step = plan.find((entry) => entry.name === name);
|
|
171
|
+
if (step !== undefined)
|
|
172
|
+
step.skipReason = 'skipped by check.skip in fireforge.json';
|
|
173
|
+
}
|
|
174
|
+
return plan;
|
|
175
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
import { type CheckStepPlan } from './check-steps.js';
|
|
4
|
+
/** Options Commander forwards from `fireforge check`. */
|
|
5
|
+
export interface CheckCommandOptions {
|
|
6
|
+
json?: boolean;
|
|
7
|
+
/** Judge only these patches in status, verify and lint. */
|
|
8
|
+
patches?: string[];
|
|
9
|
+
/** Comma-separated step names to leave out. */
|
|
10
|
+
skip?: string;
|
|
11
|
+
/** Comma-separated step names to run, leaving out the rest. */
|
|
12
|
+
only?: string;
|
|
13
|
+
/** Raw `--jobs` value. */
|
|
14
|
+
jobs?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Applies `--only` and `--skip` to the plan. A step left out this way is
|
|
18
|
+
* dropped from the table rather than shown as skipped: the caller asked
|
|
19
|
+
* for a narrower gate, not a report of what it chose not to run.
|
|
20
|
+
*/
|
|
21
|
+
export declare function selectCheckSteps(plan: CheckStepPlan[], options: Pick<CheckCommandOptions, 'only' | 'skip'>): CheckStepPlan[];
|
|
22
|
+
/** Parses `--jobs`: a positive integer. Defaults to one slot per CPU. */
|
|
23
|
+
export declare function parseCheckJobs(raw: string | undefined): number;
|
|
24
|
+
/**
|
|
25
|
+
* Runs the composed gate.
|
|
26
|
+
*
|
|
27
|
+
* @param projectRoot - Project root directory
|
|
28
|
+
* @param options - See {@link CheckCommandOptions}
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkCommand(projectRoot: string, options?: CheckCommandOptions): Promise<void>;
|
|
31
|
+
/** Registers the `check` command on the CLI program. */
|
|
32
|
+
export declare function registerCheck(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge check`: the composed static gate. Runs status, verify,
|
|
4
|
+
* per-patch lint, typecheck, Furnace validation and token coverage, plus
|
|
5
|
+
* the project's `check.extraSteps[]`, in parallel, and ends with one
|
|
6
|
+
* `FIREFORGE-VERDICT:` line over a per-step table (or one JSON document
|
|
7
|
+
* under `--json`).
|
|
8
|
+
*
|
|
9
|
+
* Each step is the real command in a child process (see check-steps.ts for
|
|
10
|
+
* why), so a step behaves exactly as it does when run on its own.
|
|
11
|
+
*/
|
|
12
|
+
import { availableParallelism } from 'node:os';
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
15
|
+
import { furnaceConfigExists } from '../core/furnace-config.js';
|
|
16
|
+
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
17
|
+
import { selectPatchSubset } from '../core/patch-subset.js';
|
|
18
|
+
import { GeneralError, InvalidArgumentError, LockContentionError } from '../errors/base.js';
|
|
19
|
+
import { ConfigError } from '../errors/config.js';
|
|
20
|
+
import { toError } from '../utils/errors.js';
|
|
21
|
+
import { info, intro, isMachineOutputMode, note, outro, setMachineOutputMode, } from '../utils/logger.js';
|
|
22
|
+
import { formatCheckTable, judgeCheck, writeCheckJson, writeCheckVerdictLine, } from './check-report.js';
|
|
23
|
+
import { runCheckSteps } from './check-run.js';
|
|
24
|
+
import { buildCheckPlan } from './check-steps.js';
|
|
25
|
+
import { resolveStatusCheckPolicy } from './status-check.js';
|
|
26
|
+
function parseNameList(raw) {
|
|
27
|
+
if (raw === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
return raw
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((name) => name.trim())
|
|
32
|
+
.filter((name) => name.length > 0);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Applies `--only` and `--skip` to the plan. A step left out this way is
|
|
36
|
+
* dropped from the table rather than shown as skipped: the caller asked
|
|
37
|
+
* for a narrower gate, not a report of what it chose not to run.
|
|
38
|
+
*/
|
|
39
|
+
export function selectCheckSteps(plan, options) {
|
|
40
|
+
const known = plan.map((step) => step.name);
|
|
41
|
+
const only = parseNameList(options.only);
|
|
42
|
+
const skip = parseNameList(options.skip);
|
|
43
|
+
for (const [flag, names] of [
|
|
44
|
+
['--only', only],
|
|
45
|
+
['--skip', skip],
|
|
46
|
+
]) {
|
|
47
|
+
const unknown = (names ?? []).filter((name) => !known.includes(name));
|
|
48
|
+
if (unknown.length > 0) {
|
|
49
|
+
throw new InvalidArgumentError(`Unknown ${flag} step "${unknown.join('", "')}". Steps: ${known.join(', ')}.`, flag);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return plan.filter((step) => (only === undefined || only.includes(step.name)) && skip?.includes(step.name) !== true);
|
|
53
|
+
}
|
|
54
|
+
/** Parses `--jobs`: a positive integer. Defaults to one slot per CPU. */
|
|
55
|
+
export function parseCheckJobs(raw) {
|
|
56
|
+
if (raw === undefined)
|
|
57
|
+
return availableParallelism();
|
|
58
|
+
const jobs = Number(raw);
|
|
59
|
+
if (!Number.isInteger(jobs) || jobs < 1) {
|
|
60
|
+
throw new InvalidArgumentError('--jobs must be a positive integer.', '--jobs');
|
|
61
|
+
}
|
|
62
|
+
return jobs;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Refuses a `check.status` policy the status step would refuse, before any
|
|
66
|
+
* step runs, as the config error it is.
|
|
67
|
+
*/
|
|
68
|
+
function assertStatusPolicy(config) {
|
|
69
|
+
const policy = config.check?.status;
|
|
70
|
+
if (policy === undefined)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
resolveStatusCheckPolicy({
|
|
74
|
+
check: true,
|
|
75
|
+
...(policy.failOn !== undefined ? { failOn: policy.failOn.join(',') } : {}),
|
|
76
|
+
...(policy.allow !== undefined ? { allow: policy.allow.join(',') } : {}),
|
|
77
|
+
...(policy.failOnUnknown === true ? { failOnUnknown: true } : {}),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new ConfigError(`Config field "check.status" is invalid: ${toError(error).message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Resolves `--patches` once, so a typo fails here rather than in three steps. */
|
|
85
|
+
async function resolvePatchScope(patchesDir, requested) {
|
|
86
|
+
if (requested === undefined || requested.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
89
|
+
if (!manifest || manifest.patches.length === 0) {
|
|
90
|
+
throw new GeneralError('--patches needs a patch queue, but patches.json lists no patches.');
|
|
91
|
+
}
|
|
92
|
+
return selectPatchSubset(manifest, requested, '--patches selects patches (filename, stem, order number, manifest name, or slug).').map((patch) => patch.filename);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Runs the composed gate.
|
|
96
|
+
*
|
|
97
|
+
* @param projectRoot - Project root directory
|
|
98
|
+
* @param options - See {@link CheckCommandOptions}
|
|
99
|
+
*/
|
|
100
|
+
export async function checkCommand(projectRoot, options = {}) {
|
|
101
|
+
const previousMachineOutputMode = isMachineOutputMode();
|
|
102
|
+
// `--json` owns stdout from the first byte. On a throw the mode stays
|
|
103
|
+
// engaged and withErrorHandling resets it after routing the refusal.
|
|
104
|
+
if (options.json === true)
|
|
105
|
+
setMachineOutputMode(true);
|
|
106
|
+
await runCheck(projectRoot, options);
|
|
107
|
+
if (isMachineOutputMode() !== previousMachineOutputMode) {
|
|
108
|
+
setMachineOutputMode(previousMachineOutputMode);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function runCheck(projectRoot, options) {
|
|
112
|
+
intro('FireForge Check');
|
|
113
|
+
const jobs = parseCheckJobs(options.jobs);
|
|
114
|
+
const config = await loadConfig(projectRoot);
|
|
115
|
+
assertStatusPolicy(config);
|
|
116
|
+
const patches = await resolvePatchScope(getProjectPaths(projectRoot).patches, options.patches);
|
|
117
|
+
const plan = selectCheckSteps(buildCheckPlan({
|
|
118
|
+
projectRoot,
|
|
119
|
+
config,
|
|
120
|
+
furnaceConfigured: await furnaceConfigExists(projectRoot),
|
|
121
|
+
patches,
|
|
122
|
+
}), options);
|
|
123
|
+
const runnable = plan.filter((step) => step.skipReason === undefined).length;
|
|
124
|
+
info(`Running ${runnable} step(s), up to ${Math.min(jobs, Math.max(1, runnable))} at once${patches ? `, judging ${patches.join(', ')}` : ''}.`);
|
|
125
|
+
const results = await runCheckSteps(plan, jobs);
|
|
126
|
+
const verdict = judgeCheck(results);
|
|
127
|
+
if (options.json === true) {
|
|
128
|
+
writeCheckJson(results, verdict);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
note(formatCheckTable(results), 'Steps');
|
|
132
|
+
for (const result of results) {
|
|
133
|
+
if (result.status === 'fail' && result.outputTail) {
|
|
134
|
+
note(result.outputTail, `${result.name}: ${result.command}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
outro(verdict.passed ? 'Check passed' : `Check failed: ${verdict.failed.join(', ')}`);
|
|
138
|
+
writeCheckVerdictLine(verdict);
|
|
139
|
+
}
|
|
140
|
+
if (verdict.reason === 'lock-timeout') {
|
|
141
|
+
throw new LockContentionError(`fireforge check: ${verdict.failed.join(', ')} could not take their locks in time. Re-run when the holder is done.`);
|
|
142
|
+
}
|
|
143
|
+
if (!verdict.passed) {
|
|
144
|
+
throw new GeneralError(`fireforge check failed: ${verdict.failed.join(', ')}. Run a failing step on its own for its full output.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Registers the `check` command on the CLI program. */
|
|
148
|
+
export function registerCheck(program, { getProjectRoot, withErrorHandling }) {
|
|
149
|
+
program
|
|
150
|
+
.command('check')
|
|
151
|
+
.description('Composed static gate: status, verify, per-patch lint, typecheck, Furnace validation, token coverage and fireforge.json check.extraSteps in parallel, with one FIREFORGE-VERDICT line')
|
|
152
|
+
.option('--json', 'Write one JSON document (verdict plus a result per step) instead of the table')
|
|
153
|
+
.option('--patches <names...>', 'Judge only these patches in the status, verify and lint steps (filename, stem, order number, manifest name, or slug)')
|
|
154
|
+
.option('--only <steps>', 'Comma-separated steps to run, leaving out the rest')
|
|
155
|
+
.option('--skip <steps>', 'Comma-separated steps to leave out')
|
|
156
|
+
.option('--jobs <n>', 'Steps to run at once (default: one per CPU)')
|
|
157
|
+
.action(withErrorHandling(async (options) => {
|
|
158
|
+
await checkCommand(getProjectRoot(), options);
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
@@ -35,6 +35,7 @@ const STRING_TYPED_KEYS = new Set([
|
|
|
35
35
|
'firefox.candidate',
|
|
36
36
|
'license',
|
|
37
37
|
'wire.subscriptDir',
|
|
38
|
+
'history.path',
|
|
38
39
|
]);
|
|
39
40
|
/**
|
|
40
41
|
* Parses a string value into the appropriate type.
|
|
@@ -131,12 +132,10 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
131
132
|
// Readers stay lock-free (see `withConfigFileLock`).
|
|
132
133
|
unchanged = await withConfigFileLock(projectRoot, async () => {
|
|
133
134
|
// Short-circuit when the new value matches the current on-disk
|
|
134
|
-
// value
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// check uses the raw on-disk document so forced keys round-trip the
|
|
139
|
-
// same as known keys.
|
|
135
|
+
// value, so a harmless re-set neither rewrites the file nor bumps
|
|
136
|
+
// its mtime. The check uses the raw on-disk document so forced keys
|
|
137
|
+
// round-trip the same as known keys. A real change is written by
|
|
138
|
+
// `writeConfigDocument`, which edits only the changed value in place.
|
|
140
139
|
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
141
140
|
const currentValue = getNestedValue(rawConfig, key);
|
|
142
141
|
if (deepEqual(currentValue, parsedValue)) {
|
|
@@ -8,5 +8,10 @@ export const ENGINE_ESLINT_DOCTOR_CHECK = {
|
|
|
8
8
|
skipIf: (ctx) => !ctx.engineExists,
|
|
9
9
|
run: async (ctx) => (await pathExists(join(ctx.paths.engine, 'node_modules/eslint/bin/eslint.js')))
|
|
10
10
|
? ok('Engine ESLint installed')
|
|
11
|
-
: warning('Engine ESLint installed', 'Engine-local ESLint is missing after source extraction; an engine-eslint ratchet cannot run.',
|
|
11
|
+
: warning('Engine ESLint installed', 'Engine-local ESLint is missing after source extraction; an engine-eslint ratchet cannot run.',
|
|
12
|
+
// `./mach npm ci` installs only the node dependencies the ratchet
|
|
13
|
+
// runs. `./mach eslint --setup` also builds mach's Python `lint`
|
|
14
|
+
// virtualenv, whose pinned requirements can stop resolving on PyPI
|
|
15
|
+
// and fail the whole step with node_modules still missing.
|
|
16
|
+
'Run "cd engine && ./mach npm ci --no-audit --no-fund" after each source refresh, before building or running the gate. It installs engine/node_modules without the Python lint virtualenv "./mach eslint --setup" also builds.'),
|
|
12
17
|
};
|
|
@@ -58,7 +58,10 @@ async function runSourcePinCheck(ctx) {
|
|
|
58
58
|
}
|
|
59
59
|
const downloaded = ctx.state.downloadedVersion;
|
|
60
60
|
const mismatches = [];
|
|
61
|
-
|
|
61
|
+
// Firefox's version.txt never carries the `esr` suffix (an ESR 153.4.0esr
|
|
62
|
+
// tree reads 153.4.0), so the engine comparison drops it from the pin. The
|
|
63
|
+
// last download recorded the full pin, so that comparison keeps it.
|
|
64
|
+
if (actual !== undefined && actual !== pinned && actual !== pinned.replace(/esr$/, '')) {
|
|
62
65
|
mismatches.push(`engine baseline browser/config/version.txt reads ${actual}`);
|
|
63
66
|
}
|
|
64
67
|
if (downloaded !== undefined && downloaded !== pinned) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The advice an export placement refusal appends for new files: "register
|
|
3
|
+
* first" when a new system module only lacks its moz.build line, and
|
|
4
|
+
* adopt-then-split for a real moved-code block, but only after checking
|
|
5
|
+
* that the adopt itself would pass.
|
|
6
|
+
*
|
|
7
|
+
* The advice used to be offered on the moved-code heuristic alone. On one
|
|
8
|
+
* queue it told the operator to adopt six new modules into an existing
|
|
9
|
+
* patch when the only error was the missing registration; the adopt then
|
|
10
|
+
* refused one module for importing later patches, and without it breached
|
|
11
|
+
* the source patch's hard size limit. Split out of export-flow.ts for the
|
|
12
|
+
* line budget.
|
|
13
|
+
*/
|
|
14
|
+
import { type PatchQueueContext } from '../core/patch-lint.js';
|
|
15
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
16
|
+
/**
|
|
17
|
+
* Why adopting `diff` into `sourceFilename` would itself be refused: the
|
|
18
|
+
* forward imports the adopted files would give the source patch, and the
|
|
19
|
+
* source patch's hard size limit. Empty when the adopt is clean.
|
|
20
|
+
*
|
|
21
|
+
* @param baseCtx - The current queue
|
|
22
|
+
* @param sourceFilename - The patch the adopt would grow
|
|
23
|
+
* @param diff - The pending export's diff
|
|
24
|
+
*/
|
|
25
|
+
export declare function findAdoptBlockers(baseCtx: PatchQueueContext, sourceFilename: string, diff: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Detail lines for a placement refusal: register-first and moved-code
|
|
28
|
+
* advice, each only when the projection supports it.
|
|
29
|
+
*
|
|
30
|
+
* @param baseCtx - The current queue
|
|
31
|
+
* @param diff - The pending export's diff
|
|
32
|
+
* @param projectedIssues - Error findings of the placement projection
|
|
33
|
+
* @param plan - The placement's new filename and order
|
|
34
|
+
*/
|
|
35
|
+
export declare function describePlacementAdvice(baseCtx: PatchQueueContext, diff: string, projectedIssues: readonly PatchLintIssue[], plan: {
|
|
36
|
+
newFilename: string;
|
|
37
|
+
insertionOrder: number;
|
|
38
|
+
}): string[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The advice an export placement refusal appends for new files: "register
|
|
4
|
+
* first" when a new system module only lacks its moz.build line, and
|
|
5
|
+
* adopt-then-split for a real moved-code block, but only after checking
|
|
6
|
+
* that the adopt itself would pass.
|
|
7
|
+
*
|
|
8
|
+
* The advice used to be offered on the moved-code heuristic alone. On one
|
|
9
|
+
* queue it told the operator to adopt six new modules into an existing
|
|
10
|
+
* patch when the only error was the missing registration; the adopt then
|
|
11
|
+
* refused one module for importing later patches, and without it breached
|
|
12
|
+
* the source patch's hard size limit. Split out of export-flow.ts for the
|
|
13
|
+
* line budget.
|
|
14
|
+
*/
|
|
15
|
+
import { detectMovedCodeOverlaps, formatMovedCodeAdvice } from '../core/export-moved-code-shape.js';
|
|
16
|
+
import { countNonBinaryDiffLines, getPatchSizeThresholds, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
17
|
+
import { computeProjectedLintRegressions } from '../core/patch-lint-projection.js';
|
|
18
|
+
import { projectEntryBody } from './patch/entry-projection.js';
|
|
19
|
+
/**
|
|
20
|
+
* Why adopting `diff` into `sourceFilename` would itself be refused: the
|
|
21
|
+
* forward imports the adopted files would give the source patch, and the
|
|
22
|
+
* source patch's hard size limit. Empty when the adopt is clean.
|
|
23
|
+
*
|
|
24
|
+
* @param baseCtx - The current queue
|
|
25
|
+
* @param sourceFilename - The patch the adopt would grow
|
|
26
|
+
* @param diff - The pending export's diff
|
|
27
|
+
*/
|
|
28
|
+
export function findAdoptBlockers(baseCtx, sourceFilename, diff) {
|
|
29
|
+
const source = baseCtx.entries.find((entry) => entry.filename === sourceFilename);
|
|
30
|
+
if (source === undefined)
|
|
31
|
+
return [];
|
|
32
|
+
const blockers = [];
|
|
33
|
+
const pending = projectEntryBody(diff);
|
|
34
|
+
const adopted = {
|
|
35
|
+
...source,
|
|
36
|
+
diff: `${source.diff}\n${diff}`,
|
|
37
|
+
newFiles: new Map([...source.newFiles, ...pending.newFiles]),
|
|
38
|
+
createdFiles: new Set([...source.createdFiles, ...pending.createdFiles]),
|
|
39
|
+
modifiedFileAdditions: new Map([
|
|
40
|
+
...source.modifiedFileAdditions,
|
|
41
|
+
...pending.modifiedFileAdditions,
|
|
42
|
+
]),
|
|
43
|
+
};
|
|
44
|
+
const isError = (issue) => issue.severity === 'error';
|
|
45
|
+
const baseline = lintPatchQueue(baseCtx).filter(isError);
|
|
46
|
+
const projected = lintPatchQueue({
|
|
47
|
+
...baseCtx,
|
|
48
|
+
entries: baseCtx.entries.map((entry) => (entry.filename === sourceFilename ? adopted : entry)),
|
|
49
|
+
}).filter(isError);
|
|
50
|
+
for (const issue of computeProjectedLintRegressions(baseline, projected)) {
|
|
51
|
+
if (issue.check !== 'forward-import' || !issue.patches?.includes(sourceFilename))
|
|
52
|
+
continue;
|
|
53
|
+
for (const declaration of issue.stagedDeclarations ?? []) {
|
|
54
|
+
blockers.push(`${declaration.file} imports "${declaration.specifier ?? ''}", created by ` +
|
|
55
|
+
`${declaration.owner}, which applies after ${sourceFilename} (forward-import)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!(source.metadata?.lintIgnore ?? []).includes('large-patch-lines')) {
|
|
59
|
+
const tier = resolvePatchSizeTier([...(source.metadata?.filesAffected ?? []), ...pending.newFiles.keys()], source.metadata?.tier);
|
|
60
|
+
const limit = getPatchSizeThresholds(tier.tier).lines.error;
|
|
61
|
+
const lines = countNonBinaryDiffLines(source.diff).textLines + countNonBinaryDiffLines(diff).textLines;
|
|
62
|
+
if (lines > limit) {
|
|
63
|
+
blockers.push(`${sourceFilename} would grow to about ${lines} lines, over its ${tier.tier} hard limit ` +
|
|
64
|
+
`of ${limit} (large-patch-lines)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return blockers;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Detail lines for a placement refusal: register-first and moved-code
|
|
71
|
+
* advice, each only when the projection supports it.
|
|
72
|
+
*
|
|
73
|
+
* @param baseCtx - The current queue
|
|
74
|
+
* @param diff - The pending export's diff
|
|
75
|
+
* @param projectedIssues - Error findings of the placement projection
|
|
76
|
+
* @param plan - The placement's new filename and order
|
|
77
|
+
*/
|
|
78
|
+
export function describePlacementAdvice(baseCtx, diff, projectedIssues, plan) {
|
|
79
|
+
const newFiles = projectEntryBody(diff).newFiles;
|
|
80
|
+
const exported = projectedIssues.filter((issue) => issue.patches?.includes(plan.newFilename) ?? issue.message.includes(plan.newFilename));
|
|
81
|
+
const unregisteredModules = [
|
|
82
|
+
...new Set(exported
|
|
83
|
+
.filter((issue) => issue.check === 'unregistered-system-module' && newFiles.has(issue.file))
|
|
84
|
+
.map((issue) => issue.file)),
|
|
85
|
+
];
|
|
86
|
+
const registrationResolvesAll = unregisteredModules.length > 0 &&
|
|
87
|
+
exported.every((issue) => issue.check === 'unregistered-system-module');
|
|
88
|
+
const overlaps = registrationResolvesAll ? [] : detectMovedCodeOverlaps(diff, baseCtx.entries);
|
|
89
|
+
const strongest = overlaps[0];
|
|
90
|
+
return formatMovedCodeAdvice({
|
|
91
|
+
overlaps,
|
|
92
|
+
newPatchName: plan.newFilename,
|
|
93
|
+
insertionOrder: plan.insertionOrder,
|
|
94
|
+
unregisteredModules,
|
|
95
|
+
registrationResolvesAll,
|
|
96
|
+
adoptBlockers: strongest !== undefined ? findAdoptBlockers(baseCtx, strongest.sourcePatch, diff) : [],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* per-file / per-function line budgets and makes each step individually
|
|
8
8
|
* testable without dragging the whole command harness along for the ride.
|
|
9
9
|
*/
|
|
10
|
-
import { join } from 'node:path';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
11
|
import {} from '../core/destructive.js';
|
|
12
|
-
import { detectMovedCodeOverlaps, formatAdoptThenSplitRemedy, } from '../core/export-moved-code-shape.js';
|
|
13
12
|
import { buildPlanExportInput, findAllPatchesForFilesWithDetails, patchNameSlug, planExport, } from '../core/patch-export.js';
|
|
14
13
|
import { buildPatchQueueContext, lintPatchQueue } from '../core/patch-lint.js';
|
|
14
|
+
import { formatLintIgnoreList } from '../core/patch-lint-ignore.js';
|
|
15
15
|
import { withPatchDirectoryLock } from '../core/patch-lock.js';
|
|
16
16
|
import { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
17
17
|
import { formatPatchOrder, requirePatchOrder } from '../core/patch-parse.js';
|
|
@@ -20,9 +20,11 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
|
20
20
|
import { toError } from '../utils/errors.js';
|
|
21
21
|
import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
|
|
22
22
|
import { info, warn } from '../utils/logger.js';
|
|
23
|
+
import { describePlacementAdvice } from './export-adopt-check.js';
|
|
23
24
|
import { groupProjectedPlacementErrors } from './export-placement-conflicts.js';
|
|
24
25
|
import { assertPlacementAvoidsReservedRanges } from './export-placement-policy.js';
|
|
25
26
|
import { findPartialOwnershipOverlap } from './export-shared.js';
|
|
27
|
+
import { acceptTreeRegisteredModules } from './export-staged-registration.js';
|
|
26
28
|
import { projectEntryBody } from './patch/entry-projection.js';
|
|
27
29
|
import { getSortedRenameEntries, renameMapsEqual } from './patch-rename-map.js';
|
|
28
30
|
function buildFilenameForPlacement(category, name, order, width) {
|
|
@@ -170,18 +172,31 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
|
|
|
170
172
|
// whether they live in a brand-new file or are added to an existing one.
|
|
171
173
|
// Leaving either map empty lets a patch land before one of its own
|
|
172
174
|
// dependencies and still pass the gate.
|
|
175
|
+
const pendingBody = projectEntryBody(diff);
|
|
173
176
|
projectedEntries.push({
|
|
174
177
|
filename: plan.newFilename,
|
|
175
178
|
order: plan.insertionOrder,
|
|
176
179
|
metadata: null,
|
|
177
|
-
...
|
|
180
|
+
...pendingBody,
|
|
178
181
|
});
|
|
179
182
|
projectedEntries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
180
183
|
const policyRest = baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
// A new module whose moz.build line is declared (staged registration)
|
|
185
|
+
// and present in the working tree, but not yet exported by its owner, is
|
|
186
|
+
// accepted with a "re-export the owner next" note instead of refused.
|
|
187
|
+
const registration = await acceptTreeRegisteredModules({
|
|
188
|
+
engineDir: join(dirname(patchesDir), 'engine'),
|
|
189
|
+
baseCtx,
|
|
190
|
+
newFilename: plan.newFilename,
|
|
191
|
+
pendingNewFiles: new Set(pendingBody.newFiles.keys()),
|
|
192
|
+
projectedIssues: lintPatchQueue({
|
|
193
|
+
entries: projectedEntries,
|
|
194
|
+
...policyRest,
|
|
195
|
+
}).filter((i) => i.severity === 'error'),
|
|
196
|
+
});
|
|
197
|
+
for (const note of registration.accepted)
|
|
198
|
+
warn(note);
|
|
199
|
+
const projectedIssues = registration.remaining;
|
|
185
200
|
if (projectedIssues.length === 0)
|
|
186
201
|
return null;
|
|
187
202
|
// Baseline lint over the un-renumbered queue: an error present here is
|
|
@@ -192,16 +207,16 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
|
|
|
192
207
|
entries: baseCtx.entries,
|
|
193
208
|
...policyRest,
|
|
194
209
|
}).filter((i) => i.severity === 'error');
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
const movedCode = detectMovedCodeOverlaps(diff, baseCtx.entries);
|
|
210
|
+
// Register-first and new-file + moved-code advice: name the working
|
|
211
|
+
// sequence instead of leaving the operator to derive it from two
|
|
212
|
+
// individually-correct guards that together dead-end, but only offer
|
|
213
|
+
// adopt-then-split for a real moved block whose adopt would itself pass.
|
|
200
214
|
return {
|
|
201
215
|
reason: `placement would introduce ${projectedIssues.length} cross-patch lint error(s)`,
|
|
202
216
|
details: [
|
|
203
217
|
...groupProjectedPlacementErrors(projectedIssues, baselineIssues, plan),
|
|
204
|
-
...
|
|
218
|
+
...registration.advice,
|
|
219
|
+
...describePlacementAdvice(baseCtx, diff, projectedIssues, plan),
|
|
205
220
|
],
|
|
206
221
|
};
|
|
207
222
|
}
|
|
@@ -380,7 +395,7 @@ export async function renderDryRunPreview(input) {
|
|
|
380
395
|
info(` tier: ${plan.metadata.tier}`);
|
|
381
396
|
}
|
|
382
397
|
if (plan.metadata.lintIgnore !== undefined && plan.metadata.lintIgnore.length > 0) {
|
|
383
|
-
info(` lintIgnore: ${plan.metadata.lintIgnore
|
|
398
|
+
info(` lintIgnore: ${formatLintIgnoreList(plan.metadata.lintIgnore)}`);
|
|
384
399
|
}
|
|
385
400
|
if (supersedeDetails.length > 0) {
|
|
386
401
|
info(`\n[dry-run] Would supersede ${supersedeDetails.length} existing patch(es):`);
|