@webpieces/dev-config 0.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +306 -0
  2. package/bin/set-version.sh +86 -0
  3. package/bin/setup-claude-patterns.sh +51 -0
  4. package/bin/start.sh +107 -0
  5. package/bin/stop.sh +65 -0
  6. package/bin/use-local-webpieces.sh +89 -0
  7. package/bin/use-published-webpieces.sh +33 -0
  8. package/config/eslint/base.mjs +91 -0
  9. package/config/typescript/tsconfig.base.json +25 -0
  10. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +360 -0
  11. package/eslint-plugin/__tests__/max-file-lines.test.ts +195 -0
  12. package/eslint-plugin/__tests__/max-method-lines.test.ts +246 -0
  13. package/eslint-plugin/index.d.ts +14 -0
  14. package/eslint-plugin/index.js +19 -0
  15. package/eslint-plugin/index.js.map +1 -0
  16. package/eslint-plugin/index.ts +18 -0
  17. package/eslint-plugin/rules/catch-error-pattern.d.ts +11 -0
  18. package/eslint-plugin/rules/catch-error-pattern.js +196 -0
  19. package/eslint-plugin/rules/catch-error-pattern.js.map +1 -0
  20. package/eslint-plugin/rules/catch-error-pattern.ts +281 -0
  21. package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
  22. package/eslint-plugin/rules/max-file-lines.js +257 -0
  23. package/eslint-plugin/rules/max-file-lines.js.map +1 -0
  24. package/eslint-plugin/rules/max-file-lines.ts +272 -0
  25. package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
  26. package/eslint-plugin/rules/max-method-lines.js +257 -0
  27. package/eslint-plugin/rules/max-method-lines.js.map +1 -0
  28. package/eslint-plugin/rules/max-method-lines.ts +304 -0
  29. package/package.json +54 -0
  30. package/patterns/CLAUDE.md +293 -0
  31. package/patterns/claude.patterns.md +798 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * ESLint rule to enforce standardized catch block error handling patterns
