pixelkiln 0.36.0 → 0.37.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.
package/dist/cli.js CHANGED
@@ -606,8 +606,8 @@ function formatCost(unit, amount) {
606
606
  }
607
607
 
608
608
  // src/pipeline/plan.ts
609
- import { existsSync as existsSync6 } from "fs";
610
- import path9 from "path";
609
+ import { existsSync as existsSync5 } from "fs";
610
+ import path8 from "path";
611
611
 
612
612
  // src/hash.ts
613
613
  import { createHash } from "crypto";
@@ -843,6 +843,18 @@ var StyleObjectSchema = z.object({
843
843
  * chocolate bar.
844
844
  */
845
845
  palette: z.array(HexColorSchema).default([]),
846
+ /**
847
+ * Snap every visible pixel of the downloaded art to the nearest `palette`
848
+ * colour, without dithering. The palette is sent to providers that take
849
+ * one, and most honour it most of the time; this makes it a guarantee
850
+ * for every generator, including the ones with no palette parameter.
851
+ * Applied by `fetch` as the file is written and recorded on the lock
852
+ * entry, so a restore reproduces the same bytes. Turning it on or off
853
+ * does not change the spec hash: `plan` reports the entry as recoverable
854
+ * and the next `fetch` or `restore` re-applies it from the art already
855
+ * on disk or in the cache, spending nothing.
856
+ */
857
+ enforcePalette: z.boolean().default(false),
846
858
  /** Fail-closed native-grid, palette, and human-approval policy for derived art. */
847
859
  quality: QualityProfileSchema.optional(),
848
860
  /**
@@ -884,6 +896,13 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
884
896
  path: ["quality"]
885
897
  });
886
898
  }
899
+ if (style.enforcePalette && style.palette.length < 2) {
900
+ ctx.addIssue({
901
+ code: z.ZodIssueCode.custom,
902
+ message: "enforcePalette needs a palette of at least two colours to snap to",
903
+ path: ["enforcePalette"]
904
+ });
905
+ }
887
906
  });
888
907
  var InheritedStyleSchema = StyleObjectSchema.partial().extend({
889
908
  extends: z.string().min(1),
@@ -1006,7 +1025,21 @@ var LockOutputSchema = z.object({
1006
1025
  path: z.string(),
1007
1026
  sha256: z.string(),
1008
1027
  role: z.string().optional(),
1009
- mediaType: MediaTypeSchema.optional()
1028
+ mediaType: MediaTypeSchema.optional(),
1029
+ /**
1030
+ * Hash of the provider's bytes before post-processing changed them, when
1031
+ * it did. The content cache keeps both, so the post-processing can be
1032
+ * undone or redone without contacting the provider.
1033
+ */
1034
+ raw: z.string().optional()
1035
+ });
1036
+ var PostprocessSchema = z.object({
1037
+ palette: z.object({
1038
+ /** Canonical lowercase `#rrggbb` values, in manifest order. */
1039
+ colors: z.array(z.string()).min(2).max(256),
1040
+ dither: z.literal("none"),
1041
+ distance: z.literal("redmean")
1042
+ }).optional()
1010
1043
  });
