Files
hanarang-rails/.plans/design/sprint-contract.md
이랑이 bac114d469 docs(plans): Sprint 000 — 전체 플랜 문서 세트 작성
- CLAUDE.md 를 .claude/rules/ 4파일로 분할 (project/stack/principles/workflow)
- Plans.md 루트 인덱스 (스프린트 목차 + 참조만)
- hooks/ pre/post-tool.sh 스켈레톤 (no-op, Sprint 002에서 구현)
- .plans/OVERVIEW.md — 목표/범위/성공기준 8개
- .plans/failure-audit.md — F1~F6 실패 감사 (증거 기반)
- .plans/design/ 6개 문서:
  * state-machine.md (XState v5 FSM 설계)
  * sprint-contract.md (Zod schema + validator)
  * skill-enforcement.md (4계층 방어)
  * handoff.md (상태 전이 기반 자매 통신)
  * retry-policy.md (backoff + escalation)
  * qa-template.md (체크리스트 runtime)
- .plans/sprints/ 8개 스프린트 명세 (SPRINT-000~007)
- .plans/migration/from-hanarang-harness.md (자산 매트릭스 + 단계별 가이드)

총 17개 문서, 약 2146 lines.

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

225 lines
7.7 KiB
Markdown

# Design — Sprint Contract
> **처방 대상**: F2 (DoD 강제 실패), F6 (환경 검증 누락)
## 컨셉
Sprint Contract 는 "이 스프린트/작업을 무엇으로 합격 판정할지" 를 **기계가 읽고 검증할 수 있는 형식**으로 고정한 불변 문서다.
- 형식: JSON (Zod schema 로 검증)
- 생성 시점: 스프린트/작업 시작 직전
- 수정 권한: 생성 후 **불변** (변경하려면 contract 버전을 올려야 함)
- 검증자: `validator.ts` 가 실행 결과 + contract → PASS/FAIL 판정
- 저장 위치: `.rails/contracts/<sprint-id>.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 <sprint-id>` 실행
3. 스프린트 문서에서 "완료 기준" / "검증 커맨드" 섹션 파싱
4. 휴리스틱 + Zod schema 로 contract 초안 생성
5. 하랑이가 필요하면 수정 (단, 스프린트 시작 후 **불변**)
6. `rails contract freeze <contract-id>` 로 잠금
7. SQLite `contracts` 테이블에 저장, pipeline state 전이 키로 사용
## Validator 흐름
```ts
async function validate(contractPath: string, workdir: string): Promise<ValidationResult> {
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 <sprint-id> # 초안 생성
rails contract edit <contract-id> # 편집 (frozen 전만)
rails contract freeze <contract-id> # 잠금
rails contract validate <contract-id> # 수동 실행
rails contract show <contract-id> # pretty print
rails contract history <sprint-id> # v1, v1.1 ... 전체 이력
```
## 참고
- `principles.md` 원칙 3, 6
- `failure-audit.md` F2, F6
- `qa-template.md``manual_checklist` kind 의 구조