Hierarchy Validation¶
Functualize validates hierarchical project structures at mount time to prevent two classes of problems: version incompatibilities between parent and child projects, and circular dependencies that would cause infinite recursion during discovery.
These checks run automatically during FunctualizeApp initialization — no changes to your application code are required.
Prerequisites
This guide assumes you're familiar with hierarchical project composition. See the Hierarchical Projects guide for setup instructions and configuration options.
Version Compatibility Checking¶
When a child project is mounted, the validator extracts the child's declared functualize builtin version from its pyproject.toml dependencies and compares it against the parent's running version.
How Comparison Works¶
The validator uses major.minor comparison to determine compatibility:
- The child's minimum functualize builtin version (major, minor) must be greater than or equal to the parent's running version (major, minor)
- Patch versions are ignored in the comparison
- If either version is unknown, the check passes with a warning
# Compatible: child (0, 3, 1) vs parent (0, 3, 0) — same major.minor
# Compatible: child (1, 2, 0) vs parent (1, 1, 5) — child minor is higher
# Incompatible: child (0, 2, 0) vs parent (0, 3, 0) — child minor is lower
# Incompatible: child (0, 5, 0) vs parent (1, 0, 0) — child major is lower
Version Resolution¶
The version resolver determines a project's functualize builtin version through two sources, tried in order:
pyproject.toml— Parses the[project.dependencies]list for a functualize entry and extracts the minimum version from the specifier- Installed package metadata — Falls back to
importlib.metadataif the pyproject.toml is missing or doesn't declare a functualize dependency
Supported specifier formats:
| Format | Example | Extracted minimum |
|---|---|---|
PEP 440 >= |
functualize>=0.3.0 |
0.3.0 |
PEP 440 == |
functualize==1.0.0 |
1.0.0 |
PEP 440 ~= |
functualize~=0.2.0 |
0.2.0 |
| Poetry caret | functualize^0.2.0 |
0.2.0 |
| No lower bound | functualize!=1.0.0 |
Unknown (None) |
| Bare name | functualize |
Unknown (None) |
Cycle Detection¶
The validator tracks an ancestry chain — the ordered sequence of project paths from the root down to the current project being validated. Before mounting each child, the validator checks whether the child's canonical path already exists in the chain.
How It Works¶
- At the root project, the ancestry chain is initialized with the root's canonical absolute path
- For each child about to be mounted, the validator resolves the child's path to its canonical form (resolving symlinks via
os.path.realpath) - If the canonical path is already in the ancestry chain, a cycle is detected
- If the child passes validation, its path is added to the chain before recursing into its own children
Depth Limit¶
The validator enforces a maximum hierarchy depth of 10 levels (root is level 0). If a project exceeds this depth, validation fails with a HierarchyValidationError regardless of the strict mode setting.
This prevents runaway recursion in deeply nested or misconfigured hierarchies.
Path Canonicalization¶
All paths are resolved to their canonical absolute form before comparison. This handles:
- Symlinks — resolved to their real target
- Relative paths — resolved to absolute
- Redundant separators — normalized (e.g.,
//→/)
This prevents false negatives where the same project appears under different path representations.
Configuration¶
Enable strict validation by setting strict_hierarchy_validation = true in the [general] section of your config.base.toml:
- Default is
false(non-strict mode). Set totrueto halt on validation failures.
Children are mounted via
JobSources(children={...})/children_glob— see Hierarchical Projects. There is no[children]config section.
Behavior Modes¶
When strict_hierarchy_validation is absent or set to false:
- Version mismatches produce a warning log and the child is skipped
- Cycle detection errors produce a warning log and the child is skipped
- Remaining children continue to be processed
- The application starts successfully with the valid children mounted
When strict_hierarchy_validation = true:
- All children are validated first, collecting all failures
- If any validation failure occurs, a
HierarchyValidationErroris raised - The error contains the complete list of failures
- Application initialization halts — no children are mounted
Error Reporting¶
Version Mismatch Warnings¶
When a version incompatibility is detected, the message includes:
- The child's namespace name
- The child's project path
- The child's declared functualize builtin version
- The parent's running functualize builtin version
Non-strict mode output:
WARNING - Validation failed for child 'tools' at /home/user/code/tools-project:
Version incompatibility for child 'tools' at /home/user/code/tools-project:
child requires functualize 0.2.0 but parent runs 0.3.0
Strict mode output:
Version incompatibility (strict mode): child 'tools' at
/home/user/code/tools-project requires functualize 0.2.0 but parent runs 0.3.0
Cycle Error Messages¶
Cycle errors include the full path chain showing how the cycle forms:
Cycle detected: /home/user/project-a → /home/user/project-b → /home/user/project-c → /home/user/project-a
The last entry in the chain is the project that closes the cycle — it matches an earlier entry in the ancestry.
Depth Exceeded Errors¶
Rich Formatting¶
If the Rich library is available in your environment, error messages are automatically formatted with color and bold markup for better terminal readability. If Rich is not installed, messages are rendered as plain text.
Multiple Failures¶
The validator processes all children before reporting, rather than stopping at the first failure. In non-strict mode, each failure is logged individually. In strict mode, the raised HierarchyValidationError contains the complete list of ValidationFailure objects:
from functualize._discovery.hierarchy_validator import HierarchyValidationError
try:
app = FunctualizeApp(name="ops-cli", ...)
except HierarchyValidationError as e:
for failure in e.failures:
print(f"{failure.child_namespace}: {failure.reason}")
# failure.failure_type is one of:
# "version_incompatible", "cycle_detected", "depth_exceeded"
Summary¶
| Scenario | Non-strict (default) | Strict mode |
|---|---|---|
| Version mismatch | Skip child, log warning | Collect failure, raise error after all checks |
| Cycle detected | Skip child, log warning | Collect failure, raise error after all checks |
| Depth exceeded | Skip child, log warning | Collect failure, raise error after all checks |
| Unknown version | Log warning, allow mount | Log warning, allow mount |
| All children valid | Mount all children | Mount all children |