lootforge 0.3.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 (243) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +764 -0
  3. package/bin/lootforge.js +28 -0
  4. package/dist/benchmarks/coarseToFineCost.d.ts +21 -0
  5. package/dist/benchmarks/coarseToFineCost.js +49 -0
  6. package/dist/benchmarks/coarseToFineCost.js.map +1 -0
  7. package/dist/checks/boundaryMetrics.d.ts +12 -0
  8. package/dist/checks/boundaryMetrics.js +102 -0
  9. package/dist/checks/boundaryMetrics.js.map +1 -0
  10. package/dist/checks/candidateScore.d.ts +11 -0
  11. package/dist/checks/candidateScore.js +462 -0
  12. package/dist/checks/candidateScore.js.map +1 -0
  13. package/dist/checks/commandParser.d.ts +5 -0
  14. package/dist/checks/commandParser.js +99 -0
  15. package/dist/checks/commandParser.js.map +1 -0
  16. package/dist/checks/consistencyOutliers.d.ts +42 -0
  17. package/dist/checks/consistencyOutliers.js +156 -0
  18. package/dist/checks/consistencyOutliers.js.map +1 -0
  19. package/dist/checks/imageAcceptance.d.ts +67 -0
  20. package/dist/checks/imageAcceptance.js +967 -0
  21. package/dist/checks/imageAcceptance.js.map +1 -0
  22. package/dist/checks/packInvariants.d.ts +56 -0
  23. package/dist/checks/packInvariants.js +1064 -0
  24. package/dist/checks/packInvariants.js.map +1 -0
  25. package/dist/checks/softAdapters.d.ts +25 -0
  26. package/dist/checks/softAdapters.js +275 -0
  27. package/dist/checks/softAdapters.js.map +1 -0
  28. package/dist/checks/vlmGate.d.ts +8 -0
  29. package/dist/checks/vlmGate.js +200 -0
  30. package/dist/checks/vlmGate.js.map +1 -0
  31. package/dist/cli/commands/atlas.d.ts +5 -0
  32. package/dist/cli/commands/atlas.js +18 -0
  33. package/dist/cli/commands/atlas.js.map +1 -0
  34. package/dist/cli/commands/eval.d.ts +6 -0
  35. package/dist/cli/commands/eval.js +23 -0
  36. package/dist/cli/commands/eval.js.map +1 -0
  37. package/dist/cli/commands/generate.d.ts +18 -0
  38. package/dist/cli/commands/generate.js +66 -0
  39. package/dist/cli/commands/generate.js.map +1 -0
  40. package/dist/cli/commands/init.d.ts +15 -0
  41. package/dist/cli/commands/init.js +146 -0
  42. package/dist/cli/commands/init.js.map +1 -0
  43. package/dist/cli/commands/package.d.ts +6 -0
  44. package/dist/cli/commands/package.js +27 -0
  45. package/dist/cli/commands/package.js.map +1 -0
  46. package/dist/cli/commands/plan.d.ts +16 -0
  47. package/dist/cli/commands/plan.js +49 -0
  48. package/dist/cli/commands/plan.js.map +1 -0
  49. package/dist/cli/commands/process.d.ts +14 -0
  50. package/dist/cli/commands/process.js +29 -0
  51. package/dist/cli/commands/process.js.map +1 -0
  52. package/dist/cli/commands/regenerate.d.ts +29 -0
  53. package/dist/cli/commands/regenerate.js +244 -0
  54. package/dist/cli/commands/regenerate.js.map +1 -0
  55. package/dist/cli/commands/review.d.ts +5 -0
  56. package/dist/cli/commands/review.js +18 -0
  57. package/dist/cli/commands/review.js.map +1 -0
  58. package/dist/cli/commands/select.d.ts +6 -0
  59. package/dist/cli/commands/select.js +21 -0
  60. package/dist/cli/commands/select.js.map +1 -0
  61. package/dist/cli/commands/serve.d.ts +16 -0
  62. package/dist/cli/commands/serve.js +100 -0
  63. package/dist/cli/commands/serve.js.map +1 -0
  64. package/dist/cli/commands/validate.d.ts +17 -0
  65. package/dist/cli/commands/validate.js +108 -0
  66. package/dist/cli/commands/validate.js.map +1 -0
  67. package/dist/cli/index.d.ts +1 -0
  68. package/dist/cli/index.js +157 -0
  69. package/dist/cli/index.js.map +1 -0
  70. package/dist/cli/parseArgs.d.ts +3 -0
  71. package/dist/cli/parseArgs.js +37 -0
  72. package/dist/cli/parseArgs.js.map +1 -0
  73. package/dist/contracts/stageArtifacts.d.ts +4031 -0
  74. package/dist/contracts/stageArtifacts.js +663 -0
  75. package/dist/contracts/stageArtifacts.js.map +1 -0
  76. package/dist/manifest/load.d.ts +3 -0
  77. package/dist/manifest/load.js +50 -0
  78. package/dist/manifest/load.js.map +1 -0
  79. package/dist/manifest/normalize-palette.d.ts +17 -0
  80. package/dist/manifest/normalize-palette.js +235 -0
  81. package/dist/manifest/normalize-palette.js.map +1 -0
  82. package/dist/manifest/normalize-policy.d.ts +48 -0
  83. package/dist/manifest/normalize-policy.js +239 -0
  84. package/dist/manifest/normalize-policy.js.map +1 -0
  85. package/dist/manifest/normalize-prompt.d.ts +14 -0
  86. package/dist/manifest/normalize-prompt.js +73 -0
  87. package/dist/manifest/normalize-prompt.js.map +1 -0
  88. package/dist/manifest/normalize-target.d.ts +49 -0
  89. package/dist/manifest/normalize-target.js +542 -0
  90. package/dist/manifest/normalize-target.js.map +1 -0
  91. package/dist/manifest/schema.d.ts +7570 -0
  92. package/dist/manifest/schema.js +373 -0
  93. package/dist/manifest/schema.js.map +1 -0
  94. package/dist/manifest/semantic-validation.d.ts +4 -0
  95. package/dist/manifest/semantic-validation.js +526 -0
  96. package/dist/manifest/semantic-validation.js.map +1 -0
  97. package/dist/manifest/types.d.ts +263 -0
  98. package/dist/manifest/types.js +2 -0
  99. package/dist/manifest/types.js.map +1 -0
  100. package/dist/manifest/validate.d.ts +12 -0
  101. package/dist/manifest/validate.js +221 -0
  102. package/dist/manifest/validate.js.map +1 -0
  103. package/dist/output/assetPackManifest.d.ts +19 -0
  104. package/dist/output/assetPackManifest.js +20 -0
  105. package/dist/output/assetPackManifest.js.map +1 -0
  106. package/dist/output/catalog.d.ts +60 -0
  107. package/dist/output/catalog.js +107 -0
  108. package/dist/output/catalog.js.map +1 -0
  109. package/dist/output/contactSheet.d.ts +13 -0
  110. package/dist/output/contactSheet.js +124 -0
  111. package/dist/output/contactSheet.js.map +1 -0
  112. package/dist/output/phaserManifest.d.ts +8 -0
  113. package/dist/output/phaserManifest.js +25 -0
  114. package/dist/output/phaserManifest.js.map +1 -0
  115. package/dist/output/pixiManifest.d.ts +8 -0
  116. package/dist/output/pixiManifest.js +37 -0
  117. package/dist/output/pixiManifest.js.map +1 -0
  118. package/dist/output/provenance.d.ts +121 -0
  119. package/dist/output/provenance.js +10 -0
  120. package/dist/output/provenance.js.map +1 -0
  121. package/dist/output/runtimeManifests.d.ts +21 -0
  122. package/dist/output/runtimeManifests.js +82 -0
  123. package/dist/output/runtimeManifests.js.map +1 -0
  124. package/dist/output/unityImportManifest.d.ts +10 -0
  125. package/dist/output/unityImportManifest.js +58 -0
  126. package/dist/output/unityImportManifest.js.map +1 -0
  127. package/dist/output/zip.d.ts +5 -0
  128. package/dist/output/zip.js +68 -0
  129. package/dist/output/zip.js.map +1 -0
  130. package/dist/pipeline/atlas.d.ts +33 -0
  131. package/dist/pipeline/atlas.js +286 -0
  132. package/dist/pipeline/atlas.js.map +1 -0
  133. package/dist/pipeline/eval.d.ts +104 -0
  134. package/dist/pipeline/eval.js +246 -0
  135. package/dist/pipeline/eval.js.map +1 -0
  136. package/dist/pipeline/generate.d.ts +44 -0
  137. package/dist/pipeline/generate.js +1088 -0
  138. package/dist/pipeline/generate.js.map +1 -0
  139. package/dist/pipeline/package.d.ts +18 -0
  140. package/dist/pipeline/package.js +218 -0
  141. package/dist/pipeline/package.js.map +1 -0
  142. package/dist/pipeline/process.d.ts +15 -0
  143. package/dist/pipeline/process.js +776 -0
  144. package/dist/pipeline/process.js.map +1 -0
  145. package/dist/pipeline/review.d.ts +10 -0
  146. package/dist/pipeline/review.js +341 -0
  147. package/dist/pipeline/review.js.map +1 -0
  148. package/dist/pipeline/seamHeal.d.ts +2 -0
  149. package/dist/pipeline/seamHeal.js +70 -0
  150. package/dist/pipeline/seamHeal.js.map +1 -0
  151. package/dist/pipeline/select.d.ts +39 -0
  152. package/dist/pipeline/select.js +79 -0
  153. package/dist/pipeline/select.js.map +1 -0
  154. package/dist/providers/job.d.ts +29 -0
  155. package/dist/providers/job.js +113 -0
  156. package/dist/providers/job.js.map +1 -0
  157. package/dist/providers/localDiffusion.d.ts +28 -0
  158. package/dist/providers/localDiffusion.js +235 -0
  159. package/dist/providers/localDiffusion.js.map +1 -0
  160. package/dist/providers/nano.d.ts +36 -0
  161. package/dist/providers/nano.js +402 -0
  162. package/dist/providers/nano.js.map +1 -0
  163. package/dist/providers/openai.d.ts +37 -0
  164. package/dist/providers/openai.js +378 -0
  165. package/dist/providers/openai.js.map +1 -0
  166. package/dist/providers/policy.d.ts +9 -0
  167. package/dist/providers/policy.js +192 -0
  168. package/dist/providers/policy.js.map +1 -0
  169. package/dist/providers/prompt.d.ts +3 -0
  170. package/dist/providers/prompt.js +63 -0
  171. package/dist/providers/prompt.js.map +1 -0
  172. package/dist/providers/registry.d.ts +24 -0
  173. package/dist/providers/registry.js +92 -0
  174. package/dist/providers/registry.js.map +1 -0
  175. package/dist/providers/runtime.d.ts +15 -0
  176. package/dist/providers/runtime.js +101 -0
  177. package/dist/providers/runtime.js.map +1 -0
  178. package/dist/providers/runtimeConfig.d.ts +20 -0
  179. package/dist/providers/runtimeConfig.js +146 -0
  180. package/dist/providers/runtimeConfig.js.map +1 -0
  181. package/dist/providers/types-core.d.ts +514 -0
  182. package/dist/providers/types-core.js +60 -0
  183. package/dist/providers/types-core.js.map +1 -0
  184. package/dist/providers/types.d.ts +4 -0
  185. package/dist/providers/types.js +5 -0
  186. package/dist/providers/types.js.map +1 -0
  187. package/dist/service/generationRequest.d.ts +58 -0
  188. package/dist/service/generationRequest.js +203 -0
  189. package/dist/service/generationRequest.js.map +1 -0
  190. package/dist/service/providerCapabilities.d.ts +40 -0
  191. package/dist/service/providerCapabilities.js +114 -0
  192. package/dist/service/providerCapabilities.js.map +1 -0
  193. package/dist/service/server.d.ts +31 -0
  194. package/dist/service/server.js +774 -0
  195. package/dist/service/server.js.map +1 -0
  196. package/dist/shared/errors.d.ts +13 -0
  197. package/dist/shared/errors.js +24 -0
  198. package/dist/shared/errors.js.map +1 -0
  199. package/dist/shared/fs.d.ts +6 -0
  200. package/dist/shared/fs.js +30 -0
  201. package/dist/shared/fs.js.map +1 -0
  202. package/dist/shared/image.d.ts +25 -0
  203. package/dist/shared/image.js +136 -0
  204. package/dist/shared/image.js.map +1 -0
  205. package/dist/shared/paths.d.ts +30 -0
  206. package/dist/shared/paths.js +103 -0
  207. package/dist/shared/paths.js.map +1 -0
  208. package/dist/shared/schemas.d.ts +209 -0
  209. package/dist/shared/schemas.js +93 -0
  210. package/dist/shared/schemas.js.map +1 -0
  211. package/dist/shared/typeGuards.d.ts +1 -0
  212. package/dist/shared/typeGuards.js +4 -0
  213. package/dist/shared/typeGuards.js.map +1 -0
  214. package/dist/shared/zod.d.ts +1 -0
  215. package/dist/shared/zod.js +14 -0
  216. package/dist/shared/zod.js.map +1 -0
  217. package/dist/showcase/format.d.ts +9 -0
  218. package/dist/showcase/format.js +61 -0
  219. package/dist/showcase/format.js.map +1 -0
  220. package/dist/showcase/panelRenderer.d.ts +59 -0
  221. package/dist/showcase/panelRenderer.js +294 -0
  222. package/dist/showcase/panelRenderer.js.map +1 -0
  223. package/dist/showcase/releaseConfig.d.ts +233 -0
  224. package/dist/showcase/releaseConfig.js +75 -0
  225. package/dist/showcase/releaseConfig.js.map +1 -0
  226. package/dist/showcase/releaseEvidence.d.ts +25 -0
  227. package/dist/showcase/releaseEvidence.js +540 -0
  228. package/dist/showcase/releaseEvidence.js.map +1 -0
  229. package/dist/showcase/releaseEvidenceSchema.d.ts +1611 -0
  230. package/dist/showcase/releaseEvidenceSchema.js +165 -0
  231. package/dist/showcase/releaseEvidenceSchema.js.map +1 -0
  232. package/dist/showcase/scenarioRenderer.d.ts +19 -0
  233. package/dist/showcase/scenarioRenderer.js +488 -0
  234. package/dist/showcase/scenarioRenderer.js.map +1 -0
  235. package/docs/ADAPTER_CONTRACT.md +141 -0
  236. package/docs/ENGINE_TARGETING.md +86 -0
  237. package/docs/MANIFEST_POLICY_COVERAGE.md +130 -0
  238. package/docs/RELEASE_WORKFLOW.md +117 -0
  239. package/docs/ROADMAP.md +411 -0
  240. package/docs/ROADMAP_ISSUES.md +244 -0
  241. package/docs/SERVICE_MODE.md +137 -0
  242. package/docs/manifest-schema.md +254 -0
  243. package/package.json +70 -0
