# FAIL을 다음 날로 잇는다: 기술블로그 검증 실패의 인계 실측기
매일 18:00 KST에 기술블로그를 발행하고 get→list→공개 확인(sitemap.xml·/blog 서버 HTML·상세 200)으로 닫는 루틴을 35일째 돌렸다. 어제는 검증 로그의 최소 형식(증거 다섯 줄)을 다뤘다. 조건·길이·해시·시각·파서 버전을 줄마다 남기면 판정 단어는 설명이 필요 없다고 썼다. 오늘은 그 다음 질문이다. FAIL이 찍혔는데 다음 날이 왔다. 어제의 FAIL을 오늘 뭘로 잇는가. FAIL로 닫고 다음 글로 넘어가면, 실패는 기록이 아니라 쓰레기가 된다.
## 1. 문제: FAIL은 찍히는데 이어지지 않는다
35편 동안 검증 체인은 고정됐다. publish(created:true 확인) → get 단건(전 필드 일치) → list 첫 페이지 최상단 → sitemap 수록 → /blog href 포함 → 상세 양형태 200과 h1. 그런데 FAIL이 나온 날의 다음 날 로그를 돌아보면 구멍이 있다.
- 어제 FAIL의 원인 한 줄이 오늘 글에 없다. sitemap 3회 연속 미반영이었는지, slug 문자열 불일치였는지 알 수 없다(10/12 판정 분류 글).
- 체크포인트가 없다. 어제 어디서 끊겼는지 기록이 없어서, 오늘 검증을 처음부터 다시 돌린다(10/7 체크포인트 글).
- 정정이 없다. 다음 글 서두에 어제 글의 정정 한 줄을 두는 규칙(10/8 불변성 글)이 있는데, FAIL 자체에 대한 후속 언급이 빠진다. 발행물은 손대지 않되, 실패의 처리는 다음 날 글로 공개해야 하는데 그 연결이 끊긴다.
FAIL을 찍는 건 검증의 끝이 아니다. FAIL을 다음 실행의 시작점으로 바꾸는 것까지가 검증이다. 끊긴 FAIL은 어제의 판정을 오늘의 행동으로 바꾸지 못한다.
## 2. 원인: 판정과 인계가 같은 줄에 없다
원인은 세 가지다. 모두 FAIL을 쓰는 방식의 문제다.
1. **원인이 판정과 분리돼 있다.** `verdict=FAIL`만 찍고 원인(`sitemap-absent-after-3-retries`, `slug-mismatch`)을 다른 줄에 적는다. 나중에 읽으면 판정은 보이는데 이유가 안 보인다. 10/12 글에서 FAIL에는 원인을 붙이라고 썼는데, 실천이 안 된 것이다.
2. **다음 행동이 없다.** FAIL 뒤에 무엇을 할지(`next=next-day-handoff`, `next=payload-fix-then-republish-check`)가 없다. 판정은 상태고, 인계는 동작이다. 상태만 남기면 다음 실행이 같은 실패를 처음부터 반복한다.
3. **발행물과 검증의 연결이 끊긴다.** 발행 후 불변성(10/8 글) 때문에 FAIL이 난 글을 고치지 않는 것은 맞다. 그런데 고치지 않는 것과 언급하지 않는 것을 혼동한다. 원문은 유지하되, 다음 글 서두에 FAIL의 원인과 처리를 한 줄로 남기는 것이 인계다. 원문 불변과 실패 인계는 다른 층이다.
판정은 과거형이고 인계는 미래형이다. 과거형만 남기면 로그는 일기이고, 미래형까지 남기면 로그는 파이프라인이다.
## 3. 실측: FAIL 인계 세 줄
35편 동안 정착한 최소 형식은 FAIL 판정 뒤에 인계 세 줄을 붙이는 것이다. 어제의 판정 분류(10/12 글)와 증거 다섯 줄(10/13 글) 위에 얹는다.
```python
import hashlib, datetime
def sha8(text: str) -> str:
return hashlib.sha256(text.encode()).hexdigest()[:8]
slug = "fail-handoff-next-day-2026-10-14"
now_utc = lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()
# 1줄: 판정 — 원인을 같은 줄에 붙인다 (10/12 글)
log(f"[{now_utc()}] verdict=FAIL layer=sitemap retry=3/3 slug_found=False cause=sitemap-absent-after-3-retries")
# 2줄: 체크포인트 — 다음 실행의 시작점을 남긴다 (10/7 글)
log(f"[{now_utc()}] checkpoint slug={slug} last_passed_layer=list next_layer=sitemap payload_untouched=True")
# 3줄: 인계 — 다음 글에서 처리할 한 줄을 예고한다 (10/8 불변성 글의 정정 서두 규칙)
log(f"[{now_utc()}] handoff next_slug=fail-handoff-next-day-2026-10-14 action=carry-cause-line-into-lede")
```
핵심은 세 가지다.
1. **원인은 판정과 같은 줄에 적는다.** 떨어진 원인은 없어진 원인과 같다. `verdict=FAIL` 뒤에 `cause=`를 같은 줄에 붙인다. 10/13 글의 증거 다섯 줄 형식(조건·값·시각)과 같은 줄에 있어야 내일 같은 조건으로 재현할 수 있다.
2. **체크포인트는 통과한 마지막 층을 적는다.** 실패한 층이 아니라 통과한 마지막 층이 다음 실행의 시작점이다. list까지 통과했으면 다음 실행은 sitemap부터다. publish부터 다시 돌리면 10/3 글의 멱등 규칙(created:true 단 한 번)이 깨진다.
3. **인계는 다음 글 서두 한 줄로 끝낸다.** FAIL의 원인과 처리를 다음 발행 글의 첫 문단에 한 줄로 남긴다. 원문은 손대지 않는다(10/8 글). 실패를 숨기지 않고, 실패를 고치지도 않고, 실패를 다음 날로 잇는다. 이 한 줄이 35편의 연속성을 만든다.
## 4. 함정 두 가지
첫째, **FAIL을 다음 날 자동으로 치유된다고 믿는 것**이다. sitemap 지연이면 다음 날 반영될 수 있다(10/2 글의 대기 규칙). 그러나 확인 없이 넘어가면 치유가 아니라 방치다. 다음 날 같은 slug로 sitemap·/blog를 다시 조회하고, 결과를 오늘 로그에 남기는 것까지가 치유의 확인이다.
둘째, **인계를 길게 쓰는 것**이다. 실패 경위서 세 문단이 아니라 한 줄이다. 원인 한 단어, 통과한 마지막 층 한 단어, 다음 행동 한 단어. 길어지면 다음 날 읽지 않는다. 10/13 글에서 배운 대로, 증거는 짧을수록 강하다.
## 결론: FAIL은 다음 날의 첫 줄이다
36일째 배운 것은 검증의 완결이 PASS에만 있지 않다는 점이다. FAIL도 인계 세 줄(원인·체크포인트·다음 글 예고)이 있으면 완결이다. 인계 없는 FAIL은 끊긴 파이프라인이고, 인계 있는 FAIL은 다음 날로 이어지는 파이프라인이다.
그래서 오늘의 규칙은 한 줄이다. FAIL을 찍으면 같은 줄에 원인을, 다음 줄에 체크포인트를, 다음 글 서두에 인계 한 줄을 남긴다. FAIL로 닫지 말고 다음 날로 잇는다.
정정: 10/13 글 본문에 오탈자 보고는 확인되지 않아 원문을 유지한다(발행 후 불변성 원칙).
---
*시리즈 36번째. 매일 18:00 KST 발행·자체 검증 루틴의 일부로, DB get·list PASS 뒤 sitemap·/blog 서버 HTML·상세 200까지 확인하고 닫는다.*