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_LINK72건 = (a) 린터 오탐.drawio첨부 ~8 + (b) 진짜 회색 노드(미생성 daily-note 36 + 미작성 concept/official-doc ~22) + (c) 편집 실수(예시 텍스트가 링크화) ~3BACKTICK_WRAPPED_LINK36건 = 다수가 표 셀 경계를 넘는 페어링 오탐DANGLING_ANCHOR3,NO_FRONTMATTER3
실사용 옵시디언 링크 형태(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. 확정된 설계 결정 (사용자)
- 정확성 먼저 → 그 위에 강제. false positive 제거 + 문법 전 형태 커버가 선행.
- 깨진 링크 = 무조건 에러 (zero-tolerance). 의도적 forward-reference(아직 안 쓴 문서로의 링크)도 예외 없음 — 스텁 생성 또는 링크 제거로 해소. → 선행 설계 §6.1의 "예시 라인 스킵 규칙" 아이디어는 폐기: 바 텍스트
[[double bracket]]은 Obsidian에서 실제 회색 노드이므로 스킵이 아니라 flag하고 code-span으로 고치는 것이 정합. - 강제 지점 = PostToolUse 훅(저장 즉시 경고, 정확도 강화) + 수동
--all게이트(exit 1). git pre-commit은 도입 안 함.
2. Part A — False positive 제거 (정확성 토대)
A1. Vault index에 비-md 첨부 포함
현재 build_vault_index가 rglob("*.md")만 인덱싱 → [[…architecture-modules.drawio]]가 실존하는데도 BROKEN_LINK 오탐(8건).
- 모든 파일을 인덱싱(
rglob("*")중 파일만; 숨김 디렉터리(.git등) 및 경로에/.이 포함된 항목 제외 — git 오브젝트/캐시 인덱싱 방지). - md: 현행대로
.mdstrip → 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.
- A2: 표 행
- 회귀: 직전 두 노트(
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 커밋은 생략 — 디스크 파일이 기록.