@hominis/fireforge 0.50.0 → 0.52.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 (212) hide show
  1. package/CHANGELOG.md +67 -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/status-output.js +5 -1
  63. package/dist/src/commands/test-build-furnace.d.ts +3 -2
  64. package/dist/src/commands/test-build-furnace.js +8 -2
  65. package/dist/src/commands/test-build-phase.d.ts +2 -0
  66. package/dist/src/commands/test-build-phase.js +34 -17
  67. package/dist/src/commands/test-checkjs.js +3 -2
  68. package/dist/src/commands/test-dispatch.d.ts +39 -0
  69. package/dist/src/commands/test-dispatch.js +220 -0
  70. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  71. package/dist/src/commands/test-furnace-reach.js +132 -0
  72. package/dist/src/commands/test-orchestrate.d.ts +14 -0
  73. package/dist/src/commands/test-orchestrate.js +25 -4
  74. package/dist/src/commands/test-perf-scope.d.ts +21 -0
  75. package/dist/src/commands/test-perf-scope.js +55 -0
  76. package/dist/src/commands/test-prelock.js +100 -4
  77. package/dist/src/commands/test-register.js +18 -3
  78. package/dist/src/commands/test-results.d.ts +62 -0
  79. package/dist/src/commands/test-results.js +62 -0
  80. package/dist/src/commands/test-run.d.ts +13 -55
  81. package/dist/src/commands/test-run.js +73 -101
  82. package/dist/src/commands/test-shards.d.ts +110 -0
  83. package/dist/src/commands/test-shards.js +255 -0
  84. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  85. package/dist/src/commands/test-stale-gate.js +11 -2
  86. package/dist/src/commands/test-verdict.d.ts +2 -1
  87. package/dist/src/commands/test.d.ts +1 -1
  88. package/dist/src/commands/test.js +68 -75
  89. package/dist/src/commands/token-lint.js +48 -8
  90. package/dist/src/commands/verify.js +9 -2
  91. package/dist/src/core/build-baseline-types.d.ts +13 -0
  92. package/dist/src/core/build-baseline-validate.js +9 -0
  93. package/dist/src/core/build-baseline.js +7 -0
  94. package/dist/src/core/build-prepare.d.ts +6 -0
  95. package/dist/src/core/build-prepare.js +29 -3
  96. package/dist/src/core/config-paths.d.ts +1 -1
  97. package/dist/src/core/config-paths.js +2 -0
  98. package/dist/src/core/config-validate-check.js +83 -7
  99. package/dist/src/core/config-validate-test-toolchains.js +14 -0
  100. package/dist/src/core/config-validate-token-scales.d.ts +3 -0
  101. package/dist/src/core/config-validate-token-scales.js +47 -5
  102. package/dist/src/core/config-validate.js +6 -0
  103. package/dist/src/core/coverage-extend.js +2 -2
  104. package/dist/src/core/coverage-records.d.ts +11 -0
  105. package/dist/src/core/coverage-records.js +34 -0
  106. package/dist/src/core/engine-session-lock.d.ts +6 -0
  107. package/dist/src/core/engine-session-lock.js +10 -2
  108. package/dist/src/core/file-lock-shared.d.ts +57 -0
  109. package/dist/src/core/file-lock-shared.js +117 -0
  110. package/dist/src/core/file-lock.d.ts +9 -0
  111. package/dist/src/core/file-lock.js +58 -2
  112. package/dist/src/core/furnace-apply.js +10 -10
  113. package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
  114. package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
  115. package/dist/src/core/furnace-config-validate.js +7 -1
  116. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  117. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  118. package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
  119. package/dist/src/core/furnace-validate-accessibility.js +75 -30
  120. package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
  121. package/dist/src/core/furnace-validate-exceptions.js +59 -4
  122. package/dist/src/core/furnace-validate-icon-button.js +30 -1
  123. package/dist/src/core/history-log-file.js +8 -1
  124. package/dist/src/core/line-prefixer.d.ts +18 -0
  125. package/dist/src/core/line-prefixer.js +34 -0
  126. package/dist/src/core/mach.d.ts +7 -0
  127. package/dist/src/core/mach.js +24 -9
  128. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  129. package/dist/src/core/module-resolution-preflight.js +24 -6
  130. package/dist/src/core/negative-control-compiled.d.ts +36 -0
  131. package/dist/src/core/negative-control-compiled.js +87 -0
  132. package/dist/src/core/negative-control-diff.d.ts +14 -2
  133. package/dist/src/core/negative-control-diff.js +49 -6
  134. package/dist/src/core/negative-control.d.ts +12 -0
  135. package/dist/src/core/negative-control.js +48 -5
  136. package/dist/src/core/park-store.d.ts +63 -0
  137. package/dist/src/core/park-store.js +216 -0
  138. package/dist/src/core/patch-export-update.d.ts +4 -0
  139. package/dist/src/core/patch-export-update.js +12 -3
  140. package/dist/src/core/patch-export.d.ts +3 -2
  141. package/dist/src/core/patch-export.js +21 -14
  142. package/dist/src/core/patch-files.js +26 -8
  143. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
  144. package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
  145. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  146. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  147. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  148. package/dist/src/core/patch-lint-checkjs.js +13 -3
  149. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  150. package/dist/src/core/patch-lint-cross.js +5 -0
  151. package/dist/src/core/patch-lint-css.js +14 -10
  152. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
  153. package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
  154. package/dist/src/core/patch-lint-ignore.d.ts +56 -12
  155. package/dist/src/core/patch-lint-ignore.js +173 -43
  156. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  157. package/dist/src/core/patch-lint-ownership.js +26 -0
  158. package/dist/src/core/patch-lint-size.js +9 -1
  159. package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
  160. package/dist/src/core/patch-lint-token-scale.js +206 -29
  161. package/dist/src/core/patch-lint.js +2 -2
  162. package/dist/src/core/patch-manifest-io.js +8 -1
  163. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  164. package/dist/src/core/patch-manifest-key-order.js +61 -0
  165. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  166. package/dist/src/core/patch-manifest-stage.js +96 -0
  167. package/dist/src/core/patch-manifest-validate.js +27 -1
  168. package/dist/src/core/patch-order-legal.d.ts +2 -1
  169. package/dist/src/core/patch-order-legal.js +7 -2
  170. package/dist/src/core/patch-policy.d.ts +6 -2
  171. package/dist/src/core/patch-policy.js +14 -7
  172. package/dist/src/core/patch-retired.d.ts +26 -4
  173. package/dist/src/core/patch-retired.js +40 -2
  174. package/dist/src/core/run-log.d.ts +6 -0
  175. package/dist/src/core/run-log.js +17 -1
  176. package/dist/src/core/test-harness-crash.d.ts +2 -1
  177. package/dist/src/core/test-harness-crash.js +5 -8
  178. package/dist/src/core/test-harness-signature.d.ts +16 -0
  179. package/dist/src/core/test-harness-signature.js +19 -1
  180. package/dist/src/core/test-raw-log.d.ts +66 -0
  181. package/dist/src/core/test-raw-log.js +122 -0
  182. package/dist/src/core/test-run-completeness.d.ts +47 -7
  183. package/dist/src/core/test-run-completeness.js +70 -23
  184. package/dist/src/core/test-stale-check.d.ts +10 -1
  185. package/dist/src/core/test-stale-check.js +15 -1
  186. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  187. package/dist/src/core/test-xpcshell-retry.js +2 -1
  188. package/dist/src/core/token-lint.d.ts +5 -2
  189. package/dist/src/core/token-lint.js +8 -3
  190. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  191. package/dist/src/core/token-sibling-sheets.js +119 -0
  192. package/dist/src/core/tree-guard.js +2 -0
  193. package/dist/src/core/typecheck-shim.d.ts +6 -1
  194. package/dist/src/core/typecheck-shim.js +139 -2
  195. package/dist/src/core/typecheck.js +3 -1
  196. package/dist/src/index.d.ts +9 -3
  197. package/dist/src/index.js +4 -1
  198. package/dist/src/types/commands/index.d.ts +2 -2
  199. package/dist/src/types/commands/options.d.ts +68 -7
  200. package/dist/src/types/commands/patches.d.ts +43 -0
  201. package/dist/src/types/config.d.ts +74 -2
  202. package/dist/src/types/furnace.d.ts +29 -0
  203. package/dist/src/types/index.d.ts +1 -1
  204. package/dist/src/utils/options.d.ts +3 -2
  205. package/dist/src/utils/options.js +9 -4
  206. package/docs/configuration.md +110 -9
  207. package/docs/furnace.md +61 -1
  208. package/docs/lifecycle-invariants.md +13 -11
  209. package/docs/machine-output.md +68 -18
  210. package/docs/patch-workflow.md +174 -13
  211. package/docs/testing.md +89 -5
  212. package/package.json +3 -3
