# 증거와 수락

> 워커 self-report와 독립 검증 구분, 계약 필드, 리뷰 렌즈, acceptance 판정 규칙과 측정 가능한 증거 형태.

- Repository: local/mogui-ADE-orchestrator

- Human docs: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac
- Complete Markdown: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac/llms-full.txt

## Source Files

- `docs/public/delegation-and-review.md`
- `docs/public/concepts.md`
- `src/master_runtime/core/acceptance/loop.py`
- `src/master_runtime/core/acceptance/verdict.py`
- `src/master_runtime/core/approval/gates.py`
- `master-ops/docs/runbooks/contract-conventions.md`
- `master-ops/docs/charter/04-worker-routing-review.md`

---

---
title: "증거와 수락"
description: "워커 self-report와 독립 검증 구분, 계약 필드, 리뷰 렌즈, acceptance 판정 규칙과 측정 가능한 증거 형태."
---

수락(acceptance)은 워커 완료 보고와 별개의 마스터 결정이다. 운영 경로에서는 계약·디스패치 게이트·독립 검증이 선행되고, 기계 경로는 `scripts/acceptance-loop`와 `src/master_runtime/core/acceptance/`가 casebook 대비 pass count를 순수 비교한다. 워커 self-report는 증거가 아니다.

## Self-report와 독립 검증

마스터는 위임 산출을 신뢰하지 않는다. 채터 규칙(`master-ops/docs/charter/03-execution-principles.md`)과 공개 가이드(`docs/public/delegation-and-review.md`)가 같은 경계선을 고정한다.

| 구분 | 의미 | 수락 기여 |
| --- | --- | --- |
| Worker self-report | "완료", "통과", "READY" 등 워커가 쓴 주장 | 없음 — 조사 시작 신호만 됨 |
| Independent verification | 마스터가 재실행·재검사한 결과 | 있음 — 수락 전제 |
| Configured | 파일·훅·스크립트·정적 계약이 존재 | 배선 증거 아님 |
| Observed | git·로컬 실행·로그·ledger·프로브가 self-report 밖에서 확인 | 운영 증거 |

공개 문서의 증거 라벨(`Configured` / `Intended` / `Observed` / `Unknown`)은 "파일이 있다"를 "동작한다"로 승격하지 않는다. 통과한 테스트 스위트는 **그 유닛의 로컬 실행 증거**일 뿐, 워크스페이스가 실경로에서 그 유닛을 강제한다는 증명은 아니다.

### 측정 가능한 증거 형태

마스터가 직접 검사할 수 있는 것만 증거다.

| 형태 | 예 | 비고 |
| --- | --- | --- |
| Diff / 변경 표면 | `git diff`, 워커가 선언한 `surfaces` | 계약 허용 표면과 대조 |
| 테스트·게이트 실행 | pytest 전체 스위트, redaction scan exit | **숫자 + 명령** 쌍으로 기록 |
| 로그·프로브 | dispatch ledger, probe stdout, model probe | 파이프 exit가 아닌 명령 자체 exit |
| 생성 파일 | 리포트, 산출 아티팩트 | 경로·해시 또는 내용 대조 |
| 권위 문서 | 승인된 스펙, 핸드오프, Role State | 채팅 기억보다 우선 |
| 배치 판정 | `in_expected_worktree` / `is_master_checkout` / `branch` | 절대 경로는 하향만, 상향은 boolean |

<Warning>
표면(surface)이 초록이어도 사물(the thing)이 아닐 수 있다. 파이프의 exit, 잘못된 Run의 mailbox, 디스패치 기록의 주입 플래그는 각각 "보고"이지 본체가 아니다. 초록 신호를 믿기 전에 **무엇을 읽었는지**를 먼저 확인한다.
</Warning>

검증 절(`contract-conventions` §8) 규칙:

- 형용사 금지 — `"Gates pass"` 대신 `"448 passed, redaction scan OK, inventory exit 1 with baseline 445"`.
- 카운트 옆에 **실행한 명령**을 붙인다. 명령 없는 숫자는 검증 불가.
- empty check conclusion은 pending이지 pass가 아니다.
- 파이프를 끼우면 exit는 파이프 것일 수 있다. 명령 자체의 exit를 읽는다.

## 계약 필드 (worker contract)

계약은 워커의 전체 세계다. 침묵한 자리에서 워커는 합리적이지만 마스터와 다른 선택을 한다. 재사용 절은 `master-ops/docs/runbooks/contract-conventions.md`에 있으며, 가능하면 **복붙**하고 의역하지 않는다.

