사람이 읽을 로그와 기계가 셀 로그를 한 파일에 겹쳤더니 2026 – 볼트 스크립트 233개 중 138개가 마크다운을 정규식으로 읽고 있었다

오늘 오전에 제 감사 스크립트가 “라우팅 로그의 41.4%가 기록되지 않았다”는 숫자를 냈습니다. 틀린 숫자였고, 같은 날 정정했습니다. 실제 누락은 0건이었습니다.

정정하면서 남은 질문이 이거였습니다. 이게 그 로그 하나만의 문제인가, 아니면 내 볼트 전체가 같은 구조로 돼 있는가. 그래서 세어 봤습니다.

233개 중 138개

제 볼트의 .claude/scripts에는 파이썬 스크립트가 233개 있습니다. 그중 마크다운 파일을 열면서 정규식을 함께 쓰는 스크립트가 138개, 전체의 59%입니다.

세는 기준은 단순하게 잡았습니다. 코드 안에 .md 경로나 *.md 글롭이 등장하고, 동시에 re.compile이나 re.findall 같은 정규식 호출이 있는 파일을 셌습니다. 마크다운을 열기만 하고 통째로 다른 데 넘기는 스크립트는 빠지고, 실제로 내용을 뜯어 읽는 쪽만 남습니다.

59%라는 비율 자체는 사실 놀랍지 않습니다. 옵시디언 볼트를 자동화하면 데이터가 거의 다 마크다운이니 당연합니다. 문제는 그 마크다운을 누가 어떤 규약으로 쓰는가입니다.

그 파일은 두 명이 쓴다

오늘 저를 틀리게 만든 캡처 로그를 예로 들면, 이 파일에는 글쓴이가 둘입니다.

첫 번째는 셸 스크립트입니다. reminders-capture.sh가 미리알림을 읽어 헤더와 불릿을 씁니다. 이 부분은 결정적입니다. 헤더의 건수와 불릿 줄이 같은 변수에서 나오기 때문에 어긋날 수가 없습니다.

두 번째는 라우팅을 수행하는 LLM 에이전트입니다. 이쪽이 그 불릿을 읽고 처리한 다음, 원래 불릿 줄을 지우고 그 자리에 결과 줄을 씁니다. - <https://...가> > URL 요약 완료 → [[260910_[요약] ...]]로 바뀌는 식입니다. 추가가 아니라 교체입니다.

제 감사 스크립트는 첫 번째 글쓴이의 형식만 알고 있었습니다. 불릿만 셌으니 두 번째 글쓴이가 처리를 잘 끝낸 항목일수록 카운터에서 사라졌습니다. 정상 동작이 결손으로 집계되는 구조였습니다.

규약이 적힌 곳은 어디에도 없었다

여기서 제일 불편한 걸 확인했습니다. 저 결과 줄 형식이 어디에 정의돼 있는지 볼트 전체를 뒤졌습니다.

"URL 요약 완료"라는 문자열을 스크립트, 스킬 문서, 에이전트 정의 전부에서 검색했더니 코드에는 한 건도 없었습니다. 스킬 문서에도 없고 에이전트 지시문에도 없습니다. 라우팅을 담당하는 스킬 문서에는 목적지 목록과 판단 기준은 적혀 있지만, 결과를 파일에 어떤 모양으로 남기라는 지시는 없습니다.

즉 이 형식은 누가 정한 게 아니라 생긴 것입니다. LLM 에이전트가 처음 라우팅할 때 적당히 고른 표기를 이후에도 일관되게 반복했고, 그 일관성이 충분히 높아서 마치 규격처럼 보였습니다. 하지만 규격 문서는 존재한 적이 없습니다.

검색 결과 중 유일하게 그 문자열을 담고 있던 코드는 제가 오늘 아침에 쓴 감사 스크립트였습니다. 볼트에서 이 규약을 문서화한 유일한 파일이 그 규약을 틀리게 이해한 파일이었다는 뜻입니다.

왜 조용히 틀리는가

이 조합이 위험한 이유는 실패 방식 때문입니다.

규약이 없는 마크다운을 정규식으로 읽으면, 형식이 어긋나도 예외가 나지 않습니다. 정규식은 매칭되는 줄만 세고 나머지는 그냥 지나갑니다. 파일이 깨진 것도 아니고 스크립트가 죽지도 않습니다. 결과 숫자만 조용히 틀립니다.

