@hominis/fireforge 0.39.0 → 0.41.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 (279) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +35 -5
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +27 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +27 -9
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +30 -10
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +73 -51
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint-report.d.ts +21 -0
  45. package/dist/src/commands/lint-report.js +62 -0
  46. package/dist/src/commands/lint.js +26 -22
  47. package/dist/src/commands/manifest.js +3 -1
  48. package/dist/src/commands/package.js +9 -16
  49. package/dist/src/commands/patch/compact.js +5 -5
  50. package/dist/src/commands/patch/delete.js +5 -5
  51. package/dist/src/commands/patch/lint-ignore.js +3 -2
  52. package/dist/src/commands/patch/move-files-create.js +22 -4
  53. package/dist/src/commands/patch/move-files-into.js +14 -10
  54. package/dist/src/commands/patch/move-files.js +6 -5
  55. package/dist/src/commands/patch/rename.d.ts +2 -2
  56. package/dist/src/commands/patch/rename.js +152 -51
  57. package/dist/src/commands/patch/reorder.d.ts +9 -0
  58. package/dist/src/commands/patch/reorder.js +14 -10
  59. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  60. package/dist/src/commands/patch/split-plan.js +22 -12
  61. package/dist/src/commands/patch/split.d.ts +1 -1
  62. package/dist/src/commands/patch/split.js +23 -9
  63. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  64. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  65. package/dist/src/commands/patch/staged-dependency.js +112 -14
  66. package/dist/src/commands/re-export-adjacent.d.ts +34 -0
  67. package/dist/src/commands/re-export-adjacent.js +105 -0
  68. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  69. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  70. package/dist/src/commands/re-export-drift.d.ts +74 -0
  71. package/dist/src/commands/re-export-drift.js +234 -0
  72. package/dist/src/commands/re-export-files.js +5 -3
  73. package/dist/src/commands/re-export-lint.d.ts +97 -0
  74. package/dist/src/commands/re-export-lint.js +210 -0
  75. package/dist/src/commands/re-export-options.js +11 -0
  76. package/dist/src/commands/re-export-register.d.ts +18 -0
  77. package/dist/src/commands/re-export-register.js +61 -0
  78. package/dist/src/commands/re-export-scan.js +13 -7
  79. package/dist/src/commands/re-export-single.d.ts +20 -0
  80. package/dist/src/commands/re-export-single.js +261 -0
  81. package/dist/src/commands/re-export.d.ts +0 -4
  82. package/dist/src/commands/re-export.js +123 -289
  83. package/dist/src/commands/rebase/abort.js +13 -4
  84. package/dist/src/commands/rebase/continue.js +11 -4
  85. package/dist/src/commands/rebase/index.js +19 -29
  86. package/dist/src/commands/rebase/patch-loop.js +12 -0
  87. package/dist/src/commands/reset.js +4 -23
  88. package/dist/src/commands/resolve.js +2 -9
  89. package/dist/src/commands/run.js +29 -23
  90. package/dist/src/commands/source.js +3 -9
  91. package/dist/src/commands/status-check.d.ts +48 -0
  92. package/dist/src/commands/status-check.js +106 -0
  93. package/dist/src/commands/status-json.d.ts +31 -0
  94. package/dist/src/commands/status-json.js +76 -0
  95. package/dist/src/commands/status-output.d.ts +1 -0
  96. package/dist/src/commands/status-output.js +10 -1
  97. package/dist/src/commands/status-ownership.d.ts +52 -0
  98. package/dist/src/commands/status-ownership.js +57 -0
  99. package/dist/src/commands/status.js +190 -170
  100. package/dist/src/commands/test-build-phase.d.ts +19 -0
  101. package/dist/src/commands/test-build-phase.js +168 -0
  102. package/dist/src/commands/test-diagnose.js +30 -2
  103. package/dist/src/commands/test-modes.d.ts +9 -1
  104. package/dist/src/commands/test-modes.js +46 -13
  105. package/dist/src/commands/test-register.js +59 -27
  106. package/dist/src/commands/test-run.d.ts +29 -3
  107. package/dist/src/commands/test-run.js +59 -7
  108. package/dist/src/commands/test-verdict.d.ts +32 -0
  109. package/dist/src/commands/test-verdict.js +48 -0
  110. package/dist/src/commands/test.d.ts +9 -0
  111. package/dist/src/commands/test.js +100 -123
  112. package/dist/src/commands/token-coverage.js +4 -9
  113. package/dist/src/commands/token.js +4 -1
  114. package/dist/src/commands/tree.d.ts +19 -0
  115. package/dist/src/commands/tree.js +320 -0
  116. package/dist/src/commands/verify.js +2 -2
  117. package/dist/src/commands/watch.js +22 -21
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.d.ts +11 -0
  120. package/dist/src/core/ast-utils.js +11 -0
  121. package/dist/src/core/build-audit-platform.js +6 -1
  122. package/dist/src/core/build-audit-registration.js +2 -0
  123. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  124. package/dist/src/core/build-audit-resolve.js +34 -13
  125. package/dist/src/core/build-audit.js +4 -20
  126. package/dist/src/core/build-baseline-types.d.ts +29 -8
  127. package/dist/src/core/build-baseline-types.js +2 -1
  128. package/dist/src/core/build-baseline.d.ts +10 -2
  129. package/dist/src/core/build-baseline.js +39 -8
  130. package/dist/src/core/build-prepare.d.ts +4 -0
  131. package/dist/src/core/build-prepare.js +7 -1
  132. package/dist/src/core/config-paths.d.ts +1 -1
  133. package/dist/src/core/config-paths.js +2 -0
  134. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  135. package/dist/src/core/config-validate.js +45 -30
  136. package/dist/src/core/config.js +16 -3
  137. package/dist/src/core/coverage-extend.d.ts +89 -0
  138. package/dist/src/core/coverage-extend.js +188 -0
  139. package/dist/src/core/destructive.js +3 -1
  140. package/dist/src/core/discard-baseline.js +18 -4
  141. package/dist/src/core/engine-precondition.d.ts +35 -0
  142. package/dist/src/core/engine-precondition.js +50 -0
  143. package/dist/src/core/engine-session-lock.d.ts +38 -1
  144. package/dist/src/core/engine-session-lock.js +78 -4
  145. package/dist/src/core/file-lock.d.ts +7 -0
  146. package/dist/src/core/file-lock.js +20 -30
  147. package/dist/src/core/firefox.js +5 -2
  148. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  149. package/dist/src/core/furnace-apply-helpers.js +12 -3
  150. package/dist/src/core/furnace-apply-output.js +9 -0
  151. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  152. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  153. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  154. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  155. package/dist/src/core/furnace-apply.d.ts +16 -4
  156. package/dist/src/core/furnace-apply.js +39 -43
  157. package/dist/src/core/furnace-config.js +2 -0
  158. package/dist/src/core/furnace-css-fragments.js +2 -0
  159. package/dist/src/core/furnace-jsconfig.js +14 -7
  160. package/dist/src/core/furnace-registration-ast.js +8 -8
  161. package/dist/src/core/furnace-registration-remove.js +6 -3
  162. package/dist/src/core/furnace-registration.js +9 -12
  163. package/dist/src/core/furnace-rollback.js +1 -1
  164. package/dist/src/core/furnace-scanner.js +2 -1
  165. package/dist/src/core/furnace-stale-export.js +2 -1
  166. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  167. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  168. package/dist/src/core/furnace-validate-helpers.js +33 -1
  169. package/dist/src/core/furnace-validate-registration.js +16 -0
  170. package/dist/src/core/furnace-validate.js +4 -2
  171. package/dist/src/core/git-diff.js +37 -2
  172. package/dist/src/core/lint-cache.d.ts +15 -5
  173. package/dist/src/core/lint-cache.js +49 -14
  174. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  175. package/dist/src/core/mach-build-artifacts.js +108 -2
  176. package/dist/src/core/mach-resource-shim.js +1 -0
  177. package/dist/src/core/marionette-port.d.ts +16 -0
  178. package/dist/src/core/marionette-port.js +93 -8
  179. package/dist/src/core/marionette-preflight.js +6 -5
  180. package/dist/src/core/patch-apply.d.ts +20 -13
  181. package/dist/src/core/patch-apply.js +32 -33
  182. package/dist/src/core/patch-export.d.ts +2 -2
  183. package/dist/src/core/patch-export.js +4 -6
  184. package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
  185. package/dist/src/core/patch-lint-checkjs.js +132 -8
  186. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  187. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  188. package/dist/src/core/patch-lint-creators.js +45 -0
  189. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  190. package/dist/src/core/patch-lint-cross.js +16 -32
  191. package/dist/src/core/patch-lint-css.js +64 -16
  192. package/dist/src/core/patch-lint-diff.d.ts +8 -0
  193. package/dist/src/core/patch-lint-diff.js +31 -0
  194. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  195. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  196. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  197. package/dist/src/core/patch-lint-module-registration.js +88 -0
  198. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  199. package/dist/src/core/patch-lint-ownership.d.ts +21 -0
  200. package/dist/src/core/patch-lint-ownership.js +30 -0
  201. package/dist/src/core/patch-lint-reexports.d.ts +2 -1
  202. package/dist/src/core/patch-lint-reexports.js +1 -0
  203. package/dist/src/core/patch-lint.d.ts +31 -0
  204. package/dist/src/core/patch-lint.js +114 -8
  205. package/dist/src/core/patch-lock.d.ts +12 -1
  206. package/dist/src/core/patch-lock.js +40 -4
  207. package/dist/src/core/patch-manifest-consistency.js +15 -3
  208. package/dist/src/core/patch-manifest.d.ts +1 -1
  209. package/dist/src/core/patch-parse.d.ts +8 -0
  210. package/dist/src/core/patch-parse.js +10 -0
  211. package/dist/src/core/patch-policy.d.ts +7 -0
  212. package/dist/src/core/patch-policy.js +4 -1
  213. package/dist/src/core/patch-transform.d.ts +10 -0
  214. package/dist/src/core/patch-transform.js +13 -2
  215. package/dist/src/core/rebase-session.d.ts +44 -2
  216. package/dist/src/core/rebase-session.js +114 -17
  217. package/dist/src/core/register-browser-content.js +2 -1
  218. package/dist/src/core/register-shared-css.js +2 -1
  219. package/dist/src/core/smoke-patterns.d.ts +24 -16
  220. package/dist/src/core/smoke-patterns.js +26 -14
  221. package/dist/src/core/status-binary.d.ts +55 -0
  222. package/dist/src/core/status-binary.js +124 -0
  223. package/dist/src/core/status-classify.d.ts +22 -1
  224. package/dist/src/core/status-classify.js +112 -70
  225. package/dist/src/core/test-harness-crash.d.ts +32 -1
  226. package/dist/src/core/test-harness-crash.js +59 -4
  227. package/dist/src/core/test-path-scope.js +2 -0
  228. package/dist/src/core/test-stale-check.js +1 -20
  229. package/dist/src/core/token-category.d.ts +40 -0
  230. package/dist/src/core/token-category.js +264 -0
  231. package/dist/src/core/token-manager.d.ts +7 -0
  232. package/dist/src/core/token-manager.js +48 -196
  233. package/dist/src/core/toolchain-preflight.js +24 -26
  234. package/dist/src/core/tree-cow.d.ts +18 -0
  235. package/dist/src/core/tree-cow.js +71 -0
  236. package/dist/src/core/tree-guard.d.ts +35 -0
  237. package/dist/src/core/tree-guard.js +179 -0
  238. package/dist/src/core/tree-store.d.ts +129 -0
  239. package/dist/src/core/tree-store.js +452 -0
  240. package/dist/src/core/typecheck-shim.d.ts +14 -0
  241. package/dist/src/core/typecheck-shim.js +41 -2
  242. package/dist/src/core/typecheck.js +2 -1
  243. package/dist/src/core/wire-destroy.js +14 -3
  244. package/dist/src/core/wire-init.js +37 -40
  245. package/dist/src/core/wire-subscript.js +11 -3
  246. package/dist/src/core/wire-utils.js +4 -4
  247. package/dist/src/core/xpcshell-appdir.js +3 -0
  248. package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
  249. package/dist/src/core/xpcshell-profile-dir.js +45 -0
  250. package/dist/src/errors/base.d.ts +13 -0
  251. package/dist/src/errors/base.js +13 -0
  252. package/dist/src/errors/git.d.ts +8 -0
  253. package/dist/src/errors/git.js +17 -0
  254. package/dist/src/errors/rebase.d.ts +13 -0
  255. package/dist/src/errors/rebase.js +17 -0
  256. package/dist/src/index.d.ts +2 -0
  257. package/dist/src/index.js +1 -0
  258. package/dist/src/types/commands/options.d.ts +119 -3
  259. package/dist/src/types/commands/patches.d.ts +8 -0
  260. package/dist/src/types/config.d.ts +19 -0
  261. package/dist/src/types/furnace.d.ts +7 -0
  262. package/dist/src/utils/build-info.d.ts +31 -0
  263. package/dist/src/utils/build-info.js +98 -0
  264. package/dist/src/utils/concurrency.d.ts +8 -0
  265. package/dist/src/utils/concurrency.js +21 -0
  266. package/dist/src/utils/errors.d.ts +39 -0
  267. package/dist/src/utils/errors.js +55 -0
  268. package/dist/src/utils/fs.js +5 -12
  269. package/dist/src/utils/logger.d.ts +14 -2
  270. package/dist/src/utils/logger.js +43 -13
  271. package/dist/src/utils/options.d.ts +21 -4
  272. package/dist/src/utils/options.js +26 -6
  273. package/dist/src/utils/package-root.js +7 -4
  274. package/dist/src/utils/process-group.js +13 -2
  275. package/dist/src/utils/stdio-drain.d.ts +28 -0
  276. package/dist/src/utils/stdio-drain.js +54 -0
  277. package/dist/src/utils/validation.d.ts +33 -2
  278. package/dist/src/utils/validation.js +57 -2
  279. package/package.json +13 -13
