@gram-lang/analyzer
Enriches a CompilationResult (from @gram-lang/kitchen) with physical properties — standardized mass, purchasing yield, nutrition estimates, and baker's percentages — by cross-referencing an ingredient database. This is the only stage that needs a database; parsing and compilation work on any recipe with no external data.
analyze
function analyze(
result: CompilationResult,
database: Record<string, IngredientData>,
options?: AnalyzerOptions,
): AnalysisResult
interface AnalysisResult {
result: AnalyzedCompilationResult;
missingIngredients: string[]; // ids present in the recipe but absent from `database`
}import { compile } from '@gram-lang/kitchen';
import { analyze } from '@gram-lang/analyzer';
const compiled = compile(ast);
const { result: analyzed, missingIngredients } = analyze(compiled, myIngredientDatabase);The ingredient database is the second positional argument, not a field on
options—analyze(compiled, database, options?).
analyze() is pure and never mutates result. AnalyzedCompilationResult extends CompilationResult — same shape, plus per-usage normalizedMass/conversionMethod/isEstimate/purchasingMass/bakersPercentage fields and a metrics.nutrition block. See Data Formats for a fully annotated example.
AnalyzerOptions
All flags default to enabled (!== false checks internally) — pass false to opt out of a given enrichment pass.
| Option | Type | Description |
|---|---|---|
enableMassStandardization | boolean | Convert ingredient quantities into standardized grams. |
enableYieldCalculation | boolean | Apply the ingredient database's physical.yield (waste factor) when standardizing mass. |
enableNutritionalEstimation | boolean | Compute metrics.nutrition (calories, macros, optionally per-portion). |
enableBakersMath | boolean | Compute bakersPercentage relative to the *-marked (or bakersReference) ingredient. |
bakersReference | string | Explicit ingredient id to use as the 100% baker's-percentage base, instead of the * modifier. |
portions | number | Portion count used to compute metrics.nutrition.perPortion. |
lang | string | Optional locale code (e.g. 'en', 'fr') for locale-scoped unit normalization and category sorting. |
validateIngredientDatabase
function validateIngredientDatabase(rawDb: unknown): {
data: Record<string, IngredientData>;
rejected: { key: string; message: string }[];
}Validates entry-by-entry rather than all-or-nothing: one malformed ingredient doesn't prevent every other valid one from loading. Use data to compile/analyze; surface rejected to the user (e.g. gram db validate). See Data Formats for the IngredientData YAML schema.
Mass utilities
function standardizeMass(
amount: number,
unit: string,
database: Record<string, IngredientData>,
ingredientName?: string,
overrides?: Record<string, number>, // slug -> density (g/mL) or unit weight (g), from frontmatter `densities:`
lang?: string,
): { mass: number; method: "physical" | "density" | "unit_weight" | "default" | "explicit"; isEstimate: boolean } | null
function convertUnit(value: number, fromUnit: string, toUnit: string, density?: number, lang?: string): number | null
function applyYield(
mass: number,
method: "physical" | "density" | "unit_weight" | "default" | "explicit",
yieldFactor?: number,
): { normalizedMass: number; purchasingMass?: number }standardizeMass resolves a quantity to grams via, in order: (1) a direct mass unit (g, kg, oz...), (2) a volume unit converted through the ingredient's resolved density, (3) an unrecognized unit (clove, slice...) treated as a count via the ingredient's unit_weight. Returns null — never guesses — when a volume unit has no resolvable density.
convertUnit converts between two arbitrary unit strings, bridging mass↔volume families via density (g/mL) when given. Returns null for an unresolvable cross-family conversion without one.
Mass & volume conversion table
The base conversion factors standardizeMass/convertUnit use for same-family (mass↔mass, volume↔volume) conversions, before any density is involved:
| Family | Unit | Factor (relative to base) |
|---|---|---|
mass (base: g) | mg | 0.001 |
mass (base: g) | g | 1 |
mass (base: g) | kg | 1000 |
mass (base: g) | oz | 28.3495 |
mass (base: g) | lb | 453.592 |
mass (base: g) | livre | 500 |
volume (base: ml) | ml | 1 |
volume (base: ml) | cl | 10 |
volume (base: ml) | dl | 100 |
volume (base: ml) | l | 1000 |
volume (base: ml) | drop | 0.078 |
volume (base: ml) | smidgen | 0.156 |
volume (base: ml) | pinch | 0.3125 |
volume (base: ml) | dash | 0.625 |
volume (base: ml) | tad | 1.25 |
volume (base: ml) | tsp | 4.9289 |
volume (base: ml) | tbsp | 14.7868 |
volume (base: ml) | cup | 236.588 |
volume (base: ml) | tasse | 250 |
volume (base: ml) | pt | 473.176 |
volume (base: ml) | qt | 946.353 |
volume (base: ml) | gal | 3785.41 |
volume (base: ml) | fl oz | 29.5735 |
calculateNutrition
function calculateNutrition(
ingredients: AnalyzedUsage[],
database: Record<string, IngredientData>,
portions?: number,
): NutritionMetricsFlattens composites and alternatives (taking the first option), skips optional-modifier and zero-quantity ingredients, and sums nutrition fields (declared per 100g in the database — see Data Formats) scaled by each ingredient's standardized mass. Returns isEstimate: true if any contributing mass was itself an estimate, and coverage (0–1: fraction of ingredients with known nutrition data).
diffRecipes
function diffRecipes(a: CompilationResult, b: CompilationResult): DiffResultStructural diff between two compiled recipes (e.g. two versions of the same file, or a scaled vs. unscaled result) — ingredient quantity/unit changes (with percentChange when comparable), preparation changes, timing deltas, added/removed/changed sections, temperature and timer changes, and frontmatter (meta) changes. hasChanges is true if any category is non-empty.