@hominis/fireforge 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/build-info.json +4 -4
  3. package/dist/src/commands/check-report.d.ts +17 -4
  4. package/dist/src/commands/check-report.js +19 -6
  5. package/dist/src/commands/check-run.d.ts +5 -0
  6. package/dist/src/commands/check-run.js +9 -1
  7. package/dist/src/commands/check-steps.d.ts +8 -0
  8. package/dist/src/commands/check-steps.js +31 -6
  9. package/dist/src/commands/check.js +1 -1
  10. package/dist/src/commands/declarations-report.d.ts +43 -0
  11. package/dist/src/commands/declarations-report.js +76 -0
  12. package/dist/src/commands/export-placement-gate.js +8 -0
  13. package/dist/src/commands/furnace/rename-registrations.js +7 -2
  14. package/dist/src/commands/furnace/validate.js +26 -1
  15. package/dist/src/commands/lint-per-patch.js +6 -2
  16. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  17. package/dist/src/commands/lint-report.js +7 -2
  18. package/dist/src/commands/manifest.js +2 -0
  19. package/dist/src/commands/park.d.ts +22 -0
  20. package/dist/src/commands/park.js +232 -0
  21. package/dist/src/commands/patch/delete.js +11 -6
  22. package/dist/src/commands/patch/index.d.ts +3 -3
  23. package/dist/src/commands/patch/index.js +8 -4
  24. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  25. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  26. package/dist/src/commands/patch/lint-ignore.js +39 -29
  27. package/dist/src/commands/patch/move-files-create.js +11 -4
  28. package/dist/src/commands/patch/move-files-into.js +10 -4
  29. package/dist/src/commands/patch/move-files.js +3 -1
  30. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  31. package/dist/src/commands/patch/removal-created-files.js +83 -0
  32. package/dist/src/commands/patch/rename.js +15 -3
  33. package/dist/src/commands/patch/reserve.d.ts +33 -0
  34. package/dist/src/commands/patch/reserve.js +152 -0
  35. package/dist/src/commands/patch/retire.js +11 -6
  36. package/dist/src/commands/patch/split-plan.d.ts +10 -1
  37. package/dist/src/commands/patch/split-plan.js +16 -1
  38. package/dist/src/commands/patch/split.js +14 -5
  39. package/dist/src/commands/patch/stage.d.ts +34 -0
  40. package/dist/src/commands/patch/stage.js +153 -0
  41. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  42. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  43. package/dist/src/commands/patch/staged-dependency.js +40 -98
  44. package/dist/src/commands/re-export-adjacent.d.ts +5 -0
  45. package/dist/src/commands/re-export-adjacent.js +4 -3
  46. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  47. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  48. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  49. package/dist/src/commands/re-export-hunks.js +220 -0
  50. package/dist/src/commands/re-export-options.js +29 -2
  51. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  52. package/dist/src/commands/re-export-purity-scope.js +131 -0
  53. package/dist/src/commands/re-export-register.js +15 -1
  54. package/dist/src/commands/re-export-removed.d.ts +1 -1
  55. package/dist/src/commands/re-export-removed.js +17 -2
  56. package/dist/src/commands/re-export-scan.d.ts +2 -0
  57. package/dist/src/commands/re-export-scan.js +4 -1
  58. package/dist/src/commands/re-export-single.js +102 -42
  59. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  60. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  61. package/dist/src/commands/re-export.js +35 -34
  62. package/dist/src/commands/status-output.js +5 -1
  63. package/dist/src/commands/test-build-furnace.d.ts +3 -2
  64. package/dist/src/commands/test-build-furnace.js +8 -2
  65. package/dist/src/commands/test-build-phase.d.ts +2 -0
  66. package/dist/src/commands/test-build-phase.js +34 -17
  67. package/dist/src/commands/test-checkjs.js +3 -2
  68. package/dist/src/commands/test-dispatch.d.ts +39 -0
  69. package/dist/src/commands/test-dispatch.js +220 -0
  70. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  71. package/dist/src/commands/test-furnace-reach.js +132 -0
  72. package/dist/src/commands/test-orchestrate.d.ts +14 -0
  73. package/dist/src/commands/test-orchestrate.js +25 -4
  74. package/dist/src/commands/test-perf-scope.d.ts +21 -0
  75. package/dist/src/commands/test-perf-scope.js +55 -0
  76. package/dist/src/commands/test-prelock.js +100 -4
  77. package/dist/src/commands/test-register.js +18 -3
  78. package/dist/src/commands/test-results.d.ts +62 -0
  79. package/dist/src/commands/test-results.js +62 -0
  80. package/dist/src/commands/test-run.d.ts +13 -55
  81. package/dist/src/commands/test-run.js +73 -101
  82. package/dist/src/commands/test-shards.d.ts +110 -0
  83. package/dist/src/commands/test-shards.js +255 -0
  84. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  85. package/dist/src/commands/test-stale-gate.js +11 -2
  86. package/dist/src/commands/test-verdict.d.ts +2 -1
  87. package/dist/src/commands/test.d.ts +1 -1
  88. package/dist/src/commands/test.js +68 -75
  89. package/dist/src/commands/token-lint.js +48 -8
  90. package/dist/src/commands/verify.js +9 -2
  91. package/dist/src/core/build-baseline-types.d.ts +13 -0
  92. package/dist/src/core/build-baseline-validate.js +9 -0
  93. package/dist/src/core/build-baseline.js +7 -0
  94. package/dist/src/core/build-prepare.d.ts +6 -0
  95. package/dist/src/core/build-prepare.js +29 -3
  96. package/dist/src/core/config-paths.d.ts +1 -1
  97. package/dist/src/core/config-paths.js +2 -0
  98. package/dist/src/core/config-validate-check.js +83 -7
  99. package/dist/src/core/config-validate-test-toolchains.js +14 -0
  100. package/dist/src/core/config-validate-token-scales.d.ts +3 -0
  101. package/dist/src/core/config-validate-token-scales.js +47 -5
  102. package/dist/src/core/config-validate.js +6 -0
  103. package/dist/src/core/coverage-extend.js +2 -2
  104. package/dist/src/core/coverage-records.d.ts +11 -0
  105. package/dist/src/core/coverage-records.js +34 -0
  106. package/dist/src/core/engine-session-lock.d.ts +6 -0
  107. package/dist/src/core/engine-session-lock.js +10 -2
  108. package/dist/src/core/file-lock-shared.d.ts +57 -0
  109. package/dist/src/core/file-lock-shared.js +117 -0
  110. package/dist/src/core/file-lock.d.ts +9 -0
  111. package/dist/src/core/file-lock.js +58 -2
  112. package/dist/src/core/furnace-apply.js +10 -10
  113. package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
  114. package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
  115. package/dist/src/core/furnace-config-validate.js +7 -1
  116. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  117. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  118. package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
  119. package/dist/src/core/furnace-validate-accessibility.js +75 -30
  120. package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
  121. package/dist/src/core/furnace-validate-exceptions.js +59 -4
  122. package/dist/src/core/furnace-validate-icon-button.js +30 -1
  123. package/dist/src/core/history-log-file.js +8 -1
  124. package/dist/src/core/line-prefixer.d.ts +18 -0
  125. package/dist/src/core/line-prefixer.js +34 -0
  126. package/dist/src/core/mach.d.ts +7 -0
  127. package/dist/src/core/mach.js +24 -9
  128. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  129. package/dist/src/core/module-resolution-preflight.js +24 -6
  130. package/dist/src/core/negative-control-compiled.d.ts +36 -0
  131. package/dist/src/core/negative-control-compiled.js +87 -0
  132. package/dist/src/core/negative-control-diff.d.ts +14 -2
  133. package/dist/src/core/negative-control-diff.js +49 -6
  134. package/dist/src/core/negative-control.d.ts +12 -0
  135. package/dist/src/core/negative-control.js +48 -5
  136. package/dist/src/core/park-store.d.ts +63 -0
  137. package/dist/src/core/park-store.js +216 -0
  138. package/dist/src/core/patch-export-update.d.ts +4 -0
  139. package/dist/src/core/patch-export-update.js +12 -3
  140. package/dist/src/core/patch-export.d.ts +3 -2
  141. package/dist/src/core/patch-export.js +21 -14
  142. package/dist/src/core/patch-files.js +26 -8
  143. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
  144. package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
  145. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  146. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  147. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  148. package/dist/src/core/patch-lint-checkjs.js +13 -3
  149. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  150. package/dist/src/core/patch-lint-cross.js +5 -0
  151. package/dist/src/core/patch-lint-css.js +14 -10
  152. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
  153. package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
  154. package/dist/src/core/patch-lint-ignore.d.ts +56 -12
  155. package/dist/src/core/patch-lint-ignore.js +173 -43
  156. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  157. package/dist/src/core/patch-lint-ownership.js +26 -0
  158. package/dist/src/core/patch-lint-size.js +9 -1
  159. package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
  160. package/dist/src/core/patch-lint-token-scale.js +206 -29
  161. package/dist/src/core/patch-lint.js +2 -2
  162. package/dist/src/core/patch-manifest-io.js +8 -1
  163. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  164. package/dist/src/core/patch-manifest-key-order.js +61 -0
  165. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  166. package/dist/src/core/patch-manifest-stage.js +96 -0
  167. package/dist/src/core/patch-manifest-validate.js +27 -1
  168. package/dist/src/core/patch-order-legal.d.ts +2 -1
  169. package/dist/src/core/patch-order-legal.js +7 -2
  170. package/dist/src/core/patch-policy.d.ts +6 -2
  171. package/dist/src/core/patch-policy.js +14 -7
  172. package/dist/src/core/patch-retired.d.ts +26 -4
  173. package/dist/src/core/patch-retired.js +40 -2
  174. package/dist/src/core/run-log.d.ts +6 -0
  175. package/dist/src/core/run-log.js +17 -1
  176. package/dist/src/core/test-harness-crash.d.ts +2 -1
  177. package/dist/src/core/test-harness-crash.js +5 -8
  178. package/dist/src/core/test-harness-signature.d.ts +16 -0
  179. package/dist/src/core/test-harness-signature.js +19 -1
  180. package/dist/src/core/test-raw-log.d.ts +66 -0
  181. package/dist/src/core/test-raw-log.js +122 -0
  182. package/dist/src/core/test-run-completeness.d.ts +47 -7
  183. package/dist/src/core/test-run-completeness.js +70 -23
  184. package/dist/src/core/test-stale-check.d.ts +10 -1
  185. package/dist/src/core/test-stale-check.js +15 -1
  186. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  187. package/dist/src/core/test-xpcshell-retry.js +2 -1
  188. package/dist/src/core/token-lint.d.ts +5 -2
  189. package/dist/src/core/token-lint.js +8 -3
  190. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  191. package/dist/src/core/token-sibling-sheets.js +119 -0
  192. package/dist/src/core/tree-guard.js +2 -0
  193. package/dist/src/core/typecheck-shim.d.ts +6 -1
  194. package/dist/src/core/typecheck-shim.js +139 -2
  195. package/dist/src/core/typecheck.js +3 -1
  196. package/dist/src/index.d.ts +9 -3
  197. package/dist/src/index.js +4 -1
  198. package/dist/src/types/commands/index.d.ts +2 -2
  199. package/dist/src/types/commands/options.d.ts +68 -7
  200. package/dist/src/types/commands/patches.d.ts +43 -0
  201. package/dist/src/types/config.d.ts +74 -2
  202. package/dist/src/types/furnace.d.ts +29 -0
  203. package/dist/src/types/index.d.ts +1 -1
  204. package/dist/src/utils/options.d.ts +3 -2
  205. package/dist/src/utils/options.js +9 -4
  206. package/docs/configuration.md +110 -9
  207. package/docs/furnace.md +61 -1
  208. package/docs/lifecycle-invariants.md +13 -11
  209. package/docs/machine-output.md +68 -18
  210. package/docs/patch-workflow.md +174 -13
  211. package/docs/testing.md +89 -5
  212. package/package.json +3 -3
