| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 0 | 14 | 0% |
| Commands | 0 | 0 | 7 | 0% |
| Section tags | 0 | 0 | 6 | 0% |
What each file covers
Sections
0 shared · 0 only in A · 14 only in B- + Imageflow Project Instructions
- + SIMD & Dispatch Crates
- + f32/f64 Clamping
- + Git Workflow
- + Test Commands
- + Known Bugs
- + FIXED (2026-07-19) — generic-quality double-mapping: ssim2 score was fed into the libjpeg-turbo-quality knob (WebP/JXL/AVIF)
- + FIXED (2026-06-25) — animated WebP/AVIF/GIF decode+encode were uncancellable (`None` at frame sites)
- + FIXED (2026-06-25) — `byte_ceiling` promoted the soft avg pre-flight threshold into a hard runtime cap
- + LOW / not-a-clear-bug — `High`-profile JPEG `gq > 85.0` chroma boundary (ZEN-ONLY build)
- + MEDIUM — single-frame PNG/WebP/AVIF/JXL decode ignores `max_threads`
- + LOW / interim (2026-06-24 audit)
- + Audit Notes (2026-06-22, "since v2.3.1-rc01" review)
- + Delayed TODOs
Commands
0 shared · 0 only in A · 7 only in B- + just test
- + just test-filter NAME
- + just test-update
- + just test-replace
- + just test-list
- + just test-build
- + cargo fmt
Section tags
0 shared · 0 only in A · 6 only in B- + build
- + test
- + code-style
- + git-pr
- + do-not
- + agent-behaviour
Line diff
imazen/imageflow · .cursor/rules/ffi.mdc
@@ −1 @@
1---
2description:
3globs:
4alwaysApply: true
5---
6Imageflow offers a very stable ABI, focused on sending/recieving JSON and byte buffers in conjuction with an endpoint string. There are wrappers in C# (the main one), go, node, elixr, and more. Keeping these wrappers up-to-date with the latest JSON features is difficult, and the nature of the JSON graph structure makes it require simpler API wrappers.
7
8For the HTTP-like endpoint names, see @/imageflow_core/src/context_method.rs
9
10For the FFI ABI, read @/imageflow_abi/src/lib.rs and [imageflow_default.h](mdc:bindings/headers/imageflow_default.h)
11
12
13
imazen/imageflow · CLAUDE.md
@@ +1 @@
1# Imageflow Project Instructions
2
3## SIMD & Dispatch Crates
4
5`multiversion` is allowed in this project for autovectorization dispatch on scalar loops (e.g., `scaling.rs`). Prefer the defaults provided by `multiversed` for new code — use `multiversion` only where `multiversed` doesn't fit.
6
7For explicit SIMD intrinsics, use `archmage` (already in use for `transpose.rs`).
8
9## f32/f64 Clamping
10
11**Do NOT replace `min(max(...))` patterns with `.clamp()` on floats.** `f32::clamp()` propagates NaN, while `min(max(...))` suppresses it. In image processing pipelines, NaN propagation turns a single bad pixel into a full-image corruption. The `min(max(...))` pattern is intentional NaN defense.
12
13## Git Workflow
14
15Always commit `cargo fmt` changes as a separate commit from code changes.
16
17## Test Commands
18
19All integration tests live in `imageflow_core/tests/integration/` as a single binary.
20
21```bash
22just test # run all tests with nextest
23just test-filter NAME # run tests matching NAME
24just test-update # run tests, auto-accept checksums within tolerance
25just test-replace # reset all checksum baselines to current output
26just test-list # list all test names
27just test-build # compile-check tests without running
28```
29
30Checksum baselines: `imageflow_core/tests/integration/visuals/*.checksums` (one line-format file
31per suite: canvas, codec, color, composition, icc, idct, orientation, scaling, trim, watermark)
32Reference images: `imageflow_core/tests/integration/visuals/images/<suite>/`
33
34## Known Bugs
35
36### FIXED (2026-07-19) — generic-quality double-mapping: ssim2 score was fed into the libjpeg-turbo-quality knob (WebP/JXL/AVIF)
37*Found & source-verified 2026-06-24; re-verified live and FIXED 2026-07-19.* `auto.rs` passed
38`generic_quality_ssim2(qp)` — an SSIMULACRA2 **score** — into `ZenEncoder::create_{webp,jxl,avif}`, which
39forward it to zencodec `with_generic_quality(...)`. That trait knob is a **calibrated 0–100 libjpeg-turbo
40quality** (`zencodec 0.1.24 traits/encoding.rs:54`), which each codec re-maps via its OWN calibration
41(zenwebp `calibrated_webp_quality`, zenavif `calibrated_avif_quality`, jxl-encoder `calibrated_jxl_quality`
42— all documented "Map generic quality (libjpeg-turbo scale) to … native quality"). Quality was mapped TWICE
43through mismatched units: `High` deflated (intended webp native ≈91.3, delivered ≈85.9); low-q profiles
44inflated — worst in the q5–q40 web range. **Fixed:** the three knob sites now pass the profile's
45libjpeg-turbo quality via `zencodec_generic_quality` (= `approximate_quality_profile`);
46`generic_quality_ssim2` is reserved for the zen JPEG path (`Quality::ApproxSsim2`, `zen_encoder.rs:88`),
47which genuinely takes ssim2 units. Unit tests `quality_mapping_tests::*` (auto.rs) pin both mappings
48(red-verified against the buggy mapping first: got 83.2 for `High`, expected 91.0). Encoded output changes
49for quality_profile-driven WebP (zen-only build) and JXL/AVIF (zen-codecs builds) — intended; no in-tree
50checksum baselines cover those paths (default CI build is c-codecs, where Jxl/Avif auto-select is disabled
51via `FEATURES_IMPLEMENTED` and the zen WebP/JPEG arms don't compile).
52
53### FIXED (2026-06-25) — animated WebP/AVIF/GIF decode+encode were uncancellable (`None` at frame sites)
54*Found 2026-06-24, fixed 2026-06-25.* The animation frame loop passed `None` for the per-call stop at all four
55sites (`render_next_frame_owned`, `push_frame`, `finish`); zengif/zenwebp/zenavif drop the job stop and honor
56only that per-call arg, so animated WebP/AVIF/GIF couldn't be interrupted mid-flight (animated JXL was fine —
57zenjxl carries the job stop). **Fixed:** the four sites now thread `Some(&stop as &dyn Stop)` — from
58`c.cancellation_token()` on the two decode sites and the push site, and from a persisted `stop_token` field on
59the encoder for `finish()` (which runs in `into_io`, where there is no `Context`). Verified: 20 animation +
6010 gif-limit + 3 webp integration tests pass. (A deterministic *mid-frame* cancellation test stays impractical
61without a codec pause-hook; the pre-call `return_if_cancelled!` gate already covers between-frame cancellation.)
62
63### FIXED (2026-06-25) — `byte_ceiling` promoted the soft avg pre-flight threshold into a hard runtime cap
64*Found 2026-06-24, fixed 2026-06-25.* `MemBudgetPolicy::byte_ceiling()` min'd over ALL three thresholds
65including `require_est_bytes_below` — a soft pre-flight check on the **avg** estimate (`check_estimates` →
66`peak_avg`) — and that ceiling became the codec's hard `max_memory_bytes` cap, so a caller who set only the
67advisory avg threshold got a hard `B − buffer` cap and a mid-flight OOM-reject despite passing pre-flight.
68**Fixed:** `byte_ceiling()` now min's over only the conservative thresholds (`require_est_max_bytes_below`,
69`require_tracked_bytes_below`). Regression tests `byte_ceiling_excludes_soft_avg_threshold` +
70`check_estimates_gates_on_the_right_metric` added (imageflow_types). Was mostly latent (all codec estimates 0).
71
72### LOW / not-a-clear-bug — `High`-profile JPEG `gq > 85.0` chroma boundary (ZEN-ONLY build)
73*Re-traced 2026-06-25 — the 2026-06-24 "MEDIUM regression / default High JPEG" framing was WRONG.* Facts that
74hold: zen `create_jpeg` sets `full_chroma = gq > 85.0`; `High` → `generic_quality_ssim2 = 85.0` exactly (the
75`(91.0, 85.0)` table knot), so `85.0 > 85.0` is false → 4:2:0. BUT: (1) this fires ONLY in the **zen-only
76build** — the `auto.rs` JPEG arm routes the generic-quality path under `#[cfg(all(not(c-codecs), zen-codecs))]`;
77the default `c-codecs` build encodes JPEG via the C `MozjpegEncoder`, and the runtime-picked `ZenJpegEncoder`
78path (`auto.rs:143`) passes `generic_quality = None` → uses the `q > 90` branch, not this boundary. (2) The C
79path's chroma is decided **content-adaptively** by `evalchroma::adjust_sampling(buf, {2,2}, chroma_quality)`
80per image — a fixed `gq` threshold cannot match it regardless of `>` vs `>=`. So this is NOT a default-build
81regression and NOT a clear bug; it's a heuristic-threshold calibration question confined to the zen-only JPEG
82path. `gq >= 85.0` would flip High → 4:4:4 there, but whether that better approximates evalchroma is
83content-dependent — needs corpus measurement, not a blind 1-char change.
84
85### MEDIUM — single-frame PNG/WebP/AVIF/JXL decode ignores `max_threads`
86*Source-verified 2026-06-24.* Only JPEG takes the buffered `run_pooled` path; PNG/WebP/AVIF/JXL single-frame
87decode runs `job.push_decode(...)` on the ambient global rayon pool (`zen_decoder.rs:~932`), so
88`ExecutionSecurity.max_threads` is silently violated for exactly the most-parallel decoders (rav1d/jxl-rs).
89**Fix:** run the single-frame `push_decode` inside `install_pooled(&self.thread_pool, …)` (mind the `&mut`
90bitmap-window sink — the closure must own/move it).
91
92### LOW / interim (2026-06-24 audit)
93- `v1/estimate` ignores `data.format` (`v1.rs:301` `let _ = &data.format;`) — encode side always 0, so the
94 returned `EncodeEstimate` is decode-only. Part of the #728 seam (codecs return `ResourceEstimate::unknown()`).
95- `check_estimates` `peak_max = …unwrap_or(peak_avg)` collapses the conservative gate onto the avg when a
96 codec sets `est` but not `max` (`ResourceEstimate::new` does exactly that). Apply a conservatism factor or
97 have codecs always set `max`.
98- `LIBJPEG_TURBO_Q_TO_SSIM2` (`auto.rs:649`) duplicates the `QUALITY_HINTS.ssim2` column verbatim — second
99 source of truth that will drift; generate one from the other.
100- Per-decode eager rayon pool (`zen_decoder.rs:239`) built even for single-frame decodes that never use it;
101 one-shot encode rebuilds a pool per call (`zen_encoder.rs:672`). Build lazily / reuse `self.thread_pool`.
102- Zero tests for the budgeting/estimate math (`check_estimates`/`byte_ceiling`/`v1/estimate`) and zero for
103 mid-flight cancellation. The `>=` reject boundary and the cap interaction are untested.
104
105## Audit Notes (2026-06-22, "since v2.3.1-rc01" review)
106
107- **CHANGELOG.md:13 is inaccurate**: claims `ExecutionSecurity` "gains … process timeout, and
108 cooperative cancellation handles." The actual struct (`imageflow_types/src/lib.rs:1127-1144`) has
109 ONLY size/byte/pixel limits — no timeout field, no cancellation handle. `JobOptions`
110 (`lib.rs:1476`) is an empty `#[non_exhaustive]` placeholder. No process timeout exists anywhere.
111- **CHANGELOG AVIF/HEIC drift**: AVIF decoder+encoder and BMP/PNM (`zenavif`/`zenbitmaps`) already
112 SHIPPED in `1bb00db5` but are listed under "QUEUED BREAKING CHANGES" as if pending; **HEIC is
113 genuinely absent** (no `heic` refs in `src/`). `ZenJxlDecoder` enum variant exists with no `zenjxl`
114 dep (scaffolding). `f06b478b` (moxcms widen) is uncited; `8e6f2483`→`bd545d11` IDCT churn is invisible.
115- **Privacy metadata = stripped by construction** (clean result): no encoder writes EXIF/XMP/IPTC/GPS;
116 EXIF orientation is applied-to-pixels-then-dropped; no preserve-metadata option exists. Source ICC is
117 read for the color transform but never re-embedded — output is plain sRGB-by-convention (only the C
118 libpng path writes an sRGB marker, `codec_png_wrapper.c:420`).
119- **Quality units**: only a codec-agnostic 0–100 scalar (`QualityProfile`, `lib.rs:644`) → static
120 per-codec tables (`auto.rs:557-716 QUALITY_HINTS`). No metric-target unit (zensim/ssim2/butteraugli);
121 `jxl.distance` is the only metric-flavored knob (JXL-only passthrough). The `ssim2` column in
122 `QUALITY_HINTS` is internal DPR-math only, not a request unit. `zensim` is a **dev-dependency only**.
123 `QualityIntent`/`codec_decisions.rs` (the richer design) is on branch `feat/zen-codecs-v3`, NOT HEAD,
124 and still collapses to one `generic_quality` float — would need structural change for metric targeting.
125
126## Delayed TODOs
127
128- **ssim2↔quality table calibration — JPEG dials DONE, remaining axes still guessed.**
129 - **DONE (2026-06-26):** `LIBJPEG_TURBO_Q_TO_SSIM2` in `imageflow_core/src/codecs/auto.rs` is now
130 **measured** (24 anchors), and a companion `MOZJPEG_EVALCHROMA_Q_TO_SSIM2` (24 anchors,
131 `#[allow(dead_code)]` until the JPEG path wires it) was added. Both are the median quality→SSIMULACRA2
132 curve from an 81,552-cell sweep (codec-corpus 502 images × {64,256,1024,native≤4MP} × q5–q100 × 2
133 encoders, fast-ssim2). Canonical copy + the `q_to_ssim2`/`ssim2_to_q`/`q_to_bpp` helpers + full
134 provenance live in `zencodecs::quality_calibration` (zenpipe); raw Parquet at
135 `/mnt/v/output/jpeg-q-ssim2-cal/2026-06-26/sweep.parquet`; docs + rosetta CSVs in
136 `imageflow/benchmarks/jpeg-q-ssim2-2026-06-26/`.
137 - **STILL UNCALIBRATED:** the `ssim2` column of `QUALITY_HINTS` (the DPR/quality-scalar math) has NO
138 empirical backing. The measured tables cover **JPEG dials only** — WebP/AVIF/JXL quality→ssim2 were
139 NOT swept, so the generic-quality target for those codecs is still a guess.
140 - **FIXED (2026-07-19) — the double-mapping correctness bug** (see the Known Bugs entry above):
141 auto.rs now feeds `with_generic_quality` the profile's *libjpeg-turbo 0–100 dial*
142 (`zencodec_generic_quality`); the ssim2-unit `generic_quality_ssim2` is reserved for the JPEG
143 `ApproxSsim2` path. Remaining follow-up: sweep WebP/AVIF/JXL to calibrate their own quality→ssim2
144 curves the way JPEG's dials now are (the codecs' internal libjpeg-q→native tables are CID22-512
145 medians; imageflow-side per-codec ssim2 columns in `QUALITY_HINTS` are still guesses, per the
146 bullet above).
147
148- **Issue #728 zencodec passthrough (currently interim heuristic).** The `target=fast|optimal` +
149 balance directive and `is_optimal`/optimality-headroom annotations are implemented in imageflow with
150 an INTERIM local cost/RD heuristic (`auto.rs`), because zencodec 0.1.19 exposes no encode
151 resource-estimate, no candidate-`ImageFormat` selector, and no optimality API. zencodec 0.1.24 adds
152 `EncoderConfig::estimate_encode_resources` plumbing but every codec returns `ResourceEstimate::unknown()`.
153 Full passthrough needs: (1) imageflow on zencodec ≥ the release that ships real
154 `estimate_encode_resources` impls in zenavif/zenjpeg/zenwebp/zenjxl, and (2) NEW zencodec APIs (a
155 candidate-format selector + optimality/would-not-improve determination) that exist in no version yet.
156 Replace the interim seams (marked `// TODO(#728): zencodec passthrough`) when those land.
157
158- **Licensing/caching module** (`imageflow_helpers/src/unused/`): ~2300 lines of draft licensing, caching, and polling code. Currently unreferenced (no `mod` declaration). Needs review, modernization, and wiring into the build when ready to complete.
159
@@ −1 +1 @@
1−---
2−description:
3−globs:
4−alwaysApply: true
5−---
6−Imageflow offers a very stable ABI, focused on sending/recieving JSON and byte buffers in conjuction with an endpoint string. There are wrappers in C# (the main one), go, node, elixr, and more. Keeping these wrappers up-to-date with the latest JSON features is difficult, and the nature of the JSON graph structure makes it require simpler API wrappers.
1+# Imageflow Project Instructions
72
8−For the HTTP-like endpoint names, see @/imageflow_core/src/context_method.rs
3+## SIMD & Dispatch Crates
94
10−For the FFI ABI, read @/imageflow_abi/src/lib.rs and [imageflow_default.h](mdc:bindings/headers/imageflow_default.h)
5+`multiversion` is allowed in this project for autovectorization dispatch on scalar loops (e.g., `scaling.rs`). Prefer the defaults provided by `multiversed` for new code — use `multiversion` only where `multiversed` doesn't fit.
116
7+For explicit SIMD intrinsics, use `archmage` (already in use for `transpose.rs`).
128
9+## f32/f64 Clamping
10+
11+**Do NOT replace `min(max(...))` patterns with `.clamp()` on floats.** `f32::clamp()` propagates NaN, while `min(max(...))` suppresses it. In image processing pipelines, NaN propagation turns a single bad pixel into a full-image corruption. The `min(max(...))` pattern is intentional NaN defense.
12+
13+## Git Workflow
14+
15+Always commit `cargo fmt` changes as a separate commit from code changes.
16+
17+## Test Commands
18+
19+All integration tests live in `imageflow_core/tests/integration/` as a single binary.
20+
21+```bash
22+just test # run all tests with nextest
23+just test-filter NAME # run tests matching NAME
24+just test-update # run tests, auto-accept checksums within tolerance
25+just test-replace # reset all checksum baselines to current output
26+just test-list # list all test names
27+just test-build # compile-check tests without running
28+```
29+
30+Checksum baselines: `imageflow_core/tests/integration/visuals/*.checksums` (one line-format file
31+per suite: canvas, codec, color, composition, icc, idct, orientation, scaling, trim, watermark)
32+Reference images: `imageflow_core/tests/integration/visuals/images/<suite>/`
33+
34+## Known Bugs
35+
36+### FIXED (2026-07-19) — generic-quality double-mapping: ssim2 score was fed into the libjpeg-turbo-quality knob (WebP/JXL/AVIF)
37+*Found & source-verified 2026-06-24; re-verified live and FIXED 2026-07-19.* `auto.rs` passed
38+`generic_quality_ssim2(qp)` — an SSIMULACRA2 **score** — into `ZenEncoder::create_{webp,jxl,avif}`, which
39+forward it to zencodec `with_generic_quality(...)`. That trait knob is a **calibrated 0–100 libjpeg-turbo
40+quality** (`zencodec 0.1.24 traits/encoding.rs:54`), which each codec re-maps via its OWN calibration
41+(zenwebp `calibrated_webp_quality`, zenavif `calibrated_avif_quality`, jxl-encoder `calibrated_jxl_quality`
42+— all documented "Map generic quality (libjpeg-turbo scale) to … native quality"). Quality was mapped TWICE
43+through mismatched units: `High` deflated (intended webp native ≈91.3, delivered ≈85.9); low-q profiles
44+inflated — worst in the q5–q40 web range. **Fixed:** the three knob sites now pass the profile's
45+libjpeg-turbo quality via `zencodec_generic_quality` (= `approximate_quality_profile`);
46+`generic_quality_ssim2` is reserved for the zen JPEG path (`Quality::ApproxSsim2`, `zen_encoder.rs:88`),
47+which genuinely takes ssim2 units. Unit tests `quality_mapping_tests::*` (auto.rs) pin both mappings
48+(red-verified against the buggy mapping first: got 83.2 for `High`, expected 91.0). Encoded output changes
49+for quality_profile-driven WebP (zen-only build) and JXL/AVIF (zen-codecs builds) — intended; no in-tree
50+checksum baselines cover those paths (default CI build is c-codecs, where Jxl/Avif auto-select is disabled
51+via `FEATURES_IMPLEMENTED` and the zen WebP/JPEG arms don't compile).
52+
53+### FIXED (2026-06-25) — animated WebP/AVIF/GIF decode+encode were uncancellable (`None` at frame sites)
54+*Found 2026-06-24, fixed 2026-06-25.* The animation frame loop passed `None` for the per-call stop at all four
55+sites (`render_next_frame_owned`, `push_frame`, `finish`); zengif/zenwebp/zenavif drop the job stop and honor
56+only that per-call arg, so animated WebP/AVIF/GIF couldn't be interrupted mid-flight (animated JXL was fine —
57+zenjxl carries the job stop). **Fixed:** the four sites now thread `Some(&stop as &dyn Stop)` — from
58+`c.cancellation_token()` on the two decode sites and the push site, and from a persisted `stop_token` field on
59+the encoder for `finish()` (which runs in `into_io`, where there is no `Context`). Verified: 20 animation +
60+10 gif-limit + 3 webp integration tests pass. (A deterministic *mid-frame* cancellation test stays impractical
61+without a codec pause-hook; the pre-call `return_if_cancelled!` gate already covers between-frame cancellation.)
62+
63+### FIXED (2026-06-25) — `byte_ceiling` promoted the soft avg pre-flight threshold into a hard runtime cap
64+*Found 2026-06-24, fixed 2026-06-25.* `MemBudgetPolicy::byte_ceiling()` min'd over ALL three thresholds
65+including `require_est_bytes_below` — a soft pre-flight check on the **avg** estimate (`check_estimates` →
66+`peak_avg`) — and that ceiling became the codec's hard `max_memory_bytes` cap, so a caller who set only the
67+advisory avg threshold got a hard `B − buffer` cap and a mid-flight OOM-reject despite passing pre-flight.
68+**Fixed:** `byte_ceiling()` now min's over only the conservative thresholds (`require_est_max_bytes_below`,
69+`require_tracked_bytes_below`). Regression tests `byte_ceiling_excludes_soft_avg_threshold` +
70+`check_estimates_gates_on_the_right_metric` added (imageflow_types). Was mostly latent (all codec estimates 0).
71+
72+### LOW / not-a-clear-bug — `High`-profile JPEG `gq > 85.0` chroma boundary (ZEN-ONLY build)
73+*Re-traced 2026-06-25 — the 2026-06-24 "MEDIUM regression / default High JPEG" framing was WRONG.* Facts that
74+hold: zen `create_jpeg` sets `full_chroma = gq > 85.0`; `High` → `generic_quality_ssim2 = 85.0` exactly (the
75+`(91.0, 85.0)` table knot), so `85.0 > 85.0` is false → 4:2:0. BUT: (1) this fires ONLY in the **zen-only
76+build** — the `auto.rs` JPEG arm routes the generic-quality path under `#[cfg(all(not(c-codecs), zen-codecs))]`;
77+the default `c-codecs` build encodes JPEG via the C `MozjpegEncoder`, and the runtime-picked `ZenJpegEncoder`
78+path (`auto.rs:143`) passes `generic_quality = None` → uses the `q > 90` branch, not this boundary. (2) The C
79+path's chroma is decided **content-adaptively** by `evalchroma::adjust_sampling(buf, {2,2}, chroma_quality)`
80+per image — a fixed `gq` threshold cannot match it regardless of `>` vs `>=`. So this is NOT a default-build
81+regression and NOT a clear bug; it's a heuristic-threshold calibration question confined to the zen-only JPEG
82+path. `gq >= 85.0` would flip High → 4:4:4 there, but whether that better approximates evalchroma is
83+content-dependent — needs corpus measurement, not a blind 1-char change.
84+
85+### MEDIUM — single-frame PNG/WebP/AVIF/JXL decode ignores `max_threads`
86+*Source-verified 2026-06-24.* Only JPEG takes the buffered `run_pooled` path; PNG/WebP/AVIF/JXL single-frame
87+decode runs `job.push_decode(...)` on the ambient global rayon pool (`zen_decoder.rs:~932`), so
88+`ExecutionSecurity.max_threads` is silently violated for exactly the most-parallel decoders (rav1d/jxl-rs).
89+**Fix:** run the single-frame `push_decode` inside `install_pooled(&self.thread_pool, …)` (mind the `&mut`
90+bitmap-window sink — the closure must own/move it).
91+
92+### LOW / interim (2026-06-24 audit)
93+- `v1/estimate` ignores `data.format` (`v1.rs:301` `let _ = &data.format;`) — encode side always 0, so the
94+ returned `EncodeEstimate` is decode-only. Part of the #728 seam (codecs return `ResourceEstimate::unknown()`).
95+- `check_estimates` `peak_max = …unwrap_or(peak_avg)` collapses the conservative gate onto the avg when a
96+ codec sets `est` but not `max` (`ResourceEstimate::new` does exactly that). Apply a conservatism factor or
97+ have codecs always set `max`.
98+- `LIBJPEG_TURBO_Q_TO_SSIM2` (`auto.rs:649`) duplicates the `QUALITY_HINTS.ssim2` column verbatim — second
99+ source of truth that will drift; generate one from the other.
100+- Per-decode eager rayon pool (`zen_decoder.rs:239`) built even for single-frame decodes that never use it;
101+ one-shot encode rebuilds a pool per call (`zen_encoder.rs:672`). Build lazily / reuse `self.thread_pool`.
102+- Zero tests for the budgeting/estimate math (`check_estimates`/`byte_ceiling`/`v1/estimate`) and zero for
103+ mid-flight cancellation. The `>=` reject boundary and the cap interaction are untested.
104+
105+## Audit Notes (2026-06-22, "since v2.3.1-rc01" review)
106+
107+- **CHANGELOG.md:13 is inaccurate**: claims `ExecutionSecurity` "gains … process timeout, and
108+ cooperative cancellation handles." The actual struct (`imageflow_types/src/lib.rs:1127-1144`) has
109+ ONLY size/byte/pixel limits — no timeout field, no cancellation handle. `JobOptions`
110+ (`lib.rs:1476`) is an empty `#[non_exhaustive]` placeholder. No process timeout exists anywhere.
111+- **CHANGELOG AVIF/HEIC drift**: AVIF decoder+encoder and BMP/PNM (`zenavif`/`zenbitmaps`) already
112+ SHIPPED in `1bb00db5` but are listed under "QUEUED BREAKING CHANGES" as if pending; **HEIC is
113+ genuinely absent** (no `heic` refs in `src/`). `ZenJxlDecoder` enum variant exists with no `zenjxl`
114+ dep (scaffolding). `f06b478b` (moxcms widen) is uncited; `8e6f2483`→`bd545d11` IDCT churn is invisible.
115+- **Privacy metadata = stripped by construction** (clean result): no encoder writes EXIF/XMP/IPTC/GPS;
116+ EXIF orientation is applied-to-pixels-then-dropped; no preserve-metadata option exists. Source ICC is
117+ read for the color transform but never re-embedded — output is plain sRGB-by-convention (only the C
118+ libpng path writes an sRGB marker, `codec_png_wrapper.c:420`).
119+- **Quality units**: only a codec-agnostic 0–100 scalar (`QualityProfile`, `lib.rs:644`) → static
120+ per-codec tables (`auto.rs:557-716 QUALITY_HINTS`). No metric-target unit (zensim/ssim2/butteraugli);
121+ `jxl.distance` is the only metric-flavored knob (JXL-only passthrough). The `ssim2` column in
122+ `QUALITY_HINTS` is internal DPR-math only, not a request unit. `zensim` is a **dev-dependency only**.
123+ `QualityIntent`/`codec_decisions.rs` (the richer design) is on branch `feat/zen-codecs-v3`, NOT HEAD,
124+ and still collapses to one `generic_quality` float — would need structural change for metric targeting.
125+
126+## Delayed TODOs
127+
128+- **ssim2↔quality table calibration — JPEG dials DONE, remaining axes still guessed.**
129+ - **DONE (2026-06-26):** `LIBJPEG_TURBO_Q_TO_SSIM2` in `imageflow_core/src/codecs/auto.rs` is now
130+ **measured** (24 anchors), and a companion `MOZJPEG_EVALCHROMA_Q_TO_SSIM2` (24 anchors,
131+ `#[allow(dead_code)]` until the JPEG path wires it) was added. Both are the median quality→SSIMULACRA2
132+ curve from an 81,552-cell sweep (codec-corpus 502 images × {64,256,1024,native≤4MP} × q5–q100 × 2
133+ encoders, fast-ssim2). Canonical copy + the `q_to_ssim2`/`ssim2_to_q`/`q_to_bpp` helpers + full
134+ provenance live in `zencodecs::quality_calibration` (zenpipe); raw Parquet at
135+ `/mnt/v/output/jpeg-q-ssim2-cal/2026-06-26/sweep.parquet`; docs + rosetta CSVs in
136+ `imageflow/benchmarks/jpeg-q-ssim2-2026-06-26/`.
137+ - **STILL UNCALIBRATED:** the `ssim2` column of `QUALITY_HINTS` (the DPR/quality-scalar math) has NO
138+ empirical backing. The measured tables cover **JPEG dials only** — WebP/AVIF/JXL quality→ssim2 were
139+ NOT swept, so the generic-quality target for those codecs is still a guess.
140+ - **FIXED (2026-07-19) — the double-mapping correctness bug** (see the Known Bugs entry above):
141+ auto.rs now feeds `with_generic_quality` the profile's *libjpeg-turbo 0–100 dial*
142+ (`zencodec_generic_quality`); the ssim2-unit `generic_quality_ssim2` is reserved for the JPEG
143+ `ApproxSsim2` path. Remaining follow-up: sweep WebP/AVIF/JXL to calibrate their own quality→ssim2
144+ curves the way JPEG's dials now are (the codecs' internal libjpeg-q→native tables are CID22-512
145+ medians; imageflow-side per-codec ssim2 columns in `QUALITY_HINTS` are still guesses, per the
146+ bullet above).
147+
148+- **Issue #728 zencodec passthrough (currently interim heuristic).** The `target=fast|optimal` +
149+ balance directive and `is_optimal`/optimality-headroom annotations are implemented in imageflow with
150+ an INTERIM local cost/RD heuristic (`auto.rs`), because zencodec 0.1.19 exposes no encode
151+ resource-estimate, no candidate-`ImageFormat` selector, and no optimality API. zencodec 0.1.24 adds
152+ `EncoderConfig::estimate_encode_resources` plumbing but every codec returns `ResourceEstimate::unknown()`.
153+ Full passthrough needs: (1) imageflow on zencodec ≥ the release that ships real
154+ `estimate_encode_resources` impls in zenavif/zenjpeg/zenwebp/zenjxl, and (2) NEW zencodec APIs (a
155+ candidate-format selector + optimality/would-not-improve determination) that exist in no version yet.
156+ Replace the interim seams (marked `// TODO(#728): zencodec passthrough`) when those land.
157+
158+- **Licensing/caching module** (`imageflow_helpers/src/unused/`): ~2300 lines of draft licensing, caching, and polling code. Currently unreferenced (no `mod` declaration). Needs review, modernization, and wiring into the build when ready to complete.
13159
