Skip to content

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 for ude_global_config.json, then ude_global.json.
  • find_product_json() looks for ude_sdk_config.json, then product.json.

Failure behavior differs sharply depending on how the file was located:

SituationBehavior
Auto-discovered global or SDK config is missing or fails to parseLogs a warning and is skipped — never fatal.
An explicitly passed --sdk-config path fails to parseFatal: UdeException(f"Failed to parse SDK JSON config: {e}")
The doc-config itself fails to parseAlways fatal: UdeException(f"Failed to parse target JSON config {config_file_path}: {e}")
The doc-config file (passed via --doc-config) does not existFatal: 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 explicit sidebar = [] 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-level language) — selects the language-specific Doxygen collector/parser: Cpp, Cs (also written csharp), Java, Py (also written python).

  • 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 raises UdeException(f"Unsupported format '{fmt}' in target config.").

    NOTE

    Fixed 2026-08-31 (config audit, DEL-B37): this page used to warn that real configs under ude_projects/* set renderer.type to 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 from ude_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, not renderer.type dispatch values.

    That same renderer-family generalization has one real side effect worth knowing: orchestrator.py's language auto-detection falls back to reading a Py/Cs/Java/Cpp prefix off renderer.type when collector.language is absent — a fallback that only ever worked while renderer.type still held a language-prefixed class name. Now that it's oda_html/oda_hugo_markdown everywhere, that fallback can never match, so explicit collector.language is effectively mandatory for every doc-config (as it already is for 76 of 78 — two Drawings Python targets were missing it and silently defaulted to cpp until this audit added it back).

  • output_subdir paired with a global/SDK-level output_base_dir — final output path is (base_dir / output_base_dir).resolve() / output_subdir. If this pair isn't set, a standalone output_dir on 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's catalog_links fully 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 bundled templates/css/default, (4) relative to the doc-config's own directory.

  • static_source_path — a string or list of directories searched for sidebar.toml type = "static" custom page source files. Once the orchestrator resolves a source_file entry to an absolute path within one of these directories, the renderer reads it via BaseRenderer._load_static_file_from_path() (engine/ude/interfaces.py), which extracts the <body>...</body> contents for .html/.htm sources or strips YAML front-matter for .md/.markdown sources, and raises RendererError if the file is missing or unreadable. Called from both HtmlRenderer (static_html.py) and HugoMarkdownRenderer (hugo_markdown.py).

  • product_name / sdk_name — read preferentially straight from the merged config; if absent, auto-discovered from ude_sdk_config.json / product.json.

  • groups is 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.