docs(readme): correct the crates table against each crate STATUS.md, and gate it - #526
Open
justin13888 wants to merge 25 commits into
Open
justin13888 wants to merge 25 commits into
justin13888 wants to merge 25 commits into
Conversation
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
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
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
This was referenced Sep 10, 2026
Open
…S.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
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.
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.
A drift guard nothing runs cannot guard against drift. The job already runs three sibling static checks and already installs the guard's only two dependencies, jq and a stable toolchain; `cargo metadata --no-deps` is the same call the release-dependency step above it makes.
This was referenced Sep 10, 2026
The `gamut-codec-abi` row said the crate ships "the backend registry". It does not: its own documentation calls the registry host-owned, and the two real registries are private types inside the consuming crates. The row now names the registry fallback *contract*, which is what the crate carries, and its consumer list gains the umbrella, which depends on it optionally under `codec-abi`. That is the same defect already fixed two rows above, so every remaining row was swept for it: each backticked identifier in each cell was resolved to a crate. No other row named a construct owned elsewhere. Five further rows overstated their crate: * the umbrella re-exports 25 sibling crates, not "the format crates" -- 11 format/codec crates and 14 primitive, container, metadata and ABI ones, with `gamut::core` alone unconditional; * `gamut-webp` was silent about the public `backend` seam the codec-abi row credits it with; * `gamut-metadata`'s C2PA store is extracted verbatim but never embedded -- `MetadataEmbedder` drops it by default and otherwise refuses, so "carried opaquely" was true in one direction only; * `gamut-jpeg`'s XYB colour mode is an encoder option; the decoder presents Rgb8/Gray8/Cmyk8; * `gamut-tonemap` listed four operators beside a Status cell claiming eight. There are eight, and AGENTS.md carried the same short list.
The crates table's preamble points at this section for the two crates whose manifest version is not what crates.io serves, so the section has to be true. It was not: it named six crates as awaiting their first manual publish on the evidence that they carry no `<crate>-v<version>` tag, and five of them are on crates.io at exactly the version their `Cargo.toml` declares. Verified against the sparse index on 2026-09-10: `gamut-deflate` 0.1.0, `gamut-dng` 1.0.0, `gamut-jpeg` 0.1.0, `gamut-jxl-sys` 0.1.0 and `gamut-png` 0.1.0 are all published; only `gamut-cmm` is absent. A missing tag is a symptom of the stall documented immediately above, which breaks the tag-and-release half of the pipeline rather than the publish half, so the section now says so instead of inferring publication state from git. `gamut-riff` -- manifest 1.0.0 against a newest published 0.1.3 -- is the other half of the same skew and the preamble's second exception, so it is recorded here too rather than only in the issue that tracks it.
…arset Three holes this guard documented rather than closed, and one it opened. The `| --- | --- | --- |` delimiter row was written down as a known hole: delete it and every row renders as a paragraph of literal pipes while each row's bytes stay intact, so the guard passed on a table that documents nothing. Documenting a hole invites shipping it forever, so it is now one assertion -- the crate rows must stand under a three-cell delimiter row, alignment colons accepted. A row was recognised by `[a-z0-9-]+`, so a phantom row naming `gamut_renamed` or `Gamut-Core` was invisible to both the membership and the shape checks. Cargo permits underscores and either case in a package name, so the pattern now matches what cargo permits and a phantom row cannot hide behind one. The emptiness fold trimmed whitespace and control codes, but the class it is really about is "renders as nothing": a cell holding only `<span></span>` or a lone `<br/>` reconstructed exactly the degenerate row the check exists to reject. HTML elements are now stripped before the fold. A carriage return on the `## Crates` heading left the section unmatched and the guard blamed comments and code fences for a rowless table. A trailing CR is now removed from every line, so a CRLF README checks out, and the rowless diagnostic names the heading as the other possible cause. The authority a row answers to is stated in three places -- this script's header, its on-failure message and the mise task's comment -- and all three said `lib.rs` first. That rule is falsified by this table's own `gamut-avif` row, which states 8/10/12-bit encode while the crate's module doc still defers 10/12-bit, and the row is the one that is right. The authority is the crate's source; lib.rs, Cargo.toml and STATUS.md are where to look, and each of them can be stale. The "eighteen rows" figure in the header and the mise comment is replaced by the measured one: 23 of the 28 rows on master are corrected and 4 crates that had no row gain one.
"primitive, container, metadata and ABI crates" has no bucket for `gamut-cmm` or `gamut-tonemap`, both of which the umbrella re-exports. The counts are unchanged and measured -- 25 re-exports, 11 of them format or codec crates -- but the remaining 14 are now named as the shared layers they are rather than forced into four labels that do not cover them.
The gamut-color cell led with "Pixel formats": PixelFormat is declared in gamut-core (src/pixel.rs:120, re-exported at src/lib.rs:70) and appears nowhere in gamut-color, whose own src/format.rs:3-4 hands the interleaved- buffer vocabulary to gamut-core by name and keeps only the coded-plane half. The cell now leads with what format.rs calls itself. The gamut-codec-abi cell counted the registries it disclaims: ten push_backend registries live across six crates (avif 1, heic 1, jpeg 2, jxl 2, png 2, webp 2), and two of the consumers the Status cell names own none. The claim needs no number, so it no longer carries one. The gamut-deflate cell read as if the crate inflated through miniz_oxide. Its manifest has no [dependencies] table at all; miniz_oxide there is a dev-only bench baseline. The inflate dependency is gamut-png's (Cargo.toml:28) and gamut-dng's (Cargo.toml:50).
…where "A three-column delimiter appears earlier in the section" is satisfied by five constructions that render no table: a blank line after the delimiter, a blanked header, a two-cell header over a three-cell delimiter, an unrelated table earlier in the section donating its delimiter once the crate table loses its own, and a delimiter indented four spaces into a code block. All five passed. The assertion is now adjacency plus agreement: the first crate row must be immediately preceded by a delimiter row, itself immediately preceded by a header row of the same column count, neither indented four spaces or more. Each failure reports which of the three it was. Emptiness also folds the HTML character references a renderer resolves to a blank, and the script says plainly that the list cannot be exhaustive. A trailing space on the `## Crates` heading no longer unmatches the section. The row pattern is unchanged and deliberately narrow; the missing-crate message now names the form it recognises, since that is where an unrecognised legal row surfaces. One bypass stays open and is named: a blank line BETWEEN two crate rows.
`cargo metadata --no-deps` gives gamut-deflate no normal or build dependency at all: its manifest has no [dependencies] table, and src/lib.rs:6 says "with no internal dependencies of its own". The edge list claimed "<- core", which this PR's own gamut-deflate row now contradicts. Only the one edge this diff would otherwise contradict is corrected here. Eight more edges in that list disagree with cargo metadata -- gamut-codec-abi is missing from three consumers, gamut-dng <- gamut-jxl is undocumented, and gamut-dsp is the other crate wrongly given a core edge -- and are filed as #595 with the measured table.
…tles The table context was asserted on exactly two lines: the ones directly above the FIRST crate row. A decoy table earlier in the section whose own first row is a crate row satisfies that assertion, so deleting the real delimiter left thirty-one rows rendering as a paragraph of literal pipes while the guard printed "lists every workspace crate" and exited 0. The blank-line split that was written down as a known bypass is the same defect: only one row's neighbourhood was ever judged. Judge the whole section instead. A table is a header row immediately followed by a delimiter of the same width and ends at the first line that is not a table row; every crate row in the section must render inside one such table. The decoy, the deleted delimiter, the blanked header, the mismatched width, the four-space indent and the blank line between rows now all fail, and the disclosure they were covered by is gone rather than reworded. Accept the CommonMark forms that were being rejected as "no crate rows": a closing `##` sequence, up to three spaces of heading or table indentation, and the setext underline. Fold the numeric spellings of every invisible character the named list already covers -- `‎` folded while `‎` did not, and U+1680 had a hex entity but no decimal one. A row with no trailing pipe now reports a missing crate, which is what the header always said it did. Then check what `cargo metadata` can decide, because a hand-maintained list of crates is the defect this guard exists to catch one level down: a `vN` version token against the crate's own manifest, a `consumed by` list against its workspace consumers, and an `always-on dependency` list against its non-optional edges. Each is opt-in, and everything else in a cell stays prose.
…hing checks The `gamut-deflate` row corrected last round shipped a fresh false enumeration: `miniz_oxide` is a normal dependency of `gamut-png`, `gamut-dng` AND `gamut-tiff`, and those same three are the crate's own consumers, not the two each half of the cell named. That is the third hand-written list on this branch to be wrong, so no cell keeps one that a machine can settle. Every machine-derivable list in the table is now written in a form the guard checks against `cargo metadata` -- `gamut-codec-abi`'s consumers, both halves of `gamut-deflate`, and the umbrella's one always-on edge -- or deleted. The umbrella's three counts (25 siblings, 11 format crates, 14 shared layers) go, and its gloss names the two groups it was leaving out, `core` and tone mapping. `gamut-tonemap`'s "eight operators" goes and the eight names stay. The preamble's crates.io figures go with them: whether the registry serves what a manifest declares needs the network, so Releases states the two gaps and the table stops counting them. `gamut-cmm` was crediting epic #323 with a phase #323 lists out of scope: P8 is pipeline optimization, which is #372. All eight phases are complete, so the attribution is corrected rather than the range shortened. `gamut-cli` described four of its eight subcommands as though that were the list; it names all eight.
The commit that gave `gamut-deflate` its true edge -- it depends on nothing, not even `gamut-core` -- left the `gamut-core` entry two bullets above still claiming "everything else depends on it", falsified by three entries in the same list (`gamut-codec-abi`, `gamut-deflate`, `gamut-jxl-sys`). Replace the universal with a pointer to the per-entry edges rather than a new enumeration: the rest of the list still disagrees with `cargo metadata` and that is #595's to fix, so nothing here should hand-count it again.
…rites Enumerating every claim in the table that `cargo metadata` can decide left two classes unchecked. A backticked feature name -- the umbrella's `codec-abi`, gamut-av1's `decode`, gamut-ifd's `bigtiff` -- is a claim about a manifest. A `gamut`-prefixed name in a Purpose cell is the same phantom-crate claim the crate cell has been guarded against from the start, written in the one cell nothing read. `feature`/`features` followed by backticked names now resolves against the crates the row talks about: its own, plus any other it cites, because a row may legitimately point at the umbrella's feature for the seam it describes. `default feature` additionally requires that crate's `default` list to enable it, so "on unless you ask otherwise" cannot quietly become opt-in. Every occurrence in a row is read, not only the first. Every `gamut`-prefixed name a cell backticks must be a workspace member, which closes the prose half of the phantom-row check: before this, a Purpose cell could name a crate that no longer exists.
The guard reads `feature`/`features` immediately before the backticked name. Two of the three cells that name a Cargo feature put the word after it and the third left it out, so all three claimed something a machine could settle in a form nothing settles. Same three facts, unchanged: the umbrella's `codec-abi`, gamut-av1's default `decode`, gamut-ifd's `bigtiff`.
Every name check in this guard reads code spans, so a crate name written as bare text is invisible to all of them: an unbackticked `gamut-ifdd` in a Purpose cell passed the cite check that exists to catch exactly that. The precondition was assumed, never enforced. Enforce it instead of widening the pattern, which is how a phantom row gets in. Inside `## Crates`, a `gamut-`/`gamut_` compound outside a code span now fails whether or not it names a real crate. The bare word `gamut` is exempt -- it is English here as well as a package name -- and so is a fenced or indented code block, where `gamut_png` is the correct spelling. Three names in the table were already bare and are backticked. The underscore spelling of a real crate stays rejected, and the script header now says so once, with the reason: a cell names a cargo package, and the crate-cell pattern reads `_` on purpose so a phantom row cannot hide behind one, so folding the spelling in one half and not the other would split the guard against itself.
…go settles Three changes to what the guard reads, and one to how it documents them. The contract is the whole `## Crates` section. The structural checks already judged it; the claim checks read crate rows only, so a machine-decidable claim written as ordinary prose above or below the table was invisible -- and one such sentence was false, claiming all cargo metadata but `version` is centralized while all 32 manifests set a description and the root manifest says why. The claim forms now read every line of the section. The two that take the row's crate as their subject -- the version token and the `consumed by`/`always-on` lists -- have nobody to be about outside a row, so writing one there fails instead of passing silently. The feature marker carries `Cargo`. Bare `feature`/`features` is an ordinary English verb, and "the crate features `chunk` walking" was read as a feature claim and rejected. A guard that rejects legal prose is a guard someone turns off, so the marker is now a form the table only ever writes deliberately. An external crate name is settled under a marker. The impossibility argument -- that no rule separates an external crate name from a module or a type name in a code span -- was too strong: four of five checks were already marker-driven and opt-in, so a marker settles this token too. `external dependency` followed by backticked names checks each against the non-dev dependencies of a crate the text names. `miniz_oxide`, the one cargo-decidable token in the table that nothing checked, is written in that form. The script header no longer enumerates the forms. It said "four" while the code checked five, which is a hand-written list inside the documentation of a list checker -- the defect this guard exists to catch, two levels up. Each form is tagged and documented at its own match site, and the header says how to print them.
The `gamut` row said each sibling crate it re-exports sits behind its own Cargo feature. That is false, and it replaced a sentence that was true. Of the 25 siblings the umbrella re-exports, 15 sit behind a feature of their own, 9 behind one of two shared features, and `gamut-core` behind none: `pub use gamut_core as core` carries no `cfg`, and `gamut` declares no `core` feature at all. The wording it displaced -- that only the core re-export is unconditional -- said exactly that. Say what the graph is, and check what a machine can check. The two shared features are now named in the form the guard reads, so `primitives` and `metadata` are settled against the umbrella's own manifest; the always-on edge to `gamut-core` was already checked and stays.
The guard grew five checks over four rounds of review, and every round proved itself with a battery of single-edit fixtures that lived only in the pull request description. A claim in a description is not a regression test: no one can re-run it, and the next round has to start from an assertion rather than from an execution. Nothing regression-tests a guard that has changed in four consecutive rounds. The battery is 69 fixtures, each one edit to the repository's own README, each asserting the exit code the guard owes it and -- where the reason matters -- a fragment of the message, so a fixture that starts failing for a different reason is caught rather than counted as a pass. Legal renderings are fixtures too, with expected code 0: both over-rejections a reviewer found after they shipped are pinned there. `CHECK_README_AWK` selects the interpreter, so the POSIX-awk claim is re-runnable instead of asserted. The task is not wired into CI: it is for whoever changes the guard, and it costs a `cargo metadata` per fixture.
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.
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
README.md's crates table is the first thing anyone evaluating gamut reads, and it had driftedacross twenty-three of its twenty-eight rows. Five crates shipping at v1.0+ (
gamut-ifd,gamut-exif,gamut-xmp,gamut-iptc,gamut-metadata) were still described asscaffolding (impl in progress, #34)—and #34 is closed, so the citation was wrong twice over. Four crates had no row at all:
gamut-codec-abi,gamut-dng,gamut-jpegandgamut-tonemap. Two more rows contradictedAGENTS.mdoutright.1. Every status cell now derives from that crate's
STATUS.mdand itsCargo.tomlversion,stated as what the crate is today rather than as a milestone. A closed issue is cited only as a
completion marker — the table's pre-existing convention, e.g.
stable (v1, #186)— never asongoing work. The only two open issues cited are the two things genuinely outstanding: #242
(gamut-ffi's consumer entry points) and #416 (the measured parity gap). Beyond the five rows the
issue names, the same defect reached
gamut-core(WIP, but v1-stabilized under #177 and now at2.0.1),
gamut-color/gamut-dsp(stabilizing api, both past v1 under #179/#192),gamut-isobmff(finalizing api, at 2.0.1),gamut-avif(stabilizing with gamut-av1, butv1.1.0 with the #250 container decoder),
gamut-ffi(placeholder, but the #280 providerboundary ships), and the
(v1, ...)markers ongamut-jxl/gamut-jxl-sys, which are at 0.4.0and 0.1.0 and were never v1.
2. Two Purpose cells that contradicted
AGENTS.mdare corrected.gamut-tiffsaid"self-contained (own IFD/tag container)" — it is built on the shared
gamut-ifdprimitive, asthe
gamut-ifdrow three lines above it already said.gamut-heicsaid"encoder/decoder | placeholder" — it is a decode-only container with slices S1–S7 delivered, and
gamut has no HEIF encoder by charter.
3. The §Scope prose carried the same defect — it called TIFF and the metadata primitives
"newly scaffolded" in the very paragraph that stakes the "exiftool-class tag coverage" goal. It
now says where that goal stands, and names the largest gap honestly: the
gamut-metadatafacadeis consumed by
gamut-dngand the umbrella only, so every other format crate still carriesEXIF/XMP/ICC as raw byte blocks rather than calling the typed path. #416 tracks that, measured.
4. A drift guard, so this cannot recur silently. Nothing read this table —
mise run versionsonly listsname versionand asserts nothing, andcheck-msrvgreps the README forone number — which is why twenty-three rows went wrong unnoticed.
mise run check-readme-crates(
tooling/check-readme-crates/run.sh) checks three things about the whole## Cratessectionagainst
cargo metadata --no-deps: membership (a workspace crate with no row, a row naming acrate that is not a member, a crate listed twice), rendering (each crate row is a three-cell
row whose Purpose and Status render as something, and every crate row in the section renders
inside one real table), and the claim forms — marker-driven, opt-in shapes such as a version
token or a
consumed bylist, whichcargo metadatacan settle.mise run check-readme-crates-fixturesdrives the guard against 69 committed single-edit fixtures.It gates what a machine can settle, and nothing else about the prose. A cell's wording is
written by a human against the crate's own source; a text gate over it would fossilise a
particular phrasing, and generating the table would move prose a human maintains into a
generator. Where a cell states something
cargo metadatadecides, the rule this branch arrivedat is that it is written in a form the guard checks or it is not written — the table forbids what
it cannot check. A green run means "every crate has a well-formed row inside a real table, and
every claim cargo can settle is settled", not "the table is true".
Changed paths
README.mdAGENTS.mdCLAUDE.mdis a symlink to this file)mise.toml[tasks.check-readme-crates]entrytooling/check-readme-crates/run.shtooling/*/Cargo.tomlfmt glob)tooling/check-readme-crates/fixtures.sh.github/workflows/ci.ymlNo crate's
STATUS.mdorCargo.tomlwas touched: they are the authority, and the README iswhat was wrong.
What the review caught, and what it changed
The first two commits were put through an independent adversarial review whose brief was to
falsify every new cell against its authority. It falsified eight of them, and commit 3 is the
repair. This is recorded rather than quietly folded in, because the errors were the same class
of error this PR exists to fix — asserting a pleasing state instead of the measured one:
gamut-metadatafacade"gamut-dngand the umbrella depend on it; this is the single largest thing #416 exists to closegamut-jpeg/gamut-dng/gamut-png"unpublished"gamut-cmmis absent from crates.iogamut-cmm"feature-complete against Little-CMS"gamut-cmm"(P1–P7)"gamut-avif"10/12-bit deferred"Av1StillDecodergamut-codec-abi"in use by gamut-jpeg and gamut-heic"gamut-core/-color/-dsp"stable (v1)"docs/testing.mdgivesgamut-ifdno in-crate oracle andgamut-metadataa round-trip law; exiv2 and XMPCore are one oraclePlus three misattributions:
gamut-heic's slices span #238/#273/#303/#429 (not #238 alone),gamut-cliencodes six formats (not two), andgamut-dsp's Purpose still advertised waveletsand filtering, which its frozen surface does not include.
The review also hardened the guard:
LC_ALL=Con both sides of everycommso a locale cannotdesynchronise them against jq's codepoint ordering, an explicit diagnostic instead of a bare
set -eabort whencommorcargo metadatafails, repository-root resolution so running itfrom a subdirectory no longer reports a cwd mistake as a table defect, and the duplicate-row
check.
Validation
Deliberately not run:
mise run lintandmise run test. This change contains no Rust — thediff is Markdown, one TOML task entry, one shell script and five lines of YAML — so the workspace
suites have nothing to say about it, and the lane contract scopes gates to what the change can
break. No workspace build was run in any round, and none is claimed.
mise run check-readme-crates(final tree)README crates table lists every workspace crate (32 crates)mise run check-readme-crates(onmaster's README, before the fix)gamut-codec-abi,gamut-dng,gamut-jpeg,gamut-tonemapas rowless. The guard proving it would have caught the defect.run.sh <copy with the gamut-dng row deleted>gamut-dng— deleted-row directionrun.sh <fixture with a phantom row>gamut-renamed— phantom-row direction. The fixture also carries amise run testrow in a different section, which the guard correctly ignores.run.sh <fixture listing gamut-dng twice>gamut-dng— duplicate-row directionrun.shinvoked fromcrates/__CARGO_TEST_ROOT=<worktree> mise run fmt-checkFinished in 11.38s)mise run check-testsmodule docs, pinned proptest seeds and oracle filenames all conformmise run check-commitsno errors in 3 commitsmise run check-msrvcheck-msrvwas run inside the mandated memory-capped systemd scope, after waiting for theshared box's load average to fall below 32.
The
__CARGO_TEST_ROOTprefix onfmt-checkis a known environment artefact of running in anested worktree, where cargo otherwise walks past the worktree root to the
primary checkout's manifests. It is not a change to any manifest.
Every issue number cited in the new table and prose was checked against
gh issue viewbeforebeing written: #24, #28, #34, #107, #109, #177, #179, #180, #182, #186, #188, #189, #192, #194,
#195, #238, #243, #249, #250, #263, #268, #273, #277, #280, #323 and #372 are CLOSED and are
cited only as completion markers; #242 and #416 are OPEN and are the only two cited as
outstanding work. Publication state was taken from the crates.io API, not from git tags.
Risks and rollout
feat/224-png-encoder-efficiency), which is open, active,and also edits
README.md. Whoever merges second resolves a textual conflict. This wasaccepted up front rather than avoided: this branch is not based on feat(png): measure the encoder end to end, then move what it exposed #485, does not coordinate
with it, and touches nothing it owns. The human merging second should expect to resolve
README.mdby hand. The conflict is confined to that file;mise.tomland the newtooling/script are untouched by feat(png): measure the encoder end to end, then move what it exposed #485.behaviour. Rollback is
git revert.not authorised to do. So today it is available to a human and to a later CI change, but nothing
runs it automatically. Filed as CI: run
mise run check-readme-cratesin the Format & Metadata job #524. Until then the guard cannot stop the next omission.No longer true as of round 3, and the correction belongs here rather than 300 lines below:
decision 17 overrode that rule and added one step to
ci.yml's Format & Metadata job, so theguard runs on every push and pull request and this PR
Closes #524.fixes — is still caught only by review, as this PR's own eight falsified cells demonstrate.
That is a deliberate limit, argued in the script's header comment and in the record below.
Rounds 2–4 widened it past membership: a crate row must also be a three-cell row whose
Purpose and Status render as something, and the crate rows must stand under the table's
delimiter row. The prose remains ungated, which is still the limit that matters.
STATUS.md.STATUS.mdfiles move, sosome rows will age; that is the residual the guard explicitly does not cover.
STATUS.mdis not the authority and rounds 2–4 stopped treating it as one — see decision 23.The residual is unchanged: a row that goes stale is caught by a reader, not by the guard.
Issue
Closes #524— CI: runmise run check-readme-cratesin the Format & Metadata job. That issuewas filed by this lane in round 1 and is resolved by this PR in round 3 (decision 17), so it is
carried here rather than left open behind its own fix.
Refs #425. Not
Closes: the issue's third scope bullet asks whether a hand-maintained statuscolumn is the right shape at all, and this PR answers "yes, with a membership guard" rather than
replacing it with a generated table — a maintainer should confirm that answer before the issue
closes. Everything in the issue's stated Acceptance ("every row matches the crate's
STATUS.mdandCargo.tomlversion. No reference to a closed issue as ongoing work") isdelivered here.
Remainders filed while working, all outside this PR's boundary:
gamut = "0.1", a version that was never published #523 — README: the Usage snippet pinsgamut = "0.1", a version that was never published(the umbrella crate is at 0.3.2, so the snippet does not resolve).
That stated reason is false — 0.1.0 is published and unyanked, so the pin resolves and is
merely three months stale. See "A correction to the Issue section above";
README: the Usage snippet pins
gamut = "0.1", a version that was never published #523 is superseded by README: the Usage snippet pinsgamut = "0.1", three months behind the current release (supersedes #523) #559, which also carries the right line number.mise run check-readme-cratesin the Format & Metadata job #524 — CI: runmise run check-readme-cratesin the Format & Metadata job.Resolved by this PR, not a remainder: round 3 wired the step (decision 17), and the
Closesabove carries it.
publish. Found by the review that falsified this PR's own "unpublished" annotations: the
section conflates "has no release tag" with "is not on crates.io", and five of the six crates
it names are on crates.io.
Fixed here in round 4, so README Releases section lists five already-published crates as awaiting their first publish #527 is resolved in substance (decision 22). This PR does not
Closesit: this run never closes an issue it did not open the fix for by agreement with amaintainer, and README Releases section lists five already-published crates as awaiting their first publish #527 was filed as a separate README defect. A maintainer should close it after
reading the §Releases diff.
No human approved this plan: this is an unattended run, and the decision record below is what a
human reads instead.
Decisions taken
This record is append-only. No entry below is edited after it is written, including
parenthetically: an entry's value is that it shows what was believed and when. Where a later
round found an entry wrong, the correction is a new numbered entry at the end carrying a
Corrects:line, and every entry that has been corrected is named there. Read to the bottombefore acting on any entry.
Unresolved review notes
Two findings from the review were not repaired here, both deliberately:
.github/workflows/ci.ymlis a workflow edit this run was not authorised to make. Filed asCI: run
mise run check-readme-cratesin the Format & Metadata job #524. Until that lands, the guard protects nothing automatically.Resolved in round 3 (decision 17): the step exists, the guard runs,
Closes #524.untagged crates as awaiting their first publish, five of which are on crates.io. That section
is outside this PR's boundary; filed as README Releases section lists five already-published crates as awaiting their first publish #527. This PR's own rows and prose are correct
against crates.io, so the contradiction is now visible in one direction only.
Resolved in round 4 (decision 22): §Releases now names only
gamut-cmmas unpublished andsays plainly that a missing tag is not a missing publish.
One further limit, not a finding but worth stating: the review verified the table against each
crate's
STATUS.mdas of this commit. Those files move independently, and nothing in this PRdetects a row that goes stale afterwards — that is exactly what decision 2 declined to automate.
Round 2 — an independent adversarial review of the pushed branch
The branch at
80a545awas handed to an independent reviewer briefed to falsify it. Ten findingscame back. What follows is each one and what this branch did with it; the decisions are appended
to the record above as 8–14.
The guard held. It was attacked in ten directions — row deleted, phantom row, duplicate row, a
real new crate created on disk, a crate renamed across three manifests, the whole table removed, a
crate-shaped row injected into another section, run from a subdirectory, and replayed against
master— and failed correctly in every one, including two this PR never claimed. 26 of the 32rows verified accurate, including every cell the first self-review repaired. Two of the
reviewer's own challenges failed honestly:
Av1StillDecoderdoes exist, and "gamut-cmm is the onecrate not yet on crates.io" is true on both readings.
The central finding was not about the guard's strength but about its reach: it licenses
membership, and membership was never the failure mode that mattered. Text staleness was — and a
green guard reads as "table checked".
gamut-bitstreamadvertised "entropy coders (ANS, arithmetic, Huffman)"; its Status "stabilizing api" had no authority at allcrates/gamut-bitstream/src/lib.rs:19-20says the ANS and Huffman coders "are not implemented yet"; thepub uselist (lines 29–33) carriesSymbolDecoder/SymbolEncoder— the AV1 §8.2 coder — and nothing else. The crate has noSTATUS.md,grep -rni 'stabiliz' crates/gamut-bitstream/returns nothing, andgh issue list --search "Release gamut-bitstream"finds no v1 issue, so the cell now states the version and the two gaps instead.gamut-dsppast what ships, dropping thejpegmodulecrates/gamut-dsp/src/lib.rs:43=pub mod jpeg;crates/gamut-dsp/src/jpeg/dct.rs:112,163=fdct8x8/idct8x8. The row had been derived faithfully fromcrates/gamut-dsp/STATUS.md:67, which still listsjpegas deferred — so decision 3's premise had a hole. Decision 9 settles it:lib.rswins.gamut-codec-abirow undercounted the seam's consumersgrep -rn 'gamut-codec-abi' crates/*/Cargo.tomlreturns eight manifests: avif, heic, jpeg, jxl, png, webp, ffi (line 27, unconditional) and the optional umbrella.crates/gamut-ffi/src/lib.rs:83-84const-assertsABI_VERSION == 1andGAMUT_MAX_PLANES == MAX_PLANES. Seven direct consumers, not six.gamut-av1understated the crate, and "(alpha)" is undefinedcrates/gamut-av1/Cargo.toml=default = ["decode"];src/lib.rs:58-59re-exportsAv1Decoderand friends;src/decode/is 4223 lines.src/decode/mod.rs:20-22bounds the implemented scope to 8-bit 4:4:4 intra key frames under #259. The undefined "(alpha)" is gone —gamut-webptwo rows below uses "+alpha" for the alpha channel, whichgamut-av1does not support.mise run check-readme-cratesis now in the Development table, which already listedcheck-commits,check-crossandcheck-msrv.gamut-riffis at local1.0.0while crates.io stops at0.1.3gamut-png's "spec-compliant decoder" of the "W3C 3rd edition" is a claimAGENTS.mditself qualifiesreferences/png/png-3.htmlcarries 87acTL/fcTL/fdAThits and an "Animated PNG" clause — whilecrates/gamut-png/STATUS.md:18puts it out of scope. The row now says "over the still-image subset" and states that an APNG decodes as its default image.gamut-color's Purpose named 5 things where the crate has 15 public modulesgrep -n '^pub mod' crates/gamut-color/src/lib.rsreturns fifteen. The row now names the metadata layer and thef64colour-science half, and carries the Tier-1 reproducibility caveat fromsrc/lib.rs:15-17.A correction to the Issue section above
The
## Issuesection above describes #523 as "the Usage snippet pinsgamut = "0.1", aversion that was never published … so the snippet does not resolve". That is false, and it is
left standing above rather than rewritten so the correction is visible.
Verified directly against
https://crates.io/api/v1/crates/gamut/versionson 2026-09-10:0.1.0was published and is not yanked, so the caret requirement^0.1does resolve — to0.1.0, two minor releases behind the current0.3.2. The defect #523 describes is real: thesnippet a new user copies hands them a release from three months ago rather than the current one.
Its stated reason is not. This run never edits an existing issue, so #523 keeps the error in
its body and this record carries the correction.
This is the same error class the first self-review caught and named — inferring publication state
from something that is not publication state — relocated from the diff into a filed issue.
The guard's behaviour on a degenerate row
The gap F6 names, demonstrated before and after. The fixture is this README with the
gamut-corerow reduced to a single cell holding nothing but the crate name:
80a545a'srun.sh)README crates table lists every workspace crate (32 crates). The gap.gamut-core has 1 cell(s); a crate row is Crate | Purpose | Statusgamut-core has an empty Purpose cell/… empty Status cellREADME crates table lists every workspace crate (32 crates)gamut-tiff)gamut-tif)The manual line in the Validation table above — "markdown structure: all 32 crate rows are
well-formed 3-cell rows" — is now the script's job rather than a claim in a PR body.
Round-2 validation
No workspace build was run and none is expected: the round-2 diff is Markdown, one TOML
description string, and one shell script.
mise run lintandmise run testwere not run andare not claimed.
mise run check-readme-cratesREADME crates table lists every workspace crate (32 crates)__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-check__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-tooling-checkmise run check-commitsno errors in 4 commitsbash -n tooling/check-readme-crates/run.sh100755run.shagainst six fixtures (degenerate, blank-celled, escaped-pipe, deleted-row, phantom-row, real)The
__CARGO_TEST_ROOTprefix remains the known nested-worktree artefact; no manifest was changedfor it.
Remainders filed in round 2
STATUS.mddrift:gamut-dsplists a shipped module as deferred,gamut-avifcontradicts itself. The two files behind F2, filed rather than edited (decision 10).
gamut-riffdeclares1.0.0but crates.io stops at0.1.3. Filed with a survey ofall 32 crates: thirty manifest versions match crates.io exactly,
gamut-riffandgamut-cmmare the only two that do not, and
gamut-cmm's gap is expected.Unresolved review notes (round 2)
review's central finding. Decision 12 added the one structural property a machine can settle;
the text of a Purpose or Status cell is still caught only by a reader. A green
check-readme-cratesmeans "every crate has a well-formed row", not "the table is true" — thescript's header comment now says so explicitly, so the next person cannot mistake one for the
other.
mise run check-readme-cratesin the Format & Metadata job #524). The guard runs on demand and in no workflow.Closed in round 3 (decision 17).
gamut-riffrow saysstable (v1, #186), which is true of the working tree and not of anything on crates.io. It isleft as it is because correcting it means deciding whether
gamut-riffis v1 — a releasedecision, not a documentation one.
README.mdconflict with PR feat(png): measure the encoder end to end, then move what it exposed #485 is unchanged and still disclosed. Round 2touches more of the crates table than round 1 did, so the conflict is wider; it is still
confined to
README.md.lib.rs,Cargo.tomlandSTATUS.mdin that order of authority. Those files move, and nothing detectsa row that goes stale afterwards.
Round 3 — a second independent adversarial review of the pushed branch
The branch at
2fb38c2was handed to a second reviewer. The guard was attacked in 28directions and failed correctly in 18 of them, including several round 2 never tried — a row split
across two lines, a phantom prefix crate, the umbrella row dropped, the Crate and Purpose
columns swapped, an uppercase letter in a crate name. Five of the six rows round 2 changed were
re-confirmed against the right authority, and both issues round 2 filed were confirmed accurate
and non-duplicative.
The findings, and what this branch did with each:
gamut-corerow names two traits that do not exist in that crateREADME.mdandAGENTS.md.grep -n "pub trait" crates/gamut-core/src/lib.rsgivesEncodeImage(378) andDecodeImage(409).Encoder/Decoderare real, incrates/gamut-codec-abi/src/lib.rs:380,396— a different crate with its own row five rows up the same table. This branch's own diff rewrote that cell (it addedconvert) and carried the false names through; the self-review and two adversarial passes all worked on rows around it and missed it. Decision 15.gamut-color's Purpose omitsycbcrandlinalgcrates/gamut-color/src/ycbcr.rsis 1436 lines, the crate's largest module — the H.273 §8.3 matrixing layer plus the libwebp-exact 8-bit BT.601 layer VP8 requires.format::ChromaSubsampling, which the cell did name, is a different thing. The cell now namesycbcrandlinalg.run.shline 130 said "taking the status from the crate'sSTATUS.md" andmise.tomlsaid the same, while the header sayslib.rs,Cargo.toml,STATUS.md. Following the message reproduces thegamut-dspdefect decision 9 fixed. All three now state the same order. Decision 18.ci.yml's "Format & Metadata" job, beside the three sibling static checks; the job already installsjq(ci.yml:49) and a stable toolchain (ci.yml:53), the guard's only two dependencies. #524 is resolved by this PR rather than left filed. Decision 17.mise.tomlclaims the guard checks the table "is well formed"; a table with its| --- |delimiter deleted passesgamut-av1's "intra key frames only" understates the decodercrates/gamut-av1/src/decode/header.rs:363acceptsINTRA_ONLY_FRAMEas well asKEY_FRAME, line 411 codes its refresh mask, and line 1301 tests it. The row now says "intra frames, key and intra-only".mise.toml's comment still says membership onlyTwo pre-existing false claims the reviewer surfaced outside this diff are filed, not fixed:
#560 —
gamut-av1'sCargo.tomldescription is encoder-only abovedefault = ["decode"], andgamut-avif's module doc lists as "Deferred, planned" both the pure-Rust AV1 codestream decoder(
gamut-av1ships it, default-on) and the alpha/10-12-bit encoding its ownEncodeImageimplsalready provide.
The guard's behaviour on each new bypass, before and after
Fixtures are this README with one row altered;
beforeis2fb38c2'srun.sh,afteris thishead's. Exit status, not just message:
gamut-core has an empty Purpose cell/… Status cell| \gamut-core` || stable |`)gamut-core has an empty Purpose cellgamut-corerow wrapped in<!-- … -->workspace crates with no row … gamut-coregamut-corerow inside a ``` fenceworkspace crates with no row … gamut-corefound no crate rows under '## Crates'\\|README crates table lists every workspace crate (32 crates)crates/The awk is POSIX-only (no
gensub, no interval expressions, dynamic regexps built as strings)because
awkon the CI runner is mawk, not gawk. All twelve fixtures were re-run undergawk --posixandgawk --traditionalwith identical results. The CI step added by F5 is thereal evidence: it runs the guard against this README under the runner's own awk, so a mawk
disagreement would show as a red check rather than as a silent pass.
The registry survey, restated with its method
All 32 workspace crates were queried against the sparse index,
https://index.crates.io/ga/mu/<name>, on 2026-09-10, one request per crate, counting everyversion row and its
yankedflag:cargo metadata --no-deps)gamut-cmmgamut-riff, manifest1.0.0, newest published0.1.3(#546)Round 2's "thirty of thirty-two match" is this figure and is correct. Any "five published"
reading is a misreading: the five-row table under "A correction to the Issue section above"
lists the
gamutumbrella crate's own five published versions (0.1.0 … 0.3.2) and is correct assuch — it was never a count of published crates, and under a five-published premise "thirty match"
could not hold.
This PR now states the point above the table itself rather than only here: a version cited in a
row is the crate's own
Cargo.toml, not what crates.io serves. That resolves the one disagreeingrow honestly without this PR pre-empting a release decision about
gamut-riff.A correction to the #545 attribution
Round 2's decision 10 and the #545 body say
gamut-avif/STATUS.md:21-24"overlaps PR #506'sterritory … if #506 already does, this half can be closed as covered."
That is true at file granularity and false at territory granularity.
gh pr diff 506 --name-onlydoes listcrates/gamut-avif/STATUS.md, but its hunks on that file start at lines48, 125 and 330 — none of them reaches lines 21-24, and #506 is a C2PA manifest-store change
that has no reason to. So merging #506 will not fix the M2 block, and "can be closed as covered"
invites exactly the silent drop the issue was filed to prevent. #545's second half should not be
closed on #506 merging; it needs its own edit. This run cannot comment on #545, so the
correction is recorded here.
Round-3 validation
No workspace build was run and none is expected: the round-3 diff is Markdown, one shell
script, one TOML comment and description, and five lines of YAML.
mise run lintandmise run testwere not run and are not claimed.mise run check-readme-cratesREADME crates table lists every workspace crate (32 crates)__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkFinished in 7.49s__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-tooling-checkFinished in 2.06smise run check-commitsno errors in 7 commitsbash -n tooling/check-readme-crates/run.sh100755run.shagainst twelve fixtures, before and after, under three awk modesRemainders filed in round 3
gamut = "0.1", three months behind the current release (supersedes #523) #559 — supersedes README: the Usage snippet pinsgamut = "0.1", a version that was never published #523: the Usage snippet pin is three months stale, not unresolvable.gamut0.1.0 is published and unyanked (sparse index, 2026-09-10), so^0.1resolves — to arelease from 2026-06-01. README: the Usage snippet pins
gamut = "0.1", a version that was never published #523 also citesREADME.mdline 108; the snippet is at line 99 onmasterand 111 on this head.gamut-av1's crate description andgamut-avif's module doc each assert acapability direction their own manifest or source contradicts.
lib.rs, thenCargo.toml, thenSTATUS.md" therepository-wide authority order for a documented claim, or does it stay local to this guard?
Filed rather than written into
AGENTS.md, because generalising it is a bigger claim than oneREADME table established.
#524 is no longer a remainder — F5 is fixed in this PR.
Unresolved review notes (round 3)
wrong crate passed every membership and shape check, and was caught by a reader. A green
check-readme-cratesmeans "every crate has a well-formed row", never "the table is true".| --- |delimiter are not checked. Delete the delimiter and everyrow renders as literal text while the guard still passes. Written down in the script header as
an unclosed hole rather than papered over by the word "well formed"; closing it is a different
property (does the table render) from the one the guard states.
Closed in round 4 (decision 25): the delimiter is now one assertion. The header row's own
text is still ungated, and deliberately — that is prose like any other cell.
written on, so portability rests on the awk being POSIX-only plus
gawk --posix/--traditionalagreement. The CI step added by F5 is what actually tests it, on the runner'sown awk.
README.mdconflict with PR feat(png): measure the encoder end to end, then move what it exposed #485 is unchanged and still disclosed.lib.rs,Cargo.tomlandSTATUS.mdin that order. Those files move, and nothing detects a row that goes staleafterwards.
Round 4 — a third independent adversarial review of the pushed branch
The branch at
c34ec3ewas handed to a third reviewer. Every round-2 finding was confirmedclosed and none could be reopened on the merits; the guard held under 19 constructed
attacks; the CI wiring was verified independently of this body's quoted log (the job carries no
conditional, the step no
continue-on-error); and the registry survey reproduced to thedigit. What came back was three Medium findings, one Low, four guard bypasses and five row
inaccuracies.
gamut-codec-abi"repr(C)vtables + the backend registry"crates/gamut-codec-abi/src/lib.rs:10— "Backends are held in a host-owned registry"; the crate declares no registry type.AGENTS.mdsays "the registry fallback contract" and the row had dropped the word that made it true. This is round 3's F1 shape exactly, two rows away from where round 3 fixed it. See the sweep below. The same row's consumer list also omitted the umbrella (crates/gamut/Cargo.toml:63,84). Decision 21.gamut-riff— the preamble's other cited exception — appeared in it nowhere. #527 is resolved in substance. Decision 22.crates/gamut-avif/src/lib.rs:143-144lists "alpha / RGBA encoding, 10/12-bit and 4:2:0/4:2:2 chroma" under "Deferred, planned" (#560), while the row says 8/10/12-bit — and the row is right:src/encoder.rs:934,972,998areimpl EncodeImage<Rgba8>,<Rgb16>,<Rgba16>. The rule applied was never "lib.rs wins"; it was "the crate's source wins". All three sites now say that, and cite this row. Decision 23._or a capital letter is invisible to the name pattern| --- |delimiter hole was documented rather than closedmise.tomlcomment: measured at 23 of 28 rows corrected, 4 rows added (master's table had 28 rows; 5 are untouched —gamut-av2,gamut-icc,gamut-riff,gamut-vvc,gamut-wasm).The five rows:
gamutcrates/gamut/src/lib.rs:44-92— 25pub usere-exports, of which 11 are format/codec crates and 14 are not;gamut_core as core(line 58) carries nocfggamut-webpcrates/gamut-webp/src/lib.rs:94pub mod backend;WebpCodestreamDecoder/WebpCodestreamEncoderandpush_backend(src/decoder.rs:73,src/encoder.rs:222) — the seam thegamut-codec-abirow credits it withbackendseamgamut-metadatasrc/embed.rs:63-64— "c2pais a carrier field, but no policy fills it";src/embed.rs:129c2pa_policy: C2paPolicy::Drop. Extraction copies it verbatim (src/extract.rs:105); embedding never writes itgamut-jpegsrc/lib.rs— XYB isJpegEncoder::with_color_mode, an encoder option; the decoder presentsRgb8/Gray8/Cmyk8gamut-tonemapcrates/gamut-tonemap/src/lib.rs:62re-exports eight:Aces,Clamp,Drago,Exposure,Hable,Linear,Reinhard,ReinhardExtendedAGENTS.mdcarried the same short list and is corrected with itThe class sweep behind M2
M2's instruction was to sweep the class, not the row. Every backticked identifier in every crate
row was extracted mechanically and resolved to a crate — 18 tokens across 13 rows — together with
the unbackticked construct names:
Correction (round 5): ‘18 tokens across 13 rows’ reproduces as 18 tokens across TWELVE rows
—
gamut,gamut-core,gamut-color,gamut-tonemap,gamut-codec-abi,gamut-av1,gamut-avif,gamut-webp,gamut-ifd,gamut-tiff,gamut-dng,gamut-cli. The token countis right; the row count is not. See decision 31.
gamut-codec-abigamut-webp/gamut-jxlgamut-coreEncodeImage,DecodeImage,convertsrc/lib.rs:378,409,56gamut-colorycbcr,matrix,linalg,f64src/lib.rs:63-77(15pub mod)gamut-avifAv1StillDecodercrates/gamut-avif/src/decode.rs:86gamut-tonemapToneCurvesrc/curve.rs, re-exportedsrc/lib.rs:60gamut-webpbackendsrc/lib.rs:94gamut-av1decodeCargo.toml:19,22(default = ["decode"])gamut-ifdbigtiffCargo.toml:23gamut-codec-abicodec-abigamut-tiff,gamut-dnggamut-ifdgamutgamut::coregamut-cligamutgamut-bitstreamSymbolDecoder/SymbolEncoder,src/lib.rs:33gamut-dspsrc/jpeg/dct.rsgamut-heicpub trait HevcDecoder,src/decode.rs:74One hit, and it is the one M2 named — plus the umbrella omission in the same row. No other row
attributes a construct to the wrong crate.
Correction (round 5), twice over. First, the table above lists 17 of the 18 tokens:
repr(C), in thegamut-codec-abirow, is in the extraction and missing from the table. Itresolves correctly (
crates/gamut-codec-abi/src/lib.rs:1, and therepr(C)vtables areDecoderVTable/EncoderVTablein that crate), so the verdict stands and the table wasincomplete. Second, "no other row attributes a construct to the wrong crate" was unsupported:
it was written from a sweep of the backticked tokens plus three unbackticked ones, while roughly
thirty unbackticked construct names had never been resolved. Round 5 enumerates all 47 of them and
finds two more hits (
gamut-color"Pixel formats",gamut-deflate"decoding stays onminiz_oxide"). See "The unbackticked enumeration" below, and decisions 27, 28, 31.
The guard's behaviour on each round-4 bypass, before and after
beforeisc34ec3e'srun.sh,afteris this head's; fixtures are this README with one thingaltered. Exit status, not just message:
| --- | --- | --- |delimiter row deletedthe … crates table has no '| --- | --- | --- |' delimiter row above its first crate row`gamut_renamed`(underscore)names crates that are not workspace members: gamut_renamed`Gamut-Core`(capital)names crates that are not workspace members: Gamut-Core<span></span>, Status<br/>gamut-core has an empty Purpose celland… empty Status cellREADME crates table lists every workspace crate (32 crates)| :--- | :---: | ---: |)README crates table lists every workspace crate (32 crates)crates/All eighteen fixtures were re-run under the system
awk,gawk --posixandgawk --traditional— 54 invocations of each script — with identical exit codes in all three; thenew code adds no gawk extension (correction, round 5: those are three MODES OF ONE
INTERPRETER.
/usr/bin/awkon this machine is GNU Awk 5.3.2 andcommand -v mawkfinds nothing,so the sweep is a self-consistency check and the CI step is the portability evidence. Decision
31.) (
sprintf("%c", 13),split,substr, no interval expressions,no dynamic regexp). Five attacks flip
pass → fail(delimiter deleted, underscore phantom,capital phantom, empty HTML element) and one flips
fail → pass(CRLF, which was never a defect);the other thirteen are unchanged, so nothing the earlier rounds caught was traded away.
Round-4 validation
No workspace build was run and none is expected: the round-4 diff is Markdown, one TOML
comment and description, and one shell script.
mise run lintandmise run testwere notrun and are not claimed.
.github/workflows/ci.ymlwas not touched in this round.mise run check-readme-cratesREADME crates table lists every workspace crate (32 crates)__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkFinished in 4.06s__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-tooling-checkFinished in 1.51smise run check-commitsno errors in 10 commitsbash -n tooling/check-readme-crates/run.sh100755run.shagainst eighteen fixtures, before and after, under three awk modesREADME crates tablestep of Format & Metadata is success on this head — the mawk evidence the previous rounds could only reason aboutgamut-riffgamut-cmmHTTP 404,gamut-riffnewest0.1.3gamut-riff,1.0.0vs0.1.3), 1 absent (gamut-cmm) — round 3's figure reproduces exactly, and the preamble now cites a §Releases section where both exceptions are actually written downRemainders in round 4
None filed. Every finding in round 4 was inside this PR's files and is fixed here; #527 is
resolved in substance by decision 22 and is left for a maintainer to close.
Unresolved review notes (round 4)
inaccuracies and one wrong-crate attribution all passed every membership, shape and delimiter
check and were caught by a reader. A green
check-readme-cratesmeans "every crate has awell-formed row under a real delimiter", never "the table is true".
gamut = "0.1", a version that was never published #523 (the Usagesnippet pin) states a reason crates.io contradicts; README: the Usage snippet pins
gamut = "0.1", three months behind the current release (supersedes #523) #559 supersedes it with the right reasonand the right line number. The lane contract forbids editing, commenting on or closing an
existing issue, so both stand and a maintainer should close README: the Usage snippet pins
gamut = "0.1", a version that was never published #523 in favour of README: the Usage snippet pinsgamut = "0.1", three months behind the current release (supersedes #523) #559.is left open for a maintainer, since this run did not file it against a fix it had agreed.
POSIX-only awk plus
gawk --posix/--traditionalagreement. The CI step is what tests it onthe runner's own awk.
column headings are called is prose, and deliberately left to a reader.
correction above.
README.mdconflict with PR feat(png): measure the encoder end to end, then move what it exposed #485 is unchanged and still disclosed, and iswider again: round 4 touches six more crate rows, the §Releases section and one
Development-table row.
nothing detects a row that goes stale afterwards.
Round 5 — a fourth independent adversarial review of the pushed branch
The branch at
703afc5was handed to a fourth reviewer. The verdict split, and the split is thefinding. The backticked half of round 4's sweep reached a fixed point: the reviewer
re-derived the extraction independently, got the same 18 tokens, resolved every one the same way,
and could not manufacture another instance of the strong form. The record was verified
append-only against the platform's own edit history, and the registry survey reproduced exactly —
all 32 index queries and all 39 cited issue numbers.
The unbackticked half was not swept. Round 4 resolved three unbackticked construct names and
then wrote "no other row attributes a construct to the wrong crate". Roughly thirty were never
looked at, and the review's one new finding came out of them.
gamut-colorcell leads with a construct declared ingamut-core, in a row this branch rewrotepass → fail. Decision 30.## Cratesheading now tolerates a trailing space, which is the one of the six that is not about the row pattern. Decision 30.gamut-codec-abicell carries a new counting errorThe unbackticked enumeration
Round 4 published its backticked extraction; this is the other half, derived the same way and
published the same way. 47 construct names across 29 rows — every noun phrase in a Purpose or
Status cell that names a type, module, trait, feature, dependency, capability or specification,
resolved to the crate that owns it. (Correction, appended before this branch moved
again: the table below has 48 entries, not 47 — the count was taken by hand and the table by
script, which is decision 29's argument arriving a fourth time. 29 rows is right. Decision 33.) The three rows carrying none are
gamut-av2,gamut-vvcandgamut-wasm, whose cells are the format name and the wordplaceholder.gamutcrates/gamut/src/lib.rs:44-92— 25pub use; 11 format (av1, av2, avif, dng, heic, jpeg, jxl, png, tiff, vvc, webp) + 14 shared (bitstream, cmm, codec_abi, color, core, dsp, exif, icc, ifd, iptc, isobmff, metadata, tonemap, xmp)gamut-coresrc/image.rs:47,151—ImageRef,ImageBufgamut-coresrc/lib.rs:326—Dimensionsgamut-coresrc/lib.rs:80,170—ErrorKind,Errorgamut-coresrc/lib.rs:56—pub mod convertgamut-colorgamut-core—src/pixel.rs:120, re-exportedsrc/lib.rs:70; zero hits ingamut-color, andgamut-color/src/format.rs:3-4disclaims it by namegamut-colorsrc/format.rs—BitDepthgamut-colorsrc/format.rs—ChromaSubsamplinggamut-colorsrc/cicp.rs— the four CICP enums,src/lib.rs:80gamut-colorsrc/planar.rs,src/planar16.rs—Planar8,Planar16gamut-colorsrc/ycbcr.rs(1436 lines, the crate's largest module)gamut-colorpub modatsrc/lib.rs:63-77gamut-colorsrc/lib.rs:15-17gamut-dspsrc/av1/mod.rs:29-32—forward_adst,forward_identity,forward_wht4x4,forward_dctgamut-dspsrc/math.rs:47—round_div_nearest, "the encoder forward-quantize rounding shared by the AV1 and VP8 encoders" (src/lib.rs:9)gamut-bitstreamsrc/lib.rs:29-30—BitReader,BitWritergamut-bitstreamsrc/lib.rs:31—leb128_len,write_leb128gamut-bitstreamsrc/lib.rs:32—pack_msb_rows,unpack_msb_rowsgamut-bitstreamsrc/lib.rs:19-20says exactly that, and thepub uselist carries neithergamut-tonemapsrc/lib.rs:62— all eight re-exportedgamut-codec-abisrc/lib.rs:374,396—Decoder,Encodergamut-codec-abigrep -rn "fn push_backend" crates/: avif 1, heic 1, jpeg 2, jxl 2, png 2, webp 2);gamut-ffiand the umbrella own nonegamut-isobmffsrc/lib.rs:5,13,52— "leaves the coded bitstream opaque"gamut-riffsrc/lib.rs:45-53—Chunk,FourCc,RiffReader,RiffWriter,webp::*gamut-av1src/lib.rs:4,66—encode_still_lossless_identity,encode_still_intra_withgamut-av1src/decode/header.rs:51,363—INTRA_ONLY_FRAMEaccepted besideKEY_FRAMEgamut-avifsrc/decode.rs(#250)gamut-jxlCargo.toml:26,38— the externaljxlcrate (its own docs call it jxl-rs:src/lib.rs:9) for decode,gamut-jxl-sysfor encodegamut-jxl-sysCargo.toml:31jpegxl-src = "=0.12.0";build.rs:24pins the same stringgamut-jpegsrc/lib.rs:35—JpegEncoder::with_color_mode;STATUS.md:59names it P13/#334; the decoder presentsRgb8/Gray8/Cmyk8gamut-jpegSTATUS.mdphase tablegamut-webpsrc/lib.rs:38-55—with_effort/Effort,with_near_lossless/NearLosslessgamut-heicsrc/decode.rs:74—pub trait HevcDecodergamut-heicSTATUS.md:9-17,86— S1 … S7 (#429)gamut-ifdsrc/lib.rs:97-99—ByteOrder;Field/Ifd/SubIfd/Variant;read/read_treegamut-exifsrc/maker_note.rs; #418 tracks decodinggamut-iccsrc/lib.rs:9— "Layouts follow ICC.1:2022 (profile version 4.4)"gamut-cmmsrc/lib.rs:7— "the transform engine";src/transform.rs:34—pub trait Transformgamut-xmpsrc/lib.rs:1,7,44— "canonical RDF/XML (Part 1 §7)", "one fixed canonical form"gamut-iptcsrc/lib.rs:83,85—pub mod iim,pub mod photo_metadatagamut-metadatasrc/lib.rs:25,83;src/embed.rs:63-64,129—c2pa_policy: C2paPolicy::Dropgamut-tiffSTATUS.md:16(JPEG-in-TIFF),:39(P15 YCbCr ⏳),:40(P16 CIE L*a*b* ⏳)gamut-dngsrc/lib.rs:22,STATUS.md:14— "the authoritative Adobe DNG SDK 1.7.1"gamut-deflateCargo.tomlhas no[dependencies]table;miniz_oxideat line 28 is under[dev-dependencies](line 18) as a bench baseline. The inflate dependency isgamut-png/Cargo.toml:28andgamut-dng/Cargo.toml:50gamut-pngsrc/lib.rs:9;STATUS.md:18gamut-clisrc/main.rs:63— the subcommand's own docgamut-clisrc/commands/{color,dsp,bitstream}.rsgamut-ffisrc/lib.rs:4— "The surface implemented today is the provider boundary (issue #280)"Three hits in 48 (published as "three hits in 47"; see the correction above) — the one D1 named, the count in the
gamut-codec-abicell, and one thereview did not name (
gamut-deflate). The claim this table supports, and the earlier one itreplaces, is now: every construct name in every cell, backticked or not, has been resolved to
a crate, and the three that resolved elsewhere are fixed. That is a result rather than an
assertion, and it is what round 4's unsupported sentence should have been.
A by-product:
crates/gamut-color/src/lib.rs:1-2is the stale line the D1 phrase came from, andcrates/gamut-cli/src/main.rs:3-4credits the third-partyimagecrate with three formats gamutdecodes itself. Both are filed as #590 — the same class as #545 and #560, and this PR's own
gamut-colorrow depended on the first of them.G1 — the five bypasses, before and after
beforeis703afc5'srun.sh,afteris this head's. Fixtures are this README with one thingaltered; the five are the reviewer's own constructions.
the line directly above the first crate row is not a '| --- | --- | --- |' delimiter rowthe delimiter above the first crate row has no header row directly above itheader and its delimiter disagree on how many columns the table has: 2 header column(s) over a 3-column delimiterthe line directly above the first crate row is not a … delimiter row , Status​gamut-core has an empty Purpose celland… empty Status cell , Status​## Crateswith a trailing space (legal)README crates table lists every workspace crate (32 crates)README crates table lists every workspace crate (32 crates)All 27 fixtures were re-run under
gawk,gawk --posixandgawk --traditionalwith identicalexit codes; see the correction to the awk claim below for what that evidence is and is not worth.
No Markdown-rendering dev dependency was added: a renderer in the path of a documentation lint is
disproportionate, and it would put a renderer's version between a contributor and a README.
What the guard now asserts, exactly: every workspace crate has a row; every row names a real
crate; no crate twice; each crate row is three cells whose Purpose and Status render as something;
a row inside an HTML comment or a fence is not a row; and the first crate row sits directly
under a delimiter that sits directly under a header of the same width, neither indented four
spaces or more.
What it does not, and the one bypass that remains: a blank line inserted between two crate
rows splits the table in two — every row below the blank renders as a paragraph while membership,
shape and the head checks all still pass, because only the first crate row's context is judged.
Closing it means asserting that the crate rows are contiguous, which would reject a legal table
interleaving a non-crate row (a
| **Codecs** | | |separator), so it is written into the scriptheader, the
mise.tomlcomment and this body instead of being covered by the word "table". Therow pattern also stays narrow on purpose: five legal renderings (an omitted leading or trailing
pipe, an indented row, a linked crate cell, prose after the code span) are still reported as a
missing crate — the message now names the recognised form so the report is readable, but the
pattern is not widened, because every widening is a door a phantom row walks through.
Corrections to figures published in earlier rounds
Three numbers in this body do not reproduce. Each is corrected here and at the point where it was
written; none of them changes a conclusion, and that is not a reason to leave them.
reproduces as 18 tokens across 12 rows. The rows carrying a backticked token are
gamut,gamut-core,gamut-color,gamut-tonemap,gamut-codec-abi,gamut-av1,gamut-avif,gamut-webp,gamut-ifd,gamut-tiff,gamut-dngandgamut-cli. Twelve.repr(C), in thegamut-codec-abirow, is inthe extraction and missing from the published table. It resolves correctly —
crates/gamut-codec-abi/src/lib.rs:1is "onerepr(C)vtable shape", and therepr(C)vtables are
DecoderVTable/EncoderVTablein that crate — so the verdict is unchanged and thetable was incomplete, not wrong.
awk,gawk --posixandgawk --traditional" are three modes of oneinterpreter.
/usr/bin/awkon the machine this branch is written on is GNU Awk 5.3.2;command -v mawkfinds nothing. mawk is what the automation runs, so the CI step is the wholeof the portability evidence and the local sweep is a self-consistency check. Rounds 3 and 4
both said "only gawk is installed here" in their unresolved notes and then described the sweep
in words that implied otherwise; this round says it in one place.
Round-5 validation
No workspace build was run and none is expected: the round-5 diff is Markdown, one shell
script and one TOML comment and description.
mise run lintandmise run testwere not runand are not claimed.
.github/workflows/ci.ymlwas not touched.mise run check-readme-cratesREADME crates table lists every workspace crate (32 crates)__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkFinished in 4.81s__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-tooling-checkFinished in 1.53smise run check-commitsno errors in 13 commitsbash -n tooling/check-readme-crates/run.sh100755run.shagainst 27 fixtures, before and after, under three gawk modesRemainders filed in round 5
gamut-color/src/lib.rs:1-2("pixel formats", a
gamut-coretype) andgamut-cli/src/main.rs:3-4(the decode path). Thefirst is the source of the D1 row this PR fixes.
Unresolved review notes (round 5)
misattributed constructs passed every membership, shape and table-head check and were found by
enumerating the cells against the tree. A green
check-readme-cratesmeans "every crate has awell-formed row under a real table head", never "the table is true".
See the G1 section for why closing it would reject legal input.
pattern stays narrow deliberately; only the diagnostic changed.
every code point has a decimal and a hex spelling. The script says so.
CI step is the portability evidence.
README Releases section lists five already-published crates as awaiting their first publish #527 (§Releases, fixed in round 4), README: the Usage snippet pins
gamut = "0.1", a version that was never published #523 (superseded by README: the Usage snippet pinsgamut = "0.1", three months behind the current release (supersedes #523) #559), and CI: runmise run check-readme-cratesin the Format & Metadata job #524 (fixed inround 3 and carried by
Closesabove). A human may close README Releases section lists five already-published crates as awaiting their first publish #527 and README: the Usage snippet pinsgamut = "0.1", a version that was never published #523 after reading the diff;the lane never closes an issue.
correction above.
README.mdconflict with PR feat(png): measure the encoder end to end, then move what it exposed #485 is unchanged and still disclosed.unbackticked construct name. Those files move, and nothing detects a row that goes stale
afterwards.
Round 5, continued — the same defect one level up
Verifying the new
gamut-deflaterow raised the crate's dependency edge, andAGENTS.md's edge list — the repository's own map of the workspace graph, whichCLAUDE.mdsymlinks to — turned out to disagree with
cargo metadataon nine crates:AGENTS.mdcargo metadata --no-depsgamut-deflategamut-dspgamut-isobmff,gamut-riffgamut-jpeg,gamut-heic,gamut-pnggamut-codec-abigamut-webpgamut-tiffgamut-dngOnly the
gamut-deflateedge is corrected in this PR, because this PR's own new row contradictsit and shipping the contradiction is not an option. The other eight are #595, with the
measured table and the one-line
jqthat produces it — and with the observation that this is theidentical failure mode one level up: a hand-maintained summary of the workspace that nothing
reads, which is what
mise run check-readme-cratesnow exists to prevent for the table below it.Decision 32.
Also corrected in passing, before publishing: this round's first draft of the
AGENTS.mdlinecalled
gamut-deflate"the only shipped crate with no dependency at all".gamut-dspandgamut-codec-abiare equally dependency-free, so the claim was false and was removed rather thanpublished — the third time this branch has caught a count of its own before it shipped, which is
decision 29's argument restated.
Additional validation for this commit:
mise run check-readme-cratespass, exit 0;__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkpass(
Finished in 2.16s);mise run check-commitspass —no errors in 14 commits. No workspacebuild;
AGENTS.mdis prose and carries no code.Remainder filed: #595 —
AGENTS.md's dependency-edge list disagrees withcargo metadataon nine crates.
Round 6 — a fifth independent adversarial review of the pushed branch
Every one of round 5's findings was re-derived against the tree before anything was changed; two
of them were executed falsifiers and reproduce exactly. Nothing in round 5's review failed to
reproduce.
gamut-deflaterow repaired in round 5 ships a false enumeration in both halvescargo metadatamakesminiz_oxidea normal dependency ofgamut-png,gamut-dngandgamut-tiff, andgamut-deflate's own consumers are the same three. The round resolved the phrase the diff removed, not the phrase it shippedconsumed by \gamut-dng`, `gamut-png` and `gamut-tiff``gamut-cmmcredits epic #323 with a phase #323 excludesSTATUS.md:34gives P8 as #372, which is parentlessgamutgloss accounts for 12 of 14 shared layerscoreandtonemapfall outside every groupgamut-clidescribes 4 of 8 subcommandscrates/gamut-cli/src/main.rs:62declaresConvert,Inspect,Icc,Isobmff,Av1,Color,Dsp,Bitstreamgamut-ffi ← gamut-codec-abigamut-core's bullet is falsified by the commit that shares its subjectgamut-codec-abi,gamut-deflateandgamut-jxl-sysall say "← nothing" in the same list‎/‏pass though‎/‏foldThe enumeration decision 3 asked for: every machine-derivable claim left in the table
Derived from one command, which needs no build and which the Format & Metadata job already runs
for
check-release-deps(.github/workflows/ci.yml:62):The rule taken: anything in a cell that
cargo metadatacan settle is written in a form theguard checks, or it is deleted — a list, a count, a version, a feature name or a crate name
alike. A claim whose authority is the crate's source or an issue tracker rather than its
manifest stays prose, because no
cargo metadataquery decides it and the guard header has heldcell wording out of gate since decision 3. The sweep that produces the candidates, rather than a
reading of the table:
96 distinct tokens over 32 rows, and the table below accounts for all 96: 32 crate names, 11
Rust items, 8
gamut-clisubcommands, 3 feature names,miniz_oxide, 3 version tokens and 39numerals —
convertbeing both agamut-coremodule and a CLI subcommand, so counted once.gamut-riff's manifest is ahead …gamut-cmmhas no release yet"gamutgamutgamutgamut::coreis unconditional"with one always-on dependency: \gamut-core``gamut-codec-abiconsumed by… 8 crates, backticked so the guard can read themgamut-deflate(Purpose)gamut-deflate(Status)gamut-pngandgamut-dnguseminiz_oxide"consumed bylist of threegamut-deflate(Status)no always-on dependencies of its ownvN/vN.Mtoken —v2,v0.2,v1…Cargo.toml, to the precision writtengamut-tonemapgamut-cmmgamut-codec-abicodec-abifeature"behind feature \codec-abi`, resolved againstgamut`, which the row namesgamut-av1decodefeature"default feature \decode`, against gamut-av1'sdefault` listgamut-ifdbigtiff"behind feature \bigtiff``gamut-prefixed name in a Purpose or Status cell (47 in the table, 32 of them the crate cells the phantom check already read)gamut-deflateminiz_oxide"consumed byset, andminiz_oxide's own non-dev consumers derive as exactlygamut-dng, gamut-png, gamut-tiffgamut-clicargo metadataquery decides itgamut-jxl-syscrates/gamut-jxl-sys/tests/version.rsreferences/is their authority#Nissue referencesEncodeImage,DecodeImage,ToneCurve,Av1StillDecoder,convert,ycbcr,matrix,linalg,backend,f64,repr(C)gamut-heic/gamut-jpeg/gamut-cmmSTATUS.mdSixteen version tokens are extracted and all sixteen agree with their manifests:
L4, derived rather than counted
AGENTS.md's edge list against the command above, crate by crate. Eleven disagree; round 5published nine over a table naming ten.
AGENTS.mdcargo metadatagamut-deflategamut-dspgamut-isobmffgamut-riffgamut-jpeggamut-codec-abigamut-heicgamut-codec-abigamut-pnggamut-codec-abigamut-webpgamut-tiffgamut-deflategamut-dnggamut-deflate,gamut-jxlgamut-ffigamut-codec-abiTen are #595's; the eleventh,
gamut-ffi, is named nowhere in it and is filed as #607.Nothing in this run edits, comments on or relabels #595.
The guard's behaviour on every fixture, before and after
Fifty-eight fixtures, each a single edit to this branch's own README. Everything that already
worked still works; everything the review constructed now fails; every legal rendering it named
now passes.
## Crates ##(closing sequence)Cratesover dashes)‎/‎/ / /‎-only cell## Crates#(a heading whose text isCrates#) ,‎, NBSP, tab,<span></span>Status cell| **Codecs** | | |inside the tablev3forgamut-core;v0.3forgamut-bitstream)consumed bylist short one crate / with a crate that does not consumealways-onnaming an optional edge / claiming an edge that does not existcodec-abii)default feature(bigtiff)default featureowned by a crate the row does not nametest-support) / a feature of another crate the row names (avifongamut)gamut-ifdd)Portability
The awk is POSIX-only (no
gensub, no interval expressions, no dynamic-regexp extensions). Thewhole fifty-eight-fixture battery was re-run under
gawk --posixand undergawk --traditionaland passes 58/58 in each. As round 5's correction established, those are modes of one interpreter
and the portability evidence is the CI step, not the local sweep:
command -v mawkstill findsnothing on this machine.
Round-6 validation
mise run check-readme-cratesgawk --posixgawk --traditional__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkFinished in 6.30smise run check-testsmise run check-release-depsmise run check-ffi-featuresconvco check origin/master..HEADmise run lintLINT_EXIT=0, whole workspace, at2f4d7b1amise run testTEST_EXIT=0, whole workspace, at2f4d7b1aThe diff of this round is
AGENTS.md,README.md,mise.tomlandtooling/check-readme-crates/run.sh— no Rust source, no manifest, noCargo.lock, no buildscript.
mise run mutants --diffhas nothing to mutate over such a diff.mise run lintandmise run testwere run over the whole workspace at2f4d7b1a; the two commits after it(decision 41) change only the guard script,
mise.tomland three README cells, so no Rustinput to either gate moved. The four required CI checks are green at that same tree and are
re-run against the head.
Remainder filed in round 6
#607 —
AGENTS.mdmissesgamut-ffi'sgamut-codec-abiedge (the eleventh crate, not in#595's list).
Unresolved review notes (round 6)
A crate row moved into a
###sub-table still satisfies membership. A###heading doesnot end the section, deliberately, so a row demoted into a sub-table under it is still counted.
Whether a sub-table is a different table from the main one is now also checked — the rows
would be "spread over 2 separate tables" and fail — but a section that is entirely one
sub-table would pass. Outside this round's manifest; left as a review concern.
Two stale organisation URLs (
crates/gamut-jxl/STATUS.md:3,crates/gamut-riff/CHANGELOG.md:15) are outside this manifest and are not touched here. Thechangelog is release-plz's to own in any case.
An
<img>-only cell still reads as empty. Deliberate, and now stated once with its reasonin the script header rather than left as a surprise.
The
consumed by,always-onandfeatureparsers read prose. They stop at the firstcharacter that is not a backticked name, a separator or the word "and", which is documented,
but a sentence that puts a name immediately after the marker in some other role would be read
as part of the list. Narrow and documented rather than general.
Row text is accurate as of this commit against each crate's source and manifest. The
manifest-derived half is now guarded; the source-derived half is not, and those files move.
The forecast
README.mdconflict with PR feat(png): measure the encoder end to end, then move what it exposed #485 is unchanged and still disclosed.AGENTS.md's edge list still disagrees withcargo metadataon ten crates (AGENTS.md's dependency-edge list disagrees with cargo metadata on nine crates #595) plus theone filed here (AGENTS.md misses gamut-ffi's gamut-codec-abi edge (the eleventh crate, not in #595's list) #607). This round corrected only the sentence this branch itself falsified.
The feature check resolves an owner, it does not pin one. A name is accepted if any
workspace crate the row talks about declares it — the row's own crate plus any it cites —
because the
gamut-codec-abirow legitimately names the umbrella's feature. A feature thatmoved between two crates named in the same cell would therefore still pass. Pinning the owner
would need a form that writes it out, which reads worse than the sentence it replaces.
miniz_oxideis the one cargo-decidable token in the table that nothing checks. The cratename check is
gamut-prefixed, because no rule separates an external crate name from a moduleor a type name in a code span, and widening the pattern is how a phantom gets in. Its claim
rides on the checked
consumed byset it qualifies, and both derive to the same three cratestoday, but that is a coincidence a future edit can break.
No human approved this round: this is an unattended run, and the record above is what a human
reads afterwards.
Round 7 — a sixth independent adversarial review of the pushed branch
Every finding was re-derived against the tree before anything changed, each by execution.
All six reproduce; nothing in the review failed to reproduce. The review had itself
re-derived round 6's census independently in another language — 141 occurrences, 96 distinct,
the partition closing member for member — and could neither add nor remove a token; that census
is not re-run here, because this round's answer is not another sweep.
The finding that decided the round is not an instance but the shape of the class, and it is the
reviewer's:
So this round does not sweep. It makes the table forbid what the guard cannot check.
gamutrow ships a false claim this branch introduced, replacing a true onepub use gamut_core as corecarries nocfgandgamutdeclares nocorefeature; of 25 siblings, 15 sit behind their own feature, 9 behind a shared one, 1 behind nonebuilt on gamut-ifddandover gamut-iccc profilesboth exit 0gamut-/gamut_compound in the section is now a failuremise.tomlcomment named three as welldescription; the root manifest says why in a commentfeaturesas an English verb, and the underscore formtooling/check-readme-crates/fixtures.sh, 69 fixturesThe corrected
gamutcell, with its derivation$ grep -n 'pub use\|cfg(feature' crates/gamut/src/lib.rs # 25 re-exports, one uncfg'dprimitivesmetadatapub use gamut_core as core, nocfg, andgamutdeclares nocorefeature15 + 9 + 1 = 25. The cell now reads "most sibling crates it re-exports sit behind a Cargo feature
of their own …and the rest behind one of the two shared Cargo features
primitivesandmetadata…the exception is the core re-export, unconditional because it is the umbrella's onealways-on dependency:
gamut-core". Two of its three claims are machine-checked: corruptingeither shared feature name fails the guard, and so does changing the always-on edge.
README.md's closing paragraph, L3's false sentence, is likewise derived rather than asserted:four keys are per-crate (
name,version,description,readme, pluslinksongamut-jxl-sys) and eight inherit via.workspace = truein all 32 manifests. The same sentenceexists in
AGENTS.md, which this round's manifest does not reach — filed as #621.The fixtures now committed
tooling/check-readme-crates/fixtures.sh, run bymise run check-readme-crates-fixtures. 69fixtures in five groups, counted from the file rather than by hand: membership and row shape
(24), table structure (13), the section heading (6), the claim forms (17), and the section's
precondition and its prose (9). Seventeen of the 69 expect exit 0 — a legal rendering the guard
must not reject. Each asserts an exit code and, where the reason matters, a fragment of the
message, so a fixture that starts failing for a different reason is caught rather than counted as
a pass.
Its own falsifiability was executed rather than assumed: reverting the round's feature-marker
change turns exactly two fixtures red — the off-by-default fixture (now passing when it must
fail) and the English-verb fixture (now failing when it must pass) — and the battery reports
67 passed, 2 failed.New in this round, beyond round 6's set: an unbackticked crate name in a cell and in section
prose; the bare word
gamutin prose and agamut_pnginside a fence, both legal; theunderscore spelling of a real crate;
featuresas an English verb, legal; a corrupted externaldependency and one no crate in the row has; and five fixtures for prose outside a row — a phantom
crate name, a refuted feature claim, and the three subject-bearing forms written where they have
no subject.
Round-7 validation
mise run check-readme-crates./tooling/check-readme-crates/fixtures.sh(system awk, GNU Awk 5.3.2)CHECK_README_AWK='gawk --posix' ./tooling/check-readme-crates/fixtures.shCHECK_README_AWK='gawk --traditional' ./tooling/check-readme-crates/fixtures.sh__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkFinished in 1.71smise run check-testsconvco check origin/master..HEADbash -non both scriptsmawkandbusybox awkare not installed on this machine, so "every awk mode available" is thethree above; the portability evidence remains the CI step, as round 5 established. No Rust source,
no manifest and no
Cargo.lockchanged in this round — the diff isREADME.md,mise.tomlandthe two scripts under
tooling/— so no mutation run was performed, and none is claimed.mise run lintandmise run testwere last run over the whole workspace at2f4d7b1a(round 6)and no Rust input to either has moved since.
Remainder filed in round 7
#621 —
AGENTS.mdsays onlyversionis per-crate; four keys are. The same sentence thisround corrected in
README.md, one file over, outside this round's manifest.Unresolved review notes (round 7)
accepted if any workspace crate the text talks about declares or depends on it. A feature that
moved between two crates named in the same cell would still pass. Carried forward from round 6
note 8, now covering the external-dependency form as well, for the same reason: a form that
writes the owner out reads worse than the sentence it replaces.
gamut. It is English in this README andthe umbrella's package name at once, so
consumed by gamut, ...with the umbrella unbacktickedwould still bypass the cite check. Documented in the script header rather than closed; closing
it means rejecting the word "gamut" in the prose of a file about gamut.
deliberate over-rejection in one direction: a sentence in the section prose that legitimately
wants to write a
vNtoken, or the words "consumed by", must move into a row or drop themarker. No such sentence exists today.
###sub-table still satisfies membership, unchanged from round 6:a section that is entirely one sub-table would pass. Outside this round's manifest.
manifest-derived half is guarded; the source-derived half is not, and those files move.
AGENTS.md's edge list still disagrees withcargo metadata(AGENTS.md's dependency-edge list disagrees with cargo metadata on nine crates #595, AGENTS.md misses gamut-ffi's gamut-codec-abi edge (the eleventh crate, not in #595's list) #607), and itsversioning sentence is now AGENTS.md says only
versionis per-crate; four keys are #621. This branch corrected only the lines it falsified itself.README.mdconflict with PR feat(png): measure the encoder end to end, then move what it exposed #485 is unchanged and still disclosed.cargo metadataper fixture; what CI must protect isthe README, and
check-readme-cratesgates that in the Format & Metadata job.No human approved this round: this is an unattended run, and the record above is what a human
reads afterwards.
Round 7, continued — the section's last unread line, found in self-review
Reviewing this round's own diff before closing it found the gap decision 49 records: the claim
forms reached every line of the
## Cratessection except a heading inside it. It is fixed atb33c7bc6, with two fixtures. No published claim about the README's contents changes; two countspublished above do.
Correction to the round-7 fixture count. The section "The fixtures now committed" above says:
That was true of
f9d2947d. Atb33c7bc6the battery is 71 fixtures — the fifth group is11, not 9, the two additions being a phantom crate name and a refuted feature claim written in
a
###sub-heading. The other four group counts and the seventeen legal-rendering fixtures areunchanged. The round-7 validation table's three
69 passed, 0 failedrows likewise read 71passed, 0 failed at this head; the
67 passed, 2 failedregression demonstration was taken atf9d2947dand is not re-run, so it stands as recorded, against 69.b33c7bc6)mise run check-readme-crates./tooling/check-readme-crates/fixtures.sh(system awk)CHECK_README_AWK='gawk --posix' ./tooling/check-readme-crates/fixtures.shCHECK_README_AWK='gawk --traditional' ./tooling/check-readme-crates/fixtures.shbash -non both scriptsconvco check origin/master..HEADNothing else in this round's record is changed: the entries above are left exactly as they were
written, and this is the correction that names them.
Round 7, continued — two claims the guard made about itself
Re-deriving the script header against the history and the code it documents, after the fixture
commit, found two more hand-counted claims in it — both about the guard, neither about the README.
They are corrected at
9c35d7bcand recorded as decision 50. The sentence "this header shipped astale one for two rounds" was wrong by one round; the exemption bullet named a fenced code block
where an indented one is exempt too. Neither changes what the guard does: the battery is
71 passed, 0 failed at
9c35d7bcunder the system awk and undergawk --posixand--traditional,mise run check-readme-cratesexits 0, andconvco check origin/master..HEADreports no errors in 25 commits.
That is the last change of this round. The required checks are observed on
9c35d7bc.