guardian-framework 0.1.11 → 0.1.12
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/dist/cli.js +1 -1
- package/dist/exports.js +1 -1
- package/package.json +1 -1
- package/templates/pi/extensions/architect-lib/generators.ts +2 -0
- package/templates/pi/extensions/architect-lib/helpers.ts +21 -0
- package/templates/pi/extensions/architect.ts +28 -1
- package/templates/pi/preflight_report.json +2 -2
- package/templates/pi/skills/agents/go-codegen.md +42 -0
- package/templates/pi/skills/agents/java-codegen.md +42 -0
- package/templates/pi/skills/agents/python-codegen.md +41 -0
- package/templates/pi/skills/agents/typescript-codegen.md +40 -0
- package/templates/pi/skills/go-enterprise-codegen.md +450 -0
- package/templates/pi/skills/java-spring-enterprise-codegen.md +498 -0
- package/templates/pi/skills/python-enterprise-codegen.md +381 -0
- package/templates/pi/skills/typescript-enterprise-codegen.md +411 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# Go Enterprise Code Generation — DDD + Clean Architecture
|
|
2
|
+
|
|
3
|
+
> Canonical skill for generating production-grade Go code following Clean Architecture with DDD.
|
|
4
|
+
> All code MUST follow these patterns. Validators enforce compliance.
|
|
5
|
+
>
|
|
6
|
+
> Source: Clean Architecture principles + Go standard library patterns + DDD patterns from context7.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Project Structure — Clean Architecture with DDD
|
|
11
|
+
|
|
12
|
+
Every bounded context follows the same 4-layer structure:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
module/
|
|
16
|
+
├── domain/ # Pure domain entities, value objects, events
|
|
17
|
+
│ ├── entity.go # Aggregate roots and entities
|
|
18
|
+
│ ├── value.go # Value objects
|
|
19
|
+
│ ├── event.go # Domain event payloads
|
|
20
|
+
│ ├── error.go # Typed error definitions
|
|
21
|
+
│ └── repository.go # Repository interfaces
|
|
22
|
+
├── application/ # Service interfaces and DTOs
|
|
23
|
+
│ ├── service.go # Service interface definitions
|
|
24
|
+
│ └── dto.go # Input/Output DTOs
|
|
25
|
+
├── infrastructure/ # Repository implementations, external adapters
|
|
26
|
+
│ ├── repository/ # Repository implementations (GORM, SQL)
|
|
27
|
+
│ │ ├── module_repo.go
|
|
28
|
+
│ │ └── module_test.go
|
|
29
|
+
│ └── persistence/ # Database connection, migrations
|
|
30
|
+
└── interfaces/ # API contracts (HTTP)
|
|
31
|
+
├── handler.go # HTTP handler definitions
|
|
32
|
+
├── router.go # Route registration
|
|
33
|
+
└── dto.go # Request/Response DTOs
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Dependency Direction Rule (Inward Dependency)
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
domain → application → infrastructure → interfaces
|
|
40
|
+
↑ ↑
|
|
41
|
+
└── inner layers never depend on outer layers
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **domain/** — depends on nothing except standard library
|
|
45
|
+
- **application/** — depends on domain
|
|
46
|
+
- **infrastructure/** — depends on domain + application
|
|
47
|
+
- **interfaces/** — depends on application (HTTP/event handlers translate to domain calls)
|
|
48
|
+
|
|
49
|
+
### Package Naming Convention
|
|
50
|
+
|
|
51
|
+
```go
|
|
52
|
+
package module // domain/ — single domain package
|
|
53
|
+
package application // application/
|
|
54
|
+
package repository // infrastructure/repository/
|
|
55
|
+
package handler // interfaces/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. Domain Layer Entities
|
|
61
|
+
|
|
62
|
+
### Entity with Encapsulated State
|
|
63
|
+
|
|
64
|
+
```go
|
|
65
|
+
// entity.go — Domain entity with encapsulate state and behavior.
|
|
66
|
+
|
|
67
|
+
// Entity is a domain object with identity (ID field).
|
|
68
|
+
type Entity struct {
|
|
69
|
+
id primitive.ObjectID
|
|
70
|
+
status Status
|
|
71
|
+
createdAt time.Time
|
|
72
|
+
updatedAt time.Time
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func NewEntity(id primitive.ObjectID) *Entity {
|
|
76
|
+
return &Entity{
|
|
77
|
+
id: id,
|
|
78
|
+
status: StatusPending,
|
|
79
|
+
createdAt: time.Now(),
|
|
80
|
+
updatedAt: time.Now(),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ID returns the immutable entity identifier.
|
|
85
|
+
func (e *Entity) ID() primitive.ObjectID { return e.id }
|
|
86
|
+
|
|
87
|
+
// Status returns the current lifecycle status.
|
|
88
|
+
func (e *Entity) Status() Status { return e.status }
|
|
89
|
+
|
|
90
|
+
// transition encapsulates state mutations — never expose setters.
|
|
91
|
+
func (e *Entity) transition(newStatus Status) error {
|
|
92
|
+
if !e.status.CanTransitionTo(newStatus) {
|
|
93
|
+
return &DomainError{
|
|
94
|
+
Code: ErrInvalidTransition,
|
|
95
|
+
Message: fmt.Sprintf("cannot transition from %s to %s", e.status, newStatus),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
e.status = newStatus
|
|
99
|
+
e.updatedAt = time.Now()
|
|
100
|
+
return nil
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Value Object
|
|
105
|
+
|
|
106
|
+
```go
|
|
107
|
+
// value.go — Immutable value object.
|
|
108
|
+
|
|
109
|
+
// Money represents an amount in the smallest currency unit.
|
|
110
|
+
type Money struct {
|
|
111
|
+
amount int64
|
|
112
|
+
currency string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func NewMoney(amount int64, currency string) (Money, error) {
|
|
116
|
+
if amount < 0 {
|
|
117
|
+
return Money{}, &DomainError{Code: ErrValidation, Message: "amount must be non-negative"}
|
|
118
|
+
}
|
|
119
|
+
if len(currency) != 3 {
|
|
120
|
+
return Money{}, &DomainError{Code: ErrValidation, Message: "currency must be ISO 4217"}
|
|
121
|
+
}
|
|
122
|
+
return Money{amount: amount, currency: strings.ToUpper(currency)}, nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func (m Money) Amount() int64 { return m.amount }
|
|
126
|
+
func (m Money) Currency() string { return m.currency }
|
|
127
|
+
func (m Money) Add(other Money) (Money, error) {
|
|
128
|
+
if m.currency != other.currency {
|
|
129
|
+
return Money{}, &DomainError{Code: ErrCurrencyMismatch, Message: "currency mismatch"}
|
|
130
|
+
}
|
|
131
|
+
return Money{amount: m.amount + other.amount, currency: m.currency}, nil
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Repository Interface
|
|
136
|
+
|
|
137
|
+
```go
|
|
138
|
+
// repository.go — Repository interface defined in domain, implemented in infrastructure.
|
|
139
|
+
|
|
140
|
+
type Repository interface {
|
|
141
|
+
FindByID(ctx context.Context, id primitive.ObjectID) (*Entity, error)
|
|
142
|
+
Save(ctx context.Context, entity *Entity) error
|
|
143
|
+
Delete(ctx context.Context, id primitive.ObjectID) error
|
|
144
|
+
FindByStatus(ctx context.Context, status Status) ([]*Entity, error)
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 3. Domain Error Handling
|
|
151
|
+
|
|
152
|
+
```go
|
|
153
|
+
// error.go — Typed domain errors.
|
|
154
|
+
|
|
155
|
+
type ErrorCode string
|
|
156
|
+
|
|
157
|
+
const (
|
|
158
|
+
ErrNotFound ErrorCode = "NOT_FOUND"
|
|
159
|
+
ErrInvalidState ErrorCode = "INVALID_STATE"
|
|
160
|
+
ErrValidation ErrorCode = "VALIDATION"
|
|
161
|
+
ErrDuplicate ErrorCode = "DUPLICATE"
|
|
162
|
+
ErrCurrencyMismatch ErrorCode = "CURRENCY_MISMATCH"
|
|
163
|
+
ErrInvalidTransition ErrorCode = "INVALID_TRANSITION"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
type DomainError struct {
|
|
167
|
+
Code ErrorCode
|
|
168
|
+
Message string
|
|
169
|
+
Err error
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func (e *DomainError) Error() string {
|
|
173
|
+
if e.Err != nil {
|
|
174
|
+
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
|
|
175
|
+
}
|
|
176
|
+
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func (e *DomainError) Unwrap() error { return e.Err }
|
|
180
|
+
|
|
181
|
+
// Sentinel errors for direct comparison.
|
|
182
|
+
var (
|
|
183
|
+
ErrNotFoundInstance = &DomainError{Code: ErrNotFound, Message: "resource not found"}
|
|
184
|
+
ErrDuplicateInstance = &DomainError{Code: ErrDuplicate, Message: "duplicate resource"}
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Rules
|
|
189
|
+
- ✅ Use `*DomainError` with typed `ErrorCode` for all domain errors
|
|
190
|
+
- ✅ Sentinel errors for direct comparison via `errors.Is()`
|
|
191
|
+
- ✅ Use `fmt.Errorf("context: %w", err)` for wrapping with context
|
|
192
|
+
- ✅ Use `errors.As()` to extract typed errors from wrapped chains
|
|
193
|
+
- ❌ Never use `panic` for error handling
|
|
194
|
+
- ❌ Never use bare strings as errors
|
|
195
|
+
- ❌ Never use `log.Fatal` in library code
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 4. Application Layer — Services
|
|
200
|
+
|
|
201
|
+
```go
|
|
202
|
+
// service.go — Application service interface.
|
|
203
|
+
|
|
204
|
+
type Service interface {
|
|
205
|
+
Execute(ctx context.Context, cmd Command) (*Entity, error)
|
|
206
|
+
GetByID(ctx context.Context, id primitive.ObjectID) (*Entity, error)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type service struct {
|
|
210
|
+
repo Repository
|
|
211
|
+
logger *slog.Logger
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func NewService(repo Repository, logger *slog.Logger) Service {
|
|
215
|
+
return &service{repo: repo, logger: logger}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func (s *service) Execute(ctx context.Context, cmd Command) (*Entity, error) {
|
|
219
|
+
entity := NewEntity(primitive.NewObjectID())
|
|
220
|
+
// Domain logic — validation, state transitions
|
|
221
|
+
if err := entity.transition(cmd.DesiredStatus); err != nil {
|
|
222
|
+
return nil, fmt.Errorf("execute: %w", err)
|
|
223
|
+
}
|
|
224
|
+
if err := s.repo.Save(ctx, entity); err != nil {
|
|
225
|
+
return nil, fmt.Errorf("save entity: %w", err)
|
|
226
|
+
}
|
|
227
|
+
s.logger.InfoContext(ctx, "entity executed", "id", entity.ID())
|
|
228
|
+
return entity, nil
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Rules
|
|
233
|
+
- ✅ Services orchestrate domain operations — no business logic
|
|
234
|
+
- ✅ Services depend on domain interfaces (Repository), not concrete implementations
|
|
235
|
+
- ✅ Use `slog.Logger` for structured logging
|
|
236
|
+
- ✅ Each service method is a single use case
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 5. Infrastructure Layer — Repository Implementation
|
|
241
|
+
|
|
242
|
+
```go
|
|
243
|
+
// repository/module_repo.go — GORM-based repository implementation.
|
|
244
|
+
|
|
245
|
+
type gormRepository struct {
|
|
246
|
+
db *gorm.DB
|
|
247
|
+
logger *slog.Logger
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func NewGormRepository(db *gorm.DB, logger *slog.Logger) Repository {
|
|
251
|
+
return &gormRepository{db: db, logger: logger}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func (r *gormRepository) FindByID(ctx context.Context, id primitive.ObjectID) (*Entity, error) {
|
|
255
|
+
var model entityModel
|
|
256
|
+
if err := r.db.WithContext(ctx).First(&model, "id = ?", id).Error; err != nil {
|
|
257
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
258
|
+
return nil, fmt.Errorf("find by id %s: %w", id.Hex(), ErrNotFoundInstance)
|
|
259
|
+
}
|
|
260
|
+
return nil, fmt.Errorf("find by id %s: %w", id.Hex(), err)
|
|
261
|
+
}
|
|
262
|
+
return model.toEntity(), nil
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Rules
|
|
267
|
+
- ✅ Repository implementations live in `infrastructure/repository/`
|
|
268
|
+
- ✅ Each repository implements interfaces defined in `domain/`
|
|
269
|
+
- ✅ Use GORM, sqlx, or standard `database/sql` — never leak DB types into domain
|
|
270
|
+
- ✅ Convert ORM models to domain entities inside the repository
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 6. Interfaces Layer — HTTP Handlers
|
|
275
|
+
|
|
276
|
+
```go
|
|
277
|
+
// handler.go — HTTP handler that translates HTTP to application calls.
|
|
278
|
+
|
|
279
|
+
type Handler struct {
|
|
280
|
+
svc application.Service
|
|
281
|
+
logger *slog.Logger
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func NewHandler(svc application.Service, logger *slog.Logger) *Handler {
|
|
285
|
+
return &Handler{svc: svc, logger: logger}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
func (h *Handler) Execute(w http.ResponseWriter, r *http.Request) {
|
|
289
|
+
var req ExecuteRequest
|
|
290
|
+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
291
|
+
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
entity, err := h.svc.Execute(r.Context(), req.ToCommand())
|
|
296
|
+
if err != nil {
|
|
297
|
+
var de *DomainError
|
|
298
|
+
if errors.As(err, &de) {
|
|
299
|
+
http.Error(w, fmt.Sprintf(`{"error":"%s"}`, de.Message), errorHTTPStatus(de.Code))
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
h.logger.ErrorContext(r.Context(), "execute failed", "error", err)
|
|
303
|
+
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
w.Header().Set("Content-Type", "application/json")
|
|
308
|
+
json.NewEncoder(w).Encode(entityToResponse(entity))
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Rules
|
|
313
|
+
- ✅ Handlers translate HTTP to application calls — no business logic
|
|
314
|
+
- ✅ Always use structured error responses (JSON)
|
|
315
|
+
- ✅ Map domain errors to appropriate HTTP status codes
|
|
316
|
+
- ✅ Use standard `net/http` or chi/gin router, not framework-specific types in handlers
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 7. Configuration — Environment-Based
|
|
321
|
+
|
|
322
|
+
```go
|
|
323
|
+
type Config struct {
|
|
324
|
+
Port int `envconfig:"PORT" default:"8080"`
|
|
325
|
+
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"`
|
|
326
|
+
LogLevel slog.Level `envconfig:"LOG_LEVEL" default:"info"`
|
|
327
|
+
APIKey string `envconfig:"API_KEY"` // Loaded from env, never from config file
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func LoadConfig() (*Config, error) {
|
|
331
|
+
var cfg Config
|
|
332
|
+
if err := envconfig.Process("APP", &cfg); err != nil {
|
|
333
|
+
return nil, fmt.Errorf("load config: %w", err)
|
|
334
|
+
}
|
|
335
|
+
return &cfg, nil
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Rules
|
|
340
|
+
- ✅ Use `envconfig` or `viper` for configuration
|
|
341
|
+
- ✅ CLI flags > Environment > Config file > Defaults
|
|
342
|
+
- ✅ Secrets loaded ONLY from environment variables
|
|
343
|
+
- ✅ Validate on startup — fail fast
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 8. Testing Patterns
|
|
348
|
+
|
|
349
|
+
### Unit Tests
|
|
350
|
+
|
|
351
|
+
```go
|
|
352
|
+
func TestEntity_Transition(t *testing.T) {
|
|
353
|
+
e := NewEntity(primitive.NewObjectID())
|
|
354
|
+
|
|
355
|
+
// Initial state
|
|
356
|
+
assert.Equal(t, StatusPending, e.Status())
|
|
357
|
+
|
|
358
|
+
// Valid transition
|
|
359
|
+
err := e.transition(StatusActive)
|
|
360
|
+
assert.NoError(t, err)
|
|
361
|
+
assert.Equal(t, StatusActive, e.Status())
|
|
362
|
+
|
|
363
|
+
// Invalid transition
|
|
364
|
+
err = e.transition(StatusPending) // Can't go back
|
|
365
|
+
assert.Error(t, err)
|
|
366
|
+
var de *DomainError
|
|
367
|
+
assert.True(t, errors.As(err, &de))
|
|
368
|
+
assert.Equal(t, ErrInvalidTransition, de.Code)
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Repository Tests with Testcontainers
|
|
373
|
+
|
|
374
|
+
```go
|
|
375
|
+
func TestGormRepository(t *testing.T) {
|
|
376
|
+
// Spin up test PostgreSQL container
|
|
377
|
+
testDB, err := setupTestDatabase(t)
|
|
378
|
+
require.NoError(t, err)
|
|
379
|
+
|
|
380
|
+
repo := NewGormRepository(testDB, slog.Default())
|
|
381
|
+
|
|
382
|
+
entity := NewEntity(primitive.NewObjectID())
|
|
383
|
+
err = repo.Save(context.Background(), entity)
|
|
384
|
+
require.NoError(t, err)
|
|
385
|
+
|
|
386
|
+
found, err := repo.FindByID(context.Background(), entity.ID())
|
|
387
|
+
require.NoError(t, err)
|
|
388
|
+
assert.Equal(t, entity.Status(), found.Status())
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 9. Anti-Patterns — NEVER DO
|
|
395
|
+
|
|
396
|
+
```go
|
|
397
|
+
// ❌ Panic for error handling
|
|
398
|
+
if err != nil { panic(err) } // BAD
|
|
399
|
+
|
|
400
|
+
// ❌ Stringly-typed errors
|
|
401
|
+
errors.New("something went wrong") // BAD — use typed DomainError
|
|
402
|
+
|
|
403
|
+
// ❌ Global state
|
|
404
|
+
var db *gorm.DB // BAD — use dependency injection
|
|
405
|
+
|
|
406
|
+
// ❌ Business logic in handlers
|
|
407
|
+
func Handler(w http.ResponseWriter, r *http.Request) {
|
|
408
|
+
// complex business logic here // BAD — belongs in domain/application
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ❌ Leaking DB models to domain
|
|
412
|
+
type User struct { gorm.Model } // BAD — domain should not know about GORM
|
|
413
|
+
|
|
414
|
+
// ❌ Ignoring errors
|
|
415
|
+
json.NewEncoder(w).Encode(data) // BAD — always handle errors
|
|
416
|
+
|
|
417
|
+
// ❌ Background goroutines in handlers
|
|
418
|
+
go doSomething() // BAD — use proper worker pools with lifecycle management
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 10. Go Project-Level Structure
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
cmd/
|
|
427
|
+
server/
|
|
428
|
+
main.go // Entry point, DI setup
|
|
429
|
+
internal/
|
|
430
|
+
module/ // One directory per bounded context
|
|
431
|
+
domain/
|
|
432
|
+
application/
|
|
433
|
+
infrastructure/
|
|
434
|
+
repository/
|
|
435
|
+
interfaces/
|
|
436
|
+
pkg/
|
|
437
|
+
framework/ // Shared infrastructure
|
|
438
|
+
database.go
|
|
439
|
+
logger.go
|
|
440
|
+
router.go
|
|
441
|
+
docker-compose.yml
|
|
442
|
+
Dockerfile
|
|
443
|
+
go.mod
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
*Version: 1.0.0*
|
|
449
|
+
*Last updated: 2026-07-03*
|
|
450
|
+
*Source: Guardian DDD patterns + Clean Architecture + context7 (/wesionaryteam/go_clean_architecture, /golang/go)*
|