목록으로

오귀속 6자리 정정 후 회수 20건: 정정 자신이 만든 오류

0 조회
개발 도구 신뢰성
#Claude Code#릴리스#정적 검사#코드리뷰#문서 검증

TL;DR

  • 근거로 쓰려던 문장이 거짓이었다. 「v1.9.x가 죽였다」인데 그 구간에 「러너」가 0건
  • 실제 출처는 v1.5.1. v1.11.0이 처음 잘못 적고 두 판이 옮겨 여섯 자리가 됐다
  • 이력 문서까지 훑는 등록 형식으로 6자리를 0자리로 고쳤다. 검사 17개에서 18개
  • 그 변경을 검토했더니 20건. 문서 쪽 12건 중 여덟은 정정 작업이 만든 것이다

웹소설 집필 도구를 만들고 있다. 미뤄 둔 개선 항목이 11건 쌓여 있어서 한 번에 훑기로 했다. 항목마다 판단하는 에이전트를 하나 붙이고 그 판단을 반박하는 에이전트를 다시 하나씩 붙였다. 22개가 돌았고 죽은 것은 없었다.

아래에서 「판」은 릴리스 한 번을 말하고, 「검사가 붉어진다」는 검증 스크립트가 그 항목을 실패로 표시한다는 뜻이다.

가르는 기준은 셋이었다. 조건이 아직 안 채워졌으면 그대로 두고, 지금 채워졌으면 다음 판 후보로 올리고, 조건을 확인할 방법 자체가 없으면 닫는다.

반박이 판단을 둘 뒤집었고 근거는 서로 달랐다.

하나는 닫자는 판단을 그대로 두자로 바꿨다. 닫자는 쪽이 원래 조건에 없던 요구를 새로 얹고, 그 얹은 요구가 불가능하다는 이유로 조건 자체를 확인 불가라고 불렀기 때문이다.

다른 하나는 반대 방향이다. 이 저장소의 미뤄 둔 항목은 대부분 「증거가 도착하는가」를 조건으로 다는데 그 한 건만 「우리가 결정하는가」를 묻고 있었다. 그 결정을 요구하는 것이 항목 자신뿐이면 조건은 혼자 참이 될 수 없다.

판단 근거로 쓰려던 문장

훑기와 별개로 릴리스 노트가 끌고 있던 항목이 하나 더 있었다. 「제안 처리 순서를 글이 아니라 프로그램으로 만들자」다. 네 판이 연속으로 미뤘고 미룬 이유가 매번 같았다.

그 이유를 릴리스 노트에서 그대로 옮겨 적으려던 참이었다.

러너 설계는 v1.9.x 가 독립 검토로 죽인 것이고, 세 릴리스가 같은 이유로 미뤘다

옮기기 전에 확인해 봤다.

$ git show 8feb7d9:CHANGELOG.md | sed -n '1519,1863p' | grep -c "러너"
0

v1.9.0부터 v1.9.2까지 구간에 그 단어가 한 번도 안 나온다.

실제 출처는 v1.5.1이었다. 프로그램으로 바꾸는 안을 이월한 것, 검사 지점 11개 중 넷만 프로그램이 직접 다시 돌려 확인할 수 있다는 진단, 설계를 20레인에 나눠 물어 착수를 물은 열 레인 전부가 착수 전 수정이나 축소를 요구한 기록, 「넣을 조건: 검토 루프백 3회」까지 전부 거기 있었다.

릴리스 노트에 그런 문장이 두 종류, 다섯 자리 있었다. 「러너 설계는 v1.9.x 가」가 셋, 「v1.9.x 가 적은 조건」이 둘이다. v1.11.0이 처음 잘못 적었고 v1.14.0과 v1.16.0이 그대로 옮겨 적었다. 릴리스 이력 문서에도 한 자리 더 있었다.

이력 문서를 통째로 건너뛰던 검사

이 저장소에는 「폐기 표현 잔존」이라는 검사가 있다. 결정이 바뀌어 거짓이 된 문장을 목록에 등록해 두면 그 문장이 되살아날 때 붉어진다.

거기에 등록하려고 코드를 열었더니 이런 줄이 있었다.

