# Cove — validation record

Worker-side validation of the Cove lending simulation. This is the contributor's own report, not an independent certification. Everything below was actually run in this workspace on 2026-09-26; anything not run is listed as unperformed.

## 1. Scope and assumptions

- **Product:** single-page React app with hash routes `#/` (dashboard), `#/markets`, `#/about`. Primary task: supply ETH, borrow USDC, repay USDC, withdraw ETH against a simulated position. Reset-demo dialog. Illustrative reference markets (ETH, USDC, WBTC; WBTC view-only).
- **Reviewed export:** `dist/` produced by the final `npm run build` (`dist/assets/index-BW2MQ6_J.js`, `dist/assets/index-Dtozmq7Q.css`). Screenshots and the browser recheck below describe that export.
- **Inferred choices:** light theme only; system font stack (no bundled web font); amounts stored as integer micro-units and formatted by string arithmetic; the review/confirmation step is inline in the action panel (not a dialog); the only dialog is the destructive reset confirmation; activity entries carry a sequence, not a timestamp, to keep the demo deterministic; interest does not accrue.
- **Excluded by the brief:** wallet connection, signatures, RPC, contracts, analytics, live prices, dark mode, localization.
- **Preview mechanism:** the guide's `test/scratch/browser/preview.json` launcher does not exist in this workspace, and the browser tool blocks `file:` URLs. The finished `dist/` was served to the assigned Playwright MCP browser through a Playwright route registered inside the tool's own process (`context.route('http://cove.local/**')` fulfilling from `dist/`), i.e. at the subpath `http://cove.local/preview/`. No shell server was started. The route was owned by the browser tool session and closed with it.

## 2. Coverage (Better Interface, six domains)

| Domain | Status | Inspected states and evidence | Unperformed subchecks |
| --- | --- | --- | --- |
| Accessibility | Checked | Accessibility tree names/roles for header, nav, tablist/tabs/tabpanel, amount textbox with label and `aria-describedby`, Max button name, dialog; keyboard walk: skip link is first Tab stop and visible when focused; skip → `main`; Tab into tablist (one stop), arrow keys move selection and focus, Tab to input, Enter submits, Tab/Tab/Enter confirms; focus moves to the review heading, then to the done heading, then back to the input on Done; errors set `aria-invalid="true"`, add the error id to `aria-describedby`, and refocus the input; polite `role="status"` region text updated after each action and after reset; reset dialog: focus lands on Cancel, Escape closes and returns focus to the header button, confirm closes and returns focus; focus ring visible on skip link, tab, input wrapper (`:focus-within`), buttons, headings (screenshots). No horizontal overflow at 320px. | Screen-reader session; browser-native 200% zoom; forced-colors rendering; automated accessibility scan; physical touch device; reduced-motion rendering (CSS guard present, not toggled in the browser). |
| Layout | Checked | Rendered at 1280×800, 960×800/700, 700×800/400, 390×844 (full page) and 320×568 (full page). Reading order = DOM order (position → actions → markets). Card grouping by space then hairline; controls distinct from content; sticky header and sticky action panel at ≥60rem. Overflow scan (`getBoundingClientRect().right > innerWidth`) returned 0 elements at 390 and 320 on the final build. | RTL mirror; widths above 1280; container queries not used. |
| Writing | Checked (source) | Verb-first buttons (`Review supply`, `Confirm borrow`, `Done`, `Reset demo`, `Cancel`); sentence case; consistent terms (Supply/Borrow/Repay/Withdraw, loan-to-value, health factor); errors state the fix and the exact limit next to the field; empty activity state points forward; placeholder `0.00` is not the label; simulation disclosures in header, notice, panel, review, success and footer. | None required (source review is sufficient for this domain). |
| Typography | Checked | Heading sizes descend (22 → 17 → 13px); body 15px, UI 13px, badges 12px, input 20px; tabular figures on all changing values; balance/pretty wrapping on headings/paragraphs; measure capped at 68ch/60ch; wrapping and truncation observed at every width listed above (see findings 1 and 12). | Native zoom text-enlargement; pseudo-localization. |
| Colors | Checked | Rendered pairs measured in Chromium with a WCAG 2 luminance ratio on the actual background (canvas-resolved `oklch` values). Final measurements: secondary text on white 6.21:1; tertiary/placeholder on white 5.16:1 and on inset 4.63:1; primary button text 6.94:1; accent badge 8.45:1; neutral badge 5.58:1; success badge 6.42:1; footer 5.89:1; link 9.09:1; focus ring 5.19:1 on white, 4.66:1 on inset, 4.83:1 on accent-soft; LTV fill 6.23:1 and mark 5.58:1 on track; control borders 3.15:1. Tokens split into primitives and roles; one meaning per hue. | Warning/danger badge pairs were not rendered (they appear only at health < 1.5, which the demo cannot reach with the 70% cap); APCA values not computed; P3 gamut not checked. |
| UI | Checked | Concentric radius on the tab track; one card shadow; scale 0.96 press feedback with reduced-motion off-switch; 120–180ms transitions on named properties only; icons `currentColor` with stroke matched to text weight; hover gated by `@media (hover: hover)`; empty/error/review/done states walked. | Animations replayed at 10% speed; hover states not screenshotted (headless). |

