guardian-framework 0.1.23 → 0.1.25
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/preflight_report.json +1 -1
- package/templates/pi/skills/agents/design-system-codegen.md +41 -0
- package/templates/pi/skills/angular-enterprise-codegen.md +330 -78
- package/templates/pi/skills/design-system-enterprise-codegen.md +351 -0
- package/templates/pi/skills/nextjs-enterprise-codegen.md +319 -74
package/dist/cli.js
CHANGED
|
@@ -1335,7 +1335,7 @@ __export(exports_package, {
|
|
|
1335
1335
|
bin: () => bin,
|
|
1336
1336
|
author: () => author
|
|
1337
1337
|
});
|
|
1338
|
-
var name = "guardian-framework", version = "0.1.
|
|
1338
|
+
var name = "guardian-framework", version = "0.1.25", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
1339
1339
|
var init_package = __esm(() => {
|
|
1340
1340
|
exports = {
|
|
1341
1341
|
".": {
|
package/dist/exports.js
CHANGED
|
@@ -995,7 +995,7 @@ __export(exports_package, {
|
|
|
995
995
|
bin: () => bin,
|
|
996
996
|
author: () => author
|
|
997
997
|
});
|
|
998
|
-
var name = "guardian-framework", version = "0.1.
|
|
998
|
+
var name = "guardian-framework", version = "0.1.25", description = "Token-optimized agentic framework scaffolder with pi-first architecture", type = "module", main = "dist/exports.js", exports, types = "dist/exports.d.ts", bin, files, engines, scripts, publishConfig, pi, repository, homepage = "https://github.com/arman-jalili/guardian-framework#readme", bugs, dependencies, devDependencies, keywords, author = "Arman Wolkensteiner-Jalili", license = "MIT", package_default;
|
|
999
999
|
var init_package = __esm(() => {
|
|
1000
1000
|
exports = {
|
|
1001
1001
|
".": {
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-system-codegen
|
|
3
|
+
description: Minimal skill for design system architecture with DDD + adapter pattern. References full patterns on demand — never loads inline. Use when building design system components, tokens, or CSS integration.
|
|
4
|
+
model: inherit
|
|
5
|
+
tools: [Read, Grep]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Design System Code Generation — DDD + Adapter Pattern
|
|
9
|
+
|
|
10
|
+
> Full reference: `.pi/skills/design-system-enterprise-codegen.md`
|
|
11
|
+
|
|
12
|
+
## Quick Reference
|
|
13
|
+
|
|
14
|
+
| When you need... | Read this section |
|
|
15
|
+
|-----------------|-------------------|
|
|
16
|
+
| Structure overview | Section 1 — DDD module layout, framework agnostic |
|
|
17
|
+
| Design tokens | Section 2 — ColorToken, SpacingToken, TypeScale as domain values |
|
|
18
|
+
| Variant engine | Section 3 — defineVariants(), type-safe component props |
|
|
19
|
+
| CSS adapter (Tailwind) | Section 4 — Tailwind class generation from tokens |
|
|
20
|
+
| CSS adapter (CSS Modules) | Section 4 — CSS variable generation |
|
|
21
|
+
| CSS adapter (MUI/Material) | Section 4 — MUI theme mapping |
|
|
22
|
+
| Component pattern | Section 5 — Button component using application layer |
|
|
23
|
+
| Theming (dark mode) | Section 6 — ThemeService, data-theme attribute |
|
|
24
|
+
| Testing | Section 7 — token tests, variant engine tests |
|
|
25
|
+
| Choosing your stack | Section 8 — Tailwind vs CSS Modules vs MUI by use case |
|
|
26
|
+
|
|
27
|
+
## DDD Layer Contract for Design Systems
|
|
28
|
+
|
|
29
|
+
| Layer | Purpose | Example | CSS framework aware? |
|
|
30
|
+
|-------|---------|---------|---------------------|
|
|
31
|
+
| `domain/tokens/` | Typed design tokens | `ColorToken.PRIMARY` | No |
|
|
32
|
+
| `application/` | Variant resolution, theming | `defineVariants()` | No |
|
|
33
|
+
| `infrastructure/` | CSS adapter | `tailwind-adapter.ts` | **Yes** — this is the only layer |
|
|
34
|
+
| `interfaces/` | Rendered components | `button.tsx` | No (uses application layer) |
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
- NEVER read full reference — read specific sections
|
|
38
|
+
- Design tokens are pure TypeScript — zero CSS, zero framework imports
|
|
39
|
+
- Components import from `application/`, never from `infrastructure/` directly
|
|
40
|
+
- The adapter is the **only file** that changes when you switch CSS frameworks
|
|
41
|
+
- Pick one adapter per project — never mix Tailwind and MUI in the same component
|
|
@@ -1,158 +1,410 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: angular-enterprise-codegen
|
|
3
|
-
description: Full reference for Angular enterprise code generation with DDD + Clean Architecture. Covers module structure,
|
|
3
|
+
description: Full reference for Angular enterprise code generation with DDD + Clean Architecture. Covers module structure, standalone components, Signal-based state, RxJS patterns, design system integration, lazy loading, testing, and performance. Loaded on-demand via agents/angular-codegen.md skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Angular Enterprise Code Generation — DDD + Clean Architecture
|
|
7
7
|
|
|
8
8
|
> Canonical skill for generating production-grade Angular code with DDD patterns.
|
|
9
9
|
> All code MUST follow these patterns. Validators enforce compliance.
|
|
10
|
+
>
|
|
11
|
+
> For design system and styling patterns, see: `.pi/skills/design-system-enterprise-codegen.md`
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
|
-
## 1. Project Structure
|
|
15
|
+
## 1. Project Structure — DDD with Angular Standalone Components
|
|
14
16
|
|
|
15
17
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
src/
|
|
19
|
+
module/ # One bounded context
|
|
20
|
+
domain/ # Pure business logic, zero Angular imports
|
|
21
|
+
entity.ts # Domain entities with encapsulated state
|
|
22
|
+
value.ts # Immutable value objects
|
|
23
|
+
events.ts # Domain events
|
|
24
|
+
errors.ts # Typed error classes
|
|
25
|
+
application/ # Application logic
|
|
26
|
+
store.ts # Signal-based state management
|
|
27
|
+
service.ts # @Injectable use case service
|
|
28
|
+
facade.ts # Facade pattern (service → store)
|
|
29
|
+
dto.ts # Command/Query DTOs
|
|
30
|
+
infrastructure/ # External adapters
|
|
31
|
+
api-client.ts # HttpClient wrapper with interceptors
|
|
32
|
+
auth.ts # Auth provider SDK wrapper
|
|
33
|
+
analytics.ts # Analytics SDK wrapper
|
|
34
|
+
interfaces/ # UI layer
|
|
35
|
+
page.component.ts # Route component (standalone)
|
|
36
|
+
list.component.ts # Smart component
|
|
37
|
+
item.component.ts # Presentational component
|
|
38
|
+
form.component.ts # Form with Reactive Forms
|
|
39
|
+
shared/ # Cross-module shared code
|
|
40
|
+
ui/ # Design system components
|
|
41
|
+
button/
|
|
42
|
+
card/
|
|
43
|
+
modal/
|
|
44
|
+
lib/ # Shared utilities
|
|
45
|
+
pipes/ # Shared Angular pipes
|
|
31
46
|
```
|
|
32
47
|
|
|
33
48
|
### Dependency Rule
|
|
34
49
|
```
|
|
35
50
|
domain → application → infrastructure → interfaces
|
|
51
|
+
↕
|
|
52
|
+
shared/ (no framework deps)
|
|
36
53
|
```
|
|
37
54
|
|
|
55
|
+
- **domain/** — zero Angular imports. Pure TypeScript. No decorators.
|
|
56
|
+
- **application/** — imports from domain. Uses `@Injectable` for DI.
|
|
57
|
+
- **infrastructure/** — imports from domain + application. Uses Angular `HttpClient`.
|
|
58
|
+
- **interfaces/** — imports from application. Standalone components with `@Component`.
|
|
59
|
+
|
|
38
60
|
---
|
|
39
61
|
|
|
40
|
-
## 2.
|
|
62
|
+
## 2. Component Architecture
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
// domain/entity.ts — No Angular imports
|
|
44
|
-
export class Order {
|
|
45
|
-
private readonly _id: string;
|
|
46
|
-
private _status: OrderStatus = OrderStatus.PENDING;
|
|
47
|
-
|
|
48
|
-
constructor(id?: string) {
|
|
49
|
-
this._id = id ?? crypto.randomUUID();
|
|
50
|
-
}
|
|
64
|
+
### Smart vs Presentational Components
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
| Type | Location | Responsibility |
|
|
67
|
+
|------|----------|---------------|
|
|
68
|
+
| Smart (Page) | `interfaces/page.component.ts` | Route handler, orchestrates data flow |
|
|
69
|
+
| Smart (List) | `interfaces/list.component.ts` | Uses store/service, passes data down |
|
|
70
|
+
| Presentational | `interfaces/item.component.ts` | Pure @Input/@Output, no DI |
|
|
71
|
+
| Form | `interfaces/form.component.ts` | Reactive Forms, validation |
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
```typescript
|
|
74
|
+
// interfaces/list.component.ts — Smart component
|
|
75
|
+
@Component({
|
|
76
|
+
selector: 'app-order-list',
|
|
77
|
+
standalone: true,
|
|
78
|
+
imports: [OrderItemComponent, AsyncPipe],
|
|
79
|
+
template: `
|
|
80
|
+
@for (order of orders$ | async; track order.id) {
|
|
81
|
+
<app-order-item [order]="order" (select)="onSelect($event)" />
|
|
82
|
+
} @empty {
|
|
83
|
+
<empty-state />
|
|
58
84
|
}
|
|
59
|
-
|
|
85
|
+
`,
|
|
86
|
+
})
|
|
87
|
+
export class OrderListComponent {
|
|
88
|
+
private readonly service = inject(OrderService);
|
|
89
|
+
readonly orders$ = this.service.orders$;
|
|
90
|
+
|
|
91
|
+
onSelect(order: Order): void {
|
|
92
|
+
this.service.selectOrder(order.id);
|
|
60
93
|
}
|
|
61
94
|
}
|
|
95
|
+
|
|
96
|
+
// interfaces/item.component.ts — Presentational component
|
|
97
|
+
@Component({
|
|
98
|
+
selector: 'app-order-item',
|
|
99
|
+
standalone: true,
|
|
100
|
+
template: `
|
|
101
|
+
<div (click)="select.emit(order.id)">
|
|
102
|
+
<h3>{{ order.id }}</h3>
|
|
103
|
+
<span [class]="statusClass(order.status)">{{ order.status }}</span>
|
|
104
|
+
</div>
|
|
105
|
+
`,
|
|
106
|
+
})
|
|
107
|
+
export class OrderItemComponent {
|
|
108
|
+
@Input({ required: true }) order!: Order;
|
|
109
|
+
@Output() select = new EventEmitter<string>();
|
|
110
|
+
}
|
|
62
111
|
```
|
|
63
112
|
|
|
64
113
|
---
|
|
65
114
|
|
|
66
|
-
## 3.
|
|
115
|
+
## 3. State Management with Signals
|
|
67
116
|
|
|
68
117
|
```typescript
|
|
69
|
-
// application/
|
|
70
|
-
import {
|
|
71
|
-
import { Order } from '../domain/entity';
|
|
118
|
+
// application/store.ts — Signal-based state
|
|
119
|
+
import { signal, computed, Injectable } from '@angular/core';
|
|
120
|
+
import { Order, OrderStatus } from '../domain/entity';
|
|
72
121
|
|
|
73
122
|
@Injectable({ providedIn: 'root' })
|
|
74
|
-
export class
|
|
123
|
+
export class OrderStore {
|
|
75
124
|
private readonly orders = signal<Order[]>([]);
|
|
125
|
+
private readonly selectedId = signal<string | null>(null);
|
|
126
|
+
private readonly isLoading = signal(false);
|
|
127
|
+
|
|
76
128
|
readonly orders$ = this.orders.asReadonly();
|
|
129
|
+
readonly selectedOrder = computed(() => {
|
|
130
|
+
const id = this.selectedId();
|
|
131
|
+
return id ? this.orders().find(o => o.id === id) ?? null : null;
|
|
132
|
+
});
|
|
133
|
+
readonly isLoading$ = this.isLoading.asReadonly();
|
|
134
|
+
readonly pendingCount = computed(() =>
|
|
135
|
+
this.orders().filter(o => o.status === OrderStatus.PENDING).length
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
async loadOrders(): Promise<void> {
|
|
139
|
+
this.isLoading.set(true);
|
|
140
|
+
try {
|
|
141
|
+
const orders = await this.api.fetchOrders();
|
|
142
|
+
this.orders.set(orders);
|
|
143
|
+
} finally {
|
|
144
|
+
this.isLoading.set(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
77
147
|
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
const order = items.find(o => o.id === id);
|
|
81
|
-
if (!order) throw new DomainError('NOT_FOUND', 'Order not found');
|
|
82
|
-
order.confirm();
|
|
83
|
-
return [...items];
|
|
84
|
-
});
|
|
148
|
+
selectOrder(id: string): void {
|
|
149
|
+
this.selectedId.set(id);
|
|
85
150
|
}
|
|
86
151
|
}
|
|
87
152
|
```
|
|
88
153
|
|
|
154
|
+
### Rules
|
|
155
|
+
- ✅ Use `signal()` over `BehaviorSubject` for state (Angular 17+)
|
|
156
|
+
- ✅ Use `computed()` for derived state — no manual subscriptions
|
|
157
|
+
- ✅ Use `effect()` only for side effects (logging, syncing to localStorage)
|
|
158
|
+
- ❌ Don't mix Signals and RxJS in the same store — pick one pattern
|
|
159
|
+
- ❌ Don't use `async` pipe with Signals — Signals work with `@for` directly
|
|
160
|
+
|
|
89
161
|
---
|
|
90
162
|
|
|
91
|
-
## 4.
|
|
163
|
+
## 4. Data Flow with RxJS
|
|
92
164
|
|
|
93
165
|
```typescript
|
|
94
|
-
//
|
|
95
|
-
import { HttpClient } from '@angular/common/http';
|
|
96
|
-
import { Injectable, inject } from '@angular/core';
|
|
97
|
-
|
|
166
|
+
// application/service.ts — RxJS for async operations
|
|
98
167
|
@Injectable({ providedIn: 'root' })
|
|
99
|
-
export class
|
|
168
|
+
export class OrderService {
|
|
100
169
|
private readonly http = inject(HttpClient);
|
|
170
|
+
private readonly store = inject(OrderStore);
|
|
171
|
+
|
|
172
|
+
// Stream: debounced search → API call → update store
|
|
173
|
+
readonly search = new Subject<string>();
|
|
174
|
+
|
|
175
|
+
constructor() {
|
|
176
|
+
this.search.pipe(
|
|
177
|
+
debounceTime(300),
|
|
178
|
+
distinctUntilChanged(),
|
|
179
|
+
switchMap(query => this.http.get<Order[]>(`/api/orders?q=${query}`)),
|
|
180
|
+
catchError(err => {
|
|
181
|
+
console.error('Search failed', err);
|
|
182
|
+
return of([]);
|
|
183
|
+
}),
|
|
184
|
+
).subscribe(orders => this.store.setOrders(orders));
|
|
185
|
+
}
|
|
101
186
|
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
// Server mutation with optimistic update
|
|
188
|
+
confirmOrder(id: string): void {
|
|
189
|
+
const previous = this.store.orders$();
|
|
190
|
+
this.store.updateOrder(id, OrderStatus.CONFIRMED); // Optimistic
|
|
191
|
+
|
|
192
|
+
this.http.post(`/api/orders/${id}/confirm`, {}).pipe(
|
|
193
|
+
catchError(err => {
|
|
194
|
+
this.store.setOrders(previous); // Rollback
|
|
195
|
+
throw err;
|
|
196
|
+
}),
|
|
197
|
+
).subscribe();
|
|
104
198
|
}
|
|
105
199
|
}
|
|
106
200
|
```
|
|
107
201
|
|
|
202
|
+
### Rules
|
|
203
|
+
- ✅ Services handle async (HTTP, timers), Stores hold synchronous state
|
|
204
|
+
- ✅ Use `switchMap` for search/autocomplete (cancel previous)
|
|
205
|
+
- ✅ Use `exhaustMap` for mutations (ignore while in-flight)
|
|
206
|
+
- ✅ Always `catchError` in service streams — never let errors propagate unhandled
|
|
207
|
+
- ❌ Never subscribe in components — use `async` pipe or `@for` with Signals
|
|
208
|
+
- ❌ Never put business logic in RxJS pipes — that's the domain layer's job
|
|
209
|
+
|
|
108
210
|
---
|
|
109
211
|
|
|
110
|
-
## 5.
|
|
212
|
+
## 5. Design System & CSS Architecture
|
|
111
213
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
214
|
+
### Structure
|
|
215
|
+
```
|
|
216
|
+
src/
|
|
217
|
+
shared/
|
|
218
|
+
ui/ # Design system components
|
|
219
|
+
tokens/ # Design tokens
|
|
220
|
+
_colors.scss
|
|
221
|
+
_typography.scss
|
|
222
|
+
_spacing.scss
|
|
223
|
+
button/
|
|
224
|
+
button.component.ts
|
|
225
|
+
button.component.scss
|
|
226
|
+
button.test.ts
|
|
227
|
+
card/
|
|
228
|
+
form/
|
|
229
|
+
input/
|
|
230
|
+
select/
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Styling Strategy
|
|
234
|
+
|
|
235
|
+
| Approach | When to use |
|
|
236
|
+
|----------|-------------|
|
|
237
|
+
| Component SCSS (`@Component styleUrls`) | Component-scoped styles |
|
|
238
|
+
| Global SCSS (`styles.scss`) | CSS reset, typography, CSS custom properties |
|
|
239
|
+
| CSS Custom Properties | Design tokens, theming |
|
|
240
|
+
| Tailwind (via ngClass) | Layout, one-off adjustments |
|
|
241
|
+
|
|
242
|
+
```scss
|
|
243
|
+
// shared/ui/tokens/_colors.scss
|
|
244
|
+
:root {
|
|
245
|
+
--color-primary: #0f766e;
|
|
246
|
+
--color-primary-foreground: #ffffff;
|
|
247
|
+
--color-secondary: #64748b;
|
|
248
|
+
--color-destructive: #dc2626;
|
|
249
|
+
--color-background: #ffffff;
|
|
250
|
+
--color-foreground: #0f172a;
|
|
251
|
+
}
|
|
116
252
|
|
|
253
|
+
// button.component.scss
|
|
254
|
+
@use '../../tokens/colors' as *;
|
|
255
|
+
|
|
256
|
+
.button {
|
|
257
|
+
display: inline-flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: center;
|
|
260
|
+
border-radius: 6px;
|
|
261
|
+
font-size: 0.875rem;
|
|
262
|
+
font-weight: 500;
|
|
263
|
+
transition: all 0.2s;
|
|
264
|
+
|
|
265
|
+
&--primary {
|
|
266
|
+
background: var(--color-primary);
|
|
267
|
+
color: var(--color-primary-foreground);
|
|
268
|
+
}
|
|
269
|
+
&--secondary {
|
|
270
|
+
background: var(--color-secondary);
|
|
271
|
+
color: #fff;
|
|
272
|
+
}
|
|
273
|
+
&--sm { height: 2.25rem; padding: 0 0.75rem; }
|
|
274
|
+
&--md { height: 2.5rem; padding: 0 1rem; }
|
|
275
|
+
&--lg { height: 2.75rem; padding: 0 2rem; }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// button.component.ts
|
|
117
281
|
@Component({
|
|
118
|
-
selector: 'app-
|
|
282
|
+
selector: 'app-button',
|
|
119
283
|
standalone: true,
|
|
120
284
|
template: `
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
</div>
|
|
285
|
+
<button [class]="'button button--' + variant() + ' button--' + size()">
|
|
286
|
+
<ng-content />
|
|
287
|
+
</button>
|
|
126
288
|
`,
|
|
127
289
|
})
|
|
128
|
-
export class
|
|
129
|
-
readonly
|
|
290
|
+
export class ButtonComponent {
|
|
291
|
+
readonly variant = input<'primary' | 'secondary'>('primary');
|
|
292
|
+
readonly size = input<'sm' | 'md' | 'lg'>('md');
|
|
130
293
|
}
|
|
131
294
|
```
|
|
132
295
|
|
|
133
296
|
---
|
|
134
297
|
|
|
135
|
-
## 6.
|
|
298
|
+
## 6. Lazy Loading & Routing
|
|
136
299
|
|
|
137
300
|
```typescript
|
|
138
|
-
|
|
139
|
-
|
|
301
|
+
// app.routes.ts — Lazy-loaded modules
|
|
302
|
+
export const routes: Routes = [
|
|
303
|
+
{
|
|
304
|
+
path: 'orders',
|
|
305
|
+
loadChildren: () => import('./orders/orders.routes'),
|
|
306
|
+
canActivate: [AuthGuard],
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
path: 'checkout',
|
|
310
|
+
loadComponent: () => import('./checkout/page.component'),
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
// orders/orders.routes.ts
|
|
315
|
+
export default [
|
|
316
|
+
{ path: '', component: OrderListComponent },
|
|
317
|
+
{ path: ':id', component: OrderDetailComponent },
|
|
318
|
+
] satisfies Routes;
|
|
319
|
+
```
|
|
140
320
|
|
|
141
|
-
|
|
142
|
-
let service: OrderService;
|
|
321
|
+
---
|
|
143
322
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
323
|
+
## 7. Error Handling
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// domain/errors.ts
|
|
327
|
+
export class DomainError extends Error {
|
|
328
|
+
constructor(
|
|
329
|
+
public readonly code: string,
|
|
330
|
+
message: string,
|
|
331
|
+
public readonly httpStatus: number = 400,
|
|
332
|
+
) {
|
|
333
|
+
super(message);
|
|
334
|
+
this.name = 'DomainError';
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// infrastructure/api-client.ts — HTTP interceptor
|
|
339
|
+
@Injectable()
|
|
340
|
+
export class ErrorInterceptor implements HttpInterceptor {
|
|
341
|
+
intercept(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
|
|
342
|
+
return next(req).pipe(
|
|
343
|
+
catchError((error: HttpErrorResponse) => {
|
|
344
|
+
const domainError = new DomainError(
|
|
345
|
+
error.error?.code ?? 'UNKNOWN',
|
|
346
|
+
error.error?.message ?? 'An unexpected error occurred',
|
|
347
|
+
error.status,
|
|
348
|
+
);
|
|
349
|
+
console.error(`[${domainError.code}] ${domainError.message}`);
|
|
350
|
+
return throwError(() => domainError);
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
148
356
|
|
|
149
|
-
|
|
150
|
-
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 8. Testing Patterns
|
|
360
|
+
|
|
361
|
+
| Test type | Tool | What to test |
|
|
362
|
+
|-----------|------|-------------|
|
|
363
|
+
| Unit | Jest / Vitest | `domain/` entities, value objects |
|
|
364
|
+
| Component | Angular TestBed | `interfaces/` components |
|
|
365
|
+
| Service | TestBed + HttpTestingController | HTTP interactions |
|
|
366
|
+
| E2E | Playwright | Full user flows |
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// domain/entity.test.ts
|
|
370
|
+
describe('Order', () => {
|
|
371
|
+
it('transitions from pending to confirmed', () => {
|
|
372
|
+
const order = new Order();
|
|
373
|
+
order.confirm();
|
|
374
|
+
expect(order.status).toBe(OrderStatus.CONFIRMED);
|
|
151
375
|
});
|
|
152
376
|
});
|
|
153
377
|
```
|
|
154
378
|
|
|
155
379
|
---
|
|
156
380
|
|
|
157
|
-
|
|
381
|
+
## 9. Anti-Patterns — NEVER DO
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// ❌ Business logic in components
|
|
385
|
+
export class OrderComponent {
|
|
386
|
+
calculateTotal(items: Item[]): number { ... } // BAD — belongs in domain/
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ❌ Subscribing in components without cleanup
|
|
390
|
+
ngOnInit() { this.service.orders$.subscribe(); } // BAD — use async pipe
|
|
391
|
+
|
|
392
|
+
// ❌ Mutable state without Signals
|
|
393
|
+
public orders: Order[] = []; // BAD — use signal()
|
|
394
|
+
|
|
395
|
+
// ❌ Direct HTTP calls in components
|
|
396
|
+
this.http.get('/api/orders'); // BAD — wrap in infrastructure/
|
|
397
|
+
|
|
398
|
+
// ❌ Domain entities with Angular decorators
|
|
399
|
+
export class Order {
|
|
400
|
+
@Input() id: string; // BAD — domain is Angular-free
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ❌ CSS in component TS
|
|
404
|
+
styles: [':host { display: block; }'] // BAD — use SCSS files
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
*Version: 1.1.0*
|
|
158
410
|
*Last updated: 2026-07-03*
|