@hominis/fireforge 0.40.0 → 0.41.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 +79 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -23,6 +23,7 @@ import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
|
23
23
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
24
24
|
import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
|
|
25
25
|
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
26
|
+
import { buildPatchQueueContext } from '../../core/patch-lint.js';
|
|
26
27
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
27
28
|
import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, validatePatchesManifest, } from '../../core/patch-manifest.js';
|
|
28
29
|
import { enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
@@ -30,7 +31,8 @@ import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
|
30
31
|
import { toError } from '../../utils/errors.js';
|
|
31
32
|
import { readText, removeFile, writeText } from '../../utils/fs.js';
|
|
32
33
|
import { info, intro, outro, success, warn } from '../../utils/logger.js';
|
|
33
|
-
import { commanderArgParser, pickDefined } from '../../utils/options.js';
|
|
34
|
+
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
|
|
35
|
+
import { normalizePatchDisplayName } from '../../utils/validation.js';
|
|
34
36
|
import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
|
|
35
37
|
import { runPatchLint } from '../export-shared.js';
|
|
36
38
|
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, mergeStagedForwardImports, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
|
|
@@ -156,7 +158,7 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
|
|
|
156
158
|
}
|
|
157
159
|
throw error;
|
|
158
160
|
}
|
|
159
|
-
});
|
|
161
|
+
}, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch split' });
|
|
160
162
|
}
|
|
161
163
|
/**
|
|
162
164
|
* Runs the `patch split` command: plans the split, lints the projection,
|
|
@@ -195,6 +197,13 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
195
197
|
(options.order === undefined && options.before === undefined ? source.filename : undefined),
|
|
196
198
|
});
|
|
197
199
|
const placement = await resolvePlacementPlan(paths.patches, placementOptions, category, options.name, config);
|
|
200
|
+
// The filename slug pipeline strips redundant category prefixes; the
|
|
201
|
+
// manifest display name must agree with the bare-slug naming policy,
|
|
202
|
+
// exactly as `export --name` already does (FORGE G13/H4).
|
|
203
|
+
const displayName = normalizePatchDisplayName(options.name, category);
|
|
204
|
+
if (displayName !== options.name) {
|
|
205
|
+
info(`Patch name normalized: "${options.name}" → "${displayName}" (the filename carries the order and category prefix).`);
|
|
206
|
+
}
|
|
198
207
|
const plan = {
|
|
199
208
|
source,
|
|
200
209
|
movedFiles,
|
|
@@ -204,7 +213,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
204
213
|
placement,
|
|
205
214
|
placementOptions,
|
|
206
215
|
category,
|
|
207
|
-
name:
|
|
216
|
+
name: displayName,
|
|
208
217
|
description: options.description ?? '',
|
|
209
218
|
ownerRewrites: findOwnerRewriteHolders(manifest.patches, source.filename, movedSet),
|
|
210
219
|
// Populated by runProjectedSplitLint below (forward edges into the new patch).
|
|
@@ -212,10 +221,14 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
212
221
|
};
|
|
213
222
|
// Per-patch lint both projected bodies, threading the source patch's
|
|
214
223
|
// tier/lintIgnore so an intentional-advisory patch can still split.
|
|
224
|
+
// The whole-queue context (built once, with the config for committed-gate
|
|
225
|
+
// parity) resolves cross-patch imports and sibling head.js harness roots
|
|
226
|
+
// instead of linting each projected body blind (FORGE I3).
|
|
227
|
+
const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
|
|
215
228
|
const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
|
|
216
|
-
await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint,
|
|
217
|
-
await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint,
|
|
218
|
-
const { conflicts, stagedDependencyAdditions } =
|
|
229
|
+
await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
|
|
230
|
+
await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
|
|
231
|
+
const { conflicts, stagedDependencyAdditions } = runProjectedSplitLint(plan, patchQueueCtx);
|
|
219
232
|
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
220
233
|
const newMetadata = buildNewPatchMetadata(plan, config);
|
|
221
234
|
enforcePatchPolicy({
|
|
@@ -253,7 +266,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
253
266
|
*/
|
|
254
267
|
export function registerPatchSplit(parent, context) {
|
|
255
268
|
const { getProjectRoot, withErrorHandling } = context;
|
|
256
|
-
parent
|
|
269
|
+
const command = parent
|
|
257
270
|
.command('split <source>')
|
|
258
271
|
.description('Move files out of a patch into a new patch as one transaction (shrink + create + staged-dependency owner rewrites)')
|
|
259
272
|
.requiredOption('--files <path...>', 'Engine-relative files to move from the source patch to the new patch')
|
|
@@ -272,8 +285,8 @@ export function registerPatchSplit(parent, context) {
|
|
|
272
285
|
.option('--dry-run', 'Show what would happen without writing')
|
|
273
286
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
274
287
|
.option('--force-unsafe', 'Bypass projected-lint refusals')
|
|
275
|
-
.option('--skip-lint', 'Skip per-patch lint of the projected bodies')
|
|
276
|
-
|
|
288
|
+
.option('--skip-lint', 'Skip per-patch lint of the projected bodies');
|
|
289
|
+
addWaitLockOption(command).action(withErrorHandling(async (sourceId, options) => {
|
|
277
290
|
await patchSplitCommand(getProjectRoot(), sourceId, {
|
|
278
291
|
files: options.files,
|
|
279
292
|
name: options.name,
|
|
@@ -287,6 +300,7 @@ export function registerPatchSplit(parent, context) {
|
|
|
287
300
|
yes: options.yes,
|
|
288
301
|
forceUnsafe: options.forceUnsafe,
|
|
289
302
|
skipLint: options.skipLint,
|
|
303
|
+
waitLock: options.waitLock,
|
|
290
304
|
}),
|
|
291
305
|
});
|
|
292
306
|
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PatchMetadata } from '../../types/commands/index.js';
|
|
2
|
+
/** The declaration fields common to both staged-dependency kinds. */
|
|
3
|
+
interface StagedDependencyShape {
|
|
4
|
+
file: string;
|
|
5
|
+
creates: string;
|
|
6
|
+
owner?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validates the `--add` declaration fields against the loaded queue.
|
|
10
|
+
* Throws on a clear flag mixup; warns (only) when `--owner` is
|
|
11
|
+
* well-formed but absent from the queue, because the owner may
|
|
12
|
+
* legitimately be exported moments later and renumbering rewrites owners.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateStagedDependencyAdd(dependency: StagedDependencyShape, queuePatches: readonly PatchMetadata[]): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shape validation for `patch staged-dependency --add` (FORGE K10).
|
|
4
|
+
*
|
|
5
|
+
* `--creates`/`--file` take engine-relative FILE paths and `--owner`
|
|
6
|
+
* takes a patch FILENAME, but all three were accepted as arbitrary
|
|
7
|
+
* strings — a `--creates <patch-name>` mixup produced a declaration
|
|
8
|
+
* nothing could ever discharge, surfacing only later as a
|
|
9
|
+
* `staged-dependency-unused` warning. Refusals here fire only on a clear
|
|
10
|
+
* patch-name shape (slash-free values that end in `.patch` or match a
|
|
11
|
+
* queue patch filename/stem): legitimate engine paths always contain a
|
|
12
|
+
* `/`, so deep paths can never false-positive.
|
|
13
|
+
*/
|
|
14
|
+
import { InvalidArgumentError } from '../../errors/base.js';
|
|
15
|
+
import { warn } from '../../utils/logger.js';
|
|
16
|
+
function matchQueuePatch(value, queuePatches) {
|
|
17
|
+
return queuePatches.find((patch) => patch.filename === value || patch.filename === `${value}.patch`)?.filename;
|
|
18
|
+
}
|
|
19
|
+
function refusePatchShapedPath(flag, value, role, queuePatches) {
|
|
20
|
+
if (value.includes('/'))
|
|
21
|
+
return;
|
|
22
|
+
const matched = matchQueuePatch(value, queuePatches);
|
|
23
|
+
if (matched === undefined && !value.endsWith('.patch'))
|
|
24
|
+
return;
|
|
25
|
+
const evidence = matched !== undefined ? `matches patch ${matched} in the queue` : 'looks like a patch filename';
|
|
26
|
+
throw new InvalidArgumentError(`${flag} takes ${role} (e.g. browser/components/foo/Foo.sys.mjs), not a patch name — ` +
|
|
27
|
+
`"${value}" ${evidence}. If you meant to name the owning patch, pass it with --owner.`, flag);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validates the `--add` declaration fields against the loaded queue.
|
|
31
|
+
* Throws on a clear flag mixup; warns (only) when `--owner` is
|
|
32
|
+
* well-formed but absent from the queue, because the owner may
|
|
33
|
+
* legitimately be exported moments later and renumbering rewrites owners.
|
|
34
|
+
*/
|
|
35
|
+
export function validateStagedDependencyAdd(dependency, queuePatches) {
|
|
36
|
+
refusePatchShapedPath('--creates', dependency.creates, 'the engine-relative path of the file a later patch creates', queuePatches);
|
|
37
|
+
refusePatchShapedPath('--file', dependency.file, 'the engine-relative file containing the declaring site inside this patch', queuePatches);
|
|
38
|
+
const owner = dependency.owner;
|
|
39
|
+
if (owner === undefined)
|
|
40
|
+
return;
|
|
41
|
+
if (owner.includes('/') || !owner.endsWith('.patch')) {
|
|
42
|
+
throw new InvalidArgumentError(`--owner names the owning patch artifact (e.g. 012-infra-foo.patch); ` +
|
|
43
|
+
`"${owner}" does not look like a patch filename. The created file path goes in --creates.`, '--owner');
|
|
44
|
+
}
|
|
45
|
+
if (!queuePatches.some((patch) => patch.filename === owner)) {
|
|
46
|
+
warn(`--owner ${owner} matches no patch currently in the queue. The declaration only ` +
|
|
47
|
+
'discharges once a patch with exactly this filename creates the --creates path.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -10,6 +10,7 @@ import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
|
10
10
|
import { toError } from '../../utils/errors.js';
|
|
11
11
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
12
12
|
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
13
|
+
import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
|
|
13
14
|
/** Joins the names of the missing flags for error attribution (FORGE G12). */
|
|
14
15
|
function missingFlagList(pairs) {
|
|
15
16
|
return pairs
|
|
@@ -231,6 +232,15 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
231
232
|
: mode === 'add'
|
|
232
233
|
? requireRegistrationOptions(options)
|
|
233
234
|
: resolveRegistrationRemovalTarget(options, targetPatch.stagedDependencies);
|
|
235
|
+
// Shape-check --add declarations against the loaded queue (FORGE K10):
|
|
236
|
+
// a patch-name-shaped --creates/--file is refused HERE instead of
|
|
237
|
+
// surfacing later as an undischargeable staged-dependency-unused.
|
|
238
|
+
if (mode === 'add') {
|
|
239
|
+
const added = importDependency ?? registrationDependency;
|
|
240
|
+
if (added !== undefined) {
|
|
241
|
+
validateStagedDependencyAdd(added, manifest.patches);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
234
244
|
const target = importDependency !== undefined
|
|
235
245
|
? importView(importDependency)
|
|
236
246
|
: registrationDependency !== undefined
|
|
@@ -17,16 +17,15 @@ import { isBrandingManagedPath } from '../core/branding.js';
|
|
|
17
17
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
18
18
|
import { getClaimedFiles } from '../core/patch-manifest.js';
|
|
19
19
|
import { isGeneratedBrandingPath } from '../core/status-classify.js';
|
|
20
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
20
21
|
import { pathExists } from '../utils/fs.js';
|
|
21
22
|
import { info, warn } from '../utils/logger.js';
|
|
23
|
+
/** Concurrency bound for existence probes (matches the classify/lint pools). */
|
|
24
|
+
const PATH_PROBE_CONCURRENCY = 8;
|
|
22
25
|
/** Returns the subset of `files` that no longer exist under `engineDir`. */
|
|
23
26
|
export async function findMissingFiles(engineDir, files) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
if (!(await pathExists(join(engineDir, file))))
|
|
27
|
-
missingFiles.push(file);
|
|
28
|
-
}
|
|
29
|
-
return missingFiles;
|
|
27
|
+
const exists = await mapWithConcurrency(files, PATH_PROBE_CONCURRENCY, (file) => pathExists(join(engineDir, file)));
|
|
28
|
+
return files.filter((_, index) => exists[index] !== true);
|
|
30
29
|
}
|
|
31
30
|
function isToolManagedPath(file, isNewFile, ctx) {
|
|
32
31
|
// Mirrors status-classify: a generated branding path is tool-managed
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { PatchesManifest } from '../types/commands/index.js';
|
|
2
2
|
/** Loads and validates a `re-export --scan-files` assignment manifest. */
|
|
3
3
|
export declare function loadScanFilesAssignments(manifestPath: string, manifest: PatchesManifest): Promise<Map<string, string[]>>;
|
|
4
|
+
/**
|
|
5
|
+
* Runtime enforcement of the dry-run purity contract (FORGE H1): fingerprints
|
|
6
|
+
* the engine tree and every patch artifact before and after `operation`, and
|
|
7
|
+
* throws when a dry-run changed anything it promised only to inspect. This
|
|
8
|
+
* turns any recurrence of the 0.40.0 "dry-run reverted a just-written export"
|
|
9
|
+
* incident into a hard, named failure instead of silent data loss discovered
|
|
10
|
+
* only when a later gate fails. The post-fingerprint runs whether `operation`
|
|
11
|
+
* resolves or rejects — a dry-run that mutates state and THEN fails is exactly
|
|
12
|
+
* the case where a rollback defect must not go unreported; a rejection with a
|
|
13
|
+
* purity violation surfaces the violation with the original error as `cause`.
|
|
14
|
+
* No-op outside dry-run.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withDryRunPurityGuard<T>(engineDir: string, patchesDir: string, isDryRun: boolean, operation: () => Promise<T>): Promise<T>;
|
|
4
17
|
/** Serializes dry-run re-export git inspection against the same project tree. */
|
|
5
18
|
export declare function withDryRunReExportLock<T>(fireforgeDir: string, isDryRun: boolean, operation: () => Promise<T>): Promise<T>;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
4
|
import { join } from 'node:path';
|
|
5
|
+
import { isUnavailableGenerationToken, snapshotEngineGeneration, unavailableGenerationReason, } from '../core/engine-session-lock.js';
|
|
3
6
|
import { createSiblingLockPath, withFileLock } from '../core/file-lock.js';
|
|
4
7
|
import { resolvePatchIdentifier } from '../core/patch-manifest.js';
|
|
5
|
-
import { InvalidArgumentError } from '../errors/base.js';
|
|
6
|
-
import {
|
|
8
|
+
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
9
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
10
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
7
11
|
import { readText } from '../utils/fs.js';
|
|
8
12
|
import { normalizeEngineRelativeInput } from './re-export-scan.js';
|
|
13
|
+
/** Concurrency bound for patch-body hashing (matches the classify/lint pools). */
|
|
14
|
+
const PATCH_HASH_CONCURRENCY = 8;
|
|
9
15
|
function isRecord(value) {
|
|
10
16
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
17
|
}
|
|
@@ -66,6 +72,140 @@ export async function loadScanFilesAssignments(manifestPath, manifest) {
|
|
|
66
72
|
}
|
|
67
73
|
return new Map([...filesByPatch.entries()].map(([patchFilename, files]) => [patchFilename, [...files].sort()]));
|
|
68
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Fingerprints every piece of state a `re-export --dry-run` promises not to
|
|
77
|
+
* disturb: the engine's commit + working-tree status (via the same generation
|
|
78
|
+
* token `fireforge test` uses) and the byte content of every regular file
|
|
79
|
+
* under `patches/` — ALL of them, not just the selected patches, because the
|
|
80
|
+
* FORGE H1 field incident was a dry-run of patch B reverting a just-written
|
|
81
|
+
* export of patch A. A missing patches directory (ENOENT/ENOTDIR) fingerprints
|
|
82
|
+
* as empty — there is nothing there to protect — but any other listing failure
|
|
83
|
+
* (EACCES, EIO, …) throws: an unreadable directory is not evidence it is
|
|
84
|
+
* empty, and treating it as such would let the guard silently vouch for state
|
|
85
|
+
* it never saw (fail closed). The same rule covers an unmeasurable engine
|
|
86
|
+
* generation and an unreadable individual patch file; a before-pass failure
|
|
87
|
+
* aborts the dry run before it starts, since a guard that measured nothing
|
|
88
|
+
* cannot vouch for anything.
|
|
89
|
+
*/
|
|
90
|
+
async function fingerprintDryRunState(engineDir, patchesDir) {
|
|
91
|
+
const fingerprint = new Map();
|
|
92
|
+
const generation = await snapshotEngineGeneration(engineDir);
|
|
93
|
+
if (isUnavailableGenerationToken(generation)) {
|
|
94
|
+
// A failed probe measured nothing — hashing the failure token would let
|
|
95
|
+
// two identical failures compare "unchanged" (vouching for unmeasured
|
|
96
|
+
// state) and two differing messages report a spurious violation.
|
|
97
|
+
throw new GeneralError(`[dry-run] cannot fingerprint the engine working tree for the purity guard: ` +
|
|
98
|
+
unavailableGenerationReason(generation));
|
|
99
|
+
}
|
|
100
|
+
fingerprint.set('the engine working tree', createHash('sha256').update(generation).digest('hex'));
|
|
101
|
+
let names = [];
|
|
102
|
+
try {
|
|
103
|
+
const entries = await readdir(patchesDir, { withFileTypes: true });
|
|
104
|
+
names = entries
|
|
105
|
+
.filter((entry) => entry.isFile())
|
|
106
|
+
.map((entry) => entry.name)
|
|
107
|
+
.sort();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// No patches directory — nothing there to protect; the same condition on
|
|
111
|
+
// the post-run pass produces the same empty set, and a directory that
|
|
112
|
+
// appears mid-dry-run correctly reports as a violation. Any OTHER failure
|
|
113
|
+
// must not read as "empty": the guard would be vouching for state it
|
|
114
|
+
// never saw.
|
|
115
|
+
const code = getNodeErrorCode(error);
|
|
116
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
117
|
+
throw new GeneralError(`[dry-run] cannot fingerprint ${patchesDir} for the purity guard: ${toError(error).message}`, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const results = await mapWithConcurrency(names, PATCH_HASH_CONCURRENCY, async (name) => {
|
|
121
|
+
try {
|
|
122
|
+
return {
|
|
123
|
+
hash: createHash('sha256')
|
|
124
|
+
.update(await readFile(join(patchesDir, name)))
|
|
125
|
+
.digest('hex'),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// A file that vanished between the listing and the read is omitted, so
|
|
130
|
+
// a dry run that DELETED a patch artifact reports as the violation it
|
|
131
|
+
// is (key present before, absent after) instead of hiding behind a
|
|
132
|
+
// fingerprinting error. Every other failure fails closed: a constant
|
|
133
|
+
// placeholder would make unreadable-before equal unreadable-after even
|
|
134
|
+
// when the bytes changed underneath.
|
|
135
|
+
const code = getNodeErrorCode(error);
|
|
136
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
137
|
+
return { vanished: true };
|
|
138
|
+
return { error };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
for (const [index, name] of names.entries()) {
|
|
142
|
+
const result = results[index];
|
|
143
|
+
if (result === undefined)
|
|
144
|
+
continue;
|
|
145
|
+
if ('error' in result) {
|
|
146
|
+
throw new GeneralError(`[dry-run] cannot fingerprint ${join(patchesDir, name)} for the purity guard: ${toError(result.error).message}`, result.error);
|
|
147
|
+
}
|
|
148
|
+
if ('vanished' in result)
|
|
149
|
+
continue;
|
|
150
|
+
fingerprint.set(`patches/${name}`, result.hash);
|
|
151
|
+
}
|
|
152
|
+
return fingerprint;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Runtime enforcement of the dry-run purity contract (FORGE H1): fingerprints
|
|
156
|
+
* the engine tree and every patch artifact before and after `operation`, and
|
|
157
|
+
* throws when a dry-run changed anything it promised only to inspect. This
|
|
158
|
+
* turns any recurrence of the 0.40.0 "dry-run reverted a just-written export"
|
|
159
|
+
* incident into a hard, named failure instead of silent data loss discovered
|
|
160
|
+
* only when a later gate fails. The post-fingerprint runs whether `operation`
|
|
161
|
+
* resolves or rejects — a dry-run that mutates state and THEN fails is exactly
|
|
162
|
+
* the case where a rollback defect must not go unreported; a rejection with a
|
|
163
|
+
* purity violation surfaces the violation with the original error as `cause`.
|
|
164
|
+
* No-op outside dry-run.
|
|
165
|
+
*/
|
|
166
|
+
export async function withDryRunPurityGuard(engineDir, patchesDir, isDryRun, operation) {
|
|
167
|
+
if (!isDryRun)
|
|
168
|
+
return operation();
|
|
169
|
+
const before = await fingerprintDryRunState(engineDir, patchesDir);
|
|
170
|
+
let result;
|
|
171
|
+
let operationError;
|
|
172
|
+
let failed = false;
|
|
173
|
+
try {
|
|
174
|
+
result = await operation();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
failed = true;
|
|
178
|
+
operationError = error;
|
|
179
|
+
}
|
|
180
|
+
let after;
|
|
181
|
+
try {
|
|
182
|
+
after = await fingerprintDryRunState(engineDir, patchesDir);
|
|
183
|
+
}
|
|
184
|
+
catch (fingerprintError) {
|
|
185
|
+
// The post-run fingerprint failing must not swallow the dry run's own
|
|
186
|
+
// failure — an operation that mutated state, broke it, AND made it
|
|
187
|
+
// unreadable is exactly the case where both facts matter. The combined
|
|
188
|
+
// message carries both; the operation error stays the `cause`.
|
|
189
|
+
if (failed) {
|
|
190
|
+
throw new GeneralError(`[dry-run] this dry run failed (${toError(operationError).message}), and its purity could ` +
|
|
191
|
+
`not be verified afterwards: ${toError(fingerprintError).message}. Inspect engine/ and ` +
|
|
192
|
+
'your patches/ diff before trusting them.', operationError);
|
|
193
|
+
}
|
|
194
|
+
throw fingerprintError;
|
|
195
|
+
}
|
|
196
|
+
const touched = [...new Set([...before.keys(), ...after.keys()])]
|
|
197
|
+
.sort()
|
|
198
|
+
.filter((key) => before.get(key) !== after.get(key));
|
|
199
|
+
if (touched.length > 0) {
|
|
200
|
+
throw new GeneralError(`[dry-run] invariant violated: this dry run modified ${touched.join(', ')}. ` +
|
|
201
|
+
'This is a FireForge bug — please report it, and inspect the listed state ' +
|
|
202
|
+
'(e.g. `git status` in engine/ and your patches/ diff) before trusting it.' +
|
|
203
|
+
(failed ? ` The dry run also failed: ${toError(operationError).message}` : ''), operationError);
|
|
204
|
+
}
|
|
205
|
+
if (failed)
|
|
206
|
+
throw operationError;
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
69
209
|
/** Serializes dry-run re-export git inspection against the same project tree. */
|
|
70
210
|
export async function withDryRunReExportLock(fireforgeDir, isDryRun, operation) {
|
|
71
211
|
if (!isDryRun)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export`
|
|
3
|
+
* (FORGE J2).
|
|
4
|
+
*
|
|
5
|
+
* A scan-less re-export rebuilds each patch body as a whole-file
|
|
6
|
+
* `git diff HEAD` over the owned files — so a CONCURRENT session's
|
|
7
|
+
* uncommitted lines inside a file the patch already owns are silently
|
|
8
|
+
* absorbed into the refreshed body. The adjacency advisory
|
|
9
|
+
* (`re-export-adjacent.ts`) warns about unmanaged NEIGHBOUR files but is
|
|
10
|
+
* structurally blind to drift inside owned files. This module compares
|
|
11
|
+
* the old body against the refreshed body per file and reports the
|
|
12
|
+
* payload lines that are about to ENTER the body; under
|
|
13
|
+
* `--refuse-foreign-drift` the patch is skipped and the run exits
|
|
14
|
+
* non-zero, mirroring `--refuse-adjacent-unmanaged`.
|
|
15
|
+
*
|
|
16
|
+
* "Foreign" is decided by content, not authorship: re-export's legitimate
|
|
17
|
+
* job is capturing intentional engine edits, so the preview always prints
|
|
18
|
+
* and the hard stop is an explicit opt-in for multi-session checkouts.
|
|
19
|
+
* The comparison is offset-insensitive — per-file MULTISETS of `+`/`-`
|
|
20
|
+
* payload lines — because a refresh legitimately shifts hunk offsets and
|
|
21
|
+
* context without changing what the patch does.
|
|
22
|
+
*/
|
|
23
|
+
import type { PatchMetadata } from '../types/commands/index.js';
|
|
24
|
+
/** Why a patch was refused under `--refuse-foreign-drift`. */
|
|
25
|
+
export type ForeignDriftRefusalReason = 'foreign-drift' | 'baseline-unreadable';
|
|
26
|
+
/** Per-run context threaded through the re-export loop (mirrors FORGE G2). */
|
|
27
|
+
export interface ForeignDriftContext {
|
|
28
|
+
refuseForeignDrift: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Engine-relative files whose drift is expected (`--expect`, FORGE L6):
|
|
31
|
+
* drift confined to these files does not refuse. Files here that never
|
|
32
|
+
* drift are reported after the run as likely typos.
|
|
33
|
+
*/
|
|
34
|
+
expectedDriftFiles: ReadonlySet<string>;
|
|
35
|
+
/** Expected files that actually drifted at least once this run. */
|
|
36
|
+
expectedSeen: Set<string>;
|
|
37
|
+
/** Number of patches whose old/new bodies reached the drift comparison. */
|
|
38
|
+
evaluationRuns: number;
|
|
39
|
+
refusals: {
|
|
40
|
+
patchFilename: string;
|
|
41
|
+
files: string[];
|
|
42
|
+
reason: ForeignDriftRefusalReason;
|
|
43
|
+
}[];
|
|
44
|
+
}
|
|
45
|
+
/** One owned file's foreign drift between the old and refreshed body. */
|
|
46
|
+
export interface FileForeignDrift {
|
|
47
|
+
file: string;
|
|
48
|
+
/** `+` payload lines in the new body not present in the old body. */
|
|
49
|
+
addedLines: number;
|
|
50
|
+
/** `-` payload lines in the new body not present in the old body. */
|
|
51
|
+
removedLines: number;
|
|
52
|
+
/** `@@` headers of new-body hunks carrying foreign lines (capped). */
|
|
53
|
+
hunkSummaries: string[];
|
|
54
|
+
/** True when a binary section's recorded new-side blob hash changed. */
|
|
55
|
+
binaryChanged: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pure comparison: per owned file, which `+`/`-` payload lines the
|
|
59
|
+
* refreshed body carries that the old body did not. Files NOT in
|
|
60
|
+
* `previousFilesAffected` are skipped — those are intentional adoptions
|
|
61
|
+
* (`--scan`), not drift.
|
|
62
|
+
*/
|
|
63
|
+
export declare function computeForeignDrift(oldBody: string, newBody: string, previousFilesAffected: readonly string[]): FileForeignDrift[];
|
|
64
|
+
/**
|
|
65
|
+
* Reads the old body, prints the always-on drift preview (including under
|
|
66
|
+
* dry-run and `--scan`), and — when `ctx.refuseForeignDrift` is set —
|
|
67
|
+
* records a refusal and returns `true` so the caller skips the write.
|
|
68
|
+
*/
|
|
69
|
+
export declare function reportForeignDrift(args: {
|
|
70
|
+
patch: PatchMetadata;
|
|
71
|
+
patchesDir: string;
|
|
72
|
+
newDiffContent: string;
|
|
73
|
+
ctx: ForeignDriftContext;
|
|
74
|
+
}): Promise<boolean>;
|