Working solution found
It turns out that the simplest way to do this is with XInclude. This is how it works:
Both XSLT files are uploaded into the database (with "Expand XIncludes turned OFF in the client -- this is important for reasons that will be clear later). The root XSLT file has an XInclude inside it, pointing to the file that should be included:
<xi:include href="globals.xsl#xpointer(//xsl:stylesheet/*)"></xi:include>
Note: this XInclude code is actually technically incorrect, but it looks this way because the XInclude implementation on the version of eXist we're running right now is non-conformant. This has been fixed in the eXist SVN, and when we upgrade to the next stable eXist version, we should be able to use the correct code, which would look like this:
<xi:include href="globals.xsl" xpointer="xpointer(//xsl:stylesheet/*)"></xi:include>
Now we invoke the transformation in the pipeline like this:
<map:match pattern="text/*.txt">
<map:generate src="xml/{1}.xml" />
<map:transform type="xslt" src="xmldb:exist:///db/teiJournal/xsl_trans/text/text_out.xsl" />
<map:serialize type="text" />
</map:match>
What happens is that Cocoon retrieves the XSLT from the database, and as it does that, eXist expands the XInclude on the fly, and a single stylesheet is created as input to the transformation. Thus the problem of one stylesheet referring to another is avoided.
In the system we envisage, a base transformation would include components from other stylesheets which are under the editable by the admins and editors of the journal, so the contents of the included file will change. This is why it's important not to expand xincludes when uploading the stylesheet in the first place: if we do that, a static copy of the original (unedited) inclusions will be permanently stored in the root stylesheet, so customizations will not have any effect.