import os import re vault_path = "vault" report_file = os.path.join(vault_path, "linking_audit_report.md") mandatory_props = [ "type", "tags", "area", "status", "Created", "Last Modified", "notebook_id", "master_note", "meta description" ] def parse_simple_yaml(yaml_text): data = {} lines = yaml_text.split("\n") current_key = None for line in lines: if not line.strip(): continue if ":" in line: parts = line.split(":", 1) key = parts[0].strip() val = parts[1].strip() val = val.strip("\"'[]") data[key] = val current_key = key elif line.strip().startswith("-") and current_key: val = line.strip().lstrip("-").strip().strip("\"'") if current_key in data: if isinstance(data[current_key], list): data[current_key].append(val) else: data[current_key] = [data[current_key], val] else: data[current_key] = [val] return data def parse_md_file(file_path): with open(file_path, "r", encoding="utf-8") as f: content = f.read() # Parse frontmatter fm = {} body = content frontmatter_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL) if frontmatter_match: fm_text = frontmatter_match.group(1) body = content[frontmatter_match.end():] try: fm = parse_simple_yaml(fm_text) except Exception as e: print(f"Error parsing simple YAML in {file_path}: {e}") # Find all [[wikilinks]] links = re.findall(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]", body) links = [l.strip() for l in links] return fm, body, links def main(): if not os.path.exists(vault_path): print(f"Vault path {vault_path} not found.") return files = [f for f in os.listdir(vault_path) if f.endswith(".md") and f != "linking_audit_report.md"] file_data = {} # Load all files for filename in files: file_path = os.path.join(vault_path, filename) name_no_ext = os.path.splitext(filename)[0] fm, body, links = parse_md_file(file_path) file_data[name_no_ext] = { "filename": filename, "path": file_path, "frontmatter": fm, "body": body, "links": links } report = [] report.append("# Báo cáo Kiểm tra Liên kết & Frontmatter (Linking & Frontmatter Audit Report)") report.append(f"> **Ngày quét**: 2026-07-01 \n> **Trạng thái**: Tự động thực hiện bởi `audit_links.py` \n") report.append("---") # 1. Frontmatter Audit Table report.append("## 1. Kiểm duyệt Thuộc tính Frontmatter (YAML compliance)") report.append("| Tệp tin | Trạng thái | Lỗi/Thiếu hụt |") report.append("| :--- | :--- | :--- |") for name, data in file_data.items(): fm = data["frontmatter"] missing = [p for p in mandatory_props if p not in fm] if not fm: status = "❌ THIẾU YAML" detail = "Không tìm thấy YAML frontmatter block." elif missing: status = "⚠️ CHƯA ĐỦ" detail = f"Thiếu thuộc tính: `{', '.join(missing)}`" else: status = "✅ ĐẠT chuẩn" detail = "Đầy đủ các thuộc tính bắt buộc." report.append(f"| `[[{name}]]` | {status} | {detail} |") report.append("\n---") # 2. Link Validity & Orphan Links report.append("## 2. Kiểm duyệt Tính liên kết (Link Validity)") orphan_links = [] all_links_count = 0 for name, data in file_data.items(): links = data["links"] all_links_count += len(links) for link in links: target_exists = False for existing_name in file_data.keys(): if existing_name.lower() == link.lower(): target_exists = True break if not target_exists: orphan_links.append((name, link)) if orphan_links: report.append("⚠️ **Phát hiện các liên kết hỏng (Orphan/Dangling Links)**:") for source, target in orphan_links: report.append(f"- Tệp `[[{source}]]` chứa liên kết hỏng đến `[[{target}]]` (không tồn tại tệp tương ứng).") else: report.append("✅ Không phát hiện liên kết hỏng. Tất cả các `[[wikilinks]]` đều trỏ tới tệp tin tồn tại thực tế.") report.append(f"\n*Tổng số liên kết chéo hiện tại*: **{all_links_count}** liên kết.") report.append("\n---") # 3. Cross-linking Recommendations (Concept-based) report.append("## 3. Khuyến nghị Bổ sung Liên kết Nhân quả (Linking Recommendations)") keyword_map = { "open_research_questions": { "keywords": ["câu hỏi nghiên cứu mở", "open research questions", "open questions", "nghi vấn", "giả thuyết nghiên cứu"], "label": "Nhật ký Câu hỏi Mở" }, "fact_check_sprint_1": { "keywords": ["kiểm định", "fact-check", "xác thực nguồn", "citation audit"], "label": "Báo cáo Kiểm định Sprint 1" }, "sprint_1_report": { "keywords": ["báo cáo thực chứng sprint 1", "sprint 1 report", "kết quả sprint 1"], "label": "Báo cáo Thực chứng Sprint 1" }, "RS01_TI_CH01_KHAI_NIEM_INBOUND_TOURISM_VA_XUAT_KHAU_DICH_VU_VO_HINH": { "keywords": ["inbound tourism", "xuất khẩu dịch vụ tại chỗ", "mode 2", "consumption abroad"], "label": "Chương 1: Khái niệm Inbound Tourism và Xuất khẩu dịch vụ vô hình" } } orq_questions = [ ("gói vay tín dụng", "tín dụng du lịch", "chính sách cho vay"), ("quỹ hỗ trợ phát triển du lịch", "quỹ du lịch công-tư", "quỹ du lịch"), ("tây âu", "thị trường châu âu", "high-yield"), ("carrying capacity", "overtourism", "quá tải du lịch", "ngưỡng tải") ] recommendations_found = False # Audit other files linking to target files for name, data in file_data.items(): if name in ["open_research_questions", "RS01_TI_INDEX"]: continue body_lower = data["body"].lower() links_lower = [l.lower() for l in data["links"]] # Check against basic file keywords for target, kw_data in keyword_map.items(): if target == name: continue if target.lower() in links_lower: continue matched_kws = [kw for kw in kw_data["keywords"] if kw in body_lower] if matched_kws: report.append(f"- **`[[{name}]]`** đề cập đến cụm từ `{', '.join(f'\"{k}\"' for k in matched_kws)}`. Khuyến nghị thêm liên kết chéo `[[{target}|{kw_data['label']}]]`.") recommendations_found = True # Check against specific open questions for idx, q_kws in enumerate(orq_questions, 1): if "open_research_questions" in data["links"] or "open_research_questions".lower() in links_lower: continue matched_q_kws = [kw for kw in q_kws if kw in body_lower] if matched_q_kws: report.append(f"- **`[[{name}]]`** chứa nội dung liên quan đến **Câu hỏi mở số {idx}** (từ khóa: `{', '.join(f'\"{k}\"' for k in matched_q_kws)}`). Khuyến nghị bổ sung liên kết đến `[[open_research_questions|Câu hỏi mở {idx}]]`.") recommendations_found = True # Audit open_research_questions linking to related chapters/sections if "open_research_questions" in file_data: orq_data = file_data["open_research_questions"] orq_body = orq_data["body"] # Split by ## headings sections = re.split(r"(## [^\n]+)", orq_body) # Map question groups to expected chapter targets orq_group_mapping = { "Nhóm 1": ("RS01_TI_CH03_KHUNG_LY_THUYET_HIEU_SUAT_CAO_VA_6_NHOM_MUC_TIEU", "Chương 3: Khung Lý thuyết Hiệu suất cao"), "Nhóm 2": ("RS01_TI_CH07_CHAN_DOAN_KHUYET_TAT_HE_THONG_VA_DINH_HINH_VAN_DE", "Chương 7: Chẩn đoán Khuyết tật Hệ thống"), "Nhóm 3": ("RS01_TI_CH06_HIEN_TRANG_VIET_NAM_VA_DOI_SANH_SAN_PHAM_NGACH", "Chương 6: Hiện trạng Việt Nam"), "Nhóm 4": ("RS01_TI_CH08_THIET_KE_GIAI_PHAP_CHINH_SACH", "Chương 8: Thiết kế Giải pháp Chính sách") } current_header = "" for item in sections: if item.strip().startswith("##"): current_header = item.strip() elif current_header: matched_group = None for group_key in orq_group_mapping.keys(): if group_key in current_header: matched_group = group_key break if matched_group: target_ch, ch_label = orq_group_mapping[matched_group] links_in_sec = [l.lower() for l in re.findall(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]", item)] if target_ch.lower() not in links_in_sec: report.append(f"- **`[[open_research_questions]]`** trong phần `{matched_group}` chứa câu hỏi liên đới nhưng chưa dẫn link tới chương liên quan. Khuyến nghị thêm `[[{target_ch}|{ch_label}]]` ngay dưới phần Câu hỏi nghiên cứu.") recommendations_found = True current_header = "" if not recommendations_found: report.append("✅ Không có khuyến nghị bổ sung liên kết mới. Các tài liệu đã được liên kết đầy đủ dựa trên từ khóa phân tích.") # Write report back to vault with open(report_file, "w", encoding="utf-8") as f: f.write("\n".join(report)) print(f"Linking audit report generated at {report_file}") if __name__ == "__main__": main()