@crossdelta/platform-sdk 0.7.8 → 0.7.10

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.
@@ -1,340 +1,61 @@
1
1
  # AI Generation Rules for CLI Code Output
2
2
 
3
- These rules define how AI-generated scaffolded services must be structured when produced by CLI tools.
4
-
5
- ## 🚨 EMERGENCY RULES - READ FIRST 🚨
6
-
7
- **FOR EVENT CONSUMER SERVICES (services that "listen to", "consume", "react to" events):**
8
-
9
- **CRITICAL:** Contract schema fields MUST match event handler usage!
10
-
11
- **MANDATORY 4-STEP WORKFLOW:**
12
-
13
- 1. **Analyze service description** → Extract required fields
14
- Example: "needs orderId, customerId, total" → Fields: `orderId`, `customerId`, `total`
15
-
16
- 2. **CREATE contract FIRST** (`packages/contracts/src/events/<event-name>.ts`):
17
- ```ts
18
- export const OrderCreatedContract = createContract({
19
- type: 'order.created',
20
- schema: z.object({
21
- orderId: z.string(), // ✅ From step 1
22
- customerId: z.string(), // ✅ From step 1
23
- total: z.number(), // ✅ From step 1
24
- }),
25
- })
26
- ```
27
-
28
- 3. **Export contract** (`packages/contracts/src/index.ts`):
29
- ```ts
30
- export * from './events/order-created'
31
- ```
32
-
33
- 4. **Create event handler** that uses SAME fields from contract:
34
- ```ts
35
- export default handleEvent(OrderCreatedContract, async (data) => {
36
- await sendNotification({
37
- orderId: data.orderId, // ✅ Exists in contract
38
- customerId: data.customerId, // ✅ Exists in contract
39
- total: data.total, // ✅ Exists in contract
40
- })
41
- })
42
- ```
43
-
44
- **DO NOT create contracts with generic fields like `id`, `createdAt` - use the actual fields from the service description!**
3
+ These rules define how AI-generated scaffolded services must be structured.
45
4
 
46
5
  ---
47
6
 
48
- ## ⚠️ CRITICAL: Code Quality Guidelines
49
-
50
- **NEVER invent APIs, package names, or TypeScript types:**
51
- - ✅ **Research actual package documentation** before using third-party libraries
52
- - ✅ **Verify TypeScript types** match the actual library's type definitions
53
- - ✅ **Check npm registry** to confirm package names exist
54
- - ❌ **DO NOT hallucinate** API methods, properties, or function signatures
55
- - ❌ **DO NOT guess** at type structures - look them up or use `any` with a TODO comment
56
-
57
- **When unsure:**
58
- 1. Use well-known, battle-tested packages (e.g., `@pusher/push-notifications-server`, not `pusher-beams`)
59
- 2. Keep code simple and explicit
60
- 3. Add TODO comments for unverified APIs: `// TODO: Verify this API signature`
61
-
62
- ---
63
-
64
- # 1. Commands Block (REQUIRED - MUST BE FIRST)
65
-
66
- **EVERY GENERATION MUST START WITH A COMMANDS BLOCK:**
67
-
68
- ```commands
69
- pf new hono-micro services/my-service -y
70
- ```
71
-
72
- Rules:
73
- - **ALWAYS** include this commands block as the FIRST thing in your output
74
- - **CRITICAL:** Services MUST be created in `services/` directory (e.g., `services/orders`, `services/notifications`)
75
- - **If user provides just a service name** (e.g., "orders"), prepend `services/` → `services/orders`
76
- - **If user provides full path with `services/`** (e.g., "services/orders"), use as-is
77
- - The `-y` flag is REQUIRED to skip prompts
78
-
79
- **Path Examples:**
80
- - ✅ User says "orders" → Generate: `pf new hono-micro services/orders -y`
81
- - ✅ User says "payment processor" → Generate: `pf new hono-micro services/payment-processor -y`
82
- - ✅ User says "services/orders" → Generate: `pf new hono-micro services/orders -y`
83
- - ❌ NEVER generate: `pf new hono-micro orders -y` (missing services/ prefix)
84
- - ❌ NEVER generate: `pf new hono-micro backend/orders -y` (wrong directory)
85
- - ❌ NEVER generate: `pf new hono-micro apps/orders -y` (apps/ is for frontends)
86
-
87
- ---
88
-
89
- # 2. Post-Generation Commands
90
-
91
- After generating a service with event **handlers** (Event Consumer or Hybrid), include a post-generation command to generate event mocks:
92
-
93
- ```post-commands
94
- pf event:generate services/my-service --overwrite
95
- ```
96
-
97
- This command:
98
- - Scans all `src/events/*.event.ts` files in the service
99
- - Generates mock JSON files based on the exported Zod schemas
100
- - Creates `*.mock.json` files in `packages/contracts/src/events/`
101
- - Enables testing with `pf event:publish <event-name>`
102
-
103
- **When to include this command:**
104
- - ✅ Service is an **Event Consumer** (has event handlers in `src/events/`)
105
- - ✅ Service is **Hybrid** (has both REST + event handlers)
106
- - ❌ Service is an **Event Publisher only** (only REST endpoints, NO event handlers)
107
-
108
- ---
109
-
110
- # 3. Dependencies Block
111
-
112
- ```dependencies
113
- zod
114
- @pusher/push-notifications-server
115
- ```
116
-
117
- Rules:
118
- - One package per line.
119
- - No versions.
120
- - Only packages not included in the scaffold.
121
- - **CRITICAL:** Only use packages that actually exist on npm. **Verify package names on npmjs.com** before using them (e.g., `@pusher/push-notifications-server` not `pusher-beams`).
122
- - **TypeScript safety:** Ensure your code respects the actual TypeScript types exported by the package. Do not invent properties or methods.
123
- - **IMPORTANT:** Workspace packages (like `{{scope}}/contracts`) are automatically detected and installed with `workspace:*` protocol. Just list the package name without version.
124
-
125
- ### Core Package Names (ALWAYS USE THESE EXACT NAMES)
126
-
127
- **CRITICAL:** The following packages are part of the monorepo and MUST be imported with these exact names:
128
-
129
- - `@crossdelta/cloudevents` - CloudEvents/NATS utilities (`handleEvent`, `consumeJetStreamEvents`, `publish`)
130
- - `@crossdelta/telemetry` - OpenTelemetry setup
131
- - `@crossdelta/infrastructure` - Infrastructure builders (for infra/ files only)
132
- - `{{scope}}/contracts` - Shared event contracts (when using Advanced Mode)
133
-
134
- **DO NOT use invented package names like:**
135
- - ❌ `shared-events`
136
- - ❌ `@orderboss/cloudevents`
137
- - ❌ `@platform/events`
138
- - ❌ Any other variations
139
-
140
- ---
141
-
142
- # 4. Required Source Files
143
-
144
- Generated services must include:
145
-
146
- **For Event Consumers:**
147
- - `src/index.ts` (with NATS setup)
148
- - `src/events/*.event.ts` (event handlers - NOT `src/handlers/`)
149
- - `src/use-cases/*.use-case.ts`
150
- - `src/use-cases/*.test.ts`
151
- - `src/types/*.ts` (Zod schemas and types)
152
- - `packages/contracts/src/events/*.ts` (event contracts)
153
- - `packages/contracts/src/index.ts` (export contract)
154
- - `README.md`
155
-
156
- **For Event Publishers:**
157
- - `src/index.ts` (REST API)
158
- - `src/use-cases/*.use-case.ts`
159
- - `src/use-cases/*.test.ts`
160
- - `src/types/*.ts` (Zod schemas and types)
161
- - `packages/contracts/src/events/*.ts` (RECOMMENDED: contracts for published events)
162
- - `packages/contracts/src/index.ts` (export contract)
163
- - `README.md`
164
-
165
- **Example for Event Consumer:**
166
- ```
167
- services/order-notifications/
168
- ├── src/
169
- │ ├── index.ts # NATS consumer setup
170
- │ ├── events/
171
- │ │ └── order-created.event.ts # Event handler
172
- │ ├── use-cases/
173
- │ │ ├── send-notification.use-case.ts
174
- │ │ └── send-notification.test.ts
175
- │ └── types/
176
- │ └── notifications.ts # Zod schemas/types
177
- └── README.md
178
-
179
- packages/contracts/
180
- ├── src/
181
- │ ├── events/
182
- │ │ └── order-created.ts # Contract definition
183
- │ └── index.ts # Export contract
184
- ```
185
-
186
- Rules:
187
- - Paths must be relative to the service root.
188
- - Event handlers MUST go in `src/events/`, NOT `src/handlers/`.
189
- - Contracts MUST go in `packages/contracts/src/events/`, NOT in service directory.
190
- - Do NOT generate controller or module folders unless requested.
191
-
192
- ---
193
-
194
- # 5. Code Style
195
-
196
- - Biome-compatible formatting.
197
- - Strict TS.
198
- - Arrow functions only.
199
- - No semicolons.
200
- - Minimal comments.
201
- - Alphabetized imports.
202
-
203
- ---
204
-
205
- # 6. Service Structure
206
-
207
- ## 🔑 CRITICAL: Event Publishers vs Event Consumers
208
-
209
- **Read the service description carefully to determine the service type:**
210
-
211
- ### 📤 Event Publisher (REST API that publishes events)
212
-
213
- **Keywords in description:** "publishes", "creates", "manages", "REST API", "HTTP endpoints"
7
+ ## 🚨 CRITICAL: Event Consumer Workflow
214
8
 
