Monday, December 31, 2018

Where does healthcare money wind up?

I wonder about the anatomy of healthcare transactions: given $100 I spend with a hospital, ED, specialist, primary care, lab, pharmacy, or insurer, what other financial transactions occur and where do the $ wind up? Who gets how much?

If we truly could "follow the money" and stop when it became a paycheck, a bonus, a tax payment, or a dividend where would it wind up?

If you have any pointers, references or other data, I would be happy to see it in comments below.

Some of this one can track through insurer EOB statements, but I’d like to take it a step or two past the payments made to the pockets of different kinds of people and organizations.

Monday, December 10, 2018

How to avoid the dark side as a developer

There's a fairly well known progression in IT development (not just HealthIT) in which a junior developer (padawan) then becomes a developer (jedi), then a senior developer (master) ... and eventually moves into management (to the dark side).  As developers, we live in a constantly evolving environment, and we cannot always take the time to learn new stuff, because we are often responsible for maintaining old stuff.

Learning is hard work, and takes time.  Sometimes you know you need to use some new thing (e.g., FHIR), but you've not had the time, the resources or support to get training on it, or the opportunity to use the training that you may have been given.  You can "steal" that time (it's not really stealing though) from your "day job".  If you are like most people, your organization will tell you that you should spend time each week improving your skills, but then, the real question becomes, do you actually have that time available, or have you stolen it from yourself to fit in all the other stuff you don't have enough time to get done.  All the while you are learning, there's this other battle going on you aren't paying attention to, much like Luke training under Yoda on Dagobah, but eventually need to go back to fight.

The advanced developer will often be able to get a really quick understanding of what the "something new" can do, and have some very specific tasks they want to use it for ("I just want to use X to do Y"), just like Luke wants to learn to use the force to defeat Vader.  The challenge for Luke is that he's got a ton of ability with the force, but never came at it from the beginning instructional level (much like his father).  The challenge for the developer is, they don't have the basic familiarity with X to understand the answers to those questions posted publicly (perhaps on Stack Overflow, or, or some other online community).  So, learning at the middle (or at the top), doesn't work really well.  You have to stop trying to be an expert, and come back at the problem as a beginner, even if you understand what the expert level capabilities of a component will let you do (defeat Vader).

The trick here, is to go back into the problem with a beginner mentality, and to forget your existing expertise.  You'll be able to reapply it eventually, but you need to start back at the beginning.

If you don't, you'll continue down that pathway that eventually leads into a transition to the dark-side (management).


P.S. Not all managers are on the dark side ... some truly are Jedi Masters, who can still use the force in battle, it's a meme.

Saturday, December 8, 2018

What's an Error?

It's not me, it's you This question came up for me a while back building my first FHIR Server, and it comes up again now.  We're all fairly familiar with exceptions by this point, and logging.

Whether you use log4j2, lockback, slf4j, Java's native logging, C#/.NET logging, et cetera, we all know that there are about 5-7 different levels of logging, from INFO, to WARN, to ERROR and beyond for engineers digging into the code.

There are three kinds of problems depending on the root cause that can result in an exception.  And you probably need to log that exception.  The first two of these problems is an exception that occurs because, well, somebody screwed up.  The last class of problems aren't really problems because you can (and often do) recover from them if you've developed a robust system, either by trying again, or trying a different way.  In these cases, your exceptions are really just that, exceptional cases that need special attention, but the fact that you got the exception was in fact expected.

Even HL7 Version 2 acknowledges that there are two kinds of errors that a message processor can negatively acknowledge (NAK); problems on the application end (AE - Application Errors ), and problems on the other end of the pipe (AR - Application Reject).  The former are problems that the receiving application encounters in trying to process the content that aren't (at least at first inspection) caused by the sender, and the sender might try again later, the latter are problems where the sender shouldn't bother trying again with until they've fixed the problem on their end.

