@hominis/fireforge 0.50.0 → 0.51.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.
Files changed (205) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/check-report.d.ts +17 -4
  4. package/dist/src/commands/check-report.js +19 -6
  5. package/dist/src/commands/check-run.d.ts +5 -0
  6. package/dist/src/commands/check-run.js +9 -1
  7. package/dist/src/commands/check-steps.d.ts +8 -0
  8. package/dist/src/commands/check-steps.js +31 -6
  9. package/dist/src/commands/check.js +1 -1
  10. package/dist/src/commands/declarations-report.d.ts +43 -0
  11. package/dist/src/commands/declarations-report.js +76 -0
  12. package/dist/src/commands/export-placement-gate.js +8 -0
  13. package/dist/src/commands/furnace/rename-registrations.js +7 -2
  14. package/dist/src/commands/furnace/validate.js +26 -1
  15. package/dist/src/commands/lint-per-patch.js +6 -2
  16. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  17. package/dist/src/commands/lint-report.js +7 -2
  18. package/dist/src/commands/manifest.js +2 -0
  19. package/dist/src/commands/park.d.ts +22 -0
  20. package/dist/src/commands/park.js +232 -0
  21. package/dist/src/commands/patch/delete.js +11 -6
  22. package/dist/src/commands/patch/index.d.ts +3 -3
  23. package/dist/src/commands/patch/index.js +8 -4
  24. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  25. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  26. package/dist/src/commands/patch/lint-ignore.js +39 -29
  27. package/dist/src/commands/patch/move-files-create.js +11 -4
  28. package/dist/src/commands/patch/move-files-into.js +10 -4
  29. package/dist/src/commands/patch/move-files.js +3 -1
  30. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  31. package/dist/src/commands/patch/removal-created-files.js +83 -0
  32. package/dist/src/commands/patch/rename.js +15 -3
  33. package/dist/src/commands/patch/reserve.d.ts +33 -0
  34. package/dist/src/commands/patch/reserve.js +152 -0
  35. package/dist/src/commands/patch/retire.js +11 -6
  36. package/dist/src/commands/patch/split-plan.d.ts +10 -1
  37. package/dist/src/commands/patch/split-plan.js +16 -1
  38. package/dist/src/commands/patch/split.js +14 -5
  39. package/dist/src/commands/patch/stage.d.ts +34 -0
  40. package/dist/src/commands/patch/stage.js +153 -0
  41. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  42. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  43. package/dist/src/commands/patch/staged-dependency.js +40 -98
  44. package/dist/src/commands/re-export-adjacent.d.ts +5 -0
  45. package/dist/src/commands/re-export-adjacent.js +4 -3
  46. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  47. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  48. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  49. package/dist/src/commands/re-export-hunks.js +220 -0
  50. package/dist/src/commands/re-export-options.js +29 -2
  51. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  52. package/dist/src/commands/re-export-purity-scope.js +131 -0
  53. package/dist/src/commands/re-export-register.js +15 -1
  54. package/dist/src/commands/re-export-removed.d.ts +1 -1
  55. package/dist/src/commands/re-export-removed.js +17 -2
  56. package/dist/src/commands/re-export-scan.d.ts +2 -0
  57. package/dist/src/commands/re-export-scan.js +4 -1
  58. package/dist/src/commands/re-export-single.js +102 -42
  59. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  60. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  61. package/dist/src/commands/re-export.js +35 -34
  62. package/dist/src/commands/test-build-furnace.d.ts +3 -2
  63. package/dist/src/commands/test-build-furnace.js +8 -2
  64. package/dist/src/commands/test-build-phase.js +24 -7
  65. package/dist/src/commands/test-checkjs.js +3 -2
  66. package/dist/src/commands/test-dispatch.d.ts +39 -0
  67. package/dist/src/commands/test-dispatch.js +211 -0
  68. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  69. package/dist/src/commands/test-furnace-reach.js +132 -0
  70. package/dist/src/commands/test-orchestrate.d.ts +9 -0
  71. package/dist/src/commands/test-orchestrate.js +22 -4
  72. package/dist/src/commands/test-perf-scope.d.ts +21 -0
  73. package/dist/src/commands/test-perf-scope.js +55 -0
  74. package/dist/src/commands/test-prelock.js +100 -4
  75. package/dist/src/commands/test-register.js +18 -3
  76. package/dist/src/commands/test-results.d.ts +62 -0
  77. package/dist/src/commands/test-results.js +62 -0
  78. package/dist/src/commands/test-run.d.ts +13 -55
  79. package/dist/src/commands/test-run.js +73 -101
  80. package/dist/src/commands/test-shards.d.ts +110 -0
  81. package/dist/src/commands/test-shards.js +255 -0
  82. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  83. package/dist/src/commands/test-stale-gate.js +4 -2
  84. package/dist/src/commands/test-verdict.d.ts +2 -1
  85. package/dist/src/commands/test.d.ts +1 -1
  86. package/dist/src/commands/test.js +65 -75
  87. package/dist/src/commands/token-lint.js +48 -8
  88. package/dist/src/commands/verify.js +9 -2
  89. package/dist/src/core/build-prepare.d.ts +6 -0
  90. package/dist/src/core/build-prepare.js +29 -3
  91. package/dist/src/core/config-paths.d.ts +1 -1
  92. package/dist/src/core/config-paths.js +2 -0
  93. package/dist/src/core/config-validate-check.js +83 -7
  94. package/dist/src/core/config-validate-test-toolchains.js +14 -0
  95. package/dist/src/core/config-validate-token-scales.d.ts +3 -0
  96. package/dist/src/core/config-validate-token-scales.js +47 -5
  97. package/dist/src/core/config-validate.js +6 -0
  98. package/dist/src/core/coverage-extend.js +2 -2
  99. package/dist/src/core/coverage-records.d.ts +11 -0
  100. package/dist/src/core/coverage-records.js +34 -0
  101. package/dist/src/core/engine-session-lock.d.ts +6 -0
  102. package/dist/src/core/engine-session-lock.js +10 -2
  103. package/dist/src/core/file-lock-shared.d.ts +57 -0
  104. package/dist/src/core/file-lock-shared.js +117 -0
  105. package/dist/src/core/file-lock.d.ts +9 -0
  106. package/dist/src/core/file-lock.js +58 -2
  107. package/dist/src/core/furnace-apply.js +10 -10
  108. package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
  109. package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
  110. package/dist/src/core/furnace-config-validate.js +7 -1
  111. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  112. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  113. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  114. package/dist/src/core/furnace-validate-accessibility.js +61 -27
  115. package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
  116. package/dist/src/core/furnace-validate-exceptions.js +41 -4
  117. package/dist/src/core/furnace-validate-icon-button.js +30 -1
  118. package/dist/src/core/history-log-file.js +8 -1
  119. package/dist/src/core/line-prefixer.d.ts +18 -0
  120. package/dist/src/core/line-prefixer.js +34 -0
  121. package/dist/src/core/mach.d.ts +7 -0
  122. package/dist/src/core/mach.js +24 -9
  123. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  124. package/dist/src/core/module-resolution-preflight.js +24 -6
  125. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  126. package/dist/src/core/negative-control-compiled.js +73 -0
  127. package/dist/src/core/negative-control-diff.d.ts +14 -2
  128. package/dist/src/core/negative-control-diff.js +49 -6
  129. package/dist/src/core/negative-control.d.ts +12 -0
  130. package/dist/src/core/negative-control.js +48 -5
  131. package/dist/src/core/park-store.d.ts +63 -0
  132. package/dist/src/core/park-store.js +216 -0
  133. package/dist/src/core/patch-export-update.d.ts +4 -0
  134. package/dist/src/core/patch-export-update.js +12 -3
  135. package/dist/src/core/patch-export.d.ts +3 -2
  136. package/dist/src/core/patch-export.js +21 -14
  137. package/dist/src/core/patch-files.js +26 -8
  138. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
  139. package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
  140. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  141. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  142. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  143. package/dist/src/core/patch-lint-checkjs.js +13 -3
  144. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  145. package/dist/src/core/patch-lint-cross.js +5 -0
  146. package/dist/src/core/patch-lint-css.js +14 -10
  147. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
  148. package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
  149. package/dist/src/core/patch-lint-ignore.d.ts +56 -12
  150. package/dist/src/core/patch-lint-ignore.js +173 -43
  151. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  152. package/dist/src/core/patch-lint-ownership.js +26 -0
  153. package/dist/src/core/patch-lint-size.js +9 -1
  154. package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
  155. package/dist/src/core/patch-lint-token-scale.js +206 -29
  156. package/dist/src/core/patch-lint.js +2 -2
  157. package/dist/src/core/patch-manifest-io.js +8 -1
  158. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  159. package/dist/src/core/patch-manifest-key-order.js +61 -0
  160. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  161. package/dist/src/core/patch-manifest-stage.js +96 -0
  162. package/dist/src/core/patch-manifest-validate.js +27 -1
  163. package/dist/src/core/patch-order-legal.d.ts +2 -1
  164. package/dist/src/core/patch-order-legal.js +7 -2
  165. package/dist/src/core/patch-policy.d.ts +6 -2
  166. package/dist/src/core/patch-policy.js +14 -7
  167. package/dist/src/core/patch-retired.d.ts +26 -4
  168. package/dist/src/core/patch-retired.js +40 -2
  169. package/dist/src/core/run-log.d.ts +6 -0
  170. package/dist/src/core/run-log.js +17 -1
  171. package/dist/src/core/test-harness-crash.d.ts +2 -1
  172. package/dist/src/core/test-harness-crash.js +5 -8
  173. package/dist/src/core/test-harness-signature.d.ts +16 -0
  174. package/dist/src/core/test-harness-signature.js +19 -1
  175. package/dist/src/core/test-raw-log.d.ts +66 -0
  176. package/dist/src/core/test-raw-log.js +122 -0
  177. package/dist/src/core/test-run-completeness.d.ts +36 -3
  178. package/dist/src/core/test-run-completeness.js +57 -17
  179. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  180. package/dist/src/core/test-xpcshell-retry.js +2 -1
  181. package/dist/src/core/token-lint.d.ts +5 -2
  182. package/dist/src/core/token-lint.js +8 -3
  183. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  184. package/dist/src/core/token-sibling-sheets.js +119 -0
  185. package/dist/src/core/tree-guard.js +2 -0
  186. package/dist/src/core/typecheck-shim.d.ts +5 -1
  187. package/dist/src/core/typecheck-shim.js +115 -2
  188. package/dist/src/core/typecheck.js +3 -1
  189. package/dist/src/index.d.ts +9 -3
  190. package/dist/src/index.js +4 -1
  191. package/dist/src/types/commands/index.d.ts +2 -2
  192. package/dist/src/types/commands/options.d.ts +68 -7
  193. package/dist/src/types/commands/patches.d.ts +43 -0
  194. package/dist/src/types/config.d.ts +74 -2
  195. package/dist/src/types/furnace.d.ts +20 -0
  196. package/dist/src/types/index.d.ts +1 -1
  197. package/dist/src/utils/options.d.ts +3 -2
  198. package/dist/src/utils/options.js +9 -4
  199. package/docs/configuration.md +94 -9
  200. package/docs/furnace.md +43 -1
  201. package/docs/lifecycle-invariants.md +13 -11
  202. package/docs/machine-output.md +64 -18
  203. package/docs/patch-workflow.md +174 -13
  204. package/docs/testing.md +73 -4
  205. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.51.0
