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

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
  }),
})

생성 흐름

  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 흐름

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, 6
  • failure-audit.md F2, F6
  • qa-template.mdmanual_checklist kind 의 구조