@hominis/fireforge 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +25 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/build.js +8 -3
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/export-flow.d.ts +1 -1
- package/dist/src/commands/export-flow.js +21 -10
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.js +31 -5
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +97 -18
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +23 -19
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.js +27 -13
- package/dist/src/commands/patch/rename.js +5 -2
- package/dist/src/commands/patch/staged-dependency.js +102 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +106 -0
- package/dist/src/commands/re-export-options.js +3 -0
- package/dist/src/commands/re-export.js +42 -53
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/run.js +18 -2
- package/dist/src/commands/status-check.d.ts +35 -0
- package/dist/src/commands/status-check.js +88 -0
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +33 -8
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +26 -5
- package/dist/src/commands/test.js +38 -32
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +16 -0
- package/dist/src/commands/tree.js +174 -0
- package/dist/src/core/build-baseline-types.d.ts +7 -0
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +2 -1
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-validate.js +25 -0
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +27 -0
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +17 -5
- package/dist/src/core/patch-export.d.ts +12 -3
- package/dist/src/core/patch-export.js +13 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -2
- package/dist/src/core/patch-lint-checkjs.js +85 -16
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint.d.ts +23 -0
- package/dist/src/core/patch-lint.js +34 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/smoke-patterns.d.ts +24 -8
- package/dist/src/core/smoke-patterns.js +23 -12
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +20 -6
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +15 -0
- package/dist/src/core/test-stale-check.js +37 -1
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +82 -193
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +124 -0
- package/dist/src/core/tree-store.d.ts +53 -0
- package/dist/src/core/tree-store.js +214 -0
- package/dist/src/core/typecheck-shim.d.ts +27 -0
- package/dist/src/core/typecheck-shim.js +58 -4
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +45 -2
- package/dist/src/types/config.d.ts +34 -0
- package/dist/src/utils/validation.d.ts +10 -0
- package/dist/src/utils/validation.js +29 -0
- package/package.json +6 -6
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch-aware restore planning for `fireforge discard` (FORGE F1, P0).
|
|
3
|
+
*
|
|
4
|
+
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
5
|
+
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|
|
6
|
+
* (`git apply` without `--index` → patch-edited tracked files are unstaged
|
|
7
|
+
* modifications, patch-created files are untracked). The pre-0.39.0
|
|
8
|
+
* `discard` was purely git-mechanical — it restored to HEAD and deleted
|
|
9
|
+
* untracked files, silently reverting patch-backed files PAST their owning
|
|
10
|
+
* patch and deleting patch-created files outright.
|
|
11
|
+
*
|
|
12
|
+
* This module classifies each status entry against the patch manifest's
|
|
13
|
+
* ownership claims and plans the correct restore target: the PATCH-APPLIED
|
|
14
|
+
* baseline (HEAD content folded through every affecting patch in order) for
|
|
15
|
+
* claimed files, and the legacy git mechanics for unmanaged ones.
|
|
16
|
+
*/
|
|
17
|
+
import type { GitStatusEntry } from './git-base.js';
|
|
18
|
+
/** How one status entry's canonical path will be restored. */
|
|
19
|
+
export type DiscardKind = 'unmanaged' | 'patch-backed' | 'patch-created' | 'patch-deleted';
|
|
20
|
+
/** Restore plan for one git status entry. */
|
|
21
|
+
export interface DiscardBaselinePlan {
|
|
22
|
+
entry: GitStatusEntry;
|
|
23
|
+
kind: DiscardKind;
|
|
24
|
+
/** Claiming patch filenames for `entry.file` ∪ `entry.originalPath`. */
|
|
25
|
+
owners: string[];
|
|
26
|
+
/** True when more than one patch claims the path (ownership conflict). */
|
|
27
|
+
conflicted: boolean;
|
|
28
|
+
/** Baseline content for `entry.file`; null = absent at baseline. Unset for 'unmanaged'. */
|
|
29
|
+
expectedContent?: string | null;
|
|
30
|
+
/** Rename/copy only: baseline content for `originalPath` when that side is claimed. */
|
|
31
|
+
expectedOriginalContent?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** Builds an all-unmanaged plan set — the `--to-upstream` (legacy) shape. */
|
|
34
|
+
export declare function planUpstreamDiscards(entries: readonly GitStatusEntry[]): DiscardBaselinePlan[];
|
|
35
|
+
/**
|
|
36
|
+
* Plans the restore target for each status entry. Entries claimed by the
|
|
37
|
+
* patch manifest restore to the patch-applied baseline; unclaimed entries
|
|
38
|
+
* keep the legacy git mechanics. A missing patches dir or empty manifest
|
|
39
|
+
* degrades every plan to `unmanaged` (identical to pre-0.39.0 behavior).
|
|
40
|
+
*
|
|
41
|
+
* A corrupt manifest or an unreconstructible patch baseline REFUSES with a
|
|
42
|
+
* GeneralError naming `--to-upstream` — silently falling back to pristine
|
|
43
|
+
* HEAD is exactly the data-loss path this module exists to close.
|
|
44
|
+
*/
|
|
45
|
+
export declare function planDiscardBaselines(patchesDir: string, engineDir: string, entries: readonly GitStatusEntry[]): Promise<DiscardBaselinePlan[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Applies one restore plan. Patch-claimed paths end up exactly as `import`
|
|
48
|
+
* leaves them: patched content as an unstaged worktree state (tracked files
|
|
49
|
+
* read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
|
|
50
|
+
* to the legacy {@link discardStatusEntry} mechanics.
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyDiscardBaseline(engineDir: string, plan: DiscardBaselinePlan): Promise<void>;
|
|
53
|
+
/** One-line annotation for dry-run listings and confirmation summaries. */
|
|
54
|
+
export declare function describeDiscardBaseline(plan: DiscardBaselinePlan): string;
|
|
55
|
+
/** Single-file outro naming the baseline that was restored. */
|
|
56
|
+
export declare function describeDiscardOutcome(plan: DiscardBaselinePlan, toUpstream: boolean): string;
|
|
57
|
+
/** Batch outro summarising how many files restored to which baseline. */
|
|
58
|
+
export declare function summarizeDiscardBaselines(plans: readonly DiscardBaselinePlan[], succeeded: number): string;
|
|
59
|
+
/** Warn text for a multi-owner (conflicted) restore. */
|
|
60
|
+
export declare function describeConflictWarning(plan: DiscardBaselinePlan): string;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Patch-aware restore planning for `fireforge discard` (FORGE F1, P0).
|
|
4
|
+
*
|
|
5
|
+
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
6
|
+
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|
|
7
|
+
* (`git apply` without `--index` → patch-edited tracked files are unstaged
|
|
8
|
+
* modifications, patch-created files are untracked). The pre-0.39.0
|
|
9
|
+
* `discard` was purely git-mechanical — it restored to HEAD and deleted
|
|
10
|
+
* untracked files, silently reverting patch-backed files PAST their owning
|
|
11
|
+
* patch and deleting patch-created files outright.
|
|
12
|
+
*
|
|
13
|
+
* This module classifies each status entry against the patch manifest's
|
|
14
|
+
* ownership claims and plans the correct restore target: the PATCH-APPLIED
|
|
15
|
+
* baseline (HEAD content folded through every affecting patch in order) for
|
|
16
|
+
* claimed files, and the legacy git mechanics for unmanaged ones.
|
|
17
|
+
*/
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { GeneralError } from '../errors/base.js';
|
|
20
|
+
import { toError } from '../utils/errors.js';
|
|
21
|
+
import { readText, removeFile, writeText } from '../utils/fs.js';
|
|
22
|
+
import { discardStatusEntry, fileExistsInHead, listTrackedInHead, restoreTrackedPath, unstageFiles, } from './git-file-ops.js';
|
|
23
|
+
import { createPatchedContentComputer } from './patch-apply.js';
|
|
24
|
+
import { loadPatchesManifestState } from './patch-manifest-io.js';
|
|
25
|
+
import { parseDiffSections } from './patch-parse.js';
|
|
26
|
+
import { buildPatchClaims } from './status-classify.js';
|
|
27
|
+
/** Builds an all-unmanaged plan set — the `--to-upstream` (legacy) shape. */
|
|
28
|
+
export function planUpstreamDiscards(entries) {
|
|
29
|
+
return entries.map((entry) => ({ entry, kind: 'unmanaged', owners: [], conflicted: false }));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Plans the restore target for each status entry. Entries claimed by the
|
|
33
|
+
* patch manifest restore to the patch-applied baseline; unclaimed entries
|
|
34
|
+
* keep the legacy git mechanics. A missing patches dir or empty manifest
|
|
35
|
+
* degrades every plan to `unmanaged` (identical to pre-0.39.0 behavior).
|
|
36
|
+
*
|
|
37
|
+
* A corrupt manifest or an unreconstructible patch baseline REFUSES with a
|
|
38
|
+
* GeneralError naming `--to-upstream` — silently falling back to pristine
|
|
39
|
+
* HEAD is exactly the data-loss path this module exists to close.
|
|
40
|
+
*/
|
|
41
|
+
export async function planDiscardBaselines(patchesDir, engineDir, entries) {
|
|
42
|
+
const state = await loadPatchesManifestState(patchesDir);
|
|
43
|
+
if (state.exists && state.parseError) {
|
|
44
|
+
throw new GeneralError('patches/patches.json is unreadable — refusing to discard while patch ownership cannot be ' +
|
|
45
|
+
'determined. Fix the manifest, or pass --to-upstream to explicitly revert to pristine ' +
|
|
46
|
+
'upstream (HEAD).');
|
|
47
|
+
}
|
|
48
|
+
const claims = state.manifest
|
|
49
|
+
? buildPatchClaims(state.manifest.patches)
|
|
50
|
+
: new Map();
|
|
51
|
+
const plans = [];
|
|
52
|
+
const claimedEntries = entries.filter((entry) => claims.has(entry.file) || (entry.originalPath !== undefined && claims.has(entry.originalPath)));
|
|
53
|
+
if (claimedEntries.length === 0) {
|
|
54
|
+
return planUpstreamDiscards(entries);
|
|
55
|
+
}
|
|
56
|
+
const computePatched = await createPatchedContentComputer(patchesDir, engineDir);
|
|
57
|
+
const claimedFiles = [
|
|
58
|
+
...new Set(claimedEntries.flatMap((entry) => entry.originalPath !== undefined ? [entry.file, entry.originalPath] : [entry.file])),
|
|
59
|
+
];
|
|
60
|
+
const trackedInHead = await listTrackedInHead(engineDir, claimedFiles);
|
|
61
|
+
const computeExpected = async (path) => {
|
|
62
|
+
try {
|
|
63
|
+
return await computePatched(path);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new GeneralError(`Cannot reconstruct the patch baseline for ${path} (${toError(error).message}). ` +
|
|
67
|
+
'Fix the owning patch, or pass --to-upstream to explicitly revert to pristine ' +
|
|
68
|
+
'upstream (HEAD).');
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
// `computePatchedContent` folds a deletion into an empty string (an
|
|
72
|
+
// apply-to-content limitation), which is indistinguishable from a
|
|
73
|
+
// legitimately empty file. Consult the owning patches' diff sections
|
|
74
|
+
// directly: the LAST section (highest-ordered owner) naming the path
|
|
75
|
+
// decides whether the baseline is "absent".
|
|
76
|
+
const sectionCache = new Map();
|
|
77
|
+
const isDeletedAtBaseline = async (path, owners) => {
|
|
78
|
+
for (let i = owners.length - 1; i >= 0; i--) {
|
|
79
|
+
const owner = owners[i];
|
|
80
|
+
if (owner === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
let sections = sectionCache.get(owner);
|
|
83
|
+
if (!sections) {
|
|
84
|
+
try {
|
|
85
|
+
sections = parseDiffSections(await readText(join(patchesDir, owner)));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
sections = [];
|
|
89
|
+
}
|
|
90
|
+
sectionCache.set(owner, sections);
|
|
91
|
+
}
|
|
92
|
+
for (const section of sections) {
|
|
93
|
+
if (section.targetPath === path || section.sourcePath === path) {
|
|
94
|
+
return section.isDeletedFile;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
};
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const owners = [
|
|
102
|
+
...new Set([
|
|
103
|
+
...(claims.get(entry.file) ?? []),
|
|
104
|
+
...(entry.originalPath !== undefined ? (claims.get(entry.originalPath) ?? []) : []),
|
|
105
|
+
]),
|
|
106
|
+
];
|
|
107
|
+
if (owners.length === 0) {
|
|
108
|
+
plans.push({ entry, kind: 'unmanaged', owners, conflicted: false });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
let expectedContent = claims.has(entry.file) ? await computeExpected(entry.file) : null;
|
|
112
|
+
if (expectedContent !== null &&
|
|
113
|
+
(await isDeletedAtBaseline(entry.file, claims.get(entry.file) ?? []))) {
|
|
114
|
+
expectedContent = null;
|
|
115
|
+
}
|
|
116
|
+
const expectedOriginalContent = entry.originalPath !== undefined && claims.has(entry.originalPath)
|
|
117
|
+
? await computeExpected(entry.originalPath)
|
|
118
|
+
: undefined;
|
|
119
|
+
const kind = expectedContent === null
|
|
120
|
+
? 'patch-deleted'
|
|
121
|
+
: trackedInHead.has(entry.file)
|
|
122
|
+
? 'patch-backed'
|
|
123
|
+
: 'patch-created';
|
|
124
|
+
plans.push({
|
|
125
|
+
entry,
|
|
126
|
+
kind,
|
|
127
|
+
owners,
|
|
128
|
+
conflicted: owners.length > 1,
|
|
129
|
+
expectedContent,
|
|
130
|
+
...(expectedOriginalContent !== undefined ? { expectedOriginalContent } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return plans;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Applies one restore plan. Patch-claimed paths end up exactly as `import`
|
|
137
|
+
* leaves them: patched content as an unstaged worktree state (tracked files
|
|
138
|
+
* read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
|
|
139
|
+
* to the legacy {@link discardStatusEntry} mechanics.
|
|
140
|
+
*/
|
|
141
|
+
export async function applyDiscardBaseline(engineDir, plan) {
|
|
142
|
+
if (plan.kind === 'unmanaged') {
|
|
143
|
+
await discardStatusEntry(engineDir, plan.entry);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const { entry } = plan;
|
|
147
|
+
// Drop any staged edits/adds/renames first so the rewritten content is a
|
|
148
|
+
// plain worktree state (harmless when nothing is staged; skipped for
|
|
149
|
+
// purely-untracked entries which have no index footprint).
|
|
150
|
+
if (!entry.isUntracked) {
|
|
151
|
+
const indexPaths = entry.originalPath !== undefined ? [entry.file, entry.originalPath] : [entry.file];
|
|
152
|
+
await unstageFiles(engineDir, indexPaths);
|
|
153
|
+
}
|
|
154
|
+
if (plan.expectedContent === null || plan.expectedContent === undefined) {
|
|
155
|
+
await removeFile(join(engineDir, entry.file));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await writeText(join(engineDir, entry.file), plan.expectedContent);
|
|
159
|
+
}
|
|
160
|
+
// A rename/copy's original side restores to ITS baseline too: the patch
|
|
161
|
+
// baseline when claimed, else the legacy HEAD restore (matching what
|
|
162
|
+
// discardStatusEntry does for that side).
|
|
163
|
+
if (entry.originalPath !== undefined) {
|
|
164
|
+
if (plan.expectedOriginalContent !== undefined) {
|
|
165
|
+
if (plan.expectedOriginalContent === null) {
|
|
166
|
+
await removeFile(join(engineDir, entry.originalPath));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
await writeText(join(engineDir, entry.originalPath), plan.expectedOriginalContent);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (await fileExistsInHead(engineDir, entry.originalPath)) {
|
|
173
|
+
await restoreTrackedPath(engineDir, entry.originalPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** One-line annotation for dry-run listings and confirmation summaries. */
|
|
178
|
+
export function describeDiscardBaseline(plan) {
|
|
179
|
+
const owners = plan.owners.join(' + ');
|
|
180
|
+
switch (plan.kind) {
|
|
181
|
+
case 'unmanaged':
|
|
182
|
+
return plan.entry.isUntracked ? 'unmanaged — delete' : 'unmanaged — revert to upstream';
|
|
183
|
+
case 'patch-backed':
|
|
184
|
+
return `patch baseline: ${owners}${plan.conflicted ? ' (conflict)' : ''}`;
|
|
185
|
+
case 'patch-created':
|
|
186
|
+
return `re-materialize from ${owners}${plan.conflicted ? ' (conflict)' : ''}`;
|
|
187
|
+
case 'patch-deleted':
|
|
188
|
+
return `delete — ${owners} removes it`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Single-file outro naming the baseline that was restored. */
|
|
192
|
+
export function describeDiscardOutcome(plan, toUpstream) {
|
|
193
|
+
if (toUpstream)
|
|
194
|
+
return 'File restored to pristine upstream (HEAD)';
|
|
195
|
+
const owners = plan.owners.join(' + ');
|
|
196
|
+
switch (plan.kind) {
|
|
197
|
+
case 'unmanaged':
|
|
198
|
+
return 'File restored to original state';
|
|
199
|
+
case 'patch-backed':
|
|
200
|
+
return `File restored to patch baseline (${owners})`;
|
|
201
|
+
case 'patch-created':
|
|
202
|
+
return `File re-materialized from patch baseline (${owners})`;
|
|
203
|
+
case 'patch-deleted':
|
|
204
|
+
return `File removed to match patch baseline (${owners} deletes it)`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Batch outro summarising how many files restored to which baseline. */
|
|
208
|
+
export function summarizeDiscardBaselines(plans, succeeded) {
|
|
209
|
+
const patchBacked = plans.filter((p) => p.kind === 'patch-backed').length;
|
|
210
|
+
const created = plans.filter((p) => p.kind === 'patch-created').length;
|
|
211
|
+
const deleted = plans.filter((p) => p.kind === 'patch-deleted').length;
|
|
212
|
+
const unmanaged = plans.filter((p) => p.kind === 'unmanaged').length;
|
|
213
|
+
if (patchBacked + created + deleted === 0) {
|
|
214
|
+
return `${String(succeeded)} file(s) restored to upstream state`;
|
|
215
|
+
}
|
|
216
|
+
const parts = [];
|
|
217
|
+
if (patchBacked > 0)
|
|
218
|
+
parts.push(`${String(patchBacked)} to patch baseline`);
|
|
219
|
+
if (created > 0)
|
|
220
|
+
parts.push(`${String(created)} re-materialized`);
|
|
221
|
+
if (deleted > 0)
|
|
222
|
+
parts.push(`${String(deleted)} removed per patch baseline`);
|
|
223
|
+
if (unmanaged > 0)
|
|
224
|
+
parts.push(`${String(unmanaged)} to upstream state`);
|
|
225
|
+
return `${String(succeeded)} file(s) restored: ${parts.join(', ')}`;
|
|
226
|
+
}
|
|
227
|
+
/** Warn text for a multi-owner (conflicted) restore. */
|
|
228
|
+
export function describeConflictWarning(plan) {
|
|
229
|
+
return (`${plan.entry.file} is claimed by ${String(plan.owners.length)} patches ` +
|
|
230
|
+
`(${plan.owners.join(', ')}). Restored the cumulative baseline; run ` +
|
|
231
|
+
'"fireforge status --ownership" to resolve ownership.');
|
|
232
|
+
}
|
|
@@ -81,6 +81,18 @@ export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
|
81
81
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
82
82
|
*/
|
|
83
83
|
export declare function containsUpstreamLicenseText(content: string, maxLines?: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
86
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
87
|
+
* comment style. Used by `export` to treat such files as vendored: offering
|
|
88
|
+
* to prepend the project's own license header onto a byte-identity-pinned
|
|
89
|
+
* upstream bundle would mislicense third-party code (FORGE F15).
|
|
90
|
+
*
|
|
91
|
+
* @param content - File content to check
|
|
92
|
+
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
93
|
+
* MIT/BSD license texts run longer than the 10-line project-header scan)
|
|
94
|
+
*/
|
|
95
|
+
export declare function hasThirdPartyPermissiveBanner(content: string, maxLines?: number): boolean;
|
|
84
96
|
/**
|
|
85
97
|
* Prepends the license header to a file on disk if it is not already present.
|
|
86
98
|
*
|
|
@@ -252,6 +252,33 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
|
|
|
252
252
|
];
|
|
253
253
|
return markers.some((marker) => scanText.includes(marker));
|
|
254
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
257
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
258
|
+
* comment style. Used by `export` to treat such files as vendored: offering
|
|
259
|
+
* to prepend the project's own license header onto a byte-identity-pinned
|
|
260
|
+
* upstream bundle would mislicense third-party code (FORGE F15).
|
|
261
|
+
*
|
|
262
|
+
* @param content - File content to check
|
|
263
|
+
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
264
|
+
* MIT/BSD license texts run longer than the 10-line project-header scan)
|
|
265
|
+
*/
|
|
266
|
+
export function hasThirdPartyPermissiveBanner(content, maxLines = 30) {
|
|
267
|
+
const head = content.split('\n').slice(0, maxLines).join('\n');
|
|
268
|
+
const scanText = normalizeLicenseHeadForScan(head).toLowerCase();
|
|
269
|
+
const markers = [
|
|
270
|
+
'mit license',
|
|
271
|
+
'permission is hereby granted, free of charge',
|
|
272
|
+
'isc license',
|
|
273
|
+
'permission to use, copy, modify',
|
|
274
|
+
'redistribution and use in source and binary forms',
|
|
275
|
+
'apache license',
|
|
276
|
+
'licensed under the apache license',
|
|
277
|
+
];
|
|
278
|
+
if (markers.some((marker) => scanText.includes(marker)))
|
|
279
|
+
return true;
|
|
280
|
+
return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0)\b/.test(scanText);
|
|
281
|
+
}
|
|
255
282
|
/**
|
|
256
283
|
* Prepends the license header to a file on disk if it is not already present.
|
|
257
284
|
*
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
2
2
|
import type { FireForgeConfig } from '../types/config.js';
|
|
3
3
|
import { type PatchQueueContext } from './patch-lint.js';
|
|
4
|
-
export declare const LINT_CACHE_SCHEMA_VERSION =
|
|
4
|
+
export declare const LINT_CACHE_SCHEMA_VERSION = 2;
|
|
5
5
|
export interface PerPatchLintCacheEntry {
|
|
6
6
|
key: string;
|
|
7
7
|
patchFilename: string;
|
|
8
8
|
issues: PatchLintIssue[];
|
|
9
|
+
/** Issues dropped by the patch's lintIgnore waivers (FORGE G10). */
|
|
10
|
+
suppressed: PatchLintIssue[];
|
|
11
|
+
/** Non-binary diff line count measured by the cached lint run. */
|
|
12
|
+
lineCount: number;
|
|
9
13
|
updatedAt: string;
|
|
10
14
|
}
|
|
11
15
|
export interface PerPatchLintCacheFile {
|
|
@@ -36,7 +40,13 @@ export declare function loadPerPatchLintCache(projectRoot: string): Promise<PerP
|
|
|
36
40
|
export declare function savePerPatchLintCache(projectRoot: string, cache: PerPatchLintCacheFile): Promise<void>;
|
|
37
41
|
/** Clears the per-patch lint cache by replacing it with an empty document. */
|
|
38
42
|
export declare function clearPerPatchLintCache(projectRoot: string): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
/** Cached per-patch lint payload returned on a key match. */
|
|
44
|
+
export interface CachedPerPatchLint {
|
|
45
|
+
issues: PatchLintIssue[];
|
|
46
|
+
suppressed: PatchLintIssue[];
|
|
47
|
+
lineCount: number;
|
|
48
|
+
}
|
|
49
|
+
/** Returns the cached lint payload for a patch when the stored key still matches. */
|
|
50
|
+
export declare function getCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string): CachedPerPatchLint | undefined;
|
|
51
|
+
/** Stores the per-patch lint payload after a successful lint calculation. */
|
|
52
|
+
export declare function setCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string, issues: PatchLintIssue[], suppressed: PatchLintIssue[], lineCount: number): void;
|
|
@@ -7,7 +7,11 @@ import { getPackageVersion } from '../utils/package-root.js';
|
|
|
7
7
|
import { getFurnacePaths } from './furnace-config.js';
|
|
8
8
|
import { git } from './git-base.js';
|
|
9
9
|
import { collectNewFileCreatorsByPath } from './patch-lint.js';
|
|
10
|
-
|
|
10
|
+
// Schema 2 (FORGE G10): entries additionally carry the lintIgnore-suppressed
|
|
11
|
+
// issues and the measured non-binary line count, so a warm run can report
|
|
12
|
+
// waived size measurements identically to a cold one (the F5-class hazard:
|
|
13
|
+
// a cache hit must never surface LESS than a fresh lint).
|
|
14
|
+
export const LINT_CACHE_SCHEMA_VERSION = 2;
|
|
11
15
|
const LINT_IMPLEMENTATION_VERSION = 1;
|
|
12
16
|
const LINT_CACHE_DIRNAME = 'lint-cache';
|
|
13
17
|
const PER_PATCH_CACHE_FILENAME = 'per-patch-v1.json';
|
|
@@ -118,6 +122,8 @@ function isCacheEntry(value) {
|
|
|
118
122
|
return (typeof entry.key === 'string' &&
|
|
119
123
|
typeof entry.patchFilename === 'string' &&
|
|
120
124
|
Array.isArray(entry.issues) &&
|
|
125
|
+
Array.isArray(entry.suppressed) &&
|
|
126
|
+
typeof entry.lineCount === 'number' &&
|
|
121
127
|
typeof entry.updatedAt === 'string');
|
|
122
128
|
}
|
|
123
129
|
/** Loads the per-patch lint cache, treating missing or invalid files as empty. */
|
|
@@ -158,19 +164,25 @@ export async function savePerPatchLintCache(projectRoot, cache) {
|
|
|
158
164
|
export async function clearPerPatchLintCache(projectRoot) {
|
|
159
165
|
await writeJson(getPerPatchLintCachePath(projectRoot), createEmptyPerPatchLintCache());
|
|
160
166
|
}
|
|
161
|
-
/** Returns cached
|
|
167
|
+
/** Returns the cached lint payload for a patch when the stored key still matches. */
|
|
162
168
|
export function getCachedPerPatchLintIssues(cache, patchFilename, key) {
|
|
163
169
|
const entry = cache.entries[patchFilename];
|
|
164
170
|
if (!entry || entry.key !== key)
|
|
165
171
|
return undefined;
|
|
166
|
-
return
|
|
172
|
+
return {
|
|
173
|
+
issues: entry.issues.map((issue) => ({ ...issue })),
|
|
174
|
+
suppressed: entry.suppressed.map((issue) => ({ ...issue })),
|
|
175
|
+
lineCount: entry.lineCount,
|
|
176
|
+
};
|
|
167
177
|
}
|
|
168
|
-
/** Stores per-patch lint
|
|
169
|
-
export function setCachedPerPatchLintIssues(cache, patchFilename, key, issues) {
|
|
178
|
+
/** Stores the per-patch lint payload after a successful lint calculation. */
|
|
179
|
+
export function setCachedPerPatchLintIssues(cache, patchFilename, key, issues, suppressed, lineCount) {
|
|
170
180
|
cache.entries[patchFilename] = {
|
|
171
181
|
key,
|
|
172
182
|
patchFilename,
|
|
173
183
|
issues: issues.map((issue) => ({ ...issue })),
|
|
184
|
+
suppressed: suppressed.map((issue) => ({ ...issue })),
|
|
185
|
+
lineCount,
|
|
174
186
|
updatedAt: new Date().toISOString(),
|
|
175
187
|
};
|
|
176
188
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { PatchCategory, PatchesManifest, PatchInfo, PatchMetadata } from '../types/commands/index.js';
|
|
2
2
|
import type { FireForgeConfig } from '../types/config.js';
|
|
3
3
|
import { type SupersedeCoverageDetail } from './patch-export-coverage.js';
|
|
4
|
-
export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, type
|
|
5
|
-
export {
|
|
4
|
+
export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, type SupersedeCoverageDetail, } from './patch-export-coverage.js';
|
|
5
|
+
export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
|
|
6
6
|
export { updatePatchAndMetadata } from './patch-export-update.js';
|
|
7
7
|
/**
|
|
8
8
|
* Gets the next patch number for a new patch.
|
|
@@ -30,7 +30,16 @@ export declare function sanitizeName(name: string): string;
|
|
|
30
30
|
* double-prefix incident) also collapses. Exported for direct testing.
|
|
31
31
|
*/
|
|
32
32
|
export declare function stripRedundantCategoryPrefix(sanitizedName: string, category: string): string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Sanitizes a patch name and drops redundant category/full-stem prefixes.
|
|
35
|
+
*
|
|
36
|
+
* A single trailing `.patch` extension is stripped before sanitisation:
|
|
37
|
+
* operators frequently pass a full patch FILENAME where a name is expected
|
|
38
|
+
* (`move-files <from> 348-ui-foo.patch --create`, `rename --to foo.patch`),
|
|
39
|
+
* and 0.38.0 slugged the extension into `-patch`, producing double-suffixed
|
|
40
|
+
* `...-patch.patch` files. A name that deliberately ends in a literal
|
|
41
|
+
* `-patch` slug segment must now be written without the dot form.
|
|
42
|
+
*/
|
|
34
43
|
export declare function patchNameSlug(name: string, category: string): string;
|
|
35
44
|
/**
|
|
36
45
|
* Generates the next patch filename with category.
|
|
@@ -10,7 +10,7 @@ import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
|
|
|
10
10
|
import { addPatchToManifest, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
|
|
11
11
|
import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
|
|
12
12
|
export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, } from './patch-export-coverage.js';
|
|
13
|
-
export { mutatePatchMetadata, updatePatchMetadata
|
|
13
|
+
export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
|
|
14
14
|
export { updatePatchAndMetadata } from './patch-export-update.js';
|
|
15
15
|
/**
|
|
16
16
|
* Gets the next patch number for a new patch.
|
|
@@ -75,9 +75,19 @@ export function stripRedundantCategoryPrefix(sanitizedName, category) {
|
|
|
75
75
|
}
|
|
76
76
|
return stripped.length > 0 ? stripped : sanitizedName;
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Sanitizes a patch name and drops redundant category/full-stem prefixes.
|
|
80
|
+
*
|
|
81
|
+
* A single trailing `.patch` extension is stripped before sanitisation:
|
|
82
|
+
* operators frequently pass a full patch FILENAME where a name is expected
|
|
83
|
+
* (`move-files <from> 348-ui-foo.patch --create`, `rename --to foo.patch`),
|
|
84
|
+
* and 0.38.0 slugged the extension into `-patch`, producing double-suffixed
|
|
85
|
+
* `...-patch.patch` files. A name that deliberately ends in a literal
|
|
86
|
+
* `-patch` slug segment must now be written without the dot form.
|
|
87
|
+
*/
|
|
79
88
|
export function patchNameSlug(name, category) {
|
|
80
|
-
|
|
89
|
+
const stem = name.replace(/\.patch$/i, '');
|
|
90
|
+
return stripRedundantCategoryPrefix(sanitizeName(stem), category);
|
|
81
91
|
}
|
|
82
92
|
/**
|
|
83
93
|
* Generates the next patch filename with category.
|
|
@@ -25,11 +25,16 @@
|
|
|
25
25
|
* while reporting only the patch under export.
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
28
|
-
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig } from '../types/config.js';
|
|
28
|
+
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig, PatchLintSeverityGate } from '../types/config.js';
|
|
29
29
|
/** How a checkJs run controls reporting and resolution; see module docs. */
|
|
30
30
|
export interface CheckJsMode {
|
|
31
31
|
strict: boolean;
|
|
32
32
|
compilerOptions?: PatchLintCheckJsCompilerOptions;
|
|
33
|
+
/**
|
|
34
|
+
* How to report undefined free identifiers (TS2304/TS2552). Default
|
|
35
|
+
* 'warning' — see `patchLint.undefinedIdentifiers` (FORGE F12).
|
|
36
|
+
*/
|
|
37
|
+
undefinedIdentifiers?: PatchLintSeverityGate;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* Result of {@link runCheckJsGrouped}: diagnostics attributed to the
|
|
@@ -58,7 +63,7 @@ export interface GroupedCheckJsResult {
|
|
|
58
63
|
* @param mode - Strictness preset plus allowlisted compiler-option overrides
|
|
59
64
|
* @returns Diagnostics grouped per owning file plus run-level errors
|
|
60
65
|
*/
|
|
61
|
-
export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode): Promise<GroupedCheckJsResult>;
|
|
66
|
+
export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode, builtinShimSuffix?: string): Promise<GroupedCheckJsResult>;
|
|
62
67
|
/**
|
|
63
68
|
* Flattens a {@link runCheckJsGrouped} run into a single issue list. When
|
|
64
69
|
* `reportScope` is supplied, only diagnostics from files in that set are
|
|
@@ -88,6 +93,24 @@ export declare function runCheckJs(repoDir: string, patchOwnedFiles: Set<string>
|
|
|
88
93
|
* not surface other patches' diagnostics)
|
|
89
94
|
*/
|
|
90
95
|
export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, reportScope?: ReadonlySet<string>): Promise<PatchLintIssue[]>;
|
|
96
|
+
/**
|
|
97
|
+
* Runs the checkJs pass over patch-owned test `.js` files (FORGE G5,
|
|
98
|
+
* `patchLint.checkJsTestFiles`). Each test file gets its OWN small
|
|
99
|
+
* program — mochitest scripts share top-level scope only with their
|
|
100
|
+
* directory's `head*.js` helpers, so one multi-script program would emit
|
|
101
|
+
* false cross-file redeclaration errors — with roots = the test file plus
|
|
102
|
+
* any same-directory patch-owned `head*.js`, checked against the built-in
|
|
103
|
+
* Firefox shim + {@link TEST_HARNESS_SHIM} + the optional consumer
|
|
104
|
+
* `checkJsTestShim`. Only the target file's diagnostics are reported per
|
|
105
|
+
* program (head helpers report from their own program), and run-level
|
|
106
|
+
* errors are de-duplicated across programs.
|
|
107
|
+
*
|
|
108
|
+
* @param repoDir - Absolute engine (repository) directory
|
|
109
|
+
* @param testFiles - Patch-owned test-script paths (repo-relative)
|
|
110
|
+
* @param patchLint - The resolved `patchLint` config block
|
|
111
|
+
* @param projectRoot - FireForge project root for shim resolution
|
|
112
|
+
*/
|
|
113
|
+
export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string): Promise<GroupedCheckJsResult>;
|
|
91
114
|
/**
|
|
92
115
|
* Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
|
|
93
116
|
* checkJs program over `patchOwnedFiles` and returns its findings grouped by
|