@hominis/fireforge 0.50.0 → 0.51.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 (205) hide show
  1. package/CHANGELOG.md +47 -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/test-build-furnace.d.ts +3 -2
  63. package/dist/src/commands/test-build-furnace.js +8 -2
  64. package/dist/src/commands/test-build-phase.js +24 -7
  65. package/dist/src/commands/test-checkjs.js +3 -2
  66. package/dist/src/commands/test-dispatch.d.ts +39 -0
  67. package/dist/src/commands/test-dispatch.js +211 -0
  68. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  69. package/dist/src/commands/test-furnace-reach.js +132 -0
  70. package/dist/src/commands/test-orchestrate.d.ts +9 -0
  71. package/dist/src/commands/test-orchestrate.js +22 -4
  72. package/dist/src/commands/test-perf-scope.d.ts +21 -0
  73. package/dist/src/commands/test-perf-scope.js +55 -0
  74. package/dist/src/commands/test-prelock.js +100 -4
  75. package/dist/src/commands/test-register.js +18 -3
  76. package/dist/src/commands/test-results.d.ts +62 -0
  77. package/dist/src/commands/test-results.js +62 -0
  78. package/dist/src/commands/test-run.d.ts +13 -55
  79. package/dist/src/commands/test-run.js +73 -101
  80. package/dist/src/commands/test-shards.d.ts +110 -0
  81. package/dist/src/commands/test-shards.js +255 -0
  82. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  83. package/dist/src/commands/test-stale-gate.js +4 -2
  84. package/dist/src/commands/test-verdict.d.ts +2 -1
  85. package/dist/src/commands/test.d.ts +1 -1
  86. package/dist/src/commands/test.js +65 -75
  87. package/dist/src/commands/token-lint.js +48 -8
  88. package/dist/src/commands/verify.js +9 -2
  89. package/dist/src/core/build-prepare.d.ts +6 -0
  90. package/dist/src/core/build-prepare.js +29 -3
  91. package/dist/src/core/config-paths.d.ts +1 -1
  92. package/dist/src/core/config-paths.js +2 -0
  93. package/dist/src/core/config-validate-check.js +83 -7
  94. package/dist/src/core/config-validate-test-toolchains.js +14 -0
  95. package/dist/src/core/config-validate-token-scales.d.ts +3 -0
  96. package/dist/src/core/config-validate-token-scales.js +47 -5
  97. package/dist/src/core/config-validate.js +6 -0
  98. package/dist/src/core/coverage-extend.js +2 -2
  99. package/dist/src/core/coverage-records.d.ts +11 -0
  100. package/dist/src/core/coverage-records.js +34 -0
  101. package/dist/src/core/engine-session-lock.d.ts +6 -0
  102. package/dist/src/core/engine-session-lock.js +10 -2
  103. package/dist/src/core/file-lock-shared.d.ts +57 -0
  104. package/dist/src/core/file-lock-shared.js +117 -0
  105. package/dist/src/core/file-lock.d.ts +9 -0
  106. package/dist/src/core/file-lock.js +58 -2
  107. package/dist/src/core/furnace-apply.js +10 -10
  108. package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
  109. package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
  110. package/dist/src/core/furnace-config-validate.js +7 -1
  111. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  112. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  113. package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
  114. package/dist/src/core/furnace-validate-accessibility.js +61 -27
  115. package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
  116. package/dist/src/core/furnace-validate-exceptions.js +41 -4
  117. package/dist/src/core/furnace-validate-icon-button.js +30 -1
  118. package/dist/src/core/history-log-file.js +8 -1
  119. package/dist/src/core/line-prefixer.d.ts +18 -0
  120. package/dist/src/core/line-prefixer.js +34 -0
  121. package/dist/src/core/mach.d.ts +7 -0
  122. package/dist/src/core/mach.js +24 -9
  123. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  124. package/dist/src/core/module-resolution-preflight.js +24 -6
  125. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  126. package/dist/src/core/negative-control-compiled.js +73 -0
  127. package/dist/src/core/negative-control-diff.d.ts +14 -2
  128. package/dist/src/core/negative-control-diff.js +49 -6
  129. package/dist/src/core/negative-control.d.ts +12 -0
  130. package/dist/src/core/negative-control.js +48 -5
  131. package/dist/src/core/park-store.d.ts +63 -0
  132. package/dist/src/core/park-store.js +216 -0
  133. package/dist/src/core/patch-export-update.d.ts +4 -0
  134. package/dist/src/core/patch-export-update.js +12 -3
  135. package/dist/src/core/patch-export.d.ts +3 -2
  136. package/dist/src/core/patch-export.js +21 -14
  137. package/dist/src/core/patch-files.js +26 -8
  138. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
  139. package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
  140. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  141. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  142. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  143. package/dist/src/core/patch-lint-checkjs.js +13 -3
  144. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  145. package/dist/src/core/patch-lint-cross.js +5 -0
  146. package/dist/src/core/patch-lint-css.js +14 -10
  147. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
  148. package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
  149. package/dist/src/core/patch-lint-ignore.d.ts +56 -12
  150. package/dist/src/core/patch-lint-ignore.js +173 -43
  151. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  152. package/dist/src/core/patch-lint-ownership.js +26 -0
  153. package/dist/src/core/patch-lint-size.js +9 -1
  154. package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
  155. package/dist/src/core/patch-lint-token-scale.js +206 -29
  156. package/dist/src/core/patch-lint.js +2 -2
  157. package/dist/src/core/patch-manifest-io.js +8 -1
  158. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  159. package/dist/src/core/patch-manifest-key-order.js +61 -0
  160. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  161. package/dist/src/core/patch-manifest-stage.js +96 -0
  162. package/dist/src/core/patch-manifest-validate.js +27 -1
  163. package/dist/src/core/patch-order-legal.d.ts +2 -1
  164. package/dist/src/core/patch-order-legal.js +7 -2
  165. package/dist/src/core/patch-policy.d.ts +6 -2
  166. package/dist/src/core/patch-policy.js +14 -7
  167. package/dist/src/core/patch-retired.d.ts +26 -4
  168. package/dist/src/core/patch-retired.js +40 -2
  169. package/dist/src/core/run-log.d.ts +6 -0
  170. package/dist/src/core/run-log.js +17 -1
  171. package/dist/src/core/test-harness-crash.d.ts +2 -1
  172. package/dist/src/core/test-harness-crash.js +5 -8
  173. package/dist/src/core/test-harness-signature.d.ts +16 -0
  174. package/dist/src/core/test-harness-signature.js +19 -1
  175. package/dist/src/core/test-raw-log.d.ts +66 -0
  176. package/dist/src/core/test-raw-log.js +122 -0
  177. package/dist/src/core/test-run-completeness.d.ts +36 -3
  178. package/dist/src/core/test-run-completeness.js +57 -17
  179. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  180. package/dist/src/core/test-xpcshell-retry.js +2 -1
  181. package/dist/src/core/token-lint.d.ts +5 -2
  182. package/dist/src/core/token-lint.js +8 -3
  183. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  184. package/dist/src/core/token-sibling-sheets.js +119 -0
  185. package/dist/src/core/tree-guard.js +2 -0
  186. package/dist/src/core/typecheck-shim.d.ts +5 -1
  187. package/dist/src/core/typecheck-shim.js +115 -2
  188. package/dist/src/core/typecheck.js +3 -1
  189. package/dist/src/index.d.ts +9 -3
  190. package/dist/src/index.js +4 -1
  191. package/dist/src/types/commands/index.d.ts +2 -2
  192. package/dist/src/types/commands/options.d.ts +68 -7
  193. package/dist/src/types/commands/patches.d.ts +43 -0
  194. package/dist/src/types/config.d.ts +74 -2
  195. package/dist/src/types/furnace.d.ts +20 -0
  196. package/dist/src/types/index.d.ts +1 -1
  197. package/dist/src/utils/options.d.ts +3 -2
  198. package/dist/src/utils/options.js +9 -4
  199. package/docs/configuration.md +94 -9
  200. package/docs/furnace.md +43 -1
  201. package/docs/lifecycle-invariants.md +13 -11
  202. package/docs/machine-output.md +64 -18
  203. package/docs/patch-workflow.md +174 -13
  204. package/docs/testing.md +73 -4
  205. package/package.json +3 -3
