# Design — Sprint Contract > **처방 대상**: F2 (DoD 강제 실패), F6 (환경 검증 누락) ## 컨셉 Sprint Contract 는 "이 스프린트/작업을 무엇으로 합격 판정할지" 를 **기계가 읽고 검증할 수 있는 형식**으로 고정한 불변 문서다. - 형식: JSON (Zod schema 로 검증) - 생성 시점: 스프린트/작업 시작 직전 - 수정 권한: 생성 후 **불변** (변경하려면 contract 버전을 올려야 함) - 검증자: `validator.ts` 가 실행 결과 + contract → PASS/FAIL 판정 - 저장 위치: `.rails/contracts/.sprint-contract.json` ## Schema (초안) ```ts const SprintContract = z.object({ version: z.literal('v1'), id: z.string().ulid(), sprintId: z.string(), // e.g. SPRINT-001 createdAt: z.string().datetime(), type: z.enum(['scaffold', 'feature', 'refactor', 'bugfix', 'migration', 'infra']), // DoD (Definition of Done) - 체크 리스트 dod: z.object({ checks: z.array(z.object({ id: z.string(), // e.g. 'backend-build' description: z.string(), // 사람이 읽는 설명 kind: z.enum([ 'file_exists', // 특정 파일 존재 'command_success', // bash 커맨드 exit 0 'regex_in_file', // 파일 내 regex 매칭 'http_status', // HTTP 엔드포인트 200 'db_query', // DB 쿼리 결과 'process_listening', // 포트 리스닝 'artifact_schema', // JSON artifact 이 schema 통과 'manual_checklist', // QA 체크리스트 (다랑이) ]), spec: z.unknown(), // kind 별 파라미터 blocking: z.boolean().default(true), // false 면 경고만 })), }), // 환경 전제 - 없으면 스프린트 시작 거부 environmentPrerequisites: z.array(z.object({ name: z.string(), // e.g. 'docker' check: z.enum(['command_exists', 'port_open', 'env_var', 'file_exists', 'http_reachable']), spec: z.unknown(), reason: z.string(), // 왜 필요한지 })), // 금지 사항 - 있으면 FAIL nonGoals: z.array(z.string()), // 실행 검증 커맨드 runtimeValidation: z.object({ commands: z.array(z.object({ name: z.string(), command: z.string(), cwd: z.string().optional(), env: z.record(z.string()).optional(), timeoutMs: z.number().default(60000), expectExitCode: z.number().default(0), })), }), // 리스크 플래그 - 다랑이가 주의 깊게 봐야 할 영역 riskFlags: z.array(z.enum([ 'security-sensitive', 'data-migration', 'breaking-change', 'ux-regression', 'performance-critical', 'needs-spike', ])), // 리뷰어 프로파일 reviewerProfile: z.enum(['static', 'runtime', 'browser']), // 승인 게이트 approvalGates: z.object({ impl: z.boolean().default(true), // 나랑이 self-test review: z.boolean().default(true), // 다랑이 QA deploy: z.boolean().default(true), // 이랑이 pre-flight }), }) ``` ## 생성 흐름 1. 하랑이(Planner)가 스프린트 문서(`.plans/sprints/SPRINT-NNN-*.md`) 작성 2. `rails contract generate ` 실행 3. 스프린트 문서에서 "완료 기준" / "검증 커맨드" 섹션 파싱 4. 휴리스틱 + Zod schema 로 contract 초안 생성 5. 하랑이가 필요하면 수정 (단, 스프린트 시작 후 **불변**) 6. `rails contract freeze ` 로 잠금 7. SQLite `contracts` 테이블에 저장, pipeline state 전이 키로 사용 ## Validator 흐름 ```ts async function validate(contractPath: string, workdir: string): Promise { const contract = SprintContract.parse(JSON.parse(await readFile(contractPath, 'utf8'))) const results: CheckResult[] = [] // 1. 환경 전제 먼저 (실패 시 short-circuit) for (const prereq of contract.environmentPrerequisites) { const r = await checkPrerequisite(prereq) if (!r.ok) return { verdict: 'ABORT_PRECHECK', failed: r } } // 2. 런타임 검증 커맨드 실행 for (const cmd of contract.runtimeValidation.commands) { results.push(await runCommand(cmd)) } // 3. DoD 체크 수행 for (const check of contract.dod.checks) { results.push(await runDodCheck(check, workdir)) } // 4. 종합 판정 const blockingFails = results.filter(r => !r.pass && r.blocking) return { verdict: blockingFails.length === 0 ? 'PASS' : 'FAIL', results, failedChecks: blockingFails, } } ``` ## DoD check 종류별 구현 | kind | 설명 | 예시 | |---|---|---| | `file_exists` | 경로에 파일 존재 | `src/app/page.tsx` | | `command_success` | 명령어 exit 0 | `pnpm tsc --noEmit` | | `regex_in_file` | 파일 내 regex 매칭 | `README.md`, `/## Getting Started/` | | `http_status` | HTTP 200 OK | `curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/health` | | `db_query` | SQL 쿼리 결과 검증 | `SELECT COUNT(*) FROM settings` ≥ 1 | | `process_listening` | 포트 LISTEN 상태 | `ss -ltn sport = :3000` | | `artifact_schema` | JSON 파일이 Zod schema 통과 | `review-output.json` ⊆ ReviewOutput | | `manual_checklist` | 사람/QA agent 체크박스 | 다랑이 체크리스트 (`qa-template.md`) | ## 예시 Contract (Sprint 001) ```json { "version": "v1", "id": "01HW0XYZ...", "sprintId": "SPRINT-001", "type": "scaffold", "createdAt": "2026-04-10T12:00:00Z", "dod": { "checks": [ { "id": "ts-strict-tsconfig", "description": "tsconfig.json 이 strict 모드", "kind": "regex_in_file", "spec": { "path": "tsconfig.json", "pattern": "\"strict\"\\s*:\\s*true" }, "blocking": true }, { "id": "typecheck", "description": "pnpm tsc --noEmit 통과", "kind": "command_success", "spec": { "command": "pnpm tsc --noEmit" }, "blocking": true }, { "id": "vitest-runs", "description": "vitest 기동 가능", "kind": "command_success", "spec": { "command": "pnpm vitest --version" }, "blocking": true } ] }, "environmentPrerequisites": [ { "name": "node22", "check": "command_exists", "spec": { "command": "node" }, "reason": "Node 22 필수" }, { "name": "pnpm", "check": "command_exists", "spec": { "command": "pnpm" }, "reason": "패키지 매니저" } ], "nonGoals": ["XState 머신 실제 구현", "자매 spawn", "디스코드 연동"], "runtimeValidation": { "commands": [ { "name": "install", "command": "pnpm install", "timeoutMs": 180000 }, { "name": "typecheck", "command": "pnpm tsc --noEmit", "timeoutMs": 60000 } ] }, "riskFlags": [], "reviewerProfile": "static", "approvalGates": { "impl": true, "review": true, "deploy": false } } ``` ## 불변성 강제 - `contracts` 테이블 `frozen_at` 컬럼 non-null 이면 write 거부 - contract 파일은 ro 퍼미션 (`chmod 0444`) - 수정이 필요하면 새 버전의 contract 를 생성 (`v1.1`) - 파이프라인은 frozen contract 만 참조 가능 ## 실패 모드별 매핑 - **F2**: validator 가 `command_success` 로 실기동 검증 강제 → build 만으로 pass 불가 - **F6**: `environmentPrerequisites` 미달 → 스프린트 시작 거부 (pre-check 실패) ## CLI ```bash rails contract generate # 초안 생성 rails contract edit # 편집 (frozen 전만) rails contract freeze # 잠금 rails contract validate # 수동 실행 rails contract show # pretty print rails contract history # v1, v1.1 ... 전체 이력 ``` ## 참고 - `principles.md` 원칙 3, 6 - `failure-audit.md` F2, F6 - `qa-template.md` — `manual_checklist` kind 의 구조