From 61b31ae800546eeb2b8fae6ca142512f53d8851f Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 9 Sep 2026 14:45:55 -0400 Subject: [PATCH 01/25] docs(readme): describe each crate as it is, not as it was scaffolded The crates table presented five crates shipping at v1.0+ as unstarted scaffolding citing issue #34 -- which is closed, so the citation was wrong twice over -- and had no row at all for four crates that carry real implementations: gamut-codec-abi, gamut-dng, gamut-jpeg and gamut-tonemap. Each status cell now comes from that crate's STATUS.md and its Cargo.toml version, stated as what the crate is today rather than as a milestone. A closed issue is cited only as a completion marker, which is the table's existing convention; the only open issues cited are #242 and the five the issue enumerated: gamut-core was "WIP" though v1-stabilized under #177 and now at 2.0.1, gamut-isobmff "finalizing api" at 2.0.1, gamut-ffi "placeholder" though the #280 provider boundary ships, and gamut-jxl/gamut-jxl-sys carried "(v1, ...)" markers at 0.4.0 and 0.1.0. Two purpose cells contradicted AGENTS.md. gamut-tiff said "self-contained (own IFD/tag container)" though it is built on the shared gamut-ifd primitive, as the gamut-ifd row three lines above it already said; gamut-heic said "encoder/decoder | placeholder" though it is a decode-only container with S1-S7 delivered, and gamut has no HEIF encoder by charter. The Scope prose carried the same defect, calling TIFF and the metadata primitives newly scaffolded in the paragraph that stakes the exiftool-class tag coverage goal. It now says where that goal stands -- MakerNote payloads round-trip verbatim but are not decoded, and tag breadth beyond the standard dictionary is still open -- and points at No crate's STATUS.md or Cargo.toml is touched: they are the authority, and the README is what was wrong. Refs #425 --- README.md | 67 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 57a77973..36a58205 100644 --- a/README.md +++ b/README.md @@ -69,18 +69,27 @@ In 2026, `gamut` started when there were no robust, well-tested Rust implementat ### Scope The initial focus is **AVIF, WebP, and JPEG** — the formats with the best -size-versus-compatibility tradeoff today. **JPEG XL** (`gamut-jxl`) is now implemented as an +size-versus-compatibility tradeoff today. **JPEG XL** (`gamut-jxl`) is implemented as an encoder + decoder (issue #243) — uniquely, by wrapping the format's reference implementations (libjxl for encode, the pure-Rust jxl-rs for decode) rather than clean-slate, a deliberate -maintainer decision documented in that crate. The other format crates in the tree (HEIC, VVC, -AV2) are scaffolding, and may move or be dropped as the focus sharpens. **TIFF 6.0** -(`gamut-tiff`) is newly scaffolded and under active implementation (issue #107) as a -royalty-free, natively still-image format — a good long-term fit for the image-first focus. - -Alongside the codecs, gamut is growing **shared image-metadata primitives** (issue #34) — EXIF, +maintainer decision documented in that crate. **TIFF 6.0** (`gamut-tiff`) is implemented at v1 +(issue #107) as a royalty-free, natively still-image format — a good long-term fit for the +image-first focus — with YCbCr/Lab and JPEG-in-TIFF still deferred, and **DNG** (`gamut-dng`) +is a raw encoder + decoder built as a TIFF/EP profile over the same `gamut-ifd` container core. +`gamut-heic` is a decode-only HEIF container, with the HEVC bitstream itself left to a pluggable +backend rather than decoded here. Of the format crates, only `gamut-vvc` and `gamut-av2` are +still scaffolding, and they may move or be dropped as the focus sharpens. + +Alongside the codecs, gamut ships **shared image-metadata primitives** (issue #34) — EXIF, XMP, ICC, and IPTC, plus the TIFF/IFD container core (`gamut-ifd`) that EXIF builds on — so the -format crates can read, preserve, and embed metadata. These are newly scaffolded; the long-term -goal is de-facto, fully-featured implementations (for EXIF, exiftool-class tag coverage). +format crates can read, preserve, and embed metadata, reached through one `gamut-metadata` +facade. These are all at v1 or later, each gated against a reference implementation (exiv2 for +EXIF and IPTC, Adobe XMPCore for XMP); `gamut-cmm` adds the ICC transform engine over the +profiles `gamut-icc` parses, is feature-complete against Little-CMS, and is the one piece not +yet published. The long-term goal is de-facto, fully-featured implementations — for EXIF, +exiftool-class tag coverage — which today's v1 crates do not yet reach: per-vendor MakerNote +payloads round-trip verbatim but are not decoded, and tag breadth beyond the standard dictionary +is still open. Issue #416 tracks that gap, measured. **gamut is image-first.** Even where a format's codec (AV1, AV2, VVC, HEVC) is fundamentally a video codec, gamut implements only the intra-frame, still-image subset those formats use — no @@ -109,33 +118,37 @@ format. | Crate | Purpose | Status | | ----------------- | ---------------------------------------------------------------------- | -------------------------------------- | | `gamut` | Umbrella crate; re-exports the format crates behind Cargo features | implemented | -| `gamut-core` | Core traits (`Encoder`/`Decoder`), image buffers, dimensions, errors | WIP | -| `gamut-color` | Color spaces, pixel formats, bit depths, chroma subsampling, transfers | stabilizing api | -| `gamut-dsp` | Shared DSP: DCT, wavelet transforms, quantization, filtering | stabilizing api | +| `gamut-core` | Core traits (`Encoder`/`Decoder`), image buffers, dimensions, errors, `convert` | stable (v1, #177); surface frozen, pixel conversion since #268 | +| `gamut-color` | Color spaces, pixel formats, bit depths, chroma subsampling, transfers | stable (v1, #179) | +| `gamut-dsp` | Shared DSP: DCT, wavelet transforms, quantization, filtering | stable (v1, #192) | | `gamut-bitstream` | Bit readers/writers and entropy coders (ANS, arithmetic, Huffman) | stabilizing api | -| `gamut-isobmff` | ISOBMFF container utilities (AVIF, HEIC) | finalizing api | +| `gamut-tonemap` | Tone-mapping curves (`ToneCurve` + Reinhard/ACES/Hable/Drago) for HDR→SDR | stable (v1, #188); eight operators, surface frozen | +| `gamut-codec-abi` | Shared codestream-backend seam: `repr(C)` vtables + the backend registry | in use by gamut-jpeg (#277) and gamut-heic (#273) | +| `gamut-isobmff` | ISOBMFF container utilities (AVIF, HEIC) | stable (v2); structure only, codestream carried opaquely | | `gamut-riff` | RIFF container utilities (WebP) | stable (v1, #186) | | `gamut-av1` | AV1 still-image (intra-frame) encoder — the codec layer beneath AVIF | implemented lossless and lossy (alpha) | | `gamut-av2` | AV2 still-image (intra-frame) encoder/decoder — AV1's successor | placeholder | -| `gamut-avif` | AVIF encoder — AV1 still frames in an ISOBMFF container | stabilizing with gamut-av1 | -| `gamut-jxl` | JPEG XL encoder (libjxl wrap) + decoder (pure-Rust jxl-rs) | encoder + decoder (v1, #243) | -| `gamut-jxl-sys` | Static libjxl 0.12.0 FFI declarations — native core of gamut-jxl encode | encoder backend (v1, #243) | +| `gamut-avif` | AVIF encoder + container decoder — AV1 still frames in ISOBMFF | encoder + decoder (v1; decoder #250); 10/12-bit deferred | +| `gamut-jxl` | JPEG XL encoder (libjxl wrap) + decoder (pure-Rust jxl-rs) | encoder + decoder (#243) | +| `gamut-jxl-sys` | Static libjxl 0.12.0 FFI declarations — native core of gamut-jxl encode | encoder backend (#243) | +| `gamut-jpeg` | JPEG-1 (ISO/IEC 10918-1) encoder + decoder — baseline & progressive, XYB | encoder + decoder (#28, P1–P13); unpublished | | `gamut-webp` | WebP (intra-frame VP8/VP8L) encoder/decoder | implemented VP8 + VP8L (+alpha, metadata, effort/near-lossless) | -| `gamut-heic` | HEIC/HEIF still-image (HEVC intra) encoder/decoder | placeholder | +| `gamut-heic` | HEIC/HEIF still-image container **decoder** — HEVC via a pluggable backend | decode-only container (#238, S1–S7); no encoder, by charter | | `gamut-vvc` | VVC (H.266) still-image (intra) encoder/decoder | placeholder | -| `gamut-ifd` | TIFF/IFD container core (byte order, field types, IFD I/O) — EXIF+TIFF | scaffolding (impl in progress, #34) | -| `gamut-exif` | EXIF (Exif 3.0) metadata parser/serializer — built on gamut-ifd | scaffolding (impl in progress, #34) | +| `gamut-ifd` | TIFF/IFD container core (byte order, field types, IFD I/O) — EXIF+TIFF | stable (v2, byte completeness #263); BigTIFF behind `bigtiff` | +| `gamut-exif` | EXIF (Exif 3.0) metadata parser/serializer — built on gamut-ifd | stable (v1, #194); MakerNote preserved verbatim, not decoded | | `gamut-icc` | ICC color profile (ICC.1:2022) parser/serializer | stable (v1, #180) | | `gamut-cmm` | ICC colour management module (transform engine) over gamut-icc profiles | epic #323 complete (P1–P7); unpublished | -| `gamut-xmp` | XMP (RDF/XML) metadata parser/serializer | scaffolding (impl in progress, #34) | -| `gamut-iptc` | IPTC photo metadata (IIM + Core/Extension over XMP) | scaffolding (impl in progress, #34) | -| `gamut-metadata` | Unified metadata facade over EXIF/XMP/ICC/IPTC (extract + embed) | scaffolding (impl in progress, #34) | -| `gamut-tiff` | TIFF 6.0 encoder/decoder — self-contained (own IFD/tag container) | baseline + extensions (YCbCr/Lab/JPEG WIP) | -| `gamut-deflate` | DEFLATE/zlib encoder (zopfli-class) — the compression under gamut-png | encoder (decoding stays on miniz_oxide) | -| `gamut-png` | PNG (W3C 3rd edition) encoder + spec-compliant decoder | encoder (#24) + decoder (#249) | -| `gamut-cli` | `gamut` CLI sandbox: encode AVIF + inspect the shared primitives | ready for use | +| `gamut-xmp` | XMP (RDF/XML) metadata parser/serializer | stable (v1, #189); canonical serializer, UTF-8 packets only | +| `gamut-iptc` | IPTC photo metadata (IIM + Core/Extension over XMP) | stable (v1, #182); Extension structures pass through as raw XMP | +| `gamut-metadata` | Unified metadata facade over EXIF/XMP/ICC/IPTC (extract + embed) | stable (v1); orchestration only, C2PA carried opaquely | +| `gamut-tiff` | TIFF 6.0 encoder/decoder — on the shared `gamut-ifd` container core | stable (v1, #107); YCbCr/Lab and JPEG-in-TIFF deferred | +| `gamut-dng` | DNG 1.7.1 raw encoder + decoder — a TIFF/EP profile over `gamut-ifd` | encoder + decoder (v1, #109), Adobe DNG SDK-gated; unpublished | +| `gamut-deflate` | DEFLATE/zlib encoder (zopfli-class) — the compression under gamut-png | encoder (#195); decoding stays on miniz_oxide | +| `gamut-png` | PNG (W3C 3rd edition) encoder + spec-compliant decoder | encoder (#24) + decoder (#249); unpublished | +| `gamut-cli` | `gamut` CLI sandbox: encode AVIF/WebP + inspect the shared primitives | ready for use | | `gamut-wasm` | WebAssembly bindings | placeholder | -| `gamut-ffi` | C-compatible FFI bindings | placeholder | +| `gamut-ffi` | C-compatible FFI bindings | provider boundary shipped (#280); consumer entry points pending (#242) | All cargo metadata except per-crate `version` is centralized in the root `[workspace.package]` / `[workspace.dependencies]`; each crate inherits the shared fields via From 275640a01d91a91d0893fcb600007da4aef1f891 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 9 Sep 2026 14:46:05 -0400 Subject: [PATCH 02/25] chore(check-readme-crates): gate the crates table's membership Nothing read the README crates table, which is why eighteen rows went wrong and four crates lost their row entirely before #425 caught it: `mise run versions` only lists `name version` and asserts nothing, and `check-msrv` greps the README for a single number. `mise run check-readme-crates` diffs the table's crate names against `cargo metadata --no-deps` in both directions -- a workspace crate with no row, and a row naming a crate that is not a member. Extraction is bounded to the "## Crates" section so the README's command table can never be mistaken for a crate row. It gates membership and not prose, deliberately. The Purpose and Status cells are written by a human against each crate's STATUS.md; a text gate over them would fossilise a particular wording, and generating the table would move prose a human maintains into a generator. Membership is the part a machine can settle. No CI job runs it yet -- that needs a workflow edit, filed as #524. Refs #425 --- mise.toml | 9 ++++ tooling/check-readme-crates/run.sh | 69 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100755 tooling/check-readme-crates/run.sh diff --git a/mise.toml b/mise.toml index 50ccb405..1d76fe44 100644 --- a/mise.toml +++ b/mise.toml @@ -296,6 +296,15 @@ run = "./tooling/mutants/run.sh --crate" description = "Enforce the mechanical test conventions in docs/testing.md" run = "./tooling/check-tests/run.sh" +# The README crates table drifted until eighteen rows were wrong and four crates had no row at +# all (issue #425), because nothing read it: `versions` only lists `name version` and asserts +# nothing, and `check-msrv` greps the README for one number. This gates *membership* -- every +# workspace crate has a row, every row names a real crate -- and deliberately not its prose, whose +# authority is each crate's STATUS.md. +[tasks.check-readme-crates] +description = "Verify the README crates table lists every workspace crate and no phantom ones" +run = "./tooling/check-readme-crates/run.sh" + [tasks.check-release-deps] description = "Reject publishable dev-only workspace edges that release-plz cannot order" run = '''cargo metadata --no-deps --format-version 1 | jq -r -e ' diff --git a/tooling/check-readme-crates/run.sh b/tooling/check-readme-crates/run.sh new file mode 100755 index 00000000..928da8ab --- /dev/null +++ b/tooling/check-readme-crates/run.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# The README's "## Crates" table is hand-maintained prose, and nothing read it. By issue #425 it +# had drifted far enough that eighteen rows needed correcting -- five shipped crates were still +# described as unstarted scaffolding citing a closed issue -- and four crates (gamut-codec-abi, +# gamut-dng, gamut-jpeg, gamut-tonemap) had no row at all. +# +# This guard checks the one property that is mechanical -- the table's *membership* -- and +# nothing else: +# +# * every workspace crate has a row, so a new crate cannot be added without documenting it; +# * every row names a crate that still exists, so a renamed or deleted crate cannot be left +# behind as a phantom row. +# +# What is deliberately NOT checked, and why: +# * The Purpose and Status cells. They are prose a human maintains, and their authority is the +# crate's own STATUS.md. A text gate over them would fossilise a particular wording, and a +# generated table would move prose a human writes into a generator -- so staleness of a row's +# *text* stays a review concern, not a lint. Membership is what a machine can settle. +# * The version. `mise run versions` already reports it, and the README deliberately states +# what each crate *is* rather than pinning a number that release-plz bumps. +set -euo pipefail + +readme="${1:-README.md}" + +test -f "$readme" || { + echo "check-readme-crates: no such file: $readme" + exit 1 +} + +# The rows of the "## Crates" table only. Bounded to that section so the README's other tables +# (the `mise run ...` command table) can never be mistaken for a crate row. +readme_crates="$( + awk '/^## Crates$/ { in_section = 1; next } /^## / { in_section = 0 } in_section' "$readme" | + sed -nE 's/^\| *`([a-z0-9-]+)` *\|.*/\1/p' | sort -u +)" + +test -n "$readme_crates" || { + echo "check-readme-crates: found no crate rows under '## Crates' in $readme" + exit 1 +} + +# The same source of truth `mise run versions` reads: --no-deps lists workspace members only. +workspace_crates="$( + cargo metadata --no-deps --format-version 1 | jq -r '.packages | sort_by(.name)[] | .name' +)" + +fail=0 + +missing="$(comm -23 <(echo "$workspace_crates") <(echo "$readme_crates"))" +if [ -n "$missing" ]; then + fail=1 + echo "check-readme-crates: workspace crates with no row in the $readme crates table:" + echo "$missing" | sed 's/^/ /' + echo " add a row (Crate | Purpose | Status), taking the status from the crate's STATUS.md." +fi + +phantom="$(comm -13 <(echo "$workspace_crates") <(echo "$readme_crates"))" +if [ -n "$phantom" ]; then + fail=1 + echo "check-readme-crates: $readme crates table names crates that are not workspace members:" + echo "$phantom" | sed 's/^/ /' + echo " drop the row, or fix the crate name it misspells." +fi + +if [ "$fail" -eq 0 ]; then + echo "README crates table lists every workspace crate ($(echo "$workspace_crates" | wc -l) crates)" +fi + +exit "$fail" From 80a545ae6303a20d963d90390e11e4eb9c7fc2bf Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Wed, 9 Sep 2026 15:04:31 -0400 Subject: [PATCH 03/25] fix(readme): correct claims an adversarial review falsified A review of the first two commits checked every new cell against its authority and falsified eight of them. Each is corrected against the evidence rather than reworded: - The Scope prose claimed the format crates reach metadata "through one gamut-metadata facade". They do not: only gamut-dng and the umbrella depend on it, and every other format crate still carries EXIF/XMP/ICC as raw byte blocks. That is the single largest item #416 exists to close, so asserting it as shipped was the worst error here. The paragraph now names it as the gap. - "unpublished" on gamut-jpeg, gamut-dng and gamut-png was inferred from a missing release tag. crates.io says all three are published at their current versions; only gamut-cmm is absent. The tag heuristic was unsound, and README's own Releases section repeats it -- filed as #527. - gamut-cmm is conformance-gated against Little-CMS but is not "feature-complete" against it: STATUS.md defers integer/f32 fast paths, extended-transform intent arrays and K-preserving intents, and #423 records it 6.6x slower. Its phase count was also stale at P1-P7; P8 (#372) is done. - gamut-avif ships 8/10/12-bit encode (#399), so "10/12-bit deferred" was wrong; what it lacks is an in-crate AV1 decoder, the codestream coming from the caller through the Av1StillDecoder seam. - gamut-codec-abi is used by six format crates, not the two named. - Three crates at 2.x were labelled "stable (v1)" while the same table wrote "v2" for gamut-ifd and gamut-isobmff at the same major. - docs/testing.md gives gamut-ifd no in-crate oracle and gamut-metadata a round-trip law, so "each gated against a reference implementation" was false for two of the six crates the sentence named; exiv2 and Adobe XMPCore are also one oracle, not two. - gamut-heic's slices span #238, #273, #303 and #429, so citing #238 for all seven misattributed five of them; gamut-cli encodes six formats, not two; gamut-dsp's purpose cell still advertised wavelets and filtering, which its frozen v1 surface does not include. The guard gains the robustness the same review found missing: LC_ALL=C on both sides of every comm so a locale cannot desynchronise them, an explicit diagnostic instead of a bare set -e abort when comm or cargo metadata fails, resolution of the repository root so running it from a subdirectory no longer reports a cwd mistake as a table defect, and a duplicate-row check so the table is a bijection rather than a set. Refs #425 --- README.md | 43 +++++++++++----------- mise.toml | 7 ++-- tooling/check-readme-crates/run.sh | 57 +++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 36a58205..5a553937 100644 --- a/README.md +++ b/README.md @@ -81,15 +81,18 @@ backend rather than decoded here. Of the format crates, only `gamut-vvc` and `ga still scaffolding, and they may move or be dropped as the focus sharpens. Alongside the codecs, gamut ships **shared image-metadata primitives** (issue #34) — EXIF, -XMP, ICC, and IPTC, plus the TIFF/IFD container core (`gamut-ifd`) that EXIF builds on — so the -format crates can read, preserve, and embed metadata, reached through one `gamut-metadata` -facade. These are all at v1 or later, each gated against a reference implementation (exiv2 for -EXIF and IPTC, Adobe XMPCore for XMP); `gamut-cmm` adds the ICC transform engine over the -profiles `gamut-icc` parses, is feature-complete against Little-CMS, and is the one piece not -yet published. The long-term goal is de-facto, fully-featured implementations — for EXIF, -exiftool-class tag coverage — which today's v1 crates do not yet reach: per-vendor MakerNote -payloads round-trip verbatim but are not decoded, and tag breadth beyond the standard dictionary -is still open. Issue #416 tracks that gap, measured. +XMP, ICC, and IPTC, plus the TIFF/IFD container core (`gamut-ifd`) that EXIF builds on. These are +all at v1 or later; EXIF, IPTC and XMP are gated differentially against exiv2 (which bundles +Adobe's XMPCore), and ICC against Little-CMS. `gamut-cmm` adds the ICC transform engine over the +profiles `gamut-icc` parses — conformance-gated against Little-CMS, though still well behind it +on throughput — and is the one crate here not yet on crates.io. + +The long-term goal is de-facto, fully-featured implementations — for EXIF, exiftool-class tag +coverage — and today's crates do not reach it yet. The largest gap is reach rather than depth: +the `gamut-metadata` facade is consumed by `gamut-dng` and the umbrella only, so every other +format crate still carries EXIF/XMP/ICC as raw byte blocks instead of calling the typed path. +Behind that sit per-vendor MakerNote payloads, which round-trip verbatim but are not decoded, and +tag breadth beyond the standard dictionary. Issue #416 tracks the whole gap, measured. **gamut is image-first.** Even where a format's codec (AV1, AV2, VVC, HEVC) is fundamentally a video codec, gamut implements only the intra-frame, still-image subset those formats use — no @@ -118,35 +121,35 @@ format. | Crate | Purpose | Status | | ----------------- | ---------------------------------------------------------------------- | -------------------------------------- | | `gamut` | Umbrella crate; re-exports the format crates behind Cargo features | implemented | -| `gamut-core` | Core traits (`Encoder`/`Decoder`), image buffers, dimensions, errors, `convert` | stable (v1, #177); surface frozen, pixel conversion since #268 | -| `gamut-color` | Color spaces, pixel formats, bit depths, chroma subsampling, transfers | stable (v1, #179) | -| `gamut-dsp` | Shared DSP: DCT, wavelet transforms, quantization, filtering | stable (v1, #192) | +| `gamut-core` | Core traits (`Encoder`/`Decoder`), image buffers, dimensions, errors, `convert` | stable (v2; v1 under #177), pixel conversion added by #268 | +| `gamut-color` | Color spaces, pixel formats, bit depths, chroma subsampling, transfers | stable (v2; v1 under #179) | +| `gamut-dsp` | Shared DSP kernels: AV1 DCT/ADST/identity/WHT, quantization rounding | stable (v2; v1 under #192) | | `gamut-bitstream` | Bit readers/writers and entropy coders (ANS, arithmetic, Huffman) | stabilizing api | | `gamut-tonemap` | Tone-mapping curves (`ToneCurve` + Reinhard/ACES/Hable/Drago) for HDR→SDR | stable (v1, #188); eight operators, surface frozen | -| `gamut-codec-abi` | Shared codestream-backend seam: `repr(C)` vtables + the backend registry | in use by gamut-jpeg (#277) and gamut-heic (#273) | +| `gamut-codec-abi` | Shared codestream-backend seam: `repr(C)` vtables + the backend registry | in use by avif, heic, jpeg, jxl, png and webp | | `gamut-isobmff` | ISOBMFF container utilities (AVIF, HEIC) | stable (v2); structure only, codestream carried opaquely | | `gamut-riff` | RIFF container utilities (WebP) | stable (v1, #186) | | `gamut-av1` | AV1 still-image (intra-frame) encoder — the codec layer beneath AVIF | implemented lossless and lossy (alpha) | | `gamut-av2` | AV2 still-image (intra-frame) encoder/decoder — AV1's successor | placeholder | -| `gamut-avif` | AVIF encoder + container decoder — AV1 still frames in ISOBMFF | encoder + decoder (v1; decoder #250); 10/12-bit deferred | +| `gamut-avif` | AVIF encoder + container decoder — AV1 still frames in ISOBMFF | encoder (v1, 8/10/12-bit) + container decode (#250); AV1 codestream decode via the `Av1StillDecoder` seam | | `gamut-jxl` | JPEG XL encoder (libjxl wrap) + decoder (pure-Rust jxl-rs) | encoder + decoder (#243) | | `gamut-jxl-sys` | Static libjxl 0.12.0 FFI declarations — native core of gamut-jxl encode | encoder backend (#243) | -| `gamut-jpeg` | JPEG-1 (ISO/IEC 10918-1) encoder + decoder — baseline & progressive, XYB | encoder + decoder (#28, P1–P13); unpublished | +| `gamut-jpeg` | JPEG-1 (ISO/IEC 10918-1) encoder + decoder — baseline & progressive, XYB | encoder + decoder (#28, P1–P13) | | `gamut-webp` | WebP (intra-frame VP8/VP8L) encoder/decoder | implemented VP8 + VP8L (+alpha, metadata, effort/near-lossless) | -| `gamut-heic` | HEIC/HEIF still-image container **decoder** — HEVC via a pluggable backend | decode-only container (#238, S1–S7); no encoder, by charter | +| `gamut-heic` | HEIC/HEIF still-image container **decoder** — HEVC via a pluggable backend | decode-only container (S1–S7); no encoder, by charter | | `gamut-vvc` | VVC (H.266) still-image (intra) encoder/decoder | placeholder | | `gamut-ifd` | TIFF/IFD container core (byte order, field types, IFD I/O) — EXIF+TIFF | stable (v2, byte completeness #263); BigTIFF behind `bigtiff` | | `gamut-exif` | EXIF (Exif 3.0) metadata parser/serializer — built on gamut-ifd | stable (v1, #194); MakerNote preserved verbatim, not decoded | | `gamut-icc` | ICC color profile (ICC.1:2022) parser/serializer | stable (v1, #180) | -| `gamut-cmm` | ICC colour management module (transform engine) over gamut-icc profiles | epic #323 complete (P1–P7); unpublished | +| `gamut-cmm` | ICC colour management module (transform engine) over gamut-icc profiles | epic #323 complete (P1–P8); not yet on crates.io | | `gamut-xmp` | XMP (RDF/XML) metadata parser/serializer | stable (v1, #189); canonical serializer, UTF-8 packets only | | `gamut-iptc` | IPTC photo metadata (IIM + Core/Extension over XMP) | stable (v1, #182); Extension structures pass through as raw XMP | | `gamut-metadata` | Unified metadata facade over EXIF/XMP/ICC/IPTC (extract + embed) | stable (v1); orchestration only, C2PA carried opaquely | | `gamut-tiff` | TIFF 6.0 encoder/decoder — on the shared `gamut-ifd` container core | stable (v1, #107); YCbCr/Lab and JPEG-in-TIFF deferred | -| `gamut-dng` | DNG 1.7.1 raw encoder + decoder — a TIFF/EP profile over `gamut-ifd` | encoder + decoder (v1, #109), Adobe DNG SDK-gated; unpublished | +| `gamut-dng` | DNG 1.7.1 raw encoder + decoder — a TIFF/EP profile over `gamut-ifd` | encoder + decoder (v1, #109), Adobe DNG SDK-gated | | `gamut-deflate` | DEFLATE/zlib encoder (zopfli-class) — the compression under gamut-png | encoder (#195); decoding stays on miniz_oxide | -| `gamut-png` | PNG (W3C 3rd edition) encoder + spec-compliant decoder | encoder (#24) + decoder (#249); unpublished | -| `gamut-cli` | `gamut` CLI sandbox: encode AVIF/WebP + inspect the shared primitives | ready for use | +| `gamut-png` | PNG (W3C 3rd edition) encoder + spec-compliant decoder | encoder (#24) + decoder (#249) | +| `gamut-cli` | `gamut` CLI sandbox: encode AVIF/WebP/TIFF/PNG/JXL/JPEG + inspect the primitives | ready for use | | `gamut-wasm` | WebAssembly bindings | placeholder | | `gamut-ffi` | C-compatible FFI bindings | provider boundary shipped (#280); consumer entry points pending (#242) | diff --git a/mise.toml b/mise.toml index 1d76fe44..912e303e 100644 --- a/mise.toml +++ b/mise.toml @@ -296,9 +296,10 @@ run = "./tooling/mutants/run.sh --crate" description = "Enforce the mechanical test conventions in docs/testing.md" run = "./tooling/check-tests/run.sh" -# The README crates table drifted until eighteen rows were wrong and four crates had no row at -# all (issue #425), because nothing read it: `versions` only lists `name version` and asserts -# nothing, and `check-msrv` greps the README for one number. This gates *membership* -- every +# Nothing read the README crates table, so it drifted: fixing issue #425 meant correcting +# eighteen rows and adding four crates that had no row at all. `versions` only lists +# `name version` and asserts nothing, and `check-msrv` greps the README for one number, so +# neither would have caught it. This gates the table's *membership* -- every # workspace crate has a row, every row names a real crate -- and deliberately not its prose, whose # authority is each crate's STATUS.md. [tasks.check-readme-crates] diff --git a/tooling/check-readme-crates/run.sh b/tooling/check-readme-crates/run.sh index 928da8ab..5e9921e9 100755 --- a/tooling/check-readme-crates/run.sh +++ b/tooling/check-readme-crates/run.sh @@ -9,7 +9,8 @@ # # * every workspace crate has a row, so a new crate cannot be added without documenting it; # * every row names a crate that still exists, so a renamed or deleted crate cannot be left -# behind as a phantom row. +# behind as a phantom row; +# * no crate is listed twice, so the table stays a bijection rather than a set. # # What is deliberately NOT checked, and why: # * The Purpose and Status cells. They are prose a human maintains, and their authority is the @@ -20,7 +21,19 @@ # what each crate *is* rather than pinning a number that release-plz bumps. set -euo pipefail -readme="${1:-README.md}" +# With no argument, check the repository's own README from wherever the task was invoked. With +# one, check that file as given -- so a caller can point the guard at a candidate README without +# the working directory changing what "README.md" means. +if [ "$#" -eq 0 ]; then + root="$(git rev-parse --show-toplevel)" || { + echo "check-readme-crates: not inside a git repository, and no README path was given" + exit 1 + } + cd "$root" || exit 1 + readme="README.md" +else + readme="$1" +fi test -f "$readme" || { echo "check-readme-crates: no such file: $readme" @@ -29,24 +42,50 @@ test -f "$readme" || { # The rows of the "## Crates" table only. Bounded to that section so the README's other tables # (the `mise run ...` command table) can never be mistaken for a crate row. -readme_crates="$( +rows="$( awk '/^## Crates$/ { in_section = 1; next } /^## / { in_section = 0 } in_section' "$readme" | - sed -nE 's/^\| *`([a-z0-9-]+)` *\|.*/\1/p' | sort -u + sed -nE 's/^\| *`([a-z0-9-]+)` *\|.*/\1/p' )" -test -n "$readme_crates" || { +test -n "$rows" || { echo "check-readme-crates: found no crate rows under '## Crates' in $readme" exit 1 } +fail=0 + +# LC_ALL=C throughout: `comm` exits non-zero on input it considers unsorted, and jq's `sort_by` +# below orders by codepoint. Collating both sides the same way keeps them comparable under any +# ambient locale. +readme_crates="$(echo "$rows" | LC_ALL=C sort -u)" + +duplicates="$(echo "$rows" | LC_ALL=C sort | uniq -d)" +if [ -n "$duplicates" ]; then + fail=1 + echo "check-readme-crates: crates listed more than once in the $readme crates table:" + echo "$duplicates" | sed 's/^/ /' +fi + # The same source of truth `mise run versions` reads: --no-deps lists workspace members only. workspace_crates="$( cargo metadata --no-deps --format-version 1 | jq -r '.packages | sort_by(.name)[] | .name' -)" +)" || { + echo "check-readme-crates: could not read the workspace crate list from cargo metadata" + exit 1 +} -fail=0 +# `comm` failing is a bug in this script (unsorted input), not a README defect, so it is reported +# as itself rather than being swallowed by `set -e` into a bare exit. The diagnostic goes to +# stderr because this runs inside a command substitution -- on stdout it would be captured as if +# it were a crate name instead of shown. +compare() { + LC_ALL=C comm "$1" <(echo "$workspace_crates") <(echo "$readme_crates") || { + echo "check-readme-crates: internal error comparing crate lists (comm $1)" >&2 + exit 1 + } +} -missing="$(comm -23 <(echo "$workspace_crates") <(echo "$readme_crates"))" +missing="$(compare -23)" if [ -n "$missing" ]; then fail=1 echo "check-readme-crates: workspace crates with no row in the $readme crates table:" @@ -54,7 +93,7 @@ if [ -n "$missing" ]; then echo " add a row (Crate | Purpose | Status), taking the status from the crate's STATUS.md." fi -phantom="$(comm -13 <(echo "$workspace_crates") <(echo "$readme_crates"))" +phantom="$(compare -13)" if [ -n "$phantom" ]; then fail=1 echo "check-readme-crates: $readme crates table names crates that are not workspace members:" From 2fb38c297e27187c80b90124b82c2bfab56d596f Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Thu, 10 Sep 2026 00:11:57 -0400 Subject: [PATCH 04/25] fix(readme): make every crate row true of its crate, not of its STATUS.md Round-1 review of the crates table found that membership -- the property the new guard checks -- was never the failure mode that mattered. Text staleness was, and six rows were still wrong, four of them rows the first pass never opened. * gamut-bitstream advertised ANS and Huffman entropy coders. lib.rs says both "are not implemented yet" and the `pub use` list carries the AV1 symbol coder alone. Its "stabilizing api" status had no authority either: the crate has no STATUS.md and no v1 release issue, so the cell now states the version and the two gaps. * gamut-dsp had been narrowed to AV1-only wording, faithfully to STATUS.md:67, which lists the `jpeg` module as deferred -- while lib.rs:43 declares `pub mod jpeg` and ships fdct8x8/idct8x8. The rule this settles: STATUS.md is the starting authority, but where it contradicts lib.rs, lib.rs wins. The stale files are #545. * gamut-codec-abi undercounted its consumers. gamut-ffi depends on it unconditionally, const-asserts ABI_VERSION and MAX_PLANES, and its whole shipped surface is push_backend over the seam: seven, not six. * gamut-av1 hid a decoder that `default = ["decode"]` turns on -- 4223 lines behind `Av1Decoder` -- and marked its encoder "(alpha)", a legend the table never defines and which gamut-webp two rows below uses for the alpha channel instead. * gamut-png claimed a "spec-compliant decoder" of the W3C 3rd edition, where APNG is normative and out of scope here. * gamut-color named five things for a crate with fifteen public modules, omitting the whole f64 colour-science half and its Tier-1 reproducibility caveat. The guard grows the one structural property the table's own validation was checking by hand: a crate row is three cells with a non-empty Purpose and Status. Membership reads a row's first cell only, so `| `gamut-core` |` passed it while documenting nothing. Refs #425 --- README.md | 13 ++++---- mise.toml | 2 +- tooling/check-readme-crates/run.sh | 51 ++++++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5a553937..0458d89c 100644 --- a/README.md +++ b/README.md @@ -122,14 +122,14 @@ format. | ----------------- | ---------------------------------------------------------------------- | -------------------------------------- | | `gamut` | Umbrella crate; re-exports the format crates behind Cargo features | implemented | | `gamut-core` | Core traits (`Encoder`/`Decoder`), image buffers, dimensions, errors, `convert` | stable (v2; v1 under #177), pixel conversion added by #268 | -| `gamut-color` | Color spaces, pixel formats, bit depths, chroma subsampling, transfers | stable (v2; v1 under #179) | -| `gamut-dsp` | Shared DSP kernels: AV1 DCT/ADST/identity/WHT, quantization rounding | stable (v2; v1 under #192) | -| `gamut-bitstream` | Bit readers/writers and entropy coders (ANS, arithmetic, Huffman) | stabilizing api | +| `gamut-color` | Pixel formats, bit depths, chroma subsampling, CICP code points and planar buffers, plus the `f64` colour science (transfer, Lab/OKLab, XYB, matrix, gamut map, CCT, profile) | stable (v2; v1 under #179); the colour science is Tier-1 `f64`, not bit-reproducible | +| `gamut-dsp` | Shared DSP kernels: AV1 DCT/ADST/identity/WHT, the JPEG 8×8 forward/inverse DCT, quantization rounding | stable (v2; v1 under #192) | +| `gamut-bitstream` | Bit readers/writers, LEB128, the AV1 §8.2 symbol (arithmetic) coder, MSB-first sample packing | v0.2, no v1 release issue yet; the ANS and Huffman coders are not implemented | | `gamut-tonemap` | Tone-mapping curves (`ToneCurve` + Reinhard/ACES/Hable/Drago) for HDR→SDR | stable (v1, #188); eight operators, surface frozen | -| `gamut-codec-abi` | Shared codestream-backend seam: `repr(C)` vtables + the backend registry | in use by avif, heic, jpeg, jxl, png and webp | +| `gamut-codec-abi` | Shared codestream-backend seam: `repr(C)` vtables + the backend registry | in use by avif, ffi, heic, jpeg, jxl, png and webp | | `gamut-isobmff` | ISOBMFF container utilities (AVIF, HEIC) | stable (v2); structure only, codestream carried opaquely | | `gamut-riff` | RIFF container utilities (WebP) | stable (v1, #186) | -| `gamut-av1` | AV1 still-image (intra-frame) encoder — the codec layer beneath AVIF | implemented lossless and lossy (alpha) | +| `gamut-av1` | AV1 still-image (intra-frame) encoder + decoder — the codec layer beneath AVIF | encoder: lossless and lossy intra keyframes; decoder (default-on `decode` feature): 8-bit 4:4:4 intra key frames only (#259) | | `gamut-av2` | AV2 still-image (intra-frame) encoder/decoder — AV1's successor | placeholder | | `gamut-avif` | AVIF encoder + container decoder — AV1 still frames in ISOBMFF | encoder (v1, 8/10/12-bit) + container decode (#250); AV1 codestream decode via the `Av1StillDecoder` seam | | `gamut-jxl` | JPEG XL encoder (libjxl wrap) + decoder (pure-Rust jxl-rs) | encoder + decoder (#243) | @@ -148,7 +148,7 @@ format. | `gamut-tiff` | TIFF 6.0 encoder/decoder — on the shared `gamut-ifd` container core | stable (v1, #107); YCbCr/Lab and JPEG-in-TIFF deferred | | `gamut-dng` | DNG 1.7.1 raw encoder + decoder — a TIFF/EP profile over `gamut-ifd` | encoder + decoder (v1, #109), Adobe DNG SDK-gated | | `gamut-deflate` | DEFLATE/zlib encoder (zopfli-class) — the compression under gamut-png | encoder (#195); decoding stays on miniz_oxide | -| `gamut-png` | PNG (W3C 3rd edition) encoder + spec-compliant decoder | encoder (#24) + decoder (#249) | +| `gamut-png` | PNG (W3C 3rd edition) encoder + decoder, over the still-image subset | encoder (#24) + decoder (#249); APNG out of scope, so an animated PNG decodes as its default image | | `gamut-cli` | `gamut` CLI sandbox: encode AVIF/WebP/TIFF/PNG/JXL/JPEG + inspect the primitives | ready for use | | `gamut-wasm` | WebAssembly bindings | placeholder | | `gamut-ffi` | C-compatible FFI bindings | provider boundary shipped (#280); consumer entry points pending (#242) | @@ -229,6 +229,7 @@ cargo test --workspace | `mise run lint` | Lint with Clippy (warnings as errors) | | `mise run lint-fix` | Lint and auto-fix | | `mise run check-commits` | Check commits are Conventional Commits | +| `mise run check-readme-crates` | Check this README's crates table names every workspace crate, no phantom ones, and every row is a three-cell row | | `mise run coverage` | Run tests with coverage (min 80%) | | `mise run check-cross ` | Cross-compile-check the libs for a target (extended CI; master/manual) | | `mise run check-msrv` | Check the libs compile on the documented MSRV (extended CI; master/manual) | diff --git a/mise.toml b/mise.toml index 912e303e..1f4bbf77 100644 --- a/mise.toml +++ b/mise.toml @@ -303,7 +303,7 @@ run = "./tooling/check-tests/run.sh" # workspace crate has a row, every row names a real crate -- and deliberately not its prose, whose # authority is each crate's STATUS.md. [tasks.check-readme-crates] -description = "Verify the README crates table lists every workspace crate and no phantom ones" +description = "Verify the README crates table covers every workspace crate, has no phantom rows, and is well formed" run = "./tooling/check-readme-crates/run.sh" [tasks.check-release-deps] diff --git a/tooling/check-readme-crates/run.sh b/tooling/check-readme-crates/run.sh index 5e9921e9..b7dc4ddb 100755 --- a/tooling/check-readme-crates/run.sh +++ b/tooling/check-readme-crates/run.sh @@ -4,19 +4,22 @@ # described as unstarted scaffolding citing a closed issue -- and four crates (gamut-codec-abi, # gamut-dng, gamut-jpeg, gamut-tonemap) had no row at all. # -# This guard checks the one property that is mechanical -- the table's *membership* -- and -# nothing else: +# This guard checks what is mechanical about the table -- its *membership* and the *shape* of a +# row -- and nothing about the prose in it: # # * every workspace crate has a row, so a new crate cannot be added without documenting it; # * every row names a crate that still exists, so a renamed or deleted crate cannot be left # behind as a phantom row; -# * no crate is listed twice, so the table stays a bijection rather than a set. +# * no crate is listed twice, so the table stays a bijection rather than a set; +# * every crate row is a well-formed three-cell row with a non-empty Purpose and Status, so a +# row cannot be reduced to a bare crate name and still satisfy membership. # # What is deliberately NOT checked, and why: -# * The Purpose and Status cells. They are prose a human maintains, and their authority is the -# crate's own STATUS.md. A text gate over them would fossilise a particular wording, and a -# generated table would move prose a human writes into a generator -- so staleness of a row's -# *text* stays a review concern, not a lint. Membership is what a machine can settle. +# * The *wording* of the Purpose and Status cells. They are prose a human maintains, and their +# authority is the crate's own lib.rs, Cargo.toml and STATUS.md. A text gate over them would +# fossilise a particular phrasing, and a generated table would move prose a human writes into +# a generator -- so staleness of a row's *text* stays a review concern, not a lint. What a +# machine can settle is membership and shape, and it settles both. # * The version. `mise run versions` already reports it, and the README deliberately states # what each crate *is* rather than pinning a number that release-plz bumps. set -euo pipefail @@ -54,6 +57,40 @@ test -n "$rows" || { fail=0 +# Shape, as distinct from prose. Membership reads a row's first cell only, so a row stripped of +# its Purpose and Status -- `| `gamut-core` |` -- names a live crate and passes membership while +# documenting nothing. An escaped pipe is swapped for a sentinel first, so a cell holding a +# literal `\|` stays content rather than becoming an extra column. +malformed="$( + awk ' + /^## Crates$/ { in_section = 1; next } + /^## / { in_section = 0 } + !in_section { next } + /^\| *`[a-z0-9-]+` *\|/ { + row = $0 + gsub(/\\\|/, "\001", row) + n = split(row, cell, "|") + name = cell[2] + gsub(/[` ]/, "", name) + if (n != 5) { + print name " has " (n - 2) " cell(s); a crate row is Crate | Purpose | Status" + next + } + purpose = cell[3] + status = cell[4] + gsub(/^[ \001]+|[ \001]+$/, "", purpose) + gsub(/^[ \001]+|[ \001]+$/, "", status) + if (purpose == "") { print name " has an empty Purpose cell" } + if (status == "") { print name " has an empty Status cell" } + } + ' "$readme" +)" +if [ -n "$malformed" ]; then + fail=1 + echo "check-readme-crates: malformed crate rows in the $readme crates table:" + echo "$malformed" | sed 's/^/ /' +fi + # LC_ALL=C throughout: `comm` exits non-zero on input it considers unsorted, and jq's `sort_by` # below orders by codepoint. Collating both sides the same way keeps them comparable under any # ambient locale. From ee1af593d225ab6e8e24eee594cee0367ffe610f Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Thu, 10 Sep 2026 00:58:51 -0400 Subject: [PATCH 05/25] fix(readme): name the traits gamut-core actually declares The gamut-core row cited `Encoder`/`Decoder`. Those traits exist, but in gamut-codec-abi (src/lib.rs:380,396) -- a different crate with its own row five rows up the same table -- so the backticks sent a reader to the wrong crate. gamut-core declares `EncodeImage` (src/lib.rs:378) and `DecodeImage` (409). AGENTS.md carried the same wording and is corrected with it. gamut-color's Purpose omitted `ycbcr`, the crate's largest module at 1436 lines (the H.273 matrixing layer plus the libwebp-exact BT.601 layer VP8 requires), and `linalg`; `format::ChromaSubsampling`, which the cell did name, is a different thing. gamut-av1's decoder status said "intra key frames only". decode/header.rs:363 accepts `INTRA_ONLY_FRAME` as well, decode/header.rs:411 codes its refresh mask, and decode/header.rs:1301 tests it. The table now also says above itself that the versions its rows cite are each crate's own Cargo.toml, not what crates.io serves. --- AGENTS.md | 2 +- README.md | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c30149d5..ab510588 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Dependency edges (a crate depends on those to its right): `cmm`, `codec-abi`, `all`); `default = []`. `primitives` re-exports shared `color`/`dsp`/`bitstream`; `isobmff`/`metadata`/`tonemap`/`codec-abi` re-export their respective primitive crates; `all` includes all of these. -- **gamut-core** — `Encoder`/`Decoder` traits, image buffers, `Dimensions`, `Error`, plus the +- **gamut-core** — `EncodeImage`/`DecodeImage` traits, image buffers, `Dimensions`, `Error`, plus the format-agnostic `convert` module: the one place any `Pixel` layout converts to another (grey↔RGB, alpha add/drop/composite, 8↔16-bit), lossless by default with loss opted into per decoder via a `ConvertPolicy`. Format crates decode to what the file carries and delegate the diff --git a/README.md b/README.md index 0458d89c..ac2808b6 100644 --- a/README.md +++ b/README.md @@ -118,18 +118,24 @@ format. ## Crates +Every row describes the crate **in this tree**: any version it cites is the crate's own +`Cargo.toml` — what `mise run versions` prints and what a workspace `path` dependency builds — not +what crates.io currently serves. The two agree for thirty of the thirty-two crates; `gamut-riff`'s +manifest is ahead of its newest release and `gamut-cmm` has no release yet, both noted in +[Releases](#releases). + | Crate | Purpose | Status | | ----------------- | ---------------------------------------------------------------------- | -------------------------------------- | | `gamut` | Umbrella crate; re-exports the format crates behind Cargo features | implemented | -| `gamut-core` | Core traits (`Encoder`/`Decoder`), image buffers, dimensions, errors, `convert` | stable (v2; v1 under #177), pixel conversion added by #268 | -| `gamut-color` | Pixel formats, bit depths, chroma subsampling, CICP code points and planar buffers, plus the `f64` colour science (transfer, Lab/OKLab, XYB, matrix, gamut map, CCT, profile) | stable (v2; v1 under #179); the colour science is Tier-1 `f64`, not bit-reproducible | +| `gamut-core` | Core traits (`EncodeImage`/`DecodeImage`), image buffers, dimensions, errors, `convert` | stable (v2; v1 under #177), pixel conversion added by #268 | +| `gamut-color` | Pixel formats, bit depths, chroma subsampling, CICP code points and planar buffers, the `ycbcr` matrixing layer (H.273, plus the libwebp-exact BT.601 one VP8 needs), and the `f64` colour science (transfer, Lab/OKLab, XYB, `matrix`/`linalg`, gamut map, CCT, profile) | stable (v2; v1 under #179); the colour science is Tier-1 `f64`, not bit-reproducible | | `gamut-dsp` | Shared DSP kernels: AV1 DCT/ADST/identity/WHT, the JPEG 8×8 forward/inverse DCT, quantization rounding | stable (v2; v1 under #192) | | `gamut-bitstream` | Bit readers/writers, LEB128, the AV1 §8.2 symbol (arithmetic) coder, MSB-first sample packing | v0.2, no v1 release issue yet; the ANS and Huffman coders are not implemented | | `gamut-tonemap` | Tone-mapping curves (`ToneCurve` + Reinhard/ACES/Hable/Drago) for HDR→SDR | stable (v1, #188); eight operators, surface frozen | | `gamut-codec-abi` | Shared codestream-backend seam: `repr(C)` vtables + the backend registry | in use by avif, ffi, heic, jpeg, jxl, png and webp | | `gamut-isobmff` | ISOBMFF container utilities (AVIF, HEIC) | stable (v2); structure only, codestream carried opaquely | | `gamut-riff` | RIFF container utilities (WebP) | stable (v1, #186) | -| `gamut-av1` | AV1 still-image (intra-frame) encoder + decoder — the codec layer beneath AVIF | encoder: lossless and lossy intra keyframes; decoder (default-on `decode` feature): 8-bit 4:4:4 intra key frames only (#259) | +| `gamut-av1` | AV1 still-image (intra-frame) encoder + decoder — the codec layer beneath AVIF | encoder: lossless and lossy intra keyframes; decoder (default-on `decode` feature): 8-bit 4:4:4 intra frames, key and intra-only (#259) | | `gamut-av2` | AV2 still-image (intra-frame) encoder/decoder — AV1's successor | placeholder | | `gamut-avif` | AVIF encoder + container decoder — AV1 still frames in ISOBMFF | encoder (v1, 8/10/12-bit) + container decode (#250); AV1 codestream decode via the `Av1StillDecoder` seam | | `gamut-jxl` | JPEG XL encoder (libjxl wrap) + decoder (pure-Rust jxl-rs) | encoder + decoder (#243) | From a91f0d1d54a42dc8530daecdf7db491541831dc3 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Thu, 10 Sep 2026 00:59:02 -0400 Subject: [PATCH 06/25] fix(check-readme-crates): reject a row a blank or a fence hides The emptiness test trimmed `[ \001]` -- ASCII space and the escaped-pipe sentinel -- so one U+00A0 in place of an empty Purpose reconstructed the exact degenerate row the check was written to catch, as did a tab. Membership also counted a row inside an HTML comment or a fenced code block, both of which render as something other than a table cell, so a crate documented only there passed as documented. A cell is now empty unless it carries a character that is neither whitespace nor a control code, with the Unicode blanks a renderer shows as nothing (U+00A0, U+1680, U+2000-U+200D, U+2028/9, U+202F, U+205F, U+2060, U+3000, U+FEFF) folded to a space first. Comment and fence state is tracked over the whole file, and membership and shape now come from one pass so the two cannot disagree about which lines are rows. The failure message and the mise task description stated STATUS.md as a row's authority; the script header states lib.rs first, and following the message instead reproduces the gamut-dsp defect this branch fixed. All three now agree. The description also claimed the table is checked to be "well formed" -- a table with its `| --- |` delimiter deleted still passes, every row then rendering as literal text -- so it now says what is checked, and the header records the hole. --- mise.toml | 14 ++- tooling/check-readme-crates/run.sh | 165 +++++++++++++++++++++++------ 2 files changed, 140 insertions(+), 39 deletions(-) diff --git a/mise.toml b/mise.toml index 1f4bbf77..ded900d5 100644 --- a/mise.toml +++ b/mise.toml @@ -299,11 +299,17 @@ run = "./tooling/check-tests/run.sh" # Nothing read the README crates table, so it drifted: fixing issue #425 meant correcting # eighteen rows and adding four crates that had no row at all. `versions` only lists # `name version` and asserts nothing, and `check-msrv` greps the README for one number, so -# neither would have caught it. This gates the table's *membership* -- every -# workspace crate has a row, every row names a real crate -- and deliberately not its prose, whose -# authority is each crate's STATUS.md. +# neither would have caught it. This gates the table's *membership* -- every workspace crate has a +# row, every row names a real crate, no crate twice -- and each crate row's *shape*: three cells +# whose Purpose and Status each carry a character that is neither whitespace nor a control code, +# and not hidden inside an HTML comment or a code fence. It gates neither the prose nor the +# table's header/delimiter rows, so a green run means "every crate has a well-formed row", not +# "the table renders" and not "the table is true". A row's prose has authority in this order: the +# crate's own lib.rs, then its Cargo.toml, then its STATUS.md where those two are silent -- a +# STATUS.md can itself be stale (issue #545), so a row must be true of the crate rather than +# faithful to a file. [tasks.check-readme-crates] -description = "Verify the README crates table covers every workspace crate, has no phantom rows, and is well formed" +description = "Verify the README crates table covers every workspace crate, lists no phantom or duplicate rows, and gives each crate a three-cell row with a non-empty Purpose and Status" run = "./tooling/check-readme-crates/run.sh" [tasks.check-release-deps] diff --git a/tooling/check-readme-crates/run.sh b/tooling/check-readme-crates/run.sh index b7dc4ddb..7607c7ea 100755 --- a/tooling/check-readme-crates/run.sh +++ b/tooling/check-readme-crates/run.sh @@ -11,15 +11,26 @@ # * every row names a crate that still exists, so a renamed or deleted crate cannot be left # behind as a phantom row; # * no crate is listed twice, so the table stays a bijection rather than a set; -# * every crate row is a well-formed three-cell row with a non-empty Purpose and Status, so a -# row cannot be reduced to a bare crate name and still satisfy membership. +# * every crate row is a three-cell row whose Purpose and Status each carry at least one +# character that is neither whitespace nor a control code, so a row cannot be reduced to a +# bare crate name -- or to a cell holding one non-breaking space -- and still satisfy +# membership; +# * a row inside an HTML comment or a fenced code block is not a row. Both render as something +# other than a table cell, so a crate documented only there is documented nowhere, and the +# membership check must see it as missing rather than as present. # # What is deliberately NOT checked, and why: # * The *wording* of the Purpose and Status cells. They are prose a human maintains, and their -# authority is the crate's own lib.rs, Cargo.toml and STATUS.md. A text gate over them would -# fossilise a particular phrasing, and a generated table would move prose a human writes into -# a generator -- so staleness of a row's *text* stays a review concern, not a lint. What a -# machine can settle is membership and shape, and it settles both. +# authority is, in this order, the crate's own `lib.rs`, its `Cargo.toml`, and its `STATUS.md` +# where the first two are silent -- a `STATUS.md` can itself be stale (issue #545), so a row +# must be true of the crate rather than merely faithful to a file. A text gate over the cells +# would fossilise a particular phrasing, and a generated table would move prose a human writes +# into a generator -- so staleness of a row's *text* stays a review concern, not a lint. +# * The table's header and `| --- |` delimiter row. Deleting the delimiter makes every row +# render as literal text while leaving each row's bytes intact, so this guard still passes on +# a table that renders as a paragraph. That is a known, unclosed hole: what is checked is that +# each crate has a row and that the row has its three cells, not that the surrounding table +# renders. # * The version. `mise run versions` already reports it, and the README deliberately states # what each crate *is* rather than pinning a number that release-plz bumps. set -euo pipefail @@ -43,48 +54,130 @@ test -f "$readme" || { exit 1 } -# The rows of the "## Crates" table only. Bounded to that section so the README's other tables -# (the `mise run ...` command table) can never be mistaken for a crate row. -rows="$( - awk '/^## Crates$/ { in_section = 1; next } /^## / { in_section = 0 } in_section' "$readme" | - sed -nE 's/^\| *`([a-z0-9-]+)` *\|.*/\1/p' -)" +# One pass over the file emits both streams -- `NAME` for membership, `BAD` for shape -- so the +# two checks can never disagree about which lines are crate rows. Bounded to the "## Crates" +# section so the README's other tables (the `mise run ...` command table) can never be mistaken +# for a crate row. +# +# LC_ALL=C makes every regexp below byte-wise, which is what the octal escapes assume; the awk +# is POSIX (no gensub, no interval expressions, dynamic regexps built as strings) so it behaves +# the same under mawk, which is what `awk` is on the CI runner. +scan="$( + LC_ALL=C awk ' + BEGIN { + # An escaped pipe is content, not a column separator, so it is swapped for a control + # byte before the split and swapped back before the cell is judged. + SENTINEL = "\001" + # ASCII space and every C0/DEL control byte. + ASCII_BLANK = "[ \001-\037\177]" + # The UTF-8 encodings of the Unicode blanks a Markdown renderer shows as nothing: + # U+00A0, U+1680, U+2000-U+200D, U+2028, U+2029, U+202F, U+205F, U+2060, U+3000 and + # U+FEFF. Without this fold a single non-breaking space reconstructs exactly the + # degenerate row the shape check exists to reject. + UNI_BLANK = "\302\240|\341\232\200|\342\200[\200-\215\250\251\257]" + UNI_BLANK = UNI_BLANK "|\342\201[\237\240]|\343\200\200|\357\273\277" + } -test -n "$rows" || { - echo "check-readme-crates: found no crate rows under '## Crates' in $readme" - exit 1 -} + # Returns the part of `line` that is outside an HTML comment, carrying `in_comment` + # across lines so a multi-line comment hides every line it spans. + function strip_comments(line, out, p) { + out = "" + while (length(line) > 0) { + if (in_comment) { + p = index(line, "-->") + if (p == 0) { return out } + in_comment = 0 + line = substr(line, p + 3) + } else { + p = index(line, ""; next} {print}' +check "a crate row hidden in a fenced code block" 1 "no row in" awkx '/^\| `gamut-png` /{print "```"; print; print "```"; next} {print}' +check "a phantom row hiding behind a capital" 1 "not workspace members" sedx 's/^| `gamut-png` /| `Gamut-png` /' +check "a phantom row hiding behind an underscore" 1 "not workspace members" sedx 's/^| `gamut-png` /| `gamut_png` /' +check "a two-cell crate row" 1 "malformed crate rows" sedx 's/^| `gamut-png` .*/| `gamut-png` | only one cell |/' +check "a four-cell crate row" 1 "malformed crate rows" sedx '/^| `gamut-png` /s/$/ extra |/' +check "an empty Status cell" 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | |/' +check "a Status cell of one  " 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \  |/' +check "a Status cell of one ‎" 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \‎ |/' +check "a Status cell of one ‎" 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \‎ |/' +check "a Status cell of one ‎ (leading zero)" 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \‎ |/' +check "a Status cell of one ‎" 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \‎ |/' +check "a Status cell of one  " 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \  |/' +check "a Status cell of one  " 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | \  |/' +check "a Status cell of one non-breaking space" 1 "empty Status cell" awkx '{ if ($0 ~ /^\| `gamut-png` /) { print "| `gamut-png` | PNG codec | \302\240 |"; next } print }' +check "a Status cell of one tab" 1 "empty Status cell" awkx '{ if ($0 ~ /^\| `gamut-png` /) { print "| `gamut-png` | PNG codec |\t|"; next } print }' +check "a Status cell of one empty HTML element" 1 "empty Status cell" sedx 's/^| `gamut-png` .*/| `gamut-png` | PNG codec | <\/span> |/' +check "a crate row with no trailing pipe" 1 "no row in" sedx '/^| `gamut-png` /s/|$//' +check "a crate row indented four spaces (a code block)" 1 "no row in" sedx '/^| `gamut-png` /s/^/ /' +check "a crate row indented two spaces (legal)" 0 "lists every workspace crate" sedx '/^| `gamut-png` /s/^/ /' + +# --------------------------------------------------------------------------------------------- +# Structure: every crate row renders inside ONE table, whatever the bytes of the row look like. +# --------------------------------------------------------------------------------------------- +check "the delimiter row deleted" 1 "do not render inside a table" sedx '/^| -----/d' +check "a blank line directly after the delimiter" 1 "do not render inside a table" sedx '/^| -----/G' +check "a blank line between two crate rows" 1 "do not render inside a table" sedx '/^| `gamut-png` /G' +check "the header row blanked" 1 "do not render inside a table" sedx 's/^| Crate .*/| | | |/' +check "a two-column header over a three-column delimiter" 1 "do not render inside a table" sedx 's/^| Crate .*/| Crate | Purpose |/' +check "the delimiter row indented four spaces" 1 "do not render inside a table" sedx '/^| -----/s/^/ /' +check "an earlier table donates its delimiter" 1 "do not render inside a table" awkx '/^\| Crate /{print "| Decoy | Decoy | Decoy |"; print "| --- | --- | --- |"; print ""} /^\| -----/{next} {print}' +check "a decoy table whose own first row is a crate row" 1 "do not render inside a table" awkx '/^\| Crate /{print "| `gamut-png` | PNG codec | shipped |"; print "| --- | --- | --- |"; print ""} /^\| -----/{next} /^\| `gamut-png` /{next} {print}' +check "the crate rows split over two valid tables" 1 "spread over" awkx '/^\| `gamut-png` /{print ""; print "| Crate | Purpose | Status |"; print "| --- | --- | --- |"} {print}' +check "alignment colons in the delimiter (legal)" 0 "lists every workspace crate" awkx '/^\| -----/{print "|:---|:---:|---:|"; next} {print}' +check "an escaped pipe inside a cell (legal)" 0 "lists every workspace crate" awkx '{ if ($0 ~ /^\| `gamut-riff` /) sub(/RIFF container utilities \(WebP\)/, "RIFF a \\| b"); print }' +check "a non-crate separator row inside the table (legal)" 0 "lists every workspace crate" awkx '/^\| `gamut-png` /{print "| **Codecs** | | |"} {print}' +check "an unrelated table earlier in the section (legal)" 0 "lists every workspace crate" awkx '/^## Crates$/{print; print ""; print "| A | B |"; print "| --- | --- |"; print "| 1 | 2 |"; print ""; next} {print}' + +# --------------------------------------------------------------------------------------------- +# The section heading, in every form CommonMark gives it. +# --------------------------------------------------------------------------------------------- +check "the heading with a closing sequence (legal)" 0 "lists every workspace crate" sedx 's/^## Crates$/## Crates ##/' +check "the heading indented three spaces (legal)" 0 "lists every workspace crate" sedx 's/^## Crates$/ ## Crates/' +check "the heading in setext form (legal)" 0 "lists every workspace crate" awkx '{ if ($0 == "## Crates") { print "Crates"; print "------"; next } print }' +check "a trailing space on the heading (legal)" 0 "lists every workspace crate" sedx 's/^## Crates$/## Crates /' +check "a CRLF file (legal)" 0 "lists every workspace crate" sedx 's/$/\r/' +check "a heading whose text is Crates#, not Crates" 1 "found no crate rows" sedx 's/^## Crates$/## Crates#/' + +# --------------------------------------------------------------------------------------------- +# The claim forms. Each is opt-in and marker-driven; each fixture writes one claim cargo refutes, +# and each is paired with the legal claim it must not reject. +# --------------------------------------------------------------------------------------------- +check "a version token contradicting the manifest" 1 "version claims" sedx '/^| `gamut-core` /s/stable (v2;/stable (v3;/' +check "a minor version contradicting the manifest" 1 "version claims" sedx '/^| `gamut-bitstream` /s/v0.2,/v0.3,/' +check "a consumed-by list one crate short" 1 "consumed by" sedx 's/`gamut-png` and `gamut-webp`;/`gamut-png`;/' +check "a consumed-by list naming a non-consumer" 1 "consumed by" sedx 's/`gamut-png` and `gamut-webp`;/`gamut-png`, `gamut-riff` and `gamut-webp`;/' +check "an always-on list naming an optional edge" 1 "always-on" sedx 's/always-on dependency: `gamut-core`/always-on dependency: `gamut-avif`/' +check "an always-on list naming an edge that is absent" 1 "always-on" sedx 's/always-on dependency: `gamut-core`/always-on dependency: `gamut-jxl-sys`/' +check "a feature no crate the row names declares" 1 "feature claims" sedx 's/behind Cargo feature `codec-abi`/behind Cargo feature `codec-abii`/' +check "an off-by-default feature called a default one" 1 "feature claims" sedx 's/BigTIFF behind Cargo feature `bigtiff`/BigTIFF behind default Cargo feature `bigtiff`/' +check "a default feature owned by a crate not named here" 1 "feature claims" sedx '/^| `gamut-riff` /s/stable (v1, #186)/stable (v1, #186); default Cargo feature `decode`/' +check "another real feature of the row crate (legal)" 0 "lists every" sedx 's/BigTIFF behind Cargo feature `bigtiff`/BigTIFF behind Cargo feature `bigtiff`, with Cargo feature `test-support` for fixtures/' +check "a feature of another crate the row names (legal)" 0 "lists every" sedx 's/behind Cargo feature `codec-abi`/behind Cargo feature `codec-abi` and Cargo feature `avif`/' +check "a cell naming a crate that does not exist" 1 "do not exist" sedx 's/built on `gamut-ifd`/built on `gamut-ifdd`/' +check "a cell naming a real crate (legal)" 0 "lists every" sedx '/^| `gamut-riff` /s/(WebP)/(WebP), read by `gamut-webp`/' +check "an external dependency cargo does not record" 1 "external-dependency" sedx 's/external dependency `miniz_oxide`/external dependency `miniz_oxidee`/' +check "an external dependency no crate in the row has" 1 "external-dependency" sedx 's/external dependency `miniz_oxide`/external dependency `serde`/' + +# The two over-rejections a reviewer found after they shipped. Both are legal prose and both must +# stay legal: bare `features` is an English verb, which is why the marker carries `Cargo`. +check "the word features as an English verb (legal)" 0 "lists every" sedx '/^| `gamut-riff` /s/(WebP)/(WebP); the crate features `chunk` walking/' +check "the underscore spelling of a real crate" 1 "do not exist" sedx 's/built on `gamut-ifd`/built on `gamut_ifd`/' + +# --------------------------------------------------------------------------------------------- +# The precondition the name checks rest on, and the section prose they now reach. +# --------------------------------------------------------------------------------------------- +check "a crate name in a cell without a code span" 1 "without a code span" sedx 's/built on `gamut-ifd`/built on gamut-ifd/' +check "a crate name in section prose without a code span" 1 "without a code span" sedx 's/^Each crate manifest sets/Each gamut-png crate manifest sets/' +check "the bare word gamut in prose (legal)" 0 "lists every" sedx 's/^Each crate manifest sets/Each gamut crate manifest sets/' +check "a crate name inside a fence in the section (legal)" 0 "lists every" awkx '/^## Prerequisites/{print "```rust"; print "use gamut_png::Encoder;"; print "```"; print ""} {print}' +check "a phantom crate named in section prose" 1 "do not exist" sedx 's/^Each crate manifest sets/Each `gamut-ifdd` crate manifest sets/' +check "a feature claim in section prose cargo refutes" 1 "feature claims" sedx 's/^Each crate manifest sets/The `gamut-png` Cargo feature `nope` is gone; each crate manifest sets/' +check "a consumed-by list outside a crate row" 1 "no crate to be about" sedx 's/^Each crate manifest sets/Each is consumed by `gamut-png`; each crate manifest sets/' +check "an always-on list outside a crate row" 1 "no crate to be about" sedx 's/^Each crate manifest sets/Each has an always-on dependency: `gamut-core`; each crate manifest sets/' +check "a version token outside a crate row" 1 "no crate to be about" sedx 's/^Each crate manifest sets/Each v9 crate manifest sets/' + +echo +echo "check-readme-crates fixtures: $passed passed, $failed failed (awk: ${CHECK_README_AWK:-awk})" +test "$failed" -eq 0 From b33c7bc610ca8669cdb96b05777efb63a13af484 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Thu, 10 Sep 2026 11:16:07 -0400 Subject: [PATCH 24/25] fix(check-readme-crates): read a sub-heading inside the section too The claim scan reached every line of the section except one: a heading. A `###` sub-heading does not end the section -- deliberately, so a sub-table under it is still part of the table's contract -- but its text was passed to the name check and not to the claim forms, so a heading inside the section could carry a refutable claim and be read only half way. Nothing in the README does that today; the guard's own header said the whole section is read, and it was not quite true. Read headings inside the section with both. The one exception is now stated where the contract is: a fenced or indented code block is outside every check here, names and claims alike, because its content is a code sample rather than a claim this workspace answers for. --- tooling/check-readme-crates/fixtures.sh | 2 ++ tooling/check-readme-crates/run.sh | 26 ++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tooling/check-readme-crates/fixtures.sh b/tooling/check-readme-crates/fixtures.sh index 010e8a6e..93a7d58f 100755 --- a/tooling/check-readme-crates/fixtures.sh +++ b/tooling/check-readme-crates/fixtures.sh @@ -177,6 +177,8 @@ check "a feature claim in section prose cargo refutes" 1 "feature claims" check "a consumed-by list outside a crate row" 1 "no crate to be about" sedx 's/^Each crate manifest sets/Each is consumed by `gamut-png`; each crate manifest sets/' check "an always-on list outside a crate row" 1 "no crate to be about" sedx 's/^Each crate manifest sets/Each has an always-on dependency: `gamut-core`; each crate manifest sets/' check "a version token outside a crate row" 1 "no crate to be about" sedx 's/^Each crate manifest sets/Each v9 crate manifest sets/' +check "a phantom crate named in a sub-heading" 1 "do not exist" awkx '/^Each crate manifest sets/{print "### The `gamut-ifdd` note"; print ""} {print}' +check "a claim in a sub-heading cargo refutes" 1 "feature claims" awkx '/^Each crate manifest sets/{print "### The `gamut-png` Cargo feature `nope`"; print ""} {print}' echo echo "check-readme-crates fixtures: $passed passed, $failed failed (awk: ${CHECK_README_AWK:-awk})" diff --git a/tooling/check-readme-crates/run.sh b/tooling/check-readme-crates/run.sh index e737de1c..c530e1b1 100755 --- a/tooling/check-readme-crates/run.sh +++ b/tooling/check-readme-crates/run.sh @@ -49,11 +49,13 @@ # # WHAT THE CONTRACT COVERS. The whole `## Crates` section, not only its rows. The structural # checks always judged the section; the claim checks do too, so a machine-decidable claim written -# as ordinary prose above or below the table is read exactly as it would be inside a cell. Two -# forms -- the version token and the `consumed by`/`always-on` lists -- name no subject of their -# own and take the row's crate as their subject, so outside a crate row they have nobody to be -# about: writing one there is a failure rather than a silent pass, and the message says to move it -# into a row. +# as ordinary prose above or below the table -- or in a `###` sub-heading inside it -- is read +# exactly as it would be inside a cell. Two forms -- the version token and the `consumed +# by`/`always-on` lists -- name no subject of their own and take the row's crate as their subject, +# so outside a crate row they have nobody to be about: writing one there is a failure rather than +# a silent pass, and the message says to move it into a row. The one exception is a fenced or +# indented code block, which is outside every check here: its content is a code sample, not a +# claim this workspace has to answer for. # # WHAT THE TABLE IS FORBIDDEN TO WRITE, and why forbidding beats widening. A check that reads # backticked names can only be as good as the assumption that names are backticked, and that @@ -485,9 +487,10 @@ scan="$( in_table = 0 if (lvl <= 2) { in_section = (htitle == "Crates") ? 1 : 0 } # A `###` sub-heading does not end the section, so its text is section text and - # is scanned like any other. The `## Crates` heading itself is scanned too, which - # costs nothing and keeps "every line of the section" true without an exception. - if (in_section) { unbackticked("line " NR, cur) } + # is read like any other line -- names and claims alike. The `## Crates` heading + # itself is read too, which costs nothing and keeps "every line of the section" + # true without an exception. + if (in_section) { unbackticked("line " NR, cur); claims("line " NR, "", cur) } prev = "" prev_para = 0 next @@ -499,9 +502,10 @@ scan="$( next } - # The contract is the whole section, not only its rows. An indented code block is - # exempt for the same reason a fence is: there a `gamut_png` is a sample of Rust or of - # a shell line, where the underscore spelling is the correct one. + # The contract is the whole section, not only its rows. A fenced or indented code + # block is exempt from all of it, names and claims alike: there a `gamut_png` is a + # sample of Rust or of a shell line, where the underscore spelling is the correct one, + # and a marker is a word in a code sample rather than a claim about this workspace. if (cur !~ /^ /) { unbackticked("line " NR, cur) } # A table runs from its delimiter row until the first line that is not a table row. From 9c35d7bc30eadd8d4b91268baca2ed5e2a8c7ac2 Mon Sep 17 00:00:00 2001 From: Justin Chung Date: Thu, 10 Sep 2026 11:18:11 -0400 Subject: [PATCH 25/25] docs(check-readme-crates): correct two claims the script header makes about itself Both found by re-deriving the header against the history and the code it documents, which is the only way this branch has ever caught one. "this header shipped a stale one for two rounds" -- it shipped stale for one. `git log -S` puts "Three claim forms" and the three checks in the same commit, and the commit that added the fourth and fifth checks is the commit that wrote "four", so the undercount begins and ends inside a single round. A hand-counted claim, wrong, in the sentence explaining why hand-counted claims are wrong. "A fenced code block is exempt too" -- an indented code block is exempt as well, and has been since the check landed: the scan skips any line indented four spaces. The contract paragraph above already said so; this bullet did not. --- tooling/check-readme-crates/run.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tooling/check-readme-crates/run.sh b/tooling/check-readme-crates/run.sh index c530e1b1..1ab9729a 100755 --- a/tooling/check-readme-crates/run.sh +++ b/tooling/check-readme-crates/run.sh @@ -37,8 +37,8 @@ # one level down. # # The forms are deliberately NOT enumerated here. An enumeration in the documentation of an -# enumeration checker is the one place a hand-written list must not be, and this header shipped a -# stale one for two rounds: it said "four" while the code checked five. `claims()` below is the +# enumeration checker is the one place a hand-written list must not be, and this header was stale +# the moment the fifth check landed: the same commit left it saying "four". `claims()` below is the # list. Every form is one `match()` in it, tagged and documented at that match site, and every # failure message names the form it rejects and the shape it wants: # @@ -67,8 +67,8 @@ # real crate, because that is the precondition every name check rests on. The bare word # `gamut` is exempt: it is the project's name in English as well as the umbrella's package # name, and requiring a code span around every mention of it would reject the prose this -# README is made of. A fenced code block is exempt too -- there a compound is a sample of -# Rust or of a shell line, where `gamut_png` is the correct spelling; +# README is made of. A fenced or indented code block is exempt too -- there a compound is a +# sample of Rust or of a shell line, where `gamut_png` is the correct spelling; # * the underscore spelling of a workspace crate is REJECTED, deliberately, and this is the one # place that decision is written down. `gamut_ifd` is a Rust identifier, not a cargo package # name; cargo publishes `gamut-ifd` and `cargo add gamut_ifd` does not resolve. Accepting it