@hominis/fireforge 0.21.3 → 0.22.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 +68 -749
- package/README.md +52 -752
- package/dist/src/commands/doctor-furnace-manifest-sync.js +42 -4
- package/dist/src/commands/doctor.js +17 -3
- package/dist/src/commands/download.js +1 -1
- package/dist/src/commands/export-flow.d.ts +7 -0
- package/dist/src/commands/export-flow.js +33 -7
- package/dist/src/commands/export-placement-policy.d.ts +14 -0
- package/dist/src/commands/export-placement-policy.js +54 -0
- package/dist/src/commands/export.js +6 -2
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -0
- package/dist/src/commands/furnace/create-dry-run.js +7 -1
- package/dist/src/commands/furnace/create-validation.d.ts +1 -1
- package/dist/src/commands/furnace/create-validation.js +4 -3
- package/dist/src/commands/furnace/create.js +41 -3
- package/dist/src/commands/lint.d.ts +6 -0
- package/dist/src/commands/lint.js +22 -2
- package/dist/src/commands/patch/compact.js +18 -1
- package/dist/src/commands/token-coverage.js +61 -2
- package/dist/src/commands/verify.d.ts +19 -0
- package/dist/src/commands/verify.js +90 -61
- package/dist/src/core/patch-policy.d.ts +1 -1
- package/dist/src/core/patch-policy.js +23 -0
- package/dist/src/types/commands/options.d.ts +9 -1
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/g
|
|
|
7
7
|
import { measureTokenCoverage } from '../core/token-coverage.js';
|
|
8
8
|
import { getTokensCssPath } from '../core/token-manager.js';
|
|
9
9
|
import { GeneralError } from '../errors/base.js';
|
|
10
|
-
import { pathExists } from '../utils/fs.js';
|
|
10
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
11
11
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
12
12
|
/**
|
|
13
13
|
* Measures design token coverage across modified CSS files.
|
|
@@ -31,6 +31,9 @@ export async function tokenCoverageCommand(projectRoot) {
|
|
|
31
31
|
// and the file-extension filter could not see the .css inside.
|
|
32
32
|
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
33
33
|
const expandedStatus = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
34
|
+
const statusTokenCssFiles = expandedStatus
|
|
35
|
+
.filter((f) => f.file === tokensCssPath)
|
|
36
|
+
.map((f) => f.file);
|
|
34
37
|
const statusCssFiles = expandedStatus
|
|
35
38
|
.filter((f) => f.file.endsWith('.css') && f.file !== tokensCssPath)
|
|
36
39
|
.map((f) => f.file);
|
|
@@ -44,11 +47,27 @@ export async function tokenCoverageCommand(projectRoot) {
|
|
|
44
47
|
// De-dupe so a file that is both a custom deploy target AND modified is
|
|
45
48
|
// scanned exactly once.
|
|
46
49
|
const cssFiles = [...new Set([...statusCssFiles, ...furnaceCssFiles])];
|
|
47
|
-
|
|
50
|
+
const tokenSourceFiles = [...new Set(statusTokenCssFiles)];
|
|
51
|
+
if (cssFiles.length === 0 && tokenSourceFiles.length === 0) {
|
|
48
52
|
info('No modified CSS files');
|
|
49
53
|
outro('Nothing to measure');
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
56
|
+
const tokenPrefix = await resolveTokenPrefix(projectRoot, config.binaryName);
|
|
57
|
+
const tokenSourceResults = await validateTokenSourceFiles(paths.engine, tokenSourceFiles, tokenPrefix);
|
|
58
|
+
for (const result of tokenSourceResults) {
|
|
59
|
+
const detail = `${result.tokenDeclarations} token declaration${result.tokenDeclarations === 1 ? '' : 's'}`;
|
|
60
|
+
if (result.unknownDeclarations.length === 0) {
|
|
61
|
+
success(`${result.file} token source valid (${detail})`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
warn(`${result.file} token source has ${result.unknownDeclarations.length} declaration${result.unknownDeclarations.length === 1 ? '' : 's'} outside prefix ${tokenPrefix}: ${result.unknownDeclarations.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (cssFiles.length === 0) {
|
|
68
|
+
outro(`${tokenSourceResults.length} token source file${tokenSourceResults.length === 1 ? '' : 's'} validated`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
52
71
|
const report = await measureTokenCoverage(paths.engine, cssFiles);
|
|
53
72
|
// Per-file breakdown
|
|
54
73
|
for (const entry of report.files) {
|
|
@@ -73,6 +92,46 @@ export async function tokenCoverageCommand(projectRoot) {
|
|
|
73
92
|
}
|
|
74
93
|
outro(`${report.filesScanned} CSS file${report.filesScanned === 1 ? '' : 's'} scanned`);
|
|
75
94
|
}
|
|
95
|
+
async function resolveTokenPrefix(projectRoot, binaryName) {
|
|
96
|
+
try {
|
|
97
|
+
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
98
|
+
if (furnaceConfig.tokenPrefix) {
|
|
99
|
+
return furnaceConfig.tokenPrefix;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Fall through to the convention used by furnace init. A broken
|
|
104
|
+
// furnace.json is already surfaced by collectFurnaceCustomCssFiles.
|
|
105
|
+
}
|
|
106
|
+
return `--${binaryName}-`;
|
|
107
|
+
}
|
|
108
|
+
async function validateTokenSourceFiles(engineDir, tokenSourceFiles, tokenPrefix) {
|
|
109
|
+
const results = [];
|
|
110
|
+
for (const file of tokenSourceFiles) {
|
|
111
|
+
const filePath = join(engineDir, file);
|
|
112
|
+
if (!(await pathExists(filePath)))
|
|
113
|
+
continue;
|
|
114
|
+
const css = (await readText(filePath)).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
115
|
+
const declarations = new Set();
|
|
116
|
+
const declarationPattern = /(^|[;{\s])(--[\w-]+)\s*:/g;
|
|
117
|
+
let match;
|
|
118
|
+
while ((match = declarationPattern.exec(css)) !== null) {
|
|
119
|
+
const declaration = match[2];
|
|
120
|
+
if (declaration)
|
|
121
|
+
declarations.add(declaration);
|
|
122
|
+
}
|
|
123
|
+
const tokenDeclarations = [...declarations].filter((name) => name.startsWith(tokenPrefix));
|
|
124
|
+
const unknownDeclarations = [...declarations]
|
|
125
|
+
.filter((name) => !name.startsWith(tokenPrefix))
|
|
126
|
+
.sort();
|
|
127
|
+
results.push({
|
|
128
|
+
file,
|
|
129
|
+
tokenDeclarations: tokenDeclarations.length,
|
|
130
|
+
unknownDeclarations,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
76
135
|
/**
|
|
77
136
|
* Returns engine-relative `.css` paths deployed by every Furnace custom
|
|
78
137
|
* component registered in `furnace.json`. Only files that actually exist
|
|
@@ -15,6 +15,24 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Command } from 'commander';
|
|
17
17
|
import type { CommandContext } from '../types/cli.js';
|
|
18
|
+
interface VerifyIssueGroup {
|
|
19
|
+
title: string;
|
|
20
|
+
issues: string[];
|
|
21
|
+
errorCount: number;
|
|
22
|
+
warningCount: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PatchQueueHealth {
|
|
25
|
+
hasPatchesDirectory: boolean;
|
|
26
|
+
groups: VerifyIssueGroup[];
|
|
27
|
+
errorCount: number;
|
|
28
|
+
warningCount: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Collects the same queue-health findings reported by `fireforge verify`
|
|
32
|
+
* without printing. Used by doctor recovery paths that need a read-only
|
|
33
|
+
* "is this queue healthy?" decision before clearing stale state.
|
|
34
|
+
*/
|
|
35
|
+
export declare function collectPatchQueueHealth(projectRoot: string): Promise<PatchQueueHealth>;
|
|
18
36
|
/**
|
|
19
37
|
* Runs the `verify` command: manifest consistency + cross-patch lint.
|
|
20
38
|
* Read-only; exits non-zero on any error-severity finding.
|
|
@@ -29,3 +47,4 @@ export declare function verifyCommand(projectRoot: string): Promise<void>;
|
|
|
29
47
|
* @param context - Shared CLI registration context
|
|
30
48
|
*/
|
|
31
49
|
export declare function registerVerify(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|
|
50
|
+
export {};
|
|
@@ -101,100 +101,129 @@ function detectCrossPatchFileClaims(manifestPatches) {
|
|
|
101
101
|
return results;
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* @param projectRoot - Project root directory
|
|
104
|
+
* Collects the same queue-health findings reported by `fireforge verify`
|
|
105
|
+
* without printing. Used by doctor recovery paths that need a read-only
|
|
106
|
+
* "is this queue healthy?" decision before clearing stale state.
|
|
108
107
|
*/
|
|
109
|
-
export async function
|
|
110
|
-
intro('FireForge Verify');
|
|
108
|
+
export async function collectPatchQueueHealth(projectRoot) {
|
|
111
109
|
const paths = getProjectPaths(projectRoot);
|
|
112
110
|
const config = await loadConfig(projectRoot);
|
|
113
111
|
if (!(await pathExists(paths.patches))) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
return {
|
|
113
|
+
hasPatchesDirectory: false,
|
|
114
|
+
groups: [],
|
|
115
|
+
errorCount: 0,
|
|
116
|
+
warningCount: 0,
|
|
117
|
+
};
|
|
117
118
|
}
|
|
119
|
+
const groups = [];
|
|
118
120
|
let errorCount = 0;
|
|
119
121
|
let warningCount = 0;
|
|
120
|
-
// 1. Manifest consistency: orphan patch files, missing entries,
|
|
121
|
-
// files-affected mismatch, duplicate entries, unparseable manifest.
|
|
122
122
|
const consistencyIssues = await validatePatchesManifestConsistency(paths.patches);
|
|
123
123
|
if (consistencyIssues.length > 0) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
const issues = consistencyIssues.map((issue) => `[${issue.code}] ${issue.message}`);
|
|
125
|
+
groups.push({
|
|
126
|
+
title: `Manifest consistency issues (${consistencyIssues.length})`,
|
|
127
|
+
issues,
|
|
128
|
+
errorCount: consistencyIssues.length,
|
|
129
|
+
warningCount: 0,
|
|
130
|
+
});
|
|
131
|
+
errorCount += consistencyIssues.length;
|
|
129
132
|
}
|
|
130
|
-
// 2. Cross-patch file claims: two or more manifest entries listing the
|
|
131
|
-
// same path in filesAffected. Not caught by per-patch consistency.
|
|
132
133
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
133
134
|
if (manifest) {
|
|
134
135
|
const policyIssues = evaluatePatchPolicy(config, manifest);
|
|
135
136
|
if (policyIssues.length > 0) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
const policyErrors = policyIssues.filter((issue) => issue.severity === 'error').length;
|
|
138
|
+
const policyWarnings = policyIssues.length - policyErrors;
|
|
139
|
+
groups.push({
|
|
140
|
+
title: `Patch policy issues (${policyIssues.length})`,
|
|
141
|
+
issues: policyIssues.map((issue) => {
|
|
142
|
+
const label = issue.severity === 'error' ? 'ERROR' : 'WARN';
|
|
143
|
+
return `${label} [${issue.code}] ${issue.filename}: ${issue.message}`;
|
|
144
|
+
}),
|
|
145
|
+
errorCount: policyErrors,
|
|
146
|
+
warningCount: policyWarnings,
|
|
147
|
+
});
|
|
148
|
+
errorCount += policyErrors;
|
|
149
|
+
warningCount += policyWarnings;
|
|
145
150
|
}
|
|
146
151
|
const crossClaims = detectCrossPatchFileClaims(manifest.patches);
|
|
147
152
|
if (crossClaims.length > 0) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
errorCount
|
|
152
|
-
|
|
153
|
+
groups.push({
|
|
154
|
+
title: `Cross-patch filesAffected conflicts (${crossClaims.length})`,
|
|
155
|
+
issues: crossClaims.map((claim) => `${claim.path} claimed by: ${claim.filenames.join(', ')}`),
|
|
156
|
+
errorCount: crossClaims.length,
|
|
157
|
+
warningCount: 0,
|
|
158
|
+
});
|
|
159
|
+
errorCount += crossClaims.length;
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
|
-
// 3. Cross-patch lint: duplicate /dev/null creation + forward imports.
|
|
156
162
|
const ctx = await buildPatchQueueContext(paths.patches);
|
|
157
163
|
const lintIssues = lintPatchQueue(ctx);
|
|
158
164
|
if (lintIssues.length > 0) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const lintErrors = lintIssues.filter((issue) => issue.severity === 'error').length;
|
|
166
|
+
const lintWarnings = lintIssues.filter((issue) => issue.severity === 'warning').length;
|
|
167
|
+
groups.push({
|
|
168
|
+
title: `Cross-patch lint issues (${lintIssues.length})`,
|
|
169
|
+
issues: lintIssues.map((issue) => {
|
|
170
|
+
const label = issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARN' : 'NOTICE';
|
|
171
|
+
return `${label} [${issue.check}] ${issue.file}: ${issue.message}`;
|
|
172
|
+
}),
|
|
173
|
+
errorCount: lintErrors,
|
|
174
|
+
warningCount: lintWarnings,
|
|
175
|
+
});
|
|
176
|
+
errorCount += lintErrors;
|
|
177
|
+
warningCount += lintWarnings;
|
|
168
178
|
}
|
|
169
|
-
// 4. Registration-consequence consistency: walk each patch body and
|
|
170
|
-
// confirm that every widget / locale registration it adds has a
|
|
171
|
-
// corresponding file body covered by the patch queue OR present in
|
|
172
|
-
// the engine working tree. 2026-04-24 eval Finding 1: a patch
|
|
173
|
-
// produced by `export-all --exclude-furnace` referenced
|
|
174
|
-
// `toolkit/content/widgets/moz-qa-panel/*.mjs` via jar.mn /
|
|
175
|
-
// customElements.js edits, but the source files themselves were
|
|
176
|
-
// excluded from the patch. `verify` used to report "clean"; it now
|
|
177
|
-
// flags each dangling reference as a `dangling-registration` error
|
|
178
|
-
// naming the specific patch and target path.
|
|
179
179
|
if (manifest) {
|
|
180
180
|
const registrationIssues = await detectDanglingRegistrations(paths.patches, paths.engine, manifest.patches);
|
|
181
181
|
if (registrationIssues.length > 0) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
errorCount
|
|
186
|
-
|
|
182
|
+
groups.push({
|
|
183
|
+
title: `Dangling registration references (${registrationIssues.length})`,
|
|
184
|
+
issues: registrationIssues.map((issue) => `${issue.patchFilename}: registers ${issue.targetPath} via ${issue.source}, but no patch body or engine file supplies it`),
|
|
185
|
+
errorCount: registrationIssues.length,
|
|
186
|
+
warningCount: 0,
|
|
187
|
+
});
|
|
188
|
+
errorCount += registrationIssues.length;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
hasPatchesDirectory: true,
|
|
193
|
+
groups,
|
|
194
|
+
errorCount,
|
|
195
|
+
warningCount,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Runs the `verify` command: manifest consistency + cross-patch lint.
|
|
200
|
+
* Read-only; exits non-zero on any error-severity finding.
|
|
201
|
+
*
|
|
202
|
+
* @param projectRoot - Project root directory
|
|
203
|
+
*/
|
|
204
|
+
export async function verifyCommand(projectRoot) {
|
|
205
|
+
intro('FireForge Verify');
|
|
206
|
+
const health = await collectPatchQueueHealth(projectRoot);
|
|
207
|
+
if (!health.hasPatchesDirectory) {
|
|
208
|
+
info('No patches directory. Nothing to verify.');
|
|
209
|
+
outro('Verify clean');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const group of health.groups) {
|
|
213
|
+
warn(`${group.title}:`);
|
|
214
|
+
for (const issue of group.issues) {
|
|
215
|
+
warn(` ${issue}`);
|
|
187
216
|
}
|
|
188
217
|
}
|
|
189
|
-
if (errorCount === 0 && warningCount === 0) {
|
|
218
|
+
if (health.errorCount === 0 && health.warningCount === 0) {
|
|
190
219
|
success('Patch queue is consistent.');
|
|
191
220
|
outro('Verify clean');
|
|
192
221
|
return;
|
|
193
222
|
}
|
|
194
|
-
info(`\nVerify: ${errorCount} error(s), ${warningCount} warning(s)`);
|
|
195
|
-
if (errorCount > 0) {
|
|
223
|
+
info(`\nVerify: ${health.errorCount} error(s), ${health.warningCount} warning(s)`);
|
|
224
|
+
if (health.errorCount > 0) {
|
|
196
225
|
outro('Verify failed');
|
|
197
|
-
throw new GeneralError(`fireforge verify found ${errorCount} error(s). Fix these before running export/import/rebase.`);
|
|
226
|
+
throw new GeneralError(`fireforge verify found ${health.errorCount} error(s). Fix these before running export/import/rebase.`);
|
|
198
227
|
}
|
|
199
228
|
outro('Verify passed with warnings');
|
|
200
229
|
}
|
|
@@ -11,7 +11,7 @@ import type { FireForgeConfig } from '../types/config.js';
|
|
|
11
11
|
/** Default patch filename contract used when a policy omits `filenamePattern`. */
|
|
12
12
|
export declare const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = "^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$";
|
|
13
13
|
/** Stable issue codes returned by patch policy evaluation. */
|
|
14
|
-
export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap';
|
|
14
|
+
export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'order-collision' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap';
|
|
15
15
|
/** A single patch policy validation finding. */
|
|
16
16
|
export interface PatchPolicyIssue {
|
|
17
17
|
code: PatchPolicyIssueCode;
|
|
@@ -260,6 +260,28 @@ function evaluateGaps(cfg, patches, severity) {
|
|
|
260
260
|
}
|
|
261
261
|
return issues;
|
|
262
262
|
}
|
|
263
|
+
function evaluateOrderCollisions(patches, severity) {
|
|
264
|
+
const byOrder = new Map();
|
|
265
|
+
for (const patch of patches) {
|
|
266
|
+
const matches = byOrder.get(patch.order) ?? [];
|
|
267
|
+
matches.push(patch);
|
|
268
|
+
byOrder.set(patch.order, matches);
|
|
269
|
+
}
|
|
270
|
+
const issues = [];
|
|
271
|
+
for (const [order, matches] of [...byOrder.entries()].sort((a, b) => a[0] - b[0])) {
|
|
272
|
+
if (matches.length <= 1)
|
|
273
|
+
continue;
|
|
274
|
+
const filenames = matches.map((patch) => patch.filename).sort((a, b) => a.localeCompare(b));
|
|
275
|
+
issues.push({
|
|
276
|
+
code: 'order-collision',
|
|
277
|
+
filename: String(order).padStart(3, '0'),
|
|
278
|
+
severity,
|
|
279
|
+
message: `patchPolicy requires unique numeric orders; order ${String(order).padStart(3, '0')} ` +
|
|
280
|
+
`is used by: ${filenames.join(', ')}.`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return issues;
|
|
284
|
+
}
|
|
263
285
|
/** Evaluates an entire patch manifest against the configured policy. */
|
|
264
286
|
export function evaluatePatchPolicy(config, manifest) {
|
|
265
287
|
const cfg = policy(config);
|
|
@@ -267,6 +289,7 @@ export function evaluatePatchPolicy(config, manifest) {
|
|
|
267
289
|
return [];
|
|
268
290
|
const severity = issueSeverity(config);
|
|
269
291
|
const issues = manifest.patches.flatMap((patch) => evaluatePatchMetadata(cfg, patch, severity));
|
|
292
|
+
issues.push(...evaluateOrderCollisions(manifest.patches, severity));
|
|
270
293
|
issues.push(...evaluateGaps(cfg, manifest.patches, severity));
|
|
271
294
|
return issues;
|
|
272
295
|
}
|
|
@@ -70,7 +70,7 @@ export interface ExportOptions {
|
|
|
70
70
|
* be superseded and which files caused the coverage.
|
|
71
71
|
*/
|
|
72
72
|
dryRun?: boolean;
|
|
73
|
-
/** Place the new patch at
|
|
73
|
+
/** Place the new patch at this exact unused order without renumbering existing patches. */
|
|
74
74
|
order?: number;
|
|
75
75
|
/** Place the new patch immediately before the named patch. */
|
|
76
76
|
before?: string;
|
|
@@ -552,6 +552,8 @@ export interface PatchCompactOptions {
|
|
|
552
552
|
yes?: boolean;
|
|
553
553
|
/** Print what would happen without writing anything. */
|
|
554
554
|
dryRun?: boolean;
|
|
555
|
+
/** Bypass force-mode patchPolicy refusals. */
|
|
556
|
+
forceUnsafe?: boolean;
|
|
555
557
|
}
|
|
556
558
|
/**
|
|
557
559
|
* Options for the status command.
|
|
@@ -584,6 +586,12 @@ export interface TokenAddOptions {
|
|
|
584
586
|
*/
|
|
585
587
|
export interface DoctorOptions {
|
|
586
588
|
repairPatchesManifest?: boolean;
|
|
589
|
+
/**
|
|
590
|
+
* Clear a stale `pendingResolution` marker, but only after the same
|
|
591
|
+
* read-only queue health checks used by `fireforge verify` report no
|
|
592
|
+
* error-severity findings.
|
|
593
|
+
*/
|
|
594
|
+
clearResolution?: boolean;
|
|
587
595
|
/**
|
|
588
596
|
* Opt-in repair path for furnace-specific checks. When true, doctor will:
|
|
589
597
|
* - clear stale `.fireforge/furnace-state.json` entries whose component is
|