use crate::error::{Error, Result};
use crate::model::Severity;
use crate::recommendation::Checkpoint;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TelemetryMode {
Off,
#[default]
Aggregate,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiscoverObjective {
#[default]
ReviewQueue,
Learner,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AutoApplyGrant {
pub analyzer: String,
pub targets: Vec<String>,
pub max_severity: Severity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceAttribution {
#[default]
Named,
Anonymous,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BaselineKind {
#[default]
NewestBeforeApply,
HighWater,
}
impl BaselineKind {
pub fn as_str(self) -> &'static str {
match self {
BaselineKind::NewestBeforeApply => "newest_before_apply",
BaselineKind::HighWater => "high_water",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MinEffect {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub points: Option<f64>,
}
impl MinEffect {
fn validate(&self) -> Result<()> {
let bad = |what: &str| Err(Error::InvalidProposal(format!("policy: outcome_evalset.min_effect: {what}")));
match (self.count, self.points) {
(None, None) => bad("give {\"count\": n} or {\"points\": p}"),
(Some(_), Some(_)) => bad("give count or points, not both"),
(Some(v), None) | (None, Some(v)) if !(v.is_finite() && v >= 0.0) => {
bad("must be a finite number ≥ 0")
}
_ => Ok(()),
}
}
pub fn resolve(&self, field: &str, total: u64) -> f64 {
match (self.count, self.points) {
(Some(c), _) => c,
(None, Some(p)) => match field {
"passed" | "failed" | "total" => p / 100.0 * total as f64,
_ => p / 100.0,
},
(None, None) => 0.0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CostBound {
pub field: String,
pub max_increase_ratio: f64,
}
impl CostBound {
fn validate(&self) -> Result<()> {
let bad = |what: &str| Err(Error::InvalidProposal(format!("policy: outcome_evalset.cost: {what}")));
if self.field.trim().is_empty() {
return bad("field must name a cost field (effects, tokens, usd, wall_ms, cost_per_pass, or a harness key)");
}
if !(self.max_increase_ratio.is_finite() && self.max_increase_ratio > 0.0) {
return bad("max_increase_ratio must be a finite number > 0");
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OutcomeEvalset {
pub hash: String,
pub field: String,
pub higher_is_better: bool,
#[serde(default = "default_horizons")]
pub horizons_ms: Vec<i64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoints: Vec<Checkpoint>,
#[serde(default)]
pub baseline: BaselineKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_effect: Option<MinEffect>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<CostBound>,
}
fn default_horizons() -> Vec<i64> {
vec![86_400_000, 7 * 86_400_000, 30 * 86_400_000]
}
impl OutcomeEvalset {
pub fn schedule(&self) -> Vec<Checkpoint> {
let mut h: Vec<Checkpoint> = if self.checkpoints.is_empty() {
self.horizons_ms.iter().map(|ms| Checkpoint::AfterMs(*ms)).collect()
} else {
self.checkpoints.clone()
};
h.sort_unstable();
h.dedup();
h
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Cadence {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub every_ms: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub every_grains: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub every_events: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub every_sessions: Option<u64>,
}
impl Cadence {
pub fn is_set(&self) -> bool {
self.every_ms.is_some()
|| self.every_grains.is_some()
|| self.every_events.is_some()
|| self.every_sessions.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SkillAuthoring {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_min_steps")]
pub min_steps: u32,
}
fn default_true() -> bool {
true
}
fn default_min_steps() -> u32 {
2
}
fn default_min_evidence() -> u32 {
1
}
impl Default for SkillAuthoring {
fn default() -> Self {
SkillAuthoring { enabled: true, min_steps: 2 }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlanAuthoring {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_min_steps")]
pub min_nodes: u32,
}
impl Default for PlanAuthoring {
fn default() -> Self {
PlanAuthoring { enabled: true, min_nodes: 2 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NearDuplicateMode {
#[default]
Flag,
Suppress,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlanReplayPolicy {
#[serde(default = "default_min_runs")]
pub min_runs: u32,
#[serde(default = "default_true")]
pub require_no_worse: bool,
#[serde(default = "default_max_out_of_support")]
pub max_out_of_support: f64,
}
fn default_min_runs() -> u32 {
3
}
fn default_max_out_of_support() -> f64 {
0.5
}
impl PlanReplayPolicy {
fn validate(&self) -> Result<()> {
if !(self.max_out_of_support.is_finite() && (0.0..=1.0).contains(&self.max_out_of_support)) {
return Err(Error::InvalidProposal(
"policy: plan_replay.max_out_of_support must be a fraction in 0..=1".into(),
));
}
Ok(())
}
pub fn refusal(&self, report: &serde_json::Value) -> Option<String> {
let runs = report["totals"]["runs"].as_u64().unwrap_or(0);
if runs < u64::from(self.min_runs) {
return None;
}
let oos = report["out_of_support_fraction"].as_f64().unwrap_or(0.0);
if oos > self.max_out_of_support {
let ids: Vec<&str> = report["runs"]
.as_array()
.map(|a| {
a.iter()
.filter(|r| r["verdict"] == "out_of_support")
.filter_map(|r| r["run_id"].as_str())
.collect()
})
.unwrap_or_default();
return Some(format!(
"{:.0}% of {runs} rehearsed runs fall outside the journal's support (limit {:.0}%): {}",
oos * 100.0,
self.max_out_of_support * 100.0,
ids.join(", ")
));
}
if self.require_no_worse && report["no_worse"].as_bool() != Some(true) {
let worse: Vec<String> = report["runs"]
.as_array()
.map(|a| {
a.iter()
.filter(|r| r["verdict"] == "worse")
.filter_map(|r| {
Some(format!(
"{} ({} → {})",
r["run_id"].as_str()?,
r["incumbent_outcome"].as_str().unwrap_or("?"),
r["candidate_outcome"].as_str().unwrap_or("?")
))
})
.collect()
})
.unwrap_or_default();
let scored = runs - report["totals"]["out_of_support"].as_u64().unwrap_or(0);
return Some(if worse.is_empty() {
format!("no rehearsed run could be scored ({scored} of {runs})")
} else {
format!("worse than the incumbent on {} of {scored} rehearsed runs: {}", worse.len(), worse.join(", "))
});
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Policy {
#[serde(default)]
pub auto_apply_enabled: bool,
#[serde(default)]
pub auto_apply: Vec<AutoApplyGrant>,
#[serde(default)]
pub deny: Vec<String>,
#[serde(default)]
pub severity_floors: BTreeMap<String, Severity>,
#[serde(default)]
pub telemetry: TelemetryMode,
#[serde(default)]
pub discover_objective: DiscoverObjective,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome_evalset: Option<OutcomeEvalset>,
#[serde(default)]
pub evidence_attribution: EvidenceAttribution,
#[serde(default, skip_serializing_if = "is_default_cadence")]
pub cadence: Cadence,
#[serde(default)]
pub skills: SkillAuthoring,
#[serde(default = "default_min_evidence")]
pub min_evidence: u32,
#[serde(default)]
pub plans: PlanAuthoring,
#[serde(default = "default_true")]
pub premise_drift: bool,
#[serde(default, skip_serializing_if = "is_default_near_duplicate")]
pub near_duplicate: NearDuplicateMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plan_replay: Option<PlanReplayPolicy>,
}
fn is_default_near_duplicate(m: &NearDuplicateMode) -> bool {
*m == NearDuplicateMode::default()
}
fn is_default_cadence(c: &Cadence) -> bool {
!c.is_set()
}
impl Default for Policy {
fn default() -> Self {
Policy {
auto_apply_enabled: false,
auto_apply: Vec::new(),
deny: Vec::new(),
severity_floors: BTreeMap::new(),
telemetry: TelemetryMode::default(),
discover_objective: DiscoverObjective::default(),
outcome_evalset: None,
evidence_attribution: EvidenceAttribution::default(),
cadence: Cadence::default(),
skills: SkillAuthoring::default(),
min_evidence: 1,
plans: PlanAuthoring::default(),
premise_drift: true,
near_duplicate: NearDuplicateMode::default(),
plan_replay: None,
}
}
}
impl Policy {
pub fn from_json(s: &str) -> Result<Self> {
let p: Policy =
serde_json::from_str(s).map_err(|e| Error::InvalidProposal(format!("policy: {e}")))?;
if let Some(m) = p.outcome_evalset.as_ref().and_then(|e| e.min_effect.as_ref()) {
m.validate()?;
}
if let Some(c) = p.outcome_evalset.as_ref().and_then(|e| e.cost.as_ref()) {
c.validate()?;
}
if let Some(r) = p.plan_replay.as_ref() {
r.validate()?;
}
Ok(p)
}
pub fn denies(&self, family: &str) -> bool {
self.deny.iter().any(|d| crate::manifest::analyzer_family(d) == family)
}
pub fn severity_floor(&self, family: &str) -> Option<Severity> {
self.severity_floors
.iter()
.find(|(k, _)| crate::manifest::analyzer_family(k) == family)
.map(|(_, v)| *v)
}
pub fn grants_auto_apply(&self, family: &str, target_class: &str, severity: Severity) -> bool {
if !self.auto_apply_enabled || target_class != "memory" {
return false;
}
self.auto_apply.iter().any(|g| {
crate::manifest::analyzer_family(&g.analyzer) == family
&& g.targets.iter().any(|t| t == target_class)
&& severity <= g.max_severity
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn code_targets_never_auto_apply_even_when_granted() {
let p = Policy::from_json(
r#"{"auto_apply_enabled": true,
"auto_apply": [{"analyzer": "loop.codegen", "targets": ["code", "evalset", "memory"], "max_severity": "high"}]}"#,
)
.unwrap();
assert!(!p.grants_auto_apply("loop.codegen", "code", Severity::Info));
assert!(!p.grants_auto_apply("loop.codegen", "evalset", Severity::Info));
assert!(
p.grants_auto_apply("loop.codegen", "memory", Severity::Low),
"the same grant's memory leg still works — the exclusion is by class"
);
}
#[test]
fn default_policy_grants_nothing() {
let p = Policy::default();
assert!(!p.grants_auto_apply("loop.duplicate_sweep", "memory", Severity::Info));
assert!(!p.denies("loop.staleness"));
assert_eq!(p.telemetry, TelemetryMode::Aggregate);
}
#[test]
fn parses_and_grants() {
let p = Policy::from_json(
r#"{"auto_apply_enabled": true,
"auto_apply": [{"analyzer": "loop.duplicate_sweep", "targets": ["memory"], "max_severity": "low"}],
"deny": ["loop.staleness"],
"severity_floors": {"loop.contradiction_sweep": "high"}}"#,
)
.unwrap();
assert!(p.grants_auto_apply("loop.duplicate_sweep", "memory", Severity::Low));
assert!(!p.grants_auto_apply("loop.duplicate_sweep", "memory", Severity::High), "above max_severity");
assert!(!p.grants_auto_apply("loop.duplicate_sweep", "query", Severity::Low), "query not granted");
assert!(p.denies("loop.staleness"));
assert_eq!(p.severity_floor("loop.contradiction_sweep"), Some(Severity::High));
}
#[test]
fn prompt_and_host_targets_never_granted() {
let p = Policy::from_json(
r#"{"auto_apply_enabled": true,
"auto_apply": [{"analyzer": "x", "targets": ["prompt", "host"], "max_severity": "high"}]}"#,
)
.unwrap();
assert!(!p.grants_auto_apply("x", "prompt", Severity::Info));
assert!(!p.grants_auto_apply("x", "host", Severity::Info));
}
#[test]
fn discover_objective_defaults_to_the_review_queue_rule() {
assert_eq!(Policy::default().discover_objective, DiscoverObjective::ReviewQueue);
let p = Policy::from_json(r#"{"discover_objective": "learner"}"#).unwrap();
assert_eq!(p.discover_objective, DiscoverObjective::Learner);
assert!(
Policy::from_json(r#"{"discover_objective": "eager"}"#).is_err(),
"an unknown objective must not load as the default"
);
}
#[test]
fn outcome_evalset_baseline_is_a_named_choice() {
let p = Policy::from_json(
r#"{"outcome_evalset": {"hash": "f", "field": "passed", "higher_is_better": true, "baseline": "high_water"}}"#,
)
.unwrap();
assert_eq!(p.outcome_evalset.unwrap().baseline, BaselineKind::HighWater);
let p = Policy::from_json(r#"{"outcome_evalset": {"hash": "f", "field": "passed", "higher_is_better": true}}"#).unwrap();
assert_eq!(p.outcome_evalset.unwrap().baseline, BaselineKind::NewestBeforeApply);
let err = Policy::from_json(
r#"{"outcome_evalset": {"hash": "f", "field": "passed", "higher_is_better": true, "baseline": "best_ever"}}"#,
)
.expect_err("unknown baseline kind");
let msg = err.to_string();
assert!(msg.contains("newest_before_apply") && msg.contains("high_water"), "{msg}");
}
#[test]
fn plan_replay_gate_reads_the_report_the_way_the_ticket_says() {
let p = Policy::from_json(r#"{"plan_replay": {"min_runs": 3, "require_no_worse": true}}"#).unwrap();
let g = p.plan_replay.unwrap();
assert_eq!((g.min_runs, g.require_no_worse, g.max_out_of_support), (3, true, 0.5));
let report = |runs: u64, worse: u64, oos: u64, no_worse: bool| {
let rows: Vec<serde_json::Value> = (0..runs)
.map(|i| {
let verdict = if i < worse { "worse" } else if i < worse + oos { "out_of_support" } else { "same" };
serde_json::json!({"run_id": format!("r{i}"), "verdict": verdict, "incumbent_outcome": "completed", "candidate_outcome": if verdict == "worse" { "failed" } else { "completed" }})
})
.collect();
serde_json::json!({"totals": {"runs": runs, "out_of_support": oos}, "no_worse": no_worse,
"out_of_support_fraction": oos as f64 / runs.max(1) as f64, "runs": rows})
};
assert_eq!(g.refusal(&report(2, 2, 0, false)), None, "under min_runs the gate abstains");
assert_eq!(g.refusal(&report(3, 0, 0, true)), None);
let why = g.refusal(&report(3, 1, 0, false)).expect("worse is refused");
assert!(why.contains("r0") && why.contains("completed → failed"), "{why}");
let why = g.refusal(&report(4, 0, 3, false)).expect("out of support beyond the limit is refused");
assert!(why.contains("75%") && why.contains("r0, r1, r2"), "{why}");
assert!(Policy::from_json(r#"{"plan_replay": {"max_out_of_support": 1.5}}"#).is_err());
assert!(Policy::from_json(r#"{"plan_replay": {"min_runs": 3, "strict": true}}"#).is_err());
}
#[test]
fn near_duplicate_mode_parses_and_rejects_unknown() {
assert_eq!(Policy::default().near_duplicate, NearDuplicateMode::Flag);
let p = Policy::from_json(r#"{"near_duplicate": "suppress"}"#).unwrap();
assert_eq!(p.near_duplicate, NearDuplicateMode::Suppress);
let err = Policy::from_json(r#"{"near_duplicate": "drop"}"#).expect_err("unknown mode");
let msg = err.to_string();
assert!(msg.contains("flag") && msg.contains("suppress"), "{msg}");
}
#[test]
fn cost_bound_needs_a_field_and_a_positive_ratio() {
let base = |extra: &str| {
format!(r#"{{"outcome_evalset": {{"hash": "f", "field": "passed", "higher_is_better": true, "cost": {extra}}}}}"#)
};
let c = Policy::from_json(&base(r#"{"field": "tokens", "max_increase_ratio": 1.5}"#))
.unwrap().outcome_evalset.unwrap().cost.unwrap();
assert_eq!((c.field.as_str(), c.max_increase_ratio), ("tokens", 1.5));
for bad in [
r#"{"field": "tokens"}"#,
r#"{"max_increase_ratio": 1.5}"#,
r#"{"field": "", "max_increase_ratio": 1.5}"#,
r#"{"field": "tokens", "max_increase_ratio": 0}"#,
r#"{"field": "tokens", "max_increase_ratio": -1}"#,
r#"{"field": "tokens", "max_increase_ratio": "1.5"}"#,
r#"{"field": "tokens", "max_increase_ratio": 1.5, "hard": true}"#,
] {
assert!(Policy::from_json(&base(bad)).is_err(), "{bad} must be a policy error");
}
}
#[test]
fn min_effect_is_one_non_negative_number() {
let base = |extra: &str| {
format!(r#"{{"outcome_evalset": {{"hash": "f", "field": "passed", "higher_is_better": true, "min_effect": {extra}}}}}"#)
};
let m = Policy::from_json(&base(r#"{"count": 5}"#)).unwrap().outcome_evalset.unwrap().min_effect.unwrap();
assert_eq!(m.resolve("passed", 387), 5.0);
let m = Policy::from_json(&base(r#"{"points": 1.0}"#)).unwrap().outcome_evalset.unwrap().min_effect.unwrap();
assert_eq!(m.resolve("passed", 200), 2.0, "points scale a count field by the run total");
assert!((m.resolve("error_rate", 200) - 0.01).abs() < 1e-12, "a ratio field reads points as a fraction");
assert!((m.resolve("category_accuracy", 200) - 0.01).abs() < 1e-12, "a host field is assumed a ratio");
for bad in [r#"{"count": -1}"#, r#"{"points": "1"}"#, r#"{}"#, r#"{"count": 1, "points": 1}"#, r#"{"count": null}"#, r#"{"width": 2}"#] {
assert!(Policy::from_json(&base(bad)).is_err(), "{bad} must be a policy error");
}
assert!(Policy::from_json(&base("null")).unwrap().outcome_evalset.unwrap().min_effect.is_none());
}
#[test]
fn outcome_evalset_parses_with_default_horizons() {
let p = Policy::from_json(
r#"{"outcome_evalset": {"hash": "abc123", "field": "exact", "higher_is_better": true}}"#,
)
.unwrap();
let e = p.outcome_evalset.expect("parsed");
assert_eq!((e.hash.as_str(), e.field.as_str(), e.higher_is_better), ("abc123", "exact", true));
assert_eq!(e.horizons_ms, vec![86_400_000, 7 * 86_400_000, 30 * 86_400_000]);
assert!(Policy::default().outcome_evalset.is_none());
assert!(
Policy::from_json(r#"{"outcome_evalset": {"hash": "abc123", "field": "exact"}}"#).is_err(),
"the direction is not optional — a guessed one could revert an improvement"
);
}
#[test]
fn checkpoints_take_the_deployments_unit_and_a_bare_integer_stays_ms() {
let p = Policy::from_json(
r#"{"outcome_evalset": {"hash": "f", "field": "task_score", "higher_is_better": true,
"checkpoints": [{"after_runs": 1}, 3600000, {"after_grains": 50}, {"after_ms": 86400000}]}}"#,
)
.unwrap();
let e = p.outcome_evalset.unwrap();
assert_eq!(
e.schedule(),
vec![
Checkpoint::AfterMs(3_600_000),
Checkpoint::AfterMs(86_400_000),
Checkpoint::AfterRuns(1),
Checkpoint::AfterGrains(50),
],
"sorted, deduplicated, and the bare integer read as milliseconds"
);
let p = Policy::from_json(r#"{"outcome_evalset": {"hash": "f", "field": "x", "higher_is_better": true}}"#).unwrap();
assert_eq!(
p.outcome_evalset.unwrap().schedule(),
vec![
Checkpoint::AfterMs(86_400_000),
Checkpoint::AfterMs(7 * 86_400_000),
Checkpoint::AfterMs(30 * 86_400_000)
]
);
for bad in [
r#"[{"after_turns": 3}]"#,
r#"[{"after_runs": -1}]"#,
r#"["1d"]"#,
r#"[{"after_runs": 1, "after_ms": 2}]"#,
] {
let js = format!(r#"{{"outcome_evalset": {{"hash": "f", "field": "x", "higher_is_better": true, "checkpoints": {bad}}}}}"#);
assert!(Policy::from_json(&js).is_err(), "{bad} must not load");
}
}
#[test]
fn cadence_defaults_to_always_due_and_parses_every_unit() {
let p = Policy::default();
assert!(!p.cadence.is_set());
let p = Policy::from_json(
r#"{"cadence": {"every_ms": 3600000, "every_events": 10, "every_sessions": 1, "every_grains": 50}}"#,
)
.unwrap();
assert!(p.cadence.is_set());
assert_eq!(p.cadence.every_events, Some(10));
assert!(
Policy::from_json(r#"{"cadence": {"every_turns": 10}}"#).is_err(),
"an unknown unit must not load as always-due"
);
assert!(!serde_json::to_string(&Policy::default()).unwrap().contains("cadence"));
}
#[test]
fn skills_default_on_with_two_steps_and_min_evidence_defaults_to_one() {
let p = Policy::default();
assert!(p.skills.enabled);
assert_eq!(p.skills.min_steps, 2);
assert_eq!(p.min_evidence, 1, "one instance may become a rule — today's behaviour");
let p = Policy::from_json(r#"{"skills": {"enabled": false}, "min_evidence": 2}"#).unwrap();
assert!(!p.skills.enabled);
assert_eq!(p.skills.min_steps, 2, "the unset field keeps its default, not zero");
assert_eq!(p.min_evidence, 2);
assert!(Policy::from_json(r#"{"skills": {"auto_apply": true}}"#).is_err(), "no back door");
let round = Policy::from_json(&serde_json::to_string(&Policy::default()).unwrap()).unwrap();
assert_eq!(round.min_evidence, 1);
assert!(round.skills.enabled);
}
#[test]
fn plans_and_premise_drift_default_on_and_are_switchable() {
let p = Policy::default();
assert!(p.plans.enabled);
assert_eq!(p.plans.min_nodes, 2);
assert!(p.premise_drift);
let p = Policy::from_json(r#"{"plans": {"enabled": false}, "premise_drift": false}"#).unwrap();
assert!(!p.plans.enabled);
assert_eq!(p.plans.min_nodes, 2);
assert!(!p.premise_drift);
assert!(Policy::from_json(r#"{"plans": {"auto_apply": true}}"#).is_err(), "no back door");
}
#[test]
fn evidence_attribution_defaults_to_named() {
assert_eq!(Policy::default().evidence_attribution, EvidenceAttribution::Named);
let p = Policy::from_json(r#"{"evidence_attribution": "anonymous"}"#).unwrap();
assert_eq!(p.evidence_attribution, EvidenceAttribution::Anonymous);
assert!(
Policy::from_json(r#"{"evidence_attribution": "redacted"}"#).is_err(),
"an unknown mode must not load as the default"
);
}
#[test]
fn unknown_keys_rejected() {
assert!(Policy::from_json(r#"{"analyzer_cmd": "evil"}"#).is_err());
assert!(Policy::from_json(r#"{"auto_apply_free_text": true}"#).is_err());
}
}