- 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>
7.7 KiB
7.7 KiB
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 (초안)
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
}),
})
생성 흐름
- 하랑이(Planner)가 스프린트 문서(
.plans/sprints/SPRINT-NNN-*.md) 작성 rails contract generate <sprint-id>실행- 스프린트 문서에서 "완료 기준" / "검증 커맨드" 섹션 파싱
- 휴리스틱 + Zod schema 로 contract 초안 생성
- 하랑이가 필요하면 수정 (단, 스프린트 시작 후 불변)
rails contract freeze <contract-id>로 잠금- SQLite
contracts테이블에 저장, pipeline state 전이 키로 사용
Validator 흐름
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)
{
"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
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, 6failure-audit.mdF2, F6qa-template.md—manual_checklistkind 의 구조