@hominis/fireforge 0.44.0 → 0.44.2
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 +65 -2
- package/README.md +20 -121
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build.js +17 -0
- package/dist/src/commands/doctor-check-core.d.ts +9 -0
- package/dist/src/commands/doctor-patch-manifest.d.ts +8 -0
- package/dist/src/commands/doctor-patch-manifest.js +155 -0
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +48 -62
- package/dist/src/commands/furnace/apply.js +2 -2
- package/dist/src/commands/furnace/deploy.js +2 -2
- package/dist/src/commands/furnace/diff.js +6 -1
- package/dist/src/commands/furnace/index.js +12 -6
- package/dist/src/commands/furnace/init.js +5 -2
- package/dist/src/commands/furnace/remove-mutations.js +5 -1
- package/dist/src/commands/furnace/sync.js +2 -2
- package/dist/src/commands/import.js +6 -2
- package/dist/src/commands/re-export-files.js +63 -23
- package/dist/src/commands/re-export-scan.js +18 -2
- package/dist/src/commands/re-export-single.js +19 -7
- package/dist/src/commands/test-register.js +5 -1
- package/dist/src/commands/test-run.d.ts +5 -0
- package/dist/src/commands/test-run.js +11 -4
- package/dist/src/commands/test-verdict.js +10 -1
- package/dist/src/commands/test.js +9 -0
- package/dist/src/commands/token.js +33 -11
- package/dist/src/core/build-audit-resolve.js +12 -3
- package/dist/src/core/build-audit.js +6 -2
- package/dist/src/core/config-mutate.js +20 -19
- package/dist/src/core/furnace-apply-ftl.js +4 -1
- package/dist/src/core/furnace-apply-helpers.js +11 -5
- package/dist/src/core/furnace-jsconfig.js +5 -3
- package/dist/src/core/furnace-operation.d.ts +25 -2
- package/dist/src/core/furnace-operation.js +27 -2
- package/dist/src/core/git-base.d.ts +14 -0
- package/dist/src/core/git-base.js +17 -0
- package/dist/src/core/git.js +3 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +17 -0
- package/dist/src/core/mach-known-noise-filter.js +19 -0
- package/dist/src/core/mach.d.ts +8 -3
- package/dist/src/core/mach.js +47 -3
- package/dist/src/core/patch-lint-checkjs.js +25 -4
- package/dist/src/core/patch-manifest-consistency.d.ts +56 -6
- package/dist/src/core/patch-manifest-consistency.js +96 -23
- package/dist/src/core/patch-manifest-files-affected.d.ts +43 -0
- package/dist/src/core/patch-manifest-files-affected.js +77 -0
- package/dist/src/core/patch-manifest.d.ts +2 -1
- package/dist/src/core/patch-manifest.js +2 -1
- package/dist/src/core/run-log.d.ts +60 -0
- package/dist/src/core/run-log.js +183 -0
- package/dist/src/core/test-harness-output.d.ts +24 -1
- package/dist/src/core/test-harness-output.js +46 -15
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -2
- package/dist/src/core/test-xpcshell-retry.js +6 -1
- package/dist/src/core/token-category.js +61 -7
- package/dist/src/core/token-dark-mode.js +20 -2
- package/dist/src/core/token-docs.js +1 -1
- package/dist/src/core/token-manager.d.ts +7 -2
- package/dist/src/core/token-manager.js +74 -20
- package/dist/src/core/token-variant.d.ts +34 -0
- package/dist/src/core/token-variant.js +98 -8
- package/dist/src/core/tree-store.js +5 -1
- package/dist/src/core/typecheck.js +7 -1
- package/dist/src/types/commands/options.d.ts +53 -1
- package/dist/src/utils/child-env.d.ts +30 -0
- package/dist/src/utils/child-env.js +30 -0
- package/dist/src/utils/options.d.ts +28 -0
- package/dist/src/utils/options.js +32 -1
- package/dist/src/utils/process.d.ts +22 -0
- package/dist/src/utils/process.js +8 -7
- package/package.json +2 -2
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface MachOptions {
|
|
|
25
25
|
* the test dispatchers only.
|
|
26
26
|
*/
|
|
27
27
|
annotateKnownTeardownNoise?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Environment variables to unset for this dispatch. Threaded to the exec
|
|
30
|
+
* layer's {@link ExecOptions.envUnset}.
|
|
31
|
+
*/
|
|
32
|
+
envUnset?: readonly string[];
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Result of running a mach command while capturing streamed output.
|
|
@@ -243,7 +248,7 @@ export declare function test(engineDir: string, testPaths?: string[], args?: str
|
|
|
243
248
|
* (merged over `process.env` by the exec layer). Used by
|
|
244
249
|
* `fireforge test --perf-samples` to publish the artifact-path contract.
|
|
245
250
|
*/
|
|
246
|
-
export declare function testWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string
|
|
251
|
+
export declare function testWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string>, fullOutput?: boolean): Promise<MachCommandResult>;
|
|
247
252
|
/**
|
|
248
253
|
* Runs `mach xpcshell-test` (the suite-specific xpcshell command) while
|
|
249
254
|
* capturing output. Unlike the generic `mach test`, the suite-specific
|
|
@@ -254,10 +259,10 @@ export declare function testWithOutput(engineDir: string, testPaths?: string[],
|
|
|
254
259
|
* Signature mirrors {@link testWithOutput} so the two are interchangeable in
|
|
255
260
|
* the dispatch path.
|
|
256
261
|
*/
|
|
257
|
-
export declare function xpcshellTestWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string
|
|
262
|
+
export declare function xpcshellTestWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string>, fullOutput?: boolean): Promise<MachCommandResult>;
|
|
258
263
|
/**
|
|
259
264
|
* Runs `mach mochitest` (covers browser-chrome / mochitest flavors) while
|
|
260
265
|
* capturing output. The suite-specific counterpart to {@link testWithOutput}
|
|
261
266
|
* for non-xpcshell single-suite runs — see {@link xpcshellTestWithOutput}.
|
|
262
267
|
*/
|
|
263
|
-
export declare function mochitestWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string
|
|
268
|
+
export declare function mochitestWithOutput(engineDir: string, testPaths?: string[], args?: string[], env?: Record<string, string>, fullOutput?: boolean): Promise<MachCommandResult>;
|
package/dist/src/core/mach.js
CHANGED
|
@@ -10,6 +10,7 @@ import { explainMachError } from './mach-error-hints.js';
|
|
|
10
10
|
import { createKnownTeardownNoiseFilter, createTeardownNoiseContext, } from './mach-known-noise-filter.js';
|
|
11
11
|
import { getPython } from './mach-python.js';
|
|
12
12
|
import { installMachResourceGuard } from './mach-resource-shim.js';
|
|
13
|
+
import { teeToRunLog, writeToActiveRunLog } from './run-log.js';
|
|
13
14
|
import { detectHarnessCrashSignature } from './test-harness-crash.js';
|
|
14
15
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
15
16
|
// This block is a frozen compatibility list, not a barrel to grow: every name
|
|
@@ -90,6 +91,7 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
90
91
|
const exitCode = await execStream(python, [machPath, ...args], {
|
|
91
92
|
cwd: engineDir,
|
|
92
93
|
...(options.env ? { env: options.env } : {}),
|
|
94
|
+
...(options.envUnset ? { envUnset: options.envUnset } : {}),
|
|
93
95
|
// Every capture dispatch (test suites, protected builds, package,
|
|
94
96
|
// storybook) runs as a process-group leader and is group-reaped on
|
|
95
97
|
// exit/abort — see ExecOptions.processGroup.
|
|
@@ -99,6 +101,12 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
99
101
|
if (stdout.length > CAPTURE_TAIL_LIMIT) {
|
|
100
102
|
stdout = stdout.slice(-CAPTURE_TAIL_LIMIT);
|
|
101
103
|
}
|
|
104
|
+
// RAW into the log, filtered into the echo. The log exists to be
|
|
105
|
+
// re-read after the fact by whoever is diagnosing the run, and the
|
|
106
|
+
// echo filter's whole purpose is to shorten what a HUMAN scrolls
|
|
107
|
+
// past — collapsing a traceback in the artifact would reintroduce
|
|
108
|
+
// the loss this log exists to prevent.
|
|
109
|
+
writeToActiveRunLog(data);
|
|
102
110
|
process.stdout.write(stdoutFilter ? stdoutFilter.transform(data) : data);
|
|
103
111
|
},
|
|
104
112
|
onStderr: (data) => {
|
|
@@ -106,6 +114,7 @@ export async function runMachCapture(args, engineDir, options = {}) {
|
|
|
106
114
|
if (stderr.length > CAPTURE_TAIL_LIMIT) {
|
|
107
115
|
stderr = stderr.slice(-CAPTURE_TAIL_LIMIT);
|
|
108
116
|
}
|
|
117
|
+
writeToActiveRunLog(data);
|
|
109
118
|
process.stderr.write(stderrFilter ? stderrFilter.transform(data) : data);
|
|
110
119
|
},
|
|
111
120
|
});
|
|
@@ -133,6 +142,10 @@ export async function runMachInheritCapture(args, engineDir, options = {}) {
|
|
|
133
142
|
return execInheritCapture(python, [machPath, ...args], {
|
|
134
143
|
cwd: engineDir,
|
|
135
144
|
...(options.env ? { env: options.env } : {}),
|
|
145
|
+
...(options.envUnset ? { envUnset: options.envUnset } : {}),
|
|
146
|
+
// This path streams through the collectors' mirror rather than per-chunk
|
|
147
|
+
// callbacks, so the run-log tee rides the mirror. `mach build` is here.
|
|
148
|
+
mirror: { stdout: teeToRunLog(process.stdout), stderr: teeToRunLog(process.stderr) },
|
|
136
149
|
processGroup: true,
|
|
137
150
|
});
|
|
138
151
|
}
|
|
@@ -378,6 +391,31 @@ export async function watchWithOutput(engineDir, options = {}) {
|
|
|
378
391
|
export async function test(engineDir, testPaths = [], args = []) {
|
|
379
392
|
return runMach(['test', ...testPaths, ...args], engineDir, { inherit: true });
|
|
380
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Environment markers mozbuild reads to decide it is running under a coding
|
|
396
|
+
* agent (`mozbuild.util.is_running_under_coding_agent`), which quiets
|
|
397
|
+
* terminal output to warnings and errors.
|
|
398
|
+
*
|
|
399
|
+
* The quieting is genuinely useful for BUILDS — that is most of what an
|
|
400
|
+
* agent session prints — but it applies to test runs too, where it removes
|
|
401
|
+
* `TEST_START` and console INFO: exactly the lines a hang or stall
|
|
402
|
+
* diagnosis needs, and the ones FireForge's own classifier reads
|
|
403
|
+
* (`Ran N checks`, `Unexpected results:`, `TEST-UNEXPECTED-*`). Unsetting
|
|
404
|
+
* the variable globally would give up the build half as well, so the
|
|
405
|
+
* dispatchers below unset it per TEST dispatch and leave the build path
|
|
406
|
+
* alone.
|
|
407
|
+
*/
|
|
408
|
+
const CODING_AGENT_ENV_MARKERS = ['CLAUDECODE'];
|
|
409
|
+
/**
|
|
410
|
+
* The `envUnset` list for a test dispatch: the coding-agent markers when
|
|
411
|
+
* full verbosity was requested, nothing otherwise.
|
|
412
|
+
*
|
|
413
|
+
* Opt-in rather than unconditional: this changes how much a third party
|
|
414
|
+
* prints, and an operator who wants mozbuild's quieting should keep it.
|
|
415
|
+
*/
|
|
416
|
+
function testVerbosityEnvUnset(fullOutput) {
|
|
417
|
+
return fullOutput === true ? CODING_AGENT_ENV_MARKERS : undefined;
|
|
418
|
+
}
|
|
381
419
|
/**
|
|
382
420
|
* Runs mach test while capturing streamed output for better diagnostics.
|
|
383
421
|
*
|
|
@@ -388,10 +426,12 @@ export async function test(engineDir, testPaths = [], args = []) {
|
|
|
388
426
|
* (merged over `process.env` by the exec layer). Used by
|
|
389
427
|
* `fireforge test --perf-samples` to publish the artifact-path contract.
|
|
390
428
|
*/
|
|
391
|
-
export async function testWithOutput(engineDir, testPaths = [], args = [], env) {
|
|
429
|
+
export async function testWithOutput(engineDir, testPaths = [], args = [], env, fullOutput) {
|
|
392
430
|
const guard = await installMachResourceGuard(engineDir);
|
|
431
|
+
const envUnset = testVerbosityEnvUnset(fullOutput);
|
|
393
432
|
return runMachCapture(['test', ...testPaths, ...args], engineDir, {
|
|
394
433
|
env: { ...guard.env, ...env },
|
|
434
|
+
...(envUnset ? { envUnset } : {}),
|
|
395
435
|
annotateKnownTeardownNoise: true,
|
|
396
436
|
});
|
|
397
437
|
}
|
|
@@ -405,10 +445,12 @@ export async function testWithOutput(engineDir, testPaths = [], args = [], env)
|
|
|
405
445
|
* Signature mirrors {@link testWithOutput} so the two are interchangeable in
|
|
406
446
|
* the dispatch path.
|
|
407
447
|
*/
|
|
408
|
-
export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [], env) {
|
|
448
|
+
export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [], env, fullOutput) {
|
|
409
449
|
const guard = await installMachResourceGuard(engineDir);
|
|
450
|
+
const envUnset = testVerbosityEnvUnset(fullOutput);
|
|
410
451
|
return runMachCapture(['xpcshell-test', ...testPaths, ...args], engineDir, {
|
|
411
452
|
env: { ...guard.env, ...env },
|
|
453
|
+
...(envUnset ? { envUnset } : {}),
|
|
412
454
|
annotateKnownTeardownNoise: true,
|
|
413
455
|
});
|
|
414
456
|
}
|
|
@@ -417,10 +459,12 @@ export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [
|
|
|
417
459
|
* capturing output. The suite-specific counterpart to {@link testWithOutput}
|
|
418
460
|
* for non-xpcshell single-suite runs — see {@link xpcshellTestWithOutput}.
|
|
419
461
|
*/
|
|
420
|
-
export async function mochitestWithOutput(engineDir, testPaths = [], args = [], env) {
|
|
462
|
+
export async function mochitestWithOutput(engineDir, testPaths = [], args = [], env, fullOutput) {
|
|
421
463
|
const guard = await installMachResourceGuard(engineDir);
|
|
464
|
+
const envUnset = testVerbosityEnvUnset(fullOutput);
|
|
422
465
|
return runMachCapture(['mochitest', ...testPaths, ...args], engineDir, {
|
|
423
466
|
env: { ...guard.env, ...env },
|
|
467
|
+
...(envUnset ? { envUnset } : {}),
|
|
424
468
|
annotateKnownTeardownNoise: true,
|
|
425
469
|
});
|
|
426
470
|
}
|
|
@@ -29,10 +29,25 @@ import { basename, resolve } from 'node:path';
|
|
|
29
29
|
import { toError } from '../utils/errors.js';
|
|
30
30
|
import { pathExists } from '../utils/fs.js';
|
|
31
31
|
import { verbose } from '../utils/logger.js';
|
|
32
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
32
33
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Public API
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* Converts a native path into the form TypeScript uses for `fileName`.
|
|
39
|
+
*
|
|
40
|
+
* TypeScript normalizes every path it hands a `CompilerHost` to forward
|
|
41
|
+
* slashes on ALL platforms, while `resolve()` here yields the platform
|
|
42
|
+
* separator. On Windows that mismatch made every `ownedAbsolute.has(fileName)`
|
|
43
|
+
* and `relByAbsolute.get(fileName)` lookup miss, so the host served an empty
|
|
44
|
+
* source file for each owned file and the whole pass reported zero findings —
|
|
45
|
+
* silently, because "no diagnostics" is indistinguishable from "clean code".
|
|
46
|
+
* Every path that crosses the TS boundary goes through here.
|
|
47
|
+
*/
|
|
48
|
+
function toTsPath(path) {
|
|
49
|
+
return normalizePathSlashes(path);
|
|
50
|
+
}
|
|
36
51
|
/**
|
|
37
52
|
* Builds the host-side module resolver for the checkJs pass: maps an import
|
|
38
53
|
* specifier to a patch-owned absolute path when the specifier's final
|
|
@@ -117,8 +132,14 @@ function createPathsResolver(ts, paths, baseDir, fileExists, onResolved) {
|
|
|
117
132
|
captured = specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
118
133
|
}
|
|
119
134
|
for (const target of targets) {
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
// A `paths` target carries at most one `*`, and TypeScript substitutes
|
|
136
|
+
// that one wildcard — spelled as index arithmetic (mirroring the
|
|
137
|
+
// prefix/suffix split on the pattern side above) rather than
|
|
138
|
+
// `replace('*', …)`, whose first-occurrence-only behaviour reads as an
|
|
139
|
+
// incomplete rewrite (CodeQL `js/incomplete-sanitization`).
|
|
140
|
+
const star = target.indexOf('*');
|
|
141
|
+
const rel = star === -1 ? target : target.slice(0, star) + captured + target.slice(star + 1);
|
|
142
|
+
const abs = toTsPath(resolve(baseDir, rel));
|
|
122
143
|
if (!fileExists(abs))
|
|
123
144
|
continue;
|
|
124
145
|
onResolved(abs);
|
|
@@ -188,7 +209,7 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
|
|
|
188
209
|
const ownedAbsolute = new Set();
|
|
189
210
|
const relByAbsolute = new Map();
|
|
190
211
|
for (const rel of resolutionOwned) {
|
|
191
|
-
const abs = resolve(repoDir, rel);
|
|
212
|
+
const abs = toTsPath(resolve(repoDir, rel));
|
|
192
213
|
if (await pathExists(abs)) {
|
|
193
214
|
if (rootScope === undefined || rootScope.has(rel))
|
|
194
215
|
rootFiles.push(abs);
|
|
@@ -228,7 +249,7 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
|
|
|
228
249
|
],
|
|
229
250
|
};
|
|
230
251
|
}
|
|
231
|
-
const shimPath = resolve(repoDir, SHIM_FILENAME);
|
|
252
|
+
const shimPath = toTsPath(resolve(repoDir, SHIM_FILENAME));
|
|
232
253
|
rootFiles.push(shimPath);
|
|
233
254
|
const strict = mode?.strict === true;
|
|
234
255
|
const strictness = strict
|
|
@@ -15,6 +15,16 @@ export interface PatchManifestConsistencyIssue {
|
|
|
15
15
|
* @returns Consistency issues between manifest metadata and on-disk patch files
|
|
16
16
|
*/
|
|
17
17
|
export declare function validatePatchesManifestConsistency(patchesDir: string): Promise<PatchManifestConsistencyIssue[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Recovery hint for an unrepaired consistency failure.
|
|
20
|
+
*
|
|
21
|
+
* Drift that is only in `filesAffected` is a derived value disagreeing with
|
|
22
|
+
* the diff it describes, and the narrow repair fixes exactly that. Naming the
|
|
23
|
+
* whole-manifest rebuild for it — as this hint used to, unconditionally — puts
|
|
24
|
+
* an operator one keystroke from rewriting every row in the manifest to
|
|
25
|
+
* correct one list.
|
|
26
|
+
*/
|
|
27
|
+
export declare function recommendManifestRepair(issues: readonly PatchManifestConsistencyIssue[]): string;
|
|
18
28
|
/**
|
|
19
29
|
* Summary of a {@link rebuildPatchesManifest} run. `recoveredFilenames`
|
|
20
30
|
* lists patches whose manifest entry was reconstructed from filename,
|
|
@@ -25,21 +35,61 @@ export declare function validatePatchesManifestConsistency(patchesDir: string):
|
|
|
25
35
|
* invented vs which was restored.
|
|
26
36
|
*/
|
|
27
37
|
export interface RebuildPatchesManifestResult {
|
|
28
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Rebuilt manifest, ready to be re-applied by callers. Persisted unless
|
|
40
|
+
* the run was a dry run — see {@link RebuildPatchesManifestResult.written}.
|
|
41
|
+
*/
|
|
29
42
|
manifest: PatchesManifest;
|
|
30
43
|
/**
|
|
31
44
|
* Filenames whose manifest entry had no pre-existing metadata to
|
|
32
45
|
* preserve — every descriptive field on these entries is inferred.
|
|
33
46
|
*/
|
|
34
47
|
recoveredFilenames: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Filenames that had a manifest entry but no patch file on disk, so the
|
|
50
|
+
* row was dropped. Discarding these silently is how a manifest quietly
|
|
51
|
+
* loses rows an operator still expected to be there.
|
|
52
|
+
*/
|
|
53
|
+
droppedFilenames: string[];
|
|
54
|
+
/** False when the run was a dry run and patches.json was left untouched. */
|
|
55
|
+
written: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** Options for {@link rebuildPatchesManifest}. */
|
|
58
|
+
export interface RebuildPatchesManifestOptions extends PatchDirectoryLockOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Proceed even when patches.json exists but cannot be parsed, in which case
|
|
61
|
+
* there is no existing metadata to preserve and every descriptive field on
|
|
62
|
+
* every entry is invented. Refused without this flag — see
|
|
63
|
+
* {@link rebuildPatchesManifest}.
|
|
64
|
+
*/
|
|
65
|
+
allowMetadataLoss?: boolean;
|
|
66
|
+
/** Compute the rebuilt manifest without writing it. */
|
|
67
|
+
dryRun?: boolean;
|
|
35
68
|
}
|
|
36
69
|
/**
|
|
37
70
|
* Rebuilds patches.json from the patch files currently present on disk.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
71
|
+
*
|
|
72
|
+
* This is a MERGE, not a from-scratch rebuild: only `filesAffected` and
|
|
73
|
+
* `order` are recomputed from the patch files, and every other field on an
|
|
74
|
+
* existing entry is carried forward untouched. Entries with no pre-existing
|
|
75
|
+
* metadata are recovered from filename structure, patch contents, and file
|
|
76
|
+
* mtimes.
|
|
77
|
+
*
|
|
78
|
+
* Refuses when patches.json exists but cannot be parsed, because that is the
|
|
79
|
+
* one case where nothing can be carried forward and the "merge" silently
|
|
80
|
+
* becomes a whole-manifest reinvention. Pass `allowMetadataLoss` to accept it.
|
|
81
|
+
*
|
|
40
82
|
* @param patchesDir - Path to the patches directory
|
|
41
83
|
* @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
|
|
42
|
-
* @
|
|
43
|
-
*
|
|
84
|
+
* @param options - Lock options plus `allowMetadataLoss` / `dryRun`
|
|
85
|
+
* @returns {@link RebuildPatchesManifestResult} — the rebuilt manifest, the
|
|
86
|
+
* filenames that were reconstructed from generic defaults, and the rows
|
|
87
|
+
* dropped because their patch file is gone.
|
|
88
|
+
*/
|
|
89
|
+
export declare function rebuildPatchesManifest(patchesDir: string, fallbackSourceEsrVersion: string, options?: RebuildPatchesManifestOptions): Promise<RebuildPatchesManifestResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Canonical form of a `filesAffected` list: deduplicated and sorted, so a
|
|
92
|
+
* comparison between a declared list and one extracted from a patch body is
|
|
93
|
+
* about membership rather than ordering.
|
|
44
94
|
*/
|
|
45
|
-
export declare function
|
|
95
|
+
export declare function normalizeAffectedFiles(files: string[]): string[];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Manifest consistency checks and rebuild/recovery operations.
|
|
4
4
|
*/
|
|
5
5
|
import { stat } from 'node:fs/promises';
|
|
6
|
+
import { GeneralError } from '../errors/base.js';
|
|
6
7
|
import { discoverPatches, getAllTargetFilesFromPatch } from './patch-files.js';
|
|
7
8
|
import { withPatchDirectoryLock } from './patch-lock.js';
|
|
8
9
|
import { loadPatchesManifestState, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest-io.js';
|
|
@@ -59,8 +60,8 @@ export async function validatePatchesManifestConsistency(patchesDir) {
|
|
|
59
60
|
});
|
|
60
61
|
continue;
|
|
61
62
|
}
|
|
62
|
-
const declaredFiles =
|
|
63
|
-
const actualFiles =
|
|
63
|
+
const declaredFiles = normalizeAffectedFiles(metadata.filesAffected);
|
|
64
|
+
const actualFiles = normalizeAffectedFiles(await getAllTargetFilesFromPatch(patch.path));
|
|
64
65
|
if (!sameStringArray(declaredFiles, actualFiles)) {
|
|
65
66
|
issues.push({
|
|
66
67
|
code: 'files-affected-mismatch',
|
|
@@ -80,26 +81,78 @@ export async function validatePatchesManifestConsistency(patchesDir) {
|
|
|
80
81
|
}
|
|
81
82
|
return issues;
|
|
82
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Recovery hint for an unrepaired consistency failure.
|
|
86
|
+
*
|
|
87
|
+
* Drift that is only in `filesAffected` is a derived value disagreeing with
|
|
88
|
+
* the diff it describes, and the narrow repair fixes exactly that. Naming the
|
|
89
|
+
* whole-manifest rebuild for it — as this hint used to, unconditionally — puts
|
|
90
|
+
* an operator one keystroke from rewriting every row in the manifest to
|
|
91
|
+
* correct one list.
|
|
92
|
+
*/
|
|
93
|
+
export function recommendManifestRepair(issues) {
|
|
94
|
+
const filesAffectedOnly = issues.length > 0 && issues.every((issue) => issue.code === 'files-affected-mismatch');
|
|
95
|
+
return filesAffectedOnly
|
|
96
|
+
? 'Run "fireforge doctor --repair-files-affected" to recompute filesAffected from the patch ' +
|
|
97
|
+
'bodies (add --dry-run to preview). It rewrites only the drifted lists and leaves every ' +
|
|
98
|
+
'other manifest field untouched.'
|
|
99
|
+
: 'Run "fireforge doctor --repair-patches-manifest" to rebuild patches.json from the patch ' +
|
|
100
|
+
'files (add --dry-run to preview). Only filesAffected and order are recomputed; every ' +
|
|
101
|
+
'other field on an existing entry is preserved.';
|
|
102
|
+
}
|
|
83
103
|
/**
|
|
84
104
|
* Rebuilds patches.json from the patch files currently present on disk.
|
|
85
|
-
*
|
|
86
|
-
*
|
|
105
|
+
*
|
|
106
|
+
* This is a MERGE, not a from-scratch rebuild: only `filesAffected` and
|
|
107
|
+
* `order` are recomputed from the patch files, and every other field on an
|
|
108
|
+
* existing entry is carried forward untouched. Entries with no pre-existing
|
|
109
|
+
* metadata are recovered from filename structure, patch contents, and file
|
|
110
|
+
* mtimes.
|
|
111
|
+
*
|
|
112
|
+
* Refuses when patches.json exists but cannot be parsed, because that is the
|
|
113
|
+
* one case where nothing can be carried forward and the "merge" silently
|
|
114
|
+
* becomes a whole-manifest reinvention. Pass `allowMetadataLoss` to accept it.
|
|
115
|
+
*
|
|
87
116
|
* @param patchesDir - Path to the patches directory
|
|
88
117
|
* @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
|
|
89
|
-
* @
|
|
90
|
-
*
|
|
118
|
+
* @param options - Lock options plus `allowMetadataLoss` / `dryRun`
|
|
119
|
+
* @returns {@link RebuildPatchesManifestResult} — the rebuilt manifest, the
|
|
120
|
+
* filenames that were reconstructed from generic defaults, and the rows
|
|
121
|
+
* dropped because their patch file is gone.
|
|
91
122
|
*/
|
|
92
|
-
export async function rebuildPatchesManifest(patchesDir, fallbackSourceEsrVersion,
|
|
123
|
+
export async function rebuildPatchesManifest(patchesDir, fallbackSourceEsrVersion, options = {}) {
|
|
93
124
|
// The whole read → discover → rebuild → save cycle runs under the shared
|
|
94
125
|
// patch-directory lock (invariant 2, docs/lifecycle-invariants.md):
|
|
95
126
|
// manifest writes serialize on this lock, so without it a repair racing a
|
|
96
127
|
// concurrent export/reorder can clobber the other writer's manifest. Not
|
|
97
128
|
// reentrant — callers must not already hold it.
|
|
98
|
-
|
|
129
|
+
//
|
|
130
|
+
// A dry run takes the lock too: it reads the same manifest and patch bodies
|
|
131
|
+
// a real run would, and a projection computed against a queue being
|
|
132
|
+
// rewritten underneath is worth less than no projection at all.
|
|
133
|
+
return withPatchDirectoryLock(patchesDir, () => rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVersion, options), options);
|
|
99
134
|
}
|
|
100
|
-
async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVersion) {
|
|
135
|
+
async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVersion, options) {
|
|
101
136
|
const manifestState = await loadPatchesManifestState(patchesDir);
|
|
102
137
|
const existingEntries = new Map();
|
|
138
|
+
// An unparseable manifest yields `manifest: null`, which the merge below
|
|
139
|
+
// cannot distinguish from "no manifest at all" — every entry would be
|
|
140
|
+
// treated as new and every descriptive field reinvented. `filesAffected`
|
|
141
|
+
// is the only field a .patch body can yield; `stagedDependencies`,
|
|
142
|
+
// `lintIgnore`, `tier`, `description`, `category`, `createdAt` and the
|
|
143
|
+
// source version cannot be reconstructed from a diff at all. Refuse rather
|
|
144
|
+
// than write that, since the write is what makes it unrecoverable on a
|
|
145
|
+
// queue that is not under version control.
|
|
146
|
+
if (manifestState.exists && manifestState.parseError && options.allowMetadataLoss !== true) {
|
|
147
|
+
throw new GeneralError(`Refusing to rebuild ${PATCHES_MANIFEST}: it exists but could not be parsed ` +
|
|
148
|
+
`(${manifestState.parseError.message}). None of its metadata can be carried ` +
|
|
149
|
+
'forward, so every entry would be rebuilt with an invented description, an ' +
|
|
150
|
+
'mtime-based createdAt and an inferred category, and any stagedDependencies, ' +
|
|
151
|
+
'lintIgnore or tier declarations would be lost — a patch body carries none of ' +
|
|
152
|
+
'them.\n' +
|
|
153
|
+
`Fix the JSON in ${PATCHES_MANIFEST} (restore it from version control if you ` +
|
|
154
|
+
'can) and re-run, or pass --allow-metadata-loss to accept the reinvention.');
|
|
155
|
+
}
|
|
103
156
|
if (manifestState.manifest) {
|
|
104
157
|
for (const entry of manifestState.manifest.patches) {
|
|
105
158
|
existingEntries.set(entry.filename, entry);
|
|
@@ -118,7 +171,7 @@ async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVers
|
|
|
118
171
|
// sequential because `nextRecoveredOrder` is loop-carried: parallelising
|
|
119
172
|
// the assignment would scramble the recovered ordinals.
|
|
120
173
|
const prefetched = await Promise.all(patches.map(async (patch) => ({
|
|
121
|
-
filesAffected:
|
|
174
|
+
filesAffected: normalizeAffectedFiles(await getAllTargetFilesFromPatch(patch.path)),
|
|
122
175
|
patchStats: await stat(patch.path),
|
|
123
176
|
})));
|
|
124
177
|
for (const [index, patch] of patches.entries()) {
|
|
@@ -140,15 +193,17 @@ async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVers
|
|
|
140
193
|
// avoid.
|
|
141
194
|
recoveredFilenames.push(patch.filename);
|
|
142
195
|
}
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
196
|
+
// The rebuild OWNS exactly two fields — `filesAffected` (recomputed from
|
|
197
|
+
// the patch body) and `order` (from the filename ordinal). Everything
|
|
198
|
+
// else on an existing entry is carried forward by the spread, so a field
|
|
199
|
+
// a .patch cannot express — `stagedDependencies`, `lintIgnore`, `tier`,
|
|
200
|
+
// and whatever is added to PatchMetadata next — survives a repair by
|
|
201
|
+
// construction rather than by someone remembering to list it here. It was
|
|
202
|
+
// an enumerated literal until `stagedDependencies` was dropped from every
|
|
203
|
+
// entry that had one, which is the failure this shape prevents from
|
|
204
|
+
// recurring.
|
|
151
205
|
const rebuilt = {
|
|
206
|
+
...(existing ?? {}),
|
|
152
207
|
filename: patch.filename,
|
|
153
208
|
order: recoveredOrder,
|
|
154
209
|
category: existing?.category ?? inferred.category,
|
|
@@ -158,24 +213,42 @@ async function rebuildPatchesManifestUnderLock(patchesDir, fallbackSourceEsrVers
|
|
|
158
213
|
createdAt: existing?.createdAt ?? new Date(patchStats.mtimeMs).toISOString(),
|
|
159
214
|
sourceEsrVersion: existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
|
|
160
215
|
sourceVersion: existing?.sourceVersion ?? existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
|
|
161
|
-
...(existing?.sourceProduct !== undefined ? { sourceProduct: existing.sourceProduct } : {}),
|
|
162
216
|
filesAffected,
|
|
163
217
|
};
|
|
218
|
+
// Copy the arrays rather than aliasing the loaded manifest's, so a caller
|
|
219
|
+
// mutating the rebuilt entry cannot reach back into `manifestState`.
|
|
164
220
|
if (existing?.lintIgnore !== undefined)
|
|
165
221
|
rebuilt.lintIgnore = [...existing.lintIgnore];
|
|
166
|
-
if (existing?.tier !== undefined)
|
|
167
|
-
rebuilt.tier = existing.tier;
|
|
168
222
|
rebuiltPatches.push(rebuilt);
|
|
223
|
+
existingEntries.delete(patch.filename);
|
|
169
224
|
}
|
|
170
225
|
rebuiltPatches.sort((left, right) => left.order - right.order || left.filename.localeCompare(right.filename));
|
|
171
226
|
const rebuiltManifest = {
|
|
172
227
|
version: 1,
|
|
173
228
|
patches: rebuiltPatches,
|
|
174
229
|
};
|
|
230
|
+
// Whatever is left in the map had a manifest row but no patch file, so the
|
|
231
|
+
// rebuild drops it. That is the correct outcome — the file is the source of
|
|
232
|
+
// truth — but dropping rows without naming them is indistinguishable from
|
|
233
|
+
// never having had them.
|
|
234
|
+
const droppedFilenames = [...existingEntries.keys()].sort((left, right) => left.localeCompare(right));
|
|
235
|
+
if (options.dryRun === true) {
|
|
236
|
+
return {
|
|
237
|
+
manifest: rebuiltManifest,
|
|
238
|
+
recoveredFilenames,
|
|
239
|
+
droppedFilenames,
|
|
240
|
+
written: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
175
243
|
await savePatchesManifest(patchesDir, rebuiltManifest);
|
|
176
|
-
return { manifest: rebuiltManifest, recoveredFilenames };
|
|
244
|
+
return { manifest: rebuiltManifest, recoveredFilenames, droppedFilenames, written: true };
|
|
177
245
|
}
|
|
178
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Canonical form of a `filesAffected` list: deduplicated and sorted, so a
|
|
248
|
+
* comparison between a declared list and one extracted from a patch body is
|
|
249
|
+
* about membership rather than ordering.
|
|
250
|
+
*/
|
|
251
|
+
export function normalizeAffectedFiles(files) {
|
|
179
252
|
return Array.from(new Set(files)).sort((left, right) => left.localeCompare(right));
|
|
180
253
|
}
|
|
181
254
|
function sameStringArray(left, right) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow repair for `filesAffected` — the one manifest field a `.patch` body
|
|
3
|
+
* can actually yield.
|
|
4
|
+
*
|
|
5
|
+
* A `files-affected-mismatch` is pure derived-value drift: the manifest's list
|
|
6
|
+
* disagrees with the diff it describes, and the diff is right by definition.
|
|
7
|
+
* Rebuilding the whole manifest to fix it is a much larger operation than the
|
|
8
|
+
* failure calls for, so this repair recomputes one field on the drifted rows
|
|
9
|
+
* and leaves every other row byte-identical.
|
|
10
|
+
*/
|
|
11
|
+
import type { PatchDirectoryLockOptions } from './patch-lock.js';
|
|
12
|
+
/** One row whose `filesAffected` list disagreed with its patch body. */
|
|
13
|
+
export interface FilesAffectedRepair {
|
|
14
|
+
/** Patch filename. */
|
|
15
|
+
filename: string;
|
|
16
|
+
/** The list the manifest declared, normalized for comparison. */
|
|
17
|
+
before: string[];
|
|
18
|
+
/** The list the patch body actually targets. */
|
|
19
|
+
after: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Summary of a {@link repairPatchesFilesAffected} run. */
|
|
22
|
+
export interface RepairFilesAffectedResult {
|
|
23
|
+
/** Rows that drifted and were (or would be) corrected. */
|
|
24
|
+
repairs: FilesAffectedRepair[];
|
|
25
|
+
/** Requested filenames with no manifest row, or no patch file on disk. */
|
|
26
|
+
skippedFilenames: string[];
|
|
27
|
+
/** False when the run was a dry run and patches.json was left untouched. */
|
|
28
|
+
written: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Options for {@link repairPatchesFilesAffected}. */
|
|
31
|
+
export interface RepairFilesAffectedOptions extends PatchDirectoryLockOptions {
|
|
32
|
+
/** Compute the corrections without writing them. */
|
|
33
|
+
dryRun?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recomputes `filesAffected` from the patch body for the named patches.
|
|
37
|
+
*
|
|
38
|
+
* @param patchesDir - Path to the patches directory
|
|
39
|
+
* @param filenames - Patch filenames to repair
|
|
40
|
+
* @param options - Lock options plus `dryRun`
|
|
41
|
+
* @returns The corrections applied (or projected) and the filenames skipped
|
|
42
|
+
*/
|
|
43
|
+
export declare function repairPatchesFilesAffected(patchesDir: string, filenames: readonly string[], options?: RepairFilesAffectedOptions): Promise<RepairFilesAffectedResult>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Narrow repair for `filesAffected` — the one manifest field a `.patch` body
|
|
4
|
+
* can actually yield.
|
|
5
|
+
*
|
|
6
|
+
* A `files-affected-mismatch` is pure derived-value drift: the manifest's list
|
|
7
|
+
* disagrees with the diff it describes, and the diff is right by definition.
|
|
8
|
+
* Rebuilding the whole manifest to fix it is a much larger operation than the
|
|
9
|
+
* failure calls for, so this repair recomputes one field on the drifted rows
|
|
10
|
+
* and leaves every other row byte-identical.
|
|
11
|
+
*/
|
|
12
|
+
import { GeneralError } from '../errors/base.js';
|
|
13
|
+
import { discoverPatches, getAllTargetFilesFromPatch } from './patch-files.js';
|
|
14
|
+
import { withPatchDirectoryLock } from './patch-lock.js';
|
|
15
|
+
import { normalizeAffectedFiles } from './patch-manifest-consistency.js';
|
|
16
|
+
import { loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, } from './patch-manifest-io.js';
|
|
17
|
+
/**
|
|
18
|
+
* Recomputes `filesAffected` from the patch body for the named patches.
|
|
19
|
+
*
|
|
20
|
+
* @param patchesDir - Path to the patches directory
|
|
21
|
+
* @param filenames - Patch filenames to repair
|
|
22
|
+
* @param options - Lock options plus `dryRun`
|
|
23
|
+
* @returns The corrections applied (or projected) and the filenames skipped
|
|
24
|
+
*/
|
|
25
|
+
export async function repairPatchesFilesAffected(patchesDir, filenames, options = {}) {
|
|
26
|
+
// Same lock contract as the full rebuild (invariant 2,
|
|
27
|
+
// docs/lifecycle-invariants.md): a manifest write serializes on the
|
|
28
|
+
// patch-directory lock, and a dry run reads under it so its projection
|
|
29
|
+
// cannot be computed against a queue being rewritten underneath.
|
|
30
|
+
return withPatchDirectoryLock(patchesDir, () => repairPatchesFilesAffectedUnderLock(patchesDir, filenames, options), options);
|
|
31
|
+
}
|
|
32
|
+
async function repairPatchesFilesAffectedUnderLock(patchesDir, filenames, options) {
|
|
33
|
+
// ForWrite: a corrupt manifest must abort here rather than be treated as
|
|
34
|
+
// empty. This repair exists precisely so that a one-field drift never
|
|
35
|
+
// reaches a whole-manifest rewrite, and silently writing over unparseable
|
|
36
|
+
// metadata would be a larger version of what it avoids.
|
|
37
|
+
const manifest = await loadPatchesManifestForWrite(patchesDir);
|
|
38
|
+
if (!manifest) {
|
|
39
|
+
throw new GeneralError(`Cannot repair filesAffected: ${PATCHES_MANIFEST} does not exist in ${patchesDir}.`);
|
|
40
|
+
}
|
|
41
|
+
const patchPathByFilename = new Map((await discoverPatches(patchesDir)).map((patch) => [patch.filename, patch.path]));
|
|
42
|
+
const rowByFilename = new Map(manifest.patches.map((entry) => [entry.filename, entry]));
|
|
43
|
+
const repairs = [];
|
|
44
|
+
const skippedFilenames = [];
|
|
45
|
+
for (const filename of new Set(filenames)) {
|
|
46
|
+
const patchPath = patchPathByFilename.get(filename);
|
|
47
|
+
const row = rowByFilename.get(filename);
|
|
48
|
+
if (patchPath === undefined || !row) {
|
|
49
|
+
// A missing patch file or a missing row is a different consistency
|
|
50
|
+
// issue with a different remedy; this repair reports it rather than
|
|
51
|
+
// inventing a row or deleting one.
|
|
52
|
+
skippedFilenames.push(filename);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const before = normalizeAffectedFiles(row.filesAffected);
|
|
56
|
+
const after = normalizeAffectedFiles(await getAllTargetFilesFromPatch(patchPath));
|
|
57
|
+
if (sameFiles(before, after))
|
|
58
|
+
continue;
|
|
59
|
+
repairs.push({ filename, before, after });
|
|
60
|
+
}
|
|
61
|
+
skippedFilenames.sort((left, right) => left.localeCompare(right));
|
|
62
|
+
if (repairs.length === 0 || options.dryRun === true) {
|
|
63
|
+
return { repairs, skippedFilenames, written: false };
|
|
64
|
+
}
|
|
65
|
+
const correctedByFilename = new Map(repairs.map((repair) => [repair.filename, repair.after]));
|
|
66
|
+
// Row-scoped mutation, so every untouched entry keeps its exact JSON shape —
|
|
67
|
+
// no re-sorted `filesAffected` lists and no reader-only fallback fields
|
|
68
|
+
// serialized into rows this repair has no business touching.
|
|
69
|
+
await mutatePatchRowsInManifest(patchesDir, [...correctedByFilename.keys()], (existing) => {
|
|
70
|
+
const corrected = correctedByFilename.get(existing.filename);
|
|
71
|
+
return corrected ? { set: { filesAffected: corrected } } : null;
|
|
72
|
+
});
|
|
73
|
+
return { repairs, skippedFilenames, written: true };
|
|
74
|
+
}
|
|
75
|
+
function sameFiles(left, right) {
|
|
76
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
77
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Callers should continue to import from this module; the internal split
|
|
5
5
|
* is an implementation detail.
|
|
6
6
|
*/
|
|
7
|
-
export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
7
|
+
export { type PatchManifestConsistencyIssue, rebuildPatchesManifest, recommendManifestRepair, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
8
|
+
export { type FilesAffectedRepair, repairPatchesFilesAffected, } from './patch-manifest-files-affected.js';
|
|
8
9
|
export { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, type PatchRenameEntry, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
|
|
9
10
|
export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
|
|
10
11
|
export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Callers should continue to import from this module; the internal split
|
|
6
6
|
* is an implementation detail.
|
|
7
7
|
*/
|
|
8
|
-
export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
8
|
+
export { rebuildPatchesManifest, recommendManifestRepair, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
9
|
+
export { repairPatchesFilesAffected, } from './patch-manifest-files-affected.js';
|
|
9
10
|
export { addPatchToManifest, loadPatchesManifest, loadPatchesManifestForWrite, mutatePatchRowsInManifest, PATCHES_MANIFEST, removePatchFileAndManifest, renumberPatchesInManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from './patch-manifest-io.js';
|
|
10
11
|
export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
|
|
11
12
|
export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
|