Skip to content

feat: advisor 패턴을 벤더 중립으로 구현하고 잰다 - #50

Merged
ictechgy merged 51 commits into
mainfrom
feat/advisor-pattern-measurement
Aug 18, 2026
Merged

feat: advisor 패턴을 벤더 중립으로 구현하고 잰다#50
ictechgy merged 51 commits into
mainfrom
feat/advisor-pattern-measurement

Conversation

@ictechgy

Copy link
Copy Markdown
Owner

Anthropic 의 Advisor 도구를 통합하는 것이 아니라 그 방식을 로컬로 재구현한다.
구독이든 API 키든, Claude 든 Codex 든 쓸 수 있고, anthropic-beta 헤더를 아예
지원하지 않는 Amazon Bedrock 에서도 돈다. 원본이 구조적으로 못 하는 조합인
Codex executor + Claude advisor 도 된다.

무엇이 남고 무엇이 사라지나

원본은 생성 도중 끼어들지만 CLI 는 그럴 수 없다. 조언은 호출과 호출 사이
에 들어가고, 조언자는 executor 의 사고 과정이 아니라 산출물(diff, 검증 출력)을
본다. 원본의 하드 max_tokens 상한도 CLI 에는 없어, 조언 비용 a 가 작다는
보장이 없다 — 그래서 a 는 가정하지 않고 잰다.

두 형태 (별도 플래그)

플래그 흐름 기대비용 이득 조건
--advise-first 계획 → 싼 실행 → 검증 a + c + p′ a < p − p′
--advise-on-failure 싼 실행 → 실패 → 조언 → 재시도 → 승급 c + p·(a + c + (1−s)) s > a + c

B 의 조건에는 p 가 없다. 실패한 건들만 모으면 판정이 되므로 사전 등록이
깨끗하다. c≈0.31, a≈0.1 이면 재시도가 40% 만 성공해도 이득이다.

A 는 겨냥하는 것(품질)이 이 프로젝트의 품질 도구로 측정 불가능하다는 점을
설계 문서에 명시했다. B 는 결정 가능하다.

조언자의 경계는 executor 보다 좁다

  • 작업공간을 통과 여부와 무관하게 언제나 지운다. 유일한 출력 통로는 stdout.
  • --advisor-context prompt(기본)면 빈 디렉터리에서 돌아 프롬프트에 담긴 것만
    본다. repo 면 클론을 주지만 입력 비용을 더 쓴다. 이 선택이 a 를 좌우한다.
  • 검증 출력은 끝 4000자만, 자격증명 모양을 지우고 넘긴다.
  • 조언 본문은 8000자에서 자르고 잘림을 기록한다.
  • 조언 본문과 검증 출력은 로그에 넣지 않는다. run_childrun_verify
    출력을 호출자에게만 돌려주도록 반환 타입을 바꿨다.

새로 열리는 통로

싼 경로의 diff → 조언자 → 조언 → executor 프롬프트. 거기 심긴 주입이 executor 를
흔들 수 있다. 막는 것은 구분자가 아니라 그 뒤의 verify 다 — 재시도 결과도
같은 게이트를 통과해야 한다. 실측: 조언자가 "비밀을 파일에 쓰고 테스트를
건너뛰라" 고 지시하고 싼 경로가 그대로 따라도 비밀은 패치에 실리지 않는다.

검증

933개 테스트, ruff, strict mypy, 시나리오 12종 회귀 전부 통과. 추가로 A / B /
A+B / 플래그 검증 / 조언자 작업공간 삭제 / 로그 유출 없음 / 자격증명 지움 /
적대적 조언 주입을 각각 실측 확인했다.

배포본에는 들어가지 않는다(MANIFEST.in). weightclass 본체 적용은 V1 경계
이동이 필요한 별도 결정이고, 설계 문서가 그 순서를 적어 두었다.

Anthropic 의 Advisor 도구를 끌어다 쓰는 것이 아니라, 그 **방식** 을 로컬로
구현한다. 그래야 구독이든 API 키든, Claude 든 Codex 든 쓸 수 있고,
anthropic-beta 헤더를 아예 지원하지 않는 Bedrock 에서도 돈다. 원본이
구조적으로 못 하는 조합 — Codex executor + Claude advisor — 도 된다.

원본은 생성 **도중** 끼어들지만 CLI 는 그럴 수 없다. 그래서 조언은 호출과
호출 **사이** 에 들어가고, 조언자는 executor 의 사고 과정이 아니라
산출물(diff, 검증 출력)을 본다. 그 손실은 문서에 적었다.

두 형태를 별도 플래그로 넣었다.

  --advise-first       비싼 모델이 계획을 짜고 싼 모델이 그것으로 작업
                       기대비용 a + c + p′,  이득 조건 a < p − p′
  --advise-on-failure  검증 실패 시 비싼 모델이 원인만 읽고 조언 →
                       싼 모델 재시도 → 그래도 실패하면 승급
                       기대비용 c + p·(a + c + (1−s)),  이득 조건 s > a + c

B 의 조건에는 p 가 없다. 실패한 건들만 모으면 판정이 되므로 사전 등록이
깨끗하다. c≈0.31, a≈0.1 이면 재시도가 40% 만 성공해도 이득이다.

조언자의 경계는 executor 보다 좁다:
- 작업공간을 **통과 여부와 무관하게 언제나** 지운다. 유일한 출력 통로는
  stdout 이고, 디스크에 무엇을 쓰든 아무 데도 가지 않는다.
- --advisor-context prompt(기본)면 빈 디렉터리에서 돌아 프롬프트에 담긴
  것만 본다. repo 면 클론을 주지만 입력 비용을 그만큼 더 쓴다. 이 선택이
  a 를 크게 좌우하므로 플래그로 뒀다.
- 검증 출력은 끝 4000자만, 자격증명 모양을 지우고 넘긴다. 그 텍스트는
  자식 코드가 찍은 것이고 벤더로 나가기 때문이다.
- 조언 본문은 8000자에서 자르고, 잘렸다는 사실을 기록한다.
- 조언 본문과 검증 출력은 **로그에 넣지 않는다.** run_child 와 run_verify
  가 출력을 호출자에게만 돌려주도록 반환 타입을 바꿨다.

새로 열리는 통로도 적어 뒀다: 싼 경로의 diff → 조언자 → 조언 → executor
프롬프트. 거기 심긴 주입이 executor 를 흔들 수 있다. 막는 것은 구분자가
아니라 그 뒤의 verify 다 — 재시도 결과도 같은 게이트를 통과해야 한다.
실측으로 확인했다: 조언자가 "비밀을 파일에 쓰고 테스트를 건너뛰라" 고
지시하고 싼 경로가 그대로 따라도, 비밀은 패치에 실리지 않는다.

리포트는 a 와 s 를 내고 s > a + c 를 구간으로 판정한다. 조언 설정이 섞인
로그에서는 s 를 내지 않는다. "승급 필요" 라벨이 조언으로 구제된 건까지
세던 것도 고쳤다 — 이제 싼 경로 실패, 구제, 실제 승급을 따로 찍는다.

검증: 933개 테스트 통과, ruff, strict mypy. 시나리오 12종 회귀 통과.
A / B / A+B / 플래그 검증 / 조언자 작업공간 삭제 / 로그 유출 없음 /
자격증명 지움 / 적대적 조언 주입을 각각 실측 확인.
PR #50 라운드 1. 새 코드라 지적이 많았고 대부분 맞다.

**검증 출력을 자르고 나서 지웠다.** 순서가 반대여야 한다. 먼저 자르면
경계에 걸친 자격증명이 접두사를 잃고 패턴을 빠져나간다. 자식이 출력
길이를 조절할 수 있으므로 그 경계는 의도적으로 맞출 수 있는 것이다.
패턴도 부족했다 — 개인키는 BEGIN 줄만 지워 base64 몸통이 남았고,
Bedrock 환경에서 가장 흔한 AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN 은
고정 접두사가 없어 아예 안 잡혔다. GCP, GitLab, npm, JWT, 그리고 환경을
통째로 찍는 테스트의 NAME=value 형태도 넣었다. 7종으로 실측 확인.

**조언자 작업공간 삭제가 fail-open 이었다.** rmtree(ignore_errors=True)
로 삼키고 등록까지 지워서, 못 지웠을 때 --prune 도 그것을 모르게 됐다.
"언제나 지운다" 가 이 함수의 유일한 보증인데 조용히 어기고 있었다.
지우지 못하면 등록을 남기고 사람에게 알린다. 등록 자체도 try 밖에 있어
그 사이 실패하면 디렉터리가 추적도 정리도 안 된 채 남았다.

**조언자의 종료 상태를 보지 않았다.** 인증 실패나 쿼터 초과도 stdout 에
무언가를 쓰고, 그것이 그대로 executor 의 과제에 붙었다. executor 가 오류
메시지를 지시로 읽는 셈이다. 0 이 아닌 코드나 타임아웃이면 버린다.

**조언 하나가 과제 전체를 죽였다.** attempt 는 예외를 기록으로 바꾸는데
advise 는 위로 던져, 조언자 바이너리가 없다는 이유로 로그 줄이 통째로
사라졌다. 조언은 있으면 좋은 것이지 없으면 못 재는 것이 아니다.

**Shape B 가 diff 를 안 넘겼다.** 문서는 "diff → 조언자" 라고 적어 놓고
검증 출력만 줬다. 게다가 실패한 시도의 패치는 디스크에 쓰이지 않으므로
파일에서 읽는 방식은 언제나 비어 있었을 것이다. attempt 가 패치 바이트를
돌려주게 하고 같은 규칙으로 지우고 잘라 넘긴다.

**Shape A 가 맨 과제만 넘겼다.** 조언자가 그것을 "작업하라" 로 읽고 코드를
쓰려 든다. 조언만 하라는 지시를 붙였다.

**인프라 실패에도 조언을 요청했다.** 클론 실패나 변경 없는 종료는 검증까지
가지도 못했는데 "검증에 실패했다" 고 말하는 것은 조언자에게 거짓을 주는
것이다.

**조언자 전용 env/HOME 이 없었다.** --child-env 로만 줄 수 있어, 조언자에게
필요한 자격증명이 싼 경로와 승급 경로에도 갔다. PR #49 에서 막은 것과 같은
누출이다. --advisor-env / --advisor-home 을 추가했다.

리포트:
- 시작 전 조언(모든 과제)과 실패 후 조언(실패한 과제)의 비용을 한 평균으로
  섞었다. 두 모집단이 다르므로 단계별로 낸다.
- a 를 점추정으로만 써서 s > a + c 판정이 실제보다 확정적으로 보였다.
  표준오차를 함께 태운다.
- s 의 분모가 "재시도가 기록된 건" 이었다. 조언이 비어 재시도조차 못 한
  건은 비용은 쓰고 승급했는데 분모에서 빠져 s 가 위로 치우쳤다.
- 조언을 켜고 잰 로그에서 c + p 를 "기대 비용" 이라 찍고 절감까지 냈다.
  그 식은 조언 없는 경로의 모형이다. 표시하되 판정하지 않는다.
- 문서의 산술 오류: c≈0.31, a≈0.1 이면 손익분기는 s > 0.41 이다. 40% 가
  이득이라고 적었는데 틀렸다.

오탐으로 확인해 고치지 않은 것: run_verify 의 Popen 에 PIPE 가 없다는
지적. 그 줄들은 diff 의 문맥이고 원래부터 있었다.

검증: 933개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과. 조언자
실패/누락/diff 전달/env 격리/작업공간 잔여 0/적대적 주입을 실측 확인.
PR #50 라운드 2.

**JSON 형태의 자격증명이 통째로 빠져나갔다.** 이름과 구분자 사이에 닫는
따옴표가 오는 형태 — "SecretAccessKey": "..." — 를 패턴이 놓쳤다. AWS CLI
와 boto 가 찍는 것이 정확히 그 형태이고, Bedrock 을 쓰는 곳에서 가장 흔한
자격증명이다. 실측으로 aws sts 출력이 그대로 조언자에게 갔다.

**PEM 을 언급만 해도 뒤가 통째로 사라졌다.** END 가 없을 때 끝까지 지우게
해 놓아서, "expected -----BEGIN PRIVATE KEY----- but got EOF" 라는 오류
한 줄이 그 뒤 출력 전체를 없앴다. 그것이야말로 조언자가 봐야 할 실패
신호다. END 가 없으면 base64 로 이어지는 만큼만 지운다.

**과잉 삭제.** 값의 모양을 안 보고 8자 이상이면 지워서,
API_KEY_HEADER = "X-Api-Key" 나 TOKEN_RE = re.compile(...) 같은 평범한
소스 줄까지 지웠다. pytest 는 실패한 소스를 함께 보여주므로 조언자가
진단할 코드를 잃는다. 자격증명처럼 생긴 문자만, 12자 이상일 때만 지운다.
진짜 비밀 6종은 지우고 소스 줄 6종은 살리는 것을 실측 확인.

**정규식을 무한정 긴 문자열에 돌렸다.** 그 텍스트는 자식이 길이를 정할 수
있다. 발췌보다 훨씬 넉넉한 창(64000자)으로 먼저 줄인 뒤 지운다 — 그 창
안에서 잘린 것은 최종 발췌에 들어가지도 않으므로, 라운드 1 이 고친 "자르고
지우면 경계에 걸친 것이 샌다" 는 문제는 다시 생기지 않는다. 200만 자에서
0.07초.

**손익분기가 잘못된 기준을 썼다.** 조언 후 재시도는 프롬프트에 조언이 붙어
최초 싼 실행보다 비싸다. 조건은 s > a + c 가 아니라 s > a + r 이다. 실측
합성 로그에서 손익분기가 0.375 에서 0.525 로 올라가고, 판정이 "실패 후
조언이 승급보다 싸다" 에서 "아직 결론 낼 수 없다" 로 뒤집힌다 — c 를 쓰던
것이 조언 경로를 실제보다 유리하게 보이게 하고 있었다.

**a 의 관측이 하나면 흔들림을 0 으로 뒀다.** 한 번 봤다는 사실을 "정확히
안다" 로 바꾸는 것이다. 그 값 자체를 폭으로 삼아 판정이 서지 않게 한다.

**조언 시도 자체가 실패한 것이 s 분모에서 사라졌다.** 라운드 1 에서 예외를
None 으로 바꾼 것이 "시도 안 함" 과 구별되지 않게 만들었다. 시도했고
실패했다는 사실을 기록으로 남긴다.

**삭제 실패 판정에 exists() 를 썼다.** 그 함수는 권한 오류에서도 False 를
주므로, 접근할 수 없게 만들어진 디렉터리가 "사라졌다" 로 잡혔다. 지우기가
실패했으면 남은 것으로 본다.

검증: 933개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과. JSON 형태,
PEM 과잉, 과잉 삭제, 백트래킹, r 기준 판정을 각각 실측 확인.
PR #50 라운드 3. 세 트랙이 같은 곳을 가리켰고, 확인해 보니 지적보다
나빴다.

**"창 안이면 안전하다" 는 내 주석이 거짓이었다.** 라운드 2 에서 64000자
창으로 먼저 줄인 뒤 지우게 하면서 "그 창 안에서 잘린 것은 최종 발췌에
들어가지 않는다" 고 적었다. 리댁션이 텍스트를 **줄인다** 는 것을 빠뜨렸다 —
PEM 하나가 6만 자를 열 자로 만들면 창 앞머리가 발췌 안으로 밀려든다.

실측해 보니 더 나빴다. 창이 키 본문 한가운데서 시작하면 BEGIN 마커가 창
밖이라 패턴이 **아예 걸리지 않고**, 개인키 본문이 통째로 조언자에게
전송됐다. 창을 줄 경계에 맞추고, 짝 없는 END 마커가 보이면 창 시작부터
거기까지를 지운다. 실측: 7만 자 앞선 텍스트 + 6만 자 PEM 에서 본문은
사라지고 실패 신호는 남는다.

**종결 PEM 분기가 DOTALL 로 수만 자를 삼켰다.** BEGIN 을 이름만 언급한
오류 줄과 그 뒤 어딘가의 진짜 키 사이 전부가 [REDACTED] 가 됐다. 본문은
base64 와 공백뿐이므로 그 문자만 허용한다.

**값의 첫 12자에 낯선 문자 하나면 빠져나갔다.** 이름이 비밀을 뜻하면 값의
모양을 따지지 않는다 — 이름이 신호다. AWS 계열은 줄 끝까지, 일반 이름은
코드처럼 보이는 문자(괄호, 쉼표 등)만 배제한다. 소스 줄 5종은 여전히
살아남는 것을 확인했다.

