@higher.archi/boe 1.0.30 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/dist/core/evaluation/decay.d.ts +13 -2
  2. package/dist/core/evaluation/decay.d.ts.map +1 -1
  3. package/dist/core/evaluation/decay.js +24 -0
  4. package/dist/core/evaluation/decay.js.map +1 -1
  5. package/dist/core/types/rule.d.ts +25 -1
  6. package/dist/core/types/rule.d.ts.map +1 -1
  7. package/dist/core/types/rule.js +28 -0
  8. package/dist/core/types/rule.js.map +1 -1
  9. package/dist/engines/bayesian/index.d.ts +1 -1
  10. package/dist/engines/bayesian/index.d.ts.map +1 -1
  11. package/dist/engines/bayesian/index.js +2 -1
  12. package/dist/engines/bayesian/index.js.map +1 -1
  13. package/dist/engines/bayesian/types.d.ts +10 -1
  14. package/dist/engines/bayesian/types.d.ts.map +1 -1
  15. package/dist/engines/bayesian/types.js +16 -1
  16. package/dist/engines/bayesian/types.js.map +1 -1
  17. package/dist/engines/constraint/types.d.ts +36 -5
  18. package/dist/engines/constraint/types.d.ts.map +1 -1
  19. package/dist/engines/constraint/types.js +44 -1
  20. package/dist/engines/constraint/types.js.map +1 -1
  21. package/dist/engines/decay/index.d.ts +1 -1
  22. package/dist/engines/decay/index.d.ts.map +1 -1
  23. package/dist/engines/decay/index.js +5 -1
  24. package/dist/engines/decay/index.js.map +1 -1
  25. package/dist/engines/decay/types.d.ts +26 -4
  26. package/dist/engines/decay/types.d.ts.map +1 -1
  27. package/dist/engines/decay/types.js +30 -1
  28. package/dist/engines/decay/types.js.map +1 -1
  29. package/dist/engines/defeasible/index.d.ts +1 -1
  30. package/dist/engines/defeasible/index.d.ts.map +1 -1
  31. package/dist/engines/defeasible/index.js +8 -1
  32. package/dist/engines/defeasible/index.js.map +1 -1
  33. package/dist/engines/defeasible/types.d.ts +40 -5
  34. package/dist/engines/defeasible/types.d.ts.map +1 -1
  35. package/dist/engines/defeasible/types.js +56 -1
  36. package/dist/engines/defeasible/types.js.map +1 -1
  37. package/dist/engines/ensemble/index.d.ts +1 -0
  38. package/dist/engines/ensemble/index.d.ts.map +1 -1
  39. package/dist/engines/ensemble/index.js +5 -1
  40. package/dist/engines/ensemble/index.js.map +1 -1
  41. package/dist/engines/ensemble/types.d.ts +17 -2
  42. package/dist/engines/ensemble/types.d.ts.map +1 -1
  43. package/dist/engines/ensemble/types.js +23 -0
  44. package/dist/engines/ensemble/types.js.map +1 -1
  45. package/dist/engines/expert/index.d.ts +1 -1
  46. package/dist/engines/expert/index.d.ts.map +1 -1
  47. package/dist/engines/expert/index.js +3 -1
  48. package/dist/engines/expert/index.js.map +1 -1
  49. package/dist/engines/expert/types.d.ts +11 -1
  50. package/dist/engines/expert/types.d.ts.map +1 -1
  51. package/dist/engines/expert/types.js +18 -1
  52. package/dist/engines/expert/types.js.map +1 -1
  53. package/dist/engines/fuzzy/fuzzy.types.d.ts +65 -8
  54. package/dist/engines/fuzzy/fuzzy.types.d.ts.map +1 -1
  55. package/dist/engines/fuzzy/fuzzy.types.js +89 -1
  56. package/dist/engines/fuzzy/fuzzy.types.js.map +1 -1
  57. package/dist/engines/loyalty/index.d.ts +1 -0
  58. package/dist/engines/loyalty/index.d.ts.map +1 -1
  59. package/dist/engines/loyalty/index.js +8 -1
  60. package/dist/engines/loyalty/index.js.map +1 -1
  61. package/dist/engines/loyalty/types.d.ts +36 -5
  62. package/dist/engines/loyalty/types.d.ts.map +1 -1
  63. package/dist/engines/loyalty/types.js +40 -0
  64. package/dist/engines/loyalty/types.js.map +1 -1
  65. package/dist/engines/menu-engineering/compiler.d.ts +11 -0
  66. package/dist/engines/menu-engineering/compiler.d.ts.map +1 -0
  67. package/dist/engines/menu-engineering/compiler.js +119 -0
  68. package/dist/engines/menu-engineering/compiler.js.map +1 -0
  69. package/dist/engines/menu-engineering/engine.d.ts +32 -0
  70. package/dist/engines/menu-engineering/engine.d.ts.map +1 -0
  71. package/dist/engines/menu-engineering/engine.js +40 -0
  72. package/dist/engines/menu-engineering/engine.js.map +1 -0
  73. package/dist/engines/menu-engineering/index.d.ts +9 -0
  74. package/dist/engines/menu-engineering/index.d.ts.map +1 -0
  75. package/dist/engines/menu-engineering/index.js +21 -0
  76. package/dist/engines/menu-engineering/index.js.map +1 -0
  77. package/dist/engines/menu-engineering/strategy.d.ts +18 -0
  78. package/dist/engines/menu-engineering/strategy.d.ts.map +1 -0
  79. package/dist/engines/menu-engineering/strategy.js +318 -0
  80. package/dist/engines/menu-engineering/strategy.js.map +1 -0
  81. package/dist/engines/menu-engineering/types.d.ts +187 -0
  82. package/dist/engines/menu-engineering/types.d.ts.map +1 -0
  83. package/dist/engines/menu-engineering/types.js +27 -0
  84. package/dist/engines/menu-engineering/types.js.map +1 -0
  85. package/dist/engines/monte-carlo/index.d.ts +1 -1
  86. package/dist/engines/monte-carlo/index.d.ts.map +1 -1
  87. package/dist/engines/monte-carlo/index.js +5 -1
  88. package/dist/engines/monte-carlo/index.js.map +1 -1
  89. package/dist/engines/monte-carlo/types.d.ts +16 -1
  90. package/dist/engines/monte-carlo/types.d.ts.map +1 -1
  91. package/dist/engines/monte-carlo/types.js +23 -1
  92. package/dist/engines/monte-carlo/types.js.map +1 -1
  93. package/dist/engines/negotiation/index.d.ts +1 -0
  94. package/dist/engines/negotiation/index.d.ts.map +1 -1
  95. package/dist/engines/negotiation/index.js +7 -1
  96. package/dist/engines/negotiation/index.js.map +1 -1
  97. package/dist/engines/negotiation/types.d.ts +23 -4
  98. package/dist/engines/negotiation/types.d.ts.map +1 -1
  99. package/dist/engines/negotiation/types.js +27 -0
  100. package/dist/engines/negotiation/types.js.map +1 -1
  101. package/dist/engines/prediction/index.d.ts +1 -1
  102. package/dist/engines/prediction/index.d.ts.map +1 -1
  103. package/dist/engines/prediction/index.js +6 -1
  104. package/dist/engines/prediction/index.js.map +1 -1
  105. package/dist/engines/prediction/types.d.ts +35 -5
  106. package/dist/engines/prediction/types.d.ts.map +1 -1
  107. package/dist/engines/prediction/types.js +39 -1
  108. package/dist/engines/prediction/types.js.map +1 -1
  109. package/dist/engines/pricing/index.d.ts +2 -2
  110. package/dist/engines/pricing/index.d.ts.map +1 -1
  111. package/dist/engines/pricing/index.js +3 -1
  112. package/dist/engines/pricing/index.js.map +1 -1
  113. package/dist/engines/pricing/types.d.ts +15 -1
  114. package/dist/engines/pricing/types.d.ts.map +1 -1
  115. package/dist/engines/pricing/types.js +16 -1
  116. package/dist/engines/pricing/types.js.map +1 -1
  117. package/dist/engines/ranking/index.d.ts +1 -1
  118. package/dist/engines/ranking/index.d.ts.map +1 -1
  119. package/dist/engines/ranking/index.js +6 -1
  120. package/dist/engines/ranking/index.js.map +1 -1
  121. package/dist/engines/ranking/types.d.ts +32 -5
  122. package/dist/engines/ranking/types.d.ts.map +1 -1
  123. package/dist/engines/ranking/types.js +36 -1
  124. package/dist/engines/ranking/types.js.map +1 -1
  125. package/dist/engines/recipe-costing/compiler.d.ts +11 -0
  126. package/dist/engines/recipe-costing/compiler.d.ts.map +1 -0
  127. package/dist/engines/recipe-costing/compiler.js +177 -0
  128. package/dist/engines/recipe-costing/compiler.js.map +1 -0
  129. package/dist/engines/recipe-costing/engine.d.ts +32 -0
  130. package/dist/engines/recipe-costing/engine.d.ts.map +1 -0
  131. package/dist/engines/recipe-costing/engine.js +40 -0
  132. package/dist/engines/recipe-costing/engine.js.map +1 -0
  133. package/dist/engines/recipe-costing/index.d.ts +9 -0
  134. package/dist/engines/recipe-costing/index.d.ts.map +1 -0
  135. package/dist/engines/recipe-costing/index.js +21 -0
  136. package/dist/engines/recipe-costing/index.js.map +1 -0
  137. package/dist/engines/recipe-costing/strategy.d.ts +20 -0
  138. package/dist/engines/recipe-costing/strategy.d.ts.map +1 -0
  139. package/dist/engines/recipe-costing/strategy.js +265 -0
  140. package/dist/engines/recipe-costing/strategy.js.map +1 -0
  141. package/dist/engines/recipe-costing/types.d.ts +213 -0
  142. package/dist/engines/recipe-costing/types.d.ts.map +1 -0
  143. package/dist/engines/recipe-costing/types.js +36 -0
  144. package/dist/engines/recipe-costing/types.js.map +1 -0
  145. package/dist/engines/scoring/index.d.ts +1 -1
  146. package/dist/engines/scoring/index.d.ts.map +1 -1
  147. package/dist/engines/scoring/index.js +3 -1
  148. package/dist/engines/scoring/index.js.map +1 -1
  149. package/dist/engines/scoring/types.d.ts +8 -1
  150. package/dist/engines/scoring/types.d.ts.map +1 -1
  151. package/dist/engines/scoring/types.js +18 -1
  152. package/dist/engines/scoring/types.js.map +1 -1
  153. package/dist/engines/sentiment/index.d.ts +1 -1
  154. package/dist/engines/sentiment/index.d.ts.map +1 -1
  155. package/dist/engines/sentiment/index.js +3 -1
  156. package/dist/engines/sentiment/index.js.map +1 -1
  157. package/dist/engines/sentiment/types.d.ts +13 -2
  158. package/dist/engines/sentiment/types.d.ts.map +1 -1
  159. package/dist/engines/sentiment/types.js +17 -1
  160. package/dist/engines/sentiment/types.js.map +1 -1
  161. package/dist/engines/state-machine/index.d.ts +1 -1
  162. package/dist/engines/state-machine/index.d.ts.map +1 -1
  163. package/dist/engines/state-machine/index.js +5 -1
  164. package/dist/engines/state-machine/index.js.map +1 -1
  165. package/dist/engines/state-machine/types.d.ts +7 -0
  166. package/dist/engines/state-machine/types.d.ts.map +1 -1
  167. package/dist/engines/state-machine/types.js +14 -0
  168. package/dist/engines/state-machine/types.js.map +1 -1
  169. package/dist/engines/utility/index.d.ts +2 -2
  170. package/dist/engines/utility/index.d.ts.map +1 -1
  171. package/dist/engines/utility/index.js +4 -1
  172. package/dist/engines/utility/index.js.map +1 -1
  173. package/dist/engines/utility/types.d.ts +21 -3
  174. package/dist/engines/utility/types.d.ts.map +1 -1
  175. package/dist/engines/utility/types.js +37 -1
  176. package/dist/engines/utility/types.js.map +1 -1
  177. package/dist/index.d.ts +28 -22
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +73 -3
  180. package/dist/index.js.map +1 -1
  181. package/package.json +1 -1
  182. package/src/core/evaluation/decay.ts +13 -2
  183. package/src/core/types/rule.ts +25 -1
  184. package/src/engines/bayesian/index.ts +1 -0
  185. package/src/engines/bayesian/types.ts +10 -8
  186. package/src/engines/constraint/types.ts +40 -11
  187. package/src/engines/decay/index.ts +4 -0
  188. package/src/engines/decay/types.ts +26 -4
  189. package/src/engines/defeasible/index.ts +7 -0
  190. package/src/engines/defeasible/types.ts +42 -18
  191. package/src/engines/ensemble/index.ts +6 -0
  192. package/src/engines/ensemble/types.ts +17 -13
  193. package/src/engines/expert/index.ts +1 -0
  194. package/src/engines/expert/types.ts +11 -9
  195. package/src/engines/fuzzy/fuzzy.types.ts +65 -31
  196. package/src/engines/loyalty/index.ts +9 -0
  197. package/src/engines/loyalty/types.ts +36 -5
  198. package/src/engines/menu-engineering/compiler.ts +145 -0
  199. package/src/engines/menu-engineering/engine.ts +48 -0
  200. package/src/engines/menu-engineering/index.ts +47 -0
  201. package/src/engines/menu-engineering/strategy.ts +414 -0
  202. package/src/engines/menu-engineering/types.ts +242 -0
  203. package/src/engines/monte-carlo/index.ts +1 -0
  204. package/src/engines/monte-carlo/types.ts +16 -21
  205. package/src/engines/negotiation/index.ts +8 -0
  206. package/src/engines/negotiation/types.ts +23 -4
  207. package/src/engines/prediction/index.ts +5 -0
  208. package/src/engines/prediction/types.ts +35 -5
  209. package/src/engines/pricing/index.ts +3 -1
  210. package/src/engines/pricing/types.ts +17 -1
  211. package/src/engines/ranking/index.ts +5 -0
  212. package/src/engines/ranking/types.ts +32 -11
  213. package/src/engines/recipe-costing/compiler.ts +219 -0
  214. package/src/engines/recipe-costing/engine.ts +48 -0
  215. package/src/engines/recipe-costing/index.ts +48 -0
  216. package/src/engines/recipe-costing/strategy.ts +357 -0
  217. package/src/engines/recipe-costing/types.ts +269 -0
  218. package/src/engines/scoring/index.ts +2 -0
  219. package/src/engines/scoring/types.ts +8 -6
  220. package/src/engines/sentiment/index.ts +2 -0
  221. package/src/engines/sentiment/types.ts +13 -2
  222. package/src/engines/state-machine/index.ts +3 -0
  223. package/src/engines/state-machine/types.ts +8 -0
  224. package/src/engines/utility/index.ts +5 -0
  225. package/src/engines/utility/types.ts +23 -3
  226. package/src/index.ts +146 -8
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Recipe Costing Engine Strategy
3
+ *
4
+ * Core execution logic for all recipe costing strategies:
5
+ * - plate-cost: compute plate cost by rolling up ingredient BOM with yield/waste
6
+ * - margin-analysis: analyze margins and flag high food-cost items
7
+ * - cost-impact: model ingredient cost changes and their impact on recipes
8
+ */
9
+
10
+ import type {
11
+ CompiledRecipeCostingRuleSet,
12
+ CompiledPlateCostRuleSet,
13
+ CompiledMarginAnalysisRuleSet,
14
+ CompiledCostImpactRuleSet,
15
+ Ingredient,
16
+ Recipe,
17
+ RecipeCostResult,
18
+ IngredientCostBreakdown,
19
+ SubRecipeCostBreakdown,
20
+ CostImpactItem,
21
+ PlateCostResult,
22
+ MarginAnalysisResult,
23
+ CostImpactResult,
24
+ RecipeCostingResult,
25
+ RecipeCostingOptions
26
+ } from './types';
27
+
28
+ // ========================================
29
+ // Helpers
30
+ // ========================================
31
+
32
+ function round(value: number, decimals: number): number {
33
+ const factor = Math.pow(10, decimals);
34
+ return Math.round(value * factor) / factor;
35
+ }
36
+
37
+ function computeIngredientCostPerUnit(ing: Ingredient): number {
38
+ const baseCostPerUnit = ing.purchaseCost / ing.purchaseQuantity;
39
+ const yieldFactor = ing.yieldFactor ?? 1;
40
+ const wastePct = ing.wastePct ?? 0;
41
+ return baseCostPerUnit / yieldFactor / (1 - wastePct / 100);
42
+ }
43
+
44
+ // ========================================
45
+ // Strategy Executor
46
+ // ========================================
47
+
48
+ export class RecipeCostingExecutor {
49
+ run(
50
+ ruleSet: CompiledRecipeCostingRuleSet,
51
+ _options: RecipeCostingOptions = {}
52
+ ): RecipeCostingResult {
53
+ switch (ruleSet.strategy) {
54
+ case 'plate-cost':
55
+ return this.runPlateCost(ruleSet);
56
+ case 'margin-analysis':
57
+ return this.runMarginAnalysis(ruleSet);
58
+ case 'cost-impact':
59
+ return this.runCostImpact(ruleSet);
60
+ default:
61
+ throw new Error(`Unknown recipe costing strategy: '${(ruleSet as any).strategy}'`);
62
+ }
63
+ }
64
+
65
+ // ========================================
66
+ // Plate Cost Strategy
67
+ // ========================================
68
+
69
+ private runPlateCost(ruleSet: CompiledPlateCostRuleSet): PlateCostResult {
70
+ const startTime = performance.now();
71
+
72
+ const ingredientMap = new Map(ruleSet.ingredients.map(i => [i.id, i]));
73
+ const recipeMap = new Map(ruleSet.recipes.map(r => [r.id, r]));
74
+ const targetFoodCostPct = ruleSet.config.targetFoodCostPct;
75
+
76
+ const recipes = ruleSet.recipes.map(recipe =>
77
+ this.computeRecipeCost(recipe, ingredientMap, recipeMap, targetFoodCostPct, new Set())
78
+ );
79
+
80
+ const executionTimeMs = round((performance.now() - startTime) * 100, 0) / 100;
81
+
82
+ return { recipes, executionTimeMs };
83
+ }
84
+
85
+ // ========================================
86
+ // Margin Analysis Strategy
87
+ // ========================================
88
+
89
+ private runMarginAnalysis(ruleSet: CompiledMarginAnalysisRuleSet): MarginAnalysisResult {
90
+ const startTime = performance.now();
91
+
92
+ const ingredientMap = new Map(ruleSet.ingredients.map(i => [i.id, i]));
93
+ const recipeMap = new Map(ruleSet.recipes.map(r => [r.id, r]));
94
+ const { targetFoodCostPct, flagThreshold } = ruleSet.config;
95
+
96
+ const allRecipes = ruleSet.recipes.map(recipe =>
97
+ this.computeRecipeCost(recipe, ingredientMap, recipeMap, targetFoodCostPct, new Set())
98
+ );
99
+
100
+ // Filter to recipes with selling price and sort by contribution margin descending
101
+ const recipesWithPrice = allRecipes
102
+ .filter(r => r.sellingPrice !== undefined)
103
+ .sort((a, b) => (b.contributionMargin ?? 0) - (a.contributionMargin ?? 0));
104
+
105
+ // Flag items above threshold
106
+ const flaggedItems = recipesWithPrice.filter(
107
+ r => r.foodCostPct !== undefined && r.foodCostPct > flagThreshold
108
+ );
109
+
110
+ // Compute summary stats
111
+ const validRecipes = recipesWithPrice.filter(r => r.foodCostPct !== undefined);
112
+ const avgFoodCostPct = validRecipes.length > 0
113
+ ? round(validRecipes.reduce((sum, r) => sum + r.foodCostPct!, 0) / validRecipes.length, 2)
114
+ : 0;
115
+ const avgContributionMargin = validRecipes.length > 0
116
+ ? round(validRecipes.reduce((sum, r) => sum + r.contributionMargin!, 0) / validRecipes.length, 2)
117
+ : 0;
118
+
119
+ const executionTimeMs = round((performance.now() - startTime) * 100, 0) / 100;
120
+
121
+ return {
122
+ recipes: allRecipes,
123
+ flaggedItems,
124
+ summary: {
125
+ totalRecipes: allRecipes.length,
126
+ recipesWithPrice: recipesWithPrice.length,
127
+ avgFoodCostPct,
128
+ avgContributionMargin,
129
+ flaggedCount: flaggedItems.length
130
+ },
131
+ executionTimeMs
132
+ };
133
+ }
134
+
135
+ // ========================================
136
+ // Cost Impact Strategy
137
+ // ========================================
138
+
139
+ private runCostImpact(ruleSet: CompiledCostImpactRuleSet): CostImpactResult {
140
+ const startTime = performance.now();
141
+
142
+ const ingredientMap = new Map(ruleSet.ingredients.map(i => [i.id, i]));
143
+ const recipeMap = new Map(ruleSet.recipes.map(r => [r.id, r]));
144
+ const { changes, targetFoodCostPct } = ruleSet.config;
145
+
146
+ // Compute baseline costs
147
+ const baselineCosts = new Map<string, RecipeCostResult>();
148
+ for (const recipe of ruleSet.recipes) {
149
+ baselineCosts.set(
150
+ recipe.id,
151
+ this.computeRecipeCost(recipe, ingredientMap, recipeMap, targetFoodCostPct, new Set())
152
+ );
153
+ }
154
+
155
+ // Clone and modify ingredient map
156
+ const modifiedIngredientMap = new Map(
157
+ ruleSet.ingredients.map(i => [i.id, { ...i }])
158
+ );
159
+ const changedIngredientIds = new Set<string>();
160
+ for (const change of changes) {
161
+ const ing = modifiedIngredientMap.get(change.ingredientId)!;
162
+ if (change.newCost !== undefined) {
163
+ ing.purchaseCost = change.newCost;
164
+ } else if (change.percentChange !== undefined) {
165
+ ing.purchaseCost = ing.purchaseCost * (1 + change.percentChange / 100);
166
+ }
167
+ changedIngredientIds.add(change.ingredientId);
168
+ }
169
+
170
+ // Recompute costs with modified ingredients
171
+ const impactItems: CostImpactItem[] = [];
172
+ for (const recipe of ruleSet.recipes) {
173
+ const newResult = this.computeRecipeCost(
174
+ recipe, modifiedIngredientMap, recipeMap, targetFoodCostPct, new Set()
175
+ );
176
+ const baseline = baselineCosts.get(recipe.id)!;
177
+
178
+ // Check if this recipe is affected
179
+ const affectedIngredients = this.findAffectedIngredients(
180
+ recipe, recipeMap, changedIngredientIds
181
+ );
182
+
183
+ if (affectedIngredients.length === 0) continue;
184
+
185
+ const plateCostChange = round(newResult.plateCost - baseline.plateCost, 4);
186
+ const plateCostChangePct = baseline.plateCost > 0
187
+ ? round((plateCostChange / baseline.plateCost) * 100, 2)
188
+ : 0;
189
+
190
+ impactItems.push({
191
+ recipeId: recipe.id,
192
+ recipeName: recipe.name,
193
+ oldPlateCost: baseline.plateCost,
194
+ newPlateCost: newResult.plateCost,
195
+ plateCostChange,
196
+ plateCostChangePct,
197
+ oldFoodCostPct: baseline.foodCostPct,
198
+ newFoodCostPct: newResult.foodCostPct,
199
+ oldMargin: baseline.contributionMargin,
200
+ newMargin: newResult.contributionMargin,
201
+ affectedIngredients
202
+ });
203
+ }
204
+
205
+ // Sort by impact severity (largest absolute change first)
206
+ impactItems.sort((a, b) => Math.abs(b.plateCostChange) - Math.abs(a.plateCostChange));
207
+
208
+ // Compute summary
209
+ const avgPlateCostChange = impactItems.length > 0
210
+ ? round(impactItems.reduce((sum, i) => sum + i.plateCostChange, 0) / impactItems.length, 4)
211
+ : 0;
212
+ const avgPlateCostChangePct = impactItems.length > 0
213
+ ? round(impactItems.reduce((sum, i) => sum + i.plateCostChangePct, 0) / impactItems.length, 2)
214
+ : 0;
215
+ const itemsCrossingThreshold = impactItems.filter(i =>
216
+ i.oldFoodCostPct !== undefined &&
217
+ i.newFoodCostPct !== undefined &&
218
+ i.oldFoodCostPct <= targetFoodCostPct &&
219
+ i.newFoodCostPct > targetFoodCostPct
220
+ ).length;
221
+
222
+ const executionTimeMs = round((performance.now() - startTime) * 100, 0) / 100;
223
+
224
+ return {
225
+ items: impactItems,
226
+ summary: {
227
+ totalRecipesAffected: impactItems.length,
228
+ avgPlateCostChange,
229
+ avgPlateCostChangePct,
230
+ itemsCrossingThreshold
231
+ },
232
+ executionTimeMs
233
+ };
234
+ }
235
+
236
+ // ========================================
237
+ // Shared Helpers
238
+ // ========================================
239
+
240
+ private computeRecipeCost(
241
+ recipe: Recipe,
242
+ ingredientMap: Map<string, Ingredient>,
243
+ recipeMap: Map<string, Recipe>,
244
+ targetFoodCostPct: number,
245
+ visited: Set<string>
246
+ ): RecipeCostResult {
247
+ if (visited.has(recipe.id)) {
248
+ throw new Error(`Circular sub-recipe dependency detected at '${recipe.id}'`);
249
+ }
250
+ visited.add(recipe.id);
251
+
252
+ const batchSize = recipe.batchSize ?? 1;
253
+ const ingredients: IngredientCostBreakdown[] = [];
254
+ const subRecipes: SubRecipeCostBreakdown[] = [];
255
+ let totalBatchCost = 0;
256
+
257
+ for (const comp of recipe.components) {
258
+ if (comp.ingredientId) {
259
+ const ing = ingredientMap.get(comp.ingredientId)!;
260
+ const costPerUnit = computeIngredientCostPerUnit(ing);
261
+ const lineCost = round(costPerUnit * comp.quantity, 4);
262
+ totalBatchCost += lineCost;
263
+
264
+ ingredients.push({
265
+ ingredientId: ing.id,
266
+ ingredientName: ing.name,
267
+ quantity: comp.quantity,
268
+ unit: comp.unit,
269
+ costPerUnit: round(costPerUnit, 4),
270
+ lineCost,
271
+ pctOfPlateCost: 0 // filled in below
272
+ });
273
+ } else if (comp.subRecipeId) {
274
+ const subRecipe = recipeMap.get(comp.subRecipeId)!;
275
+ const subResult = this.computeRecipeCost(
276
+ subRecipe, ingredientMap, recipeMap, targetFoodCostPct, new Set(visited)
277
+ );
278
+ const costPerPortion = subResult.plateCost;
279
+ const lineCost = round(costPerPortion * comp.quantity, 4);
280
+ totalBatchCost += lineCost;
281
+
282
+ subRecipes.push({
283
+ subRecipeId: subRecipe.id,
284
+ subRecipeName: subRecipe.name,
285
+ quantity: comp.quantity,
286
+ costPerPortion: round(costPerPortion, 4),
287
+ lineCost,
288
+ pctOfPlateCost: 0 // filled in below
289
+ });
290
+ }
291
+ }
292
+
293
+ const plateCost = round(totalBatchCost / batchSize, 4);
294
+
295
+ // Fill in pctOfPlateCost
296
+ if (totalBatchCost > 0) {
297
+ for (const ing of ingredients) {
298
+ ing.pctOfPlateCost = round((ing.lineCost / totalBatchCost) * 100, 2);
299
+ }
300
+ for (const sub of subRecipes) {
301
+ sub.pctOfPlateCost = round((sub.lineCost / totalBatchCost) * 100, 2);
302
+ }
303
+ }
304
+
305
+ const result: RecipeCostResult = {
306
+ recipeId: recipe.id,
307
+ recipeName: recipe.name,
308
+ category: recipe.category,
309
+ batchSize,
310
+ totalBatchCost: round(totalBatchCost, 4),
311
+ plateCost,
312
+ ingredients,
313
+ subRecipes
314
+ };
315
+
316
+ if (recipe.sellingPrice !== undefined) {
317
+ result.sellingPrice = recipe.sellingPrice;
318
+ result.contributionMargin = round(recipe.sellingPrice - plateCost, 4);
319
+ result.foodCostPct = recipe.sellingPrice > 0
320
+ ? round((plateCost / recipe.sellingPrice) * 100, 2)
321
+ : 0;
322
+ result.meetsTarget = result.foodCostPct <= targetFoodCostPct;
323
+ }
324
+
325
+ visited.delete(recipe.id);
326
+ return result;
327
+ }
328
+
329
+ private findAffectedIngredients(
330
+ recipe: Recipe,
331
+ recipeMap: Map<string, Recipe>,
332
+ changedIds: Set<string>,
333
+ visited: Set<string> = new Set()
334
+ ): string[] {
335
+ if (visited.has(recipe.id)) return [];
336
+ visited.add(recipe.id);
337
+
338
+ const affected: string[] = [];
339
+ for (const comp of recipe.components) {
340
+ if (comp.ingredientId && changedIds.has(comp.ingredientId)) {
341
+ affected.push(comp.ingredientId);
342
+ }
343
+ if (comp.subRecipeId) {
344
+ const subRecipe = recipeMap.get(comp.subRecipeId);
345
+ if (subRecipe) {
346
+ affected.push(
347
+ ...this.findAffectedIngredients(subRecipe, recipeMap, changedIds, visited)
348
+ );
349
+ }
350
+ }
351
+ }
352
+ return [...new Set(affected)];
353
+ }
354
+ }
355
+
356
+ /** Singleton instance */
357
+ export const recipeCostingStrategy = new RecipeCostingExecutor();
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Recipe Costing Engine Types
3
+ *
4
+ * Computes plate cost by rolling up ingredient costs through a bill of
5
+ * materials with yield factors and waste percentages. Supports margin
6
+ * analysis and cost-impact modeling when ingredient prices change.
7
+ */
8
+
9
+ // ========================================
10
+ // Semantic Types
11
+ // ========================================
12
+
13
+ /** Recipe costing strategy to use */
14
+ export const RecipeCostingStrategies = {
15
+ plateCost: 'plate-cost',
16
+ marginAnalysis: 'margin-analysis',
17
+ costImpact: 'cost-impact',
18
+ } as const;
19
+ export type RecipeCostingStrategy = typeof RecipeCostingStrategies[keyof typeof RecipeCostingStrategies];
20
+
21
+ /** Measurement unit for ingredients and recipes */
22
+ export const MeasurementUnits = {
23
+ oz: 'oz',
24
+ lb: 'lb',
25
+ g: 'g',
26
+ kg: 'kg',
27
+ flOz: 'fl_oz',
28
+ cup: 'cup',
29
+ qt: 'qt',
30
+ gal: 'gal',
31
+ ml: 'ml',
32
+ l: 'l',
33
+ each: 'each',
34
+ slice: 'slice',
35
+ portion: 'portion',
36
+ } as const;
37
+ export type MeasurementUnit = typeof MeasurementUnits[keyof typeof MeasurementUnits];
38
+
39
+ // ========================================
40
+ // Core Domain Types
41
+ // ========================================
42
+
43
+ /** An ingredient with purchase cost and yield data */
44
+ export type Ingredient = {
45
+ id: string;
46
+ name: string;
47
+ category?: string;
48
+ purchaseUnit: MeasurementUnit;
49
+ purchaseQuantity: number;
50
+ purchaseCost: number;
51
+ yieldFactor?: number;
52
+ wastePct?: number;
53
+ unit: MeasurementUnit;
54
+ };
55
+
56
+ /** A component in a recipe (ingredient or sub-recipe) */
57
+ export type RecipeComponent = {
58
+ ingredientId?: string;
59
+ subRecipeId?: string;
60
+ quantity: number;
61
+ unit: MeasurementUnit;
62
+ };
63
+
64
+ /** A recipe with components and optional selling price */
65
+ export type Recipe = {
66
+ id: string;
67
+ name: string;
68
+ category?: string;
69
+ sellingPrice?: number;
70
+ components: RecipeComponent[];
71
+ batchSize?: number;
72
+ };
73
+
74
+ /** A cost change for an ingredient */
75
+ export type CostChange = {
76
+ ingredientId: string;
77
+ newCost?: number;
78
+ percentChange?: number;
79
+ };
80
+
81
+ // ========================================
82
+ // Config Types
83
+ // ========================================
84
+
85
+ /** Configuration for plate-cost strategy */
86
+ export type PlateCostConfig = {
87
+ targetFoodCostPct?: number;
88
+ };
89
+
90
+ /** Configuration for margin-analysis strategy */
91
+ export type MarginAnalysisConfig = {
92
+ targetFoodCostPct?: number;
93
+ flagThreshold?: number;
94
+ };
95
+
96
+ /** Configuration for cost-impact strategy */
97
+ export type CostImpactConfig = {
98
+ changes: CostChange[];
99
+ targetFoodCostPct?: number;
100
+ };
101
+
102
+ // ========================================
103
+ // Source RuleSet Types (Discriminated Union)
104
+ // ========================================
105
+
106
+ type RecipeCostingRuleSetBase = {
107
+ id: string;
108
+ name?: string;
109
+ mode: 'recipe-costing';
110
+ ingredients: Ingredient[];
111
+ recipes: Recipe[];
112
+ };
113
+
114
+ /** Plate-cost strategy: compute cost per portion for each recipe */
115
+ export type PlateCostRuleSet = RecipeCostingRuleSetBase & {
116
+ strategy: 'plate-cost';
117
+ config?: PlateCostConfig;
118
+ };
119
+
120
+ /** Margin-analysis strategy: analyze margins and flag high food-cost items */
121
+ export type MarginAnalysisRuleSet = RecipeCostingRuleSetBase & {
122
+ strategy: 'margin-analysis';
123
+ config?: MarginAnalysisConfig;
124
+ };
125
+
126
+ /** Cost-impact strategy: model ingredient cost changes */
127
+ export type CostImpactRuleSet = RecipeCostingRuleSetBase & {
128
+ strategy: 'cost-impact';
129
+ config: CostImpactConfig;
130
+ };
131
+
132
+ export type RecipeCostingRuleSet =
133
+ | PlateCostRuleSet
134
+ | MarginAnalysisRuleSet
135
+ | CostImpactRuleSet;
136
+
137
+ // ========================================
138
+ // Compiled RuleSet Types
139
+ // ========================================
140
+
141
+ type CompiledRecipeCostingRuleSetBase = {
142
+ id: string;
143
+ name?: string;
144
+ mode: 'recipe-costing';
145
+ ingredients: Ingredient[];
146
+ recipes: Recipe[];
147
+ };
148
+
149
+ export type CompiledPlateCostRuleSet = CompiledRecipeCostingRuleSetBase & {
150
+ strategy: 'plate-cost';
151
+ config: {
152
+ targetFoodCostPct: number;
153
+ };
154
+ };
155
+
156
+ export type CompiledMarginAnalysisRuleSet = CompiledRecipeCostingRuleSetBase & {
157
+ strategy: 'margin-analysis';
158
+ config: {
159
+ targetFoodCostPct: number;
160
+ flagThreshold: number;
161
+ };
162
+ };
163
+
164
+ export type CompiledCostImpactRuleSet = CompiledRecipeCostingRuleSetBase & {
165
+ strategy: 'cost-impact';
166
+ config: {
167
+ changes: CostChange[];
168
+ targetFoodCostPct: number;
169
+ };
170
+ };
171
+
172
+ export type CompiledRecipeCostingRuleSet =
173
+ | CompiledPlateCostRuleSet
174
+ | CompiledMarginAnalysisRuleSet
175
+ | CompiledCostImpactRuleSet;
176
+
177
+ // ========================================
178
+ // Result Types
179
+ // ========================================
180
+
181
+ /** Cost breakdown for a single ingredient in a recipe */
182
+ export type IngredientCostBreakdown = {
183
+ ingredientId: string;
184
+ ingredientName: string;
185
+ quantity: number;
186
+ unit: MeasurementUnit;
187
+ costPerUnit: number;
188
+ lineCost: number;
189
+ pctOfPlateCost: number;
190
+ };
191
+
192
+ /** Cost breakdown for a sub-recipe in a recipe */
193
+ export type SubRecipeCostBreakdown = {
194
+ subRecipeId: string;
195
+ subRecipeName: string;
196
+ quantity: number;
197
+ costPerPortion: number;
198
+ lineCost: number;
199
+ pctOfPlateCost: number;
200
+ };
201
+
202
+ /** Cost result for a single recipe */
203
+ export type RecipeCostResult = {
204
+ recipeId: string;
205
+ recipeName: string;
206
+ category?: string;
207
+ batchSize: number;
208
+ totalBatchCost: number;
209
+ plateCost: number;
210
+ sellingPrice?: number;
211
+ contributionMargin?: number;
212
+ foodCostPct?: number;
213
+ meetsTarget?: boolean;
214
+ ingredients: IngredientCostBreakdown[];
215
+ subRecipes: SubRecipeCostBreakdown[];
216
+ };
217
+
218
+ /** Cost impact for a single recipe when ingredients change */
219
+ export type CostImpactItem = {
220
+ recipeId: string;
221
+ recipeName: string;
222
+ oldPlateCost: number;
223
+ newPlateCost: number;
224
+ plateCostChange: number;
225
+ plateCostChangePct: number;
226
+ oldFoodCostPct?: number;
227
+ newFoodCostPct?: number;
228
+ oldMargin?: number;
229
+ newMargin?: number;
230
+ affectedIngredients: string[];
231
+ };
232
+
233
+ /** Result from plate-cost strategy */
234
+ export type PlateCostResult = {
235
+ recipes: RecipeCostResult[];
236
+ executionTimeMs: number;
237
+ };
238
+
239
+ /** Result from margin-analysis strategy */
240
+ export type MarginAnalysisResult = {
241
+ recipes: RecipeCostResult[];
242
+ flaggedItems: RecipeCostResult[];
243
+ summary: {
244
+ totalRecipes: number;
245
+ recipesWithPrice: number;
246
+ avgFoodCostPct: number;
247
+ avgContributionMargin: number;
248
+ flaggedCount: number;
249
+ };
250
+ executionTimeMs: number;
251
+ };
252
+
253
+ /** Result from cost-impact strategy */
254
+ export type CostImpactResult = {
255
+ items: CostImpactItem[];
256
+ summary: {
257
+ totalRecipesAffected: number;
258
+ avgPlateCostChange: number;
259
+ avgPlateCostChangePct: number;
260
+ itemsCrossingThreshold: number;
261
+ };
262
+ executionTimeMs: number;
263
+ };
264
+
265
+ /** Union of all recipe costing results */
266
+ export type RecipeCostingResult = PlateCostResult | MarginAnalysisResult | CostImpactResult;
267
+
268
+ /** Runtime options */
269
+ export type RecipeCostingOptions = Record<string, never>;
@@ -7,6 +7,8 @@
7
7
 
