Files
hanarang-rails/prisma/schema.prisma
이랑이 0af4bbc685 feat(sprint-001): XState FSM + Prisma + CLI 뼈대 — 결정론적 파이프라인 코어
Sprint 001 전체 구현:

Foundation:
- package.json (pnpm + Node 22 + TypeScript strict)
- tsconfig.json (strict + noUncheckedIndexedAccess)
- .env.example (DATABASE_URL, DISCORD_TOKEN, etc.)
- vitest.config.ts

Core:
- src/env.ts — Zod 환경변수 검증
- src/logger.ts — pino 구조화 로거
- src/orchestrator/events.ts — Zod discriminated union 이벤트 스키마
- src/orchestrator/context.ts — PipelineContext 타입 + 팩토리
- src/orchestrator/machine.ts — XState v5 결정론적 FSM
  States: idle → planning → implementing → reviewing → deploying → done
  + retrying (exponential backoff 준비) + escalated + aborted
- src/orchestrator/persist.ts — Prisma 기반 상태 영속화
- prisma/schema.prisma — MariaDB 스키마 (pipelines, state_transitions, actor_spawns, contracts)

CLI (citty):
- rails start <project> — 파이프라인 생성
- rails status [id] — 상태 조회 + 타임라인
- rails serve — 오케스트레이터 서버 (Sprint 004 에서 완성)

Tests (9/9 pass):
- happy path (idle → done)
- REQUEST_CHANGES 재작업 루프 + max review round escalation
- retryable/non-retryable 에러 분기
- RESUME / ABORT
- context 추적

검증: pnpm tsc --noEmit ✓ | pnpm vitest run 9/9 ✓ | pnpm build ✓ | rails --help ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:46:13 +09:00

75 lines
2.0 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Pipeline {
id String @id @db.VarChar(26) // ULID
projectName String @db.VarChar(255)
requirements String @db.Text
currentState String @db.VarChar(50) @default("idle")
contextJson String @db.LongText
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
transitions StateTransition[]
actorSpawns ActorSpawn[]
contracts Contract[]
@@index([currentState])
@@index([createdAt])
@@map("pipelines")
}
model StateTransition {
id Int @id @default(autoincrement())
pipelineId String @db.VarChar(26)
fromState String @db.VarChar(50)
toState String @db.VarChar(50)
eventType String @db.VarChar(50)
eventPayload String @db.LongText
timestamp DateTime @default(now())
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
@@index([pipelineId, timestamp])
@@index([eventType])
@@map("state_transitions")
}
model ActorSpawn {
id Int @id @default(autoincrement())
pipelineId String @db.VarChar(26)
actorName String @db.VarChar(100)
stage String @db.VarChar(50)
spawnedAt DateTime @default(now())
exitCode Int?
exitedAt DateTime?
resultJson String? @db.LongText
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
@@index([pipelineId, spawnedAt])
@@map("actor_spawns")
}
model Contract {
id String @id @db.VarChar(26) // ULID
pipelineId String @db.VarChar(26)
sprintId String @db.VarChar(100)
version String @db.VarChar(20) @default("v1")
bodyJson String @db.LongText
frozenAt DateTime?
createdAt DateTime @default(now())
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
@@index([pipelineId])
@@index([sprintId])
@@map("contracts")
}