@hominis/fireforge 0.41.0 → 0.43.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 (201) hide show
  1. package/CHANGELOG.md +81 -34
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/cli.js +21 -2
  4. package/dist/src/commands/build.js +7 -1
  5. package/dist/src/commands/config.js +16 -27
  6. package/dist/src/commands/discard.js +1 -1
  7. package/dist/src/commands/export-flow.js +13 -3
  8. package/dist/src/commands/export-placement-conflicts.js +1 -1
  9. package/dist/src/commands/export-shared.d.ts +1 -1
  10. package/dist/src/commands/export-shared.js +4 -5
  11. package/dist/src/commands/furnace/chrome-doc-remove.js +3 -0
  12. package/dist/src/commands/furnace/chrome-doc.js +3 -0
  13. package/dist/src/commands/furnace/create.js +3 -0
  14. package/dist/src/commands/furnace/override.js +3 -0
  15. package/dist/src/commands/furnace/preview.js +3 -0
  16. package/dist/src/commands/furnace/refresh.js +3 -0
  17. package/dist/src/commands/furnace/remove.js +3 -0
  18. package/dist/src/commands/furnace/rename.js +3 -0
  19. package/dist/src/commands/furnace/scan.js +3 -0
  20. package/dist/src/commands/import.js +1 -1
  21. package/dist/src/commands/lint-per-patch.js +16 -8
  22. package/dist/src/commands/lint-per-run-checkjs.d.ts +4 -4
  23. package/dist/src/commands/lint-per-run-checkjs.js +4 -4
  24. package/dist/src/commands/lint-report.js +1 -1
  25. package/dist/src/commands/lint.js +2 -2
  26. package/dist/src/commands/patch/lint-ignore.js +1 -1
  27. package/dist/src/commands/patch/move-files-create.js +5 -3
  28. package/dist/src/commands/patch/move-files-into.d.ts +1 -1
  29. package/dist/src/commands/patch/move-files-into.js +5 -3
  30. package/dist/src/commands/patch/move-files.js +1 -1
  31. package/dist/src/commands/patch/rename.js +1 -1
  32. package/dist/src/commands/patch/reorder.d.ts +1 -2
  33. package/dist/src/commands/patch/reorder.js +1 -2
  34. package/dist/src/commands/patch/split.js +5 -3
  35. package/dist/src/commands/patch/staged-dependency-validate.js +1 -1
  36. package/dist/src/commands/patch/staged-dependency.js +4 -4
  37. package/dist/src/commands/re-export-adjacent.js +1 -1
  38. package/dist/src/commands/re-export-bulk-scan.d.ts +1 -1
  39. package/dist/src/commands/re-export-bulk-scan.js +5 -4
  40. package/dist/src/commands/re-export-drift.d.ts +4 -4
  41. package/dist/src/commands/re-export-drift.js +47 -8
  42. package/dist/src/commands/re-export-lint.d.ts +7 -5
  43. package/dist/src/commands/re-export-lint.js +9 -8
  44. package/dist/src/commands/re-export-single.d.ts +1 -1
  45. package/dist/src/commands/re-export-single.js +5 -4
  46. package/dist/src/commands/re-export.js +26 -14
  47. package/dist/src/commands/reset.js +1 -1
  48. package/dist/src/commands/run.js +1 -1
  49. package/dist/src/commands/source.js +6 -6
  50. package/dist/src/commands/status-check.d.ts +1 -1
  51. package/dist/src/commands/status-check.js +1 -1
  52. package/dist/src/commands/status-json.d.ts +2 -2
  53. package/dist/src/commands/status-json.js +1 -1
  54. package/dist/src/commands/status-output.d.ts +1 -1
  55. package/dist/src/commands/status-output.js +1 -1
  56. package/dist/src/commands/status.js +21 -7
  57. package/dist/src/commands/test-build-phase.d.ts +2 -2
  58. package/dist/src/commands/test-build-phase.js +9 -6
  59. package/dist/src/commands/test-diagnose.js +8 -1
  60. package/dist/src/commands/test-modes.d.ts +2 -2
  61. package/dist/src/commands/test-modes.js +3 -3
  62. package/dist/src/commands/test-register.js +7 -2
  63. package/dist/src/commands/test-run.d.ts +6 -0
  64. package/dist/src/commands/test-run.js +32 -3
  65. package/dist/src/commands/test-stale-gate.js +7 -2
  66. package/dist/src/commands/test.js +4 -4
  67. package/dist/src/commands/tree.js +2 -2
  68. package/dist/src/commands/typecheck.d.ts +2 -1
  69. package/dist/src/commands/typecheck.js +59 -3
  70. package/dist/src/commands/verify.js +26 -1
  71. package/dist/src/core/ast-utils.js +14 -2
  72. package/dist/src/core/branding.d.ts +16 -0
  73. package/dist/src/core/branding.js +18 -2
  74. package/dist/src/core/browser-wire.js +9 -1
  75. package/dist/src/core/build-baseline-types.d.ts +1 -1
  76. package/dist/src/core/build-baseline.d.ts +1 -1
  77. package/dist/src/core/build-baseline.js +1 -1
  78. package/dist/src/core/build-overwrite-guard.d.ts +49 -0
  79. package/dist/src/core/build-overwrite-guard.js +112 -0
  80. package/dist/src/core/build-prepare.d.ts +13 -5
  81. package/dist/src/core/build-prepare.js +42 -9
  82. package/dist/src/core/config-mutate.d.ts +3 -2
  83. package/dist/src/core/config-mutate.js +5 -2
  84. package/dist/src/core/config.d.ts +3 -2
  85. package/dist/src/core/config.js +2 -0
  86. package/dist/src/core/coverage-extend.d.ts +1 -1
  87. package/dist/src/core/coverage-extend.js +1 -1
  88. package/dist/src/core/destructive.d.ts +23 -0
  89. package/dist/src/core/destructive.js +45 -4
  90. package/dist/src/core/diff-hunks.js +31 -17
  91. package/dist/src/core/discard-baseline.d.ts +1 -1
  92. package/dist/src/core/discard-baseline.js +1 -1
  93. package/dist/src/core/display-state.d.ts +52 -0
  94. package/dist/src/core/display-state.js +89 -0
  95. package/dist/src/core/engine-session-lock.d.ts +20 -0
  96. package/dist/src/core/engine-session-lock.js +84 -14
  97. package/dist/src/core/export-moved-code-shape.d.ts +60 -0
  98. package/dist/src/core/export-moved-code-shape.js +129 -0
  99. package/dist/src/core/file-lock.d.ts +44 -0
  100. package/dist/src/core/file-lock.js +157 -42
  101. package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
  102. package/dist/src/core/furnace-apply-helpers.js +1 -1
  103. package/dist/src/core/furnace-apply-output.js +1 -1
  104. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +2 -2
  105. package/dist/src/core/furnace-apply-overwrite-warn.js +2 -2
  106. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
  107. package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
  108. package/dist/src/core/furnace-apply.js +13 -1
  109. package/dist/src/core/furnace-config-custom.d.ts +2 -1
  110. package/dist/src/core/furnace-config-order.d.ts +2 -1
  111. package/dist/src/core/furnace-config-order.js +9 -4
  112. package/dist/src/core/furnace-config-validate.d.ts +4 -3
  113. package/dist/src/core/furnace-config-validate.js +3 -3
  114. package/dist/src/core/furnace-config.js +5 -0
  115. package/dist/src/core/furnace-operation.d.ts +12 -0
  116. package/dist/src/core/furnace-operation.js +125 -1
  117. package/dist/src/core/furnace-rollback.d.ts +17 -0
  118. package/dist/src/core/furnace-rollback.js +35 -1
  119. package/dist/src/core/git-base.js +7 -2
  120. package/dist/src/core/git-diff.js +11 -2
  121. package/dist/src/core/git-readonly-index.d.ts +59 -0
  122. package/dist/src/core/git-readonly-index.js +137 -0
  123. package/dist/src/core/license-headers.d.ts +1 -1
  124. package/dist/src/core/license-headers.js +1 -1
  125. package/dist/src/core/lint-cache.d.ts +15 -5
  126. package/dist/src/core/lint-cache.js +35 -8
  127. package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
  128. package/dist/src/core/mach-known-noise-filter.js +23 -0
  129. package/dist/src/core/mach-mozconfig.js +21 -3
  130. package/dist/src/core/module-resolution-preflight.d.ts +51 -0
  131. package/dist/src/core/module-resolution-preflight.js +155 -0
  132. package/dist/src/core/patch-export.js +2 -1
  133. package/dist/src/core/patch-identifier-suggest.js +29 -3
  134. package/dist/src/core/patch-lint-checkjs.d.ts +5 -5
  135. package/dist/src/core/patch-lint-checkjs.js +6 -6
  136. package/dist/src/core/patch-lint-creators.d.ts +1 -1
  137. package/dist/src/core/patch-lint-creators.js +1 -1
  138. package/dist/src/core/patch-lint-cross.d.ts +1 -1
  139. package/dist/src/core/patch-lint-cross.js +4 -4
  140. package/dist/src/core/patch-lint-css.js +1 -1
  141. package/dist/src/core/patch-lint-diff.d.ts +1 -1
  142. package/dist/src/core/patch-lint-diff.js +1 -1
  143. package/dist/src/core/patch-lint-jsdoc.js +4 -2
  144. package/dist/src/core/patch-lint-module-registration.d.ts +13 -0
  145. package/dist/src/core/patch-lint-module-registration.js +13 -4
  146. package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
  147. package/dist/src/core/patch-lint-mozbuild.js +1 -1
  148. package/dist/src/core/patch-lint-ownership.d.ts +2 -2
  149. package/dist/src/core/patch-lint-ownership.js +2 -2
  150. package/dist/src/core/patch-lint.d.ts +2 -2
  151. package/dist/src/core/patch-lint.js +3 -3
  152. package/dist/src/core/patch-lock.d.ts +21 -0
  153. package/dist/src/core/patch-lock.js +29 -2
  154. package/dist/src/core/patch-manifest-io.d.ts +2 -1
  155. package/dist/src/core/patch-manifest-io.js +49 -0
  156. package/dist/src/core/patch-parse.d.ts +18 -0
  157. package/dist/src/core/patch-parse.js +24 -1
  158. package/dist/src/core/patch-policy.d.ts +2 -2
  159. package/dist/src/core/patch-policy.js +2 -2
  160. package/dist/src/core/smoke-patterns.d.ts +1 -1
  161. package/dist/src/core/status-binary.d.ts +1 -1
  162. package/dist/src/core/status-binary.js +1 -1
  163. package/dist/src/core/status-classify.d.ts +1 -1
  164. package/dist/src/core/status-classify.js +2 -2
  165. package/dist/src/core/test-harness-crash.d.ts +69 -3
  166. package/dist/src/core/test-harness-crash.js +174 -9
  167. package/dist/src/core/test-stale-check.d.ts +1 -1
  168. package/dist/src/core/test-stale-check.js +1 -1
  169. package/dist/src/core/token-category.js +1 -1
  170. package/dist/src/core/token-manager.d.ts +1 -1
  171. package/dist/src/core/token-manager.js +5 -5
  172. package/dist/src/core/tree-cow.js +1 -1
  173. package/dist/src/core/tree-guard.js +2 -2
  174. package/dist/src/core/tree-store.js +1 -1
  175. package/dist/src/core/typecheck-shim.d.ts +2 -2
  176. package/dist/src/core/typecheck-shim.js +2 -2
  177. package/dist/src/core/typecheck.js +10 -0
  178. package/dist/src/core/xpcshell-profile-dir.js +1 -1
  179. package/dist/src/errors/base.d.ts +22 -1
  180. package/dist/src/errors/base.js +32 -1
  181. package/dist/src/errors/codes.d.ts +10 -0
  182. package/dist/src/errors/codes.js +10 -0
  183. package/dist/src/types/commands/options.d.ts +27 -11
  184. package/dist/src/types/commands/patches.d.ts +1 -1
  185. package/dist/src/types/config.d.ts +2 -2
  186. package/dist/src/types/furnace.d.ts +1 -1
  187. package/dist/src/types/json.d.ts +22 -0
  188. package/dist/src/types/json.js +11 -0
  189. package/dist/src/utils/assert.d.ts +46 -0
  190. package/dist/src/utils/assert.js +83 -0
  191. package/dist/src/utils/build-info.d.ts +1 -1
  192. package/dist/src/utils/build-info.js +1 -1
  193. package/dist/src/utils/logger.d.ts +20 -0
  194. package/dist/src/utils/logger.js +26 -0
  195. package/dist/src/utils/options.d.ts +12 -0
  196. package/dist/src/utils/options.js +39 -0
  197. package/dist/src/utils/stdio-drain.d.ts +1 -1
  198. package/dist/src/utils/stdio-drain.js +1 -1
  199. package/dist/src/utils/validation.d.ts +12 -1
  200. package/dist/src/utils/validation.js +13 -1
  201. package/package.json +3 -3
