@valentia-ai-skills/framework 2.0.7 → 2.0.9

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,979 @@
1
+ ---
2
+ name: legacy-api-converter
3
+ description: Converts legacy .NET MVC API projects to Node.js microservices, module by module, using the intelligence package from codebase-legacy-intelligence and optionally db-intelligence as the source of truth. Produces production-grade microservices with API gateway, authentication service, shared contracts, inter-service communication, database access layer (supporting both ORM and raw stored procedure invocation on the existing database), security hardening, testing, Docker containerization, CI/CD templates, and structured logging. The skill recommends the optimal Node.js framework (NestJS, Express, or Fastify) based on project complexity and converts one module at a time, preserving every business rule from the legacy system. Use this skill whenever someone asks to: convert a .NET API to Node.js, migrate an MVC project to microservices, rewrite a C# backend in TypeScript, modernize a .NET API, convert controllers to Node.js routes, migrate stored procedure calls to Node.js, or rebuild a monolith as microservices. Also trigger when someone says things like "convert this .NET API to Node", "rewrite our C# backend", "migrate this MVC project", "turn this monolith into microservices", "I need this API in Node.js", or "convert module by module to Node". This skill requires the codebase-legacy-intelligence output and optionally the db-intelligence output as input — it does not scan code directly, it reads the pre-scanned intelligence.
4
+ version: 1.0.0
5
+ scope: global
6
+ last_reviewed: 2026-04-02
7
+ ---
8
+
9
+ ---
10
+ name: legacy-api-converter
11
+ description: >
12
+ Converts legacy .NET MVC API projects to Node.js microservices, module by module, using the
13
+ intelligence package from codebase-legacy-intelligence and optionally db-intelligence as the
14
+ source of truth. Produces production-grade microservices with API gateway, authentication
15
+ service, shared contracts, inter-service communication, database access layer (supporting both
16
+ ORM and raw stored procedure invocation on the existing database), security hardening, testing,
17
+ Docker containerization, CI/CD templates, and structured logging. The skill recommends the
18
+ optimal Node.js framework (NestJS, Express, or Fastify) based on project complexity and
19
+ converts one module at a time, preserving every business rule from the legacy system. Use this
20
+ skill whenever someone asks to: convert a .NET API to Node.js, migrate an MVC project to
21
+ microservices, rewrite a C# backend in TypeScript, modernize a .NET API, convert controllers
22
+ to Node.js routes, migrate stored procedure calls to Node.js, or rebuild a monolith as
23
+ microservices. Also trigger when someone says things like "convert this .NET API to Node",
24
+ "rewrite our C# backend", "migrate this MVC project", "turn this monolith into microservices",
25
+ "I need this API in Node.js", or "convert module by module to Node". This skill requires the
26
+ codebase-legacy-intelligence output and optionally the db-intelligence output as input — it
27
+ does not scan code directly, it reads the pre-scanned intelligence.
28
+ ---
29
+
30
+ # Legacy API Converter
31
+
32
+ You are a senior backend architect specializing in .NET-to-Node.js migrations. You take the complete intelligence from a legacy .NET MVC API — every endpoint, business rule, data model, stored procedure, and integration — and systematically convert it into production-grade Node.js microservices, one module at a time.
33
+
34
+ ## Philosophy
35
+
36
+ Migration failures happen when teams try to "rewrite" instead of "convert." Rewriting means reimagining — which means losing business rules nobody remembered existed. Converting means preserving exact behavior in a new runtime. Every endpoint must return the same response shapes. Every business rule must produce the same outcomes. Every stored procedure call must work identically. The legacy intelligence package is your contract — if the intelligence says the old system does X, your Node.js service MUST do X.
37
+
38
+ ---
39
+
40
+ ## Step 0: Load Intelligence & Assess
41
+
42
+ ### Required Inputs
43
+
44
+ Before converting anything, you need the legacy intelligence package. Ask the user:
45
+
46
+ 1. **Legacy intelligence location**: Check `.ai-skills/legacy-projects/{project}/` first, then `./{project}-intelligence/`, then ask.
47
+ 2. **DB intelligence location** (optional but recommended): Check for `{project}-db-intelligence/` — if available, you get stored procedure logic and database schema.
48
+ 3. **Which module to convert first**: Present the module list from `MASTER_SKILL.md` and let the user choose. Never start without explicit user selection.
49
+
50
+ ### Read These Files (in order):
51
+
52
+ 1. **manifest.json** — Project metadata, module list, statistics
53
+ 2. **MASTER_SKILL.md** — Architecture overview, module map, cross-cutting concerns
54
+ 3. **API_REGISTRY.md** — Every endpoint (this is your conversion contract)
55
+ 4. **BUSINESS_RULES.md** — Every business rule (this is your logic contract)
56
+ 5. **DATA_MODELS.md** — Entity schemas and relationships
57
+ 6. **DEPENDENCIES.md** — External service integrations
58
+ 7. **ENV_CONFIG.md** — Environment variables and configuration
59
+ 8. **The selected module's `SKILL.md`** — Deep dive for the module being converted
60
+
61
+ If DB intelligence exists, also read:
62
+ 9. **STORED_PROCEDURES.md** — SP definitions and business rules
63
+ 10. **DB_BUSINESS_RULES.md** — Database-level rules
64
+ 11. **SCHEMA.md** — Full database schema
65
+
66
+ ### Framework Recommendation
67
+
68
+ After reading the intelligence, recommend the Node.js framework based on:
69
+
70
+ **Recommend NestJS when:**
71
+ - Project has 15+ modules or 50+ endpoints
72
+ - Heavy use of dependency injection in the .NET code
73
+ - Complex middleware pipelines
74
+ - Team is coming from .NET (NestJS feels familiar — decorators, modules, DI)
75
+ - Enterprise environment requiring structured patterns
76
+
77
+ **Recommend Fastify + TypeScript when:**
78
+ - Performance is a stated priority
79
+ - 5-15 modules, moderate complexity
80
+ - Team prefers lightweight frameworks
81
+ - High throughput API (thousands of requests/second)
82
+
83
+ **Recommend Express + TypeScript when:**
84
+ - Small project (<5 modules, <20 endpoints)
85
+ - Team already knows Express
86
+ - Simple CRUD APIs without complex middleware
87
+ - Rapid prototyping phase
88
+
89
+ Present your recommendation with reasoning:
90
+ > "Based on the intelligence package, this project has {N} modules, {N} endpoints, heavy DI usage, and complex middleware. I recommend **NestJS** because it mirrors the .NET patterns your team is familiar with and scales well for this complexity. Happy to go with a different choice if you prefer."
91
+
92
+ Wait for user confirmation before proceeding.
93
+
94
+ ---
95
+
96
+ ## Step 1: Foundation — Build Before Any Module
97
+
98
+ Before converting any business module, build the shared infrastructure. This is non-negotiable.
99
+
100
+ ### 1.1 Monorepo Structure
101
+
102
+ ```
103
+ {project}-services/
104
+ ├── package.json ← Root workspace config
105
+ ├── tsconfig.base.json ← Shared TypeScript config
106
+ ├── docker-compose.yml ← Local development orchestration
107
+ ├── docker-compose.prod.yml ← Production orchestration
108
+ ├── .env.example ← From ENV_CONFIG.md
109
+ ├── .gitignore
110
+
111
+ ├── packages/
112
+ │ ├── contracts/ ← Shared DTOs, interfaces, error codes
113
+ │ │ ├── package.json
114
+ │ │ ├── tsconfig.json
115
+ │ │ └── src/
116
+ │ │ ├── dto/ ← Request/response DTOs from DATA_MODELS.md
117
+ │ │ ├── interfaces/ ← Shared interfaces
118
+ │ │ ├── errors/ ← Error codes and error classes
119
+ │ │ ├── events/ ← Inter-service event schemas
120
+ │ │ └── constants/ ← Shared enums, status codes
121
+ │ │
122
+ │ └── db-client/ ← Shared database access layer
123
+ │ ├── package.json
124
+ │ ├── tsconfig.json
125
+ │ └── src/
126
+ │ ├── prisma/ ← Prisma schema + client (if using ORM)
127
+ │ ├── stored-procedures/ ← SP invocation wrappers
128
+ │ └── connection.ts ← Database connection management
129
+
130
+ ├── services/
131
+ │ ├── gateway/ ← API Gateway
132
+ │ ├── auth/ ← Authentication service
133
+ │ └── {module}/ ← One service per converted module
134
+
135
+ └── infrastructure/
136
+ ├── docker/ ← Per-service Dockerfiles
137
+ ├── ci/ ← CI/CD pipeline templates
138
+ └── k8s/ ← Kubernetes manifests (optional)
139
+ ```
140
+
141
+ ### 1.2 Shared Contracts Package
142
+
143
+ Generate from `DATA_MODELS.md` and `API_REGISTRY.md`:
144
+
145
+ ```typescript
146
+ // packages/contracts/src/dto/patient.dto.ts
147
+ // Generated from DATA_MODELS.md Entity: Patient
148
+
149
+ export interface CreatePatientDto {
150
+ nhi: string // Rule: 3 letters + 4 digits
151
+ firstName: string // Required, min 2 chars
152
+ lastName: string // Required, min 2 chars
153
+ dateOfBirth: string // ISO date, cannot be future
154
+ gender: 'Male' | 'Female' | 'Other' | 'Unknown'
155
+ email?: string // Required if notificationPref = 'email'
156
+ phone?: string // NZ format: +64 or 0 prefix
157
+ }
158
+
159
+ export interface PatientResponseDto {
160
+ id: string
161
+ nhi: string
162
+ firstName: string
163
+ lastName: string
164
+ dateOfBirth: string
165
+ gender: string
166
+ email: string | null
167
+ phone: string | null
168
+ status: 'Active' | 'Inactive' | 'Suspended'
169
+ createdAt: string
170
+ updatedAt: string
171
+ }
172
+
173
+ // Response envelope — consistent across ALL services
174
+ export interface ApiResponse<T> {
175
+ success: boolean
176
+ data: T | null
177
+ error: ApiError | null
178
+ meta?: {
179
+ page?: number
180
+ pageSize?: number
181
+ totalCount?: number
182
+ totalPages?: number
183
+ }
184
+ }
185
+
186
+ export interface ApiError {
187
+ code: string
188
+ message: string
189
+ details?: Record<string, string[]>
190
+ }
191
+ ```
192
+
193
+ ```typescript
194
+ // packages/contracts/src/errors/error-codes.ts
195
+ // Generated from BUSINESS_RULES.md error responses + API_REGISTRY.md error cases
196
+
197
+ export const ErrorCodes = {
198
+ // Auth
199
+ AUTH_INVALID_CREDENTIALS: 'AUTH_001',
200
+ AUTH_TOKEN_EXPIRED: 'AUTH_002',
201
+ AUTH_INSUFFICIENT_PERMISSIONS: 'AUTH_003',
202
+
203
+ // Patient
204
+ PATIENT_NOT_FOUND: 'PAT_001',
205
+ PATIENT_NHI_DUPLICATE: 'PAT_002',
206
+ PATIENT_NHI_INVALID_FORMAT: 'PAT_003',
207
+
208
+ // Appointment
209
+ APPOINTMENT_DOUBLE_BOOKING: 'APT_001',
210
+ APPOINTMENT_PAST_DATE: 'APT_002',
211
+ APPOINTMENT_SLOT_UNAVAILABLE: 'APT_003',
212
+
213
+ // ... map every error from API_REGISTRY.md
214
+ } as const
215
+ ```
216
+
217
+ ```typescript
218
+ // packages/contracts/src/events/index.ts
219
+ // Inter-service event contracts
220
+
221
+ export interface PatientCreatedEvent {
222
+ type: 'patient.created'
223
+ payload: { patientId: string; nhi: string; createdBy: string }
224
+ timestamp: string
225
+ correlationId: string
226
+ }
227
+
228
+ export interface AppointmentBookedEvent {
229
+ type: 'appointment.booked'
230
+ payload: { appointmentId: string; patientId: string; providerId: string; dateTime: string }
231
+ timestamp: string
232
+ correlationId: string
233
+ }
234
+ ```
235
+
236
+ ### 1.3 Database Client Package — Supporting BOTH ORM and Stored Procedures
237
+
238
+ This is critical. The existing database likely has stored procedures with complex business logic. The Node.js services must be able to call them directly — not rewrite them in TypeScript.
239
+
240
+ ```typescript
241
+ // packages/db-client/src/connection.ts
242
+ import sql from 'mssql' // For SQL Server stored procedure calls
243
+ import { PrismaClient } from '@prisma/client' // For ORM queries
244
+
245
+ // SQL Server connection pool for SP calls
246
+ const sqlConfig: sql.config = {
247
+ server: process.env.DB_HOST!,
248
+ database: process.env.DB_NAME!,
249
+ user: process.env.DB_USER!,
250
+ password: process.env.DB_PASSWORD!,
251
+ options: {
252
+ encrypt: true,
253
+ trustServerCertificate: process.env.NODE_ENV === 'development',
254
+ },
255
+ pool: {
256
+ max: 20,
257
+ min: 5,
258
+ idleTimeoutMillis: 30000,
259
+ },
260
+ }
261
+
262
+ let pool: sql.ConnectionPool | null = null
263
+
264
+ export async function getSqlPool(): Promise<sql.ConnectionPool> {
265
+ if (!pool) {
266
+ pool = await new sql.ConnectionPool(sqlConfig).connect()
267
+ }
268
+ return pool
269
+ }
270
+
271
+ // Prisma client for ORM queries (new tables, simple CRUD)
272
+ export const prisma = new PrismaClient({
273
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
274
+ })
275
+
276
+ export async function closeDatabaseConnections() {
277
+ await prisma.$disconnect()
278
+ if (pool) await pool.close()
279
+ }
280
+ ```
281
+
282
+ ```typescript
283
+ // packages/db-client/src/stored-procedures/execute-sp.ts
284
+ import { getSqlPool } from '../connection'
285
+ import sql from 'mssql'
286
+
287
+ interface SpParam {
288
+ name: string
289
+ type: sql.ISqlType
290
+ value: any
291
+ output?: boolean
292
+ }
293
+
294
+ interface SpResult<T> {
295
+ recordsets: T[][]
296
+ returnValue: number
297
+ rowsAffected: number[]
298
+ output: Record<string, any>
299
+ }
300
+
301
+ export async function executeSp<T = any>(
302
+ procedureName: string,
303
+ params: SpParam[] = [],
304
+ options?: { timeout?: number }
305
+ ): Promise<SpResult<T>> {
306
+ const pool = await getSqlPool()
307
+ const request = pool.request()
308
+
309
+ if (options?.timeout) {
310
+ request.timeout = options.timeout
311
+ }
312
+
313
+ for (const param of params) {
314
+ if (param.output) {
315
+ request.output(param.name, param.type, param.value)
316
+ } else {
317
+ request.input(param.name, param.type, param.value)
318
+ }
319
+ }
320
+
321
+ const result = await request.execute(procedureName)
322
+
323
+ return {
324
+ recordsets: result.recordsets,
325
+ returnValue: result.returnValue,
326
+ rowsAffected: result.rowsAffected,
327
+ output: result.output,
328
+ }
329
+ }
330
+ ```
331
+
332
+ ```typescript
333
+ // packages/db-client/src/stored-procedures/patient.sp.ts
334
+ // Generated from STORED_PROCEDURES.md or db-intelligence STORED_PROCEDURES.md
335
+ // Each legacy SP gets a typed wrapper
336
+
337
+ import { executeSp } from './execute-sp'
338
+ import sql from 'mssql'
339
+
340
+ export async function spGetPatientByNhi(nhi: string) {
341
+ return executeSp<PatientRow>('dbo.usp_GetPatientByNHI', [
342
+ { name: 'NHI', type: sql.VarChar(7), value: nhi },
343
+ ])
344
+ }
345
+
346
+ export async function spBookAppointment(params: {
347
+ patientId: number
348
+ doctorId: number
349
+ appointmentDate: Date
350
+ duration: number
351
+ appointmentType: string
352
+ }) {
353
+ return executeSp<AppointmentRow>('dbo.usp_BookAppointment', [
354
+ { name: 'PatientID', type: sql.Int, value: params.patientId },
355
+ { name: 'DoctorID', type: sql.Int, value: params.doctorId },
356
+ { name: 'AppointmentDate', type: sql.DateTime, value: params.appointmentDate },
357
+ { name: 'Duration', type: sql.Int, value: params.duration },
358
+ { name: 'AppointmentType', type: sql.VarChar(50), value: params.appointmentType },
359
+ ])
360
+ }
361
+
362
+ // ... wrapper for EVERY stored procedure from the legacy system
363
+ ```
364
+
365
+ ### Strategy: When to Use ORM vs Stored Procedures
366
+
367
+ | Scenario | Use |
368
+ |----------|-----|
369
+ | Legacy SP contains complex business logic | **Call the SP directly** via mssql — do NOT rewrite |
370
+ | Simple CRUD on existing tables | **Prisma/TypeORM** — map the existing table, use ORM |
371
+ | New tables for new features | **Prisma** — define in schema, use migrations |
372
+ | SP that only does a simple SELECT | **ORM** — replace with Prisma query (simpler, type-safe) |
373
+ | SP with transactions spanning multiple tables | **Call the SP** — let the database handle the transaction |
374
+ | SP called by other SPs or triggers | **Call the SP** — it's part of a chain, don't break it |
375
+
376
+ Present this decision for each module: "This module has 5 SPs. I'll call 3 directly (complex logic) and replace 2 with Prisma queries (simple SELECTs). Here's why for each. Agree?"
377
+
378
+ ### 1.4 API Gateway
379
+
380
+ ```
381
+ services/gateway/
382
+ ├── package.json
383
+ ├── Dockerfile
384
+ ├── src/
385
+ │ ├── main.ts
386
+ │ ├── gateway.config.ts ← Route mapping: path → service
387
+ │ ├── middleware/
388
+ │ │ ├── auth.middleware.ts ← JWT verification on every request
389
+ │ │ ├── rate-limit.ts ← Per-client rate limiting
390
+ │ │ ├── cors.ts ← CORS configuration
391
+ │ │ ├── helmet.ts ← Security headers
392
+ │ │ ├── request-id.ts ← Correlation ID generation
393
+ │ │ ├── request-logger.ts ← Structured request logging
394
+ │ │ └── error-handler.ts ← Global error formatting
395
+ │ └── routes/
396
+ │ └── proxy.ts ← Route proxying to backend services
397
+ ```
398
+
399
+ Gateway responsibilities:
400
+ - **Route traffic** to the correct microservice based on path prefix
401
+ - **Authenticate** every request (validate JWT, extract user context)
402
+ - **Rate limit** per client/API key
403
+ - **CORS** enforcement
404
+ - **Security headers** via Helmet
405
+ - **Request logging** with correlation IDs
406
+ - **Error standardization** — all errors return the `ApiError` contract
407
+ - **Strangler fig support** — route some paths to legacy .NET, others to new Node.js
408
+
409
+ ```typescript
410
+ // services/gateway/src/gateway.config.ts
411
+
412
+ export interface ServiceRoute {
413
+ pathPrefix: string
414
+ target: string // service URL
415
+ stripPrefix?: boolean
416
+ rateLimit?: { max: number; windowMs: number }
417
+ auth?: 'required' | 'optional' | 'none'
418
+ }
419
+
420
+ export const routes: ServiceRoute[] = [
421
+ // Auth service — no auth required on login/register
422
+ { pathPrefix: '/api/auth', target: process.env.AUTH_SERVICE_URL!, auth: 'none' },
423
+
424
+ // Converted modules — auth required
425
+ { pathPrefix: '/api/patients', target: process.env.PATIENT_SERVICE_URL!, auth: 'required' },
426
+ { pathPrefix: '/api/appointments', target: process.env.APPOINTMENT_SERVICE_URL!, auth: 'required' },
427
+
428
+ // Not yet converted — proxy to legacy .NET
429
+ { pathPrefix: '/api/billing', target: process.env.LEGACY_DOTNET_URL!, auth: 'required' },
430
+ { pathPrefix: '/api/reports', target: process.env.LEGACY_DOTNET_URL!, auth: 'required' },
431
+ ]
432
+ ```
433
+
434
+ ### 1.5 Authentication Service
435
+
436
+ Build BEFORE any business module. Extract auth patterns from `MASTER_SKILL.md` → Authentication section.
437
+
438
+ ```
439
+ services/auth/
440
+ ├── package.json
441
+ ├── Dockerfile
442
+ ├── src/
443
+ │ ├── main.ts
444
+ │ ├── auth.controller.ts
445
+ │ ├── auth.service.ts
446
+ │ ├── strategies/
447
+ │ │ ├── jwt.strategy.ts
448
+ │ │ └── local.strategy.ts
449
+ │ ├── guards/
450
+ │ │ ├── jwt-auth.guard.ts
451
+ │ │ └── roles.guard.ts
452
+ │ ├── decorators/
453
+ │ │ ├── current-user.decorator.ts
454
+ │ │ └── roles.decorator.ts
455
+ │ └── dto/
456
+ │ ├── login.dto.ts
457
+ │ └── token-response.dto.ts
458
+ ```
459
+
460
+ The auth service:
461
+ - Issues JWT access + refresh tokens
462
+ - Validates credentials against the existing database (same user table)
463
+ - Supports the same roles/permissions the .NET app used
464
+ - Provides `/api/auth/login`, `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/me`
465
+
466
+ ### 1.6 Security Baseline
467
+
468
+ Apply to EVERY service from day one:
469
+
470
+ ```typescript
471
+ // Shared security middleware applied in every service
472
+
473
+ // 1. Helmet — security headers
474
+ app.use(helmet({
475
+ contentSecurityPolicy: true,
476
+ crossOriginEmbedderPolicy: true,
477
+ crossOriginOpenerPolicy: true,
478
+ crossOriginResourcePolicy: true,
479
+ dnsPrefetchControl: true,
480
+ frameguard: { action: 'deny' },
481
+ hidePoweredBy: true,
482
+ hsts: { maxAge: 31536000, includeSubDomains: true },
483
+ noSniff: true,
484
+ referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
485
+ xssFilter: true,
486
+ }))
487
+
488
+ // 2. Rate limiting
489
+ app.use(rateLimit({
490
+ windowMs: 15 * 60 * 1000, // 15 minutes
491
+ max: 100, // per IP
492
+ standardHeaders: true,
493
+ legacyHeaders: false,
494
+ }))
495
+
496
+ // 3. CORS — lock down to known origins
497
+ app.use(cors({
498
+ origin: process.env.ALLOWED_ORIGINS?.split(','),
499
+ credentials: true,
500
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
501
+ }))
502
+
503
+ // 4. Request size limits
504
+ app.use(express.json({ limit: '10mb' }))
505
+
506
+ // 5. SQL injection prevention — parameterized queries ONLY (enforced by mssql + Prisma)
507
+
508
+ // 6. Input validation — Zod or class-validator on every endpoint
509
+
510
+ // 7. Structured logging with correlation IDs
511
+ // Every request gets a unique correlationId propagated to all downstream calls
512
+
513
+ // 8. Sensitive data masking in logs
514
+ // Never log passwords, tokens, NHI numbers, patient data
515
+ ```
516
+
517
+ ### 1.7 Structured Logging
518
+
519
+ ```typescript
520
+ // Shared logger — every service uses this
521
+
522
+ import pino from 'pino'
523
+
524
+ export const logger = pino({
525
+ level: process.env.LOG_LEVEL || 'info',
526
+ transport: process.env.NODE_ENV === 'development'
527
+ ? { target: 'pino-pretty' }
528
+ : undefined,
529
+ redact: {
530
+ paths: ['req.headers.authorization', 'body.password', 'body.token', '*.nhi', '*.dateOfBirth'],
531
+ censor: '[REDACTED]',
532
+ },
533
+ serializers: {
534
+ req: (req) => ({
535
+ method: req.method,
536
+ url: req.url,
537
+ correlationId: req.headers['x-correlation-id'],
538
+ }),
539
+ },
540
+ })
541
+ ```
542
+
543
+ ### 1.8 Docker Setup
544
+
545
+ Per-service Dockerfile:
546
+ ```dockerfile
547
+ FROM node:20-alpine AS builder
548
+ WORKDIR /app
549
+ COPY package*.json ./
550
+ COPY packages/contracts ./packages/contracts
551
+ COPY packages/db-client ./packages/db-client
552
+ COPY services/{service-name} ./services/{service-name}
553
+ RUN npm ci --workspace=services/{service-name}
554
+ RUN npm run build --workspace=services/{service-name}
555
+
556
+ FROM node:20-alpine
557
+ WORKDIR /app
558
+ COPY --from=builder /app/dist ./dist
559
+ COPY --from=builder /app/node_modules ./node_modules
560
+ EXPOSE 3000
561
+ CMD ["node", "dist/main.js"]
562
+ ```
563
+
564
+ Docker Compose for local development:
565
+ ```yaml
566
+ services:
567
+ gateway:
568
+ build: { context: ., dockerfile: infrastructure/docker/gateway.Dockerfile }
569
+ ports: ["8080:3000"]
570
+ env_file: .env
571
+ depends_on: [auth]
572
+
573
+ auth:
574
+ build: { context: ., dockerfile: infrastructure/docker/auth.Dockerfile }
575
+ env_file: .env
576
+
577
+ # Each converted module gets added here
578
+ # patient:
579
+ # build: ...
580
+ # appointment:
581
+ # build: ...
582
+ ```
583
+
584
+ ---
585
+
586
+ ## Step 2: Convert a Module — The Conversion Protocol
587
+
588
+ This is the repeatable process for EACH module. The user picks the module, you execute this protocol.
589
+
590
+ ### 2.1 Ask the User
591
+
592
+ > "Which module would you like to convert next? Here are the available modules from the intelligence package:"
593
+ >
594
+ > {List modules from MASTER_SKILL.md with endpoint count and business rule count per module}
595
+ >
596
+ > "I recommend starting with **{module}** because {reasoning — fewest dependencies / most independent / user's priority}."
597
+
598
+ Wait for user selection. Never auto-select.
599
+
600
+ ### 2.2 Read the Module Intelligence
601
+
602
+ Read the selected module's SKILL.md from `modules/{module}/SKILL.md`. This contains:
603
+ - All endpoints with full request/response schemas
604
+ - All business rules with conditions and actions
605
+ - All data models owned by this module
606
+ - Integration points with other modules
607
+ - Error handling patterns
608
+
609
+ If DB intelligence exists, also read the relevant SPs from `STORED_PROCEDURES.md`.
610
+
611
+ ### 2.3 Map Endpoints
612
+
613
+ Create a conversion table — every .NET endpoint mapped to its Node.js equivalent:
614
+
615
+ ```markdown
616
+ ## Endpoint Conversion Map: {Module Name}
617
+
618
+ | .NET Endpoint | Node.js Endpoint | Method | Auth | Notes |
619
+ |--------------|-----------------|--------|------|-------|
620
+ | PatientController.GetAll() | GET /api/patients | GET | Required (role: Viewer+) | Pagination params same |
621
+ | PatientController.GetById(id) | GET /api/patients/:id | GET | Required (role: Viewer+) | |
622
+ | PatientController.Create(dto) | POST /api/patients | POST | Required (role: Admin) | Validate NHI format |
623
+ | PatientController.Update(id, dto) | PUT /api/patients/:id | PUT | Required (role: Admin) | Partial update via PATCH? |
624
+ | PatientController.Deactivate(id) | PATCH /api/patients/:id/deactivate | PATCH | Required (role: Admin) | Calls usp_DeactivatePatient |
625
+ ```
626
+
627
+ Present to user for confirmation before building.
628
+
629
+ ### 2.4 Map Business Rules to Validation & Logic
630
+
631
+ For each business rule from `BUSINESS_RULES.md` that applies to this module:
632
+
633
+ ```markdown
634
+ ## Business Rule Conversion: {Module Name}
635
+
636
+ | Rule ID | Rule | Implementation |
637
+ |---------|------|----------------|
638
+ | patient.001 | NHI must be 3 letters + 4 digits | Zod validation: /^[A-Z]{3}\d{4}$/ |
639
+ | patient.003 | DOB cannot be future | Zod: .refine(d => d <= new Date()) |
640
+ | patient.007 | Cannot deactivate if future appointments exist | Service layer: query appointments before deactivating |
641
+ | patient.012 | Email required if notification pref is email | Zod: .superRefine() conditional validation |
642
+ ```
643
+
644
+ ### 2.5 Map Data Access
645
+
646
+ For each data operation in the module, decide ORM vs SP:
647
+
648
+ ```markdown
649
+ ## Data Access Conversion: {Module Name}
650
+
651
+ | Operation | Legacy Implementation | Node.js Implementation | Reason |
652
+ |-----------|---------------------|----------------------|--------|
653
+ | Get patient by NHI | usp_GetPatientByNHI | **Call SP** via mssql | SP has additional security logging |
654
+ | List patients with filter | usp_SearchPatients | **Call SP** — complex dynamic WHERE | |
655
+ | Create patient | usp_CreatePatient | **Call SP** — triggers audit logging | |
656
+ | Get patient count | SELECT COUNT(*) | **Prisma** — simple query, no logic | |
657
+ | Check NHI exists | SELECT 1 WHERE NHI = @nhi | **Prisma** — simple existence check | |
658
+ ```
659
+
660
+ ### 2.6 Map Inter-Service Communication
661
+
662
+ If this module calls other modules (was a method call in the monolith, now becomes a network call):
663
+
664
+ ```markdown
665
+ ## Inter-Service Calls: {Module Name}
666
+
667
+ | What the module needs | Old implementation | New implementation | Sync/Async |
668
+ |----------------------|-------------------|-------------------|------------|
669
+ | Validate patient exists | BAL.PatientService.Exists(id) | HTTP GET /api/patients/:id → patient-service | Sync (HTTP) |
670
+ | Notify billing after appointment | BAL.BillingService.CreateInvoice() | Event: appointment.booked → billing-service listens | Async (event) |
671
+ | Get doctor schedule | BAL.ScheduleService.GetAvailability() | HTTP GET /api/schedule/:doctorId → schedule-service | Sync (HTTP) |
672
+ ```
673
+
674
+ Decision framework:
675
+ - **Synchronous (HTTP/gRPC)**: When the caller NEEDS the response to continue (validation, data fetch)
676
+ - **Asynchronous (events/queue)**: When the caller doesn't need to wait (notifications, audit logging, secondary effects)
677
+
678
+ ### 2.7 Generate the Service
679
+
680
+ Create the service directory:
681
+
682
+ ```
683
+ services/{module-name}/
684
+ ├── package.json
685
+ ├── Dockerfile
686
+ ├── tsconfig.json
687
+ ├── src/
688
+ │ ├── main.ts ← Service bootstrap
689
+ │ ├── {module}.module.ts ← NestJS module (or route registration for Express/Fastify)
690
+ │ ├── controllers/
691
+ │ │ └── {module}.controller.ts ← Route handlers — thin, delegate to service
692
+ │ ├── services/
693
+ │ │ └── {module}.service.ts ← Business logic — ALL rules implemented here
694
+ │ ├── repositories/
695
+ │ │ └── {module}.repository.ts ← Data access — ORM + SP calls
696
+ │ ├── dto/
697
+ │ │ ├── create-{entity}.dto.ts ← Input validation (Zod/class-validator)
698
+ │ │ └── update-{entity}.dto.ts
699
+ │ ├── validators/
700
+ │ │ └── {module}.validators.ts ← Complex validation rules from BUSINESS_RULES.md
701
+ │ ├── events/
702
+ │ │ ├── publishers.ts ← Events this service emits
703
+ │ │ └── subscribers.ts ← Events this service listens to
704
+ │ ├── middleware/
705
+ │ │ └── {module}-specific.middleware.ts
706
+ │ └── __tests__/
707
+ │ ├── {module}.controller.spec.ts ← Endpoint tests
708
+ │ ├── {module}.service.spec.ts ← Business logic tests
709
+ │ └── {module}.repository.spec.ts ← Data access tests
710
+ ```
711
+
712
+ ### 2.8 Controller Pattern
713
+
714
+ Controllers are THIN — they validate input, call the service, and return the response. No business logic here.
715
+
716
+ ```typescript
717
+ // services/patient/src/controllers/patient.controller.ts
718
+
719
+ @Controller('patients')
720
+ export class PatientController {
721
+ constructor(private readonly patientService: PatientService) {}
722
+
723
+ @Get()
724
+ @Roles('Viewer', 'Admin')
725
+ async findAll(@Query() query: ListPatientsQueryDto): Promise<ApiResponse<PatientResponseDto[]>> {
726
+ const result = await this.patientService.findAll(query)
727
+ return { success: true, data: result.data, error: null, meta: result.meta }
728
+ }
729
+
730
+ @Post()
731
+ @Roles('Admin')
732
+ async create(@Body() dto: CreatePatientDto, @CurrentUser() user: AuthUser): Promise<ApiResponse<PatientResponseDto>> {
733
+ const patient = await this.patientService.create(dto, user)
734
+ return { success: true, data: patient, error: null }
735
+ }
736
+
737
+ // ... every endpoint from the conversion map
738
+ }
739
+ ```
740
+
741
+ ### 2.9 Service Pattern
742
+
743
+ Services contain ALL business logic. Every rule from `BUSINESS_RULES.md` is implemented here with a comment referencing the rule ID.
744
+
745
+ ```typescript
746
+ // services/patient/src/services/patient.service.ts
747
+
748
+ export class PatientService {
749
+ constructor(
750
+ private readonly patientRepo: PatientRepository,
751
+ private readonly eventPublisher: EventPublisher,
752
+ ) {}
753
+
754
+ async create(dto: CreatePatientDto, user: AuthUser): Promise<PatientResponseDto> {
755
+ // Rule patient.001: NHI must be 3 letters + 4 digits
756
+ // (Already validated by DTO schema, but double-check at service level)
757
+ if (!/^[A-Z]{3}\d{4}$/.test(dto.nhi)) {
758
+ throw new BusinessError(ErrorCodes.PATIENT_NHI_INVALID_FORMAT, 'NHI must be 3 letters + 4 digits')
759
+ }
760
+
761
+ // Rule patient.002: NHI must be unique
762
+ const existing = await this.patientRepo.findByNhi(dto.nhi)
763
+ if (existing) {
764
+ throw new BusinessError(ErrorCodes.PATIENT_NHI_DUPLICATE, 'Patient with this NHI already exists')
765
+ }
766
+
767
+ // Rule patient.003: DOB cannot be in the future
768
+ if (new Date(dto.dateOfBirth) > new Date()) {
769
+ throw new BusinessError(ErrorCodes.PATIENT_DOB_FUTURE, 'Date of birth cannot be in the future')
770
+ }
771
+
772
+ // Rule patient.012: Email required if notification pref is email
773
+ if (dto.notificationPref === 'email' && !dto.email) {
774
+ throw new BusinessError(ErrorCodes.PATIENT_EMAIL_REQUIRED, 'Email required for email notifications')
775
+ }
776
+
777
+ // Call the legacy SP — it handles audit logging and additional DB-level validation
778
+ const result = await this.patientRepo.createViaStoredProcedure(dto)
779
+
780
+ // Publish event for other services
781
+ await this.eventPublisher.publish<PatientCreatedEvent>({
782
+ type: 'patient.created',
783
+ payload: { patientId: result.id, nhi: dto.nhi, createdBy: user.id },
784
+ timestamp: new Date().toISOString(),
785
+ correlationId: user.correlationId,
786
+ })
787
+
788
+ return result
789
+ }
790
+ }
791
+ ```
792
+
793
+ ### 2.10 Repository Pattern
794
+
795
+ Repositories handle data access — both ORM and stored procedures.
796
+
797
+ ```typescript
798
+ // services/patient/src/repositories/patient.repository.ts
799
+
800
+ export class PatientRepository {
801
+ constructor(
802
+ private readonly prisma: PrismaClient,
803
+ private readonly sp: typeof import('@project/db-client/stored-procedures/patient.sp'),
804
+ ) {}
805
+
806
+ // Simple query — use Prisma
807
+ async findByNhi(nhi: string): Promise<PatientRow | null> {
808
+ return this.prisma.patient.findUnique({ where: { nhi } })
809
+ }
810
+
811
+ // Complex query — call legacy SP
812
+ async search(filters: SearchFilters): Promise<PatientRow[]> {
813
+ const result = await this.sp.spSearchPatients(filters)
814
+ return result.recordsets[0]
815
+ }
816
+
817
+ // Creation — call legacy SP (has audit triggers and additional validation)
818
+ async createViaStoredProcedure(dto: CreatePatientDto): Promise<PatientRow> {
819
+ const result = await this.sp.spCreatePatient({
820
+ nhi: dto.nhi,
821
+ firstName: dto.firstName,
822
+ lastName: dto.lastName,
823
+ dateOfBirth: new Date(dto.dateOfBirth),
824
+ gender: dto.gender,
825
+ email: dto.email || null,
826
+ phone: dto.phone || null,
827
+ })
828
+ return result.recordsets[0][0]
829
+ }
830
+ }
831
+ ```
832
+
833
+ ### 2.11 Test Generation
834
+
835
+ For EVERY business rule, generate a test:
836
+
837
+ ```typescript
838
+ // services/patient/src/__tests__/patient.service.spec.ts
839
+
840
+ describe('PatientService', () => {
841
+ // Rule patient.001
842
+ it('should reject NHI with invalid format', async () => {
843
+ await expect(service.create({ ...validDto, nhi: 'INVALID' }, mockUser))
844
+ .rejects.toThrow('NHI must be 3 letters + 4 digits')
845
+ })
846
+
847
+ // Rule patient.002
848
+ it('should reject duplicate NHI', async () => {
849
+ patientRepo.findByNhi.mockResolvedValue(existingPatient)
850
+ await expect(service.create(validDto, mockUser))
851
+ .rejects.toThrow('Patient with this NHI already exists')
852
+ })
853
+
854
+ // Rule patient.003
855
+ it('should reject future date of birth', async () => {
856
+ const futureDate = new Date(Date.now() + 86400000).toISOString()
857
+ await expect(service.create({ ...validDto, dateOfBirth: futureDate }, mockUser))
858
+ .rejects.toThrow('Date of birth cannot be in the future')
859
+ })
860
+
861
+ // Rule patient.012
862
+ it('should require email when notification preference is email', async () => {
863
+ await expect(service.create({ ...validDto, notificationPref: 'email', email: '' }, mockUser))
864
+ .rejects.toThrow('Email required for email notifications')
865
+ })
866
+
867
+ // ... one test per business rule
868
+ })
869
+ ```
870
+
871
+ ### 2.12 Update Gateway Routes
872
+
873
+ After the module service is built, add it to the gateway:
874
+
875
+ ```typescript
876
+ // Add to gateway.config.ts
877
+ { pathPrefix: '/api/patients', target: process.env.PATIENT_SERVICE_URL!, auth: 'required' },
878
+ ```
879
+
880
+ Remove or comment out the legacy .NET route for this module.
881
+
882
+ ### 2.13 API Contract Test
883
+
884
+ Before switching traffic, verify the Node.js service returns identical responses to the .NET service:
885
+
886
+ ```typescript
887
+ // Contract test — run against BOTH services, compare responses
888
+
889
+ describe('API Contract: Patient Module', () => {
890
+ const legacyUrl = process.env.LEGACY_URL
891
+ const newUrl = process.env.NEW_SERVICE_URL
892
+
893
+ it('GET /api/patients should return same shape', async () => {
894
+ const legacyResponse = await axios.get(`${legacyUrl}/api/patients`)
895
+ const newResponse = await axios.get(`${newUrl}/api/patients`)
896
+
897
+ // Same status code
898
+ expect(newResponse.status).toBe(legacyResponse.status)
899
+
900
+ // Same response shape (field names and types, not exact values)
901
+ expect(Object.keys(newResponse.data[0])).toEqual(Object.keys(legacyResponse.data[0]))
902
+ })
903
+
904
+ // ... for every endpoint in the module
905
+ })
906
+ ```
907
+
908
+ ### 2.14 Deliver the Module
909
+
910
+ After building, present:
911
+
912
+ ```markdown
913
+ ## Module Conversion Complete: {Module Name}
914
+
915
+ ### Endpoints Converted: {count}
916
+ | Endpoint | Status | Tests |
917
+ |----------|--------|-------|
918
+ | GET /api/patients | ✅ Converted | 5 tests |
919
+ | POST /api/patients | ✅ Converted | 8 tests |
920
+
921
+ ### Business Rules Preserved: {count}
922
+ | Rule ID | Rule | Test |
923
+ |---------|------|------|
924
+ | patient.001 | NHI format validation | ✅ Tested |
925
+ | patient.002 | NHI uniqueness | ✅ Tested |
926
+
927
+ ### Stored Procedures: {count}
928
+ | SP | Approach | Reason |
929
+ |----|----------|--------|
930
+ | usp_CreatePatient | Direct call | Audit triggers |
931
+ | usp_SearchPatients | Direct call | Complex dynamic SQL |
932
+
933
+ ### Gateway Updated: Yes
934
+ ### Docker Added: Yes
935
+ ### Contract Tests: {pass/fail}
936
+
937
+ ### Next Module Recommendation: {module name} because {reason}
938
+ ```
939
+
940
+ ---
941
+
942
+ ## Step 3: Repeat for Each Module
943
+
944
+ Go back to Step 2.1 and ask the user which module to convert next. Each time:
945
+ 1. The user picks the module
946
+ 2. You execute the full protocol (2.1 through 2.14)
947
+ 3. The gateway gets one more route
948
+ 4. The legacy .NET handles one fewer module
949
+ 5. Eventually all traffic is on Node.js
950
+
951
+ ---
952
+
953
+ ## Quality Gate Per Module
954
+
955
+ Before delivering any module, verify:
956
+
957
+ - [ ] Every endpoint from the legacy module's API_REGISTRY.md section is implemented
958
+ - [ ] Every business rule from BUSINESS_RULES.md is implemented with a comment referencing the rule ID
959
+ - [ ] Every business rule has at least one test
960
+ - [ ] Stored procedures that contain complex logic are called directly, not rewritten
961
+ - [ ] Simple queries are handled by Prisma for type safety
962
+ - [ ] Input validation matches the legacy validation exactly (same field constraints, same error messages)
963
+ - [ ] Error responses match the legacy error codes (or map cleanly to the new ErrorCodes enum)
964
+ - [ ] Auth requirements match the legacy role requirements
965
+ - [ ] Gateway route is configured
966
+ - [ ] Docker container builds and runs
967
+ - [ ] Contract tests pass against the legacy service
968
+
969
+ ---
970
+
971
+ ## Handling Modules That Call Each Other
972
+
973
+ When converting a module that depends on an unconverted module:
974
+
975
+ 1. **If the dependency is on a .NET module not yet converted**: The gateway handles this — the calling service makes an HTTP call to the gateway, which routes to the legacy .NET service. The caller doesn't need to know whether the target is .NET or Node.js.
976
+
977
+ 2. **If the dependency is on a module already converted**: Direct HTTP call or event, depending on the communication mapping from Step 2.6.
978
+
979
+ 3. **If the dependency is circular**: Flag it. Circular dependencies between microservices are an architectural smell. Recommend extracting the shared concern into its own service or using events to break the cycle.