diff --git a/cts/schemas/test-3/ref.err/id-ref.ref.err-99 b/cts/schemas/test-3/ref.err/id-ref.ref.err-99
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/cts/schemas/test-3/ref/id-ref.ref-0 b/cts/schemas/test-3/ref/id-ref.ref-0
index 56992f0101..85d347d691 100644
--- a/cts/schemas/test-3/ref/id-ref.ref-0
+++ b/cts/schemas/test-3/ref/id-ref.ref-0
@@ -1,66 +1,72 @@
-<cib crm_feature_set="3.19.7" validate-with="pacemaker-3.10" epoch="16" num_updates="0" admin_epoch="0">
-  <configuration>
+<cib crm_feature_set="3.19.7" validate-with="pacemaker-3.10" epoch="16" num_updates="0" admin_epoch="0" original="1">
+  <configuration original="1">
     <!-- The essential elements of this test are:
          * There is a cluster_properties_set element with an id attribute (set
            to cluster-properties1) and a set of nvpair children.
          * There are two cluster_properties_set elements with an id-ref
            attribute (set to cluster-properties1): one before and one after the
            original.
          * There is a primitive resource (rsc1) with a meta_attributes element
            containing nvpair children.
            * The first nvpair is a definition (has an id attribute).
            * The second has an id-ref attribute with no name attribute.
            * The third has id-ref="cluster-properties1-option1" and
              name="option3". Setting both id-ref and name is an undocumented
              feature that allows the same nvpair value to be used with multiple
              names (see commit 3912538 and associated pull request).
 
          In this situation:
          * In the first step of the upgrade transformation pipeline:
            * Each element with an id-ref attribute without a name attribute
              should be replaced by a copy of the element whose id attribute is
              set to the same value, but with the "original" attribute set to 1.
            * Each element with an id-ref attribute and a name attribute should
              be replaced by a copy of the element whose id attribute is set to
              the original id-ref value, except that in the copy:
              * The id attribute begins with $upgrade_prefix and ends with the
                value of @name.
              * The name attribute is overridden by the reference's @name value.
          * In the final step:
            * Resolved references that did not have name attributes should be
              converted back to references.
            * For resolved references that did have name attributes, such that
              the id of the resolved element differs from the original id-ref
              value:
              * The first element with the new id value remains expanded as a
                definition.
              * Any subsequent elements with the new id value are converted to
                references to the first one.
       -->
-    <crm_config>
-      <cluster_property_set id-ref="cluster-properties1"/>
-      <cluster_property_set id="cluster-properties1">
-        <nvpair id="cluster-properties1-option1" name="option1" value="value1"/>
-        <nvpair id="cluster-properties1-option2" name="option2" value="value2"/>
+    <crm_config original="1">
+      <cluster_property_set id="cluster-properties1" original="0">
+        <nvpair id="cluster-properties1-option1" name="option1" value="value1" original="0"/>
+        <nvpair id="cluster-properties1-option2" name="option2" value="value2" original="0"/>
+      </cluster_property_set>
+      <cluster_property_set id="cluster-properties1" original="1">
+        <nvpair id="cluster-properties1-option1" name="option1" value="value1" original="1"/>
+        <nvpair id="cluster-properties1-option2" name="option2" value="value2" original="1"/>
+      </cluster_property_set>
+      <cluster_property_set id="cluster-properties1" original="0">
+        <nvpair id="cluster-properties1-option1" name="option1" value="value1" original="0"/>
+        <nvpair id="cluster-properties1-option2" name="option2" value="value2" original="0"/>
       </cluster_property_set>
-      <cluster_property_set id-ref="cluster-properties1"/>
     </crm_config>
-    <nodes/>
-    <resources>
-      <primitive id="rsc1" class="ocf" provider="pacemaker" type="Dummy">
-        <meta_attributes id="rsc1-meta_attributes">
-          <nvpair id="rsc1-meta_attributes-option1" name="option1" value="valueX"/>
-          <nvpair id-ref="cluster-properties1-option1"/>
-          <nvpair id-ref="cluster-properties1-option1" name="option3"/>
+    <nodes original="1"/>
+    <resources original="1">
+      <primitive id="rsc1" class="ocf" provider="pacemaker" type="Dummy" original="1">
+        <meta_attributes id="rsc1-meta_attributes" original="1">
+          <nvpair id="rsc1-meta_attributes-option1" name="option1" value="valueX" original="1"/>
+          <nvpair id="cluster-properties1-option1" name="option1" value="value1" original="0"/>
+          <nvpair id="pcmk__3_10_upgrade-cluster-properties1-option1-option3" name="option3" value="value1" original="0"/>
         </meta_attributes>
       </primitive>
