# spec-metrics-baseline.md
# 기준선 측정 — 상세 구현 스펙

**작성자:** 아테나 (UX/UI 디자이너, MoAI-ADK)
**작성일:** 2026-03-31
**상태:** DRAFT
**관련 Task:** MoAI-ADK Phase 1

---

## 1. 목적

Phase 1 기능 배포 전 현재 시스템의 성능 기준선을 측정하여 기록한다. 이후 Phase 1 배포 효과를 객관적으로 비교할 수 있는 근거를 마련한다. 기준선은 git tag `baseline-v1`으로 영구 보존된다.

**핵심 목표:**
- Phase 1 이전 상태의 정량적 기준선 확보
- 8개 핵심 지표에 대한 p50/p95 통계
- 직전 30건 실행 데이터 기반의 대표성 있는 샘플
- 롤백 결정 시 회귀 판단 기준으로 활용

---

## 2. 상세 스펙

### 2.1 파일 경로 및 스키마

**경로:** `.metrics/baseline/baseline_2026-04-03.json`

```json
{
  "schema_version": "1.0",
  "measured_at": "2026-04-03T00:00:00Z",
  "sample_size": 30,
  "measurement_period_days": null,
  "git_tag": "baseline-v1",
  "metrics": {
    "build_prompt_tokens_p50": null,
    "build_prompt_tokens_p95": null,
    "qc_pass_rate": null,
    "qc_fnr": null,
    "execution_time_p50": null,
    "write_edit_count_per_task": null,
    "token_cost_per_task_usd": null,
    "worktree_success_rate": null
  },
  "notes": ""
}
```

### 2.2 측정 대상 지표 (8개)

| 지표명 | 단위 | 설명 | 측정 방법 |
|---|---|---|---|
| `build_prompt_tokens_p50` | tokens | build_prompt 출력 토큰 수 중앙값 | 실행 로그에서 토큰 수 수집 |
| `build_prompt_tokens_p95` | tokens | build_prompt 출력 토큰 수 95th percentile | 실행 로그에서 토큰 수 수집 |
| `qc_pass_rate` | 0.0~1.0 | QC(pyright+ruff) 최초 통과율 | 성공 건 / 전체 건 |
| `qc_fnr` | 0.0~1.0 | QC False Negative Rate (오류 미검출률) | 수동 검토 샘플링 |
| `execution_time_p50` | seconds | 태스크 완료까지 소요 시간 중앙값 | 시작/종료 타임스탬프 차이 |
| `write_edit_count_per_task` | count | 태스크당 Write/Edit 툴 호출 횟수 | 실행 로그 카운트 |
| `token_cost_per_task_usd` | USD | 태스크당 API 토큰 비용 | 입출력 토큰 × 단가 |
| `worktree_success_rate` | 0.0~1.0 | worktree 생성 성공률 | 성공 건 / 시도 건 |

### 2.3 측정 기간 및 샘플

- **샘플 수:** 직전 30건 실행 데이터
- **측정 기준:** Phase 1 첫 플래그 활성화 전 수집 완료
- **측정 예정일:** 2026-04-03
- **데이터 소스:** `.metrics/runs/` 디렉토리의 실행 로그 JSON 파일

### 2.4 백분위 계산 방식

```python
import statistics
import numpy as np

def percentile(data: list[float], p: int) -> float:
    """p번째 백분위수 계산 (numpy 미사용 시 통계적 추정)"""
    try:
        return float(np.percentile(data, p))
    except ImportError:
        sorted_data = sorted(data)
        n = len(sorted_data)
        idx = (p / 100) * (n - 1)
        lower = int(idx)
        upper = min(lower + 1, n - 1)
        frac = idx - lower
        return sorted_data[lower] * (1 - frac) + sorted_data[upper] * frac

def p50(data: list[float]) -> float:
    return percentile(data, 50)

def p95(data: list[float]) -> float:
    return percentile(data, 95)
```

### 2.5 측정 스크립트

**경로:** `.metrics/collect_baseline.py`

```python
#!/usr/bin/env python3
# .metrics/collect_baseline.py
# 기준선 데이터 수집 및 baseline JSON 생성

import json
import os
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path

RUNS_DIR = Path(".metrics/runs")
BASELINE_DIR = Path(".metrics/baseline")
BASELINE_FILE = BASELINE_DIR / "baseline_2026-04-03.json"
GIT_TAG = "baseline-v1"
SAMPLE_SIZE = 30


def collect_runs(n: int) -> list[dict]:
    """직전 n건의 실행 로그를 수집"""
    run_files = sorted(RUNS_DIR.glob("run_*.json"), reverse=True)[:n]
    runs = []
    for f in run_files:
        try:
            runs.append(json.loads(f.read_text()))
        except json.JSONDecodeError:
            continue
    return runs


def compute_metrics(runs: list[dict]) -> dict:
    """8개 지표를 계산"""
    tokens = [r["build_prompt_tokens"] for r in runs if "build_prompt_tokens" in r]
    exec_times = [r["execution_time_s"] for r in runs if "execution_time_s" in r]
    we_counts = [r["write_edit_count"] for r in runs if "write_edit_count" in r]
    costs = [r["token_cost_usd"] for r in runs if "token_cost_usd" in r]
    qc_results = [r["qc_passed"] for r in runs if "qc_passed" in r]
    wt_results = [r["worktree_success"] for r in runs if "worktree_success" in r]

    return {
        "build_prompt_tokens_p50": p50(tokens) if tokens else None,
        "build_prompt_tokens_p95": p95(tokens) if tokens else None,
        "qc_pass_rate": sum(qc_results) / len(qc_results) if qc_results else None,
        "qc_fnr": None,  # 수동 측정 필요
        "execution_time_p50": p50(exec_times) if exec_times else None,
        "write_edit_count_per_task": p50(we_counts) if we_counts else None,
        "token_cost_per_task_usd": p50(costs) if costs else None,
        "worktree_success_rate": sum(wt_results) / len(wt_results) if wt_results else None,
    }


def write_baseline(metrics: dict, sample_size: int) -> None:
    BASELINE_DIR.mkdir(parents=True, exist_ok=True)
    data = {
        "schema_version": "1.0",
        "measured_at": datetime.now(timezone.utc).isoformat(),
        "sample_size": sample_size,
        "git_tag": GIT_TAG,
        "metrics": metrics,
        "notes": "",
    }
    fd, tmp = tempfile.mkstemp(dir=BASELINE_DIR, suffix=".tmp")
    try:
        with os.fdopen(fd, "w") as f:
            json.dump(data, f, indent=2)
            f.write("\n")
        os.replace(tmp, BASELINE_FILE)
    except Exception:
        os.unlink(tmp)
        raise
    print(f"기준선 저장 완료: {BASELINE_FILE}")


def tag_git() -> None:
    subprocess.run(
        ["git", "tag", "-a", GIT_TAG, "-m", "Phase 1 기준선 태그"],
        check=True,
    )
    print(f"git tag 생성 완료: {GIT_TAG}")


if __name__ == "__main__":
    runs = collect_runs(SAMPLE_SIZE)
    print(f"수집된 실행 건수: {len(runs)}/{SAMPLE_SIZE}")
    metrics = compute_metrics(runs)
    write_baseline(metrics, len(runs))
    tag_git()
```

