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