-      <primitive id="rsc2" class="ocf" provider="pacemaker" type="Dummy">
-        <meta_attributes id="rsc2-meta_attributes">
-          <nvpair id-ref="cluster-properties1-option1" name="option3"/>
+      <primitive id="rsc2" class="ocf" provider="pacemaker" type="Dummy" original="1">
+        <meta_attributes id="rsc2-meta_attributes" original="1">
+          <nvpair id="pcmk__3_10_upgrade-cluster-properties1-option1-option3" name="option3" value="value1" original="0"/>
         </meta_attributes>
       </primitive>
     </resources>
-    <constraints/>
+    <constraints original="1"/>
   </configuration>
-  <status/>
+  <status original="1"/>
 </cib>
diff --git a/cts/schemas/test-3/ref/id-ref.ref-0 b/cts/schemas/test-3/ref/id-ref.ref-99
similarity index 94%
copy from cts/schemas/test-3/ref/id-ref.ref-0
copy to cts/schemas/test-3/ref/id-ref.ref-99
index 56992f0101..d8a2d3975a 100644
--- a/cts/schemas/test-3/ref/id-ref.ref-0
+++ b/cts/schemas/test-3/ref/id-ref.ref-99
@@ -1,66 +1,66 @@
 <cib crm_feature_set="3.19.7" validate-with="pacemaker-3.10" epoch="16" num_updates="0" admin_epoch="0">
   <configuration>
     <!-- The essential elements of this test are:
          * There is a cluster_properties_set element with an id attribute (set
            to cluster-properties1) and a set of nvpair children.
          * There are two cluster_properties_set elements with an id-ref
            attribute (set to cluster-properties1): one before and one after the
            original.
          * There is a primitive resource (rsc1) with a meta_attributes element
            containing nvpair children.
            * The first nvpair is a definition (has an id attribute).
            * The second has an id-ref attribute with no name attribute.
            * The third has id-ref="cluster-properties1-option1" and
              name="option3". Setting both id-ref and name is an undocumented
              feature that allows the same nvpair value to be used with multiple
              names (see commit 3912538 and associated pull request).
 
          In this situation:
          * In the first step of the upgrade transformation pipeline:
            * Each element with an id-ref attribute without a name attribute
              should be replaced by a copy of the element whose id attribute is
              set to the same value, but with the "original" attribute set to 1.
            * Each element with an id-ref attribute and a name attribute should
              be replaced by a copy of the element whose id attribute is set to
              the original id-ref value, except that in the copy:
              * The id attribute begins with $upgrade_prefix and ends with the
                value of @name.
              * The name attribute is overridden by the reference's @name value.
          * In the final step:
            * Resolved references that did not have name attributes should be
              converted back to references.
            * For resolved references that did have name attributes, such that
              the id of the resolved element differs from the original id-ref
              value:
              * The first element with the new id value remains expanded as a
                definition.
              * Any subsequent elements with the new id value are converted to
                references to the first one.
       -->
     <crm_config>
       <cluster_property_set id-ref="cluster-properties1"/>
       <cluster_property_set id="cluster-properties1">
         <nvpair id="cluster-properties1-option1" name="option1" value="value1"/>
         <nvpair id="cluster-properties1-option2" name="option2" value="value2"/>
       </cluster_property_set>
       <cluster_property_set id-ref="cluster-properties1"/>
     </crm_config>
     <nodes/>
     <resources>
       <primitive id="rsc1" class="ocf" provider="pacemaker" type="Dummy">
         <meta_attributes id="rsc1-meta_attributes">
           <nvpair id="rsc1-meta_attributes-option1" name="option1" value="valueX"/>
           <nvpair id-ref="cluster-properties1-option1"/>
-          <nvpair id-ref="cluster-properties1-option1" name="option3"/>
+          <nvpair id="pcmk__3_10_upgrade-cluster-properties1-option1-option3" name="option3" value="value1"/>
         </meta_attributes>
       </primitive>
       <primitive id="rsc2" class="ocf" provider="pacemaker" type="Dummy">
         <meta_attributes id="rsc2-meta_attributes">
-          <nvpair id-ref="cluster-properties1-option1" name="option3"/>
+          <nvpair id-ref="pcmk__3_10_upgrade-cluster-properties1-option1-option3"/>
         </meta_attributes>
       </primitive>
     </resources>
     <constraints/>
   </configuration>
   <status/>
 </cib>
diff --git a/xml/upgrade-3.10-0.xsl b/xml/upgrade-3.10-0.xsl
index ed33f031c8..4a1d0a26b2 100644
--- a/xml/upgrade-3.10-0.xsl
+++ b/xml/upgrade-3.10-0.xsl
@@ -1,23 +1,85 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
  Use comments liberally as future maintainers may be unfamiliar with XSLT.
  -->
 
 <!--
  upgrade-3.10-0.xsl
 
  Guarantees after this transformation:
