@hominis/fireforge 0.49.2 → 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 +40 -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 +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.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 +1 -1
|
@@ -25,12 +25,14 @@ import { findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports,
|
|
|
25
25
|
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
|
|
26
26
|
import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
|
|
27
27
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
28
|
+
import { evaluatePatchPolicyContentForQueue } from '../core/patch-policy-content.js';
|
|
28
29
|
import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
|
|
29
30
|
import { buildPatchClaims } from '../core/status-classify.js';
|
|
30
31
|
import { classifyFiles } from '../core/status-classify.js';
|
|
31
32
|
import { GeneralError } from '../errors/base.js';
|
|
32
33
|
import { pathExists, readText } from '../utils/fs.js';
|
|
33
34
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
35
|
+
import { anyPatchInScope, isFilenameInScope, resolveVerifyScope, scopeLintIssues, scopePatches, } from './verify-scope.js';
|
|
34
36
|
/**
|
|
35
37
|
* Walks each patch body in the manifest, extracts the set of
|
|
36
38
|
* component-shaped registration references it adds (widget paths
|
|
@@ -41,7 +43,7 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
41
43
|
* registers a file that nothing in the world supplies, which fails at
|
|
42
44
|
* install time.
|
|
43
45
|
*/
|
|
44
|
-
async function detectDanglingRegistrations(patchesDir, engineDir, patches) {
|
|
46
|
+
async function detectDanglingRegistrations(patchesDir, engineDir, patches, scope) {
|
|
45
47
|
// Aggregate the set of all paths any patch in the queue is responsible for
|
|
46
48
|
// (per `filesAffected`). Individual patch bodies are deliberately not
|
|
47
49
|
// parsed for new-file creations here: `filesAffected` is already the
|
|
@@ -54,8 +56,9 @@ async function detectDanglingRegistrations(patchesDir, engineDir, patches) {
|
|
|
54
56
|
coveredByPatches.add(file);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
59
|
+
// Every patch supplies targets; only the scoped ones are judged.
|
|
57
60
|
const issues = [];
|
|
58
|
-
for (const patch of patches) {
|
|
61
|
+
for (const patch of scopePatches(patches, scope)) {
|
|
59
62
|
const patchPath = join(patchesDir, patch.filename);
|
|
60
63
|
if (!(await pathExists(patchPath)))
|
|
61
64
|
continue;
|
|
@@ -100,7 +103,7 @@ function detectCrossPatchFileClaims(manifestPatches) {
|
|
|
100
103
|
}
|
|
101
104
|
return results;
|
|
102
105
|
}
|
|
103
|
-
async function detectWorktreeOwnershipDrift(projectRoot, engineDir, patchesDir, binaryName) {
|
|
106
|
+
async function detectWorktreeOwnershipDrift(projectRoot, engineDir, patchesDir, binaryName, scope) {
|
|
104
107
|
if (!(await pathExists(engineDir)) || !(await isGitRepository(engineDir))) {
|
|
105
108
|
return { unowned: [], patchOwnedDrift: [] };
|
|
106
109
|
}
|
|
@@ -108,24 +111,51 @@ async function detectWorktreeOwnershipDrift(projectRoot, engineDir, patchesDir,
|
|
|
108
111
|
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
109
112
|
const classified = await classifyFiles(entries, engineDir, patchesDir, binaryName, furnacePrefixes);
|
|
110
113
|
return {
|
|
114
|
+
// An unowned change belongs to no patch, so no --patches scope covers it.
|
|
111
115
|
unowned: [
|
|
112
116
|
...new Set(classified
|
|
113
|
-
.filter((entry) => entry.classification === 'unmanaged')
|
|
117
|
+
.filter((entry) => scope === undefined && entry.classification === 'unmanaged')
|
|
114
118
|
.map((entry) => entry.file)),
|
|
115
119
|
].sort(),
|
|
116
120
|
patchOwnedDrift: [
|
|
117
121
|
...new Set(classified
|
|
118
|
-
.filter((entry) => entry.classification === 'patch-owned-drift'
|
|
122
|
+
.filter((entry) => entry.classification === 'patch-owned-drift' &&
|
|
123
|
+
anyPatchInScope(scope, entry.owner === undefined ? [] : [entry.owner]))
|
|
119
124
|
.map((entry) => entry.file)),
|
|
120
125
|
].sort(),
|
|
121
126
|
};
|
|
122
127
|
}
|
|
128
|
+
/** The warning groups for unowned changes and patch-owned drift. */
|
|
129
|
+
function buildWorktreeDriftGroups(drift) {
|
|
130
|
+
const groups = [];
|
|
131
|
+
if (drift.unowned.length > 0) {
|
|
132
|
+
groups.push({
|
|
133
|
+
title: `Unowned worktree changes (${drift.unowned.length})`,
|
|
134
|
+
issues: drift.unowned.map((file) => `${file} is changed in engine/ but is not listed in any patch filesAffected entry`),
|
|
135
|
+
errorCount: 0,
|
|
136
|
+
warningCount: drift.unowned.length,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (drift.patchOwnedDrift.length > 0) {
|
|
140
|
+
groups.push({
|
|
141
|
+
title: `Patch-owned worktree drift (${drift.patchOwnedDrift.length})`,
|
|
142
|
+
issues: drift.patchOwnedDrift.map((file) => `${file} is claimed by exactly one patch, but engine/ no longer matches that patch output`),
|
|
143
|
+
errorCount: 0,
|
|
144
|
+
warningCount: drift.patchOwnedDrift.length,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return groups;
|
|
148
|
+
}
|
|
123
149
|
/**
|
|
124
150
|
* Collects the same queue-health findings reported by `fireforge verify`
|
|
125
151
|
* without printing. Used by doctor recovery paths that need a read-only
|
|
126
152
|
* "is this queue healthy?" decision before clearing stale state.
|
|
153
|
+
*
|
|
154
|
+
* @param projectRoot - Project root directory
|
|
155
|
+
* @param scope - `verify --patches`: keep only the findings that name these
|
|
156
|
+
* patches (see verify-scope.ts). Omitted, every finding counts.
|
|
127
157
|
*/
|
|
128
|
-
export async function collectPatchQueueHealth(projectRoot) {
|
|
158
|
+
export async function collectPatchQueueHealth(projectRoot, scope) {
|
|
129
159
|
const paths = getProjectPaths(projectRoot);
|
|
130
160
|
const config = await loadConfig(projectRoot);
|
|
131
161
|
if (!(await pathExists(paths.patches))) {
|
|
@@ -139,7 +169,7 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
139
169
|
const groups = [];
|
|
140
170
|
let errorCount = 0;
|
|
141
171
|
let warningCount = 0;
|
|
142
|
-
const consistencyIssues = await validatePatchesManifestConsistency(paths.patches);
|
|
172
|
+
const consistencyIssues = (await validatePatchesManifestConsistency(paths.patches)).filter((issue) => isFilenameInScope(scope, issue.filename));
|
|
143
173
|
if (consistencyIssues.length > 0) {
|
|
144
174
|
const issues = consistencyIssues.map((issue) => `[${issue.code}] ${issue.message}`);
|
|
145
175
|
groups.push({
|
|
@@ -152,7 +182,10 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
152
182
|
}
|
|
153
183
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
154
184
|
if (manifest) {
|
|
155
|
-
const policyIssues =
|
|
185
|
+
const policyIssues = [
|
|
186
|
+
...evaluatePatchPolicy(config, manifest),
|
|
187
|
+
...(await evaluatePatchPolicyContentForQueue(config, projectRoot, paths.patches, manifest)),
|
|
188
|
+
].filter((issue) => isFilenameInScope(scope, issue.filename));
|
|
156
189
|
if (policyIssues.length > 0) {
|
|
157
190
|
const policyErrors = policyIssues.filter((issue) => issue.severity === 'error').length;
|
|
158
191
|
const policyWarnings = policyIssues.length - policyErrors;
|
|
@@ -174,7 +207,7 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
174
207
|
// to an existing module is named as text instead of surfacing later as a
|
|
175
208
|
// bare `xpcshell return code: -11` with zero output.
|
|
176
209
|
if (await pathExists(paths.engine)) {
|
|
177
|
-
const unresolved = await findUnresolvedSystemModuleImports(paths.engine, manifest.patches.flatMap((patch) => patch.filesAffected));
|
|
210
|
+
const unresolved = await findUnresolvedSystemModuleImports(paths.engine, scopePatches(manifest.patches, scope).flatMap((patch) => patch.filesAffected));
|
|
178
211
|
if (unresolved.length > 0) {
|
|
179
212
|
groups.push({
|
|
180
213
|
title: `Unresolvable system-module imports (${unresolved.length})`,
|
|
@@ -185,7 +218,7 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
185
218
|
errorCount += unresolved.length;
|
|
186
219
|
}
|
|
187
220
|
}
|
|
188
|
-
const crossClaims = detectCrossPatchFileClaims(manifest.patches);
|
|
221
|
+
const crossClaims = detectCrossPatchFileClaims(manifest.patches).filter((claim) => anyPatchInScope(scope, claim.filenames));
|
|
189
222
|
if (crossClaims.length > 0) {
|
|
190
223
|
groups.push({
|
|
191
224
|
title: `Cross-patch filesAffected conflicts (${crossClaims.length})`,
|
|
@@ -197,7 +230,7 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
197
230
|
}
|
|
198
231
|
}
|
|
199
232
|
const ctx = await buildPatchQueueContext(paths.patches);
|
|
200
|
-
const lintIssues = lintPatchQueue(ctx);
|
|
233
|
+
const lintIssues = scopeLintIssues(lintPatchQueue(ctx), scope);
|
|
201
234
|
if (lintIssues.length > 0) {
|
|
202
235
|
const lintErrors = lintIssues.filter((issue) => issue.severity === 'error').length;
|
|
203
236
|
const lintWarnings = lintIssues.filter((issue) => issue.severity === 'warning').length;
|
|
@@ -214,26 +247,11 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
214
247
|
warningCount += lintWarnings;
|
|
215
248
|
}
|
|
216
249
|
if (manifest) {
|
|
217
|
-
const worktreeDrift = await detectWorktreeOwnershipDrift(projectRoot, paths.engine, paths.patches, config.binaryName);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
errorCount: 0,
|
|
223
|
-
warningCount: worktreeDrift.unowned.length,
|
|
224
|
-
});
|
|
225
|
-
warningCount += worktreeDrift.unowned.length;
|
|
226
|
-
}
|
|
227
|
-
if (worktreeDrift.patchOwnedDrift.length > 0) {
|
|
228
|
-
groups.push({
|
|
229
|
-
title: `Patch-owned worktree drift (${worktreeDrift.patchOwnedDrift.length})`,
|
|
230
|
-
issues: worktreeDrift.patchOwnedDrift.map((file) => `${file} is claimed by exactly one patch, but engine/ no longer matches that patch output`),
|
|
231
|
-
errorCount: 0,
|
|
232
|
-
warningCount: worktreeDrift.patchOwnedDrift.length,
|
|
233
|
-
});
|
|
234
|
-
warningCount += worktreeDrift.patchOwnedDrift.length;
|
|
235
|
-
}
|
|
236
|
-
const registrationIssues = await detectDanglingRegistrations(paths.patches, paths.engine, manifest.patches);
|
|
250
|
+
const worktreeDrift = await detectWorktreeOwnershipDrift(projectRoot, paths.engine, paths.patches, config.binaryName, scope);
|
|
251
|
+
const driftGroups = buildWorktreeDriftGroups(worktreeDrift);
|
|
252
|
+
groups.push(...driftGroups);
|
|
253
|
+
warningCount += driftGroups.reduce((sum, group) => sum + group.warningCount, 0);
|
|
254
|
+
const registrationIssues = await detectDanglingRegistrations(paths.patches, paths.engine, manifest.patches, scope);
|
|
237
255
|
if (registrationIssues.length > 0) {
|
|
238
256
|
groups.push({
|
|
239
257
|
title: `Dangling registration references (${registrationIssues.length})`,
|
|
@@ -256,15 +274,20 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
256
274
|
* Read-only. Exits non-zero on any error-severity finding.
|
|
257
275
|
*
|
|
258
276
|
* @param projectRoot - Project root directory
|
|
277
|
+
* @param options - `patches`: the raw `--patches` values
|
|
259
278
|
*/
|
|
260
|
-
export async function verifyCommand(projectRoot) {
|
|
279
|
+
export async function verifyCommand(projectRoot, options = {}) {
|
|
261
280
|
intro('FireForge Verify');
|
|
281
|
+
const scope = await resolveVerifyScope(getProjectPaths(projectRoot).patches, options.patches);
|
|
282
|
+
if (scope !== undefined) {
|
|
283
|
+
info(`Judging only: ${[...scope.patches].join(', ')}`);
|
|
284
|
+
}
|
|
262
285
|
// `verify` is read-only to the OPERATOR but not to git: the working-tree
|
|
263
286
|
// status probe refreshes (and rewrites) `engine/.git/index`, making this
|
|
264
287
|
// command a second writer of the primary checkout and invalidating a
|
|
265
288
|
// concurrent `fireforge test`'s engine fingerprint. A private index keeps
|
|
266
289
|
// every refresh off the primary checkout.
|
|
267
|
-
const health = await withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => collectPatchQueueHealth(projectRoot));
|
|
290
|
+
const health = await withPrivateGitIndex(getProjectPaths(projectRoot).engine, () => collectPatchQueueHealth(projectRoot, scope));
|
|
268
291
|
if (!health.hasPatchesDirectory) {
|
|
269
292
|
info('No patches directory. Nothing to verify.');
|
|
270
293
|
outro('Verify clean');
|
|
@@ -298,7 +321,8 @@ export function registerVerify(program, { getProjectRoot, withErrorHandling }) {
|
|
|
298
321
|
program
|
|
299
322
|
.command('verify')
|
|
300
323
|
.description('Read-only fsck for the patch queue (manifest + cross-patch lint)')
|
|
301
|
-
.
|
|
302
|
-
|
|
324
|
+
.option('--patches <names...>', 'Judge only findings that name these patches (filename, stem, order number, manifest name, or slug; repeat or comma-separate). Queue-wide findings stay; unowned worktree changes drop out')
|
|
325
|
+
.action(withErrorHandling(async (options) => {
|
|
326
|
+
await verifyCommand(getProjectRoot(), options);
|
|
303
327
|
}));
|
|
304
328
|
}
|
|
@@ -95,8 +95,28 @@ export interface BuildBaseline {
|
|
|
95
95
|
* replaces coverage with a dropped-path notice, or refuses when the caller
|
|
96
96
|
* required --extend-coverage. Legacy packageable-only anchors cannot retain
|
|
97
97
|
* coverage because they cannot prove test scripts and fixtures unchanged.
|
|
98
|
+
*
|
|
99
|
+
* Since 0.50.0 retention is decided per staged path: see
|
|
100
|
+
* {@link BuildBaseline.testCoverageRecords} and
|
|
101
|
+
* {@link BuildBaseline.fullCoverageAnchor}.
|
|
98
102
|
*/
|
|
99
103
|
testPackagingCoverage?: TestPackagingCoverage;
|
|
104
|
+
/**
|
|
105
|
+
* Per-path anchors of a scoped claim, keyed by the request path in
|
|
106
|
+
* {@link BuildBaseline.testPackagingCoverage}. Each record holds that
|
|
107
|
+
* path's own staging inputs and their content at the build that staged
|
|
108
|
+
* it, so a later scoped build keeps the path while those inputs are
|
|
109
|
+
* unchanged, whatever moved elsewhere in the engine. A listed path without
|
|
110
|
+
* a record falls back to the baseline-wide anchor. Absent on full claims.
|
|
111
|
+
*/
|
|
112
|
+
testCoverageRecords?: Record<string, CoverageRecord>;
|
|
113
|
+
/**
|
|
114
|
+
* The anchor of the last full-coverage build, carried forward by scoped
|
|
115
|
+
* builds once something outside their scope moved. A request path outside
|
|
116
|
+
* the scoped list stays covered while its own staging inputs are unchanged
|
|
117
|
+
* against this anchor. The check runs at dispatch, per requested path.
|
|
118
|
+
*/
|
|
119
|
+
fullCoverageAnchor?: CoverageAnchor;
|
|
100
120
|
/**
|
|
101
121
|
* Hex-encoded SHA-256 of `engine/mozconfig` as it stood for this build.
|
|
102
122
|
* Recorded because the mozconfig is regenerated from project-side
|
|
@@ -141,5 +161,31 @@ export interface StaticComponentsBaseline {
|
|
|
141
161
|
*/
|
|
142
162
|
fingerprints: Record<string, string>;
|
|
143
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Content anchor of one packaging claim. A path's staged content is
|
|
166
|
+
* `fingerprints[path]` when recorded, otherwise its content at
|
|
167
|
+
* `engineHeadSha`.
|
|
168
|
+
*/
|
|
169
|
+
export interface CoverageAnchor {
|
|
170
|
+
/** Engine HEAD the staged content is relative to. */
|
|
171
|
+
engineHeadSha: string;
|
|
172
|
+
/** Hex SHA-256 of `engine/mozconfig` the claim was configured with. */
|
|
173
|
+
mozconfigHash: string;
|
|
174
|
+
/**
|
|
175
|
+
* Hex SHA-256 (or {@link DELETED_FILE_FINGERPRINT}) per engine-relative
|
|
176
|
+
* path whose staged content differs from `engineHeadSha`.
|
|
177
|
+
*/
|
|
178
|
+
fingerprints: Record<string, string>;
|
|
179
|
+
}
|
|
180
|
+
/** Anchor of one staged request path. See {@link BuildBaseline.testCoverageRecords}. */
|
|
181
|
+
export interface CoverageRecord extends CoverageAnchor {
|
|
182
|
+
/**
|
|
183
|
+
* Engine-relative paths (files or directories) whose content decides what
|
|
184
|
+
* the harness stages for the request path: its manifest directory plus
|
|
185
|
+
* whatever a manifest there names outside it. `fingerprints` is
|
|
186
|
+
* restricted to these.
|
|
187
|
+
*/
|
|
188
|
+
inputs: string[];
|
|
189
|
+
}
|
|
144
190
|
/** Coverage claim of the packaged test runtime. See {@link BuildBaseline.testPackagingCoverage}. */
|
|
145
191
|
export type TestPackagingCoverage = 'full' | string[];
|
|
@@ -18,6 +18,20 @@ function isFingerprintMap(value) {
|
|
|
18
18
|
return (isObject(value) &&
|
|
19
19
|
Object.entries(value).every(([path, fingerprint]) => isBaselinePath(path) && isFingerprint(fingerprint)));
|
|
20
20
|
}
|
|
21
|
+
function isCoverageAnchor(value) {
|
|
22
|
+
return (isObject(value) &&
|
|
23
|
+
typeof value['engineHeadSha'] === 'string' &&
|
|
24
|
+
isFingerprint(value['mozconfigHash']) &&
|
|
25
|
+
isFingerprintMap(value['fingerprints']));
|
|
26
|
+
}
|
|
27
|
+
function isCoverageRecordMap(value) {
|
|
28
|
+
return (isObject(value) &&
|
|
29
|
+
Object.entries(value).every(([path, record]) => isBaselinePath(path) &&
|
|
30
|
+
isCoverageAnchor(record) &&
|
|
31
|
+
isObject(record) &&
|
|
32
|
+
Array.isArray(record['inputs']) &&
|
|
33
|
+
record['inputs'].every(isBaselinePath)));
|
|
34
|
+
}
|
|
21
35
|
/** Validates persisted fields without converting malformed coverage into a full claim. */
|
|
22
36
|
export function isBuildBaseline(value) {
|
|
23
37
|
if (!isObject(value))
|
|
@@ -45,6 +59,11 @@ export function isBuildBaseline(value) {
|
|
|
45
59
|
return false;
|
|
46
60
|
if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string')
|
|
47
61
|
return false;
|
|
62
|
+
if (value['testCoverageRecords'] !== undefined &&
|
|
63
|
+
!isCoverageRecordMap(value['testCoverageRecords']))
|
|
64
|
+
return false;
|
|
65
|
+
if (value['fullCoverageAnchor'] !== undefined && !isCoverageAnchor(value['fullCoverageAnchor']))
|
|
66
|
+
return false;
|
|
48
67
|
const components = value['staticComponentsBaseline'];
|
|
49
68
|
return (components === undefined ||
|
|
50
69
|
(isObject(components) &&
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* on successful build completion. A failed build does not update it, so a
|
|
16
16
|
* subsequent run still audits against the last known-good tree.
|
|
17
17
|
*/
|
|
18
|
-
import { type BuildBaseline, type TestPackagingCoverage } from './build-baseline-types.js';
|
|
18
|
+
import { type BuildBaseline, type CoverageAnchor, type CoverageRecord, type TestPackagingCoverage } from './build-baseline-types.js';
|
|
19
19
|
/**
|
|
20
20
|
* Which mach build produced the baseline being written. Decides whether
|
|
21
21
|
* the `jar.mn` half of {@link BuildBaseline.buildInputFingerprints} is
|
|
@@ -52,6 +52,10 @@ export interface WriteBuildBaselineOptions {
|
|
|
52
52
|
* Omitted → field left off the marker.
|
|
53
53
|
*/
|
|
54
54
|
testPackagingCoverage?: TestPackagingCoverage | undefined;
|
|
55
|
+
/** Per-path anchors of a scoped claim. Omitted → field left off the marker. */
|
|
56
|
+
testCoverageRecords?: Record<string, CoverageRecord> | undefined;
|
|
57
|
+
/** Carried full-build anchor of a narrowed claim. Omitted → field left off. */
|
|
58
|
+
fullCoverageAnchor?: CoverageAnchor | undefined;
|
|
55
59
|
/**
|
|
56
60
|
* The baseline this write replaces, when the caller has it. A scoped
|
|
57
61
|
* (non-`'full'`) write carries its `staticComponentsBaseline` forward
|
|
@@ -81,7 +81,7 @@ export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
|
|
|
81
81
|
* @param options - See {@link WriteBuildBaselineOptions}
|
|
82
82
|
*/
|
|
83
83
|
export async function writeBuildBaseline(options) {
|
|
84
|
-
const { projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', } = options;
|
|
84
|
+
const { projectRoot, engineDir, binaryName, testPackagingCoverage, testCoverageRecords, fullCoverageAnchor, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full', } = options;
|
|
85
85
|
let engineHeadSha = '';
|
|
86
86
|
try {
|
|
87
87
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -116,6 +116,8 @@ export async function writeBuildBaseline(options) {
|
|
|
116
116
|
...(testInputFingerprints !== undefined ? { testInputFingerprints } : {}),
|
|
117
117
|
...(buildInputFingerprints !== undefined ? { buildInputFingerprints } : {}),
|
|
118
118
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
119
|
+
...(testCoverageRecords !== undefined ? { testCoverageRecords } : {}),
|
|
120
|
+
...(fullCoverageAnchor !== undefined ? { fullCoverageAnchor } : {}),
|
|
119
121
|
...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
|
|
120
122
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
121
123
|
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
@@ -14,8 +14,17 @@
|
|
|
14
14
|
* whose content is explained by neither a patch body nor the pristine
|
|
15
15
|
* baseline. Advisory by default (a warning naming every file), refusing
|
|
16
16
|
* under an explicit flag.
|
|
17
|
+
*
|
|
18
|
+
* A deployed Furnace file is checked against its component source as well
|
|
19
|
+
* (`deployed-unexported`): when the engine copy equals the Furnace-composed
|
|
20
|
+
* current source, differs from it only inside generated fragment
|
|
21
|
+
* expansions, or is exactly Furnace's last recorded deployment, the
|
|
22
|
+
* overwrite destroys nothing that is not a component source, so it is safe
|
|
23
|
+
* even though no patch records it yet. Without this, peers' deployed but
|
|
24
|
+
* unexported widgets refused every belted build on a shared checkout.
|
|
17
25
|
*/
|
|
18
26
|
import type { FireForgeConfig } from '../types/config.js';
|
|
27
|
+
import { type DeployedSourceMatch } from './furnace-composed-source.js';
|
|
19
28
|
import { type ClassifiedFile } from './status-classify.js';
|
|
20
29
|
/** One file whose unexported content a build-prepare overwrite would destroy. */
|
|
21
30
|
export interface UnexportedDriftAtRisk {
|
|
@@ -23,6 +32,24 @@ export interface UnexportedDriftAtRisk {
|
|
|
23
32
|
classification: ClassifiedFile['classification'];
|
|
24
33
|
owner?: string | undefined;
|
|
25
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A deployed Furnace file no patch records yet whose content is its
|
|
37
|
+
* component source's (see {@link DeployedSourceMatch}), so the overwrite is
|
|
38
|
+
* safe. Its patch still owes a re-export.
|
|
39
|
+
*/
|
|
40
|
+
export interface DeployedUnexportedFile {
|
|
41
|
+
file: string;
|
|
42
|
+
classification: 'deployed-unexported';
|
|
43
|
+
match: DeployedSourceMatch;
|
|
44
|
+
owner?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Outcome of {@link assessUnexportedDrift}. */
|
|
47
|
+
export interface UnexportedDriftAssessment {
|
|
48
|
+
/** Files whose content the overwrite would destroy unrecorded. */
|
|
49
|
+
atRisk: UnexportedDriftAtRisk[];
|
|
50
|
+
/** Files the overwrite rewrites from their own component source. */
|
|
51
|
+
deployedUnexported: DeployedUnexportedFile[];
|
|
52
|
+
}
|
|
26
53
|
/**
|
|
27
54
|
* Finds engine files that build-prepare is about to overwrite and whose
|
|
28
55
|
* current content is recorded nowhere.
|
|
@@ -30,11 +57,34 @@ export interface UnexportedDriftAtRisk {
|
|
|
30
57
|
* Fail-open: any probe failure is reported verbosely and yields an empty
|
|
31
58
|
* list. A build must not fail because an advisory guard could not run.
|
|
32
59
|
*
|
|
60
|
+
* @internal The at-risk half of {@link assessUnexportedDrift}, kept for the
|
|
61
|
+
* guard's tests; build-prepare consumes the full assessment.
|
|
62
|
+
*
|
|
33
63
|
* @param projectRoot - Project root
|
|
34
64
|
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
35
65
|
* @returns At-risk files, sorted by path
|
|
36
66
|
*/
|
|
37
67
|
export declare function findUnexportedDriftAtRisk(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAtRisk[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Classifies the engine files build-prepare is about to overwrite into the
|
|
70
|
+
* ones whose content is recorded nowhere (`atRisk`) and the deployed
|
|
71
|
+
* Furnace files no patch records yet whose content is their component
|
|
72
|
+
* source's (`deployedUnexported`, safe to overwrite).
|
|
73
|
+
*
|
|
74
|
+
* Fail-open like {@link findUnexportedDriftAtRisk}: a probe failure yields
|
|
75
|
+
* an empty assessment.
|
|
76
|
+
*
|
|
77
|
+
* @param projectRoot - Project root
|
|
78
|
+
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
79
|
+
* @returns Both lists, sorted by path
|
|
80
|
+
*/
|
|
81
|
+
export declare function assessUnexportedDrift(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAssessment>;
|
|
82
|
+
/**
|
|
83
|
+
* Renders the informational note for deployed Furnace files the overwrite
|
|
84
|
+
* rewrites from their own component source: safe for this build, but their
|
|
85
|
+
* patches still owe a re-export.
|
|
86
|
+
*/
|
|
87
|
+
export declare function formatDeployedUnexportedNote(files: readonly DeployedUnexportedFile[]): string;
|
|
38
88
|
/**
|
|
39
89
|
* Renders the operator-facing warning (or refusal body) naming every file
|
|
40
90
|
* whose unexported content the build is about to overwrite.
|
|
@@ -15,10 +15,19 @@
|
|
|
15
15
|
* whose content is explained by neither a patch body nor the pristine
|
|
16
16
|
* baseline. Advisory by default (a warning naming every file), refusing
|
|
17
17
|
* under an explicit flag.
|
|
18
|
+
*
|
|
19
|
+
* A deployed Furnace file is checked against its component source as well
|
|
20
|
+
* (`deployed-unexported`): when the engine copy equals the Furnace-composed
|
|
21
|
+
* current source, differs from it only inside generated fragment
|
|
22
|
+
* expansions, or is exactly Furnace's last recorded deployment, the
|
|
23
|
+
* overwrite destroys nothing that is not a component source, so it is safe
|
|
24
|
+
* even though no patch records it yet. Without this, peers' deployed but
|
|
25
|
+
* unexported widgets refused every belted build on a shared checkout.
|
|
18
26
|
*/
|
|
19
27
|
import { toError } from '../utils/errors.js';
|
|
20
28
|
import { verbose } from '../utils/logger.js';
|
|
21
29
|
import { getProjectPaths } from './config.js';
|
|
30
|
+
import { matchDeployedComponentFiles, } from './furnace-composed-source.js';
|
|
22
31
|
import { collectFurnaceManagedPrefixes } from './furnace-config.js';
|
|
23
32
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from './git-status.js';
|
|
24
33
|
import { classifyFiles } from './status-classify.js';
|
|
@@ -98,6 +107,40 @@ async function collectOverwrittenEntries(engineDir, status, binaryName, furnaceP
|
|
|
98
107
|
return expanded.filter((entry) => !entry.file.endsWith('/') &&
|
|
99
108
|
isOverwrittenByBuildPrepare(entry.file, binaryName, furnacePrefixes));
|
|
100
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Splits classified at-risk files into the ones whose engine content is a
|
|
112
|
+
* Furnace deployment of their component source and the rest.
|
|
113
|
+
*
|
|
114
|
+
* Fail-closed: when the source comparison cannot run, every file stays at
|
|
115
|
+
* risk, exactly as before the comparison existed.
|
|
116
|
+
*/
|
|
117
|
+
async function separateDeployedUnexported(projectRoot, engineDir, atRisk) {
|
|
118
|
+
let matches;
|
|
119
|
+
try {
|
|
120
|
+
matches = await matchDeployedComponentFiles(projectRoot, engineDir, atRisk.map((entry) => entry.file));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
verbose(`Unexported-drift guard could not compare component sources: ${toError(error).message}`);
|
|
124
|
+
return { atRisk, deployedUnexported: [] };
|
|
125
|
+
}
|
|
126
|
+
const deployedUnexported = [];
|
|
127
|
+
const remaining = [];
|
|
128
|
+
for (const entry of atRisk) {
|
|
129
|
+
const match = matches.get(entry.file);
|
|
130
|
+
if (match === undefined) {
|
|
131
|
+
remaining.push(entry);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
deployedUnexported.push({
|
|
135
|
+
file: entry.file,
|
|
136
|
+
classification: 'deployed-unexported',
|
|
137
|
+
match,
|
|
138
|
+
owner: entry.owner,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { atRisk: remaining, deployedUnexported };
|
|
143
|
+
}
|
|
101
144
|
/**
|
|
102
145
|
* Finds engine files that build-prepare is about to overwrite and whose
|
|
103
146
|
* current content is recorded nowhere.
|
|
@@ -105,22 +148,42 @@ async function collectOverwrittenEntries(engineDir, status, binaryName, furnaceP
|
|
|
105
148
|
* Fail-open: any probe failure is reported verbosely and yields an empty
|
|
106
149
|
* list. A build must not fail because an advisory guard could not run.
|
|
107
150
|
*
|
|
151
|
+
* @internal The at-risk half of {@link assessUnexportedDrift}, kept for the
|
|
152
|
+
* guard's tests; build-prepare consumes the full assessment.
|
|
153
|
+
*
|
|
108
154
|
* @param projectRoot - Project root
|
|
109
155
|
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
110
156
|
* @returns At-risk files, sorted by path
|
|
111
157
|
*/
|
|
112
158
|
export async function findUnexportedDriftAtRisk(projectRoot, config) {
|
|
159
|
+
return (await assessUnexportedDrift(projectRoot, config)).atRisk;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Classifies the engine files build-prepare is about to overwrite into the
|
|
163
|
+
* ones whose content is recorded nowhere (`atRisk`) and the deployed
|
|
164
|
+
* Furnace files no patch records yet whose content is their component
|
|
165
|
+
* source's (`deployedUnexported`, safe to overwrite).
|
|
166
|
+
*
|
|
167
|
+
* Fail-open like {@link findUnexportedDriftAtRisk}: a probe failure yields
|
|
168
|
+
* an empty assessment.
|
|
169
|
+
*
|
|
170
|
+
* @param projectRoot - Project root
|
|
171
|
+
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
172
|
+
* @returns Both lists, sorted by path
|
|
173
|
+
*/
|
|
174
|
+
export async function assessUnexportedDrift(projectRoot, config) {
|
|
175
|
+
const empty = { atRisk: [], deployedUnexported: [] };
|
|
113
176
|
const paths = getProjectPaths(projectRoot);
|
|
114
177
|
try {
|
|
115
178
|
const status = await getWorkingTreeStatus(paths.engine);
|
|
116
179
|
if (status.length === 0)
|
|
117
|
-
return
|
|
180
|
+
return empty;
|
|
118
181
|
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
119
182
|
const candidates = await collectOverwrittenEntries(paths.engine, status, config.binaryName, furnacePrefixes);
|
|
120
183
|
if (candidates.length === 0)
|
|
121
|
-
return
|
|
184
|
+
return empty;
|
|
122
185
|
const classified = await classifyFiles(candidates, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
|
|
123
|
-
|
|
186
|
+
const atRisk = classified
|
|
124
187
|
.filter((entry) => AT_RISK_CLASSIFICATIONS.has(entry.classification))
|
|
125
188
|
.map((entry) => ({
|
|
126
189
|
file: entry.file,
|
|
@@ -128,12 +191,29 @@ export async function findUnexportedDriftAtRisk(projectRoot, config) {
|
|
|
128
191
|
owner: entry.owner,
|
|
129
192
|
}))
|
|
130
193
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
194
|
+
if (atRisk.length === 0 || furnacePrefixes.size === 0)
|
|
195
|
+
return { ...empty, atRisk };
|
|
196
|
+
return await separateDeployedUnexported(projectRoot, paths.engine, atRisk);
|
|
131
197
|
}
|
|
132
198
|
catch (error) {
|
|
133
199
|
verbose(`Unexported-drift guard could not run: ${toError(error).message}`);
|
|
134
|
-
return
|
|
200
|
+
return empty;
|
|
135
201
|
}
|
|
136
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Renders the informational note for deployed Furnace files the overwrite
|
|
205
|
+
* rewrites from their own component source: safe for this build, but their
|
|
206
|
+
* patches still owe a re-export.
|
|
207
|
+
*/
|
|
208
|
+
export function formatDeployedUnexportedNote(files) {
|
|
209
|
+
const rows = files.map((entry) => {
|
|
210
|
+
const owner = entry.owner !== undefined ? ` (owned by ${entry.owner})` : '';
|
|
211
|
+
return ` ${entry.file} [deployed-unexported: ${entry.match}]${owner}`;
|
|
212
|
+
});
|
|
213
|
+
return (`${files.length} deployed Furnace file(s) differ from their patch body but are a Furnace ` +
|
|
214
|
+
'deployment of their component source, so this overwrite loses nothing; re-export their ' +
|
|
215
|
+
`patches when the component work is done:\n${rows.join('\n')}`);
|
|
216
|
+
}
|
|
137
217
|
/**
|
|
138
218
|
* Renders the operator-facing warning (or refusal body) naming every file
|
|
139
219
|
* whose unexported content the build is about to overwrite.
|
|
@@ -10,6 +10,8 @@ import type { BuildBaseline } from './build-baseline-types.js';
|
|
|
10
10
|
export interface BuildPreparation {
|
|
11
11
|
/** Number of Furnace components applied (0 if none or no furnace.json) */
|
|
12
12
|
furnaceApplied: number;
|
|
13
|
+
/** Names of the Furnace components applied, in apply order. */
|
|
14
|
+
furnaceAppliedNames?: string[];
|
|
13
15
|
/** True when `mach configure` was auto-run to refresh a stale backend. */
|
|
14
16
|
reconfigured: boolean;
|
|
15
17
|
/** A changed packaging manifest cannot be safely handled by `mach build faster`. */
|
|
@@ -35,6 +37,11 @@ export interface PrepareBuildOptions {
|
|
|
35
37
|
* Scripted gates want the stop. An interactive build wants the warning.
|
|
36
38
|
*/
|
|
37
39
|
refuseUnexportedDrift?: boolean | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Skip the Furnace apply, so the build packages engine/ as it stands
|
|
42
|
+
* (`fireforge test --build --no-furnace-apply`).
|
|
43
|
+
*/
|
|
44
|
+
skipFurnaceApply?: boolean | undefined;
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
47
|
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
@@ -13,7 +13,7 @@ import { info, notice, spinner, verbose, warn } from '../utils/logger.js';
|
|
|
13
13
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
14
14
|
import { isBuildInputPath, isJarManifestPath } from './build-audit.js';
|
|
15
15
|
import { evaluateJarManifestEscalation, formatJarEscalationNotice, } from './build-jar-escalation.js';
|
|
16
|
-
import {
|
|
16
|
+
import { assessUnexportedDrift, formatDeployedUnexportedNote, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
|
|
17
17
|
import { hashEngineFile } from './coverage-extend.js';
|
|
18
18
|
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
19
19
|
import { applyAllComponents } from './furnace-apply.js';
|
|
@@ -156,7 +156,12 @@ export function isBackendInvalidatingFile(path) {
|
|
|
156
156
|
* @throws GeneralError under `--refuse-unexported-drift`
|
|
157
157
|
*/
|
|
158
158
|
async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options) {
|
|
159
|
-
const atRisk = await
|
|
159
|
+
const { atRisk, deployedUnexported } = await assessUnexportedDrift(projectRoot, config);
|
|
160
|
+
// Deployed Furnace files that are their component source's own
|
|
161
|
+
// deployment never block: the overwrite loses nothing. Say so, since
|
|
162
|
+
// their patches still owe a re-export.
|
|
163
|
+
if (deployedUnexported.length > 0)
|
|
164
|
+
info(formatDeployedUnexportedNote(deployedUnexported));
|
|
160
165
|
if (atRisk.length === 0)
|
|
161
166
|
return;
|
|
162
167
|
const message = formatUnexportedDriftWarning(atRisk);
|
|
@@ -170,15 +175,15 @@ async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options
|
|
|
170
175
|
* Prints every warning the apply computed, then the loud sync banner.
|
|
171
176
|
*
|
|
172
177
|
* @param projectRoot - Project root
|
|
173
|
-
* @returns
|
|
178
|
+
* @returns Names of the components written to engine/
|
|
174
179
|
*/
|
|
175
180
|
async function applyFurnaceBeforeBuild(projectRoot) {
|
|
176
181
|
if (!(await furnaceConfigExists(projectRoot)))
|
|
177
|
-
return
|
|
182
|
+
return [];
|
|
178
183
|
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
179
184
|
const hasComponents = Object.keys(furnaceConfig.overrides).length > 0 || Object.keys(furnaceConfig.custom).length > 0;
|
|
180
185
|
if (!hasComponents)
|
|
181
|
-
return
|
|
186
|
+
return [];
|
|
182
187
|
const furnaceSpinner = spinner('Applying Furnace components...');
|
|
183
188
|
let result;
|
|
184
189
|
try {
|
|
@@ -230,7 +235,7 @@ async function applyFurnaceBeforeBuild(projectRoot) {
|
|
|
230
235
|
else {
|
|
231
236
|
furnaceSpinner.stop('Components up to date');
|
|
232
237
|
}
|
|
233
|
-
return
|
|
238
|
+
return result.applied.map((entry) => entry.name);
|
|
234
239
|
}
|
|
235
240
|
/**
|
|
236
241
|
* Runs the auto-configure step (`mach configure`) against the freshly
|
|
@@ -344,7 +349,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
344
349
|
throw error;
|
|
345
350
|
}
|
|
346
351
|
}
|
|
347
|
-
const
|
|
352
|
+
const furnaceAppliedNames = options.skipFurnaceApply === true ? [] : await applyFurnaceBeforeBuild(projectRoot);
|
|
348
353
|
// Generate mozconfig
|
|
349
354
|
const mozconfigSpinner = spinner('Generating mozconfig...');
|
|
350
355
|
let mozconfigWritten;
|
|
@@ -374,7 +379,8 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
374
379
|
reconfigured = true;
|
|
375
380
|
}
|
|
376
381
|
return {
|
|
377
|
-
furnaceApplied,
|
|
382
|
+
furnaceApplied: furnaceAppliedNames.length,
|
|
383
|
+
furnaceAppliedNames,
|
|
378
384
|
reconfigured,
|
|
379
385
|
fullBuildRequired,
|
|
380
386
|
...(fullBuildReason !== undefined ? { fullBuildReason } : {}),
|