Similarly, in the RESTful / HTTP world, there are 4XX series errors (your input sucks buddy, fix it), and 5XX errors (something's wrong on my end here, sorry for the hassle).

Distinguishing between the two is also challenging, because folks don't always take proper care with error reporting.  Even good frameworks (like HAPI on FHIR), report 5XX series errors, when in fact, the problem is NOT with the server, but with the sender (as happens when you try to call the server with a query parameter that it doesn't support).

And then, how should these be reported in logs?  Should you use logger.warn() or logger.error()? [By no means should you use, that's just plain wrong, but I've seen enough of it).

I've got some thoughts on this:
If the "error" occurs because of receiver inputs not meeting requirements of the receiver, and the receiver a) detects it, and b) reports it, this is NORMAL operation of a well running system.  These kinds of issues shouldn't be reported using logger.error().  BUT, the maintainer of that well-running system will want to know that these problems are occurring at the time of detection, and so they should be reported using logger.warn().  In some cases, a high number of warnings occurring at the very least indicates that something is broken downstream that someone (often not the maintainer) should be looking into because things could be done better.  At the very least, this gives the system maintainer an opportunity to help their customer do that (and perhaps reduce burden on the system as well).

If, on the other hand, the problem was detected, and it was something the service expected to succeed but didn't, then it should use logger.error() (at least).  Which brings me to my next question, how do you decide between logger.error() and logger.fatal().  Well, this is kind of like the difference between the engine light on your car, and (should you have one), the oil lamp.  The former reports an error that you can probably ignore for a bit, the latter reports a condition that likely needs immediate attention (or you could destroy your engine).  A fatal error is one that indicates that continued proper operation of the system is at risk, possibly even extreme risk.  Whereas, an error is one that says, hey, under these circumstances, X doesn't work.  But Y and Z might very well continue to work (I made it home 30 miles with a cylinder misfire the other day, but my daughter barely made it 5 miles with no oil in the car).  So, even though cylinder 5 was down, 1-4 and 6 were working well enough that I didn't need to two the vehicle 30 miles to the service station.  I could get it home and drop it off on Monday.  But yeah, that oil lamp left the car dead on the side of the road, and in need of a tow.  So, if the problem is likely to leave a single transaction uncompleted, its an error.  But if it means every transaction for the next hour is going to fail, it's definitely in the fatal category.

While when you log it for your own use, you differentiate, as far as the end user is concerned, it's still an error when you report it back to them.  But HTTP error response codes come to the rescue in identifying where the ID10T is for the error.  So, when you report Errors (on your end) to the end user, you should probably be using 5XX series HTTP response codes, but when you report errors on their end, you should probably be using 4XX series response codes.

Now, what do you do about the last of the three, where something went wrong, but you managed to recover ... say the database connection was lost, but you got another one solidly.  This again goes into warning territory.  In this case, the warning might even go to another log file, because these warnings are things that are causing your system grief.  Bad inputs that you detect aren't really causing you a ton of problems, but recovery from another system's failure almost surely is.  Just think about all the work you had to do just to get retry logic built in the first place.

You might think differently, but well, that's you, and this is me...


Wednesday, December 5, 2018

On Providing Feedback for HealthIT Standards and Regulations

I spend a lot of time reviewing Health IT standards and regulations, something I'm preparing to do for the next HL7 ballot cycle for which signup closes on Friday, and also on the HHS's draft strategy for reducing regulatory burden. One of the things I was taught (by both HL7 and IHE) was how to provide feedback, and that same technique also works quite well when providing feedback on regulation.

The basic principles are the same:

  1. Focus on what is important.
  2. Coordinate your feedback with others.
  3. Provide the solution first.
  4. Include your rationale, with facts to back it up when available.

Focus on what is Important

This is the key.  In both standards and regulation, there are parts that the person or organization following the requirements in the document have to do, and parts that explain that and give rationale, and parts that simply introduce new material to the reader. 