+ * There are no elements with the id-ref attribute. If there were any prior to
+   this transformation, they have been resolved as described in
+   upgrade-3.10-common.xsl.
  -->
 
 <xsl:stylesheet version="1.0"
                 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
 <xsl:import href="upgrade-3.10-common.xsl"/>
 
-<!-- Copy everything unaltered by default -->
+<!-- Index all elements in the document on the id attribute -->
+<xsl:key name="element_id" match="*" use="@id"/>
+
+<!--
+ Copy everything unaltered by default, except that we set "original"
+
+ Params:
+ * original: See identity template
+ -->
 <xsl:template match="/|@*|node()">
-    <xsl:call-template name="identity"/>
+    <!-- Default original="1" if unspecified -->
+    <xsl:param name="original" select="'1'"/>
+
+    <xsl:call-template name="identity">
+        <!-- If an element gets original="0", so do its descendants -->
+        <xsl:with-param name="original" select="$original"/>
+    </xsl:call-template>
+</xsl:template>
+
+<!--
+ If an element has an id-ref attribute, resolve it to a copy of the referenced
+ element, with original="0". See upgrade-3.10-common.xsl for details.
+ -->
+<xsl:template match="*[@id-ref]">
+    <xsl:variable name="referenced" select="key('element_id', @id-ref)"/>
+
+    <xsl:choose>
+        <xsl:when test="self::nvpair and @name">
+            <!--
+             nvpair with id-ref and name is an undocumented feature that allows
+             the same nvpair value to be used with multiple names (see commit
+             3912538 and associated pull request). The reference's name
+             attribute overrides the referenced element's name attribute.
+
+             We convert an nvpair with id-ref and name to a new nvpair with a
+             different id. At the end of the transformation pipeline, behavior
+             is preserved, and there are no longer any nvpair elements with both
+             id-ref and name.
+             -->
+            <xsl:copy>
+                <xsl:apply-templates select="$referenced/@*"/>
+
+                <xsl:attribute name="original">0</xsl:attribute>
+                <xsl:attribute name="id">
+                    <xsl:value-of select="concat($upgrade_prefix,
+                                                 $referenced/@id, '-',
+                                                 @name)"/>
+                </xsl:attribute>
+                <xsl:attribute name="name">
+                    <xsl:value-of select="@name"/>
+                </xsl:attribute>
+
+                <xsl:apply-templates select="node()"/>
+            </xsl:copy>
+        </xsl:when>
+
+        <xsl:otherwise>
+            <xsl:apply-templates select="$referenced">
+                <xsl:with-param name="original" select="'0'"/>
+            </xsl:apply-templates>
+        </xsl:otherwise>
+    </xsl:choose>
 </xsl:template>
 
 </xsl:stylesheet>
diff --git a/xml/upgrade-3.10-99.xsl b/xml/upgrade-3.10-99.xsl
new file mode 100644
index 0000000000..bad2ef5da8
--- /dev/null
+++ b/xml/upgrade-3.10-99.xsl
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Use comments liberally as future maintainers may be unfamiliar with XSLT.
+ -->
+
+<!--
+ upgrade-3.10-99.xsl
+
+ Guarantees after this transformation:
+ * All attributes of type ID are unique (assuming that was the case for the
+   original input XML). Any elements with id-refs that were resolved in the
+   first step of the transformation pipeline have been converted back to
+   id-refs. See upgrade-3.10-common.xsl for details.
+
+ This file is numbered 99 because it must be the last stylesheet in the
+ pipeline. This numbering allows us to add more stylesheets without needing to
+ continually rename this one. When all transformation development work is
+ finished, we can re-number it.
+ -->
+
+<xsl:stylesheet version="1.0"
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+<xsl:import href="upgrade-3.10-common.xsl"/>
+
+<!-- Copy everything unaltered by default -->
+<xsl:template match="/|@*|node()">
+    <xsl:call-template name="identity"/>
+</xsl:template>
+
+<!--
+ If an element was converted from id-ref to the referenced element earlier in
+ the upgrade transformation pipeline, convert it back to an id-ref as described
+ in upgrade-3.10-common.xsl
+ -->
+<xsl:template match="*[@id]">
+    <!--
+     Convert to an id-ref if @original is 0 or unset and
+     * there is any element with the same id value and original="1", or
+     * there is any preceding element with the same id value
+
+     The preceding axis doesn't include ancestors. While it would likely be
+     nonsense to reference an ancestor, it is allowed by the schema.
+
+     The idea for the second point is that if all elements with a given id value
+     have original set to "0" or unset, the first one should remain a definition
+     while the rest become references.
+     -->
+    <xsl:choose>
+        <xsl:when test="not(number(@original))
+                        and (//*[(@id = current()/@id) and number(@original)]
+                             or preceding::*[@id = current()/@id]
+                             or ancestor::*[@id = current()/@id])">
+            <xsl:copy>
+                <xsl:attribute name="id-ref">
+                    <xsl:value-of select="@id"/>
+                </xsl:attribute>
+            </xsl:copy>
+        </xsl:when>
+
+        <xsl:otherwise>
+            <xsl:call-template name="identity"/>
+        </xsl:otherwise>
+    </xsl:choose>
+</xsl:template>
+
+<!-- Drop "original" attribute -->
+<xsl:template match="@original"/>
+
+</xsl:stylesheet>
diff --git a/xml/upgrade-3.10-common.xsl b/xml/upgrade-3.10-common.xsl
index 0821ff3673..6859b61ea3 100644
--- a/xml/upgrade-3.10-common.xsl
+++ b/xml/upgrade-3.10-common.xsl
@@ -1,39 +1,98 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
  Use comments liberally as future maintainers may be unfamiliar with XSLT.
  -->
 
 <!--
  upgrade-3.10-common.xsl
 
  This stylesheet is intended to be imported by all other stylesheets in the
  upgrade-3.10-* pipeline. It provides variables and templates that are used by
  multiple stylesheets.
 
  This file should not contain any templates with a match attribute.
 
  Assumptions:
+ * The input XML validates against the pacemaker-3.10.rng schema.
  * No element of the input XML contains an id attribute whose value begins with
    "pcmk__3_10_upgrade-". This allows us to generate new IDs without fear of
    conflict. However, the schema does not enforce this assumption.
  -->
 
 <xsl:stylesheet version="1.0"
                 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
 <!-- Strip whitespace-only text nodes but indent output -->
 <xsl:strip-space elements="*"/>
 <xsl:output encoding="UTF-8" indent="yes" omit-xml-declaration="yes"/>
 
 <!-- Prefix for auto-generated IDs -->
 <xsl:variable name="upgrade_prefix" select="'pcmk__3_10_upgrade-'"/>
 
-<!-- Identity transformation: copy everything unaltered by default -->
+<!--
+ Modified identity transformation. Copy everything unaltered by default, but set
+ the "original" attribute based on the "original" template param.
+
+ "original" is a temporary attribute to indicate that an element existed in the
+ input XML. It's not allowed by the schema for any element, so we don't have to
+ worry about conflicts.
+
+ The first step in the upgrade pipeline is to resolve id-ref attributes (type
+ IDREF) to id attributes (type ID). We do this as follows. For each element with
+ an id-ref attribute, replace that element with a deep copy of the referenced
+ element. Set the "original" attribute to 0 in the copy.
+
+ At the end of the upgrade pipeline, we convert back to references as follows.
+ For each element with an id attribute and with the "original" attribute either
+ unset or set to 0:
+ * If there is another element with the same id value that either occurs before
+   the current element or has original="1", convert the current element back to
+   a reference with only the id-ref attribute.
+ * Otherwise, drop the "original" attribute and leave the rest of the current
+   element's attributes and descendants unchanged (except for converting
+   descendants back to references if needed).
+
+ Notes:
+ * We resolve all attributes named id-ref (which are of type IDREF). We do not
+   resolve all attributes of type IDREF. We resolve only in the places where
+   either a definition (with id) or a reference (with id-ref) would validate
+   against the pacemaker-3.10 schema (ignoring ID uniqueness requirements after
+   resolution).
+ * If the "original" attribute is unset for an element, the end of the
+   transformation pipeline treats the element as if it had original="0".
+ * By default, if the "original" param is set, then it's passed down with the
+   same value for all descendants.
+ -->
+
+<!--
+ Identity transformation, optionally setting the "original" attribute
+
+ Params:
+ * original: Boolean (1/0) indicating whether an element was part of the
+             original input XML. If set and this is an element node, the param
+             is used as the value for the "original" attribute for this element
+             and its descendants.
+ -->
 <xsl:template name="identity">
+    <xsl:param name="original"/>
+
     <xsl:copy>
-        <xsl:apply-templates select="@*|node()"/>
+        <!-- All existing attributes -->
+        <xsl:apply-templates select="@*"/>
+
+        <xsl:if test="self::* and $original">
+            <!-- Set "original" attribute for element nodes -->
+            <xsl:attribute name="original">
+                <xsl:value-of select="$original"/>
+            </xsl:attribute>
+        </xsl:if>
+
+        <!-- All nodes, passing down $original value recursively -->
+        <xsl:apply-templates select="node()">
+            <xsl:with-param name="original" select="$original"/>
+        </xsl:apply-templates>
     </xsl:copy>
 </xsl:template>
 
 </xsl:stylesheet>