tlc-claude-code 2.2.1 → 2.3.0

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.
@@ -0,0 +1,172 @@
1
+ // @depends server/lib/test-selector.js
2
+ import { describe, it, expect } from 'vitest';
3
+ import {
4
+ parseDependsComment,
5
+ buildTestMap,
6
+ getAffectedTests,
7
+ formatSelection,
8
+ } from './test-selector.js';
9
+
10
+ describe('test-selector', () => {
11
+ describe('parseDependsComment', () => {
12
+ it('extracts a single @depends path', () => {
13
+ const result = parseDependsComment('// @depends src/lib/auth.js');
14
+ expect(result).toEqual(['src/lib/auth.js']);
15
+ });
16
+
17
+ it('extracts multiple @depends from separate lines', () => {
18
+ const content = '// @depends src/a.js\n// @depends src/b.js';
19
+ const result = parseDependsComment(content);
20
+ expect(result).toEqual(['src/a.js', 'src/b.js']);
21
+ });
22
+
23
+ it('returns empty array when no @depends present', () => {
24
+ const result = parseDependsComment('no depends here');
25
+ expect(result).toEqual([]);
26
+ });
27
+
28
+ it('handles comma-separated dependencies on one line', () => {
29
+ const result = parseDependsComment('// @depends src/a.js, src/b.js');
30
+ expect(result).toEqual(['src/a.js', 'src/b.js']);
31
+ });
32
+
33
+ it('handles empty string', () => {
34
+ const result = parseDependsComment('');
35
+ expect(result).toEqual([]);
36
+ });
37
+
38
+ it('trims whitespace from paths', () => {
39
+ const result = parseDependsComment('// @depends src/foo.js ');
40
+ expect(result).toEqual(['src/foo.js']);
41
+ });
42
+
43
+ it('ignores @depends inside non-comment lines', () => {
44
+ const content = 'const x = "// @depends fake.js";\n// @depends real.js';
45
+ const result = parseDependsComment(content);
46
+ // Both lines start with // so the string literal line won't match
47
+ // Actually the first line starts with 'const' so it won't match
48
+ expect(result).toEqual(['real.js']);
49
+ });
50
+ });
51
+
52
+ describe('buildTestMap', () => {
53
+ it('builds mapping from test files to their dependencies', async () => {
54
+ const mockReadFile = async (filePath) => {
55
+ const files = {
56
+ 'tests/auth.test.js': '// @depends src/auth.js\nimport { auth } from "../src/auth.js";',
57
+ 'tests/db.test.js': '// @depends src/db.js, src/models.js\nimport { db } from "../src/db.js";',
58
+ 'tests/utils.test.js': 'import { utils } from "../src/utils.js";',
59
+ };
60
+ if (files[filePath] === undefined) {
61
+ throw new Error(`ENOENT: no such file: ${filePath}`);
62
+ }
63
+ return files[filePath];
64
+ };
65
+
66
+ const testFiles = ['tests/auth.test.js', 'tests/db.test.js', 'tests/utils.test.js'];
67
+ const result = await buildTestMap(testFiles, { readFile: mockReadFile });
68
+
69
+ expect(result).toBeInstanceOf(Map);
70
+ expect(result.get('tests/auth.test.js')).toEqual(['src/auth.js']);
71
+ expect(result.get('tests/db.test.js')).toEqual(['src/db.js', 'src/models.js']);
72
+ expect(result.get('tests/utils.test.js')).toEqual([]);
73
+ });
74
+
75
+ it('handles readFile errors gracefully by treating as no-depends', async () => {
76
+ const mockReadFile = async () => {
77
+ throw new Error('ENOENT: file not found');
78
+ };
79
+
80
+ const result = await buildTestMap(['missing.test.js'], { readFile: mockReadFile });
81
+
82
+ expect(result.get('missing.test.js')).toEqual([]);
83
+ });
84
+
85
+ it('returns empty map for empty test file list', async () => {
86
+ const mockReadFile = async () => '';
87
+ const result = await buildTestMap([], { readFile: mockReadFile });
88
+
89
+ expect(result).toBeInstanceOf(Map);
90
+ expect(result.size).toBe(0);
91
+ });
92
+ });
93
+
94
+ describe('getAffectedTests', () => {
95
+ const testMap = new Map([
96
+ ['tests/auth.test.js', ['src/auth.js']],
97
+ ['tests/db.test.js', ['src/db.js', 'src/models.js']],
98
+ ['tests/utils.test.js', []], // no @depends — always included
99
+ ['tests/api.test.js', ['src/api.js', 'src/auth.js']],
100
+ ]);
101
+
102
+ it('returns tests depending on a changed file', () => {
103
+ const result = getAffectedTests(['src/auth.js'], testMap);
104
+
105
+ expect(result).toContain('tests/auth.test.js');
106
+ expect(result).toContain('tests/api.test.js');
107
+ // no-depends tests are always included
108
+ expect(result).toContain('tests/utils.test.js');
109
+ });
110
+
111
+ it('returns only no-depends tests when changed file is unrelated', () => {
112
+ const result = getAffectedTests(['src/unrelated.js'], testMap);
113
+
114
+ expect(result).toEqual(['tests/utils.test.js']);
115
+ });
116
+
117
+ it('always includes tests with no @depends annotation', () => {
118
+ const result = getAffectedTests(['src/db.js'], testMap);
119
+
120
+ expect(result).toContain('tests/utils.test.js');
121
+ expect(result).toContain('tests/db.test.js');
122
+ expect(result).not.toContain('tests/auth.test.js');
123
+ });
124
+
125
+ it('returns only no-depends tests when changedFiles is empty', () => {
126
+ const result = getAffectedTests([], testMap);
127
+
128
+ expect(result).toEqual(['tests/utils.test.js']);
129
+ });
130
+
131
+ it('handles empty test map', () => {
132
+ const result = getAffectedTests(['src/auth.js'], new Map());
133
+
134
+ expect(result).toEqual([]);
135
+ });
136
+
137
+ it('handles multiple changed files', () => {
138
+ const result = getAffectedTests(['src/auth.js', 'src/db.js'], testMap);
139
+
140
+ expect(result).toContain('tests/auth.test.js');
141
+ expect(result).toContain('tests/db.test.js');
142
+ expect(result).toContain('tests/api.test.js');
143
+ expect(result).toContain('tests/utils.test.js');
144
+ });
145
+ });
146
+
147
+ describe('formatSelection', () => {
148
+ it('returns summary with skip count when not all tests selected', () => {
149
+ const result = formatSelection(3, 10);
150
+
151
+ expect(result).toBe('Running 3 of 10 tests (7 skipped — dependencies unchanged)');
152
+ });
153
+
154
+ it('returns "all tests" message when all selected', () => {
155
+ const result = formatSelection(10, 10);
156
+
157
+ expect(result).toBe('Running all 10 tests');
158
+ });
159
+
160
+ it('handles 1 of 1', () => {
161
+ const result = formatSelection(1, 1);
162
+
163
+ expect(result).toBe('Running all 1 tests');
164
+ });
165
+
166
+ it('handles 0 of N', () => {
167
+ const result = formatSelection(0, 5);
168
+
169
+ expect(result).toBe('Running 0 of 5 tests (5 skipped — dependencies unchanged)');
170
+ });
171
+ });
172
+ });
@@ -31,6 +31,12 @@ src/
31
31
  2. **No hardcoded URLs or config** - Use environment variables
