All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 00/10] qapi: enforce section ordering
@ 2026-04-08  4:55 John Snow
  2026-04-08  4:55 ` [PATCH v2 01/10] qapi: differentiate "intro" and "details" sections John Snow
                   ` (10 more replies)
  0 siblings, 11 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

Hiya, this series is meant to accomplish mostly one thing: Enforce a
stricter ordering of sections in QAPI documentation blocks.

The reason to do this is mostly for the sake of the inliner: if QAPI
documentation blocks have some known, canonical order, it is easier to
merge two documentation blocks together for the purposes of showing all
arguments for commands/etc in a simple, flat list without needing to
follow hyperlink breadcrumbs.

Another reason to do this is to simplify where we insert autogenerated
documentation. If the order is enforced, then inserting "Not Documented"
stubs for members and generated "Returns:" statements can have a much
simpler algorithm that will always match how manually written
documentation is presented, in the same order.

This is still pretty RFC quality, the tests have not been implemented
and the implementation of changes in the parser are still pretty
fuzzy. The main point of this series at this point in time is to review
the QAPI source changes and decide if the strategy employed in fixing
the section ordering is the direction we ultimately want to go in.

V2:
 - Add quite a few FIXME stubs for tests
 - Much more carefully delineate QAPI source changes into ones required
   to prevent visible changes, and ones that explictly create visible
   changes
 - Various commit message / comment changes
 - Fix heuristic for griping about Intro/Details "ambiguity" to also
   ignore generated "Returns" sections, which was missing before and
   missed quite a few cases that did impact rendered output

To verify rendering changes (or lack thereof), I used this strategy:

(1) For a reference output before a change, I ran a build:
    > V=1 DEBUG=1 make -j13;

(2) Then I created some reference output for the intermediate rST
    debugging output files (fish syntax):
    > for i in *.ir; sed -E 's|\.json:[0-9]{4}|.json:nnnn|g' $i > $i.ref; end

(3) Then after applying a patch, to check for any differences, I re-ran
    the build as in (1) and then:
    > for i in *.ir; sed -E 's|\.json:[0-9]{4}|.json:nnnn|g' $i > $i.new; end
    > for i in *.ir; meld $i.ref $i.new; end

An observation: Most of the time, the Intro section is only one
paragraph anyway. We might be able to save on some explicit "Details:"
syntax if we just formalize the idea that the intro can only ever be at
most one paragraph. I don't know if we want to do that (Do we want to
keep the ability to run long in the "intro"?) - but it would cut down on
quite a lot of markup that this series adds.

John Snow (10):
  qapi: differentiate "intro" and "details" sections
  qapi: prohibit 'details' sections between tagged sections
  qapi: add "Details:" disambiguation marker
  qapi: add "Details:" markers where needed
  qapi: add "Details:" markers where potentially needed
  qapi: detect potentially semantically ambiguous intro paragraphs
  qapi: re-order QAPI doc block sections
  qapi: enforce doc block section ordering
  qapi: re-order 'since' sections to always be last
  qapi: enforce strict positioning for "Since:" section

 docs/devel/qapi-code-gen.rst                 |  33 +++-
 docs/interop/firmware.json                   |   4 +-
 docs/interop/vhost-user.json                 |   3 +-
 docs/sphinx/qapidoc.py                       |   2 +-
 qapi/accelerator.json                        |  12 +-
 qapi/acpi.json                               |   8 +-
 qapi/block-core.json                         | 183 ++++++++++---------
 qapi/block-export.json                       |  20 +-
 qapi/block.json                              |  48 ++---
 qapi/char.json                               |  36 ++--
 qapi/control.json                            |  14 +-
 qapi/dump.json                               |  16 +-
 qapi/machine-s390x.json                      |  16 +-
 qapi/machine.json                            | 144 ++++++++-------
 qapi/migration.json                          | 102 ++++++-----
 qapi/misc-arm.json                           |   6 +-
 qapi/misc-i386.json                          |  40 ++--
 qapi/misc.json                               |  68 ++++---
 qapi/net.json                                |  42 ++---
 qapi/pci.json                                |   4 +-
 qapi/qdev.json                               |  12 +-
 qapi/qom.json                                |  34 ++--
 qapi/replay.json                             |  16 +-
 qapi/rocker.json                             |  16 +-
 qapi/run-state.json                          |  66 ++++---
 qapi/tpm.json                                |  12 +-
 qapi/trace.json                              |   8 +-
 qapi/transaction.json                        |   4 +-
 qapi/ui.json                                 |  76 ++++----
 qapi/vfio.json                               |   4 +-
 qapi/virtio.json                             |  46 ++---
 qapi/yank.json                               |   2 +-
 qga/qapi-schema.json                         |   2 +
 scripts/qapi/parser.py                       | 134 ++++++++++++--
 tests/qapi-schema/doc-good.json              |  12 +-
 tests/qapi-schema/doc-good.out               |  10 +-
 tests/qapi-schema/doc-good.txt               |  18 +-
 tests/qapi-schema/doc-misplaced-details.err  |   0
 tests/qapi-schema/doc-misplaced-details.json |   3 +
 tests/qapi-schema/doc-misplaced-details.out  |  11 ++
 tests/qapi-schema/doc-missing-details.err    |   0
 tests/qapi-schema/doc-missing-details.json   |   3 +
 tests/qapi-schema/doc-missing-details.out    |  11 ++
 tests/qapi-schema/meson.build                |   2 +
 44 files changed, 784 insertions(+), 519 deletions(-)
 create mode 100644 tests/qapi-schema/doc-misplaced-details.err
 create mode 100644 tests/qapi-schema/doc-misplaced-details.json
 create mode 100644 tests/qapi-schema/doc-misplaced-details.out
 create mode 100644 tests/qapi-schema/doc-missing-details.err
 create mode 100644 tests/qapi-schema/doc-missing-details.json
 create mode 100644 tests/qapi-schema/doc-missing-details.out

-- 
2.53.0




^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH v2 01/10] qapi: differentiate "intro" and "details" sections
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 02/10] qapi: prohibit 'details' sections between tagged sections John Snow
                   ` (9 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

This patch begins distinguishing "Plain" sections as being either
"Intro" or "Details" sections for the purpose of, in the forthcoming
documentation inliner, knowing when/where/how to inline those
sections. i.e. "Intro" sections will not be inlined, but "Details"
sections will be. The intent is for "Intro" sections to describe
(briefly) only the specific entity, but for details sections to describe
information in greater detail that would be relevant to include in
inlined documentation.

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, as of this patch, is any plaintext section after
the initial section. (It cannot appear immediately following the INTRO
section, because it would then simply be part of that intro section.)

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review note: .ir files remain unchanged before and after this patch, it
should be completely inert with regards to the rendered output. --js]

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



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 02/10] qapi: prohibit 'details' sections between tagged sections
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
  2026-04-08  4:55 ` [PATCH v2 01/10] qapi: differentiate "intro" and "details" sections John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 03/10] qapi: add "Details:" disambiguation marker John Snow
                   ` (8 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

This patch prohibits plain documentation sections from appearing between
(most*) tagged/description sections. The two existing uses of this
pattern (One in tests, one in our actual docs) are patched out.

This is being done for two main reasons:

(1) By limiting the locations where a "details" section may occur, it is
easier to reason about where a details section must be placed when
merging two or more documentation blocks together. This eases the logic
in the forthcoming inliner significantly.

(2) It improves visual consistency in the rendered HTML output. Tagged
sections and descriptions are all presented in a tabular format, so
prohibiting free-form text from interleaving the table looks better.

(*"most": As of this patch, TODO and Since sections may still occur
before, after, or between detail sections. These sections are removed
from the flow of the document when rendered to HTML, so in effect even
though we may have multiple details sections, they will be contiguous in
rendered HTML output.)

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review note: the output of *.ir files changes slightly, but
intentionally, with this patch; the two distinct changes correlate with
the edits made to the qapi .json files modified explicitly by this
patch. --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/devel/qapi-code-gen.rst                 | 15 +++++++++++----
 qapi/qom.json                                |  4 ++--
 scripts/qapi/parser.py                       | 19 +++++++++++++++++++
 tests/qapi-schema/doc-good.json              |  4 ++--
 tests/qapi-schema/doc-good.out               |  4 ++--
 tests/qapi-schema/doc-good.txt               |  8 ++++----
 tests/qapi-schema/doc-misplaced-details.err  |  0
 tests/qapi-schema/doc-misplaced-details.json |  3 +++
 tests/qapi-schema/doc-misplaced-details.out  | 11 +++++++++++
 tests/qapi-schema/meson.build                |  1 +
 10 files changed, 55 insertions(+), 14 deletions(-)
 create mode 100644 tests/qapi-schema/doc-misplaced-details.err
 create mode 100644 tests/qapi-schema/doc-misplaced-details.json
 create mode 100644 tests/qapi-schema/doc-misplaced-details.out

diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
index 3a632b4a648..06ab3547fdc 100644
--- a/docs/devel/qapi-code-gen.rst
+++ b/docs/devel/qapi-code-gen.rst
@@ -985,10 +985,17 @@ When documentation is required (see pragma_ 'doc-required'), every
 definition must have documentation.
 
 Definition documentation starts with a line naming the definition,
-followed by an optional overview, a description of each argument (for
-commands and events), member (for structs and unions), branch (for
-alternates), or value (for enums), a description of each feature (if
-any), and finally optional tagged sections.
+followed by an optional overview (the "intro"), a description of each
+argument (for commands and events), member (for structs and unions),
+branch (for alternates), or value (for enums), a description of each
+feature (if any), most optional tagged sections, plaintext detail
+paragraphs, and finally the optional "Since" tagged section.
+
+Paragraphs following the optional overview (the "intro") may not appear
+between any description or tagged section as described above. These
+sections are "detail" sections and must appear at the end of the
+documentation block, with the exception of "Since" or "TODO" sections
+which may appear after.
 
 Descriptions start with '\@name:'.  The description text must be
 indented like this::
diff --git a/qapi/qom.json b/qapi/qom.json
index c653248f85d..1b47abd44e9 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -243,12 +243,12 @@
 #
 # @typename: the type name of an object
 #
+# Returns: a list describing object properties
+#
 # .. note:: Objects can create properties at runtime, for example to
 #    describe links between different devices and/or objects.  These
 #    properties are not included in the output of this command.
 #
-# Returns: a list describing object properties
-#
 # Since: 2.12
 ##
 { 'command': 'qom-list-properties',
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index da0ac32ad89..8a21e9e8b56 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -545,6 +545,21 @@ def get_doc(self) -> 'QAPIDoc':
             self.accept(False)
             line = self.get_doc_line()
             have_tagged = False
+            no_more_tags = False
+
+            def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
+                if isinstance(this, str):
+                    this = QAPIDoc.Kind.from_string(this)
+                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
+                    return
+
+                if no_more_tags:
+                    raise QAPIParseError(
+                        self,
+                        f"'{this}' section cannot appear after plain "
+                        "paragraphs that follow other tagged sections\n"
+                        "Move this section up above the plain paragraph(s)."
+                    )
 
             while line is not None:
                 # Blank lines
@@ -558,6 +573,7 @@ def get_doc(self) -> 'QAPIDoc':
                     if doc.features:
                         raise QAPIParseError(
                             self, "duplicated 'Features:' line")
+                    _tag_check(QAPIDoc.Kind.FEATURE)
                     self.accept(False)
                     line = self.get_doc_line()
                     while line == '':
@@ -621,6 +637,7 @@ def get_doc(self) -> 'QAPIDoc':
                         )
                         raise QAPIParseError(self, emsg)
 
+                    _tag_check(match.group(1))
                     doc.new_tagged_section(
                         self.info,
                         QAPIDoc.Kind.from_string(match.group(1))
@@ -632,6 +649,8 @@ def get_doc(self) -> 'QAPIDoc':
                     have_tagged = True
                 else:
                     # plain paragraph
+                    if have_tagged:
+                        no_more_tags = True
 
                     # Paragraphs before tagged sections are "intro" paragraphs.
                     # Any appearing after are "detail" paragraphs.
diff --git a/tests/qapi-schema/doc-good.json b/tests/qapi-schema/doc-good.json
index fac13425b72..9103fed472e 100644
--- a/tests/qapi-schema/doc-good.json
+++ b/tests/qapi-schema/doc-good.json
@@ -165,12 +165,12 @@
 # @cmd-feat1: a feature
 # @cmd-feat2: another feature
 #
-# .. note:: @arg3 is undocumented
-#
 # Returns: @Object
 #
 # Errors: some
 #
+# .. note:: @arg3 is undocumented
+#
 # TODO: frobnicate
 #
 # .. admonition:: Notes
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
index 04e29e8d50f..6a0167ad580 100644
--- a/tests/qapi-schema/doc-good.out
+++ b/tests/qapi-schema/doc-good.out
@@ -175,12 +175,12 @@ description starts on the same line
 a feature
     feature=cmd-feat2
 another feature
-    section=Details
-.. note:: @arg3 is undocumented
     section=Returns
 @Object
     section=Errors
 some
+    section=Details
+.. note:: @arg3 is undocumented
     section=Todo
 frobnicate
     section=Details
diff --git a/tests/qapi-schema/doc-good.txt b/tests/qapi-schema/doc-good.txt
index 74b73681d32..ded699dd596 100644
--- a/tests/qapi-schema/doc-good.txt
+++ b/tests/qapi-schema/doc-good.txt
@@ -120,16 +120,16 @@ Command cmd (Since: 2.10)
 
       * **cmd-feat2** -- another feature
 
-   Note:
-
-     "arg3" is undocumented
-
    Return:
       "Object" -- "Object"
 
    Errors:
       some
 
+   Note:
+
+     "arg3" is undocumented
+
    Notes:
 
    * Lorem ipsum dolor sit amet
diff --git a/tests/qapi-schema/doc-misplaced-details.err b/tests/qapi-schema/doc-misplaced-details.err
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/qapi-schema/doc-misplaced-details.json b/tests/qapi-schema/doc-misplaced-details.json
new file mode 100644
index 00000000000..de593ab0f69
--- /dev/null
+++ b/tests/qapi-schema/doc-misplaced-details.json
@@ -0,0 +1,3 @@
+# FIXME / TODO
+# This test should test for the error message when we misplace details
+# sections interleaved between descriptions/tagged sections.
diff --git a/tests/qapi-schema/doc-misplaced-details.out b/tests/qapi-schema/doc-misplaced-details.out
new file mode 100644
index 00000000000..3c602d2592c
--- /dev/null
+++ b/tests/qapi-schema/doc-misplaced-details.out
@@ -0,0 +1,11 @@
+module ./builtin
+object q_empty
+enum QType
+    member none
+    member qnull
+    member qnum
+    member qstring
+    member qdict
+    member qlist
+    member qbool
+module doc-misplaced-details.json
diff --git a/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build
index debff633ac1..c233d77ab78 100644
--- a/tests/qapi-schema/meson.build
+++ b/tests/qapi-schema/meson.build
@@ -83,6 +83,7 @@ schemas = [
   'doc-invalid-section.json',
   'doc-invalid-start.json',
   'doc-long-line.json',
+  'doc-misplaced-details.json',
   'doc-missing-colon.json',
   'doc-missing-expr.json',
   'doc-missing-space.json',
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 03/10] qapi: add "Details:" disambiguation marker
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
  2026-04-08  4:55 ` [PATCH v2 01/10] qapi: differentiate "intro" and "details" sections John Snow
  2026-04-08  4:55 ` [PATCH v2 02/10] qapi: prohibit 'details' sections between tagged sections John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 04/10] qapi: add "Details:" markers where needed John Snow
                   ` (7 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

When a documentation block consists only of plaintext, there is nothing
to semantically differentiate the "intro" from the "details"
section. For the purposes of the inliner, the intro section of
documentation inlined into another documentation block will be omitted,
which may lead to unintentional omissions of details in the rendered
output.

When the delineation between "intro" and "details" is not clear because
there is no intervening description/tagged sections, the parser assumes
the entire text section is an "intro" section. This may not always be
semantically true, so this patch clarifies certain sections explicitly
as "details" sections by using an empty "Details:" marker.

Replace existing uses of "TODO:" hacks to achieve this effect with the
canonical marker.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review note: all *.ir files are completely unchanged before and after
this patch. --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/devel/qapi-code-gen.rst | 18 ++++++++++++++++++
 qapi/machine.json            |  2 +-
 qapi/migration.json          |  4 ++--
 qapi/net.json                |  2 +-
 qapi/yank.json               |  2 +-
 scripts/qapi/parser.py       |  8 ++++++++
 6 files changed, 31 insertions(+), 5 deletions(-)

diff --git a/docs/devel/qapi-code-gen.rst b/docs/devel/qapi-code-gen.rst
index 06ab3547fdc..e575b54128e 100644
--- a/docs/devel/qapi-code-gen.rst
+++ b/docs/devel/qapi-code-gen.rst
@@ -1048,6 +1048,24 @@ definition.
 QMP).  In other sections, the text is formatted, and rST markup can be
 used.
 
+In cases where documentation consists of several paragraphs of text with
+no intervening sections to delineate them, it may become necessary to
+explicitly declare the start of the details section by using a
+"Details:" marker. For example::
+
+  ##
+  # @foobar
+  #
+  # foobar is an example definition.
+  #
+  # Details:
+  #
+  # Since there are no other sections in this documentation, the above
+  # "Details:" marker is required to mark this and subsequent paragraphs
+  # as the "Details" section. Any automatically generated documentation
+  # sections will be inserted above this point instead of below.
+  ##
+
 QMP Examples can be added by using the ``.. qmp-example::`` directive.
 In its simplest form, this can be used to contain a single QMP code
 block which accepts standard JSON syntax with additional server
diff --git a/qapi/machine.json b/qapi/machine.json
index 685e4e29b87..bc2279b2526 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -1259,7 +1259,7 @@
 # Return the amount of initially allocated and present hotpluggable
 # (if enabled) memory in bytes.
 #
-# TODO: This line is a hack to separate the example from the body
+# Details:
 #
 # .. qmp-example::
 #
diff --git a/qapi/migration.json b/qapi/migration.json
index 7134d4ce47e..2142f74e3c7 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -1633,7 +1633,7 @@
 #
 # Query replication status while the vm is running.
 #