@@ -8,7 +8,7 @@ import { GeneralError } from '../errors/base.js';
8
8
  import { BuildError } from '../errors/build.js';
9
9
  import { pathExists, readText, writeText } from '../utils/fs.js';
10
10
  import { escapeRegex } from '../utils/regex.js';
11
- import { detectIndent, getNodeSource, parseScript } from './ast-utils.js';
11
+ import { asEstree, detectIndent, getNodeSource, parseScript, } from './ast-utils.js';
12
12
  import { withParserFallback } from './parser-fallback.js';
13
13
  import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findInsertionAfterFireforgeBlocks, findMethodBody, findMethodBraceIndex, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
14
14
  const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
@@ -18,6 +18,30 @@ const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
18
18
  * fallback code paths agree on the shape.
19
19
  */
20
20
  const DEFAULT_MARKER = 'FIREFORGE:';
21
+ /**
22
+ * Default insertion point: after the last consecutive fireforge block, or at
23
+ * the top of the method body when there is none.
24
+ *
25
+ * Both callers below — the `--after`-target-not-found fallthrough and the
26
+ * no-`--after` default — ran a character-for-character copy of this ladder,
27
+ * including both throw guards. `browser-init.js` is written through those two
28
+ * independent paths and they MUST emit identical blocks, so having one
29
+ * implementation enforces the invariant rather than merely tidying.
30
+ */
31
+ function resolveDefaultInsertion(content, body, fireforgeBlocks) {
32
+ if (fireforgeBlocks.length > 0) {
33
+ const lastBlock = fireforgeBlocks[fireforgeBlocks.length - 1];
34
+ if (!lastBlock)
35
+ throw new GeneralError('Unexpected empty fireforgeBlocks array');
36
+ return { insertPos: lastBlock.end, indent: detectIndent(content, lastBlock.start) };
37
+ }
38
+ const firstStmt = body.body[0];
39
+ if (firstStmt) {
40
+ const insertPos = asEstree(firstStmt).start;
41
+ return { insertPos, indent: detectIndent(content, insertPos) };
42
+ }
43
+ return { insertPos: body.start + 1, indent: ' ' };
44
+ }
21
45
  /**
22
46
  * AST-based implementation: finds onLoad() method body, locates existing
23
47
  * fireforge init blocks (TryStatements containing typeof guards), and inserts
@@ -47,7 +71,7 @@ export function addInitAST(content, expression, after, marker = DEFAULT_MARKER)
47
71
  const fireforgeBlocks = [];
48
72
  for (const stmt of body.body) {
49
73
  if (stmt.type === 'TryStatement') {
50
- const tryNode = stmt;
74
+ const tryNode = asEstree(stmt);
51
75
  const src = getNodeSource(content, tryNode);
52
76
  if (/typeof\s+\w+\s*!==\s*"undefined"/.test(src)) {
53
77
  fireforgeBlocks.push(tryNode);
@@ -68,47 +92,12 @@ export function addInitAST(content, expression, after, marker = DEFAULT_MARKER)
68
92
  }
69
93
  else {
70
94
  // --after target not found: fall through to default (after last fireforge block)
71
- if (fireforgeBlocks.length > 0) {
72
- const lastBlock = fireforgeBlocks[fireforgeBlocks.length - 1];
73
- if (!lastBlock)
74
- throw new GeneralError('Unexpected empty fireforgeBlocks array');
75
- insertPos = lastBlock.end;
76
- indent = detectIndent(content, lastBlock.start);
77
- }
78
- else {
79
- // No fireforge blocks, insert at top of method body
80
- const firstStmt = body.body[0];
81
- if (firstStmt) {
82
- insertPos = firstStmt.start;
83
- indent = detectIndent(content, insertPos);
84
- }
85
- else {
86
- insertPos = body.start + 1;
87
- indent = ' ';
88
- }
89
- }
95
+ ({ insertPos, indent } = resolveDefaultInsertion(content, body, fireforgeBlocks));
90
96
  }
91
97
  }
92
98
  else {
93
99
  // Default: insert after the last consecutive fireforge block at the start
94
- if (fireforgeBlocks.length > 0) {
95
- const lastBlock = fireforgeBlocks[fireforgeBlocks.length - 1];
96
- if (!lastBlock)
97
- throw new GeneralError('Unexpected empty fireforgeBlocks array');
98
- insertPos = lastBlock.end;
99
- indent = detectIndent(content, lastBlock.start);
100
- }
101
- else {
102
- const firstStmt = body.body[0];
103
- if (firstStmt) {
104
- insertPos = firstStmt.start;
105
- indent = detectIndent(content, insertPos);
106
- }
107
- else {
108
- insertPos = body.start + 1;
109
- indent = ' ';
110
- }
111
- }
100
+ ({ insertPos, indent } = resolveDefaultInsertion(content, body, fireforgeBlocks));
112
101
  }
113
102
  const block = [
114
103
  `${indent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
@@ -224,7 +213,15 @@ export async function addInitToBrowserInit(engineDir, expression, after, marker
224
213
  if (initPattern.test(content)) {
225
214
  return false;
226
215
  }
227
- const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS);
216
+ const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS,
217
+ // Rethrow only the internal-invariant GeneralErrors ("Unexpected empty
218
+ // …array"): those are programming bugs, and retrying the legacy scanner
219
+ // cannot fix a broken invariant — it just buries the stack. Everything
220
+ // else still falls back, which is load-bearing: the AST path raises a raw
221
+ // acorn SyntaxError on chrome sources acorn cannot parse (preprocessor
222
+ // directives), and BuildError when the file's shape is unexpected. Both
223
+ // are exactly what the legacy scanner is here to handle.
224
+ (error) => error instanceof GeneralError);
228
225
  if (usedFallback) {
229
226
  assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
230
227
  }
@@ -7,7 +7,7 @@ import MagicString from 'magic-string';
7
7
  import { GeneralError } from '../errors/base.js';
8
8
  import { BuildError } from '../errors/build.js';
9
9
  import { pathExists, readText, writeText } from '../utils/fs.js';
10
- import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
10
+ import { asEstree, detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.js';
11
11
  import { withParserFallback } from './parser-fallback.js';
12
12
  import { assertBraceBalancePreserved, findNearestTryLine, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
13
13
  const BROWSER_MAIN_JS = 'browser/base/content/browser-main.js';
@@ -28,7 +28,7 @@ export function addSubscriptAST(content, name, marker = DEFAULT_MARKER) {
28
28
  walkAST(ast, {
29
29
  enter(node) {
30
30
  if (node.type === 'TryStatement') {
31
- const n = node;
31
+ const n = asEstree(node);
32
32
  const src = getNodeSource(content, n);
33
33
  if (src.includes('loadSubScript')) {
34
34
  tryNodes.push(n);
@@ -137,7 +137,15 @@ export async function addSubscriptToBrowserMain(engineDir, name, marker = DEFAUL
137
137
  if (content.includes(`content/${name}.js"`)) {
138
138
  return false;
139
139
  }
140
- const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS);
140
+ const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS,
141
+ // Rethrow only the internal-invariant GeneralErrors ("Unexpected empty
142
+ // …array"): those are programming bugs, and retrying the legacy scanner
143
+ // cannot fix a broken invariant — it just buries the stack. Everything
144
+ // else still falls back, which is load-bearing: the AST path raises a raw
145
+ // acorn SyntaxError on chrome sources acorn cannot parse (preprocessor
146
+ // directives), and BuildError when the file's shape is unexpected. Both
147
+ // are exactly what the legacy scanner is here to handle.
148
+ (error) => error instanceof GeneralError);
141
149
  if (usedFallback) {
142
150
  assertBraceBalancePreserved(content, value, BROWSER_MAIN_JS);
143
151
  }
@@ -1,5 +1,5 @@
1
1
  import { GeneralError, ParserFallbackError } from '../errors/base.js';
2
- import { walkAST } from './ast-utils.js';
2
+ import { asEstree, walkAST } from './ast-utils.js';
3
3
  /**
4
4
  * Validates a name for safe interpolation into generated JavaScript string literals.
5
5
  * Rejects strings containing characters that could break out of JS strings or inject code.
@@ -336,13 +336,13 @@ export function findMethodBody(ast, methodName) {
336
336
  if (found)
337
337
  return;
338
338
  if (node.type === 'Property') {
339
- const prop = node;
339
+ const prop = asEstree(node);
340
340
  if (prop.key.type === 'Identifier' &&
341
341
  names.includes(prop.key.name) &&
342
342
  (prop.value.type === 'FunctionExpression' ||
343
343
  prop.value.type === 'ArrowFunctionExpression')) {
344
- const fn = prop.value;
345
- found = fn.body;
344
+ const fn = asEstree(prop.value);
345
+ found = asEstree(fn.body);
346
346
  }
347
347
  }
348
348
  },
@@ -203,6 +203,7 @@ export async function resolveAbsoluteAppPath(objDirAbs, relativeAppdir) {
203
203
  entries = await readdir(distDir);
204
204
  }
205
205
  catch {
206
+ // No readable dist directory means no appdir candidate can be resolved.
206
207
  return null;
207
208
  }
208
209
  for (const entry of entries) {
@@ -254,6 +255,8 @@ export async function resolveXpcshellAppdirArg(engineDir, testPaths, objDirName)
254
255
  body = await readText(manifestPath);
255
256
  }
256
257
  catch {
258
+ // An unreadable manifest cannot declare an appdir; skip it and keep
259
+ // walking the remaining candidates.
257
260
  continue;
258
261
  }
259
262
  // Operator already migrated — harness will read the appname-keyed
@@ -0,0 +1,12 @@
1
+ export declare const XPCSHELL_PROFILE_ENV_VAR = "XPCSHELL_TEST_PROFILE_DIR";
2
+ /**
3
+ * Runs `fn` with a fresh per-invocation xpcshell profile directory
4
+ * injected into the environment, removing the directory afterwards
5
+ * (best-effort — a failure to clean up warns and leaves the mkdtemp dir
6
+ * for the OS tmp reaper; the harness may still hold files briefly).
7
+ *
8
+ * An operator-provided `XPCSHELL_TEST_PROFILE_DIR` (in `baseEnv` or the
9
+ * process environment) is respected verbatim: no directory is minted and
10
+ * NOTHING is deleted — we never remove a directory we did not create.
11
+ */
12
+ export declare function withXpcshellProfileDir<T>(baseEnv: Record<string, string> | undefined, fn: (env: Record<string, string>) => Promise<T>): Promise<T>;
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Per-invocation xpcshell profile directory (FORGE G15b).
4
+ *
5
+ * Firefox's xpcshell harness defaults `XPCSHELL_TEST_PROFILE_DIR` to a
6
+ * FIXED path (`$TMPDIR/firefox/xpcshellprofile`), so two overlapping
7
+ * harness invocations — concurrent `fireforge test` processes, or future
8
+ * verification-tree shards — corrupt each other's profiles. FireForge
9
+ * never set the variable; this helper mints a fresh `mkdtemp` directory
10
+ * per harness invocation and exports it through the mach env channel.
11
+ */
12
+ import { mkdtemp, rm } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { toError } from '../utils/errors.js';
16
+ import { warn } from '../utils/logger.js';
17
+ export const XPCSHELL_PROFILE_ENV_VAR = 'XPCSHELL_TEST_PROFILE_DIR';
18
+ /**
19
+ * Runs `fn` with a fresh per-invocation xpcshell profile directory
20
+ * injected into the environment, removing the directory afterwards
21
+ * (best-effort — a failure to clean up warns and leaves the mkdtemp dir
22
+ * for the OS tmp reaper; the harness may still hold files briefly).
23
+ *
24
+ * An operator-provided `XPCSHELL_TEST_PROFILE_DIR` (in `baseEnv` or the
25
+ * process environment) is respected verbatim: no directory is minted and
26
+ * NOTHING is deleted — we never remove a directory we did not create.
27
+ */
28
+ export async function withXpcshellProfileDir(baseEnv, fn) {
29
+ const operatorProvided = baseEnv?.[XPCSHELL_PROFILE_ENV_VAR] ?? process.env[XPCSHELL_PROFILE_ENV_VAR];
30
+ if (operatorProvided !== undefined && operatorProvided !== '') {
31
+ return fn({ ...baseEnv, [XPCSHELL_PROFILE_ENV_VAR]: operatorProvided });
32
+ }
33
+ const profileDir = await mkdtemp(join(tmpdir(), 'fireforge-xpcshell-profile-'));
34
+ try {
35
+ return await fn({ ...baseEnv, [XPCSHELL_PROFILE_ENV_VAR]: profileDir });
36
+ }
37
+ finally {
38
+ try {
39
+ await rm(profileDir, { recursive: true, force: true });
40
+ }
41
+ catch (error) {
42
+ warn(`Could not clean up xpcshell profile dir ${profileDir}: ${toError(error).message}`);
43
+ }
44
+ }
45
+ }
@@ -83,6 +83,19 @@ export declare class ExecTimeoutError extends FireForgeError {
83
83
  timeoutMs: number, cause?: unknown);