Usually, when providing feedback, I'm MOST interested in the requirements, and least interested in introductory material.  My general priorities for feedback are in order: Meaning, Clarity, and Grammar.  Meaning addresses what I have to do or not do.  Clarity further defines what I have to do so that it is clear and testable.  The statement: "The system must be able to read C-CDA 1.1 or 2.1 documents" is unclear.  Does the system pass if it can read C-CDA 1.1, but not 2.1 (or the reverse)?  Or must it do both?  A more clear statement would be "The system must be able to read both C-CDA 1.1 and 2.1 documents."  I don't write spelling and grammar software any more (not since about the turn of the century), so I rarely focus on grammar or spelling except where it's important (e.g., HER for EHR).

In regulations, I generally skip the introductory pages up through the statutory authority simply on that basis.  Usually, when I'm reading a regulation, I already am familiar with the why and wherefore, and the statutory framework.  In standards, I often skip the scoping and introductory pages.  And when I say skip, I mean "skip in my first quick read-through".

Which brings me to the second part of focus.  I typically do three reads of a document: A quick scan, a deeper review of important parts, and a final read through of everything I think that matters to see if there's anything I missed.  For particular important things, I may just skip the "important parts" read through, and read the entire document start to finish.  But you can't do that last in one sitting for a 500 page document.

The first read through is a quick scan to identify the areas of most importance or concern.  When doing this, I don't need to read every last sentence.  I can generally get a quick idea of what is important by first looking at the table of contents, and then reading the first sentence of major paragraphs, diving a little deeper if they prove to be interesting.  This read is usually the one that I usually do my "twitter note taking" with.  Something that captures my attention gets a tweet, all tweets get the same hash tag, and if I have a quick though about the impact, that goes in my tweet as well.  You don't need to use twitter in this read through like I do, but you do need to take notes.  This is basically using a highlighter in the document so you can go back and assess.

The second read is deeper, and focuses on those sections that I've identified as important.  I generally go a bit wider than my first scan would suggest, just to see if there's something I missed.  If there's three paragraphs in a ten paragraph section that I find important, I'll probably read the whole section.  If a whole section is important, I'll look at material elsewhere that might introduce that section, or describe the scope for it that might appear elsewhere in the document.  That's where my notes are important, and I'll go back through them to understand what I have to look at more deeply.

The last read is of the whole document (save material I already know I know).

Coordinate your Feedback with Others

In both kinds of documents, you aren't alone in having to provide feedback, and you won't be looking at every perspective.  Sharing your feedback with others serves two purposes: 
  1. It helps you validate your point, and perhaps clarify it further.
  2. It helps others who think the same way as you provide supporting feedback.  The more that the originator of the document hears the same or similar comments, the better chance you have of your comment having an impact.
Coordinating your feedback can be done in several ways.  You can coordinate internally within your organization, you can also coordinate with other organizations.  For example, the HL7 Clinical Decision Support workgroup is planning on providing some of its feedback to HHS on the reducing regulatory burden.  Finally, since public comments are just that, public, you can seek out the feedback of others who have already provided theirs before preparing your own.  This last method can also be helpful in identifying things you want to focus on, so you might even consider reading the feedback of others before reading the document (especially if you are stretched thin for time).  I generally use similar but not identical wording as others when I'm coordinating feedback in this way.  Most of the time, this is much more effective than providing identical wording, because exact duplication of comments can work against you.  It's too easy to exactly duplicate the words of others, and people can become inoculated against a viewpoint they've seen too many times.

Sharing your feedback directly with others is extremely helpful, and remember, it's already going to be public, so you might as well take advantage of it.

Provide the Solution First

This is pretty simple.  Write what you would have preferred to read in the regulation or standard, rather than what you actually read, and propose the new text replace the old text.  This saves the people having to respond to comments from having to do the harder work of revising the regulation or standard.  The tricky bit here is to write in the same style as the original document is written in.  There's some obvious stuff here.  If the document uses a particular voice or perspective, you have to write from that same perspective, in the same style (unless your goal is to fix grammar, in which case, go ahead, but recall your priorities).

Explain why your Text is Better

But, don't just write it, explain why the change is better, and back up your rationale with facts.  Numbers are really good to have, and if not numbers, at least something that isn't just "I don't like this".  Explain why your change is better, and don't just argue from a position of authority.  The person reading your feedback won't likely know you from Adam (or Eve), and your authority generally won't matter all that much.

