@garethdaine/agentops 0.9.0

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.
Files changed (148) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +410 -0
  4. package/agents/architecture-researcher.md +115 -0
  5. package/agents/code-critic.md +190 -0
  6. package/agents/delegation-router.md +40 -0
  7. package/agents/feature-researcher.md +117 -0
  8. package/agents/interrogator.md +11 -0
  9. package/agents/pitfalls-researcher.md +112 -0
  10. package/agents/plan-validator.md +173 -0
  11. package/agents/proposer.md +61 -0
  12. package/agents/security-reviewer.md +189 -0
  13. package/agents/skill-builder.md +43 -0
  14. package/agents/spec-compliance-reviewer.md +154 -0
  15. package/agents/stack-researcher.md +89 -0
  16. package/commands/build.md +766 -0
  17. package/commands/code-analysis.md +39 -0
  18. package/commands/code-field.md +22 -0
  19. package/commands/compliance-check.md +34 -0
  20. package/commands/configure.md +178 -0
  21. package/commands/cost-report.md +17 -0
  22. package/commands/enterprise/adr.md +78 -0
  23. package/commands/enterprise/brainstorm.md +461 -0
  24. package/commands/enterprise/design.md +203 -0
  25. package/commands/enterprise/dev-setup.md +136 -0
  26. package/commands/enterprise/docker-dev.md +229 -0
  27. package/commands/enterprise/e2e.md +233 -0
  28. package/commands/enterprise/feature.md +218 -0
  29. package/commands/enterprise/gap-analysis.md +204 -0
  30. package/commands/enterprise/handover.md +195 -0
  31. package/commands/enterprise/herd.md +152 -0
  32. package/commands/enterprise/knowledge.md +173 -0
  33. package/commands/enterprise/onboard.md +86 -0
  34. package/commands/enterprise/qa-check.md +80 -0
  35. package/commands/enterprise/reason.md +196 -0
  36. package/commands/enterprise/review.md +177 -0
  37. package/commands/enterprise/scaffold.md +153 -0
  38. package/commands/enterprise/status-report.md +101 -0
  39. package/commands/enterprise/tech-catalog.md +170 -0
  40. package/commands/enterprise/test-gen.md +138 -0
  41. package/commands/evolve.md +39 -0
  42. package/commands/flags.md +44 -0
  43. package/commands/interrogate.md +263 -0
  44. package/commands/lesson.md +15 -0
  45. package/commands/lessons.md +10 -0
  46. package/commands/plan.md +44 -0
  47. package/commands/prune.md +27 -0
  48. package/commands/star.md +17 -0
  49. package/commands/supply-chain-scan.md +44 -0
  50. package/commands/unicode-scan.md +63 -0
  51. package/commands/verify.md +41 -0
  52. package/commands/workflow.md +436 -0
  53. package/hooks/ai-guardrails.sh +114 -0
  54. package/hooks/audit-log.sh +26 -0
  55. package/hooks/auto-delegate.sh +45 -0
  56. package/hooks/auto-evolve.sh +22 -0
  57. package/hooks/auto-lesson.sh +26 -0
  58. package/hooks/auto-plan.sh +59 -0
  59. package/hooks/auto-test.sh +46 -0
  60. package/hooks/auto-verify.sh +30 -0
  61. package/hooks/budget-check.sh +24 -0
  62. package/hooks/code-field-preamble.sh +30 -0
  63. package/hooks/compliance-gate.sh +50 -0
  64. package/hooks/content-trust.sh +22 -0
  65. package/hooks/credential-redact.sh +23 -0
  66. package/hooks/delegation-trust.sh +15 -0
  67. package/hooks/detect-test-run.sh +19 -0
  68. package/hooks/enforcement-lib.sh +60 -0
  69. package/hooks/evolve-gate.sh +32 -0
  70. package/hooks/evolve-lib.sh +32 -0
  71. package/hooks/exfiltration-check.sh +67 -0
  72. package/hooks/failure-collector.sh +27 -0
  73. package/hooks/feature-flags.sh +67 -0
  74. package/hooks/file-provenance.sh +31 -0
  75. package/hooks/flag-utils.sh +36 -0
  76. package/hooks/hooks.json +145 -0
  77. package/hooks/injection-scan.sh +58 -0
  78. package/hooks/integrity-verify.sh +91 -0
  79. package/hooks/lessons-check.sh +17 -0
  80. package/hooks/lockfile-audit.sh +109 -0
  81. package/hooks/patterns-lib.sh +22 -0
  82. package/hooks/plan-gate.sh +18 -0
  83. package/hooks/redact-lib.sh +15 -0
  84. package/hooks/runtime-mode.sh +56 -0
  85. package/hooks/session-cleanup.sh +74 -0
  86. package/hooks/skill-validator.sh +28 -0
  87. package/hooks/standards-enforce.sh +106 -0
  88. package/hooks/star-gate.sh +93 -0
  89. package/hooks/star-preamble.sh +10 -0
  90. package/hooks/telemetry.sh +33 -0
  91. package/hooks/todo-prune.sh +84 -0
  92. package/hooks/unicode-firewall.sh +122 -0
  93. package/hooks/unicode-lib.sh +66 -0
  94. package/hooks/unicode-scan-session.sh +96 -0
  95. package/hooks/validate-command.sh +103 -0
  96. package/hooks/validate-env.sh +51 -0
  97. package/hooks/validate-path.sh +81 -0
  98. package/package.json +40 -0
  99. package/settings.json +6 -0
  100. package/templates/ai-config/tool-standards.md +56 -0
  101. package/templates/architecture/api-first.md +192 -0
  102. package/templates/architecture/auth-patterns.md +302 -0
  103. package/templates/architecture/caching-strategy.md +359 -0
  104. package/templates/architecture/database-patterns.md +347 -0
  105. package/templates/architecture/event-driven.md +252 -0
  106. package/templates/architecture/integration-patterns.md +185 -0
  107. package/templates/architecture/multi-tenancy.md +104 -0
  108. package/templates/architecture/service-boundaries.md +200 -0
  109. package/templates/build/brief-template.md +86 -0
  110. package/templates/build/summary-template.md +100 -0
  111. package/templates/build/task-plan-template.md +133 -0
  112. package/templates/communication/effort-estimate.md +54 -0
  113. package/templates/communication/incident-response.md +59 -0
  114. package/templates/communication/post-mortem.md +109 -0
  115. package/templates/communication/risk-register.md +43 -0
  116. package/templates/communication/sprint-demo-checklist.md +64 -0
  117. package/templates/communication/stakeholder-presentation-outline.md +84 -0
  118. package/templates/communication/technical-proposal.md +77 -0
  119. package/templates/delivery/deployment/deployment-checklist.md +49 -0
  120. package/templates/delivery/design/solution-design-checklist.md +37 -0
  121. package/templates/delivery/discovery/stakeholder-questions.md +33 -0
  122. package/templates/delivery/handover/knowledge-transfer-checklist.md +75 -0
  123. package/templates/delivery/handover/operational-runbook.md +117 -0
  124. package/templates/delivery/handover/support-escalation-matrix.md +56 -0
  125. package/templates/delivery/implementation/blocker-escalation-template.md +55 -0
  126. package/templates/delivery/implementation/sprint-planning-template.md +49 -0
  127. package/templates/delivery/implementation/task-decomposition-guide.md +59 -0
  128. package/templates/delivery/qa/test-plan-template.md +76 -0
  129. package/templates/delivery/qa/test-results-template.md +55 -0
  130. package/templates/delivery/qa/uat-signoff-template.md +44 -0
  131. package/templates/governance/codeowners.md +60 -0
  132. package/templates/integration/adapter-pattern.md +160 -0
  133. package/templates/scaffolds/env-validation.md +85 -0
  134. package/templates/scaffolds/error-handling.md +171 -0
  135. package/templates/scaffolds/graceful-shutdown.md +139 -0
  136. package/templates/scaffolds/health-check.md +109 -0
  137. package/templates/scaffolds/structured-logging.md +134 -0
  138. package/templates/standards/engineering-standards.md +413 -0
  139. package/templates/standards/standards-checklist.md +125 -0
  140. package/templates/tech-catalog.json +663 -0
  141. package/templates/utilities/project-detection.md +75 -0
  142. package/templates/utilities/requirements-collection.md +68 -0
  143. package/templates/utilities/template-rendering.md +81 -0
  144. package/templates/workflows/architecture-decision.md +90 -0
  145. package/templates/workflows/bug-investigation.md +83 -0
  146. package/templates/workflows/feature-implementation.md +80 -0
  147. package/templates/workflows/refactoring.md +83 -0
  148. package/templates/workflows/spike-exploration.md +82 -0