-# TODO: This line is a hack to separate the example from the body
+# Details:
 #
 # .. qmp-example::
 #
@@ -1687,7 +1687,7 @@
 #
 # Query COLO status while the vm is running.
 #
-# TODO: This line is a hack to separate the example from the body
+# Details:
 #
 # .. qmp-example::
 #
diff --git a/qapi/net.json b/qapi/net.json
index 118bd349651..c011d6dc1a9 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -1070,7 +1070,7 @@
 # switches.  This can be useful when network bonds fail-over the
 # active slave.
 #
-# TODO: This line is a hack to separate the example from the body
+# Details:
 #
 # .. qmp-example::
 #
diff --git a/qapi/yank.json b/qapi/yank.json
index f3cd5c15d60..2854a8a9d2a 100644
--- a/qapi/yank.json
+++ b/qapi/yank.json
@@ -104,7 +104,7 @@
 #
 # Query yank instances.  See `YankInstance` for more information.
 #
-# TODO: This line is a hack to separate the example from the body
+# Details:
 #
 # .. qmp-example::
 #
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 8a21e9e8b56..1d52d80e672 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -590,6 +590,14 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                         raise QAPIParseError(
                             self, 'feature descriptions expected')
                     have_tagged = True
+                elif line == 'Details:':
+                    _tag_check("Details")
+                    self.accept(False)
+                    line = self.get_doc_line()
+                    while line == '':
+                        self.accept(False)
+                        line = self.get_doc_line()
+                    have_tagged = True
                 elif match := self._match_at_name_colon(line):
                     # description
                     if have_tagged:
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 04/10] qapi: add "Details:" markers where needed
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (2 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 03/10] qapi: add "Details:" disambiguation marker John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 05/10] qapi: add "Details:" markers where potentially needed John Snow
                   ` (6 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

Add additional "Details:" markers in places where doing so *currently*
impacts the rendered output order of documentation sections.

There are two cases where this is done:

(1) Where we insert stub/autogenerated documentation for
    members/args/etc and return information is dependent on the
    intro-details split location. Adding a "Details:" marker facilitates
    inserting this information in the middle of the documentation block
    instead of at the end.

    These are locations where we likely SHOULD have been using the
    "TODO:" hack which was formalized in the previous commit, but had
    not been.

(2) Cases where "Since:" currently behaves as the Intro/Details
    separation point, but in the wrong location. Since this edit changes
    the output, these changes are included in this patch as well. These
    locations are for HV_BALLOON_STATUS_REPORT, query-gic-capabilities,
    and query-sev.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review note: this patch DOES change the output of the *.ir files, but
intentionally for these locations. --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 qapi/block-core.json | 3 +++
 qapi/machine.json    | 2 ++
 qapi/misc-arm.json   | 2 ++
 qapi/misc-i386.json  | 2 ++
 qapi/qom.json        | 4 ++++
 qga/qapi-schema.json | 2 ++
 6 files changed, 15 insertions(+)

diff --git a/qapi/block-core.json b/qapi/block-core.json
index 508b081ac16..abdcddb0a09 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -5007,6 +5007,9 @@
 # @blockdev-reopen:
 #
 # Reopens one or more block devices using the given set of options.
+#
+# Details:
+#
 # Any option not specified will be reset to its default value
 # regardless of its previous status.  If an option cannot be changed
 # or a particular driver does not support reopening then the command
diff --git a/qapi/machine.json b/qapi/machine.json
index bc2279b2526..012f61e2a7e 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -1223,6 +1223,8 @@
 # Emitted when the hv-balloon driver receives a "STATUS" message from
 # the guest.
 #
+# Details:
+#
 # .. note:: This event is rate-limited.
 #
 # Since: 8.2
diff --git a/qapi/misc-arm.json b/qapi/misc-arm.json
index 4dc66d00e5c..4e3f1a54055 100644
--- a/qapi/misc-arm.json
+++ b/qapi/misc-arm.json
@@ -33,6 +33,8 @@
 # It will return a list of `GICCapability` objects that describe its
 # capability bits.
 #
+# Details:
+#
 # On non-ARM targets this command will report an error as the GIC
 # technology is not applicable.
 #
diff --git a/qapi/misc-i386.json b/qapi/misc-i386.json
index 05a94d6c416..c92853507f3 100644
--- a/qapi/misc-i386.json
+++ b/qapi/misc-i386.json
@@ -127,6 +127,8 @@
 #
 # Return information about SEV/SEV-ES/SEV-SNP.
 #
+# Details:
+#
 # If unavailable due to an incompatible configuration the returned
 # @enabled field is set to 'false' and the state of all other fields
 # is unspecified.
diff --git a/qapi/qom.json b/qapi/qom.json
index 1b47abd44e9..568b7d4b997 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -777,6 +777,8 @@
 #
 # Properties for memory-backend-shm objects.
 #
+# Details:
+#
 # This memory backend supports only shared memory, which is the
 # default.
 #
@@ -792,6 +794,8 @@
 #
 # Properties for memory-backend-epc objects.
 #
+# Details:
+#
 # The @merge boolean option is false by default with epc
 #
 # The @dump boolean option is false by default with epc
diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
index c57bc9a02f6..f2c17d08703 100644
--- a/qga/qapi-schema.json
+++ b/qga/qapi-schema.json
@@ -420,6 +420,8 @@
 #
 # Get guest fsfreeze state.
 #
+# Details:
+#
 # .. note:: This may fail to properly report the current state as a
 #    result of some other guest processes having issued an fs
 #    freeze/thaw.
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 05/10] qapi: add "Details:" markers where potentially needed
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (3 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 04/10] qapi: add "Details:" markers where needed John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 06/10] qapi: detect potentially semantically ambiguous intro paragraphs John Snow
                   ` (5 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

There are several locations within the QAPI source that are classified
as "Intro" text, even though semantically this is not true. Altering
these sections with an explicit Details: marker currently yields no
difference to rendered output. In the future, the inliner and/or the
addition of new stub sections *may* make these distinctions important.

These locations were identified using a heuristic patch to the QAPI doc
parser to emit a warning for Intro sections consisting of two or more
paragraphs without any other explicit section present in the source
code.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review note: this patch does not change the *.ir output at all; there
is no visible difference whatsoever. The point of this patch is solely
to prevent yelping by the heuristic checker. It may or may not become
relevant later once the inliner is merged, I did not audit that far -
neither did I spend time attempting to improve the heuristic yelper,
believing "KISS". --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 qapi/migration.json | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/qapi/migration.json b/qapi/migration.json
index 2142f74e3c7..558b4f145ed 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -1651,6 +1651,8 @@
 #
 # Xen uses this command to notify replication to trigger a checkpoint.
 #
+# Details:
+#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-colo-do-checkpoint" }
@@ -1724,6 +1726,8 @@
 #
 # Pause a migration.  Currently it only supports postcopy.
 #
+# Details:
+#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-pause" }
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 06/10] qapi: detect potentially semantically ambiguous intro paragraphs
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (4 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 05/10] qapi: add "Details:" markers where potentially needed John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 07/10] qapi: re-order QAPI doc block sections John Snow
                   ` (4 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

Cajole the QAPI Doc parser into yelping if a QAPI Doc Block contains two
or more paragraphs of plaintext and has no instances of Members, Errors,
Returns, or Features that would naturally delineate an introduction from
additional details such as notes, examples, and additional details.

Such locations do not currently *guarantee* a difference to rendered
manual output, but later changes to the documentation generator that may
include additional auto-generated sections, and complexities from the
inliner, may increase the odds that these locations are suspect and will
need to be explicitly delineated.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review notes: no changes to *.ir files. --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py                     | 49 ++++++++++++++++++++++
 tests/qapi-schema/doc-missing-details.err  |  0
 tests/qapi-schema/doc-missing-details.json |  3 ++
 tests/qapi-schema/doc-missing-details.out  | 11 +++++
 tests/qapi-schema/meson.build              |  1 +
 5 files changed, 64 insertions(+)
 create mode 100644 tests/qapi-schema/doc-missing-details.err
 create mode 100644 tests/qapi-schema/doc-missing-details.json
 create mode 100644 tests/qapi-schema/doc-missing-details.out

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 1d52d80e672..ade26d124df 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -32,6 +32,8 @@
 from .source import QAPISourceInfo
 
 
+# pylint: disable=too-many-lines
+
 if TYPE_CHECKING:
     # pylint: disable=cyclic-import
     # TODO: Remove cycle. [schema -> expr -> parser -> schema]
@@ -962,3 +964,50 @@ def check_args_section(
 
         check_args_section(self.args, 'member')
         check_args_section(self.features, 'feature')
+
+        # Ignore free-form documentation sections
+        if self.symbol is None:
+            return
+
+        n_intro_para = 0
+        has_intro = False
+        has_other = False
+
+        for section in self.all_sections:
+            # Ignore Since: and TODO: sections
+            if section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
+                continue
+
+            # Ignore empty plaintext sections
+            if section.kind in (QAPIDoc.Kind.INTRO, QAPIDoc.Kind.DETAILS):
+                if not section.text:
+                    continue
+
+            if section.kind == QAPIDoc.Kind.INTRO:
+                has_intro = True
+                n_intro_para = len(section.text.split("\n\n"))
+            else:
+                if section.kind == QAPIDoc.Kind.MEMBER and not section.text:
+                    pass
+                elif section.kind == QAPIDoc.Kind.RETURNS and not section.text:
+                    pass
+                else:
+                    # This section is something other than an Intro section;
+                    # but we explicitly exclude stub entries for members
+                    # (undocumented fields with no text) from consideration
+                    # because they were auto-generated; they are not useful
+                    # for identifying the case "There are multiple intro
+                    # paragraphs and no other explicit source sections."
+                    has_other = True
+
+        # If an intro section is only a single paragraph, we are
+        # confident it is well and truly just an introduction. If we
+        # have a single, multi-paragraph intro section and *no other*
+        # explicit sections in source code, it is potentially a semantic
+        # goof-em-up where the intro and details sections have bled
+        # together. Warn about this case.
+        if has_intro and n_intro_para > 1 and not has_other:
+            print(
+                f"Warning: paragraphs for {self.symbol} are ambiguous "
+                "and could be either intro or details paragraphs."
+            )
diff --git a/tests/qapi-schema/doc-missing-details.err b/tests/qapi-schema/doc-missing-details.err
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/qapi-schema/doc-missing-details.json b/tests/qapi-schema/doc-missing-details.json
new file mode 100644
index 00000000000..02c14ee768d
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-details.json
@@ -0,0 +1,3 @@
+# TODO / FIXME
+# This test intentionally triggers the parser warning
+# suggesting we use the Details: marker.
diff --git a/tests/qapi-schema/doc-missing-details.out b/tests/qapi-schema/doc-missing-details.out
new file mode 100644
index 00000000000..52ca7cb8727
--- /dev/null
+++ b/tests/qapi-schema/doc-missing-details.out
@@ -0,0 +1,11 @@
+module ./builtin
+object q_empty
+enum QType
+    member none
+    member qnull
+    member qnum
+    member qstring
+    member qdict
+    member qlist
+    member qbool
+module doc-missing-details.json
diff --git a/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build
index c233d77ab78..01da8534f2e 100644
--- a/tests/qapi-schema/meson.build
+++ b/tests/qapi-schema/meson.build
@@ -85,6 +85,7 @@ schemas = [
   'doc-long-line.json',
   'doc-misplaced-details.json',
   'doc-missing-colon.json',
+  'doc-missing-details.json',
   'doc-missing-expr.json',
   'doc-missing-space.json',
   'doc-missing.json',
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 07/10] qapi: re-order QAPI doc block sections
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (5 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 06/10] qapi: detect potentially semantically ambiguous intro paragraphs John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 08/10] qapi: enforce doc block section ordering John Snow
                   ` (3 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

A forthcoming patch will enforce a rigid source order of QAPI Doc block
sections:

(1) Intro
(2) Members
(3) Returns
(4) Errors
(5) Features
(6) Details
(7) Since

This patch specifically ensures the source ordering of items #1-6, with
the positioning of "Since:" left for a subsequent patch, as it is a much
larger commit.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Many changes to the *.ir files, generally :feat: lines moving
lower. All changes are just relative re-orderings of the field list
section of the output, and all of these changes are intentional. --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 qapi/accelerator.json           |  4 +--
 qapi/block-core.json            | 44 +++++++++++++-------------
 qapi/block-export.json          | 20 ++++++------
 qapi/block.json                 |  8 ++---
 qapi/machine-s390x.json         |  8 ++---
 qapi/machine.json               | 56 ++++++++++++++++-----------------
 qapi/misc.json                  |  4 +--
 qapi/qom.json                   |  6 ++--
 qapi/virtio.json                | 32 +++++++++----------
 tests/qapi-schema/doc-good.json |  8 ++---
 tests/qapi-schema/doc-good.txt  | 10 +++---
 11 files changed, 100 insertions(+), 100 deletions(-)

diff --git a/qapi/accelerator.json b/qapi/accelerator.json
index 0cf5e0f9d94..d333a772384 100644
--- a/qapi/accelerator.json
+++ b/qapi/accelerator.json
@@ -43,12 +43,12 @@
 #
 # Query accelerator statistics
 #
+# Returns: accelerator statistics
+#
 # Features:
 #
 # @unstable: This command is meant for debugging.
 #
-# Returns: accelerator statistics
-#
 # Since: 10.1
 ##
 { 'command': 'x-accel-stats',
diff --git a/qapi/block-core.json b/qapi/block-core.json
index abdcddb0a09..400ceda9e87 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -1955,14 +1955,14 @@
 #     `job-dismiss`.  When true, this job will automatically disappear
 #     without user intervention.  Defaults to true.  (Since 3.1)
 #
+# Errors:
+#     - If @device does not exist, DeviceNotFound
+#
 # Features:
 #
 # @deprecated: Members @base and @top are deprecated.  Use @base-node
 #     and @top-node instead.
 #
-# Errors:
-#     - If @device does not exist, DeviceNotFound
-#
 # Since: 1.3
 #
 # .. qmp-example::
@@ -1993,14 +1993,14 @@
 # 'backup'.  The operation can be stopped before it has completed
 # using the `job-cancel` or `block-job-cancel` command.
 #
+# Errors:
+#     - If @device is not a valid block device, GenericError
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `blockdev-backup`
 #     instead.
 #
-# Errors:
-#     - If @device is not a valid block device, GenericError
-#
 # Since: 1.6
 #
 # .. qmp-example::
@@ -2558,14 +2558,14 @@
 #
 # Get bitmap SHA256.
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Errors:
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found or if hashing has failed, GenericError
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 2.10
 ##
 { 'command': 'x-debug-block-dirty-bitmap-sha256',
@@ -3068,15 +3068,15 @@
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
 #
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `job-pause`
 #     instead.
 #
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-pause', 'data': { 'device': 'str' },
@@ -3097,15 +3097,15 @@
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
 #
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `job-resume`
 #     instead.
 #
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-resume', 'data': { 'device': 'str' },
@@ -3137,15 +3137,15 @@
 #     the name of the parameter), but since QEMU 2.7 it can have other
 #     values.
 #
+# Errors:
+#     - If no background operation is active on this device,
+#       DeviceNotActive
+#
 # Features:
 #
 # @deprecated: This command is deprecated.  Use `job-complete`
 #     instead.
 #
-# Errors:
-#     - If no background operation is active on this device,
-#       DeviceNotActive
-#
 # Since: 1.3
 ##
 { 'command': 'block-job-complete', 'data': { 'device': 'str' },
diff --git a/qapi/block-export.json b/qapi/block-export.json
index dd724acf1cb..d44b509968e 100644
--- a/qapi/block-export.json
+++ b/qapi/block-export.json
@@ -249,15 +249,15 @@
 # The export name will be used as the id for the resulting block
 # export.
 #
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `block-export-add`
-#     instead.
-#
 # Errors:
 #     - if the server is not running
 #     - if an export with the same name already exists
 #
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `block-export-add`
+#     instead.
+#
 # Since: 1.3
 ##
 { 'command': 'nbd-server-add',
@@ -297,16 +297,16 @@
 # @mode: Mode of command operation.  See `BlockExportRemoveMode`
 #     description.  Default is 'safe'.
 #
-# Features:
-#
-# @deprecated: This command is deprecated.  Use `block-export-del`
-#     instead.
-#
 # Errors:
 #     - if the server is not running
 #     - if export is not found
 #     - if mode is 'safe' and there are existing connections
 #
+# Features:
+#
+# @deprecated: This command is deprecated.  Use `block-export-del`
+#     instead.
+#
 # Since: 2.12
 ##
 { 'command': 'nbd-server-remove',
diff --git a/qapi/block.json b/qapi/block.json
index 46955bbb3e3..54bc0056318 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -109,13 +109,13 @@
 # @force: If true, eject regardless of whether the drive is locked.
 #     If not specified, the default value is false.
 #
-# Features:
-#
-# @deprecated: Member @device is deprecated.  Use @id instead.
-#
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
+# Features:
+#
+# @deprecated: Member @device is deprecated.  Use @id instead.
+#
 # .. note:: Ejecting a device with no media results in success.
 #
 # Since: 0.14
diff --git a/qapi/machine-s390x.json b/qapi/machine-s390x.json
index ea430e1b889..e67f180a272 100644
--- a/qapi/machine-s390x.json
+++ b/qapi/machine-s390x.json
@@ -108,12 +108,12 @@
 ##
 # @query-s390x-cpu-polarization:
 #
-# Features:
-#
-# @unstable: This command is experimental.
-#
 # Returns: the machine's CPU polarization
 #
+# Features:
+#
+# @unstable: This command is experimental.
+#
 # Since: 8.2
 ##
 { 'command': 'query-s390x-cpu-polarization', 'returns': 'CpuPolarizationInfo',
diff --git a/qapi/machine.json b/qapi/machine.json
index 012f61e2a7e..0a4b758b2d5 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -1676,12 +1676,12 @@
 #
 # Query interrupt statistics
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: interrupt statistics
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-irq',
@@ -1693,12 +1693,12 @@
 #
 # Query TCG compiler statistics
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: TCG compiler statistics
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-jit',
@@ -1711,12 +1711,12 @@
 #
 # Query NUMA topology information
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: topology information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-numa',
@@ -1728,12 +1728,12 @@
 #
 # Query system ramblock information
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: system ramblock information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-ramblock',
@@ -1745,12 +1745,12 @@
 #
 # Query information on the registered ROMS
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: registered ROMs
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-roms',
@@ -1762,12 +1762,12 @@
 #
 # Query information on the USB devices
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: USB device information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 6.2
 ##
 { 'command': 'x-query-usb',
@@ -1831,12 +1831,12 @@
 #
 # Query information on interrupt controller devices
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Interrupt controller devices information
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 9.1
 ##
 { 'command': 'x-query-interrupt-controllers',
diff --git a/qapi/misc.json b/qapi/misc.json
index 28c641fe2fe..05866837f09 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -209,14 +209,14 @@
 #
 # @cpu-index: The CPU to use for commands that require an implicit CPU
 #
+# Returns: the output of the command as a string
+#
 # Features:
 #
 # @savevm-monitor-nodes: If present, HMP command savevm only snapshots
 #     monitor-owned nodes if they have no parents.  This allows the
 #     use of 'savevm' with -blockdev.  (since 4.2)
 #
-# Returns: the output of the command as a string
-#
 # Since: 0.14
 #
 # .. note:: This command only exists as a stop-gap.  Its use is highly
diff --git a/qapi/qom.json b/qapi/qom.json
index 568b7d4b997..fd88be07e13 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -161,12 +161,12 @@
 # @paths: The absolute or partial path for each object, as described
 #     in `qom-get`.
 #
-# Errors:
-#     - If any path is not valid or is ambiguous
-#
 # Returns: A list where each element is the result for the
 #     corresponding element of @paths.
 #
+# Errors:
+#     - If any path is not valid or is ambiguous
+#
 # Since 10.1
 ##
 { 'command': 'qom-list-get',
diff --git a/qapi/virtio.json b/qapi/virtio.json
index 09dd0e6d05a..447fc182625 100644
--- a/qapi/virtio.json
+++ b/qapi/virtio.json
@@ -28,12 +28,12 @@
 #
 # Return a list of all realized VirtIODevices
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: List of gathered VirtIODevices
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 7.2
 #
 # .. qmp-example::
@@ -197,12 +197,12 @@
 #
 # @path: Canonical QOM path of the VirtIODevice
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Status of the virtio device
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 7.2
 #
 # .. qmp-example::
@@ -566,12 +566,12 @@
 #
 # @queue: VirtQueue index to examine
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Status of the queue
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # .. note:: last_avail_idx will not be displayed in the case where the
 #    selected VirtIODevice has a running vhost device and the
 #    VirtIODevice VirtQueue index (queue) does not exist for the
@@ -701,12 +701,12 @@
 #
 # @queue: vhost_virtqueue index to examine
 #
-# Features:
-#
-# @unstable: This command is meant for debugging.
-#
 # Returns: Status of the vhost_virtqueue
 #
+# Features:
+#
+# @unstable: This command is meant for debugging.
+#
 # Since: 7.2
 #
 # .. qmp-example::
diff --git a/tests/qapi-schema/doc-good.json b/tests/qapi-schema/doc-good.json
index 9103fed472e..5b567824280 100644
--- a/tests/qapi-schema/doc-good.json
+++ b/tests/qapi-schema/doc-good.json
@@ -161,14 +161,14 @@
 # @arg2: description starts on the same line
 #     remainder indented differently
 #
-# Features:
-# @cmd-feat1: a feature
-# @cmd-feat2: another feature
-#
 # Returns: @Object
 #
 # Errors: some
 #
+# Features:
+# @cmd-feat1: a feature
+# @cmd-feat2: another feature
+#
 # .. note:: @arg3 is undocumented
 #
 # TODO: frobnicate
diff --git a/tests/qapi-schema/doc-good.txt b/tests/qapi-schema/doc-good.txt
index ded699dd596..226c3d27a36 100644
--- a/tests/qapi-schema/doc-good.txt
+++ b/tests/qapi-schema/doc-good.txt
@@ -115,17 +115,17 @@ Command cmd (Since: 2.10)
 
       * **arg3** ("boolean") -- Not documented
 
-   Features:
-      * **cmd-feat1** -- a feature
-
-      * **cmd-feat2** -- another feature
-
    Return:
       "Object" -- "Object"
 
    Errors:
       some
 
+   Features:
+      * **cmd-feat1** -- a feature
+
+      * **cmd-feat2** -- another feature
+
    Note:
 
      "arg3" is undocumented
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 08/10] qapi: enforce doc block section ordering
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (6 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 07/10] qapi: re-order QAPI doc block sections John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 09/10] qapi: re-order 'since' sections to always be last John Snow
                   ` (2 subsequent siblings)
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

Ugly hack, gets the job done. Likely many simplifications can be made as
a result, but I didn't make any of them. There are some inconsistencies
with human-readable vs ENUM_NAMES in error messages in this patch, but
it appears to work anyway.

Consider this patch more of a rough idea and not anything approximating
the kind of code you'd want to see from an Enterprise Linux Senior
Software Engineer.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review note: No changes to *.ir files. --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 55 ++++++++++++++++++++++++++----------------
 1 file changed, 34 insertions(+), 21 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index ade26d124df..6e91cd19e58 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -547,22 +547,28 @@ def get_doc(self) -> 'QAPIDoc':
             self.accept(False)
             line = self.get_doc_line()
             have_tagged = False
-            no_more_tags = False
+            last_ordered_section = QAPIDoc.Kind.INTRO
 
             def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
+                nonlocal last_ordered_section
                 if isinstance(this, str):
                     this = QAPIDoc.Kind.from_string(this)
+
                 if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
                     return
 
-                if no_more_tags:
+                if this.value < last_ordered_section.value:
                     raise QAPIParseError(
                         self,
-                        f"'{this}' section cannot appear after plain "
-                        "paragraphs that follow other tagged sections\n"
-                        "Move this section up above the plain paragraph(s)."
+                        f"'{this}' section cannot appear after "
+                        f"'{last_ordered_section}' section, please re-order "
+                        "the sections and adjust phrasing as necessary to "
+                        "ensure consistent documentation flow between the "
+                        "source code and the rendered HTML manual"
                     )
 
+                last_ordered_section = this
+
             while line is not None:
                 # Blank lines
                 while line == '':
@@ -593,7 +599,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                             self, 'feature descriptions expected')
                     have_tagged = True
                 elif line == 'Details:':
-                    _tag_check("Details")
+                    _tag_check(QAPIDoc.Kind.DETAILS)
                     self.accept(False)
                     line = self.get_doc_line()
                     while line == '':
@@ -602,6 +608,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                     have_tagged = True
                 elif match := self._match_at_name_colon(line):
                     # description
+                    _tag_check(QAPIDoc.Kind.MEMBER)
                     if have_tagged:
                         raise QAPIParseError(
                             self,
@@ -658,14 +665,16 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                     line = self.get_doc_indented(doc)
                     have_tagged = True
                 else:
-                    # plain paragraph
-                    if have_tagged:
-                        no_more_tags = True
-
-                    # Paragraphs before tagged sections are "intro" paragraphs.
-                    # Any appearing after are "detail" paragraphs.
-                    intro = not have_tagged
-                    doc.ensure_untagged_section(self.info, intro)
+                    # Paragraphs appearing before any other sections are
+                    # "intro" paragraphs. Any appearing after are
+                    # "details" paragraphs.
+                    this_section = (
+                        QAPIDoc.Kind.DETAILS
+                        if have_tagged
+                        else QAPIDoc.Kind.INTRO
+                    )
+                    _tag_check(this_section)
+                    doc.ensure_untagged_section(self.info, this_section)
                     doc.append_line(line)
                     line = self.get_doc_paragraph(doc)
         else:
@@ -707,14 +716,17 @@ class QAPIDoc:
     """
 
     class Kind(enum.Enum):
+        # The order here is the order in which sections must appear in
+        # source code; with the exception of 'TODO' which may appear
+        # anywhere but is treated as a comment.
         INTRO = 0
         MEMBER = 1
-        FEATURE = 2
-        RETURNS = 3
-        ERRORS = 4
-        SINCE = 5
+        RETURNS = 2
+        ERRORS = 3
+        FEATURE = 4
+        DETAILS = 5
         TODO = 6
-        DETAILS = 7
+        SINCE = 7
 
         @staticmethod
         def from_string(kind: str) -> 'QAPIDoc.Kind':
@@ -793,9 +805,10 @@ def end(self) -> None:
     def ensure_untagged_section(
         self,
         info: QAPISourceInfo,
-        intro: bool = True,
+        kind: Optional['QAPIDoc.Kind'] = None,
     ) -> None:
-        kind = QAPIDoc.Kind.INTRO if intro else QAPIDoc.Kind.DETAILS
+        if kind is None:
+            kind = QAPIDoc.Kind.INTRO
 
         if self.all_sections and self.all_sections[-1].kind == kind:
             # extend current section
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 09/10] qapi: re-order 'since' sections to always be last
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (7 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 08/10] qapi: enforce doc block section ordering John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-08  4:55 ` [PATCH v2 10/10] qapi: enforce strict positioning for "Since:" section John Snow
  2026-04-15  9:43 ` [PATCH v2 00/10] qapi: enforce section ordering Markus Armbruster
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

Note that with "Since" being moved to the last position, we open up a
good many new cases of ambiguos intro-vs-details text which must be
corrected with "Details:" tags.

These markers are added to this patch in two cases:

(1) Where failing to add it would immediately (i.e. without the future
    inliner feature) cause a change to the rendered output
(2) Where failing to add it causes new warning message yelps.

These cases are not presently delineated.

Signed-off-by: John Snow <jsnow@redhat.com>

---

[Review notes: This patch produces no changes to the *.ir files! --js]

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/interop/firmware.json   |   4 +-
 docs/interop/vhost-user.json |   3 +-
 qapi/accelerator.json        |   8 ++-
 qapi/acpi.json               |   8 ++-
 qapi/block-core.json         | 136 ++++++++++++++++++-----------------
 qapi/block.json              |  40 +++++------
 qapi/char.json               |  36 +++++-----
 qapi/control.json            |  14 ++--
 qapi/dump.json               |  16 ++---
 qapi/machine-s390x.json      |   8 +--
 qapi/machine.json            |  84 +++++++++++++---------
 qapi/migration.json          |  94 +++++++++++++-----------
 qapi/misc-arm.json           |   4 +-
 qapi/misc-i386.json          |  38 +++++-----
 qapi/misc.json               |  64 +++++++++--------
 qapi/net.json                |  40 +++++------
 qapi/pci.json                |   4 +-
 qapi/qdev.json               |  12 ++--
 qapi/qom.json                |  20 +++---
 qapi/replay.json             |  16 +++--
 qapi/rocker.json             |  16 ++---
 qapi/run-state.json          |  66 ++++++++++-------
 qapi/tpm.json                |  12 +++-
 qapi/trace.json              |   8 +--
 qapi/transaction.json        |   4 +-
 qapi/ui.json                 |  76 +++++++++++---------
 qapi/vfio.json               |   4 +-
 qapi/virtio.json             |  20 +++---
 28 files changed, 472 insertions(+), 383 deletions(-)

diff --git a/docs/interop/firmware.json b/docs/interop/firmware.json
index 421bee0e5ed..bdfb6ccb717 100644
--- a/docs/interop/firmware.json
+++ b/docs/interop/firmware.json
@@ -551,8 +551,6 @@
 #     debugging purposes only, and management software shall
 #     explicitly ignore it.
 #
-# Since: 3.0
-#
 # .. qmp-example::
 #
 #     {
@@ -752,6 +750,8 @@
 #             "-D DEBUG_PRINT_ERROR_LEVEL=0x80000000"
 #         ]
 #     }
+#
+# Since: 3.0
 ##
 { 'struct' : 'Firmware',
   'data'   : { 'description'     : 'str',
diff --git a/docs/interop/vhost-user.json b/docs/interop/vhost-user.json
index 29c84e86e55..fbcf409838d 100644
--- a/docs/interop/vhost-user.json
+++ b/docs/interop/vhost-user.json
@@ -264,8 +264,6 @@
 #     development and debugging purposes only, and management software
 #     shall explicitly ignore it.
 #
-# Since: 4.0
-#
 # .. qmp-example:
 #
 # {
@@ -278,6 +276,7 @@
 #   ]
 # }
 #
+# Since: 4.0
 ##
 {
   'struct' : 'VhostUserBackend',
diff --git a/qapi/accelerator.json b/qapi/accelerator.json
index d333a772384..dfa1288bf3c 100644
--- a/qapi/accelerator.json
+++ b/qapi/accelerator.json
@@ -29,12 +29,14 @@
 #
 # Return information about KVM acceleration
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "query-kvm" }
 #     <- { "return": { "enabled": true, "present": true } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-kvm', 'returns': 'KvmInfo' }
 
@@ -101,11 +103,11 @@
 #
 # Returns: @AcceleratorInfo
 #
-# Since: 10.2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-accelerators" }
 #     <- { "return": { "enabled": "mshv", "present": ["kvm", "mshv", "qtest", "tcg"] } }
+#
+# Since: 10.2.0
 ##
 { 'command': 'query-accelerators', 'returns': 'AcceleratorInfo' }
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 906b3687a55..8dd87fd8f22 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -111,7 +111,7 @@
 # Return a list of `ACPIOSTInfo` for devices that support status
 # reporting via ACPI _OST method.
 #
-# Since: 2.1
+# Details:
 #
 # .. qmp-example::
 #
@@ -121,6 +121,8 @@
 #                      { "slot": "2", "slot-type": "DIMM", "source": 0, "status": 0},
 #                      { "slot": "3", "slot-type": "DIMM", "source": 0, "status": 0}
 #        ]}
+#
+# Since: 2.1
 ##
 { 'command': 'query-acpi-ospm-status', 'returns': ['ACPIOSTInfo'] }
 
@@ -131,14 +133,14 @@
 #
 # @info: OSPM Status Indication
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "ACPI_DEVICE_OST",
 #          "data": { "info": { "device": "d1", "slot": "0",
 #                              "slot-type": "DIMM", "source": 1, "status": 0 } },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 2.1
 ##
 { 'event': 'ACPI_DEVICE_OST',
      'data': { 'info': 'ACPIOSTInfo' } }
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 400ceda9e87..d1f505978c9 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -862,8 +862,6 @@
 # Returns: a list describing each virtual block device.  Filter nodes
 #     that were created implicitly are skipped over.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-block" }
@@ -947,6 +945,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-block', 'returns': ['BlockInfo'],
   'data': { '*flat': 'bool' },
@@ -1267,8 +1267,6 @@
 #
 # Returns: A list of statistics for each virtual block device.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-blockstats" }
@@ -1370,6 +1368,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-blockstats',
   'data': { '*query-nodes': 'bool' },
@@ -1560,13 +1560,13 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block_resize",
 #          "arguments": { "device": "scratch", "size": 1073741824 } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'block_resize',
   'data': { '*device': 'str',
@@ -1788,8 +1788,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-snapshot-sync",
@@ -1798,6 +1796,8 @@
 #                         "/some/place/my-image",
 #                         "format": "qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'blockdev-snapshot-sync',
   'data': 'BlockdevSnapshotSync',
@@ -1820,8 +1820,6 @@
 #     backing file of a destination of a `blockdev-mirror`.
 #     (since 5.0)
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -1837,6 +1835,8 @@
 #          "arguments": { "node": "ide-hd0",
 #                         "overlay": "node1534" } }
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-snapshot',
   'data': 'BlockdevSnapshot',
@@ -1963,14 +1963,14 @@
 # @deprecated: Members @base and @top are deprecated.  Use @base-node
 #     and @top-node instead.
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-commit",
 #          "arguments": { "device": "virtio0",
 #                         "top": "/tmp/snap1.qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'block-commit',
   'data': { '*job-id': 'str', 'device': 'str', '*base-node': 'str',
@@ -2001,8 +2001,6 @@
 # @deprecated: This command is deprecated.  Use `blockdev-backup`
 #     instead.
 #
-# Since: 1.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "drive-backup",
@@ -2010,6 +2008,8 @@
 #                         "sync": "full",
 #                         "target": "backup.img" } }
 #     <- { "return": {} }
+#
+# Since: 1.6
 ##
 { 'command': 'drive-backup', 'boxed': true,
   'data': 'DriveBackup', 'features': ['deprecated'],
@@ -2027,8 +2027,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 2.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-backup",
@@ -2036,6 +2034,8 @@
 #                         "sync": "full",
 #                         "target": "tgt-id" } }
 #     <- { "return": {} }
+#
+# Since: 2.3
 ##
 { 'command': 'blockdev-backup', 'boxed': true,
   'data': 'BlockdevBackup',
@@ -2049,8 +2049,6 @@
 # @flat: Omit the nested data about backing image ("backing-image"
 #     key) if true.  Default is false (Since 5.0)
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-named-block-nodes" }
@@ -2099,6 +2097,8 @@
 #                               "virtual-size":2048000
 #                           }
 #                        } } ] }
+#
+# Since: 2.0
 ##
 { 'command': 'query-named-block-nodes',
   'returns': [ 'BlockDeviceInfo' ],
@@ -2230,8 +2230,6 @@
 # Errors:
 #     - If @device is not a valid block device, GenericError
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "drive-mirror",
@@ -2240,6 +2238,8 @@
 #                         "sync": "full",
 #                         "format": "qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'drive-mirror', 'boxed': true,
   'data': 'DriveMirror',
@@ -2407,13 +2407,13 @@
 #     - If @node is not a valid block device or node, DeviceNotFound
 #     - If @name is already taken, GenericError
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-add",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'block-dirty-bitmap-add',
   'data': 'BlockDirtyBitmapAdd',
@@ -2431,13 +2431,13 @@
 #     - If @name is not found, GenericError
 #     - if @name is frozen by an operation, GenericError
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-remove",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'block-dirty-bitmap-remove',
   'data': 'BlockDirtyBitmap',
@@ -2454,13 +2454,13 @@
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found, GenericError
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-clear",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'block-dirty-bitmap-clear',
   'data': 'BlockDirtyBitmap',
@@ -2475,13 +2475,13 @@
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found, GenericError
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-enable",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-dirty-bitmap-enable',
   'data': 'BlockDirtyBitmap',
@@ -2496,13 +2496,13 @@
 #     - If @node is not a valid block device, DeviceNotFound
 #     - If @name is not found, GenericError
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-disable",
 #          "arguments": { "node": "drive0", "name": "bitmap0" } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-dirty-bitmap-disable',
   'data': 'BlockDirtyBitmap',
@@ -2528,14 +2528,14 @@
 #     - If any of the bitmaps have different sizes or granularities,
 #       GenericError
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-dirty-bitmap-merge",
 #          "arguments": { "node": "drive0", "target": "bitmap0",
 #                         "bitmaps": ["bitmap1"] } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-dirty-bitmap-merge',
   'data': 'BlockDirtyBitmapMerge',
@@ -2637,8 +2637,6 @@
 #     mirror.  Setting this to true when the destination is not
 #     actually all zero can corrupt the destination.  (Since 10.1)
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-mirror",
@@ -2646,6 +2644,8 @@
 #                         "target": "target0",
 #                         "sync": "full" } }
 #     <- { "return": {} }
+#
+# Since: 2.6
 ##
 { 'command': 'blockdev-mirror',
   'data': { '*job-id': 'str', 'device': 'str', 'target': 'str',
@@ -2962,14 +2962,14 @@
 # Errors:
 #     - If @device does not exist, DeviceNotFound.
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-stream",
 #          "arguments": { "device": "virtio0",
 #                         "base": "/tmp/master.qcow2" } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'block-stream',
   'data': { '*job-id': 'str', 'device': 'str', '*base': 'str',
@@ -4958,7 +4958,7 @@
 #
 # Creates a new block device.
 #
-# Since: 2.9
+# Details:
 #
 # .. qmp-example::
 #
@@ -4999,6 +4999,8 @@
 #          }
 #
 #     <- { "return": {} }
+#
+# Since: 2.9
 ##
 { 'command': 'blockdev-add', 'data': 'BlockdevOptions', 'boxed': true,
   'allow-preconfig': true }
@@ -5062,8 +5064,6 @@
 #
 # @node-name: Name of the graph node to delete.
 #
-# Since: 2.9
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -5082,6 +5082,8 @@
 #          "arguments": { "node-name": "node0" }
 #        }
 #     <- { "return": {} }
+#
+# Since: 2.9
 ##
 { 'command': 'blockdev-del', 'data': { 'node-name': 'str' },
   'allow-preconfig': true }
@@ -5102,8 +5104,6 @@
 # @active: true if the nodes should be active when the command returns
 #     success, false if they should be inactive.
 #
-# Since: 10.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-set-active",
@@ -5113,6 +5113,8 @@
 #          }
 #        }
 #     <- { "return": {} }
+#
+# Since: 10.0
 ##
 { 'command': 'blockdev-set-active',
   'data': { '*node-name': 'str', 'active': 'bool' },
@@ -5796,8 +5798,6 @@
 #
 # .. note:: This event is rate-limited, except if action is "stop".
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_IO_ERROR",
@@ -5808,6 +5808,8 @@
 #                    "action": "stop",
 #                    "reason": "No space left on device" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 0.13
 ##
 { 'event': 'BLOCK_IO_ERROR',
   'data': { 'qom-path': 'str', 'device': 'str', '*node-name': 'str',
@@ -5837,8 +5839,6 @@
 #     other than that streaming has failed and clients should not try
 #     to interpret the error string
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_COMPLETED",
@@ -5846,6 +5846,8 @@
 #                    "len": 10737418240, "offset": 10737418240,
 #                    "speed": 0 },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 1.1
 ##
 { 'event': 'BLOCK_JOB_COMPLETED',
   'data': { 'type'  : 'JobType',
@@ -5872,8 +5874,6 @@
 #
 # @speed: rate limit, bytes per second
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_CANCELLED",
@@ -5881,6 +5881,8 @@
 #                    "len": 10737418240, "offset": 134217728,
 #                    "speed": 0 },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 1.1
 ##
 { 'event': 'BLOCK_JOB_CANCELLED',
   'data': { 'type'  : 'JobType',
@@ -5901,8 +5903,6 @@
 #
 # @action: action that has been taken
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_ERROR",
@@ -5910,6 +5910,8 @@
 #                    "operation": "write",
 #                    "action": "stop" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.3
 ##
 { 'event': 'BLOCK_JOB_ERROR',
   'data': { 'device'   : 'str',
@@ -5936,14 +5938,14 @@
 # .. note:: The "ready to complete" status is always reset by a
 #    `BLOCK_JOB_ERROR` event.
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_READY",
 #          "data": { "device": "drive0", "type": "mirror", "speed": 0,
 #                    "len": 2097152, "offset": 2097152 },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.3
 ##
 { 'event': 'BLOCK_JOB_READY',
   'data': { 'type'  : 'JobType',
@@ -5964,13 +5966,13 @@
 #
 # @id: The job identifier.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "BLOCK_JOB_PENDING",
 #          "data": { "type": "mirror", "id": "backup_1" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 2.12
 ##
 { 'event': 'BLOCK_JOB_PENDING',
   'data': { 'type'  : 'JobType',
@@ -6038,14 +6040,14 @@
 # @write-threshold: configured threshold for the block device, bytes.
 #     Use 0 to disable the threshold.
 #
-# Since: 2.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block-set-write-threshold",
 #          "arguments": { "node-name": "mydev",
 #                         "write-threshold": 17179869184 } }
 #     <- { "return": {} }
+#
+# Since: 2.3
 ##
 { 'command': 'block-set-write-threshold',
   'data': { 'node-name': 'str', 'write-threshold': 'uint64' },
@@ -6079,8 +6081,6 @@
 #     'children' list of `BlockdevOptionsQuorum`, as returned by
 #     .bdrv_refresh_filename().
 #
-# Since: 2.7
-#
 # .. qmp-example::
 #    :title: Add a new node to a quorum
 #
@@ -6103,6 +6103,8 @@
 #          "arguments": { "parent": "disk1",
 #                         "child": "children.1" } }
 #     <- { "return": {} }
+#
+# Since: 2.7
 ##
 { 'command': 'x-blockdev-change',
   'data' : { 'parent': 'str',
@@ -6131,8 +6133,6 @@
 # @unstable: This command is experimental and intended for test cases
 #     that need control over IOThreads only.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #    :title: Move a node into an IOThread
 #
@@ -6148,6 +6148,8 @@
 #          "arguments": { "node-name": "disk1",
 #                         "iothread": null } }
 #     <- { "return": {} }
+#
+# Since: 2.12
 ##
 { 'command': 'x-blockdev-set-iothread',
   'data' : { 'node-name': 'str',
@@ -6185,13 +6187,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     <- { "event": "QUORUM_FAILURE",
 #          "data": { "reference": "usr1", "sector-num": 345435, "sectors-count": 5 },
 #          "timestamp": { "seconds": 1344522075, "microseconds": 745528 } }
+#
+# Since: 2.0
 ##
 { 'event': 'QUORUM_FAILURE',
   'data': { 'reference': 'str', 'sector-num': 'int', 'sectors-count': 'int' } }
@@ -6216,8 +6218,6 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #    :title: Read operation
 #
@@ -6233,6 +6233,8 @@
 #          "data": { "node-name": "node0", "sector-num": 0, "sectors-count": 2097120,
 #                    "type": "flush", "error": "Broken pipe" },
 #          "timestamp": { "seconds": 1456406829, "microseconds": 291763 } }
+#
+# Since: 2.0
 ##
 { 'event': 'QUORUM_REPORT_BAD',
   'data': { 'type': 'QuorumOpType', '*error': 'str', 'node-name': 'str',
@@ -6269,8 +6271,6 @@
 # .. note:: Only some image formats such as qcow2 and rbd support
 #    internal snapshots.
 #
-# Since: 1.7
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-snapshot-internal-sync",
@@ -6278,6 +6278,8 @@
 #                         "name": "snapshot0" }
 #        }
 #     <- { "return": {} }
+#
+# Since: 1.7
 ##
 { 'command': 'blockdev-snapshot-internal-sync',
   'data': 'BlockdevSnapshotInternal',
@@ -6305,8 +6307,6 @@
 #       GenericError
 #     - If @id and @name are both not specified, GenericError
 #
-# Since: 1.7
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-snapshot-delete-internal-sync",
@@ -6324,6 +6324,8 @@
 #                        "icount": 220414
 #          }
 #        }
+#
+# Since: 1.7
 ##
 { 'command': 'blockdev-snapshot-delete-internal-sync',
   'data': { 'device': 'str', '*id': 'str', '*name': 'str'},
diff --git a/qapi/block.json b/qapi/block.json
index 54bc0056318..a29907e0be1 100644
--- a/qapi/block.json
+++ b/qapi/block.json
@@ -118,12 +118,12 @@
 #
 # .. note:: Ejecting a device with no media results in success.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "eject", "arguments": { "id": "ide1-0-1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'eject',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -162,8 +162,6 @@
 #
 # @deprecated: Member @device is deprecated.  Use @id instead.
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-open-tray",
@@ -177,6 +175,8 @@
 #                    "tray-open": true } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-open-tray',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -200,8 +200,6 @@
 #
 # @deprecated: Member @device is deprecated.  Use @id instead.
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-close-tray",
@@ -215,6 +213,8 @@
 #                    "tray-open": false } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-close-tray',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -232,8 +232,6 @@
 #
 # @id: The name or QOM path of the guest device
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-remove-medium",
@@ -258,6 +256,8 @@
 #          "arguments": { "id": "ide0-1-0" } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.12
 ##
 { 'command': 'blockdev-remove-medium',
   'data': { 'id': 'str' } }
@@ -273,8 +273,6 @@
 #
 # @node-name: name of a node in the block driver state graph
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "blockdev-add",
@@ -290,6 +288,8 @@
 #                         "node-name": "node0" } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.12
 ##
 { 'command': 'blockdev-insert-medium',
   'data': { 'id': 'str',
@@ -343,8 +343,6 @@
 #
 # @deprecated: Member @device is deprecated.  Use @id instead.
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #    :title: Change a removable medium
 #
@@ -374,6 +372,8 @@
 #                         "read-only-mode": "read-only" } }
 #
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'blockdev-change-medium',
   'data': { '*device': { 'type': 'str', 'features': [ 'deprecated' ] },
@@ -398,8 +398,6 @@
 # @tray-open: true if the tray has been opened or false if it has been
 #     closed
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "DEVICE_TRAY_MOVED",
@@ -408,6 +406,8 @@
 #                    "tray-open": true
 #          },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.1
 ##
 { 'event': 'DEVICE_TRAY_MOVED',
   'data': { 'device': 'str', 'id': 'str', 'tray-open': 'bool' } }
@@ -422,8 +422,6 @@
 #
 # @connected: true if the PR manager is connected to a backend
 #
-# Since: 3.0
-#
 # .. qmp-example::
 #
 #     <- { "event": "PR_MANAGER_STATUS_CHANGED",
@@ -431,6 +429,8 @@
 #                    "connected": true
 #          },
 #          "timestamp": { "seconds": 1519840375, "microseconds": 450486 } }
+#
+# Since: 3.0
 ##
 { 'event': 'PR_MANAGER_STATUS_CHANGED',
   'data': { 'id': 'str', 'connected': 'bool' } }
@@ -464,8 +464,6 @@
 # Errors:
 #     - If @device is not a valid block device, DeviceNotFound
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "block_set_io_throttle",
@@ -505,6 +503,8 @@
 #                         "bps_max_length": 60,
 #                         "iops_size": 0 } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'block_set_io_throttle', 'boxed': true,
   'data': 'BlockIOThrottle',
@@ -546,8 +546,6 @@
 # Errors:
 #     - if device is not found or any boundary arrays are invalid.
 #
-# Since: 4.0
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -594,6 +592,8 @@
 #     -> { "execute": "block-latency-histogram-set",
 #          "arguments": { "id": "drive0" } }
 #     <- { "return": {} }
+#
+# Since: 4.0
 ##
 { 'command': 'block-latency-histogram-set',
   'data': {'id': 'str',
diff --git a/qapi/char.json b/qapi/char.json
index a4abafa6803..4ff980d356b 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -38,7 +38,7 @@
 #
 # Return information about current character devices.
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
@@ -62,6 +62,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-chardev', 'returns': ['ChardevInfo'],
   'allow-preconfig': true }
@@ -82,7 +84,7 @@
 #
 # Return information about character device backends.
 #
-# Since: 2.0
+# Details:
 #
 # .. qmp-example::
 #
@@ -103,6 +105,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 2.0
 ##
 { 'command': 'query-chardev-backends', 'returns': ['ChardevBackendInfo'] }
 
@@ -137,8 +141,6 @@
 #     - data itself is always Unicode regardless of format, like any
 #       other string.
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "ringbuf-write",
@@ -146,6 +148,8 @@
 #                         "data": "abcdefgh",
 #                         "format": "utf8" } }
 #     <- { "return": {} }
+#
+# Since: 1.4
 ##
 { 'command': 'ringbuf-write',
   'data': { 'device': 'str',
@@ -173,8 +177,6 @@
 #
 # Returns: data read from the device
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "ringbuf-read",
@@ -182,6 +184,8 @@
 #                         "size": 1000,
 #                         "format": "utf8" } }
 #     <- { "return": "abcdefgh" }
+#
+# Since: 1.4
 ##
 { 'command': 'ringbuf-read',
   'data': {'device': 'str', 'size': 'int', '*format': 'DataFormat'},
@@ -761,8 +765,6 @@
 #
 # @backend: backend type and parameters
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute" : "chardev-add",
@@ -784,6 +786,8 @@
 #          "arguments" : { "id" : "baz",
 #                          "backend" : { "type" : "pty", "data" : {} } } }
 #     <- { "return": { "pty" : "/dev/pty/42" } }
+#
+# Since: 1.4
 ##
 { 'command': 'chardev-add',
   'data': { 'id': 'str',
@@ -799,8 +803,6 @@
 #
 # @backend: new backend type and parameters
 #
-# Since: 2.10
-#
 # .. qmp-example::
 #
 #     -> { "execute" : "chardev-change",
@@ -825,6 +827,8 @@
 #                      "server" : true,
 #                      "wait" : false }}}}
 #     <- {"return": {}}
+#
+# Since: 2.10
 ##
 { 'command': 'chardev-change',
   'data': { 'id': 'str',
@@ -838,12 +842,12 @@
 #
 # @id: the chardev's ID, must exist and not be in use
 #
-# Since: 1.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "chardev-remove", "arguments": { "id" : "foo" } }
 #     <- { "return": {} }
+#
+# Since: 1.4
 ##
 { 'command': 'chardev-remove',
   'data': { 'id': 'str' } }
@@ -855,12 +859,12 @@
 #
 # @id: the chardev's ID, must exist
 #
-# Since: 2.10
-#
 # .. qmp-example::
 #
 #     -> { "execute": "chardev-send-break", "arguments": { "id" : "foo" } }
 #     <- { "return": {} }
+#
+# Since: 2.10
 ##
 { 'command': 'chardev-send-break',
   'data': { 'id': 'str' } }
@@ -876,13 +880,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "VSERPORT_CHANGE",
 #          "data": { "id": "channel0", "open": true },
 #          "timestamp": { "seconds": 1401385907, "microseconds": 422329 } }
+#
+# Since: 2.1
 ##
 { 'event': 'VSERPORT_CHANGE',
   'data': { 'id': 'str',
diff --git a/qapi/control.json b/qapi/control.json
index 9a5302193d6..758b176de72 100644
--- a/qapi/control.json
+++ b/qapi/control.json
@@ -97,8 +97,6 @@
 #
 # Returns: An object describing the current version of QEMU.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-version" }
@@ -112,6 +110,8 @@
 #              "package":""
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-version', 'returns': 'VersionInfo',
   'allow-preconfig': true }
@@ -134,8 +134,6 @@
 #
 # Returns: A list of all supported commands
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-commands" }
@@ -152,6 +150,8 @@
 #        }
 #
 # This example has been shortened as the real response is too long.
+#
+# Since: 0.14
 ##
 { 'command': 'query-commands', 'returns': ['CommandInfo'],
   'allow-preconfig': true }
@@ -161,16 +161,18 @@
 #
 # Request graceful QEMU process termination.
 #
+# Details:
+#
 # While every attempt is made to send the QMP response before
 # terminating, this is not guaranteed.  When using this interface, a
 # premature EOF would not be unexpected.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "quit" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'quit',
   'allow-preconfig': true }
diff --git a/qapi/dump.json b/qapi/dump.json
index 726b5208703..bda1c731544 100644
--- a/qapi/dump.json
+++ b/qapi/dump.json
@@ -94,13 +94,13 @@
 #
 # .. note:: All boolean arguments default to false.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "dump-guest-memory",
 #          "arguments": { "paging": false, "protocol": "fd:dump" } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'dump-guest-memory',
   'data': { 'paging': 'bool', 'protocol': 'str', '*detach': 'bool',
@@ -150,13 +150,13 @@
 #
 # Returns: An object showing the dump status.
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-dump" }
 #     <- { "return": { "status": "active", "completed": 1024000,
 #                      "total": 2048000 } }
+#
+# Since: 2.6
 ##
 { 'command': 'query-dump', 'returns': 'DumpQueryResult' }
 
@@ -171,14 +171,14 @@
 #     failed.  Only presents on failure.  The user should not try to
 #     interpret the error string.
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     <- { "event": "DUMP_COMPLETED",
 #          "data": { "result": { "total": 1090650112, "status": "completed",
 #                                "completed": 1090650112 } },
 #          "timestamp": { "seconds": 1648244171, "microseconds": 950316 } }
+#
+# Since: 2.6
 ##
 { 'event': 'DUMP_COMPLETED' ,
   'data': { 'result': 'DumpQueryResult', '*error': 'str' } }
@@ -201,13 +201,13 @@
 #
 # Returns: An object listing available formats for `dump-guest-memory`
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-dump-guest-memory-capability" }
 #     <- { "return": { "formats":
 #                      ["elf", "kdump-zlib", "kdump-lzo", "kdump-snappy"] } }
+#
+# Since: 2.0
 ##
 { 'command': 'query-dump-guest-memory-capability',
   'returns': 'DumpGuestMemoryCapability' }
diff --git a/qapi/machine-s390x.json b/qapi/machine-s390x.json
index e67f180a272..adc986418d7 100644
--- a/qapi/machine-s390x.json
+++ b/qapi/machine-s390x.json
@@ -79,13 +79,13 @@
 #
 # @unstable: This event is experimental.
 #
-# Since: 8.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "CPU_POLARIZATION_CHANGE",
 #          "data": { "polarization": "horizontal" },
 #          "timestamp": { "seconds": 1401385907, "microseconds": 422329 } }
+#
+# Since: 8.2
 ##
 { 'event': 'CPU_POLARIZATION_CHANGE',
   'data': { 'polarization': 'S390CpuPolarization' },
@@ -130,12 +130,12 @@
 #
 # @unstable: This event is experimental.
 #
-# Since: 10.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "SCLP_CPI_INFO_AVAILABLE",
 #          "timestamp": { "seconds": 1401385907, "microseconds": 422329 } }
+#
+# Since: 10.2
 ##
 { 'event': 'SCLP_CPI_INFO_AVAILABLE',
   'features': [ 'unstable' ]
diff --git a/qapi/machine.json b/qapi/machine.json
index 0a4b758b2d5..2cfa4b9b046 100644
--- a/qapi/machine.json
+++ b/qapi/machine.json
@@ -108,7 +108,7 @@
 #
 # Return information about all virtual CPUs.
 #
-# Since: 2.12
+# Details:
 #
 # .. qmp-example::
 #
@@ -138,6 +138,8 @@
 #             }
 #         ]
 #     }
+#
+# Since: 2.12
 ##
 { 'command': 'query-cpus-fast', 'returns': [ 'CpuInfoFast' ] }
 
@@ -223,8 +225,6 @@
 #
 # @unstable: Argument @compat-props is experimental.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-machines", "arguments": { "compat-props": true } }
@@ -247,6 +247,8 @@
 #               },
 #               ...
 #        }
+#
+# Since: 1.2
 ##
 { 'command': 'query-machines',
   'data': { '*compat-props': { 'type': 'bool',
@@ -303,10 +305,10 @@
 #
 # @UUID: the UUID of the guest
 #
-# Since: 0.14
-#
 # .. note:: If no UUID was specified for the guest, the nil UUID (all
 #    zeroes) is returned.
+#
+# Since: 0.14
 ##
 { 'struct': 'UuidInfo', 'data': {'UUID': 'str'} }
 
@@ -315,12 +317,14 @@
 #
 # Query the guest UUID information.
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "query-uuid" }
 #     <- { "return": { "UUID": "550e8400-e29b-41d4-a716-446655440000" } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-uuid', 'returns': 'UuidInfo', 'allow-preconfig': true }
 
@@ -349,12 +353,14 @@
 #
 # Performs a hard reset of a guest.
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "system_reset" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'system_reset' }
 
@@ -363,7 +369,7 @@
 #
 # Requests that a guest perform a powerdown operation.
 #
-# Since: 0.14
+# Details:
 #
 # .. note:: A guest may or may not respond to this command.  This
 #    command returning does not indicate that a guest has accepted the
@@ -374,6 +380,8 @@
 #
 #     -> { "execute": "system_powerdown" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'system_powerdown' }
 
@@ -385,7 +393,7 @@
 # `query-current-machine`), wake-up guest from suspend if the guest is
 # in SUSPENDED state.  Return an error otherwise.
 #
-# Since: 1.1
+# Details:
 #
 # .. note:: Prior to 4.0, this command does nothing in case the guest
 #    isn't suspended.
@@ -394,6 +402,8 @@
 #
 #     -> { "execute": "system_wakeup" }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'system_wakeup' }
 
@@ -436,7 +446,7 @@
 # all CPUs (ppc64).  The command fails when the guest doesn't support
 # injecting.
 #
-# Since: 0.14
+# Details:
 #
 # .. note:: Prior to 2.1, this command was only supported for x86 and
 #    s390 VMs.
@@ -445,6 +455,8 @@
 #
 #     -> { "execute": "inject-nmi" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'inject-nmi' }
 
@@ -806,8 +818,6 @@
 # @cpu-index: the index of the virtual CPU to use for translating the
 #     virtual address (defaults to CPU 0)
 #
-# Since: 0.14
-#
 # .. caution:: Errors were not reliably returned until 1.1.
 #
 # .. qmp-example::
@@ -817,6 +827,8 @@
 #                         "size": 100,
 #                         "filename": "/tmp/virtual-mem-dump" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'memsave',
   'data': {
@@ -836,8 +848,6 @@
 #
 # @filename: the file to save the memory to as binary data
 #
-# Since: 0.14
-#
 # .. caution:: Errors were not reliably returned until 1.1.
 #
 # .. qmp-example::
@@ -847,6 +857,8 @@
 #                         "size": 100,
 #                         "filename": "/tmp/physical-mem-dump" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'pmemsave',
   'data': {
@@ -900,7 +912,7 @@
 #
 # Return information for all memory backends.
 #
-# Since: 2.1
+# Details:
 #
 # .. qmp-example::
 #
@@ -927,6 +939,8 @@
 #            }
 #          ]
 #        }
+#
+# Since: 2.1
 ##
 { 'command': 'query-memdev', 'returns': ['Memdev'], 'allow-preconfig': true }
 
@@ -1015,8 +1029,6 @@
 #
 # TODO: Better documentation; currently there is none.
 #
-# Since: 2.7
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -1067,6 +1079,8 @@
 #             "props": { "core-id": 0 }
 #          }
 #        ]}
+#
+# Since: 2.7
 ##
 { 'command': 'query-hotpluggable-cpus', 'returns': ['HotpluggableCPU'],
              'allow-preconfig': true }
@@ -1105,8 +1119,6 @@
 #    returns, the balloon size may not have changed.  A guest can
 #    change the balloon size independent of this command.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -1116,6 +1128,8 @@
 #      <- { "return": {} }
 #
 #    With a 2.5GiB guest this command inflated the ballon to 3GiB.
+#
+# Since: 0.14
 ##
 { 'command': 'balloon', 'data': {'value': 'int'} }
 
@@ -1141,8 +1155,6 @@
 #       the KVM kernel module cannot support it, KVMMissingCap
 #     - If no balloon device is present, DeviceNotActive
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-balloon" }
@@ -1150,6 +1162,8 @@
 #              "actual": 1073741824
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-balloon', 'returns': 'BalloonInfo' }
 
@@ -1165,13 +1179,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "BALLOON_CHANGE",
 #          "data": { "actual": 944766976 },
 #          "timestamp": { "seconds": 1267020223, "microseconds": 435656 } }
+#
+# Since: 1.2
 ##
 { 'event': 'BALLOON_CHANGE',
   'data': { 'actual': 'int' } }
@@ -1204,8 +1218,6 @@
 #       reporting is not enabled or no guest memory status report
 #       received yet, GenericError
 #
-# Since: 8.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-hv-balloon-status-report" }
@@ -1214,6 +1226,8 @@
 #              "available": 3333054464
 #           }
 #        }
+#
+# Since: 8.2
 ##
 { 'command': 'query-hv-balloon-status-report', 'returns': 'HvBalloonInfo' }
 
@@ -1227,13 +1241,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 8.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "HV_BALLOON_STATUS_REPORT",
 #          "data": { "committed": 816640000, "available": 3333054464 },
 #          "timestamp": { "seconds": 1600295492, "microseconds": 661044 } }
+#
+# Since: 8.2
 ##
 { 'event': 'HV_BALLOON_STATUS_REPORT',
   'data': 'HvBalloonInfo' }
@@ -1540,7 +1554,7 @@
 #
 # Lists available memory devices and their state
 #
-# Since: 2.1
+# Details:
 #
 # .. qmp-example::
 #
@@ -1556,6 +1570,8 @@
 #                             "slot": 0},
 #                        "type": "dimm"
 #                      } ] }
+#
+# Since: 2.1
 ##
 { 'command': 'query-memory-devices', 'returns': ['MemoryDeviceInfo'] }
 
@@ -1574,14 +1590,14 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 5.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "MEMORY_DEVICE_SIZE_CHANGE",
 #          "data": { "id": "vm0", "size": 1073741824,
 #                    "qom-path": "/machine/unattached/device[2]" },
 #          "timestamp": { "seconds": 1588168529, "microseconds": 201316 } }
+#
+# Since: 5.1
 ##
 { 'event': 'MEMORY_DEVICE_SIZE_CHANGE',
   'data': { '*id': 'str', 'size': 'size', 'qom-path' : 'str'} }
@@ -1814,13 +1830,13 @@
 #
 # @filename: name of the dtb file to be created
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "dumpdtb" }
 #          "arguments": { "filename": "fdt.dtb" } }
 #     <- { "return": {} }
+#
+# Since: 7.2
 ##
 { 'command': 'dumpdtb',
   'data': { 'filename': 'str' },
@@ -1879,13 +1895,13 @@
 #
 # @filename: the path to the file to dump to
 #
-# Since: 2.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "dump-skeys",
 #          "arguments": { "filename": "/tmp/skeys" } }
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'dump-skeys',
   'data': { 'filename': 'str' } }
diff --git a/qapi/migration.json b/qapi/migration.json
index 558b4f145ed..79c24e36958 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -339,7 +339,7 @@
 # Return information about current migration process.  If migration is
 # active there will be another json-object with RAM migration status.
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #    :title: Before the first migration
@@ -426,6 +426,8 @@
 #              }
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-migrate', 'returns': 'MigrationInfo' }
 
@@ -562,13 +564,13 @@
 #
 # @capabilities: json array of capability modifications to make
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-set-capabilities" , "arguments":
 #          { "capabilities": [ { "capability": "xbzrle", "state": true } ] } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'migrate-set-capabilities',
   'data': { 'capabilities': ['MigrationCapabilityStatus'] } }
@@ -578,7 +580,7 @@
 #
 # Return information about the current migration capabilities status
 #
-# Since: 1.2
+# Details:
 #
 # .. qmp-example::
 #
@@ -591,6 +593,8 @@
 #           {"state": false, "capability": "postcopy-ram"},
 #           {"state": false, "capability": "x-colo"}
 #        ]}
+#
+# Since: 1.2
 ##
 { 'command': 'query-migrate-capabilities', 'returns':   ['MigrationCapabilityStatus']}
 
@@ -838,13 +842,15 @@
 #
 # Set migration parameters.  All arguments are optional.
 #
-# Since: 2.4
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-set-parameters" ,
 #          "arguments": { "multifd-channels": 5 } }
 #     <- { "return": {} }
+#
+# Since: 2.4
 ##
 { 'command': 'migrate-set-parameters', 'boxed': true,
   'data': 'MigrationParameters' }
@@ -1056,7 +1062,7 @@
 # @block-bitmap-mapping, which is only present if it has been
 # previously set.
 #
-# Since: 2.4
+# Details:
 #
 # .. qmp-example::
 #
@@ -1069,6 +1075,8 @@
 #              "downtime-limit": 300
 #           }
 #        }
+#
+# Since: 2.4
 ##
 { 'command': 'query-migrate-parameters',
   'returns': 'MigrationParameters' }
@@ -1080,12 +1088,14 @@
 # mode.  The postcopy-ram capability must be set on both source and
 # destination before the original migration command.
 #
-# Since: 2.5
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-start-postcopy" }
 #     <- { "return": {} }
+#
+# Since: 2.5
 ##
 { 'command': 'migrate-start-postcopy' }
 
@@ -1096,13 +1106,13 @@
 #
 # @status: `MigrationStatus` describing the current migration status.
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     <- {"timestamp": {"seconds": 1432121972, "microseconds": 744001},
 #         "event": "MIGRATION",
 #         "data": {"status": "completed"} }
+#
+# Since: 2.4
 ##
 { 'event': 'MIGRATION',
   'data': {'status': 'MigrationStatus'}}
@@ -1115,12 +1125,12 @@
 #
 # @pass: An incrementing count (starting at 1 on the first pass)
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1449669631, "microseconds": 239225},
 #           "event": "MIGRATION_PASS", "data": {"pass": 2} }
+#
+# Since: 2.6
 ##
 { 'event': 'MIGRATION_PASS',
   'data': { 'pass': 'int' } }
@@ -1199,12 +1209,12 @@
 #
 # @reason: describes the reason for the COLO exit.
 #
-# Since: 3.1
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 2032141960, "microseconds": 417172},
 #          "event": "COLO_EXIT", "data": {"mode": "primary", "reason": "request" } }
+#
+# Since: 3.1
 ##
 { 'event': 'COLO_EXIT',
   'data': {'mode': 'COLOMode', 'reason': 'COLOExitReason' } }
@@ -1242,12 +1252,12 @@
 #
 # @unstable: This command is experimental.
 #
-# Since: 2.8
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-colo-lost-heartbeat" }
 #     <- { "return": {} }
+#
+# Since: 2.8
 ##
 { 'command': 'x-colo-lost-heartbeat',
   'features': [ 'unstable' ],
@@ -1260,15 +1270,17 @@
 # migration to be started right after.  When postcopy-ram is in use,
 # cancelling is not allowed after the postcopy phase has started.
 #
+# Details:
+#
 # .. note:: This command succeeds even if there is no migration
 #    process running.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate_cancel" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'migrate_cancel' }
 
@@ -1279,13 +1291,13 @@
 #
 # @state: The state the migration is currently expected to be in
 #
-# Since: 2.11
-#
 # .. qmp-example::
 #
 #     -> { "execute": "migrate-continue" , "arguments":
 #          { "state": "pre-switchover" } }
 #     <- { "return": {} }
+#
+# Since: 2.11
 ##
 { 'command': 'migrate-continue', 'data': {'state': 'MigrationStatus'} }
 
@@ -1394,8 +1406,6 @@
 #     will fail unless migration is in "postcopy-paused" state.
 #     (default: false, since 3.0)
 #
-# Since: 0.14
-#
 # .. admonition:: Notes
 #
 #     1. The `query-migrate` command should be used to check
@@ -1449,6 +1459,8 @@
 #                                        "filename": "/tmp/migfile",
 #                                        "offset": "0x1000" } } ] } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'migrate',
   'data': {'*uri': 'str',
@@ -1472,8 +1484,6 @@
 #     :qapi:event:`MIGRATION` event, and error details could be
 #     retrieved with `query-migrate`.  (since 9.1)
 #
-# Since: 2.3
-#
 # .. admonition:: Notes
 #
 #     1. It's a bad idea to use a string for the uri, but it needs to
@@ -1521,6 +1531,8 @@
 #                                        "host": "10.12.34.9",
 #                                        "port": "1050" } } ] } }
 #     <- { "return": {} }
+#
+# Since: 2.3
 ##
 { 'command': 'migrate-incoming',
              'data': {'*uri': 'str',
@@ -1540,13 +1552,13 @@
 # @live: Optional argument to ask QEMU to treat this command as part
 #     of a live migration.  Default to true.  (since 2.11)
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-save-devices-state",
 #          "arguments": { "filename": "/tmp/save" } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'xen-save-devices-state',
   'data': {'filename': 'str', '*live':'bool' } }
@@ -1558,13 +1570,13 @@
 #
 # @enable: true to enable, false to disable.
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-set-global-dirty-log",
 #          "arguments": { "enable": true } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'xen-set-global-dirty-log', 'data': { 'enable': 'bool' } }
 
@@ -1578,13 +1590,13 @@
 #     data.  See `xen-save-devices-state`.txt for a description of the
 #     binary format.
 #
-# Since: 2.7
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-load-devices-state",
 #          "arguments": { "filename": "/tmp/resume" } }
 #     <- { "return": {} }
+#
+# Since: 2.7
 ##
 { 'command': 'xen-load-devices-state', 'data': {'filename': 'str'} }
 
@@ -1747,13 +1759,13 @@
 #
 # @device-id: QEMU device id of the unplugged device
 #
-# Since: 4.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "UNPLUG_PRIMARY",
 #          "data": { "device-id": "hostdev0" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 4.2
 ##
 { 'event': 'UNPLUG_PRIMARY',
   'data': { 'device-id': 'str' } }
@@ -1907,8 +1919,6 @@
 #     'page-sampling'.  Others are 'dirty-bitmap' and 'dirty-ring'.
 #     (Since 6.1)
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> {"execute": "calc-dirty-rate", "arguments": {"calc-time": 1,
@@ -1924,6 +1934,8 @@
 #         "calc-time-unit": "millisecond", "mode": "dirty-bitmap"} }
 #
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'calc-dirty-rate', 'data': {'calc-time': 'int64',
                                          '*calc-time-unit': 'TimeUnit',
@@ -1938,8 +1950,6 @@
 # @calc-time-unit: time unit in which to report calculation time.
 #     By default it is reported in seconds.  (Since 8.2)
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #    :title: Measurement is in progress
 #
@@ -1953,6 +1963,8 @@
 #     <- {"status": "measured", "sample-pages": 512, "dirty-rate": 108,
 #         "mode": "page-sampling", "start-time": 1693900454, "calc-time": 10,
 #         "calc-time-unit": "second"}
+#
+# Since: 5.2
 ##
 { 'command': 'query-dirty-rate', 'data': {'*calc-time-unit': 'TimeUnit' },
                                  'returns': 'DirtyRateInfo' }
@@ -1989,14 +2001,14 @@
 #
 # @dirty-rate: upper limit of dirty page rate (MB/s) for virtual CPUs.
 #
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     -> {"execute": "set-vcpu-dirty-limit"}
 #         "arguments": { "dirty-rate": 200,
 #                        "cpu-index": 1 } }
 #     <- { "return": {} }
+#
+# Since: 7.1
 ##
 { 'command': 'set-vcpu-dirty-limit',
   'data': { '*cpu-index': 'int',
@@ -2013,13 +2025,13 @@
 #
 # @cpu-index: index of a virtual CPU, default is all.
 #
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     -> {"execute": "cancel-vcpu-dirty-limit"},
 #         "arguments": { "cpu-index": 1 } }
 #     <- { "return": {} }
+#
+# Since: 7.1
 ##
 { 'command': 'cancel-vcpu-dirty-limit',
   'data': { '*cpu-index': 'int'} }
@@ -2029,7 +2041,7 @@
 #
 # Return information about virtual CPU dirty page rate limits, if any.
 #
-# Since: 7.1
+# Details:
 #
 # .. qmp-example::
 #
@@ -2037,6 +2049,8 @@
 #     <- {"return": [
 #            { "limit-rate": 60, "current-rate": 3, "cpu-index": 0},
 #            { "limit-rate": 60, "current-rate": 3, "cpu-index": 1}]}
+#
+# Since: 7.1
 ##
 { 'command': 'query-vcpu-dirty-limit',
   'returns': [ 'DirtyLimitInfo' ] }
diff --git a/qapi/misc-arm.json b/qapi/misc-arm.json
index 4e3f1a54055..4a2746ee1aa 100644
--- a/qapi/misc-arm.json
+++ b/qapi/misc-arm.json
@@ -38,13 +38,13 @@
 # On non-ARM targets this command will report an error as the GIC
 # technology is not applicable.
 #
-# Since: 2.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-gic-capabilities" }
 #     <- { "return": [{ "version": 2, "emulated": true, "kernel": false },
 #                     { "version": 3, "emulated": false, "kernel": true } ] }
+#
+# Since: 2.6
 ##
 { 'command': 'query-gic-capabilities', 'returns': ['GICCapability'] }
 
diff --git a/qapi/misc-i386.json b/qapi/misc-i386.json
index c92853507f3..01cdf8d706e 100644
--- a/qapi/misc-i386.json
+++ b/qapi/misc-i386.json
@@ -10,16 +10,18 @@
 # mechanism to synchronize guest time is in effect, for example QEMU
 # guest agent's `guest-set-time` command.
 #
+# Details:
+#
 # Use of this command is only applicable for x86 machines with an RTC,
 # and on other machines will silently return without performing any
 # action.
 #
-# Since: 2.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "rtc-reset-reinjection" }
 #     <- { "return": {} }
+#
+# Since: 2.1
 ##
 { 'command': 'rtc-reset-reinjection' }
 
@@ -133,14 +135,14 @@
 # @enabled field is set to 'false' and the state of all other fields
 # is unspecified.
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sev" }
 #     <- { "return": { "enabled": true, "api-major" : 0, "api-minor" : 0,
 #                      "build-id" : 0, "policy" : 0, "state" : "running",
 #                      "handle" : 1 } }
+#
+# Since: 2.12
 ##
 { 'command': 'query-sev', 'returns': 'SevInfo' }
 
@@ -171,12 +173,12 @@
 #       invalid guest configuration or if the guest has not reached
 #       the required SEV state, GenericError
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sev-launch-measure" }
 #     <- { "return": { "data": "4l8LXeNlSPUDlXPJG5966/8%YZ" } }
+#
+# Since: 2.12
 ##
 { 'command': 'query-sev-launch-measure', 'returns': 'SevLaunchMeasureInfo' }
 
@@ -216,14 +218,14 @@
 # Errors:
 #     - If SEV is not available on the platform, GenericError
 #
-# Since: 2.12
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-sev-capabilities" }
 #     <- { "return": { "pdh": "8CCDD8DDD", "cert-chain": "888CCCDDDEE",
 #                      "cpu0-id": "2lvmGwo+...61iEinw==",
 #                      "cbitpos": 47, "reduced-phys-bits": 1}}
+#
+# Since: 2.12
 ##
 { 'command': 'query-sev-capabilities', 'returns': 'SevCapability' }
 
@@ -282,13 +284,13 @@
 #       invalid guest configuration or because the guest has not
 #       reached the required SEV state, GenericError
 #
-# Since: 6.1
-#
 # .. qmp-example::
 #
 #     -> { "execute" : "query-sev-attestation-report",
 #                      "arguments": { "mnonce": "aaaaaaa" } }
 #     <- { "return" : { "data": "aaaaaaaabbbddddd"} }
+#
+# Since: 6.1
 ##
 { 'command': 'query-sev-attestation-report',
   'data': { 'mnonce': 'str' },
@@ -338,7 +340,7 @@
 #
 # Return information about configured SGX capabilities of guest
 #
-# Since: 6.2
+# Details:
 #
 # .. qmp-example::
 #
@@ -347,6 +349,8 @@
 #                      "flc": true,
 #                      "sections": [{"node": 0, "size": 67108864},
 #                      {"node": 1, "size": 29360128}]} }
+#
+# Since: 6.2
 ##
 { 'command': 'query-sgx', 'returns': 'SgxInfo' }
 
@@ -355,7 +359,7 @@
 #
 # Return information about SGX capabilities of host
 #
-# Since: 6.2
+# Details:
 #
 # .. qmp-example::
 #
@@ -364,6 +368,8 @@
 #                      "flc": true,
 #                      "section" : [{"node": 0, "size": 67108864},
 #                      {"node": 1, "size": 29360128}]} }
+#
+# Since: 6.2
 ##
 { 'command': 'query-sgx-capabilities', 'returns': 'SgxInfo' }
 
@@ -426,8 +432,6 @@
 #
 # Returns: list of open event channel ports.
 #
-# Since: 8.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-event-list" }
@@ -452,6 +456,8 @@
 #             }
 #          ]
 #        }
+#
+# Since: 8.0
 ##
 { 'command': 'xen-event-list',
   'returns': ['EvtchnInfo'] }
@@ -463,12 +469,12 @@
 #
 # @port: The port number
 #
-# Since: 8.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "xen-event-inject", "arguments": { "port": 1 } }
 #     <- { "return": { } }
+#
+# Since: 8.0
 ##
 { 'command': 'xen-event-inject',
   'data': { 'port': 'uint32' } }
diff --git a/qapi/misc.json b/qapi/misc.json
index 05866837f09..40f5b9559cb 100644
--- a/qapi/misc.json
+++ b/qapi/misc.json
@@ -30,13 +30,13 @@
 #
 # @tls: whether to perform TLS.  Only applies to the "spice" protocol
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "add_client", "arguments": { "protocol": "vnc",
 #                                                  "fdname": "myclient" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'add_client',
   'data': { 'protocol': 'str', 'fdname': 'str', '*skipauth': 'bool',
@@ -58,12 +58,14 @@
 #
 # Return the name information of a guest.
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "query-name" }
 #     <- { "return": { "name": "qemu-name" } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-name', 'returns': 'NameInfo', 'allow-preconfig': true }
 
@@ -109,8 +111,6 @@
 #
 # Returns: a list of info for each iothread
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-iothreads" }
@@ -125,6 +125,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 2.0
 ##
 { 'command': 'query-iothreads', 'returns': ['IOThreadInfo'],
   'allow-preconfig': true }
@@ -134,7 +136,7 @@
 #
 # Stop guest VM execution.
 #
-# Since: 0.14
+# Details:
 #
 # .. note:: This function will succeed even if the guest is already in
 #    the stopped state.  In "inmigrate" state, it will ensure that the
@@ -148,6 +150,8 @@
 #
 #     -> { "execute": "stop" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'stop' }
 
@@ -156,7 +160,7 @@
 #
 # Resume guest VM execution.
 #
-# Since: 0.14
+# Details:
 #
 # .. note:: This command will succeed if the guest is currently
 #    running.  It will also succeed if the guest is in the "inmigrate"
@@ -172,6 +176,8 @@
 #
 #     -> { "execute": "cont" }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'cont' }
 
@@ -190,12 +196,12 @@
 #
 # @unstable: This command is experimental.
 #
-# Since: 3.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-exit-preconfig" }
 #     <- { "return": {} }
+#
+# Since: 3.0
 ##
 { 'command': 'x-exit-preconfig', 'allow-preconfig': true,
   'features': [ 'unstable' ] }
@@ -217,8 +223,6 @@
 #     monitor-owned nodes if they have no parents.  This allows the
 #     use of 'savevm' with -blockdev.  (since 4.2)
 #
-# Since: 0.14
-#
 # .. note:: This command only exists as a stop-gap.  Its use is highly
 #    discouraged.  The semantics of this command are not guaranteed:
 #    this means that command names, arguments and responses can change
@@ -237,6 +241,8 @@
 #     -> { "execute": "human-monitor-command",
 #          "arguments": { "command-line": "info kvm" } }
 #     <- { "return": "kvm support: enabled\r\n" }
+#
+# Since: 0.14
 ##
 { 'command': 'human-monitor-command',
   'data': {'command-line': 'str', '*cpu-index': 'int'},
@@ -250,8 +256,6 @@
 #
 # @fdname: file descriptor name
 #
-# Since: 0.14
-#
 # .. note:: If @fdname already exists, the file descriptor assigned to
 #    it will be closed and replaced by the received file descriptor.
 #
@@ -262,6 +266,8 @@
 #
 #     -> { "execute": "getfd", "arguments": { "fdname": "fd1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'getfd', 'data': {'fdname': 'str'}, 'if': 'CONFIG_POSIX' }
 
@@ -277,8 +283,6 @@
 #
 # @fdname: file descriptor name
 #
-# Since: 8.0
-#
 # .. note:: If @fdname already exists, the file descriptor assigned to
 #    it will be closed and replaced by the received file descriptor.
 #
@@ -290,6 +294,8 @@
 #     -> { "execute": "get-win32-socket",
 #          "arguments": { "info": "abcd123..", "fdname": "skclient" } }
 #     <- { "return": {} }
+#
+# Since: 8.0
 ##
 { 'command': 'get-win32-socket', 'data': {'info': 'str', 'fdname': 'str'}, 'if': 'CONFIG_WIN32' }
 
@@ -300,12 +306,12 @@
 #
 # @fdname: file descriptor name
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "closefd", "arguments": { "fdname": "fd1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'closefd', 'data': {'fdname': 'str'} }
 
@@ -341,12 +347,12 @@
 # .. note:: If @fdset-id is not specified, a new fd set will be
 #    created.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "add-fd", "arguments": { "fdset-id": 1 } }
 #     <- { "return": { "fdset-id": 1, "fd": 3 } }
+#
+# Since: 1.2
 ##
 { 'command': 'add-fd',
   'data': { '*fdset-id': 'int',
@@ -365,8 +371,6 @@
 # Errors:
 #     - If @fdset-id or @fd is not found, GenericError
 #
-# Since: 1.2
-#
 # .. note:: The list of fd sets is shared by all monitor connections.
 #
 # .. note:: If @fd is not specified, all file descriptors in @fdset-id
@@ -376,6 +380,8 @@
 #
 #     -> { "execute": "remove-fd", "arguments": { "fdset-id": 1, "fd": 3 } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'remove-fd', 'data': {'fdset-id': 'int', '*fd': 'int'} }
 
@@ -412,7 +418,7 @@
 #
 # Return information describing all fd sets.
 #
-# Since: 1.2
+# Details:
 #
 # .. note:: The list of fd sets is shared by all monitor connections.
 #
@@ -446,6 +452,8 @@
 #            }
 #          ]
 #        }
+#
+# Since: 1.2
 ##
 { 'command': 'query-fdsets', 'returns': ['FdsetInfo'] }
 
@@ -516,8 +524,6 @@
 # Errors:
 #     - if the given @option doesn't exist
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-command-line-options",
@@ -538,6 +544,8 @@
 #             }
 #          ]
 #        }
+#
+# Since: 1.5
 ##
 {'command': 'query-command-line-options',
  'data': {'*option': 'str'},
@@ -558,13 +566,13 @@
 #    RTC in the system implements this event, or even that the system
 #    has an RTC at all.
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "RTC_CHANGE",
 #          "data": { "offset": 78 },
 #          "timestamp": { "seconds": 1267020223, "microseconds": 435656 } }
+#
+# Since: 0.13
 ##
 { 'event': 'RTC_CHANGE',
   'data': { 'offset': 'int', 'qom-path': 'str' } }
@@ -585,8 +593,6 @@
 #
 # @dev-qom-path: path to attached PCI device in the QOM tree
 #
-# Since: 7.1
-#
 # .. qmp-example::
 #
 #     <- { "event": "VFU_CLIENT_HANGUP",
@@ -595,6 +601,8 @@
 #                    "dev-id": "sas1",
 #                    "dev-qom-path": "/machine/peripheral/sas1" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 7.1
 ##
 { 'event': 'VFU_CLIENT_HANGUP',
   'data': { 'vfu-id': 'str', 'vfu-qom-path': 'str',
diff --git a/qapi/net.json b/qapi/net.json
index c011d6dc1a9..2deb0b6dfdf 100644
--- a/qapi/net.json
+++ b/qapi/net.json
@@ -22,8 +22,6 @@
 # Errors:
 #     - If @name is not a valid network device, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. note:: Not all network adapters support setting link status.
 #    This command will succeed even if the network adapter does not
 #    support link status notification.
@@ -33,6 +31,8 @@
 #     -> { "execute": "set_link",
 #          "arguments": { "name": "e1000.0", "up": false } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'set_link', 'data': {'name': 'str', 'up': 'bool'} }
 
@@ -43,8 +43,6 @@
 #
 # Additional arguments depend on the type.
 #
-# Since: 0.14
-#
 # Errors:
 #     - If @type is not a valid network backend, DeviceNotFound
 #
@@ -54,6 +52,8 @@
 #          "arguments": { "type": "user", "id": "netdev1",
 #                         "dnssearch": [ { "str": "example.org" } ] } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'netdev_add', 'data': 'Netdev', 'boxed': true,
   'allow-preconfig': true }
@@ -68,12 +68,12 @@
 # Errors:
 #     - If @id is not a valid network backend, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "netdev_del", "arguments": { "id": "netdev1" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'netdev_del', 'data': {'id': 'str'},
   'allow-preconfig': true }
@@ -975,8 +975,6 @@
 #     - if the given NIC doesn't support rx-filter querying
 #     - if the given net client isn't a NIC
 #
-# Since: 1.6
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rx-filter", "arguments": { "name": "vnet0" } }
@@ -1005,6 +1003,8 @@
 #             }
 #           ]
 #        }
+#
+# Since: 1.6
 ##
 { 'command': 'query-rx-filter',
   'data': { '*name': 'str' },
@@ -1020,14 +1020,14 @@
 #
 # @path: device path
 #
-# Since: 1.6
-#
 # .. qmp-example::
 #
 #     <- { "event": "NIC_RX_FILTER_CHANGED",
 #          "data": { "name": "vnet0",
 #                    "path": "/machine/peripheral/vnet0/virtio-backend" },
 #          "timestamp": { "seconds": 1368697518, "microseconds": 326866 } }
+#
+# Since: 1.6
 ##
 { 'event': 'NIC_RX_FILTER_CHANGED',
   'data': { '*name': 'str', 'path': 'str' } }
@@ -1095,13 +1095,13 @@
 #
 # @device-id: QEMU device id of the unplugged device
 #
-# Since: 4.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "FAILOVER_NEGOTIATED",
 #          "data": { "device-id": "net1" },
 #          "timestamp": { "seconds": 1368697518, "microseconds": 326866 } }
+#
+# Since: 4.2
 ##
 { 'event': 'FAILOVER_NEGOTIATED',
   'data': {'device-id': 'str'} }
@@ -1115,8 +1115,6 @@
 #
 # @addr: The destination address
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "NETDEV_STREAM_CONNECTED",
@@ -1131,6 +1129,8 @@
 #          "data": { "netdev-id": "netdev0",
 #                    "addr": { "path": "/tmp/qemu0", "type": "unix" } },
 #          "timestamp": { "seconds": 1666269706, "microseconds": 413651 } }
+#
+# Since: 7.2
 ##
 { 'event': 'NETDEV_STREAM_CONNECTED',
   'data': { 'netdev-id': 'str',
@@ -1143,13 +1143,13 @@
 #
 # @netdev-id: QEMU netdev id that is disconnected
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "NETDEV_STREAM_DISCONNECTED",
 #          "data": {"netdev-id": "netdev0"},
 #          "timestamp": {"seconds": 1663330937, "microseconds": 526695} }
+#
+# Since: 7.2
 ##
 { 'event': 'NETDEV_STREAM_DISCONNECTED',
   'data': { 'netdev-id': 'str' } }
@@ -1163,13 +1163,13 @@
 #
 # @chardev-id: The character device id used by the QEMU netdev
 #
-# Since: 10.0
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1739538638, "microseconds": 354181 },
 #          "event": "NETDEV_VHOST_USER_CONNECTED",
 #          "data": { "netdev-id": "netdev0", "chardev-id": "chr0" } }
+#
+# Since: 10.0
 ##
 { 'event': 'NETDEV_VHOST_USER_CONNECTED',
   'data': { 'netdev-id': 'str', 'chardev-id': 'str' } }
@@ -1181,13 +1181,13 @@
 #
 # @netdev-id: QEMU netdev id that is disconnected
 #
-# Since: 10.0
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": { "seconds": 1739538634, "microseconds": 920450 },
 #          "event": "NETDEV_VHOST_USER_DISCONNECTED",
 #          "data": { "netdev-id": "netdev0" } }
+#
+# Since: 10.0
 ##
 { 'event': 'NETDEV_VHOST_USER_DISCONNECTED',
   'data': { 'netdev-id': 'str' } }
diff --git a/qapi/pci.json b/qapi/pci.json
index 694c741e420..c36af01a100 100644
--- a/qapi/pci.json
+++ b/qapi/pci.json
@@ -182,8 +182,6 @@
 #     of all PCI devices attached to it.  Each device is represented
 #     by a json-object.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-pci" }
@@ -314,5 +312,7 @@
 #        }
 #
 # This example has been shortened as the real response is too long.
+#
+# Since: 0.14
 ##
 { 'command': 'query-pci', 'returns': ['PciInfo'] }
diff --git a/qapi/qdev.json b/qapi/qdev.json
index 974cf9c5830..483bc9eb9cd 100644
--- a/qapi/qdev.json
+++ b/qapi/qdev.json
@@ -103,8 +103,6 @@
 #    device will not be removed and a `DEVICE_UNPLUG_GUEST_ERROR`
 #    event is sent.  Some errors cannot be detected.
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "device_del",
@@ -116,6 +114,8 @@
 #     -> { "execute": "device_del",
 #          "arguments": { "id": "/machine/peripheral-anon/device[0]" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'device_del', 'data': {'id': 'str'} }
 
@@ -131,14 +131,14 @@
 #
 # @path: the device's QOM path
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     <- { "event": "DEVICE_DELETED",
 #          "data": { "device": "virtio-net-pci-0",
 #                    "path": "/machine/peripheral/virtio-net-pci-0" },
 #          "timestamp": { "seconds": 1265044230, "microseconds": 450486 } }
+#
+# Since: 1.5
 ##
 { 'event': 'DEVICE_DELETED',
   'data': { '*device': 'str', 'path': 'str' } }
@@ -153,14 +153,14 @@
 #
 # @path: the device's QOM path
 #
-# Since: 6.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "DEVICE_UNPLUG_GUEST_ERROR",
 #          "data": { "device": "core1",
 #                    "path": "/machine/peripheral/core1" },
 #          "timestamp": { "seconds": 1615570772, "microseconds": 202844 } }
+#
+# Since: 6.2
 ##
 { 'event': 'DEVICE_UNPLUG_GUEST_ERROR',
   'data': { '*device': 'str', 'path': 'str' } }
diff --git a/qapi/qom.json b/qapi/qom.json
index fd88be07e13..9733a815a9b 100644
--- a/qapi/qom.json
+++ b/qapi/qom.json
@@ -85,8 +85,6 @@
 #
 # Returns: a list that describe the properties of the object.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "qom-list",
@@ -95,6 +93,8 @@
 #                      { "name": "parallel0", "type": "child<chardev-vc>" },
 #                      { "name": "serial0", "type": "child<chardev-vc>" },
 #                      { "name": "mon0", "type": "child<chardev-stdio>" } ] }
+#
+# Since: 1.2
 ##
 { 'command': 'qom-list',
   'data': { 'path': 'str' },
@@ -129,8 +129,6 @@
 #     child<> and link<> properties are returned as #str pathnames.
 #     All integer property types (u8, u16, etc) are returned as #int.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #    :title: Use absolute path
 #
@@ -146,6 +144,8 @@
 #          "arguments": { "path": "unattached/sysbus",
 #                         "property": "type" } }
 #     <- { "return": "System" }
+#
+# Since: 1.2
 ##
 { 'command': 'qom-get',
   'data': { 'path': 'str', 'property': 'str' },
@@ -186,8 +186,6 @@
 # @value: a value who's type is appropriate for the property type.
 #     See `qom-get` for a description of type mapping.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "qom-set",
@@ -195,6 +193,8 @@
 #                         "property": "graphics",
 #                         "value": false } }
 #     <- { "return": {} }
+#
+# Since: 1.2
 ##
 { 'command': 'qom-set',
   'data': { 'path': 'str', 'property': 'str', 'value': 'any' },
@@ -1342,14 +1342,14 @@
 # Errors:
 #     - If @qom-type is not a valid class name
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "object-add",
 #          "arguments": { "qom-type": "rng-random", "id": "rng1",
 #                         "filename": "/dev/hwrng" } }
 #     <- { "return": {} }
+#
+# Since: 2.0
 ##
 { 'command': 'object-add', 'data': 'ObjectOptions', 'boxed': true,
   'allow-preconfig': true }
@@ -1364,12 +1364,12 @@
 # Errors:
 #     - If @id is not a valid id for a QOM object
 #
-# Since: 2.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "object-del", "arguments": { "id": "rng1" } }
 #     <- { "return": {} }
+#
+# Since: 2.0
 ##
 { 'command': 'object-del', 'data': {'id': 'str'},
   'allow-preconfig': true }
diff --git a/qapi/replay.json b/qapi/replay.json
index ccf84da68ef..bdee8e0ecfe 100644
--- a/qapi/replay.json
+++ b/qapi/replay.json
@@ -54,12 +54,12 @@
 #
 # Returns: record/replay information.
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-replay" }
 #     <- { "return": { "mode": "play", "filename": "log.rr", "icount": 220414 } }
+#
+# Since: 5.2
 ##
 { 'command': 'query-replay',
   'returns': 'ReplayInfo' }
@@ -76,12 +76,12 @@
 #
 # @icount: instruction count to stop at
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "replay-break", "arguments": { "icount": 220414 } }
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'replay-break', 'data': { 'icount': 'int' } }
 
@@ -91,12 +91,14 @@
 # Remove replay breakpoint which was set with `replay-break`.  The
 # command is ignored when there are no replay breakpoints.
 #
-# Since: 5.2
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "replay-delete-break" }
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'replay-delete-break' }
 
@@ -112,11 +114,11 @@
 #
 # @icount: target instruction count
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "replay-seek", "arguments": { "icount": 220414 } }
 #     <- { "return": {} }
+#
+# Since: 5.2
 ##
 { 'command': 'replay-seek', 'data': { 'icount': 'int' } }
diff --git a/qapi/rocker.json b/qapi/rocker.json
index 5d2dbd26034..e8efffc4c9f 100644
--- a/qapi/rocker.json
+++ b/qapi/rocker.json
@@ -30,12 +30,12 @@
 #
 # @name: switch name
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker", "arguments": { "name": "sw1" } }
 #     <- { "return": {"name": "sw1", "ports": 2, "id": 1327446905938}}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker',
   'data': { 'name': 'str' },
@@ -98,8 +98,6 @@
 #
 # @name: port name
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker-ports", "arguments": { "name": "sw1" } }
@@ -108,6 +106,8 @@
 #                      {"duplex": "full", "enabled": true, "name": "sw1.2",
 #                       "autoneg": "off", "link-up": true, "speed": 10000}
 #        ]}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker-ports',
   'data': { 'name': 'str' },
@@ -240,8 +240,6 @@
 #
 # Returns: rocker OF-DPA flow information
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker-of-dpa-flows",
@@ -254,6 +252,8 @@
 #                      },
 #                      ...
 #        ]}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker-of-dpa-flows',
   'data': { 'name': 'str', '*tbl-id': 'uint32' },
@@ -315,8 +315,6 @@
 #
 # Returns: rocker OF-DPA group information
 #
-# Since: 2.4
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-rocker-of-dpa-groups",
@@ -334,6 +332,8 @@
 #                       "pport": 0, "vlan-id": 3840,
 #                       "pop-vlan": 1, "id": 251658240}
 #        ]}
+#
+# Since: 2.4
 ##
 { 'command': 'query-rocker-of-dpa-groups',
   'data': { 'name': 'str', '*type': 'uint8' },
diff --git a/qapi/run-state.json b/qapi/run-state.json
index a5771ad4681..1bc4e905f00 100644
--- a/qapi/run-state.json
+++ b/qapi/run-state.json
@@ -121,13 +121,15 @@
 #
 # Query the run status of the VM
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "query-status" }
 #     <- { "return": { "running": true,
 #                      "status": "running" } }
+#
+# Since: 0.14
 ##
 { 'command': 'query-status', 'returns': 'StatusInfo',
   'allow-preconfig': true }
@@ -150,13 +152,13 @@
 #    specified, QEMU will not exit, and a `STOP` event will eventually
 #    follow the `SHUTDOWN` event.
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "SHUTDOWN",
 #          "data": { "guest": true, "reason": "guest-shutdown" },
 #          "timestamp": { "seconds": 1267040730, "microseconds": 682951 } }
+#
+# Since: 0.12
 ##
 { 'event': 'SHUTDOWN', 'data': { 'guest': 'bool', 'reason': 'ShutdownCause' } }
 
@@ -166,12 +168,14 @@
 # Emitted when the virtual machine is powered down through the power
 # control system, such as via ACPI.
 #
-# Since: 0.12
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "event": "POWERDOWN",
 #          "timestamp": { "seconds": 1267040730, "microseconds": 682951 } }
+#
+# Since: 0.12
 ##
 { 'event': 'POWERDOWN' }
 
@@ -187,13 +191,13 @@
 #
 # @reason: The `ShutdownCause` of the `RESET`.  (since 4.0)
 #
-# Since: 0.12
-#
 # .. qmp-example::
 #
 #     <- { "event": "RESET",
 #          "data": { "guest": false, "reason": "guest-reset" },
 #          "timestamp": { "seconds": 1267041653, "microseconds": 9518 } }
+#
+# Since: 0.12
 ##
 { 'event': 'RESET', 'data': { 'guest': 'bool', 'reason': 'ShutdownCause' } }
 
@@ -202,12 +206,14 @@
 #
 # Emitted when the virtual machine is stopped
 #
-# Since: 0.12
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "event": "STOP",
 #          "timestamp": { "seconds": 1267041730, "microseconds": 281295 } }
+#
+# Since: 0.12
 ##
 { 'event': 'STOP' }
 
@@ -216,12 +222,14 @@
 #
 # Emitted when the virtual machine resumes execution
 #
-# Since: 0.12
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "event": "RESUME",
 #          "timestamp": { "seconds": 1271770767, "microseconds": 582542 } }
+#
+# Since: 0.12
 ##
 { 'event': 'RESUME' }
 
@@ -231,12 +239,14 @@
 # Emitted when guest enters a hardware suspension state, for example,
 # S3 state, which is sometimes called standby state
 #
-# Since: 1.1
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "event": "SUSPEND",
 #          "timestamp": { "seconds": 1344456160, "microseconds": 309119 } }
+#
+# Since: 1.1
 ##
 { 'event': 'SUSPEND' }
 
@@ -247,15 +257,17 @@
 # saved on disk, for example, S4 state, which is sometimes called
 # hibernate state
 #
+# Details:
+#
 # .. note:: QEMU shuts down (similar to event `SHUTDOWN`) when
 #    entering this state.
 #
-# Since: 1.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "SUSPEND_DISK",
 #          "timestamp": { "seconds": 1344456160, "microseconds": 309119 } }
+#
+# Since: 1.2
 ##
 { 'event': 'SUSPEND_DISK' }
 
@@ -265,12 +277,14 @@
 # Emitted when the guest has woken up from suspend state and is
 # running
 #
-# Since: 1.1
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "event": "WAKEUP",
 #          "timestamp": { "seconds": 1344522075, "microseconds": 745528 } }
+#
+# Since: 1.1
 ##
 { 'event': 'WAKEUP' }
 
@@ -287,13 +301,13 @@
 #
 # .. note:: This event is rate-limited.
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "WATCHDOG",
 #          "data": { "action": "reset" },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 0.13
 ##
 { 'event': 'WATCHDOG',
   'data': { 'action': 'WatchdogAction' } }
@@ -380,13 +394,13 @@
 #
 # @action: `WatchdogAction` action taken when watchdog timer expires.
 #
-# Since: 2.11
-#
 # .. qmp-example::
 #
 #     -> { "execute": "watchdog-set-action",
 #          "arguments": { "action": "inject-nmi" } }
 #     <- { "return": {} }
+#
+# Since: 2.11
 ##
 { 'command': 'watchdog-set-action', 'data' : {'action': 'WatchdogAction'} }
 
@@ -405,8 +419,6 @@
 # @watchdog: `WatchdogAction` action taken when watchdog timer
 #     expires.
 #
-# Since: 6.0
-#
 # .. qmp-example::
 #
 #     -> { "execute": "set-action",
@@ -415,6 +427,8 @@
 #                         "panic": "pause",
 #                         "watchdog": "inject-nmi" } }
 #     <- { "return": {} }
+#
+# Since: 6.0
 ##
 { 'command': 'set-action',
   'data': { '*reboot': 'RebootAction',
@@ -432,13 +446,13 @@
 #
 # @info: information about a panic (since 2.9)
 #
-# Since: 1.5
-#
 # .. qmp-example::
 #
 #     <- { "event": "GUEST_PANICKED",
 #          "data": { "action": "pause" },
 #          "timestamp": { "seconds": 1648245231, "microseconds": 900001 } }
+#
+# Since: 1.5
 ##
 { 'event': 'GUEST_PANICKED',
   'data': { 'action': 'GuestPanicAction', '*info': 'GuestPanicInformation' } }
@@ -452,13 +466,13 @@
 #
 # @info: information about a panic
 #
-# Since: 5.0
-#
 # .. qmp-example::
 #
 #     <- { "event": "GUEST_CRASHLOADED",
 #          "data": { "action": "run" },
 #          "timestamp": { "seconds": 1648245259, "microseconds": 893771 } }
+#
+# Since: 5.0
 ##
 { 'event': 'GUEST_CRASHLOADED',
   'data': { 'action': 'GuestPanicAction', '*info': 'GuestPanicInformation' } }
@@ -468,12 +482,14 @@
 #
 # Emitted when guest submits a shutdown request via pvpanic interface
 #
-# Since: 9.1
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "event": "GUEST_PVSHUTDOWN",
 #          "timestamp": { "seconds": 1648245259, "microseconds": 893771 } }
+#
+# Since: 9.1
 ##
 { 'event': 'GUEST_PVSHUTDOWN' }
 
@@ -655,8 +671,6 @@
 #
 # @flags: flags for `MemoryFailureAction`.
 #
-# Since: 5.2
-#
 # .. qmp-example::
 #
 #     <- { "event": "MEMORY_FAILURE",
@@ -665,6 +679,8 @@
 #                    "flags": { "action-required": false,
 #                               "recursive": false } },
 #          "timestamp": { "seconds": 1267061043, "microseconds": 959568 } }
+#
+# Since: 5.2
 ##
 { 'event': 'MEMORY_FAILURE',
   'data': { 'recipient': 'MemoryFailureRecipient',
diff --git a/qapi/tpm.json b/qapi/tpm.json
index 3f2850a5733..03c624e1ab8 100644
--- a/qapi/tpm.json
+++ b/qapi/tpm.json
@@ -29,12 +29,14 @@
 #
 # Return a list of supported TPM models
 #
-# Since: 1.5
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "query-tpm-models" }
 #     <- { "return": [ "tpm-tis", "tpm-crb", "tpm-spapr" ] }
+#
+# Since: 1.5
 ##
 { 'command': 'query-tpm-models', 'returns': ['TpmModel'],
   'if': 'CONFIG_TPM' }
@@ -58,12 +60,14 @@
 #
 # Return a list of supported TPM types
 #
-# Since: 1.5
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "query-tpm-types" }
 #     <- { "return": [ "passthrough", "emulator" ] }
+#
+# Since: 1.5
 ##
 { 'command': 'query-tpm-types', 'returns': ['TpmType'],
   'if': 'CONFIG_TPM' }
@@ -164,7 +168,7 @@
 #
 # Return information about the TPM device
 #
-# Since: 1.5
+# Details:
 #
 # .. qmp-example::
 #
@@ -183,6 +187,8 @@
 #            }
 #          ]
 #        }
+#
+# Since: 1.5
 ##
 { 'command': 'query-tpm', 'returns': ['TPMInfo'],
   'if': 'CONFIG_TPM' }
diff --git a/qapi/trace.json b/qapi/trace.json
index de369dae6b5..63685b3d5ec 100644
--- a/qapi/trace.json
+++ b/qapi/trace.json
@@ -51,13 +51,13 @@
 #
 # Returns: a list of info for the matching events
 #
-# Since: 2.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "trace-event-get-state",
 #          "arguments": { "name": "qemu_memalign" } }
 #     <- { "return": [ { "name": "qemu_memalign", "state": "disabled", "vcpu": false } ] }
+#
+# Since: 2.2
 ##
 { 'command': 'trace-event-get-state',
   'data': {'name': 'str' },
@@ -74,13 +74,13 @@
 #
 # @ignore-unavailable: Do not match unavailable events with @name.
 #
-# Since: 2.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "trace-event-set-state",
 #          "arguments": { "name": "qemu_memalign", "enable": true } }
 #     <- { "return": {} }
+#
+# Since: 2.2
 ##
 { 'command': 'trace-event-set-state',
   'data': {'name': 'str', 'enable': 'bool', '*ignore-unavailable': 'bool' } }
diff --git a/qapi/transaction.json b/qapi/transaction.json
index 4b4eb09bf38..1e6b5d37980 100644
--- a/qapi/transaction.json
+++ b/qapi/transaction.json
@@ -244,8 +244,6 @@
 #    in an error condition, and subsequent actions will not have been
 #    attempted.
 #
-# Since: 1.1
-#
 # .. qmp-example::
 #
 #     -> { "execute": "transaction",
@@ -266,6 +264,8 @@
 #                                          "device": "ide-hd2",
 #                                          "name": "snapshot0" } } ] } }
 #     <- { "return": {} }
+#
+# Since: 1.1
 ##
 { 'command': 'transaction',
   'data': { 'actions': [ 'TransactionAction' ],
diff --git a/qapi/ui.json b/qapi/ui.json
index e3da77632a8..535e329e77c 100644
--- a/qapi/ui.json
+++ b/qapi/ui.json
@@ -83,13 +83,13 @@
 # Errors:
 #     - If Spice is not enabled, DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "set_password", "arguments": { "protocol": "vnc",
 #                                                    "password": "secret" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'set_password', 'boxed': true, 'data': 'SetPasswordOptions' }
 
@@ -145,13 +145,13 @@
 #     - If @protocol is 'spice' and Spice is not active,
 #       DeviceNotFound
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "expire_password", "arguments": { "protocol": "vnc",
 #                                                       "time": "+60" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'expire_password', 'boxed': true, 'data': 'ExpirePasswordOptions' }
 
@@ -187,13 +187,13 @@
 #
 # @format: image format for `screendump`.  (default: ppm) (Since 7.1)
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "screendump",
 #          "arguments": { "filename": "/tmp/image" } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'screendump',
   'data': {'filename': 'str', '*device': 'str', '*head': 'int',
@@ -328,7 +328,7 @@
 #
 # Return information about the current SPICE server
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
@@ -364,6 +364,8 @@
 #              ]
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-spice', 'returns': 'SpiceInfo',
   'if': 'CONFIG_SPICE' }
@@ -377,8 +379,6 @@
 #
 # @client: client information
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 388707},
@@ -387,6 +387,8 @@
 #            "server": { "port": "5920", "family": "ipv4", "host": "127.0.0.1"},
 #            "client": {"port": "52873", "family": "ipv4", "host": "127.0.0.1"}
 #        }}
+#
+# Since: 0.14
 ##
 { 'event': 'SPICE_CONNECTED',
   'data': { 'server': 'SpiceBasicInfo',
@@ -403,8 +405,6 @@
 #
 # @client: client information
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 417172},
@@ -415,6 +415,8 @@
 #                              "connection-id": 1804289383, "host": "127.0.0.1",
 #                              "channel-id": 0, "tls": true}
 #        }}
+#
+# Since: 0.14
 ##
 { 'event': 'SPICE_INITIALIZED',
   'data': { 'server': 'SpiceServerInfo',
@@ -430,8 +432,6 @@
 #
 # @client: client information
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 388707},
@@ -440,6 +440,8 @@
 #            "server": { "port": "5920", "family": "ipv4", "host": "127.0.0.1"},
 #            "client": {"port": "52873", "family": "ipv4", "host": "127.0.0.1"}
 #        }}
+#
+# Since: 0.14
 ##
 { 'event': 'SPICE_DISCONNECTED',
   'data': { 'server': 'SpiceBasicInfo',
@@ -451,12 +453,14 @@
 #
 # Emitted when SPICE migration has completed
 #
-# Since: 1.3
+# Details:
 #
 # .. qmp-example::
 #
 #     <- { "timestamp": {"seconds": 1290688046, "microseconds": 417172},
 #          "event": "SPICE_MIGRATE_COMPLETED" }
+#
+# Since: 1.3
 ##
 { 'event': 'SPICE_MIGRATE_COMPLETED',
   'if': 'CONFIG_SPICE' }
@@ -658,7 +662,7 @@
 #
 # Return information about the current VNC server
 #
-# Since: 0.14
+# Details:
 #
 # .. qmp-example::
 #
@@ -679,6 +683,8 @@
 #              ]
 #           }
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-vnc', 'returns': 'VncInfo',
   'if': 'CONFIG_VNC' }
@@ -700,11 +706,11 @@
 #
 # @password: the new password to use with VNC authentication
 #
-# Since: 1.1
-#
 # .. note:: An empty password in this command will set the password to
 #    the empty string.  Existing clients are unaffected by executing
 #    this command.
+#
+# Since: 1.1
 ##
 { 'command': 'change-vnc-password',
   'data': { 'password': 'str' },
@@ -722,8 +728,6 @@
 # .. note:: This event is emitted before any authentication takes
 #    place, thus the authentication ID is not provided.
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "VNC_CONNECTED",
@@ -733,6 +737,8 @@
 #                "client": { "family": "ipv4", "service": "58425",
 #                            "host": "127.0.0.1", "websocket": false } },
 #          "timestamp": { "seconds": 1262976601, "microseconds": 975795 } }
+#
+# Since: 0.13
 ##
 { 'event': 'VNC_CONNECTED',
   'data': { 'server': 'VncServerInfo',
@@ -749,8 +755,6 @@
 #
 # @client: client information
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <-  { "event": "VNC_INITIALIZED",
@@ -760,6 +764,8 @@
 #                "client": { "family": "ipv4", "service": "46089", "websocket": false,
 #                            "host": "127.0.0.1", "sasl_username": "luiz" } },
 #           "timestamp": { "seconds": 1263475302, "microseconds": 150772 } }
+#
+# Since: 0.13
 ##
 { 'event': 'VNC_INITIALIZED',
   'data': { 'server': 'VncServerInfo',
@@ -775,8 +781,6 @@
 #
 # @client: client information
 #
-# Since: 0.13
-#
 # .. qmp-example::
 #
 #     <- { "event": "VNC_DISCONNECTED",
@@ -786,6 +790,8 @@
 #                "client": { "family": "ipv4", "service": "58425", "websocket": false,
 #                            "host": "127.0.0.1", "sasl_username": "luiz" } },
 #          "timestamp": { "seconds": 1262976601, "microseconds": 975795 } }
+#
+# Since: 0.13
 ##
 { 'event': 'VNC_DISCONNECTED',
   'data': { 'server': 'VncServerInfo',
@@ -825,8 +831,6 @@
 #
 # Returns: a list of info for each device
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "query-mice" }
@@ -845,6 +849,8 @@
 #              }
 #           ]
 #        }
+#
+# Since: 0.14
 ##
 { 'command': 'query-mice', 'returns': ['MouseInfo'] }
 
@@ -1035,8 +1041,6 @@
 # Errors:
 #     - If key is unknown or redundant, GenericError
 #
-# Since: 1.3
-#
 # .. qmp-example::
 #
 #     -> { "execute": "send-key",
@@ -1044,6 +1048,8 @@
 #                                   { "type": "qcode", "data": "alt" },
 #                                   { "type": "qcode", "data": "delete" } ] } }
 #     <- { "return": {} }
+#
+# Since: 1.3
 ##
 { 'command': 'send-key',
   'data': { 'keys': ['KeyValue'], '*hold-time': 'int' } }
@@ -1265,8 +1271,6 @@
 #
 # @events: List of `InputEvent` union.
 #
-# Since: 2.6
-#
 # .. note:: The consoles are visible in the qom tree, under
 #    ``/backend/console[$index]``.  They have a device link and head
 #    property, so it is possible to map which console belongs to which
@@ -1308,6 +1312,8 @@
 #                    { "type": "abs", "data" : { "axis": "x", "value" : 20000 } },
 #                    { "type": "abs", "data" : { "axis": "y", "value" : 400 } } ] } }
 #     <- { "return": {} }
+#
+# Since: 2.6
 ##
 { 'command': 'input-send-event',
   'data': { '*device': 'str',
@@ -1620,13 +1626,15 @@
 #
 # Reload display configuration.
 #
-# Since: 6.0
+# Details:
 #
 # .. qmp-example::
 #
 #     -> { "execute": "display-reload",
 #          "arguments": { "type": "vnc", "tls-certs": true  } }
 #     <- { "return": {} }
+#
+# Since: 6.0
 ##
 { 'command': 'display-reload',
   'data': 'DisplayReloadOptions',
@@ -1677,7 +1685,7 @@
 #
 # Update display configuration.
 #
-# Since: 7.1
+# Details:
 #
 # .. qmp-example::
 #
@@ -1686,6 +1694,8 @@
 #                         [ { "type": "inet", "host": "0.0.0.0",
 #                             "port": "5901" } ] } }
 #     <- { "return": {} }
+#
+# Since: 7.1
 ##
 { 'command': 'display-update',
   'data': 'DisplayUpdateOptions',
@@ -1708,8 +1718,6 @@
 #
 # @cert-subject: server certificate subject
 #
-# Since: 0.14
-#
 # .. qmp-example::
 #
 #     -> { "execute": "client_migrate_info",
@@ -1717,6 +1725,8 @@
 #                         "hostname": "virt42.lab.kraxel.org",
 #                         "port": 1234 } }
 #     <- { "return": {} }
+#
+# Since: 0.14
 ##
 { 'command': 'client_migrate_info',
   'data': { 'protocol': 'str', 'hostname': 'str', '*port': 'int',
diff --git a/qapi/vfio.json b/qapi/vfio.json
index 17b60468712..630e2595c82 100644
--- a/qapi/vfio.json
+++ b/qapi/vfio.json
@@ -58,8 +58,6 @@
 #
 # @device-state: The new changed device migration state.
 #
-# Since: 9.1
-#
 # .. qmp-example::
 #
 #     <- { "timestamp": { "seconds": 1713771323, "microseconds": 212268 },
@@ -68,6 +66,8 @@
 #              "device-id": "vfio_dev1",
 #              "qom-path": "/machine/peripheral/vfio_dev1",
 #              "device-state": "stop" } }
+#
+# Since: 9.1
 ##
 { 'event': 'VFIO_MIGRATION',
   'data': {
diff --git a/qapi/virtio.json b/qapi/virtio.json
index 447fc182625..ae795e1e852 100644
--- a/qapi/virtio.json
+++ b/qapi/virtio.json
@@ -34,8 +34,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #
 #     -> { "execute": "x-query-virtio" }
@@ -62,6 +60,8 @@
 #              }
 #          ]
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio',
   'returns': [ 'VirtioInfo' ],
@@ -203,8 +203,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -437,6 +435,8 @@
 #              "use-started": true
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-status',
   'data': { 'path': 'str' },
@@ -579,8 +579,6 @@
 #    shadow_avail_idx will not be displayed in the case where the
 #    selected VirtIODevice has a running vhost device.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :annotated:
 #
@@ -635,6 +633,8 @@
 #              "vring-num": 128
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-queue-status',
   'data': { 'path': 'str', 'queue': 'uint16' },
@@ -707,8 +707,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :title: Get vhost_virtqueue status for vhost-crypto
 #
@@ -756,6 +754,8 @@
 #              "kick": 0
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-vhost-queue-status',
   'data': { 'path': 'str', 'queue': 'uint16' },
@@ -854,8 +854,6 @@
 #
 # @unstable: This command is meant for debugging.
 #
-# Since: 7.2
-#
 # .. qmp-example::
 #    :title: Introspect on virtio-net's VirtQueue 0 at index 5
 #
@@ -943,6 +941,8 @@
 #              }
 #          }
 #        }
+#
+# Since: 7.2
 ##
 { 'command': 'x-query-virtio-queue-element',
   'data': { 'path': 'str', 'queue': 'uint16', '*index': 'uint16' },
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH v2 10/10] qapi: enforce strict positioning for "Since:" section
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (8 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 09/10] qapi: re-order 'since' sections to always be last John Snow
@ 2026-04-08  4:55 ` John Snow
  2026-04-15  9:43 ` [PATCH v2 00/10] qapi: enforce section ordering Markus Armbruster
  10 siblings, 0 replies; 12+ messages in thread
From: John Snow @ 2026-04-08  4:55 UTC (permalink / raw)
  To: qemu-devel
  Cc: Kashyap Chamarthy, Stefan Berger, Mauro Carvalho Chehab,
	Michael Roth, Philippe Mathieu-Daudé, qemu-block,
	Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Markus Armbruster, John Snow, Ani Sinha,
	Marcel Apfelbaum

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 6e91cd19e58..4e8692ca0b1 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -554,7 +554,7 @@ def _tag_check(this: Union['QAPIDoc.Kind', str]) -> None:
                 if isinstance(this, str):
                     this = QAPIDoc.Kind.from_string(this)
 
-                if this in (QAPIDoc.Kind.TODO, QAPIDoc.Kind.SINCE):
+                if this == QAPIDoc.Kind.TODO:
                     return
 
                 if this.value < last_ordered_section.value:
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCH v2 00/10] qapi: enforce section ordering
  2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
                   ` (9 preceding siblings ...)
  2026-04-08  4:55 ` [PATCH v2 10/10] qapi: enforce strict positioning for "Since:" section John Snow
@ 2026-04-15  9:43 ` Markus Armbruster
  10 siblings, 0 replies; 12+ messages in thread
From: Markus Armbruster @ 2026-04-15  9:43 UTC (permalink / raw)
  To: John Snow
  Cc: qemu-devel, Kashyap Chamarthy, Stefan Berger,
	Mauro Carvalho Chehab, Michael Roth, Philippe Mathieu-Daudé,
	qemu-block, Pierrick Bouvier, Yanan Wang, Hanna Reitz, Peter Xu,
	Igor Mammedov, Michael S. Tsirkin, Kevin Wolf,
	Marc-André Lureau, Stefano Garzarella,
	Daniel P. Berrangé, Lukas Straub, Jason Wang,
	Alex Williamson, Paolo Bonzini, Fabiano Rosas, Zhao Liu,
	Richard Henderson, Cédric Le Goater, Stefan Hajnoczi,
	Peter Maydell, Eric Blake, Alex Bennée, Kostiantyn Kostiuk,
	Jiri Pirko, Ani Sinha, Marcel Apfelbaum

John Snow <jsnow@redhat.com> writes:

> Hiya, this series is meant to accomplish mostly one thing: Enforce a
> stricter ordering of sections in QAPI documentation blocks.
>
> The reason to do this is mostly for the sake of the inliner: if QAPI
> documentation blocks have some known, canonical order, it is easier to
> merge two documentation blocks together for the purposes of showing all
> arguments for commands/etc in a simple, flat list without needing to
> follow hyperlink breadcrumbs.
>
> Another reason to do this is to simplify where we insert autogenerated
> documentation. If the order is enforced, then inserting "Not Documented"
> stubs for members and generated "Returns:" statements can have a much
> simpler algorithm that will always match how manually written
> documentation is presented, in the same order.
>
> This is still pretty RFC quality, the tests have not been implemented
> and the implementation of changes in the parser are still pretty
> fuzzy. The main point of this series at this point in time is to review
> the QAPI source changes and decide if the strategy employed in fixing
> the section ordering is the direction we ultimately want to go in.
>
> V2:
>  - Add quite a few FIXME stubs for tests
>  - Much more carefully delineate QAPI source changes into ones required
>    to prevent visible changes, and ones that explictly create visible
>    changes
>  - Various commit message / comment changes
>  - Fix heuristic for griping about Intro/Details "ambiguity" to also
>    ignore generated "Returns" sections, which was missing before and
>    missed quite a few cases that did impact rendered output
>
> To verify rendering changes (or lack thereof), I used this strategy:
>
> (1) For a reference output before a change, I ran a build:
>     > V=1 DEBUG=1 make -j13;
>
> (2) Then I created some reference output for the intermediate rST
>     debugging output files (fish syntax):
>     > for i in *.ir; sed -E 's|\.json:[0-9]{4}|.json:nnnn|g' $i > $i.ref; end
>
> (3) Then after applying a patch, to check for any differences, I re-ran
>     the build as in (1) and then:
>     > for i in *.ir; sed -E 's|\.json:[0-9]{4}|.json:nnnn|g' $i > $i.new; end
>     > for i in *.ir; meld $i.ref $i.new; end
>
> An observation: Most of the time, the Intro section is only one
> paragraph anyway. We might be able to save on some explicit "Details:"
> syntax if we just formalize the idea that the intro can only ever be at
> most one paragraph. I don't know if we want to do that (Do we want to
> keep the ability to run long in the "intro"?) - but it would cut down on
> quite a lot of markup that this series adds.

The series adds 59 "Details:" markers to ~1150 doc comments, i.e. about
one in twenty doc comments needs one.

I can see several ways to skin this cat:

1. Intro ends when something else begins

   Intro can be any number of paragraphs.

   When intro is followed by details, we need a way to mark the
   boundary.  That's your "Details:" marker.  Relatively rare.

   Drawback: since you need "Details:" only rarely, it is easy to
   forget.  Details are then mistaken for intro or the other way round.
   The inliner will lose details / fail to lose intro.

   This is what your series gives us.

2. Intro ends after the first paragraph if we have one.

   Intro can be empty or one paragraph.

   When empty intro is followed by details, we need a way to mark the
   boundary, such as "Details:".  This is quite rare.

   Drawback: "at most one paragraph" is not obvious and easy to forget.
   Extra intro paragraphs end up in details then, where the inliner will
   copy them.

   Drawback: since you need "Details:" almost never, it is easy to
   forget.  The first paragraph of Details is mistaken for intro then.
   The inliner will lose it.

3. Make the end of intro syntactically obvious always

3a. Require the "Details:" line

   Intro can be any number of paragraphs.

   Drawback: the syntax is rather clumsy.  ~270 doc comments have
   details, and every one of them needs a "Details:" line.

   Drawback: you can still forget the "Details:" line.  If you do, the
   entire details will be mistaken for intro, and the inliner will lose
   them.

3b. Require a "Details:" tag

   Like 3a, but with slightly less clumsy syntax.  Instead of

      # @Frob:
      #
      # Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
      # eiusmod tempor incididunt ut labore et dolore magna aliqua.
      #
      # Details:
      #
      # Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      # nisi ut aliquip ex ea commodo consequat.
      #
      # Duis aute irure dolor in reprehenderit in voluptate velit esse
      # cillum dolore eu fugiat nulla pariatur.

   we get

      # @Frob:
      #
      # Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
      # eiusmod tempor incididunt ut labore et dolore magna aliqua.
      #
      # Details: Ut enim ad minim veniam, quis nostrud exercitation ullamco
      #     laboris nisi ut aliquip ex ea commodo consequat.
      #
      #     Duis aute irure dolor in reprehenderit in voluptate velit esse
      #     cillum dolore eu fugiat nulla pariatur.

   Drawback: the syntax is still clumsy.  ~270 doc comments have
   details, and every one of them needs a "Details:" tag and be
   reindented.

   Drawback: if you forget to tag details, they get mistaken for intro,
   and the inliner will lose them.  Less likely than with 3a?

3c. Indent intro like descriptions and tagged sections.

   Like so:

      # @Frob:
      #     Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
      #     eiusmod tempor incididunt ut labore et dolore magna aliqua.
      #
      # Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      # nisi ut aliquip ex ea commodo consequat.
      #
      # Duis aute irure dolor in reprehenderit in voluptate velit esse
      # cillum dolore eu fugiat nulla pariatur.

   Drawback: all the intros get reindented.

   Drawback: if you format intro the old way, it gets mistaken for
   details, and the inliner will fail to lose it.  Likely to happen
   initially, but hopefully becomes unlikely once people got used to it.

   Anything else?

3d. Both 3b and 3c

   Intros and details written the old way get rejected.

   Drawback: all intros and all details need to be reindented.

   Anything else?

Thoughts?



^ permalink raw reply	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2026-04-15  9:45 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-08  4:55 [PATCH v2 00/10] qapi: enforce section ordering John Snow
2026-04-08  4:55 ` [PATCH v2 01/10] qapi: differentiate "intro" and "details" sections John Snow
2026-04-08  4:55 ` [PATCH v2 02/10] qapi: prohibit 'details' sections between tagged sections John Snow
2026-04-08  4:55 ` [PATCH v2 03/10] qapi: add "Details:" disambiguation marker John Snow
2026-04-08  4:55 ` [PATCH v2 04/10] qapi: add "Details:" markers where needed John Snow
2026-04-08  4:55 ` [PATCH v2 05/10] qapi: add "Details:" markers where potentially needed John Snow
2026-04-08  4:55 ` [PATCH v2 06/10] qapi: detect potentially semantically ambiguous intro paragraphs John Snow
2026-04-08  4:55 ` [PATCH v2 07/10] qapi: re-order QAPI doc block sections John Snow
2026-04-08  4:55 ` [PATCH v2 08/10] qapi: enforce doc block section ordering John Snow
2026-04-08  4:55 ` [PATCH v2 09/10] qapi: re-order 'since' sections to always be last John Snow
2026-04-08  4:55 ` [PATCH v2 10/10] qapi: enforce strict positioning for "Since:" section John Snow
2026-04-15  9:43 ` [PATCH v2 00/10] qapi: enforce section ordering Markus Armbruster

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.