- 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>
225 lines
7.7 KiB
Markdown
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 의 구조
|