# 이력 문서는 검사하지 않는다 — 옛 표현이 남아 있는 것이 정상이다.
DEPRECATED_SKIP = ('CHANGELOG.md',)

옳은 규칙이다. 옛 판의 릴리스 노트에 그 시점의 용어가 남아 있는 것은 정상이고 그것까지 고치면 이력이 이력이 아니게 된다.

그런데 이번 것은 용어가 바뀐 게 아니라 사실이 틀린 것이다. v1.9.x를 읽는 사람에게도 거짓이고 실제로 다음 판이 그것을 근거로 삼았다. 그대로 등록했으면 검사는 통과하는데 거짓 문장은 다섯 자리에 그대로 남는다.

등록 형식에 다섯 번째 자리를 만들었다. 켜면 릴리스 노트와 이력 문서까지 훑는다. 기본값은 끄기다. 등록할 때 용어가 바뀐 것인지 사실이 틀린 것인지를 먼저 가르게 했다.

같이 훑다가 하나 더 나왔다. 이 저장소는 판을 낼 때마다 글감 초안을 남기는데, 그걸 본다는 검사가 실제로는 폴더가 비었는지만 봤다. 초안이 자기 판 번호를 말할 자리가 없어서다. 파일 이름에는 날짜만 있고 본문의 v1.x는 인용이다. 초안 맨 위에 적는 형식을 정하고 기존 16개에 소급해 붙였다.

죽여 보기, 그리고 살아 있는 줄 알았던 레인

v1.15.0이 「붉어지지 않는 검사는 없는 검사다」를 적었다. 그 다음 판이 붉어지지 않는 검사를 두 개 만들었다. 둘 다 기대 문자열에 판별력이 없어서였다. 그래서 이번에는 무엇을 죽였을 때 어떤 문장이 나와야 하는지를 먼저 적고 대조했다. 넷 다 붉어졌고 서로 다른 문장을 냈다.

그 상태로 독립 검토를 두 갈래로 띄웠다. 게이트가 실제로 붉어지는지 보는 쪽과 문서가 서로 어긋나는지 보는 쪽이다. 아래에서 「레인」은 그렇게 따로 띄운 검토 에이전트 하나를 말한다.

게이트 쪽이 세션 한도로 죽었다. 완료 알림에는 이렇게 왔다.

status: failed
result: "Now let me run the checks and probe them."

중간 진행 문장이다. 이대로 「회수 0」으로 넘길 뻔했다.

그런데 그 레인이 죽기 전에 보고서 파일을 써 뒀다. 412줄이었고 끝에서 둘째 줄이 RECALL: 8이었다. 알림에는 그 사실이 없다.

바로 앞 작업에서 순서 문서에 넣은 한 줄이 이것이었다. 「반환값이 초록이어도 레인이 살아 있었는지 먼저 확인하라. 빈 배열은 지적 0건과 생김새가 같다.」 그 줄이 같은 세션에서 곧바로 필요했다.

격리가 만든 두 번의 중단

문서 쪽 레인은 세 번 만에 돌아왔다.

1차는 판 확인에서 멈췄다. 프롬프트에 검토 대상 커밋의 SHA를 박아 두는데, 레인이 보고한 HEAD가 그 커밋의 부모였다. 격리된 작업 폴더를 만들어 주는 옵션을 켰더니 그 폴더가 스폰 시점 HEAD의 부모에 붙어 있었다.

그대로 진행됐으면 검토 대상 파일이 전부 변경 이전 상태이므로 「구현이 아예 없다」, 「정정이 전파되지 않았다」가 나온다. 파일과 줄 번호까지 붙은, 전부 거짓인 보고서다. SHA 지문이 그것을 막았다.

2차는 세션 한도로 죽었다. 3차에서 격리를 끄고 공유 폴더에서 돌려 받았다.

회수 20건, 그중 여덟은 이 작업이 만든 것

게이트 쪽 8건, 문서 쪽 12건이다. 게이트 쪽은 대부분 새 검사의 구멍이었다. 값이 빈 version:이 조용히 통과하는데 성공 문구는 「전부 표기됨」이라고 말하는 자리, 닫는 ---를 안 요구해서 이 규격을 설명하는 글이 인용한 예시가 선언으로 읽히는 자리 같은 것들이다.

