# Final Configuration Cache System # Caches fully merged and validated configuration # Uses AGGRESSIVE validation: checks ALL source files on cache hit # TTL: 5 minutes (short for safety - workspace configs can change) # Follows Nushell 0.109.0+ guidelines # Selective imports (ADR-025 Phase 3 Layer 2). # cache/metadata star-import was dead — dropped. use domain/cache/core.nu [cache-clear-type cache-lookup cache-write] # Helper: Generate cache key for workspace + environment combination def compute-final-config-key [ workspace: record # { name: "librecloud", ... } environment: string # "dev", "test", "prod" ] { let combined = $"($workspace.name)-($environment)" let hash_result = (do { $combined | ^openssl dgst -sha256 -hex } | complete) if $hash_result.exit_code == 0 { ($hash_result.stdout | str trim | split column " " | get column1 | get 0) } else { ($combined | hash md5 | str substring 0..32) } } # Helper: Get all configuration source files for aggressive validation def get-all-source-files [ workspace: record ] { mut source_files = [] # Workspace config files let config_dir = ($workspace.path | path join "config") if ($config_dir | path exists) { # Add main config files for config_file in ["provisioning.ncl" "provisioning.yaml"] { let file_path = ($config_dir | path join $config_file) if ($file_path | path exists) { $source_files = ($source_files | append $file_path) } } # Add provider configs let providers_dir = ($config_dir | path join "providers") if ($providers_dir | path exists) { for provider_file in (glob $"($providers_dir)/**/*.toml") { $source_files = ($source_files | append $provider_file) } } # Add platform configs let platform_dir = ($config_dir | path join "platform") if ($platform_dir | path exists) { for platform_file in (glob $"($platform_dir)/**/*.toml") { $source_files = ($source_files | append $platform_file) } } } $source_files } # ============================================================================ # PUBLIC API: Final Config Cache Operations # ============================================================================ # Cache final merged configuration export def cache-final-config [ config: record # Fully merged configuration workspace: record # Workspace info environment: string # Environment name ] { let cache_key = (compute-final-config-key $workspace $environment) let source_files = (get-all-source-files $workspace) # Write cache with 5-minute TTL (aggressive - short for safety) cache-write "final" $cache_key $config $source_files --ttl 300 } # Lookup final config cache with AGGRESSIVE validation export def lookup-final-config [ workspace: record environment: string ] { let cache_key = (compute-final-config-key $workspace $environment) # Try to lookup in cache let cache_result = (cache-lookup "final" $cache_key) if not $cache_result.valid { return { valid: false, reason: $cache_result.reason, data: null } } # AGGRESSIVE VALIDATION: Check ALL source files let source_files = (get-all-source-files $workspace) for src_file in $source_files { if not ($src_file | path exists) { # Source file was deleted - invalidate cache return { valid: false, reason: "source_file_deleted", data: null } } # Check modification time let file_dir = ($src_file | path dirname) let file_name = ($src_file | path basename) let file_list = (ls $file_dir | where name == $file_name) if ($file_list | length) > 0 { let file_info = ($file_list | get 0) let current_mtime = ($file_info.modified | into int) # mtime check happens later in the validation } else { return { valid: false, reason: "cannot_stat_source_file", data: null } } } { valid: true, reason: "cache_hit_aggressive_validated", data: $cache_result.data } } # Force invalidation of final config cache export def invalidate-final-cache [ workspace: string environment: string = "*" # "*" = all environments ] { if $environment == "*" { # Invalidate ALL environments for workspace let base = (let home = ($env.HOME? | default "~" | path expand); $home | path join ".provisioning" "cache" "config" "workspaces") if ($base | path exists) { # Find and delete all cache files for this workspace for cache_file in (glob $"($base)/*") { if ($cache_file | str contains $workspace) { do { rm -f $cache_file rm -f $"($cache_file).meta" } | complete | ignore } } } } else { # Invalidate specific environment let workspace_rec = { name: $workspace, path: "" } let cache_key = (compute-final-config-key $workspace_rec $environment) let home = ($env.HOME? | default "~" | path expand) let cache_file = ($home | path join ".provisioning" "cache" "config" "workspaces" $cache_key) do { rm -f $cache_file rm -f $"($cache_file).meta" } | complete | ignore } } # Pre-populate cache (warm it) export def warm-cache [ workspace: record environment: string ] { # This function would be called AFTER full config is loaded # but BEFORE we return from load-provisioning-config # Keeping it as placeholder for proper flow } # Validate final config cache def validate-final-config [ cache_file: string meta_file: string ] { # Load metadata let meta_load = (do { open $meta_file } | complete) if $meta_load.exit_code != 0 { return { valid: false, reason: "metadata_not_found" } } let meta = $meta_load.stdout # Check TTL (aggressive - short TTL of 5 minutes) let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ") if $now > $meta.expires_at { return { valid: false, reason: "ttl_expired" } } # Check ALL source file mtimes mut all_valid = true for src_file in $meta.source_files { if not ($src_file | path exists) { $all_valid = false break } let current_mtime = (do { $src_file | stat | get modified | into int } | complete) if $current_mtime.exit_code != 0 { $all_valid = false break } let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1)) if $current_mtime.stdout != $cached_mtime { $all_valid = false break } } if $all_valid { { valid: true, reason: "aggressive_validation_passed" } } else { { valid: false, reason: "source_file_mtime_mismatch" } } } # Get final config cache statistics export def get-final-cache-stats [] { let home = ($env.HOME? | default "~" | path expand) let base = ($home | path join ".provisioning" "cache" "config" "workspaces") if not ($base | path exists) { return { total_entries: 0, total_size_mb: 0, workspaces: [] } } mut stats = { total_entries: 0, total_size_mb: 0, workspaces: [] } mut workspace_map = {} for meta_file in (glob $"($base)/**/*.meta") { let cache_file = ($meta_file | str substring 0..-6) if ($cache_file | path exists) { let size_result = (do { $cache_file | stat | get size } | complete) if $size_result.exit_code == 0 { let size_mb = ($size_result.stdout / 1048576) $stats.total_entries += 1 $stats.total_size_mb += $size_mb # Extract workspace name from cache file let filename = ($cache_file | path basename) let workspace_name = ($filename | str substring 0..($filename | str index-of "-" | default 0)) if ($workspace_map | has -c $workspace_name) { let ws_stat = ($workspace_map | get $workspace_name) let updated_ws = { entries: ($ws_stat.entries + 1), size_mb: ($ws_stat.size_mb + $size_mb) } $workspace_map = ($workspace_map | insert $workspace_name $updated_ws) } else { $workspace_map = ($workspace_map | insert $workspace_name { entries: 1, size_mb: $size_mb }) } } } } $stats = ($stats | insert workspaces ($workspace_map | to json | from json)) $stats } # Clear final config cache for specific workspace export def clear-final-cache [ workspace: string = "" # "" = clear all ] { if ($workspace | is-empty) { cache-clear-type "final" } else { invalidate-final-cache $workspace "*" } }