If you can align your text with the proposed goals of the standard or regulation, and explain why it will help the originator achieve those goals, that's even better.


If you follow these guidelines, and do your job well, don't be surprise if the text you wrote shows up in the revised document.  I can speak to at least two cases where text I wrote survived virtually unchanged in revised ONC and CMS regulations, more times where I had the desired effect (from my perspective), and plenty of times where similar things have happened in HL7 and IHE specifications.  And there are many times where changes that I wrote about in this blog achieved were quite successful.

    - Keith

Tuesday, December 4, 2018

A one day build for Version 2 FHIR StructureDefinition

I love Adam Savage's one day builds.  I also like pieces of useful software I can build in a day.  Most recently I've been investigating how to do conversions of legacy data types (e.g., HL7 Version 2) to FHIR, and one of the things that I discovered I might need was a way to represent a V2 resource as a FHIR StructureDefinition (especially since I already have access to one Grahame built for CDA).

You see, I already have some tools that can plow through the details of a StructureDefinition to support conversion to FHIR.  If I want a reversible conversion, I probably need to work with the same data structures on either side.  Most of the data I need to populate the StructureDefinition can be found in various places, but the easiest (for me) to access and use are the HL7 Version 2 schema.  You can get schema for every Version 2 version from 2.1 all the way up through 2.8.2.  I chose to work with the 2.8.1 schema because that's the highest version that HAPI HL7 V2 supports at the moment.

I'm working with 2.8.1 because its generally backwards compatible (not completely, but nearly so, and I can fix the removed content) with all prior versions of HL7 V2, and I could be seeing many different versions.  Dumbing down a version is easier that augmenting one.

To convert a schema to a StructureDefinition, I'm going to need to pick some tools.  There's lots of ways to go from one to the other, but if you've been reading this blog, you already know I spend a lot of time (way too much in fact) using XSLT.  So, this build is going to use XSLT Version 2.0 as one of my tools, and the XSLT transformer will be Saxon Personal Edition 9.8 (because that's the version that my XML Editor uses).  For XML editors, just about any will do, but I happen to like the tools from Oxygen.  These days I'm using XML Developer, though I have in the past also used XML Editor.

There are a lot of different ways still to handle this.  I could build something that generally understood XML Schema, but that is by no means a one day build.  Schema is way to complicated for that.  Fortunately, the V2 XML Schema's are produced from the V2 database, and have a pretty constrained use of XML Schema, which will make my work a great deal simpler.

The first step is to take a look at the schema, and figure out how to map the data to a FHIR StructureDefinition (if you think that having to create a mapping to create a mapping to ... is a bit recursive, you are surely correct).

There's actually a few dozen schema, one for each message type.  That's ok, the computer doesn't really care how many of these it has to build.  Lets pick one, like ADT_A01 to start with.
<?xml version="1.0"?>
<!--   v2.xml Message Definitions for Version 2.8.2 - here: ADT_A01-->
<!--   Copyright (c) 1999-2016, Health Level Seven. All rights reserved.   -->
<!--   (generated on 17.11.2016 by HL7-Database) D:\Eigene Dateien\HL7\Datenbank\hl7_92.mdb-->
<xsd:schema xmlns:xsd="" xmlns="urn:hl7-org:v2xml"
    xmlns:hl7="urn:hl7-org:v2xml" targetNamespace="urn:hl7-org:v2xml" version="1.1">
    <!-- import segment definition for version -->
    <xsd:include schemaLocation="segments.xsd"/>
    <!-- MESSAGE ADT_A01 -->
    <!-- .. message definition ADT_A01 -->

Already we can see some values in comments that will be useful for the StructureDefinition data (things like copyright, dates, publisher, descriptions, version, et cetera).  Most of the other messages look the same.  

