@clipboard-health/ai-rules 1.7.7 → 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 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,21 @@ 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" }
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: `"worker"` not `"workers"`
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
- Use metrics for counting:
534
+ ```typescript
535
+ datadogMetrics.increment("negotiation.errors", { state: "New York" });
536
+ ```
485
537
 
486
- ```typescript
487
- datadogMetrics.increment("negotiation.errors", { state: "New York" });
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 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` |
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
- 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,21 @@ 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" }
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: `"worker"` not `"workers"`
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
- Use metrics for counting:
534
+ ```typescript
535
+ datadogMetrics.increment("negotiation.errors", { state: "New York" });
536
+ ```
485
537
 
486
- ```typescript
487
- datadogMetrics.increment("negotiation.errors", { state: "New York" });
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 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` |
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.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"