가르니까 참이던 문장이 조용히 거짓이 됐다
발행물을 만드는 자동화가 하나 있었습니다. 매주 월요일 새벽에 혼자 깨어나 자료를 조사하고, 카드를 그리고, 네 군데로 내보냅니다.
보는 시간 단위가 서로 달라서 이 작업을 셋으로 나눴습니다.
일간 화·금 06:00 발행 사이에 생긴 사건 3~5장
주간 월 06:00 추세 전체 + 차트 11장
월간 11일 07:00 순환 국면 (원자료 공시 기준) 7장
코드 쪽은 깔끔했습니다. 각자 자기 일정대로 돌고, 서로를 깨우지 않고, 잘 돌았습니다.
낡은 것은 코드보다 문장 쪽이었습니다.
여섯 문장
나눈 뒤에 문서를 훑어보니, 아무 소리 없이 거짓이 된 문장이 여섯 개 있었습니다. 모두 쓸 때는 참이었습니다.
“Drive 폴더의 교체는 이 작업만 한다.”
“재고 순환 작업도 같은 이유로 Drive 를 건너뛴다.”
“월=주간, 수=재고 순환, 목=구조 변화.”
“빈도 선택지에 분기가 없다.”
“Drive 는 루트(주간) / 하위폴더(일간)로 갈라 쓴다.”
“매주 월요일 06시 … 카드뉴스 10장.”
마지막 두 문장은 제 공개 사이트에 있던 것입니다. 발행 주기도 장수도 틀렸습니다. 장수는 작업을 나눈 것과 상관없이, 카드가 한 장 늘었을 때부터 이미 틀려 있었습니다.
이런 문장은 테스트에 걸리지 않는다는 점이 중요합니다. 거짓이 된 문장은 예외를 던지지 않고, 그 자리에 그대로 남아 읽는 사람에게 더는 없는 상황을 설명합니다.
가장 위험한 것은 결론이 맞는 문장이었다
여섯 가운데 하나는 성질이 달랐습니다. 분기 작업의 문서에 이렇게 적혀 있었습니다.
“주간이 그 폴더를 매주 전부 비우고 교체한다. 여기 올리면 서로를 지운다. 그러니 Drive 를 건너뛴다.”
분기 작업은 지금도 Drive 를 건너뜁니다. 결론은 맞습니다. 이유가 완전히 달라졌을 뿐입니다. 주간 작업은 더 이상 루트를 비우지 않고, 일간·주간·월간 셋이 하위 폴더를 하나씩 나눠 쓰게 됐습니다. 지금 건너뛰는 진짜 이유는 분기 몫의 하위 폴더가 아직 없다는 것 하나입니다.
결론이 맞으면 아무도 다시 보지 않습니다. 틀린 결론은 언젠가 사고를 내서 스스로 드러나지만, 근거만 틀린 문장은 조용합니다. 그러다 누군가 "주간이 이제 루트를 비우지 않으니 올려도 되겠네" 하고, 사라진 근거 위에서 그럴듯한 판단을 내려 되살립니다.
결론은 그대로 두고 근거만 바꿨습니다. 무엇을 하는지는 그대로이고, 왜 그렇게 하는지를 고친 것입니다.
문장만 낡은 것이 아니었다
나누기 전에는 한 작업이 원자료를 한 번 읽어 카드를 모두 그렸습니다. 나눈 뒤에는 두 작업이 각자 원자료를 읽고, 둘 다 가장 최근 달을 가져옵니다.
평소에는 같은 달을 가져오지만, 원자료 공시가 늦어지는 달이 있습니다. 그러면 월간은 발송 기록이 잡아 둔 지난달 기준으로 나가고, 같은 주의 주간 요약 카드는 이번 달 기준으로 나갑니다.
월간 발행물 6월치
주간 요약 카드 7월치 ← 같은 지표, 다른 달
두 발행물을 함께 받는 사람에게는 같은 이름의 지표가 다른 값으로 보입니다. 어느 쪽도 계산을 틀리지 않았는데도 그렇습니다.
둘 다 최신 달을 보게 하면 월간이 자기 발송 기록과 어긋나기 때문에 그 방법은 쓰지 않았습니다. 대신 주간 요약을 월간이 마지막으로 내보낸 기준월에 고정했습니다. 그 달 자료가 없는 항목은 억지로 옮기지 않고, 맞추지 못했다는 사실을 보고서에 남깁니다.
여기서 정직하게 적어 둘 것이 있습니다. 이 어긋남은 아직 한 번도 일어나지 않았습니다. 쌓인 원자료 스냅샷이 한 달 치뿐이라 갈릴 기회가 없었고, 이 경로는 겪고 나서 고친 것이 아니라 코드를 따라가다 찾은 것입니다.
막으려고 넣은 코드가 정상 동작을 사고로 표시했다
고정했다는 사실을 보고서에 남기는 부분을 만들고 돌려 보니 이렇게 나왔습니다.
[6] 데이터 품질
· 수집 실패: 주간 요약 카드용으로 월 단위 축을 기준월 2026-07 에 고정했다
고정은 실패가 아니라 의도한 동작인데, 그 사실을 남길 자리로 skipped(수집 실패) 목록을 골랐더니, 그 목록을 출력하는 함수가 앞에 "수집 실패:"를 붙였습니다.
우스운 것은 그 함수 몇 줄 아래에 이미 이런 주석이 있었다는 점입니다.
# 설정 문제를 “수집 실패” 한 줄로 뭉개면 뭘 고쳐야 할지 모른다
같은 파일이 같은 규칙을 이미 알고 있었는데, 새로 들어온 코드는 그 규칙을 모르는 채 들어왔습니다. 주석은 자기가 들어 있는 파일조차 강제하지 못합니다. 목록을 나눠 · 기준월 고정: 줄로 따로 출력하도록 고쳤습니다.
문서를 믿었다면 보지 못했을 것
폴더 구조를 문서에 반영하면서, 문서만 고치지 않고 실제 폴더를 열어 보기로 했습니다. 루트에 하위 폴더 셋만 있고 흩어진 파일이 없다는 것은 맞았습니다.
주간 폴더에는 카드가 10장 있었습니다. 덱은 11장입니다.
주간/ card01.png … card10.png (10개)
card11.png ← 없다
적어도 한 회차는 마지막 카드가 빠진 채로 올라갔다는 뜻입니다. 복사 명령이 card*.png 라서 원본에 있었다면 함께 복사됐을 테니, 원본 폴더에 없었을 가능성이 큽니다. 그래도 어느 회차에서 빠졌는지는 밝히지 못했습니다. 다음 실행에서 눈으로 확인하라는 경고를 검증 절에 달아 두는 것까지가 이날 한 일입니다.
같은 문서의 다른 두 자리가 아직 "10"이라고 적고 있던 것도 이때 드러났습니다. 사진 앱으로 가져왔는지 확인하는 "정확히 10 증가"와 삭제 절차의 "10개 선택됨"입니다. 덱이 11장이 된 뒤로 고치지 않은 자리였습니다.
고칠 때 숫자를 11 로 다시 적지 않았습니다. "카드 장수만큼", "폴더 안 카드 전부"로 바꿨습니다. 같은 저장소의 다른 문서가 이미 "문서의 숫자 말고 폴더를 믿는다"고 적어 두고 있었기 때문입니다. 숫자를 적는 순간 그 자리는 다음 변경 때 다시 낡습니다.
조심하는 것만으로는 안 된다
여기까지가 "찾아서 고쳤다"입니다. 문제는 이 일을 매번 사람이 해야 한다는 점이고, 이번에도 네 가지는 우연히 눈에 걸린 것이었습니다.
한 자리에는 사람 대신 대조 도구를 두었습니다. 월간 작업의 문서는 같은 지침을 두 벌 담고 있습니다. 문서 위쪽의 절들과, 예약 화면에 붙여 넣을 코드 블록입니다. 지금은 글자까지 같지만 한쪽만 고치기 쉽고, 둘이 갈라져도 예약 작업이 실제로 돌 때까지 드러나지 않습니다. 그때는 이미 다른 지침으로 배포가 끝난 뒤입니다.
$ check-cycle-skill.py
같다 (1728자)
# 상단에 오타를 하나 심고 다시
$ check-cycle-skill.py
갈라졌다 — 위가 문서 상단, 아래가 붙여넣기 블록
--- 상단 0~5절
+++ 지침 블록
-/usr/bin/python3 run.py --mark-delivered-오타
+/usr/bin/python3 run.py --mark-delivered
exit=1
일부러 두 벌을 어긋나게 해 놓고 도구가 잡아내는 것까지 확인했습니다. 첫 시도에서는 잡히지 않았는데, 틀린 것은 도구보다 제 시험 쪽이었습니다. 두 벌 어디에도 속하지 않는 자리를 고쳐 놓고 "못 잡네" 할 뻔했습니다. 재는 자를 의심하려다 재는 방법은 의심하지 않은 셈입니다.
제가 새 거짓 문장을 심었다
여기서 끝났다면 깔끔한 이야기였겠지만, 그렇지 않았습니다.
작업을 나누느라 낡은 문장들을 고치고 나서, 제 사이트의 프로젝트 카드에 이번 작업을 이렇게 요약해 올렸습니다.
“같은 지표를 두 종이 다른 달로 말하고 있었음”
실제로 겪은 일처럼 읽히지만 그렇지 않았습니다. 앞에 적었듯이 이 어긋남은 아직 일어난 적이 없고, 그 사실을 이 글을 쓰려고 근거를 확인하다가 스냅샷이 한 달 치뿐인 것을 보고 알았습니다.
이 사이트의 전제는 "측정한 것과 추론한 것을 나눈다"입니다. 그 전제를 어긴 문장이 이틀 동안 걸려 있었습니다. 낡은 문장 여섯 개를 고치면서 새 문장 하나를 심은 것입니다.
그 문장은 "말할 수 있는 구조였음 … 아직 갈린 적은 없고 미리 막았음"으로 고쳤습니다. 이 문단을 지우지 않고 그대로 남겨 두는 것은, 이런 일을 조용히 고치고 넘어가면 또 하게 되기 때문입니다.
정리하면
- 작업을 나누면 문장이 거짓이 됩니다. 코드는 나눠도 돌지만, "이 작업만 한다" 같은 문장은 두 번째 작업이 생기는 순간 거짓이 됩니다.
- 거짓이 된 문장은 예외를 던지지 않습니다. 테스트에 걸리지 않고, 그 자리에 남아 더는 없는 상황을 설명합니다.
- 결론은 맞고 근거만 틀린 문장이 가장 위험합니다. 아무도 다시 보지 않고, 다음 사람이 사라진 근거 위에서 되살립니다.
- 작업을 나누면 산출물끼리도 어긋날 수 있습니다. 하나가 읽던 것을 둘이 각자 읽으면, 평소에는 같고 공시가 늦는 달에만 다릅니다.
- 주석은 자기 파일조차 강제하지 못합니다. 규칙이 적혀 있어도 새 코드는 그 규칙을 모르고 들어옵니다.
- 숫자를 문서에 적어 넣지 않습니다. "11장" 대신 "폴더 안 전부"로 적으면 다음 변경 때 낡지 않습니다.
- 사람의 조심을 대조 도구로 바꿀 수 있는 자리가 있습니다. 두 벌로 적힌 것은 기계가 매번 대신 봅니다.
- 고치는 김에 새 문제를 심을 수 있습니다. 이번에도 그랬습니다.
재는 자가 흔들려 없는 문제를 볼 뻔한 이야기는 흔들린 건 곡선이 아니라 재는 자였다에, 날짜를 지어내 놓고 날짜처럼 보여서 믿었던 이야기는 거짓 날짜도 날짜처럼 생겼다에 있습니다. 이 자동화를 문서 한 장에 담기로 한 처음 이야기는 매주 혼자 도는 자동화를 문서 한 장에 담기에 적어 두었습니다.
I had one automation that produces a publication. Every Monday before dawn it wakes on its own, researches, draws the cards, and sends them to four places.
I split it into three, because they watch different clocks.
daily Tue/Fri 06:00 news since last issue 3-5
weekly Mon 06:00 the trend, with charts 11
monthly 11th 07:00 the cycle phase 7
The code side was clean. Each runs on its own schedule, none wakes another, and they all work.
What died was not code. It was sentences.
Six sentences
Reading the documents afterwards, six sentences had become false without a sound. Every one of them was true when written.
“Only this job replaces the Drive folder.”
“The cycle job skips Drive for the same reason.”
“Mon = weekly, Wed = cycle, Thu = structural.”
“The frequency menu has no quarterly option.”
“Drive is split as root (weekly) / subfolder (daily).”
“Every Monday at 06:00 … ten card-news slides.”
The last two were on my own public site. Both the cadence and the count were wrong — and the count had been wrong since a card was added to the deck, quite apart from the split.
What matters is that none of this fails a test. A dead sentence throws no exception. It just sits there, describing a world that no longer exists to whoever reads it.
The dangerous one was the sentence whose conclusion still held
One of the six was a different animal. The quarterly job's document said:
“The weekly job empties and replaces that folder every week. Upload here and they erase each other. So skip Drive.”
The quarterly job still skips Drive. The conclusion is correct. The reason is not. The weekly job no longer empties the root; all three now write into their own subfolder. The real reason to skip today is one thing only — there is no subfolder for the quarterly job yet.
Nobody re-examines a sentence whose conclusion is right. A wrong conclusion eventually causes an incident and exposes itself; a sentence with only its reasoning dead stays quiet. Then somebody thinks “the weekly job doesn't empty the root any more, so this is fine now” and revives it — a plausible decision, made on a reason that no longer exists.
So the conclusion stayed and the reasoning was swapped out. What got fixed was not “what it does” but “why it does it”.
Sentences were not the only casualty
Before the split, one job read the source data once and drew every card. After it, two read it separately. And both reach for “the most recent month”.
Usually that is the same month. But some months the source publishes late. Then the monthly issue goes out on last month, held there by its delivery marker, while that same week's weekly summary card goes out on this month.
monthly issue June figures
weekly summary card July figures
^ same indicator, different month
To anyone receiving both, an indicator with one name shows two values — with neither side having computed anything wrong.
The fix was not “make them both take the latest”. That would put the monthly job at odds with its own delivery record. Instead the weekly summary is pinned to the reference month the monthly job last shipped. Any axis without data for that month is left alone, and the fact that it could not be pinned is written into the report.
One honest note. This divergence has never actually happened. Only one month of source snapshots has accumulated, so there has been no chance for it to. It is a path I traced through the code, not something I hit and then repaired.
The code meant to prevent it logged normal behaviour as a failure
I built the reporting side, ran it, and got this:
[6] Data quality
· collection failed: pinned monthly axes to 2026-07
Pinning is not a failure. It is the intended behaviour. But the place I chose to record it was the skipped list, and the function that prints that list prefixes it with “collection failed:”.
The funny part is what already sat a few lines below in the same function:
# flattening a configuration problem into one “collection failed” line hides what to fix
The same file already knew the rule. The new code simply arrived from outside it. A comment cannot enforce the file it lives in. The fix was to separate the lists and emit a distinct · pinned: line.
What the documents would never have shown me
While writing the folder layout into the docs, I decided to open the actual folder rather than only edit prose. The root did hold exactly three subfolders and no loose files, as claimed.
But the weekly folder held ten cards. The deck is eleven.
weekly/ card01.png ... card10.png (10)
card11.png <- missing
So for at least one issue, the last card never made it. The copy command is card*.png, so it would have followed had it existed in the source — which points at the source folder — but I could not determine which issue dropped it. Today's work ends at putting a warning in the verification step to check this by eye on the next run.
Two other places in the same document still said “10”: the Photos import check (“exactly 10 more”) and the deletion step (“10 selected”). Neither was updated when the deck became eleven.
Fixing them, I did not write 11 back in. They now say “as many as there are cards” and “everything in the folder”. Another document in the same repository had already put it well — “trust the folder, not the number in the document.” A number nailed into prose goes stale at the next change by construction.
Care is not a mechanism
All of the above is “found it and fixed it”. The problem is that it takes a person every time, and four of these were noticed by luck.
So in one place a comparator replaced the person. The monthly job's document carries the same instructions twice — as sections at the top, and as a code block meant to be pasted into the scheduler. They are identical to the character today, but only one side is easy to edit, and a split would not surface until the scheduled run fired. By then it has already deployed under the other instructions.
$ check-cycle-skill.py
identical (1728 chars)
# now plant one typo in the top section
$ check-cycle-skill.py
split - top is the document, bottom is the paste block
--- sections 0-5
+++ paste block
-/usr/bin/python3 run.py --mark-delivered-typo
+/usr/bin/python3 run.py --mark-delivered
exit=1
Verified by deliberately splitting them and watching it catch. The first attempt caught nothing — and it was my test that was wrong, not the comparator. I had edited a spot belonging to neither copy and nearly reported it as a miss. Suspecting the instrument, while forgetting to suspect how I was using it.
And then I planted a fresh false sentence
It would be a tidy story if it ended there. It doesn't.
Having fixed the sentences the split had killed, I summarised the work on a project card on my own site like this:
“Two of the series were stating the same indicator for different months”
That reads as something that happened. It did not. As written above, the divergence has never occurred — and I learned that while checking sources for this very post, on seeing that only one month of snapshots exists.
The premise of this site is separating what was measured from what was reasoned. A sentence breaking that premise sat on it for two days. While fixing six stale ones, I planted a new one.
It is corrected now — “could state … it has not happened yet; it was closed first”. And this section stays. Quietly fixing this kind of thing is how it happens again.
In short
- Splitting kills sentences. Code survives a split; “only this job does X” becomes false the moment a second job exists.
- Dead sentences throw no exceptions. No test catches them; they sit there describing a world that is gone.
- The most dangerous kind still has the right conclusion. Nobody re-reads it, and the next person revives it on reasoning that no longer exists.
- Splitting also splits the outputs. What one job read, two now read separately — identical most months, different exactly when the source is late.
- A comment cannot enforce its own file. The rule can be written down and new code still arrives from outside it.
- Don't nail counts into prose. “Everything in the folder” survives the next change; “11 cards” does not.
- Some care can be converted into a comparator. Anything written down twice can be checked by a machine every time.
- You plant new errors while fixing old ones. I did here.
A measuring instrument that moved, nearly making me chase a problem that wasn't there, is in It Was the Instrument Shaking, Not the Curve; believing invented dates because they looked like dates is in A False Date Looks Like a Date. The original decision to put this automation into a single document is in One Document for an Automation That Runs Alone.