Skip to content

feat(metadata): wire the facade into gamut-jpeg, gamut-jxl and gamut-heic + capability query - #509

Draft
justin13888 wants to merge 15 commits into
feat/449-xmp-dcterms-provenancefrom
feat/420-metadata-facade-wiring
Draft

justin13888 wants to merge 15 commits into
feat/449-xmp-dcterms-provenancefrom
feat/420-metadata-facade-wiring

Conversation

@justin13888

@justin13888 justin13888 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #503 (feat/449-xmp-dcterms-provenance); this PR's base is that branch, and only the commits after it belong here.

This is an unattended run. No human approved the plan; the ## Decisions taken record below is what a human reads afterwards.

Summary

  • gamut-metadata: new capability module — Format / Carrier / Direction (repr(u8), append-only, ALL constants — slices, not arrays, on the two #[non_exhaustive] enums), const fn supports(format, carrier, direction) and const fn typed_wiring(format), a const table transcribed from each crate's STATUS.md with the row cited on every arm and pinned by a full-matrix test. README gains the capability table and the consumer-integration pattern.
  • gamut-jpeg, gamut-jxl, gamut-heic: an optional metadata Cargo feature (a normal, feature-gated dependency on gamut-metadata) adding the gamut-dng pattern — blocks() (raw located payloads as MetadataBlocks: JPEG EXIF without the Exif\0\0 signature, ISOBMFF/JXL Exif payload with its exif_tiff_header_offset applied, ICC reassembled) and metadata() (Metadata::from_blocks); the two encoders gain with_metadata(&Metadata) and with_encoded_metadata(&EncodedMetadata), routing each carrier to the existing raw setter and refusing carriers the container cannot write (IPTC-IIM, C2PA) with a typed Unsupported. C2PA is never copied forward (facade policy).
  • gamut-jxl additionally learns to read its own Exif / xml container boxes back (JxlDecoder::metadataJxlMetadata): jxl-rs swallows auxiliary boxes, so the crate walks the top-level box sequence itself; a Brotli-compressed (brob) metadata box is a typed Unsupported.
  • gamut-heic gains two ungated lenses used by the wiring: HeifItem::exif_tiff_stream (the TIFF stream behind the 4-byte offset, ISO/IEC 23008-12 §A.2.1) and HeifItem::icc_profile (the colr ICC bytes regardless of nclx order). HEIF has no IPTC-IIM item type, and a C2PA manifest store lives in a top-level uuid box outside the item model, so neither block is produced by blocks(); HeifContainer::c2pa still locates the store.
  • gamut umbrella: the metadata feature now forwards gamut-jpeg?/metadata, gamut-jxl?/metadata and gamut-heic?/metadata, so gamut = { features = ["jpeg", "metadata"] } reaches the typed accessors from the front door. The forwards are weak (?/): metadata alone still pulls in no codec, and a format alone still pulls in no facade. This bullet has now been revised twice — it first claimed the forwarding existed when it did not, was then corrected to say this branch would not add it, and now records that the branch's manifest was widened so it could. The forwards are pinned by crates/gamut/tests/feature_forwarding.rs (decision 16). See decisions 15 and 16; the change is what gamut: forward gamut-{jpeg,jxl,heic}'s metadata feature from the umbrella's own metadata #622 describes.

Repair round — the five red required checks

Format & Metadata and all four Incremental (PR diff) shards had been failing since the run of
2026-09-06. Both mechanisms are caused by this branch; neither comes from the base.

Check Mechanism Class Fix
Format & Metadata (fmt-check) The wiring landed without a nightly cargo fmt --all pass. Fifteen hunks across gamut-jpeg/src/{lib,metadata}.rs, gamut-jxl/src/{decoder,encoder,lib}.rs and gamut-jxl/tests/metadata_facade.rs. fmt-tooling-check itself passed; only fmt-check failed caused style(jpeg) + style(jxl), formatting only
Incremental shards 0–3 Exit 3 = timeouts, not survivors. read_box returned the offset just past the box it read, so container_metadata_boxes's progress lived in a callee's return value: every one of cargo-mutants' seven return-value replacements of read_box makes the walk loop forever, and each is scored TIMEOUT after 60 s. Shards saw 3, 4, 3 and 3 of them caused refactor(jxl): parse_box_header now reports only what the header claims and slices nothing; the walk owns every bound and steps at least the 8-byte minimum header per iteration, so it terminates for any value a mutant can return and the mutants become killable by an assertion
Incremental shard 0, additionally One MISSED mutant: size < 8size <= 8 in read_box. Nothing asserted the empty-box boundary, so rejecting a box whose size is exactly its 8-byte header survived the suite caused test(jxl): an empty free box between the metadata boxes is stepped over, and an empty xml box yields an empty payload rather than an absent one

The branch's base (feat/449-xmp-dcterms-provenance, d6fd0a0) has not moved since 2026-09-06
and is still an ancestor of this head, so no part of the red is a stale-base effect.

One further discrepancy found while re-observing: the gamut-heic half of the deliverable — the two
lenses and the metadata feature that the Summary and decision 7 both describe — had never been
committed, so the pull request as pushed did not contain what it described. It is committed now
(decision 13). The umbrella's feature forwarding, which the Summary also claimed, is not done
here and is filed as #622 (decision 14); the Summary is corrected in place.

Correction, appended. The paragraph above says of the umbrella's feature forwarding that it "is not done here and is filed as #622 (decision 14)". That was true when it was written and is no longer: the branch's manifest was subsequently widened to include crates/gamut/Cargo.toml and the three weak forwards are now in this pull request (decision 15). The sentence it corrects is left standing above. #622 remains open — this run does not close or comment on an issue, including one it filed — and a human can close it against this pull request.

Review round 2 — the three Low findings and one design question

The first independent review of this branch found no blocking defect: it cross-checked the
capability table cell by cell against the crates it cites and found no wrong cell, re-verified the
container-walk repair structurally and with a sweep of 21 584 hostile inputs, and re-derived the
feature forwards from cargo's unit graph. What it did find were three Low findings and one
informational note. All four were reproduced here before anything was changed.

Finding Reproduced how Repair
L3 — the weakness pin matched its three forwards over the whole manifest, so it could not see which feature list an entry belonged to Moved "gamut-jpeg?/metadata" out of metadata = [ … ] into jpeg = [ … ]: both tests still passed, while gamut --features jpeg then resolved the format crate with its metadata wiring and therefore the whole facade — the build the weak form exists to prevent The feature's own entry list is sliced out of the manifest and presence asserted over that, plus a manifest-wide count of one so the entry cannot also be attached to a format feature. Both attacks now fail, each naming its own cause (rows in Validation)
L1 — the rustdoc and the README prose said typed wiring sits behind "that crate's metadata Cargo feature" gamut-dng has no such feature: gamut-metadata is an unconditional dependency there, so gamut-dng/metadata is a hard cargo error. The README's table had DNG right; only the prose and the rustdoc were wrong Both name the three crates that gate the surface and name DNG as the one that does not. No cell changed
L2 — the docs said the raw surface is one "every format crate ships unconditionally" False for gamut-jxl, whose reader is gated on decode: under default-features = false, features = ["encode"] the table answers r and JxlDecoder::metadata does not exist The docs now say what is true — a const table sees neither another crate's features nor the target, so it describes the surface a crate defines — and name the gated case. The table is not weakened
I4 (informational) — a dropped forward and a renamed accessor produced the same compile error in the resolution pin Deleting "gamut-jpeg?/metadata" and renaming the accessor both produced one no associated function named blocks error A feature witness per crate (gamut::jpeg::Metadata, re-exported under each crate's own metadata cfg). A dropped forward now breaks the witness and the accessor; a rename breaks only the accessor

Separately, design question 3 was taken: Format::ALL and Carrier::ALL sat on
#[non_exhaustive] enums as fixed-length arrays, so appending a variant would have changed each
constant's type — a breaking change for any caller who named it, baked into API this pull request
introduces. Both are &'static [Self] now. Direction is exhaustive and keeps its array.

Design questions 1, 2 and 4 are filed, not taken (see ## Issue): each is a behavioural or
cross-crate fork that should not be decided at the close of a review loop. The one whose current
behaviour could surprise a caller — a present ICC replacing a ColorSpec set earlier — is now
documented where a caller reads it.

Validation

Every command below was run on this branch at the head this section describes, from a nested
worktree of the repository. __CARGO_TEST_ROOT is set for the formatting tasks because
fmt-tooling-check loops over every tooling/*/Cargo.toml and cargo metadata otherwise walks
past a nested worktree's root; it changes nothing about what is checked.

Command Result
cargo test -p gamut-heic --all-features pass
cargo test -p gamut-jxl --all-features pass
cargo clippy -p gamut-heic --all-targets --all-features -- -D warnings pass
cargo clippy -p gamut-jxl --all-targets --all-features -- -D warnings pass
__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-check pass (was the Format & Metadata failure)
mise run check-tests pass — "module docs, pinned proptest seeds and oracle filenames all conform"
convco check origin/feat/449-xmp-dcterms-provenance..HEAD pass — "no errors in 8 commits" (the stacked base, not origin/master)
mise run check-release-deps pass — "release dependency graph has no dev-only workspace edges"
mise run check-ffi-features pass — "gamut-ffi features in sync with gamut"
mise run lint pass (whole workspace, 6m16s)
mise run test pass — 3818 tests over 206 suites, 0 failed. The +2/+1 over the run before the pin is exactly tests/feature_forwarding.rs: the suite now notices if a forward disappears
cargo build -p gamut --example <probe> --features "jpeg,jxl,heic,metadata"before the forwards compile error: blocks / metadata / with_metadata not found on JpegMetadata, JxlMetadata, HeifImage, JpegEncoder (5 accessors, named only through gamut::)
the same command after the forwards compiles
cargo build -p gamut --features "jpeg,jxl,heic,metadata" -v all three crates are invoked with --cfg feature="metadata"
cargo build -p gamut --features "jpeg,jxl,heic" -v the same three are compiled, none with --cfg feature="metadata" (0 occurrences)
cargo tree -p gamut --no-default-features --features "jpeg,jxl,heic" 0 facade crates in the graph — a format alone does not pull gamut-metadata/exif/icc/xmp/iptc in
cargo tree -p gamut --no-default-features --features "metadata" 0 codec crates in the graph — metadata alone does not pull gamut-jpeg/jxl/heic in
cargo test -p gamut --all-features --test feature_forwarding pass, 2 tests
cargo test -p gamut --no-default-features --features metadata --test feature_forwarding pass, 1 test — the weakness pin carries no cfg, so it still runs where the resolution pin is compiled out
falsifier: delete "gamut-jxl?/metadata", re-run fails — compile error, blocks not found on JxlMetadata
falsifier: delete "gamut-heic?/metadata", re-run fails — compile error, blocks not found on HeifImage
falsifier: change "gamut-heic?/metadata" to "gamut-heic/metadata", re-run fails"the forward "gamut-heic/metadata" is not weak; enabling metadata alone would now pull gamut-heic into builds that asked for no codec"
GAMUT_MUTANTS_BASE=origin/feat/449-xmp-dcterms-provenance mise run mutants-diff --shard i/4 --verbose, i = 0..3 75 mutants, 72 caught, 3 unviable, 0 missed, 0 timeouts (19 / 19 / 19 / 18 per shard; the three unviable are the blocks() return-value replacements, whose Default::default() substitute does not typecheck)

The probe used for the two compile rows was a throwaway example naming one forwarded accessor per crate; it was deleted after the measurement and is not part of the diff — tests/feature_forwarding.rs is the committed pin that replaced it. The three falsifier rows were measured by editing the manifest, running, and restoring it; the manifest in the diff is unchanged by them. Every gate above was re-run after the umbrella commit and again after the pin; lint and test were each run three times in total and passed every time, and the --in-diff mutant selection is byte-identical to the one the four shards below were run against (the umbrella commit touches only a Cargo.toml, which contributes no mutants), so that result stands for this head.

Mutation gate base. This branch is stacked, so GAMUT_MUTANTS_BASE names
origin/feat/449-xmp-dcterms-provenance. Against the default origin/master the selection would
fold in every mutant belonging to #503 underneath and would not be evidence about this diff. The
75 mutants selected against the correct base are: 50 in gamut-jxl/src/decoder.rs, 12 in
gamut-heic/src/image.rs, 6 in gamut-metadata/src/capability.rs, 5 in
gamut-jpeg/src/metadata.rs, 2 in gamut-jxl/src/encoder.rs.

Workspace-wide runs were executed inside a memory-capped scope (MemoryMax=16G,
MemorySwapMax=0, CARGO_BUILD_JOBS=2); mise run fetch-av1-oracles was run first so the
AV1/AVIF oracle builds resolve.

Round 2 — re-run at head 7937317

Every command below was executed in this round, on this head, from a nested worktree.
Workspace-wide runs used the same memory-capped scope (MemoryMax=16G, MemorySwapMax=0,
CARGO_BUILD_JOBS=2, ulimit -v 12 GiB).

Command Result
cargo test -p gamut --features "metadata,jpeg,jxl,heic" --test feature_forwarding pass, 2 tests
cargo test -p gamut --features metadata --test feature_forwarding pass, 1 test — the weakness pin carries no cfg
cargo test -p gamut --features "jpeg,jxl,heic" --test feature_forwarding pass, 1 test
falsifier (the reviewer's own attack): move "gamut-jpeg?/metadata" from metadata = [ … ] into jpeg = [ … ] fails"the umbrella's metadata feature no longer lists "gamut-jpeg?/metadata"; … and if the entry moved to that format's own feature, that format alone now drags in the whole facade". Before this round the same edit left both tests passing
falsifier: leave the entry in metadata and also add it to jpeg = [ … ] fails""gamut-jpeg?/metadata" is listed more than once; a second copy under a format feature makes that format alone drag in the whole facade" (left 2, right 1)
falsifier (I4, cause A): delete "gamut-jpeg?/metadata" 2 compile errorscannot find type Metadata in crate gamut::jpeg and no associated function named blocks
falsifier (I4, cause B): the accessor named in the pin no longer exists 1 compile error — the accessor only. The two causes are now distinguishable
cargo test -p gamut-metadata --all-features pass (17 + 15 + 4 + 17 + 9 doctests)
cargo clippy -p gamut-metadata --all-targets --all-features -- -D warnings pass
cargo tree -p gamut --features "jpeg,jxl,heic" 0 facade crates — the evidence half of the negative direction still holds
__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt then … mise run fmt-check pass, no reformatting fallout
mise run check-tests pass — "module docs, pinned proptest seeds and oracle filenames all conform"
mise run check-commits (convco check against the stacked base) pass — "no errors in 19 commits"
mise run lint pass, whole workspace, exit 0
mise run test pass, whole workspace, exit 0 — 206 suites, 0 failures
mise run check-release-deps pass — "release dependency graph has no dev-only workspace edges"
mise run check-ffi-features pass — "gamut-ffi features in sync with gamut"
GAMUT_MUTANTS_BASE=origin/feat/449-xmp-dcterms-provenance mise run mutants-diff 75 mutants, 72 caught, 3 unviable, 0 missed, 0 timeouts — byte-identical selection to the previous round, which is the expected result: consts are not mutated, documentation is not mutated, and crates/gamut/tests/ is mutation-invisible

Every falsifier above was reverted; git status is clean and the manifest in the diff is unchanged
by them.

Mutation gate base and cap. The correct base is the stacked head
origin/feat/449-xmp-dcterms-provenance, and that is what the published count was measured against.
A first run against the default origin/master was also executed (87 mutants, 83 caught, 4 unviable,
0 missed); it is a diluted superset that folds in #503's mutants and is reported only for
completeness, not as evidence about this diff. Both ran under ulimit -v — an address-space cap CI
does not have — so a mutant that hangs by allocating would abort here and score caught while CI
scores it TIMEOUT. This round adds no loop and no hand-written iterator, and all four CI shards
were already green at the head this round started from.

Risks and rollout

  • Additive, feature-gated (off by default) in all three format crates; their metadata types are #[non_exhaustive], so the new JxlMetadata and the new methods are minor bumps. gamut-metadata gains a module (minor).
  • gamut-jxl's JxlDecoder::metadata is available under the decode feature regardless of metadata (it returns raw bytes); the walk is bounded by the input length and every box overrun is a typed error.
  • No new external dependency. The metadata feature of a format crate pulls gamut-metadata and its four leaf crates into that consumer only when enabled.
  • gamut-heic gains an optional, feature-gated normal dependency on gamut-metadata, so release ordering follows it; mise run check-release-deps confirms no dev-only workspace edge was introduced.
  • The JXL container walk was restructured (parse_box_header + a walk that owns its bounds). Behaviour is unchanged: every fault keeps the message its tests already pin, and the walk's step is now at least the 8-byte minimum header per iteration, so it cannot fail to terminate on any input.

Issue

Refs #420 — the remaining four crates (png, webp, avif, tiff) are filed as #510 because open
pull requests are changing their metadata surfaces.

#622 ("forward gamut-{jpeg,jxl,heic}'s metadata feature from the umbrella's own") was filed by
this branch when the forwarding was out of scope, and is now covered by this pull request. It is
deliberately left open and uncommented: this run does not write to any issue, including one it filed.
A human can close it against this pull request.

Three design questions the second review raised are filed rather than decided here, each with the
review's evidence:

A fourth issue was filed after the three above:

  • gamut-core: PixelFormat::ALL is a fixed-length array on a #[non_exhaustive] enum, so appending a format is a type change #629gamut-core: PixelFormat::ALL is a fixed-length array on a #[non_exhaustive] enum
    whose own docs say "new variants append", so appending a twelfth format changes the constant's
    type. It is the last ALL in the workspace with that shape — gamut-icc, gamut-exif,
    gamut-xmp and (as of this pull request) gamut-metadata all use slices. Not changed here:
    gamut-core is outside this branch's manifest and, unlike the constants this branch owns, the
    constant is already released at 2.0.1, so converting it is itself a breaking change to the
    workspace's root crate — a price somebody has to choose to pay, not a repair to slip into a
    documentation round. The issue carries the three options and the in-repo callers, including
    crates/gamut-ffi/DESIGN.md, which enumerates that constant to generate the C surface.

Decisions taken

Issue 420 - gamut-metadata: wire the facade into the format crates + capability query
Plan:     v1
Branch:   feat/420-metadata-facade-wiring
Base:     head of PR #503 (origin/feat/449-xmp-dcterms-provenance, d6fd0a0) - stacked; PR opened against that branch
Cause:    -
Touches:  gamut-metadata new capability.rs + lib.rs + tests + docs; gamut-jpeg, gamut-jxl, gamut-heic: optional `metadata` feature with a normal gamut-metadata dep, `metadata_blocks()` + typed `metadata()` accessors, encoder `with_metadata(&Metadata)` where an encoder exists; Cargo.lock; tests
Will not: touch gamut-png, gamut-webp, gamut-avif, gamut-tiff (their metadata surfaces are being changed by open PRs #485/#440, #445, #506, #446 - filed as the remainder); change the facade's container-agnostic design; wire audio/video (#216)
Lane:     serialised behind E-449 (stacked)
Settled:  S2 semver (all three crates' metadata types are #[non_exhaustive]; additive, feature-gated -> minor); S3 docs/testing.md; S4 Refs #420 + filed remainder

Decisions taken.
1. Deliverable boundary
   Taken:    capability table for every format now + typed wiring for jpeg, jxl and heic (the three crates no open PR touches); the remaining four crates are filed as one issue to land after their C2PA PRs merge, linked from the PR body
   Rejected: all seven crates in one branch - collides with four open PRs; waiting for them - stalls the highest-value item in #416
   Reverses: extend this branch to the other four after the stack merges
   Filed:    "gamut-metadata: wire the facade into gamut-png, gamut-webp, gamut-avif and gamut-tiff (#420 remainder)" - search first, file if absent
2. Feature gating
   Taken:    each format crate gains a Cargo feature `metadata` (off by default) enabling an optional normal dependency on gamut-metadata; the accessors and `with_metadata` are `#[cfg(feature = "metadata")]`; the umbrella `gamut` forwards it only if its existing `metadata` feature does not already imply the per-crate feature (inspect crates/gamut/Cargo.toml; do the minimum); `mise run check-release-deps` and `check-ffi-features` must pass
   Rejected: unconditional dependency - pulls four metadata crates into every jpeg consumer; dev-dependency only - rejected by check-release-deps
   Reverses: make the dependency unconditional
3. Accessor shape (the gamut-dng pattern)
   Taken:    `JpegMetadata::blocks(&self) -> Vec<MetadataBlock<'_>>` (raw, located payloads, EXIF block = the TIFF stream without the `Exif\0\0` prefix, ICC = reassembled profile) and `JpegMetadata::metadata(&self) -> gamut_metadata::Result<Metadata>` (= MetadataExtractor::new().extract(&blocks)); same on gamut-jxl's decoder surface and gamut-heic's `HeifImage`; encoders: `JpegEncoder::with_metadata(&Metadata) -> Result<Self>` and `JxlEncoder::with_metadata` embed via MetadataEmbedder and route each EncodedMetadata field to the existing raw setter; C2PA never copied forward (the facade drops it by policy; document)
   Rejected: a trait in gamut-metadata that format crates implement - inverts the dependency direction the facade's README forbids
   Reverses: replace the inherent methods with a trait
4. Capability query
   Taken:    `gamut_metadata::capability` with `Format` (Jpeg, Png, WebP, Avif, Heic, Jxl, Tiff, Dng - `#[non_exhaustive] #[repr(u8)]`, append-only), `Carrier` (Exif, Xmp, Icc, IptcIim, C2pa), `Direction` (Read, Write) and `pub const fn supports(format, carrier, direction) -> bool` backed by a const table derived from each crate's STATUS.md TODAY (what the format crate can locate/write raw), plus `typed_wiring(format) -> bool` saying whether the typed accessors exist (true for dng, jpeg, jxl, heic after this PR); every cell cites the STATUS.md row that justifies it in a comment; a test asserts the table against the raw surfaces where a crate is a dev-dependency? No - gamut-metadata must not depend on format crates (release topology): the table is documented and pinned by an example-based test only; #216's audio/video half stays out
   Rejected: a runtime registry - the set is static; a trait method per crate - dependency direction again
   Reverses: move the table into each format crate
5. Oracle and tests
   Taken:    typed round-trip per wired crate (encode with a Metadata, decode, compare - the facade's tests/roundtrip.rs equality extended through the container), and exiv2 differential where exiv2 reads the container (JPEG certainly; HEIC/JXL per the oracle's build); each cell that cannot be oracle-tested is named in STATUS.md

Appended during the run (same shape):

6. exiv2 differential
   Taken:    no format crate gains an exiv2-oracle dev-dependency. `tooling/exiv2-oracle` as built is an in-memory, block-level API (`exif_get`/`exif_roundtrip` over a bare TIFF stream, `get_property`/`roundtrip` over an XMP packet) with no container reader compiled in, so "exiv2 reads the JPEG/HEIC/JXL" is not testable with it in any cell. Block-level agreement with exiv2 is already pinned in the leaf crates (gamut-exif, gamut-xmp), and each container crate pins the located payload byte-exact against its own oracle (libjpeg-turbo ICC, libheif Exif/XMP items, a raw box scan of libjxl output); the typed round-trip is the evidence for the wiring, and every STATUS.md names the container-level exiv2 cell as untested
   Rejected: extending the oracle shim with exiv2's ImageFactory container readers - `tooling/exiv2-oracle` is outside this lane's manifest, and it would add the GPL oracle's C++ build to three more crates' test builds for a check the leaf crates already make
   Reverses: add exiv2 container readers to the oracle and a `tests/oracle_metadata.rs` per crate; filed with the remainder issue
7. HEIC accessor fallibility
   Taken:    `HeifImage::blocks()` returns `gamut_core::Result` (a hostile Exif item can carry a truncated or out-of-range `exif_tiff_header_offset`) and `HeifImage::metadata()` returns `gamut_core::Result<Metadata>`, wrapping the facade's `MetadataError` as `InvalidInput` with the facade message as detail; jpeg/jxl keep the record's infallible `blocks()` and facade-typed `metadata()` because their locating step (`gamut_jpeg::metadata` / `JxlDecoder::metadata`) has already validated the payloads
   Rejected: skipping a malformed Exif item silently (contradicts the crate's byte-accounting guarantee); a `MetadataError` variant for container faults (the facade is container-agnostic)
   Reverses: add an infallible `exif_tiff()` lens on `HeifImage` and make `blocks()` infallible over it
8. Encoder policy seam
   Taken:    encoders also expose `with_encoded_metadata(&EncodedMetadata) -> Result<Self>`, so a caller can choose the embedder's policies (`C2paPolicy::Reject`, `ExtensionPolicy::Reject`, IIM emission); `with_metadata` is exactly that over `MetadataEmbedder::new()`. Carriers the container cannot write (IPTC-IIM in JPEG/JXL, C2PA in both) are typed `Unsupported` errors, never dropped. Fields absent from the `EncodedMetadata` leave an earlier `with_*` setting untouched (additive builder semantics)
   Rejected: a `with_metadata_with(&Metadata, &MetadataEmbedder)` overload - duplicates the embedder's builder
   Reverses: remove `with_encoded_metadata` and inline the default embedder
9. JXL decode-side box walk
   Taken:    a crate-local walk of the top-level box sequence in `decoder.rs`, compiled under the `decode` feature next to `embedded_icc_profile`: jxl-rs 0.4.3 consumes auxiliary boxes without exposing them (jxl-rs #674) and its `ContainerBoxHeader::parse` is `pub(super)`. Handles the 32-bit, `size == 0` (to EOF) and `size == 1` (64-bit XLBox) forms; the `Exif` payload's 4-byte big-endian tiff-header offset is applied (ISO/IEC 23008-12 §A.2.1, reused by 18181-2); first `Exif`/`xml ` box wins (the crate's JPEG convention); a `brob` box wrapping `Exif`/`xml ` is `Unsupported` (Brotli decode would need a new dependency), any other `brob` is skipped; every overrun is `InvalidInput`
   Rejected: depending on gamut-isobmff (JXL boxes are top-level only; a new dependency edge the record does not name); waiting for jxl-rs to expose boxes
   Reverses: replace the walk with jxl-rs box events once exposed
10. gamut-metadata STATUS.md
   Taken:    not created - the crate has never had one (its README carries the status), and the manifest entry was a prediction, not a requirement
   Rejected: creating a STATUS.md for one section
   Reverses: add one
11. JXL ICC routing
   Taken:    `JxlEncoder::with_metadata` routes `Metadata::icc` to `ColorSpec::Icc` - in JPEG XL the ICC profile *is* the codestream's colour encoding rather than a container box, and this mirrors what JPEG's APP2 profile declares (the samples' colour); libjxl validates the profile against the image's colour family at encode time
   Rejected: dropping the ICC carrier for JXL (a typed model with an ICC would silently lose it)
   Reverses: refuse `Metadata::icc` in `with_metadata` and require `with_color` explicitly

Appended during the repair round (same shape):

12. Repairing the incremental mutation gate's timeouts
   Taken:    restructure the JXL container walk so its progress is local to it - `parse_box_header` reports only what the header claims and slices nothing, `container_metadata_boxes` owns every bound and steps at least the 8-byte minimum header per iteration - which makes the seven return-value mutants of the old `read_box` fail an assertion instead of hanging; the one MISSED mutant (`size < 8` -> `size <= 8`) is killed by a new test asserting the empty-box boundary in both directions
   Rejected: excluding `read_box` from mutation in `.cargo/mutants.toml` - a signature-anchored hang exclusion is warranted when a mutant is unkillable in principle, and this one is killable once the loop cannot hang; raising the mutants timeout - that hides the hang rather than removing it and leaves the gate red on four shards
   Reverses: restore the offset-returning `read_box` and add a signature-anchored exclusion
13. The gamut-heic half of the deliverable
   Taken:    commit it. The Summary and decision 7 both describe `HeifImage::blocks` / `HeifImage::metadata` and the two ungated lenses, and the work existed complete with tests but had never been committed, so the described pull request and the pushed one did not match. It is now `feat(heic): wire the gamut-metadata facade behind a metadata feature`
   Rejected: dropping it and narrowing the Summary - gamut-heic is the third of the three crates this branch's boundary (decision 1) admits and no open pull request touches its metadata surface; leaving it uncommitted - a pushed commit is the only durable state
   Reverses: revert the heic commit and strike gamut-heic from the Summary
14. The umbrella's feature forwarding
   Taken:    not done on this branch. `crates/gamut/Cargo.toml` is not a file this branch touches, so adding `gamut-{jpeg,jxl,heic}?/metadata` to the umbrella's `metadata` feature widens the manifest; the Summary's claim that it was already done is corrected in place and the change is filed as #622
   Rejected: adding the three weak forwards anyway on the grounds that they are three low-risk lines - low risk is not the test, the manifest is
   Reverses: add the forwards here once a manifest revision covers crates/gamut
15. The umbrella's feature forwarding, reversed
   Taken:    add the three weak forwards to `crates/gamut/Cargo.toml`, the manifest having been widened to
             include that one file. Decision 14 declined the same change for a reason that still holds - the
             manifest, not the size of the diff, is what bounds a branch - but the outcome it protected was a
             facade wiring that a caller arriving through the umbrella, the crate this project points people
             at, could not switch on; a feature nobody can enable from the front door is not finished work,
             and #622 would have left it that way behind a second pull request. Proved at the rustc
             invocation rather than by inspection, in both directions - see `## Validation`
   Rejected: leaving it to #622 - it defers the only part of the wiring a consumer meets first; a non-weak
             forward (`gamut-jpeg/metadata`) - it would pull the facade and its four leaf crates into every
             consumer of a format feature, a worse defect than the gap it closes; widening further to add a
             regression test under `crates/gamut/tests/` - the grant covers one manifest and a lockfile
             refresh, and the lockfile did not even need refreshing
   Reverses: drop the three forwards and reinstate decision 14
16. Pinning the umbrella's feature forwards
   Taken:    add `crates/gamut/tests/feature_forwarding.rs`, under a second manifest grant covering that
             directory. Two tests, two techniques, because only one of the two directions is observable
             from a compiled build: `the_metadata_feature_reaches_each_format_crates_accessors` names one
             accessor per crate - the fewest it takes to observe the three edges - each of which exists only
             under that crate's `metadata` feature, so a dropped forward is a compile error; and
             `every_format_metadata_forward_is_weak` reads the compiled-in manifest and asserts each entry
             carries the `?`. Nothing is called and no fixture is built, so a fixture bug or a signature
             change cannot fail a test whose subject is a feature edge. Both were falsified before landing:
             deleting a forward fails the first at compile time naming that crate, removing a `?` fails the
             second with the message about weakness, and the non-weak form is checked first so that dropping
             the `?` is diagnosed as dropping the `?`
   Rejected: the resolution test alone - it cannot see weakness, and weakness is the whole of what stops
             `metadata` alone from pulling three codecs into a build that asked for none, which this lane's
             own measurement identified as the worse defect; building fixtures and calling the accessors -
             it would let a parser or fixture defect fail a feature-graph pin
   Reverses: delete the file; the forwards then have nothing holding them again
   Note:     `crates/gamut/tests/` is mutation-invisible and `AGENTS.md` forbids pinning anything there *by
             choice*. This is not by choice - the edge is in the umbrella's own feature graph, no lower crate
             can observe who enabled its features, and the three format crates must not gain dev-dependency
             edges on one another - so it is the linkage exception the same rule names. The consequence is
             stated plainly: no mutant anywhere can kill these two tests, and the forwards are held by this
             file alone

Appended during the second review round (same shape):

17. L3 - the weakness pin's reach
   Taken:    slice the umbrella's own `metadata = [ ... ]` entry list out of the manifest and assert
             presence over that, not over the whole file, plus a manifest-wide count of one so a forward
             cannot also be attached to a format feature. Falsified with the reviewer's own attack: moving
             an entry into `jpeg = [ ... ]` now fails naming that, and duplicating it fails naming that,
             where before this round the move left both tests passing
   Rejected: leaving the whole-file match with the boundary disclosed - a pin a one-line manifest move
             defeats is the shape this run has been closing, and the disclosure was of a hole, not of a
             tradeoff; asserting over a parsed manifest via a TOML dependency - a new dev-dependency to
             read six lines of text, and the record names no new dependency
   Reverses: restore the whole-file match
18. L1 and L2 - the two overclaiming statements
   Taken:    correct both. `typed_wiring` names the three crates that gate the surface behind their own
             `metadata` feature and names `gamut-dng` as the one that does not (its dependency is
             unconditional, so `gamut-dng/metadata` is a hard cargo error). `supports` says what is true of
             a `const` table - it sees neither another crate's Cargo features nor the target, so it
             describes the surface a crate *defines* - and names the gated case (`gamut-jxl`'s reader under
             `decode`). The same two corrections are made in the README prose
   Rejected: weakening the table to match the prose - every cell was verified correct by the review, and
             the defect is in the prose; adding a runtime or feature-aware query - the release topology
             forbids the reverse edge, which is why the table is `const` at all
   Reverses: restore the two sentences
19. Design question 3 - the `ALL` constants' type
   Taken:    make `Format::ALL` and `Carrier::ALL` `&'static [Self]`. They sit on `#[non_exhaustive]`
             enums, so an appended variant would change a fixed-length array's *type* and break every
             caller who named the constant - the opposite of what `#[non_exhaustive]` promises, and far
             cheaper to fix before this new surface ships than after. `Direction` is exhaustive and keeps
             its array, with the docs saying why the two differ
   Rejected: shipping the arrays and revisiting at the next append - the break would land on callers, not
             on us; a generated iterator method instead - more surface for the same guarantee
   Reverses: restore the array types
20. Design questions 1, 2 and 4 - filed, not taken
   Taken:    file all three with the review's evidence (#626 ICC precedence, #627 a codestream fault
             discarding located boxes, #628 the accessor shape across crates) and change no behaviour.
             Document the ICC precedence exactly as it behaves - a present carrier overwrites, and for ICC
             the setter it overwrites is the colour dial - at `JxlEncoder::with_metadata`, at
             `with_encoded_metadata`, and in the facade README, so a caller is not surprised by a rule
             nobody has chosen yet
   Rejected: deciding any of them here - each is a behavioural or cross-crate fork, and the close of a
             review loop is the worst moment to take one; leaving the precedence undocumented until it is
             decided - a caller meets the behaviour now, not when the issue closes
   Reverses: close the three issues unfiled and decide them in this branch
21. The same array-typed `ALL` on `gamut_core::PixelFormat` - disclosed, not changed (appended fork; the
    record does not cover it)
   Taken:    leave it. `PixelFormat` is `#[non_exhaustive]` and `PixelFormat::ALL` is `[PixelFormat; 11]`,
             so decision 19's argument applies to it verbatim - but `gamut-core` is outside this branch's
             manifest, the constant is *already published* API (so changing it is a real break rather than
             a break avoided), and this round's bound allows three new issues, all three of which are
             spent. It is disclosed here and in the round report instead
   Rejected: widening the manifest to `crates/gamut-core/**` at the close of a loop; filing a fourth issue
             past the bound - the bound is the instruction, and reporting the fact upward is what it
             leaves open
   Reverses: file it and repeat decision 19 there

Correction, appended. Decision 21 above is superseded, and its text is left standing
unchanged so this correction can be checked against it. It records as Taken that the finding "is
disclosed here and in the round report instead" of being filed, and as Rejected "filing a fourth
issue past the bound - the bound is the instruction". The bound was subsequently lifted by the
operator as the wrong shape — a numeric cap on filings is not a safety property, and letting one
convert a finding into something nobody knows about is the failure it caused. The finding is now
filed as #629. Nothing else in decision 21 changes: the constant is still not touched, and for
the reasons it gives.

22. Filing the `gamut_core::PixelFormat::ALL` finding (supersedes decision 21)
   Taken:    file it as #629 with the constant, the enum's documented extensibility, the workspace-wide
             contrast (every other `ALL` is already a slice), and the reason this case differs from
             decision 19's - those constants had not shipped, this one is released at gamut-core 2.0.1, so
             converting it is itself the break it prevents later. The issue states plainly that this run
             did not change it and why, and lists the three ways out for whoever picks it up
   Rejected: leaving it disclosed only in this body and the round report - a disclosure nobody is
             notified of is a finding lost; changing it here - `gamut-core` is outside this branch's
             manifest and the change is a major bump of the crate every other crate sits on, which is not
             a decision a documentation round gets to take
   Reverses: close #629 unfiled

Unresolved review notes

  • Three design questions are open, not resolved: gamut-jxl: a present ICC in with_metadata silently replaces the caller's ColorSpec #626 (a present ICC replaces the caller's
    ColorSpec in JPEG XL), gamut-jxl: JxlDecoder::metadata loses located Exif/XMP boxes when the codestream is at fault #627 (a codestream fault discards container boxes this crate located),
    gamut-metadata: settle one shape for the format crates' typed accessors (blocks fallible or not, and whose error) #628 (the four wired crates disagree on whether blocks() is fallible and on whose error it
    returns). Each is filed with the review's evidence and none changes behaviour in this pull
    request. gamut-jxl: a present ICC in with_metadata silently replaces the caller's ColorSpec #626's current behaviour is documented so a caller is not surprised by it.

  • gamut_core::PixelFormat::ALL has the same defect decision 19 fixes here — a fixed-length
    array constant on a #[non_exhaustive] enum — and is not fixed: gamut-core is outside this
    branch's manifest, the constant is already published, and this round's three-issue budget is
    spent on the design questions above. Recorded here and reported upward so a human can file it.

  • A green local mutation run is not proof CI's mutation gate is green: the local runner caps
    address space (ulimit -v), so a mutant that hangs by allocating aborts and scores caught,
    where CI has no such cap and scores it TIMEOUT. This round adds no loop and no hand-written
    iterator, and all four Incremental shards were green at the head it started from.

  • The umbrella's metadata feature does not reach the format crates' metadata features (gamut: forward gamut-{jpeg,jxl,heic}'s metadata feature from the umbrella's own metadata #622).
    Until it does, gamut = { features = ["jpeg", "metadata"] } compiles the facade and the codec but
    not the wiring between them; a consumer depends on gamut-jpeg / gamut-jxl / gamut-heic
    directly to reach the typed accessors. Left undone deliberately — crates/gamut/Cargo.toml is
    outside this branch's manifest (decision 14).

  • No container-level exiv2 differential exists for any of the three crates (decision 6):
    tooling/exiv2-oracle is block-level and in-memory with no container reader compiled in. The
    located payloads are pinned byte-exact against each crate's own oracle (libjpeg-turbo, libheif, a
    raw box scan of libjxl output) and the leaf crates pin the payloads against exiv2; each affected
    STATUS.md names the untested cell.

  • No committed regression test pins the umbrella's three feature forwards. Resolved in this round by crates/gamut/tests/feature_forwarding.rs (decision 16), which pins both directions and was falsified before landing. The note is kept rather than deleted so the trail is legible. What remains true, and is a standing property rather than an open item: that pin is mutation-invisible.cargo/mutants.toml excludes crates/gamut/**, so no mutant anywhere can kill those two tests, and the three forwards are held by that file alone. It is in its only legal home (the linkage exception in AGENTS.md: the edge is in the umbrella's own feature graph and no lower crate can observe it), and the module docs say so at the test.

Add `gamut_metadata::capability`: `Format`, `Carrier` and `Direction`
enums (`repr(u8)`, append-only discriminants, `ALL` constants) with a
`const fn supports(format, carrier, direction)` answering whether the
format crate can locate or write a carrier as a raw payload, and
`const fn typed_wiring(format)` saying whether that crate also exposes
the facade's typed models behind its `metadata` feature.

The table is a transcription of each crate's STATUS.md; every arm cites
the row that justifies it, and a full-matrix test pins every cell. It is
a const table rather than a runtime registry because the format set is
the workspace's own and the release topology forbids the facade
depending on a format crate.

Refs #420, #216
Add an optional, normal dependency on gamut-metadata behind a new
`metadata` Cargo feature (off by default), and the gamut-dng pattern on
top of the raw APP-segment surface:

- `JpegMetadata::blocks` hands the located payloads over as
  `MetadataBlock`s (EXIF = the TIFF stream without `Exif\0\0`, XMP = the
  xpacket, ICC = the reassembled profile) and `JpegMetadata::metadata`
  parses them into a unified `Metadata`;
- `JpegEncoder::with_metadata(&Metadata)` embeds through the default
  `MetadataEmbedder` and `with_encoded_metadata(&EncodedMetadata)`
  accepts caller-chosen policies, routing each carrier to the existing
  raw setter. IPTC-IIM (APP13) and C2PA (APP11) blocks are typed
  `Unsupported`, never dropped; a manifest store is never copied
  forward.

The typed extract -> embed -> extract equality is pinned through the
stream. The exiv2 oracle has no JPEG reader, so the container-level
differential cell is recorded as untested in STATUS.md.

Refs #420
Add `JxlDecoder::metadata` -> `JxlMetadata { exif, xmp, icc }`, reading
a stream's `Exif` / `xml ` container boxes and its codestream ICC
profile without decoding pixels. jxl-rs consumes auxiliary boxes without
exposing them (jxl-rs #674) and its box-header parser is `pub(super)`,
so the crate walks the top-level box sequence itself: the 32-bit,
`size == 0` and 64-bit `largesize` forms, the `Exif` payload's
tiff-header offset applied (ISO/IEC 23008-12 A.2.1, reused by the JXL
container), first box of a kind wins, a `brob`-wrapped `Exif`/`xml `
box is a typed `Unsupported`, and every overrun is `InvalidInput`.

Behind a new optional `metadata` feature (a normal dependency on
gamut-metadata), add the gamut-dng pattern: `JxlMetadata::blocks` /
`metadata`, and `JxlEncoder::with_metadata(&Metadata)` /
`with_encoded_metadata(&EncodedMetadata)` routing EXIF (`Exif\0\0`
stripped) and XMP to the boxes and the ICC profile to `ColorSpec::Icc`.
IPTC-IIM and C2PA blocks are typed `Unsupported`; a manifest store is
never copied forward.

The read-back is pinned against what the encoder writes, the walk's size
forms and hostile-input refusals are unit-tested beside it, and the
facade's typed extract -> embed -> extract equality holds through the
container. The exiv2 oracle has no JPEG XL reader, so that cell is
recorded as untested in STATUS.md.

Refs #420
The `metadata` wiring landed without a nightly `cargo fmt --all` pass, so
`fmt-check` has been failing on this branch. Formatting only; no behaviour
changes.
The `metadata` wiring landed without a nightly `cargo fmt --all` pass, so
`fmt-check` has been failing on this branch. Formatting only; no behaviour
changes.
`read_box` returned the offset just past the box it read, so the walk's
progress lived in the value a callee returned: a `read_box` that reported a
non-advancing offset left `container_metadata_boxes` looping forever. Under
`cargo mutants --in-diff` that is seven return-value mutants of `read_box` that
no test can kill because they hang instead of failing, and the incremental gate
reports them as timeouts.

Split the header parse from the walk. `parse_box_header` now only reports what
the header claims -- type, header length, box length -- and slices nothing; the
walk owns every bound: the box must fit in what remains, it must be at least the
8-byte minimum header, and the body must be a range inside it. The step is then
at least 8 bytes per iteration whatever the parser reports, so the loop
terminates for any return value a mutant can produce and the mutants become
killable by an ordinary assertion.

Behaviour is unchanged: every fault keeps the message the tests already pin --
the 64-bit form declaring a `largesize` below its own 16-byte header still ends
as `malformed box size` (the body range is empty-to-negative), and a `largesize`
no address space can hold saturates and is reported by the overrun check.
The walk's minimum step is the 8-byte header, so a box whose `size` is exactly
that header is legal §4.2 framing carrying no payload. Nothing asserted it, and
`cargo mutants --in-diff` reported the boundary open: relaxing the rule to
`box_len <= 8` -- which rejects the empty box -- survived the suite.

Assert both halves of the boundary: an empty `free` box between the metadata
boxes is stepped over and the walk keeps going, and an empty `xml ` box yields
an empty payload rather than an absent one.
The crate located the Exif and XMP items and the `colr` property already, but
handed every payload back opaque, so a caller wanting a typed model had to know
the `ExifDataBlock` framing and the `colr` variants itself.

Add the two lenses that framing needs, ungated: `HeifItem::exif_tiff_stream`
applies the payload's 4-byte big-endian `exif_tiff_header_offset` and yields the
TIFF stream `gamut-exif` parses (ISO/IEC 23008-12 §A.2.1), refusing a non-Exif
item, a payload shorter than the offset field and an offset past the payload's
end; `HeifItem::icc_profile` yields the `rICC`/`prof` bytes whichever order the
item's `colr` properties are in, where `colour()` reports only the first.

Over them, behind an optional `metadata` feature (off by default, a normal
optional dependency so release ordering follows it), `HeifImage::blocks` hands
the three located payloads to the facade as `MetadataBlock`s and
`HeifImage::metadata` parses them into a unified `Metadata`. Both are fallible:
a hostile Exif item can carry a truncated or out-of-range offset, and a facade
parse failure is carried as `InvalidInput` with the facade's message, naming the
carrier, as the error's detail.

HEIF has no IPTC-IIM item type, and a C2PA manifest store lives in a top-level
`uuid` box outside the item model, so neither block is produced here;
`HeifContainer::c2pa` still locates the store and STATUS.md records that a
caller appends it itself.
…umbrella

`gamut-jpeg`, `gamut-jxl` and `gamut-heic` each gained an optional `metadata`
feature carrying the typed accessors (`blocks()`, `metadata()`,
`with_metadata`). The umbrella's own `metadata` feature enabled only the
metadata crates, so `gamut = { features = ["jpeg", "metadata"] }` compiled the
facade and the codec but not the wiring between them: reaching
`JpegMetadata::blocks` meant depending on `gamut-jpeg` directly, which is what
the umbrella exists to avoid.

Add the three weak forwards. Weak (`?/`) is what keeps both directions honest:
`metadata` alone still pulls in no codec, and a format alone still pulls in no
facade -- each forward fires only when that format's feature already brought the
crate into the graph.

Verified at the rustc invocation rather than by inspection: with
`--features jpeg,jxl,heic,metadata` all three crates are compiled with
`--cfg feature="metadata"` and five accessors named only through `gamut::` go
from a compile error to a compile; with `--features jpeg,jxl,heic` the same
three are compiled without it; with the formats alone no facade crate is in the
dependency graph, and with `metadata` alone no codec crate is.

No new feature name is introduced, so `gamut-ffi`'s mirrored table is unchanged,
and no package is added, so the lockfile is unchanged.
The forwards added in the previous commit had nothing holding them: `mise run
test` reported the same 3816 tests before and after them, because no test named
anything the forwards switch on. A feature edge that nothing notices when it
disappears is the defect this suite exists to catch, so it should not be the
shape the fix itself ships in.

Pin both directions, by different techniques, because only one of them is
observable from a compiled build. `the_metadata_feature_reaches_each_format_
crates_accessors` names one accessor per crate -- the fewest it takes to observe
the three edges -- and each exists only under that crate's `metadata` feature,
so a dropped forward is a compile error; nothing is called and no fixture is
built, so a fixture bug or a signature change cannot fail it.
`every_format_metadata_forward_is_weak` reads the compiled-in manifest and
asserts each entry carries the `?`, which is the whole of what stops `metadata`
alone from pulling three codecs into a build that asked for none. It checks the
non-weak form first so that dropping the `?` is diagnosed as dropping the `?`
rather than as a missing forward.

Both were falsified before landing: deleting a forward fails the resolution test
at compile time naming that crate, and removing a `?` fails the weakness test
with the message about weakness.

`crates/gamut/tests/` is mutation-invisible and `AGENTS.md` forbids pinning
anything there *by choice*. This is the linkage exception the same rule names:
the edge under test is in the umbrella's own feature graph, no lower crate can
observe who enabled its features, and the three format crates must not gain
dev-dependency edges on one another. The module docs say so at the test.
The weakness guard matched its three forwards over the whole manifest, so it
could not see which feature list an entry belonged to. Moving
`"gamut-jpeg?/metadata"` out of `metadata = [ … ]` and into `jpeg = [ … ]` left
both assertions passing while making `gamut --features jpeg` resolve the format
crate with its metadata wiring — and therefore the entire facade — which is the
build the weak form exists to prevent.

Slice the feature's own entry list out of the manifest and assert presence over
that, plus a manifest-wide count of one so the entry cannot also be attached to
a format feature. Both attacks now fail, each naming its own cause.

The resolution half named one accessor per crate, so a dropped forward and a
renamed accessor produced the same compile error. Add a feature witness per
crate — the facade's `Metadata`, re-exported under each crate's own `metadata`
cfg — which the forward breaks and a rename does not, so the two faults differ
by which errors appear.
The table's prose asserted two things the crates disprove.

`typed_wiring` was documented as answering for "that crate's `metadata` Cargo
feature". `gamut-dng` has no such feature — it depends on `gamut-metadata`
unconditionally, since its `DngMetadata` holds the facade's `Exif` by value — so
a reader following the instruction reaches a hard cargo error. Three of the four
wired crates gate the surface; name them, and name DNG as the one that does not.

`supports` was documented as the surface "every format crate ships
unconditionally". `gamut-jxl` gates its reader on `decode` and its encoder on
`encode`, so a build with `default-features = false, features = ["encode"]`
compiles no reader while the table answers `true` for `Read`. A `const fn` can
see neither another crate's features nor the target, so say what is true: the
table describes the surface a crate defines, not what a build compiled, and name
the gated case. The cells themselves are unchanged and were verified correct.
`Format::ALL` and `Carrier::ALL` were fixed-length arrays on `#[non_exhaustive]`
enums, so appending a variant would change each constant's *type* and break
every caller who had named one — the exact breakage `#[non_exhaustive]` exists
to prevent, and it would have shipped baked into new API. A `&'static [Self]`
absorbs the append.

`Direction` is exhaustive and cannot gain a variant, so it keeps its array; the
docs now say why the two differ. The discriminant pin collects instead of
mapping over an array, which keeps the length inside what it compares.
…spec

`with_metadata` routes a present ICC profile to `with_color(ColorSpec::Icc(..))`,
so it overwrites a colour encoding the caller chose through a different builder
call, not merely an earlier profile — JPEG XL is the one wired format where the
profile *is* the codestream's colour encoding rather than a container payload.
The docs said only that absent carriers leave earlier settings untouched, which
left the present case for a caller to discover.

State the precedence and the ordering it implies. Whether last-write-wins is the
right rule here, or the conflict should be refused, is issue #626; this records
today's behaviour rather than settling it.
The consumer-integration section described what `with_metadata` embeds but not
what it displaces. A carrier absent from the model leaves an earlier setting
untouched and a present one overwrites it, which matters most for ICC in JPEG
XL, where the profile is the codestream's colour encoding rather than a
container box. Say so where a caller reads about the seam, and point at #626 for
the open question of whether that rule is the right one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant