@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.
- package/dist/core/evaluation/decay.d.ts +13 -2
- package/dist/core/evaluation/decay.d.ts.map +1 -1
- package/dist/core/evaluation/decay.js +24 -0
- package/dist/core/evaluation/decay.js.map +1 -1
- package/dist/core/types/rule.d.ts +25 -1
- package/dist/core/types/rule.d.ts.map +1 -1
- package/dist/core/types/rule.js +28 -0
- package/dist/core/types/rule.js.map +1 -1
- package/dist/engines/bayesian/index.d.ts +1 -1
- package/dist/engines/bayesian/index.d.ts.map +1 -1
- package/dist/engines/bayesian/index.js +2 -1
- package/dist/engines/bayesian/index.js.map +1 -1
- package/dist/engines/bayesian/types.d.ts +10 -1
- package/dist/engines/bayesian/types.d.ts.map +1 -1
- package/dist/engines/bayesian/types.js +16 -1
- package/dist/engines/bayesian/types.js.map +1 -1
- package/dist/engines/constraint/types.d.ts +36 -5
- package/dist/engines/constraint/types.d.ts.map +1 -1
- package/dist/engines/constraint/types.js +44 -1
- package/dist/engines/constraint/types.js.map +1 -1
- package/dist/engines/decay/index.d.ts +1 -1
- package/dist/engines/decay/index.d.ts.map +1 -1
- package/dist/engines/decay/index.js +5 -1
- package/dist/engines/decay/index.js.map +1 -1
- package/dist/engines/decay/types.d.ts +26 -4
- package/dist/engines/decay/types.d.ts.map +1 -1
- package/dist/engines/decay/types.js +30 -1
- package/dist/engines/decay/types.js.map +1 -1
- package/dist/engines/defeasible/index.d.ts +1 -1
- package/dist/engines/defeasible/index.d.ts.map +1 -1
- package/dist/engines/defeasible/index.js +8 -1
- package/dist/engines/defeasible/index.js.map +1 -1
- package/dist/engines/defeasible/types.d.ts +40 -5
- package/dist/engines/defeasible/types.d.ts.map +1 -1
- package/dist/engines/defeasible/types.js +56 -1
- package/dist/engines/defeasible/types.js.map +1 -1
- package/dist/engines/ensemble/index.d.ts +1 -0
- package/dist/engines/ensemble/index.d.ts.map +1 -1
- package/dist/engines/ensemble/index.js +5 -1
- package/dist/engines/ensemble/index.js.map +1 -1
- package/dist/engines/ensemble/types.d.ts +17 -2
- package/dist/engines/ensemble/types.d.ts.map +1 -1
- package/dist/engines/ensemble/types.js +23 -0
- package/dist/engines/ensemble/types.js.map +1 -1
- package/dist/engines/expert/index.d.ts +1 -1
- package/dist/engines/expert/index.d.ts.map +1 -1
- package/dist/engines/expert/index.js +3 -1
- package/dist/engines/expert/index.js.map +1 -1
- package/dist/engines/expert/types.d.ts +11 -1
- package/dist/engines/expert/types.d.ts.map +1 -1
- package/dist/engines/expert/types.js +18 -1
- package/dist/engines/expert/types.js.map +1 -1
- package/dist/engines/fuzzy/fuzzy.types.d.ts +65 -8
- package/dist/engines/fuzzy/fuzzy.types.d.ts.map +1 -1
- package/dist/engines/fuzzy/fuzzy.types.js +89 -1
- package/dist/engines/fuzzy/fuzzy.types.js.map +1 -1
- package/dist/engines/loyalty/index.d.ts +1 -0
- package/dist/engines/loyalty/index.d.ts.map +1 -1
- package/dist/engines/loyalty/index.js +8 -1
- package/dist/engines/loyalty/index.js.map +1 -1
- package/dist/engines/loyalty/types.d.ts +36 -5
- package/dist/engines/loyalty/types.d.ts.map +1 -1
- package/dist/engines/loyalty/types.js +40 -0
- package/dist/engines/loyalty/types.js.map +1 -1
- package/dist/engines/menu-engineering/compiler.d.ts +11 -0
- package/dist/engines/menu-engineering/compiler.d.ts.map +1 -0
- package/dist/engines/menu-engineering/compiler.js +119 -0
- package/dist/engines/menu-engineering/compiler.js.map +1 -0
- package/dist/engines/menu-engineering/engine.d.ts +32 -0
- package/dist/engines/menu-engineering/engine.d.ts.map +1 -0
- package/dist/engines/menu-engineering/engine.js +40 -0
- package/dist/engines/menu-engineering/engine.js.map +1 -0
- package/dist/engines/menu-engineering/index.d.ts +9 -0
- package/dist/engines/menu-engineering/index.d.ts.map +1 -0
- package/dist/engines/menu-engineering/index.js +21 -0
- package/dist/engines/menu-engineering/index.js.map +1 -0
- package/dist/engines/menu-engineering/strategy.d.ts +18 -0
- package/dist/engines/menu-engineering/strategy.d.ts.map +1 -0
- package/dist/engines/menu-engineering/strategy.js +318 -0
- package/dist/engines/menu-engineering/strategy.js.map +1 -0
- package/dist/engines/menu-engineering/types.d.ts +187 -0
- package/dist/engines/menu-engineering/types.d.ts.map +1 -0
- package/dist/engines/menu-engineering/types.js +27 -0
- package/dist/engines/menu-engineering/types.js.map +1 -0
- package/dist/engines/monte-carlo/index.d.ts +1 -1
- package/dist/engines/monte-carlo/index.d.ts.map +1 -1
- package/dist/engines/monte-carlo/index.js +5 -1
- package/dist/engines/monte-carlo/index.js.map +1 -1
- package/dist/engines/monte-carlo/types.d.ts +16 -1
- package/dist/engines/monte-carlo/types.d.ts.map +1 -1
- package/dist/engines/monte-carlo/types.js +23 -1
- package/dist/engines/monte-carlo/types.js.map +1 -1
- package/dist/engines/negotiation/index.d.ts +1 -0
- package/dist/engines/negotiation/index.d.ts.map +1 -1
- package/dist/engines/negotiation/index.js +7 -1
- package/dist/engines/negotiation/index.js.map +1 -1
- package/dist/engines/negotiation/types.d.ts +23 -4
- package/dist/engines/negotiation/types.d.ts.map +1 -1
- package/dist/engines/negotiation/types.js +27 -0
- package/dist/engines/negotiation/types.js.map +1 -1
- package/dist/engines/prediction/index.d.ts +1 -1
- package/dist/engines/prediction/index.d.ts.map +1 -1
- package/dist/engines/prediction/index.js +6 -1
- package/dist/engines/prediction/index.js.map +1 -1
- package/dist/engines/prediction/types.d.ts +35 -5
- package/dist/engines/prediction/types.d.ts.map +1 -1
- package/dist/engines/prediction/types.js +39 -1
- package/dist/engines/prediction/types.js.map +1 -1
- package/dist/engines/pricing/index.d.ts +2 -2
- package/dist/engines/pricing/index.d.ts.map +1 -1
- package/dist/engines/pricing/index.js +3 -1
- package/dist/engines/pricing/index.js.map +1 -1
- package/dist/engines/pricing/types.d.ts +15 -1
- package/dist/engines/pricing/types.d.ts.map +1 -1
- package/dist/engines/pricing/types.js +16 -1
- package/dist/engines/pricing/types.js.map +1 -1
- package/dist/engines/ranking/index.d.ts +1 -1
- package/dist/engines/ranking/index.d.ts.map +1 -1
- package/dist/engines/ranking/index.js +6 -1
- package/dist/engines/ranking/index.js.map +1 -1
- package/dist/engines/ranking/types.d.ts +32 -5
- package/dist/engines/ranking/types.d.ts.map +1 -1
- package/dist/engines/ranking/types.js +36 -1
- package/dist/engines/ranking/types.js.map +1 -1
- package/dist/engines/recipe-costing/compiler.d.ts +11 -0
- package/dist/engines/recipe-costing/compiler.d.ts.map +1 -0
- package/dist/engines/recipe-costing/compiler.js +177 -0
- package/dist/engines/recipe-costing/compiler.js.map +1 -0
- package/dist/engines/recipe-costing/engine.d.ts +32 -0
- package/dist/engines/recipe-costing/engine.d.ts.map +1 -0
- package/dist/engines/recipe-costing/engine.js +40 -0
- package/dist/engines/recipe-costing/engine.js.map +1 -0
- package/dist/engines/recipe-costing/index.d.ts +9 -0
- package/dist/engines/recipe-costing/index.d.ts.map +1 -0
- package/dist/engines/recipe-costing/index.js +21 -0
- package/dist/engines/recipe-costing/index.js.map +1 -0
- package/dist/engines/recipe-costing/strategy.d.ts +20 -0
- package/dist/engines/recipe-costing/strategy.d.ts.map +1 -0
- package/dist/engines/recipe-costing/strategy.js +265 -0
- package/dist/engines/recipe-costing/strategy.js.map +1 -0
- package/dist/engines/recipe-costing/types.d.ts +213 -0
- package/dist/engines/recipe-costing/types.d.ts.map +1 -0
- package/dist/engines/recipe-costing/types.js +36 -0
- package/dist/engines/recipe-costing/types.js.map +1 -0
- package/dist/engines/scoring/index.d.ts +1 -1
- package/dist/engines/scoring/index.d.ts.map +1 -1
- package/dist/engines/scoring/index.js +3 -1
- package/dist/engines/scoring/index.js.map +1 -1
- package/dist/engines/scoring/types.d.ts +8 -1
- package/dist/engines/scoring/types.d.ts.map +1 -1
- package/dist/engines/scoring/types.js +18 -1
- package/dist/engines/scoring/types.js.map +1 -1
- package/dist/engines/sentiment/index.d.ts +1 -1
- package/dist/engines/sentiment/index.d.ts.map +1 -1
- package/dist/engines/sentiment/index.js +3 -1
- package/dist/engines/sentiment/index.js.map +1 -1
- package/dist/engines/sentiment/types.d.ts +13 -2
- package/dist/engines/sentiment/types.d.ts.map +1 -1
- package/dist/engines/sentiment/types.js +17 -1
- package/dist/engines/sentiment/types.js.map +1 -1
- package/dist/engines/state-machine/index.d.ts +1 -1
- package/dist/engines/state-machine/index.d.ts.map +1 -1
- package/dist/engines/state-machine/index.js +5 -1
- package/dist/engines/state-machine/index.js.map +1 -1
- package/dist/engines/state-machine/types.d.ts +7 -0
- package/dist/engines/state-machine/types.d.ts.map +1 -1
- package/dist/engines/state-machine/types.js +14 -0
- package/dist/engines/state-machine/types.js.map +1 -1
- package/dist/engines/utility/index.d.ts +2 -2
- package/dist/engines/utility/index.d.ts.map +1 -1
- package/dist/engines/utility/index.js +4 -1
- package/dist/engines/utility/index.js.map +1 -1
- package/dist/engines/utility/types.d.ts +21 -3
- package/dist/engines/utility/types.d.ts.map +1 -1
- package/dist/engines/utility/types.js +37 -1
- package/dist/engines/utility/types.js.map +1 -1
- package/dist/index.d.ts +28 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/evaluation/decay.ts +13 -2
- package/src/core/types/rule.ts +25 -1
- package/src/engines/bayesian/index.ts +1 -0
- package/src/engines/bayesian/types.ts +10 -8
- package/src/engines/constraint/types.ts +40 -11
- package/src/engines/decay/index.ts +4 -0
- package/src/engines/decay/types.ts +26 -4
- package/src/engines/defeasible/index.ts +7 -0
- package/src/engines/defeasible/types.ts +42 -18
- package/src/engines/ensemble/index.ts +6 -0
- package/src/engines/ensemble/types.ts +17 -13
- package/src/engines/expert/index.ts +1 -0
- package/src/engines/expert/types.ts +11 -9
- package/src/engines/fuzzy/fuzzy.types.ts +65 -31
- package/src/engines/loyalty/index.ts +9 -0
- package/src/engines/loyalty/types.ts +36 -5
- package/src/engines/menu-engineering/compiler.ts +145 -0
- package/src/engines/menu-engineering/engine.ts +48 -0
- package/src/engines/menu-engineering/index.ts +47 -0
- package/src/engines/menu-engineering/strategy.ts +414 -0
- package/src/engines/menu-engineering/types.ts +242 -0
- package/src/engines/monte-carlo/index.ts +1 -0
- package/src/engines/monte-carlo/types.ts +16 -21
- package/src/engines/negotiation/index.ts +8 -0
- package/src/engines/negotiation/types.ts +23 -4
- package/src/engines/prediction/index.ts +5 -0
- package/src/engines/prediction/types.ts +35 -5
- package/src/engines/pricing/index.ts +3 -1
- package/src/engines/pricing/types.ts +17 -1
- package/src/engines/ranking/index.ts +5 -0
- package/src/engines/ranking/types.ts +32 -11
- package/src/engines/recipe-costing/compiler.ts +219 -0
- package/src/engines/recipe-costing/engine.ts +48 -0
- package/src/engines/recipe-costing/index.ts +48 -0
- package/src/engines/recipe-costing/strategy.ts +357 -0
- package/src/engines/recipe-costing/types.ts +269 -0
- package/src/engines/scoring/index.ts +2 -0
- package/src/engines/scoring/types.ts +8 -6
- package/src/engines/sentiment/index.ts +2 -0
- package/src/engines/sentiment/types.ts +13 -2
- package/src/engines/state-machine/index.ts +3 -0
- package/src/engines/state-machine/types.ts +8 -0
- package/src/engines/utility/index.ts +5 -0
- package/src/engines/utility/types.ts +23 -3
- 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>;
|
|
@@ -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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
@@ -11,10 +11,21 @@
|
|
|
11
11
|
// ========================================
|
|
12
12
|
|
|
13
13
|
/** Sentiment analysis strategy */
|
|
14
|
-
export
|
|
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
|
|
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
|