@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
@@ -61,9 +61,17 @@ export interface GroupedCheckJsResult {
61
61
  * the built-in Firefox-globals shim (from `patchLint.checkJsExtraShim`)
62
62
  * @param projectRoot - Absolute project root for resolving `extraShimPath`
63
63
  * @param mode - Strictness preset plus allowlisted compiler-option overrides
64
+ * @param builtinShimSuffix - Optional shim text appended AFTER the consumer shim
65
+ * @param rootScope - When set, only these repo-relative files become program
66
+ * ROOTS; resolution (and the host allowlist) still spans all of
67
+ * `resolutionOwned`, so a subset root's cross-patch imports type-check
68
+ * against the real owning sources while unrelated owned files are never
69
+ * parsed. `.mjs` files are module-scoped, so a root's diagnostics are
70
+ * identical whether other owned files are roots or mere resolution
71
+ * targets (pinned by the rootScope parity test — FORGE J1c).
64
72
  * @returns Diagnostics grouped per owning file plus run-level errors
65
73
  */
66
- export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode): Promise<GroupedCheckJsResult>;
74
+ export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode, builtinShimSuffix?: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
67
75
  /**
68
76
  * Flattens a {@link runCheckJsGrouped} run into a single issue list. When
69
77
  * `reportScope` is supplied, only diagnostics from files in that set are
@@ -93,6 +101,24 @@ export declare function runCheckJs(repoDir: string, patchOwnedFiles: Set<string>
93
101
  * not surface other patches' diagnostics)
94
102
  */
95
103
  export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, reportScope?: ReadonlySet<string>): Promise<PatchLintIssue[]>;
104
+ /**
105
+ * Runs the checkJs pass over patch-owned test `.js` files (FORGE G5,
106
+ * `patchLint.checkJsTestFiles`). Each test file gets its OWN small
107
+ * program — mochitest scripts share top-level scope only with their
108
+ * directory's `head*.js` helpers, so one multi-script program would emit
109
+ * false cross-file redeclaration errors — with roots = the test file plus
110
+ * any same-directory patch-owned `head*.js`, checked against the built-in
111
+ * Firefox shim + {@link TEST_HARNESS_SHIM} + the optional consumer
112
+ * `checkJsTestShim`. Only the target file's diagnostics are reported per
113
+ * program (head helpers report from their own program), and run-level
114
+ * errors are de-duplicated across programs.
115
+ *
116
+ * @param repoDir - Absolute engine (repository) directory
117
+ * @param testFiles - Patch-owned test-script paths (repo-relative)
118
+ * @param patchLint - The resolved `patchLint` config block
119
+ * @param projectRoot - FireForge project root for shim resolution
120
+ */
121
+ export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
96
122
  /**
97
123
  * Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
98
124
  * checkJs program over `patchOwnedFiles` and returns its findings grouped by
@@ -104,5 +130,19 @@ export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles:
104
130
  * @param patchOwnedFiles - Every patch-owned `.sys.mjs` in the queue
105
131
  * @param patchLint - The resolved `patchLint` config block
106
132
  * @param projectRoot - FireForge project root for shim resolution
133
+ * @param rootScope - Optional subset of files to use as program roots
134
+ * (`--patches`); resolution still spans the whole queue (FORGE J1c)
135
+ */
136
+ export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
137
+ /**
138
+ * Cheap probe reproducing the only run-level ("global") checkJs findings
139
+ * the build path can produce — a missing `typescript` package and an
140
+ * unreadable consumer shim (`checkJsExtraShim`, and `checkJsTestShim`
141
+ * when `checkJsTestFiles` is on). The built program itself never
142
+ * contributes globals (see {@link runCheckJsGrouped}), so a warm
143
+ * all-cache-hit run can satisfy the "warm never reports less than cold"
144
+ * invariant (FORGE F5) with this probe instead of building the whole
145
+ * TypeScript program. Issue objects are byte-identical to the build
146
+ * path's, deduplicated by message like {@link runCheckJsTestFilesGrouped}.
107
147
  */
108
- export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string): Promise<GroupedCheckJsResult>;
148
+ export declare function probeCheckJsGlobalIssues(patchLint: PatchLintConfig, projectRoot: string): Promise<PatchLintIssue[]>;
@@ -26,9 +26,10 @@
26
26
  * while reporting only the patch under export.
27
27
  */