215
- **Example:** "Order management service that handles order creation. Publishes order.created events."
9
+ **FOR EVENT CONSUMER SERVICES (services that "consume", "listen to", "react to" events):**
216
10
 
217
- **Structure:**
218
- - ✅ REST endpoints (POST, GET, PUT, DELETE)
219
- - ✅ Use `publish()` from `@crossdelta/cloudevents`
220
- - ✅ **RECOMMENDED:** Create contracts in `packages/contracts` for published events
221
- - ❌ NO event handlers in `src/events/`
222
- - ❌ NO `consumeJetStreamEvents()`
11
+ ### Naming Convention (CRITICAL!)
223
12
 
224
- **Event Publishing Strategy:**
13
+ **Event Type → Contract Name → File Name:**
225
14
 
226
- Publishers can use **contracts** (recommended) or **string literals**:
15
+ | Event Type | Contract Name | File Name |
16
+ |------------|---------------|-----------|
17
+ | `orders.created` | `OrdersCreatedContract` | `orders-created.ts` |
18
+ | `users.registered` | `UsersRegisteredContract` | `users-registered.ts` |
19
+ | `payments.completed` | `PaymentsCompletedContract` | `payments-completed.ts` |
227
20
 
228
- ```ts
229
- // Option 1: With contract (RECOMMENDED - type-safe)
230
- import { OrderCreatedContract } from '@scope/contracts'
231
- await publish(OrderCreatedContract, orderData)
232
-
233
- // Option 2: String literal (OK for prototyping)
234
- await publish('order.created', orderData, {
235
- source: 'my-service',
236
- subject: orderId,
237
- })
238
- ```
21
+ **Rule:** Always use **plural namespace** (`orders.`, `users.`, `payments.`) to match JetStream stream subjects (`orders.>`, `users.>`, etc.)
239
22
 
240
- **When to create contracts for published events:**
241
- - ✅ **Default/Recommended** - Create contract in `packages/contracts` for type-safety
242
- - ✅ Event will be consumed by other services (now or future)
243
- - ✅ Event schema should be documented and versioned
244
- - ❌ Only skip if rapid prototyping or service-internal events
23
+ ### 4-Step Workflow
245
24
 