### 2.6 git tag

```bash
# 기준선 수집 스크립트가 자동으로 태그를 생성함
python3 .metrics/collect_baseline.py

# 수동 태그 생성 (필요 시)
git tag -a baseline-v1 -m "Phase 1 기준선 태그"
git push origin baseline-v1
```

---

## 3. 인터페이스

### 3.1 파일 구조

```
.metrics/
  baseline/
    baseline_2026-04-03.json    ← 기준선 데이터 (schema_version: "1.0")
  runs/
    run_YYYYMMDD_HHMMSS.json    ← 개별 실행 로그 (자동 생성)
  collect_baseline.py           ← 기준선 수집 스크립트
```

### 3.2 실행 로그 스키마 (runs/*.json)

```json
{
  "run_id": "20260401_120000_abc123",
  "started_at": "2026-04-01T12:00:00Z",
  "completed_at": "2026-04-01T12:05:30Z",
  "execution_time_s": 330,
  "build_prompt_tokens": 1450,
  "write_edit_count": 8,
  "token_cost_usd": 0.0042,
  "qc_passed": true,
  "worktree_success": true
}
```

### 3.3 기준선 JSON 전체 스키마

```
{
  "schema_version": string,   // "1.0"
  "measured_at": string,      // ISO 8601 UTC
  "sample_size": integer,     // 실제 수집된 샘플 수
  "git_tag": string,          // "baseline-v1"
  "metrics": {
    "build_prompt_tokens_p50": float | null,
    "build_prompt_tokens_p95": float | null,
    "qc_pass_rate": float | null,     // 0.0~1.0
    "qc_fnr": float | null,           // 0.0~1.0, 수동 입력
    "execution_time_p50": float | null,
    "write_edit_count_per_task": float | null,
    "token_cost_per_task_usd": float | null,
    "worktree_success_rate": float | null  // 0.0~1.0
  },
  "notes": string
}
```

---

## 4. 테스트 기준

### 4.1 단위 테스트

```python
# tests/test_metrics_baseline.py

def test_p50_calculation():
    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    assert p50(data) == 5.5

def test_p95_calculation():
    data = list(range(1, 101))
    assert p95(data) == pytest.approx(95.05, rel=0.01)

def test_baseline_json_schema_valid(tmp_path):
    """생성된 baseline JSON이 스키마를 충족하는지 검증"""
    # 30건 더미 실행 데이터 생성 후 collect_baseline 호출
    ...
    data = json.loads(baseline_file.read_text())
    assert data["schema_version"] == "1.0"
    assert "metrics" in data
    assert len(data["metrics"]) == 8

def test_null_when_no_data(tmp_path):
    """실행 데이터 없을 때 metrics 값이 null"""
    runs = []
    metrics = compute_metrics(runs)
    assert all(v is None for v in metrics.values())

def test_atomic_write(tmp_path):
    """baseline 파일이 atomic하게 기록됨"""
    # tempfile → os.replace 검증
    ...
```

### 4.2 검증 체크리스트

- [ ] 30건 샘플 모두 수집 확인 (`sample_size == 30`)
- [ ] 8개 지표 모두 null이 아닌 값으로 채워짐
- [ ] `schema_version: "1.0"` 정확히 기록됨
- [ ] `git tag baseline-v1` 존재 확인: `git tag -l baseline-v1`
- [ ] `qc_fnr`는 수동 측정값 입력 완료

---

## 5. DoD (Definition of Done)

- [ ] `.metrics/runs/` 디렉토리에 30건 이상 실행 로그 존재
- [ ] `collect_baseline.py` 실행 완료
- [ ] `.metrics/baseline/baseline_2026-04-03.json` 생성 완료
  - [ ] `schema_version: "1.0"`
  - [ ] 8개 지표 모두 기록 (qc_fnr은 수동 입력 허용)
  - [ ] `sample_size: 30`
  - [ ] `git_tag: "baseline-v1"`
- [ ] `git tag baseline-v1` 생성 완료
- [ ] 단위 테스트 5건 이상 통과 (pytest)
- [ ] pyright 타입 오류 0건
- [ ] ruff 스타일 경고 0건
- [ ] PR 리뷰 승인 후 merge
