260718_7
This commit is contained in:
+107
-144
@@ -1,157 +1,120 @@
|
||||
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")
|
||||
WIKI_DIR = r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발\docs\wiki"
|
||||
PAGES_DIR = os.path.join(WIKI_DIR, "pages")
|
||||
CONCEPTS_DIR = os.path.join(WIKI_DIR, "concepts")
|
||||
INDEX_PATH = os.path.join(WIKI_DIR, "index.md")
|
||||
|
||||
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
|
||||
errors = []
|
||||
warnings = []
|
||||
all_wiki_files = {} # relative_path -> absolute_path
|
||||
|
||||
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
|
||||
# Collect all markdown files in wiki/ (ignoring graphify-out and memory directories)
|
||||
for root, dirs, files in os.walk(WIKI_DIR):
|
||||
# skip graphify-out or memory
|
||||
if "graphify-out" in root or "memory" in root:
|
||||
continue
|
||||
for file in files:
|
||||
if file.endswith(".md"):
|
||||
abs_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(abs_path, WIKI_DIR).replace("\\", "/")
|
||||
all_wiki_files[rel_path] = abs_path
|
||||
simple_name = os.path.splitext(rel_path)[0]
|
||||
all_wiki_files[simple_name] = abs_path
|
||||
|
||||
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)
|
||||
# Read index.md content
|
||||
with open(INDEX_PATH, "r", encoding="utf-8") as f:
|
||||
index_content = f.read()
|
||||
|
||||
# Pattern for wikilinks: [[link]] or [[link|label]]
|
||||
wikilink_pat = re.compile(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]")
|
||||
|
||||
for rel_path, abs_path in list(all_wiki_files.items()):
|
||||
if "/" not in rel_path and rel_path != "index" and rel_path != "log":
|
||||
continue
|
||||
if rel_path in ["index", "log", "index.md", "log.md"]:
|
||||
continue
|
||||
|
||||
# 기본 규칙 검사
|
||||
if not fm:
|
||||
errors.append(f"Missing Frontmatter: {rel_path}")
|
||||
continue
|
||||
|
||||
# status 값 검사
|
||||
status = fm.get("status")
|
||||
with open(abs_path, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
line_count = len(lines)
|
||||
content = "".join(lines)
|
||||
|
||||
# Rule 14: Max 100 lines
|
||||
if line_count > 100:
|
||||
warnings.append(f"[Line Count] `{rel_path}.md` exceeds 100 lines ({line_count} lines).")
|
||||
|
||||
# Check YAML Frontmatter via simple regex
|
||||
frontmatter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
|
||||
if not frontmatter_match:
|
||||
errors.append(f"[Frontmatter] `{rel_path}.md` has no valid YAML frontmatter.")
|
||||
continue
|
||||
|
||||
fm_text = frontmatter_match.group(1)
|
||||
|
||||
# Simple regex parsing for status & page_id
|
||||
status_match = re.search(r"^status:\s*(\w+)", fm_text, re.MULTILINE)
|
||||
page_id_match = re.search(r"^page_id:\s*([^\n\r]+)", fm_text, re.MULTILINE)
|
||||
|
||||
if not status_match:
|
||||
errors.append(f"[Status] `{rel_path}.md` has no status field in frontmatter.")
|
||||
else:
|
||||
status = status_match.group(1).strip()
|
||||
if status not in ["draft", "stable", "stale"]:
|
||||
errors.append(f"Invalid status '{status}' in {rel_path}. Must be draft, stable, or stale.")
|
||||
errors.append(f"[Status] `{rel_path}.md` has invalid status '{status}'. Must be draft, stable, or stale.")
|
||||
elif status == "stale":
|
||||
warnings.append(f"[Stale Page] `{rel_path}.md` is marked as stale and needs updates from raw inputs.")
|
||||
|
||||
# 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}")
|
||||
# Rule 5: page_id required for pages/
|
||||
if "pages/" in rel_path:
|
||||
if not page_id_match:
|
||||
errors.append(f"[page_id] `{rel_path}.md` is in pages/ but lacks a 'page_id' field.")
|
||||
|
||||
# 페이지와 컨셉 분류
|
||||
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
|
||||
# Check for broken wikilinks in content
|
||||
links = wikilink_pat.findall(content)
|
||||
for link in links:
|
||||
link_clean = link.strip().replace("\\", "/").split("#")[0] # ignore anchor for file existence check
|
||||
if not link_clean:
|
||||
continue
|
||||
found = False
|
||||
|
||||
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
|
||||
# 1. Absolute link from vault root (e.g. concepts/storage_paths)
|
||||
if link_clean in all_wiki_files:
|
||||
found = True
|
||||
# 2. Simple name match (e.g. storage_paths, B01_frontend)
|
||||
elif link_clean.split("/")[-1] in all_wiki_files:
|
||||
found = True
|
||||
# 3. Handle subpages like [[B01_Dashboard/B01_frontend]]
|
||||
elif f"pages/{link_clean}" in all_wiki_files:
|
||||
found = True
|
||||
elif f"concepts/{link_clean}" in all_wiki_files:
|
||||
found = True
|
||||
# 4. Handle nested directory patterns
|
||||
elif link_clean.startswith("../"):
|
||||
# Resolve relative link
|
||||
curr_dir = os.path.dirname(rel_path)
|
||||
resolved = os.path.normpath(os.path.join(curr_dir, link_clean)).replace("\\", "/")
|
||||
if resolved in all_wiki_files or resolved.replace("pages/", "") in all_wiki_files:
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
errors.append(f"[Broken Link] `{rel_path}.md` contains broken wikilink: [[{link}]]")
|
||||
|
||||
# 링크 검사
|
||||
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}]]")
|
||||
# Check if this file is registered in index.md (avoid orphans)
|
||||
file_base = os.path.basename(abs_path)
|
||||
file_name_no_ext = os.path.splitext(file_base)[0]
|
||||
if file_name_no_ext not in index_content:
|
||||
# check if relative path is in index
|
||||
rel_path_no_ext = rel_path.replace(".md", "")
|
||||
if rel_path_no_ext not in index_content and rel_path_no_ext.split("/")[-1] not in index_content:
|
||||
warnings.append(f"[Orphan Page] `{rel_path}.md` is not linked or mentioned in index.md.")
|
||||
|
||||
# 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()
|
||||
print("=== LINT ERRORS ===")
|
||||
for e in sorted(list(set(errors))):
|
||||
print(e)
|
||||
print("\n=== LINT WARNINGS ===")
|
||||
for w in sorted(list(set(warnings))):
|
||||
print(w)
|
||||
print("\nLint completed.")
|
||||
|
||||
Reference in New Issue
Block a user