@hominis/fireforge 0.52.0 → 0.53.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 (243) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/bootstrap.js +2 -3
  4. package/dist/src/commands/build-prelock.d.ts +3 -3
  5. package/dist/src/commands/build-prelock.js +3 -3
  6. package/dist/src/commands/build.js +4 -0
  7. package/dist/src/commands/declarations-report.d.ts +9 -3
  8. package/dist/src/commands/declarations-report.js +18 -11
  9. package/dist/src/commands/discard.js +3 -3
  10. package/dist/src/commands/doctor-furnace-config-sync.js +4 -4
  11. package/dist/src/commands/doctor-furnace.js +5 -5
  12. package/dist/src/commands/export-adopt-check.d.ts +5 -6
  13. package/dist/src/commands/export-adopt-check.js +5 -6
  14. package/dist/src/commands/export-flow.js +6 -9
  15. package/dist/src/commands/export-staged-registration.d.ts +4 -4
  16. package/dist/src/commands/export-staged-registration.js +4 -4
  17. package/dist/src/commands/forward-import-remedy.d.ts +2 -3
  18. package/dist/src/commands/forward-import-remedy.js +2 -3
  19. package/dist/src/commands/furnace/create-dry-run.d.ts +2 -2
  20. package/dist/src/commands/furnace/deploy-fragment-includers.d.ts +2 -2
  21. package/dist/src/commands/furnace/deploy-fragment-includers.js +2 -2
  22. package/dist/src/commands/furnace/index.js +2 -2
  23. package/dist/src/commands/lint.js +4 -4
  24. package/dist/src/commands/patch/compact.js +1 -1
  25. package/dist/src/commands/patch/lint-ignore-from.d.ts +3 -4
  26. package/dist/src/commands/patch/lint-ignore-from.js +3 -4
  27. package/dist/src/commands/patch/move-files-adopt.d.ts +33 -0
  28. package/dist/src/commands/patch/move-files-adopt.js +65 -0
  29. package/dist/src/commands/patch/move-files-create.js +41 -20
  30. package/dist/src/commands/patch/move-files-into.d.ts +18 -0
  31. package/dist/src/commands/patch/move-files-into.js +125 -60
  32. package/dist/src/commands/patch/move-files-retire.d.ts +39 -0
  33. package/dist/src/commands/patch/move-files-retire.js +91 -0
  34. package/dist/src/commands/patch/move-files.d.ts +3 -0
  35. package/dist/src/commands/patch/move-files.js +42 -12
  36. package/dist/src/commands/patch/move-size-tier.d.ts +27 -0
  37. package/dist/src/commands/patch/move-size-tier.js +66 -0
  38. package/dist/src/commands/patch/rename.js +1 -1
  39. package/dist/src/commands/patch/reorder-projection.d.ts +17 -0
  40. package/dist/src/commands/patch/reorder-projection.js +49 -0
  41. package/dist/src/commands/patch/reorder-shift.d.ts +30 -0
  42. package/dist/src/commands/patch/reorder-shift.js +307 -0
  43. package/dist/src/commands/patch/reorder.d.ts +9 -15
  44. package/dist/src/commands/patch/reorder.js +52 -50
  45. package/dist/src/commands/patch/split-plan.d.ts +31 -3
  46. package/dist/src/commands/patch/split-plan.js +36 -12
  47. package/dist/src/commands/patch/split-staged.d.ts +2 -2
  48. package/dist/src/commands/patch/split-staged.js +2 -2
  49. package/dist/src/commands/patch/split.js +18 -5
  50. package/dist/src/commands/patch/staged-dependency-entries.d.ts +9 -1
  51. package/dist/src/commands/patch/staged-dependency-entries.js +32 -4
  52. package/dist/src/commands/patch/staged-dependency-from.js +15 -9
  53. package/dist/src/commands/patch/staged-dependency.js +13 -9
  54. package/dist/src/commands/patch/tier.js +2 -2
  55. package/dist/src/commands/patch-rename-map.d.ts +14 -3
  56. package/dist/src/commands/patch-rename-map.js +21 -3
  57. package/dist/src/commands/re-export-adjacent.d.ts +4 -5
  58. package/dist/src/commands/re-export-lint.d.ts +4 -3
  59. package/dist/src/commands/re-export-lint.js +4 -3
  60. package/dist/src/commands/re-export-preflight.js +1 -2
  61. package/dist/src/commands/re-export-purity-scope.d.ts +1 -1
  62. package/dist/src/commands/re-export-purity-scope.js +13 -7
  63. package/dist/src/commands/re-export-retired-declarations.d.ts +22 -0
  64. package/dist/src/commands/re-export-retired-declarations.js +113 -0
  65. package/dist/src/commands/re-export-single.js +2 -2
  66. package/dist/src/commands/re-export-stale-registrations.d.ts +27 -6
  67. package/dist/src/commands/re-export-stale-registrations.js +65 -21
  68. package/dist/src/commands/re-export.js +4 -0
  69. package/dist/src/commands/rebase/continue.js +3 -3
  70. package/dist/src/commands/register.js +5 -4
  71. package/dist/src/commands/status.js +1 -1
  72. package/dist/src/commands/test-build-furnace.d.ts +1 -2
  73. package/dist/src/commands/test-build-furnace.js +1 -2
  74. package/dist/src/commands/test-build-phase.d.ts +8 -0
  75. package/dist/src/commands/test-build-phase.js +42 -7
  76. package/dist/src/commands/test-control-restore.d.ts +40 -0
  77. package/dist/src/commands/test-control-restore.js +86 -0
  78. package/dist/src/commands/test-dispatch.js +68 -7
  79. package/dist/src/commands/test-multi-verdict.js +1 -0
  80. package/dist/src/commands/test-orchestrate.d.ts +6 -0
  81. package/dist/src/commands/test-orchestrate.js +28 -0
  82. package/dist/src/commands/test-perf-scope.d.ts +1 -1
  83. package/dist/src/commands/test-perf-scope.js +1 -1
  84. package/dist/src/commands/test-prelock.d.ts +7 -8
  85. package/dist/src/commands/test-prelock.js +15 -12
  86. package/dist/src/commands/test-register.js +2 -2
  87. package/dist/src/commands/test-results.d.ts +10 -1
  88. package/dist/src/commands/test-results.js +20 -3
  89. package/dist/src/commands/test-run.d.ts +6 -0
  90. package/dist/src/commands/test-run.js +23 -4
  91. package/dist/src/commands/test-shards.d.ts +7 -3
  92. package/dist/src/commands/test-shards.js +24 -7
  93. package/dist/src/commands/test-stale-gate.d.ts +6 -1
  94. package/dist/src/commands/test-stale-gate.js +40 -2
  95. package/dist/src/commands/test-verdict.d.ts +5 -3
  96. package/dist/src/commands/test.js +10 -0
  97. package/dist/src/commands/token.js +2 -2
  98. package/dist/src/commands/verify.js +1 -1
  99. package/dist/src/commands/watch.js +1 -1
  100. package/dist/src/commands/wire.js +1 -1
  101. package/dist/src/core/build-audit-resolve.d.ts +4 -5
  102. package/dist/src/core/build-audit-resolve.js +4 -5
  103. package/dist/src/core/build-audit-skip.d.ts +2 -2
  104. package/dist/src/core/build-audit.d.ts +3 -4
  105. package/dist/src/core/build-baseline-types.d.ts +24 -3
  106. package/dist/src/core/build-baseline-validate.d.ts +30 -1
  107. package/dist/src/core/build-baseline-validate.js +127 -35
  108. package/dist/src/core/build-baseline.d.ts +8 -2
  109. package/dist/src/core/build-baseline.js +25 -16
  110. package/dist/src/core/build-jar-escalation.d.ts +4 -4
  111. package/dist/src/core/build-jar-escalation.js +4 -4
  112. package/dist/src/core/build-prepare.js +4 -4
  113. package/dist/src/core/config-validate.js +4 -4
  114. package/dist/src/core/coverage-extend.d.ts +5 -3
  115. package/dist/src/core/coverage-extend.js +8 -3
  116. package/dist/src/core/coverage-records.d.ts +4 -5
  117. package/dist/src/core/coverage-records.js +4 -5
  118. package/dist/src/core/destructive.d.ts +2 -2
  119. package/dist/src/core/destructive.js +2 -2
  120. package/dist/src/core/discard-baseline.d.ts +2 -2
  121. package/dist/src/core/discard-baseline.js +3 -3
  122. package/dist/src/core/engine-session-lock.d.ts +1 -1
  123. package/dist/src/core/engine-session-lock.js +1 -1
  124. package/dist/src/core/file-lock-owner.d.ts +5 -5
  125. package/dist/src/core/file-lock-owner.js +5 -5
  126. package/dist/src/core/file-lock-queue.js +6 -8
  127. package/dist/src/core/file-lock-shared.d.ts +4 -4
  128. package/dist/src/core/file-lock-shared.js +4 -4
  129. package/dist/src/core/file-lock.js +7 -10
  130. package/dist/src/core/firefox.js +2 -2
  131. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +1 -1
  132. package/dist/src/core/furnace-apply-overwrite-warn.js +1 -1
  133. package/dist/src/core/furnace-apply.js +3 -3
  134. package/dist/src/core/furnace-operation.js +5 -5
  135. package/dist/src/core/furnace-registration-jar-inplace.js +5 -6
  136. package/dist/src/core/furnace-registration-rename.d.ts +5 -5
  137. package/dist/src/core/furnace-registration-rename.js +5 -5
  138. package/dist/src/core/furnace-registration-validate.d.ts +2 -1
  139. package/dist/src/core/furnace-registration-validate.js +2 -1
  140. package/dist/src/core/furnace-registration.js +2 -2
  141. package/dist/src/core/git-base.js +4 -3
  142. package/dist/src/core/git-diff.js +1 -1
  143. package/dist/src/core/git.js +1 -1
  144. package/dist/src/core/harness-orphans.d.ts +6 -6
  145. package/dist/src/core/harness-orphans.js +6 -6
  146. package/dist/src/core/harness-port-reservation.d.ts +60 -0
  147. package/dist/src/core/harness-port-reservation.js +122 -0
  148. package/dist/src/core/history-log-file.d.ts +9 -9
  149. package/dist/src/core/history-log-file.js +15 -15
  150. package/dist/src/core/history-log.d.ts +3 -3
  151. package/dist/src/core/history-log.js +3 -3
  152. package/dist/src/core/launchable-build.d.ts +39 -0
  153. package/dist/src/core/launchable-build.js +85 -0
  154. package/dist/src/core/marionette-port.js +1 -3
  155. package/dist/src/core/moz-manifest-rules.js +3 -2
  156. package/dist/src/core/negative-control-compiled.d.ts +13 -3
  157. package/dist/src/core/negative-control-compiled.js +27 -5
  158. package/dist/src/core/parser-fallback.d.ts +5 -5
  159. package/dist/src/core/parser-fallback.js +5 -5
  160. package/dist/src/core/patch-identifier-suggest.d.ts +6 -0
  161. package/dist/src/core/patch-identifier-suggest.js +11 -4
  162. package/dist/src/core/patch-lint-checkjs.d.ts +1 -1
  163. package/dist/src/core/patch-lint-checkjs.js +1 -1
  164. package/dist/src/core/patch-lint-css.js +2 -2
  165. package/dist/src/core/patch-lint-file-size.d.ts +5 -7
  166. package/dist/src/core/patch-lint-file-size.js +5 -7
  167. package/dist/src/core/patch-lint-ignore.d.ts +3 -3
  168. package/dist/src/core/patch-lint-ignore.js +3 -3
  169. package/dist/src/core/patch-lint-prettier.d.ts +4 -6
  170. package/dist/src/core/patch-lint-prettier.js +4 -6
  171. package/dist/src/core/patch-lint-projection.d.ts +1 -1
  172. package/dist/src/core/patch-lint-projection.js +1 -1
  173. package/dist/src/core/patch-lint-queue-types.d.ts +2 -2
  174. package/dist/src/core/patch-lint-queue-types.js +2 -2
  175. package/dist/src/core/patch-manifest-consistency.d.ts +2 -2
  176. package/dist/src/core/patch-manifest-consistency.js +2 -2
  177. package/dist/src/core/patch-manifest-io.d.ts +5 -1
  178. package/dist/src/core/patch-manifest-io.js +12 -10
  179. package/dist/src/core/patch-manifest-key-order.d.ts +3 -4
  180. package/dist/src/core/patch-manifest-key-order.js +3 -4
  181. package/dist/src/core/patch-parse.d.ts +3 -3
  182. package/dist/src/core/patch-parse.js +3 -3
  183. package/dist/src/core/patch-policy.d.ts +1 -1
  184. package/dist/src/core/patch-policy.js +1 -1
  185. package/dist/src/core/patch-source-metadata.d.ts +1 -1
  186. package/dist/src/core/patch-source-metadata.js +1 -1
  187. package/dist/src/core/patch-subset.d.ts +5 -3
  188. package/dist/src/core/patch-subset.js +8 -7
  189. package/dist/src/core/patch-transform.d.ts +2 -2
  190. package/dist/src/core/patch-transform.js +2 -2
  191. package/dist/src/core/rebase-session.js +4 -6
  192. package/dist/src/core/register-xpcshell-test.d.ts +28 -7
  193. package/dist/src/core/register-xpcshell-test.js +76 -27
  194. package/dist/src/core/run-log.d.ts +8 -8
  195. package/dist/src/core/run-log.js +8 -8
  196. package/dist/src/core/status-classify.d.ts +1 -2
  197. package/dist/src/core/status-classify.js +1 -2
  198. package/dist/src/core/test-harness-crash.d.ts +5 -3
  199. package/dist/src/core/test-harness-crash.js +13 -14
  200. package/dist/src/core/test-harness-infra.d.ts +32 -0
  201. package/dist/src/core/test-harness-infra.js +69 -0
  202. package/dist/src/core/test-harness-signature.d.ts +6 -0
  203. package/dist/src/core/test-stale-check.d.ts +38 -0
  204. package/dist/src/core/test-stale-check.js +82 -1
  205. package/dist/src/core/test-stall-triage.js +5 -7
  206. package/dist/src/core/token-category.js +2 -2
  207. package/dist/src/core/token-inventory.js +3 -3
  208. package/dist/src/core/token-manager.d.ts +2 -2
  209. package/dist/src/core/token-manager.js +5 -7
  210. package/dist/src/core/token-variant.d.ts +6 -6
  211. package/dist/src/core/token-variant.js +7 -7
  212. package/dist/src/core/typecheck-shim.js +1 -1
  213. package/dist/src/core/wire-destroy.d.ts +1 -1
  214. package/dist/src/core/wire-destroy.js +1 -1
  215. package/dist/src/core/wire-dom-fragment.js +1 -1
  216. package/dist/src/core/wire-init.d.ts +1 -1
  217. package/dist/src/core/wire-init.js +1 -1
  218. package/dist/src/core/wire-subscript.d.ts +1 -1
  219. package/dist/src/core/wire-subscript.js +1 -1
  220. package/dist/src/core/wire-utils.d.ts +2 -2
  221. package/dist/src/core/wire-utils.js +3 -3
  222. package/dist/src/errors/base.d.ts +1 -1
  223. package/dist/src/errors/base.js +1 -1
  224. package/dist/src/errors/rebase.d.ts +1 -1
  225. package/dist/src/errors/rebase.js +1 -1
  226. package/dist/src/types/commands/options.d.ts +26 -3
  227. package/dist/src/types/config.d.ts +5 -7
  228. package/dist/src/utils/options.js +2 -2
  229. package/dist/src/utils/process.js +2 -2
  230. package/dist/src/utils/regex.d.ts +2 -2
  231. package/dist/src/utils/regex.js +3 -3
  232. package/dist/src/utils/sleep.d.ts +5 -6
  233. package/dist/src/utils/sleep.js +5 -6
  234. package/dist/src/utils/validation.js +2 -2
  235. package/docs/configuration.md +21 -23
  236. package/docs/exit-codes.md +8 -11
  237. package/docs/furnace.md +3 -3
  238. package/docs/lifecycle-invariants.md +36 -45
  239. package/docs/machine-output.md +41 -24
  240. package/docs/patch-workflow.md +129 -24
  241. package/docs/run-logs.md +8 -9
  242. package/docs/testing.md +105 -46
  243. package/package.json +1 -1
