From 409e3a89ad29a2320ca5fb3c1e49389e2e50cb97 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 13 May 2026 13:40:39 +0200 Subject: [PATCH 01/23] Add 'generic' parameter to writeTreeXSD() When generic=true, oneNodeGroup uses xs:any processContents="lax" instead of a closed xs:choice, allowing unknown custom node elements to pass validation. Top-level xs:element declarations are also emitted so that lax processing can still resolve and validate the known built-in node types by name. Co-Authored-By: Claude Sonnet 4.6 --- include/behaviortree_cpp/xml_parsing.h | 15 ++++++--- src/xml_parsing.cpp | 42 +++++++++++++++++++++----- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/include/behaviortree_cpp/xml_parsing.h b/include/behaviortree_cpp/xml_parsing.h index d815ef85b..0ddb684ec 100644 --- a/include/behaviortree_cpp/xml_parsing.h +++ b/include/behaviortree_cpp/xml_parsing.h @@ -59,13 +59,20 @@ void VerifyXML(const std::string& xml_text, bool include_builtin = false); /** - * @brief writeTreeXSD generates an XSD for the nodes defined in the factory + * @brief writeTreeXSD generates an XSD for the nodes defined in the factory. * - * @param factory the factory with the registered types + * @param factory the factory with the registered types + * @param generic if true, oneNodeGroup uses xs:any processContents="lax" + * instead of a closed xs:choice, allowing unknown custom + * node elements to pass validation. Top-level xs:element + * declarations are also emitted so that lax processing can + * still resolve and validate the known built-in node types. + * Defaults to false (strict, closed-vocabulary schema). * - * @return string containing the XML. + * @return string containing the XSD XML. */ -[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory); +[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory, + bool generic = false); /** * @brief WriteTreeToXML create a string that contains the XML that corresponds to a given tree. diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index b4a82e976..9c92c4513 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1473,7 +1473,7 @@ std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory, return std::string(printer.CStr(), size_t(printer.CStrSize() - 1)); } -std::string writeTreeXSD(const BehaviorTreeFactory& factory) +std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) { // There are 2 forms of representation for a node: // compact: and explicit: @@ -1635,15 +1635,27 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory) XMLElement* one_node_group = doc.NewElement("xs:group"); { one_node_group->SetAttribute("name", "oneNodeGroup"); - std::ostringstream xsd; - xsd << ""; - for(const auto& [registration_id, model] : ordered_models) + if(generic) + { + // Generic: accept any element; lax processing validates known built-ins + // via the top-level xs:element declarations emitted at the end of this function. + parse_and_insert(one_node_group, "" + "" + ""); + } + else { - xsd << ""; + std::ostringstream xsd; + xsd << ""; + for(const auto& [registration_id, model] : ordered_models) + { + xsd << ""; + } + xsd << ""; + parse_and_insert(one_node_group, xsd.str().c_str()); } - xsd << ""; - parse_and_insert(one_node_group, xsd.str().c_str()); schema_element->InsertEndChild(one_node_group); } @@ -1739,6 +1751,20 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory) schema_element->InsertEndChild(type); } + // Generic mode: emit top-level xs:element declarations so that + // processContents="lax" in oneNodeGroup can resolve and validate + // known built-in node elements by name. + if(generic) + { + for(const auto& [registration_id, model] : ordered_models) + { + XMLElement* elem = doc.NewElement("xs:element"); + elem->SetAttribute("name", registration_id.c_str()); + elem->SetAttribute("type", (registration_id + "Type").c_str()); + schema_element->InsertEndChild(elem); + } + } + XMLPrinter printer; doc.Print(&printer); return std::string(printer.CStr(), size_t(printer.CStrSize() - 1)); From 3a3c9e1398218f2de89411f560c410a36540272c Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 13 May 2026 13:40:39 +0200 Subject: [PATCH 02/23] Add generate_xsd tool Generates the generic XSD schema from the default factory (built-in nodes only) and prints it to stdout, suitable for use as a generic static validator that stays in sync with the built-in node inventory. Co-Authored-By: Claude Sonnet 4.6 --- tools/CMakeLists.txt | 5 +++++ tools/generate_xsd.cpp | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tools/generate_xsd.cpp diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index e5d3f4219..7e5adbb61 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -29,3 +29,8 @@ add_executable(bt4_nodes_model bt_nodes_model.cpp ) target_link_libraries(bt4_nodes_model ${BTCPP_LIBRARY} ) install(TARGETS bt4_nodes_model DESTINATION ${BTCPP_BIN_DESTINATION} ) + +add_executable(bt4_generate_xsd generate_xsd.cpp ) +target_link_libraries(bt4_generate_xsd ${BTCPP_LIBRARY} ) +install(TARGETS bt4_generate_xsd + DESTINATION ${BTCPP_BIN_DESTINATION} ) diff --git a/tools/generate_xsd.cpp b/tools/generate_xsd.cpp new file mode 100644 index 000000000..9a9ed77b0 --- /dev/null +++ b/tools/generate_xsd.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include + +// Generates a generic XSD schema for BehaviorTree.CPP XML files and +// prints it to stdout. +// +// The schema covers all built-in node types with their specific attributes +// and child-count constraints. Unknown custom node elements are accepted via +// xs:any processContents="lax", and top-level xs:element declarations allow +// lax processing to still validate built-in node elements by name. +// +// Usage: +// ./bt4_generate_xsd > btcpp4.xsd +// xmllint --noout --schema btcpp4.xsd myfile.xml +int main() +{ + BT::BehaviorTreeFactory factory; + std::cout << BT::writeTreeXSD(factory, /*generic=*/true); + return 0; +} From ee6699d0df6d6ae2248b6bf85d126b939a02b757 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 13 May 2026 13:51:40 +0200 Subject: [PATCH 03/23] Fix non-deterministic port attribute order in writeTreeXSD() PortsList is an unordered_map so iterating it directly produced XSD attributes in hash-table order, which varies across runs and platforms. Sort ports into a std::map before emitting xs:attribute elements. Co-Authored-By: Claude Sonnet 4.6 --- src/xml_parsing.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 9c92c4513..484ded127 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1722,8 +1722,12 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) XMLElement* common_attr_group = doc.NewElement("xs:attributeGroup"); common_attr_group->SetAttribute("ref", "commonAttributeGroup"); type->InsertEndChild(common_attr_group); + std::map ordered_ports; for(const auto& [port_name, port_info] : model->ports) + ordered_ports.insert({ port_name, &port_info }); + for(const auto& [port_name, port_info_ptr] : ordered_ports) { + const auto& port_info = *port_info_ptr; XMLElement* attr = doc.NewElement("xs:attribute"); attr->SetAttribute("name", port_name.c_str()); const auto xsd_attribute_type = xsdAttributeType(port_info); From 0ae3219afedc47bf1f6dc2b688063b87c4c6150c Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Tue, 26 May 2026 11:19:44 +0200 Subject: [PATCH 04/23] Add writeTreeSchematron() for cross-reference validation of BT XML files XSD alone cannot express constraints like "every custom node element used in a BehaviorTree body must have a matching TreeNodesModel entry" Co-Authored-By: Claude Sonnet 4.6 --- CMakeLists.txt | 10 ++++ include/behaviortree_cpp/xml_parsing.h | 23 ++++++++ src/btcpp4_schematron.sch | 79 ++++++++++++++++++++++++++ src/schematron_template.h.in | 7 +++ src/xml_parsing.cpp | 21 +++++++ 5 files changed, 140 insertions(+) create mode 100644 src/btcpp4_schematron.sch create mode 100644 src/schematron_template.h.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 416c2c332..1fef1aa3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -210,6 +210,15 @@ if (WIN32) list(APPEND BT_SOURCE src/shared_library_WIN.cpp ) endif() +# Embed src/btcpp4_schematron.sch as a C++ string constant. +# configure_file tracks the input file: CMake reruns automatically when it changes. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/btcpp4_schematron.sch" BTCPP_SCHEMATRON_TEMPLATE) +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/src/schematron_template.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/generated/schematron_template.gen.h" + @ONLY +) + if (BTCPP_SHARED_LIBS) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) add_library(${BTCPP_LIBRARY} SHARED ${BT_SOURCE}) @@ -252,6 +261,7 @@ target_include_directories(${BTCPP_LIBRARY} $ PRIVATE ${BTCPP_EXTRA_INCLUDE_DIRS} + ${CMAKE_CURRENT_BINARY_DIR}/generated ) target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_LIBRARY_VERSION="${CMAKE_PROJECT_VERSION}") diff --git a/include/behaviortree_cpp/xml_parsing.h b/include/behaviortree_cpp/xml_parsing.h index 0ddb684ec..3342cf7a9 100644 --- a/include/behaviortree_cpp/xml_parsing.h +++ b/include/behaviortree_cpp/xml_parsing.h @@ -74,6 +74,29 @@ void VerifyXML(const std::string& xml_text, [[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic = false); +/** + * @brief writeTreeSchematron generates an ISO Schematron schema for BehaviorTree.CPP XML files. + * + * XSD alone cannot express cross-reference constraints. This Schematron + * complements writeTreeXSD() with three rule patterns: + * - treeNodesModel: every custom (non-built-in) node element appearing in a + * BehaviorTree body must have a matching TreeNodesModel entry (required by + * Groot2 for port display and editing). + * - subtreeResolution: every must resolve to a + * in the same file (relaxed when is present). + * - rootStructure: main_tree_to_execute must name an existing BehaviorTree. + * + * The built-in node list is derived from factory.builtinNodes() so that the + * schema stays in sync as new built-ins are added. + * + * @param factory factory from which the built-in node names are taken; + * a default-constructed BehaviorTreeFactory suffices for most uses. + * + * @return string containing the Schematron XML (queryBinding="xslt", + * compatible with xsltproc and lxml.isoschematron). + */ +[[nodiscard]] std::string writeTreeSchematron(const BehaviorTreeFactory& factory); + /** * @brief WriteTreeToXML create a string that contains the XML that corresponds to a given tree. * When using this function with a logger, you should probably set both add_metadata and diff --git a/src/btcpp4_schematron.sch b/src/btcpp4_schematron.sch new file mode 100644 index 000000000..21aacc651 --- /dev/null +++ b/src/btcpp4_schematron.sch @@ -0,0 +1,79 @@ + + + BehaviorTree.CPP XML validation rules + + + + TreeNodesModel completeness + + + + + Custom node '' used in tree + '' + has no <TreeNodesModel> entry. + + + + + + + Node with ID='' used in tree + '' + has no <TreeNodesModel> entry. + + + + + + + SubTree ID cross-references + + + SubTree ID='' is not defined in this file. + (If the definition lives in an included file, add an <include>.) + + + + + + Root element consistency + + + main_tree_to_execute='' + does not match any BehaviorTree ID in this file. + + + + + diff --git a/src/schematron_template.h.in b/src/schematron_template.h.in new file mode 100644 index 000000000..972943419 --- /dev/null +++ b/src/schematron_template.h.in @@ -0,0 +1,7 @@ +// Auto-generated from src/btcpp4_schematron.sch — do not edit directly. +// To update: edit src/btcpp4_schematron.sch and re-run CMake. +namespace BT { namespace detail { +static const char* const schematron_template = +R"BTCPP4SCH(@BTCPP_SCHEMATRON_TEMPLATE@)BTCPP4SCH"; +} // namespace detail +} // namespace BT diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 484ded127..df69eb3b9 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -53,6 +53,8 @@ #include #endif +#include "schematron_template.gen.h" + #include "behaviortree_cpp/blackboard.h" #include "behaviortree_cpp/tree_node.h" #include "behaviortree_cpp/utils/demangle_util.h" @@ -1774,6 +1776,25 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) return std::string(printer.CStr(), size_t(printer.CStrSize() - 1)); } +std::string writeTreeSchematron(const BehaviorTreeFactory& factory) +{ + // Build the pipe-delimited builtin node list, e.g. "|AlwaysFailure|Fallback|...|", + // then substitute the ##BUILTIN_PIPE## placeholder in the embedded template. + // The template itself lives in src/btcpp4_schematron.sch (human-readable); + // schematron_template.gen.h is generated from it by CMake. + std::ostringstream builtin_pipe_ss; + builtin_pipe_ss << "|"; + for(const auto& name : factory.builtinNodes()) + builtin_pipe_ss << name << "|"; + + std::string result = BT::detail::schematron_template; + const std::string placeholder = "##BUILTIN_PIPE##"; + const auto pos = result.find(placeholder); + if(pos != std::string::npos) + result.replace(pos, placeholder.size(), builtin_pipe_ss.str()); + return result; +} + std::string WriteTreeToXML(const Tree& tree, bool add_metadata, bool add_builtin_models) { XMLDocument doc; From 3506f11f05ef5ef4002083691ae5dd24d24ea627 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Tue, 26 May 2026 11:25:51 +0200 Subject: [PATCH 05/23] Add generate_schematron tool Generates an ISO Schematron schema (btcpp4.sch) complementing the generic XSD Co-Authored-By: Claude Sonnet 4.6 --- tools/CMakeLists.txt | 5 ++++ tools/generate_schematron.cpp | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tools/generate_schematron.cpp diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 7e5adbb61..d822898b7 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -34,3 +34,8 @@ add_executable(bt4_generate_xsd generate_xsd.cpp ) target_link_libraries(bt4_generate_xsd ${BTCPP_LIBRARY} ) install(TARGETS bt4_generate_xsd DESTINATION ${BTCPP_BIN_DESTINATION} ) + +add_executable(bt4_generate_schematron generate_schematron.cpp ) +target_link_libraries(bt4_generate_schematron ${BTCPP_LIBRARY} ) +install(TARGETS bt4_generate_schematron + DESTINATION ${BTCPP_BIN_DESTINATION} ) diff --git a/tools/generate_schematron.cpp b/tools/generate_schematron.cpp new file mode 100644 index 000000000..40f61f6d9 --- /dev/null +++ b/tools/generate_schematron.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include + +// Generates an ISO Schematron schema for BehaviorTree.CPP XML files and +// prints it to stdout. +// +// The Schematron complements the generic XSD (bt4_generate_xsd) with +// cross-reference rules that XSD cannot express: +// +// - treeNodesModel: every custom (non-built-in) node used in a BehaviorTree +// body must have a matching entry (required by Groot2). +// - subtreeResolution: every must resolve to a +// in the same file. +// - rootStructure: main_tree_to_execute must name an existing BehaviorTree. +// +// Usage (two-step validation): +// +// # Generate schemas +// ./bt4_generate_xsd > btcpp4.xsd +// ./bt4_generate_schematron > btcpp4.sch +// +// # Step 1 — structural validation (XSD) +// xmllint --noout --schema btcpp4.xsd myfile.xml +// +// # Step 2 — cross-reference validation (Schematron) +// # Option A: xsltproc + ISO Schematron XSLT1 skeleton +// # (download iso_schematron_skeleton_for_xslt1.xsl from +// # https://github.com/Schematron/schematron) +// xsltproc iso_schematron_skeleton_for_xslt1.xsl btcpp4.sch > btcpp4_validator.xsl +// xsltproc btcpp4_validator.xsl myfile.xml +// +// # Option B: Python + lxml (no XSLT skeleton needed) +// pip install lxml +// python3 - << 'EOF' +// from lxml import etree, isoschematron +// ns = "http://purl.oclc.org/dsdl/svrl" +// sch = isoschematron.Schematron(etree.parse("btcpp4.sch"), store_report=True) +// if not sch.validate(etree.parse("myfile.xml")): +// for fa in sch.validation_report.findall(f".//{{{ns}}}failed-assert"): +// print(f"{fa.get('location')}: {fa.findtext(f'{{{ns}}}text', '').strip()}") +// EOF +// +// # Option C: Java + SchXslt +// java -jar schxslt-cli.jar -s btcpp4.sch -i myfile.xml +int main() +{ + BT::BehaviorTreeFactory factory; + std::cout << BT::writeTreeSchematron(factory); + return 0; +} From ccaf48e0c07e761e6293273af43047647ef5453d Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Tue, 26 May 2026 13:17:58 +0200 Subject: [PATCH 06/23] Add generated XSD and Schematron schemas --- tools/generated/bt4.sch | 79 +++++++++ tools/generated/bt4.xsd | 374 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 tools/generated/bt4.sch create mode 100644 tools/generated/bt4.xsd diff --git a/tools/generated/bt4.sch b/tools/generated/bt4.sch new file mode 100644 index 000000000..ff58d4749 --- /dev/null +++ b/tools/generated/bt4.sch @@ -0,0 +1,79 @@ + + + BehaviorTree.CPP XML validation rules + + + + TreeNodesModel completeness + + + + + Custom node '' used in tree + '' + has no <TreeNodesModel> entry. + + + + + + + Node with ID='' used in tree + '' + has no <TreeNodesModel> entry. + + + + + + + SubTree ID cross-references + + + SubTree ID='' is not defined in this file. + (If the definition lives in an included file, add an <include>.) + + + + + + Root element consistency + + + main_tree_to_execute='' + does not match any BehaviorTree ID in this file. + + + + + diff --git a/tools/generated/bt4.xsd b/tools/generated/bt4.xsd new file mode 100644 index 000000000..6daf42649 --- /dev/null +++ b/tools/generated/bt4.xsd @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f4412efc6aea18b9db42cb65265eb60b568971e0 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Tue, 26 May 2026 13:49:29 +0200 Subject: [PATCH 07/23] Add validate_xml.py - XSD and Schematron validation tool Co-Authored-By: Claude Sonnet 4.6 --- tools/validate_xml.py | 121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 tools/validate_xml.py diff --git a/tools/validate_xml.py b/tools/validate_xml.py new file mode 100755 index 000000000..5df3bb81f --- /dev/null +++ b/tools/validate_xml.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Validate a BehaviorTree.CPP XML file against XSD and/or Schematron schemas. + +Exit codes: + 0 all enabled validations passed + 1 one or more validation errors + 2 usage error or file not found +""" + +import argparse +import shutil +import subprocess +import sys +from pathlib import Path + +try: + from lxml import etree, isoschematron + _LXML = True +except ImportError: + _LXML = False + +_SCRIPT_DIR = Path(__file__).resolve().parent +_DEFAULT_XSD = _SCRIPT_DIR / "generated" / "bt4.xsd" +_DEFAULT_SCH = _SCRIPT_DIR / "generated" / "bt4.sch" + + +def validate_xsd(xml_file: str, schema_file: str, force_lxml: bool = False) -> bool: + if not force_lxml and shutil.which("xmllint"): + result = subprocess.run( + ["xmllint", "--noout", "--schema", schema_file, xml_file], + capture_output=True, text=True, + ) + if result.returncode != 0: + sys.stderr.write(result.stderr) + return False + return True + + if not _LXML: + print("error: neither xmllint nor lxml is available for XSD validation", file=sys.stderr) + sys.exit(2) + + schema = etree.XMLSchema(etree.parse(schema_file)) + doc = etree.parse(xml_file) + if not schema.validate(doc): + for err in schema.error_log: + print(f"{xml_file}:{err.line}: {err.message}", file=sys.stderr) + return False + return True + + +def validate_schematron(xml_file: str, sch_file: str) -> bool: + if not _LXML: + print("error: lxml is required for Schematron validation (pip install lxml)", file=sys.stderr) + sys.exit(2) + + sch = isoschematron.Schematron(etree.parse(sch_file), store_report=True) + ok = sch.validate(etree.parse(xml_file)) + if not ok: + ns = "http://purl.oclc.org/dsdl/svrl" + for fa in sch.validation_report.findall(f".//{{{ns}}}failed-assert"): + loc = fa.get("location") + text = fa.findtext(f"{{{ns}}}text", "").strip() + print(f"{xml_file}: {loc}: {text}", file=sys.stderr) + return ok + + +def main(): + parser = argparse.ArgumentParser( + description="Validate a BehaviorTree.CPP XML file against XSD and Schematron schemas.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +examples: + %(prog)s myfile.xml + %(prog)s --schema custom.xsd --schematron custom.sch myfile.xml + %(prog)s --no-xsd myfile.xml + %(prog)s --no-sch myfile.xml +""", + ) + parser.add_argument("xml_file", help="BehaviorTree XML file to validate") + parser.add_argument( + "-s", "--schema", + default=str(_DEFAULT_XSD), + metavar="FILE", + help=f"XSD schema (default: {_DEFAULT_XSD})", + ) + parser.add_argument( + "-t", "--schematron", + default=str(_DEFAULT_SCH), + metavar="FILE", + help=f"Schematron schema (default: {_DEFAULT_SCH})", + ) + parser.add_argument("--no-xsd", action="store_true", help="skip XSD validation") + parser.add_argument("--no-sch", action="store_true", help="skip Schematron validation") + parser.add_argument("--lxml", action="store_true", help="force use of lxml for XSD validation") + + args = parser.parse_args() + + if not Path(args.xml_file).exists(): + parser.error(f"file not found: {args.xml_file}") + + passed = True + + if not args.no_xsd: + if not Path(args.schema).exists(): + parser.error(f"XSD schema not found: {args.schema}") + if not validate_xsd(args.xml_file, args.schema, force_lxml=args.lxml): + passed = False + + if not args.no_sch: + if not Path(args.schematron).exists(): + parser.error(f"Schematron schema not found: {args.schematron}") + if not validate_schematron(args.xml_file, args.schematron): + passed = False + + if passed: + print(f"{args.xml_file} validates OK") + sys.exit(0 if passed else 1) + + +if __name__ == "__main__": + main() From 5fabd280fb47dab777c893a64d60ef041ae18c64 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 27 May 2026 08:47:45 +0200 Subject: [PATCH 08/23] Only built-ins should go into generic xsd file --- src/xml_parsing.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index df69eb3b9..07c26a2c4 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1483,10 +1483,12 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) // make sense with XSD since we would need to allow any attribute. // Prepare the data + const auto& builtin_set = factory.builtinNodes(); std::map ordered_models; for(const auto& [registration_id, model] : factory.manifests()) { - ordered_models.insert({ registration_id, &model }); + if(!generic || builtin_set.count(registration_id)) + ordered_models.insert({ registration_id, &model }); } XMLDocument doc; From e1d967c1ea0e9e48622001a5408c917476e5a9d9 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 27 May 2026 08:49:27 +0200 Subject: [PATCH 09/23] Add tests for writeTreeXSD() and writeTreeSchematron() Co-Authored-By: Claude Sonnet 4.6 --- tests/gtest_factory.cpp | 109 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/gtest_factory.cpp b/tests/gtest_factory.cpp index 01718cbce..c90e22604 100644 --- a/tests/gtest_factory.cpp +++ b/tests/gtest_factory.cpp @@ -777,3 +777,112 @@ TEST(BehaviorTreeFactory, MalformedXML_UnknownNodeType) BehaviorTreeFactory factory; EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError); } + +// --------------------------------------------------------------------------- +// writeTreeXSD tests +// --------------------------------------------------------------------------- + +TEST(WriteTreeXSD, WellFormedOutput) +{ + BehaviorTreeFactory factory; + auto xsd = writeTreeXSD(factory); + EXPECT_NE(xsd.find(""), std::string::npos); +} + +TEST(WriteTreeXSD, ContainsBuiltinNodes) +{ + BehaviorTreeFactory factory; + auto xsd = writeTreeXSD(factory); + EXPECT_NE(xsd.find("name=\"Sequence\""), std::string::npos); + EXPECT_NE(xsd.find("name=\"Fallback\""), std::string::npos); + EXPECT_NE(xsd.find("name=\"Inverter\""), std::string::npos); +} + +TEST(WriteTreeXSD, CustomNodeAppearsInOutput) +{ + BehaviorTreeFactory factory; + factory.registerNodeType("SaySomething"); + auto xsd = writeTreeXSD(factory); + EXPECT_NE(xsd.find("name=\"SaySomething\""), std::string::npos); + EXPECT_NE(xsd.find("name=\"message\""), std::string::npos); +} + +TEST(WriteTreeXSD, GenericModeUsesXsAny) +{ + BehaviorTreeFactory factory; + auto xsd_strict = writeTreeXSD(factory, false); + auto xsd_generic = writeTreeXSD(factory, true); + EXPECT_EQ(xsd_strict.find("processContents=\"lax\""), std::string::npos); + EXPECT_NE(xsd_generic.find("processContents=\"lax\""), std::string::npos); +} + +TEST(WriteTreeXSD, GenericModeExcludesCustomNodes) +{ + BehaviorTreeFactory factory; + factory.registerNodeType("SaySomething"); + auto xsd_strict = writeTreeXSD(factory, false); + auto xsd_generic = writeTreeXSD(factory, true); + // Custom nodes must appear in strict but not in generic (generic accepts them via xs:any) + EXPECT_NE(xsd_strict.find("SaySomething"), std::string::npos); + EXPECT_EQ(xsd_generic.find("SaySomething"), std::string::npos); +} + +TEST(WriteTreeXSD, Deterministic) +{ + BehaviorTreeFactory factory; + factory.registerNodeType("SaySomething"); + auto xsd1 = writeTreeXSD(factory); + auto xsd2 = writeTreeXSD(factory); + EXPECT_EQ(xsd1, xsd2); +} + +// --------------------------------------------------------------------------- +// writeTreeSchematron tests +// --------------------------------------------------------------------------- + +TEST(WriteTreeSchematron, WellFormedOutput) +{ + BehaviorTreeFactory factory; + auto sch = writeTreeSchematron(factory); + EXPECT_NE(sch.find(""), std::string::npos); +} + +TEST(WriteTreeSchematron, PlaceholderReplaced) +{ + BehaviorTreeFactory factory; + auto sch = writeTreeSchematron(factory); + EXPECT_EQ(sch.find("##BUILTIN_PIPE##"), std::string::npos); +} + +TEST(WriteTreeSchematron, BuiltinNodesInPipeList) +{ + BehaviorTreeFactory factory; + auto sch = writeTreeSchematron(factory); + EXPECT_NE(sch.find("|Sequence|"), std::string::npos); + EXPECT_NE(sch.find("|Fallback|"), std::string::npos); + EXPECT_NE(sch.find("|Inverter|"), std::string::npos); +} + +TEST(WriteTreeSchematron, ContainsAllPatternIds) +{ + BehaviorTreeFactory factory; + auto sch = writeTreeSchematron(factory); + EXPECT_NE(sch.find("id=\"treeNodesModel\""), std::string::npos); + EXPECT_NE(sch.find("id=\"subtreeResolution\""), std::string::npos); + EXPECT_NE(sch.find("id=\"rootStructure\""), std::string::npos); +} + +TEST(WriteTreeSchematron, CustomNodeNotInBuiltinPipe) +{ + BehaviorTreeFactory factory; + factory.registerNodeType("SaySomething"); + auto sch = writeTreeSchematron(factory); + // Custom nodes must not appear in the builtin pipe exclusion list + EXPECT_EQ(sch.find("|SaySomething|"), std::string::npos); + // Builtins must still be present + EXPECT_NE(sch.find("|Sequence|"), std::string::npos); +} From 4f60ec40efb6c7885d1ceef84fc4e68734c8db04 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 27 May 2026 09:23:10 +0200 Subject: [PATCH 10/23] Fix implicit conversion --- src/xml_parsing.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 07c26a2c4..c3ff18357 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1487,7 +1487,7 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) std::map ordered_models; for(const auto& [registration_id, model] : factory.manifests()) { - if(!generic || builtin_set.count(registration_id)) + if(!generic || builtin_set.count(registration_id) != 0) ordered_models.insert({ registration_id, &model }); } From 89b3a255bf430083be9b140d7dea5dbb501d3ddc Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Wed, 27 May 2026 16:18:36 +0200 Subject: [PATCH 11/23] Also handle inout_port in XSD --- src/xml_parsing.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index c3ff18357..ef6b8fdbf 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1569,6 +1569,15 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) + + + + + + + + + @@ -1602,6 +1611,7 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) + From b2289d6be3cfd79da7debc799fb0b3ae12dbb49b Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 08:58:07 +0200 Subject: [PATCH 12/23] Fix reconfiguration after editing src/btcpp4_schematron.sch --- CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 80b233cdb..5e73e91b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,7 +214,10 @@ if (WIN32) endif() # Embed src/btcpp4_schematron.sch as a C++ string constant. -# configure_file tracks the input file: CMake reruns automatically when it changes. +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/btcpp4_schematron.sch" +) + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/btcpp4_schematron.sch" BTCPP_SCHEMATRON_TEMPLATE) configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/src/schematron_template.h.in" From dc21d9d01bb12b4bd8700d384e65d717f9ceab49 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 09:14:29 +0200 Subject: [PATCH 13/23] Fix writeTreeXSD() signature not to break ABI --- include/behaviortree_cpp/xml_parsing.h | 13 ++++++++++--- src/xml_parsing.cpp | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/include/behaviortree_cpp/xml_parsing.h b/include/behaviortree_cpp/xml_parsing.h index 3342cf7a9..8fe9df500 100644 --- a/include/behaviortree_cpp/xml_parsing.h +++ b/include/behaviortree_cpp/xml_parsing.h @@ -58,6 +58,15 @@ void VerifyXML(const std::string& xml_text, [[nodiscard]] std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory, bool include_builtin = false); +/** + * @brief writeTreeXSD generates an XSD for the nodes defined in the factory. + * + * @param factory the factory with the registered types + * + * @return string containing the XSD XML (strict, closed-vocabulary schema). + */ +[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory); + /** * @brief writeTreeXSD generates an XSD for the nodes defined in the factory. * @@ -67,12 +76,10 @@ void VerifyXML(const std::string& xml_text, * node elements to pass validation. Top-level xs:element * declarations are also emitted so that lax processing can * still resolve and validate the known built-in node types. - * Defaults to false (strict, closed-vocabulary schema). * * @return string containing the XSD XML. */ -[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory, - bool generic = false); +[[nodiscard]] std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic); /** * @brief writeTreeSchematron generates an ISO Schematron schema for BehaviorTree.CPP XML files. diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index a8ab4edea..2abec5ea9 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1483,6 +1483,11 @@ std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory, return std::string(printer.CStr(), size_t(printer.CStrSize() - 1)); } +std::string writeTreeXSD(const BehaviorTreeFactory& factory) +{ + return writeTreeXSD(factory, false); +} + std::string writeTreeXSD(const BehaviorTreeFactory& factory, bool generic) { // There are 2 forms of representation for a node: From 84f4a73d0cb306829c964c51304a616ed168ee97 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 09:55:25 +0200 Subject: [PATCH 14/23] Change placeholder replacement logic for Schematron --- include/behaviortree_cpp/xml_parsing.h | 6 +++++ src/btcpp4_schematron.sch | 2 +- src/xml_parsing.cpp | 33 +++++++++++++++++++++++--- tests/gtest_factory.cpp | 16 +++++++++++-- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/include/behaviortree_cpp/xml_parsing.h b/include/behaviortree_cpp/xml_parsing.h index 8fe9df500..08eca56d8 100644 --- a/include/behaviortree_cpp/xml_parsing.h +++ b/include/behaviortree_cpp/xml_parsing.h @@ -118,6 +118,12 @@ void VerifyXML(const std::string& xml_text, [[nodiscard]] std::string WriteTreeToXML(const Tree& tree, bool add_metadata, bool add_builtin_models); +namespace detail +{ +/// Returns true when @p pos inside @p s falls within an XML comment (). +[[nodiscard]] bool insideXmlComment(const std::string& s, std::size_t pos); +} // namespace detail + } // namespace BT #endif // XML_PARSING_BT_H diff --git a/src/btcpp4_schematron.sch b/src/btcpp4_schematron.sch index 21aacc651..a0311659c 100644 --- a/src/btcpp4_schematron.sch +++ b/src/btcpp4_schematron.sch @@ -9,7 +9,7 @@ Built-in node types are excluded — they are always known to the library and do not need an explicit declaration. - \#\#BUILTIN_PIPE\#\# is a placeholder expanded at runtime by writeTreeSchematron() + ##BUILTIN_PIPE## is a placeholder expanded at runtime by writeTreeSchematron() with a pipe-delimited list of all built-in node names derived from the factory, e.g. |AlwaysFailure|AlwaysSuccess|Fallback|Sequence|...|. diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 2abec5ea9..d4a3527a7 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -1814,9 +1814,24 @@ std::string writeTreeSchematron(const BehaviorTreeFactory& factory) std::string result = BT::detail::schematron_template; const std::string placeholder = "##BUILTIN_PIPE##"; - const auto pos = result.find(placeholder); - if(pos != std::string::npos) - result.replace(pos, placeholder.size(), builtin_pipe_ss.str()); + const std::string builtin_pipe = builtin_pipe_ss.str(); + int replacements = 0; + + for(std::size_t pos = result.find(placeholder); pos != std::string::npos; + pos = result.find(placeholder, pos)) + { + if(detail::insideXmlComment(result, pos)) + { + pos += placeholder.size(); + continue; + } + result.replace(pos, placeholder.size(), builtin_pipe); + pos += builtin_pipe.size(); + ++replacements; + } + if(replacements == 0) + throw RuntimeError("writeTreeSchematron: placeholder '", placeholder, + "' not found outside comments in schematron template"); return result; } @@ -1835,4 +1850,16 @@ std::string WriteTreeToXML(const Tree& tree, bool add_metadata, bool add_builtin return std::string(printer.CStr(), size_t(printer.CStrSize() - 1)); } +namespace detail +{ +bool insideXmlComment(const std::string& s, std::size_t pos) +{ + const auto open = s.rfind("", pos); + return close == std::string::npos || close < open; +} +} // namespace detail + } // namespace BT diff --git a/tests/gtest_factory.cpp b/tests/gtest_factory.cpp index 64ce47336..9a47600ca 100644 --- a/tests/gtest_factory.cpp +++ b/tests/gtest_factory.cpp @@ -851,11 +851,23 @@ TEST(WriteTreeSchematron, WellFormedOutput) EXPECT_NE(sch.find(""), std::string::npos); } +TEST(InsideXmlComment, BasicCases) +{ + const std::string s = "before after"; + EXPECT_FALSE(BT::detail::insideXmlComment(s, 0)); // "before" + EXPECT_TRUE(BT::detail::insideXmlComment(s, 13)); // inside comment + EXPECT_FALSE(BT::detail::insideXmlComment(s, 24)); // "after" + EXPECT_FALSE(BT::detail::insideXmlComment("no comment", 3)); +} + TEST(WriteTreeSchematron, PlaceholderReplaced) { + // Every ##BUILTIN_PIPE## that survives in the output must be inside a comment. BehaviorTreeFactory factory; - auto sch = writeTreeSchematron(factory); - EXPECT_EQ(sch.find("##BUILTIN_PIPE##"), std::string::npos); + const auto sch = writeTreeSchematron(factory); + const std::string ph = "##BUILTIN_PIPE##"; + for(std::size_t p = sch.find(ph); p != std::string::npos; p = sch.find(ph, p + 1)) + EXPECT_TRUE(BT::detail::insideXmlComment(sch, p)); } TEST(WriteTreeSchematron, BuiltinNodesInPipeList) From ad1c70ce7f7dc853fb1c14fa5f4c53cf9db4b818 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 10:04:18 +0200 Subject: [PATCH 15/23] Add more test cases for InsideXmlComment() --- tests/gtest_factory.cpp | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/gtest_factory.cpp b/tests/gtest_factory.cpp index 9a47600ca..ad551f963 100644 --- a/tests/gtest_factory.cpp +++ b/tests/gtest_factory.cpp @@ -853,13 +853,40 @@ TEST(WriteTreeSchematron, WellFormedOutput) TEST(InsideXmlComment, BasicCases) { + // "before after" + // ^ ^ ^ ^ ^ + // 0 7 10 20 24 const std::string s = "before after"; - EXPECT_FALSE(BT::detail::insideXmlComment(s, 0)); // "before" - EXPECT_TRUE(BT::detail::insideXmlComment(s, 13)); // inside comment - EXPECT_FALSE(BT::detail::insideXmlComment(s, 24)); // "after" + EXPECT_FALSE(BT::detail::insideXmlComment(s, 0)); // 'b' — before any comment + EXPECT_FALSE(BT::detail::insideXmlComment(s, 6)); // ' ' — space right before + EXPECT_FALSE(BT::detail::insideXmlComment(s, 22)); // '>' — at the '>' of --> + EXPECT_FALSE(BT::detail::insideXmlComment(s, 24)); // 'a' — after comment EXPECT_FALSE(BT::detail::insideXmlComment("no comment", 3)); } +TEST(InsideXmlComment, MultipleComments) +{ + // " middle end" + // ^ ^ ^ ^ ^ + // 0 11 22 34 38 + const std::string s = " middle end"; + EXPECT_TRUE(BT::detail::insideXmlComment(s, 0)); // '<' — at first + EXPECT_FALSE(BT::detail::insideXmlComment(s, 13)); // '>' — at '>' of first --> + EXPECT_FALSE(BT::detail::insideXmlComment(s, 15)); // 'm' — between comments + EXPECT_FALSE(BT::detail::insideXmlComment(s, 21)); // ' ' — space right before second + EXPECT_FALSE(BT::detail::insideXmlComment(s, 36)); // '>' — at '>' of second --> + EXPECT_FALSE(BT::detail::insideXmlComment(s, 38)); // 'e' — after both comments +} + TEST(WriteTreeSchematron, PlaceholderReplaced) { // Every ##BUILTIN_PIPE## that survives in the output must be inside a comment. From 809c6915182f640bed36701c7b8ce99e9b102071 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 14:47:16 +0200 Subject: [PATCH 16/23] Add xsd and schematron checks to CI (intentionally broken) --- .github/workflows/cmake_ubuntu.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/cmake_ubuntu.yml b/.github/workflows/cmake_ubuntu.yml index 0f2674256..544d0f5bd 100644 --- a/.github/workflows/cmake_ubuntu.yml +++ b/.github/workflows/cmake_ubuntu.yml @@ -50,6 +50,17 @@ jobs: - name: run test (Linux) run: ctest --test-dir build/${{env.BUILD_TYPE}} + - name: Check generated schemas are up to date + run: | + ./build/${{env.BUILD_TYPE}}/tools/bt4_generate_xsd > /tmp/bt4.xsd + ./build/${{env.BUILD_TYPE}}/tools/bt4_generate_schematron > /tmp/bt4.sch + fail=0 + diff tools/generated/bt4.xsd /tmp/bt4.xsd || \ + { echo "XSD is stale - regenerate: ./build/Release/tools/bt4_generate_xsd > tools/generated/bt4.xsd"; fail=1; } + diff tools/generated/bt4.sch /tmp/bt4.sch || \ + { echo "Schematron is stale - regenerate: ./build/Release/tools/bt4_generate_schematron > tools/generated/bt4.sch"; fail=1; } + exit $fail + coverage: runs-on: ubuntu-24.04 From d4b31a16184b5fa7541a920980f5c47ef03719c4 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 14:54:20 +0200 Subject: [PATCH 17/23] Fix clang-format issues --- tests/gtest_factory.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/gtest_factory.cpp b/tests/gtest_factory.cpp index ad551f963..07fafe0ef 100644 --- a/tests/gtest_factory.cpp +++ b/tests/gtest_factory.cpp @@ -857,14 +857,14 @@ TEST(InsideXmlComment, BasicCases) // ^ ^ ^ ^ ^ // 0 7 10 20 24 const std::string s = "before after"; - EXPECT_FALSE(BT::detail::insideXmlComment(s, 0)); // 'b' — before any comment - EXPECT_FALSE(BT::detail::insideXmlComment(s, 6)); // ' ' — space right before - EXPECT_FALSE(BT::detail::insideXmlComment(s, 22)); // '>' — at the '>' of --> - EXPECT_FALSE(BT::detail::insideXmlComment(s, 24)); // 'a' — after comment + EXPECT_FALSE(BT::detail::insideXmlComment(s, 0)); // 'b' - before any comment + EXPECT_FALSE(BT::detail::insideXmlComment(s, 6)); // ' ' - space right before + EXPECT_FALSE(BT::detail::insideXmlComment(s, 22)); // '>' - at the '>' of --> + EXPECT_FALSE(BT::detail::insideXmlComment(s, 24)); // 'a' - after comment EXPECT_FALSE(BT::detail::insideXmlComment("no comment", 3)); } @@ -874,17 +874,17 @@ TEST(InsideXmlComment, MultipleComments) // ^ ^ ^ ^ ^ // 0 11 22 34 38 const std::string s = " middle end"; - EXPECT_TRUE(BT::detail::insideXmlComment(s, 0)); // '<' — at first - EXPECT_FALSE(BT::detail::insideXmlComment(s, 13)); // '>' — at '>' of first --> - EXPECT_FALSE(BT::detail::insideXmlComment(s, 15)); // 'm' — between comments - EXPECT_FALSE(BT::detail::insideXmlComment(s, 21)); // ' ' — space right before second - EXPECT_FALSE(BT::detail::insideXmlComment(s, 36)); // '>' — at '>' of second --> - EXPECT_FALSE(BT::detail::insideXmlComment(s, 38)); // 'e' — after both comments + EXPECT_TRUE(BT::detail::insideXmlComment(s, 0)); // '<' - at first + EXPECT_FALSE(BT::detail::insideXmlComment(s, 13)); // '>' - at '>' of first --> + EXPECT_FALSE(BT::detail::insideXmlComment(s, 15)); // 'm' - between comments + EXPECT_FALSE(BT::detail::insideXmlComment(s, 21)); // ' ' - space before second + EXPECT_FALSE(BT::detail::insideXmlComment(s, 36)); // '>' - at '>' of second --> + EXPECT_FALSE(BT::detail::insideXmlComment(s, 38)); // 'e' - after both comments } TEST(WriteTreeSchematron, PlaceholderReplaced) From 5a0e5100cd5d594efe39cc6338595fd0b4bf56bd Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 15:01:09 +0200 Subject: [PATCH 18/23] Regenerate schemas to pass CI --- tools/generated/bt4.sch | 2 +- tools/generated/bt4.xsd | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/generated/bt4.sch b/tools/generated/bt4.sch index ff58d4749..f3f564203 100644 --- a/tools/generated/bt4.sch +++ b/tools/generated/bt4.sch @@ -9,7 +9,7 @@ Built-in node types are excluded — they are always known to the library and do not need an explicit declaration. - \#\#BUILTIN_PIPE\#\# is a placeholder expanded at runtime by writeTreeSchematron() + ##BUILTIN_PIPE## is a placeholder expanded at runtime by writeTreeSchematron() with a pipe-delimited list of all built-in node names derived from the factory, e.g. |AlwaysFailure|AlwaysSuccess|Fallback|Sequence|...|. diff --git a/tools/generated/bt4.xsd b/tools/generated/bt4.xsd index 6daf42649..6b5c3e23c 100644 --- a/tools/generated/bt4.xsd +++ b/tools/generated/bt4.xsd @@ -40,6 +40,15 @@ + + + + + + + + + @@ -62,6 +71,7 @@ + From 36abdc3edfe1eb08803286ac2701fd2edad2851e Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Mon, 20 Jul 2026 15:14:13 +0200 Subject: [PATCH 19/23] Add guard for --no-xsd --no-sch together to validate_xml.py --- tools/validate_xml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/validate_xml.py b/tools/validate_xml.py index 5df3bb81f..18ed43cce 100755 --- a/tools/validate_xml.py +++ b/tools/validate_xml.py @@ -95,6 +95,9 @@ def main(): args = parser.parse_args() + if args.no_xsd and args.no_sch: + parser.error("--no-xsd and --no-sch together disable all validation; nothing to do") + if not Path(args.xml_file).exists(): parser.error(f"file not found: {args.xml_file}") From d5af5db17bde05f0d0aa37d3edaf158ad23e5b83 Mon Sep 17 00:00:00 2001 From: Victor Voronin Date: Fri, 24 Jul 2026 08:33:55 +0200 Subject: [PATCH 20/23] Remove rule checking explicit notation against TreeNodeModel --- src/btcpp4_schematron.sch | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/btcpp4_schematron.sch b/src/btcpp4_schematron.sch index a0311659c..66d2a454d 100644 --- a/src/btcpp4_schematron.sch +++ b/src/btcpp4_schematron.sch @@ -3,11 +3,12 @@ BehaviorTree.CPP XML validation rules - - - Node with ID='' used in tree - '' - has no <TreeNodesModel> entry. - - - - - Node with ID='' used in tree - '' - has no <TreeNodesModel> entry. - - + + Explicit-notation node structure + + + + + ID='' + must have no children (has ). + + + + + + + Decorator ID='' + must have exactly one child (has ). + + + + + + + Control ID='' must have at least one child. + + + + + + Explicit-notation node structure + + + + + ID='' + must have no children (has ). + + + + + + + Decorator ID='' + must have exactly one child (has ). + + + + + + + Control ID='' must have at least one child. + + + +