Files
llm-wiki/docs/superpowers/specs/2026-06-02-obsidian-link-validation-hardening-design.md

7.8 KiB

Design: 옵시디언 링크 검증 강화 (wiki-structure-lint P3 hardening)

  • 날짜: 2026-06-02
  • 대상: .claude/hooks/wiki_structure_lint.py (C2 링크 검사 + vault index)
  • 선행: docs/superpowers/specs/2026-06-01-wiki-structure-lint-design (린터 최초 설계). 본 문서는 그 §6.1 "알려진 한계"를 정확성 방향으로 해소하는 hardening 패스.
  • 목적: 옵시디언 그래프뷰의 회색 노드(unresolved link)가 누적되는 문제를, 린터를 정확하게(false positive 제거 + 옵시디언 문법 전 형태 커버) 만든 뒤 zero-tolerance로 강제하여 근절.

0. 배경 / 문제

사용자가 Obsidian 그래프뷰에서 회색 노드(타깃 파일이 없는 링크)가 많음을 관찰. 린터는 BROKEN_LINK검출은 하지만 PostToolUse 훅이 non-blocking이라 누적됨. 추가로 두 종류의 린터 false positive가 신호를 오염시켜 강제를 불가능하게 함.

vault 전체 진단 (425개 문서):

  • BROKEN_LINK 72건 = (a) 린터 오탐 .drawio 첨부 ~8 + (b) 진짜 회색 노드(미생성 daily-note 36 + 미작성 concept/official-doc ~22) + (c) 편집 실수(예시 텍스트가 링크화) ~3
  • BACKTICK_WRAPPED_LINK 36건 = 다수가 표 셀 경계를 넘는 페어링 오탐
  • DANGLING_ANCHOR 3, NO_FRONTMATTER 3