8
8
  // Types
9
9
  export {
10
+ // Scoring method const
11
+ ScoringMethods,
10
12
  // Scoring methods
11
13
  ScoringMethod,
12
14
  ScoringCaps,
@@ -26,12 +26,14 @@ import { DecayConfig, DecayInfo } from '../../core/evaluation/decay';
26
26
  * - Per-rule methods (normalized, capped, adaptive): Applied to each rule's score
27
27
  * - Global methods (weighted-percent): Applied across all rules
28
28
  */
29
- export type ScoringMethod =
30
- | 'raw' // No normalization (pass-through)
31
- | 'normalized' // Normalize scores to 0-1 range using min/max bounds (strict - requires normalize field)
32
- | 'adaptive' // Smart normalization: use provided bounds or auto-detect from observed max
33
- | 'weighted-percent' // Auto-normalize weights to sum to 1.0
34
- | 'capped'; // Apply max caps to rule contributions
29
+ export const ScoringMethods = {
30
+ raw: 'raw',
31
+ normalized: 'normalized',
32
+ adaptive: 'adaptive',
33
+ weightedPercent: 'weighted-percent',
34
+ capped: 'capped',
35
+ } as const;
36
+ export type ScoringMethod = typeof ScoringMethods[keyof typeof ScoringMethods];
35
37
 
36
38
  /**
37
39
  * Max contribution caps for each rule
@@ -28,6 +28,8 @@ export type {
28
28
 
29
29
  // Constants & utilities
30
30
  export {
31
+ SentimentStrategies,
32
+ SentimentLabels,
31
33
  SENTIMENT_DEFAULTS,
32
34
  resolveSentimentLabel
33
35
  } from './types';
@@ -11,10 +11,21 @@
11
11
  // ========================================
12
12
 
13
13
  /** Sentiment analysis strategy */
14
- export type SentimentStrategy = 'token-level' | 'document-level' | 'aspect-based';
14
+ export const SentimentStrategies = {
15
+ tokenLevel: 'token-level',
16
+ documentLevel: 'document-level',
17
+ aspectBased: 'aspect-based',
18
+ } as const;
19
+ export type SentimentStrategy = typeof SentimentStrategies[keyof typeof SentimentStrategies];
15
20
 
16
21
  /** Resolved sentiment label */
17
- export type SentimentLabel = 'positive' | 'neutral' | 'negative' | 'mixed';
22
+ export const SentimentLabels = {
23
+ positive: 'positive',
24
+ neutral: 'neutral',
25
+ negative: 'negative',
26
+ mixed: 'mixed',
27
+ } as const;
28
+ export type SentimentLabel = typeof SentimentLabels[keyof typeof SentimentLabels];
18
29
 
19
30
  // ========================================
20
31
  // Lexicon Types
@@ -7,6 +7,9 @@
7
7
 
8
8
  // Types
9
9
  export {
10
+ // Constants
11
+ ActionTypes,
12
+ ActionType,
10
13
  // Source types
11
14
  StateMachineDefinition,
12
15
  StateMachineConfig,