4
+
5
+ ### Test harness
6
+
7
+ - `started=` counts start lines as the output streams, or reads the mozlog raw log when the run wrote one. A suite printing more than 2 MB, or any `--full-output` run, no longer reports files that ran as unstarted. A refusal names its evidence with `started-source=`.
8
+ - Sharded runs print a `FIREFORGE-SHARD:` line per shard and `failed-shards=` on the verdict. New `--shard-size <n>` runs n files per mach invocation, judged per file. New `--results <path>` writes per-file JSON (status, expected, duration, log offset) from mozlog and adds `results=` to the verdict.
9
+ - Negative controls accept a plain unified diff, and a `git diff --no-index` against an edited copy. A control on compiled source is refused without `--build`. With `--build` it goes in before the build and is restored afterwards (`control-build=compiled`). A build-less control on stale packaging is refused before the lock.
10
+ - Mid-run engine edits the running browser cannot see no longer make a run inconclusive (`drift-tolerated=<n>`). A scoped `--build` of one file keeps its manifest siblings' coverage, and packaging refusals arrive before the lock.
11
+ - `--furnace-apply all|reached|none` and `test.furnaceApply` scope the pre-test Furnace apply; components left out are named in `furnace-skipped=`. The default stays `all`.
12
+
13
+ ### Locks
14
+
15
+ - New opt-in `--shared-lock` and `test.lockMode: "shared"` let build-less test runs with auto ports overlap on one objdir; builds, re-exports and every other writer stay exclusive. New `test --jobs <n>` runs a request's shards on n harness instances inside one hold, each on its own ports, with `[job i]`-prefixed output.
16
+ - `--wait-lock` and `FIREFORGE_WAIT_LOCK` accept up to 86400 seconds.
17
+
18
+ ### Patch queue
19
+
20
+ - Added `patch reserve <order>`, which holds a claimed order under `allowGaps: false`; default allocation skips it, and the export that takes it fills it. Added `patch stage <name>...`, which stages only the named patches' rows of `patches.json` (validated) and their patch files. Every writer emits one canonical `patches.json` key order, so the first full save may reorder keys once.
21
+ - Added `re-export --hunks-from <diff>` and `--exclude-hunks <diff>`, which export a shared file hunk by hunk. Added `fireforge park`, which sets a session's edits to a patch's files aside and merges them back three-way, warning about Fluent ids and files still referenced outside the patch.
22
+ - `re-export --dry-run` takes no engine lock and runs on a private git index. Its purity guard covers only the selected patches, their rows and the engine files they own, and names a concurrent writer instead of calling a peer's write a FireForge bug.
23
+ - Refused `re-export --scan`, `patch move-files` and `patch split` write one `--from`-ready declaration report (`--declarations-out`) and name one `staged-dependency --add --from` call per holding patch. `--refuse-adjacent-unmanaged` combines with `--scan`.
24
+ - `staged-dependency --remove` accepts `--creates` alone, matches registration lines regardless of whitespace, and exits 8 when nothing matches. Re-export drops a staged registration whose `creates` path no patch owns. An `--expect-removed` path owned by an unselected patch names that owner.
25
+ - `patch delete` and `retire` name created files still on disk and gain `--delete-created`. A description-only `patch rename` needs no `--yes`.
26
+ - An unscoped `verify`, and so `fireforge check`, also reads unexported engine files for imports of unregistered `resource:///modules` modules.
27
+
28
+ ### Lint and tokens
29
+
30
+ - `lintIgnore` ceilings take a free-form `meta` object and `share: "test"|"product"`, and a notice no longer exceeds a ceiling. `lint --report` adds `lintIgnoreWaivers` for bare waivers. `patch lint-ignore` gains `--add <rule>=<max>`, `--share` and `--from <json|report>`.
31
+ - `patchLint.tokenScales` skips `@font-face` and other descriptor at-rules. Each scale also takes a `prefix` list, `allowInCalc`, `offsetCap`, `exclude` globs and a `minFiles` floor (`token-scale-vacuous`). `token lint` treats a `var()` with a fallback as optional and resolves tokens declared by sibling sheets: from document links, from `@import`, or from `furnace.json#tokenSiblingSheets`.
32
+ - A test script's per-patch checkJs resolves modules created by earlier patches. Opt-in `typecheck.litTypes` types `LitElement` and `MozLitElement`. A scoped `lint --per-patch --patches` keeps stale staged declarations. `discoverPatches` follows symlinked patch files.
33
+ - The API exports `measurePatchSize`, `isPatchSizeRegistrationFile`, `codeLineMask` and `evaluatePatchPolicyContentForQueue`.
34
+
35
+ ### Gates
36
+
37
+ - `fireforge check` gains `check.timeout` and `check.timeouts.<step>` for built-in steps, `check.failOnSkipped`, `check.lintReport` (passed to extra steps as `FIREFORGE_CHECK_LINT_REPORT`), and `extraSteps[].after`. The verdict line names `skipped-steps=`.
38
+
39
+ ### Furnace
40
+
41
+ - `furnace validate`: an exception no longer suppresses an error (`validation-exception-on-error`), and an exception can name a `tag`. `minComponents` sets a floor (`component-floor`). `panel-item` counts as natively interactive, and the accessibility checks judge the host module together with its companion modules.
42
+ - `furnace deploy` and `furnace rename` edit `jar.mn` lines in place instead of re-sorting the widget's block.
43
+ - The `icon-button-label` check scans a button's content for text outside tags instead of stripping tags with a regex. A `>` inside a quoted attribute no longer ends the tag.
44
+
45
+ ### Other
46
+
47
+ - Windows: when two commands race to migrate the legacy history log, the loser stands down instead of failing on a claim the other command holds. The `check` process test no longer assumes a POSIX shell, and the `build` and `test` pre-lock tests no longer assume POSIX paths.
48
+ - Dev tooling: `@types/node` 26.6.5, `@vitest/coverage-v8` and `vitest` 5.0.5, `eslint-plugin-jsdoc` 65.3.6, `knip` 6.41.0, `prettier` 3.9.10, `typescript-eslint` 8.71.2 and `vite` 8.3.4. Runtime: `acorn` 8.19.0 and `magic-string` 1.4.3. CI: `actions/setup-node` 7.1.0 and `github/codeql-action` 4.38.3.
49
+
3
50
  ## 0.50.0
