@hominis/fireforge 0.17.0 → 0.18.1
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 +53 -0
- package/README.md +60 -33
- package/dist/src/commands/build.js +18 -4
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/doctor-working-tree.d.ts +29 -0
- package/dist/src/commands/doctor-working-tree.js +93 -0
- package/dist/src/commands/doctor.js +22 -12
- package/dist/src/commands/export-all.js +74 -4
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/create-xpcshell.js +4 -2
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/preview.js +38 -0
- package/dist/src/commands/furnace/remove.js +75 -1
- package/dist/src/commands/furnace/rename-xpcshell.d.ts +35 -0
- package/dist/src/commands/furnace/rename-xpcshell.js +97 -0
- package/dist/src/commands/furnace/rename.js +32 -4
- package/dist/src/commands/lint.js +19 -6
- package/dist/src/commands/patch/delete.js +4 -1
- package/dist/src/commands/patch/reorder.js +4 -1
- package/dist/src/commands/re-export-files.js +3 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/rebase/index.js +19 -1
- package/dist/src/commands/register.js +11 -0
- package/dist/src/commands/status.js +44 -5
- package/dist/src/commands/test.js +68 -16
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/verify.js +81 -6
- package/dist/src/commands/watch.js +43 -7
- package/dist/src/commands/wire.js +16 -0
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/furnace-constants.d.ts +14 -0
- package/dist/src/core/furnace-constants.js +16 -0
- package/dist/src/core/furnace-validate.js +67 -1
- package/dist/src/core/git-base.d.ts +27 -2
- package/dist/src/core/git-base.js +41 -3
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/git.js +53 -14
- package/dist/src/core/mach.d.ts +26 -8
- package/dist/src/core/mach.js +24 -8
- package/dist/src/core/manifest-rules.js +10 -1
- package/dist/src/core/manifest-tokenizers.d.ts +6 -0
- package/dist/src/core/manifest-tokenizers.js +28 -0
- package/dist/src/core/marionette-preflight.d.ts +16 -0
- package/dist/src/core/marionette-preflight.js +19 -0
- package/dist/src/core/patch-lint-diff-tag.d.ts +20 -0
- package/dist/src/core/patch-lint-diff-tag.js +25 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +94 -18
- package/dist/src/core/patch-manifest-consistency.js +15 -2
- package/dist/src/core/patch-manifest-io.js +10 -0
- package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
- package/dist/src/core/patch-manifest-resolve.js +29 -2
- package/dist/src/core/patch-manifest-validate.js +25 -1
- package/dist/src/core/patch-registration-refs.d.ts +42 -0
- package/dist/src/core/patch-registration-refs.js +117 -0
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-init.d.ts +9 -3
- package/dist/src/core/wire-init.js +18 -6
- package/dist/src/core/wire-subscript.d.ts +7 -3
- package/dist/src/core/wire-subscript.js +11 -4
- package/dist/src/core/xpcshell-appdir.d.ts +19 -5
- package/dist/src/core/xpcshell-appdir.js +46 -20
- package/dist/src/errors/git.d.ts +20 -0
- package/dist/src/errors/git.js +39 -0
- package/dist/src/types/commands/patches.d.ts +23 -0
- package/dist/src/types/furnace.d.ts +9 -0
- package/dist/src/utils/parse.d.ts +7 -0
- package/dist/src/utils/parse.js +15 -0
- package/package.json +1 -1
package/dist/src/core/git.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { GitError, GitIndexLockError, PatchApplyError } from '../errors/git.js';
|
|
4
|
+
import { GitError, GitIndexingTimeoutError, GitIndexLockError, PatchApplyError, } from '../errors/git.js';
|
|
5
5
|
import { toError } from '../utils/errors.js';
|
|
6
6
|
import { pathExists, removeFile } from '../utils/fs.js';
|
|
7
7
|
import { verbose } from '../utils/logger.js';
|
|
8
8
|
import { exec } from '../utils/process.js';
|
|
9
|
-
import { configureGitPerformance, ensureGit, git, GIT_ADD_CHUNK_TIMEOUT_MS, GIT_ADD_TIMEOUT_MS, } from './git-base.js';
|
|
9
|
+
import { configureGitPerformance, ensureGit, git, GIT_ADD_CHUNK_TIMEOUT_ENV_VAR, GIT_ADD_CHUNK_TIMEOUT_MS, GIT_ADD_TIMEOUT_MS, } from './git-base.js';
|
|
10
10
|
import { getWorkingTreeStatus } from './git-status.js';
|
|
11
11
|
// ── Functions that remain in this file ──
|
|
12
12
|
/**
|
|
@@ -38,12 +38,22 @@ export async function ensureOriginRemote(dir) {
|
|
|
38
38
|
const GIT_ADD_ENV = { GIT_INDEX_THREADS: '0' };
|
|
39
39
|
/**
|
|
40
40
|
* Returns true when the error looks like a process killed by the spawn timeout
|
|
41
|
-
* (SIGTERM → exit code 143)
|
|
41
|
+
* (SIGTERM → exit code 143) OR an AbortError raised by
|
|
42
|
+
* `AbortSignal.timeout`. The AbortSignal path is the one observed during
|
|
43
|
+
* the 2026-04-24 eval (Finding 10): Node's `child_process` layer
|
|
44
|
+
* rejects with an AbortError when the signal fires, so the timeout
|
|
45
|
+
* detection here needs to recognise that shape too.
|
|
42
46
|
*/
|
|
43
47
|
function isTimeoutError(error) {
|
|
48
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
49
|
+
return true;
|
|
44
50
|
if (!(error instanceof GitError))
|
|
45
51
|
return false;
|
|
46
|
-
|
|
52
|
+
if (/SIGTERM|timed out|exit code 143/i.test(error.message))
|
|
53
|
+
return true;
|
|
54
|
+
if (error.cause instanceof Error && error.cause.name === 'AbortError')
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
47
57
|
}
|
|
48
58
|
/**
|
|
49
59
|
* Removes `.git/index.lock` left behind by a killed git process.
|
|
@@ -59,6 +69,12 @@ async function cleanupIndexLock(dir) {
|
|
|
59
69
|
* Stages every file by walking top-level directories one at a time.
|
|
60
70
|
* This avoids a single monolithic `git add -A` that may time out on
|
|
61
71
|
* very large (~300 K file) trees like Firefox.
|
|
72
|
+
*
|
|
73
|
+
* 2026-04-24 eval Finding 10: a chunked pass that hits its own timeout
|
|
74
|
+
* now raises a typed {@link GitIndexingTimeoutError} rather than the
|
|
75
|
+
* opaque `AbortError: The operation was aborted` the caller otherwise
|
|
76
|
+
* saw. The typed error carries the environment-variable override so the
|
|
77
|
+
* operator can extend the budget and re-run.
|
|
62
78
|
*/
|
|
63
79
|
async function stageAllFilesChunked(dir, options = {}) {
|
|
64
80
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -66,21 +82,30 @@ async function stageAllFilesChunked(dir, options = {}) {
|
|
|
66
82
|
.filter((e) => e.isDirectory() && e.name !== '.git')
|
|
67
83
|
.map((e) => e.name)
|
|
68
84
|
.sort();
|
|
85
|
+
async function runChunk(args, label) {
|
|
86
|
+
try {
|
|
87
|
+
await git(args, dir, {
|
|
88
|
+
timeout: GIT_ADD_CHUNK_TIMEOUT_MS,
|
|
89
|
+
env: GIT_ADD_ENV,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (isTimeoutError(error)) {
|
|
94
|
+
throw new GitIndexingTimeoutError('chunked', GIT_ADD_CHUNK_TIMEOUT_MS, GIT_ADD_CHUNK_TIMEOUT_ENV_VAR, error instanceof Error ? error : undefined);
|
|
95
|
+
}
|
|
96
|
+
verbose(`Chunked staging failed on ${label}: ${toError(error).message}`);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
69
100
|
for (const dirName of directories) {
|
|
70
101
|
options.onProgress?.(`Staging directory: ${dirName}/...`);
|
|
71
|
-
await
|
|
72
|
-
timeout: GIT_ADD_CHUNK_TIMEOUT_MS,
|
|
73
|
-
env: GIT_ADD_ENV,
|
|
74
|
-
});
|
|
102
|
+
await runChunk(['add', '--', dirName], dirName);
|
|
75
103
|
}
|
|
76
104
|
// Stage any top-level files
|
|
77
105
|
const topLevelFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
78
106
|
if (topLevelFiles.length > 0) {
|
|
79
107
|
options.onProgress?.('Staging top-level files...');
|
|
80
|
-
await
|
|
81
|
-
timeout: GIT_ADD_CHUNK_TIMEOUT_MS,
|
|
82
|
-
env: GIT_ADD_ENV,
|
|
83
|
-
});
|
|
108
|
+
await runChunk(['add', '--', ...topLevelFiles], 'top-level files');
|
|
84
109
|
}
|
|
85
110
|
}
|
|
86
111
|
/**
|
|
@@ -122,11 +147,25 @@ export async function stageAllFiles(dir, options = {}) {
|
|
|
122
147
|
if (!isTimeoutError(error)) {
|
|
123
148
|
throw await maybeWrapIndexLockError(dir, error);
|
|
124
149
|
}
|
|
125
|
-
|
|
150
|
+
// 2026-04-24 eval Finding 10: the fallback transition used to be
|
|
151
|
+
// an implementation detail invisible to operators watching the
|
|
152
|
+
// spinner. Emit a loud, one-line banner so non-TTY log scrapers
|
|
153
|
+
// and TTY operators both see that the monolithic attempt lost and
|
|
154
|
+
// the chunked pass is starting. This was the missing signal in
|
|
155
|
+
// the eval log where the heartbeat went quiet for ~600s between
|
|
156
|
+
// the monolithic timeout and the chunked-pass failure.
|
|
157
|
+
options.onProgress?.(`Monolithic git add reached the ${Math.round(timeout / 1000)}s timeout; falling back to chunked staging. This pass may take several more minutes on a large tree.`);
|
|
126
158
|
}
|
|
127
159
|
// The killed process may have left an index lock
|
|
128
160
|
await cleanupIndexLock(dir);
|
|
129
|
-
|
|
161
|
+
try {
|
|
162
|
+
await stageAllFilesChunked(dir, options);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof GitIndexingTimeoutError)
|
|
166
|
+
throw error;
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
130
169
|
}
|
|
131
170
|
finally {
|
|
132
171
|
if (heartbeatTimer)
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -60,19 +60,25 @@ export declare function bootstrapWithOutput(engineDir: string): Promise<MachComm
|
|
|
60
60
|
/**
|
|
61
61
|
* Runs a full mach build. On a non-zero exit, any matched error hints are
|
|
62
62
|
* surfaced on top of the raw mach output so operators get an actionable
|
|
63
|
-
* nudge alongside the cryptic mozbuild traceback.
|
|
63
|
+
* nudge alongside the cryptic mozbuild traceback. Returns the captured
|
|
64
|
+
* result so the caller (e.g. `fireforge build`) can inspect the tail
|
|
65
|
+
* for post-build diagnostics that mach prints AFTER "Your build was
|
|
66
|
+
* successful!" — notably the stale `config.status is out of date`
|
|
67
|
+
* notice that mach emits when a tool-managed edit landed on
|
|
68
|
+
* `moz.configure` before the build.
|
|
64
69
|
* @param engineDir - Path to the engine directory
|
|
65
70
|
* @param jobs - Number of parallel jobs (optional)
|
|
66
|
-
* @returns
|
|
71
|
+
* @returns Captured mach result (stdout tail, stderr tail, exit code)
|
|
67
72
|
*/
|
|
68
|
-
export declare function build(engineDir: string, jobs?: number): Promise<
|
|
73
|
+
export declare function build(engineDir: string, jobs?: number): Promise<MachCommandResult>;
|
|
69
74
|
/**
|
|
70
75
|
* Runs a fast UI-only build. On a non-zero exit, any matched error hints are
|
|
71
|
-
* surfaced on top of the raw mach output.
|
|
76
|
+
* surfaced on top of the raw mach output. See {@link build} for why the
|
|
77
|
+
* full captured result is returned rather than just the exit code.
|
|
72
78
|
* @param engineDir - Path to the engine directory
|
|
73
|
-
* @returns
|
|
79
|
+
* @returns Captured mach result
|
|
74
80
|
*/
|
|
75
|
-
export declare function buildUI(engineDir: string): Promise<
|
|
81
|
+
export declare function buildUI(engineDir: string): Promise<MachCommandResult>;
|
|
76
82
|
/**
|
|
77
83
|
* Runs an operation while holding a sidecar build lock keyed on the
|
|
78
84
|
* project root. Concurrent `fireforge build` / `fireforge build --ui`
|
|
@@ -162,9 +168,21 @@ export declare function watch(engineDir: string): Promise<number>;
|
|
|
162
168
|
/**
|
|
163
169
|
* Runs mach watch while preserving stdin and capturing emitted output.
|
|
164
170
|
* @param engineDir - Path to the engine directory
|
|
171
|
+
* @param options - Optional environment overrides merged into the mach subprocess env
|
|
165
172
|
* @returns Captured output and exit code
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
*
|
|
174
|
+
* 2026-04-24 eval Finding 12: the pre-0.18.1 shape accepted no options
|
|
175
|
+
* and so never forwarded the detected watchman path into the mach
|
|
176
|
+
* subprocess env. `fireforge watch` could locate `watchman` via PATH
|
|
177
|
+
* (the probe's `which` succeeded) but the mach subprocess spawned with
|
|
178
|
+
* the parent's PATH only — on macOS that typically omits
|
|
179
|
+
* `/opt/homebrew/bin`, so `mach watch` failed at the `watch-project`
|
|
180
|
+
* subscription step. Accepting `env` here lets the caller prepend the
|
|
181
|
+
* resolved watchman directory to PATH in a way mach inherits.
|
|
182
|
+
*/
|
|
183
|
+
export declare function watchWithOutput(engineDir: string, options?: {
|
|
184
|
+
env?: Record<string, string>;
|
|
185
|
+
}): Promise<MachCommandResult>;
|
|
168
186
|
/**
|
|
169
187
|
* Runs mach test with the given test paths.
|
|
170
188
|
* @param engineDir - Path to the engine directory
|
package/dist/src/core/mach.js
CHANGED
|
@@ -137,10 +137,15 @@ function surfaceMachErrorHints(result) {
|
|
|
137
137
|
/**
|
|
138
138
|
* Runs a full mach build. On a non-zero exit, any matched error hints are
|
|
139
139
|
* surfaced on top of the raw mach output so operators get an actionable
|
|
140
|
-
* nudge alongside the cryptic mozbuild traceback.
|
|
140
|
+
* nudge alongside the cryptic mozbuild traceback. Returns the captured
|
|
141
|
+
* result so the caller (e.g. `fireforge build`) can inspect the tail
|
|
142
|
+
* for post-build diagnostics that mach prints AFTER "Your build was
|
|
143
|
+
* successful!" — notably the stale `config.status is out of date`
|
|
144
|
+
* notice that mach emits when a tool-managed edit landed on
|
|
145
|
+
* `moz.configure` before the build.
|
|
141
146
|
* @param engineDir - Path to the engine directory
|
|
142
147
|
* @param jobs - Number of parallel jobs (optional)
|
|
143
|
-
* @returns
|
|
148
|
+
* @returns Captured mach result (stdout tail, stderr tail, exit code)
|
|
144
149
|
*/
|
|
145
150
|
export async function build(engineDir, jobs) {
|
|
146
151
|
const args = ['build'];
|
|
@@ -151,20 +156,21 @@ export async function build(engineDir, jobs) {
|
|
|
151
156
|
if (result.exitCode !== 0) {
|
|
152
157
|
surfaceMachErrorHints(result);
|
|
153
158
|
}
|
|
154
|
-
return result
|
|
159
|
+
return result;
|
|
155
160
|
}
|
|
156
161
|
/**
|
|
157
162
|
* Runs a fast UI-only build. On a non-zero exit, any matched error hints are
|
|
158
|
-
* surfaced on top of the raw mach output.
|
|
163
|
+
* surfaced on top of the raw mach output. See {@link build} for why the
|
|
164
|
+
* full captured result is returned rather than just the exit code.
|
|
159
165
|
* @param engineDir - Path to the engine directory
|
|
160
|
-
* @returns
|
|
166
|
+
* @returns Captured mach result
|
|
161
167
|
*/
|
|
162
168
|
export async function buildUI(engineDir) {
|
|
163
169
|
const result = await runMachInheritCapture(['build', 'faster'], engineDir);
|
|
164
170
|
if (result.exitCode !== 0) {
|
|
165
171
|
surfaceMachErrorHints(result);
|
|
166
172
|
}
|
|
167
|
-
return result
|
|
173
|
+
return result;
|
|
168
174
|
}
|
|
169
175
|
/**
|
|
170
176
|
* Runs an operation while holding a sidecar build lock keyed on the
|
|
@@ -274,10 +280,20 @@ export async function watch(engineDir) {
|
|
|
274
280
|
/**
|
|
275
281
|
* Runs mach watch while preserving stdin and capturing emitted output.
|
|
276
282
|
* @param engineDir - Path to the engine directory
|
|
283
|
+
* @param options - Optional environment overrides merged into the mach subprocess env
|
|
277
284
|
* @returns Captured output and exit code
|
|
285
|
+
*
|
|
286
|
+
* 2026-04-24 eval Finding 12: the pre-0.18.1 shape accepted no options
|
|
287
|
+
* and so never forwarded the detected watchman path into the mach
|
|
288
|
+
* subprocess env. `fireforge watch` could locate `watchman` via PATH
|
|
289
|
+
* (the probe's `which` succeeded) but the mach subprocess spawned with
|
|
290
|
+
* the parent's PATH only — on macOS that typically omits
|
|
291
|
+
* `/opt/homebrew/bin`, so `mach watch` failed at the `watch-project`
|
|
292
|
+
* subscription step. Accepting `env` here lets the caller prepend the
|
|
293
|
+
* resolved watchman directory to PATH in a way mach inherits.
|
|
278
294
|
*/
|
|
279
|
-
export async function watchWithOutput(engineDir) {
|
|
280
|
-
return runMachInheritCapture(['watch'], engineDir);
|
|
295
|
+
export async function watchWithOutput(engineDir, options = {}) {
|
|
296
|
+
return runMachInheritCapture(['watch'], engineDir, options.env ? { env: options.env } : {});
|
|
281
297
|
}
|
|
282
298
|
/**
|
|
283
299
|
* Runs mach test with the given test paths.
|
|
@@ -24,7 +24,16 @@ export function getRules(binaryName) {
|
|
|
24
24
|
// proposed a bogus jar.mn entry. The lookahead blocks the match so
|
|
25
25
|
// `getUnregistrableAdvice` gets a chance to emit the correct
|
|
26
26
|
// guidance for the `.inc.xhtml` case.
|
|
27
|
-
|
|
27
|
+
//
|
|
28
|
+
// Test implementation files under `browser/base/content/test/` are
|
|
29
|
+
// also excluded: they belong in the nearest `browser.toml` manifest,
|
|
30
|
+
// not in jar.mn. 2026-04-23 eval 2: `status --unmanaged` proposed
|
|
31
|
+
// `fireforge register browser/base/content/test/<dir>/browser_*.js`
|
|
32
|
+
// which would have clutter-registered a test file as browser
|
|
33
|
+
// chrome content. The negative lookahead routes those paths to
|
|
34
|
+
// `getUnregistrableAdvice`, which returns the correct
|
|
35
|
+
// browser.toml-centric guidance.
|
|
36
|
+
pattern: /^browser\/base\/content\/(?!.+\.inc\.xhtml$)(?!test\/)(.+\.(?:js|mjs|xhtml|css))$/,
|
|
28
37
|
isRegistered: (engineDir, fileName) => isBrowserContentRegistered(engineDir, fileName),
|
|
29
38
|
register: (engineDir, after, dryRun, fileName) => registerBrowserContent(engineDir, fileName, after, undefined, dryRun),
|
|
30
39
|
extractArgs: (m) => [m[1] ?? ''],
|
|
@@ -26,6 +26,12 @@ export declare function tokenizeJarMn(lines: string[]): JarMnToken[];
|
|
|
26
26
|
/**
|
|
27
27
|
* Tokenizes a moz.build Python list block, returning the tokens and their
|
|
28
28
|
* line range within the file.
|
|
29
|
+
*
|
|
30
|
+
* Supports both multi-line lists (the common shape) and single-line
|
|
31
|
+
* empty lists of the form `EXTRA_JS_MODULES += []` — the eval-2 finding
|
|
32
|
+
* case where a freshly-scaffolded module directory's `moz.build`
|
|
33
|
+
* started with an empty list and the tokenizer returned `null`,
|
|
34
|
+
* leaving `register` unable to add the first entry.
|
|
29
35
|
*/
|
|
30
36
|
export declare function tokenizeMozBuildList(lines: string[], listPattern: RegExp): {
|
|
31
37
|
tokens: MozBuildToken[];
|
|
@@ -44,6 +44,12 @@ export function tokenizeJarMn(lines) {
|
|
|
44
44
|
/**
|
|
45
45
|
* Tokenizes a moz.build Python list block, returning the tokens and their
|
|
46
46
|
* line range within the file.
|
|
47
|
+
*
|
|
48
|
+
* Supports both multi-line lists (the common shape) and single-line
|
|
49
|
+
* empty lists of the form `EXTRA_JS_MODULES += []` — the eval-2 finding
|
|
50
|
+
* case where a freshly-scaffolded module directory's `moz.build`
|
|
51
|
+
* started with an empty list and the tokenizer returned `null`,
|
|
52
|
+
* leaving `register` unable to add the first entry.
|
|
47
53
|
*/
|
|
48
54
|
export function tokenizeMozBuildList(lines, listPattern) {
|
|
49
55
|
const tokens = [];
|
|
@@ -53,6 +59,28 @@ export function tokenizeMozBuildList(lines, listPattern) {
|
|
|
53
59
|
const raw = lines[i] ?? '';
|
|
54
60
|
if (startLine === -1) {
|
|
55
61
|
if (listPattern.test(raw)) {
|
|
62
|
+
// Single-line empty-list handling: a fresh scaffold sometimes
|
|
63
|
+
// writes `EXTRA_JS_MODULES += []` on one line. The pre-fix
|
|
64
|
+
// tokenizer returned `null` because it never saw a line
|
|
65
|
+
// starting with `]`, which stranded `register` with a "Could
|
|
66
|
+
// not find module list section" error against the documented
|
|
67
|
+
// browser/modules/<fork>/ scaffold (eval 2).
|
|
68
|
+
//
|
|
69
|
+
// The in-place split rewrites the single-line form into the
|
|
70
|
+
// canonical multi-line shape so the caller's
|
|
71
|
+
// `lines.splice(insertIndex, 0, entry)` lands inside the list
|
|
72
|
+
// body. The tokens are emitted to mirror the new structure.
|
|
73
|
+
const singleLineMatch = /^([^[]*\[)\s*\]\s*$/.exec(raw);
|
|
74
|
+
if (singleLineMatch) {
|
|
75
|
+
const openPart = singleLineMatch[1] ?? '';
|
|
76
|
+
lines[i] = openPart;
|
|
77
|
+
lines.splice(i + 1, 0, ']');
|
|
78
|
+
startLine = i;
|
|
79
|
+
endLine = i + 1;
|
|
80
|
+
tokens.push({ type: 'list-open', raw: openPart, lineIndex: i });
|
|
81
|
+
tokens.push({ type: 'list-close', raw: ']', lineIndex: i + 1 });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
56
84
|
startLine = i;
|
|
57
85
|
tokens.push({ type: 'list-open', raw, lineIndex: i });
|
|
58
86
|
}
|
|
@@ -44,3 +44,19 @@ export interface MarionettePreflightOptions {
|
|
|
44
44
|
export declare function runMarionettePreflight(engineDir: string, options?: MarionettePreflightOptions): Promise<MarionettePreflightResult>;
|
|
45
45
|
/** Renders a PASS/FAIL banner to the CLI using the shared logger helpers. */
|
|
46
46
|
export declare function reportMarionettePreflight(result: MarionettePreflightResult): void;
|
|
47
|
+
/**
|
|
48
|
+
* Formats the PASS/FAIL banner as a plain string for direct
|
|
49
|
+
* `process.stdout.write` use — bypasses the clack logger entirely so
|
|
50
|
+
* operators running `fireforge test --doctor` under a non-TTY (pipe,
|
|
51
|
+
* CI, `tee`-wrapped capture) always see the final line even when the
|
|
52
|
+
* clack renderer swallows trailing log output just before process exit.
|
|
53
|
+
*
|
|
54
|
+
* 2026-04-24 eval Finding 7 reproducibly captured only the `"Running
|
|
55
|
+
* marionette preflight..."` intro and no PASS line at all — the
|
|
56
|
+
* `success()` + `outro()` + direct `stdout.write` belt-and-suspenders
|
|
57
|
+
* we used to ship still lost the summary under some non-TTY flush
|
|
58
|
+
* races. Returning the raw string here lets the caller compose a single
|
|
59
|
+
* authoritative write without any clack layer between the probe and
|
|
60
|
+
* the captured log.
|
|
61
|
+
*/
|
|
62
|
+
export declare function formatMarionettePreflightLine(result: MarionettePreflightResult): string;
|
|
@@ -288,4 +288,23 @@ export function reportMarionettePreflight(result) {
|
|
|
288
288
|
warn(`Marionette preflight: FAIL (${result.durationMs}ms) — ${result.detail}`);
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Formats the PASS/FAIL banner as a plain string for direct
|
|
293
|
+
* `process.stdout.write` use — bypasses the clack logger entirely so
|
|
294
|
+
* operators running `fireforge test --doctor` under a non-TTY (pipe,
|
|
295
|
+
* CI, `tee`-wrapped capture) always see the final line even when the
|
|
296
|
+
* clack renderer swallows trailing log output just before process exit.
|
|
297
|
+
*
|
|
298
|
+
* 2026-04-24 eval Finding 7 reproducibly captured only the `"Running
|
|
299
|
+
* marionette preflight..."` intro and no PASS line at all — the
|
|
300
|
+
* `success()` + `outro()` + direct `stdout.write` belt-and-suspenders
|
|
301
|
+
* we used to ship still lost the summary under some non-TTY flush
|
|
302
|
+
* races. Returning the raw string here lets the caller compose a single
|
|
303
|
+
* authoritative write without any clack layer between the probe and
|
|
304
|
+
* the captured log.
|
|
305
|
+
*/
|
|
306
|
+
export function formatMarionettePreflightLine(result) {
|
|
307
|
+
const status = result.ok ? 'PASS' : 'FAIL';
|
|
308
|
+
return `Marionette preflight: ${status} (${result.durationMs}ms) — ${result.detail}`;
|
|
309
|
+
}
|
|
291
310
|
//# sourceMappingURL=marionette-preflight.js.map
|
|
@@ -18,6 +18,13 @@ import type { PatchLintIssue } from '../types/commands/index.js';
|
|
|
18
18
|
* @param rev Git revision to diff against (e.g. `HEAD`, a branch, a SHA).
|
|
19
19
|
*/
|
|
20
20
|
export declare function collectDiffFilePaths(engineDir: string, rev: string): Promise<Set<string>>;
|
|
21
|
+
/**
|
|
22
|
+
* Synthetic "file" value used by aggregate patch-size rules
|
|
23
|
+
* (`large-patch-files` / `large-patch-lines`) to flag that a finding
|
|
24
|
+
* describes the whole diff rather than a single path. Exported so callers
|
|
25
|
+
* can keep the tagging contract visible in one place.
|
|
26
|
+
*/
|
|
27
|
+
export declare const AGGREGATE_PATCH_FILE = "(patch)";
|
|
21
28
|
/**
|
|
22
29
|
* Annotates a list of lint issues with `introduced` / `cumulative` tags
|
|
23
30
|
* based on whether the issue's file is part of the supplied diff set.
|
|
@@ -27,6 +34,19 @@ export declare function collectDiffFilePaths(engineDir: string, rev: string): Pr
|
|
|
27
34
|
* describe queue-wide state — are always `cumulative` under `--since`
|
|
28
35
|
* because they describe drift accumulated across many commits, not a
|
|
29
36
|
* single current-task edit.
|
|
37
|
+
*
|
|
38
|
+
* Aggregate patch-size rules emit `issue.file === AGGREGATE_PATCH_FILE`,
|
|
39
|
+
* which is a synthetic placeholder that will never appear in a real
|
|
40
|
+
* `diffFiles` set. Without special-casing, `large-patch-files` /
|
|
41
|
+
* `large-patch-lines` were always tagged `[cumulative]` under
|
|
42
|
+
* `--only-introduced` even when the diff WAS the aggregate the rules
|
|
43
|
+
* measured — the eval (Finding #4) reported a stack of 20+ imported
|
|
44
|
+
* patches whose aggregate-size warnings printed as `[cumulative]` under
|
|
45
|
+
* `lint --since HEAD --only-introduced`, which reads as "this pre-existed"
|
|
46
|
+
* to an operator asking "what did this diff introduce?" We promote the
|
|
47
|
+
* aggregate tag to `introduced` whenever the diff set has any content —
|
|
48
|
+
* non-empty `diffFiles` means the operator asked about a specific diff
|
|
49
|
+
* scope and the aggregate-rule finding describes exactly that scope.
|
|
30
50
|
* @param issues Issues returned by the lint orchestrator.
|
|
31
51
|
* @param diffFiles File paths touched since the user's revision.
|
|
32
52
|
*/
|
|
@@ -58,6 +58,13 @@ export async function collectDiffFilePaths(engineDir, rev) {
|
|
|
58
58
|
}
|
|
59
59
|
return files;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Synthetic "file" value used by aggregate patch-size rules
|
|
63
|
+
* (`large-patch-files` / `large-patch-lines`) to flag that a finding
|
|
64
|
+
* describes the whole diff rather than a single path. Exported so callers
|
|
65
|
+
* can keep the tagging contract visible in one place.
|
|
66
|
+
*/
|
|
67
|
+
export const AGGREGATE_PATCH_FILE = '(patch)';
|
|
61
68
|
/**
|
|
62
69
|
* Annotates a list of lint issues with `introduced` / `cumulative` tags
|
|
63
70
|
* based on whether the issue's file is part of the supplied diff set.
|
|
@@ -67,15 +74,33 @@ export async function collectDiffFilePaths(engineDir, rev) {
|
|
|
67
74
|
* describe queue-wide state — are always `cumulative` under `--since`
|
|
68
75
|
* because they describe drift accumulated across many commits, not a
|
|
69
76
|
* single current-task edit.
|
|
77
|
+
*
|
|
78
|
+
* Aggregate patch-size rules emit `issue.file === AGGREGATE_PATCH_FILE`,
|
|
79
|
+
* which is a synthetic placeholder that will never appear in a real
|
|
80
|
+
* `diffFiles` set. Without special-casing, `large-patch-files` /
|
|
81
|
+
* `large-patch-lines` were always tagged `[cumulative]` under
|
|
82
|
+
* `--only-introduced` even when the diff WAS the aggregate the rules
|
|
83
|
+
* measured — the eval (Finding #4) reported a stack of 20+ imported
|
|
84
|
+
* patches whose aggregate-size warnings printed as `[cumulative]` under
|
|
85
|
+
* `lint --since HEAD --only-introduced`, which reads as "this pre-existed"
|
|
86
|
+
* to an operator asking "what did this diff introduce?" We promote the
|
|
87
|
+
* aggregate tag to `introduced` whenever the diff set has any content —
|
|
88
|
+
* non-empty `diffFiles` means the operator asked about a specific diff
|
|
89
|
+
* scope and the aggregate-rule finding describes exactly that scope.
|
|
70
90
|
* @param issues Issues returned by the lint orchestrator.
|
|
71
91
|
* @param diffFiles File paths touched since the user's revision.
|
|
72
92
|
*/
|
|
73
93
|
export function tagLintIssues(issues, diffFiles) {
|
|
94
|
+
const hasDiffContent = diffFiles.size > 0;
|
|
74
95
|
for (const issue of issues) {
|
|
75
96
|
if (!issue.file) {
|
|
76
97
|
issue.tag = 'cumulative';
|
|
77
98
|
continue;
|
|
78
99
|
}
|
|
100
|
+
if (issue.file === AGGREGATE_PATCH_FILE) {
|
|
101
|
+
issue.tag = hasDiffContent ? 'introduced' : 'cumulative';
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
79
104
|
issue.tag = diffFiles.has(issue.file) ? 'introduced' : 'cumulative';
|
|
80
105
|
}
|
|
81
106
|
return issues;
|
|
@@ -67,10 +67,49 @@ export declare function lintPatchedJs(repoDir: string, affectedFiles: string[],
|
|
|
67
67
|
* @returns Array of lint issues
|
|
68
68
|
*/
|
|
69
69
|
export declare function lintModificationComments(diffContent: string, config: FireForgeConfig): PatchLintIssue[];
|
|
70
|
+
/**
|
|
71
|
+
* Describes which tier `resolvePatchSizeTier` selected and why.
|
|
72
|
+
* Consumers that want to surface the tier choice to the operator
|
|
73
|
+
* (e.g. a one-line `info()` when branding thresholds kick in) read
|
|
74
|
+
* this alongside the issues array from `lintPatchSize`.
|
|
75
|
+
*/
|
|
76
|
+
export type PatchSizeTierDecision = {
|
|
77
|
+
tier: 'general';
|
|
78
|
+
} | {
|
|
79
|
+
tier: 'test';
|
|
80
|
+
} | {
|
|
81
|
+
tier: 'branding';
|
|
82
|
+
source: 'auto' | 'explicit';
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Decides which `large-patch-lines` threshold tier applies to a patch.
|
|
86
|
+
* Exported so `runPatchLint` and the per-patch `lint` command can
|
|
87
|
+
* surface the tier choice to the operator *without* depending on
|
|
88
|
+
* `lintPatchSize`'s internal return shape — the rule itself stays a
|
|
89
|
+
* pure issues-array API, and the decision is computed separately for
|
|
90
|
+
* the sole purpose of reporting.
|
|
91
|
+
*
|
|
92
|
+
* Precedence: test > branding (explicit) > branding (auto) > general.
|
|
93
|
+
* The test tier beats branding because a table-driven regression test
|
|
94
|
+
* is legitimately large independent of whether the patch also claims
|
|
95
|
+
* branding shape, and the test-tier thresholds are already more
|
|
96
|
+
* permissive than branding — so "tests beat branding" is the
|
|
97
|
+
* defensive-for-tests choice.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolvePatchSizeTier(filesAffected: ReadonlyArray<string>, patchTier?: 'branding'): PatchSizeTierDecision;
|
|
70
100
|
/**
|
|
71
101
|
* Checks patch size and emits advisory warnings.
|
|
102
|
+
*
|
|
103
|
+
* @param filesAffected - Files touched by the patch
|
|
104
|
+
* @param lineCount - Non-binary line count of the unified diff
|
|
105
|
+
* @param patchTier - Optional explicit tier override declared on
|
|
106
|
+
* `PatchMetadata.tier`. When `"branding"`, forces the branding
|
|
107
|
+
* thresholds regardless of `filesAffected`. Tests still win over
|
|
108
|
+
* branding (precedence `test > branding > general`) because the
|
|
109
|
+
* test-tier thresholds are already more permissive and an all-tests
|
|
110
|
+
* patch that is also branding-shaped is vanishingly rare.
|
|
72
111
|
*/
|
|
73
|
-
export declare function lintPatchSize(filesAffected: string[], lineCount: number): PatchLintIssue[];
|
|
112
|
+
export declare function lintPatchSize(filesAffected: string[], lineCount: number, patchTier?: 'branding'): PatchLintIssue[];
|
|
74
113
|
/**
|
|
75
114
|
* Checks that modified (non-new) files with a supported extension still
|
|
76
115
|
* start with a recognized license header.
|
|
@@ -94,6 +133,12 @@ export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles:
|
|
|
94
133
|
* is advisory-noisy by nature (a cohesive branding bundle, auto-generated
|
|
95
134
|
* manifest, etc.) can opt out of a specific rule without reaching for the
|
|
96
135
|
* blunt `--skip-lint` hammer. Not mutated by this function.
|
|
136
|
+
* @param patchTier - Optional explicit tier override, threaded from
|
|
137
|
+
* `PatchMetadata.tier`. When `"branding"` forces the branding
|
|
138
|
+
* thresholds on the `large-patch-lines` rule. Callers with a
|
|
139
|
+
* per-patch manifest context (re-export, per-patch lint) should
|
|
140
|
+
* pass this; aggregate-mode callers without a specific patch
|
|
141
|
+
* context skip it and fall through to auto-detection.
|
|
97
142
|
* @returns Array of all lint issues found
|
|
98
143
|
*/
|
|
99
|
-
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string
|
|
144
|
+
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding'): Promise<PatchLintIssue[]>;
|