pub fn generate_c_monitor(window_size: usize, threshold: f64) -> String {
format!(r#"/* dfa_monitor.c -- Generated by struktura codegen
* Self-contained DFA structural health monitor.
* Compile: gcc -Wall -Werror -O2 -lm -o dfa_monitor dfa_monitor.c
* Zero dependencies. Pre-80s discipline.
* https://github.com/koscak-labs/struktura
*/
#include <math.h>
#include <string.h>
#define DFA_WINDOW_SIZE {window}
#define DFA_THRESHOLD {threshold:.4}
#define DFA_NUM_BOXES 6
static const int DFA_BOXES[DFA_NUM_BOXES] = {{16, 24, 36, 54, 81, 121}};
typedef struct {{
double alpha;
double r_squared;
}} dfa_result_t;
typedef struct {{
double buffer[DFA_WINDOW_SIZE];
int pos;
int filled;
double baseline_alpha;
int baseline_set;
int learning_count;
}} dfa_monitor_t;
static dfa_result_t dfa_compute(const double *values, int n) {{
dfa_result_t result = {{0.5, 0.0}};
if (n < 64) return result;
double mean = 0.0;
int i, seg, b;
for (i = 0; i < n; i++) mean += values[i];
mean /= (double)n;
/* Cumulative profile */
double y[DFA_WINDOW_SIZE];
double cum = 0.0;
for (i = 0; i < n; i++) {{
cum += values[i] - mean;
y[i] = cum;
}}
/* DFA: measure fluctuation at each box size */
double log_s[DFA_NUM_BOXES], log_f[DFA_NUM_BOXES];
int pts = 0;
for (b = 0; b < DFA_NUM_BOXES && DFA_BOXES[b] <= n / 4; b++) {{
int s = DFA_BOXES[b];
int num_segs = n / s;
if (num_segs == 0) continue;
double f2_sum = 0.0;
for (seg = 0; seg < num_segs; seg++) {{
int start = seg * s;
double sx = 0, sy = 0, sxy = 0, sx2 = 0;
for (i = 0; i < s; i++) {{
double xi = (double)i;
sx += xi;
sy += y[start + i];
sxy += xi * y[start + i];
sx2 += xi * xi;
}}
double k = (double)s;
double det = k * sx2 - sx * sx;
if (fabs(det) < 1e-15) continue;
double a0 = (sx2 * sy - sx * sxy) / det;
double a1 = (k * sxy - sx * sy) / det;
double resid = 0.0;
for (i = 0; i < s; i++) {{
double d = y[start + i] - (a0 + a1 * (double)i);
resid += d * d;
}}
f2_sum += resid / k;
}}
double f = sqrt(f2_sum / (double)num_segs);
if (f > 0.0) {{
log_s[pts] = log((double)s);
log_f[pts] = log(f);
pts++;
}}
}}
if (pts < 3) return result;
/* Log-log linear regression */
double k = (double)pts;
double sx = 0, sy = 0, sxy = 0, sx2 = 0;
for (i = 0; i < pts; i++) {{
sx += log_s[i]; sy += log_f[i];
sxy += log_s[i] * log_f[i]; sx2 += log_s[i] * log_s[i];
}}
double slope = (k * sxy - sx * sy) / (k * sx2 - sx * sx);
double ic = (sy - slope * sx) / k;
double ym = sy / k;
double sst = 0, ssr = 0;
for (i = 0; i < pts; i++) {{
sst += (log_f[i] - ym) * (log_f[i] - ym);
ssr += (log_f[i] - slope * log_s[i] - ic) * (log_f[i] - slope * log_s[i] - ic);
}}
result.alpha = slope;
result.r_squared = 1.0 - ssr / (sst > 1e-15 ? sst : 1e-15);
return result;
}}
/* Initialize monitor */
static void dfa_monitor_init(dfa_monitor_t *m) {{
memset(m, 0, sizeof(*m));
}}
/* Push a sample. Returns: 0=learning, 1=healthy, 2=watch, 3=warning, 4=critical */
static int dfa_monitor_push(dfa_monitor_t *m, double value) {{
m->buffer[m->pos] = value;
m->pos = (m->pos + 1) % DFA_WINDOW_SIZE;
if (m->pos == 0) m->filled = 1;
if (!m->filled) return 0;
m->learning_count++;
dfa_result_t r = dfa_compute(m->buffer, DFA_WINDOW_SIZE);
/* Learning phase: first 10 windows establish baseline */
if (!m->baseline_set && m->learning_count >= 10 && r.r_squared > 0.7) {{
m->baseline_alpha = r.alpha;
m->baseline_set = 1;
return 0;
}}
if (!m->baseline_set) return 0;
/* Health check */
double shift = fabs(r.alpha - m->baseline_alpha);
if (shift < DFA_THRESHOLD * 0.375) return 1; /* healthy */
if (shift < DFA_THRESHOLD) return 2; /* watch */
if (shift < DFA_THRESHOLD * 1.875) return 3; /* warning */
return 4; /* critical */
}}
"#, window = window_size, threshold = threshold)
}
pub fn generate_fprime_component(name: &str, window_size: usize) -> String {
let mut s = String::with_capacity(1024);
s.push_str(&format!("// {}.fpp -- Generated F Prime DFA health monitor\n", name));
s.push_str("// Generated by: struktura codegen --fprime\n\n");
s.push_str("module Svc {\n");
s.push_str(&format!(" passive component {} {{\n\n", name));
s.push_str(" sync input port schedIn: Svc.Sched\n");
s.push_str(" guarded input port tlmIn: Fw.Tlm\n\n");
s.push_str(" event StructuralShift(\n");
s.push_str(" channelId: FwChanIdType\n");
s.push_str(" baseline_alpha: F64\n");
s.push_str(" current_alpha: F64\n");
s.push_str(" delta: F64\n");
s.push_str(" ) severity warning high\n\n");
s.push_str(" event BaselineEstablished(\n");
s.push_str(" channelId: FwChanIdType\n");
s.push_str(" alpha: F64\n");
s.push_str(" r_squared: F64\n");
s.push_str(" ) severity activity high\n\n");
s.push_str(" telemetry DfaAlpha: F64\n");
s.push_str(" telemetry DfaR2: F64\n\n");
s.push_str(" time get port timeCaller\n");
s.push_str(" event port logOut\n");
s.push_str(" telemetry port tlmOut\n");
s.push_str(" }\n}\n\n");
s.push_str(&format!("// Link with libstruktura.a, window size: {}\n", window_size));
s.push_str("// https://github.com/koscak-labs/struktura\n");
s
}
pub fn generate_fprime_rover() -> String {
let mut s = String::with_capacity(4096);
s.push_str("// RoverHealth.fpp -- F Prime autonomous health monitor\n");
s.push_str("// Generated by: struktura generate --fprime --rover\n");
s.push_str("// 10 channels, 3 detection legs, no heap. ~6KB RAM.\n\n");
s.push_str("module Rover {\n");
s.push_str(" passive component RoverHealth {\n\n");
s.push_str(" // Rate-group driven: call schedIn every sample tick\n");
s.push_str(" sync input port schedIn: Svc.Sched\n\n");
let channels = [
("wheelFL", "Front-left wheel motor current (A)"),
("wheelFR", "Front-right wheel motor current (A)"),
("wheelRL", "Rear-left wheel motor current (A)"),
("wheelRR", "Rear-right wheel motor current (A)"),
("suspTilt", "Rocker-bogie tilt angle (deg)"),
("batVoltage", "Main bus voltage (V)"),
("batSOC", "Battery state of charge (0-1)"),
("thermCPU", "CPU temperature (C)"),
("thermMotor", "Average motor temperature (C)"),
("commSignal", "Downlink signal strength (dBm)"),
];
for (name, doc) in &channels {
s.push_str(&format!(" @ {}\n", doc));
s.push_str(&format!(" guarded input port {}: Fw.Tlm\n", name));
}
s.push('\n');
s.push_str(" @ Gradual drift detected on a channel\n");
s.push_str(" event Drift(channel: string size 20) severity warning high\n\n");
s.push_str(" @ Sensor appears stuck (same value repeating)\n");
s.push_str(" event SensorStuck(channel: string size 20) severity warning high\n\n");
s.push_str(" @ Signal shifted to a new operating level\n");
s.push_str(" event LevelShift(channel: string size 20) severity warning high\n\n");
s.push_str(" @ Channel quarantined, using reconstructed values\n");
s.push_str(" event Quarantined(channel: string size 20) severity warning high\n\n");
s.push_str(" @ Environment changed, learning new baseline\n");
s.push_str(" event Adapting() severity activity high\n\n");
s.push_str(" @ New baseline accepted\n");
s.push_str(" event Recalibrated() severity activity high\n\n");
s.push_str(" @ Adaptation rejected, fault confirmed\n");
s.push_str(" event FaultConfirmed(channel: string size 20) severity warning high\n\n");
for (name, _) in &channels {
s.push_str(&format!(" telemetry {}_health: U8 @ 0=ok 1=warning 2=quarantined\n", name));
}
s.push('\n');
s.push_str(" time get port timeCaller\n");
s.push_str(" event port logOut\n");
s.push_str(" telemetry port tlmOut\n\n");
s.push_str(" }\n}\n\n");
s.push_str("// Implementation: link with rover_flight.rs compiled as staticlib.\n");
s.push_str("// RoverMonitor::new() is const, lives in BSS, zero init cost.\n");
s.push_str("// RAM: ~6KB for 10 channels. No heap. Bounded worst-case per tick.\n");
s.push_str("// Calibration constants baked at build time or loaded from EEPROM.\n");
s.push_str("// https://github.com/koscak-labs/struktura\n");
s
}
pub fn generate_cfs_app(name: &str, window_size: usize) -> String {
let mut s = String::with_capacity(2048);
s.push_str(&format!("/* {}_app.c -- Generated cFS DFA health monitor app\n", name.to_lowercase()));
s.push_str(" * Generated by: struktura codegen --cfs\n");
s.push_str(" * Link with libstruktura.a or embed dfa_monitor.c\n */\n\n");
s.push_str("#include \"cfe.h\"\n");
s.push_str("#include \"struktura.h\"\n\n");
s.push_str(&format!("#define {}_WINDOW_SIZE {}\n\n", name.to_uppercase(), window_size));
s.push_str("typedef struct {\n");
s.push_str(&format!(" double buffer[{}_WINDOW_SIZE];\n", name.to_uppercase()));
s.push_str(" uint32 pos;\n");
s.push_str(" uint32 filled;\n");
s.push_str(" double baseline;\n");
s.push_str(" uint8 baseline_set;\n");
s.push_str(&format!("}} {}_Data_t;\n\n", name));
s.push_str(&format!("static {}_Data_t {}_Data;\n\n", name, name));
s.push_str(&format!("void {}_Init(void) {{\n", name));
s.push_str(&format!(" memset(&{}_Data, 0, sizeof({}_Data));\n", name, name));
s.push_str(" CFE_EVS_SendEvent(1, CFE_EVS_EventType_INFORMATION,\n");
s.push_str(&format!(" \"{} DFA health monitor initialized (window={})\");\n", name, window_size));
s.push_str("}\n\n");
s.push_str(&format!("void {}_ProcessSample(double value) {{\n", name));
s.push_str(&format!(" {}_Data.buffer[{}_Data.pos] = value;\n", name, name));
s.push_str(&format!(" {}_Data.pos = ({}_Data.pos + 1) % {}_WINDOW_SIZE;\n", name, name, name.to_uppercase()));
s.push_str(&format!(" if ({}_Data.pos == 0) {}_Data.filled = 1;\n", name, name));
s.push_str(&format!(" if (!{}_Data.filled) return;\n\n", name));
s.push_str(&format!(" struktura_dfa_result_t r = struktura_dfa({}_Data.buffer, {}_WINDOW_SIZE);\n", name, name.to_uppercase()));
s.push_str(&format!(" if (!{}_Data.baseline_set && r.r_squared > 0.7) {{\n", name));
s.push_str(&format!(" {}_Data.baseline = r.alpha;\n", name));
s.push_str(&format!(" {}_Data.baseline_set = 1;\n", name));
s.push_str(" CFE_EVS_SendEvent(2, CFE_EVS_EventType_INFORMATION,\n");
s.push_str(" \"DFA baseline established: alpha=%.3f R2=%.4f\", r.alpha, r.r_squared);\n");
s.push_str(" return;\n }\n");
s.push_str(&format!(" if (!{}_Data.baseline_set) return;\n\n", name));
s.push_str(&format!(" uint8_t verdict = struktura_health_check(r.alpha, {}_Data.baseline);\n", name));
s.push_str(" if (verdict >= STRUKTURA_WARNING) {\n");
s.push_str(" CFE_EVS_SendEvent(3, CFE_EVS_EventType_ERROR,\n");
s.push_str(&format!(" \"{} structural shift: alpha=%.3f baseline=%.3f verdict=%d\",\n", name));
s.push_str(&format!(" r.alpha, {}_Data.baseline, verdict);\n", name));
s.push_str(" }\n}\n\n");
s.push_str("// See: https://github.com/koscak-labs/struktura\n");
s
}
pub fn generate_hybrid_c(export: &crate::monitor::MonitorExport) -> String {
let nch = export.channels.len();
let mut s = String::new();
s.push_str("/* hybrid_monitor.c -- generated by struktura, calibration baked in.\n");
s.push_str(" * Five-leg hybrid telemetry health monitor: residual (AR1),\n");
s.push_str(" * repeated-value, windowed DFA, rolling-mean level, residual CUSUM.\n");
s.push_str(" * All thresholds calibrated (Gumbel return levels).\n");
s.push_str(" * Static memory only. Bounded loops only. C99 + libm.\n");
s.push_str(" * Compile: gcc -std=c99 -Wall -Werror -O2 -o hybrid hybrid_monitor.c -lm\n");
s.push_str(" */\n#include <math.h>\n#include <string.h>\n\n");
s.push_str(&format!("#define HYB_CHANNELS {}\n", nch));
s.push_str("#define HYB_WINDOW 96\n#define HYB_ROLL 96\n");
s.push_str("#define HYB_DFA_STRIDE 2\n");
s.push_str(&format!("#define HYB_RES_THR {:.17e}\n", export.res_thr));
s.push_str(&format!("#define HYB_DFA_THR {:.17e}\n", export.dfa_thr));
s.push_str(&format!("#define HYB_CUSUM_THR {:.17e}\n", export.cusum_thr));
s.push_str("#define HYB_CUSUM_K 1.0\n#define HYB_REPEAT_MARGIN 4\n");
s.push_str("#define HYB_DFA_PERSIST 5\n#define HYB_ROLL_PERSIST 10\n\n");
s.push_str("typedef struct {\n double ar_a, ar_b, ar_sd;\n");
s.push_str(" double alpha_mean, alpha_sd;\n double mean, roll_thr;\n");
s.push_str(" int max_run;\n int repeat_enabled;\n} hyb_calib_t;\n\n");
s.push_str("static const hyb_calib_t HYB_CALIB[HYB_CHANNELS] = {\n");
for c in &export.channels {
s.push_str(&format!(
" {{ {:.17e}, {:.17e}, {:.17e}, {:.17e}, {:.17e}, {:.17e}, {:.17e}, {}, {} }},\n",
c.ar_a, c.ar_b, c.ar_sd, c.alpha_mean, c.alpha_sd, c.mean, c.roll_thr,
c.max_run, if c.repeat_enabled { 1 } else { 0 }
));
}
s.push_str("};\n\n");
s.push_str("typedef enum {\n HYB_OK = 0,\n HYB_ALARM_RESIDUAL,\n");
s.push_str(" HYB_ALARM_REPEATED,\n HYB_ALARM_DFA,\n");
s.push_str(" HYB_ALARM_LEVEL,\n HYB_ALARM_CUSUM\n} hyb_verdict_t;\n\n");
s.push_str("typedef struct {\n double ring[HYB_WINDOW];\n");
s.push_str(" double roll_ring[HYB_ROLL];\n double prev;\n");
s.push_str(" double cusum_pos, cusum_neg;\n unsigned long t;\n");
s.push_str(" unsigned long res_hit_prev;\n int run;\n");
s.push_str(" int dfa_streak;\n int roll_streak;\n} hyb_channel_t;\n\n");
s.push_str("typedef struct {\n hyb_channel_t ch[HYB_CHANNELS];\n");
s.push_str(" int alarmed;\n} hyb_monitor_t;\n\n");
s.push_str("static void hyb_init(hyb_monitor_t *m) {\n");
s.push_str(" int c;\n memset(m, 0, sizeof(*m));\n");
s.push_str(" for (c = 0; c < HYB_CHANNELS; c++) {\n");
s.push_str(" m->ch[c].run = 1;\n");
s.push_str(" m->ch[c].res_hit_prev = (unsigned long)-1;\n }\n}\n\n");
s.push_str("static double hyb_dfa_alpha(const double *v, int n) {\n");
s.push_str(" static const int BOXES[8] = {16, 17, 18, 19, 20, 21, 22, 23};\n");
s.push_str(" double mean = 0.0, cum = 0.0;\n double y[HYB_WINDOW];\n");
s.push_str(" double log_s[8], log_f[8];\n int i, b, pts = 0;\n");
s.push_str(" for (i = 0; i < n; i++) mean += v[i];\n mean /= (double)n;\n");
s.push_str(" for (i = 0; i < n; i++) { cum += v[i] - mean; y[i] = cum; }\n");
s.push_str(" for (b = 0; b < 8; b++) {\n int s = BOXES[b];\n");
s.push_str(" int num_segs = n / s;\n double k = (double)s;\n");
s.push_str(" double sx = k * (k - 1.0) / 2.0;\n");
s.push_str(" double sx2 = k * (k - 1.0) * (2.0 * k - 1.0) / 6.0;\n");
s.push_str(" double det = k * sx2 - sx * sx;\n");
s.push_str(" double f2 = 0.0, f;\n int seg;\n");
s.push_str(" if (num_segs == 0 || s > n / 4) continue;\n");
s.push_str(" for (seg = 0; seg < num_segs; seg++) {\n");
s.push_str(" int st = seg * s;\n");
s.push_str(" double sy = 0, sxy = 0, sy2 = 0, a0, a1, resid;\n");
s.push_str(" for (i = 0; i < s; i++) {\n");
s.push_str(" double yi = y[st + i];\n");
s.push_str(" sy += yi; sxy += (double)i * yi; sy2 += yi * yi;\n");
s.push_str(" }\n");
s.push_str(" a0 = (sx2 * sy - sx * sxy) / det;\n");
s.push_str(" a1 = (k * sxy - sx * sy) / det;\n");
s.push_str(" resid = sy2 - a0 * sy - a1 * sxy;\n");
s.push_str(" if (resid < 0.0) resid = 0.0;\n");
s.push_str(" f2 += resid / k;\n }\n");
s.push_str(" f = sqrt(f2 / (double)num_segs);\n");
s.push_str(" if (f > 0.0) { log_s[pts] = log((double)s); log_f[pts] = log(f); pts++; }\n");
s.push_str(" }\n if (pts < 3) return 0.5;\n {\n");
s.push_str(" double n_ = (double)pts, sxa = 0, sya = 0, sxya = 0, sx2a = 0;\n");
s.push_str(" for (i = 0; i < pts; i++) {\n");
s.push_str(" sxa += log_s[i]; sya += log_f[i];\n");
s.push_str(" sxya += log_s[i] * log_f[i]; sx2a += log_s[i] * log_s[i];\n");
s.push_str(" }\n");
s.push_str(" return (n_ * sxya - sxa * sya) / (n_ * sx2a - sxa * sxa);\n");
s.push_str(" }\n}\n\n");
s.push_str("/* Feed one sample for one channel. Returns HYB_OK or the first alarm. */\n");
s.push_str("static hyb_verdict_t hyb_push(hyb_monitor_t *m, int c, double v) {\n");
s.push_str(" hyb_channel_t *st;\n const hyb_calib_t *cc;\n");
s.push_str(" unsigned long t;\n double zs;\n");
s.push_str(" if (m->alarmed || c < 0 || c >= HYB_CHANNELS) return HYB_OK;\n");
s.push_str(" st = &m->ch[c];\n cc = &HYB_CALIB[c];\n t = st->t++;\n");
s.push_str(" if (t == 0) {\n st->prev = v;\n st->ring[0] = v;\n");
s.push_str(" st->roll_ring[0] = v;\n return HYB_OK;\n }\n");
s.push_str(" zs = (v - (cc->ar_a + cc->ar_b * st->prev)) / cc->ar_sd;\n");
s.push_str(" st->cusum_pos += zs - HYB_CUSUM_K;\n");
s.push_str(" if (st->cusum_pos < 0.0) st->cusum_pos = 0.0;\n");
s.push_str(" st->cusum_neg += -zs - HYB_CUSUM_K;\n");
s.push_str(" if (st->cusum_neg < 0.0) st->cusum_neg = 0.0;\n");
s.push_str(" if (v == st->prev) {\n st->run++;\n");
s.push_str(" if (cc->repeat_enabled && st->run >= cc->max_run + HYB_REPEAT_MARGIN) {\n");
s.push_str(" m->alarmed = 1; return HYB_ALARM_REPEATED;\n }\n");
s.push_str(" } else {\n st->run = 1;\n }\n");
s.push_str(" st->prev = v;\n");
s.push_str(" st->ring[t % HYB_WINDOW] = v;\n");
s.push_str(" st->roll_ring[t % HYB_ROLL] = v;\n");
s.push_str(" if (fabs(zs) > HYB_RES_THR) {\n");
s.push_str(" if (st->res_hit_prev != (unsigned long)-1 && t - st->res_hit_prev < 20) {\n");
s.push_str(" m->alarmed = 1; return HYB_ALARM_RESIDUAL;\n }\n");
s.push_str(" st->res_hit_prev = t;\n }\n");
s.push_str(" if (st->cusum_pos > HYB_CUSUM_THR || st->cusum_neg > HYB_CUSUM_THR) {\n");
s.push_str(" m->alarmed = 1; return HYB_ALARM_CUSUM;\n }\n");
s.push_str(" if (t >= HYB_WINDOW && t % HYB_DFA_STRIDE == 0) {\n");
s.push_str(" double lin[HYB_WINDOW];\n double a;\n int i;\n");
s.push_str(" unsigned long start = (t + 1) % HYB_WINDOW;\n");
s.push_str(" for (i = 0; i < HYB_WINDOW; i++)\n");
s.push_str(" lin[i] = st->ring[(start + (unsigned long)i) % HYB_WINDOW];\n");
s.push_str(" a = hyb_dfa_alpha(lin, HYB_WINDOW);\n");
s.push_str(" if (fabs(a - cc->alpha_mean) / cc->alpha_sd > HYB_DFA_THR) {\n");
s.push_str(" st->dfa_streak += HYB_DFA_STRIDE;\n");
s.push_str(" if (st->dfa_streak >= HYB_DFA_PERSIST) {\n");
s.push_str(" m->alarmed = 1; return HYB_ALARM_DFA;\n }\n");
s.push_str(" } else {\n st->dfa_streak = 0;\n }\n }\n");
s.push_str(" if (t >= HYB_ROLL) {\n double sum = 0.0;\n int i;\n");
s.push_str(" for (i = 0; i < HYB_ROLL; i++) sum += st->roll_ring[i];\n");
s.push_str(" if (fabs(sum / (double)HYB_ROLL - cc->mean) > cc->roll_thr) {\n");
s.push_str(" st->roll_streak++;\n");
s.push_str(" if (st->roll_streak >= HYB_ROLL_PERSIST) {\n");
s.push_str(" m->alarmed = 1; return HYB_ALARM_LEVEL;\n }\n");
s.push_str(" } else {\n st->roll_streak = 0;\n }\n }\n");
s.push_str(" return HYB_OK;\n}\n\n");
let test_ch = export
.channels
.iter()
.position(|c| c.repeat_enabled)
.unwrap_or(0);
s.push_str("#ifdef HYBRID_STANDALONE_TEST\n#include <stdio.h>\n");
s.push_str(&format!("#define HYB_TEST_CH {}\n", test_ch));
s.push_str("int main(void) {\n hyb_monitor_t m;\n int t, c;\n");
s.push_str(" hyb_verdict_t v = HYB_OK;\n hyb_init(&m);\n");
s.push_str(" /* Small deterministic wobble around each channel mean; stuck\n");
s.push_str(" * fault on a repeat-enabled channel from t=400 (value frozen). */\n");
s.push_str(" for (t = 0; t < 700 && v == HYB_OK; t++) {\n");
s.push_str(" for (c = 0; c < HYB_CHANNELS; c++) {\n");
s.push_str(" double x = HYB_CALIB[c].mean\n");
s.push_str(" + 0.5 * HYB_CALIB[c].ar_sd * sin(0.7 * (double)t + (double)c);\n");
s.push_str(" if (c == HYB_TEST_CH && t >= 400) x = HYB_CALIB[HYB_TEST_CH].mean;\n");
s.push_str(" v = hyb_push(&m, c, x);\n");
s.push_str(" if (v != HYB_OK) break;\n }\n }\n");
s.push_str(" if (v == HYB_ALARM_REPEATED && t >= 400) {\n");
s.push_str(" printf(\"SELFTEST PASS: stuck detected at t=%d (leg=repeated)\\n\", t);\n");
s.push_str(" return 0;\n }\n");
s.push_str(" printf(\"SELFTEST FAIL: verdict=%d t=%d\\n\", (int)v, t);\n");
s.push_str(" return 1;\n}\n#endif\n");
s
}