Analyse Sémantique (@gram/analyzer)
Le paquet @gram/analyzer représente l'étape analytique finale dans le pipeline de compilation. Il prend le résultat logiquement valide CompilationResult produit par @gram/kitchen et effectue l'Enrichissement Physique en croisant la recette avec une base de données externe ingredients.yaml.
C'est ici que le monde physique rencontre le code numérique. L'analyseur calcule cinq ensembles majeurs de fonctionnalités : Standardisation des Masses, Calcul du Rendement, Agrégation de la Liste de Courses, Estimation Nutritionnelle, et Pourcentages du Boulanger — chacune pouvant être activée ou désactivée individuellement via les options.
1. Standardisation des Masses
Gram est conçu pour unifier et normaliser les masses à travers les recettes, calculant la véritable Masse Totale d'un plat même si les ingrédients sont écrits en volumes (tasses) ou en unités (œufs).
L'algorithme standardizeMass suit un ordre de priorité strict :
- Masse Physique : Si l'unité est déjà un poids (
g,kg,oz,mg,lb), elle est simplement convertie en grammes. - Volume → Densité : Si l'unité est un volume connu (ex :
tasse,c.à.s,ml), l'analyseur vérifie d'abord une surcharge explicite — un blocdensitiesdans le frontmatter YAML de la recette — et l'utilise si présent. Sinon, il recherche la densité de l'ingrédient (g/ml) dans la base de donnéesingredients.yaml. - Nombre → Poids Unitaire : Si l'unité n'est ni une masse ni un volume (ex :
@ail{3 gousses}), l'analyseur vérifie là encore s'il y a une surcharge, puis se rabat sur ununit_weight(poids unitaire) spécifique dans la base de données pour estimer la masse.
Pas de devinette silencieuse
Si aucune densité n'est disponible pour une unité de volume (pas de surcharge, pas d'entrée en base de données), l'analyseur ne se rabat pas sur la supposition de la densité de l'eau. Il renvoie délibérément "aucune masse" plutôt que de risquer une fausse agrégation (ex : traiter silencieusement "1 tasse de farine" comme si elle pesait le même poids que de l'eau). L'article est listé via son unité brute, non convertie.
2. Calcul du Rendement (Facteur de Déchet)
De nombreux ingrédients crus ont des déchets naturels comme des pelures, des trognons ou des coquilles. L'Analyseur fait la distinction entre la Masse Nette (ce qui entre dans la recette) et la Masse d'Achat (Masse Brute — ce que vous devez réellement acheter), en utilisant le champ physical.yield de la base de données d'ingrédients.
Par exemple, si un ingrédient a un facteur de rendement de 0.65 (35 % de déchets) :
- Recette :
@banane{100 g}(Ceci fait référence à 100 g de chair comestible). - Calcul de l'Analyseur :
100 g / 0.65 ≈ 154 g. - Sortie de la Liste de Courses : Le système vous indiquera d'acheter ~154 g de l'ingrédient non pelé pour s'assurer que vous avez 100 g de chair utilisable.
La direction de ce calcul dépend de la manière dont la quantité a été écrite. Une masse/volume explicite (comme la banane ci-dessus) est supposée être la masse Nette, avec la masse Brute (achat) dérivée à l'envers (Nette ÷ rendement). Un nombre (ex : @avocat{1}) fonctionne dans l'autre sens : le poids unitaire (unit_weight) de l'ingrédient représente déjà l'unité entière achetée (Brute), donc la masse Nette réellement utilisée par la recette et par les calculs de nutrition est dérivée vers l'avant (Brute × rendement). Voir Standardisation des Masses & Rendement pour le détail complet.
Cet ajustement est calculé par utilisation de l'ingrédient — y compris les mentions en ligne dans le texte de la recette, et pas seulement pour le total agrégé de la liste de courses — bien que la sortie HTML de référence @gram/renderer n'affiche actuellement le chiffre de Masse Brute que dans la liste de courses.
3. Agrégation de la Liste de Courses
Au-delà d'enrichir les masses individuelles des ingrédients, l'Analyseur regroupe également la liste de courses elle-même. @gram/kitchen regroupe purement par l'id brut qu'elle a attribué lors de l'analyse syntaxique, sans connaissance de ingredients.yaml — elle ne peut donc pas savoir que @butter et @beurre sont le même ingrédient, ni fusionner 100 g d'un ingrédient avec 1 tasse de ce même ingrédient en un seul total.
En utilisant la base de données des ingrédients, l'Analyseur :
- Résout chaque ingrédient vers son id canonique (
resolveCanonicalId), fusionnant les entrées aliasées ensemble. - Fusionne les quantités inter-unités en un seul total en grammes chaque fois que toutes les entrées contributives se résolvent en masse (densité/poids unitaire connu).
- Se rabat sur le maintien des entrées séparées — renommées avec l'id canonique et marquées
multiUnit: true— lorsqu'une masse ne peut pas être résolue pour au moins l'une d'entre elles (ex : aucune densité disponible), plutôt que de deviner.
Voir Agrégation de la Liste de Courses pour le détail complet, y compris le comportement de repli.
4. Estimation Nutritionnelle
L'Analyseur peut calculer automatiquement une estimation des Calories et Macronutriments (Protéines, Glucides, Lipides, Sucre, Fibres, Sodium) en se basant sur la liste des ingrédients. Le schéma de la base de données des ingrédients supporte également des champs plus fins (sat_fat, mono_fat, poly_fat, alcohol), mais ceux-ci ne sont pas actuellement agrégés dans les totaux de la recette.
- Premièrement, chaque ingrédient passe par l'algorithme de Standardisation des Masses.
- L'analyseur recherche les données nutritionnelles (pour 100 g) de chaque ingrédient dans la base de données.
- Il agrège la contribution de chaque ingrédient dans une somme totale.
- Si une valeur
portionsest transmise via les options de l'analyseur, il divise les totaux pour fournir des valeurs Par Portion.
Des données partielles transparentes, non cachées
Contrairement à un modèle strict de type tout-ou-rien, l'analyseur renvoie toujours tous les totaux nutritionnels qu'il a pu calculer, accompagnés d'un ratio de coverage (couverture) et d'avertissements pour tout ingrédient pour lequel des données nutritionnelles manquaient ou qui n'a pas pu être standardisé en masse. Les consommateurs de ces données (comme le visualiseur CLI ou la sortie HTML de @gram/renderer) utilisent cela pour afficher un indicateur explicite de "données incomplètes" à côté des totaux partiels, plutôt que de masquer complètement le panneau.
5. Pourcentages du Boulanger
Si la recette (ou l'appelant, via une option bakersReference) désigne un ingrédient de référence — typiquement la farine, marquée avec le modificateur * — l'analyseur calcule la masse de chaque autre ingrédient comme un pourcentage de cette référence, à la fois pour les articles de la liste de courses et les ingrédients en ligne de la recette.
Architecture Ouverte
@gram/analyzer est conçu pour les écosystèmes ouverts. Il ne lit pas les fichiers directement depuis le système de fichiers. À la place, l'application hôte (comme le CLI ou le Playground) charge la base de données et la passe en paramètre à la fonction analyze(compilationResult, database, options?), où options permet à l'appelant d'activer ou de désactiver chaque fonctionnalité et de configurer des choses comme la référence pour les pourcentages du boulanger ou le nombre de portions.
Cela signifie que vous pouvez facilement brancher une base de données personnalisée provenant d'une API REST, d'un fichier JSON local, ou de n'importe quelle autre source, tant qu'elle est conforme au schéma de données de Gram.