84
84
  get userMessage(): string;
85
85
  }
86
+ /**
87
+ * Error thrown when a file-lock wait times out because another process
88
+ * holds the lock (FORGE H5).
89
+ *
90
+ * Dedicated subclass so the CLI boundary renders lock contention as the
91
+ * one-line reason-first/remedy-second refusal it is — before this, the
92
+ * timeout surfaced as a plain `Error`, which `withErrorHandling` treats
93
+ * as an internal failure and prints with a five-frame stack. The refusal
94
+ * was always correct; the presentation made it look like a crash.
95
+ */
96
+ export declare class LockContentionError extends FireForgeError {
97
+ readonly code: 1;
98
+ }
86
99
  /**
87
100
  * Error thrown when the user cancels an interactive prompt.
88
101
  */
@@ -113,6 +113,19 @@ export class ExecTimeoutError extends FireForgeError {
113
113
  'If the host is just slow or loaded, re-running may succeed.');
114
114
  }
115
115
  }
116
+ /**
117
+ * Error thrown when a file-lock wait times out because another process
118
+ * holds the lock (FORGE H5).
119
+ *
120
+ * Dedicated subclass so the CLI boundary renders lock contention as the
121
+ * one-line reason-first/remedy-second refusal it is — before this, the
122
+ * timeout surfaced as a plain `Error`, which `withErrorHandling` treats
123
+ * as an internal failure and prints with a five-frame stack. The refusal
124
+ * was always correct; the presentation made it look like a crash.
125
+ */
126
+ export class LockContentionError extends FireForgeError {
127
+ code = ExitCode.GENERAL_ERROR;
128
+ }
116
129
  /**
117
130
  * Error thrown when the user cancels an interactive prompt.
118
131
  */