32
32
  3. **No magic strings** - Define constants in `constants/` folder
33
33
  4. **JSDoc required** - Document all public functions, classes, and methods
34
+ 5. **No direct `process.env`** - All config through a validated config module
35
+ 6. **Ownership checks on every endpoint** - Auth is not enough; verify the user owns the resource
36
+ 7. **Never expose secrets in responses** - API keys and tokens are write-only
37
+ 8. **Hash sensitive data at rest** - OTPs, reset tokens, session secrets
38
+ 9. **Escape all HTML output** - No raw interpolation of user values
39
+ 10. **Use DI** - Never manually instantiate services with `new`
34
40
 
35
41
  ### Standards Reference
36
42
 
@@ -241,25 +241,64 @@ if (status === 'pending') { ... } // TypeScript will validate
241
241
 
242
242
  ---
243
243
 
244
- ## 6. Configuration: No Hardcoded Values
244
+ ## 6. Configuration: Validated, Centralized, No Direct `process.env`
245
+
246
+ ### Centralized Config with Validation
247
+
248
+ All configuration MUST go through a validated config module. Never read `process.env` directly in application code.
245
249
 
246
250
  ```typescript
247
- // ❌ NEVER
251
+ // ❌ NEVER: Direct process.env in services/controllers
248
252
  class PaymentService {
249
- private baseUrl = "https://api.stripe.com";
250
- private timeout = 30000;
253
+ private baseUrl = process.env.STRIPE_BASE_URL || 'https://api.stripe.com';
254
+ private apiKey = process.env.STRIPE_API_KEY; // silently undefined if missing
251
255
  }
252
256
 
253
- // ALWAYS
254
- // lib/configuration.ts or shared/config/stripe.config.ts
257
+ // NEVER: Config without validation
255
258
  export const stripeConfig = {
256
259
  baseUrl: process.env.STRIPE_BASE_URL || 'https://api.stripe.com',
257
- apiKey: process.env.STRIPE_API_KEY,
258
- timeout: parseInt(process.env.STRIPE_TIMEOUT || '30000'),
259
- } as const;
260
+ apiKey: process.env.STRIPE_API_KEY, // no validation, no fail-fast
261
+ };
262
+
263
+ // ✅ ALWAYS: Validated config module with fail-fast
264
+ // src/config/config.ts
265
+ import { z } from 'zod';
266
+
267
+ const configSchema = z.object({
268
+ stripe: z.object({
269
+ baseUrl: z.string().url().default('https://api.stripe.com'),
270
+ apiKey: z.string().min(1, 'STRIPE_API_KEY is required'),
271
+ timeout: z.coerce.number().int().positive().default(30000),
272
+ }),
273
+ });
274
+
275
+ export type AppConfig = z.infer<typeof configSchema>;
276
+
277
+ export function loadConfig(): AppConfig {
278
+ const result = configSchema.safeParse({
279
+ stripe: {
280
+ baseUrl: process.env.STRIPE_BASE_URL,
281
+ apiKey: process.env.STRIPE_API_KEY,
282
+ timeout: process.env.STRIPE_TIMEOUT,
283
+ },
284
+ });
285
+
286
+ if (!result.success) {
287
+ throw new Error(`Config validation failed:\n${result.error.format()}`);
288
+ }
289
+ return result.data;
290
+ }
260
291
  ```
