@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 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
- └── example/
31
- ├── data/
32
- │ ├── example.dao.mapper.ts
33
- ├── example.repo.ts
34
- │ └── notification.gateway.ts
35
- ├── entrypoints/
36
- │ ├── example.controller.ts
37
- ├── example.consumer.ts
38
- └── example.dto.mapper.ts
39
- ├── logic/
40
- ├── jobs/
41
- │ │ └── exampleCreated.job.ts
42
- │ ├── example.do.ts
43
- │ └── example.service.ts
44
- └── example.module.ts
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 only
205
- ├── indexes.ts # Indexes only
206
- └── index.ts # Model export
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": "worker",
321
- "attributes": { "firstName": "Alex", "lastName": "Smith" }
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: `"worker"` not `"workers"`
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
- Use metrics for counting:
535
+ ```typescript
536
+ datadogMetrics.increment("negotiation.errors", { state: "New York" });
537
+ ```
485
538
 
486
- ```typescript
487
- datadogMetrics.increment("negotiation.errors", { state: "New York" });
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 unless widely known
561
-
562
- | Element | Convention | Example |
563
- | ---------------------------------- | --------------------- | ---------------------------- |
564
- | File-scope constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
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
- Use metrics for counting:
80
+ ```typescript
81
+ datadogMetrics.increment("negotiation.errors", { state: "New York" });
82
+ ```
76
83
 
77
- ```typescript
78
- datadogMetrics.increment("negotiation.errors", { state: "New York" });
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 unless widely known
152
-
153
- | Element | Convention | Example |
154
- | ---------------------------------- | --------------------- | ---------------------------- |
155
- | File-scope constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
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;
@@ -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
- Use metrics for counting:
80
+ ```typescript
81
+ datadogMetrics.increment("negotiation.errors", { state: "New York" });
82
+ ```
76
83
 
77
- ```typescript
78
- datadogMetrics.increment("negotiation.errors", { state: "New York" });
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 unless widely known
152
-
153
- | Element | Convention | Example |
154
- | ---------------------------------- | --------------------- | ---------------------------- |
155
- | File-scope constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
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
 
@@ -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
- └── example/
31
- ├── data/
32
- │ ├── example.dao.mapper.ts
33
- ├── example.repo.ts
34
- │ └── notification.gateway.ts
35
- ├── entrypoints/
36
- │ ├── example.controller.ts
37
- ├── example.consumer.ts
38
- └── example.dto.mapper.ts
39
- ├── logic/
40
- ├── jobs/
41
- │ │ └── exampleCreated.job.ts
42
- │ ├── example.do.ts
43
- │ └── example.service.ts
44
- └── example.module.ts
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 only
205
- ├── indexes.ts # Indexes only
206
- └── index.ts # Model export
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": "worker",
321
- "attributes": { "firstName": "Alex", "lastName": "Smith" }
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: `"worker"` not `"workers"`
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
- Use metrics for counting:
535
+ ```typescript
536
+ datadogMetrics.increment("negotiation.errors", { state: "New York" });
537
+ ```
485
538
 
486
- ```typescript
487
- datadogMetrics.increment("negotiation.errors", { state: "New York" });
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 unless widely known
561
-
562
- | Element | Convention | Example |
563
- | ---------------------------------- | --------------------- | ---------------------------- |
564
- | File-scope constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
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.8",
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"