Files
Aislo/scratch/wiki_linter.py
T
2026-07-17 17:16:09 +09:00

158 lines
5.9 KiB
Python

import os
import re
from pathlib import Path
# 위키 루트 경로 설정
wiki_root = Path(r"D:\02_Software_Prog\임도설계 및 견적자동화 프로그램 개발\docs\wiki")
raw_root = Path(r"D:\02_Software_Prog\임도설계 및 견적자동화 프로그램 개발\docs\raw")
def load_frontmatter(file_path):
content = file_path.read_text(encoding="utf-8")
# Frontmatter regex
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if match:
fm_text = match.group(1)
fm = {}
# Simple YAML key-value parser for basic string/list properties
for line in fm_text.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line:
key, val = line.split(":", 1)
key = key.strip()
val = val.strip().strip("'\"")
fm[key] = val
return fm, content[match.end():]
return None, content
def extract_wiki_links(text):
# Matches [[link]] or [[link|alias]] or [[link#section]] or [[link#section|alias]]
links = re.findall(r"\[\[(.*?)\]\]", text)
cleaned_links = []
for l in links:
# Split alias
if "|" in l:
l = l.split("|")[0]
# Split section
if "#" in l:
l = l.split("#")[0]
l = l.strip()
if l:
cleaned_links.append(l)
return cleaned_links
def run_lint():
print("=== Start Wiki Linting (No PyYAML, Filtered) ===")
all_files = list(wiki_root.glob("**/*.md"))
index_file = wiki_root / "index.md"
log_file = wiki_root / "log.md"
# 1. 파일 목록화 및 Frontmatter 체크
pages = {}
concepts = {}
errors = []
warnings = []
for f in all_files:
rel_path = f.relative_to(wiki_root).as_posix()
# Skip output files generated by graphify in graphify-out
if rel_path.startswith("graphify-out/") or rel_path in ["index.md", "log.md", "AGENTS.md", "CLAUDE.md"]:
continue
fm, body = load_frontmatter(f)
# 기본 규칙 검사
if not fm:
errors.append(f"Missing Frontmatter: {rel_path}")
continue
# status 값 검사
status = fm.get("status")
if status not in ["draft", "stable", "stale"]:
errors.append(f"Invalid status '{status}' in {rel_path}. Must be draft, stable, or stale.")
# 100줄 제한 검사 (10% 즉 110줄 허용)
lines_count = len(f.read_text(encoding="utf-8").splitlines())
if lines_count > 110:
warnings.append(f"File exceeds 110 lines ({lines_count} lines): {rel_path}")
# 페이지와 컨셉 분류
if "pages/" in rel_path:
pages[rel_path] = {
"fm": fm,
"body": body,
"path": f,
"links": extract_wiki_links(body)
}
# 파일명 형식 규칙 5 검사: {page_id}_{기능명}.md
filename = f.name
page_id = fm.get("page_id")
if not page_id:
errors.append(f"Missing page_id in page: {rel_path}")
else:
expected_prefix = page_id.split("_")[0] # e.g. A01
if not filename.startswith(expected_prefix):
warnings.append(f"Filename does not match page_id format: {rel_path} (page_id: {page_id})")
else:
concepts[rel_path] = {
"fm": fm,
"body": body,
"path": f,
"links": extract_wiki_links(body)
}
# 2. 위키 링크 정합성 검증 (Broken Links)
# 개념/페이지 맵 구성
available_links = {}
for p_rel in pages:
name_no_ext = Path(p_rel).with_suffix("").as_posix()
available_links[name_no_ext] = p_rel
# Also map short form if distinct
short_name = Path(p_rel).name[:-3]
available_links[short_name] = p_rel
for c_rel in concepts:
name_no_ext = Path(c_rel).with_suffix("").as_posix()
available_links[name_no_ext] = c_rel
# Short form
short_name = Path(c_rel).name[:-3]
available_links[short_name] = c_rel
# Subdirectories for concepts like db_schema/*
if "concepts/" in name_no_ext:
available_links[name_no_ext.replace("concepts/", "")] = c_rel
# 링크 검사
for rel_path, info in {**pages, **concepts}.items():
for link in info["links"]:
if link.startswith("http://") or link.startswith("https://") or link.startswith("file:///"):
continue
normalized_link = link.replace("\\", "/")
if normalized_link not in available_links and f"concepts/{normalized_link}" not in available_links and f"pages/{normalized_link}" not in available_links:
warnings.append(f"Broken Link in {rel_path}: [[{link}]]")
# 3. index.md 등록 상태 확인
index_content = index_file.read_text(encoding="utf-8") if index_file.exists() else ""
for rel_path in pages:
short_name = Path(rel_path).name[:-3]
dir_name = Path(rel_path).parent.name
expected_ref_dir = f"{dir_name}/{short_name}"
if expected_ref_dir not in index_content and f"[[{short_name}]]" not in index_content and short_name not in index_content:
warnings.append(f"Page not indexed in index.md: {rel_path} (Expected link to [[{expected_ref_dir}]] or [[{short_name}]])")
# 4. 결과 출력
print(f"\nScanning completed: {len(all_files)} total markdown files.")
print(f"Detected {len(pages)} pages and {len(concepts)} concept documents.")
print(f"\n--- Errors ({len(errors)}) ---")
for e in errors:
print(f"[ERROR] {e}")
print(f"\n--- Warnings ({len(warnings)}) ---")
for w in warnings:
print(f"[WARN] {w}")
if __name__ == "__main__":
run_lint()