@clipboard-health/ai-rules 1.6.43 → 1.7.0

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