@clipboard-health/ai-rules 2.26.0 → 2.27.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.
- package/README.md +56 -39
- package/package.json +1 -1
- package/rules/backend/architecture.md +4 -0
- package/rules/backend/asyncMessaging.md +4 -0
- package/rules/backend/infrastructure.md +4 -0
- package/rules/backend/mongodb.md +4 -33
- package/rules/backend/notifications.md +4 -0
- package/rules/backend/postgres.md +4 -0
- package/rules/backend/restApiDesign.md +10 -54
- package/rules/backend/serviceTests.md +4 -0
- package/rules/common/configuration.md +4 -0
- package/rules/common/coreLibraries.md +4 -0
- package/rules/common/featureFlags.md +4 -0
- package/rules/common/gitWorkflow.md +4 -0
- package/rules/common/libraryAuthoring.md +13 -0
- package/rules/common/loggingObservability.md +4 -0
- package/rules/common/testing.md +5 -1
- package/rules/common/typeScript.md +12 -61
- package/rules/datamodeling/analytics.md +22 -15
- package/rules/datamodeling/castingDbtStagingModels.md +4 -0
- package/rules/datamodeling/dbtModelDevelopment.md +16 -12
- package/rules/datamodeling/dbtYamlDocumentation.md +4 -0
- package/rules/frontend/{fileOrganization.md → architecture.md} +22 -1
- package/rules/frontend/customHooks.md +4 -12
- package/rules/frontend/dataFetching.md +25 -16
- package/rules/frontend/e2eTesting.md +5 -10
- package/rules/frontend/reactComponents.md +48 -42
- package/rules/frontend/styling.md +4 -10
- package/rules/frontend/testing.md +6 -35
- package/scripts/constants.js +1 -79
- package/scripts/rules.js +126 -0
- package/scripts/sync.js +20 -61
- package/rules/frontend/bottomSheets.md +0 -25
- package/rules/frontend/businessLogicPlacement.md +0 -3
- package/rules/frontend/errorHandling.md +0 -39
- package/rules/frontend/frontendTechnologyStack.md +0 -10
- package/rules/frontend/interactiveElements.md +0 -19
- package/rules/frontend/modalRoutes.md +0 -15
- package/skills/flaky-test-bulk-debugger/SKILL.md +0 -89
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Frontend architecture: technology stack, file organization, where business logic lives"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Frontend Architecture
|
|
6
|
+
|
|
7
|
+
## Technology Stack
|
|
8
|
+
|
|
9
|
+
- **React** with TypeScript (strict mode)
|
|
10
|
+
- **MUI** for UI components
|
|
11
|
+
- **React Query** (@tanstack/react-query) for data fetching
|
|
12
|
+
- **Zod** for runtime validation
|
|
13
|
+
- **Vitest** + **@testing-library/react** for testing
|
|
14
|
+
- **MSW** for API mocking
|
|
15
|
+
- **Playwright** for E2E tests
|
|
16
|
+
- **constate** for shared state
|
|
17
|
+
|
|
18
|
+
## File Organization
|
|
2
19
|
|
|
3
20
|
Organize frontend code by concept/feature (e.g., `Shifts/`, `Invites/`), not by type (e.g., `components/`, `hooks/`).
|
|
4
21
|
|
|
@@ -18,3 +35,7 @@ FeatureName/
|
|
|
18
35
|
├── paths.ts # Route constants
|
|
19
36
|
└── types.ts # Shared types
|
|
20
37
|
```
|
|
38
|
+
|
|
39
|
+
## Business Logic Placement
|
|
40
|
+
|
|
41
|
+
Flag business logic in frontend code that should be a backend API call instead. Frontend/backend divergence causes bugs.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Creating React custom hooks: naming, shared state with constate"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Custom Hooks
|
|
2
6
|
|
|
3
7
|
## Naming
|
|
@@ -6,18 +10,6 @@
|
|
|
6
10
|
- Data: `useGet*`, `use*Data`
|
|
7
11
|
- Actions: `useSubmit*`, `useCreate*`
|
|
8
12
|
|
|
9
|
-
## Structure
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
export function useFeature(params: Params, options: Options = {}) {
|
|
13
|
-
const query = useQuery(...);
|
|
14
|
-
const computed = useMemo(() => transform(query.data), [query.data]);
|
|
15
|
-
const handleAction = useCallback(async () => { ... }, []);
|
|
16
|
-
|
|
17
|
-
return { data: computed, isLoading: query.isLoading, handleAction };
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
13
|
## Shared State with Constate
|
|
22
14
|
|
|
23
15
|
```typescript
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Implementing data fetching and error handling: React Query, API calls, caching, parsedApi"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Data Fetching
|
|
2
6
|
|
|
3
7
|
## Core Rules
|
|
4
8
|
|
|
5
9
|
1. Use React Query for all API calls
|
|
6
10
|
2. Define Zod schemas for all request/response types
|
|
7
|
-
3. Use `enabled` option for conditional fetching
|
|
11
|
+
3. Use the `enabled` option for conditional fetching: `{ enabled: isDefined(dependencyData?.id) }`
|
|
8
12
|
4. Use `invalidateQueries` (not `refetch`) for disabled queries
|
|
9
13
|
|
|
10
14
|
## Hook Pattern
|
|
@@ -36,7 +40,21 @@ export function useGetFeature(id: string, options = {}) {
|
|
|
36
40
|
|
|
37
41
|
- Log errors via `meta.logErrorMessage` using centralized event constants
|
|
38
42
|
- Display user-facing errors via `meta.userErrorMessage`
|
|
39
|
-
- Do not use the deprecated `onError` callback for useQuery queries
|
|
43
|
+
- Do not use the deprecated `onError` callback for useQuery queries — use the `meta` pattern. `onError` remains valid for useMutation.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
useMutation({
|
|
47
|
+
mutationFn: createItem,
|
|
48
|
+
onSuccess: () => {
|
|
49
|
+
showSuccessToast("Created");
|
|
50
|
+
queryClient.invalidateQueries(["items"]);
|
|
51
|
+
},
|
|
52
|
+
meta: {
|
|
53
|
+
logErrorMessage: APP_EVENTS.CREATE_FAILURE,
|
|
54
|
+
userErrorMessage: "Failed to create",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
40
58
|
|
|
41
59
|
## Query Keys
|
|
42
60
|
|
|
@@ -47,23 +65,14 @@ queryKey: ["users", userId];
|
|
|
47
65
|
queryKey: ["users", userId, "posts"];
|
|
48
66
|
```
|
|
49
67
|
|
|
50
|
-
##
|
|
68
|
+
## `parsedApi.ts` vs `api.ts`
|
|
51
69
|
|
|
52
|
-
|
|
53
|
-
const { data } = useGetFeature({ id: dependencyData?.id }, { enabled: isDefined(dependencyData?.id) });
|
|
54
|
-
```
|
|
70
|
+
Frontend repos have two API layers:
|
|
55
71
|
|
|
56
|
-
|
|
72
|
+
- **`api.ts`** (legacy) — does not parse responses through Zod schemas. Inferred types say `Date` for `dateTimeSchema()` fields but the runtime value is still a string. Zod transforms (`.transform()`, `dateTimeSchema()`, enum fallbacks) produce **incorrect types at runtime**.
|
|
73
|
+
- **`parsedApi.ts`** — parses both inputs (`z.input`) and outputs (`z.output`) through schemas. Types match runtime values.
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
export function useCreateItem() {
|
|
60
|
-
const queryClient = useQueryClient();
|
|
61
|
-
return useMutation({
|
|
62
|
-
mutationFn: (data: CreateItemRequest) => api.post("/items", data),
|
|
63
|
-
onSuccess: () => queryClient.invalidateQueries(["items"]),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
```
|
|
75
|
+
Use `parsedApi.ts` for all new API calls. However, `parsedApi.ts` means invalid contract schemas will fail at runtime — ensure contracts are forwards-compatible. Do not use `parsedApi.ts` if the contract contains bare `z.enum()` values that the backend may extend, as new enum values will cause parse failures on old clients. Migrate bare `z.enum()` to `requiredEnumWithFallback`/`optionalEnumWithFallback` first.
|
|
67
76
|
|
|
68
77
|
## Test Utilities
|
|
69
78
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Writing E2E tests with Playwright"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# E2E Testing (Playwright)
|
|
2
6
|
|
|
3
7
|
## Core Rules
|
|
@@ -5,15 +9,7 @@
|
|
|
5
9
|
- Test critical user flows only—not exhaustive scenarios; when in doubt, write a component test instead
|
|
6
10
|
- Each test sets up its own data (no shared state between tests)
|
|
7
11
|
- Mock feature flags and third-party services
|
|
8
|
-
- Use user-centric locators
|
|
9
|
-
|
|
10
|
-
## Locator Priority
|
|
11
|
-
|
|
12
|
-
1. `page.getByRole()`
|
|
13
|
-
2. `page.getByLabel()`
|
|
14
|
-
3. `page.getByPlaceholder()`
|
|
15
|
-
4. `page.getByText()`
|
|
16
|
-
5. `page.getByTestId()` (last resort)
|
|
12
|
+
- Use user-centric locators in priority order: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`; use `getByTestId` only as a last resort—avoid CSS/XPath selectors
|
|
17
13
|
|
|
18
14
|
## Assertions
|
|
19
15
|
|
|
@@ -38,4 +34,3 @@ Before adding an E2E test:
|
|
|
38
34
|
- Hard-coded timeouts (`page.waitForTimeout`)
|
|
39
35
|
- Testing loading states (non-deterministic)
|
|
40
36
|
- Shared data between tests
|
|
41
|
-
- CSS/XPath selectors
|
|
@@ -1,37 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
```typescript
|
|
6
|
-
// Use interface for: props, object shapes, extensible types
|
|
7
|
-
interface UserCardProps {
|
|
8
|
-
user: User;
|
|
9
|
-
onSelect: (id: string) => void;
|
|
10
|
-
}
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Structure Order
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
export function Component({ userId, onUpdate }: Props) {
|
|
17
|
-
// 1. Hooks
|
|
18
|
-
const { data, isLoading } = useGetUser(userId);
|
|
19
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
20
|
-
|
|
21
|
-
// 2. Derived state (no useMemo for cheap operations)
|
|
22
|
-
const displayName = formatName(data);
|
|
23
|
-
|
|
24
|
-
// 3. Event handlers
|
|
25
|
-
const handleSave = useCallback(async () => { ... }, [deps]);
|
|
26
|
-
|
|
27
|
-
// 4. Early returns for loading/error/empty
|
|
28
|
-
if (isLoading) return <Loading />;
|
|
29
|
-
if (!data) return <NotFound />;
|
|
1
|
+
---
|
|
2
|
+
description: "Building UI components: structure, composition, modals, bottom sheets, interactive elements, a11y, Storybook"
|
|
3
|
+
---
|
|
30
4
|
|
|
31
|
-
|
|
32
|
-
return <Card>...</Card>;
|
|
33
|
-
}
|
|
34
|
-
```
|
|
5
|
+
# React Components
|
|
35
6
|
|
|
36
7
|
## Naming Conventions
|
|
37
8
|
|
|
@@ -60,15 +31,6 @@ interface Props {
|
|
|
60
31
|
}
|
|
61
32
|
```
|
|
62
33
|
|
|
63
|
-
## Navigation & Layout
|
|
64
|
-
|
|
65
|
-
- Show bottom navigation on all top-level tabs/pages; hide it on nested or drilled-in views
|
|
66
|
-
- Use `Title` with correct heading levels (`h1`-`h6`) and maintain a structured `h1`→`h2`→`h3` hierarchy per page
|
|
67
|
-
|
|
68
|
-
## Storybook
|
|
69
|
-
|
|
70
|
-
Register every new or updated shared UI component in Storybook before merging; include a `Default` story first with all relevant props exposed via controls.
|
|
71
|
-
|
|
72
34
|
## Inline JSX and Handlers
|
|
73
35
|
|
|
74
36
|
```typescript
|
|
@@ -87,6 +49,50 @@ const handleSave = useCallback(async () => { ... }, [deps]);
|
|
|
87
49
|
return <MemoizedChild onSave={handleSave} />;
|
|
88
50
|
```
|
|
89
51
|
|
|
52
|
+
## Interactive Elements
|
|
53
|
+
|
|
54
|
+
Never add `onClick` to `div` or `span` — it breaks focus states, keyboard navigation, and ARIA roles. Use:
|
|
55
|
+
|
|
56
|
+
- `<button>` for actions
|
|
57
|
+
- `<a>` (Link) for navigation
|
|
58
|
+
- MUI interactive components (`Button`, `IconButton`, `ListItemButton`)
|
|
59
|
+
|
|
60
|
+
Every interactive element must be tab focusable with a visible focus indicator and keyboard event handling.
|
|
61
|
+
|
|
62
|
+
## Navigation & Layout
|
|
63
|
+
|
|
64
|
+
- Show bottom navigation on all top-level tabs/pages; hide it on nested or drilled-in views
|
|
65
|
+
- Use `Title` with correct heading levels (`h1`-`h6`) and maintain a structured `h1`→`h2`→`h3` hierarchy per page
|
|
66
|
+
|
|
67
|
+
## Modal Routes
|
|
68
|
+
|
|
69
|
+
Modal visibility is driven by URL, not local state:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
<ModalRoute
|
|
73
|
+
path={`${basePath}/confirm`}
|
|
74
|
+
closeModalPath={basePath}
|
|
75
|
+
render={({ modalState }) => (
|
|
76
|
+
<ConfirmDialog modalState={modalState} />
|
|
77
|
+
)}
|
|
78
|
+
/>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use `history.replace` (not `push`) when navigating between modals to avoid awkward back-button behavior.
|
|
82
|
+
|
|
83
|
+
## Bottom Sheets
|
|
84
|
+
|
|
85
|
+
- Every bottom sheet must include a close button in the top-right corner via `BottomSheetHeader`: `<BottomSheet modalState={modalState} header={<BottomSheetHeader onClose={onClose} />}>`
|
|
86
|
+
- Buttons inside a bottom sheet must always stack vertically (never side by side). Use `<DialogFooter orientation="vertical">` or a vertical `<Stack>`.
|
|
87
|
+
- Split each bottom sheet into two components for Storybook testability:
|
|
88
|
+
- **`SomethingBottomSheet`** — connected component that owns data fetching, analytics, and state; renders `<BottomSheet>` and delegates content to the presentational component.
|
|
89
|
+
- **`SomethingBottomSheetContent`** — pure presentational component that accepts only primitive/callback props; has a `.stories.tsx` file. The stories file should render a button that opens the content in a `<BottomSheet>`.
|
|
90
|
+
- Exception: a bottom sheet that is purely presentational with no data fetching/mutations can be one `.tsx` file.
|
|
91
|
+
|
|
92
|
+
## Storybook
|
|
93
|
+
|
|
94
|
+
Register every new or updated shared UI component in Storybook before merging; include a `Default` story first with all relevant props exposed via controls.
|
|
95
|
+
|
|
90
96
|
## Component Reuse
|
|
91
97
|
|
|
92
98
|
Before creating a new component, search for existing ones in this order:
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Styling components with MUI sx prop: theme tokens, spacing, no CSS/SCSS"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Styling
|
|
2
6
|
|
|
3
7
|
## Core Rules
|
|
@@ -40,13 +44,3 @@ Use `rem` for fonts/heights (scales with user zoom), spacing indices for padding
|
|
|
40
44
|
|
|
41
45
|
- ✅ Use full names: `padding`, `paddingX`, `marginY`
|
|
42
46
|
- ❌ Avoid abbreviations: `p`, `px`, `my`
|
|
43
|
-
|
|
44
|
-
## Pseudo-classes
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
<Box sx={(theme) => ({
|
|
48
|
-
"&:hover": { backgroundColor: theme.palette.primary.dark },
|
|
49
|
-
"&:disabled": { opacity: 0.5 },
|
|
50
|
-
"& .MuiTypography-root": { color: theme.palette.text.secondary },
|
|
51
|
-
})} />
|
|
52
|
-
```
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Writing frontend tests: React Testing Library, component tests"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Testing
|
|
2
6
|
|
|
3
7
|
## Philosophy
|
|
@@ -6,38 +10,9 @@ Focus on integration tests—test how components work together as users experien
|
|
|
6
10
|
|
|
7
11
|
**Priority:** Static (TS/ESLint) → Integration → Unit (pure utilities only) → E2E (critical flows only)
|
|
8
12
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
describe("ComponentName", () => {
|
|
13
|
-
it("should [behavior] when [condition]", async () => {
|
|
14
|
-
const user = userEvent.setup();
|
|
15
|
-
render(<Component />);
|
|
16
|
-
|
|
17
|
-
await user.click(screen.getByRole("button", { name: "Submit" }));
|
|
18
|
-
|
|
19
|
-
await waitFor(() => {
|
|
20
|
-
expect(screen.getByText("Success")).toBeInTheDocument();
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Query Priority
|
|
13
|
+
## Queries
|
|
27
14
|
|
|
28
|
-
|
|
29
|
-
2. `getByLabelText` (form fields)
|
|
30
|
-
3. `getByText` (non-interactive content)
|
|
31
|
-
4. `getByTestId` (last resort only)
|
|
32
|
-
|
|
33
|
-
```typescript
|
|
34
|
-
// ✅ Prefer
|
|
35
|
-
screen.getByRole("button", { name: /submit/i });
|
|
36
|
-
screen.getByLabelText("Email");
|
|
37
|
-
|
|
38
|
-
// ❌ Avoid
|
|
39
|
-
screen.getByTestId("submit-button");
|
|
40
|
-
```
|
|
15
|
+
Prefer user-centric queries in priority order: `getByRole`, `getByLabelText`, `getByText`; use `getByTestId` only as a last resort.
|
|
41
16
|
|
|
42
17
|
## MSW Handlers
|
|
43
18
|
|
|
@@ -50,7 +25,3 @@ export const createUserHandler = (userData: User) => rest.get("/api/user", (req,
|
|
|
50
25
|
// Usage
|
|
51
26
|
mockServer.use(createUserHandler(customData));
|
|
52
27
|
```
|
|
53
|
-
|
|
54
|
-
## What to Test
|
|
55
|
-
|
|
56
|
-
✅ Test: user interactions, all states (loading/success/error), integration between components
|
package/scripts/constants.js
CHANGED
|
@@ -3,8 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PROFILES = exports.
|
|
7
|
-
exports.toRulePath = toRulePath;
|
|
6
|
+
exports.PROFILES = exports.FILES = exports.PATHS = void 0;
|
|
8
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
8
|
const PACKAGE_ROOT = node_path_1.default.join(__dirname, "..");
|
|
10
9
|
exports.PATHS = {
|
|
@@ -15,83 +14,6 @@ exports.FILES = {
|
|
|
15
14
|
agents: "AGENTS.md",
|
|
16
15
|
claude: "CLAUDE.md",
|
|
17
16
|
};
|
|
18
|
-
exports.RULE_FILES = {
|
|
19
|
-
"common/configuration": "Adding config, secrets, or third-party dependencies: SSM, LaunchDarkly, DB, NPM packages",
|
|
20
|
-
"common/coreLibraries": "Adding dependencies, implementing functionality, or debugging errors involving a @clipboard-health/* library",
|
|
21
|
-
"common/featureFlags": "Creating or managing feature flags: naming, lifecycle, SDK usage, Zod schemas",
|
|
22
|
-
"common/gitWorkflow": "Writing commit messages, PR titles, or reviewing pull requests",
|
|
23
|
-
"common/loggingObservability": "Adding logging, metrics, monitoring, or observability: levels, context, PII, Datadog",
|
|
24
|
-
"common/testing": "Writing unit tests: conventions, naming, structure",
|
|
25
|
-
"common/typeScript": "Writing ANY TypeScript code",
|
|
26
|
-
"backend/architecture": "Structuring NestJS modules, services, repos: three-tier, microservices, ts-rest contracts",
|
|
27
|
-
"backend/asyncMessaging": "Working with queues, async messaging, or background jobs",
|
|
28
|
-
"backend/mongodb": "Working with MongoDB/Mongoose: schemas, indexes, queries, transactions, migrations",
|
|
29
|
-
"backend/notifications": "Implementing notifications via Knock: push notifications, deep links, workflow design",
|
|
30
|
-
"backend/postgres": "Working with Postgres: column types, schema changes, query patterns, Prisma TypedSQL",
|
|
31
|
-
"backend/restApiDesign": "Designing REST APIs: JSON:API, auth, validation, pagination, ts-rest contracts, DTOs",
|
|
32
|
-
"backend/infrastructure": "Provisioning infrastructure: Terraform, Docker, ECS, DNS",
|
|
33
|
-
"backend/serviceTests": "Writing service tests: test data, background jobs, bug handling, migrations",
|
|
34
|
-
"frontend/businessLogicPlacement": "Implementing or reviewing business logic: flag logic that should live on the backend",
|
|
35
|
-
"frontend/customHooks": "Creating React custom hooks: naming, structure, shared state with constate",
|
|
36
|
-
"frontend/dataFetching": "Implementing data fetching: React Query, API calls, caching",
|
|
37
|
-
"frontend/e2eTesting": "Writing E2E tests with Playwright",
|
|
38
|
-
"frontend/errorHandling": "Handling errors in React: component, mutation (meta pattern), Zod validation",
|
|
39
|
-
"frontend/fileOrganization": "Organizing files and folders in a frontend project",
|
|
40
|
-
"frontend/frontendTechnologyStack": "Choosing frontend libraries, frameworks, or tools",
|
|
41
|
-
"frontend/interactiveElements": "Adding interactive elements: semantic HTML, a11y, keyboard accessibility",
|
|
42
|
-
"frontend/modalRoutes": "Implementing modals or route-based dialogs",
|
|
43
|
-
"frontend/reactComponents": "Writing React components: structure, composition, navigation, Storybook, inline JSX",
|
|
44
|
-
"frontend/styling": "Styling components with MUI sx prop: theme tokens, spacing, no CSS/SCSS",
|
|
45
|
-
"frontend/testing": "Writing frontend tests: React Testing Library, component tests",
|
|
46
|
-
"datamodeling/analytics": "Querying analytics data: dbt-mcp, Snowflake, source columns, output formatting",
|
|
47
|
-
"datamodeling/castingDbtStagingModels": "Casting data types in dbt staging models",
|
|
48
|
-
"datamodeling/dbtModelDevelopment": "Developing dbt models: naming, structure, testing",
|
|
49
|
-
"datamodeling/dbtYamlDocumentation": "Writing dbt YAML documentation and schema files",
|
|
50
|
-
};
|
|
51
|
-
function toRulePath(ruleId) {
|
|
52
|
-
return `${ruleId}.md`;
|
|
53
|
-
}
|
|
54
|
-
exports.CATEGORIES = {
|
|
55
|
-
common: [
|
|
56
|
-
"common/configuration",
|
|
57
|
-
"common/coreLibraries",
|
|
58
|
-
"common/featureFlags",
|
|
59
|
-
"common/gitWorkflow",
|
|
60
|
-
"common/loggingObservability",
|
|
61
|
-
"common/testing",
|
|
62
|
-
"common/typeScript",
|
|
63
|
-
],
|
|
64
|
-
backend: [
|
|
65
|
-
"backend/architecture",
|
|
66
|
-
"backend/asyncMessaging",
|
|
67
|
-
"backend/infrastructure",
|
|
68
|
-
"backend/mongodb",
|
|
69
|
-
"backend/notifications",
|
|
70
|
-
"backend/postgres",
|
|
71
|
-
"backend/restApiDesign",
|
|
72
|
-
"backend/serviceTests",
|
|
73
|
-
],
|
|
74
|
-
frontend: [
|
|
75
|
-
"frontend/businessLogicPlacement",
|
|
76
|
-
"frontend/customHooks",
|
|
77
|
-
"frontend/dataFetching",
|
|
78
|
-
"frontend/e2eTesting",
|
|
79
|
-
"frontend/errorHandling",
|
|
80
|
-
"frontend/fileOrganization",
|
|
81
|
-
"frontend/frontendTechnologyStack",
|
|
82
|
-
"frontend/interactiveElements",
|
|
83
|
-
"frontend/modalRoutes",
|
|
84
|
-
"frontend/reactComponents",
|
|
85
|
-
"frontend/styling",
|
|
86
|
-
"frontend/testing",
|
|
87
|
-
],
|
|
88
|
-
datamodeling: [
|
|
89
|
-
"datamodeling/analytics",
|
|
90
|
-
"datamodeling/castingDbtStagingModels",
|
|
91
|
-
"datamodeling/dbtModelDevelopment",
|
|
92
|
-
"datamodeling/dbtYamlDocumentation",
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
17
|
exports.PROFILES = {
|
|
96
18
|
common: { include: ["common"] },
|
|
97
19
|
frontend: { include: ["common", "frontend"] },
|
package/scripts/rules.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseRuleFile = parseRuleFile;
|
|
7
|
+
exports.discoverRules = discoverRules;
|
|
8
|
+
exports.resolveRules = resolveRules;
|
|
9
|
+
exports.generateAgentsIndex = generateAgentsIndex;
|
|
10
|
+
exports.generateReadmeRulesSection = generateReadmeRulesSection;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const FRONTMATTER_PATTERN = /^---\r?\n(?<frontmatter>[\s\S]*?)\r?\n---/;
|
|
14
|
+
const DESCRIPTION_PATTERN = /^description:\s*(?<description>.+)$/m;
|
|
15
|
+
const HEADING_PATTERN = /^#\s+(?<heading>.+)$/m;
|
|
16
|
+
/**
|
|
17
|
+
* Parses a rule file's frontmatter `description` (the "When to Read" text) and its H1 heading.
|
|
18
|
+
* The frontmatter is the single source of truth for rule metadata; throws when it's missing so
|
|
19
|
+
* tests catch unregistered descriptions before publishing.
|
|
20
|
+
*/
|
|
21
|
+
function parseRuleFile(params) {
|
|
22
|
+
const { content, filePath } = params;
|
|
23
|
+
const frontmatter = FRONTMATTER_PATTERN.exec(content)?.groups?.["frontmatter"];
|
|
24
|
+
const rawDescription = frontmatter
|
|
25
|
+
? DESCRIPTION_PATTERN.exec(frontmatter)?.groups?.["description"]
|
|
26
|
+
: undefined;
|
|
27
|
+
if (!rawDescription) {
|
|
28
|
+
throw new Error(`Rule file ${filePath} is missing a frontmatter 'description'`);
|
|
29
|
+
}
|
|
30
|
+
const description = stripQuotes(rawDescription.trim());
|
|
31
|
+
const heading = HEADING_PATTERN.exec(content)?.groups?.["heading"] ?? node_path_1.default.basename(filePath, ".md");
|
|
32
|
+
return { description, heading };
|
|
33
|
+
}
|
|
34
|
+
function stripQuotes(value) {
|
|
35
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
36
|
+
return value.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Discovers rules from the rules directory: each `<category>/<name>.md` file is a rule with id
|
|
42
|
+
* `<category>/<name>`. Categories are the directory names; no separate registration required.
|
|
43
|
+
*/
|
|
44
|
+
async function discoverRules(rulesRoot) {
|
|
45
|
+
const entries = await (0, promises_1.readdir)(rulesRoot, { withFileTypes: true });
|
|
46
|
+
const categories = entries
|
|
47
|
+
.filter((entry) => entry.isDirectory())
|
|
48
|
+
.map((entry) => entry.name)
|
|
49
|
+
.toSorted();
|
|
50
|
+
const rules = await Promise.all(categories.map(async (category) => {
|
|
51
|
+
const fileNames = await (0, promises_1.readdir)(node_path_1.default.join(rulesRoot, category));
|
|
52
|
+
const files = fileNames.filter((file) => file.endsWith(".md")).toSorted();
|
|
53
|
+
return await Promise.all(files.map(async (file) => {
|
|
54
|
+
const relativePath = node_path_1.default.join(category, file);
|
|
55
|
+
const content = await (0, promises_1.readFile)(node_path_1.default.join(rulesRoot, relativePath), "utf8");
|
|
56
|
+
const { description, heading } = parseRuleFile({ content, filePath: relativePath });
|
|
57
|
+
return {
|
|
58
|
+
category,
|
|
59
|
+
description,
|
|
60
|
+
heading,
|
|
61
|
+
id: `${category}/${node_path_1.default.basename(file, ".md")}`,
|
|
62
|
+
relativePath,
|
|
63
|
+
};
|
|
64
|
+
}));
|
|
65
|
+
}));
|
|
66
|
+
return rules.flat();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolves the rules to sync: profile categories expanded in order, plus includes, minus
|
|
70
|
+
* excludes. Unknown ids are reported (not fatal) so a stale `--include`/`--exclude` in a consuming
|
|
71
|
+
* repo degrades gracefully instead of breaking installs.
|
|
72
|
+
*/
|
|
73
|
+
function resolveRules(params) {
|
|
74
|
+
const { excludes, includes, profileCategories, rules } = params;
|
|
75
|
+
const rulesById = new Map(rules.map((rule) => [rule.id, rule]));
|
|
76
|
+
const unknownIds = [...includes, ...excludes].filter((id) => !rulesById.has(id));
|
|
77
|
+
const ruleIds = new Set([
|
|
78
|
+
...profileCategories.flatMap((category) => rules.filter((rule) => rule.category === category).map((rule) => rule.id)),
|
|
79
|
+
...includes.filter((id) => rulesById.has(id)),
|
|
80
|
+
]);
|
|
81
|
+
for (const id of excludes) {
|
|
82
|
+
ruleIds.delete(id);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
rules: [...ruleIds].map((id) => rulesById.get(id)).filter((rule) => rule !== undefined),
|
|
86
|
+
unknownIds,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function generateAgentsIndex(rules) {
|
|
90
|
+
const rows = rules.map((rule) => `| ${rule.heading} | .rules/${toPosixPath(rule.relativePath)} | ${rule.description} |`);
|
|
91
|
+
return [
|
|
92
|
+
"<!-- Generated by @clipboard-health/ai-rules -->",
|
|
93
|
+
"",
|
|
94
|
+
"# Coding Rules",
|
|
95
|
+
"",
|
|
96
|
+
"IMPORTANT: You MUST read the relevant rule files below before writing or reviewing code.",
|
|
97
|
+
"",
|
|
98
|
+
"| Rule | File | When to Read |",
|
|
99
|
+
"|------|------|-------------|",
|
|
100
|
+
...rows,
|
|
101
|
+
"",
|
|
102
|
+
"## Agent Skills",
|
|
103
|
+
"",
|
|
104
|
+
"Agent skills are linked from `node_modules/@clipboard-health/ai-rules` into `.agents/`.",
|
|
105
|
+
"If a referenced skill is missing or unreadable, run `npm ci` from the repository root and retry.",
|
|
106
|
+
"",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generates the README "Available Rules" tables from rule frontmatter. A test asserts the README
|
|
111
|
+
* contains this exact content; run scripts/populateReadme.ts after changing rules.
|
|
112
|
+
*/
|
|
113
|
+
function generateReadmeRulesSection(rules) {
|
|
114
|
+
const uniqueCategories = [...new Set(rules.map((rule) => rule.category))];
|
|
115
|
+
const categories = uniqueCategories.toSorted();
|
|
116
|
+
const sections = categories.map((category) => {
|
|
117
|
+
const rows = rules
|
|
118
|
+
.filter((rule) => rule.category === category)
|
|
119
|
+
.map((rule) => `| \`${rule.id}\` | ${rule.description} |`);
|
|
120
|
+
return [`### ${category}`, "", "| Rule ID | When to Read |", "| --- | --- |", ...rows].join("\n");
|
|
121
|
+
});
|
|
122
|
+
return sections.join("\n\n");
|
|
123
|
+
}
|
|
124
|
+
function toPosixPath(filePath) {
|
|
125
|
+
return filePath.split(node_path_1.default.sep).join("/");
|
|
126
|
+
}
|