3
+ *
4
+ * Enforces three approved patterns:
5
+ * 1. Standard: catch (err: any) { const error = toError(err); }
6
+ * 2. Ignored: catch (err: any) { //const error = toError(err); }
7
+ * 3. Nested: catch (err2: any) { const error2 = toError(err2); }
8
+ */
9
+
10
+ import type { Rule } from 'eslint';
11
+
12
+ // Using any for ESTree nodes to avoid complex type gymnastics
13
+ // ESLint rules work with dynamic AST nodes anyway
14
+ interface CatchClauseNode {
15
+ type: 'CatchClause';
16
+ param?: IdentifierNode | null;
17
+ body: BlockStatementNode;
18
+ [key: string]: any;
19
+ }
20
+
21
+ interface IdentifierNode {
22
+ type: 'Identifier';
23
+ name: string;
24
+ typeAnnotation?: TypeAnnotationNode;
25
+ [key: string]: any;
26
+ }
27
+
28
+ interface TypeAnnotationNode {
29
+ typeAnnotation?: {
30
+ type: string;
31
+ };
32
+ }
33
+
34
+ interface BlockStatementNode {
35
+ type: 'BlockStatement';
36
+ body: any[];
37
+ range: [number, number];
38
+ [key: string]: any;
39
+ }
40
+
41
+ interface VariableDeclarationNode {
42
+ type: 'VariableDeclaration';
43
+ declarations: VariableDeclaratorNode[];
44
+ [key: string]: any;
45
+ }
46
+
47
+ interface VariableDeclaratorNode {
48
+ type: 'VariableDeclarator';
49
+ id: IdentifierNode;
50
+ init?: CallExpressionNode | null;
51
+ [key: string]: any;
52
+ }
53
+
54
+ interface CallExpressionNode {
55
+ type: 'CallExpression';
56
+ callee: IdentifierNode;
57
+ arguments: any[];
58
+ [key: string]: any;
59
+ }
60
+
61
+ const rule: Rule.RuleModule = {
62
+ meta: {
63
+ type: 'problem',
64
+ docs: {
65
+ description: 'Enforce standardized catch block error handling patterns',
66
+ category: 'Best Practices',
67
+ recommended: true,
68
+ url: 'https://github.com/deanhiller/webpieces-ts/blob/main/claude.patterns.md#error-handling-pattern',
69
+ },
70
+ messages: {
71
+ missingToError:
72
+ 'Catch block must call toError({{param}}) as first statement or comment it out to explicitly ignore errors',
73
+ wrongVariableName: 'Error variable must be named "{{expected}}", got "{{actual}}"',
74
+ missingTypeAnnotation: 'Catch parameter must be typed as "any": catch ({{param}}: any)',
75
+ wrongParameterName:
76
+ 'Catch parameter must be named "err" (or "err2", "err3" for nested catches), got "{{actual}}"',
77
+ toErrorNotFirst: 'toError({{param}}) must be the first statement in the catch block',
78
+ },
79
+ fixable: undefined,
80
+ schema: [],
81
+ },
82
+
83
+ create(context: Rule.RuleContext): Rule.RuleListener {
84
+ // Track nesting depth for err, err2, err3, etc.
85
+ const catchStack: CatchClauseNode[] = [];
86
+
87
+ return {
88
+ CatchClause(node: any): void {
89
+ const catchNode = node as CatchClauseNode;
90
+
91
+ // Calculate depth (1-based: first catch is depth 1)
92
+ const depth = catchStack.length + 1;
93
+ catchStack.push(catchNode);
94
+
95
+ // Build expected names based on depth
96
+ const suffix = depth === 1 ? '' : String(depth);
97
+ const expectedParamName = 'err' + suffix;
98
+ const expectedVarName = 'error' + suffix;
99
+
100
+ // Get the catch parameter
101
+ const param = catchNode.param;
102
+ if (!param) {
103
+ // No parameter - unusual but technically valid (though not our pattern)
104
+ context.report({
105
+ node: catchNode,
106
+ messageId: 'missingTypeAnnotation',
107
+ data: { param: expectedParamName },
108
+ });
109
+ return;
110
+ }
111
+
112
+ // Track the actual parameter name for validation (may differ from expected)
113
+ const actualParamName =
114
+ param.type === 'Identifier' ? param.name : expectedParamName;
115
+
116
+ // RULE 1: Parameter must be named correctly (err, err2, err3, etc.)
117
+ if (param.type === 'Identifier' && param.name !== expectedParamName) {
118
+ context.report({
119
+ node: param,
120
+ messageId: 'wrongParameterName',
121
+ data: {
122
+ actual: param.name,
123
+ },
124
+ });
125
+ }
126
+
127
+ // RULE 2: Must have type annotation ": any"
128
+ if (
129
+ !param.typeAnnotation ||
130
+ !param.typeAnnotation.typeAnnotation ||
131
+ param.typeAnnotation.typeAnnotation.type !== 'TSAnyKeyword'
132
+ ) {
133
+ context.report({
134
+ node: param,
135
+ messageId: 'missingTypeAnnotation',
136
+ data: {
137
+ param: param.name || expectedParamName,
138
+ },
139
+ });
140
+ }
141
+
142
+ // RULE 3: Check first statement in catch block
143
+ const body = catchNode.body.body;
144
+ const sourceCode = context.sourceCode || context.getSourceCode();
145
+
146
+ // IMPORTANT: Check for commented ignore pattern FIRST (before checking if body is empty)
147
+ // This allows Pattern 2 (empty catch with only comment) to be valid
148
+ // Look for: //const error = toError(err);
149
+ const catchBlockStart = catchNode.body.range![0];
150
+ const catchBlockEnd = catchNode.body.range![1];
151
+ const catchBlockText = sourceCode.text.substring(catchBlockStart, catchBlockEnd);
152
+
153
+ const ignorePattern = new RegExp(
154
+ `//\\s*const\\s+${expectedVarName}\\s*=\\s*toError\\(${actualParamName}\\)`,
155
+ );
156
+
157
+ if (ignorePattern.test(catchBlockText)) {
158
+ // Pattern 2: Explicitly ignored - valid!
159
+ return;
160
+ }
161
+
162
+ // Now check if body is empty (after checking for commented pattern)
163
+ if (body.length === 0) {
164
+ // Empty catch block without comment - not allowed
165
+ context.report({
166
+ node: catchNode.body,
167
+ messageId: 'missingToError',
168
+ data: {
169
+ param: expectedParamName,
170
+ },
171
+ });
172
+ return;
173
+ }
174
+
175
+ const firstStatement = body[0];
176
+
177
+ // Check if first statement is: const error = toError(err)
178
+ if (firstStatement.type !== 'VariableDeclaration') {
179
+ context.report({
180
+ node: firstStatement,
181
+ messageId: 'missingToError',
182
+ data: {
183
+ param: expectedParamName,
184
+ },
185
+ });
186
+ return;
187
+ }
188
+
189
+ const varDecl = firstStatement as VariableDeclarationNode;
190
+ const declaration = varDecl.declarations[0];
191
+ if (!declaration) {
192
+ context.report({
193
+ node: firstStatement,
194
+ messageId: 'missingToError',
195
+ data: {
196
+ param: expectedParamName,
197
+ },
198
+ });
199
+ return;
200
+ }
201
+
202
+ // Check variable name
203
+ if (
204
+ declaration.id.type !== 'Identifier' ||
205
+ declaration.id.name !== expectedVarName
206
+ ) {
207
+ context.report({
208
+ node: declaration.id,
209
+ messageId: 'wrongVariableName',
210
+ data: {
211
+ expected: expectedVarName,
212
+ actual: declaration.id.name || 'unknown',
213
+ },
214
+ });
215
+ return;
216
+ }
217
+
218
+ // Check initialization: toError(err)
219
+ if (!declaration.init) {
220
+ context.report({
221
+ node: declaration,
222
+ messageId: 'missingToError',
223
+ data: {
224
+ param: expectedParamName,
225
+ },
226
+ });
227
+ return;
228
+ }
229
+
230
+ if (declaration.init.type !== 'CallExpression') {
231
+ context.report({
232
+ node: declaration.init,
233
+ messageId: 'missingToError',
234
+ data: {
235
+ param: expectedParamName,
236
+ },
237
+ });
238
+ return;
239
+ }
240
+
241
+ const callExpr = declaration.init as CallExpressionNode;
242
+ const callee = callExpr.callee;
243
+ if (callee.type !== 'Identifier' || callee.name !== 'toError') {
244
+ context.report({
245
+ node: callee,
246
+ messageId: 'missingToError',
247
+ data: {
248
+ param: expectedParamName,
249
+ },
250
+ });
251
+ return;
252
+ }
253
+
254
+ // Check argument: must be the catch parameter (use actual param name)
255
+ const args = callExpr.arguments;
256
+ if (
257
+ args.length !== 1 ||
258
+ args[0].type !== 'Identifier' ||
259
+ (args[0] as IdentifierNode).name !== actualParamName
260
+ ) {
261
+ context.report({
262
+ node: callExpr,
263
+ messageId: 'missingToError',
264
+ data: {
265
+ param: actualParamName,
266
+ },
267
+ });
268
+ return;
269
+ }
270
+
271
+ // All checks passed! ✅
272
+ },
273
+
274
+ 'CatchClause:exit'(): void {
275
+ catchStack.pop();
276
+ },
277
+ };
278
+ },
279
+ };
280
+
281
+ export = rule;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ESLint rule to enforce maximum file length
3
+ *
4
+ * Enforces a configurable maximum line count for files.
5
+ * Default: 700 lines
6
+ *
7
+ * Configuration:
8
+ * '@webpieces/max-file-lines': ['error', { max: 700 }]
9
+ */
10
+ import type { Rule } from 'eslint';
11
+ declare const rule: Rule.RuleModule;
12
+ export = rule;
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint rule to enforce maximum file length
4
+ *
5
+ * Enforces a configurable maximum line count for files.
6
+ * Default: 700 lines
7
+ *
8
+ * Configuration:
9
+ * '@webpieces/max-file-lines': ['error', { max: 700 }]
10
+ */
11
+ const tslib_1 = require("tslib");
12
+ const fs = tslib_1.__importStar(require("fs"));
13
+ const path = tslib_1.__importStar(require("path"));
14
+ const FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
15
+
16
+ **READ THIS FILE to fix files that are too long**
17
+
18
+ ## Core Principle
19
+ Files should contain a SINGLE COHESIVE UNIT.
20
+ - One class per file (Java convention)
21
+ - If class is too large, extract child responsibilities
22
+ - Use dependency injection to compose functionality
23
+
24
+ ## Command: Reduce File Size
25
+
26
+ ### Step 1: Check for Multiple Classes
27
+ If the file contains multiple classes, **SEPARATE each class into its own file**.
28
+
29
+ \`\`\`typescript
30
+ // ❌ BAD: UserController.ts (multiple classes)
31
+ export class UserController { /* ... */ }
32
+ export class UserValidator { /* ... */ }
33
+ export class UserNotifier { /* ... */ }
34
+
35
+ // ✅ GOOD: Three separate files
36
+ // UserController.ts
37
+ export class UserController { /* ... */ }
38
+
39
+ // UserValidator.ts
40
+ export class UserValidator { /* ... */ }
41
+
42
+ // UserNotifier.ts
43
+ export class UserNotifier { /* ... */ }
44
+ \`\`\`
45
+
46
+ ### Step 2: Extract Child Responsibilities (if single class is too large)
47
+
48
+ #### Pattern: Create New Service Class with Dependency Injection
49
+
50
+ \`\`\`typescript
51
+ // ❌ BAD: UserController.ts (800 lines, single class)
52
+ @provideSingleton()
53
+ @Controller()
54
+ export class UserController {
55
+ // 200 lines: CRUD operations
56
+ // 300 lines: validation logic
57
+ // 200 lines: notification logic
58
+ // 100 lines: analytics logic
59
+ }
60
+
61
+ // ✅ GOOD: Extract validation service
62
+ // 1. Create UserValidationService.ts
63
+ @provideSingleton()
64
+ export class UserValidationService {
65
+ validateUserData(data: UserData): ValidationResult {
66
+ // 300 lines of validation logic moved here
67
+ }
68
+
69
+ validateEmail(email: string): boolean { /* ... */ }
70
+ validatePassword(password: string): boolean { /* ... */ }
71
+ }
72
+
73
+ // 2. Inject into UserController.ts
74
+ @provideSingleton()
75
+ @Controller()
76
+ export class UserController {
77
+ constructor(
78
+ @inject(TYPES.UserValidationService)
79
+ private validator: UserValidationService
80
+ ) {}
81
+
82
+ async createUser(data: UserData): Promise<User> {
83
+ const validation = this.validator.validateUserData(data);
84
+ if (!validation.isValid) {
85
+ throw new ValidationError(validation.errors);
86
+ }
87
+ // ... rest of logic
88
+ }
89
+ }
90
+ \`\`\`
91
+
92
+ ## AI Agent Action Steps
93
+
94
+ 1. **ANALYZE** the file structure:
95
+ - Count classes (if >1, separate immediately)
96
+ - Identify logical responsibilities within single class
97
+
98
+ 2. **IDENTIFY** "child code" to extract:
99
+ - Validation logic → ValidationService
100
+ - Notification logic → NotificationService
101
+ - Data transformation → TransformerService
102
+ - External API calls → ApiService
103
+ - Business rules → RulesEngine
104
+
105
+ 3. **CREATE** new service file(s):
106
+ - Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
107
+ - Add \`@provideSingleton()\` decorator
108
+ - Move child methods to new class
109
+
110
+ 4. **UPDATE** dependency injection:
111
+ - Add to \`TYPES\` constants (if using symbol-based DI)
112
+ - Inject new service into original class constructor
113
+ - Replace direct method calls with \`this.serviceName.method()\`
114
+
115
+ 5. **RENAME** extracted file:
116
+ - Read the extracted code to understand its purpose
117
+ - Rename \`XXXX.ts\` to logical name (e.g., \`UserValidationService.ts\`)
118
+
119
+ 6. **VERIFY** file sizes:
120
+ - Original file should now be <700 lines
121
+ - Each extracted file should be <700 lines
122
+ - If still too large, extract more services
123
+
124
+ ## Examples of Child Responsibilities to Extract
125
+
126
+ | If File Contains | Extract To | Pattern |
127
+ |-----------------|------------|---------|
128
+ | Validation logic (200+ lines) | \`XValidator.ts\` or \`XValidationService.ts\` | Singleton service |
129
+ | Notification logic (150+ lines) | \`XNotifier.ts\` or \`XNotificationService.ts\` | Singleton service |
130
+ | Data transformation (200+ lines) | \`XTransformer.ts\` | Singleton service |
131
+ | External API calls (200+ lines) | \`XApiClient.ts\` | Singleton service |
132
+ | Complex business rules (300+ lines) | \`XRulesEngine.ts\` | Singleton service |
133
+ | Database queries (200+ lines) | \`XRepository.ts\` | Singleton service |
134
+
135
+ ## WebPieces Dependency Injection Pattern
136
+
137
+ \`\`\`typescript
138
+ // 1. Define service with @provideSingleton
139
+ import { provideSingleton } from '@webpieces/http-routing';
140
+
141
+ @provideSingleton()
142
+ export class MyService {
143
+ doSomething(): void { /* ... */ }
144
+ }
145
+
146
+ // 2. Inject into consumer
147
+ import { inject } from 'inversify';
148
+ import { TYPES } from './types';
149
+
150
+ @provideSingleton()
151
+ @Controller()
152
+ export class MyController {
153
+ constructor(
154
+ @inject(TYPES.MyService) private service: MyService
155
+ ) {}
156
+ }
157
+ \`\`\`
158
+
159
+ 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.
160
+ `;
161
+ // Module-level flag to prevent redundant file creation
162
+ let fileDocCreated = false;
163
+ function getWorkspaceRoot(context) {
164
+ const filename = context.filename || context.getFilename();
165
+ let dir = path.dirname(filename);
166
+ // Walk up directory tree to find workspace root
167
+ while (dir !== path.dirname(dir)) {
168
+ const pkgPath = path.join(dir, 'package.json');
169
+ if (fs.existsSync(pkgPath)) {
170
+ try {
171
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
172
+ if (pkg.workspaces || pkg.name === 'webpieces-ts') {
173
+ return dir;
174
+ }
175
+ }
176
+ catch (err) {
177
+ //const error = toError(err);
178
+ // Continue searching if JSON parse fails
179
+ }
180
+ }
181
+ dir = path.dirname(dir);
182
+ }
183
+ return process.cwd(); // Fallback
184
+ }
185
+ function ensureDocFile(docPath, content) {
186
+ try {
187
+ fs.mkdirSync(path.dirname(docPath), { recursive: true });
188
+ fs.writeFileSync(docPath, content, 'utf-8');
189
+ return true;
190
+ }
191
+ catch (err) {
192
+ //const error = toError(err);
193
+ // Graceful degradation: log warning but don't break lint
194
+ console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
195
+ return false;
196
+ }
197
+ }
198
+ function ensureFileDoc(context) {
199
+ if (fileDocCreated)
200
+ return; // Performance: only create once per lint run
201
+ const workspaceRoot = getWorkspaceRoot(context);
202
+ const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.filesize.md');
203
+ if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {
204
+ fileDocCreated = true;
205
+ }
206
+ }
207
+ const rule = {
208
+ meta: {
209
+ type: 'suggestion',
210
+ docs: {
211
+ description: 'Enforce maximum file length',
212
+ category: 'Best Practices',
213
+ recommended: false,
214
+ url: 'https://github.com/deanhiller/webpieces-ts',
215
+ },
216
+ messages: {
217
+ tooLong: 'AI Agent: READ tmp/webpieces/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',
218
+ },
219
+ fixable: undefined,
220
+ schema: [
221
+ {
222
+ type: 'object',
223
+ properties: {
224
+ max: {
225
+ type: 'integer',
226
+ minimum: 1,
227
+ },
228
+ },
229
+ additionalProperties: false,
230
+ },
231
+ ],
232
+ },
233
+ create(context) {
234
+ const options = context.options[0];
235
+ const maxLines = options?.max ?? 700;
236
+ return {
237
+ Program(node) {
238
+ ensureFileDoc(context);
239
+ const sourceCode = context.sourceCode || context.getSourceCode();
240
+ const lines = sourceCode.lines;
241
+ const lineCount = lines.length;
242
+ if (lineCount > maxLines) {
243
+ context.report({
244
+ node,
245
+ messageId: 'tooLong',
246
+ data: {
247
+ actual: String(lineCount),
248
+ max: String(maxLines),
249
+ },
250
+ });
251
+ }
252
+ },
253
+ };
254
+ },
255
+ };
256
+ module.exports = rule;
257
+ //# sourceMappingURL=max-file-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/max-file-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum file length\n *\n * Enforces a configurable maximum line count for files.\n * Default: 700 lines\n *\n * Configuration:\n * '@webpieces/max-file-lines': ['error', { max: 700 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\ninterface FileLinesOptions {\n max: number;\n}\n\nconst FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\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 <700 lines\n - Each extracted file should be <700 lines\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\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// Module-level flag to prevent redundant file creation\nlet fileDocCreated = false;\n\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n // Walk up directory tree to find workspace root\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: any) {\n //const error = toError(err);\n // Continue searching if JSON parse fails\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd(); // Fallback\n}\n\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n //const error = toError(err);\n // Graceful degradation: log warning but don't break lint\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);\n return false;\n }\n}\n\nfunction ensureFileDoc(context: Rule.RuleContext): void {\n if (fileDocCreated) return; // Performance: only create once per lint run\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.filesize.md');\n\n if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {\n fileDocCreated = true;\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum file length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ tmp/webpieces/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as FileLinesOptions | undefined;\n const maxLines = options?.max ?? 700;\n\n return {\n Program(node: any): void {\n ensureFileDoc(context);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n const lines = sourceCode.lines;\n const lineCount = lines.length;\n\n if (lineCount > maxLines) {\n context.report({\n node,\n messageId: 'tooLong',\n data: {\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}