공개 가이드가 요구하는 관찰 가능 축:

| 필드 | 역할 |
| --- | --- |
| target repository / checkout | 어느 트리에서 작업하는지 |
| allowed work surface | 편집·조회 허용 범위 |
| acceptance criteria | 마스터가 재현할 통과 조건 |
| required evidence | 어떤 측정 산출을 제출할지 |
| commit / push / branch rules | 커밋·푸시·브랜치 권한 (추론 금지) |
| known exclusions / forbidden edits | 금지 표면과 제외 |

### 필수에 가까운 운영 절

<AccordionGroup>
  <Accordion title="Workspace / placement">
    계약 **하향**에는 절대 경로를 명시한다. 워커 **상향 보고**는 경로 대신 판정만:

    ```text
    FIRST ACTION (report these three, and no absolute path):
    - in_expected_worktree: yes|no
    - is_master_checkout:   yes|no   # yes stops immediately
    - branch:               `git branch --show-current`
    ```

    `is_master_checkout: yes`면 즉시 중단. 마스터 체크아웃 오염을 막기 위한 비대칭(경로는 아래, 판정은 위)이다.
  </Accordion>
  <Accordion title="Commit 권한">
    계약에 없으면 보수적으로 uncommitted 상태로 보고한다. 예:

    ```text
    Local commit allowed. Push forbidden. Commit only files changed for this job.
    Evidence file stays uncommitted.
    ```
  </Accordion>
  <Accordion title="Verification / merge stewardship">
    워커가 PR을 열면 merge-ready까지 보유한다. 보고 시점 네 측정:

    1. 모든 체크 non-empty conclusion
    2. unresolved review thread 0 (스레드별 개별 회신)
    3. `origin/main` fetch·merge, conflict 없음
    4. merge 후 게이트 재실행 + 카운트

    `READY` 또는 정확한 블로커. **머지 자체는 마스터**.
  </Accordion>
  <Accordion title="Takeover / Writing / redaction">
    - takeover는 **새 계약 파일** — `dispatch-gate`는 contract hash ledger로 최근 동일 해시를 중복으로 거절할 수 있다.
    - Writing 블록(conventional commits, PR 템플릿 섹션명, 절대 경로·username 금지)은 계약에 그대로 싣는다.
    - 공개 forge 표면에는 absolute path / identity string 금지; 인용은 `~/path`, `<home>` 형태.
  </Accordion>
</AccordionGroup>

계약 해석이 갈리면 부분 수락하지 않는다. 마스터가 리스를 수정하고, 개정 이유를 기록한 뒤 redispatch하거나 변경을 요청한다. 워커 완료와 수락은 분리된 상태로 남긴다.

## 디스패치 경계와 수락 순서

```text
check -> dispatch -> register -> independent verification -> acceptance
```

| 단계 | 역할 | 증거 |
| --- | --- | --- |
| `check` | 계약 읽기, ledger에 허가 기록 | dispatch ledger 결정 |
| `dispatch` | 런타임에 워커 기동 (게이트가 래핑하지 않음) | 호스트/세션 배치 |
| `register` | job id가 기대 아티팩트에 있는지 **probe** | probe exit 0 + stdout에 id |
| independent verification | 계약 기준 재검사 | 마스터 실행 결과 |
| acceptance | 마스터 판정 | 수락 리포트 / 게이트 결정 |

`register`의 probe는 예시처럼 `grep <job-id> ./worker.log`처럼 exit 0과 id 출력을 동시에 만족해야 한다. 경고 부재를 허가로 읽지 않는다.

## 리뷰 렌즈

비중대한 머지·직접 shared-state 변경의 기본은 **세 렌즈 분할**이다 (charter §4, `delegation-and-review`).

| 렌즈 | 질문 |
| --- | --- |
| general correctness | 결과가 동작하는가 |
| regression disproof | 기존 동작이 깨지지 않았음을 반증할 수 있는가 |
| contract and scope | 워커가 계약을 지켰는가 |

규칙:

- **다수 판결**을 쓰되, 소수 **P1 `FIX_FIRST`** 는 처리하거나 증거와 함께 명시 기각한다.
- 인기 투표가 아니다. contract/regression 렌즈의 blocking 이슈는 다른 렌즈가 긍정적이어도 수락 전에 처리한다.
- 렌즈 분리는 **운영 규율**이다. 현재 코드에 별도 합의 엔진은 없다 (`concepts.md` Steering: Intended).