@@ -0,0 +1,776 @@
1
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
2
+ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { cpus } from "node:os";
4
+ import path from "node:path";
5
+ import sharp from "sharp";
6
+ import { applySeamHeal } from "./seamHeal.js";
7
+ import { runImageAcceptanceChecks, assertImageAcceptanceReport, } from "../checks/imageAcceptance.js";
8
+ import { buildCatalog } from "../output/catalog.js";
9
+ import { getTargetPostProcessPolicy } from "../providers/types.js";
10
+ import { writeJsonFile } from "../shared/fs.js";
11
+ import { openImage } from "../shared/image.js";
12
+ import { normalizeTargetOutPath, resolvePathWithinDir, resolveStagePathLayout, } from "../shared/paths.js";
13
+ function parseTargetsIndex(raw, filePath) {
14
+ try {
15
+ return JSON.parse(raw);
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Failed to parse targets index JSON (${filePath}): ${error instanceof Error ? error.message : String(error)}`);
19
+ }
20
+ }
21
+ function kernelForAlgorithm(algorithm) {
22
+ if (algorithm === "nearest") {
23
+ return "nearest";
24
+ }
25
+ return "lanczos3";
26
+ }
27
+ function parseColorHex(input) {
28
+ const fallback = { r: 0, g: 0, b: 0 };
29
+ if (!input)
30
+ return fallback;
31
+ const value = input.trim().toLowerCase();
32
+ const raw = value.startsWith("#") ? value.slice(1) : value;
33
+ if (!/^[0-9a-f]{6}$/i.test(raw)) {
34
+ return fallback;
35
+ }
36
+ return {
37
+ r: Number.parseInt(raw.slice(0, 2), 16),
38
+ g: Number.parseInt(raw.slice(2, 4), 16),
39
+ b: Number.parseInt(raw.slice(4, 6), 16),
40
+ };
41
+ }
42
+ async function applyOutline(imageBuffer, size, colorHex) {
43
+ const metadata = await sharp(imageBuffer).metadata();
44
+ const width = metadata.width;
45
+ const height = metadata.height;
46
+ if (!width || !height || size <= 0) {
47
+ return imageBuffer;
48
+ }
49
+ const alphaMask = await sharp(imageBuffer)
50
+ .ensureAlpha()
51
+ .extractChannel("alpha")
52
+ .threshold(1)
53
+ .dilate(size)
54
+ .toBuffer();
55
+ const color = parseColorHex(colorHex);
56
+ const outlineLayer = await sharp({
57
+ create: {
58
+ width,
59
+ height,
60
+ channels: 3,
61
+ background: color,
62
+ },
63
+ })
64
+ .joinChannel(alphaMask)
65
+ .png()
66
+ .toBuffer();
67
+ return sharp(outlineLayer)
68
+ .composite([
69
+ {
70
+ input: imageBuffer,
71
+ },
72
+ ])
73
+ .png()
74
+ .toBuffer();
75
+ }
76
+ function withVariantSuffix(filePath, variantName) {
77
+ const ext = path.extname(filePath);
78
+ const base = filePath.slice(0, filePath.length - ext.length);
79
+ const safeName = variantName
80
+ .trim()
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9_-]+/g, "-");
83
+ return `${base}__${safeName}${ext}`;
84
+ }
85
+ async function writeDerivedVariant(params) {
86
+ const encoded = await openImage(params.buffer, "pipeline")
87
+ .png({ compressionLevel: 9 })
88
+ .toBuffer();
89
+ await mkdir(path.dirname(params.processedPath), { recursive: true });
90
+ await writeFile(params.processedPath, encoded);
91
+ if (params.mirrorLegacy) {
92
+ await mkdir(path.dirname(params.legacyPath), { recursive: true });
93
+ await writeFile(params.legacyPath, encoded);
94
+ }
95
+ }
96
+ function clamp(value, min, max) {
97
+ return Math.max(min, Math.min(max, value));
98
+ }
99
+ async function smartCropImage(imageBuffer, options) {
100
+ const mode = options.mode ?? "alpha-bounds";
101
+ const padding = clamp(Math.round(options.padding ?? 0), 0, 256);
102
+ const metadata = await openImage(imageBuffer, "pipeline").metadata();
103
+ const width = metadata.width;
104
+ const height = metadata.height;
105
+ if (width <= 0 || height <= 0) {
106
+ return imageBuffer;
107
+ }
108
+ let cropLeft = 0;
109
+ let cropTop = 0;
110
+ let cropWidth = width;
111
+ let cropHeight = height;
112
+ if (mode === "center") {
113
+ const targetAspect = options.targetAspect;
114
+ if (typeof targetAspect === "number" && Number.isFinite(targetAspect) && targetAspect > 0) {
115
+ const currentAspect = width / height;
116
+ if (currentAspect > targetAspect) {
117
+ cropWidth = Math.max(1, Math.round(height * targetAspect));
118
+ cropHeight = height;
119
+ cropLeft = Math.floor((width - cropWidth) / 2);
120
+ cropTop = 0;
121
+ }
122
+ else if (currentAspect < targetAspect) {
123
+ cropWidth = width;
124
+ cropHeight = Math.max(1, Math.round(width / targetAspect));
125
+ cropLeft = 0;
126
+ cropTop = Math.floor((height - cropHeight) / 2);
127
+ }
128
+ else {
129
+ return imageBuffer;
130
+ }
131
+ }
132
+ else {
133
+ return imageBuffer;
134
+ }
135
+ }
136
+ else {
137
+ const rawResult = await openImage(imageBuffer, "pipeline")
138
+ .ensureAlpha()
139
+ .raw()
140
+ .toBuffer({ resolveWithObject: true });
141
+ const data = rawResult.data;
142
+ const channels = rawResult.info.channels;
143
+ const rawWidth = rawResult.info.width;
144
+ const rawHeight = rawResult.info.height;
145
+ let minX = rawWidth;
146
+ let minY = rawHeight;
147
+ let maxX = -1;
148
+ let maxY = -1;
149
+ for (let y = 0; y < rawHeight; y += 1) {
150
+ for (let x = 0; x < rawWidth; x += 1) {
151
+ const index = (y * rawWidth + x) * channels;
152
+ const alpha = channels >= 4 ? data[index + 3] : 255;
153
+ if (alpha === 0) {
154
+ continue;
155
+ }
156
+ minX = Math.min(minX, x);
157
+ minY = Math.min(minY, y);
158
+ maxX = Math.max(maxX, x);
159
+ maxY = Math.max(maxY, y);
160
+ }
161
+ }
162
+ if (maxX < minX || maxY < minY) {
163
+ return imageBuffer;
164
+ }
165
+ cropLeft = clamp(minX - padding, 0, rawWidth - 1);
166
+ cropTop = clamp(minY - padding, 0, rawHeight - 1);
167
+ const right = clamp(maxX + padding, 0, rawWidth - 1);
168
+ const bottom = clamp(maxY + padding, 0, rawHeight - 1);
169
+ cropWidth = Math.max(1, right - cropLeft + 1);
170
+ cropHeight = Math.max(1, bottom - cropTop + 1);
171
+ }
172
+ if (cropLeft === 0 && cropTop === 0 && cropWidth === width && cropHeight === height) {
173
+ return imageBuffer;
174
+ }
175
+ return openImage(imageBuffer, "pipeline")
176
+ .extract({
177
+ left: cropLeft,
178
+ top: cropTop,
179
+ width: cropWidth,
180
+ height: cropHeight,
181
+ })
182
+ .png()
183
+ .toBuffer();
184
+ }
185
+ function parsePaletteColors(colors) {
186
+ if (!colors || colors.length === 0) {
187
+ return [];
188
+ }
189
+ return colors
190
+ .map((input) => {
191
+ const value = input.trim().toLowerCase();
192
+ const raw = value.startsWith("#") ? value.slice(1) : value;
193
+ if (!/^[0-9a-f]{6}$/i.test(raw)) {
194
+ return null;
195
+ }
196
+ return {
197
+ r: Number.parseInt(raw.slice(0, 2), 16),
198
+ g: Number.parseInt(raw.slice(2, 4), 16),
199
+ b: Number.parseInt(raw.slice(4, 6), 16),
200
+ };
201
+ })
202
+ .filter((color) => color !== null);
203
+ }
204
+ function nearestPaletteColor(source, palette) {
205
+ let best = palette[0];
206
+ let bestDistance = Number.POSITIVE_INFINITY;
207
+ for (const candidate of palette) {
208
+ const dr = source.r - candidate.r;
209
+ const dg = source.g - candidate.g;
210
+ const db = source.b - candidate.b;
211
+ const distance = dr * dr + dg * dg + db * db;
212
+ if (distance < bestDistance) {
213
+ bestDistance = distance;
214
+ best = candidate;
215
+ }
216
+ }
217
+ return best;
218
+ }
219
+ function packRgb(color) {
220
+ return ((color.r << 16) | (color.g << 8) | color.b) >>> 0;
221
+ }
222
+ function computeExactPaletteCompliance(params) {
223
+ if (params.allowedColors.size === 0) {
224
+ return { compliance: 0, counted: 0, matches: 0 };
225
+ }
226
+ let counted = 0;
227
+ let matches = 0;
228
+ for (let i = 0; i < params.raw.length; i += params.channels) {
229
+ const alpha = params.channels >= 4 ? params.raw[i + 3] : 255;
230
+ if (alpha === 0) {
231
+ continue;
232
+ }
233
+ counted += 1;
234
+ const packed = ((params.raw[i] << 16) | (params.raw[i + 1] << 8) | params.raw[i + 2]) >>> 0;
235
+ if (params.allowedColors.has(packed)) {
236
+ matches += 1;
237
+ }
238
+ }
239
+ if (counted === 0) {
240
+ return { compliance: 1, counted: 0, matches: 0 };
241
+ }
242
+ return {
243
+ compliance: matches / counted,
244
+ counted,
245
+ matches,
246
+ };
247
+ }
248
+ async function quantizeToExactPalette(imageBuffer, paletteColors) {
249
+ const palette = parsePaletteColors(paletteColors);
250
+ if (palette.length === 0) {
251
+ return imageBuffer;
252
+ }
253
+ const rawResult = await sharp(imageBuffer)
254
+ .ensureAlpha()
255
+ .raw()
256
+ .toBuffer({ resolveWithObject: true });
257
+ const data = rawResult.data;
258
+ const channels = rawResult.info.channels;
259
+ const width = rawResult.info.width;
260
+ const height = rawResult.info.height;
261
+ for (let i = 0; i < data.length; i += channels) {
262
+ const alpha = channels >= 4 ? data[i + 3] : 255;
263
+ if (alpha === 0) {
264
+ // Avoid transparent fringe colors by forcing fully transparent RGB to black.
265
+ data[i] = 0;
266
+ data[i + 1] = 0;
267
+ data[i + 2] = 0;
268
+ continue;
269
+ }
270
+ const nearest = nearestPaletteColor({ r: data[i], g: data[i + 1], b: data[i + 2] }, palette);
271
+ data[i] = nearest.r;
272
+ data[i + 1] = nearest.g;
273
+ data[i + 2] = nearest.b;
274
+ }
275
+ return sharp(data, { raw: { width, height, channels } }).png().toBuffer();
276
+ }
277
+ async function processSingleTarget(params) {
278
+ const postProcess = getTargetPostProcessPolicy(params.target);
279
+ const emitVariants = postProcess.operations?.emitVariants;
280
+ const pixelPerfect = postProcess.operations?.pixelPerfect;
281
+ const pixelPerfectEnabled = pixelPerfect ? pixelPerfect.enabled !== false : false;
282
+ let variantCount = 0;
283
+ let outputBuffer = await openImage(params.rawImagePath, "pipeline")
284
+ .ensureAlpha()
285
+ .png()
286
+ .toBuffer();
287
+ if (emitVariants?.raw === true) {
288
+ await writeDerivedVariant({
289
+ buffer: outputBuffer,
290
+ processedPath: withVariantSuffix(params.processedImagePath, "raw"),
291
+ legacyPath: withVariantSuffix(params.legacyImagePath, "raw"),
292
+ mirrorLegacy: params.mirrorLegacy,
293
+ });
294
+ variantCount += 1;
295
+ }
296
+ const trimConfig = postProcess.operations?.trim;
297
+ if (trimConfig?.enabled === true || trimConfig?.threshold !== undefined) {
298
+ outputBuffer = await openImage(outputBuffer, "pipeline")
299
+ .trim({ threshold: trimConfig.threshold ?? 10 })
300
+ .png()
301
+ .toBuffer();
302
+ }
303
+ const padConfig = postProcess.operations?.pad;
304
+ if (padConfig && padConfig.pixels > 0) {
305
+ const background = parseColorHex(padConfig.background);
306
+ outputBuffer = await openImage(outputBuffer, "pipeline")
307
+ .extend({
308
+ top: padConfig.pixels,
309
+ bottom: padConfig.pixels,
310
+ left: padConfig.pixels,
311
+ right: padConfig.pixels,
312
+ background: {
313
+ r: background.r,
314
+ g: background.g,
315
+ b: background.b,
316
+ alpha: params.target.acceptance?.alpha === true ? 0 : 1,
317
+ },
318
+ })
319
+ .png()
320
+ .toBuffer();
321
+ }
322
+ const smartCrop = postProcess.operations?.smartCrop;
323
+ if (smartCrop && smartCrop.enabled !== false) {
324
+ const targetAspect = postProcess.resizeTo && postProcess.resizeTo.height > 0
325
+ ? postProcess.resizeTo.width / postProcess.resizeTo.height
326
+ : undefined;
327
+ outputBuffer = await smartCropImage(outputBuffer, {
328
+ mode: smartCrop.mode,
329
+ padding: smartCrop.padding,
330
+ targetAspect,
331
+ });
332
+ }
333
+ if (postProcess.resizeTo) {
334
+ outputBuffer = await openImage(outputBuffer, "pipeline")
335
+ .resize(postProcess.resizeTo.width, postProcess.resizeTo.height, {
336
+ fit: "contain",
337
+ background: params.target.acceptance?.alpha === true ||
338
+ params.target.runtimeSpec?.alphaRequired === true
339
+ ? { r: 0, g: 0, b: 0, alpha: 0 }
340
+ : { r: 0, g: 0, b: 0, alpha: 1 },
341
+ kernel: pixelPerfectEnabled
342
+ ? kernelForAlgorithm("nearest")
343
+ : kernelForAlgorithm(postProcess.algorithm),
344
+ withoutEnlargement: true,
345
+ })
346
+ .png()
347
+ .toBuffer();
348
+ }
349
+ if (pixelPerfectEnabled &&
350
+ !postProcess.resizeTo &&
351
+ typeof pixelPerfect?.scale === "number" &&
352
+ pixelPerfect.scale > 1) {
353
+ const metadata = await openImage(outputBuffer, "pipeline").metadata();
354
+ if (metadata.width && metadata.height) {
355
+ outputBuffer = await openImage(outputBuffer, "pipeline")
356
+ .resize(metadata.width * pixelPerfect.scale, metadata.height * pixelPerfect.scale, {
357
+ fit: "fill",
358
+ kernel: kernelForAlgorithm("nearest"),
359
+ })
360
+ .png()
361
+ .toBuffer();
362
+ }
363
+ }
364
+ if (emitVariants?.styleRef === true) {
365
+ await writeDerivedVariant({
366
+ buffer: outputBuffer,
367
+ processedPath: withVariantSuffix(params.processedImagePath, "style_ref"),
368
+ legacyPath: withVariantSuffix(params.legacyImagePath, "style_ref"),
369
+ mirrorLegacy: params.mirrorLegacy,
370
+ });
371
+ variantCount += 1;
372
+ }
373
+ const outlineConfig = postProcess.operations?.outline;
374
+ if (outlineConfig?.size) {
375
+ outputBuffer = await applyOutline(outputBuffer, outlineConfig.size, outlineConfig.color);
376
+ }
377
+ outputBuffer = await applySeamHeal(outputBuffer, params.target);
378
+ const quantizeConfig = postProcess.operations?.quantize;
379
+ const paletteColors = quantizeConfig?.colors ?? postProcess.pngPaletteColors;
380
+ if (params.target.palette?.mode === "exact" && params.target.palette.colors?.length) {
381
+ outputBuffer = await quantizeToExactPalette(outputBuffer, params.target.palette.colors);
382
+ }
383
+ if (postProcess.stripMetadata === false) {
384
+ // Intentionally no-op: sharp preserves no metadata by default, and this pipeline
385
+ // stays deterministic by avoiding metadata passthrough.
386
+ }
387
+ const usePaletteQuantization = typeof paletteColors === "number";
388
+ const encodedMain = await sharp(outputBuffer)
389
+ .png({
390
+ compressionLevel: 9,
391
+ palette: usePaletteQuantization,
392
+ colours: paletteColors,
393
+ dither: quantizeConfig?.dither,
394
+ effort: 8,
395
+ })
396
+ .toBuffer();
397
+ let mainRaw;
398
+ let mainRawInfo;
399
+ if (usePaletteQuantization) {
400
+ // Palette quantization changes pixel colors during PNG encode, so we must
401
+ // decode back to get the quantized raw pixel data for variant generation.
402
+ const decodedMain = await openImage(encodedMain, "pipeline")
403
+ .ensureAlpha()
404
+ .raw()
405
+ .toBuffer({ resolveWithObject: true });
406
+ mainRaw = decodedMain.data;
407
+ mainRawInfo = decodedMain.info;
408
+ }
409
+ else {
410
+ // No quantization: decode raw pixels directly from the pre-encode buffer,
411
+ // avoiding a wasteful decode of the compressed PNG.
412
+ const rawResult = await openImage(outputBuffer, "pipeline")
413
+ .ensureAlpha()
414
+ .raw()
415
+ .toBuffer({ resolveWithObject: true });
416
+ mainRaw = rawResult.data;
417
+ mainRawInfo = rawResult.info;
418
+ }
419
+ if (params.target.palette?.mode === "exact" && params.target.palette.strict === true) {
420
+ const allowed = new Set(parsePaletteColors(params.target.palette.colors).map((color) => packRgb(color)));
421
+ if (allowed.size === 0) {
422
+ throw new Error(`Strict exact palette enforcement for target "${params.target.id}" requires at least one valid palette color.`);
423
+ }
424
+ const compliance = computeExactPaletteCompliance({
425
+ raw: mainRaw,
426
+ channels: mainRawInfo.channels,
427
+ allowedColors: allowed,
428
+ });
429
+ if (compliance.compliance < 1) {
430
+ const nonCompliant = compliance.counted - compliance.matches;
431
+ throw new Error(`Strict exact palette enforcement failed for target "${params.target.id}": ${nonCompliant}/${compliance.counted} visible pixels fall outside the configured palette.`);
432
+ }
433
+ }
434
+ await mkdir(path.dirname(params.processedImagePath), { recursive: true });
435
+ await writeFile(params.processedImagePath, encodedMain);
436
+ if (params.mirrorLegacy) {
437
+ await mkdir(path.dirname(params.legacyImagePath), { recursive: true });
438
+ await cp(params.processedImagePath, params.legacyImagePath, { force: true });
439
+ }
440
+ if (emitVariants?.pixel === true) {
441
+ await mkdir(path.dirname(withVariantSuffix(params.processedImagePath, "pixel")), {
442
+ recursive: true,
443
+ });
444
+ await writeFile(withVariantSuffix(params.processedImagePath, "pixel"), encodedMain);
445
+ if (params.mirrorLegacy) {
446
+ await mkdir(path.dirname(withVariantSuffix(params.legacyImagePath, "pixel")), {
447
+ recursive: true,
448
+ });
449
+ await writeFile(withVariantSuffix(params.legacyImagePath, "pixel"), encodedMain);
450
+ }
451
+ variantCount += 1;
452
+ }
453
+ if (emitVariants?.layerColor === true) {
454
+ const layerColorRaw = Buffer.from(mainRaw);
455
+ for (let index = 0; index < layerColorRaw.length; index += mainRawInfo.channels) {
456
+ const alpha = mainRawInfo.channels >= 4 ? layerColorRaw[index + 3] : 255;
457
+ if (alpha === 0) {
458
+ layerColorRaw[index] = 0;
459
+ layerColorRaw[index + 1] = 0;
460
+ layerColorRaw[index + 2] = 0;
461
+ }
462
+ if (mainRawInfo.channels >= 4) {
463
+ layerColorRaw[index + 3] = 255;
464
+ }
465
+ }
466
+ const layerColorBuffer = await sharp(layerColorRaw, { raw: mainRawInfo }).png().toBuffer();
467
+ await writeDerivedVariant({
468
+ buffer: layerColorBuffer,
469
+ processedPath: withVariantSuffix(params.processedImagePath, "layer_color"),
470
+ legacyPath: withVariantSuffix(params.legacyImagePath, "layer_color"),
471
+ mirrorLegacy: params.mirrorLegacy,
472
+ });
473
+ variantCount += 1;
474
+ }
475
+ if (emitVariants?.layerMatte === true) {
476
+ const layerMatteBuffer = await sharp(mainRaw, { raw: mainRawInfo })
477
+ .extractChannel(mainRawInfo.channels >= 4 ? 3 : 0)
478
+ .png()
479
+ .toBuffer();
480
+ await writeDerivedVariant({
481
+ buffer: layerMatteBuffer,
482
+ processedPath: withVariantSuffix(params.processedImagePath, "layer_matte"),
483
+ legacyPath: withVariantSuffix(params.legacyImagePath, "layer_matte"),
484
+ mirrorLegacy: params.mirrorLegacy,
485
+ });
486
+ variantCount += 1;
487
+ }
488
+ const variants = postProcess.operations?.resizeVariants?.variants ?? [];
489
+ for (const variant of variants) {
490
+ const variantPath = withVariantSuffix(params.processedImagePath, variant.name);
491
+ const variantLegacyPath = withVariantSuffix(params.legacyImagePath, variant.name);
492
+ const variantBuffer = await sharp(mainRaw, { raw: mainRawInfo })
493
+ .resize(variant.width, variant.height, {
494
+ fit: "contain",
495
+ background: params.target.acceptance?.alpha === true ||
496
+ params.target.runtimeSpec?.alphaRequired === true
497
+ ? { r: 0, g: 0, b: 0, alpha: 0 }
498
+ : { r: 0, g: 0, b: 0, alpha: 1 },
499
+ kernel: kernelForAlgorithm(variant.algorithm),
500
+ })
501
+ .png({ compressionLevel: 9 })
502
+ .toBuffer();
503
+ await mkdir(path.dirname(variantPath), { recursive: true });
504
+ await writeFile(variantPath, variantBuffer);
505
+ if (params.mirrorLegacy) {
506
+ await mkdir(path.dirname(variantLegacyPath), { recursive: true });
507
+ await writeFile(variantLegacyPath, variantBuffer);
508
+ }
509
+ variantCount += 1;
510
+ }
511
+ if (params.target.auxiliaryMaps?.normalFromHeight) {
512
+ const normalPath = withVariantSuffix(params.processedImagePath, "normal");
513
+ const normalLegacyPath = withVariantSuffix(params.legacyImagePath, "normal");
514
+ const normalBuffer = await sharp(mainRaw, { raw: mainRawInfo })
515
+ .greyscale()
516
+ .convolve({
517
+ width: 3,
518
+ height: 3,
519
+ kernel: [-1, -2, -1, 0, 0, 0, 1, 2, 1],
520
+ })
521
+ .png({ compressionLevel: 9 })
522
+ .toBuffer();
523
+ await writeFile(normalPath, normalBuffer);
524
+ if (params.mirrorLegacy) {
525
+ await writeFile(normalLegacyPath, normalBuffer);
526
+ }
527
+ }
528
+ if (params.target.auxiliaryMaps?.specularFromLuma) {
529
+ const specularPath = withVariantSuffix(params.processedImagePath, "specular");
530
+ const specularLegacyPath = withVariantSuffix(params.legacyImagePath, "specular");
531
+ const specularBuffer = await sharp(mainRaw, { raw: mainRawInfo }).greyscale().png().toBuffer();
532
+ await writeFile(specularPath, specularBuffer);
533
+ if (params.mirrorLegacy) {
534
+ await writeFile(specularLegacyPath, specularBuffer);
535
+ }
536
+ }
537
+ if (params.target.auxiliaryMaps?.aoFromLuma) {
538
+ const aoPath = withVariantSuffix(params.processedImagePath, "ao");
539
+ const aoLegacyPath = withVariantSuffix(params.legacyImagePath, "ao");
540
+ const aoBuffer = await sharp(mainRaw, { raw: mainRawInfo })
541
+ .greyscale()
542
+ .linear(0.8, 10)
543
+ .png()
544
+ .toBuffer();
545
+ await writeFile(aoPath, aoBuffer);
546
+ if (params.mirrorLegacy) {
547
+ await writeFile(aoLegacyPath, aoBuffer);
548
+ }
549
+ }
550
+ return { variantCount };
551
+ }
552
+ function animationMetadataPath(filePath) {
553
+ const ext = path.extname(filePath);
554
+ const base = filePath.slice(0, filePath.length - ext.length);
555
+ return `${base}.anim.json`;
556
+ }
557
+ async function assembleSpritesheetTarget(params) {
558
+ if (params.frameTargets.length === 0) {
559
+ return;
560
+ }
561
+ const orderedAnimations = params.sheetTarget.spritesheet?.animations?.map((animation) => animation.name) ?? [];
562
+ const animationOrder = new Map(orderedAnimations.map((name, index) => [name, index]));
563
+ const orderedFrames = [...params.frameTargets].sort((left, right) => {
564
+ const leftAnimation = left.spritesheet?.animationName ?? "";
565
+ const rightAnimation = right.spritesheet?.animationName ?? "";
566
+ const leftOrder = animationOrder.get(leftAnimation) ?? Number.MAX_SAFE_INTEGER;
567
+ const rightOrder = animationOrder.get(rightAnimation) ?? Number.MAX_SAFE_INTEGER;
568
+ if (leftOrder !== rightOrder) {
569
+ return leftOrder - rightOrder;
570
+ }
571
+ if (leftAnimation !== rightAnimation) {
572
+ return leftAnimation.localeCompare(rightAnimation);
573
+ }
574
+ return (left.spritesheet?.frameIndex ?? 0) - (right.spritesheet?.frameIndex ?? 0);
575
+ });
576
+ const frameBuffers = [];
577
+ for (const frameTarget of orderedFrames) {
578
+ const framePath = resolvePathWithinDir(params.processedImagesDir, frameTarget.out, `spritesheet frame for target "${frameTarget.id}"`);
579
+ const result = await openImage(framePath, "pipeline")
580
+ .png()
581
+ .toBuffer({ resolveWithObject: true });
582
+ const { width, height } = result.info;
583
+ if (!width || !height) {
584
+ continue;
585
+ }
586
+ frameBuffers.push({
587
+ target: frameTarget,
588
+ buffer: result.data,
589
+ width,
590
+ height,
591
+ });
592
+ }
593
+ if (frameBuffers.length === 0) {
594
+ return;
595
+ }
596
+ const frameWidth = Math.max(...frameBuffers.map((frame) => frame.width));
597
+ const frameHeight = Math.max(...frameBuffers.map((frame) => frame.height));
598
+ const framesByAnimation = new Map();
599
+ for (const frame of frameBuffers) {
600
+ const animation = frame.target.spritesheet?.animationName ?? "default";
601
+ const list = framesByAnimation.get(animation) ?? [];
602
+ list.push(frame);
603
+ framesByAnimation.set(animation, list);
604
+ }
605
+ const animationNames = Array.from(framesByAnimation.keys()).sort((left, right) => {
606
+ const leftOrder = animationOrder.get(left) ?? Number.MAX_SAFE_INTEGER;
607
+ const rightOrder = animationOrder.get(right) ?? Number.MAX_SAFE_INTEGER;
608
+ if (leftOrder !== rightOrder) {
609
+ return leftOrder - rightOrder;
610
+ }
611
+ return left.localeCompare(right);
612
+ });
613
+ const maxFramesPerAnimation = Math.max(...Array.from(framesByAnimation.values()).map((frames) => frames.length));
614
+ const sheetWidth = Math.max(1, maxFramesPerAnimation) * frameWidth;
615
+ const sheetHeight = Math.max(1, animationNames.length) * frameHeight;
616
+ const composites = [];
617
+ const framesMeta = {};
618
+ for (const [animationIndex, animationName] of animationNames.entries()) {
619
+ const frames = (framesByAnimation.get(animationName) ?? []).sort((left, right) => (left.target.spritesheet?.frameIndex ?? 0) - (right.target.spritesheet?.frameIndex ?? 0));
620
+ for (const [frameOffset, frame] of frames.entries()) {
621
+ const frameIndex = frame.target.spritesheet?.frameIndex ?? frameOffset;
622
+ const x = frameIndex * frameWidth;
623
+ const y = animationIndex * frameHeight;
624
+ composites.push({
625
+ input: frame.buffer,
626
+ left: x,
627
+ top: y,
628
+ });
629
+ framesMeta[`${params.sheetTarget.id}.${animationName}.${frameIndex}`] = {
630
+ frame: { x, y, w: frame.width, h: frame.height },
631
+ animation: {
632
+ name: animationName,
633
+ fps: frame.target.spritesheet?.fps ?? 8,
634
+ loop: frame.target.spritesheet?.loop ?? true,
635
+ pivot: frame.target.spritesheet?.pivot ?? { x: 0.5, y: 0.85 },
636
+ },
637
+ };
638
+ }
639
+ }
640
+ const sheetBuffer = await sharp({
641
+ create: {
642
+ width: sheetWidth,
643
+ height: sheetHeight,
644
+ channels: 4,
645
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
646
+ },
647
+ })
648
+ .composite(composites)
649
+ .png({ compressionLevel: 9 })
650
+ .toBuffer();
651
+ const processedSheetPath = resolvePathWithinDir(params.processedImagesDir, params.sheetTarget.out, `spritesheet output for target "${params.sheetTarget.id}"`);
652
+ const legacySheetPath = resolvePathWithinDir(params.legacyImagesDir, params.sheetTarget.out, `legacy spritesheet output for target "${params.sheetTarget.id}"`);
653
+ await mkdir(path.dirname(processedSheetPath), { recursive: true });
654
+ await writeFile(processedSheetPath, sheetBuffer);
655
+ if (params.mirrorLegacy) {
656
+ await mkdir(path.dirname(legacySheetPath), { recursive: true });
657
+ await writeFile(legacySheetPath, sheetBuffer);
658
+ }
659
+ const metadata = {
660
+ generatedAt: new Date().toISOString(),
661
+ sheetTargetId: params.sheetTarget.id,
662
+ frameWidth,
663
+ frameHeight,
664
+ animations: animationNames.map((name) => ({
665
+ name,
666
+ frameCount: framesByAnimation.get(name)?.length ?? 0,
667
+ })),
668
+ frames: framesMeta,
669
+ };
670
+ const processedMetaPath = animationMetadataPath(processedSheetPath);
671
+ await writeFile(processedMetaPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
672
+ if (params.mirrorLegacy) {
673
+ const legacyMetaPath = animationMetadataPath(legacySheetPath);
674
+ await writeFile(legacyMetaPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
675
+ }
676
+ }
677
+ export async function runProcessPipeline(options) {
678
+ const layout = resolveStagePathLayout(options.outDir);
679
+ const targetsIndexPath = path.resolve(options.targetsIndexPath ?? path.join(layout.jobsDir, "targets-index.json"));
680
+ const strict = options.strict ?? true;
681
+ const mirrorLegacy = options.mirrorLegacyImages ?? true;
682
+ const rawIndex = await readFile(targetsIndexPath, "utf8");
683
+ const index = parseTargetsIndex(rawIndex, targetsIndexPath);
684
+ const targets = (Array.isArray(index.targets) ? index.targets : []).map((target, targetIndex) => {
685
+ try {
686
+ return {
687
+ ...target,
688
+ out: normalizeTargetOutPath(target.out),
689
+ };
690
+ }
691
+ catch (error) {
692
+ throw new Error(`targets[${targetIndex}].out is invalid: ${error instanceof Error ? error.message : String(error)}`);
693
+ }
694
+ });
695
+ // Targets that have raw images and need post-processing.
696
+ // Excludes sheet assembler targets (virtual, no raw image) but includes
697
+ // strip-sliced frames whose raw images come from slicing, not generation.
698
+ const processableTargets = targets.filter((target) => target.spritesheet?.isSheet !== true);
699
+ const spritesheetSheetTargets = targets.filter((target) => target.spritesheet?.isSheet === true);
700
+ await mkdir(layout.processedImagesDir, { recursive: true });
701
+ if (mirrorLegacy) {
702
+ await mkdir(layout.legacyImagesDir, { recursive: true });
703
+ }
704
+ const processJobs = await runPipelineWorkers(processableTargets, Math.max(1, Math.min(cpus().length, 8)), async (target) => {
705
+ const rawImagePath = resolvePathWithinDir(layout.rawDir, target.out, `raw image for target "${target.id}"`);
706
+ const processedImagePath = resolvePathWithinDir(layout.processedImagesDir, target.out, `processed image for target "${target.id}"`);
707
+ const legacyImagePath = resolvePathWithinDir(layout.legacyImagesDir, target.out, `legacy image for target "${target.id}"`);
708
+ return processSingleTarget({
709
+ target,
710
+ rawImagePath,
711
+ processedImagePath,
712
+ mirrorLegacy,
713
+ legacyImagePath,
714
+ });
715
+ });
716
+ const frameTargetsBySheetId = new Map();
717
+ for (const target of processableTargets) {
718
+ const sheetTargetId = target.spritesheet?.sheetTargetId;
719
+ if (!sheetTargetId || target.spritesheet?.isSheet === true) {
720
+ continue;
721
+ }
722
+ const list = frameTargetsBySheetId.get(sheetTargetId) ?? [];
723
+ list.push(target);
724
+ frameTargetsBySheetId.set(sheetTargetId, list);
725
+ }
726
+ await runPipelineWorkers(spritesheetSheetTargets, Math.max(1, Math.min(cpus().length, 4)), (sheetTarget) => assembleSpritesheetTarget({
727
+ sheetTarget,
728
+ frameTargets: frameTargetsBySheetId.get(sheetTarget.id) ?? [],
729
+ processedImagesDir: layout.processedImagesDir,
730
+ legacyImagesDir: layout.legacyImagesDir,
731
+ mirrorLegacy,
732
+ }));
733
+ const processedResults = processJobs;
734
+ const variantCount = processedResults.reduce((sum, result) => sum + result.variantCount, 0);
735
+ const acceptanceReport = await runImageAcceptanceChecks({
736
+ targets,
737
+ imagesDir: layout.processedImagesDir,
738
+ strict,
739
+ });
740
+ await mkdir(layout.checksDir, { recursive: true });
741
+ const acceptanceReportPath = path.join(layout.checksDir, "image-acceptance-report.json");
742
+ await writeJsonFile(acceptanceReportPath, acceptanceReport);
743
+ assertImageAcceptanceReport(acceptanceReport);
744
+ const catalog = await buildCatalog(targets, layout.processedImagesDir);
745
+ const catalogPath = path.join(layout.processedDir, "catalog.json");
746
+ await writeJsonFile(catalogPath, catalog);
747
+ return {
748
+ processedImagesDir: layout.processedImagesDir,
749
+ legacyImagesDir: layout.legacyImagesDir,
750
+ catalogPath,
751
+ acceptanceReportPath,
752
+ processedCount: targets.filter((target) => !target.catalogDisabled).length,
753
+ variantCount,
754
+ };
755
+ }
756
+ async function runPipelineWorkers(items, concurrency, worker) {
757
+ if (items.length === 0) {
758
+ return [];
759
+ }
760
+ const maxWorkers = Math.max(1, Math.floor(concurrency));
761
+ const results = new Array(items.length);
762
+ let nextIndex = 0;
763
+ const workers = [];
764
+ for (let workerIndex = 0; workerIndex < maxWorkers; workerIndex += 1) {
765
+ workers.push((async () => {
766
+ while (nextIndex < items.length) {
767
+ const currentIndex = nextIndex;
768
+ nextIndex += 1;
769
+ results[currentIndex] = await worker(items[currentIndex]);
770
+ }
771
+ })());
772
+ }
773
+ await Promise.all(workers);
774
+ return results;
775
+ }
776
+ //# sourceMappingURL=process.js.map