@webpieces/rules-config 0.2.115 → 0.2.117
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.
- package/package.json +1 -1
- package/templates/webpieces.dependencies.md +136 -0
- package/templates/webpieces.exceptions.md +694 -0
- package/templates/webpieces.filesize.md +173 -0
- package/templates/webpieces.methods.md +97 -0
- package/templates/webpieces.methodsize.md +132 -0
- package/templates/webpieces.transitivedeps.md +102 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.117",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Instructions: Architecture Dependency Violation
|
|
2
|
+
|
|
3
|
+
IN GENERAL, it is better to avoid these changes and find a different way by moving classes
|
|
4
|
+
around to existing packages you already depend on. It is not always avoidable though.
|
|
5
|
+
A clean dependency graph keeps you out of huge trouble later.
|
|
6
|
+
|
|
7
|
+
If you are a human, simply run these commands:
|
|
8
|
+
* nx run architecture:visualize - to see the new dependencies and validate that change is desired
|
|
9
|
+
* nx run architecture:generate - updates the dep graph
|
|
10
|
+
* git diff architecture/dependencies.json - to see the deps changes you made
|
|
11
|
+
|
|
12
|
+
**READ THIS FILE FIRST before making any changes!**
|
|
13
|
+
|
|
14
|
+
## ⚠️ CRITICAL WARNING ⚠️
|
|
15
|
+
|
|
16
|
+
**This is a VERY IMPORTANT change that has LARGE REPERCUSSIONS later!**
|
|
17
|
+
|
|
18
|
+
Adding new dependencies creates technical debt that compounds over time:
|
|
19
|
+
- Creates coupling between packages that may be hard to undo
|
|
20
|
+
- Can create circular dependency tangles
|
|
21
|
+
- Makes packages harder to test in isolation
|
|
22
|
+
- Increases build times and bundle sizes
|
|
23
|
+
- May force unnecessary upgrades across the codebase
|
|
24
|
+
|
|
25
|
+
**DO NOT add dependencies without senior developer approval!**
|
|
26
|
+
|
|
27
|
+
## Understanding the Error
|
|
28
|
+
|
|
29
|
+
You've attempted to import from a package that is not in your project's allowed dependencies.
|
|
30
|
+
The architecture enforces a layered dependency structure where:
|
|
31
|
+
- Level 0 packages are foundation packages with NO dependencies on other @webpieces packages
|
|
32
|
+
- Higher level packages can only depend on lower level packages
|
|
33
|
+
- All dependencies must be explicitly declared
|
|
34
|
+
|
|
35
|
+
## Steps to Resolve
|
|
36
|
+
|
|
37
|
+
### Step 1: Generate Current Dependency Graph
|
|
38
|
+
Run this command to see the current architecture:
|
|
39
|
+
```bash
|
|
40
|
+
npx nx run architecture:generate
|
|
41
|
+
```
|
|
42
|
+
This creates/updates `architecture/dependencies.json` showing all packages and their levels.
|
|
43
|
+
|
|
44
|
+
### Step 2: Analyze the Proposed Change
|
|
45
|
+
Ask yourself:
|
|
46
|
+
1. **Is this import truly necessary?** Can you refactor to avoid it?
|
|
47
|
+
2. **Should the code move instead?** Maybe the code belongs in a different package.
|
|
48
|
+
3. **Will this create a cycle?** Use `npx nx graph` to visualize dependencies.
|
|
49
|
+
4. **Can you use an interface/abstraction?** Define interface in lower-level package, implement in higher-level.
|
|
50
|
+
|
|
51
|
+
### Step 3: Get Senior Developer Approval
|
|
52
|
+
|
|
53
|
+
## 🛑 AI AGENTS: STOP HERE AND ASK FOR HUMAN APPROVAL! 🛑
|
|
54
|
+
|
|
55
|
+
**YOU MUST NOT PROCEED TO STEP 4 WITHOUT EXPLICIT HUMAN APPROVAL!**
|
|
56
|
+
|
|
57
|
+
**REQUIRED**: Discuss this architectural change with a senior developer before proceeding.
|
|
58
|
+
- Explain why the dependency is needed
|
|
59
|
+
- Show you've considered alternatives (Step 2)
|
|
60
|
+
- **WAIT for explicit approval before making ANY changes to project.json or package.json**
|
|
61
|
+
|
|
62
|
+
**AI Agent Instructions:**
|
|
63
|
+
1. Present your analysis from Step 2 to the human
|
|
64
|
+
2. Explain which package needs which dependency and why
|
|
65
|
+
3. ASK: "Do you approve adding this dependency?"
|
|
66
|
+
4. **DO NOT modify project.json or package.json until you receive explicit "yes" or approval**
|
|
67
|
+
|
|
68
|
+
### Step 4: If Approved, Add the Dependency
|
|
69
|
+
|
|
70
|
+
## ⛔ NEVER MODIFY THESE FILES WITHOUT HUMAN APPROVAL FROM STEP 3! ⛔
|
|
71
|
+
|
|
72
|
+
Only after receiving explicit human approval in Step 3, make these changes:
|
|
73
|
+
|
|
74
|
+
1. **Update project.json** - Add to `build.dependsOn`:
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"targets": {
|
|
78
|
+
"build": {
|
|
79
|
+
"dependsOn": ["^build", "dep1:build", "NEW_PACKAGE:build"]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
2. **Update package.json** - Add to `dependencies`:
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"dependencies": {
|
|
89
|
+
"@webpieces/NEW_PACKAGE": "*"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Step 5: Update Architecture Definition
|
|
95
|
+
Run this command to validate and update the architecture:
|
|
96
|
+
```bash
|
|
97
|
+
npx nx run architecture:generate
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This will:
|
|
101
|
+
- Detect any cycles (which MUST be fixed before proceeding)
|
|
102
|
+
- Update `architecture/dependencies.json` with the new dependency
|
|
103
|
+
- Recalculate package levels
|
|
104
|
+
|
|
105
|
+
### Step 6: Verify No Cycles
|
|
106
|
+
```bash
|
|
107
|
+
npx nx run architecture:validate-no-architecture-cycles
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If cycles are detected, you MUST refactor to break the cycle. Common strategies:
|
|
111
|
+
- Move shared code to a lower-level package
|
|
112
|
+
- Use dependency inversion (interfaces in low-level, implementations in high-level)
|
|
113
|
+
- Restructure package boundaries
|
|
114
|
+
|
|
115
|
+
## Alternative Solutions (Preferred over adding dependencies)
|
|
116
|
+
|
|
117
|
+
### Option A: Move the Code
|
|
118
|
+
If you need functionality from another package, consider moving that code to a shared lower-level package.
|
|
119
|
+
|
|
120
|
+
### Option B: Dependency Inversion
|
|
121
|
+
Define an interface in the lower-level package, implement it in the higher-level package:
|
|
122
|
+
```typescript
|
|
123
|
+
// In foundation package (level 0)
|
|
124
|
+
export interface Logger { log(msg: string): void; }
|
|
125
|
+
|
|
126
|
+
// In higher-level package
|
|
127
|
+
export class ConsoleLogger implements Logger { ... }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Option C: Pass Dependencies as Parameters
|
|
131
|
+
Instead of importing, receive the dependency as a constructor or method parameter.
|
|
132
|
+
|
|
133
|
+
## Remember
|
|
134
|
+
- Every dependency you add today is technical debt for tomorrow
|
|
135
|
+
- The best dependency is the one you don't need
|
|
136
|
+
- When in doubt, refactor rather than add dependencies
|
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# AI Agent Instructions: Try-Catch Blocks Detected
|
|
2
|
+
|
|
3
|
+
**READ THIS FILE to understand why try-catch blocks are restricted and how to fix violations**
|
|
4
|
+
|
|
5
|
+
## GETTING STARTED: Rolling Out This Rule
|
|
6
|
+
|
|
7
|
+
**Why this rule exists**: AI agents tend to randomly add try-catch blocks ~50% of the time, creating pointless error handling that swallows exceptions and breaks debugging.
|
|
8
|
+
|
|
9
|
+
**How to roll out on existing codebases**:
|
|
10
|
+
1. Enable the rule: `'@webpieces/no-unmanaged-exceptions': 'error'`
|
|
11
|
+
2. Have AI add `// eslint-disable-next-line @webpieces/no-unmanaged-exceptions` to EACH try-catch line (NOT file-level disables)
|
|
12
|
+
3. This forces AI to consciously acknowledge each exception handling location
|
|
13
|
+
4. Going forward, the rule makes AI think twice before adding new try-catch blocks
|
|
14
|
+
|
|
15
|
+
**What the global error handler provides** (when exceptions bubble up properly):
|
|
16
|
+
1. **Logs it** - Full error with stack trace and traceId
|
|
17
|
+
2. **Reports to operations** - Sends to monitoring (Sentry/Datadog) so AI/team can fix
|
|
18
|
+
3. **Shows user-friendly error** - Pops error dialog with errorId (user receives email with same ID for support)
|
|
19
|
+
|
|
20
|
+
**Per-line disables are intentional**: Each disable comment serves as documentation explaining WHY that specific try-catch exists, making code review and future AI sessions aware of the exception handling decision.
|
|
21
|
+
|
|
22
|
+
## Core Principle
|
|
23
|
+
|
|
24
|
+
**EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**
|
|
25
|
+
|
|
26
|
+
The webpieces framework uses a global error handling architecture where:
|
|
27
|
+
- Every request gets a unique traceId stored in RequestContext
|
|
28
|
+
- All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)
|
|
29
|
+
- Error IDs enable lookup via `/debugLocal/{id}` and `/debugCloud/{id}` endpoints
|
|
30
|
+
- Local try-catch blocks break this pattern by losing error IDs and context
|
|
31
|
+
|
|
32
|
+
This is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.
|
|
33
|
+
|
|
34
|
+
## Why This Rule Exists
|
|
35
|
+
|
|
36
|
+
### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)
|
|
37
|
+
AI agents tend to add defensive try-catch blocks everywhere, which:
|
|
38
|
+
- Swallows errors and loses traceId
|
|
39
|
+
- Shows custom error messages without debugging context
|
|
40
|
+
- Makes production issues impossible to trace
|
|
41
|
+
- Creates "blind spots" where errors disappear
|
|
42
|
+
|
|
43
|
+
### Problem 2: Lost TraceId = Lost Debugging Capability
|
|
44
|
+
Without traceId in errors:
|
|
45
|
+
- `/debugLocal/{id}` endpoint cannot retrieve error details
|
|
46
|
+
- `/debugCloud/{id}` endpoint cannot correlate logs
|
|
47
|
+
- DevOps cannot trace request flow through distributed systems
|
|
48
|
+
- Users report "an error occurred" with no way to investigate
|
|
49
|
+
|
|
50
|
+
### Problem 3: Pointless Try-Catch-Rethrow
|
|
51
|
+
```typescript
|
|
52
|
+
// BAD: Catching just to rethrow without adding value
|
|
53
|
+
try {
|
|
54
|
+
await operation();
|
|
55
|
+
} catch (err: unknown) {
|
|
56
|
+
const error = toError(err);
|
|
57
|
+
console.error('Failed:', error);
|
|
58
|
+
throw error; // No new info added - why catch?
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**However, try-catch-rethrow IS acceptable when:**
|
|
63
|
+
1. **Adding context to the error**: `throw new Error("Failed to process order #123", { cause: error })`
|
|
64
|
+
2. **Edge code logging** (see "Edge Code Patterns" section below)
|
|
65
|
+
|
|
66
|
+
The key question: Are you adding meaningful information or context? If yes, it may be valid.
|
|
67
|
+
|
|
68
|
+
### Problem 4: Swallowing Exceptions = Lazy Programming
|
|
69
|
+
```typescript
|
|
70
|
+
// BAD: "I don't want to deal with this error"
|
|
71
|
+
try {
|
|
72
|
+
await riskyOperation();
|
|
73
|
+
} catch (err: unknown) {
|
|
74
|
+
// Silence...
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
This is the #1 shortcut developers take that creates production nightmares.
|
|
78
|
+
|
|
79
|
+
## Industry Best Practices (2025)
|
|
80
|
+
|
|
81
|
+
### Distributed Tracing: The Three Pillars
|
|
82
|
+
Modern observability requires correlation across:
|
|
83
|
+
1. **Traces** - Request flow through services
|
|
84
|
+
2. **Logs** - Contextual debugging information
|
|
85
|
+
3. **Metrics** - Aggregated system health
|
|
86
|
+
|
|
87
|
+
TraceId (also called correlation ID, request ID) ties these together.
|
|
88
|
+
|
|
89
|
+
### Research Findings
|
|
90
|
+
- **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)
|
|
91
|
+
- **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)
|
|
92
|
+
- **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)
|
|
93
|
+
- **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: "Effective Error Handling" - JavaScript design patterns)
|
|
94
|
+
|
|
95
|
+
### 2025 Trends
|
|
96
|
+
- Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)
|
|
97
|
+
- Structured logging with context (Winston, Pino)
|
|
98
|
+
- Middleware-based error boundaries reduce boilerplate
|
|
99
|
+
- Frontend: React Error Boundaries, not scattered try-catch
|
|
100
|
+
|
|
101
|
+
## Command: Remove Try-Catch and Use Global Handler
|
|
102
|
+
|
|
103
|
+
## AI Agent Action Steps
|
|
104
|
+
|
|
105
|
+
1. **IDENTIFY** the try-catch block flagged in the error message
|
|
106
|
+
|
|
107
|
+
2. **ANALYZE** the purpose and ASK USER if needed:
|
|
108
|
+
- Is it catching errors just to log them? → Remove (use LogApiFilter)
|
|
109
|
+
- Is it catching to show custom message? → Remove (use global handler)
|
|
110
|
+
- Is it catching to retry? → Requires approval (see Acceptable Patterns)
|
|
111
|
+
- Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)
|
|
112
|
+
- Is it catching for cleanup? → Usually wrong pattern
|
|
113
|
+
- **Is this a global entry point?** → **ASK USER**: "I think this code is the entry point where we need a global try-catch block. Is this correct?" (95% of the time it is NOT!)
|
|
114
|
+
- **Is this edge code calling external services?** → **ASK USER**: "This looks like edge code calling an external service. Should I add request/response logging with try-catch?"
|
|
115
|
+
- **Is this form error handling?** → Valid IF: catches only `HttpUserError` for display AND rethrows other errors (see Form Error Handling Pattern)
|
|
116
|
+
- Is it adding context to the error before rethrowing? → May be valid (see Problem 3)
|
|
117
|
+
|
|
118
|
+
3. **IF REMOVING** the try-catch block:
|
|
119
|
+
- Delete the `try {` and `} catch (err: unknown) { ... }` wrapper
|
|
120
|
+
- Let the code execute normally
|
|
121
|
+
- Errors will bubble to global handler automatically
|
|
122
|
+
|
|
123
|
+
4. **IF KEEPING** (after user approval):
|
|
124
|
+
- Add eslint-disable comment with justification
|
|
125
|
+
- Ensure traceId is logged/preserved
|
|
126
|
+
- Follow patterns in "Global Try-Catch Entry Points" or "Edge Code Patterns" sections
|
|
127
|
+
|
|
128
|
+
5. **VERIFY** global handler exists:
|
|
129
|
+
- Check that WebpiecesMiddleware.globalErrorHandler is registered
|
|
130
|
+
- Check that ContextFilter is setting up RequestContext
|
|
131
|
+
- Check that traceId is being added to RequestContext
|
|
132
|
+
|
|
133
|
+
6. **ADD** traceId to RequestContext (if not already present):
|
|
134
|
+
- In ContextFilter or similar high-priority filter
|
|
135
|
+
- Use `RequestContext.put('TRACE_ID', generateTraceId())`
|
|
136
|
+
|
|
137
|
+
7. **TEST** error flow:
|
|
138
|
+
- Trigger an error in the code
|
|
139
|
+
- Verify error is logged with traceId
|
|
140
|
+
- Verify `/debugLocal/{traceId}` endpoint works
|
|
141
|
+
|
|
142
|
+
## Pattern 1: Global Error Handler (GOOD)
|
|
143
|
+
|
|
144
|
+
### Server-Side: WebpiecesMiddleware
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// packages/http/http-server/src/WebpiecesMiddleware.ts
|
|
148
|
+
@provideSingleton()
|
|
149
|
+
@injectable()
|
|
150
|
+
export class WebpiecesMiddleware {
|
|
151
|
+
async globalErrorHandler(
|
|
152
|
+
req: Request,
|
|
153
|
+
res: Response,
|
|
154
|
+
next: NextFunction
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
console.log('[GlobalErrorHandler] Request START:', req.method, req.path);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Await catches BOTH sync throws AND rejected promises
|
|
160
|
+
await next();
|
|
161
|
+
console.log('[GlobalErrorHandler] Request END (success)');
|
|
162
|
+
} catch (err: unknown) {
|
|
163
|
+
const error = toError(err);
|
|
164
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
165
|
+
|
|
166
|
+
// Log with traceId for /debugLocal lookup
|
|
167
|
+
console.error('[GlobalErrorHandler] ERROR:', {
|
|
168
|
+
traceId,
|
|
169
|
+
message: error.message,
|
|
170
|
+
stack: error.stack,
|
|
171
|
+
path: req.path,
|
|
172
|
+
method: req.method,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Store error for /debugLocal/{id} endpoint
|
|
176
|
+
ErrorStore.save(traceId, error);
|
|
177
|
+
|
|
178
|
+
if (!res.headersSent) {
|
|
179
|
+
res.status(500).send(`
|
|
180
|
+
<!DOCTYPE html>
|
|
181
|
+
<html>
|
|
182
|
+
<head><title>Server Error</title></head>
|
|
183
|
+
<body>
|
|
184
|
+
<h1>Server Error</h1>
|
|
185
|
+
<p>An error occurred. Reference ID: ${traceId}</p>
|
|
186
|
+
<p>Contact support with this ID to investigate.</p>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Adding TraceId: ContextFilter
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// packages/http/http-server/src/filters/ContextFilter.ts
|
|
200
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
201
|
+
|
|
202
|
+
@provideSingleton()
|
|
203
|
+
@injectable()
|
|
204
|
+
export class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {
|
|
205
|
+
async filter(
|
|
206
|
+
meta: MethodMeta,
|
|
207
|
+
nextFilter: Service<MethodMeta, WpResponse<unknown>>
|
|
208
|
+
): Promise<WpResponse<unknown>> {
|
|
209
|
+
return RequestContext.run(async () => {
|
|
210
|
+
// Generate unique traceId for this request
|
|
211
|
+
const traceId = uuidv4();
|
|
212
|
+
RequestContext.put('TRACE_ID', traceId);
|
|
213
|
+
RequestContext.put('METHOD_META', meta);
|
|
214
|
+
RequestContext.put('REQUEST_PATH', meta.path);
|
|
215
|
+
|
|
216
|
+
return await nextFilter.invoke(meta);
|
|
217
|
+
// RequestContext auto-cleared when done
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Pattern 2: Debug Endpoints (GOOD)
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Example debug endpoint for local development
|
|
227
|
+
@provideSingleton()
|
|
228
|
+
@Controller()
|
|
229
|
+
export class DebugController implements DebugApi {
|
|
230
|
+
@Get()
|
|
231
|
+
@Path('/debugLocal/:id')
|
|
232
|
+
async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {
|
|
233
|
+
const error = ErrorStore.get(id);
|
|
234
|
+
if (!error) {
|
|
235
|
+
throw new HttpNotFoundError(`Error ${id} not found`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
traceId: id,
|
|
240
|
+
message: error.message,
|
|
241
|
+
stack: error.stack,
|
|
242
|
+
timestamp: error.timestamp,
|
|
243
|
+
requestPath: error.requestPath,
|
|
244
|
+
requestMethod: error.requestMethod,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ErrorStore singleton (in-memory for local, Redis for production)
|
|
250
|
+
class ErrorStoreImpl {
|
|
251
|
+
private errors = new Map<string, ErrorRecord>();
|
|
252
|
+
|
|
253
|
+
save(traceId: string, error: Error): void {
|
|
254
|
+
this.errors.set(traceId, {
|
|
255
|
+
traceId,
|
|
256
|
+
message: error.message,
|
|
257
|
+
stack: error.stack,
|
|
258
|
+
timestamp: new Date(),
|
|
259
|
+
requestPath: RequestContext.get('REQUEST_PATH'),
|
|
260
|
+
requestMethod: RequestContext.get('HTTP_METHOD'),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get(traceId: string): ErrorRecord | undefined {
|
|
265
|
+
return this.errors.get(traceId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const ErrorStore = new ErrorStoreImpl();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Examples
|
|
273
|
+
|
|
274
|
+
### BAD Example 1: Local Try-Catch That Swallows Error
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// BAD: Error is swallowed, no traceId in logs
|
|
278
|
+
async function processOrder(order: Order): Promise<void> {
|
|
279
|
+
try {
|
|
280
|
+
await validateOrder(order);
|
|
281
|
+
await saveToDatabase(order);
|
|
282
|
+
} catch (err: unknown) {
|
|
283
|
+
// Error disappears into void - debugging nightmare!
|
|
284
|
+
console.log('Order processing failed');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Problem**: When this fails in production, you have:
|
|
290
|
+
- No traceId to look up the error
|
|
291
|
+
- No stack trace
|
|
292
|
+
- No request context
|
|
293
|
+
- No way to investigate
|
|
294
|
+
|
|
295
|
+
### BAD Example 2: Try-Catch With Custom Error (No TraceId)
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// BAD: Shows custom message but loses traceId
|
|
299
|
+
async function fetchUserData(userId: string): Promise<User> {
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
302
|
+
return await response.json();
|
|
303
|
+
} catch (err: unknown) {
|
|
304
|
+
const error = toError(err);
|
|
305
|
+
// Custom message without traceId
|
|
306
|
+
throw new Error(`Failed to fetch user ${userId}: ${error.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Problem**:
|
|
312
|
+
- Original error context is lost
|
|
313
|
+
- No traceId attached to new error
|
|
314
|
+
- Global handler receives generic error, can't trace root cause
|
|
315
|
+
|
|
316
|
+
### GOOD Example 1: Let Error Bubble
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// GOOD: Error bubbles to global handler with traceId
|
|
320
|
+
async function processOrder(order: Order): Promise<void> {
|
|
321
|
+
// No try-catch needed!
|
|
322
|
+
await validateOrder(order);
|
|
323
|
+
await saveToDatabase(order);
|
|
324
|
+
// If error occurs, it bubbles with traceId intact
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Why GOOD**:
|
|
329
|
+
- Global handler catches error
|
|
330
|
+
- TraceId from RequestContext is preserved
|
|
331
|
+
- Full stack trace available
|
|
332
|
+
- `/debugLocal/{traceId}` endpoint works
|
|
333
|
+
|
|
334
|
+
### GOOD Example 2: Global Handler Logs With TraceId
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// GOOD: Global handler has full context
|
|
338
|
+
// In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)
|
|
339
|
+
catch (err: unknown) {
|
|
340
|
+
const error = toError(err);
|
|
341
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
342
|
+
|
|
343
|
+
console.error('[GlobalErrorHandler] ERROR:', {
|
|
344
|
+
traceId, // Unique ID for this request
|
|
345
|
+
message: error.message,
|
|
346
|
+
stack: error.stack,
|
|
347
|
+
path: req.path, // Request context preserved
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Why GOOD**:
|
|
353
|
+
- TraceId logged with every error
|
|
354
|
+
- Full request context available
|
|
355
|
+
- Error stored for `/debugLocal/{id}` lookup
|
|
356
|
+
- DevOps can trace distributed requests
|
|
357
|
+
|
|
358
|
+
### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// ACCEPTABLE: Retry pattern requires try-catch
|
|
362
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff
|
|
363
|
+
async function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {
|
|
364
|
+
const maxRetries = 3;
|
|
365
|
+
let lastError: Error | undefined;
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
368
|
+
try {
|
|
369
|
+
return await vendorApi.call(request);
|
|
370
|
+
} catch (err: unknown) {
|
|
371
|
+
const error = toError(err);
|
|
372
|
+
lastError = error;
|
|
373
|
+
console.warn(`Retry ${i + 1}/${maxRetries} failed:`, error.message);
|
|
374
|
+
await sleep(1000 * Math.pow(2, i)); // Exponential backoff
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// After retries exhausted, throw with traceId
|
|
379
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
380
|
+
throw new HttpVendorError(
|
|
381
|
+
`Vendor API failed after ${maxRetries} retries. TraceId: ${traceId}`,
|
|
382
|
+
lastError
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Why ACCEPTABLE**:
|
|
388
|
+
- Legitimate use case: retry logic
|
|
389
|
+
- Final error still includes traceId
|
|
390
|
+
- Error still bubbles to global handler
|
|
391
|
+
- Requires senior developer approval (enforced by PR review)
|
|
392
|
+
|
|
393
|
+
### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
// ACCEPTABLE: Batching requires try-catch to continue processing
|
|
397
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures
|
|
398
|
+
async function processBatch(items: Item[]): Promise<BatchResult> {
|
|
399
|
+
const results: ItemResult[] = [];
|
|
400
|
+
const errors: ItemError[] = [];
|
|
401
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
402
|
+
|
|
403
|
+
for (const item of items) {
|
|
404
|
+
try {
|
|
405
|
+
const result = await processItem(item);
|
|
406
|
+
results.push(result);
|
|
407
|
+
} catch (err: unknown) {
|
|
408
|
+
const error = toError(err);
|
|
409
|
+
// Log individual error with traceId
|
|
410
|
+
console.error(`[Batch] Item ${item.id} failed (traceId: ${traceId}):`, error);
|
|
411
|
+
errors.push({ itemId: item.id, error: error.message, traceId });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Return both successes and failures
|
|
416
|
+
return {
|
|
417
|
+
traceId,
|
|
418
|
+
successCount: results.length,
|
|
419
|
+
failureCount: errors.length,
|
|
420
|
+
results,
|
|
421
|
+
errors,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Why ACCEPTABLE**:
|
|
427
|
+
- Legitimate use case: partial failure handling
|
|
428
|
+
- Each error logged with traceId
|
|
429
|
+
- Batch traceId included in response
|
|
430
|
+
- Requires senior developer approval (enforced by PR review)
|
|
431
|
+
|
|
432
|
+
### UNACCEPTABLE Example: Pointless Try-Catch-Rethrow (Internal Code)
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// UNACCEPTABLE: Pointless try-catch in INTERNAL code
|
|
436
|
+
async function saveUser(user: User): Promise<void> {
|
|
437
|
+
try {
|
|
438
|
+
await userRepository.save(user); // Internal call, not edge
|
|
439
|
+
} catch (err: unknown) {
|
|
440
|
+
const error = toError(err);
|
|
441
|
+
console.error('Save failed:', error);
|
|
442
|
+
throw error; // No value added - why catch?
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Why UNACCEPTABLE for internal code**:
|
|
448
|
+
- Adds no value - logging should be in LogApiFilter
|
|
449
|
+
- Global handler already logs errors
|
|
450
|
+
- Just adds noise and confusion
|
|
451
|
+
- Remove the try-catch entirely!
|
|
452
|
+
|
|
453
|
+
**CONTRAST with edge code (ACCEPTABLE)**:
|
|
454
|
+
```typescript
|
|
455
|
+
// ACCEPTABLE: Edge code calling external database service
|
|
456
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Edge code: database logging
|
|
457
|
+
async function saveUserToDb(user: User): Promise<void> {
|
|
458
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
459
|
+
try {
|
|
460
|
+
logRequest('[DB] Saving user', { traceId, userId: user.id });
|
|
461
|
+
await externalDbClient.save('users', user); // EDGE: external service
|
|
462
|
+
logSuccess('[DB] User saved', { traceId, userId: user.id });
|
|
463
|
+
} catch (err: unknown) {
|
|
464
|
+
const error = toError(err);
|
|
465
|
+
logFailure('[DB] Save failed', { traceId, userId: user.id, error: error.message });
|
|
466
|
+
throw error; // Rethrow - logging value at the edge
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**The difference**: Edge code benefits from request/response/failure logging at the service boundary. Internal code does not.
|
|
472
|
+
|
|
473
|
+
## When eslint-disable IS Acceptable
|
|
474
|
+
|
|
475
|
+
You may use `// eslint-disable-next-line @webpieces/no-unmanaged-exceptions` ONLY for:
|
|
476
|
+
|
|
477
|
+
1. **Retry loops** with exponential backoff (vendor API calls)
|
|
478
|
+
2. **Batching patterns** where partial failure is expected
|
|
479
|
+
3. **Resource cleanup** with explicit approval
|
|
480
|
+
4. **Global error handler entry points** (see below)
|
|
481
|
+
5. **Edge code patterns** for vendor/external service calls (see below)
|
|
482
|
+
6. **Form error handling** - catching `HttpUserError` for display, rethrowing others (see below)
|
|
483
|
+
|
|
484
|
+
All require:
|
|
485
|
+
- Comment explaining WHY try-catch is needed
|
|
486
|
+
- TraceId must still be logged/included in final error (or error must be rethrown)
|
|
487
|
+
|
|
488
|
+
## Global Try-Catch Entry Points (MUST ASK USER)
|
|
489
|
+
|
|
490
|
+
**CRITICAL: 95% of the time, the code you're looking at is NOT a global entry point!**
|
|
491
|
+
|
|
492
|
+
Before adding a global try-catch, **AI agents MUST ask the user**: "I think this code is the entry point where we need a global try-catch block. Is this correct?"
|
|
493
|
+
|
|
494
|
+
### Examples of LEGITIMATE Global Error Handlers
|
|
495
|
+
|
|
496
|
+
These are the rare places where global try-catch IS correct:
|
|
497
|
+
|
|
498
|
+
1. **Node.js/Express middleware** (at the TOP, after setting up traceId in context):
|
|
499
|
+
```typescript
|
|
500
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Global error handler entry point
|
|
501
|
+
app.use(async (req, res, next) => {
|
|
502
|
+
// First: set up traceId in RequestContext
|
|
503
|
+
const traceId = uuidv4();
|
|
504
|
+
RequestContext.put('TRACE_ID', traceId);
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
await next();
|
|
508
|
+
} catch (err: unknown) {
|
|
509
|
+
const error = toError(err);
|
|
510
|
+
// Report to Sentry/observability
|
|
511
|
+
Sentry.captureException(error, { extra: { traceId } });
|
|
512
|
+
res.status(500).json({ error: 'Internal error', traceId });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
2. **RxJS global error handler**:
|
|
518
|
+
```typescript
|
|
519
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- RxJS global unhandled error hook
|
|
520
|
+
config.onUnhandledError = (err: any) => {
|
|
521
|
+
const error = toError(err);
|
|
522
|
+
Sentry.captureException(error);
|
|
523
|
+
console.error('[RxJS Unhandled]', error);
|
|
524
|
+
};
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
3. **Browser window unhandled promise rejection**:
|
|
528
|
+
```typescript
|
|
529
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Browser global unhandled promise handler
|
|
530
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
531
|
+
const error = toError(event.reason);
|
|
532
|
+
Sentry.captureException(error);
|
|
533
|
+
console.error('[Unhandled Promise]', error);
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
4. **Angular ErrorHandler** (may need try-catch to prevent double recording):
|
|
538
|
+
```typescript
|
|
539
|
+
@Injectable()
|
|
540
|
+
export class GlobalErrorHandler implements ErrorHandler {
|
|
541
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Angular global error handler
|
|
542
|
+
handleError(error: any): void {
|
|
543
|
+
try {
|
|
544
|
+
Sentry.captureException(error);
|
|
545
|
+
} catch (sentryError) {
|
|
546
|
+
// Prevent infinite loop if Sentry itself fails
|
|
547
|
+
console.error('[Sentry failed]', sentryError);
|
|
548
|
+
}
|
|
549
|
+
console.error('[Angular Error]', error);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
5. **3rd party vendor event listeners**:
|
|
555
|
+
```typescript
|
|
556
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Vendor callback error boundary
|
|
557
|
+
vendorSdk.on('event', async (data) => {
|
|
558
|
+
try {
|
|
559
|
+
await processVendorEvent(data);
|
|
560
|
+
} catch (err: unknown) {
|
|
561
|
+
const error = toError(err);
|
|
562
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
563
|
+
Sentry.captureException(error, { extra: { traceId, vendorData: data } });
|
|
564
|
+
// Don't rethrow - vendor SDK may not handle errors gracefully
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**All global handlers should report to observability (Sentry, Datadog, etc.) in production.**
|
|
570
|
+
|
|
571
|
+
## Edge Code Patterns (MUST ASK USER)
|
|
572
|
+
|
|
573
|
+
Edge code is code that interacts with external systems (vendors, APIs, databases, email services, etc.). These often benefit from a try-catch pattern for **logging the full request/response cycle**.
|
|
574
|
+
|
|
575
|
+
**AI agents MUST ask the user** before adding edge code try-catch: "This looks like edge code calling an external service. Should I add request/response logging with try-catch?"
|
|
576
|
+
|
|
577
|
+
### Example: sendMail Pattern
|
|
578
|
+
```typescript
|
|
579
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Edge code: external email service logging
|
|
580
|
+
async function sendMail(request: MailRequest): Promise<MailResponse> {
|
|
581
|
+
const traceId = RequestContext.get<string>('TRACE_ID');
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
logRequest('[Email] Sending', { traceId, to: request.to, subject: request.subject });
|
|
585
|
+
const response = await emailService.send(request);
|
|
586
|
+
logSuccess('[Email] Sent', { traceId, messageId: response.messageId });
|
|
587
|
+
return response;
|
|
588
|
+
} catch (err: unknown) {
|
|
589
|
+
const error = toError(err);
|
|
590
|
+
logFailure('[Email] Failed', { traceId, error: error.message, to: request.to });
|
|
591
|
+
throw error; // Rethrow - adds logging value at the edge
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Why This Pattern Is Valuable at Edges
|
|
597
|
+
|
|
598
|
+
1. **Complete audit trail**: Request logged, then either success OR failure logged
|
|
599
|
+
2. **Vendor debugging**: When vendor says "we never received it", you have proof
|
|
600
|
+
3. **Performance monitoring**: Track timing at service boundaries
|
|
601
|
+
4. **Correlation**: TraceId connects this edge call to the overall request
|
|
602
|
+
|
|
603
|
+
### Where Edge Code Patterns Apply
|
|
604
|
+
|
|
605
|
+
- HTTP client calls to external APIs
|
|
606
|
+
- Database operations (especially writes)
|
|
607
|
+
- Message queue publish/consume
|
|
608
|
+
- Email/SMS/notification services
|
|
609
|
+
- Payment gateway calls
|
|
610
|
+
- File storage operations (S3, GCS, etc.)
|
|
611
|
+
- Any call leaving your service boundary
|
|
612
|
+
|
|
613
|
+
## Form Error Handling Pattern (Client-Side)
|
|
614
|
+
|
|
615
|
+
Frontend forms often need to catch user-facing errors (like validation errors) to display in the UI, while rethrowing unexpected errors to the global handler.
|
|
616
|
+
|
|
617
|
+
**This pattern is ACCEPTABLE because**:
|
|
618
|
+
- It catches ONLY user-facing errors (`HttpUserError`) for display
|
|
619
|
+
- Unexpected errors are RETHROWN (not swallowed)
|
|
620
|
+
- Server throws `HttpUserError` → protocol translates to error payload → client translates back to exception
|
|
621
|
+
|
|
622
|
+
### Example: Form Submission Error Handling
|
|
623
|
+
```typescript
|
|
624
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Form error display: catch user errors, rethrow others
|
|
625
|
+
async submitForm(): Promise<void> {
|
|
626
|
+
try {
|
|
627
|
+
await this.apiClient.saveData(this.formData);
|
|
628
|
+
this.router.navigate(['/success']);
|
|
629
|
+
} catch (err: unknown) {
|
|
630
|
+
const error = toError(err);
|
|
631
|
+
|
|
632
|
+
if (error instanceof HttpUserError) {
|
|
633
|
+
// User-facing error - display in form
|
|
634
|
+
this.formError = error.message;
|
|
635
|
+
this.cdr.detectChanges();
|
|
636
|
+
} else {
|
|
637
|
+
// Unexpected error - let global handler deal with it
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Why This Pattern Is Valid
|
|
645
|
+
|
|
646
|
+
1. **Selective catching**: Only catches errors meant for user display
|
|
647
|
+
2. **No swallowing**: Unexpected errors bubble to global handler with traceId
|
|
648
|
+
3. **Protocol design**: Server intentionally throws `HttpUserError` for user-facing messages
|
|
649
|
+
4. **UX requirement**: Forms must show validation errors inline, not via global error page
|
|
650
|
+
|
|
651
|
+
### Key Requirements
|
|
652
|
+
|
|
653
|
+
- **ONLY catch specific error types** (e.g., `HttpUserError`, `ValidationError`)
|
|
654
|
+
- **ALWAYS rethrow** errors that aren't user-facing
|
|
655
|
+
- The server-side code should throw `HttpUserError` for user-displayable messages
|
|
656
|
+
|
|
657
|
+
## How to Request Approval
|
|
658
|
+
|
|
659
|
+
If you believe you have a legitimate use case for try-catch:
|
|
660
|
+
|
|
661
|
+
1. **Add a comment** explaining why it's needed:
|
|
662
|
+
```typescript
|
|
663
|
+
// JUSTIFICATION: Vendor API requires retry loop with exponential backoff
|
|
664
|
+
// to handle rate limiting. Final error includes traceId for debugging.
|
|
665
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
2. **Ensure traceId is preserved** in final error or logged
|
|
669
|
+
|
|
670
|
+
3. **Request PR review** from senior developer
|
|
671
|
+
|
|
672
|
+
4. **Be prepared to justify** - 99% of try-catch can be removed
|
|
673
|
+
|
|
674
|
+
## Summary
|
|
675
|
+
|
|
676
|
+
**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.
|
|
677
|
+
|
|
678
|
+
**Key takeaways**:
|
|
679
|
+
- Global error handler with traceId = debuggable production issues
|
|
680
|
+
- Local try-catch in internal code = lost context and debugging nightmares
|
|
681
|
+
- 95% of try-catch blocks can be removed safely
|
|
682
|
+
- Acceptable try-catch uses: retries, batching, global entry points, edge code
|
|
683
|
+
- **AI agents MUST ask user** before adding global try-catch or edge code patterns
|
|
684
|
+
- TraceId enables `/debugLocal/{id}` and `/debugCloud/{id}` endpoints
|
|
685
|
+
|
|
686
|
+
**Acceptable patterns (with eslint-disable)**:
|
|
687
|
+
1. **Global entry points**: Express middleware, RxJS error hooks, Angular ErrorHandler, browser unhandledrejection
|
|
688
|
+
2. **Edge code**: External API calls, database operations, email services - use logRequest/logSuccess/logFailure pattern
|
|
689
|
+
3. **Retry loops**: Vendor APIs with exponential backoff
|
|
690
|
+
4. **Batching**: Partial failure handling where processing must continue
|
|
691
|
+
5. **Form error handling**: Catch `HttpUserError` for UI display, rethrow all other errors
|
|
692
|
+
|
|
693
|
+
**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.
|
|
694
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# AI Agent Instructions: File Too Long
|
|
2
|
+
|
|
3
|
+
**READ THIS FILE to fix files that are too long**
|
|
4
|
+
|
|
5
|
+
## Core Principle
|
|
6
|
+
|
|
7
|
+
With **stateless systems + dependency injection, refactor is trivial**.
|
|
8
|
+
Pick a method or a few and move to new class XXXXX, then inject XXXXX
|
|
9
|
+
into all users of those methods via the constructor.
|
|
10
|
+
Delete those methods from original class.
|
|
11
|
+
|
|
12
|
+
**99% of files can be less than the configured max lines of code.**
|
|
13
|
+
|
|
14
|
+
Files should contain a SINGLE COHESIVE UNIT.
|
|
15
|
+
- One class per file (Java convention)
|
|
16
|
+
- If class is too large, extract child responsibilities
|
|
17
|
+
- Use dependency injection to compose functionality
|
|
18
|
+
|
|
19
|
+
## Command: Reduce File Size
|
|
20
|
+
|
|
21
|
+
### Step 1: Check for Multiple Classes
|
|
22
|
+
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// BAD: UserController.ts (multiple classes)
|
|
26
|
+
export class UserController { /* ... */ }
|
|
27
|
+
export class UserValidator { /* ... */ }
|
|
28
|
+
export class UserNotifier { /* ... */ }
|
|
29
|
+
|
|
30
|
+
// GOOD: Three separate files
|
|
31
|
+
// UserController.ts
|
|
32
|
+
export class UserController { /* ... */ }
|
|
33
|
+
|
|
34
|
+
// UserValidator.ts
|
|
35
|
+
export class UserValidator { /* ... */ }
|
|
36
|
+
|
|
37
|
+
// UserNotifier.ts
|
|
38
|
+
export class UserNotifier { /* ... */ }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Step 2: Extract Child Responsibilities (if single class is too large)
|
|
42
|
+
|
|
43
|
+
#### Pattern: Create New Service Class with Dependency Injection
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// BAD: UserController.ts (800 lines, single class)
|
|
47
|
+
@provideSingleton()
|
|
48
|
+
@Controller()
|
|
49
|
+
export class UserController {
|
|
50
|
+
// 200 lines: CRUD operations
|
|
51
|
+
// 300 lines: validation logic
|
|
52
|
+
// 200 lines: notification logic
|
|
53
|
+
// 100 lines: analytics logic
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// GOOD: Extract validation service
|
|
57
|
+
// 1. Create UserValidationService.ts
|
|
58
|
+
@provideSingleton()
|
|
59
|
+
export class UserValidationService {
|
|
60
|
+
validateUserData(data: UserData): ValidationResult {
|
|
61
|
+
// 300 lines of validation logic moved here
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
validateEmail(email: string): boolean { /* ... */ }
|
|
65
|
+
validatePassword(password: string): boolean { /* ... */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Inject into UserController.ts
|
|
69
|
+
@provideSingleton()
|
|
70
|
+
@Controller()
|
|
71
|
+
export class UserController {
|
|
72
|
+
constructor(
|
|
73
|
+
@inject(TYPES.UserValidationService)
|
|
74
|
+
private validator: UserValidationService
|
|
75
|
+
) {}
|
|
76
|
+
|
|
77
|
+
async createUser(data: UserData): Promise<User> {
|
|
78
|
+
const validation = this.validator.validateUserData(data);
|
|
79
|
+
if (!validation.isValid) {
|
|
80
|
+
throw new ValidationError(validation.errors);
|
|
81
|
+
}
|
|
82
|
+
// ... rest of logic
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## AI Agent Action Steps
|
|
88
|
+
|
|
89
|
+
1. **ANALYZE** the file structure:
|
|
90
|
+
- Count classes (if >1, separate immediately)
|
|
91
|
+
- Identify logical responsibilities within single class
|
|
92
|
+
|
|
93
|
+
2. **IDENTIFY** "child code" to extract:
|
|
94
|
+
- Validation logic -> ValidationService
|
|
95
|
+
- Notification logic -> NotificationService
|
|
96
|
+
- Data transformation -> TransformerService
|
|
97
|
+
- External API calls -> ApiService
|
|
98
|
+
- Business rules -> RulesEngine
|
|
99
|
+
|
|
100
|
+
3. **CREATE** new service file(s):
|
|
101
|
+
- Start with temporary name: `XXXX.ts` or `ChildService.ts`
|
|
102
|
+
- Add `@provideSingleton()` decorator
|
|
103
|
+
- Move child methods to new class
|
|
104
|
+
|
|
105
|
+
4. **UPDATE** dependency injection:
|
|
106
|
+
- Add to `TYPES` constants (if using symbol-based DI)
|
|
107
|
+
- Inject new service into original class constructor
|
|
108
|
+
- Replace direct method calls with `this.serviceName.method()`
|
|
109
|
+
|
|
110
|
+
5. **RENAME** extracted file:
|
|
111
|
+
- Read the extracted code to understand its purpose
|
|
112
|
+
- Rename `XXXX.ts` to logical name (e.g., `UserValidationService.ts`)
|
|
113
|
+
|
|
114
|
+
6. **VERIFY** file sizes:
|
|
115
|
+
- Original file should now be under the limit
|
|
116
|
+
- Each extracted file should be under the limit
|
|
117
|
+
- If still too large, extract more services
|
|
118
|
+
|
|
119
|
+
## Examples of Child Responsibilities to Extract
|
|
120
|
+
|
|
121
|
+
| If File Contains | Extract To | Pattern |
|
|
122
|
+
|-----------------|------------|---------|
|
|
123
|
+
| Validation logic (200+ lines) | `XValidator.ts` or `XValidationService.ts` | Singleton service |
|
|
124
|
+
| Notification logic (150+ lines) | `XNotifier.ts` or `XNotificationService.ts` | Singleton service |
|
|
125
|
+
| Data transformation (200+ lines) | `XTransformer.ts` | Singleton service |
|
|
126
|
+
| External API calls (200+ lines) | `XApiClient.ts` | Singleton service |
|
|
127
|
+
| Complex business rules (300+ lines) | `XRulesEngine.ts` | Singleton service |
|
|
128
|
+
| Database queries (200+ lines) | `XRepository.ts` | Singleton service |
|
|
129
|
+
|
|
130
|
+
## WebPieces Dependency Injection Pattern
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// 1. Define service with @provideSingleton
|
|
134
|
+
import { provideSingleton } from '@webpieces/http-routing';
|
|
135
|
+
|
|
136
|
+
@provideSingleton()
|
|
137
|
+
export class MyService {
|
|
138
|
+
doSomething(): void { /* ... */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Inject into consumer
|
|
142
|
+
import { inject } from 'inversify';
|
|
143
|
+
import { TYPES } from './types';
|
|
144
|
+
|
|
145
|
+
@provideSingleton()
|
|
146
|
+
@Controller()
|
|
147
|
+
export class MyController {
|
|
148
|
+
constructor(
|
|
149
|
+
@inject(TYPES.MyService) private service: MyService
|
|
150
|
+
) {}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Escape Hatch
|
|
155
|
+
|
|
156
|
+
If refactoring is genuinely not feasible (generated files, complex algorithms, etc.),
|
|
157
|
+
add a disable comment at the TOP of the file (within first 5 lines) with a DATE:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// webpieces-disable max-lines-modified-files 2025/01/15 -- Complex generated file, refactoring would break generation
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**IMPORTANT**: The date format is yyyy/mm/dd. The disable will EXPIRE after 1 month from this date.
|
|
164
|
+
After expiration, you must either fix the file or update the date to get another month.
|
|
165
|
+
This ensures that disable comments are reviewed periodically.
|
|
166
|
+
|
|
167
|
+
For ESLint-enforced file size limits, use:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// eslint-disable-next-line @webpieces/max-file-lines
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
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.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# AI Agent Instructions: Method Too Long
|
|
2
|
+
|
|
3
|
+
**READ THIS FILE to fix methods that are too long**
|
|
4
|
+
|
|
5
|
+
## Core Principle
|
|
6
|
+
Every method should read like a TABLE OF CONTENTS of a book.
|
|
7
|
+
- Each method call is a "chapter"
|
|
8
|
+
- When you dive into a method, you find another table of contents
|
|
9
|
+
- Keeping methods under 70 lines is achievable with proper extraction
|
|
10
|
+
|
|
11
|
+
## Command: Extract Code into Named Methods
|
|
12
|
+
|
|
13
|
+
### Pattern 1: Extract Loop Bodies
|
|
14
|
+
```typescript
|
|
15
|
+
// BAD: 50 lines embedded in loop
|
|
16
|
+
for (const order of orders) {
|
|
17
|
+
// 20 lines of validation logic
|
|
18
|
+
// 15 lines of processing logic
|
|
19
|
+
// 10 lines of notification logic
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// GOOD: Extracted to named methods
|
|
23
|
+
for (const order of orders) {
|
|
24
|
+
validateOrder(order);
|
|
25
|
+
processOrderItems(order);
|
|
26
|
+
sendNotifications(order);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
31
|
+
```typescript
|
|
32
|
+
// GOOD: Separates success path from error handling
|
|
33
|
+
async function handleRequest(req: Request): Promise<Response> {
|
|
34
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
35
|
+
try {
|
|
36
|
+
return await executeRequest(req);
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
const error = toError(err);
|
|
39
|
+
return createErrorResponse(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
45
|
+
```typescript
|
|
46
|
+
// GOOD: Self-documenting steps
|
|
47
|
+
function processOrder(order: Order): void {
|
|
48
|
+
validateOrderData(order);
|
|
49
|
+
calculateTotals(order);
|
|
50
|
+
applyDiscounts(order);
|
|
51
|
+
processPayment(order);
|
|
52
|
+
updateInventory(order);
|
|
53
|
+
sendConfirmation(order);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Pattern 4: Separate Data Object Creation
|
|
58
|
+
```typescript
|
|
59
|
+
// BAD: 15 lines of inline object creation
|
|
60
|
+
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
61
|
+
|
|
62
|
+
// GOOD: Extract to factory method
|
|
63
|
+
const request = createRequestObject(data);
|
|
64
|
+
doSomething(request);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Pattern 5: Extract Inline Logic to Named Functions
|
|
68
|
+
```typescript
|
|
69
|
+
// BAD: Complex inline logic
|
|
70
|
+
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
71
|
+
// 30 lines of admin logic
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// GOOD: Extract to named methods
|
|
75
|
+
if (isAdminWithWriteAccess(user)) {
|
|
76
|
+
performAdminOperation(user);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## AI Agent Action Steps
|
|
81
|
+
|
|
82
|
+
1. **IDENTIFY** the long method in the error message
|
|
83
|
+
2. **READ** the method to understand its logical sections
|
|
84
|
+
3. **EXTRACT** logical units into separate methods with descriptive names
|
|
85
|
+
4. **REPLACE** inline code with method calls
|
|
86
|
+
5. **VERIFY** each extracted method is <70 lines
|
|
87
|
+
6. **TEST** that functionality remains unchanged
|
|
88
|
+
|
|
89
|
+
## Examples of "Logical Units" to Extract
|
|
90
|
+
- Validation logic -> `validateX()`
|
|
91
|
+
- Data transformation -> `transformXToY()`
|
|
92
|
+
- API calls -> `fetchXFromApi()`
|
|
93
|
+
- Object creation -> `createX()`
|
|
94
|
+
- Loop bodies -> `processItem()`
|
|
95
|
+
- Error handling -> `handleXError()`
|
|
96
|
+
|
|
97
|
+
Remember: Methods should read like a table of contents. Each line should be a "chapter title" (method call) that describes what happens, not how it happens.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Instructions: Method Too Long
|
|
2
|
+
|
|
3
|
+
## Requirement
|
|
4
|
+
|
|
5
|
+
**~99% of the time**, you can stay under the `limit` from nx.json
|
|
6
|
+
by extracting logical units into well-named methods.
|
|
7
|
+
Nearly all software can be written with methods under this size.
|
|
8
|
+
Take the extra time to refactor - it's worth it for long-term maintainability.
|
|
9
|
+
|
|
10
|
+
## The "Table of Contents" Principle
|
|
11
|
+
|
|
12
|
+
Good code reads like a book's table of contents:
|
|
13
|
+
- Chapter titles (method names) tell you WHAT happens
|
|
14
|
+
- Reading chapter titles gives you the full story
|
|
15
|
+
- You can dive into chapters (implementations) for details
|
|
16
|
+
|
|
17
|
+
## Why Limit Method Sizes?
|
|
18
|
+
|
|
19
|
+
Methods under reasonable limits are:
|
|
20
|
+
- Easy to review in a single screen
|
|
21
|
+
- Simple to understand without scrolling
|
|
22
|
+
- Quick for AI to analyze and suggest improvements
|
|
23
|
+
- More testable in isolation
|
|
24
|
+
- Self-documenting through well-named extracted methods
|
|
25
|
+
|
|
26
|
+
## Gradual Cleanup Strategy
|
|
27
|
+
|
|
28
|
+
This codebase uses a gradual cleanup approach:
|
|
29
|
+
- **New methods**: Must be under `limit` from nx.json
|
|
30
|
+
- **Modified methods**: Must be under `limit` from nx.json
|
|
31
|
+
- **Untouched methods**: No limit (legacy code is allowed until touched)
|
|
32
|
+
|
|
33
|
+
## How to Refactor
|
|
34
|
+
|
|
35
|
+
Instead of:
|
|
36
|
+
```typescript
|
|
37
|
+
async processOrder(order: Order): Promise<Result> {
|
|
38
|
+
// 100 lines of validation, transformation, saving, notifications...
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Write:
|
|
43
|
+
```typescript
|
|
44
|
+
async processOrder(order: Order): Promise<Result> {
|
|
45
|
+
const validated = this.validateOrder(order);
|
|
46
|
+
const transformed = this.applyBusinessRules(validated);
|
|
47
|
+
const saved = await this.saveToDatabase(transformed);
|
|
48
|
+
await this.notifyStakeholders(saved);
|
|
49
|
+
return this.buildResult(saved);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Now the main method is a "table of contents" - each line tells part of the story!
|
|
54
|
+
|
|
55
|
+
## Patterns for Extraction
|
|
56
|
+
|
|
57
|
+
### Pattern 1: Extract Loop Bodies
|
|
58
|
+
```typescript
|
|
59
|
+
// BEFORE
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
// 20 lines of processing
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// AFTER
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
this.processItem(item);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Pattern 2: Extract Conditional Blocks
|
|
71
|
+
```typescript
|
|
72
|
+
// BEFORE
|
|
73
|
+
if (isAdmin(user)) {
|
|
74
|
+
// 15 lines of admin logic
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// AFTER
|
|
78
|
+
if (isAdmin(user)) {
|
|
79
|
+
this.handleAdminUser(user);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pattern 3: Extract Data Transformations
|
|
84
|
+
```typescript
|
|
85
|
+
// BEFORE
|
|
86
|
+
const result = {
|
|
87
|
+
// 10+ lines of object construction
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// AFTER
|
|
91
|
+
const result = this.buildResultObject(data);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## If Refactoring Is Not Feasible
|
|
95
|
+
|
|
96
|
+
Sometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).
|
|
97
|
+
|
|
98
|
+
**Escape hatch for new methods**: Add a webpieces-disable comment with justification:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// webpieces-disable max-lines-new-methods -- Complex state machine, splitting reduces clarity
|
|
102
|
+
async complexStateMachine(): Promise<void> {
|
|
103
|
+
// ... longer method with justification
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Escape hatch for modified methods**: Add a webpieces-disable comment with DATE and justification:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// webpieces-disable max-lines-modified 2025/01/15 -- Complex state machine, splitting reduces clarity
|
|
111
|
+
async complexStateMachine(): Promise<void> {
|
|
112
|
+
// ... longer method with justification
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**IMPORTANT**: The date format is yyyy/mm/dd. The disable will EXPIRE after 1 month from this date.
|
|
117
|
+
After expiration, you must either fix the method or update the date to get another month.
|
|
118
|
+
This ensures that disable comments are reviewed periodically.
|
|
119
|
+
|
|
120
|
+
## AI Agent Action Steps
|
|
121
|
+
|
|
122
|
+
1. **READ** the method to understand its logical sections
|
|
123
|
+
2. **IDENTIFY** logical units that can be extracted
|
|
124
|
+
3. **EXTRACT** into well-named private methods
|
|
125
|
+
4. **VERIFY** the main method now reads like a table of contents
|
|
126
|
+
5. **IF NOT FEASIBLE**: Add webpieces-disable comment with clear justification
|
|
127
|
+
|
|
128
|
+
## Remember
|
|
129
|
+
|
|
130
|
+
- Every method you write today will be read many times tomorrow
|
|
131
|
+
- The best code explains itself through structure
|
|
132
|
+
- When in doubt, extract and name it
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# AI Agent Instructions: Redundant Transitive Dependency Violation
|
|
2
|
+
|
|
3
|
+
**READ THIS FILE FIRST before making any changes!**
|
|
4
|
+
|
|
5
|
+
## Why This Rule Exists
|
|
6
|
+
|
|
7
|
+
This rule keeps the architecture dependency graph **CLEAN and SIMPLE**.
|
|
8
|
+
|
|
9
|
+
When you run `npx nx run architecture:visualize`, it generates a visual diagram of all
|
|
10
|
+
package dependencies. Without this rule, you end up with a tangled mess of 100+ lines
|
|
11
|
+
where everything depends on everything - making it impossible to understand.
|
|
12
|
+
|
|
13
|
+
**Clean graphs = easier understanding for humans AND AI agents.**
|
|
14
|
+
|
|
15
|
+
## Understanding the Error
|
|
16
|
+
|
|
17
|
+
You have a **redundant transitive dependency**. This means:
|
|
18
|
+
|
|
19
|
+
1. Project A directly depends on Project C
|
|
20
|
+
2. BUT Project A also depends on Project B
|
|
21
|
+
3. AND Project B already brings in Project C (transitively)
|
|
22
|
+
|
|
23
|
+
Therefore, Project A's direct dependency on C is **redundant** - it's already available
|
|
24
|
+
through B. This extra line clutters the dependency graph.
|
|
25
|
+
|
|
26
|
+
**Example:**
|
|
27
|
+
```
|
|
28
|
+
http-server depends on: [http-routing, http-filters, core-util]
|
|
29
|
+
^^^^^^^^^ ^^^^^^^^
|
|
30
|
+
REDUNDANT! REDUNDANT!
|
|
31
|
+
|
|
32
|
+
Why? Because http-routing already brings in:
|
|
33
|
+
- http-filters (direct)
|
|
34
|
+
- core-util (via http-api)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How to Fix
|
|
38
|
+
|
|
39
|
+
### Step 1: Identify the Redundant Dependency
|
|
40
|
+
|
|
41
|
+
Look at the error message. It tells you:
|
|
42
|
+
- Which project has the problem
|
|
43
|
+
- Which dependency is redundant
|
|
44
|
+
- Which other dependency already brings it in
|
|
45
|
+
|
|
46
|
+
### Step 2: Remove from project.json
|
|
47
|
+
|
|
48
|
+
Remove the redundant dependency from `build.dependsOn`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"targets": {
|
|
53
|
+
"build": {
|
|
54
|
+
"dependsOn": [
|
|
55
|
+
"^build",
|
|
56
|
+
"http-routing:build"
|
|
57
|
+
// REMOVE: "http-filters:build" <-- redundant, http-routing brings it in
|
|
58
|
+
// REMOVE: "core-util:build" <-- redundant, http-routing brings it in
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Step 3: Remove from package.json
|
|
66
|
+
|
|
67
|
+
Remove the redundant dependency from `dependencies`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@webpieces/http-routing": "*"
|
|
73
|
+
// REMOVE: "@webpieces/http-filters": "*" <-- redundant
|
|
74
|
+
// REMOVE: "@webpieces/core-util": "*" <-- redundant
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Step 4: Regenerate Architecture
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx nx run architecture:generate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Step 5: Verify
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run build-all
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Important Notes
|
|
92
|
+
|
|
93
|
+
- You DON'T lose access to the transitive dependency - it's still available through the parent
|
|
94
|
+
- This is about keeping the DECLARED dependencies minimal and clean
|
|
95
|
+
- The actual runtime/compile behavior is unchanged
|
|
96
|
+
- TypeScript will still find the types through the transitive path
|
|
97
|
+
|
|
98
|
+
## Remember
|
|
99
|
+
|
|
100
|
+
- Fewer lines in the graph = easier to understand
|
|
101
|
+
- Only declare what you DIRECTLY need that isn't already transitively available
|
|
102
|
+
- When in doubt, check with `npx nx run architecture:visualize`
|