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 deployment-phase floor (gpu-operator version pin + standard health checks) that applies to every service (EKS, OKE, etc.) and every intent for the accelerator.
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 (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 gpu-operator version pin from gb200-any 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 would still match independently.)
Naming convention. The -any (or -any-<intent>) segment signals this pattern: the static segments indicate the fixed criteria dimensions (accelerator, optionally intent), and any marks the wildcard dimension. Examples: gb200-any.yaml, h100-any.yaml, rtx-pro-6000-any.yaml.
Don’t carry per-fabric values here. Cross-service-uniform content (gpu-operator version pin, standard health checks) is a good fit. Per-fabric content (NCCL bandwidth thresholds across services with different network fabrics — EFA, TCPXO, RoCE) is not — declare those in each service-specific leaf instead. The intent-scoped gb200-any-training.yaml and b200-any-training.yaml overlays that previously carried cross-service NCCL thresholds were retired for this reason (gb200-any-training in #1052, b200-any-training in #1053).
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. Both top-level constraints and spec.validation.<phase> blocks merge per-field — the more-specific leaf adds to or overrides the wildcard’s block without replacing it wholesale:
- 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 (readiness, deployment, performance, conformance) also merge per-field:checks: an omitted field (nil) inherits the parent’s list; an explicit empty list (checks: []) clears the inherited list for that phase; a non-empty list unions with the parent’s, deduplicated and preserving order (parent entries first, then leaf-only entries appended).constraints: same nil-vs-empty rule aschecks; a non-empty list unions bynamewith the leaf winning on same-name (analogous to the top-level constraint merge).nodeSelection: overlay replaces wholesale when non-nil (full struct replace).timeout,infrastructure: overlay-wins-if-non-empty.
A leaf that only needs to tighten one constraint can declare just that constraint — the wildcard’s checks and other constraints are inherited automatically:
Leaves may still restate the inherited list when it documents intent (e.g., recording which checks the leaf depends on). With per-field union, restating is a no-op: duplicates dedupe against the wildcard’s entries.
To intentionally drop an inherited check or constraint, declare the field as an explicit empty list (checks: [] / constraints: []) rather than omitting it. Omission means “inherit”; explicit empty means “clear”:
This distinction is preserved by yaml.v3: a YAML [] decodes as a non-nil empty slice (clear), while an omitted or null field decodes as nil (inherit).
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.
Regenerating the BOM
docs/user/container-images.md is an auto-generated bill of materials listing every container image AICR pulls across all components. It is regenerated by make bom-docs, which renders each Helm chart against its live OCI source and extracts image references from the rendered templates.
Run make bom-docs and commit the regenerated docs/user/container-images.md in the same PR whenever you:
- Add or remove a component in
recipes/registry.yaml - Bump a chart version (in
registry.yaml, an overlay, or a mixin) - Modify a component’s
values.yamlin a way that changes which images render (image repo override, subchart enable/disable, etc.)
The regen can also surface drift from upstream chart updates — when a chart bumps an image inside its own templates without a registry pin change on our side. That drift will appear in the BOM diff whether you expected it or not. Land it as part of the same PR that triggered the regen, or split it out as a separate “BOM catch-up” PR if the unrelated diff would obscure the primary change.
Freshness is not gated. make bom-check verifies the committed docs/user/container-images.md matches a fresh regen, but it is opt-in — neither make qualify nor make lint runs it today, and the merge gate has no PR-time BOM-staleness check (it only runs bom-pinning-check, which is the chart-pin verification per ADR-006). Run make bom-docs explicitly whenever you touch a component; do not rely on local qualify or CI to catch a missed regen. Wiring bom-check into the gate is a desirable follow-up.
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 (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.
Cluster Fingerprint
aicr snapshot emits a structured fingerprint: block alongside the raw
measurements. The fingerprint is a normalized, schema-stable view of the
cluster-identity dimensions used to bind a snapshot to a recipe — service,
accelerator, OS, Kubernetes server version, region, total node count, and
GPU node count — so an evidence bundle (per
ADR-007) can prove the recipe was
tested on hardware matching its declared criteria.
The fingerprint is derived from the same collector outputs that populate
measurements:; it is not a separate collection pass. Dimensions whose
source signal is missing surface as zero-value entries — the verifier
treats those as “unknown” rather than fabricating a match.
The persisted
fingerprint:block is advisory only. It is a convenience for humans reading the snapshot YAML, not a trust-bearing claim. The snapshot file is not signed at this layer — an attacker controlling it could swap the embedded fingerprint without touching the measurements that back it. Trust-bearing consumers — the evidence bundler (#754), the verifier (#753), and any downstream policy gate — MUST recompute viafingerprint.FromMeasurements(snap.Measurements)before acting on the result, and treat zero-value entries as “unknown” per the match semantics below.
Fingerprint Schema
Heterogeneous and stale-registry dimensions
When accelerator or region cannot be collapsed to a single Value,
the fingerprint surfaces the reason via an optional note: field
instead of fabricating one. The verifier renders this distinct from
“value not captured” in its Markdown output. Three notes are emitted
today:
multi-region— nodes carry differenttopology.kubernetes.io/regionlabelsmulti-gpu— nodes carry differentnvidia.com/gpu.productlabelsunknown-sku— nvidia-smi or the GPU operator reported a product string that is not in the recipe accelerator registry (likely registry staleness; the raw model is still recoverable from the underlying measurement)
Every dimension carries a value (the resolved, normalized string the
recipe criteria block can be compared against), a source string
identifying which collector signal produced it, and an optional note
string carrying a short audit hint when Value is empty for a reason
other than missing data (the cases above). ADR-007 reserves additional
optional fields (signals[], confidence) for a future multi-signal
corroboration extension; V1 records source and note only.
Detection Sources
A dimension whose source signal is missing keeps its zero value. The
verifier reports it as unknown rather than mismatched.
Match Semantics
fingerprint.Fingerprint.Match compares a fingerprint against a
recipe’s criteria and returns a per-dimension diff plus an overall
matched flag. Each criteria dimension resolves to one of three
outcomes:
matched— the recipe is generic (any/ empty) for this dimension, OR the fingerprint captured the same value the recipe requires.mismatched— the recipe requires a specific value and the fingerprint captured a different specific value.unknown— the recipe requires a specific value but the fingerprint cannot prove or disprove it. Two cases produceunknown: a dimension the cluster does not reveal (intent,platform— recipe-author choices) and a dimension the fingerprint failed to detect (e.g., no GPU collector output).
The overall matched flag is true when no dimension is mismatched.
Unknowns surface in the per-dimension diff for human review without
flipping the overall outcome — the fingerprint cannot disprove a
match it does not capture.
Worked Example
Recipe criteria: service=eks, accelerator=h100, intent=training, os=ubuntu, platform=kubeflow
plus the fingerprint above.
perDimension is an ordered list so iteration is deterministic and
serialization is byte-stable; consumers needing lookup by name use
MatchResult.Find rather than indexing.
The bundle’s predicate body (per ADR-007
PR-A / #754) records this diff as criteriaMatch.perDimension; the
verifier (#753) renders it in a Markdown summary so the maintainer
sees exactly which dimensions the fingerprint corroborated.
The predicate body preserves the three-way match: state verbatim
(matched | mismatched | unknown) rather than collapsing to a bool.
The ADR-007 example shows match: true for the happy-path case where
every dimension is matched, but the schema must keep unknown
distinguishable from matched — a maintainer reviewing a bundle
needs to see “intent and platform were not corroborated by the
fingerprint” rather than “everything matched.” A CI gate keyed on
criteriaMatch.matched: true alone gives unknown dimensions a free
pass; gates that need stronger guarantees should also assert that
no per-dimension entry has match: unknown.
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:
Cancellation is honored at I/O boundaries — before each file open and
between WalkDir entries — not mid-syscall on an in-flight read.
LayeredDataProvider reads external files via os.Open + io.ReadAll,
which are not cancelable once started; a hung NFS / sshfs mount blocked
mid-readv will see cancellation honored on the next file the walk
touches, not the one currently blocked.
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)
Builder-bound providers
WithDataProvider is the canonical way to attach a DataProvider to a recipe
build. The returned Builder resolves its metadata store, component registry,
and per-component values files through the bound provider — never through the
process-global one. Each call site that builds recipes should construct its
own provider and pass it through WithDataProvider:
The resulting *RecipeResult carries the same provider so downstream
consumers (GetValuesForComponent, GetManifestContentWithProvider) resolve
files against the build’s provider rather than whatever global is currently
installed. Recover it via result.DataProvider() — nil-safe on the receiver
and returns nil when the result was built against the package-global fallback.
Per-Builder isolation
Builders with distinct providers do not share cache state. LoadMetadataStoreFor
and GetComponentRegistryFor key their caches by DataProvider identity, so a
process can host multiple tenants concurrently without one tenant’s --data
overlay leaking into another’s catalog. Use this pattern in multi-tenant
servers, test harnesses that need clean state per case, or anywhere two
Builder instances may evaluate different inputs at the same time:
To force a rebuild on the next read (e.g., after rewriting an external overlay on disk), drop the entries for that provider:
Evict* is a no-op on a nil receiver, and concurrent builders against
other providers are unaffected — eviction is scoped to the supplied
provider only.
CLI initialization
Each CLI command owns a per-command aicr.Client whose own DataProvider
backs recipe resolution and the per-provider criteria registry — no
process-global provider is installed:
The Client binds its provider through recipe.WithDataProvider internally,
so concurrent commands (and multi-tenant servers) stay isolated. Library
callers construct their own aicr.Client, or call
recipe.NewBuilder(recipe.WithDataProvider(dp)) directly.
The former package-global accessors (SetDataProvider, GetDataProvider,
GetDataProviderGeneration) have been removed — bind a provider via
WithDataProvider and recover it with (*RecipeResult).DataProvider().
Provider-bound helpers that need a default fall back to a single shared
embedded provider internally; callers no longer pass the global.
Criteria Registry
The criteria registry is a per-DataProvider cache of valid criteria values
(service, accelerator, intent, os, platform) populated from
loaded overlays. It is the mechanism by which a --data overlay can
introduce a new criteria value (e.g., service: ncp-internal) and have
it admitted by (*CriteriaRegistry).ParseService without a code change.
Each aicr.Client (and so each Builder constructed with
WithDataProvider) owns its own registry, scoped by DataProvider
identity. Concurrent clients with different --data directories do not
share registry state; closing the client evicts its entry.
Why a registry
Before the registry existed, each criteria parser had a hardcoded
switch of valid string values; an unknown value returned
ErrCodeInvalidRequest before the overlay catalog was even consulted.
That made it impossible to add a new criteria value via --data —
internal/proprietary values required either a fork or an upstream
contribution, neither of which scales for undisclosed NCPs or
proprietary product overlays.
The registry decouples which values are valid from what the OSS
binary knows about. The fast-path switch arms remain for canonical
aliases (self-managed → any, al2 → amazonlinux), and any value not
matched there falls through to the registry, which is seeded by the
overlay loader on catalog load.
Origin tracking
Each registered value carries a CriteriaOrigin:
OriginEmbedded— declared in an overlay loaded from the binary’s embedded data filesystem (the OSS catalog).OriginExternal— declared in an overlay loaded from--data.
When the same value is registered from both sources, embedded wins —
Register never downgrades an embedded value to external, so strict
mode lookups remain stable across reloads.
Strict mode
Strict mode hides external-origin entries from registry lookups, restoring the pre-registry behavior in which only OSS canonical values validate. Three sources can enable it (logical OR):
--criteria-strictCLI flag (added onaicr recipe).spec.recipe.criteriaStrict: truein--config.AICR_CRITERIA_STRICT=1env var (read at registry construction —NewCriteriaRegistry/GetCriteriaRegistryFor).
Strict mode is intended for OSS CI gates — make qualify exports
AICR_CRITERIA_STRICT=1 for the unit-test step so the upstream catalog
cannot accidentally start depending on internal-only values that only
exist in someone’s --data directory.
Seeding the registry
The metadata-store loader (pkg/recipe/metadata_store.go) walks every
overlay during catalog load and stages each overlay’s criteria for
registration. The provider’s Source(path) returns "embedded" /
"external" / "merged"; the seed helper maps "embedded" to
OriginEmbedded and every non-embedded source (including "merged"
and any unknown future category) to OriginExternal. The registration
is deferred until after all overlays parse cleanly, the base recipe
is present, and dependency validation passes — partial catalog loads
never leak into the registry.
Eager load via LoadCatalog
The metadata store loads lazily on first read. Because criteria
validation runs before the recipe build pulls the catalog, a fresh
process with --data would otherwise reject a custom criteria value on
the very first call — the registry would still be empty.
(*aicr.Client).LoadCatalog(ctx) forces an eager catalog parse, seeding
the Client’s per-provider registry. The CLI recipe Action calls it right
after constructing the Client so the registry is populated before any
criteria lookup. Any caller that wires its own --data provider must
likewise call LoadCatalog before validating criteria for the same
reason. (The package-level recipe.LoadCatalog(ctx) seeds the default
embedded provider’s registry and is used by the embedded-only path.)
API surface
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