feat(core): rule color/palette-conformance - #112
Conversation
Flags computed colors whose CIEDE2000 distance to every entry in `color.tokens` exceeds `color.delta_e_tolerance` (default 2.0). The palette is converted to CIE Lab once per check call. Translucent foregrounds are composited over the closest opaque ancestor `background-color` (defaulting to `#ffffff`) before the ΔE measurement so overlays are judged where they actually render. Properties iterated: `color`, `background-color`, the four `border-*-color` longhands, and `outline-color`. One violation per `(node, property)` pair. Adds a `parse_css_color` helper that handles the `rgb(...) / rgba(...) / #hex` shapes Chromium normalizes to. Closes #23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I have a complete picture. Here is the full review. Review:
|
…t sources When the source color carries `alpha < 1.0`, the rule's nearest-palette match runs against the COMPOSITED appearance (over the resolved opaque background), not the literal CSS value. Replacing the literal with the opaque token's hex therefore changes more than the rendered color. Branch the fix's `confidence` to `Low` in that case; opaque sources keep `Medium`. Also add unit tests for `parse_css_color` covering hex-3, hex-4, hex-8, percentage rgb, whitespace-separated rgb, rgba, the `transparent` keyword, and malformed input. Regenerate the golden snapshot — the translucent fixture now reports `"confidence": "low"`.
Brings in #112 (color/palette-conformance rule) and #114 (CSS custom-properties scraper). Resolves a trivial conflict in plumb-config/src/lib.rs (both branches added a new mod + pub use; combine them) and corrects the CHANGELOG entry to reflect the lowered MAX_NESTING (256 -> 64) from the prior review-feedback commit.
Brings the color/palette-conformance rule (#112) onto the MVP-bundle branch and corrects a stale 'rounded mean' inline comment in edge/near_alignment.rs that the prior review pass left untouched.
- docs/src/rules/sibling-height-consistency.md: align example fix
text with the committed snapshot ('Drift: 30px.').
- docs/src/rules/overview.md + docs/src/SUMMARY.md: list
color/palette-conformance now that PR #112 has merged, and trim the
stale 'Coming soon' paragraph that still listed all three landed
categories.
- crates/plumb-core/src/rules/edge/near_alignment.rs: drop the dead
'#[allow(clippy::too_many_arguments)]' (clippy.toml threshold is
12; the helper has 11 args), keep the rationale comment, and add a
one-line invariant note above the unreachable '> tolerance' guard.
* feat(core): rule radius/scale-conformance Add the radius/scale-conformance rule, mirroring spacing/scale-conformance but operating over the four physical-longhand corner-radius properties. Border-radius values not within 0.5px of a member of `radius.scale` fire a violation with a nearest-in-scale fix at confidence: medium. Fixes #24 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core): rule a11y/touch-target Add the a11y/touch-target rule, implementing WCAG 2.5.8 (Target Size, Minimum). Interactive nodes (button/select/textarea, anchor with href, button-shaped input, role="button") whose rendered Rect is below `a11y.touch_target.min_width_px` × `min_height_px` (default 24×24) fire a violation, with a description-shaped fix at confidence: low. Fixes #26 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core): rule sibling/height-consistency Add the sibling/height-consistency rule: groups DOM siblings by parent, clusters them into visual rows by top-edge proximity (±2 CSS px) and horizontal overlap (≥50% of the smaller width), then flags any element whose height drifts from its row's median by more than 4 CSS px. Falls back to a single DOM-sibling group when row clustering yields only singletons (e.g. absolute positioning, transforms). Default severity Info. Fixes #25 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core): rule edge/near-alignment Add the edge/near-alignment rule. For each parent group of siblings, the rule clusters left/right/top/bottom edges within `alignment.tolerance_px` (default 3 CSS px) and flags any element whose edge sits within `0 < delta <= tolerance_px` of its cluster's centroid. Pixel-perfect alignments (delta == 0) and clusters of size < 2 are skipped. Default severity Info. Fixes #27 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(core): address review feedback on MVP rule bundle - Switch private intra-doc links in `sibling/height_consistency.rs` module-level docs to inline code spans (fixes `cargo doc -Dwarnings` failure on `private_intra_doc_links`). - Replace "drift is small but visible" wording in violation fix text; the deviation can be 30+px which is not small. Use neutral "Drift: {dev}px." Also regenerate the golden snapshot. - Correct misleading test comment on the three-cards fixture in `golden_sibling_height_consistency.rs`; that fixture exercises the DOM-sibling fallback, not the primary clustering path. - Fix terminology in `edge/near_alignment.rs` and `docs/src/rules/edge-near-alignment.md`: the centroid is an integer truncated mean (`sum / len`), not a rounded mean. * docs(core): correct terminology and fixture comment for MVP rules - `edge/near_alignment` module doc + docs page: the cluster centroid is an integer mean (`sum / len`, truncating), not a rounded mean. - `golden_sibling_height_consistency` fixture comment: clarify that the three-cards row exercises the DOM-sibling fallback (the cards are spaced 220px apart with no rect overlap), and point readers to the dedicated unit test for the primary clustering path. * fix(core): document clippy::too_many_arguments rationale on emit_violation Per CLAUDE.md, every `#[allow(...)]` carries a one-line rationale. The `emit_violation` helper takes the loop's locals; grouping into a struct duplicates them without hiding complexity. * fix: address review nits on PR #113 - docs/src/rules/sibling-height-consistency.md: align example fix text with the committed snapshot ('Drift: 30px.'). - docs/src/rules/overview.md + docs/src/SUMMARY.md: list color/palette-conformance now that PR #112 has merged, and trim the stale 'Coming soon' paragraph that still listed all three landed categories. - crates/plumb-core/src/rules/edge/near_alignment.rs: drop the dead '#[allow(clippy::too_many_arguments)]' (clippy.toml threshold is 12; the helper has 11 args), keep the rationale comment, and add a one-line invariant note above the unreachable '> tolerance' guard. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Target branch
mainSpec
Fixes #23
Summary
color/palette-conformancebuilt-in rule. Flags computed colors whose CIEDE2000 distance to every entry incolor.tokensexceedscolor.delta_e_tolerance(default2.0).background-color(default#ffffff) in linear sRGB before the ΔE measurement, so overlays are judged where they actually render.checkcall (never per node) and cached asVec<PaletteEntry>.Crates touched
plumb-coreplumb-formatplumb-cdpplumb-configplumb-mcpplumb-clixtaskdocs/.agents/or.claude/.github/System impact
# Errorssection if fallible)tools/listentry + protocol test)register_builtinentry)cargo xtask schema+ commit result).agents/rules/determinism.md)The rule reuses
Config::color(tokens+delta_e_tolerance), which already shipped with PR #109; no schema change required.Architectural compliance
plumb-corehas no internal deps; unsafe only inplumb-cdp;println!/eprintln!only inplumb-cli.thiserror-derived in libs;anyhowonly inplumb-cli::main.unwrap/expect/panic!in library crates.SystemTime::now/Instant::nowinplumb-core.HashMapin observable output paths (useIndexMap).#[allow(...)]is local and has a one-line rationale.Test plan
just validatepasses locallycargo xtask pre-releasepasses (4 built-in rules; all have docs pages)just determinism-checkpasses (3× byte-diff clean)cargo deny checkpassespalette_conformance.rs::tests, golden snapshot intests/golden_color_palette_conformance.rsDocumentation
PaletteConformance)# Errorssection on every new public fallible fn (no new fallible fn)docs/src/updated when user-visible behavior changeddocs/src/rules/<category>-<id>.mdwritten for new rulescolor-palette-conformance.md)Breaking change?
Checklist
codex/<primary>-<type>-<slug>/gh-review --local-diff main...HEADrun locallyImplementation notes
ΔE00 implementation choice.
palette0.7 only implementsCiede2000forLab<D65, _>andLch<D65, _>; theOklabtype carriesEuclideanDistanceonly. Per the spec's "use what palette exposes" hint, I converted candidates and tokens throughLinSrgb → Xyz → Lab(D65)and usedCiede2000::differencefor ΔE00. Math runs onf32to keep the conversion path light.Backdrop fallback.
resolve_backdropwalksparent_dom_orderuntil it finds an ancestor withbackground-colorandalpha == 1.0. If the walk never finds one (or hits the root), the rule defaults toCssColor { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }— the User Agent default. The walk starts atstart.parent, never the start node, since the start node's own color is what's being judged.Compositing. Standard "source over" Porter–Duff in linear-light sRGB. The ancestor walk guarantees the destination has
alpha == 1.0, so the simplifiedout = src.a * src + (1 - src.a) * dstform is correct.🤖 Generated with Claude Code