28
28
  import { basename, resolve } from 'node:path';
29
+ import { toError } from '../utils/errors.js';
29
30
  import { pathExists } from '../utils/fs.js';
30
31
  import { verbose } from '../utils/logger.js';
31
- import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
32
+ import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
32
33
  // ---------------------------------------------------------------------------
33
34
  // Public API
34
35
  // ---------------------------------------------------------------------------
@@ -146,9 +147,17 @@ function createPathsResolver(ts, paths, baseDir, fileExists, onResolved) {
146
147
  * the built-in Firefox-globals shim (from `patchLint.checkJsExtraShim`)
147
148
  * @param projectRoot - Absolute project root for resolving `extraShimPath`
148
149
  * @param mode - Strictness preset plus allowlisted compiler-option overrides
150
+ * @param builtinShimSuffix - Optional shim text appended AFTER the consumer shim
151
+ * @param rootScope - When set, only these repo-relative files become program
152
+ * ROOTS; resolution (and the host allowlist) still spans all of
153
+ * `resolutionOwned`, so a subset root's cross-patch imports type-check
154
+ * against the real owning sources while unrelated owned files are never
155
+ * parsed. `.mjs` files are module-scoped, so a root's diagnostics are
156
+ * identical whether other owned files are roots or mere resolution
157
+ * targets (pinned by the rootScope parity test — FORGE J1c).
149
158
  * @returns Diagnostics grouped per owning file plus run-level errors
150
159
  */
151
- export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath, projectRoot, mode) {
160
+ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath, projectRoot, mode, builtinShimSuffix, rootScope) {
152
161
  const empty = { byFile: new Map(), global: [] };
153
162
  if (resolutionOwned.size === 0)
154
163
  return empty;
@@ -171,14 +180,18 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
171
180
  ],
172
181
  };
173
182
  }
174
- // Resolve absolute paths for root files, filtering to files that exist
183
+ // Resolve absolute paths for root files, filtering to files that exist.
184
+ // Under a rootScope, non-scoped owned files stay resolvable (host
185
+ // allowlist + relByAbsolute) but are not program roots — they are only
186
+ // parsed if a scoped root's import closure reaches them.
175
187
  const rootFiles = [];
176
188
  const ownedAbsolute = new Set();
177
189
  const relByAbsolute = new Map();
178
190
  for (const rel of resolutionOwned) {
179
191
  const abs = resolve(repoDir, rel);
180
192
  if (await pathExists(abs)) {
181
- rootFiles.push(abs);
193
+ if (rootScope === undefined || rootScope.has(rel))
194
+ rootFiles.push(abs);
182
195
  ownedAbsolute.add(abs);
183
196
  relByAbsolute.set(abs, rel);
184
197
  }
@@ -193,7 +206,11 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
193
206
  let shimSource;
194
207
  try {
195
208
  const composed = await composeShimSource(projectRoot ?? repoDir, extraShimPath);
196
- shimSource = composed.source;
209
+ // The suffix (e.g. the loose test-harness baseline, FORGE G5) goes
210
+ // AFTER the consumer's extra shim: TypeScript resolves conflicting
211
+ // `declare var` redeclarations to the FIRST declaration, so a typed
212
+ // consumer declaration must precede the loose fallback to win.
213
+ shimSource = builtinShimSuffix ? `${composed.source}\n${builtinShimSuffix}` : composed.source;
197
214
  if (composed.extraShimAppended) {
198
215
  verbose(`checkJs: extra shim ${extraShimPath ?? ''} appended to Firefox globals shim`);
199
216
  }
@@ -205,7 +222,7 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
205
222
  {
206
223
  file: extraShimPath ?? '(checkJs)',
207
224
  check: 'checkjs-type-error',
208
- message: err instanceof Error ? err.message : String(err),
225
+ message: toError(err).message,
209
226
  severity: 'error',
210
227
  },
211
228
  ],
@@ -402,6 +419,60 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
402
419
  export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint, projectRoot, reportScope) {
403
420
  return runCheckJs(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint), reportScope);
404
421
  }
