Files
ARCADIA-Studio/harness/experiments/palette-readability-20260913/palette-readability-audit-20260912.md
T

71 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Palette readability audit and bounded candidate decision
Independent source audit, 2026-09-12. Reviewed production source at `d61f1f38779ab70a77efcde523ae31c432aefcfd`; root owns #26, integration, compiler gates and native acceptance. No production edits, build, UI interaction, or Gitea mutation performed. Local calculator was executed; neither proposed palette was rendered in the application.
## Requirement and benchmark
[User issue #26](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/26) asks for graphical elements to display clearly and sharply on monitors with limited color space. Current evidence establishes low source contrast and fine geometry; it does **not** establish a particular monitor gamut, calibration, or failure mechanism. Avoid presenting a saturated replacement color as a cure for an unmeasured display.
Use WCAG 2.2 as an explicit engineering benchmark for this native application, not an assertion that this audit establishes web conformance or creates a binding native-app requirement. Normal text should reach 4.5:1 against its actual background; large text has a 3:1 benchmark. These 10–14 px labels should use the normal-text target. Inactive controls and decoration have exceptions, but semantic helper text is not decoration. [W3C contrast explanation](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html)
Meaningful graphical boundaries and visual information identifying controls/states should reach 3:1 against adjacent colors; every decorative divider need not become dark. Assess both sides of an essential border where either side can merge visually. [W3C non-text contrast explanation](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)
Information must survive loss of hue: text, geometry, pattern, symbols or another visible cue should identify meaning. Simply replacing one hue with another does not address hue-dependent status identification. [W3C use-of-color explanation](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html)
## Actionable source findings
**P1 — Essential unselected geometry uses the decorative LINE token.** `style.rs:10` supplies `(0.86, 0.89, 0.91)`. Architecture node boundaries use it at `diagram.rs:1599`; scenario activation borders use it at `behavior.rs:2004`, and participant/frame boundaries at `behavior.rs:2022`. Against white it is **1.30:1**, against activation fill `#F3F8F8` **1.21:1**. The activation fill itself is only **1.07:1** against white. An unselected narrow execution cannot rely on either signal. Ordinary architecture edges `#909EAD` at `diagram.rs:1176` are **2.57:1** against their `#F6F8FB` canvas (`diagram.rs:1124`). Reproduction: open an unselected architecture node/ordinary link or a nested scenario and identify its boundary without selecting it. Split essential boundary/edge roles from dividers; changing LINE everywhere indiscriminately adds visual noise to all panels.
**P1 — Semantic secondary text is below the normal-text benchmark.** MUTED (`style.rs:8`) measures **4.34:1** on white, **3.92:1** on BG and **3.81:1** on state-machine fill. It paints Inspector labels/help (`forms/view_fields.rs:30,154,316`), scenario message labels/details (`behavior.rs:2048,2088,2114`), state kind/action labels (`state_canvas.rs:1480,1511`), and routing warnings (`diagram.rs:1462`). Reproduction: read action/detail text at 100% or identify a message at Fit. Darken the semantic-secondary role; do not achieve hierarchy through reduced opacity of necessary text.
**P2 — Category text has additional failures that a MUTED-only change misses.** `diagram.rs:1613` uses kind colors for 11 px headers on the six-percent tint computed at `diagram.rs:1591`. Current actor/tint is **4.48:1** (do not round it to a pass), generic kind/tint **4.42:1**, capability/mission amber/tint **3.43:1**. The other categories in the calculator reach 4.5. Keep category names and existing glyphs; darken the three failing accents or render their text in semantic ink while retaining a color swatch.
**P2 — Selected navigation and rows are too dependent on faint fill changes; warning-edge identification explicitly requires hue.** `style.rs:67` selected navigation uses `#DAEEEB` fill (**1.21:1** against white) and `#97CBC3` border (**1.80:1** against white, **1.49:1** against fill). State relation-row selection (`state_canvas.rs:1517,1533`) changes fill/text color without a selection marker. Architecture selection already increases stroke width; state nodes and scenario messages also have useful width changes. However warning links at `diagram.rs:1165` change to amber without a warning symbol; the fallback instruction at `diagram.rs:1448` literally asks to select an amber line. Add a compact visible selection marker for rows/navigation and a warning glyph/label on affected routes, then change that instruction to refer to the cue. Keep reply dashes, include/extend dashes, region dashes, node kind text, collapsed text and pseudostate shapes; they already carry non-hue information and must not be repurposed for focus.
**P2 — Inspector/editor colors bypass the five global constants.** `views.rs:1187` constructs the source editor with the default widget style, then installs `highlight::format`; form fields/pick lists similarly use Iced defaults. Installed `iced_widget-0.14.2/src/text_input.rs:1758`, `text_editor.rs:1466`, `pick_list.rs:904` derive backgrounds/borders/placeholders/selection from the extended theme. Border is `background.strong.color`, placeholder is `secondary.base.color`, selection is `primary.weak.color`; none is LINE or MUTED. `iced_core-0.14.0/src/theme/palette.rs:471,506,531` generates them with Oklch deviation and linear-light mixing. Do not substitute guessed hex values for these generated controls. Add small explicit widget-style adapters in existing `style.rs` for essential borders, readable placeholders, visible focus, and selection; test resolved styles in the actual current Iced version. Open/hovered/disabled/checked states and popup menus need review too.
**P2 — Source comments and text selection require independent coverage.** Hardcoded lexer colors ignore Theme at `highlight.rs:62`. Comment `#6F7984` is **4.00:1** on editor BG; other ordinary syntax colors reach 4.5. The generated selection fill, computed from Iced's exact linear mix, is approximately `#A0B8B6`. All seven current token colors calculate below 4.5 against this fill (**2.11–3.38:1**). Iced's editor draw (`text_editor.rs:1011,1022,1053`) supplies highlight text and the selection quad without a dedicated selected-text color. Treat this as a source-grounded render risk requiring a native highlighted-selection witness, not as a verified new native failure. The calculator does not model GPU draw batching. Use a lighter selection fill with a clear independent boundary/caret or deliberately adapt selected text; merely strengthening the primary button color may worsen this pair.
**Scale limitation — contrast cannot make arbitrarily tiny text readable.** Scenario frame scales geometry/text together (`behavior.rs:1964`); normal message stroke is 1.2 and activation stroke 1 (`behavior.rs:1981,2010,2063`). Native Fit at 66% therefore has approximately 0.79/0.66 logical-pixel strokes and 7.9 px message text before device scaling. Interactive scenario zoom permits 5% (`canvas_workspace.rs:158`), state zoom 15% (`app/state_diagram.rs:137`). At these extremes, palette acceptance must distinguish overview/orientation from detailed reading. Test that boundaries/selection remain traceable and that zooming in recovers labels; do not promise every detail readable at the lowest zoom. If the requirement means detail readability at every allowed scale, geometry/text presentation needs a separate explicit decision.
## Existing native evidence inspected
Archive: `.local/native-qa/canvas-followup-20260912-01/evidence/`, source `83f53c7c6e277e250f06d2ae6bfa6a7988bd2e4b`. Root is also archiving these under `harness/experiments/canvas-native-83f53c7-20260912/evidence/`.
- `arch-delete-saved-0.png`: at 150%, the large unselected entity has a faint enclosing edge, while its name and type stripe are substantially stronger. Inspector label/field-border hierarchy is visible but weak. This is direct evidence of the current presentation, not a calibrated display measurement.
- `state-selected-0.png`: at 103%, state-machine and region are distinguishable by solid/dashed dark boundaries even though their fill contrast is only 1.09. Selected state gains width and teal; action/help text remains small and pale. This supports retaining state shape/dash cues rather than needlessly darkening all region fills.
- `sa-nested-minimum-fit-0.png`: a 1100×700-class window shows the scenario at 66%, with slender unselected activation borders and small message labels. Selected dashed reply is much more visible. This demonstrates both color and scale pressure.
- `oa-outer-activation-selected-0.png` was inspected in the preceding paint audit. Its erased border segments belong to the already separate painter repair; do not attribute those missing pixels to gamut or claim a palette change repairs painter order.
No native83 textual-editor screenshot was located in this archive. Source-editor findings therefore remain source/numeric findings. No screenshot was recolored or edited.
## Candidate comparison and recommendation
Both candidates keep white/light surfaces and opaque semantic strokes. A dark inversion is not needed to test the present hypothesis and would require replacing many hardcoded white backplates/fills; it is not the minimal intervention.
| Criterion | A: stronger light palette, retain category accents | B: neutral content, one blue interaction accent |
|---|---|---|
| Representative tokens | ink `#202C3A`, secondary `#4B5968`, boundary `#667586`, accent `#00665C`, selection fill `#D9EEE8` | ink `#202020`, secondary `#494949`, boundary `#595959`, accent `#124E96`, selection fill `#E2ECFA` |
| Numeric margin | secondary/BG 6.49; boundary/white 4.72; boundary/state-machine fill 4.14; accent/white 6.87; accent/selection fill 5.68 | secondary/BG 8.14; boundary/white 7.00; boundary/state-machine fill 6.15; accent/white 8.22; accent/selection fill 6.89 |
| State distinctions | Retain familiar categories; selection/focus/warning require independent cues. Adjust actor `#754499`, generic `#326B80`, amber `#865B00`; all clear the text benchmark on their six-percent tint. | Make ordinary headers/edges neutral; rely on existing category words/shapes and new status markers. Blue identifies interaction only; warning/danger need words/symbols. |
| Cognitive load | Preserves current category scan aid. Risk of clutter if strong boundary is applied to every divider; separate roles prevents this. | Simplest luminance hierarchy and least reliance on category hue. Loses an existing category scan aid; higher stroke weight everywhere may dominate dense diagrams. |
| Migration cost | Small role split plus targeted hardcoded replacements and control adapters. Lexer keeps its working colors; comment darkens and selection fill is controlled. | Similar control adapters, but broader removal/reassignment of category colors and header/edge rendering conventions. More native review for changed visual grouping. |
**Recommend A as the first bounded candidate**, with B retained as the meaningful fallback if degraded/native task evaluation still shows ambiguity. This is a reversible design recommendation, not acceptance. A supplies contrast margin without discarding already useful category cues. Do not introduce a theme framework or new preference settings.
Minimal roles in existing `style.rs`: canvas/surface/background; primary text; secondary semantic text; decorative divider; essential boundary/edge; interaction accent; selection fill; warning/danger (reuse existing status roles); existing kind-color mapping. Selection and keyboard focus can share a color but must differ geometrically: selected object/row marker versus a spatially separate focus outline. A new color is not required for each widget, hover state or semantic kind. Keep opacity only where justified by layering (modal scrim) or inactive state, not for essential geometry.
The audit inventory must include the existing hardcoded modal `rgba(.08,.12,.18,.38)` (`views.rs:374`, `state_view.rs:229`, `hosted/view.rs:35`) and context-menu shadow `rgba(.1,.15,.2,.17)` (`context_menu.rs:682`). These are layering/decorative uses. Iced button disabled styling scales background/text alpha to 0.5 (`button.rs:748`), and text-button hovered styling uses text alpha 0.8 (`button.rs:684`; custom nav restores INK when enabled). Keep disabled meaning explicit through operability and visible state, and inspect composited colors on actual backing surfaces. Do not apply an opaque-source contrast ratio to an alpha-composited pixel.
## Minimal evaluator suite before accepting A
1. **Resolved-color contract:** execute sRGB checks on production style outputs, not only copied candidate hex values. Cover secondary text, every kind/header tint, all syntax classes on ordinary and selected editor backgrounds, field placeholder/value/selection, navigation, checkbox/pick-list states, essential canvas boundaries on both adjacent fills, warning/danger. Require 4.5 text/3 meaningful graphics as stated benchmark; retain a margin for small/thin strokes. Exclude decoration by named role, not broad file exemptions. Include a negative control asserting the old MUTED/BG, LINE/activation and ordinary edge/canvas pairs fail.
2. **Rendered witnesses:** real WGPU output of the same OA/SA nested scenario, architecture edges/ports, state region/actions/pseudostates, Inspector inputs/open pick list, and source tokens with selected text. Inspect ordinary, selected, keyboard-focused, hovered, disabled, warning/error and modal states. Sample interior color and continuous border runs; check masks/order do not remove geometry. Existing painter continuity evaluator remains necessary and must be adapted if its selected-color predicate is hardcoded.
3. **Native tasks:** at 100%, native minimum window, Fit, and actual lowest allowed zoom, locate a nested activation and its Call/Reply, follow an ordinary architecture route, identify state action/region, edit/revert Inspector text, select/copy highlighted source, locate warning route and transfer keyboard focus. Require ordinary edges and current focus/selection to remain identifiable without hover. Treat lowest zoom as overview unless a separate detail requirement is adopted. Record scale/DPI, source commit and app build.
4. **Loss-of-color/degradation falsifiers:** on separately generated analysis output (leave original native witnesses intact), convert to luminance grayscale, quantize channels, and apply a documented moderate luminance-range compression/veiling approximation. Repeat selected/focused/warning/disabled identification; mark simulation parameters and failures. Grayscale preserves luminance contrast but removes hue. These stress tests are not a simulation of all limited-gamut monitors and do not establish arbitrary-monitor proof. Validate on the user's problematic physical display if available; do not block the useful bounded candidate on unknowable hardware details.
5. **Scope guard:** preserve hit targets, layer order, correlation/activation geometry, keyboard ownership and existing dash/shape semantics. Passing color calculations cannot replace native interaction evidence or close #26 by itself.
## Reproduction
`palette-contrast-20260912.py` next to this report is dependency-free Python. It retains source float constants, linearizes sRGB with the 0.04045 threshold, uses luminance weights 0.2126/0.7152/0.0722, and computes `(Llighter+0.05)/(Ldarker+0.05)`. Source tint and Iced selection mixing are explicit. Executed output is `palette-contrast-20260912.json`; six decimals are retained and threshold comparisons use unrounded values. Hex labels for float constants are display approximations. This measures specified opaque source pairs, not anti-aliasing, color-management, physical monitor performance or a new native palette.