RAG Regression Gate
by db-inlee
README.md
# RAG Regression Gate (MCP)


**What this is** — RAG 운영자를 위한 진단 도구: *"무엇이 회귀했나"*(`run_gate`)와 *"지금 뭘 고쳐야 하나"*(`analyze_failures`)를 둘 다 답한다. 단순 "점수 하락"이 아니라 **실패모드**(retrieval_miss·hallucination 등)를 본다. RAG를 실행하지 않고 이미 만들어진 run-log를 **소비·판정**한다([ADR-001](docs/adr/ADR-001-run-log-based-gate.md)). *(baseline의 answerable 정확도 20%는 측정 대상일 뿐 — 산출물은 게이트의 분별력.)*
**Inputs** — baseline·candidate 각 디렉토리의 `run.jsonl` + `attribution.jsonl` (+ baseline `noise_band.json`). 필드·타입·예시(스키마 계약): [`docs/artifact_contract.md`](docs/artifact_contract.md).
**Outputs** — `PASS / WARN / FAIL` + `exit_code`(FAIL=1 → CI 머지 차단) + **실패모드 귀인**(무엇이 *유의하게* 회귀했나) + 룰 기반 제안. 노이즈밴드 + 부트스트랩으로 "유의한 회귀만", **거짓경보 0건**(같은 config 재실행은 항상 PASS — [ADR-002](docs/adr/ADR-002-bootstrap-ci-noise-band.md)). CI에서 PR에 보이는 모습: [`examples/pr_comment.md`](examples/pr_comment.md).
**30-second demo** — ↓ 아래 섹션(LLM·임베딩·GPU 없이 PASS/FAIL 재현).
**Architecture** — 판정 코어 `app/core`는 프레임워크 무관, **CLI · MCP · REST API** 3종 인터페이스가 포트-어댑터로 같은 코어를 호출(같은 입력 → 같은 판정, 5중 일치). 통계는 pydantic만. ↓ "아키텍처"·"REST API" 섹션, [ADR-004](docs/adr/ADR-004-mcp-is-an-interface.md).
**Why not DART-specific** — 엔진은 도메인을 모르고 4개 플러그인이 도메인 의존부를 담당. **3도메인**(DART 한국 금융 100 / 영어 위키 20 / Allganize 한국 법률·공공 40, 외부 공개 gold)에서 **엔진 git diff = 0** 실증. DART는 레퍼런스 인스턴스일 뿐. → [ADR-003](docs/adr/ADR-003-domain-adapters.md) · [`docs/portability.md`](docs/portability.md).
**Limitations** — **gold(평가셋) 전제**(reference-free는 범위 밖); 위키·Allganize는 인터페이스 검증용 **미니 인스턴스**(20·40문항, 방향성 증거 — DART 100이 메인 레퍼런스); Allganize는 **문서 단위 매칭**이라 DART(페이지/표)보다 거침. ↓ "적용 범위와 한계".
---
## 30초 데모 — CI 없이 로컬에서 게이트가 막는 것을 재현
게이트는 **LLM·임베딩·GPU 없이** 동작한다(`pydantic`만 필요). candidate의 채점·귀인 결과
(`run.jsonl` + `attribution.jsonl`)를 baseline과 통계 비교할 뿐이다.
```bash
pip install -r requirements-gate.txt # pydantic 하나
# ① 중립 변경(noise r2, 무변화) → 통과
python scripts/run_gate.py --baseline examples/baseline --candidate examples/demo_neutral
# → 🟢 GATE: PASS (exit 0)
# ② 회귀(top_k 5→1, 검색 약화) → 차단
python scripts/run_gate.py --baseline examples/baseline --candidate examples/demo_regression
# → 🔴 GATE: FAIL (exit 1)
# ❌ retrieval_miss 65→73 유의 증가 (CI [+2, +14]) → 검색 회귀
# ❌ 정답 정확도(grounded) 0.20→0.08 유의 하락 (CI [-0.20, -0.06])
```
CI에서는 이 `exit 1`이 머지를 막는다(GitHub branch protection의 Required check).
> **코퍼스에 대해**: `data/corpus/extracted/`의 코퍼스는 DART 공시 가공본이라 저작권상 repo에
> 포함하지 않는다. **full RAG(인덱싱~생성)**를 재현하려면 [DART](https://dart.fss.or.kr)에서 해당
> 보고서 PDF를 받아 `data/corpus/raw/`에 두고 `python scripts/extract_tables.py`를 실행하면 된다.
> 단, **헤드라인인 게이트 데모(PASS/FAIL)는 코퍼스 없이 `examples/`만으로 재현된다**(위 30초 데모).
---
## MCP 서버 — Claude/Cursor에서 "두 실행 비교해줘"
같은 게이트를 **MCP 도구**로 노출한다 — **두 도구**(`run_gate` + `analyze_failures`)로.
Claude Desktop/Cursor 같은 클라이언트가 이를 호출해 **회귀 판정·실패모드 진단·개선 힌트**를 받는다.
### 두 도구의 역할 — "무엇이 회귀했나" + "지금 뭐가 안 되나"
운영자는 두 가지를 묻는다. 이 서버는 그 둘을 각각 다른 도구로 답한다.
| | `run_gate` | `analyze_failures` |
|---|---|---|
| 답하는 질문 | **"무엇이 회귀했나?"** (변경 후 깨진 것) | **"지금 뭐가 안 되고, 뭘 먼저 고칠까?"** (현재 약점) |
| 입력 | 두 실행(baseline ↔ candidate) | 한 실행(`run_dir`) |
| 하는 일 | 통계적 회귀 판정 + 원인 config 역추적 | 병목 단계·약한 슬라이스 진단 + 개선 힌트 |
| 산출 | PASS/WARN/FAIL + 유의하게 회귀한 실패모드 | 실패 분포·병목 단계·슬라이스별 약점·RAGAS 환산·개선 우선순위 |
| 공통 | 둘 다 **운영자가 "다음에 뭘 할지"** 를 알게 한다 — 한쪽은 **회귀 진단**, 한쪽은 **개선 진단**. |
**`analyze_failures`의 핵심 가치 한 줄**: 점수만 보는 게 아니라 **어느 슬라이스가 어느 단계
(검색/생성/그라운딩/거부)에서 막히는지**를 짚어, 운영자가 **무엇부터 손볼지 방향**을 잡게 한다.
— 즉 개선을 대신 해주는 게 아니라 **개선 힌트**를 준다.
**운영 사이클 — 두 도구가 맞물리는 순환**:
```
analyze_failures → 운영자가 개선 적용 → run_gate → (개선 성공 시)
(약점·병목 진단, (검색/청킹/프롬프트… (그 변경이 회귀 없이 → 그게 새 baseline
개선 힌트) 운영자가 직접 수정) 개선됐는지 검증) 으로 채택
↑ │
└──────────── 운영 중 회귀 발생 시 재진단 ←───────────────────────────┘
run_gate("무엇이 깨졌나") + analyze_failures("어디가 문제인가") → 반복
```
진단(`analyze_failures`)으로 방향을 잡고 → 운영자가 고치고 → 검증(`run_gate`)으로 회귀 없는
개선임을 확인하면 그게 **새 baseline**이 된다. 운영 중 회귀가 나면 다시 두 도구로 재진단 —
이 순환을 반복한다.
> **정직성**: 두 도구 모두 개선을 **대신 수행**하거나 **"개선됐다"고 보장**하지 않는다.
> 약점을 짚어주는 **진단·힌트** 역할이고, 실제 개선은 **운영자**가, 그 효과 검증은 **`run_gate`**가 한다.
```bash
pip install ".[mcp]" # fastmcp는 옵션 extra (게이트 코어는 여전히 pydantic만)
python -m app.mcp.server # stdio MCP 서버 실행
```
Claude Desktop / Cursor의 `mcpServers` 설정에 등록:
```json
{
"mcpServers": {
"rag-regression-gate": {
"command": "python",
"args": ["-m", "app.mcp.server"],
"cwd": "/absolute/path/to/rag_regression"
}
}
}
```
**사용 시나리오**: Claude에게 *"이 두 RAG 실행 비교해줘"*(baseline/candidate 디렉토리 경로) →
`run_gate(baseline_dir, candidate_dir)` 호출 → **PASS/WARN/FAIL + 부트스트랩 CI + 실패모드 진단 + 제안**
(`GateResult`)을 돌려준다. 입력은 단순 경로 문자열(게이트 CLI와 동일 계약), 출력은 구조화된 Pydantic.
```
run_gate("examples/baseline", "examples/demo_regression")
→ verdict=FAIL, exit_code=1
regressions: retrieval_miss 65→73 (CI [+2,+14]), 정답정확도 0.20→0.08
suggestions: "[retrieval_miss] 검색 단계 회귀 … 원인 후보: top_k 5→1 → 우선 되돌림(top_k 1→5) 검토 …"
```
> **제안은 "검토 후보"지 "정답"이 아니다.** LLM이 생성하지 않고 **룰 기반 카탈로그**
> ([`docs/remediation_catalog.md`](docs/remediation_catalog.md): 실패모드→단계→기법 + config diff 역추적)로
> 결정적으로 만든다. **suggestion-only** — 게이트는 config를 자동 수정/실행하지 않으며, 모든 제안에
> "사람이 적용 후 이 게이트로 재검증" 문구가 붙는다. MCP 계층은 통계 로직을 한 줄도 재구현하지 않고
> 기존 엔진(`detect`→`gate`)을 **호출만** 한다 → CLI와 수치 동일.
### `analyze_failures` — 단일 실행 진단 (run_gate의 짝)
`run_gate`가 **두 실행을 비교**(회귀 감지)한다면, `analyze_failures`는 **한 실행을 진단**한다 —
*"바꿨더니 나빠졌나?"* 가 아니라 *"지금 어디가 약하고, 뭘 먼저 손볼까?"* 에 답한다. 운영자의 두 번째
니즈(성능을 올려야 할 때)를 위한 도구다. `run_dir` 하나만 받는다(비교 대상이 없으니 통계 검정 없음).
```
analyze_failures("examples/baseline")
→ failure_distribution: {retrieval_miss: 65, correct: 34, hallucination: 1}
bottleneck: retrieval ("retrieval_miss가 65건으로 가장 큰 병목")
groundedness: grounded 17 / unsupported 2 (맞았지만 근거 미실재 = 리스크)
ragas_equivalent: context_recall 0.19, faithfulness 0.89, answer_correctness 0.20
improvement_priorities: ① 검색(top_k↑·청크 축소) ② 표값 슬라이스 집중 … (적용 후 run_gate로 검증)
```
**RAGAS 환산 (judge 없이 결정적, 차별점)** — RAGAS의 친숙한 지표를 우리의 **결정적 측정으로 환산**한다.
LLM judge 호출이 없어 **같은 입력엔 같은 값**(재현 가능):
| RAGAS 개념 | 우리 결정적 측정 | 비고 |
|---|---|---|
| `context_recall` | `retrieval_success_strict` (gold 근거 ⊆ retrieved) | judge 없음 |
| `faithfulness` | `grounded / (grounded + unsupported)` | judge 없음 |
| `answer_correctness` | `answerable_accuracy` (grounded 기준) | judge 없음 |
| ~~`context_precision`~~ | — | **의도적 생략**: precomputed attribution(gold-free)만으론 산출 불가 |
| ~~`answer_relevancy`~~ | — | **의도적 생략**: judge 필요 → 결정성과 충돌 |
> **gold-free·결정적**: `analyze_failures`는 precomputed `attribution.jsonl`(케이스별 boolean)만 집계한다 —
> `eval_cases.jsonl`(gold)을 다시 읽지 않으므로 Phase 6의 gold 제거를 되돌리지 않는다. 새 통계/채점 로직 0.
> **suggestion-only + 닫힌 루프**: 개선 우선순위는 "검토 후보"이며, `analyze_failures`(약점 파악) →
> 개선 적용 → `run_gate`(개선 검증)로 닫는다.
---
## REST API — 같은 코어를 부르는 3번째 인터페이스
CLI · MCP에 더해 **FastAPI REST 서비스**로도 노출한다. 셋 다 프레임워크 중립 코어(`app/core/`)를
호출하므로 **같은 입력 → 같은 판정**(CLI == MCP == API). API 층은 fastapi/uvicorn만 의존하고 통계 로직을
재구현하지 않는다(게이트 코어는 여전히 pydantic만).
> **포트-어댑터 구조**: 판정·통계 코어(`app/core/`)는 프레임워크를 모르고, 각 어댑터가 자기 프로토콜로
> 그 코어를 노출한다 — CLI(`scripts/run_gate.py`) / MCP(`app/mcp/`, fastmcp) / REST(`app/api/`, fastapi).
> 새 인터페이스를 붙여도 코어는 그대로. 인터페이스 3종 = **같은 엔진의 3가지 접근**.
| 엔드포인트 | 설명 |
|---|---|
| `GET /health` | 헬스체크 → `{"status":"ok","version":...}` |
| `POST /evaluate` | 두 run(baseline↔candidate) 비교 → PASS/WARN/FAIL (`run_gate` 코어) |
| `POST /analyze` | 단일 run 진단 → 병목·슬라이스·RAGAS·우선순위 (`analyze_failures` 코어) |
| `GET /docs`·`/redoc` | OpenAPI 자동 문서(Swagger/ReDoc) |
입력은 run 아티팩트 디렉토리 경로(`attribution.jsonl` (+baseline `noise_band.json`)) — CLI/MCP와 동일 계약.
필드·타입·예시(입출력 스키마)는 [`docs/artifact_contract.md`](docs/artifact_contract.md)에 정의(테스트로 강제).
에러는 명확한 상태코드: **404**(경로/파일 없음) · **422**(필드 누락·타입오류·깨진 JSONL).
```bash
pip install -r requirements-api.txt # gate(pydantic) + fastapi + uvicorn
uvicorn app.api.main:app --port 8000 # /docs 에서 바로 호출 가능
# 회귀 판정 (CLI/MCP와 동일 수치)
curl -s localhost:8000/evaluate -H 'content-type: application/json' \
-d '{"baseline_dir":"examples/allganize_baseline","candidate_dir":"examples/allganize_candidate"}'
# → {"verdict":"FAIL","exit_code":1,"regressions":[{"metric":"answerable_accuracy","delta":-0.25,...}], ...}
# 단일 run 진단
curl -s localhost:8000/analyze -H 'content-type: application/json' \
-d '{"run_dir":"examples/allganize_baseline"}'
# → {"bottleneck_stage":"grounding","failure_distribution":{...},"ragas_equivalent":{...}, ...}
```
**Docker** (경량 — LLM/임베딩/torch 없음):
```bash
docker build -t rag-gate-api .
docker run -p 8000:8000 rag-gate-api
curl localhost:8000/health # {"status":"ok","version":"0.1.0"}
```
> **범위**: 이 API는 run-log/attribution을 받아 **게이트 판정·진단**을 노출하는 평가 서비스다. RAG 실행
> 자체(무거운 인덱싱/LLM)는 범위 밖 — 사용자 RAG가 run-log를 내보내면 게이트가 소비한다(run-log 계약).
**★ 5중 일치 (같은 입력 → 같은 판정)**: 게이트 수치는 5개 경로에서 동일하다 —
**독립 부트스트랩**(교차검증용 재구현) = **CLI** = **in-memory**(`detect()`) = **MCP** = **REST API**.
앞 셋(엔진 결정성)에 더해 MCP·API가 같은 코어를 부르므로 인터페이스가 수치를 왜곡하지 않는다.
CLI == MCP == API는 [`scripts/verify_api_equivalence.py`](scripts/verify_api_equivalence.py)가 같은 입력
(allganize baseline/candidate)에 대해 verdict·exit_code·전 메트릭·전 필드 동일을 출력해 증명한다.
---
## 메타 평가 — "분별 있게 반응한다" (Phase 5, config만 바꿔 생성)
동일 baseline에 **config 한 개씩만** 바꿔 게이트에 통과시킨 결과(임계 조작 없음):
| 변경 | answerable_acc | retrieval_miss | 게이트 | 진단 |
|---|---|---|---|---|
| (baseline) | 0.20 | 65 | — | — |
| **A. top_k 5→1** | 0.20→0.08 | 65→73 | 🔴 **FAIL** | **검색 회귀** |
| **B. reranker off→on** | 0.20→0.19 | 65→60 | 🟡 WARN | **개선 효과 없음** |
| **C. overlap 150→155** | 0.20→0.19 | 65→66 | 🟡 WARN | 중립(경계) |
- **A 진짜 회귀 → 잡음.** **B "좋아질 줄 알았던" reranker → 데이터상 유의한 개선 없음(WARN), 게이트가 유의한 개선이 아님을 정확히 판정** — 이게 회귀 게이트의 존재 이유. **C 중립 → FAIL 회피.**
- 위 표가 핵심이며 CI·유의성은 게이트가 그대로 출력. 전체 분석: [`reports/demo_summary.md`](reports/demo_summary.md).
---
## 오해 3가지에 대한 답 (의도적 프레이밍)
**1. "정확도 20%면 RAG가 별로 아닌가?"** — 이 프로젝트의 산출물은 정확도가 아니라 **게이트의 분별력**이다.
baseline은 튜닝 안 한 측정 기준점일 뿐. 20%든 80%든, 게이트가 해야 할 일은 "변경이 이걸 **유의하게**
악화시켰는가"를 정직하게 판정하는 것이고, 위 메타 평가가 그걸 증명한다.
**2. "100문항은 표본이 작아 노이즈 아닌가?"** — 맞다. 그래서 노이즈를 **정직하게** 다룬다:
- **노이즈 밴드**: 같은 config를 5회 반복 실행해 "가만히 있어도 흔들리는 범위"를 데이터로 측정(이 repo에선 결정적이라 밴드≈0, 불안정 케이스 0).
- **부트스트랩 신뢰구간**: 케이스 단위 paired 리샘플링으로 차이의 95% CI를 구해, **CI가 0을 벗어나고(통계적 유의) 노이즈 밴드(±1 case floor)도 넘을 때만** 회귀로 판정. 둘 중 하나만이면 WARN.
- 결과: **같은 config 재실행(거짓경보 테스트)은 회귀 0건**, 합성/실제 회귀는 FAIL.
**3. "DART 전용 아닌가?"** — 회귀 엔진(노이즈밴드·부트스트랩·게이트·judge검증·실패모드 귀인)은 **도메인 무관**이고,
DART(평가셋·표추출·한국어 숫자정규화·표 도메인 taxonomy)는 **레퍼런스 인스턴스**다.
경계와 MCP 인터페이스 후보(EvalSet/RAG Adapter/Scoring Plugin/Taxonomy): [`docs/portability.md`](docs/portability.md).
---
## 차별점
- **실패모드 진단**: "점수 하락"이 아니라 `retrieval_miss`/`hallucination`/`over_answer` 중 무엇이 유의하게 회귀했는지 귀인. retrieval_miss는 gold 근거 ↔ 검색 청크 매칭으로 judge 없이 결정적 판정.
- **judge 신뢰성 검증**: 본문 채점 LLM(gpt-4o)을 gold로 검증 — 정답/오답 probe로 judge_accuracy = 0.987(혼동행렬 포함). 미묘 변형 probe까지 써서 거짓 고득점을 방지. → [`reports/judge_validation.json`](reports/judge_validation.json).
- **채점 전략: judge는 선택적·검증 후 사용**: 숫자/표값은 단위 정규화(조·억) + ±0.1% 허용오차로 judge 없이 결정적, 답없음은 거부 문구 매칭으로 결정적, retrieval_miss는 gold 근거 ⊆ 검색 청크로 결정적. judge는 본문(서술형)에만 쓰고 그조차 gold로 검증(0.933→0.987). 회귀 게이트는 *'같은 입력엔 같은 판정'* 이 생명이라 `temperature=0`+seed로 노이즈밴드 std=0을 달성했고, 비결정성의 표면적을 본문으로 좁혔다.
- **RAGAS 대비**: RAGAS는 훌륭한 범용 RAG 평가 프레임워크다. 우리는 그걸 부정하는 게 아니라, *'CI 회귀 게이트'* 목적상 결정성을 우선했다 — *개념은 빌리되(groundedness 등) 측정은 가능한 한 결정적으로*. judge 한 번의 흔들림이 PASS/FAIL을 뒤집으면 게이트로 못 쓰기 때문. (설계 근거 전문: [`docs/JOURNEY.md` — 설계 결정](docs/JOURNEY.md))
- **groundedness 분리**: 맞은 답도 정답값이 검색 근거에 실재(grounded)하는지 확인. 암기/운으로 맞은 `unsupported_correct`는 헤드라인 정확도에서 분리(모델 암기력이 RAG 점수를 부풀리지 않게).
- **no_answer 착시 방어**: answerable 정확도와 no_answer 정확도를 항상 짝으로 보고(전부 거부하는 시스템이 들통나도록).
- **통계적 정직 + 거짓경보 0건**: 노이즈밴드 + 부트스트랩으로 "유의한 회귀만" FAIL. 같은 config 재실행은 항상 PASS(거짓경보 0건)로 게이트 신뢰성 검증 — 데모용 임계 조작 없음.
- **도메인 범용성 실증(3도메인)**: DART(한국 금융) / 영어 위키 QA / Allganize(한국 법률·공공, 외부 공개 gold)에서 같은 게이트 작동, 엔진 코드 0줄 변경(git diff = 0). ★ Allganize는 우리가 만들지 않은 남의 gold이고 병목이 DART와 정반대(검색 vs 생성/그라운딩) — 같은 `analyze_failures`가 DART엔 top_k↑, Allganize엔 citation을 처방한다(더 강한 범용성 증거). 단 위키·Allganize는 인터페이스 검증용 미니 인스턴스(20·40문항)이고 DART(100문항)가 메인 레퍼런스다. Allganize는 문서 단위 매칭이라 DART보다 거칠고 스캔 이미지 문서는 제외. 출처: datalama/RAG-Evaluation-Dataset-KO(MIT). → [`docs/portability.md` §5](docs/portability.md).
## 적용 범위와 한계
이 엔진은 **gold(평가셋)를 전제**로 한다. 가진 gold의 종류에 따라 작동 범위가 갈린다:
- **정답 + 근거 라벨**(DART) → accuracy · retrieval_miss · groundedness **전 기능**(gold 근거 ⊆ retrieved, 결정적).
- **정답만**(근거 라벨 없음 — 더 흔함) → accuracy 작동, retrieval_miss는 *'정답 텍스트가 검색 청크에 있나'* 로 대체 가능(위키 `wiki_value_present`가 이 방식).
- **정답조차 없음(reference-free)** → **범위 밖**. 정답 없이 옳고 그름을 판정하려면 judge 의존이 불가피해 우리의 *결정성* 원칙과 충돌한다.
**왜 한계가 아니라 정의인가**: 회귀 감지는 본질적으로 **비교 기준**이 있어야 성립한다 — 고정 평가셋 없이 *'깨졌다'* 를 판단하는 건 원리적으로 불가능하다. 따라서 평가셋 전제는 '회귀 게이트'의 정의에 내재한 조건이다(promptfoo·RAGAS의 reference 기반 평가도 같은 전제). → 상세 [`docs/JOURNEY.md` — 설계 결정](docs/JOURNEY.md), [`docs/portability.md`](docs/portability.md).
---
## 아키텍처 — 게이트(가벼움)와 RAG 실행(무거움) 분리
```
[무거움 — CI 밖/선택] [가벼움 — CI 안, 활성]
인덱싱(bge-m3 2.2GB) + 100케이스 LLM 게이트: run-log+attribution을 baseline과
+ gpt-4o judge → run-log + attribution ──▶ 부트스트랩 비교 → PASS/WARN/FAIL (pydantic만)
```
- **모델 1(현재)**: 무거운 RAG는 CI 밖에서 실행, candidate 산출물을 PR에 첨부. CI는 게이트만(`.github/workflows/regression-gate.yml`).
- **모델 2(전환 경로)**: `generate-candidate.yml`(Job 1, 스켈레톤)의 트리거를 `pull_request`로 바꾸고 게이트로 핸드오프하면 완전 자동 — **게이트 로직은 한 줄도 안 바뀐다.**
## 테스트
`python -m pytest -q` → **56 passed** (LLM·임베딩 없이; 게이트는 결정적이라 기대값을 정확히 박는다).
부품 테스트(scorer/judge/table_extract)에 더해 **게이트 행동 자체**를 검증한다:
| 파일 | 무엇을 고정하나 |
|---|---|
| `test_gate_behavior.py` | 회귀 FAIL / 동일 run PASS / 노이즈 floor·경계 WARN (거짓경보 0건) |
| `test_interface_equivalence.py` | 코어 == REST API == `detect_paths` 동치, API의 fastmcp 비의존 |
| `test_generic_adapter.py` | Generic 어댑터가 전용 어댑터를 정확히 재현(GoldMatcher/EvalProvider/value_present) |
| `test_noise_floor_domain.py` | floor가 런타임 모집단(40·100) 사용 — denom 하드코딩 회귀 방지 |
| `test_attribution_contract.py` | gold-free 판정 + attribution 필수 8필드([`artifact_contract.md`](docs/artifact_contract.md) 강제) |
## 구조
```
app/rag/ 수집·표추출·청킹·인덱싱·pipeline (DART/RAG)
app/evaluator/ scorer · judge · validate_judge · attribution · metrics · case_eval
app/regression/ detect(부트스트랩) · gate(PASS/WARN/FAIL) ← 도메인 무관 엔진(3개 도메인 공유)
app/interfaces.py 플러그인 Protocol 4종(+화이트리스트) app/adapters/ dart · wiki · allganize · generic 구현체
app/core/ service(run_gate 코어) · analyze(analyze_failures 코어) · suggest(룰 제안) ← 프레임워크 중립 코어
app/mcp/ server ← 인터페이스① MCP 어댑터(fastmcp, 옵션 extra) — app/core 호출
app/api/ main · schemas ← 인터페이스② REST 어댑터(fastapi/uvicorn) — app/core 호출
scripts/ run_gate(인터페이스③ CLI) · run_eval · run_attribution · measure_noise · *_gate_demo · demo_generic · verify_api_equivalence
examples/ baseline / demo_* / wiki_* / allganize_baseline · allganize_candidate · allganize_generic (게이트 입력 데모)
data/wiki_eval/ SQuAD 2.0 발췌(20문항, CC BY-SA 4.0) data/allganize_eval/ Allganize 법률·공공 발췌(40문항)+코퍼스+라이선스(MIT)
reports/ 원본 산출물(메트릭·노이즈밴드·judge검증·시나리오) gate_runs/ 는 게이트 부산물
docs/ adr/(설계 결정 기록 4종) · portability(엔진 vs 도메인 경계 + 3도메인 실증 + 범용성 절감) · interfaces(플러그인 설계) · remediation_catalog(제안 근거) · artifact_contract(입출력 스키마 계약)
Dockerfile REST API 컨테이너(경량 — pydantic+fastapi+uvicorn, LLM/임베딩 없음)
```
> **인터페이스 3종 = 같은 코어**: 판정·통계는 `app/core/`(프레임워크 무관)에 한 번만 있고, ① MCP(`app/mcp`)
> ② REST API(`app/api`) ③ CLI(`scripts/run_gate.py`)가 포트-어댑터로 그 코어를 노출한다 — 같은 입력 → 같은 판정.
표추출의 알려진 한계는 [`KNOWN_ISSUES.md`](KNOWN_ISSUES.md). (빌드 티켓·불변 규칙·기획 등 내부 작업 문서는 공개 범위에서 제외.)
**검증으로 실제 잡은 결함들**(연도누락·환각·채점 false negative·judge 무효 probe·암기정답·가짜 retrieval_miss·음수기준 증가율 등)과 그 수정 기록: [`docs/JOURNEY.md`](docs/JOURNEY.md).
**설계 결정(왜 이렇게 만들었나) — 결정·대안·트레이드오프**: [`docs/adr/`](docs/adr/) (run-log 기반 게이트 / bootstrap+noise band / 도메인 어댑터 / MCP는 인터페이스).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues