diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h
index d56270d0c4..01f1970fe5 100644
--- a/include/crm/common/xml_compat.h
+++ b/include/crm/common/xml_compat.h
@@ -1,167 +1,163 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_XML_COMPAT__H
 #define PCMK__CRM_COMMON_XML_COMPAT__H
 
 #include <glib.h>               // gboolean
 #include <libxml/tree.h>        // xmlNode
 
 #include <crm/common/nvpair.h>  // crm_xml_add()
 #include <crm/common/xml_names.h>   // PCMK_XE_CLONE
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief Deprecated Pacemaker XML API
  * \ingroup core
  * \deprecated Do not include this header directly. The XML APIs in this
  *             header, and the header itself, will be removed in a future
  *             release.
  */
 
 //! \deprecated Do not use (will be removed in a future release)
 #define XML_PARANOIA_CHECKS 0
 
 //! \deprecated This function will be removed in a future release
 xmlDoc *getDocPtr(xmlNode *node);
 
 //! \deprecated This function will be removed in a future release
 int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child);
 
 //! \deprecated This function will be removed in a future release
 xmlNode *find_entity(xmlNode *parent, const char *node_name, const char *id);
 
 //! \deprecated This function will be removed in a future release
 char *xml_get_path(const xmlNode *xml);
 
 //! \deprecated This function will be removed in a future release
 void xml_log_changes(uint8_t level, const char *function, const xmlNode *xml);
 
 //! \deprecated This function will be removed in a future release
 void xml_log_patchset(uint8_t level, const char *function, const xmlNode *xml);
 
 //!  \deprecated Use xml_apply_patchset() instead
 gboolean apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml);
 
 //! \deprecated Do not use (will be removed in a future release)
 void crm_destroy_xml(gpointer data);
 
 //! \deprecated Check children member directly
 gboolean xml_has_children(const xmlNode *root);
 
 //! \deprecated Use crm_xml_add() with "true" or "false" instead
 static inline const char *
 crm_xml_add_boolean(xmlNode *node, const char *name, gboolean value)
 {
     return crm_xml_add(node, name, (value? "true" : "false"));
 }
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Use name member directly
 static inline const char *
 crm_element_name(const xmlNode *xml)
 {
     return (xml == NULL)? NULL : (const char *) xml->name;
 }
 
 //! \deprecated Do not use
 char *crm_xml_escape(const char *text);
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *copy_xml(xmlNode *src_node);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *add_node_copy(xmlNode *new_parent, xmlNode *xml_node);
 
 //! \deprecated Do not use
 void purge_diff_markers(xmlNode *a_node);
 
 //! \deprecated Do not use
 xmlNode *diff_xml_object(xmlNode *left, xmlNode *right, gboolean suppress);
 
 //! \deprecated Do not use
 xmlNode *subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right,
                              gboolean full, gboolean *changed,
                              const char *marker);
 
 //! \deprecated Do not use
 gboolean can_prune_leaf(xmlNode *xml_node);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *create_xml_node(xmlNode *parent, const char *name);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *pcmk_create_xml_text_node(xmlNode *parent, const char *name,
                                    const char *content);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *pcmk_create_html_node(xmlNode *parent, const char *element_name,
                                const char *id, const char *class_name,
                                const char *text);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *first_named_child(const xmlNode *parent, const char *name);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *find_xml_node(const xmlNode *root, const char *search_path,
                        gboolean must_find);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *crm_next_same_xml(const xmlNode *sibling);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 void xml_remove_prop(xmlNode *obj, const char *name);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 gboolean replace_xml_child(xmlNode *parent, xmlNode *child, xmlNode *update,
                            gboolean delete_only);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 gboolean update_xml_child(xmlNode *child, xmlNode *to_update);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 int find_xml_children(xmlNode **children, xmlNode *root, const char *tag,
                       const char *field, const char *value,
                       gboolean search_matches);
 
