guardian-framework 0.1.22 → 0.1.24

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.
@@ -0,0 +1,410 @@
1
+ ---
2
+ name: angular-enterprise-codegen
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
+ ---
5
+
6
+ # Angular Enterprise Code Generation — DDD + Clean Architecture
7
+
8
+ > Canonical skill for generating production-grade Angular code with DDD patterns.
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`
12
+
13
+ ---
14
+
15
+ ## 1. Project Structure — DDD with Angular Standalone Components
16
+
17
+ ```
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
46
+ ```
47
+
48
+ ### Dependency Rule
49
+ ```
50
+ domain → application → infrastructure → interfaces
51
+
52
+ shared/ (no framework deps)
53
+ ```
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
+
60
+ ---
61
+
62
+ ## 2. Component Architecture
63
+
64
+ ### Smart vs Presentational Components
65
+
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 |
72
+
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 />
84
+ }
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);
93
+ }
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
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 3. State Management with Signals
116
+
117
+ ```typescript
118
+ // application/store.ts — Signal-based state
119
+ import { signal, computed, Injectable } from '@angular/core';
120
+ import { Order, OrderStatus } from '../domain/entity';
121
+
122
+ @Injectable({ providedIn: 'root' })
123
+ export class OrderStore {
124
+ private readonly orders = signal<Order[]>([]);
125
+ private readonly selectedId = signal<string | null>(null);
126
+ private readonly isLoading = signal(false);
127
+
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
+ }
147
+
148
+ selectOrder(id: string): void {
149
+ this.selectedId.set(id);
150
+ }
151
+ }
152
+ ```
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
+
161
+ ---
162
+
163
+ ## 4. Data Flow with RxJS
164
+
165
+ ```typescript
166
+ // application/service.ts — RxJS for async operations
167
+ @Injectable({ providedIn: 'root' })
168
+ export class OrderService {
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
+ }
186
+
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();
198
+ }
199
+ }
200
+ ```
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
+
210
+ ---
211
+
212
+ ## 5. Design System & CSS Architecture
213
+
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
+ }
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
281
+ @Component({
282
+ selector: 'app-button',
283
+ standalone: true,
284
+ template: `
285
+ <button [class]="'button button--' + variant() + ' button--' + size()">
286
+ <ng-content />
287
+ </button>
288
+ `,
289
+ })
290
+ export class ButtonComponent {
291
+ readonly variant = input<'primary' | 'secondary'>('primary');
292
+ readonly size = input<'sm' | 'md' | 'lg'>('md');
293
+ }
294
+ ```
295
+
296
+ ---
297
+
298
+ ## 6. Lazy Loading & Routing
299
+
300
+ ```typescript
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
+ ```
320
+
321
+ ---
322
+
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
+ ```
356
+
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);
375
+ });
376
+ });
377
+ ```
378
+
379
+ ---
380
+
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*
410
+ *Last updated: 2026-07-03*