@webpieces/dev-config 0.2.17 → 0.2.23

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 (85) hide show
  1. package/README.md +44 -1
  2. package/architecture/executors/generate/executor.d.ts +17 -0
  3. package/architecture/executors/generate/executor.js +67 -0
  4. package/architecture/executors/generate/executor.js.map +1 -0
  5. package/architecture/executors/generate/executor.ts +83 -0
  6. package/architecture/executors/generate/schema.json +14 -0
  7. package/architecture/executors/validate-architecture-unchanged/executor.d.ts +17 -0
  8. package/architecture/executors/validate-architecture-unchanged/executor.js +65 -0
  9. package/architecture/executors/validate-architecture-unchanged/executor.js.map +1 -0
  10. package/architecture/executors/validate-architecture-unchanged/executor.ts +81 -0
  11. package/architecture/executors/validate-architecture-unchanged/schema.json +14 -0
  12. package/architecture/executors/validate-no-cycles/executor.d.ts +16 -0
  13. package/architecture/executors/validate-no-cycles/executor.js +48 -0
  14. package/architecture/executors/validate-no-cycles/executor.js.map +1 -0
  15. package/architecture/executors/validate-no-cycles/executor.ts +60 -0
  16. package/architecture/executors/validate-no-cycles/schema.json +8 -0
  17. package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +19 -0
  18. package/architecture/executors/validate-no-skiplevel-deps/executor.js +227 -0
  19. package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +1 -0
  20. package/architecture/executors/validate-no-skiplevel-deps/executor.ts +267 -0
  21. package/architecture/executors/validate-no-skiplevel-deps/schema.json +8 -0
  22. package/architecture/executors/visualize/executor.d.ts +17 -0
  23. package/architecture/executors/visualize/executor.js +49 -0
  24. package/architecture/executors/visualize/executor.js.map +1 -0
  25. package/architecture/executors/visualize/executor.ts +63 -0
  26. package/architecture/executors/visualize/schema.json +14 -0
  27. package/architecture/index.d.ts +19 -0
  28. package/architecture/index.js +23 -0
  29. package/architecture/index.js.map +1 -0
  30. package/architecture/index.ts +20 -0
  31. package/architecture/lib/graph-comparator.d.ts +39 -0
  32. package/architecture/lib/graph-comparator.js +100 -0
  33. package/architecture/lib/graph-comparator.js.map +1 -0
  34. package/architecture/lib/graph-comparator.ts +141 -0
  35. package/architecture/lib/graph-generator.d.ts +19 -0
  36. package/architecture/lib/graph-generator.js +88 -0
  37. package/architecture/lib/graph-generator.js.map +1 -0
  38. package/architecture/lib/graph-generator.ts +102 -0
  39. package/architecture/lib/graph-loader.d.ts +31 -0
  40. package/architecture/lib/graph-loader.js +70 -0
  41. package/architecture/lib/graph-loader.js.map +1 -0
  42. package/architecture/lib/graph-loader.ts +82 -0
  43. package/architecture/lib/graph-sorter.d.ts +37 -0
  44. package/architecture/lib/graph-sorter.js +110 -0
  45. package/architecture/lib/graph-sorter.js.map +1 -0
  46. package/architecture/lib/graph-sorter.ts +137 -0
  47. package/architecture/lib/graph-visualizer.d.ts +29 -0
  48. package/architecture/lib/graph-visualizer.js +209 -0
  49. package/architecture/lib/graph-visualizer.js.map +1 -0
  50. package/architecture/lib/graph-visualizer.ts +222 -0
  51. package/architecture/lib/package-validator.d.ts +38 -0
  52. package/architecture/lib/package-validator.js +105 -0
  53. package/architecture/lib/package-validator.js.map +1 -0
  54. package/architecture/lib/package-validator.ts +144 -0
  55. package/config/eslint/base.mjs +6 -0
  56. package/eslint-plugin/__tests__/max-file-lines.test.ts +207 -0
  57. package/eslint-plugin/__tests__/max-method-lines.test.ts +258 -0
  58. package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +359 -0
  59. package/eslint-plugin/index.d.ts +11 -0
  60. package/eslint-plugin/index.js +15 -0
  61. package/eslint-plugin/index.js.map +1 -1
  62. package/eslint-plugin/index.ts +15 -0
  63. package/eslint-plugin/rules/enforce-architecture.d.ts +15 -0
  64. package/eslint-plugin/rules/enforce-architecture.js +406 -0
  65. package/eslint-plugin/rules/enforce-architecture.js.map +1 -0
  66. package/eslint-plugin/rules/enforce-architecture.ts +469 -0
  67. package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
  68. package/eslint-plugin/rules/max-file-lines.js +257 -0
  69. package/eslint-plugin/rules/max-file-lines.js.map +1 -0
  70. package/eslint-plugin/rules/max-file-lines.ts +272 -0
  71. package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
  72. package/eslint-plugin/rules/max-method-lines.js +240 -0
  73. package/eslint-plugin/rules/max-method-lines.js.map +1 -0
  74. package/eslint-plugin/rules/max-method-lines.ts +287 -0
  75. package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +22 -0
  76. package/eslint-plugin/rules/no-unmanaged-exceptions.js +605 -0
  77. package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +1 -0
  78. package/eslint-plugin/rules/no-unmanaged-exceptions.ts +621 -0
  79. package/executors.json +29 -0
  80. package/generators/init/generator.ts +130 -0
  81. package/generators/init/schema.json +15 -0
  82. package/generators.json +10 -0
  83. package/package.json +20 -3
  84. package/plugin/README.md +236 -0
  85. package/plugin/index.ts +4 -0