@@ -0,0 +1,216 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Storage and merge logic behind `fireforge park`.
4
+ *
5
+ * Several sessions edit the same Hominis files on one engine, and a peer's
6
+ * `re-export --refuse-foreign-drift` refuses while another session's hunks
7
+ * sit in its patch's files. Setting those hunks aside meant reversing each
8
+ * one by hand. A park records, for every file of one patch that drifted:
9
+ *
10
+ * - `before`: the file as the session left it (what the park takes away),
11
+ * - `after`: what the park writes in its place (the queue-applied
12
+ * baseline, or the working tree minus the named hunks).
13
+ *
14
+ * Restoring is a three-way merge of the file as it is now (ours) with
15
+ * `before` (theirs) over `after` (base), so the owner's commit in between
16
+ * is kept and the parked hunks come back on top of it.
17
+ *
18
+ * A park lives under `.fireforge/park/<label>/`: `park.json` plus one copy
19
+ * of each side per file. Absent files are recorded as absent, never as
20
+ * empty.
21
+ */
22
+ import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
23
+ import { tmpdir } from 'node:os';
24
+ import { dirname, join } from 'node:path';
25
+ import { InvalidArgumentError } from '../errors/base.js';
26
+ import { getNodeErrorCode } from '../utils/errors.js';
27
+ import { ensureDir, pathExists, readJson, readText, removeFile, writeJson, writeText, } from '../utils/fs.js';
28
+ import { exec } from '../utils/process.js';
29
+ import { FIREFORGE_DIR } from './config-paths.js';
30
+ const LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
31
+ /** The directory holding every park of a project. */
32
+ function parkRoot(projectRoot) {
33
+ return join(projectRoot, FIREFORGE_DIR, 'park');
34
+ }
35
+ /** Refuses a label that is not a plain file-name segment. */
36
+ function assertParkLabel(label) {
37
+ if (!LABEL_PATTERN.test(label)) {
38
+ throw new InvalidArgumentError(`Park label "${label}" must be letters, digits, ".", "_" or "-", starting with a letter or digit.`, '--label');
39
+ }
40
+ }
41
+ function sideDir(dir, side) {
42
+ return join(dir, side);
43
+ }
44
+ /** Writes a park; refuses to overwrite an existing label. */
45
+ export async function writePark(projectRoot, record) {
46
+ assertParkLabel(record.label);
47
+ const dir = join(parkRoot(projectRoot), record.label);
48
+ if (await pathExists(dir)) {
49
+ throw new InvalidArgumentError(`A park named "${record.label}" already exists. Restore or drop it first, or pass another --label.`, '--label');
50
+ }
51
+ for (const file of record.files) {
52
+ for (const side of ['before', 'after']) {
53
+ const content = file[side];
54
+ if (content === null)
55
+ continue;
56
+ const target = join(sideDir(dir, side), file.path);
57
+ await ensureDir(dirname(target));
58
+ await writeText(target, content);
59
+ }
60
+ }
61
+ const index = {
62
+ label: record.label,
63
+ patch: record.patch,
64
+ createdAt: record.createdAt,
65
+ mode: record.mode,
66
+ files: record.files.map((file) => ({
67
+ path: file.path,
68
+ before: file.before !== null,
69
+ after: file.after !== null,
70
+ })),
71
+ };
72
+ await writeJson(join(dir, 'park.json'), index);
73
+ return dir;
74
+ }
75
+ /** Reads a park back, contents included. */
76
+ export async function readPark(projectRoot, label) {
77
+ assertParkLabel(label);
78
+ const dir = join(parkRoot(projectRoot), label);
79
+ if (!(await pathExists(join(dir, 'park.json')))) {
80
+ throw new InvalidArgumentError(`No park named "${label}". List them with "fireforge park --list".`, label);
81
+ }
82
+ const index = await readJson(join(dir, 'park.json'));
83
+ const files = [];
84
+ for (const entry of index.files) {
85
+ files.push({
86
+ path: entry.path,
87
+ before: entry.before ? await readText(join(sideDir(dir, 'before'), entry.path)) : null,
88
+ after: entry.after ? await readText(join(sideDir(dir, 'after'), entry.path)) : null,
89
+ });
90
+ }
91
+ return { ...index, files };
92
+ }
93
+ /** Every park of the project, oldest first. */
94
+ export async function listParks(projectRoot) {
95
+ let names;
96
+ try {
97
+ names = (await readdir(parkRoot(projectRoot), { withFileTypes: true }))
98
+ .filter((entry) => entry.isDirectory())
99
+ .map((entry) => entry.name);
100
+ }
101
+ catch (error) {
102
+ if (getNodeErrorCode(error) === 'ENOENT')
103
+ return [];
104
+ throw error;
105
+ }
106
+ const parks = [];
107
+ for (const name of names) {
108
+ const path = join(parkRoot(projectRoot), name, 'park.json');
109
+ if (await pathExists(path))
110
+ parks.push(await readJson(path));
111
+ }
112
+ return parks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
113
+ }
114
+ /** Deletes a park. */
115
+ export async function dropPark(projectRoot, label) {
116
+ assertParkLabel(label);
117
+ const dir = join(parkRoot(projectRoot), label);
118
+ if (!(await pathExists(dir))) {
119
+ throw new InvalidArgumentError(`No park named "${label}".`, label);
120
+ }
121
+ await rm(dir, { recursive: true, force: true });
122
+ }
123
+ /** Reads an engine file, null when absent. */
124
+ export async function readEngineFile(engineDir, path) {
125
+ const full = join(engineDir, path);
126
+ return (await pathExists(full)) ? await readText(full) : null;
127
+ }
128
+ /** Writes an engine file, removing it for null. */
129
+ export async function writeEngineFile(engineDir, path, content) {
130
+ const full = join(engineDir, path);
131
+ if (content === null) {
132
+ if (await pathExists(full))
133
+ await removeFile(full);
134
+ return;
135
+ }
136
+ await ensureDir(dirname(full));
137
+ await writeText(full, content);
138
+ }
139
+ /**
140
+ * Three-way merges `ours` and `theirs` over `base` with `git merge-file`.
141
+ * Conflicts come back as marker blocks.
142
+ */
143
+ export async function mergeThreeWay(ours, base, theirs, path) {
144
+ const scratch = await mkdtemp(join(tmpdir(), 'fireforge-park-'));
145
+ try {
146
+ const files = {
147
+ ours: join(scratch, 'ours'),
148
+ base: join(scratch, 'base'),
149
+ theirs: join(scratch, 'theirs'),
150
+ };
151
+ await writeFile(files.ours, ours);
152
+ await writeFile(files.base, base);
153
+ await writeFile(files.theirs, theirs);
154
+ const result = await exec('git', [
155
+ 'merge-file',
156
+ '-p',
157
+ '-L',
158
+ `${path} (now)`,
159
+ '-L',
160
+ `${path} (parked from)`,
161
+ '-L',
162
+ `${path} (parked)`,
163
+ files.ours,
164
+ files.base,
165
+ files.theirs,
166
+ ]);
167
+ if (result.exitCode < 0 || result.exitCode > 127) {
168
+ throw new Error(`git merge-file failed for ${path}: ${result.stderr.trim()}`);
169
+ }
170
+ return { content: result.stdout, conflicts: result.exitCode };
171
+ }
172
+ finally {
173
+ await rm(scratch, { recursive: true, force: true });
174
+ }
175
+ }
176
+ /**
177
+ * What restoring one parked file writes: `before` when the file is still
178
+ * as the park left it, nothing when it already matches `before`, and a
179
+ * three-way merge otherwise. Absence only merges with absence.
180
+ */
181
+ export async function planRestore(file, now) {
182
+ if (now === file.after)
183
+ return { outcome: 'restored', content: file.before };
184
+ if (now === file.before)
185
+ return { outcome: 'unchanged', content: now };
186
+ if (now === null || file.before === null || file.after === null) {
187
+ return { outcome: 'conflict', content: now };
188
+ }
189
+ const merged = await mergeThreeWay(now, file.after, file.before, file.path);
190
+ return { outcome: merged.conflicts > 0 ? 'conflict' : 'merged', content: merged.content };
191
+ }
192
+ /** Message ids a Fluent file's lines declare (`id = …`, `-term = …`). */
193
+ export function fluentIds(lines) {
194
+ const ids = [];
195
+ for (const line of lines) {
196
+ const match = /^(-?[A-Za-z][\w-]*)\s*=/.exec(line);
197
+ if (match?.[1] !== undefined)
198
+ ids.push(match[1]);
199
+ }
200
+ return ids;
201
+ }
202
+ /** Lines `before` has that `after` lacks, as a multiset difference. */
203
+ export function removedLines(before, after) {
204
+ const remaining = new Map();
205
+ for (const line of (after ?? '').split('\n'))
206
+ remaining.set(line, (remaining.get(line) ?? 0) + 1);
207
+ const removed = [];
208
+ for (const line of (before ?? '').split('\n')) {
209
+ const count = remaining.get(line) ?? 0;
210
+ if (count > 0)
211
+ remaining.set(line, count - 1);
212
+ else
213
+ removed.push(line);
214
+ }
215
+ return removed;
216
+ }
@@ -24,6 +24,8 @@ export interface UpdatePatchAndMetadataOptions {
24
24
  newContent: string;
25
25
  /** Manifest-row fields to merge into the patch's metadata. */
26
26
  updates: Partial<PatchMetadata>;
27
+ /** Manifest-row fields to delete, e.g. a staged block left empty. */
28
+ unset?: readonly (keyof PatchMetadata)[] | undefined;
27
29
  /** Hook run under the lock after the mutation succeeds. */
28
30
  onCommitted?: UpdatePatchCommittedHook | undefined;
29
31
  /** Policy gate run against the under-lock projected manifest. */
@@ -49,3 +51,5 @@ export interface UpdatePatchAndMetadataOptions {
49
51
  * already byte-identical to the new content.
50
52
  */
51
53
  export declare function updatePatchAndMetadata(options: UpdatePatchAndMetadataOptions): Promise<boolean>;
54
+ /** A copy of `row` without `fields`. */
55
+ export declare function withoutFields(row: PatchMetadata, fields: readonly (keyof PatchMetadata)[]): PatchMetadata;
@@ -25,7 +25,7 @@ import { buildProjectedManifest, enforcePatchPolicy } from './patch-policy.js';
25
25
  * already byte-identical to the new content.
26
26
  */
27
27
  export async function updatePatchAndMetadata(options) {
28
- const { patchesDir, filename, newContent, updates, onCommitted, policyGate, lockOptions = {}, } = options;
28
+ const { patchesDir, filename, newContent, updates, unset = [], onCommitted, policyGate, lockOptions = {}, } = options;
29
29
  return withPatchDirectoryLock(patchesDir, async () => {
30
30
  const manifest = await loadPatchesManifestForWrite(patchesDir);
31
31
  if (!manifest) {
@@ -41,7 +41,7 @@ export async function updatePatchAndMetadata(options) {
41
41
  }
42
42
  const originalContent = await readText(patchPath);
43
43
  const existingPatch = manifest.patches[patchIndex];
44
- manifest.patches[patchIndex] = { ...existingPatch, ...updates };
44
+ manifest.patches[patchIndex] = withoutFields({ ...existingPatch, ...updates }, unset);
45
45
  if (policyGate !== undefined) {
46
46
  enforcePatchPolicy({
47
47
  config: policyGate.config,
@@ -59,7 +59,7 @@ export async function updatePatchAndMetadata(options) {
59
59
  await writeText(patchPath, normalized);
60
60
  patchWritten = true;
61
61
  }
62
- await mutatePatchRowsInManifest(patchesDir, [filename], () => ({ set: updates }));
62
+ await mutatePatchRowsInManifest(patchesDir, [filename], () => ({ set: updates, unset }));
63
63
  }
64
64
  catch (error) {
65
65
  if (patchWritten) {
@@ -84,3 +84,12 @@ export async function updatePatchAndMetadata(options) {
84
84
  return bodyChanged;
85
85
  }, lockOptions);
86
86
  }
87
+ /** A copy of `row` without `fields`. */
88
+ export function withoutFields(row, fields) {
89
+ if (fields.length === 0)
90
+ return row;
91
+ const copy = { ...row };
92
+ for (const field of fields)
93
+ Reflect.deleteProperty(copy, field);
94
+ return copy;
95
+ }
@@ -18,9 +18,10 @@ export declare function buildPlanExportInput(input: PlanExportInput): PlanExport
18
18
  /**
19
19
  * Gets the next patch number for a new patch.
20
20
  * @param patchesDir - Path to the patches directory
21
+ * @param reserved - Orders held by `patch reserve`, skipped
21
22
  * @returns Next patch number (e.g., "005" for 4 existing patches)
22
23
  */
23
- export declare function getNextPatchNumber(patchesDir: string): Promise<string>;
24
+ export declare function getNextPatchNumber(patchesDir: string, reserved?: ReadonlySet<number>): Promise<string>;
24
25
  /**
25
26
  * Sanitizes a human-readable name into a filename slug.
26
27
  *
@@ -59,7 +60,7 @@ export declare function patchNameSlug(name: string, category: string): string;
59
60
  * @param name - Human-readable name
60
61
  * @returns Filename like "001-ui-sidebar.patch"
61
62
  */
62
- export declare function getNextPatchFilename(patchesDir: string, category: PatchCategory, name: string): Promise<string>;
63
+ export declare function getNextPatchFilename(patchesDir: string, category: PatchCategory, name: string, reserved?: ReadonlySet<number>): Promise<string>;
63
64
  export interface CommitExportedPatchInput {
64
65
  patchesDir: string;
65
66
  category: PatchCategory;
@@ -2,13 +2,13 @@
2
2
  import { join } from 'node:path';
3
3
  import { toError } from '../utils/errors.js';
4
4
  import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
5
- import { warn } from '../utils/logger.js';
5
+ import { info, warn } from '../utils/logger.js';
6
6
  import { discoverPatches, withPatchDirectoryLock } from './patch-apply.js';
7
7
  import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
8
8
  import { addPatchToManifest, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
9
9
  import { formatPatchOrder, requirePatchOrder } from './patch-parse.js';
10
10
  import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
11
- import { manifestWithRetiredSlots } from './patch-retired.js';
11
+ import { manifestWithRetiredSlots, reservationAt, reservedOrderSet } from './patch-retired.js';
12
12
  export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
13
13
  import { escapeRegex } from '../utils/regex.js';
14
14
  export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
@@ -40,18 +40,16 @@ export function buildPlanExportInput(input) {
40
40
  /**
41
41
  * Gets the next patch number for a new patch.
42
42
  * @param patchesDir - Path to the patches directory
43
+ * @param reserved - Orders held by `patch reserve`, skipped
43
44
  * @returns Next patch number (e.g., "005" for 4 existing patches)
44
45
  */
45
- export async function getNextPatchNumber(patchesDir) {
46
+ export async function getNextPatchNumber(patchesDir, reserved = new Set()) {
46
47
  const patches = await discoverPatches(patchesDir);
47
- if (patches.length === 0) {
48
- return '001';
49
- }
50
48
  const finitePatches = patches.filter((p) => Number.isFinite(p.order));
51
- if (finitePatches.length === 0)
52
- return '001';
53
49
  const maxOrder = finitePatches.reduce((max, p) => Math.max(max, p.order), 0);
54
- const nextNumber = maxOrder + 1;
50
+ let nextNumber = maxOrder + 1;
51
+ while (reserved.has(nextNumber))
52
+ nextNumber++;
55
53
  return String(nextNumber).padStart(Math.max(3, String(nextNumber).length), '0');
56
54
  }
57
55
  /**
@@ -118,8 +116,8 @@ export function patchNameSlug(name, category) {
118
116
  * @param name - Human-readable name
119
117
  * @returns Filename like "001-ui-sidebar.patch"
120
118
  */
121
- export async function getNextPatchFilename(patchesDir, category, name) {
122
- const patchNumber = await getNextPatchNumber(patchesDir);
119
+ export async function getNextPatchFilename(patchesDir, category, name, reserved = new Set()) {
120
+ const patchNumber = await getNextPatchNumber(patchesDir, reserved);
123
121
  const sanitizedName = patchNameSlug(name, category);
124
122
  return `${patchNumber}-${category}-${sanitizedName}.patch`;
125
123
  }
@@ -234,12 +232,15 @@ async function computeExportPlanUnderLock(input) {
234
232
  // wipes the queue metadata, and the rollback path would then delete
235
233
  // patches.json entirely because manifestBefore looked absent.
236
234
  const manifestBefore = await loadPatchesManifestForWrite(input.patchesDir);
235
+ // A default allocation never lands on a reservation; only an explicit
236
+ // `--order` placement fills one.
237
+ const reserved = reservedOrderSet(manifestBefore ?? {});
237
238
  const policyOrder = input.config !== undefined
238
- ? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category)
239
+ ? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category, reserved)
239
240
  : null;
240
241
  const patchFilename = policyOrder !== null
241
242
  ? `${formatPatchOrder(policyOrder)}-${input.category}-${patchNameSlug(input.name, input.category)}.patch`
242
- : await getNextPatchFilename(input.patchesDir, input.category, input.name);
243
+ : await getNextPatchFilename(input.patchesDir, input.category, input.name, reserved);
243
244
  const patchPath = join(input.patchesDir, patchFilename);
244
245
  const metadata = {
245
246
  filename: patchFilename,
@@ -267,6 +268,12 @@ async function computeExportPlanUnderLock(input) {
267
268
  const afterPatches = (manifestBefore?.patches ?? []).filter((p) => !supersededSet.has(p.filename) && p.filename !== patchFilename);
268
269
  afterPatches.push(metadata);
269
270
  afterPatches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
271
+ const reservation = reservationAt(manifestBefore, metadata.order);
272
+ if (reservation !== undefined) {
273
+ info(`Order ${formatPatchOrder(metadata.order)} was reserved by ${reservation.owner}` +
274
+ `${reservation.reason !== undefined ? ` (${reservation.reason})` : ''}; ` +
275
+ 'this export fills the reservation.');
276
+ }
270
277
  return {
271
278
  patchFilename,
272
279
  patchPath,
@@ -274,7 +281,7 @@ async function computeExportPlanUnderLock(input) {
274
281
  supersededDetails,
275
282
  supersededPatches,
276
283
  manifestBefore: manifestBefore ?? null,
277
- // A patch landing on a retired order fills that slot.
284
+ // A patch landing on a retired or reserved order fills that slot.
278
285
  manifestAfter: manifestWithRetiredSlots(manifestBefore, afterPatches),
279
286
  };
280
287
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { readdir } from 'node:fs/promises';
2
+ import { readdir, stat } from 'node:fs/promises';
3
3
  import { extname, join } from 'node:path';
4
4
  import { pathExists, readText } from '../utils/fs.js';
5
5
  import { extractAffectedFiles, extractOrder } from './patch-parse.js';
@@ -9,16 +9,34 @@ export async function discoverPatches(patchesDir) {
9
9
  return [];
10
10
  }
11
11
  const entries = await readdir(patchesDir, { withFileTypes: true });
12
- const patches = entries
13
- .filter((entry) => entry.isFile() && extname(entry.name) === '.patch')
14
- .map((entry) => ({
15
- path: join(patchesDir, entry.name),
16
- filename: entry.name,
17
- order: extractOrder(entry.name),
18
- }));
12
+ const patches = [];
13
+ for (const entry of entries) {
14
+ if (extname(entry.name) !== '.patch')
15
+ continue;
16
+ // A symlinked patch (a scratch queue built from links to the real
17
+ // patches) counts when it resolves to a regular file; a dangling link or
18
+ // a link to a directory does not.
19
+ if (!entry.isFile() &&
20
+ !(entry.isSymbolicLink() && (await isLinkToFile(patchesDir, entry.name)))) {
21
+ continue;
22
+ }
23
+ patches.push({
24
+ path: join(patchesDir, entry.name),
25
+ filename: entry.name,
26
+ order: extractOrder(entry.name),
27
+ });
28
+ }
19
29
  patches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
20
30
  return patches;
21
31
  }
32
+ async function isLinkToFile(dir, name) {
33
+ try {
34
+ return (await stat(join(dir, name))).isFile();
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
22
40
  /** Counts the patch files currently present in a patch directory. */
23
41
  export async function countPatches(patchesDir) {
24
42
  const patches = await discoverPatches(patchesDir);
@@ -2,10 +2,12 @@
2
2
  * Explains an implicit-`any` checkJs finding that comes from a URL import
3
3
  * the per-patch program cannot resolve.
4
4
  *
5
- * The program only holds patch-owned files (and, for a test, its own
6
- * directory's head.js), so a `resource:///` or `chrome://` import of a
7
- * module another patch creates falls back to the shim's
8
- * `declare module 'resource:*'` wildcard and types as `any`. A callback
5
+ * The program resolves URL imports only to modules the queue creates (for
6
+ * a test, those created up to its own patch, plus its directory's
7
+ * head.js), so a `resource:///` or `chrome://` import of a module a later
8
+ * patch creates, an upstream module, or a basename two patches create
9
+ * falls back to the shim's `declare module 'resource:*'` wildcard and
10
+ * types as `any`. A callback
9
11
  * over its values then has implicitly-`any` parameters under
10
12
  * `checkJsStrict`, while the whole-tree `fireforge typecheck`, which sees
11
13
  * the module, is clean. Without the note the two disagree with no
@@ -3,10 +3,12 @@
3
3
  * Explains an implicit-`any` checkJs finding that comes from a URL import
4
4
  * the per-patch program cannot resolve.
5
5
  *
6
- * The program only holds patch-owned files (and, for a test, its own
7
- * directory's head.js), so a `resource:///` or `chrome://` import of a
8
- * module another patch creates falls back to the shim's
9
- * `declare module 'resource:*'` wildcard and types as `any`. A callback
6
+ * The program resolves URL imports only to modules the queue creates (for
7
+ * a test, those created up to its own patch, plus its directory's
8
+ * head.js), so a `resource:///` or `chrome://` import of a module a later
9
+ * patch creates, an upstream module, or a basename two patches create
10
+ * falls back to the shim's `declare module 'resource:*'` wildcard and
11
+ * types as `any`. A callback
10
12
  * over its values then has implicitly-`any` parameters under
11
13
  * `checkJsStrict`, while the whole-tree `fireforge typecheck`, which sees
12
14
  * the module, is clean. Without the note the two disagree with no
@@ -35,9 +37,9 @@ export function anyTypedImportHint(code, specifiers) {
35
37
  return '';
36
38
  const list = [...specifiers].sort().join(', ');
37
39
  return (` (This file imports ${list}, which this per-patch checkJs program cannot resolve — ` +
38
- 'the module is created by another patch outside this program, a later one or one this ' +
39
- 'test does not share a directory with — so the import is typed `any`. Annotate the ' +
40
- 'parameter explicitly; the whole-tree "fireforge typecheck" resolves the real type.)');
40
+ 'no patch up to this one creates the module under that name: a later patch creates it, ' +
41
+ 'it is upstream, or two patches create the same basename — so the import is typed `any`. ' +
42
+ 'Annotate the parameter explicitly; the whole-tree "fireforge typecheck" resolves the real type.)');
41
43
  }
42
44
  /**
43
45
  * The host's `resolveModuleNameLiterals`: owned specifiers first, then a
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Collects a checkJs program's diagnostics, for every file or for a report
3
+ * scope only. A per-test program resolves the modules its imports reach
4
+ * (see `runCheckJsTestFilesGrouped`), and asking for the whole program's
5
+ * diagnostics would type-check each of those modules once per test, so a
6
+ * scoped run asks only for the files it reports. Split out of
7
+ * patch-lint-checkjs.ts for the line budget.
8
+ */
9
+ /**
10
+ * The program's semantic and syntactic diagnostics, limited to the files
11
+ * `reportOnly` names when it is set.
12
+ *
13
+ * @param program - The built program
14
+ * @param relByAbsolute - Owned files: TS path → repo-relative path
15
+ * @param reportOnly - Repo-relative files to collect for, or all when unset
16
+ */
17
+ export declare function collectCheckJsDiagnostics(program: import('typescript').Program, relByAbsolute: ReadonlyMap<string, string>, reportOnly: ReadonlySet<string> | undefined): import('typescript').Diagnostic[];
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Collects a checkJs program's diagnostics, for every file or for a report
4
+ * scope only. A per-test program resolves the modules its imports reach
5
+ * (see `runCheckJsTestFilesGrouped`), and asking for the whole program's
6
+ * diagnostics would type-check each of those modules once per test, so a
7
+ * scoped run asks only for the files it reports. Split out of
8
+ * patch-lint-checkjs.ts for the line budget.
9
+ */
10
+ /**
11
+ * The program's semantic and syntactic diagnostics, limited to the files
12
+ * `reportOnly` names when it is set.
13
+ *
14
+ * @param program - The built program
15
+ * @param relByAbsolute - Owned files: TS path → repo-relative path
16
+ * @param reportOnly - Repo-relative files to collect for, or all when unset
17
+ */
18
+ export function collectCheckJsDiagnostics(program, relByAbsolute, reportOnly) {
19
+ if (reportOnly === undefined) {
20
+ return [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()];
21
+ }
22
+ return program
23
+ .getSourceFiles()
24
+ .filter((sourceFile) => {
25
+ const rel = relByAbsolute.get(sourceFile.fileName);
26
+ return rel !== undefined && reportOnly.has(rel);
27
+ })
28
+ .flatMap((sourceFile) => [
29
+ ...program.getSemanticDiagnostics(sourceFile),
30
+ ...program.getSyntacticDiagnostics(sourceFile),
31
+ ]);
32
+ }
@@ -82,6 +82,8 @@ export interface RunCheckJsGroupedInput {
82
82
  * compiler options and shim in one run. See {@link runCheckJsTestFilesGrouped}.
83
83
  */
84
84
  sourceFileCache?: CheckJsSourceFileCache;
85
+ /** When set, diagnostics are collected only for these repo-relative files. */
86
+ reportOnly?: ReadonlySet<string>;
85
87
  }
86
88
  /**
87
89
  * Builds the checkJs program once over `resolutionOwned` and returns its
@@ -136,12 +138,19 @@ export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles:
136
138
  * reported per program (head helpers report from their own program), and
137
139
  * run-level errors are de-duplicated across programs.
138
140
  *
141
+ * With `resolvableModules`, a test's `resource:///` and `chrome://`
142
+ * imports also resolve to the modules it returns for that test (the
143
+ * modules the queue creates up to the test's own patch), so a test that
144
+ * imports an earlier patch's module is typed against its real source
145
+ * instead of the shim's `any` wildcard. Those modules are resolution
146
+ * targets only: never roots, and never reported on.
147
+ *
139
148
  * @param repoDir - Absolute engine (repository) directory
140
149
  * @param testFiles - Patch-owned test-script paths (repo-relative)
141
150
  * @param patchLint - The resolved `patchLint` config block
142
151
  * @param projectRoot - FireForge project root for shim resolution
143
152
  */
144
- export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
153
+ export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>, resolvableModules?: (testFile: string) => ReadonlySet<string>): Promise<GroupedCheckJsResult>;
145
154
  /**
146
155
  * Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
147
156
  * checkJs program over `patchOwnedFiles` and returns its findings grouped by
@@ -33,6 +33,7 @@ import { normalizePathSlashes } from '../utils/paths.js';
33
33
  import { withGeckoDomTypes } from './gecko-dom-types.js';
34
34
  import { anyTypedImportHint, createModuleLiteralResolver, } from './patch-lint-checkjs-any-import.js';
35
35
  import { withSharedSourceFiles } from './patch-lint-checkjs-cache.js';
36
+ import { collectCheckJsDiagnostics } from './patch-lint-checkjs-diagnostics.js';
36
37
  import { collectUnmanagedCompanions, retargetUnmanagedCompanionHints, } from './patch-lint-unmanaged-companion.js';
37
38
  import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
38
39
  // ---------------------------------------------------------------------------
@@ -340,7 +341,7 @@ export async function runCheckJsGrouped(input) {
340
341
  resolveModuleNameLiterals: createModuleLiteralResolver(resolveOwnedSpecifier, resolveViaPaths, anyTypedImports),
341
342
  };
342
343
  const program = ts.createProgram(rootFiles, options, withSharedSourceFiles(host, input.sourceFileCache, shimPath, shimSource));
343
- const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning', anyTypedImports);
344
+ const byFile = groupOwnedDiagnostics(ts, collectCheckJsDiagnostics(program, relByAbsolute, input.reportOnly), relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning', anyTypedImports);
344
345
  verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
345
346
  return { byFile, global: [] };
346
347
  }
@@ -448,12 +449,19 @@ export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint
448
449
  * reported per program (head helpers report from their own program), and
449
450
  * run-level errors are de-duplicated across programs.
450
451
  *
452
+ * With `resolvableModules`, a test's `resource:///` and `chrome://`
453
+ * imports also resolve to the modules it returns for that test (the
454
+ * modules the queue creates up to the test's own patch), so a test that
455
+ * imports an earlier patch's module is typed against its real source
456
+ * instead of the shim's `any` wildcard. Those modules are resolution
457
+ * targets only: never roots, and never reported on.
458
+ *
451
459
  * @param repoDir - Absolute engine (repository) directory
452
460
  * @param testFiles - Patch-owned test-script paths (repo-relative)
453
461
  * @param patchLint - The resolved `patchLint` config block
454
462
  * @param projectRoot - FireForge project root for shim resolution
455
463
  */
456
- export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot, rootScope) {
464
+ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot, rootScope, resolvableModules) {
457
465
  const merged = { byFile: new Map(), global: [] };
458
466
  if (testFiles.size === 0)
459
467
  return merged;
@@ -484,7 +492,9 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
484
492
  }
485
493
  const result = await runCheckJsGrouped({
486
494
  repoDir,
487
- resolutionOwned: roots,
495
+ resolutionOwned: new Set([...roots, ...(resolvableModules?.(file) ?? [])]),
496
+ rootScope: roots,
497
+ reportOnly: new Set([file]),
488
498
  ...(patchLint.checkJsTestShim !== undefined
489
499
  ? { extraShimPath: patchLint.checkJsTestShim }
490
500
  : {}),
@@ -10,7 +10,7 @@
10
10
  * module.
11
11
  */
12
12
  import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
13
- import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
13
+ import type { FireForgeConfig, PatchLintTokenScale, PatchPolicyConfig } from '../types/config.js';
14
14
  import type { ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
15
15
  /**
16
16
  * One patch's contribution to {@link PatchQueueContext}.
@@ -82,6 +82,8 @@ export interface PatchQueueContext {
82
82
  * `patchLint.forwardRegistration`. Absent = `support-files` only.
83
83
  */
84
84
  forwardRegistration?: ForwardRegistrationScope;
85
+ /** `patchLint.tokenScales`, for the scales' `minFiles` floors. */
86
+ tokenScales?: PatchLintTokenScale[];
85
87
  }
86
88
  /**
87
89
  * Builds a {@link PatchQueueContext} by reading every .patch file in the
@@ -21,6 +21,7 @@ import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-dif
21
21
  import { lintPatchQueueForwardRegistrations } from './patch-lint-forward-registration.js';
22
22
  import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
23
23
  import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, STAGED_DEPENDENCY_REASON_PLACEHOLDER, stagedDependencyUnusedFingerprint, } from './patch-lint-staged-registration.js';
24
+ import { lintTokenScaleFloors } from './patch-lint-token-scale.js';
24
25
  import { loadPatchesManifest } from './patch-manifest-io.js';
25
26
  import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
26
27
  import { extractNewFileContentsFromDiff } from './patch-transform.js';
@@ -88,6 +89,9 @@ export async function buildPatchQueueContext(patchesDir, config) {
88
89
  ...(config?.patchLint?.forwardRegistration !== undefined
89
90
  ? { forwardRegistration: config.patchLint.forwardRegistration }
90
91
  : {}),
92
+ ...(config?.patchLint?.tokenScales !== undefined
93
+ ? { tokenScales: config.patchLint.tokenScales }
94
+ : {}),
91
95
  };
92
96
  }
93
97
  // The creators map (split out of `lintPatchQueueDuplicateCreations` so
@@ -581,5 +585,6 @@ export function lintPatchQueue(ctx) {
581
585
  ...lintPatchQueueModuleRegistrations(ctx),
582
586
  ...lintPatchQueueForwardRegistrations(ctx),
583
587
  ...lintPatchQueueBinaryBodies(ctx),
588
+ ...lintTokenScaleFloors(ctx.entries.flatMap((entry) => [...entry.newFiles]), ctx.tokenScales ?? []),
584
589
  ];
585
590
  }