**이름 없이 값만 찍힌 자격증명은 모양으로 못 잡는다.** AWS 비밀 액세스
키는 고정 접두사가 없고, 40자 base64 는 해시나 테스트 데이터와 구별되지
않는다. 대신 **아는 것** 을 정확히 일치로 지운다 — 이 머신의 환경에 있는
자격증명 값들. 자식이 우리 환경을 읽어 찍었다면 그 값은 거기 있다. 그
목록은 출력하거나 기록하지 않고 대조에만 쓴다.

**조언 본문도 같은 검사를 거친다.** --advisor-context repo 면 조언자가
저장소를 읽고 본 것을 되뱉을 수 있다.

**구조화 출력과 조언 본문이 충돌했다.** a 를 재려면 조언자도
--output-format json 으로 불러야 하는데, 그러면 stdout 이 JSON 봉투라
그것을 그대로 과제에 붙이면 executor 가 조언 대신 계측 데이터를 읽는다.
봉투에서 본문만 꺼낸다.

리포트:
- r 을 점추정으로 두어 판정이 확정적으로 보였다. a 와 같이 흔들림을 태운다.
- 시작 전 조언까지 켜져 있으면 그 성공률은 s 가 아니라 s′ 다. 라벨을
  바꾸고 두 설정의 수를 섞지 말라고 알린다.
- 재시도조차 못 한 실패에 r 을 물리는 것이 보수적인 쪽으로 틀린다는 것을
  명시한다.
- 문서의 s > a + c 와 A+B 모델식에 남은 c 를 r 로 고쳤다.

검증: 933개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과. 창 경계
PEM, PEM 과잉 삭제, 값 우회 4종, 소스 줄 5종 유지, 백트래킹을 실측 확인.
PR #50 라운드 4. 세 트랙이 같은 회귀를 잡았다 — 라운드 3 에서 내가 만든
것이다.

**PEM 본문을 base64 로 좁힌 것이 회귀였다.** BEGIN 과 END 사이에 base64 가
아닌 문자가 하나라도 있으면 분기가 통째로 실패한다. RFC 1421 암호화 키의
Proc-Type/DEK-Info 헤더, JSON 에 escape 된 GCP 서비스 계정 키가 여기
해당하고, 그러면 키가 아예 안 걸려 통째로 나간다. 반대로 임의 문자를
허용하면 길이를 제한해도 비매치 입력에서 이차 시간이 된다.

정규식으로는 두 조건을 동시에 만족시킬 수 없어 인덱스 주사로 옮겼다.
선형이고 내용을 가리지 않는다. 더해서 "이름만 언급한 줄" 과 "실제 키
블록" 을 가른다 — 마커 뒤 200자에 PEM 헤더나 12자 이상 base64 연속이
없으면 언급으로 보고 지나간다. 그러지 않으면 오류 줄의 BEGIN 과 뒤쪽
진짜 키가 한 구간으로 묶여 사이의 실패 신호가 사라진다.

실측: 평문/암호화/JSON escape/END 없음 4종을 모두 지우고, CERTIFICATE 는
건드리지 않고, 언급 뒤 60줄의 실패 신호는 남는다. 6만 자에서 0.08초.

**조언을 자르고 나서 지웠다.** 검증 출력에서 라운드 1 이 고친 것과 같은
실수를 조언 경로에서 반복했다. 8000자 경계에 걸린 값이 앵커를 잃는다.
지우고 자른다.

**조언에 verify_excerpt 를 재사용해 4000자 절단이 함께 걸렸다.** 그것은
**뒤** 4000자를 남기므로 조언의 앞머리 — 결론과 계획이 오는 자리 — 가
버려진다. 자르지 않고 지우기만 하는 redact_text 를 분리했다.

**advice_text 가 Claude 봉투만 알았다.** Codex 는 JSONL 이고 본문이
item.text 처럼 중첩돼 있어, 그대로면 executor 가 조언 대신 계측 데이터를
읽는다. 중첩을 훑는다.

**줄바꿈 없는 창을 통째로 버렸다.** 압축된 한 줄 출력이면 조언자가
아무것도 못 본다.

리포트:
- 재시도조차 못 한 실패에 r 을 물렸다. 조건은 s > a + q·r 이고 q 는 재시도
  시도율이다. 실측에서 "손익분기 s = a + 0.86·r" 로 표시된다.
- a 와 r 의 구간이 분모(승급 과제의 비싼 평균)의 불확실성을 빠뜨렸다.
  c 의 구간이 그것을 이미 재 놓았으므로 그 비율만큼 넓힌다.
- c + p 가 모형이 아니라고 말해 놓고 그 식의 절감률을 계속 찍었다. 조언이
  켜진 로그에서는 참고값만 내고 절감은 내지 않는다.

검증: 933개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 5. 두 트랙이 내 새 주사에서 CRITICAL 을 올렸고 둘 다 맞다.

**END 를 앞으로 무한정 찾았다.** "-----END OF REPORT-----" 같은 배너나 다른
블록의 END 가 걸리면 그 사이가 통째로 사라진다. 실측: BEGIN 다음에 배너가
오고 30줄의 실패 신호 뒤에 진짜 키가 있으면, 그 30줄이 전부 지워졌다.
본문의 끝을 먼저 정하고 그 부근(200자)까지만 END 를 찾는다.

**END 없는 암호화 키의 본문이 그대로 남았다.** 폴백이 허용 문자로 훑는
방식이라 `Proc-Type` 의 하이픈에서 5자 만에 멈췄다. 마커만 지우고 키는
남기는, 가장 나쁜 결과다. 줄 단위로 본다 — PEM 헤더 줄이거나 base64 로
보이는 줄이면 키의 일부다.

**base64 판정이 헐거워 평범한 영어 줄을 삼켰다.** "FAILED next" 도 전부
alnum 이다. 공백이 있으면 본문이 아니고, 진짜 본문 줄은 대개 64자다.
짧은 줄은 키의 마지막 줄일 수 있으므로 한 줄 더 받고 멈춘다.

마커는 이제 완전한 형태(`-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----`)로 잡는다.
접두사만 보던 방식은 한 물리적 줄에 인증서와 개인키가 함께 있는 JSON
출력에서 인증서의 BEGIN 을 키의 시작으로 착각했다.

실측 8종 전부 통과: 평문/암호화/END 없는 암호화/JSON escape GCP/배너 끼임/
언급만/CERTIFICATE 유지/여러 줄 키. 6만 자에서 0.08초.

리포트:
- r 을 못 쟀을 때 c 로 대신하고 판정까지 냈다. 조언이 붙은 재시도는 c 보다
  비싸므로 그 대입은 언제나 조언 쪽에 유리하다. r 이 없으면 손익분기 줄과
  판정을 아예 내지 않는다.
- q(재시도 시도율) 자체의 표본 오차를 빠뜨렸다. Wilson 구간을 r 에 태운다.
- a 와 r 의 분산이 0 이면 구간이 점이 됐다. 분모 불확실성 비율로 최소 폭을
  준다.
- 모형 문구와 안내 문구에 남은 r 을 q·r 로 고쳤다.

검증: 933개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 6. codex 가 올린 CRITICAL 이 맞았고, 내 라운드 5 테스트가
그것을 놓친 이유까지 분명하다 — 테스트의 JSON 키가 짧아서 END 마커가
탐색 상한 200자 안에 들어왔다.

**실제 크기의 GCP 서비스 계정 키는 통째로 나갔다.** JSON 에 직렬화된 키는
물리적 줄바꿈이 없고 `\n` 두 글자가 구분자다. _key_body_end 가 그것을 줄로
보지 않아 키 전체를 한 줄로 취급했고, 그 안의 END 마커에 있는 공백 때문에
"본문이 아니다" 로 판정해 즉시 멈췄다. 그러면 END 탐색 상한도 body_at+200
이라 1.7KB 짜리 키의 END 를 못 찾고, 마커만 지운 뒤 본문 25줄이 전부
남았다. 실측으로 재현하고 고쳤다 — 이제 0/25 이고 뒤의 client_email 필드는
남는다.

리포트:
- c 의 구간을 분모(승급 과제의 비싼 평균) 불확실성의 대용으로 썼다. 둘은
  같지 않다 — 싼 비용과 비싼 비용이 같은 비율로 움직이면 c 의 구간은
  좁은데 분모는 여전히 흔들린다. 분모의 표준오차를 직접 낸다.
- 비용을 얻지 못한 조언/재시도가 a 와 r 의 표본에서만 빠졌다. s 와 q 는
  전부를 세므로 같은 모집단이 아니다. 몇 건이 그런지 알린다.

리댁션 8종 회귀 유지: 평문/암호화/END 없는 암호화/JSON escape(실제 크기)/
배너 끼임/언급만/CERTIFICATE 유지/여러 줄 키.

검증: 933개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
리뷰 6라운드 동안 리댁션에서 여섯 번 연속 유출이 나왔다. 그중 한 번은
직전 라운드에 내가 "확인했다" 고 보고한 항목이었다 — 테스트에 쓴 JSON
키가 짧아서 END 마커가 우연히 탐색 범위 안에 들어왔던 것이다. 실제
자격증명은 짧지 않다. 임시 셸 검사로는 이 부류를 못 잡는다.

18개 테스트를 저장소에 남긴다. 픽스처는 실제 크기다 — 1.2KB 본문의 PEM,
RFC 1421 암호화 키, JSON 에 직렬화된 GCP 서비스 계정 키.

검사는 양방향이다. 리댁션은 두 방향으로 다 틀릴 수 있고, 한쪽만 보면
반대쪽으로 넘어간 것을 통과시킨다 — 실제로 라운드 4 와 5 에서 그렇게
넘어갔다.

  지워야 하는 것: 평문/암호화/END 없는 암호화/JSON 직렬화 개인키,
    AWS 환경 변수와 JSON 양쪽 형태, 세션 토큰, 값에 낯선 문자가 섞인 경우,
    GCP 키, JWT, 환경 덤프, 창 경계에 걸친 키, 이름 없이 값만 찍힌 것.
  남아야 하는 것: 실패 신호(AssertionError, 파일:줄), 평범한 소스 줄
    (API_KEY_HEADER = 'X-Api-Key' 같은 것), 인증서, 마커를 언급만 한
    줄 뒤의 보고서 40줄, 배너가 끼어든 뒤의 보고서 30줄.

그 밖에: 조언은 지우되 자르지 않는다는 것, 봉투에서 본문을 꺼낸다는 것,
다른 플래그의 값으로 구조화 출력이 켜지지 않는다는 것, 적대적 입력에서
시간이 폭발하지 않는다는 것.

tools/ 는 배포본에 들어가지 않으므로 importlib 로 읽는다. 파일이 없으면
건너뛴다.

검증: 951개 테스트 통과(신규 18개 포함), ruff, strict mypy.
PR #50 라운드 7. 두 트랙이 같은 유출을 잡았다.

**\r\n 으로 직렬화된 개인키가 25줄 전부 나갔다.** 라운드 6 에서 `\n` 을
구분자로 인식하게 고쳤는데, 구분자를 하나씩 열거하는 방식이라 다음
형태에서 그대로 뚫렸다. `\r\n` 은 네 글자이고, 부스러기를 걷어내면 "r"
한 글자만 남아 짧은 마지막 줄로 통과했다. 실제 줄바꿈과 이스케이프된
줄바꿈을 정규식 하나로 함께 본다 — 이중 이스케이프도 포함.

**프록시 URL 의 비밀번호가 그대로 나갔다.** 이 스크립트는 "프록시 URL 은
http://user:pass@host 형태가 흔하고 그 userinfo 는 그대로 자격증명이다"
라고 스스로 경고하면서 그 변수를 자식에게 넘긴다. 그런데 정확 일치
목록의 이름 필터는 PROXY 를 비밀로 보지 않아, 자식이 그 URL 을 찍으면
비밀번호가 조언자에게 갔다. userinfo 만 뽑아 지운다 — 프록시 주소는 실패
진단에 필요하고 비밀이 아니므로 남긴다.

리포트:
- 비용을 얻은 재시도가 절반 미만이면 경고로 끝내지 않고 판정을 보류한다.
  a 와 r 은 값이 있는 것만 보고 s 와 q 는 전부를 세므로 같은 모집단이
  아니고, 빠진 비용이 얼마였을지 모르니 방향도 모른다.
- 분자와 분모의 불확실성을 max 로 골라 덜 덮었다. 둘 다 있으므로 더한다.
- 분모의 구간이 0 을 지나면 비율의 상한이 없다. 상한을 지어내지 않고
  판정을 막는다.

테스트 4개 추가(22개): 직렬화 구분자 3종과 프록시 userinfo. 구분자를
하나씩 막다가 두 라운드 연속 뚫렸으므로 그 축을 매개변수로 고정한다.

오탐으로 확인해 고치지 않은 것: run_verify 의 Popen 에 PIPE 가 없다는
지적. 그 줄들은 diff 의 문맥이고 원래부터 있었다(라운드 1 에서도 같은
지적이 나왔다).

검증: 955개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 8. 세 트랙이 같은 뿌리를 가리켰다.

**근본 원인은 fail-open 이었다.** _key_body_end 가 줄 단위로 범위를 정하지
못하면 body_at 을 그대로 돌려주고, 그러면 아무것도 지우지 않은 채 다음
반복으로 넘어가 키가 통째로 출력된다. 세 형태가 여기 걸렸다 — 본문과 END
가 한 물리적 줄에 있는 경우, 줄에 공백 섞인 접두사가 붙은 경우, 그리고
PEM_MAX_SPAN 이 큰 키를 중간에서 자른 경우. 마지막 것은 219줄 중 219줄이
전부 나갔다.

범위를 못 정했으면 문자 단위로라도 키처럼 보이는 만큼을 삼킨다. 여기까지
왔다는 것은 마커 뒤가 키 본문이라고 이미 판정했다는 뜻이므로, 덜 지우는
것보다 더 지우는 편이 낫다. 그리고 어떤 경우에도 최소 한 글자는 전진한다.

본문 주사의 상한도 없앴다. 상한이 있으면 큰 키(16384비트 RSA, 암호화 키)
가 중간에서 잘려 나머지가 나간다. 주사는 비키 내용에서 자연히 멈춘다.

**PGP armored 개인키를 놓쳤다.** 마커가 "PRIVATE KEY-----" 로 끝나야
한다고 가정해서 "-----BEGIN PGP PRIVATE KEY BLOCK-----" 이 안 걸렸다.

**프록시 수정이 절반만 됐다.** 스킴을 요구해서 user:pass@host 형태가
빠져나갔다 — curl, wget, pip 가 다 받아들이고 사내 설정에서 흔하다.
비밀번호 길이 하한 6도 짧은 비밀번호를 통과시켰다. 이름과 값을 붙인
형태로도 지워 하한 없이 막는다. 사용자 이름 자리에 토큰을 넣는 형태도
길면 지운다.

리포트:
- 비율 구간을 반폭을 더해 냈다. 분자와 분모가 동시에 불리한 조합을 덜
  덮는다. 끝점으로 계산한다.
- 조언 비용의 결측은 경고만 하고 판정을 막지 않았다. 재시도와 같은
  기준으로 막는다.
- 시작 전 조언이 켜졌으면 관측된 실패율은 p 가 아니라 p′ 다. 라벨을 바꾸고
  참고값도 그렇게 표시한다.

테스트 4개 추가(26개): 한 줄 키, 거대 키, PGP armored, 스킴 없는 프록시와
짧은 비밀번호.

검증: 959개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 9. 라운드 8 에서 내가 넣은 fail-open 가드가 **도달
불가능했다** — BEGIN 뒤의 줄바꿈이 빈 줄로 소비되어 위치가 언제나
전진하므로, "전진했는가" 로는 실패를 못 알아본다. "내용을 소비했는가" 를
본다.

그 결과 드러난 유출: **줄마다 접두사가 붙은 키가 통째로 나갔다.**
"[INFO] ", "stdout | ", 타임스탬프 — CI 출력은 거의 언제나 그렇다. 접두사의
공백 때문에 첫 본문 줄이 "본문 아님" 으로 판정돼 주사가 즉시 멈췄고, 그
뒤 END 탐색도 그 자리 근처만 봐서 못 찾았다. 25줄 중 25줄이 전송됐다.
마지막 공백 뒤 조각도 함께 판정한다. 접두사 4종으로 실측 확인.

