FL2 Evidence Gate — 증거 없는 완료 차단
한 줄 요약
완료를 단언하는 문장(done·완료·통과 등)이 실측 증거 토큰 없이 등장하면 차단하는 fail-closed 게이트다. 왜 배우나: 에이전트가 “다 했습니다”라고 말만 하고 실제로는 안 한 경우를 코드 한 줄 호출로 결정론적으로 걸러내기 위해서다.
그림
flowchart TD
A["입력 텍스트 / Claim"] --> B{"검사 진입점"}
B -->|"check_claim(Claim)"| C{"완료어 등장?<br/>COMPLETION_RE"}
B -->|"scan_text(str)"| S0{"입력이 비었나?"}
C -->|"없음"| P1["passed=True<br/>게이트 대상 아님"]
C -->|"있음"| D{"evidence 비었나?"}
D -->|"비어 있음"| F1["passed=False<br/>fail-closed: 증거 없음"]
D -->|"있음"| E{"증거 토큰 중<br/>ACCEPTED_EVIDENCE_RE 매치?"}
E -->|"하나도 없음"| F2["passed=False<br/>fail-closed: 증거 불일치"]
E -->|"하나 이상"| P2["passed=True<br/>증거로 뒷받침됨"]
S0 -->|"비었음"| F3["passed=False<br/>fail-closed: 검증 불가"]
S0 -->|"내용 있음"| R{"범위-축소 패턴<br/>SCOPE_REDUCTION 매치?"}
R -->|"매치"| F4["passed=False<br/>즉시 위반 (증거 무관)"]
R -->|"없음"| Q{"약속 패턴 매치<br/>그리고 완료-증거 전무?"}
Q -->|"예"| F5["passed=False<br/>약속-미실행"]
Q -->|"아니오"| P3["passed=True<br/>early-stop 패턴 없음"]
쉽게 풀기
회사에 “끝냈습니다”라고 보고하는 직원이 있다고 하자. 정직한 관리자라면 보고를 곧이곧대로 믿지 않고 “증거를 보여줘”라고 한다. Evidence Gate가 바로 이 관리자다.
직원의 보고서를 받으면 관리자는 두 가지 방식으로 검사한다.
- 단계 하나: 보고서에 “끝”, “완료”, “통과”, “배포됨” 같은 완료 선언이 있는지 본다. 없으면 애초에 “끝났다”는 주장 자체가 없으니 통과시킨다. 트집 잡을 일이 없다.
- 단계 둘: 완료 선언이 있다면, 그 옆에 증거가 붙어 있는지 본다. 여기서 인정되는 증거는 정해져 있다. 종료 코드(exit code), 테스트 통과/실패 기록, grep 결과, “3개 발견” 같은 개수, 실제 파일 경로(예: src/app.py), 렌더링 결과, 스크린샷. 이 중 하나도 없으면 “말로만 했네” 하고 차단한다.
핵심 철학은 “의심스러우면 막는다(fail-closed)“이다. 보통의 시스템은 “문제 없어 보이면 통과”인데, 이 게이트는 반대다. 증거가 확실하지 않으면 무조건 실패 처리한다. 입력이 아예 비어 있어도 “검증할 수 없으니 실패”다.
두 번째 검사기(scan_text)는 또 다른 두 가지 나쁜 습관을 잡는다. 하나는 “범위 축소” — “시간 관계상 나머지는 생략하겠습니다” 같은 꼼수. 이건 증거가 있건 없건 보이는 즉시 위반이다. 다른 하나는 “약속만 하고 안 함” — “이제 구현하겠습니다”라고 말해놓고 정작 했다는 증거(“구현했”, “통과”, “exit 0” 등)가 한 줄도 없는 경우. 약속만 있고 실행 증거가 없으면 막는다. 단, “구현하겠습니다 … 구현했고 테스트 통과”처럼 약속 뒤에 실행 증거가 함께 있으면 정상 보고로 보고 통과시킨다(오탐 방지).
핵심 정리
| 항목 | 함수/패턴 | 판정 기준 | 결과 |
|---|---|---|---|
| 완료 단언 검증 | check_claim | 완료어 있는데 증거 비었거나 불일치 | passed=False |
| 완료어 없음 | check_claim | COMPLETION_RE 미매치 | passed=True (대상 아님) |
| 범위 축소 | scan_text | SCOPE_REDUCTION 매치 | passed=False (즉시) |
| 약속-미실행 | scan_text | 약속 있고 완료-증거 전무 | passed=False |
| 빈 입력 | scan_text | s가 빈 문자열 | passed=False (fail-closed) |
[!note]- 인정 증거 정규식(ACCEPTED_EVIDENCE_RE) — 글자 그대로
(exit\s*code|exit\s*\d|test.*(pass|fail)|grep|\d+\s*(개|matches)|/[\w./-]+\.\w+|render|스크린샷)즉 인정되는 증거 종류: 종료 코드(exit code / exit 0), 테스트 통과·실패 기록, grep, “N개” 또는 “N matches” 형태의 개수, 확장자가 붙은 파일 경로, render, 스크린샷.
[!note]- 완료어 정규식(COMPLETION_RE)
(done|완료|통과|fixed|배포됨|작동(한다|함)|passed)이 표현이 텍스트에 있으면 게이트 대상이 되어 증거 검사를 받는다.
[!note]- 세 가지 패턴 묶음의 역할 차이
- SCOPE_REDUCTION_PATTERNS: 꼼수 차단. 매치 즉시 위반, 증거와 무관.
- PROMISE_PATTERNS: “하겠습니다 / will now / let me” 등 약속 표현 탐지.
- EVIDENCE_PATTERNS: “구현했 / 통과 / exit 0 / deployed” 등 약속이 실행됐다는 신호. 약속과 함께 있으면 위반에서 제외(오탐 완화).
실제 예시
# fablelayer/evidence_gate.py — 핵심 자료구조와 진입점
@dataclass(frozen=True)
class Claim:
text: str
evidence: tuple[str, ...]
@dataclass(frozen=True)
class GateResult:
passed: bool
reasons: tuple[str, ...]
def check_claim(claim: Claim) -> GateResult:
has_completion = _COMPLETION.search(claim.text) is not None
if not has_completion:
return GateResult(passed=True,
reasons=("no completion claim; gate not applicable",))
if not claim.evidence:
return GateResult(passed=False,
reasons=("completion claim present but evidence empty (fail-closed)",))
matched = tuple(e for e in claim.evidence if _has_accepted_evidence(e))
if not matched:
return GateResult(passed=False,
reasons=("completion claim present but no evidence token matches "
"ACCEPTED_EVIDENCE_RE (fail-closed)",))
return GateResult(passed=True,
reasons=(f"completion claim grounded by {len(matched)} evidence token(s)",))
# 사용 예시 — 호출 측 관점
# 1) 증거 없는 완료 단언 → 차단
r = check_claim(Claim(text="구현 완료했습니다", evidence=()))
# r.passed == False
# r.reasons == ("completion claim present but evidence empty (fail-closed)",)
# 2) 증거가 있으나 인정 패턴에 안 맞음 → 차단
r = check_claim(Claim(text="통과", evidence=("아마 될 거예요",)))
# r.passed == False (ACCEPTED_EVIDENCE_RE 불일치)
# 3) 인정 증거 동반 → 통과
r = check_claim(Claim(text="완료", evidence=("exit code 0", "tests 12 pass")))
# r.passed == True
# 4) 완료어 자체가 없음 → 게이트 대상 아님 → 통과
r = check_claim(Claim(text="작업을 검토 중입니다", evidence=()))
# r.passed == True
# scan_text — early-stop.sh 포팅 (약속-미실행 + 범위-축소)
# 범위 축소 꼼수 → 즉시 위반 (증거 무관)
scan_text("핵심만 구현했고 exit 0, 나머지는 생략하겠습니다").passed # False
# 약속만 하고 실행 증거 전무 → 위반
scan_text("이제 테스트를 작성하겠습니다").passed # False
# 약속 + 실행 증거 동반 → 통과
scan_text("구현하겠습니다. 구현했고 tests passed, exit 0").passed # True
# 빈 입력 → fail-closed
scan_text("").passed # False
요약 & 셀프체크
3줄 요약:
- check_claim은 완료어가 있는데 인정 증거(ACCEPTED_EVIDENCE_RE)가 없으면 passed=False로 막는다.
- scan_text는 범위-축소 패턴은 즉시 위반, 약속 패턴은 완료-증거가 전혀 없을 때만 위반으로 처리한다.
- 전 구간이 fail-closed: 불확실하거나 입력이 비면 통과가 아니라 실패다.
질문:
check_claim(Claim(text="완료", evidence=("나중에 확인"))처럼 증거가 비어있지 않지만 인정 패턴에 안 맞으면 결과는 무엇이고 그 이유 문자열은 어떤 것인가?- scan_text에서 “구현하겠습니다”가 들어 있어도 통과될 수 있는 조건은 무엇인가? 어떤 패턴 묶음이 그 통과를 만드는가?
- 범위-축소 패턴과 약속-미실행 패턴이 위반을 판정하는 방식의 결정적 차이는 무엇인가(증거 고려 여부 관점)?