261
292
 
262
- **Rule**: If it could differ between environments, it's config.
293
+ ### Rules
294
+
295
+ 1. **All `process.env` access MUST be in the config module** — never in services, controllers, or middleware.
296
+ 2. **All required env vars MUST be validated at startup** — fail fast, not at first request.
297
+ 3. **Distinguish required vs optional** — required vars throw on boot; optional have explicit defaults.
298
+ 4. **No silent empty-string fallbacks** for secrets or connection strings.
299
+ 5. **Config is injected via DI or imported from the config module** — never read from env directly.
300
+
301
+ **Rule**: If it could differ between environments, it's config. If it's required, validate it at boot.
263
302
 
264
303
  ---
265
304
 
@@ -432,6 +471,13 @@ Before committing any code:
432
471
  - [ ] Typed errors, not generic throws
433
472
  - [ ] Tests co-located with module
434
473
  - [ ] Build passes (`npm run build`)
474
+ - [ ] **No direct `process.env`** in services/controllers — config module only
475
+ - [ ] **Config validated at startup** with schema (Zod/Joi)
476
+ - [ ] **Ownership checks** on every data-access endpoint
477
+ - [ ] **No secrets in responses** — API keys, tokens are write-only
478
+ - [ ] **Sensitive data hashed** at rest (OTPs, reset tokens)
479
+ - [ ] **All HTML output escaped** — no raw interpolation of user values
480
+ - [ ] **No manual `new Service()`** — use DI container
435
481
 
436
482
  ---
437
483
 
@@ -588,6 +634,295 @@ Refs: #456
588
634
 
