Scan all wiki pages for [[wikilinks]] pointing to non-existent targets. Args: wiki: Path to the wiki root directory. Returns: List of error strings describing each broken link.
(wiki: Path)
| 259 | |
| 260 | |
| 261 | def find_broken_links(wiki: Path) -> list[str]: |
| 262 | """Scan all wiki pages for [[wikilinks]] pointing to non-existent targets. |
| 263 | |
| 264 | Args: |
| 265 | wiki: Path to the wiki root directory. |
| 266 | |
| 267 | Returns: |
| 268 | List of error strings describing each broken link. |
| 269 | """ |
| 270 | pages = _all_wiki_pages(wiki) |
| 271 | errors: list[str] = [] |
| 272 | |
| 273 | for md in wiki.rglob("*.md"): |
| 274 | if md.name in _EXCLUDED_FILES: |
| 275 | continue |
| 276 | # Skip reports/ and sources/ — auto-generated, not wiki content |
| 277 | rel_parts = md.relative_to(wiki).parts |
| 278 | if rel_parts and rel_parts[0] in ("reports", "sources"): |
| 279 | continue |
| 280 | text = _read_md(md) |
| 281 | for target in _extract_wikilinks(text): |
| 282 | # Normalise target: strip leading/trailing whitespace and slashes |
| 283 | target_norm = target.strip().strip("/") |
| 284 | # Check if target resolves as a key in our page map |
| 285 | if target_norm not in pages: |
| 286 | rel = md.relative_to(wiki) |
| 287 | errors.append(f"Broken link [[{target}]] in {rel}") |
| 288 | |
| 289 | return sorted(errors) |
| 290 | |
| 291 | |
| 292 | def find_orphans(wiki: Path) -> list[str]: |