1011
1044
  var LockSourceSchema = z.object({
1012
1045
  url: z.string(),
@@ -1032,6 +1065,7 @@ var LockHistoryEntrySchema = z.object({
1032
1065
  cost: z.number().finite().nonnegative().default(0),
1033
1066
  costUnit: z.string().min(1).default("generations"),
1034
1067
  provider: z.string().default("pixellab"),
1068
+ postprocess: PostprocessSchema.optional(),
1035
1069
  /** When the replacement was submitted. */
1036
1070
  retiredAt: z.string()
1037
1071
  });
@@ -1103,14 +1137,7 @@ var LockEntrySchema = z.object({
1103
1137
  * `role` labels non-primary artifacts (e.g. "portrait", "spriteframes") so a
1104
1138
  * consumer can find the one it wants without pattern-matching on paths.
1105
1139
  */
1106
- outputs: z.array(
1107
- z.object({
1108
- path: z.string(),
1109
- sha256: z.string(),
1110
- role: z.string().optional(),
1111
- mediaType: MediaTypeSchema.optional()
1112
- })
1113
- ).default([]),
1140
+ outputs: z.array(LockOutputSchema).default([]),
1114
1141
  /**
1115
1142
  * Provider-owned data retained for downstream consumers, namespaced by
1116
1143
  * provider id. PixelLab connectable sets keep their exact `tileRules` here
@@ -1126,7 +1153,9 @@ var LockEntrySchema = z.object({
1126
1153
  /** Which provider produced this. Absent on entries written before providers. */
1127
1154
  provider: z.string().default("pixellab"),
1128
1155
  /** Generations this entry replaced, newest first; see LockHistoryEntrySchema. */
1129
- history: z.array(LockHistoryEntrySchema).optional()
1156
+ history: z.array(LockHistoryEntrySchema).optional(),
1157
+ /** Post-processing applied to the recorded outputs; absent when the bytes are the provider's. */
1158
+ postprocess: PostprocessSchema.optional()
1130
1159
  });
1131
1160
  var LockSchema = z.object({
1132
1161
  version: z.literal(2),
@@ -1876,13 +1905,13 @@ function resolveSpecOutputs(spec, lock, manifestDir) {
1876
1905
  }
1877
1906
 
1878
1907
  // src/pipeline/quality-profile.ts
1879
- import { existsSync as existsSync4 } from "fs";
1880
- import path7 from "path";
1908
+ import { existsSync as existsSync3 } from "fs";
1909
+ import path6 from "path";
1881
1910
 
1882
1911
  // src/pipeline/refine.ts
1883
- import path6 from "path";
1912
+ import path5 from "path";
1884
1913
  import { execFile } from "child_process";
1885
- import { mkdtemp, readFile as readFile4, rm as rm3 } from "fs/promises";
1914
+ import { mkdtemp, readFile as readFile3, rm as rm3 } from "fs/promises";
1886
1915
  import { tmpdir } from "os";
1887
1916
  import { promisify } from "util";
1888
1917
 
@@ -2343,10 +2372,19 @@ async function writeArtifactBundle(files, options = {}) {
2343
2372
  };
2344
2373
  }
2345
2374
 
2346
- // src/pipeline/audit.ts
2347
- import { readFile as readFile3 } from "fs/promises";
2348
- import { existsSync as existsSync3 } from "fs";
2349
- import path5 from "path";
2375
+ // src/palette.ts
2376
+ function normalizePalette(values) {
2377
+ const colors = /* @__PURE__ */ new Map();
2378
+ for (const value of values) {
2379
+ const rgb = parseHex(value);
2380
+ const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
2381
+ if (!colors.has(hex2)) colors.set(hex2, { hex: hex2, ...rgb });
2382
+ }
2383
+ if (colors.size < 2 || colors.size > 256) {
2384
+ throw new Error(`A palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
2385
+ }
2386
+ return [...colors.values()];
2387
+ }
2350
2388
  function colorDistance(a, b) {
2351
2389
  const rmean = (a.r + b.r) / 2;
2352
2390
  const dr = a.r - b.r;
@@ -2356,146 +2394,45 @@ function colorDistance(a, b) {
2356
2394
  (512 + rmean) * dr * dr / 256 + 4 * dg * dg + (767 - rmean) * db * db / 256
2357
2395
  );
2358
2396
  }
2359
- function paletteDistance(asset, reference) {
2360
- if (!asset.length || !reference.length) return 0;
2361
- let total = 0;
2362
- let weight = 0;
2363
- for (const color of asset) {
2364
- let nearest = Infinity;
2365
- for (const ref of reference) nearest = Math.min(nearest, colorDistance(color, ref));
2366
- total += nearest * color.weight;
2367
- weight += color.weight;
2368
- }
2369
- return weight === 0 ? 0 : total / weight;
2370
- }
2371
- function mergePalettes(palettes, topN = 24) {
2372
- const totals = /* @__PURE__ */ new Map();
2373
- for (const palette of palettes) {
2374
- for (const color of palette) {
2375
- const key = `${color.r},${color.g},${color.b}`;
2376
- const existing = totals.get(key);
2377
- if (existing) existing.weight += color.weight;
2378
- else totals.set(key, { ...color });
2397
+ function quantize(png, palette) {
2398
+ const colors = normalizePalette(palette);
2399
+ const pixels = Buffer.from(png.pixels);
2400
+ const nearestOf = /* @__PURE__ */ new Map();
2401
+ let changed = 0;
2402
+ for (let i = 0; i < pixels.length; i += 4) {
2403
+ const alpha = pixels[i + 3];
2404
+ if (alpha === 0) {
2405
+ if (pixels[i] || pixels[i + 1] || pixels[i + 2]) changed++;
2406
+ pixels[i] = 0;
2407
+ pixels[i + 1] = 0;
2408
+ pixels[i + 2] = 0;
2409
+ continue;
2379
2410
  }
2380
- }
2381
- const merged = [...totals.values()].sort((a, b) => b.weight - a.weight).slice(0, topN);
2382
- const sum = merged.reduce((s, c) => s + c.weight, 0) || 1;
2383
- return merged.map((c) => ({ ...c, weight: c.weight / sum }));
2384
- }
2385
- async function auditStyle(loaded, specs, styleId, lock) {
2386
- const style = loaded.manifest.styles[styleId];
2387
- if (!style) throw new Error(`Unknown style "${styleId}"`);
2388
- const mine = specs.filter((s) => s.styleId === styleId);
2389
- const assets = [];
2390
- const missing = [];
2391
- const unreadable = [];
2392
- for (const spec of mine) {
2393
- for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
2394
- if (!existsSync3(output.absolutePath)) {
2395
- missing.push(output.id);
2396
- continue;
2397
- }
2398
- try {
2399
- const png = decodePng(await readFile3(output.absolutePath));
2400
- const palette = extractPalette(png, 12);
2401
- assets.push({
2402
- assetId: spec.assetId,
2403
- ...output.role ? { outputRole: output.role } : {},
2404
- id: output.id,
2405
- file: output.absolutePath,
2406
- width: png.width,
2407
- height: png.height,
2408
- paletteDistance: 0,
2409
- // filled once the reference is known
2410
- transparency: transparencyRatio(png),
2411
- colorCount: countColors(png),
2412
- palette
2413
- });
2414
- } catch (err) {
2415
- unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
2411
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
2412
+ let nearest = nearestOf.get(key);
2413
+ if (!nearest) {
2414
+ const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
2415
+ nearest = colors[0];
2416
+ let distance = colorDistance(pixel, nearest);
2417
+ for (let j = 1; j < colors.length; j++) {
2418
+ const candidate = colors[j];
2419
+ const candidateDistance = colorDistance(pixel, candidate);
2420
+ if (candidateDistance < distance) {
2421
+ nearest = candidate;
2422
+ distance = candidateDistance;
2423
+ }
2416
2424
  }
2425
+ nearestOf.set(key, nearest);
2417
2426
  }
2427
+ if (pixels[i] !== nearest.r || pixels[i + 1] !== nearest.g || pixels[i + 2] !== nearest.b) changed++;
2428
+ pixels[i] = nearest.r;
2429
+ pixels[i + 1] = nearest.g;
2430
+ pixels[i + 2] = nearest.b;
2418
2431
  }
2419
- let reference = [];
2420
- let referenceFromStyleImages = false;
2421
- const refPalettes = [];
2422
- for (const img of style.styleImages) {
2423
- const abs = path5.resolve(loaded.root, img.path);
2424
- if (!existsSync3(abs)) continue;
2425
- try {
2426
- refPalettes.push(extractPalette(decodePng(await readFile3(abs)), 12));
2427
- } catch {
2428
- }
2429
- }
2430
- if (refPalettes.length) {
2431
- reference = mergePalettes(refPalettes);
2432
- referenceFromStyleImages = true;
2433
- for (const asset of assets) {
2434
- asset.paletteDistance = paletteDistance(asset.palette, reference);
2435
- }
2436
- } else {
2437
- reference = mergePalettes(assets.map((a) => a.palette));
2438
- for (const asset of assets) {
2439
- const others = assets.filter((a) => a !== asset).map((a) => a.palette);
2440
- asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
2441
- }
2442
- }
2443
- assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
2444
- return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
2445
- }
2446
- function countColors(png) {
2447
- const seen = /* @__PURE__ */ new Set();
2448
- for (let i = 0; i < png.pixels.length; i += 4) {
2449
- if (png.pixels[i + 3] < 128) continue;
2450
- seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
2451
- }
2452
- return seen.size;
2432
+ return { png: { width: png.width, height: png.height, pixels }, changed };
2453
2433
  }
2454
- function outliers(audit, sigma = 1.5) {
2455
- const n = audit.assets.length;
2456
- if (n < 3) return [];
2457
- const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
2458
- const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
2459
- const sd = Math.sqrt(variance);
2460
- if (sd === 0) return [];
2461
- return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
2462
- }
2463
- function evaluateAudit(audit, thresholds = {}) {
2464
- const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
2465
- const relative = outliers(audit, resolved.sigma);
2466
- const relativeIds = new Set(relative.map((asset) => asset.id));
2467
- const violations = [];
2468
- for (const asset of audit.assets) {
2469
- const reasons = [];
2470
- if (relativeIds.has(asset.id)) {
2471
- reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
2472
- }
2473
- if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
2474
- reasons.push(
2475
- `palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
2476
- );
2477
- }
2478
- if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
2479
- reasons.push(
2480
- `transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
2481
- );
2482
- }
2483
- if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
2484
- reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
2485
- }
2486
- if (reasons.length) violations.push({ id: asset.id, reasons });
2487
- }
2488
- return {
2489
- safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
2490
- thresholds: resolved,
2491
- outliers: [...relativeIds],
2492
- violations,
2493
- missing: [...audit.missing],
2494
- unreadable: [...audit.unreadable]
2495
- };
2496
- }
2497
- function hex(c) {
2498
- return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
2434
+ function quantizeToPalette(png, palette) {
2435
+ return quantize(png, palette).png;
2499
2436
  }
2500
2437
 
2501
2438
  // src/pipeline/refine.ts
@@ -2582,48 +2519,12 @@ function parseDetection(raw) {
2582
2519
  confidence: confidenceFor(consensus)
2583
2520
  };
2584
2521
  }
2585
- function normalizePalette(values) {
2586
- const colors = /* @__PURE__ */ new Map();
2587
- for (const value of values) {
2588
- const rgb = parseHex(value);
2589
- const hex2 = `#${rgb.r.toString(16).padStart(2, "0")}${rgb.g.toString(16).padStart(2, "0")}${rgb.b.toString(16).padStart(2, "0")}`;
2590
- colors.set(hex2, { hex: hex2, ...rgb });
2591
- }
2592
- if (colors.size < 2 || colors.size > 256) {
2593
- throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${colors.size}.`);
2594
- }
2595
- return [...colors.values()];
2596
- }
2597
2522
  function normalizeRefinementPalette(values) {
2598
- return normalizePalette(values).map((color) => color.hex);
2599
- }
2600
- function quantizeToPalette(png, palette) {
2601
- const colors = normalizePalette(palette);
2602
- const pixels = Buffer.from(png.pixels);
2603
- for (let i = 0; i < pixels.length; i += 4) {
2604
- const alpha = pixels[i + 3];
2605
- if (alpha === 0) {
2606
- pixels[i] = 0;
2607
- pixels[i + 1] = 0;
2608
- pixels[i + 2] = 0;
2609
- continue;
2610
- }
2611
- const pixel = { r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] };
2612
- let nearest = colors[0];
2613
- let distance = colorDistance(pixel, nearest);
2614
- for (let j = 1; j < colors.length; j++) {
2615
- const candidate = colors[j];
2616
- const candidateDistance = colorDistance(pixel, candidate);
2617
- if (candidateDistance < distance) {
2618
- nearest = candidate;
2619
- distance = candidateDistance;
2620
- }
2621
- }
2622
- pixels[i] = nearest.r;
2623
- pixels[i + 1] = nearest.g;
2624
- pixels[i + 2] = nearest.b;
2523
+ try {
2524
+ return normalizePalette(values).map((color) => color.hex);
2525
+ } catch (err) {
2526
+ throw new Error(`A refinement palette must contain 2\u2013256 unique colors; got ${new Set(values.map((v) => v.toLowerCase())).size}.`, { cause: err });
2625
2527
  }
2626
- return { width: png.width, height: png.height, pixels };
2627
2528
  }
2628
2529
  function colorCount(png) {
2629
2530
  const colors = /* @__PURE__ */ new Set();
@@ -2733,16 +2634,16 @@ function sameFrameGrid(first, next) {
2733
2634
  return next.detection.stepX === first.detection.stepX && next.detection.stepY === first.detection.stepY && next.detection.columns === first.detection.columns && next.detection.rows === first.detection.rows && phaseMatches(next.detection.phaseX, first.detection.phaseX, first.detection.stepX) && phaseMatches(next.detection.phaseY, first.detection.phaseY, first.detection.stepY) && next.audit.width === first.audit.width && next.audit.height === first.audit.height && next.sourceWidth === first.sourceWidth && next.sourceHeight === first.sourceHeight;
2734
2635
  }
2735
2636
  async function refineAsset(options) {
2736
- const source = path6.resolve(options.source);
2737
- const output = path6.resolve(options.output);
2637
+ const source = path5.resolve(options.source);
2638
+ const output = path5.resolve(options.output);
2738
2639
  if (source === output) throw new Error("Refinement source and output must be different files.");
2739
- const record = path6.resolve(qualityRecordPath(output));
2640
+ const record = path5.resolve(qualityRecordPath(output));
2740
2641
  const palette = normalizePalette(options.palette).map((color) => color.hex);
2741
2642
  const minGridConfidence = requireConfidence(options.minGridConfidence ?? "high");
2742
2643
  if (options.minTransparency !== void 0 && (!Number.isFinite(options.minTransparency) || options.minTransparency < 0 || options.minTransparency > 1)) throw new Error("minTransparency must be between 0 and 1.");
2743
2644
  let sourcePng;
2744
2645
  try {
2745
- sourcePng = decodePng(await readFile4(source));
2646
+ sourcePng = decodePng(await readFile3(source));
2746
2647
  } catch (error) {
2747
2648
  throw new Error(`Cannot read refinement source ${source}: ${message2(error)}`, { cause: error });
2748
2649
  }
@@ -2759,8 +2660,8 @@ async function refineAsset(options) {
2759
2660
  );
2760
2661
  }
2761
2662
  }
2762
- const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-"));
2763
- const recovered = path6.join(temp, "native.png");
2663
+ const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-"));
2664
+ const recovered = path5.join(temp, "native.png");
2764
2665
  const fixerCommand = compatibilityCommand ?? options.fixerPython ?? process.env.PIXELKILN_PIXEL_FIXER_PYTHON ?? "python3";
2765
2666
  const args = compatibilityCommand ? [...options.fixerArgsPrefix ?? [], source, "--extract", recovered] : ["-c", PYTHON_FIXER_BRIDGE, source, recovered];
2766
2667
  try {
@@ -2781,7 +2682,7 @@ async function refineAsset(options) {
2781
2682
  );
2782
2683
  }
2783
2684
  const detection = parseDetection(firstJsonObject(stdout));
2784
- const native = decodePng(await readFile4(recovered));
2685
+ const native = decodePng(await readFile3(recovered));
2785
2686
  const quantized = quantizeToPalette(native, palette);
2786
2687
  const png = encodeRgbaPng(quantized.width, quantized.height, quantized.pixels);
2787
2688
  const audit = auditRefinement(
@@ -2845,18 +2746,18 @@ async function refineFrameSet(options) {
2845
2746
  if (roles.some((role) => !role.trim()) || new Set(roles).size !== roles.length) {
2846
2747
  throw new Error("Frame-set roles must be non-empty and unique.");
2847
2748
  }
2848
- const output = path6.resolve(options.output);
2849
- const record = path6.resolve(qualityRecordPath(output));
2749
+ const output = path5.resolve(options.output);
2750
+ const record = path5.resolve(qualityRecordPath(output));
2850
2751
  const palette = normalizeRefinementPalette(options.palette);
2851
2752
  const finalOutputs = options.sources.map((source, index) => qualityFrameOutputPath(output, source.role, index));
2852
2753
  if (new Set(finalOutputs).size !== finalOutputs.length) {
2853
2754
  throw new Error("Frame-set roles resolve to duplicate output filenames.");
2854
2755
  }
2855
- const sourceSet = new Set(options.sources.map((source) => path6.resolve(source.path)));
2756
+ const sourceSet = new Set(options.sources.map((source) => path5.resolve(source.path)));
2856
2757
  if (finalOutputs.some((file) => sourceSet.has(file))) {
2857
2758
  throw new Error("Frame-set refinement sources and outputs must be different files.");
2858
2759
  }
2859
- const temp = await mkdtemp(path6.join(tmpdir(), "pixelkiln-refine-frames-"));
2760
+ const temp = await mkdtemp(path5.join(tmpdir(), "pixelkiln-refine-frames-"));
2860
2761
  try {
2861
2762
  const frames = [];
2862
2763
  const outputs = [];
@@ -2864,8 +2765,8 @@ async function refineFrameSet(options) {
2864
2765
  const sourcePaths = [];
2865
2766
  let firstOptions = null;
2866
2767
  for (const [index, sourceEntry] of options.sources.entries()) {
2867
- const source = path6.resolve(sourceEntry.path);
2868
- const temporaryOutput = path6.join(temp, `${String(index).padStart(2, "0")}.png`);
2768
+ const source = path5.resolve(sourceEntry.path);
2769
+ const temporaryOutput = path5.join(temp, `${String(index).padStart(2, "0")}.png`);
2869
2770
  const refined = await refineAsset({
2870
2771
  source,
2871
2772
  output: temporaryOutput,
@@ -2897,7 +2798,7 @@ async function refineFrameSet(options) {
2897
2798
  });
2898
2799
  outputs.push({
2899
2800
  path: finalOutputs[index],
2900
- data: await readFile4(temporaryOutput)
2801
+ data: await readFile3(temporaryOutput)
2901
2802
  });
2902
2803
  }
2903
2804
  const first = frames[0];
@@ -2940,7 +2841,7 @@ async function refineFrameSet(options) {
2940
2841
  );
2941
2842
  return {
2942
2843
  sources: sourcePaths,
2943
- outputs: outputs.map((item) => path6.resolve(item.path)),
2844
+ outputs: outputs.map((item) => path5.resolve(item.path)),
2944
2845
  record,
2945
2846
  frames,
2946
2847
  palette,
@@ -2951,12 +2852,12 @@ async function refineFrameSet(options) {
2951
2852
  }
2952
2853
  }
2953
2854
  async function checkQualityRecord(recordPath) {
2954
- const record = path6.resolve(recordPath);
2855
+ const record = path5.resolve(recordPath);
2955
2856
  const manifest = await readArtifactBundleManifest(record);
2956
2857
  const options = parseRecordOptions(manifest, record);
2957
2858
  const verification = await verifyArtifactBundle(record);
2958
- const sources = manifest.sources.map((source2) => path6.resolve(path6.dirname(record), source2.path));
2959
- const outputs = manifest.outputs.map((output2) => path6.resolve(path6.dirname(record), output2.path));
2859
+ const sources = manifest.sources.map((source2) => path5.resolve(path5.dirname(record), source2.path));
2860
+ const outputs = manifest.outputs.map((output2) => path5.resolve(path5.dirname(record), output2.path));
2960
2861
  const source = sources[0] ?? "";
2961
2862
  const output = outputs[0] ?? "";
2962
2863
  const expectedCount = options.frameSet?.count ?? 1;
@@ -2989,7 +2890,7 @@ async function checkQualityRecord(recordPath) {
2989
2890
  async function approveQualityRecord(recordPath, approval) {
2990
2891
  const reviewer = approval.reviewer.trim();
2991
2892
  if (!reviewer) throw new Error("A human reviewer name is required.");
2992
- const record = path6.resolve(recordPath);
2893
+ const record = path5.resolve(recordPath);
2993
2894
  const manifest = await readArtifactBundleManifest(record);
2994
2895
  const options = parseRecordOptions(manifest, record);
2995
2896
  const verification = await verifyArtifactBundle(record);
@@ -3010,14 +2911,14 @@ async function approveQualityRecord(recordPath, approval) {
3010
2911
  `A refinement record must contain exactly ${expectedCount} source(s) and output(s).`
3011
2912
  );
3012
2913
  }
3013
- const root = path6.dirname(record);
2914
+ const root = path5.dirname(record);
3014
2915
  const outputs = await Promise.all(manifest.outputs.map(async (entry) => {
3015
- const outputPath = path6.resolve(root, entry.path);
3016
- return { path: outputPath, data: await readFile4(outputPath) };
2916
+ const outputPath = path5.resolve(root, entry.path);
2917
+ return { path: outputPath, data: await readFile3(outputPath) };
3017
2918
  }));
3018
2919
  const sources = manifest.sources.map((source) => ({
3019
2920
  ...source,
3020
- path: path6.resolve(root, source.path)
2921
+ path: path5.resolve(root, source.path)
3021
2922
  }));
3022
2923
  const approved = {
3023
2924
  ...options,
@@ -3049,8 +2950,8 @@ async function qualitySource(spec, lock) {
3049
2950
  if (spec.generator === "frames") {
3050
2951
  return { items: null, reason: "frame-set quality requires ordered downloaded provider outputs" };
3051
2952
  }
3052
- const source = path7.resolve(spec.root, spec.source);
3053
- return existsSync4(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
2953
+ const source = path6.resolve(spec.root, spec.source);
2954
+ return existsSync3(source) ? { items: [{ role: "source", path: source }] } : { items: null, reason: `declared source is missing: ${spec.source}` };
3054
2955
  }
3055
2956
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
3056
2957
  if (!entry) return { items: null, reason: "raw provider output is not in the lockfile" };
@@ -3079,7 +2980,7 @@ async function qualitySource(spec, lock) {
3079
2980
  }
3080
2981
  if (role) roles.add(role);
3081
2982
  const source = currentEntryOutputPath(entry, spec, index);
3082
- if (!existsSync4(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
2983
+ if (!existsSync3(source)) return { items: null, reason: `raw provider output is missing: ${source}` };
3083
2984
  if (await sha256File(source) !== output.sha256) {
3084
2985
  return { items: null, reason: "raw provider output was modified after download" };
3085
2986
  }
@@ -3098,7 +2999,7 @@ async function inspectQualityProfile(spec, lock) {
3098
2999
  }
3099
3000
  const sourcePaths = source.items.map((item) => item.path);
3100
3001
  const expectedOutputs = spec.generator === "frames" ? source.items.map((item, index) => qualityFrameOutputPath(output, item.role, index)) : [output];
3101
- if (!existsSync4(record)) {
3002
+ if (!existsSync3(record)) {
3102
3003
  return {
3103
3004
  key,
3104
3005
  state: "needs-refinement",
@@ -3116,7 +3017,7 @@ async function inspectQualityProfile(spec, lock) {
3116
3017
  const expectedRevision = spec.quality.fixerRevision ?? PIXEL_ART_FIXER_REVISION;
3117
3018
  const expectedTransparency = spec.quality.minTransparency ?? null;
3118
3019
  const expectedFrameSet = spec.generator === "frames" ? { fps: spec.quality.fps ?? 12, count: source.items.length, roles: source.items.map((item) => item.role) } : null;
3119
- const contractChanged = JSON.stringify(check.sources.map((item) => path7.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path7.resolve(item))) || JSON.stringify(check.outputs.map((item) => path7.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path7.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
3020
+ const contractChanged = JSON.stringify(check.sources.map((item) => path6.resolve(item))) !== JSON.stringify(sourcePaths.map((item) => path6.resolve(item))) || JSON.stringify(check.outputs.map((item) => path6.resolve(item))) !== JSON.stringify(expectedOutputs.map((item) => path6.resolve(item))) || JSON.stringify(check.options.palette.colors) !== JSON.stringify(expectedPalette) || check.options.nativeGrid.revision !== expectedRevision || check.options.audit.thresholds.minGridConfidence !== spec.quality.minGridConfidence || check.options.audit.thresholds.minTransparency !== expectedTransparency || (expectedFrameSet ? !check.options.frameSet || check.options.frameSet.fps !== expectedFrameSet.fps || check.options.frameSet.count !== expectedFrameSet.count || JSON.stringify(check.options.frameSet.roles) !== JSON.stringify(expectedFrameSet.roles) : check.options.frameSet !== void 0);
3120
3021
  if (contractChanged) {
3121
3022
  return {
3122
3023
  key,
@@ -3278,9 +3179,38 @@ async function requireApprovedQualitySources(specs, lock, assetIds, outputRoles)
3278
3179
  return sources;
3279
3180
  }
3280
3181
 
3182
+ // src/pipeline/postprocess.ts
3183
+ function postprocessFor(spec) {
3184
+ if (!spec.enforcePalette || spec.palette.length < 2) return void 0;
3185
+ return {
3186
+ palette: {
3187
+ colors: normalizePalette(spec.palette).map((color) => color.hex),
3188
+ dither: "none",
3189
+ distance: "redmean"
3190
+ }
3191
+ };
3192
+ }
3193
+ function samePostprocess(a, b) {
3194
+ const pa = a?.palette;
3195
+ const pb = b?.palette;
3196
+ if (!pa && !pb) return true;
3197
+ if (!pa || !pb) return false;
3198
+ return pa.dither === pb.dither && pa.distance === pb.distance && pa.colors.length === pb.colors.length && pa.colors.every((color, i) => color === pb.colors[i]);
3199
+ }
3200
+ function postprocessCurrent(entry, spec) {
3201
+ return samePostprocess(entry.postprocess, postprocessFor(spec));
3202
+ }
3203
+ function applyPostprocess(bytes, mediaType, record) {
3204
+ if (!record?.palette || mediaType !== MediaType.PNG) return { bytes, changed: false };
3205
+ const png = decodePng(bytes);
3206
+ const { png: snapped, changed } = quantize(png, record.palette.colors);
3207
+ if (!changed) return { bytes, changed: false };
3208
+ return { bytes: encodeRgbaPng(snapped.width, snapped.height, snapped.pixels), changed: true };
3209
+ }
3210
+
3281
3211
  // src/pipeline/revision.ts
3282
- import { existsSync as existsSync5 } from "fs";
3283
- import path8 from "path";
3212
+ import { existsSync as existsSync4 } from "fs";
3213
+ import path7 from "path";
3284
3214
  async function inspectRevisionReadiness(spec, lock) {
3285
3215
  if (!spec.revision) return null;
3286
3216
  return inspectRevision(spec, lock, /* @__PURE__ */ new Set());
@@ -3295,14 +3225,14 @@ async function inspectRevision(spec, lock, seen) {
3295
3225
  reason: `revision source ${spec.styleId}/${revision.sourceAssetId} is not ready: ${dependency.reason}`
3296
3226
  };
3297
3227
  }
3298
- if (!revision.sourceSha256 || !existsSync5(revision.sourceFile)) {
3228
+ if (!revision.sourceSha256 || !existsSync4(revision.sourceFile)) {
3299
3229
  return { ready: false, reason: `revision source is missing: ${revision.sourceFile}` };
3300
3230
  }
3301
3231
  if (await sha256File(revision.sourceFile) !== revision.sourceSha256) {
3302
3232
  return { ready: false, reason: "revision source changed after the manifest was resolved" };
3303
3233
  }
3304
3234
  if (revision.maskFile) {
3305
- if (!revision.maskSha256 || !existsSync5(revision.maskFile)) {
3235
+ if (!revision.maskSha256 || !existsSync4(revision.maskFile)) {
3306
3236
  return { ready: false, reason: `revision mask is missing: ${revision.maskFile}` };
3307
3237
  }
3308
3238
  if (await sha256File(revision.maskFile) !== revision.maskSha256) {
@@ -3332,8 +3262,8 @@ async function inspectParent(spec, lock, seen) {
3332
3262
  };
3333
3263
  }
3334
3264
  if (spec.source) {
3335
- const source = path8.resolve(spec.root, spec.source);
3336
- return existsSync5(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
3265
+ const source = path7.resolve(spec.root, spec.source);
3266
+ return existsSync4(source) ? { ready: true, reason: "declared source is present" } : { ready: false, reason: `declared source is missing: ${spec.source}` };
3337
3267
  }
3338
3268
  const entry = lock.entries[key];
3339
3269
  if (!entry) return { ready: false, reason: "parent is not in the lockfile" };
@@ -3347,7 +3277,7 @@ async function inspectParent(spec, lock, seen) {
3347
3277
  for (let index = 0; index < entry.outputs.length; index++) {
3348
3278
  const output = entry.outputs[index];
3349
3279
  const file = currentEntryOutputPath(entry, spec, index);
3350
- if (!existsSync5(file)) return { ready: false, reason: `parent output is missing: ${file}` };
3280
+ if (!existsSync4(file)) return { ready: false, reason: `parent output is missing: ${file}` };
3351
3281
  if (await sha256File(file) !== output.sha256) {
3352
3282
  return { ready: false, reason: `parent output was modified: ${file}` };
3353
3283
  }
@@ -3376,7 +3306,7 @@ function resumeActions(specs, lock) {
3376
3306
  const entry = lock.entries[key];
3377
3307
  if (!entry || entry.specHash !== spec.specHash) continue;
3378
3308
  if (entry.submissionComplete === false) continue;
3379
- const command = resumeCommandForStatus(entry.status);
3309
+ const command = resumeCommandForStatus(entry.status) ?? (entry.status === "downloaded" && entry.outputs.length && !postprocessCurrent(entry, spec) ? "fetch" : null);
3380
3310
  if (!command) continue;
3381
3311
  if (command === "poll" && !entry.jobId) continue;
3382
3312
  if (command === "pick" && !entry.reviewObjectId) continue;
@@ -3402,7 +3332,7 @@ async function buildPlan(specs, lock, opts = {}) {
3402
3332
  state = "missing";
3403
3333
  reason = "--force";
3404
3334
  } else if (!entry && spec.source && spec.generator !== "frames") {
3405
- if (existsSync6(path9.resolve(spec.root, spec.source))) {
3335
+ if (existsSync5(path8.resolve(spec.root, spec.source))) {
3406
3336
  state = "ok";
3407
3337
  reason = `placed from ${spec.source}; not generated`;
3408
3338
  } else {
@@ -3410,7 +3340,7 @@ async function buildPlan(specs, lock, opts = {}) {
3410
3340
  reason = `declared source is missing: ${spec.source}`;
3411
3341
  }
3412
3342
  } else if (!entry) {
3413
- if (existsSync6(spec.outFile)) {
3343
+ if (existsSync5(spec.outFile)) {
3414
3344
  state = "untracked";
3415
3345
  reason = "file on disk, no provenance recorded";
3416
3346
  } else {
@@ -3442,12 +3372,15 @@ async function buildPlan(specs, lock, opts = {}) {
3442
3372
  } else if (entry.status !== "downloaded") {
3443
3373
  state = "in-flight";
3444
3374
  reason = `awaiting ${entry.status}`;
3445
- } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync6(currentEntryOutputPath(entry, spec, index)))) {
3375
+ } else if (entry.outputs.length === 0 || entry.outputs.some((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index)))) {
3446
3376
  state = "orphaned";
3447
- reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync6(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
3377
+ reason = entry.outputs.length === 0 ? "no recorded output files" : `missing on disk: ${entry.outputs.filter((_output, index) => !existsSync5(currentEntryOutputPath(entry, spec, index))).map((o) => o.role ?? "asset").join(", ")}`;
3448
3378
  } else if (await anyOutputModified(entry, spec)) {
3449
3379
  state = "orphaned";
3450
3380
  reason = "output modified since download";
3381
+ } else if (!postprocessCurrent(entry, spec)) {
3382
+ state = "recoverable";
3383
+ reason = spec.enforcePalette ? "palette enforcement turned on; run pixelkiln fetch to snap the file (no generation cost)" : "palette enforcement turned off; run pixelkiln fetch to restore the provider's bytes (no generation cost)";
3451
3384
  } else {
3452
3385
  state = "ok";
3453
3386
  reason = "up to date";
@@ -3607,12 +3540,12 @@ async function confirm(question, auto) {
3607
3540
  }
3608
3541
 
3609
3542
  // src/cli/commands/account.ts
3610
- import path21 from "path";
3611
- import { existsSync as existsSync16 } from "fs";
3543
+ import path20 from "path";
3544
+ import { existsSync as existsSync15 } from "fs";
3612
3545
 
3613
3546
  // src/env.ts
3614
- import { readFileSync, existsSync as existsSync7 } from "fs";
3615
- import path10 from "path";
3547
+ import { readFileSync, existsSync as existsSync6 } from "fs";
3548
+ import path9 from "path";
3616
3549
  function loadEnvFiles(dir) {
3617
3550
  const loaded = [];
3618
3551
  for (const [file, vars] of envFiles(dir)) {
@@ -3635,8 +3568,8 @@ function readEnvFiles(dir) {
3635
3568
  function envFiles(dir) {
3636
3569
  const out = [];
3637
3570
  for (const name of [".env.local", ".env"]) {
3638
- const file = path10.join(dir, name);
3639
- if (!existsSync7(file)) continue;
3571
+ const file = path9.join(dir, name);
3572
+ if (!existsSync6(file)) continue;
3640
3573
  try {
3641
3574
  out.push([file, parseEnv(readFileSync(file, "utf8"))]);
3642
3575
  } catch {
@@ -3665,8 +3598,8 @@ function parseEnv(contents) {
3665
3598
 
3666
3599
  // src/providers/comfyui.ts
3667
3600
  import { randomUUID as randomUUID2 } from "crypto";
3668
- import { readFile as readFile5 } from "fs/promises";
3669
- import path11 from "path";
3601
+ import { readFile as readFile4 } from "fs/promises";
3602
+ import path10 from "path";
3670
3603
 
3671
3604
  // src/http.ts
3672
3605
  var MAX_RETRIES = 4;
@@ -3853,10 +3786,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
3853
3786
  }
3854
3787
  async resolveOptions(value, context) {
3855
3788
  const options = parseOptions(value);
3856
- const workflowPath = path11.resolve(context.root, options.workflowFile);
3789
+ const workflowPath = path10.resolve(context.root, options.workflowFile);
3857
3790
  let raw;
3858
3791
  try {
3859
- raw = await readFile5(workflowPath, "utf8");
3792
+ raw = await readFile4(workflowPath, "utf8");
3860
3793
  } catch (err) {
3861
3794
  throw new Error(
3862
3795
  `ComfyUI workflow for style "${context.styleId}" could not be read at ${workflowPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -3908,10 +3841,10 @@ var ComfyUIProvider = class _ComfyUIProvider {
3908
3841
  if (typeof item !== "string") {
3909
3842
  throw new Error(`ComfyUI ${label} must be a manifest-relative PNG or JPEG path`);
3910
3843
  }
3911
- const file = path11.resolve(context.root, item);
3844
+ const file = path10.resolve(context.root, item);
3912
3845
  let bytes;
3913
3846
  try {
3914
- bytes = await readFile5(file);
3847
+ bytes = await readFile4(file);
3915
3848
  } catch (error) {
3916
3849
  throw new Error(
3917
3850
  `ComfyUI ${label} could not be read at ${file}: ${error instanceof Error ? error.message : String(error)}`
@@ -4043,7 +3976,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4043
3976
  setBinding(workflow2, binding, input);
4044
3977
  return;
4045
3978
  }
4046
- const bytes = await readFile5(input.path);
3979
+ const bytes = await readFile4(input.path);
4047
3980
  if (sha256(bytes) !== input.sha256) {
4048
3981
  throw new Error(`ComfyUI provider input "${name}" changed before upload`);
4049
3982
  }
@@ -4110,7 +4043,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4110
4043
  if (!spec.revision.sourceSha256 || !spec.revision.sourceFormat) {
4111
4044
  throw new Error("ComfyUI revision source is not ready");
4112
4045
  }
4113
- const sourceBytes = await readFile5(spec.revision.sourceFile);
4046
+ const sourceBytes = await readFile4(spec.revision.sourceFile);
4114
4047
  if (sha256(sourceBytes) !== spec.revision.sourceSha256) {
4115
4048
  throw new Error("ComfyUI revision source changed before upload");
4116
4049
  }
@@ -4124,7 +4057,7 @@ var ComfyUIProvider = class _ComfyUIProvider {
4124
4057
  if (!spec.revision.maskSha256 || !spec.revision.maskFormat) {
4125
4058
  throw new Error("ComfyUI revision mask is not ready");
4126
4059
  }
4127
- const maskBytes = await readFile5(spec.revision.maskFile);
4060
+ const maskBytes = await readFile4(spec.revision.maskFile);
4128
4061
  if (sha256(maskBytes) !== spec.revision.maskSha256) {
4129
4062
  throw new Error("ComfyUI revision mask changed before upload");
4130
4063
  }
@@ -4595,10 +4528,10 @@ function canonical2(value) {
4595
4528
  }
4596
4529
 
4597
4530
  // src/providers/pixellab.ts
4598
- import { mkdirSync, existsSync as existsSync8, readFileSync as readFileSync2, writeFileSync } from "fs";
4531
+ import { mkdirSync, existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
4599
4532
  import { randomUUID as randomUUID3 } from "crypto";
4600
4533
  import os from "os";
4601
- import path12 from "path";
4534
+ import path11 from "path";
4602
4535
 
4603
4536
  // src/client.ts
4604
4537
  import { z as z2 } from "zod";
@@ -4950,7 +4883,7 @@ var PixelLabProvider = class _PixelLabProvider {
4950
4883
  * filename, polling is an existence check, and downloading is a file read.
4951
4884
  */
4952
4885
  static cacheDir() {
4953
- const dir = path12.join(os.tmpdir(), "pixelkiln-pixflux");
4886
+ const dir = path11.join(os.tmpdir(), "pixelkiln-pixflux");
4954
4887
  mkdirSync(dir, { recursive: true });
4955
4888
  return dir;
4956
4889
  }
@@ -5019,7 +4952,7 @@ var PixelLabProvider = class _PixelLabProvider {
5019
4952
  seed: spec.seed
5020
4953
  });
5021
4954
  const jobId = randomUUID3();
5022
- writeFileSync(path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
4955
+ writeFileSync(path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`), png);
5023
4956
  return { jobId };
5024
4957
  }
5025
4958
  if (spec.generator === "tiles") {
@@ -5060,8 +4993,8 @@ var PixelLabProvider = class _PixelLabProvider {
5060
4993
  }
5061
4994
  async poll(jobId, generator, context) {
5062
4995
  if (generator === "pixflux") {
5063
- const file = path12.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
5064
- if (existsSync8(file)) {
4996
+ const file = path11.join(_PixelLabProvider.cacheDir(), `${jobId}.png`);
4997
+ if (existsSync7(file)) {
5065
4998
  const sourceUrl = `file://${file}`;
5066
4999
  return { status: "ready", objectId: jobId, sourceUrl, sources: [{ url: sourceUrl }] };
5067
5000
  }
@@ -6114,13 +6047,13 @@ registerProvider({
6114
6047
  });
6115
6048
 
6116
6049
  // src/manifest.ts
6117
- import { readFile as readFile6 } from "fs/promises";
6118
- import { existsSync as existsSync9 } from "fs";
6119
- import path13 from "path";
6050
+ import { readFile as readFile5 } from "fs/promises";
6051
+ import { existsSync as existsSync8 } from "fs";
6052
+ import path12 from "path";
6120
6053
  async function loadManifest(manifestPath) {
6121
- const abs = path13.resolve(manifestPath);
6122
- if (!existsSync9(abs)) throw new ProjectError(`No manifest at ${abs}`);
6123
- const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
6054
+ const abs = path12.resolve(manifestPath);
6055
+ if (!existsSync8(abs)) throw new ProjectError(`No manifest at ${abs}`);
6056
+ const input = ManifestInputSchema.safeParse(JSON.parse(await readFile5(abs, "utf8")));
6124
6057
  if (!input.success) {
6125
6058
  const issues = formatManifestIssues(input.error.issues);
6126
6059
  throw new ProjectError(`Manifest at ${abs} is invalid:
@@ -6191,7 +6124,7 @@ ${issues}`);
6191
6124
  throw new ProjectError(`Manifest at ${abs} is invalid:
6192
6125
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
6193
6126
  }
6194
- return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
6127
+ return { manifest: parsed.data, root: path12.dirname(abs), path: abs };
6195
6128
  }
6196
6129
  function formatManifestIssues(issues) {
6197
6130
  return issues.flatMap((issue) => {
@@ -6288,11 +6221,11 @@ async function resolveSpecs(loaded, filter) {
6288
6221
  }
6289
6222
  const styleImageCache = /* @__PURE__ */ new Map();
6290
6223
  async function loadStyleImage(rel) {
6291
- const abs = path13.resolve(root, rel);
6224
+ const abs = path12.resolve(root, rel);
6292
6225
  let hit = styleImageCache.get(abs);
6293
6226
  if (!hit) {
6294
- if (!existsSync9(abs)) throw new Error(`Style image not found: ${abs}`);
6295
- const buf = await readFile6(abs);
6227
+ if (!existsSync8(abs)) throw new Error(`Style image not found: ${abs}`);
6228
+ const buf = await readFile5(abs);
6296
6229
  const metadata = imageMetadata(buf);
6297
6230
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${abs}`);
6298
6231
  if (metadata.width < 1 || metadata.height < 1) {
@@ -6318,7 +6251,7 @@ async function resolveSpecs(loaded, filter) {
6318
6251
  styleImageDimensions.push(loadedImage);
6319
6252
  }
6320
6253
  const resolvedImages = style.styleImages.map((image) => {
6321
- const hit = styleImageCache.get(path13.resolve(root, image.path));
6254
+ const hit = styleImageCache.get(path12.resolve(root, image.path));
6322
6255
  return { base64: hit.base64, width: hit.width, height: hit.height, format: hit.format };
6323
6256
  });
6324
6257
  const styleSpecs = /* @__PURE__ */ new Map();
@@ -6363,9 +6296,9 @@ async function resolveSpecs(loaded, filter) {
6363
6296
  }
6364
6297
  const subject = asset.promptByStyle[styleId] ?? asset.prompt;
6365
6298
  const prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
6366
- const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
6367
- const outFile = path13.resolve(root, style.outDir, relFile);
6368
- const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
6299
+ const relFile = asset.file ?? path12.join(asset.category ?? "", `${assetId}.png`);
6300
+ const outFile = path12.resolve(root, style.outDir, relFile);
6301
+ const qualityOutFile = style.quality ? path12.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
6369
6302
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
6370
6303
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
6371
6304
  const base = {
@@ -6386,6 +6319,7 @@ async function resolveSpecs(loaded, filter) {
6386
6319
  detail: style.detail,
6387
6320
  seed: style.seed,
6388
6321
  palette: style.palette,
6322
+ enforcePalette: style.enforcePalette,
6389
6323
  noBackground: style.noBackground,
6390
6324
  tileSize: generator === "tiles" ? tileSize : void 0,
6391
6325
  tileType: generator === "tiles" ? style.tileType : void 0,
@@ -6416,7 +6350,7 @@ async function resolveSpecs(loaded, filter) {
6416
6350
  minGridConfidence: style.quality.minGridConfidence,
6417
6351
  ...style.quality.minTransparency == null ? {} : { minTransparency: style.quality.minTransparency },
6418
6352
  ...style.quality.fixerRevision ? { fixerRevision: style.quality.fixerRevision } : {},
6419
- ...style.quality.fixerPython ? { fixerPython: path13.resolve(root, style.quality.fixerPython) } : {},
6353
+ ...style.quality.fixerPython ? { fixerPython: path12.resolve(root, style.quality.fixerPython) } : {},
6420
6354
  ...style.quality.fps == null ? {} : { fps: style.quality.fps }
6421
6355
  }
6422
6356
  } : {},
@@ -6445,9 +6379,9 @@ async function resolveSpecs(loaded, filter) {
6445
6379
  );
6446
6380
  }
6447
6381
  const sourceSpec = await finalize(asset.revision.from);
6448
- const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path13.resolve(root, sourceSpec.source) : sourceSpec.outFile);
6382
+ const sourceFile = sourceSpec.quality?.outFile ?? (sourceSpec.source ? path12.resolve(root, sourceSpec.source) : sourceSpec.outFile);
6449
6383
  const sourceImage = await optionalRevisionImage(sourceFile, "revision source");
6450
- const maskFile = asset.revision.mask ? path13.resolve(root, asset.revision.mask) : void 0;
6384
+ const maskFile = asset.revision.mask ? path12.resolve(root, asset.revision.mask) : void 0;
6451
6385
  const maskImage = maskFile ? await optionalRevisionImage(maskFile, "revision mask", "png") : null;
6452
6386
  if (sourceImage && maskImage && (sourceImage.width !== maskImage.width || sourceImage.height !== maskImage.height)) {
6453
6387
  throw new Error(
@@ -6528,12 +6462,12 @@ async function resolveSpecs(loaded, filter) {
6528
6462
  return specs;
6529
6463
  }
6530
6464
  function pngPath(file) {
6531
- const extension = path13.extname(file);
6465
+ const extension = path12.extname(file);
6532
6466
  return extension ? `${file.slice(0, -extension.length)}.png` : `${file}.png`;
6533
6467
  }
6534
6468
  async function optionalRevisionImage(file, label, requiredFormat) {
6535
- if (!existsSync9(file)) return null;
6536
- const bytes = await readFile6(file);
6469
+ if (!existsSync8(file)) return null;
6470
+ const bytes = await readFile5(file);
6537
6471
  const metadata = imageMetadata(bytes);
6538
6472
  if (!metadata) throw new Error(`${label} is not a readable PNG or JPEG: ${file}`);
6539
6473
  if (requiredFormat && metadata.format !== requiredFormat) {
@@ -6546,8 +6480,8 @@ async function resolveStyleImages(loaded, styleId) {
6546
6480
  if (!style) throw new Error(`Unknown style "${styleId}"`);
6547
6481
  const out = [];
6548
6482
  for (const img of style.styleImages) {
6549
- const file = path13.resolve(loaded.root, img.path);
6550
- const buf = await readFile6(file);
6483
+ const file = path12.resolve(loaded.root, img.path);
6484
+ const buf = await readFile5(file);
6551
6485
  const metadata = imageMetadata(buf);
6552
6486
  if (!metadata) throw new Error(`Style image is not a readable PNG or JPEG: ${file}`);
6553
6487
  if (metadata.width < 1 || metadata.height < 1) {
@@ -6587,9 +6521,9 @@ function imageMetadata(buf) {
6587
6521
  }
6588
6522
 
6589
6523
  // src/pipeline/fetch.ts
6590
- import { existsSync as existsSync10, readFileSync as readFileSync3 } from "fs";
6591
- import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
6592
- import path14 from "path";
6524
+ import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
6525
+ import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
6526
+ import path13 from "path";
6593
6527
 
6594
6528
  // src/source-url.ts
6595
6529
  var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
@@ -6635,18 +6569,24 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6635
6569
  const result = { downloaded: 0, skipped: 0, failed: 0, ...opts.refresh ? { unchanged: 0 } : {} };
6636
6570
  const specByKey = new Map(specs.map((s) => [lockKey(s.styleId, s.assetId), s]));
6637
6571
  normalizeLockOutputPaths(lock, specs);
6638
- const cacheDir = opts.cacheDir === false ? null : path14.resolve(opts.cacheDir ?? path14.join(path14.dirname(lockPath), ".pixelkiln", "cache"));
6572
+ const cacheDir = opts.cacheDir === false ? null : path13.resolve(opts.cacheDir ?? path13.join(path13.dirname(lockPath), ".pixelkiln", "cache"));
6639
6573
  const pending = Object.entries(lock.entries).filter(([key, e]) => {
6640
6574
  if (e.provider !== provider.id || !specByKey.has(key)) return false;
6641
6575
  if (e.status === "selected" || e.status === "download-failed") return true;
6642
6576
  if (e.status !== "downloaded") return false;
6643
6577
  if (opts.refresh) return e.outputs.length > 0 && Boolean(e.sourceUrls?.length || e.sourceUrl);
6644
- if (!opts.repair) return false;
6645
6578
  const spec = specByKey.get(key);
6579
+ if (spec && e.specHash === spec.specHash && e.outputs.length && !postprocessCurrent(e, spec)) {
6580
+ return Boolean(opts.force) || e.outputs.every((output) => {
6581
+ const file = resolveOutputPath(output.path, spec.root);
6582
+ return existsSync9(file) && sha256(readFileSync3(file)) === output.sha256;
6583
+ });
6584
+ }
6585
+ if (!opts.repair) return false;
6646
6586
  if (e.outputs.length === 0 || !spec) return true;
6647
6587
  return e.outputs.some((output) => {
6648
6588
  const file = resolveOutputPath(output.path, spec.root);
6649
- if (!existsSync10(file)) return true;
6589
+ if (!existsSync9(file)) return true;
6650
6590
  const current = sha256(readFileSync3(file));
6651
6591
  if (current === output.sha256) return false;
6652
6592
  return Boolean(opts.force) || (e.supersededOutputs ?? []).some((old) => old.sha256 === current);
@@ -6681,6 +6621,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6681
6621
  }
6682
6622
  const outputs = [];
6683
6623
  let wrote = false;
6624
+ const wanted = postprocessFor(spec);
6625
+ const reapply = entry.status === "downloaded" && !postprocessCurrent(entry, spec);
6684
6626
  try {
6685
6627
  for (let index = 0; index < sources.length; index++) {
6686
6628
  const source = sources[index];
@@ -6688,24 +6630,28 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6688
6630
  (o) => source.role ? o.role === source.role : !o.role && sources.length === 1
6689
6631
  );
6690
6632
  const target = recorded ? resolveOutputPath(recorded.path, spec.root) : expectedOutputPath(spec, source.role, index, sources.length, source.mediaType);
6633
+ const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
6634
+ const recordedRaw = recorded?.raw ?? recorded?.sha256;
6691
6635
  let fresh = null;
6692
6636
  if (opts.refresh) {
6693
6637
  if (!source.url) throw new Error(`no durable source URL to refresh ${source.role ?? "asset"} from`);
6694
6638
  fresh = await provider.download(source.url);
6695
- if (recorded && sha256(fresh) === recorded.sha256) {
6696
- log2(` current ${path14.relative(process.cwd(), target)}`);
6639
+ if (recorded && sha256(fresh) === recordedRaw && !reapply) {
6640
+ log2(` current ${path13.relative(process.cwd(), target)}`);
6697
6641
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
6698
6642
  continue;
6699
6643
  }
6700
6644
  }
6701
- if (existsSync10(target)) {
6645
+ let raw = null;
6646
+ let final = null;
6647
+ if (existsSync9(target)) {
6702
6648
  const currentHash = await sha256File(target);
6703
6649
  if (recorded && currentHash === recorded.sha256) {
6704
- if (!fresh) {
6650
+ if (!fresh && !reapply) {
6705
6651
  if (cacheDir) {
6706
6652
  await cacheMedia(
6707
6653
  cacheDir,
6708
- await readFile7(target),
6654
+ await readFile6(target),
6709
6655
  recorded.mediaType ?? MediaType.PNG,
6710
6656
  recorded.sha256
6711
6657
  );
@@ -6713,7 +6659,12 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6713
6659
  outputs.push({ ...recorded, path: portableOutputPath(target, spec.root) });
6714
6660
  continue;
6715
6661
  }
6716
- log2(` changed ${path14.relative(process.cwd(), target)} (upstream)`);
6662
+ if (reapply && !fresh) {
6663
+ if (!recorded.raw) raw = await readFile6(target);
6664
+ log2(` reapply ${path13.relative(process.cwd(), target)}`);
6665
+ } else {
6666
+ log2(` changed ${path13.relative(process.cwd(), target)} (upstream)`);
6667
+ }
6717
6668
  } else {
6718
6669
  const superseded = (entry.supersededOutputs ?? []).find(
6719
6670
  (output, oldIndex, all) => currentOutputPath(output, spec, oldIndex, all.length) === target
@@ -6731,45 +6682,61 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6731
6682
  }
6732
6683
  }
6733
6684
  log2(
6734
- ` replace ${path14.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
6685
+ ` replace ${path13.relative(process.cwd(), target)}` + (opts.force ? " (--force)" : " (previous tracked generation)")
6735
6686
  );
6736
6687
  }
6737
6688
  }
6738
- const expectedMediaType = source.mediaType ?? recorded?.mediaType ?? MediaType.PNG;
6739
- let buf = fresh ?? (recorded && cacheDir ? await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType) : null);
6740
- if (buf) {
6741
- log2(` cached ${path14.relative(process.cwd(), target)}`);
6742
- } else {
6689
+ if (fresh) {
6690
+ raw = fresh;
6691
+ } else if (!raw && recorded && cacheDir) {
6692
+ if (!reapply) final = await readCachedMedia(cacheDir, recorded.sha256, expectedMediaType);
6693
+ if (!final && recordedRaw) raw = await readCachedMedia(cacheDir, recordedRaw, expectedMediaType);
6694
+ if (final || raw) log2(` cached ${path13.relative(process.cwd(), target)}`);
6695
+ }
6696
+ if (!final && !raw) {
6743
6697
  if (!source.url) {
6744
6698
  throw new Error(`no source URL or cached bytes remain for ${source.role ?? "asset"}`);
6745
6699
  }
6746
- buf = await provider.download(source.url);
6700
+ raw = await provider.download(source.url);
6747
6701
  }
6748
6702
  let mediaType;
6703
+ const bytes = final ?? raw;
6749
6704
  try {
6750
- mediaType = validateMedia(buf, expectedMediaType);
6705
+ mediaType = validateMedia(bytes, expectedMediaType);
6751
6706
  } catch (err) {
6752
6707
  const label = expectedMediaType === MediaType.GIF ? "GIF" : "PNG";
6753
- const mismatch = detectMediaType(buf) !== expectedMediaType;
6708
+ const mismatch = detectMediaType(bytes) !== expectedMediaType;
6754
6709
  throw new Error(
6755
- `response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${buf.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
6710
+ `response for ${source.role ?? "asset"} was not ${mismatch ? "a" : "a valid"} ${label}` + (mismatch ? ` (${bytes.length} bytes)` : `: ${err instanceof Error ? err.message : String(err)}`)
6756
6711
  );
6757
6712
  }
6713
+ let rawHash = final ? recorded?.raw : void 0;
6714
+ let buf = bytes;
6715
+ if (!final) {
6716
+ if (cacheDir) await cacheMedia(cacheDir, raw, mediaType);
6717
+ const processed = applyPostprocess(raw, mediaType, wanted);
6718
+ buf = processed.bytes;
6719
+ if (processed.changed) {
6720
+ rawHash = sha256(raw);
6721
+ log2(` snapped ${path13.relative(process.cwd(), target)} to ${wanted.palette.colors.length} colours`);
6722
+ }
6723
+ }
6758
6724
  if (cacheDir) await cacheMedia(cacheDir, buf, mediaType);
6759
- await mkdir3(path14.dirname(target), { recursive: true });
6725
+ await mkdir3(path13.dirname(target), { recursive: true });
6760
6726
  const tmp = `${target}.pixelkiln.tmp`;
6761
6727
  await writeFile3(tmp, buf);
6762
6728
  outputs.push({
6763
6729
  path: portableOutputPath(target, spec.root),
6764
6730
  sha256: sha256(buf),
6765
6731
  ...source.role ? { role: source.role } : {},
6766
- mediaType
6732
+ mediaType,
6733
+ ...rawHash ? { raw: rawHash } : {}
6767
6734
  });
6768
6735
  upsert(lock, key, { outputs: mergeOutputs(entry.outputs, outputs) });
6769
6736
  await saveLock(lockPath, lock);
6770
6737
  await rename3(tmp, target);
6771
6738
  wrote = true;
6772
- log2(` wrote ${path14.relative(process.cwd(), target)}`);
6739
+ log2(` wrote ${path13.relative(process.cwd(), target)}`);
6773
6740
  }
6774
6741
  if (opts.refresh && !wrote) {
6775
6742
  result.unchanged = (result.unchanged ?? 0) + 1;
@@ -6787,7 +6754,8 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6787
6754
  sourceUrls: persistentSources,
6788
6755
  downloadedAt: (/* @__PURE__ */ new Date()).toISOString(),
6789
6756
  error: null,
6790
- supersededOutputs: []
6757
+ supersededOutputs: [],
6758
+ postprocess: wanted
6791
6759
  });
6792
6760
  result.downloaded++;
6793
6761
  } catch (err) {
@@ -6808,10 +6776,10 @@ async function fetchAssets(provider, specs, lock, lockPath, opts = {}) {
6808
6776
  return result;
6809
6777
  }
6810
6778
  async function readCachedMedia(cacheDir, hash, mediaType) {
6811
- const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
6812
- if (!existsSync10(file)) return null;
6779
+ const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
6780
+ if (!existsSync9(file)) return null;
6813
6781
  try {
6814
- const buf = await readFile7(file);
6782
+ const buf = await readFile6(file);
6815
6783
  if (sha256(buf) !== hash) return null;
6816
6784
  validateMedia(buf, mediaType);
6817
6785
  return buf;
@@ -6822,7 +6790,7 @@ async function readCachedMedia(cacheDir, hash, mediaType) {
6822
6790
  async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
6823
6791
  const hash = knownHash ?? sha256(buf);
6824
6792
  validateMedia(buf, mediaType);
6825
- const file = path14.join(cacheDir, cacheFileName(hash, mediaType));
6793
+ const file = path13.join(cacheDir, cacheFileName(hash, mediaType));
6826
6794
  if (await readCachedMedia(cacheDir, hash, mediaType)) return;
6827
6795
  await mkdir3(cacheDir, { recursive: true });
6828
6796
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -6830,7 +6798,7 @@ async function cacheMedia(cacheDir, buf, mediaType, knownHash) {
6830
6798
  try {
6831
6799
  await rename3(tmp, file);
6832
6800
  } catch (err) {
6833
- if (!existsSync10(file)) throw err;
6801
+ if (!existsSync9(file)) throw err;
6834
6802
  } finally {
6835
6803
  await rm4(tmp, { force: true });
6836
6804
  }
@@ -6863,13 +6831,13 @@ async function pushTags(provider, specs, lock, opts = {}) {
6863
6831
  }
6864
6832
 
6865
6833
  // src/pipeline/adopt.ts
6866
- import { existsSync as existsSync12 } from "fs";
6867
- import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
6868
- import path15 from "path";
6834
+ import { existsSync as existsSync11 } from "fs";
6835
+ import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
6836
+ import path14 from "path";
6869
6837
 
6870
6838
  // src/cache.ts
6871
- import { mkdir as mkdir4, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
6872
- import { existsSync as existsSync11 } from "fs";
6839
+ import { mkdir as mkdir4, readFile as readFile7, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
6840
+ import { existsSync as existsSync10 } from "fs";
6873
6841
  import pathModule from "path";
6874
6842
  import { z as z3 } from "zod";
6875
6843
  var HashCacheSchema = z3.object({
@@ -6885,9 +6853,9 @@ function parseCache(value) {
6885
6853
  return HashCacheSchema.parse(value);
6886
6854
  }
6887
6855
  async function loadCache(path43) {
6888
- if (!existsSync11(path43)) return { version: 1, hashes: {} };
6856
+ if (!existsSync10(path43)) return { version: 1, hashes: {} };
6889
6857
  try {
6890
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path43, "utf8")));
6858
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile7(path43, "utf8")));
6891
6859
  if (!parsed.success) return { version: 1, hashes: {} };
6892
6860
  return {
6893
6861
  version: 1,
@@ -6941,11 +6909,11 @@ async function adopt(provider, specs, lock, lockPath, opts = {}) {
6941
6909
  const localByHash = /* @__PURE__ */ new Map();
6942
6910
  const unmatchedLocal = [];
6943
6911
  for (const spec of specs) {
6944
- if (!existsSync12(spec.outFile)) {
6912
+ if (!existsSync11(spec.outFile)) {
6945
6913
  unmatchedLocal.push(`${spec.styleId}/${spec.assetId} (no file at ${spec.outFile})`);
6946
6914
  continue;
6947
6915
  }
6948
- const bytes = await readFile9(spec.outFile);
6916
+ const bytes = await readFile8(spec.outFile);
6949
6917
  try {
6950
6918
  decodePng(bytes);
6951
6919
  } catch (err) {
@@ -7065,7 +7033,7 @@ async function writePromptsBack(manifestPath, lock, opts = {}) {
7065
7033
  const log2 = opts.onProgress ?? (() => {
7066
7034
  });
7067
7035
  const selectedAssets = opts.assetIds ? new Set(opts.assetIds) : null;
7068
- const raw = JSON.parse(await readFile9(manifestPath, "utf8"));
7036
+ const raw = JSON.parse(await readFile8(manifestPath, "utf8"));
7069
7037
  const best = /* @__PURE__ */ new Map();
7070
7038
  for (const entry of Object.values(lock.entries)) {
7071
7039
  if (opts.provider && entry.provider !== opts.provider) continue;
@@ -7098,19 +7066,19 @@ function formatUnmatchedRemote(objects, limit = 15) {
7098
7066
  }
7099
7067
 
7100
7068
  // src/pipeline/salvage.ts
7101
- import { readFile as readFile10 } from "fs/promises";
7102
- import { existsSync as existsSync13 } from "fs";
7103
- import path16 from "path";
7069
+ import { readFile as readFile9 } from "fs/promises";
7070
+ import { existsSync as existsSync12 } from "fs";
7071
+ import path15 from "path";
7104
7072
  function providerClaimId(provider, objectId) {
7105
7073
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
7106
7074
  }
7107
7075
  async function loadClaims(lockPaths, opts = {}) {
7108
7076
  const claimed = /* @__PURE__ */ new Set();
7109
7077
  for (const p of lockPaths) {
7110
- if (!existsSync13(p)) throw new Error(`Claim lockfile not found: ${p}`);
7078
+ if (!existsSync12(p)) throw new Error(`Claim lockfile not found: ${p}`);
7111
7079
  let parsed;
7112
7080
  try {
7113
- parsed = parseLock(JSON.parse(await readFile10(p, "utf8")));
7081
+ parsed = parseLock(JSON.parse(await readFile9(p, "utf8")));
7114
7082
  } catch {
7115
7083
  throw new Error(`Claim lockfile is malformed: ${p}`);
7116
7084
  }
@@ -7158,20 +7126,20 @@ function matchStyleByPattern(prompt, manifest) {
7158
7126
  return null;
7159
7127
  }
7160
7128
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
7161
- const own = path16.resolve(ownManifestPath);
7129
+ const own = path15.resolve(ownManifestPath);
7162
7130
  const siblingManifestPaths = [
7163
7131
  .../* @__PURE__ */ new Set([
7164
7132
  ...workspaceManifestPaths,
7165
- ...claimPaths.map((c) => path16.join(path16.dirname(path16.resolve(c)), "pixelkiln.manifest.json"))
7133
+ ...claimPaths.map((c) => path15.join(path15.dirname(path15.resolve(c)), "pixelkiln.manifest.json"))
7166
7134
  ])
7167
7135
  ];
7168
7136
  const siblings = [];
7169
7137
  for (const siblingManifestPath of siblingManifestPaths) {
7170
- if (path16.resolve(siblingManifestPath) === own) continue;
7171
- if (!existsSync13(siblingManifestPath)) continue;
7138
+ if (path15.resolve(siblingManifestPath) === own) continue;
7139
+ if (!existsSync12(siblingManifestPath)) continue;
7172
7140
  try {
7173
7141
  const { manifest } = await loadManifest(siblingManifestPath);
7174
- siblings.push({ label: path16.basename(path16.dirname(siblingManifestPath)), manifest });
7142
+ siblings.push({ label: path15.basename(path15.dirname(siblingManifestPath)), manifest });
7175
7143
  } catch {
7176
7144
  }
7177
7145
  }
@@ -7287,9 +7255,9 @@ async function applyTags(provider, decisions, existing, opts = {}) {
7287
7255
  }
7288
7256
 
7289
7257
  // src/workspace.ts
7290
- import { mkdir as mkdir5, readFile as readFile11, rename as rename5, rm as rm6, writeFile as writeFile6 } from "fs/promises";
7291
- import { existsSync as existsSync14 } from "fs";
7292
- import path17 from "path";
7258
+ import { mkdir as mkdir5, readFile as readFile10, rename as rename5, rm as rm6, writeFile as writeFile6 } from "fs/promises";
7259
+ import { existsSync as existsSync13 } from "fs";
7260
+ import path16 from "path";
7293
7261
  import { z as z4 } from "zod";
7294
7262
  var WorkspaceProjectSchema = z4.object({
7295
7263
  id: z4.string().min(1),
@@ -7314,10 +7282,10 @@ ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message
7314
7282
  );
7315
7283
  }
7316
7284
  async function loadWorkspace(workspacePath) {
7317
- if (!existsSync14(workspacePath)) return { version: 1, projects: [] };
7285
+ if (!existsSync13(workspacePath)) return { version: 1, projects: [] };
7318
7286
  let raw;
7319
7287
  try {
7320
- raw = JSON.parse(await readFile11(workspacePath, "utf8"));
7288
+ raw = JSON.parse(await readFile10(workspacePath, "utf8"));
7321
7289
  } catch (err) {
7322
7290
  throw new ProjectError(
7323
7291
  `Workspace catalog at ${workspacePath} is malformed:
@@ -7331,7 +7299,7 @@ async function saveWorkspace(workspacePath, ws) {
7331
7299
  version: 1,
7332
7300
  projects: [...ws.projects].sort((a, b) => a.id.localeCompare(b.id))
7333
7301
  };
7334
- await mkdir5(path17.dirname(path17.resolve(workspacePath)), { recursive: true });
7302
+ await mkdir5(path16.dirname(path16.resolve(workspacePath)), { recursive: true });
7335
7303
  const tmp = `${workspacePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
7336
7304
  try {
7337
7305
  await writeFile6(tmp, JSON.stringify(sorted, null, 2) + "\n");
@@ -7341,12 +7309,12 @@ async function saveWorkspace(workspacePath, ws) {
7341
7309
  }
7342
7310
  }
7343
7311
  function toPortablePath(dir, absolute) {
7344
- return path17.relative(dir, absolute).split(path17.sep).join("/");
7312
+ return path16.relative(dir, absolute).split(path16.sep).join("/");
7345
7313
  }
7346
7314
  function resolveProject(dir, project) {
7347
7315
  return {
7348
- manifestPath: path17.resolve(dir, project.manifest.split("/").join(path17.sep)),
7349
- lockPath: path17.resolve(dir, project.lock.split("/").join(path17.sep))
7316
+ manifestPath: path16.resolve(dir, project.manifest.split("/").join(path16.sep)),
7317
+ lockPath: path16.resolve(dir, project.lock.split("/").join(path16.sep))
7350
7318
  };
7351
7319
  }
7352
7320
  function validateWorkspace(ws, dir) {
@@ -7359,21 +7327,21 @@ function validateWorkspace(ws, dir) {
7359
7327
  const { manifestPath, lockPath } = resolveProject(dir, project);
7360
7328
  lockOwners.set(lockPath, [...lockOwners.get(lockPath) ?? [], project.id]);
7361
7329
  manifestOwners.set(manifestPath, [...manifestOwners.get(manifestPath) ?? [], project.id]);
7362
- if (path17.isAbsolute(project.manifest) || path17.isAbsolute(project.lock)) {
7330
+ if (path16.isAbsolute(project.manifest) || path16.isAbsolute(project.lock)) {
7363
7331
  diagnostics.push({
7364
7332
  id: "absolute-path",
7365
7333
  level: "warning",
7366
7334
  message: `project "${project.id}" stores an absolute path \u2014 the catalog will not resolve correctly if this tree is cloned or moved elsewhere`
7367
7335
  });
7368
7336
  }
7369
- if (!existsSync14(manifestPath)) {
7337
+ if (!existsSync13(manifestPath)) {
7370
7338
  diagnostics.push({
7371
7339
  id: "missing-manifest",
7372
7340
  level: "error",
7373
7341
  message: `project "${project.id}" manifest not found: ${manifestPath}`
7374
7342
  });
7375
7343
  }
7376
- if (!existsSync14(lockPath)) {
7344
+ if (!existsSync13(lockPath)) {
7377
7345
  diagnostics.push({
7378
7346
  id: "missing-lock",
7379
7347
  level: "error",
@@ -7420,8 +7388,8 @@ function validateWorkspace(ws, dir) {
7420
7388
  }
7421
7389
 
7422
7390
  // src/pick/salvage-server.ts
7423
- import { mkdir as mkdir6, writeFile as writeFile7, readFile as readFile13 } from "fs/promises";
7424
- import path18 from "path";
7391
+ import { mkdir as mkdir6, writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
7392
+ import path17 from "path";
7425
7393
 
7426
7394
  // src/pick/salvage-sheet.ts
7427
7395
  var escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -7581,7 +7549,7 @@ refresh();
7581
7549
 
7582
7550
  // src/pick/review-server.ts
7583
7551
  import { createServer } from "http";
7584
- import { readFile as readFile12 } from "fs/promises";
7552
+ import { readFile as readFile11 } from "fs/promises";
7585
7553
 
7586
7554
  // src/open.ts
7587
7555
  import { spawn } from "child_process";
@@ -7608,7 +7576,7 @@ function serveReviewPage(opts) {
7608
7576
  if (req.method === "GET" && req.url && opts.assets?.has(req.url)) {
7609
7577
  try {
7610
7578
  const asset = opts.assets.get(req.url);
7611
- const bytes = await readFile12(asset.path);
7579
+ const bytes = await readFile11(asset.path);
7612
7580
  res.writeHead(200, {
7613
7581
  "Content-Type": asset.contentType,
7614
7582
  "Cache-Control": "no-store",
@@ -7685,7 +7653,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7685
7653
  });
7686
7654
  const html = renderSalvageSheet(orphans, {
7687
7655
  styleId: ctx.styleId,
7688
- importDir: path18.relative(process.cwd(), ctx.importDir) || "."
7656
+ importDir: path17.relative(process.cwd(), ctx.importDir) || "."
7689
7657
  });
7690
7658
  const byId = new Map(orphans.map((o) => [o.id, o]));
7691
7659
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -7714,9 +7682,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7714
7682
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
7715
7683
  decodePng(buf);
7716
7684
  const assetId = idFromPrompt(orphan.prompt, taken);
7717
- const rel = path18.join("_salvaged", `${assetId}.png`);
7718
- const outFile = path18.resolve(ctx.importDir, rel);
7719
- await mkdir6(path18.dirname(outFile), { recursive: true });
7685
+ const rel = path17.join("_salvaged", `${assetId}.png`);
7686
+ const outFile = path17.resolve(ctx.importDir, rel);
7687
+ await mkdir6(path17.dirname(outFile), { recursive: true });
7720
7688
  await writeFile7(outFile, buf);
7721
7689
  ctx.manifest.assets[assetId] = {
7722
7690
  prompt: orphan.prompt,
@@ -7749,7 +7717,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7749
7717
  sourceUrl: durableSource,
7750
7718
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
7751
7719
  outputs: [{
7752
- path: portableOutputPath(outFile, path18.dirname(ctx.manifestPath)),
7720
+ path: portableOutputPath(outFile, path17.dirname(ctx.manifestPath)),
7753
7721
  sha256: sha256(buf)
7754
7722
  }],
7755
7723
  submittedAt: orphan.createdAt,
@@ -7773,7 +7741,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7773
7741
  }
7774
7742
  }
7775
7743
  await applyTags(provider, decisions, existingTags, { onProgress: log2 });
7776
- const raw = JSON.parse(await readFile13(ctx.manifestPath, "utf8"));
7744
+ const raw = JSON.parse(await readFile12(ctx.manifestPath, "utf8"));
7777
7745
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
7778
7746
  await writeFile7(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
7779
7747
  await saveLock(ctx.lockPath, ctx.lock);
@@ -7783,19 +7751,19 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
7783
7751
  }
7784
7752
 
7785
7753
  // src/cli/project.ts
7786
- import path20 from "path";
7787
- import { existsSync as existsSync15 } from "fs";
7754
+ import path19 from "path";
7755
+ import { existsSync as existsSync14 } from "fs";
7788
7756
 
7789
7757
  // src/project.ts
7790
- import path19 from "path";
7758
+ import path18 from "path";
7791
7759
  async function openProject(manifestPath = "pixelkiln.manifest.json", opts = {}) {
7792
- const absolute = path19.resolve(manifestPath);
7793
- const root = path19.dirname(absolute);
7760
+ const absolute = path18.resolve(manifestPath);
7761
+ const root = path18.dirname(absolute);
7794
7762
  if (opts.env !== false) {
7795
7763
  loadEnvFiles(root);
7796
- if (path19.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
7764
+ if (path18.resolve(process.cwd()) !== root) loadEnvFiles(process.cwd());
7797
7765
  }
7798
- const lockPath = path19.resolve(opts.lockPath ?? path19.join(root, "pixelkiln.lock.json"));
7766
+ const lockPath = path18.resolve(opts.lockPath ?? path18.join(root, "pixelkiln.lock.json"));
7799
7767
  const loaded = await loadManifest(absolute);
7800
7768
  const specs = await resolveSpecs(loaded, { styles: opts.styles ?? [], assets: opts.assets ?? [] });
7801
7769
  const lock = await loadLock(lockPath);
@@ -7926,11 +7894,11 @@ async function workspaceStatus(ws, dir) {
7926
7894
 
7927
7895
  // src/cli/project.ts
7928
7896
  async function provenanceFile(id, file) {
7929
- const absolute = path20.resolve(file);
7897
+ const absolute = path19.resolve(file);
7930
7898
  return {
7931
7899
  id,
7932
7900
  path: absolute,
7933
- sha256: existsSync15(absolute) ? await sha256File(absolute) : null,
7901
+ sha256: existsSync14(absolute) ? await sha256File(absolute) : null,
7934
7902
  included: true
7935
7903
  };
7936
7904
  }
@@ -8002,10 +7970,10 @@ function budgetsForPlan(plan, args) {
8002
7970
  return out;
8003
7971
  }
8004
7972
  async function requireCompleteWorkspaceClaims(workspacePath) {
8005
- if (!existsSync15(workspacePath)) {
7973
+ if (!existsSync14(workspacePath)) {
8006
7974
  throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
8007
7975
  }
8008
- const dir = path20.dirname(path20.resolve(workspacePath));
7976
+ const dir = path19.dirname(path19.resolve(workspacePath));
8009
7977
  const ws = await loadWorkspace(workspacePath);
8010
7978
  const diagnostics = validateWorkspace(ws, dir);
8011
7979
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -8028,9 +7996,9 @@ async function openAccountProject(args) {
8028
7996
  return project;
8029
7997
  }
8030
7998
  async function open(args) {
8031
- if (!existsSync15(path20.resolve(args.manifest))) {
7999
+ if (!existsSync14(path19.resolve(args.manifest))) {
8032
8000
  throw new ProjectError(
8033
- `No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
8001
+ `No manifest at ${path19.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
8034
8002
  );
8035
8003
  }
8036
8004
  const project = await openProject(args.manifest, { lockPath: args.lock, styles: args.styles, assets: args.assets });
@@ -8072,7 +8040,7 @@ function providerCache(mode2) {
8072
8040
 
8073
8041
  // src/cli/commands/account.ts
8074
8042
  async function runBalance(args) {
8075
- loadEnvFiles(path21.dirname(path21.resolve(args.manifest)));
8043
+ loadEnvFiles(path20.dirname(path20.resolve(args.manifest)));
8076
8044
  loadEnvFiles(process.cwd());
8077
8045
  const loaded = await loadManifest(args.manifest);
8078
8046
  const selectedProvider = accountProviderId(loaded.manifest, args.provider, "balance");
@@ -8119,12 +8087,12 @@ async function runAdopt(args) {
8119
8087
  tagged ${n} object(s) upstream`);
8120
8088
  }
8121
8089
  if (args.writePrompts) {
8122
- const { filled, stillEmpty } = await writePromptsBack(path21.resolve(args.manifest), lock, {
8090
+ const { filled, stillEmpty } = await writePromptsBack(path20.resolve(args.manifest), lock, {
8123
8091
  onProgress: log,
8124
8092
  provider: provider.id,
8125
8093
  assetIds: specs.map((spec) => spec.assetId)
8126
8094
  });
8127
- log(` recovered ${filled} prompt(s) into ${path21.relative(process.cwd(), args.manifest)}`);
8095
+ log(` recovered ${filled} prompt(s) into ${path20.relative(process.cwd(), args.manifest)}`);
8128
8096
  const reloaded = await loadManifest(args.manifest);
8129
8097
  const rebased = (await resolveSpecs(reloaded, {
8130
8098
  styles: args.styles,
@@ -8154,12 +8122,12 @@ async function runSalvage2(args) {
8154
8122
  const provider = providerFor(accountProvider);
8155
8123
  const jsonMode = args.dryRun && args.json;
8156
8124
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
8157
- const ownLock = path21.resolve(args.lock);
8125
+ const ownLock = path20.resolve(args.lock);
8158
8126
  let workspaceProjects = [];
8159
8127
  let workspaceDir = "";
8160
8128
  if (args.workspace) {
8161
- const workspacePath = path21.resolve(args.workspace);
8162
- workspaceDir = path21.dirname(workspacePath);
8129
+ const workspacePath = path20.resolve(args.workspace);
8130
+ workspaceDir = path20.dirname(workspacePath);
8163
8131
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
8164
8132
  workspaceProjects = complete.ws.projects;
8165
8133
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -8168,12 +8136,12 @@ async function runSalvage2(args) {
8168
8136
  const lockPaths = [
8169
8137
  .../* @__PURE__ */ new Set([
8170
8138
  ...workspaceLockPaths,
8171
- ...existsSync16(ownLock) ? [ownLock] : [],
8172
- ...args.claims.map((c) => path21.resolve(c))
8139
+ ...existsSync15(ownLock) ? [ownLock] : [],
8140
+ ...args.claims.map((c) => path20.resolve(c))
8173
8141
  ])
8174
8142
  ];
8175
8143
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
8176
- for (const p of lockPaths) diag(` ${path21.relative(process.cwd(), p)}`);
8144
+ for (const p of lockPaths) diag(` ${path20.relative(process.cwd(), p)}`);
8177
8145
  if (!args.claims.length && !args.workspace) {
8178
8146
  diag(
8179
8147
  `
@@ -8181,7 +8149,7 @@ async function runSalvage2(args) {
8181
8149
  pass every other project's lockfile via --claims a.json,b.json, or
8182
8150
  register every project in a workspace catalog and pass --workspace.`
8183
8151
  );
8184
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path21.resolve(args.manifest))) {
8152
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path20.resolve(args.manifest))) {
8185
8153
  diag(
8186
8154
  `
8187
8155
  This project's manifest is not registered in the workspace catalog. Its own
@@ -8265,7 +8233,7 @@ async function runSalvage2(args) {
8265
8233
  manifestPath: loaded.path,
8266
8234
  manifest: accountManifest,
8267
8235
  styleId,
8268
- importDir: path21.resolve(loaded.root, style.outDir),
8236
+ importDir: path20.resolve(loaded.root, style.outDir),
8269
8237
  lock,
8270
8238
  lockPath: args.lock
8271
8239
  },
@@ -8366,17 +8334,17 @@ async function runTag(args) {
8366
8334
  }
8367
8335
 
8368
8336
  // src/cli/commands/edit.ts
8369
- import path24 from "path";
8337
+ import path23 from "path";
8370
8338
 
8371
8339
  // src/pipeline/hand-edit.ts
8372
- import { existsSync as existsSync17 } from "fs";
8373
- import { copyFile, mkdir as mkdir7, readFile as readFile15, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
8374
- import path23 from "path";
8340
+ import { existsSync as existsSync16 } from "fs";
8341
+ import { copyFile, mkdir as mkdir7, readFile as readFile14, rename as rename7, rm as rm8, writeFile as writeFile9 } from "fs/promises";
8342
+ import path22 from "path";
8375
8343
  import { z as z6 } from "zod";
8376
8344
 
8377
8345
  // src/manifest-edit.ts
8378
- import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
8379
- import path22 from "path";
8346
+ import { readFile as readFile13, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
8347
+ import path21 from "path";
8380
8348
  import { z as z5 } from "zod";
8381
8349
  var HexSha = z5.string().regex(/^[0-9a-f]{64}$/);
8382
8350
  var AssetPatchSchema = z5.object({
@@ -8450,6 +8418,8 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
8450
8418
  promptSuffix: z5.string().optional(),
8451
8419
  /** `#rrggbb` values; null or empty clears the style's own palette. */
8452
8420
  palette: z5.array(z5.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
8421
+ /** Snap downloaded art to the palette; false clears the style's own value. */
8422
+ enforcePalette: z5.boolean().optional(),
8453
8423
  /** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
8454
8424
  view: z5.string().max(64).nullable().optional(),
8455
8425
  /** pixflux only; null clears the style's own value. */
@@ -8541,6 +8511,7 @@ function applyEdit(raw, edit) {
8541
8511
  const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
8542
8512
  setOrDelete(style, "palette", colors.length ? colors : null);
8543
8513
  }
8514
+ if (patch2.enforcePalette !== void 0) setOrDelete(style, "enforcePalette", patch2.enforcePalette || null);
8544
8515
  if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
8545
8516
  if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
8546
8517
  return;
@@ -8588,8 +8559,8 @@ function applyEdit(raw, edit) {
8588
8559
  }
8589
8560
  }
8590
8561
  async function applyManifestEdit(manifestPath, edit) {
8591
- const absolute = path22.resolve(manifestPath);
8592
- const original = await readFile14(absolute, "utf8");
8562
+ const absolute = path21.resolve(manifestPath);
8563
+ const original = await readFile13(absolute, "utf8");
8593
8564
  const before = sha256(original);
8594
8565
  if (before !== edit.expectedSha256) throw new ManifestDriftError(absolute);
8595
8566
  let raw;
@@ -8603,9 +8574,9 @@ async function applyManifestEdit(manifestPath, edit) {
8603
8574
  applyEdit(raw, edit);
8604
8575
  const next = serializeLike(original, raw);
8605
8576
  if (next === original) return { manifestPath: absolute, sha256: before, changed: false };
8606
- const tmp = path22.join(
8607
- path22.dirname(absolute),
8608
- `.${path22.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
8577
+ const tmp = path21.join(
8578
+ path21.dirname(absolute),
8579
+ `.${path21.basename(absolute)}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`
8609
8580
  );
8610
8581
  try {
8611
8582
  await writeFile8(tmp, next);
@@ -8627,11 +8598,11 @@ async function applyManifestEdit(manifestPath, edit) {
8627
8598
  var HAND_EDIT_DIR = "edits";
8628
8599
  function handEditPath(loaded, spec) {
8629
8600
  const style = loaded.manifest.styles[spec.styleId];
8630
- const outDir = path23.resolve(loaded.root, style?.outDir ?? ".");
8631
- const relative = path23.relative(outDir, spec.outFile);
8632
- const inside = relative && !relative.startsWith("..") && !path23.isAbsolute(relative);
8633
- const file = inside ? relative : path23.basename(spec.outFile);
8634
- return path23.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
8601
+ const outDir = path22.resolve(loaded.root, style?.outDir ?? ".");
8602
+ const relative = path22.relative(outDir, spec.outFile);
8603
+ const inside = relative && !relative.startsWith("..") && !path22.isAbsolute(relative);
8604
+ const file = inside ? relative : path22.basename(spec.outFile);
8605
+ return path22.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
8635
8606
  }
8636
8607
  function handEditBases(spec, lock) {
8637
8608
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -8644,10 +8615,10 @@ function handEditBases(spec, lock) {
8644
8615
  }));
8645
8616
  if (members.some((member) => member.mediaType && member.mediaType !== "image/png")) return null;
8646
8617
  if (entry.outputs.length === 1 && !primaryOutput(entry)) return null;
8647
- if (!members.every((member) => existsSync17(member.path))) return null;
8618
+ if (!members.every((member) => existsSync16(member.path))) return null;
8648
8619
  return members.map(({ role, path: file }) => ({ role, path: file }));
8649
8620
  }
8650
- return existsSync17(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
8621
+ return existsSync16(spec.outFile) ? [{ role: null, path: spec.outFile }] : null;
8651
8622
  }
8652
8623
  function handEditMembers(loaded, spec, lock, editPath) {
8653
8624
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
@@ -8673,18 +8644,18 @@ async function startHandEdit(loaded, lock, spec, opts = {}) {
8673
8644
  `${spec.styleId}/${spec.assetId} declares ${spec.source}, one committed file placed for the whole set; clear that source (pixelkiln edit detach) to hand-edit the members one by one`
8674
8645
  );
8675
8646
  }
8676
- const existing = spec.source ? path23.resolve(loaded.root, spec.source) : null;
8647
+ const existing = spec.source ? path22.resolve(loaded.root, spec.source) : null;
8677
8648
  const editPath = existing ?? handEditPath(loaded, spec);
8678
8649
  const members = handEditMembers(loaded, spec, lock, editPath);
8679
8650
  let created = false;
8680
8651
  for (const member of members) {
8681
- if (existsSync17(member.path)) continue;
8652
+ if (existsSync16(member.path)) continue;
8682
8653
  if (!member.base) {
8683
8654
  throw new ManifestEditError(
8684
8655
  `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
8685
8656
  );
8686
8657
  }
8687
- await mkdir7(path23.dirname(member.path), { recursive: true });
8658
+ await mkdir7(path22.dirname(member.path), { recursive: true });
8688
8659
  await copyFile(member.base, member.path);
8689
8660
  created = true;
8690
8661
  }
@@ -8763,7 +8734,7 @@ function handEditProjectPath(editPath) {
8763
8734
  async function readHandEditCompanion(editPath) {
8764
8735
  let text;
8765
8736
  try {
8766
- text = await readFile15(handEditCompanionPath(editPath), "utf8");
8737
+ text = await readFile14(handEditCompanionPath(editPath), "utf8");
8767
8738
  } catch {
8768
8739
  return null;
8769
8740
  }
@@ -8806,7 +8777,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
8806
8777
  }
8807
8778
  });
8808
8779
  const bases = handEditBases(spec, lock);
8809
- const expectedSize = bases?.[0] ? decodePng(await readFile15(bases[0].path)) : { width: spec.width, height: spec.height };
8780
+ const expectedSize = bases?.[0] ? decodePng(await readFile14(bases[0].path)) : { width: spec.width, height: spec.height };
8810
8781
  for (const [index, image] of decoded.entries()) {
8811
8782
  if (image.width !== expectedSize.width || image.height !== expectedSize.height) {
8812
8783
  const which = memberSet ? `member ${incoming[index].role ?? index}` : "the edit";
@@ -8822,7 +8793,7 @@ async function saveHandEdit(loaded, lock, spec, input) {
8822
8793
  editor: input.editor,
8823
8794
  protocol: input.protocol,
8824
8795
  savedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
8825
- project: projectPath ? path23.basename(projectPath) : null,
8796
+ project: projectPath ? path22.basename(projectPath) : null,
8826
8797
  outputs: incoming.map((frame, index) => ({
8827
8798
  role: frame.role,
8828
8799
  basedOn: basedOnHash(entry, index),
@@ -8882,7 +8853,7 @@ async function runEdit(args) {
8882
8853
  const set = started.members.length > 1;
8883
8854
  const kind = spec.generator === "frames" ? "frames" : "members";
8884
8855
  log(` ${started.created ? "created" : "found"} ${started.source}${set ? ` (${started.members.length} ${kind})` : ""}${started.declared ? " and declared it as the asset's source" : ""}`);
8885
- if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path24.relative(process.cwd(), member.path)}`);
8856
+ if (set) for (const member of started.members) log(` ${member.role ?? ""} ${path23.relative(process.cwd(), member.path)}`);
8886
8857
  if (args.noOpen) {
8887
8858
  log(` edit ${set ? "them" : "it"} with your own tool, then run pixelkiln plan; mount and pack place ${set ? "them" : "it"} in place of the generated art`);
8888
8859
  } else {
@@ -8892,17 +8863,17 @@ async function runEdit(args) {
8892
8863
  }
8893
8864
 
8894
8865
  // src/cli/commands/gallery.ts
8895
- import path31 from "path";
8896
- import { existsSync as existsSync23 } from "fs";
8866
+ import path30 from "path";
8867
+ import { existsSync as existsSync22 } from "fs";
8897
8868
 
8898
8869
  // src/gallery/snapshot.ts
8899
8870
  import { createHash as createHash3 } from "crypto";
8900
- import { existsSync as existsSync19 } from "fs";
8901
- import { readFile as readFile16, stat as stat2 } from "fs/promises";
8902
- import path27 from "path";
8871
+ import { existsSync as existsSync18 } from "fs";
8872
+ import { readFile as readFile15, stat as stat2 } from "fs/promises";
8873
+ import path26 from "path";
8903
8874
 
8904
8875
  // src/gallery/edit.ts
8905
- import path25 from "path";
8876
+ import path24 from "path";
8906
8877
  import { z as z7 } from "zod";
8907
8878
  var Base64 = z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
8908
8879
  var HandEditRequestSchema = z7.discriminatedUnion("action", [
@@ -8992,7 +8963,7 @@ function createGalleryEditHandler(opts) {
8992
8963
  }
8993
8964
  const result = await applyManifestEdit(manifestPath, parsed.data);
8994
8965
  if (result.changed) {
8995
- const relative = path25.relative(process.cwd(), result.manifestPath);
8966
+ const relative = path24.relative(process.cwd(), result.manifestPath);
8996
8967
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
8997
8968
  const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
8998
8969
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
@@ -9002,8 +8973,8 @@ function createGalleryEditHandler(opts) {
9002
8973
  }
9003
8974
 
9004
8975
  // src/pipeline/history.ts
9005
- import { existsSync as existsSync18 } from "fs";
9006
- import path26 from "path";
8976
+ import { existsSync as existsSync17 } from "fs";
8977
+ import path25 from "path";
9007
8978
  var HISTORY_ENV = "PIXELKILN_HISTORY";
9008
8979
  function historyLimit(manifest, env = process.env) {
9009
8980
  if (manifest?.history !== void 0) return manifest.history;
@@ -9035,6 +9006,7 @@ function retireGeneration(entry, retiredAt) {
9035
9006
  cost: entry.cost,
9036
9007
  costUnit: entry.costUnit,
9037
9008
  provider: entry.provider,
9009
+ postprocess: entry.postprocess,
9038
9010
  retiredAt
9039
9011
  };
9040
9012
  }
@@ -9047,9 +9019,13 @@ function historyAfterReplacing(previous, limit, retiredAt) {
9047
9019
  }
9048
9020
  function referencedHashes(lock) {
9049
9021
  const hashes = /* @__PURE__ */ new Set();
9022
+ const add = (output) => {
9023
+ hashes.add(output.sha256);
9024
+ if (output.raw) hashes.add(output.raw);
9025
+ };
9050
9026
  for (const entry of Object.values(lock.entries)) {
9051
- for (const output of entry.outputs) hashes.add(output.sha256);
9052
- for (const generation of entry.history ?? []) for (const output of generation.outputs) hashes.add(output.sha256);
9027
+ for (const output of entry.outputs) add(output);
9028
+ for (const generation of entry.history ?? []) for (const output of generation.outputs) add(output);
9053
9029
  }
9054
9030
  return hashes;
9055
9031
  }
@@ -9084,10 +9060,10 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
9084
9060
  }
9085
9061
  for (let i = 0; i < entry.outputs.length; i++) {
9086
9062
  const file = currentEntryOutputPath(entry, spec, i);
9087
- if (!existsSync18(file)) continue;
9063
+ if (!existsSync17(file)) continue;
9088
9064
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
9089
9065
  throw new OverwriteRefusedError(
9090
- `${path26.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
9066
+ `${path25.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
9091
9067
  [file]
9092
9068
  );
9093
9069
  }
@@ -9120,6 +9096,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
9120
9096
  cost: generation.cost,
9121
9097
  costUnit: generation.costUnit,
9122
9098
  provider: generation.provider,
9099
+ postprocess: generation.postprocess,
9123
9100
  history
9124
9101
  });
9125
9102
  await saveLock(lockPath, lock);
@@ -9138,7 +9115,7 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
9138
9115
  // src/gallery/snapshot.ts
9139
9116
  var PROJECT_FILE_TYPE = "application/octet-stream";
9140
9117
  function galleryMediaId(absolutePath) {
9141
- return createHash3("sha256").update(path27.resolve(absolutePath)).digest("hex").slice(0, 24);
9118
+ return createHash3("sha256").update(path26.resolve(absolutePath)).digest("hex").slice(0, 24);
9142
9119
  }
9143
9120
  function galleryMediaRoute(id) {
9144
9121
  return `/media/${id}`;
@@ -9157,9 +9134,9 @@ function describeHistory(media, entry, cacheDir) {
9157
9134
  return (entry.history ?? []).map((generation, i) => {
9158
9135
  const outputs = generation.outputs.map((output) => {
9159
9136
  const mediaType = output.mediaType ?? MediaType.PNG;
9160
- const file = path27.join(cacheDir, cacheFileName(output.sha256, mediaType));
9137
+ const file = path26.join(cacheDir, cacheFileName(output.sha256, mediaType));
9161
9138
  let url = null;
9162
- if (existsSync19(file)) {
9139
+ if (existsSync18(file)) {
9163
9140
  const id = galleryMediaId(file);
9164
9141
  media.set(id, { path: file, contentType: mediaType });
9165
9142
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -9188,7 +9165,7 @@ function describeHistory(media, entry, cacheDir) {
9188
9165
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
9189
9166
  async function samePixels(a, b) {
9190
9167
  try {
9191
- const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
9168
+ const [left, right] = await Promise.all([readFile15(a), readFile15(b)]);
9192
9169
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
9193
9170
  const x = decodePng(left);
9194
9171
  const y = decodePng(right);
@@ -9241,10 +9218,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
9241
9218
  async function describeQuality(media, root, inspection) {
9242
9219
  const outputPaths = inspection.outputs ?? [inspection.output];
9243
9220
  const outputs = await Promise.all(
9244
- outputPaths.map((file) => describeOutput(media, root, path27.resolve(root, file)))
9221
+ outputPaths.map((file) => describeOutput(media, root, path26.resolve(root, file)))
9245
9222
  );
9246
- const recordPath = path27.resolve(root, inspection.record);
9247
- const recordExists = existsSync19(recordPath);
9223
+ const recordPath = path26.resolve(root, inspection.record);
9224
+ const recordExists = existsSync18(recordPath);
9248
9225
  let options = null;
9249
9226
  let check = null;
9250
9227
  if (recordExists) {
@@ -9263,7 +9240,7 @@ async function describeQuality(media, root, inspection) {
9263
9240
  return {
9264
9241
  state: inspection.state,
9265
9242
  reason: inspection.reason,
9266
- output: portableOutputPath(path27.resolve(root, inspection.output), root),
9243
+ output: portableOutputPath(path26.resolve(root, inspection.output), root),
9267
9244
  record: portableOutputPath(recordPath, root),
9268
9245
  recordExists,
9269
9246
  outputs,
@@ -9300,9 +9277,9 @@ async function buildGallerySnapshot(opts) {
9300
9277
  const media = /* @__PURE__ */ new Map();
9301
9278
  const plan = await buildPlan(specs, lock);
9302
9279
  const items = [];
9303
- const manifestText = await readFile16(loaded.path, "utf8");
9280
+ const manifestText = await readFile15(loaded.path, "utf8");
9304
9281
  const manifestSha256 = sha256(manifestText);
9305
- const cacheDir = path27.resolve(path27.dirname(opts.lockPath), ".pixelkiln", "cache");
9282
+ const cacheDir = path26.resolve(path26.dirname(opts.lockPath), ".pixelkiln", "cache");
9306
9283
  let rawStyles = {};
9307
9284
  try {
9308
9285
  const raw = JSON.parse(manifestText);
@@ -9326,12 +9303,12 @@ async function buildGallerySnapshot(opts) {
9326
9303
  )
9327
9304
  );
9328
9305
  if (spec.source) {
9329
- const editPath = path27.resolve(root, spec.source);
9306
+ const editPath = path26.resolve(root, spec.source);
9330
9307
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
9331
9308
  const editShas = await Promise.all(
9332
9309
  Array.from({ length: memberCount }, async (_, index) => {
9333
9310
  const file = sourceOutputPath(spec.source, entry, index, root);
9334
- return { file, sha256: existsSync19(file) ? await sha256File(file) : null };
9311
+ return { file, sha256: existsSync18(file) ? await sha256File(file) : null };
9335
9312
  })
9336
9313
  );
9337
9314
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -9339,7 +9316,7 @@ async function buildGallerySnapshot(opts) {
9339
9316
  const companion = await readHandEditCompanion(editPath);
9340
9317
  if (companion) {
9341
9318
  const projectPath = handEditProjectPath(editPath);
9342
- const project = companion.project && existsSync19(projectPath) ? await fileInfo(projectPath) : null;
9319
+ const project = companion.project && existsSync18(projectPath) ? await fileInfo(projectPath) : null;
9343
9320
  let projectUrl = null;
9344
9321
  if (project) {
9345
9322
  const id = galleryMediaId(projectPath);
@@ -9375,8 +9352,8 @@ async function buildGallerySnapshot(opts) {
9375
9352
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
9376
9353
  }
9377
9354
  } else if (spec.source) {
9378
- outputs = [await describeOutput(media, root, path27.resolve(root, spec.source))];
9379
- } else if (!entry && existsSync19(spec.outFile)) {
9355
+ outputs = [await describeOutput(media, root, path26.resolve(root, spec.source))];
9356
+ } else if (!entry && existsSync18(spec.outFile)) {
9380
9357
  outputs = [await describeOutput(media, root, spec.outFile)];
9381
9358
  } else {
9382
9359
  outputs = [];
@@ -9418,6 +9395,7 @@ async function buildGallerySnapshot(opts) {
9418
9395
  outputs,
9419
9396
  quality,
9420
9397
  providerMetadata: entry?.providerMetadata ?? {},
9398
+ postprocess: entry?.postprocess ?? null,
9421
9399
  asset,
9422
9400
  source: spec.source ?? null,
9423
9401
  edit,
@@ -9478,6 +9456,7 @@ async function buildGallerySnapshot(opts) {
9478
9456
  outputs,
9479
9457
  quality: null,
9480
9458
  providerMetadata: entry.providerMetadata ?? {},
9459
+ postprocess: entry.postprocess ?? null,
9481
9460
  asset: null,
9482
9461
  source: null,
9483
9462
  edit: null,
@@ -9526,6 +9505,7 @@ async function buildGallerySnapshot(opts) {
9526
9505
  promptPrefix: style?.promptPrefix ?? "",
9527
9506
  promptSuffix: style?.promptSuffix ?? "",
9528
9507
  palette: style?.palette ?? [],
9508
+ enforcePalette: style?.enforcePalette ?? false,
9529
9509
  view: style?.view ?? null,
9530
9510
  noBackground: style?.noBackground ?? true,
9531
9511
  quality: Boolean(style?.quality),
@@ -9555,7 +9535,7 @@ async function buildGallerySnapshot(opts) {
9555
9535
  id: loaded.manifest.name,
9556
9536
  name: loaded.manifest.name,
9557
9537
  manifest: loaded.path,
9558
- lock: path27.resolve(opts.lockPath),
9538
+ lock: path26.resolve(opts.lockPath),
9559
9539
  root,
9560
9540
  provider: loaded.manifest.provider,
9561
9541
  account: null,
@@ -9581,7 +9561,7 @@ async function buildGallerySnapshot(opts) {
9581
9561
  return { snapshot, media };
9582
9562
  }
9583
9563
  async function buildWorkspaceGallerySnapshot(opts) {
9584
- const dir = path27.dirname(path27.resolve(opts.workspacePath));
9564
+ const dir = path26.dirname(path26.resolve(opts.workspacePath));
9585
9565
  const media = /* @__PURE__ */ new Map();
9586
9566
  const projects = [];
9587
9567
  const items = [];
@@ -9624,11 +9604,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
9624
9604
  id: project.id,
9625
9605
  name: loaded.manifest.name,
9626
9606
  manifest: manifestPath,
9627
- lock: path27.resolve(lockPath),
9607
+ lock: path26.resolve(lockPath),
9628
9608
  root: loaded.root,
9629
9609
  provider: loaded.manifest.provider,
9630
9610
  account: project.account ?? null,
9631
- manifestSha256: manifestSha256 ?? sha256(await readFile16(manifestPath, "utf8")),
9611
+ manifestSha256: manifestSha256 ?? sha256(await readFile15(manifestPath, "utf8")),
9632
9612
  historyLimit: historyLimit(loaded.manifest),
9633
9613
  entries: Object.keys(lock.entries).length,
9634
9614
  items: projectItems.length,
@@ -9640,8 +9620,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
9640
9620
  id: project.id,
9641
9621
  name: project.id,
9642
9622
  manifest: manifestPath,
9643
- lock: path27.resolve(lockPath),
9644
- root: path27.dirname(manifestPath),
9623
+ lock: path26.resolve(lockPath),
9624
+ root: path26.dirname(manifestPath),
9645
9625
  provider: project.provider,
9646
9626
  account: project.account ?? null,
9647
9627
  manifestSha256: null,
@@ -9668,7 +9648,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
9668
9648
  version: 1,
9669
9649
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
9670
9650
  project: null,
9671
- workspace: { path: path27.resolve(opts.workspacePath), dir, projects },
9651
+ workspace: { path: path26.resolve(opts.workspacePath), dir, projects },
9672
9652
  filter,
9673
9653
  totals: {
9674
9654
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -9687,7 +9667,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
9687
9667
  // src/gallery/server.ts
9688
9668
  import { randomBytes } from "crypto";
9689
9669
  import { createServer as createServer2 } from "http";
9690
- import { readFile as readFile17 } from "fs/promises";
9670
+ import { readFile as readFile16 } from "fs/promises";
9691
9671
 
9692
9672
  // src/gallery/page.ts
9693
9673
  import { readFileSync as readFileSync4 } from "fs";
@@ -9928,7 +9908,7 @@ async function serveGallery(opts) {
9928
9908
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
9929
9909
  if (!asset) return fail(404, "no such review asset");
9930
9910
  try {
9931
- const bytes = await readFile17(asset.path);
9911
+ const bytes = await readFile16(asset.path);
9932
9912
  res.writeHead(200, {
9933
9913
  "Content-Type": asset.contentType,
9934
9914
  "Cache-Control": "no-store",
@@ -10004,7 +9984,7 @@ async function serveGallery(opts) {
10004
9984
  if (!asset) return fail(404, "no such gallery media");
10005
9985
  let bytes;
10006
9986
  try {
10007
- bytes = await readFile17(asset.path);
9987
+ bytes = await readFile16(asset.path);
10008
9988
  } catch {
10009
9989
  return fail(404, "gallery media is no longer on disk; refresh the page");
10010
9990
  }
@@ -10317,13 +10297,13 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
10317
10297
  }
10318
10298
 
10319
10299
  // src/pick/server.ts
10320
- import { existsSync as existsSync21 } from "fs";
10321
- import { readFile as readFile19 } from "fs/promises";
10300
+ import { existsSync as existsSync20 } from "fs";
10301
+ import { readFile as readFile18 } from "fs/promises";
10322
10302
 
10323
10303
  // src/pipeline/init.ts
10324
- import { readdir, readFile as readFile18, writeFile as writeFile10 } from "fs/promises";
10325
- import { existsSync as existsSync20 } from "fs";
10326
- import path28 from "path";
10304
+ import { readdir, readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
10305
+ import { existsSync as existsSync19 } from "fs";
10306
+ import path27 from "path";
10327
10307
  function pngSize(buf) {
10328
10308
  if (buf.length < 24) return null;
10329
10309
  const isPng = buf.readUInt32BE(0) === 2303741511 && buf.readUInt32BE(4) === 218765834;
@@ -10337,7 +10317,7 @@ function slugify(value) {
10337
10317
  async function walk(dir, exclude) {
10338
10318
  const out = [];
10339
10319
  for (const entry of await readdir(dir, { withFileTypes: true })) {
10340
- const full = path28.join(dir, entry.name);
10320
+ const full = path27.join(dir, entry.name);
10341
10321
  if (exclude.some((x) => full.includes(x))) continue;
10342
10322
  if (entry.isDirectory()) out.push(...await walk(full, exclude));
10343
10323
  else if (entry.name.toLowerCase().endsWith(".png")) out.push(full);
@@ -10351,14 +10331,14 @@ async function scanAssets(root, opts = {}) {
10351
10331
  const skipped = [];
10352
10332
  const seen = /* @__PURE__ */ new Map();
10353
10333
  for (const file of files.sort()) {
10354
- const size = pngSize(await readFile18(file));
10334
+ const size = pngSize(await readFile17(file));
10355
10335
  if (!size) {
10356
10336
  skipped.push(`${file} (not a readable PNG)`);
10357
10337
  continue;
10358
10338
  }
10359
- const rel = path28.relative(root, file);
10360
- const category = path28.dirname(rel) === "." ? "" : path28.dirname(rel);
10361
- let id = slugify(path28.basename(rel));
10339
+ const rel = path27.relative(root, file);
10340
+ const category = path27.dirname(rel) === "." ? "" : path27.dirname(rel);
10341
+ let id = slugify(path27.basename(rel));
10362
10342
  if (seen.has(id)) {
10363
10343
  const n = seen.get(id) + 1;
10364
10344
  seen.set(id, n);
@@ -10401,6 +10381,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
10401
10381
  promptSuffix: "",
10402
10382
  styleImages: [],
10403
10383
  palette: [],
10384
+ enforcePalette: false,
10404
10385
  outDir,
10405
10386
  tags: [name],
10406
10387
  providerOptions: {}
@@ -10415,7 +10396,7 @@ function mode(values) {
10415
10396
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 64;
10416
10397
  }
10417
10398
  async function writeManifestFile(target, manifest) {
10418
- if (existsSync20(target)) {
10399
+ if (existsSync19(target)) {
10419
10400
  throw new Error(`${target} already exists \u2014 refusing to overwrite an existing manifest.`);
10420
10401
  }
10421
10402
  await writeFile10(target, JSON.stringify(manifest, null, 2) + "\n");
@@ -10818,13 +10799,13 @@ async function currentArt(entry, spec) {
10818
10799
  const previous = primaryOutput({ ...entry, outputs: entry.supersededOutputs });
10819
10800
  if (!previous) return null;
10820
10801
  const file = resolveOutputPath(previous.path, spec.root);
10821
- if (!existsSync21(file)) return null;
10802
+ if (!existsSync20(file)) return null;
10822
10803
  const contentType = previous.mediaType ?? mediaTypeFromExtension(file);
10823
10804
  if (!contentType) return null;
10824
10805
  let width = entry.width;
10825
10806
  let height = entry.height;
10826
10807
  if (contentType === "image/png") {
10827
- const size = pngSize(await readFile19(file));
10808
+ const size = pngSize(await readFile18(file));
10828
10809
  if (size) ({ width, height } = size);
10829
10810
  }
10830
10811
  return { path: file, contentType, width, height };
@@ -11317,15 +11298,15 @@ function createGenerateHandlers(opts) {
11317
11298
  }
11318
11299
 
11319
11300
  // src/gallery/editor.ts
11320
- import { readFile as readFile21 } from "fs/promises";
11321
- import path30 from "path";
11301
+ import { readFile as readFile20 } from "fs/promises";
11302
+ import path29 from "path";
11322
11303
 
11323
11304
  // src/editor/install.ts
11324
11305
  import { createHash as createHash4 } from "crypto";
11325
- import { existsSync as existsSync22 } from "fs";
11326
- import { mkdir as mkdir8, readFile as readFile20, rename as rename8, rm as rm9, stat as stat3, writeFile as writeFile11 } from "fs/promises";
11306
+ import { existsSync as existsSync21 } from "fs";
11307
+ import { mkdir as mkdir8, readFile as readFile19, rename as rename8, rm as rm9, stat as stat3, writeFile as writeFile11 } from "fs/promises";
11327
11308
  import os2 from "os";
11328
- import path29 from "path";
11309
+ import path28 from "path";
11329
11310
 
11330
11311
  // tools/pixelorama-bridge/pin.json
11331
11312
  var pin_default = {
@@ -11389,15 +11370,15 @@ var EditorInstallError = class extends Error {
11389
11370
  };
11390
11371
  function editorToolsRoot() {
11391
11372
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
11392
- if (override) return path29.resolve(override);
11393
- if (process.platform === "darwin") return path29.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
11373
+ if (override) return path28.resolve(override);
11374
+ if (process.platform === "darwin") return path28.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
11394
11375
  if (process.platform === "win32") {
11395
- return path29.join(process.env.LOCALAPPDATA ?? path29.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
11376
+ return path28.join(process.env.LOCALAPPDATA ?? path28.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
11396
11377
  }
11397
- return path29.join(process.env.XDG_CACHE_HOME ?? path29.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
11378
+ return path28.join(process.env.XDG_CACHE_HOME ?? path28.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
11398
11379
  }
11399
11380
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
11400
- return path29.join(root, "pixelorama", pin.release ?? "unpublished");
11381
+ return path28.join(root, "pixelorama", pin.release ?? "unpublished");
11401
11382
  }
11402
11383
  function editorBaseUrl(pin = EDITOR_PIN) {
11403
11384
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -11407,7 +11388,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
11407
11388
  }
11408
11389
  async function sha256Of(file) {
11409
11390
  try {
11410
- return createHash4("sha256").update(await readFile20(file)).digest("hex");
11391
+ return createHash4("sha256").update(await readFile19(file)).digest("hex");
11411
11392
  } catch {
11412
11393
  return null;
11413
11394
  }
@@ -11420,8 +11401,8 @@ async function editorStatus(opts = {}) {
11420
11401
  let totalBytes = 0;
11421
11402
  for (const [name, expected] of Object.entries(pin.files)) {
11422
11403
  totalBytes += expected.bytes;
11423
- const file = path29.join(dir, name);
11424
- const info = existsSync22(file) ? await stat3(file).catch(() => null) : null;
11404
+ const file = path28.join(dir, name);
11405
+ const info = existsSync21(file) ? await stat3(file).catch(() => null) : null;
11425
11406
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
11426
11407
  installedBytes += expected.bytes;
11427
11408
  } else {
@@ -11478,7 +11459,7 @@ async function installEditor(opts = {}) {
11478
11459
  if (digest2 !== expected.sha256) {
11479
11460
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
11480
11461
  }
11481
- const final = path29.join(dir, name);
11462
+ const final = path28.join(dir, name);
11482
11463
  const tmp = `${final}.${process.pid}.part`;
11483
11464
  try {
11484
11465
  await writeFile11(tmp, bytes);
@@ -11524,7 +11505,7 @@ var EDITOR_CONTENT_TYPES = {
11524
11505
  ".json": "application/json; charset=utf-8"
11525
11506
  };
11526
11507
  function editorContentType(name) {
11527
- return EDITOR_CONTENT_TYPES[path29.extname(name).toLowerCase()] ?? null;
11508
+ return EDITOR_CONTENT_TYPES[path28.extname(name).toLowerCase()] ?? null;
11528
11509
  }
11529
11510
 
11530
11511
  // src/gallery/editor.ts
@@ -11600,7 +11581,7 @@ function createGalleryEditorHandlers(opts = {}) {
11600
11581
  if (!status.installed) return null;
11601
11582
  let bytes;
11602
11583
  try {
11603
- bytes = await readFile21(path30.join(dir, name));
11584
+ bytes = await readFile20(path29.join(dir, name));
11604
11585
  } catch {
11605
11586
  verified = null;
11606
11587
  return null;
@@ -11640,13 +11621,13 @@ async function serveUntilStopped(initial, reload, args, access2) {
11640
11621
  };
11641
11622
  const loadProject = async (project) => {
11642
11623
  const ctx = await access2.loadProject(project);
11643
- const dir = path31.dirname(ctx.loaded.path);
11624
+ const dir = path30.dirname(ctx.loaded.path);
11644
11625
  const declared = readEnvFiles(dir);
11645
11626
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
11646
11627
  for (const [name, value] of Object.entries(declared)) {
11647
11628
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
11648
11629
  throw new Error(
11649
- `${path31.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
11630
+ `${path30.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
11650
11631
  );
11651
11632
  }
11652
11633
  }
@@ -11691,8 +11672,8 @@ async function serveUntilStopped(initial, reload, args, access2) {
11691
11672
  }
11692
11673
  async function runGallery(args) {
11693
11674
  if (args.workspace) {
11694
- const workspacePath = path31.resolve(args.workspace);
11695
- if (!existsSync23(workspacePath)) {
11675
+ const workspacePath = path30.resolve(args.workspace);
11676
+ if (!existsSync22(workspacePath)) {
11696
11677
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
11697
11678
  }
11698
11679
  const filter2 = { styles: args.styles, assets: args.assets };
@@ -11712,7 +11693,7 @@ async function runGallery(args) {
11712
11693
  const registered = async (projectId) => {
11713
11694
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
11714
11695
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
11715
- return resolveProject(path31.dirname(workspacePath), project);
11696
+ return resolveProject(path30.dirname(workspacePath), project);
11716
11697
  };
11717
11698
  await serveUntilStopped(initial, build2, args, {
11718
11699
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -11737,7 +11718,7 @@ async function runGallery(args) {
11737
11718
  return buildGallerySnapshot({ ...fresh, filter });
11738
11719
  };
11739
11720
  await serveUntilStopped(await build(), reload, args, {
11740
- manifestFor: () => path31.resolve(args.manifest),
11721
+ manifestFor: () => path30.resolve(args.manifest),
11741
11722
  loadProject: () => openProject(args.manifest, { lockPath: args.lock, env: false })
11742
11723
  });
11743
11724
  }
@@ -11989,18 +11970,18 @@ async function runRestoreGeneration(args) {
11989
11970
  }
11990
11971
 
11991
11972
  // src/cli/commands/housekeeping.ts
11992
- import { existsSync as existsSync25 } from "fs";
11973
+ import { existsSync as existsSync24 } from "fs";
11993
11974
 
11994
11975
  // src/pipeline/cache-health.ts
11995
- import { existsSync as existsSync24 } from "fs";
11996
- import { readFile as readFile22, readdir as readdir2, rm as rm10 } from "fs/promises";
11997
- import path32 from "path";
11976
+ import { existsSync as existsSync23 } from "fs";
11977
+ import { readFile as readFile21, readdir as readdir2, rm as rm10 } from "fs/promises";
11978
+ import path31 from "path";
11998
11979
  async function inspectCaches(lock, lockPath, options = {}) {
11999
- if (options.prune && !existsSync24(lockPath)) {
12000
- throw new Error(`Refusing to prune without an existing lockfile at ${path32.resolve(lockPath)}`);
11980
+ if (options.prune && !existsSync23(lockPath)) {
11981
+ throw new Error(`Refusing to prune without an existing lockfile at ${path31.resolve(lockPath)}`);
12001
11982
  }
12002
- const contentDir = path32.resolve(path32.dirname(lockPath), ".pixelkiln", "cache");
12003
- const remotePath = path32.resolve(cachePathFor(lockPath));
11983
+ const contentDir = path31.resolve(path31.dirname(lockPath), ".pixelkiln", "cache");
11984
+ const remotePath = path31.resolve(cachePathFor(lockPath));
12004
11985
  const referenced = referencedHashes(lock);
12005
11986
  let content = await inspectContentCache(contentDir, referenced);
12006
11987
  let remoteHashes = await inspectRemoteHashCache(remotePath);
@@ -12012,7 +11993,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12012
11993
  ]);
12013
11994
  for (const name of names) {
12014
11995
  try {
12015
- await rm10(path32.join(contentDir, name), { force: true });
11996
+ await rm10(path31.join(contentDir, name), { force: true });
12016
11997
  removed.contentFiles++;
12017
11998
  } catch {
12018
11999
  }
@@ -12021,7 +12002,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12021
12002
  await saveCache(remotePath, { version: 1, hashes: {} });
12022
12003
  removed.resetRemoteHashCache = true;
12023
12004
  } else if (remoteHashes.invalidIds.length) {
12024
- const cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
12005
+ const cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
12025
12006
  for (const id of remoteHashes.invalidIds) delete cache.hashes[id];
12026
12007
  removed.remoteHashEntries = remoteHashes.invalidIds.length;
12027
12008
  await saveCache(remotePath, cache);
@@ -12039,7 +12020,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
12039
12020
  async function inspectContentCache(contentDir, referenced) {
12040
12021
  const report = {
12041
12022
  path: contentDir,
12042
- exists: existsSync24(contentDir),
12023
+ exists: existsSync23(contentDir),
12043
12024
  files: 0,
12044
12025
  bytes: 0,
12045
12026
  valid: 0,
@@ -12060,12 +12041,12 @@ async function inspectContentCache(contentDir, referenced) {
12060
12041
  continue;
12061
12042
  }
12062
12043
  report.files++;
12063
- const file = path32.join(contentDir, entry.name);
12044
+ const file = path31.join(contentDir, entry.name);
12064
12045
  const mediaType = mediaTypeFromExtension(entry.name);
12065
12046
  const expected = mediaType ? entry.name.slice(0, -4) : "";
12066
12047
  let bytes;
12067
12048
  try {
12068
- bytes = await readFile22(file);
12049
+ bytes = await readFile21(file);
12069
12050
  report.bytes += bytes.length;
12070
12051
  } catch (err) {
12071
12052
  report.invalid.push({
@@ -12103,7 +12084,7 @@ async function inspectContentCache(contentDir, referenced) {
12103
12084
  async function inspectRemoteHashCache(remotePath) {
12104
12085
  const report = {
12105
12086
  path: remotePath,
12106
- exists: existsSync24(remotePath),
12087
+ exists: existsSync23(remotePath),
12107
12088
  entries: 0,
12108
12089
  valid: 0,
12109
12090
  invalidIds: [],
@@ -12112,7 +12093,7 @@ async function inspectRemoteHashCache(remotePath) {
12112
12093
  if (!report.exists) return report;
12113
12094
  let cache;
12114
12095
  try {
12115
- cache = parseCache(JSON.parse(await readFile22(remotePath, "utf8")));
12096
+ cache = parseCache(JSON.parse(await readFile21(remotePath, "utf8")));
12116
12097
  } catch (err) {
12117
12098
  report.error = err instanceof Error ? err.message : String(err);
12118
12099
  return report;
@@ -12176,7 +12157,7 @@ async function runAccept(args) {
12176
12157
  let intact = true;
12177
12158
  for (const output of entry.outputs) {
12178
12159
  const file = resolveOutputPath(output.path, item.spec.root);
12179
- if (!existsSync25(file) || await sha256File(file) !== output.sha256) {
12160
+ if (!existsSync24(file) || await sha256File(file) !== output.sha256) {
12180
12161
  intact = false;
12181
12162
  break;
12182
12163
  }
@@ -12231,42 +12212,42 @@ This drops their provenance from the lockfile. The art on disk is`);
12231
12212
  }
12232
12213
 
12233
12214
  // src/cli/commands/init.ts
12234
- import path33 from "path";
12235
- import { existsSync as existsSync26 } from "fs";
12215
+ import path32 from "path";
12216
+ import { existsSync as existsSync25 } from "fs";
12236
12217
  async function runInit(args) {
12237
12218
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
12238
- const root = path33.resolve(args.from);
12239
- if (!existsSync26(root)) throw new Error(`No directory at ${root}`);
12219
+ const root = path32.resolve(args.from);
12220
+ if (!existsSync25(root)) throw new Error(`No directory at ${root}`);
12240
12221
  const generator = args.generator ?? "map";
12241
12222
  if (generator !== "1dir" && generator !== "map") {
12242
12223
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
12243
12224
  }
12244
- const target = path33.resolve(args.out ?? "pixelkiln.manifest.json");
12225
+ const target = path32.resolve(args.out ?? "pixelkiln.manifest.json");
12245
12226
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
12246
12227
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
12247
12228
  const manifest = buildManifest(
12248
- args.name ?? path33.basename(path33.dirname(target)),
12229
+ args.name ?? path32.basename(path32.dirname(target)),
12249
12230
  args.styles[0] ?? "base",
12250
12231
  generator,
12251
- path33.relative(path33.dirname(target), root) || ".",
12232
+ path32.relative(path32.dirname(target), root) || ".",
12252
12233
  assets
12253
12234
  );
12254
12235
  await writeManifestFile(target, manifest);
12255
- log(` scanned ${assets.length} PNG(s) under ${path33.relative(process.cwd(), root)}`);
12236
+ log(` scanned ${assets.length} PNG(s) under ${path32.relative(process.cwd(), root)}`);
12256
12237
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
12257
- log(` wrote ${path33.relative(process.cwd(), target)}`);
12238
+ log(` wrote ${path32.relative(process.cwd(), target)}`);
12258
12239
  log(`
12259
12240
  Prompts are intentionally empty. To recover the real ones from your`);
12260
12241
  log(` PixelLab account instead of inventing them:`);
12261
12242
  log(`
12262
- pixelkiln adopt --manifest ${path33.relative(process.cwd(), target)} --write-prompts
12243
+ pixelkiln adopt --manifest ${path32.relative(process.cwd(), target)} --write-prompts
12263
12244
  `);
12264
12245
  }
12265
12246
 
12266
12247
  // src/pipeline/doctor.ts
12267
- import { constants, existsSync as existsSync27 } from "fs";
12248
+ import { constants, existsSync as existsSync26 } from "fs";
12268
12249
  import { access, stat as stat4 } from "fs/promises";
12269
- import path34 from "path";
12250
+ import path33 from "path";
12270
12251
  async function doctor(loaded, specs, lock, lockPath, opts = {}) {
12271
12252
  const checks = [];
12272
12253
  const add = (id, level, message6) => checks.push({ id, level, message: message6 });
@@ -12277,8 +12258,8 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
12277
12258
  );
12278
12259
  add("lock", "ok", `${Object.keys(lock.entries).length} valid v2 lock entr(ies)`);
12279
12260
  const dirs = [...new Set(specs.flatMap((spec) => [
12280
- path34.dirname(spec.outFile),
12281
- ...spec.quality ? [path34.dirname(spec.quality.outFile)] : []
12261
+ path33.dirname(spec.outFile),
12262
+ ...spec.quality ? [path33.dirname(spec.quality.outFile)] : []
12282
12263
  ]))];
12283
12264
  const unwritable = [];
12284
12265
  for (const dir of dirs) {
@@ -12301,7 +12282,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
12301
12282
  for (let index = 0; index < entry.outputs.length; index++) {
12302
12283
  const output = entry.outputs[index];
12303
12284
  const spec = specByKey.get(key);
12304
- const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path34.dirname(lockPath));
12285
+ const absolute = spec ? currentEntryOutputPath(entry, spec, index) : resolveOutputPath(output.path, path33.dirname(lockPath));
12305
12286
  const owner = outputOwners.get(absolute);
12306
12287
  if (owner && owner !== key) duplicateOutputs.push(`${output.path} (${owner}, ${key})`);
12307
12288
  outputOwners.set(absolute, key);
@@ -12316,7 +12297,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
12316
12297
  } else {
12317
12298
  add("lock-outputs", "ok", `${outputOwners.size} recorded output path(s) are uniquely owned`);
12318
12299
  }
12319
- const cacheDir = path34.join(path34.dirname(lockPath), ".pixelkiln", "cache");
12300
+ const cacheDir = path33.join(path33.dirname(lockPath), ".pixelkiln", "cache");
12320
12301
  const stranded = [];
12321
12302
  const invalidState = [];
12322
12303
  const staleMapUrls = [];
@@ -12332,11 +12313,11 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
12332
12313
  const hasSource = Boolean(entry.sourceUrl || entry.sourceUrls?.length);
12333
12314
  let hasCache = false;
12334
12315
  for (const output of entry.outputs) {
12335
- const cached = path34.join(
12316
+ const cached = path33.join(
12336
12317
  cacheDir,
12337
12318
  cacheFileName(output.sha256, output.mediaType ?? MediaType.PNG)
12338
12319
  );
12339
- if (existsSync27(cached) && await sha256File(cached) === output.sha256) {
12320
+ if (existsSync26(cached) && await sha256File(cached) === output.sha256) {
12340
12321
  hasCache = true;
12341
12322
  break;
12342
12323
  }
@@ -12432,12 +12413,158 @@ async function nearestExistingDirectory(start) {
12432
12413
  if ((await stat4(current)).isDirectory()) return current;
12433
12414
  } catch {
12434
12415
  }
12435
- const parent = path34.dirname(current);
12416
+ const parent = path33.dirname(current);
12436
12417
  if (parent === current) return current;
12437
12418
  current = parent;
12438
12419
  }
12439
12420
  }
12440
12421
 
12422
+ // src/pipeline/audit.ts
12423
+ import { readFile as readFile22 } from "fs/promises";
12424
+ import { existsSync as existsSync27 } from "fs";
12425
+ import path34 from "path";
12426
+ function paletteDistance(asset, reference) {
12427
+ if (!asset.length || !reference.length) return 0;
12428
+ let total = 0;
12429
+ let weight = 0;
12430
+ for (const color of asset) {
12431
+ let nearest = Infinity;
12432
+ for (const ref of reference) nearest = Math.min(nearest, colorDistance(color, ref));
12433
+ total += nearest * color.weight;
12434
+ weight += color.weight;
12435
+ }
12436
+ return weight === 0 ? 0 : total / weight;
12437
+ }
12438
+ function mergePalettes(palettes, topN = 24) {
12439
+ const totals = /* @__PURE__ */ new Map();
12440
+ for (const palette of palettes) {
12441
+ for (const color of palette) {
12442
+ const key = `${color.r},${color.g},${color.b}`;
12443
+ const existing = totals.get(key);
12444
+ if (existing) existing.weight += color.weight;
12445
+ else totals.set(key, { ...color });
12446
+ }
12447
+ }
12448
+ const merged = [...totals.values()].sort((a, b) => b.weight - a.weight).slice(0, topN);
12449
+ const sum = merged.reduce((s, c) => s + c.weight, 0) || 1;
12450
+ return merged.map((c) => ({ ...c, weight: c.weight / sum }));
12451
+ }
12452
+ async function auditStyle(loaded, specs, styleId, lock) {
12453
+ const style = loaded.manifest.styles[styleId];
12454
+ if (!style) throw new Error(`Unknown style "${styleId}"`);
12455
+ const mine = specs.filter((s) => s.styleId === styleId);
12456
+ const assets = [];
12457
+ const missing = [];
12458
+ const unreadable = [];
12459
+ for (const spec of mine) {
12460
+ for (const output of resolveSpecOutputs(spec, lock, loaded.root)) {
12461
+ if (!existsSync27(output.absolutePath)) {
12462
+ missing.push(output.id);
12463
+ continue;
12464
+ }
12465
+ try {
12466
+ const png = decodePng(await readFile22(output.absolutePath));
12467
+ const palette = extractPalette(png, 12);
12468
+ assets.push({
12469
+ assetId: spec.assetId,
12470
+ ...output.role ? { outputRole: output.role } : {},
12471
+ id: output.id,
12472
+ file: output.absolutePath,
12473
+ width: png.width,
12474
+ height: png.height,
12475
+ paletteDistance: 0,
12476
+ // filled once the reference is known
12477
+ transparency: transparencyRatio(png),
12478
+ colorCount: countColors(png),
12479
+ palette
12480
+ });
12481
+ } catch (err) {
12482
+ unreadable.push(`${output.id}: ${err instanceof Error ? err.message : String(err)}`);
12483
+ }
12484
+ }
12485
+ }
12486
+ let reference = [];
12487
+ let referenceFromStyleImages = false;
12488
+ const refPalettes = [];
12489
+ for (const img of style.styleImages) {
12490
+ const abs = path34.resolve(loaded.root, img.path);
12491
+ if (!existsSync27(abs)) continue;
12492
+ try {
12493
+ refPalettes.push(extractPalette(decodePng(await readFile22(abs)), 12));
12494
+ } catch {
12495
+ }
12496
+ }
12497
+ if (refPalettes.length) {
12498
+ reference = mergePalettes(refPalettes);
12499
+ referenceFromStyleImages = true;
12500
+ for (const asset of assets) {
12501
+ asset.paletteDistance = paletteDistance(asset.palette, reference);
12502
+ }
12503
+ } else {
12504
+ reference = mergePalettes(assets.map((a) => a.palette));
12505
+ for (const asset of assets) {
12506
+ const others = assets.filter((a) => a !== asset).map((a) => a.palette);
12507
+ asset.paletteDistance = others.length ? paletteDistance(asset.palette, mergePalettes(others)) : 0;
12508
+ }
12509
+ }
12510
+ assets.sort((a, b) => b.paletteDistance - a.paletteDistance);
12511
+ return { styleId, referenceFromStyleImages, reference, assets, missing, unreadable };
12512
+ }
12513
+ function countColors(png) {
12514
+ const seen = /* @__PURE__ */ new Set();
12515
+ for (let i = 0; i < png.pixels.length; i += 4) {
12516
+ if (png.pixels[i + 3] < 128) continue;
12517
+ seen.add(png.pixels[i] << 16 | png.pixels[i + 1] << 8 | png.pixels[i + 2]);
12518
+ }
12519
+ return seen.size;
12520
+ }
12521
+ function outliers(audit, sigma = 1.5) {
12522
+ const n = audit.assets.length;
12523
+ if (n < 3) return [];
12524
+ const mean = audit.assets.reduce((s, a) => s + a.paletteDistance, 0) / n;
12525
+ const variance = audit.assets.reduce((s, a) => s + (a.paletteDistance - mean) ** 2, 0) / n;
12526
+ const sd = Math.sqrt(variance);
12527
+ if (sd === 0) return [];
12528
+ return audit.assets.filter((a) => a.paletteDistance > mean + sigma * sd);
12529
+ }
12530
+ function evaluateAudit(audit, thresholds = {}) {
12531
+ const resolved = { ...thresholds, sigma: thresholds.sigma ?? 1.5 };
12532
+ const relative = outliers(audit, resolved.sigma);
12533
+ const relativeIds = new Set(relative.map((asset) => asset.id));
12534
+ const violations = [];
12535
+ for (const asset of audit.assets) {
12536
+ const reasons = [];
12537
+ if (relativeIds.has(asset.id)) {
12538
+ reasons.push(`palette outlier beyond ${resolved.sigma} standard deviations`);
12539
+ }
12540
+ if (resolved.maxDistance !== void 0 && asset.paletteDistance > resolved.maxDistance) {
12541
+ reasons.push(
12542
+ `palette distance ${asset.paletteDistance.toFixed(1)} exceeds ${resolved.maxDistance}`
12543
+ );
12544
+ }
12545
+ if (resolved.minTransparency !== void 0 && asset.transparency < resolved.minTransparency) {
12546
+ reasons.push(
12547
+ `transparency ${(asset.transparency * 100).toFixed(1)}% is below ${(resolved.minTransparency * 100).toFixed(1)}%`
12548
+ );
12549
+ }
12550
+ if (resolved.maxColors !== void 0 && asset.colorCount > resolved.maxColors) {
12551
+ reasons.push(`color count ${asset.colorCount} exceeds ${resolved.maxColors}`);
12552
+ }
12553
+ if (reasons.length) violations.push({ id: asset.id, reasons });
12554
+ }
12555
+ return {
12556
+ safe: violations.length === 0 && audit.missing.length === 0 && audit.unreadable.length === 0,
12557
+ thresholds: resolved,
12558
+ outliers: [...relativeIds],
12559
+ violations,
12560
+ missing: [...audit.missing],
12561
+ unreadable: [...audit.unreadable]
12562
+ };
12563
+ }
12564
+ function hex(c) {
12565
+ return `#${[c.r, c.g, c.b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
12566
+ }
12567
+
12441
12568
  // src/cli/commands/inspect.ts
12442
12569
  async function runStatus(args) {
12443
12570
  const { lock } = await openProject2(args);
@@ -13993,7 +14120,7 @@ async function readRecipeFile(recipePath, bundled) {
13993
14120
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
13994
14121
  );
13995
14122
  }
13996
- return { recipe: parsed.data, path: absolute, dir: path39.dirname(absolute), bundled };
14123
+ return { recipe: parsed.data, authored: raw, path: absolute, dir: path39.dirname(absolute), bundled };
13997
14124
  }
13998
14125
  async function walkRecipeFiles(root) {
13999
14126
  if (!existsSync29(root)) return [];
@@ -14063,7 +14190,7 @@ async function verifyFile(root, file) {
14063
14190
  }
14064
14191
  async function verifyRecipe(target, options = {}) {
14065
14192
  const loaded = await resolveRecipe(target, options.bundledRoot);
14066
- const actualIntegrity = recipeDigest(loaded.recipe);
14193
+ const actualIntegrity = recipeDigest(loaded.authored);
14067
14194
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
14068
14195
  const modelRoot = options.modelRoot ? path39.resolve(options.modelRoot) : null;
14069
14196
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({