@@ -24,6 +24,16 @@ export const LINT_IGNORE_CEILING_RULES = [
24
24
  'large-patch-lines',
25
25
  'large-patch-files',
26
26
  ];
27
+ /** The only rule whose findings carry a {@link PatchSizeShare}. */
28
+ const SHARE_RULE = 'large-patch-lines';
29
+ const SHARES = ['product', 'test'];
30
+ /**
31
+ * The key a ceiling is stored under: the rule, plus `#<share>` for a
32
+ * share-scoped ceiling, so one rule can carry a ceiling per share.
33
+ */
34
+ function ceilingKey(rule, share) {
35
+ return share === undefined ? rule : `${rule}#${share}`;
36
+ }
27
37
  /** Narrows an entry to the ceiling form. */
28
38
  export function isLintIgnoreCeiling(entry) {
29
39
  return typeof entry !== 'string';
@@ -32,9 +42,19 @@ export function isLintIgnoreCeiling(entry) {
32
42
  export function lintIgnoreRule(entry) {
33
43
  return isLintIgnoreCeiling(entry) ? entry.rule : entry;
34
44
  }
35
- /** One-word rendering for summaries and dry runs: `file-too-large<=1200`. */
45
+ /**
46
+ * One-word rendering for summaries and dry runs: `file-too-large<=1200`,
47
+ * `large-patch-lines[test]<=900`.
48
+ */
36
49
  export function formatLintIgnoreEntry(entry) {
37
- return isLintIgnoreCeiling(entry) ? `${entry.rule}<=${String(entry.max)}` : entry;
50
+ if (!isLintIgnoreCeiling(entry))
51
+ return entry;
52
+ const share = entry.share === undefined ? '' : `[${entry.share}]`;
53
+ return `${entry.rule}${share}<=${String(entry.max)}`;
54
+ }
55
+ /** The identity an entry is upserted and deduplicated by. */
56
+ function entryKey(entry) {
57
+ return isLintIgnoreCeiling(entry) ? ceilingKey(entry.rule, entry.share) : entry;
38
58
  }
39
59
  /** Renders a list for messages, `(empty)` when there is nothing to show. */
40
60
  export function formatLintIgnoreList(entries) {
@@ -43,7 +63,11 @@ export function formatLintIgnoreList(entries) {
43
63
  function sameEntry(a, b) {
44
64
  if (!isLintIgnoreCeiling(a) || !isLintIgnoreCeiling(b))
45
65
  return a === b;
46
- return a.rule === b.rule && a.max === b.max && a.reason === b.reason;
66
+ return (a.rule === b.rule &&
67
+ a.max === b.max &&
68
+ a.reason === b.reason &&
69
+ a.share === b.share &&
70
+ JSON.stringify(a.meta) === JSON.stringify(b.meta));
47
71
  }
48
72
  /**
49
73
  * Validates a raw `lintIgnore` value from `patches.json`.
@@ -53,6 +77,9 @@ function sameEntry(a, b) {
53
77
  * integer `max` and a non-empty `reason`, and must be the only entry for
54
78
  * its rule: a bare ID beside a ceiling for the same rule would make the
55
79
  * ceiling dead, and two ceilings would leave the effective one ambiguous.
80
+ * The one exception is `large-patch-lines`, which may carry one ceiling
81
+ * per `share` instead of a single share-less one. `meta` must be a JSON
82
+ * object and is kept verbatim.
56
83
  *
57
84
  * @param raw - The value under `lintIgnore`
58
85
  * @param label - Field label for error messages, e.g. `patches[3].lintIgnore`
@@ -73,13 +100,21 @@ export function parseLintIgnoreEntries(raw, label) {
73
100
  }
74
101
  entries.push(parseCeiling(item, itemLabel));
75
102
  }
103
+ const sharedRules = new Set();
76
104
  for (const entry of entries) {
77
105
  if (!isLintIgnoreCeiling(entry))
78
106
  continue;
79
- if (ceilingRules.has(entry.rule) || entries.includes(entry.rule)) {
80
- throw new Error(`${label} names "${entry.rule}" more than once; keep a single entry (bare or ceiling) per rule`);
107
+ const key = ceilingKey(entry.rule, entry.share);
108
+ const clash = ceilingRules.has(key) ||
109
+ entries.includes(entry.rule) ||
110
+ (entry.share === undefined ? sharedRules.has(entry.rule) : ceilingRules.has(entry.rule));
111
+ if (clash) {
112
+ throw new Error(`${label} names "${entry.rule}" more than once; keep a single entry (bare or ceiling) ` +
113
+ 'per rule, or one ceiling per share');
81
114
  }
82
- ceilingRules.add(entry.rule);
115
+ ceilingRules.add(key);
116
+ if (entry.share !== undefined)
117
+ sharedRules.add(entry.rule);
83
118
  }
84
119
  return entries;
85
120
  }
@@ -88,11 +123,11 @@ function parseCeiling(item, label) {
88
123
  throw new Error(`${label} must be a check ID string or a { rule, max, reason } object`);
89
124
  }
90
125
  const record = item;
91
- const unknownKeys = Object.keys(record).filter((key) => key !== 'rule' && key !== 'max' && key !== 'reason');
126
+ const unknownKeys = Object.keys(record).filter((key) => !CEILING_KEYS.has(key));
92
127
  if (unknownKeys.length > 0) {
93
128
  throw new Error(`${label} has unknown field(s): ${unknownKeys.join(', ')}`);
94
129
  }
95
- const { rule, max, reason } = record;
130
+ const { rule, max, reason, share, meta } = record;
96
131
  if (typeof rule !== 'string' || !LINT_IGNORE_CEILING_RULES.includes(rule)) {
97
132
  throw new Error(`${label}.rule must be one of the measured size rules: ${LINT_IGNORE_CEILING_RULES.join(', ')}`);
98
133
  }
@@ -102,8 +137,22 @@ function parseCeiling(item, label) {
102
137
  if (typeof reason !== 'string' || reason.trim() === '') {
103
138
  throw new Error(`${label}.reason must be a non-empty string`);
104
139
  }
105
- return { rule, max, reason };
140
+ const ceiling = { rule, max, reason };
141
+ if (share !== undefined) {
142
+ if (rule !== SHARE_RULE || !SHARES.includes(share)) {
143
+ throw new Error(`${label}.share must be "product" or "test", and only on ${SHARE_RULE}`);
144
+ }
145
+ ceiling.share = share;
146
+ }
147
+ if (meta !== undefined) {
148
+ if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
149
+ throw new Error(`${label}.meta must be a JSON object`);
150
+ }
151
+ ceiling.meta = meta;
152
+ }
153
+ return ceiling;
106
154
  }
155
+ const CEILING_KEYS = new Set(['rule', 'max', 'reason', 'share', 'meta']);
107
156
  /**
108
157
  * Builds the waiver filter for a patch's entries.
109
158
  *
@@ -117,7 +166,7 @@ export function buildLintIgnoreChecks(entries) {
117
166
  const ceilings = new Map();
118
167
  for (const entry of entries) {
119
168
  if (isLintIgnoreCeiling(entry))
120
- ceilings.set(entry.rule, entry);
169
+ ceilings.set(ceilingKey(entry.rule, entry.share), entry);
121
170
  else
122
171
  plain.add(entry);
123
172
  }
@@ -138,38 +187,52 @@ export function lintIgnoreCacheTokens(checks) {
138
187
  * semantics `export` / `re-export --lint-ignore` have always had. A bare ID
139
188
  * whose rule already has an entry (bare or ceiling) is a no-op: widening a
140
189
  * reviewed ceiling to an unbounded waiver is `patch lint-ignore`'s job, where
141
- * it is confirmed explicitly.
190
+ * it is confirmed explicitly. Ceilings deduplicate by rule and share, so a
191
+ * rule's per-share ceilings all survive.
142
192
  *
143
193
  * @returns The merged list, or undefined when it is empty
144
194
  */
145
195
  export function mergeLintIgnoreIds(existing, ids) {
146
196
  const merged = [];
197
+ const keys = new Set();
147
198
  const rules = new Set();
148
199
  for (const entry of [...(existing ?? []), ...(ids ?? [])]) {
149
- const rule = lintIgnoreRule(entry);
150
- if (rules.has(rule))
200
+ const blocked = isLintIgnoreCeiling(entry) ? keys.has(entryKey(entry)) : rules.has(entry);
201
+ if (blocked)
151
202
  continue;
152
- rules.add(rule);
203
+ keys.add(entryKey(entry));
204
+ rules.add(lintIgnoreRule(entry));
153
205
  merged.push(entry);
154
206
  }
155
207
  return merged.length > 0 ? merged : undefined;
156
208
  }
157
209
  /**
158
- * Adds or replaces entries by rule, the `patch lint-ignore --add` semantics:
159
- * an explicit `--add <rule>` turns a ceiling into a bare waiver, and
160
- * `--add <rule> --max <n>` sets or moves the ceiling.
210
+ * Adds or replaces entries, the `patch lint-ignore --add` semantics: an
211
+ * explicit `--add <rule>` turns a rule's ceilings into a bare waiver,
212
+ * `--add <rule> --max <n>` sets or moves the rule's ceiling, and a
213
+ * share-scoped ceiling replaces only its own share (plus any bare or
214
+ * share-less entry for the rule, which it would contradict).
161
215
  *
162
216
  * @returns The new list, or undefined when it is empty
163
217
  */
164
218
  export function upsertLintIgnoreEntries(existing, additions) {
165
- const next = [...existing];
219
+ let next = [...existing];
166
220
  for (const addition of additions) {
167
221
  const rule = lintIgnoreRule(addition);
168
- const index = next.findIndex((entry) => lintIgnoreRule(entry) === rule);
169
- if (index === -1)
170
- next.push(addition);
171
- else
172
- next[index] = addition;
222
+ const scoped = isLintIgnoreCeiling(addition) && addition.share !== undefined;
223
+ const replaced = (entry) => lintIgnoreRule(entry) === rule &&
224
+ (!scoped ||
225
+ !isLintIgnoreCeiling(entry) ||
226
+ entry.share === undefined ||
227
+ entryKey(entry) === entryKey(addition));
228
+ const index = next.findIndex(replaced);
229
+ const kept = next.filter((entry) => !replaced(entry));
230
+ // The addition takes the first replaced entry's place, so a moved
231
+ // ceiling keeps its position in the list.
232
+ next =
233
+ index === -1
234
+ ? [...kept, addition]
235
+ : [...kept.slice(0, index), addition, ...kept.slice(index)];
173
236
  }
174
237
  return mergeLintIgnoreIds(next, undefined);
175
238
  }
@@ -200,7 +263,7 @@ export function partitionLintIgnoredIssues(issues, checks) {
200
263
  suppressed.push(issue);
201
264
  continue;
202
265
  }
203
- const ceiling = checks?.ceilings?.get(issue.check);
266
+ const ceiling = ceilingFor(issue, checks);
204
267
  if (ceiling === undefined) {
205
268
  kept.push(issue);
206
269
  continue;
@@ -209,12 +272,31 @@ export function partitionLintIgnoredIssues(issues, checks) {
209
272
  suppressed.push(issue);
210
273
  continue;
211
274
  }
275
+ // A notice never gates, so a ceiling does not bound it: one past `max`
276
+ // surfaces as the plain notice it is, not as an exceeded waiver.
277
+ if (issue.severity === 'notice') {
278
+ kept.push(issue);
279
+ continue;
280
+ }
212
281
  kept.push({ ...issue, message: `${issue.message} ${describeExceededCeiling(ceiling)}` });
213
282
  }
214
283
  return { kept, suppressed };
215
284
  }
285
+ /** The ceiling that governs `issue`: its share's, else the rule's share-less one. */
286
+ function ceilingFor(issue, checks) {
287
+ const ceilings = checks?.ceilings;
288
+ if (ceilings === undefined)
289
+ return undefined;
290
+ if (issue.share !== undefined) {
291
+ const scoped = ceilings.get(ceilingKey(issue.check, issue.share));
292
+ if (scoped !== undefined)
293
+ return scoped;
294
+ }
295
+ return ceilings.get(issue.check);
296
+ }
216
297
  function describeExceededCeiling(ceiling) {
217
- return (`This exceeds the patch's lintIgnore ceiling for ${ceiling.rule} ` +
298
+ const share = ceiling.share === undefined ? '' : ` (${ceiling.share} share)`;
299
+ return (`This exceeds the patch's lintIgnore ceiling for ${ceiling.rule}${share} ` +
218
300
  `(max ${String(ceiling.max)}: ${ceiling.reason}).`);
219
301
  }
220
302
  /** True when `checks` waives `issue` (see {@link partitionLintIgnoredIssues}). */
@@ -242,6 +324,31 @@ export function filterLintIgnoredByOwners(issues, owners) {
242
324
  return issues;
243
325
  return issues.filter((issue) => !(byFile.get(issue.file) ?? []).some((checks) => isLintIgnored(issue, checks)));
244
326
  }
327
+ function judgeWaiver(rule, max, share, issues, patchMeasurements) {
328
+ const findings = issues.filter((issue) => issue.check === rule && (share === undefined || issue.share === share));
329
+ const measured = findings
330
+ .map((issue) => issue.measurement)
331
+ .filter((value) => value !== undefined);
332
+ // A share-scoped ceiling has no patch-level fallback: the total counts
333
+ // both shares, which is not what it bounds.
334
+ const fallback = share !== undefined
335
+ ? null
336
+ : rule === 'large-patch-lines'
337
+ ? patchMeasurements.lines
338
+ : rule === 'large-patch-files'
339
+ ? patchMeasurements.files
340
+ : null;
341
+ const measurement = measured.length > 0 ? Math.max(...measured) : fallback;
342
+ const gating = findings.filter((issue) => issue.severity !== 'notice');
343
+ const exceeded = max !== null &&
344
+ gating.some((issue) => issue.measurement !== undefined && issue.measurement > max);
345
+ const state = exceeded
346
+ ? 'exceeded'
347
+ : gating.length > 0
348
+ ? 'within'
349
+ : 'removable';
350
+ return { measurement, state };
351
+ }
245
352
  /**
246
353
  * Evaluates each ceiling on a patch against the findings its lint produced
247
354
  * (both surfaced and suppressed).
@@ -257,26 +364,48 @@ export function evaluateLintIgnoreCeilings(entries, issues, patchMeasurements) {
257
364
  for (const entry of entries ?? []) {
258
365
  if (!isLintIgnoreCeiling(entry))
259
366
  continue;
260
- const findings = issues.filter((issue) => issue.check === entry.rule);
261
- const measured = findings
262
- .map((issue) => issue.measurement)
263
- .filter((value) => value !== undefined);
264
- const fallback = entry.rule === 'large-patch-lines'
265
- ? patchMeasurements.lines
266
- : entry.rule === 'large-patch-files'
267
- ? patchMeasurements.files
268
- : null;
269
- const measurement = measured.length > 0 ? Math.max(...measured) : fallback;
270
- const gating = findings.some((issue) => issue.severity !== 'notice');
271
- const state = measured.some((value) => value > entry.max)
272
- ? 'exceeded'
273
- : gating
274
- ? 'within'
275
- : 'removable';
276
- statuses.push({ rule: entry.rule, max: entry.max, reason: entry.reason, measurement, state });
367
+ statuses.push({
368
+ rule: entry.rule,
369
+ max: entry.max,
370
+ reason: entry.reason,
371
+ ...(entry.share !== undefined ? { share: entry.share } : {}),
372
+ ...(entry.meta !== undefined ? { meta: entry.meta } : {}),
373
+ ...judgeWaiver(entry.rule, entry.max, entry.share, issues, patchMeasurements),
374
+ });
277
375
  }
278
376
  return statuses;
279
377
  }
378
+ /**
379
+ * Evaluates every waiver on a patch, bare IDs included, so a waiver audit
380
+ * reads each one's current measurement from the report instead of
381
+ * re-measuring bare waivers from `suppressedIssues`.
382
+ *
383
+ * @param entries - The patch's `lintIgnore`
384
+ * @param issues - Every finding of the patch's lint, surfaced and suppressed
385
+ * @param patchMeasurements - See {@link evaluateLintIgnoreCeilings}
386
+ */
387
+ export function evaluateLintIgnoreWaivers(entries, issues, patchMeasurements) {
388
+ return (entries ?? []).map((entry) => {
389
+ if (!isLintIgnoreCeiling(entry)) {
390
+ return {
391
+ kind: 'bare',
392
+ rule: entry,
393
+ max: null,
394
+ reason: null,
395
+ ...judgeWaiver(entry, null, undefined, issues, patchMeasurements),
396
+ };
397
+ }
398
+ return {
399
+ kind: 'ceiling',
400
+ rule: entry.rule,
401
+ max: entry.max,
402
+ reason: entry.reason,
403
+ ...(entry.share !== undefined ? { share: entry.share } : {}),
404
+ ...(entry.meta !== undefined ? { meta: entry.meta } : {}),
405
+ ...judgeWaiver(entry.rule, entry.max, entry.share, issues, patchMeasurements),
406
+ };
407
+ });
408
+ }
280
409
  /**
281
410
  * Notice-severity findings for ceilings that are no longer needed, so the
282
411
  * waiver's removal is prompted by the tool that enforces it rather than
@@ -287,10 +416,11 @@ export function removableCeilingIssues(patchFilename, statuses) {
287
416
  .filter((status) => status.state === 'removable')
288
417
  .map((status) => {
289
418
  const now = status.measurement === null ? 'below its notice band' : `at ${String(status.measurement)}`;
419
+ const share = status.share === undefined ? '' : `, ${status.share} share`;
290
420
  return {
291
421
  file: patchFilename,
292
422
  check: 'lint-ignore-removable',
293
- message: `lintIgnore ceiling for ${status.rule} (max ${String(status.max)}) is no longer needed: ` +
423
+ message: `lintIgnore ceiling for ${status.rule} (max ${String(status.max)}${share}) is no longer needed: ` +
294
424
  `the measurement is ${now}, under the rule's own warning threshold. ` +
295
425
  `Remove it with "fireforge patch lint-ignore ${patchFilename} --remove ${status.rule}".`,
296
426
  severity: 'notice',
@@ -51,6 +51,16 @@ export declare function resolvePatchOwnedChromeScripts(currentNewFiles: Set<stri
51
51
  * `patch-lint-ownership.test.ts`.
52
52
  */
53
53
  export declare function isTestScriptFile(file: string): boolean;
54
+ /**
55
+ * For each test script, the ES modules (`.mjs`, `.sys.mjs`) the queue
56
+ * creates up to and including the test's own patch: what its imports can
57
+ * resolve to in a per-test checkJs program. A test no patch touches sees
58
+ * every module the queue creates.
59
+ *
60
+ * @param patchQueueCtx - The queue, in apply order
61
+ * @returns Lookup from a test script path to its resolvable modules
62
+ */
63
+ export declare function resolvableModulesForTests(patchQueueCtx: PatchQueueContext): (testFile: string) => ReadonlySet<string>;
54
64
  /**
55
65
  * Returns the set of patch-owned test `.js` files. Same
56
66
  * ownership semantics as {@link resolvePatchOwnedSysMjs}. Consumed by the
@@ -84,6 +84,32 @@ export function isTestScriptFile(file) {
84
84
  const basename = file.split('/').pop() ?? '';
85
85
  return /^(?:browser_|test_|xpcshell_).*\.js$/.test(basename);
86
86
  }
87
+ /**
88
+ * For each test script, the ES modules (`.mjs`, `.sys.mjs`) the queue
89
+ * creates up to and including the test's own patch: what its imports can
90
+ * resolve to in a per-test checkJs program. A test no patch touches sees
91
+ * every module the queue creates.
92
+ *
93
+ * @param patchQueueCtx - The queue, in apply order
94
+ * @returns Lookup from a test script path to its resolvable modules
95
+ */
96
+ export function resolvableModulesForTests(patchQueueCtx) {
97
+ const byTest = new Map();
98
+ const upTo = new Set();
99
+ for (const entry of patchQueueCtx.entries) {
100
+ for (const file of entry.newFiles.keys()) {
101
+ if (file.endsWith('.mjs') && !isTestScriptFile(file))
102
+ upTo.add(file);
103
+ }
104
+ const snapshot = new Set(upTo);
105
+ for (const file of [...entry.newFiles.keys(), ...entry.modifiedFileAdditions.keys()]) {
106
+ if (isTestScriptFile(file) && !byTest.has(file))
107
+ byTest.set(file, snapshot);
108
+ }
109
+ }
110
+ const all = upTo;
111
+ return (testFile) => byTest.get(testFile) ?? all;
112
+ }
87
113
  /**
88
114
  * Returns the set of patch-owned test `.js` files. Same
89
115
  * ownership semantics as {@link resolvePatchOwnedSysMjs}. Consumed by the
@@ -248,19 +248,26 @@ function formatPatchSizeMessage(subject, band, thresholds) {
248
248
  */
249
249
  function lineChecks(measurement, tier, thresholds, config) {
250
250
  const perClass = measurement.metric === 'addedCode' && tier === 'general';
251
- if (!perClass || measurement.testLines === 0) {
251
+ if (!perClass) {
252
252
  return [{ count: measurement.lines, thresholds: thresholds.lines, scope: '' }];
253
253
  }
254
+ if (measurement.testLines === 0) {
255
+ return [
256
+ { count: measurement.lines, thresholds: thresholds.lines, scope: '', share: 'product' },
257
+ ];
258
+ }
254
259
  return [
255
260
  {
256
261
  count: measurement.lines - measurement.testLines,
257
262
  thresholds: thresholds.lines,
258
263
  scope: ' in non-test files',
264
+ share: 'product',
259
265
  },
260
266
  {
261
267
  count: measurement.testLines,
262
268
  thresholds: getPatchSizeThresholds('test', config).lines,
263
269
  scope: ' in test files (test tier)',
270
+ share: 'test',
264
271
  },
265
272
  ];
266
273
  }
@@ -317,6 +324,7 @@ export function lintPatchSize(filesAffected, measured, patchTier, config) {
317
324
  message: formatPatchSizeMessage(subject, band, check.thresholds),
318
325
  severity: band,
319
326
  measurement: check.count,
327
+ ...(check.share !== undefined ? { share: check.share } : {}),
320
328
  });
321
329
  }
322
330
  return issues;
@@ -6,20 +6,29 @@
6
6
  * or referencing foreign custom properties, but say nothing about
7
7
  * `gap: 7px`, `font-size: 13px` or `z-index: 9999`. Those are the values a
8
8
  * design scale exists to remove, and without a rule they drift back one
9
- * literal at a time. A scale names its properties, the token prefix that
10
- * defines its steps, and the literals that are not steps at all (`0`,
11
- * `auto`, a percentage).
9
+ * literal at a time. A scale names its properties, the token prefix (or
10
+ * prefixes) that defines its steps, and the literals that are not steps at
11
+ * all (`0`, `auto`, a percentage).
12
12
  *
13
13
  * Judged per value atom, after peeling `var()` innermost-first so a fallback
14
14
  * is held to the same scale as the token it backs: `var(--x-gap, 7px)` is a
15
15
  * literal in disguise. `calc()`, `min()`, `max()` and `clamp()` are
16
16
  * transparent, so a value built from tokens passes and one that adds a raw
17
- * length does not. Unitless multipliers inside them are literals too and
18
- * need an `allowLiterals` entry.
17
+ * length does not. Inside them, `allowInCalc` names literals legal only
18
+ * there (a unitless multiplier), and `offsetCap` lets a declaration
19
+ * subtract a few px from a token (a border inset) without allowing the px
20
+ * value anywhere else.
21
+ *
22
+ * Declarations inside descriptor at-rules (`@font-face`, `@counter-style`,
23
+ * `@property`, `@page`, …) are not property declarations and are not
24
+ * judged: `font-weight` in `@font-face` names a face, not a type step.
19
25
  *
20
26
  * Runs over CSS files the patch creates, not files it modifies: an upstream
21
- * stylesheet is not the fork's to put on a scale. A line carrying
22
- * `fireforge-ignore: token-scale` is skipped.
27
+ * stylesheet is not the fork's to put on a scale. A file a scale `exclude`s
28
+ * is not judged against that scale. A line carrying
29
+ * `fireforge-ignore: token-scale` is skipped. A scale with `minFiles` also
30
+ * fails the queue (`token-scale-vacuous`) when fewer created files carry
31
+ * its properties, so an emptied scan set cannot pass by judging nothing.
23
32
  */
24
33
  import type { PatchLintIssue } from '../types/commands/index.js';
25
34
  import type { PatchLintTokenScale } from '../types/config.js';
@@ -31,9 +40,18 @@ export declare function findOffScaleAtom(value: string, scale: PatchLintTokenSca
31
40
  /**
32
41
  * Lints one patch-created CSS file against the configured scales.
33
42
  *
34
- * @param file - Engine-relative path, for the issue
43
+ * @param file - Engine-relative path, for the issue and the `exclude` globs
35
44
  * @param css - The file's full content
36
45
  * @param scales - `patchLint.tokenScales`
37
46
  * @returns One `token-scale` error per off-scale declaration
38
47
  */
39
48
  export declare function lintTokenScales(file: string, css: string, scales: readonly PatchLintTokenScale[]): PatchLintIssue[];
49
+ /**
50
+ * The `minFiles` floor of every scale over the queue's created stylesheets:
51
+ * one `token-scale-vacuous` error per scale that fewer created CSS files
52
+ * (outside its `exclude`) carry a declaration of.
53
+ *
54
+ * @param createdFiles - Every patch-created file's path and content
55
+ * @param scales - `patchLint.tokenScales`
56
+ */
57
+ export declare function lintTokenScaleFloors(createdFiles: Iterable<readonly [string, string]>, scales: readonly PatchLintTokenScale[]): PatchLintIssue[];