@gram-lang/kitchen
The compiler. Takes a RecipeAST (from @gram-lang/parser) and produces a CompilationResult: a clean, structured, render-ready JSON payload — shopping list, per-section instructions with timings, a global ingredient/cookware registry, and any structural warnings. No ingredient database is involved at this stage; that's @gram-lang/analyzer's job.
compile
function compile(ast: RecipeAST, options?: CompilerOptions): CompilationResultimport { getAST } from '@gram-lang/parser';
import { compile } from '@gram-lang/kitchen';
const ast = getAST(source);
const compiled = compile(ast);
// compiled.shopping_list, compiled.sections, compiled.metrics, compiled.warnings, ...Throws a plain Error for structural violations that can't be represented as a recoverable warning — e.g. more than one ingredient marked with the baker's-percentage (*) modifier.
CompilerOptions
interface CompilerOptions {
scaleFactor?: number; // bakes a flat multiplier into the compiled output
}CompilationResult
interface CompilationResult {
title: string | null;
slug: string | null;
meta: Meta; // parsed frontmatter
scaleFactor?: number; // present once any scaling has been applied
registry: {
ingredients: Record<string, RegistryEntry>;
cookware: Record<string, { id: string; name: string }>;
};
shopping_list: (ShoppingListItem | CompositeItem | Usage)[];
cookware: Usage[];
sections: ProcessedSection[];
warnings: Warning[];
metrics: {
preparationTime: number; // estimated mise-en-place time (minutes)
cookTime: number; // critical path duration (minutes)
activeTime: number; // sum of active cook work time (minutes)
totalTime: number; // preparationTime + cookTime
};
}See Data Formats for a fully annotated example of this shape, and Warnings for what can appear in .warnings.
Scaling
Recipes are compiled at their base quantities; scaling is a separate, composable step so callers (e.g. a live "servings" slider in a UI) can re-scale without re-parsing or re-compiling.
function resolveScaleFactor(
compiled: CompilationResult | null,
request: ScaleRequest,
convertUnit?: UnitConverter,
): ScaleResolution
function applyScale(result: CompilationResult, factor: number): CompilationResultScaleRequest is either a flat multiplier or a target quantity for a specific shopping-list ingredient, which resolveScaleFactor turns into a single factor:
type ScaleRequest =
| { type: "factor"; value: number }
| { type: "target"; id: string; qty: number; unit: string | null };import { resolveScaleFactor, applyScale } from '@gram-lang/kitchen';
// "I want 300g of flour total" -> derives the multiplier from the compiled shopping list
const { factor } = resolveScaleFactor(compiled, { type: 'target', id: 'flour', qty: 300, unit: 'g' });
const scaled = applyScale(compiled, factor);resolveScaleFactor throws a typed ScaleError subclass (each with a .code) when the request can't be satisfied: InvalidFactorError (thrown if scale factor is not positive finite, or if a scaled quantity overflows Infinity), IngredientNotFoundError, NestedOnlyTargetError (target only exists inside a composite sub-recipe), AlternativeTargetError (target is one option of an @a|@b group), FixedIngredientError (marked @= or non-numeric), RelativeTargetError (a %-derived quantity), AmbiguousMultiUnitError (used with incompatible units across the recipe), NonNumericTargetError, UnitMismatchError.
applyScale is pure — it never mutates its input, so the same CompilationResult can be re-scaled repeatedly (e.g. on every slider tick) without compounding. It satisfies the scaling parity invariant: applyScale(compile(ast), factor) ≡ compile(ast, { scaleFactor: factor }).
Shopping list & timing (lower-level)
compile() already calls these internally; they're exported for advanced use (e.g. recomputing a shopping list from a custom-built ProcessedSection[]).
function generateShoppingList(
sections: ProcessedSection[],
registry: Registry,
options?: CompilerOptions,
): (ShoppingListItem | CompositeItem | Usage)[]
function calculatePreparationTime(sections: ProcessedSection[], registry: Registry): numberRecipeRegistry
The mutable ingredient/cookware registry built up during compilation, keyed by slugify(name). Implements the Registry interface (ingredients: Map, cookware: Map, warnings: Warning[]).
class RecipeRegistry implements Registry {
registerIngredient(name: string, data?: Partial<Omit<RegistryEntry, "id" | "name">>): string; // returns id
registerCookware(name: string): string; // returns id
getIngredientId(name: string): string;
toPlainObject(): { ingredients: Record<string, RegistryEntry>; cookware: Record<string, {...}> };
}RegistryEntry carries id, name, and optionally default_unit, is_composite, parent (for composite sub-recipe children), and is_intermediate.
Warnings
CompilationResult.warnings is a Warning[] — structural issues detected during compilation (undefined references, scope conflicts, invalid timer/temperature units, circular references...). See the Warnings reference for the full code list, severities, and WarningCode/pushWarning exports.