code wiki / (root) / nx_ferment_process.nx

nx_ferment_process.nx source

↩ module page · 191 lines · 7850 B

1// nx_ferment_process.nx -- R2 process rung: the ferment state machine. 2// 3// A fermentation recipe is a SEQUENCE OF PHASES, each with a target 4// temperature (for the R1 thermal controller) and an exit condition 5// (temperature reached, pH reached, or time elapsed). This is the 6// yogurt-maker / cheesemaker LOGIC: pasteurize -> cool -> inoculate -> 7// ferment-hold -> chill (yogurt); ripen -> rennet -> cut -> cook -> 8// drain -> press -> age (cheese). One generic executor, data-driven 9// per Rule 6/11 -- the phase table is data, not code. 10// 11// SAFETY COMPOSITION: every phase flagged is_ferment passes through the 12// R0 never-poison law (nx_ferment_validate) BEFORE its exit is checked. 13// Any refusal HALTS the whole process (status = HALTED_UNSAFE) and 14// records the verdict. The never-poison law thus propagates up the 15// ladder -- an unsafe batch can never run to "done". 16// 17// Non-ferment phases (e.g. the 82 C pasteurization, which is above 18// culture-kill) are NOT validated by the ferment law -- no culture is 19// present yet, so culture-kill does not apply. That separation is why 20// nx_ferment_validate documents itself as the ferment-HOLD predicate. 21// 22// genealogy_id: nishi_ferment_safety_envelope_2026 (via nx_ferment_safety) 23// + state_machine_recipe_executor_pattern 24 25import "nx_syscalls.nx" 26import "nx_ferment_safety.nx" 27const NX_MAGIC_82000: i64 = 82000 28const NX_MAGIC_43000: i64 = 43000 29const NX_MAGIC_4500: i64 = 4500 30const NX_MAGIC_5000: i64 = 5000 31 32// ===== Sealed enum: phase kind =================================== 33 34const NX_FP_PASTEURIZE: nx_int = 0 35const NX_FP_COOL: nx_int = 1 36const NX_FP_INOCULATE: nx_int = 2 37const NX_FP_FERMENT: nx_int = 3 38const NX_FP_CHILL: nx_int = 4 39const NX_FP_PRESS: nx_int = 5 40const NX_FP_AGE: nx_int = 6 41 42// ===== Sealed enum: exit-condition kind ========================== 43 44const NX_FX_TEMP_AT_OR_ABOVE: nx_int = 0 // temp >= threshold (heating done) 45const NX_FX_TEMP_AT_OR_BELOW: nx_int = 1 // temp <= threshold (cooling done) 46const NX_FX_PH_AT_OR_BELOW: nx_int = 2 // pH <= threshold (acidified) 47const NX_FX_TIME_ELAPSED: nx_int = 3 // elapsed >= threshold 48 49// ===== Sealed enum: process status =============================== 50 51const NX_PR_RUNNING: nx_int = 0 52const NX_PR_COMPLETE: nx_int = 1 53const NX_PR_HALTED_UNSAFE: nx_int = 2 54 55// ===== Structs =================================================== 56 57struct NxFermentPhase { 58 kind: nx_int, 59 target_milli_c: i64, 60 exit_kind: nx_int, 61 exit_threshold: i64, 62 is_ferment: nx_int, // 1 = run the R0 never-poison law each step 63 oxygen: nx_int, // NX_OX_* for the ferment law 64} 65 66const NX_FP_PHASE_BYTES: nx_size = 48 67 68struct NxFermentProcess { 69 phases: *NxFermentPhase, 70 n_phases: nx_int, 71 current: nx_int, 72 env: *NxFermentSafetyEnvelope, 73 status: nx_int, 74 last_verdict: nx_int, 75 phase_start_hours: i64, 76 ferment_kind: nx_int, 77} 78 79func nx_ferment_phases_new(n: nx_size) -> *NxFermentPhase { 80 let bytes: nx_size = n * NX_FP_PHASE_BYTES 81 return (sys_mmap(bytes)) as *NxFermentPhase 82} 83 84func _fp_phase_at(phases: *NxFermentPhase, idx: nx_size) -> *NxFermentPhase { 85 return (phases as i64 + (idx as i64) * NX_FP_PHASE_BYTES) as *NxFermentPhase 86} 87 88func nx_ferment_phase_set(phases: *NxFermentPhase, idx: nx_size, 89 kind: nx_int, target_milli_c: i64, 90 exit_kind: nx_int, exit_threshold: i64, 91 is_ferment: nx_int, oxygen: nx_int) -> i64 { 92 let p: *NxFermentPhase = _fp_phase_at(phases, idx) 93 p.kind = kind 94 p.target_milli_c = target_milli_c 95 p.exit_kind = exit_kind 96 p.exit_threshold = exit_threshold 97 p.is_ferment = is_ferment 98 p.oxygen = oxygen 99 return 0 100} 101 102func nx_ferment_process_new(env: *NxFermentSafetyEnvelope, 103 phases: *NxFermentPhase, n_phases: nx_int, 104 ferment_kind: nx_int) -> *NxFermentProcess { 105 let pr: *NxFermentProcess = (sys_mmap(64)) as *NxFermentProcess 106 pr.phases = phases 107 pr.n_phases = n_phases 108 pr.current = 0 109 pr.env = env 110 pr.status = NX_PR_RUNNING 111 pr.last_verdict = NX_FS_OK 112 pr.phase_start_hours = 0 113 pr.ferment_kind = ferment_kind 114 return pr 115} 116 117// The current phase's setpoint -- what the R1 thermal controller targets. 118func nx_ferment_process_target(pr: *NxFermentProcess) -> i64 { 119 if pr.current >= pr.n_phases { return 0 } 120 let ph: *NxFermentPhase = _fp_phase_at(pr.phases, pr.current as nx_size) 121 return ph.target_milli_c 122} 123 124// Is the current phase a safety-validated ferment hold? 125func nx_ferment_process_current_is_ferment(pr: *NxFermentProcess) -> nx_int { 126 if pr.current >= pr.n_phases { return 0 } 127 let ph: *NxFermentPhase = _fp_phase_at(pr.phases, pr.current as nx_size) 128 return ph.is_ferment 129} 130 131// One process step. On a ferment phase it runs the R0 never-poison law 132// and HALTS on any refusal; otherwise it checks the current phase's exit 133// condition and advances. Returns the process status. 134func nx_ferment_process_step(pr: *NxFermentProcess, 135 temp_milli_c: i64, ph_milli: i64, 136 salt_pct_milli: i64, age_days: i64, 137 now_hours: i64, 138 log: *NxFermentReadingLog) -> nx_int { 139 if pr.status != NX_PR_RUNNING { return pr.status } 140 if pr.current >= pr.n_phases { pr.status = NX_PR_COMPLETE; return pr.status } 141 142 let cur: *NxFermentPhase = _fp_phase_at(pr.phases, pr.current as nx_size) 143 144 // Never-poison gate on ferment-hold phases. 145 if cur.is_ferment == 1 { 146 let elapsed: i64 = now_hours - pr.phase_start_hours 147 let verdict: nx_int = nx_ferment_validate(pr.env, pr.ferment_kind, 148 cur.oxygen, temp_milli_c as nx_size, ph_milli as nx_size, 149 salt_pct_milli as nx_size, elapsed as nx_size, 150 age_days as nx_size, log, now_hours as nx_size) 151 pr.last_verdict = verdict 152 if verdict != NX_FS_OK { 153 pr.status = NX_PR_HALTED_UNSAFE 154 return pr.status 155 } 156 } 157 158 // Exit-condition check. 159 var advance: nx_int = 0 160 if cur.exit_kind == NX_FX_TEMP_AT_OR_ABOVE { 161 if temp_milli_c >= cur.exit_threshold { advance = 1 } 162 } 163 if cur.exit_kind == NX_FX_TEMP_AT_OR_BELOW { 164 if temp_milli_c <= cur.exit_threshold { advance = 1 } 165 } 166 if cur.exit_kind == NX_FX_PH_AT_OR_BELOW { 167 if ph_milli <= cur.exit_threshold { advance = 1 } 168 } 169 if cur.exit_kind == NX_FX_TIME_ELAPSED { 170 let el: i64 = now_hours - pr.phase_start_hours 171 if el >= cur.exit_threshold { advance = 1 } 172 } 173 174 if advance == 1 { 175 pr.current = pr.current + 1 176 pr.phase_start_hours = now_hours 177 if pr.current >= pr.n_phases { pr.status = NX_PR_COMPLETE } 178 } 179 return pr.status 180} 181 182// Build the canonical yogurt phase table into a 5-phase buffer. 183// 0 PASTEURIZE 82 C 1 COOL 43 C 2 INOCULATE 3 FERMENT->pH4.5 4 CHILL 5 C 184func nx_ferment_build_yogurt(phases: *NxFermentPhase) -> i64 { 185 nx_ferment_phase_set(phases, 0, NX_FP_PASTEURIZE, NX_MAGIC_82000, NX_FX_TEMP_AT_OR_ABOVE, NX_MAGIC_82000, 0, NX_OX_AEROBIC) 186 nx_ferment_phase_set(phases, 1, NX_FP_COOL, NX_MAGIC_43000, NX_FX_TEMP_AT_OR_BELOW, NX_MAGIC_43000, 0, NX_OX_AEROBIC) 187 nx_ferment_phase_set(phases, 2, NX_FP_INOCULATE, NX_MAGIC_43000, NX_FX_TIME_ELAPSED, 0, 0, NX_OX_AEROBIC) 188 nx_ferment_phase_set(phases, 3, NX_FP_FERMENT, NX_MAGIC_43000, NX_FX_PH_AT_OR_BELOW, NX_MAGIC_4500, 1, NX_OX_ANAEROBIC) 189 nx_ferment_phase_set(phases, 4, NX_FP_CHILL, NX_MAGIC_5000, NX_FX_TEMP_AT_OR_BELOW, NX_MAGIC_5000, 0, NX_OX_AEROBIC) 190 return 0 191}