@hominis/fireforge 0.10.1 → 0.11.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 +93 -1
- package/README.md +125 -238
- package/dist/bin/fireforge.js +26 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +131 -52
- package/dist/src/commands/bootstrap.js +6 -2
- package/dist/src/commands/build.js +4 -2
- package/dist/src/commands/discard.js +16 -4
- package/dist/src/commands/doctor-furnace.d.ts +8 -0
- package/dist/src/commands/doctor-furnace.js +422 -0
- package/dist/src/commands/doctor.d.ts +115 -0
- package/dist/src/commands/doctor.js +327 -258
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/export-all.js +15 -0
- package/dist/src/commands/export-flow.d.ts +91 -0
- package/dist/src/commands/export-flow.js +344 -0
- package/dist/src/commands/export.js +151 -5
- package/dist/src/commands/furnace/apply.d.ts +3 -2
- package/dist/src/commands/furnace/apply.js +169 -36
- package/dist/src/commands/furnace/create.js +162 -52
- package/dist/src/commands/furnace/deploy.js +156 -144
- package/dist/src/commands/furnace/diff.d.ts +8 -4
- package/dist/src/commands/furnace/diff.js +142 -73
- package/dist/src/commands/furnace/index.d.ts +6 -2
- package/dist/src/commands/furnace/index.js +76 -25
- package/dist/src/commands/furnace/init.d.ts +11 -0
- package/dist/src/commands/furnace/init.js +76 -0
- package/dist/src/commands/furnace/list.d.ts +4 -1
- package/dist/src/commands/furnace/list.js +35 -3
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +216 -26
- package/dist/src/commands/furnace/preview.js +184 -30
- package/dist/src/commands/furnace/refresh.d.ts +10 -0
- package/dist/src/commands/furnace/refresh.js +268 -0
- package/dist/src/commands/furnace/remove.js +285 -89
- package/dist/src/commands/furnace/rename.d.ts +5 -0
- package/dist/src/commands/furnace/rename.js +308 -0
- package/dist/src/commands/furnace/scan.d.ts +4 -1
- package/dist/src/commands/furnace/scan.js +72 -11
- package/dist/src/commands/furnace/status.js +85 -20
- package/dist/src/commands/furnace/sync.d.ts +12 -0
- package/dist/src/commands/furnace/sync.js +77 -0
- package/dist/src/commands/furnace/validate.d.ts +4 -1
- package/dist/src/commands/furnace/validate.js +99 -3
- package/dist/src/commands/furnace/validation-output.d.ts +24 -1
- package/dist/src/commands/furnace/validation-output.js +93 -1
- package/dist/src/commands/import.js +37 -4
- package/dist/src/commands/lint.js +11 -2
- package/dist/src/commands/manifest.d.ts +39 -0
- package/dist/src/commands/manifest.js +59 -0
- package/dist/src/commands/patch/delete.d.ts +28 -0
- package/dist/src/commands/patch/delete.js +209 -0
- package/dist/src/commands/patch/index.d.ts +17 -0
- package/dist/src/commands/patch/index.js +25 -0
- package/dist/src/commands/patch/reorder.d.ts +30 -0
- package/dist/src/commands/patch/reorder.js +377 -0
- package/dist/src/commands/re-export-files.d.ts +17 -0
- package/dist/src/commands/re-export-files.js +177 -0
- package/dist/src/commands/re-export.js +44 -0
- package/dist/src/commands/rebase/abort.d.ts +1 -1
- package/dist/src/commands/rebase/abort.js +12 -3
- package/dist/src/commands/rebase/confirm.d.ts +3 -3
- package/dist/src/commands/rebase/confirm.js +4 -4
- package/dist/src/commands/rebase/index.js +13 -4
- package/dist/src/commands/reset.js +20 -4
- package/dist/src/commands/run.js +46 -1
- package/dist/src/commands/setup-support.js +5 -5
- package/dist/src/commands/status.js +97 -6
- package/dist/src/commands/test.js +5 -37
- package/dist/src/commands/verify.d.ts +31 -0
- package/dist/src/commands/verify.js +126 -0
- package/dist/src/core/build-prepare.js +40 -16
- package/dist/src/core/destructive.d.ts +96 -0
- package/dist/src/core/destructive.js +137 -0
- package/dist/src/core/diff-hunks.d.ts +73 -0
- package/dist/src/core/diff-hunks.js +268 -0
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
- package/dist/src/core/furnace-apply-helpers.js +302 -57
- package/dist/src/core/furnace-apply-output.d.ts +16 -0
- package/dist/src/core/furnace-apply-output.js +57 -0
- package/dist/src/core/furnace-apply.d.ts +21 -3
- package/dist/src/core/furnace-apply.js +260 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
- package/dist/src/core/furnace-checksum-utils.js +24 -0
- package/dist/src/core/furnace-config.d.ts +28 -1
- package/dist/src/core/furnace-config.js +180 -17
- package/dist/src/core/furnace-constants.d.ts +22 -0
- package/dist/src/core/furnace-constants.js +36 -0
- package/dist/src/core/furnace-graph-utils.d.ts +11 -0
- package/dist/src/core/furnace-graph-utils.js +94 -0
- package/dist/src/core/furnace-operation.d.ts +108 -0
- package/dist/src/core/furnace-operation.js +220 -0
- package/dist/src/core/furnace-refresh.d.ts +20 -0
- package/dist/src/core/furnace-refresh.js +118 -0
- package/dist/src/core/furnace-registration-ast.d.ts +5 -0
- package/dist/src/core/furnace-registration-ast.js +134 -4
- package/dist/src/core/furnace-registration-remove.d.ts +25 -3
- package/dist/src/core/furnace-registration-remove.js +196 -62
- package/dist/src/core/furnace-registration-validate.d.ts +13 -1
- package/dist/src/core/furnace-registration-validate.js +15 -3
- package/dist/src/core/furnace-registration.d.ts +27 -4
- package/dist/src/core/furnace-registration.js +93 -11
- package/dist/src/core/furnace-rollback.d.ts +11 -0
- package/dist/src/core/furnace-rollback.js +78 -7
- package/dist/src/core/furnace-scanner.d.ts +8 -2
- package/dist/src/core/furnace-scanner.js +152 -55
- package/dist/src/core/furnace-stories.js +7 -5
- package/dist/src/core/furnace-validate-accessibility.js +7 -1
- package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +85 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
- package/dist/src/core/furnace-validate-helpers.js +31 -0
- package/dist/src/core/furnace-validate-registration.d.ts +17 -2
- package/dist/src/core/furnace-validate-registration.js +73 -3
- package/dist/src/core/furnace-validate-structure.d.ts +10 -2
- package/dist/src/core/furnace-validate-structure.js +45 -3
- package/dist/src/core/furnace-validate.d.ts +10 -1
- package/dist/src/core/furnace-validate.js +80 -6
- package/dist/src/core/furnace-version-drift.d.ts +55 -0
- package/dist/src/core/furnace-version-drift.js +101 -0
- package/dist/src/core/git-file-ops.d.ts +8 -0
- package/dist/src/core/git-file-ops.js +19 -6
- package/dist/src/core/lint-projection.d.ts +25 -0
- package/dist/src/core/lint-projection.js +44 -0
- package/dist/src/core/mach.d.ts +4 -2
- package/dist/src/core/mach.js +17 -2
- package/dist/src/core/markdown-table.d.ts +104 -0
- package/dist/src/core/markdown-table.js +266 -0
- package/dist/src/core/ownership-table.d.ts +53 -0
- package/dist/src/core/ownership-table.js +144 -0
- package/dist/src/core/patch-apply.d.ts +17 -3
- package/dist/src/core/patch-apply.js +86 -8
- package/dist/src/core/patch-export.d.ts +119 -5
- package/dist/src/core/patch-export.js +183 -25
- package/dist/src/core/patch-lint-cross.d.ts +195 -0
- package/dist/src/core/patch-lint-cross.js +428 -0
- package/dist/src/core/patch-lint-diff.d.ts +33 -0
- package/dist/src/core/patch-lint-diff.js +84 -0
- package/dist/src/core/patch-lint.d.ts +2 -4
- package/dist/src/core/patch-lint.js +12 -50
- package/dist/src/core/patch-lock.js +2 -1
- package/dist/src/core/patch-manifest-io.d.ts +102 -1
- package/dist/src/core/patch-manifest-io.js +270 -2
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-transform.d.ts +12 -0
- package/dist/src/core/patch-transform.js +21 -7
- package/dist/src/core/token-manager.js +67 -69
- package/dist/src/core/wire-destroy.js +6 -3
- package/dist/src/core/wire-init.js +10 -4
- package/dist/src/core/wire-subscript.js +9 -3
- package/dist/src/core/wire-utils.d.ts +52 -5
- package/dist/src/core/wire-utils.js +69 -6
- package/dist/src/errors/base.d.ts +20 -0
- package/dist/src/errors/base.js +24 -0
- package/dist/src/errors/furnace.js +7 -1
- package/dist/src/errors/rebase.js +6 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +125 -4
- package/dist/src/types/commands/patches.d.ts +11 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +55 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +30 -1
- package/dist/src/utils/package-root.d.ts +5 -0
- package/dist/src/utils/package-root.js +12 -0
- package/dist/src/utils/process.js +9 -4
- package/dist/src/utils/validation.d.ts +20 -2
- package/dist/src/utils/validation.js +26 -3
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CustomComponentConfig, FurnaceConfig, RegistrationStatus, ValidationIssue } from '../types/furnace.js';
|
|
1
|
+
import type { CustomComponentConfig, FurnaceConfig, RegistrationStatus, StepError, ValidationIssue } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
3
|
* Validates that all Furnace-managed .mjs components are registered in the
|
|
4
4
|
* DOMContentLoaded/importESModule block (Pattern B), not the loadSubScript
|
|
@@ -20,7 +20,7 @@ export declare function validateRegistrationPatterns(root: string, config: Furna
|
|
|
20
20
|
* @param config - Custom component configuration
|
|
21
21
|
* @returns Registration status with per-check booleans and drift info
|
|
22
22
|
*/
|
|
23
|
-
export declare function checkRegistrationConsistency(root: string, name: string, config: CustomComponentConfig): Promise<RegistrationStatus>;
|
|
23
|
+
export declare function checkRegistrationConsistency(root: string, name: string, config: CustomComponentConfig, ftlDir?: string): Promise<RegistrationStatus>;
|
|
24
24
|
/**
|
|
25
25
|
* Validates that each custom component with `register: true` has its .mjs and
|
|
26
26
|
* .css entries in jar.mn.
|
|
@@ -35,3 +35,18 @@ export declare function validateJarMnEntries(root: string, config: FurnaceConfig
|
|
|
35
35
|
* linked in browser.xhtml. Without the link, tokens silently resolve to nothing.
|
|
36
36
|
*/
|
|
37
37
|
export declare function validateTokenLink(componentDir: string, tagName: string, root: string, tokenPrefix?: string): Promise<ValidationIssue[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Post-apply registration consistency check for custom components.
|
|
40
|
+
*
|
|
41
|
+
* Detects customElements.js / jar.mn inconsistencies caused by a partial
|
|
42
|
+
* apply. Errors are surfaced as step-level warnings on the affected
|
|
43
|
+
* component rather than blocking the entire apply.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runPostApplyConsistencyChecks(root: string, config: {
|
|
46
|
+
custom: Record<string, CustomComponentConfig>;
|
|
47
|
+
}, result: {
|
|
48
|
+
applied: Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
stepErrors?: StepError[];
|
|
51
|
+
}>;
|
|
52
|
+
}, ftlDir: string): Promise<void>;
|
|
@@ -8,7 +8,7 @@ import { warn } from '../utils/logger.js';
|
|
|
8
8
|
import { stripJsComments } from '../utils/regex.js';
|
|
9
9
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
10
10
|
import { getFurnacePaths } from './furnace-config.js';
|
|
11
|
-
import { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
11
|
+
import { CUSTOM_ELEMENTS_JS, FTL_DIR, JAR_MN } from './furnace-constants.js';
|
|
12
12
|
import { getTokensCssPath } from './token-manager.js';
|
|
13
13
|
/**
|
|
14
14
|
* Validates that all Furnace-managed .mjs components are registered in the
|
|
@@ -62,7 +62,7 @@ export async function validateRegistrationPatterns(root, config) {
|
|
|
62
62
|
* @param config - Custom component configuration
|
|
63
63
|
* @returns Registration status with per-check booleans and drift info
|
|
64
64
|
*/
|
|
65
|
-
export async function checkRegistrationConsistency(root, name, config) {
|
|
65
|
+
export async function checkRegistrationConsistency(root, name, config, ftlDir) {
|
|
66
66
|
const { engine: engineDir } = getProjectPaths(root);
|
|
67
67
|
const furnacePaths = getFurnacePaths(root);
|
|
68
68
|
const componentDir = join(furnacePaths.customDir, name);
|
|
@@ -112,6 +112,30 @@ export async function checkRegistrationConsistency(root, name, config) {
|
|
|
112
112
|
else {
|
|
113
113
|
status.filesInSync = false;
|
|
114
114
|
}
|
|
115
|
+
// Localized components deploy a .ftl file outside `targetDir` (into the
|
|
116
|
+
// shared Fluent tree). The .mjs/.css loop above cannot see it, so drift
|
|
117
|
+
// there would otherwise be invisible to apply's fast-path and to `status`.
|
|
118
|
+
if (config.localized) {
|
|
119
|
+
const ftlName = `${name}.ftl`;
|
|
120
|
+
const ftlSrc = join(componentDir, ftlName);
|
|
121
|
+
if (await pathExists(ftlSrc)) {
|
|
122
|
+
const ftlDest = join(engineDir, ftlDir ?? FTL_DIR, ftlName);
|
|
123
|
+
if (!(await pathExists(ftlDest))) {
|
|
124
|
+
status.missingTargetFiles.push(ftlName);
|
|
125
|
+
status.filesInSync = false;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const srcContent = await readText(ftlSrc);
|
|
129
|
+
const destContent = await readText(ftlDest);
|
|
130
|
+
const srcHash = createHash('sha256').update(srcContent).digest('hex');
|
|
131
|
+
const destHash = createHash('sha256').update(destContent).digest('hex');
|
|
132
|
+
if (srcHash !== destHash) {
|
|
133
|
+
status.driftedFiles.push(ftlName);
|
|
134
|
+
status.filesInSync = false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
115
139
|
// Check jar.mn entries
|
|
116
140
|
const jarMnPath = join(engineDir, JAR_MN);
|
|
117
141
|
if (await pathExists(jarMnPath)) {
|
|
@@ -153,6 +177,7 @@ export async function validateJarMnEntries(root, config) {
|
|
|
153
177
|
return issues;
|
|
154
178
|
}
|
|
155
179
|
const jarContent = await readText(jarMnPath);
|
|
180
|
+
const furnacePaths = getFurnacePaths(root);
|
|
156
181
|
for (const [name, customConfig] of Object.entries(config.custom)) {
|
|
157
182
|
if (!customConfig.register)
|
|
158
183
|
continue;
|
|
@@ -164,7 +189,13 @@ export async function validateJarMnEntries(root, config) {
|
|
|
164
189
|
message: `${name}.mjs is not registered in jar.mn. Run "fireforge furnace deploy" to register.`,
|
|
165
190
|
});
|
|
166
191
|
}
|
|
167
|
-
|
|
192
|
+
// Only complain about a missing CSS entry when the source actually
|
|
193
|
+
// ships a CSS file. Components that intentionally have no CSS would
|
|
194
|
+
// otherwise generate a permanent false-positive that trains developers
|
|
195
|
+
// to ignore validator output.
|
|
196
|
+
const cssSourcePath = join(furnacePaths.customDir, name, `${name}.css`);
|
|
197
|
+
if ((await pathExists(cssSourcePath)) &&
|
|
198
|
+
!jarContent.includes(`content/global/elements/${name}.css`)) {
|
|
168
199
|
issues.push({
|
|
169
200
|
component: name,
|
|
170
201
|
severity: 'warning',
|
|
@@ -217,4 +248,43 @@ export async function validateTokenLink(componentDir, tagName, root, tokenPrefix
|
|
|
217
248
|
}
|
|
218
249
|
return issues;
|
|
219
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Post-apply registration consistency check for custom components.
|
|
253
|
+
*
|
|
254
|
+
* Detects customElements.js / jar.mn inconsistencies caused by a partial
|
|
255
|
+
* apply. Errors are surfaced as step-level warnings on the affected
|
|
256
|
+
* component rather than blocking the entire apply.
|
|
257
|
+
*/
|
|
258
|
+
export async function runPostApplyConsistencyChecks(root, config, result, ftlDir) {
|
|
259
|
+
for (const [name, customConfig] of Object.entries(config.custom)) {
|
|
260
|
+
if (!customConfig.register)
|
|
261
|
+
continue;
|
|
262
|
+
if (!result.applied.some((a) => a.name === name))
|
|
263
|
+
continue;
|
|
264
|
+
try {
|
|
265
|
+
const status = await checkRegistrationConsistency(root, name, customConfig, ftlDir);
|
|
266
|
+
const issues = [];
|
|
267
|
+
if (!status.customElementsPresent) {
|
|
268
|
+
issues.push('missing customElements.js registration');
|
|
269
|
+
}
|
|
270
|
+
if (!status.jarMnMjs && status.sourceExists) {
|
|
271
|
+
issues.push('missing jar.mn .mjs entry');
|
|
272
|
+
}
|
|
273
|
+
if (issues.length > 0) {
|
|
274
|
+
const entry = result.applied.find((a) => a.name === name);
|
|
275
|
+
if (entry) {
|
|
276
|
+
const stepErrors = entry.stepErrors ?? [];
|
|
277
|
+
stepErrors.push({
|
|
278
|
+
step: 'post-apply consistency',
|
|
279
|
+
error: `Registration inconsistency: ${issues.join(', ')}`,
|
|
280
|
+
});
|
|
281
|
+
entry.stepErrors = stepErrors;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Consistency check is best-effort; failures here should not block apply
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
220
290
|
//# sourceMappingURL=furnace-validate-registration.js.map
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type { ComponentType, ValidationIssue } from '../types/furnace.js';
|
|
1
|
+
import type { ComponentType, CustomComponentConfig, ValidationIssue } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
3
|
* Validates the file structure of a component directory.
|
|
4
4
|
* Checks for required files and naming conventions.
|
|
5
|
+
*
|
|
6
|
+
* @param componentDir - Component source directory
|
|
7
|
+
* @param tagName - Component tag name
|
|
8
|
+
* @param type - Component type (stock, override, custom)
|
|
9
|
+
* @param customConfig - When `type === 'custom'`, the matching config from
|
|
10
|
+
* furnace.json. Used to derive `localized`, which gates the `.ftl`
|
|
11
|
+
* requirement. Optional so existing callers without config in scope (e.g.
|
|
12
|
+
* the structure-only test fixtures) can keep calling without changes.
|
|
5
13
|
*/
|
|
6
|
-
export declare function validateStructure(componentDir: string, tagName: string, type: ComponentType): Promise<ValidationIssue[]>;
|
|
14
|
+
export declare function validateStructure(componentDir: string, tagName: string, type: ComponentType, customConfig?: CustomComponentConfig): Promise<ValidationIssue[]>;
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { pathExists } from '../utils/fs.js';
|
|
4
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
5
5
|
/**
|
|
6
6
|
* Validates the file structure of a component directory.
|
|
7
7
|
* Checks for required files and naming conventions.
|
|
8
|
+
*
|
|
9
|
+
* @param componentDir - Component source directory
|
|
10
|
+
* @param tagName - Component tag name
|
|
11
|
+
* @param type - Component type (stock, override, custom)
|
|
12
|
+
* @param customConfig - When `type === 'custom'`, the matching config from
|
|
13
|
+
* furnace.json. Used to derive `localized`, which gates the `.ftl`
|
|
14
|
+
* requirement. Optional so existing callers without config in scope (e.g.
|
|
15
|
+
* the structure-only test fixtures) can keep calling without changes.
|
|
8
16
|
*/
|
|
9
|
-
export async function validateStructure(componentDir, tagName, type) {
|
|
17
|
+
export async function validateStructure(componentDir, tagName, type, customConfig) {
|
|
10
18
|
const issues = [];
|
|
11
19
|
const mjsPath = join(componentDir, `${tagName}.mjs`);
|
|
12
20
|
const cssPath = join(componentDir, `${tagName}.css`);
|
|
@@ -28,8 +36,42 @@ export async function validateStructure(componentDir, tagName, type) {
|
|
|
28
36
|
message: `No ${tagName}.css found. Consider adding styles.`,
|
|
29
37
|
});
|
|
30
38
|
}
|
|
39
|
+
// Localized custom components must have a {tag}.ftl file. Without one,
|
|
40
|
+
// apply silently deploys nothing for the locale and the runtime
|
|
41
|
+
// localization payload is empty, which is hard to spot in review.
|
|
42
|
+
if (type === 'custom' && customConfig?.localized) {
|
|
43
|
+
const ftlPath = join(componentDir, `${tagName}.ftl`);
|
|
44
|
+
if (!(await pathExists(ftlPath))) {
|
|
45
|
+
issues.push({
|
|
46
|
+
component: tagName,
|
|
47
|
+
severity: 'error',
|
|
48
|
+
check: 'missing-ftl',
|
|
49
|
+
message: `Component is marked localized: true but ${tagName}.ftl is missing. Create the file or set localized: false in furnace.json.`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Conflict markers left by furnace refresh (three-way merge) must be
|
|
54
|
+
// resolved before the component can be applied or deployed.
|
|
55
|
+
const dirEntries = await readdir(componentDir, { withFileTypes: true });
|
|
56
|
+
for (const entry of dirEntries) {
|
|
57
|
+
if (!entry.isFile())
|
|
58
|
+
continue;
|
|
59
|
+
if (!entry.name.endsWith('.mjs') &&
|
|
60
|
+
!entry.name.endsWith('.css') &&
|
|
61
|
+
!entry.name.endsWith('.ftl'))
|
|
62
|
+
continue;
|
|
63
|
+
const content = await readText(join(componentDir, entry.name));
|
|
64
|
+
if (/^<{7}\s/m.test(content) || /^>{7}\s/m.test(content) || /^={7}$/m.test(content)) {
|
|
65
|
+
issues.push({
|
|
66
|
+
component: tagName,
|
|
67
|
+
severity: 'error',
|
|
68
|
+
check: 'conflict-markers',
|
|
69
|
+
message: `File "${entry.name}" contains unresolved merge conflict markers. Resolve conflicts before applying.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
31
73
|
// File names should match tag name
|
|
32
|
-
const entries =
|
|
74
|
+
const entries = dirEntries;
|
|
33
75
|
for (const entry of entries) {
|
|
34
76
|
if (!entry.isFile())
|
|
35
77
|
continue;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import type { ComponentType, FurnaceConfig, ValidationIssue } from '../types/furnace.js';
|
|
2
2
|
/**
|
|
3
3
|
* Runs all validation checks on a single component.
|
|
4
|
+
*
|
|
4
5
|
* @param componentDir - Path to the component directory
|
|
5
6
|
* @param tagName - Component tag name
|
|
6
7
|
* @param type - Component type (stock, override, custom)
|
|
8
|
+
* @param config - Optional furnace config for cross-component checks
|
|
9
|
+
* @param root - Optional project root for checks that read outside componentDir
|
|
10
|
+
* @param options - Optional behavior flags. `skipAggregateChecks` suppresses the
|
|
11
|
+
* per-component registration/jar.mn scan so that an outer caller
|
|
12
|
+
* (e.g. validateAllComponents) can run the aggregate versions once
|
|
13
|
+
* without double-reporting the same issues.
|
|
7
14
|
* @returns Combined list of validation issues
|
|
8
15
|
*/
|
|
9
|
-
export declare function validateComponent(componentDir: string, tagName: string, type: ComponentType, config?: FurnaceConfig, root?: string
|
|
16
|
+
export declare function validateComponent(componentDir: string, tagName: string, type: ComponentType, config?: FurnaceConfig, root?: string, options?: {
|
|
17
|
+
skipAggregateChecks?: boolean;
|
|
18
|
+
}): Promise<ValidationIssue[]>;
|
|
10
19
|
/**
|
|
11
20
|
* Validates all components registered in furnace.json.
|
|
12
21
|
* Stock components are skipped (no local files to validate).
|
|
@@ -1,30 +1,60 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists } from '../utils/fs.js';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
4
5
|
import { getFurnacePaths, loadFurnaceConfig } from './furnace-config.js';
|
|
6
|
+
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
5
7
|
import { validateAccessibility, validateCompatibility, validateJarMnEntries, validateRegistrationPatterns, validateStructure, validateTokenLink, } from './furnace-validate-checks.js';
|
|
8
|
+
import { findOverrideBaseVersionDrift, } from './furnace-version-drift.js';
|
|
9
|
+
function buildOverrideVersionDriftIssues(config, currentVersion, tagName) {
|
|
10
|
+
return findOverrideBaseVersionDrift(config, currentVersion)
|
|
11
|
+
.filter((entry) => tagName === undefined || entry.name === tagName)
|
|
12
|
+
.map((entry) => ({
|
|
13
|
+
component: entry.name,
|
|
14
|
+
severity: 'error',
|
|
15
|
+
check: 'override-base-version-drift',
|
|
16
|
+
message: `Override targets Firefox ${entry.baseVersion}, but fireforge.json records ${entry.currentVersion}. ` +
|
|
17
|
+
'Refresh the override if upstream changed, or update baseVersion in furnace.json to acknowledge the new baseline.',
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
6
20
|
// ---------------------------------------------------------------------------
|
|
7
21
|
// Aggregate validators
|
|
8
22
|
// ---------------------------------------------------------------------------
|
|
9
23
|
/**
|
|
10
24
|
* Runs all validation checks on a single component.
|
|
25
|
+
*
|
|
11
26
|
* @param componentDir - Path to the component directory
|
|
12
27
|
* @param tagName - Component tag name
|
|
13
28
|
* @param type - Component type (stock, override, custom)
|
|
29
|
+
* @param config - Optional furnace config for cross-component checks
|
|
30
|
+
* @param root - Optional project root for checks that read outside componentDir
|
|
31
|
+
* @param options - Optional behavior flags. `skipAggregateChecks` suppresses the
|
|
32
|
+
* per-component registration/jar.mn scan so that an outer caller
|
|
33
|
+
* (e.g. validateAllComponents) can run the aggregate versions once
|
|
34
|
+
* without double-reporting the same issues.
|
|
14
35
|
* @returns Combined list of validation issues
|
|
15
36
|
*/
|
|
16
|
-
export async function validateComponent(componentDir, tagName, type, config, root) {
|
|
37
|
+
export async function validateComponent(componentDir, tagName, type, config, root, options) {
|
|
17
38
|
const issues = [];
|
|
18
|
-
|
|
39
|
+
// Pass the matching custom config so structure validation can enforce
|
|
40
|
+
// the .ftl-when-localized invariant. Non-custom validations ignore the
|
|
41
|
+
// parameter, so this is a no-op for stock and override components.
|
|
42
|
+
issues.push(...(await validateStructure(componentDir, tagName, type, type === 'custom' ? config?.custom[tagName] : undefined)));
|
|
19
43
|
issues.push(...(await validateAccessibility(componentDir, tagName)));
|
|
20
44
|
issues.push(...(await validateCompatibility(componentDir, tagName, type, config, root)));
|
|
45
|
+
if (root && config && type === 'override') {
|
|
46
|
+
const forgeConfig = await loadConfig(root);
|
|
47
|
+
issues.push(...buildOverrideVersionDriftIssues(config, forgeConfig.firefox.version, tagName));
|
|
48
|
+
}
|
|
21
49
|
// Check for missing token link in browser.xhtml
|
|
22
50
|
if (root) {
|
|
23
51
|
issues.push(...(await validateTokenLink(componentDir, tagName, root, config?.tokenPrefix)));
|
|
24
52
|
}
|
|
25
53
|
// When root is provided and this is a custom component with registration,
|
|
26
54
|
// also run registration pattern and jar.mn validation for this component.
|
|
27
|
-
|
|
55
|
+
// Skipped when an outer orchestrator (validateAllComponents) will run the
|
|
56
|
+
// aggregate versions itself; otherwise the same issues are reported twice.
|
|
57
|
+
if (root && config && type === 'custom' && !options?.skipAggregateChecks) {
|
|
28
58
|
const customConfig = config.custom[tagName];
|
|
29
59
|
if (customConfig?.register) {
|
|
30
60
|
const singleConfig = {
|
|
@@ -47,6 +77,47 @@ export async function validateAllComponents(root) {
|
|
|
47
77
|
const config = await loadFurnaceConfig(root);
|
|
48
78
|
const furnacePaths = getFurnacePaths(root);
|
|
49
79
|
const results = new Map();
|
|
80
|
+
// Validate composition graph integrity (dangling references and cycles)
|
|
81
|
+
try {
|
|
82
|
+
validateComposesReferences(config.stock, config.overrides, config.custom);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
// Attribute the issue to the first custom component with a bad composes reference
|
|
87
|
+
for (const [name, cfg] of Object.entries(config.custom)) {
|
|
88
|
+
if (cfg.composes) {
|
|
89
|
+
const existing = results.get(name) ?? [];
|
|
90
|
+
existing.push({
|
|
91
|
+
component: name,
|
|
92
|
+
severity: 'error',
|
|
93
|
+
check: 'composes-dangling-reference',
|
|
94
|
+
message,
|
|
95
|
+
});
|
|
96
|
+
results.set(name, existing);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
detectComposesCycles(config.custom);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
// Attribute the cycle issue to the first custom component in the cycle
|
|
107
|
+
for (const name of Object.keys(config.custom)) {
|
|
108
|
+
if (config.custom[name]?.composes) {
|
|
109
|
+
const existing = results.get(name) ?? [];
|
|
110
|
+
existing.push({
|
|
111
|
+
component: name,
|
|
112
|
+
severity: 'error',
|
|
113
|
+
check: 'composes-cycle',
|
|
114
|
+
message,
|
|
115
|
+
});
|
|
116
|
+
results.set(name, existing);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
50
121
|
// Override components
|
|
51
122
|
for (const name of Object.keys(config.overrides)) {
|
|
52
123
|
const componentDir = join(furnacePaths.overridesDir, name);
|
|
@@ -79,9 +150,12 @@ export async function validateAllComponents(root) {
|
|
|
79
150
|
continue;
|
|
80
151
|
}
|
|
81
152
|
// Pass root so that per-component token link validation runs.
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
153
|
+
// Skip registration/jar.mn checks inside validateComponent — the aggregate
|
|
154
|
+
// validators below run them exactly once across all components, which both
|
|
155
|
+
// surfaces cross-component issues and avoids double-counting.
|
|
156
|
+
const issues = await validateComponent(componentDir, name, 'custom', config, root, {
|
|
157
|
+
skipAggregateChecks: true,
|
|
158
|
+
});
|
|
85
159
|
results.set(name, issues);
|
|
86
160
|
}
|
|
87
161
|
// Registration pattern validation (customElements.js Pattern A vs B)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects drift between an override's stored `baseVersion` and the current
|
|
3
|
+
* Firefox version recorded in `fireforge.json`.
|
|
4
|
+
*
|
|
5
|
+
* Overrides are forks of Firefox source files taken at a specific point in
|
|
6
|
+
* time. If Firefox moves forward and the override's `baseVersion` is not
|
|
7
|
+
* refreshed, the override may apply against a file whose upstream shape has
|
|
8
|
+
* changed — which is the single biggest silent failure mode for furnace.
|
|
9
|
+
*
|
|
10
|
+
* This module is deliberately pure and string-only: it does no I/O and does
|
|
11
|
+
* not parse Firefox version components. Comparing by string equality is
|
|
12
|
+
* sufficient because `fireforge.json` stores a canonical version string
|
|
13
|
+
* (e.g. `"146.0esr"`) and overrides are created with exactly that string
|
|
14
|
+
* copied from `forgeConfig.firefox.version`. Any string mismatch is worth
|
|
15
|
+
* surfacing — even "140.0" vs "146.0esr" is a real drift signal.
|
|
16
|
+
*
|
|
17
|
+
* The result is advisory: apply/deploy emit warnings but do not fail, and
|
|
18
|
+
* status reports drift alongside the component overview. Nothing here
|
|
19
|
+
* should be wired into a blocking code path without an operator prompt.
|
|
20
|
+
*/
|
|
21
|
+
import type { FurnaceConfig } from '../types/furnace.js';
|
|
22
|
+
/** Severity of the version drift between an override's base and the current Firefox version. */
|
|
23
|
+
export type DriftSeverity = 'major' | 'minor' | 'patch';
|
|
24
|
+
export interface OverrideVersionDrift {
|
|
25
|
+
name: string;
|
|
26
|
+
/** The version the override was originally created against. */
|
|
27
|
+
baseVersion: string;
|
|
28
|
+
/** The Firefox version currently recorded in `fireforge.json`. */
|
|
29
|
+
currentVersion: string;
|
|
30
|
+
/** How severe the drift is, based on comparing version components. */
|
|
31
|
+
severity: DriftSeverity;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Classifies how severe a version drift is by comparing the numeric
|
|
35
|
+
* components of the two version strings. Falls back to `'major'` when
|
|
36
|
+
* either version is unparseable — this ensures that unusual version
|
|
37
|
+
* formats surface with the highest visibility rather than being silently
|
|
38
|
+
* downgraded.
|
|
39
|
+
*/
|
|
40
|
+
export declare function classifyDriftSeverity(baseVersion: string, currentVersion: string): DriftSeverity;
|
|
41
|
+
/**
|
|
42
|
+
* Returns every override whose recorded `baseVersion` does not match the
|
|
43
|
+
* current Firefox version. Returns an empty array when everything is in
|
|
44
|
+
* sync, when there are no overrides, or when `currentVersion` is empty
|
|
45
|
+
* (the caller should surface config problems via a different path).
|
|
46
|
+
*/
|
|
47
|
+
export declare function findOverrideBaseVersionDrift(config: FurnaceConfig, currentVersion: string): OverrideVersionDrift[];
|
|
48
|
+
/**
|
|
49
|
+
* Formats a single drift entry into a one-line human-readable warning.
|
|
50
|
+
* Kept alongside the detector so the same wording is reused by every
|
|
51
|
+
* command that surfaces drift.
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatOverrideBaseVersionDriftWarning(entry: OverrideVersionDrift): string;
|
|
54
|
+
/** Formats a blocking preflight error for one or more stale overrides. */
|
|
55
|
+
export declare function formatOverrideBaseVersionDriftError(entries: OverrideVersionDrift[]): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Detects drift between an override's stored `baseVersion` and the current
|
|
4
|
+
* Firefox version recorded in `fireforge.json`.
|
|
5
|
+
*
|
|
6
|
+
* Overrides are forks of Firefox source files taken at a specific point in
|
|
7
|
+
* time. If Firefox moves forward and the override's `baseVersion` is not
|
|
8
|
+
* refreshed, the override may apply against a file whose upstream shape has
|
|
9
|
+
* changed — which is the single biggest silent failure mode for furnace.
|
|
10
|
+
*
|
|
11
|
+
* This module is deliberately pure and string-only: it does no I/O and does
|
|
12
|
+
* not parse Firefox version components. Comparing by string equality is
|
|
13
|
+
* sufficient because `fireforge.json` stores a canonical version string
|
|
14
|
+
* (e.g. `"146.0esr"`) and overrides are created with exactly that string
|
|
15
|
+
* copied from `forgeConfig.firefox.version`. Any string mismatch is worth
|
|
16
|
+
* surfacing — even "140.0" vs "146.0esr" is a real drift signal.
|
|
17
|
+
*
|
|
18
|
+
* The result is advisory: apply/deploy emit warnings but do not fail, and
|
|
19
|
+
* status reports drift alongside the component overview. Nothing here
|
|
20
|
+
* should be wired into a blocking code path without an operator prompt.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Parses a version string into its major, minor, and patch numeric
|
|
24
|
+
* components. Non-numeric suffixes (e.g. "esr") are stripped. Returns
|
|
25
|
+
* `[NaN, NaN, NaN]` for unparseable strings.
|
|
26
|
+
*/
|
|
27
|
+
function parseVersionComponents(version) {
|
|
28
|
+
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version);
|
|
29
|
+
if (!match)
|
|
30
|
+
return [NaN, NaN, NaN];
|
|
31
|
+
return [
|
|
32
|
+
Number(match[1]),
|
|
33
|
+
match[2] !== undefined ? Number(match[2]) : 0,
|
|
34
|
+
match[3] !== undefined ? Number(match[3]) : 0,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Classifies how severe a version drift is by comparing the numeric
|
|
39
|
+
* components of the two version strings. Falls back to `'major'` when
|
|
40
|
+
* either version is unparseable — this ensures that unusual version
|
|
41
|
+
* formats surface with the highest visibility rather than being silently
|
|
42
|
+
* downgraded.
|
|
43
|
+
*/
|
|
44
|
+
export function classifyDriftSeverity(baseVersion, currentVersion) {
|
|
45
|
+
const [baseMajor, baseMinor] = parseVersionComponents(baseVersion);
|
|
46
|
+
const [curMajor, curMinor] = parseVersionComponents(currentVersion);
|
|
47
|
+
if (isNaN(baseMajor) || isNaN(curMajor))
|
|
48
|
+
return 'major';
|
|
49
|
+
if (baseMajor !== curMajor)
|
|
50
|
+
return 'major';
|
|
51
|
+
if (baseMinor !== curMinor)
|
|
52
|
+
return 'minor';
|
|
53
|
+
return 'patch';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns every override whose recorded `baseVersion` does not match the
|
|
57
|
+
* current Firefox version. Returns an empty array when everything is in
|
|
58
|
+
* sync, when there are no overrides, or when `currentVersion` is empty
|
|
59
|
+
* (the caller should surface config problems via a different path).
|
|
60
|
+
*/
|
|
61
|
+
export function findOverrideBaseVersionDrift(config, currentVersion) {
|
|
62
|
+
if (!currentVersion)
|
|
63
|
+
return [];
|
|
64
|
+
const drift = [];
|
|
65
|
+
for (const [name, override] of Object.entries(config.overrides)) {
|
|
66
|
+
if (override.baseVersion && override.baseVersion !== currentVersion) {
|
|
67
|
+
drift.push({
|
|
68
|
+
name,
|
|
69
|
+
baseVersion: override.baseVersion,
|
|
70
|
+
currentVersion,
|
|
71
|
+
severity: classifyDriftSeverity(override.baseVersion, currentVersion),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return drift;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Formats a single drift entry into a one-line human-readable warning.
|
|
79
|
+
* Kept alongside the detector so the same wording is reused by every
|
|
80
|
+
* command that surfaces drift.
|
|
81
|
+
*/
|
|
82
|
+
export function formatOverrideBaseVersionDriftWarning(entry) {
|
|
83
|
+
const severityLabel = entry.severity === 'major'
|
|
84
|
+
? ' (major version jump)'
|
|
85
|
+
: entry.severity === 'minor'
|
|
86
|
+
? ' (minor version change)'
|
|
87
|
+
: ' (patch-level change)';
|
|
88
|
+
return `Override "${entry.name}" was created against Firefox ${entry.baseVersion}, but fireforge.json records ${entry.currentVersion}${severityLabel}. The upstream component may have changed — re-run "fireforge furnace validate ${entry.name}" and refresh the override if needed.`;
|
|
89
|
+
}
|
|
90
|
+
/** Formats a blocking preflight error for one or more stale overrides. */
|
|
91
|
+
export function formatOverrideBaseVersionDriftError(entries) {
|
|
92
|
+
const names = entries.map((entry) => entry.name).sort();
|
|
93
|
+
const summary = entries.length === 1
|
|
94
|
+
? `Override "${names[0]}" is stale against the Firefox version recorded in fireforge.json.`
|
|
95
|
+
: `${entries.length} overrides are stale against the Firefox version recorded in fireforge.json (${names.join(', ')}).`;
|
|
96
|
+
return (`${summary}\n\n` +
|
|
97
|
+
'Run "fireforge furnace refresh <name>" to merge upstream changes, ' +
|
|
98
|
+
'update baseVersion in furnace.json to acknowledge the new baseline, ' +
|
|
99
|
+
'or pass --force to proceed despite the drift.');
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=furnace-version-drift.js.map
|
|
@@ -48,6 +48,14 @@ export declare function unstageFiles(repoDir: string, files: string[]): Promise<
|
|
|
48
48
|
* @returns true if file exists in HEAD
|
|
49
49
|
*/
|
|
50
50
|
export declare function fileExistsInHead(repoDir: string, filePath: string): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Gets the content of a file at a specific git ref (HEAD by default).
|
|
53
|
+
* @param repoDir - Repository directory
|
|
54
|
+
* @param filePath - Path to the file (relative to repo)
|
|
55
|
+
* @param ref - Git ref to read from (commit hash, branch, tag). Defaults to HEAD.
|
|
56
|
+
* @returns File content or null if file doesn't exist at that ref
|
|
57
|
+
*/
|
|
58
|
+
export declare function getFileContentAtRef(repoDir: string, filePath: string, ref?: string): Promise<string | null>;
|
|
51
59
|
/**
|
|
52
60
|
* Gets the content of a file from HEAD commit.
|
|
53
61
|
* @param repoDir - Repository directory
|
|
@@ -96,23 +96,36 @@ export async function fileExistsInHead(repoDir, filePath) {
|
|
|
96
96
|
return (await git(['ls-tree', 'HEAD', '--', filePath], repoDir)).trim().length > 0;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
* Gets the content of a file
|
|
99
|
+
* Gets the content of a file at a specific git ref (HEAD by default).
|
|
100
100
|
* @param repoDir - Repository directory
|
|
101
101
|
* @param filePath - Path to the file (relative to repo)
|
|
102
|
-
* @
|
|
102
|
+
* @param ref - Git ref to read from (commit hash, branch, tag). Defaults to HEAD.
|
|
103
|
+
* @returns File content or null if file doesn't exist at that ref
|
|
103
104
|
*/
|
|
104
|
-
export async function
|
|
105
|
+
export async function getFileContentAtRef(repoDir, filePath, ref = 'HEAD') {
|
|
105
106
|
await ensureGit();
|
|
106
|
-
const result = await exec('git', ['show',
|
|
107
|
+
const result = await exec('git', ['show', `${ref}:${filePath}`], { cwd: repoDir });
|
|
107
108
|
if (result.exitCode !== 0) {
|
|
108
109
|
const stderr = result.stderr.trim();
|
|
109
|
-
|
|
110
|
+
// Recognise the "file does not exist at this ref" variants across git versions.
|
|
111
|
+
// The ref name in quotes varies with what was passed (HEAD, a SHA, a tag), so
|
|
112
|
+
// match loosely rather than interpolating ref into a regex.
|
|
113
|
+
if (/exists on disk, but not in '[^']*'|path '[^']*' exists, but not '[^']*'|path '[^']*' does not exist in '[^']*'/i.test(stderr)) {
|
|
110
114
|
return null;
|
|
111
115
|
}
|
|
112
|
-
throw new GitError(stderr || 'Git command failed', `show
|
|
116
|
+
throw new GitError(stderr || 'Git command failed', `show ${ref}:${filePath}`);
|
|
113
117
|
}
|
|
114
118
|
return result.stdout;
|
|
115
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Gets the content of a file from HEAD commit.
|
|
122
|
+
* @param repoDir - Repository directory
|
|
123
|
+
* @param filePath - Path to the file (relative to repo)
|
|
124
|
+
* @returns File content or null if file doesn't exist in HEAD
|
|
125
|
+
*/
|
|
126
|
+
export async function getFileContentFromHead(repoDir, filePath) {
|
|
127
|
+
return getFileContentAtRef(repoDir, filePath, 'HEAD');
|
|
128
|
+
}
|
|
116
129
|
/**
|
|
117
130
|
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
118
131
|
* Uses the same heuristic as git.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Diffs projected lint issues against the baseline and returns those
|
|
4
|
+
* considered "new" regressions.
|
|
5
|
+
*
|
|
6
|
+
* The equality key is the issue fingerprint when present, otherwise the
|
|
7
|
+
* full `(check, file, message)` triple. Fingerprints are emitted by rules
|
|
8
|
+
* whose message text can drift between otherwise-equivalent runs (for
|
|
9
|
+
* example because the message embeds later-owner filenames or positional
|
|
10
|
+
* detail). Falling back to the full tuple keeps the helper conservative
|
|
11
|
+
* for older/non-fingerprinted rules: if their message changes, we would
|
|
12
|
+
* rather surface a potential regression than silently swallow it.
|
|
13
|
+
*
|
|
14
|
+
* Consumption order within the projected list is stable (the input
|
|
15
|
+
* order is preserved), so when baseline has N issues for a key and
|
|
16
|
+
* projected has N+M for the same key, the *last* M projected issues on
|
|
17
|
+
* that key are reported as regressions. That keeps the reporting
|
|
18
|
+
* deterministic and gives the operator at least one concrete message
|
|
19
|
+
* per regression even when only counts differ.
|
|
20
|
+
*
|
|
21
|
+
* @param baseline - Error-severity issues from the current queue
|
|
22
|
+
* @param projected - Error-severity issues from the projected queue
|
|
23
|
+
* @returns Subset of `projected` not matched by a baseline counterpart
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeProjectedLintRegressions(baseline: PatchLintIssue[], projected: PatchLintIssue[]): PatchLintIssue[];
|