@webpieces/dev-config 0.2.61 → 0.2.63

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,23 @@
1
+ /**
2
+ * Validate Modified Files Executor
3
+ *
4
+ * Validates that modified files don't exceed a maximum line count (default 900).
5
+ * This encourages keeping files small and focused - when you touch a file,
6
+ * you must bring it under the limit.
7
+ *
8
+ * Usage:
9
+ * nx affected --target=validate-modified-files --base=origin/main
10
+ *
11
+ * Escape hatch: Add webpieces-disable max-lines-modified-files comment with date and justification
12
+ * Format: // webpieces-disable max-lines-modified-files 2025/01/15 -- [reason]
13
+ * The disable expires after 1 month from the date specified.
14
+ */
15
+ import type { ExecutorContext } from '@nx/devkit';
16
+ export interface ValidateModifiedFilesOptions {
17
+ max?: number;
18
+ forceLimit?: boolean;
19
+ }
20
+ export interface ExecutorResult {
21
+ success: boolean;
22
+ }
23
+ export default function runExecutor(options: ValidateModifiedFilesOptions, context: ExecutorContext): Promise<ExecutorResult>;
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ /**
3
+ * Validate Modified Files Executor
4
+ *
5
+ * Validates that modified files don't exceed a maximum line count (default 900).
6
+ * This encourages keeping files small and focused - when you touch a file,
7
+ * you must bring it under the limit.
8
+ *
9
+ * Usage:
10
+ * nx affected --target=validate-modified-files --base=origin/main
11
+ *
12
+ * Escape hatch: Add webpieces-disable max-lines-modified-files comment with date and justification
13
+ * Format: // webpieces-disable max-lines-modified-files 2025/01/15 -- [reason]
14
+ * The disable expires after 1 month from the date specified.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.default = runExecutor;
18
+ const tslib_1 = require("tslib");
19
+ const child_process_1 = require("child_process");
20
+ const fs = tslib_1.__importStar(require("fs"));
21
+ const path = tslib_1.__importStar(require("path"));
22
+ const TMP_DIR = 'tmp/webpieces';
23
+ const TMP_MD_FILE = 'webpieces.filesize.md';
24
+ const FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
25
+
26
+ **READ THIS FILE to fix files that are too long**
27
+
28
+ ## Core Principle
29
+
30
+ With **stateless systems + dependency injection, refactor is trivial**.
31
+ Pick a method or a few and move to new class XXXXX, then inject XXXXX
32
+ into all users of those methods via the constructor.
33
+ Delete those methods from original class.
34
+
35
+ **99% of files can be less than the configured max lines of code.**
36
+
37
+ Files should contain a SINGLE COHESIVE UNIT.
38
+ - One class per file (Java convention)
39
+ - If class is too large, extract child responsibilities
40
+ - Use dependency injection to compose functionality
41
+
42
+ ## Command: Reduce File Size
43
+
44
+ ### Step 1: Check for Multiple Classes
45
+ If the file contains multiple classes, **SEPARATE each class into its own file**.
46
+
47
+ \`\`\`typescript
48
+ // BAD: UserController.ts (multiple classes)
49
+ export class UserController { /* ... */ }
50
+ export class UserValidator { /* ... */ }
51
+ export class UserNotifier { /* ... */ }
52
+
53
+ // GOOD: Three separate files
54
+ // UserController.ts
55
+ export class UserController { /* ... */ }
56
+
57
+ // UserValidator.ts
58
+ export class UserValidator { /* ... */ }
59
+
60
+ // UserNotifier.ts
61
+ export class UserNotifier { /* ... */ }
62
+ \`\`\`
63
+
64
+ ### Step 2: Extract Child Responsibilities (if single class is too large)
65
+
66
+ #### Pattern: Create New Service Class with Dependency Injection
67
+
68
+ \`\`\`typescript
69
+ // BAD: UserController.ts (800 lines, single class)
70
+ @provideSingleton()
71
+ @Controller()
72
+ export class UserController {
73
+ // 200 lines: CRUD operations
74
+ // 300 lines: validation logic
75
+ // 200 lines: notification logic
76
+ // 100 lines: analytics logic
77
+ }
78
+
79
+ // GOOD: Extract validation service
80
+ // 1. Create UserValidationService.ts
81
+ @provideSingleton()
82
+ export class UserValidationService {
83
+ validateUserData(data: UserData): ValidationResult {
84
+ // 300 lines of validation logic moved here
85
+ }
86
+
87
+ validateEmail(email: string): boolean { /* ... */ }
88
+ validatePassword(password: string): boolean { /* ... */ }
89
+ }
90
+
91
+ // 2. Inject into UserController.ts
92
+ @provideSingleton()
93
+ @Controller()
94
+ export class UserController {
95
+ constructor(
96
+ @inject(TYPES.UserValidationService)
97
+ private validator: UserValidationService
98
+ ) {}
99
+
100
+ async createUser(data: UserData): Promise<User> {
101
+ const validation = this.validator.validateUserData(data);
102
+ if (!validation.isValid) {
103
+ throw new ValidationError(validation.errors);
104
+ }
105
+ // ... rest of logic
106
+ }
107
+ }
108
+ \`\`\`
109
+
110
+ ## AI Agent Action Steps
111
+
112
+ 1. **ANALYZE** the file structure:
113
+ - Count classes (if >1, separate immediately)
114
+ - Identify logical responsibilities within single class
115
+
116
+ 2. **IDENTIFY** "child code" to extract:
117
+ - Validation logic -> ValidationService
118
+ - Notification logic -> NotificationService
119
+ - Data transformation -> TransformerService
120
+ - External API calls -> ApiService
121
+ - Business rules -> RulesEngine
122
+
123
+ 3. **CREATE** new service file(s):
124
+ - Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
125
+ - Add \`@provideSingleton()\` decorator
126
+ - Move child methods to new class
127
+
128
+ 4. **UPDATE** dependency injection:
129
+ - Add to \`TYPES\` constants (if using symbol-based DI)
130
+ - Inject new service into original class constructor
131
+ - Replace direct method calls with \`this.serviceName.method()\`
132
+
133
+ 5. **RENAME** extracted file:
134
+ - Read the extracted code to understand its purpose
135
+ - Rename \`XXXX.ts\` to logical name (e.g., \`UserValidationService.ts\`)
136
+
137
+ 6. **VERIFY** file sizes:
138
+ - Original file should now be under the limit
139
+ - Each extracted file should be under the limit
140
+ - If still too large, extract more services
141
+
142
+ ## Examples of Child Responsibilities to Extract
143
+
144
+ | If File Contains | Extract To | Pattern |
145
+ |-----------------|------------|---------|
146
+ | Validation logic (200+ lines) | \`XValidator.ts\` or \`XValidationService.ts\` | Singleton service |
147
+ | Notification logic (150+ lines) | \`XNotifier.ts\` or \`XNotificationService.ts\` | Singleton service |
148
+ | Data transformation (200+ lines) | \`XTransformer.ts\` | Singleton service |
149
+ | External API calls (200+ lines) | \`XApiClient.ts\` | Singleton service |
150
+ | Complex business rules (300+ lines) | \`XRulesEngine.ts\` | Singleton service |
151
+ | Database queries (200+ lines) | \`XRepository.ts\` | Singleton service |
152
+
153
+ ## WebPieces Dependency Injection Pattern
154
+
155
+ \`\`\`typescript
156
+ // 1. Define service with @provideSingleton
157
+ import { provideSingleton } from '@webpieces/http-routing';
158
+
159
+ @provideSingleton()
160
+ export class MyService {
161
+ doSomething(): void { /* ... */ }
162
+ }
163
+
164
+ // 2. Inject into consumer
165
+ import { inject } from 'inversify';
166
+ import { TYPES } from './types';
167
+
168
+ @provideSingleton()
169
+ @Controller()
170
+ export class MyController {
171
+ constructor(
172
+ @inject(TYPES.MyService) private service: MyService
173
+ ) {}
174
+ }
175
+ \`\`\`
176
+
177
+ ## Escape Hatch
178
+
179
+ If refactoring is genuinely not feasible (generated files, complex algorithms, etc.),
180
+ add a disable comment at the TOP of the file (within first 5 lines) with a DATE:
181
+
182
+ \`\`\`typescript
183
+ // webpieces-disable max-lines-modified-files 2025/01/15 -- Complex generated file, refactoring would break generation
184
+ \`\`\`
185
+
186
+ **IMPORTANT**: The date format is yyyy/mm/dd. The disable will EXPIRE after 1 month from this date.
187
+ After expiration, you must either fix the file or update the date to get another month.
188
+ This ensures that disable comments are reviewed periodically.
189
+
190
+ Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
191
+ `;
192
+ /**
193
+ * Write the instructions documentation to tmp directory
194
+ */
195
+ function writeTmpInstructions(workspaceRoot) {
196
+ const tmpDir = path.join(workspaceRoot, TMP_DIR);
197
+ const mdPath = path.join(tmpDir, TMP_MD_FILE);
198
+ fs.mkdirSync(tmpDir, { recursive: true });
199
+ fs.writeFileSync(mdPath, FILESIZE_DOC_CONTENT);
200
+ return mdPath;
201
+ }
202
+ /**
203
+ * Get changed TypeScript files between base and working tree.
204
+ * Uses `git diff base` (no three-dots) to match what `nx affected` does -
205
+ * this includes both committed and uncommitted changes in one diff.
206
+ */
207
+ function getChangedTypeScriptFiles(workspaceRoot, base) {
208
+ try {
209
+ // Use two-dot diff (base to working tree) - same as nx affected
210
+ const output = (0, child_process_1.execSync)(`git diff --name-only ${base} -- '*.ts' '*.tsx'`, {
211
+ cwd: workspaceRoot,
212
+ encoding: 'utf-8',
213
+ });
214
+ return output
215
+ .trim()
216
+ .split('\n')
217
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
218
+ }
219
+ catch {
220
+ return [];
221
+ }
222
+ }
223
+ /**
224
+ * Parse a date string in yyyy/mm/dd format and return a Date object.
225
+ * Returns null if the format is invalid.
226
+ */
227
+ function parseDisableDate(dateStr) {
228
+ // Match yyyy/mm/dd format
229
+ const match = dateStr.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
230
+ if (!match)
231
+ return null;
232
+ const year = parseInt(match[1], 10);
233
+ const month = parseInt(match[2], 10) - 1; // JS months are 0-indexed
234
+ const day = parseInt(match[3], 10);
235
+ const date = new Date(year, month, day);
236
+ // Validate the date is valid (e.g., not Feb 30)
237
+ if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
238
+ return null;
239
+ }
240
+ return date;
241
+ }
242
+ /**
243
+ * Check if a date is within the last month (not expired).
244
+ */
245
+ function isDateWithinMonth(date) {
246
+ const now = new Date();
247
+ const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
248
+ return date >= oneMonthAgo;
249
+ }
250
+ /**
251
+ * Check if a file has a valid, non-expired disable comment at the top (within first 5 lines).
252
+ * Returns status object with details about the disable comment.
253
+ */
254
+ // webpieces-disable max-lines-new-methods -- Date validation logic requires checking multiple conditions
255
+ function checkDisableComment(content) {
256
+ const lines = content.split('\n').slice(0, 5);
257
+ for (const line of lines) {
258
+ if (line.includes('webpieces-disable') && line.includes('max-lines-modified-files')) {
259
+ // Found disable comment, now check for date
260
+ // Format: // webpieces-disable max-lines-modified-files yyyy/mm/dd -- reason
261
+ const dateMatch = line.match(/max-lines-modified-files\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
262
+ if (!dateMatch) {
263
+ // No date found - invalid disable comment
264
+ return { hasDisable: true, isValid: false, isExpired: false };
265
+ }
266
+ const dateStr = dateMatch[1];
267
+ // Secret permanent disable
268
+ if (dateStr === 'XXXX/XX/XX') {
269
+ return { hasDisable: true, isValid: true, isExpired: false, date: dateStr };
270
+ }
271
+ const date = parseDisableDate(dateStr);
272
+ if (!date) {
273
+ // Invalid date format
274
+ return { hasDisable: true, isValid: false, isExpired: false, date: dateStr };
275
+ }
276
+ if (!isDateWithinMonth(date)) {
277
+ // Date is expired (older than 1 month)
278
+ return { hasDisable: true, isValid: true, isExpired: true, date: dateStr };
279
+ }
280
+ // Valid and not expired
281
+ return { hasDisable: true, isValid: true, isExpired: false, date: dateStr };
282
+ }
283
+ }
284
+ return { hasDisable: false, isValid: false, isExpired: false };
285
+ }
286
+ /**
287
+ * Count lines in a file and check for violations
288
+ */
289
+ // webpieces-disable max-lines-new-methods -- File iteration with disable checking logic
290
+ function findViolations(workspaceRoot, changedFiles, maxLines, forceLimit) {
291
+ const violations = [];
292
+ for (const file of changedFiles) {
293
+ const fullPath = path.join(workspaceRoot, file);
294
+ if (!fs.existsSync(fullPath))
295
+ continue;
296
+ const content = fs.readFileSync(fullPath, 'utf-8');
297
+ const lineCount = content.split('\n').length;
298
+ // Skip files under the limit
299
+ if (lineCount <= maxLines)
300
+ continue;
301
+ // When forceLimit is true, ignore all disable comments
302
+ if (forceLimit) {
303
+ violations.push({ file, lines: lineCount });
304
+ continue;
305
+ }
306
+ // Check for disable comment
307
+ const disableStatus = checkDisableComment(content);
308
+ if (disableStatus.hasDisable) {
309
+ if (disableStatus.isValid && !disableStatus.isExpired) {
310
+ // Valid, non-expired disable - skip this file
311
+ continue;
312
+ }
313
+ if (disableStatus.isExpired) {
314
+ // Expired disable - report as violation with expired info
315
+ violations.push({
316
+ file,
317
+ lines: lineCount,
318
+ expiredDisable: true,
319
+ expiredDate: disableStatus.date,
320
+ });
321
+ continue;
322
+ }
323
+ // Invalid disable (missing/bad date) - fall through to report as violation
324
+ }
325
+ violations.push({
326
+ file,
327
+ lines: lineCount,
328
+ });
329
+ }
330
+ return violations;
331
+ }
332
+ /**
333
+ * Auto-detect the base branch by finding the merge-base with origin/main.
334
+ */
335
+ function detectBase(workspaceRoot) {
336
+ try {
337
+ const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD origin/main', {
338
+ cwd: workspaceRoot,
339
+ encoding: 'utf-8',
340
+ stdio: ['pipe', 'pipe', 'pipe'],
341
+ }).trim();
342
+ if (mergeBase) {
343
+ return mergeBase;
344
+ }
345
+ }
346
+ catch {
347
+ try {
348
+ const mergeBase = (0, child_process_1.execSync)('git merge-base HEAD main', {
349
+ cwd: workspaceRoot,
350
+ encoding: 'utf-8',
351
+ stdio: ['pipe', 'pipe', 'pipe'],
352
+ }).trim();
353
+ if (mergeBase) {
354
+ return mergeBase;
355
+ }
356
+ }
357
+ catch {
358
+ // Ignore
359
+ }
360
+ }
361
+ return null;
362
+ }
363
+ /**
364
+ * Get today's date in yyyy/mm/dd format for error messages
365
+ */
366
+ function getTodayDateString() {
367
+ const now = new Date();
368
+ const year = now.getFullYear();
369
+ const month = String(now.getMonth() + 1).padStart(2, '0');
370
+ const day = String(now.getDate()).padStart(2, '0');
371
+ return `${year}/${month}/${day}`;
372
+ }
373
+ /**
374
+ * Report violations to console
375
+ */
376
+ // webpieces-disable max-lines-new-methods -- Error output formatting with multiple message sections
377
+ function reportViolations(violations, maxLines, forceLimit) {
378
+ console.error('');
379
+ console.error('❌ YOU MUST FIX THIS AND NOT be more than ' + maxLines + ' lines of code per file');
380
+ console.error(' as it slows down IDEs AND is VERY VERY EASY to refactor.');
381
+ console.error('');
382
+ console.error('📚 With stateless systems + dependency injection, refactor is trivial:');
383
+ console.error(' Pick a method or a few and move to new class XXXXX, then inject XXXXX');
384
+ console.error(' into all users of those methods via the constructor.');
385
+ console.error(' Delete those methods from original class.');
386
+ console.error(' 99% of files can be less than ' + maxLines + ' lines of code.');
387
+ console.error('');
388
+ console.error('⚠️ *** READ tmp/webpieces/webpieces.filesize.md for detailed guidance on how to fix this easily *** ⚠️');
389
+ console.error('');
390
+ for (const v of violations) {
391
+ if (v.expiredDisable) {
392
+ console.error(` ❌ ${v.file} (${v.lines} lines, max: ${maxLines})`);
393
+ console.error(` ⏰ EXPIRED DISABLE: Your disable comment dated ${v.expiredDate} has expired (>1 month old).`);
394
+ console.error(` You must either FIX the file or UPDATE the date to get another month.`);
395
+ }
396
+ else {
397
+ console.error(` ❌ ${v.file} (${v.lines} lines, max: ${maxLines})`);
398
+ }
399
+ }
400
+ console.error('');
401
+ // Only show escape hatch instructions when forceLimit is not enabled
402
+ if (!forceLimit) {
403
+ console.error(' You can disable this error, but you will be forced to fix again in 1 month');
404
+ console.error(' since 99% of files can be less than ' + maxLines + ' lines of code.');
405
+ console.error('');
406
+ console.error(' Use escape with DATE (expires in 1 month):');
407
+ console.error(` // webpieces-disable max-lines-modified-files ${getTodayDateString()} -- [your reason]`);
408
+ console.error('');
409
+ }
410
+ else {
411
+ console.error(' ⚠️ forceModifiedFilesLimit is enabled - disable comments are NOT allowed.');
412
+ console.error(' You MUST refactor to reduce file size.');
413
+ console.error('');
414
+ }
415
+ }
416
+ async function runExecutor(options, context) {
417
+ const workspaceRoot = context.root;
418
+ const maxLines = options.max ?? 900;
419
+ const forceLimit = options.forceLimit ?? false;
420
+ let base = process.env['NX_BASE'];
421
+ if (!base) {
422
+ base = detectBase(workspaceRoot) ?? undefined;
423
+ if (!base) {
424
+ console.log('\n⏭️ Skipping modified files validation (could not detect base branch)');
425
+ console.log(' To run explicitly: nx affected --target=validate-modified-files --base=origin/main');
426
+ console.log('');
427
+ return { success: true };
428
+ }
429
+ console.log('\n📏 Validating Modified File Sizes (auto-detected base)\n');
430
+ }
431
+ else {
432
+ console.log('\n📏 Validating Modified File Sizes\n');
433
+ }
434
+ console.log(` Base: ${base}`);
435
+ console.log(' Comparing to: working tree (includes uncommitted changes)');
436
+ console.log(` Max lines for modified files: ${maxLines}`);
437
+ if (forceLimit) {
438
+ console.log(' Force limit: ENABLED (disable comments will be ignored)');
439
+ }
440
+ console.log('');
441
+ try {
442
+ const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base);
443
+ if (changedFiles.length === 0) {
444
+ console.log('✅ No TypeScript files changed');
445
+ return { success: true };
446
+ }
447
+ console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);
448
+ const violations = findViolations(workspaceRoot, changedFiles, maxLines, forceLimit);
449
+ if (violations.length === 0) {
450
+ console.log('✅ All modified files are under ' + maxLines + ' lines');
451
+ return { success: true };
452
+ }
453
+ writeTmpInstructions(workspaceRoot);
454
+ reportViolations(violations, maxLines, forceLimit);
455
+ return { success: false };
456
+ }
457
+ catch (err) {
458
+ const error = err instanceof Error ? err : new Error(String(err));
459
+ console.error('❌ Modified files validation failed:', error.message);
460
+ return { success: false };
461
+ }
462
+ }
463
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/dev-config/architecture/executors/validate-modified-files/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Modified Files Executor\n *\n * Validates that modified files don't exceed a maximum line count (default 900).\n * This encourages keeping files small and focused - when you touch a file,\n * you must bring it under the limit.\n *\n * Usage:\n * nx affected --target=validate-modified-files --base=origin/main\n *\n * Escape hatch: Add webpieces-disable max-lines-modified-files comment with date and justification\n * Format: // webpieces-disable max-lines-modified-files 2025/01/15 -- [reason]\n * The disable expires after 1 month from the date specified.\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nexport interface ValidateModifiedFilesOptions {\n max?: number;\n forceLimit?: boolean;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\ninterface FileViolation {\n file: string;\n lines: number;\n expiredDisable?: boolean;\n expiredDate?: string;\n}\n\nconst TMP_DIR = 'tmp/webpieces';\nconst TMP_MD_FILE = 'webpieces.filesize.md';\n\nconst FILESIZE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\n\nWith **stateless systems + dependency injection, refactor is trivial**.\nPick a method or a few and move to new class XXXXX, then inject XXXXX\ninto all users of those methods via the constructor.\nDelete those methods from original class.\n\n**99% of files can be less than the configured max lines of code.**\n\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (multiple classes)\nexport class UserController { /* ... */ }\nexport class UserValidator { /* ... */ }\nexport class UserNotifier { /* ... */ }\n\n// GOOD: Three separate files\n// UserController.ts\nexport class UserController { /* ... */ }\n\n// UserValidator.ts\nexport class UserValidator { /* ... */ }\n\n// UserNotifier.ts\nexport class UserNotifier { /* ... */ }\n\\`\\`\\`\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n // 100 lines: analytics logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult {\n // 300 lines of validation logic moved here\n }\n\n validateEmail(email: string): boolean { /* ... */ }\n validatePassword(password: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n\n async createUser(data: UserData): Promise<User> {\n const validation = this.validator.validateUserData(data);\n if (!validation.isValid) {\n throw new ValidationError(validation.errors);\n }\n // ... rest of logic\n }\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Start with temporary name: \\`XXXX.ts\\` or \\`ChildService.ts\\`\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Add to \\`TYPES\\` constants (if using symbol-based DI)\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **RENAME** extracted file:\n - Read the extracted code to understand its purpose\n - Rename \\`XXXX.ts\\` to logical name (e.g., \\`UserValidationService.ts\\`)\n\n6. **VERIFY** file sizes:\n - Original file should now be under the limit\n - Each extracted file should be under the limit\n - If still too large, extract more services\n\n## Examples of Child Responsibilities to Extract\n\n| If File Contains | Extract To | Pattern |\n|-----------------|------------|---------|\n| Validation logic (200+ lines) | \\`XValidator.ts\\` or \\`XValidationService.ts\\` | Singleton service |\n| Notification logic (150+ lines) | \\`XNotifier.ts\\` or \\`XNotificationService.ts\\` | Singleton service |\n| Data transformation (200+ lines) | \\`XTransformer.ts\\` | Singleton service |\n| External API calls (200+ lines) | \\`XApiClient.ts\\` | Singleton service |\n| Complex business rules (300+ lines) | \\`XRulesEngine.ts\\` | Singleton service |\n| Database queries (200+ lines) | \\`XRepository.ts\\` | Singleton service |\n\n## WebPieces Dependency Injection Pattern\n\n\\`\\`\\`typescript\n// 1. Define service with @provideSingleton\nimport { provideSingleton } from '@webpieces/http-routing';\n\n@provideSingleton()\nexport class MyService {\n doSomething(): void { /* ... */ }\n}\n\n// 2. Inject into consumer\nimport { inject } from 'inversify';\nimport { TYPES } from './types';\n\n@provideSingleton()\n@Controller()\nexport class MyController {\n constructor(\n @inject(TYPES.MyService) private service: MyService\n ) {}\n}\n\\`\\`\\`\n\n## Escape Hatch\n\nIf refactoring is genuinely not feasible (generated files, complex algorithms, etc.),\nadd a disable comment at the TOP of the file (within first 5 lines) with a DATE:\n\n\\`\\`\\`typescript\n// webpieces-disable max-lines-modified-files 2025/01/15 -- Complex generated file, refactoring would break generation\n\\`\\`\\`\n\n**IMPORTANT**: The date format is yyyy/mm/dd. The disable will EXPIRE after 1 month from this date.\nAfter expiration, you must either fix the file or update the date to get another month.\nThis ensures that disable comments are reviewed periodically.\n\nRemember: Find the \"child code\" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.\n`;\n\n/**\n * Write the instructions documentation to tmp directory\n */\nfunction writeTmpInstructions(workspaceRoot: string): string {\n const tmpDir = path.join(workspaceRoot, TMP_DIR);\n const mdPath = path.join(tmpDir, TMP_MD_FILE);\n\n fs.mkdirSync(tmpDir, { recursive: true });\n fs.writeFileSync(mdPath, FILESIZE_DOC_CONTENT);\n\n return mdPath;\n}\n\n/**\n * Get changed TypeScript files between base and working tree.\n * Uses `git diff base` (no three-dots) to match what `nx affected` does -\n * this includes both committed and uncommitted changes in one diff.\n */\nfunction getChangedTypeScriptFiles(workspaceRoot: string, base: string): string[] {\n try {\n // Use two-dot diff (base to working tree) - same as nx affected\n const output = execSync(`git diff --name-only ${base} -- '*.ts' '*.tsx'`, {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n return output\n .trim()\n .split('\\n')\n .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));\n } catch {\n return [];\n }\n}\n\n/**\n * Parse a date string in yyyy/mm/dd format and return a Date object.\n * Returns null if the format is invalid.\n */\nfunction parseDisableDate(dateStr: string): Date | null {\n // Match yyyy/mm/dd format\n const match = dateStr.match(/^(\\d{4})\\/(\\d{2})\\/(\\d{2})$/);\n if (!match) return null;\n\n const year = parseInt(match[1], 10);\n const month = parseInt(match[2], 10) - 1; // JS months are 0-indexed\n const day = parseInt(match[3], 10);\n\n const date = new Date(year, month, day);\n\n // Validate the date is valid (e.g., not Feb 30)\n if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {\n return null;\n }\n\n return date;\n}\n\n/**\n * Check if a date is within the last month (not expired).\n */\nfunction isDateWithinMonth(date: Date): boolean {\n const now = new Date();\n const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());\n return date >= oneMonthAgo;\n}\n\ninterface DisableStatus {\n hasDisable: boolean;\n isValid: boolean;\n isExpired: boolean;\n date?: string;\n}\n\n/**\n * Check if a file has a valid, non-expired disable comment at the top (within first 5 lines).\n * Returns status object with details about the disable comment.\n */\n// webpieces-disable max-lines-new-methods -- Date validation logic requires checking multiple conditions\nfunction checkDisableComment(content: string): DisableStatus {\n const lines = content.split('\\n').slice(0, 5);\n\n for (const line of lines) {\n if (line.includes('webpieces-disable') && line.includes('max-lines-modified-files')) {\n // Found disable comment, now check for date\n // Format: // webpieces-disable max-lines-modified-files yyyy/mm/dd -- reason\n const dateMatch = line.match(/max-lines-modified-files\\s+(\\d{4}\\/\\d{2}\\/\\d{2}|XXXX\\/XX\\/XX)/);\n\n if (!dateMatch) {\n // No date found - invalid disable comment\n return { hasDisable: true, isValid: false, isExpired: false };\n }\n\n const dateStr = dateMatch[1];\n\n // Secret permanent disable\n if (dateStr === 'XXXX/XX/XX') {\n return { hasDisable: true, isValid: true, isExpired: false, date: dateStr };\n }\n\n const date = parseDisableDate(dateStr);\n if (!date) {\n // Invalid date format\n return { hasDisable: true, isValid: false, isExpired: false, date: dateStr };\n }\n\n if (!isDateWithinMonth(date)) {\n // Date is expired (older than 1 month)\n return { hasDisable: true, isValid: true, isExpired: true, date: dateStr };\n }\n\n // Valid and not expired\n return { hasDisable: true, isValid: true, isExpired: false, date: dateStr };\n }\n }\n\n return { hasDisable: false, isValid: false, isExpired: false };\n}\n\n/**\n * Count lines in a file and check for violations\n */\n// webpieces-disable max-lines-new-methods -- File iteration with disable checking logic\nfunction findViolations(workspaceRoot: string, changedFiles: string[], maxLines: number, forceLimit: boolean): FileViolation[] {\n const violations: FileViolation[] = [];\n\n for (const file of changedFiles) {\n const fullPath = path.join(workspaceRoot, file);\n\n if (!fs.existsSync(fullPath)) continue;\n\n const content = fs.readFileSync(fullPath, 'utf-8');\n const lineCount = content.split('\\n').length;\n\n // Skip files under the limit\n if (lineCount <= maxLines) continue;\n\n // When forceLimit is true, ignore all disable comments\n if (forceLimit) {\n violations.push({ file, lines: lineCount });\n continue;\n }\n\n // Check for disable comment\n const disableStatus = checkDisableComment(content);\n\n if (disableStatus.hasDisable) {\n if (disableStatus.isValid && !disableStatus.isExpired) {\n // Valid, non-expired disable - skip this file\n continue;\n }\n\n if (disableStatus.isExpired) {\n // Expired disable - report as violation with expired info\n violations.push({\n file,\n lines: lineCount,\n expiredDisable: true,\n expiredDate: disableStatus.date,\n });\n continue;\n }\n\n // Invalid disable (missing/bad date) - fall through to report as violation\n }\n\n violations.push({\n file,\n lines: lineCount,\n });\n }\n\n return violations;\n}\n\n/**\n * Auto-detect the base branch by finding the merge-base with origin/main.\n */\nfunction detectBase(workspaceRoot: string): string | null {\n try {\n const mergeBase = execSync('git merge-base HEAD origin/main', {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (mergeBase) {\n return mergeBase;\n }\n } catch {\n try {\n const mergeBase = execSync('git merge-base HEAD main', {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n\n if (mergeBase) {\n return mergeBase;\n }\n } catch {\n // Ignore\n }\n }\n return null;\n}\n\n/**\n * Get today's date in yyyy/mm/dd format for error messages\n */\nfunction getTodayDateString(): string {\n const now = new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n return `${year}/${month}/${day}`;\n}\n\n/**\n * Report violations to console\n */\n// webpieces-disable max-lines-new-methods -- Error output formatting with multiple message sections\nfunction reportViolations(violations: FileViolation[], maxLines: number, forceLimit: boolean): void {\n console.error('');\n console.error('❌ YOU MUST FIX THIS AND NOT be more than ' + maxLines + ' lines of code per file');\n console.error(' as it slows down IDEs AND is VERY VERY EASY to refactor.');\n console.error('');\n console.error('📚 With stateless systems + dependency injection, refactor is trivial:');\n console.error(' Pick a method or a few and move to new class XXXXX, then inject XXXXX');\n console.error(' into all users of those methods via the constructor.');\n console.error(' Delete those methods from original class.');\n console.error(' 99% of files can be less than ' + maxLines + ' lines of code.');\n console.error('');\n console.error('⚠️ *** READ tmp/webpieces/webpieces.filesize.md for detailed guidance on how to fix this easily *** ⚠️');\n console.error('');\n\n for (const v of violations) {\n if (v.expiredDisable) {\n console.error(` ❌ ${v.file} (${v.lines} lines, max: ${maxLines})`);\n console.error(` ⏰ EXPIRED DISABLE: Your disable comment dated ${v.expiredDate} has expired (>1 month old).`);\n console.error(` You must either FIX the file or UPDATE the date to get another month.`);\n } else {\n console.error(` ❌ ${v.file} (${v.lines} lines, max: ${maxLines})`);\n }\n }\n console.error('');\n\n // Only show escape hatch instructions when forceLimit is not enabled\n if (!forceLimit) {\n console.error(' You can disable this error, but you will be forced to fix again in 1 month');\n console.error(' since 99% of files can be less than ' + maxLines + ' lines of code.');\n console.error('');\n console.error(' Use escape with DATE (expires in 1 month):');\n console.error(` // webpieces-disable max-lines-modified-files ${getTodayDateString()} -- [your reason]`);\n console.error('');\n } else {\n console.error(' ⚠️ forceModifiedFilesLimit is enabled - disable comments are NOT allowed.');\n console.error(' You MUST refactor to reduce file size.');\n console.error('');\n }\n}\n\nexport default async function runExecutor(\n options: ValidateModifiedFilesOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const maxLines = options.max ?? 900;\n const forceLimit = options.forceLimit ?? false;\n\n let base = process.env['NX_BASE'];\n\n if (!base) {\n base = detectBase(workspaceRoot) ?? undefined;\n\n if (!base) {\n console.log('\\n⏭️ Skipping modified files validation (could not detect base branch)');\n console.log(' To run explicitly: nx affected --target=validate-modified-files --base=origin/main');\n console.log('');\n return { success: true };\n }\n\n console.log('\\n📏 Validating Modified File Sizes (auto-detected base)\\n');\n } else {\n console.log('\\n📏 Validating Modified File Sizes\\n');\n }\n\n console.log(` Base: ${base}`);\n console.log(' Comparing to: working tree (includes uncommitted changes)');\n console.log(` Max lines for modified files: ${maxLines}`);\n if (forceLimit) {\n console.log(' Force limit: ENABLED (disable comments will be ignored)');\n }\n console.log('');\n\n try {\n const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base);\n\n if (changedFiles.length === 0) {\n console.log('✅ No TypeScript files changed');\n return { success: true };\n }\n\n console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);\n\n const violations = findViolations(workspaceRoot, changedFiles, maxLines, forceLimit);\n\n if (violations.length === 0) {\n console.log('✅ All modified files are under ' + maxLines + ' lines');\n return { success: true };\n }\n\n writeTmpInstructions(workspaceRoot);\n reportViolations(violations, maxLines, forceLimit);\n return { success: false };\n } catch (err: unknown) {\n const error = err instanceof Error ? err : new Error(String(err));\n console.error('❌ Modified files validation failed:', error.message);\n return { success: false };\n }\n}\n"]}