@@ -0,0 +1,113 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `re-export --expect-removed` drops the staged declarations whose
4
+ * `creates` path the run retires.
5
+ *
6
+ * Retiring a file would otherwise leave another patch's staged
7
+ * registration (or forward import) for it in place, unflagged until the
8
+ * per-patch lint after both re-exports reported `staged-dependency-unused`.
9
+ * The run that retires the path drops every such declaration, in any patch of the queue, in one manifest
10
+ * update, and names each one.
11
+ */
12
+ import { withPatchDirectoryLock } from '../core/patch-lock.js';
13
+ import { loadPatchesManifestForWrite, mutatePatchRowsInManifest } from '../core/patch-manifest.js';
14
+ import { info } from '../utils/logger.js';
15
+ import { recordStagedDeclarationPrune, } from './re-export-stale-registrations.js';
16
+ /**
17
+ * Finds the declarations whose `creates` path is retired and owned by no
18
+ * patch outside `retiring`.
19
+ *
20
+ * @param manifest - The queue to judge
21
+ * @param removed - The run's `--expect-removed` paths
22
+ * @param retiring - Patches whose ownership of a removed path does not
23
+ * count: the selected owners in a dry run, whose rows are not rewritten
24
+ * yet; nothing after the write, where a failed owner still owns its path
25
+ */
26
+ function planRetiredDeclarationDrops(manifest, removed, retiring) {
27
+ const orphaned = new Set(removed.filter((file) => !manifest.patches.some((patch) => !retiring.has(patch.filename) && patch.filesAffected.includes(file))));
28
+ if (orphaned.size === 0)
29
+ return [];
30
+ const drops = [];
31
+ for (const patch of manifest.patches) {
32
+ const drop = planHolderDrop(patch, orphaned);
33
+ if (drop !== undefined)
34
+ drops.push(drop);
35
+ }
36
+ return drops;
37
+ }
38
+ function planHolderDrop(patch, orphaned) {
39
+ const forwardImports = patch.stagedDependencies?.forwardImports ?? [];
40
+ const registrations = patch.stagedDependencies?.registrations ?? [];
41
+ const dropped = [
42
+ ...registrations
43
+ .filter((entry) => orphaned.has(entry.creates))
44
+ .map((entry) => ({ kind: 'registration', ...entry })),
45
+ ...forwardImports
46
+ .filter((entry) => orphaned.has(entry.creates))
47
+ .map((entry) => ({ kind: 'import', ...entry })),
48
+ ];
49
+ if (dropped.length === 0)
50
+ return undefined;
51
+ const keptImports = forwardImports.filter((entry) => !orphaned.has(entry.creates));
52
+ const keptRegistrations = registrations.filter((entry) => !orphaned.has(entry.creates));
53
+ const staged = {
54
+ ...(keptImports.length > 0 ? { forwardImports: keptImports } : {}),
55
+ ...(keptRegistrations.length > 0 ? { registrations: keptRegistrations } : {}),
56
+ };
57
+ return {
58
+ holder: patch.filename,
59
+ dropped,
60
+ stagedDependencies: Object.keys(staged).length > 0 ? staged : undefined,
61
+ };
62
+ }
63
+ function reportRetiredDeclarationDrops(drops, isDryRun) {
64
+ const verb = isDryRun ? 'would drop' : 'drops';
65
+ for (const { holder, dropped } of drops) {
66
+ for (const entry of dropped) {
67
+ const what = entry.kind === 'registration'
68
+ ? `staged registration ${entry.file} "${entry.line.trim()}"`
69
+ : `staged forward-import ${entry.file} "${entry.specifier}"`;
70
+ info(`${holder}: ${verb} ${what}: --expect-removed retires ${entry.creates}.`);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Drops (or, in a dry run, names) every staged declaration whose `creates`
76
+ * path this run retired. Runs after the selected patches are written, so a
77
+ * path whose owner failed to re-export is still owned and its declarations
78
+ * stay.
79
+ */
80
+ export async function dropRetiredPathDeclarations(args) {
81
+ const { manifest, isDryRun } = args;
82
+ const patchesDir = args.paths.patches;
83
+ const removed = args.options.expectRemoved ?? [];
84
+ const selected = new Set(args.selectedPatches.map((patch) => patch.filename));
85
+ if (removed.length === 0)
86
+ return;
87
+ if (isDryRun) {
88
+ reportRetiredDeclarationDrops(planRetiredDeclarationDrops(manifest, removed, selected), isDryRun);
89
+ return;
90
+ }
91
+ const drops = await withPatchDirectoryLock(patchesDir, async () => {
92
+ const current = await loadPatchesManifestForWrite(patchesDir);
93
+ if (!current)
94
+ return [];
95
+ const planned = planRetiredDeclarationDrops(current, removed, new Set());
96
+ const byHolder = new Map(planned.map((drop) => [drop.holder, drop]));
97
+ if (byHolder.size === 0)
98
+ return [];
99
+ await mutatePatchRowsInManifest(patchesDir, [...byHolder.keys()], (existing) => {
100
+ const drop = byHolder.get(existing.filename);
101
+ if (drop === undefined)
102
+ return null;
103
+ return drop.stagedDependencies === undefined
104
+ ? { unset: ['stagedDependencies'] }
105
+ : { set: { stagedDependencies: drop.stagedDependencies } };
106
+ });
107
+ return planned;
108
+ }, { command: 're-export' });
109
+ reportRetiredDeclarationDrops(drops, isDryRun);
110
+ for (const drop of drops) {
111
+ await recordStagedDeclarationPrune(patchesDir, drop.holder, drop.dropped, 'staged declarations on retired paths');
112
+ }
113
+ }
@@ -23,7 +23,7 @@ import { reportForeignDrift } from './re-export-drift.js';
23
23
  import { buildHunkSelectedDiff, hunkSelectionOf } from './re-export-hunks.js';
24
24
  import { assertRefreshedBodyHasNoNewQueueErrors, lintReExportedPatch, refreshQueueCtxEntry, storeReExportLintResult, } from './re-export-lint.js';
25
25
  import { assertScanAdoptionsHaveNoForwardImports, assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, scanPatchFilesForReExport, } from './re-export-scan.js';
26
- import { planStaleRegistrationDrop, recordStaleRegistrationDrop, reportStaleRegistrationDrop, } from './re-export-stale-registrations.js';
26
+ import { findPendingRegistrationCreates, planStaleRegistrationDrop, recordStaleRegistrationDrop, reportStaleRegistrationDrop, } from './re-export-stale-registrations.js';
27
27
  const GIT_INDEX_LOCK_RETRY_MS = 300;
28
28
  /**
29
29
  * Retries a single patch re-export exactly once when the failure is
@@ -211,7 +211,7 @@ async function reExportSinglePatch(args) {
211
211
  return false;
212
212
  }
213
213
  const { updates, ignoreChecks, effectiveTier, existingIgnoreSet, flagIgnoreSet } = computeReExportUpdates(patch, options, currentFilesAffected);
214
- const stalePlan = planStaleRegistrationDrop(manifest, patch, currentFilesAffected);
214
+ const stalePlan = planStaleRegistrationDrop(manifest, patch, currentFilesAffected, await findPendingRegistrationCreates(paths.engine, manifest, patch, currentFilesAffected, adjacentCtx.expectedNew));
215
215
  if (stalePlan !== undefined)
216
216
  reportStaleRegistrationDrop(patch.filename, stalePlan, isDryRun);
217
217
  const unset = applyStaleRegistrationDrop(stalePlan, updates);
@@ -1,18 +1,39 @@
1
- import type { PatchesManifest, PatchMetadata, PatchStagedDependencies, PatchStagedRegistration } from '../types/commands/index.js';
1
+ import type { PatchesManifest, PatchMetadata, PatchStagedDependencies, PatchStagedForwardImport, PatchStagedRegistration } from '../types/commands/index.js';
2
2
  /** The holder's registrations split by whether the queue still owns `creates`. */
3
3
  export interface StaleRegistrationPlan {
4
4
  /** Registrations whose `creates` path no patch owns. */
5
5
  dropped: PatchStagedRegistration[];
6
+ /** Unowned registrations kept because their `creates` file is pending adoption. */
7
+ pending: PatchStagedRegistration[];
6
8
  /** The staged block to write, or undefined when nothing is left. */
7
9
  stagedDependencies: PatchStagedDependencies | undefined;
8
10
  }
11
+ /**
12
+ * The unowned `creates` paths of `patch`'s registrations that are pending
13
+ * adoption: the file exists in `engine/`, or is an `--expect-new` path of
14
+ * this run. Such a registration is waiting for its subject patch's scan.
15
+ */
16
+ export declare function findPendingRegistrationCreates(engineDir: string, manifest: PatchesManifest, patch: PatchMetadata, currentFilesAffected: readonly string[], expectedNew: ReadonlySet<string>): Promise<Set<string>>;
9
17
  /**
10
18
  * Finds `patch`'s registrations whose `creates` path no patch in the queue
11
- * owns, judging `patch` by the file list this re-export will write.
12
- * Returns undefined when nothing is stale.
19
+ * owns, judging `patch` by the file list this re-export will write. A
20
+ * registration whose `creates` path is in `pending` (see
21
+ * {@link findPendingRegistrationCreates}) is kept and reported instead.
22
+ * Returns undefined when nothing is stale or pending.
13
23
  */
14
- export declare function planStaleRegistrationDrop(manifest: PatchesManifest, patch: PatchMetadata, currentFilesAffected: readonly string[]): StaleRegistrationPlan | undefined;
15
- /** Names each dropped registration, in both a dry run and a write. */
24
+ export declare function planStaleRegistrationDrop(manifest: PatchesManifest, patch: PatchMetadata, currentFilesAffected: readonly string[], pendingCreates?: ReadonlySet<string>): StaleRegistrationPlan | undefined;
25
+ /** Names each dropped and each kept-pending registration, in both a dry run and a write. */
16
26
  export declare function reportStaleRegistrationDrop(patchFilename: string, plan: StaleRegistrationPlan, isDryRun: boolean): void;
17
- /** Records the dropped declarations as a delta, like `staged-dependency --remove`. */
27
+ /** One declaration a re-export pruned, as the history log records it. */
28
+ export type PrunedStagedDeclaration = ({
29
+ kind: 'registration';
30
+ } & PatchStagedRegistration) | ({
31
+ kind: 'import';
32
+ } & PatchStagedForwardImport);
33
+ /**
34
+ * Records declarations a re-export pruned from one patch as a delta, like
35
+ * `staged-dependency --remove`.
36
+ */
37
+ export declare function recordStagedDeclarationPrune(patchesDir: string, patchFilename: string, removed: readonly PrunedStagedDeclaration[], what: string): Promise<void>;
38
+ /** Records the dropped registrations as a delta, like `staged-dependency --remove`. */
18
39
  export declare function recordStaleRegistrationDrop(patchesDir: string, patchFilename: string, plan: StaleRegistrationPlan): Promise<void>;
@@ -3,23 +3,40 @@
3
3
  * Re-export prunes a staged registration the queue can no longer satisfy.
4
4
  *
5
5
  * A merge that deletes a file whose jar.mn line lives in another patch
6
- * left the staged registration on that patch: re-export kept it, lint then
7
- * warned `staged-dependency-unused`, and the stale declaration also kept
8
- * the holder measuring above a just-lowered size ceiling. When no patch in
6
+ * leaves the staged registration on that patch: lint warns
7
+ * `staged-dependency-unused`, and the stale declaration keeps the holder
8
+ * measuring above a just-lowered size ceiling. When no patch in
9
9
  * the queue owns a registration's `creates` path any more, the declaration
10
10
  * cannot be met, so the holder's own re-export drops it and says so.
11
+ *
12
+ * A registration whose `creates` file exists in `engine/`, or is an
13
+ * `--expect-new` path of the run, is kept: a manifest owner's re-export can
14
+ * run between declaring the registration and the scan that adopts the
15
+ * file, and pruning it there would break the scan that needs it next.
11
16
  */
17
+ import { join } from 'node:path';
12
18
  import { appendHistoryBestEffort } from '../core/history-log.js';
19
+ import { pathExists } from '../utils/fs.js';
13
20
  import { info } from '../utils/logger.js';
14
21
  /**
15
- * Finds `patch`'s registrations whose `creates` path no patch in the queue
16
- * owns, judging `patch` by the file list this re-export will write.
17
- * Returns undefined when nothing is stale.
22
+ * The unowned `creates` paths of `patch`'s registrations that are pending
23
+ * adoption: the file exists in `engine/`, or is an `--expect-new` path of
24
+ * this run. Such a registration is waiting for its subject patch's scan.
18
25
  */
19
- export function planStaleRegistrationDrop(manifest, patch, currentFilesAffected) {
20
- const registrations = patch.stagedDependencies?.registrations ?? [];
21
- if (registrations.length === 0)
22
- return undefined;
26
+ export async function findPendingRegistrationCreates(engineDir, manifest, patch, currentFilesAffected, expectedNew) {
27
+ const owned = ownedPaths(manifest, patch, currentFilesAffected);
28
+ const pending = new Set();
29
+ for (const registration of patch.stagedDependencies?.registrations ?? []) {
30
+ const creates = registration.creates;
31
+ if (owned.has(creates) || pending.has(creates))
32
+ continue;
33
+ if (expectedNew.has(creates) || (await pathExists(join(engineDir, creates)))) {
34
+ pending.add(creates);
35
+ }
36
+ }
37
+ return pending;
38
+ }
39
+ function ownedPaths(manifest, patch, currentFilesAffected) {
23
40
  const owned = new Set(currentFilesAffected);
24
41
  for (const entry of manifest.patches) {
25
42
  if (entry.filename === patch.filename)
@@ -27,10 +44,26 @@ export function planStaleRegistrationDrop(manifest, patch, currentFilesAffected)
27
44
  for (const file of entry.filesAffected)
28
45
  owned.add(file);
29
46
  }
30
- const dropped = registrations.filter((registration) => !owned.has(registration.creates));
31
- if (dropped.length === 0)
47
+ return owned;
48
+ }
49
+ /**
50
+ * Finds `patch`'s registrations whose `creates` path no patch in the queue
51
+ * owns, judging `patch` by the file list this re-export will write. A
52
+ * registration whose `creates` path is in `pending` (see
53
+ * {@link findPendingRegistrationCreates}) is kept and reported instead.
54
+ * Returns undefined when nothing is stale or pending.
55
+ */
56
+ export function planStaleRegistrationDrop(manifest, patch, currentFilesAffected, pendingCreates = new Set()) {
57
+ const registrations = patch.stagedDependencies?.registrations ?? [];
58
+ if (registrations.length === 0)
32
59
  return undefined;
33
- const kept = registrations.filter((registration) => owned.has(registration.creates));
60
+ const owned = ownedPaths(manifest, patch, currentFilesAffected);
61
+ const unowned = registrations.filter((registration) => !owned.has(registration.creates));
62
+ const pending = unowned.filter((registration) => pendingCreates.has(registration.creates));
63
+ const dropped = unowned.filter((registration) => !pendingCreates.has(registration.creates));
64
+ if (dropped.length === 0 && pending.length === 0)
65
+ return undefined;
66
+ const kept = registrations.filter((registration) => !dropped.includes(registration));
34
67
  const forwardImports = patch.stagedDependencies?.forwardImports ?? [];
35
68
  const staged = {
36
69
  ...(forwardImports.length > 0 ? { forwardImports } : {}),
@@ -38,25 +71,36 @@ export function planStaleRegistrationDrop(manifest, patch, currentFilesAffected)
38
71
  };
39
72
  return {
40
73
  dropped,
74
+ pending,
41
75
  stagedDependencies: Object.keys(staged).length > 0 ? staged : undefined,
42
76
  };
43
77
  }
44
- /** Names each dropped registration, in both a dry run and a write. */
78
+ /** Names each dropped and each kept-pending registration, in both a dry run and a write. */
45
79
  export function reportStaleRegistrationDrop(patchFilename, plan, isDryRun) {
46
80
  const verb = isDryRun ? 'would drop' : 'drops';
47
81
  for (const registration of plan.dropped) {
48
82
  info(`${patchFilename}: ${verb} staged registration ${registration.file} "${registration.line.trim()}": ` +
49
83
  `no patch in the queue creates ${registration.creates} any more.`);
50
84
  }
85
+ for (const registration of plan.pending) {
86
+ info(`${patchFilename}: staged registration ${registration.file} "${registration.line.trim()}" ` +
87
+ `for ${registration.creates} kept: file exists but no patch owns it yet.`);
88
+ }
51
89
  }
52
- /** Records the dropped declarations as a delta, like `staged-dependency --remove`. */
53
- export async function recordStaleRegistrationDrop(patchesDir, patchFilename, plan) {
90
+ /**
91
+ * Records declarations a re-export pruned from one patch as a delta, like
92
+ * `staged-dependency --remove`.
93
+ */
94
+ export async function recordStagedDeclarationPrune(patchesDir, patchFilename, removed, what) {
95
+ if (removed.length === 0)
96
+ return;
54
97
  await appendHistoryBestEffort(patchesDir, {
55
98
  operation: 're-export-staged-prune',
56
- args: {
57
- filename: patchFilename,
58
- removed: plan.dropped.map((registration) => ({ kind: 'registration', ...registration })),
59
- },
99
+ args: { filename: patchFilename, removed: [...removed] },
60
100
  result: 'ok',
61
- }, `re-export pruned stale staged registrations on ${patchFilename}`);
101
+ }, `re-export pruned ${what} on ${patchFilename}`);
102
+ }
103
+ /** Records the dropped registrations as a delta, like `staged-dependency --remove`. */
104
+ export async function recordStaleRegistrationDrop(patchesDir, patchFilename, plan) {
105
+ await recordStagedDeclarationPrune(patchesDir, patchFilename, plan.dropped.map((registration) => ({ kind: 'registration', ...registration })), 'stale staged registrations');
62
106
  }
@@ -17,6 +17,7 @@ import { reExportFilesInPlace } from './re-export-files.js';
17
17
  import { buildReExportLintContext, finishReExportLint } from './re-export-lint.js';
18
18
  import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
19
19
  import { validateExpectedRemovals } from './re-export-removed.js';
20
+ import { dropRetiredPathDeclarations } from './re-export-retired-declarations.js';
20
21
  import { normalizeEngineRelativeInput, normalizeScanFiles } from './re-export-scan.js';
21
22
  import { assignScanFileOwners, scanOptionsForPatch } from './re-export-scan-owner.js';
22
23
  import { reExportSinglePatchWithIndexLockRetry, } from './re-export-single.js';
@@ -190,6 +191,9 @@ export async function reExportCommand(projectRoot, patches, options) {
190
191
  progress.error('Re-export failed');
191
192
  throw new GeneralError('All selected patches failed to re-export. Check the errors above.');
192
193
  }
194
+ // Declarations on a path this run retired can never be met; drop them
195
+ // now rather than leave them for the next per-patch lint to report.
196
+ await dropRetiredPathDeclarations({ paths, manifest, options, selectedPatches, isDryRun });
193
197
  // `--stamp` only fires on a run where every selected patch refreshed
194
198
  // cleanly. A partial success would leave some patches with a stale body
195
199
  // but a new version, the opposite of the "what I tested, what the
@@ -21,9 +21,9 @@ import { runPatchLoop } from './patch-loop.js';
21
21
  */
22
22
  export async function handleContinue(projectRoot, maxFuzz, waitLockSeconds) {
23
23
  intro('FireForge Rebase — Continue');
24
- // A present-but-unreadable session is not "no session in progress". That
25
- // conflation is what made a corrupt file unrecoverable. Name the file and
26
- // point at --abort, which can now clear it.
24
+ // A present-but-unreadable session is not "no session in progress":
25
+ // treating it as one leaves a corrupt file unrecoverable. Name the file
26
+ // and point at --abort, which can clear it.
27
27
  const read = await readRebaseSession(projectRoot);
28
28
  if (!read.present)
29
29
  throw new NoRebaseSessionError();
@@ -32,9 +32,9 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
32
32
  }
33
33
  // Accept either repo-root-relative (`engine/browser/...`) or
34
34
  // engine-relative (`browser/...`) inputs. Operators frequently paste
35
- // the former from the output of tab completion or `git status`, and
36
- // the mismatch used to produce a "File not found" error that named
37
- // the original path with no hint that dropping `engine/` would fix it.
35
+ // the former from the output of tab completion or `git status`, so
36
+ // both shapes resolve to the same path instead of a "File not found"
37
+ // error.
38
38
  const engineRelativePath = stripEnginePrefix(filePath);
39
39
  // Verify the file exists in engine/ (skip for dry-run)
40
40
  if (!options.dryRun) {
@@ -97,7 +97,8 @@ export function registerRegister(program, { getProjectRoot, withErrorHandling })
97
97
  .command('register <path>')
98
98
  .description('Register a file in the appropriate build manifest')
99
99
  .option('--dry-run', 'Show what would be changed without writing')
100
- .option('--after <entry>', 'Place entry after line containing this substring (instead of alphabetical)')
100
+ .option('--after <entry>', 'Place entry after line containing this substring (instead of alphabetical); for an ' +
101
+ 'xpcshell test, after the whole section of the xpcshell.toml entry it names')
101
102
  .option('--create-manifest', 'Scaffold a missing manifest (directory moz.build with EXTRA_JS_MODULES / XPCSHELL_TESTS_MANIFESTS, or xpcshell.toml) and wire the parent DIRS chain instead of failing with "Manifest not found"')
102
103
  .action(withErrorHandling(async (path, options) => {
103
104
  await registerCommand(getProjectRoot(), path, pickDefined(options));
@@ -409,7 +409,7 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
409
409
  .option('--fail-on <classifications>', 'Comma-separated classification list that fails --check, replacing the default set (implies --check). Valid: patch-backed, patch-owned-drift, unmanaged, branding, furnace, conflict, binary-unsupported, ownership-conflict (a path claimed by more than one patch)')
410
410
  .option('--allow <classifications>', 'Comma-separated classification list that never fails --check, removed from the fail set (implies --check; same values as --fail-on)')
411
411
  .option('--fail-on-unknown', 'With --allow: also fail on every present classification that neither --fail-on nor --allow names, so a classification added in a later FireForge fails closed (implies --check)')
412
- .option('--patches <names...>', 'Judge only the named patches: keeps the files each owns or claims and the ownership rows naming it (filename, stem, order number, manifest name, or slug; repeat or comma-separate). Unmanaged files belong to no patch and drop out. Default view, --json and --ownership only')
412
+ .option('--patches <names...>', 'Judge only the named patches: keeps the files each owns or claims and the ownership rows naming it (filename, stem, order number, manifest name, or slug; repeat, or separate with commas or whitespace). Unmanaged files belong to no patch and drop out. Default view, --json and --ownership only')
413
413
  .action(withErrorHandling(async (options) => {
414
414
  await statusCommand(getProjectRoot(), options);
415
415
  }));
@@ -3,8 +3,7 @@
3
3
  * runs `furnace apply` over the whole `components/` tree, so a
4
4
  * `fireforge test --build` meant to capture the committed behaviour could
5
5
  * silently ship component edits nobody had deployed yet (another session's
6
- * included), and nothing in the test output said so. The applied names now
7
- * ride the verdict line, and `--no-furnace-apply` packages `engine/` as it
6
+ * included). The applied names therefore ride the verdict line, and `--no-furnace-apply` packages `engine/` as it
8
7
  * stands, naming the undeployed edits it left out.
9
8
  */
10
9
  import type { BuildPreparation } from '../core/build-prepare.js';
@@ -4,8 +4,7 @@
4
4
  * runs `furnace apply` over the whole `components/` tree, so a
5
5
  * `fireforge test --build` meant to capture the committed behaviour could
6
6
  * silently ship component edits nobody had deployed yet (another session's
7
- * included), and nothing in the test output said so. The applied names now
8
- * ride the verdict line, and `--no-furnace-apply` packages `engine/` as it
7
+ * included). The applied names therefore ride the verdict line, and `--no-furnace-apply` packages `engine/` as it
9
8
  * stands, naming the undeployed edits it left out.
10
9
  */
11
10
  import { listStaleFurnaceComponents } from '../core/furnace-staleness.js';
@@ -3,6 +3,7 @@
3
3
  * the per-file line budget): the incremental protected mach build with
4
4
  * its baseline write, and the `--build-only` union-build shape.
5
5
  */
6
+ import type { ControlBuildStamp } from '../core/build-baseline-types.js';
6
7
  import type { TestOptions } from '../types/commands/index.js';
7
8
  import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
8
9
  import type { HarnessClassification } from './test-modes.js';
@@ -18,4 +19,11 @@ export declare function runTestBuildPhase(projectRoot: string, paths: ProjectPat
18
19
  normalizedPaths: string[];
19
20
  /** Forces a full pre-test build (a compiled negative control). */
20
21
  fullBuildReason?: string;
22
+ /** Recorded on the pre-test build's baseline (a compiled negative control). */
23
+ controlBuild?: ControlBuildStamp;
24
+ /**
25
+ * Engine-relative launchable binary: anchors the stale-compiled check,
26
+ * which refuses a build-less run and escalates a `--build` one.
27
+ */
28
+ launchablePath?: string | undefined;
21
29
  }): Promise<boolean>;
@@ -7,10 +7,12 @@
7
7
  import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
8
8
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
9
9
  import { applyMozconfigAnchor, formatCoverageRetentionNotice, formatExtendCoverageRefusal, planCoverageRetention, recordScopedCoverage, } from '../core/coverage-extend.js';
10
+ import { normalizeRequestPath } from '../core/coverage-records.js';
10
11
  import { hasBuildArtifacts } from '../core/mach.js';
11
12
  import { runProtectedMachBuild, withBuildLock } from '../core/mach.js';
12
13
  import { assertHealthyPartialConfig, pruneDanglingTestLinks } from '../core/objdir-maintenance.js';
13
14
  import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
15
+ import { findCompiledSourcesNewerThanBinary, formatStaleCompiledBuildNotice, } from '../core/test-stale-check.js';
14
16
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
15
17
  import { BuildError } from '../errors/build.js';
16
18
  import { toError } from '../utils/errors.js';
@@ -27,7 +29,7 @@ import { emitPassVerdict } from './test-verdict.js';
27
29
  * @param options - See {@link RunPreTestBuildOptions}
28
30
  */
29
31
  async function runPreTestBuild(options) {
30
- const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, furnace, fullBuildReason, } = options;
32
+ const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, furnace, fullBuildReason, controlBuild, } = options;
31
33
  await withBuildLock(projectRoot, async () => {
32
34
  // Pass the previous baseline exactly like `fireforge build` does, so
33
35
  // auto-configure runs under the same conditions on both paths. The
@@ -121,6 +123,7 @@ async function runPreTestBuild(options) {
121
123
  ? 'carry-forward'
122
124
  : 'auto',
123
125
  buildKind,
126
+ controlBuild,
124
127
  });
125
128
  }
126
129
  catch (baselineError) {
@@ -200,7 +203,7 @@ function describeBuildInvocation(extending, coverage) {
200
203
  * next steps, and emits the run's single `PASS` verdict.
201
204
  */
202
205
  export async function runTestBuildPhase(projectRoot, paths, projectConfig, harnessRetries, options, request) {
203
- const { classification, normalizedPaths, fullBuildReason } = request;
206
+ const { classification, normalizedPaths, controlBuild, launchablePath } = request;
204
207
  const extending = options.extendCoverage === true;
205
208
  if (extending) {
206
209
  assertExtendCoverageUsage(options, normalizedPaths);
@@ -213,19 +216,28 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
213
216
  // rebuild also cannot regenerate the compiled StaticComponents table,
214
217
  // so it runs the components.conf gate up-front (the path-less shape
215
218
  // refreshes the anchor itself and skips it).
216
- const coverage = normalizedPaths.length === 0 ? 'full' : normalizedPaths;
217
- if (normalizedPaths.length > 0) {
219
+ // Coverage paths are recorded normalized (no trailing slash, no `./`):
220
+ // the baseline validator refuses the raw `dir/` CLI shape, which would
221
+ // block every later plain `fireforge test`. A request
222
+ // for the engine root (`.`) packages everything, so it records `'full'`.
223
+ const coveragePaths = normalizedPaths.map(normalizeRequestPath);
224
+ const coverage = coveragePaths.length === 0 || coveragePaths.includes('')
225
+ ? 'full'
226
+ : [...new Set(coveragePaths)];
227
+ if (coverage !== 'full') {
218
228
  const previousBaseline = await readBuildBaseline(projectRoot);
219
229
  await enforceStaticComponentsGate(paths.engine, previousBaseline, options);
220
230
  if (extending) {
221
231
  // Staging-input half of the plan, before the build lock: a refusal
222
232
  // here costs neither build time nor the lock.
223
- const plan = await planCoverageRetention(paths.engine, previousBaseline, normalizedPaths);
233
+ const plan = await planCoverageRetention(paths.engine, previousBaseline, coverage);
224
234
  if (plan.dropped.length > 0) {
225
235
  throw new GeneralError(formatExtendCoverageRefusal(plan.dropped));
226
236
  }
227
237
  }
228
238
  }
239
+ const fullBuildReason = request.fullBuildReason ??
240
+ (await describeCompiledEscalation(projectRoot, paths.engine, launchablePath));
229
241
  await runPreTestBuild({
230
242
  projectRoot,
231
243
  paths,
@@ -233,8 +245,9 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
233
245
  harnessRetries,
234
246
  testPackagingCoverage: coverage,
235
247
  refuseUnexportedDrift: options.refuseUnexportedDrift === true,
236
- extend: extending ? { requestedPaths: normalizedPaths } : undefined,
248
+ extend: extending && coverage !== 'full' ? { requestedPaths: coverage } : undefined,
237
249
  fullBuildReason,
250
+ controlBuild,
238
251
  furnace: await planPreTestFurnace(projectRoot, paths, projectConfig, options, normalizedPaths),
239
252
  });
240
253
  info('');
@@ -249,7 +262,29 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
249
262
  }
250
263
  }
251
264
  else {
252
- await enforceStaleBuildGate(projectRoot, paths.engine, options, normalizedPaths);
265
+ await enforceStaleBuildGate(projectRoot, paths.engine, options, normalizedPaths, {
266
+ launchablePath,
267
+ });
253
268
  }
254
269
  return false;
255
270
  }
271
+ /**
272
+ * Why a `--build` run's pre-test build must be a full `mach build` although
273
+ * nothing asked for one: compiled sources changed after the binary was
274
+ * linked, or the binary still carries a negative control a killed run (or a
275
+ * failed restore build) left compiled in. `mach build faster` compiles
276
+ * nothing, so either would otherwise survive the build.
277
+ */
278
+ async function describeCompiledEscalation(projectRoot, engineDir, launchablePath) {
279
+ const baseline = await readBuildBaseline(projectRoot);
280
+ const stale = await findCompiledSourcesNewerThanBinary(engineDir, baseline, launchablePath);
281
+ if (stale !== undefined && stale.paths.length > 0)
282
+ return formatStaleCompiledBuildNotice(stale);
283
+ const stamp = baseline?.controlBuild;
284
+ if (stamp !== undefined) {
285
+ return (`The binary was built with the negative control ${stamp.diffFile} applied and not ` +
286
+ 'rebuilt since; escalating this pre-test build to a full mach build so it recompiles ' +
287
+ 'the restored source.');
288
+ }
289
+ return undefined;
290
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The restore build after a compiled negative control.
3
+ *
4
+ * A control on compiled source is applied before a full pre-test build, so
5
+ * the binary under test carries it. Restoring the source byte-exact after
6
+ * the run does not unbuild it: the objdir binary keeps the mutation until a
7
+ * build recompiles the original. Every later `fireforge test` would then
8
+ * test the control. The orchestrator therefore runs one more full
9
+ * `mach build` right after a successful restore, still under the same
10
+ * engine-session hold, and its baseline drops the `controlBuild` stamp the
11
+ * control's build recorded. When that build fails, or the run never gets
12
+ * here (killed), the stamp stays and the next `fireforge test` refuses with
13
+ * `note=control-built` until `fireforge build` replaces it.
14
+ */
15
+ import type { ControlBuildStamp } from '../core/build-baseline-types.js';
16
+ /** Outcome of {@link runControlRestoreBuild}, stamped as `control-restore-build=`. */
17
+ export type ControlRestoreBuildOutcome = 'done' | 'failed';
18
+ /**
19
+ * Whether the recorded baseline is the one this run's control build wrote:
20
+ * only then did a build compile the control in (a failed control build
21
+ * writes no baseline, and its restored source's fresh mtime trips the
22
+ * stale-compiled check instead).
23
+ *
24
+ * @param projectRoot - Project root
25
+ * @param stamp - The stamp this run's control build recorded
26
+ */
27
+ export declare function controlBuildRecorded(projectRoot: string, stamp: ControlBuildStamp): Promise<boolean>;
28
+ /**
29
+ * Rebuilds the binary after a compiled control was restored: a full
30
+ * protected `mach build` under the build lock, then a baseline without the
31
+ * stamp. The previous baseline's packaging-coverage claim is carried
32
+ * forward unchanged, since recompiling the restored source stages no tests.
33
+ * Never throws: a failure leaves the stamp in place, which is the refusal
34
+ * the next run needs.
35
+ *
36
+ * @param projectRoot - Project root
37
+ * @param stamp - The stamp this run's control build recorded
38
+ * @param harnessRetries - Recognized-crash retry budget for the build
39
+ */
40
+ export declare function runControlRestoreBuild(projectRoot: string, stamp: ControlBuildStamp, harnessRetries: number | undefined): Promise<ControlRestoreBuildOutcome>;