문서 쪽 12건 중 여덟은 이번 변경이 스스로 만든 것이었다. 무거운 넷은 이렇다.

첫째, 「네 판이 베꼈다」가 세 판이다. v1.11.0이 처음 적고 v1.14.0과 v1.16.0이 옮겼으니 셋인데, 한 문장 안에서 1+2를 4로 적었다. 이 저장소에는 「개수 세는 문장」을 잡는 검사가 있다. 그 검사는 릴리스 노트를 아예 안 본다. 옛 판에 옛 표현이 남는 것이 정상이라 대상에서 통째로 뺐기 때문이다.

둘째, 같은 절에 같은 항목이 「이월 유지」와 「기각」 두 판단으로 나란히 섰다. 앞엣것을 지우고 뒤엣것을 붙인 게 아니라 뒤에 덧붙였다. 릴리스 노트 생성기는 절을 통째로 뽑고 기각 이력을 찾는 쪽은 앞엣것을 먼저 집는다.

셋째, ### 추가### 변경 절 제목이 각각 두 번 나왔다. 기존 절을 안 보고 새로 붙였다.

넷째가 가장 무겁다. 격리 함정을 발견하고 그 처방을 순서 문서에 넣었는데, 그 처방의 둘째 갈래가 이랬다.

격리를 쓸 거면 대상 커밋이 스폰 시점의 HEAD 여야 한다.

같은 절 세 줄 위에 「워크트리는 스폰 시점 HEAD의 부모에 붙는다」고 적어 놓고 쓴 문장이다. 대상이 HEAD일 때 붙는 것이 바로 그 부모다. 실패를 막으라고 쓴 문장이 같은 실패를 다시 배치하고 있었다.

그리고 상위 규범 문서는 여전히 격리를 강제하고 있었다. 하위 문서에 실측을 넣고 규범을 안 고쳤으니, 다음 사람은 규범을 따라 같은 중단을 세 번째로 겪는다.

고친 뒤의 값

거짓 출처를 적은 문장이 6자리에서 0자리가 됐다. 되살리면 붉어진다.

검사가 17개에서 18개가 됐다. 판 번호와 글감의 짝을 보는 자리가 0개에서 1개다.

훑은 11건 중 2건을 닫았고 2건을 다음 판 후보로 올렸다. 남은 이월은 9건이다. 릴리스 노트에서 네 판을 끌던 항목 둘도 닫혔다. 판단 근거는 항목마다 이슈 코멘트로 남겼다.

폐기 표현 검사의 출력에 「이력 2/2」가 붙었다. 이력 문서를 몇 개 중 몇 개나 실제로 훑었는지다. 전에는 파일이 없으면 조용히 넘어가서 「훑었는데 깨끗하다」와 「못 훑었다」가 같았다.

남은 것

tools/ 아래의 파이썬 파일은 폐기 표현 검사 대상이 아니다. 그 파일이 이 저장소에서 「무엇을 보고 무엇을 안 보는가」를 가장 많이 진술하는데, 그 진술을 보는 검사가 없다. 이번에도 거기서 거짓이 된 주석을 검사가 아니라 검토 에이전트가 잡았다.

새 검사가 실패를 내는 시점은 병합이 아니라 릴리스 노트에 판 번호 절이 생길 때다. 규범 문서는 병합할 때 함께 있어야 한다고 적는다. 둘의 시점이 다르다는 사실을 문서에 적어 두는 것까지만 했다. 병합 시점을 기계가 보게 하려면 아직 안 정한 판을 어떻게 표기할지부터 정해야 한다.

문서 쪽 검토가 확인 못 한 것이 다섯 있다. 이슈 쪽 사실 관계, 앞선 판의 레인 사망 수치, 새 검사를 죽여 본 결과의 재실행 등이다. 마지막은 게이트 쪽이 이미 했지만, 두 검토가 같은 항목을 각각 확인한 것은 아니다.


댓글

댓글 작성