@webpieces/dev-config 0.2.39 → 0.2.40

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,22 @@
1
+ /**
2
+ * Validate New Methods Executor
3
+ *
4
+ * Validates that newly added methods don't exceed a maximum line count.
5
+ * Only runs when NX_BASE environment variable is set (affected mode).
6
+ *
7
+ * This validator encourages writing methods that read like a "table of contents"
8
+ * where each method call describes a larger piece of work.
9
+ *
10
+ * Usage:
11
+ * nx affected --target=validate-new-methods --base=origin/main
12
+ *
13
+ * Escape hatch: Add eslint-disable comment with justification
14
+ */
15
+ import type { ExecutorContext } from '@nx/devkit';
16
+ export interface ValidateNewMethodsOptions {
17
+ max?: number;
18
+ }
19
+ export interface ExecutorResult {
20
+ success: boolean;
21
+ }
22
+ export default function runExecutor(options: ValidateNewMethodsOptions, context: ExecutorContext): Promise<ExecutorResult>;
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ /**
3
+ * Validate New Methods Executor
4
+ *
5
+ * Validates that newly added methods don't exceed a maximum line count.
6
+ * Only runs when NX_BASE environment variable is set (affected mode).
7
+ *
8
+ * This validator encourages writing methods that read like a "table of contents"
9
+ * where each method call describes a larger piece of work.
10
+ *
11
+ * Usage:
12
+ * nx affected --target=validate-new-methods --base=origin/main
13
+ *
14
+ * Escape hatch: Add eslint-disable comment with justification
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 ts = tslib_1.__importStar(require("typescript"));
23
+ const TMP_DIR = 'tmp/webpieces';
24
+ const TMP_MD_FILE = 'webpieces.methodsize.md';
25
+ const METHODSIZE_DOC_CONTENT = `# Instructions: New Method Too Long
26
+
27
+ ## The "Table of Contents" Principle
28
+
29
+ Good code reads like a book's table of contents:
30
+ - Chapter titles (method names) tell you WHAT happens
31
+ - Reading chapter titles gives you the full story
32
+ - You can dive into chapters (implementations) for details
33
+
34
+ ## Why Limit New Methods to 20-30 Lines?
35
+
36
+ Methods under 20-30 lines are:
37
+ - Easy to review in a single screen
38
+ - Simple to understand without scrolling
39
+ - Quick for AI to analyze and suggest improvements
40
+ - More testable in isolation
41
+ - Self-documenting through well-named extracted methods
42
+
43
+ **~50% of the time**, you can stay under 20-30 lines in new code by extracting
44
+ logical units into well-named methods. This makes code more readable for both
45
+ AI and humans.
46
+
47
+ ## How to Refactor
48
+
49
+ Instead of:
50
+ \`\`\`typescript
51
+ async processOrder(order: Order): Promise<Result> {
52
+ // 50 lines of validation, transformation, saving, notifications...
53
+ }
54
+ \`\`\`
55
+
56
+ Write:
57
+ \`\`\`typescript
58
+ async processOrder(order: Order): Promise<Result> {
59
+ const validated = this.validateOrder(order);
60
+ const transformed = this.applyBusinessRules(validated);
61
+ const saved = await this.saveToDatabase(transformed);
62
+ await this.notifyStakeholders(saved);
63
+ return this.buildResult(saved);
64
+ }
65
+ \`\`\`
66
+
67
+ Now the main method is a "table of contents" - each line tells part of the story!
68
+
69
+ ## Patterns for Extraction
70
+
71
+ ### Pattern 1: Extract Loop Bodies
72
+ \`\`\`typescript
73
+ // BEFORE
74
+ for (const item of items) {
75
+ // 20 lines of processing
76
+ }
77
+
78
+ // AFTER
79
+ for (const item of items) {
80
+ this.processItem(item);
81
+ }
82
+ \`\`\`
83
+
84
+ ### Pattern 2: Extract Conditional Blocks
85
+ \`\`\`typescript
86
+ // BEFORE
87
+ if (isAdmin(user)) {
88
+ // 15 lines of admin logic
89
+ }
90
+
91
+ // AFTER
92
+ if (isAdmin(user)) {
93
+ this.handleAdminUser(user);
94
+ }
95
+ \`\`\`
96
+
97
+ ### Pattern 3: Extract Data Transformations
98
+ \`\`\`typescript
99
+ // BEFORE
100
+ const result = {
101
+ // 10+ lines of object construction
102
+ };
103
+
104
+ // AFTER
105
+ const result = this.buildResultObject(data);
106
+ \`\`\`
107
+
108
+ ## If Refactoring Is Not Feasible
109
+
110
+ Sometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).
111
+
112
+ **Escape hatch**: Add an eslint-disable comment with justification:
113
+
114
+ \`\`\`typescript
115
+ // eslint-disable-next-line @webpieces/max-method-lines -- Complex state machine, splitting reduces clarity
116
+ async complexStateMachine(): Promise<void> {
117
+ // ... longer method with justification
118
+ }
119
+ \`\`\`
120
+
121
+ ## AI Agent Action Steps
122
+
123
+ 1. **READ** the method to understand its logical sections
124
+ 2. **IDENTIFY** logical units that can be extracted
125
+ 3. **EXTRACT** into well-named private methods
126
+ 4. **VERIFY** the main method now reads like a table of contents
127
+ 5. **IF NOT FEASIBLE**: Add eslint-disable with clear justification
128
+
129
+ ## Remember
130
+
131
+ - Every method you write today will be read many times tomorrow
132
+ - The best code explains itself through structure
133
+ - When in doubt, extract and name it
134
+ `;
135
+ /**
136
+ * Write the instructions documentation to tmp directory
137
+ */
138
+ function writeTmpInstructions(workspaceRoot) {
139
+ const tmpDir = path.join(workspaceRoot, TMP_DIR);
140
+ const mdPath = path.join(tmpDir, TMP_MD_FILE);
141
+ fs.mkdirSync(tmpDir, { recursive: true });
142
+ fs.writeFileSync(mdPath, METHODSIZE_DOC_CONTENT);
143
+ return mdPath;
144
+ }
145
+ /**
146
+ * Get changed TypeScript files between base and head
147
+ */
148
+ function getChangedTypeScriptFiles(workspaceRoot, base, head) {
149
+ try {
150
+ const output = (0, child_process_1.execSync)(`git diff --name-only ${base}...${head} -- '*.ts' '*.tsx'`, {
151
+ cwd: workspaceRoot,
152
+ encoding: 'utf-8',
153
+ });
154
+ return output
155
+ .trim()
156
+ .split('\n')
157
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ }
163
+ /**
164
+ * Get the diff content for a specific file
165
+ */
166
+ function getFileDiff(workspaceRoot, file, base, head) {
167
+ try {
168
+ return (0, child_process_1.execSync)(`git diff ${base}...${head} -- "${file}"`, {
169
+ cwd: workspaceRoot,
170
+ encoding: 'utf-8',
171
+ });
172
+ }
173
+ catch {
174
+ return '';
175
+ }
176
+ }
177
+ /**
178
+ * Parse diff to find newly added method signatures
179
+ */
180
+ function findNewMethodSignaturesInDiff(diffContent) {
181
+ const newMethods = new Set();
182
+ const lines = diffContent.split('\n');
183
+ // Patterns to match method definitions
184
+ const patterns = [
185
+ // async methodName( or methodName(
186
+ /^\+\s*(async\s+)?(\w+)\s*\(/,
187
+ // function methodName(
188
+ /^\+\s*(async\s+)?function\s+(\w+)\s*\(/,
189
+ // const/let methodName = (async)? (
190
+ /^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
191
+ // const/let methodName = (async)? function
192
+ /^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
193
+ ];
194
+ for (const line of lines) {
195
+ if (line.startsWith('+') && !line.startsWith('+++')) {
196
+ for (const pattern of patterns) {
197
+ const match = line.match(pattern);
198
+ if (match) {
199
+ // Extract method name from different capture groups
200
+ const methodName = match[2] || match[1];
201
+ if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {
202
+ newMethods.add(methodName);
203
+ }
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return newMethods;
210
+ }
211
+ /**
212
+ * Parse a TypeScript file and find methods with their line counts
213
+ */
214
+ function findMethodsInFile(filePath, workspaceRoot) {
215
+ const fullPath = path.join(workspaceRoot, filePath);
216
+ if (!fs.existsSync(fullPath))
217
+ return [];
218
+ const content = fs.readFileSync(fullPath, 'utf-8');
219
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
220
+ const methods = [];
221
+ function visit(node) {
222
+ let methodName;
223
+ let startLine;
224
+ let endLine;
225
+ if (ts.isMethodDeclaration(node) && node.name) {
226
+ methodName = node.name.getText(sourceFile);
227
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
228
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
229
+ startLine = start.line + 1;
230
+ endLine = end.line + 1;
231
+ }
232
+ else if (ts.isFunctionDeclaration(node) && node.name) {
233
+ methodName = node.name.getText(sourceFile);
234
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
235
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
236
+ startLine = start.line + 1;
237
+ endLine = end.line + 1;
238
+ }
239
+ else if (ts.isArrowFunction(node)) {
240
+ // Check if it's assigned to a variable
241
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
242
+ methodName = node.parent.name.getText(sourceFile);
243
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
244
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
245
+ startLine = start.line + 1;
246
+ endLine = end.line + 1;
247
+ }
248
+ }
249
+ if (methodName && startLine !== undefined && endLine !== undefined) {
250
+ methods.push({
251
+ name: methodName,
252
+ line: startLine,
253
+ lines: endLine - startLine + 1,
254
+ });
255
+ }
256
+ ts.forEachChild(node, visit);
257
+ }
258
+ visit(sourceFile);
259
+ return methods;
260
+ }
261
+ /**
262
+ * Find new methods that exceed the line limit
263
+ */
264
+ function findViolations(workspaceRoot, changedFiles, base, head, maxLines) {
265
+ const violations = [];
266
+ for (const file of changedFiles) {
267
+ // Get the diff to find which methods are NEW (not just modified)
268
+ const diff = getFileDiff(workspaceRoot, file, base, head);
269
+ const newMethodNames = findNewMethodSignaturesInDiff(diff);
270
+ if (newMethodNames.size === 0)
271
+ continue;
272
+ // Parse the current file to get method line counts
273
+ const methods = findMethodsInFile(file, workspaceRoot);
274
+ for (const method of methods) {
275
+ // Only check NEW methods
276
+ if (newMethodNames.has(method.name) && method.lines > maxLines) {
277
+ violations.push({
278
+ file,
279
+ methodName: method.name,
280
+ line: method.line,
281
+ lines: method.lines,
282
+ isNew: true,
283
+ });
284
+ }
285
+ }
286
+ }
287
+ return violations;
288
+ }
289
+ async function runExecutor(options, context) {
290
+ const workspaceRoot = context.root;
291
+ const maxLines = options.max ?? 30;
292
+ // Check if running in affected mode
293
+ const base = process.env['NX_BASE'];
294
+ const head = process.env['NX_HEAD'] || 'HEAD';
295
+ if (!base) {
296
+ console.log('\n⏭️ Skipping new method validation (not in affected mode)');
297
+ console.log(' To run: nx affected --target=validate-new-methods --base=origin/main');
298
+ console.log('');
299
+ return { success: true };
300
+ }
301
+ console.log('\n📏 Validating New Method Sizes\n');
302
+ console.log(` Base: ${base}`);
303
+ console.log(` Head: ${head}`);
304
+ console.log(` Max lines for new methods: ${maxLines}`);
305
+ console.log('');
306
+ try {
307
+ // Get changed TypeScript files
308
+ const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
309
+ if (changedFiles.length === 0) {
310
+ console.log('✅ No TypeScript files changed');
311
+ return { success: true };
312
+ }
313
+ console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);
314
+ // Find violations
315
+ const violations = findViolations(workspaceRoot, changedFiles, base, head, maxLines);
316
+ if (violations.length === 0) {
317
+ console.log('✅ All new methods are under ' + maxLines + ' lines');
318
+ return { success: true };
319
+ }
320
+ // Write instructions file
321
+ const mdPath = writeTmpInstructions(workspaceRoot);
322
+ // Report violations
323
+ console.error('');
324
+ console.error('❌ New methods exceed ' + maxLines + ' lines!');
325
+ console.error('');
326
+ console.error('📚 Methods should read like a "table of contents" - each method call');
327
+ console.error(' describes a larger piece of work. ~50% of the time, you can refactor');
328
+ console.error(' to stay under ' + maxLines + ' lines. If not feasible, use the escape hatch.');
329
+ console.error('');
330
+ for (const v of violations) {
331
+ console.error(` ❌ ${v.file}:${v.line}`);
332
+ console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);
333
+ console.error(` READ ${mdPath} to fix this error properly`);
334
+ console.error('');
335
+ }
336
+ console.error('💡 To fix:');
337
+ console.error(' 1. Refactor the method to read like a table of contents (preferred)');
338
+ console.error(' 2. OR add eslint-disable comment with justification:');
339
+ console.error(' // eslint-disable-next-line @webpieces/max-method-lines -- [reason]');
340
+ console.error('');
341
+ console.error(`⚠️ *** READ ${mdPath} for detailed guidance *** ⚠️`);
342
+ console.error('');
343
+ return { success: false };
344
+ }
345
+ catch (err) {
346
+ const error = err instanceof Error ? err : new Error(String(err));
347
+ console.error('❌ New method validation failed:', error.message);
348
+ return { success: false };
349
+ }
350
+ }
351
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/dev-config/architecture/executors/validate-new-methods/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate New Methods Executor\n *\n * Validates that newly added methods don't exceed a maximum line count.\n * Only runs when NX_BASE environment variable is set (affected mode).\n *\n * This validator encourages writing methods that read like a \"table of contents\"\n * where each method call describes a larger piece of work.\n *\n * Usage:\n * nx affected --target=validate-new-methods --base=origin/main\n *\n * Escape hatch: Add eslint-disable comment with justification\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\n\nexport interface ValidateNewMethodsOptions {\n max?: number;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\ninterface MethodViolation {\n file: string;\n methodName: string;\n line: number;\n lines: number;\n isNew: boolean;\n}\n\nconst TMP_DIR = 'tmp/webpieces';\nconst TMP_MD_FILE = 'webpieces.methodsize.md';\n\nconst METHODSIZE_DOC_CONTENT = `# Instructions: New Method Too Long\n\n## The \"Table of Contents\" Principle\n\nGood code reads like a book's table of contents:\n- Chapter titles (method names) tell you WHAT happens\n- Reading chapter titles gives you the full story\n- You can dive into chapters (implementations) for details\n\n## Why Limit New Methods to 20-30 Lines?\n\nMethods under 20-30 lines are:\n- Easy to review in a single screen\n- Simple to understand without scrolling\n- Quick for AI to analyze and suggest improvements\n- More testable in isolation\n- Self-documenting through well-named extracted methods\n\n**~50% of the time**, you can stay under 20-30 lines in new code by extracting\nlogical units into well-named methods. This makes code more readable for both\nAI and humans.\n\n## How to Refactor\n\nInstead of:\n\\`\\`\\`typescript\nasync processOrder(order: Order): Promise<Result> {\n // 50 lines of validation, transformation, saving, notifications...\n}\n\\`\\`\\`\n\nWrite:\n\\`\\`\\`typescript\nasync processOrder(order: Order): Promise<Result> {\n const validated = this.validateOrder(order);\n const transformed = this.applyBusinessRules(validated);\n const saved = await this.saveToDatabase(transformed);\n await this.notifyStakeholders(saved);\n return this.buildResult(saved);\n}\n\\`\\`\\`\n\nNow the main method is a \"table of contents\" - each line tells part of the story!\n\n## Patterns for Extraction\n\n### Pattern 1: Extract Loop Bodies\n\\`\\`\\`typescript\n// BEFORE\nfor (const item of items) {\n // 20 lines of processing\n}\n\n// AFTER\nfor (const item of items) {\n this.processItem(item);\n}\n\\`\\`\\`\n\n### Pattern 2: Extract Conditional Blocks\n\\`\\`\\`typescript\n// BEFORE\nif (isAdmin(user)) {\n // 15 lines of admin logic\n}\n\n// AFTER\nif (isAdmin(user)) {\n this.handleAdminUser(user);\n}\n\\`\\`\\`\n\n### Pattern 3: Extract Data Transformations\n\\`\\`\\`typescript\n// BEFORE\nconst result = {\n // 10+ lines of object construction\n};\n\n// AFTER\nconst result = this.buildResultObject(data);\n\\`\\`\\`\n\n## If Refactoring Is Not Feasible\n\nSometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).\n\n**Escape hatch**: Add an eslint-disable comment with justification:\n\n\\`\\`\\`typescript\n// eslint-disable-next-line @webpieces/max-method-lines -- Complex state machine, splitting reduces clarity\nasync complexStateMachine(): Promise<void> {\n // ... longer method with justification\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **READ** the method to understand its logical sections\n2. **IDENTIFY** logical units that can be extracted\n3. **EXTRACT** into well-named private methods\n4. **VERIFY** the main method now reads like a table of contents\n5. **IF NOT FEASIBLE**: Add eslint-disable with clear justification\n\n## Remember\n\n- Every method you write today will be read many times tomorrow\n- The best code explains itself through structure\n- When in doubt, extract and name it\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, METHODSIZE_DOC_CONTENT);\n\n return mdPath;\n}\n\n/**\n * Get changed TypeScript files between base and head\n */\nfunction getChangedTypeScriptFiles(workspaceRoot: string, base: string, head: string): string[] {\n try {\n const output = execSync(`git diff --name-only ${base}...${head} -- '*.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 * Get the diff content for a specific file\n */\nfunction getFileDiff(workspaceRoot: string, file: string, base: string, head: string): string {\n try {\n return execSync(`git diff ${base}...${head} -- \"${file}\"`, {\n cwd: workspaceRoot,\n encoding: 'utf-8',\n });\n } catch {\n return '';\n }\n}\n\n/**\n * Parse diff to find newly added method signatures\n */\nfunction findNewMethodSignaturesInDiff(diffContent: string): Set<string> {\n const newMethods = new Set<string>();\n const lines = diffContent.split('\\n');\n\n // Patterns to match method definitions\n const patterns = [\n // async methodName( or methodName(\n /^\\+\\s*(async\\s+)?(\\w+)\\s*\\(/,\n // function methodName(\n /^\\+\\s*(async\\s+)?function\\s+(\\w+)\\s*\\(/,\n // const/let methodName = (async)? (\n /^\\+\\s*(?:const|let)\\s+(\\w+)\\s*=\\s*(?:async\\s*)?\\(/,\n // const/let methodName = (async)? function\n /^\\+\\s*(?:const|let)\\s+(\\w+)\\s*=\\s*(?:async\\s+)?function/,\n ];\n\n for (const line of lines) {\n if (line.startsWith('+') && !line.startsWith('+++')) {\n for (const pattern of patterns) {\n const match = line.match(pattern);\n if (match) {\n // Extract method name from different capture groups\n const methodName = match[2] || match[1];\n if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {\n newMethods.add(methodName);\n }\n break;\n }\n }\n }\n }\n\n return newMethods;\n}\n\n/**\n * Parse a TypeScript file and find methods with their line counts\n */\nfunction findMethodsInFile(\n filePath: string,\n workspaceRoot: string\n): Array<{ name: string; line: number; lines: number }> {\n const fullPath = path.join(workspaceRoot, filePath);\n if (!fs.existsSync(fullPath)) return [];\n\n const content = fs.readFileSync(fullPath, 'utf-8');\n const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);\n\n const methods: Array<{ name: string; line: number; lines: number }> = [];\n\n function visit(node: ts.Node): void {\n let methodName: string | undefined;\n let startLine: number | undefined;\n let endLine: number | undefined;\n\n if (ts.isMethodDeclaration(node) && node.name) {\n methodName = node.name.getText(sourceFile);\n const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());\n startLine = start.line + 1;\n endLine = end.line + 1;\n } else if (ts.isFunctionDeclaration(node) && node.name) {\n methodName = node.name.getText(sourceFile);\n const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());\n startLine = start.line + 1;\n endLine = end.line + 1;\n } else if (ts.isArrowFunction(node)) {\n // Check if it's assigned to a variable\n if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {\n methodName = node.parent.name.getText(sourceFile);\n const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());\n startLine = start.line + 1;\n endLine = end.line + 1;\n }\n }\n\n if (methodName && startLine !== undefined && endLine !== undefined) {\n methods.push({\n name: methodName,\n line: startLine,\n lines: endLine - startLine + 1,\n });\n }\n\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return methods;\n}\n\n/**\n * Find new methods that exceed the line limit\n */\nfunction findViolations(\n workspaceRoot: string,\n changedFiles: string[],\n base: string,\n head: string,\n maxLines: number\n): MethodViolation[] {\n const violations: MethodViolation[] = [];\n\n for (const file of changedFiles) {\n // Get the diff to find which methods are NEW (not just modified)\n const diff = getFileDiff(workspaceRoot, file, base, head);\n const newMethodNames = findNewMethodSignaturesInDiff(diff);\n\n if (newMethodNames.size === 0) continue;\n\n // Parse the current file to get method line counts\n const methods = findMethodsInFile(file, workspaceRoot);\n\n for (const method of methods) {\n // Only check NEW methods\n if (newMethodNames.has(method.name) && method.lines > maxLines) {\n violations.push({\n file,\n methodName: method.name,\n line: method.line,\n lines: method.lines,\n isNew: true,\n });\n }\n }\n }\n\n return violations;\n}\n\nexport default async function runExecutor(\n options: ValidateNewMethodsOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n const maxLines = options.max ?? 30;\n\n // Check if running in affected mode\n const base = process.env['NX_BASE'];\n const head = process.env['NX_HEAD'] || 'HEAD';\n\n if (!base) {\n console.log('\\n⏭️ Skipping new method validation (not in affected mode)');\n console.log(' To run: nx affected --target=validate-new-methods --base=origin/main');\n console.log('');\n return { success: true };\n }\n\n console.log('\\n📏 Validating New Method Sizes\\n');\n console.log(` Base: ${base}`);\n console.log(` Head: ${head}`);\n console.log(` Max lines for new methods: ${maxLines}`);\n console.log('');\n\n try {\n // Get changed TypeScript files\n const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);\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 // Find violations\n const violations = findViolations(workspaceRoot, changedFiles, base, head, maxLines);\n\n if (violations.length === 0) {\n console.log('✅ All new methods are under ' + maxLines + ' lines');\n return { success: true };\n }\n\n // Write instructions file\n const mdPath = writeTmpInstructions(workspaceRoot);\n\n // Report violations\n console.error('');\n console.error('❌ New methods exceed ' + maxLines + ' lines!');\n console.error('');\n console.error('📚 Methods should read like a \"table of contents\" - each method call');\n console.error(' describes a larger piece of work. ~50% of the time, you can refactor');\n console.error(' to stay under ' + maxLines + ' lines. If not feasible, use the escape hatch.');\n console.error('');\n\n for (const v of violations) {\n console.error(` ❌ ${v.file}:${v.line}`);\n console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);\n console.error(` READ ${mdPath} to fix this error properly`);\n console.error('');\n }\n\n console.error('💡 To fix:');\n console.error(' 1. Refactor the method to read like a table of contents (preferred)');\n console.error(' 2. OR add eslint-disable comment with justification:');\n console.error(' // eslint-disable-next-line @webpieces/max-method-lines -- [reason]');\n console.error('');\n console.error(`⚠️ *** READ ${mdPath} for detailed guidance *** ⚠️`);\n console.error('');\n\n return { success: false };\n } catch (err: unknown) {\n const error = err instanceof Error ? err : new Error(String(err));\n console.error('❌ New method validation failed:', error.message);\n return { success: false };\n }\n}\n"]}
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Validate New Methods Executor
3
+ *
4
+ * Validates that newly added methods don't exceed a maximum line count.
5
+ * Only runs when NX_BASE environment variable is set (affected mode).
6
+ *
7
+ * This validator encourages writing methods that read like a "table of contents"
8
+ * where each method call describes a larger piece of work.
9
+ *
10
+ * Usage:
11
+ * nx affected --target=validate-new-methods --base=origin/main
12
+ *
13
+ * Escape hatch: Add eslint-disable comment with justification
14
+ */
15
+
16
+ import type { ExecutorContext } from '@nx/devkit';
17
+ import { execSync } from 'child_process';
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import * as ts from 'typescript';
21
+
22
+ export interface ValidateNewMethodsOptions {
23
+ max?: number;
24
+ }
25
+
26
+ export interface ExecutorResult {
27
+ success: boolean;
28
+ }
29
+
30
+ interface MethodViolation {
31
+ file: string;
32
+ methodName: string;
33
+ line: number;
34
+ lines: number;
35
+ isNew: boolean;
36
+ }
37
+
38
+ const TMP_DIR = 'tmp/webpieces';
39
+ const TMP_MD_FILE = 'webpieces.methodsize.md';
40
+
41
+ const METHODSIZE_DOC_CONTENT = `# Instructions: New Method Too Long
42
+
43
+ ## The "Table of Contents" Principle
44
+
45
+ Good code reads like a book's table of contents:
46
+ - Chapter titles (method names) tell you WHAT happens
47
+ - Reading chapter titles gives you the full story
48
+ - You can dive into chapters (implementations) for details
49
+
50
+ ## Why Limit New Methods to 20-30 Lines?
51
+
52
+ Methods under 20-30 lines are:
53
+ - Easy to review in a single screen
54
+ - Simple to understand without scrolling
55
+ - Quick for AI to analyze and suggest improvements
56
+ - More testable in isolation
57
+ - Self-documenting through well-named extracted methods
58
+
59
+ **~50% of the time**, you can stay under 20-30 lines in new code by extracting
60
+ logical units into well-named methods. This makes code more readable for both
61
+ AI and humans.
62
+
63
+ ## How to Refactor
64
+
65
+ Instead of:
66
+ \`\`\`typescript
67
+ async processOrder(order: Order): Promise<Result> {
68
+ // 50 lines of validation, transformation, saving, notifications...
69
+ }
70
+ \`\`\`
71
+
72
+ Write:
73
+ \`\`\`typescript
74
+ async processOrder(order: Order): Promise<Result> {
75
+ const validated = this.validateOrder(order);
76
+ const transformed = this.applyBusinessRules(validated);
77
+ const saved = await this.saveToDatabase(transformed);
78
+ await this.notifyStakeholders(saved);
79
+ return this.buildResult(saved);
80
+ }
81
+ \`\`\`
82
+
83
+ Now the main method is a "table of contents" - each line tells part of the story!
84
+
85
+ ## Patterns for Extraction
86
+
87
+ ### Pattern 1: Extract Loop Bodies
88
+ \`\`\`typescript
89
+ // BEFORE
90
+ for (const item of items) {
91
+ // 20 lines of processing
92
+ }
93
+
94
+ // AFTER
95
+ for (const item of items) {
96
+ this.processItem(item);
97
+ }
98
+ \`\`\`
99
+
100
+ ### Pattern 2: Extract Conditional Blocks
101
+ \`\`\`typescript
102
+ // BEFORE
103
+ if (isAdmin(user)) {
104
+ // 15 lines of admin logic
105
+ }
106
+
107
+ // AFTER
108
+ if (isAdmin(user)) {
109
+ this.handleAdminUser(user);
110
+ }
111
+ \`\`\`
112
+
113
+ ### Pattern 3: Extract Data Transformations
114
+ \`\`\`typescript
115
+ // BEFORE
116
+ const result = {
117
+ // 10+ lines of object construction
118
+ };
119
+
120
+ // AFTER
121
+ const result = this.buildResultObject(data);
122
+ \`\`\`
123
+
124
+ ## If Refactoring Is Not Feasible
125
+
126
+ Sometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).
127
+
128
+ **Escape hatch**: Add an eslint-disable comment with justification:
129
+
130
+ \`\`\`typescript
131
+ // eslint-disable-next-line @webpieces/max-method-lines -- Complex state machine, splitting reduces clarity
132
+ async complexStateMachine(): Promise<void> {
133
+ // ... longer method with justification
134
+ }
135
+ \`\`\`
136
+
137
+ ## AI Agent Action Steps
138
+
139
+ 1. **READ** the method to understand its logical sections
140
+ 2. **IDENTIFY** logical units that can be extracted
141
+ 3. **EXTRACT** into well-named private methods
142
+ 4. **VERIFY** the main method now reads like a table of contents
143
+ 5. **IF NOT FEASIBLE**: Add eslint-disable with clear justification
144
+
145
+ ## Remember
146
+
147
+ - Every method you write today will be read many times tomorrow
148
+ - The best code explains itself through structure
149
+ - When in doubt, extract and name it
150
+ `;
151
+
152
+ /**
153
+ * Write the instructions documentation to tmp directory
154
+ */
155
+ function writeTmpInstructions(workspaceRoot: string): string {
156
+ const tmpDir = path.join(workspaceRoot, TMP_DIR);
157
+ const mdPath = path.join(tmpDir, TMP_MD_FILE);
158
+
159
+ fs.mkdirSync(tmpDir, { recursive: true });
160
+ fs.writeFileSync(mdPath, METHODSIZE_DOC_CONTENT);
161
+
162
+ return mdPath;
163
+ }
164
+
165
+ /**
166
+ * Get changed TypeScript files between base and head
167
+ */
168
+ function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head: string): string[] {
169
+ try {
170
+ const output = execSync(`git diff --name-only ${base}...${head} -- '*.ts' '*.tsx'`, {
171
+ cwd: workspaceRoot,
172
+ encoding: 'utf-8',
173
+ });
174
+ return output
175
+ .trim()
176
+ .split('\n')
177
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
178
+ } catch {
179
+ return [];
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get the diff content for a specific file
185
+ */
186
+ function getFileDiff(workspaceRoot: string, file: string, base: string, head: string): string {
187
+ try {
188
+ return execSync(`git diff ${base}...${head} -- "${file}"`, {
189
+ cwd: workspaceRoot,
190
+ encoding: 'utf-8',
191
+ });
192
+ } catch {
193
+ return '';
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Parse diff to find newly added method signatures
199
+ */
200
+ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
201
+ const newMethods = new Set<string>();
202
+ const lines = diffContent.split('\n');
203
+
204
+ // Patterns to match method definitions
205
+ const patterns = [
206
+ // async methodName( or methodName(
207
+ /^\+\s*(async\s+)?(\w+)\s*\(/,
208
+ // function methodName(
209
+ /^\+\s*(async\s+)?function\s+(\w+)\s*\(/,
210
+ // const/let methodName = (async)? (
211
+ /^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
212
+ // const/let methodName = (async)? function
213
+ /^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
214
+ ];
215
+
216
+ for (const line of lines) {
217
+ if (line.startsWith('+') && !line.startsWith('+++')) {
218
+ for (const pattern of patterns) {
219
+ const match = line.match(pattern);
220
+ if (match) {
221
+ // Extract method name from different capture groups
222
+ const methodName = match[2] || match[1];
223
+ if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {
224
+ newMethods.add(methodName);
225
+ }
226
+ break;
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ return newMethods;
233
+ }
234
+
235
+ /**
236
+ * Parse a TypeScript file and find methods with their line counts
237
+ */
238
+ function findMethodsInFile(
239
+ filePath: string,
240
+ workspaceRoot: string
241
+ ): Array<{ name: string; line: number; lines: number }> {
242
+ const fullPath = path.join(workspaceRoot, filePath);
243
+ if (!fs.existsSync(fullPath)) return [];
244
+
245
+ const content = fs.readFileSync(fullPath, 'utf-8');
246
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
247
+
248
+ const methods: Array<{ name: string; line: number; lines: number }> = [];
249
+
250
+ function visit(node: ts.Node): void {
251
+ let methodName: string | undefined;
252
+ let startLine: number | undefined;
253
+ let endLine: number | undefined;
254
+
255
+ if (ts.isMethodDeclaration(node) && node.name) {
256
+ methodName = node.name.getText(sourceFile);
257
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
258
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
259
+ startLine = start.line + 1;
260
+ endLine = end.line + 1;
261
+ } else if (ts.isFunctionDeclaration(node) && node.name) {
262
+ methodName = node.name.getText(sourceFile);
263
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
264
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
265
+ startLine = start.line + 1;
266
+ endLine = end.line + 1;
267
+ } else if (ts.isArrowFunction(node)) {
268
+ // Check if it's assigned to a variable
269
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
270
+ methodName = node.parent.name.getText(sourceFile);
271
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
272
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
273
+ startLine = start.line + 1;
274
+ endLine = end.line + 1;
275
+ }
276
+ }
277
+
278
+ if (methodName && startLine !== undefined && endLine !== undefined) {
279
+ methods.push({
280
+ name: methodName,
281
+ line: startLine,
282
+ lines: endLine - startLine + 1,
283
+ });
284
+ }
285
+
286
+ ts.forEachChild(node, visit);
287
+ }
288
+
289
+ visit(sourceFile);
290
+ return methods;
291
+ }
292
+
293
+ /**
294
+ * Find new methods that exceed the line limit
295
+ */
296
+ function findViolations(
297
+ workspaceRoot: string,
298
+ changedFiles: string[],
299
+ base: string,
300
+ head: string,
301
+ maxLines: number
302
+ ): MethodViolation[] {
303
+ const violations: MethodViolation[] = [];
304
+
305
+ for (const file of changedFiles) {
306
+ // Get the diff to find which methods are NEW (not just modified)
307
+ const diff = getFileDiff(workspaceRoot, file, base, head);
308
+ const newMethodNames = findNewMethodSignaturesInDiff(diff);
309
+
310
+ if (newMethodNames.size === 0) continue;
311
+
312
+ // Parse the current file to get method line counts
313
+ const methods = findMethodsInFile(file, workspaceRoot);
314
+
315
+ for (const method of methods) {
316
+ // Only check NEW methods
317
+ if (newMethodNames.has(method.name) && method.lines > maxLines) {
318
+ violations.push({
319
+ file,
320
+ methodName: method.name,
321
+ line: method.line,
322
+ lines: method.lines,
323
+ isNew: true,
324
+ });
325
+ }
326
+ }
327
+ }
328
+
329
+ return violations;
330
+ }
331
+
332
+ export default async function runExecutor(
333
+ options: ValidateNewMethodsOptions,
334
+ context: ExecutorContext
335
+ ): Promise<ExecutorResult> {
336
+ const workspaceRoot = context.root;
337
+ const maxLines = options.max ?? 30;
338
+
339
+ // Check if running in affected mode
340
+ const base = process.env['NX_BASE'];
341
+ const head = process.env['NX_HEAD'] || 'HEAD';
342
+
343
+ if (!base) {
344
+ console.log('\n⏭️ Skipping new method validation (not in affected mode)');
345
+ console.log(' To run: nx affected --target=validate-new-methods --base=origin/main');
346
+ console.log('');
347
+ return { success: true };
348
+ }
349
+
350
+ console.log('\n📏 Validating New Method Sizes\n');
351
+ console.log(` Base: ${base}`);
352
+ console.log(` Head: ${head}`);
353
+ console.log(` Max lines for new methods: ${maxLines}`);
354
+ console.log('');
355
+
356
+ try {
357
+ // Get changed TypeScript files
358
+ const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
359
+
360
+ if (changedFiles.length === 0) {
361
+ console.log('✅ No TypeScript files changed');
362
+ return { success: true };
363
+ }
364
+
365
+ console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);
366
+
367
+ // Find violations
368
+ const violations = findViolations(workspaceRoot, changedFiles, base, head, maxLines);
369
+
370
+ if (violations.length === 0) {
371
+ console.log('✅ All new methods are under ' + maxLines + ' lines');
372
+ return { success: true };
373
+ }
374
+
375
+ // Write instructions file
376
+ const mdPath = writeTmpInstructions(workspaceRoot);
377
+
378
+ // Report violations
379
+ console.error('');
380
+ console.error('❌ New methods exceed ' + maxLines + ' lines!');
381
+ console.error('');
382
+ console.error('📚 Methods should read like a "table of contents" - each method call');
383
+ console.error(' describes a larger piece of work. ~50% of the time, you can refactor');
384
+ console.error(' to stay under ' + maxLines + ' lines. If not feasible, use the escape hatch.');
385
+ console.error('');
386
+
387
+ for (const v of violations) {
388
+ console.error(` ❌ ${v.file}:${v.line}`);
389
+ console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);
390
+ console.error(` READ ${mdPath} to fix this error properly`);
391
+ console.error('');
392
+ }
393
+
394
+ console.error('💡 To fix:');
395
+ console.error(' 1. Refactor the method to read like a table of contents (preferred)');
396
+ console.error(' 2. OR add eslint-disable comment with justification:');
397
+ console.error(' // eslint-disable-next-line @webpieces/max-method-lines -- [reason]');
398
+ console.error('');
399
+ console.error(`⚠️ *** READ ${mdPath} for detailed guidance *** ⚠️`);
400
+ console.error('');
401
+
402
+ return { success: false };
403
+ } catch (err: unknown) {
404
+ const error = err instanceof Error ? err : new Error(String(err));
405
+ console.error('❌ New method validation failed:', error.message);
406
+ return { success: false };
407
+ }
408
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "title": "Validate New Methods Executor",
4
+ "description": "Validates that newly added methods don't exceed a maximum line count. Only runs in affected mode (when NX_BASE is set).",
5
+ "type": "object",
6
+ "properties": {
7
+ "max": {
8
+ "type": "number",
9
+ "description": "Maximum number of lines allowed for new methods",
10
+ "default": 30
11
+ }
12
+ },
13
+ "required": []
14
+ }
package/executors.json CHANGED
@@ -30,6 +30,11 @@
30
30
  "schema": "./architecture/executors/validate-packagejson/schema.json",
31
31
  "description": "Validate package.json dependencies match project.json build dependencies"
32
32
  },
33
+ "validate-new-methods": {
34
+ "implementation": "./architecture/executors/validate-new-methods/executor",
35
+ "schema": "./architecture/executors/validate-new-methods/schema.json",
36
+ "description": "Validate new methods don't exceed max line count (only runs in affected mode)"
37
+ },
33
38
  "help": {
34
39
  "implementation": "./executors/help/executor",
35
40
  "schema": "./executors/help/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/dev-config",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "description": "Development configuration, scripts, and patterns for WebPieces projects",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/plugin.js CHANGED
@@ -35,6 +35,7 @@ const DEFAULT_OPTIONS = {
35
35
  noSkipLevelDeps: true,
36
36
  architectureUnchanged: true,
37
37
  validatePackageJson: true,
38
+ validateNewMethods: true,
38
39
  },
39
40
  features: {
40
41
  generate: true,
@@ -161,6 +162,9 @@ function createWorkspaceTargetsWithoutPrefix(opts) {
161
162
  if (opts.workspace.validations.validatePackageJson) {
162
163
  targets['validate-packagejson'] = createValidatePackageJsonTarget();
163
164
  }
165
+ if (opts.workspace.validations.validateNewMethods) {
166
+ targets['validate-new-methods'] = createValidateNewMethodsTarget();
167
+ }
164
168
  // Add validate-complete target that runs all validations
165
169
  const validationTargets = [];
166
170
  if (opts.workspace.validations.noCycles) {
@@ -175,6 +179,9 @@ function createWorkspaceTargetsWithoutPrefix(opts) {
175
179
  if (opts.workspace.validations.validatePackageJson) {
176
180
  validationTargets.push('validate-packagejson');
177
181
  }
182
+ if (opts.workspace.validations.validateNewMethods) {
183
+ validationTargets.push('validate-new-methods');
184
+ }
178
185
  if (validationTargets.length > 0) {
179
186
  targets['validate-complete'] = createValidateCompleteTarget(validationTargets);
180
187
  }
@@ -208,6 +215,9 @@ function createWorkspaceTargets(opts) {
208
215
  if (opts.workspace.validations.validatePackageJson) {
209
216
  targets[`${prefix}validate-packagejson`] = createValidatePackageJsonTarget();
210
217
  }
218
+ if (opts.workspace.validations.validateNewMethods) {
219
+ targets[`${prefix}validate-new-methods`] = createValidateNewMethodsTarget();
220
+ }
211
221
  return targets;
212
222
  }
213
223
  function createGenerateTarget(graphPath) {
@@ -290,6 +300,18 @@ function createValidatePackageJsonTarget() {
290
300
  },
291
301
  };
292
302
  }
303
+ function createValidateNewMethodsTarget() {
304
+ return {
305
+ executor: '@webpieces/dev-config:validate-new-methods',
306
+ cache: false, // Don't cache - depends on git state
307
+ inputs: ['default'],
308
+ options: { max: 30 },
309
+ metadata: {
310
+ technologies: ['nx'],
311
+ description: 'Validate new methods do not exceed max line count (only runs in affected mode)',
312
+ },
313
+ };
314
+ }
293
315
  function createValidateCompleteTarget(validationTargets) {
294
316
  return {
295
317
  executor: 'nx:noop',