@northbridge-security/secureai 0.1.13

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.
Files changed (50) hide show
  1. package/.claude/README.md +122 -0
  2. package/.claude/commands/architect/clean.md +978 -0
  3. package/.claude/commands/architect/kiss.md +762 -0
  4. package/.claude/commands/architect/review.md +704 -0
  5. package/.claude/commands/catchup.md +90 -0
  6. package/.claude/commands/code.md +115 -0
  7. package/.claude/commands/commit.md +1218 -0
  8. package/.claude/commands/cover.md +1298 -0
  9. package/.claude/commands/fmea.md +275 -0
  10. package/.claude/commands/kaizen.md +312 -0
  11. package/.claude/commands/pr.md +503 -0
  12. package/.claude/commands/todo.md +99 -0
  13. package/.claude/commands/worktree.md +738 -0
  14. package/.claude/commands/wrapup.md +103 -0
  15. package/LICENSE +183 -0
  16. package/README.md +108 -0
  17. package/dist/cli.js +75634 -0
  18. package/docs/agents/devops-reviewer.md +889 -0
  19. package/docs/agents/kiss-simplifier.md +1088 -0
  20. package/docs/agents/typescript.md +8 -0
  21. package/docs/guides/README.md +109 -0
  22. package/docs/guides/agents.clean.arch.md +244 -0
  23. package/docs/guides/agents.clean.arch.ts.md +1314 -0
  24. package/docs/guides/agents.gotask.md +1037 -0
  25. package/docs/guides/agents.markdown.md +1209 -0
  26. package/docs/guides/agents.onepassword.md +285 -0
  27. package/docs/guides/agents.sonar.md +857 -0
  28. package/docs/guides/agents.tdd.md +838 -0
  29. package/docs/guides/agents.tdd.ts.md +1062 -0
  30. package/docs/guides/agents.typesript.md +1389 -0
  31. package/docs/guides/github-mcp.md +1075 -0
  32. package/package.json +130 -0
  33. package/packages/secureai-cli/src/cli.ts +21 -0
  34. package/tasks/README.md +880 -0
  35. package/tasks/aws.yml +64 -0
  36. package/tasks/bash.yml +118 -0
  37. package/tasks/bun.yml +738 -0
  38. package/tasks/claude.yml +183 -0
  39. package/tasks/docker.yml +420 -0
  40. package/tasks/docs.yml +127 -0
  41. package/tasks/git.yml +1336 -0
  42. package/tasks/gotask.yml +132 -0
  43. package/tasks/json.yml +77 -0
  44. package/tasks/markdown.yml +95 -0
  45. package/tasks/onepassword.yml +350 -0
  46. package/tasks/security.yml +102 -0
  47. package/tasks/sonar.yml +437 -0
  48. package/tasks/template.yml +74 -0
  49. package/tasks/vscode.yml +103 -0
  50. package/tasks/yaml.yml +121 -0
