@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,199 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Format-preserving rewrites of a JSON document.
|
|
4
|
+
*
|
|
5
|
+
* `fireforge source set`, `fireforge config` and `furnace init` change a
|
|
6
|
+
* few keys of `fireforge.json`. Re-serializing the whole document with
|
|
7
|
+
* `JSON.stringify` expands every compact object and array the project keeps
|
|
8
|
+
* on one line, so a one-key change becomes a diff across unrelated policy
|
|
9
|
+
* sections. This module diffs the document on disk against the wanted one
|
|
10
|
+
* and edits only the values that differ, in place, keeping every other byte
|
|
11
|
+
* (indentation, compact containers, key order, trailing newline).
|
|
12
|
+
*
|
|
13
|
+
* Inserted values follow the style of the container they land in: one line
|
|
14
|
+
* inside a one-line container, the container's own indentation otherwise.
|
|
15
|
+
*/
|
|
16
|
+
import { applyEdits, findNodeAtLocation, modify, parseTree } from 'jsonc-parser';
|
|
17
|
+
import { isJsonObject } from '../utils/validation.js';
|
|
18
|
+
/** The same reviver `readJson` uses, so both sides of the diff agree. */
|
|
19
|
+
function parseJsonDocument(text) {
|
|
20
|
+
return JSON.parse(text, (key, value) => key === '__proto__' || key === 'constructor' || key === 'prototype' ? undefined : value);
|
|
21
|
+
}
|
|
22
|
+
function jsonEqual(a, b) {
|
|
23
|
+
if (a === b)
|
|
24
|
+
return true;
|
|
25
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
26
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
27
|
+
return false;
|
|
28
|
+
return a.every((value, index) => jsonEqual(value, b[index]));
|
|
29
|
+
}
|
|
30
|
+
if (!isJsonObject(a) || !isJsonObject(b))
|
|
31
|
+
return false;
|
|
32
|
+
const keysA = definedKeys(a);
|
|
33
|
+
const keysB = definedKeys(b);
|
|
34
|
+
return keysA.length === keysB.length && keysA.every((key) => jsonEqual(a[key], b[key]));
|
|
35
|
+
}
|
|
36
|
+
/** Keys whose value survives `JSON.stringify` (an `undefined` value is an absent key). */
|
|
37
|
+
function definedKeys(object) {
|
|
38
|
+
return Object.keys(object).filter((key) => object[key] !== undefined);
|
|
39
|
+
}
|
|
40
|
+
function collectChanges(path, before, after, changes) {
|
|
41
|
+
if (jsonEqual(before, after))
|
|
42
|
+
return;
|
|
43
|
+
if (isJsonObject(before) && isJsonObject(after)) {
|
|
44
|
+
for (const key of definedKeys(before)) {
|
|
45
|
+
if (after[key] === undefined)
|
|
46
|
+
changes.push({ kind: 'remove', path: [...path, key] });
|
|
47
|
+
}
|
|
48
|
+
for (const key of definedKeys(after)) {
|
|
49
|
+
const next = after[key];
|
|
50
|
+
const previous = before[key];
|
|
51
|
+
if (previous === undefined) {
|
|
52
|
+
changes.push({ kind: 'set', path: [...path, key], value: next });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
collectChanges([...path, key], previous, next, changes);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
61
|
+
const isAppend = after.length > before.length &&
|
|
62
|
+
before.every((value, index) => jsonEqual(value, after[index]));
|
|
63
|
+
if (isAppend) {
|
|
64
|
+
for (const value of after.slice(before.length))
|
|
65
|
+
changes.push({ kind: 'append', path, value });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (after.length === before.length) {
|
|
69
|
+
before.forEach((value, index) => {
|
|
70
|
+
collectChanges([...path, index], value, after[index], changes);
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
changes.push({ kind: 'set', path, value: after });
|
|
76
|
+
}
|
|
77
|
+
function detectStyle(text) {
|
|
78
|
+
const indent = /^([ \t]+)\S/m.exec(text)?.[1];
|
|
79
|
+
return {
|
|
80
|
+
eol: text.includes('\r\n') ? '\r\n' : '\n',
|
|
81
|
+
indentUnit: indent === undefined ? ' ' : indent.startsWith('\t') ? '\t' : indent,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isMultiline(text, node) {
|
|
85
|
+
return node !== undefined && text.slice(node.offset, node.offset + node.length).includes('\n');
|
|
86
|
+
}
|
|
87
|
+
/** The object or array holding `node` (skipping the property node of an object member). */
|
|
88
|
+
function containerOf(node) {
|
|
89
|
+
const parent = node.parent;
|
|
90
|
+
return parent?.type === 'property' ? parent.parent : parent;
|
|
91
|
+
}
|
|
92
|
+
/** Leading whitespace of the line `offset` sits on. */
|
|
93
|
+
function lineIndent(text, offset) {
|
|
94
|
+
const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
|
|
95
|
+
return /^[ \t]*/.exec(text.slice(lineStart))?.[0] ?? '';
|
|
96
|
+
}
|
|
97
|
+
function renderCompact(value) {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
return `[${value.map(renderCompact).join(', ')}]`;
|
|
100
|
+
}
|
|
101
|
+
if (isJsonObject(value)) {
|
|
102
|
+
const entries = definedKeys(value).map((key) => `${JSON.stringify(key)}: ${renderCompact(value[key])}`);
|
|
103
|
+
return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`;
|
|
104
|
+
}
|
|
105
|
+
return JSON.stringify(value);
|
|
106
|
+
}
|
|
107
|
+
function renderValue(value, multiline, indent, style) {
|
|
108
|
+
if (!multiline)
|
|
109
|
+
return renderCompact(value);
|
|
110
|
+
return JSON.stringify(value, null, style.indentUnit)
|
|
111
|
+
.split('\n')
|
|
112
|
+
.join(style.eol + indent);
|
|
113
|
+
}
|
|
114
|
+
function replaceRange(text, offset, length, content) {
|
|
115
|
+
return text.slice(0, offset) + content + text.slice(offset + length);
|
|
116
|
+
}
|
|
117
|
+
/** Replaces an existing value, keeping a container's one-line or multi-line shape. */
|
|
118
|
+
function replaceNode(text, node, value, style) {
|
|
119
|
+
const isContainer = node.type === 'object' || node.type === 'array';
|
|
120
|
+
const multiline = isContainer ? isMultiline(text, node) : isMultiline(text, containerOf(node));
|
|
121
|
+
const rendered = renderValue(value, multiline, lineIndent(text, node.offset), style);
|
|
122
|
+
return replaceRange(text, node.offset, node.length, rendered);
|
|
123
|
+
}
|
|
124
|
+
/** Appends `entry` (a property or an array element) after the container's last child. */
|
|
125
|
+
function appendEntry(text, container, renderEntry, style) {
|
|
126
|
+
const children = container.children ?? [];
|
|
127
|
+
const last = children[children.length - 1];
|
|
128
|
+
const containerIndent = lineIndent(text, container.offset);
|
|
129
|
+
if (last === undefined) {
|
|
130
|
+
// Empty container: open it up the way its parent is laid out.
|
|
131
|
+
const multiline = isMultiline(text, containerOf(container));
|
|
132
|
+
const [open, close] = container.type === 'array' ? ['[', ']'] : ['{', '}'];
|
|
133
|
+
const childIndent = containerIndent + style.indentUnit;
|
|
134
|
+
const body = multiline
|
|
135
|
+
? `${open}${style.eol}${childIndent}${renderEntry(true, childIndent)}${style.eol}${containerIndent}${close}`
|
|
136
|
+
: container.type === 'array'
|
|
137
|
+
? `${open}${renderEntry(false, childIndent)}${close}`
|
|
138
|
+
: `${open} ${renderEntry(false, childIndent)} ${close}`;
|
|
139
|
+
return replaceRange(text, container.offset, container.length, body);
|
|
140
|
+
}
|
|
141
|
+
const multiline = isMultiline(text, container);
|
|
142
|
+
const indent = multiline ? lineIndent(text, last.offset) : containerIndent;
|
|
143
|
+
const separator = multiline ? `,${style.eol}${indent}` : ', ';
|
|
144
|
+
return replaceRange(text, last.offset + last.length, 0, separator + renderEntry(multiline, indent));
|
|
145
|
+
}
|
|
146
|
+
function applyChange(text, change, style) {
|
|
147
|
+
const root = parseTree(text);
|
|
148
|
+
if (root === undefined) {
|
|
149
|
+
throw new Error('document is not valid JSON');
|
|
150
|
+
}
|
|
151
|
+
if (change.kind === 'remove') {
|
|
152
|
+
return applyEdits(text, modify(text, change.path, undefined, {}));
|
|
153
|
+
}
|
|
154
|
+
const target = findNodeAtLocation(root, change.path);
|
|
155
|
+
if (change.kind === 'append') {
|
|
156
|
+
if (target?.type !== 'array')
|
|
157
|
+
throw new Error('append target is not an array');
|
|
158
|
+
return appendEntry(text, target, (multiline, indent) => renderValue(change.value, multiline, indent, style), style);
|
|
159
|
+
}
|
|
160
|
+
if (target !== undefined) {
|
|
161
|
+
return replaceNode(text, target, change.value, style);
|
|
162
|
+
}
|
|
163
|
+
const parent = findNodeAtLocation(root, change.path.slice(0, -1));
|
|
164
|
+
const key = change.path[change.path.length - 1];
|
|
165
|
+
if (parent?.type !== 'object' || typeof key !== 'string') {
|
|
166
|
+
throw new Error('insertion parent is not an object');
|
|
167
|
+
}
|
|
168
|
+
return appendEntry(text, parent, (multiline, indent) => `${JSON.stringify(key)}: ${renderValue(change.value, multiline, indent, style)}`, style);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Rewrites `text` so it parses to `next`, editing only the values that
|
|
172
|
+
* differ. Returns `undefined` when `text` is not a JSON object or the edit
|
|
173
|
+
* cannot be proven equivalent, so the caller can fall back to a full
|
|
174
|
+
* re-serialization.
|
|
175
|
+
*/
|
|
176
|
+
export function rewriteJsonPreservingFormat(text, next) {
|
|
177
|
+
let current;
|
|
178
|
+
try {
|
|
179
|
+
current = parseJsonDocument(text);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
if (!isJsonObject(current))
|
|
185
|
+
return undefined;
|
|
186
|
+
const changes = [];
|
|
187
|
+
collectChanges([], current, next, changes);
|
|
188
|
+
const style = detectStyle(text);
|
|
189
|
+
try {
|
|
190
|
+
let edited = text;
|
|
191
|
+
for (const change of changes)
|
|
192
|
+
edited = applyChange(edited, change, style);
|
|
193
|
+
// The edits are structural; prove the result before trusting it.
|
|
194
|
+
return jsonEqual(parseJsonDocument(edited), next) ? edited : undefined;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
package/dist/src/core/mach.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import { MachNotFoundError } from '../errors/build.js';
|
|
4
4
|
import { pathExists } from '../utils/fs.js';
|
|
5
5
|
import { info, warn } from '../utils/logger.js';
|
|
6
6
|
import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from '../utils/process.js';
|
|
7
|
+
import { withBuildRunRecord } from './build-run-record.js';
|
|
7
8
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
8
9
|
import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
|
|
9
10
|
import { explainMachError } from './mach-error-hints.js';
|
|
@@ -227,6 +228,12 @@ export async function runProtectedMachBuild(kind, engineDir, options = {}) {
|
|
|
227
228
|
if (kind === 'full' && options.jobs !== undefined) {
|
|
228
229
|
args.push('-j', String(options.jobs));
|
|
229
230
|
}
|
|
231
|
+
// The run record lets any session see why a bundle is missing after this
|
|
232
|
+
// build dies part-way (see build-run-record.ts). The engine directory is
|
|
233
|
+
// always `<projectRoot>/engine`.
|
|
234
|
+
return withBuildRunRecord(dirname(engineDir), `mach ${args.join(' ')}`, () => runProtectedMachBuildAttempts(args, kind, engineDir, options));
|
|
235
|
+
}
|
|
236
|
+
async function runProtectedMachBuildAttempts(args, kind, engineDir, options) {
|
|
230
237
|
const maxAttempts = Math.max(1, (options.retries ?? DEFAULT_BUILD_CRASH_RETRIES) + 1);
|
|
231
238
|
for (let attempt = 1;; attempt += 1) {
|
|
232
239
|
const { env } = await installMachResourceGuard(engineDir);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing and strict application of a `fireforge test --negative-control`
|
|
3
|
+
* diff.
|
|
4
|
+
*
|
|
5
|
+
* A control is a reviewed mutation, so the applier is deliberately strict:
|
|
6
|
+
* every context and removed line must match exactly, the only tolerance is
|
|
7
|
+
* a line offset (the same one `git apply` allows, never fuzz), and a hunk
|
|
8
|
+
* that does not apply refuses the whole control before any file is
|
|
9
|
+
* written. Only plain text modifications of existing files are accepted: a
|
|
10
|
+
* created, deleted, renamed or binary file has no byte-exact restore story
|
|
11
|
+
* that is worth its risk for a neuter.
|
|
12
|
+
*/
|
|
13
|
+
import { type ParsedHunk } from './patch-parse.js';
|
|
14
|
+
/** One file of a control diff. */
|
|
15
|
+
export interface ControlDiffFile {
|
|
16
|
+
/** The path the diff names, without git's `a/`/`b/` prefix. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** The hunks to apply, in file order. */
|
|
19
|
+
hunks: ParsedHunk[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parses a control diff. Accepts `git diff` output (a `diff --git` header
|
|
23
|
+
* per file), which is what `git -C engine diff -- <file>` or a project-root
|
|
24
|
+
* `git diff` writes.
|
|
25
|
+
*
|
|
26
|
+
* @param content - The diff file's text
|
|
27
|
+
* @returns One entry per file, in diff order
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseControlDiff(content: string): ControlDiffFile[];
|
|
30
|
+
/**
|
|
31
|
+
* Applies `hunks` to `original`, strictly.
|
|
32
|
+
*
|
|
33
|
+
* @param original - The file's current text
|
|
34
|
+
* @param hunks - Hunks parsed from the control diff
|
|
35
|
+
* @param label - File label for refusal messages
|
|
36
|
+
* @returns The mutated text
|
|
37
|
+
* @throws GeneralError when any hunk does not apply
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyControlHunks(original: string, hunks: readonly ParsedHunk[], label: string): string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Parsing and strict application of a `fireforge test --negative-control`
|
|
4
|
+
* diff.
|
|
5
|
+
*
|
|
6
|
+
* A control is a reviewed mutation, so the applier is deliberately strict:
|
|
7
|
+
* every context and removed line must match exactly, the only tolerance is
|
|
8
|
+
* a line offset (the same one `git apply` allows, never fuzz), and a hunk
|
|
9
|
+
* that does not apply refuses the whole control before any file is
|
|
10
|
+
* written. Only plain text modifications of existing files are accepted: a
|
|
11
|
+
* created, deleted, renamed or binary file has no byte-exact restore story
|
|
12
|
+
* that is worth its risk for a neuter.
|
|
13
|
+
*/
|
|
14
|
+
import { GeneralError } from '../errors/base.js';
|
|
15
|
+
import { isContainedRelativePath, normalizePathSlashes } from '../utils/paths.js';
|
|
16
|
+
import { parseDiffSections } from './patch-parse.js';
|
|
17
|
+
function refuse(message) {
|
|
18
|
+
throw new GeneralError(`Negative control diff refused: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parses a control diff. Accepts `git diff` output (a `diff --git` header
|
|
22
|
+
* per file), which is what `git -C engine diff -- <file>` or a project-root
|
|
23
|
+
* `git diff` writes.
|
|
24
|
+
*
|
|
25
|
+
* @param content - The diff file's text
|
|
26
|
+
* @returns One entry per file, in diff order
|
|
27
|
+
*/
|
|
28
|
+
export function parseControlDiff(content) {
|
|
29
|
+
const sections = parseDiffSections(content);
|
|
30
|
+
if (sections.length === 0) {
|
|
31
|
+
refuse('no `diff --git` sections found. Write the control with git, e.g. ' +
|
|
32
|
+
'`git -C engine diff -- <file> > control.diff` after making the edit, then revert the edit.');
|
|
33
|
+
}
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
return sections.map((section) => {
|
|
36
|
+
const path = normalizePathSlashes(section.targetPath);
|
|
37
|
+
if (section.isBinary)
|
|
38
|
+
refuse(`${path} is a binary section.`);
|
|
39
|
+
if (section.isNewFile || section.isDeletedFile) {
|
|
40
|
+
refuse(`${path} creates or deletes a file; a control may only modify existing text files.`);
|
|
41
|
+
}
|
|
42
|
+
if (section.sourcePath !== section.targetPath) {
|
|
43
|
+
refuse(`${section.sourcePath} -> ${path} is a rename; a control may only modify files.`);
|
|
44
|
+
}
|
|
45
|
+
if (!isContainedRelativePath(path)) {
|
|
46
|
+
refuse(`${path} is not a contained relative path.`);
|
|
47
|
+
}
|
|
48
|
+
if (section.hunks.length === 0)
|
|
49
|
+
refuse(`${path} has no hunks.`);
|
|
50
|
+
if (seen.has(path))
|
|
51
|
+
refuse(`${path} appears in more than one section.`);
|
|
52
|
+
seen.add(path);
|
|
53
|
+
return { path, hunks: section.hunks };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function hunkSides(hunk, label, index) {
|
|
57
|
+
const oldLines = [];
|
|
58
|
+
const newLines = [];
|
|
59
|
+
for (const line of hunk.lines) {
|
|
60
|
+
const body = line.slice(1);
|
|
61
|
+
if (line.startsWith(' ') || line.startsWith('-'))
|
|
62
|
+
oldLines.push(body);
|
|
63
|
+
if (line.startsWith(' ') || line.startsWith('+'))
|
|
64
|
+
newLines.push(body);
|
|
65
|
+
}
|
|
66
|
+
if (oldLines.length !== hunk.oldCount || newLines.length !== hunk.newCount) {
|
|
67
|
+
refuse(`hunk ${index + 1} of ${label} is malformed: its header counts ` +
|
|
68
|
+
`(-${hunk.oldCount} +${hunk.newCount}) do not match its body ` +
|
|
69
|
+
`(-${oldLines.length} +${newLines.length}).`);
|
|
70
|
+
}
|
|
71
|
+
return { oldLines, newLines };
|
|
72
|
+
}
|
|
73
|
+
function matchesAt(lines, block, position) {
|
|
74
|
+
if (position < 0 || position + block.length > lines.length)
|
|
75
|
+
return false;
|
|
76
|
+
return block.every((line, offset) => lines[position + offset] === line);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The position nearest `anchor`, at or after `floor`, where `block`
|
|
80
|
+
* matches, or -1.
|
|
81
|
+
*/
|
|
82
|
+
function findBlock(lines, block, anchor, floor) {
|
|
83
|
+
const start = Math.max(anchor, floor);
|
|
84
|
+
for (let delta = 0; delta <= lines.length; delta += 1) {
|
|
85
|
+
if (matchesAt(lines, block, start + delta))
|
|
86
|
+
return start + delta;
|
|
87
|
+
const below = anchor - delta;
|
|
88
|
+
if (delta > 0 && below >= floor && matchesAt(lines, block, below))
|
|
89
|
+
return below;
|
|
90
|
+
}
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Applies `hunks` to `original`, strictly.
|
|
95
|
+
*
|
|
96
|
+
* @param original - The file's current text
|
|
97
|
+
* @param hunks - Hunks parsed from the control diff
|
|
98
|
+
* @param label - File label for refusal messages
|
|
99
|
+
* @returns The mutated text
|
|
100
|
+
* @throws GeneralError when any hunk does not apply
|
|
101
|
+
*/
|
|
102
|
+
export function applyControlHunks(original, hunks, label) {
|
|
103
|
+
const hasFinalNewline = original.endsWith('\n');
|
|
104
|
+
const lines = original === '' ? [] : (hasFinalNewline ? original.slice(0, -1) : original).split('\n');
|
|
105
|
+
const result = [];
|
|
106
|
+
let cursor = 0;
|
|
107
|
+
let finalNewline = hasFinalNewline;
|
|
108
|
+
hunks.forEach((hunk, index) => {
|
|
109
|
+
const { oldLines, newLines } = hunkSides(hunk, label, index);
|
|
110
|
+
const anchor = hunk.oldCount === 0 ? hunk.oldStart : hunk.oldStart - 1;
|
|
111
|
+
const position = oldLines.length === 0
|
|
112
|
+
? anchor >= cursor && anchor <= lines.length
|
|
113
|
+
? anchor
|
|
114
|
+
: -1
|
|
115
|
+
: findBlock(lines, oldLines, anchor, cursor);
|
|
116
|
+
const reachesEnd = position + oldLines.length === lines.length;
|
|
117
|
+
const endMatches = position >= 0 &&
|
|
118
|
+
(reachesEnd && oldLines.length > 0
|
|
119
|
+
? hunk.noNewlineAtEndOld === !hasFinalNewline
|
|
120
|
+
: !hunk.noNewlineAtEndOld);
|
|
121
|
+
if (position < 0 || !endMatches) {
|
|
122
|
+
const alreadyApplied = newLines.length > 0 && findBlock(lines, newLines, Math.max(0, hunk.newStart - 1), 0) >= 0;
|
|
123
|
+
refuse(`hunk ${index + 1} of ${label} (@@ -${hunk.oldStart},${hunk.oldCount} ` +
|
|
124
|
+
`+${hunk.newStart},${hunk.newCount} @@) does not apply to the current file.` +
|
|
125
|
+
(alreadyApplied
|
|
126
|
+
? ' Its new side is already present: the mutation looks applied already (an earlier control that was not restored?).'
|
|
127
|
+
: ' The file changed since the diff was written; regenerate the control against it.'));
|
|
128
|
+
}
|
|
129
|
+
result.push(...lines.slice(cursor, position), ...newLines);
|
|
130
|
+
cursor = position + oldLines.length;
|
|
131
|
+
if (reachesEnd)
|
|
132
|
+
finalNewline = !hunk.noNewlineAtEndNew;
|
|
133
|
+
});
|
|
134
|
+
result.push(...lines.slice(cursor));
|
|
135
|
+
return result.join('\n') + (finalNewline && result.length > 0 ? '\n' : '');
|
|
136
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge test --negative-control <diff>`: apply a reviewed mutation to
|
|
3
|
+
* engine sources for exactly one harness dispatch, then put every byte back.
|
|
4
|
+
*
|
|
5
|
+
* Safety model (also stated in docs/testing.md):
|
|
6
|
+
*
|
|
7
|
+
* - The control is applied and restored only while the caller holds the
|
|
8
|
+
* engine-session lock, after the pre-test build has packaged and before
|
|
9
|
+
* the harness spawns, so no peer build or test can observe it and a
|
|
10
|
+
* Furnace deploy cannot overwrite it.
|
|
11
|
+
* - Before the first mutated byte is written, the original bytes of every
|
|
12
|
+
* target go to `.fireforge/negative-control/` together with a journal
|
|
13
|
+
* recording each file's original and mutated SHA-256, mode and
|
|
14
|
+
* timestamps. A killed FireForge therefore leaves enough on disk to undo
|
|
15
|
+
* the control, and the next `fireforge test` does so before anything else.
|
|
16
|
+
* - Restore writes the original bytes back atomically, re-applies the
|
|
17
|
+
* original mode and timestamps, and reads the file back to prove the
|
|
18
|
+
* bytes match. It only overwrites a file that still holds exactly the
|
|
19
|
+
* bytes the control wrote: anything else means someone edited the file
|
|
20
|
+
* while the control ran, and that edit is kept, never clobbered, with the
|
|
21
|
+
* original left in the journal directory and the conflict reported.
|
|
22
|
+
* - A SIGINT or SIGTERM restores through {@link restoreActiveNegativeControlsForSignal},
|
|
23
|
+
* which the entry point runs after the harness has shut down and before
|
|
24
|
+
* it releases the held locks.
|
|
25
|
+
*/
|
|
26
|
+
import type { ParsedHunk } from './patch-parse.js';
|
|
27
|
+
/** One file a control mutates. */
|
|
28
|
+
export interface NegativeControlTarget {
|
|
29
|
+
/** The path as the diff names it. */
|
|
30
|
+
diffPath: string;
|
|
31
|
+
/** The engine-relative file the control mutates. */
|
|
32
|
+
enginePath: string;
|
|
33
|
+
/** Hunks to apply to the engine file. */
|
|
34
|
+
hunks: ParsedHunk[];
|
|
35
|
+
/** True when the diff named a `components/` source mapped to its deployed copy. */
|
|
36
|
+
fromComponents: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** A parsed, resolved control, ready to apply. */
|
|
39
|
+
export interface NegativeControlPlan {
|
|
40
|
+
/** The diff file, as the operator passed it. */
|
|
41
|
+
diffFile: string;
|
|
42
|
+
targets: NegativeControlTarget[];
|
|
43
|
+
}
|
|
44
|
+
/** Locations a control works against. */
|
|
45
|
+
export interface NegativeControlPaths {
|
|
46
|
+
engineDir: string;
|
|
47
|
+
/** The project's `.fireforge/` directory. */
|
|
48
|
+
fireforgeDir: string;
|
|
49
|
+
}
|
|
50
|
+
interface JournalEntry {
|
|
51
|
+
enginePath: string;
|
|
52
|
+
backup: string;
|
|
53
|
+
originalSha256: string;
|
|
54
|
+
mutatedSha256: string;
|
|
55
|
+
mode: number;
|
|
56
|
+
atimeMs: number;
|
|
57
|
+
mtimeMs: number;
|
|
58
|
+
}
|
|
59
|
+
interface Journal {
|
|
60
|
+
version: 1;
|
|
61
|
+
diffFile: string;
|
|
62
|
+
appliedAt: string;
|
|
63
|
+
entries: JournalEntry[];
|
|
64
|
+
}
|
|
65
|
+
/** A control currently applied to the engine. */
|
|
66
|
+
export interface ActiveNegativeControl {
|
|
67
|
+
paths: NegativeControlPaths;
|
|
68
|
+
journal: Journal;
|
|
69
|
+
/**
|
|
70
|
+
* The one restore in flight, set synchronously by whichever path (the
|
|
71
|
+
* run's own `finally`, or the signal handler) gets there first, so the
|
|
72
|
+
* other awaits the same restore instead of racing or skipping it.
|
|
73
|
+
*/
|
|
74
|
+
restoring?: Promise<NegativeControlRestoreResult>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Reads, parses and resolves a control diff, and (unless `deferApplyCheck`)
|
|
78
|
+
* proves every hunk applies to the current engine file, so a broken control
|
|
79
|
+
* is refused before any build time is spent.
|
|
80
|
+
*
|
|
81
|
+
* `deferApplyCheck` is for a run that builds first: Furnace redeploys a
|
|
82
|
+
* component's engine copy during the build, so a `components/` control can
|
|
83
|
+
* only be checked against what the build left behind.
|
|
84
|
+
*
|
|
85
|
+
* @param projectRoot - Project root; the diff path resolves against it
|
|
86
|
+
* @param engineDir - Absolute engine directory
|
|
87
|
+
* @param diffFile - The `--negative-control` argument
|
|
88
|
+
* @param options - `deferApplyCheck` skips the up-front apply check for
|
|
89
|
+
* `components/` targets
|
|
90
|
+
*/
|
|
91
|
+
export declare function planNegativeControl(projectRoot: string, engineDir: string, diffFile: string, options?: {
|
|
92
|
+
deferApplyCheck?: boolean;
|
|
93
|
+
}): Promise<NegativeControlPlan>;
|
|
94
|
+
/**
|
|
95
|
+
* Applies the control: every target is computed first (a hunk that does
|
|
96
|
+
* not apply refuses with nothing written), the originals and the journal
|
|
97
|
+
* are persisted, then the mutated bytes are written. Runs as a signal
|
|
98
|
+
* critical section, so a Ctrl+C mid-apply waits for the apply to finish
|
|
99
|
+
* and the signal path then restores a complete journal.
|
|
100
|
+
*
|
|
101
|
+
* @returns The handle {@link restoreNegativeControl} takes
|
|
102
|
+
*/
|
|
103
|
+
export declare function applyNegativeControl(paths: NegativeControlPaths, plan: NegativeControlPlan): Promise<ActiveNegativeControl>;
|
|
104
|
+
/** What happened to each file of a restored control. */
|
|
105
|
+
export interface NegativeControlRestoreResult {
|
|
106
|
+
/** Engine-relative files put back to their original bytes. */
|
|
107
|
+
restored: string[];
|
|
108
|
+
/** Engine-relative files that held neither the control's bytes nor the original. */
|
|
109
|
+
conflicts: string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Restores a control applied by {@link applyNegativeControl}. Idempotent:
|
|
113
|
+
* a second caller (the signal handler racing the run's own `finally`)
|
|
114
|
+
* receives the restore already in flight.
|
|
115
|
+
*
|
|
116
|
+
* @throws GeneralError when a file changed under the control (the edit is
|
|
117
|
+
* kept and the original stays in the journal directory) or did not read
|
|
118
|
+
* back byte-exact
|
|
119
|
+
*/
|
|
120
|
+
export declare function restoreNegativeControl(control: ActiveNegativeControl): Promise<NegativeControlRestoreResult>;
|
|
121
|
+
/**
|
|
122
|
+
* Restores every applied control on the way out of a SIGINT/SIGTERM. Called
|
|
123
|
+
* by the entry point after child shutdown and before the held locks are
|
|
124
|
+
* force-released. Never throws: a failure is reported on stderr and the
|
|
125
|
+
* journal stays for the next run's recovery.
|
|
126
|
+
*/
|
|
127
|
+
export declare function restoreActiveNegativeControlsForSignal(): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Undoes a control a killed FireForge left applied. Called at the start of
|
|
130
|
+
* every `fireforge test`, under the engine-session lock. A file that still
|
|
131
|
+
* holds the control's bytes is restored; a file somebody changed since is
|
|
132
|
+
* refused rather than overwritten.
|
|
133
|
+
*
|
|
134
|
+
* @throws PreflightRefusalError (`note=negative-control-pending`) when a
|
|
135
|
+
* journal entry cannot be restored safely
|
|
136
|
+
*/
|
|
137
|
+
export declare function recoverPendingNegativeControl(paths: NegativeControlPaths): Promise<void>;
|
|
138
|
+
export {};
|