Then we need to look at segments, which again generally all look the same.
    <xsd:complexType name="MSH.CONTENT">
            <xsd:element ref="MSH.1" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.2" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.3" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.4" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.5" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.6" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.7" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.8" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.9" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.10" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.11" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.12" minOccurs="1" maxOccurs="1"/>
            <xsd:element ref="MSH.13" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.14" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.15" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.16" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.17" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.18" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:element ref="MSH.19" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.20" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.21" minOccurs="0" maxOccurs="unbounded"/>
            <xsd:element ref="MSH.22" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.23" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.24" minOccurs="0" maxOccurs="1"/>
            <xsd:element ref="MSH.25" minOccurs="0" maxOccurs="1"/>
            <xsd:any processContents="lax" namespace="##other" minOccurs="0"/>

    <xsd:element name="MSH" type="MSH.CONTENT"/>

Now I can see though, where cardinality comes from, and that I will have to traverse through element to complexType/sequence to get to parts of this via type/name links.

Next I look at Fields:
    <!-- FIELD MSH.2-->
    <xsd:attributeGroup name="MSH.2.ATTRIBUTES">
        <xsd:attribute name="Item" type="xsd:string" fixed="2"/>
        <xsd:attribute name="Type" type="xsd:string" fixed="ST"/>
        <xsd:attribute name="LongName" type="xsd:string" fixed="Encoding Characters"/>
        <xsd:attribute name="minLength" type="xsd:integer" fixed="4"/>
        <xsd:attribute name="maxLength" type="xsd:integer" fixed="5"/>
    <xsd:complexType name="MSH.2.CONTENT">
            <xsd:documentation xml:lang="en">Encoding Characters</xsd:documentation>
            <xsd:documentation xml:lang="de">Weitere Trennzeichen</xsd:documentation>
                <hl7:LongName>HL7Encoding Characters</hl7:LongName>
            <xsd:extension base="ST">
                <xsd:attributeGroup ref="MSH.2.ATTRIBUTES"/>
    <xsd:element name="MSH.2" type="MSH.2.CONTENT"/>
    <!-- FIELD MSH.3-->
    <xsd:attributeGroup name="MSH.3.ATTRIBUTES">
        <xsd:attribute name="Item" type="xsd:string" fixed="3"/>
        <xsd:attribute name="Type" type="xsd:string" fixed="HD"/>
        <xsd:attribute name="Table" type="xsd:string" fixed="HL70361"/>
        <xsd:attribute name="LongName" type="xsd:string" fixed="Sending Application"/>
    <xsd:complexType name="MSH.3.CONTENT">
            <xsd:documentation xml:lang="en">Sending Application</xsd:documentation>
            <xsd:documentation xml:lang="de">Sendende Anwendung / Sendender
                <hl7:LongName>HL7Sending Application</hl7:LongName>
            <xsd:extension base="HD">
                <xsd:attributeGroup ref="MSH.3.ATTRIBUTES"/>

    <xsd:element name="MSH.3" type="MSH.3.CONTENT"/>

And stuff starts to get interesting, because some fields are complex and some are simple, and some data is actually in fixed attributes rather than schema or annotations.  And I'll have to navigate UP a type hierarchy through extension/@base.

and finally, we come to Datatypes, the (eventually) terminal nodes of the element tree in StructureDefinition.

For some things, I want to create the StructureDefinition the same way that Grahame did it for CDA, so I'll look at some key places there as well.  The stuff I've highlighted is where I've identified stuff I think I want to do the same way Grahame did.

    <valueUri value="urn:hl7-org:v3"/>
  <url value=""/>
  <version value="0.0.1"/>
  <name value="CDAR2.ClinicalDocument"/>
  <title value="ClinicalDocument (CDA Class)"/>
  <status value="active"/>
  <experimental value="false"/>
  <date value="2018-07-26T05:39:34+00:00"/>
  <publisher value="HL7"/>
  <fhirVersion value="3.0.1"/>
  <kind value="logical"/>
  <abstract value="false"/>
  <type value="ClinicalDocument"/>
  <baseDefinition value=""/>
  <derivation value="specialization"/>

So here are my first cut at mappings for the "header" of the StructureDefinition.

Where does it come from in the Schema


