Aller au contenu

@gram-lang/analyzer

Enrichit un CompilationResult (issu de @gram-lang/kitchen) avec des propriétés physiques (masse standardisée, rendement d’achat, estimations nutritionnelles et % du boulanger) en les croisant avec une base de données d’ingrédients. C’est la seule étape qui requiert une base de données : le parsing et la compilation fonctionnent sur n’importe quelle recette, sans la moindre donnée externe.

function analyze(
  result: CompilationResult,
  database: Record<string, IngredientData>,
  options?: AnalyzerOptions,
): AnalysisResult

interface AnalysisResult {
  result: AnalyzedCompilationResult;
  missingIngredients: string[]; // ids présents dans la recette mais absents de `database`
}
import { compile } from '@gram-lang/kitchen';
import { analyze } from '@gram-lang/analyzer';

const compiled = compile(ast);
const { result: analyzed, missingIngredients } = analyze(compiled, myIngredientDatabase);

La base de données d’ingrédients est le deuxième argument positionnel, pas un champ de optionsanalyze(compiled, database, options?).

analyze() est une fonction pure et ne mute jamais result. AnalyzedCompilationResult étend CompilationResult (même forme) en y ajoutant les champs normalizedMass/conversionMethod/isEstimate/purchasingMass/bakersPercentage pour chaque ingrédient, ainsi qu’un bloc metrics.nutrition. Voir Formats de données pour un exemple entièrement annoté.

Tous les flags d’enrichissement sont activés par défaut (via des vérifications internes !== false). Il suffit de passer false pour désactiver l’enrichissement correspondant.

OptionTypeDescription
enableMassStandardizationbooleanConvertit les quantités d’ingrédients en grammes standardisés.
enableYieldCalculationbooleanApplique le physical.yield (facteur de perte) de la base de données lors de la standardisation de la masse.
enableNutritionalEstimationbooleanCalcule metrics.nutrition (calories, macros, éventuellement par portion).
enableBakersMathbooleanCalcule bakersPercentage par rapport à l’ingrédient marqué * (ou bakersReference).
bakersReferencestringId d’ingrédient explicite à utiliser comme base 100% du pourcentage boulanger, au lieu du modificateur *.
portionsnumberNombre de portions utilisé pour calculer metrics.nutrition.perPortion.
langstringCode de langue optionnel (ex : 'en', 'fr') pour la normalisation d’unités et le tri des catégories par langue.
function validateIngredientDatabase(rawDb: unknown): {
  data: Record<string, IngredientData>;
  rejected: { key: string; message: string }[];
}

Valide la base entrée par entrée plutôt qu’en bloc : un ingrédient malformé ne fera pas planter le chargement des autres. Utilisez data pour la compilation/analyse, et remontez rejected à l’utilisateur (ex : gram db validate). Voir Formats de données pour le schéma YAML de IngredientData.

function standardizeMass(
  amount: number,
  unit: string,
  database: Record<string, IngredientData>,
  ingredientName?: string,
  overrides?: Record<string, number>, // slug -> densité (g/mL) ou poids unitaire (g), issu du 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 résout une quantité en grammes selon l’ordre suivant : (1) une unité de masse directe (g, kg, oz…), (2) une unité de volume convertie grâce à la densité de l’ingrédient, (3) une unité non reconnue (gousse, tranche…) traitée comme un comptage via le unit_weight de l’ingrédient. La fonction retourne null (elle ne devine jamais) lorsqu’une unité de volume n’a aucune densité résolue.

convertUnit convertit entre deux chaînes d’unités arbitraires, et jette un pont masse↔volume si une density (g/mL) est fournie. Retourne null lorsqu’une conversion inter-famille est impossible faute de densité.

Voici les facteurs de conversion par défaut employés par standardizeMass et convertUnit pour les conversions intra-famille (masse↔masse, volume↔volume), c’est-à-dire avant même de requérir une densité :

FamilleUnitéFacteur (par rapport à la base)
mass (base: g)mg0.001
mass (base: g)g1
mass (base: g)kg1000
mass (base: g)oz28.3495
mass (base: g)lb453.592
mass (base: g)livre500
volume (base: ml)ml1
volume (base: ml)cl10
volume (base: ml)dl100
volume (base: ml)l1000
volume (base: ml)drop0.078
volume (base: ml)smidgen0.156
volume (base: ml)pinch0.3125
volume (base: ml)dash0.625
volume (base: ml)tad1.25
volume (base: ml)tsp4.9289
volume (base: ml)tbsp14.7868
volume (base: ml)cup236.588
volume (base: ml)tasse250
volume (base: ml)pt473.176
volume (base: ml)qt946.353
volume (base: ml)gal3785.41
volume (base: ml)fl oz29.5735
function calculateNutrition(
  ingredients: AnalyzedUsage[],
  database: Record<string, IngredientData>,
  portions?: number,
): NutritionMetrics

Aplatit les composites et les alternatives (en conservant la première option), ignore les ingrédients marqués optional et les quantités nulles, puis fait la somme des champs nutrition (déclarés pour 100g dans la base de données — voir Formats de données) ajustés (scale) via la masse standardisée de chaque ingrédient. Retourne isEstimate: true si au moins une masse contributrice était déjà une estimation, ainsi qu’un taux de coverage de 0 à 1 (fraction d’ingrédients possédant des données nutritionnelles).

function diffRecipes(a: CompilationResult, b: CompilationResult): DiffResult

Calcule le diff structurel entre deux recettes compilées (ex : deux versions d’un même fichier, ou la version de base contre la version scalée). Il gère les changements de quantité/unité des ingrédients (incluant un percentChange pertinent), de préparation, d’écarts de durée, les ajouts/modifications/suppressions de sections, les variations de température et de minuteur, ainsi que les altérations du frontmatter (meta). hasChanges vaut true dès qu’une catégorie n’est pas vide.

Une section importée via @use (ProcessedSection.module renseigné) est exclue des comparaisons de sections/préparations/températures/minuteurs — sans quoi l’ajout d’un simple import décalerait la position de toutes les autres sections et ferait croire que toute la recette a changé. Son propre import est comparé séparément, par (uri, binding, facteur d'échelle), dans modules: ModuleDelta[] — un import re-scalé ou re-lié est signalé comme "changed".