Skip to content

fix: close the ungated idealized-palette hole and the remaining audit findings#65

Merged
g4bri3lDev merged 4 commits into
mainfrom
fix/palette-gate-coverage
Jul 22, 2026
Merged

fix: close the ungated idealized-palette hole and the remaining audit findings#65
g4bri3lDev merged 4 commits into
mainfrom
fix/palette-gate-coverage

Conversation

@g4bri3lDev

Copy link
Copy Markdown
Member

Follow-up to the 7-PR audit remediation (6970ecc), closing the six findings from the whole-set review. No dithered output changes: the three visual-regression fixture tests pass unchanged and no ColorScheme value, palette order, or RGB triple was modified.

1. CRITICAL — idealized per-scheme palettes were completely ungated

PALETTES in packages/javascript/src/palettes.ts was hand-written. In the ColorScheme path, ditherImage() gets palette indices from WASM (computed against Rust's PALETTE_* statics) but returns the colors from that TS object, where key order is the palette index — and nothing tied the two together. The reviewer proved it: swapping yellow and red in PALETTES[BWRY] left vitest 48/48 green, check_enum_parity.py green (it parses enum names and integers only), and gen_ts_palettes --check green (it covered only the measured palettes). A JS consumer would get index 2, read result.palette[2], and render/encode red where the hardware puts yellow.

gen_ts_palettes.rs now also emits SCHEME_PALETTES from ColorScheme::palette() + ColorScheme::color_names(), so the existing --check CI gate covers the idealized palettes too. palettes.ts narrows the generated record instead of duplicating it.

  • Public API unchanged: getPalette, getColorCount, fromValue, ColorPalette, and every import path.
  • Zero data drift: the generated record was diffed against the deleted hand-written one on key order, RGB values and accent — identical for all 9 schemes.
  • Generator stays deterministic (fixed SCHEMES array + CATALOG order, no timestamps, no absolute paths); two consecutive --stdout runs are byte-identical. assert_schemes_complete() aborts generation if a new enum variant is added but forgotten in SCHEMES.
  • New vitest table pins key order and literal RGB for all nine schemes, hand-written rather than derived from the code under test. Previously only SEVEN_COLOR and BWGBRY_SPLIT had key order pinned and RGB was only range-checked 0 <= c <= 255.

Acceptance test — repeating the reviewer's swap at the new source of truth (PALETTE_BWRY in palettes.rs) now fails three ways:

gate result
cargo testcolor_names_match_expected_rgb_values FAILED: Bwry: palette().colors[2] (yellow) RGB mismatch, left: [255, 0, 0]
gen_ts_palettes -- --check exit 1, first differing line: 56 / committed: yellow: { r: 255, g: 255, b: 0 } / generated: yellow: { r: 255, g: 0, b: 0 }
vitest (after regenerating, so the swap reaches TS) FAILED: ColorScheme > scheme 3 has exact palette key order and RGB values

2. IMPORTANT — measured RGB triples were not tied to their color names

measured_palettes_follow_canonical_color_order compared names, lengths and an accent_idx bound, but never read entry.palette.colors. Swapping the yellow and red rows inside BWRY_3_97 passed, then propagated to Python (which derives from CATALOG via FFI) and to the generated TypeScript.

New measured_colors_match_their_names asserts a loose hue sanity check per color name over every CATALOG entry, and accent_idx is now matched exactly against the scheme's canonical accent rather than only bounds-checked. Thresholds are swap detectors, not calibration pins — these are photographed values (measured yellow is [172, 128, 0], measured white [168, 180, 182]), so a re-measurement should never need to touch them. An unrecognised color name panics.

Proof: swapping yellow/red in BWRY_3_97BWRY_3_97: yellow = [85, 24, 14]: yellow must have R and G high.

3. IMPORTANT — stale GRAYSCALE_8 shipping to npm

packages/javascript/README.md advertised 8 Color Schemes: ... GRAYSCALE\_4/8/16. GRAYSCALE_8 was removed in protocol v2, the count is 9, and SEVEN_COLOR/BWGBRY_SPLIT were missing. Now matches the root README.

4. IMPORTANT — both READMEs documented the BWRY ink order backwards

packages/javascript/README.md said BWRY = 3, // Black, White, Red, Yellow and packages/python/README.md said Black, white, red, yellow. The wire order everywhere in code is black, white, yellow, red (PALETTE_BWRY; firmware BBEP_YELLOW=2, BBEP_RED=3). Both fixed. Every other documented ink order in both READMEs was checked against the palette definitions and is correct.

5. MINOR — error type clarification

One-line doc note on DitherError stating it is not the crate-wide error type. composite::InvalidRgbaLength is deliberately left separate, per the reviewer's explicit recommendation.

6. TRIVIAL — BWGBRY_SPLIT missing from the demos

Added to the scheme picker and SCHEMES map in docs/index.html, packages/javascript/demo.html and packages/javascript/dev.html.

Verification

cargo test --workspace                 65 passed + 3 regression fixtures passed (unchanged)
cargo clippy --workspace -- -D warnings clean
gen_ts_palettes -- --check             OK (9 schemes, 8 measured palettes); 2 runs byte-identical
check_enum_parity.py                   ENUMS CONSISTENT ACROSS LANGUAGES
check_enum_parity.py --header ...      CONSISTENT ACROSS LANGUAGES AND WITH THE CANONICAL HEADER
bun run type-check / test / build      clean / 58 passed / 3 builds OK
pytest                                 99 passed

WASM was rebuilt before the JS runs.

… of truth

The `PALETTES` record in packages/javascript/src/palettes.ts was hand-written and
completely ungated. In the ColorScheme path, ditherImage() gets palette *indices*
from WASM (computed against Rust's PALETTE_* statics) but returns the *colors* from
that TS object, where key order IS the palette index -- and nothing tied the two
together. Swapping yellow and red in PALETTES[BWRY] left vitest, check_enum_parity.py
and gen_ts_palettes --check all green while shipping red where the hardware puts
yellow.

Commit c53441a established "generate TypeScript from the Rust source of truth" for
the eight measured palettes; this extends the same generator to the nine idealized
ones, emitting SCHEME_PALETTES from ColorScheme::palette() + color_names() so the
existing --check CI gate covers both. palettes.ts now narrows the generated record
instead of duplicating it; getPalette/getColorCount/fromValue/ColorPalette and every
import path are unchanged, and the generated values are byte-equivalent in data
(key order, RGB, accent) to the hand-written table they replace.

Also adds a vitest table pinning key order and literal RGB values for all nine
schemes, written by hand rather than derived from the code under test -- the existing
tests pinned key order for only two schemes and checked RGB only for 0 <= c <= 255,
so a typo passed everywhere.
measured_palettes_follow_canonical_color_order compared only names, lengths and an
accent_idx bound -- it never read entry.palette.colors. Swapping the yellow and red
rows inside BWRY_3_97 passed it, then propagated to Python (which derives from
CATALOG via FFI) and to the generated TypeScript: wrong inks on a real 3.97" panel.

Adds a per-name hue assertion over every CATALOG entry, plus an exact accent_idx
match against the scheme's canonical palette (previously only bounds-checked).
Thresholds are deliberately loose because these are photographed values, not pure
sRGB: they detect a misplaced row, they do not pin calibration. An unrecognised
color name panics so a new ink cannot slip through unchecked.
It covers unknown-discriminant conversions only; composite::InvalidRgbaLength is a
deliberately separate typed struct carrying the offending len.
- Both READMEs documented BWRY as "black, white, red, yellow". The wire order
  everywhere in code is black, white, YELLOW, red (PALETTE_BWRY; firmware
  BBEP_YELLOW=2, BBEP_RED=3). Since the same READMEs get BWGBRY right, the BWRY
  line read as authoritative rather than as a typo -- and py-opendisplay still
  needs encode_image branches written for these schemes.
- packages/javascript/README.md advertised "8 Color Schemes ... GRAYSCALE_4/8/16":
  GRAYSCALE_8 no longer exists, the count is 9, and SEVEN_COLOR and BWGBRY_SPLIT
  were missing. Now matches the root README.
- The demo scheme pickers (docs/index.html, demo.html, dev.html) listed eight
  schemes and omitted BWGBRY_SPLIT.

Every other documented ink order in both READMEs was checked against the palette
definitions and is correct.
@g4bri3lDev
g4bri3lDev merged commit 496858d into main Jul 22, 2026
8 checks passed
@g4bri3lDev
g4bri3lDev deleted the fix/palette-gate-coverage branch July 22, 2026 11:11
@github-actions github-actions Bot mentioned this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant