Summary
Complete the facade-ownership story started by #1078 by replacing the four remaining transparent type aliases in pkg/client/v1/types.go (Recipe, AllowLists, Criteria, CriteriaRegistry) with facade-owned structs and translation funcs at the boundary.
Motivation
pkg/client/v1 is the Public (stable) facade per docs/integrator/public-api.md, but four of its types remain transparent aliases into pkg/recipe (Public (evolving)):
type Recipe = recipe.RecipeResult
type AllowLists = recipe.AllowLists
type Criteria = recipe.Criteria
type CriteriaRegistry = recipe.CriteriaRegistry
This is the same anti-pattern #1078 fixed for Snapshot / AgentConfig / PhaseResult / Phase: any field-shape change in pkg/recipe propagates to external Go importers without a minor-version signal, undermining the semver promise.
#1078 was originally scoped to the snapshotter/validator-tier aliases only. The recipe-tier aliases were added later by #1077 (the CLI/API migration PR) and tracked here for follow-up.
Complication — existing dual surface
The facade today has both:
aicr.Recipe = recipe.RecipeResult — transparent alias used by ResolveRecipeFromCriteria, ResolveRecipeFromSnapshot, LoadRecipe, AdoptRecipe, SelectFromRecipe
aicr.RecipeResult struct { … internal *recipe.RecipeResult … } — facade-owned wrapper used by ResolveRecipe, BundleComponents, ValidateState
The two coexist because they were added in different PRs (RecipeResult by #1072; Recipe alias by #1077). They overlap functionally but expose different fields.
Resolving this means picking one shape, not just adding a wrapper. Options:
- Consolidate on
RecipeResult (facade-owned struct). Delete the Recipe alias; migrate methods that returned *Recipe to return *RecipeResult. Translation at the boundary populates the public fields from the internal *recipe.RecipeResult.
- Consolidate on
Recipe (now as a facade-owned struct). Delete RecipeResult; migrate the existing methods. Recipe would carry both the public payload and an unexported internal *recipe.RecipeResult field.
Either way, this is a bigger change than just dropping the = aliases.
Type-by-type sketch
| Type |
Underlying |
Wrap shape |
Recipe (or RecipeResult) |
recipe.RecipeResult (~10 fields, sub-types: Constraint, ComponentRef, ValidationConfig, ExcludedOverlay, ConstraintWarning) |
Pick the dual-surface resolution above. Mirror public fields; hold internal *recipe.RecipeResult for zero-copy round-trip and the ownership/provider tokens. |
AllowLists |
recipe.AllowLists (~5 string-slice fields) |
Mirror fields; translate at WithAllowLists / ParseAllowListsFromEnv. |
Criteria |
recipe.Criteria (~7 enum fields) |
Mirror fields; translate at REST parseQuery / parseBody, ResolveRecipeFromCriteria, SelectFromRecipe. |
CriteriaRegistry |
recipe.CriteriaRegistry (complex internal state, methods: Has, Values, ParseService etc.) |
Trickiest — the registry has behavior, not just data. Two options: (a) re-expose a minimal interface; (b) keep as alias indefinitely and document why. |
CriteriaRegistry may be the right candidate to keep aliased — it's a behavior-rich type that an external importer holding a *Builder needs the same one of. Wrapping it loses the per-provider identity link.
Acceptance
Risk / size
High — ~1500-2000 LOC est. Recipe is the central type; the dual-surface resolution alone deserves its own design discussion before code changes.
Out of scope
Related
Summary
Complete the facade-ownership story started by #1078 by replacing the four remaining transparent type aliases in
pkg/client/v1/types.go(Recipe,AllowLists,Criteria,CriteriaRegistry) with facade-owned structs and translation funcs at the boundary.Motivation
pkg/client/v1is the Public (stable) facade perdocs/integrator/public-api.md, but four of its types remain transparent aliases intopkg/recipe(Public (evolving)):This is the same anti-pattern #1078 fixed for
Snapshot/AgentConfig/PhaseResult/Phase: any field-shape change inpkg/recipepropagates to external Go importers without a minor-version signal, undermining the semver promise.#1078 was originally scoped to the snapshotter/validator-tier aliases only. The recipe-tier aliases were added later by #1077 (the CLI/API migration PR) and tracked here for follow-up.
Complication — existing dual surface
The facade today has both:
aicr.Recipe = recipe.RecipeResult— transparent alias used byResolveRecipeFromCriteria,ResolveRecipeFromSnapshot,LoadRecipe,AdoptRecipe,SelectFromRecipeaicr.RecipeResult struct { … internal *recipe.RecipeResult … }— facade-owned wrapper used byResolveRecipe,BundleComponents,ValidateStateThe two coexist because they were added in different PRs (
RecipeResultby #1072;Recipealias by #1077). They overlap functionally but expose different fields.Resolving this means picking one shape, not just adding a wrapper. Options:
RecipeResult(facade-owned struct). Delete theRecipealias; migrate methods that returned*Recipeto return*RecipeResult. Translation at the boundary populates the public fields from the internal*recipe.RecipeResult.Recipe(now as a facade-owned struct). DeleteRecipeResult; migrate the existing methods. Recipe would carry both the public payload and an unexportedinternal *recipe.RecipeResultfield.Either way, this is a bigger change than just dropping the
=aliases.Type-by-type sketch
Recipe(orRecipeResult)recipe.RecipeResult(~10 fields, sub-types:Constraint,ComponentRef,ValidationConfig,ExcludedOverlay,ConstraintWarning)internal *recipe.RecipeResultfor zero-copy round-trip and the ownership/provider tokens.AllowListsrecipe.AllowLists(~5 string-slice fields)WithAllowLists/ParseAllowListsFromEnv.Criteriarecipe.Criteria(~7 enum fields)parseQuery/parseBody,ResolveRecipeFromCriteria,SelectFromRecipe.CriteriaRegistryrecipe.CriteriaRegistry(complex internal state, methods:Has,Values,ParseServiceetc.)CriteriaRegistrymay be the right candidate to keep aliased — it's a behavior-rich type that an external importer holding a*Builderneeds the same one of. Wrapping it loses the per-provider identity link.Acceptance
Recipevs.RecipeResult); decision documented in PR description.Recipe/AllowLists/Criteriaare facade-owned structs inpkg/client/v1/types.go; aliases removed.CriteriaRegistryeither wrapped or explicitly documented as a behavior-rich type kept as alias intentionally.pkg/client/v1/translate.go(extend the existing file).pkg/cli,pkg/api) updated to consume the new shapes.docs/integrator/public-api.md"Facade type ownership" table updated.docs/integrator/go-library.mdexamples updated.make qualifyclean.Risk / size
High — ~1500-2000 LOC est. Recipe is the central type; the dual-surface resolution alone deserves its own design discussion before code changes.
Out of scope
pkg/recipeback-compat shims — tracked separately (see [Epic]: Post-facade adoption and surface hardening (follow-ups to #1072) #1076 follow-up issues).Related