@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
|
@@ -2,14 +2,32 @@
|
|
|
2
2
|
import { rm } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { FurnaceError } from '../errors/furnace.js';
|
|
5
|
+
import { assert } from '../utils/assert.js';
|
|
5
6
|
import { toError } from '../utils/errors.js';
|
|
6
7
|
import { verbose, warn } from '../utils/logger.js';
|
|
7
8
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
8
|
-
import { withFileLock } from './file-lock.js';
|
|
9
|
+
import { readLockStatus, withFileLock } from './file-lock.js';
|
|
9
10
|
import { loadFurnaceState, updateFurnaceState } from './furnace-config.js';
|
|
10
11
|
import { restoreRollbackJournal } from './furnace-rollback.js';
|
|
11
12
|
/** Sidecar lock filename used to serialize concurrent furnace mutations. */
|
|
12
13
|
const FURNACE_LOCK_FILENAME = 'furnace.lock';
|
|
14
|
+
/**
|
|
15
|
+
* Claims the rollback for one operation, or reports that another path
|
|
16
|
+
* already has it.
|
|
17
|
+
*
|
|
18
|
+
* Must stay synchronous and must be called before the caller's first
|
|
19
|
+
* await — that is what makes the claim atomic against the signal handler.
|
|
20
|
+
*
|
|
21
|
+
* @param operation - The in-flight operation to claim
|
|
22
|
+
* @returns True when the caller now owns the rollback
|
|
23
|
+
*/
|
|
24
|
+
function claimRollback(operation) {
|
|
25
|
+
if (operation.rollbackState !== undefined) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
operation.rollbackState = 'in-flight';
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
13
31
|
const activeOperations = new Map();
|
|
14
32
|
let nextOperationToken = 1;
|
|
15
33
|
let signalRollbackInFlight = false;
|
|
@@ -69,6 +87,13 @@ export async function rollbackActiveOperationsForSignal(signal) {
|
|
|
69
87
|
signalRollbackInFlight = true;
|
|
70
88
|
warn(`Received ${signal}; rolling back in-flight furnace mutations…`);
|
|
71
89
|
for (const op of snapshot) {
|
|
90
|
+
// The body may already be rolling back on its own throw path. Claim the
|
|
91
|
+
// operation (or skip it) before the first await so the two paths cannot
|
|
92
|
+
// both restore the same journal.
|
|
93
|
+
if (!claimRollback(op)) {
|
|
94
|
+
verbose(`Rollback for ${op.kind} already in progress; leaving it to the throw path.`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
72
97
|
const cleanupErrors = [];
|
|
73
98
|
// Run extra cleanup callbacks first (e.g. preview's cleanStories), so the
|
|
74
99
|
// engine is in its tidiest possible shape before the journal restore
|
|
@@ -98,6 +123,7 @@ export async function rollbackActiveOperationsForSignal(signal) {
|
|
|
98
123
|
catch (error) {
|
|
99
124
|
rollbackError = toError(error).message;
|
|
100
125
|
}
|
|
126
|
+
op.rollbackState = 'done';
|
|
101
127
|
// A clean signal-driven rollback is not itself a repairable problem:
|
|
102
128
|
// preview/apply/deploy/remove were interrupted, but the engine was restored
|
|
103
129
|
// successfully and the next `doctor` run should remain green. Persist a
|
|
@@ -119,6 +145,75 @@ export async function rollbackActiveOperationsForSignal(signal) {
|
|
|
119
145
|
}
|
|
120
146
|
}
|
|
121
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Rolls back one operation whose body threw, mirroring the signal path.
|
|
150
|
+
*
|
|
151
|
+
* Never throws: the body's own error is what the operator needs to see, so
|
|
152
|
+
* a rollback failure becomes a `pendingRepair` marker plus a warning
|
|
153
|
+
* rather than replacing it. Skips entirely when the body already restored
|
|
154
|
+
* its own journal and said so via `ctx.markRolledBack()`.
|
|
155
|
+
*
|
|
156
|
+
* @param operation - The in-flight operation whose body threw
|
|
157
|
+
*/
|
|
158
|
+
async function rollbackOperationForThrow(operation) {
|
|
159
|
+
if (!claimRollback(operation)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const cleanupErrors = [];
|
|
163
|
+
// Cleanups before the journal restore, for the same reason the signal path
|
|
164
|
+
// does it in that order: the restore writes original contents back, and it
|
|
165
|
+
// should do so over the tidiest possible tree.
|
|
166
|
+
for (const cleanup of operation.cleanups) {
|
|
167
|
+
try {
|
|
168
|
+
await withTimeout(cleanup(), SIGNAL_ROLLBACK_TIMEOUT_MS, 'Cleanup callback');
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
cleanupErrors.push(toError(error).message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!operation.journal) {
|
|
175
|
+
// Nothing was captured, so there is nothing to restore. Unlike the signal
|
|
176
|
+
// path we do NOT write a pendingRepair marker here: a body that threw
|
|
177
|
+
// before registering a journal is overwhelmingly a refusal raised during
|
|
178
|
+
// pre-flight (a validation failure, a missing file), and marking the root
|
|
179
|
+
// as needing repair would block every later furnace mutation behind a
|
|
180
|
+
// `doctor --repair-furnace` that has nothing to reconcile. Bodies that
|
|
181
|
+
// genuinely mutate before registering a journal are the ones the
|
|
182
|
+
// journal-before-mutation assertions catch.
|
|
183
|
+
operation.rollbackState = 'done';
|
|
184
|
+
if (cleanupErrors.length > 0) {
|
|
185
|
+
warn(`Cleanup after a failed ${operation.kind} reported: ${cleanupErrors.join('; ')}`);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let rollbackError;
|
|
190
|
+
try {
|
|
191
|
+
await withTimeout(restoreRollbackJournal(operation.journal), SIGNAL_ROLLBACK_TIMEOUT_MS, 'Rollback journal restore');
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
rollbackError = toError(error).message;
|
|
195
|
+
}
|
|
196
|
+
operation.rollbackState = 'done';
|
|
197
|
+
if (!rollbackError && cleanupErrors.length === 0) {
|
|
198
|
+
verbose(`Rolled back ${operation.kind} after a failure; engine restored.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const reasonParts = [`${operation.kind} failed`];
|
|
202
|
+
if (rollbackError) {
|
|
203
|
+
reasonParts.push(`automatic rollback failed: ${rollbackError}`);
|
|
204
|
+
warn(`Automatic rollback after a failed ${operation.kind} did not complete: ${rollbackError}. ` +
|
|
205
|
+
'Run "fireforge doctor --repair-furnace" to reconcile.');
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
reasonParts.push('automatic rollback succeeded');
|
|
209
|
+
}
|
|
210
|
+
if (cleanupErrors.length > 0) {
|
|
211
|
+
reasonParts.push(`cleanup errors: ${cleanupErrors.join('; ')}`);
|
|
212
|
+
}
|
|
213
|
+
await persistPendingRepair(operation.root, operation.kind, reasonParts.join('; ')).catch((error) => {
|
|
214
|
+
warn(`Could not persist pending-repair marker: ${toError(error).message}`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
122
217
|
async function persistPendingRepair(root, operation, reason) {
|
|
123
218
|
await updateFurnaceState(root, (state) => ({
|
|
124
219
|
...state,
|
|
@@ -189,6 +284,7 @@ export async function runFurnaceMutation(root, kind, body, options = {}) {
|
|
|
189
284
|
return body({
|
|
190
285
|
registerJournal: () => undefined,
|
|
191
286
|
registerCleanup: () => undefined,
|
|
287
|
+
markRolledBack: () => undefined,
|
|
192
288
|
});
|
|
193
289
|
}
|
|
194
290
|
// Pre-flight: refuse to mutate when a previous operation left the engine in
|
|
@@ -214,6 +310,16 @@ export async function runFurnaceMutation(root, kind, body, options = {}) {
|
|
|
214
310
|
'A previous fireforge process may have crashed.',
|
|
215
311
|
};
|
|
216
312
|
return withFileLock(lockPath, async () => {
|
|
313
|
+
// Everything the body does from here is serialized only by the furnace
|
|
314
|
+
// lock, so confirm we are actually inside it before registering as a
|
|
315
|
+
// live mutation. The owner record is deliberately best-effort in
|
|
316
|
+
// `withFileLock` (a live holder can legitimately have no readable PID),
|
|
317
|
+
// so the PID half only tightens the check when a record is present —
|
|
318
|
+
// asserting on its existence would fire on a lock we really do hold.
|
|
319
|
+
const lockStatus = await readLockStatus(lockPath);
|
|
320
|
+
assert(lockStatus.held, 'furnace lock held before the mutation body runs');
|
|
321
|
+
assert(lockStatus.holder === undefined || lockStatus.holder.pid === process.pid, () => `furnace lock is owned by this process ` +
|
|
322
|
+
`(held by PID ${String(lockStatus.holder?.pid)}, we are ${String(process.pid)})`);
|
|
217
323
|
activeOperations.set(token, operation);
|
|
218
324
|
try {
|
|
219
325
|
return await body({
|
|
@@ -223,8 +329,26 @@ export async function runFurnaceMutation(root, kind, body, options = {}) {
|
|
|
223
329
|
registerCleanup: (cleanup) => {
|
|
224
330
|
operation.cleanups.push(cleanup);
|
|
225
331
|
},
|
|
332
|
+
markRolledBack: () => {
|
|
333
|
+
operation.rollbackState = 'done';
|
|
334
|
+
},
|
|
226
335
|
});
|
|
227
336
|
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
// A thrown error leaves the engine mutated exactly as a signal does,
|
|
339
|
+
// so it gets the same treatment. Before 0.43.0 this was a bare
|
|
340
|
+
// finally: the throw path deregistered the operation and marked it
|
|
341
|
+
// completed, which removed it from the signal handler's view too, so
|
|
342
|
+
// a body that threw outside its own catch (notably anywhere in
|
|
343
|
+
// `applyAllComponents` outside its two per-component try blocks) left
|
|
344
|
+
// the checkout torn with no marker.
|
|
345
|
+
//
|
|
346
|
+
// This runs inside the withFileLock callback, so the furnace lock is
|
|
347
|
+
// still held for the duration of the restore — a competing process
|
|
348
|
+
// cannot start mutating half-way through it.
|
|
349
|
+
await rollbackOperationForThrow(operation);
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
228
352
|
finally {
|
|
229
353
|
operation.completed = true;
|
|
230
354
|
activeOperations.delete(token);
|
|
@@ -24,6 +24,23 @@ export declare function recordCreatedDir(journal: RollbackJournal, dirPath: stri
|
|
|
24
24
|
export declare function snapshotDir(journal: RollbackJournal, dirPath: string): Promise<void>;
|
|
25
25
|
/** Snapshots a file once so rollback can restore its previous contents or absence. */
|
|
26
26
|
export declare function snapshotFile(journal: RollbackJournal, filePath: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Asserts that a file about to be mutated was already snapshotted into the
|
|
29
|
+
* journal that will undo the mutation.
|
|
30
|
+
*
|
|
31
|
+
* The snapshot list and the mutation list are separately maintained in every
|
|
32
|
+
* caller that writes conditionally, and they drift: a new conditional write
|
|
33
|
+
* whose matching snapshot condition was not updated produces a file the
|
|
34
|
+
* rollback cannot restore, and nothing notices until a real failure. This
|
|
35
|
+
* checks the pairing at the point of the write, where the two lists are
|
|
36
|
+
* supposed to agree.
|
|
37
|
+
*
|
|
38
|
+
* @param journal - The journal that will restore this file
|
|
39
|
+
* @param filePath - Absolute path about to be written
|
|
40
|
+
* @param context - What is about to write it, for the failure message
|
|
41
|
+
* @throws {@link InternalInvariantError} when the file was never snapshotted.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertSnapshotted(journal: RollbackJournal, filePath: string, context: string): void;
|
|
27
44
|
/** Restores all snapshotted files and removes directories created during the operation. */
|
|
28
45
|
export declare function restoreRollbackJournal(journal: RollbackJournal): Promise<void>;
|
|
29
46
|
/** Restores a rollback journal and wraps rollback failures in a FurnaceError. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { chmod, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
4
4
|
import { FurnaceError } from '../errors/furnace.js';
|
|
5
|
+
import { assert } from '../utils/assert.js';
|
|
5
6
|
import { toError } from '../utils/errors.js';
|
|
6
7
|
import { pathExists } from '../utils/fs.js';
|
|
7
8
|
/** Creates an empty rollback journal for tracking touched files and created directories. */
|
|
@@ -45,6 +46,13 @@ export async function snapshotDir(journal, dirPath) {
|
|
|
45
46
|
}
|
|
46
47
|
/** Snapshots a file once so rollback can restore its previous contents or absence. */
|
|
47
48
|
export async function snapshotFile(journal, filePath) {
|
|
49
|
+
// Journal keys are the paths rollback will later write back to, and the
|
|
50
|
+
// restore runs from whatever cwd the process happens to be in by then. A
|
|
51
|
+
// relative key would therefore restore somewhere other than where the
|
|
52
|
+
// snapshot was taken — silently, and only on the failure path where nobody
|
|
53
|
+
// is watching. Every caller composes an absolute path already; this keeps
|
|
54
|
+
// it that way.
|
|
55
|
+
assert(isAbsolute(filePath), () => `snapshot path is absolute (got "${filePath}")`);
|
|
48
56
|
if (journal.files.has(filePath)) {
|
|
49
57
|
return;
|
|
50
58
|
}
|
|
@@ -59,6 +67,25 @@ export async function snapshotFile(journal, filePath) {
|
|
|
59
67
|
mode: fileStat.mode,
|
|
60
68
|
});
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Asserts that a file about to be mutated was already snapshotted into the
|
|
72
|
+
* journal that will undo the mutation.
|
|
73
|
+
*
|
|
74
|
+
* The snapshot list and the mutation list are separately maintained in every
|
|
75
|
+
* caller that writes conditionally, and they drift: a new conditional write
|
|
76
|
+
* whose matching snapshot condition was not updated produces a file the
|
|
77
|
+
* rollback cannot restore, and nothing notices until a real failure. This
|
|
78
|
+
* checks the pairing at the point of the write, where the two lists are
|
|
79
|
+
* supposed to agree.
|
|
80
|
+
*
|
|
81
|
+
* @param journal - The journal that will restore this file
|
|
82
|
+
* @param filePath - Absolute path about to be written
|
|
83
|
+
* @param context - What is about to write it, for the failure message
|
|
84
|
+
* @throws {@link InternalInvariantError} when the file was never snapshotted.
|
|
85
|
+
*/
|
|
86
|
+
export function assertSnapshotted(journal, filePath, context) {
|
|
87
|
+
assert(journal.files.has(filePath), () => `${context} snapshotted "${filePath}" before mutating it`);
|
|
88
|
+
}
|
|
62
89
|
async function restoreFile(filePath, snapshot) {
|
|
63
90
|
if (!snapshot.existed) {
|
|
64
91
|
await rm(filePath, { force: true });
|
|
@@ -119,6 +146,13 @@ export async function restoreRollbackJournal(journal) {
|
|
|
119
146
|
const summary = errors.map((e) => `${e.path}: ${e.error}`).join('; ');
|
|
120
147
|
throw new FurnaceError(`Rollback failed to restore ${errors.length} file(s): ${summary}`);
|
|
121
148
|
}
|
|
149
|
+
// Directory removal is only safe once every file is back where it belongs:
|
|
150
|
+
// a created directory may hold a restored file, and removing it recursively
|
|
151
|
+
// would undo the restore we just performed. The throw above is what
|
|
152
|
+
// normally enforces this; the assertion names it so a future early-return
|
|
153
|
+
// or error-swallowing edit cannot quietly reach the rm loop with failures
|
|
154
|
+
// outstanding.
|
|
155
|
+
assert(errors.length === 0, 'all journal files restored before created directories are removed');
|
|
122
156
|
const createdDirs = [...journal.createdDirs].sort((left, right) => right.length - left.length);
|
|
123
157
|
for (const dirPath of createdDirs) {
|
|
124
158
|
await rm(dirPath, { recursive: true, force: true });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { GitError, GitNotFoundError } from '../errors/git.js';
|
|
3
3
|
import { exec, executableExists } from '../utils/process.js';
|
|
4
|
+
import { readOnlyGitIndexEnv } from './git-readonly-index.js';
|
|
4
5
|
/**
|
|
5
6
|
* Environment variable that overrides the monolithic `git add -A` timeout
|
|
6
7
|
* (milliseconds). 2026-04-24 eval Finding 10: operators on slow or loaded
|
|
@@ -63,8 +64,12 @@ export async function git(args, cwd, options) {
|
|
|
63
64
|
if (options?.timeout !== undefined) {
|
|
64
65
|
execOptions.timeout = options.timeout;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
// A read-only command's private-index scope overlays GIT_INDEX_FILE so
|
|
68
|
+
// index refreshes never touch the primary checkout.
|
|
69
|
+
// Merged UNDER the caller's own env so an explicit override still wins.
|
|
70
|
+
const readOnlyIndexEnv = readOnlyGitIndexEnv(cwd);
|
|
71
|
+
if (options?.env !== undefined || readOnlyIndexEnv !== undefined) {
|
|
72
|
+
execOptions.env = { ...readOnlyIndexEnv, ...options?.env };
|
|
68
73
|
}
|
|
69
74
|
const result = await exec('git', args, execOptions);
|
|
70
75
|
if (result.exitCode !== 0) {
|
|
@@ -9,9 +9,18 @@ import { verbose } from '../utils/logger.js';
|
|
|
9
9
|
import { exec } from '../utils/process.js';
|
|
10
10
|
import { chunkPathspecs, ensureGit, git } from './git-base.js';
|
|
11
11
|
import { fileExistsInHead, hashObjectBatch, isBinaryFile, listTrackedInHead, } from './git-file-ops.js';
|
|
12
|
+
import { readOnlyGitIndexEnv } from './git-readonly-index.js';
|
|
12
13
|
import { getUntrackedFiles, getUntrackedFilesInDir } from './git-status.js';
|
|
13
14
|
async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0]) {
|
|
14
|
-
|
|
15
|
+
// Honour a read-only command's private-index scope: the `--intent-to-add`
|
|
16
|
+
// / `update-index` staging dance below is a genuine
|
|
17
|
+
// index write, and inside a read-only command it must land on the
|
|
18
|
+
// private index, not the primary checkout's.
|
|
19
|
+
const readOnlyIndexEnv = readOnlyGitIndexEnv(repoDir);
|
|
20
|
+
const result = await exec('git', args, {
|
|
21
|
+
cwd: repoDir,
|
|
22
|
+
...(readOnlyIndexEnv !== undefined ? { env: readOnlyIndexEnv } : {}),
|
|
23
|
+
});
|
|
15
24
|
if (allowedExitCodes.includes(result.exitCode)) {
|
|
16
25
|
return result;
|
|
17
26
|
}
|
|
@@ -466,7 +475,7 @@ export async function generateBinaryFilePatch(repoDir, filePath) {
|
|
|
466
475
|
// *removing* whatever entry the index carried, so any staged state that
|
|
467
476
|
// arrived between the read-only diff above and this staging block (a
|
|
468
477
|
// concurrent writer, or a staged-add whose worktree file was deleted)
|
|
469
|
-
// was silently discarded
|
|
478
|
+
// was silently discarded. Restoring the captured entry
|
|
470
479
|
// instead makes the cleanup exact regardless of how the entry got there.
|
|
471
480
|
const priorEntry = (await execGitWithAllowedExitCodes(repoDir, ['ls-files', '--stage', '--', filePath])).stdout;
|
|
472
481
|
try {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private-index scope for FireForge's READ-ONLY git plumbing.
|
|
3
|
+
*
|
|
4
|
+
* `git status` and `git diff HEAD` are read-only in the sense operators
|
|
5
|
+
* care about — they never change tracked content — but they are NOT
|
|
6
|
+
* read-only to git: both refresh the index's stat cache and write
|
|
7
|
+
* `.git/index` (through `.git/index.lock`) when a file's stat data has
|
|
8
|
+
* moved. That makes `fireforge verify` / `lint --per-patch` / `typecheck`
|
|
9
|
+
* a second WRITER of the primary `engine/` checkout, even when the command
|
|
10
|
+
* was pointed at a CoW tree clone.
|
|
11
|
+
*
|
|
12
|
+
* The consequence is not cosmetic. A concurrent `fireforge test`
|
|
13
|
+
* fingerprints `engine/` before and after the harness run and refuses a
|
|
14
|
+
* verdict taken across a change (`FAIL reason=inconclusive`). That refusal
|
|
15
|
+
* is CORRECT and stays; the defect is that FireForge's own read-only
|
|
16
|
+
* commands were tripping it. Measured downstream: perfect correlation
|
|
17
|
+
* between lane overlap and inconclusive verdicts, on suites that had
|
|
18
|
+
* actually passed.
|
|
19
|
+
*
|
|
20
|
+
* The fix is the standard git one: point the plumbing at a PRIVATE index
|
|
21
|
+
* (`GIT_INDEX_FILE`) seeded from the repository's own, in a temp
|
|
22
|
+
* directory. Refreshes land there and are thrown away; the primary
|
|
23
|
+
* `.git/index` — and `.git/index.lock`, which is what made a concurrent
|
|
24
|
+
* `git status` fail outright rather than merely churn — is never touched.
|
|
25
|
+
*
|
|
26
|
+
* Fail-open by design: if the git dir or the index cannot be resolved or
|
|
27
|
+
* copied, the scope simply does not activate and the commands behave
|
|
28
|
+
* exactly as before. A read-only command must not start failing because
|
|
29
|
+
* an optimisation could not be set up.
|
|
30
|
+
*
|
|
31
|
+
* The scope is process-global and non-reentrant on purpose: FireForge
|
|
32
|
+
* commands are one-shot CLI invocations, and nesting two different private
|
|
33
|
+
* indexes for the same run has no meaning. A nested call reuses the outer
|
|
34
|
+
* scope.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Environment overlay a git invocation in `cwd` must carry, or undefined
|
|
38
|
+
* when no private-index scope covers it.
|
|
39
|
+
*
|
|
40
|
+
* Matching is by resolved path prefix: plumbing frequently runs with `cwd`
|
|
41
|
+
* set to a subdirectory of the scoped repository, and those invocations
|
|
42
|
+
* share the same index.
|
|
43
|
+
*
|
|
44
|
+
* @param cwd - Working directory the git process will run in
|
|
45
|
+
* @returns `{ GIT_INDEX_FILE }` when scoped, otherwise undefined
|
|
46
|
+
*/
|
|
47
|
+
export declare function readOnlyGitIndexEnv(cwd: string): Record<string, string> | undefined;
|
|
48
|
+
/** True while a private-index scope is installed (diagnostics and tests). */
|
|
49
|
+
export declare function hasReadOnlyGitIndexScope(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Runs `operation` with FireForge's git plumbing pointed at a private
|
|
52
|
+
* index seeded from `repoDir`'s own, so the primary checkout's
|
|
53
|
+
* `.git/index` (and `.git/index.lock`) are never written.
|
|
54
|
+
*
|
|
55
|
+
* @param repoDir - Repository whose plumbing should use a private index
|
|
56
|
+
* @param operation - The read-only command body
|
|
57
|
+
* @returns Whatever `operation` returns
|
|
58
|
+
*/
|
|
59
|
+
export declare function withPrivateGitIndex<T>(repoDir: string, operation: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Private-index scope for FireForge's READ-ONLY git plumbing.
|
|
4
|
+
*
|
|
5
|
+
* `git status` and `git diff HEAD` are read-only in the sense operators
|
|
6
|
+
* care about — they never change tracked content — but they are NOT
|
|
7
|
+
* read-only to git: both refresh the index's stat cache and write
|
|
8
|
+
* `.git/index` (through `.git/index.lock`) when a file's stat data has
|
|
9
|
+
* moved. That makes `fireforge verify` / `lint --per-patch` / `typecheck`
|
|
10
|
+
* a second WRITER of the primary `engine/` checkout, even when the command
|
|
11
|
+
* was pointed at a CoW tree clone.
|
|
12
|
+
*
|
|
13
|
+
* The consequence is not cosmetic. A concurrent `fireforge test`
|
|
14
|
+
* fingerprints `engine/` before and after the harness run and refuses a
|
|
15
|
+
* verdict taken across a change (`FAIL reason=inconclusive`). That refusal
|
|
16
|
+
* is CORRECT and stays; the defect is that FireForge's own read-only
|
|
17
|
+
* commands were tripping it. Measured downstream: perfect correlation
|
|
18
|
+
* between lane overlap and inconclusive verdicts, on suites that had
|
|
19
|
+
* actually passed.
|
|
20
|
+
*
|
|
21
|
+
* The fix is the standard git one: point the plumbing at a PRIVATE index
|
|
22
|
+
* (`GIT_INDEX_FILE`) seeded from the repository's own, in a temp
|
|
23
|
+
* directory. Refreshes land there and are thrown away; the primary
|
|
24
|
+
* `.git/index` — and `.git/index.lock`, which is what made a concurrent
|
|
25
|
+
* `git status` fail outright rather than merely churn — is never touched.
|
|
26
|
+
*
|
|
27
|
+
* Fail-open by design: if the git dir or the index cannot be resolved or
|
|
28
|
+
* copied, the scope simply does not activate and the commands behave
|
|
29
|
+
* exactly as before. A read-only command must not start failing because
|
|
30
|
+
* an optimisation could not be set up.
|
|
31
|
+
*
|
|
32
|
+
* The scope is process-global and non-reentrant on purpose: FireForge
|
|
33
|
+
* commands are one-shot CLI invocations, and nesting two different private
|
|
34
|
+
* indexes for the same run has no meaning. A nested call reuses the outer
|
|
35
|
+
* scope.
|
|
36
|
+
*/
|
|
37
|
+
import { copyFile } from 'node:fs/promises';
|
|
38
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
39
|
+
import { tmpdir } from 'node:os';
|
|
40
|
+
import { join, resolve } from 'node:path';
|
|
41
|
+
import { toError } from '../utils/errors.js';
|
|
42
|
+
import { pathExists } from '../utils/fs.js';
|
|
43
|
+
import { verbose } from '../utils/logger.js';
|
|
44
|
+
import { exec } from '../utils/process.js';
|
|
45
|
+
/** The active private-index scope, when a read-only command installed one. */
|
|
46
|
+
let activeScope;
|
|
47
|
+
/** Bound on the git-dir probe so a wedged git cannot stall a read-only run. */
|
|
48
|
+
const GIT_DIR_PROBE_TIMEOUT_MS = 30_000;
|
|
49
|
+
/**
|
|
50
|
+
* Environment overlay a git invocation in `cwd` must carry, or undefined
|
|
51
|
+
* when no private-index scope covers it.
|
|
52
|
+
*
|
|
53
|
+
* Matching is by resolved path prefix: plumbing frequently runs with `cwd`
|
|
54
|
+
* set to a subdirectory of the scoped repository, and those invocations
|
|
55
|
+
* share the same index.
|
|
56
|
+
*
|
|
57
|
+
* @param cwd - Working directory the git process will run in
|
|
58
|
+
* @returns `{ GIT_INDEX_FILE }` when scoped, otherwise undefined
|
|
59
|
+
*/
|
|
60
|
+
export function readOnlyGitIndexEnv(cwd) {
|
|
61
|
+
if (activeScope === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
const resolved = resolve(cwd);
|
|
64
|
+
const scoped = activeScope.repoDir;
|
|
65
|
+
if (resolved !== scoped && !resolved.startsWith(`${scoped}/`))
|
|
66
|
+
return undefined;
|
|
67
|
+
return { GIT_INDEX_FILE: activeScope.indexFile };
|
|
68
|
+
}
|
|
69
|
+
/** True while a private-index scope is installed (diagnostics and tests). */
|
|
70
|
+
export function hasReadOnlyGitIndexScope() {
|
|
71
|
+
return activeScope !== undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves a repository's git directory. Returns undefined for anything
|
|
75
|
+
* that is not a readable git checkout — `engine/` is not always one
|
|
76
|
+
* (`fireforge download` extracts a source tarball).
|
|
77
|
+
*/
|
|
78
|
+
async function resolveGitDir(repoDir) {
|
|
79
|
+
try {
|
|
80
|
+
const result = await exec('git', ['rev-parse', '--absolute-git-dir'], {
|
|
81
|
+
cwd: repoDir,
|
|
82
|
+
timeout: GIT_DIR_PROBE_TIMEOUT_MS,
|
|
83
|
+
});
|
|
84
|
+
if (result.exitCode !== 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
const gitDir = result.stdout.trim();
|
|
87
|
+
return gitDir.length > 0 ? gitDir : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
verbose(`Private git index: could not resolve the git dir — ${toError(error).message}`);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Runs `operation` with FireForge's git plumbing pointed at a private
|
|
96
|
+
* index seeded from `repoDir`'s own, so the primary checkout's
|
|
97
|
+
* `.git/index` (and `.git/index.lock`) are never written.
|
|
98
|
+
*
|
|
99
|
+
* @param repoDir - Repository whose plumbing should use a private index
|
|
100
|
+
* @param operation - The read-only command body
|
|
101
|
+
* @returns Whatever `operation` returns
|
|
102
|
+
*/
|
|
103
|
+
export async function withPrivateGitIndex(repoDir, operation) {
|
|
104
|
+
if (activeScope !== undefined)
|
|
105
|
+
return operation();
|
|
106
|
+
const gitDir = await resolveGitDir(repoDir);
|
|
107
|
+
if (gitDir === undefined)
|
|
108
|
+
return operation();
|
|
109
|
+
let tempDir;
|
|
110
|
+
try {
|
|
111
|
+
tempDir = await mkdtemp(join(tmpdir(), 'fireforge-roindex-'));
|
|
112
|
+
const privateIndex = join(tempDir, 'index');
|
|
113
|
+
const sourceIndex = join(gitDir, 'index');
|
|
114
|
+
// A repository with no index yet (a fresh init) is fine: git creates
|
|
115
|
+
// the private one on first use.
|
|
116
|
+
if (await pathExists(sourceIndex)) {
|
|
117
|
+
await copyFile(sourceIndex, privateIndex);
|
|
118
|
+
}
|
|
119
|
+
activeScope = { repoDir: resolve(repoDir), indexFile: privateIndex };
|
|
120
|
+
verbose(`Private git index active for ${repoDir} (${privateIndex}).`);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// Fail-open: an unset-up scope must never fail a read-only command.
|
|
124
|
+
verbose(`Private git index unavailable — ${toError(error).message}`);
|
|
125
|
+
activeScope = undefined;
|
|
126
|
+
if (tempDir !== undefined)
|
|
127
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
128
|
+
return operation();
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return await operation();
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
activeScope = undefined;
|
|
135
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -86,7 +86,7 @@ export declare function containsUpstreamLicenseText(content: string, maxLines?:
|
|
|
86
86
|
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
87
87
|
* comment style. Used by `export` to treat such files as vendored: offering
|
|
88
88
|
* to prepend the project's own license header onto a byte-identity-pinned
|
|
89
|
-
* upstream bundle would mislicense third-party code
|
|
89
|
+
* upstream bundle would mislicense third-party code.
|
|
90
90
|
*
|
|
91
91
|
* @param content - File content to check
|
|
92
92
|
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
@@ -257,7 +257,7 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
|
|
|
257
257
|
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
258
258
|
* comment style. Used by `export` to treat such files as vendored: offering
|
|
259
259
|
* to prepend the project's own license header onto a byte-identity-pinned
|
|
260
|
-
* upstream bundle would mislicense third-party code
|
|
260
|
+
* upstream bundle would mislicense third-party code.
|
|
261
261
|
*
|
|
262
262
|
* @param content - File content to check
|
|
263
263
|
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
@@ -1,15 +1,20 @@
|
|
|
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 = 4;
|
|
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
|
|
9
|
+
/** Issues dropped by the patch's lintIgnore waivers. */
|
|
10
10
|
suppressed: PatchLintIssue[];
|
|
11
11
|
/** Non-binary diff line count measured by the cached lint run. */
|
|
12
12
|
lineCount: number;
|
|
13
|
+
/**
|
|
14
|
+
* Sorted lint-ignore waiver ids in force when this entry was computed
|
|
15
|
+
*. A lookup presenting a different effective set misses.
|
|
16
|
+
*/
|
|
17
|
+
lintIgnore: string[];
|
|
13
18
|
updatedAt: string;
|
|
14
19
|
}
|
|
15
20
|
export interface PerPatchLintCacheFile {
|
|
@@ -46,7 +51,12 @@ export interface CachedPerPatchLint {
|
|
|
46
51
|
suppressed: PatchLintIssue[];
|
|
47
52
|
lineCount: number;
|
|
48
53
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Returns the cached lint payload for a patch when the stored key still
|
|
56
|
+
* matches AND the entry was computed under the same lint-ignore waiver set
|
|
57
|
+
*. A waiver change makes a replayed verdict actively wrong, so
|
|
58
|
+
* the waiver set is compared explicitly rather than trusted to the key.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string, lintIgnore?: Iterable<string>): CachedPerPatchLint | undefined;
|
|
51
61
|
/** 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;
|
|
62
|
+
export declare function setCachedPerPatchLintIssues(cache: PerPatchLintCacheFile, patchFilename: string, key: string, issues: PatchLintIssue[], suppressed: PatchLintIssue[], lineCount: number, lintIgnore?: Iterable<string>): void;
|