use car_ir::precondition::{self, StateView};
use car_ir::{build_dag, Action, ActionProposal, ActionType, ToolSchema};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
pub mod attempt;
pub mod concurrency;
pub mod cwm;
pub mod dag;
pub mod goal;
pub mod infoflow;
pub mod intent;
pub mod montecarlo;
pub mod plan_check;
pub mod trace_policy;
pub mod transaction;
pub mod verifier;
pub use attempt::{Attempt, AttemptAdvice, AttemptLedger, AttemptOutcome, Exclusion, FailureClass};
pub use goal::{
anchor_directive, evaluate_goal, governor_check, run_goal_loop, GoalCondition, GoalGovernor,
GoalHalt, GoalInputs, GoalRun, GoalRunState, GoalSpec, GoalStatus, GoalVerdict,
IterationOutcome,
};
pub use intent::{
check_intent, gate_intent, intent_actions_from, IntentAction, IntentDisposition,
IntentGateDecision, IntentGatePolicy, IntentReport, IntentSpec, IntentViolation,
IntentViolationKind,
};
pub use montecarlo::{
simulate_monte_carlo, ActionOutcome, Distribution, KeyOutcome, MonteCarloConfig,
MonteCarloResult, ValueFrequency,
};
pub use plan_check::{
check_plan, PlanCheckReport, PlanCheckRequest, PlanDefect, PlanDefectKind, PlanStep,
};
pub use verifier::{
admit, required_classes, AdmissionDecision, AdmissionOutcome, EvidenceRequirement, UnmetReason,
UnmetRequirement, VerifierAuthority, VerifierCost, VerifierDescriptor, VerifierOutcome,
VerifierVerdict,
};
pub mod workflow_graph;
pub use concurrency::{
analyze as analyze_concurrency, gate_concurrency, AgentOp, AnomalyFinding, ConcurrencyAnomaly,
ConcurrencyGate, ConcurrencyGatePolicy, ConcurrencyReport, ConsistencyLevel, Disposition,
GatedRemediation, Remediation,
};
pub use cwm::{
score, score_predictions, simulate_with_model, synthesize_cwm, CwmRequest, CwmResult,
EffectModel, Failure, GatedEffectModel, GatedPrediction, ScoreReport, Transition,
};
pub use infoflow::{
check_information_flow, gate_flow, Confidentiality, FlowAction, FlowGateDecision,
FlowGatePolicy, FlowPolicy, FlowReport, FlowViolation, FlowViolationKind, ToolLabels,
TrustLevel,
};
pub use transaction::{
check_transaction, check_transaction_with_predictions, ConflictKind, TransactionConflict,
TransactionReport,
};
pub use workflow_graph::{
check_temporal_policies, verify_workflow_graph, PolicyReport, PolicyViolation, TemporalPolicy,
WorkflowDefect, WorkflowDefectKind, WorkflowEdge, WorkflowGraph, WorkflowVerifyReport,
};
#[derive(Debug, Clone)]
pub struct StaticState {
pub known: HashMap<String, Value>,
pub unknown_keys: HashSet<String>,
}
impl StaticState {
pub fn new() -> Self {
Self {
known: HashMap::new(),
unknown_keys: HashSet::new(),
}
}
pub fn from_map(map: HashMap<String, Value>) -> Self {
Self {
known: map,
unknown_keys: HashSet::new(),
}
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.known.get(key)
}
pub fn exists(&self, key: &str) -> bool {
self.known.contains_key(key)
}
pub fn is_unknown(&self, key: &str) -> bool {
self.unknown_keys.contains(key)
}
pub fn set(&mut self, key: &str, value: Value) {
self.known.insert(key.to_string(), value);
self.unknown_keys.remove(key);
}
}
impl Default for StaticState {
fn default() -> Self {
Self::new()
}
}
impl StateView for StaticState {
fn get_value(&self, key: &str) -> Option<Value> {
self.known.get(key).cloned()
}
fn key_exists(&self, key: &str) -> bool {
self.known.contains_key(key)
}
fn is_unknown(&self, key: &str) -> bool {
self.unknown_keys.contains(key)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceTier {
DecisionProcedure,
Heuristic,
Sampled,
}
impl EvidenceTier {
pub const fn as_str(&self) -> &'static str {
match self {
EvidenceTier::DecisionProcedure => "decision_procedure",
EvidenceTier::Heuristic => "heuristic",
EvidenceTier::Sampled => "sampled",
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
#[non_exhaustive]
pub struct VerifyIssue {
pub action_id: String,
pub severity: String, pub message: String,
pub tier: EvidenceTier,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct CheckRecord {
pub name: String,
pub ran: bool,
pub verifies: String,
pub cannot_verify: String,
pub findings: usize,
pub tier: EvidenceTier,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct VerificationEvidence {
pub checks: Vec<CheckRecord>,
pub assumptions: Vec<String>,
pub untested_regions: Vec<String>,
pub residual_risks: Vec<String>,
pub confidence: f64,
}
#[derive(Debug, serde::Serialize)]
pub struct VerifyResult {
pub valid: bool,
pub issues: Vec<VerifyIssue>,
pub simulated_state: HashMap<String, Value>,
pub execution_levels: Vec<Vec<String>>,
pub conflicts: Vec<(String, String, String)>, pub evidence: VerificationEvidence,
}
impl VerifyResult {
pub fn errors(&self) -> Vec<&VerifyIssue> {
self.issues
.iter()
.filter(|i| i.severity == "error")
.collect()
}
pub fn warnings(&self) -> Vec<&VerifyIssue> {
self.issues
.iter()
.filter(|i| i.severity == "warning")
.collect()
}
pub fn issues_with_tier(&self, tier: EvidenceTier) -> Vec<&VerifyIssue> {
self.issues.iter().filter(|i| i.tier == tier).collect()
}
}
pub(crate) fn apply_action_effects(action: &Action, state: &mut StaticState) {
if action.action_type == ActionType::StateWrite {
if let Some(key) = action.parameters.get("key").and_then(|v| v.as_str()) {
let value = action
.parameters
.get("value")
.cloned()
.unwrap_or(Value::Null);
state.set(key, value);
}
}
for (key, value) in &action.expected_effects {
state.set(key, value.clone());
}
}
fn detect_conflicts(actions: &[Action]) -> Vec<(String, String, String)> {
let mut writers: HashMap<String, Vec<String>> = HashMap::new();
for action in actions {
let mut keys_written = HashSet::new();
if action.action_type == ActionType::StateWrite {
if let Some(k) = action.parameters.get("key").and_then(|v| v.as_str()) {
keys_written.insert(k.to_string());
}
}
for key in action.expected_effects.keys() {
keys_written.insert(key.clone());
}
for key in keys_written {
writers.entry(key).or_default().push(action.id.clone());
}
}
let dep_map: HashMap<String, HashSet<String>> = actions
.iter()
.map(|a| (a.id.clone(), a.state_dependencies.iter().cloned().collect()))
.collect();
let mut conflicts = Vec::new();
for (key, action_ids) in &writers {
if action_ids.len() < 2 {
continue;
}
for i in 0..action_ids.len() {
for j in (i + 1)..action_ids.len() {
let a1 = &action_ids[i];
let a2 = &action_ids[j];
let deps_a2 = dep_map.get(a2).cloned().unwrap_or_default();
let deps_a1 = dep_map.get(a1).cloned().unwrap_or_default();
if !deps_a2.contains(key) && !deps_a1.contains(key) {
conflicts.push((a1.clone(), a2.clone(), key.clone()));
}
}
}
}
conflicts
}
fn json_type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn value_matches_type(v: &Value, expected: &str) -> bool {
match expected {
"string" => v.is_string(),
"number" => v.is_number(),
"integer" => {
v.is_i64() || v.is_u64() || v.as_f64().map(|f| f.fract() == 0.0).unwrap_or(false)
}
"boolean" => v.is_boolean(),
"array" => v.is_array(),
"object" => v.is_object(),
"null" => v.is_null(),
_ => true,
}
}
fn validate_tool_params(params: &HashMap<String, Value>, schema: &Value) -> Vec<String> {
let mut out = Vec::new();
let Some(schema_obj) = schema.as_object() else {
return out;
};
if let Some(Value::Array(required)) = schema_obj.get("required") {
for req in required {
if let Some(name) = req.as_str() {
if !params.contains_key(name) {
out.push(format!("missing required parameter '{name}'"));
}
}
}
}
if let Some(Value::Object(properties)) = schema_obj.get("properties") {
for (key, val) in params {
let Some(prop_schema) = properties.get(key).and_then(|s| s.as_object()) else {
continue;
};
let ok = match prop_schema.get("type") {
Some(Value::String(t)) => value_matches_type(val, t),
Some(Value::Array(types)) => types
.iter()
.filter_map(|t| t.as_str())
.any(|t| value_matches_type(val, t)),
_ => true,
};
if !ok {
let expected = match prop_schema.get("type") {
Some(Value::String(t)) => t.clone(),
Some(Value::Array(types)) => types
.iter()
.filter_map(|t| t.as_str())
.collect::<Vec<_>>()
.join("|"),
_ => String::new(),
};
out.push(format!(
"parameter '{key}' has wrong type: expected {expected}, got {}",
json_type_name(val)
));
}
}
}
out
}
pub fn verify(
proposal: &ActionProposal,
initial_state: Option<&HashMap<String, Value>>,
registered_tools: Option<&HashSet<String>>,
max_actions: usize,
) -> VerifyResult {
verify_inner(proposal, initial_state, registered_tools, None, max_actions)
}
pub fn verify_with_schemas(
proposal: &ActionProposal,
initial_state: Option<&HashMap<String, Value>>,
tool_schemas: Option<&HashMap<String, ToolSchema>>,
max_actions: usize,
) -> VerifyResult {
verify_inner(proposal, initial_state, None, tool_schemas, max_actions)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EffectMode {
Optimistic,
ExecutionFaithful,
}
fn verify_inner(
proposal: &ActionProposal,
initial_state: Option<&HashMap<String, Value>>,
registered_tools: Option<&HashSet<String>>,
tool_schemas: Option<&HashMap<String, ToolSchema>>,
max_actions: usize,
) -> VerifyResult {
verify_inner_with_effects(
proposal,
initial_state,
registered_tools,
tool_schemas,
max_actions,
EffectMode::Optimistic,
)
}
fn verify_inner_with_effects(
proposal: &ActionProposal,
initial_state: Option<&HashMap<String, Value>>,
registered_tools: Option<&HashSet<String>>,
tool_schemas: Option<&HashMap<String, ToolSchema>>,
max_actions: usize,
effect_mode: EffectMode,
) -> VerifyResult {
let mut state = match initial_state {
Some(s) => StaticState::from_map(s.clone()),
None => StaticState::new(),
};
let mut issues = Vec::new();
let mut precondition_findings = 0usize;
let mut state_dependency_findings = 0usize;
let mut tool_existence_findings = 0usize;
let mut param_schema_findings = 0usize;
let mut has_tool_calls = false;
let mut saw_missing_tool = false;
let mut compensation_findings = 0usize;
let mut saw_compensation_ref = false;
let has_tool_registry = tool_schemas.is_some() || registered_tools.is_some();
let param_schema_ran = tool_schemas.is_some();
let issues_before_bounds = issues.len();
if proposal.actions.len() > max_actions {
issues.push(VerifyIssue {
action_id: proposal
.actions
.first()
.map(|a| a.id.clone())
.unwrap_or_default(),
severity: "warning".to_string(),
message: format!(
"excessive actions: {} (limit {})",
proposal.actions.len(),
max_actions
),
tier: EvidenceTier::DecisionProcedure,
});
}
let resource_bound_findings = issues.len() - issues_before_bounds;
let issues_before_loop = issues.len();
let mut seen_calls: HashMap<String, u32> = HashMap::new();
for action in &proposal.actions {
if action.action_type == ActionType::ToolCall {
if let Some(ref tool) = action.tool {
let params = serde_json::to_string(&action.parameters).unwrap_or_default();
let key = format!("{}:{}", tool, params);
*seen_calls.entry(key).or_insert(0) += 1;
}
}
}
for (call_key, count) in &seen_calls {
let tool_name = call_key.split(':').next().unwrap_or("?");
if *count >= 3 {
issues.push(VerifyIssue {
action_id: "proposal".to_string(),
severity: "error".to_string(),
message: format!(
"repeated identical tool call: {} ({}x) — likely loop",
tool_name, count
),
tier: EvidenceTier::Heuristic,
});
} else if *count == 2 {
issues.push(VerifyIssue {
action_id: "proposal".to_string(),
severity: "warning".to_string(),
message: format!("duplicate tool call: {} ({}x)", tool_name, count),
tier: EvidenceTier::Heuristic,
});
}
}
let loop_detection_findings = issues.len() - issues_before_loop;
let levels = build_dag(&proposal.actions);
let execution_levels: Vec<Vec<String>> = levels
.iter()
.map(|level| {
level
.iter()
.map(|&i| proposal.actions[i].id.clone())
.collect()
})
.collect();
for level in &levels {
for &idx in level {
let action = &proposal.actions[idx];
let mut blocked = false;
for pre in &action.preconditions {
if let Some(error) = precondition::check_precondition(pre, &state) {
precondition_findings += 1;
blocked = true;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: format!("precondition will fail: {}", error),
tier: EvidenceTier::DecisionProcedure,
});
}
}
for dep in &action.state_dependencies {
if !state.exists(dep) && !state.is_unknown(dep) {
state_dependency_findings += 1;
blocked = true;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: format!("state dependency '{}' not available at this point", dep),
tier: EvidenceTier::DecisionProcedure,
});
}
}
if action.action_type == ActionType::ToolCall {
has_tool_calls = true;
if let Some(ref tool) = action.tool {
let registered = match (tool_schemas, registered_tools) {
(Some(schemas), _) => Some(schemas.contains_key(tool.as_str())),
(None, Some(names)) => Some(names.contains(tool.as_str())),
(None, None) => None,
};
if registered == Some(false) {
tool_existence_findings += 1;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: format!("tool '{}' is not registered", tool),
tier: EvidenceTier::DecisionProcedure,
});
}
if let Some(schema) = tool_schemas.and_then(|s| s.get(tool.as_str())) {
for msg in validate_tool_params(&action.parameters, &schema.parameters) {
param_schema_findings += 1;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: format!("tool '{tool}': {msg}"),
tier: EvidenceTier::DecisionProcedure,
});
}
}
} else {
saw_missing_tool = true;
tool_existence_findings += 1;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: "tool_call action has no tool specified".to_string(),
tier: EvidenceTier::DecisionProcedure,
});
}
}
match &action.compensation {
Some(car_ir::Compensation::Tool { tool, .. }) => {
let registered = match (tool_schemas, registered_tools) {
(Some(schemas), _) => Some(schemas.contains_key(tool.as_str())),
(None, Some(names)) => Some(names.contains(tool.as_str())),
(None, None) => None,
};
if registered == Some(false) {
compensation_findings += 1;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: format!(
"compensation names tool '{tool}', which is not registered"
),
tier: EvidenceTier::DecisionProcedure,
});
}
}
Some(car_ir::Compensation::ActionRef { action_id }) => {
saw_compensation_ref = true;
if !proposal.actions.iter().any(|a| &a.id == action_id) {
compensation_findings += 1;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: format!(
"compensation references action '{action_id}', which is not in this proposal"
),
tier: EvidenceTier::DecisionProcedure,
});
}
}
None => {}
}
if action.missing_required_compensation() {
compensation_findings += 1;
issues.push(VerifyIssue {
action_id: action.id.clone(),
severity: "error".to_string(),
message: "action declares reversibility 'compensable' but no compensation"
.to_string(),
tier: EvidenceTier::DecisionProcedure,
});
}
if effect_mode == EffectMode::Optimistic || !blocked {
apply_action_effects(action, &mut state);
}
}
}
let conflicts = detect_conflicts(&proposal.actions);
for (a1, a2, key) in &conflicts {
issues.push(VerifyIssue {
action_id: a1.clone(),
severity: "warning".to_string(),
message: format!(
"write conflict on '{}' with action {} (no dependency declared)",
key, a2
),
tier: EvidenceTier::DecisionProcedure,
});
}
let conflict_findings = conflicts.len();
let has_errors = issues.iter().any(|i| i.severity == "error");
let warning_count = issues.iter().filter(|i| i.severity == "warning").count();
let checks = vec![
CheckRecord {
name: "resource_bounds".into(),
ran: true,
verifies: format!("action count is within the limit ({max_actions})"),
cannot_verify: "per-action cost, wall-clock time, or memory at runtime".into(),
findings: resource_bound_findings,
tier: EvidenceTier::DecisionProcedure,
},
CheckRecord {
name: "loop_detection".into(),
ran: true,
verifies: "no identical tool call is repeated enough to look like a loop".into(),
cannot_verify: "semantically redundant calls with differing arguments".into(),
findings: loop_detection_findings,
tier: EvidenceTier::Heuristic,
},
CheckRecord {
name: "preconditions".into(),
ran: true,
verifies: "declared preconditions hold against the statically-known state".into(),
cannot_verify: "preconditions over keys whose values are only known at runtime".into(),
findings: precondition_findings,
tier: EvidenceTier::DecisionProcedure,
},
CheckRecord {
name: "state_dependencies".into(),
ran: true,
verifies: "each declared state dependency is produced before it is read".into(),
cannot_verify: "undeclared reads — state a tool consumes without listing it".into(),
findings: state_dependency_findings,
tier: EvidenceTier::DecisionProcedure,
},
CheckRecord {
name: "tool_existence".into(),
ran: has_tool_registry || saw_missing_tool,
verifies: if has_tool_registry {
"every tool_call names a registered tool".into()
} else if saw_missing_tool {
"tool_call structural well-formedness (a tool is named); registry not supplied so existence unchecked".into()
} else {
"(skipped — no tool registry supplied)".into()
},
cannot_verify: "whether the registered tool behaves as its name/description implies"
.into(),
findings: tool_existence_findings,
tier: EvidenceTier::DecisionProcedure,
},
CheckRecord {
name: "param_schema".into(),
ran: param_schema_ran,
verifies: if param_schema_ran {
"tool_call parameters match the registered JSON Schema (types + required)".into()
} else {
"(skipped — no tool schemas supplied; existence only)".into()
},
cannot_verify:
"value-level constraints beyond type/required (ranges, formats, cross-field)".into(),
findings: param_schema_findings,
tier: EvidenceTier::DecisionProcedure,
},
CheckRecord {
name: "compensation_resolution".into(),
ran: has_tool_registry || saw_compensation_ref || compensation_findings > 0,
verifies: "a declared compensation names a registered tool or an action in this \
proposal, and a `compensable` action declares one at all"
.into(),
cannot_verify: "whether the named compensation actually undoes the effect — that it \
is the right inverse, and that it will still work later"
.into(),
findings: compensation_findings,
tier: EvidenceTier::DecisionProcedure,
},
CheckRecord {
name: "write_conflicts".into(),
ran: true,
verifies: "concurrent writers to the same key declare an ordering dependency".into(),
cannot_verify:
"semantic conflicts — two actions whose effects are logically incompatible".into(),
findings: conflict_findings,
tier: EvidenceTier::DecisionProcedure,
},
];
let mut untested_regions: Vec<String> = Vec::new();
for action in &proposal.actions {
if action.action_type == ActionType::ToolCall {
if let Some(ref tool) = action.tool {
untested_regions.push(format!(
"runtime output of tool '{tool}' (action {})",
action.id
));
}
for key in action.expected_effects.keys() {
untested_regions.push(format!(
"state key '{key}' (value set at runtime by action {})",
action.id
));
}
}
}
untested_regions.sort();
untested_regions.dedup();
let mut assumptions = vec![
"supplied initial-state values are accurate".to_string(),
"tool implementations honor their declared effects and side effects".to_string(),
];
if !param_schema_ran && has_tool_calls {
assumptions.push(
"tool_call parameters are well-formed (no schemas supplied to check them)".to_string(),
);
}
let mut residual_risks = Vec::new();
if !conflicts.is_empty() {
residual_risks.push(format!(
"{} undeclared write conflict(s) — last-writer-wins at runtime",
conflicts.len()
));
}
if warning_count > 0 {
residual_risks.push(format!(
"{warning_count} warning(s) not blocking the verdict"
));
}
if !untested_regions.is_empty() {
residual_risks.push(
"outcomes depending on runtime tool output or runtime-set state are unverified"
.to_string(),
);
}
let mut confidence: f64 = 1.0;
if has_tool_calls && !has_tool_registry {
confidence -= 0.15;
}
if has_tool_calls && !param_schema_ran {
confidence -= 0.20;
}
confidence -= (untested_regions.len() as f64 * 0.02).min(0.25);
confidence -= (warning_count as f64 * 0.05).min(0.20);
let confidence = confidence.clamp(0.0, 1.0);
let evidence = VerificationEvidence {
checks,
assumptions,
untested_regions,
residual_risks,
confidence,
};
VerifyResult {
valid: !has_errors,
issues,
simulated_state: state.known,
execution_levels,
conflicts,
evidence,
}
}
pub fn simulate(
proposal: &ActionProposal,
initial_state: Option<&HashMap<String, Value>>,
) -> HashMap<String, Value> {
verify_inner_with_effects(
proposal,
initial_state,
None,
None,
usize::MAX,
EffectMode::ExecutionFaithful,
)
.simulated_state
}
pub fn equivalent(
p1: &ActionProposal,
p2: &ActionProposal,
test_states: Option<&[HashMap<String, Value>]>,
) -> bool {
let defaults = vec![
HashMap::new(),
[
("x".to_string(), Value::from(1)),
("y".to_string(), Value::from(2)),
]
.into(),
];
let states = test_states.unwrap_or(&defaults);
for state in states {
let s1 = simulate(p1, Some(state));
let s2 = simulate(p2, Some(state));
if s1 != s2 {
return false;
}
}
true
}
pub fn optimize(proposal: &ActionProposal) -> ActionProposal {
let mut written_keys = HashSet::new();
for action in &proposal.actions {
if action.action_type == ActionType::StateWrite {
if let Some(k) = action.parameters.get("key").and_then(|v| v.as_str()) {
written_keys.insert(k.to_string());
}
}
for key in action.expected_effects.keys() {
written_keys.insert(key.clone());
}
}
let optimized_actions: Vec<Action> = proposal
.actions
.iter()
.map(|action| {
let pruned: Vec<String> = action
.state_dependencies
.iter()
.filter(|d| written_keys.contains(d.as_str()))
.cloned()
.collect();
if pruned.len() != action.state_dependencies.len() {
let mut new_action = action.clone();
new_action.state_dependencies = pruned;
new_action
} else {
action.clone()
}
})
.collect();
ActionProposal {
id: proposal.id.clone(),
source: proposal.source.clone(),
actions: optimized_actions,
timestamp: proposal.timestamp,
context: proposal.context.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use car_ir::Precondition;
fn tool_call(id: &str, tool: &str) -> Action {
{
let mut a = Action::new(ActionType::ToolCall);
a.id = id.to_string();
a.tool = Some(tool.to_string());
a
}
}
fn state_write(id: &str, key: &str, value: Value) -> Action {
{
let mut a = Action::new(ActionType::StateWrite);
a.id = id.to_string();
a.parameters = [
("key".to_string(), Value::from(key)),
("value".to_string(), value),
]
.into();
a
}
}
fn prop(actions: Vec<Action>) -> ActionProposal {
ActionProposal {
id: "test".to_string(),
source: "test".to_string(),
actions,
timestamp: chrono::Utc::now(),
context: HashMap::new(),
}
}
#[test]
fn verify_valid_proposal() {
let p = prop(vec![state_write("a1", "x", Value::from(1)), {
let mut a = tool_call("a2", "search");
a.state_dependencies = vec!["x".to_string()];
a
}]);
let r = verify(&p, None, Some(&["search".to_string()].into()), 30);
assert!(r.valid);
}
fn echo_schema_parameters() -> Value {
serde_json::json!({
"type": "object",
"properties": { "msg": { "type": "string" } },
"required": ["msg"],
})
}
fn schema_map(parameters: Value) -> HashMap<String, ToolSchema> {
[(
"echo".to_string(),
ToolSchema {
name: "echo".to_string(),
description: String::new(),
parameters,
returns: None,
idempotent: true,
cache_ttl_secs: None,
rate_limit: None,
},
)]
.into()
}
fn echo_call(params: HashMap<String, Value>) -> ActionProposal {
let mut a = tool_call("a1", "echo");
a.parameters = params;
prop(vec![a])
}
#[test]
fn schema_verify_accepts_well_typed_params() {
let p = echo_call([("msg".to_string(), Value::from("hi"))].into());
let r = verify_with_schemas(&p, None, Some(&schema_map(echo_schema_parameters())), 30);
assert!(r.valid, "{:?}", r.issues);
}
#[test]
fn schema_verify_rejects_type_mismatch() {
let p = echo_call([("msg".to_string(), Value::from(42))].into());
let r = verify_with_schemas(&p, None, Some(&schema_map(echo_schema_parameters())), 30);
assert!(!r.valid);
assert!(r
.issues
.iter()
.any(|i| i.message.contains("wrong type") && i.message.contains("msg")));
}
#[test]
fn schema_verify_rejects_missing_required() {
let p = echo_call(HashMap::new());
let r = verify_with_schemas(&p, None, Some(&schema_map(echo_schema_parameters())), 30);
assert!(!r.valid);
assert!(
r.issues
.iter()
.any(|i| i.message.contains("missing required parameter")
&& i.message.contains("msg"))
);
}
#[test]
fn schema_verify_rejects_unknown_tool() {
let mut a = tool_call("a1", "nope");
a.parameters = [("msg".to_string(), Value::from("hi"))].into();
let r = verify_with_schemas(
&prop(vec![a]),
None,
Some(&schema_map(echo_schema_parameters())),
30,
);
assert!(!r.valid);
assert!(r
.issues
.iter()
.any(|i| i.message.contains("not registered")));
}
#[test]
fn name_only_verify_still_skips_param_validation() {
let p = echo_call([("msg".to_string(), Value::from(42))].into());
let r = verify(&p, None, Some(&["echo".to_string()].into()), 30);
assert!(
r.valid,
"name-only verify must not validate params: {:?}",
r.issues
);
}
#[test]
fn schema_verify_accepts_integer_and_union_types() {
let parameters = serde_json::json!({
"type": "object",
"properties": {
"n": { "type": "integer" },
"maybe": { "type": ["string", "null"] },
},
"required": ["n"],
});
let p = echo_call(
[
("n".to_string(), Value::from(7)),
("maybe".to_string(), Value::Null),
]
.into(),
);
let r = verify_with_schemas(&p, None, Some(&schema_map(parameters)), 30);
assert!(r.valid, "{:?}", r.issues);
}
#[test]
fn schema_verify_empty_schema_imposes_no_constraints() {
let p = echo_call([("anything".to_string(), Value::from(42))].into());
let r = verify_with_schemas(&p, None, Some(&schema_map(serde_json::json!({}))), 30);
assert!(r.valid, "{:?}", r.issues);
}
#[test]
fn verify_catches_unsatisfied_precondition() {
let mut a = tool_call("a1", "deploy");
a.preconditions = vec![Precondition {
key: "tests_passed".to_string(),
operator: "eq".to_string(),
value: Value::Bool(true),
description: String::new(),
}];
let r = verify(&prop(vec![a]), None, None, 30);
assert!(!r.valid);
}
#[test]
fn verify_precondition_satisfied_by_earlier_action() {
let mut a2 = tool_call("a2", "deploy");
a2.preconditions = vec![Precondition {
key: "ready".to_string(),
operator: "eq".to_string(),
value: Value::Bool(true),
description: String::new(),
}];
a2.state_dependencies = vec!["ready".to_string()];
let p = prop(vec![state_write("a1", "ready", Value::Bool(true)), a2]);
let r = verify(&p, None, None, 30);
assert!(r.valid);
}
#[test]
fn verify_missing_state_dependency() {
let mut a = tool_call("a1", "x");
a.state_dependencies = vec!["nonexistent".to_string()];
let r = verify(&prop(vec![a]), None, None, 30);
assert!(!r.valid);
}
#[test]
fn verify_tool_not_registered() {
let a = tool_call("a1", "quantum");
let r = verify(&prop(vec![a]), None, Some(&HashSet::new()), 30);
assert!(!r.valid);
}
#[test]
fn compensation_naming_an_unregistered_tool_is_a_finding() {
let mut a = tool_call("a1", "poll");
a.reversibility = car_ir::Reversibility::Compensable;
a.compensation = Some(car_ir::Compensation::Tool {
tool: "db.delet".into(), parameters: Default::default(),
});
let r = verify(&prop(vec![a]), None, Some(&["poll".to_string()].into()), 30);
assert!(!r.valid);
assert!(r
.issues
.iter()
.any(|i| i.message.contains("compensation names tool 'db.delet'")));
let mut a = tool_call("a1", "poll");
a.reversibility = car_ir::Reversibility::Compensable;
a.compensation = Some(car_ir::Compensation::Tool {
tool: "undo".into(),
parameters: Default::default(),
});
let reg = ["poll".to_string(), "undo".to_string()].into();
assert!(verify(&prop(vec![a]), None, Some(®), 30).valid);
}
#[test]
fn compensation_action_ref_must_resolve_within_the_proposal() {
let mut a = tool_call("a1", "deploy");
a.reversibility = car_ir::Reversibility::Compensable;
a.compensation = Some(car_ir::Compensation::ActionRef {
action_id: "rollback-1".into(),
});
let r = verify(&prop(vec![a.clone()]), None, None, 30);
assert!(!r.valid);
assert!(r
.issues
.iter()
.any(|i| i.message.contains("references action 'rollback-1'")));
let mut undo = tool_call("rollback-1", "rollback");
undo.id = "rollback-1".into();
let r = verify(&prop(vec![a, undo]), None, None, 30);
assert!(
!r.issues
.iter()
.any(|i| i.message.contains("references action")),
"{:?}",
r.issues
);
}
#[test]
fn compensable_with_no_compensation_declared_is_a_finding() {
let mut a = tool_call("a1", "poll");
a.reversibility = car_ir::Reversibility::Compensable;
a.compensation = None;
let r = verify(&prop(vec![a]), None, None, 30);
assert!(!r.valid);
assert!(r.issues.iter().any(|i| i
.message
.contains("declares reversibility 'compensable' but no compensation")));
assert!(r
.issues_with_tier(EvidenceTier::DecisionProcedure)
.iter()
.any(|i| i.message.contains("no compensation")));
let rec = r
.evidence
.checks
.iter()
.find(|c| c.name == "compensation_resolution")
.expect("compensation_resolution check is recorded");
assert!(rec.ran);
assert_eq!(rec.findings, 1);
}
#[test]
fn verify_no_tool_specified() {
let mut a = tool_call("a1", "x");
a.tool = None;
let r = verify(&prop(vec![a]), None, None, 30);
assert!(!r.valid);
}
#[test]
fn detect_write_conflict() {
let p = prop(vec![
state_write("a1", "x", Value::from(1)),
state_write("a2", "x", Value::from(2)),
]);
let r = verify(&p, None, None, 30);
assert!(!r.conflicts.is_empty());
}
#[test]
fn simulate_state_writes() {
let p = prop(vec![
state_write("a1", "x", Value::from(10)),
state_write("a2", "y", Value::from(20)),
]);
let s = simulate(&p, None);
assert_eq!(s.get("x"), Some(&Value::from(10)));
assert_eq!(s.get("y"), Some(&Value::from(20)));
}
#[test]
fn simulate_skips_effects_of_a_provably_blocked_action() {
let mut deploy = tool_call("deploy", "deploy");
deploy.preconditions = vec![Precondition {
key: "tests_passed".to_string(),
operator: "eq".to_string(),
value: Value::Bool(true),
description: String::new(),
}];
deploy
.expected_effects
.insert("deployed".to_string(), Value::Bool(true));
let p = prop(vec![deploy]);
let failing: HashMap<String, Value> =
[("tests_passed".to_string(), Value::Bool(false))].into();
let s = simulate(&p, Some(&failing));
assert_eq!(
s.get("deployed"),
None,
"a deploy whose precondition provably fails must not appear deployed: {s:?}"
);
let passing: HashMap<String, Value> =
[("tests_passed".to_string(), Value::Bool(true))].into();
let s = simulate(&p, Some(&passing));
assert_eq!(s.get("deployed"), Some(&Value::Bool(true)));
}
#[test]
fn verify_stays_optimistic_so_it_reports_every_finding() {
let mut deploy = tool_call("deploy", "deploy");
deploy.preconditions = vec![Precondition {
key: "tests_passed".to_string(),
operator: "eq".to_string(),
value: Value::Bool(true),
description: String::new(),
}];
deploy
.expected_effects
.insert("deployed".to_string(), Value::Bool(true));
let mut notify = tool_call("notify", "notify");
notify.state_dependencies = vec!["deployed".to_string()];
let p = prop(vec![deploy, notify]);
let failing: HashMap<String, Value> =
[("tests_passed".to_string(), Value::Bool(false))].into();
let r = verify(&p, Some(&failing), None, 30);
assert!(!r.valid);
assert_eq!(
r.errors().len(),
1,
"expected only the precondition finding, got {:?}",
r.issues
);
assert!(r.issues[0].message.contains("precondition will fail"));
}
#[test]
fn simulate_cascade_follows_data_dependencies() {
let mut build = tool_call("build", "build");
build.preconditions = vec![Precondition {
key: "ready".to_string(),
operator: "eq".to_string(),
value: Value::Bool(true),
description: String::new(),
}];
build
.expected_effects
.insert("artifact".to_string(), Value::from("app.tar.gz"));
let mut deploy = tool_call("deploy", "deploy");
deploy.state_dependencies = vec!["artifact".to_string()];
deploy
.expected_effects
.insert("deployed".to_string(), Value::Bool(true));
let s = simulate(&prop(vec![build, deploy]), None);
assert_eq!(
s.get("artifact"),
None,
"blocked build produced no artifact"
);
assert_eq!(
s.get("deployed"),
None,
"deploy depends on the artifact that never appeared: {s:?}"
);
}
#[test]
fn equivalent_distinguishes_a_gated_proposal_from_an_ungated_one() {
let mut gated = tool_call("a", "deploy");
gated.preconditions = vec![Precondition {
key: "tests_passed".to_string(),
operator: "eq".to_string(),
value: Value::Bool(true),
description: String::new(),
}];
gated
.expected_effects
.insert("deployed".to_string(), Value::Bool(true));
let mut ungated = tool_call("b", "deploy");
ungated
.expected_effects
.insert("deployed".to_string(), Value::Bool(true));
let failing: Vec<HashMap<String, Value>> =
vec![[("tests_passed".to_string(), Value::Bool(false))].into()];
assert!(
!equivalent(&prop(vec![gated]), &prop(vec![ungated]), Some(&failing)),
"a gate that blocks one proposal and not the other is a real difference"
);
}
#[test]
fn equivalent_proposals() {
let p1 = prop(vec![
state_write("a1", "x", Value::from(1)),
state_write("a2", "y", Value::from(2)),
]);
let p2 = prop(vec![
state_write("b1", "y", Value::from(2)),
state_write("b2", "x", Value::from(1)),
]);
assert!(equivalent(&p1, &p2, None));
}
#[test]
fn non_equivalent_proposals() {
let p1 = prop(vec![state_write("a1", "x", Value::from(1))]);
let p2 = prop(vec![state_write("b1", "x", Value::from(99))]);
assert!(!equivalent(&p1, &p2, None));
}
#[test]
fn optimize_removes_phantom_deps() {
let mut a = tool_call("a1", "search");
a.state_dependencies = vec!["phantom".to_string()];
let p = prop(vec![a]);
let optimized = optimize(&p);
assert!(optimized.actions[0].state_dependencies.is_empty());
}
#[test]
fn optimize_preserves_real_deps() {
let mut a2 = tool_call("a2", "x");
a2.state_dependencies = vec!["x".to_string()];
let p = prop(vec![state_write("a1", "x", Value::from(1)), a2]);
let optimized = optimize(&p);
assert_eq!(optimized.actions[1].state_dependencies, vec!["x"]);
}
#[test]
fn loop_detection_duplicates() {
let p = prop(vec![tool_call("a1", "search"), tool_call("a2", "search")]);
let r = verify(&p, None, None, 30);
assert!(r.issues.iter().any(|i| i.message.contains("duplicate")));
}
#[test]
fn loop_detection_triple() {
let p = prop(vec![
tool_call("a1", "search"),
tool_call("a2", "search"),
tool_call("a3", "search"),
]);
let r = verify(&p, None, None, 30);
assert!(!r.valid);
assert!(r.issues.iter().any(|i| i.message.contains("likely loop")));
}
#[test]
fn resource_bounds() {
let actions: Vec<Action> = (0..35)
.map(|i| tool_call(&format!("a{}", i), &format!("t{}", i)))
.collect();
let r = verify(&prop(actions), None, None, 30);
assert!(r.issues.iter().any(|i| i.message.contains("excessive")));
}
#[test]
fn evidence_declares_all_check_scopes() {
let p = echo_call([("msg".to_string(), Value::from("hi"))].into());
let r = verify_with_schemas(&p, None, Some(&schema_map(echo_schema_parameters())), 30);
for want in [
"resource_bounds",
"loop_detection",
"preconditions",
"state_dependencies",
"tool_existence",
"param_schema",
"write_conflicts",
] {
let rec = r
.evidence
.checks
.iter()
.find(|c| c.name == want)
.unwrap_or_else(|| panic!("missing check record {want}"));
assert!(!rec.verifies.is_empty());
assert!(!rec.cannot_verify.is_empty());
}
let by = |n: &str| r.evidence.checks.iter().find(|c| c.name == n).unwrap();
assert!(by("param_schema").ran);
assert!(by("tool_existence").ran);
}
#[test]
fn evidence_marks_param_schema_skipped_without_schemas() {
let p = prop(vec![tool_call("a1", "search")]);
let r = verify(&p, None, None, 30);
let param = r
.evidence
.checks
.iter()
.find(|c| c.name == "param_schema")
.unwrap();
assert!(!param.ran);
assert!(
r.evidence.confidence < 1.0,
"skipped check should dock coverage"
);
assert!(r
.evidence
.assumptions
.iter()
.any(|a| a.contains("well-formed")));
}
#[test]
fn evidence_full_confidence_for_pure_state_writes() {
let p = prop(vec![state_write("a1", "x", Value::from(1))]);
let r = verify(&p, None, None, 30);
assert!(r.valid);
assert_eq!(r.evidence.confidence, 1.0);
assert!(r.evidence.untested_regions.is_empty());
}
#[test]
fn evidence_conflicts_become_residual_risk() {
let p = prop(vec![
state_write("a1", "k", Value::from(1)),
state_write("a2", "k", Value::from(2)),
]);
let r = verify(&p, None, None, 30);
assert!(r.valid, "conflicts are warnings, not errors");
assert!(!r.conflicts.is_empty());
assert!(r
.evidence
.residual_risks
.iter()
.any(|s| s.contains("write conflict")));
let wc = r
.evidence
.checks
.iter()
.find(|c| c.name == "write_conflicts")
.unwrap();
assert_eq!(wc.findings, r.conflicts.len());
}
#[test]
fn evidence_untested_includes_runtime_set_effect_keys() {
let mut a = tool_call("a1", "fetch");
a.expected_effects = [("out".to_string(), Value::from("placeholder"))].into();
let r = verify(
&prop(vec![a]),
None,
Some(&["fetch".to_string()].into()),
30,
);
assert!(r
.evidence
.untested_regions
.iter()
.any(|s| s.contains("state key 'out'")));
assert!(r
.evidence
.untested_regions
.iter()
.any(|s| s.contains("runtime output of tool 'fetch'")));
}
#[test]
fn evidence_tool_existence_ran_consistent_with_findings() {
let mut a = tool_call("a1", "x");
a.tool = None;
let r = verify(&prop(vec![a]), None, None, 30);
assert!(!r.valid);
let te = r
.evidence
.checks
.iter()
.find(|c| c.name == "tool_existence")
.unwrap();
assert!(te.findings >= 1);
assert!(
te.ran,
"ran must be true whenever the check produced a finding"
);
}
#[test]
fn loop_detection_findings_are_heuristic_and_the_rest_are_not() {
let p = prop(vec![
tool_call("a1", "poll"),
tool_call("a2", "poll"),
tool_call("a3", "poll"),
tool_call("a4", "ghost"),
]);
let r = verify(&p, None, Some(&["poll".to_string()].into()), 30);
let heuristic = r.issues_with_tier(EvidenceTier::Heuristic);
assert_eq!(
heuristic.len(),
1,
"only the repeated-call finding is heuristic: {:?}",
r.issues
);
assert!(heuristic[0]
.message
.contains("repeated identical tool call"));
let decided = r.issues_with_tier(EvidenceTier::DecisionProcedure);
assert!(decided
.iter()
.any(|i| i.message.contains("'ghost' is not registered")));
assert!(r.issues_with_tier(EvidenceTier::Sampled).is_empty());
}
#[test]
fn check_records_and_issues_agree_on_tier() {
let p = prop(vec![
tool_call("a1", "poll"),
tool_call("a2", "poll"),
{
let mut a = tool_call("a3", "ghost");
a.state_dependencies = vec!["missing".to_string()];
a
},
state_write("a4", "x", Value::from(1)),
state_write("a5", "x", Value::from(2)),
]);
let r = verify(&p, None, Some(&["poll".to_string()].into()), 30);
for tier in [
EvidenceTier::DecisionProcedure,
EvidenceTier::Heuristic,
EvidenceTier::Sampled,
] {
let declared: usize = r
.evidence
.checks
.iter()
.filter(|c| c.tier == tier)
.map(|c| c.findings)
.sum();
let actual = r.issues_with_tier(tier).len();
assert_eq!(
declared,
actual,
"checks at tier {} declare {declared} findings but {actual} issues carry it: {:?}",
tier.as_str(),
r.issues
);
}
let total: usize = r.evidence.checks.iter().map(|c| c.findings).sum();
assert_eq!(total, r.issues.len(), "unaccounted issues: {:?}", r.issues);
assert_eq!(
r.issues_with_tier(EvidenceTier::Heuristic).len(),
1,
"expected exactly the duplicate-call finding: {:?}",
r.issues
);
assert!(
r.issues_with_tier(EvidenceTier::DecisionProcedure).len() >= 3,
"expected the unregistered tool, the missing dependency, and the \
write conflict: {:?}",
r.issues
);
}
#[test]
fn tier_serializes_as_stable_snake_case() {
let p = prop(vec![tool_call("a1", "ghost")]);
let r = verify(&p, None, Some(&HashSet::new()), 30);
let json = serde_json::to_value(&r.issues[0]).expect("issue serializes");
assert_eq!(json["tier"], Value::from("decision_procedure"));
assert_eq!(
json["tier"],
Value::from(r.issues[0].tier.as_str()),
"as_str and the serde representation must not drift"
);
}
}