PR review-bot 스레드는 라운드마다 오너 지시 없이 워커가 처리하고, 마스터가 검증·기각만 판정한다. bot finding은 코드 대비 재측정한 뒤 행동한다. 읽지 않고 resolve한 스레드는 리뷰가 아니다.

머지 직전 재측정: `review_measured_at` 이후 `submittedAt`이 더 늦은 bot review가 있으면 전량 읽고 나서 머지한다. 깨끗한 워커 보고는 이후 bot review가 생기는 순간 소모된다.

## Approval 게이트 (steering)

`src/master_runtime/core/approval/gates.py`와 `registry.py`는 **Proposal → Approval → Execution** 을 강제한다.

### GateClass

| 클래스 | 조건 |
| --- | --- |
| `G0_READ_ONLY` | 읽기만 |
| `G1_REVERSIBLE_LOCAL` | 로컬 가역 쓰기 |
| `G2_SHARED_STATE` | 공유 상태 쓰기 |
| `G3_IRREVERSIBLE` | 비가역 |

`classify(ActionSpec)`는 가장 엄격한 게이트를 고른다. `read_only`이면서 write/irreversible이면 `ValueError`.

### ProposalRegistry

- `propose` → `proposal-N` id
- `decision(verdict, authority)` — 한 번만 (`AlreadyDecided`)
- `guard(action, proposal_id)` — G0 제외 시 승인된 동일 `ActionSpec` 필요; 성공 시 `CONSUMED`
- G2/G3 **승인**은 `ApprovalAuthority.HUMAN` 필수 (`POLICY` 불가)

이 레지스트리는 수락 루프와 직교한다: 위험 행동 실행 권한과, 산출물 품질 수락은 다른 축이다.

## 기계적 acceptance loop

Deterministic loop: `scripts/acceptance-loop` → `run_acceptance_loop` / `decide`. **모델 판단·휴리스틱 점수·자연어 근거는 판정에 참여하지 않는다.** 비교 단위는 gated split의 **combined pass count** 뿐이다.

### Case split

| Split | 별칭 | 가시성 | 역할 |
| --- | --- | --- | --- |
| `train` | `visible` | proposer 가시 | 수정 대상 실패 목록 |
| `holdout` | `private` | 비공개 | overfitting 방지 게이트 |
| `scorecard` | `acceptance` | 비공개 메타 | 게이트 외 참고 점수 |

- `GATED_SPLITS = (train, holdout)` — `combined_passed` / `decide` 비교 대상
- 가시성 단일 술어: `is_visible_split` (`VISIBLE_SPLITS = {train}`)
- casebook validation: 최소 1 case, train·holdout 각각 1+, **seed strata 집합 동일**, `case_id` 중복 금지
- 누락 case 결과는 fail-closed (`MISSING_RESULT_DETAIL`) — evaluator skip으로 pass count 상승 불가
- split/stratum 소유권은 **casebook**, evaluator 아님

### 판정 규칙 (`decide`)

| `AcceptanceReason` | 조건 | `accepted` |
| --- | --- | --- |
| `PASS_COUNT_INCREASED` | `candidate_combined > current_combined` 이고 surface 변경 | `true` |
| `NO_PASS_COUNT_INCREASE` | 변경했으나 combined pass 증가 없음 | `false` |
| `NO_CANDIDATE_CHANGE` | `surfaces` 비어 변경 없음 (`candidate.changed == false`) | `false` |

동일 pass면 거부. "아무 것도 안 바꿨다"도 `decision.json`에 남겨 감사 추적을 유지한다.

### 루프 동작 요약

1. baseline 평가 → 현재 scorecard
2. 완전 통과(`is_complete`)면 중단
3. proposer workspace에 **visible failures만** 기록 (`visible_failures.json`, `casebook_visible.json`, `task.md`)
4. proposer가 `candidate.json` (`surfaces`, `summary`) 작성; 없으면 iteration 종료
5. 변경 시 gated 재평가 → `decide` → 수락 시 current 교체, 거부 시 `on_reject` restore
6. in-place mutator + `max_iterations >= 2` 이면 restore hook 필수 (`--restore-cmd`)
7. `report.json` / `report.md` 기록

### 산출물 레이아웃

```text
<run_dir>/
  manifest.json
  split.json
  split.md
  report.json
  report.md
  history/
    visible/          # proposer에 복사 가능
      iterations/NNN/decision.json|md
      iterations/NNN/proposer_workspace/
      train/<label>/result.json
    private/          # proposer 금지
      holdout/<label>/result.json
```

