zeroclaw_config/validation_warnings.rs
1//! Non-fatal validation warnings — config that loads and validates
2//! successfully (i.e. `Config::validate()` returns `Ok(())`) but will fail
3//! at agent runtime because of a logical inconsistency the schema can't
4//! enforce structurally.
5//!
6//! The CLI surfaces these via `zeroclaw_log::record!` so operators see them on
7//! stderr. The gateway HTTP API surfaces them via the `warnings` field on
8//! `PropResponse` / `PatchResponse` so dashboard callers see the same
9//! signal — closing the parity gap that previously left a dashboard user
10//! with no indication their config would fail at runtime.
11//!
12//! Each warning carries:
13//! - a stable `code` (machine-friendly, matches across releases for a
14//! given check)
15//! - a human-readable `message` (suitable for direct display to operators)
16//! - the dotted property `path` the warning concerns (so the dashboard
17//! can highlight the offending field)
18//!
19//! Adding a new warning: append the check to `Config::collect_warnings`
20//! in `schema.rs` and pick a stable `code`. `Config::validate` emits each
21//! collected warning via `zeroclaw_log::record!` so logs continue to show them.
22
23use serde::{Deserialize, Serialize};
24
25/// One non-fatal validation issue surfaced after a successful save.
26///
27/// Stable codes (extend as new warnings are added):
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
30pub struct ValidationWarning {
31 /// Stable machine-readable identifier for the warning class.
32 pub code: String,
33 /// Human-readable description suitable for direct display.
34 pub message: String,
35 /// Dotted property path the warning concerns
36 /// (e.g. `"agents.researcher.model_provider"`).
37 pub path: String,
38}
39
40impl ValidationWarning {
41 pub fn new(
42 code: impl Into<String>,
43 message: impl Into<String>,
44 path: impl Into<String>,
45 ) -> Self {
46 Self {
47 code: code.into(),
48 message: message.into(),
49 path: path.into(),
50 }
51 }
52}