package artifacts import ( "os" "path/filepath" "strings" ) // ResolveSessionLocalPathForRead resolves manifest/local artifact paths for read use-cases. // Invariants for stage consumers: // - local paths may be absolute, workspace-root qualified, or session-workdir relative. // - callers should resolve through this helper instead of manually joining session/workdir roots. func ResolveSessionLocalPathForRead(paths SessionPaths, localPath string) string { p := filepath.Clean(strings.TrimSpace(localPath)) if p == "" { return "" } if filepath.IsAbs(p) { return p } // Already qualified against known roots. if underRoot(p, paths.WorkspaceRoot) || underRoot(p, paths.Root) { return p } candidates := []string{ p, filepath.Clean(filepath.Join(paths.WorkspaceRoot, p)), filepath.Clean(filepath.Join(paths.Root, p)), } for _, c := range candidates { if c == "" { continue } if _, err := os.Stat(c); err == nil { return c } } // Deterministic fallback for ambiguous relative values. if strings.TrimSpace(paths.WorkspaceRoot) != "" { return candidates[1] } return candidates[2] } func underRoot(path, root string) bool { p := filepath.Clean(strings.TrimSpace(path)) r := filepath.Clean(strings.TrimSpace(root)) if p == "" || r == "" { return false } if p == r { return true } return strings.HasPrefix(p, r+string(filepath.Separator)) }