하네스는 매 세션 신호를 발행했다. 그런데 그 신호를 지우는 코드가 저장소에 없었다. 중복 억제는 통째로 열려 있었고, 검색 품질 측정기는 SQL 에러를 0으로 바꿔 거짓 실패를 매 주기 내보내고 있었다. 둘 다 에러 없이 조용히 작동했다.
자가개선 루프는 매 세션 신호를 만든다. fix 커밋이 쌓이면 훅이 pending/*.json을 발행하고, 다음 세션이 그걸 읽어 규칙으로 접은 뒤 지운다 — 지워야 다음 세션에 또 뜨지 않는다. 그 "지우는" 코드가 저장소에 없었다.
검색 품질 측정기 search-recall-probe는 매 주기 "recall 5/10 실패"를 보고하고 있었다. 실제로는 검색이 나쁜 게 아니라 질의 자체가 SQL 에러로 죽어 있었다. 그 에러가 세 단계에 걸쳐 삼켜져 "측정된 0"으로 둔갑했다.
2>/dev/null) · 기본값 대입(${v:-0}) · 종료코드 미검사 세 개가 겹치면 조회 실패와 "결과 0건"이 구분되지 않는다. 측정기는 죽은 채로 매 주기 리포트를 냈다.두 결함은 형태가 반대다. 하나는 해야 할 일을 안 했고(소비 부재 → 거짓 통과), 다른 하나는 하지 않은 측정을 했다고 보고했다(에러 → 거짓 실패). 공통점은 둘 다 에러를 내지 않았다는 것이다. 리포트만 보면 시스템은 정상이었다.
소비자를 배선하는 일은 원래 작은 작업이었다. 그런데 신호 큐를 건드리는 순간 동시성·원자성·소유권이 전부 걸렸고, 교차 모델 적대 리뷰가 라운드마다 새 결함을 뱉었다. 실제 수렴에 4시간 15분, fix 커밋 54건이 들었다.
신호 큐는 여러 세션이 동시에 쓰는 공유 상태다. 파일 하나를 옮기는 코드가 실제로는 크래시 원자성·락·소유권·심링크를 전부 다뤄야 했다. 아래는 커밋 메시지에서 분류한 실제 수리 내역이다.
| 결함 클래스 | 무엇이 깨졌나 | 어떻게 닫았나 |
|---|---|---|
| 신호 소실 | 락 경합·조기 종료·병합 과정에서 신호가 조용히 버려짐. recurrence_signals 유실, 워터마크가 미소비 신호를 삭제, clean 런이 거짓 실패 신호를 생성 |
폐기 대신 spill로 영속 보존 후 재흡수, 흡수 주체를 명시 배정해 고립 경로 마감 |
| 동시성 · 락 | 디렉토리 기반 락은 PID 재사용에 걸려 wedge, 전역 락이 무관한 프로젝트를 서로 막음. 전환을 절반만 적용해 실동작 버그까지 생김 | flock(fcntl) 전면 전환 + 소유권 토큰 + 프로젝트별 스코프. 원장 변경은 직렬화 |
| 경로 · 주입 | 레포 경로가 shell=True에 보간, 임의 절대경로 JSON이 archive로 이동, 심링크·FIFO pending으로 원장 오염과 훅 정지. 동명 repo 5쌍이 실제로 존재해 교차 오dedup CRITICAL 2 |
argv 전환 · 대상 디렉토리 제한 + 스키마 검증 · O_NOFOLLOW + FIFO 가드 · 경로 다이제스트 스코프 · SQL 파라미터 바인딩 |
| 크래시 원자성 | 원장 기록과 archive 이동의 순서를 어느 쪽으로 두든 크래시 창이 남음 — 순서 조정으로 5회 왕복했으나 해결되지 않음 | 순서가 아니라 상태로 판정하도록 전환 + 실행 nonce 기준 역연산(롤백)으로 종결 |
| 자기 수정이 만든 결함 | 수리 과정에서 && false로 회수 분기가 죽고, spill 병합이 직렬화 뒤라 내용 없이 삭제되고, --dry-run이 상태를 바꿈 |
라운드마다 리뷰가 재검출 → 실패 주입·경합 재현 테스트로 재확인 후 수정 |
"원장에 먼저 기록하고 archive로 옮길까, 반대로 할까"를 다섯 번 뒤집었다. 어느 순서든 그 사이에 죽으면 신호가 사라지거나 두 번 처리된다. 답은 순서를 고르는 게 아니라 중간에 죽어도 되돌릴 수 있게 만드는 것이었다 — 순서 최적화를 포기하고 상태 판정 + 역연산으로 바꾸자 진동이 멈췄다.
수리보다 중요한 건 같은 실수가 다시 나지 않게 하는 것이다. 이번엔 새 규칙 파일을 만들지 않고 기존 6개 규칙에 부록·절을 덧붙였다 — 표면을 늘리면 그 자체가 다음 세션의 비용이 된다.
| 규칙 | 무엇에 데었나 | 굳은 형태 |
|---|---|---|
conformance-baseline-correctness +29 |
측정기가 아무것도 재지 않으면서 정상처럼 보였다. 효과 측정 원장은 평가 111행 중 65행이 "판정 보류"로 동결, 그중 50건은 100일 넘게 그대로였다 | 측정기 무음 사망 3형태(동결 · 평가창 어긋남 · 에러 흡수)와 자가 점검: "이 0이 대상 탓인지 측정기 고장 탓인지 리포트로 구분되는가" |
ephemeral-state-not-knowledge +21 |
제외 목록을 접두 비교로 구현하면 이름이 접두인 형제 프로젝트까지 조용히 삼킨다. 게다가 두 소비자가 서로 다른 술어를 써서 한쪽만 걸러졌다 | 경계 매칭 강제(정확일치 또는 x/ 접두) + 술어 단일화 + 자가 점검 "이 술어가 삼킬 형제 이름을 하나 적어보라" |
soft-to-hard-promotion +18 |
"N곳 전수 정렬 완료"는 그 시점의 상태일 뿐이다. 이후 추가되는 사이트는 그 계약을 모른 채 태어나 증상이 부분 재현된다 | 전수 sweep 직후 같은 계약을 exit-code 린트로 고정. 판별 질문: "이 계약을 어기는 새 코드를 오늘 쓰면 무엇이 막는가" |
tdd-enforcement +2 |
픽스처가 프로덕션에 없는 형식이면 결함 경로에 닿지도 못한다 — 수정을 되돌려도 테스트가 통과해 회귀 검증이 통째로 헛통과 | 픽스처는 실제 출력 형태를 실행해서 확인한 뒤 반영. "이럴 것이다"로 만들지 않는다 |
backend-patterns +1 |
타임스탬프를 문자열로 비교하면 포맷이 다를 때 대소가 조용히 뒤집힌다 — 만료된 리스가 언제나 fresh로 읽혔다 | 양변을 파싱해 비교(또는 epoch 정수). 에러가 없어 무음이고, 테스트 포맷이 다르면 회귀도 못 잡는다 |
evidence-only-reporting +2 |
기록에서 값을 지우며 그럴듯한 대체값을 넣으면, 실행되지 않은 것이 실행된 것처럼 읽히는 거짓 기록이 된다 | 편집이 필요하면 [removed]처럼 제거가 드러나는 표시로만 |
"실행해서 확인한 것만 닫혔다고 적는다"
"clean은 마지막 실행 기준이지 증명이 아니다"
하네스는 자기가 일하고 있다고 믿었다. 신호를 발행했고, 측정기는 매 주기 리포트를 냈고, 아무 에러도 없었다. 실제로는 소비가 한 번도 일어나지 않았고 측정은 조회 실패를 결과로 보고하고 있었다. 그리고 그걸 고치는 823줄이 다시 90건의 결함을 뱉었다 — 공유 상태를 다루는 코드에서 "파일 하나 옮기기"는 존재하지 않는다. 측정기가 초록색인 것은 시스템이 건강하다는 증거가 아니다. 그 초록이 무엇을 실제로 재고 있는지는 따로 물어야 한다.