扫描 wiki/ 所有页面里带 ^anchor 的双链,列出失效的引用。 覆盖两类目标: - [[raw/...#^anchor]] — raw 文件不存在 / anchor 不存在(raw 有 outline,但 anchor 集合直接从 .md 扫,与 wiki 同路) - [[wiki/...#^anchor]] — wiki 文件不存在 / anchor 不存在(wiki 页无 outline.json, 用 _collect_anchors_in_md 直扫目标 .md 的锚点集合判断) 无 ^anch
(pages)
| 1857 | |
| 1858 | |
| 1859 | def list_broken_refs(pages) -> list[dict]: |
| 1860 | """扫描 wiki/ 所有页面里带 ^anchor 的双链,列出失效的引用。 |
| 1861 | |
| 1862 | 覆盖两类目标: |
| 1863 | - [[raw/...#^anchor]] — raw 文件不存在 / anchor 不存在(raw 有 outline,但 |
| 1864 | anchor 集合直接从 .md 扫,与 wiki 同路) |
| 1865 | - [[wiki/...#^anchor]] — wiki 文件不存在 / anchor 不存在(wiki 页无 outline.json, |
| 1866 | 用 _collect_anchors_in_md 直扫目标 .md 的锚点集合判断) |
| 1867 | |
| 1868 | 无 ^anchor 的纯页面链接([[wiki/concepts/X]])不在此检查——那属于孤儿 / 出链 |
| 1869 | 维度,由 list-orphans / outlinks 覆盖。 |
| 1870 | """ |
| 1871 | out = [] |
| 1872 | # 目标 .md 路径(rel)→ 其锚点集合,避免重复扫盘 |
| 1873 | anchor_cache: dict[str, set[str]] = {} # raw 目标:内联锚点集合 |
| 1874 | resolvable_cache: dict[str, tuple] = {} # wiki 目标:read_block 同口径的可解析索引 |
| 1875 | |
| 1876 | for page in pages: |
| 1877 | # 跳过反引号包裹的字面占位(如周报里写 `[[raw/...#^anchor]]` 当格式示例) |
| 1878 | scan_text = mask_code_spans(page.raw_content) |
| 1879 | for m in WIKILINK_RE.finditer(scan_text): |
| 1880 | target = m.group(1) |
| 1881 | anchor = m.group(2) |
| 1882 | if not anchor or not anchor.startswith("^"): |
| 1883 | continue |
| 1884 | target_norm = normalize_link_target(target) |
| 1885 | is_raw = bool(RAW_REF_PREFIX_RE.match(target_norm)) |
| 1886 | is_wiki = bool(WIKI_REF_PREFIX_RE.match(target_norm)) |
| 1887 | if not is_raw and not is_wiki: |
| 1888 | continue |
| 1889 | # 拒绝越界目标([[raw/../etc/passwd]])——既不打开也不报告 |
| 1890 | doc_path = _safe_join_under_root(target_norm) |
| 1891 | if doc_path is None: |
| 1892 | continue |
| 1893 | anchor_id = anchor.lstrip("^") |
| 1894 | line = page.raw_content[: m.start()].count("\n") + 1 |
| 1895 | kind_label = "raw" if is_raw else "wiki" |
| 1896 | reason = None |
| 1897 | if not doc_path.exists(): |
| 1898 | reason = f"{kind_label} 文件不存在" |
| 1899 | elif is_wiki: |
| 1900 | # wiki 页:与 read_block 同口径(计算块锚点 + hash6 容错)判断锚点是否真能解析, |
| 1901 | # 避免旧的内联扫描被正文裸锚点文本骗过而漏报坏块引用。 |
| 1902 | if target_norm not in resolvable_cache: |
| 1903 | resolvable_cache[target_norm] = _resolvable_anchor_index(doc_path) |
| 1904 | if not _anchor_resolvable(anchor_id, resolvable_cache[target_norm]): |
| 1905 | reason = "anchor 不存在" |
| 1906 | else: |
| 1907 | if target_norm not in anchor_cache: |
| 1908 | anchor_cache[target_norm] = _collect_anchors_in_md(doc_path) |
| 1909 | if anchor_id not in anchor_cache[target_norm]: |
| 1910 | reason = "anchor 不存在" |
| 1911 | if reason: |
| 1912 | out.append({ |
| 1913 | "from_path": page.path, |
| 1914 | "from_title": page.title, |
| 1915 | "line": line, |
| 1916 | "target": target_norm, |
no test coverage detected