@@ -39,6 +39,14 @@ export declare class GitIndexLockError extends GitError {
39
39
  constructor(lockPath: string, ageMs?: number | undefined);
40
40
  get userMessage(): string;
41
41
  }
42
+ /**
43
+ * Detects transient git `index.lock` contention — an external git process
44
+ * holding `.git/index.lock` while we stage or diff. Mirrors the message
45
+ * heuristics `core/git.ts` uses when wrapping stale-lock failures during
46
+ * download, but lives here so command modules can consult it without
47
+ * importing (frequently vi.mocked) core git internals.
48
+ */
49
+ export declare function isGitIndexLockConflict(error: unknown): boolean;
42
50
  /**
43
51
  * Error thrown when `git add` (monolithic or chunked) exceeds the
44
52
  * configured timeout while indexing the Firefox source tree.
@@ -96,6 +96,23 @@ export class GitIndexLockError extends GitError {
96
96
  ' 3. Re-run "fireforge download --force"');
97
97
  }
98
98
  }
99
+ /**
100
+ * Detects transient git `index.lock` contention — an external git process
101
+ * holding `.git/index.lock` while we stage or diff. Mirrors the message
102
+ * heuristics `core/git.ts` uses when wrapping stale-lock failures during
103
+ * download, but lives here so command modules can consult it without
104
+ * importing (frequently vi.mocked) core git internals.
105
+ */
106
+ export function isGitIndexLockConflict(error) {
107
+ if (error instanceof GitIndexLockError) {
108
+ return true;
109
+ }
110
+ if (!(error instanceof GitError)) {
111
+ return false;
112
+ }
113
+ return (/index\.lock/i.test(error.message) &&
114
+ /(unable to create|another git process seems to be running|file exists|locked)/i.test(error.message));
115
+ }
99
116
  /**
100
117
  * Error thrown when `git add` (monolithic or chunked) exceeds the
101
118
  * configured timeout while indexing the Firefox source tree.
@@ -18,3 +18,16 @@ export declare class RebaseSessionExistsError extends RebaseError {
18
18
  export declare class NoRebaseSessionError extends RebaseError {
19
19
  constructor();
20
20
  }
21
+ /**
22
+ * Thrown when the session file exists but cannot be used.
23
+ *
24
+ * This case had no error of its own before 0.41.0 and was reported as
25
+ * {@link NoRebaseSessionError} by `--continue`/`--abort` while `rebase`
26
+ * reported {@link RebaseSessionExistsError} — a closed cycle in which each
27
+ * command pointed at the other two. The message names the file so an operator
28
+ * always has a way out, and `--abort` now clears a corrupt session rather than
29
+ * refusing to run against one.
30
+ */
31
+ export declare class CorruptRebaseSessionError extends RebaseError {
32
+ constructor(sessionPath: string, reason: string);
33
+ }
@@ -32,3 +32,20 @@ export class NoRebaseSessionError extends RebaseError {
32
32
  super('No rebase session in progress. Start one with "fireforge rebase".');
33
33
  }
