package model import ( "fmt" "os" "path/filepath" "strings" "voicesnap/internal/paths" ) type resolveCandidate struct { dir string sourceDirKind string fromState bool } func ResolveModel(modelID string) (ResolvedModel, error) { return ResolveModelInRoot(modelID, paths.ModelsRoot()) } func ResolveModelInRoot(modelID, modelsRoot string) (ResolvedModel, error) { profile, err := GetModelProfile(modelID) if err != nil { return ResolvedModel{}, err } result := ResolvedModel{ ModelID: profile.ID, Profile: profile, BackendKind: profile.BackendKind, ProviderOrder: append([]string(nil), profile.ProviderOrder...), Status: ModelNotInstalled, Files: make(map[string]string), } state, stateErr := LoadInstallStateFromRoot(modelsRoot) if stateErr != nil { result.Problems = append(result.Problems, fmt.Sprintf("state invalid: %v", stateErr)) } candidates := make([]resolveCandidate, 0, 3) seen := make(map[string]bool) stateCandidateFailed := stateErr != nil addCandidate := func(relOrAbs, sourceDirKind string, fromState bool) { if relOrAbs == "" { return } if fromState && !validStateRelativePath(relOrAbs) { stateCandidateFailed = true result.Problems = append(result.Problems, fmt.Sprintf("state path escapes models root: %s", relOrAbs)) return } dir := filepath.Join(modelsRoot, relOrAbs) clean := filepath.Clean(dir) if seen[clean] { return } seen[clean] = true candidates = append(candidates, resolveCandidate{ dir: clean, sourceDirKind: sourceDirKind, fromState: fromState, }) } if state != nil { if installed, ok := state.InstalledModels[profile.ID]; ok { addCandidate(installed.Path, installed.SourceDirKind, true) } } addCandidate(profile.InstallDirName, "new", false) for _, legacy := range profile.LegacyDirNames { addCandidate(legacy, "legacy", false) } anyDirExists := false var lastValidation ValidationResult var existingInvalidValidation ValidationResult for _, candidate := range candidates { if directoryExists(candidate.dir) { anyDirExists = true } validation := ValidateModelDir(profile, candidate.dir) if validation.Valid { result.RootDir = candidate.dir result.SourceDirKind = candidate.sourceDirKind result.Files = validation.Files if stateCandidateFailed { result.Status = ModelStateInvalidFallbackFound } else { result.Status = ModelInstalledValid } return result, nil } lastValidation = validation if directoryExists(candidate.dir) && !validation.Valid && len(existingInvalidValidation.Missing) == 0 && len(existingInvalidValidation.Problems) == 0 { existingInvalidValidation = validation } if candidate.fromState { stateCandidateFailed = true result.Problems = append(result.Problems, validation.Problems...) } } switch { case stateCandidateFailed: result.Status = ModelStateInvalidNoFallback case anyDirExists: result.Status = ModelInstalledIncomplete result.Missing = append(result.Missing, existingInvalidValidation.Missing...) result.Problems = append(result.Problems, existingInvalidValidation.Problems...) default: result.Status = ModelNotInstalled result.Missing = append(result.Missing, lastValidation.Missing...) result.Problems = append(result.Problems, lastValidation.Problems...) } return result, nil } func directoryExists(path string) bool { info, err := os.Stat(path) return err == nil && info.IsDir() } func validStateRelativePath(path string) bool { if filepath.IsAbs(path) { return false } clean := filepath.Clean(path) return clean != "." && clean != ".." && !strings.HasPrefix(clean, ".."+string(os.PathSeparator)) }