String of the form #[.#+]+ in a comment containing the text version
Text of version comment up through version for message
Value of complexType/annotation/appInfo/LongName for CONTENT definition of Field
Same as name
From Copyright line between , and .
From generated on comment in form
Same as name
First comment containing the text Copyright
HL7 V2 Event code from name of first element
Same as for CDA
As for CDA

Start with top level element and to a depth first traversal of the schema until you get to types (e.g., CE, ST)

OK, now I'm ready to populate the header, with something like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl=""
    xmlns:xs="" xmlns:f=""
    xmlns:v2="urn:hl7-org:v2xml" xmlns="" exclude-result-prefixes="xs f v2"
    <xsl:output indent="yes"/>
    <xsl:variable name="segments" select="document('segments.xsd')"/>
    <xsl:variable name="fields" select="document('fields.xsd')"/>
    <xsl:variable name="datatypes" select="document('datatypes.xsd')"/>
    <xsl:variable name="parts" select="$segments | $fields | $datatypes"/>

    <xsl:template name="start">
        <xsl:apply-templates select="$msg"/>
    <xsl:template match="/">
            <extension url="">
                <valueUri value="{/xs:schema/@targetNamespace}"/>
            <url value="{//xs:element[1]/@name}"/>
            <xsl:variable name="versionText" select="normalize-space(/comment()[contains(., 'Version')])"/>
            <!-- version String of the form #[.#+]+ in a comment containing the text version -->
            <xsl:variable name="versionNumber"
                select="replace($versionText, '^.* ([0-9]+(\.[0-9]+)+).*$', '$1')"/>
            <version value="{$versionNumber}"/>
            <!-- Value of complexType/annotation/appInfo/LongName for CONTENT definition of Field -->
            <name value="{$versionText}"/>
            <display value="{$versionText}"/>
            <status value="active"/>
            <experimental value="false"/>
            <xsl:variable name="copyrightText" select="normalize-space(/comment()[contains(., 'Copyright')])"/>
            <xsl:variable name="publisherName"
                select="substring-before(substring-after($copyrightText, ', '),'.')"/>
            <xsl:variable name="publishedText" select="normalize-space(/comment()[contains(., 'generated on')])"/>
            <xsl:variable name="publishedDate"
                select="substring-before(substring-after($publishedText, 'generated on '), ' ')"/>
            <publisher value="{$publisherName}"/>
                value="{substring($publishedDate, 7, 4)}{substring($publishedDate, 4, 2)}{substring($publishedDate, 1, 2)}"/>
            <description value="{$versionText}"/>
            <copyright value="{$copyrightText}"/>
                <system value=""/>
                <code value="{//xs:element[1]/@name}"/>
            <fhirVersion value="1.0.2"/>
            <kind value="logical"/>
            <abstract value="false"/>
            <constrainedType value="{//xs:element[1]/@name}"/>
            <baseDefinition value=""/>
    <xsl:template match="xs:complexType"/>

Next thing I need to figure out is how to populate elements in the snapshot.  Here is approximately what they look like.

Where does it come from/Comments
Use FHIR . Notation for elements
Only if an xml attribute, in which case it says "xmlAttr"
Fixed up name (remember to clean up after dots)
Probably same as name
Concise definition (e.g., Admit Patient) from Annotations in the schema
element/@minOccurs (or 0 if not specified)
element/@maxOccurs (* if unbounded)
Duplicates definition

References URL to type's StructureDefinition

Where ### comes from @Type
True if min > 0
From the MaxLength annotations in the schema


Where #### is the table number
From the Table annotations in the schema

After <baseDefinition>, I can put in the data for the first element in the snapshot.

            <baseDefinition value=""/>
                <element id="{//xs:element[1]/@name}">
                    <path value="{//xs:element[1]/@name}"/>
                    <min value="1"/>
                    <max value="1"/>
                        <path value="{//xs:element[1]/@name}"/>
                        <min value="1"/>
                        <max value="1"/>

And then do a depth first traversal after that element.
                    select="$msg//xs:complexType[@name = //xs:element[1]/@type]">
                    <xsl:with-param name="path" select="//xs:element[1]/@name"/>
                    <xsl:with-param name="depth" select="2"/>

For which I'll need a template to do some more work.
   <xsl:template match="xs:complexType">
        <xsl:param name="path"/>
        <xsl:param name="depth"/>
        <xsl:for-each select="xs:sequence/xs:element">
            <xsl:variable name="ref" select="@ref"/>
            <xsl:variable name="element"
                 select="($msg | $parts)//xs:element[@name = $ref]"/>
            <xsl:variable name="name" select="translate($element/@name,'.','-')"/>
            <xsl:variable name="min" select="if (@minOccurs) then (@minOccurs) else ('1')"/>
            <xsl:variable name="max" 
                 select="if (@maxOccurs='unbounded') 
                     then ('*') 
                     else if (@maxOccurs) 
                     then @maxOccurs else 1"/>
            <xsl:variable name="type" 
                          @name = $parts//xs:element[@name = current()/@ref]/@type
            <xsl:variable name="base" 
            <element id="{$path}.{$name}">
                <path value="{$path}.{$name}"/>
                <label value="{$name}"/>
                <min value="{$min}"/>
                <max value="{$max}"/>
                    <path value="{$path}.{$name}"/>
                    <min value="{$min}"/>
                    <max value="{$max}"/>
                    <code value="
                                 {if ($type/xs:simpleContent) then ($base) else ($name)}"/>
                <mustSupport value="{if(string($min) &gt; '0') then ('true') else ('false')}"/>
            <xsl:apply-templates select="$type">
                <xsl:with-param name="path" select="concat($path,'.',$name)"/>
                <xsl:with-param name="depth" select="$depth + 1"/>

Before the for-each, I figured out I need to walk up the type hierarchy
        <xsl:if test="xs:complexContent/xs:extension">
            <!-- Handle this
                <xsd:extension base="HD">
                    <xsd:attributeGroup ref="MSH.6.ATTRIBUTES"/>
            <xsl:variable name='base' select='xs:complexContent/xs:extension/@base'/>
            <xsl:apply-templates select="$parts//xs:complexType[@name=$base]">
                <xsl:with-param name="path" select="$path"/>
                <xsl:with-param name="depth" select="$depth"/>

To get the short descriptions in annotations, this needs to be added after the label element.
                <!-- handle this:
                        <xsd:documentation xml:lang="en">Triage Code</xsd:documentation>
                <xsl:if test="$type/xs:annotation/xs:documentation[@xml:lang='en']">
To get the maximum length of the string for the data, I needed to add this before the mustSupport element.
                <!-- Get the maximum length of the string 
                <xsd:complexType name="HD.3.CONTENT">
                        <xsd:extension base="ID">
                            <xsd:attributeGroup ref="HD.3.ATTRIBUTES"/>
                <xsd:attributeGroup name="HD.3.ATTRIBUTES">
                    <xsd:attribute name="minLength" type="xsd:integer" fixed="1"/>
                <xsl:variable name="atts" 
                               @name = $type/xs:simpleContent/xs:extension/xs:attributeGroup/@ref
                <xsl:if test="$atts/xs:attribute[@name='maxLength']">
                    <maxLength value="{$atts/xs:attribute[@name='maxLength']/@fixed}"/>

And table bindings to V2 value sets come after it:
                <!-- Handle binding to tables
                    <xsd:attributeGroup name="MSH.15.ATTRIBUTES">
                        <xsd:attribute name="Table" type="xsd:string" fixed="HL70155"/>
                <xsl:if test="$atts/xs:attribute[@name='Table']">
                    <xsl:variable name="tableNumber" 
                        <strength value="extensible"/>
                            <reference value="{$tableNumber}"/>

After several more tweaks, I can now generate a StructureDefinition from the V2.8.1 schema.

I don't know how useful this will be, and it's very dependent upon the V2 XML tooling, but it works well enough for now.

Unlike Adam, for which, if you like what you see you have to go build it yourself, you can find my one day build completely downloadable online.