@clipboard-health/ai-rules 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1037 +1 @@
1
- <!-- Source: .ruler/common/codeStyleAndStructure.md -->
2
-
3
- # Code style and structure
4
-
5
- - Write concise, technical TypeScript code with accurate examples.
6
- - Use functional and declarative programming patterns.
7
- - Prefer iteration and modularization over code duplication.
8
- - Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
9
- - Structure files: constants, types, exported functions, non-exported functions.
10
- - Avoid magic strings and numbers; define constants.
11
- - Use camelCase for files and directories (e.g., modules/shiftOffers.ts).
12
- - When declaring functions, use the `function` keyword, not `const`.
13
- - Files should read from top to bottom: `export`ed items live on top and the internal functions and methods they call go below them.
14
- - Prefer data immutability.
15
-
16
- # Commit messages
17
-
18
- - Follow the Conventional Commits 1.0 spec for commit messages and in pull request titles.
19
-
20
- <!-- Source: .ruler/common/errorHandlingAndValidation.md -->
21
-
22
- # Error handling and validation
23
-
24
- - Sanitize user input.
25
- - Handle errors and edge cases at the beginning of functions.
26
- - Use early returns for error conditions to avoid deeply nested if statements.
27
- - Place the happy path last in the function for improved readability.
28
- - Avoid unnecessary else statements; use the if-return pattern instead.
29
- - Use guard clauses to handle preconditions and invalid states early.
30
- - Implement proper error logging and user-friendly error messages.
31
- - Favor `@clipboard-health/util-ts`'s `Either` type for expected errors instead of `try`/`catch`.
32
-
33
- <!-- Source: .ruler/common/testing.md -->
34
-
35
- # Testing
36
-
37
- - Follow the Arrange-Act-Assert convention for tests with newlines between each section.
38
- - Name test variables using the `mockX`, `input`, `expected`, `actual` convention.
39
- - Aim for high test coverage, writing both positive and negative test cases.
40
- - Prefer `it.each` for multiple test cases.
41
- - Avoid conditional logic in tests.
42
-
43
- <!-- Source: .ruler/common/typeScript.md -->
44
-
45
- # TypeScript usage
46
-
47
- - Use strict-mode TypeScript for all code; prefer interfaces over types.
48
- - Avoid enums; use const maps instead.
49
- - Strive for precise types. Look for type definitions in the codebase and create your own if none exist.
50
- - Avoid using type assertions like `as` or `!` unless absolutely necessary.
51
- - Use the `unknown` type instead of `any` when the type is truly unknown.
52
- - Use an object to pass multiple function params and to return results.
53
- - Leverage union types, intersection types, and conditional types for complex type definitions.
54
- - Use mapped types and utility types (e.g., `Partial<T>`, `Pick<T>`, `Omit<T>`) to transform existing types.
55
- - Implement generic types to create reusable, flexible type definitions.
56
- - Utilize the `keyof` operator and index access types for dynamic property access.
57
- - Implement discriminated unions for type-safe handling of different object shapes where appropriate.
58
- - Use the `infer` keyword in conditional types for type inference.
59
- - Leverage `readonly` properties for function parameter immutability.
60
- - Prefer narrow types whenever possible with `as const` assertions, `typeof`, `instanceof`, `satisfies`, and custom type guards.
61
- - Implement exhaustiveness checking using `never`.
62
-
63
- <!-- Source: .ruler/frontend/custom-hooks.md -->
64
-
65
- # Custom Hook Standards
66
-
67
- ## Hook Structure
68
-
69
- ```typescript
70
- export function useFeature(params: Params, options: UseFeatureOptions = {}) {
71
- const query = useQuery(...);
72
- const computed = useMemo(() => transformData(query.data), [query.data]);
73
- const handleAction = useCallback(async () => { /* ... */ }, []);
74
-
75
- return { data: computed, isLoading: query.isLoading, handleAction };
76
- }
77
- ```
78
-
79
- ## Naming Rules
80
-
81
- - **Always** prefix with `use`
82
- - Boolean: Use `useIs*`, `useHas*`, or `useCan*` (e.g., `useIsEnabled`, `useHasPermission`, `useCanEdit`)
83
- - Data: `useGetUser`, `useFeatureData`
84
- - Actions: `useSubmitForm`
85
-
86
- ## Return Values
87
-
88
- - Return objects (not arrays) for complex hooks
89
- - Name clearly: `isLoading` not `loading`
90
-
91
- ## State Management with Constate
92
-
93
- Use `constate` for shared state between components:
94
-
95
- ```typescript
96
- import constate from "constate";
97
-
98
- function useFilters() {
99
- const [filters, setFilters] = useState<Filters>({});
100
- return { filters, setFilters };
101
- }
102
-
103
- export const [FiltersProvider, useFiltersContext] = constate(useFilters);
104
- ```
105
-
106
- **When to use:** Sharing state between siblings, feature-level state
107
- **When NOT:** Server state (use React Query), simple parent-child (use props)
108
-
109
- ## Hook Patterns
110
-
111
- ```typescript
112
- // Boolean hooks
113
- export function useIsFeatureEnabled(): boolean {
114
- return useFeatureFlags().includes("feature");
115
- }
116
-
117
- // Data transformation
118
- export function useTransformedData() {
119
- const { data, isLoading } = useGetRawData();
120
- const transformed = useMemo(() => data?.map(format), [data]);
121
- return { data: transformed, isLoading };
122
- }
123
-
124
- // Composite hooks
125
- export function useBookingsData() {
126
- const shifts = useGetShifts();
127
- const invites = useGetInvites();
128
-
129
- const combined = useMemo(
130
- () => [...(shifts.data ?? []), ...(invites.data ?? [])],
131
- [shifts.data, invites.data],
132
- );
133
-
134
- return { data: combined, isLoading: shifts.isLoading || invites.isLoading };
135
- }
136
- ```
137
-
138
- ## Best Practices
139
-
140
- - Use options object for flexibility
141
- - Be explicit about dependencies
142
- - API hooks in `api/`, logic hooks in `hooks/`
143
- - Return stable references with `useCallback`/`useMemo`
144
-
145
- <!-- Source: .ruler/frontend/data-fetching.md -->
146
-
147
- # Data Fetching Standards
148
-
149
- ## Technology Stack
150
-
151
- - **React Query** (@tanstack/react-query) for all API calls
152
- - **Zod** for response validation
153
-
154
- ## Core Principles
155
-
156
- 1. **Use URL and query parameters in query keys** - Makes cache invalidation predictable
157
- 2. **Define Zod schemas** for all API requests/responses
158
- 3. **Rely on React Query state** - Use `isLoading`, `isError`, `isSuccess`
159
- 4. **Use `enabled` for conditional fetching**
160
- 5. **Use `invalidateQueries` for disabled queries** - Not `refetch()` which ignores enabled state
161
-
162
- ## Hook Patterns
163
-
164
- ```typescript
165
- // Simple query
166
- export function useGetUser(userId: string) {
167
- return useQuery({
168
- queryKey: ["users", userId],
169
- queryFn: () => api.get(`/users/${userId}`),
170
- responseSchema: userSchema,
171
- enabled: !!userId,
172
- });
173
- }
174
-
175
- // Paginated query
176
- export function usePaginatedItems(params: Params) {
177
- return useInfiniteQuery({
178
- queryKey: ["items", params],
179
- queryFn: ({ pageParam }) => api.get("/items", { cursor: pageParam, ...params }),
180
- getNextPageParam: (lastPage) => lastPage.nextCursor,
181
- });
182
- }
183
-
184
- // Mutations
185
- export function useCreateItem() {
186
- const queryClient = useQueryClient();
187
- return useMutation({
188
- mutationFn: (data: CreateItemRequest) => api.post("/items", data),
189
- onSuccess: () => queryClient.invalidateQueries(["items"]),
190
- });
191
- }
192
- ```
193
-
194
- ## Error Handling
195
-
196
- For detailed error handling strategies, see `error-handling.md`.
197
-
198
- ```typescript
199
- useQuery({
200
- queryKey: ["resource"],
201
- queryFn: fetchResource,
202
- useErrorBoundary: (error) => {
203
- // Show error boundary for 500s, not 404s
204
- return !(axios.isAxiosError(error) && error.response?.status === 404);
205
- },
206
- retry: (failureCount, error) => {
207
- // Don't retry 4xx errors
208
- if (axios.isAxiosError(error) && error.response?.status < 500) return false;
209
- return failureCount < 3;
210
- },
211
- });
212
- ```
213
-
214
- ## Query Keys
215
-
216
- ```typescript
217
- // Include URL and params for predictable cache invalidation
218
- export const queryKeys = {
219
- users: ["users"] as const,
220
- user: (id: string) => ["users", id] as const,
221
- userPosts: (id: string) => ["users", id, "posts"] as const,
222
- };
223
- ```
224
-
225
- ## Conditional Fetching
226
-
227
- ```typescript
228
- const { data } = useQuery({
229
- queryKey: ["resource", id],
230
- queryFn: () => fetchResource(id),
231
- enabled: !!id, // Only fetch when id exists
232
- });
233
- ```
234
-
235
- ## Naming Conventions
236
-
237
- - `useGetX` - Simple queries
238
- - `usePaginatedX` - Infinite queries
239
- - `useCreateX`, `useUpdateX`, `useDeleteX` - Mutations
240
-
241
- ## Hook Location
242
-
243
- Place in `api/` folder within feature directory. One endpoint = one hook.
244
-
245
- <!-- Source: .ruler/frontend/error-handling.md -->
246
-
247
- # Error Handling Standards
248
-
249
- ## React Query Error Handling
250
-
251
- ```typescript
252
- useQuery({
253
- queryKey: ["resource"],
254
- queryFn: fetchResource,
255
- useErrorBoundary: (error) => {
256
- // Show error boundary for 500s, not 404s
257
- return !(axios.isAxiosError(error) && error.response?.status === 404);
258
- },
259
- retry: (failureCount, error) => {
260
- // Don't retry 4xx errors
261
- if (axios.isAxiosError(error) && error.response?.status < 500) return false;
262
- return failureCount < 3;
263
- },
264
- });
265
- ```
266
-
267
- ## Component Error States
268
-
269
- ```typescript
270
- export function DataComponent() {
271
- const { data, isLoading, isError, refetch } = useGetData();
272
-
273
- if (isLoading) return <LoadingState />;
274
- if (isError) return <ErrorState onRetry={refetch} />;
275
-
276
- return <DataDisplay data={data} />;
277
- }
278
- ```
279
-
280
- ## Mutation Error Handling
281
-
282
- ```typescript
283
- export function useCreateDocument() {
284
- return useMutation({
285
- mutationFn: createDocumentApi,
286
- onSuccess: () => {
287
- queryClient.invalidateQueries(["documents"]);
288
- showSuccessToast("Document created");
289
- },
290
- onError: (error) => {
291
- logError("CREATE_DOCUMENT_FAILURE", error);
292
- showErrorToast("Failed to create document");
293
- },
294
- });
295
- }
296
- ```
297
-
298
- ## Validation Errors
299
-
300
- ```typescript
301
- // Zod validation
302
- const formSchema = z.object({
303
- email: z.string().email("Invalid email"),
304
- age: z.number().min(18, "Must be 18+"),
305
- });
306
-
307
- try {
308
- const validated = formSchema.parse(formData);
309
- } catch (error) {
310
- if (error instanceof z.ZodError) {
311
- error.errors.forEach((err) => showFieldError(err.path.join("."), err.message));
312
- }
313
- }
314
- ```
315
-
316
- ## Error Boundaries
317
-
318
- ```typescript
319
- export class ErrorBoundary extends Component<Props, State> {
320
- state = { hasError: false };
321
-
322
- static getDerivedStateFromError(error: Error) {
323
- return { hasError: true, error };
324
- }
325
-
326
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
327
- logEvent("ERROR_BOUNDARY", { error: error.message });
328
- }
329
-
330
- render() {
331
- return this.state.hasError ? <ErrorFallback /> : this.props.children;
332
- }
333
- }
334
- ```
335
-
336
- ## Best Practices
337
-
338
- - **Always handle errors** - Check `isError` state
339
- - **User-friendly messages** - Don't show technical errors
340
- - **Log for debugging** - Include context
341
- - **Provide recovery actions** - Retry, dismiss, navigate
342
- - **Different strategies** - Error boundary vs inline vs retry
343
-
344
- <!-- Source: .ruler/frontend/file-organization.md -->
345
-
346
- # File Organization Standards
347
-
348
- ## Feature-based Structure
349
-
350
- ```text
351
- FeatureName/
352
- ├── api/ # Data fetching hooks
353
- │ ├── useGetFeature.ts
354
- │ ├── useUpdateFeature.ts
355
- │ └── useDeleteFeature.ts
356
- ├── components/ # Feature-specific components
357
- │ ├── FeatureCard.tsx
358
- │ ├── FeatureList.tsx
359
- │ └── FeatureHeader.tsx
360
- ├── hooks/ # Feature-specific hooks (non-API)
361
- │ ├── useFeatureLogic.ts
362
- │ └── useFeatureState.ts
363
- ├── utils/ # Feature utilities
364
- │ ├── formatFeature.ts
365
- │ ├── formatFeature.test.ts
366
- │ └── validateFeature.ts
367
- ├── __tests__/ # Integration tests (optional)
368
- │ └── FeatureFlow.test.tsx
369
- ├── Page.tsx # Main page component
370
- ├── Router.tsx # Feature routes
371
- ├── paths.ts # Route paths
372
- ├── types.ts # Shared types
373
- ├── constants.ts # Constants
374
- └── README.md # Feature documentation (optional)
375
- ```
376
-
377
- ## Naming Conventions
378
-
379
- - Components: `PascalCase.tsx` (`UserProfile.tsx`)
380
- - Hooks: `camelCase.ts` (`useUserProfile.ts`)
381
- - Utils: `camelCase.ts` (`formatDate.ts`)
382
- - Types: `camelCase.types.ts` (`user.types.ts`)
383
- - Tests: `ComponentName.test.tsx`
384
-
385
- <!-- Source: .ruler/frontend/react-patterns.md -->
386
-
387
- # React Component Patterns
388
-
389
- ## Component Structure
390
-
391
- ```typescript
392
- interface Props {
393
- userId: string;
394
- onUpdate?: (user: User) => void;
395
- }
396
-
397
- export function UserProfile({ userId, onUpdate }: Props) {
398
- // 1. Hooks
399
- const { data, isLoading, error } = useGetUser(userId);
400
- const [isEditing, setIsEditing] = useState(false);
401
-
402
- // 2. Derived state (avoid useMemo for inexpensive operations)
403
- const displayName = formatName(data);
404
-
405
- // 3. Event handlers
406
- const handleSave = useCallback(async () => {
407
- await saveUser(data);
408
- onUpdate?.(data);
409
- }, [data, onUpdate]);
410
-
411
- // 4. Early returns
412
- if (isLoading) return <Loading />;
413
- if (error) return <Error message={error.message} />;
414
- if (!data) return <NotFound />;
415
-
416
- // 5. Main render
417
- return (
418
- <Card>
419
- <Typography>{displayName}</Typography>
420
- <Button onClick={handleSave}>Save</Button>
421
- </Card>
422
- );
423
- }
424
- ```
425
-
426
- ## Naming Conventions
427
-
428
- - Components: `PascalCase` (`UserProfile`)
429
- - Props interface: `Props` (co-located with component) or `ComponentNameProps` (exported/shared)
430
- - Event handlers: `handle*` (`handleClick`, `handleSubmit`)
431
- - Boolean props: `is*`, `has*`, `should*`
432
-
433
- ## Props Patterns
434
-
435
- ```typescript
436
- // Simple props
437
- interface Props {
438
- title: string;
439
- count: number;
440
- onAction: () => void;
441
- }
442
-
443
- // Discriminated unions for variants
444
- type ButtonProps = { variant: "link"; href: string } | { variant: "button"; onClick: () => void };
445
-
446
- // Optional callbacks
447
- interface Props {
448
- onSuccess?: (data: Data) => void;
449
- onError?: (error: Error) => void;
450
- }
451
- ```
452
-
453
- ## Composition Patterns
454
-
455
- ```typescript
456
- // Container/Presentational
457
- export function UserListContainer() {
458
- const { data, isLoading, error } = useUsers();
459
- if (isLoading) return <Loading />;
460
- if (error) return <Error />;
461
- return <UserList users={data} />;
462
- }
463
-
464
- // Compound components
465
- <Card>
466
- <Card.Header title="User" />
467
- <Card.Body>Content</Card.Body>
468
- <Card.Actions>
469
- <Button>Save</Button>
470
- </Card.Actions>
471
- </Card>
472
-
473
- // Render props
474
- <DataProvider>
475
- {({ data, isLoading }) => (
476
- isLoading ? <Loading /> : <Display data={data} />
477
- )}
478
- </DataProvider>
479
- ```
480
-
481
- ## Children Patterns
482
-
483
- ```typescript
484
- // Typed children
485
- interface Props {
486
- children: ReactNode;
487
- }
488
-
489
- // Render prop pattern
490
- interface Props {
491
- children: (data: Data) => ReactNode;
492
- }
493
-
494
- // Element restrictions
495
- interface Props {
496
- children: ReactElement<ButtonProps>;
497
- }
498
- ```
499
-
500
- ## List Rendering
501
-
502
- ```typescript
503
- // ✅ Proper keys
504
- {users.map((user) => <UserCard key={user.id} user={user} />)}
505
-
506
- // ✅ Empty states
507
- {users.length === 0 ? <EmptyState /> : users.map(...)}
508
-
509
- // ❌ Never use index as key when list can change
510
- {items.map((item, index) => <Item key={index} />)} // Wrong!
511
- ```
512
-
513
- ## Conditional Rendering
514
-
515
- ```typescript
516
- // Simple conditional
517
- {isLoading && <Spinner />}
518
-
519
- // If-else
520
- {isError ? <Error /> : <Success />}
521
-
522
- // Multiple conditions
523
- {isLoading ? <Loading /> : isError ? <Error /> : <Data />}
524
-
525
- // With early returns (preferred for complex logic)
526
- if (isLoading) return <Loading />;
527
- if (isError) return <Error />;
528
- return <Data />;
529
- ```
530
-
531
- ## Performance
532
-
533
- ```typescript
534
- // Memoize expensive components
535
- const MemoItem = memo(({ item }: Props) => <div>{item.name}</div>);
536
-
537
- // Split state to avoid re-renders
538
- function Parent() {
539
- const [count, setCount] = useState(0);
540
- const [text, setText] = useState("");
541
-
542
- // Only CountDisplay re-renders when count changes
543
- return (
544
- <>
545
- <CountDisplay count={count} />
546
- <TextInput value={text} onChange={setText} />
547
- </>
548
- );
549
- }
550
- ```
551
-
552
- ## Best Practices
553
-
554
- - **Single Responsibility** - One component, one purpose
555
- - **Composition over Props** - Use children and compound components
556
- - **Colocate State** - Keep state close to where it's used
557
- - **Type Everything** - Full TypeScript coverage
558
- - **Test Behavior** - Test user interactions, not implementation
559
-
560
- <!-- Source: .ruler/frontend/react.md -->
561
-
562
- # React
563
-
564
- - Destructure props in function body (improves readability and prop documentation)
565
- - Prefer inline JSX over extracted variables
566
- - Use custom hooks to encapsulate and reuse stateful logic
567
- - Use Zod for request/response schemas in data-fetching hooks
568
- - Use react-hook-form with Zod resolver for forms
569
- - Use date-fns for date operations
570
-
571
- <!-- Source: .ruler/frontend/styling.md -->
572
-
573
- # Styling Standards
574
-
575
- ## Core Principles
576
-
577
- 1. **Always use `sx` prop** - Never CSS/SCSS/SASS files
578
- 2. **Use theme tokens** - Never hardcode colors/spacing
579
- 3. **Type-safe theme access** - Use `sx={(theme) => ({...})}`
580
- 4. **Use semantic names** - `theme.palette.text.primary`, not `common.white`
581
- 5. **Check Storybook first** - It's the single source of truth
582
-
583
- ## Restricted Patterns
584
-
585
- ❌ **DO NOT USE:**
586
-
587
- - `styled()` or `makeStyles()` from MUI
588
- - CSS/SCSS/SASS files
589
- - Inline `style` prop (use `sx` instead)
590
- - String paths for theme (`"text.secondary"` - not type-safe)
591
-
592
- ## Type-Safe Theme Access
593
-
594
- ```typescript
595
- // ✅ Always use function form for type safety
596
- <Box sx={(theme) => ({
597
- backgroundColor: theme.palette.background.primary,
598
- color: theme.palette.text.secondary,
599
- padding: theme.spacing(4), // or just: 4
600
- })} />
601
-
602
- // ❌ Never hardcode
603
- <Box sx={{
604
- backgroundColor: "red", // ❌ Raw color
605
- padding: "16px", // ❌ Raw size
606
- color: "text.secondary", // ❌ String path
607
- }} />
608
- ```
609
-
610
- ## Spacing System
611
-
612
- Use indices 1-12 (4px-64px):
613
-
614
- ```typescript
615
- <Box sx={{ padding: 5 }} /> // → 16px
616
- <Box sx={{ marginX: 4 }} /> // → 12px left and right
617
- <Box sx={{ gap: 3 }} /> // → 8px
618
- ```
619
-
620
- **Use `rem` for fonts/heights (scales with user zoom), `px` for spacing:**
621
-
622
- ```typescript
623
- <Box sx={(theme) => ({
624
- height: "3rem", // ✅ Scales
625
- fontSize: theme.typography.body1.fontSize,
626
- padding: 5, // ✅ Prevents overflow
627
- })} />
628
- ```
629
-
630
- ## Responsive Styles
631
-
632
- ```typescript
633
- <Box sx={{
634
- width: { xs: "100%", md: "50%" },
635
- padding: { xs: 1, md: 3 },
636
- }} />
637
- ```
638
-
639
- ## Pseudo-classes
640
-
641
- ```typescript
642
- <Box sx={(theme) => ({
643
- "&:hover": { backgroundColor: theme.palette.primary.dark },
644
- "&:disabled": { opacity: 0.5 },
645
- "& .child": { color: theme.palette.text.secondary },
646
- })} />
647
- ```
648
-
649
- ## Shorthand Properties
650
-
651
- ✅ Use full names: `padding`, `paddingX`, `marginY`
652
- ❌ Avoid abbreviations: `p`, `px`, `my`
653
-
654
- ## Layout Components
655
-
656
- Safe to import directly from MUI:
657
-
658
- ```typescript
659
- import { Box, Stack, Container, Grid } from "@mui/material";
660
- ```
661
-
662
- ## Best Practices
663
-
664
- - Type-safe access with `sx={(theme) => ({...})}`
665
- - Use semantic token names
666
- - Full property names, not abbreviations
667
-
668
- <!-- Source: .ruler/frontend/testing.md -->
669
-
670
- # Testing Standards
671
-
672
- ## Testing Trophy Philosophy
673
-
674
- Focus on **Integration tests** - test how components work together as users experience them.
675
-
676
- **Investment Priority:**
677
-
678
- 1. **Static** (TypeScript/ESLint) - Free confidence
679
- 2. **Integration** - Most valuable, test features not isolated components
680
- 3. **Unit** - For pure helpers/utilities only
681
- 4. **E2E** - Critical flows only
682
-
683
- **Key Principle:** Test as close to how users interact with your app as possible.
684
-
685
- ## Technology Stack
686
-
687
- - **Vitest** for test runner
688
- - **@testing-library/react** for component testing
689
- - **@testing-library/user-event** for user interactions
690
- - **Mock Service Worker (MSW)** for API mocking
691
-
692
- ## Test Structure
693
-
694
- ```typescript
695
- describe("ComponentName", () => {
696
- it("should render correctly", () => {
697
- render(<Component />);
698
- expect(screen.getByText("Expected")).toBeInTheDocument();
699
- });
700
-
701
- it("should handle user interaction", async () => {
702
- const user = userEvent.setup();
703
- render(<Component />);
704
- await user.click(screen.getByRole("button", { name: "Submit" }));
705
- await waitFor(() => expect(screen.getByText("Success")).toBeInTheDocument());
706
- });
707
- });
708
- ```
709
-
710
- ## Test Naming
711
-
712
- Pattern: `should [behavior] when [condition]`
713
-
714
- ## Querying Elements - Priority Order
715
-
716
- 1. `getByRole` - Best for accessibility
717
- 2. `getByLabelText` - For form fields
718
- 3. `getByText` - For non-interactive content
719
- 4. `getByTestId` - ⚠️ **LAST RESORT**
720
-
721
- ```typescript
722
- // ✅ Prefer
723
- screen.getByRole("button", { name: /submit/i });
724
- screen.getByLabelText("Email");
725
-
726
- // ❌ Avoid
727
- screen.getByClassName("user-card");
728
- screen.getByTestId("element");
729
- ```
730
-
731
- ## User Interactions
732
-
733
- ```typescript
734
- it("should handle form submission", async () => {
735
- const user = userEvent.setup();
736
- const onSubmit = vi.fn();
737
- render(<Form onSubmit={onSubmit} />);
738
-
739
- await user.type(screen.getByLabelText("Name"), "John");
740
- await user.click(screen.getByRole("button", { name: "Submit" }));
741
-
742
- expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
743
- });
744
- ```
745
-
746
- ## Hook Testing
747
-
748
- ```typescript
749
- describe("useCustomHook", () => {
750
- it("should return data after loading", async () => {
751
- const { result } = renderHook(() => useCustomHook());
752
- await waitFor(() => expect(result.current.isLoading).toBe(false));
753
- expect(result.current.data).toEqual(expectedData);
754
- });
755
- });
756
- ```
757
-
758
- ## MSW Pattern
759
-
760
- **Always export factory functions, not static handlers:**
761
-
762
- ```typescript
763
- // ✅ Good - flexible per test
764
- export const createTestHandler = (data: Data[]) =>
765
- rest.get(`/api/resource`, (req, res, ctx) => res(ctx.json(data)));
766
-
767
- // Usage
768
- mockServer.use(createTestHandler(customData));
769
- ```
770
-
771
- ## Mocking
772
-
773
- ```typescript
774
- vi.mock("@/lib/api", () => ({
775
- get: vi.fn().mockResolvedValue({ data: { id: "1" } }),
776
- }));
777
- ```
778
-
779
- ## What to Test
780
-
781
- **✅ Do Test:**
782
-
783
- - Integration tests for features
784
- - Unit tests for utilities
785
- - All states (loading, success, error)
786
- - User interactions
787
-
788
- **❌ Don't Test:**
789
-
790
- - Implementation details
791
- - Third-party libraries
792
- - Styles/CSS
793
-
794
- <!-- Source: .ruler/frontend/typeScript.md -->
795
-
796
- # TypeScript Standards
797
-
798
- ## Core Principles
799
-
800
- - **Strict mode enabled** - Avoid `any`
801
- - **Prefer type inference** - Let TypeScript infer when possible
802
- - **Explicit return types** for exported functions
803
- - **Zod for runtime validation** - Single source of truth
804
- - **No type assertions** unless unavoidable
805
-
806
- ## Type vs Interface
807
-
808
- ```typescript
809
- // ✅ Use interface for: props, object shapes, extensible types
810
- interface UserCardProps {
811
- user: User;
812
- onSelect: (id: string) => void;
813
- }
814
-
815
- interface BaseEntity {
816
- id: string;
817
- createdAt: string;
818
- }
819
-
820
- interface User extends BaseEntity {
821
- name: string;
822
- }
823
-
824
- // ✅ Use type for: unions, intersections, tuples, derived types
825
- type Status = "pending" | "active" | "completed";
826
- type UserWithRoles = User & { roles: string[] };
827
- type Coordinate = [number, number];
828
- type UserKeys = keyof User;
829
- ```
830
-
831
- ## Naming Conventions
832
-
833
- ```typescript
834
- // ✅ Suffix with purpose (no I or T prefix)
835
- interface ButtonProps { ... }
836
- type ApiResponse = { ... };
837
- type UserOptions = { ... };
838
-
839
- // ✅ Boolean properties
840
- interface User {
841
- isActive: boolean;
842
- hasPermission: boolean;
843
- shouldNotify: boolean;
844
- }
845
- ```
846
-
847
- ## Zod Integration
848
-
849
- ```typescript
850
- // Define schema, infer type
851
- const userSchema = z.object({
852
- id: z.string(),
853
- name: z.string(),
854
- });
855
-
856
- export type User = z.infer<typeof userSchema>;
857
-
858
- // Validate API responses
859
- const response = await get({
860
- url: "/api/users",
861
- responseSchema: userSchema,
862
- });
863
- ```
864
-
865
- ## Type Guards
866
-
867
- ```typescript
868
- export function isUser(value: unknown): value is User {
869
- return userSchema.safeParse(value).success;
870
- }
871
-
872
- // Usage
873
- if (isUser(data)) {
874
- console.log(data.name); // TypeScript knows data is User
875
- }
876
- ```
877
-
878
- ## Constants
879
-
880
- ```typescript
881
- // Use const assertions
882
- export const STATUSES = {
883
- DRAFT: "draft",
884
- PUBLISHED: "published",
885
- } as const;
886
-
887
- export type Status = (typeof STATUSES)[keyof typeof STATUSES];
888
- ```
889
-
890
- ## Utility Types
891
-
892
- ```typescript
893
- // Built-in utilities
894
- type PartialUser = Partial<User>;
895
- type RequiredUser = Required<User>;
896
- type ReadonlyUser = Readonly<User>;
897
- type UserIdOnly = Pick<User, "id" | "name">;
898
- type UserWithoutPassword = Omit<User, "password">;
899
- type UserMap = Record<string, User>;
900
- type DefinedString = NonNullable<string | null>;
901
-
902
- // Function utilities
903
- type GetUserResult = ReturnType<typeof getUser>;
904
- type UpdateUserParams = Parameters<typeof updateUser>;
905
- ```
906
-
907
- ## Avoiding `any`
908
-
909
- ```typescript
910
- // ❌ Bad - Loses type safety
911
- function process(data: any) { ... }
912
-
913
- // ✅ Use unknown for truly unknown types
914
- function process(data: unknown) {
915
- if (typeof data === "object" && data !== null) {
916
- // Type guard
917
- }
918
- }
919
-
920
- // ✅ Better - Use generics
921
- function process<T extends { value: string }>(data: T) {
922
- return data.value;
923
- }
924
-
925
- // ✅ Best - Use Zod
926
- const dataSchema = z.object({ value: z.string() });
927
- function process(data: unknown) {
928
- const parsed = dataSchema.parse(data);
929
- return parsed.value;
930
- }
931
- ```
932
-
933
- ## Generics
934
-
935
- ```typescript
936
- // Generic function
937
- function getFirst<T>(array: T[]): T | undefined {
938
- return array[0];
939
- }
940
-
941
- // Generic component
942
- interface ListProps<T> {
943
- items: T[];
944
- renderItem: (item: T) => ReactNode;
945
- }
946
-
947
- export function List<T>({ items, renderItem }: ListProps<T>) {
948
- return <>{items.map((item) => renderItem(item))}</>;
949
- }
950
-
951
- // Constrained generics
952
- function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
953
- return items.find((item) => item.id === id);
954
- }
955
- ```
956
-
957
- ## Discriminated Unions
958
-
959
- ```typescript
960
- // State machine
961
- type QueryState<T> =
962
- | { status: "idle" }
963
- | { status: "loading" }
964
- | { status: "success"; data: T }
965
- | { status: "error"; error: Error };
966
-
967
- // Automatic type narrowing
968
- if (state.status === "success") {
969
- console.log(state.data); // TypeScript knows data exists
970
- }
971
-
972
- // Actions
973
- type Action = { type: "SET_USER"; payload: User } | { type: "CLEAR_USER" };
974
-
975
- function reducer(state: State, action: Action) {
976
- switch (action.type) {
977
- case "SET_USER":
978
- return { ...state, user: action.payload };
979
- case "CLEAR_USER":
980
- return { ...state, user: null };
981
- }
982
- }
983
- ```
984
-
985
- ## Type Narrowing
986
-
987
- ```typescript
988
- // typeof
989
- function format(value: string | number) {
990
- if (typeof value === "string") {
991
- return value.toUpperCase();
992
- }
993
- return value.toFixed(2);
994
- }
995
-
996
- // in operator
997
- function move(animal: Fish | Bird) {
998
- if ("swim" in animal) {
999
- animal.swim();
1000
- } else {
1001
- animal.fly();
1002
- }
1003
- }
1004
-
1005
- // instanceof
1006
- function handleError(error: unknown) {
1007
- if (error instanceof Error) {
1008
- console.log(error.message);
1009
- }
1010
- }
1011
- ```
1012
-
1013
- ## Common Patterns
1014
-
1015
- ```typescript
1016
- // Optional chaining
1017
- const userName = user?.profile?.name;
1018
-
1019
- // Nullish coalescing
1020
- const displayName = userName ?? "Anonymous";
1021
-
1022
- // Non-null assertion (use sparingly)
1023
- const user = getUser()!;
1024
-
1025
- // Template literal types
1026
- type Route = `/users/${string}` | `/posts/${string}`;
1027
- type EventHandler = `on${Capitalize<string>}`;
1028
- ```
1029
-
1030
- ## Best Practices
1031
-
1032
- - **Never use `any`** - Use `unknown` or generics
1033
- - **Discriminated unions** for state machines
1034
- - **Zod** for runtime validation
1035
- - **Type guards** over type assertions
1036
- - **Const assertions** for readonly values
1037
- - **Utility types** to transform existing types
1
+ @AGENTS.md