실사용 옵시디언 링크 형태(grep): embed ![[]] 9, alias [[a|b]] 2, heading anchor [[a#h]] 27, 비-md 첨부(drawio 8 + svg 1). block-ref [[a#^id]]·heading-only [[#h]]는 0건.

1. 확정된 설계 결정 (사용자)

  1. 정확성 먼저 → 그 위에 강제. false positive 제거 + 문법 전 형태 커버가 선행.
  2. 깨진 링크 = 무조건 에러 (zero-tolerance). 의도적 forward-reference(아직 안 쓴 문서로의 링크)도 예외 없음 — 스텁 생성 또는 링크 제거로 해소. → 선행 설계 §6.1의 "예시 라인 스킵 규칙" 아이디어는 폐기: 바 텍스트 [[double bracket]]은 Obsidian에서 실제 회색 노드이므로 스킵이 아니라 flag하고 code-span으로 고치는 것이 정합.
  3. 강제 지점 = PostToolUse 훅(저장 즉시 경고, 정확도 강화) + 수동 --all 게이트(exit 1). git pre-commit은 도입 안 함.

2. Part A — False positive 제거 (정확성 토대)

A1. Vault index에 비-md 첨부 포함

현재 build_vault_indexrglob("*.md")만 인덱싱 → [[…architecture-modules.drawio]]가 실존하는데도 BROKEN_LINK 오탐(8건).

  • 모든 파일을 인덱싱(rglob("*") 중 파일만; 숨김 디렉터리(.git 등) 및 경로에 /.이 포함된 항목 제외 — git 오브젝트/캐시 인덱싱 방지).
  • md: 현행대로 .md strip → rel-path(확장자 없음) + basename(stem).
  • 비-md: 확장자 포함 rel-path + 확장자 포함 basename으로 등록(Obsidian은 첨부를 확장자 포함으로 링크).
  • C2 타깃 해석에서 [[x.md]]는 strip 후 md index 조회, 그 외 확장자(.drawio/.svg/…)는 확장자 포함으로 조회.
  • C1 린트 대상(iter_docs)은 변경 없음 — md만 구조 검사. index 확장은 링크 타깃 해석에만 영향.

A2. Backtick 셀 경계 페어링 버그 수정

현재 BACKTICK_LINK = re.compile(r"[^\n]*\[\[[^\]]*\]\][^\n]*")는 backtick을 좌→우 연속 페어링하지 않고, 임의의 두 backtick 사이에 낀 위키링크를 매칭 → 표의 Decision 칸 인라인코드와 Supporting Claims 칸 인라인코드 사이에 위치한 정상 위키링크를 오탐.

  • 수정: 위키링크가 진짜 code span 내부일 때만 BACKTICK_WRAPPED_LINK. 판정 = 위키링크 시작 위치 앞의 (fence 밖) backtick 개수가 홀수이면 code span 내부(CommonMark 연속 페어링과 일치).
  • 구현: 라인에서 각 [[…]] 매치의 start() 이전 backtick 수를 세어 홀짝 판정. 짝수 → 정상 링크(검사 계속), 홀수 → wrapped(flag, BROKEN 검사 제외 — 이미 bare 제거 로직과 정합).
  • 효과: 직전 두 노트의 8건 + vault 36건 중 다수 오탐 제거. 진짜 래핑 `[[x]]`만 남김.

3. Part B — 옵시디언 문법 전 형태 정확 처리

형태 현재 설계
alias [[t|a]] ✓ split | 유지
embed ![[t]] ✓ 정규식이 [[]] 포함 A1로 이미지/첨부 embed도 resolve
heading anchor [[t#h]] 느슨한 substring (anchor.lower() in txt.lower()) 강화: 대상 문서의 실제 heading 텍스트 집합(모든 #-레벨, 정규화: markdown 제거+lowercase+공백정리)과 매칭. substring 통과 false-negative 제거
block-ref [[t#^id]] substring anchor가 ^로 시작 → 대상에 ^id 행말 토큰 존재 검사(사용 0건 — 최소 지원, 오탐 방지 우선)
heading-only [[#h]] target 비어 skip 같은 파일 heading 검사(사용 0건 — skip 유지, crash/오탐만 방지)
비-md 첨부 ✗ 오탐 A1로 해소
code-span 예시 `[[x]]` A2 전엔 혼동 A2로 링크 아님 정확 제외

heading anchor 정규화 규칙: 대상 문서에서 ^#{1,6}\s+(.+)$ 캡처 → 양끝 공백 strip → lowercase. 링크 anchor도 동일 정규화 후 집합 매칭. (Obsidian의 heading 링크 매칭에 보수적으로 근사 — 매칭 실패 시 DANGLING_ANCHOR.)

4. Part C — Zero-tolerance 강제 wiring

  • --all 게이트: 깨진 링크/dangling anchor 1건이라도 있으면 exit 1 (현행 유지 — A1/A2로 정확한 목록이 됨). forward-reference 예외 없음.
  • PostToolUse 훅: 유지 + A1/A2 정확도 강화 반영. 비-blocking 즉시 경고. (훅은 Claude의 도구 편집 시에만 발동 — Obsidian 직접 편집은 --all이 진실 소스.)
  • 롤아웃: A1/A2 적용 후 --all --links-only 재실행 → 오탐 제거된 진짜 깨진 링크 목록 산출. (정리=스텁 생성/링크 수정은 별도 단계, 본 설계는 린터까지.)

5. Part D — 문법 계약 문서화

린터 docstring의 C2 설명을 "지원 옵시디언 문법 + 위반 정의" 표로 명문화(Part B 표 기반). rules/linking-rules.md에서 "옵시디언 링크 문법의 결정론 집행기 = wiki_structure_lint.py C2"임을 1줄 참조. (별도 rules 파일 신설은 안 함 — YAGNI.)

6. 검증 / 수용 기준

  • 단위 테스트(pytest 또는 stdlib unittest, 의존성 0 유지):
    • A2: 표 행 | code| text [[link]] text |code |(짝수 backtick)이 BACKTICK_WRAPPED_LINK 미발생, 진짜 `[[x]]`는 발생.
    • A1: [[path/foo.drawio]]가 실존 시 BROKEN_LINK 미발생, 부재 시 발생.
    • B(anchor): 실존 heading은 통과, 오타 heading은 DANGLING_ANCHOR.
  • 회귀: 직전 두 노트(feature-operational-error-observability-foundation, feature-api-contract-baseline)의 backtick 오탐(8건+6건)이 --file에서 사라짐.
  • vault-wide: --all --links-only의 BACKTICK 36건과 BROKEN 72건이 정확한 수치로 감소(.drawio 8건 등 오탐 제거).
  • 기존 C1/C3 동작 불변(링크 검사만 수정).

7. 범위 밖 (YAGNI)

  • git pre-commit 게이트.
  • block-ref / heading-only 엄격 검증 (사용 0건 — 오탐 방지만).
  • 자동 스텁 생성 / 깨진 링크 자동 수정 (A2 backtick autofix는 기존 --fix 유지, 변경 없음).
  • 멀티 CLI 전파(Codex/Gemini).
  • 진짜 깨진 링크 ~60건의 실제 정리(별도 작업 — 본 설계 산출물인 정확한 목록을 입력으로).

8. 산출물

파일 작업
.claude/hooks/wiki_structure_lint.py build_vault_index(A1), check_c2/backtick 판정(A2), anchor 강화(B), docstring(D)
.claude/hooks/test_wiki_structure_lint.py (신규) A1/A2/B 단위 테스트
rules/linking-rules.md C2 집행기 참조 1줄(D)

비고: 현재 .git이 빈 디렉터리(미초기화)라 spec 커밋은 생략 — 디스크 파일이 기록.