thefinalboss commited on
Commit
c2a1e0d
·
verified ·
1 Parent(s): f55785e

Add AICL example: 25_hexagonal_arch.aicl

Browse files
data/aicl/examples/25_hexagonal_arch.aicl ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AICL Example: Hexagonal Architecture
2
+ # Implements a hexagonal (ports and adapters) architecture with clean domain core isolation,
3
+ # inbound and outbound ports, infrastructure adapters, and comprehensive testability guarantees.
4
+
5
+ # Level 1: Architecture
6
+ Goal: Build a hexagonal architecture that isolates the domain core from all infrastructure concerns,
7
+ defines explicit ports for inbound (driving) and outbound (driven) interactions,
8
+ provides swappable adapters for each port, and guarantees the domain is fully testable
9
+ without any infrastructure dependency.
10
+
11
+ Constraint: Domain core must have zero imports from infrastructure or framework packages.
12
+ Constraint: All external interactions must flow through declared ports with defined interfaces.
13
+ Constraint: Adapters must depend on ports; ports must never depend on adapters.
14
+ Constraint: Each port must have at least one production adapter and one test adapter.
15
+ Constraint: Domain events must be defined in the domain and published through an outbound port.
16
+
17
+ Risk: Domain logic leaks into adapter implementation
18
+ Recovery: Enforce dependency rule via architectural fitness functions in CI; auto-fail builds on violation
19
+
20
+ Risk: Port interface too generic reduces domain expressiveness
21
+ Recovery: Design ports with domain-specific method names; avoid generic CRUD-style port interfaces
22
+
23
+ Risk: Adapter state inconsistency during domain operation
24
+ Recovery: Wrap multi-port operations in unit of work; ensure atomic commit across adapters
25
+
26
+ Risk: Test adapter diverges from production adapter behavior
27
+ Recovery: Run contract tests against both adapters; validate output equivalence for same inputs
28
+
29
+ Risk: Infrastructure exception propagates into domain core
30
+ Recovery: Adapter must catch and translate all infrastructure exceptions to domain error types at port boundary
31
+
32
+ Risk: Missing adapter for required port at startup
33
+ Recovery: Validate all ports have registered adapters at application bootstrap; fail fast with missing list
34
+
35
+ Layer: Domain
36
+ SubLayer: DomainModel
37
+ SubLayer: DomainService
38
+ SubLayer: DomainEvent
39
+ SubLayer: ValueObject
40
+ Layer: Application
41
+ SubLayer: InboundPort
42
+ SubLayer: UseCase
43
+ SubLayer: OutboundPort
44
+ SubLayer: PortRegistry
45
+ Layer: Infrastructure
46
+ SubLayer: PersistenceAdapter
47
+ SubLayer: MessagingAdapter
48
+ SubLayer: ExternalServiceAdapter
49
+ SubLayer: TestAdapter
50
+
51
+ Validation: Domain package must have no imports from infrastructure or adapter packages
52
+ Validation: Every outbound port must have at least one adapter implementation
53
+ Validation: Every inbound port must have at least one driving adapter
54
+ Validation: Adapter must not expose infrastructure types through port interface
55
+ Validation: Domain model invariants must hold after any use case execution
56
+ Validation: Test adapter must pass same contract tests as production adapter
57
+
58
+ # Level 2: Entities
59
+ Entity Port
60
+ portId: string
61
+ portName: string
62
+ direction: string
63
+ interfaceSignature: dict
64
+ methods: list
65
+ adapterCount: integer
66
+ isActive: boolean
67
+ domainArea: string
68
+
69
+ Entity Adapter
70
+ adapterId: string
71
+ adapterName: string
72
+ portName: string
73
+ adapterType: string
74
+ technology: string
75
+ config: dict
76
+ isPrimary: boolean
77
+ healthStatus: string
78
+ registeredAt: datetime
79
+
80
+ Entity DomainModel
81
+ modelId: string
82
+ modelName: string
83
+ aggregateType: string
84
+ invariants: list
85
+ methods: list
86
+ events: list
87
+ dependencies: list
88
+ isImmutable: boolean
89
+
90
+ Entity UseCase
91
+ useCaseId: string
92
+ useCaseName: string
93
+ inboundPort: string
94
+ outboundPorts: list
95
+ preconditions: list
96
+ postconditions: list
97
+ errorCases: list
98
+ transactionBoundary: string
99
+
100
+ Entity DomainEvent
101
+ eventId: string
102
+ eventType: string
103
+ aggregateId: string
104
+ payload: dict
105
+ occurredAt: datetime
106
+ publishedVia: string
107
+ schemaVersion: string
108
+ isImmutable: boolean
109
+
110
+ Entity PortContract
111
+ contractId: string
112
+ portName: string
113
+ testCases: list
114
+ invariantChecks: list
115
+ lastValidatedAt: datetime
116
+ productionPassRate: float
117
+ testAdapterPassRate: float
118
+ driftDetected: boolean
119
+
120
+ # Level 3: Behaviors
121
+ Behavior RegisterAdapter
122
+ Input:
123
+ adapter: Adapter
124
+ portName: string
125
+ Output:
126
+ registrationId: string
127
+ Action:
128
+ Validate adapter implements port interface fully
129
+ Run contract test suite against adapter
130
+ Compare results with existing adapter for drift
131
+ Register adapter with port in port registry
132
+ Mark as primary if no other adapter is primary
133
+ Validate port now has at least one adapter
134
+ Return registration confirmation
135
+
136
+ Behavior ExecuteUseCase
137
+ Input:
138
+ useCaseName: string
139
+ inputPayload: dict
140
+ Output:
141
+ result: dict
142
+ events: list
143
+ Action:
144
+ Resolve use case from registry
145
+ Validate preconditions against input
146
+ Begin transaction boundary
147
+ Execute domain logic through inbound port
148
+ Persist state changes via outbound port
149
+ Publish domain events via outbound port
150
+ Validate postconditions
151
+ Commit transaction
152
+ Return result and emitted events
153
+
154
+ Behavior TranslateException
155
+ Input:
156
+ adapterId: string
157
+ infrastructureException: any
158
+ portMethod: string
159
+ Output:
160
+ domainError: dict
161
+ Action:
162
+ Catch infrastructure exception at adapter boundary
163
+ Map exception type to domain error category
164
+ Preserve root cause in domain error details
165
+ Strip infrastructure-specific stack frames
166
+ Log translation event for debugging
167
+ Return domain-appropriate error type
168
+
169
+ Behavior ValidateArchitecture
170
+ Input:
171
+ applicationRoot: string
172
+ Output:
173
+ valid: boolean
174
+ violations: list
175
+ Action:
176
+ Scan all import dependencies in domain package
177
+ Flag any import from infrastructure or adapter package
178
+ Verify all declared ports have adapter implementations
179
+ Check inbound ports are only called from adapters
180
+ Check domain events are only published through outbound ports
181
+ Return validation result with any violations
182
+
183
+ Behavior RunContractTest
184
+ Input:
185
+ portName: string
186
+ adapterId: string
187
+ Output:
188
+ passRate: float
189
+ failures: list
190
+ Action:
191
+ Load contract test suite for port
192
+ Execute each test case against adapter
193
+ Compare output with expected results
194
+ Detect behavioral drift from reference adapter
195
+ Record pass rate and failure details
196
+ Alert if drift exceeds threshold
197
+ Return test results
198
+
199
+ Behavior PublishDomainEvent
200
+ Input:
201
+ event: DomainEvent
202
+ portName: string
203
+ Output:
204
+ published: boolean
205
+ Action:
206
+ Validate event schema matches declared type
207
+ Resolve outbound port for event publishing
208
+ Translate event to adapter-specific format
209
+ Publish via adapter without blocking use case
210
+ Handle publish failure with retry or dead letter
211
+ Return publication status
212
+
213
+ # Level 4: Conditions
214
+ Condition: ArchitectureViolationDetected
215
+ When domain package imports from infrastructure or adapter package
216
+ Then fail CI build with violation report listing illegal dependencies and suggested fixes
217
+
218
+ Condition: AdapterContractDrift
219
+ When test adapter and production adapter produce different results for same input
220
+ Then flag drift in port contract and require review before next deployment
221
+
222
+ Condition: MissingAdapterAtStartup
223
+ When port has no registered adapter at application initialization
224
+ Then fail application startup with error listing all ports missing adapters
225
+
226
+ # Level 5: Events
227
+ Event: OnAdapterRegistered
228
+ On new adapter registered for a port
229
+ Action: Run contract tests, update port registry, notify dependent use cases
230
+
231
+ Event: OnDomainEventPublished
232
+ On domain event published through outbound port
233
+ Action: Log event for audit trail, trigger any registered side-effect handlers
234
+
235
+ Event: OnArchitectureViolation
236
+ On fitness function detects domain leaking infrastructure dependency
237
+ Action: Block deployment, create architectural debt ticket, notify team lead
238
+
239
+ Event: OnContractTestFailed
240
+ On adapter fails contract test against port specification
241
+ Action: Alert adapter owner, prevent adapter from being marked as primary
242
+
243
+ # Level 6: Concurrency
244
+ Parallel:
245
+ Contract test execution across multiple adapters simultaneously
246
+ Domain event publishing to multiple outbound port adapters
247
+ Architecture validation scanning across all packages
248
+ Use case execution with independent port interactions
249
+ Adapter health monitoring for all registered adapters
250
+
251
+ # Level 7: Optimization
252
+ Optimize: Adapter resolution latency
253
+ Priority: Cache adapter lookup per port; pre-instantiate stateless adapters; pool stateful ones
254
+
255
+ Optimize: Contract test execution time
256
+ Priority: Run contract tests in parallel per adapter; incremental testing on changed adapters only
257
+
258
+ # Level 8: Learning
259
+ Learn: Adapter performance characteristics
260
+ Goal: Select optimal adapter per port based on operation profile and performance requirements
261
+ Adapt: Primary adapter selection per port
262
+ Based: Adapter latency, throughput, and error rate metrics across different operation types
263
+
264
+ Learn: Domain model complexity trends
265
+ Goal: Identify domain areas growing too complex and suggest decomposition
266
+ Adapt: Use case and aggregate boundary recommendations
267
+ Based: Method count, dependency depth, and invariant complexity per domain model
268
+
269
+ # Level 9: Security
270
+ Security:
271
+ Encrypt: All data crossing port boundaries with TLS for network adapters and AES-256 for persistence
272
+ Encrypt: Domain event payloads containing PII before passing to outbound port adapters
273
+ Protect: Domain core from injection attacks by validating all inputs at inbound port boundary
274
+ Protect: Port interfaces from unauthorized access with role-based guards at driving adapter layer
275
+ Protect: Adapter credentials with vault-managed secrets and automatic rotation
276
+
277
+ # Level 10: Native
278
+ Python
279
+ {
280
+ from abc import ABC, abstractmethod
281
+ from dataclasses import dataclass, field
282
+ from typing import Any, Dict, List, Optional, Type
283
+ from enum import Enum
284
+ import time
285
+ from contextlib import contextmanager
286
+
287
+ # ===================== DOMAIN CORE =====================
288
+
289
+ @dataclass(frozen=True)
290
+ class OrderId:
291
+ value: str
292
+
293
+ @dataclass(frozen=True)
294
+ class Money:
295
+ amount: float
296
+ currency: str
297
+
298
+ class OrderStatus(Enum):
299
+ DRAFT = "draft"
300
+ CONFIRMED = "confirmed"
301
+ SHIPPED = "shipped"
302
+ CANCELLED = "cancelled"
303
+
304
+ @dataclass
305
+ class LineItem:
306
+ product_id: str
307
+ quantity: int
308
+ unit_price: Money
309
+
310
+ class Order:
311
+ def __init__(self, order_id: OrderId, customer_id: str):
312
+ self._order_id = order_id
313
+ self._customer_id = customer_id
314
+ self._items: List[LineItem] = []
315
+ self._status = OrderStatus.DRAFT
316
+ self._events: List['DomainEvent'] = []
317
+
318
+ @property
319
+ def order_id(self) -> OrderId:
320
+ return self._order_id
321
+
322
+ @property
323
+ def status(self) -> OrderStatus:
324
+ return self._status
325
+
326
+ @property
327
+ def total(self) -> Money:
328
+ if not self._items:
329
+ return Money(0.0, "USD")
330
+ total = sum(i.quantity * i.unit_price.amount for i in self._items)
331
+ return Money(total, self._items[0].unit_price.currency)
332
+
333
+ def add_item(self, item: LineItem) -> None:
334
+ self._validate_status(OrderStatus.DRAFT)
335
+ self._items.append(item)
336
+
337
+ def confirm(self) -> None:
338
+ self._validate_status(OrderStatus.DRAFT)
339
+ if not self._items:
340
+ raise ValueError("Cannot confirm empty order")
341
+ self._status = OrderStatus.CONFIRMED
342
+ self._events.append(OrderConfirmedEvent(
343
+ aggregate_id=self._order_id.value,
344
+ total=self.total.amount
345
+ ))
346
+
347
+ def cancel(self) -> None:
348
+ if self._status == OrderStatus.SHIPPED:
349
+ raise ValueError("Cannot cancel shipped order")
350
+ self._status = OrderStatus.CANCELLED
351
+
352
+ def _validate_status(self, expected: OrderStatus) -> None:
353
+ if self._status != expected:
354
+ raise ValueError(f"Expected {expected.value}, got {self._status.value}")
355
+
356
+ def pop_events(self) -> List['DomainEvent']:
357
+ events = self._events[:]
358
+ self._events.clear()
359
+ return events
360
+
361
+ @dataclass
362
+ class DomainEvent:
363
+ event_type: str
364
+ aggregate_id: str
365
+ payload: Dict[str, Any]
366
+ occurred_at: float = field(default_factory=time.time)
367
+
368
+ @dataclass
369
+ class OrderConfirmedEvent(DomainEvent):
370
+ aggregate_id: str
371
+ total: float
372
+ event_type: str = "order.confirmed"
373
+ payload: Dict[str, Any] = field(default_factory=dict)
374
+
375
+ # ===================== PORTS =====================
376
+
377
+ class OrderRepositoryPort(ABC):
378
+ @abstractmethod
379
+ def find_by_id(self, order_id: OrderId) -> Optional[Order]:
380
+ pass
381
+
382
+ @abstractmethod
383
+ def save(self, order: Order) -> None:
384
+ pass
385
+
386
+ class EventPublisherPort(ABC):
387
+ @abstractmethod
388
+ def publish(self, events: List[DomainEvent]) -> None:
389
+ pass
390
+
391
+ class NotificationPort(ABC):
392
+ @abstractmethod
393
+ def send_order_confirmation(self, customer_id: str, order_id: str, total: float) -> None:
394
+ pass
395
+
396
+ # ===================== USE CASES =====================
397
+
398
+ class ConfirmOrderUseCase:
399
+ def __init__(self, repo: OrderRepositoryPort, publisher: EventPublisherPort,
400
+ notifier: NotificationPort):
401
+ self._repo = repo
402
+ self._publisher = publisher
403
+ self._notifier = notifier
404
+
405
+ def execute(self, order_id: str) -> Dict[str, Any]:
406
+ oid = OrderId(order_id)
407
+ order = self._repo.find_by_id(oid)
408
+ if not order:
409
+ raise ValueError(f"Order {order_id} not found")
410
+
411
+ order.confirm()
412
+ self._repo.save(order)
413
+
414
+ events = order.pop_events()
415
+ self._publisher.publish(events)
416
+
417
+ for event in events:
418
+ if isinstance(event, OrderConfirmedEvent):
419
+ self._notifier.send_order_confirmation(
420
+ order._customer_id, order_id, event.total
421
+ )
422
+
423
+ return {"order_id": order_id, "status": order.status.value}
424
+
425
+ # ===================== ADAPTERS =====================
426
+
427
+ class InMemoryOrderRepository(OrderRepositoryPort):
428
+ def __init__(self):
429
+ self._store: Dict[str, Order] = {}
430
+
431
+ def find_by_id(self, order_id: OrderId) -> Optional[Order]:
432
+ return self._store.get(order_id.value)
433
+
434
+ def save(self, order: Order) -> None:
435
+ self._store[order.order_id.value] = order
436
+
437
+ class ConsoleEventPublisher(EventPublisherPort):
438
+ def publish(self, events: List[DomainEvent]) -> None:
439
+ for event in events:
440
+ print(f"[EVENT] {event.event_type}: {event.aggregate_id}")
441
+
442
+ class ConsoleNotification(NotificationPort):
443
+ def send_order_confirmation(self, customer_id: str, order_id: str, total: float) -> None:
444
+ print(f"[NOTIFY] Customer {customer_id}: Order {order_id} confirmed, total: ${total:.2f}")
445
+
446
+ # ===================== WIRED APPLICATION =====================
447
+
448
+ repo = InMemoryOrderRepository()
449
+ publisher = ConsoleEventPublisher()
450
+ notifier = ConsoleNotification()
451
+ use_case = ConfirmOrderUseCase(repo, publisher, notifier)
452
+
453
+ order = Order(OrderId("ORD-001"), "CUST-42")
454
+ order.add_item(LineItem("PROD-A", 2, Money(29.99, "USD")))
455
+ order.add_item(LineItem("PROD-B", 1, Money(49.99, "USD")))
456
+ repo.save(order)
457
+
458
+ result = use_case.execute("ORD-001")
459
+ print(f"Result: {result}")
460
+ }