use nibli_engine::{EngineLogicBuffer, EngineLogicNode, NibliEngine};
fn fresh_engine() -> NibliEngine {
NibliEngine::new()
}
fn has_predicate_base(buf: &EngineLogicBuffer, base: &str) -> bool {
let role_prefix = format!("{base}_x");
buf.nodes.iter().any(|n| match n {
EngineLogicNode::Predicate((rel, _)) => rel == base || rel.starts_with(&role_prefix),
_ => false,
})
}
#[test] fn implication_tensed_antecedent_must_not_fire_unconditionally() {
let engine = fresh_engine();
if engine
.assert_text("past runs(Adam) -> animal(Adam).")
.is_ok()
{
let r = engine.query_holds("animal(Adam).").unwrap();
assert!(
!r.is_true(),
"tensed antecedent was dropped → rule fired with no supporting fact: {r:?}"
);
}
}
#[test] fn implication_disjunctive_antecedent_must_not_fire_unconditionally() {
let engine = fresh_engine();
if engine
.assert_text("dog(Adam) | cat(Adam) -> animal(Adam).")
.is_ok()
{
let r = engine.query_holds("animal(Adam).").unwrap();
assert!(
!r.is_true(),
"disjunctive antecedent was dropped → rule fired with no supporting fact: {r:?}"
);
}
}
#[test] fn disjunction_assert_must_be_observable() {
let engine = fresh_engine();
let s = "dog(Adam) | cat(Adam).";
if engine.assert_text(s).is_ok() {
let r = engine.query_holds(s).unwrap();
assert!(
r.is_true(),
"disjunction asserted Ok but queries back non-true (nothing was ingested): {r:?}"
);
}
}
#[test] fn xor_assert_must_be_observable() {
let engine = fresh_engine();
let s = "dog(Adam) ^ cat(Adam).";
if engine.assert_text(s).is_ok() {
let r = engine.query_holds(s).unwrap();
assert!(
r.is_true(),
"xor asserted Ok but queries back non-true (nothing was ingested): {r:?}"
);
}
}
#[test] fn negated_ground_fact_then_contrary_positive_is_a_contradiction() {
let engine = fresh_engine();
let neg = engine.assert_text("~dog(Adam).");
if neg.is_ok() {
engine.assert_text("dog(Adam).").unwrap();
assert!(
!engine.check_contradictions().is_empty(),
"negative fact was a silent no-op; the contradiction was not detected"
);
}
}
#[test] fn rel_clause_on_name_must_not_be_dropped() {
let engine = fresh_engine();
engine.assert_text("goes(Adam).").unwrap(); let r = engine.query_holds("goes(Adam where dog).").unwrap();
assert!(
!r.is_true(),
"the `poi gerku` clause was dropped → unsound TRUE for an unproven restriction: {r:?}"
);
}
#[test] fn rel_clause_on_name_positive_direction() {
let engine = fresh_engine();
engine.assert_text("dog(Adam).").unwrap();
engine.assert_text("goes(Adam).").unwrap();
let r = engine.query_holds("goes(Adam where dog).").unwrap();
assert!(
r.is_true(),
"both conjuncts known → restricted query must hold: {r:?}"
);
let engine2 = fresh_engine();
engine2.assert_text("goes(Adam where dog).").unwrap();
let g = engine2.query_holds("dog(Adam).").unwrap();
let k = engine2.query_holds("goes(Adam).").unwrap();
assert!(
g.is_true() && k.is_true(),
"asserting the restricted sentence must assert both conjuncts: gerku={g:?}, klama={k:?}"
);
}
#[test] fn fa_tag_beyond_arity_must_error_not_silently_drop() {
let engine = fresh_engine();
let r = engine.assert_text("fu do gerku");
assert!(
r.is_err(),
"over-arity FA tag silently dropped the bound term instead of reporting an error"
);
}
#[test] fn pair_in_where_clause_must_not_be_falsely_rejected() {
let engine = fresh_engine();
assert!(
engine
.assert_text("goes(some dog where fast runs).")
.is_ok(),
"a valid tanru-in-poi relative clause was rejected by the ambiguity firewall"
);
}
#[test] fn lexer_must_not_silently_truncate_input() {
let engine = fresh_engine();
let s = "goes(me) 7 loves(you). animal(some dog).";
if engine.assert_text(s).is_ok() {
let r = engine.query_holds("animal(some dog).").unwrap();
assert!(
r.is_true(),
"input was silently truncated at `7`; the trailing sentence never reached the KB: {r:?}"
);
}
}
#[test] fn da_after_universal_description_must_not_lose_the_rule() {
let engine = fresh_engine();
engine.assert_text("eats(every dog, $da).").unwrap(); engine.assert_text("dog(Alis).").unwrap(); let r = engine.query_holds("eats(Alis, $da).").unwrap();
assert!(
r.is_true(),
"the `ro lo ... da` rule was silently dropped (∃ closed outside ∀): {r:?}"
);
}
#[test] fn abstraction_body_over_connected_must_reference_real_body() {
let engine = fresh_engine();
let buf = engine
.compile_debug("desires(me, event { dog(Adam) -> goes(Adam) }).")
.expect("should compile");
assert!(
has_predicate_base(&buf, "goes"),
"the abstraction body's consequent `klama` was dropped — flattener bound \
the wrong (antecedent) sentence index. FOL:\n{buf:?}"
);
}
#[test] fn abstraction_body_over_rel_clause_must_reference_real_body() {
let engine = fresh_engine();
let buf = engine
.compile_debug("desires(me, event { goes(some dog where big) }).")
.expect("should compile");
assert!(
has_predicate_base(&buf, "goes"),
"the abstraction body head `klama` was dropped — flattener bound the \
rel-clause (`barda`) sentence index instead. FOL:\n{buf:?}"
);
}
#[test] fn duplicate_ground_fact_survives_retracting_one_copy() {
let flat_dog_adam = || nibli_types::logic::LogicBuffer {
nodes: vec![nibli_types::logic::LogicNode::Predicate((
"gerku".to_string(),
vec![nibli_types::logic::LogicalTerm::Constant(
"adam".to_string(),
)],
))],
roots: vec![0],
};
let engine = fresh_engine();
let id1 = engine
.kb()
.assert_fact(flat_dog_adam(), ":assert gerku".to_string())
.unwrap();
let _id2 = engine
.kb()
.assert_fact(flat_dog_adam(), ":assert gerku".to_string())
.unwrap();
let before = engine.kb().query_entailment(flat_dog_adam()).unwrap();
assert!(
before.is_true(),
"sanity: two live records assert gerku(adam): {before:?}"
);
engine.kb().retract_fact(id1).unwrap();
let r = engine.kb().query_entailment(flat_dog_adam()).unwrap();
assert!(
r.is_true(),
"a live record still asserts gerku(adam), but retracting the duplicate \
removed the fact entirely (HashSet store has no multiplicity): {r:?}"
);
}
#[test] fn ch12_consent_case_study_traced_query_completes() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let engine = fresh_engine();
engine.assert_text("owns(some person, some data).").unwrap();
engine
.assert_text("permits(some person, event { uses(tool: some data) }).")
.unwrap();
engine
.assert_text(
"obligated_by(every person where permits(it, event { uses(tool: some data) }), event { uses(tool: some data) }).",
)
.unwrap();
let (verdict, _trace, _json) = engine
.query_text_with_proof("obligated_by(some person, event { uses(tool: some data) }).")
.unwrap();
tx.send(verdict).ok();
});
let verdict = rx.recv_timeout(std::time::Duration::from_secs(10)).expect(
"Ch 12 consent query exceeded the 10s debug budget for a 3-fact \
KB — the ∃-heavy nested-abstraction candidate search has blown up",
);
assert!(
verdict.is_true(),
"consent → processing obligation must be derivable: {verdict:?}"
);
}