use crate::json::JsonValue;
use crate::models::LinkRole;
use crate::models::LinkSet;
use crate::models::StatementGroundLink;
use crate::normative::models::GroundSet;
use crate::normative::models::License;
use crate::normative::models::Modality;
use crate::normative::models::Scope;
use std::collections::BTreeSet;
pub struct LicenseDeriver;
impl LicenseDeriver {
pub fn derive(&self, ground_set: &GroundSet, links: Option<&LinkSet>) -> License {
match links {
Some(link_set) => self.derive_with_links(ground_set, link_set),
None => self.derive_conservative(ground_set),
}
}
fn derive_conservative(&self, ground_set: &GroundSet) -> License {
if ground_set.is_empty() {
return license_from([Modality::Refusal]);
}
let factual_strength = ground_set.get_scope_strength(Scope::Factual);
match factual_strength.as_deref() {
None => license_from([Modality::Refusal]),
Some("strong") => license_from([
Modality::Assertive,
Modality::Conditional,
Modality::Refusal,
]),
Some(_) => license_from([Modality::Conditional, Modality::Refusal]),
}
}
fn derive_with_links(&self, ground_set: &GroundSet, links: &LinkSet) -> License {
let support_links: Vec<&StatementGroundLink> = links
.links
.iter()
.filter(|link| link.role == LinkRole::Supports)
.collect();
if support_links.is_empty() {
return license_from([Modality::Refusal]);
}
let mut used = Vec::new();
for link in support_links {
if let Some(g) = ground_set.resolve_ground(&link.ground_id) {
used.push(g);
}
}
if used.is_empty() {
return license_from([Modality::Refusal]);
}
let factual: Vec<_> = used
.into_iter()
.filter(|g| g.scope == Scope::Factual)
.collect();
if factual.is_empty() {
return license_from([Modality::Refusal]);
}
if factual.iter().any(|g| g.strength == "strong") {
return license_from([
Modality::Assertive,
Modality::Conditional,
Modality::Refusal,
]);
}
license_from([Modality::Conditional, Modality::Refusal])
}
pub fn derive_with_trace(
&self,
ground_set: &GroundSet,
links: Option<&LinkSet>,
) -> (License, JsonValue) {
let license = self.derive(ground_set, links);
let mut obj = std::collections::BTreeMap::new();
obj.insert(
"mode".to_string(),
JsonValue::String(
if links.is_some() {
"links"
} else {
"conservative"
}
.to_string(),
),
);
obj.insert(
"ground_set_size".to_string(),
JsonValue::Number(ground_set.nodes.len() as f64),
);
obj.insert(
"is_empty".to_string(),
JsonValue::Bool(ground_set.is_empty()),
);
let mut factual = std::collections::BTreeMap::new();
factual.insert(
"present".to_string(),
JsonValue::Bool(ground_set.has_scope(Scope::Factual)),
);
factual.insert(
"strength".to_string(),
match ground_set.get_scope_strength(Scope::Factual) {
Some(v) => JsonValue::String(v),
None => JsonValue::Null,
},
);
factual.insert(
"has_strong".to_string(),
JsonValue::Bool(ground_set.has_strong_in_scope(Scope::Factual)),
);
obj.insert("factual".to_string(), JsonValue::Object(factual));
obj.insert(
"permitted_modalities".to_string(),
JsonValue::Array(
license
.permitted_modalities
.iter()
.map(|m| JsonValue::String(m.as_str().to_string()))
.collect(),
),
);
if let Some(linkset) = links {
let supports_count = linkset
.links
.iter()
.filter(|l| l.role == LinkRole::Supports)
.count();
obj.insert(
"supports_links_count".to_string(),
JsonValue::Number(supports_count as f64),
);
}
(license, JsonValue::Object(obj))
}
}
fn license_from<const N: usize>(modalities: [Modality; N]) -> License {
let mut set = BTreeSet::new();
for m in modalities {
set.insert(m);
}
License {
permitted_modalities: set,
}
}
#[cfg(test)]
mod tests {
use super::LicenseDeriver;
use crate::json::JsonValue;
use crate::models::CreatorType;
use crate::models::EvidenceType;
use crate::models::LinkRole;
use crate::models::LinkSet;
use crate::models::Provenance;
use crate::models::StatementGroundLink;
use crate::normative::models::GroundSet;
use crate::normative::models::KnowledgeNode;
use crate::normative::models::Modality;
use crate::normative::models::Scope;
use crate::normative::models::Source;
use crate::normative::models::Status;
fn node(id: &str, scope: Scope, strength: &str) -> KnowledgeNode {
KnowledgeNode::new(
id.to_string(),
Source::Observed,
Status::Confirmed,
1.0,
scope,
strength.to_string(),
Some(format!("sem_{id}")),
)
.expect("must create node")
}
#[test]
fn license_with_links_strong_supports_assertive() {
let deriver = LicenseDeriver;
let ground_set = GroundSet {
nodes: vec![node("n1", Scope::Factual, "strong")],
};
let link = StatementGroundLink {
statement_id: "s1".to_string(),
ground_id: "n1".to_string(),
role: LinkRole::Supports,
provenance: Provenance {
creator: CreatorType::Human,
evidence_type: EvidenceType::Explicit,
evidence_content: None,
signature: None,
},
};
let license = deriver.derive(&ground_set, Some(&LinkSet { links: vec![link] }));
assert!(license.permits(Modality::Assertive));
}
#[test]
fn conservative_empty_only_refusal() {
let deriver = LicenseDeriver;
let license = deriver.derive(&GroundSet { nodes: vec![] }, None);
assert!(license.permits(Modality::Refusal));
assert!(!license.permits(Modality::Assertive));
}
#[test]
fn derive_with_trace_sets_mode_from_links_presence() {
let deriver = LicenseDeriver;
let ground_set = GroundSet { nodes: vec![] };
let (_, trace_no_links) = deriver.derive_with_trace(&ground_set, None);
let JsonValue::Object(no_links_obj) = trace_no_links else {
panic!("trace object expected")
};
assert_eq!(
no_links_obj.get("mode"),
Some(&JsonValue::String("conservative".to_string()))
);
let links = LinkSet {
links: vec![StatementGroundLink {
statement_id: "s1".to_string(),
ground_id: "missing".to_string(),
role: LinkRole::Supports,
provenance: Provenance {
creator: CreatorType::Human,
evidence_type: EvidenceType::Explicit,
evidence_content: None,
signature: None,
},
}],
};
let (_, trace_links) = deriver.derive_with_trace(&ground_set, Some(&links));
let JsonValue::Object(links_obj) = trace_links else {
panic!("trace object expected")
};
assert_eq!(
links_obj.get("mode"),
Some(&JsonValue::String("links".to_string()))
);
}
}