34
34
  }
35
+ /**
36
+ * Thrown when the session file exists but cannot be used.
37
+ *
38
+ * This case had no error of its own before 0.41.0 and was reported as
39
+ * {@link NoRebaseSessionError} by `--continue`/`--abort` while `rebase`
40
+ * reported {@link RebaseSessionExistsError} — a closed cycle in which each
41
+ * command pointed at the other two. The message names the file so an operator
42
+ * always has a way out, and `--abort` now clears a corrupt session rather than
43
+ * refusing to run against one.
44
+ */
45
+ export class CorruptRebaseSessionError extends RebaseError {
46
+ constructor(sessionPath, reason) {
47
+ super(`The rebase session at ${sessionPath} cannot be read (${reason}).\n` +
48
+ 'Run "fireforge rebase --abort" to discard it and restore the engine, ' +
49
+ 'or delete the file to start over.');
50
+ }
51
+ }
@@ -14,6 +14,8 @@ export { loadConfig, validateConfig } from './core/config.js';
14
14
  export { applyAllComponents } from './core/furnace-apply.js';
15
15
  export { ensureFurnaceConfig, loadFurnaceConfig, loadFurnaceState, saveFurnaceState, validateFurnaceConfig, } from './core/furnace-config.js';
16
16
  export { validateAllComponents, validateComponent } from './core/furnace-validate.js';