@@ -0,0 +1,621 @@
1
+ /**
2
+ * ESLint rule to discourage try-catch blocks outside test files
3
+ *
4
+ * Works alongside catch-error-pattern rule:
5
+ * - catch-error-pattern: Enforces HOW to handle exceptions (with toError())
6
+ * - no-unmanaged-exceptions: Enforces WHERE try-catch is allowed (tests only by default)
7
+ *
8
+ * Philosophy: Exceptions should bubble to global error handlers where they are logged
9
+ * with traceId and stored for debugging via /debugLocal and /debugCloud endpoints.
10
+ * Local try-catch blocks break this architecture and create blind spots in production.
11
+ *
12
+ * Auto-allowed in:
13
+ * - Test files (.test.ts, .spec.ts, __tests__/)
14
+ *
15
+ * Requires eslint-disable comment in:
16
+ * - Retry loops with exponential backoff
17
+ * - Batch processing where partial failure is expected
18
+ * - Resource cleanup (with approval)
19
+ */
20
+
21
+ import type { Rule } from 'eslint';
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+
25
+ /**
26
+ * Determines if a file is a test file based on naming conventions
27
+ * Test files are auto-allowed to use try-catch blocks
28
+ */
29
+ function isTestFile(filename: string): boolean {
30
+ const normalizedPath = filename.toLowerCase();
31
+
32
+ // Check file extensions
33
+ if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {
34
+ return true;
35
+ }
36
+
37
+ // Check directory names (cross-platform)
38
+ if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\__tests__\\')) {
39
+ return true;
40
+ }
41
+
42
+ return false;
43
+ }
44
+
45
+ /**
46
+ * Finds the workspace root by walking up the directory tree
47
+ * Looks for package.json with workspaces or name === 'webpieces-ts'
48
+ */
49
+ function getWorkspaceRoot(context: Rule.RuleContext): string {
50
+ const filename = context.filename || context.getFilename();
51
+ let dir = path.dirname(filename);
52
+
53
+ // Walk up directory tree
54
+ for (let i = 0; i < 10; i++) {
55
+ const pkgPath = path.join(dir, 'package.json');
56
+ if (fs.existsSync(pkgPath)) {
57
+ try {
58
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
59
+ // Check if this is the root workspace
60
+ if (pkg.workspaces || pkg.name === 'webpieces-ts') {
61
+ return dir;
62
+ }
63
+ } catch {
64
+ // Invalid JSON, keep searching
65
+ }
66
+ }
67
+
68
+ const parentDir = path.dirname(dir);
69
+ if (parentDir === dir) break; // Reached filesystem root
70
+ dir = parentDir;
71
+ }
72
+
73
+ // Fallback: return current directory
74
+ return process.cwd();
75
+ }
76
+
77
+ /**
78
+ * Ensures a documentation file exists at the given path
79
+ * Creates parent directories if needed
80
+ */
81
+ function ensureDocFile(docPath: string, content: string): boolean {
82
+ try {
83
+ const dir = path.dirname(docPath);
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ }
87
+
88
+ // Only write if file doesn't exist or is empty
89
+ if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {
90
+ fs.writeFileSync(docPath, content, 'utf-8');
91
+ }
92
+
93
+ return true;
94
+ } catch (error) {
95
+ // Silently fail - don't break linting if file creation fails
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Ensures the exception documentation markdown file exists
102
+ * Only creates file once per lint run using module-level flag
103
+ */
104
+ function ensureExceptionDoc(context: Rule.RuleContext): void {
105
+ if (exceptionDocCreated) return;
106
+
107
+ const workspaceRoot = getWorkspaceRoot(context);
108
+ const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.exceptions.md');
109
+
110
+ if (ensureDocFile(docPath, EXCEPTION_DOC_CONTENT)) {
111
+ exceptionDocCreated = true;
112
+ }
113
+ }
114
+
115
+ // Module-level flag to prevent redundant markdown file creation
116
+ let exceptionDocCreated = false;
117
+
118
+ // Comprehensive markdown documentation content
119
+ const EXCEPTION_DOC_CONTENT = `# AI Agent Instructions: Try-Catch Blocks Detected
120
+
121
+ **READ THIS FILE to understand why try-catch blocks are restricted and how to fix violations**
122
+
123
+ ## Core Principle
124
+
125
+ **EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**
126
+
127
+ The webpieces framework uses a global error handling architecture where:
128
+ - Every request gets a unique traceId stored in RequestContext
129
+ - All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)
130
+ - Error IDs enable lookup via \`/debugLocal/{id}\` and \`/debugCloud/{id}\` endpoints
131
+ - Local try-catch blocks break this pattern by losing error IDs and context
132
+
133
+ This is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.
134
+
135
+ ## Why This Rule Exists
136
+
137
+ ### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)
138
+ AI agents tend to add defensive try-catch blocks everywhere, which:
139
+ - Swallows errors and loses traceId
140
+ - Shows custom error messages without debugging context
141
+ - Makes production issues impossible to trace
142
+ - Creates "blind spots" where errors disappear
143
+
144
+ ### Problem 2: Lost TraceId = Lost Debugging Capability
145
+ Without traceId in errors:
146
+ - \`/debugLocal/{id}\` endpoint cannot retrieve error details
147
+ - \`/debugCloud/{id}\` endpoint cannot correlate logs
148
+ - DevOps cannot trace request flow through distributed systems
149
+ - Users report "an error occurred" with no way to investigate
150
+
151
+ ### Problem 3: Try-Catch-Rethrow Is Code Smell
152
+ \`\`\`typescript
153
+ // BAD: Why catch if you're just rethrowing?
154
+ try {
155
+ await operation();
156
+ } catch (err: any) {
157
+ const error = toError(err);
158
+ console.error('Failed:', error);
159
+ throw error; // Why catch at all???
160
+ }
161
+ \`\`\`
162
+ 99% of the time, there's a better pattern (logging filter, global handler, etc.).
163
+
164
+ ### Problem 4: Swallowing Exceptions = Lazy Programming
165
+ \`\`\`typescript
166
+ // BAD: "I don't want to deal with this error"
167
+ try {
168
+ await riskyOperation();
169
+ } catch (err: any) {
170
+ // Silence...
171
+ }
172
+ \`\`\`
173
+ This is the #1 shortcut developers take that creates production nightmares.
174
+
175
+ ## Industry Best Practices (2025)
176
+
177
+ ### Distributed Tracing: The Three Pillars
178
+ Modern observability requires correlation across:
179
+ 1. **Traces** - Request flow through services
180
+ 2. **Logs** - Contextual debugging information
181
+ 3. **Metrics** - Aggregated system health
182
+
183
+ TraceId (also called correlation ID, request ID) ties these together.
184
+
185
+ ### Research Findings
186
+ - **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)
187
+ - **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)
188
+ - **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)
189
+ - **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: "Effective Error Handling" - JavaScript design patterns)
190
+
191
+ ### 2025 Trends
192
+ - Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)
193
+ - Structured logging with context (Winston, Pino)
194
+ - Middleware-based error boundaries reduce boilerplate
195
+ - Frontend: React Error Boundaries, not scattered try-catch
196
+
197
+ ## Command: Remove Try-Catch and Use Global Handler
198
+
199
+ ## AI Agent Action Steps
200
+
201
+ 1. **IDENTIFY** the try-catch block flagged in the error message
202
+
203
+ 2. **ANALYZE** the purpose:
204
+ - Is it catching errors just to log them? → Remove (use LogApiFilter)
205
+ - Is it catching to show custom message? → Remove (use global handler)
206
+ - Is it catching to retry? → Requires approval (see Acceptable Patterns)
207
+ - Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)
208
+ - Is it catching for cleanup? → Usually wrong pattern
209
+
210
+ 3. **REMOVE** the try-catch block:
211
+ - Delete the \`try {\` and \`} catch (err: any) { ... }\` wrapper
212
+ - Let the code execute normally
213
+ - Errors will bubble to global handler automatically
214
+
215
+ 4. **VERIFY** global handler exists:
216
+ - Check that WebpiecesMiddleware.globalErrorHandler is registered
217
+ - Check that ContextFilter is setting up RequestContext
218
+ - Check that traceId is being added to RequestContext
219
+
220
+ 5. **ADD** traceId to RequestContext (if not already present):
221
+ - In ContextFilter or similar high-priority filter
222
+ - Use \`RequestContext.put('TRACE_ID', generateTraceId())\`
223
+
224
+ 6. **TEST** error flow:
225
+ - Trigger an error in the code
226
+ - Verify error is logged with traceId
227
+ - Verify \`/debugLocal/{traceId}\` endpoint works
228
+
229
+ ## Pattern 1: Global Error Handler (GOOD)
230
+
231
+ ### Server-Side: WebpiecesMiddleware
232
+
233
+ \`\`\`typescript
234
+ // packages/http/http-server/src/WebpiecesMiddleware.ts
235
+ @provideSingleton()
236
+ @injectable()
237
+ export class WebpiecesMiddleware {
238
+ async globalErrorHandler(
239
+ req: Request,
240
+ res: Response,
241
+ next: NextFunction
242
+ ): Promise<void> {
243
+ console.log('[GlobalErrorHandler] Request START:', req.method, req.path);
244
+
245
+ try {
246
+ // Await catches BOTH sync throws AND rejected promises
247
+ await next();
248
+ console.log('[GlobalErrorHandler] Request END (success)');
249
+ } catch (err: any) {
250
+ const error = toError(err);
251
+ const traceId = RequestContext.get<string>('TRACE_ID');
252
+
253
+ // Log with traceId for /debugLocal lookup
254
+ console.error('[GlobalErrorHandler] ERROR:', {
255
+ traceId,
256
+ message: error.message,
257
+ stack: error.stack,
258
+ path: req.path,
259
+ method: req.method,
260
+ });
261
+
262
+ // Store error for /debugLocal/{id} endpoint
263
+ ErrorStore.save(traceId, error);
264
+
265
+ if (!res.headersSent) {
266
+ res.status(500).send(\`
267
+ <!DOCTYPE html>
268
+ <html>
269
+ <head><title>Server Error</title></head>
270
+ <body>
271
+ <h1>Server Error</h1>
272
+ <p>An error occurred. Reference ID: \${traceId}</p>
273
+ <p>Contact support with this ID to investigate.</p>
274
+ </body>
275
+ </html>
276
+ \`);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ \`\`\`
282
+
283
+ ### Adding TraceId: ContextFilter
284
+
285
+ \`\`\`typescript
286
+ // packages/http/http-server/src/filters/ContextFilter.ts
287
+ import { v4 as uuidv4 } from 'uuid';
288
+
289
+ @provideSingleton()
290
+ @injectable()
291
+ export class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {
292
+ async filter(
293
+ meta: MethodMeta,
294
+ nextFilter: Service<MethodMeta, WpResponse<unknown>>
295
+ ): Promise<WpResponse<unknown>> {
296
+ return RequestContext.run(async () => {
297
+ // Generate unique traceId for this request
298
+ const traceId = uuidv4();
299
+ RequestContext.put('TRACE_ID', traceId);
300
+ RequestContext.put('METHOD_META', meta);
301
+ RequestContext.put('REQUEST_PATH', meta.path);
302
+
303
+ return await nextFilter.invoke(meta);
304
+ // RequestContext auto-cleared when done
305
+ });
306
+ }
307
+ }
308
+ \`\`\`
309
+
310
+ ## Pattern 2: Debug Endpoints (GOOD)
311
+
312
+ \`\`\`typescript
313
+ // Example debug endpoint for local development
314
+ @provideSingleton()
315
+ @Controller()
316
+ export class DebugController implements DebugApi {
317
+ @Get()
318
+ @Path('/debugLocal/:id')
319
+ async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {
320
+ const error = ErrorStore.get(id);
321
+ if (!error) {
322
+ throw new HttpNotFoundError(\`Error \${id} not found\`);
323
+ }
324
+
325
+ return {
326
+ traceId: id,
327
+ message: error.message,
328
+ stack: error.stack,
329
+ timestamp: error.timestamp,
330
+ requestPath: error.requestPath,
331
+ requestMethod: error.requestMethod,
332
+ };
333
+ }
334
+ }
335
+
336
+ // ErrorStore singleton (in-memory for local, Redis for production)
337
+ class ErrorStoreImpl {
338
+ private errors = new Map<string, ErrorRecord>();
339
+
340
+ save(traceId: string, error: Error): void {
341
+ this.errors.set(traceId, {
342
+ traceId,
343
+ message: error.message,
344
+ stack: error.stack,
345
+ timestamp: new Date(),
346
+ requestPath: RequestContext.get('REQUEST_PATH'),
347
+ requestMethod: RequestContext.get('HTTP_METHOD'),
348
+ });
349
+ }
350
+
351
+ get(traceId: string): ErrorRecord | undefined {
352
+ return this.errors.get(traceId);
353
+ }
354
+ }
355
+
356
+ export const ErrorStore = new ErrorStoreImpl();
357
+ \`\`\`
358
+
359
+ ## Examples
360
+
361
+ ### BAD Example 1: Local Try-Catch That Swallows Error
362
+
363
+ \`\`\`typescript
364
+ // BAD: Error is swallowed, no traceId in logs
365
+ async function processOrder(order: Order): Promise<void> {
366
+ try {
367
+ await validateOrder(order);
368
+ await saveToDatabase(order);
369
+ } catch (err: any) {
370
+ // Error disappears into void - debugging nightmare!
371
+ console.log('Order processing failed');
372
+ }
373
+ }
374
+ \`\`\`
375
+
376
+ **Problem**: When this fails in production, you have:
377
+ - No traceId to look up the error
378
+ - No stack trace
379
+ - No request context
380
+ - No way to investigate
381
+
382
+ ### BAD Example 2: Try-Catch With Custom Error (No TraceId)
383
+
384
+ \`\`\`typescript
385
+ // BAD: Shows custom message but loses traceId
386
+ async function fetchUserData(userId: string): Promise<User> {
387
+ try {
388
+ const response = await fetch(\`/api/users/\${userId}\`);
389
+ return await response.json();
390
+ } catch (err: any) {
391
+ const error = toError(err);
392
+ // Custom message without traceId
393
+ throw new Error(\`Failed to fetch user \${userId}: \${error.message}\`);
394
+ }
395
+ }
396
+ \`\`\`
397
+
398
+ **Problem**:
399
+ - Original error context is lost
400
+ - No traceId attached to new error
401
+ - Global handler receives generic error, can't trace root cause
402
+
403
+ ### GOOD Example 1: Let Error Bubble
404
+
405
+ \`\`\`typescript
406
+ // GOOD: Error bubbles to global handler with traceId
407
+ async function processOrder(order: Order): Promise<void> {
408
+ // No try-catch needed!
409
+ await validateOrder(order);
410
+ await saveToDatabase(order);
411
+ // If error occurs, it bubbles with traceId intact
412
+ }
413
+ \`\`\`
414
+
415
+ **Why GOOD**:
416
+ - Global handler catches error
417
+ - TraceId from RequestContext is preserved
418
+ - Full stack trace available
419
+ - \`/debugLocal/{traceId}\` endpoint works
420
+
421
+ ### GOOD Example 2: Global Handler Logs With TraceId
422
+
423
+ \`\`\`typescript
424
+ // GOOD: Global handler has full context
425
+ // In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)
426
+ catch (err: any) {
427
+ const error = toError(err);
428
+ const traceId = RequestContext.get<string>('TRACE_ID');
429
+
430
+ console.error('[GlobalErrorHandler] ERROR:', {
431
+ traceId, // Unique ID for this request
432
+ message: error.message,
433
+ stack: error.stack,
434
+ path: req.path, // Request context preserved
435
+ });
436
+ }
437
+ \`\`\`
438
+
439
+ **Why GOOD**:
440
+ - TraceId logged with every error
441
+ - Full request context available
442
+ - Error stored for \`/debugLocal/{id}\` lookup
443
+ - DevOps can trace distributed requests
444
+
445
+ ### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)
446
+
447
+ \`\`\`typescript
448
+ // ACCEPTABLE: Retry pattern requires try-catch
449
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff
450
+ async function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {
451
+ const maxRetries = 3;
452
+ let lastError: Error | undefined;
453
+
454
+ for (let i = 0; i < maxRetries; i++) {
455
+ try {
456
+ return await vendorApi.call(request);
457
+ } catch (err: any) {
458
+ const error = toError(err);
459
+ lastError = error;
460
+ console.warn(\`Retry \${i + 1}/\${maxRetries} failed:\`, error.message);
461
+ await sleep(1000 * Math.pow(2, i)); // Exponential backoff
462
+ }
463
+ }
464
+
465
+ // After retries exhausted, throw with traceId
466
+ const traceId = RequestContext.get<string>('TRACE_ID');
467
+ throw new HttpVendorError(
468
+ \`Vendor API failed after \${maxRetries} retries. TraceId: \${traceId}\`,
469
+ lastError
470
+ );
471
+ }
472
+ \`\`\`
473
+
474
+ **Why ACCEPTABLE**:
475
+ - Legitimate use case: retry logic
476
+ - Final error still includes traceId
477
+ - Error still bubbles to global handler
478
+ - Requires senior developer approval (enforced by PR review)
479
+
480
+ ### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)
481
+
482
+ \`\`\`typescript
483
+ // ACCEPTABLE: Batching requires try-catch to continue processing
484
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures
485
+ async function processBatch(items: Item[]): Promise<BatchResult> {
486
+ const results: ItemResult[] = [];
487
+ const errors: ItemError[] = [];
488
+ const traceId = RequestContext.get<string>('TRACE_ID');
489
+
490
+ for (const item of items) {
491
+ try {
492
+ const result = await processItem(item);
493
+ results.push(result);
494
+ } catch (err: any) {
495
+ const error = toError(err);
496
+ // Log individual error with traceId
497
+ console.error(\`[Batch] Item \${item.id} failed (traceId: \${traceId}):\`, error);
498
+ errors.push({ itemId: item.id, error: error.message, traceId });
499
+ }
500
+ }
501
+
502
+ // Return both successes and failures
503
+ return {
504
+ traceId,
505
+ successCount: results.length,
506
+ failureCount: errors.length,
507
+ results,
508
+ errors,
509
+ };
510
+ }
511
+ \`\`\`
512
+
513
+ **Why ACCEPTABLE**:
514
+ - Legitimate use case: partial failure handling
515
+ - Each error logged with traceId
516
+ - Batch traceId included in response
517
+ - Requires senior developer approval (enforced by PR review)
518
+
519
+ ### UNACCEPTABLE Example: Try-Catch-Rethrow
520
+
521
+ \`\`\`typescript
522
+ // UNACCEPTABLE: Pointless try-catch that just rethrows
523
+ async function saveUser(user: User): Promise<void> {
524
+ try {
525
+ await database.save(user);
526
+ } catch (err: any) {
527
+ const error = toError(err);
528
+ console.error('Save failed:', error);
529
+ throw error; // Why catch at all???
530
+ }
531
+ }
532
+ \`\`\`
533
+
534
+ **Why UNACCEPTABLE**:
535
+ - Adds no value - logging should be in LogApiFilter
536
+ - Global handler already logs errors
537
+ - Just adds noise and confusion
538
+ - Remove the try-catch entirely!
539
+
540
+ ## When eslint-disable IS Acceptable
541
+
542
+ You may use \`// eslint-disable-next-line @webpieces/no-unmanaged-exceptions\` ONLY for:
543
+
544
+ 1. **Retry loops** with exponential backoff (vendor API calls)
545
+ 2. **Batching patterns** where partial failure is expected
546
+ 3. **Resource cleanup** with explicit approval
547
+
548
+ All three require:
549
+ - Senior developer approval in PR review
550
+ - Comment explaining WHY try-catch is needed
551
+ - TraceId must still be logged/included in final error
552
+
553
+ ## How to Request Approval
554
+
555
+ If you believe you have a legitimate use case for try-catch:
556
+
557
+ 1. **Add a comment** explaining why it's needed:
558
+ \`\`\`typescript
559
+ // JUSTIFICATION: Vendor API requires retry loop with exponential backoff
560
+ // to handle rate limiting. Final error includes traceId for debugging.
561
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
562
+ \`\`\`
563
+
564
+ 2. **Ensure traceId is preserved** in final error or logged
565
+
566
+ 3. **Request PR review** from senior developer
567
+
568
+ 4. **Be prepared to justify** - 99% of try-catch can be removed
569
+
570
+ ## Summary
571
+
572
+ **The webpieces philosophy**: Errors should bubble to the global handler where they are logged with traceId and stored for debugging. Local try-catch blocks break this architecture and create blind spots in production.
573
+
574
+ **Key takeaways**:
575
+ - Global error handler with traceId = debuggable production issues
576
+ - Local try-catch = lost context and debugging nightmares
577
+ - 99% of try-catch blocks can be removed safely
578
+ - Only use try-catch for: retries, batching (with approval)
579
+ - TraceId enables \`/debugLocal/{id}\` and \`/debugCloud/{id}\` endpoints
580
+
581
+ **Remember**: If you can't handle the error meaningfully, don't catch it. Let it bubble to the global handler where it will be logged with full context and traceId.
582
+ `;
583
+
584
+ const rule: Rule.RuleModule = {
585
+ meta: {
586
+ type: 'problem',
587
+ docs: {
588
+ description: 'Discourage try-catch blocks outside test files - use global error handlers',
589
+ category: 'Best Practices',
590
+ recommended: true,
591
+ url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',
592
+ },
593
+ messages: {
594
+ noUnmanagedExceptions:
595
+ 'AI Agent: READ tmp/webpieces/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',
596
+ },
597
+ fixable: undefined,
598
+ schema: [],
599
+ },
600
+
601
+ create(context: Rule.RuleContext): Rule.RuleListener {
602
+ return {
603
+ TryStatement(node: any): void {
604
+ // Auto-allow in test files
605
+ const filename = context.filename || context.getFilename();
606
+ if (isTestFile(filename)) {
607
+ return;
608
+ }
609
+
610
+ // Not in test file - report violation
611
+ ensureExceptionDoc(context);
612
+ context.report({
613
+ node,
614
+ messageId: 'noUnmanagedExceptions',
615
+ });
616
+ },
617
+ };
618
+ },
619
+ };
620
+
621
+ export = rule;
package/executors.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "executors": {
3
+ "generate": {
4
+ "implementation": "./architecture/executors/generate/executor",
5
+ "schema": "./architecture/executors/generate/schema.json",
6
+ "description": "Generate the architecture dependency graph and save it"
7
+ },
8
+ "visualize": {
9
+ "implementation": "./architecture/executors/visualize/executor",
10
+ "schema": "./architecture/executors/visualize/schema.json",
11
+ "description": "Generate visual representations of the architecture graph"
12
+ },
13
+ "validate-no-cycles": {
14
+ "implementation": "./architecture/executors/validate-no-cycles/executor",
15
+ "schema": "./architecture/executors/validate-no-cycles/schema.json",
16
+ "description": "Validate the architecture graph has no circular dependencies"
17
+ },
18
+ "validate-architecture-unchanged": {
19
+ "implementation": "./architecture/executors/validate-architecture-unchanged/executor",
20
+ "schema": "./architecture/executors/validate-architecture-unchanged/schema.json",
21
+ "description": "Validate the architecture graph matches the saved blessed graph"
22
+ },
23
+ "validate-no-skiplevel-deps": {
24
+ "implementation": "./architecture/executors/validate-no-skiplevel-deps/executor",
25
+ "schema": "./architecture/executors/validate-no-skiplevel-deps/schema.json",
26
+ "description": "Validate no project has redundant transitive dependencies"
27
+ }
28
+ }
29
+ }