게다가 그 숫자는 정밀해 보입니다. 제 스크립트가 낸 값은 “41.4%”였습니다. 소수점 한 자리까지 찍혀 나오니 계산이 정교하다는 인상을 줍니다. 실제로는 분자가 통째로 잘못 정의돼 있었는데, 출력 형식이 그걸 가려 줬습니다.

JSON이나 JSONL이었다면 이렇게 안 됩니다. 스키마가 바뀌면 키가 없어서 예외가 나거나, 최소한 None이 튀어나와 눈에 띕니다. 마크다운 정규식은 그 방어선이 없습니다.

그럼 전부 JSONL로 바꾸면 되나

처음엔 그렇게 결론 내리려다 멈췄습니다. 두 가지 때문입니다.

첫째, 사람이 읽는 게 이 파일의 주 목적입니다. 불릿을 결과로 교체하는 설계는 파일을 열었을 때 아직 처리 안 된 항목만 불릿으로 남아 한눈에 보이게 하려는 것입니다. 옵시디언에서 직접 열어 보는 노트라면 이건 좋은 선택입니다. 기계가 세기 편하자고 사람 쪽 가독성을 버리면 원래 용도를 잃습니다.

둘째, 이번 사건에서 로그는 아무 잘못도 하지 않았습니다. 12개 파일 전부 선언 건수와 실제 기록이 정확히 일치했고, 병합한 사실까지 주석으로 남아 있었습니다. 멀쩡한 로그 형식을 감사 스크립트 하나 잘못 짰다고 갈아엎는 건 과잉 반응입니다.

그래서 정한 규칙 세 개

대신 제가 지킬 규칙을 정했습니다. 형식을 바꾸는 게 아니라 읽는 쪽 순서를 바꾸는 규칙입니다.

규칙 1. 로그를 데이터로 쓰기 전에 그 로그를 쓰는 코드를 먼저 읽는다. 이번엔 산출물만 보고 세다가 틀렸습니다. reminders-capture.sh를 30초만 봤으면 헤더 건수와 불릿이 같은 변수에서 나온다는 걸 알았을 테고, 그러면 “선언과 기록이 다르다”는 관찰에서 가능한 원인이 “내가 잘못 세고 있다” 하나로 좁혀집니다.

규칙 2. 글쓴이가 둘 이상인 파일은 글쓴이 수만큼 형식을 확인한다. 스크립트가 쓴 형식만 보고 에이전트가 덧쓴 형식을 놓쳤습니다. 파일 하나에 형식이 하나일 거라는 가정이 틀렸던 겁니다.

규칙 3. 집계 결과에 내부 모순이 보이면 그때 멈춘다. 제 스크립트는 “항목 41건, 판정 47건”을 같이 출력하고 있었습니다. 41건에서 47개 판정이 나올 수 없는데, 저는 조사 대신 그럴듯한 설명을 만들어 원고에 적었습니다. 숫자 두 개가 안 맞을 때 설명을 지어내는 건 조사가 아니라 은폐에 가깝습니다.

세 규칙 중 실제로 사고를 막았을 건 규칙 1입니다. 나머지 둘은 규칙 1을 놓쳤을 때의 2차 방어선입니다.

규칙 3은 코드로 굳힐 수 있어서 그렇게 했습니다. 이 로그에는 “선언한 건수와 집계한 건수가 같아야 한다”는 불변식이 있습니다. 감사 스크립트가 그 차이를 계산해서 출력하게 해 두면, 규약을 잘못 이해한 순간 차이가 0이 아닌 값으로 나옵니다. 실제로 제가 정정 작업을 하면서 한 번 더 틀렸을 때도 이 값이 먼저 잡아냈습니다. 병합 주석을 중복으로 세는 바람에 한 파일이 7건 대신 10건으로 집계됐고, 차이가 마이너스로 찍히면서 드러났습니다. 정답을 모르는 상태에서도 “뭔가 안 맞는다”는 건 알 수 있게 해 주는 장치입니다.

LLM이 쓰는 로그라면 한 가지 더

이번 건에서 두 번째 글쓴이는 사람이 아니라 LLM 에이전트였습니다. 여기엔 사람 협업자와 다른 성질이 하나 있습니다.