589
635
  ---
590
636
 
637
+ ## 18. File Size Limits
638
+
639
+ Large files are a code smell. They indicate a class or module is doing too much.
640
+
641
+ | Threshold | Action |
642
+ |---|---|
643
+ | **< 300 lines** | Ideal. No action needed. |
644
+ | **300-500 lines** | Acceptable. Review if it can be split. |
645
+ | **500-1000 lines** | Warning. Should be split into focused sub-modules. |
646
+ | **> 1000 lines** | Violation. MUST be split before merging. |
647
+
648
+ ### How to Split
649
+
650
+ **Controllers with many routes:**
651
+ ```
652
+ # Bad: csp.controller.ts (2,000+ lines)
653
+ # Good: Split by resource/feature:
654
+ modules/csp/
655
+ controllers/
656
+ policy.controller.ts # CRUD for policies
657
+ report.controller.ts # CSP violation reports
658
+ directive.controller.ts # Directive management
659
+ csp.routes.ts # Route registration (thin)
660
+ ```
661
+
662
+ **Services with many methods:**
663
+ ```
664
+ # Bad: user.service.ts (1,500 lines)
665
+ # Good: Split by responsibility:
666
+ modules/user/
667
+ services/
668
+ user-auth.service.ts # Login, register, password reset
669
+ user-profile.service.ts # Profile CRUD, avatar, preferences
670
+ user-admin.service.ts # Admin operations, ban, role changes
671
+ user.service.ts # Thin facade re-exporting sub-services
672
+ ```
673
+
674
+ ---
675
+
676
+ ## 19. Folder Overcrowding
677
+
678
+ Too many files in one folder makes navigation difficult. Organize into subfolders by domain.
679
+
680
+ | Threshold | Action |
681
+ |---|---|
682
+ | **< 8 files** | Fine as-is. |
683
+ | **8-15 files** | Consider grouping into subfolders. |
684
+ | **> 15 files** | MUST be organized into subfolders. |
685
+
686
+ ```
687
+ # Bad: 25 files dumped in controllers/
688
+ controllers/
689
+ auth.controller.ts
690
+ user.controller.ts
691
+ payment.controller.ts
692
+ invoice.controller.ts
693
+ product.controller.ts
694
+ ... (20 more)
695
+
696
+ # Good: Organized by domain
697
+ modules/
698
+ auth/auth.controller.ts
699
+ user/user.controller.ts
700
+ billing/
701
+ payment.controller.ts
702
+ invoice.controller.ts
703
+ catalog/
704
+ product.controller.ts
705
+ ```
706
+
707
+ ---
708
+
709
+ ## 20. Strict Typing
710
+
711
+ TypeScript without strict types is just JavaScript with extra steps.
712
+
713
+ ### Rules
714
+
715
+ 1. **No `any` type** - Use `unknown` and narrow, or define a proper interface.
716
+ 2. **No implicit `any`** - Enable `noImplicitAny` in tsconfig.
717
+ 3. **All functions must have explicit return types** - Not just exported functions.
718
+ 4. **All parameters must be typed** - No untyped function parameters.
719
+ 5. **Prefer `interface` over `type`** for object shapes - Easier to extend.
720
+ 6. **Use `strict: true`** in tsconfig.json.
721
+
722
+ ```typescript
723
+ // Bad: Missing types, implicit any
724
+ function processData(data) {
725
+ const result = data.items.map(item => item.value);
726
+ return result;
727
+ }
728
+
729
+ // Good: Explicit types everywhere
730
+ interface DataPayload {
731
+ items: Array<{ value: number }>;
732
+ }
733
+
734
+ function processData(data: DataPayload): number[] {
735
+ return data.items.map((item) => item.value);
736
+ }
737
+ ```
738
+
739
+ ```typescript
740
+ // Bad: any type
741
+ function handleResponse(response: any): any {
742
+ return response.data;
743
+ }
744
+
745
+ // Good: Proper types
746
+ interface ApiResponse<T> {
747
+ data: T;
748
+ status: number;
749
+ }
750
+
751
+ function handleResponse<T>(response: ApiResponse<T>): T {
752
+ return response.data;
753
+ }
754
+ ```
755
+
756
+ ### tsconfig.json Requirements
757
+
758
+ ```json
759
+ {
760
+ "compilerOptions": {
761
+ "strict": true,
762
+ "noImplicitAny": true,
763
+ "noImplicitReturns": true,
764
+ "noUnusedLocals": true,
765
+ "noUnusedParameters": true
766
+ }
767
+ }
768
+ ```
769
+
770
+ ---
771
+
772
+ ## 21. Security: Authorization, Secrets, and Output Encoding
773
+
774
+ ### Resource Ownership Checks
775
+
776
+ Every endpoint that accesses a resource MUST verify the requesting user owns or has permission to access that resource. Authentication alone is not enough.
777
+
778
+ ```typescript
779
+ // ❌ WRONG: Only checks authentication, not ownership
780
+ @Get('settings/:merchantId')
781
+ @UseGuards(AuthGuard)
782
+ async getSettings(@Param('merchantId') merchantId: string): Promise<Settings> {
783
+ return this.settingsService.findByMerchant(merchantId); // any authed user can read any merchant
784
+ }
785
+
786
+ // ✅ CORRECT: Verifies ownership
787
+ @Get('settings/:merchantId')
788
+ @UseGuards(AuthGuard)
789
+ async getSettings(
790
+ @Param('merchantId') merchantId: string,
791
+ @CurrentUser() user: User,
792
+ ): Promise<Settings> {
793
+ if (user.merchantId !== merchantId && user.role !== 'admin') {
794
+ throw new ForbiddenError('Cannot access another merchant\'s settings');
795
+ }
796
+ return this.settingsService.findByMerchant(merchantId);
797
+ }
798
+ ```
799
+
800
+ **Rules:**
801
+ 1. Every data-access endpoint MUST check that the requesting user owns the resource.
802
+ 2. Tests MUST prove that user A cannot read/modify user B's data.
803
+ 3. Prefer a reusable ownership guard over per-endpoint checks.
804
+
805
+ ### Never Expose Secrets in Responses
806
+
807
+ API keys, webhook secrets, tokens, and credentials MUST never appear in API responses or rendered HTML.
808
+
809
+ ```typescript
810
+ // ❌ WRONG: Returning secrets to the client
811
+ return {
812
+ merchantId: merchant.id,
813
+ apiKey: merchant.apiKey, // NEVER
814
+ webhookSecret: merchant.webhookSecret, // NEVER
815
+ };
816
+
817
+ // ✅ CORRECT: Mask or omit secrets
818
+ return {
819
+ merchantId: merchant.id,
820
+ apiKey: mask(merchant.apiKey), // "sk_live_...4x7f"
821
+ webhookSecret: '••••••••', // write-only, never read back
822
+ };
823
+ ```
824
+
825
+ **Rules:**
826
+ 1. Secrets are **write-only** — accept on create/update, never return in GET responses.
827
+ 2. If display is needed, return masked values (first 4 + last 4 chars).
828
+ 3. Audit every response DTO and HTML template for leaked credentials.
829
+
830
+ ### Hash Sensitive Data at Rest
831
+
832
+ OTP codes, reset tokens, and session secrets MUST be stored as one-way hashes, never plaintext.
833
+
834
+ ```typescript
835
+ // ❌ WRONG: Plaintext OTP
836
+ await db.otpSessions.insert({ code: '123456', expiresAt });
837
+
838
+ // ✅ CORRECT: Hashed OTP
839
+ import { createHash } from 'crypto';
840
+ const hashedCode = createHash('sha256').update(code).digest('hex');
841
+ await db.otpSessions.insert({ codeHash: hashedCode, expiresAt });
842
+
843
+ // Verification: hash the input and compare
844
+ function verifyOtp(input: string, stored: string): boolean {
845
+ const inputHash = createHash('sha256').update(input).digest('hex');
846
+ return timingSafeEqual(Buffer.from(inputHash), Buffer.from(stored));
847
+ }
848
+ ```
849
+
850
+ ### Output Encoding (XSS Prevention)
851
+
852
+ Never interpolate user-controlled values into HTML without escaping. This applies to inline HTML generation, template strings, and server-rendered pages.
853
+
854
+ ```typescript
855
+ // ❌ WRONG: Raw interpolation — XSS risk
856
+ const html = `<h1>Welcome, ${user.name}</h1>`;
857
+ const html = `<a href="${redirectUrl}">Continue</a>`;
858
+
859
+ // ✅ CORRECT: Always escape
860
+ import { escapeHtml } from '@/shared/utils/escape';
861
+
862
+ const html = `<h1>Welcome, ${escapeHtml(user.name)}</h1>`;
863
+ const html = `<a href="${escapeHtml(redirectUrl)}">Continue</a>`;
864
+ ```
865
+
866
+ ```typescript
867
+ // shared/utils/escape.ts
868
+ /**
869
+ * Escapes HTML special characters to prevent XSS.
870
+ */
871
+ export function escapeHtml(str: string): string {
872
+ return str
873
+ .replace(/&/g, '&amp;')
874
+ .replace(/</g, '&lt;')
875
+ .replace(/>/g, '&gt;')
876
+ .replace(/"/g, '&quot;')
877
+ .replace(/'/g, '&#x27;');
878
+ }
879
+ ```
880
+
881
+ **Rules:**
882
+ 1. **Every** dynamic value in HTML output MUST be escaped.
883
+ 2. Prefer a real templating engine over string concatenation for HTML.
884
+ 3. Audit merchant-controlled, query-string, and user-input values first.
885
+ 4. Do not add new pages using inline HTML string builders — use a proper frontend.
886
+
887
+ ---
888
+
889
+ ## 22. Dependency Injection: No Manual Instantiation
890
+
891
+ Never manually instantiate services or providers that should be managed by the DI container.
892
+
893
+ ```typescript
894
+ // ❌ WRONG: Bypassing DI
895
+ class PaymentService {
896
+ async processPayment(method: string): Promise<void> {
897
+ const provider = method === 'cyberpay'
898
+ ? new CyberpayProvider(process.env.CYBERPAY_KEY) // untestable, unmanaged
899
+ : new CodProvider();
900
+ await provider.charge(amount);
901
+ }
902
+ }
903
+
904
+ // ✅ CORRECT: Provider registry via DI
905
+ @Injectable()
906
+ class PaymentService {
907
+ constructor(
908
+ @Inject('PAYMENT_PROVIDERS') private providers: Map<string, PaymentProvider>,
909
+ ) {}
910
+
911
+ async processPayment(method: string): Promise<void> {
912
+ const provider = this.providers.get(method);
913
+ if (!provider) throw new BadRequestError(`Unknown payment method: ${method}`);
914
+ await provider.charge(amount);
915
+ }
916
+ }
917
+ ```
918
+
919
+ **Rules:**
920
+ 1. All providers/services MUST be registered in the DI container.
921
+ 2. Never use `new ServiceClass()` in application code — let the framework manage lifecycle.
922
+ 3. Use factory providers or provider registries for dynamic selection.
923
+
924
+ ---
925
+
591
926
  ## AI Instructions
592
927
 
593
928
  When generating code:
@@ -605,6 +940,17 @@ When generating code:
605
940
  11. **Always** add JSDoc to public members
606
941
  12. **Always** create index.ts with barrel exports
607
942
  13. **Always** verify build passes after changes
943
+ 14. **Never** let files exceed 1000 lines - split into sub-modules
944
+ 15. **Never** let folders exceed 15 files - organize into subfolders
945
+ 16. **Never** use `any` type - use `unknown` or proper interfaces
946
+ 17. **Always** add explicit return types to functions
947
+ 18. **Never** read `process.env` outside the config module
948
+ 19. **Always** validate config at startup with a schema
949
+ 20. **Always** add ownership/authorization checks on data-access endpoints
950
+ 21. **Never** return secrets (API keys, tokens) in API responses or HTML
951
+ 22. **Always** hash OTPs, reset tokens, and session secrets before storing
952
+ 23. **Always** escape dynamic values in HTML output
953
+ 24. **Never** use `new ServiceClass()` — register in DI and inject
608
954
 
609
955
  ### Cleanup Tasks
610
956