4
51
 
5
52
  ### Size budget
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.50.0",
4
- "commit": "996c59a67aa2144ff889a8c47e9304b071afae81",
5
- "shortCommit": "996c59a67aa2",
3
+ "version": "0.51.0",
4
+ "commit": "bd93338b037832fdcd69259a70a3a4b134b98906",
5
+ "shortCommit": "bd93338b0378",
6
6
  "dirty": false,
7
7
  "dirtyHash": null,
8
- "buildTime": "2026-10-12T00:43:41.476Z"
8
+ "buildTime": "2026-10-19T12:33:48.474Z"
9
9
  }
@@ -3,8 +3,15 @@ import type { CheckStepResult } from './check-run.js';
3
3
  * Why a check failed. `step-failures`: at least one step went red.
4
4
  * `lock-timeout`: every failing step exited 15, so nothing was judged red,
5
5
  * the steps just never got their locks. Re-queue rather than fix.
6
+ * `skipped-steps`: nothing went red, but `check.failOnSkipped` is set and a
7
+ * step had nothing to run.
6
8
  */
7
- export type CheckVerdictReason = 'step-failures' | 'lock-timeout';
9
+ export type CheckVerdictReason = 'step-failures' | 'lock-timeout' | 'skipped-steps';
10
+ /** How the verdict treats skipped steps. */
11
+ export interface CheckJudgeOptions {
12
+ /** `check.failOnSkipped`: a step skipped for want of anything to run fails the check. */
13
+ failOnSkipped?: boolean | undefined;
14
+ }
8
15
  /** The verdict over all steps. */