@@ -60,7 +60,7 @@ export async function runPatchLint(engineDir, filesAffected, diffContent, config
60
60
  /**
61
61
  * Prints a patch-lint issue list with `runPatchLint`'s exact severity
62
62
  * buckets and skip-lint semantics, throwing on errors unless `skipLint`.
63
- * Factored out so `re-export`'s cached-lint path (FORGE J1) can replay
63
+ * Factored out so `re-export`'s cached-lint path can replay
64
64
  * stored issues through the identical reporter without re-linting.
65
65
  */
66
66
  export function reportPatchLintOutcome(issues, skipLint) {
@@ -112,7 +112,7 @@ export async function promptExportPatchMetadata(options, isInteractive, commandN
112
112
  const categories = config !== undefined ? getPatchPolicyCategories(config) : [...PATCH_CATEGORIES];
113
113
  // A filename-shaped `--name foo.patch` must not die on the dot rule —
114
114
  // strip the extension before validation so the one-step flow works for
115
- // both name shapes (FORGE G13).
115
+ // both name shapes.
116
116
  let patchName = options.name?.trim().replace(/\.patch$/i, '');
117
117
  if (patchName) {
118
118
  const validationError = validatePatchName(patchName);
@@ -170,8 +170,7 @@ export async function promptExportPatchMetadata(options, isInteractive, commandN
170
170
  }
171
171
  // The filename slug pipeline strips redundant category prefixes; the
172
172
  // manifest display name must agree, or every `export --name ui-foo`
173
- // needs a follow-up `patch rename` to satisfy bare-slug naming policy
174
- // (FORGE G13).
173
+ // needs a follow-up `patch rename` to satisfy bare-slug naming policy.
175
174
  const normalized = normalizePatchDisplayName(patchName, category);
176
175
  if (normalized !== patchName) {
177
176
  info(`Patch name normalized: "${patchName}" → "${normalized}" (the filename carries the order and category prefix).`);
@@ -254,7 +253,7 @@ export async function autoFixLicenseHeaders(engineDir, diffContent, config, isIn
254
253
  if (!isAcceptableNewFileHeader(file, content, style, license)) {
255
254
  // A recognized third-party permissive banner marks the file as
256
255
  // vendored: prepending the project's own license header would break
257
- // byte-identity pins and mislicense upstream code (FORGE F15).
256
+ // byte-identity pins and mislicense upstream code.
258
257
  if (hasThirdPartyPermissiveBanner(content)) {
259
258
  vendoredFiles.push(file);
260
259
  }
@@ -87,6 +87,9 @@ async function performChromeDocRemoveMutations(args) {
87
87
  await removeEmptyDirIfPresent(join(args.engineDir, 'browser/base/content/test', `${args.binaryName}-xpcshell`), journal);
88
88
  }
89
89
  catch (error) {
90
+ // This body owns its rollback end to end, so tell the lifecycle wrapper
91
+ // not to restore the same journal again on the way out.
92
+ args.operationContext.markRolledBack();
90
93
  try {
91
94
  await restoreRollbackJournalOrThrow(journal, `Failed to remove chrome-doc "${args.name}"`);
92
95
  }
@@ -274,6 +274,9 @@ async function performChromeDocMutations(args) {
274
274
  }
275
275
  }
276
276
  catch (error) {
277
+ // This body owns its rollback end to end, so tell the lifecycle wrapper
278
+ // not to restore the same journal again on the way out.
279
+ args.operationContext.markRolledBack();
277
280
  try {
278
281
  await restoreRollbackJournalOrThrow(journal, `Failed to scaffold chrome-doc "${args.name}"`);
279
282
  }
@@ -199,6 +199,9 @@ async function performCreateMutations(args) {
199
199
  }
200
200
  }
201
201
  catch (error) {
202
+ // This body owns its rollback end to end, so tell the lifecycle wrapper
203
+ // not to restore the same journal again on the way out.
204
+ args.operationContext?.markRolledBack();
202
205
  try {
203
206
  await restoreRollbackJournalOrThrow(journal, `Failed to create custom component "${args.componentName}"`);
204
207
  }
@@ -137,6 +137,9 @@ async function performOverrideMutations(args) {
137
137
  return filesCopied;
138
138
  }
139
139
  catch (error) {
140
+ // This body owns its rollback end to end, so tell the lifecycle
141
+ // wrapper not to restore the same journal again on the way out.
142
+ ctx.markRolledBack();
140
143
  try {
141
144
  await restoreRollbackJournalOrThrow(journal, `Failed to override component "${args.componentName}"`);
142
145
  }
@@ -325,6 +325,9 @@ export async function furnacePreviewCommand(projectRoot, options = {}) {
325
325
  // message with no guidance. We now collect both failures and, if anything
326
326
  // went wrong, persist a `pendingRepair` marker that `fireforge doctor`
327
327
  // consumes to finish the reconciliation.
328
+ // Teardown restores the journal itself, so the lifecycle wrapper must not
329
+ // restore it again when the throw below propagates out of this body.
330
+ ctx.markRolledBack();
328
331
  const teardownErrors = await runPreviewTeardown(paths.engine, storiesCleanupRequired, previewJournal);
329
332
  if (teardownErrors.length > 0) {
330
333
  const teardownSummary = teardownErrors.map((err) => err.message).join('; ');
@@ -145,6 +145,9 @@ async function refreshSingleOverride(projectRoot, name, options = {}) {
145
145
  }
146
146
  catch (error) {
147
147
  if (!dryRun) {
148
+ // This body owns its rollback end to end, so tell the lifecycle
149
+ // wrapper not to restore the same journal again on the way out.
150
+ ctx.markRolledBack();
148
151
  try {
149
152
  await restoreRollbackJournalOrThrow(journal, `Failed to refresh override "${name}"`);
150
153
  }
@@ -405,6 +405,9 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
405
405
  }
406
406
  }
407
407
  catch (error) {
408
+ // This body owns its rollback end to end, so tell the lifecycle wrapper
409
+ // not to restore the same journal again on the way out.
410
+ ctx.markRolledBack();
408
411
  try {
409
412
  await restoreRollbackJournalOrThrow(journal, `Failed to remove component "${name}"`);
410
413
  }
@@ -308,6 +308,9 @@ async function performRenameMutations(args) {
308
308
  info(`Renamed ${componentType} component: ${oldName} → ${newName}`);
309
309
  }
310
310
  catch (error) {
311
+ // This body owns its rollback end to end, so tell the lifecycle wrapper
312
+ // not to restore the same journal again on the way out.
313
+ ctx.markRolledBack();
311
314
  try {
312
315
  if (await pathExists(newDir)) {
313
316
  await removeDir(newDir);
@@ -92,6 +92,9 @@ async function persistStockComponents(projectRoot, names) {
92
92
  await writeFurnaceConfig(projectRoot, config);
93
93
  }
94
94
  catch (error) {
95
+ // This body owns its rollback end to end, so tell the lifecycle wrapper
96
+ // not to restore the same journal again on the way out.
97
+ ctx.markRolledBack();
95
98
  try {
96
99
  await restoreRollbackJournalOrThrow(journal, 'Failed to update furnace.json during scan');
97
100
  }
@@ -408,7 +408,7 @@ export async function importCommand(projectRoot, options = {}) {
408
408
  }
409
409
  // The re-applied queue may have moved components.conf away from what
410
410
  // the last full build compiled in — surface that now instead of at the
411
- // next test refusal (FORGE F13).
411
+ // next test refusal.
412
412
  await warnIfStaticComponentsStale(projectRoot, paths.engine);
413
413
  outro('All patches applied successfully!');
414
414
  }
@@ -2,6 +2,7 @@
2
2
  import { join } from 'node:path';
3
3
  import { getProjectPaths, loadConfig } from '../core/config.js';
4
4
  import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
5
+ import { withPrivateGitIndex } from '../core/git-readonly-index.js';
5
6
  import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, getPerPatchLintCacheHeadSha, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/lint-cache.js';
6
7
  import { buildPatchQueueContext, countNonBinaryDiffLines, formatPatchLintIssue, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
7
8
  import { loadPatchesManifest } from '../core/patch-manifest.js';
@@ -59,13 +60,13 @@ async function lintQueuedPatch(patch, lintCtx) {
59
60
  queueContext: ctx,
60
61
  ...(engineHeadSha === undefined ? {} : { engineHeadSha }),
61
62
  });
62
- const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey);
63
+ const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey, ignore);
63
64
  if (cached) {
64
65
  // Returning before the empty-diff probe below is safe: the cache key
65
66
  // hashes every affected engine file's content plus engineHeadSha, so
66
67
  // an engine-side revert that would empty the diff always changes the
67
68
  // key and misses the cache (pinned by the "engine-side content
68
- // revert invalidates the per-patch cache" test — FORGE F5).
69
+ // revert invalidates the per-patch cache" test).
69
70
  return {
70
71
  status: 'cached',
71
72
  existingFiles: existing,
@@ -115,11 +116,11 @@ async function lintQueuedPatch(patch, lintCtx) {
115
116
  usedCheckJs,
116
117
  };
117
118
  if (cache && cacheKey) {
118
- result.cacheWrite = { key: cacheKey };
119
+ result.cacheWrite = { key: cacheKey, lintIgnore: [...(ignore ?? [])] };
119
120
  }
120
121
  return result;
121
122
  }
122
- /** Size-rule check IDs whose waived measurement is still reported (FORGE G10). */
123
+ /** Size-rule check IDs whose waived measurement is still reported. */
123
124
  const SUPPRESSED_SIZE_CHECKS = new Set([
124
125
  'large-patch-lines',
125
126
  'large-patch-files',
@@ -156,20 +157,20 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
156
157
  // Deliberately NOT gated on `result.usedCheckJs`: global findings are
157
158
  // run-level, never cached, and an all-cache-hit run previously dropped
158
159
  // them entirely, letting a warm run report fewer errors than a cold
159
- // one (FORGE F5 hardening). PerRunCheckJs builds its program lazily,
160
+ // one (hardening). PerRunCheckJs builds its program lazily,
160
161
  // so the cost only materialises when checkJs is configured.
161
162
  if (checkJs && !globalCheckJsEmitted) {
162
163
  globalCheckJsEmitted = true;
163
164
  issues.push(...(await checkJs.getGlobal()));
164
165
  }
165
166
  if (result.cacheWrite && cache) {
166
- setCachedPerPatchLintIssues(cache, patch.filename, result.cacheWrite.key, result.rawIssues, result.suppressedIssues, result.lineCount);
167
+ setCachedPerPatchLintIssues(cache, patch.filename, result.cacheWrite.key, result.rawIssues, result.suppressedIssues, result.lineCount, result.cacheWrite.lintIgnore);
167
168
  totals.cacheDirty = true;
168
169
  }
169
170
  for (const issue of result.rawIssues) {
170
171
  issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
171
172
  }
172
- // A waived size finding still reports its MEASUREMENT (FORGE G10):
173
+ // A waived size finding still reports its MEASUREMENT:
173
174
  // the finding stays suppressed (no exit-code / --max-warnings effect)
174
175
  // but the current count is readable from the tool that enforces it,
175
176
  // so a waiver's cited size can be calibrated without hand-measuring.
@@ -310,6 +311,13 @@ function selectPatchSubset(manifest, requested) {
310
311
  * queue-level findings are scoped to files those patches touch).
311
312
  */
312
313
  export async function lintPerPatch(projectRoot, paths, options = {}) {
314
+ // Read-only to the operator, an index WRITER to git: the per-patch diffs
315
+ // run `git diff HEAD` (and, for untracked binaries, a real stage/unstage
316
+ // pair) against the primary checkout. A private index absorbs all of it
317
+ // so a concurrent `fireforge test` still gets an evidential verdict.
318
+ return withPrivateGitIndex(paths.engine, () => lintPerPatchInner(projectRoot, paths, options));
319
+ }
320
+ async function lintPerPatchInner(projectRoot, paths, options = {}) {
313
321
  const manifest = await loadPatchesManifest(paths.patches);
314
322
  if (!manifest || manifest.patches.length === 0) {
315
323
  info('No patches in manifest — nothing to lint per-patch.');
@@ -352,7 +360,7 @@ export async function lintPerPatch(projectRoot, paths, options = {}) {
352
360
  }
353
361
  // With `--patches`, the checkJs program roots at only the subset's owned
354
362
  // files — the full queue stays resolvable, so a cold subset run costs the
355
- // subset's import closure instead of the whole queue (FORGE J1c).
363
+ // subset's import closure instead of the whole queue.
356
364
  const checkJs = buildPerRunCheckJs(projectRoot, paths, config, ctx, isSubset ? subsetNames : undefined);
357
365
  // Lint patches concurrently, then apply every side effect in patch order so
358
366
  // the issue rows, the run-level checkJs errors, and the saved cache are
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Per-run checkJs program controller, shared by `lint --per-patch` and
3
- * `re-export` (FORGE J1).
3
+ * `re-export`.
4
4
  *
5
5
  * The dominant fixed cost of patch lint is one `ts.createProgram` over the
6
6
  * queue's patch-owned `.sys.mjs` files (~37 s on a 290-patch consumer
@@ -13,11 +13,11 @@
13
13
  * - **Root scoping** (`rootScopePatches`): with `--patches <subset>` the
14
14
  * program's roots are only the subset's owned files; the full queue
15
15
  * stays resolvable, so cross-patch imports type-check identically while
16
- * unrelated files are never parsed (FORGE J1c).
16
+ * unrelated files are never parsed.
17
17
  * - **Warm-run probe**: run-level ("global") checkJs findings come only
18
18
  * from a missing `typescript` package or an unreadable shim — never
19
19
  * from the built program — so an all-cache-hit run satisfies "warm
20
- * never reports less than cold" (FORGE F5) via
20
+ * never reports less than cold" via
21
21
  * `probeCheckJsGlobalIssues` instead of building anything.
22
22
  */
23
23
  import type { getProjectPaths, loadConfig } from '../core/config.js';
@@ -43,7 +43,7 @@ export interface PerRunCheckJs {
43
43
  getGrouped: () => Promise<GroupedCheckJsResult>;
44
44
  /** Run-level checkJs errors (e.g. TypeScript missing). When the program
45
45
  * was never built (all-warm run) this probes the two global failure
46
- * sources directly instead of building it — FORGE F5 holds because the
46
+ * sources directly instead of building it holds because the
47
47
  * built program never contributes globals of its own. */
48
48
  getGlobal: () => Promise<PatchLintIssue[]>;
49
49
  /** The byFile slices for `files` (globals deliberately EXCLUDED — they
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Per-run checkJs program controller, shared by `lint --per-patch` and
4
- * `re-export` (FORGE J1).
4
+ * `re-export`.
5
5
  *
6
6
  * The dominant fixed cost of patch lint is one `ts.createProgram` over the
7
7
  * queue's patch-owned `.sys.mjs` files (~37 s on a 290-patch consumer
@@ -14,11 +14,11 @@
14
14
  * - **Root scoping** (`rootScopePatches`): with `--patches <subset>` the
15
15
  * program's roots are only the subset's owned files; the full queue
16
16
  * stays resolvable, so cross-patch imports type-check identically while
17
- * unrelated files are never parsed (FORGE J1c).
17
+ * unrelated files are never parsed.
18
18
  * - **Warm-run probe**: run-level ("global") checkJs findings come only
19
19
  * from a missing `typescript` package or an unreadable shim — never
20
20
  * from the built program — so an all-cache-hit run satisfies "warm
21
- * never reports less than cold" (FORGE F5) via
21
+ * never reports less than cold" via
22
22
  * `probeCheckJsGlobalIssues` instead of building anything.
23
23
  */
24
24
  import { invokePatchLintCheckJsGrouped, probeCheckJsGlobalIssues, runCheckJsTestFilesGrouped, } from '../core/patch-lint-checkjs.js';
@@ -64,7 +64,7 @@ export function buildPerRunCheckJs(projectRoot, paths, config, ctx, rootScopePat
64
64
  }
65
65
  }
66
66
  // One build for the whole run: the queue-wide `.sys.mjs` program plus —
67
- // when `patchLint.checkJsTestFiles` is on (FORGE G5) — one small
67
+ // when `patchLint.checkJsTestFiles` is on — one small
68
68
  // script-scope program per patch-owned test file, merged by file.
69
69
  const buildAll = async () => {
70
70
  const sys = await invokePatchLintCheckJsGrouped(paths.engine, resolvePatchOwnedSysMjs(new Set(), ctx), patchLint, projectRoot, scopedRoots);
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Machine-readable per-patch lint report (`lint --per-patch --report
4
- * <path>`, FORGE G9 + G10). One mechanism serves both observability
4
+ * <path>`, + G10). One mechanism serves both observability
5
5
  * gaps: consumers get each patch's size metrics against the SAME
6
6
  * thresholds the size rules fire on (no more mirroring
7
7
  * `countNonBinaryDiffLines`), and lintIgnore-suppressed issues stay
@@ -220,7 +220,7 @@ export function applyAggregateLintIgnoreSuppression(issues, ctx) {
220
220
  * without `--since` and non-integer `--max-warnings` — each a
221
221
  * misconfiguration that should fail loud rather than silently narrow the
222
222
  * result. Positional arguments in per-patch mode are patch selectors
223
- * (FORGE G14) and are validated downstream by `selectPatchSubset`.
223
+ * and are validated downstream by `selectPatchSubset`.
224
224
  */
225
225
  function validateLintFlags(options) {
226
226
  // `--only-introduced` scopes the exit code to `--since`-tagged issues, so
@@ -434,7 +434,7 @@ export async function lintCommand(projectRoot, files, options = {}) {
434
434
  }
435
435
  if (options.perPatch) {
436
436
  // Positional arguments in per-patch mode select PATCHES through the
437
- // same alias resolution as --patches (FORGE G14) — a non-matching
437
+ // same alias resolution as --patches — a non-matching
438
438
  // positional fails loud against the queue listing, so an engine path
439
439
  // can never silently lint. Explicit --patches entries come first.
440
440
  const perPatchOptions = files.length > 0 ? { ...options, patches: [...(options.patches ?? []), ...files] } : options;
@@ -32,7 +32,7 @@ import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
32
32
  * writes turns per-patch lint green immediately, but consumer projects
33
33
  * commonly audit lintIgnore lists against a reviewed allow-map — the
34
34
  * discovery that the map also needs updating otherwise costs a full
35
- * downstream gate run (FORGE F6).
35
+ * downstream gate run.
36
36
  */
37
37
  const LINT_IGNORE_REVIEW_WARNING = "This lint waiver is subject to the project's patch-policy review process. " +
38
38
  'Downstream policy audits that mirror lintIgnore lists in a reviewed allow-map ' +
@@ -11,7 +11,7 @@
11
11
  * new patch's name/slug the way `split --name` does.
12
12
  */
13
13
  import { getProjectPaths, loadConfig } from '../../core/config.js';
14
- import { confirmDestructive } from '../../core/destructive.js';
14
+ import { assertConfirmationAvailable, confirmDestructive } from '../../core/destructive.js';
15
15
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
16
16
  import { buildPatchQueueContext } from '../../core/patch-lint.js';
17
17
  import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
@@ -31,6 +31,8 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
31
31
  intro(options.dryRun === true
32
32
  ? 'FireForge patch move-files --create (dry run)'
33
33
  : 'FireForge patch move-files --create');
34
+ // Refuse a prompt-less run BEFORE the diff/lint work, not after it.
35
+ assertConfirmationAvailable('patch move-files --create', options);
34
36
  const paths = getProjectPaths(projectRoot);
35
37
  const config = await loadConfig(projectRoot);
36
38
  const manifest = await loadPatchesManifest(paths.patches);
@@ -64,7 +66,7 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
64
66
  const placement = await resolvePlacementPlan(paths.patches, placementOptions, category, newPatchName, config);
65
67
  // The filename slug pipeline (resolvePlacementPlan above) strips redundant
66
68
  // category prefixes; the manifest display name must agree with the bare-slug
67
- // naming policy, exactly as `export --name` already does (FORGE G13/H4).
69
+ // naming policy, exactly as `export --name` already does.
68
70
  const displayName = normalizePatchDisplayName(newPatchName, category);
69
71
  if (displayName !== newPatchName) {
70
72
  info(`Patch name normalized: "${newPatchName}" → "${displayName}" (the filename carries the order and category prefix).`);
@@ -90,7 +92,7 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
90
92
  // same patch-policy shape as the committed `lint --per-patch` gate) makes
91
93
  // cross-patch `resource:///` imports and sibling head.js harness roots
92
94
  // resolve exactly as they will after the move lands — without it the
93
- // projection lint was blind (FORGE I3).
95
+ // projection lint was blind.
94
96
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
95
97
  const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
96
98
  await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Transactional file-ownership move into an EXISTING patch (FORGE F4).
2
+ * Transactional file-ownership move into an EXISTING patch.
3
3
  *
4
4
  * Before 0.39.0, `patch move-files <from> <to>` with an existing target was
5
5
  * preview-only: it printed two `re-export --files` commands whose real
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Transactional file-ownership move into an EXISTING patch (FORGE F4).
3
+ * Transactional file-ownership move into an EXISTING patch.
4
4
  *
5
5
  * Before 0.39.0, `patch move-files <from> <to>` with an existing target was
6
6
  * preview-only: it printed two `re-export --files` commands whose real
@@ -15,7 +15,7 @@
15
15
  * the worktree.
16
16
  */
17
17
  import { join } from 'node:path';
18
- import { appendHistory, confirmDestructive } from '../../core/destructive.js';
18
+ import { appendHistory, assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
19
19
  import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
20
20
  import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
21
21
  import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
@@ -193,12 +193,14 @@ async function commitMoveInto(patchesDir, plan, waitLockSeconds) {
193
193
  */
194
194
  export async function runMoveFilesInto(args) {
195
195
  const { enginePath, patchesDir, source, target, files, sourceAfter, targetAfter, options } = args;
196
+ // Refuse a prompt-less run BEFORE the diff/lint work, not after it.
197
+ assertConfirmationAvailable('patch move-files', options);
196
198
  const sourceDiff = await buildSplitDiff(enginePath, sourceAfter, 'remaining', source.filename);
197
199
  const targetDiff = await buildSplitDiff(enginePath, targetAfter, 'moved', target.filename);
198
200
  // Per-patch lint both projected bodies with each patch's own waivers.
199
201
  // The whole-queue context (built once, with the config for committed-gate
200
202
  // parity) resolves cross-patch imports and sibling head.js harness roots
201
- // instead of linting each projected body blind (FORGE I3).
203
+ // instead of linting each projected body blind.
202
204
  const patchQueueCtx = await buildPatchQueueContext(patchesDir, args.config);
203
205
  await runPatchLint(enginePath, sourceAfter, sourceDiff, args.config, options.skipLint, patchQueueCtx, source.lintIgnore ? new Set(source.lintIgnore) : undefined, source.tier);
204
206
  await runPatchLint(enginePath, targetAfter, targetDiff, args.config, options.skipLint, patchQueueCtx, target.lintIgnore ? new Set(target.lintIgnore) : undefined, target.tier);
@@ -133,7 +133,7 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
133
133
  // content right now): the equivalent re-export choreography, in
134
134
  // EXECUTABLE order — the shrink must land before the grow, or the
135
135
  // projected cross-patch lint refuses the pair with
136
- // duplicate-new-file-creation (FORGE F4).
136
+ // duplicate-new-file-creation.
137
137
  const applySource = formatReExportCommand(source.filename, sourceAfter, [
138
138
  '--allow-shrink',
139
139
  '--yes',
@@ -208,7 +208,7 @@ function resolveRenamePlan(target, options, config) {
208
208
  * Pre-flight refusals: order collision (with a pointer to the verb that
209
209
  * renumbers siblings — `--order` means "this exact unused sparse slot",
210
210
  * mirroring `export --order`), projected cross-patch lint on an order
211
- * change (forward imports resolve by queue position, FORGE J10), and the
211
+ * change (forward imports resolve by queue position), and the
212
212
  * filename collision. The authoritative collision checks run again inside
213
213
  * the lock to close the TOCTOU window.
214
214
  */
@@ -17,8 +17,7 @@ export declare function rebuildFilenameForOrder(existing: PatchMetadata, newOrde
17
17
  /**
18
18
  * Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
19
19
  * can run against the projected state without touching disk. Exported for
20
- * `patch rename --order`, which projects the same single-row move shape
21
- * (FORGE J10).
20
+ * `patch rename --order`, which projects the same single-row move shape.
22
21
  */
23
22
  export declare function projectReorder(base: PatchQueueContext, renameMap: Map<string, PatchRenameEntry>): PatchQueueContext;
24
23
  /**
@@ -120,8 +120,7 @@ function renameMapsEqual(left, right) {
120
120
  /**
121
121
  * Applies a rename map to a {@link PatchQueueContext} so cross-patch lint
122
122
  * can run against the projected state without touching disk. Exported for
123
- * `patch rename --order`, which projects the same single-row move shape
124
- * (FORGE J10).
123
+ * `patch rename --order`, which projects the same single-row move shape.
125
124
  */
126
125
  export function projectReorder(base, renameMap) {
127
126
  const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
@@ -20,7 +20,7 @@
20
20
  import { join } from 'node:path';
21
21
  import { Command } from 'commander';
22
22
  import { getProjectPaths, loadConfig } from '../../core/config.js';
23
- import { appendHistory, confirmDestructive } from '../../core/destructive.js';
23
+ import { appendHistory, assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
24
24
  import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
25
25
  import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
26
26
  import { buildPatchQueueContext } from '../../core/patch-lint.js';
@@ -166,6 +166,8 @@ export async function commitPatchSplit(patchesDir, plan, newMetadata, options, c
166
166
  */
167
167
  export async function patchSplitCommand(projectRoot, sourceId, options) {
168
168
  intro(options.dryRun ? 'FireForge patch split (dry run)' : 'FireForge patch split');
169
+ // Refuse a prompt-less run BEFORE the diff/lint work, not after it.
170
+ assertConfirmationAvailable('patch split', options);
169
171
  const paths = getProjectPaths(projectRoot);
170
172
  const config = await loadConfig(projectRoot);
171
173
  const manifest = await loadPatchesManifest(paths.patches);
@@ -199,7 +201,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
199
201
  const placement = await resolvePlacementPlan(paths.patches, placementOptions, category, options.name, config);
200
202
  // The filename slug pipeline strips redundant category prefixes; the
201
203
  // manifest display name must agree with the bare-slug naming policy,
202
- // exactly as `export --name` already does (FORGE G13/H4).
204
+ // exactly as `export --name` already does.
203
205
  const displayName = normalizePatchDisplayName(options.name, category);
204
206
  if (displayName !== options.name) {
205
207
  info(`Patch name normalized: "${options.name}" → "${displayName}" (the filename carries the order and category prefix).`);
@@ -223,7 +225,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
223
225
  // tier/lintIgnore so an intentional-advisory patch can still split.
224
226
  // The whole-queue context (built once, with the config for committed-gate
225
227
  // parity) resolves cross-patch imports and sibling head.js harness roots
226
- // instead of linting each projected body blind (FORGE I3).
228
+ // instead of linting each projected body blind.
227
229
  const patchQueueCtx = await buildPatchQueueContext(paths.patches, config);
228
230
  const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
229
231
  await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, patchQueueCtx, ignoreChecks, source.tier);
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Shape validation for `patch staged-dependency --add` (FORGE K10).
3
+ * Shape validation for `patch staged-dependency --add`.
4
4
  *
5
5
  * `--creates`/`--file` take engine-relative FILE paths and `--owner`
6
6
  * takes a patch FILENAME, but all three were accepted as arbitrary
@@ -11,7 +11,7 @@ import { toError } from '../../utils/errors.js';
11
11
  import { info, intro, outro, warn } from '../../utils/logger.js';
12
12
  import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
13
13
  import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
14
- /** Joins the names of the missing flags for error attribution (FORGE G12). */
14
+ /** Joins the names of the missing flags for error attribution. */
15
15
  function missingFlagList(pairs) {
16
16
  return pairs
17
17
  .filter(([, value]) => value === undefined || value === '')
@@ -83,7 +83,7 @@ function requireRegistrationOptions(options) {
83
83
  return dependency;
84
84
  }
85
85
  /**
86
- * Resolves the `--remove` target for forward-imports (FORGE G12):
86
+ * Resolves the `--remove` target for forward-imports:
87
87
  * `--file` + `--specifier` suffice when they identify at most one staged
88
88
  * entry — `--creates` is inferred from a unique match, an ambiguous match
89
89
  * refuses with the candidate list, and no match falls through to the
@@ -219,7 +219,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
219
219
  const kind = kindFromOptions(options);
220
220
  // The patch queue loads BEFORE the declaration fields resolve so
221
221
  // `--remove` can infer `--creates` from the target patch's staged
222
- // entries (FORGE G12).
222
+ // entries.
223
223
  const { paths, manifest } = await requirePatchQueue(projectRoot);
224
224
  const targetPatch = requirePatchTarget(identifier, manifest.patches);
225
225
  const importDependency = mode === 'clear' || kind !== 'import'
@@ -232,7 +232,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
232
232
  : mode === 'add'
233
233
  ? requireRegistrationOptions(options)
234
234
  : resolveRegistrationRemovalTarget(options, targetPatch.stagedDependencies);
235
- // Shape-check --add declarations against the loaded queue (FORGE K10):
235
+ // Shape-check --add declarations against the loaded queue:
236
236
  // a patch-name-shaped --creates/--file is refused HERE instead of
237
237
  // surfacing later as an undischargeable staged-dependency-unused.
238
238
  if (mode === 'add') {
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Scan-less re-export adjacency advisory (FORGE G2). A plain `re-export`
3
+ * Scan-less re-export adjacency advisory. A plain `re-export`
4
4
  * keeps the manifest's filesAffected unchanged, so a brand-new file an
5
5
  * author just created beside a patch's owned files is silently left out
6
6
  * of the refreshed body. This module detects such adjacent files, filters
@@ -2,7 +2,7 @@ import type { PatchesManifest } from '../types/commands/index.js';
2
2
  /** Loads and validates a `re-export --scan-files` assignment manifest. */
3
3
  export declare function loadScanFilesAssignments(manifestPath: string, manifest: PatchesManifest): Promise<Map<string, string[]>>;
4
4
  /**
5
- * Runtime enforcement of the dry-run purity contract (FORGE H1): fingerprints
5
+ * Runtime enforcement of the dry-run purity contract: fingerprints
6
6
  * the engine tree and every patch artifact before and after `operation`, and
7
7
  * throws when a dry-run changed anything it promised only to inspect. This
8
8
  * turns any recurrence of the 0.40.0 "dry-run reverted a just-written export"
@@ -4,6 +4,7 @@ import { readdir, readFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { isUnavailableGenerationToken, snapshotEngineGeneration, unavailableGenerationReason, } from '../core/engine-session-lock.js';
6
6
  import { createSiblingLockPath, withFileLock } from '../core/file-lock.js';
7
+ import { formatPatchNotFoundError } from '../core/patch-identifier-suggest.js';
7
8
  import { resolvePatchIdentifier } from '../core/patch-manifest.js';
8
9
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
9
10
  import { mapWithConcurrency } from '../utils/concurrency.js';
@@ -55,8 +56,8 @@ export async function loadScanFilesAssignments(manifestPath, manifest) {
55
56
  for (const assignment of parsed.assignments) {
56
57
  const patch = resolvePatchIdentifier(assignment.patch, manifest.patches);
57
58
  if (!patch) {
58
- const available = manifest.patches.map((entry) => entry.filename).join(', ');
59
- throw new InvalidArgumentError(`--scan-files patch "${assignment.patch}" not found in manifest.\n\nAvailable patches: ${available}`, '--scan-files');
59
+ // Suggest, never dump.
60
+ throw new InvalidArgumentError(`--scan-files: ${formatPatchNotFoundError(assignment.patch, manifest.patches)}`, '--scan-files');
60
61
  }
61
62
  const patchFiles = filesByPatch.get(patch.filename) ?? new Set();
62
63
  filesByPatch.set(patch.filename, patchFiles);
@@ -77,7 +78,7 @@ export async function loadScanFilesAssignments(manifestPath, manifest) {
77
78
  * disturb: the engine's commit + working-tree status (via the same generation
78
79
  * token `fireforge test` uses) and the byte content of every regular file
79
80
  * under `patches/` — ALL of them, not just the selected patches, because the
80
- * FORGE H1 field incident was a dry-run of patch B reverting a just-written
81
+ * field incident was a dry-run of patch B reverting a just-written
81
82
  * export of patch A. A missing patches directory (ENOENT/ENOTDIR) fingerprints
82
83
  * as empty — there is nothing there to protect — but any other listing failure
83
84
  * (EACCES, EIO, …) throws: an unreadable directory is not evidence it is
@@ -152,7 +153,7 @@ async function fingerprintDryRunState(engineDir, patchesDir) {
152
153
  return fingerprint;
153
154
  }
154
155
  /**
155
- * Runtime enforcement of the dry-run purity contract (FORGE H1): fingerprints
156
+ * Runtime enforcement of the dry-run purity contract: fingerprints
156
157
  * the engine tree and every patch artifact before and after `operation`, and
157
158
  * throws when a dry-run changed anything it promised only to inspect. This
158
159
  * turns any recurrence of the 0.40.0 "dry-run reverted a just-written export"
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export`
3
- * (FORGE J2).
2
+ * Foreign-drift preview + `--refuse-foreign-drift` gate for `re-export`.
4
3
  *
5
4
  * A scan-less re-export rebuilds each patch body as a whole-file
6
5
  * `git diff HEAD` over the owned files — so a CONCURRENT session's
@@ -23,11 +22,11 @@
23
22
  import type { PatchMetadata } from '../types/commands/index.js';
24
23
  /** Why a patch was refused under `--refuse-foreign-drift`. */
25
24
  export type ForeignDriftRefusalReason = 'foreign-drift' | 'baseline-unreadable';
26
- /** Per-run context threaded through the re-export loop (mirrors FORGE G2). */
25
+ /** Per-run context threaded through the re-export loop (mirrors). */
27
26
  export interface ForeignDriftContext {
28
27
  refuseForeignDrift: boolean;
29
28
  /**
30
- * Engine-relative files whose drift is expected (`--expect`, FORGE L6):
29
+ * Engine-relative files whose drift is expected (`--expect`):
31
30
  * drift confined to these files does not refuse. Files here that never
32
31
  * drift are reported after the run as likely typos.
33
32
  */
@@ -69,6 +68,7 @@ export declare function computeForeignDrift(oldBody: string, newBody: string, pr
69
68
  export declare function reportForeignDrift(args: {
70
69
  patch: PatchMetadata;
71
70
  patchesDir: string;
71
+ engineDir: string;
72
72
  newDiffContent: string;
73
73
  ctx: ForeignDriftContext;
74
74
  }): Promise<boolean>;