@clipboard-health/ai-rules 1.7.8 → 1.7.10
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/backend/AGENTS.md +116 -44
- package/common/AGENTS.md +44 -18
- package/frontend/AGENTS.md +44 -19
- package/fullstack/AGENTS.md +116 -45
- package/package.json +1 -1
package/backend/AGENTS.md
CHANGED
|
@@ -25,23 +25,43 @@ All NestJS microservices follow a three-tier layered architecture:
|
|
|
25
25
|
|
|
26
26
|
**Module Structure:**
|
|
27
27
|
|
|
28
|
+
`ts-rest` contracts:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
packages/contract-<service>/src/
|
|
32
|
+
├── index.ts
|
|
33
|
+
└── lib
|
|
34
|
+
├── constants.ts
|
|
35
|
+
└── contracts
|
|
36
|
+
├── contract.ts
|
|
37
|
+
├── health.contract.ts
|
|
38
|
+
├── index.ts
|
|
39
|
+
└── user
|
|
40
|
+
├── index.ts
|
|
41
|
+
├── user.contract.ts
|
|
42
|
+
└── shared.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
NestJS microservice modules:
|
|
46
|
+
|
|
28
47
|
```text
|
|
29
|
-
modules/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
src/modules/user
|
|
49
|
+
├── user.module.ts
|
|
50
|
+
├── data
|
|
51
|
+
│ ├── user.dao.mapper.ts
|
|
52
|
+
│ └── user.repo.ts
|
|
53
|
+
├── entrypoints
|
|
54
|
+
│ ├── user.controller.ts
|
|
55
|
+
│ ├── user.dto.mapper.ts
|
|
56
|
+
│ └── userCreated.consumer.ts
|
|
57
|
+
└── logic
|
|
58
|
+
├── user.do.ts
|
|
59
|
+
├── user.service.ts
|
|
60
|
+
├── userCreated.service.ts
|
|
61
|
+
└── jobs
|
|
62
|
+
├── user.job.mapper.ts
|
|
63
|
+
├── userCreated.job.spec.ts
|
|
64
|
+
└── userCreated.job.ts
|
|
45
65
|
```
|
|
46
66
|
|
|
47
67
|
**File Patterns:**
|
|
@@ -61,7 +81,7 @@ modules/
|
|
|
61
81
|
**Tier Rules:**
|
|
62
82
|
|
|
63
83
|
- Controllers → Services (never repos directly)
|
|
64
|
-
- Services → Repos/Gateways within module (never controllers)
|
|
84
|
+
- Services → Repos/Gateways within module (never controllers and never Mongoose models directly)
|
|
65
85
|
- Repos → Database only (never services/repos/controllers)
|
|
66
86
|
- Entry points are thin layers calling services
|
|
67
87
|
- Enforce with `dependency-cruiser`
|
|
@@ -201,9 +221,10 @@ const result = await Users.aggregate<ResultType>()
|
|
|
201
221
|
|
|
202
222
|
```text
|
|
203
223
|
models/User/
|
|
204
|
-
├── schema.ts # Schema
|
|
205
|
-
├── indexes.ts #
|
|
206
|
-
|
|
224
|
+
├── schema.ts # Schema definition, schemaName, InferSchemaType
|
|
225
|
+
├── indexes.ts # Index definitions only
|
|
226
|
+
├── types.ts # Re-export types
|
|
227
|
+
└── index.ts # Model creation and export
|
|
207
228
|
```
|
|
208
229
|
|
|
209
230
|
**Verify query plans:**
|
|
@@ -261,6 +282,8 @@ throw new ServiceError({
|
|
|
261
282
|
- Guard clauses for preconditions
|
|
262
283
|
- Early returns for error conditions
|
|
263
284
|
- Happy path last
|
|
285
|
+
- Use `toError(unknownTypedError)` from `@clipboard-health/util-ts` over hardcoded strings or type casting (`as Error`)
|
|
286
|
+
- Use `ERROR_CODES` from `@clipboard-health/util-ts`, not `HttpStatus` from NestJS
|
|
264
287
|
|
|
265
288
|
## Controller Translation
|
|
266
289
|
|
|
@@ -317,14 +340,22 @@ Follow [JSON:API spec](https://jsonapi.org/).
|
|
|
317
340
|
"data": [
|
|
318
341
|
{
|
|
319
342
|
"id": "1",
|
|
320
|
-
"type": "
|
|
321
|
-
"attributes": { "
|
|
343
|
+
"type": "shift",
|
|
344
|
+
"attributes": { "qualification": "nurse" },
|
|
345
|
+
"relationships": {
|
|
346
|
+
"assignedWorker": {
|
|
347
|
+
"data": { "type": "worker", "id": "9" }
|
|
348
|
+
},
|
|
349
|
+
"location": {
|
|
350
|
+
"data": { "type": "workplace", "id": "17" }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
322
353
|
}
|
|
323
354
|
]
|
|
324
355
|
}
|
|
325
356
|
```
|
|
326
357
|
|
|
327
|
-
- Singular `type` values: `
|
|
358
|
+
- Singular `type` values: `shift` not `shifts`
|
|
328
359
|
- Links optional (use only for pagination)
|
|
329
360
|
- Use `include` for related resources
|
|
330
361
|
- Avoid `meta` unless necessary
|
|
@@ -390,16 +421,13 @@ describe("Documents", () => {
|
|
|
390
421
|
|
|
391
422
|
describe("GET /documents", () => {
|
|
392
423
|
it("returns existing documents for authenticated user", async () => {
|
|
393
|
-
// Arrange
|
|
394
424
|
const authToken = await tc.auth.createUser({ role: "employee" });
|
|
395
425
|
await tc.fixtures.createDocument({ name: "doc-1" });
|
|
396
426
|
|
|
397
|
-
// Act
|
|
398
427
|
const response = await tc.http.get("/documents", {
|
|
399
428
|
headers: { authorization: authToken },
|
|
400
429
|
});
|
|
401
430
|
|
|
402
|
-
// Assert
|
|
403
431
|
expect(response.statusCode).toBe(200);
|
|
404
432
|
expect(response.parsedBody.data).toHaveLength(1);
|
|
405
433
|
});
|
|
@@ -409,6 +437,24 @@ describe("Documents", () => {
|
|
|
409
437
|
|
|
410
438
|
**Qualities:** One behavior per test, no shared setup, no mocking, <1 second, parallelizable.
|
|
411
439
|
|
|
440
|
+
**Testing Background Jobs:**
|
|
441
|
+
|
|
442
|
+
Don't spy on job enqueuing. Instead, run the job and assert side effects:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// Run the job
|
|
446
|
+
await tc.jobs.drainQueues("shift.reminder");
|
|
447
|
+
|
|
448
|
+
// Assert side effects
|
|
449
|
+
const shift = await tc.http.get(`/shifts/${shiftId}`);
|
|
450
|
+
expect(shift.reminderSent).toBe(true);
|
|
451
|
+
|
|
452
|
+
// Or check fakes for external calls
|
|
453
|
+
expect(tc.fakes.notifications.requests).toHaveLength(1);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Side effects to assert: database changes, published messages, external HTTP requests.
|
|
457
|
+
|
|
412
458
|
<!-- Source: .ruler/common/commitMessages.md -->
|
|
413
459
|
|
|
414
460
|
# Commit Messages
|
|
@@ -429,6 +475,8 @@ Contains secrets?
|
|
|
429
475
|
└── No → LaunchDarkly feature flag
|
|
430
476
|
```
|
|
431
477
|
|
|
478
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
479
|
+
|
|
432
480
|
<!-- Source: .ruler/common/featureFlags.md -->
|
|
433
481
|
|
|
434
482
|
# Feature Flags
|
|
@@ -451,6 +499,8 @@ Contains secrets?
|
|
|
451
499
|
- Always provide default values in code
|
|
452
500
|
- Clean up after full launch
|
|
453
501
|
|
|
502
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
503
|
+
|
|
454
504
|
<!-- Source: .ruler/common/loggingObservability.md -->
|
|
455
505
|
|
|
456
506
|
# Logging & Observability
|
|
@@ -479,13 +529,23 @@ logger.error("Exporting urgent shifts to CSV failed", {
|
|
|
479
529
|
});
|
|
480
530
|
```
|
|
481
531
|
|
|
482
|
-
**Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
532
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
533
|
+
- Use metrics for counting:
|
|
483
534
|
|
|
484
|
-
|
|
535
|
+
```typescript
|
|
536
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
537
|
+
```
|
|
485
538
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
539
|
+
- Log IDs or specific fields instead of full objects:
|
|
540
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
541
|
+
- `shiftId` (not `shift`)
|
|
542
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
const logContext = { shiftId, workerId };
|
|
546
|
+
logger.info("Processing shift", logContext);
|
|
547
|
+
logger.info("Notification sent", logContext);
|
|
548
|
+
```
|
|
489
549
|
|
|
490
550
|
<!-- Source: .ruler/common/pullRequests.md -->
|
|
491
551
|
|
|
@@ -557,13 +617,11 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
557
617
|
|
|
558
618
|
## Naming Conventions
|
|
559
619
|
|
|
560
|
-
- Avoid acronyms and abbreviations
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
| Widely known acronyms in camelCase | Lowercase after first | `httpRequest`, `gpsPosition` |
|
|
566
|
-
| Files | Singular, dotted | `user.service.ts` |
|
|
620
|
+
- Avoid acronyms and abbreviations; for those widely known, use camelCase: `httpRequest`, `gpsPosition`, `cliArguments`, `apiResponse`
|
|
621
|
+
- File-scoped constants: `MAX_RETRY_COUNT`
|
|
622
|
+
- Instead of `agentRequirement`, `agentReq`, `workerType`, use `qualification`
|
|
623
|
+
- Instead of `agent`, `hcp`, `healthcareProvider`, use `worker`
|
|
624
|
+
- Instead of `facility`, `hcf`, `healthcareFacility`, use `workplace`
|
|
567
625
|
|
|
568
626
|
## Core Rules
|
|
569
627
|
|
|
@@ -577,6 +635,26 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
577
635
|
- Files read top-to-bottom: exports first, internal helpers below
|
|
578
636
|
- Boolean props: `is*`, `has*`, `should*`, `can*`
|
|
579
637
|
- Use const assertions for constants: `as const`
|
|
638
|
+
- Use `date-fns` for date/time manipulation and `@clipboard-health/date-time` for formatting
|
|
639
|
+
|
|
640
|
+
## Null/Undefined Checks
|
|
641
|
+
|
|
642
|
+
Use `isDefined` helper from `@clipboard-health/util-ts`:
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
// Bad: truthy check fails for 0, "", false
|
|
646
|
+
if (shiftId && facilityId) {
|
|
647
|
+
}
|
|
648
|
+
// Bad: use utility instead
|
|
649
|
+
if (shift === null) {
|
|
650
|
+
}
|
|
651
|
+
if (facility === undefined) {
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Good: explicit defined check
|
|
655
|
+
if (isDefined(shiftId) && isDefined(facilityId)) {
|
|
656
|
+
}
|
|
657
|
+
```
|
|
580
658
|
|
|
581
659
|
## Types
|
|
582
660
|
|
|
@@ -585,12 +663,6 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
585
663
|
function process(arg: unknown) {} // Better than any
|
|
586
664
|
function process<T>(arg: T) {} // Best
|
|
587
665
|
|
|
588
|
-
// Nullable checks
|
|
589
|
-
if (foo == null) {
|
|
590
|
-
} // Clear intent
|
|
591
|
-
if (isDefined(foo)) {
|
|
592
|
-
} // Better with utility
|
|
593
|
-
|
|
594
666
|
// Quantity values—always unambiguous
|
|
595
667
|
const money = { amountInMinorUnits: 500, currencyCode: "USD" };
|
|
596
668
|
const durationMinutes = 30;
|
package/common/AGENTS.md
CHANGED
|
@@ -20,6 +20,8 @@ Contains secrets?
|
|
|
20
20
|
└── No → LaunchDarkly feature flag
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
24
|
+
|
|
23
25
|
<!-- Source: .ruler/common/featureFlags.md -->
|
|
24
26
|
|
|
25
27
|
# Feature Flags
|
|
@@ -42,6 +44,8 @@ Contains secrets?
|
|
|
42
44
|
- Always provide default values in code
|
|
43
45
|
- Clean up after full launch
|
|
44
46
|
|
|
47
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
48
|
+
|
|
45
49
|
<!-- Source: .ruler/common/loggingObservability.md -->
|
|
46
50
|
|
|
47
51
|
# Logging & Observability
|
|
@@ -70,13 +74,23 @@ logger.error("Exporting urgent shifts to CSV failed", {
|
|
|
70
74
|
});
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
**Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
77
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
78
|
+
- Use metrics for counting:
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
```typescript
|
|
81
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
82
|
+
```
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
- Log IDs or specific fields instead of full objects:
|
|
85
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
86
|
+
- `shiftId` (not `shift`)
|
|
87
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const logContext = { shiftId, workerId };
|
|
91
|
+
logger.info("Processing shift", logContext);
|
|
92
|
+
logger.info("Notification sent", logContext);
|
|
93
|
+
```
|
|
80
94
|
|
|
81
95
|
<!-- Source: .ruler/common/pullRequests.md -->
|
|
82
96
|
|
|
@@ -148,13 +162,11 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
148
162
|
|
|
149
163
|
## Naming Conventions
|
|
150
164
|
|
|
151
|
-
- Avoid acronyms and abbreviations
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
| Widely known acronyms in camelCase | Lowercase after first | `httpRequest`, `gpsPosition` |
|
|
157
|
-
| Files | Singular, dotted | `user.service.ts` |
|
|
165
|
+
- Avoid acronyms and abbreviations; for those widely known, use camelCase: `httpRequest`, `gpsPosition`, `cliArguments`, `apiResponse`
|
|
166
|
+
- File-scoped constants: `MAX_RETRY_COUNT`
|
|
167
|
+
- Instead of `agentRequirement`, `agentReq`, `workerType`, use `qualification`
|
|
168
|
+
- Instead of `agent`, `hcp`, `healthcareProvider`, use `worker`
|
|
169
|
+
- Instead of `facility`, `hcf`, `healthcareFacility`, use `workplace`
|
|
158
170
|
|
|
159
171
|
## Core Rules
|
|
160
172
|
|
|
@@ -168,6 +180,26 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
168
180
|
- Files read top-to-bottom: exports first, internal helpers below
|
|
169
181
|
- Boolean props: `is*`, `has*`, `should*`, `can*`
|
|
170
182
|
- Use const assertions for constants: `as const`
|
|
183
|
+
- Use `date-fns` for date/time manipulation and `@clipboard-health/date-time` for formatting
|
|
184
|
+
|
|
185
|
+
## Null/Undefined Checks
|
|
186
|
+
|
|
187
|
+
Use `isDefined` helper from `@clipboard-health/util-ts`:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Bad: truthy check fails for 0, "", false
|
|
191
|
+
if (shiftId && facilityId) {
|
|
192
|
+
}
|
|
193
|
+
// Bad: use utility instead
|
|
194
|
+
if (shift === null) {
|
|
195
|
+
}
|
|
196
|
+
if (facility === undefined) {
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Good: explicit defined check
|
|
200
|
+
if (isDefined(shiftId) && isDefined(facilityId)) {
|
|
201
|
+
}
|
|
202
|
+
```
|
|
171
203
|
|
|
172
204
|
## Types
|
|
173
205
|
|
|
@@ -176,12 +208,6 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
176
208
|
function process(arg: unknown) {} // Better than any
|
|
177
209
|
function process<T>(arg: T) {} // Best
|
|
178
210
|
|
|
179
|
-
// Nullable checks
|
|
180
|
-
if (foo == null) {
|
|
181
|
-
} // Clear intent
|
|
182
|
-
if (isDefined(foo)) {
|
|
183
|
-
} // Better with utility
|
|
184
|
-
|
|
185
211
|
// Quantity values—always unambiguous
|
|
186
212
|
const money = { amountInMinorUnits: 500, currencyCode: "USD" };
|
|
187
213
|
const durationMinutes = 30;
|
package/frontend/AGENTS.md
CHANGED
|
@@ -20,6 +20,8 @@ Contains secrets?
|
|
|
20
20
|
└── No → LaunchDarkly feature flag
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
24
|
+
|
|
23
25
|
<!-- Source: .ruler/common/featureFlags.md -->
|
|
24
26
|
|
|
25
27
|
# Feature Flags
|
|
@@ -42,6 +44,8 @@ Contains secrets?
|
|
|
42
44
|
- Always provide default values in code
|
|
43
45
|
- Clean up after full launch
|
|
44
46
|
|
|
47
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
48
|
+
|
|
45
49
|
<!-- Source: .ruler/common/loggingObservability.md -->
|
|
46
50
|
|
|
47
51
|
# Logging & Observability
|
|
@@ -70,13 +74,23 @@ logger.error("Exporting urgent shifts to CSV failed", {
|
|
|
70
74
|
});
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
**Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
77
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
78
|
+
- Use metrics for counting:
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
```typescript
|
|
81
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
82
|
+
```
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
- Log IDs or specific fields instead of full objects:
|
|
85
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
86
|
+
- `shiftId` (not `shift`)
|
|
87
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const logContext = { shiftId, workerId };
|
|
91
|
+
logger.info("Processing shift", logContext);
|
|
92
|
+
logger.info("Notification sent", logContext);
|
|
93
|
+
```
|
|
80
94
|
|
|
81
95
|
<!-- Source: .ruler/common/pullRequests.md -->
|
|
82
96
|
|
|
@@ -148,13 +162,11 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
148
162
|
|
|
149
163
|
## Naming Conventions
|
|
150
164
|
|
|
151
|
-
- Avoid acronyms and abbreviations
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
| Widely known acronyms in camelCase | Lowercase after first | `httpRequest`, `gpsPosition` |
|
|
157
|
-
| Files | Singular, dotted | `user.service.ts` |
|
|
165
|
+
- Avoid acronyms and abbreviations; for those widely known, use camelCase: `httpRequest`, `gpsPosition`, `cliArguments`, `apiResponse`
|
|
166
|
+
- File-scoped constants: `MAX_RETRY_COUNT`
|
|
167
|
+
- Instead of `agentRequirement`, `agentReq`, `workerType`, use `qualification`
|
|
168
|
+
- Instead of `agent`, `hcp`, `healthcareProvider`, use `worker`
|
|
169
|
+
- Instead of `facility`, `hcf`, `healthcareFacility`, use `workplace`
|
|
158
170
|
|
|
159
171
|
## Core Rules
|
|
160
172
|
|
|
@@ -168,6 +180,26 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
168
180
|
- Files read top-to-bottom: exports first, internal helpers below
|
|
169
181
|
- Boolean props: `is*`, `has*`, `should*`, `can*`
|
|
170
182
|
- Use const assertions for constants: `as const`
|
|
183
|
+
- Use `date-fns` for date/time manipulation and `@clipboard-health/date-time` for formatting
|
|
184
|
+
|
|
185
|
+
## Null/Undefined Checks
|
|
186
|
+
|
|
187
|
+
Use `isDefined` helper from `@clipboard-health/util-ts`:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Bad: truthy check fails for 0, "", false
|
|
191
|
+
if (shiftId && facilityId) {
|
|
192
|
+
}
|
|
193
|
+
// Bad: use utility instead
|
|
194
|
+
if (shift === null) {
|
|
195
|
+
}
|
|
196
|
+
if (facility === undefined) {
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Good: explicit defined check
|
|
200
|
+
if (isDefined(shiftId) && isDefined(facilityId)) {
|
|
201
|
+
}
|
|
202
|
+
```
|
|
171
203
|
|
|
172
204
|
## Types
|
|
173
205
|
|
|
@@ -176,12 +208,6 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
176
208
|
function process(arg: unknown) {} // Better than any
|
|
177
209
|
function process<T>(arg: T) {} // Best
|
|
178
210
|
|
|
179
|
-
// Nullable checks
|
|
180
|
-
if (foo == null) {
|
|
181
|
-
} // Clear intent
|
|
182
|
-
if (isDefined(foo)) {
|
|
183
|
-
} // Better with utility
|
|
184
|
-
|
|
185
211
|
// Quantity values—always unambiguous
|
|
186
212
|
const money = { amountInMinorUnits: 500, currencyCode: "USD" };
|
|
187
213
|
const durationMinutes = 30;
|
|
@@ -560,7 +586,6 @@ export function Component({ userId, onUpdate }: Props) {
|
|
|
560
586
|
|
|
561
587
|
## Naming Conventions
|
|
562
588
|
|
|
563
|
-
- Components: `PascalCase`
|
|
564
589
|
- Event handlers: `handle*` (e.g., `handleClick`)
|
|
565
590
|
- Props interface: `Props` (co-located) or `ComponentNameProps` (exported)
|
|
566
591
|
|
package/fullstack/AGENTS.md
CHANGED
|
@@ -25,23 +25,43 @@ All NestJS microservices follow a three-tier layered architecture:
|
|
|
25
25
|
|
|
26
26
|
**Module Structure:**
|
|
27
27
|
|
|
28
|
+
`ts-rest` contracts:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
packages/contract-<service>/src/
|
|
32
|
+
├── index.ts
|
|
33
|
+
└── lib
|
|
34
|
+
├── constants.ts
|
|
35
|
+
└── contracts
|
|
36
|
+
├── contract.ts
|
|
37
|
+
├── health.contract.ts
|
|
38
|
+
├── index.ts
|
|
39
|
+
└── user
|
|
40
|
+
├── index.ts
|
|
41
|
+
├── user.contract.ts
|
|
42
|
+
└── shared.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
NestJS microservice modules:
|
|
46
|
+
|
|
28
47
|
```text
|
|
29
|
-
modules/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
src/modules/user
|
|
49
|
+
├── user.module.ts
|
|
50
|
+
├── data
|
|
51
|
+
│ ├── user.dao.mapper.ts
|
|
52
|
+
│ └── user.repo.ts
|
|
53
|
+
├── entrypoints
|
|
54
|
+
│ ├── user.controller.ts
|
|
55
|
+
│ ├── user.dto.mapper.ts
|
|
56
|
+
│ └── userCreated.consumer.ts
|
|
57
|
+
└── logic
|
|
58
|
+
├── user.do.ts
|
|
59
|
+
├── user.service.ts
|
|
60
|
+
├── userCreated.service.ts
|
|
61
|
+
└── jobs
|
|
62
|
+
├── user.job.mapper.ts
|
|
63
|
+
├── userCreated.job.spec.ts
|
|
64
|
+
└── userCreated.job.ts
|
|
45
65
|
```
|
|
46
66
|
|
|
47
67
|
**File Patterns:**
|
|
@@ -61,7 +81,7 @@ modules/
|
|
|
61
81
|
**Tier Rules:**
|
|
62
82
|
|
|
63
83
|
- Controllers → Services (never repos directly)
|
|
64
|
-
- Services → Repos/Gateways within module (never controllers)
|
|
84
|
+
- Services → Repos/Gateways within module (never controllers and never Mongoose models directly)
|
|
65
85
|
- Repos → Database only (never services/repos/controllers)
|
|
66
86
|
- Entry points are thin layers calling services
|
|
67
87
|
- Enforce with `dependency-cruiser`
|
|
@@ -201,9 +221,10 @@ const result = await Users.aggregate<ResultType>()
|
|
|
201
221
|
|
|
202
222
|
```text
|
|
203
223
|
models/User/
|
|
204
|
-
├── schema.ts # Schema
|
|
205
|
-
├── indexes.ts #
|
|
206
|
-
|
|
224
|
+
├── schema.ts # Schema definition, schemaName, InferSchemaType
|
|
225
|
+
├── indexes.ts # Index definitions only
|
|
226
|
+
├── types.ts # Re-export types
|
|
227
|
+
└── index.ts # Model creation and export
|
|
207
228
|
```
|
|
208
229
|
|
|
209
230
|
**Verify query plans:**
|
|
@@ -261,6 +282,8 @@ throw new ServiceError({
|
|
|
261
282
|
- Guard clauses for preconditions
|
|
262
283
|
- Early returns for error conditions
|
|
263
284
|
- Happy path last
|
|
285
|
+
- Use `toError(unknownTypedError)` from `@clipboard-health/util-ts` over hardcoded strings or type casting (`as Error`)
|
|
286
|
+
- Use `ERROR_CODES` from `@clipboard-health/util-ts`, not `HttpStatus` from NestJS
|
|
264
287
|
|
|
265
288
|
## Controller Translation
|
|
266
289
|
|
|
@@ -317,14 +340,22 @@ Follow [JSON:API spec](https://jsonapi.org/).
|
|
|
317
340
|
"data": [
|
|
318
341
|
{
|
|
319
342
|
"id": "1",
|
|
320
|
-
"type": "
|
|
321
|
-
"attributes": { "
|
|
343
|
+
"type": "shift",
|
|
344
|
+
"attributes": { "qualification": "nurse" },
|
|
345
|
+
"relationships": {
|
|
346
|
+
"assignedWorker": {
|
|
347
|
+
"data": { "type": "worker", "id": "9" }
|
|
348
|
+
},
|
|
349
|
+
"location": {
|
|
350
|
+
"data": { "type": "workplace", "id": "17" }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
322
353
|
}
|
|
323
354
|
]
|
|
324
355
|
}
|
|
325
356
|
```
|
|
326
357
|
|
|
327
|
-
- Singular `type` values: `
|
|
358
|
+
- Singular `type` values: `shift` not `shifts`
|
|
328
359
|
- Links optional (use only for pagination)
|
|
329
360
|
- Use `include` for related resources
|
|
330
361
|
- Avoid `meta` unless necessary
|
|
@@ -390,16 +421,13 @@ describe("Documents", () => {
|
|
|
390
421
|
|
|
391
422
|
describe("GET /documents", () => {
|
|
392
423
|
it("returns existing documents for authenticated user", async () => {
|
|
393
|
-
// Arrange
|
|
394
424
|
const authToken = await tc.auth.createUser({ role: "employee" });
|
|
395
425
|
await tc.fixtures.createDocument({ name: "doc-1" });
|
|
396
426
|
|
|
397
|
-
// Act
|
|
398
427
|
const response = await tc.http.get("/documents", {
|
|
399
428
|
headers: { authorization: authToken },
|
|
400
429
|
});
|
|
401
430
|
|
|
402
|
-
// Assert
|
|
403
431
|
expect(response.statusCode).toBe(200);
|
|
404
432
|
expect(response.parsedBody.data).toHaveLength(1);
|
|
405
433
|
});
|
|
@@ -409,6 +437,24 @@ describe("Documents", () => {
|
|
|
409
437
|
|
|
410
438
|
**Qualities:** One behavior per test, no shared setup, no mocking, <1 second, parallelizable.
|
|
411
439
|
|
|
440
|
+
**Testing Background Jobs:**
|
|
441
|
+
|
|
442
|
+
Don't spy on job enqueuing. Instead, run the job and assert side effects:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// Run the job
|
|
446
|
+
await tc.jobs.drainQueues("shift.reminder");
|
|
447
|
+
|
|
448
|
+
// Assert side effects
|
|
449
|
+
const shift = await tc.http.get(`/shifts/${shiftId}`);
|
|
450
|
+
expect(shift.reminderSent).toBe(true);
|
|
451
|
+
|
|
452
|
+
// Or check fakes for external calls
|
|
453
|
+
expect(tc.fakes.notifications.requests).toHaveLength(1);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
Side effects to assert: database changes, published messages, external HTTP requests.
|
|
457
|
+
|
|
412
458
|
<!-- Source: .ruler/common/commitMessages.md -->
|
|
413
459
|
|
|
414
460
|
# Commit Messages
|
|
@@ -429,6 +475,8 @@ Contains secrets?
|
|
|
429
475
|
└── No → LaunchDarkly feature flag
|
|
430
476
|
```
|
|
431
477
|
|
|
478
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
479
|
+
|
|
432
480
|
<!-- Source: .ruler/common/featureFlags.md -->
|
|
433
481
|
|
|
434
482
|
# Feature Flags
|
|
@@ -451,6 +499,8 @@ Contains secrets?
|
|
|
451
499
|
- Always provide default values in code
|
|
452
500
|
- Clean up after full launch
|
|
453
501
|
|
|
502
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
503
|
+
|
|
454
504
|
<!-- Source: .ruler/common/loggingObservability.md -->
|
|
455
505
|
|
|
456
506
|
# Logging & Observability
|
|
@@ -479,13 +529,23 @@ logger.error("Exporting urgent shifts to CSV failed", {
|
|
|
479
529
|
});
|
|
480
530
|
```
|
|
481
531
|
|
|
482
|
-
**Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
532
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
533
|
+
- Use metrics for counting:
|
|
483
534
|
|
|
484
|
-
|
|
535
|
+
```typescript
|
|
536
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
537
|
+
```
|
|
485
538
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
539
|
+
- Log IDs or specific fields instead of full objects:
|
|
540
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
541
|
+
- `shiftId` (not `shift`)
|
|
542
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
const logContext = { shiftId, workerId };
|
|
546
|
+
logger.info("Processing shift", logContext);
|
|
547
|
+
logger.info("Notification sent", logContext);
|
|
548
|
+
```
|
|
489
549
|
|
|
490
550
|
<!-- Source: .ruler/common/pullRequests.md -->
|
|
491
551
|
|
|
@@ -557,13 +617,11 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
557
617
|
|
|
558
618
|
## Naming Conventions
|
|
559
619
|
|
|
560
|
-
- Avoid acronyms and abbreviations
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
| Widely known acronyms in camelCase | Lowercase after first | `httpRequest`, `gpsPosition` |
|
|
566
|
-
| Files | Singular, dotted | `user.service.ts` |
|
|
620
|
+
- Avoid acronyms and abbreviations; for those widely known, use camelCase: `httpRequest`, `gpsPosition`, `cliArguments`, `apiResponse`
|
|
621
|
+
- File-scoped constants: `MAX_RETRY_COUNT`
|
|
622
|
+
- Instead of `agentRequirement`, `agentReq`, `workerType`, use `qualification`
|
|
623
|
+
- Instead of `agent`, `hcp`, `healthcareProvider`, use `worker`
|
|
624
|
+
- Instead of `facility`, `hcf`, `healthcareFacility`, use `workplace`
|
|
567
625
|
|
|
568
626
|
## Core Rules
|
|
569
627
|
|
|
@@ -577,6 +635,26 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
577
635
|
- Files read top-to-bottom: exports first, internal helpers below
|
|
578
636
|
- Boolean props: `is*`, `has*`, `should*`, `can*`
|
|
579
637
|
- Use const assertions for constants: `as const`
|
|
638
|
+
- Use `date-fns` for date/time manipulation and `@clipboard-health/date-time` for formatting
|
|
639
|
+
|
|
640
|
+
## Null/Undefined Checks
|
|
641
|
+
|
|
642
|
+
Use `isDefined` helper from `@clipboard-health/util-ts`:
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
// Bad: truthy check fails for 0, "", false
|
|
646
|
+
if (shiftId && facilityId) {
|
|
647
|
+
}
|
|
648
|
+
// Bad: use utility instead
|
|
649
|
+
if (shift === null) {
|
|
650
|
+
}
|
|
651
|
+
if (facility === undefined) {
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Good: explicit defined check
|
|
655
|
+
if (isDefined(shiftId) && isDefined(facilityId)) {
|
|
656
|
+
}
|
|
657
|
+
```
|
|
580
658
|
|
|
581
659
|
## Types
|
|
582
660
|
|
|
@@ -585,12 +663,6 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
585
663
|
function process(arg: unknown) {} // Better than any
|
|
586
664
|
function process<T>(arg: T) {} // Best
|
|
587
665
|
|
|
588
|
-
// Nullable checks
|
|
589
|
-
if (foo == null) {
|
|
590
|
-
} // Clear intent
|
|
591
|
-
if (isDefined(foo)) {
|
|
592
|
-
} // Better with utility
|
|
593
|
-
|
|
594
666
|
// Quantity values—always unambiguous
|
|
595
667
|
const money = { amountInMinorUnits: 500, currencyCode: "USD" };
|
|
596
668
|
const durationMinutes = 30;
|
|
@@ -969,7 +1041,6 @@ export function Component({ userId, onUpdate }: Props) {
|
|
|
969
1041
|
|
|
970
1042
|
## Naming Conventions
|
|
971
1043
|
|
|
972
|
-
- Components: `PascalCase`
|
|
973
1044
|
- Event handlers: `handle*` (e.g., `handleClick`)
|
|
974
1045
|
- Props interface: `Props` (co-located) or `ComponentNameProps` (exported)
|
|
975
1046
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipboard-health/ai-rules",
|
|
3
3
|
"description": "Pre-built AI agent rules for consistent coding standards.",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.10",
|
|
5
5
|
"bugs": "https://github.com/ClipboardHealth/core-utils/issues",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@intellectronica/ruler": "0.3.24"
|