9
16
  export interface CheckVerdict {
10
17
  passed: boolean;
@@ -13,11 +20,17 @@ export interface CheckVerdict {
13
20
  skipped: string[];
14
21
  steps: number;
15
22
  }
16
- /** Judges the results: any failing step fails the check. */
17
- export declare function judgeCheck(results: readonly CheckStepResult[]): CheckVerdict;
23
+ /**
24
+ * Judges the results: any failing step fails the check. Under
25
+ * `failOnSkipped`, so does a step skipped because the project has nothing
26
+ * for it to run; it is then listed in `failed` too.
27
+ */
28
+ export declare function judgeCheck(results: readonly CheckStepResult[], options?: CheckJudgeOptions): CheckVerdict;
18
29
  /**
19
30
  * The verdict line: `FIREFORGE-VERDICT: PASS steps=<n> skipped=<n>` or
20
- * `FIREFORGE-VERDICT: FAIL reason=<reason> failed=<a,b> steps=<n> skipped=<n>`.
31
+ * `FIREFORGE-VERDICT: FAIL reason=<reason> failed=<a,b> steps=<n> skipped=<n>`,
32
+ * with an additive `skipped-steps=<a,b>` naming the skipped steps when
33
+ * there are any.
21
34
  */
22
35
  export declare function formatCheckVerdictLine(verdict: CheckVerdict): string;
23
36
  /** The per-step table: one aligned row per step, with its detail. */
@@ -8,12 +8,19 @@
8
8
  import { setStdoutSealed } from '../utils/logger.js';
9
9
  /** Exit code a step reports when its lock wait expired. */
10
10
  const LOCK_TIMEOUT_EXIT = 15;
11
- /** Judges the results: any failing step fails the check. */
12
- export function judgeCheck(results) {
11
+ /**
12
+ * Judges the results: any failing step fails the check. Under
13
+ * `failOnSkipped`, so does a step skipped because the project has nothing
14
+ * for it to run; it is then listed in `failed` too.
15
+ */
16
+ export function judgeCheck(results, options = {}) {
13
17
  const failedResults = results.filter((result) => result.status === 'fail');
18
+ const gaps = options.failOnSkipped === true
19
+ ? results.filter((result) => result.status === 'skipped' && result.skipKind !== 'config')
20
+ : [];
14
21
  const verdict = {
15
- passed: failedResults.length === 0,
16
- failed: failedResults.map((result) => result.name),
22
+ passed: failedResults.length === 0 && gaps.length === 0,
23
+ failed: [...failedResults, ...gaps].map((result) => result.name),
17
24
  skipped: results.filter((result) => result.status === 'skipped').map((result) => result.name),
18
25
  steps: results.length,
19
26
  };
@@ -22,14 +29,20 @@ export function judgeCheck(results) {
22
29
  ? 'lock-timeout'
23
30
  : 'step-failures';
24
31
  }
32
+ else if (gaps.length > 0) {
33
+ verdict.reason = 'skipped-steps';
34
+ }
25
35
  return verdict;
26
36
  }
27
37
  /**
28
38
  * The verdict line: `FIREFORGE-VERDICT: PASS steps=<n> skipped=<n>` or
29
- * `FIREFORGE-VERDICT: FAIL reason=<reason> failed=<a,b> steps=<n> skipped=<n>`.
39
+ * `FIREFORGE-VERDICT: FAIL reason=<reason> failed=<a,b> steps=<n> skipped=<n>`,
40
+ * with an additive `skipped-steps=<a,b>` naming the skipped steps when
41
+ * there are any.
30
42
  */
31
43
  export function formatCheckVerdictLine(verdict) {
32
- const counts = `steps=${verdict.steps} skipped=${verdict.skipped.length}`;
44
+ const skippedNames = verdict.skipped.length > 0 ? ` skipped-steps=${verdict.skipped.join(',')}` : '';
45
+ const counts = `steps=${verdict.steps} skipped=${verdict.skipped.length}${skippedNames}`;
33
46
  if (verdict.passed)
34
47
  return `FIREFORGE-VERDICT: PASS ${counts}`;
35
48
  return `FIREFORGE-VERDICT: FAIL reason=${verdict.reason ?? 'step-failures'} failed=${verdict.failed.join(',')} ${counts}`;
@@ -6,6 +6,11 @@ export interface CheckStepResult {
6
6
  /** The command line, as the table prints it. */
7
7
  command: string;
8
8
  status: 'pass' | 'fail' | 'skipped';
9
+ /**
10
+ * For a skipped step: `config` when `check.skip` names it, `unavailable`
11
+ * when the project has nothing for it to run.
12
+ */
13
+ skipKind?: 'config' | 'unavailable';
9
14
  /** The child's exit code. Null when it never ran or was killed at its budget. */
10
15
  exitCode: number | null;
11
16
  durationMs: number;
@@ -19,7 +19,14 @@ function tailLines(text) {
19
19
  async function runStep(plan, now) {
20
20
  const base = { name: plan.name, kind: plan.kind, command: plan.display };
21
21
  if (plan.skipReason !== undefined) {
22
- return { ...base, status: 'skipped', exitCode: null, durationMs: 0, detail: plan.skipReason };
22
+ return {
23
+ ...base,
24
+ status: 'skipped',
25
+ skipKind: plan.skipKind ?? 'unavailable',
26
+ exitCode: null,
27
+ durationMs: 0,
28
+ detail: plan.skipReason,
29
+ };
23
30
  }
24
31
  let stdout = '';
25
32
  let combined = '';
@@ -34,6 +41,7 @@ async function runStep(plan, now) {
34
41
  cwd: plan.cwd,
35
42
  processGroup: true,
36
43
  ...(plan.timeoutMs !== undefined ? { timeout: plan.timeoutMs } : {}),
44
+ ...(plan.env !== undefined ? { env: plan.env } : {}),
37
45
  onStdout: (chunk) => {
38
46
  stdout += chunk;
39
47
  append(chunk);
@@ -15,6 +15,14 @@ export interface CheckStepPlan {
15
15
  after: string[];
16
16
  /** Set when the step is planned but not run, with the reason. */
17
17
  skipReason?: string | undefined;
18
+ /**
19
+ * Why it is skipped: `config` when `check.skip` names it (a decision),
20
+ * `unavailable` when the project has nothing for it to run (a gap,
21
+ * which `check.failOnSkipped` fails).
22
+ */
23
+ skipKind?: 'config' | 'unavailable' | undefined;
24
+ /** Extra environment for the child. */
25
+ env?: Record<string, string> | undefined;
18
26
  /** Condenses a finished run into the table's detail cell. */
19
27
  summarize: (output: {
20
28
  stdout: string;
@@ -110,11 +110,18 @@ function builtinStep(invocation, projectRoot, name, args, summarize, extra = {})
110
110
  args: [...invocation.prefix, ...args],
111
111
  display: `fireforge ${args.join(' ')}`,
112
112
  cwd: projectRoot,
113
+ timeoutMs: extra.timeoutMs,
113
114
  after: extra.after ?? [],
114
115
  skipReason: extra.skipReason,
116
+ skipKind: extra.skipReason !== undefined ? 'unavailable' : undefined,
115
117
  summarize,
116
118
  };
117
119
  }
120
+ /** A built-in step's budget: `check.timeouts.<step>`, else `check.timeout`. */
121
+ function builtinTimeoutMs(check, name) {
122
+ const seconds = check.timeouts?.[name] ?? check.timeout;
123
+ return seconds !== undefined ? seconds * 1000 : undefined;
124
+ }
118
125
  /** Runs a project command line through the platform shell. */
119
126
  function shellInvocation(commandLine) {
120
127
  return process.platform === 'win32'
@@ -139,18 +146,33 @@ export function buildCheckPlan(input) {
139
146
  const furnaceWarnings = check.maxWarnings?.furnace !== undefined
140
147
  ? ['--max-warnings', String(check.maxWarnings.furnace)]
141
148
  : [];
149
+ const lintReport = check.lintReport !== undefined ? join(projectRoot, check.lintReport) : undefined;
150
+ const lintReportArgs = lintReport !== undefined ? ['--report', lintReport] : [];
151
+ const timeoutMs = (name) => builtinTimeoutMs(check, name);
142
152
  const plan = [
143
- builtinStep(invocation, projectRoot, 'status', ['status', '--json', '--summary', '--check', ...statusPolicyArgs(check.status), ...patchArgs], summarizeStatus),
144
- builtinStep(invocation, projectRoot, 'verify', ['verify', ...patchArgs], generic),
145
- builtinStep(invocation, projectRoot, 'lint', ['lint', '--per-patch', '--notices', 'summary', ...lintWarnings, ...patchArgs], generic),
153
+ builtinStep(invocation, projectRoot, 'status', ['status', '--json', '--summary', '--check', ...statusPolicyArgs(check.status), ...patchArgs], summarizeStatus, { timeoutMs: timeoutMs('status') }),
154
+ builtinStep(invocation, projectRoot, 'verify', ['verify', ...patchArgs], generic, {
155
+ timeoutMs: timeoutMs('verify'),
156
+ }),
157
+ builtinStep(invocation, projectRoot, 'lint', [
158
+ 'lint',
159
+ '--per-patch',
160
+ '--notices',
161
+ 'summary',
162
+ ...lintWarnings,
163
+ ...lintReportArgs,
164
+ ...patchArgs,
165
+ ], generic, { timeoutMs: timeoutMs('lint') }),
146
166
  builtinStep(invocation, projectRoot, 'typecheck', ['typecheck'], generic, {
147
167
  skipReason: config.typecheck === undefined ? 'no "typecheck" block in fireforge.json' : undefined,
168
+ timeoutMs: timeoutMs('typecheck'),
148
169
  }),
149
170
  builtinStep(invocation, projectRoot, 'furnace-validate', ['furnace', 'validate', '--json', ...furnaceWarnings], summarizeFurnace, {
150
171
  after: ['typecheck'],
151
172
  skipReason: input.furnaceConfigured ? undefined : 'no furnace.json',
173
+ timeoutMs: timeoutMs('furnace-validate'),
152
174
  }),
153
- builtinStep(invocation, projectRoot, 'token-coverage', ['token', 'coverage'], summarizeTokenCoverage),
175
+ builtinStep(invocation, projectRoot, 'token-coverage', ['token', 'coverage'], summarizeTokenCoverage, { timeoutMs: timeoutMs('token-coverage') }),
154
176
  ];
155
177
  for (const step of check.extraSteps ?? []) {
156
178
  const shell = shellInvocation(step.command);
@@ -162,14 +184,17 @@ export function buildCheckPlan(input) {
162
184
  display: step.command,
163
185
  cwd: step.cwd !== undefined ? join(projectRoot, step.cwd) : projectRoot,
164
186
  timeoutMs: step.timeout !== undefined ? step.timeout * 1000 : undefined,
165
- after: [],
187
+ after: step.after ?? [],
188
+ env: lintReport !== undefined ? { FIREFORGE_CHECK_LINT_REPORT: lintReport } : undefined,
166
189
  summarize: generic,
167
190
  });
168
191
  }
169
192
  for (const name of check.skip ?? []) {
170
193
  const step = plan.find((entry) => entry.name === name);
171
- if (step !== undefined)
194
+ if (step !== undefined) {
172
195
  step.skipReason = 'skipped by check.skip in fireforge.json';
196
+ step.skipKind = 'config';
197
+ }
173
198
  }
174
199
  return plan;
175
200
  }
@@ -123,7 +123,7 @@ async function runCheck(projectRoot, options) {
123
123
  const runnable = plan.filter((step) => step.skipReason === undefined).length;
124
124
  info(`Running ${runnable} step(s), up to ${Math.min(jobs, Math.max(1, runnable))} at once${patches ? `, judging ${patches.join(', ')}` : ''}.`);
125
125
  const results = await runCheckSteps(plan, jobs);
126
- const verdict = judgeCheck(results);
126
+ const verdict = judgeCheck(results, { failOnSkipped: config.check?.failOnSkipped });
127
127
  if (options.json === true) {
128
128
  writeCheckJson(results, verdict);
129
129
  }
@@ -0,0 +1,43 @@
1
+ import type { ConflictReport } from '../core/destructive.js';
2
+ import type { PatchLintIssue, PatchLintStagedDeclaration } from '../types/commands/index.js';
3
+ /** A written declaration report and the commands that apply it. */
4
+ export interface DeclarationsReport {
5
+ /** Absolute path of the JSON report. */
6
+ path: string;
7
+ /** Declarations written. */
8
+ count: number;
9
+ /** One `patch staged-dependency <holder> --add --from` call per holder. */
10
+ commands: string[];
11
+ }
12
+ /**
13
+ * A declaration as the report records it. `owner` is dropped when it names
14
+ * a patch the refused change would have created, which does not exist yet.
15
+ */
16
+ export type ReportedDeclaration = Omit<PatchLintStagedDeclaration, 'owner'> & {
17
+ owner?: string;
18
+ };
19
+ /** Rewrites a projected declaration into the names the current queue uses. */
20
+ export type DeclarationRenamer = (declaration: PatchLintStagedDeclaration) => ReportedDeclaration;
21
+ /**
22
+ * Writes every declaration `issues` propose into one report, or returns
23
+ * undefined when they propose none.
24
+ *
25
+ * @param projectRoot - Project root (the default report directory lives under it)
26
+ * @param issues - The refusal's findings
27
+ * @param out - `--declarations-out`, relative to the working directory
28
+ * @param rename - Maps projected patch names back to current ones
29
+ */
30
+ export declare function writeDeclarationsReport(projectRoot: string, issues: readonly PatchLintIssue[], out?: string, rename?: DeclarationRenamer): Promise<DeclarationsReport | undefined>;
31
+ /** The refusal lines that point at a written report. */
32
+ export declare function formatDeclarationsReport(report: DeclarationsReport): string[];
33
+ /**
34
+ * Appends the report lines to a refusal, writing the report from the
35
+ * refusal's findings. A refusal that proposes no declaration is returned
36
+ * unchanged, and so is no refusal at all.
37
+ */
38
+ export declare function withDeclarationsReport(conflicts: ConflictReport | null, args: {
39
+ projectRoot: string;
40
+ regressions: readonly PatchLintIssue[];
41
+ out?: string | undefined;
42
+ rename?: DeclarationRenamer;
43
+ }): Promise<ConflictReport | null>;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * One `--from`-ready declaration report for a refused queue change.
4
+ *
5
+ * Moving a test out of a patch that owns a shared manifest refused with one
6
+ * `forward-registration` error per manifest line, and each line needed its
7
+ * own `patch staged-dependency --add` call: a slice that added seven suites
8
+ * wrote eighteen declarations by hand. `patch move-files`, `patch split`
9
+ * and `re-export --scan` now write every declaration their refusal proposes
10
+ * into one lint-report-shaped JSON (`queueIssues[].stagedDeclarations`) and
11
+ * name the single `patch staged-dependency <holder> --add --from <file>`
12
+ * call per holding patch that discharges them.
13
+ */
14
+ import { join, resolve } from 'node:path';
15
+ import { STAGED_DEPENDENCY_REASON_PLACEHOLDER } from '../core/patch-lint-staged-registration.js';
16
+ import { ensureDir, writeJson } from '../utils/fs.js';
17
+ /** Where an unnamed report goes, relative to the project root. */
18
+ const DEFAULT_REPORT_DIR = join('.fireforge', 'declarations');
19
+ /** The checks whose findings carry declarations that discharge them. */
20
+ const DECLARABLE_CHECKS = new Set(['forward-import', 'forward-registration']);
21
+ /**
22
+ * Writes every declaration `issues` propose into one report, or returns
23
+ * undefined when they propose none.
24
+ *
25
+ * @param projectRoot - Project root (the default report directory lives under it)
26
+ * @param issues - The refusal's findings
27
+ * @param out - `--declarations-out`, relative to the working directory
28
+ * @param rename - Maps projected patch names back to current ones
29
+ */
30
+ export async function writeDeclarationsReport(projectRoot, issues, out, rename = (declaration) => declaration) {
31
+ const queueIssues = issues
32
+ .filter((issue) => DECLARABLE_CHECKS.has(issue.check))
33
+ .map((issue) => ({
34
+ check: issue.check,
35
+ file: issue.file,
36
+ message: issue.message,
37
+ stagedDeclarations: (issue.stagedDeclarations ?? []).map(rename),
38
+ }))
39
+ .filter((issue) => issue.stagedDeclarations.length > 0);
40
+ const declarations = queueIssues.flatMap((issue) => issue.stagedDeclarations);
41
+ if (declarations.length === 0)
42
+ return undefined;
43
+ const path = out !== undefined
44
+ ? resolve(out)
45
+ : join(projectRoot, DEFAULT_REPORT_DIR, `${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}.json`);
46
+ await ensureDir(resolve(path, '..'));
47
+ await writeJson(path, { queueIssues });
48
+ const holders = [...new Set(declarations.map((declaration) => declaration.patch))].sort();
49
+ return {
50
+ path,
51
+ count: declarations.length,
52
+ commands: holders.map((holder) => `fireforge patch staged-dependency ${holder} --add --from ${path} ` +
53
+ STAGED_DEPENDENCY_REASON_PLACEHOLDER),
54
+ };
55
+ }
56
+ /** The refusal lines that point at a written report. */
57
+ export function formatDeclarationsReport(report) {
58
+ return [
59
+ `All ${report.count} declaration(s) this refusal proposes are in ${report.path}; ` +
60
+ 'declare them in one call per holding patch:',
61
+ ...report.commands.map((command) => ` ${command}`),
62
+ ];
63
+ }
64
+ /**
65
+ * Appends the report lines to a refusal, writing the report from the
66
+ * refusal's findings. A refusal that proposes no declaration is returned
67
+ * unchanged, and so is no refusal at all.
68
+ */
69
+ export async function withDeclarationsReport(conflicts, args) {
70
+ if (conflicts === null)
71
+ return null;
72
+ const report = await writeDeclarationsReport(args.projectRoot, args.regressions, args.out, args.rename);
73
+ if (report === undefined)
74
+ return conflicts;
75
+ return { ...conflicts, details: [...conflicts.details, ...formatDeclarationsReport(report)] };
76
+ }
@@ -8,6 +8,7 @@
8
8
  import { confirmDestructive } from '../core/destructive.js';
9
9
  import { loadPatchesManifest } from '../core/patch-manifest.js';
10
10
  import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
11
+ import { reservationAt } from '../core/patch-retired.js';
11
12
  import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
12
13
  import { InvalidArgumentError } from '../errors/base.js';
13
14
  import { proceedAfterDecision } from './destructive-decision.js';
@@ -68,6 +69,13 @@ export async function gatePlacementPlan(args) {
68
69
  forceUnsafe: options.forceUnsafe === true,
69
70
  });
70
71
  const summary = placementSummary(placementPlan);
72
+ const reservation = reservationAt(currentManifest, placementPlan.insertionOrder);
73
+ if (reservation !== undefined) {
74
+ const notice = `order ${placementPlan.insertionOrder} is reserved by ${reservation.owner}; ` +
75
+ 'this export fills the reservation';
76
+ summary.push(notice);
77
+ s.message(notice);
78
+ }
71
79
  const renameCount = placementPlan.renameMap.size;
72
80
  // Route through confirmDestructive when the operation is destructive
73
81
  // enough to warrant a prompt (more than one rename) or when the user asked
@@ -10,9 +10,11 @@ import { join } from 'node:path';
10
10
  import { loadConfig } from '../../core/config.js';
11
11
  import { resolveFtlChromeSubPath, resolveFtlLocaleJarMnPath, } from '../../core/furnace-constants.js';
12
12
  import { resolveFurnaceMarkerComment } from '../../core/furnace-marker.js';
13
- import { addCustomElementRegistration, addJarMnEntries, addLocaleFtlJarMnEntry, removeCustomElementRegistration, removeJarMnEntries, removeLocaleFtlJarMnEntry, renameCustomElementRegistration, } from '../../core/furnace-registration.js';
13
+ import { addCustomElementRegistration, addJarMnEntries, addLocaleFtlJarMnEntry, removeCustomElementRegistration, removeLocaleFtlJarMnEntry, renameCustomElementRegistration, } from '../../core/furnace-registration.js';
14
+ import { renameJarMnEntriesInPlace } from '../../core/furnace-registration-jar-inplace.js';
14
15
  import { snapshotFile } from '../../core/furnace-rollback.js';
15
16
  import { pathExists, readText, removeFile, writeText } from '../../utils/fs.js';
17
+ import { renameComponentFileName } from './rename-helpers.js';
16
18
  /**
17
19
  * The project marker (`// <MARKER>:`) a registration line written by the
18
20
  * rename carries, resolved exactly as `furnace apply` resolves it. An
@@ -50,10 +52,13 @@ export async function updateEngineRegistrations(options) {
50
52
  }
51
53
  if (await pathExists(jarMnPath)) {
52
54
  await snapshotFile(journal, jarMnPath);
53
- await removeJarMnEntries(engineDir, oldName);
54
55
  const files = (await readdir(newDir, { withFileTypes: true }))
55
56
  .filter((e) => e.isFile() && (e.name.endsWith('.mjs') || e.name.endsWith('.css')))
56
57
  .map((e) => e.name);
58
+ // Rewrite the old lines where they stand, like the customElements.js
59
+ // entry above, so the rename moves no other line of the block; only a
60
+ // file with no line yet is inserted.
61
+ await renameJarMnEntriesInPlace(engineDir, oldName, newName, files, (file) => renameComponentFileName(file, oldName, newName));
57
62
  if (files.length > 0) {
58
63
  await addJarMnEntries(engineDir, newName, files);
59
64
  }
@@ -7,6 +7,7 @@ import { assertFurnaceReady } from '../../core/furnace-precondition.js';
7
7
  import { addCustomElementRegistration, addJarMnEntries, pruneStaleJarMnEntries, } from '../../core/furnace-registration.js';
8
8
  import { validateAllComponents, validateComponent } from '../../core/furnace-validate.js';
9
9
  import { applyValidationExceptions, } from '../../core/furnace-validate-exceptions.js';
10
+ import { createIssue } from '../../core/furnace-validate-helpers.js';
10
11
  import { FurnaceError } from '../../errors/furnace.js';
11
12
  import { toError } from '../../utils/errors.js';
12
13
  import { pathExists } from '../../utils/fs.js';
@@ -189,6 +190,20 @@ async function applyAutoFix(projectRoot, config, furnacePaths, run, countedIssue
189
190
  function exceptRun(run, config) {
190
191
  return applyValidationExceptions([...run.results.values()].flat(), config.validationExceptions, run.judged);
191
192
  }
193
+ /**
194
+ * The `furnace.json#minComponents` floor of a full run: an error when it
195
+ * judged fewer components, so a run over an emptied or moved components
196
+ * tree cannot pass by validating nothing. A run naming one component is
197
+ * not judged against it.
198
+ */
199
+ function componentFloorIssue(config, name, judged) {
200
+ if (name !== undefined || config.minComponents === undefined)
201
+ return undefined;
202
+ if (judged >= config.minComponents)
203
+ return undefined;
204
+ return createIssue('furnace.json', 'error', 'component-floor', `Validated ${judged} component(s), under furnace.json minComponents ${config.minComponents}. ` +
205
+ 'Check that the components tree is where furnace.json says, or lower the floor.');
206
+ }
192
207
  /**
193
208
  * Runs the furnace validate command to perform static analysis on components.
194
209
  * @param projectRoot - Root directory of the project
@@ -216,22 +231,32 @@ async function runFurnaceValidate(projectRoot, name, options) {
216
231
  const furnacePaths = getFurnacePaths(projectRoot);
217
232
  const run = await runValidationPass(projectRoot, name, config, furnacePaths);
218
233
  if (run === undefined) {
234
+ const floor = componentFloorIssue(config, name, 0);
219
235
  if (options.json === true) {
220
236
  renderValidateJson({
221
237
  components: [],
222
- outcome: { issues: [], excepted: [], stale: [] },
238
+ outcome: { issues: floor ? [floor] : [], excepted: [], stale: [] },
223
239
  maxWarnings: options.maxWarnings,
224
240
  });
225
241
  }
242
+ if (floor !== undefined) {
243
+ renderValidateHuman([], [floor]);
244
+ throw new FurnaceError('Validation failed with 1 error(s).');
245
+ }
226
246
  outro(name ? 'Validation complete' : 'Done');
227
247
  return;
228
248
  }
249
+ const floor = componentFloorIssue(config, name, run.results.size);
229
250
  const firstPass = exceptRun(run, config);
251
+ if (floor !== undefined)
252
+ firstPass.issues.push(floor);
230
253
  renderValidateHuman([...run.results.keys()], firstPass.issues);
231
254
  let outcome = firstPass;
232
255
  if (options.fix && firstPass.issues.length > 0) {
233
256
  await applyAutoFix(projectRoot, config, furnacePaths, run, firstPass.issues);
234
257
  outcome = exceptRun(run, config);
258
+ if (floor !== undefined)
259
+ outcome.issues.push(floor);
235
260
  }
236
261
  const report = {
237
262
  components: [...run.results.keys()],
@@ -205,7 +205,8 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache, hel
205
205
  heldNotices.push({ check: suppressedIssue.check, patch: patch.filename, suppressed: true });
206
206
  continue;
207
207
  }
208
- const ceiling = patch.lintIgnore?.find((entry) => isLintIgnoreCeiling(entry) && entry.rule === suppressedIssue.check);
208
+ const ceilings = (patch.lintIgnore ?? []).filter((entry) => isLintIgnoreCeiling(entry) && entry.rule === suppressedIssue.check);
209
+ const ceiling = ceilings.find((entry) => isLintIgnoreCeiling(entry) && entry.share === suppressedIssue.share) ?? ceilings[0];
209
210
  const waiver = ceiling === undefined ? 'lintIgnore' : `lintIgnore (${formatLintIgnoreEntry(ceiling)})`;
210
211
  info(`NOTICE [${suppressedIssue.check}] ${patch.filename}: suppressed by ${waiver} — ${suppressedIssue.message}`);
211
212
  }
@@ -404,7 +405,10 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
404
405
  const { linted, skipped, cacheDirty, reusedCacheEntries, suppressed } = await applyPerPatchResults(subset, results, issues, checkJs, cache, heldNotices);
405
406
  const queueIssues = [];
406
407
  for (const issue of lintPatchQueue(ctx)) {
407
- if (isSubset && !subsetTouchedFiles.has(issue.file))
408
+ // A finding that names a subset patch stays in scope even when its file
409
+ // is one the patch no longer touches (a stale staged declaration).
410
+ const namesSubset = issue.patches?.some((name) => subsetNames.has(name)) === true;
411
+ if (isSubset && !namesSubset && !subsetTouchedFiles.has(issue.file))
408
412
  continue;
409
413
  issues.push(issue);
410
414
  queueIssues.push(issue);
@@ -21,7 +21,7 @@
21
21
  * cold" via `probeCheckJsGlobalIssues` instead of building anything.
22
22
  */
23
23
  import { invokePatchLintCheckJsGrouped, probeCheckJsGlobalIssues, runCheckJsTestFilesGrouped, } from '../core/patch-lint-checkjs.js';
24
- import { isTestScriptFile, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from '../core/patch-lint-ownership.js';
24
+ import { isTestScriptFile, resolvableModulesForTests, resolvePatchOwnedSysMjs, resolvePatchOwnedTestScripts, } from '../core/patch-lint-ownership.js';
25
25
  /**
26
26
  * Builds the per-run checkJs program controller when `patchLint.checkJs` is
27
27
  * enabled, or returns undefined. With `rootScopePatches` set (the
@@ -69,7 +69,7 @@ export function buildPerRunCheckJs(projectRoot, paths, config, ctx, rootScopePat
69
69
  const sys = await invokePatchLintCheckJsGrouped(paths.engine, resolvePatchOwnedSysMjs(new Set(), ctx), patchLint, projectRoot, scopedRoots);
70
70
  if (!testFilesEnabled)
71
71
  return sys;
72
- const tests = await runCheckJsTestFilesGrouped(paths.engine, resolvePatchOwnedTestScripts(new Set(), ctx), patchLint, projectRoot, scopedRoots);
72
+ const tests = await runCheckJsTestFilesGrouped(paths.engine, resolvePatchOwnedTestScripts(new Set(), ctx), patchLint, projectRoot, scopedRoots, resolvableModulesForTests(ctx));
73
73
  const byFile = new Map(sys.byFile);
74
74
  for (const [rel, list] of tests.byFile) {
75
75
  byFile.set(rel, [...(byFile.get(rel) ?? []), ...list]);
@@ -10,7 +10,7 @@
10
10
  * none is wanted.
11
11
  */
12
12
  import { getPatchSizeThresholds, resolvePatchSizeTier, } from '../core/patch-lint.js';
13
- import { evaluateLintIgnoreCeilings, } from '../core/patch-lint-ignore.js';
13
+ import { evaluateLintIgnoreCeilings, evaluateLintIgnoreWaivers, } from '../core/patch-lint-ignore.js';
14
14
  import { writeJson } from '../utils/fs.js';
15
15
  import { info } from '../utils/logger.js';
16
16
  const LINT_REPORT_SCHEMA_VERSION = 1;
@@ -37,6 +37,8 @@ export async function writePerPatchLintReport(reportPath, subset, results, confi
37
37
  if (!patch || !result)
38
38
  continue;
39
39
  const tier = resolvePatchSizeTier(result.existingFiles, patch.tier);
40
+ const findings = [...result.rawIssues, ...result.suppressedIssues];
41
+ const measurements = { lines: result.lineCount, files: patch.filesAffected.length };
40
42
  patches.push({
41
43
  filename: patch.filename,
42
44
  name: patch.name,
@@ -50,7 +52,10 @@ export async function writePerPatchLintReport(reportPath, subset, results, confi
50
52
  suppressedIssues: result.suppressedIssues,
51
53
  lintIgnoreCeilings: result.status === 'skipped'
52
54
  ? []
53
- : evaluateLintIgnoreCeilings(patch.lintIgnore, [...result.rawIssues, ...result.suppressedIssues], { lines: result.lineCount, files: patch.filesAffected.length }),
55
+ : evaluateLintIgnoreCeilings(patch.lintIgnore, findings, measurements),
56
+ lintIgnoreWaivers: result.status === 'skipped'
57
+ ? []
58
+ : evaluateLintIgnoreWaivers(patch.lintIgnore, findings, measurements),
54
59
  });
55
60
  if (result.status === 'skipped')
56
61
  skipped++;
@@ -11,6 +11,7 @@ import { registerFurnace } from './furnace/index.js';
11
11
  import { registerImport } from './import.js';
12
12
  import { registerLint } from './lint.js';
13
13
  import { registerPackage } from './package.js';
14
+ import { registerPark } from './park.js';
14
15
  import { registerPatch } from './patch/index.js';
15
16
  import { registerReExport } from './re-export-register.js';
16
17
  import { registerRebase } from './rebase/index.js';
@@ -47,6 +48,7 @@ export const COMMAND_MANIFEST = [
47
48
  { name: 'export', group: 'workflow', register: registerExport },
48
49
  { name: 'export-all', group: 'workflow', register: registerExportAll },
49
50
  { name: 're-export', group: 'workflow', register: registerReExport },
51
+ { name: 'park', group: 'workflow', register: registerPark },
50
52
  { name: 'patch', group: 'workflow', register: registerPatch },
51
53
  { name: 'rebase', group: 'workflow', register: registerRebase },
52
54
  { name: 'package', group: 'workflow', register: registerPackage },