@clipboard-health/ai-rules 2.26.0 → 2.28.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.
Files changed (39) hide show
  1. package/README.md +56 -39
  2. package/package.json +1 -1
  3. package/rules/backend/architecture.md +4 -0
  4. package/rules/backend/asyncMessaging.md +4 -0
  5. package/rules/backend/infrastructure.md +4 -0
  6. package/rules/backend/mongodb.md +4 -33
  7. package/rules/backend/notifications.md +4 -0
  8. package/rules/backend/postgres.md +4 -0
  9. package/rules/backend/restApiDesign.md +10 -54
  10. package/rules/backend/serviceTests.md +4 -0
  11. package/rules/common/configuration.md +4 -0
  12. package/rules/common/coreLibraries.md +4 -0
  13. package/rules/common/featureFlags.md +4 -0
  14. package/rules/common/gitWorkflow.md +4 -0
  15. package/rules/common/libraryAuthoring.md +13 -0
  16. package/rules/common/loggingObservability.md +4 -0
  17. package/rules/common/testing.md +5 -1
  18. package/rules/common/typeScript.md +12 -61
  19. package/rules/datamodeling/analytics.md +22 -15
  20. package/rules/datamodeling/castingDbtStagingModels.md +4 -0
  21. package/rules/datamodeling/dbtModelDevelopment.md +16 -12
  22. package/rules/datamodeling/dbtYamlDocumentation.md +4 -0
  23. package/rules/frontend/{fileOrganization.md → architecture.md} +22 -1
  24. package/rules/frontend/customHooks.md +4 -12
  25. package/rules/frontend/dataFetching.md +25 -16
  26. package/rules/frontend/e2eTesting.md +5 -10
  27. package/rules/frontend/reactComponents.md +48 -42
  28. package/rules/frontend/styling.md +91 -6
  29. package/rules/frontend/testing.md +6 -35
  30. package/scripts/constants.js +1 -79
  31. package/scripts/rules.js +126 -0
  32. package/scripts/sync.js +20 -61
  33. package/rules/frontend/bottomSheets.md +0 -25
  34. package/rules/frontend/businessLogicPlacement.md +0 -3
  35. package/rules/frontend/errorHandling.md +0 -39
  36. package/rules/frontend/frontendTechnologyStack.md +0 -10
  37. package/rules/frontend/interactiveElements.md +0 -19
  38. package/rules/frontend/modalRoutes.md +0 -15
  39. package/skills/flaky-test-bulk-debugger/SKILL.md +0 -89
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: "Writing dbt YAML documentation and schema files"
3
+ ---
4
+
1
5
  # .yaml documentation rules
2
6
 
3
7
  - The YAML should include the following sections: `version`, `models`, and `columns`.
@@ -1,4 +1,21 @@
1
- # File Organization
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 , use the `meta` pattern to handle errors. This rule doesn't apply to useMutation, using onError for useMutation is a valid pattern
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
- ## Conditional Fetching
68
+ ## `parsedApi.ts` vs `api.ts`
51
69
 
52
- ```typescript
53
- const { data } = useGetFeature({ id: dependencyData?.id }, { enabled: isDefined(dependencyData?.id) });
54
- ```
70
+ Frontend repos have two API layers:
55
71
 
56
- ## Mutations
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
- ```typescript
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 (role, label, text)—avoid CSS selectors
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
- # React Components
2
-
3
- ## Type vs Interface
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
- // 5. Main render
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
@@ -41,12 +45,93 @@ Use `rem` for fonts/heights (scales with user zoom), spacing indices for padding
41
45
  - ✅ Use full names: `padding`, `paddingX`, `marginY`
42
46
  - ❌ Avoid abbreviations: `p`, `px`, `my`
43
47
 
44
- ## Pseudo-classes
48
+ ## Dimensions
49
+
50
+ Always specify explicit `width`/`height`/`minWidth`/`maxWidth`/`minHeight`/`maxHeight` on elements whose layout should not depend on their content's intrinsic size (images, illustrations, fixed cards, skeletons). Use unit choices in this order:
51
+
52
+ 1. **Theme-spacing tokens** (`theme.spacing(n)`) — preferred for any dimension within the spacing scale. The CBH theme uses an **array-based** spacing scale, so `n` is an **index, not a multiplier**:
53
+
54
+ | n | px |
55
+ | --- | --- |
56
+ | 0 | 0 |
57
+ | 1 | 4 |
58
+ | 2 | 6 |
59
+ | 3 | 8 |
60
+ | 4 | 12 |
61
+ | 5 | 16 |
62
+ | 6 | 20 |
63
+ | 7 | 24 |
64
+ | 8 | 32 |
65
+ | 9 | 40 |
66
+ | 10 | 48 |
67
+ | 11 | 56 |
68
+ | 12 | 64 |
69
+
70
+ Indices > 12 are out of range and resolve to `undefined` (which serializes to `0`/empty — usually rendered as `0%`). Max representable size is **64 px**.
71
+
72
+ 2. **`rem`** — for any dimension > 64 px (e.g. hero illustrations, full-page graphics). Scales with user zoom. Examples: `"16rem"` (≈256px), `"11rem"` (≈176px).
73
+
74
+ 3. **Percentages / `vw` / `vh`** — for responsive layouts that should track the viewport or parent container.
75
+
76
+ 4. **Raw pixel strings** (`"258px"`) — only as a last resort with justification.
77
+
78
+ **MUI gotcha:** in `sx`, numeric `width`/`height` values are treated as **raw pixels**, not `theme.spacing(n)`. The spacing shortcut only applies to `padding`/`margin`/`gap`. To get theme spacing on a dimension, call `theme.spacing()` explicitly via the `sx` callback.
45
79
 
46
80
  ```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
