@hominis/fireforge 0.49.2 → 0.50.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 +40 -1
- package/README.md +1 -1
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build-prelock.d.ts +20 -0
- package/dist/src/commands/build-prelock.js +50 -0
- package/dist/src/commands/build.d.ts +3 -1
- package/dist/src/commands/build.js +13 -6
- package/dist/src/commands/check-report.d.ts +28 -0
- package/dist/src/commands/check-report.js +78 -0
- package/dist/src/commands/check-run.d.ts +28 -0
- package/dist/src/commands/check-run.js +104 -0
- package/dist/src/commands/check-steps.d.ts +67 -0
- package/dist/src/commands/check-steps.js +175 -0
- package/dist/src/commands/check.d.ts +32 -0
- package/dist/src/commands/check.js +160 -0
- package/dist/src/commands/config.js +5 -6
- package/dist/src/commands/doctor-engine-eslint.js +6 -1
- package/dist/src/commands/doctor-source-pin.js +4 -1
- package/dist/src/commands/export-adopt-check.d.ts +38 -0
- package/dist/src/commands/export-adopt-check.js +98 -0
- package/dist/src/commands/export-flow.js +29 -14
- package/dist/src/commands/export-placement-gate.d.ts +6 -2
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-staged-registration.d.ts +46 -0
- package/dist/src/commands/export-staged-registration.js +91 -0
- package/dist/src/commands/forward-import-remedy.d.ts +32 -0
- package/dist/src/commands/forward-import-remedy.js +63 -0
- package/dist/src/commands/furnace/apply.js +3 -3
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
- package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc.js +2 -2
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
- package/dist/src/commands/furnace/deploy-named.js +107 -0
- package/dist/src/commands/furnace/deploy.d.ts +3 -2
- package/dist/src/commands/furnace/deploy.js +50 -86
- package/dist/src/commands/furnace/diff.js +26 -3
- package/dist/src/commands/furnace/index.js +66 -41
- package/dist/src/commands/furnace/init.js +13 -6
- package/dist/src/commands/furnace/override.js +4 -2
- package/dist/src/commands/furnace/preview.js +4 -3
- package/dist/src/commands/furnace/refresh.js +2 -2
- package/dist/src/commands/furnace/remove.js +4 -3
- package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
- package/dist/src/commands/furnace/rename-dry-run.js +127 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
- package/dist/src/commands/furnace/rename-helpers.js +15 -1
- package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
- package/dist/src/commands/furnace/rename-registrations.js +93 -0
- package/dist/src/commands/furnace/rename.d.ts +2 -1
- package/dist/src/commands/furnace/rename.js +28 -66
- package/dist/src/commands/furnace/scan.d.ts +9 -4
- package/dist/src/commands/furnace/scan.js +11 -9
- package/dist/src/commands/furnace/sync.js +3 -2
- package/dist/src/commands/furnace/validate-report.d.ts +45 -0
- package/dist/src/commands/furnace/validate-report.js +91 -0
- package/dist/src/commands/furnace/validate.d.ts +1 -1
- package/dist/src/commands/furnace/validate.js +127 -114
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +31 -60
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +15 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/delete-gap-notice.js +3 -1
- package/dist/src/commands/patch/delete.d.ts +1 -1
- package/dist/src/commands/patch/delete.js +13 -98
- package/dist/src/commands/patch/index.d.ts +1 -1
- package/dist/src/commands/patch/index.js +4 -2
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +65 -28
- package/dist/src/commands/patch/move-files-create.js +26 -17
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
- package/dist/src/commands/patch/removal-dependents.js +133 -0
- package/dist/src/commands/patch/rename.js +7 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +128 -0
- package/dist/src/commands/patch/split-plan.d.ts +7 -7
- package/dist/src/commands/patch/split-plan.js +45 -28
- package/dist/src/commands/patch/split-staged.d.ts +108 -0
- package/dist/src/commands/patch/split-staged.js +202 -0
- package/dist/src/commands/patch/split.d.ts +5 -0
- package/dist/src/commands/patch/split.js +51 -15
- package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
- package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
- package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
- package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
- package/dist/src/commands/patch/staged-dependency-from.js +108 -0
- package/dist/src/commands/patch/staged-dependency.js +14 -41
- package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
- package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
- package/dist/src/commands/re-export-adjacent.d.ts +45 -2
- package/dist/src/commands/re-export-adjacent.js +156 -37
- package/dist/src/commands/re-export-drift.d.ts +36 -8
- package/dist/src/commands/re-export-drift.js +73 -49
- package/dist/src/commands/re-export-files.js +11 -8
- package/dist/src/commands/re-export-lint.d.ts +2 -1
- package/dist/src/commands/re-export-lint.js +23 -10
- package/dist/src/commands/re-export-options.js +45 -4
- package/dist/src/commands/re-export-preflight.d.ts +23 -0
- package/dist/src/commands/re-export-preflight.js +160 -0
- package/dist/src/commands/re-export-register.js +42 -17
- package/dist/src/commands/re-export-removed.d.ts +12 -1
- package/dist/src/commands/re-export-removed.js +41 -11
- package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
- package/dist/src/commands/re-export-scan-owner.js +116 -0
- package/dist/src/commands/re-export-scan.d.ts +17 -9
- package/dist/src/commands/re-export-scan.js +56 -23
- package/dist/src/commands/re-export-single.js +22 -6
- package/dist/src/commands/re-export-summary.d.ts +28 -0
- package/dist/src/commands/re-export-summary.js +36 -0
- package/dist/src/commands/re-export.js +37 -24
- package/dist/src/commands/rebase/index.js +17 -17
- package/dist/src/commands/run.js +16 -2
- package/dist/src/commands/status-check.d.ts +55 -16
- package/dist/src/commands/status-check.js +92 -37
- package/dist/src/commands/status-json.d.ts +22 -9
- package/dist/src/commands/status-json.js +47 -32
- package/dist/src/commands/status-output.js +3 -1
- package/dist/src/commands/status-scope.d.ts +37 -0
- package/dist/src/commands/status-scope.js +54 -0
- package/dist/src/commands/status.js +66 -23
- package/dist/src/commands/test-browser-preflight.js +19 -3
- package/dist/src/commands/test-build-furnace.d.ts +21 -0
- package/dist/src/commands/test-build-furnace.js +48 -0
- package/dist/src/commands/test-build-phase.js +47 -41
- package/dist/src/commands/test-checkjs.js +7 -0
- package/dist/src/commands/test-diagnose.js +7 -0
- package/dist/src/commands/test-modes.d.ts +16 -1
- package/dist/src/commands/test-modes.js +24 -1
- package/dist/src/commands/test-multi-verdict.d.ts +61 -0
- package/dist/src/commands/test-multi-verdict.js +111 -0
- package/dist/src/commands/test-orchestrate.d.ts +43 -0
- package/dist/src/commands/test-orchestrate.js +222 -0
- package/dist/src/commands/test-perf-scope.d.ts +29 -0
- package/dist/src/commands/test-perf-scope.js +83 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +177 -0
- package/dist/src/commands/test-register.js +54 -10
- package/dist/src/commands/test-run.js +4 -1
- package/dist/src/commands/test-stale-gate.js +57 -27
- package/dist/src/commands/test-verdict.d.ts +10 -1
- package/dist/src/commands/test-verdict.js +28 -1
- package/dist/src/commands/test.d.ts +6 -1
- package/dist/src/commands/test.js +66 -60
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +111 -0
- package/dist/src/commands/token.js +10 -0
- package/dist/src/commands/verify-scope.d.ts +28 -0
- package/dist/src/commands/verify-scope.js +67 -0
- package/dist/src/commands/verify.d.ts +10 -2
- package/dist/src/commands/verify.js +59 -35
- package/dist/src/core/build-baseline-types.d.ts +46 -0
- package/dist/src/core/build-baseline-validate.js +19 -0
- package/dist/src/core/build-baseline.d.ts +5 -1
- package/dist/src/core/build-baseline.js +3 -1
- package/dist/src/core/build-overwrite-guard.d.ts +50 -0
- package/dist/src/core/build-overwrite-guard.js +84 -4
- package/dist/src/core/build-prepare.d.ts +7 -0
- package/dist/src/core/build-prepare.js +14 -8
- package/dist/src/core/build-run-record.d.ts +74 -0
- package/dist/src/core/build-run-record.js +136 -0
- package/dist/src/core/checkjs-support-heads.d.ts +41 -0
- package/dist/src/core/checkjs-support-heads.js +104 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +8 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +143 -0
- package/dist/src/core/config-validate-file-size.d.ts +9 -6
- package/dist/src/core/config-validate-file-size.js +91 -2
- package/dist/src/core/config-validate-history.d.ts +11 -0
- package/dist/src/core/config-validate-history.js +38 -0
- package/dist/src/core/config-validate-patch-policy.js +100 -0
- package/dist/src/core/config-validate-test-toolchains.js +7 -0
- package/dist/src/core/config-validate-token-scales.d.ts +6 -0
- package/dist/src/core/config-validate-token-scales.js +50 -0
- package/dist/src/core/config-validate.js +14 -5
- package/dist/src/core/config.d.ts +5 -0
- package/dist/src/core/config.js +16 -0
- package/dist/src/core/coverage-dispatch.d.ts +24 -0
- package/dist/src/core/coverage-dispatch.js +44 -0
- package/dist/src/core/coverage-extend.d.ts +70 -45
- package/dist/src/core/coverage-extend.js +199 -144
- package/dist/src/core/coverage-records.d.ts +97 -0
- package/dist/src/core/coverage-records.js +350 -0
- package/dist/src/core/destructive.d.ts +7 -4
- package/dist/src/core/destructive.js +10 -10
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -8
- package/dist/src/core/export-moved-code-shape.d.ts +49 -11
- package/dist/src/core/export-moved-code-shape.js +135 -44
- package/dist/src/core/file-lock-queue.d.ts +67 -0
- package/dist/src/core/file-lock-queue.js +221 -0
- package/dist/src/core/file-lock.d.ts +4 -25
- package/dist/src/core/file-lock.js +105 -138
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +31 -1
- package/dist/src/core/furnace-apply.js +12 -2
- package/dist/src/core/furnace-composed-source.d.ts +76 -0
- package/dist/src/core/furnace-composed-source.js +167 -0
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
- package/dist/src/core/furnace-config-validate.js +8 -0
- package/dist/src/core/furnace-css-fragments.d.ts +10 -0
- package/dist/src/core/furnace-css-fragments.js +26 -0
- package/dist/src/core/furnace-operation.d.ts +3 -3
- package/dist/src/core/furnace-registration-remove.d.ts +22 -1
- package/dist/src/core/furnace-registration-remove.js +53 -17
- package/dist/src/core/furnace-registration-rename.d.ts +26 -0
- package/dist/src/core/furnace-registration-rename.js +77 -0
- package/dist/src/core/furnace-registration.d.ts +14 -4
- package/dist/src/core/furnace-registration.js +44 -10
- package/dist/src/core/furnace-staleness.d.ts +7 -0
- package/dist/src/core/furnace-staleness.js +34 -24
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +10 -1
- package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
- package/dist/src/core/furnace-validate-exceptions.js +43 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +49 -0
- package/dist/src/core/furnace-validate.js +1 -1
- package/dist/src/core/harness-port-fallback.d.ts +60 -0
- package/dist/src/core/harness-port-fallback.js +96 -0
- package/dist/src/core/history-log-file.d.ts +61 -0
- package/dist/src/core/history-log-file.js +157 -0
- package/dist/src/core/history-log.d.ts +2 -1
- package/dist/src/core/history-log.js +2 -1
- package/dist/src/core/json-preserving-edit.d.ts +22 -0
- package/dist/src/core/json-preserving-edit.js +199 -0
- package/dist/src/core/mach.js +8 -1
- package/dist/src/core/negative-control-diff.d.ts +39 -0
- package/dist/src/core/negative-control-diff.js +136 -0
- package/dist/src/core/negative-control.d.ts +138 -0
- package/dist/src/core/negative-control.js +356 -0
- package/dist/src/core/objdir-install-manifests.d.ts +73 -0
- package/dist/src/core/objdir-install-manifests.js +226 -0
- package/dist/src/core/patch-export-update.d.ts +2 -0
- package/dist/src/core/patch-export-update.js +1 -0
- package/dist/src/core/patch-export.js +3 -4
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -11
- package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
- package/dist/src/core/patch-lint-code-lines.js +105 -0
- package/dist/src/core/patch-lint-cross.js +12 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +10 -4
- package/dist/src/core/patch-lint-file-size.d.ts +12 -5
- package/dist/src/core/patch-lint-file-size.js +22 -11
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +152 -0
- package/dist/src/core/patch-lint-ignore.js +299 -0
- package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
- package/dist/src/core/patch-lint-size-thresholds.js +110 -0
- package/dist/src/core/patch-lint-size.d.ts +117 -0
- package/dist/src/core/patch-lint-size.js +323 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
- package/dist/src/core/patch-lint-staged-registration.js +20 -0
- package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
- package/dist/src/core/patch-lint-token-scale.js +109 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +64 -286
- package/dist/src/core/patch-lock.js +7 -6
- package/dist/src/core/patch-manifest-consistency.js +7 -6
- package/dist/src/core/patch-manifest-io.d.ts +6 -3
- package/dist/src/core/patch-manifest-io.js +19 -8
- package/dist/src/core/patch-manifest-validate.js +34 -2
- package/dist/src/core/patch-order-legal.d.ts +25 -0
- package/dist/src/core/patch-order-legal.js +83 -0
- package/dist/src/core/patch-policy-content.d.ts +40 -0
- package/dist/src/core/patch-policy-content.js +216 -0
- package/dist/src/core/patch-policy.d.ts +16 -3
- package/dist/src/core/patch-policy.js +16 -9
- package/dist/src/core/patch-retired.d.ts +35 -0
- package/dist/src/core/patch-retired.js +54 -0
- package/dist/src/core/patch-subset.d.ts +16 -0
- package/dist/src/core/patch-subset.js +66 -0
- package/dist/src/core/preflight-wait.d.ts +20 -0
- package/dist/src/core/preflight-wait.js +38 -0
- package/dist/src/core/rebase-from-version.d.ts +46 -0
- package/dist/src/core/rebase-from-version.js +71 -0
- package/dist/src/core/run-log.d.ts +20 -0
- package/dist/src/core/run-log.js +89 -3
- package/dist/src/core/run-profile.d.ts +7 -0
- package/dist/src/core/run-profile.js +7 -1
- package/dist/src/core/smoke-patterns.d.ts +5 -0
- package/dist/src/core/smoke-patterns.js +13 -0
- package/dist/src/core/test-harness-crash.d.ts +18 -1
- package/dist/src/core/test-harness-crash.js +6 -4
- package/dist/src/core/test-harness-signature.d.ts +17 -0
- package/dist/src/core/test-harness-signature.js +4 -0
- package/dist/src/core/test-run-completeness.d.ts +57 -0
- package/dist/src/core/test-run-completeness.js +144 -0
- package/dist/src/core/test-stale-check.d.ts +27 -1
- package/dist/src/core/test-stale-check.js +39 -49
- package/dist/src/core/test-stale-exempt.d.ts +40 -0
- package/dist/src/core/test-stale-exempt.js +70 -0
- package/dist/src/core/token-lint.d.ts +50 -0
- package/dist/src/core/token-lint.js +126 -0
- package/dist/src/core/tree-guard.js +3 -0
- package/dist/src/core/typecheck-shim.d.ts +1 -1
- package/dist/src/core/typecheck-shim.js +90 -2
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +3 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +148 -21
- package/dist/src/types/commands/patches.d.ts +75 -1
- package/dist/src/types/config.d.ts +192 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +43 -1
- package/dist/src/types/furnace.js +7 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +34 -6
- package/dist/src/utils/options.js +86 -5
- package/dist/src/utils/path-glob.d.ts +15 -0
- package/dist/src/utils/path-glob.js +47 -0
- package/dist/src/utils/regex.d.ts +15 -3
- package/dist/src/utils/regex.js +37 -16
- package/docs/configuration.md +282 -9
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +97 -3
- package/docs/lifecycle-invariants.md +52 -19
- package/docs/machine-output.md +74 -12
- package/docs/patch-workflow.md +297 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +276 -20
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ import { warn } from '../utils/logger.js';
|
|
|
20
20
|
import { isArray, isObject } from '../utils/validation.js';
|
|
21
21
|
import { assertPatchDirectoryLockHeld } from './patch-lock.js';
|
|
22
22
|
import { validatePatchesManifest } from './patch-manifest-validate.js';
|
|
23
|
+
import { manifestWithRetiredSlots } from './patch-retired.js';
|
|
23
24
|
/** Filename for the patches manifest */
|
|
24
25
|
export const PATCHES_MANIFEST = 'patches.json';
|
|
25
26
|
/**
|
|
@@ -171,7 +172,8 @@ export async function addPatchToManifest(patchesDir, metadata, removeFilenames)
|
|
|
171
172
|
// Add new entry and sort by order
|
|
172
173
|
manifest.patches.push(metadata);
|
|
173
174
|
manifest.patches.sort((a, b) => a.order - b.order);
|
|
174
|
-
|
|
175
|
+
// A patch landing on a retired order fills that slot.
|
|
176
|
+
await savePatchesManifest(patchesDir, manifestWithRetiredSlots(manifest, manifest.patches));
|
|
175
177
|
}
|
|
176
178
|
/**
|
|
177
179
|
* Removes a single patch entry from the manifest by filename. Leaves the
|
|
@@ -184,10 +186,11 @@ export async function addPatchToManifest(patchesDir, metadata, removeFilenames)
|
|
|
184
186
|
*
|
|
185
187
|
* @param patchesDir - Path to the patches directory
|
|
186
188
|
* @param filename - Filename of the patch to remove from the manifest
|
|
189
|
+
* @param retiredSlot - Optional placeholder to record for the removed order
|
|
187
190
|
* @returns True when the manifest was written (i.e. an entry was removed),
|
|
188
191
|
* false when no matching entry existed
|
|
189
192
|
*/
|
|
190
|
-
export async function removePatchFromManifest(patchesDir, filename) {
|
|
193
|
+
export async function removePatchFromManifest(patchesDir, filename, retiredSlot) {
|
|
191
194
|
const manifest = await loadPatchesManifestForWrite(patchesDir);
|
|
192
195
|
if (!manifest)
|
|
193
196
|
return false;
|
|
@@ -196,6 +199,14 @@ export async function removePatchFromManifest(patchesDir, filename) {
|
|
|
196
199
|
if (manifest.patches.length === originalLength) {
|
|
197
200
|
return false;
|
|
198
201
|
}
|
|
202
|
+
if (retiredSlot !== undefined) {
|
|
203
|
+
// `patch retire`: the row leaves and its order stays spent, in the
|
|
204
|
+
// same write, so no reader ever sees the gap in between.
|
|
205
|
+
manifest.retired = [
|
|
206
|
+
...(manifest.retired ?? []).filter((slot) => slot.order !== retiredSlot.order),
|
|
207
|
+
retiredSlot,
|
|
208
|
+
].sort((a, b) => a.order - b.order);
|
|
209
|
+
}
|
|
199
210
|
await savePatchesManifest(patchesDir, manifest);
|
|
200
211
|
return true;
|
|
201
212
|
}
|
|
@@ -436,10 +447,8 @@ export async function renumberPatchesInManifest(patchesDir, renameMap) {
|
|
|
436
447
|
// try, so a violation unwinds through the Phase 3 rollback that puts the
|
|
437
448
|
// filenames back.
|
|
438
449
|
assertRenumberedOrderIsWellFormed(updatedPatches);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
patches: updatedPatches,
|
|
442
|
-
});
|
|
450
|
+
// A patch renumbered onto a retired order fills that slot.
|
|
451
|
+
await savePatchesManifest(patchesDir, manifestWithRetiredSlots(manifest, updatedPatches));
|
|
443
452
|
}
|
|
444
453
|
catch (error) {
|
|
445
454
|
// Phase 3 rollback: reverse every completed rename. The manifest
|
|
@@ -504,14 +513,16 @@ export class PatchDeleteRollbackError extends FireForgeError {
|
|
|
504
513
|
*
|
|
505
514
|
* @param patchesDir - Path to the patches directory
|
|
506
515
|
* @param filename - Patch filename to delete
|
|
516
|
+
* @param retiredSlot - When set (`patch retire`), the placeholder written in
|
|
517
|
+
* the same manifest update that removes the row
|
|
507
518
|
*/
|
|
508
|
-
export async function removePatchFileAndManifest(patchesDir, filename) {
|
|
519
|
+
export async function removePatchFileAndManifest(patchesDir, filename, retiredSlot) {
|
|
509
520
|
await assertPatchDirectoryLockHeld(patchesDir, 'deleting a patch and its manifest row');
|
|
510
521
|
const patchPath = join(patchesDir, filename);
|
|
511
522
|
// ForWrite: deleting the patch file while a corrupt manifest still
|
|
512
523
|
// references it would strand the queue. Abort on corruption instead.
|
|
513
524
|
const originalManifest = await loadPatchesManifestForWrite(patchesDir);
|
|
514
|
-
const removedFromManifest = await removePatchFromManifest(patchesDir, filename);
|
|
525
|
+
const removedFromManifest = await removePatchFromManifest(patchesDir, filename, retiredSlot);
|
|
515
526
|
try {
|
|
516
527
|
if (await pathExistsStrict(patchPath)) {
|
|
517
528
|
await removeFile(patchPath);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { parseObject } from '../utils/parse.js';
|
|
6
6
|
import { isArray, isObject, isValidFirefoxProduct, isValidFirefoxVersion, PATCH_CATEGORIES, } from '../utils/validation.js';
|
|
7
|
+
import { parseLintIgnoreEntries } from './patch-lint-ignore.js';
|
|
7
8
|
import { checkDocumentVersion, describeNewerDocument } from './state-file.js';
|
|
8
9
|
function parseForwardImports(data, label) {
|
|
9
10
|
if (!isArray(data)) {
|
|
@@ -99,7 +100,11 @@ function validatePatchMetadata(data, index) {
|
|
|
99
100
|
// `lintIgnore` to suppress a rule is quietly re-tripped the next time the
|
|
100
101
|
// manifest validates. Preserve both `lintIgnore` and `tier` here. Future
|
|
101
102
|
// optional fields follow the same template.
|
|
102
|
-
|
|
103
|
+
// Entries are bare check IDs or `{ rule, max, reason }` ceilings.
|
|
104
|
+
const rawLintIgnore = rec.raw('lintIgnore');
|
|
105
|
+
const lintIgnore = rawLintIgnore === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: parseLintIgnoreEntries(rawLintIgnore, `patches[${index}].lintIgnore`);
|
|
103
108
|
const rawTier = rec.raw('tier');
|
|
104
109
|
let tier;
|
|
105
110
|
if (rawTier !== undefined) {
|
|
@@ -131,6 +136,30 @@ function validatePatchMetadata(data, index) {
|
|
|
131
136
|
}
|
|
132
137
|
return result;
|
|
133
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Validates the `retired` placeholders `patch retire` leaves behind. Kept on
|
|
141
|
+
* the manifest so every write that round-trips a loaded manifest carries
|
|
142
|
+
* them; dropping them would silently reopen the gaps they were retired to
|
|
143
|
+
* close.
|
|
144
|
+
*/
|
|
145
|
+
function parseRetiredSlots(data) {
|
|
146
|
+
if (!isArray(data)) {
|
|
147
|
+
throw new Error('patches.json field "retired" must be an array');
|
|
148
|
+
}
|
|
149
|
+
return data.map((entry, index) => {
|
|
150
|
+
const rec = parseObject(entry, `retired[${index}]`);
|
|
151
|
+
const slot = {
|
|
152
|
+
order: rec.nonNegativeInteger('order'),
|
|
153
|
+
category: rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)'),
|
|
154
|
+
filename: rec.string('filename'),
|
|
155
|
+
retiredAt: rec.string('retiredAt'),
|
|
156
|
+
};
|
|
157
|
+
const reason = rec.optionalString('reason');
|
|
158
|
+
if (reason !== undefined)
|
|
159
|
+
slot.reason = reason;
|
|
160
|
+
return slot;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
134
163
|
/** Manifest schema version this build writes and understands. */
|
|
135
164
|
const PATCHES_MANIFEST_VERSION = 1;
|
|
136
165
|
/**
|
|
@@ -156,10 +185,13 @@ export function validatePatchesManifest(data) {
|
|
|
156
185
|
if (!isArray(data['patches'])) {
|
|
157
186
|
throw new Error('patches.json field "patches" must be an array');
|
|
158
187
|
}
|
|
159
|
-
|
|
188
|
+
const manifest = {
|
|
160
189
|
version: 1,
|
|
161
190
|
patches: data['patches'].map((patch, index) => validatePatchMetadata(patch, index)),
|
|
162
191
|
};
|
|
192
|
+
if (data['retired'] !== undefined)
|
|
193
|
+
manifest.retired = parseRetiredSlots(data['retired']);
|
|
194
|
+
return manifest;
|
|
163
195
|
}
|
|
164
196
|
/**
|
|
165
197
|
* Infers patch metadata from a filename pattern.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The earliest order a new patch can take after a given order, under the
|
|
3
|
+
* queue's occupancy and `patchPolicy`.
|
|
4
|
+
*
|
|
5
|
+
* A refusal that says "export it as its own later patch" leaves the
|
|
6
|
+
* operator to find a number that is free, inside the category's range, and
|
|
7
|
+
* (with `allowGaps: false`) does not open a gap. Each wrong guess costs a
|
|
8
|
+
* full export attempt. This answers it once, with the same policy
|
|
9
|
+
* evaluation the export itself will run.
|
|
10
|
+
*/
|
|
11
|
+
import type { PatchesManifest } from '../types/commands/index.js';
|
|
12
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
13
|
+
/**
|
|
14
|
+
* Smallest order strictly greater than `after` that no patch occupies and
|
|
15
|
+
* that adds no order-related `patchPolicy` finding (collision, category
|
|
16
|
+
* range, reserved range, numeric gap) for a `category` patch. A retired
|
|
17
|
+
* slot counts as free, since a patch may fill it. Returns null when no such
|
|
18
|
+
* order exists inside the category's configured ranges.
|
|
19
|
+
*
|
|
20
|
+
* @param config - Project config (only `patchPolicy` is read)
|
|
21
|
+
* @param manifest - Current manifest
|
|
22
|
+
* @param category - Category the new patch would carry
|
|
23
|
+
* @param after - The order the new patch must come after
|
|
24
|
+
*/
|
|
25
|
+
export declare function earliestLegalOrderAfter(config: Pick<FireForgeConfig, 'patchPolicy'>, manifest: PatchesManifest, category: string, after: number): number | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The earliest order a new patch can take after a given order, under the
|
|
4
|
+
* queue's occupancy and `patchPolicy`.
|
|
5
|
+
*
|
|
6
|
+
* A refusal that says "export it as its own later patch" leaves the
|
|
7
|
+
* operator to find a number that is free, inside the category's range, and
|
|
8
|
+
* (with `allowGaps: false`) does not open a gap. Each wrong guess costs a
|
|
9
|
+
* full export attempt. This answers it once, with the same policy
|
|
10
|
+
* evaluation the export itself will run.
|
|
11
|
+
*/
|
|
12
|
+
import { formatPatchOrder } from './patch-parse.js';
|
|
13
|
+
import { evaluatePatchPolicy } from './patch-policy.js';
|
|
14
|
+
/** Policy codes an order choice alone can trip. */
|
|
15
|
+
const ORDER_CODES = new Set([
|
|
16
|
+
'order-collision',
|
|
17
|
+
'category-range',
|
|
18
|
+
'reserved-range',
|
|
19
|
+
'numeric-gap',
|
|
20
|
+
]);
|
|
21
|
+
/** Upper bound on the search when no policy range bounds it. */
|
|
22
|
+
const UNBOUNDED_SEARCH = 1000;
|
|
23
|
+
const GAP_ORDERS_PATTERN = /numeric gap\(s\): (?<orders>[^.]+)\./;
|
|
24
|
+
/**
|
|
25
|
+
* Order-related findings as comparable keys. A `numeric-gap` finding
|
|
26
|
+
* becomes one key per missing order, so filling one gap of several reads as
|
|
27
|
+
* removing a key rather than as a new, differently worded finding.
|
|
28
|
+
*/
|
|
29
|
+
function orderIssueKeys(config, manifest) {
|
|
30
|
+
const keys = new Set();
|
|
31
|
+
for (const issue of evaluatePatchPolicy(config, manifest)) {
|
|
32
|
+
if (!ORDER_CODES.has(issue.code))
|
|
33
|
+
continue;
|
|
34
|
+
const gaps = issue.code === 'numeric-gap' ? GAP_ORDERS_PATTERN.exec(issue.message) : null;
|
|
35
|
+
if (gaps?.groups?.['orders'] === undefined) {
|
|
36
|
+
keys.add(`${issue.code}|${issue.message}`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const order of gaps.groups['orders'].split(',')) {
|
|
40
|
+
keys.add(`numeric-gap|${issue.filename}|${order.trim()}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return keys;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Smallest order strictly greater than `after` that no patch occupies and
|
|
47
|
+
* that adds no order-related `patchPolicy` finding (collision, category
|
|
48
|
+
* range, reserved range, numeric gap) for a `category` patch. A retired
|
|
49
|
+
* slot counts as free, since a patch may fill it. Returns null when no such
|
|
50
|
+
* order exists inside the category's configured ranges.
|
|
51
|
+
*
|
|
52
|
+
* @param config - Project config (only `patchPolicy` is read)
|
|
53
|
+
* @param manifest - Current manifest
|
|
54
|
+
* @param category - Category the new patch would carry
|
|
55
|
+
* @param after - The order the new patch must come after
|
|
56
|
+
*/
|
|
57
|
+
export function earliestLegalOrderAfter(config, manifest, category, after) {
|
|
58
|
+
const occupied = new Set(manifest.patches.map((patch) => patch.order));
|
|
59
|
+
const ranges = config.patchPolicy?.ranges.filter((range) => range.category === category) ?? [];
|
|
60
|
+
const limit = ranges.length > 0 ? Math.max(...ranges.map((range) => range.to)) : after + UNBOUNDED_SEARCH;
|
|
61
|
+
const baseline = config.patchPolicy ? orderIssueKeys(config, manifest) : new Set();
|
|
62
|
+
for (let order = after + 1; order <= limit; order++) {
|
|
63
|
+
if (occupied.has(order))
|
|
64
|
+
continue;
|
|
65
|
+
if (!config.patchPolicy)
|
|
66
|
+
return order;
|
|
67
|
+
const probe = {
|
|
68
|
+
filename: `${formatPatchOrder(order)}-${category}-probe.patch`,
|
|
69
|
+
order,
|
|
70
|
+
category,
|
|
71
|
+
name: 'probe',
|
|
72
|
+
description: 'probe',
|
|
73
|
+
createdAt: '',
|
|
74
|
+
sourceEsrVersion: '',
|
|
75
|
+
filesAffected: [],
|
|
76
|
+
};
|
|
77
|
+
const projected = { ...manifest, patches: [...manifest.patches, probe] };
|
|
78
|
+
const introduced = [...orderIssueKeys(config, projected)].filter((key) => !baseline.has(key));
|
|
79
|
+
if (introduced.length === 0)
|
|
80
|
+
return order;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-only patch policy: the `patchPolicy` rules that need the
|
|
3
|
+
* patch bodies, not just the manifest.
|
|
4
|
+
*
|
|
5
|
+
* - `upstreamAllowlist`: which upstream files (files no patch in the queue
|
|
6
|
+
* creates) a non-reserved patch may modify.
|
|
7
|
+
* - `addOnly` allowlist entries: for shared registration files, a patch may
|
|
8
|
+
* only add lines, and each non-blank added line must carry the marker.
|
|
9
|
+
* - `scopeRules`: limit a class of patch (a category, or a filename pattern
|
|
10
|
+
* such as `-tests\.patch$`) to the files its globs name.
|
|
11
|
+
* - `verifyDocumentPaths`: every reserved allowlist entry's `adr` /
|
|
12
|
+
* `documentation` path must exist.
|
|
13
|
+
*
|
|
14
|
+
* Every rule is opt-in, and none can be evaluated from `patches.json`
|
|
15
|
+
* alone, which is why they live apart from `patch-policy.ts`: that module
|
|
16
|
+
* answers synchronously over a (possibly projected) manifest, this one reads
|
|
17
|
+
* patch bodies and the filesystem. `verify` and `lint --per-patch` run both.
|
|
18
|
+
*/
|
|
19
|
+
import type { PatchesManifest } from '../types/commands/index.js';
|
|
20
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
21
|
+
import { type PatchQueueEntry } from './patch-lint-cross.js';
|
|
22
|
+
import type { PatchPolicyIssue } from './patch-policy.js';
|
|
23
|
+
/** True when the policy sets any rule this module evaluates. */
|
|
24
|
+
export declare function hasPatchPolicyContentRules(config: FireForgeConfig): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Evaluates the body-dependent `patchPolicy` rules over a patch queue.
|
|
27
|
+
*
|
|
28
|
+
* @param config - Project configuration
|
|
29
|
+
* @param projectRoot - Project root, for the document-path check
|
|
30
|
+
* @param manifest - Current manifest (category and order per patch)
|
|
31
|
+
* @param entries - Patch queue entries with their bodies
|
|
32
|
+
* @returns Findings, in the same shape {@link evaluatePatchPolicy} returns
|
|
33
|
+
*/
|
|
34
|
+
export declare function evaluatePatchPolicyContent(config: FireForgeConfig, projectRoot: string, manifest: PatchesManifest, entries: readonly PatchQueueEntry[]): Promise<PatchPolicyIssue[]>;
|
|
35
|
+
/**
|
|
36
|
+
* {@link evaluatePatchPolicyContent} for a caller without a queue context
|
|
37
|
+
* in hand (`verify`): reads the patch bodies only when a body-dependent
|
|
38
|
+
* rule is configured.
|
|
39
|
+
*/
|
|
40
|
+
export declare function evaluatePatchPolicyContentForQueue(config: FireForgeConfig, projectRoot: string, patchesDir: string, manifest: PatchesManifest): Promise<PatchPolicyIssue[]>;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Registration-only patch policy: the `patchPolicy` rules that need the
|
|
4
|
+
* patch bodies, not just the manifest.
|
|
5
|
+
*
|
|
6
|
+
* - `upstreamAllowlist`: which upstream files (files no patch in the queue
|
|
7
|
+
* creates) a non-reserved patch may modify.
|
|
8
|
+
* - `addOnly` allowlist entries: for shared registration files, a patch may
|
|
9
|
+
* only add lines, and each non-blank added line must carry the marker.
|
|
10
|
+
* - `scopeRules`: limit a class of patch (a category, or a filename pattern
|
|
11
|
+
* such as `-tests\.patch$`) to the files its globs name.
|
|
12
|
+
* - `verifyDocumentPaths`: every reserved allowlist entry's `adr` /
|
|
13
|
+
* `documentation` path must exist.
|
|
14
|
+
*
|
|
15
|
+
* Every rule is opt-in, and none can be evaluated from `patches.json`
|
|
16
|
+
* alone, which is why they live apart from `patch-policy.ts`: that module
|
|
17
|
+
* answers synchronously over a (possibly projected) manifest, this one reads
|
|
18
|
+
* patch bodies and the filesystem. `verify` and `lint --per-patch` run both.
|
|
19
|
+
*/
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { pathExists } from '../utils/fs.js';
|
|
22
|
+
import { matchesAnyGlob } from '../utils/path-glob.js';
|
|
23
|
+
import { buildPatchQueueContext } from './patch-lint-cross.js';
|
|
24
|
+
import { parseDiffSections } from './patch-parse.js';
|
|
25
|
+
/** True when the policy sets any rule this module evaluates. */
|
|
26
|
+
export function hasPatchPolicyContentRules(config) {
|
|
27
|
+
const cfg = config.patchPolicy;
|
|
28
|
+
return (cfg !== undefined &&
|
|
29
|
+
(cfg.upstreamAllowlist !== undefined ||
|
|
30
|
+
cfg.scopeRules !== undefined ||
|
|
31
|
+
cfg.verifyDocumentPaths === true));
|
|
32
|
+
}
|
|
33
|
+
function normalizeAllowEntry(entry) {
|
|
34
|
+
return typeof entry === 'string' ? { path: entry } : entry;
|
|
35
|
+
}
|
|
36
|
+
function isReservedOrder(cfg, order) {
|
|
37
|
+
return (cfg.reservedRanges ?? []).some((range) => order >= range.from && order <= range.to);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The upstream files a patch body edits, keyed by path: every section that
|
|
41
|
+
* does not create its file, minus paths some patch in the queue creates. A
|
|
42
|
+
* rename counts against both of its paths.
|
|
43
|
+
*/
|
|
44
|
+
function collectUpstreamEdits(diff, queueCreated) {
|
|
45
|
+
const edits = new Map();
|
|
46
|
+
for (const section of parseDiffSections(diff)) {
|
|
47
|
+
if (section.isNewFile)
|
|
48
|
+
continue;
|
|
49
|
+
const paths = new Set([section.sourcePath, section.targetPath]);
|
|
50
|
+
for (const path of paths) {
|
|
51
|
+
if (queueCreated.has(path))
|
|
52
|
+
continue;
|
|
53
|
+
const edit = edits.get(path) ?? { added: [], removed: [], deleted: false, binary: false };
|
|
54
|
+
edit.deleted ||= section.isDeletedFile;
|
|
55
|
+
edit.binary ||= section.isBinary;
|
|
56
|
+
for (const hunk of section.hunks) {
|
|
57
|
+
for (const line of hunk.lines) {
|
|
58
|
+
if (line.startsWith('+'))
|
|
59
|
+
edit.added.push(line.slice(1));
|
|
60
|
+
else if (line.startsWith('-'))
|
|
61
|
+
edit.removed.push(line.slice(1));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
edits.set(path, edit);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return edits;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Why an add-only file's edit breaks the mode, or undefined when it holds.
|
|
71
|
+
* Blank lines are free in both directions: they carry no registration.
|
|
72
|
+
*/
|
|
73
|
+
function addOnlyViolation(edit, marker) {
|
|
74
|
+
if (edit.deleted)
|
|
75
|
+
return 'deletes the file';
|
|
76
|
+
if (edit.binary)
|
|
77
|
+
return 'carries a binary change';
|
|
78
|
+
const removed = edit.removed.filter((line) => line.trim() !== '');
|
|
79
|
+
if (removed.length > 0) {
|
|
80
|
+
return `removes ${String(removed.length)} line(s), first: ${JSON.stringify(removed[0])}`;
|
|
81
|
+
}
|
|
82
|
+
const needle = marker.toLowerCase();
|
|
83
|
+
const unmarked = edit.added.filter((line) => line.trim() !== '' && !line.toLowerCase().includes(needle));
|
|
84
|
+
if (unmarked.length > 0) {
|
|
85
|
+
return (`adds ${String(unmarked.length)} line(s) without the "${marker}" marker, ` +
|
|
86
|
+
`first: ${JSON.stringify(unmarked[0])}`);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
function evaluateUpstreamEdits(cfg, filename, edits, severity) {
|
|
91
|
+
const allowlist = (cfg.upstreamAllowlist ?? []).map(normalizeAllowEntry);
|
|
92
|
+
const issues = [];
|
|
93
|
+
const outside = [];
|
|
94
|
+
for (const [path, edit] of [...edits].sort(([a], [b]) => a.localeCompare(b))) {
|
|
95
|
+
const matches = allowlist.filter((entry) => matchesAnyGlob(path, [entry.path]));
|
|
96
|
+
if (matches.length === 0) {
|
|
97
|
+
outside.push(path);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Strictest wins: any add-only match makes the file add-only.
|
|
101
|
+
const addOnly = matches.find((entry) => entry.addOnly === true);
|
|
102
|
+
if (!addOnly)
|
|
103
|
+
continue;
|
|
104
|
+
const marker = addOnly.marker ?? cfg.addOnlyMarker ?? '';
|
|
105
|
+
const violation = addOnlyViolation(edit, marker);
|
|
106
|
+
if (violation !== undefined) {
|
|
107
|
+
issues.push({
|
|
108
|
+
code: 'add-only',
|
|
109
|
+
filename,
|
|
110
|
+
severity,
|
|
111
|
+
message: `${filename} edits ${path}, which patchPolicy.upstreamAllowlist marks add-only ` +
|
|
112
|
+
`(added lines carrying "${marker}"), but ${violation}.`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (outside.length > 0) {
|
|
117
|
+
issues.push({
|
|
118
|
+
code: 'upstream-allowlist',
|
|
119
|
+
filename,
|
|
120
|
+
severity,
|
|
121
|
+
message: `${filename} modifies upstream file(s) outside patchPolicy.upstreamAllowlist: ` +
|
|
122
|
+
`${outside.join(', ')}. Move the change into a fork-owned file, or add a reviewed allowlist entry.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return issues;
|
|
126
|
+
}
|
|
127
|
+
function evaluateScopeRules(cfg, filename, category, files, severity) {
|
|
128
|
+
const issues = [];
|
|
129
|
+
for (const [index, rule] of (cfg.scopeRules ?? []).entries()) {
|
|
130
|
+
if (rule.category !== undefined && rule.category !== category)
|
|
131
|
+
continue;
|
|
132
|
+
if (rule.filenamePattern !== undefined && !new RegExp(rule.filenamePattern).test(filename)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const outside = files.filter((file) => !matchesAnyGlob(file, rule.files));
|
|
136
|
+
if (outside.length === 0)
|
|
137
|
+
continue;
|
|
138
|
+
issues.push({
|
|
139
|
+
code: 'scope',
|
|
140
|
+
filename,
|
|
141
|
+
severity,
|
|
142
|
+
message: `${filename} is scoped by patchPolicy.scopeRules[${String(index)}] to ` +
|
|
143
|
+
`${rule.files.join(', ')}, but touches: ${outside.join(', ')}. ` +
|
|
144
|
+
'Move those files to a patch of the class that owns them.',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return issues;
|
|
148
|
+
}
|
|
149
|
+
async function evaluateDocumentPaths(cfg, projectRoot, severity) {
|
|
150
|
+
if (cfg.verifyDocumentPaths !== true)
|
|
151
|
+
return [];
|
|
152
|
+
const issues = [];
|
|
153
|
+
for (const range of cfg.reservedRanges ?? []) {
|
|
154
|
+
for (const allowed of range.allowed) {
|
|
155
|
+
for (const [field, path] of [
|
|
156
|
+
['adr', allowed.adr],
|
|
157
|
+
['documentation', allowed.documentation],
|
|
158
|
+
]) {
|
|
159
|
+
if (path === undefined || (await pathExists(join(projectRoot, path))))
|
|
160
|
+
continue;
|
|
161
|
+
issues.push({
|
|
162
|
+
code: 'reserved-document-missing',
|
|
163
|
+
filename: allowed.filename,
|
|
164
|
+
severity,
|
|
165
|
+
message: `${allowed.filename}'s reserved-range allowlist entry names ${field} "${path}", ` +
|
|
166
|
+
'which does not exist. Restore the document or correct the path.',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return issues;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Evaluates the body-dependent `patchPolicy` rules over a patch queue.
|
|
175
|
+
*
|
|
176
|
+
* @param config - Project configuration
|
|
177
|
+
* @param projectRoot - Project root, for the document-path check
|
|
178
|
+
* @param manifest - Current manifest (category and order per patch)
|
|
179
|
+
* @param entries - Patch queue entries with their bodies
|
|
180
|
+
* @returns Findings, in the same shape {@link evaluatePatchPolicy} returns
|
|
181
|
+
*/
|
|
182
|
+
export async function evaluatePatchPolicyContent(config, projectRoot, manifest, entries) {
|
|
183
|
+
const cfg = config.patchPolicy;
|
|
184
|
+
if (!cfg || !hasPatchPolicyContentRules(config))
|
|
185
|
+
return [];
|
|
186
|
+
const severity = cfg.mutationMode === 'warn' ? 'warning' : 'error';
|
|
187
|
+
const queueCreated = new Set();
|
|
188
|
+
for (const entry of entries)
|
|
189
|
+
for (const file of entry.createdFiles)
|
|
190
|
+
queueCreated.add(file);
|
|
191
|
+
const bodies = new Map(entries.map((entry) => [entry.filename, entry.diff]));
|
|
192
|
+
const issues = [];
|
|
193
|
+
for (const patch of manifest.patches) {
|
|
194
|
+
const diff = bodies.get(patch.filename);
|
|
195
|
+
if (cfg.upstreamAllowlist !== undefined && diff !== undefined) {
|
|
196
|
+
if (!isReservedOrder(cfg, patch.order)) {
|
|
197
|
+
const edits = collectUpstreamEdits(diff, queueCreated);
|
|
198
|
+
issues.push(...evaluateUpstreamEdits(cfg, patch.filename, edits, severity));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
issues.push(...evaluateScopeRules(cfg, patch.filename, patch.category, patch.filesAffected, severity));
|
|
202
|
+
}
|
|
203
|
+
issues.push(...(await evaluateDocumentPaths(cfg, projectRoot, severity)));
|
|
204
|
+
return issues;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* {@link evaluatePatchPolicyContent} for a caller without a queue context
|
|
208
|
+
* in hand (`verify`): reads the patch bodies only when a body-dependent
|
|
209
|
+
* rule is configured.
|
|
210
|
+
*/
|
|
211
|
+
export async function evaluatePatchPolicyContentForQueue(config, projectRoot, patchesDir, manifest) {
|
|
212
|
+
if (!hasPatchPolicyContentRules(config))
|
|
213
|
+
return [];
|
|
214
|
+
const { entries } = await buildPatchQueueContext(patchesDir);
|
|
215
|
+
return evaluatePatchPolicyContent(config, projectRoot, manifest, entries);
|
|
216
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
|
|
10
10
|
import type { FireForgeConfig, PatchPolicyConfig, PatchPolicyRange } from '../types/config.js';
|
|
11
11
|
/** Stable issue codes returned by patch policy evaluation. */
|
|
12
|
-
export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'order-collision' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap';
|
|
12
|
+
export type PatchPolicyIssueCode = 'filename-pattern' | 'filename-captures' | 'filename-metadata-mismatch' | 'order-collision' | 'category-range' | 'reserved-range' | 'reserved-documentation' | 'reserved-files' | 'description-required' | 'numeric-gap' | 'upstream-allowlist' | 'add-only' | 'scope' | 'reserved-document-missing';
|
|
13
13
|
/** A single patch policy validation finding. */
|
|
14
14
|
export interface PatchPolicyIssue {
|
|
15
15
|
code: PatchPolicyIssueCode;
|
|
@@ -30,6 +30,15 @@ export interface PatchPolicyEnforcementInput {
|
|
|
30
30
|
* `patch move-files --create`).
|
|
31
31
|
*/
|
|
32
32
|
hints?: Partial<Record<PatchPolicyIssueCode, string>>;
|
|
33
|
+
/**
|
|
34
|
+
* Patch filenames whose findings this mutation answers for. When set, only
|
|
35
|
+
* issues attributed to these rows are enforced: queue-level findings
|
|
36
|
+
* (order collisions, numeric gaps) and other patches' findings are left to
|
|
37
|
+
* `verify` and `lint --per-patch`. For mutations that change only the
|
|
38
|
+
* scoped rows' contents (`re-export`), so one patch's stale row cannot fail
|
|
39
|
+
* an unrelated patch refreshed in the same call.
|
|
40
|
+
*/
|
|
41
|
+
scope?: readonly string[];
|
|
33
42
|
}
|
|
34
43
|
/** Returns valid categories for prompts and CLI validation under the config. */
|
|
35
44
|
export declare function getPatchPolicyCategories(config: FireForgeConfig): string[];
|
|
@@ -49,8 +58,12 @@ export declare function categoryRangeLabel(ranges: readonly PatchPolicyRange[],
|
|
|
49
58
|
*/
|
|
50
59
|
export declare function categoryRangeForOrder(cfg: PatchPolicyConfig, category: string, order: number): PatchPolicyRange | null;
|
|
51
60
|
/** Evaluates an entire patch manifest against the configured policy. */
|
|
52
|
-
export declare function evaluatePatchPolicy(config: FireForgeConfig, manifest: PatchesManifest): PatchPolicyIssue[];
|
|
53
|
-
/**
|
|
61
|
+
export declare function evaluatePatchPolicy(config: Pick<FireForgeConfig, 'patchPolicy'>, manifest: PatchesManifest): PatchPolicyIssue[];
|
|
62
|
+
/**
|
|
63
|
+
* Builds a sorted manifest snapshot from projected patch metadata. Retired
|
|
64
|
+
* slots of `current` carry over unless a projected patch fills them, so a
|
|
65
|
+
* projection judges gaps the way the written manifest will.
|
|
66
|
+
*/
|
|
54
67
|
export declare function buildProjectedManifest(current: PatchesManifest | null, patches: PatchMetadata[]): PatchesManifest;
|
|
55
68
|
/** Applies a filename/order rename projection to a manifest without mutating it. */
|
|
56
69
|
export declare function applyRenameMapToManifest(manifest: PatchesManifest, renameMap: ReadonlyMap<string, {
|
|
@@ -12,6 +12,7 @@ import { warn } from '../utils/logger.js';
|
|
|
12
12
|
import { PATCH_CATEGORIES } from '../utils/validation.js';
|
|
13
13
|
import { rewriteStagedDependencyOwners } from './patch-manifest-io.js';
|
|
14
14
|
import { formatOrderRange, formatPatchOrder } from './patch-parse.js';
|
|
15
|
+
import { manifestWithRetiredSlots, retiredOrderSet } from './patch-retired.js';
|
|
15
16
|
/** Default patch filename contract used when a policy omits `filenamePattern`. */
|
|
16
17
|
const DEFAULT_PATCH_POLICY_FILENAME_PATTERN = '^(?<order>\\d{3})-(?<category>[a-z][a-z0-9-]*)-(?<slug>[a-z0-9-]+)\\.patch$';
|
|
17
18
|
function policy(config) {
|
|
@@ -222,7 +223,7 @@ function evaluatePatchMetadata(cfg, patch, severity) {
|
|
|
222
223
|
}
|
|
223
224
|
return issues;
|
|
224
225
|
}
|
|
225
|
-
function evaluateGaps(cfg, patches, severity) {
|
|
226
|
+
function evaluateGaps(cfg, patches, severity, retiredOrders = new Set()) {
|
|
226
227
|
if (cfg.allowGaps !== false)
|
|
227
228
|
return [];
|
|
228
229
|
const issues = [];
|
|
@@ -243,7 +244,11 @@ function evaluateGaps(cfg, patches, severity) {
|
|
|
243
244
|
const last = occupied[occupied.length - 1];
|
|
244
245
|
const missing = [];
|
|
245
246
|
for (let order = first; order <= last; order++) {
|
|
246
|
-
|
|
247
|
+
// A retired slot (`patch retire`) is an occupied order with no
|
|
248
|
+
// patch behind it: the number stays spent, so it is not a gap.
|
|
249
|
+
if (!occupiedSet.has(order) &&
|
|
250
|
+
!retiredOrders.has(order) &&
|
|
251
|
+
reservedRangeForOrder(cfg, order) === null) {
|
|
247
252
|
missing.push(order);
|
|
248
253
|
}
|
|
249
254
|
}
|
|
@@ -290,18 +295,19 @@ export function evaluatePatchPolicy(config, manifest) {
|
|
|
290
295
|
const severity = mutationMode(config) === 'warn' ? 'warning' : 'error';
|
|
291
296
|
const issues = manifest.patches.flatMap((patch) => evaluatePatchMetadata(cfg, patch, severity));
|
|
292
297
|
issues.push(...evaluateOrderCollisions(manifest.patches, severity));
|
|
293
|
-
issues.push(...evaluateGaps(cfg, manifest.patches, severity));
|
|
298
|
+
issues.push(...evaluateGaps(cfg, manifest.patches, severity, retiredOrderSet(manifest)));
|
|
294
299
|
return issues;
|
|
295
300
|
}
|
|
296
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* Builds a sorted manifest snapshot from projected patch metadata. Retired
|
|
303
|
+
* slots of `current` carry over unless a projected patch fills them, so a
|
|
304
|
+
* projection judges gaps the way the written manifest will.
|
|
305
|
+
*/
|
|
297
306
|
export function buildProjectedManifest(current, patches) {
|
|
298
307
|
const next = patches
|
|
299
308
|
.map((patch) => ({ ...patch, filesAffected: [...patch.filesAffected] }))
|
|
300
309
|
.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
301
|
-
return
|
|
302
|
-
version: current?.version ?? 1,
|
|
303
|
-
patches: next,
|
|
304
|
-
};
|
|
310
|
+
return manifestWithRetiredSlots(current, next);
|
|
305
311
|
}
|
|
306
312
|
/** Applies a filename/order rename projection to a manifest without mutating it. */
|
|
307
313
|
export function applyRenameMapToManifest(manifest, renameMap) {
|
|
@@ -325,7 +331,8 @@ export function applyRenameMapToManifest(manifest, renameMap) {
|
|
|
325
331
|
export function enforcePatchPolicy(input) {
|
|
326
332
|
if (policy(input.config) === undefined)
|
|
327
333
|
return;
|
|
328
|
-
const
|
|
334
|
+
const scope = input.scope === undefined ? undefined : new Set(input.scope);
|
|
335
|
+
const issues = evaluatePatchPolicy(input.config, input.manifest).filter((issue) => scope === undefined || scope.has(issue.filename));
|
|
329
336
|
if (issues.length === 0)
|
|
330
337
|
return;
|
|
331
338
|
const mode = mutationMode(input.config);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retired order slots: the placeholders `patch retire` leaves in
|
|
3
|
+
* `patches.json#retired` so a patch can leave the queue without opening a
|
|
4
|
+
* numeric gap or renumbering its successors.
|
|
5
|
+
*
|
|
6
|
+
* A slot is not a patch. It has no `.patch` file and no row in `patches`,
|
|
7
|
+
* so everything that applies, exports, lints or rebases the queue walks past
|
|
8
|
+
* it without knowing it exists. The one reader is the
|
|
9
|
+
* `patchPolicy.allowGaps: false` gap check, which counts a retired order as
|
|
10
|
+
* occupied. A patch that later takes the same order fills the slot, and
|
|
11
|
+
* {@link carryRetiredSlots} drops it on that write, so the manifest never
|
|
12
|
+
* holds a slot and a patch at one order.
|
|
13
|
+
*/
|
|
14
|
+
import type { PatchesManifest, PatchMetadata, PatchRetiredSlot } from '../types/commands/index.js';
|
|
15
|
+
/**
|
|
16
|
+
* The retired slots of `current` that no patch in `patches` occupies, or
|
|
17
|
+
* undefined when none remain. Every manifest write that rebuilds the
|
|
18
|
+
* `patches` list routes the slots through here, so a slot survives the
|
|
19
|
+
* rewrite and a filled one does not.
|
|
20
|
+
*
|
|
21
|
+
* @param current - Manifest the slots come from (null or slot-free: none)
|
|
22
|
+
* @param patches - The rows the written manifest will hold
|
|
23
|
+
*/
|
|
24
|
+
export declare function carryRetiredSlots(current: Pick<PatchesManifest, 'retired'> | null | undefined, patches: readonly Pick<PatchMetadata, 'order'>[]): PatchRetiredSlot[] | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Builds a manifest from `patches` that keeps `current`'s unfilled retired
|
|
27
|
+
* slots. The `retired` key is omitted entirely when no slot remains, so a
|
|
28
|
+
* queue that never retired anything writes the same JSON as before.
|
|
29
|
+
*
|
|
30
|
+
* @param current - Manifest the slots come from
|
|
31
|
+
* @param patches - The rows the written manifest will hold
|
|
32
|
+
*/
|
|
33
|
+
export declare function manifestWithRetiredSlots(current: Pick<PatchesManifest, 'retired'> | null | undefined, patches: PatchMetadata[]): PatchesManifest;
|
|
34
|
+
/** The set of order numbers held by retired slots. */
|
|
35
|
+
export declare function retiredOrderSet(manifest: Pick<PatchesManifest, 'retired'>): Set<number>;
|