246
- **Example index.ts with contracts:**
247
- ```ts
248
- import '@crossdelta/telemetry'
249
-
250
- import { publish } from '@crossdelta/cloudevents'
251
- import { OrderCreatedContract } from '@scope/contracts'
252
- import { Hono } from 'hono'
253
- import { CreateOrderSchema } from './types/orders'
254
- import { createOrder } from './use-cases/create-order.use-case'
255
-
256
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4001
257
- const app = new Hono()
258
- const orders = new Map()
259
-
260
- app.get('/health', (c) => c.json({ status: 'ok' }))
261
-
262
- app.post('/orders', async (c) => {
263
- const data = CreateOrderSchema.parse(await c.req.json())
264
- const order = await createOrder(data, orders)
265
-
266
- // Publish with contract (type-safe)
267
- await publish(OrderCreatedContract, order)
268
-
269
- return c.json({ success: true, order }, 201)
270
- })
271
-
272
- Bun.serve({ port, fetch: app.fetch })
273
- ```
274
-
275
- ### 📥 Event Consumer (Reacts to events from other services)
276
-
277
- **Keywords in description:** "consumes", "listens to", "reacts to", "handles events", "processes events"
278
-
279
- **Example:** "Notification service that sends emails when orders are created. Consumes order.created events."
280
-
281
- **⚠️⚠️⚠️ MANDATORY WORKFLOW FOR EVENT CONSUMERS ⚠️⚠️⚠️**
282
-
283
- **YOU MUST FOLLOW THIS EXACT ORDER:**
284
-
285
- **STEP 1: Analyze Service Description**
286
- Extract the required event data fields from the description.
287
-
288
- Example: "sends push notifications when orders are created. Needs orderId, customerId, and total"
289
- → Required fields: `orderId`, `customerId`, `total`
290
-
291
- **STEP 2: CREATE Contract FIRST (packages/contracts/src/events/<event-name>.ts)**
25
+ **STEP 1: CREATE Contract** (`packages/contracts/src/events/<event-name>.ts`):
292
26
  ```ts
293
27
  import { createContract } from '@crossdelta/cloudevents'
294
28
  import { z } from 'zod'
295
29
 
296
- export const OrderCreatedContract = createContract({
297
- type: 'order.created',
30
+ export const OrdersCreatedContract = createContract({
31
+ type: 'orders.created', // plural namespace!
298
32
  schema: z.object({
299
- orderId: z.string(), // ✅ From Step 1
300
- customerId: z.string(), // ✅ From Step 1
301
- total: z.number(), // ✅ From Step 1
33
+ orderId: z.string(),
34
+ customerId: z.string(),
35
+ total: z.number(),
302
36
  }),
303
37
  })
304
38
 
305
- export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
39
+ export type OrdersCreatedData = z.infer<typeof OrdersCreatedContract.schema>
306
40
  ```
307
41
 
308
- **STEP 3: Export Contract (packages/contracts/src/index.ts)**
42
+ **STEP 2: Export Contract** (`packages/contracts/src/index.ts`):
309
43
  ```ts
310
- export * from './events/order-created'
44
+ export * from './events/orders-created'
311
45
  ```
312
46
 
313
- **STEP 4: Create Event Handler (src/events/order-created.event.ts)**
47
+ **STEP 3: Create Event Handler** (`src/events/orders-created.event.ts`):
314
48
  ```ts
315
49
  import { handleEvent } from '@crossdelta/cloudevents'
316
- import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
317
- import { sendNotification } from '../use-cases/send-notification.use-case'
318
-
319
- export default handleEvent(OrderCreatedContract, async (data: OrderCreatedData) => {
320
- await sendNotification({
321
- orderId: data.orderId, // ✅ Field exists in contract
322
- customerId: data.customerId, // ✅ Field exists in contract
323
- total: data.total, // ✅ Field exists in contract
324
- })
50
+ import { OrdersCreatedContract, type OrdersCreatedData } from '{{scope}}/contracts'
51
+ import { processOrder } from '../use-cases/process-order.use-case'
52
+
53
+ export default handleEvent(OrdersCreatedContract, async (data: OrdersCreatedData) => {
54
+ await processOrder(data)
325
55
  })
326
56
  ```
327
57
 
328
- **CRITICAL:** The contract schema fields (Step 2) MUST match the event handler usage (Step 4)!
329
-
330
- **Structure:**
331
- - ✅ Event handlers in `src/events/*.event.ts`
332
- - ✅ Use `consumeJetStreamEvents()` in index.ts
333
- - ✅ Use `handleEvent()` for each event type
334
- - ❌ Usually NO REST endpoints (except /health)
335
-
336
- **⚠️ CRITICAL: Event Consumer index.ts Template - COPY THIS EXACTLY:**
337
-
58
+ **STEP 4: Setup Consumer** (`src/index.ts`):
338
59
  ```ts
339
60
  import '@crossdelta/telemetry'
340
61
 
@@ -348,13 +69,11 @@ app.get('/health', (c) => c.json({ status: 'ok' }))
348
69
 
349
70
  Bun.serve({ port, fetch: app.fetch })
350
71
 
351
- // Ensure stream exists (idempotent - safe to call on every start)
352
72
  await ensureJetStreamStream({
353
73
  stream: 'ORDERS',
354
74
  subjects: ['orders.>'],
355
75
  })
356
76
 
357
- // Start NATS consumer - handlers are auto-discovered
358
77
  consumeJetStreamEvents({
359
78
  stream: 'ORDERS',
360
79
  consumer: 'my-service',
@@ -362,682 +81,184 @@ consumeJetStreamEvents({
362
81
  })
363
82
  ```
364
83
 