**폴백이 줄을 넘어가 다음 줄 토큰을 가운데서 잘랐다.** 그러면 그 토큰의
앵커(ghp_ 같은 접두사)가 사라져 뒤의 모양 패턴이 못 잡는다. 지우려다
오히려 통과시키는 것이다. 폴백은 줄 안에서만 삼킨다.

리포트: 일부 기록만 시작 전 조언인 로그에서 전체 실패율에 p′ 라벨을
붙였다. 그 값은 p 도 p′ 도 아니다 — 두 설정이 섞였다고 경고하고 라벨은
모두가 켜진 경우에만 붙인다.

테스트 5개 추가(31개): 로그 접두사 4종과 다음 줄 토큰 보존.

검증: 964개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 10. 라운드 9 가 "폴백이 줄을 넘어가 다음 줄 토큰을 자른다"
를 잡아 줄바꿈을 뺐는데, 이번에는 **밑줄에서 잘렸다.** 집합을 고르는 방식
자체가 문제다 — 어떤 문자 집합을 골라도 그 밖의 글자에서 멈추고, 그 자리가
토큰 한가운데면 앵커(ghp_ 의 접두사)가 사라져 본체만 남는다. 지우려는
동작이 유출을 만든다.

폴백을 줄 끝까지로 바꿨다. 여기까지 온 것은 마커 뒤가 키 본문이라고 이미
판정한 자리이므로, 한 줄을 통째로 잃는 것이 토큰을 반토막 내는 것보다
낫다. 임의 크기 점프도 함께 없앴다 — 자식이 그 거리를 맞추면 앵커만
잘라 낼 수 있고, 40가지 정렬로 재현했다.

**프록시 비밀번호에 @ 가 있으면 앞부분만 잡았다.** 첫 @ 에서 끊는 방식이라
http://user:Str0ng@Pass!word@proxy:8080 에서 Str0ng 만 목록에 들어가고
Pass!word 는 그대로 나갔다. 특수문자 정책이 있는 곳에서 흔한 형태다.
마지막 @ 까지 탐욕적으로 가되 공백은 넘지 않는다.

리포트:
- 비용이 절반만 있어도 판정이 나갔다. 값 없는 재시도 하나가 임의로
  비쌌을 수 있고 그것이 결론을 뒤집는다. 돈에 대한 단정적 판정에는
  전수를 요구한다.
- 설정이 섞인 로그에서 "두 설정이 섞였다" 고 경고해 놓고 실패율에 p
  라벨을 붙였다. 어느 이름도 맞지 않으므로 이름을 붙이지 않는다.

테스트 3개 추가(35개): 같은 줄 토큰 보존, @ 포함 프록시 비밀번호,
diff 안의 개인키. 시간 단정은 15초로 완화했다 — 잡으려는 것은 파국적
백트래킹이고 그것은 분 단위로 나타난다. 빠듯하게 잡으면 부하 걸린
기계에서 실패해 신뢰만 깎는다.

이번 라운드는 Claude 트랙이 50KB 프롬프트에서 빈 출력을 냈다. 샤드를
반으로 나누니 동작했고, 그 두 조각이 위 두 건을 잡았다.

검증: 967개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 11.

