code wiki / (root) / nx_ferment_safety.nx

nx_ferment_safety.nx source

↩ module page · 296 lines · 12927 B

1// nx_ferment_safety.nx -- NEVER-POISON substrate (Global Rule 26 recast). 2// 3// Global Rule 26 ("Never Brick The Electronics") for fermentation: 4// a fermentation capability may NEVER recommend or execute a process 5// that risks producing unsafe food. No botulism, no time-temperature 6// pathogen abuse, no under-salted anaerobic vegetable ferment, no 7// raw-milk cheese under the legal aging floor. STRUCTURAL refusal -- 8// not a policy, not a warning string -- mirrored exactly on the proven 9// nx_battery_safety architecture (envelope + sealed verdicts + 10// validate predicate + abuse-PATTERN detector). 11// 12// The never-poison law (no higher rung may bypass this layer): 13// - botulism_ph_milli: an anaerobic low-acid food MUST reach 14// pH <= 4.6 (FDA 21 CFR 114 acidified-foods 15// boundary -- C. botulinum cannot grow below 16// pH 4.6). Past its acidification window and 17// still above 4.6 (or pH unknown) = REFUSE. 18// - danger zone 4..60 C: USDA-FSIS "danger zone" (40-140 F). 19// - min_salt_pct_milli: >= 2.0% w/w selects Lactobacillus over 20// pathogens in vegetable ferments (NCHFP). 21// - min_raw_milk_age_days: raw-milk cheese aged >= 60 days (FDA). 22// - culture_kill_temp: above this the culture dies -> no 23// acidification -> the food never becomes 24// safe. REFUSE. 25// 26// The STALLED-FERMENT detector is the never-poison analog of 27// nx_battery_safety's thermal-runaway detector. A warm ferment is 28// NORMAL early on (danger-zone temp + still-high pH while the culture 29// sours it). It is ABUSE only when pH FAILS TO DECLINE across the 30// window -- stuck high = trending septic, not souring. This is proven 31// MECHANICALLY by the gate (the stall must fire; a souring batch must 32// NOT be refused) -- never asserted as a promise (Rule 26 discipline). 33// 34// genealogy_id: fda_21cfr114_acidified_ph4.6 35// + usda_fsis_danger_zone_40_140F 36// + fda_raw_milk_cheese_60day_aging 37// + nchfp_vegetable_ferment_2pct_salt 38// + nishi_battery_safety_envelope_architecture_2026 39// 40// NOTE: the public-health constants below are NAMED + CITED (Rule 11, 41// no magic numbers). They are grounded/verified against fetched 42// FDA/USDA/NCHFP sources by nx_ferment_research_fetch (the Nishi 43// researcher) -- a later rung. 44 45import "nx_syscalls.nx" 46 47// ===== Sealed enum: NxFermentKind ================================ 48// Oxygen exposure + substrate class drive which rules apply. 49 50const NX_FK_VEG_ANAEROBIC: nx_int = 0 // kraut, kimchi, brined pickles 51const NX_FK_DAIRY_CULTURED: nx_int = 1 // yogurt, kefir, buttermilk 52const NX_FK_CHEESE_RAWMILK: nx_int = 2 // raw-milk cheese (60-day rule) 53const NX_FK_CHEESE_PASTEUR: nx_int = 3 // pasteurized-milk cheese 54const NX_FK_MOLD_RIPENED: nx_int = 4 // tempeh, koji, blue (intended mold) 55const NX_FK_ALCOHOL_ACID: nx_int = 5 // kombucha, vinegar, wine 56const NX_FK_N_KINDS: nx_int = 6 57 58// ===== Sealed enum: NxOxygen ===================================== 59 60const NX_OX_ANAEROBIC: nx_int = 0 61const NX_OX_AEROBIC: nx_int = 1 62 63// ===== Sealed enum: NxFermentSafetyVerdict ======================= 64 65const NX_FS_OK: nx_int = 0 66const NX_FS_REFUSED_BOTULISM_PH: nx_int = 1 67const NX_FS_REFUSED_TIME_TEMP_ABUSE: nx_int = 2 68const NX_FS_REFUSED_INSUFFICIENT_SALT: nx_int = 3 69const NX_FS_REFUSED_CULTURE_KILLED: nx_int = 4 70const NX_FS_REFUSED_RAW_MILK_AGE: nx_int = 5 71const NX_FS_REFUSED_PATHOGEN_PATTERN: nx_int = 6 72const NX_FS_REFUSED_BAD_KIND: nx_int = 7 73 74// ===== Canonical food-safety boundaries (public-health constants) = 75 76const NX_FS_BOTULISM_PH_MILLI: nx_size = 4600 // pH 4.600 x1000 77const NX_FS_DZ_LO_MILLI_C: nx_size = 4000 // 4.0 C (40 F) 78const NX_FS_DZ_HI_MILLI_C: nx_size = 60000 // 60.0 C (140 F) 79const NX_FS_CULTURE_KILL_MILLI_C: nx_size = 55000 // 55 C thermophile death 80const NX_FS_MIN_SALT_PCT_MILLI: nx_size = 2000 // 2.000% w/w 81const NX_FS_RAW_MILK_AGE_DAYS: nx_size = 60 // FDA 82const NX_FS_ABUSE_WINDOW_HOURS: nx_size = 48 83const NX_FS_ABUSE_THRESHOLD: nx_int = 3 84const NX_FS_DEFAULT_ACID_HOURS: nx_size = 48 // must cross pH 4.6 within 48 h 85const NX_FS_STALL_MIN_PROGRESS_MILLI: nx_size = 200 // pH must drop >= 0.2 / window or stalled 86 87// ===== Struct: NxFermentSafetyEnvelope =========================== 88 89struct NxFermentSafetyEnvelope { 90 ferment_id: nx_int, 91 botulism_ph_milli: nx_size, 92 max_hours_to_acidify: nx_size, 93 danger_zone_lo_milli_c: nx_size, 94 danger_zone_hi_milli_c: nx_size, 95 culture_kill_temp_milli_c: nx_size, 96 min_salt_pct_milli: nx_size, 97 min_raw_milk_age_days: nx_size, 98 abuse_window_hours: nx_size, 99 abuse_pattern_threshold: nx_int, 100 stall_min_progress_milli: nx_size, 101} 102 103// ===== Struct: NxFermentReading / NxFermentReadingLog ============ 104// 105// Caller-supplied ring of recent (temp, pH, time) observations. The 106// substrate uses it to detect the stalled-ferment pattern. 107 108struct NxFermentReading { 109 temp_milli_c: nx_size, 110 ph_milli: nx_size, 111 ts_hours: nx_size, 112} 113 114const NX_FS_READING_BYTES: nx_size = 24 115 116struct NxFermentReadingLog { 117 readings: *NxFermentReading, 118 capacity: nx_size, 119 head: nx_size, 120 count: nx_size, 121} 122 123func nx_fk_is_valid(k: nx_int) -> nx_int { 124 if k < 0 { return 0 } 125 if k >= NX_FK_N_KINDS { return 0 } 126 return 1 127} 128 129func nx_ferment_safety_envelope_new(ferment_id: nx_int, 130 botulism_ph: nx_size, 131 max_acid_hours: nx_size, 132 dz_lo: nx_size, 133 dz_hi: nx_size, 134 kill_temp: nx_size, 135 min_salt: nx_size, 136 min_age_days: nx_size, 137 abuse_window: nx_size, 138 abuse_threshold: nx_int, 139 stall_min_progress: nx_size) -> *NxFermentSafetyEnvelope { 140 let e: *NxFermentSafetyEnvelope = (sys_mmap(88)) as *NxFermentSafetyEnvelope 141 e.ferment_id = ferment_id 142 e.botulism_ph_milli = botulism_ph 143 e.max_hours_to_acidify = max_acid_hours 144 e.danger_zone_lo_milli_c = dz_lo 145 e.danger_zone_hi_milli_c = dz_hi 146 e.culture_kill_temp_milli_c = kill_temp 147 e.min_salt_pct_milli = min_salt 148 e.min_raw_milk_age_days = min_age_days 149 e.abuse_window_hours = abuse_window 150 e.abuse_pattern_threshold = abuse_threshold 151 e.stall_min_progress_milli = stall_min_progress 152 return e 153} 154 155// Canonical default envelope -- the cited public-health constants. 156func nx_ferment_safety_envelope_default(ferment_id: nx_int) -> *NxFermentSafetyEnvelope { 157 return nx_ferment_safety_envelope_new(ferment_id, 158 NX_FS_BOTULISM_PH_MILLI, NX_FS_DEFAULT_ACID_HOURS, 159 NX_FS_DZ_LO_MILLI_C, NX_FS_DZ_HI_MILLI_C, NX_FS_CULTURE_KILL_MILLI_C, 160 NX_FS_MIN_SALT_PCT_MILLI, NX_FS_RAW_MILK_AGE_DAYS, 161 NX_FS_ABUSE_WINDOW_HOURS, NX_FS_ABUSE_THRESHOLD, 162 NX_FS_STALL_MIN_PROGRESS_MILLI) 163} 164 165func nx_ferment_reading_log_new(capacity: nx_size) -> *NxFermentReadingLog { 166 let l: *NxFermentReadingLog = (sys_mmap(32)) as *NxFermentReadingLog 167 let bytes: nx_size = capacity * NX_FS_READING_BYTES 168 l.readings = (sys_mmap(bytes)) as *NxFermentReading 169 l.capacity = capacity 170 l.head = 0 171 l.count = 0 172 return l 173} 174 175func _fs_reading_at(l: *NxFermentReadingLog, idx: nx_size) -> *NxFermentReading { 176 return (l.readings as i64 + (idx as i64) * NX_FS_READING_BYTES) as *NxFermentReading 177} 178 179// ===== nx_ferment_validate ======================================= 180// 181// THE SUBSTRATE-LEVEL NEVER-POISON REFUSAL POINT. Validates the 182// FERMENT-HOLD state (post-inoculation); the optional pre-pasteur heat 183// step is a separate non-ferment operation. Returns a sealed verdict. 184 185func nx_ferment_validate(env: *NxFermentSafetyEnvelope, 186 kind: nx_int, 187 oxygen: nx_int, 188 temp_milli_c: nx_size, 189 ph_milli: nx_size, 190 salt_pct_milli: nx_size, 191 elapsed_hours: nx_size, 192 age_days: nx_size, 193 log: *NxFermentReadingLog, 194 now_hours: nx_size) -> nx_int { 195 // Layer 0: kind must be a sealed value 196 if nx_fk_is_valid(kind) == 0 { return NX_FS_REFUSED_BAD_KIND } 197 198 // Layer 1: culture-kill -- above this temp the culture dies, the 199 // food never acidifies, so it never becomes safe. 200 if temp_milli_c > env.culture_kill_temp_milli_c { return NX_FS_REFUSED_CULTURE_KILLED } 201 202 // Layer 2: salt floor for anaerobic vegetable ferments 203 if kind == NX_FK_VEG_ANAEROBIC { 204 if salt_pct_milli < env.min_salt_pct_milli { return NX_FS_REFUSED_INSUFFICIENT_SALT } 205 } 206 207 // Layer 3: raw-milk cheese legal aging floor 208 if kind == NX_FK_CHEESE_RAWMILK { 209 if age_days < env.min_raw_milk_age_days { return NX_FS_REFUSED_RAW_MILK_AGE } 210 } 211 212 // Layer 4: BOTULISM -- an anaerobic low-acid food that has had the 213 // full acidification window but has NOT crossed pH 4.6 (or has no 214 // confirmed pH) is a botulism risk. Structural refusal. 215 if oxygen == NX_OX_ANAEROBIC { 216 if elapsed_hours >= env.max_hours_to_acidify { 217 if ph_milli == 0 { return NX_FS_REFUSED_BOTULISM_PH } 218 if ph_milli > env.botulism_ph_milli { return NX_FS_REFUSED_BOTULISM_PH } 219 } 220 } 221 222 // Layer 5: STALLED-FERMENT pattern. Count danger-zone readings in 223 // the window and find the OLDEST one's pH. A batch is stalled iff 224 // there are threshold+ danger-zone readings, the current pH is still 225 // above the botulism line, AND pH dropped < min_progress from the 226 // window's oldest reading (no acidification = trending septic). A 227 // healthy souring batch (pH visibly declining) is NEVER refused. 228 if (log as i64) != 0 { 229 var dz_count: nx_int = 0 230 var have_oldest: nx_int = 0 231 var oldest_ts: nx_size = now_hours 232 var oldest_ph: nx_size = 0 233 var live: nx_size = log.count 234 if live > log.capacity { live = log.capacity } 235 var i: nx_size = 0 236 while i < live { 237 let r: *NxFermentReading = _fs_reading_at(log, i) 238 if r.ts_hours <= now_hours { 239 if now_hours - r.ts_hours < env.abuse_window_hours { 240 if r.temp_milli_c >= env.danger_zone_lo_milli_c { 241 if r.temp_milli_c <= env.danger_zone_hi_milli_c { 242 dz_count = dz_count + 1 243 if have_oldest == 0 { 244 oldest_ts = r.ts_hours 245 oldest_ph = r.ph_milli 246 have_oldest = 1 247 } 248 if r.ts_hours < oldest_ts { 249 oldest_ts = r.ts_hours 250 oldest_ph = r.ph_milli 251 } 252 } 253 } 254 } 255 } 256 i = i + 1 257 } 258 // include the current reading in the danger-zone count 259 if temp_milli_c >= env.danger_zone_lo_milli_c { 260 if temp_milli_c <= env.danger_zone_hi_milli_c { 261 dz_count = dz_count + 1 262 } 263 } 264 if dz_count >= env.abuse_pattern_threshold { 265 if ph_milli > env.botulism_ph_milli { 266 if have_oldest == 1 { 267 // dropped < min_progress <=> ph_now + min_progress > ph_oldest 268 if ph_milli + env.stall_min_progress_milli > oldest_ph { 269 return NX_FS_REFUSED_PATHOGEN_PATTERN 270 } 271 } 272 } 273 } 274 } 275 276 // OK -- append the current reading for future pattern detection. 277 if (log as i64) != 0 { 278 let r: *NxFermentReading = _fs_reading_at(log, log.head) 279 r.temp_milli_c = temp_milli_c 280 r.ph_milli = ph_milli 281 r.ts_hours = now_hours 282 log.head = log.head + 1 283 if log.head >= log.capacity { log.head = 0 } 284 log.count = log.count + 1 285 } 286 return NX_FS_OK 287} 288 289// ===== nx_ferment_envelope_ph_ceiling ============================ 290// 291// Predicate getter: the pH a ferment must reach to clear the botulism 292// gate. Lets a higher-rung planner bound its acidification target. 293 294func nx_ferment_envelope_ph_ceiling(env: *NxFermentSafetyEnvelope) -> nx_size { 295 return env.botulism_ph_milli 296}