-//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
-xmlNode *get_xpath_object_relative(const char *xpath, xmlNode *xml_obj,
-                                   int error_level);
-
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Do not use
 gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs);
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Call \c crm_log_init() or \c crm_log_cli_init() instead
 void crm_xml_init(void);
 
 //! \deprecated Exit with \c crm_exit() instead
 void crm_xml_cleanup(void);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 void pcmk_free_xml_subtree(xmlNode *xml);
 
 // NOTE: sbd (as of at least 1.5.2) uses this
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 void free_xml(xmlNode *child);
 
 //! \deprecated Do not use Pacemaker for general-purpose XML manipulation
 xmlNode *expand_idref(xmlNode *input, xmlNode *top);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif // PCMK__CRM_COMMON_XML_COMPAT__H
diff --git a/lib/common/xpath.c b/lib/common/xpath.c
index 9fc95c5a5f..0eb0578877 100644
--- a/lib/common/xpath.c
+++ b/lib/common/xpath.c
@@ -1,396 +1,374 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 #include <stdio.h>
 #include <string.h>
 #include <crm/common/xml.h>
 #include <crm/common/xml_internal.h>
 #include "crmcommon_private.h"
 
 /*
  * From xpath2.c
  *
  * All the elements returned by an XPath query are pointers to
  * elements from the tree *except* namespace nodes where the XPath
  * semantic is different from the implementation in libxml2 tree.
  * As a result when a returned node set is freed when
  * xmlXPathFreeObject() is called, that routine must check the
  * element type. But node from the returned set may have been removed
  * by xmlNodeSetContent() resulting in access to freed data.
  *
  * This can be exercised by running
  *       valgrind xpath2 test3.xml '//discarded' discarded
  *
  * There is 2 ways around it:
  *   - make a copy of the pointers to the nodes from the result set
  *     then call xmlXPathFreeObject() and then modify the nodes
  * or
  * - remove the references from the node set, if they are not
        namespace nodes, before calling xmlXPathFreeObject().
  */
 void
 freeXpathObject(xmlXPathObjectPtr xpathObj)
 {
     int lpc, max = numXpathResults(xpathObj);
 
     if (xpathObj == NULL) {
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         if (xpathObj->nodesetval->nodeTab[lpc] && xpathObj->nodesetval->nodeTab[lpc]->type != XML_NAMESPACE_DECL) {
             xpathObj->nodesetval->nodeTab[lpc] = NULL;
         }
     }
 
     /* _Now_ it's safe to free it */
     xmlXPathFreeObject(xpathObj);
 }
 
 xmlNode *
 getXpathResult(xmlXPathObjectPtr xpathObj, int index)
 {
     xmlNode *match = NULL;
     int max = numXpathResults(xpathObj);
 
     CRM_CHECK(index >= 0, return NULL);
     CRM_CHECK(xpathObj != NULL, return NULL);
 
     if (index >= max) {
         crm_err("Requested index %d of only %d items", index, max);
         return NULL;
 
     } else if(xpathObj->nodesetval->nodeTab[index] == NULL) {
         /* Previously requested */
         return NULL;
     }
 
     match = xpathObj->nodesetval->nodeTab[index];
     CRM_CHECK(match != NULL, return NULL);
 
     if (xpathObj->nodesetval->nodeTab[index]->type != XML_NAMESPACE_DECL) {
         /* See the comment for freeXpathObject() */
         xpathObj->nodesetval->nodeTab[index] = NULL;
     }
 
     if (match->type == XML_DOCUMENT_NODE) {
         /* Will happen if section = '/' */
         match = match->children;
 
     } else if (match->type != XML_ELEMENT_NODE
                && match->parent && match->parent->type == XML_ELEMENT_NODE) {
         /* Return the parent instead */
         match = match->parent;
 
     } else if (match->type != XML_ELEMENT_NODE) {
         /* We only support searching nodes */
         crm_err("We only support %d not %d", XML_ELEMENT_NODE, match->type);
         match = NULL;
     }
     return match;
 }
 
 void
 dedupXpathResults(xmlXPathObjectPtr xpathObj)
 {
     int lpc, max = numXpathResults(xpathObj);
 
     if (xpathObj == NULL) {
         return;
     }
 
     for (lpc = 0; lpc < max; lpc++) {
         xmlNode *xml = NULL;
         gboolean dedup = FALSE;
 
         if (xpathObj->nodesetval->nodeTab[lpc] == NULL) {
             continue;
         }
 
         xml = xpathObj->nodesetval->nodeTab[lpc]->parent;
 
         for (; xml; xml = xml->parent) {
             int lpc2 = 0;
 
             for (lpc2 = 0; lpc2 < max; lpc2++) {
                 if (xpathObj->nodesetval->nodeTab[lpc2] == xml) {
                     xpathObj->nodesetval->nodeTab[lpc] = NULL;
                     dedup = TRUE;
                     break;
                 }
             }
 
             if (dedup) {
                 break;
             }
         }
     }
 }
 
 /* the caller needs to check if the result contains a xmlDocPtr or xmlNodePtr */
 xmlXPathObjectPtr
 xpath_search(const xmlNode *xml_top, const char *path)
 {
     xmlXPathObjectPtr xpathObj = NULL;
     xmlXPathContextPtr xpathCtx = NULL;
     const xmlChar *xpathExpr = (pcmkXmlStr) path;
 
     CRM_CHECK(path != NULL, return NULL);
     CRM_CHECK(xml_top != NULL, return NULL);
     CRM_CHECK(strlen(path) > 0, return NULL);
 
     xpathCtx = xmlXPathNewContext(xml_top->doc);
     pcmk__mem_assert(xpathCtx);
 
     xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx);
     xmlXPathFreeContext(xpathCtx);
     return xpathObj;
 }
 
 /*!
  * \brief Run a supplied function for each result of an xpath search
  *
  * \param[in,out] xml        XML to search
  * \param[in]     xpath      XPath search string
  * \param[in]     helper     Function to call for each result
  * \param[in,out] user_data  Data to pass to supplied function
  *
  * \note The helper function will be passed the XML node of the result,
  *       and the supplied user_data. This function does not otherwise
  *       use user_data.
  */
 void
 crm_foreach_xpath_result(xmlNode *xml, const char *xpath,
                          void (*helper)(xmlNode*, void*), void *user_data)
 {
     xmlXPathObjectPtr xpathObj = xpath_search(xml, xpath);
     int nresults = numXpathResults(xpathObj);
     int i;
 
     for (i = 0; i < nresults; i++) {
         xmlNode *result = getXpathResult(xpathObj, i);
 
         CRM_LOG_ASSERT(result != NULL);
         if (result) {
             (*helper)(result, user_data);
         }
     }
     freeXpathObject(xpathObj);
 }
 
 xmlNode *
 get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level)
 {
     int max;
     xmlNode *result = NULL;
     xmlXPathObjectPtr xpathObj = NULL;
     char *nodePath = NULL;
     char *matchNodePath = NULL;
 
     if (xpath == NULL) {
         return xml_obj;         /* or return NULL? */
     }
 
     xpathObj = xpath_search(xml_obj, xpath);
     nodePath = (char *)xmlGetNodePath(xml_obj);
     max = numXpathResults(xpathObj);
 
     if (max < 1) {
         if (error_level < LOG_NEVER) {
             do_crm_log(error_level, "No match for %s in %s",
                        xpath, pcmk__s(nodePath, "unknown path"));
             crm_log_xml_explicit(xml_obj, "Unexpected Input");
         }
 
     } else if (max > 1) {
         if (error_level < LOG_NEVER) {
             int lpc = 0;
 
             do_crm_log(error_level, "Too many matches for %s in %s",
                        xpath, pcmk__s(nodePath, "unknown path"));
 
             for (lpc = 0; lpc < max; lpc++) {
                 xmlNode *match = getXpathResult(xpathObj, lpc);
 
                 CRM_LOG_ASSERT(match != NULL);
                 if (match != NULL) {
                     matchNodePath = (char *) xmlGetNodePath(match);
                     do_crm_log(error_level, "%s[%d] = %s",
                                xpath, lpc,
                                pcmk__s(matchNodePath, "unrecognizable match"));
                     free(matchNodePath);
                 }
             }
             crm_log_xml_explicit(xml_obj, "Bad Input");
         }
 
     } else {
         result = getXpathResult(xpathObj, 0);
     }
 
     freeXpathObject(xpathObj);
     free(nodePath);
 
     return result;
 }
 
 /*!
  * \internal
  * \brief Get an XPath string that matches an XML element as closely as possible
  *
  * \param[in] xml  The XML element for which to build an XPath string
  *
  * \return A \p GString that matches \p xml, or \p NULL if \p xml is \p NULL.
  *
  * \note The caller is responsible for freeing the string using
  *       \p g_string_free().
  */
 GString *
 pcmk__element_xpath(const xmlNode *xml)
 {
     const xmlNode *parent = NULL;
     GString *xpath = NULL;
     const char *id = NULL;
 
     if (xml == NULL) {
         return NULL;
     }
 
     parent = xml->parent;
     xpath = pcmk__element_xpath(parent);
     if (xpath == NULL) {
         xpath = g_string_sized_new(256);
     }
 
     // Build xpath like "/" -> "/cib" -> "/cib/configuration"
     if (parent == NULL) {
         g_string_append_c(xpath, '/');
     } else if (parent->parent == NULL) {
         g_string_append(xpath, (const gchar *) xml->name);
     } else {
         pcmk__g_strcat(xpath, "/", (const char *) xml->name, NULL);
     }
 
     id = pcmk__xe_id(xml);
     if (id != NULL) {
         pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", id, "']", NULL);
     }
 
     return xpath;
 }
 
 char *
 pcmk__xpath_node_id(const char *xpath, const char *node)
 {
     char *retval = NULL;
     char *patt = NULL;
     char *start = NULL;
     char *end = NULL;
 
     if (node == NULL || xpath == NULL) {
         return retval;
     }
 
     patt = crm_strdup_printf("/%s[@" PCMK_XA_ID "=", node);
     start = strstr(xpath, patt);
 
     if (!start) {
         free(patt);
         return retval;
     }
 
     start += strlen(patt);
     start++;
 
     end = strstr(start, "\'");
     CRM_ASSERT(end);
     retval = strndup(start, end-start);
 
     free(patt);
     return retval;
 }
 
 static int
 output_attr_child(xmlNode *child, void *userdata)
 {
     pcmk__output_t *out = userdata;
 
     out->info(out, "  Value: %s \t(id=%s)",
               crm_element_value(child, PCMK_XA_VALUE),
               pcmk__s(pcmk__xe_id(child), "<none>"));
     return pcmk_rc_ok;
 }
 
 void
 pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
                                  const char *name)
 {
     if (out == NULL || name == NULL || search == NULL ||
         search->children == NULL) {
         return;
     }
 
     out->info(out, "Multiple attributes match " PCMK_XA_NAME "=%s", name);
     pcmk__xe_foreach_child(search, NULL, output_attr_child, out);
 }
 
 // Deprecated functions kept only for backward API compatibility
 // LCOV_EXCL_START
 
 #include <crm/common/xml_compat.h>
 
 /*!
  * \deprecated This function will be removed in a future release
  * \brief Get an XPath string that matches an XML element as closely as possible
  *
  * \param[in] xml  The XML element for which to build an XPath string
  *
  * \return A string that matches \p xml, or \p NULL if \p xml is \p NULL.
  *
  * \note The caller is responsible for freeing the string using free().
  */
 char *
 xml_get_path(const xmlNode *xml)
 {
     char *path = NULL;
     GString *g_path = pcmk__element_xpath(xml);
 
     if (g_path == NULL) {
         return NULL;
     }
     path = pcmk__str_copy(g_path->str);
     g_string_free(g_path, TRUE);
     return path;
 }
 
-xmlNode *
-get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level)
-{
-    xmlNode *result = NULL;
-    char *xpath_full = NULL;
-    char *xpath_prefix = NULL;
-
-    if (xml_obj == NULL || xpath == NULL) {
-        return NULL;
-    }
-
-    xpath_prefix = (char *)xmlGetNodePath(xml_obj);
-
-    xpath_full = crm_strdup_printf("%s%s", xpath_prefix, xpath);
-
-    result = get_xpath_object(xpath_full, xml_obj, error_level);
-
-    free(xpath_prefix);
-    free(xpath_full);
-    return result;
-}
-
 // LCOV_EXCL_STOP
 // End deprecated API