**라운드 10 의 프록시 수정이 스킴 있는 URL 을 깨뜨렸다.** `(?:://|^)` 는
위치 0 의 `^` 대안이 먼저 걸려 스킴을 사용자 이름으로 잡는다
(http://alice:s3cr3t@proxy 에서 user='http'). 그러면 진짜 비밀번호가 정확
일치 목록에 없고, 자식이 값만 찍으면 그대로 나간다. 정규식 하나로 URL 을
가르려던 것이 애초에 무리였다 — 스킴을 떼고 **마지막** @ 를 경계로 삼는
코드로 옮겼다. 비밀번호에 @ 가 있는 경우도 같은 규칙으로 처리된다.

**줄마다 접두사가 붙은 암호화 키가 통째로 나갔다.** 두 겹이었다.
하나는 앞쪽 창이 헤더 줄로만 채워져 "이름만 언급" 으로 분류된 것 —
접두사 때문에 앵커된 헤더 패턴에 안 걸린다. 다른 하나는 접두사만 있는
줄("+", "[INFO] ")이 내용으로 잡혀 "짧은 마지막 줄" 규칙에서 멈춘 것이다.
접두사를 문자 단위로 벗기면 "[INFO]" 처럼 문자로 시작하는 형태를 못
벗긴다. 대괄호 태그, 로거 이름과 |, ISO 타임스탬프, diff 기호를 함께 보는
접두사 제거를 넣었다.

실측: 접두사 7종 × 평문/암호화 = 14조합 전부 0/25.

리포트: 타임아웃난 조언이나 재시도의 부분 사용량이 "전부 가격이 있다" 를
만족시켜, 판정이 부분값 위에 섰다. 타임아웃은 가격 없음으로 본다.

짧은 프록시 비밀번호는 URL 문맥이 있을 때만 지운다. 값만 단독으로 찍힌
짧은 값을 무조건 지우면, 그것이 흔한 단어일 때 보고서 전체가 지워져
조언자가 아무것도 못 본다 — 그 위험이 더 크다.

테스트 17개 추가(52개): 접두사 7종 × 키 2종, 프록시 URL 3형태.

검증: 985개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 12. 샤드를 전부 돌리게 고친 뒤 10개 트랙이 usable 이었고,
유출 네 건이 나왔다.

**창 슬라이스를 없앴다.** 라운드 1 부터 이 함수의 유출 중 여러 건이
"자르는 행위" 에서 나왔다. 자르고 지우면 경계에 걸친 값이 앵커를 잃고,
지우기 전에 창을 잡아도 리댁션이 텍스트를 줄여 창 앞머리가 발췌 안으로
밀려든다. 줄 경계 정렬과 짝 없는 END 처리는 그 부류를 하나씩 막을 뿐
없애지 못했다. 전부 지운 뒤 자르면 부류가 통째로 사라진다. 비용을 쟀다 —
200KB 0.27초, 1MB 1.37초. PEM 주사는 선형이고 패턴에 중첩 수량자가 없다.

**겹친 접두사가 통과했다.** "+[INFO] " 처럼 두 겹이면 한 번만 벗겨 남은
겹이 헤더 인식을 막고, 암호화 키가 25/25 나갔다. 더 벗겨지지 않을 때까지
반복한다.

**16자로 접힌 본문이 첫 줄에서 끊겼다.** "짧은 줄은 마지막 줄" 규칙이
모든 줄이 짧은 형태에서 첫 줄만 지우고 멈춘다 — 100줄 중 100줄이 남았다.
다음 줄도 본문처럼 보이면 계속 간다.

**프록시 URL 의 authority 를 안 잘랐다.** 경로·쿼리의 @(?notify=ops@ex.com)
가 경계로 잡혀 비밀번호에 주소와 쿼리가 붙고, 진짜 비밀번호는 목록에
없었다. 그리고 https://TOKEN@host 처럼 비밀번호 없는 형태에서 None 을
돌려주어 토큰이 그대로 나갔다.

조언 봉투에서 본문을 못 꺼내면 원문이 오고 거기서는 줄바꿈이 `\n` 두
글자라, 줄 단위로 도는 블록 리댁터가 전체를 한 줄로 본다. 리댁션 전에
실제 줄바꿈으로 되돌린다.

리포트:
- a 와 r 의 분모(승급 과제의 비싼 평균)에 타임아웃의 부분 사용량이 섞이면
  분모가 작아져 두 비율이 커진다. 위쪽 c 판정은 그래서 멈추는데 조언
  분기가 그 금지를 우회했다.
- 비용 필터가 > 0 을 요구해 0 을 보고하는 구독 실행이 "가격 없음" 으로
  잡혔고, 그러면 전수 검사가 영원히 통과하지 못한다. cost_of 와 같이 0 을
  관측값으로 받는다.

테스트 5개 추가(59개). 창 상수를 참조하던 테스트는 새 동작에 맞게 고쳤다.

리뷰 절차 자체도 두 곳 고쳤다. gh pr diff 가 빈 출력을 냈을 때 가드가
없어 네 트랙이 빈 대상을 리뷰했고 두 트랙이 APPROVE 를 냈다 — 한 트랙만
"해시가 빈 문자열의 것" 이라고 알아챘다. 이제 diff 표식을 확인하고 없으면
라운드를 시작하지 않는다. 그리고 샤드를 쪼개 놓고 첫 조각만 리뷰하고
있었다.

검증: 991개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 13.

**접두사 제거에 6회 상한을 뒀다.** 겹이 그보다 많으면 남고, 남은 겹이
헤더 인식을 막아 암호화 키가 통째로 나간다. 상한을 없앴다 — 매 번
문자열이 짧아지거나 같아지므로 고정점에 반드시 도달한다. 1/2/4/8겹으로
실측.

**이어짐 판정의 하한이 본 주사보다 높았다.** 본 주사는 짧은 줄도 본문으로
받는데 이어짐 판정만 16자를 요구해, 12자나 15자로 접힌 키가 첫 줄에서
끊기고 나머지가 통째로 남았다(134줄 중 133줄). 하한을 부르는 자리마다
다르게 준다 — 접두사 후보를 고를 때는 높게(평범한 단어를 본문으로 오인
하지 않게), 이어짐을 볼 때는 낮게. 12/15/16/24/64자 접힘으로 실측.

**봉투 정규화가 정확 일치 앵커를 부쉈다.** 구조화 출력에서 본문을 못
꺼냈을 때 `\n` 을 실제 줄바꿈으로 되돌렸는데, 값 안에 그 두 글자가 있는
비밀은 두 조각으로 갈려 어느 쪽도 목록과 맞지 않게 된다. 줄 구분자
패턴이 이스케이프된 줄바꿈을 이미 줄로 보므로 정규화 자체가 필요 없었다.

**리포트의 분모가 0 을 버렸다.** `if x.expensive` 는 0 을 결측으로
만든다. 비용 [0, 100] 이면 평균이 50 이 아니라 100 이 되어 a 와 r 이 모두
작아진다 — 조언 쪽에 유리한 방향이다. 5곳을 `is not None` 으로 고쳤다.

문서가 코드보다 뒤처져 있었다. 코드는 s > a + q·r 로 판정하는데 설계
문서는 s > a + r 이었다. 조언이 비어 재시도조차 못 한 실패에는 r 이 들지
않으므로, 모든 실패에 물리면 조언 쪽에 불리하게 틀린다.

테스트 9개 추가(68개): 접힘 너비 5종, 접두사 겹 4종.

검증: 1001개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 14. 12개 트랙이 usable 이었고, "두 경로가 같은 판정을 다르게
한다" 는 각도를 지시했더니 그 형태로 네 건이 나왔다.

- 이어짐 탐침이 물리적 줄바꿈만 봤다. 본 주사는 이스케이프된 구분자도
  본다. 직렬화된 키에서 뒤 전체가 한 줄로 잡혀 판정이 실패했다.
- 블록 게이트가 12자 연속 base64 를 요구했다. 본 주사는 더 짧은 줄도
  받는다. 8자로 접힌 키가 "이름만 언급" 으로 분류되어 통째로 남았다.
- 접두사 후보 선택이 기본 16자를 썼다. 같은 이유로 짧게 접힌 본문이
  후보에서 탈락했다.
- advice_text 가 최상위 배열 봉투를 무시했다. _first_text 는 리스트를
  지원하는데 호출부가 dict 만 통과시켰다.

**그리고 내가 라운드 13 에서 넣은 회귀.** `if x.expensive` 를 0 을 버리는
필터로 착각해 `is not None` 으로 바꿨는데, 그것은 **0 나눗셈 방어** 였다.
공허한 조건이 되어, 다른 곳의 진위값 검사가 우연히 0 을 걸러 주는 덕에만
살아 있었다. 0 을 관측값으로 받겠다는 라운드 12 결정과도 어긋난다.
비싼 비용 0 은 분모로 못 쓰므로 이유를 붙여 따로 세고, 나눗셈 앞에 방어를
복원하고, 평균이 0 이면 조용히 죽는 대신 말하고 멈춘다.

수정 도중 판정 순서 문제도 드러났다. 후보 선택이 헤더 검사보다 먼저
돌아서 "Proc-Type: 4,ENCRYPTED" 의 마지막 조각 "4,ENCRYPTED" 가 본문 줄로
뽑히고, 그러면 그 줄이 헤더로 인식되지 못한 채 짧은 줄 규칙에 걸려 본문
앞에서 멈췄다. 헤더를 먼저 본다.

테스트에도 같은 부류가 있었다. 리뷰가 지적한 대로 세 곳이 lines[0] 만
확인하고 있었고 — 그것은 라운드 13 이 찾은 실패 형태를 그대로 통과시킨다 —
한 곳은 줄 분해 방식이라 더 약했다. 전부 부분 문자열 포함으로 통일했고,
고치자마자 실제 유출 두 건이 드러났다.

테스트 24개 추가(92개): 접힘 너비 × 직렬화 구분자 12조합, 접힘 × 접두사
9조합, 봉투 3형태.

검증: 1047개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 15. 12개 트랙.

**게이트와 주사가 "어디를 보는가" 에서 갈렸다.** 게이트는 마커 뒤 고정
400자 창을 splitlines() 로 훑었는데, 주사는 이스케이프된 구분자도 보고
접두사도 벗긴다. 그래서 접두사가 길면(51겹 "+[INFO] " 이면 400자를 넘는다)
본문 첫 글자가 창 밖으로 밀려 키가 "이름만 언급" 으로 분류되고 통째로
남았다. 게이트를 줄 수 제한으로 바꾸고 주사와 같은 구분자·후보·하한을
쓰게 했다. 1/8/51/120겹으로 실측.

**폴백이 CRLF 에서 한 글자만 전진했다.** body_at+1 로 넘기면 `\r` 다음이
바로 `\n` 이라 find 가 그 자리를 돌려준다. 구분자 연속을 건너뛴다. 그리고
폴백이 한 줄만 지우고 끝나면 그 아래 정상 본문이 남으므로, 폴백 뒤부터
주사를 다시 시도한다.

**프록시 userinfo 의 한쪽이 비면 거부했다.** http://:pass@host 와
http://token:@host 는 둘 다 유효하고 실제로 쓰인다.

**아는 값이 JSON 으로 직렬화되면 정확 일치가 어긋난다.** 따옴표와
역슬래시가 바뀌기 때문이다. 직렬화된 형태도 목록에 넣는다.

**구조화 출력을 요청했는데 본문을 못 꺼내면 봉투를 그대로 붙였다.**
executor 가 조언 대신 계측 데이터를 읽고, 리댁션도 인코딩된 텍스트와
디코딩된 값을 비교하게 된다. 그때는 조언을 버린다. content 배열은 조각을
이어야 본문이 되는데 첫 조각만 쓰고 있었다.

리포트:
- **0 을 분모 평균에서까지 뺀 것이 조언 쪽에 유리한 편향이었다.** 0 은
  나눗셈의 분모로만 못 쓴다. 평균에는 들어가야 한다. 둘을 통째로 묶어
  빼면 평균이 커지고 a 와 r 이 작아진다. 관측값과 분모 자격을 분리했다.
- 승급 짝이 하나뿐이면 분모 확대를 조용히 건너뛰고 확정 판정까지 갔다.
- 같은 술어('이 시도가 타임아웃했는가')의 두 번째 구현이 있었다 — 이
  파일이 반복해서 만든 결함 부류다. 하나로 합쳤다.
- 참고값 문구가 누락분을 "조언 비용" 뿐이라고 말했다.
- 시작 전 조언만 켠 로그에 존재하지 않는 판정(s > a + q·r)을 가리켰다.

게이트 재작성이 이차 시간을 만들었고(줄바꿈 없는 텍스트에서 마커마다
끝까지 훑음: 54KB 2.5초, 135KB 15.9초), 문자 상한을 함께 두어 선형으로
되돌렸다(540KB 3.0초). 성능 테스트의 마커 반복을 5천에서 2만으로 올렸다.

테스트 12개 추가(104개).

검증: 1034개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 16. 12개 트랙.

**라운드 15 에서 성능을 위해 넣은 3072자 상한이 그 자체로 fail-open
이었다.** 400겹짜리 접두사가 본문을 상한 밖으로 밀어내면 키가 "이름만
언급" 으로 분류되어 통째로 나간다. 세 트랙이 같은 지적을 했고 실측으로
400겹에서 정확히 재현됐다. 예산 안에서 결론을 못 내면 **키로 간주** 한다.
과잉 삭제는 한 블록이지만 fail-open 은 키 하나다. 51/200/400/1000겹 실측.

**END 탐색이 폴백 재개보다 앞을 가리키면 뒤가 남았다.** 이미 키로 판정한
부분이 그대로 나간다. 뒤로만 간다.

**이어짐 탐침이 남은 텍스트를 매번 복사했다.** 접힌 본문에서는 모든 줄이
짧아 줄마다 복사가 일어나 이차 시간이 되고, 그 길이는 자식이 정한다.
오프셋으로만 다룬다.

**봉투 추출 성공 여부를 첫 글자로 되추정했다.**
`{"result":"[1] Inspect the parser"}` 처럼 정당한 조언이 `[` 로 시작하면
봉투로 오인해 버려진다. 아는 쪽(advice_text)이 직접 알려 주게 했다.

**http://TOKEN:@host 의 문맥 문자열이 실제로 나타나지 않는 형태였다.**
`TOKEN@` 만 넣었는데 그 입력에는 `TOKEN:@` 가 있다.

**advise 예외 경로의 child 누락** — 세 라운드 연속 지적된 자리다. 성공
경로 바로 아래에서 record["child"]["seconds"] 를 읽는다. 이제 넣는다.

리포트:
- 판정 게이트가 타임아웃만 막고 보통의 미가격 승급은 통과시켰다. 그것도
  분모에서 빠지므로 비싼 평균이 승급 전체를 대표하지 않는다.
- advisor_on 분기가 타임아웃 경고를 가렸다. 빠진 비용이 위쪽으로 열려
  있다는 사실은 조언 판정에도 그대로 해당한다.

설계 문서:
- **Shape A 가 조언 붙은 싼 실행에 c 를 썼다.** 그 실행은 프롬프트에
  계획을 담으므로 c 가 아니라 c_A 다 — Shape B 의 재시도에 r 을 준 것과
  같은 이유다. 이득 조건은 a < p − p′ 가 아니라 a + (c_A − c) < p − p′ 다.
  c 를 쓰면 arm 에 유리한 방향으로 틀린다.
- A+B 를 측정 구성 2 로 돌리면서 동시에 "아무도 안 돌린 것" 으로 적어
  모델이라고 했다. A+B 는 측정된다. 모델로 낼 것은 Shape A 단독뿐이다.
- A+B 식의 재시도 항들을 프라임으로 바꿨다. 계획을 받은 뒤의 재시도 비용,
  시도율, 구제율은 Shape B 단독의 같은 이름과 다른 양이다.

테스트 7개 추가(111개).

검증: 1041개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
PR #50 라운드 17. 12개 트랙.

**리포트가 Shape A 를 여전히 a + c + p′ 로 모델링했다.** 네 트랙이 같은
지적을 했다. 라운드 16 에서 설계 문서만 고치고 코드는 안 고쳤다 — 문서와
구현이 갈리면 구현이 이긴다. c=0.30, c_A=0.36, a=0.10, p=0.30, p′=0.17 이면
안내대로는 이득으로 나오지만 실제로는 손해다. 이득 조건은
a + (c_A − c) < p − p′ 이고, 이 로그의 c 가 이미 c_A 라는 것도 명시했다.

**빈 줄 여섯 개로 게이트를 빠져나갈 수 있었다.** 라운드 16 이 문자 상한을
fail-closed 로 뒤집었는데 줄 수 예산에는 같은 구멍이 남아, 빈 줄이 예산을
먹고 끝나면 False 로 떨어져 키가 통째로 남았다. 빈 줄은 예산 밖에서 세고,
그것도 과하면 키로 간주한다. 0/6/20/100 실측.

**이어짐 탐침이 본 주사와 다른 정규화를 했다.** 본 주사는 따옴표를 벗기는데
탐침은 안 벗겨, 따옴표로 감싼 16자 본문에서 100줄 중 100줄이 남았다.

**content 조각을 줄바꿈으로 이어 앵커를 갈랐다.** "-----BEGIN PRI" 와
"VATE KEY-----" 가 조각 경계에 걸리면 그 사이 줄바꿈 때문에 마커로 안
잡힌다. 그대로 잇는다. 그리고 메시지 스트림과 content 배열을 모양으로
구별한다 — 전자는 마지막이 결과이고 후자는 이어야 본문이다.

기타:
- zero_expensive 안내가 코드와 반대로 말했다. 0 은 과제별 비율에서만
  빠지고 평균에는 들어간다.
- "실제 승급" 집계가 비용 루프와 다른 술어를 인라인으로 썼다. 공유 함수로
  합쳤다.
- advisor+타임아웃 분기가 advise_on_failure 여부를 안 봐서, 시작 전 조언만
  켠 로그에 존재하지 않는 판정을 가리켰다.
- 주석 세 곳이 판정식을 s > a + c 라고 적고 있었다.

설계 문서:
- s 를 "재시도가 통과할 확률"(조건부)로 정의해 놓고 식에서는 무조건으로
  썼다. 조언이 비어 재시도조차 못 한 실패도 승급하므로 s 의 분모에 남는다.
- q < 1 의 방향을 틀리게 적었다. 문턱은 낮아지지만 s 의 상한도 q 다.
- A+B 의 실패 단계 조언 비용에 unprimed a 를 썼다. 그 호출은 이미 계획이
  담긴 과제를 입력으로 받으므로 a′ 다.
- s > a + q·r 이 A+B 전체의 판정인 것처럼 읽혔다. 그것은 "계획이 있는
  상태에서 실패 단계 조언을 더할 값이 있는가" 다.

테스트 7개 추가(118개).

검증: 1048개 테스트, ruff, strict mypy. 시나리오 12종 회귀 통과.
조각을 줄바꿈으로 이으면 경계에 걸친 마커가 갈라져 리댁션이 못 잡는다.
구분자 없이 잇기로 했으므로 그 결정을 검사하는 쪽도 바뀌어야 한다.

직전 커밋에 실패하는 테스트가 섞여 들어갔다 — 회귀 시나리오만 보고
전체 스위트 결과를 확인하지 않았다.
조각마다 strip 한 뒤 이으면 사이의 공백이 사라져 단어가 붙는다
("first " + "second" -> "firstsecond"). 앵커를 지키려고 구분자를 없앤
것이지 공백을 없애려던 것이 아니다. 텍스트 블록은 원문을 그대로 잇고
전체에서 한 번만 다듬는다.

검증: 1048개 테스트 통과, ruff, strict mypy.
라운드 18 이 찾은 것들:

- 검증 출력과 diff 를 각각 지운 뒤 이어 붙이면 그 이음매가 리댁션의
  사각지대였다. 앞쪽이 `-----BEGIN PRI` 로 끝나고 뒤쪽이 나머지로 시작하면
  따로 볼 때 둘 다 무해하지만 이어 붙이면 온전한 키다. untrusted_block 이
  **먼저 이어 붙인 뒤 한 번에** 지운다. 구분자도 넣지 않는다.
- 같은 이유로 stdout 과 stderr 사이에 넣던 줄바꿈을 없앤다. 자식이 마커를
  두 스트림에 갈라 쓰면 그 줄바꿈이 회피로가 됐다.
- URL 의 경계 해석 둘이 서로를 깬다. authority 를 먼저 자르면 userinfo 안의
  `/` 가 `@` 를 지우고, `@` 를 먼저 찾으면 쿼리의 `@` 가 경계가 된다. 고르지
  않고 **둘 다** 후보로 낸다. 퍼센트 복호 형태와 이중 이스케이프 형태도 함께
  등록하고, ftp_proxy 를 인식 목록에 넣는다.
- `json.dumps(pem.splitlines())` 가 마커 뒤에 남기는 `",` 가 게이트를 닫아
  키가 통째로 남았다. 직렬화 잔해 **모양** 의 짧은 줄만 넘긴다 — 산문을
  넘기면 마커를 언급한 보고서가 통째로 지워진다.
- 봉투 판정이 `all` 이라 텍스트와 tool_use 가 섞인 배열이 스트림으로 분류돼
  파일 본문이 조언 자리에 들어갔다. 하나라도 텍스트 블록이면 조각으로 보고,
  output_text 계열 타입 이름도 받는다.

보고서 쪽:

- 라운드 17 에서 붙인 failure_advice_on 조건이 시작 전 조언만 켠 로그의
  타임아웃 경고를 통째로 건너뛰게 했다. 불완전한 c 를 c_A 라고 소개하던
  경로다. 조건에서 뗀다.
- zero_expensive 는 분모에 들어가는데 판정 게이트에서는 빠지는 것처럼
  걸려 있었다. 게이트는 unpriced_escalations 만 본다.
- estimate_c 는 0 을 빼고 점추정은 0 을 넣어, 구간이 점추정과 다른 값을
  중심으로 잡혔다. 점추정도 estimate_c 를 통과시켜 규칙을 한 곳에 둔다.
- A+B 로그의 한계 판정을 s′ > a′ + q′·r′ 로 표기한다.

설계 문서: 기호 `a` 하나가 입력이 다른 두 조언 호출을 가리켰다. a_A / a_B
로 가르고, A+B 의 자식 수를 일곱으로 고치고, Shape A 를 "모델" 이라 부르는
근거를 실제 이유(p′ 를 옮겨 쓸 수 없다)로 바꾼다.

회귀 테스트 13건 추가.
라운드 19. 이번에는 유출뿐 아니라 **과잉 삭제** 가 여러 건 나왔다. 조언자가
봐야 할 소스 줄을 지우면 이 기능의 존재 이유가 사라지므로 같은 무게로 다룬다.

- 이름이 비밀을 뜻하면 값의 모양을 안 따지던 규칙이 소스 줄을 삼켰다.
  `AWS_SESSION_TOKEN: Optional[str] = None` 과
  `AWS_SECRET_ACCESS_KEY = credentials.secret_key` 가 통째로 지워졌다. AWS
  분기는 따옴표 안의 값이거나 자격증명처럼 생긴 값(숫자와 대문자, 스무 자
  이상)만 지운다. 일반 분기는 값에서 점을 뺀다 — 점이 든 값은 거의 언제나
  코드이고, 자격증명 쪽에서 점이 나오는 JWT 와 PEM 은 전용 분기가 따로 있다.
- untrusted_block 이 앞쪽을 자르는데 검증 출력을 먼저 넘기고 있었다. 8000자
  넘는 패치 하나가 실패 신호를 통째로 밀어냈다 — 직전 커밋이 만든 것이고,
  수정의 실패 방향이 그 수정을 부른 사례와 달라진 전형이다. diff 를 먼저,
  검증 출력을 마지막에 넘긴다.
- 스트림을 잇는 순서를 고정하면 한 방향만 막힌다. stdout 을 먼저 두면
  `KEY=<값>` + `AWS_SECRET_ACCESS_` 순서가 되어 이름이 값 뒤로 간다.
  join_streams 가 두 순서를 모두 보고, 역순에서만 잡히는 것이 있으면 경계
  양쪽 4096자를 버린다.
- 네 자씩 재접힌 base64 본문은 어느 한 줄도 본문으로 안 보였다. 줄을
  가로질러 누적해 48자가 이어지면 본문으로 본다. 산문은 공백과 문장부호
  때문에 걸리지 않는다.

보고서:

- a, r, q, s 의 모집단이 usable 이라 싼 비용을 못 얻어 c 의 모집단에서 빠진
  레코드가 분자에만 남았다. 세 트랙이 함께 지적한 것으로, 분자와 분모가
  다른 표본이 되는 부류다. costed_records 로 통일한다.
- 얇은 분모 경고가 판정 사다리 안에 있어 elif advisor_on 이 먼저 흡수했다.
  조언 로그를 보는 사람만 유보 문구 없이 얇은 구간을 근거로 판단하게 된다.
  사다리 밖으로 뺀다 — 그것은 판정이 아니라 이미 출력된 c 에 붙는 단서다.
- Shape A 손익식의 측정 출처가 틀렸다. a_A, c_A, p′ 는 이 로그가 이미 주고,
  조언을 끄고 다시 재야 하는 것은 c 와 p 두 항뿐이다.

설계 문서:

- 라운드 18 에 내가 쓴 "Shape A 는 모델" 근거가 틀렸다. Shape B 는 검증
  **뒤** 에 도므로 이미 관측된 실패율 p′ 를 바꿀 수 없다. Shape A 는 측정된다.
- q < 1 이 판정을 쉽게 만들 수 없다는 단정은 재시도를 선별하지 않을 때만
  참이다. 선별하면 q·r 이 s 보다 빨리 줄어 통과할 수 있다.

회귀 테스트 20건 추가.
라운드 20. 직전 커밋이 만든 것이 셋이고, 그중 둘은 네 트랙이 함께 지적했다.

- join_streams 가 버리는 창의 방향이 정확히 반대였다. 역순 이음매를 이루는
  조각은 err 의 꼬리와 out 의 머리인데, 버린 것은 out 의 꼬리와 err 의
  머리였다 — 검출을 유발한 두 조각이 고스란히 살아남았다.
- 그 검출을 길이 비교로 했다. 두 순서가 서로 다른 자격증명을 같은 길이만큼
  지우면 길이는 같은데 잡은 것은 다르다. "따로 지운 것과 이어서 지운 것이
  다른가" 를 직접 묻는다.
- 스트림이 창보다 짧으면 통째로 버린다. 그 경우 값은 한 스트림 안에 통째로
  들어 있고(갈라진 것은 이름 쪽이다) 순서를 바꿔도 남으므로, 남길 방법이 없다.
- 일반 분기에서 점을 뺀 것이 반대 방향으로 넘어갔다.
  `PASSWORD=correct.horse.battery` 와 SendGrid 키(`SG.<22자>.<43자>`)가
  통째로 나갔다. 점이 아니라 **구분자 주변의 공백** 으로 가른다 — 사람이 쓴
  코드는 `NAME = value` 처럼 띄우고 환경 덤프는 붙인다.
- 파생 표현에 길이 하한이 없었다. 퍼센트 복호는 길이를 줄이므로
  `%2F%2F%2F%2F` 가 하한을 통과한 뒤 `////` 로 목록에 들어가 경로마다
  [REDACTED] 를 박았다. 복호 후 직렬화 형태도 함께 등록한다.
- Shape A 가 빈 조언을 검사하지 않았다. Shape B 는 `if text:` 로 막는데
  쌍둥이 호출부만 안 막아, 계획이 붙었다고 기록됐지만 실제로는 안 붙은 로그가
  남을 수 있었다. 그 로그의 p′ 는 계획 없는 실행의 실패율이다.

보고서:

- costed_records 로 모집단을 통일한 것이 틀린 방향이었다. s 와 q 는 세는
  값이지 비용이 아니어서, 싼 비용이 없다고 빼면 선택 편향이 된다 — 구제된
  건의 비용만 누락돼도 s 가 0 으로 떨어진다. 모집단을 usable 로 되돌리고,
  대신 a/r 의 **분모** 를 싼 비용과 무관하게 모은다.
- 그 분모의 완전성을 c 쪽 카운터로 보고 있었다. 그 카운터들은 싼 비용이 없어
  먼저 빠진 레코드보다 뒤에서 증가하므로 눈이 멀어 있었다. 전용 카운터를 둔다.
- p′ 를 "통과율" 이라 불렀다. 실패율이다 — 식이 p − p′ 를 쓰므로 그 라벨을
  따르면 부호가 뒤집혀 결론이 반대가 된다.
- 조언 분기가 c_range 가드를 갖지 않아, c 를 재지 못한 로그에서도 "위에 찍힌
  c 가 c_A" 라고 단언했다.

이스케이프 정규화에 대해 서로 반대되는 주석 두 블록이 나란히 남아 있던 것도
정리한다.

회귀 테스트 11건 추가.
라운드 21. 여섯 트랙이 합의한 것이 하나, 네 트랙이 하나 있다.

- 계획이 안 붙었는데 붙었다고 기록됐다(여섯 트랙). 직전 커밋의 `if text:`
  가드가 빈 조언을 안 붙이는데 레코드는 설정값을 그대로 썼다. 그러면 계획
  없이 돈 실행이 Shape A 표본에 섞이고, 보고서는 섞인 실패율을 p′ 라 부르고
  섞인 비용을 c_A 라 부른다. 레코드에 **실제로 붙었는지** 를 남기고
  요청값은 따로 둔다. 요청됐는데 안 붙은 건이 있으면 보고서가 경고한다.
- 구분자 주변 공백 규칙이 **양쪽 공백만** 다뤄, 한쪽에만 공백이 있는 형식이
  통째로 나갔다(네 트랙). YAML 의 `password: value` 는 가장 흔한 설정
  형식이다. 한쪽 공백을 포함해 다루되 값이 자격증명처럼 생겨야 한다 —
  숫자가 있거나, 점 없이 열여섯 자 이상이거나.
- join_streams 의 고정 4096자 창을 **없앤다.** 그 창은 언제나 틀린 크기다.
  작으면 긴 PEM 본문과 긴 세션 토큰을 놓치고, 크면 멀쩡한 검증 출력을 통째로
  버린다. 조각이 한 스트림의 머리부터 수천 자에 걸칠 수 있으므로 어떤
  고정값도 맞지 않는다. 역순에서만 잡히면 역순 결과를 내보낸다 — 내용은 같고
  순서만 다르며, 정방향 이음매를 이루던 두 조각은 양 끝에 떨어져 형성되지
  않는다. 창과 전체 폐기 분기가 함께 사라진다.
- 아는 값의 파생 표현을 열거하던 것을 정규화로 바꾼다. 열거는 깊이가 언제나
  임의적이다 — 세 겹 직렬화나 두 번 인코딩은 목록에 없었다. 원문 하나만 두고
  **텍스트 쪽** 을 한 겹씩 풀며 같은 자리를 본다.

보고서:

- a 와 r 의 분모가 조언이 닿을 수 없었던 인프라 실패까지 포함했다(세 트랙).
  러너는 검증까지 못 간 실패에 조언하지 않고 바로 승급한다. 러너와 같은
  술어(reached_verify)로 거른다.
- 그 분모가 비싼 비용 0 을 버렸는데, c 의 분모는 0 을 포함하도록 이미
  고쳐져 있었다. 같은 술어를 두 코드 경로가 다른 모집단으로 판정하던 것이다.

run_verify 의 파이프 미설정 지적은 오탐이다 — stdout/stderr=PIPE, text=True
가 이미 설정돼 있고, 리뷰어는 diff 문맥만 봤다.

회귀 테스트 13건 추가.
라운드 22. 네 트랙 합의 하나, 세 트랙 합의 셋. 다섯 건이 직전 커밋에서 나왔다.

- 조언이 전부 비거나 실패하면 `advisor_on` 이 거짓이 됐다(네 트랙). 직전
  커밋이 `advise_first` 에 실제 적용 여부를 담았는데, 보고서는 그 키로 "조언을
  켰는가" 를 판정한다. 그러면 조언 호출 비용을 이미 지불한 로그에서 조언
  구간이 통째로 건너뛰어지고 조언 없는 c + p 판정이 나간다. 나란한 두 키가
  서로 다른 의미를 갖던 것도 함께 고친다 — `advise_first` 와
  `advise_on_failure` 는 둘 다 요청을 뜻하고, 실제 적용은
  `advise_first_applied` 가 따로 담는다. p′ 와 c_A 라벨만 실제 쪽을 본다.
- join_streams 가 역순 결과를 그대로 내보냈다(세 트랙). **두 순서가 각각
  다른 자격증명을 만들 수 있고**, 그때 정방향에서만 잡히던 값이 이름 없이
  되살아난다. 역순에서 지워진 구간을 되짚어(removed_spans) 이음매를 가로지르면
  두 조각으로 나눠 정방향 결과에서도 지운다. 창은 여전히 필요 없다.
- 표현을 텍스트에서 풀던 방식을 버린다(세 트랙). `json.loads` 는 여러 줄
  텍스트나 따옴표가 든 텍스트를 거부하므로 실제 검증 출력에서 첫 겹부터
  멈춘다. 값 쪽에서 두 변환(JSON 이스케이프, 퍼센트 인코딩)의 **합성에 대해
  닫힌** 집합을 만든다 — 깊이 셋이면 형태가 열다섯 개이고 빠지는 조합이 없다.
  소문자 십육진수(`%2f`)도 함께 낸다.
- 공백 없는 할당이 코드를 삼켰다(두 트랙). `self.api_token=config.api_token`
  과 `password=get_password(user)` 가 통째로 지워졌다. 이름 앞의 점은 속성
  접근이고 값 뒤의 괄호는 호출이다 — 둘 다 코드다. snake_case 값도 뺀다
  (`API_TOKEN = authentication_failure`).
- 그 lookbehind 가 앞말에 붙은 이름까지 막으므로, 값이 확실한 자격증명
  모양(숫자 있고 점 없고 열두 자 이상)이면 위치를 안 따지는 갈래를 더한다.

`route_failed` 가 소비되지 않는다는 지적은 오탐이다 — `text` 가 그 경우 이미
빈 문자열이므로 `if text:` 가 같은 판정이다.

회귀 테스트 16건 추가.
라운드 23. 지적 수가 줄었고 다섯 트랙이 지적 없음/APPROVE 였다. 남은 것 중
네 트랙 합의가 둘이다.

- 자격증명 값의 모양 규칙이 갈래마다 따로 적혀 있었다. **구분자 주변의
  공백으로 코드와 자격증명을 가르던 것을 버린다** — 앞선 두 판이 그것으로
  가르려다 각각 반대 방향으로 틀렸다(양쪽 공백만 보면 YAML 을 흘리고, 공백
  없는 형태를 무조건 받으면 `api_token=credentials.secret_key` 를 지운다).
  서식은 값이 무엇인지 말해 주지 않는다. 판정은 값의 모양 하나로 하고,
  그 정의를 한 곳(_CREDENTIAL_VALUE)에 둔다. AWS 갈래도 같은 정의를 쓴다.
- 토큰의 끝을 고정한다. 안 하면 `authentication_failure` 에서 밑줄 앞의
  `authentication` 만 매치돼 절반만 지워진다 — 지우지 말았어야 할 것을
  절반만 지운, 가장 나쁜 결과다.
- removed_spans 의 함정 셋을 막는다. (1) 맞닿은 두 표식 사이의 빈 조각을
  "끝까지 지움" 으로 읽어 뒤쪽 전부를 한 구간으로 삼았다. (2) 살아남은
  조각이 지워진 구간 안에도 있으면 앞쪽 자리를 골라 이음매 복구가 헛돌았다.
  남은 꼬리 전체로 자리를 정한다. (3) 되짚기 실패의 폴백이 fail-open 이었다 —
  자식이 출력에 `[REDACTED]` 를 한 번 찍는 것만으로 무력화할 수 있었으므로
  fail-closed 로 간다.
- 이음매 조각의 치환 범위를 길이로 가른다. 긴 조각은 전역으로 지운다(자식이
  같은 값을 여러 곳에 심어 둘 수 있다). 짧은 조각은 그 자리에서만 지운다
  (자식이 매치를 경계 한 글자 앞에서 시작시켜 흔한 조각을 전역 삭제의
  무기로 삼을 수 있다).

보고서:

- 프라임 라벨(s′, p′)이 요청 플래그를 보고 정해졌다(네 트랙). 직전 커밋이
  계산해 둔 plan_applied_on 은 아무 데서도 쓰이지 않았다. 계획이 붙은 실행과
  안 붙은 실행이 섞이면 프라임 라벨을 쓰지 않는다.
- 설정 지문에 레코드별 결과값이 섞여 있었다. 그러면 계획 적용 여부가 다른
  것을 "설정 두 종" 으로 보고 엉뚱한 이유를 대며 s 를 거부한다. 두 질문을
  따로 묻는다.
- a 와 r 의 나눗셈 가드가 분모와 다른 리스트(priced_pairs)를 봤다.

PEM 게이트가 산문에서 실패 신호를 삼킨다는 지적은 관측 수준에서 재현되지
않는다 — 세 가지 형태로 확인했고 FAILED 줄이 모두 남는다.

회귀 테스트 11건 추가.
라운드 24. 다섯 트랙이 APPROVE 였지만, 남은 지적 중에 이 파일에서 가장 오래
살아남은 결함이 있었다.

**내 테스트 두 개가 공허했다.**

- test_key_bodies_folded_at_any_width_are_redacted 의 프로브가 접힘 너비보다
  긴 40자 연속이었다. 너비 4/8/16 에서는 그 문자열이 입력 어디에도 없으므로
  리댁션이 항등 함수여도 통과한다. 실제로 재 보니 **네 자로 접힌 키의 본문
  400줄이 전부 남아 있었다** — 게이트는 열리는데 범위 주사가 아무것도 안
  먹어 마커만 지워지고 있었다. 라운드 19 의 수정은 처음부터 동작하지 않았고,
  그것을 증명한다고 쓴 테스트가 다섯 라운드 동안 가렸다.
- test_short_decoded_forms_do_not_enter_the_redaction_list 는 슬래시 여섯 개를
  등록하고 네 개짜리 문자열로 쟀다. 정확 일치이므로 무엇을 해도 통과한다.
  제대로 재 보니 실제로 과잉 삭제가 있었다.

두 테스트를 고치고, 드러난 결함을 고친다.

- 게이트와 범위 주사가 **같은 분류 함수**(key_line_kind)를 쓴다. 세 라운드
  연속 지적된 것이고, 이번에 리뷰어가 구체적 입력을 줘서 재현됐다.
- 줄 **안** 의 base64 연속만으로 게이트를 열던 규칙을 없앤다. END 마커가 함께
  있을 때만 본다 — `FAILED authenticationfailure here` 가 키로 오인돼 실패
  신호를 통째로 삼켰다.
- 짧은 줄 판정에 접두사 변형을 쓰지 않는다. 변형이 낱말 경계에서도 자르므로
  산문의 마지막 낱말(`bundle`)이 짧은 본문으로 둔갑했다.
- 복호 형태의 등록 조건에 **글자 종류** 를 더한다. 한두 글자의 반복은
  자격증명이 아니라 구분자다.

**값 모양 판정을 한 방향으로 고정한다.** 네 라운드 동안 세 기준(점, 밑줄,
구분자 주변 공백)을 시도했고 셋 다 한쪽으로 틀렸다. `correct_horse_battery`
와 `authentication_failure` 는 모양이 같다 — 정규식으로 갈릴 수 있는 것이
아니다. 갈래를 하나로 줄이고 코드는 세 신호로만 살린다: 이름 앞의 점(속성
접근), 값 뒤의 여는 괄호(호출), 따옴표 안의 공백(문장). 그 셋에 안 걸리는
소스 줄은 지워진다 — 과잉 삭제이지만 조언자에게 diff 가 함께 가므로 되찾을
수 있고, 반대 방향은 되돌릴 수 없다. 뒤집힌 판정에 해당하는 테스트 일곱 개를
새 규칙과 그 근거로 다시 썼다.

보고서:

- 계획 적용이 섞인 모집단에서 경고만 하고 판정을 계속 냈다(네 트랙). 이제
  판정을 내지 않는다.
- 비싼 비용이 전부 0 이면 a 와 r 의 나눗셈이 죽었다. estimate_c 에는 있던
  가드가 같은 분모를 쓰는 두 곳에 없었다.
- A+B 로그에서 s′ 만 프라임이고 a, q, r 은 아니었다. 넷 다 계획을 받은 뒤에
  측정된 값이다.

회귀 테스트 20건 추가(공허하지 않은지 각각 확인했다).
라운드 25. 모든 지적에 구체적 입력이 붙어 왔고(메모에 그것을 요구했다),
여덟 건이 그대로 재현됐다. 여섯 트랙은 지적이 없었다.

**게이트와 범위 주사의 관계를 규칙으로 못 박는다.** 직전 라운드에 둘을 같은
함수로 묶었지만, 이번에 산문 방어를 넣으면서 다시 어긋났다. 방향이 있다:
게이트(strict=True)는 산문을 키로 오인하지 않도록 엄격해야 하고, 주사
(strict=False)는 그보다 관대해야 한다. 주사가 더 엄격하면 게이트가 열리고
아무것도 안 먹어 마커만 지워진다 — 이 파일에서 가장 오래 살아남은 결함이
정확히 그 형태였다. 이제 매개변수 이름과 문서로 그 순서를 남긴다.

- 한 낱말짜리 실패 줄이 접힌 본문으로 누적됐다(네 트랙). 접기는 길이가
  같은 줄을 만들고 산문은 제각각인 낱말을 만든다. 길이의 균일함과
  base64 다움을 함께 본다.
- `AssertionError: boom` 이 PEM 헤더로 분류됐다. 헤더 이름을 RFC 1421 이
  정한 것으로 한정한다.
- BEGIN/END 쌍이 있으면 그 사이 내용의 모양으로만 판정한다. 마커 쌍 자체가
  강한 신호이므로 글자 종류를 따질 필요가 없다.
- AWS 갈래에 속성 접근 가드가 없었다(두 트랙).
  `self.aws_secret_access_key = ...` 이 통째로 지워졌다.
- 대괄호 색인과 괄호 앞 공백도 코드다(세 트랙). 다만 그 검사는 **같은 줄**
  안에서만 한다 — `\s*` 로 두면 다음 줄의 대괄호를 호출로 오인한다.
- 복호 값의 등록 조건(글자 네 종)이 진짜 프록시 비밀번호를 놓쳤다(네 트랙).
  막으려던 것은 `//////` 처럼 한 글자의 반복이므로 두 종이면 충분하다.
- 퍼센트 이스케이프의 대소문자는 하나하나 독립이다. 형태를 열거하면 조합이
  2^n 이므로 정규식으로 본다.
- 이음매 조각의 전역 치환 하한이 여섯 자였다. `FAILED` 가 걸려 출력 전체에서
  지워졌다. 자격증명 길이 하한(16)으로 올린다.
- _align_tail 이 지워진 구간 안에서 정렬됐다. **가장 늦은 자리** 를 고른다 —
  구간이 커지므로 틀려도 더 지우는 쪽이다.

보고서:

- 혼합 로그에서 판정만 막고 수는 찍었다(세 트랙). 경고는 읽히지 않고 숫자는
  인용된다. a, q, r, s 를 아예 내지 않는다.
- 두 조언 호출을 둘 다 `a` 로 찍었다. 설계 문서의 기호(a_A, a_B, a_B′)를 쓴다.
- A+B 에서 r 과 손익분기도 프라임 표기한다.

**tools/check_test_vacuity.py 를 남긴다.** 리댁션 함수를 항등으로 바꾼 사본에서
테스트를 돌려, 그때도 통과하는 것을 목록으로 낸다. 라운드 24 의 공허한 테스트
두 개가 다섯 라운드를 가렸으므로 그 부류를 기계적으로 잡을 수단이 필요하다.
지금은 65개 실패 / 39개 통과이고, 통과한 것은 전부 보존 테스트이거나 봉투
파싱 단위 테스트다 — 유출을 보는 테스트 중 공허한 것은 없다.

회귀 테스트 19건 추가.
라운드 26. 다섯 트랙 합의 하나, 셋 합의 하나. 지적마다 입력과 실제 출력이
함께 왔다.

**마커 쌍 사이를 모양으로 재던 것을 버린다.** 직전 판은 조각 길이의 균일함을
봤는데 양쪽으로 틀렸다 — 같은 낱말이 여덟 번 이어진 진단 로그를 키로
오인했고(세 트랙), 줄 길이가 들쭉날쭉한 진짜 키를 놓쳤다. 구분자 주변 공백으로
코드와 자격증명을 가르려던 것과 같은 부류의 실수다.

대신 내용이 실제로 무엇인지 본다. base64 로 풀리는가, 푼 결과가 DER
SEQUENCE(0x30)로 시작하는가. PKCS#1 과 PKCS#8 개인키는 언제나 그렇다. DER 이
아닌 형식(PGP armor)은 양으로 본다. 이 교체 하나로 세 지적이 함께 닫혔다 —
PGP armor 키가 통째로 새던 것(두 트랙)도 여기 포함된다.

- 이음매 조각의 전역 치환 기준도 길이에서 **값 쪽인지** 로 바꾼다. 길이로
  가르면 양쪽이 부딪힌다 — 낮으면 `FAILED` 가 출력 전체에서 지워지고,
  높으면 열두 자짜리 중복 값이 남는다(두 트랙).
- 합성 인코딩에도 퍼센트 대소문자 정규식을 적용한다. JSON 으로 감싼 뒤
  퍼센트 인코딩한 형태는 그 자체가 이스케이프를 담는다.

보고서:

- 혼합 계획 적용에서 `return 0` 으로 함수를 끝내던 것을 걷어낸다(다섯 트랙).
  뒤따르는 타임아웃 판정 보류와 얇은 분모 경고가 통째로 사라졌다. 조언
  구간만 건너뛰고, 이미 출력된 비용비가 혼합값이라는 사실을 함께 알린다.
- 조언 구간의 구간 추정이 고정 z=1.96 을 썼다(두 트랙). c 구간은 이미 t
  분위수를 쓰는데 여기만 정규분위수라, 관측 서넛짜리 분모에서 구간이 좁게
  나와 조언에 유리하게 틀렸다. 같은 헬퍼를 쓴다.

**공허성 점검 도구가 자기 결함을 드러냈다.** 파라미터 ID 를 뭉개서, 한
파라미터만 실패해도 그 이름 전체가 "검사된 것" 으로 보였다 — 이 도구가 잡으라고
만든 바로 그 부류다(두 트랙). 노드 ID 를 보존하도록 고치자마자 라운드 19 의
공허한 테스트가 아직 남아 있는 것을 찾아냈고(새 테스트를 더하면서 옛 것을 안
지웠다), 그것을 지웠다. 지금은 부분 공허 항목이 없다.

회귀 테스트 12건 추가.
라운드 27. 여섯 트랙이 같은 것을 지적했다.

**직전 커밋의 수정이 죽은 코드였다.** 라운드 26 에서 깨진 편집을 되돌리려고
`git checkout` 을 했는데, 그때 커밋돼 있던 옛 `return 0` 블록이 복원됐다.
그 위에 새 블록을 얹었으니 같은 조건의 분기가 둘 연달아 있고 앞의 것이
함수를 끝낸다 — 새 블록도, 그 뒤의 타임아웃 판정 보류도 도달할 수 없었다.
편집 도구를 되돌린 뒤 같은 자리를 다시 고칠 때 앞의 것이 남아 있는지 보지
않은 것이 원인이다. 판정 사다리 안쪽의 중복 가드도 함께 걷었다 —
`else` 안에 있어 언제나 거짓이었다.

**공허성 점검 도구가 고장나 있었다.** pytest 요약 줄은 `FAILED <id> - <사유>`
인데 사유까지 ID 로 저장해, 수집한 ID 와 맞지 않는 것은 전부 "통과" 로
분류됐다. 이 도구가 잡으라고 만든 부류를 이 도구가 저질렀다(네 트랙).
고치자마자 라운드 26 에 내가 쓴 가변 접힘 테스트가 공허한 것을 찾아냈다 —
프로브가 접힌 자리를 가로지르는 40자 연속이라 입력 어디에도 없었다.

리댁션:

- 마커 쌍 사이 판정에 **표식** 을 도입한다. DER(0x30 + 최소 길이),
  PGP armor 체크섬 줄, PEM 헤더 줄. 표식이 하나도 없으면 양의 문턱을 훨씬
  높인다. 앞선 판은 200자만 보고 `FAILED` 를 서른네 번 이어 붙인 로그를
  키로 삼켰고(두 트랙), DER 첫 바이트만 보고 `MAAA` 를 키로 삼켰다.
- 헤더를 **줄 단위** 로 건너뛴다. 낱말 단위로 보면
  `Version: GnuPG v2.4.7 (GNU/Linux)` 에서 `GnuPG` 가 본문으로 섞이고
  `v2.4.7` 에서 산문으로 판정돼 PGP 개인키 전체가 빠져나갔다(세 트랙).
- 이음매 조각의 값/이름 구분을 **모양에서 위치로** 바꾼다. 역순 이음매는
  `err 의 꼬리` + `out 의 머리` 로 이름-값을 만드니, err 쪽이 이름이고
  out 쪽이 값이다. 모양으로 가르려던 두 판(길이, 글자 종류)은 양쪽으로
  틀렸다 — 값 쪽 중복을 놓치거나 소스의 식별자를 통째로 지웠다.

보고서:

- a 와 r 이 `priced_fields_missing` 이 붙은 부분 가격을 완전한 비용으로
  받았다(네 트랙). 싼/비싼 경로는 이미 같은 술어로 거른다.
- `실제로 쓴 돈` 이 조언과 재시도 비용을 빼고 셌다(두 트랙). 그 로그에서
  절감의 부호까지 뒤집힌다.
- 최종 안내와 `c + p′` 라벨이 **요청** 플래그를 봤다(세 트랙). 계획이 한 번도
  안 붙은 로그에서 c 를 c_A 라고, 실패율을 p′ 라고 말하게 된다.
- 조언 로그에서 조언 없는 모형의 절감률과 손익분기를 무표식으로 찍었다
  (두 트랙). 경고는 읽히지 않고 숫자는 인용된다.

회귀 테스트 9건 추가, 공허했던 테스트 1건 수정.
라운드 28. 다섯 트랙 합의 하나, 셋~넷 합의 셋.

- `Name: value` 를 **전부** 헤더로 보던 것이 양쪽으로 틀렸다(네 트랙).
  `AssertionError: boom` 이 헤더가 되어 표식 없는 400자 문턱을 200자로
  낮추고 그 줄 자체도 건너뛰어 실패 증거를 지웠고, `PrivateBody: <base64>`
  로 위장한 본문은 통째로 버려져 키가 빠져나갔다. 세 갈래로 가른다 —
  값이 본문이면 본문으로 세고, 이름이 아는 헤더면 건너뛰고, 둘 다 아니면
  산문이다.
- 이음매의 "err 쪽이 이름, out 쪽이 값" 단정이 이름-값 형태에만 맞았다
  (세 트랙). `sk-` 나 `ghp_` 처럼 접두사가 붙는 토큰은 구분자가 없고 값
  하나가 이음매를 가로지를 뿐이라, out 쪽 조각(`FAILED`)을 값으로 보고
  전역 삭제하면 멀쩡한 로그가 지워졌다. 값의 범위는 **첫** 구분자가 정한다 —
  마지막 것을 잡으면 값 안에 우연히 든 콜론(`Traceback:`)이 경계가 된다.

보고서:

- 직전 커밋의 `advisory_spend` 가 a 와 r 이 거부한 부분 가격을 다시 받아
  정확한 지출액처럼 합산했다(다섯 트랙). 같은 보고서가 "비용을 얻지 못했다"
  고 경고하면서 그 값을 더하는 셈이다. 같은 술어로 거르고, 타임아웃으로 뺀
  비용이 있으면 지출이 실제보다 작다고 알린다.
- a 와 r 의 **분모** 도 부분 가격을 완전한 비용처럼 담았다(세 트랙).
  분자만 거르면 분모가 작아져 a 와 r 이 커진다.
- 조언 설정(route, context)이 섞이면 s 만 막고 p′ 와 c_A 는 계속 찍었다.
  라벨에도 같은 검사를 건다.

**공허성 도구의 세 번째 파싱 결함.** `FAILED` 만 실패로 세어, pytest 가
수집·픽스처 오류를 `ERROR` 로 내면 그것이 "통과" 로 분류됐다(두 트랙).
비정상 종료 코드도 함께 본다.

설계 문서: 승급 비용만 프라임이 안 붙는 이유(비싼 경로는 원래 과제를 받는다)를
적고, V1 경계 표의 자식 수를 모양별로 나눈다.

회귀 테스트 5건 추가.
라운드 29.

**OpenSSH 개인키가 통째로 샜다.** ssh-keygen 의 기본 출력인데, 본문이
`openssh-key-v1\0` 로 시작해 DER SEQUENCE 검사에 안 걸리고 armor 체크섬도
PEM 헤더도 없다. 좁게 접으면 줄 단위 판정도 빠져나가 400자 문턱만 남는데
ed25519 키는 그보다 짧다. 아는 키 형식의 **매직 바이트** 를 목록으로 둔다 —
DER, OpenSSH, OpenPGP 비밀키 패킷 태그.

- 헤더 값의 길이 문턱이 여덟 자라 네 자씩 쪼개 헤더로 위장한 본문이
  빠져나갔다(두 트랙). base64 한 묶음(4자)이면 본문으로 센다. 그리고
  **범위 주사도 그 형태를 먹게** 한다 — 게이트만 고치면 마커만 지워지고
  본문이 남는, 이 파일에서 가장 자주 재발한 형태가 된다.
- 이음매에서 지울 것은 조각이 아니라 **값 텍스트** 다. 조각째 지우면
  앞머리(`=`)가 붙어 있어 같은 값의 맨몸 중복이 남는다.
- 정확 일치로 등록된 값 안의 콜론은 이름-값 경계가 아니다. 그렇게 읽으면
  흔한 낱말(`FAILED`)이 전역 삭제의 대상이 된다(두 트랙).

보고서:

- 프라임 라벨(p′, c_A, s′, a_B′)이 붙는 **네 자리가 각각 다른 조건** 을 썼다
  (세 트랙). 한 보고서 안에서 같은 프라임이 서로 다른 뜻이 됐다. 조건을
  하나로 모은다 — 모든 실행에 계획이 붙었고(전칭), 조언 설정이 하나여야 한다.
- 설정 순수성과 계획 적용을 한 변수에 합쳤던 것을 되돌린다(두 트랙).
  덮어쓰면 "계획이 붙었는가" 의 답을 잃는다.

**테스트 세 건이 공허했다.**

- 라운드 28 에 내가 쓴 헤더 위장 테스트의 프로브가 64자 경계를 가로질러
  입력에 없었다. 도구가 찾아냈다.
- 라운드 24 가 공허성을 발견하고 고친 판을 **새로 추가만** 하고 깨진 원본을
  안 지웠다 — 같은 수정을 쌍둥이에 적용하지 않은 형태다. 원본을 지운다.
- 이음매 판정 테스트가 바로 위 테스트와 바이트 단위로 같은 단정 하나뿐이라,
  길이 비교든 동등성 비교든 통과했다. 실제로 두 순서가 다른 값을 잡는
  입력으로 바꾼다.

claude-B2 의 CRITICAL(테스트 모듈 구문 오류, 파일이 .md 로 추가됨)은 재현되지
않는다 — ast.parse 정상, 리터럴 `[REDACTED]` 열 곳은 전부 의도된 단정이다.
그 트랙이 받은 샤드가 훼손된 것으로 본다.

회귀 테스트 4건 추가.
라운드 30. 네 트랙 합의 하나, 셋 합의 하나.

**이음매 전역 삭제의 방향을 여기서 고정한다.** 이 자리는 세 라운드 동안
두 방향을 왕복했다 — 전역으로 지우면 `authentication_failure` 같은 소스
앵커가 출력 전체에서 사라지고(세 트랙), 자리에서만 지우면 같은 값의 맨몸
중복이 남는다. 규칙을 명시한다: 이음매에서 배운 값이 **자격증명처럼 생겼을
때만**(숫자나 대소문자가 섞이고 열두 자 이상) 전역으로 지운다. 순소문자
낱말 조합의 맨몸 중복은 이름 없이 남는데, 그것은 이 리댁터의 원래
한계(이름 없는 문자열은 못 잡는다)와 같은 것이고 소스를 지우는 쪽보다 낫다.
라운드 28 의 테스트가 문법보다 강한 것을 기대하고 있었으므로 함께 고쳤다.

- PuTTY 개인키(.ppk)가 검사 대상 밖이었다(두 트랙). PEM 마커가 없고
  `Private-Lines: N` 이 뒤이어 오는 N 줄을 본문이라고 스스로 선언한다.
  그 선언을 그대로 쓴다 — 모양을 짐작할 필요가 없다. 선언된 줄 수에 상한을
  둬, 자식이 큰 수를 적어 출력 전체를 지우게 만드는 것을 막는다.
- DER 을 태그 한 바이트로만 판정했다(두 트랙). `MAAA`(0x30 0x00 0x00)를
  서른 번 이어 붙인 진단 로그가 키가 됐다. 길이 필드까지 본다.
- 헤더 위장 본문의 네 자 문턱이 진단 줄을 본문으로 만들었다. 게이트 쪽에서만
  글자 종류를 함께 본다 — 주사 쪽은 더 관대해야 게이트가 연 블록을 소비한다.
- `known_secret_forms()` 가 lru_cache 로 환경을 한 번 읽어 붙잡았다.
  자식을 띄운 뒤의 변화를 못 따라간다. 캐시를 뗀다.

보고서:

- 최종 안내만 요청 여부(`advise_first_on`)로 프라임을 골랐다(네 트랙).
  계산부는 `shape_a_measured` 를 쓰는데 여기만 달라, 계획이 실제로 안 붙은
  로그에 A+B 식을 가리켰다. 술어를 통일하고 `a′` 표기도 `a_B′` 로 맞춘다.
- Shape A 가 실제 적용된 로그의 비용비를 `c` 로 찍었다(두 트랙). `c_A` 다.
- 조언 설정 지문이 블랙리스트였다. 레코드에 과제별 값이 하나만 추가돼도
  모든 지문이 달라져 단일 설정 로그가 "혼합" 으로 잡힌다. 화이트리스트로 바꾼다.
- s 판정에 최소 표본 검사가 없었다. 조언받은 실패 세 건으로도 Wilson 하한이
  손익분기를 넘을 수 있다.

회귀 테스트 6건 추가.
라운드 31. codex 쪽 네 트랙 합의가 둘, 셋 합의가 둘. claude 트랙 아홉 개는
모두 invalid 로 끝나 이번 라운드의 커버리지는 한쪽에 치우쳤다.

직전 커밋이 만든 것들:

- PPK 의 빠른 검사가 대소문자를 구분해 정규식과 어긋났다(네 트랙).
  `private-lines:` 는 빠른 검사에서 걸러져 정규식이 아예 안 돌았다 — 빠른
  검사가 본 검사보다 좁으면 그 자체가 fail-open 이다.
- 선언된 줄 수를 그대로 믿었다(네 트랙). 적게 선언하면 남은 본문이 그대로
  나간다. 선언을 **최소치로만** 믿고, 그 뒤로도 본문 모양의 줄이 이어지면
  계속 지운다(상한은 그대로 자식의 과대 선언을 막는다).
- 이음매 전역 삭제의 **주석과 코드가 어긋났다**(세 트랙). 주석은 "숫자가
  있거나 대소문자가 섞였으면" 인데 코드는 `_looks_like_base64` 를 불러 두
  종류를 요구했고, 그래서 숫자만으로 된 값(`123456789012`)이 이음매에서만
  지워지고 중복이 남았다. 주석대로 고치고 그 판정에 이름을 준다.
- 값에 붙은 따옴표까지 함께 지워, 따옴표 없는 같은 값이 남았다.

보고서:

- 이득 조건 s > a + q·r 에는 c 가 나오지 않는데 `c_range` 로 그 판정을
  막고 있었다(두 트랙). 비용이 0 이라 c 를 못 내는 구독 로그에서 Shape B 를
  영영 판정할 수 없게 되는데, 그것이 이 도구가 겨냥한 바로 그 사례다.
- 단일 출처 규칙이 싼/비싼 경로만 봤다(두 트랙). 조언과 재시도가 다른
  회계에서 온 로그가 통과해 그 수로 판정하게 된다. 출처 집합이 네 단계를
  모두 담는다 — 술어를 한 곳에 두는 편이 검사를 늘리는 것보다 낫다.
- 비용비의 이름이 두 갈래뿐이라, 모집단이 섞인 값에 `c` 가 붙었다(세 트랙).
  세 갈래로 나눈다 — c_A, c, 그리고 **어느 이름도 아닌 경우**.

회귀 테스트 4건 추가.
라운드 32. 하네스를 고쳐 claude 트랙이 돌아왔다(아홉 중 일곱이 usable,
셋은 APPROVE). 두 라운드 동안 절반의 커버리지를 잃고 있었다.

**PPK 리댁션의 다섯 결함.** 직전 두 라운드에 넣은 코드이고 가장 덜 검토된
부분이었다. 전부 "선언을 그대로 믿은" 데서 나온다.

- 앵커 정규식만 물리적 줄바꿈을 봤다. 본문 소비 루프는 이스케이프된
  줄바꿈까지 한 줄로 보는데 앵커가 그러지 않아, JSON 에 직렬화된 로그에서
  앵커가 안 잡혔다(두 트랙).
- 상한을 넘는 선언에 `continue` 를 걸어, 그 상한이 **리댁션을 끄는
  스위치** 가 됐다(두 트랙). 잘라 낼 뿐 건너뛰지 않는다.
- 이어짐 판정이 base64 글자로만 된 아무 낱말이나 본문으로 봐서 `FAILED`,
  `Traceback` 같은 실패 앵커가 지워졌다. 본문 줄은 최소 열여섯 자다.
- 선언분은 내용과 무관하게 지웠다. 봉투(`PuTTY-User-Key-File`)가 없으면
  그 한 줄은 자식이 심을 수 있는 문자열이므로 믿지 않는다.
- 상한에 닿으면 그 뒤가 그대로 나갔다.

이제 **봉투가 있으면 선언을 믿고, 없으면 모양만 본다.**

- 과제 텍스트가 조언자에게 지워지지 않고 갔다(두 라운드 연속 지적).
  자식의 출력만 막고 입력을 안 막으면, 과제에 붙어 온 자격증명이 그대로
  나간다 — 조언자는 이 실행에서 유일하게 외부로 나가는 경로다.
- `advice_text` 는 아무 데서도 안 불리는 죽은 코드였다. 세 라운드 연속
  "쌍둥이인데 실패 방향이 반대" 로 지적받았고, 실제로 그 쌍둥이만 fail-open
  이었다. 지운다. 테스트가 그 죽은 쪽을 부르고 있어 실제 쓰이는 쪽으로 옮겼다.

보고서:

- 출처 수집이 truthiness 로 걸러 **비용 0 인 단계가 통째로 빠졌다**(두 트랙).
  구독 로그에서는 그것이 대부분이고, 같은 파일이 다른 곳에서는 0 을 관측값
  으로 취급한다.
- B 전용 로그가 "모집단 혼합" 으로 라벨링됐다(두 트랙). 실패 후 조언은
  검증 뒤에 오므로 싼 실행의 비용을 바꾸지 않는다 — 그 비용비는 c 다.
- 설정 지문이 advisor 블록 없는 레코드를 빼, 조언 로그와 비조언 로그가
  섞인 것을 단일 설정으로 봤다.

회귀 테스트 6건 추가.
라운드 33. 세 트랙 합의 하나.

**PPK 에서 선언과 봉투를 믿는 것을 그만둔다.** 세 라운드에 걸쳐 그 둘을 믿는
판을 세 번 냈고 세 번 다 틀렸다.

- 상한 초과 선언에 `continue` 를 걸어 리댁션을 껐다(라운드 32 에 고침)
- 적게 선언하면 남은 본문이 나갔다(라운드 31 에 고침)
- 봉투 신뢰가 문서 전역이라, 문서 어딘가의 언급 하나가 모든 선언을
  신뢰하게 만들었다(세 트랙). 봉투를 붙이고 큰 수를 적으면 그 만큼의
  진단 로그가 지워졌다(두 트랙).

이제 `Private-Lines:` 는 **어디를 볼지** 만 말한다. 무엇을 지울지는 줄의
모양이 정한다. 그러자 선언 수도 봉투도 아무 영향이 없어져, 그 개념을 통째로
걷어냈다 — 규칙이 하나 줄고 결함이 셋 사라졌다.

앵커가 줄 접두사를 허용하지 않던 것도 고친다. PEM 경로는 이미 벗기는데
여기만 안 벗기면 쌍둥이 한쪽에만 적용된 방어가 된다.

남는 한계는 문서화했다: 자식이 본문 아닌 줄로 채워 진짜 본문을 뒤로 밀면
그 본문은 남는다. 그러나 이름 없는 base64 로 남는 것이고, 이 리댁터가 원래
못 잡는 형태다 — 진단 로그를 지우는 쪽보다 낫다.

보고서:

- `실제로 쓴 돈` 이 부분 가격 레코드를 통째로 뺐다(두 트랙). **지출은 통계와
  다른 질문이다** — 통계는 "이 수를 판정에 쓸 수 있는가" 를 묻고 부분 가격을
  빼지만, 지출은 "얼마를 썼는가" 를 묻는다. 빼면 실제보다 적게 쓴 것처럼
  보이고 절감의 부호까지 뒤집힌다.
- 그 경고가 타임아웃으로 세면서 제외는 부분 가격으로 했다. 경고와 제외가
  같은 술어여야 한다.

`first_flags` 가 비조언 레코드를 뺀다는 지적은 오탐이다 — 그 컴프리헨션은
usable 전수를 돌고 advisor 블록이 없으면 False 를 낸다.

"테스트가 공허하다" 는 반복 지적의 원인도 확인했다. 하네스가 리뷰 대상을
자기 리댁터로 지워 보내므로, 키 픽스처가 리뷰어에게는 `[REDACTED ...]` 로
보인다. 그쪽에서는 실제로 프로브가 입력에 없다.

회귀 테스트 3건 추가.
라운드 34. 두 트랙 합의가 넷.

**PPK 를 줄 단위 주사 하나로 다시 쓴다.** 앵커를 정규식으로 잡던 판은 접두사
허용을 손으로 깎았고 본문 루프는 `strip_log_prefix` 를 썼다 — 같은 질문(이
줄은 무엇인가)을 두 곳이 다르게 판정하는, 이 파일에서 가장 자주 재발한
형태다(두 트랙). 이제 한 루프가 모든 줄을 같은 방식으로 정규화한다. 그러자
직렬화된 줄바꿈 문제도 함께 사라졌다.

- 본문의 **마지막 줄은 짧다**(두 트랙). PuTTY 는 예순네 자로 접으므로
  마지막 줄이 네 자일 수 있는데 모든 줄에 열여섯 자를 요구해, 대략 다섯
  키에 하나꼴로 꼬리가 남았다. 길이 조건은 첫 줄에만 건다 — 산문 한 낱말로
  본문이 시작되는 것만 막으면 된다.
- 값 뒤의 **공백을 사이에 둔 괄호** 를 호출로 보던 것이 자격증명을 흘렸다.
  `API_TOKEN = abc123def456ghi789 (note)` 가 그대로 나간다. 숫자가 든 값은
  붙은 괄호만 호출로 보고, 숫자 없는 값에만 그 완화를 남긴다 — 그쪽은
  애초에 자격증명이라는 근거가 약해 대가가 작다.

보고서:

- a 와 r 의 분모가 타임아웃 시도를 안 걸렀다(두 라운드 연속). 분자는 거른다 —
  중간에 끊긴 부분값이 분모를 낮춰 a 와 r 을 키운다.
- 직전 커밋이 부분 가격을 지출에 넣으면서 그 사실을 안 알렸다(두 트랙).
  넣는 것은 안 쓴 것처럼 보이지 않기 위해서고, 밝히는 것은 그 수가 판정에는
  안 쓰였기 때문이다. 두 계수를 따로 센다.
- `(조언 없는 모형의 수다)` 표식이 같은 실행의 판정문과 반대로 말했다(두 라운드
  연속). 그 c 는 이 로그에서 잰 c_A 다 — 틀린 것은 c 가 아니라 **모형** 이다.
- 계획 적용이 섞이면 a, q, r, s 를 안 내는데, 최종 안내는 "위의 판정을
  보라" 고 했다. 없는 것을 가리키면 읽는 쪽은 다른 수를 찾아 쓴다.
- `mixed_application` 이 조언 구간 안에서만 대입돼 NameError 위험이 있었다.

회귀 테스트 4건 추가.
라운드 35. 두 트랙 합의가 둘.

- `join_streams` 의 진입 조건이 "이음매에서 자격증명이 형성됐다" 와 동치가
  아니었다(두 트랙). `reverse != 따로 지운 것` 은 역순이 **덜** 지울 때도
  참이고, 그때 가로지르는 구간이 없다는 이유로 검증 출력을 통째로 버렸다 —
  자식이 스트림 하나를 마침표 하나로 만들어 진단을 날릴 수 있었다. 가로지르는
  구간이 있을 때만 복구한다. 없으면 한 스트림 안에서 형성된 것이고 정방향
  리댁션이 이미 잡았다.
- 값과 괄호 사이의 공백을 호출로 보던 완화를 걷는다(두 트랙). 직전 커밋이
  숫자 없는 값에만 남겼는데, 자식이 `(copied)` 한 마디를 덧붙이는 것만으로
  `PASSWORD=orchid_copper_velvet` 을 통과시킬 수 있었다. 대가는 그 서식의
  소스 줄이 지워지는 것인데, PEP8 이 금지하는 형태이고 diff 가 함께 간다.
- 이름과 값 사이의 **구분자가 줄바꿈을 넘었다**. 값의 끝은 같은 줄로 못박아
  놓고 시작은 안 그랬다 — 비밀 이름이 줄 끝에 있으면 다음 줄 첫 토큰이 값으로
  잡혀 두 줄이 한 매치로 지워진다. 세 갈래 모두 고친다.
- PPK 가 400줄에서 멈춰 같은 본문의 나머지를 그대로 내보냈다. 모양 판정이
  자연히 끝을 잡으므로 상한이 필요 없다 — 끝까지 base64 인 것을 더 지우는
  것은 안전한 방향이다.

보고서:

- `(참고) 이 로그의 c + p′` 가 c_A 를 c 라고 불렀다. 프라임은 두 항 모두에
  붙는다.
- `aside` 가 c 를 못 잰 로그에서도 c_A 를 주장했다. 바로 아래 사다리가
  "c_A 도 없으므로" 라고 말하는데 앞뒤가 어긋났다.
- c 를 못 잰 이유를 Shape A 로만 설명해, 실패 후 조언만 켠 로그에 켜지도
  않은 팔을 이유로 댔다.
- 계획 적용 혼합 검사가 `failure_advice_on` 가지 안에만 있어, 시작 전 조언만
  켠 로그에서 섞인 표본에 Shape A 손익식을 그대로 제시했다.

"조언자 stdout 이 리댁션 없이 과제에 들어간다" 는 세 라운드 연속 지적은
오탐이다 — `ask_advisor` 가 `redact_text(extracted)` 로 지운 뒤 돌려준다.

회귀 테스트 3건 추가.
라운드 36. 일곱 트랙이 APPROVE 했다 — 지금까지 최다다.

- `join_streams` 가 이음매의 **한 방향만** 막고 있었다(두 트랙). 이음매는
  자격증명을 만들 수도 있고 이미 있던 리댁션을 없앨 수도 있다 —
  `API_TOKEN=<값>` 뒤에 stderr 의 첫 글자가 `(` 이면 값 끝 판정이 그것을
  호출로 보고 매치를 통째로 죽인다. 직전 커밋이 만든 방향이다.
  **조각을 먼저 지운 뒤 이어 다시 훑는다** — 조각 안에서 잡힌 것은 이미
  사라졌으므로 억제될 수 없고, 이음매에서만 형성되는 것은 두 번째 훑기가
  잡는다. 두 방향이 한 수로 막힌다.
- YAML 블록 스칼라(`password: |` 뒤 들여쓴 줄)가 빠져나갔다. 직전 커밋이
  구분자를 같은 줄로 못박으면서 생긴 구멍이다. 구분자를 다시 열면 그때의
  결함이 돌아오므로 **이 한 형태만** 따로 잡는다.
- 조언 레코드가 "잘렸다" 와 "타임아웃이었다" 를 한 불리언에 합쳐, 아무것도
  안 잘렸는데 참이 됐다. 봉투를 못 읽어 조언을 버린 사실(`envelope_only`)도
  레코드에 남긴다 — "조언자가 답을 못 냈다" 와 다른 사건이고 비용은 이미
  지불했다.

보고서:

- 판정 사다리에서 `c_range is None` 이 타임아웃 가지보다 앞서, c 를 못 잰
  이유가 타임아웃일 때 그 사실이 안 나왔다. 사용자가 할 일이 다르다.
- `c` 의 분모와 `a`/`r` 의 분모가 같은 질문("승급 과제의 비싼 비용 평균")을
  세 가지 필터 차이로 다르게 판정했다(세 라운드 연속). 부분 가격과 타임아웃은
  어느 쪽에서도 쓸 수 없으므로 양쪽에 건다. 남는 차이는 `reached_verify`
  하나이고, 그것은 의도된 차이다 — 조언이 닿을 수 있었던 승급만 세야 한다.

회귀 테스트 4건 추가.
라운드 37. **열한 트랙이 APPROVE 했다.** 남은 지적은 직전 커밋이 바꾼 두
곳에 몰렸다.

- 조각을 먼저 지우도록 바꾼 순간, 그 조각에 들어간 `[REDACTED]` 표식이
  되짚기를 **언제나** 포기하게 만들었다(두 트랙). removed_spans 는 원문에
  그 표식이 있으면 위치를 못 짚으므로 None 을 내고, 그때 폴백이 검증 출력을
  통째로 버린다. 정상 로그에서 그것이 매번 일어났다.
  **분석은 원문에서, 출력은 지운 조각에서** 한다. 두 목적이 다른 텍스트를
  요구한다는 것을 이제 코드가 말한다.
- YAML 블록 스칼라가 뒤따르는 들여쓴 줄을 전부 먹었다(두 트랙). 같은 깊이의
  진단 줄이 함께 지워진다. 키의 들여쓰기를 역참조로 잡아 **키보다 더 들여쓴
  줄까지** 만 본다.
- 지시자(`|2`)와 chomping(`|-`, `>+`)을 못 받아 그 형태가 통째로 나갔다.

보고서:

- 판정 사다리에서 `shape_a_measured` 가 `mixed_application` 보다 앞서,
  섞인 표본도 계획이 한 번은 붙었으므로 첫 가지에 흡수돼 혼합 가지가
  도달 불가였다.
- 비용비 출력의 건수(`len(paired)`)와 평균(`observed_expensive_costs`)이
  서로 다른 모집단이었다. 정확히 비싼 비용 0 인 승급 수만큼 어긋난다.
- `aside` 가 타임아웃이 있는 로그에서도 c 를 c_A 라 불러, 바로 아래 사다리가
  "그 c 를 c_A 로 쓰면 안 된다" 고 말하는 것과 어긋났다.

`cost_symbol` 이 설정 혼합을 검사하지 않는다는 지적은 오탐이다 —
`shape_a_measured` 가 `single_advisor_config` 를 이미 포함하고, 지문은
그 라벨보다 앞(264행)에서 만들어진다.

회귀 테스트 5건 추가.
라운드 38(축소판, 여섯 트랙 중 셋이 APPROVE).

- 이음매 복구가 **원문에서 찾은 구간을 이미 지운 텍스트에 대고** 맞췄다.
  그 텍스트에는 `[REDACTED]` 표식이 박혀 있어 정확 일치가 대부분 실패한다 —
  분석과 출력을 갈랐던 직전 커밋이 남긴 어긋남이다. 조각을 원문에서 걷어낸
  뒤 마지막에 한 번 지운다. 조각별 리댁션 뒤 이어서 다시 훑는 두 단계는
  `_join_redacted` 로 뽑아, 세 군데가 같은 규칙을 쓰게 했다.
- 값의 시작을 `구분자 + 1` 로 잡았다. 패턴이 `[=:][ \t]*` 이므로 공백이
  뒤따를 수 있고 따옴표도 값이 아니다 — 한 글자만 넘기면 잡아낸 값이 실제
  값과 달라 중복이 남는다.
- YAML 블록 스칼라의 지시자와 chomping 이 **어느 순서로도** 온다(`|2-`,
  `|-2` 둘 다 유효). 한쪽만 받아 다른 쪽이 통째로 샜다.

라운드가 두 번 연속 중간에 죽어(백그라운드 작업 중단) 샤드를 셋으로 줄여
돌렸다. `round.sh` 에 `ROUND_SHARDS` 를 넣어 조절할 수 있게 했다.

회귀 테스트 5건 추가.
라운드 39(축소판, 여섯 트랙 중 둘이 APPROVE).

**수를 세는 판정은 정규식에 두지 않는다.** `|2` 는 본문이 두 칸 더 들여쓴
줄이라고 말하는데, 정규식은 수를 셀 수 없어 지시자를 무시했고 한 칸만
들여쓴 진단 줄을 본문으로 먹었다. 이 파일에서 정규식으로 옮긴 판정은 매번
양쪽으로 틀렸고(구분자 공백, 점 허용, 마커 사이 모양, 지시자), 코드로 옮긴
판정(PPK, userinfo)은 그러지 않았다. 같은 수를 둔다.

- `redact_block_scalars` 가 키의 들여쓰기를 세고 명시 지시자를 읽어 본문
  범위를 정한다. 빈 줄은 본문 안에 있을 수 있으므로 판정을 미룬다.
- `_is_credential_shaped` 가 `_looks_like_base64` 에 위임하던 것을 끊는다.
  전자는 "이 값을 출력 전체에서 지워도 되는가", 후자는 "접힌 키 본문 한
  줄인가" 를 묻는다. 지금은 같은 답을 내더라도 한쪽을 고칠 때 다른 쪽이
  함께 움직이면 안 된다 — 이 파일에서 가장 자주 재발한 결함이 그 형태였다.

`_join_redacted` 가 조각 매치를 이어붙인 매치의 진부분집합으로 놓친다는
지적과, 문서 첫 줄 앵커가 안 잡힌다는 지적은 재현되지 않는다 — 두 단계
리댁션이 각각 `[REDACTED][REDACTED]` 와 정상 삭제를 낸다.

회귀 테스트 6건 추가.
라운드 40. 직전 커밋이 넣은 `redact_block_scalars` 에 결함이 다섯,
`_is_credential_shaped` 에 하나. 새 코드가 가장 덜 검토된 부분이라는 것이
이 루프에서 여러 번 확인된 패턴이고, 이번에도 그랬다.

- `|0` 은 유효하지 않은 지시자인데 그 수를 그대로 믿어, 필요한 들여쓰기가
  0 이 되어 뒤의 **모든 줄** 을 먹었다. YAML 이 허용하는 1~9 만 받는다.
- 탭으로 시작하는 줄을 본문으로 셌다. YAML 은 탭을 들여쓰기로 금지한다.
- 본문이 파일 끝에서 끝날 때 원문에 없던 줄바꿈을 넣었다.
- 머리를 물리적 줄바꿈만 걷은 원문에 대고 맞춰, 본문 순회가 이스케이프된
  줄바꿈을 보는 것과 어긋났다. JSON 에 직렬화된 로그에서 통째로 샜다.
- 이름의 구분자를 밑줄만 봤다. `api-key` 가 샌다.
- `_is_credential_shaped` 가 숫자 하나만 있어도 참이라, 이음매가 만든 **가짜**
  자격증명(`API_TOKEN=` + 숫자열)이 출력 전체에서 지워졌다. 그 숫자열이
  진단에 쓰인 수면 `assert retry_after == 123456789012` 가 함께 사라진다.
  글자와 숫자가 섞이거나 대소문자가 섞여야 한다.

머리 판정에서 `strip_log_prefix` 를 쓰려다 되돌렸다 — 그 함수는 `이름: ` 도
접두사로 보므로 `password: |` 가 `|` 가 되어 머리 자체를 못 알아본다.
쌍둥이와 같은 정규화를 쓰는 것보다 이 함수의 계약(들여쓰기를 센다)이
우선이라는 것을 주석에 남겼다.

회귀 테스트 6건 추가.
라운드 41.

**이음매에서 얻은 값을 출력 전체에서 지우던 것을 그만둔다.** 이 자리는
여섯 라운드 동안 양쪽으로 왕복했다.

- 넓히면 `AuthenticationFailure` 같은 소스 식별자와 `assert retry_after ==
  123456789012` 같은 진단이 출력 전체에서 사라진다.
- 좁히면 `correcthorsebattery` 처럼 진짜 자격증명의 맨몸 중복이 남는다.

기준을 길이로, 글자 종류로, 숫자 유무로 세 번 바꿨고 매번 한쪽이 틀렸다.
그 사이에 안정된 자리가 없다는 것이 이제 분명하다.

이음매에서 얻은 근거는 **그 자리에 대한 것** 이다. 다른 자리의 같은
문자열에는 이름이 붙어 있지 않고, 이름 없는 문자열을 못 잡는 것은 이
리댁터의 원래 한계다 — 그 한계를 넘으려다 진단을 지우는 쪽이 더 나쁘다.
근거가 있는 자리만 지운다. 규칙이 하나 줄고 결함 두 부류가 함께 사라진다.

- 하이픈 이름을 `_BLOCK_SCALAR` 에만 넣고 쌍둥이 `_SECRET_SHAPES` 에는
  안 넣었다. `api-key=<값>` 이 샜다.
- 유효하지 않은 지시자(`|0`)를 1 로 대신했다. 그러면 그 수를 자식이 고르는
  것은 똑같고 뒤의 진단이 먹힌다. 머리 자체를 거부한다.
- 패치 안의 YAML(`+password: |`)을 못 알아봤다. 머리와 본문에 같은 diff
  표식을 요구한다.
- 두 번째 communicate 타임아웃이 이미 읽은 검증 출력을 버렸다(두 라운드
  연속 지적). CPython 이 예외에 달아 주는 부분 출력을 쓴다.

전역 삭제를 전제하던 테스트 여섯 개를 지우고, 그 결정과 근거를 담은 테스트
하나로 대체했다.
라운드 42. 직전 커밋이 넣은 diff 표식 맞추기가 두 방향으로 틀렸다.

- YAML 시퀀스(`- password: |`)의 하이픈을 diff 표식으로 먹었다. 본문 줄이
  하이픈으로 시작하기를 요구하게 되어 아무것도 안 지웠다.
- 통합 diff 는 머리가 문맥 줄(` `)이고 본문만 바뀐 줄(`+`/`-`)일 수 있다.
  표식이 다르다는 이유로 본문이 통째로 남았다(두 트랙).

**맞추려 하지 말고 벗기기만 한다.** 벗긴 뒤의 들여쓰기가 그 줄의 진짜
깊이이고, 세 경우가 한 규칙으로 처리된다. 규칙이 하나 줄었다.

- 하이픈을 이름의 **접두사에만** 넣고 접미사에는 안 넣었다.
  `db-password-primary: <값>` 이 샌다. 같은 수정을 쌍둥이에 적용하지 않은
  형태가 두 라운드 연속 나왔다.
- 조언 프롬프트를 조각별로 지웠다. 과제에 이름이 있고 출력에 값만 있는
  자격증명은 어느 쪽에서도 안 잡힌다 — 조립한 뒤 한 번 더 지운다. 리댁션은
  없애기만 하므로 다시 훑는 것이 안전하다.
- `_first_text` 의 dict 분기가 `content` 문자열을 무조건 조언으로 골랐다.
  배열 쪽은 막아 두고 여기만 안 막은 비대칭이고, 그 틈으로 도구 결과의
  파일 내용이 조언 자리에 들어간다.
- 전역 삭제를 없앤 뒤 남은 두 분기가 같은 동작을 하고 있었다. 합친다.

회귀 테스트 5건 추가.
라운드 43. 직전 커밋이 넣은 두 곳에 지적이 몰렸다.

**줄머리 표식을 다루는 세 번째 판이다.** 앞의 둘은 각각 한 방향으로 틀렸다.

- 머리와 본문이 **맞추도록** 요구하면, 통합 diff 에서 머리가 문맥 줄이고
  본문만 바뀐 줄일 때 본문이 통째로 남는다(라운드 42 에 고침).
- **무조건 벗기면**, `password: |` 뒤의 진단 불릿(`- AssertionError…`)이
  들여쓴 본문으로 둔갑해 실패 증거가 지워진다(두 트랙).

머리에 표식이 있을 때만, 그 표식이 붙은 줄에서만 벗긴다. 세 경우(문맥 머리
+ 바뀐 본문 / 표식 머리 + 표식 본문 / 표식 없는 머리 + 불릿)의 요구가 서로
반대라 한 규칙으로 만들 수 없다는 것이 이제 분명하다. 표식 없는 머리 쪽을
택했다 — 진단을 지우는 것이 값 하나를 놓치는 것보다 나쁘고, 놓친 값은
이름 없이 남는 문자열과 같은 부류다. **그 한계를 테스트로 기록했다.**

- `_looks_like_tool_payload` 가 양쪽으로 틀렸다(두 트랙). `name`, `path`,
  `command` 를 표식으로 삼아 평범한 조언 봉투를 버렸고, 정식 tool_result
  모양(`content` 가 텍스트 블록 배열)은 문자열 검사에 안 걸려 그대로
  통과했다. 명백한 표식(`tool_use_id` 계열, `type` 에 tool)만 보고,
  걸리면 **페이로드 전체** 를 건너뛴다.

`redact_block_scalars` 의 독스트링이 로그 접두사를 벗긴다고 적어 놓고 실제로는
안 벗기던 것도 고쳤다. "조언자 출력이 리댁션 없이 전달된다" 는 지적은 세
라운드 연속 오탐이다 — `ask_advisor` 가 `redact_text(extracted)` 로 지운 뒤
돌려준다. 구문 오류 지적도 재현되지 않는다(ast.parse 정상).

회귀 테스트 3건 추가, 한계 기록 1건.
라운드 44. 다섯 건이 재현됐다.

- 이름 앞 점을 막는 가드가 속성 접근(`self.api_key`)과 **설정 키**
  (`db.password`, `spring.datasource.password`)를 함께 막았다. 값이 점 없는
  것일 때만 받는 갈래를 더한다 — 두 형태의 차이는 이름이 아니라 값에 있다.
- CI 로그 태그가 붙은 YAML 블록 스칼라가 빠져나갔다. 태그도 **머리가
  정한다** — 표식과 같은 방식이고, `strip_log_prefix` 는 `password: ` 까지
  접두사로 보므로 쓸 수 없다.
- PPK 본문 판정이 첫 줄 뒤로 아무 검사도 안 했다. `AuthenticationFailure`
  가 첫 줄로 잡히면 `Traceback` 과 `FAILED` 가 본문으로 세어져 진단이 통째로
  지워진다. 본문은 **고정 폭** 으로 접히고 마지막 줄만 짧다는 성질을 쓴다.
- 도구 페이로드 판정이 필드 순회 **뒤** 에 있어, `result` 나 `text` 로 실려
  온 것이 먼저 반환됐다. 앞으로 옮긴다.
- PPK 가 원문에 없던 줄바꿈을 넣었다. 쌍둥이 redact_block_scalars 는 이미
  안 넣는다 — 마지막 본문 줄의 구분자를 남기는 것으로 맞춘다.

작업 중 편집 스크립트의 마지막 검증이 실패해 그 스크립트의 변경이 **전부
안 쓰인** 일이 다시 있었다. 확인용 치환을 스크립트 끝에 두지 않는다.

회귀 테스트 6건 추가.
라운드 45. 네 건이 재현됐다.

- 로그 태그를 **글자 그대로** 요구했다(두 트랙). 줄마다 시간이 찍히는
  형식에서는 두 줄의 태그가 절대 같지 않아 본문이 통째로 남는다. 머리는
  "태그가 붙는 형식인가" 만 정하고 각 줄은 자기 태그를 벗긴다. 태그가 뒤
  공백까지 먹어 본문 들여쓰기가 사라지던 것도 함께 고쳤다.
- PPK 본문의 첫 줄 판정이 길이 문턱뿐이었다. 문턱을 올리면 짧게 접힌 진짜
  본문(`T3BlblNTSC1rZXktdjEA`, 20자)을 놓치고, 내리면 긴 낱말
  (`AuthenticationFailure`, 21자)을 먹는다. **base64 는 네 글자씩 묶인다** —
  그 성질을 쓰면 둘이 갈린다. 휴리스틱이 아니라 형식의 성질이다.
- `_looks_like_tool_payload` 가 키의 **존재만** 봐서, 평범한 메시지 스키마가
  선택적 필드를 `null` 로 내보내면 조언을 버렸다. 값이 있어야 마커다.
- 점 붙은 설정 키 갈래가 값에 점이 있으면 안 걸려
  `spring.datasource.password=correct.horse.battery` 가 샜다. 점이 **둘
  이상** 인 이름은 설정 네임스페이스이고, 파이썬 속성 접근은 보통 점이
  하나다 — 그 경계로 값의 점을 허용할지 가른다.

테스트 픽스처 하나가 66자짜리 base64 본문을 쓰고 있었다. 실제 base64 는 네
글자 묶음이므로 그 픽스처는 애초에 키가 아니었다. 60자로 고쳤다.

`attempt` 의 `verify_output` 이 한 분기에서만 대입된다는 지적은 오탐이다 —
함수 앞에서 빈 문자열로 초기화한다.

회귀 테스트 5건 추가.
라운드 46. 네 건이 재현됐다.

- 접두사 토큰(`sk-`, `ghp_`, `AIza`…)의 **시작을 안 고정** 했다. 값의 끝은
  `_VALUE_END` 로 고정해 놓고 시작은 그러지 않아, `disk-inventory-collector`
  안의 `sk-` 가 걸려 평범한 오류 메시지가 반쪽 지워졌다. 여덟 갈래 전부에
  시작 경계를 붙인다.
- 점 붙은 이름을 점 **개수** 로 두 갈래로 나눈 것이 그 사이에 구멍을
  만들었다 — 점 하나짜리 이름에 점 있는 값(`db.password=Passw0rd.With.Dots`)
  이 어느 갈래에도 안 걸렸다.
- 갈래를 합치자 `correct.horse.battery`(암구호)와 `config.api_key`(속성)를
  값의 모양으로 구별할 수 없다는 것이 드러났다. 둘은 같은 모양이다.
  가르는 것은 **수신자 이름** 이다 — `self`, `this`, `cls` 는 언어가 정한
  것이고 나머지는 관례적인 변수 이름이며, 설정 네임스페이스가 그 이름으로
  시작하는 일은 없다. 목록이라는 것이 이 판정의 한계지만, 모양 짐작과 달리
  **왜 그런지 읽을 수 있고 늘릴 수 있다.**
- 태그를 벗겨 빈 줄이 된 것을 블록 스칼라 안의 빈 줄로 오인했다. 로그
  레코드의 메시지가 비었을 뿐이고 그 뒤는 블록 안이 아니다 — 빈 메시지
  하나로 뒤의 진단을 계속 먹었다.
- `_first_text` 가 Gemini CLI 의 `response` 필드를 몰라 정상 조언과 실패
  진단을 전부 버렸다.

PPK 첫 줄의 4의 배수 검사가 낱말을 통과시킨다는 지적과, Shape B 가 지우기
전에 자른다는 지적은 재현되지 않는다 — 전자는 `AuthenticationFail`(18자)이
문턱에 못 미치고, 후자는 `untrusted_block` 이 지운 뒤에 자른다.

회귀 테스트 11건 추가.
라운드 47(포그라운드 1샤드). 직전 커밋의 두 수정이 한 라운드 만에 양쪽으로
틀렸다.

**수신자 이름 목록을 없앤다.** 목록에 있는 이름(`config`, `settings`)이 흔한
설정 네임스페이스이기도 해서 자격증명이 샜고(`settings.api_key=<값>`), 목록에
없는 이름(`state`)은 소스 줄이 통째로 지워졌다. 이름을 열거하는 한 그 둘을
동시에 만족할 수 없다.

가르는 것은 **값** 이다. 점으로 이어진 소문자 식별자 사슬이면 속성 접근이고,
아니면 자격증명이다. 남는 한계 — `correct.horse.battery` 같은 암구호는
`credentials.secret_key` 와 모양이 같아 남는다 — 를 테스트로 기록했다.
소스를 살리는 쪽을 택한 이유도 함께 적었다: 조언자가 볼 것이 없어지는 편이
더 나쁘고, 남는 값은 이름 없는 문자열과 같은 부류다.

- 토큰 시작 경계에 하이픈이 들어 있어, diff 삭제 줄의 표식이 토큰에 붙으면
  여덟 갈래가 전부 죽었다(`-ghp_…`). 하이픈을 빼도 `disk-inventory-collector`
  의 `sk-` 는 앞 글자가 `k` 라 여전히 안 걸린다.
- PPK 의 마지막 줄이 짧을 수 있다는 규칙이 아무 짧은 줄이나 받았다. 마지막
  줄도 **base64 길이** 여야 한다 — 네 글자 묶음에서 남는 길이는 2 나 3 뿐이고
  `Traceback`(9자)은 나머지가 1 이라 조각이 될 수 없다.
- 자격증명 이름을 단 환경변수가 **비밀이 있는 곳**(파일 경로)을 담는 일이
  흔하다. 그 경로를 목록에 넣으면 파일을 못 찾았다는 진단이 무슨 파일인지
  알 수 없게 된다.
- `verify_excerpt` 가 `redact_text` 의 다섯 단계를 손으로 다시 적고 있었다.
  지금은 동작이 같지만 한쪽에 단계를 더할 때 다른 쪽이 뒤처진다 — 이 파일에서
  가장 자주 재발한 결함의 쌍둥이다. `redact_text` 를 부른다.

백그라운드 라운드가 다섯 번 중단돼 이 라운드는 포그라운드에서 한 샤드만
돌렸다. 커버리지는 좁지만 완주한다.

회귀 테스트 4건 추가, 한계 기록 1건.
@ictechgy
ictechgy merged commit 7ff2917 into main Aug 18, 2026
14 of 16 checks passed
@ictechgy
ictechgy deleted the feat/advisor-pattern-measurement branch August 18, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant