@clipboard-health/ai-rules 1.7.8 → 1.7.9
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 -45
- package/common/AGENTS.md +44 -18
- package/frontend/AGENTS.md +44 -19
- package/fullstack/AGENTS.md +116 -46
- 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,21 @@ Follow [JSON:API spec](https://jsonapi.org/).
|
|
|
317
340
|
"data": [
|
|
318
341
|
{
|
|
319
342
|
"id": "1",
|
|
320
|
-
"type": "
|
|
321
|
-
"attributes": { "
|
|
322
|
-
|
|
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
|
+
}
|
|
323
353
|
]
|
|
324
354
|
}
|
|
325
355
|
```
|
|
326
356
|
|
|
327
|
-
- Singular `type` values: `
|
|
357
|
+
- Singular `type` values: `shift` not `shifts`
|
|
328
358
|
- Links optional (use only for pagination)
|
|
329
359
|
- Use `include` for related resources
|
|
330
360
|
- Avoid `meta` unless necessary
|
|
@@ -390,16 +420,13 @@ describe("Documents", () => {
|
|
|
390
420
|
|
|
391
421
|
describe("GET /documents", () => {
|
|
392
422
|
it("returns existing documents for authenticated user", async () => {
|
|
393
|
-
// Arrange
|
|
394
423
|
const authToken = await tc.auth.createUser({ role: "employee" });
|
|
395
424
|
await tc.fixtures.createDocument({ name: "doc-1" });
|
|
396
425
|
|
|
397
|
-
// Act
|
|
398
426
|
const response = await tc.http.get("/documents", {
|
|
399
427
|
headers: { authorization: authToken },
|
|
400
428
|
});
|
|
401
429
|
|
|
402
|
-
// Assert
|
|
403
430
|
expect(response.statusCode).toBe(200);
|
|
404
431
|
expect(response.parsedBody.data).toHaveLength(1);
|
|
405
432
|
});
|
|
@@ -409,6 +436,24 @@ describe("Documents", () => {
|
|
|
409
436
|
|
|
410
437
|
**Qualities:** One behavior per test, no shared setup, no mocking, <1 second, parallelizable.
|
|
411
438
|
|
|
439
|
+
**Testing Background Jobs:**
|
|
440
|
+
|
|
441
|
+
Don't spy on job enqueuing. Instead, run the job and assert side effects:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// Run the job
|
|
445
|
+
await tc.jobs.drainQueues("shift.reminder");
|
|
446
|
+
|
|
447
|
+
// Assert side effects
|
|
448
|
+
const shift = await tc.http.get(`/shifts/${shiftId}`);
|
|
449
|
+
expect(shift.reminderSent).toBe(true);
|
|
450
|
+
|
|
451
|
+
// Or check fakes for external calls
|
|
452
|
+
expect(tc.fakes.notifications.requests).toHaveLength(1);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Side effects to assert: database changes, published messages, external HTTP requests.
|
|
456
|
+
|
|
412
457
|
<!-- Source: .ruler/common/commitMessages.md -->
|
|
413
458
|
|
|
414
459
|
# Commit Messages
|
|
@@ -429,6 +474,8 @@ Contains secrets?
|
|
|
429
474
|
└── No → LaunchDarkly feature flag
|
|
430
475
|
```
|
|
431
476
|
|
|
477
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
478
|
+
|
|
432
479
|
<!-- Source: .ruler/common/featureFlags.md -->
|
|
433
480
|
|
|
434
481
|
# Feature Flags
|
|
@@ -451,6 +498,8 @@ Contains secrets?
|
|
|
451
498
|
- Always provide default values in code
|
|
452
499
|
- Clean up after full launch
|
|
453
500
|
|
|
501
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
502
|
+
|
|
454
503
|
<!-- Source: .ruler/common/loggingObservability.md -->
|
|
455
504
|
|
|
456
505
|
# Logging & Observability
|
|
@@ -479,13 +528,23 @@ logger.error("Exporting urgent shifts to CSV failed", {
|
|
|
479
528
|
});
|
|
480
529
|
```
|
|
481
530
|
|
|
482
|
-
**Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
531
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
532
|
+
- Use metrics for counting:
|
|
483
533
|
|
|
484
|
-
|
|
534
|
+
```typescript
|
|
535
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
536
|
+
```
|
|
485
537
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
538
|
+
- Log IDs or specific fields instead of full objects:
|
|
539
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
540
|
+
- `shiftId` (not `shift`)
|
|
541
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const logContext = { shiftId, workerId };
|
|
545
|
+
logger.info("Processing shift", logContext);
|
|
546
|
+
logger.info("Notification sent", logContext);
|
|
547
|
+
```
|
|
489
548
|
|
|
490
549
|
<!-- Source: .ruler/common/pullRequests.md -->
|
|
491
550
|
|
|
@@ -557,13 +616,11 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
557
616
|
|
|
558
617
|
## Naming Conventions
|
|
559
618
|
|
|
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` |
|
|
619
|
+
- Avoid acronyms and abbreviations; for those widely known, use camelCase: `httpRequest`, `gpsPosition`, `cliArguments`, `apiResponse`
|
|
620
|
+
- File-scoped constants: `MAX_RETRY_COUNT`
|
|
621
|
+
- Instead of `agentRequirement`, `agentReq`, `workerType`, use `qualification`
|
|
622
|
+
- Instead of `agent`, `hcp`, `healthcareProvider`, use `worker`
|
|
623
|
+
- Instead of `facility`, `hcf`, `healthcareFacility`, use `workplace`
|
|
567
624
|
|
|
568
625
|
## Core Rules
|
|
569
626
|
|
|
@@ -577,6 +634,26 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
577
634
|
- Files read top-to-bottom: exports first, internal helpers below
|
|
578
635
|
- Boolean props: `is*`, `has*`, `should*`, `can*`
|
|
579
636
|
- Use const assertions for constants: `as const`
|
|
637
|
+
- Use `date-fns` for date/time manipulation and `@clipboard-health/date-time` for formatting
|
|
638
|
+
|
|
639
|
+
## Null/Undefined Checks
|
|
640
|
+
|
|
641
|
+
Use `isDefined` helper from `@clipboard-health/util-ts`:
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
// Bad: truthy check fails for 0, "", false
|
|
645
|
+
if (shiftId && facilityId) {
|
|
646
|
+
}
|
|
647
|
+
// Bad: use utility instead
|
|
648
|
+
if (shift === null) {
|
|
649
|
+
}
|
|
650
|
+
if (facility === undefined) {
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Good: explicit defined check
|
|
654
|
+
if (isDefined(shiftId) && isDefined(facilityId)) {
|
|
655
|
+
}
|
|
656
|
+
```
|
|
580
657
|
|
|
581
658
|
## Types
|
|
582
659
|
|
|
@@ -585,12 +662,6 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
585
662
|
function process(arg: unknown) {} // Better than any
|
|
586
663
|
function process<T>(arg: T) {} // Best
|
|
587
664
|
|
|
588
|
-
// Nullable checks
|
|
589
|
-
if (foo == null) {
|
|
590
|
-
} // Clear intent
|
|
591
|
-
if (isDefined(foo)) {
|
|
592
|
-
} // Better with utility
|
|
593
|
-
|
|
594
665
|
// Quantity values—always unambiguous
|
|
595
666
|
const money = { amountInMinorUnits: 500, currencyCode: "USD" };
|
|
596
667
|
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,21 @@ Follow [JSON:API spec](https://jsonapi.org/).
|
|
|
317
340
|
"data": [
|
|
318
341
|
{
|
|
319
342
|
"id": "1",
|
|
320
|
-
"type": "
|
|
321
|
-
"attributes": { "
|
|
322
|
-
|
|
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
|
+
}
|
|
323
353
|
]
|
|
324
354
|
}
|
|
325
355
|
```
|
|
326
356
|
|
|
327
|
-
- Singular `type` values: `
|
|
357
|
+
- Singular `type` values: `shift` not `shifts`
|
|
328
358
|
- Links optional (use only for pagination)
|
|
329
359
|
- Use `include` for related resources
|
|
330
360
|
- Avoid `meta` unless necessary
|
|
@@ -390,16 +420,13 @@ describe("Documents", () => {
|
|
|
390
420
|
|
|
391
421
|
describe("GET /documents", () => {
|
|
392
422
|
it("returns existing documents for authenticated user", async () => {
|
|
393
|
-
// Arrange
|
|
394
423
|
const authToken = await tc.auth.createUser({ role: "employee" });
|
|
395
424
|
await tc.fixtures.createDocument({ name: "doc-1" });
|
|
396
425
|
|
|
397
|
-
// Act
|
|
398
426
|
const response = await tc.http.get("/documents", {
|
|
399
427
|
headers: { authorization: authToken },
|
|
400
428
|
});
|
|
401
429
|
|
|
402
|
-
// Assert
|
|
403
430
|
expect(response.statusCode).toBe(200);
|
|
404
431
|
expect(response.parsedBody.data).toHaveLength(1);
|
|
405
432
|
});
|
|
@@ -409,6 +436,24 @@ describe("Documents", () => {
|
|
|
409
436
|
|
|
410
437
|
**Qualities:** One behavior per test, no shared setup, no mocking, <1 second, parallelizable.
|
|
411
438
|
|
|
439
|
+
**Testing Background Jobs:**
|
|
440
|
+
|
|
441
|
+
Don't spy on job enqueuing. Instead, run the job and assert side effects:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// Run the job
|
|
445
|
+
await tc.jobs.drainQueues("shift.reminder");
|
|
446
|
+
|
|
447
|
+
// Assert side effects
|
|
448
|
+
const shift = await tc.http.get(`/shifts/${shiftId}`);
|
|
449
|
+
expect(shift.reminderSent).toBe(true);
|
|
450
|
+
|
|
451
|
+
// Or check fakes for external calls
|
|
452
|
+
expect(tc.fakes.notifications.requests).toHaveLength(1);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Side effects to assert: database changes, published messages, external HTTP requests.
|
|
456
|
+
|
|
412
457
|
<!-- Source: .ruler/common/commitMessages.md -->
|
|
413
458
|
|
|
414
459
|
# Commit Messages
|
|
@@ -429,6 +474,8 @@ Contains secrets?
|
|
|
429
474
|
└── No → LaunchDarkly feature flag
|
|
430
475
|
```
|
|
431
476
|
|
|
477
|
+
**NPM package management**: Use exact versions: add `save-exact=true` to `.npmrc`
|
|
478
|
+
|
|
432
479
|
<!-- Source: .ruler/common/featureFlags.md -->
|
|
433
480
|
|
|
434
481
|
# Feature Flags
|
|
@@ -451,6 +498,8 @@ Contains secrets?
|
|
|
451
498
|
- Always provide default values in code
|
|
452
499
|
- Clean up after full launch
|
|
453
500
|
|
|
501
|
+
**When making feature flag changes**: include LaunchDarkly link: `https://app.launchdarkly.com/projects/default/flags/{flag-key}`
|
|
502
|
+
|
|
454
503
|
<!-- Source: .ruler/common/loggingObservability.md -->
|
|
455
504
|
|
|
456
505
|
# Logging & Observability
|
|
@@ -479,13 +528,23 @@ logger.error("Exporting urgent shifts to CSV failed", {
|
|
|
479
528
|
});
|
|
480
529
|
```
|
|
481
530
|
|
|
482
|
-
**Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
531
|
+
- **Never log:** PII, PHI, tokens, secrets, SSN, account numbers, entire request/response/headers.
|
|
532
|
+
- Use metrics for counting:
|
|
483
533
|
|
|
484
|
-
|
|
534
|
+
```typescript
|
|
535
|
+
datadogMetrics.increment("negotiation.errors", { state: "New York" });
|
|
536
|
+
```
|
|
485
537
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
538
|
+
- Log IDs or specific fields instead of full objects:
|
|
539
|
+
- `workerId` (not `agent`, `hcp`, `worker`)
|
|
540
|
+
- `shiftId` (not `shift`)
|
|
541
|
+
- When multiple log statements share context, create a reusable `logContext` object:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const logContext = { shiftId, workerId };
|
|
545
|
+
logger.info("Processing shift", logContext);
|
|
546
|
+
logger.info("Notification sent", logContext);
|
|
547
|
+
```
|
|
489
548
|
|
|
490
549
|
<!-- Source: .ruler/common/pullRequests.md -->
|
|
491
550
|
|
|
@@ -557,13 +616,11 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
557
616
|
|
|
558
617
|
## Naming Conventions
|
|
559
618
|
|
|
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` |
|
|
619
|
+
- Avoid acronyms and abbreviations; for those widely known, use camelCase: `httpRequest`, `gpsPosition`, `cliArguments`, `apiResponse`
|
|
620
|
+
- File-scoped constants: `MAX_RETRY_COUNT`
|
|
621
|
+
- Instead of `agentRequirement`, `agentReq`, `workerType`, use `qualification`
|
|
622
|
+
- Instead of `agent`, `hcp`, `healthcareProvider`, use `worker`
|
|
623
|
+
- Instead of `facility`, `hcf`, `healthcareFacility`, use `workplace`
|
|
567
624
|
|
|
568
625
|
## Core Rules
|
|
569
626
|
|
|
@@ -577,6 +634,26 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
577
634
|
- Files read top-to-bottom: exports first, internal helpers below
|
|
578
635
|
- Boolean props: `is*`, `has*`, `should*`, `can*`
|
|
579
636
|
- Use const assertions for constants: `as const`
|
|
637
|
+
- Use `date-fns` for date/time manipulation and `@clipboard-health/date-time` for formatting
|
|
638
|
+
|
|
639
|
+
## Null/Undefined Checks
|
|
640
|
+
|
|
641
|
+
Use `isDefined` helper from `@clipboard-health/util-ts`:
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
// Bad: truthy check fails for 0, "", false
|
|
645
|
+
if (shiftId && facilityId) {
|
|
646
|
+
}
|
|
647
|
+
// Bad: use utility instead
|
|
648
|
+
if (shift === null) {
|
|
649
|
+
}
|
|
650
|
+
if (facility === undefined) {
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Good: explicit defined check
|
|
654
|
+
if (isDefined(shiftId) && isDefined(facilityId)) {
|
|
655
|
+
}
|
|
656
|
+
```
|
|
580
657
|
|
|
581
658
|
## Types
|
|
582
659
|
|
|
@@ -585,12 +662,6 @@ Use when: error handling hard to trigger black-box, concurrency scenarios, >5 va
|
|
|
585
662
|
function process(arg: unknown) {} // Better than any
|
|
586
663
|
function process<T>(arg: T) {} // Best
|
|
587
664
|
|
|
588
|
-
// Nullable checks
|
|
589
|
-
if (foo == null) {
|
|
590
|
-
} // Clear intent
|
|
591
|
-
if (isDefined(foo)) {
|
|
592
|
-
} // Better with utility
|
|
593
|
-
|
|
594
665
|
// Quantity values—always unambiguous
|
|
595
666
|
const money = { amountInMinorUnits: 500, currencyCode: "USD" };
|
|
596
667
|
const durationMinutes = 30;
|
|
@@ -969,7 +1040,6 @@ export function Component({ userId, onUpdate }: Props) {
|
|
|
969
1040
|
|
|
970
1041
|
## Naming Conventions
|
|
971
1042
|
|
|
972
|
-
- Components: `PascalCase`
|
|
973
1043
|
- Event handlers: `handle*` (e.g., `handleClick`)
|
|
974
1044
|
- Props interface: `Props` (co-located) or `ComponentNameProps` (exported)
|
|
975
1045
|
|
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.9",
|
|
5
5
|
"bugs": "https://github.com/ClipboardHealth/core-utils/issues",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@intellectronica/ruler": "0.3.24"
|