vital_solver library

vital_solver — Pure-Dart nutritional solver for the VITAL FlutterFlow app.

This package contains all the pure-logic of the solver pipeline: quality rules → coordinate-descent optimizer → pre-solve report.

It has zero dependencies on Firebase, FlutterFlow, or any UI framework, which makes it fully testable via dart test and reusable in other contexts (CLI, server, alternate UI, etc.).

Surface API

Phase 2 — types + helpers (active)

Phases 3-5 — pipeline (TODO)

  • applyQualityRules(input) → enrichedRows
  • runOptimizer(enrichedRows, targets, options) → SolverOutput
  • solveSingleMeal(input) → SolverOutput (full pipeline)
  • buildPreSolveReport(input) → PreSolveReport (UI gate)

Classes

EnrichedRow
Row emitted by applyQualityRulesBeforeSolver, consumed by the optimizer.
IngredientInput
Single ingredient as supplied to the solver.
MacroTotals
Macro totals over a solved recipe.
Per100g
Per-100g macro values. All values clamp at 0 (legacy parity).
PreSolveReport
Top-level pre-solve report.
PreSolveReportResult
Result of the pre-solve report. The raw map is the legacy-shape output ready to be returned to the FF UI; the typed fields are convenience accessors.
QualityResult
Result of applying the quality rules pipeline to a set of rows.
RecipeContext
Wrapper around the recipe-context option map.
ReportItem
Single-entry reason / warning / suggestion in the report.
ReportMessage
A single report item (blocking, warning, or suggestion).
SolvePipelineResult
Result of the solve pipeline. Wraps the legacy-shape solver output along with the intermediate quality-rules result so callers can introspect.
SolverErrors
Errors vs targets after the solver finishes.
SolverOutput
Top-level solver output (post-corrected if applicable).
SolverTargets
Macro + fiber targets supplied to the solver.
SupportVegCulinaryDecision
Decision produced by computeSupportVegCulinaryDecision.
SweetBatchV22bDecision
Result of the V22b cascade decision — all the values the orchestrator needs to apply bounds to rows + build diagnostics.
VegetableOnlyBalancedDecision
Decision produced by computeVegetableOnlyBalancedDecision. Carries the activation flag, per-group candidates, target shares, and (if applicable) the escape-overflow allocations.

Functions

