@hominis/fireforge 0.41.0 → 0.43.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 +81 -34
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +21 -2
- package/dist/src/commands/build.js +7 -1
- package/dist/src/commands/config.js +16 -27
- package/dist/src/commands/discard.js +1 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.js +1 -1
- package/dist/src/commands/export-shared.d.ts +1 -1
- package/dist/src/commands/export-shared.js +4 -5
- package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
- package/dist/src/commands/furnace/chrome-doc.js +3 -0
- package/dist/src/commands/furnace/create.js +3 -0
- package/dist/src/commands/furnace/override.js +3 -0
- package/dist/src/commands/furnace/preview.js +3 -0
- package/dist/src/commands/furnace/refresh.js +3 -0
- package/dist/src/commands/furnace/remove.js +3 -0
- package/dist/src/commands/furnace/rename.js +3 -0
- package/dist/src/commands/furnace/scan.js +3 -0
- package/dist/src/commands/import.js +1 -1
- package/dist/src/commands/lint-per-patch.js +16 -8
- package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
- package/dist/src/commands/lint-per-run-checkjs.js +4 -4
- package/dist/src/commands/lint-report.js +1 -1
- package/dist/src/commands/lint.js +2 -2
- package/dist/src/commands/patch/lint-ignore.js +1 -1
- package/dist/src/commands/patch/move-files-create.js +5 -3
- package/dist/src/commands/patch/move-files-into.d.ts +1 -1
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/reorder.d.ts +1 -2
- package/dist/src/commands/patch/reorder.js +1 -2
- package/dist/src/commands/patch/split.js +5 -3
- package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
- package/dist/src/commands/patch/staged-dependency.js +4 -4
- package/dist/src/commands/re-export-adjacent.js +1 -1
- package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
- package/dist/src/commands/re-export-bulk-scan.js +5 -4
- package/dist/src/commands/re-export-drift.d.ts +4 -4
- package/dist/src/commands/re-export-drift.js +47 -8
- package/dist/src/commands/re-export-lint.d.ts +7 -5
- package/dist/src/commands/re-export-lint.js +9 -8
- package/dist/src/commands/re-export-single.d.ts +1 -1
- package/dist/src/commands/re-export-single.js +5 -4
- package/dist/src/commands/re-export.js +26 -14
- package/dist/src/commands/reset.js +1 -1
- package/dist/src/commands/run.js +1 -1
- package/dist/src/commands/source.js +6 -6
- package/dist/src/commands/status-check.d.ts +1 -1
- package/dist/src/commands/status-check.js +1 -1
- package/dist/src/commands/status-json.d.ts +2 -2
- package/dist/src/commands/status-json.js +1 -1
- package/dist/src/commands/status-output.d.ts +1 -1
- package/dist/src/commands/status-output.js +1 -1
- package/dist/src/commands/status.js +21 -7
- package/dist/src/commands/test-build-phase.d.ts +2 -2
- package/dist/src/commands/test-build-phase.js +9 -6
- package/dist/src/commands/test-diagnose.js +8 -1
- package/dist/src/commands/test-modes.d.ts +2 -2
- package/dist/src/commands/test-modes.js +3 -3
- package/dist/src/commands/test-register.js +7 -2
- package/dist/src/commands/test-run.d.ts +6 -0
- package/dist/src/commands/test-run.js +32 -3
- package/dist/src/commands/test-stale-gate.js +7 -2
- package/dist/src/commands/test.js +4 -4
- package/dist/src/commands/tree.js +2 -2
- package/dist/src/commands/typecheck.d.ts +2 -1
- package/dist/src/commands/typecheck.js +59 -3
- package/dist/src/commands/verify.js +26 -1
- package/dist/src/core/ast-utils.js +14 -2
- package/dist/src/core/branding.d.ts +16 -0
- package/dist/src/core/branding.js +18 -2
- package/dist/src/core/browser-wire.js +9 -1
- package/dist/src/core/build-baseline-types.d.ts +1 -1
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +1 -1
- package/dist/src/core/build-overwrite-guard.d.ts +49 -0
- package/dist/src/core/build-overwrite-guard.js +112 -0
- package/dist/src/core/build-prepare.d.ts +13 -5
- package/dist/src/core/build-prepare.js +42 -9
- package/dist/src/core/config-mutate.d.ts +3 -2
- package/dist/src/core/config-mutate.js +5 -2
- package/dist/src/core/config.d.ts +3 -2
- package/dist/src/core/config.js +2 -0
- package/dist/src/core/coverage-extend.d.ts +1 -1
- package/dist/src/core/coverage-extend.js +1 -1
- package/dist/src/core/destructive.d.ts +23 -0
- package/dist/src/core/destructive.js +45 -4
- package/dist/src/core/diff-hunks.js +31 -17
- package/dist/src/core/discard-baseline.d.ts +1 -1
- package/dist/src/core/discard-baseline.js +1 -1
- package/dist/src/core/display-state.d.ts +52 -0
- package/dist/src/core/display-state.js +89 -0
- package/dist/src/core/engine-session-lock.d.ts +20 -0
- package/dist/src/core/engine-session-lock.js +84 -14
- package/dist/src/core/export-moved-code-shape.d.ts +60 -0
- package/dist/src/core/export-moved-code-shape.js +129 -0
- package/dist/src/core/file-lock.d.ts +44 -0
- package/dist/src/core/file-lock.js +157 -42
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-apply-helpers.js +1 -1
- package/dist/src/core/furnace-apply-output.js +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
- package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
- package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
- package/dist/src/core/furnace-apply.js +13 -1
- package/dist/src/core/furnace-config-custom.d.ts +2 -1
- package/dist/src/core/furnace-config-order.d.ts +2 -1
- package/dist/src/core/furnace-config-order.js +9 -4
- package/dist/src/core/furnace-config-validate.d.ts +4 -3
- package/dist/src/core/furnace-config-validate.js +3 -3
- package/dist/src/core/furnace-config.js +5 -0
- package/dist/src/core/furnace-operation.d.ts +12 -0
- package/dist/src/core/furnace-operation.js +125 -1
- package/dist/src/core/furnace-rollback.d.ts +17 -0
- package/dist/src/core/furnace-rollback.js +35 -1
- package/dist/src/core/git-base.js +7 -2
- package/dist/src/core/git-diff.js +11 -2
- package/dist/src/core/git-readonly-index.d.ts +59 -0
- package/dist/src/core/git-readonly-index.js +137 -0
- package/dist/src/core/license-headers.d.ts +1 -1
- package/dist/src/core/license-headers.js +1 -1
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +35 -8
- package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
- package/dist/src/core/mach-known-noise-filter.js +23 -0
- package/dist/src/core/mach-mozconfig.js +21 -3
- package/dist/src/core/module-resolution-preflight.d.ts +51 -0
- package/dist/src/core/module-resolution-preflight.js +155 -0
- package/dist/src/core/patch-export.js +2 -1
- package/dist/src/core/patch-identifier-suggest.js +29 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
- package/dist/src/core/patch-lint-checkjs.js +6 -6
- package/dist/src/core/patch-lint-creators.d.ts +1 -1
- package/dist/src/core/patch-lint-creators.js +1 -1
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +4 -4
- package/dist/src/core/patch-lint-css.js +1 -1
- package/dist/src/core/patch-lint-diff.d.ts +1 -1
- package/dist/src/core/patch-lint-diff.js +1 -1
- package/dist/src/core/patch-lint-jsdoc.js +4 -2
- package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
- package/dist/src/core/patch-lint-module-registration.js +13 -4
- package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
- package/dist/src/core/patch-lint-mozbuild.js +1 -1
- package/dist/src/core/patch-lint-ownership.d.ts +2 -2
- package/dist/src/core/patch-lint-ownership.js +2 -2
- package/dist/src/core/patch-lint.d.ts +2 -2
- package/dist/src/core/patch-lint.js +3 -3
- package/dist/src/core/patch-lock.d.ts +21 -0
- package/dist/src/core/patch-lock.js +29 -2
- package/dist/src/core/patch-manifest-io.d.ts +2 -1
- package/dist/src/core/patch-manifest-io.js +49 -0
- package/dist/src/core/patch-parse.d.ts +18 -0
- package/dist/src/core/patch-parse.js +24 -1
- package/dist/src/core/patch-policy.d.ts +2 -2
- package/dist/src/core/patch-policy.js +2 -2
- package/dist/src/core/smoke-patterns.d.ts +1 -1
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +1 -1
- package/dist/src/core/status-classify.d.ts +1 -1
- package/dist/src/core/status-classify.js +2 -2
- package/dist/src/core/test-harness-crash.d.ts +69 -3
- package/dist/src/core/test-harness-crash.js +174 -9
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +1 -1
- package/dist/src/core/token-category.js +1 -1
- package/dist/src/core/token-manager.d.ts +1 -1
- package/dist/src/core/token-manager.js +5 -5
- package/dist/src/core/tree-cow.js +1 -1
- package/dist/src/core/tree-guard.js +2 -2
- package/dist/src/core/tree-store.js +1 -1
- package/dist/src/core/typecheck-shim.d.ts +2 -2
- package/dist/src/core/typecheck-shim.js +2 -2
- package/dist/src/core/typecheck.js +10 -0
- package/dist/src/core/xpcshell-profile-dir.js +1 -1
- package/dist/src/errors/base.d.ts +22 -1
- package/dist/src/errors/base.js +32 -1
- package/dist/src/errors/codes.d.ts +10 -0
- package/dist/src/errors/codes.js +10 -0
- package/dist/src/types/commands/options.d.ts +27 -11
- package/dist/src/types/commands/patches.d.ts +1 -1
- package/dist/src/types/config.d.ts +2 -2
- package/dist/src/types/furnace.d.ts +1 -1
- package/dist/src/types/json.d.ts +22 -0
- package/dist/src/types/json.js +11 -0
- package/dist/src/utils/assert.d.ts +46 -0
- package/dist/src/utils/assert.js +83 -0
- package/dist/src/utils/build-info.d.ts +1 -1
- package/dist/src/utils/build-info.js +1 -1
- package/dist/src/utils/logger.d.ts +20 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/options.d.ts +12 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/stdio-drain.d.ts +1 -1
- package/dist/src/utils/stdio-drain.js +1 -1
- package/dist/src/utils/validation.d.ts +12 -1
- package/dist/src/utils/validation.js +13 -1
- package/package.json +3 -3
|
@@ -86,7 +86,7 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
86
86
|
* static-components anchor whenever the coverage claim is `'full'` or
|
|
87
87
|
* absent. `'refresh'` records it even for a scoped coverage claim whose
|
|
88
88
|
* implementation escalated to a full build. `'carry-forward'` always keeps the previous anchor: needed by
|
|
89
|
-
*
|
|
89
|
+
* `--extend-coverage`, whose union can EVALUATE to `'full'`
|
|
90
90
|
* while the build that produced it was still a scoped `mach build
|
|
91
91
|
* faster` that did not rebake the compiled table.
|
|
92
92
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loud guard against silently reverting UNEXPORTED engine drift.
|
|
3
|
+
*
|
|
4
|
+
* `build-prepare` rewrites engine files from FireForge-owned sources
|
|
5
|
+
* before every build: the branding tree, every Furnace-managed component,
|
|
6
|
+
* and `mozconfig`. On a multi-session checkout that is a silent
|
|
7
|
+
* destructive write. The recorded incident: one session edited a file in
|
|
8
|
+
* `engine/` while another held the engine lock; the lock-holder's build
|
|
9
|
+
* rewrote the file back to its patch baseline; the editing session's later
|
|
10
|
+
* `re-export --wait-lock` then captured a HYBRID file — and every gate
|
|
11
|
+
* passed on the hybrid capture. Only a hand-run grep of the patch body
|
|
12
|
+
* caught it.
|
|
13
|
+
*
|
|
14
|
+
* The `setupBranding` half of this class was closed in `31fdf744` (the
|
|
15
|
+
* branding writer preserves unmanaged lines). The general shape — any
|
|
16
|
+
* build-prepare overwrite destroying local drift that no patch records —
|
|
17
|
+
* stayed silent, and silence is what made the hybrid capture possible.
|
|
18
|
+
*
|
|
19
|
+
* This guard runs BEFORE the writes, classifies the engine's dirty files
|
|
20
|
+
* with the same classifier `fireforge status` uses, and reports the ones
|
|
21
|
+
* whose content is explained by neither a patch body nor the pristine
|
|
22
|
+
* baseline. Advisory by default (a warning naming every file), refusing
|
|
23
|
+
* under an explicit flag.
|
|
24
|
+
*/
|
|
25
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
26
|
+
import { type ClassifiedFile } from './status-classify.js';
|
|
27
|
+
/** One file whose unexported content a build-prepare overwrite would destroy. */
|
|
28
|
+
export interface UnexportedDriftAtRisk {
|
|
29
|
+
file: string;
|
|
30
|
+
classification: ClassifiedFile['classification'];
|
|
31
|
+
owner?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Finds engine files that build-prepare is about to overwrite and whose
|
|
35
|
+
* current content is recorded nowhere.
|
|
36
|
+
*
|
|
37
|
+
* Fail-open: any probe failure is reported verbosely and yields an empty
|
|
38
|
+
* list. A build must not fail because an advisory guard could not run.
|
|
39
|
+
*
|
|
40
|
+
* @param projectRoot - Project root
|
|
41
|
+
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
42
|
+
* @returns At-risk files, sorted by path
|
|
43
|
+
*/
|
|
44
|
+
export declare function findUnexportedDriftAtRisk(projectRoot: string, config: FireForgeConfig): Promise<UnexportedDriftAtRisk[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Renders the operator-facing warning (or refusal body) naming every file
|
|
47
|
+
* whose unexported content the build is about to overwrite.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatUnexportedDriftWarning(files: readonly UnexportedDriftAtRisk[]): string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Loud guard against silently reverting UNEXPORTED engine drift.
|
|
4
|
+
*
|
|
5
|
+
* `build-prepare` rewrites engine files from FireForge-owned sources
|
|
6
|
+
* before every build: the branding tree, every Furnace-managed component,
|
|
7
|
+
* and `mozconfig`. On a multi-session checkout that is a silent
|
|
8
|
+
* destructive write. The recorded incident: one session edited a file in
|
|
9
|
+
* `engine/` while another held the engine lock; the lock-holder's build
|
|
10
|
+
* rewrote the file back to its patch baseline; the editing session's later
|
|
11
|
+
* `re-export --wait-lock` then captured a HYBRID file — and every gate
|
|
12
|
+
* passed on the hybrid capture. Only a hand-run grep of the patch body
|
|
13
|
+
* caught it.
|
|
14
|
+
*
|
|
15
|
+
* The `setupBranding` half of this class was closed in `31fdf744` (the
|
|
16
|
+
* branding writer preserves unmanaged lines). The general shape — any
|
|
17
|
+
* build-prepare overwrite destroying local drift that no patch records —
|
|
18
|
+
* stayed silent, and silence is what made the hybrid capture possible.
|
|
19
|
+
*
|
|
20
|
+
* This guard runs BEFORE the writes, classifies the engine's dirty files
|
|
21
|
+
* with the same classifier `fireforge status` uses, and reports the ones
|
|
22
|
+
* whose content is explained by neither a patch body nor the pristine
|
|
23
|
+
* baseline. Advisory by default (a warning naming every file), refusing
|
|
24
|
+
* under an explicit flag.
|
|
25
|
+
*/
|
|
26
|
+
import { toError } from '../utils/errors.js';
|
|
27
|
+
import { verbose } from '../utils/logger.js';
|
|
28
|
+
import { getProjectPaths } from './config.js';
|
|
29
|
+
import { collectFurnaceManagedPrefixes } from './furnace-config.js';
|
|
30
|
+
import { getWorkingTreeStatus } from './git-status.js';
|
|
31
|
+
import { classifyFiles } from './status-classify.js';
|
|
32
|
+
/**
|
|
33
|
+
* Classifications whose on-disk content is explained by NEITHER a patch
|
|
34
|
+
* body nor the pristine baseline — exactly the content a build-prepare
|
|
35
|
+
* overwrite would destroy without a record anywhere.
|
|
36
|
+
*
|
|
37
|
+
* `patch-backed` and `furnace` are excluded because their content IS the
|
|
38
|
+
* recorded state; `branding` is excluded because the branding writer
|
|
39
|
+
* preserves unmanaged lines rather than overwriting them.
|
|
40
|
+
*/
|
|
41
|
+
const AT_RISK_CLASSIFICATIONS = new Set(['unmanaged', 'patch-owned-drift', 'conflict']);
|
|
42
|
+
/**
|
|
43
|
+
* True when `file` sits under a path build-prepare rewrites from a
|
|
44
|
+
* FireForge-owned source: the branding tree, a Furnace-managed prefix, or
|
|
45
|
+
* the generated `mozconfig`.
|
|
46
|
+
*/
|
|
47
|
+
function isOverwrittenByBuildPrepare(file, binaryName, furnacePrefixes) {
|
|
48
|
+
if (file === 'mozconfig')
|
|
49
|
+
return true;
|
|
50
|
+
if (file.startsWith(`browser/branding/${binaryName}/`))
|
|
51
|
+
return true;
|
|
52
|
+
for (const prefix of furnacePrefixes) {
|
|
53
|
+
if (file === prefix || file.startsWith(prefix))
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Finds engine files that build-prepare is about to overwrite and whose
|
|
60
|
+
* current content is recorded nowhere.
|
|
61
|
+
*
|
|
62
|
+
* Fail-open: any probe failure is reported verbosely and yields an empty
|
|
63
|
+
* list. A build must not fail because an advisory guard could not run.
|
|
64
|
+
*
|
|
65
|
+
* @param projectRoot - Project root
|
|
66
|
+
* @param config - Loaded FireForge config (reads `binaryName`)
|
|
67
|
+
* @returns At-risk files, sorted by path
|
|
68
|
+
*/
|
|
69
|
+
export async function findUnexportedDriftAtRisk(projectRoot, config) {
|
|
70
|
+
const paths = getProjectPaths(projectRoot);
|
|
71
|
+
try {
|
|
72
|
+
const status = await getWorkingTreeStatus(paths.engine);
|
|
73
|
+
if (status.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
76
|
+
const candidates = status.filter((entry) => isOverwrittenByBuildPrepare(entry.file, config.binaryName, furnacePrefixes));
|
|
77
|
+
if (candidates.length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
const classified = await classifyFiles(candidates, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
|
|
80
|
+
return classified
|
|
81
|
+
.filter((entry) => AT_RISK_CLASSIFICATIONS.has(entry.classification))
|
|
82
|
+
.map((entry) => ({
|
|
83
|
+
file: entry.file,
|
|
84
|
+
classification: entry.classification,
|
|
85
|
+
owner: entry.owner,
|
|
86
|
+
}))
|
|
87
|
+
.sort((a, b) => a.file.localeCompare(b.file));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
verbose(`Unexported-drift guard could not run: ${toError(error).message}`);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Renders the operator-facing warning (or refusal body) naming every file
|
|
96
|
+
* whose unexported content the build is about to overwrite.
|
|
97
|
+
*/
|
|
98
|
+
export function formatUnexportedDriftWarning(files) {
|
|
99
|
+
const rows = files.map((entry) => {
|
|
100
|
+
const owner = entry.owner !== undefined ? ` (owned by ${entry.owner})` : '';
|
|
101
|
+
return ` ${entry.file} [${entry.classification}]${owner}`;
|
|
102
|
+
});
|
|
103
|
+
return (`This build is about to rewrite ${String(files.length)} engine file(s) from FireForge-owned ` +
|
|
104
|
+
'sources, and their current content matches NEITHER a patch body NOR the pristine ' +
|
|
105
|
+
'baseline — so the edits below are recorded nowhere and the build will destroy them:\n' +
|
|
106
|
+
`${rows.join('\n')}\n\n` +
|
|
107
|
+
'On a multi-session checkout this is how a hybrid capture happens: the overwrite lands, ' +
|
|
108
|
+
"the editing session's later re-export captures a half-reverted file, and every gate " +
|
|
109
|
+
'passes on it.\n\n' +
|
|
110
|
+
'Export the edits first (`fireforge re-export <patch>`, or `fireforge export` for new ' +
|
|
111
|
+
'work), or pass --refuse-unexported-drift to make this a hard stop in scripted runs.');
|
|
112
|
+
}
|
|
@@ -25,6 +25,11 @@ export interface PrepareBuildOptions {
|
|
|
25
25
|
* means.
|
|
26
26
|
*/
|
|
27
27
|
previousBaseline?: BuildBaseline | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Turn the unexported-drift warning into a hard refusal.
|
|
30
|
+
* Scripted gates want the stop; an interactive build wants the warning.
|
|
31
|
+
*/
|
|
32
|
+
refuseUnexportedDrift?: boolean | undefined;
|
|
28
33
|
}
|
|
29
34
|
/** Packaging manifests whose graph/destination directories need a full build. */
|
|
30
35
|
export declare function requiresFullBuildForIncrementalTest(path: string): boolean;
|
|
@@ -33,7 +38,7 @@ export declare function requiresFullBuildForIncrementalTest(path: string): boole
|
|
|
33
38
|
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
34
39
|
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
35
40
|
* that in the failure text saves the operator a fruitless log hunt
|
|
36
|
-
|
|
41
|
+
*. Returns undefined for codes <= 128 (regular failures) and
|
|
37
42
|
* for codes past the conventional signal range, so callers append nothing.
|
|
38
43
|
*/
|
|
39
44
|
export declare function describeSignalShapedExit(exitCode: number): string | undefined;
|
|
@@ -44,14 +49,17 @@ export declare function describeSignalShapedExit(exitCode: number): string | und
|
|
|
44
49
|
export declare function isBackendInvalidatingFile(path: string): boolean;
|
|
45
50
|
/**
|
|
46
51
|
* Runs the shared pre-flight steps for build and package commands:
|
|
47
|
-
* 1.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
52
|
+
* 1. Warns (or refuses) about unexported engine drift the writes below
|
|
53
|
+
* would destroy
|
|
54
|
+
* 2. Cleans Furnace stories from engine (prevents leaking into production)
|
|
55
|
+
* 3. Sets up branding directory if not already done
|
|
56
|
+
* 4. Applies Furnace components if furnace.json exists
|
|
57
|
+
* 5. Generates mozconfig
|
|
51
58
|
*
|
|
52
59
|
* @param projectRoot - Root directory of the project
|
|
53
60
|
* @param paths - Resolved project paths
|
|
54
61
|
* @param config - Loaded FireForge configuration
|
|
62
|
+
* @param options - Baseline and drift-refusal options
|
|
55
63
|
* @returns Preparation results
|
|
56
64
|
*/
|
|
57
65
|
export declare function prepareBuildEnvironment(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig, options?: PrepareBuildOptions): Promise<BuildPreparation>;
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
5
5
|
*/
|
|
6
6
|
import { constants as osConstants } from 'node:os';
|
|
7
|
+
import { GeneralError } from '../errors/base.js';
|
|
7
8
|
import { BuildError } from '../errors/build.js';
|
|
8
9
|
import { FurnaceError } from '../errors/furnace.js';
|
|
9
10
|
import { toError } from '../utils/errors.js';
|
|
10
11
|
import { pathExists } from '../utils/fs.js';
|
|
11
|
-
import { info, spinner, verbose, warn } from '../utils/logger.js';
|
|
12
|
+
import { info, notice, spinner, verbose, warn } from '../utils/logger.js';
|
|
12
13
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
14
|
+
import { findUnexportedDriftAtRisk, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
|
|
13
15
|
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
14
16
|
import { applyAllComponents } from './furnace-apply.js';
|
|
15
17
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
@@ -43,7 +45,7 @@ function extractMachConfigureError(result) {
|
|
|
43
45
|
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
44
46
|
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
45
47
|
* that in the failure text saves the operator a fruitless log hunt
|
|
46
|
-
|
|
48
|
+
*. Returns undefined for codes <= 128 (regular failures) and
|
|
47
49
|
* for codes past the conventional signal range, so callers append nothing.
|
|
48
50
|
*/
|
|
49
51
|
export function describeSignalShapedExit(exitCode) {
|
|
@@ -87,16 +89,44 @@ export function isBackendInvalidatingFile(path) {
|
|
|
87
89
|
}
|
|
88
90
|
return false;
|
|
89
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Loud before the overwrite, not silent after it.
|
|
94
|
+
*
|
|
95
|
+
* Every write `prepareBuildEnvironment` performs — branding, Furnace
|
|
96
|
+
* components, `mozconfig` — rewrites engine files from a FireForge-owned
|
|
97
|
+
* source. Content recorded in neither a patch body nor the pristine
|
|
98
|
+
* baseline is destroyed by that, and on a multi-session checkout the
|
|
99
|
+
* destruction is what lets a later re-export capture a half-reverted
|
|
100
|
+
* hybrid that every gate then passes.
|
|
101
|
+
*
|
|
102
|
+
* @param projectRoot - Project root
|
|
103
|
+
* @param config - Loaded FireForge config
|
|
104
|
+
* @param options - Build-prepare options (reads `refuseUnexportedDrift`)
|
|
105
|
+
* @throws GeneralError under `--refuse-unexported-drift`
|
|
106
|
+
*/
|
|
107
|
+
async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options) {
|
|
108
|
+
const atRisk = await findUnexportedDriftAtRisk(projectRoot, config);
|
|
109
|
+
if (atRisk.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const message = formatUnexportedDriftWarning(atRisk);
|
|
112
|
+
if (options.refuseUnexportedDrift === true) {
|
|
113
|
+
throw new GeneralError(message);
|
|
114
|
+
}
|
|
115
|
+
notice(message);
|
|
116
|
+
}
|
|
90
117
|
/**
|
|
91
118
|
* Runs the shared pre-flight steps for build and package commands:
|
|
92
|
-
* 1.
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
119
|
+
* 1. Warns (or refuses) about unexported engine drift the writes below
|
|
120
|
+
* would destroy
|
|
121
|
+
* 2. Cleans Furnace stories from engine (prevents leaking into production)
|
|
122
|
+
* 3. Sets up branding directory if not already done
|
|
123
|
+
* 4. Applies Furnace components if furnace.json exists
|
|
124
|
+
* 5. Generates mozconfig
|
|
96
125
|
*
|
|
97
126
|
* @param projectRoot - Root directory of the project
|
|
98
127
|
* @param paths - Resolved project paths
|
|
99
128
|
* @param config - Loaded FireForge configuration
|
|
129
|
+
* @param options - Baseline and drift-refusal options
|
|
100
130
|
* @returns Preparation results
|
|
101
131
|
*/
|
|
102
132
|
export async function prepareBuildEnvironment(projectRoot, paths, config, options = {}) {
|
|
@@ -111,6 +141,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
111
141
|
'Run "fireforge doctor --repair-furnace" to reconcile engine state before building.');
|
|
112
142
|
}
|
|
113
143
|
}
|
|
144
|
+
await reportUnexportedDriftBeforeOverwrite(projectRoot, config, options);
|
|
114
145
|
// Auto-configure: if any backend-invalidating file (moz.build, moz.configure,
|
|
115
146
|
// Makefile.in) changed since the last successful build, run `mach configure`
|
|
116
147
|
// before the build step. Prevents incremental builds from silently skipping
|
|
@@ -122,8 +153,10 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
122
153
|
const invalidating = changed.filter(isBackendInvalidatingFile);
|
|
123
154
|
fullBuildRequired = changed.some(requiresFullBuildForIncrementalTest);
|
|
124
155
|
if (invalidating.length > 0) {
|
|
125
|
-
|
|
126
|
-
info
|
|
156
|
+
// Warning severity: this is the "why is this run slow" explanation,
|
|
157
|
+
// and an info-level line does not survive an agent output filter that
|
|
158
|
+
// keeps only warnings and errors.
|
|
159
|
+
notice(`Backend config changed; running backend regeneration first (${invalidating.length} file${invalidating.length === 1 ? '' : 's'} touched). Backend command: mach configure.`);
|
|
127
160
|
const configureSpinner = spinner('Running mach configure...');
|
|
128
161
|
try {
|
|
129
162
|
const captured = await runMachCapture(['configure'], paths.engine);
|
|
@@ -222,7 +255,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
222
255
|
// Loud banner: the build operator needs to see that engine/ was
|
|
223
256
|
// updated before this build, otherwise a silent re-apply is
|
|
224
257
|
// indistinguishable from a build that shipped stale components.
|
|
225
|
-
|
|
258
|
+
notice(`Furnace: source → engine sync wrote ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'} before build (${appliedNames}). engine/ now matches components/.`);
|
|
226
259
|
}
|
|
227
260
|
else {
|
|
228
261
|
furnaceSpinner.stop('Components up to date');
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Immutable config mutation with dot-path keys.
|
|
3
3
|
*/
|
|
4
4
|
import type { FireForgeConfig } from '../types/config.js';
|
|
5
|
+
import type { JsonObject, JsonValue } from '../types/json.js';
|
|
5
6
|
/**
|
|
6
7
|
* Creates a mutated copy of a config with a nested key set to a new value,
|
|
7
8
|
* optionally re-validated.
|
|
@@ -11,5 +12,5 @@ import type { FireForgeConfig } from '../types/config.js';
|
|
|
11
12
|
* @param skipValidation - If true, skip re-validation (for --force)
|
|
12
13
|
* @returns The mutated config
|
|
13
14
|
*/
|
|
14
|
-
export declare function mutateConfig(config: FireForgeConfig, key: string, value:
|
|
15
|
-
export declare function mutateConfig(config: FireForgeConfig |
|
|
15
|
+
export declare function mutateConfig(config: FireForgeConfig, key: string, value: JsonValue, skipValidation?: false): FireForgeConfig;
|
|
16
|
+
export declare function mutateConfig(config: FireForgeConfig | JsonObject, key: string, value: JsonValue, skipValidation: true): JsonObject;
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
import { ConfigError } from '../errors/config.js';
|
|
6
6
|
import { toError } from '../utils/errors.js';
|
|
7
7
|
import { verbose } from '../utils/logger.js';
|
|
8
|
-
import { isObject } from '../utils/validation.js';
|
|
8
|
+
import { isJsonObject, isObject } from '../utils/validation.js';
|
|
9
9
|
import { validateConfig } from './config-validate.js';
|
|
10
10
|
function cloneConfigDocument(config) {
|
|
11
11
|
const cloned = structuredClone(config);
|
|
12
12
|
if (!isObject(cloned)) {
|
|
13
13
|
throw new ConfigError('Config clone unexpectedly produced a non-object value');
|
|
14
14
|
}
|
|
15
|
+
// Both input shapes hold only JSON data (`FireForgeConfig` is plain
|
|
16
|
+
// parsed-config data), and `structuredClone` preserves that, so the
|
|
17
|
+
// object check above is the only invariant left to establish.
|
|
15
18
|
return cloned;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
@@ -33,7 +36,7 @@ function assertNoSentinelSegments(key, parts) {
|
|
|
33
36
|
}
|
|
34
37
|
function getOrCreateChildRecord(parent, key) {
|
|
35
38
|
const existing = parent[key];
|
|
36
|
-
if (
|
|
39
|
+
if (isJsonObject(existing)) {
|
|
37
40
|
return existing;
|
|
38
41
|
}
|
|
39
42
|
const child = {};
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* config-state.ts — state file management
|
|
9
9
|
*/
|
|
10
10
|
import type { FireForgeConfig } from '../types/config.js';
|
|
11
|
+
import type { JsonObject } from '../types/json.js';
|
|
11
12
|
export { mutateConfig } from './config-mutate.js';
|
|
12
13
|
export { CONFIG_FILENAME, FIREFORGE_DIR, getProjectPaths, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
|
|
13
14
|
export { loadState, saveState, updateState } from './config-state.js';
|
|
@@ -41,7 +42,7 @@ export declare function loadConfig(root: string): Promise<FireForgeConfig>;
|
|
|
41
42
|
* @throws ConfigNotFoundError when fireforge.json is missing
|
|
42
43
|
* @throws ConfigError when the file is not valid JSON
|
|
43
44
|
*/
|
|
44
|
-
export declare function loadRawConfigDocument(root: string): Promise<
|
|
45
|
+
export declare function loadRawConfigDocument(root: string): Promise<JsonObject>;
|
|
45
46
|
/**
|
|
46
47
|
* Writes a configuration to fireforge.json.
|
|
47
48
|
* @param root - Root directory of the project
|
|
@@ -61,7 +62,7 @@ export declare function writeConfig(root: string, config: FireForgeConfig): Prom
|
|
|
61
62
|
* {@link withConfigFileLock} for the full round-trip to serialise
|
|
62
63
|
* against other writers.
|
|
63
64
|
*/
|
|
64
|
-
export declare function writeConfigDocument(root: string, config: FireForgeConfig |
|
|
65
|
+
export declare function writeConfigDocument(root: string, config: FireForgeConfig | JsonObject): Promise<void>;
|
|
65
66
|
/**
|
|
66
67
|
* Runs an operation while holding a sidecar lock on `fireforge.json`.
|
|
67
68
|
*
|
package/dist/src/core/config.js
CHANGED
|
@@ -95,6 +95,8 @@ export async function loadRawConfigDocument(root) {
|
|
|
95
95
|
if (!isObject(data)) {
|
|
96
96
|
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: expected an object`);
|
|
97
97
|
}
|
|
98
|
+
// `data` came out of JSON.parse, which can only ever produce JSON
|
|
99
|
+
// values, so with the object check above the JsonObject contract holds.
|
|
98
100
|
return data;
|
|
99
101
|
}
|
|
100
102
|
catch (error) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* `test --build/--build-only --extend-coverage
|
|
3
|
+
* `test --build/--build-only --extend-coverage`: unions the
|
|
4
4
|
* requested paths into the recorded `testPackagingCoverage` instead of
|
|
5
5
|
* replacing it, under an anchor guard.
|
|
6
6
|
*
|
|
@@ -68,6 +68,29 @@ export interface HistoryEntry {
|
|
|
68
68
|
/** Result: `'ok'` on success; other strings describe failure. */
|
|
69
69
|
result: string;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* UP-FRONT non-interactive refusal for a command that will end at
|
|
73
|
+
* {@link confirmDestructive}.
|
|
74
|
+
*
|
|
75
|
+
* `confirmDestructive` already refuses a prompt-less run rather than
|
|
76
|
+
* proceeding silently — but it does so at the END, after the command has
|
|
77
|
+
* built diffs and run projected per-patch lint. On `patch move-files
|
|
78
|
+
* --create` that is minutes of work before a scripted run learns it needed
|
|
79
|
+
* `--yes`, and the failure reads as an obscure late error rather than a
|
|
80
|
+
* usage problem. Calling this at command entry turns it into a usage
|
|
81
|
+
* refusal that names the flag, in the same shape as the 0.41.0
|
|
82
|
+
* `move-files --create` / `--description` refusal.
|
|
83
|
+
*
|
|
84
|
+
* A `--yes` or `--dry-run` run never prompts, so neither is refused.
|
|
85
|
+
*
|
|
86
|
+
* @param operation - Stable operation id, e.g. `patch-move-files-create`
|
|
87
|
+
* @param options - The run's `--yes` / `--dry-run` state
|
|
88
|
+
* @throws InvalidArgumentError when the run would prompt but cannot
|
|
89
|
+
*/
|
|
90
|
+
export declare function assertConfirmationAvailable(operation: string, options: {
|
|
91
|
+
yes?: boolean | undefined;
|
|
92
|
+
dryRun?: boolean | undefined;
|
|
93
|
+
}): void;
|
|
71
94
|
/**
|
|
72
95
|
* Executes the destructive-operation contract: summary → conflict refusal →
|
|
73
96
|
* dry-run / force / prompt / non-TTY refusal.
|
|
@@ -39,6 +39,50 @@ function printConflicts(conflicts) {
|
|
|
39
39
|
info(` ${detail}`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* True when BOTH standard handles are real TTYs, i.e. a `confirm()` prompt
|
|
44
|
+
* can actually be answered.
|
|
45
|
+
*
|
|
46
|
+
* Node types `isTTY` as `boolean`, but at runtime it is `true | undefined`
|
|
47
|
+
* (absent when the handle is not a TTY). Unusual harnesses (vitest stdio
|
|
48
|
+
* capture, CI spawners, mocked pipes) can also leave it unset on only one
|
|
49
|
+
* handle, so every falsy variant — false, undefined, a null-patched mock —
|
|
50
|
+
* must route to the non-interactive path.
|
|
51
|
+
*/
|
|
52
|
+
function confirmationIsAnswerable() {
|
|
53
|
+
const stdinIsTTY = process.stdin.isTTY;
|
|
54
|
+
const stdoutIsTTY = process.stdout.isTTY;
|
|
55
|
+
return stdinIsTTY === true && stdoutIsTTY === true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* UP-FRONT non-interactive refusal for a command that will end at
|
|
59
|
+
* {@link confirmDestructive}.
|
|
60
|
+
*
|
|
61
|
+
* `confirmDestructive` already refuses a prompt-less run rather than
|
|
62
|
+
* proceeding silently — but it does so at the END, after the command has
|
|
63
|
+
* built diffs and run projected per-patch lint. On `patch move-files
|
|
64
|
+
* --create` that is minutes of work before a scripted run learns it needed
|
|
65
|
+
* `--yes`, and the failure reads as an obscure late error rather than a
|
|
66
|
+
* usage problem. Calling this at command entry turns it into a usage
|
|
67
|
+
* refusal that names the flag, in the same shape as the 0.41.0
|
|
68
|
+
* `move-files --create` / `--description` refusal.
|
|
69
|
+
*
|
|
70
|
+
* A `--yes` or `--dry-run` run never prompts, so neither is refused.
|
|
71
|
+
*
|
|
72
|
+
* @param operation - Stable operation id, e.g. `patch-move-files-create`
|
|
73
|
+
* @param options - The run's `--yes` / `--dry-run` state
|
|
74
|
+
* @throws InvalidArgumentError when the run would prompt but cannot
|
|
75
|
+
*/
|
|
76
|
+
export function assertConfirmationAvailable(operation, options) {
|
|
77
|
+
if (options.yes === true || options.dryRun === true)
|
|
78
|
+
return;
|
|
79
|
+
if (confirmationIsAnswerable())
|
|
80
|
+
return;
|
|
81
|
+
throw new InvalidArgumentError(`"${operation}" ends in an interactive confirmation, but this run has no TTY to answer it ` +
|
|
82
|
+
'(stdin/stdout are not both terminals). Re-run with --yes to confirm non-interactively ' +
|
|
83
|
+
'(required for CI and agent-driven runs), or with --dry-run to preview without ' +
|
|
84
|
+
'confirming. Refusing up front rather than after the planning and lint work.', '--yes');
|
|
85
|
+
}
|
|
42
86
|
/**
|
|
43
87
|
* Executes the destructive-operation contract: summary → conflict refusal →
|
|
44
88
|
* dry-run / force / prompt / non-TTY refusal.
|
|
@@ -93,10 +137,7 @@ export async function confirmDestructive(input) {
|
|
|
93
137
|
// one handle. We cast to the real runtime type and compare against
|
|
94
138
|
// literal `true` so any falsy variant — false, undefined, or a
|
|
95
139
|
// null-patched mock — routes to the non-interactive path.
|
|
96
|
-
|
|
97
|
-
const stdoutIsTTY = process.stdout.isTTY;
|
|
98
|
-
const isInteractive = stdinIsTTY === true && stdoutIsTTY === true;
|
|
99
|
-
if (!isInteractive) {
|
|
140
|
+
if (!confirmationIsAnswerable()) {
|
|
100
141
|
printSummary(title, summary);
|
|
101
142
|
throw new InvalidArgumentError(`Interactive confirmation not available for "${operation}". ` +
|
|
102
143
|
'Use --yes to run non-interactively (required for CI).', '--yes');
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
|
|
3
|
-
* This module is a tight DP/backtrack line-diff whose bounds are checked
|
|
4
|
-
* structurally (every index into `ops`, `dp`, or the line arrays is either
|
|
5
|
-
* guarded by an explicit `k < range.end` / `i <= m` loop condition or sits
|
|
6
|
-
* inside an array built and consumed in the same function). Wrapping every
|
|
7
|
-
* access in an `if (value === undefined) throw` branch would bloat the hot
|
|
8
|
-
* path without catching a real class of bugs — the non-null assertions here
|
|
9
|
-
* encode the invariant the surrounding logic already guarantees.
|
|
10
|
-
*/
|
|
2
|
+
import { assert, expectDefined } from '../utils/assert.js';
|
|
11
3
|
/**
|
|
12
4
|
* Maximum combined LCS table side-length before we bail out of the exact
|
|
13
5
|
* O(m·n) diff and fall back to the simpler single-region coalesce. Chosen so
|
|
@@ -37,8 +29,28 @@ function splitLines(text) {
|
|
|
37
29
|
export function computeLineDiff(oldLines, newLines) {
|
|
38
30
|
const m = oldLines.length;
|
|
39
31
|
const n = newLines.length;
|
|
32
|
+
// The DP below indexes `dp`, `oldLines`, and `newLines` about 4·m·n times
|
|
33
|
+
// for a worst-case pair, so its bounds are established once here rather
|
|
34
|
+
// than re-checked per access. Callers gate on LCS_LINE_LIMIT before
|
|
35
|
+
// reaching this function; if that gate is ever moved or removed, this is
|
|
36
|
+
// where the resulting allocation blow-up surfaces as a named failure
|
|
37
|
+
// instead of an out-of-memory crash.
|
|
38
|
+
assert(m <= LCS_LINE_LIMIT && n <= LCS_LINE_LIMIT, () => `line diff inputs are within LCS_LINE_LIMIT (got ${m}x${n}, limit ${LCS_LINE_LIMIT})`);
|
|
40
39
|
const dp = new Int32Array((m + 1) * (n + 1));
|
|
41
40
|
const stride = n + 1;
|
|
41
|
+
// Every index into `dp` is `row * stride + column` with row ≤ m and
|
|
42
|
+
// column ≤ n, which is exactly the extent allocated above.
|
|
43
|
+
assert(dp.length === (m + 1) * stride, 'DP table is sized to its stride');
|
|
44
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion --
|
|
45
|
+
* The DP fill and backtrack below are the one hot path in this module:
|
|
46
|
+
* O(m·n) iterations, up to ~4M for a LCS_LINE_LIMIT-sized pair. Every
|
|
47
|
+
* index is structurally bounded by the loop conditions (`i <= m`,
|
|
48
|
+
* `j <= n`, `i > 0`, `j > 0`) against the extents asserted immediately
|
|
49
|
+
* above, so the non-null assertions encode a property already checked
|
|
50
|
+
* once per call rather than one that is never checked at all. The
|
|
51
|
+
* rendering loops further down, which run O(ops), use `expectDefined`
|
|
52
|
+
* instead — this exemption is for the quadratic block only.
|
|
53
|
+
*/
|
|
42
54
|
for (let i = 1; i <= m; i++) {
|
|
43
55
|
const oldLine = oldLines[i - 1];
|
|
44
56
|
const rowOffset = i * stride;
|
|
@@ -83,6 +95,7 @@ export function computeLineDiff(oldLines, newLines) {
|
|
|
83
95
|
ops.push({ type: 'insert', newIndex: j - 1, line: newLines[j - 1] });
|
|
84
96
|
j--;
|
|
85
97
|
}
|
|
98
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
86
99
|
ops.reverse();
|
|
87
100
|
return ops;
|
|
88
101
|
}
|
|
@@ -97,9 +110,10 @@ export function buildHunks(ops, context) {
|
|
|
97
110
|
const editRanges = [];
|
|
98
111
|
let i = 0;
|
|
99
112
|
while (i < ops.length) {
|
|
100
|
-
if (ops[i].type !== 'equal') {
|
|
113
|
+
if (expectDefined(ops[i], () => `diff op at index ${i}`).type !== 'equal') {
|
|
101
114
|
const start = i;
|
|
102
|
-
while (i < ops.length &&
|
|
115
|
+
while (i < ops.length &&
|
|
116
|
+
expectDefined(ops[i], () => `diff op at index ${i}`).type !== 'equal')
|
|
103
117
|
i++;
|
|
104
118
|
editRanges.push({ start, end: i });
|
|
105
119
|
}
|
|
@@ -126,7 +140,7 @@ export function buildHunks(ops, context) {
|
|
|
126
140
|
let oldLine = 1;
|
|
127
141
|
let newLine = 1;
|
|
128
142
|
for (let k = 0; k < range.start; k++) {
|
|
129
|
-
const op = ops[k];
|
|
143
|
+
const op = expectDefined(ops[k], () => `diff op at index ${k}`);
|
|
130
144
|
if (op.type === 'equal') {
|
|
131
145
|
oldLine++;
|
|
132
146
|
newLine++;
|
|
@@ -142,7 +156,7 @@ export function buildHunks(ops, context) {
|
|
|
142
156
|
let oldLength = 0;
|
|
143
157
|
let newLength = 0;
|
|
144
158
|
for (let k = range.start; k < range.end; k++) {
|
|
145
|
-
const op = ops[k];
|
|
159
|
+
const op = expectDefined(ops[k], () => `diff op at index ${k}`);
|
|
146
160
|
if (op.type === 'equal') {
|
|
147
161
|
hunkLines.push({ marker: ' ', content: op.line });
|
|
148
162
|
oldLength++;
|
|
@@ -197,13 +211,13 @@ function coalescedHunk(oldLines, newLines, context) {
|
|
|
197
211
|
const contextEndNew = Math.min(newLines.length - 1, lastNewDiff + context);
|
|
198
212
|
const hunkLines = [];
|
|
199
213
|
for (let k = contextStart; k < firstDiff; k++) {
|
|
200
|
-
hunkLines.push({ marker: ' ', content: oldLines[k] });
|
|
214
|
+
hunkLines.push({ marker: ' ', content: expectDefined(oldLines[k], () => `old line ${k}`) });
|
|
201
215
|
}
|
|
202
216
|
for (let k = firstDiff; k <= lastOldDiff; k++) {
|
|
203
|
-
hunkLines.push({ marker: '-', content: oldLines[k] });
|
|
217
|
+
hunkLines.push({ marker: '-', content: expectDefined(oldLines[k], () => `old line ${k}`) });
|
|
204
218
|
}
|
|
205
219
|
for (let k = firstDiff; k <= lastNewDiff; k++) {
|
|
206
|
-
hunkLines.push({ marker: '+', content: newLines[k] });
|
|
220
|
+
hunkLines.push({ marker: '+', content: expectDefined(newLines[k], () => `new line ${k}`) });
|
|
207
221
|
}
|
|
208
222
|
// Trailing context comes from the common suffix, which lives at
|
|
209
223
|
// DIFFERENT indices on each side (lastOldDiff+1… vs lastNewDiff+1…).
|
|
@@ -212,7 +226,7 @@ function coalescedHunk(oldLines, newLines, context) {
|
|
|
212
226
|
// lines were excluded from the @@ header lengths entirely — so on
|
|
213
227
|
// >LCS_LINE_LIMIT files the rendered header disagreed with the body.
|
|
214
228
|
for (let k = lastNewDiff + 1; k <= contextEndNew; k++) {
|
|
215
|
-
hunkLines.push({ marker: ' ', content: newLines[k] });
|
|
229
|
+
hunkLines.push({ marker: ' ', content: expectDefined(newLines[k], () => `new line ${k}`) });
|
|
216
230
|
}
|
|
217
231
|
const leadingContext = firstDiff - contextStart;
|
|
218
232
|
// Common-suffix symmetry: oldLen-1-lastOldDiff === newLen-1-lastNewDiff,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Patch-aware restore planning for `fireforge discard` (
|
|
2
|
+
* Patch-aware restore planning for `fireforge discard` (P0).
|
|
3
3
|
*
|
|
4
4
|
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
5
5
|
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Patch-aware restore planning for `fireforge discard` (
|
|
3
|
+
* Patch-aware restore planning for `fireforge discard` (P0).
|
|
4
4
|
*
|
|
5
5
|
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
6
6
|
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|