Appearance
Target Pipeline Configurations
Flude employs a decentralized configuration design where each software product owns its own pipeline parameters via an individual doc-config file — conventionally ude_doc_config.json (older projects use the name ude_config.json; both are just a JSON file passed via --doc-config). This architecture avoids a single monolithic configuration and lets teams manage their documentation pipelines alongside their codebase.
📐 Decentralized Configuration Strategy
Instead of maintaining a giant system-wide map of all source repositories, each submodule or code module includes its own target configuration file. This provides several operational benefits:
- Isolated Customization: Teams can adjust collector/parser/renderer settings for their own product without affecting others.
- Local Developer Testing: Developers can compile references locally over their workspace using the same pipeline parameters as the production CI/CD environment.
- Version Control Integration: Configuration files live in the same Git branch as the source code, preventing configuration drift when API schemas change.
NOTE
There is no formal JSON-Schema file for the target config anywhere in the engine. It is consumed as an untyped Python dict via .get() calls (engine/ude/orchestrator.py), unlike the strictly-typed global config (see Global Settings). The tables below document the keys the orchestrator actually reads — not a schema the engine enforces up front.
🧬 The 3-Tier Configuration Cascade
This is the core mechanic behind "decentralized" configuration: three JSON documents are merged into one dict before a compile runs, in a strict priority order:
In code (orchestrator.py:343):
python
config = deep_merge(deep_merge(resolved_global, sdk_config), doc_config)deep_merge(dict1, dict2) recursively merges two dicts, with dict2 winning on every leaf conflict.
CAUTION
Lists never element-merge. A list value in dict2 fully replaces the corresponding list in dict1 — it is not concatenated or appended. For example, if an SDK-level config sets "catalog_links": [{"label": "SDK Home", "url": "..."}] and a doc-config also sets "catalog_links": [...], the doc-config's list is what survives; the SDK-level entries are gone, not merged in. If you want a doc-config to add to an SDK-level list, you must repeat the SDK-level entries yourself in the doc-config's list.
Auto-discovery of the global and SDK tiers
If no explicit --global-config / --sdk-config path is given, both are auto-discovered by walking up the directory tree from the doc-config's own directory:
find_global_config()looks forude_global_config.json, thenude_global.json.find_product_json()looks forude_sdk_config.json, thenproduct.json.
Failure behavior differs sharply depending on how the file was located:
| Situation | Behavior |
|---|---|
| Auto-discovered global or SDK config is missing or fails to parse | Logs a warning and is skipped — never fatal. |
An explicitly passed --sdk-config path fails to parse | Fatal: UdeException(f"Failed to parse SDK JSON config: {e}") |
| The doc-config itself fails to parse | Always fatal: UdeException(f"Failed to parse target JSON config {config_file_path}: {e}") |
The doc-config file (passed via --doc-config) does not exist | Fatal: UdeException(f"Configuration file {config_file_path} does not exist.") |
🗂️ The [groups] Sidebar Taxonomy
[groups] lives in sidebar.toml, alongside [[sidebar]] — see Sidebar & Groups for the full reference. The short version, because it corrects a claim this page used to make:
There is no cascade and no engine-tier default for [groups]. The document's own sidebar.toml is the single, unconditional source. config["groups"] is validated by a strict GroupsConfig Pydantic model (engine/ude/config.py:56-68, extra="forbid") that declares exactly one permitted key: namespace_level. There is no class_level key anywhere in the schema. Anything else, including a simple typo like namespac_level, raises:
text
UdeException: Invalid [groups] configuration: only 'namespace_level' is a permitted key. Details: ...NOTE
ENG-01 (ToDo/v3.0/EngineDefects_ToDo.md) tracked an earlier version of this message that named class_level as a permitted key at the exact moment it rejected it — fixed 2026-08-19 (engine, DEL-B03). The message above is the corrected, current text.
groups and sidebar keys are forbidden in all three JSON config layers._reject_forbidden_sidebar_keys() (engine/ude/orchestrator.py:164-184) rejects either key in the global (:263), SDK (:297), and doc (:310) config; GlobalConfig._reject_sidebar_keys() (engine/ude/config.py:125-162) rejects them again at the GlobalConfig level, alongside the removed sidebar_structures_dir key. Putting groups or sidebar in ude_global_config.json / ude_sdk_config.json / ude_doc_config.json is a configuration error, not a legitimate override — there is nothing here to deep-merge.
Separately, the [[sidebar]] navigation array in sidebar.toml has no engine-default fallback at any tier — there is nothing for a project's own [[sidebar]] to override. A project that declares one gets exactly that navigation; a project that declares none fails the build.
Is sidebar.toml required?
Yes, unconditionally. resolve_config() calls the single strict loader, load_sidebar_toml(), on every compile — there is no other, more lenient loader in the codebase. Three ways to fail, all fatal (see migration-v2.md for the exact messages):
- the file is missing from the document directory;
- the file exists but is unparseable TOML;
- the file exists and parses but declares zero
[[sidebar]]entries (an explicitsidebar = []is rejected the same way).
This was a deliberate v2.0 breaking change (IMP-32.10, 2026-08-02): navigation that renders successfully but wasn't what its author intended is harder to notice than a build that stops outright. Treat sidebar.toml as strictly mandatory for every document directory, Hugo variants included — not "recommended."
📑 Target Config Schema — Real Keys
Below are the real keys the orchestrator reads from a doc-config, confirmed against engine/ude/orchestrator.py. This is not an exhaustive formal schema (none exists) — it's the set of keys with confirmed read/behavior, plus a smaller set of keys that are real in example configs but whose precise runtime behavior wasn't traced in this pass.
Confirmed keys with specific behavior
src_dir— a JSON array of source paths, required. A bare string is rejected:UdeException("'src_dir' must be a list of paths. String values are not accepted — wrap single paths in a JSON array."). Each entry is resolved relative to the doc-config's own directory.collector.language(or a top-levellanguage) — selects the language-specific Doxygen collector/parser:Cpp,Cs(also writtencsharp),Java,Py(also writtenpython).renderer.type— dispatch is by a generic, lowercased token match, not an arbitrary string:"html"/"static_html","oda_html","oda_hugo_markdown"/"oda_markdown"/"oda_hugo","hugo_markdown"/"markdown"/"hugo". An unmatched value raisesUdeException(f"Unsupported format '{fmt}' in target config.").NOTE
Fixed 2026-08-31 (config audit,
DEL-B37): this page used to warn that real configs underude_projects/*setrenderer.typeto a concrete renderer class name (e.g."CppHtmlDefaultRenderer"), which doesn't match any generic dispatch token. Re-checked against the live repo: all 78 real doc-configs now uniformly use the generic tokens"oda_html"/"oda_hugo_markdown"— the concrete-class-name pattern is gone fromude_projects/*. Still prefer the generic tokens in any new config; concrete renderer class names (see the class matrix in this project's core rules) identify implementation classes, notrenderer.typedispatch values.That same renderer-family generalization has one real side effect worth knowing:
orchestrator.py's language auto-detection falls back to reading aPy/Cs/Java/Cppprefix offrenderer.typewhencollector.languageis absent — a fallback that only ever worked whilerenderer.typestill held a language-prefixed class name. Now that it'soda_html/oda_hugo_markdowneverywhere, that fallback can never match, so explicitcollector.languageis effectively mandatory for every doc-config (as it already is for 76 of 78 — twoDrawingsPython targets were missing it and silently defaulted tocppuntil this audit added it back).output_subdirpaired with a global/SDK-leveloutput_base_dir— final output path is(base_dir / output_base_dir).resolve() / output_subdir. If this pair isn't set, a standaloneoutput_diron the doc-config is used as a fallback instead.cache_root_dir— render-stage (L2) cache location for this target; falls back to the resolved output directory if unset.catalog_links— a list of{"label": ..., "url": ...}dicts, appended as extra links in every rendered page's footer. Remember: lists never element-merge across the cascade (see above), so a doc-config'scatalog_linksfully replaces any SDK-level list of the same name.stylesheet_dir— HTML/ODA HTML asset source directory, resolved via a 4-tier fallback, first existing directory wins: (1) relative to the global config's own directory, (2) relative to the auto-discovered global config's directory, (3) the engine's own bundledtemplates/css/default, (4) relative to the doc-config's own directory.static_source_path— a string or list of directories searched forsidebar.tomltype = "static"custom page source files. Once the orchestrator resolves asource_fileentry to an absolute path within one of these directories, the renderer reads it viaBaseRenderer._load_static_file_from_path()(engine/ude/interfaces.py), which extracts the<body>...</body>contents for.html/.htmsources or strips YAML front-matter for.md/.markdownsources, and raisesRendererErrorif the file is missing or unreadable. Called from bothHtmlRenderer(static_html.py) andHugoMarkdownRenderer(hugo_markdown.py).product_name/sdk_name— read preferentially straight from the merged config; if absent, auto-discovered fromude_sdk_config.json/product.json.groupsis not a valid doc-config key — see "The[groups]Sidebar Taxonomy" above; putting it here raises a configuration error rather than being merged.
Real-world keys without traced precise behavior
These appear in real example configs and are accepted by the orchestrator, but this documentation deliberately does not over-claim their exact internal behavior: project_name, short_project_name, static_pages_dir, copyright_start_year, incremental (bool), max_workers (int), validator.type, collector.file_patterns, collector.include_private.
📑 Complete Target Template (Example)
Here is a real target configuration file from this repository, ude_projects/IGES/iges_api_cpp/ude_doc_config.json (re-checked 2026-08-31, DEL-B37):
json
{
"project_name": "IGES CPP API Reference",
"short_project_name": "IGES",
"src_dir": ["../../../workspace/sdk_sources/IGES/Include"],
"static_pages_dir": "./",
"copyright_start_year": 2026,
"incremental": true,
"max_workers": 8,
"collector": {"type": "CppDoxygenCollector", "language": "cpp", "file_patterns": ["**/*.h", "**/*.hpp"], "include_private": false, "doxygen_timeout_seconds": 3600},
"parser": {"type": "CppDoxygenParser", "export_macros": ["IGES_EXPORT", "IGES_EXPORT_STATIC", "_IGES_BUILD_OPTIONS_H"]},
"renderer": {"type": "oda_html"},
"output_subdir": "iges_api_cpp",
"validator": {"type": "CppCommentValidator"}
}TIP
renderer.type here is the generic dispatch token "oda_html" (registered by flude-oda-plugin), and collector.language is set explicitly rather than relied upon as a fallback — see the note above on why that fallback is no longer reliable. If you're authoring a new config, follow this same pattern: a generic renderer.type token plus an explicit collector.language.
🔄 Relative Path Resolution
src_dir entries, and most other path-shaped keys, are resolved relative to the doc-config file's own directory on disk — not relative to the current working directory the CLI is invoked from. This lets the same config produce identical results whether invoked from the project root, the submodule directory, or any other workspace folder.
🧭 Where This Fits
For the schema of the global config that sits at the bottom of this cascade — including error_policy, coverage_threshold, and the removed sidebar_structures_dir key — see Global Settings.
