[PATCH 1/8] qapi: differentiate "intro" and "details" sections

John Snow posted 8 patches 3 weeks ago
Maintainers: "Philippe Mathieu-Daudé" <philmd@linaro.org>, "Daniel P. Berrangé" <berrange@redhat.com>, Kashyap Chamarthy <kchamart@redhat.com>, Pierrick Bouvier <pierrick.bouvier@linaro.org>, "Michael S. Tsirkin" <mst@redhat.com>, Stefano Garzarella <sgarzare@redhat.com>, Markus Armbruster <armbru@redhat.com>, Michael Roth <michael.roth@amd.com>, John Snow <jsnow@redhat.com>, Peter Maydell <peter.maydell@linaro.org>, Mauro Carvalho Chehab <mchehab+huawei@kernel.org>, Richard Henderson <richard.henderson@linaro.org>, Paolo Bonzini <pbonzini@redhat.com>, Eric Blake <eblake@redhat.com>, Igor Mammedov <imammedo@redhat.com>, Ani Sinha <anisinha@redhat.com>, Kevin Wolf <kwolf@redhat.com>, Hanna Reitz <hreitz@redhat.com>, "Marc-André Lureau" <marcandre.lureau@redhat.com>, Marcel Apfelbaum <marcel.apfelbaum@gmail.com>, Yanan Wang <wangyanan55@huawei.com>, Zhao Liu <zhao1.liu@intel.com>, Peter Xu <peterx@redhat.com>, Fabiano Rosas <farosas@suse.de>, Jason Wang <jasowang@redhat.com>, "Alex Bennée" <alex.bennee@linaro.org>, Jiri Pirko <jiri@resnulli.us>, Stefan Berger <stefanb@linux.vnet.ibm.com>, Stefan Hajnoczi <stefanha@redhat.com>, Alex Williamson <alex@shazbot.org>, "Cédric Le Goater" <clg@redhat.com>, Lukas Straub <lukasstraub2@web.de>
[PATCH 1/8] qapi: differentiate "intro" and "details" sections
Posted by John Snow 3 weeks ago
This patch begins distinguishing "Plain" sections as being either
"Intro" or "Details" sections for the purpose of knowing when/where/how
to inline those sections.

The Intro section is always the first section of any doc block. It may
be empty or any number of paragraphs. It is interrupted by any other
non-plaintext section, i.e.; Members, Features, Errors, Returns, Since,
and TODO.

The details section, when present, is either the last section or the
second-to-last section when a "Since:" section is present. It consists
of any plain text in the doc block that follows any named sections if
present.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapidoc.py         |  2 +-
 scripts/qapi/parser.py         | 35 +++++++++++++++++++++++-----------
 tests/qapi-schema/doc-good.out |  8 ++++----
 3 files changed, 29 insertions(+), 16 deletions(-)

diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index c2f09bac16c..e359836f110 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -368,7 +368,7 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
         for i, section in enumerate(sections):
             section.text = self.reformat_arobase(section.text)
 
-            if section.kind == QAPIDoc.Kind.PLAIN:
+            if section.kind.name in ("INTRO", "DETAILS"):
                 self.visit_paragraph(section)
             elif section.kind == QAPIDoc.Kind.MEMBER:
                 assert isinstance(section, QAPIDoc.ArgSection)
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index c3cf33904ef..da0ac32ad89 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -544,7 +544,7 @@ def get_doc(self) -> 'QAPIDoc':
             doc = QAPIDoc(info, symbol)
             self.accept(False)
             line = self.get_doc_line()
-            no_more_args = False
+            have_tagged = False
 
             while line is not None:
                 # Blank lines
@@ -573,10 +573,10 @@ def get_doc(self) -> 'QAPIDoc':
                     if not doc.features:
                         raise QAPIParseError(
                             self, 'feature descriptions expected')
-                    no_more_args = True
+                    have_tagged = True
                 elif match := self._match_at_name_colon(line):
                     # description
-                    if no_more_args:
+                    if have_tagged:
                         raise QAPIParseError(
                             self,
                             "description of '@%s:' follows a section"
@@ -588,7 +588,7 @@ def get_doc(self) -> 'QAPIDoc':
                         if text:
                             doc.append_line(text)
                         line = self.get_doc_indented(doc)
-                    no_more_args = True
+                    have_tagged = True
                 elif match := re.match(
                         r'(Returns|Errors|Since|Notes?|Examples?|TODO)'
                         r'(?!::): *',
@@ -629,10 +629,14 @@ def get_doc(self) -> 'QAPIDoc':
                     if text:
                         doc.append_line(text)
                     line = self.get_doc_indented(doc)
-                    no_more_args = True
+                    have_tagged = True
                 else:
                     # plain paragraph
-                    doc.ensure_untagged_section(self.info)
+
+                    # Paragraphs before tagged sections are "intro" paragraphs.
+                    # Any appearing after are "detail" paragraphs.
+                    intro = not have_tagged
+                    doc.ensure_untagged_section(self.info, intro)
                     doc.append_line(line)
                     line = self.get_doc_paragraph(doc)
         else:
@@ -674,13 +678,14 @@ class QAPIDoc:
     """
 
     class Kind(enum.Enum):
-        PLAIN = 0
+        INTRO = 0
         MEMBER = 1
         FEATURE = 2
         RETURNS = 3
         ERRORS = 4
         SINCE = 5
         TODO = 6
+        DETAILS = 7
 
         @staticmethod
         def from_string(kind: str) -> 'QAPIDoc.Kind':
@@ -730,7 +735,7 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
         self.symbol: Optional[str] = symbol
         # the sections in textual order
         self.all_sections: List[QAPIDoc.Section] = [
-            QAPIDoc.Section(info, QAPIDoc.Kind.PLAIN)
+            QAPIDoc.Section(info, QAPIDoc.Kind.INTRO)
         ]
         # the body section
         self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
@@ -748,12 +753,20 @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
     def end(self) -> None:
         for section in self.all_sections:
             section.text = section.text.strip('\n')
-            if section.kind != QAPIDoc.Kind.PLAIN and section.text == '':
+            if (
+                    section.kind not in (
+                        QAPIDoc.Kind.INTRO, QAPIDoc.Kind.DETAILS
+                    ) and section.text == ''
+            ):
                 raise QAPISemError(
                     section.info, "text required after '%s:'" % section.kind)
 
-    def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
-        kind = QAPIDoc.Kind.PLAIN
+    def ensure_untagged_section(
+        self,
+        info: QAPISourceInfo,
+        intro: bool = True,
+    ) -> None:
+        kind = QAPIDoc.Kind.INTRO if intro else QAPIDoc.Kind.DETAILS
 
         if self.all_sections and self.all_sections[-1].kind == kind:
             # extend current section
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 04a55072646..04e29e8d50f 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -116,7 +116,7 @@ The _one_ {and only}, description on the same line
 Also _one_ {and only}
     feature=enum-member-feat
 a member feature
-    section=Plain
+    section=Details
 @two is undocumented
 doc symbol=Base
     body=
@@ -175,7 +175,7 @@ description starts on the same line
 a feature
     feature=cmd-feat2
 another feature
-    section=Plain
+    section=Details
 .. note:: @arg3 is undocumented
     section=Returns
 @Object
@@ -183,7 +183,7 @@ another feature
 some
     section=Todo
 frobnicate
-    section=Plain
+    section=Details
 .. admonition:: Notes
 
  - Lorem ipsum dolor sit amet
@@ -216,7 +216,7 @@ If you're bored enough to read this, go see a video of boxed cats
 a feature
     feature=cmd-feat2
 another feature
-    section=Plain
+    section=Details
 .. qmp-example::
 
    -> "this example"
-- 
2.53.0
Re: [PATCH 1/8] qapi: differentiate "intro" and "details" sections
Posted by Markus Armbruster 2 weeks, 3 days ago
John Snow <jsnow@redhat.com> writes:

> This patch begins distinguishing "Plain" sections as being either
> "Intro" or "Details" sections for the purpose of knowing when/where/how
> to inline those sections.
>
> The Intro section is always the first section of any doc block. It may
> be empty or any number of paragraphs. It is interrupted by any other
> non-plaintext section, i.e.; Members, Features, Errors, Returns, Since,
> and TODO.
>
> The details section, when present, is either the last section or the
> second-to-last section when a "Since:" section is present. It consists
> of any plain text in the doc block that follows any named sections if
> present.
>
> Signed-off-by: John Snow <jsnow@redhat.com>

The commit message explains what kinds INTRO and DETAILS are, but not
why they're useful.  My guess:

1. Represent the future "Details:" marker: the plain section before it
is of kind INTRO, the one afterwards is of kind DETAILS.

2. Future programming convenience?  With just PLAIN, code may have to
understand the section's context to make decisions, and with INTRO and
DETAILS is doesn't.

Guess close enough?

The commit message covers PLAIN sections at the beginning and at the end
(modulo Since:).  It doesn't cover PLAIN sections between tagged
sections / member descriptions.  You disallow these in PATCH 2.  You can
either cover them here, or get rid of them by swapping PATCH 1 and 2.

Hmm, is your description of DETAILS accurate?  Looks like it isn't; see
my review of tests/qapi-schema/doc-good.out below.

> ---
>  docs/sphinx/qapidoc.py         |  2 +-
>  scripts/qapi/parser.py         | 35 +++++++++++++++++++++++-----------
>  tests/qapi-schema/doc-good.out |  8 ++++----
>  3 files changed, 29 insertions(+), 16 deletions(-)

[Skipping the Python code in my first pass...]

> diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
> index 04a55072646..04e29e8d50f 100644
> --- a/tests/qapi-schema/doc-good.out
> +++ b/tests/qapi-schema/doc-good.out
> @@ -116,7 +116,7 @@ The _one_ {and only}, description on the same line
>  Also _one_ {and only}
>      feature=enum-member-feat
>  a member feature
> -    section=Plain
> +    section=Details
>  @two is undocumented

The section containing "@two is undocumented" changes from PLAIN to
DETAILS.  Doc source:

    ##
    # @Enum:
    #
    # @one: The _one_ {and only}, description on the same line
    #
    # Features:
    # @enum-feat: Also _one_ {and only}
    # @enum-member-feat: a member feature
    #
    # @two is undocumented
    ##

It is at the end.  Good.

>  doc symbol=Base
>      body=
> @@ -175,7 +175,7 @@ description starts on the same line
>  a feature
>      feature=cmd-feat2
>  another feature
> -    section=Plain
> +    section=Details
>  .. note:: @arg3 is undocumented

The section containing "@arg3 is undocumented" changes from PLAIN to
DETAILS.  Doc source:

Doc source:

    ##
    # @cmd:
    #
    # @arg1:
    #     description starts on a new line,
    #     indented
    #
    # @arg2: description starts on the same line
    #     remainder indented differently
    #
    # Returns: @Object
    #
    # Errors: some
    #
    # Features:
    # @cmd-feat1: a feature
    # @cmd-feat2: another feature
    #
    # .. note:: @arg3 is undocumented
    #
    # TODO: frobnicate
    #

It is *not* at the end.  Is the commit message inaccurate?

>      section=Returns
>  @Object
> @@ -183,7 +183,7 @@ another feature
>  some
>      section=Todo
>  frobnicate
> -    section=Plain
> +    section=Details
>  .. admonition:: Notes
>  
>   - Lorem ipsum dolor sit amet
    - Ut enim ad minim veniam

    [some section body text elided for brevity...]

   Note::
       Ceci n'est pas une note
       section=Since
   2.10

The section starting with the adminition and ending after "Ceci n'est
pas une note" changes from PLAIN to DETAILS.  Doc source continued:

    # .. admonition:: Notes
    #
    #  - Lorem ipsum dolor sit amet
    #  - Ut enim ad minim veniam
    #
    [same text elided...]
    #
    # Note::
    #     Ceci n'est pas une note
    #
    # Since: 2.10
    ##

It is second-to-last, and the last section is SINCE.  Good.

> @@ -216,7 +216,7 @@ If you're bored enough to read this, go see a video of boxed cats
>  a feature
>      featurec=md-feat2
>  another feature
> -    section=Plain
> +    section=Details
>  .. qmp-example::
>  
>     -> "this example"

This one is also at the end.  Good.
Re: [PATCH 1/8] qapi: differentiate "intro" and "details" sections
Posted by John Snow 2 weeks, 3 days ago
On Fri, Mar 20, 2026, 8:24 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > This patch begins distinguishing "Plain" sections as being either
> > "Intro" or "Details" sections for the purpose of knowing when/where/how
> > to inline those sections.
> >
> > The Intro section is always the first section of any doc block. It may
> > be empty or any number of paragraphs. It is interrupted by any other
> > non-plaintext section, i.e.; Members, Features, Errors, Returns, Since,
> > and TODO.
> >
> > The details section, when present, is either the last section or the
> > second-to-last section when a "Since:" section is present. It consists
> > of any plain text in the doc block that follows any named sections if
> > present.
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
>
> The commit message explains what kinds INTRO and DETAILS are, but not
> why they're useful.  My guess:
>

Right, my apologies. Leaning on you having some existing knowledge here,
and also going light on details because the series is still obviously in
flux.

Let me elaborate on the motivations here...


> 1. Represent the future "Details:" marker: the plain section before it
> is of kind INTRO, the one afterwards is of kind DETAILS.
>

Yes.


> 2. Future programming convenience?  With just PLAIN, code may have to
> understand the section's context to make decisions, and with INTRO and
> DETAILS is doesn't.
>
> Guess close enough?
>

Pretty much.

Originally, I thought I'd inline things like this:

Intro (child)
Intro (parent)
...other sections...
Details (parent)
Details (child)

Last time we went over this, you mused that there was likely never a
sufficient reason to actually inline the intro. I recall believing and
accepting this.

So functionally what this does for us is:

(1) Explicitly delineate what comes before the tabular section of the docs
(members, Returns, Errors, features) and what comes after.

(2) Explicitly defines what will not be inlined.


> The commit message covers PLAIN sections at the beginning and at the end
> (modulo Since:).  It doesn't cover PLAIN sections between tagged
> sections / member descriptions.  You disallow these in PATCH 2.  You can
> either cover them here, or get rid of them by swapping PATCH 1 and 2.
>

Sure. For now they're separated just to separate concerns in review, but if
you believe both should be all-at-once, that's fine too. I think there's
not a regression by separating it, though. It's just a semantic change from
details meaning "anything after the intro" to "specifically the closing
section". The intermediate meaning exists for only one patch.


> Hmm, is your description of DETAILS accurate?  Looks like it isn't; see
> my review of tests/qapi-schema/doc-good.out below.
>

Whoops, future-think. It winds up being true, but isn't true yet as of this
commit. Well, almost. See below.


> > ---
> >  docs/sphinx/qapidoc.py         |  2 +-
> >  scripts/qapi/parser.py         | 35 +++++++++++++++++++++++-----------
> >  tests/qapi-schema/doc-good.out |  8 ++++----
> >  3 files changed, 29 insertions(+), 16 deletions(-)
>
> [Skipping the Python code in my first pass...]
>
> > diff --git a/tests/qapi-schema/doc-good.out
> b/tests/qapi-schema/doc-good.out
> > index 04a55072646..04e29e8d50f 100644
> > --- a/tests/qapi-schema/doc-good.out
> > +++ b/tests/qapi-schema/doc-good.out
> > @@ -116,7 +116,7 @@ The _one_ {and only}, description on the same line
> >  Also _one_ {and only}
> >      feature=enum-member-feat
> >  a member feature
> > -    section=Plain
> > +    section=Details
> >  @two is undocumented
>
> The section containing "@two is undocumented" changes from PLAIN to
> DETAILS.  Doc source:
>
>     ##
>     # @Enum:
>     #
>     # @one: The _one_ {and only}, description on the same line
>     #
>     # Features:
>     # @enum-feat: Also _one_ {and only}
>     # @enum-member-feat: a member feature
>     #
>     # @two is undocumented
>     ##
>
> It is at the end.  Good.
>
> >  doc symbol=Base
> >      body=
> > @@ -175,7 +175,7 @@ description starts on the same line
> >  a feature
> >      feature=cmd-feat2
> >  another feature
> > -    section=Plain
> > +    section=Details
> >  .. note:: @arg3 is undocumented
>
> The section containing "@arg3 is undocumented" changes from PLAIN to
> DETAILS.  Doc source:
>
> Doc source:
>
>     ##
>     # @cmd:
>     #
>     # @arg1:
>     #     description starts on a new line,
>     #     indented
>     #
>     # @arg2: description starts on the same line
>     #     remainder indented differently
>     #
>     # Returns: @Object
>     #
>     # Errors: some
>     #
>     # Features:
>     # @cmd-feat1: a feature
>     # @cmd-feat2: another feature
>     #
>     # .. note:: @arg3 is undocumented
>     #
>     # TODO: frobnicate
>     #
>
> It is *not* at the end.  Is the commit message inaccurate?
>

Ah. I wasn't considering TODO... Since it is a comment I mentally elided it.

What I mean to say is that Details is (will be) essentially the last
content section that is actually rendered.

In the HTML, it will always be last if present because both TODO and Since
are actually entirely removed from the flow of the document.

Though as we both point out, what i write is not technically true here. (It
can currently be an intermediate section AND Since is not the only section
that may follow it.)


> >      section=Returns
> >  @Object
> > @@ -183,7 +183,7 @@ another feature
> >  some
> >      section=Todo
> >  frobnicate
> > -    section=Plain
> > +    section=Details
> >  .. admonition:: Notes
> >
> >   - Lorem ipsum dolor sit amet
>     - Ut enim ad minim veniam
>
>     [some section body text elided for brevity...]
>
>    Note::
>        Ceci n'est pas une note
>        section=Since
>    2.10
>
> The section starting with the adminition and ending after "Ceci n'est
> pas une note" changes from PLAIN to DETAILS.  Doc source continued:
>
>     # .. admonition:: Notes
>     #
>     #  - Lorem ipsum dolor sit amet
>     #  - Ut enim ad minim veniam
>     #
>     [same text elided...]
>     #
>     # Note::
>     #     Ceci n'est pas une note
>     #
>     # Since: 2.10
>     ##
>
> It is second-to-last, and the last section is SINCE.  Good.
>
> > @@ -216,7 +216,7 @@ If you're bored enough to read this, go see a video
> of boxed cats
> >  a feature
> >      featurec=md-feat2
> >  another feature
> > -    section=Plain
> > +    section=Details
> >  .. qmp-example::
> >
> >     -> "this example"
>
> This one is also at the end.  Good.
>
>
Re: [PATCH 1/8] qapi: differentiate "intro" and "details" sections
Posted by Markus Armbruster 2 weeks ago
John Snow <jsnow@redhat.com> writes:

> On Fri, Mar 20, 2026, 8:24 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > This patch begins distinguishing "Plain" sections as being either
>> > "Intro" or "Details" sections for the purpose of knowing when/where/how
>> > to inline those sections.
>> >
>> > The Intro section is always the first section of any doc block. It may
>> > be empty or any number of paragraphs. It is interrupted by any other
>> > non-plaintext section, i.e.; Members, Features, Errors, Returns, Since,
>> > and TODO.
>> >
>> > The details section, when present, is either the last section or the
>> > second-to-last section when a "Since:" section is present. It consists
>> > of any plain text in the doc block that follows any named sections if
>> > present.
>> >
>> > Signed-off-by: John Snow <jsnow@redhat.com>
>>
>> The commit message explains what kinds INTRO and DETAILS are, but not
>> why they're useful.  My guess:
>>
>
> Right, my apologies. Leaning on you having some existing knowledge here,
> and also going light on details because the series is still obviously in
> flux.

No worries!

> Let me elaborate on the motivations here...
>
>
>> 1. Represent the future "Details:" marker: the plain section before it
>> is of kind INTRO, the one afterwards is of kind DETAILS.
>>
>
> Yes.
>
>
>> 2. Future programming convenience?  With just PLAIN, code may have to
>> understand the section's context to make decisions, and with INTRO and
>> DETAILS is doesn't.
>>
>> Guess close enough?
>>
>
> Pretty much.
>
> Originally, I thought I'd inline things like this:
>
> Intro (child)
> Intro (parent)
> ...other sections...
> Details (parent)
> Details (child)
>
> Last time we went over this, you mused that there was likely never a
> sufficient reason to actually inline the intro. I recall believing and
> accepting this.

Turns out we use the first section (i.e. INTRO) for describing the thing
being defined.  Inlining such descriptions results in nonsense.  For
instance, here's SevGuestProperties and its base SevGuestProperties:

    ##
    # @SevCommonProperties:
    #
    # Properties common to objects that are derivatives of sev-common.
    #
    [...]
    ##
    # @SevGuestProperties:
    #
    # Properties for sev-guest objects.
    #
    # @dh-cert-file: guest owners DH certificate (encoded with base64)
    #
    [...]

Inlining the base as you originally thought would result in something
like

    Object SevGuestProperties (Since: 2.12)

       Properties for sev-guest objects.

       Properties common to objects that are derivatives of sev-common.

       Members: * dh-cert-file (string, optional) -- guest owners DH
       certificate (encoded with base64)

The intro paragraph inlined from the base is nonsense.

We either stop writing such descriptions (ugh), or we drop them on
inlining.  If we drop, code needs to recognize them.  The only method I
can see is to assume INTRO is description.

Now, the QAPI generator cannot stop people from putting stuff in INTRO
is *not* description and *should* be inlined.  This is a risk we will
have to accept to gain the benefits of inlining.

For the inliner's initial merge, we'll want to review all the INTRO
sections the inliner drops.  If almost all of them are fine, the risk is
low.

> So functionally what this does for us is:
>
> (1) Explicitly delineate what comes before the tabular section of the docs
> (members, Returns, Errors, features) and what comes after.
>
> (2) Explicitly defines what will not be inlined.

Yes.

I think we'll want to at least hint at this in the final commit message.
The full story would be too much, I guess.

>> The commit message covers PLAIN sections at the beginning and at the end
>> (modulo Since:).  It doesn't cover PLAIN sections between tagged
>> sections / member descriptions.  You disallow these in PATCH 2.  You can
>> either cover them here, or get rid of them by swapping PATCH 1 and 2.
>>
>
> Sure. For now they're separated just to separate concerns in review, but if
> you believe both should be all-at-once, that's fine too. I think there's
> not a regression by separating it, though. It's just a semantic change from
> details meaning "anything after the intro" to "specifically the closing
> section". The intermediate meaning exists for only one patch.

Have you tried swapping the patches?

If that's bothersome, squashing them together may well do.

>> Hmm, is your description of DETAILS accurate?  Looks like it isn't; see
>> my review of tests/qapi-schema/doc-good.out below.
>>
>
> Whoops, future-think. It winds up being true, but isn't true yet as of this
> commit. Well, almost. See below.
>
>
>> > ---
>> >  docs/sphinx/qapidoc.py         |  2 +-
>> >  scripts/qapi/parser.py         | 35 +++++++++++++++++++++++-----------
>> >  tests/qapi-schema/doc-good.out |  8 ++++----
>> >  3 files changed, 29 insertions(+), 16 deletions(-)
>>
>> [Skipping the Python code in my first pass...]
>>
>> > diff --git a/tests/qapi-schema/doc-good.out
>> b/tests/qapi-schema/doc-good.out
>> > index 04a55072646..04e29e8d50f 100644
>> > --- a/tests/qapi-schema/doc-good.out
>> > +++ b/tests/qapi-schema/doc-good.out
>> > @@ -116,7 +116,7 @@ The _one_ {and only}, description on the same line
>> >  Also _one_ {and only}
>> >      feature=enum-member-feat
>> >  a member feature
>> > -    section=Plain
>> > +    section=Details
>> >  @two is undocumented
>>
>> The section containing "@two is undocumented" changes from PLAIN to
>> DETAILS.  Doc source:
>>
>>     ##
>>     # @Enum:
>>     #
>>     # @one: The _one_ {and only}, description on the same line
>>     #
>>     # Features:
>>     # @enum-feat: Also _one_ {and only}
>>     # @enum-member-feat: a member feature
>>     #
>>     # @two is undocumented
>>     ##
>>
>> It is at the end.  Good.
>>
>> >  doc symbol=Base
>> >      body=
>> > @@ -175,7 +175,7 @@ description starts on the same line
>> >  a feature
>> >      feature=cmd-feat2
>> >  another feature
>> > -    section=Plain
>> > +    section=Details
>> >  .. note:: @arg3 is undocumented
>>
>> The section containing "@arg3 is undocumented" changes from PLAIN to
>> DETAILS.  Doc source:
>>
>> Doc source:
>>
>>     ##
>>     # @cmd:
>>     #
>>     # @arg1:
>>     #     description starts on a new line,
>>     #     indented
>>     #
>>     # @arg2: description starts on the same line
>>     #     remainder indented differently
>>     #
>>     # Returns: @Object
>>     #
>>     # Errors: some
>>     #
>>     # Features:
>>     # @cmd-feat1: a feature
>>     # @cmd-feat2: another feature
>>     #
>>     # .. note:: @arg3 is undocumented
>>     #
>>     # TODO: frobnicate
>>     #
>>
>> It is *not* at the end.  Is the commit message inaccurate?
>>
>
> Ah. I wasn't considering TODO... Since it is a comment I mentally elided it.
>
> What I mean to say is that Details is (will be) essentially the last
> content section that is actually rendered.
>
> In the HTML, it will always be last if present because both TODO and Since
> are actually entirely removed from the flow of the document.

We can handwave these two away, but the issue persists until the next
patch.

Consider this change to the test before the series:

    diff --git a/tests/qapi-schema/doc-good.json b/tests/qapi-schema/doc-good.json
    index fac13425b7..4d586b043f 100644
    --- a/tests/qapi-schema/doc-good.json
    +++ b/tests/qapi-schema/doc-good.json
    @@ -169,6 +169,8 @@
     #
     # Returns: @Object
     #
    +# plain in the middle
    +#
     # Errors: some
     #
     # TODO: frobnicate
    diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
    index 04a5507264..040e901474 100644
    --- a/tests/qapi-schema/doc-good.out
    +++ b/tests/qapi-schema/doc-good.out
    @@ -179,6 +179,8 @@ another feature
     .. note:: @arg3 is undocumented
         section=Returns
     @Object
    +    section=Plain
    +plain in the middle
         section=Errors
     some
         section=Todo

The current patch then acquires another hunk:

    diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
    index 04e29e8d50..28abb1d98e 100644
    --- a/tests/qapi-schema/doc-good.out
    +++ b/tests/qapi-schema/doc-good.out
    @@ -179,6 +179,8 @@ another feature
     .. note:: @arg3 is undocumented
         section=Returns
     @Object
    +    section=Details
    +plain in the middle
         section=Errors
     some
         section=Todo

> Though as we both point out, what i write is not technically true here. (It
> can currently be an intermediate section AND Since is not the only section
> that may follow it.)

[...]