422
+ /**
423
+ * Runs the checkJs pass over patch-owned test `.js` files (FORGE G5,
424
+ * `patchLint.checkJsTestFiles`). Each test file gets its OWN small
425
+ * program — mochitest scripts share top-level scope only with their
426
+ * directory's `head*.js` helpers, so one multi-script program would emit
427
+ * false cross-file redeclaration errors — with roots = the test file plus
428
+ * any same-directory patch-owned `head*.js`, checked against the built-in
429
+ * Firefox shim + {@link TEST_HARNESS_SHIM} + the optional consumer
430
+ * `checkJsTestShim`. Only the target file's diagnostics are reported per
431
+ * program (head helpers report from their own program), and run-level
432
+ * errors are de-duplicated across programs.
433
+ *
434
+ * @param repoDir - Absolute engine (repository) directory
435
+ * @param testFiles - Patch-owned test-script paths (repo-relative)
436
+ * @param patchLint - The resolved `patchLint` config block
437
+ * @param projectRoot - FireForge project root for shim resolution
438
+ */
439
+ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot, rootScope) {
440
+ const merged = { byFile: new Map(), global: [] };
441
+ if (testFiles.size === 0)
442
+ return merged;
443
+ const mode = modeFromPatchLintConfig(patchLint);
444
+ const seenGlobal = new Set();
445
+ const files = [...testFiles].sort((a, b) => a.localeCompare(b));
446
+ for (const file of files) {
447
+ // Under a rootScope only the scoped files get their own program, but
448
+ // head.js helper discovery still spans the full owned set so a scoped
449
+ // test keeps its cross-patch harness globals (FORGE J1c).
450
+ if (rootScope !== undefined && !rootScope.has(file))
451
+ continue;
452
+ const dir = file.slice(0, file.lastIndexOf('/') + 1);
453
+ const roots = new Set([file]);
454
+ for (const candidate of files) {
455
+ if (candidate === file)
456
+ continue;
457
+ const base = candidate.split('/').pop() ?? '';
458
+ if (candidate.startsWith(dir) && /^head(?:_.*)?\.js$/.test(base)) {
459
+ roots.add(candidate);
460
+ }
461
+ }
462
+ const result = await runCheckJsGrouped(repoDir, roots, patchLint.checkJsTestShim, projectRoot, mode, TEST_HARNESS_SHIM);
463
+ const own = result.byFile.get(file);
464
+ if (own && own.length > 0) {
465
+ merged.byFile.set(file, [...(merged.byFile.get(file) ?? []), ...own]);
466
+ }
467
+ for (const globalIssue of result.global) {
468
+ if (seenGlobal.has(globalIssue.message))
469
+ continue;
470
+ seenGlobal.add(globalIssue.message);
471
+ merged.global.push(globalIssue);
472
+ }
473
+ }
474
+ return merged;
475
+ }
405
476
  /** Derives the {@link CheckJsMode} from a `patchLint` config block. */
406
477
  function modeFromPatchLintConfig(patchLint) {
407
478
  const strict = patchLint.checkJsStrict === true;
@@ -426,7 +497,60 @@ function modeFromPatchLintConfig(patchLint) {
426
497
  * @param patchOwnedFiles - Every patch-owned `.sys.mjs` in the queue
427
498
  * @param patchLint - The resolved `patchLint` config block
428
499
  * @param projectRoot - FireForge project root for shim resolution
500
+ * @param rootScope - Optional subset of files to use as program roots
501
+ * (`--patches`); resolution still spans the whole queue (FORGE J1c)
502
+ */
503
+ export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, patchLint, projectRoot, rootScope) {
504
+ return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint), undefined, rootScope);
505
+ }
506
+ /**
507
+ * Cheap probe reproducing the only run-level ("global") checkJs findings
508
+ * the build path can produce — a missing `typescript` package and an
509
+ * unreadable consumer shim (`checkJsExtraShim`, and `checkJsTestShim`
510
+ * when `checkJsTestFiles` is on). The built program itself never
511
+ * contributes globals (see {@link runCheckJsGrouped}), so a warm
512
+ * all-cache-hit run can satisfy the "warm never reports less than cold"
513
+ * invariant (FORGE F5) with this probe instead of building the whole
514
+ * TypeScript program. Issue objects are byte-identical to the build
515
+ * path's, deduplicated by message like {@link runCheckJsTestFilesGrouped}.
429
516
  */