@@ -13,6 +13,7 @@ import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
13
13
  import { stagedDependencyDelta } from './staged-dependency-batch.js';
14
14
  import { applyMode, countStagedEntries, dependencyLabel, importView, registrationView, } from './staged-dependency-entries.js';
15
15
  import { runBatchAdd } from './staged-dependency-from.js';
16
+ import { selectRemovalEntries } from './staged-dependency-remove.js';
16
17
  import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
17
18
  /** Joins the names of the missing flags for error attribution. */
18
19
  function missingFlagList(pairs) {
@@ -85,90 +86,27 @@ function requireRegistrationOptions(options) {
85
86
  dependency.reason = options.reason;
86
87
  return dependency;
87
88
  }
88
- /**
89
- * Resolves the `--remove` target for forward-imports:
90
- * `--file` + `--specifier` suffice when they identify at most one staged
91
- * entry. `--creates` is inferred from a unique match, an ambiguous match
92
- * refuses with the candidate list, and no match falls through to the
93
- * honest "no staged forward-import matched" summary.
94
- */
95
- function resolveImportRemovalTarget(options, staged) {
96
- if (!options.file || !options.specifier) {
97
- throw new InvalidArgumentError('--remove requires --file and --specifier; --creates is only needed to disambiguate when several staged entries share them.', missingFlagList([
98
- ['--file', options.file],
99
- ['--specifier', options.specifier],
100
- ]));
101
- }
102
- if (options.creates) {
103
- return requireForwardImportOptions(options);
104
- }
105
- const { file, specifier } = options;
106
- const candidates = (staged?.forwardImports ?? []).filter((entry) => entry.file === file &&
107
- entry.specifier === specifier &&
108
- (options.owner === undefined || entry.owner === options.owner));
109
- const single = candidates.length === 1 ? candidates[0] : undefined;
110
- if (single !== undefined) {
111
- info(`Inferred --creates ${single.creates} from the single matching staged entry.`);
112
- return { ...single };
113
- }
114
- if (candidates.length > 1) {
115
- const list = candidates
116
- .map((entry) => ` - ${dependencyLabel('import', importView(entry))}`)
117
- .join('\n');
118
- throw new GeneralError(`--remove matches ${candidates.length} staged forward-imports on this patch; ` +
119
- `pass --creates to pick one:\n${list}`);
120
- }
121
- return { file, specifier, creates: '(no match)' };
122
- }
123
- /** Registration twin of {@link resolveImportRemovalTarget}, keyed on `--line`. */
124
- function resolveRegistrationRemovalTarget(options, staged) {
125
- if (!options.file || !options.line) {
126
- throw new InvalidArgumentError('--remove --kind registration requires --file and --line; --creates is only needed to disambiguate when several staged entries share them.', missingFlagList([
127
- ['--file', options.file],
128
- ['--line', options.line],
129
- ]));
130
- }
131
- if (options.creates) {
132
- return requireRegistrationOptions(options);
133
- }
134
- const { file, line } = options;
135
- const candidates = (staged?.registrations ?? []).filter((entry) => entry.file === file &&
136
- entry.line === line &&
137
- (options.owner === undefined || entry.owner === options.owner));
138
- const single = candidates.length === 1 ? candidates[0] : undefined;
139
- if (single !== undefined) {
140
- info(`Inferred --creates ${single.creates} from the single matching staged entry.`);
141
- return { ...single };
142
- }
143
- if (candidates.length > 1) {
144
- const list = candidates
145
- .map((entry) => ` - ${dependencyLabel('registration', registrationView(entry))}`)
146
- .join('\n');
147
- throw new GeneralError(`--remove matches ${candidates.length} staged registrations on this patch; ` +
148
- `pass --creates to pick one:\n${list}`);
149
- }
150
- return { file, line, creates: '(no match)' };
151
- }
152
89
  /**
153
90
  * Renders a one-line summary of a staged-dependency metadata change.
154
91
  */
155
- function describeStagedDependencyChange(beforeCount, afterCount, mode, kind, target) {
92
+ function describeStagedDependencyChange(beforeCount, afterCount, mode, kind, targets) {
156
93
  const noun = kind === 'import' ? 'staged forward-import' : 'staged registration';
157
94
  if (mode === 'clear') {
158
95
  return beforeCount === 0
159
96
  ? 'stagedDependencies was already empty — no change'
160
97
  : `cleared ${beforeCount} staged dependency declaration(s)`;
161
98
  }
162
- if (!target)
99
+ const label = targets.map((target) => dependencyLabel(kind, target)).join('; ');
100
+ if (targets.length === 0)
163
101
  return 'stagedDependencies unchanged';
164
102
  if (mode === 'add') {
165
103
  return afterCount === beforeCount
166
- ? `${noun} already present: ${dependencyLabel(kind, target)}`
167
- : `added ${noun}: ${dependencyLabel(kind, target)}`;
104
+ ? `${noun} already present: ${label}`
105
+ : `added ${noun}: ${label}`;
168
106
  }
169
107
  return afterCount === beforeCount
170
- ? `no ${noun} matched: ${dependencyLabel(kind, target)}`
171
- : `removed ${beforeCount - afterCount} ${noun} declaration(s): ${dependencyLabel(kind, target)}`;
108
+ ? `no ${noun} matched: ${label}`
109
+ : `removed ${beforeCount - afterCount} ${noun} declaration(s): ${label}`;
172
110
  }
173
111
  /**
174
112
  * Runs the metadata-only staged-dependency mutation command.
@@ -191,35 +129,34 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
191
129
  // entries.
192
130
  const { paths, manifest } = await requirePatchQueue(projectRoot);
193
131
  const targetPatch = requirePatchTarget(identifier, manifest.patches);
194
- const importDependency = mode === 'clear' || kind !== 'import'
195
- ? undefined
196
- : mode === 'add'
197
- ? requireForwardImportOptions(options)
198
- : resolveImportRemovalTarget(options, targetPatch.stagedDependencies);
199
- const registrationDependency = mode === 'clear' || kind !== 'registration'
200
- ? undefined
201
- : mode === 'add'
202
- ? requireRegistrationOptions(options)
203
- : resolveRegistrationRemovalTarget(options, targetPatch.stagedDependencies);
132
+ const importDependency = mode === 'add' && kind === 'import' ? requireForwardImportOptions(options) : undefined;
133
+ const registrationDependency = mode === 'add' && kind === 'registration' ? requireRegistrationOptions(options) : undefined;
204
134
  // Shape-check --add declarations against the loaded queue: a
205
135
  // patch-name-shaped --creates/--file is refused now instead of surfacing
206
136
  // later as an undischargeable staged-dependency-unused.
207
- if (mode === 'add') {
208
- const added = importDependency ?? registrationDependency;
209
- if (added !== undefined) {
210
- validateStagedDependencyAdd(added, manifest.patches);
211
- }
212
- }
213
- const target = importDependency !== undefined
214
- ? importView(importDependency)
215
- : registrationDependency !== undefined
216
- ? registrationView(registrationDependency)
217
- : undefined;
137
+ const added = importDependency ?? registrationDependency;
138
+ if (added !== undefined) {
139
+ validateStagedDependencyAdd(added, manifest.patches);
140
+ }
141
+ const targets = mode === 'remove'
142
+ ? selectRemovalEntries(kind, options, targetPatch.stagedDependencies)
143
+ : importDependency !== undefined
144
+ ? [importView(importDependency)]
145
+ : registrationDependency !== undefined
146
+ ? [registrationView(registrationDependency)]
147
+ : [];
148
+ const applyKind = (entries, own, toView, addition) => {
149
+ if (!own)
150
+ return [...entries];
151
+ if (mode !== 'remove')
152
+ return applyMode(entries, mode, targets[0], toView, addition);
153
+ return targets.reduce((remaining, target) => applyMode(remaining, 'remove', target, toView, undefined), [...entries]);
154
+ };
218
155
  const applyToStaged = (staged) => {
219
156
  if (mode === 'clear')
220
157
  return {};
221
- const forwardImports = applyMode(staged?.forwardImports ?? [], kind === 'import' ? mode : 'add', kind === 'import' ? target : undefined, importView, importDependency);
222
- const registrations = applyMode(staged?.registrations ?? [], kind === 'registration' ? mode : 'add', kind === 'registration' ? target : undefined, registrationView, registrationDependency);
158
+ const forwardImports = applyKind(staged?.forwardImports ?? [], kind === 'import', importView, importDependency);
159
+ const registrations = applyKind(staged?.registrations ?? [], kind === 'registration', registrationView, registrationDependency);
223
160
  return {
224
161
  ...(forwardImports.length > 0 ? { forwardImports } : {}),
225
162
  ...(registrations.length > 0 ? { registrations } : {}),
@@ -227,7 +164,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
227
164
  };
228
165
  const existing = targetPatch.stagedDependencies;
229
166
  const projected = applyToStaged(existing);
230
- const summary = describeStagedDependencyChange(countStagedEntries(existing), countStagedEntries(projected), mode, kind, target);
167
+ const summary = describeStagedDependencyChange(countStagedEntries(existing), countStagedEntries(projected), mode, kind, targets);
231
168
  if (isDryRun) {
232
169
  info(`[dry-run] ${targetPatch.filename}: ${summary}.`);
233
170
  outro('Dry run complete — no changes made');
@@ -247,7 +184,12 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
247
184
  }
248
185
  const before = result.before.stagedDependencies;
249
186
  const after = result.after.stagedDependencies;
250
- info(`${targetPatch.filename}: ${describeStagedDependencyChange(countStagedEntries(before), countStagedEntries(after), mode, kind, target)}.`);
187
+ info(`${targetPatch.filename}: ${describeStagedDependencyChange(countStagedEntries(before), countStagedEntries(after), mode, kind, targets)}.`);
188
+ if (mode === 'remove' && countStagedEntries(after) === countStagedEntries(before)) {
189
+ // The selection matched when the queue was read; a peer removed the
190
+ // entries before the locked write. Still a miss, so still a refusal.
191
+ throw new InvalidArgumentError(`${targetPatch.filename}: the selected staged declaration(s) were already gone when the manifest was written.`, '--remove');
192
+ }
251
193
  await appendHistoryBestEffort(paths.patches, {
252
194
  operation: 'patch-staged-dependency',
253
195
  // Only the delta: the whole list on both sides grew the log
@@ -275,13 +217,13 @@ export function registerPatchStagedDependency(parent, context) {
275
217
  .command('staged-dependency <name>')
276
218
  .description('Edit PatchMetadata.stagedDependencies on a single patch (no .patch body rewrite).')
277
219
  .option('--add', 'Add a staged dependency declaration')
278
- .option('--remove', 'Remove matching staged dependency declaration(s)')
220
+ .option('--remove', 'Remove matching staged dependency declaration(s); a selection that matches nothing exits 8')
279
221
  .option('--clear', 'Drop the stagedDependencies field entirely')
280
222
  .option('--kind <kind>', 'Declaration shape: "import" (forward import, the default) or "registration" (jar.mn packaging line, customElements or actor registration)')
281
223
  .option('--file <path>', 'Declaring file path relative to engine/')
282
224
  .option('--specifier <specifier>', 'Exact import specifier as it appears in source (--kind import)')
283
- .option('--line <text>', 'Registration/packaging line as the patch adds it, compared whitespace-trimmed (--kind registration)')
284
- .option('--creates <path>', 'Later-created file path relative to engine/')
225
+ .option('--line <text>', 'Registration/packaging line as the patch adds it, compared whitespace-insensitively (--kind registration)')
226
+ .option('--creates <path>', 'Later-created file path relative to engine/ (with --remove and no --file: removes every declaration for that path)')
285
227
  .option('--owner <patch>', 'Exact later patch filename expected to create --creates')
286
228
  .option('--reason <text>', 'Human-readable rationale stored with the declaration (with --from: the default for entries without one)')
287
229
  .option('--from <json-file>', 'Add every declaration in a JSON file under one lock hold (--add only): a "lint --per-patch --report" ' +
@@ -93,6 +93,11 @@ export declare function reportAdjacentUnmanagedFiles(args: {
93
93
  manifest: PatchesManifest;
94
94
  currentFilesAffected: readonly string[];
95
95
  ctx: AdjacentUnmanagedContext;
96
+ /**
97
+ * Set beside `--scan`: the scan has already reconciled missing files and
98
+ * adopted what it was asked to, so only the neighbours it left are judged.
99
+ */
100
+ afterScan?: boolean;
96
101
  }): Promise<boolean>;