## 3. Findings and fixes

Severity per the pinned guide. Locations are in the final source. Each fix was rebuilt and rechecked in the browser unless noted.

1. **MEDIUM · Layout/Typography — stat values broke mid-number.** `src/styles/components.css:342` (`.stat-grid`), `:352` (`.stat__number`), `:358` (`.stat__unit`), `src/styles/tokens.css:128` (`--panel-width`). Evidence: “0.00 / USDC” at 320, “0.00 / ETH” at 960, “1,000.00 USD / C” at 1280 (screenshots). Fix: auto-fit columns `minmax(8.5rem, 1fr)`, non-breaking number span, unit as a non-breaking inline block, panel narrowed 380 → 360px. Recheck: bounding boxes show number and unit on one line at 1280 for all stats; 4/3/2/1 columns at 1280/960/390/320.
2. **MEDIUM · Colors — tertiary text and placeholder below 4.5:1.** `src/styles/tokens.css:19` (`--gray-500`). Measured 4.24:1 on white at 15px (wallet USD parenthetical) and placeholder. Fix: lightness 0.58 → 0.535. Recheck: 5.16:1 on white, 4.63:1 on `--gray-100`.
3. **MEDIUM · Colors/UI — control boundaries at 1.53:1.** `src/styles/tokens.css:18,69` (`--gray-400`, `--color-border-control`), applied at `src/styles/components.css:721` (`.btn--secondary`) and `:849` (`.amount`). Fix: dedicated control-border token at L 0.655. Recheck: 3.15:1 on white.
4. **MEDIUM · Layout — market table clipped inside its scroll container on mobile with no cue.** `src/styles/components.css:599` (stacked table rules), `src/components/MarketTable.tsx` (`data-label` on cells). Evidence: at 320 the Price column ended at the card edge with Supply/Borrow/Status hidden; overflow scan listed the table. Fix: below 40rem rows stack and each cell prints its label. Recheck: 0 overflowing elements at 390 and 320; full-page screenshots show every column as a labelled row.
5. **MEDIUM · Layout — header clipped the reset button at ~700px; three rows at 320.** `src/styles/components.css:149` (header breakpoint and ordering). Fix: wrap below 48rem, reset action joins the brand row, nav wraps. Recheck: 700×400 screenshot shows brand + reset on row one, nav on row two, nothing clipped; at 320 the nav wraps to two lines and the reset sits under the brand.
6. **MEDIUM · UI/Accessibility — stale action panel after Reset demo.** `src/state/useDemo.ts:16,36` (`epoch`), `src/App.tsx:64` (`key={state.epoch}`). Evidence: after reset the Withdraw tab, the typed “1” and its error remained. Fix: remount the panel on reset. Recheck: after reset the selected tab is Supply, the input is empty and no error is shown; focus returned to the header button.
7. **LOW · Writing — “No debt” shown twice** (value and badge). `src/components/PositionSummary.tsx:76-81`. Fix: with no debt the sub-line reads “Liquidation at 80%” and the band badge appears only when debt exists. Rechecked in screenshots.
8. **LOW · Colors — market role badges used the success green** that also means “Healthy”. `src/components/MarketTable.tsx:71`. Fix: default accent badge for interactive roles; neutral for view-only. Recheck: 8.45:1.
9. **LOW · Layout — missing section gap/divider** between the LTV legend and the wallet section, and before “How to read this table”. `src/components/PositionSummary.tsx:53,103,120`, `src/pages/MarketsPage.tsx:22,25`, `src/styles/components.css:398`. Fix: wrap groups in `.card__section`. Rechecked in screenshots.
10. **LOW · Writing — rounded limit in error text.** `src/lib/format.ts:30` (`fmtEth`). Evidence: “Withdrawable: 0.5238 ETH” while the exact limit was 0.523809, so 0.52381 would be rejected with a message naming a figure the user had not exceeded. Fix: ETH shown with up to 6 decimals. Recheck: “0.523809 ETH” in the wallet list and error/limit text.
11. **LOW · Writing — projected LTV “0.00%” when withdrawing all collateral with debt outstanding.** `src/lib/format.ts:62` (`ltvLabel`). Fix: label “No collateral” for debt with zero collateral. Verified in source and by typecheck; the exact rendered preview was not re-exercised after the fix.
12. **LOW · Layout — key/value terms wrapped when a projected delta was wide.** `src/styles/components.css:447` (`.kv` → `auto 1fr`), `:481` (`.delta__to`, arrow grouped with the target value). Recheck: over-limit supply preview at 1280 shows “1.00 ETH → 1,001.00 ETH” on one line with an unwrapped term.

