@hominis/fireforge 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -34
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +21 -2
- package/dist/src/commands/build.js +7 -1
- package/dist/src/commands/config.js +16 -27
- package/dist/src/commands/discard.js +1 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.js +1 -1
- package/dist/src/commands/export-shared.d.ts +1 -1
- package/dist/src/commands/export-shared.js +4 -5
- package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
- package/dist/src/commands/furnace/chrome-doc.js +3 -0
- package/dist/src/commands/furnace/create.js +3 -0
- package/dist/src/commands/furnace/override.js +3 -0
- package/dist/src/commands/furnace/preview.js +3 -0
- package/dist/src/commands/furnace/refresh.js +3 -0
- package/dist/src/commands/furnace/remove.js +3 -0
- package/dist/src/commands/furnace/rename.js +3 -0
- package/dist/src/commands/furnace/scan.js +3 -0
- package/dist/src/commands/import.js +1 -1
- package/dist/src/commands/lint-per-patch.js +16 -8
- package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
- package/dist/src/commands/lint-per-run-checkjs.js +4 -4
- package/dist/src/commands/lint-report.js +1 -1
- package/dist/src/commands/lint.js +2 -2
- package/dist/src/commands/patch/lint-ignore.js +1 -1
- package/dist/src/commands/patch/move-files-create.js +5 -3
- package/dist/src/commands/patch/move-files-into.d.ts +1 -1
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/rename.js +1 -1
- package/dist/src/commands/patch/reorder.d.ts +1 -2
- package/dist/src/commands/patch/reorder.js +1 -2
- package/dist/src/commands/patch/split.js +5 -3
- package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
- package/dist/src/commands/patch/staged-dependency.js +4 -4
- package/dist/src/commands/re-export-adjacent.js +1 -1
- package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
- package/dist/src/commands/re-export-bulk-scan.js +5 -4
- package/dist/src/commands/re-export-drift.d.ts +4 -4
- package/dist/src/commands/re-export-drift.js +47 -8
- package/dist/src/commands/re-export-lint.d.ts +7 -5
- package/dist/src/commands/re-export-lint.js +9 -8
- package/dist/src/commands/re-export-single.d.ts +1 -1
- package/dist/src/commands/re-export-single.js +5 -4
- package/dist/src/commands/re-export.js +26 -14
- package/dist/src/commands/reset.js +1 -1
- package/dist/src/commands/run.js +1 -1
- package/dist/src/commands/source.js +6 -6
- package/dist/src/commands/status-check.d.ts +1 -1
- package/dist/src/commands/status-check.js +1 -1
- package/dist/src/commands/status-json.d.ts +2 -2
- package/dist/src/commands/status-json.js +1 -1
- package/dist/src/commands/status-output.d.ts +1 -1
- package/dist/src/commands/status-output.js +1 -1
- package/dist/src/commands/status.js +21 -7
- package/dist/src/commands/test-build-phase.d.ts +2 -2
- package/dist/src/commands/test-build-phase.js +9 -6
- package/dist/src/commands/test-diagnose.js +8 -1
- package/dist/src/commands/test-modes.d.ts +2 -2
- package/dist/src/commands/test-modes.js +3 -3
- package/dist/src/commands/test-register.js +7 -2
- package/dist/src/commands/test-run.d.ts +6 -0
- package/dist/src/commands/test-run.js +32 -3
- package/dist/src/commands/test-stale-gate.js +7 -2
- package/dist/src/commands/test.js +4 -4
- package/dist/src/commands/tree.js +2 -2
- package/dist/src/commands/typecheck.d.ts +2 -1
- package/dist/src/commands/typecheck.js +59 -3
- package/dist/src/commands/verify.js +26 -1
- package/dist/src/core/ast-utils.js +14 -2
- package/dist/src/core/browser-wire.js +9 -1
- package/dist/src/core/build-baseline-types.d.ts +1 -1
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +1 -1
- package/dist/src/core/build-overwrite-guard.d.ts +49 -0
- package/dist/src/core/build-overwrite-guard.js +112 -0
- package/dist/src/core/build-prepare.d.ts +13 -5
- package/dist/src/core/build-prepare.js +42 -9
- package/dist/src/core/config-mutate.d.ts +3 -2
- package/dist/src/core/config-mutate.js +5 -2
- package/dist/src/core/config.d.ts +3 -2
- package/dist/src/core/config.js +2 -0
- package/dist/src/core/coverage-extend.d.ts +1 -1
- package/dist/src/core/coverage-extend.js +1 -1
- package/dist/src/core/destructive.d.ts +23 -0
- package/dist/src/core/destructive.js +45 -4
- package/dist/src/core/diff-hunks.js +31 -17
- package/dist/src/core/discard-baseline.d.ts +1 -1
- package/dist/src/core/discard-baseline.js +1 -1
- package/dist/src/core/display-state.d.ts +52 -0
- package/dist/src/core/display-state.js +89 -0
- package/dist/src/core/engine-session-lock.d.ts +20 -0
- package/dist/src/core/engine-session-lock.js +84 -14
- package/dist/src/core/export-moved-code-shape.d.ts +60 -0
- package/dist/src/core/export-moved-code-shape.js +129 -0
- package/dist/src/core/file-lock.d.ts +44 -0
- package/dist/src/core/file-lock.js +157 -42
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-apply-helpers.js +1 -1
- package/dist/src/core/furnace-apply-output.js +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
- package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
- package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
- package/dist/src/core/furnace-apply.js +13 -1
- package/dist/src/core/furnace-config-custom.d.ts +2 -1
- package/dist/src/core/furnace-config-order.d.ts +2 -1
- package/dist/src/core/furnace-config-order.js +9 -4
- package/dist/src/core/furnace-config-validate.d.ts +4 -3
- package/dist/src/core/furnace-config-validate.js +3 -3
- package/dist/src/core/furnace-config.js +5 -0
- package/dist/src/core/furnace-operation.d.ts +12 -0
- package/dist/src/core/furnace-operation.js +125 -1
- package/dist/src/core/furnace-rollback.d.ts +17 -0
- package/dist/src/core/furnace-rollback.js +35 -1
- package/dist/src/core/git-base.js +7 -2
- package/dist/src/core/git-diff.js +11 -2
- package/dist/src/core/git-readonly-index.d.ts +59 -0
- package/dist/src/core/git-readonly-index.js +137 -0
- package/dist/src/core/license-headers.d.ts +1 -1
- package/dist/src/core/license-headers.js +1 -1
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +35 -8
- package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
- package/dist/src/core/mach-known-noise-filter.js +23 -0
- package/dist/src/core/mach-mozconfig.js +12 -2
- package/dist/src/core/module-resolution-preflight.d.ts +51 -0
- package/dist/src/core/module-resolution-preflight.js +155 -0
- package/dist/src/core/patch-export.js +2 -1
- package/dist/src/core/patch-identifier-suggest.js +29 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
- package/dist/src/core/patch-lint-checkjs.js +6 -6
- package/dist/src/core/patch-lint-creators.d.ts +1 -1
- package/dist/src/core/patch-lint-creators.js +1 -1
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +4 -4
- package/dist/src/core/patch-lint-css.js +1 -1
- package/dist/src/core/patch-lint-diff.d.ts +1 -1
- package/dist/src/core/patch-lint-diff.js +1 -1
- package/dist/src/core/patch-lint-jsdoc.js +4 -2
- package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
- package/dist/src/core/patch-lint-module-registration.js +13 -4
- package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
- package/dist/src/core/patch-lint-mozbuild.js +1 -1
- package/dist/src/core/patch-lint-ownership.d.ts +2 -2
- package/dist/src/core/patch-lint-ownership.js +2 -2
- package/dist/src/core/patch-lint.d.ts +2 -2
- package/dist/src/core/patch-lint.js +3 -3
- package/dist/src/core/patch-lock.d.ts +21 -0
- package/dist/src/core/patch-lock.js +29 -2
- package/dist/src/core/patch-manifest-io.d.ts +2 -1
- package/dist/src/core/patch-manifest-io.js +49 -0
- package/dist/src/core/patch-parse.d.ts +18 -0
- package/dist/src/core/patch-parse.js +24 -1
- package/dist/src/core/patch-policy.d.ts +2 -2
- package/dist/src/core/patch-policy.js +2 -2
- package/dist/src/core/smoke-patterns.d.ts +1 -1
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +1 -1
- package/dist/src/core/status-classify.d.ts +1 -1
- package/dist/src/core/status-classify.js +2 -2
- package/dist/src/core/test-harness-crash.d.ts +69 -3
- package/dist/src/core/test-harness-crash.js +174 -9
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +1 -1
- package/dist/src/core/token-category.js +1 -1
- package/dist/src/core/token-manager.d.ts +1 -1
- package/dist/src/core/token-manager.js +5 -5
- package/dist/src/core/tree-cow.js +1 -1
- package/dist/src/core/tree-guard.js +2 -2
- package/dist/src/core/tree-store.js +1 -1
- package/dist/src/core/typecheck-shim.d.ts +2 -2
- package/dist/src/core/typecheck-shim.js +2 -2
- package/dist/src/core/typecheck.js +10 -0
- package/dist/src/core/xpcshell-profile-dir.js +1 -1
- package/dist/src/errors/base.d.ts +22 -1
- package/dist/src/errors/base.js +32 -1
- package/dist/src/errors/codes.d.ts +10 -0
- package/dist/src/errors/codes.js +10 -0
- package/dist/src/types/commands/options.d.ts +27 -11
- package/dist/src/types/commands/patches.d.ts +1 -1
- package/dist/src/types/config.d.ts +2 -2
- package/dist/src/types/furnace.d.ts +1 -1
- package/dist/src/types/json.d.ts +22 -0
- package/dist/src/types/json.js +11 -0
- package/dist/src/utils/assert.d.ts +46 -0
- package/dist/src/utils/assert.js +83 -0
- package/dist/src/utils/build-info.d.ts +1 -1
- package/dist/src/utils/build-info.js +1 -1
- package/dist/src/utils/logger.d.ts +20 -0
- package/dist/src/utils/logger.js +26 -0
- package/dist/src/utils/options.d.ts +12 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/stdio-drain.d.ts +1 -1
- package/dist/src/utils/stdio-drain.js +1 -1
- package/dist/src/utils/validation.d.ts +12 -1
- package/dist/src/utils/validation.js +13 -1
- package/package.json +3 -3
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export
|
|
4
|
-
* (FORGE J2).
|
|
3
|
+
* Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export`.
|
|
5
4
|
*
|
|
6
5
|
* A scan-less re-export rebuilds each patch body as a whole-file
|
|
7
6
|
* `git diff HEAD` over the owned files — so a CONCURRENT session's
|
|
@@ -21,6 +20,7 @@
|
|
|
21
20
|
* payload lines — because a refresh legitimately shifts hunk offsets and
|
|
22
21
|
* context without changing what the patch does.
|
|
23
22
|
*/
|
|
23
|
+
import { stat } from 'node:fs/promises';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import { parseDiffSections } from '../core/patch-parse.js';
|
|
26
26
|
import { toError } from '../utils/errors.js';
|
|
@@ -160,7 +160,7 @@ export function computeForeignDrift(oldBody, newBody, previousFilesAffected) {
|
|
|
160
160
|
* records a refusal and returns `true` so the caller skips the write.
|
|
161
161
|
*/
|
|
162
162
|
export async function reportForeignDrift(args) {
|
|
163
|
-
const { patch, patchesDir, newDiffContent, ctx } = args;
|
|
163
|
+
const { patch, patchesDir, engineDir, newDiffContent, ctx } = args;
|
|
164
164
|
const oldPath = join(patchesDir, patch.filename);
|
|
165
165
|
let oldBody;
|
|
166
166
|
try {
|
|
@@ -181,16 +181,20 @@ export async function reportForeignDrift(args) {
|
|
|
181
181
|
if (ctx.expectedDriftFiles.has(entry.file))
|
|
182
182
|
ctx.expectedSeen.add(entry.file);
|
|
183
183
|
}
|
|
184
|
+
const recentlyEdited = await findFilesEditedSinceLastExport(engineDir, oldPath, drift.map((d) => d.file));
|
|
184
185
|
const totalLines = drift.reduce((sum, d) => sum + d.addedLines + d.removedLines, 0);
|
|
185
186
|
const lineNoun = totalLines === 1 ? 'line' : 'lines';
|
|
186
187
|
warn(`${patch.filename}: refreshed body absorbs ${String(totalLines)} ${lineNoun} not present in the old patch body (${String(drift.length)} file(s)):`);
|
|
187
188
|
for (const entry of drift.slice(0, MAX_FILES_REPORTED)) {
|
|
188
189
|
const expectedTag = ctx.expectedDriftFiles.has(entry.file) ? ' (expected via --expect)' : '';
|
|
190
|
+
const authorshipTag = recentlyEdited.has(entry.file)
|
|
191
|
+
? ' [edited since your last export]'
|
|
192
|
+
: ' [unchanged since your last export — another session may own it]';
|
|
189
193
|
if (entry.binaryChanged) {
|
|
190
|
-
info(` ${entry.file}: binary content changed${expectedTag}`);
|
|
194
|
+
info(` ${entry.file}: binary content changed${expectedTag}${authorshipTag}`);
|
|
191
195
|
continue;
|
|
192
196
|
}
|
|
193
|
-
info(` ${entry.file}: +${String(entry.addedLines)}/-${String(entry.removedLines)} newly captured line(s)${expectedTag}`);
|
|
197
|
+
info(` ${entry.file}: +${String(entry.addedLines)}/-${String(entry.removedLines)} newly captured line(s)${expectedTag}${authorshipTag}`);
|
|
194
198
|
for (const summary of entry.hunkSummaries) {
|
|
195
199
|
info(` ${summary}`);
|
|
196
200
|
}
|
|
@@ -212,12 +216,47 @@ export async function reportForeignDrift(args) {
|
|
|
212
216
|
return true;
|
|
213
217
|
}
|
|
214
218
|
warn(' If these are intentional engine edits, this is the normal re-export flow. ' +
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
"FireForge cannot PROVE authorship; the per-file tags above compare each file's " +
|
|
220
|
+
'modification time against this patch\'s last export, so "edited since your last ' +
|
|
221
|
+
'export" marks lines you most likely wrote yourself. If another session owns them, ' +
|
|
222
|
+
"re-run with --refuse-foreign-drift, or commit/stash that session's edits first.");
|
|
217
223
|
return false;
|
|
218
224
|
}
|
|
219
225
|
/**
|
|
220
|
-
*
|
|
226
|
+
* Splits the drifting files into "you probably wrote this" and "this
|
|
227
|
+
* predates your last export". `--refuse-foreign-drift` calls every
|
|
228
|
+
* absorbed line "foreign" — including the operator's OWN additions
|
|
229
|
+
* from the same session — and "foreign" reads as "another session's",
|
|
230
|
+
* which is precisely the case where proceeding would be wrong. Authorship
|
|
231
|
+
* is unknowable, but RECENCY is not: a file whose mtime is newer than the
|
|
232
|
+
* patch body this run is refreshing changed after the last export, which
|
|
233
|
+
* on a single-operator slice is the operator's own edit. Returns the
|
|
234
|
+
* subset of `files` that qualifies; an unstattable path is reported as
|
|
235
|
+
* NOT recently edited, so the cautious wording is the fallback.
|
|
236
|
+
*/
|
|
237
|
+
async function findFilesEditedSinceLastExport(engineDir, patchBodyPath, files) {
|
|
238
|
+
const recent = new Set();
|
|
239
|
+
let patchMtimeMs;
|
|
240
|
+
try {
|
|
241
|
+
patchMtimeMs = (await stat(patchBodyPath)).mtimeMs;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
verbose(`Could not stat ${patchBodyPath} for drift recency: ${toError(error).message}`);
|
|
245
|
+
return recent;
|
|
246
|
+
}
|
|
247
|
+
for (const file of files) {
|
|
248
|
+
try {
|
|
249
|
+
if ((await stat(join(engineDir, file))).mtimeMs > patchMtimeMs)
|
|
250
|
+
recent.add(file);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
verbose(`Could not stat ${file} for drift recency: ${toError(error).message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return recent;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Fail-closed baseline handling: a missing or unreadable old
|
|
221
260
|
* patch body means the drift comparison CANNOT run, so under
|
|
222
261
|
* `--refuse-foreign-drift` the patch is refused rather than written on the
|
|
223
262
|
* strength of a check that never happened. Without the flag this stays the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hoisted + cached lint context for `re-export
|
|
2
|
+
* Hoisted + cached lint context for `re-export`.
|
|
3
3
|
*
|
|
4
4
|
* The per-patch loop used to rebuild the whole patch-queue context AND a
|
|
5
5
|
* fresh queue-wide checkJs TypeScript program for every patch — ~37 s of
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* - builds the queue context and the {@link PerRunCheckJs} program
|
|
10
10
|
* controller ONCE per invocation (`buildReExportLintContext`) and slices
|
|
11
|
-
*
|
|
11
|
+
* per-patch findings out of it (the shape `move-files`/`split`
|
|
12
12
|
* already use, plus the program-level hoist `lint --per-patch` has);
|
|
13
13
|
* - reuses the existing per-patch lint RESULT cache
|
|
14
14
|
* (`.fireforge/lint-cache/per-patch-v1.json`) across invocations —
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* hashes engine HEAD, per-file content, patch body, config, shims, and
|
|
18
18
|
* ownership, so a repeat single re-export is a warm hit (~4 s instead
|
|
19
19
|
* of ~41 s). `--no-cache` opts out; run-level checkJs globals are
|
|
20
|
-
*
|
|
20
|
+
* never cached and are emitted once per invocation;
|
|
21
21
|
* - keeps the in-memory queue context honest after each write
|
|
22
22
|
* (`refreshQueueCtxEntry`), so later iterations of an `--all` run lint
|
|
23
23
|
* against the just-refreshed body instead of the stale one.
|
|
@@ -34,7 +34,7 @@ export interface ReExportLintContext {
|
|
|
34
34
|
paths: ReturnType<typeof getProjectPaths>;
|
|
35
35
|
config: FireForgeConfig;
|
|
36
36
|
/** Whole-queue context, built WITH config so the forward-import hints
|
|
37
|
-
*
|
|
37
|
+
* match `lint --per-patch`; undefined when no patches dir. */
|
|
38
38
|
patchQueueCtx: PatchQueueContext | undefined;
|
|
39
39
|
/** Queue-wide checkJs controller; undefined when checkJs is off. */
|
|
40
40
|
checkJs: PerRunCheckJs | undefined;
|
|
@@ -46,13 +46,15 @@ export interface ReExportLintContext {
|
|
|
46
46
|
/** Run-level checkJs globals are emitted once per invocation. */
|
|
47
47
|
globalsEmitted: boolean;
|
|
48
48
|
}
|
|
49
|
-
/** Builds the once-per-invocation lint context
|
|
49
|
+
/** Builds the once-per-invocation lint context. */
|
|
50
50
|
export declare function buildReExportLintContext(projectRoot: string, paths: ReturnType<typeof getProjectPaths>, config: Awaited<ReturnType<typeof loadConfig>>, noCache: boolean): Promise<ReExportLintContext>;
|
|
51
51
|
/** Fresh lint payload carried to the post-write cache store. */
|
|
52
52
|
export interface ReExportLintResult {
|
|
53
53
|
issues: PatchLintIssue[];
|
|
54
54
|
suppressed: PatchLintIssue[];
|
|
55
55
|
lineCount: number;
|
|
56
|
+
/** Waiver ids in force for this lint run. */
|
|
57
|
+
lintIgnore: string[];
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Lints one re-exported patch body using the hoisted context: cache hit →
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Hoisted + cached lint context for `re-export
|
|
3
|
+
* Hoisted + cached lint context for `re-export`.
|
|
4
4
|
*
|
|
5
5
|
* The per-patch loop used to rebuild the whole patch-queue context AND a
|
|
6
6
|
* fresh queue-wide checkJs TypeScript program for every patch — ~37 s of
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - builds the queue context and the {@link PerRunCheckJs} program
|
|
11
11
|
* controller ONCE per invocation (`buildReExportLintContext`) and slices
|
|
12
|
-
*
|
|
12
|
+
* per-patch findings out of it (the shape `move-files`/`split`
|
|
13
13
|
* already use, plus the program-level hoist `lint --per-patch` has);
|
|
14
14
|
* - reuses the existing per-patch lint RESULT cache
|
|
15
15
|
* (`.fireforge/lint-cache/per-patch-v1.json`) across invocations —
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* hashes engine HEAD, per-file content, patch body, config, shims, and
|
|
19
19
|
* ownership, so a repeat single re-export is a warm hit (~4 s instead
|
|
20
20
|
* of ~41 s). `--no-cache` opts out; run-level checkJs globals are
|
|
21
|
-
*
|
|
21
|
+
* never cached and are emitted once per invocation;
|
|
22
22
|
* - keeps the in-memory queue context honest after each write
|
|
23
23
|
* (`refreshQueueCtxEntry`), so later iterations of an `--all` run lint
|
|
24
24
|
* against the just-refreshed body instead of the stale one.
|
|
@@ -33,7 +33,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
33
33
|
import { info } from '../utils/logger.js';
|
|
34
34
|
import { reportPatchLintOutcome } from './export-shared.js';
|
|
35
35
|
import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
|
|
36
|
-
/** Builds the once-per-invocation lint context
|
|
36
|
+
/** Builds the once-per-invocation lint context. */
|
|
37
37
|
export async function buildReExportLintContext(projectRoot, paths, config, noCache) {
|
|
38
38
|
const patchQueueCtx = (await pathExists(paths.patches))
|
|
39
39
|
? await buildPatchQueueContext(paths.patches, config)
|
|
@@ -93,7 +93,7 @@ export async function lintReExportedPatch(args) {
|
|
|
93
93
|
: 'Lint: branding threshold tier applied (patch is all under browser/branding/ plus registration siblings).');
|
|
94
94
|
}
|
|
95
95
|
// Run-level checkJs globals surface exactly once per invocation, never
|
|
96
|
-
// from the cache
|
|
96
|
+
// from the cache. On an all-warm run this is a cheap probe,
|
|
97
97
|
// not a program build.
|
|
98
98
|
let globalIssues = [];
|
|
99
99
|
if (checkJs && !lintCtx.globalsEmitted) {
|
|
@@ -111,7 +111,7 @@ export async function lintReExportedPatch(args) {
|
|
|
111
111
|
queueContext: patchQueueCtx,
|
|
112
112
|
...(lintCtx.engineHeadSha === undefined ? {} : { engineHeadSha: lintCtx.engineHeadSha }),
|
|
113
113
|
});
|
|
114
|
-
const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey);
|
|
114
|
+
const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, args.ignoreChecks);
|
|
115
115
|
if (cached) {
|
|
116
116
|
lintCtx.reusedCacheEntries++;
|
|
117
117
|
reportPatchLintOutcome([...globalIssues, ...cached.issues], args.skipLint);
|
|
@@ -131,7 +131,7 @@ export async function lintReExportedPatch(args) {
|
|
|
131
131
|
else {
|
|
132
132
|
// Fresh `--scan` adoption created checkJs-relevant files the hoisted
|
|
133
133
|
// program has never seen — fall back to a per-patch build with the
|
|
134
|
-
// usual report scope so nothing is silently unchecked
|
|
134
|
+
// usual report scope so nothing is silently unchecked.
|
|
135
135
|
lintOptions.checkJsReportScope = resolvePatchOwnedSysMjs(detectNewFilesInDiff(diffContent));
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -144,6 +144,7 @@ export async function lintReExportedPatch(args) {
|
|
|
144
144
|
issues,
|
|
145
145
|
suppressed: suppressedIssues,
|
|
146
146
|
lineCount: countNonBinaryDiffLines(diffContent).textLines,
|
|
147
|
+
lintIgnore: [...(args.ignoreChecks ?? [])],
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
@@ -166,7 +167,7 @@ export async function storeReExportLintResult(lintCtx, patchFilename, projectedM
|
|
|
166
167
|
queueContext: patchQueueCtx,
|
|
167
168
|
...(lintCtx.engineHeadSha === undefined ? {} : { engineHeadSha: lintCtx.engineHeadSha }),
|
|
168
169
|
});
|
|
169
|
-
setCachedPerPatchLintIssues(cache, patchFilename, cacheKey, result.issues, result.suppressed, result.lineCount);
|
|
170
|
+
setCachedPerPatchLintIssues(cache, patchFilename, cacheKey, result.issues, result.suppressed, result.lineCount, result.lintIgnore);
|
|
170
171
|
lintCtx.cacheDirty = true;
|
|
171
172
|
}
|
|
172
173
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single-patch refresh core for `fireforge re-export`, split out of
|
|
3
|
-
* `re-export.ts` (at the per-file line budget after the
|
|
3
|
+
* `re-export.ts` (at the per-file line budget after the
|
|
4
4
|
* hoisted-lint and foreign-drift wiring). The orchestrator loop calls
|
|
5
5
|
* {@link reExportSinglePatchWithIndexLockRetry} per selected patch.
|
|
6
6
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Single-patch refresh core for `fireforge re-export`, split out of
|
|
4
|
-
* `re-export.ts` (at the per-file line budget after the
|
|
4
|
+
* `re-export.ts` (at the per-file line budget after the
|
|
5
5
|
* hoisted-lint and foreign-drift wiring). The orchestrator loop calls
|
|
6
6
|
* {@link reExportSinglePatchWithIndexLockRetry} per selected patch.
|
|
7
7
|
*/
|
|
@@ -133,7 +133,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
133
133
|
warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
|
|
134
134
|
return false;
|
|
135
135
|
}
|
|
136
|
-
// Foreign-drift preview + optional hard stop
|
|
136
|
+
// Foreign-drift preview + optional hard stop: show which
|
|
137
137
|
// payload lines are about to enter the body that the old body did not
|
|
138
138
|
// carry — the case where a concurrent session's uncommitted edits in
|
|
139
139
|
// OWNED files would be silently absorbed. Always previews (including
|
|
@@ -141,6 +141,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
141
141
|
if (await reportForeignDrift({
|
|
142
142
|
patch,
|
|
143
143
|
patchesDir: paths.patches,
|
|
144
|
+
engineDir: paths.engine,
|
|
144
145
|
newDiffContent: diffContent,
|
|
145
146
|
ctx: driftCtx,
|
|
146
147
|
})) {
|
|
@@ -155,7 +156,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
155
156
|
});
|
|
156
157
|
// Lint against the ONCE-per-invocation hoisted context (queue context +
|
|
157
158
|
// queue-wide checkJs program + per-patch result cache) instead of
|
|
158
|
-
// rebuilding ~37 s of setup for every patch in the loop
|
|
159
|
+
// rebuilding ~37 s of setup for every patch in the loop.
|
|
159
160
|
const projectedMetadata = { ...patch, ...updates };
|
|
160
161
|
const lintResult = await lintReExportedPatch({
|
|
161
162
|
lintCtx,
|
|
@@ -200,7 +201,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
200
201
|
* leave body and `filesAffected` disagreeing), then keeps the run's
|
|
201
202
|
* in-memory state honest: the queue context entry is refreshed and the
|
|
202
203
|
* fresh lint result is stored keyed against the just-written body — the
|
|
203
|
-
* state a repeat invocation will observe
|
|
204
|
+
* state a repeat invocation will observe — and the in-memory
|
|
204
205
|
* manifest row mirrors the on-disk write for later loop iterations
|
|
205
206
|
* (notably `--all --scan`, where `getClaimedFiles` reads this manifest).
|
|
206
207
|
*/
|
|
@@ -3,6 +3,7 @@ import { multiselect } from '@clack/prompts';
|
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
5
5
|
import { isGitRepository } from '../core/git.js';
|
|
6
|
+
import { formatPatchNotFoundError } from '../core/patch-identifier-suggest.js';
|
|
6
7
|
import { loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
7
8
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
8
9
|
import { elapsedSince } from '../utils/elapsed.js';
|
|
@@ -24,8 +25,9 @@ async function resolveSelectedPatches(patches, options, manifest) {
|
|
|
24
25
|
for (const identifier of patches) {
|
|
25
26
|
const match = resolvePatchIdentifier(identifier, manifest.patches);
|
|
26
27
|
if (!match) {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Suggest, never dump: a wrong guess used to print the
|
|
29
|
+
// whole ~300-entry manifest, burying the error it was reporting.
|
|
30
|
+
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
29
31
|
}
|
|
30
32
|
selectedPatches.push(match);
|
|
31
33
|
}
|
|
@@ -123,7 +125,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
123
125
|
}
|
|
124
126
|
const config = await loadConfig(projectRoot);
|
|
125
127
|
// Classification inputs for the scan-less adjacency advisory, computed
|
|
126
|
-
// once for the whole run
|
|
128
|
+
// once for the whole run.
|
|
127
129
|
const adjacentCtx = {
|
|
128
130
|
binaryName: config.binaryName,
|
|
129
131
|
furnacePrefixes: await collectFurnaceManagedPrefixes(paths.root),
|
|
@@ -131,7 +133,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
131
133
|
refusals: [],
|
|
132
134
|
};
|
|
133
135
|
const driftCtx = buildForeignDriftContext(options);
|
|
134
|
-
// Hoisted lint context, one per run
|
|
136
|
+
// Hoisted lint context, one per run: queue context + checkJs
|
|
135
137
|
// program + per-patch result cache, shared across every loop iteration.
|
|
136
138
|
const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true);
|
|
137
139
|
let reExported = 0;
|
|
@@ -189,8 +191,8 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
193
|
/**
|
|
192
|
-
* Foreign-drift guard context, one per run
|
|
193
|
-
*
|
|
194
|
+
* Foreign-drift guard context, one per run. `--expect` paths
|
|
195
|
+
* are normalized once here so the per-patch comparison matches
|
|
194
196
|
* them against engine-relative diff-section paths.
|
|
195
197
|
*/
|
|
196
198
|
function buildForeignDriftContext(options) {
|
|
@@ -203,7 +205,7 @@ function buildForeignDriftContext(options) {
|
|
|
203
205
|
};
|
|
204
206
|
}
|
|
205
207
|
/**
|
|
206
|
-
* Names `--expect` paths that never drifted this run
|
|
208
|
+
* Names `--expect` paths that never drifted this run. A typo'd
|
|
207
209
|
* `--expect` path silently degrades the flag back to refusing the slice it
|
|
208
210
|
* was meant to admit, so surface the mismatch — but only as a warning: an
|
|
209
211
|
* expected file legitimately shows no drift when the slice was already
|
|
@@ -217,11 +219,16 @@ function warnUnseenExpectedDrift(driftCtx) {
|
|
|
217
219
|
warn(`--expect path(s) were not evaluated because no selected patch reached the drift check (for example, an earlier refusal): ${unseen.join(', ')}`);
|
|
218
220
|
return;
|
|
219
221
|
}
|
|
220
|
-
warn(
|
|
222
|
+
warn('--expect path(s) showed no drift this run: ' +
|
|
223
|
+
`${unseen.join(', ')}. ` +
|
|
224
|
+
'Causes, in rough order of likelihood: a typo in the path; the slice was already ' +
|
|
225
|
+
'captured by an earlier export in this session; or the engine content already matches ' +
|
|
226
|
+
'the patch body — a concurrent session may have exported the same change, so the drift ' +
|
|
227
|
+
'converged before this run looked.');
|
|
221
228
|
}
|
|
222
229
|
/**
|
|
223
|
-
* Turns the run's collected `--refuse-adjacent-unmanaged`
|
|
224
|
-
* `--refuse-foreign-drift`
|
|
230
|
+
* Turns the run's collected `--refuse-adjacent-unmanaged` and
|
|
231
|
+
* `--refuse-foreign-drift` refusals into the non-zero exit,
|
|
225
232
|
* naming every refused patch and the remedy.
|
|
226
233
|
*/
|
|
227
234
|
function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
|
|
@@ -240,13 +247,18 @@ function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
|
|
|
240
247
|
? ` ${String(unreadable.length)} of these refused because the old patch body was missing or unreadable, so the drift comparison could not run (fail-closed).`
|
|
241
248
|
: '';
|
|
242
249
|
throw new GeneralError(`Refused ${String(driftCtx.refusals.length)} patch(es) whose refreshed body would absorb ` +
|
|
243
|
-
`engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart}
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
`engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart}\n\n` +
|
|
251
|
+
'The flag cannot tell WHO wrote those lines, and the two populations have opposite ' +
|
|
252
|
+
'remedies:\n' +
|
|
253
|
+
' - Lines you added yourself since the last export (the per-file report tags these ' +
|
|
254
|
+
'"edited since your last export"): name them with --expect <path> so this run captures ' +
|
|
255
|
+
'them, or re-run without the flag to capture every drifting file.\n' +
|
|
256
|
+
" - Lines another session owns: do NOT proceed — commit or stash that session's work " +
|
|
257
|
+
'first, then re-run.');
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
260
|
/**
|
|
249
|
-
* Prints the end-of-run summary and enforces the
|
|
261
|
+
* Prints the end-of-run summary and enforces the exit contract:
|
|
250
262
|
* a partial run exits non-zero BY DEFAULT (adjudicated for 0.41.0, per the
|
|
251
263
|
* follow-up 0.40.0's G7 recorded). "Re-exported 2 of 3" used to print and
|
|
252
264
|
* exit 0, letting a partial refresh ride an `&&` chain as success. The
|
|
@@ -70,7 +70,7 @@ export async function resetCommand(projectRoot, options) {
|
|
|
70
70
|
s.stop('Changes reset');
|
|
71
71
|
// The reset may have moved components.conf away from what the last full
|
|
72
72
|
// build compiled in — surface that now instead of at the next test
|
|
73
|
-
// refusal
|
|
73
|
+
// refusal.
|
|
74
74
|
await warnIfStaticComponentsStale(projectRoot, paths.engine);
|
|
75
75
|
outro('Working tree restored to clean state');
|
|
76
76
|
}
|
package/dist/src/commands/run.js
CHANGED
|
@@ -312,7 +312,7 @@ function reportSmokeSummary(args) {
|
|
|
312
312
|
info(` Allowlisted error hits (suppressed): ${String(args.allowlistedErrorHits)}`);
|
|
313
313
|
info(` Allowlisted lines total: ${String(args.allowlistedTotalHits)}`);
|
|
314
314
|
info(` Child exit code: ${String(args.exitCode)}`);
|
|
315
|
-
// Per-entry attribution
|
|
315
|
+
// Per-entry attribution: first-match credit per line, with
|
|
316
316
|
// zero-hit entries always visible — an allowlist entry whose suppressed
|
|
317
317
|
// shape changed upstream is only detectable as a 0× row.
|
|
318
318
|
if (args.allowlist.length > 0) {
|
|
@@ -4,15 +4,14 @@ import { configExists, loadRawConfigDocument, validateConfig, withConfigFileLock
|
|
|
4
4
|
import { resolveArchive } from '../core/firefox-archive.js';
|
|
5
5
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
6
6
|
import { info, intro, outro, success } from '../utils/logger.js';
|
|
7
|
-
import { FIREFOX_PRODUCTS, isValidFirefoxCandidate, isValidFirefoxProduct, } from '../utils/validation.js';
|
|
8
|
-
function isRecord(value) {
|
|
9
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
-
}
|
|
7
|
+
import { FIREFOX_PRODUCTS, isJsonObject, isObject, isValidFirefoxCandidate, isValidFirefoxProduct, } from '../utils/validation.js';
|
|
11
8
|
function cloneRawConfig(raw) {
|
|
12
9
|
const cloned = structuredClone(raw);
|
|
13
|
-
if (!
|
|
10
|
+
if (!isObject(cloned)) {
|
|
14
11
|
throw new GeneralError('Cannot update fireforge.json: config clone was not an object.');
|
|
15
12
|
}
|
|
13
|
+
// A structured clone of a JSON document is itself a JSON document; the
|
|
14
|
+
// object check above is the only invariant left to establish.
|
|
16
15
|
return cloned;
|
|
17
16
|
}
|
|
18
17
|
function parseSourceProduct(product) {
|
|
@@ -44,7 +43,8 @@ export async function sourceSetCommand(projectRoot, options) {
|
|
|
44
43
|
const written = await withConfigFileLock(projectRoot, async () => {
|
|
45
44
|
const raw = await loadRawConfigDocument(projectRoot);
|
|
46
45
|
const updated = cloneRawConfig(raw);
|
|
47
|
-
const
|
|
46
|
+
const existingFirefox = updated['firefox'];
|
|
47
|
+
const firefox = isJsonObject(existingFirefox) ? { ...existingFirefox } : {};
|
|
48
48
|
firefox['version'] = options.version;
|
|
49
49
|
firefox['product'] = options.product;
|
|
50
50
|
if (options.clearSha256 === true) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `status --check` enforcement policy
|
|
2
|
+
* `status --check` enforcement policy. Turns the classified
|
|
3
3
|
* status view into a CI-enforceable non-zero exit: the default policy
|
|
4
4
|
* fails on any classification outside {patch-backed, branding, furnace},
|
|
5
5
|
* and `--fail-on <class,...>` replaces that set for finer control.
|
|
@@ -12,7 +12,7 @@ const ALL_CLASSIFICATIONS = [
|
|
|
12
12
|
* Default `--check` policy: everything outside {patch-backed, branding,
|
|
13
13
|
* furnace, binary-unsupported} fails. `binary-unsupported` is deliberately
|
|
14
14
|
* excluded — an honest "cannot compare" must not keep the gate permanently
|
|
15
|
-
* red
|
|
15
|
+
* red; strict CI can opt in via `--fail-on binary-unsupported`.
|
|
16
16
|
*/
|
|
17
17
|
const DEFAULT_FAIL_CLASSIFICATIONS = [
|
|
18
18
|
'unmanaged',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Two shapes share one counting pass:
|
|
7
7
|
* - the full payload (`renderJsonStatus`) with the per-file `files[]` list;
|
|
8
|
-
* - the `--summary` gate payload (`renderJsonSummaryStatus
|
|
8
|
+
* - the `--summary` gate payload (`renderJsonSummaryStatus`),
|
|
9
9
|
* which omits `files[]` entirely — engine-clean gates need the verdict,
|
|
10
10
|
* per-class counts, and offender names, not a payload that grows with
|
|
11
11
|
* the queue (175 KB+ observed on a 290-patch consumer).
|
|
@@ -15,7 +15,7 @@ import { type StatusCheckPolicy } from './status-check.js';
|
|
|
15
15
|
import type { OwnershipJsonBlock } from './status-ownership.js';
|
|
16
16
|
/**
|
|
17
17
|
* Full `--json` payload: summary plus the per-file `files[]` list, and —
|
|
18
|
-
* under `--include-ownership`
|
|
18
|
+
* under `--include-ownership` — the additive `ownership` block.
|
|
19
19
|
* Additive within schemaVersion 1: a consumer that does not ask for the
|
|
20
20
|
* block never sees the key.
|
|
21
21
|
*/
|
|
@@ -16,7 +16,7 @@ function countByClassification(classified) {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Full `--json` payload: summary plus the per-file `files[]` list, and —
|
|
19
|
-
* under `--include-ownership`
|
|
19
|
+
* under `--include-ownership` — the additive `ownership` block.
|
|
20
20
|
* Additive within schemaVersion 1: a consumer that does not ask for the
|
|
21
21
|
* block never sees the key.
|
|
22
22
|
*/
|
|
@@ -14,7 +14,7 @@ export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], to
|
|
|
14
14
|
/** Renders the default classified status buckets. */
|
|
15
15
|
export declare function renderDefaultStatus(totalModified: number, buckets: ClassifiedBuckets, projectRoot: string, binaryName: string): Promise<void>;
|
|
16
16
|
/**
|
|
17
|
-
* Renders `fireforge status --test-coverage
|
|
17
|
+
* Renders `fireforge status --test-coverage`: a READ-ONLY view
|
|
18
18
|
* of the last build baseline's test-packaging coverage. Before this
|
|
19
19
|
* existed, the only way to learn the recorded coverage scope — which
|
|
20
20
|
* concurrent sessions sharing one engine tree overwrite constantly — was
|
|
@@ -191,7 +191,7 @@ export async function renderDefaultStatus(totalModified, buckets, projectRoot, b
|
|
|
191
191
|
outro(parts.join(', '));
|
|
192
192
|
}
|
|
193
193
|
/**
|
|
194
|
-
* Renders `fireforge status --test-coverage
|
|
194
|
+
* Renders `fireforge status --test-coverage`: a READ-ONLY view
|
|
195
195
|
* of the last build baseline's test-packaging coverage. Before this
|
|
196
196
|
* existed, the only way to learn the recorded coverage scope — which
|
|
197
197
|
* concurrent sessions sharing one engine tree overwrite constantly — was
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { readBuildBaseline } from '../core/build-baseline.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
+
import { formatEngineSessionLockStatus, readEngineSessionLockStatus, } from '../core/engine-session-lock.js';
|
|
5
6
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
6
7
|
import { getHead, getStatusWithCodes, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
7
8
|
import { getUntrackedFilesInDir, resolveMaxUntrackedFilesPerDir } from '../core/git-status.js';
|
|
@@ -96,7 +97,7 @@ async function classifyStatusFiles(files, paths, projectRoot, binaryName) {
|
|
|
96
97
|
* The one worktree scan every classifying mode shares: porcelain status,
|
|
97
98
|
* collapsed-directory expansion, atomic-temp-file filtering, and the
|
|
98
99
|
* truncation banner. `--ownership` used to carry its own copy of this
|
|
99
|
-
* block
|
|
100
|
+
* block; a non-git engine degrades to an empty list there,
|
|
100
101
|
* which is why the git probe is a parameter rather than a hard guard.
|
|
101
102
|
* @param engineDir - Path to the engine directory
|
|
102
103
|
* @param requireGitRepository - When false, a non-git engine yields `[]`
|
|
@@ -114,7 +115,7 @@ async function scanEngineStatusFiles(engineDir, requireGitRepository) {
|
|
|
114
115
|
* Emits the `--json` payload (full or `--summary`) and applies the
|
|
115
116
|
* `--check` policy, which stays the SOLE exit driver on this path.
|
|
116
117
|
*
|
|
117
|
-
* `--include-ownership`
|
|
118
|
+
* `--include-ownership` is a MODIFIER, not a mode: it appends an
|
|
118
119
|
* ownership block without touching exit semantics, so an ownership conflict
|
|
119
120
|
* still fails only the human `--ownership` mode.
|
|
120
121
|
* @param classified - Classified worktree entries
|
|
@@ -201,27 +202,39 @@ async function runStatusCommandBody(projectRoot, options) {
|
|
|
201
202
|
options.ownership,
|
|
202
203
|
options.testCoverage,
|
|
203
204
|
options.json,
|
|
205
|
+
options.lock,
|
|
204
206
|
].filter((v) => v === true).length;
|
|
205
207
|
if (modeCount > 1) {
|
|
206
|
-
throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, --test-coverage, and --json together. Pick at most one.');
|
|
208
|
+
throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, --test-coverage, --lock, and --json together. Pick at most one.');
|
|
207
209
|
}
|
|
208
|
-
// --summary elides the files[] payload for gate consumers
|
|
210
|
+
// --summary elides the files[] payload for gate consumers;
|
|
209
211
|
// it only makes sense on the JSON shape.
|
|
210
212
|
if (options.summary === true && options.json !== true) {
|
|
211
213
|
throw new InvalidArgumentError('--summary requires --json.', '--summary');
|
|
212
214
|
}
|
|
213
215
|
// --include-ownership adds a block to the JSON payload; it is meaningless
|
|
214
216
|
// on the human views, where --ownership is the mode that renders the
|
|
215
|
-
// table
|
|
217
|
+
// table.
|
|
216
218
|
if (options.includeOwnership === true && options.json !== true) {
|
|
217
219
|
throw new InvalidArgumentError('--include-ownership requires --json. Use --ownership for the human table.', '--include-ownership');
|
|
218
220
|
}
|
|
219
|
-
// --check / --fail-on enforcement policy
|
|
221
|
+
// --check / --fail-on enforcement policy. Applies only
|
|
220
222
|
// where classification runs (the default view and --json).
|
|
221
223
|
const checkPolicy = resolveStatusCheckPolicy(options);
|
|
222
224
|
if (!options.raw && !options.json) {
|
|
223
225
|
intro('FireForge Status');
|
|
224
226
|
}
|
|
227
|
+
// Lock visibility. Placed before every engine/git guard: the
|
|
228
|
+
// whole point is to answer "who is holding this?" on a checkout that a
|
|
229
|
+
// concurrent command is busy with, and that answer must not depend on
|
|
230
|
+
// engine/ being readable.
|
|
231
|
+
if (options.lock === true) {
|
|
232
|
+
for (const line of formatEngineSessionLockStatus(await readEngineSessionLockStatus(projectRoot))) {
|
|
233
|
+
info(line);
|
|
234
|
+
}
|
|
235
|
+
outro('Lock status');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
225
238
|
// Test-coverage mode needs only the project root (the baseline lives
|
|
226
239
|
// in .fireforge/), so it short-circuits before any engine/git guards.
|
|
227
240
|
if (options.testCoverage) {
|
|
@@ -247,7 +260,7 @@ async function runStatusCommandBody(projectRoot, options) {
|
|
|
247
260
|
}
|
|
248
261
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
249
262
|
// Same scan and the SAME classification pass as every other mode
|
|
250
|
-
|
|
263
|
+
//. This branch keeps its historical guard shape on purpose:
|
|
251
264
|
// no baseline-commit assertion, and a non-git engine degrades to an
|
|
252
265
|
// empty table rather than the recovery banner.
|
|
253
266
|
const rawFilesOwnership = await scanEngineStatusFiles(paths.engine, false);
|
|
@@ -336,6 +349,7 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
|
|
|
336
349
|
.command('status')
|
|
337
350
|
.description('Show modified files in engine/')
|
|
338
351
|
.option('--raw', 'Show raw worktree status without patch classification')
|
|
352
|
+
.option('--lock', 'Report the engine session lock instead of file status: holder pid, its command, how long it has held the lock, and how many waiters are queued behind it (read-only; never acquires it)')
|
|
339
353
|
.option('--unmanaged', 'Show only unmanaged changes (not covered by patches or tools)')
|
|
340
354
|
.option('--ownership', 'Show a flat path → owning patch table (flags files claimed by multiple patches)')
|
|
341
355
|
.option('--test-coverage', 'Show what the last recorded build covers for test packaging (full or scoped paths)')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pre-test build phase for `fireforge test`, split out of `test.ts` (at
|
|
3
3
|
* the per-file line budget): the incremental protected mach build with
|
|
4
|
-
* its baseline write, and the
|
|
4
|
+
* its baseline write, and the `--build-only` union-build shape.
|
|
5
5
|
*/
|
|
6
6
|
import type { getProjectPaths, loadConfig } from '../core/config.js';
|
|
7
7
|
import type { TestOptions } from '../types/commands/index.js';
|
|
@@ -9,7 +9,7 @@ import type { HarnessClassification } from './test-modes.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Runs the pre-test build (or the stale-build gate when no build was
|
|
11
11
|
* requested). Returns `true` when the run is COMPLETE — the `--build-only`
|
|
12
|
-
* union-build shape
|
|
12
|
+
* union-build shape, which packages every requested path
|
|
13
13
|
* (mixed harnesses legal, nothing dispatches), prints the per-harness
|
|
14
14
|
* next steps, and emits the run's single `PASS` verdict.
|
|
15
15
|
*/
|