Data Architecture
This document describes the recipe metadata system used by the CLI and API to generate optimized system configuration recommendations (i.e. recipes) based on environment parameters.
Overview
The recipe system is a rule-based configuration engine that generates tailored system configurations by:
- Starting with a base recipe - Universal component definitions and constraints applicable to every recipe
- Matching environment-specific overlays - Targeted configurations based on query criteria (service, accelerator, OS, intent)
- Resolving inheritance chains - Overlays can inherit from intermediate recipes to reduce duplication
- Merging configurations - Components, constraints, and values are merged with overlay precedence
- Computing deployment order - Topological sort of components based on dependency references
The recipe data is organized in recipes/ as multiple YAML files:
Note: These files are embedded into both the CLI binary and API server at compile time, making the system fully self-contained with no external dependencies.
Extensibility: The embedded data can be extended or overridden using the
--dataflag. See External Data Provider for details.
Recipe Usage Patterns:
-
CLI Query Mode - Direct recipe generation from criteria parameters:
-
CLI Snapshot Mode - Analyze captured system state to infer criteria:
-
API Server - HTTP endpoint (query mode only):
Data Structure
Recipe Metadata Format
Each recipe file follows this structure:
Top-Level Fields
Criteria Fields
Criteria define when a recipe matches a user query:
All fields are optional. Unpopulated fields act as wildcards (match any value).
Constraint Format
Constraints use fully qualified measurement paths:
Constraint Path Format: {MeasurementType}.{Subtype}.{Key}
Supported Operators: >=, <=, >, <, ==, !=, or exact match (no operator)
Component Reference Structure
Each component in componentRefs defines a deployable unit. Components can be either Helm or Kustomize based.
Helm Component Example:
Kustomize Component Example:
Component Fields
Multi-Level Inheritance
Recipe files support multi-level inheritance through the spec.base field. This enables building inheritance chains where intermediate recipes capture shared configurations, reducing duplication and improving maintainability.
Inheritance Mechanism
Each recipe can specify a parent recipe via spec.base:
Inheritance Chain Example
The system supports inheritance chains of arbitrary depth:
Resolution Order: When resolving gb200-eks-ubuntu-training:
- Start with
overlays/base.yaml(root) - Merge
overlays/eks.yaml(EKS-specific settings) - Merge
overlays/eks-training.yaml(training optimizations) - Merge
overlays/gb200-eks-training.yaml(GB200 + training-specific overrides) - Merge
overlays/gb200-eks-ubuntu-training.yaml(Ubuntu + full-spec overrides)
Inheritance Rules
1. Base Resolution
spec.base: ""or omitted → Inherits directly fromoverlays/base.yamlspec.base: "eks"→ Inherits from the recipe named “eks”- The root
overlays/base.yamlhas no parent (it’s the foundation)
2. Merge Precedence Later recipes in the chain override earlier ones:
3. Field Merging
- Constraints: Same-named constraints are overridden; new constraints are added
- ComponentRefs: Same-named components are merged field-by-field; new components are added
- Criteria: Each recipe defines its own criteria (not inherited)
Intermediate vs Leaf Recipes
Intermediate Recipes (e.g., eks.yaml, eks-training.yaml):
- Have partial criteria (not all fields specified)
- Capture shared configurations for a category
- Can be matched by user queries (but typically less specific)
Leaf Recipes (e.g., gb200-eks-ubuntu-training.yaml):
- Have complete criteria (all required fields)
- Matched by specific user queries
- Contain final, hardware-specific overrides
Example: Inheritance Chain
Benefits of Multi-Level Inheritance
Mixin Composition
Inheritance is single-parent (spec.base), which means cross-cutting concerns like OS constraints or platform components would need to be duplicated across leaf overlays. Mixins solve this by providing composable fragments that leaf overlays reference via spec.mixins.
Mixin files live in recipes/mixins/ and use kind: RecipeMixin:
Leaf overlays compose mixins alongside inheritance:
Mixin rules:
- Mixins carry only
constraintsandcomponentRefs— nocriteria,base,mixins, orvalidation - Mixins are applied after inheritance chain merging but before constraint evaluation
- Conflict detection: a mixin constraint or component that conflicts with the inheritance chain or a previously applied mixin produces an error
- When a snapshot is provided, mixin constraints are evaluated against it after merging; if any fail, the entire composed candidate is invalid and falls back to base-only output. In plain query mode (no snapshot), mixin constraints are merged but not evaluated
Criteria-Wildcard Overlays
Some overlays apply across a criteria dimension without being referenced via spec.base or included via spec.mixins. The resolver picks them up automatically because FindMatchingOverlays can return multiple independent maximal-leaf overlays for a single query, not just one. Ancestors of a matched leaf are filtered out of the candidate set, but sibling leaves whose criteria independently match are kept and their inheritance chains are resolved and merged in parallel. See Criteria Matching Algorithm and Recipe Generation Process for details.
This is useful for content that cross-cuts one criteria dimension but must stay tied to others — for example, a GB200 NCCL bandwidth target that applies to every service (EKS, OKE, etc.) but only for GB200 + training.
When a query specifies {service: eks, accelerator: gb200, intent: training}, the resolver returns three maximal leaves — gb200-eks-training (matched by explicit criteria), gb200-any-training (matched by wildcard service: any), and monitoring-hpa (matched by wildcard intent: any). Their inheritance chains are resolved and merged with the base spec:
The nccl-all-reduce-bw constraint from gb200-any-training lands in the hydrated recipe without being duplicated in each service-specific overlay. (Adding os: ubuntu to the query would extend the chain with gb200-eks-ubuntu-training as the maximal leaf in place of gb200-eks-training; gb200-any-training would still match independently.)
Naming convention. The -any- segment signals this pattern: the static segments indicate the fixed criteria dimensions (accelerator, intent), and any marks the wildcard dimension. Examples: gb200-any-training.yaml, b200-any-training.yaml.
When to use a criteria-wildcard overlay vs a mixin:
Precedence when a wildcard overlay and a service-specific leaf collide. FindMatchingOverlays sorts its returned leaves by Criteria.Specificity() ascending, so less-specific overlays merge first and more-specific overlays merge last. Two different merge rules apply — they are not the same:
- Top-level
spec.constraintsmerge by name. A same-named constraint from the more-specific leaf overrides the wildcard’s value (the “overridden, new added” rule from the merge algorithm). spec.validation.<phase>blocks (deployment, performance, conformance) are replaced wholesale when a later overlay defines the same phase — no field-level merge. The leaf’schecksandconstraintsreplace the wildcard’s entire block.
This distinction matters. To override only the threshold in the wildcard example above, a service-specific leaf must restate both checks and constraints:
Setting only constraints drops the wildcard’s checks, which causes filterEntriesByRecipe to return zero entries and the performance phase to be skipped entirely — the opposite of the “lower the threshold” intent.
Criteria-wildcard overlays are only appropriate when the content is genuinely uniform across the wildcard dimension. If the value diverges (e.g., H100 NCCL targets differ by cloud: AKS ≥ 100, EKS ≥ 300, GKE ≥ 250), keep it inline in each service-specific overlay — collapsing divergent values to a lowest-common-denominator wildcard silently weakens validation.
See also: ADR-005: Overlay Refactoring — rationale for the maximal-leaf resolver semantics (Phase 2) and why wildcard overlays are preferred over multi-parent inheritance or intermediate-reparenting approaches that were prototyped and rejected.
Cycle Detection
The system detects circular inheritance to prevent infinite loops:
Tests in pkg/recipe/yaml_test.go automatically validate:
- All
spec.basereferences point to existing recipes - No circular inheritance chains exist
- Inheritance depth is reasonable (max 10 levels)
Component Configuration
Components are configured through a three-tier system with clear precedence.
Configuration Patterns
Pattern 1: ValuesFile Only Traditional approach - all values in a separate file:
Pattern 2: Overrides Only Fully self-contained recipe with inline values:
Pattern 3: ValuesFile + Overrides (Hybrid) Reusable base with recipe-specific tweaks:
Value Merge Precedence
When values are resolved, merge order is:
- Base ValuesFile: Values from inherited recipes
- Overlay ValuesFile: Values file specified in the matching overlay
- Overlay Overrides: Inline
overridesin the overlay’s componentRef - CLI —set flags: Runtime overrides from
aicr bundle --set
Component Values Files
Values files are stored in recipes/components/{component}/:
Dependency Management
Components can declare dependencies via dependencyRefs:
The system performs topological sort to compute deployment order, ensuring dependencies are deployed before dependents. The resulting order is exposed in RecipeResult.DeploymentOrder.
Criteria Matching Algorithm
The recipe system uses an asymmetric rule matching algorithm where recipe criteria (rules) match against user queries (candidates).
Matching Rules
A recipe’s criteria matches a user query when every non-”any” field in the criteria is satisfied by the query:
- Empty/unpopulated fields in recipe criteria = Wildcard (matches any query value)
- Populated fields must match exactly (case-insensitive)
- Matching is asymmetric: A recipe with specific fields (e.g.,
accelerator: h100) will NOT match a generic query (e.g.,accelerator: any)
Asymmetric Matching Explained
The key insight is that matching is one-directional:
- Recipe “any” (or empty) → Matches ANY query value (acts as wildcard)
- Query “any” → Only matches recipe “any” (does NOT match specific recipes)
This prevents overly specific recipes from being selected when the user hasn’t specified those criteria.
Matching Logic
Specificity Scoring
When multiple recipes match, they are sorted by specificity (number of non-”any” fields). More specific recipes are applied later, giving them higher precedence:
Matching Examples
Example 1: Broad Recipe Matches Specific Query
Example 2: Specific Recipe Doesn’t Match Different Specific Query
Example 3: Specific Recipe Doesn’t Match Generic Query (Asymmetric)
This asymmetric behavior ensures that a generic query like --service eks --intent training only matches generic recipes, not hardware-specific ones like gb200-eks-training.yaml.
Example 4: Multiple Maximal Matches (Fully Specific Query)
Note that multiple maximal leaves can coexist when their inheritance chains are independent — gb200-any-training (via wildcard service: any) and gb200-eks-ubuntu-training (via explicit criteria) are both kept because neither is an ancestor of the other. This is what enables the criteria-wildcard overlay pattern.
Recipe Generation Process
The recipe builder (pkg/recipe/metadata_store.go) generates recipes through the following steps:
Step 1: Load Metadata Store
- Embedded YAML files are parsed into Go structs
- Cached in memory on first access (singleton pattern with
sync.Once) - Contains base recipe, all overlays, mixins, and component values files
Step 2: Find Matching Overlays
- Iterate all overlays in
s.Overlays(the base recipe is held separately ins.Baseand is not a candidate here — it is injected as the merge seed byinitBaseMergedSpec()in Step 4) - Check if each overlay’s criteria matches the user query
- Filter to maximal leaves via
filterToMaximalLeaves(): drop any match that is an ancestor (viaspec.base) of another match. Ancestors are re-added later via chain resolution; this filter ensures that a matched descendant isn’t double-counted with its own chain - Sort maximal-leaf matches by specificity (least specific first)
Multiple maximal leaves can be returned for one query when they sit on independent inheritance chains — for example, a service: any wildcard overlay and the most-specific service-specific leaf are both kept (see Criteria-Wildcard Overlays).
Step 3: Resolve Inheritance Chains
For each maximal-leaf match from step 2:
- Build the chain from root (base) to the target overlay by walking
spec.base - Detect cycles to prevent infinite loops
- Example:
["base", "eks", "eks-training", "gb200-eks-ubuntu-training"]
Ancestors filtered out in step 2 re-enter the output here as part of their descendant’s chain.
Step 4: Merge Specifications
The merge starts from a seed containing the base spec, then applies each resolved chain on top:
This is why base always appears first in appliedOverlays even though it is not returned by FindMatchingOverlays.
Merge Algorithm
- Constraints: Same-named constraints are overridden; new constraints are added
- ComponentRefs: Same-named components are merged field-by-field using
mergeComponentRef()
Step 5: Apply Mixins
- If the leaf overlay declares
spec.mixins, each named mixin is loaded fromrecipes/mixins/ - Mixin constraints and componentRefs are appended to the merged spec
- Conflict detection prevents duplicates between the inheritance chain, previously applied mixins, and the current mixin
- When a snapshot evaluator is provided, mixin constraints are evaluated against it after merging; failure invalidates the entire composed candidate. In plain query mode (no snapshot), mixin constraints are merged but not evaluated
Step 6: Validate Dependencies
- Verify all
dependencyRefsreference existing components - Detect circular dependencies
Step 7: Compute Deployment Order
- Topologically sort components based on
dependencyRefs - Ensures dependencies are deployed before dependents
Step 8: Build RecipeResult
Complete Flow Diagram
Usage Examples
CLI Usage
Basic recipe generation (query mode):
Full specification:
From snapshot (snapshot mode):
API Usage
Basic query:
Full specification:
Example Response (RecipeResult)
Maintenance Guide
Adding a New Recipe
-
Create the recipe file in
recipes/: -
Create intermediate recipes if needed (e.g.,
gke.yaml,gke-inference.yaml) -
Add component values files if using new configurations:
-
Run tests to validate:
Modifying Existing Recipes
-
Update constraints - Change version requirements:
-
Update component versions - Bump chart versions:
-
Add inline overrides - Recipe-specific tweaks:
Updating Component Values
-
Modify values file in
recipes/components/{component}/values.yaml -
Create variant values file for specific environments:
values.yaml- Base configurationvalues-eks-training.yaml- EKS training optimizationvalues-gke-inference.yaml- GKE inference optimization
-
Reference in recipe:
Automated Validation
The recipe data system includes comprehensive automated tests to ensure data integrity. These tests run automatically as part of make test and validate all recipe metadata files and component values.
Test Suite Overview
The test suite is organized in pkg/recipe/:
Test Categories
Inheritance-Specific Tests
Running Tests
CI/CD Integration
Tests run automatically on:
- Pull Requests: All tests must pass before merge
- Push to main: Validates no regressions
- Release builds: Ensures data integrity in released binaries
Adding New Tests
When adding new recipe metadata or component configurations:
- Create the new file in
recipes/ - Run tests to verify the file is valid:
- Check for conflicts with existing recipes:
- Verify references if using valuesFile or dependencyRefs:
External Data Provider
The recipe system supports extending or overriding embedded data with external files via the --data CLI flag. This enables customization without rebuilding the CLI binary.
Architecture Overview
Data Provider Interface
The system uses a DataProvider interface to abstract file access:
Provider Types:
EmbeddedDataProvider: Wraps Go’sembed.FSfor compile-time embedded dataLayeredDataProvider: Overlays external directory on top of embedded data
Merge Behavior
Registry Merge Algorithm
When merging registry.yaml, components are matched by their name field:
Merge Order:
- Start with all embedded components
- Replace any that have same name in external
- Add any new components from external
Security Validations
The LayeredDataProvider enforces security constraints:
Configuration Options
Usage Example
Creating an external data directory:
External registry.yaml (adds custom Helm component):
External registry.yaml (adds custom Kustomize component):
Note: A component must have either helm OR kustomize configuration, not both.
CLI usage:
Debugging
Use --debug flag to see detailed logging about external data loading:
Debug logs include:
- Files discovered in external directory
- Source resolution for each file (embedded vs external vs merged)
- Component merge details (added, overridden, retained)
Implementation Details
The data provider is initialized early in CLI command execution:
Global Provider Pattern:
SetDataProvider()sets the global data providerGetDataProvider()returns the current provider (defaults to embedded)GetDataProviderGeneration()returns a counter for cache invalidation
See Also
- Recipe Development Guide - Adding and modifying recipe data
- CLI Architecture - How the CLI uses recipe data
- CLI Reference - Complete CLI flag reference
- API Server Architecture - How the API serves recipes
- OpenAPI Specification - Recipe API contract
- Recipe Package Documentation - Go implementation details