@celestia-island/plana-types 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +38 -0
- package/bindings/FileAnchor.ts +3 -0
- package/bindings/engine.ts +277 -0
- package/bindings/enums.ts +31 -0
- package/bindings/httpTypes.ts +197 -0
- package/bindings/index.ts +47 -0
- package/bindings/mcp/aporia.ts +61 -0
- package/bindings/mcp/eleos.ts +18 -0
- package/bindings/mcp/epieikeia.ts +48 -0
- package/bindings/mcp/haplotes.ts +47 -0
- package/bindings/mcp/hubris.ts +41 -0
- package/bindings/mcp/index.ts +13 -0
- package/bindings/mcp/kalos.ts +47 -0
- package/bindings/mcp/neikos.ts +76 -0
- package/bindings/mcp/orexis.ts +64 -0
- package/bindings/mcp/philia.ts +57 -0
- package/bindings/mcp/polemos.ts +55 -0
- package/bindings/mcp/skemma.ts +58 -0
- package/bindings/mcp/skopeo.ts +56 -0
- package/bindings/mcp/webAutomation.ts +31 -0
- package/bindings/model.ts +240 -0
- package/bindings/package.json +16 -0
- package/bindings/region.ts +3 -0
- package/bindings/serde_json/JsonValue.ts +3 -0
- package/bindings/ws/agentLifecycle.ts +41 -0
- package/bindings/ws/auth.ts +15 -0
- package/bindings/ws/baseMessages.ts +7 -0
- package/bindings/ws/bridgeNetwork.ts +71 -0
- package/bindings/ws/core.ts +51 -0
- package/bindings/ws/fileBrowsing.ts +57 -0
- package/bindings/ws/handshake.ts +23 -0
- package/bindings/ws/industrial.ts +72 -0
- package/bindings/ws/knowledgeBase.ts +10 -0
- package/bindings/ws/layer2.ts +25 -0
- package/bindings/ws/llmProvider.ts +74 -0
- package/bindings/ws/logs.ts +11 -0
- package/bindings/ws/malkuth.ts +62 -0
- package/bindings/ws/noa.ts +17 -0
- package/bindings/ws/stateSync.ts +19 -0
- package/bindings/ws/systemUi.ts +5 -0
- package/bindings/ws/tasks.ts +6 -0
- package/bindings/ws/views.ts +163 -0
- package/bindings/ws/workspace.ts +11 -0
- package/bindings/ws/yolo.ts +32 -0
- package/examples/schema_dump.rs +51 -0
- package/package.json +6 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/engine.rs +602 -0
- package/src/enums.rs +334 -0
- package/src/external_mcp.rs +132 -0
- package/src/http.rs +1077 -0
- package/src/identity.rs +160 -0
- package/src/lib.rs +1215 -0
- package/src/malkuth.rs +145 -0
- package/src/mcp/aporia.rs +272 -0
- package/src/mcp/eleos.rs +210 -0
- package/src/mcp/epieikeia.rs +194 -0
- package/src/mcp/haplotes.rs +310 -0
- package/src/mcp/hubris.rs +377 -0
- package/src/mcp/kalos.rs +251 -0
- package/src/mcp/mod.rs +23 -0
- package/src/mcp/neikos.rs +533 -0
- package/src/mcp/orexis.rs +493 -0
- package/src/mcp/philia.rs +267 -0
- package/src/mcp/polemos.rs +241 -0
- package/src/mcp/skemma.rs +442 -0
- package/src/mcp/skopeo.rs +282 -0
- package/src/mcp/web_automation.rs +122 -0
- package/src/model.rs +421 -0
- package/src/protocol/base_messages.rs +125 -0
- package/src/protocol/handshake.rs +364 -0
- package/src/protocol/jsonrpc.rs +888 -0
- package/src/protocol/mod.rs +10 -0
- package/src/rbac.rs +786 -0
- package/src/region.rs +362 -0
- package/src/tracing_helpers.rs +9 -0
- package/src/ws/agent/agent_lifecycle.rs +221 -0
- package/src/ws/agent/layer2.rs +126 -0
- package/src/ws/agent/mod.rs +9 -0
- package/src/ws/agent/state_sync.rs +111 -0
- package/src/ws/agent/tasks.rs +43 -0
- package/src/ws/agent/yolo.rs +161 -0
- package/src/ws/mod.rs +10 -0
- package/src/ws/services/auth.rs +99 -0
- package/src/ws/services/industrial.rs +647 -0
- package/src/ws/services/knowledge_base.rs +59 -0
- package/src/ws/services/llm_provider.rs +371 -0
- package/src/ws/services/mod.rs +7 -0
- package/src/ws/ui/bridge_network.rs +96 -0
- package/src/ws/ui/file_browsing.rs +88 -0
- package/src/ws/ui/logs.rs +55 -0
- package/src/ws/ui/mod.rs +11 -0
- package/src/ws/ui/noa.rs +105 -0
- package/src/ws/ui/system_ui.rs +27 -0
- package/src/ws/ui/views.rs +159 -0
- package/src/ws/ui/workspace.rs +73 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
//! Industrial Control — telemetry, alarms, discovery, write approval, topology.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors `entelecheia/packages/shared/state_types/src/gateway/tui_types/
|
|
4
|
+
//! message/types/mod.rs` (the canonical source of truth) 1:1. Field naming,
|
|
5
|
+
//! serde rename rules, and the `Industrial`-prefixed names all match entelecheia
|
|
6
|
+
//! so both sides of the WebSocket stay in sync without remapping.
|
|
7
|
+
|
|
8
|
+
use serde::{Deserialize, Serialize};
|
|
9
|
+
use ts_rs::TS;
|
|
10
|
+
|
|
11
|
+
// ── Value types (canonical, from entelecheia) ──────────────
|
|
12
|
+
|
|
13
|
+
/// Severity ordering matches ISA-18.2 alarm severity.
|
|
14
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
15
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
16
|
+
#[serde(rename_all = "PascalCase")]
|
|
17
|
+
pub enum IndustrialAlarmLevel {
|
|
18
|
+
Log,
|
|
19
|
+
LowLow,
|
|
20
|
+
Low,
|
|
21
|
+
High,
|
|
22
|
+
HighHigh,
|
|
23
|
+
RateOfChange,
|
|
24
|
+
Emergency,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// A single live reading from an industrial field (e.g. pressure cell on a
|
|
28
|
+
/// Modbus register, S7 DBX bit). Pushed by scepter at the scan cycle of the
|
|
29
|
+
/// underlying transport.
|
|
30
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
31
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
32
|
+
pub struct IndustrialSensorReading {
|
|
33
|
+
pub station_id: String,
|
|
34
|
+
pub protocol: String,
|
|
35
|
+
pub address: String,
|
|
36
|
+
pub name: String,
|
|
37
|
+
pub raw_value: f64,
|
|
38
|
+
pub scaled_value: f64,
|
|
39
|
+
pub unit: String,
|
|
40
|
+
pub quality: String,
|
|
41
|
+
pub timestamp: String,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Fired on threshold breach (breached=true) or clear (breached=false).
|
|
45
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
46
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
47
|
+
pub struct IndustrialAlarmEvent {
|
|
48
|
+
pub station_id: String,
|
|
49
|
+
pub protocol: String,
|
|
50
|
+
pub address: String,
|
|
51
|
+
pub field_name: String,
|
|
52
|
+
pub level: IndustrialAlarmLevel,
|
|
53
|
+
pub value: f64,
|
|
54
|
+
pub threshold: f64,
|
|
55
|
+
pub unit: String,
|
|
56
|
+
pub breached: bool,
|
|
57
|
+
pub timestamp: String,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Phases of an evernight discovery scan. Ordered by typical progress.
|
|
61
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
62
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
63
|
+
#[serde(rename_all = "PascalCase")]
|
|
64
|
+
pub enum IndustrialDiscoveryPhase {
|
|
65
|
+
TransportScan,
|
|
66
|
+
ProtocolIdentify,
|
|
67
|
+
DataModelScan,
|
|
68
|
+
SemanticInference,
|
|
69
|
+
ManifestGeneration,
|
|
70
|
+
ManifestValidation,
|
|
71
|
+
Complete,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
75
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
76
|
+
pub struct IndustrialDiscoveryProgress {
|
|
77
|
+
pub session_id: String,
|
|
78
|
+
pub phase: IndustrialDiscoveryPhase,
|
|
79
|
+
pub message: String,
|
|
80
|
+
pub found_devices: u64,
|
|
81
|
+
pub progress_percent: u32,
|
|
82
|
+
#[serde(default)]
|
|
83
|
+
#[ts(optional, type = "Record<string, unknown> | null")]
|
|
84
|
+
pub raw_findings: Option<serde_json::Value>,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Operator confirmation gate for safety-critical PLC writes.
|
|
88
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
|
89
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
90
|
+
#[serde(rename_all = "lowercase")]
|
|
91
|
+
pub enum WriteApprovalRisk {
|
|
92
|
+
Safe,
|
|
93
|
+
Caution,
|
|
94
|
+
Critical,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
98
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
99
|
+
pub struct WriteApprovalRequest {
|
|
100
|
+
/// Unique id assigned by the producer (orexis). The operator UI echoes it
|
|
101
|
+
/// back in `industrial.approveWrite` so scepter's resolver can match the
|
|
102
|
+
/// response to the pending oneshot. `#[serde(default)]` keeps the wire
|
|
103
|
+
/// format backward-compatible with older push events that predate it.
|
|
104
|
+
#[serde(default)]
|
|
105
|
+
pub request_id: String,
|
|
106
|
+
pub station_id: String,
|
|
107
|
+
pub protocol: String,
|
|
108
|
+
pub address: String,
|
|
109
|
+
pub field_name: String,
|
|
110
|
+
pub current_value: f64,
|
|
111
|
+
pub proposed_value: f64,
|
|
112
|
+
pub unit: String,
|
|
113
|
+
pub reason: String,
|
|
114
|
+
pub agent: String,
|
|
115
|
+
pub risk_level: WriteApprovalRisk,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
119
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
120
|
+
pub struct IndustrialStationField {
|
|
121
|
+
pub address: String,
|
|
122
|
+
pub name: String,
|
|
123
|
+
pub data_type: String,
|
|
124
|
+
#[serde(default)]
|
|
125
|
+
#[ts(optional)]
|
|
126
|
+
pub unit: Option<String>,
|
|
127
|
+
#[serde(default)]
|
|
128
|
+
#[ts(optional)]
|
|
129
|
+
pub alarm: Option<IndustrialAlarmThresholds>,
|
|
130
|
+
#[serde(default)]
|
|
131
|
+
#[ts(optional)]
|
|
132
|
+
pub current_value: Option<f64>,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
136
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
137
|
+
pub struct IndustrialAlarmThresholds {
|
|
138
|
+
#[serde(default)]
|
|
139
|
+
#[ts(optional)]
|
|
140
|
+
pub ll: Option<f64>,
|
|
141
|
+
#[serde(default)]
|
|
142
|
+
#[ts(optional)]
|
|
143
|
+
pub l: Option<f64>,
|
|
144
|
+
#[serde(default)]
|
|
145
|
+
#[ts(optional)]
|
|
146
|
+
pub h: Option<f64>,
|
|
147
|
+
#[serde(default)]
|
|
148
|
+
#[ts(optional)]
|
|
149
|
+
pub hh: Option<f64>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
153
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
154
|
+
pub struct IndustrialStationInfo {
|
|
155
|
+
pub station_id: String,
|
|
156
|
+
pub protocol: String,
|
|
157
|
+
pub connection: String,
|
|
158
|
+
pub device_class: String,
|
|
159
|
+
#[serde(default)]
|
|
160
|
+
#[ts(optional)]
|
|
161
|
+
pub vendor: Option<String>,
|
|
162
|
+
#[serde(default)]
|
|
163
|
+
#[ts(optional)]
|
|
164
|
+
pub model: Option<String>,
|
|
165
|
+
#[serde(default)]
|
|
166
|
+
#[ts(optional)]
|
|
167
|
+
pub firmware: Option<String>,
|
|
168
|
+
pub status: String,
|
|
169
|
+
#[serde(default)]
|
|
170
|
+
pub fields: Vec<IndustrialStationField>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// One entry in the historical alarm log (last N days).
|
|
174
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
175
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
176
|
+
pub struct IndustrialAlarmHistoryEntry {
|
|
177
|
+
pub station_id: String,
|
|
178
|
+
pub protocol: String,
|
|
179
|
+
pub address: String,
|
|
180
|
+
pub field_name: String,
|
|
181
|
+
pub level: IndustrialAlarmLevel,
|
|
182
|
+
pub value: f64,
|
|
183
|
+
pub threshold: f64,
|
|
184
|
+
pub unit: String,
|
|
185
|
+
pub breached: bool,
|
|
186
|
+
pub timestamp: String,
|
|
187
|
+
/// Whether an operator acknowledged the alarm, and when.
|
|
188
|
+
#[serde(default)]
|
|
189
|
+
pub acknowledged: bool,
|
|
190
|
+
#[serde(default)]
|
|
191
|
+
#[ts(optional)]
|
|
192
|
+
pub acknowledged_at: Option<String>,
|
|
193
|
+
#[serde(default)]
|
|
194
|
+
#[ts(optional)]
|
|
195
|
+
pub acknowledged_by: Option<String>,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
199
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
200
|
+
pub struct IndustrialAlarmHistory {
|
|
201
|
+
pub entries: Vec<IndustrialAlarmHistoryEntry>,
|
|
202
|
+
pub total: u64,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── WS push / RPC param wrappers ───────────────────────────
|
|
206
|
+
//
|
|
207
|
+
// These wrap the canonical value types above as the `params` payload of the
|
|
208
|
+
// `Industrial*` / `topology.*` SyncMessage variants. They are arona-specific
|
|
209
|
+
// (entelecheia dispatches them inline on the SyncMessage enum) but are vendored
|
|
210
|
+
// by shittim-chest's webui, so they are retained here.
|
|
211
|
+
|
|
212
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
213
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
214
|
+
pub struct IndustrialTelemetryBatch {
|
|
215
|
+
pub readings: Vec<IndustrialSensorReading>,
|
|
216
|
+
pub station_id: String,
|
|
217
|
+
pub timestamp: String,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
221
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
222
|
+
pub struct IndustrialTelemetryPushParams {
|
|
223
|
+
pub batch: IndustrialTelemetryBatch,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
227
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
228
|
+
pub struct IndustrialAlarmPushParams {
|
|
229
|
+
pub alarm: IndustrialAlarmEvent,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
233
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
234
|
+
pub struct IndustrialAlarmAckParams {
|
|
235
|
+
pub station_id: String,
|
|
236
|
+
pub address: String,
|
|
237
|
+
pub acknowledged_by: String,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
241
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
242
|
+
pub struct IndustrialDiscoveryProgressPushParams {
|
|
243
|
+
pub event: IndustrialDiscoveryProgress,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
247
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
248
|
+
pub struct WriteApprovalRequestParams {
|
|
249
|
+
pub request: WriteApprovalRequest,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
253
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
254
|
+
pub struct WriteApprovalResponseParams {
|
|
255
|
+
pub request_id: String,
|
|
256
|
+
pub approved: bool,
|
|
257
|
+
pub approved_by: String,
|
|
258
|
+
#[serde(default)]
|
|
259
|
+
#[ts(optional)]
|
|
260
|
+
pub modified_value: Option<f64>,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
264
|
+
#[ts(export, export_to = "ws/industrial.ts")]
|
|
265
|
+
pub struct IndustrialTopologyParams {
|
|
266
|
+
pub stations: Vec<IndustrialStationInfo>,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[cfg(test)]
|
|
270
|
+
mod tests {
|
|
271
|
+
use super::*;
|
|
272
|
+
use serde_json::json;
|
|
273
|
+
|
|
274
|
+
// ── IndustrialAlarmLevel (PascalCase serde) ───────────────────
|
|
275
|
+
|
|
276
|
+
#[test]
|
|
277
|
+
fn alarm_level_pascal_case_round_trip() {
|
|
278
|
+
for level in [
|
|
279
|
+
IndustrialAlarmLevel::Log,
|
|
280
|
+
IndustrialAlarmLevel::LowLow,
|
|
281
|
+
IndustrialAlarmLevel::Low,
|
|
282
|
+
IndustrialAlarmLevel::High,
|
|
283
|
+
IndustrialAlarmLevel::HighHigh,
|
|
284
|
+
IndustrialAlarmLevel::RateOfChange,
|
|
285
|
+
IndustrialAlarmLevel::Emergency,
|
|
286
|
+
] {
|
|
287
|
+
let s = serde_json::to_string(&level).unwrap();
|
|
288
|
+
let back: IndustrialAlarmLevel = serde_json::from_str(&s).unwrap();
|
|
289
|
+
assert_eq!(back, level);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[test]
|
|
294
|
+
fn alarm_level_serializes_pascal_case() {
|
|
295
|
+
assert_eq!(
|
|
296
|
+
serde_json::to_string(&IndustrialAlarmLevel::HighHigh).unwrap(),
|
|
297
|
+
r#""HighHigh""#
|
|
298
|
+
);
|
|
299
|
+
assert_eq!(
|
|
300
|
+
serde_json::to_string(&IndustrialAlarmLevel::RateOfChange).unwrap(),
|
|
301
|
+
r#""RateOfChange""#
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── IndustrialSensorReading ───────────────────────────────────
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
fn sensor_reading_round_trip() {
|
|
309
|
+
let r = IndustrialSensorReading {
|
|
310
|
+
station_id: "PLC-01".into(),
|
|
311
|
+
protocol: "modbus-tcp".into(),
|
|
312
|
+
address: "40001".into(),
|
|
313
|
+
name: "reactor_pressure".into(),
|
|
314
|
+
raw_value: 16384.0,
|
|
315
|
+
scaled_value: 6.4,
|
|
316
|
+
unit: "bar".into(),
|
|
317
|
+
quality: "good".into(),
|
|
318
|
+
timestamp: "2026-07-07T12:00:00Z".into(),
|
|
319
|
+
};
|
|
320
|
+
let v = serde_json::to_value(&r).unwrap();
|
|
321
|
+
assert_eq!(v["station_id"], "PLC-01");
|
|
322
|
+
assert_eq!(v["scaled_value"], 6.4);
|
|
323
|
+
assert_eq!(v["unit"], "bar");
|
|
324
|
+
let back: IndustrialSensorReading = serde_json::from_value(v).unwrap();
|
|
325
|
+
assert_eq!(back.raw_value, 16384.0);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── IndustrialAlarmEvent ──────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
#[test]
|
|
331
|
+
fn alarm_event_breach_round_trip() {
|
|
332
|
+
let e = IndustrialAlarmEvent {
|
|
333
|
+
station_id: "PLC-01".into(),
|
|
334
|
+
protocol: "modbus-tcp".into(),
|
|
335
|
+
address: "40001".into(),
|
|
336
|
+
field_name: "reactor_pressure".into(),
|
|
337
|
+
level: IndustrialAlarmLevel::HighHigh,
|
|
338
|
+
value: 9.8,
|
|
339
|
+
threshold: 8.0,
|
|
340
|
+
unit: "bar".into(),
|
|
341
|
+
breached: true,
|
|
342
|
+
timestamp: "2026-07-07T12:00:00Z".into(),
|
|
343
|
+
};
|
|
344
|
+
let v = serde_json::to_value(&e).unwrap();
|
|
345
|
+
assert_eq!(v["level"], "HighHigh");
|
|
346
|
+
assert_eq!(v["breached"], true);
|
|
347
|
+
assert_eq!(v["value"], 9.8);
|
|
348
|
+
let back: IndustrialAlarmEvent = serde_json::from_value(v).unwrap();
|
|
349
|
+
assert_eq!(back.level, IndustrialAlarmLevel::HighHigh);
|
|
350
|
+
assert!(back.breached);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#[test]
|
|
354
|
+
fn alarm_event_clear_round_trip() {
|
|
355
|
+
let e = IndustrialAlarmEvent {
|
|
356
|
+
station_id: "PLC-01".into(),
|
|
357
|
+
protocol: "modbus-tcp".into(),
|
|
358
|
+
address: "40001".into(),
|
|
359
|
+
field_name: "pressure".into(),
|
|
360
|
+
level: IndustrialAlarmLevel::High,
|
|
361
|
+
value: 7.0,
|
|
362
|
+
threshold: 8.0,
|
|
363
|
+
unit: "bar".into(),
|
|
364
|
+
breached: false,
|
|
365
|
+
timestamp: "2026-07-07T12:01:00Z".into(),
|
|
366
|
+
};
|
|
367
|
+
let v = serde_json::to_value(&e).unwrap();
|
|
368
|
+
assert_eq!(v["breached"], false);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── IndustrialDiscoveryPhase ──────────────────────────────────
|
|
372
|
+
|
|
373
|
+
#[test]
|
|
374
|
+
fn discovery_phase_round_trip_all_variants() {
|
|
375
|
+
for phase in [
|
|
376
|
+
IndustrialDiscoveryPhase::TransportScan,
|
|
377
|
+
IndustrialDiscoveryPhase::ProtocolIdentify,
|
|
378
|
+
IndustrialDiscoveryPhase::DataModelScan,
|
|
379
|
+
IndustrialDiscoveryPhase::SemanticInference,
|
|
380
|
+
IndustrialDiscoveryPhase::ManifestGeneration,
|
|
381
|
+
IndustrialDiscoveryPhase::ManifestValidation,
|
|
382
|
+
IndustrialDiscoveryPhase::Complete,
|
|
383
|
+
] {
|
|
384
|
+
let s = serde_json::to_string(&phase).unwrap();
|
|
385
|
+
let back: IndustrialDiscoveryPhase = serde_json::from_str(&s).unwrap();
|
|
386
|
+
assert_eq!(back, phase);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#[test]
|
|
391
|
+
fn discovery_progress_round_trip() {
|
|
392
|
+
let p = IndustrialDiscoveryProgress {
|
|
393
|
+
session_id: "disc-001".into(),
|
|
394
|
+
phase: IndustrialDiscoveryPhase::ProtocolIdentify,
|
|
395
|
+
message: "Identifying Modbus devices".into(),
|
|
396
|
+
found_devices: 3,
|
|
397
|
+
progress_percent: 45,
|
|
398
|
+
raw_findings: Some(json!({"candidates": ["PLC-01", "PLC-02"]})),
|
|
399
|
+
};
|
|
400
|
+
let v = serde_json::to_value(&p).unwrap();
|
|
401
|
+
assert_eq!(v["phase"], "ProtocolIdentify");
|
|
402
|
+
assert_eq!(v["progress_percent"], 45);
|
|
403
|
+
assert_eq!(v["raw_findings"]["candidates"][0], "PLC-01");
|
|
404
|
+
let back: IndustrialDiscoveryProgress = serde_json::from_value(v).unwrap();
|
|
405
|
+
assert_eq!(back.found_devices, 3);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[test]
|
|
409
|
+
fn discovery_progress_without_raw_findings() {
|
|
410
|
+
let p = IndustrialDiscoveryProgress {
|
|
411
|
+
session_id: "s".into(),
|
|
412
|
+
phase: IndustrialDiscoveryPhase::Complete,
|
|
413
|
+
message: "done".into(),
|
|
414
|
+
found_devices: 0,
|
|
415
|
+
progress_percent: 100,
|
|
416
|
+
raw_findings: None,
|
|
417
|
+
};
|
|
418
|
+
let v = serde_json::to_value(&p).unwrap();
|
|
419
|
+
// #[ts(optional)] without skip → null on wire.
|
|
420
|
+
assert_eq!(v["raw_findings"], serde_json::Value::Null);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── WriteApprovalRisk (lowercase serde) ───────────────────────
|
|
424
|
+
|
|
425
|
+
#[test]
|
|
426
|
+
fn write_approval_risk_lowercase_round_trip() {
|
|
427
|
+
for risk in [
|
|
428
|
+
WriteApprovalRisk::Safe,
|
|
429
|
+
WriteApprovalRisk::Caution,
|
|
430
|
+
WriteApprovalRisk::Critical,
|
|
431
|
+
] {
|
|
432
|
+
let s = serde_json::to_string(&risk).unwrap();
|
|
433
|
+
let back: WriteApprovalRisk = serde_json::from_str(&s).unwrap();
|
|
434
|
+
assert_eq!(back, risk);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn write_approval_risk_serializes_lowercase() {
|
|
440
|
+
assert_eq!(
|
|
441
|
+
serde_json::to_string(&WriteApprovalRisk::Safe).unwrap(),
|
|
442
|
+
r#""safe""#
|
|
443
|
+
);
|
|
444
|
+
assert_eq!(
|
|
445
|
+
serde_json::to_string(&WriteApprovalRisk::Critical).unwrap(),
|
|
446
|
+
r#""critical""#
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ── WriteApprovalRequest (safety-critical) ────────────────────
|
|
451
|
+
|
|
452
|
+
#[test]
|
|
453
|
+
fn write_approval_request_round_trip() {
|
|
454
|
+
let r = WriteApprovalRequest {
|
|
455
|
+
request_id: "req-001".into(),
|
|
456
|
+
station_id: "PLC-01".into(),
|
|
457
|
+
protocol: "modbus-tcp".into(),
|
|
458
|
+
address: "40001".into(),
|
|
459
|
+
field_name: "setpoint".into(),
|
|
460
|
+
current_value: 50.0,
|
|
461
|
+
proposed_value: 75.0,
|
|
462
|
+
unit: "%".into(),
|
|
463
|
+
reason: "Agent requested setpoint increase".into(),
|
|
464
|
+
agent: "polemos".into(),
|
|
465
|
+
risk_level: WriteApprovalRisk::Caution,
|
|
466
|
+
};
|
|
467
|
+
let v = serde_json::to_value(&r).unwrap();
|
|
468
|
+
assert_eq!(v["risk_level"], "caution");
|
|
469
|
+
assert_eq!(v["proposed_value"], 75.0);
|
|
470
|
+
assert_eq!(v["request_id"], "req-001");
|
|
471
|
+
let back: WriteApprovalRequest = serde_json::from_value(v).unwrap();
|
|
472
|
+
assert_eq!(back.risk_level, WriteApprovalRisk::Caution);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#[test]
|
|
476
|
+
fn write_approval_request_id_defaults_empty() {
|
|
477
|
+
// request_id has #[serde(default)] — old messages without it
|
|
478
|
+
// deserialize to empty string.
|
|
479
|
+
let raw = json!({
|
|
480
|
+
"station_id": "PLC-01",
|
|
481
|
+
"protocol": "modbus-tcp",
|
|
482
|
+
"address": "40001",
|
|
483
|
+
"field_name": "setpoint",
|
|
484
|
+
"current_value": 50.0,
|
|
485
|
+
"proposed_value": 75.0,
|
|
486
|
+
"unit": "%",
|
|
487
|
+
"reason": "test",
|
|
488
|
+
"agent": "agent",
|
|
489
|
+
"risk_level": "safe"
|
|
490
|
+
});
|
|
491
|
+
let r: WriteApprovalRequest = serde_json::from_value(raw).unwrap();
|
|
492
|
+
assert_eq!(r.request_id, "");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#[test]
|
|
496
|
+
fn write_approval_response_with_modified_value() {
|
|
497
|
+
let r = WriteApprovalResponseParams {
|
|
498
|
+
request_id: "req-001".into(),
|
|
499
|
+
approved: true,
|
|
500
|
+
approved_by: "operator-1".into(),
|
|
501
|
+
modified_value: Some(70.0),
|
|
502
|
+
};
|
|
503
|
+
let v = serde_json::to_value(&r).unwrap();
|
|
504
|
+
assert_eq!(v["approved"], true);
|
|
505
|
+
assert_eq!(v["modified_value"], 70.0);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
#[test]
|
|
509
|
+
fn write_approval_response_without_modified_value() {
|
|
510
|
+
let r = WriteApprovalResponseParams {
|
|
511
|
+
request_id: "req-001".into(),
|
|
512
|
+
approved: false,
|
|
513
|
+
approved_by: "operator-1".into(),
|
|
514
|
+
modified_value: None,
|
|
515
|
+
};
|
|
516
|
+
let v = serde_json::to_value(&r).unwrap();
|
|
517
|
+
assert_eq!(v["modified_value"], serde_json::Value::Null);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── IndustrialStationInfo with nested fields ─────────────────
|
|
521
|
+
|
|
522
|
+
#[test]
|
|
523
|
+
fn station_info_with_alarm_thresholds() {
|
|
524
|
+
let s = IndustrialStationInfo {
|
|
525
|
+
station_id: "PLC-01".into(),
|
|
526
|
+
protocol: "modbus-tcp".into(),
|
|
527
|
+
connection: "192.168.1.5:502".into(),
|
|
528
|
+
device_class: "PLC".into(),
|
|
529
|
+
vendor: Some("Siemens".into()),
|
|
530
|
+
model: Some("S7-1200".into()),
|
|
531
|
+
firmware: None,
|
|
532
|
+
status: "online".into(),
|
|
533
|
+
fields: vec![IndustrialStationField {
|
|
534
|
+
address: "40001".into(),
|
|
535
|
+
name: "pressure".into(),
|
|
536
|
+
data_type: "float32".into(),
|
|
537
|
+
unit: Some("bar".into()),
|
|
538
|
+
alarm: Some(IndustrialAlarmThresholds {
|
|
539
|
+
ll: None,
|
|
540
|
+
l: Some(2.0),
|
|
541
|
+
h: Some(8.0),
|
|
542
|
+
hh: Some(10.0),
|
|
543
|
+
}),
|
|
544
|
+
current_value: Some(5.5),
|
|
545
|
+
}],
|
|
546
|
+
};
|
|
547
|
+
let v = serde_json::to_value(&s).unwrap();
|
|
548
|
+
assert_eq!(v["vendor"], "Siemens");
|
|
549
|
+
assert_eq!(v["fields"][0]["alarm"]["hh"], 10.0);
|
|
550
|
+
assert_eq!(v["fields"][0]["current_value"], 5.5);
|
|
551
|
+
let back: IndustrialStationInfo = serde_json::from_value(v).unwrap();
|
|
552
|
+
assert_eq!(back.fields.len(), 1);
|
|
553
|
+
assert_eq!(back.fields[0].alarm.as_ref().unwrap().hh, Some(10.0));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#[test]
|
|
557
|
+
fn station_info_minimal() {
|
|
558
|
+
let s = IndustrialStationInfo {
|
|
559
|
+
station_id: "X".into(),
|
|
560
|
+
protocol: "p".into(),
|
|
561
|
+
connection: "c".into(),
|
|
562
|
+
device_class: "d".into(),
|
|
563
|
+
vendor: None,
|
|
564
|
+
model: None,
|
|
565
|
+
firmware: None,
|
|
566
|
+
status: "offline".into(),
|
|
567
|
+
fields: vec![],
|
|
568
|
+
};
|
|
569
|
+
let v = serde_json::to_value(&s).unwrap();
|
|
570
|
+
assert_eq!(v["vendor"], serde_json::Value::Null);
|
|
571
|
+
assert_eq!(v["fields"], json!([]));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── Telemetry batch ───────────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
#[test]
|
|
577
|
+
fn telemetry_batch_round_trip() {
|
|
578
|
+
let batch = IndustrialTelemetryBatch {
|
|
579
|
+
readings: vec![IndustrialSensorReading {
|
|
580
|
+
station_id: "PLC-01".into(),
|
|
581
|
+
protocol: "modbus-tcp".into(),
|
|
582
|
+
address: "40001".into(),
|
|
583
|
+
name: "temp".into(),
|
|
584
|
+
raw_value: 1.0,
|
|
585
|
+
scaled_value: 25.5,
|
|
586
|
+
unit: "°C".into(),
|
|
587
|
+
quality: "good".into(),
|
|
588
|
+
timestamp: "2026-07-07T00:00:00Z".into(),
|
|
589
|
+
}],
|
|
590
|
+
station_id: "PLC-01".into(),
|
|
591
|
+
timestamp: "2026-07-07T00:00:00Z".into(),
|
|
592
|
+
};
|
|
593
|
+
let v = serde_json::to_value(&batch).unwrap();
|
|
594
|
+
assert_eq!(v["readings"].as_array().unwrap().len(), 1);
|
|
595
|
+
assert_eq!(v["readings"][0]["unit"], "°C");
|
|
596
|
+
let back: IndustrialTelemetryBatch = serde_json::from_value(v).unwrap();
|
|
597
|
+
assert_eq!(back.readings.len(), 1);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ── Alarm history ─────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
#[test]
|
|
603
|
+
fn alarm_history_entry_acknowledged() {
|
|
604
|
+
let e = IndustrialAlarmHistoryEntry {
|
|
605
|
+
station_id: "PLC-01".into(),
|
|
606
|
+
protocol: "modbus-tcp".into(),
|
|
607
|
+
address: "40001".into(),
|
|
608
|
+
field_name: "pressure".into(),
|
|
609
|
+
level: IndustrialAlarmLevel::High,
|
|
610
|
+
value: 9.0,
|
|
611
|
+
threshold: 8.0,
|
|
612
|
+
unit: "bar".into(),
|
|
613
|
+
breached: true,
|
|
614
|
+
timestamp: "2026-07-07T00:00:00Z".into(),
|
|
615
|
+
acknowledged: true,
|
|
616
|
+
acknowledged_at: Some("2026-07-07T00:01:00Z".into()),
|
|
617
|
+
acknowledged_by: Some("operator-1".into()),
|
|
618
|
+
};
|
|
619
|
+
let v = serde_json::to_value(&e).unwrap();
|
|
620
|
+
assert_eq!(v["acknowledged"], true);
|
|
621
|
+
assert_eq!(v["acknowledged_by"], "operator-1");
|
|
622
|
+
let back: IndustrialAlarmHistoryEntry = serde_json::from_value(v).unwrap();
|
|
623
|
+
assert!(back.acknowledged);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#[test]
|
|
627
|
+
fn alarm_history_entry_unacknowledged() {
|
|
628
|
+
let e = IndustrialAlarmHistoryEntry {
|
|
629
|
+
station_id: "X".into(),
|
|
630
|
+
protocol: "p".into(),
|
|
631
|
+
address: "a".into(),
|
|
632
|
+
field_name: "f".into(),
|
|
633
|
+
level: IndustrialAlarmLevel::Low,
|
|
634
|
+
value: 1.0,
|
|
635
|
+
threshold: 2.0,
|
|
636
|
+
unit: "u".into(),
|
|
637
|
+
breached: true,
|
|
638
|
+
timestamp: "t".into(),
|
|
639
|
+
acknowledged: false,
|
|
640
|
+
acknowledged_at: None,
|
|
641
|
+
acknowledged_by: None,
|
|
642
|
+
};
|
|
643
|
+
let v = serde_json::to_value(&e).unwrap();
|
|
644
|
+
assert_eq!(v["acknowledged"], false);
|
|
645
|
+
assert_eq!(v["acknowledged_at"], serde_json::Value::Null);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//! Knowledge Base — RAG store info & lifecycle responses.
|
|
2
|
+
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
use ts_rs::TS;
|
|
5
|
+
|
|
6
|
+
use crate::{EmbeddingModel, KnowledgeBaseStatus};
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
9
|
+
#[ts(export, export_to = "ws/knowledgeBase.ts")]
|
|
10
|
+
pub struct KbGenericResponseParams {
|
|
11
|
+
pub ok: bool,
|
|
12
|
+
#[serde(default)]
|
|
13
|
+
#[ts(optional)]
|
|
14
|
+
pub error: Option<String>,
|
|
15
|
+
#[serde(default)]
|
|
16
|
+
#[ts(optional)]
|
|
17
|
+
pub id: Option<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
21
|
+
#[ts(export, export_to = "ws/knowledgeBase.ts")]
|
|
22
|
+
pub struct KnowledgeBaseInfo {
|
|
23
|
+
#[ts(type = "string")]
|
|
24
|
+
pub id: uuid::Uuid,
|
|
25
|
+
pub name: String,
|
|
26
|
+
#[serde(default)]
|
|
27
|
+
#[ts(optional)]
|
|
28
|
+
pub description: Option<String>,
|
|
29
|
+
#[serde(default)]
|
|
30
|
+
pub status: KnowledgeBaseStatus,
|
|
31
|
+
#[serde(default)]
|
|
32
|
+
#[ts(optional)]
|
|
33
|
+
pub embedding_model: Option<EmbeddingModel>,
|
|
34
|
+
#[serde(default)]
|
|
35
|
+
#[ts(optional)]
|
|
36
|
+
pub custom_embedding_endpoint: Option<String>,
|
|
37
|
+
pub document_count: usize,
|
|
38
|
+
#[serde(default)]
|
|
39
|
+
pub subscription_count: usize,
|
|
40
|
+
#[serde(default)]
|
|
41
|
+
pub tags: Vec<String>,
|
|
42
|
+
pub created_at: String,
|
|
43
|
+
#[serde(default)]
|
|
44
|
+
pub updated_at: String,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
48
|
+
#[ts(export, export_to = "ws/knowledgeBase.ts")]
|
|
49
|
+
pub struct ListKnowledgeBasesResponseParams {
|
|
50
|
+
pub knowledge_bases: Vec<KnowledgeBaseInfo>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
54
|
+
#[ts(export, export_to = "ws/knowledgeBase.ts")]
|
|
55
|
+
pub struct GetKnowledgeBaseResponseParams {
|
|
56
|
+
#[serde(default)]
|
|
57
|
+
#[ts(optional)]
|
|
58
|
+
pub knowledge_base: Option<KnowledgeBaseInfo>,
|
|
59
|
+
}
|