Audience: contributors. Work through every section before marking a pull request ready for review. Each item maps to a gate CI enforces or a manual check that automated tooling cannot replace. A PR that skips a section without a written reason will be asked to revisit it.
- Copy the PR evidence block into your pull request description.
- Run the automated gates locally (they are the same commands CI runs).
- Walk each manual section that touches code changed by your PR.
- If a section is not applicable, note why in the PR description — do not silently skip it.
Run these commands locally before pushing. They are identical to what the CI workflow executes on every PR and main push.
npm run format:check # Prettier — confirms all files match the enforced style
npm run lint # ESLint — no errors allowed
npm run build # tsc -b + Vite production bundle — must exit 0
npm run test # Vitest — all tests must pass| Gate | Command | Pass condition |
|---|---|---|
| Format | npm run format:check |
No diff reported; all files match Prettier output |
| Lint | npm run lint |
Zero ESLint errors (warnings are allowed but should be reviewed) |
| Type-check + Build | npm run build |
TypeScript exits 0; dist/ is produced with no errors |
| Unit tests | npm run test |
All Vitest suites green; no test exits with a non-zero code |
If any gate fails, fix it before requesting review. See TESTING.md for how to write and run tests, and RELEASE_CHECKLIST.md for a condensed operator view.
- No
anytypes introduced without a comment explaining whyunknownor a narrower type is not usable. - Public component props have explicit TypeScript interfaces — no inline object literals used as the only type definition.
- API boundary types come from
src/api/types.ts(or the generated filesrc/api/generated.ts); local duplications are removed. See Architecture Overview.
// ✅ correct — use named types from the API layer
import type { Bond } from '@/api/types'
// ❌ avoid — parallel local definition that can drift from the spec
type Bond = { id: string; amount: number }- If you changed or added a public component prop, update
docs/COMPONENTS.mdand the matching Storybook story. - No prop-drilling beyond two levels — new cross-component state belongs in a context or a hook. See STATE_MANAGEMENT.md.
No hard-coded colours, spacing, radii, or shadow values. Use CSS custom properties or the Tailwind config aliases.
// ✅ correct
<div className="bg-[var(--credence-surface-1)] rounded-[var(--credence-radius-md)]">
// ❌ avoid
<div style={{ background: '#1a1a2e', borderRadius: '8px' }}>See DESIGN_TOKENS.md for the full token reference.
Complete the flows below in a local dev session (npm run dev → http://localhost:5173). Test both the happy path and at least one error or edge case for each flow your PR touches.
- Connect wallet — open the app, click "Connect Wallet", complete the Freighter (or stub) flow, and confirm the connected address appears in the header.
- Network label — the UI shows the current network name (e.g. "Testnet") and warns visually when the wallet is on an unexpected network. See WALLET_INTEGRATION.md.
- Disconnect — clicking disconnect clears the address and returns the UI to the disconnected state without a page reload.
- Create bond — fill in amount and duration, submit, and verify the bond transitions from "Pending" to "Active". Real example entry point:
src/pages/Bond.tsx→CreateBondPage. - Withdrawal confirm — initiate a withdrawal, confirm in the dialog, and verify the bond moves to "Withdrawn". Check that the
ConfirmDialogtraps focus and closes on Escape.
// The confirm dialog lives at src/components/ConfirmDialog.tsx
// A minimal render to verify locally:
import { ConfirmDialog } from '@/components/ConfirmDialog'- Slash calculation — if the withdrawal is early, verify the penalty amount displays correctly. Penalty logic lives in
src/lib/penalty.ts.
- Address lookup — enter a valid Stellar address (
G…, 56 chars) and confirm a score and tier are returned. UseisValidStellarAddressfromsrc/lib/stellar.tsto pre-validate inputs. - Invalid address — enter a malformed address and confirm the error state is shown without crashing.
// Valid address format for manual testing:
// GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWNA
import { isValidStellarAddress } from '@/lib/stellar'
isValidStellarAddress('GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWNA') // → true- Clear
credence:onboarding:stepandcredence:onboarding:onboardedAtfromlocalStorage, reload, and verify the tour launches for a connected user. - Skip the tour and confirm the skip is persisted so the tour does not re-appear on next load.
- Trigger a network error (disable the backend or use DevTools → Network → Offline) and confirm an
ErrorStatecomponent is shown, not a blank screen or uncaught exception. - Verify empty-state copy matches the tone guide. See COPY_TONE.md and UI_STATES_GUIDE.md.
Run the relevant subset of the ACCESSIBILITY.md checklist for every UI surface touched by the PR.
- Start the dev server and navigate to every route changed by the PR.
- Run axe DevTools (browser extension) or the Storybook a11y panel against each changed component state.
- Zero critical or serious violations. Accepted false positives must be documented in the PR with: selector, rule ID, and reason it is not actionable.
- Navigate from the browser address bar using only Tab / Shift+Tab / Enter / Space / Escape / Arrow keys.
- Focus order matches the visible reading order for every form and dialog touched.
- Every interactive element has a visible focus ring (not
outline: nonewithout a replacement). - Modals and drawers trap focus while open and return focus to the opener on close.
Real interaction paths to verify (walk only the ones your PR touches):
| Path | Key interactions |
|---|---|
| Bond creation | Tab through Amount → Duration → Submit; Enter submits the form |
| Confirm dialog | Escape closes and returns focus to "Withdraw" button |
| Wallet modal | Escape closes; focus returns to "Connect Wallet" |
| Mobile nav | Enter/Space opens drawer; Escape closes; focus returns to hamburger |
See keyboard-interactions.md and focus-patterns.md.
- Page title and
<h1>identify the current view (e.g.Bond · Credence/ "Active Bonds"). - Form labels, helper text, and inline errors are announced in a useful order.
- Toast notifications and async status changes announce through a live region without repeating.
- Icon-only buttons use
aria-labelthat describes the action, not the icon ("Close modal"not"X").
- Normal text: ≥ 4.5:1 contrast ratio.
- Large text and meaningful non-text UI: ≥ 3:1.
- Status and validation states do not rely on color alone — pair color with an icon or label.
- All new color values use design tokens (
var(--credence-*)) not hard-coded hex.
- Animations are absent or duration-reduced when
prefers-reduced-motion: reduceis set. See motion-guidelines.md. - Semi-transparent overlays switch to fully opaque when
prefers-reduced-transparency: reduceis set. Components must use the--credence-backdrop-*tokens so the global override insrc/index.cssfires automatically.
Open DevTools device emulation and verify your changes at these three widths:
| Breakpoint | Width | Key checks |
|---|---|---|
| Mobile | 360 px | No horizontal scroll; tap targets ≥ 44 × 44 px; mobile nav hamburger present |
| Tablet | 768 px | Layout transitions correctly; desktop nav not yet fully visible |
| Desktop | 1280 px | Desktop nav visible; no oversized whitespace |
See RESPONSIVE.md for the full breakpoint contract.
- Toggle between light and dark themes via the
ThemeTogglein the header. - All text, icons, and interactive states remain legible in both themes.
- Focus rings, hover states, and validation colours are visible in both themes.
- No component hardcodes a color that breaks under the opposite theme.
The theme switches by writing data-theme="dark" on <html>. Use [data-theme="dark"] selectors in CSS, not JavaScript class toggles. See dark-mode.md.
If you changed openapi.yaml or any type in src/api/:
- Run
npm run generate:apiand commit the updatedsrc/api/generated.ts. - Verify
src/api/types.tsre-exports are still correct and no named alias was silently removed. - Confirm consumers that import from
src/api/types.tsstill type-check (npm run build).
See API_TYPES.md for the full codegen workflow.
If you touched USDC display or Stellar address logic:
- Use
formatUsdc/normalizeUSDC/sanitizeUSDCInputfromsrc/lib/format.ts— do not add a local copy. - Use
isValidStellarAddress/truncateAddressfromsrc/lib/stellar.ts. - Run the utility-specific tests to confirm no regression:
npm run test -- --run src/lib/format.test.ts src/lib/stellar.test.ts- No secret values added to
VITE_*env variables — Vite env vars are bundled into the browser build and are public. Configuration intent only (URLs, feature flags). See SECURITY.md. - No new inline
Content-Security-Policybypass (unsafe-inline,unsafe-eval) without a maintainer sign-off. See SECURITY_HEADERS.md. - Cookie secrets rotation procedure is unaffected, or COOKIE_SECRETS.md is updated.
If you added or modified a dashboard widget:
- The widget uses
useWidgetCache<T>(key, fetchFn)with a stable, namespaced key (e.g.'bond:active-bonds'). - The key is declared as a constant in
src/config/widgetCache.ts, not an inline string literal. - A
<WidgetRefreshButton>is wired up so users can manually re-fetch without refreshing the page. - The refresh button has an accessible
labelprop (label="active bonds"→ announced as "Refresh active bonds").
// Real usage from the docs — verify yours matches this pattern
import { useWidgetCache } from '../widgetCache'
import { WidgetRefreshButton } from '../components/widget'
const bondsWidget = useWidgetCache<BondRow[]>('bond:active-bonds', fetchActiveBonds)
return (
<header>
<h2>Active Bonds</h2>
<WidgetRefreshButton
onRefresh={bondsWidget.refresh}
isLoading={bondsWidget.isLoading}
lastUpdated={bondsWidget.lastUpdated}
label="active bonds"
/>
</header>
)See widget-cache.md for the full API and token-driven styling notes.
Paste this block into your PR description and fill in each line. Write "n/a — [reason]" if a section does not apply.
QA checklist:
- Automated gates (format / lint / build / test):
- Functionality smoke test (flows touched):
- Accessibility — axe scan:
- Accessibility — keyboard nav:
- Accessibility — screen reader:
- Responsive layout (360 / 768 / 1280):
- Theme parity (dark + light):
- API types / codegen (if changed):
- Formatting utilities (if changed):
- Security / secrets review:
- Widget cache (if changed):
| Document | When to read it |
|---|---|
| TESTING.md | Writing Vitest tests, mocking matchMedia / localStorage / clipboard |
| RELEASE_CHECKLIST.md | Condensed operator view of automated + manual gates |
| ACCESSIBILITY.md | Full axe, keyboard, screen reader, and contrast standards |
| ARCHITECTURE.md | Provider tree, data flow seams, API layer boundaries |
| DESIGN_TOKENS.md | --credence-* CSS variable reference |
| COMPONENTS.md | Props, Storybook stories, and accessibility notes for shared UI |
| STATE_MANAGEMENT.md | When to use context vs. local state vs. URL params |
| COPY_TONE.md | How to phrase success, error, empty, and loading copy |
| WALLET_INTEGRATION.md | useWallet API and connection state machine |
| widget-cache.md | useWidgetCache hook and WidgetRefreshButton API |
| keyboard-interactions.md | Expected keyboard behaviour for every interactive component |
| focus-patterns.md | Focus-restore contract and patterns for dialogs and drawers |
| motion-guidelines.md | Reduced-motion token strategy and animation best practices |
| RESPONSIVE.md | Breakpoint contract and layout rules |
| dark-mode.md | Theming mechanics and data-theme CSS scoping |
| SECURITY.md | Security practices and secret handling rules |
| API_TYPES.md | OpenAPI codegen workflow and type re-export conventions |