sdg-agents 1.0.5
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/LICENSE +15 -0
- package/README.md +161 -0
- package/package.json +88 -0
- package/src/assets/dev-guides/agent-deep-flow.md +139 -0
- package/src/assets/dev-guides/prompt-tracks/00-lite-mode/01-project-scope-and-mvp.md +45 -0
- package/src/assets/dev-guides/prompt-tracks/00-lite-mode/02-stack-and-data-model.md +56 -0
- package/src/assets/dev-guides/prompt-tracks/00-lite-mode/03-external-integrations.md +44 -0
- package/src/assets/dev-guides/prompt-tracks/00-lite-mode/04-launch-checklist.md +26 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/00-foundation/01-project-vision.md +77 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/00-foundation/02-problem-definition.md +63 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/00-foundation/03-success-metrics.md +48 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/00-foundation/05-engineering-culture-and-silos.md +41 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/00-foundation/06-foundation-approval.md +57 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/01-setup/01-tech-stack.md +62 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/01-setup/02-local-environment.md +50 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/01-setup/03-version-control-and-tracking.md +65 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/01-setup/04-hello-world-validation.md +37 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/01-setup/05-setup-approval.md +61 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/01-folder-structure.md +49 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/02-design-patterns.md +59 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/03-apis-and-communication.md +55 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/04-security-and-access.md +63 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/05-security-audit.md +64 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/06-design-system-ux.md +72 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/07-ai-strategy.md +72 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/08-observability.md +65 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/09-technical-documentation.md +64 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/10-infrastructure-and-costs.md +40 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/11-data-privacy-compliance.md +41 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/12-performance-and-scale.md +46 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/13-disaster-recovery.md +39 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/02-architecture/14-architecture-approval.md +81 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/03-delivery/01-ci-cd-pipeline.md +49 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/03-delivery/02-quality-standards.md +46 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/03-delivery/03-delivery-approval.md +58 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/00-feature-template.md +30 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/01-context-and-scope.md +46 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/02-business-rules.md +39 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/03-architecture-and-design.md +50 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/04-testing-strategy.md +48 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/05-implementation-plan.md +45 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/04-features/06-feature-approval.md +46 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/05-evolution/01-rfcs-and-changes.md +63 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/05-evolution/02-incident-postmortems.md +64 -0
- package/src/assets/dev-guides/prompt-tracks/01-new-evolution/05-evolution/03-adr-template.md +66 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/00-foundation/01-legacy-vision.md +73 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/00-foundation/02-foundation-approval.md +53 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/01-analysis/01-tech-debt-inventory.md +55 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/01-analysis/02-security-audit.md +63 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/01-analysis/03-regression-baseline.md +49 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/01-analysis/04-analysis-approval.md +65 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/02-strategy/01-modernization-approach.md +60 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/02-strategy/02-versioning-and-coexistence.md +57 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/02-strategy/03-new-demands-triage.md +41 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/02-strategy/04-strategy-approval.md +56 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/03-refactor/01-cleanup-backlog.md +45 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/03-refactor/02-refactor-approval.md +53 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/04-migration/01-migration-roadmap.md +47 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/04-migration/02-data-sync-strategy.md +56 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/04-migration/03-migration-approval.md +55 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/05-sunset/01-decommission-plan.md +47 -0
- package/src/assets/dev-guides/prompt-tracks/02-legacy-modernization/05-sunset/02-sunset-approval.md +60 -0
- package/src/assets/dev-guides/prompt-tracks/README.md +144 -0
- package/src/assets/dev-guides/software-development-lifecycle-sdlc.md +147 -0
- package/src/assets/dev-guides/spec-driven-dev-guide.md +81 -0
- package/src/assets/dev-guides/ui-prompt-guide.md +181 -0
- package/src/assets/img/sdg-agents-icon-dark.svg +55 -0
- package/src/assets/img/sdg-agents-icon-light.svg +55 -0
- package/src/assets/img/sdg-agents-menu-v1.png +0 -0
- package/src/assets/img/sdg-icon.svg +110 -0
- package/src/assets/instructions/commands/sdg-docs.md +69 -0
- package/src/assets/instructions/commands/sdg-feat.md +45 -0
- package/src/assets/instructions/commands/sdg-fix.md +41 -0
- package/src/assets/instructions/commands/sdg-land.md +128 -0
- package/src/assets/instructions/competencies/backend.md +353 -0
- package/src/assets/instructions/competencies/frontend.md +439 -0
- package/src/assets/instructions/core/agent-roles.md +104 -0
- package/src/assets/instructions/core/api-design.md +65 -0
- package/src/assets/instructions/core/ci-cd.md +144 -0
- package/src/assets/instructions/core/cloud.md +63 -0
- package/src/assets/instructions/core/code-style.md +144 -0
- package/src/assets/instructions/core/containers.md +115 -0
- package/src/assets/instructions/core/data-access.md +119 -0
- package/src/assets/instructions/core/engineering-standards.md +502 -0
- package/src/assets/instructions/core/naming.md +136 -0
- package/src/assets/instructions/core/observability.md +73 -0
- package/src/assets/instructions/core/security-pipeline.md +209 -0
- package/src/assets/instructions/core/security.md +45 -0
- package/src/assets/instructions/core/sql-style.md +138 -0
- package/src/assets/instructions/core/staff-dna.md +72 -0
- package/src/assets/instructions/core/testing-principles.md +212 -0
- package/src/assets/instructions/core/ui/architecture.md +171 -0
- package/src/assets/instructions/core/ui/design-thinking.md +319 -0
- package/src/assets/instructions/core/ui/presets.md +200 -0
- package/src/assets/instructions/core/ui/standards.md +144 -0
- package/src/assets/instructions/core/writing-soul.md +82 -0
- package/src/assets/instructions/flavors/legacy/principles.md +64 -0
- package/src/assets/instructions/flavors/lite/principles.md +39 -0
- package/src/assets/instructions/flavors/mvc/principles.md +124 -0
- package/src/assets/instructions/flavors/vertical-slice/principles.md +178 -0
- package/src/assets/instructions/idioms/csharp/patterns.md +367 -0
- package/src/assets/instructions/idioms/flutter/patterns.md +97 -0
- package/src/assets/instructions/idioms/go/patterns.md +193 -0
- package/src/assets/instructions/idioms/java/patterns.md +233 -0
- package/src/assets/instructions/idioms/javascript/patterns.md +223 -0
- package/src/assets/instructions/idioms/kotlin/patterns.md +94 -0
- package/src/assets/instructions/idioms/python/patterns.md +185 -0
- package/src/assets/instructions/idioms/rust/patterns.md +189 -0
- package/src/assets/instructions/idioms/scripts/patterns.md +81 -0
- package/src/assets/instructions/idioms/sql/patterns.md +109 -0
- package/src/assets/instructions/idioms/swift/patterns.md +97 -0
- package/src/assets/instructions/idioms/typescript/patterns.md +304 -0
- package/src/assets/instructions/idioms/vbnet/patterns.md +96 -0
- package/src/assets/instructions/idioms/vbnet-legacy/patterns.md +104 -0
- package/src/assets/instructions/templates/workflow.md +244 -0
- package/src/assets/instructions/workflows/governance.md +162 -0
- package/src/engine/bin/add-idiom.mjs +186 -0
- package/src/engine/bin/index.mjs +226 -0
- package/src/engine/config/stack-display.mjs +75 -0
- package/src/engine/config/stack-versions.mjs +76 -0
- package/src/engine/lib/cli-parser.mjs +80 -0
- package/src/engine/lib/display-utils.mjs +20 -0
- package/src/engine/lib/fs-utils.mjs +99 -0
- package/src/engine/lib/instruction-assembler.mjs +487 -0
- package/src/engine/lib/manifest-utils.mjs +128 -0
- package/src/engine/lib/prompt-utils.mjs +137 -0
- package/src/engine/lib/result-utils.mjs +14 -0
- package/src/engine/lib/ruleset-injector.mjs +183 -0
- package/src/engine/lib/ui-utils.mjs +216 -0
- package/src/engine/lib/wizard.mjs +472 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# C# — Project Conventions
|
|
2
|
+
|
|
3
|
+
> Universal principles (naming, composition, DRY, performance, security) are in `../../core/staff-dna.md`.
|
|
4
|
+
> This file contains only decisions specific to this language and stack.
|
|
5
|
+
|
|
6
|
+
<ruleset name="CSharpConventions">
|
|
7
|
+
|
|
8
|
+
## Error Handling
|
|
9
|
+
|
|
10
|
+
- **Strategy**: Result Pattern in the domain (`Result<T>` via sealed record); exceptions only for unexpected failures (infra/runtime)
|
|
11
|
+
- **Propagation**: Result is explicitly returned in business flows; exceptions bubble up to the global middleware
|
|
12
|
+
- **Domain errors**: `sealed record ApiError(string Message, string Code)`; pattern matching for mapping
|
|
13
|
+
- **Never**: `throw` for business rules; `.Result` / `.Wait()` in async code; leak internal details
|
|
14
|
+
|
|
15
|
+
### Result Pattern
|
|
16
|
+
|
|
17
|
+
> <rule name="ResultPatternCSharp">
|
|
18
|
+
|
|
19
|
+
```csharp
|
|
20
|
+
public sealed record ApiError(string Message, string Code);
|
|
21
|
+
|
|
22
|
+
public record Result<T>(bool IsSuccess, bool IsFailure, T? Value, ApiError? Error)
|
|
23
|
+
{
|
|
24
|
+
public static Result<T> Success(T value) => new(true, false, value, null);
|
|
25
|
+
public static Result<T> Fail(string msg, string code) => new(false, true, default, new ApiError(msg, code));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pattern matching
|
|
29
|
+
public IActionResult GetOrder(Result<Order> result) => result switch
|
|
30
|
+
{
|
|
31
|
+
{ IsSuccess: true, Value: var o } => Ok(o),
|
|
32
|
+
{ IsFailure: true, Error: var e } => MapError(e),
|
|
33
|
+
_ => Problem()
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> </rule>
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## HTTP & API
|
|
42
|
+
|
|
43
|
+
- **Framework**: ASP.NET Core — Minimal APIs (preferred) or Controllers when necessary
|
|
44
|
+
- **Style**: API First + BFF
|
|
45
|
+
- **Route organization**: Vertical slice per feature (`Features/Orders/OrderEndpoints.cs`)
|
|
46
|
+
- **Middleware/hooks**: Auth in the pipeline; validation at the boundary; centralized logging/tracing
|
|
47
|
+
- **DI**: Constructor injection via primary constructors; registration in `Program.cs`; never service locator
|
|
48
|
+
|
|
49
|
+
### Minimal API per Feature
|
|
50
|
+
|
|
51
|
+
> <rule name="MinimalAPI">
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
public static class OrderEndpoints
|
|
55
|
+
{
|
|
56
|
+
public static void MapOrderEndpoints(this WebApplication app)
|
|
57
|
+
{
|
|
58
|
+
var group = app.MapGroup("/api/orders").WithTags("Orders");
|
|
59
|
+
group.MapPost("/", CreateOrder);
|
|
60
|
+
group.MapGet("/{id:guid}", GetOrder);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static async Task<IResult> CreateOrder(
|
|
64
|
+
CreateOrderRequest request,
|
|
65
|
+
OrderService service,
|
|
66
|
+
CancellationToken ct)
|
|
67
|
+
{
|
|
68
|
+
var result = await service.CreateOrderAsync(request, ct);
|
|
69
|
+
var httpResponse = ToEnvelope(result);
|
|
70
|
+
return httpResponse;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Adapter — only layer that knows both Result<T> and HTTP envelope
|
|
74
|
+
private static IResult ToEnvelope<T>(Result<T> result) => result switch
|
|
75
|
+
{
|
|
76
|
+
{ IsSuccess: true, Value: var value } =>
|
|
77
|
+
Results.Ok(new { success = true, error = (object?)null, data = value }),
|
|
78
|
+
{ IsFailure: true, Error: var error } =>
|
|
79
|
+
Results.Json(
|
|
80
|
+
new { success = false, error = new { code = error!.Code, message = error.Message }, data = (object?)null },
|
|
81
|
+
statusCode: MapStatusCode(error.Code)
|
|
82
|
+
),
|
|
83
|
+
_ => Results.Problem()
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private static int MapStatusCode(string code) => code switch
|
|
87
|
+
{
|
|
88
|
+
"NOT_FOUND" => 404,
|
|
89
|
+
"UNAUTHORIZED" => 401,
|
|
90
|
+
"FORBIDDEN" => 403,
|
|
91
|
+
"CONFLICT" => 409,
|
|
92
|
+
"INVALID_INPUT" => 400,
|
|
93
|
+
_ => 422
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> </rule>
|
|
99
|
+
|
|
100
|
+
### Dependency Injection
|
|
101
|
+
|
|
102
|
+
> <rule name="CSharpDI">
|
|
103
|
+
|
|
104
|
+
```csharp
|
|
105
|
+
public class OrderService(IOrderRepository repo, INotifier notifier)
|
|
106
|
+
{
|
|
107
|
+
public async Task<Result<Order>> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default)
|
|
108
|
+
{
|
|
109
|
+
var order = await repo.SaveAsync(Order.From(request), ct);
|
|
110
|
+
await notifier.SendAsync(order, ct);
|
|
111
|
+
return Result<Order>.Success(order);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Program.cs
|
|
116
|
+
builder.Services.AddScoped<IOrderRepository, PostgresOrderRepository>();
|
|
117
|
+
builder.Services.AddScoped<OrderService>();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> </rule>
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Testing
|
|
125
|
+
|
|
126
|
+
- **Framework**: xUnit + FluentAssertions + NSubstitute
|
|
127
|
+
- **Style**: Flat, behavior-oriented
|
|
128
|
+
- **Naming**: `MethodName_ShouldDoX_WhenY`
|
|
129
|
+
- **Mocks**: NSubstitute for external dependencies; never mock the domain
|
|
130
|
+
- **What to test**: Business rules, error cases (Result), API contracts
|
|
131
|
+
|
|
132
|
+
### Unit Testing
|
|
133
|
+
|
|
134
|
+
> <rule name="CSharpTesting">
|
|
135
|
+
|
|
136
|
+
```csharp
|
|
137
|
+
public class OrderServiceTests
|
|
138
|
+
{
|
|
139
|
+
private readonly IOrderRepository _repo = Substitute.For<IOrderRepository>();
|
|
140
|
+
private readonly INotifier _notifier = Substitute.For<INotifier>();
|
|
141
|
+
private readonly OrderService _sut;
|
|
142
|
+
|
|
143
|
+
public OrderServiceTests() => _sut = new OrderService(_repo, _notifier);
|
|
144
|
+
|
|
145
|
+
[Fact]
|
|
146
|
+
public async Task CreateOrder_ShouldReturnSuccess()
|
|
147
|
+
{
|
|
148
|
+
_repo.SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
|
|
149
|
+
.Returns(new Order(Guid.NewGuid(), "prod-1", 2));
|
|
150
|
+
|
|
151
|
+
var result = await _sut.CreateOrderAsync(new CreateOrderRequest("prod-1", 2));
|
|
152
|
+
|
|
153
|
+
result.IsSuccess.Should().BeTrue();
|
|
154
|
+
result.Value!.ProductId.Should().Be("prod-1");
|
|
155
|
+
await _notifier.Received(1).SendAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> </rule>
|
|
161
|
+
|
|
162
|
+
### Integration Testing
|
|
163
|
+
|
|
164
|
+
> <rule name="CSharpIntegration">
|
|
165
|
+
|
|
166
|
+
```csharp
|
|
167
|
+
public class OrdersApiTests(WebApplicationFactory<Program> factory)
|
|
168
|
+
: IClassFixture<WebApplicationFactory<Program>>
|
|
169
|
+
{
|
|
170
|
+
private readonly HttpClient _client = factory.CreateClient();
|
|
171
|
+
|
|
172
|
+
[Fact]
|
|
173
|
+
public async Task CreateOrder_ReturnsCreated()
|
|
174
|
+
{
|
|
175
|
+
var response = await _client.PostAsJsonAsync("/api/orders", new { ProductId = "abc", Quantity = 2 });
|
|
176
|
+
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> </rule>
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Types & Contracts
|
|
186
|
+
|
|
187
|
+
- **DTOs**: `record` for Request/Response; each layer has its own type; never expose EF entities
|
|
188
|
+
- **Validation**: Validation at the boundary — factory method with Result, FluentValidation when rules are complex
|
|
189
|
+
- **Async**: All I/O methods are `async Task<T>`; `Async` suffix; `CancellationToken` in public APIs; never `.Result` / `.Wait()`
|
|
190
|
+
- **Naming**: PascalCase for public members; `_camelCase` for private fields; `I` prefix for interfaces; `Async` suffix for async methods
|
|
191
|
+
|
|
192
|
+
### Records as Data Carriers
|
|
193
|
+
|
|
194
|
+
> <rule name="CSharpRecords">
|
|
195
|
+
|
|
196
|
+
```csharp
|
|
197
|
+
public record CreateUserRequest(string Email, string Password);
|
|
198
|
+
public record UserResponse(Guid Id, string Email);
|
|
199
|
+
|
|
200
|
+
// Factory method with validation
|
|
201
|
+
public record CreateOrderRequest(string ProductId, int Quantity)
|
|
202
|
+
{
|
|
203
|
+
public static Result<CreateOrderRequest> Create(string productId, int quantity)
|
|
204
|
+
{
|
|
205
|
+
if (string.IsNullOrWhiteSpace(productId))
|
|
206
|
+
return Result<CreateOrderRequest>.Fail("Product ID is required.", "INVALID_PRODUCT_ID");
|
|
207
|
+
if (quantity < 1)
|
|
208
|
+
return Result<CreateOrderRequest>.Fail("Quantity must be at least 1.", "INVALID_QUANTITY");
|
|
209
|
+
return Result<CreateOrderRequest>.Success(new(productId, quantity));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> </rule>
|
|
215
|
+
|
|
216
|
+
### Async Pattern
|
|
217
|
+
|
|
218
|
+
> <rule name="CSharpAsync">
|
|
219
|
+
|
|
220
|
+
```csharp
|
|
221
|
+
public async Task<Result<Dashboard>> GetDashboardAsync(string userId, CancellationToken ct = default)
|
|
222
|
+
{
|
|
223
|
+
var profileTask = _users.FindByIdAsync(userId, ct);
|
|
224
|
+
var ordersTask = _orders.GetRecentAsync(userId, ct);
|
|
225
|
+
var notificationsTask = _notifications.GetUnreadAsync(userId, ct);
|
|
226
|
+
|
|
227
|
+
await Task.WhenAll(profileTask, ordersTask, notificationsTask);
|
|
228
|
+
|
|
229
|
+
return Result<Dashboard>.Success(new Dashboard(
|
|
230
|
+
Profile: await profileTask,
|
|
231
|
+
Orders: await ordersTask,
|
|
232
|
+
Notifications: await notificationsTask
|
|
233
|
+
));
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
> </rule>
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## LINQ & Collections
|
|
242
|
+
|
|
243
|
+
> Use LINQ for clarity, not cleverness. Prefer readability over chaining.
|
|
244
|
+
|
|
245
|
+
### Core Principles
|
|
246
|
+
|
|
247
|
+
- Prefer **readability over conciseness**
|
|
248
|
+
- Keep queries **simple and predictable**
|
|
249
|
+
- Avoid complex chaining ("magic LINQ")
|
|
250
|
+
- LINQ is for **data transformation**, not business logic
|
|
251
|
+
- Use `foreach` for complex logic or accumulation (sum, total)
|
|
252
|
+
|
|
253
|
+
### Code Flow Alignment
|
|
254
|
+
|
|
255
|
+
Keep LINQ inside the **execution step** — never mix orchestration with complex queries.
|
|
256
|
+
|
|
257
|
+
> <rule name="CSharpLINQ">
|
|
258
|
+
|
|
259
|
+
````carousel
|
|
260
|
+
```csharp
|
|
261
|
+
// ❌ BAD: Chained LINQ magic — logic buried, untestable, hard to extend
|
|
262
|
+
var result = orders
|
|
263
|
+
.Where(o => o.Status == "CONFIRMED" && o.CreatedAt > DateTime.UtcNow.AddDays(-30))
|
|
264
|
+
.GroupBy(o => o.CustomerId)
|
|
265
|
+
.Select(g => new { CustomerId = g.Key, Total = g.Sum(o => o.Items.Sum(i => i.Price * i.Qty)) })
|
|
266
|
+
.OrderByDescending(x => x.Total)
|
|
267
|
+
.ToList();
|
|
268
|
+
```
|
|
269
|
+
<!-- slide -->
|
|
270
|
+
```csharp
|
|
271
|
+
// ✅ GOOD: LINQ for 1-to-1 transform; foreach for accumulation — each step is readable
|
|
272
|
+
var recentOrders = orders
|
|
273
|
+
.Where(o => o.Status == "CONFIRMED" && o.CreatedAt > DateTime.UtcNow.AddDays(-30))
|
|
274
|
+
.ToList();
|
|
275
|
+
|
|
276
|
+
var summaries = recentOrders
|
|
277
|
+
.GroupBy(o => o.CustomerId)
|
|
278
|
+
.Select(group => BuildCustomerSummary(group))
|
|
279
|
+
.OrderByDescending(summary => summary.Total)
|
|
280
|
+
.ToList();
|
|
281
|
+
|
|
282
|
+
return summaries;
|
|
283
|
+
|
|
284
|
+
static CustomerSummary BuildCustomerSummary(IGrouping<Guid, Order> group)
|
|
285
|
+
{
|
|
286
|
+
decimal total = 0;
|
|
287
|
+
foreach (var order in group)
|
|
288
|
+
foreach (var item in order.Items)
|
|
289
|
+
{
|
|
290
|
+
var lineAmount = item.Price * item.Qty;
|
|
291
|
+
total += lineAmount;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var customerSummary = new CustomerSummary(group.Key, total);
|
|
295
|
+
return customerSummary;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
````
|
|
299
|
+
|
|
300
|
+
> </rule>
|
|
301
|
+
|
|
302
|
+
### Materialization (ToList / ToArray)
|
|
303
|
+
|
|
304
|
+
Materialize only when needed: multiple iterations, snapshot of data, or returning a concrete collection.
|
|
305
|
+
|
|
306
|
+
- Default → `IEnumerable<T>`
|
|
307
|
+
- Use `.ToList()` at boundaries (return, serialization, EF Core terminal calls)
|
|
308
|
+
- Never materialize prematurely inside a pipeline
|
|
309
|
+
|
|
310
|
+
### Side Effects
|
|
311
|
+
|
|
312
|
+
LINQ must be **pure** — no side effects inside queries. Never use `.Select(u => { Log(u); return u; })`.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Data Access — EF Core
|
|
317
|
+
|
|
318
|
+
> Data access rules in [Universal Data Access Principles](../../core/data-access.md).
|
|
319
|
+
|
|
320
|
+
### DbContext & Tracking
|
|
321
|
+
|
|
322
|
+
> <rule name="EFContextLifetime">
|
|
323
|
+
> `DbContext` Scoped (one per request). `AsNoTracking()` in read-only queries. Never Singleton.
|
|
324
|
+
|
|
325
|
+
```csharp
|
|
326
|
+
public async Task<IReadOnlyList<OrderSummary>> GetRecentOrdersAsync(CancellationToken ct) =>
|
|
327
|
+
await _context.Orders
|
|
328
|
+
.AsNoTracking()
|
|
329
|
+
.Where(o => o.CreatedAt > DateTime.UtcNow.AddDays(-30))
|
|
330
|
+
.Select(o => new OrderSummary(o.Id, o.Total, o.CreatedAt))
|
|
331
|
+
.ToListAsync(ct);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
> </rule>
|
|
335
|
+
|
|
336
|
+
### Projection — Select over Full Entities
|
|
337
|
+
|
|
338
|
+
> <rule name="EFProjection">
|
|
339
|
+
> Always project to DTOs via `.Select()`. Never expose EF entities to the API layer.
|
|
340
|
+
|
|
341
|
+
```csharp
|
|
342
|
+
public async Task<ProductDetail?> GetProductDetailAsync(Guid id, CancellationToken ct) =>
|
|
343
|
+
await _context.Products
|
|
344
|
+
.Where(p => p.Id == id)
|
|
345
|
+
.Select(p => new ProductDetail(p.Id, p.Name, p.Price, p.Category.Name, p.Reviews.Count))
|
|
346
|
+
.FirstOrDefaultAsync(ct);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
> </rule>
|
|
350
|
+
|
|
351
|
+
### N+1 Prevention
|
|
352
|
+
|
|
353
|
+
> <rule name="EFNPlusOne">
|
|
354
|
+
> Lazy loading disabled. `.Include()` for known graphs. `AsSplitQuery()` for multiple collections.
|
|
355
|
+
|
|
356
|
+
```csharp
|
|
357
|
+
var orders = await _context.Orders
|
|
358
|
+
.Include(o => o.Items)
|
|
359
|
+
.Include(o => o.Payments)
|
|
360
|
+
.AsSplitQuery()
|
|
361
|
+
.Where(o => o.CustomerId == customerId)
|
|
362
|
+
.ToListAsync(ct);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
> </rule>
|
|
366
|
+
|
|
367
|
+
</ruleset>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Flutter (Dart) — Project Conventions
|
|
2
|
+
|
|
3
|
+
> Universal principles (naming, composition, DRY, performance, security) are in `../../core/staff-dna.md`.
|
|
4
|
+
> This file contains only decisions specific to this language and stack.
|
|
5
|
+
|
|
6
|
+
<ruleset name="FlutterConventions">
|
|
7
|
+
|
|
8
|
+
## Error Handling
|
|
9
|
+
|
|
10
|
+
- **Strategy**: Result Pattern (`Result<T>`) in the domain; `throw` only for unexpected failures
|
|
11
|
+
- **Propagation**: Explicit Result between layers; exceptions handled at the boundary (ViewModel/Controller)
|
|
12
|
+
- **Domain errors**: Sealed classes via `freezed` for union types; standard structure (`code`, `message`)
|
|
13
|
+
- **Never**: Exception for business rules; swallow errors; expose technical errors in the UI
|
|
14
|
+
|
|
15
|
+
### Result Pattern (Sealed Class / Freezed)
|
|
16
|
+
|
|
17
|
+
> <rule name="ResultPatternFlutter">
|
|
18
|
+
|
|
19
|
+
```dart
|
|
20
|
+
@freezed
|
|
21
|
+
class Result<T> with _$Result<T> {
|
|
22
|
+
const factory Result.success(T data) = Success<T>;
|
|
23
|
+
const factory Result.failure(String message, String code) = Failure<T>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Usage in ViewModel
|
|
27
|
+
final result = await _useCase.execute(request);
|
|
28
|
+
state = result.when(
|
|
29
|
+
success: (data) => state.copyWith(data: data, isLoading: false),
|
|
30
|
+
failure: (msg, code) => state.copyWith(errorMessage: msg, isLoading: false),
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> </rule>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## HTTP & API
|
|
39
|
+
|
|
40
|
+
- **Style**: API First + BFF
|
|
41
|
+
- **Client**: Dio (preferred) or `http`; centralized and typed API client
|
|
42
|
+
- **Serialization**: `json_serializable` or `freezed`
|
|
43
|
+
- **Never**: Call API directly from the widget; mix parsing with UI
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Testing
|
|
48
|
+
|
|
49
|
+
- **Framework**: `flutter_test` / `mocktail`
|
|
50
|
+
- **Style**: Behavior-oriented
|
|
51
|
+
- **Naming**: `shouldDoXWhenY`
|
|
52
|
+
- **Mocks**: Mock only external I/O; domain is always real
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Types & Contracts
|
|
57
|
+
|
|
58
|
+
- **Classes**: Immutable by default (`const`, `final`)
|
|
59
|
+
- **Union types**: `freezed` for complex DTOs and states
|
|
60
|
+
- **Null safety**: Mandatory; avoid `!`
|
|
61
|
+
- **DTOs**: Separated from the domain; never expose internal structure
|
|
62
|
+
- **Validation**: At the boundary (input/API)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Flutter-Specific Delta
|
|
67
|
+
|
|
68
|
+
- Widget = rendering only; no business logic in `build()`
|
|
69
|
+
- Logic outside the widget (Controller/ViewModel)
|
|
70
|
+
- State management with Riverpod or BLoC — no uncontrolled global state
|
|
71
|
+
- Strong componentization — small and focused widgets
|
|
72
|
+
- Organization by feature
|
|
73
|
+
- Naming: `camelCase` for variables/functions; `PascalCase` for classes; `snake_case` for files
|
|
74
|
+
- `map` is valid for pure 1-to-1 collection transforms; `for` loops are preferred for accumulation or complex logic
|
|
75
|
+
|
|
76
|
+
### Collections & UI Lists
|
|
77
|
+
|
|
78
|
+
> <rule name="FlutterCollections">
|
|
79
|
+
|
|
80
|
+
```dart
|
|
81
|
+
// ✅ map — valid for 1-to-1 transform (e.g. data to widget)
|
|
82
|
+
final userWidgets = users
|
|
83
|
+
.where((u) => u.isActive)
|
|
84
|
+
.map((u) => UserCard(user: u))
|
|
85
|
+
.toList();
|
|
86
|
+
|
|
87
|
+
// ✅ for loop — preferred for accumulation
|
|
88
|
+
double total = 0;
|
|
89
|
+
for (final item in order.items) {
|
|
90
|
+
final lineAmount = item.qty * item.price;
|
|
91
|
+
total += lineAmount;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> </rule>
|
|
96
|
+
|
|
97
|
+
</ruleset>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Go — Project Conventions
|
|
2
|
+
|
|
3
|
+
> Universal principles (naming, composition, DRY, performance, security) are in `../../core/staff-dna.md`.
|
|
4
|
+
> This file contains only decisions specific to this language and stack.
|
|
5
|
+
|
|
6
|
+
<ruleset name="GoConventions">
|
|
7
|
+
|
|
8
|
+
## Error Handling
|
|
9
|
+
|
|
10
|
+
- **Strategy**: Explicit `(T, error)`; no exceptions, no `panic` for business logic
|
|
11
|
+
- **Propagation**: Errors are explicitly returned; wrapping with `fmt.Errorf("ctx: %w", err)` for traceability
|
|
12
|
+
- **Domain errors**: Sentinel `var` for simple cases; custom `type` with `Error()` when necessary; `errors.Is` / `errors.As` for comparison
|
|
13
|
+
- **Never**: Ignore errors (`_`); `panic` for business rules; swallow errors
|
|
14
|
+
- **Global Handling**: HTTP handler converts `error` → response; structured logging (`slog`)
|
|
15
|
+
|
|
16
|
+
### Result Pattern (Go Style)
|
|
17
|
+
|
|
18
|
+
> <rule name="ResultPatternGo">
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type AppError struct {
|
|
22
|
+
Message string
|
|
23
|
+
Code string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func (e *AppError) Error() string {
|
|
27
|
+
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func FindUser(ctx context.Context, id string) (*User, error) {
|
|
31
|
+
if id == "" {
|
|
32
|
+
return nil, &AppError{Message: "id is required", Code: "INVALID_ID"}
|
|
33
|
+
}
|
|
34
|
+
user, err := db.QueryUser(ctx, id)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, fmt.Errorf("FindUser: %w", err)
|
|
37
|
+
}
|
|
38
|
+
return user, nil
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> </rule>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## HTTP & API
|
|
47
|
+
|
|
48
|
+
- **Framework**: `net/http` (preferred) or Echo/Fiber when necessary
|
|
49
|
+
- **Style**: API First + BFF
|
|
50
|
+
- **Route organization**: Vertical slice per feature/package; avoid generic packages (`utils`, `common`)
|
|
51
|
+
- **Middleware/hooks**: Auth via middleware; validation at the boundary; logging/tracing in the pipeline
|
|
52
|
+
- **DI**: Manual — explicit dependency passing via structs; constructor functions (`New...`); avoid containers
|
|
53
|
+
|
|
54
|
+
### Interface Design
|
|
55
|
+
|
|
56
|
+
> <rule name="InterfaceDesign">
|
|
57
|
+
> Interfaces defined by the consumer, not the provider. Small interfaces (1-2 methods). `-er` suffix for single-method.
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
type UserFinder interface {
|
|
61
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type OrderService struct {
|
|
65
|
+
users UserFinder
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func NewOrderService(users UserFinder) *OrderService {
|
|
69
|
+
return &OrderService{users: users}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> </rule>
|
|
74
|
+
|
|
75
|
+
### Context Propagation
|
|
76
|
+
|
|
77
|
+
> <rule name="ContextPropagation">
|
|
78
|
+
> `context.Context` is the first parameter of every function that performs I/O or can be canceled.
|
|
79
|
+
|
|
80
|
+
```go
|
|
81
|
+
func (s *Service) CreateOrder(ctx context.Context, input CreateOrderRequest) (*Order, error) {
|
|
82
|
+
user, err := s.users.FindByID(ctx, input.UserID)
|
|
83
|
+
if err != nil {
|
|
84
|
+
return nil, fmt.Errorf("CreateOrder: %w", err)
|
|
85
|
+
}
|
|
86
|
+
return s.orders.Save(ctx, newOrder(user, input))
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> </rule>
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Testing
|
|
95
|
+
|
|
96
|
+
- **Framework**: built-in (`testing`)
|
|
97
|
+
- **Style**: Table-driven tests (preferred)
|
|
98
|
+
- **Naming**: `TestShouldDoXWhenY`
|
|
99
|
+
- **Mocks**: Small interfaces for abstraction; mock only external I/O; avoid heavy frameworks
|
|
100
|
+
|
|
101
|
+
### Table-Driven Tests
|
|
102
|
+
|
|
103
|
+
> <rule name="TableTests">
|
|
104
|
+
|
|
105
|
+
```go
|
|
106
|
+
func TestParseVersion(t *testing.T) {
|
|
107
|
+
tests := []struct {
|
|
108
|
+
name string
|
|
109
|
+
input string
|
|
110
|
+
want float64
|
|
111
|
+
wantErr bool
|
|
112
|
+
}{
|
|
113
|
+
{name: "simple", input: "10", want: 10},
|
|
114
|
+
{name: "decimal", input: "3.14", want: 3.14},
|
|
115
|
+
{name: "empty", input: "", wantErr: true},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for _, tt := range tests {
|
|
119
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
120
|
+
got, err := ParseVersion(tt.input)
|
|
121
|
+
if (err != nil) != tt.wantErr {
|
|
122
|
+
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
|
|
123
|
+
}
|
|
124
|
+
if got != tt.want {
|
|
125
|
+
t.Errorf("got %v, want %v", got, tt.want)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> </rule>
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Types & Contracts
|
|
137
|
+
|
|
138
|
+
- **struct** for data; small interfaces defined by the consumer
|
|
139
|
+
- No nulls — use zero values carefully; pointers only when strictly necessary
|
|
140
|
+
- Well-defined JSON tags; never expose internal structs directly
|
|
141
|
+
- Manual validation or lightweight library (`go-playground/validator`); simple and explicit
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Go-Specific Delta
|
|
146
|
+
|
|
147
|
+
- Idiomatic > "beautiful architecture" — simplicity is the standard, not the exception
|
|
148
|
+
- Short and clear names; exported = PascalCase; unexported = camelCase; acronyms in caps (`ID`, `HTTP`)
|
|
149
|
+
- Early return for error flow — no `else` after `return err`
|
|
150
|
+
- Avoid "god structs" and "god packages" — focused packages by responsibility
|
|
151
|
+
- `context.Context` always applicable — cancellation and timeout propagated
|
|
152
|
+
- Managed lifecycle goroutines (`errgroup`, `WaitGroup`); never fire-and-forget
|
|
153
|
+
- `defer` immediately after resource acquisition
|
|
154
|
+
- Structured logging (`slog`)
|
|
155
|
+
- Package names: lowercase, single word, no underscores
|
|
156
|
+
- `for` loops are the standard for all collection operations; mapping and transformations follow the imperative pattern
|
|
157
|
+
|
|
158
|
+
### Goroutines & Concurrency
|
|
159
|
+
|
|
160
|
+
> <rule name="GoroutinesChannels">
|
|
161
|
+
|
|
162
|
+
```go
|
|
163
|
+
func ProcessBatch(ctx context.Context, items []Item) error {
|
|
164
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
165
|
+
for _, item := range items {
|
|
166
|
+
g.Go(func() error {
|
|
167
|
+
return processItem(ctx, item)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
return g.Wait()
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> </rule>
|
|
175
|
+
|
|
176
|
+
### Defer for Cleanup
|
|
177
|
+
|
|
178
|
+
> <rule name="DeferCleanup">
|
|
179
|
+
|
|
180
|
+
```go
|
|
181
|
+
func ReadConfig(path string) ([]byte, error) {
|
|
182
|
+
f, err := os.Open(path)
|
|
183
|
+
if err != nil {
|
|
184
|
+
return nil, fmt.Errorf("ReadConfig: %w", err)
|
|
185
|
+
}
|
|
186
|
+
defer f.Close()
|
|
187
|
+
return io.ReadAll(f)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
> </rule>
|
|
192
|
+
|
|
193
|
+
</ruleset>
|