17
+ export type { PatchSizeTierDecision } from './core/patch-lint.js';
18
+ export { countNonBinaryDiffLines, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
17
19
  export type { AddTokenOptions, AddTokenResult, TokenMode } from './core/token-manager.js';
18
20
  export { addToken, getTokensCssPath, validateTokenAdd } from './core/token-manager.js';
19
21
  export { CancellationError, CommandError, FireForgeError, GeneralError, InvalidArgumentError, ResolutionError, } from './errors/base.js';
package/dist/src/index.js CHANGED
@@ -15,6 +15,7 @@ export { loadConfig, validateConfig } from './core/config.js';
15
15
  export { applyAllComponents } from './core/furnace-apply.js';
16
16
  export { ensureFurnaceConfig, loadFurnaceConfig, loadFurnaceState, saveFurnaceState, validateFurnaceConfig, } from './core/furnace-config.js';
17
17
  export { validateAllComponents, validateComponent } from './core/furnace-validate.js';
18
+ export { countNonBinaryDiffLines, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
18
19
  export { addToken, getTokensCssPath, validateTokenAdd } from './core/token-manager.js';
19
20
  export { CancellationError, CommandError, FireForgeError, GeneralError, InvalidArgumentError, ResolutionError, } from './errors/base.js';
20
21
  export { ExitCode } from './errors/codes.js';
@@ -279,6 +279,41 @@ export interface ReExportOptions {
279
279
  * the command implementation ignores it.
280
280
  */
281
281
  waitLock?: number | boolean;
282
+ /**
283
+ * Refuse (non-zero exit, patch not written) a scan-less re-export when
284
+ * unmanaged files exist adjacent to the patch's ownership, instead of
285
+ * warning. Gate-driven workflows use this so a freshly created file
286
+ * beside a patch's owned files cannot be silently left out of the
287
+ * refreshed body. Only meaningful on the plain path — mutually
288
+ * exclusive with `--scan` and `--files`, which set filesAffected
289
+ * explicitly.
290
+ */
291
+ refuseAdjacentUnmanaged?: boolean;
292
+ /**
293
+ * Bypass per-patch lint result cache reads AND writes for this
294
+ * invocation (`--no-cache`). The cache is what makes a repeat
295
+ * re-export of an unchanged patch skip the checkJs program build
296
+ * entirely (FORGE J1).
297
+ */
298
+ noCache?: boolean;
299
+ /**
300
+ * Refuse (non-zero exit, patch not written) a re-export whose refreshed
301
+ * body would absorb engine lines not present in the old patch body —
302
+ * the multi-session guard against silently capturing another session's
303
+ * uncommitted edits in files the patch already owns. The drift preview
304
+ * always prints; this flag turns it into a hard stop. Mutually
305
+ * exclusive with `--scan` and `--files`.
306
+ */
307
+ refuseForeignDrift?: boolean;
308
+ /**
309
+ * Engine-relative files whose drift is EXPECTED (`--expect`, repeatable;
310
+ * requires `--refuse-foreign-drift`). The content-based drift detector
311
+ * cannot tell the exporting session's own slice edits from another
312
+ * session's (FORGE L6), so a real slice export under the refusal flag
313
+ * names its intended files here: drift confined to expected files
314
+ * proceeds, drift anywhere else still refuses.
315
+ */
316
+ expect?: string[];
282
317
  }
283
318
  /**
284
319
  * Options for the `fireforge patch tier` subcommand. Sets or clears the
@@ -376,6 +411,8 @@ export interface PatchSplitOptions {
376
411
  forceUnsafe?: boolean;
377
412
  /** Skip per-patch lint of the projected bodies. */
378
413
  skipLint?: boolean;
414
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
415
+ waitLock?: number | boolean;
379
416
  }
380
417
  /**
381
418
  * Options for the `fireforge patch move-files` subcommand. Without
@@ -403,6 +440,8 @@ export interface PatchMoveFilesOptions {
403
440
  forceUnsafe?: boolean;
404
441
  /** Skip per-patch lint of the projected bodies. */
405
442
  skipLint?: boolean;
443
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
444
+ waitLock?: number | boolean;
406
445
  }