@@ -0,0 +1,1314 @@
1
+ # Clean Architecture in TypeScript
2
+
3
+ This guide shows how to implement clean architecture principles in TypeScript, focusing on interface-based dependency injection, the `*.system.ts` naming convention, and constructor-based DI patterns.
4
+
5
+ **See also**: [Clean Architecture for AI Agents](./agents.clean.arch.md) - Language-agnostic principles
6
+
7
+ ## Target Audience
8
+
9
+ AI agents refactoring TypeScript/Node.js/Bun projects to improve testability, test coverage, and separation of concerns using clean architecture patterns.
10
+
11
+ ## Problem Statement
12
+
13
+ TypeScript codebases often mix business logic with system interactions (file I/O, shell execution, network calls, database queries). This creates several issues:
14
+
15
+ **Testing challenges:**
16
+
17
+ - Cannot unit test without triggering real system operations
18
+ - Tests become slow, flaky, and environment-dependent
19
+ - Complex mocking required for every test
20
+
21
+ **Coverage challenges:**
22
+
23
+ - Business logic coverage diluted by untestable system calls
24
+ - Coverage metrics don't reflect actual code complexity
25
+ - Must exclude entire files from coverage, losing visibility into business logic
26
+
27
+ **Maintenance challenges:**
28
+
29
+ - System interaction code scattered across many files
30
+ - Hard to change system interaction strategy (e.g., switch from child_process to worker_threads)
31
+ - Difficult to mock system behavior for testing edge cases
32
+
33
+ ## Solution Pattern
34
+
35
+ Use **interface-based dependency injection** with a **naming convention** to separate concerns:
36
+
37
+ 1. **Interface** - Define contracts for system operations
38
+ 2. **System implementation** - Thin wrapper around actual system calls (excluded from coverage)
39
+ 3. **Mock implementation** - In-memory test doubles
40
+ 4. **Business logic** - Uses interface, fully testable
41
+
42
+ ### File Naming Convention
43
+
44
+ **Standard naming pattern:**
45
+
46
+ - **Interface**: `{domain}-interface.ts` (pure TypeScript types, no .system suffix)
47
+ - **System wrapper**: `{domain}.system.ts` (actual system calls, excluded from coverage)
48
+ - **Mock**: `tests/mocks/{domain}-mock.ts` (test doubles)
49
+ - **Business logic**: Original filename or separate module
50
+
51
+ **Benefits of `*.system.ts` convention:**
52
+
53
+ - **Self-documenting** - File name clearly indicates system interaction
54
+ - **Simple exclusion** - Single glob pattern (`**/*.system.ts`) in coverage config
55
+ - **Scalable** - Works for all future refactorings without config changes
56
+ - **Discoverable** - Easy to find all system boundary files
57
+
58
+ ### Folder Organization
59
+
60
+ **Module-based structure (Recommended):**
61
+
62
+ Organize related files into feature/module folders:
63
+
64
+ ```typescript
65
+ // ✅ GOOD - Module folder with clear separation
66
+ src/utils/onepassword/
67
+ ├── index.ts // Public API: re-export public functions
68
+ ├── cli-interface.ts // IOnePasswordCLI interface definition
69
+ ├── cli.system.ts // System implementation (excluded from coverage)
70
+ ├── session.ts // Business logic: session management
71
+ ├── resolver.ts // Business logic: secret resolution
72
+ └── secrets.ts // Business logic: secret parsing
73
+
74
+ tests/mocks/onepassword/
75
+ └── cli-mock.ts // Mock implementation for testing
76
+
77
+ // Import from module
78
+ import { authenticate, readSecret } from '@/utils/onepassword';
79
+ ```
80
+
81
+ **Flat structure (Current, but less scalable):**
82
+
83
+ ```typescript
84
+ // ⚠️ ACCEPTABLE - Flat structure with prefixes
85
+ src/utils/
86
+ ├── onepassword-cli-interface.ts
87
+ ├── onepassword-cli.system.ts
88
+ ├── onepassword-session.ts
89
+ ├── onepassword-resolver.ts
90
+ ├── onepassword-secrets.ts
91
+ └── onepassword.ts // Re-exports
92
+
93
+ tests/mocks/
94
+ └── onepassword-cli-mock.ts
95
+
96
+ // Import with full names
97
+ import { authenticate } from '@/utils/onepassword-session';
98
+ import { readSecret } from '@/utils/onepassword-resolver';
99
+ ```
100
+
101
+ **Benefits of module folders:**
102
+
103
+ 1. **Co-location** - All related code in one directory
104
+ 2. **Clear boundaries** - Module folder defines public API via `index.ts`
105
+ 3. **Easier to find** - All onepassword code in `onepassword/` folder
106
+ 4. **Scales better** - Add new modules without cluttering parent directory
107
+ 5. **Testability** - Module-level mocks in `tests/mocks/{module}/`
108
+ 6. **Refactoring** - Move entire module without updating many imports
109
+
110
+ **When to use each:**
111
+
112
+ - **Module folders**: 5+ related files, clear domain boundary
113
+ - **Flat structure**: 2-3 related files, simple utility functions
114
+
115
+ **Real-world example transformation:**
116
+
117
+ ```diff
118
+ # Before (flat structure)
119
+ src/utils/
120
+ + onepassword-cli-interface.ts (42 lines)
121
+ + onepassword-cli.system.ts (179 lines)
122
+ + onepassword-session.ts (254 lines)
123
+ + onepassword-resolver.ts (331 lines)
124
+ + onepassword-secrets.ts (187 lines)
125
+ + onepassword.ts (15 lines)
126
+ = 1,008 lines across 6 files with "onepassword-" prefix
127
+
128
+ # After (module structure)
129
+ src/utils/op/
130
+ + index.ts (275 lines - namespaced public API)
131
+ + cli-interface.ts (42 lines)
132
+ + cli.system.ts (179 lines)
133
+ + session.ts (254 lines)
134
+ + resolver.ts (331 lines)
135
+ + secrets.ts (187 lines)
136
+ = Same 1,008 lines, better organized
137
+
138
+ # Import comparison
139
+ - import { parseSecretReference, buildSecretReference } from '@/utils/onepassword-secrets';
140
+ + import { Secrets } from '@/utils/op';
141
+ + Secrets.parse('op://...');
142
+ + Secrets.build({ vault: 'Private', item: 'API', field: 'token' });
143
+ ```
144
+
145
+ ### Clean Namespace API Pattern
146
+
147
+ **Problem**: Exporting many individual functions creates messy imports and unclear API boundaries.
148
+
149
+ ```typescript
150
+ // ❌ BAD - Function-based exports are verbose and unclear
151
+ import {
152
+ parseSecretReference,
153
+ buildSecretReference,
154
+ validateSecretReference,
155
+ findSecretReferences,
156
+ extractSecretReferences,
157
+ hasSecretReferences,
158
+ redactSecretReferences,
159
+ isSecretReference,
160
+ getOnePasswordStatus,
161
+ getOnePasswordCLIPath,
162
+ getOnePasswordCLIVersion,
163
+ isOnePasswordCLIAvailable,
164
+ authenticateWithSession,
165
+ readSecretWithSession,
166
+ signOut,
167
+ } from "@/utils/op";
168
+
169
+ // Use functions
170
+ const result = parseSecretReference("op://Private/API/token");
171
+ const status = await getOnePasswordStatus();
172
+ ```
173
+
174
+ **Solution**: Use namespaced classes with static methods for cleaner API.
175
+
176
+ ```typescript
177
+ // ✅ GOOD - Namespaced classes provide clear boundaries
178
+ import { Secrets, Status, Session } from "@/utils/op";
179
+
180
+ // Use namespaced methods
181
+ const result = Secrets.parse("op://Private/API/token");
182
+ const status = await Status.get();
183
+ await Session.authenticate();
184
+ ```
185
+
186
+ **Implementation pattern:**
187
+
188
+ <details>
189
+ <summary><strong>Step 1: Module files export functions AND classes</strong></summary>
190
+
191
+ Each module file exports both functions and a namespaced class:
192
+
193
+ ```typescript
194
+ // src/utils/op/secrets.ts
195
+
196
+ // Individual functions (implementation)
197
+ export function parseSecretReference(uri: string): ParseResult {
198
+ // Implementation
199
+ }
200
+
201
+ export function buildSecretReference(ref: SecretReference): string {
202
+ // Implementation
203
+ }
204
+
205
+ export function validateSecretReference(ref: SecretReference): ParseResult {
206
+ // Implementation
207
+ }
208
+
209
+ // Namespaced class (public API)
210
+ export class Secrets {
211
+ static build = buildSecretReference;
212
+ static extract = extractSecretReferences;
213
+ static find = findSecretReferences;
214
+ static has = hasSecretReferences;
215
+ static is = isSecretReference;
216
+ static parse = parseSecretReference;
217
+ static redact = redactSecretReferences;
218
+ static validate = validateSecretReference;
219
+ }
220
+ ```
221
+
222
+ **Why both exports?**
223
+
224
+ - **Functions**: Backward compatibility, tree-shaking optimization
225
+ - **Class**: Clean namespace API, better discoverability
226
+
227
+ </details>
228
+
229
+ <details>
230
+ <summary><strong>Step 2: Index.ts re-exports classes</strong></summary>
231
+
232
+ The `index.ts` file simply re-exports classes from modules:
233
+
234
+ ```typescript
235
+ // src/utils/op/index.ts
236
+
237
+ // ============================================================================
238
+ // Namespaced Class Exports
239
+ // ============================================================================
240
+
241
+ export { Errors } from "./errors.js";
242
+ export { Resolver } from "./resolver.js";
243
+ export { Secrets } from "./secrets.js";
244
+ export { Session } from "./session.js";
245
+ export { Setup } from "./setup.js";
246
+ export { Status } from "./status.js";
247
+
248
+ // ============================================================================
249
+ // Individual Function Exports (for backward compatibility)
250
+ // NOTE: Only applicable if the library is used externally in production, or we are not
251
+ // moving to a new major version.
252
+ // Always ASK THE USER if they need this unless this is only used inside the
253
+ // project itself.
254
+ // ============================================================================
255
+
256
+ export {
257
+ parseSecretReference,
258
+ buildSecretReference,
259
+ validateSecretReference,
260
+ // ... other functions
261
+ } from "./secrets.js";
262
+
263
+ // Export types directly
264
+ export type { ParseResult, SecretReference } from "./secrets.js";
265
+ export type { OnePasswordStatus } from "./status.js";
266
+ export type { AuthResult } from "./session.js";
267
+ ```
268
+
269
+ **Benefits of this approach:**
270
+
271
+ - Classes are defined where implementation lives
272
+ - `index.ts` is a simple re-export gateway
273
+ - Maintainers see class and functions together in same file
274
+ - No mental mapping between file structure and API surface
275
+
276
+ </details>
277
+
278
+ <details>
279
+ <summary><strong>Step 3: Consumers use namespaced API</strong></summary>
280
+
281
+ Application code imports and uses namespaced classes:
282
+
283
+ ```typescript
284
+ // src/services/config-loader.ts
285
+ import { Secrets, Session } from "@/utils/op";
286
+
287
+ export async function loadSecretConfig(configText: string): Promise<Config> {
288
+ // Check if config contains secrets
289
+ if (!Secrets.has(configText)) {
290
+ return parseConfig(configText);
291
+ }
292
+
293
+ // Authenticate if needed
294
+ if (!Session.isValid()) {
295
+ await Session.authenticate();
296
+ }
297
+
298
+ // Resolve all secrets in config
299
+ const resolved = await Resolver.resolveInText(configText);
300
+
301
+ return parseConfig(resolved.text);
302
+ }
303
+ ```
304
+
305
+ </details>
306
+
307
+ **Benefits of namespaced classes:**
308
+
309
+ 1. **Clear organization** - Related functions grouped under logical namespaces
310
+ 2. **Discoverable API** - IDE autocomplete shows `Secrets.` and lists all secret operations
311
+ 3. **Self-documenting** - Class name indicates domain (`Secrets`, `Session`, `Status`)
312
+ 4. **Easier imports** - Import 3 classes instead of 20 functions
313
+ 5. **Better tree-shaking** - Unused namespaces can be eliminated by bundlers
314
+ 6. **Consistent naming** - `Secrets.parse()` instead of `parseSecretReference()`
315
+ 7. **Type safety** - Same TypeScript checking as functions
316
+ 8. **No runtime overhead** - Static methods compile to function calls
317
+
318
+ **API comparison:**
319
+
320
+ | Pattern | Import Statement | Usage | Clarity |
321
+ | -------------------- | ------------------------------------ | ---------------------------------- | ---------- |
322
+ | Individual functions | `import { parseSecret... } from '.'` | `parseSecretReference('op://...')` | ❌ Verbose |
323
+ | Namespaced classes | `import { Secrets } from '.'` | `Secrets.parse('op://...')` | ✅ Clear |
324
+ | Default export | `import secrets from '.'` | `secrets.parse('op://...')` | ⚠️ Opaque |
325
+ | Instance methods | `const s = new Secrets(); s.parse()` | Creates unnecessary instances | ❌ Wrong |
326
+
327
+ **When to use this pattern:**
328
+
329
+ - **5+ related functions** - Namespace reduces import clutter
330
+ - **Clear domain boundaries** - Functions naturally group by purpose
331
+ - **Stable API** - Public API unlikely to change frequently
332
+ - **Module folders** - Works best with organized folder structure
333
+
334
+ **When NOT to use:**
335
+
336
+ - **1-3 utility functions** - Direct exports are simpler
337
+ - **Mixed concerns** - Functions don't group naturally
338
+ - **Tree-shakeable critical** - Function-level exports optimize better in some bundlers
339
+
340
+ **Coverage note**: `index.ts` files with only re-exports should be excluded from coverage:
341
+
342
+ ```toml
343
+ # bunfig.toml
344
+ [test]
345
+ coveragePathIgnorePatterns = [
346
+ "**/*.system.ts",
347
+ "**/index.ts", # Re-export files have no business logic
348
+ ]
349
+ ```
350
+
351
+ ## Step-by-Step Refactoring Process
352
+
353
+ ### Step 1: Identify System Boundaries
354
+
355
+ Find files that mix business logic with system interactions:
356
+
357
+ ```typescript
358
+ // ❌ Mixed concerns - hard to test
359
+ export async function processUserData(userId: string): Promise<User> {
360
+ // Business logic
361
+ if (!userId || userId.length < 5) {
362
+ throw new Error("Invalid user ID");
363
+ }
364
+
365
+ // System interaction (blocks testing)
366
+ const { stdout } = await execAsync(`curl https://api.example.com/users/${userId}`);
367
+ const rawData = JSON.parse(stdout);
368
+
369
+ // Business logic
370
+ return {
371
+ id: rawData.id,
372
+ name: rawData.full_name.toUpperCase(),
373
+ email: rawData.email_address,
374
+ verified: rawData.status === "active",
375
+ };
376
+ }
377
+ ```
378
+
379
+ **Identify the boundary:**
380
+
381
+ - **Business logic**: Validation, transformation, business rules
382
+ - **System interaction**: HTTP calls, file I/O, process execution, database queries
383
+
384
+ ### Step 2: Define Interface
385
+
386
+ Create an interface that describes all system operations:
387
+
388
+ ```typescript
389
+ // src/services/user-api-interface.ts
390
+
391
+ /**
392
+ * User API system operations interface
393
+ * Abstracts HTTP calls for testability
394
+ */
395
+ export interface IUserAPI {
396
+ /**
397
+ * Fetch user data from remote API
398
+ *
399
+ * @param userId - User identifier
400
+ * @returns Raw user data from API
401
+ * @throws {Error} If API call fails or user not found
402
+ */
403
+ fetchUser(userId: string): Promise<RawUserData>;
404
+
405
+ /**
406
+ * Update user data via API
407
+ *
408
+ * @param userId - User identifier
409
+ * @param updates - Fields to update
410
+ * @returns Updated user data
411
+ */
412
+ updateUser(userId: string, updates: Partial<RawUserData>): Promise<RawUserData>;
413
+ }
414
+
415
+ /**
416
+ * Raw API response shape
417
+ */
418
+ export interface RawUserData {
419
+ id: string;
420
+ full_name: string;
421
+ email_address: string;
422
+ status: "active" | "inactive" | "suspended";
423
+ created_at: string;
424
+ }
425
+ ```
426
+
427
+ **Interface design guidelines:**
428
+
429
+ - **One interface per system boundary** (HTTP client, database, file system, etc.)
430
+ - **Synchronous when possible** - Use sync methods for file system if business logic needs sync behavior
431
+ - **Throw errors** - Don't return error codes; use exceptions for system failures
432
+ - **Platform-agnostic** - Abstract away OS-specific details
433
+ - **Minimal surface area** - Only expose operations actually needed by business logic
434
+
435
+ ### Step 3: Create System Implementation
436
+
437
+ Implement the interface with actual system calls in a `*.system.ts` file:
438
+
439
+ ```typescript
440
+ // src/services/user-api.system.ts
441
+
442
+ import { exec } from "node:child_process";
443
+ import { promisify } from "node:util";
444
+ import type { IUserAPI, RawUserData } from "./user-api-interface.js";
445
+
446
+ const execAsync = promisify(exec);
447
+
448
+ /**
449
+ * Real User API implementation using curl
450
+ *
451
+ * **Coverage note**: This file uses the .system.ts suffix and is automatically
452
+ * excluded from coverage via glob pattern: **\/*.system.ts
453
+ *
454
+ * This is a thin wrapper with no business logic - all system interaction.
455
+ *
456
+ * @file user-api.system.ts
457
+ */
458
+ export class UserAPISystem implements IUserAPI {
459
+ constructor(private baseURL: string = "https://api.example.com") {}
460
+
461
+ async fetchUser(userId: string): Promise<RawUserData> {
462
+ try {
463
+ const { stdout } = await execAsync(`curl -s ${this.baseURL}/users/${userId}`);
464
+ return JSON.parse(stdout);
465
+ } catch (error) {
466
+ throw new Error(
467
+ `Failed to fetch user ${userId}: ${error instanceof Error ? error.message : String(error)}`
468
+ );
469
+ }
470
+ }
471
+
472
+ async updateUser(userId: string, updates: Partial<RawUserData>): Promise<RawUserData> {
473
+ try {
474
+ const payload = JSON.stringify(updates);
475
+ const { stdout } = await execAsync(
476
+ `curl -s -X PATCH -H "Content-Type: application/json" -d '${payload}' ${this.baseURL}/users/${userId}`
477
+ );
478
+ return JSON.parse(stdout);
479
+ } catch (error) {
480
+ throw new Error(
481
+ `Failed to update user ${userId}: ${error instanceof Error ? error.message : String(error)}`
482
+ );
483
+ }
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Singleton instance for convenience
489
+ * Use this in production code
490
+ */
491
+ export const defaultUserAPI = new UserAPISystem();
492
+ ```
493
+
494
+ **System implementation guidelines:**
495
+
496
+ - **Keep it thin** - No business logic, only system call wrappers
497
+ - **Document exclusion** - Add comment about `*.system.ts` coverage exclusion
498
+ - **Provide default instance** - Export singleton for convenience
499
+ - **Error translation** - Convert system errors to domain exceptions
500
+ - **Configuration via constructor** - Allow dependency injection of config (URLs, paths, etc.)
501
+
502
+ ### Step 4: Create Mock Implementation
503
+
504
+ Create a test double that implements the interface with in-memory operations:
505
+
506
+ ```typescript
507
+ // tests/mocks/user-api-mock.ts
508
+
509
+ import type { IUserAPI, RawUserData } from "../../src/services/user-api-interface.js";
510
+
511
+ /**
512
+ * Mock User API for testing
513
+ * No network calls, all operations are in-memory
514
+ */
515
+ export class UserAPIMock implements IUserAPI {
516
+ private users = new Map<string, RawUserData>();
517
+ private fetchShouldFail = false;
518
+ private updateShouldFail = false;
519
+
520
+ /**
521
+ * Pre-populate mock with user data
522
+ */
523
+ setUser(userId: string, data: RawUserData): void {
524
+ this.users.set(userId, data);
525
+ }
526
+
527
+ /**
528
+ * Simulate API failures for error testing
529
+ */
530
+ setFetchShouldFail(fail: boolean): void {
531
+ this.fetchShouldFail = fail;
532
+ }
533
+
534
+ setUpdateShouldFail(fail: boolean): void {
535
+ this.updateShouldFail = fail;
536
+ }
537
+
538
+ /**
539
+ * Clear all mock data
540
+ */
541
+ clear(): void {
542
+ this.users.clear();
543
+ this.fetchShouldFail = false;
544
+ this.updateShouldFail = false;
545
+ }
546
+
547
+ // IUserAPI implementation
548
+
549
+ async fetchUser(userId: string): Promise<RawUserData> {
550
+ if (this.fetchShouldFail) {
551
+ throw new Error("Network error: Connection timeout");
552
+ }
553
+
554
+ const user = this.users.get(userId);
555
+ if (!user) {
556
+ throw new Error(`User not found: ${userId}`);
557
+ }
558
+
559
+ return user;
560
+ }
561
+
562
+ async updateUser(userId: string, updates: Partial<RawUserData>): Promise<RawUserData> {
563
+ if (this.updateShouldFail) {
564
+ throw new Error("Network error: Server returned 500");
565
+ }
566
+
567
+ const existing = this.users.get(userId);
568
+ if (!existing) {
569
+ throw new Error(`User not found: ${userId}`);
570
+ }
571
+
572
+ const updated = { ...existing, ...updates };
573
+ this.users.set(userId, updated);
574
+ return updated;
575
+ }
576
+ }
577
+ ```
578
+
579
+ **Mock implementation guidelines:**
580
+
581
+ - **Implement full interface** - All methods from system interface
582
+ - **In-memory state** - No actual system calls
583
+ - **Configuration methods** - Allow tests to control behavior (simulate failures, set data)
584
+ - **Realistic behavior** - Throw same errors as real implementation
585
+ - **Stateful** - Maintain state across calls within a test
586
+
587
+ ### Step 5: Refactor Business Logic
588
+
589
+ Update business logic to accept interface parameter with default value:
590
+
591
+ ```typescript
592
+ // src/services/user-service.ts
593
+
594
+ import type { IUserAPI, RawUserData } from "./user-api-interface.js";
595
+ import { defaultUserAPI } from "./user-api.system.js";
596
+
597
+ /**
598
+ * Normalized user model for application use
599
+ */
600
+ export interface User {
601
+ id: string;
602
+ name: string;
603
+ email: string;
604
+ verified: boolean;
605
+ }
606
+
607
+ /**
608
+ * Process and normalize user data
609
+ *
610
+ * @param userId - User identifier
611
+ * @param api - User API implementation (defaults to real API)
612
+ * @returns Normalized user object
613
+ * @throws {Error} If user ID invalid or API call fails
614
+ */
615
+ export async function processUserData(
616
+ userId: string,
617
+ api: IUserAPI = defaultUserAPI
618
+ ): Promise<User> {
619
+ // Business logic: Validation
620
+ if (!userId || userId.length < 5) {
621
+ throw new Error("Invalid user ID: must be at least 5 characters");
622
+ }
623
+
624
+ // System interaction (delegated to interface)
625
+ const rawData = await api.fetchUser(userId);
626
+
627
+ // Business logic: Transformation
628
+ return {
629
+ id: rawData.id,
630
+ name: rawData.full_name.toUpperCase(),
631
+ email: rawData.email_address,
632
+ verified: rawData.status === "active",
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Update user status
638
+ *
639
+ * @param userId - User identifier
640
+ * @param active - Whether user should be active
641
+ * @param api - User API implementation (defaults to real API)
642
+ */
643
+ export async function updateUserStatus(
644
+ userId: string,
645
+ active: boolean,
646
+ api: IUserAPI = defaultUserAPI
647
+ ): Promise<void> {
648
+ // Business logic: Determine status value
649
+ const status: RawUserData["status"] = active ? "active" : "inactive";
650
+
651
+ // System interaction (delegated to interface)
652
+ await api.updateUser(userId, { status });
653
+ }
654
+ ```
655
+
656
+ **Refactoring guidelines:**
657
+
658
+ - **Add interface parameter last** - With default value for backward compatibility
659
+ - **Use interface type** - Not concrete implementation type
660
+ - **Keep business logic pure** - All system interaction through interface
661
+ - **Maintain public API** - Existing callers work without changes
662
+ - **Document parameter** - Explain why interface is injectable
663
+
664
+ ### Step 6: Write Tests
665
+
666
+ Use mock implementation for fast, deterministic tests:
667
+
668
+ ```typescript
669
+ // tests/unit/services/user-service.test.ts
670
+
671
+ import { describe, expect, it, beforeEach } from "bun:test";
672
+ import { UserAPIMock } from "../../mocks/user-api-mock.js";
673
+ import { processUserData, updateUserStatus } from "../../../src/services/user-service.js";
674
+
675
+ describe("User Service", () => {
676
+ let mockAPI: UserAPIMock;
677
+
678
+ beforeEach(() => {
679
+ mockAPI = new UserAPIMock();
680
+ });
681
+
682
+ describe("processUserData", () => {
683
+ it("should normalize user data from API", async () => {
684
+ // Arrange
685
+ mockAPI.setUser("user-12345", {
686
+ id: "user-12345",
687
+ full_name: "john doe",
688
+ email_address: "john@example.com",
689
+ status: "active",
690
+ created_at: "2024-01-01T00:00:00Z",
691
+ });
692
+
693
+ // Act
694
+ const user = await processUserData("user-12345", mockAPI);
695
+
696
+ // Assert
697
+ expect(user).toEqual({
698
+ id: "user-12345",
699
+ name: "JOHN DOE", // Uppercased
700
+ email: "john@example.com",
701
+ verified: true, // active -> verified
702
+ });
703
+ });
704
+
705
+ it("should reject invalid user IDs", async () => {
706
+ // Act & Assert
707
+ await expect(processUserData("123", mockAPI)).rejects.toThrow(
708
+ "Invalid user ID: must be at least 5 characters"
709
+ );
710
+ });
711
+
712
+ it("should handle API failures", async () => {
713
+ // Arrange
714
+ mockAPI.setFetchShouldFail(true);
715
+
716
+ // Act & Assert
717
+ await expect(processUserData("user-12345", mockAPI)).rejects.toThrow("Network error");
718
+ });
719
+
720
+ it("should mark inactive users as unverified", async () => {
721
+ // Arrange
722
+ mockAPI.setUser("user-12345", {
723
+ id: "user-12345",
724
+ full_name: "jane doe",
725
+ email_address: "jane@example.com",
726
+ status: "inactive",
727
+ created_at: "2024-01-01T00:00:00Z",
728
+ });
729
+
730
+ // Act
731
+ const user = await processUserData("user-12345", mockAPI);
732
+
733
+ // Assert
734
+ expect(user.verified).toBe(false);
735
+ });
736
+ });
737
+
738
+ describe("updateUserStatus", () => {
739
+ beforeEach(() => {
740
+ mockAPI.setUser("user-12345", {
741
+ id: "user-12345",
742
+ full_name: "john doe",
743
+ email_address: "john@example.com",
744
+ status: "active",
745
+ created_at: "2024-01-01T00:00:00Z",
746
+ });
747
+ });
748
+
749
+ it("should activate user", async () => {
750
+ // Act
751
+ await updateUserStatus("user-12345", true, mockAPI);
752
+
753
+ // Assert
754
+ const updated = await mockAPI.fetchUser("user-12345");
755
+ expect(updated.status).toBe("active");
756
+ });
757
+
758
+ it("should deactivate user", async () => {
759
+ // Act
760
+ await updateUserStatus("user-12345", false, mockAPI);
761
+
762
+ // Assert
763
+ const updated = await mockAPI.fetchUser("user-12345");
764
+ expect(updated.status).toBe("inactive");
765
+ });
766
+
767
+ it("should handle update failures", async () => {
768
+ // Arrange
769
+ mockAPI.setUpdateShouldFail(true);
770
+
771
+ // Act & Assert
772
+ await expect(updateUserStatus("user-12345", true, mockAPI)).rejects.toThrow("Network error");
773
+ });
774
+ });
775
+ });
776
+ ```
777
+
778
+ **Test guidelines:**
779
+
780
+ - **No system calls** - Tests run in-memory only
781
+ - **Fast execution** - Entire suite should run in seconds
782
+ - **Deterministic** - Same inputs always produce same outputs
783
+ - **Test business logic** - Focus on validation, transformation, error handling
784
+ - **Test error paths** - Use mock configuration to simulate failures
785
+
786
+ ### Step 7: Configure Coverage Exclusion
787
+
788
+ Add `*.system.ts` pattern to coverage configuration:
789
+
790
+ <details>
791
+ <summary><strong>Bun (bunfig.toml)</strong></summary>
792
+
793
+ ```toml
794
+ [test]
795
+ coveragePathIgnorePatterns = [
796
+ "**/*.system.ts", # All system interaction files
797
+ "tests/**", # Test files
798
+ "**/*.test.ts", # More test files
799
+ "**/types/**", # Type definition files
800
+ ]
801
+ ```
802
+
803
+ </details>
804
+
805
+ <details>
806
+ <summary><strong>Jest (jest.config.js)</strong></summary>
807
+
808
+ ```javascript
809
+ export default {
810
+ coveragePathIgnorePatterns: [
811
+ ".*\\.system\\.ts$", // All system interaction files
812
+ "/node_modules/",
813
+ "/tests/",
814
+ "\\.test\\.ts$",
815
+ ],
816
+ };
817
+ ```
818
+
819
+ </details>
820
+
821
+ <details>
822
+ <summary><strong>Vitest (vitest.config.ts)</strong></summary>
823
+
824
+ ```typescript
825
+ export default defineConfig({
826
+ test: {
827
+ coverage: {
828
+ exclude: [
829
+ "**/*.system.ts", // All system interaction files
830
+ "tests/**",
831
+ "**/*.test.ts",
832
+ "**/types/**",
833
+ ],
834
+ },
835
+ },
836
+ });
837
+ ```
838
+
839
+ </details>
840
+
841
+ <details>
842
+ <summary><strong>SonarQube (sonar-project.properties)</strong></summary>
843
+
844
+ ```properties
845
+ # Coverage exclusions
846
+ sonar.coverage.exclusions=\
847
+ **/*.system.ts,\
848
+ tests/**,\
849
+ **/*.test.ts,\
850
+ **/types/**
851
+ ```
852
+
853
+ </details>
854
+
855
+ ## Common Patterns
856
+
857
+ ### Pattern 1: File System Operations
858
+
859
+ <details>
860
+ <summary><strong>File system abstraction example</strong></summary>
861
+
862
+ **Interface:**
863
+
864
+ ```typescript
865
+ // src/utils/filesystem-interface.ts
866
+
867
+ export interface IFileSystem {
868
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
869
+ writeFile(path: string, content: string, encoding: BufferEncoding): Promise<void>;
870
+ exists(path: string): Promise<boolean>;
871
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
872
+ readdir(path: string): Promise<string[]>;
873
+ }
874
+ ```
875
+
876
+ **System implementation:**
877
+
878
+ ```typescript
879
+ // src/utils/filesystem.system.ts
880
+
881
+ import { readFile, writeFile, access, mkdir, readdir } from "node:fs/promises";
882
+ import type { IFileSystem } from "./filesystem-interface.js";
883
+
884
+ export class FileSystemReal implements IFileSystem {
885
+ async readFile(path: string, encoding: BufferEncoding): Promise<string> {
886
+ return readFile(path, encoding);
887
+ }
888
+
889
+ async writeFile(path: string, content: string, encoding: BufferEncoding): Promise<void> {
890
+ await writeFile(path, content, encoding);
891
+ }
892
+
893
+ async exists(path: string): Promise<boolean> {
894
+ try {
895
+ await access(path);
896
+ return true;
897
+ } catch {
898
+ return false;
899
+ }
900
+ }
901
+
902
+ async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
903
+ await mkdir(path, options);
904
+ }
905
+
906
+ async readdir(path: string): Promise<string[]> {
907
+ return readdir(path);
908
+ }
909
+ }
910
+
911
+ export const defaultFS = new FileSystemReal();
912
+ ```
913
+
914
+ **Mock:**
915
+
916
+ ```typescript
917
+ // tests/mocks/filesystem-mock.ts
918
+
919
+ export class FileSystemMock implements IFileSystem {
920
+ private files = new Map<string, string>();
921
+ private dirs = new Set<string>();
922
+
923
+ setFile(path: string, content: string): void {
924
+ this.files.set(path, content);
925
+ }
926
+
927
+ setDir(path: string): void {
928
+ this.dirs.add(path);
929
+ }
930
+
931
+ clear(): void {
932
+ this.files.clear();
933
+ this.dirs.clear();
934
+ }
935
+
936
+ async readFile(path: string, encoding: BufferEncoding): Promise<string> {
937
+ const content = this.files.get(path);
938
+ if (!content) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
939
+ return content;
940
+ }
941
+
942
+ async writeFile(path: string, content: string, encoding: BufferEncoding): Promise<void> {
943
+ this.files.set(path, content);
944
+ }
945
+
946
+ async exists(path: string): Promise<boolean> {
947
+ return this.files.has(path) || this.dirs.has(path);
948
+ }
949
+
950
+ async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
951
+ this.dirs.add(path);
952
+ }
953
+
954
+ async readdir(path: string): Promise<string[]> {
955
+ if (!this.dirs.has(path))
956
+ throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
957
+ return Array.from(this.files.keys()).filter((f) => f.startsWith(path + "/"));
958
+ }
959
+ }
960
+ ```
961
+
962
+ </details>
963
+
964
+ ### Pattern 2: Shell Command Execution
965
+
966
+ <details>
967
+ <summary><strong>Shell execution abstraction example</strong></summary>
968
+
969
+ **Interface:**
970
+
971
+ ```typescript
972
+ // src/utils/shell-interface.ts
973
+
974
+ export interface IShellExecutor {
975
+ exec(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>;
976
+ execSync(command: string): { stdout: string; stderr: string; exitCode: number };
977
+ commandExists(command: string): Promise<boolean>;
978
+ }
979
+ ```
980
+
981
+ **System implementation:**
982
+
983
+ ```typescript
984
+ // src/utils/shell.system.ts
985
+
986
+ import { exec, execSync as nodeExecSync } from "node:child_process";
987
+ import { promisify } from "node:util";
988
+ import type { IShellExecutor } from "./shell-interface.js";
989
+
990
+ const execAsync = promisify(exec);
991
+
992
+ export class ShellExecutorReal implements IShellExecutor {
993
+ async exec(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
994
+ try {
995
+ const { stdout, stderr } = await execAsync(command);
996
+ return { stdout, stderr, exitCode: 0 };
997
+ } catch (error: any) {
998
+ return {
999
+ stdout: error.stdout || "",
1000
+ stderr: error.stderr || "",
1001
+ exitCode: error.code || 1,
1002
+ };
1003
+ }
1004
+ }
1005
+
1006
+ execSync(command: string): { stdout: string; stderr: string; exitCode: number } {
1007
+ try {
1008
+ const stdout = nodeExecSync(command, { encoding: "utf-8" });
1009
+ return { stdout, stderr: "", exitCode: 0 };
1010
+ } catch (error: any) {
1011
+ return {
1012
+ stdout: error.stdout || "",
1013
+ stderr: error.stderr || "",
1014
+ exitCode: error.status || 1,
1015
+ };
1016
+ }
1017
+ }
1018
+
1019
+ async commandExists(command: string): Promise<boolean> {
1020
+ const result = await this.exec(`command -v ${command}`);
1021
+ return result.exitCode === 0;
1022
+ }
1023
+ }
1024
+
1025
+ export const defaultShell = new ShellExecutorReal();
1026
+ ```
1027
+
1028
+ **Mock:**
1029
+
1030
+ ```typescript
1031
+ // tests/mocks/shell-mock.ts
1032
+
1033
+ export class ShellExecutorMock implements IShellExecutor {
1034
+ private commands = new Map<string, { stdout: string; stderr: string; exitCode: number }>();
1035
+ private availableCommands = new Set<string>(["ls", "cat", "grep"]);
1036
+
1037
+ setCommandOutput(command: string, stdout: string, stderr = "", exitCode = 0): void {
1038
+ this.commands.set(command, { stdout, stderr, exitCode });
1039
+ }
1040
+
1041
+ setCommandExists(command: string, exists: boolean): void {
1042
+ if (exists) {
1043
+ this.availableCommands.add(command);
1044
+ } else {
1045
+ this.availableCommands.delete(command);
1046
+ }
1047
+ }
1048
+
1049
+ clear(): void {
1050
+ this.commands.clear();
1051
+ }
1052
+
1053
+ async exec(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
1054
+ const result = this.commands.get(command);
1055
+ if (result) return result;
1056
+
1057
+ // Default behavior for unknown commands
1058
+ return { stdout: "", stderr: `sh: command not found: ${command}`, exitCode: 127 };
1059
+ }
1060
+
1061
+ execSync(command: string): { stdout: string; stderr: string; exitCode: number } {
1062
+ return this.exec(command) as any; // Sync version returns same data
1063
+ }
1064
+
1065
+ async commandExists(command: string): Promise<boolean> {
1066
+ return this.availableCommands.has(command);
1067
+ }
1068
+ }
1069
+ ```
1070
+
1071
+ </details>
1072
+
1073
+ ### Pattern 3: HTTP Client
1074
+
1075
+ <details>
1076
+ <summary><strong>HTTP client abstraction example</strong></summary>
1077
+
1078
+ **Interface:**
1079
+
1080
+ ```typescript
1081
+ // src/utils/http-interface.ts
1082
+
1083
+ export interface IHTTPClient {
1084
+ get<T>(url: string, headers?: Record<string, string>): Promise<T>;
1085
+ post<T>(url: string, body: any, headers?: Record<string, string>): Promise<T>;
1086
+ put<T>(url: string, body: any, headers?: Record<string, string>): Promise<T>;
1087
+ delete<T>(url: string, headers?: Record<string, string>): Promise<T>;
1088
+ }
1089
+ ```
1090
+
1091
+ **System implementation:**
1092
+
1093
+ ```typescript
1094
+ // src/utils/http.system.ts
1095
+
1096
+ export class HTTPClientReal implements IHTTPClient {
1097
+ async get<T>(url: string, headers?: Record<string, string>): Promise<T> {
1098
+ const response = await fetch(url, { method: "GET", headers });
1099
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1100
+ return response.json();
1101
+ }
1102
+
1103
+ async post<T>(url: string, body: any, headers?: Record<string, string>): Promise<T> {
1104
+ const response = await fetch(url, {
1105
+ method: "POST",
1106
+ headers: { "Content-Type": "application/json", ...headers },
1107
+ body: JSON.stringify(body),
1108
+ });
1109
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1110
+ return response.json();
1111
+ }
1112
+
1113
+ async put<T>(url: string, body: any, headers?: Record<string, string>): Promise<T> {
1114
+ const response = await fetch(url, {
1115
+ method: "PUT",
1116
+ headers: { "Content-Type": "application/json", ...headers },
1117
+ body: JSON.stringify(body),
1118
+ });
1119
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1120
+ return response.json();
1121
+ }
1122
+
1123
+ async delete<T>(url: string, headers?: Record<string, string>): Promise<T> {
1124
+ const response = await fetch(url, { method: "DELETE", headers });
1125
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1126
+ return response.json();
1127
+ }
1128
+ }
1129
+
1130
+ export const defaultHTTP = new HTTPClientReal();
1131
+ ```
1132
+
1133
+ **Mock:**
1134
+
1135
+ ```typescript
1136
+ // tests/mocks/http-mock.ts
1137
+
1138
+ export class HTTPClientMock implements IHTTPClient {
1139
+ private routes = new Map<string, any>();
1140
+ private shouldFail = false;
1141
+
1142
+ setRoute(method: string, url: string, response: any): void {
1143
+ this.routes.set(`${method}:${url}`, response);
1144
+ }
1145
+
1146
+ setShouldFail(fail: boolean): void {
1147
+ this.shouldFail = fail;
1148
+ }
1149
+
1150
+ clear(): void {
1151
+ this.routes.clear();
1152
+ this.shouldFail = false;
1153
+ }
1154
+
1155
+ async get<T>(url: string, headers?: Record<string, string>): Promise<T> {
1156
+ if (this.shouldFail) throw new Error("HTTP 500: Internal Server Error");
1157
+ const response = this.routes.get(`GET:${url}`);
1158
+ if (!response) throw new Error(`HTTP 404: Not Found`);
1159
+ return response;
1160
+ }
1161
+
1162
+ async post<T>(url: string, body: any, headers?: Record<string, string>): Promise<T> {
1163
+ if (this.shouldFail) throw new Error("HTTP 500: Internal Server Error");
1164
+ const response = this.routes.get(`POST:${url}`);
1165
+ if (!response) throw new Error(`HTTP 404: Not Found`);
1166
+ return response;
1167
+ }
1168
+
1169
+ async put<T>(url: string, body: any, headers?: Record<string, string>): Promise<T> {
1170
+ if (this.shouldFail) throw new Error("HTTP 500: Internal Server Error");
1171
+ const response = this.routes.get(`PUT:${url}`);
1172
+ if (!response) throw new Error(`HTTP 404: Not Found`);
1173
+ return response;
1174
+ }
1175
+
1176
+ async delete<T>(url: string, headers?: Record<string, string>): Promise<T> {
1177
+ if (this.shouldFail) throw new Error("HTTP 500: Internal Server Error");
1178
+ const response = this.routes.get(`DELETE:${url}`);
1179
+ if (!response) throw new Error(`HTTP 404: Not Found`);
1180
+ return response;
1181
+ }
1182
+ }
1183
+ ```
1184
+
1185
+ </details>
1186
+
1187
+ ## Benefits Summary
1188
+
1189
+ ### Testing Benefits
1190
+
1191
+ - **Fast tests** - No system calls, all in-memory
1192
+ - **Deterministic** - Same inputs always produce same outputs
1193
+ - **Isolated** - No external dependencies
1194
+ - **Comprehensive** - Easy to test edge cases and error paths
1195
+ - **No mocking complexity** - Simple mock classes instead of complex spy setups
1196
+
1197
+ ### Coverage Benefits
1198
+
1199
+ - **Accurate metrics** - Coverage reflects business logic complexity
1200
+ - **Focused exclusions** - Only thin system wrappers excluded
1201
+ - **Better visibility** - Business logic coverage clearly visible
1202
+ - **Scalable pattern** - One glob pattern covers all system files
1203
+
1204
+ ### Maintainability Benefits
1205
+
1206
+ - **Clear boundaries** - Business logic vs. system interaction
1207
+ - **Single responsibility** - System files only do system calls
1208
+ - **Easy to change** - Swap system implementation without touching business logic
1209
+ - **Type safety** - Interface contract enforced at compile time
1210
+ - **Self-documenting** - File naming convention makes architecture clear
1211
+
1212
+ ## Migration Checklist
1213
+
1214
+ When refactoring existing code:
1215
+
1216
+ - [ ] Identify file with mixed business logic and system calls
1217
+ - [ ] Define interface for system operations (`{domain}-interface.ts`)
1218
+ - [ ] Create system implementation (`{domain}.system.ts`)
1219
+ - [ ] Create mock implementation (`tests/mocks/{domain}-mock.ts`)
1220
+ - [ ] Refactor business logic to accept interface parameter
1221
+ - [ ] Add default parameter for backward compatibility
1222
+ - [ ] Update tests to use mock implementation
1223
+ - [ ] Add `**/*.system.ts` to coverage exclusion config
1224
+ - [ ] Verify all tests pass with mocks
1225
+ - [ ] Verify production code works with real implementation
1226
+ - [ ] Remove old file from coverage exclusion list (if it was excluded)
1227
+ - [ ] Update documentation to explain new architecture
1228
+
1229
+ ## Anti-Patterns to Avoid
1230
+
1231
+ **Don't put business logic in `*.system.ts` files:**
1232
+
1233
+ ```typescript
1234
+ // ❌ BAD - Business logic in system file
1235
+ export class UserAPISystem implements IUserAPI {
1236
+ async fetchUser(userId: string): Promise<RawUserData> {
1237
+ // Business logic belongs in service layer, not here
1238
+ if (!userId || userId.length < 5) {
1239
+ throw new Error("Invalid user ID");
1240
+ }
1241
+
1242
+ const { stdout } = await execAsync(`curl ${this.baseURL}/users/${userId}`);
1243
+ return JSON.parse(stdout);
1244
+ }
1245
+ }
1246
+ ```
1247
+
1248
+ ```typescript
1249
+ // ✅ GOOD - Pure system interaction
1250
+ export class UserAPISystem implements IUserAPI {
1251
+ async fetchUser(userId: string): Promise<RawUserData> {
1252
+ const { stdout } = await execAsync(`curl ${this.baseURL}/users/${userId}`);
1253
+ return JSON.parse(stdout);
1254
+ }
1255
+ }
1256
+ ```
1257
+
1258
+ **Don't create overly broad interfaces:**
1259
+
1260
+ ```typescript
1261
+ // ❌ BAD - Too many responsibilities
1262
+ export interface ISystem {
1263
+ execCommand(cmd: string): Promise<string>;
1264
+ readFile(path: string): Promise<string>;
1265
+ httpGet(url: string): Promise<any>;
1266
+ queryDatabase(sql: string): Promise<any[]>;
1267
+ }
1268
+ ```
1269
+
1270
+ ```typescript
1271
+ // ✅ GOOD - Focused interfaces
1272
+ export interface IShellExecutor {
1273
+ exec(command: string): Promise<{ stdout: string; stderr: string }>;
1274
+ }
1275
+
1276
+ export interface IFileSystem {
1277
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
1278
+ }
1279
+
1280
+ export interface IHTTPClient {
1281
+ get<T>(url: string): Promise<T>;
1282
+ }
1283
+
1284
+ export interface IDatabase {
1285
+ query<T>(sql: string, params?: any[]): Promise<T[]>;
1286
+ }
1287
+ ```
1288
+
1289
+ **Don't skip the interface:**
1290
+
1291
+ ```typescript
1292
+ // ❌ BAD - Direct dependency on concrete class
1293
+ export async function processUser(userId: string, api: UserAPISystem): Promise<User> {
1294
+ // Now can't substitute mock
1295
+ }
1296
+ ```
1297
+
1298
+ ```typescript
1299
+ // ✅ GOOD - Depend on interface
1300
+ export async function processUser(userId: string, api: IUserAPI): Promise<User> {
1301
+ // Can use real or mock implementation
1302
+ }
1303
+ ```
1304
+
1305
+ ## Related Documentation
1306
+
1307
+ - [Clean Architecture for AI Agents](./agents.clean.arch.md) - Language-agnostic clean architecture principles
1308
+ - [Task Master AI Integration](./agents.task-master.md) - Project task management
1309
+ - [GoTask Usage Guide](./agents.gotask.md) - Build automation patterns
1310
+ - [Markdown Standards](./agents.markdown.md) - Documentation formatting
1311
+
1312
+ ---
1313
+
1314
+ **For AI Agents**: This guide implements clean architecture principles for TypeScript. Use `*.system.ts` naming convention for system interaction files, define interfaces for all external dependencies, and use constructor-based dependency injection for testable code.