#!/usr/bin/env nu # Documentation Link Validator # Scans all markdown files and validates internal links const DOCS_ROOT = "/Users/Akasha/project-provisioning/docs" const PROVISIONING_DOCS_ROOT = "/Users/Akasha/project-provisioning/provisioning/docs" # Find all markdown files in documentation def find-all-docs []: nothing -> list { let docs_root = "/Users/Akasha/project-provisioning/docs" let prov_docs_root = "/Users/Akasha/project-provisioning/provisioning/docs" mut all_docs = [] # Find docs in main docs directory try { let main_docs = (glob ($docs_root + "/**/*.md")) $all_docs = ($all_docs | append $main_docs) } # Find docs in provisioning/docs directory try { let prov_docs = (glob ($prov_docs_root + "/**/*.md")) $all_docs = ($all_docs | append $prov_docs) } $all_docs | uniq } # Extract all markdown links from a file def extract-links [file: string]: nothing -> table { let content = (open $file) let lines = ($content | split row "\n") mut results = [] for idx in 0..<($lines | length) { let line = ($lines | get $idx) # Match markdown links: [text](path) let matches = ($line | parse -r '\[([^\]]+)\]\(([^)]+)\)') for match in $matches { let link_text = ($match | get capture0) let link_path = ($match | get capture1) # Classify link type let link_type = if ($link_path | str starts-with "http") { "external" } else if ($link_path | str starts-with "#") { "anchor" } else { "internal" } $results = ($results | append { line: ($idx + 1) text: $link_text path: $link_path type: $link_type }) } } $results } # Resolve relative path from source file to target def resolve-path [source: string, target: string]: nothing -> string { let source_dir = ($source | path dirname) # Handle absolute paths if ($target | str starts-with "/") { return $target } # Handle relative paths let resolved = ([$source_dir, $target] | path join) # Normalize path (handle ../ and ./) $resolved | path expand } # Check if a link target exists def check-link-exists [source: string, link: record]: nothing -> record { if $link.type == "external" { return ($link | insert exists true | insert resolved $link.path) } if $link.type == "anchor" { # For now, mark anchors as valid (would need to parse headings to validate) return ($link | insert exists true | insert resolved $link.path) } # Internal link - resolve and check existence let target_path = (resolve-path $source $link.path) let exists = ($target_path | path exists) $link | insert exists $exists | insert resolved $target_path } # Validate all links in a file def validate-file-links [file: string]: nothing -> table { print $"Checking ($file)..." let links = (extract-links $file) $links | each {|link| let checked = (check-link-exists $file $link) $checked | insert source_file $file } } # Main validation function def main [ --fix # Attempt to fix broken links automatically --format: string = "table" # Output format: table, json, or markdown ] { print "šŸ“š Documentation Link Validator" print "================================\n" print "Finding all documentation files..." let docs = (find-all-docs) print $"Found ($docs | length) markdown files\n" print "Extracting and validating links..." mut all_results = [] for doc in $docs { try { let results = (validate-file-links $doc) $all_results = ($all_results | append $results) } catch {|err| print $"āš ļø Error processing ($doc): ($err.msg)" } } # Summary statistics let total_links = ($all_results | length) let internal_links = ($all_results | where type == "internal" | length) let external_links = ($all_results | where type == "external" | length) let anchor_links = ($all_results | where type == "anchor" | length) let broken_links = ($all_results | where exists == false | length) print "\nšŸ“Š Summary Statistics" print "=====================" print $"Total links found: ($total_links)" print $" Internal links: ($internal_links)" print $" External links: ($external_links)" print $" Anchor links: ($anchor_links)" print $" Broken links: āŒ ($broken_links)" # Show broken links if any if $broken_links > 0 { print "\nāŒ Broken Links Report" print "======================" let broken = ($all_results | where exists == false) if $format == "json" { $broken | to json } else if $format == "markdown" { print "| Source File | Line | Link Text | Target Path |" print "|-------------|------|-----------|-------------|" for link in $broken { print $"| ($link.source_file) | ($link.line) | ($link.text) | ($link.path) |" } } else { $broken | select source_file line text path resolved | table -e } # Save broken links report let report_file = "/Users/Akasha/project-provisioning/provisioning/tools/broken-links-report.json" $broken | save -f $report_file print $"\nšŸ’¾ Broken links report saved to: ($report_file)" if $fix { print "\nšŸ”§ Attempting to fix broken links..." # TODO: Implement auto-fix logic print "āš ļø Auto-fix not yet implemented" } } else { print "\nāœ… All links are valid!" } # Save full validation report let full_report_file = "/Users/Akasha/project-provisioning/provisioning/tools/doc-validation-full-report.json" $all_results | save -f $full_report_file print $"\nšŸ’¾ Full validation report saved to: ($full_report_file)" # Return exit code based on broken links if $broken_links > 0 { exit 1 } } # Export utility functions for use in other scripts export def "links find" [file: string] { extract-links $file } export def "links validate" [file: string] { validate-file-links $file } export def "links check" [source: string, target: string] { let resolved = (resolve-path $source $target) { source: $source target: $target resolved: $resolved exists: ($resolved | path exists) } }