nx_allergen.nx source
↩ module page · 138 lines · 5384 B
1// nx_allergen.nx -- FOOD-SCIENCE SUITE / ALLERGEN MANAGEMENT rung. Tracks
2// the FDA "big 9" allergens (FASTER Act 2021) as a bitmask, rolls a
3// recipe's ingredients up into its full allergen profile (catching HIDDEN
4// allergens a naive list misses -- traditional soy sauce carries BOTH soy
5// AND wheat), checks a recipe against a person's avoid-profile, flags
6// shared-equipment CROSS-CONTACT ("may contain"), and suggests a
7// substitution that NEVER reintroduces an avoided allergen.
8//
9// THE exceed: allergen safety is computed by ROLLUP + personalization +
10// cross-contact + safe-substitution, not a static "contains nuts" line;
11// and a substitute is validated against the SAME avoid-profile so it can't
12// swap one allergen for another (the classic milk->soy-milk mistake for a
13// soy-allergic person).
14//
15// Grounded: fda_faster_act_2021_big9_allergens + fda_food_allergen_labeling
16// genealogy_id: fda_big9_allergens + nishi_food_science_suite
17
18import "nx_syscalls.nx"
19
20// ===== The big-9 allergens (bit + index forms) ====================
21
22const AL_MILK: i64 = 1
23const AL_EGG: i64 = 2
24const AL_FISH: i64 = 4
25const AL_SHELLFISH: i64 = 8
26const AL_TREENUT: i64 = 16
27const AL_PEANUT: i64 = 32
28const AL_WHEAT: i64 = 64
29const AL_SOY: i64 = 128
30const AL_SESAME: i64 = 256
31const AL_N: i64 = 9 // number of allergen bits
32
33// ===== Ingredient catalog =========================================
34
35const ING_MILK: i64 = 0
36const ING_CHEESE: i64 = 1
37const ING_YOGURT: i64 = 2
38const ING_EGG: i64 = 3
39const ING_WHEAT_FLOUR: i64 = 4
40const ING_BREAD: i64 = 5
41const ING_SOY_SAUCE: i64 = 6
42const ING_TOFU: i64 = 7
43const ING_PEANUT: i64 = 8
44const ING_ALMOND: i64 = 9
45const ING_SHRIMP: i64 = 10
46const ING_SALMON: i64 = 11
47const ING_SESAME_OIL: i64 = 12
48const ING_TAHINI: i64 = 13
49const ING_RICE: i64 = 14
50const ING_CHICKEN: i64 = 15
51const ING_OLIVE_OIL: i64 = 16
52// substitutes (allergen-free relative to the big 9):
53const ING_OAT_MILK: i64 = 17
54const ING_RICE_FLOUR: i64 = 18
55const ING_COCONUT_AMINOS: i64 = 19
56const ING_FLAX_EGG: i64 = 20
57const ING_SUNFLOWER_BUTTER: i64 = 21
58const ING_N: i64 = 22
59
60// Ingredient -> its big-9 allergen bitmask (unknown -> none).
61func nx_ingredient_allergens(id: i64) -> i64 {
62 if id == ING_MILK { return AL_MILK }
63 if id == ING_CHEESE { return AL_MILK }
64 if id == ING_YOGURT { return AL_MILK }
65 if id == ING_EGG { return AL_EGG }
66 if id == ING_WHEAT_FLOUR { return AL_WHEAT }
67 if id == ING_BREAD { return AL_WHEAT }
68 if id == ING_SOY_SAUCE { return AL_SOY | AL_WHEAT } // hidden wheat!
69 if id == ING_TOFU { return AL_SOY }
70 if id == ING_PEANUT { return AL_PEANUT }
71 if id == ING_ALMOND { return AL_TREENUT }
72 if id == ING_SHRIMP { return AL_SHELLFISH }
73 if id == ING_SALMON { return AL_FISH }
74 if id == ING_SESAME_OIL { return AL_SESAME }
75 if id == ING_TAHINI { return AL_SESAME }
76 return 0
77}
78
79// ===== Recipe rollup + checks =====================================
80
81// Accumulate an ingredient into a running recipe allergen mask.
82func al_mask_add(mask: i64, ing_id: i64) -> i64 {
83 return mask | nx_ingredient_allergens(ing_id)
84}
85
86func al_contains(recipe_mask: i64, allergen_bit: i64) -> i64 {
87 if (recipe_mask & allergen_bit) != 0 { return 1 }
88 return 0
89}
90
91// Is a recipe safe for a person who must AVOID a set of allergens?
92func al_safe_for(recipe_mask: i64, avoid_mask: i64) -> i64 {
93 if (recipe_mask & avoid_mask) == 0 { return 1 }
94 return 0
95}
96
97// How many distinct big-9 allergens are present.
98func al_count(recipe_mask: i64) -> i64 {
99 var n: i64 = 0
100 var i: i64 = 0
101 while i < AL_N {
102 let bit: i64 = 1 << i
103 if (recipe_mask & bit) != 0 { n = n + 1 }
104 i = i + 1
105 }
106 return n
107}
108
109// CROSS-CONTACT: even if not an ingredient, a shared-equipment allergen is
110// a "may contain" risk for someone avoiding it.
111func al_cross_contact_risk(facility_mask: i64, avoid_mask: i64) -> i64 {
112 if (facility_mask & avoid_mask) != 0 { return 1 }
113 return 0
114}
115
116// ===== Safe substitution ==========================================
117
118// A substitute for an ingredient that reintroduces NONE of the avoided
119// allergens. Already-safe ingredients are returned unchanged; if no safe
120// substitute is known, returns -1.
121func al_substitute(ing_id: i64, avoid_mask: i64) -> i64 {
122 if (nx_ingredient_allergens(ing_id) & avoid_mask) == 0 { return ing_id }
123 var cand: i64 = -1
124 if ing_id == ING_MILK { cand = ING_OAT_MILK }
125 if ing_id == ING_CHEESE { cand = ING_OAT_MILK }
126 if ing_id == ING_YOGURT { cand = ING_OAT_MILK }
127 if ing_id == ING_WHEAT_FLOUR { cand = ING_RICE_FLOUR }
128 if ing_id == ING_BREAD { cand = ING_RICE_FLOUR }
129 if ing_id == ING_SOY_SAUCE { cand = ING_COCONUT_AMINOS }
130 if ing_id == ING_TOFU { cand = ING_COCONUT_AMINOS }
131 if ing_id == ING_EGG { cand = ING_FLAX_EGG }
132 if ing_id == ING_PEANUT { cand = ING_SUNFLOWER_BUTTER }
133 if ing_id == ING_ALMOND { cand = ING_SUNFLOWER_BUTTER }
134 if cand < 0 { return -1 }
135 // never reintroduce an avoided allergen (defensive, by construction).
136 if (nx_ingredient_allergens(cand) & avoid_mask) != 0 { return -1 }
137 return cand
138}