407
446
  /**
408
447
  * Options for the rebase command.
@@ -474,6 +513,24 @@ export interface TestOptions {
474
513
  headless?: boolean;
475
514
  /** Run incremental UI build before testing */
476
515
  build?: boolean;
516
+ /**
517
+ * Run the scoped pre-test build (packaging exactly the requested paths,
518
+ * MIXED harnesses allowed) and exit without dispatching tests. One
519
+ * ~10-minute build then covers both an xpcshell and a browser-chrome
520
+ * half of a slice; run each harness afterwards without `--build`
521
+ * (FORGE J9). Emits `FIREFORGE-VERDICT: PASS` on success.
522
+ */
523
+ buildOnly?: boolean;
524
+ /**
525
+ * Union the paths this build packages onto the recorded coverage claim
526
+ * instead of REPLACING it (FORGE L1), so successive scoped builds
527
+ * accumulate and an earlier slice's build-less runs stay covered. Valid
528
+ * only with `--build`/`--build-only` and explicit paths. Refused
529
+ * fail-closed unless the build anchor is unchanged: same engine HEAD,
530
+ * same `engine/mozconfig`, and every previously fingerprinted packageable
531
+ * file byte-identical. See `src/core/coverage-extend.ts`.
532
+ */
533
+ extendCoverage?: boolean;
477
534
  /** Forward mach's pathless auto-selection mode. Valid only with no explicit paths. */
478
535
  auto?: boolean;
479
536
  /**
@@ -735,6 +792,8 @@ export interface PatchDeleteOptions {
735
792
  dryRun?: boolean;
736
793
  /** Bypass the hard refusal when later patches depend on the target. */
737
794
  forceUnsafe?: boolean;
795
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
796
+ waitLock?: number | boolean;
738
797
  }
739
798
  /**
740
799
  * Options for the `fireforge patch rename` subcommand. Updates the
@@ -750,9 +809,22 @@ export interface PatchRenameOptions {
750
809
  * New human-readable name. Sanitised the same way `export --name`
751
810
  * sanitises into the filename slug (lowercase, non-alphanumerics
752
811
  * collapsed to `-`, length-capped). The patch's `name` field stores
753
- * the raw value; the filename uses the sanitised slug.
812
+ * the raw value; the filename uses the sanitised slug. Optional when
813
+ * `--category`/`--order`/`--description` carry the change.
754
814
  */
755
815
  to?: string;
816
+ /**
817
+ * New category (validated against configured categories). Rewrites
818
+ * the filename prefix and the manifest row in one transaction —
819
+ * recategorising no longer requires `patch delete` + fresh export.
820
+ */
821
+ category?: string;
822
+ /**
823
+ * Move the patch to this exact UNUSED sparse order. Collisions refuse
824
+ * with a pointer to `patch reorder` (which renumbers siblings); an
825
+ * order change is projected through cross-patch lint first.
826
+ */
827
+ order?: number;
756
828
  /**
757
829
  * Replacement description. Omit to leave the description unchanged
758
830
  * (intentional — operators frequently want to relabel the slug
@@ -765,6 +837,8 @@ export interface PatchRenameOptions {
765
837
  yes?: boolean;
766
838
  /** Bypass force-mode patchPolicy refusals. */
767
839
  forceUnsafe?: boolean;
840
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
841
+ waitLock?: number | boolean;
768
842
  }
769
843
  /**
770
844
  * Options for the patch reorder command.
@@ -776,6 +850,8 @@ export interface PatchReorderOptions {
776
850
  yes?: boolean;
777
851
  dryRun?: boolean;
778
852
  forceUnsafe?: boolean;
853
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
854
+ waitLock?: number | boolean;
779
855
  }
780
856
  /**
781
857
  * Options for the patch compact command.
@@ -787,6 +863,8 @@ export interface PatchCompactOptions {
787
863
  dryRun?: boolean;
788
864
  /** Bypass force-mode patchPolicy refusals. */
789
865
  forceUnsafe?: boolean;
866
+ /** Wait budget for the patch directory lock (`--wait-lock [seconds]`). */
867
+ waitLock?: number | boolean;
790
868
  }
