Skip to content

Make a JSON manifest the source of truth for help#398

Closed
marcosbento wants to merge 49 commits into
developfrom
task/external_docs
Closed

Make a JSON manifest the source of truth for help#398
marcosbento wants to merge 49 commits into
developfrom
task/external_docs

Conversation

@marcosbento

@marcosbento marcosbento commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

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:

  • All authors agree to the Contributor License Agreement.
  • The code follows the project's coding standards.
  • I have performed self-review and added comments where needed.
  • I have added or updated tests to verify that my changes are effective and functional.
  • I have run all existing tests and confirmed they pass.

🌦️ >> Documentation << 🌦️
https://sites.ecmwf.int/docs/dev-section/ecflow/pull-requests/PR-398

@codecov-commenter

codecov-commenter commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.44693% with 70 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.18%. Comparing base (1045714) to head (b2fc664).

Files with missing lines Patch % Lines
libs/client/src/ecflow/client/Help.cpp 77.27% 10 Missing ⚠️
libs/base/src/ecflow/base/cts/user/CheckPtCmd.cpp 10.00% 9 Missing ⚠️
...bs/base/src/ecflow/base/cts/user/EditScriptCmd.cpp 10.00% 9 Missing ⚠️
libs/base/src/ecflow/base/cts/user/AlterCmd.cpp 33.33% 6 Missing ⚠️
libs/base/src/ecflow/base/cts/user/ForceCmd.cpp 14.28% 6 Missing ⚠️
Viewer/ecflowUI/src/CommandDesignerWidget.cpp 0.00% 5 Missing ⚠️
libs/base/src/ecflow/base/cts/user/FreeDepCmd.cpp 16.66% 5 Missing ⚠️
...s/base/src/ecflow/base/cts/user/RequeueNodeCmd.cpp 16.66% 5 Missing ⚠️
libs/base/src/ecflow/base/cts/user/RunNodeCmd.cpp 20.00% 4 Missing ⚠️
libs/base/src/ecflow/base/cts/user/BeginCmd.cpp 25.00% 3 Missing ⚠️
... and 6 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@marcosbento
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
marcosbento deleted the task/external_docs branch July 24, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants