옵시디언 고아 노트 464개는 271개였습니다 — 정리 스크립트가 틀리면 정리도 틀립니다

정기창·

옵시디언(Obsidian) vault 의 고아 노트, 그러니까 어느 노트에서도 링크되지 않은 노트를 점검 스크립트로 셌더니 464개가 나왔습니다. 실제로는 271개였습니다. 그 숫자를 믿고 고아 노트를 정리했다면 멀쩡한 노트 약 190개를 건드렸을 것입니다. 정리하는 동안 가장 많이 틀린 것은 노트가 아니라, 노트를 재는 도구였습니다.

지난 글에서는 이 vault 의 인덱스가 한 번 줄인 뒤 12주 만에 다시 약 네 배로 불어난 이유와, 층마다 크기 상한을 거는 해법을 적었습니다. 이 vault 는 Claude Code 에이전트가 장기 기억으로 쓰는 마크다운 노트 약 2,000개짜리 저장소이고, 자체 git 레포로 관리합니다. 그 정리에서 키워드를 고르거나 요약하는 것처럼 판단이 필요한 다시 쓰기는 서브에이전트에게 맡겼고, 옮기고 크기를 재고 대조하는 일은 스크립트가 했습니다. 그러니 스크립트가 내놓는 숫자가 곧 무엇을 옮기고 무엇을 고칠지의 근거였습니다.

이 글은 그 숫자들이, 그리고 숫자를 다루던 제 손이 틀렸거나 틀릴 뻔한 아홉 번의 기록입니다. 도구가 말한 수와 실제가 크게 어긋난 것부터 추리면 이렇습니다.

잰 것 도구가 말한 수 실제 어긋난 이유
고아 노트 464개 271개 경로를 붙인 링크를 알아보지 못함
깨진 링크 141회 113회 코드 안의 예시 문자열까지 셈
옮긴 폴더를 가리키던 옛 경로 참조 0건 4곳 찾을 때와 같은 방법으로 확인함
60일 넘게 커밋이 없던 프로젝트 폴더 42개 59개 제 정리 커밋을 활동으로 셈

틀린 까닭은 넷으로 묶입니다. 도구가 옵시디언의 규칙을 몰랐고, 검증이 오류를 잡을 수 없는 방법이었고, 정리 스크립트와 셸 자체에 결함이 있었고, 마지막으로 제 손이 틀렸습니다. 사례마다 어떻게 알아챘고 무엇으로 막았는지를 함께 적습니다. 한 번은 물리기 전에 막은 경우입니다.

재는 도구가 옵시디언의 링크 규칙을 몰랐습니다

측정에서는 도구가 재려는 것을 정말 재고 있는지를 타당도(validity)라고 부릅니다. 고아 노트를 세는 스크립트가 재야 하는 것은 "옵시디언에서 어느 노트에서도 링크되지 않은 노트"입니다. 그런데 스크립트가 아는 링크와 옵시디언이 아는 링크가 달랐습니다. 계산이 틀렸다기보다, 세는 대상이 처음부터 달랐습니다.

옵시디언 앱을 열어 백링크나 그래프를 보면 될 일 같지만, 이 vault 를 읽고 쓰는 쪽은 터미널에서 일하는 AI(Claude Code)이고 저는 앱을 거의 열지 않습니다. 점검은 세션 시작 훅이나 정리 스크립트 안에서 매번 같은 결과를 내야 해서 결정적인 스크립트로 잽니다. 사람이 보는 표면인 앱 화면은 AI 의 점검 도구가 될 수 없으니, 스크립트가 앱의 링크 규칙을 흉내 내야 했고 흉내가 어긋난 만큼 숫자가 틀렸습니다.

고아 노트 464개는 271개였습니다

고아 노트는 두 가지 방법으로 셌습니다. 기존 점검 스크립트와 별도로, 링크 대상을 정규화해 링크 그래프를 훑는 측정 스크립트를 새로 짰습니다. 기존 스크립트는 464개, 새 측정 스크립트는 271개라고 했고, 그 차이를 따라가 원인을 찾았습니다.

기존 스크립트는 파일 이름을 [[...]] 안의 텍스트와 그대로 비교했습니다. [[노트]] 는 알아봤지만 [[폴더/노트]] 처럼 경로를 붙인 링크는 알아보지 못했습니다. 옵시디언에서는 멀쩡히 이어져 있는 노트 약 190개가, 기존 스크립트 눈에는 아무도 가리키지 않는 노트였습니다.

새 측정 스크립트는 링크 대상에서 별칭(| 뒤)과 제목 앵커(# 뒤)를 떼고 마지막 경로 조각만 남긴 뒤, 파일명과 링크를 모두 유니코드 NFC 로 맞춰 비교합니다. 한글은 같은 글자도 음절 하나로 된 형태(NFC)와 자모로 풀어 쓴 형태(NFD) 두 가지로 저장될 수 있어서, 눈에는 같은 이름이 문자열 비교에서는 다를 수 있습니다. 271개는 전체 노트의 13.1%였습니다.

고아 노트를 정리하는 일은 결국 무언가를 옮기거나 지우는 일이라, 이 숫자는 그대로 작업 목록이 됩니다. 예전에 에이전트 메모리를 감사하면서 정리에서 가장 위험한 순간은 부채를 발견했을 때가 아니라 발견했다고 믿었을 때라고 적은 적이 있는데, 464가 꼭 그런 숫자였습니다. 한 가지 방법으로만 쟀다면 464를 의심할 계기가 없었을지도 모릅니다.

고친 셈법에도 한계는 남아 있습니다. 마지막 경로 조각만 보니 이름이 같은 노트 둘은 하나로 봅니다. 이름이 겹치는 노트 이야기는 3편에서 따로 적겠습니다.

깨진 링크 141회 가운데 28회는 코드 안에 있었습니다

없는 노트를 가리키는 깨진 링크는 141회로 나왔습니다. 그중 28회는 코드 블록과 인라인 코드 안의 예시 문자열이었습니다. 옵시디언은 코드 안의 [[...]] 를 링크로 보지 않습니다. 화면에서는 그냥 글자인 것을 스크립트는 링크로 셌습니다.

목록에는 이번 정리에서 새로 만든 쓰기 규약 파일(_AI-WRITING-GUIDE.md)도 들어 있었습니다. "[[...]] 는 vault 안의 노트에만 쓴다"는 문장이, 표기를 보여 주려고 코드로 감싸 둔 바로 그 예시 때문에 깨진 링크로 잡혔습니다. 링크 규칙을 설명하는 문장이 링크 규칙 위반으로 잡힌 셈입니다. 측정 스크립트가 코드 영역을 먼저 걷어 낸 뒤 링크를 세도록 고쳤고, 깨진 링크는 113회가 됐습니다.

고친 셈법을 줄이면 이 정도입니다(파이썬 3.9 이상). 코드 영역을 먼저 지우고, 링크 대상을 마지막 경로 조각으로 정규화한 뒤, git ls-files 로 얻은 노트 이름과 맞춰 봅니다. 아무 링크도 받지 못한 노트가 고아 노트이고, 없는 이름을 가리킨 링크가 깨진 링크입니다.

import re
import subprocess
import unicodedata
from collections import Counter
from pathlib import Path

# 닫는 펜스는 여는 펜스와 같은 문자·길이라고 단순화했다
FENCED = re.compile(r"^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$", re.M | re.S)
INLINE = re.compile(r"(?<!`)(`+)(?!`).+?(?<!`)\1(?!`)", re.S)
WIKILINK = re.compile(r"!?\[\[([^\[\]\n]+?)\]\]")
ATTACHMENT = re.compile(r"\.(png|jpe?g|gif|svg|webp|pdf|mp4)$", re.I)


def link_targets(markdown: str) -> list[str]:
    """코드 영역을 걷어 낸 본문에서 노트 링크의 대상을 NFC 이름으로 돌려준다."""
    text = INLINE.sub("", FENCED.sub("", markdown))
    targets = []
    for m in WIKILINK.finditer(text):
        target = m.group(1).split("|", 1)[0].split("#", 1)[0].strip()  # 별칭·제목 앵커 제거
        if not target:  # [[#제목]] 은 같은 노트 안의 링크
            continue
        name = target.rstrip("/").rsplit("/", 1)[-1].removesuffix(".md")  # [[폴더/노트]] → 노트
        if ATTACHMENT.search(name):  # 첨부 파일은 여기서 판정하지 않는다
            continue
        targets.append(unicodedata.normalize("NFC", name))
    return targets


vault = Path("vault")  # vault 루트
out = subprocess.run(["git", "-C", str(vault), "ls-files", "-z", "*.md"],
                     capture_output=True, check=True).stdout
files = [vault / p for p in out.decode().split("\0") if p]
notes = {unicodedata.normalize("NFC", f.stem) for f in files}
links = Counter(t for f in files for t in link_targets(f.read_text(encoding="utf-8")))

orphans = sorted(notes - set(links))                         # 아무도 가리키지 않는 노트
broken = sum(n for t, n in links.items() if t not in notes)  # 없는 노트를 가리킨 횟수

미리보기(dry-run)가 이미지 170개를 살렸습니다

세는 데서 끝나면 틀린 숫자는 틀린 보고서에 그칩니다. 고치는 스크립트는 다릅니다. 틀리면 파일이 바뀝니다. 그래서 깨진 링크를 고치는 스크립트는 적용하기 전에 무엇을 몇 건 바꿀지만 보여 주는 미리보기(dry-run)부터 돌렸습니다.

첫 미리보기는 "vault 밖 경로를 가리키는 링크는 백틱 표기로 바꾼다"는 규칙 하나로 194건을 바꾸겠다고 했습니다. 깨진 링크는 모두 113회였습니다. 깨진 링크를 고치는 스크립트가 깨진 링크보다 많이 고칠 수는 없으니, 이 숫자는 그 자체로 경고였습니다.

배치 처리 감사에서는 건수나 합계를 미리 알아 두고 처리 결과와 맞춰 보는 방법을 통제 합계(control total)라고 부르는데, 같은 발상입니다. 이미 아는 총량과 나란히 놓는 것만으로 이상이 보였습니다.

들여다보니 194건 중 170건이 ![[평가폴더/desktop.png]] 같은 이미지 삽입이었습니다. 옵시디언은 첨부 파일을 경로 일부만으로도 찾습니다. 스크립트는 그 경로를 노트가 있는 위치 기준의 상대 경로로 계산했고, 그 자리에 파일이 없으니 vault 밖을 가리킨다고 판정했습니다. 앞의 두 사례와 같은 종류의 오류로, 이번에는 옵시디언이 첨부 파일을 찾는 규칙을 몰랐습니다. 그대로 적용했다면 이미지 170개가 노트에서 사라지고, 그 자리에 백틱으로 감싼 경로 글자만 남았을 것입니다.

규칙을 고쳤습니다. 이미지 삽입은 건드리지 않고, 첨부 파일은 있고 없음을 판정하지 않습니다. 두 번째 미리보기에는 53건만 남았고, 그것만 적용했습니다.

고친 링크 건수 처리
vault 밖(레포 작업 폴더 등)을 가리키던 링크 8 백틱 표기로
예전 정리 때 빈 노트라서 지운 대상을 가리키던 링크 17 링크만 풀고 글자는 그대로
날짜가 하루 어긋난 링크 10 같은 이름에 ±3일 안 후보가 정확히 1개일 때만 연결
띄어쓰기·하이픈 표기만 다른 링크 8 실제 노트 이름으로
폴더를 가리키던 링크 10 그 폴더의 인덱스(_INDEX.md)로
합계 53

이름이 비슷한 후보를 골라 주는 유사도 매칭(파이썬 difflib)은 쓰지 않았습니다. 미리보기에서 전혀 다른 대상을 짝지은 경우가 보였기 때문입니다. 더 많이 고치는 쪽(재현율)보다 틀리게 고치지 않는 쪽(정밀도)을 택했습니다. 잘못 이어 붙인 링크는 더 이상 깨진 링크로 잡히지 않아서, 다음 점검에서도 드러나지 않습니다.

고친 뒤 다시 세니 깨진 링크는 70회였습니다. 템플릿의 자리표시자나 아직 쓰지 않은 노트의 제목처럼 확실한 규칙에 들지 않는 것들이라 그대로 남겼습니다. 그런데 113회에서 53건을 고쳤으면 60회가 남아야 계산이 맞습니다.

53건 가운데 측정 스크립트가 깨진 링크로 세던 것은 43건뿐이었습니다. 나머지 10건은 [[../폴더/]] 같은 폴더 링크였는데, 측정 스크립트는 파일 이름만 보고 이것을 같은 이름의 폴더 노트로 읽어 정상으로 셌습니다(위 코드도 그렇게 읽습니다). 수선 스크립트는 더 엄격하게 해석되지 않는 링크로 봤습니다. 같은 링크를 두 도구가 다른 규칙으로 읽으니 숫자도 두 갈래로 갈렸고, 앞에서 대조한 113이라는 총량 역시 측정 스크립트의 규칙으로 센 숫자였던 셈입니다.

검증이 오류를 잡을 수 없는 방법이었습니다

비활성 프로젝트 폴더 53개를 보관 폴더(4-Archives/)로 옮긴 뒤, 옛 경로를 가리키는 참조가 남아 있는지 확인했습니다. 결과는 0건이었습니다. 그런데 그 확인은 1-Projects/폴더명 형태의 문자열만 찾았습니다. 옛 경로를 찾아 고칠 때 쓴 것과 같은 검색이었습니다.

루트 인덱스(_AI-INDEX.md)에는 경로 없이 폴더명만 적어 둔 줄임 표기가 4곳 있었고, 그 검색에는 걸리지 않았습니다. "인덱스 표에 적힌 모든 경로가 지금 실제로 있는가"라는 다른 방법으로 대조하고 나서야 드러났습니다. 옛 경로를 찾는 대신, 적혀 있는 경로가 살아 있는지를 물은 것입니다.

안전 공학에는 공통 모드 고장(common-mode failure)이라는 말이 있습니다. 이중으로 둔 장치가 같은 원인 하나로 한꺼번에 고장 나는 것을 가리킵니다. 그래서 중요한 계통에는 일부러 서로 다른 방식의 장치를 겹쳐 두는데, 이것을 설계 다양성이라고 부릅니다.

검증도 다르지 않았습니다. 찾을 때와 확인할 때 같은 방법을 쓰면, 그 방법의 맹점까지 같이 "확인"됩니다. 앞의 고아 노트도 새 측정 스크립트라는 다른 방법으로 쟀기에 464와 271이 갈라져 보였습니다. 이 일 뒤로는 확인을 일부러 다른 방법으로, 가능하면 더 느슨한 방법으로 합니다. 느슨한 확인에서는 엉뚱한 것까지 걸려 나오는데, 넓게 훑었다는 뜻이니 그 노이즈는 나오는 것이 정상입니다.

덧붙이면 그 존재 대조 스크립트도 처음에는 누락이 90건이라고 보고했습니다. 인덱스에 폴더 → _INDEX.md 처럼 폴더 뒤에 적어 둔 파일명까지 vault 루트 기준으로 찾은 오탐이었습니다. 파일명은 앞에 적힌 폴더 안에서 찾아야 했습니다. 검증하는 쪽에도 맹점이 있다는 것은 결함 주입으로 테스트를 재 볼 때도 겪은 일입니다. 확인하는 도구도 확인이 필요했습니다.

정리 스크립트와 셸에도 결함이 있었습니다

정리 스크립트가 방금 쓴 파일을 다시 읽고 있었습니다

긴 로그형 노트의 지난 절을 <노트>-이력.md 로 옮기는 도구를 만들었습니다. 절을 옮기면 그 절을 가리키던 [[노트#절]] 링크도 [[노트-이력#절]] 로 고쳐야 합니다. 옮긴 뒤에는 1편에서처럼 무손실 대조를 돌렸습니다. 원본의 비공백 줄이 새 파일들 어딘가에 모두 있는지 보는 것입니다.

def missing_lines(original: str, outputs: list[str]) -> list[str]:
    """원본의 비공백 줄 가운데, 새 파일들 어디에도 그대로는 없는 줄."""
    pool = {line.strip() for text in outputs for line in text.splitlines()}
    return [
        line for line in original.splitlines()
        if line.strip() and line.strip() not in pool
    ]

한 노트에서 24줄이 "누락"으로 나왔습니다. 하나씩 대조해 보니 24줄 모두 링크 대상만 이력 파일로 바뀐 줄이었습니다. [[노트#절]] 이 [[노트-이력#절]] 이 되었으니 줄 전체로는 원본에 없는 줄이 된 것이고, 내용이 사라진 줄은 없었습니다. 경보는 오탐이었습니다.

그런데 의심하며 코드를 다시 읽어 보니, 결함은 다른 자리에 실제로 있었습니다. 링크를 고치는 단계가 vault 전체를 훑으면서 방금 쓴 대상 노트와 이력 파일까지 다시 읽고 있었습니다. 미리 계산해 둔 옛 버전이 나중에 쓰이면서 고친 내용을 덮어쓸 수 있는 구조였습니다. 데이터베이스에서 갱신 손실(lost update)이라고 부르는 모양입니다. 같은 대상을 두 단계가 각자 읽고 고치고 쓰면, 늦게 쓰는 쪽이 먼저 쓴 쪽의 변경을 모른 채 덮어씁니다.

대상 파일은 메모리에서 고친 뒤 한 번만 쓰도록 바꿨습니다. 보관 폴더의 기록은 원문 그대로 두는 것이 원칙이라, 링크 앵커도 고치지 않게 했습니다. 이 일로 원칙이 하나 더 생겼습니다. 대조가 걸리면 걸린 수만 보지 않고, 줄 단위로 사유를 나눕니다. 24라는 숫자만 봤다면 스물네 줄이 사라졌다고 읽었을 것이고, 사유를 나누지 않았다면 오탐이라는 것도, 그 옆의 진짜 결함도 놓쳤을지 모릅니다.

find 의 빈 출력은 0건이 아닐 수 있습니다

이 머신에서는 에이전트가 읽을 명령 출력을 줄여 주는 CLI 프록시를 씁니다. 이 프록시는 find 의 -not, 괄호, -exec 같은 복합 조건을 거부하는데, 거부할 때 출력은 비어 있고 종료 코드는 1입니다. vault 점검 스킬의 명령에도 이런 find 가 들어 있었습니다. 이번 정리에서는 물리기 전에 막았지만, 물렸다면 알아채기 어려운 종류의 결함이었습니다.

빈 출력에 종료 코드 1은 셸에서 흔히 보는 "찾은 것 없음"의 모양입니다. grep 이 일치하는 줄이 없을 때 돌려주는 값이 바로 1이기 때문입니다. 그래서 이 실패는 실패처럼 보이지 않고 0건처럼 보입니다.

구별할 단서는 종료 코드의 뜻에 있습니다. find 는 찾은 것이 없어도 0을 돌려주고, 1은 무언가 실패했다는 뜻입니다. 증거의 부재는 부재의 증거가 아니라는 오래된 말 그대로, 빈 출력은 "없다"가 아니라 "보지 못했다"일 수 있습니다.

이 머신의 지침에 이미 적어 둔 함정이라, 점검 스킬의 명령을 스크립트 파일로 돌려 피했습니다. 프록시는 에이전트가 셸에 넘기는 명령 줄을 고쳐 쓰는 방식이라, 스크립트 파일 안에서 도는 명령에는 끼어들지 않습니다. 그러고 나서 스킬 자체를 스크립트와 git ls-files 로 바꿨습니다. 드러난 결함을 고친 것이 아니라, 잠재 결함을 미리 막은 경우입니다.

vault 가 git 레포라서 git ls-files 로는 추적 중인 파일만 정확히 나옵니다. 한글 파일명이 많다면 하나만 더 챙기면 됩니다. git ls-files 는 기본값으로 한글 경로를 8진수 이스케이프로 바꿔 따옴표로 감싸 내보내는데, 앞의 코드처럼 -z 를 붙이거나 core.quotePath 를 끄면 원래 이름이 나옵니다(git-config 문서).

zsh 의 path 와 LINES 는 평범한 변수가 아니었습니다

정리 스크립트를 짜던 서브에이전트가 셸 특수 변수에 걸렸습니다. 이 머신의 셸은 zsh 입니다. zsh 에서 소문자 path 는 $PATH 와 묶인 배열이라, 파일 경로를 담으려고 path=... 라고 쓰는 순간 명령을 찾는 경로가 그 값으로 바뀝니다. 그 뒤의 모든 외부 명령이 command not found 가 됩니다. LINES 는 터미널 높이를 담는 정수형 특수 변수라, 파일 내용을 담으면 bad math expression 오류가 나거나 잘린 값이 남습니다.

# zsh 5.9 에서 재현 (-f: 사용자 설정 파일을 읽지 않는다)
zsh -f -c 'path="notes/회고.md"; ls'
# zsh:1: command not found: ls

zsh -f -c 'LINES=$(printf "line one\nline two"); echo "$LINES"'
# zsh:1: bad math expression: operator expected at `one\nline t...'

zsh -f -c 'LINES="3.7"; echo "$LINES"'
# 3

bash 에서는 같은 줄들이 평범하게 동작합니다. 두 변수 모두 zsh 매뉴얼의 셸이 쓰는 변수 목록에 올라 있습니다. 직접 재현해 확인한 뒤, 모든 세션이 읽는 글로벌 지침에 함정으로 적어 두었습니다. 변수에는 relpath, content 처럼 셸이 쓰지 않는 이름을 붙이게 했습니다.

도구가 대상의 규칙을 몰랐다는 점에서는 앞의 사례들과 같았습니다. 이번 대상이 옵시디언이 아니라 스크립트가 도는 셸이었을 뿐입니다.

제 손으로 적은 숫자도 틀렸습니다

같은 파일이 425KB 이기도, 436KB 이기도 했습니다

같은 파일의 크기를 한 곳에서는 425KB, 다른 곳에서는 436KB 로 보고했습니다. 앞의 것은 1,024바이트를, 뒤의 것은 1,000바이트를 1KB 로 센 값이었습니다. 국제 표준(IEC)은 1,024바이트 단위를 KiB 라는 이름으로 따로 부르게 정해 두었지만, 도구마다 KB 를 섞어 씁니다. 그래서 단위를 하나로 정하고 적어 두었습니다. 이 시리즈의 KB 는 모두 1,000바이트 기준입니다.

부끄럽지만 커밋 메시지에 적은 합계도 틀렸습니다. 1,768KB 라고 적었는데, 보고서를 쓰면서 이력을 나눈 노트 11개의 크기를 다시 더해 보니 1,668KB 였습니다. 함께 적은 건수 두 개도 "24·6"이 아니라 "23·7"이었습니다. 스크립트 출력을 복사하지 않고 손으로 옮겨 적은 탓입니다.

이렇게 옮겨 적다 생기는 오류를 전사 오류(transcription error)라고 부르는데, 한 자리만 어긋나도 그럴듯한 다른 숫자가 되어 눈으로는 잘 걸러지지 않습니다. 이미 push 한 커밋은 고쳐 쓰지 않았습니다. 대신 결정 기록 노트에 바로잡은 값을 적었습니다. 그 뒤로 숫자는 도구 출력에서 복사해서만 씁니다.

마지막 커밋 날짜에 제 정리 커밋이 섞였습니다

보관할 폴더를 가리려고 마지막 커밋이 60일 넘은 프로젝트 폴더를 셌습니다. 60개였던 것이 같은 날 42개로 줄어 보였습니다. 그날 한 정리 커밋이 여러 폴더를 한꺼번에 건드리면서, 그 폴더들의 "마지막 커밋"이 오늘이 된 것입니다. 활동을 재는 숫자에 정리하는 제 손자국이 섞였습니다.

정리 커밋(chore(vault), docs(vault))을 활동에서 빼도록 측정을 고치니 59개가 됐습니다. git 에서는 커밋 메시지로 걸러 낼 수 있습니다. --grep 을 여러 개 주면 그중 하나라도 맞는 커밋이 빠집니다.

# 정리 커밋을 뺀, 폴더의 마지막 내용 변경일
git log -1 --format=%cs --invert-grep \
  --grep='^chore(vault)' --grep='^docs(vault)' \
  -- "1-Projects/<폴더>"

59개를 날짜만 보고 모두 옮기지는 않았습니다. 날짜 말고 무엇을 함께 봤는지는 3편에서 적겠습니다. 다만 그 판단의 첫 재료였던 날짜부터 제 작업에 오염되어 있었다는 것은 여기 남겨 둡니다. 활동을 잴 때는 내 작업이 만든 흔적부터 빼야 합니다.

재는 도구를 믿기 전에 확인할 것들

아홉 번을 돌아보면, 대부분의 경우 도구는 제가 정해 준 규칙대로 정확히 셌습니다. 틀린 것은 그 규칙이었습니다. 옵시디언이 링크를 읽는 방식과, 스크립트가 도는 셸과, 제가 일하는 방식과 어긋나 있었습니다. 이번 정리에서 남은 원칙은 이렇습니다.

  • 재는 도구는 대상 앱이 무엇을 어떻게 해석하는지부터 알아야 합니다. 링크를 어떻게 풀어 읽는지, 코드 영역을 어떻게 다루는지, 첨부 파일을 어떤 경로로 찾는지 모르면 숫자가 틀립니다.
  • 바꾸기 전에 미리보기를 돌리고, 결과 수를 이미 아는 총량과 대조합니다. 194가 113보다 크다는 이상은 숫자를 나란히 놓기만 해도 보입니다.
  • 열거와 검증에는 서로 다른 방법을 씁니다. 확인은 일부러 더 느슨하게 하고, 확인하는 도구도 확인합니다.
  • 옮긴 뒤에는 무손실 대조를 하고, 대조가 걸리면 줄 단위로 사유를 나눕니다.
  • 숫자는 손으로 옮기지 않고 도구 출력에서 복사합니다. 단위도 하나로 정해 적어 둡니다.
  • 활동을 잴 때는 내 작업이 만든 흔적을 뺍니다.

1편에서는 "지우지 말고 덧붙인다"는 안전한 규칙도 크기 상한 없이 쓰면 모든 파일이 로그가 된다고 적었습니다. 이번 교훈도 결이 비슷합니다. 안전하게 정리하려고 먼저 재는데, 재는 도구를 확인하지 않으면 틀린 것을 정확하게 고치게 됩니다. 정리하는 날에는 정리할 노트보다 재는 도구를 먼저 의심해야 한다는 생각이 들었습니다.

정리를 하다 보니 전제 하나도 바뀌었습니다. 이 vault 를 가장 많이 읽고 쓰는 것은 제가 아니라 AI 였습니다. 사람이 거의 열지 않는 옵시디언은 무엇이 달라야 하는지는 3편에서 이어 쓰겠습니다.

옵시디언에이전트 메모리고아 노트깨진 링크dry-run검증zsh