430
- export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, patchLint, projectRoot) {
431
- return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint));
517
+ export async function probeCheckJsGlobalIssues(patchLint, projectRoot) {
518
+ try {
519
+ await import('typescript');
520
+ }
521
+ catch {
522
+ return [
523
+ {
524
+ file: '(checkJs)',
525
+ check: 'checkjs-type-error',
526
+ message: 'patchLint.checkJs is enabled but the "typescript" package is not installed. ' +
527
+ 'Run "npm install typescript" to enable type checking.',
528
+ severity: 'error',
529
+ },
530
+ ];
531
+ }
532
+ const issues = [];
533
+ const seen = new Set();
534
+ const probeShim = async (shimPath) => {
535
+ try {
536
+ await composeShimSource(projectRoot, shimPath);
537
+ }
538
+ catch (err) {
539
+ const message = toError(err).message;
540
+ if (seen.has(message))
541
+ return;
542
+ seen.add(message);
543
+ issues.push({
544
+ file: shimPath ?? '(checkJs)',
545
+ check: 'checkjs-type-error',
546
+ message,
547
+ severity: 'error',
548
+ });
549
+ }
550
+ };
551
+ await probeShim(patchLint.checkJsExtraShim);
552
+ if (patchLint.checkJsTestFiles === true) {
553
+ await probeShim(patchLint.checkJsTestShim);
554
+ }
555
+ return issues;
432
556
  }
@@ -40,6 +40,15 @@ export function validateChromeScriptJsDoc(source, options) {
40
40
  ast = parseScript(source, comments);
41
41
  }
42
42
  catch {
43
+ // Deliberate carve-out, unlike the `.sys.mjs` walker in
44
+ // `patch-lint-jsdoc.ts` (which reports a parse failure as an issue).
45
+ // `parseScript` rejects `import`/`export`, so a chrome subscript that was
46
+ // misclassified as a `.js` file — or that mistakenly uses module syntax —
47
+ // would emit a pseudo-issue for every rule here. The orchestrator already
48
+ // runs the export walker on `.sys.mjs` separately, so silently declining
49
+ // to lint an unparseable script is the correct degradation for this file
50
+ // and only this file. Pinned by "returns no issues when the source uses
51
+ // module-only syntax" in patch-lint-chrome-jsdoc.test.ts.
43
52
  return [];
44
53
  }
45
54
  const issues = [];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * New-file creators map for the patch queue, split out of