365
- **⚠️ MANDATORY CHECKLIST - Event Consumer index.ts MUST INCLUDE:**
366
- - [ ] `import { consumeJetStreamEvents, ensureJetStreamStream } from '@crossdelta/cloudevents'`
367
- - [ ] `await ensureJetStreamStream({ stream: 'ORDERS', subjects: ['orders.>'] })`
368
- - [ ] `consumeJetStreamEvents({ stream: 'ORDERS', consumer: 'service-name', discover: './src/events/**/*.event.ts' })`
369
- - [ ] Top-level await (Bun supports this natively)
370
- - [ ] `ensureJetStreamStream()` called BEFORE `consumeJetStreamEvents()`
371
-
372
- **CRITICAL ERRORS TO AVOID:**
373
- - ❌ Missing `ensureJetStreamStream()` → Stream won't exist, events won't be consumed
374
- - ❌ Missing `consumeJetStreamEvents()` → Service won't listen to events
375
- - ❌ Wrong import order → Telemetry must be first
376
- - ❌ Using `filterSubjects` in `ensureJetStreamStream()` → Use `subjects` parameter
377
- - ❌ Using `subjects` in `consumeJetStreamEvents()` → Use `filterSubjects` parameter if needed
378
-
379
- ### � HTTP CloudEvent Receiver (Receives CloudEvents via HTTP POST)
380
-
381
- **Keywords in description:** "webhook", "receives CloudEvents via HTTP", "HTTP push endpoint", "Pub/Sub Push"
382
-
383
- **Example:** "Webhook service that receives payment events from Stripe as CloudEvents via HTTP POST."
384
-
385
- **Structure:**
386
- - ✅ Event handlers in `src/events/*.event.ts`
387
- - ✅ Use `cloudEvents()` middleware in index.ts
388
- - ✅ Use `handleEvent()` for each event type
389
- - ❌ NO `consumeJetStreamEvents()` - events come via HTTP, not NATS
390
-
391
- **Example index.ts:**
392
- ```ts
393
- import '@crossdelta/telemetry'
84
+ ---
394
85
 
395
- import { cloudEvents } from '@crossdelta/cloudevents'
396
- import { Hono } from 'hono'
86
+ ## ⚠️ Code Quality Guidelines
397
87
 
398
- const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4003
399
- const app = new Hono()
88
+ - **Verify package names** on npmjs.com before using
89
+ - Use exact package names: `@crossdelta/cloudevents`, `@crossdelta/telemetry`
90
+ - ❌ **DO NOT hallucinate** APIs or package names
400
91
 
401
- app.get('/health', (c) => c.json({ status: 'ok' }))
92
+ ---
402
93
 
403
- // HTTP CloudEvent receiver - handlers are auto-discovered
404
- app.use('/webhooks/*', cloudEvents({
405
- discover: './src/events/**/*.event.ts',
406
- log: true
407
- }))
94
+ # 1. Commands Block (REQUIRED - MUST BE FIRST)
408
95
 
409
- Bun.serve({ port, fetch: app.fetch })
96
+ ```commands
97
+ pf new hono-micro services/my-service -y
410
98
  ```
411
99
 
412
- **When to use:**
413
- - Receiving webhooks (Stripe, GitHub, etc.)
414
- - ✅ Google Cloud Pub/Sub Push endpoints
415
- - ✅ CloudEvent-to-CloudEvent bridges
416
- - ❌ NOT for regular REST APIs (use Event Publisher pattern instead)
417
-
418
- ### �🔄 Hybrid Service (Both Publisher and Consumer)
419
-
420
- Some services both consume events AND expose REST endpoints:
421
-
422
- **Example:** "Payment service that processes payments via REST API and listens for order.created events."
423
-
424
- **Structure:**
425
- - ✅ REST endpoints that publish events
426
- - ✅ Event handlers that consume events
427
- - ✅ Use both `publish()` and `consumeJetStreamEvents()`
100
+ - Services MUST be in `services/` directory
101
+ - User says "orders" Generate: `services/orders`
428
102
 
429
103
  ---
430
104
 
431
- ### index.ts Rules
432
-
433
- Contains only:
434
- - Telemetry import (MUST be first)
435
- - Server setup (Hono)
436
- - Health endpoint
437
- - REST endpoints (for Publishers/Hybrid)
438
- - Event consumer registration (for Consumers/Hybrid)
439
- - Port configuration from env vars
105
+ # 2. Post-Generation Commands
440
106
 
441
- **Import schemas and types from `src/types/` directory:**
442
- ```ts
443
- import { CreateOrderSchema, UpdateOrderSchema } from './types/orders'
107
+ For Event Consumer services:
444
108
 
445
- app.post('/orders', async (c) => {
446
- const data = CreateOrderSchema.parse(await c.req.json())
447
- const order = await createOrder(data, orders)
448
- await publish('order.created', order, { source: 'my-service' })
449
- return c.json({ success: true, order }, 201)
450
- })
109
+ ```post-commands
110
+ pf event:generate services/my-service --overwrite
451
111
  ```
452
112
 
453
113
  ---
454
114
 
