nx_maint_action.nx source
↩ module page · 191 lines · 7685 B
1// nx_maint_action.nx -- R5: diagnosis -> action. Turns a kernel verdict + the
2// R4 trend into an actionable plan: probable CAUSE, recommended ACTION
3// (monitor / fix-DIY / fix-pro / replace consumable|component|unit / shut down),
4// DIY-vs-pro split (feeds the two R6 fronts), and an urgency.
5//
6// Operator (2026-06-23): "from the hardware rung up each rung." This is R5 on
7// top of R3's unified severity + R4's trend -- it completes monitor->diagnose->ACT.
8//
9// Rules that matter:
10// - SAFETY OVERRIDE: any verdict that unifies to SAFETY (overload, ground-
11// fault, overheat, burst) -> SHUTDOWN_SAFETY + call a pro, regardless of
12// kernel. The R3 safety guarantee drives this.
13// - PREDICTIVE ESCALATION: a WORSENING trend turns a "monitor" into a "fix
14// now" and bumps urgency -- act before failure (the R4 trend earns its keep).
15//
16// (Starter maintenance playbook -- the cause/action mappings are real field
17// practice; in production they live in svc-config #11, refined per asset.)
18//
19// NEVER-BRICK (#26): emits a recommendation; takes no action, writes nothing.
20//
21// genealogy_id: project-maintenance-platform-sclass-2026-06-23 (R5 action)
22// license_tier: ORIGINAL
23
24import "nx_maint_unify.nx" // nx_maint_severity + NX_HEALTH_* + kernel verdict consts + NX_MK_*
25import "nx_maint_ledger.nx" // NX_TREND_*
26
27// ---- Recommended action (ascending intervention) -------------------
28const NX_ACT_NONE: i64 = 0
29const NX_ACT_MONITOR: i64 = 1
30const NX_ACT_FIX_DIY: i64 = 2
31const NX_ACT_FIX_PRO: i64 = 3
32const NX_ACT_REPLACE_CONSUMABLE: i64 = 4
33const NX_ACT_REPLACE_COMPONENT: i64 = 5
34const NX_ACT_REPLACE_UNIT: i64 = 6
35const NX_ACT_SHUTDOWN_SAFETY: i64 = 7
36const NX_ACT_N: i64 = 8
37
38// ---- Probable cause (the diagnosis) --------------------------------
39const NX_CAUSE_NONE: i64 = 0
40const NX_CAUSE_OVERSIZED: i64 = 1
41const NX_CAUSE_DIRTY_FILTER: i64 = 2
42const NX_CAUSE_CONTROL_CAL: i64 = 3
43const NX_CAUSE_IMBALANCE: i64 = 4
44const NX_CAUSE_BEARING: i64 = 5
45const NX_CAUSE_CONSUMABLE_SPENT: i64 = 6
46const NX_CAUSE_SEAL_LEAK: i64 = 7
47const NX_CAUSE_PUMP_OR_BIGLEAK: i64 = 8
48const NX_CAUSE_CIRCUIT_LOAD: i64 = 9
49const NX_CAUSE_VOLTAGE_SUPPLY: i64 = 10
50const NX_CAUSE_SAFETY_HAZARD: i64 = 11
51const NX_CAUSE_N: i64 = 12
52
53func nx_action_is_valid(a: i64) -> i64 {
54 if a < 0 { return 0 }
55 if a >= NX_ACT_N { return 0 }
56 return 1
57}
58
59// A DIYer can do: nothing-needed, monitor, a DIY fix, a consumable swap.
60// A pro is needed for: pro fix, component/unit replace, a safety shutdown.
61func nx_action_is_diy(a: i64) -> i64 {
62 if a == NX_ACT_NONE { return 1 }
63 if a == NX_ACT_MONITOR { return 1 }
64 if a == NX_ACT_FIX_DIY { return 1 }
65 if a == NX_ACT_REPLACE_CONSUMABLE { return 1 }
66 return 0
67}
68
69// ---- Per-kernel base action (non-safety verdicts) ------------------
70func nx_act_thermal(v: i64) -> i64 {
71 if v == NX_HVAC_SHORT_CYCLING { return NX_ACT_FIX_PRO }
72 if v == NX_HVAC_DEGRADED_CAPACITY { return NX_ACT_FIX_DIY }
73 if v == NX_HVAC_POOR_TRACKING { return NX_ACT_MONITOR }
74 return NX_ACT_NONE
75}
76func nx_act_rot(v: i64) -> i64 {
77 if v == NX_ROT_ROUGH_WARNING { return NX_ACT_MONITOR }
78 if v == NX_ROT_ROUGH_ALARM { return NX_ACT_FIX_PRO }
79 if v == NX_ROT_BEARING_WEAR { return NX_ACT_REPLACE_COMPONENT }
80 return NX_ACT_NONE
81}
82func nx_act_cons(v: i64) -> i64 {
83 if v == NX_CONS_DUE_SOON { return NX_ACT_MONITOR }
84 if v == NX_CONS_DUE { return NX_ACT_REPLACE_CONSUMABLE }
85 if v == NX_CONS_OVERDUE { return NX_ACT_REPLACE_CONSUMABLE }
86 return NX_ACT_NONE
87}
88func nx_act_fluid(v: i64) -> i64 {
89 if v == NX_FLUID_SEEPAGE { return NX_ACT_MONITOR }
90 if v == NX_FLUID_LEAK { return NX_ACT_FIX_DIY }
91 if v == NX_FLUID_NO_PRESSURE { return NX_ACT_FIX_PRO }
92 return NX_ACT_NONE
93}
94func nx_act_elec(v: i64) -> i64 {
95 if v == NX_ELEC_HIGH_LOAD { return NX_ACT_FIX_DIY }
96 if v == NX_ELEC_VOLTAGE_SAG { return NX_ACT_FIX_PRO }
97 if v == NX_ELEC_VOLTAGE_SWELL { return NX_ACT_FIX_PRO }
98 return NX_ACT_NONE
99}
100
101// ---- Per-kernel probable cause -------------------------------------
102func nx_cause_thermal(v: i64) -> i64 {
103 if v == NX_HVAC_SHORT_CYCLING { return NX_CAUSE_OVERSIZED }
104 if v == NX_HVAC_DEGRADED_CAPACITY { return NX_CAUSE_DIRTY_FILTER }
105 if v == NX_HVAC_POOR_TRACKING { return NX_CAUSE_CONTROL_CAL }
106 return NX_CAUSE_NONE
107}
108func nx_cause_rot(v: i64) -> i64 {
109 if v == NX_ROT_ROUGH_WARNING { return NX_CAUSE_IMBALANCE }
110 if v == NX_ROT_ROUGH_ALARM { return NX_CAUSE_IMBALANCE }
111 if v == NX_ROT_BEARING_WEAR { return NX_CAUSE_BEARING }
112 return NX_CAUSE_NONE
113}
114func nx_cause_cons(v: i64) -> i64 {
115 if v == NX_CONS_DUE_SOON { return NX_CAUSE_CONSUMABLE_SPENT }
116 if v == NX_CONS_DUE { return NX_CAUSE_CONSUMABLE_SPENT }
117 if v == NX_CONS_OVERDUE { return NX_CAUSE_CONSUMABLE_SPENT }
118 return NX_CAUSE_NONE
119}
120func nx_cause_fluid(v: i64) -> i64 {
121 if v == NX_FLUID_SEEPAGE { return NX_CAUSE_SEAL_LEAK }
122 if v == NX_FLUID_LEAK { return NX_CAUSE_SEAL_LEAK }
123 if v == NX_FLUID_NO_PRESSURE { return NX_CAUSE_PUMP_OR_BIGLEAK }
124 return NX_CAUSE_NONE
125}
126func nx_cause_elec(v: i64) -> i64 {
127 if v == NX_ELEC_HIGH_LOAD { return NX_CAUSE_CIRCUIT_LOAD }
128 if v == NX_ELEC_VOLTAGE_SAG { return NX_CAUSE_VOLTAGE_SUPPLY }
129 if v == NX_ELEC_VOLTAGE_SWELL { return NX_CAUSE_VOLTAGE_SUPPLY }
130 return NX_CAUSE_NONE
131}
132
133func nx_action_for(kernel: i64, verdict: i64) -> i64 {
134 if kernel == NX_MK_THERMAL { return nx_act_thermal(verdict) }
135 if kernel == NX_MK_ROTATING { return nx_act_rot(verdict) }
136 if kernel == NX_MK_CONSUMABLE { return nx_act_cons(verdict) }
137 if kernel == NX_MK_FLUID { return nx_act_fluid(verdict) }
138 if kernel == NX_MK_ELECTRICAL { return nx_act_elec(verdict) }
139 return NX_ACT_NONE
140}
141func nx_cause_for(kernel: i64, verdict: i64) -> i64 {
142 if kernel == NX_MK_THERMAL { return nx_cause_thermal(verdict) }
143 if kernel == NX_MK_ROTATING { return nx_cause_rot(verdict) }
144 if kernel == NX_MK_CONSUMABLE { return nx_cause_cons(verdict) }
145 if kernel == NX_MK_FLUID { return nx_cause_fluid(verdict) }
146 if kernel == NX_MK_ELECTRICAL { return nx_cause_elec(verdict) }
147 return NX_CAUSE_NONE
148}
149
150// ---- The actionable plan -------------------------------------------
151struct ActionPlan {
152 action: i64, // NX_ACT_*
153 cause: i64, // NX_CAUSE_*
154 diy: i64, // 1 = DIYer can do it, 0 = needs a pro
155 urgency: i64, // NX_HEALTH_* severity, escalated one step if WORSENING
156}
157
158func nx_plan_for(kernel: i64, verdict: i64, trend: i64, out: *ActionPlan) -> i64 {
159 let sev: i64 = nx_maint_severity(kernel, verdict)
160
161 // urgency = severity, escalated one step on a worsening trend (real concerns only)
162 var urg: i64 = sev
163 if trend == NX_TREND_WORSENING {
164 if sev >= NX_HEALTH_WATCH {
165 if sev < NX_HEALTH_SAFETY {
166 urg = sev + 1
167 }
168 }
169 }
170 out.urgency = urg
171
172 // SAFETY override: any SAFETY verdict -> shut it down, get a pro.
173 if sev == NX_HEALTH_SAFETY {
174 out.action = NX_ACT_SHUTDOWN_SAFETY
175 out.cause = NX_CAUSE_SAFETY_HAZARD
176 out.diy = 0
177 return out.action
178 }
179
180 var act: i64 = nx_action_for(kernel, verdict)
181 // PREDICTIVE ESCALATION: don't just watch a worsening asset -- fix it.
182 if act == NX_ACT_MONITOR {
183 if trend == NX_TREND_WORSENING {
184 act = NX_ACT_FIX_DIY
185 }
186 }
187 out.action = act
188 out.cause = nx_cause_for(kernel, verdict)
189 out.diy = nx_action_is_diy(act)
190 return out.action
191}