import type { Recipe } from "./schema/recipe.js"; /** * Cross-recipe validation: walks every handle reference in a recipe set * and verifies it resolves to an extant recipe of the right kind. * * `compileRecipe` operates on one recipe at a time and can't see the * surrounding set; this module is the seam that catches dangling or * mistyped references before the planner / executor swallows them. * * Reference inventory checked: * * ComponentTemplateRecipe / ContentTemplateRecipe * fields[*].sitecore.source.types[*] → any template-bearing recipe * params[*].sitecore.source.types[*] → any template-bearing recipe * insertOptions[*] → any template-bearing recipe * * ComponentTemplateRecipe * placeholders[*].allowedComponents[*]→ ComponentTemplateRecipe * * ContentItemRecipe * templateType → any template-bearing recipe * fields[*].link-internal.ref → any recipe * fields[*].reference.refs[*] → any recipe * * PageTemplateRecipe * fields[*].sitecore.source.types[*] → any template-bearing recipe * insertOptions[*] → PageTemplateRecipe * layout.placeholders[*][*].componentHandle → ComponentTemplateRecipe * layout.placeholders[*][*].datasourceRef.handle → ContentItemRecipe * * PlaceholderRecipe * allowedComponents[*] → ComponentTemplateRecipe * * PageRecipe * template → PageTemplateRecipe * fields[*].link-internal.ref → any recipe * fields[*].reference.refs[*] → any recipe * layout.placeholders[*][*].componentHandle → ComponentTemplateRecipe * layout.placeholders[*][*].datasourceRef.handle → ContentItemRecipe * * PartialDesignRecipe * layout.placeholders[*][*].componentHandle → ComponentTemplateRecipe * layout.placeholders[*][*].datasourceRef.handle → ContentItemRecipe * * PageDesignRecipe * appliesTo[*] → PageTemplateRecipe * partials[*] → PartialDesignRecipe * layout.placeholders[*][*].componentHandle → ComponentTemplateRecipe * layout.placeholders[*][*].datasourceRef.handle → ContentItemRecipe * * SiteTemplateRecipe * dictionaries[*] → DictionaryRecipe * * DictionaryRecipe * site → SiteRecipe * * Shared-site uniqueness: at most ONE SiteRecipe per collection (keyed * on collectionId / collectionName) may carry `siteRole: "shared"`. * Reported as a `FieldShapeError` on the first offender. * * Beyond reference resolution this also checks **placement legality** — * a layout placement into a recipe-defined placeholder whose * `Allowed Controls` whitelist doesn't include the component is reported * as a `PlacementViolation` — and flags a placeholder `key` declared by * more than one recipe. * * Cycle detection covers `insertOptions` chains * (`ComponentTemplate.insertOptions → ContentTemplate.insertOptions → …`) * — the only place the current schema permits transitive recipe-to-recipe * references that could loop. Partial-to-partial cycles aren't possible * today (`PartialDesignRecipe` doesn't reference other partials); if * sub-partial composition is ever added, extend the DFS below. */ export type RecipeKind = Recipe["kind"]; /** A handle reference that doesn't resolve, or resolves to the wrong kind. */ export interface UnresolvedHandle { /** Handle of the recipe that contains the bad reference. */ fromRecipe: string; /** Dotted path inside the recipe — `layout.placeholders./header.0.componentHandle`. */ fromField: string; /** The reference value that didn't resolve. */ handle: string; /** Which recipe kinds would be valid resolutions. */ expectedKinds: readonly RecipeKind[]; /** The kind that was found (or undefined if no recipe with that handle exists). */ actualKind: RecipeKind | undefined; } /** A handle that appears more than once in the recipe set. */ export interface DuplicateHandle { handle: string; count: number; } /** A cyclic chain of `insertOptions` references. */ export interface CyclicReference { /** First handle in the cycle. */ startHandle: string; /** Ordered handles that form the cycle (last entry = startHandle, closing the loop). */ cycle: readonly string[]; } /** * A field-shape constraint that Zod can't enforce. Today this is the * `SiteRecipe` collectionId XOR collectionName presence check — the * Zod schema can't carry a `.refine()` because `RecipeSchema` is a * discriminated union, and discriminated unions reject `ZodEffects` * members. Cross-field constraints land here instead. */ export interface FieldShapeError { /** Handle of the recipe with the bad shape. */ fromRecipe: string; /** Dotted path to the field(s) involved. */ fromField: string; /** Operator-readable explanation. */ message: string; } /** * A layout placement that drops a component into a recipe-defined * placeholder whose `Allowed Controls` whitelist doesn't include it — * the "what's allowed in a placeholder" enforcement. * * Only raised for placeholders the recipe set itself defines (a * `PlaceholderRecipe` or an inline `ComponentTemplateRecipe.placeholders` * slot) AND that carry a non-empty whitelist. Placements into * pre-existing tenant placeholders, or into recipe-defined placeholders * with an empty (unrestricted) whitelist, are not checkable here and * pass. */ export interface PlacementViolation { /** Handle of the recipe that holds the offending layout. */ fromRecipe: string; /** Dotted path to the placement — `layout.placeholders./header.0`. */ fromField: string; /** The component handle being placed. */ componentHandle: string; /** The placeholder key it was placed into. */ placeholderKey: string; /** The component handles the placeholder's whitelist does allow. */ allowedComponents: readonly string[]; } export interface ValidationResult { unresolvedHandles: UnresolvedHandle[]; duplicateHandles: DuplicateHandle[]; cycles: CyclicReference[]; fieldShapeErrors: FieldShapeError[]; placementViolations: PlacementViolation[]; } export declare const isValid: (result: ValidationResult) => boolean; /** * Render a `ValidationResult` as a multi-line, human-readable error * report. Use `validateRecipeSetOrThrow` if you just want exceptions. */ export declare function formatValidationErrors(result: ValidationResult): string; /** * Validate cross-recipe references in a recipe set. Returns a result * with all detected problems — caller decides whether to throw, log, * or surface them in CLI output. */ export declare function validateRecipeSet(recipes: readonly Recipe[]): ValidationResult; /** * Convenience: validate and throw on any error. Use in pipelines that * should hard-stop before compilation when the recipe set is malformed. */ export declare function validateRecipeSetOrThrow(recipes: readonly Recipe[]): void;