audit-react-vite-tsx-codebase
Perform a single critical audit pass on a React + Vite + TypeScript (TSX) codebase folder. Designed to be re-invoked until convergence (zero remaining issues).
Instructions
You are performing a single audit pass on the React/Vite/TypeScript codebase at $ARGUMENTS. This skill is idempotent and convergent — each invocation examines the current state, fixes what it can, and reports what remains.
Phase 0: Baseline Snapshot
- Run the TypeScript compiler in check mode:
npx tsc --noEmit— capture error count. - Run the linter:
npm run lint(ornpx oxlint/npx eslint .) — capture violation count. - Run tests if they exist:
npx vitest run --reporter=verbose— capture pass/fail count. - Check build:
npm run build— does it succeed? - Record all counts — you will compare at the end.
Phase 1: TypeScript Strictness & Type Safety
Ensure the project uses strict TypeScript and every value is properly typed.
tsconfig.json requirements:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
Rules:
- No
anytype — useunknownwith type guards, or define proper types. - No
// @ts-ignoreor// @ts-expect-errorwithout a comment explaining why. - Every function has explicit parameter types and return types.
- Every component has a typed
Propsinterface (not inline object types). - Event handlers are typed:
React.MouseEvent<HTMLButtonElement>, notany. - API responses have proper interfaces (not
anyor untyped JSON). - Use discriminated unions for state that can be in multiple shapes.
- Prefer
interfacefor object shapes (extendable),typefor unions/intersections. - Use
satisfiesoperator where appropriate for type narrowing without widening. - Run
npx tsc --noEmit— target zero errors.
Pattern to enforce:
// Props interface above component
interface UserCardProps {
user: User;
onSelect: (userId: string) => void;
variant?: "compact" | "full";
}
// Explicit return type on components with complex returns
export function UserCard({ user, onSelect, variant = "full" }: UserCardProps): React.ReactElement {
// ...
}
Phase 2: Component Documentation (JSDoc + TSDoc)
Module/File-level: Every .tsx/.ts file with exports gets a top-of-file JSDoc comment.
/**
* @module PipelineStatus
* @description Real-time pipeline execution dashboard with SSE-driven updates.
* Displays agent progress, HITL gates, and final decision rendering.
*/
Component documentation:
/**
* Displays the coverage determination result with approval/denial reasoning.
*
* @remarks
* Subscribes to the pipeline SSE stream and updates in real-time.
* Renders a skeleton loader until the first event arrives.
*
* @example
* ```tsx
* <DecisionCard caseId="PA-2024-001" />
* ```
*/
export function DecisionCard({ caseId, onAppeal }: DecisionCardProps): React.ReactElement {
Hook documentation:
/**
* Manages WebSocket connection for HITL gate interactions.
*
* @param caseId - Active case identifier
* @param onGateReached - Callback when pipeline hits a HITL gate
* @returns Connection state, gate data, and approval/rejection actions
*
* @example
* ```tsx
* const { gateData, approve, reject } = useHITLGate("PA-001", handleGate);
* ```
*/
export function useHITLGate(caseId: string, onGateReached: GateCallback): HITLGateHook {
Rules:
- Every exported component, hook, utility function, and type gets a JSDoc block.
- Include
@remarksfor non-obvious behavior (side effects, subscriptions, performance). - Include
@examplewith a usage snippet for components and hooks. - Include
@paramand@returnsfor hooks and utility functions. - Internal/private helpers get at minimum a one-line
/** ... */comment. - Type interfaces get
/** */on the interface AND on non-obvious properties.
Phase 3: Component Architecture & React Best Practices
Rules for component structure:
| Principle | Smell | Action |
|---|---|---|
| Single Responsibility | Component > 150 LOC or renders unrelated sections | Extract sub-components |
| Separation of concerns | Business logic mixed with rendering | Extract to custom hooks |
| DRY | Same JSX pattern in 3+ places | Extract shared component |
| Pure components | Component re-renders without prop changes | Add React.memo or fix parent |
| Colocation | Related files scattered across folders | Co-locate component + hook + test + types |
| Composition over config | Component with 10+ boolean props | Use compound component or slots pattern |
Specific checks:
- No direct DOM manipulation (
document.querySelector) — use refs. - No
useEffectfor derived state — compute inline or useuseMemo. - No
useEffectfor event responses — handle in the event handler. useEffectdependencies are correct and exhaustive.- No
eslint-disableon exhaustive-deps without justification. - State that is always set together lives in one
useStateoruseReducer. - Expensive computations are wrapped in
useMemowith correct deps. - Event handlers passed to children are wrapped in
useCallbackwhen children are memoized. - No prop drilling beyond 2 levels — use context or composition.
- Keys on lists are stable and unique (not array index unless list is static).
Folder structure enforcement:
src/
├── components/ # Reusable UI components
│ └── ComponentName/
│ ├── ComponentName.tsx
│ ├── ComponentName.test.tsx
│ ├── useComponentLogic.ts # (if complex logic)
│ └── index.ts
├── hooks/ # Shared custom hooks
├── pages/ # Route-level components
├── services/ # API layer
├── types/ # Shared TypeScript types
├── utils/ # Pure utility functions
└── constants/ # App-wide constants
Phase 4: Error Handling & Error Boundaries
Rules:
- Every page/route has an Error Boundary wrapping it.
- API calls have proper error handling (try/catch or
.catch()). - Loading and error states are handled for every async operation (not just happy path).
- User-facing errors show meaningful messages (not raw error strings or stack traces).
- No unhandled promise rejections — every
.then()has a.catch()or isawait-ed in a try/catch. - Network failures are retried or gracefully degraded (not silent failures).
- Form validation shows inline errors, not just console logs.
Pattern to enforce:
// Error boundary at route level
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<PageSkeleton />}>
<PipelineDashboard />
</Suspense>
</ErrorBoundary>
// Async state pattern
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
Custom error types:
export class APIError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly endpoint: string,
) {
super(message);
this.name = "APIError";
}
}
Phase 5: Accessibility (a11y)
Rules:
- Every interactive element is keyboard-accessible (Tab, Enter, Escape).
- Every image has an
altattribute (emptyalt=""for decorative images). - Form inputs have associated
<label>elements (oraria-label). - Color is never the only way to convey information (add icons/text).
- Focus management on route changes and modal opens.
- ARIA roles on custom interactive widgets (tabs, accordions, dialogs).
- Semantic HTML:
<button>for actions (not<div onClick>),<nav>,<main>,<section>. - Sufficient color contrast (WCAG AA: 4.5:1 for text, 3:1 for large text).
- No
tabIndex> 0 (disrupts natural tab order).
Phase 6: Performance & Bundle Hygiene
Rules:
- No unused imports or dead code (tree-shaking can't save everything).
- Heavy dependencies are lazy-loaded:
React.lazy()+Suspense. - Images use proper formats (WebP/AVIF) and are sized appropriately.
- Lists with 50+ items use virtualization (
@tanstack/react-virtualor similar). - No synchronous blocking in render (heavy computation → Web Worker or
useMemo). - Bundle size check:
npx vite-bundle-visualizer— flag anything > 50KB that could be lazy-loaded. - No barrel file re-exports that defeat tree-shaking (
export * from). - CSS: Tailwind utility classes only (per project constraint) — no unused custom CSS.
Phase 7: Testing (Vitest + Testing Library)
Rules:
- Run
npx vitest run --coverageto identify untested components. - Test behavior, not implementation: "when user clicks X, Y appears" — not "useState was called".
- Use
@testing-library/react— query by role/label/text, not test-ids (unless necessary). - Every component with conditional rendering needs tests for each branch.
- Every custom hook gets a test via
renderHook. - API integration tests mock at the network level (
msw), not at the module level. - Shared test utilities go in a
test-utils.tsxthat re-exports from@testing-library/reactwith providers. - Snapshot tests are acceptable ONLY for small, stable components — prefer explicit assertions.
Pattern:
// test-utils.tsx
function renderWithProviders(ui: React.ReactElement, options?: RenderOptions) {
return render(ui, {
wrapper: ({ children }) => (
<QueryClientProvider client={testQueryClient}>
<ThemeProvider>{children}</ThemeProvider>
</QueryClientProvider>
),
...options,
});
}
// Component.test.tsx
describe("DecisionCard", () => {
it("shows approval status when case is approved", async () => {
renderWithProviders(<DecisionCard caseId="PA-001" />);
expect(await screen.findByRole("status")).toHaveTextContent("Approved");
});
it("calls onAppeal when appeal button is clicked", async () => {
const
renderWithProviders(<DecisionCard caseId="PA-001" />);
await userEvent.click(screen.getByRole("button", { name: /appeal/i }));
expect(onAppeal).toHaveBeenCalledWith("PA-001");
});
});
Phase 8: Final Clean Code Gate
Run in sequence — all must pass with zero violations:
npx tsc --noEmit— zero TypeScript errors.npm run lint(ornpx oxlint ./npx eslint .) — zero lint errors.npm run build— builds successfully with no warnings treated as errors.npx vitest run— all tests pass.- Review any
// @ts-expect-errororeslint-disable— each must have justification.
Phase 9: Smoke Tests
npm run dev— dev server starts without errors.- Open in browser — no console errors on initial load.
- Navigate the primary user flow (e.g., submit a case → see pipeline → get decision).
- Check for visual regressions (layout, responsiveness at mobile/desktop breakpoints).
- Check network tab — no failed requests in happy path.
npm run build && npm run preview— production build serves correctly.- Compare final tsc/lint counts against Phase 0 baseline.
Output: Audit Scorecard
End every invocation with this exact format:
── Audit Scorecard ─────────────────────────────────────────
Pass: N (where N is which re-invocation this is, default 1)
Folder: <folder_path>
tsc errors: [before] → [after]
lint violations: [before] → [after]
missing docs: [before] → [after]
test coverage: [before]% → [after]%
tests passing: [before] → [after]
build status: [PASS/FAIL] → [PASS/FAIL]
a11y issues: [before] → [after]
Issues fixed this pass: X
Issues remaining: Y
Items needing human decision:
- [list each item with file:line and reason]
Recommendation: RE-RUN | CONVERGED ✓
────────────────────────────────────────────────────────────
Recommendation logic:
RE-RUNif any issues remain that another pass could fix.CONVERGED ✓if tsc=0, lint=0, all tests pass, build succeeds, and no new issues found.
Important Constraints
- Commit changes at the end of each phase with a message like
audit: phase N — <description>. - If a refactoring might break things, run
npx tsc --noEmitand tests immediately — do not accumulate risk. - Do NOT add features, new routes, or speculative abstractions.
- Do NOT delete tests or weaken assertions to make them pass.
- Do NOT change the build tooling (Vite config, Tailwind setup) unless it's broken.
- Respect the existing Tailwind-only CSS constraint — do NOT introduce CSS modules or styled-components.
- If unsure whether a change is safe, flag it in "Items needing human decision" and skip it.
- Preserve existing component APIs (props interfaces) — changing them is a breaking change for consumers.