claude-code-pilot 3.1.1 → 3.3.0
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/CHANGELOG.md +57 -0
- package/README.md +16 -11
- package/bin/install.js +127 -11
- package/manifest.json +20 -1
- package/package.json +4 -3
- package/src/agents/a11y-architect.md +141 -0
- package/src/agents/code-architect.md +71 -0
- package/src/agents/code-explorer.md +69 -0
- package/src/agents/code-simplifier.md +47 -0
- package/src/agents/comment-analyzer.md +45 -0
- package/src/agents/csharp-reviewer.md +101 -0
- package/src/agents/dart-build-resolver.md +201 -0
- package/src/agents/django-build-resolver.md +252 -0
- package/src/agents/django-reviewer.md +169 -0
- package/src/agents/fastapi-reviewer.md +79 -0
- package/src/agents/fsharp-reviewer.md +109 -0
- package/src/agents/pr-test-analyzer.md +45 -0
- package/src/agents/silent-failure-hunter.md +50 -0
- package/src/agents/swift-build-resolver.md +170 -0
- package/src/agents/swift-reviewer.md +116 -0
- package/src/agents/type-design-analyzer.md +41 -0
- package/src/available-rules/README.md +3 -1
- package/src/available-rules/dart/coding-style.md +159 -0
- package/src/available-rules/dart/hooks.md +66 -0
- package/src/available-rules/dart/patterns.md +261 -0
- package/src/available-rules/dart/security.md +135 -0
- package/src/available-rules/dart/testing.md +215 -0
- package/src/available-rules/web/coding-style.md +105 -0
- package/src/available-rules/web/design-quality.md +72 -0
- package/src/available-rules/web/hooks.md +129 -0
- package/src/available-rules/web/patterns.md +88 -0
- package/src/available-rules/web/performance.md +73 -0
- package/src/available-rules/web/security.md +66 -0
- package/src/available-rules/web/testing.md +64 -0
- package/src/commands/ccp/ai-integration-phase.md +36 -0
- package/src/commands/ccp/audit-fix.md +33 -0
- package/src/commands/ccp/code-review-fix.md +52 -0
- package/src/commands/ccp/cost-report.md +107 -0
- package/src/commands/ccp/eval-review.md +32 -0
- package/src/commands/ccp/extract_learnings.md +22 -0
- package/src/commands/ccp/import.md +37 -0
- package/src/commands/ccp/ingest-docs.md +42 -0
- package/src/commands/ccp/intel.md +179 -0
- package/src/commands/ccp/mvp-phase.md +45 -0
- package/src/commands/ccp/plan-prd.md +160 -0
- package/src/commands/ccp/plan-review-convergence.md +58 -0
- package/src/commands/ccp/pr-ecc.md +184 -0
- package/src/commands/ccp/scan.md +26 -0
- package/src/commands/ccp/security-scan.md +74 -0
- package/src/commands/ccp/sketch-wrap-up.md +31 -0
- package/src/commands/ccp/sketch.md +54 -0
- package/src/commands/ccp/spec-phase.md +62 -0
- package/src/commands/ccp/spike-wrap-up.md +31 -0
- package/src/commands/ccp/spike.md +51 -0
- package/src/commands/ccp/ultraplan-phase.md +33 -0
- package/src/hooks/ccp-bash-hook-dispatcher.js +96 -0
- package/src/hooks/ccp-context-monitor.js +23 -0
- package/src/hooks/ccp-doc-file-warning.js +93 -0
- package/src/hooks/ccp-pre-bash-dispatcher.js +24 -0
- package/src/hooks/ccp-read-injection-scanner.js +152 -0
- package/src/hooks/ccp-write-gateguard.js +868 -0
- package/src/hooks/kit-check-update.js +59 -7
- package/src/hooks/run-with-flags-shell.sh +1 -0
- package/src/hooks/run-with-flags.js +48 -1
- package/src/hooks/session-end.js +88 -1
- package/src/lib/hook-flags.js +14 -0
- package/src/lib/project-detect.js +0 -2
- package/src/lib/shell-substitution.js +499 -0
- package/src/pilot/references/agent-contracts.md +79 -0
- package/src/pilot/references/ai-evals.md +156 -0
- package/src/pilot/references/ai-frameworks.md +186 -0
- package/src/pilot/references/doc-conflict-engine.md +91 -0
- package/src/pilot/references/execute-mvp-tdd.md +81 -0
- package/src/pilot/references/gate-prompts.md +100 -0
- package/src/pilot/references/gates.md +70 -0
- package/src/pilot/references/mandatory-initial-read.md +2 -0
- package/src/pilot/references/mvp-concepts.md +49 -0
- package/src/pilot/references/planner-graphify-auto-update.md +67 -0
- package/src/pilot/references/planner-human-verify-mode.md +57 -0
- package/src/pilot/references/planner-mvp-mode.md +53 -0
- package/src/pilot/references/project-skills-discovery.md +19 -0
- package/src/pilot/references/revision-loop.md +97 -0
- package/src/pilot/references/skeleton-template.md +48 -0
- package/src/pilot/references/sketch-interactivity.md +41 -0
- package/src/pilot/references/sketch-theme-system.md +94 -0
- package/src/pilot/references/sketch-tooling.md +45 -0
- package/src/pilot/references/sketch-variant-patterns.md +81 -0
- package/src/pilot/references/spidr-splitting.md +69 -0
- package/src/pilot/references/thinking-models-debug.md +44 -0
- package/src/pilot/references/thinking-models-execution.md +50 -0
- package/src/pilot/references/thinking-models-planning.md +62 -0
- package/src/pilot/references/thinking-models-research.md +50 -0
- package/src/pilot/references/thinking-models-verification.md +55 -0
- package/src/pilot/references/user-story-template.md +58 -0
- package/src/pilot/references/verify-mvp-mode.md +85 -0
- package/src/pilot/references/worktree-path-safety.md +89 -0
- package/src/pilot/templates/AI-SPEC.md +246 -0
- package/src/pilot/templates/spec.md +307 -0
- package/src/pilot/workflows/ai-integration-phase.md +284 -0
- package/src/pilot/workflows/audit-fix.md +175 -0
- package/src/pilot/workflows/code-review-fix.md +497 -0
- package/src/pilot/workflows/eval-review.md +155 -0
- package/src/pilot/workflows/extract_learnings.md +242 -0
- package/src/pilot/workflows/help.md +5 -0
- package/src/pilot/workflows/import.md +246 -0
- package/src/pilot/workflows/ingest-docs.md +328 -0
- package/src/pilot/workflows/mvp-phase.md +199 -0
- package/src/pilot/workflows/plan-review-convergence.md +329 -0
- package/src/pilot/workflows/scan.md +102 -0
- package/src/pilot/workflows/sketch-wrap-up.md +285 -0
- package/src/pilot/workflows/sketch.md +360 -0
- package/src/pilot/workflows/spec-phase.md +262 -0
- package/src/pilot/workflows/spike-wrap-up.md +306 -0
- package/src/pilot/workflows/spike.md +452 -0
- package/src/pilot/workflows/ultraplan-phase.md +189 -0
- package/src/skills/accessibility/SKILL.md +146 -0
- package/src/skills/agent-architecture-audit/SKILL.md +256 -0
- package/src/skills/agent-eval/SKILL.md +145 -0
- package/src/skills/agent-harness-design/SKILL.md +73 -0
- package/src/skills/agent-introspection-debugging/SKILL.md +153 -0
- package/src/skills/android-clean-architecture/SKILL.md +339 -0
- package/src/skills/angular-developer/SKILL.md +154 -0
- package/src/skills/angular-developer/references/angular-animations.md +160 -0
- package/src/skills/angular-developer/references/angular-aria.md +410 -0
- package/src/skills/angular-developer/references/cli.md +86 -0
- package/src/skills/angular-developer/references/component-harnesses.md +59 -0
- package/src/skills/angular-developer/references/component-styling.md +91 -0
- package/src/skills/angular-developer/references/components.md +117 -0
- package/src/skills/angular-developer/references/creating-services.md +97 -0
- package/src/skills/angular-developer/references/data-resolvers.md +69 -0
- package/src/skills/angular-developer/references/define-routes.md +67 -0
- package/src/skills/angular-developer/references/defining-providers.md +72 -0
- package/src/skills/angular-developer/references/di-fundamentals.md +120 -0
- package/src/skills/angular-developer/references/e2e-testing.md +56 -0
- package/src/skills/angular-developer/references/effects.md +83 -0
- package/src/skills/angular-developer/references/hierarchical-injectors.md +43 -0
- package/src/skills/angular-developer/references/host-elements.md +80 -0
- package/src/skills/angular-developer/references/injection-context.md +63 -0
- package/src/skills/angular-developer/references/inputs.md +101 -0
- package/src/skills/angular-developer/references/linked-signal.md +59 -0
- package/src/skills/angular-developer/references/loading-strategies.md +61 -0
- package/src/skills/angular-developer/references/mcp.md +108 -0
- package/src/skills/angular-developer/references/navigate-to-routes.md +69 -0
- package/src/skills/angular-developer/references/outputs.md +86 -0
- package/src/skills/angular-developer/references/reactive-forms.md +122 -0
- package/src/skills/angular-developer/references/rendering-strategies.md +44 -0
- package/src/skills/angular-developer/references/resource.md +77 -0
- package/src/skills/angular-developer/references/route-animations.md +56 -0
- package/src/skills/angular-developer/references/route-guards.md +52 -0
- package/src/skills/angular-developer/references/router-lifecycle.md +45 -0
- package/src/skills/angular-developer/references/router-testing.md +87 -0
- package/src/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
- package/src/skills/angular-developer/references/signal-forms.md +795 -0
- package/src/skills/angular-developer/references/signals-overview.md +94 -0
- package/src/skills/angular-developer/references/tailwind-css.md +69 -0
- package/src/skills/angular-developer/references/template-driven-forms.md +114 -0
- package/src/skills/angular-developer/references/testing-fundamentals.md +65 -0
- package/src/skills/api-connector-builder/SKILL.md +120 -0
- package/src/skills/code-tour/SKILL.md +236 -0
- package/src/skills/compose-multiplatform-patterns/SKILL.md +299 -0
- package/src/skills/csharp-testing/SKILL.md +321 -0
- package/src/skills/dart-flutter-patterns/SKILL.md +563 -0
- package/src/skills/dashboard-builder/SKILL.md +108 -0
- package/src/skills/dotnet-patterns/SKILL.md +321 -0
- package/src/skills/error-handling/SKILL.md +376 -0
- package/src/skills/fastapi-patterns/SKILL.md +327 -0
- package/src/skills/flox-environments/SKILL.md +496 -0
- package/src/skills/frontend-design/SKILL.md +145 -0
- package/src/skills/frontend-slides/SKILL.md +184 -0
- package/src/skills/frontend-slides/STYLE_PRESETS.md +330 -0
- package/src/skills/fsharp-testing/SKILL.md +280 -0
- package/src/skills/gateguard/SKILL.md +121 -0
- package/src/skills/github-ops/SKILL.md +144 -0
- package/src/skills/hookify-rules/SKILL.md +128 -0
- package/src/skills/ios-icon-gen/SKILL.md +157 -0
- package/src/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
- package/src/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
- package/src/skills/knowledge-ops/SKILL.md +154 -0
- package/src/skills/liquid-glass-design/SKILL.md +279 -0
- package/src/skills/make-interfaces-feel-better/SKILL.md +151 -0
- package/src/skills/mysql-patterns/SKILL.md +412 -0
- package/src/skills/nestjs-patterns/SKILL.md +230 -0
- package/src/skills/plan-orchestrate/SKILL.md +220 -0
- package/src/skills/prisma-patterns/SKILL.md +371 -0
- package/src/skills/production-audit/SKILL.md +206 -0
- package/src/skills/security-bounty-hunter/SKILL.md +99 -0
- package/src/skills/security-scan/references/agentshield-policy-exception/candidate-playbook.md +49 -0
- package/src/skills/security-scan/references/agentshield-policy-exception/report.json +35 -0
- package/src/skills/security-scan/references/agentshield-policy-exception/scenario.json +62 -0
- package/src/skills/security-scan/references/agentshield-policy-exception/trace.json +45 -0
- package/src/skills/security-scan/references/agentshield-policy-exception/verifier-result.json +35 -0
- package/src/skills/swift-actor-persistence/SKILL.md +143 -0
- package/src/skills/swift-protocol-di-testing/SKILL.md +190 -0
- package/src/skills/swiftui-patterns/SKILL.md +259 -0
- package/src/skills/terminal-ops/SKILL.md +109 -0
- package/src/skills/ui-demo/SKILL.md +465 -0
- package/src/skills/vite-patterns/SKILL.md +449 -0
- package/src/skills/windows-desktop-e2e/SKILL.md +887 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dotnet-patterns
|
|
3
|
+
description: Idiomatic C# and .NET patterns, conventions, dependency injection, async/await, and best practices for building robust, maintainable .NET applications.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# .NET Development Patterns
|
|
8
|
+
|
|
9
|
+
Idiomatic C# and .NET patterns for building robust, performant, and maintainable applications.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Writing new C# code
|
|
14
|
+
- Reviewing C# code
|
|
15
|
+
- Refactoring existing .NET applications
|
|
16
|
+
- Designing service architectures with ASP.NET Core
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
### 1. Prefer Immutability
|
|
21
|
+
|
|
22
|
+
Use records and init-only properties for data models. Mutability should be an explicit, justified choice.
|
|
23
|
+
|
|
24
|
+
```csharp
|
|
25
|
+
// Good: Immutable value object
|
|
26
|
+
public sealed record Money(decimal Amount, string Currency);
|
|
27
|
+
|
|
28
|
+
// Good: Immutable DTO with init setters
|
|
29
|
+
public sealed class CreateOrderRequest
|
|
30
|
+
{
|
|
31
|
+
public required string CustomerId { get; init; }
|
|
32
|
+
public required IReadOnlyList<OrderItem> Items { get; init; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Bad: Mutable model with public setters
|
|
36
|
+
public class Order
|
|
37
|
+
{
|
|
38
|
+
public string CustomerId { get; set; }
|
|
39
|
+
public List<OrderItem> Items { get; set; }
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Explicit Over Implicit
|
|
44
|
+
|
|
45
|
+
Be clear about nullability, access modifiers, and intent.
|
|
46
|
+
|
|
47
|
+
```csharp
|
|
48
|
+
// Good: Explicit access modifiers and nullability
|
|
49
|
+
public sealed class UserService
|
|
50
|
+
{
|
|
51
|
+
private readonly IUserRepository _repository;
|
|
52
|
+
private readonly ILogger<UserService> _logger;
|
|
53
|
+
|
|
54
|
+
public UserService(IUserRepository repository, ILogger<UserService> logger)
|
|
55
|
+
{
|
|
56
|
+
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
57
|
+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async Task<User?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
|
|
61
|
+
{
|
|
62
|
+
return await _repository.FindByIdAsync(id, cancellationToken);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Depend on Abstractions
|
|
68
|
+
|
|
69
|
+
Use interfaces for service boundaries. Register via DI container.
|
|
70
|
+
|
|
71
|
+
```csharp
|
|
72
|
+
// Good: Interface-based dependency
|
|
73
|
+
public interface IOrderRepository
|
|
74
|
+
{
|
|
75
|
+
Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
|
|
76
|
+
Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId, CancellationToken cancellationToken);
|
|
77
|
+
Task AddAsync(Order order, CancellationToken cancellationToken);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Registration
|
|
81
|
+
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Async/Await Patterns
|
|
85
|
+
|
|
86
|
+
### Proper Async Usage
|
|
87
|
+
|
|
88
|
+
```csharp
|
|
89
|
+
// Good: Async all the way, with CancellationToken
|
|
90
|
+
public async Task<OrderSummary> GetOrderSummaryAsync(
|
|
91
|
+
Guid orderId,
|
|
92
|
+
CancellationToken cancellationToken)
|
|
93
|
+
{
|
|
94
|
+
var order = await _repository.FindByIdAsync(orderId, cancellationToken)
|
|
95
|
+
?? throw new NotFoundException($"Order {orderId} not found");
|
|
96
|
+
|
|
97
|
+
var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken);
|
|
98
|
+
|
|
99
|
+
return new OrderSummary(order, customer);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Bad: Blocking on async
|
|
103
|
+
public OrderSummary GetOrderSummary(Guid orderId)
|
|
104
|
+
{
|
|
105
|
+
var order = _repository.FindByIdAsync(orderId, CancellationToken.None).Result; // Deadlock risk
|
|
106
|
+
return new OrderSummary(order);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Parallel Async Operations
|
|
111
|
+
|
|
112
|
+
```csharp
|
|
113
|
+
// Good: Concurrent independent operations
|
|
114
|
+
public async Task<DashboardData> LoadDashboardAsync(CancellationToken cancellationToken)
|
|
115
|
+
{
|
|
116
|
+
var ordersTask = _orderService.GetRecentAsync(cancellationToken);
|
|
117
|
+
var metricsTask = _metricsService.GetCurrentAsync(cancellationToken);
|
|
118
|
+
var alertsTask = _alertService.GetActiveAsync(cancellationToken);
|
|
119
|
+
|
|
120
|
+
await Task.WhenAll(ordersTask, metricsTask, alertsTask);
|
|
121
|
+
|
|
122
|
+
return new DashboardData(
|
|
123
|
+
Orders: await ordersTask,
|
|
124
|
+
Metrics: await metricsTask,
|
|
125
|
+
Alerts: await alertsTask);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Options Pattern
|
|
130
|
+
|
|
131
|
+
Bind configuration sections to strongly-typed objects.
|
|
132
|
+
|
|
133
|
+
```csharp
|
|
134
|
+
public sealed class SmtpOptions
|
|
135
|
+
{
|
|
136
|
+
public const string SectionName = "Smtp";
|
|
137
|
+
|
|
138
|
+
public required string Host { get; init; }
|
|
139
|
+
public required int Port { get; init; }
|
|
140
|
+
public required string Username { get; init; }
|
|
141
|
+
public bool UseSsl { get; init; } = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Registration
|
|
145
|
+
builder.Services.Configure<SmtpOptions>(
|
|
146
|
+
builder.Configuration.GetSection(SmtpOptions.SectionName));
|
|
147
|
+
|
|
148
|
+
// Usage via injection
|
|
149
|
+
public class EmailService(IOptions<SmtpOptions> options)
|
|
150
|
+
{
|
|
151
|
+
private readonly SmtpOptions _smtp = options.Value;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Result Pattern
|
|
156
|
+
|
|
157
|
+
Return explicit success/failure instead of throwing for expected failures.
|
|
158
|
+
|
|
159
|
+
```csharp
|
|
160
|
+
public sealed record Result<T>
|
|
161
|
+
{
|
|
162
|
+
public bool IsSuccess { get; }
|
|
163
|
+
public T? Value { get; }
|
|
164
|
+
public string? Error { get; }
|
|
165
|
+
|
|
166
|
+
private Result(T value) { IsSuccess = true; Value = value; }
|
|
167
|
+
private Result(string error) { IsSuccess = false; Error = error; }
|
|
168
|
+
|
|
169
|
+
public static Result<T> Success(T value) => new(value);
|
|
170
|
+
public static Result<T> Failure(string error) => new(error);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Usage
|
|
174
|
+
public async Task<Result<Order>> PlaceOrderAsync(CreateOrderRequest request)
|
|
175
|
+
{
|
|
176
|
+
if (request.Items.Count == 0)
|
|
177
|
+
return Result<Order>.Failure("Order must contain at least one item");
|
|
178
|
+
|
|
179
|
+
var order = Order.Create(request);
|
|
180
|
+
await _repository.AddAsync(order, CancellationToken.None);
|
|
181
|
+
return Result<Order>.Success(order);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Repository Pattern with EF Core
|
|
186
|
+
|
|
187
|
+
```csharp
|
|
188
|
+
public sealed class SqlOrderRepository : IOrderRepository
|
|
189
|
+
{
|
|
190
|
+
private readonly AppDbContext _db;
|
|
191
|
+
|
|
192
|
+
public SqlOrderRepository(AppDbContext db) => _db = db;
|
|
193
|
+
|
|
194
|
+
public async Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
|
|
195
|
+
{
|
|
196
|
+
return await _db.Orders
|
|
197
|
+
.Include(o => o.Items)
|
|
198
|
+
.AsNoTracking()
|
|
199
|
+
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public async Task<IReadOnlyList<Order>> FindByCustomerAsync(
|
|
203
|
+
string customerId,
|
|
204
|
+
CancellationToken cancellationToken)
|
|
205
|
+
{
|
|
206
|
+
return await _db.Orders
|
|
207
|
+
.Where(o => o.CustomerId == customerId)
|
|
208
|
+
.OrderByDescending(o => o.CreatedAt)
|
|
209
|
+
.AsNoTracking()
|
|
210
|
+
.ToListAsync(cancellationToken);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public async Task AddAsync(Order order, CancellationToken cancellationToken)
|
|
214
|
+
{
|
|
215
|
+
_db.Orders.Add(order);
|
|
216
|
+
await _db.SaveChangesAsync(cancellationToken);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Middleware and Pipeline
|
|
222
|
+
|
|
223
|
+
```csharp
|
|
224
|
+
// Custom middleware
|
|
225
|
+
public sealed class RequestTimingMiddleware
|
|
226
|
+
{
|
|
227
|
+
private readonly RequestDelegate _next;
|
|
228
|
+
private readonly ILogger<RequestTimingMiddleware> _logger;
|
|
229
|
+
|
|
230
|
+
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
|
|
231
|
+
{
|
|
232
|
+
_next = next;
|
|
233
|
+
_logger = logger;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public async Task InvokeAsync(HttpContext context)
|
|
237
|
+
{
|
|
238
|
+
var stopwatch = Stopwatch.StartNew();
|
|
239
|
+
try
|
|
240
|
+
{
|
|
241
|
+
await _next(context);
|
|
242
|
+
}
|
|
243
|
+
finally
|
|
244
|
+
{
|
|
245
|
+
stopwatch.Stop();
|
|
246
|
+
_logger.LogInformation(
|
|
247
|
+
"Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
|
|
248
|
+
context.Request.Method,
|
|
249
|
+
context.Request.Path,
|
|
250
|
+
stopwatch.ElapsedMilliseconds,
|
|
251
|
+
context.Response.StatusCode);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Minimal API Patterns
|
|
258
|
+
|
|
259
|
+
```csharp
|
|
260
|
+
// Organized with route groups
|
|
261
|
+
var orders = app.MapGroup("/api/orders")
|
|
262
|
+
.RequireAuthorization()
|
|
263
|
+
.WithTags("Orders");
|
|
264
|
+
|
|
265
|
+
orders.MapGet("/{id:guid}", async (
|
|
266
|
+
Guid id,
|
|
267
|
+
IOrderRepository repository,
|
|
268
|
+
CancellationToken cancellationToken) =>
|
|
269
|
+
{
|
|
270
|
+
var order = await repository.FindByIdAsync(id, cancellationToken);
|
|
271
|
+
return order is not null
|
|
272
|
+
? TypedResults.Ok(order)
|
|
273
|
+
: TypedResults.NotFound();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
orders.MapPost("/", async (
|
|
277
|
+
CreateOrderRequest request,
|
|
278
|
+
IOrderService service,
|
|
279
|
+
CancellationToken cancellationToken) =>
|
|
280
|
+
{
|
|
281
|
+
var result = await service.PlaceOrderAsync(request, cancellationToken);
|
|
282
|
+
return result.IsSuccess
|
|
283
|
+
? TypedResults.Created($"/api/orders/{result.Value!.Id}", result.Value)
|
|
284
|
+
: TypedResults.BadRequest(result.Error);
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Guard Clauses
|
|
289
|
+
|
|
290
|
+
```csharp
|
|
291
|
+
// Good: Early returns with clear validation
|
|
292
|
+
public async Task<ProcessResult> ProcessPaymentAsync(
|
|
293
|
+
PaymentRequest request,
|
|
294
|
+
CancellationToken cancellationToken)
|
|
295
|
+
{
|
|
296
|
+
ArgumentNullException.ThrowIfNull(request);
|
|
297
|
+
|
|
298
|
+
if (request.Amount <= 0)
|
|
299
|
+
throw new ArgumentOutOfRangeException(nameof(request.Amount), "Amount must be positive");
|
|
300
|
+
|
|
301
|
+
if (string.IsNullOrWhiteSpace(request.Currency))
|
|
302
|
+
throw new ArgumentException("Currency is required", nameof(request.Currency));
|
|
303
|
+
|
|
304
|
+
// Happy path continues here without nesting
|
|
305
|
+
var gateway = _gatewayFactory.Create(request.Currency);
|
|
306
|
+
return await gateway.ChargeAsync(request, cancellationToken);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Anti-Patterns to Avoid
|
|
311
|
+
|
|
312
|
+
| Anti-Pattern | Fix |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `async void` methods | Return `Task` (except event handlers) |
|
|
315
|
+
| `.Result` or `.Wait()` | Use `await` |
|
|
316
|
+
| `catch (Exception) { }` | Handle or rethrow with context |
|
|
317
|
+
| `new Service()` in constructors | Use constructor injection |
|
|
318
|
+
| `public` fields | Use properties with appropriate accessors |
|
|
319
|
+
| `dynamic` in business logic | Use generics or explicit types |
|
|
320
|
+
| Mutable `static` state | Use DI scoping or `ConcurrentDictionary` |
|
|
321
|
+
| `string.Format` in loops | Use `StringBuilder` or interpolated string handlers |
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-handling
|
|
3
|
+
description: Patterns for robust error handling across TypeScript, Python, and Go. Covers typed errors, error boundaries, retries, circuit breakers, and user-facing error messages.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Error Handling Patterns
|
|
8
|
+
|
|
9
|
+
Consistent, robust error handling patterns for production applications.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Designing error types or exception hierarchies for a new module or service
|
|
14
|
+
- Adding retry logic or circuit breakers for unreliable external dependencies
|
|
15
|
+
- Reviewing API endpoints for missing error handling
|
|
16
|
+
- Implementing user-facing error messages and feedback
|
|
17
|
+
- Debugging cascading failures or silent error swallowing
|
|
18
|
+
|
|
19
|
+
## Core Principles
|
|
20
|
+
|
|
21
|
+
1. **Fail fast and loudly** — surface errors at the boundary where they occur; don't bury them
|
|
22
|
+
2. **Typed errors over string messages** — errors are first-class values with structure
|
|
23
|
+
3. **User messages ≠ developer messages** — show friendly text to users, log full context server-side
|
|
24
|
+
4. **Never swallow errors silently** — every `catch` block must either handle, re-throw, or log
|
|
25
|
+
5. **Errors are part of your API contract** — document every error code a client may receive
|
|
26
|
+
|
|
27
|
+
## TypeScript / JavaScript
|
|
28
|
+
|
|
29
|
+
### Typed Error Classes
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Define an error hierarchy for your domain
|
|
33
|
+
export class AppError extends Error {
|
|
34
|
+
constructor(
|
|
35
|
+
message: string,
|
|
36
|
+
public readonly code: string,
|
|
37
|
+
public readonly statusCode: number = 500,
|
|
38
|
+
public readonly details?: unknown,
|
|
39
|
+
) {
|
|
40
|
+
super(message)
|
|
41
|
+
this.name = this.constructor.name
|
|
42
|
+
// Maintain correct prototype chain in transpiled ES5 JavaScript.
|
|
43
|
+
// Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)
|
|
44
|
+
// to work correctly when extending the built-in Error class.
|
|
45
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class NotFoundError extends AppError {
|
|
50
|
+
constructor(resource: string, id: string) {
|
|
51
|
+
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ValidationError extends AppError {
|
|
56
|
+
constructor(message: string, details: { field: string; message: string }[]) {
|
|
57
|
+
super(message, 'VALIDATION_ERROR', 422, details)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class UnauthorizedError extends AppError {
|
|
62
|
+
constructor(reason = 'Authentication required') {
|
|
63
|
+
super(reason, 'UNAUTHORIZED', 401)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class RateLimitError extends AppError {
|
|
68
|
+
constructor(public readonly retryAfterMs: number) {
|
|
69
|
+
super('Rate limit exceeded', 'RATE_LIMITED', 429)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Result Pattern (no-throw style)
|
|
75
|
+
|
|
76
|
+
For operations where failure is expected and common (parsing, external calls):
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
type Result<T, E = AppError> =
|
|
80
|
+
| { ok: true; value: T }
|
|
81
|
+
| { ok: false; error: E }
|
|
82
|
+
|
|
83
|
+
function ok<T>(value: T): Result<T> {
|
|
84
|
+
return { ok: true, value }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function err<E>(error: E): Result<never, E> {
|
|
88
|
+
return { ok: false, error }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Usage
|
|
92
|
+
async function fetchUser(id: string): Promise<Result<User>> {
|
|
93
|
+
try {
|
|
94
|
+
const user = await db.users.findUnique({ where: { id } })
|
|
95
|
+
if (!user) return err(new NotFoundError('User', id))
|
|
96
|
+
return ok(user)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return err(new AppError('Database error', 'DB_ERROR'))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await fetchUser('abc-123')
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
// TypeScript knows result.error here
|
|
105
|
+
logger.error('Failed to fetch user', { error: result.error })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
// TypeScript knows result.value here
|
|
109
|
+
console.log(result.value.email)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### API Error Handler (Next.js / Express)
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
116
|
+
|
|
117
|
+
function handleApiError(error: unknown): NextResponse {
|
|
118
|
+
// Known application error
|
|
119
|
+
if (error instanceof AppError) {
|
|
120
|
+
return NextResponse.json(
|
|
121
|
+
{
|
|
122
|
+
error: {
|
|
123
|
+
code: error.code,
|
|
124
|
+
message: error.message,
|
|
125
|
+
...(error.details ? { details: error.details } : {}),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{ status: error.statusCode },
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Zod validation error
|
|
133
|
+
if (error instanceof z.ZodError) {
|
|
134
|
+
return NextResponse.json(
|
|
135
|
+
{
|
|
136
|
+
error: {
|
|
137
|
+
code: 'VALIDATION_ERROR',
|
|
138
|
+
message: 'Request validation failed',
|
|
139
|
+
details: error.issues.map(i => ({
|
|
140
|
+
field: i.path.join('.'),
|
|
141
|
+
message: i.message,
|
|
142
|
+
})),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{ status: 422 },
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Unexpected error — log details, return generic message
|
|
150
|
+
console.error('Unexpected error:', error)
|
|
151
|
+
return NextResponse.json(
|
|
152
|
+
{ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },
|
|
153
|
+
{ status: 500 },
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function POST(req: NextRequest) {
|
|
158
|
+
try {
|
|
159
|
+
// ... handler logic
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return handleApiError(error)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### React Error Boundary
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { Component, ErrorInfo, ReactNode } from 'react'
|
|
170
|
+
|
|
171
|
+
interface Props {
|
|
172
|
+
fallback: ReactNode
|
|
173
|
+
onError?: (error: Error, info: ErrorInfo) => void
|
|
174
|
+
children: ReactNode
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface State {
|
|
178
|
+
hasError: boolean
|
|
179
|
+
error: Error | null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
183
|
+
state: State = { hasError: false, error: null }
|
|
184
|
+
|
|
185
|
+
static getDerivedStateFromError(error: Error): State {
|
|
186
|
+
return { hasError: true, error }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
190
|
+
this.props.onError?.(error, info)
|
|
191
|
+
console.error('Unhandled React error:', error, info)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
render() {
|
|
195
|
+
if (this.state.hasError) return this.props.fallback
|
|
196
|
+
return this.props.children
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Usage
|
|
201
|
+
<ErrorBoundary fallback={<p>Something went wrong. Please refresh.</p>}>
|
|
202
|
+
<MyComponent />
|
|
203
|
+
</ErrorBoundary>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Python
|
|
207
|
+
|
|
208
|
+
### Custom Exception Hierarchy
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
class AppError(Exception):
|
|
212
|
+
"""Base application error."""
|
|
213
|
+
def __init__(self, message: str, code: str, status_code: int = 500):
|
|
214
|
+
super().__init__(message)
|
|
215
|
+
self.code = code
|
|
216
|
+
self.status_code = status_code
|
|
217
|
+
|
|
218
|
+
class NotFoundError(AppError):
|
|
219
|
+
def __init__(self, resource: str, id: str):
|
|
220
|
+
super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
|
|
221
|
+
|
|
222
|
+
class ValidationError(AppError):
|
|
223
|
+
def __init__(self, message: str, details: list[dict] | None = None):
|
|
224
|
+
super().__init__(message, "VALIDATION_ERROR", 422)
|
|
225
|
+
self.details = details or []
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### FastAPI Global Exception Handler
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from fastapi import FastAPI, Request
|
|
232
|
+
from fastapi.responses import JSONResponse
|
|
233
|
+
|
|
234
|
+
app = FastAPI()
|
|
235
|
+
|
|
236
|
+
@app.exception_handler(AppError)
|
|
237
|
+
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
|
|
238
|
+
return JSONResponse(
|
|
239
|
+
status_code=exc.status_code,
|
|
240
|
+
content={"error": {"code": exc.code, "message": str(exc)}},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@app.exception_handler(Exception)
|
|
244
|
+
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
245
|
+
# Log full details, return generic message
|
|
246
|
+
logger.exception("Unexpected error", exc_info=exc)
|
|
247
|
+
return JSONResponse(
|
|
248
|
+
status_code=500,
|
|
249
|
+
content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}},
|
|
250
|
+
)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Go
|
|
254
|
+
|
|
255
|
+
### Sentinel Errors and Error Wrapping
|
|
256
|
+
|
|
257
|
+
```go
|
|
258
|
+
package domain
|
|
259
|
+
|
|
260
|
+
import "errors"
|
|
261
|
+
|
|
262
|
+
// Sentinel errors for type-checking
|
|
263
|
+
var (
|
|
264
|
+
ErrNotFound = errors.New("not found")
|
|
265
|
+
ErrUnauthorized = errors.New("unauthorized")
|
|
266
|
+
ErrConflict = errors.New("conflict")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Wrap errors with context — never lose the original
|
|
270
|
+
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
|
|
271
|
+
user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
|
|
272
|
+
if errors.Is(err, sql.ErrNoRows) {
|
|
273
|
+
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
|
|
274
|
+
}
|
|
275
|
+
if err != nil {
|
|
276
|
+
return nil, fmt.Errorf("querying user %s: %w", id, err)
|
|
277
|
+
}
|
|
278
|
+
return user, nil
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// At the handler level, unwrap to determine response
|
|
282
|
+
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
283
|
+
user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
|
|
284
|
+
if err != nil {
|
|
285
|
+
switch {
|
|
286
|
+
case errors.Is(err, domain.ErrNotFound):
|
|
287
|
+
writeError(w, http.StatusNotFound, "not_found", err.Error())
|
|
288
|
+
case errors.Is(err, domain.ErrUnauthorized):
|
|
289
|
+
writeError(w, http.StatusForbidden, "forbidden", "Access denied")
|
|
290
|
+
default:
|
|
291
|
+
slog.Error("unexpected error", "err", err)
|
|
292
|
+
writeError(w, http.StatusInternalServerError, "internal_error", "An unexpected error occurred")
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
writeJSON(w, http.StatusOK, user)
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Retry with Exponential Backoff
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
interface RetryOptions {
|
|
304
|
+
maxAttempts?: number
|
|
305
|
+
baseDelayMs?: number
|
|
306
|
+
maxDelayMs?: number
|
|
307
|
+
retryIf?: (error: unknown) => boolean
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function withRetry<T>(
|
|
311
|
+
fn: () => Promise<T>,
|
|
312
|
+
options: RetryOptions = {},
|
|
313
|
+
): Promise<T> {
|
|
314
|
+
const {
|
|
315
|
+
maxAttempts = 3,
|
|
316
|
+
baseDelayMs = 500,
|
|
317
|
+
maxDelayMs = 10_000,
|
|
318
|
+
retryIf = () => true,
|
|
319
|
+
} = options
|
|
320
|
+
|
|
321
|
+
let lastError: unknown
|
|
322
|
+
|
|
323
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
324
|
+
try {
|
|
325
|
+
return await fn()
|
|
326
|
+
} catch (error) {
|
|
327
|
+
lastError = error
|
|
328
|
+
if (attempt === maxAttempts || !retryIf(error)) throw error
|
|
329
|
+
|
|
330
|
+
const jitter = Math.random() * baseDelayMs
|
|
331
|
+
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
|
|
332
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
throw lastError
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Usage: retry transient network errors, not 4xx
|
|
340
|
+
const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
|
|
341
|
+
maxAttempts: 3,
|
|
342
|
+
retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
|
|
343
|
+
})
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## User-Facing Error Messages
|
|
347
|
+
|
|
348
|
+
Map error codes to human-readable messages. Keep technical details out of user-visible text.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
const USER_ERROR_MESSAGES: Record<string, string> = {
|
|
352
|
+
NOT_FOUND: 'The requested item could not be found.',
|
|
353
|
+
UNAUTHORIZED: 'Please sign in to continue.',
|
|
354
|
+
FORBIDDEN: "You don't have permission to do that.",
|
|
355
|
+
VALIDATION_ERROR: 'Please check your input and try again.',
|
|
356
|
+
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
|
|
357
|
+
INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function getUserMessage(code: string): string {
|
|
361
|
+
return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Error Handling Checklist
|
|
366
|
+
|
|
367
|
+
Before merging any code that touches error handling:
|
|
368
|
+
|
|
369
|
+
- [ ] Every `catch` block handles, re-throws, or logs — no silent swallowing
|
|
370
|
+
- [ ] API errors follow the standard envelope `{ error: { code, message } }`
|
|
371
|
+
- [ ] User-facing messages contain no stack traces or internal details
|
|
372
|
+
- [ ] Full error context is logged server-side
|
|
373
|
+
- [ ] Custom error classes extend a base `AppError` with a `code` field
|
|
374
|
+
- [ ] Async functions surface errors to callers — no fire-and-forget without fallback
|
|
375
|
+
- [ ] Retry logic only retries retriable errors (not 4xx client errors)
|
|
376
|
+
- [ ] React components are wrapped in `ErrorBoundary` for rendering errors
|