사람이라면 형식을 조금씩 바꿔 쓰다가도 “우리 이거 형식 정하자”는 대화가 언젠가 나옵니다. LLM 에이전트는 그 대화를 먼저 꺼내지 않습니다. 매번 문맥을 보고 적당히 일관된 표기를 고르고, 그게 꽤 잘 유지돼서 규격이 있는 것처럼 보이는 상태가 오래 갑니다. 제 경우 최소 12일치 로그에서 형식이 흔들리지 않았습니다.

그래서 에이전트에게 파일 쓰기를 맡길 때는, 출력 형식을 지시문에 박아 두거나 아니면 최소한 지금 쓰이고 있는 형식을 관찰해서 문서로 남기는 단계가 필요합니다. 지금 제 볼트에는 그 단계가 없었고, 그 공백을 제가 오늘 오전에 잘못된 추측으로 채웠습니다.

한계

이 글의 138이라는 숫자는 제 볼트 한 곳을 센 값이고, 판정 기준도 제가 임의로 정한 정규식입니다. 마크다운을 읽되 정규식을 안 쓰는 파서나, 정규식을 쓰되 마크다운이 아닌 대상을 보는 스크립트는 이 분류에서 빠지거나 잘못 들어갔을 수 있습니다. 비율의 정확한 값보다 “절반 이상”이라는 규모만 신뢰하시는 게 안전합니다.

그리고 138개 각각이 실제로 위험한지는 세지 않았습니다. 이 글이 확인한 건 “마크다운을 정규식으로 읽는 스크립트가 많다”와 “그중 최소 한 곳에서 실제로 조용히 틀린 숫자가 나왔다”까지입니다. 나머지 137개 중 몇 개가 같은 상태인지는 아직 모릅니다. 그걸 세는 게 다음 작업입니다.

FAQ — 자주 묻는 질문

Q1. 138개라는 숫자는 어떻게 셌나요?

A: .claude/scripts의 파이썬 파일 233개 중, 코드에 .md 경로나 글롭이 있고 동시에 re.compile·re.findall·re.search·re.match 중 하나를 쓰는 파일을 셌습니다.

Q2. 결과 줄 형식이 정말 어디에도 없나요?

A: 스크립트, 스킬 문서, 에이전트 정의를 전부 검색했을 때 해당 문자열을 쓰는 코드는 없었습니다. 로그 파일 자체와, 오늘 제가 쓴 감사 스크립트에만 있었습니다.

Q3. 그냥 로그를 JSONL로 바꾸는 게 낫지 않나요?

A: 이 파일은 사람이 옵시디언에서 직접 열어 보는 노트라 가독성이 주 목적입니다. 그리고 이번 사건에서 로그 자체는 완전했습니다. 판정 이력을 기계가 대량으로 소비할 일이 생기면 그때 별도 적재를 추가하는 쪽이 맞다고 봅니다.

Q4. 정규식 대신 마크다운 파서를 쓰면 해결되나요?

A: 부분적으로만 해결됩니다. 파서는 문서 구조는 정확히 읽지만, “불릿이 결과 줄로 교체된다”처럼 의미 층위의 규약은 여전히 모릅니다. 이번 오류는 문법이 아니라 의미를 몰라서 났습니다.

Q5. LLM 에이전트에게 형식을 어떻게 강제하나요?

A: 지시문에 출력 템플릿을 박아 두는 게 가장 확실합니다. 이미 형식이 굳어진 로그라면, 먼저 현재 쓰이는 형식을 관찰해 문서로 만든 다음 그 문서를 지시문에 넣는 순서가 안전합니다.

관련 글

참고 자료

  • TypeSafe AI 모델 문서 — 이 시리즈의 출발점인 Jev 스펙: docs.typesafe.ai/models (2026-09-21 확인)
  • TypeSafe AI, Introducing System One Models & Jev (2026-09-15) (2026-09-21 확인)
  • 이 글의 집계 대상은 공개 저장소가 아닌 제 개인 볼트입니다. 검증에 쓴 파일은 .claude/scripts/reminders-capture.sh(로그 생성), .claude/scripts/capture_log_audit.py(집계), 00.Inbox/*_capture.md(대상 로그 12개)입니다. 숫자는 각자의 환경에서 같은 기준으로 다시 세어야 의미가 있습니다.