diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7227fd5..8dfa412 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -89,7 +89,7 @@ repos: args: ['-config-file', '.github/actionlint.yaml'] files: ^\.forgejo/workflows/ - # Documentation - check for duplicate titles (required for Quartz wiki-link resolution) + # Documentation validation - repo: local hooks: - id: doc-titles @@ -98,10 +98,12 @@ repos: language: system files: ^docs/.*\.md$ pass_filenames: false - - # Documentation - validate wiki-links point to existing titles - - repo: local - hooks: + - id: doc-filenames + name: doc-filenames + entry: mise run doc-filenames + language: system + files: ^docs/.*\.md$ + pass_filenames: false - id: doc-links name: doc-links entry: mise run doc-links diff --git a/docs/index.md b/docs/index.md index 38ba99d..14b1503 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,8 +4,8 @@ title: blumeops-documentation Welcome to the BlumeOps documentation. -[[readme | Documentation Home]] - Temporary home while docs are being restructured (see [Diataxis](https://diataxis.fr/) restructuring plan) +[[README | Documentation Home]] - Temporary home while docs are being restructured (see [Diataxis](https://diataxis.fr/) restructuring plan) ## Sections -- [[reference]] - Technical reference cards for services, infrastructure, and operations +- [[index | reference]] - Technical reference cards for services, infrastructure, and operations diff --git a/docs/reference/index.md b/docs/reference/index.md index 30f12f5..fe55778 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -14,7 +14,7 @@ Individual service reference cards with URLs and configuration details. | Service | Description | Location | |---------|-------------|----------| -| [[grafana-alloy | Alloy]] | Observability collector (metrics & logs) | indri + k8s | +| [[alloy | Alloy]] | Observability collector (metrics & logs) | indri + k8s | | [[argocd]] | GitOps continuous delivery | k8s | | [[borgmatic]] | Backup system | indri | | [[1password]] | Secrets management | cloud + k8s | @@ -36,27 +36,27 @@ Individual service reference cards with URLs and configuration details. Host inventory and network configuration. -- [[host-inventory | Hosts]] - Device inventory +- [[hosts | Hosts]] - Device inventory - [[indri]] - Primary server - [[gilbert]] - Development workstation - [[tailscale]] - ACLs, groups, tags -- [[service-routing | Routing]] - DNS domains, port mappings +- [[routing | Routing]] - DNS domains, port mappings ## Kubernetes Cluster configuration and application registry. -- [[kubernetes-cluster | Cluster]] - Minikube specs, storage, networking -- [[argocd-applications | Apps]] - ArgoCD application registry +- [[cluster | Cluster]] - Minikube specs, storage, networking +- [[apps | Apps]] - ArgoCD application registry - [[external-secrets]] - Secrets management ## Storage Network storage and backup configuration. -- [[sifaka-nas | Sifaka]] - Synology NAS configuration +- [[sifaka | Sifaka]] - Synology NAS configuration - [[postgresql-storage]] - Database cluster -- [[backup-policy | Backups]] - Backup policy and schedule +- [[backups | Backups]] - Backup policy and schedule ## Operations diff --git a/docs/reference/infrastructure/gilbert.md b/docs/reference/infrastructure/gilbert.md index a8a0a7a..b4ed983 100644 --- a/docs/reference/infrastructure/gilbert.md +++ b/docs/reference/infrastructure/gilbert.md @@ -24,4 +24,4 @@ Managed via `Brewfile` and `mise.toml` in the blumeops repo. ## Related - [[indri]] - Server accessed from gilbert -- [[kubernetes-cluster | Cluster]] - Remote k8s access +- [[cluster | Cluster]] - Remote k8s access diff --git a/docs/reference/infrastructure/hosts.md b/docs/reference/infrastructure/hosts.md index 50533d2..a4a4d86 100644 --- a/docs/reference/infrastructure/hosts.md +++ b/docs/reference/infrastructure/hosts.md @@ -14,7 +14,7 @@ All devices connected via [Tailscale](https://login.tailscale.com/) tailnet `tai |------|-------------|------| | **Indri** | Mac Mini M1, 2020 - Primary server | [[indri | Details]] | | **Gilbert** | MacBook Air M4, 2025 - Workstation | [[gilbert | Details]] | -| **[[sifaka-nas | Sifaka]]** | Synology NAS - Storage & backups | [[sifaka-nas | Details]] | +| **[[sifaka | Sifaka]]** | Synology NAS - Storage & backups | [[sifaka | Details]] | | **Mouse** | MacBook Air M2 - Allison's laptop | - | | **UniFi** | UniFi Express 7 - Home WiFi | - | | **Dwarf** | iPad Air - Employer-provided, off tailnet | - | @@ -22,4 +22,4 @@ All devices connected via [Tailscale](https://login.tailscale.com/) tailnet `tai ## Related - [[tailscale]] - Network configuration -- [[service-routing | Routing]] - Service URLs +- [[routing | Routing]] - Service URLs diff --git a/docs/reference/infrastructure/indri.md b/docs/reference/infrastructure/indri.md index d702a82..055b62f 100644 --- a/docs/reference/infrastructure/indri.md +++ b/docs/reference/infrastructure/indri.md @@ -26,13 +26,13 @@ Primary BlumeOps server. Mac Mini M1 (2020). - [[zot]] - Container registry - [[jellyfin]] - Media server - [[borgmatic]] - Backup system -- [[grafana-alloy | Alloy]] - Metrics/logs collector +- [[alloy | Alloy]] - Metrics/logs collector - Caddy - Reverse proxy for `*.ops.eblu.me` **Kubernetes (via minikube):** -- [[argocd-applications | All k8s applications]] +- [[apps | All k8s applications]] ## Related -- [[service-routing | Routing]] - Port mappings -- [[kubernetes-cluster | Cluster]] - Minikube details +- [[routing | Routing]] - Port mappings +- [[cluster | Cluster]] - Minikube details diff --git a/docs/reference/infrastructure/routing.md b/docs/reference/infrastructure/routing.md index 647c27e..09abf8b 100644 --- a/docs/reference/infrastructure/routing.md +++ b/docs/reference/infrastructure/routing.md @@ -38,7 +38,7 @@ DNS points to indri's Tailscale IP (100.98.163.89). TLS via Let's Encrypt (ACME | [[navidrome]] | https://dj.ops.eblu.me | Music streaming | | [[jellyfin]] | https://jellyfin.ops.eblu.me | Media server | | [[postgresql]] | pg.ops.eblu.me:5432 | Database | -| [[sifaka-nas | Sifaka]] | https://nas.ops.eblu.me | NAS dashboard | +| [[sifaka | Sifaka]] | https://nas.ops.eblu.me | NAS dashboard | ## Tailscale-Only Services diff --git a/docs/reference/infrastructure/tailscale.md b/docs/reference/infrastructure/tailscale.md index 9a3c37c..423734c 100644 --- a/docs/reference/infrastructure/tailscale.md +++ b/docs/reference/infrastructure/tailscale.md @@ -58,5 +58,5 @@ Pulumi uses OAuth client from 1Password (blumeops vault): ## Related -- [[service-routing | Routing]] - Service URLs -- [[host-inventory | Hosts]] - Device inventory +- [[routing | Routing]] - Service URLs +- [[hosts | Hosts]] - Device inventory diff --git a/docs/reference/kubernetes/apps.md b/docs/reference/kubernetes/apps.md index d987efc..db5ab58 100644 --- a/docs/reference/kubernetes/apps.md +++ b/docs/reference/kubernetes/apps.md @@ -26,7 +26,7 @@ Registry of all applications deployed via [[argocd]]. | `grafana` | monitoring | Helm chart (forge mirror) | [[grafana]] | | `grafana-config` | monitoring | `argocd/manifests/grafana-config/` | [[grafana]] | | `immich` | immich | Helm chart | [[immich]] | -| `alloy-k8s` | alloy | `argocd/manifests/alloy-k8s/` | [[grafana-alloy | Alloy]] | +| `alloy-k8s` | alloy | `argocd/manifests/alloy-k8s/` | [[alloy | Alloy]] | | `kube-state-metrics` | monitoring | `argocd/manifests/kube-state-metrics/` | K8s metrics | | `miniflux` | miniflux | `argocd/manifests/miniflux/` | [[miniflux]] | | `kiwix` | kiwix | `argocd/manifests/kiwix/` | [[kiwix]] | @@ -45,4 +45,4 @@ Registry of all applications deployed via [[argocd]]. ## Related - [[argocd]] - GitOps platform details -- [[kubernetes-cluster | Cluster]] - Kubernetes infrastructure +- [[cluster | Cluster]] - Kubernetes infrastructure diff --git a/docs/reference/kubernetes/cluster.md b/docs/reference/kubernetes/cluster.md index 0193a8f..f22d274 100644 --- a/docs/reference/kubernetes/cluster.md +++ b/docs/reference/kubernetes/cluster.md @@ -24,7 +24,7 @@ Single-node Minikube cluster running on [[indri]]. ## Volume Mounting -Pods mount NFS directly from [[sifaka-nas | Sifaka]]. Docker NATs outbound traffic through indri's LAN IP (192.168.1.50), allowing access to Sifaka's NFS exports. +Pods mount NFS directly from [[sifaka | Sifaka]]. Docker NATs outbound traffic through indri's LAN IP (192.168.1.50), allowing access to Sifaka's NFS exports. ## Registry Mirror @@ -34,6 +34,6 @@ Mirrors configured: `registry.ops.eblu.me`, `docker.io`, `ghcr.io`, `quay.io` ## Related -- [[argocd-applications | Apps]] - ArgoCD applications +- [[apps | Apps]] - ArgoCD applications - [[argocd]] - GitOps deployment - [[zot]] - Registry mirror diff --git a/docs/reference/operations/backup.md b/docs/reference/operations/backup.md index ffeb733..fd3a31c 100644 --- a/docs/reference/operations/backup.md +++ b/docs/reference/operations/backup.md @@ -11,5 +11,5 @@ Daily automated backups of BlumeOps data. ## Components - [[borgmatic]] - Backup orchestration -- [[sifaka-nas | Sifaka]] - Backup target (NAS) -- [[backup-policy]] - What gets backed up and retention +- [[sifaka | Sifaka]] - Backup target (NAS) +- [[backups | backup-policy]] - What gets backed up and retention diff --git a/docs/reference/operations/disaster-recovery.md b/docs/reference/operations/disaster-recovery.md index c939076..fbe19cd 100644 --- a/docs/reference/operations/disaster-recovery.md +++ b/docs/reference/operations/disaster-recovery.md @@ -8,7 +8,7 @@ tags: TBD. Current state: -- [[borgmatic]] provides daily backups to [[sifaka-nas | Sifaka]] +- [[borgmatic]] provides daily backups to [[sifaka | Sifaka]] - Infrastructure can be rebootstrapped using the blumeops repo - Detailed DR procedures not yet documented diff --git a/docs/reference/operations/observability.md b/docs/reference/operations/observability.md index 22a5ab2..1673d25 100644 --- a/docs/reference/operations/observability.md +++ b/docs/reference/operations/observability.md @@ -12,5 +12,5 @@ Metrics, logs, and dashboards for BlumeOps infrastructure. - [[prometheus]] - Metrics storage and querying - [[loki]] - Log aggregation -- [[grafana-alloy | Alloy]] - Metrics and log collection +- [[alloy | Alloy]] - Metrics and log collection - [[grafana]] - Dashboards and visualization diff --git a/docs/reference/services/argocd.md b/docs/reference/services/argocd.md index 041091f..fb37539 100644 --- a/docs/reference/services/argocd.md +++ b/docs/reference/services/argocd.md @@ -7,7 +7,7 @@ tags: # ArgoCD -GitOps continuous delivery platform for the [[kubernetes-cluster | Kubernetes cluster]]. +GitOps continuous delivery platform for the [[cluster | Kubernetes cluster]]. ## Quick Reference @@ -33,5 +33,5 @@ GitOps continuous delivery platform for the [[kubernetes-cluster | Kubernetes cl ## Related -- [[argocd-applications | Apps]] - Full application registry +- [[apps | Apps]] - Full application registry - [[forgejo]] - Git source diff --git a/docs/reference/services/borgmatic.md b/docs/reference/services/borgmatic.md index f3f1594..b4b9a95 100644 --- a/docs/reference/services/borgmatic.md +++ b/docs/reference/services/borgmatic.md @@ -16,7 +16,7 @@ Daily backup system using Borg backup, running on indri. | **Install** | mise (pipx) | | **Config** | `~/.config/borgmatic/config.yaml` | | **Schedule** | Daily at 2:00 AM | -| **Repository** | `/Volumes/backups/borg/` on [[sifaka-nas | Sifaka]] | +| **Repository** | `/Volumes/backups/borg/` on [[sifaka | Sifaka]] | ## What Gets Backed Up @@ -55,6 +55,6 @@ Dashboard: "Borgmatic Backups" in [[grafana]] ## Related -- [[backup-policy | Backups]] - Full backup policy -- [[sifaka-nas | Sifaka]] - Backup target +- [[backups | Backups]] - Full backup policy +- [[sifaka | Sifaka]] - Backup target - [[postgresql]] - Database backups diff --git a/docs/reference/services/grafana.md b/docs/reference/services/grafana.md index c6a9ed1..1810017 100644 --- a/docs/reference/services/grafana.md +++ b/docs/reference/services/grafana.md @@ -47,4 +47,4 @@ Optional annotation: `grafana_folder: "FolderName"` - [[prometheus]] - Metrics datasource - [[loki]] - Logs datasource -- [[grafana-alloy | Alloy]] - Data collector +- [[alloy | Alloy]] - Data collector diff --git a/docs/reference/services/immich.md b/docs/reference/services/immich.md index 35d9526..d590be4 100644 --- a/docs/reference/services/immich.md +++ b/docs/reference/services/immich.md @@ -17,10 +17,10 @@ Self-hosted photo and video management. | **Namespace** | `immich` | | **Deployment** | Helm chart (k8s) | | **Database** | [[postgresql]] (CNPG) | -| **Storage** | [[sifaka-nas | Sifaka]] photos volume | +| **Storage** | [[sifaka | Sifaka]] photos volume | ## Related - [[postgresql]] - Database backend -- [[sifaka-nas | Sifaka]] - Photo storage +- [[sifaka | Sifaka]] - Photo storage - [[jellyfin]] - Video streaming (separate service) diff --git a/docs/reference/services/jellyfin.md b/docs/reference/services/jellyfin.md index 93e7a5f..067a93f 100644 --- a/docs/reference/services/jellyfin.md +++ b/docs/reference/services/jellyfin.md @@ -42,10 +42,10 @@ Dashboard > Playback: ## Observability - Metrics: `jellyfin_metrics` ansible role -- Logs: Forwarded via [[grafana-alloy | Alloy]] +- Logs: Forwarded via [[alloy | Alloy]] - Dashboard: "Jellyfin Media Server" in [[grafana]] ## Related - [[navidrome]] - Music streaming -- [[sifaka-nas | Sifaka]] - Media storage +- [[sifaka | Sifaka]] - Media storage diff --git a/docs/reference/services/kiwix.md b/docs/reference/services/kiwix.md index f4ebd24..368559b 100644 --- a/docs/reference/services/kiwix.md +++ b/docs/reference/services/kiwix.md @@ -17,7 +17,7 @@ Offline Wikipedia and ZIM archive server. | **Tailscale URL** | https://kiwix.tail8d86e.ts.net | | **Namespace** | `kiwix` | | **Image** | `ghcr.io/kiwix/kiwix-serve:3.8.1` | -| **Storage** | NFS from [[sifaka-nas | Sifaka]] (`/volume1/torrents`) | +| **Storage** | NFS from [[sifaka | Sifaka]] (`/volume1/torrents`) | ## Architecture @@ -49,4 +49,4 @@ Full list: `argocd/manifests/kiwix/configmap-zim-torrents.yaml` ## Related - [[transmission]] - Downloads ZIM files -- [[sifaka-nas | Sifaka]] - ZIM storage +- [[sifaka | Sifaka]] - ZIM storage diff --git a/docs/reference/services/loki.md b/docs/reference/services/loki.md index 327fc68..2013cc1 100644 --- a/docs/reference/services/loki.md +++ b/docs/reference/services/loki.md @@ -24,7 +24,7 @@ Log aggregation system for BlumeOps infrastructure. - Single-node deployment with filesystem storage - TSDB index with 24h period -- Logs collected by [[grafana-alloy | Alloy]] and pushed via Loki API +- Logs collected by [[alloy | Alloy]] and pushed via Loki API - Queried via [[grafana]] ## Log Sources @@ -46,6 +46,6 @@ Log aggregation system for BlumeOps infrastructure. ## Related -- [[grafana-alloy | Alloy]] - Log collector +- [[alloy | Alloy]] - Log collector - [[grafana]] - Log visualization - [[prometheus]] - Metrics counterpart diff --git a/docs/reference/services/navidrome.md b/docs/reference/services/navidrome.md index e003b98..20e5b1e 100644 --- a/docs/reference/services/navidrome.md +++ b/docs/reference/services/navidrome.md @@ -39,4 +39,4 @@ The `/data` directory contains SQLite database, configuration, and cache. ## Related - [[jellyfin]] - Video streaming -- [[sifaka-nas | Sifaka]] - Music storage +- [[sifaka | Sifaka]] - Music storage diff --git a/docs/reference/services/prometheus.md b/docs/reference/services/prometheus.md index a094bf4..fd76b81 100644 --- a/docs/reference/services/prometheus.md +++ b/docs/reference/services/prometheus.md @@ -23,19 +23,19 @@ Metrics storage and querying for BlumeOps infrastructure. ## Data Sources ### Remote Write (from Alloy) -- Indri system metrics via [[grafana-alloy | Alloy]] remote_write +- Indri system metrics via [[alloy | Alloy]] remote_write - Textfile metrics: minikube, borgmatic, zot, jellyfin ### Scrape Targets | Target | Metrics | |--------|---------| -| `sifaka:9100` | [[sifaka-nas | Sifaka]] NAS (node_exporter) | +| `sifaka:9100` | [[sifaka | Sifaka]] NAS (node_exporter) | | `cnpg-metrics.tail8d86e.ts.net:9187` | [[postgresql | CloudNativePG]] metrics | | `kube-state-metrics.monitoring.svc:8080` | Kubernetes resource metrics | ## Related -- [[grafana-alloy | Alloy]] - Metrics collector +- [[alloy | Alloy]] - Metrics collector - [[grafana]] - Visualization - [[loki]] - Logs counterpart diff --git a/docs/reference/services/transmission.md b/docs/reference/services/transmission.md index 4e687e9..7f22af3 100644 --- a/docs/reference/services/transmission.md +++ b/docs/reference/services/transmission.md @@ -17,7 +17,7 @@ BitTorrent daemon, primarily for downloading ZIM archives for [[kiwix]]. | **Tailscale URL** | https://torrent.tail8d86e.ts.net | | **Namespace** | `torrent` | | **Image** | `lscr.io/linuxserver/transmission:latest` | -| **Storage** | NFS PVC from [[sifaka-nas | Sifaka]] | +| **Storage** | NFS PVC from [[sifaka | Sifaka]] | ## Storage Layout @@ -43,11 +43,11 @@ When downloads complete, the zim-watcher CronJob detects new ZIMs and restarts K ## Monitoring -Basic uptime via blackbox probe in [[grafana-alloy | Alloy]] k8s (Services Health dashboard). +Basic uptime via blackbox probe in [[alloy | Alloy]] k8s (Services Health dashboard). Web UI shows: active/seeding/paused counts, speeds, disk usage. ## Related - [[kiwix]] - ZIM archive consumer -- [[sifaka-nas | Sifaka]] - Download storage +- [[sifaka | Sifaka]] - Download storage diff --git a/docs/reference/services/zot.md b/docs/reference/services/zot.md index 5f766f5..c506f31 100644 --- a/docs/reference/services/zot.md +++ b/docs/reference/services/zot.md @@ -30,7 +30,7 @@ OCI-native container registry providing pull-through cache and private image sto ## Pull-Through Cache -When [[kubernetes-cluster | minikube]] pulls an image, containerd checks zot first. If cached, returns immediately. If not, zot fetches from upstream, caches it, then returns. +When [[cluster | minikube]] pulls an image, containerd checks zot first. If cached, returns immediately. If not, zot fetches from upstream, caches it, then returns. ## Security Model @@ -39,4 +39,4 @@ Network access only (no authentication). Defense is the Tailscale ACL boundary. ## Related - [[forgejo]] - Container build CI -- [[kubernetes-cluster | Cluster]] - Registry consumer +- [[cluster | Cluster]] - Registry consumer diff --git a/docs/reference/storage/backups.md b/docs/reference/storage/backups.md index ffe2292..f763443 100644 --- a/docs/reference/storage/backups.md +++ b/docs/reference/storage/backups.md @@ -7,7 +7,7 @@ tags: # Backup Policy -Daily automated backups from [[indri]] to [[sifaka-nas | Sifaka]] NAS. +Daily automated backups from [[indri]] to [[sifaka | Sifaka]] NAS. ## Schedule @@ -53,7 +53,7 @@ Daily automated backups from [[indri]] to [[sifaka-nas | Sifaka]] NAS. ## Backup Target -Repository: `/Volumes/backups/borg/` on [[sifaka-nas | Sifaka]] +Repository: `/Volumes/backups/borg/` on [[sifaka | Sifaka]] ## Monitoring @@ -67,5 +67,5 @@ Dashboard: "Borgmatic Backups" in [[grafana]] ## Related - [[borgmatic]] - Backup system details -- [[sifaka-nas | Sifaka]] - Backup storage +- [[sifaka | Sifaka]] - Backup storage - [[postgresql]] - Database backups diff --git a/docs/reference/storage/sifaka.md b/docs/reference/storage/sifaka.md index 8e3eaf8..81e6391 100644 --- a/docs/reference/storage/sifaka.md +++ b/docs/reference/storage/sifaka.md @@ -52,7 +52,7 @@ Data protection for sifaka itself currently relies on the Synology RAID 5 config ## Related -- [[backup-policy | Backups]] - Backup policy +- [[backups | Backups]] - Backup policy - [[borgmatic]] - Backup system - [[immich]] - Photo consumer - [[jellyfin]] - Media consumer diff --git a/mise-tasks/doc-filenames b/mise-tasks/doc-filenames new file mode 100755 index 0000000..912effc --- /dev/null +++ b/mise-tasks/doc-filenames @@ -0,0 +1,88 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["rich>=13.0.0"] +# /// +#MISE description="Detect duplicate filenames in documentation" +"""Detect duplicate filenames in documentation. + +This script scans all markdown files in the docs/ directory (excluding +changelog.d/ and zk/) and reports any duplicate filenames that could +cause wiki-link resolution issues. + +With Quartz, wiki-links like [[filename]] resolve by filename, +so filenames must be unique across the documentation. + +Usage: mise run doc-filenames +""" + +import sys +from collections import defaultdict +from pathlib import Path + +from rich.console import Console +from rich.table import Table + +DOCS_DIR = Path(__file__).parent.parent / "docs" + + +def main() -> int: + console = Console() + + # Collect all filenames and their paths + # Key: filename (without .md), Value: list of file paths + filenames: dict[str, list[str]] = defaultdict(list) + + # Scan all markdown files (excluding zk/, changelog.d/, and index.md files) + for md_file in sorted(DOCS_DIR.rglob("*.md")): + if "changelog.d" in md_file.parts or "zk" in md_file.parts: + continue + # Skip index.md files - they're expected to exist in multiple directories + if md_file.name == "index.md": + continue + + rel_path = str(md_file.relative_to(DOCS_DIR)) + filename = md_file.stem # filename without .md + filenames[filename].append(rel_path) + + # Find duplicates + duplicates = {name: paths for name, paths in filenames.items() if len(paths) > 1} + + # Print results + console.print("[bold]Doc Filename Inventory[/bold]") + console.print() + console.print("With Quartz, wiki-links like [[filename]] resolve by filename,") + console.print("so filenames must be unique across the documentation.") + console.print() + + # Duplicates table (if any) + if duplicates: + console.print("[bold red]Duplicate Filenames Found[/bold red]") + dup_table = Table(show_header=True, header_style="bold") + dup_table.add_column("Filename") + dup_table.add_column("Paths") + + for name in sorted(duplicates.keys()): + paths = duplicates[name] + dup_table.add_row(name, "\n".join(paths)) + + console.print(dup_table) + console.print() + + # Summary + console.print(f"Total files: {sum(len(p) for p in filenames.values())}") + console.print(f"Unique filenames: {len(filenames)}") + console.print(f"Duplicate filenames: {len(duplicates)}") + + if duplicates: + console.print() + console.print("[bold red]Action required:[/bold red] Rename files to ensure unique wiki-link resolution.") + return 1 + + console.print() + console.print("[bold green]All filenames are unique![/bold green]") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/mise-tasks/doc-links b/mise-tasks/doc-links index c940059..4307571 100755 --- a/mise-tasks/doc-links +++ b/mise-tasks/doc-links @@ -1,29 +1,26 @@ #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.12" -# dependencies = ["pyyaml>=6.0", "rich>=13.0.0"] +# dependencies = ["rich>=13.0.0"] # /// -#MISE description="Validate all wiki-links point to existing doc titles" -"""Validate that all wiki-links in documentation point to existing titles. +#MISE description="Validate all wiki-links point to existing doc filenames" +"""Validate that all wiki-links in documentation point to existing filenames. This script scans all markdown files in the docs/ directory (excluding changelog.d/ and zk/), extracts wiki-links, and verifies each link target -exists as a frontmatter title in the documentation. +exists as a filename in the documentation. Wiki-link formats supported: -- [[target]] - links to a doc with frontmatter title "target" -- [[target | Display Text]] - links to "target", displays "Display Text" - (spaces around the pipe are REQUIRED) +- [[filename]] - links to filename.md +- [[filename | Display Text]] - links to filename.md, displays "Display Text" Usage: mise run doc-links """ import re import sys -from collections import defaultdict from pathlib import Path -import yaml from rich.console import Console from rich.markup import escape from rich.table import Table @@ -31,32 +28,12 @@ from rich.table import Table DOCS_DIR = Path(__file__).parent.parent / "docs" # Regex to match wiki-links: [[Target]] or [[Target | Display]] -WIKILINK_PATTERN = re.compile(r"\[\[([^\]|]+)(?:\s+\|\s+[^\]]+)?\]\]") - -# Regex to detect any wiki-link with a pipe (for format checking) -WIKILINK_WITH_PIPE_PATTERN = re.compile(r"\[\[([^\]]+)\]\]") +WIKILINK_PATTERN = re.compile(r"\[\[([^\]|]+)(?:\s*\|\s*[^\]]+)?\]\]") # Regex to match inline code (backticks) INLINE_CODE_PATTERN = re.compile(r"`[^`]+`") -def extract_frontmatter(file_path: Path) -> dict | None: - """Extract YAML frontmatter from a markdown file.""" - content = file_path.read_text() - if not content.startswith("---"): - return None - - end_idx = content.find("---", 3) - if end_idx == -1: - return None - - frontmatter_text = content[3:end_idx].strip() - try: - return yaml.safe_load(frontmatter_text) or {} - except yaml.YAMLError: - return None - - def extract_wikilinks(file_path: Path) -> list[tuple[str, int]]: """Extract all wiki-link targets from a markdown file with line numbers. @@ -75,41 +52,20 @@ def extract_wikilinks(file_path: Path) -> list[tuple[str, int]]: return links -def find_unspaced_pipes(file_path: Path) -> list[tuple[str, int]]: - """Find wiki-links with unspaced pipes (e.g., [[target|display]] instead of [[target | display]]).""" - content = file_path.read_text() - issues = [] - - for line_num, line in enumerate(content.splitlines(), start=1): - # Remove inline code before searching - line_without_code = INLINE_CODE_PATTERN.sub("", line) - for match in WIKILINK_WITH_PIPE_PATTERN.finditer(line_without_code): - inner = match.group(1) - # Check if there's a pipe without proper spacing (space on both sides) - if "|" in inner and " | " not in inner: - issues.append((match.group(0), line_num)) - - return issues - - def main() -> int: console = Console() - # Collect all valid titles from frontmatter - valid_titles: set[str] = set() + # Collect all valid filenames + valid_filenames: set[str] = set() - # Scan all markdown files for titles (excluding zk/ and changelog.d/) + # Scan all markdown files for filenames (excluding zk/ and changelog.d/) for md_file in DOCS_DIR.rglob("*.md"): if "changelog.d" in md_file.parts or "zk" in md_file.parts: continue + valid_filenames.add(md_file.stem) - frontmatter = extract_frontmatter(md_file) - if frontmatter and frontmatter.get("title"): - valid_titles.add(frontmatter["title"]) - - # Collect all broken links and format issues + # Collect all broken links broken_links: list[tuple[str, int, str]] = [] - unspaced_pipes: list[tuple[str, int, str]] = [] # Scan all markdown files for wiki-links (excluding zk/ and changelog.d/) for md_file in sorted(DOCS_DIR.rglob("*.md")): @@ -117,45 +73,19 @@ def main() -> int: continue rel_path = str(md_file.relative_to(DOCS_DIR)) - - # Check for unspaced pipes - for link_text, line_num in find_unspaced_pipes(md_file): - unspaced_pipes.append((rel_path, line_num, link_text)) - - # Check for broken links links = extract_wikilinks(md_file) + for target, line_num in links: - if target not in valid_titles: + if target not in valid_filenames: broken_links.append((rel_path, line_num, target)) # Print results console.print("[bold]Wiki-Link Validation[/bold]") console.print() - console.print(f"Found {len(valid_titles)} valid titles in documentation.") + console.print(f"Found {len(valid_filenames)} valid filenames in documentation.") console.print() - has_errors = False - - # Report unspaced pipes - if unspaced_pipes: - has_errors = True - console.print("[bold red]Unspaced Pipe in Wiki-Links[/bold red]") - console.print("Wiki-links with display text must use spaces: [[target | display]]") - console.print() - table = Table(show_header=True, header_style="bold") - table.add_column("File") - table.add_column("Line", justify="right") - table.add_column("Link") - - for file_path, line_num, link_text in unspaced_pipes: - table.add_row(file_path, str(line_num), escape(link_text)) - - console.print(table) - console.print() - - # Report broken links if broken_links: - has_errors = True console.print("[bold red]Broken Wiki-Links Found[/bold red]") table = Table(show_header=True, header_style="bold") table.add_column("File") @@ -167,12 +97,9 @@ def main() -> int: console.print(table) console.print() - console.print("Each wiki-link target must match a frontmatter title in docs/.") + console.print(f"[bold red]{len(broken_links)} broken link(s) found.[/bold red]") console.print() - - if has_errors: - error_count = len(broken_links) + len(unspaced_pipes) - console.print(f"[bold red]{error_count} issue(s) found.[/bold red]") + console.print("Each wiki-link target must match a filename in docs/.") return 1 console.print("[bold green]All wiki-links are valid![/bold green]")