nx_ac_monitor.nx source
↩ module page · 148 lines · 6003 B
1// nx_ac_monitor.nx -- the PRODUCT BRAIN: one call takes a raw sensor reading
2// and produces the full trustworthy monitoring assessment, with the LIAR-KILLER
3// GATING the diagnosis.
4//
5// THE THESIS IN CODE (operator: "monitoring as a liar killer"): the reading is
6// first stamped by nx_ac_sensor_verify. ONLY a PLAUSIBLE or CORROBORATED
7// reading is allowed to drive a fault verdict or an alert. An impossible or
8// self-contradicting reading returns alert = UNTRUSTED and emits NO fault --
9// the monitor structurally CANNOT raise a false alarm from a lying sensor, nor
10// silently hide a real one behind a bad reading.
11//
12// Pipeline: reading -> [TRUST gate] -> derive superheat/subcool/split/... ->
13// FDD verdict (nx_ac_fdd) + efficiency drift vs baseline COP ->
14// single sealed alert level (OK / INFO / WARN / CRITICAL / UNTRUSTED)
15//
16// PURE (no syscalls): the caller owns all scratch structs (allocate once, reuse
17// across readings -> daemon-safe, no per-reading allocation). Deterministic ->
18// hard-gateable. NEVER-BRICK #26: read-only advisory, no device/firmware write.
19//
20// genealogy_id: project-hvac-efficiency-sclass-2026-06-23 (R5 unified monitor)
21// + project-nishi-verification-stack-sota-2026-07-14 (trust-gates-output)
22// license_tier: ORIGINAL
23//
24// nx_capability_claims:
25// needs: [nx_ac_sensor_verify, nx_ac_fdd]
26// provides: [unified_ac_monitor, liar_killer_gates_diagnosis,
27// trustworthy_alerting, efficiency_drift_alert, sealed_alert_level]
28// safety: [no_floating_point, no_syscall, bounded_iteration,
29// read_only_no_device_write, sealed_enum_verdict,
30// untrusted_reading_emits_no_fault]
31// verdict: [sealed_enum_alert, no_silent_failure, trust_gates_output]
32// license: ORIGINAL
33// kind: iot_runtime_primitive
34// sss: [S0 (bit-equal), S6 (no cloud), S7 (sealed verdict)]
35
36import "nx_ac_sensor_verify.nx"
37import "nx_ac_fdd.nx"
38
39// ---- sealed-enum alert level ---------------------------------------
40const NX_ALERT_OK: i64 = 0
41const NX_ALERT_INFO: i64 = 1
42const NX_ALERT_WARN: i64 = 2
43const NX_ALERT_CRITICAL: i64 = 3
44const NX_ALERT_UNTRUSTED: i64 = 4
45const NX_ALERT_N: i64 = 5
46
47func nx_alert_is_valid(v: i64) -> i64 {
48 if v < 0 { return 0 }
49 if v >= NX_ALERT_N { return 0 }
50 return 1
51}
52
53// ---- unified monitoring result -------------------------------------
54struct MonitorResult {
55 trust_verdict: i64, // NX_ACV_*
56 trust_reason: i64,
57 corroborated: i64,
58 q_cool_w: i64,
59 cop_x100: i64,
60 efficiency_drift_pm: i64, // vs baseline (0 if not computable)
61 fdd_verdict: i64, // NX_FDD_* (INSUFFICIENT if reading untrusted)
62 alert_level: i64, // NX_ALERT_*
63}
64
65// caller-owned scratch: av (AcVerify), fin (FddInput), fth (FddThresh config),
66// fout (FddOut). Allocate once; reuse per reading.
67func nx_ac_monitor(rd: *AcReading, av: *AcVerify, fin: *FddInput, fth: *FddThresh,
68 fout: *FddOut, baseline_cop_x100: i64, warn_drift_pm: i64,
69 info_drift_pm: i64, out: *MonitorResult) -> i64 {
70 // ---- 1. trust the reading (the liar-killer) ------------------
71 nx_acv_verify(rd, av)
72 out.trust_verdict = av.verdict
73 out.trust_reason = av.reason
74 out.corroborated = av.corroborated
75 out.q_cool_w = av.q_cool_w
76 out.cop_x100 = av.cop_x100
77 out.efficiency_drift_pm = 0
78 out.fdd_verdict = NX_FDD_INSUFFICIENT_DATA
79 out.alert_level = NX_ALERT_UNTRUSTED
80
81 var trusted: i64 = 0
82 if av.verdict == NX_ACV_PLAUSIBLE { trusted = 1 }
83 if av.verdict == NX_ACV_CORROBORATED { trusted = 1 }
84 if trusted == 0 {
85 // untrusted reading -> refuse to diagnose (no fault, no alert-from-lie)
86 return out.alert_level
87 }
88
89 // ---- 2. derive FDD inputs from the trusted reading -----------
90 fin.superheat_mC = NX_FDD_ABSENT
91 if rd.t_suction_mC != NX_ACV_ABSENT {
92 if rd.t_sat_evap_mC != NX_ACV_ABSENT {
93 fin.superheat_mC = rd.t_suction_mC - rd.t_sat_evap_mC
94 }
95 }
96 fin.subcool_mC = NX_FDD_ABSENT
97 if rd.t_sat_cond_mC != NX_ACV_ABSENT {
98 if rd.t_liquid_mC != NX_ACV_ABSENT {
99 fin.subcool_mC = rd.t_sat_cond_mC - rd.t_liquid_mC
100 }
101 }
102 fin.suction_sat_mC = NX_FDD_ABSENT
103 if rd.t_sat_evap_mC != NX_ACV_ABSENT {
104 fin.suction_sat_mC = rd.t_sat_evap_mC
105 }
106 fin.evap_split_mC = rd.t_return_mC - rd.t_supply_mC
107 fin.cond_over_ambient_mC = NX_FDD_ABSENT
108 if rd.t_sat_cond_mC != NX_ACV_ABSENT {
109 if rd.t_outdoor_mC != NX_ACV_ABSENT {
110 fin.cond_over_ambient_mC = rd.t_sat_cond_mC - rd.t_outdoor_mC
111 }
112 }
113 fin.cfm_per_ton = NX_FDD_ABSENT // needs capacity spec (per-unit config)
114 fin.cph_x10 = NX_FDD_ABSENT // needs time series (nx_hvac_efficiency)
115
116 // ---- 3. FDD verdict ------------------------------------------
117 nx_ac_fdd_analyze(fin, fth, fout)
118 out.fdd_verdict = fout.verdict
119
120 // ---- 4. efficiency drift vs baseline COP ---------------------
121 var drift: i64 = 0
122 if av.cop_x100 != NX_ACM_INFEASIBLE {
123 if baseline_cop_x100 > 0 {
124 drift = nx_ac_efficiency_drift_permille(av.cop_x100, baseline_cop_x100)
125 }
126 }
127 out.efficiency_drift_pm = drift
128
129 // ---- 5. single alert level -----------------------------------
130 // fault severity first, then efficiency drift.
131 var lvl: i64 = NX_ALERT_OK
132 if fout.verdict == NX_FDD_FROZEN_COIL { lvl = NX_ALERT_CRITICAL }
133 if lvl == NX_ALERT_OK {
134 if fout.verdict != NX_FDD_NOMINAL {
135 if fout.verdict != NX_FDD_INSUFFICIENT_DATA {
136 lvl = NX_ALERT_WARN
137 }
138 }
139 }
140 if lvl == NX_ALERT_OK {
141 if drift >= warn_drift_pm { lvl = NX_ALERT_WARN }
142 }
143 if lvl == NX_ALERT_OK {
144 if drift >= info_drift_pm { lvl = NX_ALERT_INFO }
145 }
146 out.alert_level = lvl
147 return lvl
148}