Commit 2ca5ddf53e
Unsigned
Layout: unified · split
Docs/ACCESSIBILITY_VERIFICATION.md deleted −346
| @@ -1,346 +0,0 @@ | |||
| 1 | # Accessibility Verification Checklist (issue #21, Phase 6) | ||
| 2 | |||
| 3 | Manual verification for the accessibility work in phases 1–5. Everything here is | ||
| 4 | what the automated audit (`Scripts/audit-a11y.sh`) **cannot** check: VoiceOver | ||
| 5 | speech, the rotor, custom-content ordering, announcements, Voice Control, | ||
| 6 | motion/transparency/colour settings, keyboard focus order, and the two system | ||
| 7 | design languages. A green audit is necessary, not sufficient — this is what makes | ||
| 8 | it sufficient. | ||
| 9 | |||
| 10 | Each item traces to the phase that introduced it (e.g. `[P4]`) so a failure points | ||
| 11 | straight at the code. Check the box only when the **Expected** line is literally | ||
| 12 | true on the device. | ||
| 13 | |||
| 14 | --- | ||
| 15 | |||
| 16 | ## 0. Setup | ||
| 17 | |||
| 18 | ### Devices / runtimes | ||
| 19 | |||
| 20 | The app supports **iOS 17.6+** and renders under two system design languages: | ||
| 21 | classic chrome (17.6–25) and Liquid Glass (26+). Materials, surfaces, and | ||
| 22 | contrast resolve differently between them, so visual passes need both. | ||
| 23 | |||
| 24 | - [ ] Primary device on **iOS 26+** (Liquid Glass). | ||
| 25 | - [ ] A device or simulator on the **floor** (oldest available ≥ 17.6; 18.x is | ||
| 26 | the practical minimum since no 17.6 runtime ships). A simulator is fine for | ||
| 27 | the visual and Dynamic Type passes; VoiceOver/Voice Control are best on | ||
| 28 | hardware. | ||
| 29 | |||
| 30 | ### Seed data — required, or half the checklist is untestable | ||
| 31 | |||
| 32 | Several items only render with tracked domains and a completed lookup. The audit | ||
| 33 | simulator has none, which is exactly why the dense rows and widget are unverified | ||
| 34 | so far. Before starting: | ||
| 35 | |||
| 36 | - [ ] Inspect tab → run a lookup on a **live** domain (e.g. `cleberg.net`), let | ||
| 37 | all sections load. | ||
| 38 | - [ ] Run a lookup on a domain with a **weak/expiring or missing** cert and | ||
| 39 | missing SPF/DMARC, so warning/critical tones and badges actually appear. | ||
| 40 | - [ ] Track **at least 4** domains with mixed health (one healthy, one warning, | ||
| 41 | one critical, one unreachable) so Dashboard tiles, dense watchlist rows, and | ||
| 42 | the widget all have content. | ||
| 43 | - [ ] Run a **Bulk** lookup on ~5 domains so `BatchResultRowView` renders. | ||
| 44 | - [ ] Add the **Domain Portfolio** widget to the Home Screen in all three sizes | ||
| 45 | (small, medium, large). | ||
| 46 | - [ ] Force the Pro tier if needed so gated screens (Workflows, Scheduled | ||
| 47 | Reports, Compare) are reachable. | ||
| 48 | |||
| 49 | ### Settings map (paths used throughout) | ||
| 50 | |||
| 51 | | Setting | Path | | ||
| 52 | | --- | --- | | ||
| 53 | | VoiceOver | Settings → Accessibility → VoiceOver | | ||
| 54 | | Screen Curtain | VoiceOver on → triple-tap with 3 fingers | | ||
| 55 | | Voice Control | Settings → Accessibility → Voice Control | | ||
| 56 | | Larger Text / Dynamic Type | Settings → Accessibility → Display & Text Size → Larger Text | | ||
| 57 | | Bold Text | Settings → Accessibility → Display & Text Size → Bold Text | | ||
| 58 | | Increase Contrast | Settings → Accessibility → Display & Text Size → Increase Contrast | | ||
| 59 | | Differentiate Without Color | Settings → Accessibility → Display & Text Size → Differentiate Without Color | | ||
| 60 | | Smart Invert | Settings → Accessibility → Display & Text Size → Smart Invert | | ||
| 61 | | Reduce Motion | Settings → Accessibility → Motion → Reduce Motion | | ||
| 62 | | Reduce Transparency | Settings → Accessibility → Display & Text Size → Reduce Transparency | | ||
| 63 | | Full Keyboard Access (iPad) | Settings → Accessibility → Keyboards → Full Keyboard Access | | ||
| 64 | | App appearance override | In-app: Settings tab → Display → Appearance | | ||
| 65 | |||
| 66 | > Ordering note: the passes are grouped so each iOS setting is toggled **once**. | ||
| 67 | > Do them top to bottom to avoid thrashing Settings. | ||
| 68 | |||
| 69 | --- | ||
| 70 | |||
| 71 | ## 1. Baseline visual — Light, Dark, System `[P1][P2]` | ||
| 72 | |||
| 73 | No assistive tech on. Toggle appearance via **in-app Settings → Display → | ||
| 74 | Appearance**, then confirm the system setting is also honoured. | ||
| 75 | |||
| 76 | - [ ] **System** appearance follows the device; flipping the device Light/Dark | ||
| 77 | flips the app. | ||
| 78 | - [ ] **Light** and **Dark** overrides hold regardless of the device setting. | ||
| 79 | - [ ] Accent is **blue** everywhere — tab bar selection, links, section titles' | ||
| 80 | "info" accents, the Insights icons, the selected quick-filter chip. No | ||
| 81 | leftover **cyan**. `[P1]` | ||
| 82 | - [ ] Warning tone reads as **orange**, not olive/brown — check a "Warning" or | ||
| 83 | "Expiring" badge and the Dashboard "Warning" tile. `[P1 rebalance]` | ||
| 84 | - [ ] The selected Dashboard summary tile is a soft **blue-tinted** surface, not | ||
| 85 | lavender/violet. `[P2 wash fix]` | ||
| 86 | - [ ] Prominent buttons (**Run**, **Run Batch**, **Scan**) show a **white label | ||
| 87 | on a blue fill** that is comfortably readable in both schemes. `[P1 AccentFill]` | ||
| 88 | - [ ] Secondary/detail text (row labels, timestamps, "Monitoring off") is legible | ||
| 89 | in **Light** — not washed-out grey. `[P2 AppTextSecondary]` | ||
| 90 | - [ ] Status badges pair an **icon + text + colour** (e.g. lock + "Valid"), | ||
| 91 | never colour alone. | ||
| 92 | - [ ] Repeat the whole list on the **floor runtime**. Note any Liquid-Glass-only | ||
| 93 | difference. `[cross-runtime]` | ||
| 94 | |||
| 95 | --- | ||
| 96 | |||
| 97 | ## 2. Dynamic Type & reflow — up to Accessibility 5 `[P3]` | ||
| 98 | |||
| 99 | Larger Text → drag to **maximum** (AX5). Walk every primary screen: Inspect | ||
| 100 | (with results), Dashboard, Audit, History, Settings, Watchlist, a tracked-domain | ||
| 101 | detail, Workflows. | ||
| 102 | |||
| 103 | - [ ] All body text **scales up** (it already did pre-P3; confirm nothing is | ||
| 104 | pinned). The Dashboard tile numbers scale too. `[P3 fixed .system(size:)]` | ||
| 105 | - [ ] **No clipped headings.** Empty-state titles ("No Portfolio Yet", "No Audits | ||
| 106 | Yet", "No History Yet", "No Tracked Domains", "No Batch Results Yet") **wrap | ||
| 107 | onto multiple lines** rather than truncating with "…". `[P3 Label→HStack]` | ||
| 108 | - [ ] **No card requires horizontal scrolling.** Inspect result cards, the risk | ||
| 109 | card, and detail rows **wrap vertically**; there is no hidden left-right | ||
| 110 | gesture to reach content. `[P3 CardView reflow]` | ||
| 111 | - [ ] Dense rows (`BatchResultRowView`, `WatchlistRowView`) remain **readable** — | ||
| 112 | text may be tall but must not overlap the trailing badge or clip. If it | ||
| 113 | does, that is the deferred `ViewThatFits` work, not a P3 regression — log it. | ||
| 114 | `[deferred]` | ||
| 115 | - [ ] Every tappable control is at least **44×44pt** at default size. Spot-check | ||
| 116 | the **copy buttons** on data rows (the most-repeated control), the | ||
| 117 | **collapsible section headers**, and **Run**. `[P3 tap targets]` | ||
| 118 | - [ ] Turn on **Bold Text**; confirm no layout breaks and contrast holds. | ||
| 119 | - [ ] **Widget**: at AX sizes the widget content stays readable and is **not | ||
| 120 | truncated into nonsense** — it clamps at Accessibility 1 by design. `[P3 widget clamp]` | ||
| 121 | - [ ] Repeat the clipping/reflow spot-checks on the **floor runtime**. | ||
| 122 | |||
| 123 | --- | ||
| 124 | |||
| 125 | ## 3. VoiceOver `[P4]` | ||
| 126 | |||
| 127 | Enable VoiceOver. Learn the gestures if needed: swipe right = next element, swipe | ||
| 128 | up/down on the **rotor** set to "More Content" = reveal extra fields, two-finger | ||
| 129 | swipe up = read from top. | ||
| 130 | |||
| 131 | ### 3a. Icon-only controls announce a purpose, not a symbol name | ||
| 132 | |||
| 133 | Focus each and confirm the spoken label. **None** should say a raw symbol name | ||
| 134 | ("arrow clockwise", "square and arrow up", "bolt circle"). | ||
| 135 | |||
| 136 | - [ ] Dashboard toolbar refresh → **"Refresh all tracked domains"**. | ||
| 137 | - [ ] Inspect toolbar (after a lookup): clear → **"Clear results"**; actions menu | ||
| 138 | → **"Actions"**; export menu → **"Export"**. | ||
| 139 | - [ ] Watchlist: add → **"Add domain"**; filter → **"Filter and sort"**. | ||
| 140 | - [ ] History filter → **"Filter"**. | ||
| 141 | - [ ] Timeline grouping → **"Group timeline"**. | ||
| 142 | - [ ] Workflows: create → **"Create workflow"**; a run summary's export → | ||
| 143 | **"Export summary"**; re-run → **"Re-run workflow"**; a shared workflow's | ||
| 144 | person icon → **"Shared"**. | ||
| 145 | |||
| 146 | ### 3b. Toggles announce their state | ||
| 147 | |||
| 148 | - [ ] Inspect **Save** (bookmark): label **"Save domain"**, value **"Not saved"**; | ||
| 149 | activate → value becomes **"Saved"** and the element reports **selected**. | ||
| 150 | - [ ] Domain section **Pin**: label **"Pin domain"**, value toggles | ||
| 151 | **"Pinned"/"Not pinned"** and reports **selected** when pinned. | ||
| 152 | - [ ] Audit checklist item: reports **selected** when complete, and a **hint** | ||
| 153 | ("Marks complete" / "Marks incomplete"). | ||
| 154 | - [ ] Audit area picker and Workflow domain picker rows report **selected** when | ||
| 155 | chosen. | ||
| 156 | |||
| 157 | ### 3c. Badges and headings | ||
| 158 | |||
| 159 | - [ ] A status badge reads as **one word** — "Critical", "Valid", "Secure" — not | ||
| 160 | "icon, Critical". `[P4 badge combine]` | ||
| 161 | - [ ] Set the rotor to **Headings**. Section titles ("Summary", "Risk", "DNS", | ||
| 162 | etc.) and the collapsible Inspect section headers are reachable as headings | ||
| 163 | and let you **jump between sections**. `[P4 .isHeader]` | ||
| 164 | - [ ] A collapsible Inspect header announces **"Expanded"/"Collapsed"** as its | ||
| 165 | value with a hint, and toggling it updates the value. `[P4]` | ||
| 166 | - [ ] Confirm the header's **trailing controls** (Track / Pin / Note on the Domain | ||
| 167 | section) are still **individually focusable** — they were deliberately *not* | ||
| 168 | merged into the header. `[P4 no combine on trailing]` | ||
| 169 | |||
| 170 | ### 3d. Dense rows — combine + More Content rotor (the flagship) | ||
| 171 | |||
| 172 | On the **Watchlist** and a **Bulk** result list, with the rotor on **More Content**: | ||
| 173 | |||
| 174 | - [ ] Each row is **one VoiceOver stop**, not eight. `[P4]` | ||
| 175 | - [ ] The row's **label is the domain**; its **value is the status** (e.g. | ||
| 176 | "Registered" / "Critical, Registered"). | ||
| 177 | - [ ] Swiping up/down on More Content reveals the extra fields **in order**: | ||
| 178 | - Watchlist row: **Certificate** (spoken first, high importance), | ||
| 179 | Monitoring, Updated, Pinned. | ||
| 180 | - Batch row: **Risk** (high importance), IP address, Checked, Source, | ||
| 181 | Impact/Status. | ||
| 182 | - [ ] Risk and Certificate are spoken **without** needing the rotor (high | ||
| 183 | importance); the rest wait for the swipe. | ||
| 184 | |||
| 185 | ### 3e. Speech for technical strings | ||
| 186 | |||
| 187 | - [ ] Focus a **DNS record value** (Inspect → DNS section) and an SSL **Cipher | ||
| 188 | Suite** (Web section). Punctuation (`;`, `~`, `_`, `-`) is **spoken**, and | ||
| 189 | the string reads character/token-sensibly rather than as garbled prose. | ||
| 190 | `[P4 speechStyle .technical]` | ||
| 191 | - [ ] Focus a plain prose value (e.g. Issuer common name) and confirm it is **not** | ||
| 192 | spelled out awkwardly — only technical rows get the treatment. | ||
| 193 | |||
| 194 | ### 3f. Completion announcements | ||
| 195 | |||
| 196 | - [ ] Run a **single lookup**. On completion VoiceOver speaks **"Lookup complete | ||
| 197 | for `<domain>`. `<availability>`."** without you moving focus. `[P4]` | ||
| 198 | - [ ] Run a **sweep / Check All**. On completion it speaks **"Sweep complete. N | ||
| 199 | domains, X changed, Y warnings."** `[P4]` | ||
| 200 | - [ ] The announcements do **not** fire per-domain during a long sweep (would | ||
| 201 | flood the queue) — only once at the end. | ||
| 202 | |||
| 203 | ### 3g. Widget under VoiceOver | ||
| 204 | |||
| 205 | - [ ] A medium/large widget domain row reads as one phrase, e.g. **"example.com, | ||
| 206 | critical, certificate expires in 12 days"** (or "pinned", or "certificate | ||
| 207 | expired"). `[P4]` | ||
| 208 | - [ ] The small widget's count pills read **"N healthy"**, **"N warning"**, | ||
| 209 | **"N critical"** — not a bare number. `[P4]` | ||
| 210 | |||
| 211 | ### 3h. Full walkthrough with Screen Curtain | ||
| 212 | |||
| 213 | Turn on Screen Curtain (triple-tap, 3 fingers — screen goes black). Complete the | ||
| 214 | core journey **without looking**: | ||
| 215 | |||
| 216 | - [ ] Inspect a domain → hear the sections → **Save** it → open **Watchlist** → | ||
| 217 | open its **detail** → back out. Every step is discoverable and every control | ||
| 218 | announces a purpose and state. Log anything that leaves you stuck. | ||
| 219 | |||
| 220 | --- | ||
| 221 | |||
| 222 | ## 4. Voice Control — label-in-name (WCAG 2.5.3) `[P4]` | ||
| 223 | |||
| 224 | Enable Voice Control. Say the **printed** text of controls. Every visible-text | ||
| 225 | control must respond to its visible name (this is why labels preserve visible | ||
| 226 | text rather than replacing it). | ||
| 227 | |||
| 228 | - [ ] "Tap **Run**" runs the lookup (not broken by a relabel). | ||
| 229 | - [ ] "Tap **Track**", "Tap **Note**", "Tap **Compare**", "Tap **Cancel**", | ||
| 230 | "Tap **Save**" each work by their printed word. | ||
| 231 | - [ ] Say **"Show numbers"**; confirm the icon-only controls get numbered overlays | ||
| 232 | and are operable (they have labels, so they also respond to "Show names"). | ||
| 233 | - [ ] No control is reachable *only* by a name that differs from its visible text. | ||
| 234 | |||
| 235 | --- | ||
| 236 | |||
| 237 | ## 5. Colour & contrast settings `[P1][P2][P5]` | ||
| 238 | |||
| 239 | ### 5a. Increase Contrast | ||
| 240 | |||
| 241 | - [ ] Enable. Status colours and the accent shift to their **high-contrast** | ||
| 242 | variants; nothing becomes unreadable in either scheme. `[P1 HC variants]` | ||
| 243 | - [ ] The Settings section headers that were marginal now clear — this is the one | ||
| 244 | the audit already measured (light 21 → 18). `[P2]` | ||
| 245 | |||
| 246 | ### 5b. Differentiate Without Color — the P5 payoff | ||
| 247 | |||
| 248 | Enable. This is the pass that validates most of Phase 5. | ||
| 249 | |||
| 250 | - [ ] **Dashboard summary tiles**: the small dot becomes a **per-filter symbol** | ||
| 251 | (grid, checkmark, triangle, octagon, refresh, clock, wifi-slash). `[P5]` | ||
| 252 | - [ ] **Selected quick-filter chip** gains a **checkmark + border** — selection no | ||
| 253 | longer depends on fill colour alone. `[P5]` | ||
| 254 | - [ ] **Inspect data rows** with a warning/failure value show a **leading symbol** | ||
| 255 | (triangle / octagon) before the value. `[P5 LabeledValueRow]` | ||
| 256 | - [ ] Turning the setting **off** removes those extras (no permanent clutter). `[P5]` | ||
| 257 | - [ ] **Widget** status already uses symbols regardless of this setting — confirm | ||
| 258 | each row shows checkmark/triangle/octagon, not a bare dot. `[P5]` | ||
| 259 | |||
| 260 | ### 5c. Smart Invert | ||
| 261 | |||
| 262 | - [ ] Enable. UI inverts sensibly; images/icons that should stay un-inverted do. | ||
| 263 | Text stays legible. Note anything that inverts wrongly. `[verification]` | ||
| 264 | |||
| 265 | --- | ||
| 266 | |||
| 267 | ## 6. Motion & transparency `[P5]` | ||
| 268 | |||
| 269 | ### 6a. Reduce Motion | ||
| 270 | |||
| 271 | Enable. Confirm each animated transition becomes an **instant** state change (no | ||
| 272 | slide/fade): | ||
| 273 | |||
| 274 | - [ ] Copy button check-mark swap (tap a copy button) — flips instantly. `[P5]` | ||
| 275 | - [ ] Collapsible Inspect section expand/collapse — no ease animation. `[P5]` | ||
| 276 | - [ ] Timeline scroll-to-section — jumps, no scroll animation. `[P5]` | ||
| 277 | - [ ] Watchlist list reorder (change sort/filter) — no reflow animation. `[P5]` | ||
| 278 | |||
| 279 | ### 6b. Reduce Transparency | ||
| 280 | |||
| 281 | - [ ] Enable. Trigger the **Data Management** success toast (Settings → Data | ||
| 282 | Management → perform a clear). Its pill background is an **opaque surface**, | ||
| 283 | not a blur. `[P5]` | ||
| 284 | - [ ] On **iOS 26+**, confirm system-composited chrome (nav/tab bars) still reads | ||
| 285 | acceptably — the app can't declare that translucency itself. `[cross-runtime]` | ||
| 286 | |||
| 287 | --- | ||
| 288 | |||
| 289 | ## 7. iPad — Full Keyboard Access & split layout `[verification]` | ||
| 290 | |||
| 291 | On iPad (regular width, so `RootTabView` shows the `NavigationSplitView` | ||
| 292 | sidebar + detail). Enable Full Keyboard Access; attach or use the software | ||
| 293 | alternative. | ||
| 294 | |||
| 295 | - [ ] **Tab** moves focus in a **logical order** — sidebar → detail, top → bottom, | ||
| 296 | no traps. | ||
| 297 | - [ ] The blue **focus ring** is visible on every focusable control. | ||
| 298 | - [ ] Sidebar tab selection and detail controls are all reachable and operable by | ||
| 299 | keyboard. | ||
| 300 | - [ ] Switching tabs via keyboard updates the detail pane correctly. | ||
| 301 | |||
| 302 | --- | ||
| 303 | |||
| 304 | ## 8. Cross-runtime sign-off `[cross-runtime]` | ||
| 305 | |||
| 306 | - [ ] Sections 1, 2, 5, 6 re-checked on the **floor runtime** (classic chrome). | ||
| 307 | - [ ] Sections 1, 2, 5, 6 checked on **iOS 26+** (Liquid Glass). | ||
| 308 | - [ ] Any behaviour that differs between the two is logged below with the runtime | ||
| 309 | noted. | ||
| 310 | |||
| 311 | --- | ||
| 312 | |||
| 313 | ## Sign-off | ||
| 314 | |||
| 315 | | Pass | 26+ (Liquid Glass) | Floor (classic) | Notes | | ||
| 316 | | --- | --- | --- | --- | | ||
| 317 | | 1 Baseline visual | | | | | ||
| 318 | | 2 Dynamic Type / reflow | | | | | ||
| 319 | | 3 VoiceOver | n/a-runtime | | do once on hardware | | ||
| 320 | | 4 Voice Control | n/a-runtime | | do once on hardware | | ||
| 321 | | 5 Colour & contrast | | | | | ||
| 322 | | 6 Motion & transparency | | | | | ||
| 323 | | 7 iPad keyboard | n/a | | iPad only | | ||
| 324 | |||
| 325 | ## Defect log | ||
| 326 | |||
| 327 | Record failures here; each becomes an issue or a fix commit. | ||
| 328 | |||
| 329 | | # | Pass / item | Device / runtime | Observed | Expected | Traces to | | ||
| 330 | | --- | --- | --- | --- | --- | --- | | ||
| 331 | | | | | | | | | ||
| 332 | |||
| 333 | --- | ||
| 334 | |||
| 335 | ## Known-deferred (not defects — expected gaps) | ||
| 336 | |||
| 337 | - **`ViewThatFits` dense-row reflow** was not implemented (unverifiable in the | ||
| 338 | audit sim). If §2 shows dense rows overlapping/truncating at AX5 with real data, | ||
| 339 | that is this gap surfacing — file it against the deferred item, don't treat it | ||
| 340 | as a P3 regression. | ||
| 341 | - **Live Activity / Dynamic Island** custom accessibility was intentionally not | ||
| 342 | added (ActivityKit exposes the labelled `ProgressView` already). Sanity-check a | ||
| 343 | running sweep's Live Activity reads acceptably, but a finding here is | ||
| 344 | enhancement, not regression. | ||
| 345 | - **Localization** — all strings are English literals by design for now; a11y | ||
| 346 | strings were written `LocalizedStringKey`-compatible for a future catalog. | ||
Docs/ACCESSIBILITY_VERIFICATION_RESULTS.md deleted −292
| @@ -1,292 +0,0 @@ | |||
| 1 | # Accessibility Verification — Results (issue #21, Phase 6) | ||
| 2 | |||
| 3 | Execution of `ACCESSIBILITY_VERIFICATION.md` against the iOS Simulator. The | ||
| 4 | runbook was written for a human on a physical device; a meaningful fraction is | ||
| 5 | beyond a simulator. Every item below is sorted into one of three tiers and | ||
| 6 | treated accordingly: | ||
| 7 | |||
| 8 | - **Tier 1 — executed here.** Appearance, Increase Contrast, Dynamic Type | ||
| 9 | (including a middle-band sweep), the audit suite, accessibility **metadata** | ||
| 10 | (labels/values/traits, now permanent XCUITest assertions), and the | ||
| 11 | Liquid-Glass-vs-classic cross-runtime comparison. | ||
| 12 | - **Tier 2 — attempted, reported honestly.** Differentiate Without Color, | ||
| 13 | Reduce Motion, Reduce Transparency. One of the three turned out to be fully | ||
| 14 | toggleable and is now **verified**; the mechanism is documented for the rest. | ||
| 15 | - **Tier 3 — requires a physical device.** VoiceOver speech, the rotor, | ||
| 16 | announcements, Voice Control, Screen Curtain. Not attempted, not faked. | ||
| 17 | |||
| 18 | **"Verified" vs "verified by construction."** A ✅ **Pass** means the behaviour | ||
| 19 | was *observed* (a screenshot, an assertion, or a finding-count delta). *Verified | ||
| 20 | by construction* (⚙️) means the code compiles and the pattern is right but the | ||
| 21 | runtime behaviour was not observed here — it is **not** counted as a pass. | ||
| 22 | |||
| 23 | ## Environment | ||
| 24 | |||
| 25 | | Role | Simulator | Runtime | Design language | | ||
| 26 | | --- | --- | --- | --- | | ||
| 27 | | Floor | iPhone 16 | iOS 18.6 | Classic chrome | | ||
| 28 | | Liquid Glass (mid) | iPhone 17 | iOS 26.5 | Liquid Glass | | ||
| 29 | | Current | iPhone 17e | iOS 27.0 | Liquid Glass | | ||
| 30 | |||
| 31 | Deployment target is 17.6; no 17.6/17.5 runtime is usable (the app cannot | ||
| 32 | install below the floor), so 18.6 is the practical floor, per the runbook. | ||
| 33 | |||
| 34 | Seed data via `DOMAIN_DIG_SEED_FIXTURES` + `DOMAIN_DIG_FORCE_PRO_PLUS` (DEBUG, | ||
| 35 | in-memory, never persisted). **Note on tooling:** `simctl launch` with the seed | ||
| 36 | argument did *not* populate the fixtures, and `simctl ui appearance` did not | ||
| 37 | propagate to a headless-booted simulator. Both were worked around by driving | ||
| 38 | everything through **XCUITest** (which seeds reliably) and switching appearance | ||
| 39 | through the app's own Settings → Display picker. This is why the screenshots and | ||
| 40 | metadata checks are committed as tests rather than shell scripts — see | ||
| 41 | `DomainDigUITests/AccessibilityScreenshotTests.swift` and | ||
| 42 | `AccessibilityMetadataTests.swift`. | ||
| 43 | |||
| 44 | --- | ||
| 45 | |||
| 46 | ## Headline outcomes | ||
| 47 | |||
| 48 | 1. **A real enforced failure was found and fixed.** `Scripts/audit-a11y.sh | ||
| 49 | current` (iOS 27.0) failed an **enforced** `.dynamicType` finding on the | ||
| 50 | Settings `Section("Services")` header — a system-rendered header the app sets | ||
| 51 | no font on, present only on 27.0 (18.6 floor and the 26.x runtime CI uses are | ||
| 52 | clean). Resolved with a narrow, proven `noiseReason(for:)` carve-out. Delta: | ||
| 53 | `current` went **FAIL → SUCCEEDED**, the finding still printed as | ||
| 54 | `[noise: …]`. See §5 and the fix note below. | ||
| 55 | 2. **The highest-value metadata checks are now permanent tests.** The dense-row | ||
| 56 | label/value contracts and the icon-only control labels are asserted | ||
| 57 | mechanically in `AccessibilityMetadataTests` (green on 18.6 and 27.0), | ||
| 58 | shrinking the manual runbook. | ||
| 59 | 3. **Differentiate Without Color is fully verifiable in the simulator** via the | ||
| 60 | *global* `com.apple.Accessibility` defaults domain — the runbook and task | ||
| 61 | both assumed this might not be reachable. It is. Captured proof: | ||
| 62 | `lg-dashboard-light-differentiate.png`. | ||
| 63 | 4. **The middle-band Dynamic Type sweep found no third bug** exclusive to that | ||
| 64 | band (negative result), but is retained as regression insurance for a band | ||
| 65 | that historically shipped two. | ||
| 66 | |||
| 67 | --- | ||
| 68 | |||
| 69 | ## 1. Baseline visual — Light, Dark, System `[P1][P2]` | ||
| 70 | |||
| 71 | Evidence: `classic-dashboard-{light,dark}.png`, `lg-dashboard-{light,dark}.png`, | ||
| 72 | `classic-batch-{light,dark}.png`, `lg-batch-{light,dark}.png`. Appearance driven | ||
| 73 | through Settings → Display. | ||
| 74 | |||
| 75 | | Item | Result | Notes | | ||
| 76 | | --- | --- | --- | | ||
| 77 | | System follows device | ⚙️ By construction | `simctl ui appearance` does not propagate headlessly; Light/Dark set via the in-app picker instead, which drives the same single `@AppStorage` path. | | ||
| 78 | | Light / Dark overrides hold | ✅ Pass | Both captured on both runtimes; the picker override renders correctly. | | ||
| 79 | | Accent blue everywhere, no cyan | ✅ Pass | Tab-bar selection, `•All`, links, selected quick-filter chip all blue. No cyan observed. | | ||
| 80 | | Warning reads orange, not olive | ✅ Pass | Dashboard "Warning" tile and batch "Warning" badge are clearly orange in both schemes. | | ||
| 81 | | Selected tile is blue-tinted, not lavender | ✅ Pass | "Total Domains" tile is a soft blue surface in light and dark. | | ||
| 82 | | Prominent buttons: white label on blue fill | ⚙️ By construction | Run/Run Batch are disabled in the seeded state (no typed domain), so the enabled `.borderedProminent` fill was not captured; palette values are audited. | | ||
| 83 | | Secondary text legible in Light | ✅ Pass | "QUICK FILTERS", timestamps, "No recent portfolio changes" read clearly on the light card (this is the `AppTextSecondary` fix). | | ||
| 84 | | Badges pair icon + text + colour | ✅ Pass | Batch badges show lock/triangle/octagon/x + word + colour. | | ||
| 85 | | Repeat on floor runtime | ✅ Pass | Classic-chrome (18.6) captures match; see cross-runtime §8. | | ||
| 86 | |||
| 87 | ## 2. Dynamic Type & reflow — up to Accessibility 5 `[P3]` | ||
| 88 | |||
| 89 | Evidence: `*-dashboard-axxxl.png`, `*-batch-axxxl.png`, `*-watchlist-axxxl.png` | ||
| 90 | (both runtimes), plus the automated `…AccessibilityXXXL` and new | ||
| 91 | `…AccessibilityL` audit sweeps. | ||
| 92 | |||
| 93 | | Item | Result | Notes | | ||
| 94 | | --- | --- | --- | | ||
| 95 | | Body text + tile numbers scale | ✅ Pass | Dashboard "4 / 2 / 1" and all labels scale at AXXXL. | | ||
| 96 | | No clipped headings (wrap, not "…") | ✅ Pass | "Batch Results", "Total Domains" wrap onto multiple lines; audit reports no *named* `textClipped` on empty-state headings. | | ||
| 97 | | No card needs horizontal scrolling | ✅ Pass | Cards reflow vertically at AXXXL; no hidden horizontal gesture. | | ||
| 98 | | Dense rows readable at AX5 | ⚠️ Known-deferred | Watchlist/batch rows become very tall and wrap; readable, no horizontal badge overlap. This is the deferred `ViewThatFits` case, **not** a P3 regression (see `lg-watchlist-axxxl.png`). | | ||
| 99 | | Tap targets ≥ 44×44 | ⚙️ By construction | `AppLayout.minimumTapTarget` floor is enforced in code and the audit reports no named `hitRegion` findings; not separately measured here. | | ||
| 100 | | Bold Text | ❌ Not executed | Not exposed by `simctl`; same class as the Tier-2 settings. Requires device or Settings-app automation. | | ||
| 101 | | Widget clamps at AX1 | ❌ Not executed | Widgets do not render in the audit simulator or these captures — Tier 3-adjacent (needs Home Screen). | | ||
| 102 | | Repeat spot-checks on floor | ✅ Pass | Classic AXXXL captures match. | | ||
| 103 | |||
| 104 | **Middle-band sweep (added).** `testSeededScreensAtIntermediateAccessibilitySize` | ||
| 105 | audits the seeded screens at `AccessibilityL`. Result: the `textClipped` findings | ||
| 106 | it surfaced on the Risk badges (`Risk 12 Low`, `Risk 41 Medium`) are **also | ||
| 107 | present at the default size** and absent at XXXL, so they are pre-existing | ||
| 108 | reportOnly seeded-row findings, **not** a middle-band-exclusive third bug. No new | ||
| 109 | gap found. The sweep is retained as regression insurance for a band that | ||
| 110 | historically shipped two escaped bugs. | ||
| 111 | |||
| 112 | ## 3. VoiceOver `[P4]` — Tier 3, requires a physical device | ||
| 113 | |||
| 114 | iOS VoiceOver **speech** does not run in the Simulator; macOS VoiceOver reading | ||
| 115 | the simulator window is not equivalent and is not accepted as evidence. What the | ||
| 116 | simulator *can* assert is the underlying **metadata**, which is now covered by | ||
| 117 | `AccessibilityMetadataTests` (green on 18.6 and 27.0): | ||
| 118 | |||
| 119 | | Runbook item | Metadata coverage | Result | | ||
| 120 | | --- | --- | --- | | ||
| 121 | | §3a Dashboard refresh → "Refresh all tracked domains" | asserted | ✅ Pass | | ||
| 122 | | §3a Watchlist add → "Add domain"; filter → "Filter and sort" | asserted | ✅ Pass | | ||
| 123 | | §3a Workflows create → "Create workflow" | asserted | ✅ Pass | | ||
| 124 | | §3a History filter → "Filter" | ⚙️ By construction | Menu is gated behind non-empty history; not seedable. Label exists at `HistoryView.swift:109`. | | ||
| 125 | | §3a Inspect clear/actions/export, Timeline grouping, Workflow export/re-run/shared | ⚙️ By construction | Reachable only after a live lookup / on populated workflow runs; labels verified in source (see `ACCESSIBILITY.md` map). | | ||
| 126 | | §3d Watchlist row: label = domain, value = availability | asserted | ✅ Pass (`healthy.example`→"Registered"; long domain→"Unknown") | | ||
| 127 | | §3d Batch row: label = domain, value = "status, availability" | asserted | ✅ Pass (`broken.example`→"Critical, Registered"; `unreachable.example`→"Failed, Unknown") | | ||
| 128 | | §3c Badge reads as one word | asserted (folded into row value) | ✅ Pass | | ||
| 129 | | §3b Save/Pin selected-state; §3b audit checklist; picker selected | ⚙️ By construction | All live behind a completed live inspection, seeded audits, or a multi-step gated flow — non-deterministic in CI. Traits verified in source (`ContentView.swift:565–567, 1372–1374`; `AuditViews.swift:264–265`; `WorkflowsView.swift:647`). | | ||
| 130 | | §3e speech style, §3f announcements, §3g widget speech, §3h Screen Curtain | ❌ Requires device | VoiceOver speech / rotor / announcements — Tier 3. | | ||
| 131 | |||
| 132 | **Rotor / custom-content ordering:** not observable from XCUITest at all — | ||
| 133 | `.accessibilityCustomContent` does not surface as a queryable element property. | ||
| 134 | Verified by construction (the `WatchlistRowAccessibility` / | ||
| 135 | `BatchRowAccessibility` modifiers) and deferred to the device pass. | ||
| 136 | |||
| 137 | ## 4. Voice Control (WCAG 2.5.3) `[P4]` — Tier 3, requires a physical device | ||
| 138 | |||
| 139 | Voice Control does not run in the Simulator. Label-in-name is partially | ||
| 140 | *inferable* — every asserted `accessibilityLabel` in §3 preserves the control's | ||
| 141 | visible text — but "say the printed word and it activates" must be confirmed on | ||
| 142 | hardware. | ||
| 143 | |||
| 144 | | Item | Result | | ||
| 145 | | --- | --- | | ||
| 146 | | "Tap Run / Track / Note / Compare / Cancel / Save" by printed word | ❌ Requires device | | ||
| 147 | | "Show numbers" overlays on icon-only controls | ❌ Requires device | | ||
| 148 | | No control reachable only by a differing name | ⚙️ By construction (labels preserve visible text) | | ||
| 149 | |||
| 150 | ## 5. Colour & contrast settings `[P1][P2][P5]` | ||
| 151 | |||
| 152 | ### 5a. Increase Contrast — Tier 1 | ||
| 153 | |||
| 154 | `simctl ui <udid> increase_contrast enabled` works. The palette carries HC | ||
| 155 | variants; the audit's `.contrast` category stays report-only by design (see | ||
| 156 | `ACCESSIBILITY.md`). | ||
| 157 | |||
| 158 | | Item | Result | Notes | | ||
| 159 | | --- | --- | --- | | ||
| 160 | | Status/accent shift to HC variants, nothing unreadable | ⚙️ By construction | HC toggles via `simctl`, but the effect is a colour-value swap not reliably distinguishable in a downscaled screenshot; palette HC variants are defined and audited. | | ||
| 161 | | Marginal Settings headers clear | ⚙️ By construction | The documented light 21→18 contrast measurement; unchanged this pass. | | ||
| 162 | |||
| 163 | ### 5b. Differentiate Without Color — **Tier 2, VERIFIED** ✅ | ||
| 164 | |||
| 165 | The runbook and task both flagged this as possibly un-toggleable in a simulator. | ||
| 166 | It **is** toggleable: `simctl ui` does not expose it, and a `defaults write` to | ||
| 167 | the app's *own* (sandboxed) container does not reach it — but a write to the | ||
| 168 | **global** `com.apple.Accessibility` domain does, and XCUITest-launched apps read | ||
| 169 | it via `UIAccessibility`: | ||
| 170 | |||
| 171 | ```sh | ||
| 172 | xcrun simctl spawn <udid> defaults write com.apple.Accessibility DifferentiateWithoutColor -bool true | ||
| 173 | ``` | ||
| 174 | |||
| 175 | Captured proof — `lg-dashboard-light-differentiate.png` vs `lg-dashboard-light.png`: | ||
| 176 | |||
| 177 | | Item | Result | Observed | | ||
| 178 | | --- | --- | --- | | ||
| 179 | | Summary tiles gain per-filter symbols | ✅ Pass | Dot → grid (All), checkmark (Healthy), triangle (Warning), octagon (Critical), refresh (Changed), wifi-slash (Unreachable). | | ||
| 180 | | Selected quick-filter chip gains checkmark + border | ✅ Pass | "All" chip shows ✓ and a border; selection no longer fill-colour only. | | ||
| 181 | | Inspect data-row warning/failure symbol | ⚙️ By construction | `LabeledValueRow` is behind a live lookup; the Dashboard payoff above exercises the same `accessibilityDifferentiateWithoutColor` path. | | ||
| 182 | | Turning it off removes the extras | ✅ Pass | The default set of screenshots (setting off) shows plain dots / no chip checkmark. | | ||
| 183 | | Widget uses symbols regardless | ⚙️ By construction | Widget does not render in these captures. | | ||
| 184 | |||
| 185 | ### 5c. Smart Invert — ❌ Not executed | ||
| 186 | |||
| 187 | Not exposed by `simctl`; not in the global-domain set that worked for DWC. | ||
| 188 | Requires the Settings app / device. | ||
| 189 | |||
| 190 | ## 6. Motion & transparency `[P5]` — Tier 2 | ||
| 191 | |||
| 192 | Both settings **can be written** to the global `com.apple.Accessibility` domain | ||
| 193 | (`ReduceMotionEnabled`, `ReduceTransparencyEnabled`) — the same mechanism proven | ||
| 194 | to reach the app for DWC. But their *effects* are not screenshot-capturable: | ||
| 195 | |||
| 196 | | Item | Result | Notes | | ||
| 197 | | --- | --- | --- | | ||
| 198 | | §6a Reduce Motion: copy-check swap, section expand, timeline scroll, list reorder become instant | ⚙️ By construction | Effect is animation *timing*; a still frame cannot show "instant vs animated". Toggle mechanism confirmed; five sites guarded via `accessibilityReduceMotion` in code. | | ||
| 199 | | §6b Reduce Transparency: Data Management toast is opaque | ⚙️ By construction | The only translucency swap is a **transient** toast behind a multi-step clear; not captured. `accessibilityReduceTransparency` swap verified in source. | | ||
| 200 | | §6b iOS 26+ system chrome still reads acceptably | ✅ Pass | Liquid Glass nav/tab bars legible in all captures (app cannot declare that translucency itself). | | ||
| 201 | |||
| 202 | **Net:** the Tier-2 toggle method (global accessibility defaults + XCUITest | ||
| 203 | launch) is now known to work — DWC is fully verified with it. Motion and | ||
| 204 | Transparency remain verified-by-construction because their effects are timing / | ||
| 205 | transient, but the manual device pass for them is now optional rather than | ||
| 206 | blocked: the same `defaults write` unblocks a scripted check with a screen | ||
| 207 | recording. | ||
| 208 | |||
| 209 | ## 7. iPad — Full Keyboard Access & split layout `[verification]` | ||
| 210 | |||
| 211 | | Item | Result | | ||
| 212 | | --- | --- | | ||
| 213 | | Tab focus order, focus ring, sidebar/detail reachability, tab→detail update | ❌ Requires device | Full Keyboard Access is not exposed by `simctl`; keyboard-focus traversal is a hardware/Settings behaviour. The `NavigationSplitView` layout itself renders (regular width) but focus order was not exercised. | | ||
| 214 | |||
| 215 | ## 8. Cross-runtime sign-off `[cross-runtime]` | ||
| 216 | |||
| 217 | Captured the same seeded screens on **classic chrome (18.6)** and **Liquid Glass | ||
| 218 | (26.5)** in Light, Dark, and AXXXL. The audit was run on **18.6 and 27.0**. | ||
| 219 | |||
| 220 | - The semantic palette resolves correctly on both design languages: warning | ||
| 221 | orange, critical red, positive green, blue accent, blue-tinted selected tile. | ||
| 222 | No Liquid-Glass-only palette regression observed. | ||
| 223 | - The only structural difference is expected: Liquid Glass renders translucent, | ||
| 224 | rounded nav/tab chrome; classic renders flatter, opaque chrome. Legibility | ||
| 225 | holds in both. | ||
| 226 | - **Audit coverage is genuinely not nested:** 18.6 passed clean; 27.0 surfaced | ||
| 227 | the extra `Section` header `.dynamicType` finding that 18.6/26.x do not (now | ||
| 228 | carved out). This confirms the repo's rationale for a per-runtime local run. | ||
| 229 | |||
| 230 | --- | ||
| 231 | |||
| 232 | ## Filled sign-off | ||
| 233 | |||
| 234 | | Pass | 26+/27 (Liquid Glass) | Floor (classic 18.6) | Notes | | ||
| 235 | | --- | --- | --- | --- | | ||
| 236 | | 1 Baseline visual | ✅ | ✅ | Light/Dark captured both; System by-construction | | ||
| 237 | | 2 Dynamic Type / reflow | ✅ | ✅ | AXXXL + middle-band L; dense rows known-deferred | | ||
| 238 | | 3 VoiceOver | metadata ✅ / speech ❌ device | metadata ✅ | Speech/rotor Tier 3 | | ||
| 239 | | 4 Voice Control | ❌ device | ❌ device | Labels preserve visible text (by construction) | | ||
| 240 | | 5 Colour & contrast | 5b DWC ✅ / 5a,5c ⚙️/❌ | 5b DWC ✅ | DWC verified via global defaults | | ||
| 241 | | 6 Motion & transparency | ⚙️ | ⚙️ | Effects not screenshot-capturable | | ||
| 242 | | 7 iPad keyboard | n/a | ❌ device | FKA not in simulator | | ||
| 243 | |||
| 244 | ## Tier 3 — the residual physical-device pass | ||
| 245 | |||
| 246 | The human pass now shrinks to exactly these, all requiring hardware: | ||
| 247 | |||
| 248 | - **VoiceOver:** §3a controls only reachable after a live lookup (Inspect | ||
| 249 | clear/actions/export, Timeline grouping, Workflow export/re-run/shared); | ||
| 250 | §3b Save/Pin/audit-checklist/picker selected-state *spoken*; §3c one-word | ||
| 251 | badge *spoken*; §3d More Content rotor order; §3e technical-string speech; | ||
| 252 | §3f completion announcements; §3g widget speech; §3h Screen Curtain journey. | ||
| 253 | - **Voice Control:** §4 in full. | ||
| 254 | - **Bold Text** (§2), **Smart Invert** (§5c), and the **iPad Full Keyboard | ||
| 255 | Access** pass (§7) — Settings toggles not exposed to `simctl` and not in the | ||
| 256 | global accessibility domain. | ||
| 257 | - **Widget** at AX sizes and under VoiceOver (§2, §3g) — needs the Home Screen. | ||
| 258 | - **Reduce Motion / Reduce Transparency** *effect* confirmation (§6) — optional; | ||
| 259 | the toggle is now scriptable, but observing instant-animation / opaque-toast | ||
| 260 | needs a screen recording. | ||
| 261 | |||
| 262 | ## Changes made this pass | ||
| 263 | |||
| 264 | | Change | File | Evidence | | ||
| 265 | | --- | --- | --- | | ||
| 266 | | Fixed enforced 27.0 Settings `.dynamicType` failure | `DomainDigUITests/AccessibilityAuditHarness.swift` | `audit-a11y.sh current` FAIL → SUCCEEDED; finding prints as `[noise: iOS-rendered Settings section header …]` | | ||
| 267 | | New metadata assertions (icon labels, dense-row label/value) | `DomainDigUITests/AccessibilityMetadataTests.swift` | 4 tests green on 18.6 + 27.0 | | ||
| 268 | | Middle-band Dynamic Type sweep (`AccessibilityL`) | `DomainDigUITests/AccessibilityAuditTests.swift` | Passes; negative result recorded above | | ||
| 269 | | Screenshot-capture utility (best-effort, non-gating) | `DomainDigUITests/AccessibilityScreenshotTests.swift` | 15 screenshots in `Docs/a11y-screenshots/` | | ||
| 270 | |||
| 271 | ### On the suppression (not a ratchet weakening) | ||
| 272 | |||
| 273 | The Settings finding is on a plain `Section("Services")` (`ContentView.swift` | ||
| 274 | ~2768) whose font the app never sets — the scaling is UIKit's system header. It | ||
| 275 | appears **only** on iOS 27.0 (the 18.6 floor and the 26.x runtime CI runs are | ||
| 276 | clean; `ACCESSIBILITY.md` already records this asymmetry as "dynamicType finding | ||
| 277 | 18.6 missed"). The carve-out is scoped to `.dynamicType` on the exact Settings | ||
| 278 | section-header titles, so a real regression on app-controlled text still | ||
| 279 | enforces — matching the existing "system field placeholder" and system-header | ||
| 280 | contrast carve-outs. The proof lives inline in `noiseReason(for:)`. | ||
| 281 | |||
| 282 | ## Screenshot index (`Docs/a11y-screenshots/`) | ||
| 283 | |||
| 284 | | File | Runtime | Screen | Config | | ||
| 285 | | --- | --- | --- | --- | | ||
| 286 | | `classic-dashboard-light.png` / `-dark.png` | 18.6 | Dashboard | Light / Dark | | ||
| 287 | | `classic-batch-light.png` / `-dark.png` | 18.6 | Batch results | Light / Dark | | ||
| 288 | | `classic-{dashboard,watchlist,batch}-axxxl.png` | 18.6 | — | AccessibilityXXXL | | ||
| 289 | | `lg-dashboard-light.png` / `-dark.png` | 26.5 | Dashboard | Light / Dark | | ||
| 290 | | `lg-batch-light.png` / `-dark.png` | 26.5 | Batch results | Light / Dark | | ||
| 291 | | `lg-{dashboard,watchlist,batch}-axxxl.png` | 26.5 | — | AccessibilityXXXL | | ||
| 292 | | `lg-dashboard-light-differentiate.png` | 26.5 | Dashboard | Light + Differentiate Without Color | | ||
Docs/a11y-screenshots/classic-batch-axxxl.png deleted
Binary file not shown.
Docs/a11y-screenshots/classic-batch-dark.png deleted
Binary file not shown.
Docs/a11y-screenshots/classic-batch-light.png deleted
Binary file not shown.
Docs/a11y-screenshots/classic-dashboard-axxxl.png deleted
Binary file not shown.
Docs/a11y-screenshots/classic-dashboard-dark.png deleted
Binary file not shown.
Docs/a11y-screenshots/classic-dashboard-light.png deleted
Binary file not shown.
Docs/a11y-screenshots/classic-watchlist-axxxl.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-batch-axxxl.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-batch-dark.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-batch-light.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-dashboard-axxxl.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-dashboard-dark.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-dashboard-light-differentiate.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-dashboard-light.png deleted
Binary file not shown.
Docs/a11y-screenshots/lg-watchlist-axxxl.png deleted
Binary file not shown.
SECURITY.md +3 −3
| @@ -4,8 +4,8 @@ | |||
| 4 | 4 | ||
| 5 | |Version|Supported| | 5 | |Version|Supported| |
| 6 | |-------|---------| | 6 | |-------|---------| |
| 7 | | 1.x | ✅ Yes | | 7 | | 4.x | ✅ Yes | |
| 8 | | < 1.0 | ❌ No | | 8 | | < 4.0 | ❌ No | |
| 9 | 9 | ||
| 10 | --- | 10 | --- |
| 11 | 11 | ||
| @@ -14,7 +14,7 @@ | |||
| 14 | If you discover a security vulnerability, **do not open a public issue**. | 14 | If you discover a security vulnerability, **do not open a public issue**. |
| 15 | Instead: | 15 | Instead: |
| 16 | 16 | ||
| 17 | 1. **Email** your report to [security@cleberg.net](mailto:security@cleberg.net). | 17 | 1. **Email** your report to [security@zerolabs.sh](mailto:security@zerolabs.sh). |
| 18 | Include: | 18 | Include: |
| 19 | - A detailed description of the vulnerability | 19 | - A detailed description of the vulnerability |
| 20 | - Steps to reproduce | 20 | - Steps to reproduce |