3
+ * `patch-lint-cross.ts` (at the per-file line budget). Re-exported from
4
+ * there so callers keep importing from `patch-lint.ts` unchanged.
5
+ *
6
+ * Memoised per context object (FORGE J1): ownership resolution and
7
+ * cache-key fingerprints call this 2-3× per linted patch, and each call
8
+ * used to re-run `detectNewFilesInDiff` over EVERY queue entry's diff.
9
+ * Keyed weakly so a discarded context frees its map; a caller that
10
+ * mutates a context entry in place must call
11
+ * {@link invalidateNewFileCreatorsCache}.
12
+ */
13
+ /** The slice of a queue context the creators map is derived from. */
14
+ interface CreatorsContext {
15
+ entries: {
16
+ filename: string;
17
+ diff: string;
18
+ }[];
19
+ }
20
+ /** Drops the memoised creators map after an in-place context entry update. */
21
+ export declare function invalidateNewFileCreatorsCache(ctx: CreatorsContext): void;
22
+ /**
23
+ * Returns the raw `path → patches[]` map of files created in `new file
24
+ * mode` by at least one patch in the queue. Paths created by only one
25
+ * patch are also included so callers can distinguish "no creator" from
26
+ * "exactly one creator" without re-scanning the diffs. The returned map
27
+ * is memoised and shared — treat it as read-only.
28
+ */
29
+ export declare function collectNewFileCreatorsByPath(ctx: CreatorsContext): Map<string, string[]>;
30
+ export {};
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * New-file creators map for the patch queue, split out of
4
+ * `patch-lint-cross.ts` (at the per-file line budget). Re-exported from
5
+ * there so callers keep importing from `patch-lint.ts` unchanged.
6
+ *
7
+ * Memoised per context object (FORGE J1): ownership resolution and
8
+ * cache-key fingerprints call this 2-3× per linted patch, and each call
9
+ * used to re-run `detectNewFilesInDiff` over EVERY queue entry's diff.
10
+ * Keyed weakly so a discarded context frees its map; a caller that
11
+ * mutates a context entry in place must call
12
+ * {@link invalidateNewFileCreatorsCache}.
13
+ */
14
+ import { detectNewFilesInDiff } from './patch-lint-diff.js';
15
+ const newFileCreatorsMemo = new WeakMap();
16
+ /** Drops the memoised creators map after an in-place context entry update. */
17
+ export function invalidateNewFileCreatorsCache(ctx) {
18
+ newFileCreatorsMemo.delete(ctx);
19
+ }
20
+ /**
21
+ * Returns the raw `path → patches[]` map of files created in `new file
22
+ * mode` by at least one patch in the queue. Paths created by only one
23
+ * patch are also included so callers can distinguish "no creator" from
24
+ * "exactly one creator" without re-scanning the diffs. The returned map
25
+ * is memoised and shared — treat it as read-only.
26
+ */
27
+ export function collectNewFileCreatorsByPath(ctx) {
28
+ const memoised = newFileCreatorsMemo.get(ctx);
29
+ if (memoised)
30
+ return memoised;
31
+ const creators = new Map();
32
+ for (const entry of ctx.entries) {
33
+ const newFiles = detectNewFilesInDiff(entry.diff);
34
+ for (const file of newFiles) {
35
+ let owners = creators.get(file);
36
+ if (!owners) {
37
+ owners = [];
38
+ creators.set(file, owners);
39
+ }
40
+ owners.push(entry.filename);
41
+ }
42
+ }
43
+ newFileCreatorsMemo.set(ctx, creators);
44
+ return creators;
45
+ }
@@ -79,23 +79,7 @@ export interface PatchQueueContext {
79
79
  * @param patchesDir - Path to the patches directory
80
80
  */
81
81
  export declare function buildPatchQueueContext(patchesDir: string, config?: FireForgeConfig): Promise<PatchQueueContext>;
82
- /**
83
- * Returns the raw `path → patches[]` map of files created in `new file
84
- * mode` by at least one patch in the queue. Paths created by only one
85
- * patch are also included so callers can distinguish "no creator" from
86
- * "exactly one creator" without re-scanning the diffs.
87
- *
88
- * Split out from {@link lintPatchQueueDuplicateCreations} so
89
- * `status --ownership` (and any future caller that wants ownership
90
- * without a rendered PatchLintIssue) can consume the same structured
91
- * data the rule itself relies on. Previously status had to parse the
92
- * rule's human-readable message to recover the patch list, which was
93
- * both fragile and made the lint message format part of an implicit
94
- * contract.
95
- *
96
- * @param ctx - Pre-built queue context
97
- */
98
- export declare function collectNewFileCreatorsByPath(ctx: PatchQueueContext): Map<string, string[]>;
82
+ export { collectNewFileCreatorsByPath, invalidateNewFileCreatorsCache, } from './patch-lint-creators.js';
99
83
  /**
100
84
  * Cross-patch lint rule: the same path is newly created (`--- /dev/null →
101
85
  * +++ b/path`) by more than one patch. This is the failure mode that
@@ -16,7 +16,9 @@ import { readText } from '../utils/fs.js';
16
16
  import { verbose } from '../utils/logger.js';
17
17
  import { stripJsComments } from '../utils/regex.js';
18
18
  import { discoverPatches } from './patch-files.js';
19
+ import { collectNewFileCreatorsByPath } from './patch-lint-creators.js';
19
20
  import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
21
+ import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
20
22
  import { loadPatchesManifest } from './patch-manifest-io.js';
21
23
  import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
22
24
  import { extractNewFileContent } from './patch-transform.js';
@@ -81,37 +83,11 @@ export async function buildPatchQueueContext(patchesDir, config) {
81
83
  entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
82
84
  return { entries, ...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}) };
83
85
  }
84
- /**
85
- * Returns the raw `path → patches[]` map of files created in `new file
86
- * mode` by at least one patch in the queue. Paths created by only one
87
- * patch are also included so callers can distinguish "no creator" from
88
- * "exactly one creator" without re-scanning the diffs.
89
- *
90
- * Split out from {@link lintPatchQueueDuplicateCreations} so
91
- * `status --ownership` (and any future caller that wants ownership
92
- * without a rendered PatchLintIssue) can consume the same structured
93
- * data the rule itself relies on. Previously status had to parse the
94
- * rule's human-readable message to recover the patch list, which was
95
- * both fragile and made the lint message format part of an implicit
96
- * contract.
97
- *
98
- * @param ctx - Pre-built queue context
99
- */
100
- export function collectNewFileCreatorsByPath(ctx) {
101
- const creators = new Map();
102
- for (const entry of ctx.entries) {
103
- const newFiles = detectNewFilesInDiff(entry.diff);
104
- for (const file of newFiles) {
105
- let owners = creators.get(file);
106
- if (!owners) {
107
- owners = [];
108
- creators.set(file, owners);
109
- }
110
- owners.push(entry.filename);
111
- }
112
- }
113
- return creators;
114
- }
86
+ // The creators map (split out of `lintPatchQueueDuplicateCreations` so
87
+ // `status --ownership` can consume the structured data) lives in
88
+ // `patch-lint-creators.ts` with its per-context memoisation (FORGE J1);
89
+ // re-exported here so callers keep importing from `patch-lint.ts`.
90
+ export { collectNewFileCreatorsByPath, invalidateNewFileCreatorsCache, } from './patch-lint-creators.js';
115
91
  /**
116
92
  * Cross-patch lint rule: the same path is newly created (`--- /dev/null →
117
93
  * +++ b/path`) by more than one patch. This is the failure mode that
@@ -131,6 +107,7 @@ export function lintPatchQueueDuplicateCreations(ctx) {
131
107
  issues.push({
132
108
  file,
133
109
  check: 'duplicate-new-file-creation',
110
+ patches: [...owners].sort((a, b) => a.localeCompare(b)),
134
111
  fingerprint: `duplicate-new-file-creation|${file}|${[...owners].sort((a, b) => a.localeCompare(b)).join(',')}`,
135
112
  message: `File "${file}" is created (new file mode) by multiple patches: ${owners.join(', ')}. ` +
136
113
  'Only one patch may create a given path. Use "patch delete" or ' +
@@ -519,6 +496,7 @@ export function lintPatchQueueForwardImports(ctx) {
519
496
  issues.push({
520
497
  file: sitePath,
521
498
  check: 'forward-import',
499
+ patches: [entry.filename],
522
500
  fingerprint: `forward-import|${sitePath}|${cleaned}|${fingerprintOwners}`,
523
501
  message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
524
502
  `but the matching new file is created by a later patch: ${ownersSummary}. ` +
@@ -537,6 +515,7 @@ export function lintPatchQueueForwardImports(ctx) {
537
515
  issues.push({
538
516
  file: dependency.file,
539
517
  check: 'staged-dependency-unused',
518
+ patches: [entry.filename],
540
519
  fingerprint: `staged-dependency-unused|${entry.filename}|${dependency.file}|${dependency.specifier}|${dependency.creates}|${dependency.owner ?? ''}`,
541
520
  message: `${entry.filename} declares a staged forward import from ${dependency.file} ` +
542
521
  `to "${dependency.specifier}" / ${dependency.creates}, but that exact forward dependency was not found. ` +
@@ -559,6 +538,7 @@ export function lintPatchQueueForwardImports(ctx) {
559
538
  issues.push({
560
539
  file: registration.file,
561
540
  check: 'staged-dependency-unused',
541
+ patches: [entry.filename],
562
542
  fingerprint: `staged-dependency-unused|${entry.filename}|${registration.file}|reg:${registration.line}|${registration.creates}|${registration.owner ?? ''}`,
563
543
  message: `${entry.filename} declares a staged registration in ${registration.file} ` +
564
544
  `("${registration.line}") for ${registration.creates}, but ${describeRegistrationFailure(failure)} ` +
@@ -631,5 +611,9 @@ function isRegistrationLinePresent(entry, registration) {
631
611
  * manually for projected/hypothetical states)
632
612
  */
633
613
  export function lintPatchQueue(ctx) {
634
- return [...lintPatchQueueDuplicateCreations(ctx), ...lintPatchQueueForwardImports(ctx)];
614
+ return [
615
+ ...lintPatchQueueDuplicateCreations(ctx),
616
+ ...lintPatchQueueForwardImports(ctx),
617
+ ...lintPatchQueueModuleRegistrations(ctx),
618
+ ];
635
619
  }
@@ -15,7 +15,7 @@ import { pathExists, readText } from '../utils/fs.js';
15
15
  import { verbose } from '../utils/logger.js';
16
16
  import { hasRawCssColors } from '../utils/regex.js';
17
17
  import { loadFurnaceConfig } from './furnace-config.js';
18
- import { extractAddedLinesPerFile } from './patch-lint-diff.js';
18
+ import { extractAddedLineNumbersPerFile, extractAddedLinesPerFile } from './patch-lint-diff.js';
19
19
  /**
20
20
  * Loads the furnace token-prefix lint inputs gracefully — returns
21
21
  * undefined (skipping the token-prefix check) when furnace.json cannot
@@ -38,11 +38,48 @@ async function loadCssTokenContext(repoDir) {
38
38
  }
39
39
  return undefined;
40
40
  }
41
+ /**
42
+ * Masks CSS block comments with spaces, preserving newlines so line
43
+ * numbers stay stable. An unclosed trailing `/*` is masked to EOF —
44
+ * matching how a CSS parser treats it (FORGE G4).
45
+ */
46
+ function maskCssComments(source) {
47
+ const masked = source.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
48
+ const openerIndex = masked.indexOf('/*');
49
+ if (openerIndex === -1)
50
+ return masked;
51
+ return (masked.slice(0, openerIndex) +
52
+ masked
53
+ .slice(openerIndex)
54
+ .split('\n')
55
+ .map((line) => ' '.repeat(line.length))
56
+ .join('\n'));
57
+ }
58
+ const RAW_COLOR_IGNORE_MARKER = 'fireforge-ignore: raw-color-value';
59
+ /**
60
+ * Builds the scan source for the diff branch with full-file comment
61
+ * awareness: comments are masked across the whole post-patch file so a
62
+ * comment spanning the context/added boundary suppresses its hex, then
63
+ * only the patch's added line numbers are scanned. Returns undefined
64
+ * when any added line number falls outside the on-disk file (patch not
65
+ * applied / drifted) — the caller then falls back to the legacy
66
+ * joined-added-lines scan rather than misattributing lines.
67
+ */
68
+ function buildAddedLinesScanSource(rawCss, addedNumbers) {
69
+ const rawLines = rawCss.split('\n');
70
+ if (!addedNumbers.every((n) => n >= 1 && n <= rawLines.length))
71
+ return undefined;
72
+ const maskedLines = maskCssComments(rawCss).split('\n');
73
+ return addedNumbers
74
+ .filter((n) => !(rawLines[n - 1] ?? '').includes(RAW_COLOR_IGNORE_MARKER))
75
+ .map((n) => maskedLines[n - 1] ?? '')
76
+ .join('\n');
77
+ }
41
78
  /**
42
79
  * Raw-color check for one patched CSS file, scoped to introduced lines
43
80
  * when diff context is available. Pushes onto `issues`.
44
81
  */
45
- function checkRawColorValues(file, rawCss, addedLinesByFile, config, issues) {
82
+ function checkRawColorValues(file, rawCss, addedLinesByFile, addedLineNumbersByFile, config, issues) {
46
83
  // Check only introduced raw color values when diff context is available.
47
84
  // Skip files on the raw-color allowlist (exact path or basename match) and
48
85
  // auto-exempt files under `browser/branding/` — those are the fork's
@@ -55,25 +92,33 @@ function checkRawColorValues(file, rawCss, addedLinesByFile, config, issues) {
55
92
  const allowlist = config?.patchLint?.rawColorAllowlist;
56
93
  const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
57
94
  const isBranding = file.startsWith('browser/branding/');
58
- if (!isAllowlisted && !isBranding) {
59
- // Strip lines with inline fireforge-ignore: raw-color-value suppression.
60
- // Check against rawCss (before comment stripping) so the CSS comment marker is still present.
95
+ if (isAllowlisted || isBranding)
96
+ return;
97
+ let suppressedContent;
98
+ const addedNumbers = addedLineNumbersByFile?.get(file);
99
+ if (addedNumbers !== undefined) {
100
+ suppressedContent = buildAddedLinesScanSource(rawCss, addedNumbers);
101
+ }
102
+ if (suppressedContent === undefined) {
103
+ // Legacy scan: full file (no diff), or the joined added lines when
104
+ // the diff and on-disk file disagree. Waiver lines are filtered
105
+ // against the pre-strip text so the CSS comment marker is present.
61
106
  const sourceForSuppression = addedLinesByFile
62
107
  ? (addedLinesByFile.get(file) ?? []).join('\n')
63
108
  : rawCss;
64
- const suppressedContent = sourceForSuppression
109
+ suppressedContent = sourceForSuppression
65
110
  .split('\n')
66
- .filter((line) => !line.includes('fireforge-ignore: raw-color-value'))
111
+ .filter((line) => !line.includes(RAW_COLOR_IGNORE_MARKER))
67
112
  .join('\n')
68
113
  .replace(/\/\*[\s\S]*?\*\//g, '');
69
- if (hasRawCssColors(suppressedContent)) {
70
- issues.push({
71
- file,
72
- check: 'raw-color-value',
73
- message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
74
- severity: 'error',
75
- });
76
- }
114
+ }
115
+ if (hasRawCssColors(suppressedContent)) {
116
+ issues.push({
117
+ file,
118
+ check: 'raw-color-value',
119
+ message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
120
+ severity: 'error',
121
+ });
77
122
  }
78
123
  }
79
124
  /**
@@ -157,6 +202,9 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
157
202
  const tokenContext = await loadCssTokenContext(repoDir);
158
203
  const issues = [];
159
204
  const addedLinesByFile = diffContent ? extractAddedLinesPerFile(diffContent) : undefined;
205
+ const addedLineNumbersByFile = diffContent
206
+ ? extractAddedLineNumbersPerFile(diffContent)
207
+ : undefined;
160
208
  for (const file of cssFiles) {
161
209
  const filePath = join(repoDir, file);
162
210
  if (!(await pathExists(filePath)))
@@ -164,7 +212,7 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
164
212
  const rawCss = await readText(filePath);
165
213
  // Strip block comments before scanning
166
214
  const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
167
- checkRawColorValues(file, rawCss, addedLinesByFile, config, issues);
215
+ checkRawColorValues(file, rawCss, addedLinesByFile, addedLineNumbersByFile, config, issues);
168
216
  checkTokenPrefixViolations(file, cssContent, addedLinesByFile, tokenContext, issues);
169
217
  }
170
218
  return issues;
@@ -18,6 +18,14 @@ export declare function detectNewFilesInDiff(diffContent: string): Set<string>;
18
18
  * Returns a map of file path → array of added line contents (without the leading `+`).
19
19
  */
20
20
  export declare function extractAddedLinesPerFile(diffContent: string): Map<string, string[]>;
21
+ /**
22
+ * Extracts 1-based post-patch line numbers of added lines per file from a
23
+ * unified diff. Walking each hunk with a new-file line counter (`+` and
24
+ * context lines advance it, `-` lines do not) lets rules that need
25
+ * full-file context — e.g. comment masking across the context/added
26
+ * boundary (FORGE G4) — map the patch's additions onto the applied file.
27
+ */
28
+ export declare function extractAddedLineNumbersPerFile(diffContent: string): Map<string, number[]>;
21
29
  /**
22
30
  * Builds the `modifiedFileAdditions` map the cross-patch lint expects for
23
31
  * a given unified diff. Exposed so callers that construct synthetic /
@@ -45,6 +45,37 @@ export function extractAddedLinesPerFile(diffContent) {
45
45
  }
46
46
  return result;
47
47
  }
48
+ /**
49
+ * Extracts 1-based post-patch line numbers of added lines per file from a
50
+ * unified diff. Walking each hunk with a new-file line counter (`+` and
51
+ * context lines advance it, `-` lines do not) lets rules that need
52
+ * full-file context — e.g. comment masking across the context/added
53
+ * boundary (FORGE G4) — map the patch's additions onto the applied file.
54
+ */
55
+ export function extractAddedLineNumbersPerFile(diffContent) {
56
+ const result = new Map();
57
+ for (const section of parseDiffSections(diffContent)) {
58
+ for (const hunk of section.hunks) {
59
+ let newLine = hunk.newStart;
60
+ for (const line of hunk.lines) {
61
+ if (line.startsWith('\\'))
62
+ continue; // ""
63
+ if (line.startsWith('-'))
64
+ continue;
65
+ if (line.startsWith('+')) {
66
+ let arr = result.get(section.targetPath);
67
+ if (!arr) {
68
+ arr = [];
69
+ result.set(section.targetPath, arr);
70
+ }
71
+ arr.push(newLine);
72
+ }
73
+ newLine++;
74
+ }
75
+ }
76
+ }
77
+ return result;
78
+ }
48
79
  /**
49
80
  * Builds the `modifiedFileAdditions` map the cross-patch lint expects for
50
81
  * a given unified diff. Exposed so callers that construct synthetic /