You've gotten good answers already. In the chase of brevity, I present my 16 line solution, based on Dimitres answer:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/*">
<choices>
<xsl:for-each-group select="option" group-by="title">
<choice>
<xsl:sequence select="title"/>
<xsl:for-each-group select="current-group()/desc" group-by=".">
<xsl:sequence select="."/>
</xsl:for-each-group>
</choice>
</xsl:for-each-group>
</choices>
</xsl:template>
</xsl:stylesheet>
Note that the current context node inside for-each-group
is the first item in the current group, while current-group()
returns the list of all items in the current group. I exploit the fact that the title
element is identical for input and output, and copy the first title from each group.
And for completeness, the XSLT 1.0 solution using Muenchian grouping (20 lines):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="title" match="option/title" use="."/>
<xsl:key name="desc" match="option/desc" use="."/>
<xsl:template match="/*">
<choices>
<xsl:for-each select="option/title[count(.|key('title',.)[1]) = 1]">
<choice>
<xsl:copy-of select="."/>
<xsl:for-each select="key('title',.)/../desc
[count(.|key('desc', .)[../title=current()][1]) = 1]">
<xsl:copy-of select="."/>
</xsl:for-each>
</choice>
</xsl:for-each>
</choices>
</xsl:template>
</xsl:stylesheet>