- })} />
81
+ // Correct — within the spacing scale, via callback
82
+ <Box sx={(theme) => ({ width: theme.spacing(8), height: theme.spacing(8) })} />
83
+
84
+ // Correct larger than the spacing scale, use rem
85
+ <Box sx={{ width: "16rem", height: "11rem" }} />
86
+
87
+ // ❌ Wrong — out of range (max index is 12). theme.spacing(32) returns undefined → renders as 0
88
+ <Box sx={(theme) => ({ width: theme.spacing(32), height: theme.spacing(22) })} />
89
+
90
+ // ❌ Wrong — renders as 8×8 px (raw pixels), not theme.spacing(8) = 32px
91
+ <Box sx={{ width: 8, height: 8 }} />
92
+
93
+ // ❌ Wrong — raw pixel string without justification
94
+ <Box sx={{ width: "258px", height: "176px" }} />
95
+ ```
96
+
97
+ For `padding`/`margin`/`gap`, numeric values pass through the spacing scale automatically — `padding: 4` is `12px`. That auto-resolution does **not** happen for `width`/`height`. The asymmetry is a MUI quirk, not a CBH choice.
98
+
99
+ ## Images
100
+
101
+ Images (`<img>`, `<Box component="img">`, `<Image>`, MUI `<Avatar>` with an image `src`, SVG components rendered from asset files) **must specify both an explicit `width` and `height`** per the Dimensions rule above. Pair with `objectFit: "contain"` (or `"cover"`) so a swapped asset stays inside the fixed box.
102
+
103
+ **Why:** without explicit dimensions, the rendered box collapses to the source asset's intrinsic aspect ratio. Swapping the asset (e.g. a redesigned PNG/SVG with different dimensions) then shifts surrounding layout — in the worst case, pushing content off the viewport.
104
+
105
+ ```typescript
106
+ // ✅ Correct — small icon, theme spacing fits
107
+ <Image
108
+ src="/assets/icons/check.svg"
109
+ alt="Verified"
110
+ sx={(theme) => ({
111
+ width: theme.spacing(6),
112
+ height: theme.spacing(6),
113
+ objectFit: "contain",
114
+ })}
115
+ />
116
+
117
+ // ✅ Correct — hero illustration, larger than spacing scale, use rem
118
+ <Image
119
+ src="/assets/images/work-badge.svg"
120
+ alt="Create Work Badge"
121
+ width="16rem"
122
+ height="11rem"
123
+ sx={{ objectFit: "contain" }}
124
+ />
125
+
126
+ // ❌ Wrong — width/height props on Image flow into sx as raw pixels (8×8, not theme.spacing(8))
127
+ <Image src="..." alt="..." width={8} height={8} />
128
+
129
+ // ❌ Wrong — theme.spacing(32) out of range, renders as 0
130
+ <Image src="..." alt="..." sx={(theme) => ({ width: theme.spacing(32) })} />
131
+
132
+ // ❌ Wrong — height tracks the source asset's aspect ratio
133
+ <Image src="..." alt="..." width="258px" height="auto" />
134
+
135
+ // ❌ Wrong — no dimensions; layout depends entirely on the asset's intrinsic size
136
+ <Image src="/assets/images/work-badge.png" alt="Work Badge" />
52
137
  ```
@@ -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
- ## Test Structure
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
- 1. `getByRole` (best for accessibility)
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
@@ -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.CATEGORIES = exports.RULE_FILES = exports.FILES = exports.PATHS = void 0;
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"] },
@@ -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
+ }