Observation, not a defect: in headless Chromium, pressing Tab past the last dialog button moves focus out of the page (browser chrome) rather than wrapping, while the background stays inert; this is native `showModal()` behaviour. Escape and both buttons behave correctly.

## 4. Verification

### Commands (all exit 0)

```
npm install --no-audit --no-fund            # 69 packages (+ @types/node)
npm run typecheck                           # tsc -p tsconfig.json --noEmit && tsc -p tsconfig.node.json --noEmit
npm test                                    # node --test test/scratch/*.test.ts → 7 pass, 0 fail
npm run build                               # vite v6.4.3 → dist/index.html 0.77 kB, index-Dtozmq7Q.css 20.31 kB, index-BW2MQ6_J.js 252.52 kB
```

Unit tests (scratch, not shipped) cover: decimal parsing and rejection of junk/negative/over-precise input; exact string formatting; the acceptance scenario; 70% LTV caps on borrow and withdraw; repay capped by wallet and debt; full round trip back to the initial state; `applyAction` refusing invalid amounts.

### Browser (Playwright MCP 0.0.82, headless Chromium, assigned tool)

- Viewports: 1280×800 (desktop), 960×800 and 960×700 (grid breakpoint), 700×800 and 700×400 (header breakpoint), 390×844 (mobile, full page), 320×568 (reflow, full page).
- Console: 0 errors, 0 warnings across the session. Network: `index.html`, JS, CSS and `favicon.svg` all 200 on every load; no other requests.
- Primary scenario on the final build (mouse and keyboard variants): supply 1 ETH → review → confirm; borrow 1,000 USDC → review → confirm. Position read from the DOM: Collateral 1.00 ETH / $3,000.00; Debt 1,000.00 USDC / $1,000.00; Loan-to-value 33.33%; Health factor 2.40 (Healthy); wallet 1.00 ETH and 2,000.00 USDC; available to borrow 1,100.00 USDC; withdrawable 0.523809 ETH; activity lists both actions; status region: “Borrowed 1,000.00 USDC (simulated). Loan-to-value 33.33%, health factor 2.40.”
- Validation/recovery: borrow 5000 → “Borrowing that much would exceed the 70% maximum loan-to-value. You can borrow up to 1,100.00 USDC.”; `abc` → “Enter a number using digits and at most one decimal point, for example 0.5.”; repay 3000 → “The demo wallet holds 1,000.00 USDC. Enter that amount or less.”; withdraw 1 → “Withdrawing that much would push loan-to-value above 70%. You can withdraw up to 0.5238 ETH, or repay USDC first.” (before finding 10; the limit now renders as 0.523809 ETH); supply 1000 with 1 ETH in the wallet → “The demo wallet holds 1.00 ETH. Enter that amount or less.” In each case `aria-invalid="true"`, `aria-describedby` includes the error id and focus is on the input. Max fills the exact limit and refocuses the input.
- Repay and withdraw: repay 400 → debt 600.00 USDC, wallet 1,600.00 USDC, LTV 20.00%, health 4.00, available 1,500.00 USDC, withdrawable 0.714285 ETH; withdraw 0.5 → collateral 0.50 ETH ($1,500.00), wallet 1.50 ETH, LTV 40.00%, health 2.00, available 450.00 USDC, withdrawable 0.214285 ETH; activity lists four entries newest first.
- Reset: dialog opens with focus on Cancel; Escape closes and returns focus to the header “Reset demo” button; confirming resets to 2.00 ETH / 1,000.00 USDC with no collateral or debt, empties the activity list, resets the action panel, announces the reset and returns focus to the trigger.
- Routes: `#/markets` and `#/about` render, `aria-current="page"` follows the route, focus moves to `main` on navigation.
- Screenshots (final build): `artifacts/desktop.png` (1280×800 viewport, after the acceptance scenario) and `artifacts/mobile.png` (390 wide, full page, same state). Intermediate screenshots were inspected in the tool and not kept.

## 5. Limitations

- No screen-reader session, no browser-native zoom, no forced-colors or reduced-motion emulation, no automated accessibility scan, no RTL render, no physical device: recorded as unperformed above.
- Finding 11 was verified in source only.
- Warning/danger health badges cannot be reached in the demo (70% cap), so their rendered pairs are unmeasured.
- Publication: a public GitHub source push and a hosted IPFS preview were requested. Neither was performed: the workspace has no `gh` CLI, no Git remote, no GitHub credentials and no IPFS tooling or pinning credentials. The README documents how to publish the committed `dist/`.

## 6. Completion

**Complete for the stated build-and-review scope** (usable site, working primary interactions, responsive layouts, committed `dist/` with relative asset URLs, typecheck/build/tests run, browser review against the six domains with fixes rechecked, DESIGN.md and delivery documentation). **Blocked:** GitHub and IPFS publication, for lack of credentials and tooling in this environment.