455
- # 6.5. Types & Schemas Structure
456
-
457
- **CRITICAL:** All Zod schemas and types must live in `src/types/` directory.
458
-
459
- ```
460
- src/
461
- ├── index.ts # Adapters (REST endpoints)
462
- ├── types/ # Schemas & Types (shared)
463
- │ ├── orders.ts # Order-related schemas & types
464
- │ ├── payments.ts # Payment-related schemas & types
465
- │ └── index.ts
466
- ├── events/ # Event handlers (if Consumer)
467
- │ └── *.event.ts
468
- └── use-cases/ # Business logic
469
- └── *.use-case.ts
470
- ```
471
-
472
- **Types file structure:**
473
- ```ts
474
- // src/types/orders.ts
475
- import { z } from 'zod'
476
-
477
- // Define Zod schemas
478
- export const CreateOrderSchema = z.object({
479
- customerId: z.string().min(1),
480
- items: z.array(z.object({
481
- productId: z.string().min(1),
482
- quantity: z.number().int().min(1),
483
- price: z.number().min(0),
484
- })).min(1),
485
- total: z.number().min(0),
486
- })
115
+ # 3. Dependencies Block
487
116
 
488
- export const UpdateOrderSchema = z.object({
489
- items: z.array(z.object({
490
- productId: z.string().min(1),
491
- quantity: z.number().int().min(1),
492
- price: z.number().min(0),
493
- })).optional(),
494
- total: z.number().min(0).optional(),
495
- status: z.enum(['created', 'updated', 'completed']).optional(),
496
- })
117
+ Only include packages not in the scaffold:
497
118
 
498
- // Export inferred types
499
- export type CreateOrderInput = z.infer<typeof CreateOrderSchema>
500
- export type UpdateOrderInput = z.infer<typeof UpdateOrderSchema>
119
+ ```dependencies
120
+ zod
121
+ @pusher/push-notifications-server
501
122
  ```
502
123
 
503
- **Why `src/types/`?**
504
- - ✅ Single source of truth (Zod schema → TypeScript types)
505
- - ✅ No circular dependencies (types → use-cases, types → adapters)
506
- - ✅ No duplication between schemas and types
507
- - ✅ Shared between adapters (REST, Events) and use-cases
124
+ **Note:** `{{scope}}/contracts` is already in the template's package.json.
508
125
 
509
126
  ---
510
127
 
511
- # 7. Use-Case Rules (Lean Hexagonal Architecture)
512
-
513
- Services follow a **Lean Hexagonal Architecture** pattern:
514
- - **Types & Schemas** (in `src/types/`) = Zod schemas and inferred types (shared by all layers)
515
- - **Event Handlers** (in `src/events/`) = Adapters (receive events, validate, delegate to use-cases)
516
- - **REST Endpoints** (in `src/index.ts`) = Adapters (receive HTTP requests, validate, delegate to use-cases)
517
- - **Use-Cases** (in `src/use-cases/`) = Application Core (business logic, NO validation, NO schemas)
518
- - No explicit ports/interfaces unless needed
519
-
520
- **IMPORTANT:** A single service description may result in **multiple use-cases**. Break down complex functionality into focused, single-responsibility use-cases.
521
-
522
- Example: "Payment processing service" might generate:
523
- - `validate-payment.use-case.ts` - Validate payment details
524
- - `process-payment.use-case.ts` - Execute payment with provider
525
- - `notify-payment-status.use-case.ts` - Send payment notifications
526
- - `refund-payment.use-case.ts` - Handle refund requests
527
-
528
- Use-Case Rules:
529
- - All domain logic lives in `src/use-cases/`
530
- - One use-case = one responsibility
531
- - Pure functions preferred
532
- - No framework imports (no Hono, no Zod, no @crossdelta/cloudevents)
533
- - **CRITICAL: Import types from `src/types/`** - do NOT redefine types in use-cases
534
- - **CRITICAL: NO Zod schemas in use-cases!** Schemas live in `src/types/`
535
- - **CRITICAL: NO manual validation (if/throw) for input data!** Trust that adapters validated the data
536
- - **CRITICAL: NO globalThis hacks or in-memory state!** Pass state as parameters (Dependency Injection)
537
- - **DO NOT export domain entity types from use-cases** - define those in `src/types/` if needed
538
- - **DO NOT create repository interfaces** - keep it lean, inject concrete dependencies
539
- - For complex persistence needs, add `// TODO: Replace with proper database persistence`
540
- - Return plain data (no Response objects, no validation)
541
- - Compose use-cases when needed (call one from another)
542
-
543
- **Example - CORRECT use-case (Lean Hexagonal):**
544
- ```ts
545
- // ✅ Import types from src/types/, concrete dependency injection
546
- import type { CreateOrderInput } from '../types/orders'
128
+ # 4. Required Files
547
129
 
548
- // Inject concrete dependencies (Map, Set, etc.) - keep it lean
549
- export const createOrder = async (
550
- input: CreateOrderInput,
551
- orderStore: Map<string, any> // Concrete dependency, but injected
552
- ) => {
553
- const orderId = `order-${Date.now()}`
554
-
555
- const order = {
556
- orderId,
557
- customerId: input.customerId,
558
- items: input.items,
559
- total: input.total,
560
- status: 'created',
561
- createdAt: new Date().toISOString(),
562
- }
563
-
564
- orderStore.set(orderId, order)
565
- return order
566
- }
130
+ **Event Consumer:**
567
131
  ```
132
+ services/my-service/
133
+ ├── src/
134
+ │ ├── index.ts # NATS consumer setup
135
+ │ ├── events/
136
+ │ │ └── orders-created.event.ts # Event handler
137
+ │ ├── use-cases/
138
+ │ │ ├── process-order.use-case.ts
139
+ │ │ └── process-order.test.ts
140
+ │ └── types/
141
+ │ └── orders.ts
142
+ └── README.md
568
143
 
569
- **Example - CORRECT use-case with extended input:**
570
- ```ts
571
- // ✅ Extend imported type for additional parameters
572
- import type { UpdateOrderInput } from '../types/orders'
573
-
574
- export const updateOrder = async (
575
- input: UpdateOrderInput & { orderId: string }, // Extend with orderId
576
- orderStore: Map<string, any>
577
- ) => {
578
- const order = orderStore.get(input.orderId)
579
- if (!order) {
580
- throw new Error('Order not found') // ✅ Business logic validation OK
581
- }
582
-
583
- const updatedOrder = {
584
- ...order,
585
- ...input,
586
- updatedAt: new Date().toISOString(),
587
- }
588
-
589
- orderStore.set(input.orderId, updatedOrder)
590
- return updatedOrder
591
- }
144
+ packages/contracts/
145
+ ├── src/
146
+ │ ├── events/
147
+ │ │ └── orders-created.ts # Contract
148
+ │ └── index.ts
592
149
  ```
593
150
 
594
- **Example - CORRECT use-case with DB TODO:**
595
- ```ts
596
- import type { CreateOrderInput } from '../types/orders'
597
-
598
- export const createOrder = async (
599
- input: CreateOrderInput,
600
- orderStore: Map<string, any> // TODO: Replace with proper database persistence
601
- ) => {
602
- const orderId = `order-${Date.now()}`
603
- const order = { orderId, ...input, status: 'created', createdAt: new Date().toISOString() }
604
- orderStore.set(orderId, order)
605
- return order
606
- }
151
+ **Event Publisher:**
607
152
  ```
