Make a JSON manifest the source of truth for help#398
Closed
marcosbento wants to merge 49 commits into
Closed
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #398 +/- ##
===========================================
+ Coverage 52.10% 52.18% +0.08%
===========================================
Files 1248 1251 +3
Lines 101827 101968 +141
Branches 15155 15171 +16
===========================================
+ Hits 53053 53213 +160
+ Misses 48774 48755 -19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
marcosbento
marked this pull request as ready for review
July 23, 2026 20:32
Introduces help.schema.json, the JSON Schema for a future manifest describing ecflow_client CLI help metadata. Command kind is restricted to task/user, option kind to global-option/command-option, and a command-option must name its single owning command. Adds validate_help_manifest.py, checking both schema conformance and cross-referential rules a schema alone cannot express: unique names per array, command-option -> command linkage, and symmetric pairs_with references between options. Wires the validator into the docs build as the ecflow_client_help_validate CMake target, a dependency of ecflow_client_docs, so an invalid manifest fails fast before Sphinx runs. Adds jsonschema to docs/requirements.txt accordingly. Adds help.json as a minimal, empty seed manifest that already validates; later changes will populate it with real content, moving CLI help off today's runtime-parsed --help text.
Populates help.json's options array with the eleven global options (host, port, user, password, rid, ssl, http, https, help, version, debug), text taken verbatim from ClientOptions.cpp/CtsCmdRegistry.cpp and the checked-in RST pages. help and version are modeled with no environment-variable section, correcting Help.cpp's current misclassification of them as commands by elimination -- the reason they get one today. rid's description corrects "child commands" to "task commands", the one wording change the terminology fix permits; every other string is unchanged.
Populates help.json's topics array with the five keyword values --help=<topic> accepts: all, summary, task, user, option. Four are taken verbatim from Documentation::show_help()'s "Try:" banner (including the existing "user command" singular wording), with the child/task correction applied to the fourth. option has no banner text in the source -- it works via Documentation::show()'s dispatch but isn't advertised there -- so its summary is new text written to match the style of the other four.
Populates help.json's environment_variables array with all twelve variables from Help.cpp's known_env_options, text taken verbatim. Extends help.schema.json with an overridable_by field, replacing the mandatory*/optional* asterisk convention: it names the global-option whose value can substitute for the variable, set only for ECF_HOST, ECF_PORT, and ECF_SSL, matching exactly which three carry an asterisk in the source today. ECF_RID has a real --rid override but is never marked as such in the source, so it correctly gets none. Adds a validator check confirming every overridable_by reference names a real global-option entry.
Populates help.json's commands array with abort, the first real command migrated (previous steps only covered options/topics/env vars). description holds only AbortCmd::desc()'s prose paragraphs, with the "child command" -> "task command" correction applied; the argN = .../Usage: text is deliberately omitted, to be synthesized at render time from arguments/usage instead, avoiding duplication between free text and structured data. Extends help.schema.json with a required type field on arguments, matching the source's "argN = (optional) type(name)" convention (e.g. string(reason)). Preserves abort's "reasonX" usage example as-is; this looks like a leftover typo for "reason" but is out of scope for byte-for-byte migration.
Populates help.json's commands array with complete and init, text taken verbatim from CompleteCmd::desc()/InitCmd::desc(), with the "child command" -> "task command" correction applied to each. Confirms a pattern first seen as a question during init's own migration: both commands document a companion command-option via an argN(--flag)(optional) line rather than a true positional argument (init's add, complete's remove) -- modeled as a second arguments entry on each command, consistent with the earlier decision for init's add line. argN/Usage: text is omitted from description throughout, per the synthesis approach decided while migrating abort.
Reverses the previous decision to synthesize argN = .../Usage: text at render time from structured arguments/usage. After comparing the actual formatting of siz commands shows the convention is inconsistent hand-written prose, not a reproducible template: type/name/description sit in different positions relative to =, some commands omit a type or a separate description line, and one uses a union type (string | int). Restores the full verbatim argN = .../Usage: text to abort's, complete's, and init's description arrays, and drops the placeholder type: "any" from complete's remove and init's add argument entries, since neither has a real type in the source. Loosens help.schema.json's argument definition accordingly: type becomes free text instead of a closed enum, and both type and description become optional, since the source does not always supply them.
Populates help.json's commands array with event, label, and meter, text taken verbatim from EventCmd::desc()/LabelCmd::desc()/ MeterCmd::desc(), with the "child command" -> "task command" correction applied to each. description holds the full original text, including each command's argN = ... and Usage: lines, per the verbatim-storage approach. arguments/usage are populated as best-effort structured metadata: event's arg1 uses a union type (string | int), and neither event's arg2 nor label's two arguments have a clean identifier name in the source, so the literal source phrase is used as name instead.
Populates help.json's commands array with queue and wait. Text was taken verbatim from QueueCmd::desc()/CtsWaitCmd::desc(), with the "child command" -> "task command" correction applied. queue's usage doesn't fit the short-invocation shape used by every other command's usage array -- desc() embeds a full shell script demonstrating a queue-draining loop -- so it is stored as a single verbatim block matching its description's Usage section exactly, rather than a paraphrased summary.
Adds cmake/GenerateClientHelp.cmake, which wraps
docs/client_api/help.json in a header defining client_help_json as a
std::string_view raw string literal, guarding against the delimiter
appearing in the JSON.
Wired into libs/CMakeLists.txt as a build-time add_custom_command
rather than configure_file(), so the header regenerates on manifest
changes without a full reconfigure -- verified with ninja alone.
Uses ${ecflow_SOURCE_DIR}, not ${CMAKE_SOURCE_DIR}, which resolves to
the wrapping project's root when built as a subproject.
Adds ecf::HelpCatalog, exposing manifest() as a lazily-parsed, cached nlohmann::json reference to the embedded help.json content. Parsing happens only on first use, since ecflow_client is invoked per task, potentially thousands of times per suite run, and only a small fraction of those invocations ever request help. Adds TestHelpCatalog.cpp, verifying the manifest parses, contains known entries, and is parsed once (same reference on repeated calls).
Adds find_command(), find_option(), and find_topic() to ecf::HelpCatalog, each returning const nlohmann::json* -- nullptr on a miss, matching the codebase's existing find_nothrow-style convention rather than throwing. Shares the scan logic via a private find_by_name() helper instead of repeating it three times. Adds six test cases (a hit and a miss per lookup) to TestHelpCatalog.cpp.
Documentation::show_summary() and show_command_help() now check HelpCatalog for a matching command/option and render its summary or description when present, falling back to the existing Boost-registered text for anything not yet migrated. This lets --help output for the 8 migrated commands and 11 migrated options come from the JSON manifest without disturbing any of the ~90 still-unmigrated entries. Also fixes six description-array bugs in help.json found while verifying manifest text against the original C++ source byte-for-byte: ssl/debug had a single paragraph incorrectly split across two array elements, and event/queue/http/https were each missing a trailing newline present in the source.
Commands in base/ (task/user addOption(), CtsCmdRegistry) need to the header. Add summary_for()/description_for() returning std::optional<std::string>, and switch Help.cpp to use them instead of its own duplicate helpers.
Migrates CSyncCmd's four actions (step 8.d), each registered as its own CLI option by a separate CSyncCmd instance in CtsCmdRegistry. Descriptions verified byte-for-byte against CSyncCmd.cpp's add_options() literals, and live --help output confirmed unchanged. Relax help.schema.json's usage field to optional: none of these four have "Usage:" text in their source, unlike every command migrated so far, so there is nothing verbatim to populate it with.
Migrates ZombieCmd's six actions, each registered as its own CLI option by the same ZombieCmd class dispatching on a ZombieCtrlAction enum value.
…help manifest Migrates PathsCmd's eight actions, each registered as its own CLI option by the same PathsCmd class dispatching on an Api enum value. archive and restore have multi-paragraph descriptions including an embedded suite/family configuration example; all descriptions verified byte-for-byte against PathsCmd.cpp's add_options() literals, and live --help output confirmed unchanged.
Migrates all eighteen CtsCmd actions (zombie_get, restore_from_checkpt, restart, shutdown, halt, terminate, reloadwsfile, reloadpasswdfile, reloadcustompasswdfile, force-dep-eval, ping, stats, stats_server, stats_reset, suites, debug_server_on, debug_server_off, server_load), each registered as its own CLI option by the same CtsCmd class dispatching on an Api enum value. Several of these embed pre-formatted content (a shared ASCII server- state table, and password/whitelist file format examples with blank lines of their own); all reproduce byte-for-byte as plain description array elements split at blank-line boundaries, with no need for the schema's verbatim block type.
Migrates five of CtsNodeCmd's six actions, each registered as its own CLI option by the same CtsNodeCmd class dispatching on an Api enum value. The sixth action, why, is group-only and stays out of scope here. migrate's description includes a small feature-comparison table against get/get_state; reproduces byte-for-byte as plain description array elements, same as CtsCmd's tables.
Migrates all seven ClientHandleCmd actions, each registered as its own CLI option by the same ClientHandleCmd class dispatching on an Api enum value.
Migrates begin, check_pt, delete, edit_script, file, force, free-dep, load, log, msg, order, replace, requeue, run, and server_version -- each a single-action UserCmd subclass with its own addOption(). Descriptions verified byte-for-byte against each class's add_options() literal or named desc()-style function, and live --help output confirmed unchanged.
Migrates AlterCmd's single alter command. Its argument shape is genuinely conditional per operation (delete/change/add/set_flag/ clear_flag/sort each accept different attribute types), so per the already-resolved decision this stays a single prose block rather than structured JSON.
Migrates QueryCmd's query command and PlugCmd's plug command.
Migrates GroupCTSCmd's single group command -- registered by default in CtsCmdRegistry (addGroupCmd defaults to true), unlike every other command here.
Migrates the add command-option on init and the remove command-option on complete, both registered inline in their owning command's addOption(), as command-option entries linked via the manifest's command field. add is in Help.cpp's CommandFilter::known_options (no env-var banner at runtime); remove is not (gets the full banner) -- both behaviors preserved and verified byte-for-byte against live --help output.
Migrates CtsNodeCmd's why action and ShowCmd's show command, both with visibility "group-only" since they can only be used inside --group and are registered normally in CtsCmdRegistry alongside every public command.
Migrates MoveCmd's single move command, with visibility "internal" since it is never registered in CtsCmdRegistry (created on the fly by PlugCmd) and so has no CLI-reachable --help output to verify against -- confirmed live, "No matching command found". Description verified by exact regex extraction from MoveCmd::desc()'s add_options() literal instead.
The description_block schema allowed either a plain string or a
{"verbatim": string} object, intended for preformatted content such
as CtsCmd's ASCII-art tables. In practice every command migrated so
far, including the ones with the richest preformatted output,
reproduces its content byte-for-byte through the plain string form
alone: joining blocks with a blank line is a lossless round-trip
regardless of how many blank lines the original text contains.
Drop the verbatim object type from help.schema.json and simplify
HelpCatalog::description_for() to match, removing the now-dead branch
that handled it.
CheckPtCmd, BeginCmd, RequeueNodeCmd, FreeDepCmd, CFileCmd, LoadDefsCmd, LogCmd, ForceCmd, RunNodeCmd, EditScriptCmd, AlterCmd, QueryCmd, and PlugCmd each embed their own usage text directly in the errors thrown when create() rejects malformed CLI arguments. That text was a second, independent copy of the same content already carried in the help manifest, left to drift the moment either copy changed. Look each error's text up via HelpCatalog::description_for(), falling back to the original desc()/arg_desc() text when the manifest has no matching entry (there is currently no case where it does not, but the fallback keeps these call sites safe against a future manifest gap).
build.py generated docs/client_api's index/table/per-command pages by subprocess-invoking the built ecflow_client and scraping its terminal output. This tied doc generation to a full C++ build and let content drift from the manifest whenever it changed without a regeneration.
build.py no longer subprocess-invokes ecflow_client; it renders CLI docs directly from help.json. The DEPENDS ecflow_client line and the PATH-prepend that let build.py find the built binary are both dead weight now, and worse, they mean the docs target only regenerates when the client happens to rebuild, not when the manifest itself changes. Replace that dependency with the manifest, schema, validator script, and build.py itself, so ecflow_client_docs regenerates exactly when its real inputs change and no longer requires a full client build.
Documentation::show_list_options() (backs --help=all) streamed the raw boost::program_options::options_description object directly, bypassing HelpCatalog entirely and rendering every option's Boost-registered literal regardless of manifest migration status - the last rendering path standing between the manifest and being the sole source of command/option documentation. Replace it with a plain per-option listing: a "Client options:" header with the client version, then each option's name and description indented for readability - sourced exclusively from HelpCatalog::description_for().
Replace every fallback with a fixed "Description not provided for this option" message, so nothing in the codebase reads Boost's registered description text anymore, only ecflow_client's own addOption() registration (Boost's API requires the text to be handed over eagerly there, and it is never read back for rendering).
Every addOption() implementation still passed a description string to boost::program_options::add_options(), even though nothing reads option_description::description() back anymore - the last two rendering paths (show_summary()'s and show_command_help()'s Boost fallback) were replaced with a fixed warning message, and CLI help now comes exclusively from the JSON manifest via HelpCatalog. Drop the description argument from every 3-arg add_options() call (value-taking options), keeping only the name and value-semantic. For the 22 flag-only options previously registered via the 2-arg (name, description) form - restart, ssl, ping, and 19 others - replace the description text with an empty string, since Boost has no 1-arg overload and building the same untyped_value(true) some other way would risk changing parsing behaviour for zero benefit. Cmd::desc() functions themselves are untouched: they remain the fallback text for the ~29 thrown-parser-error call sites migrated earlier (HelpCatalog::description_for(name).value_or(Cmd::desc())), and Boost's addOption() registration still requires a value-semantic to know each option's shape (single value, multitoken, implicit value) even with no description attached.
The ~29 call sites migrated to route thrown-error text through
HelpCatalog::description_for() kept a fallback to the original
Cmd::desc()/arg_desc() text for the (theoretical) case where the
manifest lacked an entry. That fallback duplicates the same
"unreachable in practice" reasoning already applied to Help.cpp's own
Boost-description fallback, which was replaced with a fixed warning
message rather than a second copy of the text.
Replace every .value_or(Cmd::desc()) with
.value_or("Description not provided for this option"), for
consistency with that same decision.
The "Description not provided for this option" message used to be a separate string literal duplicated at every fallback call site: in Help.cpp's two rendering paths, CommandDesignerWidget's two lookups, and the ~29 thrown-parser-error sites across 13 command classes. Add a single ecf::HelpCatalog::not_provided constant and reference it everywhere instead, so the message exists in exactly one place.
These functions existed solely to feed text to Boost's option parser and, for some commands, thrown parser errors. Both uses were already migrated to read from the HelpCatalog manifest, leaving these as orphaned duplicates that could silently drift from it. Removing them keeps the manifest the single source of truth for command/option help.
To make HelpCatalog the sole source of help information, replace the hardcoded table with a runtime query of the catalogue.
Rename output text throughout the CLI's --help dispatch to match the "task" terminology already used everywhere else. Update the recognized --help topic (child -> task), its summary banner, the kind column shown for task commands, and the Try: usage line that advertises it.
Update Help.cpp's and build.py's environment-variable banner text to say "task commands" instead of "child commands", matching the terminology used everywhere else in this migration. Regenerate the CLI docs to match, which also catches up docs/client_api/'s generated output with several already-verified renames from earlier steps that were never actually committed: the task commands' "child command" -> "task command" zombie-handling text, and step 6's help/version/remove reclassification and user.rst rendering fix.
Make the catalogue a runtime initialisation to avoid an apparent limitation of NVidia compiler, that otherwise fails to compile generated_client_help.hpp due to: > error type "const char [92920]" too large for constant-expression evaluation
Gate the ECF_SSL environment-variable help on ECF_OPENSSL via a new env_var_enabled() predicate, so a client built without OpenSSL no longer advertises a variable it cannot use. This restores the pre-migration behaviour that was lost when the hardcoded env-var table moved into the help manifest. Add TestHelp.cpp covering the manifest-driven help rendering: the summary/task/user topic listings, the environment-variable banners for task and user commands, the option-versus-command distinction, the unknown-topic fallback, and ECF_SSL visibility per build. Document the new Help.cpp environment-variable helpers with Doxygen blocks, and neutralise HelpCatalog::not_provided so its text reads correctly when used as the fallback for commands as well as options. Rename the 'child command' glossary term to 'task command', matching the child-to-task rename already applied to the client help output.
A previous commit renamed the glossary term itself but left its
references reading "child command". Make the glossary internally
consistent with the client help output, without breaking the many
:term:`child command` references in other documents.
Convert every in-glossary :term:`child command` role and prose
mention to "task command", and switch the applicability-table labels
from (Child only)/(User + Child) to (Task only)/(User + Task).
Add a "child command" glossary entry that redirects to "task command"
("See task command"), so cross-references from other pages still
resolve.
Each command/option "description" was an array of paragraphs joined by a blank line, with multi-line paragraphs held as escaped newlines in a single JSON string. Store one line per element instead (a blank line is an empty string), so no string carries an embedded newline and help-text edits diff line by line. Reconstruction joins the lines with a newline, an exact inverse, so the rendered help and generated docs are unchanged byte for byte. Both consumers now join with "\n" (HelpCatalog::description_for and build.py); the C++ join is made empty-line-safe. The schema renames description_block to description_line and drops its minimum length, and the validator now rejects an embedded newline in any line. Also silence -Woverlength-strings for the embedded manifest literal.
Every command help entry now has one blank line before and after the "Usage:" heading, and all heading variants (notably "Usage::") are canonicalised to "Usage:". Documentation was regenerated to match.
Remove the redundant "ecflow_client" prefix from every Usage example and indent all options to 4 spaces, for consistent help output. Delete the "usage" field from the schema and all commands: it merely duplicated the description's Usage text and was read by nothing. Also strip the trailing blank line from descriptions that had one, and regenerate the CLI documentation. The manifest formatting (topics, arrays) is normalised in passing.
command_internals.rst is hand-maintained, but its per-command "Help" tab reproduces the ecflow_client --help text, which drifted from the manifest (stale "child command" wording, old Usage format). Add update_internal.py to regenerate that text in place from help.json and hook it into the documentation build after build.py. The update is content-only: it rewrites the code-block bodies and the HTML selector panels, leaving the surrounding prose, tables, panel order/ids, and the other tabs untouched. Refresh all 33 Help tabs.
Corrections preserved byte-for-byte during the manifest migration,
now applied:
- ECF_TRYNO: close the unmatched parenthesis.
- ECF_DENIED: add the missing apostrophe ("ECF_TIMEOUT's wait").
- ECF_RID: set overridable_by "rid" so it renders [mandatory*], like
the other CLI-overridable variables.
- abort: use the <some-reason> usage placeholder instead of the
"reasonX" typo.
- queue: split the run-together "attributes.If the path" into two
lines.
Regenerate the CLI docs and the command_internals Help tabs.
Each command's inline "argN = ..." text used a different hand-written layout (type/name/description in different positions, some commands omitting a type or description entirely). Replace it with a uniform "Argument(s):" block for every command that documents arguments: one name per line, with "(optional)" and its type inlined, and a stacked "#"-prefixed description underneath. Applies to all 35 commands with argument text except alter, whose per-operation conditional argument shape is already tracked as separate future work and stays a single prose block for now. Regenerate the CLI docs and the command_internals Help tabs to match.
marcosbento
force-pushed
the
task/external_docs
branch
from
July 24, 2026 09:29
ddd4677 to
b2fc664
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
As per PR title.
Drive both ecflow-client --help and the documentation generation based on the contents of an external manifest file.
Contributor Declaration
By opening this pull request, I affirm the following:
🌦️ >> Documentation << 🌦️
https://sites.ecmwf.int/docs/dev-section/ecflow/pull-requests/PR-398