@clipboard-health/ai-rules 1.8.0 → 2.0.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 +112 -19
- package/package.json +2 -5
- package/rules/backend/architecture.md +81 -0
- package/rules/backend/asyncMessaging.md +78 -0
- package/rules/backend/mongodb.md +59 -0
- package/rules/backend/notifications.md +18 -0
- package/rules/backend/postgres.md +5 -0
- package/rules/backend/restApiDesign.md +59 -0
- package/rules/backend/serviceTests.md +43 -0
- package/rules/common/configuration.md +21 -0
- package/rules/common/featureFlags.md +21 -0
- package/rules/common/gitWorkflow.md +13 -0
- package/rules/common/loggingObservability.md +43 -0
- package/rules/common/testing.md +12 -0
- package/rules/common/typeScript.md +86 -0
- package/rules/datamodeling/analytics.md +48 -0
- package/rules/datamodeling/castingDbtStagingModels.md +17 -0
- package/rules/datamodeling/dbtModelDevelopment.md +28 -0
- package/rules/datamodeling/dbtYamlDocumentation.md +18 -0
- package/rules/frontend/customHooks.md +36 -0
- package/rules/frontend/dataFetching.md +62 -0
- package/rules/frontend/e2eTesting.md +33 -0
- package/rules/frontend/errorHandling.md +39 -0
- package/rules/frontend/fileOrganization.md +25 -0
- package/rules/frontend/frontendTechnologyStack.md +10 -0
- package/rules/frontend/interactiveElements.md +9 -0
- package/rules/frontend/modalRoutes.md +15 -0
- package/rules/frontend/reactComponents.md +89 -0
- package/rules/frontend/styling.md +52 -0
- package/rules/frontend/testing.md +58 -0
- package/scripts/constants.js +81 -9
- package/scripts/execAndLog.js +21 -0
- package/scripts/sync.js +136 -42
- package/backend/AGENTS.md +0 -750
- package/backend/CLAUDE.md +0 -1
- package/common/AGENTS.md +0 -295
- package/common/CLAUDE.md +0 -1
- package/datamodeling/AGENTS.md +0 -124
- package/datamodeling/CLAUDE.md +0 -1
- package/frontend/AGENTS.md +0 -756
- package/frontend/CLAUDE.md +0 -1
- package/fullstack/AGENTS.md +0 -1211
- package/fullstack/CLAUDE.md +0 -1
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# @clipboard-health/ai-rules
|
|
2
2
|
|
|
3
|
-
Pre-built AI agent rules for consistent coding standards.
|
|
3
|
+
Pre-built AI agent rules for consistent coding standards. Uses a retrieval-based approach: generates a compressed index in `AGENTS.md` pointing to individual rule files that agents read on demand.
|
|
4
4
|
|
|
5
5
|
## Table of contents
|
|
6
6
|
|
|
7
|
-
- [
|
|
8
|
-
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
- [Install](#install)
|
|
8
|
+
- [Usage](#usage)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [Include/Exclude Rules](#includeexclude-rules)
|
|
11
|
+
- [Updating Rules](#updating-rules)
|
|
12
|
+
- [Available Rules](#available-rules)
|
|
13
|
+
- [Migration from v1](#migration-from-v1)
|
|
14
|
+
- [Local development commands](#local-development-commands)
|
|
14
15
|
|
|
15
16
|
## Install
|
|
16
17
|
|
|
@@ -22,7 +23,7 @@ npm install --save-dev @clipboard-health/ai-rules
|
|
|
22
23
|
|
|
23
24
|
### Quick Start
|
|
24
25
|
|
|
25
|
-
1. If you have an existing `AGENTS.md` and/or `CLAUDE.md` file in your repository, rename it to `OVERLAY.md`. The
|
|
26
|
+
1. If you have an existing `AGENTS.md` and/or `CLAUDE.md` file in your repository, rename it to `OVERLAY.md`. The sync script appends this file's contents to generated `AGENTS.md` so it's loaded into LLM agent contexts.
|
|
26
27
|
|
|
27
28
|
2. Choose the profile that matches your project type:
|
|
28
29
|
|
|
@@ -34,12 +35,6 @@ npm install --save-dev @clipboard-health/ai-rules
|
|
|
34
35
|
| `fullstack` | common + frontend + backend | Monorepos, fullstack apps |
|
|
35
36
|
| `datamodeling` | datamodeling | DBT data modeling |
|
|
36
37
|
|
|
37
|
-
**Rule categories:**
|
|
38
|
-
- **common**: TypeScript, testing, code style, error handling, key conventions
|
|
39
|
-
- **frontend**: React patterns, hooks, performance, styling, data fetching, custom hooks
|
|
40
|
-
- **backend**: NestJS APIs, three-tier architecture, controllers, services
|
|
41
|
-
- **datamodeling**: data modeling, testing, yaml documentation, data cleaning, analytics
|
|
42
|
-
|
|
43
38
|
3. Add it to your `package.json`:
|
|
44
39
|
|
|
45
40
|
```json
|
|
@@ -60,10 +55,35 @@ npm install --save-dev @clipboard-health/ai-rules
|
|
|
60
55
|
5. Commit the generated files:
|
|
61
56
|
|
|
62
57
|
```bash
|
|
63
|
-
git add .
|
|
58
|
+
git add rules/ AGENTS.md CLAUDE.md
|
|
64
59
|
git commit -m "feat: add AI coding rules"
|
|
65
60
|
```
|
|
66
61
|
|
|
62
|
+
### Include/Exclude Rules
|
|
63
|
+
|
|
64
|
+
Fine-tune which rules are synced using `--include` and `--exclude`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Backend profile without MongoDB rules
|
|
68
|
+
node sync.js backend --exclude backend/mongodb
|
|
69
|
+
|
|
70
|
+
# Common profile plus one backend rule
|
|
71
|
+
node sync.js common --include backend/architecture
|
|
72
|
+
|
|
73
|
+
# Multiple overrides
|
|
74
|
+
node sync.js backend --exclude backend/mongodb backend/postgres --include frontend/testing
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Update your `package.json` script accordingly:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"scripts": {
|
|
82
|
+
"sync-ai-rules": "node ./node_modules/@clipboard-health/ai-rules/scripts/sync.js backend --exclude backend/mongodb"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
67
87
|
### Updating Rules
|
|
68
88
|
|
|
69
89
|
When we release new rules or improvements:
|
|
@@ -72,17 +92,90 @@ When we release new rules or improvements:
|
|
|
72
92
|
# Update the package
|
|
73
93
|
npm update @clipboard-health/ai-rules
|
|
74
94
|
|
|
75
|
-
# The postinstall script automatically
|
|
95
|
+
# The postinstall script automatically syncs the latest files
|
|
76
96
|
npm install
|
|
77
97
|
|
|
78
98
|
# Review the changes
|
|
79
|
-
git diff .
|
|
99
|
+
git diff rules/ AGENTS.md
|
|
80
100
|
|
|
81
101
|
# Commit the updates
|
|
82
|
-
git add .
|
|
102
|
+
git add rules/ AGENTS.md CLAUDE.md
|
|
83
103
|
git commit -m "chore: update AI coding rules"
|
|
84
104
|
```
|
|
85
105
|
|
|
106
|
+
## Available Rules
|
|
107
|
+
|
|
108
|
+
### common
|
|
109
|
+
|
|
110
|
+
| Rule ID | Description |
|
|
111
|
+
| ----------------------------- | -------------------------------------------------------------- |
|
|
112
|
+
| `common/configuration` | Config decisions: secrets, SSM, feature flags, DB vs hardcoded |
|
|
113
|
+
| `common/featureFlags` | Feature flag naming, lifecycle, and cleanup |
|
|
114
|
+
| `common/gitWorkflow` | Commit messages, PR titles, and pull request guidelines |
|
|
115
|
+
| `common/loggingObservability` | Log levels, structured context, PII avoidance |
|
|
116
|
+
| `common/testing` | Unit test conventions and structure |
|
|
117
|
+
| `common/typeScript` | TypeScript naming, types, functions, error handling |
|
|
118
|
+
|
|
119
|
+
### backend
|
|
120
|
+
|
|
121
|
+
| Rule ID | Description |
|
|
122
|
+
| ------------------------ | --------------------------------------------------- |
|
|
123
|
+
| `backend/architecture` | Three-tier pattern: modules, services, controllers |
|
|
124
|
+
| `backend/asyncMessaging` | Queues, async messaging, background jobs |
|
|
125
|
+
| `backend/mongodb` | MongoDB/Mongoose: schemas, indexes, queries |
|
|
126
|
+
| `backend/notifications` | Notification and messaging patterns |
|
|
127
|
+
| `backend/postgres` | Postgres queries: Prisma, subqueries, feature flags |
|
|
128
|
+
| `backend/restApiDesign` | REST API design: JSON:API, endpoints, contracts |
|
|
129
|
+
| `backend/serviceTests` | Service-level integration tests |
|
|
130
|
+
|
|
131
|
+
### frontend
|
|
132
|
+
|
|
133
|
+
| Rule ID | Description |
|
|
134
|
+
| ---------------------------------- | -------------------------------------------- |
|
|
135
|
+
| `frontend/customHooks` | React custom hooks patterns |
|
|
136
|
+
| `frontend/dataFetching` | React Query, API calls, caching |
|
|
137
|
+
| `frontend/e2eTesting` | E2E testing with Playwright |
|
|
138
|
+
| `frontend/errorHandling` | React error handling patterns |
|
|
139
|
+
| `frontend/fileOrganization` | Frontend file and folder organization |
|
|
140
|
+
| `frontend/frontendTechnologyStack` | Frontend library and framework choices |
|
|
141
|
+
| `frontend/interactiveElements` | Forms, buttons, inputs |
|
|
142
|
+
| `frontend/modalRoutes` | Modals and route-based dialogs |
|
|
143
|
+
| `frontend/reactComponents` | React component patterns, props, composition |
|
|
144
|
+
| `frontend/styling` | CSS, themes, responsive design |
|
|
145
|
+
| `frontend/testing` | React Testing Library, component tests |
|
|
146
|
+
|
|
147
|
+
### datamodeling
|
|
148
|
+
|
|
149
|
+
| Rule ID | Description |
|
|
150
|
+
| -------------------------------------- | --------------------------------------- |
|
|
151
|
+
| `datamodeling/analytics` | Analytics data models |
|
|
152
|
+
| `datamodeling/castingDbtStagingModels` | Data type casting in dbt staging models |
|
|
153
|
+
| `datamodeling/dbtModelDevelopment` | dbt model naming, structure, testing |
|
|
154
|
+
| `datamodeling/dbtYamlDocumentation` | dbt YAML documentation and schema files |
|
|
155
|
+
|
|
156
|
+
## Migration from v1
|
|
157
|
+
|
|
158
|
+
v2 replaces the monolithic `AGENTS.md` with a retrieval-based approach. Rule files are now committed to your repo under `rules/`.
|
|
159
|
+
|
|
160
|
+
1. Update the package:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npm install --save-dev @clipboard-health/ai-rules@latest
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
2. Run install to trigger sync:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm install
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
3. Add `rules/` to git and commit:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
git add rules/ AGENTS.md CLAUDE.md
|
|
176
|
+
git commit -m "feat!: update ai-rules to v2 retrieval-based approach"
|
|
177
|
+
```
|
|
178
|
+
|
|
86
179
|
## Local development commands
|
|
87
180
|
|
|
88
181
|
See [`package.json`](./package.json) `scripts` for a list of commands.
|
package/package.json
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/ai-rules",
|
|
3
3
|
"description": "Pre-built AI agent rules for consistent coding standards.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"bugs": "https://github.com/ClipboardHealth/core-utils/issues",
|
|
6
|
-
"devDependencies": {
|
|
7
|
-
"@intellectronica/ruler": "0.3.31"
|
|
8
|
-
},
|
|
9
6
|
"keywords": [
|
|
10
7
|
"ai",
|
|
11
8
|
"best-practices",
|
|
@@ -28,6 +25,6 @@
|
|
|
28
25
|
"url": "git+https://github.com/ClipboardHealth/core-utils.git"
|
|
29
26
|
},
|
|
30
27
|
"scripts": {
|
|
31
|
-
"format": "prettier --write '
|
|
28
|
+
"format": "prettier --write 'rules/**/*.md'"
|
|
32
29
|
}
|
|
33
30
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Three-Tier Architecture
|
|
4
|
+
|
|
5
|
+
All NestJS microservices follow a three-tier layered architecture:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ Entrypoints (Controllers, message consumers) │
|
|
10
|
+
│ - HTTP request/response, JSON:API DTO translation, auth │
|
|
11
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
12
|
+
│ Logic (NestJS services, message publishers, background jobs) │
|
|
13
|
+
│ - ALL business logic; works with DOs only │
|
|
14
|
+
│ - Knows nothing about HTTP or database specifics │
|
|
15
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
16
|
+
│ Data (Data repositories, gateways) │
|
|
17
|
+
│ - Database via ORM (Prisma/Mongoose), DAO ↔ DO translation │
|
|
18
|
+
│ - External service integrations (Gateways) │
|
|
19
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Module Structure:**
|
|
23
|
+
|
|
24
|
+
`ts-rest` contracts:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
packages/contract-<service>/src/
|
|
28
|
+
├── index.ts
|
|
29
|
+
└── lib
|
|
30
|
+
├── constants.ts
|
|
31
|
+
└── contracts
|
|
32
|
+
├── contract.ts
|
|
33
|
+
├── health.contract.ts
|
|
34
|
+
├── index.ts
|
|
35
|
+
└── user
|
|
36
|
+
├── index.ts
|
|
37
|
+
├── user.contract.ts
|
|
38
|
+
└── shared.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
NestJS microservice modules:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
src/modules/user
|
|
45
|
+
├── user.module.ts
|
|
46
|
+
├── data
|
|
47
|
+
│ ├── user.dao.mapper.ts
|
|
48
|
+
│ └── user.repo.ts
|
|
49
|
+
├── entrypoints
|
|
50
|
+
│ ├── user.controller.ts
|
|
51
|
+
│ ├── user.dto.mapper.ts
|
|
52
|
+
│ └── userCreated.consumer.ts
|
|
53
|
+
└── logic
|
|
54
|
+
├── user.do.ts
|
|
55
|
+
├── user.service.ts
|
|
56
|
+
├── userCreated.service.ts
|
|
57
|
+
└── jobs
|
|
58
|
+
├── user.job.mapper.ts
|
|
59
|
+
├── userCreated.job.spec.ts
|
|
60
|
+
└── userCreated.job.ts
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**File Patterns:**
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
*.controller.ts - HTTP controllers (entrypoints)
|
|
67
|
+
*.consumer.ts - Message consumers (entrypoints)
|
|
68
|
+
*.service.ts - Business logic (logic)
|
|
69
|
+
*.job.ts - Background jobs (logic)
|
|
70
|
+
*.repo.ts - Database access (data)
|
|
71
|
+
*.gateway.ts - External services (data)
|
|
72
|
+
*.do.ts - Domain objects
|
|
73
|
+
*.dto.mapper.ts - DTO transformation
|
|
74
|
+
*.dao.mapper.ts - DAO transformation
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Microservices Principles:**
|
|
78
|
+
|
|
79
|
+
- One domain = one module (bounded contexts)
|
|
80
|
+
- Specific modules know about generic, not vice versa
|
|
81
|
+
- Don't block Node.js thread—use background jobs for expensive operations
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Async Messaging & Background Jobs
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
|
|
5
|
+
| Scenario | Solution |
|
|
6
|
+
| -------------------------------- | ----------------- |
|
|
7
|
+
| Same service producer/consumer | Background Jobs |
|
|
8
|
+
| Cross-service communication | EventBridge + SQS |
|
|
9
|
+
| Deferred work from API path | Background Jobs |
|
|
10
|
+
| Replacing `void` fire-and-forget | Background Jobs |
|
|
11
|
+
| Scaling CRON jobs | Background Jobs |
|
|
12
|
+
|
|
13
|
+
## Background Jobs
|
|
14
|
+
|
|
15
|
+
**Creation with Transaction:**
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
async function createLicense() {
|
|
19
|
+
await db.transaction(async (tx) => {
|
|
20
|
+
const license = await tx.license.create({ data });
|
|
21
|
+
await jobs.enqueue(VerificationJob, { licenseId: license.id }, { transaction: tx });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Handler Pattern:**
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
class ShiftReminderJob implements Handler<ShiftReminderPayload> {
|
|
30
|
+
static queueName = "shift.reminder";
|
|
31
|
+
|
|
32
|
+
async perform(payload: ShiftReminderPayload, job: Job): Promise<string> {
|
|
33
|
+
const { shiftId } = payload;
|
|
34
|
+
|
|
35
|
+
// Fetch fresh data—don't trust stale payload
|
|
36
|
+
const shift = await this.shiftRepo.findById({ id: shiftId });
|
|
37
|
+
|
|
38
|
+
if (!shift || shift.isCancelled) {
|
|
39
|
+
return `Skipping: shift ${shiftId} not found or cancelled`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await this.notificationService.performSideEffect(shift);
|
|
44
|
+
return `Reminder sent for shift ${shiftId}`;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof KnownRecoverableError) throw error; // Retry
|
|
47
|
+
return `Skipping: ${error.message}`; // No retry
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Key Practices:**
|
|
54
|
+
|
|
55
|
+
- Pass minimal arguments (IDs, not objects)
|
|
56
|
+
- Fetch fresh data in handler
|
|
57
|
+
- Implement idempotency
|
|
58
|
+
- Check state before action
|
|
59
|
+
- Use Expand/Contract for job code updates
|
|
60
|
+
|
|
61
|
+
**Avoid Circular Dependencies:**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Shared types file
|
|
65
|
+
export const NOTIFICATION_JOB = "shift-notification";
|
|
66
|
+
export interface NotificationJobPayload {
|
|
67
|
+
shiftId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Enqueue by string name
|
|
71
|
+
await jobs.enqueue<NotificationJobPayload>(NOTIFICATION_JOB, { shiftId });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## SQS/EventBridge
|
|
75
|
+
|
|
76
|
+
**Producer:** Single producer per message type, publish atomically (use jobs as outbox), deterministic message IDs, don't rely on strict ordering.
|
|
77
|
+
|
|
78
|
+
**Consumer:** Own queue per consumer, must be idempotent, separate process from API, don't auto-consume DLQs.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# MongoDB/Mongoose
|
|
2
|
+
|
|
3
|
+
**ObjectId:**
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import mongoose, { Types, Schema } from "mongoose";
|
|
7
|
+
|
|
8
|
+
const id = new Types.ObjectId();
|
|
9
|
+
|
|
10
|
+
// In schemas
|
|
11
|
+
const schema = new Schema({
|
|
12
|
+
authorId: { type: Schema.Types.ObjectId, ref: "User" },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// In interfaces
|
|
16
|
+
interface Post {
|
|
17
|
+
authorId: Types.ObjectId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Validation
|
|
21
|
+
if (mongoose.isObjectIdOrHexString(value)) {
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Indexes:**
|
|
26
|
+
|
|
27
|
+
- Add only when needed (slower writes tradeoff)
|
|
28
|
+
- Apply via code only
|
|
29
|
+
- Separate `indexes.ts` from `schema.ts`
|
|
30
|
+
- Index definitions only in owning service
|
|
31
|
+
- Set `autoIndex: false`
|
|
32
|
+
- Design covering indexes for high-traffic queries
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
models/User/
|
|
36
|
+
├── schema.ts # Schema definition, schemaName, InferSchemaType
|
|
37
|
+
├── indexes.ts # Index definitions only
|
|
38
|
+
├── types.ts # Re-export types
|
|
39
|
+
└── index.ts # Model creation and export
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Verify query plans:**
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const explanation = await ShiftModel.find(query).explain("executionStats");
|
|
46
|
+
// Check: totalDocsExamined ≈ totalDocsReturned
|
|
47
|
+
// Good: stage 'IXSCAN'; Bad: stage 'COLLSCAN'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Repository Pattern
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
class UserRepo {
|
|
54
|
+
// Named methods over generic CRUD
|
|
55
|
+
async findById(request: { id: UserId }): Promise<UserDo> {}
|
|
56
|
+
async findByEmail(request: { email: string }): Promise<UserDo> {}
|
|
57
|
+
async updateEmail(request: { id: UserId; email: string }): Promise<UserDo> {}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Notifications
|
|
2
|
+
|
|
3
|
+
Send via [Knock](https://docs.knock.app) using `@clipboard-health/notifications`.
|
|
4
|
+
|
|
5
|
+
Use `triggerChunked` to store full trigger request at job enqueue time. See package documentation for setup of `triggerNotification.job.ts`, `notificationClient.provider.ts`, and workflow keys.
|
|
6
|
+
|
|
7
|
+
**Job enqueue pattern:**
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const jobData: SerializableTriggerChunkedRequest = {
|
|
11
|
+
body: { recipients: ["userId-1"], data: notificationData },
|
|
12
|
+
expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(),
|
|
13
|
+
keysToRedact: ["secret"],
|
|
14
|
+
workflowKey: WORKFLOW_KEYS.eventStartingReminder,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
await adapter.enqueue(TRIGGER_NOTIFICATION_JOB_NAME, jobData, { session });
|
|
18
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Postgres
|
|
2
|
+
|
|
3
|
+
- Avoid correlated subqueries that execute per-row; they can exhaust connection pools on high-traffic endpoints
|
|
4
|
+
- Put significant query changes (new joins, subqueries, query rewrites) behind a feature flag for gradual rollout and instant rollback
|
|
5
|
+
- For complex queries (joins, aggregations, conditional filtering), prefer Prisma TypedSQL over Prisma client methods
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# REST API Design
|
|
2
|
+
|
|
3
|
+
## JSON:API Specification
|
|
4
|
+
|
|
5
|
+
Follow [JSON:API spec](https://jsonapi.org/).
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"data": [
|
|
10
|
+
{
|
|
11
|
+
"id": "1",
|
|
12
|
+
"type": "shift",
|
|
13
|
+
"attributes": { "qualification": "nurse" },
|
|
14
|
+
"relationships": {
|
|
15
|
+
"assignedWorker": {
|
|
16
|
+
"data": { "type": "worker", "id": "9" }
|
|
17
|
+
},
|
|
18
|
+
"location": {
|
|
19
|
+
"data": { "type": "workplace", "id": "17" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Singular `type` values: `shift` not `shifts`
|
|
28
|
+
- Links optional (use only for pagination)
|
|
29
|
+
- Use `include` for related resources
|
|
30
|
+
- Avoid `meta` unless necessary
|
|
31
|
+
|
|
32
|
+
## URLs
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
GET /urgent-shifts # lowercase kebab-case, plural nouns
|
|
36
|
+
GET /workers/:workerId/shifts
|
|
37
|
+
POST /workers/:workerId/referral-codes
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## HTTP Conventions
|
|
41
|
+
|
|
42
|
+
- No PUT support — use PATCH for updates
|
|
43
|
+
- POST returns the created resource DTO
|
|
44
|
+
- 422 for unsupported filters/sorts (not 400)
|
|
45
|
+
|
|
46
|
+
## Filtering, Sorting, Pagination
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
GET /shifts?filter[verified]=true&sort=startDate,-urgency&page[cursor]=abc&page[size]=50
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Cursor-based pagination only (not offset)
|
|
53
|
+
- Avoid count totals (performance)
|
|
54
|
+
- Only implement filters/sorts clients need
|
|
55
|
+
|
|
56
|
+
## Contracts
|
|
57
|
+
|
|
58
|
+
- Add contracts to `contract-<repo-name>` package
|
|
59
|
+
- Use `ts-rest` with composable Zod schemas
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Service Tests (Primary Testing Approach)
|
|
2
|
+
|
|
3
|
+
Test the public contract (REST endpoints, events) with real local dependencies (Postgres, Mongo, Redis). Fake slow/external services (Zendesk, Stripe).
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
describe("Documents", () => {
|
|
7
|
+
let tc: TestContext;
|
|
8
|
+
|
|
9
|
+
describe("GET /documents", () => {
|
|
10
|
+
it("returns existing documents for authenticated user", async () => {
|
|
11
|
+
const authToken = await tc.auth.createUser({ role: "employee" });
|
|
12
|
+
await tc.fixtures.createDocument({ name: "doc-1" });
|
|
13
|
+
|
|
14
|
+
const response = await tc.http.get("/documents", {
|
|
15
|
+
headers: { authorization: authToken },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(response.statusCode).toBe(200);
|
|
19
|
+
expect(response.parsedBody.data).toHaveLength(1);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Qualities:** One behavior per test, no shared setup, no mocking, <1 second, parallelizable.
|
|
26
|
+
|
|
27
|
+
**Testing Background Jobs:**
|
|
28
|
+
|
|
29
|
+
Don't spy on job enqueuing. Instead, run the job and assert side effects:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Run the job
|
|
33
|
+
await tc.jobs.drainQueues("shift.reminder");
|
|
34
|
+
|
|
35
|
+
// Assert side effects
|
|
36
|
+
const shift = await tc.http.get(`/shifts/${shiftId}`);
|
|
37
|
+
expect(shift.reminderSent).toBe(true);
|
|
38
|
+
|
|
39
|
+
// Or check fakes for external calls
|
|
40
|
+
expect(tc.fakes.notifications.requests).toHaveLength(1);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Side effects to assert: database changes, published messages, external HTTP requests.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
Contains secrets?
|
|
5
|
+
└── Yes → SSM Parameter Store
|
|
6
|
+
└── No → Engineers-only, tolerate 1hr propagation?
|
|
7
|
+
└── Yes → Hardcode with @clipboard-health/config
|
|
8
|
+
└── No → 1:1 with DB entity OR needs custom UI?
|
|
9
|
+
└── Yes → Database
|
|
10
|
+
└── No → LaunchDarkly feature flag
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
14
|
+
|
|
15
|
+
## Secrets
|
|
16
|
+
|
|
17
|
+
- `.env` locally (gitignored)
|
|
18
|
+
- Production: AWS SSM Parameter Store
|
|
19
|
+
- Prefer short-lived tokens
|
|
20
|
+
|
|
21
|
+
**Naming:** `[ENV]_[VENDOR]_[TYPE]_usedBy_[CLIENT]_[SCOPE]_[CREATED_AT]_[OWNER]`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Feature Flags
|
|
2
|
+
|
|
3
|
+
**Naming:** `YYYY-MM-[kind]-[subject]` (e.g., `2024-03-release-new-booking-flow`)
|
|
4
|
+
|
|
5
|
+
| Kind | Purpose |
|
|
6
|
+
| ------------ | ------------------------ |
|
|
7
|
+
| `release` | Gradual rollout to 100% |
|
|
8
|
+
| `enable` | Kill switch |
|
|
9
|
+
| `experiment` | Trial for small audience |
|
|
10
|
+
| `configure` | Runtime config |
|
|
11
|
+
|
|
12
|
+
**Rules:**
|
|
13
|
+
|
|
14
|
+
- "Off" = default/safer value
|
|
15
|
+
- No permanent flags (except `configure`)
|
|
16
|
+
- Create archival ticket when creating flag
|
|
17
|
+
- Validate staging before production
|
|
18
|
+
- Always provide default values in code
|
|
19
|
+
- Clean up after full launch
|
|
20
|
+
|
|
21
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Git Workflow
|
|
2
|
+
|
|
3
|
+
Follow Conventional Commits 1.0 spec for commit messages and PR titles.
|
|
4
|
+
|
|
5
|
+
## Pull Requests
|
|
6
|
+
|
|
7
|
+
1. Clear title: change summary + ticket
|
|
8
|
+
2. Thorough description: why, not just what
|
|
9
|
+
3. Small & focused: single concept
|
|
10
|
+
4. Tested: service tests + validation proof
|
|
11
|
+
5. Passing CI
|
|
12
|
+
|
|
13
|
+
Link ticket, context, reasoning, and areas of concern in description.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Logging & Observability
|
|
2
|
+
|
|
3
|
+
## Log Levels
|
|
4
|
+
|
|
5
|
+
| Level | When |
|
|
6
|
+
| ----- | ------------------------------------------ |
|
|
7
|
+
| ERROR | Required functionality broken (2am pager?) |
|
|
8
|
+
| WARN | Optional broken OR recovered from failure |
|
|
9
|
+
| INFO | Informative, ignorable during normal ops |
|
|
10
|
+
| DEBUG | Local only, not production |
|
|
11
|
+
|
|
12
|
+
## Best Practices
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// Bad
|
|
16
|
+
logger.error("Operation failed");
|
|
17
|
+
logger.error(`Operation failed for workplace ${workplaceId}`);
|
|
18
|
+
|
|
19
|
+
// Good—structured context
|
|
20
|
+
logger.error("Exporting urgent shifts to CSV failed", {
|
|
21
|
+
workplaceId,
|
|
22
|
+
startDate,
|
|
23
|
+
endDate,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
28
|
+
- Use metrics for counting:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Log IDs or specific fields instead of full objects:
|
|
35
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
36
|
+
- `shiftId` (not `shift`)
|
|
37
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const logContext = { shiftId, workerId };
|
|
41
|
+
logger.info("Processing shift", logContext);
|
|
42
|
+
logger.info("Notification sent", logContext);
|
|
43
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## Unit Tests
|
|
4
|
+
|
|
5
|
+
Use when: error handling hard to trigger black-box, concurrency scenarios, >5 variations, pure function logic.
|
|
6
|
+
|
|
7
|
+
## Conventions
|
|
8
|
+
|
|
9
|
+
- `describe` for grouping
|
|
10
|
+
- Arrange-Act-Assert with newlines between
|
|
11
|
+
- Variables: `mockX`, `input`, `expected`, `actual`
|
|
12
|
+
- Prefer `it.each` for multiple cases
|