608
-
609
- **Example - WRONG use-case:**
610
- ```ts
611
- // ❌ globalThis hack for state
612
- const orders = globalThis.__ORDERS__ || (globalThis.__ORDERS__ = {}) // ❌ NO!
613
-
614
- // ❌ Domain entity types exported from use-case
615
- export type Order = { ... } // ❌ NO! Define in adapter
616
-
617
- // ❌ Repository interface (too complex for simple services)
618
- export interface OrderRepository { // ❌ NO! Keep it lean
619
- save: (order: any) => Promise<void>
620
- }
621
-
622
- // ❌ Manual validation in use-case
623
- export const createOrder = async (input: CreateOrderInput) => {
624
- if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
625
- **Example - WRONG use-case:**
626
- ```ts
627
- // ❌ Defining types in use-case instead of importing from src/types/
628
- export type CreateOrderInput = { ... } // ❌ NO! Import from src/types/
629
-
630
- // ❌ Zod schema in use-case
631
- import { z } from 'zod'
632
- const CreateOrderSchema = z.object({ ... }) // ❌ NO! Define in src/types/
633
-
634
- // ❌ Manual input validation
635
- export const createOrder = async (input: CreateOrderInput) => {
636
- if (!input.customerId) throw new Error('Missing customerId') // ❌ NO!
637
- // Trust adapters validated the data!
638
- }
153
+ services/my-service/
154
+ ├── src/
155
+ │ ├── index.ts # REST API + publish()
156
+ │ ├── use-cases/
157
+ │ │ ├── create-order.use-case.ts
158
+ │ │ └── create-order.test.ts
159
+ │ └── types/
160
+ │ └── orders.ts
161
+ └── README.md
639
162
  ```
640
163
 
641
- Types and schemas belong in **`src/types/`**, NOT in use-cases!
642
-
643
164
  ---
644
165
 
645
- # 8. Event Handler Rules (For Event Consumers Only!)
646
-
647
- **CRITICAL:** Event handlers are ONLY for services that **consume/react to** events from other services!
648
-
649
- **Do NOT create event handlers if:**
650
- - ❌ Service description says "publishes events" or "creates/manages X"
651
- - ❌ Service is a REST API that publishes events
652
- - ❌ Service is the source/origin of the events
653
-
654
- **DO create event handlers if:**
655
- - ✅ Service description says "consumes", "listens to", "reacts to", "handles events"
656
- - ✅ Service processes events published by other services
657
-
658
- **CRITICAL: Event handlers MUST live in `src/events/` directory, NOT `src/handlers/`!**
659
-
660
- ## Event Handling Modes
661
-
662
- The platform supports **Advanced Mode** (contracts) with automatic creation for missing contracts.
663
-
664
- ### 🎯 DECISION RULE (CRITICAL - READ FIRST!)
665
-
666
- **Always use Advanced Mode (contracts) - You MUST create complete contracts!**
667
-
668
- When generating an Event Consumer service:
669
-
670
- 1. **CREATE the contract** in `packages/contracts/src/events/` with **complete schema** based on the service description
671
- 2. **Use the contract** in the event handler (`import { OrderCreatedContract } from '{{scope}}/contracts'`)
672
- 3. **Export the contract** in `packages/contracts/src/index.ts`
673
-
674
- **⚠️ CRITICAL: Contract Schema Must Match Handler Usage!**
675
-
676
- If the event handler uses fields like `orderId`, `customerId`, `total`, the contract schema MUST include those exact fields!
677
-
678
- **Example Flow:**
679
- ```
680
- User prompt: "Notification service that sends push notifications when orders are created"
681
-
682
- Step 1: Analyze what data the service needs:
683
- - orderId (to identify the order)
684
- - customerId (to send notification to customer)
685
- - total (to show order amount)
686
-
687
- Step 2: CREATE contract with those fields:
688
- packages/contracts/src/events/order-created.ts:
689
- schema: z.object({
690
- orderId: z.string(),
691
- customerId: z.string(),
692
- total: z.number(),
693
- createdAt: z.string().optional(), // Add optional context fields
694
- })
695
-
696
- Step 3: CREATE event handler that uses those fields:
697
- services/order-notifications/src/events/order-created.event.ts:
698
- import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
699
- export default handleEvent(OrderCreatedContract, async (data) => {
700
- await sendNotification({
701
- orderId: data.orderId, // ✅ Field exists in contract
702
- customerId: data.customerId, // ✅ Field exists in contract
703
- total: data.total, // ✅ Field exists in contract
704
- })
705
- })
706
- ```
707
-
708
- **Why Create Contracts Immediately?**
709
- - ✅ Schema matches handler requirements from the start
710
- - ✅ No schema mismatch errors at runtime
711
- - ✅ Type-safety between contract and handler
712
- - ✅ Single source of truth for event structure
166
+ # 5. Code Style
713
167
 
714
- **MANDATORY for Event Consumers:**
715
- - Always create `packages/contracts/src/events/<event-name>.ts` file
716
- - Schema must include ALL fields used by the event handler
717
- - Export contract in `packages/contracts/src/index.ts`
718
- - Include `pf event:generate` in post-commands to create mock JSON
168
+ - Biome-compatible, strict TS, arrow functions only
169
+ - No semicolons, minimal comments, alphabetized imports
719
170
 
720
- ### 🟢 ADVANCED MODE (Default - Recommended)
171
+ ---
721
172
 
722
- **Use Advanced Mode for Event Consumers** - you MUST create contracts with complete schemas:
173
+ # 6. Service Types
723
174
 
724
- **⚠️ CRITICAL CONTRACT CREATION RULES:**
175
+ ## 📤 Event Publisher (REST API)
725
176
 
726
- 1. **Analyze service description** to determine required event fields
727
- 2. **CREATE contract file** in `packages/contracts/src/events/<event-name>.ts`
728
- 3. **Include ALL fields** that the event handler will use
729
- 4. **Export in index** - add to `packages/contracts/src/index.ts`
730
- 5. **Add dependency** `{{scope}}/contracts` to service's dependencies block
177
+ **Keywords:** "publishes", "creates", "manages", "REST API"
731
178
 
732
- **Contract File Template:**
733
179
  ```ts
734
- // packages/contracts/src/events/<event-name>.ts
735
- import { createContract } from '@crossdelta/cloudevents'
736
- import { z } from 'zod'
737
-
738
- export const OrderCreatedContract = createContract({
739
- type: 'order.created', // Exact event type string
740
- schema: z.object({
741
- // REQUIRED: Fields used by the event handler
742
- orderId: z.string(),
743
- customerId: z.string(),
744
- total: z.number(),
745
-
746
- // OPTIONAL: Additional context fields
747
- items: z.array(z.object({
748
- productId: z.string(),
749
- quantity: z.number(),
750
- price: z.number(),
751
- })).optional(),
752
- status: z.enum(['created', 'processing', 'completed']).optional(),
753
- createdAt: z.string().optional(),
754
- }),
755
- })
180
+ import '@crossdelta/telemetry'
756
181
 
