#!/usr/bin/env nu # ID Consistency Validation Script # Nushell version of validate-id-consistency.sh # Validates ID consistency across languages for content files def main [...args] { let config = { content_dir: ($env.SITE_CONTENT_PATH? | default "site/content"), languages: ["en", "es"] } print $"(ansi blue)🔍 ID Consistency Validation(ansi reset)" if ($args | length) > 0 and ($args | first) in ["-h", "--help", "help"] { show_id_consistency_usage exit 0 } # Get content types dynamically let content_types = get_content_types $config.content_dir mut total_errors = 0 $total_errors = $total_errors + (validate_filename_consistency $config $content_types) $total_errors = $total_errors + (validate_id_field_consistency $config $content_types) $total_errors = $total_errors + (validate_index_id_consistency $config $content_types) show_id_consistency_summary $config $content_types $total_errors if $total_errors > 0 { exit 1 } } # Show usage information def show_id_consistency_usage [] { print "Usage: nu validate-id-consistency.nu [OPTIONS]" print "" print "This script validates ID consistency across languages:" print " • Filename consistency (same files exist in all languages)" print " • Frontmatter ID field consistency" print " • Index.json ID consistency with actual files" print "" print "Options:" print " -h, --help Show this help message" print "" print "Environment Variables:" print " SITE_CONTENT_PATH Content root directory [default: site/content]" } # Get content types from directory structure def get_content_types [content_dir: string] { if not ($content_dir | path exists) { return [] } ls $content_dir | where type == dir | where name !~ "(locales|themes|menus|footer|tmp|routes)" | get name | path basename } # Validate filename consistency across languages def validate_filename_consistency [config, content_types: list] { print $"(ansi blue)🔧 Validating filename consistency...(ansi reset)" mut errors = 0 for content_type in $content_types { print $"(ansi yellow) Checking ($content_type) filename consistency...(ansi reset)" # Collect filenames for each language mut language_files = {} mut all_unique_filenames = [] for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if ($content_dir | path exists) { let md_files = (ls $"($content_dir)/**/*.md" | where type == file) let filenames = ($md_files | each { |file| $file.name | path basename | path parse | get stem } | sort) $language_files = ($language_files | upsert $language $filenames) $all_unique_filenames = ($all_unique_filenames | append $filenames | uniq | sort) print $"(ansi blue) ($language): ($filenames | length) files(ansi reset)" } else { print $"(ansi yellow) ($language): directory not found(ansi reset)" $language_files = ($language_files | upsert $language []) } } if ($all_unique_filenames | length) == 0 { print $"(ansi yellow) No content files found for ($content_type)(ansi reset)" continue } # Check each unique filename exists in all languages let languages_with_content = ($language_files | columns | where {|lang| ($language_files | get $lang | length) > 0}) if ($languages_with_content | length) < 2 { print $"(ansi yellow) Only one language has content for ($content_type)(ansi reset)" continue } print $"(ansi blue) Checking ($all_unique_filenames | length) unique filenames across ($languages_with_content | length) languages...(ansi reset)" for filename in $all_unique_filenames { mut missing_languages = [] for language in $languages_with_content { let language_filenames = ($language_files | get $language) if $filename not-in $language_filenames { $missing_languages = ($missing_languages | append $language) } } if ($missing_languages | length) > 0 { print $"(ansi red) ❌ ($filename): missing in languages: ($missing_languages | str join ', ')(ansi reset)" $errors += ($missing_languages | length) } else { if ($all_unique_filenames | length) <= 10 { # Only show details for small sets print $"(ansi green) ✅ ($filename): present in all languages(ansi reset)" } } } if $errors == 0 and ($all_unique_filenames | length) > 10 { print $"(ansi green) ✅ All ($all_unique_filenames | length) files present in all languages(ansi reset)" } } if $errors == 0 { print $"(ansi green)✅ Filename consistency validation passed(ansi reset)" } else { print $"(ansi red)❌ Filename consistency validation failed with ($errors) missing files(ansi reset)" } $errors } # Validate ID field consistency in frontmatter def validate_id_field_consistency [config, content_types: list] { print $"(ansi blue)🔧 Validating frontmatter ID field consistency...(ansi reset)" mut errors = 0 for content_type in $content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if not ($content_dir | path exists) { continue } print $"(ansi yellow) Checking ($content_type) ID fields (($language))...(ansi reset)" let md_files = (ls $"($content_dir)/**/*.md" | where type == file) for file_info in $md_files { let md_file = $file_info.name let expected_id = ($md_file | path basename | path parse | get stem) try { let content_lines = (open $md_file | lines) let frontmatter = (extract_frontmatter $content_lines) let actual_id = (extract_frontmatter_field $frontmatter "id" "") if not ($actual_id | is-empty) and $actual_id != $expected_id { print $"(ansi red) ❌ ($expected_id): frontmatter id '($actual_id)' doesn't match filename(ansi reset)" $errors += 1 } else if not ($actual_id | is-empty) { print $"(ansi green) ✅ ($expected_id): ID field matches filename(ansi reset)" } # Note: Empty ID field is acceptable as filename will be used } catch { print $"(ansi red) ❌ ($expected_id): failed to parse frontmatter(ansi reset)" # Note: Error count handled at validation level } } } } if $errors == 0 { print $"(ansi green)✅ ID field consistency validation passed(ansi reset)" } else { print $"(ansi red)❌ ID field consistency validation failed with ($errors) inconsistencies(ansi reset)" } $errors } # Validate index.json ID consistency with actual files def validate_index_id_consistency [config, content_types: list] { print $"(ansi blue)🔧 Validating index.json ID consistency...(ansi reset)" mut errors = 0 for content_type in $content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" let index_file = $"($content_dir)/index.json" if not ($content_dir | path exists) or not ($index_file | path exists) { continue } print $"(ansi yellow) Checking ($content_type) index consistency (($language))...(ansi reset)" try { # Read index.json let index_content = (open $index_file | from json) # Get content array let array_fields = ($index_content | columns | where $it =~ "_posts|prescriptions") if ($array_fields | length) == 0 { print $"(ansi yellow) ⚠️ No content array found in index.json(ansi reset)" continue } let array_name = ($array_fields | first) let content_entries = ($index_content | get $array_name) # Get actual markdown files let md_files = (ls $"($content_dir)/**/*.md" | where type == file) let actual_filenames = ($md_files | each { |file| $file.name | path basename | path parse | get stem } | sort) # Extract IDs from index entries let index_ids = ($content_entries | where {|entry| "id" in ($entry | columns)} | get id | sort) print $"(ansi blue) Index entries: ($content_entries | length), Actual files: ($actual_filenames | length)(ansi reset)" # Check index entries have corresponding files for index_id in $index_ids { if $index_id not-in $actual_filenames { print $"(ansi red) ❌ Index entry '($index_id)' has no corresponding file ($index_id).md(ansi reset)" $errors += 1 } } # Check files have corresponding index entries for filename in $actual_filenames { if $filename not-in $index_ids { print $"(ansi yellow) ⚠️ File '($filename).md' not found in index.json(ansi reset)" # This is a warning, not an error, as files might be unpublished } } # Check for duplicate IDs in index let duplicate_ids = ($index_ids | group-by {|x| $x} | where {|group| ($group | get items | length) > 1} | get group | get 0) for dup_id in $duplicate_ids { print $"(ansi red) ❌ Duplicate ID in index: '($dup_id)'(ansi reset)" $errors += 1 } if $errors == 0 { print $"(ansi green) ✅ Index consistency validated: ($index_ids | length) entries match files(ansi reset)" } } catch { print $"(ansi red) ❌ Failed to validate index.json(ansi reset)" # Note: Error count handled at validation level } } } if $errors == 0 { print $"(ansi green)✅ Index ID consistency validation passed(ansi reset)" } else { print $"(ansi red)❌ Index ID consistency validation failed with ($errors) issues(ansi reset)" } $errors } # Extract frontmatter from content lines def extract_frontmatter [content_lines: list] { if ($content_lines | length) == 0 or ($content_lines | first) != "---" { return [] } mut frontmatter_lines = [] mut in_frontmatter = false mut line_count = 0 for line in $content_lines { $line_count = $line_count + 1 if $line_count == 1 and $line == "---" { $in_frontmatter = true continue } if $in_frontmatter and $line == "---" { break } if $in_frontmatter { $frontmatter_lines = ($frontmatter_lines | append $line) } } $frontmatter_lines } # Extract field value from frontmatter def extract_frontmatter_field [frontmatter: list, key: string, default: string] { for line in $frontmatter { if $line =~ $"($key):\\s*(.+)" { let matches = ($line | parse --regex $"($key):\\s*\"?([^\"\\n]+)\"?") if ($matches | length) > 0 { return ($matches | first | get capture0 | str trim) } } } $default } # Show ID consistency validation summary def show_id_consistency_summary [config, content_types: list, total_errors: int] { print "" print $"(ansi blue)📊 ID Consistency Validation Summary(ansi reset)" print $"(ansi blue)Content root: ($config.content_dir)(ansi reset)" print $"(ansi blue)Languages: ($config.languages | str join ', ')(ansi reset)" print $"(ansi blue)Content types: ($content_types | str join ', ')(ansi reset)" print "" if $total_errors == 0 { print $"(ansi green)🎉 All ID consistency validations passed!(ansi reset)" print "" print $"(ansi green)Your content has perfect ID consistency with:(ansi reset)" print $"(ansi green) ✅ Matching filenames across all languages(ansi reset)" print $"(ansi green) ✅ Consistent frontmatter ID fields(ansi reset)" print $"(ansi green) ✅ Accurate index.json entries(ansi reset)" } else { print $"(ansi red)❌ ID consistency validation failed with ($total_errors) total issues(ansi reset)" print "" print $"(ansi yellow)💡 To fix ID consistency issues:(ansi reset)" print " 1. Create missing content files in all languages using the same filename" print " 2. Ensure frontmatter 'id' fields match the filename (or remove the id field)" print " 3. Update index.json files to include all published content" print " 4. Remove duplicate entries from index.json files" print " 5. Use the content manager to regenerate indices:" print $" nu scripts/content/content-manager.nu generate-indices" print " 6. Run validation again after fixes" } }