쓰기 경로는 원자적 replace (`write_json` / `write_text`)라 중단된 run도 잘린 JSON을 사실로 읽히지 않게 한다.

### CLI

| 하위 명령 | 입력 | exit |
| --- | --- | --- |
| `validate --config` | casebook 로드 검증, 카운트 JSON | 0 / 설정 오류 2 |
| `split --config [--output-dir]` | split manifest 기록 | 0 |
| `run --config [--max-iterations] [--baseline-ref] [--restore-cmd]` | 루프 실행, Markdown 리포트 stdout | 완전 통과 0, 미완전 1, 설정/가드 2 |
| `inspect --run-dir` | `report.json` 출력 | 없음 2 |

설정 JSON 핵심 키: `name`, `workspace_root`, `run_dir`, `max_iterations`(기본 3), `proposer.runtime` / `model` / `timeout_seconds`, `cases[]`, optional `regression_log`.

케이스 필드:

```json
{
  "case_id": "t1",
  "split": "train",
  "stratum": "unit",
  "command": ["pytest", "-q"]
}
```

`command` 없는 케이스는 `command_evaluator`에서 fail (`case has no command`).

<RequestExample>
```bash
scripts/acceptance-loop validate --config ./acceptance.json
scripts/acceptance-loop run \
  --config ./acceptance.json \
  --max-iterations 3 \
  --restore-cmd 'git checkout -- .'
scripts/acceptance-loop inspect --run-dir ./runs/example
```
</RequestExample>

## 운영 수락 체크리스트

마스터 수동 수락(일반 디스패치)과 기계 루프를 같은 회의 테이블에 둔다.

| 단계 | 수동 경로 | 기계 경로 |
| --- | --- | --- |
| 계약 연결 | 리스·contract hash·job id | casebook + baseline ref |
| 변경 표면 | diff vs allowed surface | `candidate.surfaces` |
| 필수 검사 재실행 | 계약 named commands, 카운트 | gated case commands |
| 범위/회귀 | 3-lens review | train+holdout pass delta |
| 잔여 위험 | 미실행 검사·열린 스레드 명시 | incomplete scorecard |
| 사후 | `worker-reap`, ledger | `report.json` 보존 |

수락 리포트는 **통과한 것**, **돌리지 않은 것**, **남은 위험**을 적는다. 검증 불가면 아직 미수락이다.

## 실패 모드

| 증상 | 원인 | 대응 |
| --- | --- | --- |
| 워커 "pytest 2 passed" | 전체 스위트 대신 부분 파일 | 계약에 명령·기대 카운트 명시, 마스터 재실행 |
| holdout 누락 config | casebook invalid | `validate` exit 2, strata 정합 |
| 반복 거부 후 다음 후보가 더 나쁨 | in-place 미복원 | `--restore-cmd` 또는 `max_iterations=1` |
| `NO_CANDIDATE_CHANGE` | empty `surfaces` | proposer가 `candidate.json` 작성 규칙 준수 |
| register 실패 | probe에 job id 없음 | 아티팩트·probe-cmd 수정, 미등록 디스패치를 사후 인정하지 않음 |
| 머지 후 bot 재발견 | stale worker report | merge-time re-measure (`review_measured_at`) |
| placement 오염 | 모호한 worktree 절 | 절대 경로 쌍 + boolean 보고 절 |

## 관련 런타임 유닛

| Unit | 관련 |
| --- | --- |
| U5 Worker Scheduler | `dispatch_gate` check/register, 계약 해시 ledger |
| U6 Approval Manager | `approval/gates.py`, `approval/registry.py` |
| U11 Observability | `acceptance/` 점수·리포트, digest/watchdog 부분 |

## Next

<CardGroup>
  <Card title="Supervised dispatch" href="/supervised-dispatch">
    check → dispatch → register, 계약 해시·ledger, model probe
  </Card>
  <Card title="acceptance-loop 레퍼런스" href="/acceptance-loop-reference">
    validate·split·run·inspect, holdout, max-iterations, 산출물
  </Card>
  <Card title="Worker reap" href="/worker-reap">
    수락 후 lease·terminal·worktree 정리
  </Card>
  <Card title="방어 인벤토리" href="/defense-inventory">
    디스패치·프로브·placement·redaction 가드 표
  </Card>
</CardGroup>
