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}