@hominis/fireforge 0.39.0 → 0.41.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 +100 -0
- package/README.md +35 -5
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +27 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +27 -9
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +30 -10
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +73 -51
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +26 -22
- package/dist/src/commands/manifest.js +3 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +152 -51
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +112 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +105 -0
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +11 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +123 -289
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +29 -23
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +48 -0
- package/dist/src/commands/status-check.js +106 -0
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +190 -170
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +59 -7
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +19 -0
- package/dist/src/commands/tree.js +320 -0
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +45 -30
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +49 -14
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.d.ts +2 -2
- package/dist/src/core/patch-export.js +4 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
- package/dist/src/core/patch-lint-checkjs.js +132 -8
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +2 -1
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +31 -0
- package/dist/src/core/patch-lint.js +114 -8
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +24 -16
- package/dist/src/core/smoke-patterns.js +26 -14
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +22 -1
- package/dist/src/core/status-classify.js +112 -70
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +179 -0
- package/dist/src/core/tree-store.d.ts +129 -0
- package/dist/src/core/tree-store.js +452 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +119 -3
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +33 -2
- package/dist/src/utils/validation.js +57 -2
- package/package.json +13 -13
|
@@ -3,14 +3,15 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
5
5
|
import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, getPerPatchLintCacheHeadSha, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/lint-cache.js';
|
|
6
|
-
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
7
|
-
import { invokePatchLintCheckJsGrouped, } from '../core/patch-lint-checkjs.js';
|
|
8
|
-
import { resolvePatchOwnedSysMjs } from '../core/patch-lint-ownership.js';
|
|
6
|
+
import { buildPatchQueueContext, countNonBinaryDiffLines, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
9
7
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
10
8
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
11
9
|
import { GeneralError } from '../errors/base.js';
|
|
10
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
12
11
|
import { pathExists } from '../utils/fs.js';
|
|
13
12
|
import { info, outro, success, warn } from '../utils/logger.js';
|
|
13
|
+
import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
|
|
14
|
+
import { writePerPatchLintReport } from './lint-report.js';
|
|
14
15
|
function buildPerPatchMaxWarningsMessage(count, maxWarnings, linted) {
|
|
15
16
|
return (`Patch lint found ${count} warning(s) across ${linted} patch(es), exceeding --max-warnings ${maxWarnings}.` +
|
|
16
17
|
' If this is a release gate, run with --per-patch to identify the owning patch. For intentional staged imports, use patch staged-dependency; for ownership repairs, preview patch move-files, patch reorder --dry-run, or re-export --files --dry-run; add scoped lintIgnore only after review.');
|
|
@@ -33,13 +34,17 @@ function emitTierNotice(filename, files, tier) {
|
|
|
33
34
|
*/
|
|
34
35
|
async function lintQueuedPatch(patch, lintCtx) {
|
|
35
36
|
const { projectRoot, paths, config, ctx, cache, engineHeadSha } = lintCtx;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (await pathExists(join(paths.engine, f)))
|
|
39
|
-
existing.push(f);
|
|
40
|
-
}
|
|
37
|
+
const present = await mapWithConcurrency(patch.filesAffected, 8, (f) => pathExists(join(paths.engine, f)));
|
|
38
|
+
const existing = patch.filesAffected.filter((_, index) => present[index] === true);
|
|
41
39
|
if (existing.length === 0) {
|
|
42
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
status: 'skipped',
|
|
42
|
+
existingFiles: [],
|
|
43
|
+
rawIssues: [],
|
|
44
|
+
suppressedIssues: [],
|
|
45
|
+
lineCount: 0,
|
|
46
|
+
usedCheckJs: false,
|
|
47
|
+
};
|
|
43
48
|
}
|
|
44
49
|
const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
45
50
|
let cacheKey;
|
|
@@ -61,16 +66,33 @@ async function lintQueuedPatch(patch, lintCtx) {
|
|
|
61
66
|
// an engine-side revert that would empty the diff always changes the
|
|
62
67
|
// key and misses the cache (pinned by the "engine-side content
|
|
63
68
|
// revert invalidates the per-patch cache" test — FORGE F5).
|
|
64
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
status: 'cached',
|
|
71
|
+
existingFiles: existing,
|
|
72
|
+
rawIssues: cached.issues,
|
|
73
|
+
suppressedIssues: cached.suppressed,
|
|
74
|
+
lineCount: cached.lineCount,
|
|
75
|
+
usedCheckJs: false,
|
|
76
|
+
};
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
const diff = await getDiffForFilesAgainstHead(paths.engine, existing);
|
|
68
80
|
if (!diff.trim()) {
|
|
69
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
status: 'skipped',
|
|
83
|
+
existingFiles: [],
|
|
84
|
+
rawIssues: [],
|
|
85
|
+
suppressedIssues: [],
|
|
86
|
+
lineCount: 0,
|
|
87
|
+
usedCheckJs: false,
|
|
88
|
+
};
|
|
70
89
|
}
|
|
71
90
|
// checkJs: instead of rebuilding the program per patch, slice this patch's
|
|
72
91
|
// findings out of the one queue-wide program (built lazily on first miss).
|
|
73
|
-
|
|
92
|
+
const suppressedIssues = [];
|
|
93
|
+
const lintOptions = {
|
|
94
|
+
onSuppressed: (suppressed) => suppressedIssues.push(...suppressed),
|
|
95
|
+
};
|
|
74
96
|
let usedCheckJs = false;
|
|
75
97
|
if (lintCtx.checkJs) {
|
|
76
98
|
const grouped = await lintCtx.checkJs.getGrouped();
|
|
@@ -81,20 +103,28 @@ async function lintQueuedPatch(patch, lintCtx) {
|
|
|
81
103
|
for (const rel of owned)
|
|
82
104
|
precomputedCheckJs.push(...(grouped.byFile.get(rel) ?? []));
|
|
83
105
|
}
|
|
84
|
-
lintOptions =
|
|
106
|
+
lintOptions.precomputedCheckJs = precomputedCheckJs;
|
|
85
107
|
}
|
|
86
108
|
const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore, patch.tier, lintOptions);
|
|
87
109
|
const result = {
|
|
88
110
|
status: 'linted',
|
|
89
111
|
existingFiles: existing,
|
|
90
112
|
rawIssues: patchIssues,
|
|
113
|
+
suppressedIssues,
|
|
114
|
+
lineCount: countNonBinaryDiffLines(diff).textLines,
|
|
91
115
|
usedCheckJs,
|
|
92
116
|
};
|
|
93
117
|
if (cache && cacheKey) {
|
|
94
|
-
result.cacheWrite = { key: cacheKey
|
|
118
|
+
result.cacheWrite = { key: cacheKey };
|
|
95
119
|
}
|
|
96
120
|
return result;
|
|
97
121
|
}
|
|
122
|
+
/** Size-rule check IDs whose waived measurement is still reported (FORGE G10). */
|
|
123
|
+
const SUPPRESSED_SIZE_CHECKS = new Set([
|
|
124
|
+
'large-patch-lines',
|
|
125
|
+
'large-patch-files',
|
|
126
|
+
'file-too-large',
|
|
127
|
+
]);
|
|
98
128
|
/**
|
|
99
129
|
* Applies the per-patch results in patch order so the bounded concurrency
|
|
100
130
|
* cannot reorder output: emits each tier notice, the once-only run-level
|
|
@@ -108,6 +138,7 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
|
|
|
108
138
|
skipped: 0,
|
|
109
139
|
cacheDirty: false,
|
|
110
140
|
reusedCacheEntries: 0,
|
|
141
|
+
suppressed: 0,
|
|
111
142
|
};
|
|
112
143
|
let globalCheckJsEmitted = false;
|
|
113
144
|
for (let i = 0; i < subset.length; i++) {
|
|
@@ -132,12 +163,22 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
|
|
|
132
163
|
issues.push(...(await checkJs.getGlobal()));
|
|
133
164
|
}
|
|
134
165
|
if (result.cacheWrite && cache) {
|
|
135
|
-
setCachedPerPatchLintIssues(cache, patch.filename, result.cacheWrite.key, result.
|
|
166
|
+
setCachedPerPatchLintIssues(cache, patch.filename, result.cacheWrite.key, result.rawIssues, result.suppressedIssues, result.lineCount);
|
|
136
167
|
totals.cacheDirty = true;
|
|
137
168
|
}
|
|
138
169
|
for (const issue of result.rawIssues) {
|
|
139
170
|
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
140
171
|
}
|
|
172
|
+
// A waived size finding still reports its MEASUREMENT (FORGE G10):
|
|
173
|
+
// the finding stays suppressed (no exit-code / --max-warnings effect)
|
|
174
|
+
// but the current count is readable from the tool that enforces it,
|
|
175
|
+
// so a waiver's cited size can be calibrated without hand-measuring.
|
|
176
|
+
for (const suppressedIssue of result.suppressedIssues) {
|
|
177
|
+
if (!SUPPRESSED_SIZE_CHECKS.has(suppressedIssue.check))
|
|
178
|
+
continue;
|
|
179
|
+
info(`NOTICE [${suppressedIssue.check}] ${patch.filename}: suppressed by lintIgnore — ${suppressedIssue.message}`);
|
|
180
|
+
}
|
|
181
|
+
totals.suppressed += result.suppressedIssues.length;
|
|
141
182
|
if (result.status === 'cached')
|
|
142
183
|
totals.reusedCacheEntries++;
|
|
143
184
|
totals.linted++;
|
|
@@ -193,11 +234,11 @@ function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
|
193
234
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
194
235
|
const notices = issues.filter((i) => i.severity === 'notice');
|
|
195
236
|
for (const issue of notices)
|
|
196
|
-
info(`NOTICE
|
|
237
|
+
info(`NOTICE ${formatPatchLintIssue(issue)}`);
|
|
197
238
|
for (const issue of warnings)
|
|
198
|
-
warn(
|
|
239
|
+
warn(formatPatchLintIssue(issue));
|
|
199
240
|
for (const issue of errors)
|
|
200
|
-
warn(`ERROR
|
|
241
|
+
warn(`ERROR ${formatPatchLintIssue(issue)}`);
|
|
201
242
|
info(`\nLint (per-patch over ${linted} patch(es)): ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
202
243
|
if (errors.length > 0) {
|
|
203
244
|
outro('Lint failed');
|
|
@@ -248,7 +289,9 @@ function selectPatchSubset(manifest, requested) {
|
|
|
248
289
|
const found = manifest.patches.filter((p) => matches(p, name));
|
|
249
290
|
if (found.length === 0) {
|
|
250
291
|
const available = manifest.patches.map((p) => p.filename).join(', ');
|
|
251
|
-
throw new GeneralError(
|
|
292
|
+
throw new GeneralError(`No patch in the queue matches "${name}". In --per-patch mode, positional arguments ` +
|
|
293
|
+
`and --patches select patches (filename, stem, manifest name, or slug) — not engine ` +
|
|
294
|
+
`files; drop --per-patch to lint engine paths. Available patches: ${available}`);
|
|
252
295
|
}
|
|
253
296
|
for (const p of found) {
|
|
254
297
|
if (!seen.has(p.filename)) {
|
|
@@ -259,36 +302,6 @@ function selectPatchSubset(manifest, requested) {
|
|
|
259
302
|
}
|
|
260
303
|
return selected;
|
|
261
304
|
}
|
|
262
|
-
/**
|
|
263
|
-
* Builds the per-run checkJs program controller when `patchLint.checkJs` is
|
|
264
|
-
* enabled, or returns undefined. The program is built lazily on the first
|
|
265
|
-
* cache miss (so an all-warm run never pays for it) and reused for every
|
|
266
|
-
* subsequent patch in the run.
|
|
267
|
-
*/
|
|
268
|
-
function buildPerRunCheckJs(projectRoot, paths, config, ctx) {
|
|
269
|
-
const patchLint = config.patchLint;
|
|
270
|
-
if (!patchLint?.checkJs)
|
|
271
|
-
return undefined;
|
|
272
|
-
const ownedByPatch = new Map();
|
|
273
|
-
for (const entry of ctx.entries) {
|
|
274
|
-
const owned = new Set();
|
|
275
|
-
for (const f of entry.newFiles.keys()) {
|
|
276
|
-
if (f.endsWith('.sys.mjs'))
|
|
277
|
-
owned.add(f);
|
|
278
|
-
}
|
|
279
|
-
if (owned.size > 0)
|
|
280
|
-
ownedByPatch.set(entry.filename, owned);
|
|
281
|
-
}
|
|
282
|
-
// Memoise the *promise*, not the resolved value: under the bounded pool
|
|
283
|
-
// several patches can reach `getGrouped` before the first build resolves, and
|
|
284
|
-
// `??=` on the promise (a synchronous expression) guarantees a single build.
|
|
285
|
-
let groupedPromise;
|
|
286
|
-
return {
|
|
287
|
-
ownedByPatch,
|
|
288
|
-
getGrouped: () => (groupedPromise ??= invokePatchLintCheckJsGrouped(paths.engine, resolvePatchOwnedSysMjs(new Set(), ctx), patchLint, projectRoot)),
|
|
289
|
-
getGlobal: async () => (await (groupedPromise ??= invokePatchLintCheckJsGrouped(paths.engine, resolvePatchOwnedSysMjs(new Set(), ctx), patchLint, projectRoot))).global,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
305
|
/**
|
|
293
306
|
* Lints each patch in the queue as its own isolated diff, honouring
|
|
294
307
|
* per-patch `lintIgnore` entries. Cross-patch rules still run once over
|
|
@@ -337,7 +350,10 @@ export async function lintPerPatch(projectRoot, paths, options = {}) {
|
|
|
337
350
|
severity: issue.severity,
|
|
338
351
|
});
|
|
339
352
|
}
|
|
340
|
-
|
|
353
|
+
// With `--patches`, the checkJs program roots at only the subset's owned
|
|
354
|
+
// files — the full queue stays resolvable, so a cold subset run costs the
|
|
355
|
+
// subset's import closure instead of the whole queue (FORGE J1c).
|
|
356
|
+
const checkJs = buildPerRunCheckJs(projectRoot, paths, config, ctx, isSubset ? subsetNames : undefined);
|
|
341
357
|
// Lint patches concurrently, then apply every side effect in patch order so
|
|
342
358
|
// the issue rows, the run-level checkJs errors, and the saved cache are
|
|
343
359
|
// identical to a serial run.
|
|
@@ -350,7 +366,7 @@ export async function lintPerPatch(projectRoot, paths, options = {}) {
|
|
|
350
366
|
engineHeadSha,
|
|
351
367
|
checkJs,
|
|
352
368
|
});
|
|
353
|
-
const { linted, skipped, cacheDirty, reusedCacheEntries } = await applyPerPatchResults(subset, results, issues, checkJs, cache);
|
|
369
|
+
const { linted, skipped, cacheDirty, reusedCacheEntries, suppressed } = await applyPerPatchResults(subset, results, issues, checkJs, cache);
|
|
354
370
|
for (const issue of lintPatchQueue(ctx)) {
|
|
355
371
|
if (isSubset && !subsetTouchedFiles.has(issue.file))
|
|
356
372
|
continue;
|
|
@@ -361,5 +377,11 @@ export async function lintPerPatch(projectRoot, paths, options = {}) {
|
|
|
361
377
|
if (reusedCacheEntries > 0) {
|
|
362
378
|
info(`Reused lint cache for ${reusedCacheEntries} patch${reusedCacheEntries === 1 ? '' : 'es'}.`);
|
|
363
379
|
}
|
|
380
|
+
if (suppressed > 0) {
|
|
381
|
+
info(`Suppressed ${suppressed} issue(s) via per-patch lintIgnore.`);
|
|
382
|
+
}
|
|
383
|
+
if (options.report !== undefined) {
|
|
384
|
+
await writePerPatchLintReport(options.report, subset, results);
|
|
385
|
+
}
|
|
364
386
|
reportPerPatchOutcome(issues, linted, skipped, options);
|
|
365
387
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-run checkJs program controller, shared by `lint --per-patch` and
|
|
3
|
+
* `re-export` (FORGE J1).
|
|
4
|
+
*
|
|
5
|
+
* The dominant fixed cost of patch lint is one `ts.createProgram` over the
|
|
6
|
+
* queue's patch-owned `.sys.mjs` files (~37 s on a 290-patch consumer
|
|
7
|
+
* queue). This controller builds that program at most ONCE per command
|
|
8
|
+
* invocation (lazily, promise-memoised) and lets callers slice per-patch
|
|
9
|
+
* findings out of the grouped result. Three levers keep the cost paid only
|
|
10
|
+
* when it buys something:
|
|
11
|
+
*
|
|
12
|
+
* - **Lazy build**: nothing is built until the first cache miss asks.
|
|
13
|
+
* - **Root scoping** (`rootScopePatches`): with `--patches <subset>` the
|
|
14
|
+
* program's roots are only the subset's owned files; the full queue
|
|
15
|
+
* stays resolvable, so cross-patch imports type-check identically while
|
|
16
|
+
* unrelated files are never parsed (FORGE J1c).
|
|
17
|
+
* - **Warm-run probe**: run-level ("global") checkJs findings come only
|
|
18
|
+
* from a missing `typescript` package or an unreadable shim — never
|
|
19
|
+
* from the built program — so an all-cache-hit run satisfies "warm
|
|
20
|
+
* never reports less than cold" (FORGE F5) via
|
|
21
|
+
* `probeCheckJsGlobalIssues` instead of building anything.
|
|
22
|
+
*/
|
|
23
|
+
import type { getProjectPaths, loadConfig } from '../core/config.js';
|
|
24
|
+
import { type GroupedCheckJsResult } from '../core/patch-lint-checkjs.js';
|
|
25
|
+
import type { PatchQueueContext } from '../core/patch-lint-cross.js';
|
|
26
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
27
|
+
/**
|
|
28
|
+
* Queue-wide checkJs program built once per run and sliced per patch, so a
|
|
29
|
+
* single type regression surfaces once against its owning patch instead of
|
|
30
|
+
* being duplicated for every patch in the queue.
|
|
31
|
+
*/
|
|
32
|
+
export interface PerRunCheckJs {
|
|
33
|
+
/** Patch filename → the checkJs-relevant files that patch creates. */
|
|
34
|
+
ownedByPatch: Map<string, Set<string>>;
|
|
35
|
+
/** Every checkJs-relevant file in the queue (sys + test scripts) — the
|
|
36
|
+
* resolution universe the program can see. Callers use it to detect
|
|
37
|
+
* files the hoisted program has never heard of (fresh `--scan`
|
|
38
|
+
* adoptions) and fall back to a per-patch build. */
|
|
39
|
+
resolutionSet: ReadonlySet<string>;
|
|
40
|
+
/** Builds (once, lazily on first cache miss) and returns the grouped run.
|
|
41
|
+
* Promise-memoised so the bounded per-patch pool builds the program exactly
|
|
42
|
+
* once even when several patches reach it concurrently. */
|
|
43
|
+
getGrouped: () => Promise<GroupedCheckJsResult>;
|
|
44
|
+
/** Run-level checkJs errors (e.g. TypeScript missing). When the program
|
|
45
|
+
* was never built (all-warm run) this probes the two global failure
|
|
46
|
+
* sources directly instead of building it — FORGE F5 holds because the
|
|
47
|
+
* built program never contributes globals of its own. */
|
|
48
|
+
getGlobal: () => Promise<PatchLintIssue[]>;
|
|
49
|
+
/** The byFile slices for `files` (globals deliberately EXCLUDED — they
|
|
50
|
+
* are run-level, never cached, and emitted once per invocation via
|
|
51
|
+
* `getGlobal`). Feed to `LintExportedPatchOptions.precomputedCheckJs`. */
|
|
52
|
+
sliceFor: (files: ReadonlySet<string>) => Promise<PatchLintIssue[]>;
|
|
53
|
+
/** True when this file kind participates in the run's checkJs surface
|
|
54
|
+
* (`.sys.mjs`, plus test scripts when `checkJsTestFiles` is on). */
|
|
55
|
+
isRelevant: (file: string) => boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds the per-run checkJs program controller when `patchLint.checkJs` is
|
|
59
|
+
* enabled, or returns undefined. With `rootScopePatches` set (the
|
|
60
|
+
* `--patches` subset, by patch filename), the program roots at only the
|
|
61
|
+
* subset's owned files and the test-file pass runs only the subset's own
|
|
62
|
+
* scripts (head.js discovery still spans the queue).
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildPerRunCheckJs(projectRoot: string, paths: ReturnType<typeof getProjectPaths>, config: Awaited<ReturnType<typeof loadConfig>>, ctx: PatchQueueContext, rootScopePatches?: ReadonlySet<string>): PerRunCheckJs | undefined;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Per-run checkJs program controller, shared by `lint --per-patch` and
|
|
4
|
+
* `re-export` (FORGE J1).
|
|
5
|
+
*
|
|
6
|
+
* The dominant fixed cost of patch lint is one `ts.createProgram` over the
|
|
7
|
+
* queue's patch-owned `.sys.mjs` files (~37 s on a 290-patch consumer
|
|
8
|
+
* queue). This controller builds that program at most ONCE per command
|
|
9
|
+
* invocation (lazily, promise-memoised) and lets callers slice per-patch
|
|
10
|
+
* findings out of the grouped result. Three levers keep the cost paid only
|
|
11
|
+
* when it buys something:
|
|
12
|
+
*
|
|
13
|
+
* - **Lazy build**: nothing is built until the first cache miss asks.
|
|
14
|
+
* - **Root scoping** (`rootScopePatches`): with `--patches <subset>` the
|
|
15
|
+
* program's roots are only the subset's owned files; the full queue
|
|
16
|
+
* stays resolvable, so cross-patch imports type-check identically while
|
|
17
|
+
* unrelated files are never parsed (FORGE J1c).
|
|
18
|
+
* - **Warm-run probe**: run-level ("global") checkJs findings come only
|
|
19
|
+
* from a missing `typescript` package or an unreadable shim — never
|
|
20
|
+
* from the built program — so an all-cache-hit run satisfies "warm
|
|
21
|
+
* never reports less than cold" (FORGE F5) via
|
|
22
|
+
* `probeCheckJsGlobalIssues` instead of building anything.
|
|
23
|
+
*/
|
|
24
|
+
import { invokePatchLintCheckJsGrouped, probeCheckJsGlobalIssues, runCheckJsTestFilesGrouped, } from '../core/patch-lint-checkjs.js';
|
|
25
|
+
import { isTestScriptFile, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from '../core/patch-lint-ownership.js';
|
|
26
|
+
/**
|
|
27
|
+
* Builds the per-run checkJs program controller when `patchLint.checkJs` is
|
|
28
|
+
* enabled, or returns undefined. With `rootScopePatches` set (the
|
|
29
|
+
* `--patches` subset, by patch filename), the program roots at only the
|
|
30
|
+
* subset's owned files and the test-file pass runs only the subset's own
|
|
31
|
+
* scripts (head.js discovery still spans the queue).
|
|
32
|
+
*/
|
|
33
|
+
export function buildPerRunCheckJs(projectRoot, paths, config, ctx, rootScopePatches) {
|
|
34
|
+
const patchLint = config.patchLint;
|
|
35
|
+
if (!patchLint?.checkJs)
|
|
36
|
+
return undefined;
|
|
37
|
+
const testFilesEnabled = patchLint.checkJsTestFiles === true;
|
|
38
|
+
const ownedByPatch = new Map();
|
|
39
|
+
for (const entry of ctx.entries) {
|
|
40
|
+
const owned = new Set();
|
|
41
|
+
for (const f of entry.newFiles.keys()) {
|
|
42
|
+
if (f.endsWith('.sys.mjs'))
|
|
43
|
+
owned.add(f);
|
|
44
|
+
else if (testFilesEnabled && isTestScriptFile(f))
|
|
45
|
+
owned.add(f);
|
|
46
|
+
}
|
|
47
|
+
if (owned.size > 0)
|
|
48
|
+
ownedByPatch.set(entry.filename, owned);
|
|
49
|
+
}
|
|
50
|
+
const resolutionSet = new Set();
|
|
51
|
+
for (const files of ownedByPatch.values()) {
|
|
52
|
+
for (const f of files)
|
|
53
|
+
resolutionSet.add(f);
|
|
54
|
+
}
|
|
55
|
+
// The subset's owned files, split by program kind; undefined = full queue.
|
|
56
|
+
let scopedRoots;
|
|
57
|
+
if (rootScopePatches !== undefined) {
|
|
58
|
+
scopedRoots = new Set();
|
|
59
|
+
for (const [patchFilename, files] of ownedByPatch) {
|
|
60
|
+
if (!rootScopePatches.has(patchFilename))
|
|
61
|
+
continue;
|
|
62
|
+
for (const f of files)
|
|
63
|
+
scopedRoots.add(f);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// One build for the whole run: the queue-wide `.sys.mjs` program plus —
|
|
67
|
+
// when `patchLint.checkJsTestFiles` is on (FORGE G5) — one small
|
|
68
|
+
// script-scope program per patch-owned test file, merged by file.
|
|
69
|
+
const buildAll = async () => {
|
|
70
|
+
const sys = await invokePatchLintCheckJsGrouped(paths.engine, resolvePatchOwnedSysMjs(new Set(), ctx), patchLint, projectRoot, scopedRoots);
|
|
71
|
+
if (!testFilesEnabled)
|
|
72
|
+
return sys;
|
|
73
|
+
const tests = await runCheckJsTestFilesGrouped(paths.engine, resolvePatchOwnedTestScripts(new Set(), ctx), patchLint, projectRoot, scopedRoots);
|
|
74
|
+
const byFile = new Map(sys.byFile);
|
|
75
|
+
for (const [rel, list] of tests.byFile) {
|
|
76
|
+
byFile.set(rel, [...(byFile.get(rel) ?? []), ...list]);
|
|
77
|
+
}
|
|
78
|
+
return { byFile, global: [...sys.global, ...tests.global] };
|
|
79
|
+
};
|
|
80
|
+
// Memoise the *promise*, not the resolved value: under the bounded pool
|
|
81
|
+
// several patches can reach `getGrouped` before the first build resolves, and
|
|
82
|
+
// `??=` on the promise (a synchronous expression) guarantees a single build.
|
|
83
|
+
let groupedPromise;
|
|
84
|
+
const getGrouped = () => (groupedPromise ??= buildAll());
|
|
85
|
+
return {
|
|
86
|
+
ownedByPatch,
|
|
87
|
+
resolutionSet,
|
|
88
|
+
getGrouped,
|
|
89
|
+
getGlobal: async () => groupedPromise !== undefined
|
|
90
|
+
? (await groupedPromise).global
|
|
91
|
+
: probeCheckJsGlobalIssues(patchLint, projectRoot),
|
|
92
|
+
sliceFor: async (files) => {
|
|
93
|
+
const grouped = await getGrouped();
|
|
94
|
+
const issues = [];
|
|
95
|
+
for (const rel of files) {
|
|
96
|
+
issues.push(...(grouped.byFile.get(rel) ?? []));
|
|
97
|
+
}
|
|
98
|
+
return issues;
|
|
99
|
+
},
|
|
100
|
+
isRelevant: (file) => file.endsWith('.sys.mjs') || (testFilesEnabled && isTestScriptFile(file)),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Structural view of one per-patch lint outcome. Mirrors the relevant
|
|
4
|
+
* fields of lint-per-patch's QueuedPatchResult without importing it —
|
|
5
|
+
* that module imports this one, and a type back-edge would trip the
|
|
6
|
+
* dpdm cycle gate.
|
|
7
|
+
*/
|
|
8
|
+
export interface PerPatchReportResult {
|
|
9
|
+
status: 'skipped' | 'cached' | 'linted';
|
|
10
|
+
existingFiles: string[];
|
|
11
|
+
rawIssues: PatchLintIssue[];
|
|
12
|
+
suppressedIssues: PatchLintIssue[];
|
|
13
|
+
lineCount: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Writes the per-patch lint report JSON. Called after the per-patch
|
|
17
|
+
* results are assembled in patch order; queue-level and policy findings
|
|
18
|
+
* are patch-attributed by their `file`/`filename` fields upstream and
|
|
19
|
+
* are NOT duplicated here — the report is the per-patch view.
|
|
20
|
+
*/
|
|
21
|
+
export declare function writePerPatchLintReport(reportPath: string, subset: readonly PatchMetadata[], results: readonly PerPatchReportResult[]): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Machine-readable per-patch lint report (`lint --per-patch --report
|
|
4
|
+
* <path>`, FORGE G9 + G10). One mechanism serves both observability
|
|
5
|
+
* gaps: consumers get each patch's size metrics against the SAME
|
|
6
|
+
* thresholds the size rules fire on (no more mirroring
|
|
7
|
+
* `countNonBinaryDiffLines`), and lintIgnore-suppressed issues stay
|
|
8
|
+
* visible with their measurements instead of vanishing.
|
|
9
|
+
*
|
|
10
|
+
* Helper module consumed by lint-per-patch.ts; no registrar is exported
|
|
11
|
+
* and none is wanted.
|
|
12
|
+
*/
|
|
13
|
+
import { getPatchSizeThresholds, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
14
|
+
import { writeJson } from '../utils/fs.js';
|
|
15
|
+
import { info } from '../utils/logger.js';
|
|
16
|
+
const LINT_REPORT_SCHEMA_VERSION = 1;
|
|
17
|
+
/**
|
|
18
|
+
* Writes the per-patch lint report JSON. Called after the per-patch
|
|
19
|
+
* results are assembled in patch order; queue-level and policy findings
|
|
20
|
+
* are patch-attributed by their `file`/`filename` fields upstream and
|
|
21
|
+
* are NOT duplicated here — the report is the per-patch view.
|
|
22
|
+
*/
|
|
23
|
+
export async function writePerPatchLintReport(reportPath, subset, results) {
|
|
24
|
+
const patches = [];
|
|
25
|
+
let errors = 0;
|
|
26
|
+
let warnings = 0;
|
|
27
|
+
let suppressed = 0;
|
|
28
|
+
let linted = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
for (let i = 0; i < subset.length; i++) {
|
|
31
|
+
const patch = subset[i];
|
|
32
|
+
const result = results[i];
|
|
33
|
+
if (!patch || !result)
|
|
34
|
+
continue;
|
|
35
|
+
const tier = resolvePatchSizeTier(result.existingFiles, patch.tier);
|
|
36
|
+
patches.push({
|
|
37
|
+
filename: patch.filename,
|
|
38
|
+
name: patch.name,
|
|
39
|
+
status: result.status,
|
|
40
|
+
lineCount: result.lineCount,
|
|
41
|
+
filesAffected: patch.filesAffected.length,
|
|
42
|
+
tier,
|
|
43
|
+
thresholds: getPatchSizeThresholds(tier.tier),
|
|
44
|
+
issues: result.rawIssues,
|
|
45
|
+
suppressedIssues: result.suppressedIssues,
|
|
46
|
+
});
|
|
47
|
+
if (result.status === 'skipped')
|
|
48
|
+
skipped++;
|
|
49
|
+
else
|
|
50
|
+
linted++;
|
|
51
|
+
errors += result.rawIssues.filter((issue) => issue.severity === 'error').length;
|
|
52
|
+
warnings += result.rawIssues.filter((issue) => issue.severity === 'warning').length;
|
|
53
|
+
suppressed += result.suppressedIssues.length;
|
|
54
|
+
}
|
|
55
|
+
await writeJson(reportPath, {
|
|
56
|
+
schemaVersion: LINT_REPORT_SCHEMA_VERSION,
|
|
57
|
+
generatedAt: new Date().toISOString(),
|
|
58
|
+
patches,
|
|
59
|
+
totals: { linted, skipped, errors, warnings, suppressed },
|
|
60
|
+
});
|
|
61
|
+
info(`Per-patch lint report written to ${reportPath}.`);
|
|
62
|
+
}
|
|
@@ -10,7 +10,7 @@ import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
|
10
10
|
import { expandUntrackedDirectoryEntries, getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, getWorkingTreeStatus, } from '../core/git-status.js';
|
|
11
11
|
import { clearPerPatchLintCache } from '../core/lint-cache.js';
|
|
12
12
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
13
|
-
import { buildPatchQueueContext, countNonBinaryDiffLines, lintExportedPatch, lintPatchQueue, lintPatchSize, } from '../core/patch-lint.js';
|
|
13
|
+
import { buildPatchQueueContext, countNonBinaryDiffLines, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, lintPatchSize, } from '../core/patch-lint.js';
|
|
14
14
|
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
15
15
|
import { GeneralError } from '../errors/base.js';
|
|
16
16
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -217,11 +217,12 @@ export function applyAggregateLintIgnoreSuppression(issues, ctx) {
|
|
|
217
217
|
}
|
|
218
218
|
/**
|
|
219
219
|
* Up-front flag validation for `lintCommand`: rejects `--only-introduced`
|
|
220
|
-
* without `--since
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* without `--since` and non-integer `--max-warnings` — each a
|
|
221
|
+
* misconfiguration that should fail loud rather than silently narrow the
|
|
222
|
+
* result. Positional arguments in per-patch mode are patch selectors
|
|
223
|
+
* (FORGE G14) and are validated downstream by `selectPatchSubset`.
|
|
223
224
|
*/
|
|
224
|
-
function validateLintFlags(options
|
|
225
|
+
function validateLintFlags(options) {
|
|
225
226
|
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
226
227
|
// without a revision to anchor the diff there is no "introduced" subset
|
|
227
228
|
// to scope to — reject the combination up-front so a misconfigured CI
|
|
@@ -234,22 +235,16 @@ function validateLintFlags(options, files) {
|
|
|
234
235
|
(!Number.isInteger(options.maxWarnings) || options.maxWarnings < 0)) {
|
|
235
236
|
throw new GeneralError('--max-warnings must be a non-negative integer.');
|
|
236
237
|
}
|
|
237
|
-
// `--per-patch` rescopes the diff from "aggregate engine state" to "each
|
|
238
|
-
// patch's own filesAffected". Mixing in explicit engine file paths would
|
|
239
|
-
// produce an ambiguous set — is the file list an additional filter, or
|
|
240
|
-
// does it replace the per-patch scope? Reject up-front, but point at the
|
|
241
|
-
// first-class subset filter so an operator who wanted to target patches
|
|
242
|
-
// (not engine files) knows the supported syntax.
|
|
243
|
-
if (options.perPatch && files.length > 0) {
|
|
244
|
-
throw new GeneralError('--per-patch cannot be combined with explicit engine file paths. ' +
|
|
245
|
-
'To lint a subset of patches, use `--per-patch --patches <name…>`; ' +
|
|
246
|
-
'to lint specific engine files, drop --per-patch.');
|
|
247
|
-
}
|
|
248
238
|
// `--patches` only means something in per-patch mode (it filters the
|
|
249
239
|
// queue); in aggregate/file-list mode there is no patch loop to narrow.
|
|
250
240
|
if (options.patches !== undefined && !options.perPatch) {
|
|
251
241
|
throw new GeneralError('--patches requires --per-patch.');
|
|
252
242
|
}
|
|
243
|
+
// The JSON report is a per-patch artifact (per-patch size metrics and
|
|
244
|
+
// suppressed issues); aggregate mode has no per-patch rows to report.
|
|
245
|
+
if (options.report !== undefined && !options.perPatch) {
|
|
246
|
+
throw new GeneralError('--report requires --per-patch.');
|
|
247
|
+
}
|
|
253
248
|
}
|
|
254
249
|
/**
|
|
255
250
|
* Aggregate-mode patch-size softening: when the linted diff is every
|
|
@@ -366,13 +361,13 @@ async function reportLintOutcome(engineDir, issues, options) {
|
|
|
366
361
|
const notices = issues.filter((i) => i.severity === 'notice');
|
|
367
362
|
const tagPrefix = (issue) => sinceActive && issue.tag ? `[${issue.tag}] ` : '';
|
|
368
363
|
for (const issue of notices) {
|
|
369
|
-
info(`${tagPrefix(issue)}NOTICE
|
|
364
|
+
info(`${tagPrefix(issue)}NOTICE ${formatPatchLintIssue(issue)}`);
|
|
370
365
|
}
|
|
371
366
|
for (const issue of warnings) {
|
|
372
|
-
warn(`${tagPrefix(issue)}
|
|
367
|
+
warn(`${tagPrefix(issue)}${formatPatchLintIssue(issue)}`);
|
|
373
368
|
}
|
|
374
369
|
for (const issue of errors) {
|
|
375
|
-
warn(`${tagPrefix(issue)}ERROR
|
|
370
|
+
warn(`${tagPrefix(issue)}ERROR ${formatPatchLintIssue(issue)}`);
|
|
376
371
|
}
|
|
377
372
|
if (sinceActive) {
|
|
378
373
|
const introducedErrors = errors.filter((i) => i.tag === 'introduced').length;
|
|
@@ -429,7 +424,7 @@ async function reportLintOutcome(engineDir, issues, options) {
|
|
|
429
424
|
*/
|
|
430
425
|
export async function lintCommand(projectRoot, files, options = {}) {
|
|
431
426
|
intro('FireForge Lint');
|
|
432
|
-
validateLintFlags(options
|
|
427
|
+
validateLintFlags(options);
|
|
433
428
|
const paths = getProjectPaths(projectRoot);
|
|
434
429
|
if (!(await pathExists(paths.engine))) {
|
|
435
430
|
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
@@ -438,7 +433,12 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
438
433
|
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
439
434
|
}
|
|
440
435
|
if (options.perPatch) {
|
|
441
|
-
|
|
436
|
+
// Positional arguments in per-patch mode select PATCHES through the
|
|
437
|
+
// same alias resolution as --patches (FORGE G14) — a non-matching
|
|
438
|
+
// positional fails loud against the queue listing, so an engine path
|
|
439
|
+
// can never silently lint. Explicit --patches entries come first.
|
|
440
|
+
const perPatchOptions = files.length > 0 ? { ...options, patches: [...(options.patches ?? []), ...files] } : options;
|
|
441
|
+
await lintPerPatch(projectRoot, paths, perPatchOptions);
|
|
442
442
|
return;
|
|
443
443
|
}
|
|
444
444
|
// Load the config before resolving the diff so we can pass
|
|
@@ -511,9 +511,10 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
511
511
|
.option('--since <git-rev>', 'Tag issues as [introduced] or [cumulative] based on whether the file changed since <git-rev> (e.g. HEAD, a branch, a SHA)')
|
|
512
512
|
.option('--only-introduced', 'Fail only on issues tagged [introduced] (requires --since). Cumulative errors still print but do not set a non-zero exit.')
|
|
513
513
|
.option('--per-patch', "Lint each patch in the queue as its own isolated diff. Rescopes patch-size rules so they fire against individual patches rather than the aggregate. Honours each patch's `lintIgnore` entries.")
|
|
514
|
-
.option('--patches <names...>', 'With --per-patch, lint only the named patches. Accepts repeated flags, comma lists, full filenames/stems, manifest names, category-prefixed slugs, or bare slugs.')
|
|
514
|
+
.option('--patches <names...>', 'With --per-patch, lint only the named patches. Accepts repeated flags, comma lists, full filenames/stems, manifest names, category-prefixed slugs, or bare slugs. Positional arguments after --per-patch are treated the same way.')
|
|
515
515
|
.option('--max-warnings <n>', 'Fail when lint reports more than <n> warning(s); use 0 for warning-clean release gates.')
|
|
516
516
|
.option('--no-cache', 'Bypass per-patch lint result cache reads and writes.')
|
|
517
|
+
.option('--report <path>', 'With --per-patch, write a machine-readable JSON report (per-patch size metrics, tier, thresholds, issues, and lintIgnore-suppressed issues) to <path>.')
|
|
517
518
|
.action(withErrorHandling(async (paths, options) => {
|
|
518
519
|
const lintOptions = {};
|
|
519
520
|
if (options.since !== undefined) {
|
|
@@ -538,6 +539,9 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
538
539
|
if (options.cache === false) {
|
|
539
540
|
lintOptions.noCache = true;
|
|
540
541
|
}
|
|
542
|
+
if (options.report !== undefined) {
|
|
543
|
+
lintOptions.report = options.report;
|
|
544
|
+
}
|
|
541
545
|
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
542
546
|
}));
|
|
543
547
|
const lintCache = lint
|
|
@@ -11,7 +11,7 @@ import { registerImport } from './import.js';
|
|
|
11
11
|
import { registerLint } from './lint.js';
|
|
12
12
|
import { registerPackage } from './package.js';
|
|
13
13
|
import { registerPatch } from './patch/index.js';
|
|
14
|
-
import { registerReExport } from './re-export.js';
|
|
14
|
+
import { registerReExport } from './re-export-register.js';
|
|
15
15
|
import { registerRebase } from './rebase.js';
|
|
16
16
|
import { registerRegister } from './register.js';
|
|
17
17
|
import { registerReset } from './reset.js';
|
|
@@ -22,6 +22,7 @@ import { registerSource } from './source.js';
|
|
|
22
22
|
import { registerStatus } from './status.js';
|
|
23
23
|
import { registerTest } from './test-register.js';
|
|
24
24
|
import { registerToken } from './token.js';
|
|
25
|
+
import { registerTree } from './tree.js';
|
|
25
26
|
import { registerTypecheck } from './typecheck.js';
|
|
26
27
|
import { registerVerify } from './verify.js';
|
|
27
28
|
import { registerWatch } from './watch.js';
|
|
@@ -50,6 +51,7 @@ export const COMMAND_MANIFEST = [
|
|
|
50
51
|
{ name: 'package', group: 'workflow', register: registerPackage },
|
|
51
52
|
{ name: 'watch', group: 'workflow', register: registerWatch },
|
|
52
53
|
{ name: 'test', group: 'workflow', register: registerTest },
|
|
54
|
+
{ name: 'tree', group: 'workflow', register: registerTree },
|
|
53
55
|
{ name: 'config', group: 'project', register: registerConfig },
|
|
54
56
|
{ name: 'doctor', group: 'diagnostics', register: registerDoctor },
|
|
55
57
|
{ name: 'register', group: 'workflow', register: registerRegister },
|