97
102
  /**
98
103
  * End-of-run accounting for `--expect-new` and `--adjacent-ignore`.
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Scan-less re-export adjacency advisory.
3
+ * Re-export adjacency advisory.
4
4
  *
5
5
  * A plain `re-export` keeps the manifest's filesAffected unchanged, so a
6
6
  * brand-new file an author just created beside a patch's owned files is
@@ -8,7 +8,8 @@
8
8
  * files, filters out tool-managed paths (branding, furnace) so the notice
9
9
  * names only genuinely unmanaged candidates, and, under
10
10
  * `--refuse-adjacent-unmanaged`, lets the caller turn the notice into a
11
- * non-zero refusal.
11
+ * non-zero refusal. Beside `--scan` it judges the files the scan did not
12
+ * adopt, so a targeted `--scan-file` adoption keeps the belt.
12
13
  *
13
14
  * Split out of re-export.ts to keep that file inside the max-lines budget.
14
15
  * Exports helpers consumed by re-export.ts, no registrar.
@@ -197,7 +198,7 @@ function reportAdmitted(patchFilename, groups) {
197
198
  */
198
199
  export async function reportAdjacentUnmanagedFiles(args) {
199
200
  const { patch, paths, manifest, currentFilesAffected, ctx } = args;
200
- const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
201
+ const missingFiles = args.afterScan === true ? [] : await findMissingFiles(paths.engine, currentFilesAffected);
201
202
  if (missingFiles.length > 0) {
202
203
  warn(`${patch.filename}: some files in patches.json no longer exist on disk ` +
203
204
  `(${missingFiles.join(', ')}). Without --scan, re-export keeps the manifest's ` +
@@ -1,4 +1,5 @@
1
1
  import type { PatchesManifest } from '../types/commands/index.js';
2
+ import { type DryRunPurityScope } from './re-export-purity-scope.js';
2
3
  /** Loads and validates a `re-export --scan-files` assignment manifest. */
3
4
  export declare function loadScanFilesAssignments(manifestPath: string, manifest: PatchesManifest): Promise<Map<string, string[]>>;
4
5
  /**
@@ -13,6 +14,6 @@ export declare function loadScanFilesAssignments(manifestPath: string, manifest:
13
14
  * violation surfaces the violation with the original error as `cause`.
14
15
  * No-op outside dry-run.
15
16
  */
16
- export declare function withDryRunPurityGuard<T>(engineDir: string, patchesDir: string, isDryRun: boolean, operation: () => Promise<T>): Promise<T>;
17
+ export declare function withDryRunPurityGuard<T>(engineDir: string, patchesDir: string, isDryRun: boolean, operation: () => Promise<T>, scope?: DryRunPurityScope): Promise<T>;
17
18
  /** Serializes dry-run re-export git inspection against the same project tree. */
18
19
  export declare function withDryRunReExportLock<T>(fireforgeDir: string, isDryRun: boolean, operation: () => Promise<T>): Promise<T>;
@@ -11,6 +11,7 @@ import { getNodeErrorCode, toError } from '../utils/errors.js';
11
11
  import { readText } from '../utils/fs.js';
12
12
  import { sha256Hex } from '../utils/hash.js';
13
13
  import { isObject } from '../utils/validation.js';
14
+ import { describeForeignWriters, fingerprintScopedDryRunState, } from './re-export-purity-scope.js';
14
15
  import { normalizeEngineRelativeInput } from './re-export-scan.js';
15
16
  /** Concurrency bound for patch-body hashing (matches the classify/lint pools). */
16
17
  const PATCH_HASH_CONCURRENCY = 8;
@@ -161,10 +162,14 @@ async function fingerprintDryRunState(engineDir, patchesDir) {
161
162
  * violation surfaces the violation with the original error as `cause`.
162
163
  * No-op outside dry-run.
163
164
  */
164
- export async function withDryRunPurityGuard(engineDir, patchesDir, isDryRun, operation) {
165
+ export async function withDryRunPurityGuard(engineDir, patchesDir, isDryRun, operation, scope) {
165
166
  if (!isDryRun)
166
167
  return operation();
167
- const before = await fingerprintDryRunState(engineDir, patchesDir);
168
+ const fingerprint = () => scope === undefined
169
+ ? fingerprintDryRunState(engineDir, patchesDir)
170
+ : fingerprintScopedDryRunState(engineDir, patchesDir, scope);
171
+ const startedAt = Date.now();
172
+ const before = await fingerprint();
168
173
  let result;
169
174
  let operationError;
170
175
  let failed = false;
@@ -177,7 +182,7 @@ export async function withDryRunPurityGuard(engineDir, patchesDir, isDryRun, ope
177
182
  }
178
183
  let after;
179
184
  try {
180
- after = await fingerprintDryRunState(engineDir, patchesDir);
185
+ after = await fingerprint();
181
186
  }
182
187
  catch (fingerprintError) {
183
188
  // The post-run fingerprint failing must not swallow the dry run's own
@@ -195,10 +200,20 @@ export async function withDryRunPurityGuard(engineDir, patchesDir, isDryRun, ope
195
200
  .sort()
196
201
  .filter((key) => before.get(key) !== after.get(key));
197
202
  if (touched.length > 0) {
198
- throw new GeneralError(`[dry-run] invariant violated: this dry run modified ${touched.join(', ')}. ` +
199
- 'This is a FireForge bug — please report it, and inspect the listed state ' +
200
- '(e.g. `git status` in engine/ and your patches/ diff) before trusting it.' +
201
- (failed ? ` The dry run also failed: ${toError(operationError).message}` : ''), operationError);
203
+ const also = failed ? ` The dry run also failed: ${toError(operationError).message}` : '';
204
+ const writers = await describeForeignWriters(patchesDir, startedAt);
205
+ if (writers.length > 0) {
206
+ throw new GeneralError(`[dry-run] ${touched.join(', ')} changed during this dry run, written by another ` +
207
+ `writer: ${writers.join('; ')}. The preview raced that write; rerun it once the ` +
208
+ `writer is done.${also}`, operationError);
209
+ }
210
+ throw new GeneralError(`[dry-run] invariant violated: ${touched.join(', ')} changed during this dry run. No ` +
211
+ 'other FireForge command recorded a write in that window, so either another writer ' +
212
+ '(an editor, an agent or a git command in engine/) changed it, or this is a FireForge ' +
213
+ 'bug. Rerun the dry run; if it repeats on a quiet tree, please report it, and inspect ' +
214
+ 'the listed state (e.g. `git status` in engine/ and your patches/ diff) before ' +
215
+ 'trusting it.' +
216
+ also, operationError);
202
217
  }
203
218
  if (failed)
204
219
  throw operationError;
@@ -0,0 +1,35 @@
1
+ import { type ParsedHunk } from '../core/patch-parse.js';
2
+ /** Which hunk mode a re-export runs in, and the diff naming the hunks. */
3
+ export interface HunkSelection {
4
+ mode: 'include' | 'exclude';
5
+ diffPath: string;
6
+ flag: '--hunks-from' | '--exclude-hunks';
7
+ }
8
+ /** The selection a re-export's options ask for, if any. */
9
+ export declare function hunkSelectionOf(options: {
10
+ hunksFrom?: string;
11
+ excludeHunks?: string;
12
+ }): HunkSelection | undefined;
13
+ /** Splits a diff into its raw per-file sections, keyed by target path. */
14
+ export declare function splitRawSections(diff: string): Map<string, string>;
15
+ /**
16
+ * Applies `hunks` to `content` by context, forward or reversed. A hunk
17
+ * whose context is not found refuses, naming it.
18
+ */
19
+ export declare function applyHunksByContext(content: string, hunks: readonly ParsedHunk[], reverse: boolean, file: string): string;
20
+ /**
21
+ * Builds the re-export body for a hunk-selected run: named files from the
22
+ * selected hunks, the rest from the old body (`--hunks-from`) or the normal
23
+ * working-tree diff (`--exclude-hunks`).
24
+ *
25
+ * @param args.normalDiff - The diff a plain re-export would write
26
+ * @param args.oldBody - The patch's current body
27
+ * @param args.files - The files the refreshed patch owns
28
+ */
29
+ export declare function buildHunkSelectedDiff(args: {
30
+ engineDir: string;
31
+ selection: HunkSelection;
32
+ oldBody: string;
33
+ normalDiff: string;
34
+ files: readonly string[];
35
+ }): Promise<string>;
@@ -0,0 +1,220 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `re-export --hunks-from <diff>` and `--exclude-hunks <diff>`: export a
4
+ * file by hunk rather than as a whole.
5
+ *
6
+ * Re-export captures a file's working-tree content against engine HEAD, so
7
+ * on a shared engine a peer's unexported line in the same manifest or chrome
8
+ * document landed in the exporting patch, and the only way out was for the
9
+ * peer to park its hunk by hand. These two modes build the file's new
10
+ * content from the hunks a session names:
11
+ *
12
+ * - `--hunks-from`: the patch's current body for the file (HEAD with the
13
+ * old section applied) plus only the named hunks. Files the diff does
14
+ * not name keep their old section.
15
+ * - `--exclude-hunks`: the working tree minus the named hunks. Files the
16
+ * diff does not name export as usual.
17
+ *
18
+ * Hunks are placed by their context, searched outward from the line their
19
+ * header names, so a diff taken before a peer's edit above it still applies.
20
+ * The synthetic content is diffed against HEAD by git itself, on a
21
+ * throwaway index, so the section is byte-for-byte what a plain re-export
22
+ * would write for that content.
23
+ */
24
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
25
+ import { tmpdir } from 'node:os';
26
+ import { join } from 'node:path';
27
+ import { git } from '../core/git-base.js';
28
+ import { getFileContentAtRef, listTrackedInHead } from '../core/git-file-ops.js';
29
+ import { mintDisposableGitIndex } from '../core/git-readonly-index.js';
30
+ import { parseDiffGitHeader, parseDiffSections, } from '../core/patch-parse.js';
31
+ import { applyPatchTextToContent } from '../core/patch-transform.js';
32
+ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
33
+ import { PatchError } from '../errors/patch.js';
34
+ import { pathExists, readText } from '../utils/fs.js';
35
+ import { normalizeEngineRelativeInput } from './re-export-scan.js';
36
+ /** The selection a re-export's options ask for, if any. */
37
+ export function hunkSelectionOf(options) {
38
+ if (options.hunksFrom !== undefined) {
39
+ return { mode: 'include', diffPath: options.hunksFrom, flag: '--hunks-from' };
40
+ }
41
+ if (options.excludeHunks !== undefined) {
42
+ return { mode: 'exclude', diffPath: options.excludeHunks, flag: '--exclude-hunks' };
43
+ }
44
+ return undefined;
45
+ }
46
+ /** Splits a diff into its raw per-file sections, keyed by target path. */
47
+ export function splitRawSections(diff) {
48
+ const sections = new Map();
49
+ let path;
50
+ let lines = [];
51
+ const flush = () => {
52
+ if (path !== undefined)
53
+ sections.set(path, `${lines.join('\n')}\n`);
54
+ };
55
+ for (const line of diff.split('\n')) {
56
+ const header = parseDiffGitHeader(line);
57
+ if (header) {
58
+ flush();
59
+ path = header.targetPath;
60
+ lines = [line];
61
+ }
62
+ else if (path !== undefined) {
63
+ lines.push(line);
64
+ }
65
+ }
66
+ flush();
67
+ for (const [key, text] of sections)
68
+ sections.set(key, text.replace(/\n+$/, '\n'));
69
+ return sections;
70
+ }
71
+ /** The old and new line blocks a hunk replaces, swapped when reversed. */
72
+ function hunkBlocks(hunk, reverse) {
73
+ const oldLines = [];
74
+ const newLines = [];
75
+ for (const line of hunk.lines) {
76
+ const marker = line[0];
77
+ const text = line.slice(1);
78
+ if (marker === ' ' || marker === '-')
79
+ oldLines.push(text);
80
+ if (marker === ' ' || marker === '+')
81
+ newLines.push(text);
82
+ }
83
+ return reverse ? { from: newLines, to: oldLines } : { from: oldLines, to: newLines };
84
+ }
85
+ function matchesAt(lines, block, at) {
86
+ if (at < 0 || at + block.length > lines.length)
87
+ return false;
88
+ return block.every((line, index) => lines[at + index] === line);
89
+ }
90
+ /** The match of `block` nearest `expected`, at or after `floor`. */
91
+ function findBlock(lines, block, expected, floor) {
92
+ for (let distance = 0; distance <= lines.length; distance++) {
93
+ for (const at of distance === 0 ? [expected] : [expected - distance, expected + distance]) {
94
+ if (at >= floor && matchesAt(lines, block, at))
95
+ return at;
96
+ }
97
+ }
98
+ return undefined;
99
+ }
100
+ /**
101
+ * Applies `hunks` to `content` by context, forward or reversed. A hunk
102
+ * whose context is not found refuses, naming it.
103
+ */
104
+ export function applyHunksByContext(content, hunks, reverse, file) {
105
+ const endsWithNewline = content.endsWith('\n') || content === '';
106
+ const lines = content === '' ? [] : content.replace(/\n$/, '').split('\n');
107
+ let offset = 0;
108
+ let floor = 0;
109
+ for (const hunk of hunks) {
110
+ const { from, to } = hunkBlocks(hunk, reverse);
111
+ const start = (reverse ? hunk.newStart : hunk.oldStart) - (from.length === 0 ? 0 : 1);
112
+ const expected = Math.max(0, start + offset);
113
+ const at = from.length === 0 ? expected : findBlock(lines, from, expected, floor);
114
+ if (at === undefined) {
115
+ throw new PatchError(`The hunk @@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@ ` +
116
+ `does not ${reverse ? 'match the working tree' : 'apply to the patch body'} of ${file}: ` +
117
+ `its ${reverse ? 'new' : 'old'}-side lines were not found.`);
118
+ }
119
+ lines.splice(at, from.length, ...to);
120
+ offset += to.length - from.length + (at - expected);
121
+ floor = at + to.length;
122
+ }
123
+ if (lines.length === 0)
124
+ return '';
125
+ return `${lines.join('\n')}${endsWithNewline ? '\n' : ''}`;
126
+ }
127
+ /**
128
+ * Diffs `content` for `file` against engine HEAD the way a re-export would,
129
+ * on a throwaway index so the shared one never moves.
130
+ */
131
+ async function diffContentAgainstHead(engineDir, file, content, tracked) {
132
+ const index = await mintDisposableGitIndex(engineDir);
133
+ if (index === undefined) {
134
+ throw new GeneralError(`Cannot build a private git index in ${engineDir} to diff the hunk-selected ${file}.`);
135
+ }
136
+ const scratch = await mkdtemp(join(tmpdir(), 'fireforge-hunks-'));
137
+ try {
138
+ const temp = join(scratch, 'content');
139
+ await writeFile(temp, content);
140
+ const blob = (await git(['hash-object', '-w', '--path', file, '--', temp], engineDir)).trim();
141
+ const mode = tracked
142
+ ? ((await git(['ls-tree', 'HEAD', '--', file], engineDir)).split(/\s/)[0] ?? '100644')
143
+ : '100644';
144
+ await git(['update-index', '--add', '--cacheinfo', `${mode},${blob},${file}`], engineDir, {
145
+ env: index.env,
146
+ });
147
+ return await git(['diff', '--binary', '--cached', 'HEAD', '--', file], engineDir, {
148
+ env: index.env,
149
+ });
150
+ }
151
+ finally {
152
+ await rm(scratch, { recursive: true, force: true });
153
+ await index.dispose();
154
+ }
155
+ }
156
+ /** Reads and checks the hunk diff: text sections for files the patch owns. */
157
+ async function readSelection(selection, owned) {
158
+ if (!(await pathExists(selection.diffPath))) {
159
+ throw new InvalidArgumentError(`${selection.flag} ${selection.diffPath} does not exist.`, selection.flag);
160
+ }
161
+ const sections = parseDiffSections(await readText(selection.diffPath));
162
+ if (sections.length === 0) {
163
+ throw new InvalidArgumentError(`${selection.flag} ${selection.diffPath} holds no "diff --git" section.`, selection.flag);
164
+ }
165
+ const ownedSet = new Set(owned);
166
+ const byFile = new Map();
167
+ for (const section of sections) {
168
+ const file = normalizeEngineRelativeInput(section.targetPath, selection.flag);
169
+ if (section.isBinary || section.sourcePath !== section.targetPath) {
170
+ throw new InvalidArgumentError(`${selection.flag}: the section for ${file} is binary or a rename; only text hunks can be selected.`, selection.flag);
171
+ }
172
+ if (!ownedSet.has(file)) {
173
+ throw new InvalidArgumentError(`${selection.flag}: ${file} is not a file this patch owns.`, selection.flag);
174
+ }
175
+ byFile.set(file, section);
176
+ }
177
+ return byFile;
178
+ }
179
+ /**
180
+ * Builds the re-export body for a hunk-selected run: named files from the
181
+ * selected hunks, the rest from the old body (`--hunks-from`) or the normal
182
+ * working-tree diff (`--exclude-hunks`).
183
+ *
184
+ * @param args.normalDiff - The diff a plain re-export would write
185
+ * @param args.oldBody - The patch's current body
186
+ * @param args.files - The files the refreshed patch owns
187
+ */
188
+ export async function buildHunkSelectedDiff(args) {
189
+ const { engineDir, selection, oldBody, normalDiff, files } = args;
190
+ const named = await readSelection(selection, files);
191
+ const tracked = await listTrackedInHead(engineDir, [...named.keys()]);
192
+ const fallback = splitRawSections(selection.mode === 'include' ? oldBody : normalDiff);
193
+ const out = [];
194
+ for (const file of [...files].sort()) {
195
+ const section = named.get(file);
196
+ if (section === undefined) {
197
+ const raw = fallback.get(file);
198
+ if (raw !== undefined)
199
+ out.push(raw);
200
+ continue;
201
+ }
202
+ const head = tracked.has(file) ? await getFileContentAtRef(engineDir, file) : null;
203
+ let content;
204
+ if (selection.mode === 'include') {
205
+ const base = applyPatchTextToContent(head, oldBody, file);
206
+ content = applyHunksByContext(base, section.hunks, false, file);
207
+ }
208
+ else {
209
+ const worktree = join(engineDir, file);
210
+ const current = (await pathExists(worktree)) ? await readText(worktree) : '';
211
+ content = applyHunksByContext(current, section.hunks, true, file);
212
+ }
213
+ if (head !== null && content === head)
214
+ continue;
215
+ const diff = await diffContentAgainstHead(engineDir, file, content, tracked.has(file));
216
+ if (diff.trim())
217
+ out.push(diff.endsWith('\n') ? diff : `${diff}\n`);
218
+ }
219
+ return out.join('');
220
+ }
@@ -103,12 +103,37 @@ function validateExpectedRemovalUsage(patches, options) {
103
103
  throw new InvalidArgumentError('--expect-removed requires explicit selected patches and the scan-less path (no --scan, --files, or --all).', '--expect-removed');
104
104
  }
105
105
  }
106
+ /**
107
+ * `--hunks-from` and `--exclude-hunks` rebuild one patch's files from named
108
+ * hunks, so they take exactly one explicit patch and no flag that sets the
109
+ * file list another way.
110
+ */
111
+ function validateHunkSelectionUsage(patches, options) {
112
+ if (options.hunksFrom !== undefined && options.excludeHunks !== undefined) {
113
+ throw new InvalidArgumentError('--hunks-from and --exclude-hunks are alternatives; pass one.', '--hunks-from');
114
+ }
115
+ const flag = options.hunksFrom !== undefined
116
+ ? '--hunks-from'
117
+ : options.excludeHunks !== undefined
118
+ ? '--exclude-hunks'
119
+ : undefined;
120
+ if (flag === undefined)
121
+ return;
122
+ if (patches.length !== 1 ||
123
+ options.all ||
124
+ options.scan ||
125
+ options.files !== undefined ||
126
+ options.scanFilesManifest !== undefined) {
127
+ throw new InvalidArgumentError(`${flag} rebuilds one patch's files from named hunks: pass exactly one patch, without --scan, --files or --all.`, flag);
128
+ }
129
+ }
106
130
  /** Validates mutually exclusive `re-export` targeting and metadata options. */
107
131
  export function validateReExportOptionCombinations(patches, options) {
108
132
  // First: a swallowed flag would otherwise surface as whichever
109
133
  // combination error it happens to trip, naming the wrong problem.
110
134
  validatePathFlagValues(options);
111
135
  validateExpectedRemovalUsage(patches, options);
136
+ validateHunkSelectionUsage(patches, options);
112
137
  if (options.files !== undefined) {
113
138
  if (options.all || options.scan) {
114
139
  throw new InvalidArgumentError('--files cannot be combined with --scan or --all.', '--files');
@@ -140,8 +165,10 @@ export function validateReExportOptionCombinations(patches, options) {
140
165
  throw new InvalidArgumentError('--scan-files selects patches from its manifest and cannot be combined with positional patches or --all.', '--scan-files');
141
166
  }
142
167
  }
143
- if (options.refuseAdjacentUnmanaged === true && (options.scan || options.files !== undefined)) {
144
- throw new InvalidArgumentError('--refuse-adjacent-unmanaged applies to the scan-less path only and cannot be combined with --scan or --files (those set filesAffected explicitly).', '--refuse-adjacent-unmanaged');
168
+ // Beside --scan the belt judges the neighbours the scan did not adopt;
169
+ // --files replaces the file list outright, so there is nothing to judge.
170
+ if (options.refuseAdjacentUnmanaged === true && options.files !== undefined) {
171
+ throw new InvalidArgumentError('--refuse-adjacent-unmanaged cannot be combined with --files (it sets filesAffected explicitly).', '--refuse-adjacent-unmanaged');
145
172
  }
146
173
  // Beside --scan the belt covers the files each patch already owns; the
147
174
  // files a scan adopts are the run's stated intent. --files replaces the
@@ -0,0 +1,18 @@
1
+ import type { PatchMetadata } from '../types/commands/index.js';
2
+ /** What a scoped dry run could touch: the patches it was asked about. */
3
+ export interface DryRunPurityScope {
4
+ /** The selected patches. */
5
+ patches: readonly PatchMetadata[];
6
+ }
7
+ /**
8
+ * Fingerprints exactly the state a scoped `re-export --dry-run` could
9
+ * disturb. Every key names what it covers, so a violation reads as the
10
+ * path that moved.
11
+ */
12
+ export declare function fingerprintScopedDryRunState(engineDir: string, patchesDir: string, scope: DryRunPurityScope): Promise<Map<string, string>>;
13
+ /**
14
+ * Names whoever else wrote during the dry run: history records from other
15
+ * invocations since it started, and a live holder of the engine-session
16
+ * lock (which a dry run no longer takes). Empty when there is no evidence.
17
+ */
18
+ export declare function describeForeignWriters(patchesDir: string, sinceMs: number): Promise<string[]>;