문서는 커밋에 딸려오지 않는다
새 프로젝트 두 개를 사이트에 올리려고 README 를 읽었습니다. 카드에 무엇을 쓸지 정하려면 그 앱이 무엇을 하는지부터 알아야 했습니다.
읽은 대로 쓰기 전에 코드를 열어 봤더니 README 가 틀려 있었습니다. 같은 종류의 틀린 문장을 한자리에서 세 군데 더 찾았습니다.
한 커밋 만에 낡았다
번역본 추천 앱의 README 에는 국립중앙도서관에서 도서를 모아 오고 취향 다섯 축으로 순위를 매긴다고 적혀 있었습니다. 그럴듯한 설명이었고, 파일 맨 위에 있어서 그 저장소에서 가장 먼저 눈에 띄는 문장이었습니다.
코드에는 국립중앙도서관이 한 글자도 없었습니다. 검색은 알라딘 한 곳으로 합쳐져 있었고, 취향 축은 넷으로 바뀌어 있었습니다. 커밋 기록을 보니 README 는 첫 커밋에서 쓰였고, 바로 다음 커밋이 데이터 출처를 통째로 바꾸면서 README 는 건드리지 않았습니다. 딱 한 커밋 만에 낡은 것입니다.
같은 문서의 Claude 모델 이름도 실제와 달랐고, 옆 프로젝트의 설정 문서도 모델을 다른 이름으로 적고 있었습니다. 셋 다 모양이 같습니다. 코드에서 바뀐 값이 문서에만 예전 그대로 남아 있었습니다.
코드는 왜 이렇게 되지 않나
흥미로운 점은 코드 쪽이 오히려 깨끗했다는 것입니다. 국립중앙도서관을 걷어 낼 때 관련 파일과 호출, 남은 변수까지 전부 지워서 어중간하게 남은 것이 하나도 없었습니다.
문서가 낡은 것이 보이지 않았던 이유가 바로 거기에 있습니다. 코드에 찌꺼기가 남아 있었다면 언젠가 컴파일러나 테스트에서 걸렸을 텐데, 깨끗이 지웠으니 걸릴 것이 없었습니다. 남은 것은 아무도 실행하지 않는 문장 하나뿐이었습니다.
하마터면 사이트에 그대로 올릴 뻔했다
문제는 저장소 안에서 끝나지 않았습니다. 저는 그 README 를 근거로 공개 사이트의 카드를 쓰려던 참이었습니다. 그대로 썼다면 없는 데이터 출처와 틀린 축 개수가 사이트에 올라갔을 것입니다.
더 민망한 것은 이 사이트의 자동화 지침에 이미 그러지 말라고 적혀 있었다는 점입니다. "커밋 메시지를 그대로 옮기지 말 것. 반드시 해당 소스 파일을 열어 확인한 뒤 쓴다." 전에 같은 사고가 났을 때 제가 적어 둔 문장이고, 코드가 맞고 문서가 틀렸다는 설명을 괄호로 붙여 두기까지 했습니다.
적어 두는 것과 막는 것은 다릅니다. 그 문장은 제가 그때 마침 읽었기 때문에 지켜졌을 뿐, 읽지 않았다면 또 하나의 문서로 남았을 것입니다. 지난번에도 그랬듯이 적어 둔 규칙만으로는 아무것도 막지 못합니다.
대조할 수 있게 쓰기
다른 앱의 README 를 새로 쓸 때는 방식을 바꿨습니다. 다 쓴 뒤에 문서가 주장하는 값과 코드에 있는 값을 나란히 뽑아 봤습니다.
Claude 모델 README=claude-sonnet-4-6 코드=claude-sonnet-4-6
검색 상한 README=4/8 코드=hasResearch ? 4 : 8
배포 타깃 README=18.0 코드="18.0"
번들 ID README=com.kidstay.app 코드=com.kidstay.app
이력 상한 README=50 코드=maxCount = 50
온디바이스 게이트 README=iOS 26.0 코드=@available(iOS 26.0
발달 단계 수 README=8 코드=8
여기서 배운 것은 "문서를 잘 갱신하자"보다 "문서를 대조할 수 있는 모양으로 쓰자"에 가깝습니다. 모델 이름, 상한값, 번들 ID, 배포 대상처럼 코드에 같은 문자열로 들어 있는 값은 눈으로 읽지 않아도 기계가 맞춰 봅니다. 문서를 쓸 때 이런 값을 일부러 적어 두면, 나중에 그 문서가 낡았는지 물어볼 수 있는 기준이 생깁니다.
대조할 수 없는 것도 있다
모든 문장이 그렇게 되지는 않습니다. 그 README 에는 "API 키가 없어 실제 서버로는 아직 돌려 보지 못했다"는 줄도 있었습니다.
이 내용은 코드 어디에도 없고, 사실인지는 그 문장을 쓴 사람만 압니다. 저는 확인할 방법이 없어서 손대지 않고 그대로 두었습니다. 고칠 수 없는 것을 고친 척하는 쪽이 낡은 문장을 남겨 두는 것보다 나쁘다고 봤습니다.
정리하면
- 코드에 적은 틀린 내용은 여러 단계가 막아 주지만, 문서에 적은 틀린 내용은 아무것도 막지 않습니다. 틀려도 아무 일이 일어나지 않는다는 것이 문제입니다.
- 코드를 깨끗하게 지울수록 문서가 낡은 것은 더 안 보입니다. 걸릴 찌꺼기가 남지 않기 때문입니다.
- "이러지 말자"고 적어 두는 것만으로는 막지 못합니다. 저는 제가 적어 둔 경고를 읽고도 코드를 열어 보고 나서야 알았습니다.
- 모델 이름, 상한값, ID처럼 코드에 같은 문자열로 있는 값은 일부러 문서에 적어 두면 기계가 대신 맞춰 봅니다. 판단과 상태는 대조할 수 없으니 손대지 않습니다.
문서가 단언한 수를 사람 대신 검사가 대조하게 만든 뒤의 이야기는 계측기를 놓은 날에 있습니다.
I opened two projects' READMEs so I could add them to this site. To write a card for an app you have to know what the app does.
Instead of writing from what I read, I opened the code — and the README was wrong. Then, in one sitting, I found the same kind of lie in three more places.
It went stale in one commit
The README for the translation-picking app said this: it collects books from the National Library, and ranks them on five taste axes. It was plausible, it was at the top of the file, and it was the first sentence anyone would read in that repo.
The code contained not one mention of the National Library. Search had been consolidated into a single bookstore API and the taste axes were now four. The log showed the README was written in the first commit, and the very next commit replaced the data source wholesale without touching it. One commit was all it took.
In the same file the Claude model name was wrong too, and a neighbouring project's setup doc named a different model than its code did. All three have the same shape — a value that changed in code and stayed put in prose.
Why code doesn't rot this way
The interesting part is that the code was clean. When the National Library went, so did its file, its call sites, and every leftover variable. Nothing was left half-done.
Which is exactly why the stale doc was invisible. Had there been debris in the code, a compiler or a test would eventually have tripped on it — but it was removed properly, so there was nothing left to trip. All that remained was one sentence nobody executes.
I nearly published it as-is
This did not stay inside the repo. I was about to write a public card on this site using that README as my source. Had I done it, a data source that does not exist and a number of axes that does not exist would have gone up on the site.
What makes it worse is that this site's own automation notes already told me not to. "Never copy a commit message across. Open the source file and confirm what is actually true first" — a line I wrote myself after the same thing happened once before. Parenthetical included: the code is right and the doc is wrong.
Writing it down and enforcing it are different things. That sentence worked because I happened to read it; unread, it would have been one more document. As last time, and the time before, a rule is not a gate.
So write claims you can diff
Writing the other app's README, I changed the approach. Once it was written, I printed what the document claimed next to what the code held.
Claude model README=claude-sonnet-4-6 code=claude-sonnet-4-6
search cap README=4/8 code=hasResearch ? 4 : 8
deployment target README=18.0 code="18.0"
bundle id README=com.kidstay.app code=com.kidstay.app
history cap README=50 code=maxCount = 50
on-device gate README=iOS 26.0 code=@available(iOS 26.0
dev stage count README=8 code=8
The lesson here is not "keep your docs updated." It is write documents in a shape you can check. Model names, caps, bundle ids, deployment targets — anything that exists in the code as the same literal string can be matched by a machine rather than read by a person. Putting those values in deliberately gives you a question you can ask later: is this document still true?
Some things cannot be diffed
Not everything converts. That README also contained this line: "with no API key, this has not been run against the real servers yet."
That appears nowhere in the code. Whether it is still true is known only to the person who wrote it. I had no way to check, so I left it exactly as it was. Pretending to fix what you cannot verify is worse than leaving a stale sentence standing.
In short
- Code has several layers that catch a false claim; prose has none. The problem is that nothing happens when it is wrong.
- The more cleanly you delete from code, the less visible the stale doc becomes — there is no debris left to trip over.
- Writing "don't do this" down is not a gate. I had written the warning myself and still only caught it by opening the code.
- Values that live in code as literal strings — model names, caps, ids — can be checked by machine if you put them in on purpose. Judgements and status cannot, so leave those alone.
What happened after the numbers a document asserts were handed to a check to compare is in The Day the Gauges Went In.