use crate::ir_nodes::IRProgram;
use super::effects;
use super::proof_term::{
CapabilityContainmentWitness, CapabilityIsolationWitness, ComplianceCoverageWitness,
EffectRowSoundnessWitness, ProofTerm, PropertyClass, ResourceBoundsWitness,
ShieldHaltGuaranteeWitness, ToolCallSoundnessWitness, Witness, MAX_RETRIES,
VALID_BREACH_POLICIES,
};
pub fn artifact_digest(ir: &IRProgram) -> String {
match serde_json::to_value(ir) {
Ok(v) => crate::esk::provenance::content_hash(&v),
Err(_) => "<ir-unserializable>".to_string(),
}
}
fn canonical_classes(raw: &[String]) -> Vec<String> {
let mut v: Vec<String> = raw.to_vec();
v.sort();
v.dedup();
v
}
pub fn derive_compliance_coverage_witness(
endpoint_name: &str,
declared_compliance: &[String],
shield_ref: &str,
ir: &IRProgram,
) -> ComplianceCoverageWitness {
let required_classes = canonical_classes(declared_compliance);
let resolved_shield = if shield_ref.is_empty() {
None
} else {
ir.shields.iter().find(|s| s.name == shield_ref)
};
let shield_present = resolved_shield.is_some();
let provided_classes = resolved_shield
.map(|s| canonical_classes(&s.compliance))
.unwrap_or_default();
let unknown_classes: Vec<String> = required_classes
.iter()
.filter(|c| !crate::esk::compliance::is_known(c))
.cloned()
.collect();
let mut uncovered_classes: Vec<String> =
crate::esk::compliance::covers(provided_classes.iter(), required_classes.iter())
.into_iter()
.collect();
uncovered_classes.sort();
ComplianceCoverageWitness {
endpoint_name: endpoint_name.to_string(),
required_classes,
shield_ref: shield_ref.to_string(),
shield_present,
provided_classes,
unknown_classes,
uncovered_classes,
}
}
pub fn generate_compliance_coverage_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let mut proofs = Vec::new();
for ep in &ir.endpoints {
if ep.compliance.is_empty() {
continue;
}
let witness =
derive_compliance_coverage_witness(&ep.name, &ep.compliance, &ep.shield_ref, ir);
proofs.push(ProofTerm {
property: PropertyClass::ComplianceCoverage,
artifact_digest: digest.clone(),
witness: Witness::ComplianceCoverage(witness),
axon_version: axon_version.to_string(),
});
}
proofs
}
pub fn derive_effect_row_soundness_witness(
tool_name: &str,
effect_row: &[String],
extension_effect_members: &std::collections::HashSet<String>,
) -> EffectRowSoundnessWitness {
let declared_effects = canonical_classes(effect_row);
let mut unknown_bases = Vec::new();
let mut missing_qualifier = Vec::new();
let mut invalid_stream_qualifier = Vec::new();
let mut has_pure = false;
let mut has_other = false;
for entry in &declared_effects {
if extension_effect_members.contains(entry) || effects::is_epistemic_provenance(entry) {
has_other = true;
continue;
}
let (base, qualifier) = effects::split_effect(entry);
if !effects::is_known_base(base) {
unknown_bases.push(entry.clone());
has_other = true;
continue;
}
if base == "pure" {
has_pure = true;
} else {
has_other = true;
}
if effects::requires_qualifier(base) && qualifier.is_none() {
missing_qualifier.push(entry.clone());
}
if base == "stream" {
if let Some(q) = qualifier {
if !effects::is_valid_stream_qualifier(q) {
invalid_stream_qualifier.push(entry.clone());
}
}
}
}
let purity_violation = has_pure && has_other;
EffectRowSoundnessWitness {
tool_name: tool_name.to_string(),
declared_effects,
unknown_bases,
missing_qualifier,
invalid_stream_qualifier,
purity_violation,
}
}
pub fn extension_effect_members(ir: &IRProgram) -> std::collections::HashSet<String> {
let mut set = std::collections::HashSet::new();
for ext in &ir.extensions {
if ext.category != "effects" {
continue;
}
for m in &ext.members {
let (base, _) = effects::split_effect(&m.name);
if !effects::is_known_base(base) {
set.insert(m.name.clone());
}
}
}
set
}
pub fn generate_effect_row_soundness_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let ext_members = extension_effect_members(ir);
let mut proofs = Vec::new();
for tool in &ir.tools {
if tool.effect_row.is_empty() {
continue;
}
let witness =
derive_effect_row_soundness_witness(&tool.name, &tool.effect_row, &ext_members);
proofs.push(ProofTerm {
property: PropertyClass::EffectRowSoundness,
artifact_digest: digest.clone(),
witness: Witness::EffectRowSoundness(witness),
axon_version: axon_version.to_string(),
});
}
proofs
}
pub fn derive_capability_isolation_witness(
store_name: &str,
capability: &str,
) -> CapabilityIsolationWitness {
let malformed = !capability.is_empty() && !crate::parser::is_valid_capability_slug(capability);
CapabilityIsolationWitness {
store_name: store_name.to_string(),
capability: capability.to_string(),
malformed,
}
}
pub fn generate_capability_isolation_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let mut proofs = Vec::new();
for store in &ir.axonstore_specs {
if store.capability.is_empty() {
continue;
}
let witness = derive_capability_isolation_witness(&store.name, &store.capability);
proofs.push(ProofTerm {
property: PropertyClass::CapabilityIsolation,
artifact_digest: digest.clone(),
witness: Witness::CapabilityIsolation(witness),
axon_version: axon_version.to_string(),
});
}
proofs
}
pub fn derive_endpoint_retry_witness(endpoint_name: &str, retries: i64) -> ResourceBoundsWitness {
ResourceBoundsWitness::EndpointRetry {
endpoint_name: endpoint_name.to_string(),
retries,
in_bounds: (0..=MAX_RETRIES).contains(&retries),
}
}
pub fn derive_socket_credit_witness(socket_name: &str, credit: i64) -> ResourceBoundsWitness {
ResourceBoundsWitness::SocketCredit {
socket_name: socket_name.to_string(),
credit,
positive: credit >= 1,
}
}
pub fn generate_resource_bounds_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let mut proofs = Vec::new();
for ep in &ir.endpoints {
let witness = derive_endpoint_retry_witness(&ep.name, ep.retries);
proofs.push(ProofTerm {
property: PropertyClass::ResourceBounds,
artifact_digest: digest.clone(),
witness: Witness::ResourceBounds(witness),
axon_version: axon_version.to_string(),
});
}
for socket in &ir.sockets {
if let Some(credit) = socket.backpressure_credit {
let witness = derive_socket_credit_witness(&socket.name, credit);
proofs.push(ProofTerm {
property: PropertyClass::ResourceBounds,
artifact_digest: digest.clone(),
witness: Witness::ResourceBounds(witness),
axon_version: axon_version.to_string(),
});
}
}
proofs
}
pub fn derive_shield_halt_witness(
shield_name: &str,
on_breach: &str,
scan: &[String],
) -> ShieldHaltGuaranteeWitness {
let known_policy = VALID_BREACH_POLICIES.contains(&on_breach);
let scan_count = scan.len();
let vacuous_halt = on_breach == "halt" && scan.is_empty();
ShieldHaltGuaranteeWitness {
shield_name: shield_name.to_string(),
on_breach: on_breach.to_string(),
known_policy,
scan_count,
vacuous_halt,
}
}
pub fn generate_shield_halt_guarantee_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let mut proofs = Vec::new();
for shield in &ir.shields {
if shield.on_breach.is_empty() {
continue;
}
let witness = derive_shield_halt_witness(&shield.name, &shield.on_breach, &shield.scan);
proofs.push(ProofTerm {
property: PropertyClass::ShieldHaltGuarantee,
artifact_digest: digest.clone(),
witness: Witness::ShieldHaltGuarantee(witness),
axon_version: axon_version.to_string(),
});
}
proofs
}
fn collect_store_accesses(steps: &[crate::ir_nodes::IRFlowNode], out: &mut Vec<String>) {
use crate::ir_nodes::IRFlowNode as N;
for step in steps {
match step {
N::Retrieve(s) => out.push(s.store_name.clone()),
N::Persist(s) => out.push(s.store_name.clone()),
N::Mutate(s) => out.push(s.store_name.clone()),
N::Purge(s) => out.push(s.store_name.clone()),
N::Conditional(c) => {
collect_store_accesses(&c.then_body, out);
collect_store_accesses(&c.else_body, out);
}
N::ForIn(f) => collect_store_accesses(&f.body, out),
N::Step(_)
| N::Probe(_)
| N::Reason(_)
| N::Validate(_)
| N::Refine(_)
| N::Weave(_)
| N::UseTool(_)
| N::Remember(_)
| N::Recall(_)
| N::Let(_)
| N::Return(_)
| N::Break(_)
| N::Continue(_)
| N::LambdaDataApply(_)
| N::Par(_)
| N::Hibernate(_)
| N::Deliberate(_)
| N::Consensus(_)
| N::Forge(_)
| N::Focus(_)
| N::Associate(_)
| N::Aggregate(_)
| N::Explore(_)
| N::Ingest(_)
| N::ShieldApply(_)
| N::Stream(_)
| N::Navigate(_)
| N::Drill(_)
| N::Trail(_)
| N::Corroborate(_)
| N::OtsApply(_)
| N::MandateApply(_)
| N::ComputeApply(_)
| N::Listen(_)
| N::DaemonStep(_)
| N::Emit(_)
| N::Publish(_)
| N::Discover(_)
| N::Transact(_) => {}
}
}
}
pub fn derive_capability_containment_witness(
endpoint_name: &str,
execute_flow: &str,
declared_requires_raw: &[String],
ir: &IRProgram,
) -> CapabilityContainmentWitness {
let declared_requires = canonical_classes(declared_requires_raw);
let flow = ir.flows.iter().find(|f| f.name == execute_flow);
let flow_resolved = flow.is_some();
let mut reached_stores: Vec<String> = Vec::new();
if let Some(f) = flow {
collect_store_accesses(&f.steps, &mut reached_stores);
}
let mut reached_gates: Vec<String> = reached_stores
.iter()
.filter_map(|name| {
ir.axonstore_specs
.iter()
.find(|s| &s.name == name)
.map(|s| s.capability.clone())
})
.filter(|cap| !cap.is_empty())
.collect();
reached_gates.sort();
reached_gates.dedup();
let mut uncovered_gates: Vec<String> =
crate::esk::compliance::covers(declared_requires.iter(), reached_gates.iter())
.into_iter()
.collect();
uncovered_gates.sort();
CapabilityContainmentWitness {
endpoint_name: endpoint_name.to_string(),
execute_flow: execute_flow.to_string(),
flow_resolved,
declared_requires,
reached_gates,
uncovered_gates,
}
}
pub fn generate_capability_containment_proofs(
ir: &IRProgram,
axon_version: &str,
) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let mut proofs = Vec::new();
for ep in &ir.endpoints {
let witness = derive_capability_containment_witness(
&ep.name,
&ep.execute_flow,
&ep.requires_capabilities,
ir,
);
if witness.declared_requires.is_empty() && witness.reached_gates.is_empty() {
continue;
}
proofs.push(ProofTerm {
property: PropertyClass::CapabilityContainment,
artifact_digest: digest.clone(),
witness: Witness::CapabilityContainment(witness),
axon_version: axon_version.to_string(),
});
}
proofs
}
fn infer_arg_literal_type(value: &str) -> Option<&'static str> {
if value == "true" || value == "false" {
return Some("Bool");
}
if value.contains('.') && value.parse::<f64>().is_ok() {
return Some("Float");
}
let digits = value.strip_prefix('-').unwrap_or(value);
if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) {
return Some("Int");
}
None
}
fn tool_arg_types_align(value_ty: &str, decl_ty: &str) -> bool {
let base = decl_ty.trim_end_matches('?').split('<').next().unwrap_or(decl_ty);
base == "Any" || base == value_ty || (base == "Float" && value_ty == "Int")
}
fn collect_named_use_tool_calls<'a>(
steps: &'a [crate::ir_nodes::IRFlowNode],
out: &mut Vec<&'a crate::ir_nodes::IRUseToolStep>,
) {
use crate::ir_nodes::IRFlowNode as N;
for step in steps {
match step {
N::UseTool(u) => {
if !u.named_args.is_empty() {
out.push(u);
}
}
N::Conditional(c) => {
collect_named_use_tool_calls(&c.then_body, out);
collect_named_use_tool_calls(&c.else_body, out);
}
N::ForIn(f) => collect_named_use_tool_calls(&f.body, out),
N::Step(_)
| N::Probe(_)
| N::Reason(_)
| N::Validate(_)
| N::Refine(_)
| N::Weave(_)
| N::Remember(_)
| N::Recall(_)
| N::Let(_)
| N::Return(_)
| N::Break(_)
| N::Continue(_)
| N::LambdaDataApply(_)
| N::Par(_)
| N::Hibernate(_)
| N::Deliberate(_)
| N::Consensus(_)
| N::Forge(_)
| N::Focus(_)
| N::Associate(_)
| N::Aggregate(_)
| N::Explore(_)
| N::Ingest(_)
| N::ShieldApply(_)
| N::Stream(_)
| N::Navigate(_)
| N::Drill(_)
| N::Trail(_)
| N::Corroborate(_)
| N::OtsApply(_)
| N::MandateApply(_)
| N::ComputeApply(_)
| N::Listen(_)
| N::DaemonStep(_)
| N::Emit(_)
| N::Publish(_)
| N::Discover(_)
| N::Retrieve(_)
| N::Persist(_)
| N::Mutate(_)
| N::Purge(_)
| N::Transact(_) => {}
}
}
}
fn canonical_names(raw: &[String]) -> Vec<String> {
let mut v = raw.to_vec();
v.sort();
v.dedup();
v
}
pub fn derive_tool_call_soundness_witness(
flow_name: &str,
call_index: usize,
ir: &IRProgram,
) -> Option<ToolCallSoundnessWitness> {
let flow = ir.flows.iter().find(|f| f.name == flow_name)?;
let mut calls = Vec::new();
collect_named_use_tool_calls(&flow.steps, &mut calls);
let call = calls.get(call_index)?;
let tool_name = call.tool_name.clone();
let arg_pairs: Vec<(String, String)> = call
.named_args
.iter()
.map(|a| (a.name.clone(), a.value.clone()))
.collect();
let arg_names: Vec<String> = arg_pairs.iter().map(|(n, _)| n.clone()).collect();
let params: Vec<(String, String, bool)> = ir
.tools
.iter()
.find(|t| t.name == tool_name)
.map(|t| {
t.parameters
.iter()
.map(|p| (p.name.clone(), p.type_name.clone(), p.optional))
.collect()
})
.unwrap_or_default();
let schema_present = !params.is_empty();
let declared_params = canonical_names(
¶ms.iter().map(|(n, _, _)| n.clone()).collect::<Vec<_>>(),
);
let mut seen = std::collections::HashSet::new();
let mut dup = std::collections::HashSet::new();
for name in &arg_names {
if !seen.insert(name.clone()) {
dup.insert(name.clone());
}
}
let duplicate_args = canonical_names(&dup.into_iter().collect::<Vec<_>>());
let unknown_args = canonical_names(
&arg_names
.iter()
.filter(|n| !params.iter().any(|(p, _, _)| &p == n))
.cloned()
.collect::<Vec<_>>(),
);
let mut missing_required: Vec<String> = params
.iter()
.filter(|(p, _, optional)| !optional && !arg_names.iter().any(|n| n == p))
.map(|(p, _, _)| p.clone())
.collect();
missing_required.sort();
missing_required.dedup();
let mut type_mismatches: Vec<String> = Vec::new();
for (name, value) in &arg_pairs {
if let Some((_, decl_ty, _)) = params.iter().find(|(p, _, _)| p == name) {
if let Some(val_ty) = infer_arg_literal_type(value) {
if !tool_arg_types_align(val_ty, decl_ty) {
type_mismatches.push(format!("{name}:{decl_ty}:{val_ty}"));
}
}
}
}
type_mismatches.sort();
type_mismatches.dedup();
Some(ToolCallSoundnessWitness {
flow_name: flow_name.to_string(),
call_index,
tool_name,
arg_names,
declared_params,
schema_present,
unknown_args,
duplicate_args,
missing_required,
type_mismatches,
})
}
pub fn generate_tool_call_soundness_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let digest = artifact_digest(ir);
let mut proofs = Vec::new();
for flow in &ir.flows {
let mut calls = Vec::new();
collect_named_use_tool_calls(&flow.steps, &mut calls);
for (call_index, _) in calls.iter().enumerate() {
let Some(witness) = derive_tool_call_soundness_witness(&flow.name, call_index, ir)
else {
continue;
};
if !witness.schema_present {
continue;
}
proofs.push(ProofTerm {
property: PropertyClass::ToolCallSoundness,
artifact_digest: digest.clone(),
witness: Witness::ToolCallSoundness(witness),
axon_version: axon_version.to_string(),
});
}
}
proofs
}
pub fn generate_all_proofs(ir: &IRProgram, axon_version: &str) -> Vec<ProofTerm> {
let mut proofs = Vec::new();
proofs.extend(generate_compliance_coverage_proofs(ir, axon_version));
proofs.extend(generate_effect_row_soundness_proofs(ir, axon_version));
proofs.extend(generate_capability_isolation_proofs(ir, axon_version));
proofs.extend(generate_resource_bounds_proofs(ir, axon_version));
proofs.extend(generate_shield_halt_guarantee_proofs(ir, axon_version));
proofs.extend(generate_capability_containment_proofs(ir, axon_version));
proofs.extend(generate_tool_call_soundness_proofs(ir, axon_version));
proofs
}