@@ -0,0 +1,252 @@
1
+ # Architecture Pattern: Event-Driven Architecture
2
+
3
+ ## When to Use
4
+
5
+ - Decoupling producers from consumers so they can evolve independently
6
+ - Processing that can tolerate eventual consistency (order fulfilment, notifications, analytics)
7
+ - Fan-out scenarios where one action triggers multiple downstream reactions
8
+ - Replacing synchronous chains that create fragile coupling between services
9
+
10
+ ## Pattern Description
11
+
12
+ Event-driven architecture uses events as the primary communication mechanism between components. A producer emits an event describing something that happened. One or more consumers react to that event independently. The producer does not know (or care) who consumes the event or what they do with it.
13
+
14
+ ## Event Schema Design (CloudEvents)
15
+
16
+ Use a standardised envelope to ensure interoperability across services:
17
+
18
+ ```typescript
19
+ /**
20
+ * CloudEvents-compliant event envelope.
21
+ * See https://cloudevents.io for the full specification.
22
+ */
23
+ export interface DomainEvent<T = unknown> {
24
+ /** Unique event identifier */
25
+ id: string;
26
+ /** Reverse-DNS event type, e.g. 'com.acme.order.created' */
27
+ type: string;
28
+ /** Event origin, e.g. 'orders-service' */
29
+ source: string;
30
+ /** CloudEvents spec version */
31
+ specversion: '1.0';
32
+ /** ISO 8601 timestamp */
33
+ time: string;
34
+ /** Content type of the data payload */
35
+ datacontenttype: 'application/json';
36
+ /** The event payload */
37
+ data: T;
38
+ /** Optional: subject the event relates to */
39
+ subject?: string;
40
+ }
41
+
42
+ // Factory function for consistent event creation
43
+ export function createEvent<T>(
44
+ type: string,
45
+ source: string,
46
+ data: T,
47
+ subject?: string,
48
+ ): DomainEvent<T> {
49
+ return {
50
+ id: crypto.randomUUID(),
51
+ type,
52
+ source,
53
+ specversion: '1.0',
54
+ time: new Date().toISOString(),
55
+ datacontenttype: 'application/json',
56
+ data,
57
+ subject,
58
+ };
59
+ }
60
+ ```
61
+
62
+ ## Event Bus Abstraction
63
+
64
+ ```typescript
65
+ export interface EventBus {
66
+ publish(event: DomainEvent): Promise<void>;
67
+ subscribe(eventType: string, handler: EventHandler): void;
68
+ }
69
+
70
+ export type EventHandler<T = unknown> = (event: DomainEvent<T>) => Promise<void>;
71
+
72
+ /**
73
+ * In-memory implementation for development and testing.
74
+ * Swap for SQS/EventBridge/Redis Streams in production.
75
+ */
76
+ export class InMemoryEventBus implements EventBus {
77
+ private handlers = new Map<string, EventHandler[]>();
78
+
79
+ async publish(event: DomainEvent): Promise<void> {
80
+ const subscribers = this.handlers.get(event.type) ?? [];
81
+ await Promise.allSettled(subscribers.map((h) => h(event)));
82
+ }
83
+
84
+ subscribe(eventType: string, handler: EventHandler): void {
85
+ const existing = this.handlers.get(eventType) ?? [];
86
+ this.handlers.set(eventType, [...existing, handler]);
87
+ }
88
+ }
89
+ ```
90
+
91
+ ## Idempotency
92
+
93
+ Consumers must handle receiving the same event more than once. At-least-once delivery is the norm for most message systems.
94
+
95
+ ```typescript
96
+ export class IdempotentHandler<T> {
97
+ constructor(
98
+ private store: IdempotencyStore,
99
+ private handler: EventHandler<T>,
100
+ ) {}
101
+
102
+ async handle(event: DomainEvent<T>): Promise<void> {
103
+ const alreadyProcessed = await this.store.exists(event.id);
104
+ if (alreadyProcessed) {
105
+ logger.info('Skipping duplicate event', { eventId: event.id });
106
+ return;
107
+ }
108
+
109
+ await this.handler(event);
110
+ await this.store.markProcessed(event.id, { ttlSeconds: 86400 * 7 });
111
+ }
112
+ }
113
+
114
+ export interface IdempotencyStore {
115
+ exists(eventId: string): Promise<boolean>;
116
+ markProcessed(eventId: string, options?: { ttlSeconds: number }): Promise<void>;
117
+ }
118
+
119
+ // Redis-backed implementation
120
+ export class RedisIdempotencyStore implements IdempotencyStore {
121
+ constructor(private redis: RedisClient) {}
122
+
123
+ async exists(eventId: string): Promise<boolean> {
124
+ const result = await this.redis.get(`idempotency:${eventId}`);
125
+ return result !== null;
126
+ }
127
+
128
+ async markProcessed(eventId: string, options?: { ttlSeconds: number }): Promise<void> {
129
+ await this.redis.set(`idempotency:${eventId}`, '1', { EX: options?.ttlSeconds ?? 604800 });
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Dead Letter Queue Handling
135
+
136
+ When a consumer fails repeatedly, move the event to a dead letter queue for manual inspection:
137
+
138
+ ```typescript
139
+ export class RetryableConsumer<T> {
140
+ constructor(
141
+ private handler: EventHandler<T>,
142
+ private dlq: DeadLetterQueue,
143
+ private maxRetries: number = 3,
144
+ ) {}
145
+
146
+ async process(event: DomainEvent<T>, attemptCount: number): Promise<void> {
147
+ try {
148
+ await this.handler(event);
149
+ } catch (error) {
150
+ if (attemptCount >= this.maxRetries) {
151
+ logger.error('Max retries exceeded, sending to DLQ', {
152
+ eventId: event.id,
153
+ eventType: event.type,
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ await this.dlq.send(event, {
157
+ error: error instanceof Error ? error.message : String(error),
158
+ attempts: attemptCount,
159
+ failedAt: new Date().toISOString(),
160
+ });
161
+ return;
162
+ }
163
+ throw error; // Let the queue infrastructure retry
164
+ }
165
+ }
166
+ }
167
+
168
+ export interface DeadLetterQueue {
169
+ send(event: DomainEvent, metadata: Record<string, unknown>): Promise<void>;
170
+ }
171
+ ```
172
+
173
+ ## Event Ordering Guarantees
174
+
175
+ Most message systems guarantee ordering only within a partition or message group:
176
+
177
+ ```typescript
178
+ /**
179
+ * Use a consistent partition key so events for the same entity
180
+ * are processed in order.
181
+ *
182
+ * SQS FIFO: MessageGroupId
183
+ * Kafka: partition key
184
+ * Redis Streams: stream key
185
+ */
186
+ export function publishOrderEvent(event: DomainEvent<OrderData>): Promise<void> {
187
+ return queue.send({
188
+ body: JSON.stringify(event),
189
+ // All events for the same order go to the same partition
190
+ messageGroupId: event.data.orderId,
191
+ // Deduplication ID for exactly-once in FIFO queues
192
+ messageDeduplicationId: event.id,
193
+ });
194
+ }
195
+ ```
196
+
197
+ ## Saga / Choreography Pattern
198
+
199
+ Coordinate multi-step workflows through event chains rather than a central orchestrator:
200
+
201
+ ```typescript
202
+ /**
203
+ * Example: Order fulfilment saga via choreography
204
+ *
205
+ * 1. OrderService emits 'order.placed'
206
+ * 2. PaymentService listens, charges card, emits 'payment.captured'
207
+ * 3. InventoryService listens, reserves stock, emits 'inventory.reserved'
208
+ * 4. ShippingService listens, creates shipment, emits 'shipment.created'
209
+ *
210
+ * Compensating actions if any step fails:
211
+ * - 'payment.failed' -> OrderService cancels the order
212
+ * - 'inventory.insufficient' -> PaymentService refunds, OrderService cancels
213
+ */
214
+
215
+ // Payment service consumer
216
+ eventBus.subscribe('order.placed', async (event: DomainEvent<OrderPlaced>) => {
217
+ try {
218
+ const payment = await paymentService.charge(event.data.paymentMethodId, event.data.total);
219
+ await eventBus.publish(createEvent('payment.captured', 'payment-service', {
220
+ orderId: event.data.orderId,
221
+ paymentId: payment.id,
222
+ }));
223
+ } catch (error) {
224
+ await eventBus.publish(createEvent('payment.failed', 'payment-service', {
225
+ orderId: event.data.orderId,
226
+ reason: error instanceof Error ? error.message : 'Unknown error',
227
+ }));
228
+ }
229
+ });
230
+
231
+ // Order service listens for compensation
232
+ eventBus.subscribe('payment.failed', async (event: DomainEvent<PaymentFailed>) => {
233
+ await orderService.cancel(event.data.orderId, event.data.reason);
234
+ });
235
+ ```
236
+
237
+ ## Trade-offs
238
+
239
+ - **Eventual consistency:** Consumers process events asynchronously. The system is not immediately consistent after a write.
240
+ - **Debugging difficulty:** Tracing a request across event chains requires correlation IDs and distributed tracing.
241
+ - **Ordering complexity:** Global ordering is expensive. Design for partition-level ordering or tolerate out-of-order delivery.
242
+ - **Schema evolution:** Changing event shapes requires versioning and backward-compatible consumers.
243
+ - **Operational overhead:** Dead letter queues, retry policies, and monitoring infrastructure must be maintained.
244
+
245
+ ## Common Pitfalls
246
+
247
+ 1. **Fat events with too much data** — Include only what consumers need. Large payloads increase coupling and bandwidth costs.
248
+ 2. **Missing idempotency** — At-least-once delivery means duplicates will happen. Every consumer must handle them.
249
+ 3. **Synchronous event processing disguised as async** — Publishing an event and then polling for the result defeats the purpose.
250
+ 4. **No dead letter queue** — Failed events that disappear silently cause data loss.
251
+ 5. **Tight schema coupling** — Consumers that break when a new field is added to an event are too tightly coupled. Use tolerant readers.
252
+ 6. **Ignoring backpressure** — A fast producer can overwhelm a slow consumer. Use queue depth monitoring and rate limiting.
@@ -0,0 +1,185 @@
1
+ # Architecture Pattern: Integration Patterns
2
+
3
+ ## Adapter Pattern
4
+
5
+ The primary pattern for integrating with external systems. Protects your domain from external API changes.
6
+
7
+ ### Interface Definition
8
+
9
+ ```typescript
10
+ /**
11
+ * Define a typed interface for every external system integration.
12
+ * This is the contract your domain depends on — NOT the external API shape.
13
+ */
14
+ export interface ProcurementAdapter {
15
+ getOrders(tenantId: string, filters?: OrderFilters): Promise<PaginatedResult<Order>>;
16
+ getOrderById(tenantId: string, orderId: string): Promise<Order | null>;
17
+ createOrder(tenantId: string, data: CreateOrderInput): Promise<Order>;
18
+ updateOrderStatus(tenantId: string, orderId: string, status: OrderStatus): Promise<Order>;
19
+ }
20
+
21
+ export interface OrderFilters {
22
+ status?: OrderStatus;
23
+ dateFrom?: Date;
24
+ dateTo?: Date;
25
+ limit?: number;
26
+ offset?: number;
27
+ }
28
+ ```
29
+
30
+ ### Mock Implementation
31
+
32
+ ```typescript
33
+ /**
34
+ * Mock adapter for development and testing.
35
+ * Returns realistic data without hitting external APIs.
36
+ */
37
+ export class MockProcurementAdapter implements ProcurementAdapter {
38
+ private orders: Map<string, Order[]> = new Map();
39
+
40
+ async getOrders(tenantId: string, filters?: OrderFilters): Promise<PaginatedResult<Order>> {
41
+ const tenantOrders = this.orders.get(tenantId) ?? this.seedData(tenantId);
42
+ // Apply filters, pagination...
43
+ return { data: tenantOrders, total: tenantOrders.length, limit: 20, offset: 0 };
44
+ }
45
+
46
+ // ... implement all interface methods with in-memory data
47
+ }
48
+ ```
49
+
50
+ ### Real Implementation
51
+
52
+ ```typescript
53
+ /**
54
+ * Real adapter calling the external procurement API.
55
+ * Handles auth, retries, error mapping, and response transformation.
56
+ */
57
+ export class HttpProcurementAdapter implements ProcurementAdapter {
58
+ constructor(
59
+ private baseUrl: string,
60
+ private apiKey: string,
61
+ private httpClient: HttpClient,
62
+ ) {}
63
+
64
+ async getOrders(tenantId: string, filters?: OrderFilters): Promise<PaginatedResult<Order>> {
65
+ const response = await this.httpClient.get(`${this.baseUrl}/orders`, {
66
+ headers: { 'X-Tenant-ID': tenantId, Authorization: `Bearer ${this.apiKey}` },
67
+ params: this.mapFilters(filters),
68
+ });
69
+ return this.mapResponse(response.data);
70
+ }
71
+
72
+ /** Map external API response to domain model */
73
+ private mapResponse(external: ExternalOrderResponse): PaginatedResult<Order> {
74
+ return {
75
+ data: external.items.map(this.mapOrder),
76
+ total: external.totalCount,
77
+ limit: external.pageSize,
78
+ offset: external.page * external.pageSize,
79
+ };
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Factory / DI Registration
85
+
86
+ ```typescript
87
+ export function createProcurementAdapter(): ProcurementAdapter {
88
+ if (env.NODE_ENV === 'test' || env.USE_MOCK_ADAPTERS === 'true') {
89
+ return new MockProcurementAdapter();
90
+ }
91
+ return new HttpProcurementAdapter(env.PROCUREMENT_API_URL, env.PROCUREMENT_API_KEY, httpClient);
92
+ }
93
+ ```
94
+
95
+ ## Anti-Corruption Layer
96
+
97
+ Prevent external API models from leaking into your domain:
98
+ - Define your own domain types (never use external response types directly)
99
+ - Map at the adapter boundary
100
+ - Validate external responses before mapping
101
+ - Handle missing/unexpected fields gracefully
102
+
103
+ ## Circuit Breaker Pattern
104
+
105
+ ```typescript
106
+ class CircuitBreaker {
107
+ private failures = 0;
108
+ private lastFailure?: Date;
109
+ private state: 'closed' | 'open' | 'half-open' = 'closed';
110
+
111
+ constructor(
112
+ private threshold: number = 5,
113
+ private resetTimeoutMs: number = 30_000,
114
+ ) {}
115
+
116
+ async execute<T>(fn: () => Promise<T>): Promise<T> {
117
+ if (this.state === 'open') {
118
+ if (Date.now() - this.lastFailure!.getTime() > this.resetTimeoutMs) {
119
+ this.state = 'half-open';
120
+ } else {
121
+ throw new AppError('Service unavailable (circuit open)', { statusCode: 503 });
122
+ }
123
+ }
124
+ try {
125
+ const result = await fn();
126
+ this.onSuccess();
127
+ return result;
128
+ } catch (error) {
129
+ this.onFailure();
130
+ throw error;
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Retry Policy
137
+
138
+ ```typescript
139
+ async function withRetry<T>(
140
+ fn: () => Promise<T>,
141
+ options: { maxRetries: number; backoffMs: number; retryableErrors?: string[] },
142
+ ): Promise<T> {
143
+ for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
144
+ try {
145
+ return await fn();
146
+ } catch (error) {
147
+ if (attempt === options.maxRetries) throw error;
148
+ const isRetryable = error instanceof AppError && error.statusCode >= 500;
149
+ if (!isRetryable) throw error;
150
+ await sleep(options.backoffMs * Math.pow(2, attempt));
151
+ }
152
+ }
153
+ throw new Error('Unreachable');
154
+ }
155
+ ```
156
+
157
+ ## Contract Testing
158
+
159
+ ```typescript
160
+ /**
161
+ * Contract tests verify that both mock and real adapters behave identically.
162
+ * Run against mock in CI, against real in integration environment.
163
+ */
164
+ describe('ProcurementAdapter contract', () => {
165
+ const adapters = [
166
+ ['mock', new MockProcurementAdapter()],
167
+ // ['real', new HttpProcurementAdapter(...)], // Enable in integration env
168
+ ] as const;
169
+
170
+ adapters.forEach(([name, adapter]) => {
171
+ describe(name, () => {
172
+ it('should return paginated orders for a tenant', async () => {
173
+ const result = await adapter.getOrders('tenant-1');
174
+ expect(result.data).toBeInstanceOf(Array);
175
+ expect(result.total).toBeGreaterThanOrEqual(0);
176
+ });
177
+
178
+ it('should return null for non-existent order', async () => {
179
+ const result = await adapter.getOrderById('tenant-1', 'non-existent');
180
+ expect(result).toBeNull();
181
+ });
182
+ });
183
+ });
184
+ });
185
+ ```
@@ -0,0 +1,104 @@
1
+ # Architecture Pattern: Multi-Tenancy
2
+
3
+ ## Tenant Context Propagation
4
+
5
+ Every request must carry tenant context. Extract once at the edge, propagate through the entire request lifecycle.
6
+
7
+ ### Middleware
8
+
9
+ ```typescript
10
+ export function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
11
+ const tenantId = req.headers['x-tenant-id'] as string;
12
+ if (!tenantId) {
13
+ throw new AuthenticationError('Tenant ID required');
14
+ }
15
+ // Validate tenant exists and is active
16
+ req.tenantId = tenantId;
17
+ next();
18
+ }
19
+ ```
20
+
21
+ ### AsyncLocalStorage for Deep Propagation
22
+
23
+ ```typescript
24
+ import { AsyncLocalStorage } from 'node:async_hooks';
25
+
26
+ interface TenantContext {
27
+ tenantId: string;
28
+ userId?: string;
29
+ permissions?: string[];
30
+ }
31
+
32
+ export const tenantStore = new AsyncLocalStorage<TenantContext>();
33
+
34
+ export function getCurrentTenant(): TenantContext {
35
+ const ctx = tenantStore.getStore();
36
+ if (!ctx) throw new Error('No tenant context — ensure tenantMiddleware is applied');
37
+ return ctx;
38
+ }
39
+ ```
40
+
41
+ ## Database Isolation Strategies
42
+
43
+ ### Row-Level Security (Recommended)
44
+
45
+ ```sql
46
+ -- Every table has a tenant_id column
47
+ ALTER TABLE orders ADD COLUMN tenant_id TEXT NOT NULL;
48
+ CREATE INDEX idx_orders_tenant ON orders(tenant_id);
49
+
50
+ -- Prisma: always filter by tenant
51
+ const orders = await prisma.order.findMany({
52
+ where: { tenantId: getCurrentTenant().tenantId, ...filters },
53
+ });
54
+ ```
55
+
56
+ ### Query Scoping Pattern
57
+
58
+ ```typescript
59
+ /** Base repository that automatically scopes all queries by tenant */
60
+ export class TenantScopedRepository<T> {
61
+ constructor(private model: PrismaModel<T>) {}
62
+
63
+ async findMany(where: Partial<T>): Promise<T[]> {
64
+ const { tenantId } = getCurrentTenant();
65
+ return this.model.findMany({ where: { ...where, tenantId } });
66
+ }
67
+
68
+ async create(data: Omit<T, 'tenantId'>): Promise<T> {
69
+ const { tenantId } = getCurrentTenant();
70
+ return this.model.create({ data: { ...data, tenantId } });
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## Data Leakage Prevention
76
+
77
+ 1. **Never trust client-provided tenant IDs for data access** — always derive from auth token
78
+ 2. **Audit cross-tenant queries** — log and alert on any query that doesn't include tenant scoping
79
+ 3. **Test isolation** — write tests that verify tenant A cannot see tenant B's data
80
+ 4. **API response validation** — verify responses only contain data for the requesting tenant
81
+
82
+ ## Tenant-Aware Caching
83
+
84
+ ```typescript
85
+ function cacheKey(key: string): string {
86
+ const { tenantId } = getCurrentTenant();
87
+ return `tenant:${tenantId}:${key}`;
88
+ }
89
+ ```
90
+
91
+ ## Testing Multi-Tenancy
92
+
93
+ ```typescript
94
+ describe('tenant isolation', () => {
95
+ it('should not return orders from other tenants', async () => {
96
+ await createOrder({ tenantId: 'tenant-a', item: 'Widget A' });
97
+ await createOrder({ tenantId: 'tenant-b', item: 'Widget B' });
98
+
99
+ const result = await getOrders('tenant-a');
100
+ expect(result.every((o) => o.tenantId === 'tenant-a')).toBe(true);
101
+ expect(result.some((o) => o.item === 'Widget B')).toBe(false);
102
+ });
103
+ });
104
+ ```