791
869
  /**
792
870
  * Options for the status command.
@@ -809,6 +887,36 @@ export interface StatusOptions {
809
887
  testCoverage?: boolean;
810
888
  /** Output machine-readable JSON instead of human-readable text. */
811
889
  json?: boolean;
890
+ /**
891
+ * With `--json`: emit only the summary counts (plus the offending
892
+ * files per fail-set classification when the `--check`/`--fail-on`
893
+ * policy is active), omitting the per-file `files[]` payload that
894
+ * grows with the queue (FORGE K8). Refused without `--json`.
895
+ */
896
+ summary?: boolean;
897
+ /**
898
+ * With `--json` (and composing with `--summary`), append an additive
899
+ * `ownership` block — the flat path→owning-patch rows plus
900
+ * managed/unmanaged/conflict counts — so a gate reads ownership,
901
+ * classification, and the check verdict from ONE scan instead of three
902
+ * back-to-back `status` invocations (FORGE L3). A modifier, not a mode:
903
+ * exit semantics are unchanged, so ownership conflicts still fail only
904
+ * the human `--ownership` mode. Refused without `--json`.
905
+ */
906
+ includeOwnership?: boolean;
907
+ /**
908
+ * Exit non-zero when any classification in the fail policy is
909
+ * non-empty (default policy: unmanaged, patch-owned-drift, conflict).
910
+ * Composes with the default view and `--json`; refused alongside
911
+ * `--raw`/`--unmanaged`/`--ownership`/`--test-coverage` (FORGE G1).
912
+ */
913
+ check?: boolean;
914
+ /**
915
+ * Comma-separated classification list replacing the default `--check`
916
+ * policy. Implies `--check`. Unknown values refuse, naming the valid
917
+ * classification set.
918
+ */
919
+ failOn?: string;
812
920
  }
813
921
  /**
814
922
  * Options for the token add command.
@@ -900,8 +1008,9 @@ export interface LintCommandOptions {
900
1008
  * the cross-patch rules once over the whole queue so queue-level
901
1009
  * findings (duplicate creations, forward imports) still surface.
902
1010
  *
903
- * Mutually exclusive with passing explicit file paths — the two
904
- * scope contracts are different.
1011
+ * Positional file arguments change meaning under this flag: they are
1012
+ * PATCH selectors resolved like {@link patches} entries, not engine
1013
+ * file paths (FORGE G14).
905
1014
  */
906
1015
  perPatch?: boolean;
907
1016
  /**
@@ -923,4 +1032,11 @@ export interface LintCommandOptions {
923
1032
  * for CLI consistency, but only `--per-patch` currently uses the cache.
924
1033
  */
925
1034
  noCache?: boolean;
1035
+ /**
1036
+ * With `--per-patch`, write a machine-readable JSON report (schemaVersion
1037
+ * 1: per-patch lineCount, filesAffected, tier, thresholds, issues, and
1038
+ * lintIgnore-suppressed issues) to this path (FORGE G9/G10). Requires
1039
+ * {@link perPatch}.
1040
+ */
1041
+ report?: string;
926
1042
  }
@@ -212,6 +212,14 @@ export interface PatchLintIssue {
212
212
  fingerprint?: string;
213
213
  /** Human-readable description of the issue */
214
214
  message: string;
215
+ /**
216
+ * Queue entry filenames this issue implicates (the declaring/importing
217
+ * patch, or every creator for a duplicate-creation clash). Used by the
218
+ * export placement gate to attribute projected errors to the exported
219
+ * patch vs pre-existing patches without parsing messages or the
220
+ * rename-sensitive fingerprint (FORGE K9).
221
+ */
222
+ patches?: string[];
215
223
  /** Severity: errors block export, warnings are advisory, notices are informational (not counted) */
216
224
  severity: 'error' | 'warning' | 'notice';
217
225
  /**
@@ -253,6 +253,25 @@ export interface PatchLintConfig {
253
253
  * behaviour (built-in shim only).
254
254
  */
255
255
  checkJsExtraShim?: string;
256
+ /**
257
+ * Extend the checkJs pass to patch-owned test `.js` files
258
+ * (`browser_*` / `test_*` / `xpcshell_*` basenames and files under a
259
+ * `/test/` path), each checked as its own small script-scope program
260
+ * with same-directory patch-owned `head*.js` helpers included (FORGE
261
+ * G5 — a harness global that does not exist should fail at the patch
262
+ * boundary where the test was authored, not in a downstream
263
+ * jsconfig-project run). Opt-in: it is a new failure surface with
264
+ * nonzero compile cost. Requires `checkJs: true`.
265
+ */
266
+ checkJsTestFiles?: boolean;
267
+ /**
268
+ * Project-relative `.d.ts` appended to the built-in test-harness shim
269
+ * (loose `TestUtils`/`BrowserTestUtils`/`add_task`/… declarations) for
270
+ * the `checkJsTestFiles` pass — a consumer-typed `TestUtils` here is
271
+ * what turns a call to a nonexistent harness member into a TS2339 at
272
+ * export time. Requires `checkJsTestFiles: true`.
273
+ */
274
+ checkJsTestShim?: string;
256
275
  /** File paths exempt from the raw-color-value check (exact or basename match) */
257
276
  rawColorAllowlist?: string[];
258
277
  /** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
@@ -260,6 +260,13 @@ export interface ApplyResult {
260
260
  * persisted — the engine has been restored to its pre-apply state.
261
261
  */
262
262
  rolledBack?: boolean;
263
+ /**
264
+ * Operator-facing warnings, printed by `logApplyResult`. Populated when
265
+ * apply overwrote a deployed, patch-owned file whose engine bytes
266
+ * differed from the component source (FORGE J6) — the case where a
267
+ * shipped engine-only fix is silently reverted by the copy.
268
+ */
269
+ warnings?: string[];
263
270
  }
264
271
  /**
265
272
  * An action that would be performed during a dry-run deploy.