757
- export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
758
- ```
182
+ import { publish } from '@crossdelta/cloudevents'
183
+ import { Hono } from 'hono'
759
184
 
760
- **Export in Index:**
761
- ```ts
762
- // packages/contracts/src/index.ts
763
- export * from './events/order-created'
764
- // ... other exports
765
- ```
185
+ const app = new Hono()
766
186
 
767
- **Event Handler Using Contract:**
768
- ```ts
769
- // services/order-notifications/src/events/order-created.event.ts
770
- import { handleEvent } from '@crossdelta/cloudevents'
771
- import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
772
- import { sendNotification } from '../use-cases/send-notification.use-case'
773
-
774
- export default handleEvent(OrderCreatedContract, async (data: OrderCreatedData) => {
775
- // data has all fields from contract schema
776
- await sendNotification({
777
- orderId: data.orderId, // ✅ Exists in contract
778
- customerId: data.customerId, // ✅ Exists in contract
779
- total: data.total, // ✅ Exists in contract
780
- })
187
+ app.post('/orders', async (c) => {
188
+ const data = await c.req.json()
189
+ await publish('orders.created', data, { source: 'my-service' })
190
+ return c.json({ success: true })
781
191
  })
782
- ```
783
-
784
- **Why Contracts as Default?**
785
- - ✅ Strong type-safety between Publisher and Consumer
786
- - ✅ Single source of truth for event schemas
787
- - ✅ Schema evolution and versioning
788
- - ✅ Automatic mock generation
789
- - ✅ Better documentation
790
192
 
791
- **IMPORTANT:** The `packages/contracts` package is your Schema Registry. Always create complete contracts for Event Consumers.
193
+ Bun.serve({ port: 4001, fetch: app.fetch })
194
+ ```
792
195
 
793
- ### 📋 DEPRECATED: Basic Mode
196
+ ## 📥 Event Consumer (NATS)
794
197
 
795
- **Basic Mode is deprecated and no longer recommended.**
198
+ **Keywords:** "consumes", "listens to", "reacts to", "handles events"
796
199
 
797
- Always use Advanced Mode (contracts) for Event Consumers.
200
+ See 4-Step Workflow above.
798
201
 
799
- **Previous Basic Mode approach:**
800
- - ❌ Local schemas in `src/types/events.ts`
801
- - ❌ String literals for event types
802
- - ❌ No type-safety between services
202
+ ## 🔄 Hybrid (Both)
803
203
 
804
- **Current approach (Advanced Mode only):**
805
- - ✅ Always create contracts in `packages/contracts/src/events/`
806
- - ✅ Complete schemas with all required fields
807
- - ✅ Type-safety across all services
808
- - ✅ Single source of truth
204
+ Combines REST endpoints + NATS consumer.
809
205
 
810
206
  ---
811
207
 
812
- ## General Event Handler Requirements
813
-
814
- - Live under `src/events/*.event.ts` (NOT `src/handlers/`)
815
- - Use Zod for validation (NO `type` field in schema!)
816
- - Delegate ALL logic to use-cases
817
- - Contain NO business logic
818
- - **ALWAYS import from `@crossdelta/cloudevents`** (NOT `shared-events` or any other package!)
208
+ # 7. Use-Case Rules
819
209
 
820
- **CRITICAL:** Schema validates ONLY the event data payload (without `type` field). Event type is declared in the options object or contract.
210
+ - Live in `src/use-cases/*.use-case.ts`
211
+ - Pure functions, no framework imports
212
+ - Import types from `src/types/`, NOT Zod schemas
213
+ - Inject dependencies as parameters (Map, services, etc.)
214
+ - NO manual validation (adapters handle that)
821
215
 
822
- **CRITICAL:** ALWAYS use the exact import: `import { handleEvent } from '@crossdelta/cloudevents'`
823
-
824
- **Naming conventions:**
825
- - Schema constants: PascalCase with `Schema` suffix (e.g., `OrderCreatedSchema`)
826
- - Exported types: PascalCase with `Data` suffix (e.g., `OrderCreatedData`)
827
- - Contract constants: PascalCase with `Contract` suffix (e.g., `OrderCreatedContract`)
828
-
829
- **Contract Structure (Advanced Mode - Default):**
830
216
  ```ts
831
- // packages/contracts/src/events/order-created.ts
832
- import { createContract } from '@crossdelta/cloudevents'
833
- import { z } from 'zod'
217
+ import type { OrdersCreatedData } from '{{scope}}/contracts'
834
218
 
835
- export const OrderCreatedSchema = z.object({
836
- orderId: z.string(),
837
- customerId: z.string(),
838
- total: z.number(),
839
- })
840
-
841
- export const OrderCreatedContract = createContract({
842
- type: 'order.created',
843
- schema: OrderCreatedSchema,
844
- })
845
-
846
- export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
219
+ export const processOrder = async (
220
+ data: OrdersCreatedData,
221
+ orderStore: Map<string, any>
222
+ ) => {
223
+ orderStore.set(data.orderId, { ...data, processedAt: new Date() })
224
+ return { success: true }
225
+ }
847
226
  ```
848
227
 
849
- **Decision Rule:**
850
- - **Always use Advanced Mode** - contracts from `{{scope}}/contracts`
851
- - **Contracts auto-created** - `pf event:generate` creates missing contracts
852
- - Meaning → `packages/contracts`
853
- - Behavior → `services/*/src`
854
-
855
- Forbidden in handlers:
856
- - `type: z.literal('...')` in schema (redundant and causes errors)
857
- - DB access
858
- - Multi-step logic
859
- - Env var parsing
860
-
861
228
  ---
862
229
 
863
- # 9. Testing Rules
230
+ # 8. Testing Rules
864
231
 
865
- **CRITICAL:**
866
- - Test ONLY use-cases (NOT event handlers)
867
- - Use Bun's native test runner: `import { describe, expect, it, beforeEach, afterEach } from 'bun:test'`
868
- - **NO mocking available** - Bun does NOT have `vi`, `mock`, `jest.fn()`, or module mocking
869
- - Focus on validation (input params, env vars) and error handling
870
- - Keep tests simple - avoid complex scenarios
232
+ - Test ONLY use-cases
233
+ - Use `import { describe, expect, it } from 'bun:test'`
234
+ - NO mocking (Bun doesn't support it)
235
+ - Focus on error handling and validation
871
236
 
872
- **What to test:**
873
- - ✅ Input validation (missing/empty parameters)
874
- - ✅ Environment variable validation
875
- - ✅ Error messages and error types
876
- - ❌ External API integrations (Pusher, AWS, etc.)
877
- - ❌ Complex mocking scenarios
878
-
879
- Example:
880
237
  ```ts
881
- import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
882
- import { sendNotification } from './send-notification.use-case'
883
-
884
- describe('Send Notification Use Case', () => {
885
- const originalEnv = process.env
886
-
887
- beforeEach(() => {
888
- process.env = { ...originalEnv }
889
- })
890
-
891
- afterEach(() => {
892
- process.env = originalEnv
893
- })
894
-
895
- it('should throw error if orderId is missing', async () => {
896
- await expect(
897
- sendNotification({ orderId: '', customerId: 'cust-1' })
898
- ).rejects.toThrow('Missing orderId')
899
- })
900
-
901
- it('should throw error if credentials not set', async () => {
902
- delete process.env.PUSHER_BEAMS_INSTANCE_ID
903
-
904
- await expect(
905
- sendNotification({ orderId: '123', customerId: 'cust-1' })
906
- ).rejects.toThrow('Missing Pusher Beams credentials')
238
+ import { describe, expect, it } from 'bun:test'
239
+ import { processOrder } from './process-order.use-case'
240
+
241
+ describe('Process Order', () => {
242
+ it('should store order', async () => {
243
+ const store = new Map()
244
+ const result = await processOrder({ orderId: '123', customerId: 'c1', total: 100 }, store)
245
+ expect(store.has('123')).toBe(true)
907
246
  })
908
247
  })
909
248
  ```
910
249
 
911
250
  ---
912
251
 
913
- # 10. README Requirements
914
-
915
- MUST include:
916
- - Description
917
- - Features list
918
- - Environment variable table
919
- - Published/consumed events (with schemas)
920
- - API endpoints
921
- - **Testing section with event mock examples**
922
- - Dev commands
923
-
924
- ### README Structure
925
-
926
- ```markdown
927
- # Service Name
928
-
929
- Brief description of what this service does.
930
-
931
- ## Features
932
-
933
- - Feature 1
934
- - Feature 2
935
-
936
- ## Environment Variables
937
-
938
- | Variable | Description | Required | Default |
939
- |----------|-------------|----------|---------|
940
- | `SERVICE_PORT` | Port the service runs on | No | 4001 |
941
- | `NATS_URL` | NATS server URL | Yes | - |
942
-
943
- ## Events
944
-
945
- ### Publishes
946
- - `orderboss.service.event` - Description
947
- ```json
948
- {
949
- "field": "value"
950
- }
951
- ```
952
-
953
- ### Consumes
954
- - `orderboss.other.event` - Description
955
- ```json
956
- {
957
- "field": "value"
958
- }
959
- ```
960
-
961
- ## API Endpoints
962
-
963
- ### `GET /health`
964
- Health check endpoint.
965
-
966
- **Response:**
967
- \`\`\`json
968
- { "status": "ok" }
969
- \`\`\`
970
-
971
- ## Testing
972
-
973
- ### Event Mocks
974
-
975
- This service includes generated mock files for all event handlers in `src/events/*.mock.json`. These mocks can be used for testing and development.
976
-
977
- **List all available event mocks:**
978
- \`\`\`bash
979
- pf event:list
980
- \`\`\`
981
-
982
- **Publish a test event using a mock:**
983
- \`\`\`bash
984
- pf event:publish order-created
985
- \`\`\`
986
-
987
- **Regenerate event mocks:**
988
- \`\`\`bash
989
- pf event:generate . --overwrite
990
- \`\`\`
991
-
992
- ### Manual Testing
993
-
994
- To test event handlers manually:
995
-
996
- 1. Start the service:
997
- \`\`\`bash
998
- bun dev
999
- \`\`\`
1000
-
1001
- 2. In another terminal, publish a test event:
1002
- \`\`\`bash
1003
- pf event:publish order-created
1004
- \`\`\`
1005
-
1006
- 3. Check the service logs for the handler output
1007
-
1008
- ### Unit Tests
1009
-
1010
- Run use-case tests:
1011
- \`\`\`bash
1012
- bun test
1013
- \`\`\`
1014
-
1015
- ## Development
1016
-
1017
- \`\`\`bash
1018
- bun dev # Start in development mode
1019
- bun test # Run tests
1020
- bun lint # Check code quality
1021
- \`\`\`
1022
- ```
1023
-
1024
- ---
1025
-
1026
- # 11. Absolute Rules
252
+ # 9. Absolute Rules
1027
253
 
1028
- DO NOT:
254
+ **DO NOT:**
1029
255
  - Generate full rewrites
1030
- - Add new architecture concepts
1031
256
  - Use raw NATS clients
1032
- - Put logic in event handlers or index.ts
257
+ - Put logic in handlers or index.ts
258
+ - Use `src/handlers/` (use `src/events/`)
1033
259
  - Insert semicolons
1034
- - Add unused folders/files
1035
- - **Use `src/handlers/` directory (use `src/events/` instead!)**
1036
260
 
1037
- DO:
1038
- - Produce minimal, clean code
1039
- - Follow structure and conventions strictly
1040
- - Keep output compact and correct
1041
- - **Always place event handlers in `src/events/` directory**
1042
-
1043
- ---
261
+ **DO:**
262
+ - Follow naming convention (plural event types)
263
+ - Create contracts in `packages/contracts`
264
+ - Keep code minimal and clean