Skip to content

fix(color): one canonical color rendering everywhere (#30) - #36

Merged
steveclarke merged 2 commits into
masterfrom
30-color-normalize
Jul 25, 2026
Merged

steveclarke merged 2 commits into
masterfrom
30-color-normalize

Conversation

@steveclarke

Copy link
Copy Markdown
Owner

Closes #30.

Root cause

The two readers disagreed because a color exists in three shapes and nothing folded them together:

Source Stored as
ZB/CK snapshot (a full read) ABGR-packed integer — 4288910991
Live PC delta raw RGBA bytes — 8f 96 a3 ff
Config / set argument hex string — 8f96a3

Client.Get routed KindChars through a humanizer; Client.Snapshot returned the tree verbatim. verify reads Snapshot, so it compared the config's "8f96a3ff" against the snapshot's 4288910991 and drifted on every color, forever.

Fix

Fix it at the source rather than at the comparison. New internal/color is the single codec — every form folds to 8 lowercase RGBA hex digits — and the client canonicalizes chars values on the way into the state tree, so Get, GetRaw, Snapshot, dump, and verify all see the same string.

  • internal/color — Canonical (any form → RGBA hex) and Parse (hex → wire bytes), replacing four separate ad-hoc parsers (client.humanizeColor, client.toCharsVal, helpers.SetColor, cli.parseColor).
  • client — canonicalize PC deltas and ZB/CK snapshots at ingest.
  • boardconfig — Compile pushes canonical hex for both wire and human values, Diff compares canonical forms for KindChars, and ToConfig now emits color so dump --as-config → verify round-trips.
  • diff — compare []byte bytewise. Two byte payloads reaching == panicked (comparing uncomparable type []uint8); a live PC delta landing mid-session was enough to hit it. Found while writing the regression tests.
  • Canonical rendering documented in README, PROTOCOL.md, and the ucmix skill.

Contract changes a reviewer should see

  • dump --raw --json changed the JSON type for colors: "line/ch19/color": 4288910991 → "line/ch19/color": "8f96a3ff". Number to string.
  • --raw no longer means "unnormalized" for color. Colors normalize at ingest, so raw and humanized reads agree. Flag help updated on both get and dump.
  • dump --as-config now emits a color: line for every channel, including 00000000 for uncolored ones. Writing 00000000 to an already-uncolored channel is value-identical (confirmed on the real board), but a dumped config no longer has "absent color line = leave alone" semantics.

Regression coverage

All fail on master:

  • TestDiffColorAcrossRepresentations — packed int / int / float / RGBA bytes / canonical hex / 6-digit hex / uppercase all Diff clean against one config; a different color still drifts, with both sides of the row printed canonically.
  • TestDiffColorDoesNotPanicOnByteSnapshot — pins the == panic.
  • TestToConfigEmitsCanonicalColor and TestToConfigColorSurvivesYAML — the dump --as-config → verify round-trip, including an all-numeric color YAML would otherwise read back as an integer.
  • TestColorIsCanonicalFromSnapshot / ...FromDelta — Get, GetRaw, and Snapshot agree, from both ingest paths.
  • Four e2e cases in test/apply_verify.bats, plus a color added to the board-as-code.yml fixture so the existing apply/verify/dump round-trip covers it.

408 unit (was 373), 76 e2e (was 72). just test, test-race, lint, vet, fmt-check, test-e2e all green.

Confirmed on the real 32R

The fake was telling the truth. On the live board:

  • get line/ch19/color and get line/ch19/color --raw both render 8f96a3ff (master: 4288910991 on --raw).
  • All four spellings — 8f96a3, 8f96a3ff, #8f96a3, 8F96A3FF — verify clean against the real color.
  • ff0000 still drifts, exit 1, printed as want ff0000ff got 8f96a3ff.
  • dump --as-config → verify: clean, 2089 settings, including all 32 channel colors.
  • One targeted write on ch32 (uncolored): wrote 8f96a3, read back 8f96a3ff, verified clean against a 6-digit config, restored to 00000000. No other board state touched — no apply, no reset.

https://claude.ai/code/session_01RGKiwtBjK7VU9xZHMZwMgo

A board reports a channel color two different ways — the ABGR-packed
integer in a ZB/CK snapshot, the raw RGBA bytes in a live PC delta — and
a config declares a third, hex. Nothing folded those together, so the two
readers disagreed: `get` printed 8f96a3ff while `verify` compared the
config's hex against the snapshot's 4288910991 and reported drift on
every color, forever.

Fix it at the source rather than at the comparison. internal/color is now
the single codec: every form folds to 8 lowercase RGBA hex digits, and
the client canonicalizes chars values on the way into the state tree, so
Get, GetRaw, Snapshot, dump and verify all see the same string.

- internal/color: Canonical (any form -> RGBA hex) and Parse (hex -> wire
  bytes), replacing four separate ad-hoc parsers.
- client: canonicalize PC deltas and ZB/CK snapshots at ingest.
- boardconfig: Compile pushes canonical hex for both wire and human, Diff
  compares canonical forms for KindChars, ToConfig emits color so
  `dump --as-config` -> `verify` round-trips clean on a colored board.
- diff: compare []byte bytewise; two byte payloads reaching == panicked.
- Document the canonical rendering in README, PROTOCOL, and the skill.

Regression coverage that fails on master: Diff across all four color
representations, the ToConfig/YAML round-trip, client Get/GetRaw/Snapshot
agreement from both a snapshot and a delta, and four e2e cases.

Closes #30

Claude-Session: https://claude.ai/code/session_01RGKiwtBjK7VU9xZHMZwMgo
Colors are canonicalized on the way into the state tree, so --raw prints
the same 8 hex digits as a humanized read. Say so in the flag help rather
than promising a raw wire value it no longer returns.

Claude-Session: https://claude.ai/code/session_01RGKiwtBjK7VU9xZHMZwMgo
@steveclarke
steveclarke merged commit af8fc1d into master Jul 25, 2026
5 checks passed
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.

verify always reports drift on color: board returns decimal, config holds hex

1 participant