applyCarbBounds(List<Map<String, dynamic>> rows, Map<String, dynamic> ctx, {required String selectedCarbAnchorId}) List<Map<String, dynamic>>
Applies per-class caps for carb candidates:
applyCarbMainMinimumWhenLegumePresent(List<Map<String, dynamic>> rows, Map<String, dynamic> recipeContext) List<Map<String, dynamic>>
applyFatCaps(List<Map<String, dynamic>> rows, Map<String, dynamic> ctx) List<Map<String, dynamic>>
Applies absolute max caps to fat rows: oil ≤ 20g, butter ≤ 18g, other fat ≤ 120g. Sets fatArchitectureClass.
applyProteinSupplementBounds(List<Map<String, dynamic>> rows, Map<String, dynamic> ctx) List<Map<String, dynamic>>
Applies the soft-dose bounds for whey-style protein supplements: minG (default 15g), softCenter (default 20g), softMax (default 20g), rescueMax (default 240g, no hard max unless explicitly enabled).
applyQualityRules({required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> ctx}) QualityResult
Applies the quality rules pipeline to a set of rows.
applySupportVegCulinaryBounds({required List<Map<String, dynamic>> rows, required SupportVegCulinaryDecision decision, required Map<String, dynamic> ctx}) List<Map<String, dynamic>>
Apply the decision to the rows in place (returns the same list). Safe to call when decision.active == false (no-op).
applyV18OptionDefaults(Map<String, dynamic> options, Map<String, dynamic> targets) → void
applyV22bMutations({required SweetBatchV22bDecision decision, required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> ctx}) List<Map<String, dynamic>>
Applies the V22b cascade decision to the rows, mutating each one's bounds (minG, maxG, softCenterG, weights, diag fields) in place according to its category.
applyVegetableOnlyBalancedBounds({required List<Map<String, dynamic>> rows, required VegetableOnlyBalancedDecision decision, required Map<String, dynamic> ctx}) List<Map<String, dynamic>>
Apply the bounds policy described by decision to rows. Rows not touched by the mode (proteins, fats, fixed rows, recipe blocks, etc.) are returned unchanged. When decision.active is false, the rows are returned unchanged.
asList(dynamic v) List
Coerces v into List<dynamic>. Decodes JSON strings. Returns an empty list for non-list inputs.
asMap(dynamic v) Map<String, dynamic>
Coerces v into Map<String, dynamic>. Decodes JSON strings, handles non-typed Map (e.g. Map<dynamic, dynamic> coming from JSON.decode), and falls back to .toMap() for FlutterFlow data structs. Returns an empty map if v is null, not a Map, or a Map-like object cannot be extracted.
buildPreSolveReport({required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> options}) PreSolveReportResult
Build a pre-solve report from raw rows, targets, and options.
carbAnchorBoundViolationCheck(List<Map<String, dynamic>> rows, Map<String, double> gramsById, Map<String, dynamic> recipeContext) Map<String, dynamic>
carbAnchorCheck(List<Map<String, dynamic>> rows, Map<String, double> gramsById, Map<String, dynamic> recipeContext) Map<String, dynamic>
cFromG(Map<String, dynamic> r, double g) double
Returns net carbs in grams for g grams of r.
clampD(double v, double lo, double hi) double
Clamps v to the range `lo`..`hi`. Returns lo for non-finite values (legacy behavior: NaN/Infinity are treated as the lower bound).
cleanMapForDiag(Map<String, dynamic> m) Map<String, dynamic>
Cleans a diagnostic map: drops keys whose value is non-finite (the firestoreSafe would return null for them, but we keep explicit nulls because they may carry meaning in diag).
computeDynamicFruitBounds(Map<String, dynamic> fruit, Map<String, dynamic> fruitPolicy, Map<String, dynamic> ctx, bool hasSimpleCarb) Map<String, dynamic>
Computes dynamic min/center/max bounds for a fruit row based on its density and fiber profile.
computeMicrosSnapshot({required List<Map<String, dynamic>> rows, required Map<String, dynamic> gramsById}) Map<String, dynamic>
Aggregates the four micronutrient groups (vitamins, minerals, fiber, lipidProfil) from solved rows weighted by their grams in gramsById.
computeSupportVegCulinaryDecision({required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> ctx}) SupportVegCulinaryDecision
Compute the decision for the current row set.
computeSweetBatchV22bCascade({required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> ctx}) SweetBatchV22bDecision
Computes the V22b cascade decision for a recipe.
computeVegetableOnlyBalancedDecision({required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> ctx}) VegetableOnlyBalancedDecision
Compute the VegetableOnlyBalancedDecision from rows that have already passed through applyCarbBounds. Returns an inactive decision when any activation condition fails.
decodeIfJsonString(dynamic v) → dynamic
If v is a JSON-encoded string, decodes it. Otherwise returns v unchanged. Empty strings are returned as-is. Decoding errors return v unchanged (best-effort).
fatPer100(Map<String, dynamic> r) double
Reads fatPer100g (or legacy alias lipidsPer100g) and clamps at 0.
fiberFromG(Map<String, dynamic> r, double g) double
Returns fiber in grams for g grams of r.
fiberPer100(Map<String, dynamic> r) double
Reads fiber per 100g, checking the long alias chain: fiberPer100gfiber_gmicrosPer100g.fiber.fiber_gmicrosPer100g_fiber_fiber_g. Clamps at 0.
firestoreSafe(dynamic v) → dynamic
Recursively converts v into a Firestore-safe value:
hasAnyTag(Map<String, dynamic> r, List<String> tags) bool
True if any of tags (after norming) appears in tagsOf(r).
isCarbCandidate(Map<String, dynamic> r) bool
True if the row is a candidate for the carb anchor selection — it must be variable, not a recipe block, not a sweet simple carb, have non- negligible net carbs, and have a genericCarbClass in the UI carb set.
isCarbUiClass(String cls) bool
True if cls is one of the carb UI buckets (carb_main, legumineuse, carb_secondary, veg_structural, leafy_finish).
isCondimentRole(String role) bool
True if role (already normalized) is one of the condiment / seasoning roles.
isFatButter(Map<String, dynamic> r) bool
True if the row is butter (beurre / butter).
isFatOil(Map<String, dynamic> r) bool
True if the row is a fat oil (huile / oil).
isFruitRow(Map<String, dynamic> r) bool
True if the row is a fruit (lexical match on name/category OR mealUiRole/smartCarbGroup/tags). Excludes sweet simple carbs to avoid honey/maple syrup being miscategorized.
isPreparedSweetBatchRow(Map<String, dynamic> r) bool
True if the row is a prepared sweet batch (pâte à gauffre, pancake batter, etc.) — a recipe block whose blockType contains "batter" or "pate", or whose branch indicates sweet/batter, or that is explicitly marked fromPreparedBatch.
isProteinSupplement(Map<String, dynamic> r) bool
True if the row is a protein supplement (whey, isolate, plant-based protein powder, etc.).
isRecipeBlockRow(Map<String, dynamic> r) bool
True if the row represents a recipe block (synthetic prepared batch row aggregating multiple base ingredients).
isSweetSimpleCarbDoc(Map<String, dynamic> r) bool
True if the row is a sweet simple carb (sucre/miel/sirop/confiture/ caramel/agave/maple).
kcalPer100(Map<String, dynamic> r) double
Reads kcal per 100g if present, else estimates from macros via Atwater approximation (4 kcal/g protein + 4 kcal/g net-carbs + 9 kcal/g fat).
lFromG(Map<String, dynamic> r, double g) double
Returns fat in grams for g grams of r.
netCarbsPer100(Map<String, dynamic> r) double
Reads net carbs per 100g (= total - fiber when not provided explicitly). Checks carbsForSolverPer100gcarbsPer100g_solvernetCarbsPer100g before computing the difference. Clamps at 0.
norm(dynamic v) String
Normalizes a string for case- and accent-insensitive comparison. French-specific accents are folded to ASCII (legacy behavior).
normalizeMacrosAndRoles(List<Map<String, dynamic>> rows, Map<String, dynamic> ctx) List<Map<String, dynamic>>
Normalizes macros (sets canonical per100g fields), infers culinary role (protein_main / fat / carb_main / etc.), and sets genericCarbClass, section, creationSection, foodRole, anchorRole.
normalizeProteinSupplementBoundsAfterQuality(List<Map<String, dynamic>> rows, Map<String, dynamic> recipeContext) List<Map<String, dynamic>>
parseStringList(dynamic v) List<String>
Parses a tag-list value that may be either a JSON-encoded array, a comma- separated string, or already a List. Each element is normalized via norm; empty entries are dropped.
pFromG(Map<String, dynamic> r, double g) double
Returns protein in grams for g grams of r.
proteinMainCheck(List<Map<String, dynamic>> rows, Map<String, double> gramsById, Map<String, dynamic> recipeContext) Map<String, dynamic>
proteinPer100(Map<String, dynamic> r) double
Reads proteinPer100g (or legacy alias proteinsPer100g) and clamps at 0.
rowIsVariable(Map<String, dynamic> r) bool
True if the row is variable (the solver may adjust its grams). False for explicit fixed rows (role == 'fixed').
runMacroOptimizer(List ingredientsParam, dynamic targetsParam) Map<String, dynamic>
Run the macro optimizer on a list of ingredient row maps with the given targets/options payload. Byte-equivalent to legacy macroOptimizerPQDynamicMaxAction(ingredientsParam, targetsParam).
selectCarbAnchorId(List<Map<String, dynamic>> rows, Map<String, dynamic> targets, Map<String, dynamic> ctx) String
Selects the carb anchor C using priority chain: carb_main → légumineuse → carb_secondary → veg_structural → leafy_finish. Within a class, prefers fruit rows, then highest density. Returns the stableId of the selected anchor, or '' if none.
slug(dynamic v) String
Slugifies via norm then collapses non-alphanumeric runs to a single underscore. Returns 'item' for empty inputs (legacy fallback).
solvePipeline({required List<Map<String, dynamic>> rows, required Map<String, dynamic> targets, required Map<String, dynamic> options}) SolvePipelineResult
Run the full pre-solve → solve pipeline on a list of legacy-shape rows.
stableId(Map<String, dynamic> r) String
Resolves a stable identifier for a row by checking the legacy alias chain: rowId → ingredientId → id → docId → recipeBlockId → name. Returns '' if none of these are set.
sugarsPer100(Map<String, dynamic> r) double
Reads sugars per 100g (similar alias chain to fiber). Clamps at 0.
syncContextFromPreSolveDiag(List<Map<String, dynamic>> rows, Map<String, dynamic> options, Map<String, dynamic> targets) → void
tagsOf(Map<String, dynamic> r) List<String>
Extracts and normalizes the union of qualityTags, tags, and Tags fields on a row. Order is preserved (insertion order via Set/List).
toB(dynamic v, [bool fallback = false]) bool
Coerces v to bool using the legacy semantics:
toD(dynamic v, [double fallback = 0.0]) double
Coerces v to a finite double. Returns fallback for null, non-numeric, non-finite (NaN, Infinity), or unparseable values.
toI(dynamic v, [int fallback = 0]) int
Coerces v to an int. Numeric values are rounded (legacy behavior). Returns fallback for null or unparseable strings.
toS(dynamic v, [String fallback = '']) String
Coerces v to a non-empty String. Returns fallback when the string representation of v is empty or v is null.
totalCarbsPer100(Map<String, dynamic> r) double
Reads total carbs per 100g via alias chain: totalCarbsPer100gcarbsTotalPer100gglucidesTotauxPer100gcarbsPer100gCCa. Clamps at 0.
totalCFromG(Map<String, dynamic> r, double g) double
Returns total carbs in grams for g grams of r (legacy alternate path used in some scoring code).