use nibli_engine::{
EngineAggregateOp, EngineComputeRequest, EngineError, EngineLogicBuffer, EngineLogicNode,
EngineLogicalTerm, EngineQueryResult, NibliEngine,
};
use nibli_render::{
DRUG_INTERACTIONS_OVERLAY, GDPR_OVERLAY, Register, render_collapsed_text_with,
summarize_proof_with,
};
use nibli_store::NibliStore;
use std::fs;
use std::path::{Path, PathBuf};
fn fresh_engine() -> NibliEngine {
NibliEngine::new()
}
fn fresh_open(path: &Path, expect_msg: &str) -> NibliEngine {
NibliEngine::open(path).expect(expect_msg)
}
fn engine_with_facts(lines: &[&str]) -> NibliEngine {
let engine = fresh_engine();
for line in lines {
engine
.assert_text(line)
.unwrap_or_else(|e| panic!("Failed to assert '{}': {}", line, e));
}
engine
}
fn assert_true(result: &EngineQueryResult, msg: &str) {
assert!(result.is_true(), "{msg}: got {result:?}");
}
fn assert_false(result: &EngineQueryResult, msg: &str) {
assert!(result.is_false(), "{msg}: got {result:?}");
}
fn temp_db_path(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join("nibli_engine_integration_tests");
fs::create_dir_all(&dir).unwrap();
dir.join(format!("{name}.redb"))
}
fn find_role<'a>(buf: &'a EngineLogicBuffer, role: &str) -> Option<&'a [EngineLogicalTerm]> {
buf.nodes.iter().find_map(|n| match n {
EngineLogicNode::Predicate((rel, args)) if rel == role => Some(args.as_slice()),
_ => None,
})
}
fn role_has_const(buf: &EngineLogicBuffer, role: &str, value: &str) -> bool {
find_role(buf, role).is_some_and(|args| {
args.iter()
.any(|t| matches!(t, EngineLogicalTerm::Constant(c) if c == value))
})
}
fn cleanup(path: &Path) {
let _ = fs::remove_file(path);
}
#[test]
fn simple_assertion_and_query() {
let engine = engine_with_facts(&["big(some dog)."]);
let (holds, trace, json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_true(&holds, "Query for asserted fact should hold");
assert!(!trace.is_empty(), "Proof trace should be non-empty");
assert!(!json.is_empty(), "Proof JSON should be non-empty");
}
#[test]
fn simple_negation_query_false() {
let engine = engine_with_facts(&["big(some dog)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("big(some cat).").unwrap();
assert_false(&holds, "Query for unasserted fact should not hold");
}
#[test]
fn equals_surface_equivalence_transfers_fact() {
let engine = engine_with_facts(&["Coumadin = Varfarin.", "chemical(Coumadin)."]);
let (holds, _t, _j) = engine.query_text_with_proof("chemical(Varfarin).").unwrap();
assert_true(
&holds,
"chemical should transfer from Coumadin to Varfarin via surface du",
);
}
#[test]
fn equals_surface_equivalence_is_symmetric() {
let engine = engine_with_facts(&["Coumadin = Varfarin.", "chemical(Varfarin)."]);
let (holds, _t, _j) = engine.query_text_with_proof("chemical(Coumadin).").unwrap();
assert_true(&holds, "du equivalence is symmetric");
}
#[test]
fn equals_surface_negative_control() {
let engine = engine_with_facts(&["chemical(Coumadin)."]);
let (holds, _t, _j) = engine.query_text_with_proof("chemical(Varfarin).").unwrap();
assert_false(&holds, "without du, the fact must not transfer");
}
#[test]
fn equals_over_numeric_literals() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("1 = 1.").unwrap(),
"1 du 1 must be TRUE by reflexivity",
);
assert_false(
&engine.query_holds("1 = 2.").unwrap(),
"1 du 2 must be FALSE (distinct literals, nothing asserted)",
);
assert_true(
&engine.query_holds("Djan = Djan.").unwrap(),
"constant reflexivity sanity: djan du djan must be TRUE",
);
}
#[test]
fn not_equals_surface_contradiction() {
let engine = engine_with_facts(&["Djan = Jan.", "~Djan = Jan."]);
let violations = engine.check_contradictions();
assert!(
violations
.iter()
.any(|v| v.contains("Inequality contradiction")),
"du + na du for the same pair must be flagged: {violations:?}"
);
}
fn rendered_proof_surfaces(engine: &NibliEngine, query: &str) -> Vec<(&'static str, String)> {
let (_r, trace) = engine.query_text_raw_proof(query).unwrap();
let mut out = vec![
(
"collapsed",
render_collapsed_text_with(&trace, Register::Spec, 2, true, None),
),
(
"verbose",
nibli_render::render_proof_text(&trace, Register::Spec),
),
];
if let Some(why) = nibli_render::summarize_proof(&trace, Register::Spec) {
out.push(("why", why));
}
out
}
#[test]
fn rendered_proofs_carry_no_lojban_description_or_identity_spelling() {
let desc = engine_with_facts(&["dog(the cat)."]);
let ident = engine_with_facts(&["Adam = Bob.", "dog(Adam)."]);
let cases = [
("description", &desc, "dog(the cat).", "the cat"),
("identity", &ident, "dog(Bob).", "bob = adam"),
];
for (case, engine, query, expected) in cases {
assert_true(
&engine.query_holds(query).unwrap(),
&format!("{case}: query must hold for the proof to be populated"),
);
let mut saw_expected = false;
for (surface, text) in rendered_proof_surfaces(engine, query) {
assert!(
!text.contains("le "),
"{case}/{surface}: Lojban description article leaked: {text}"
);
assert!(
!text.contains(" du "),
"{case}/{surface}: Lojban identity spelling leaked: {text}"
);
saw_expected |= text.contains(expected);
}
assert!(
saw_expected,
"{case}: expected {expected:?} in some rendered surface"
);
}
}
#[test]
fn engine_cancel_flag_aborts_query() {
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
let engine = engine_with_facts(&["dog(Adam).", "animal(every dog)."]);
let flag = Arc::new(AtomicBool::new(true));
engine.set_cancel_flag(flag.clone());
let proof = engine.query_text_with_proof("animal(Adam).");
assert!(
proof.is_err(),
"cancelled proof query must Err, got {proof:?}"
);
assert!(
proof
.unwrap_err()
.to_string()
.to_lowercase()
.contains("cancel")
);
let holds = engine.query_holds("animal(Adam).");
assert!(
holds.is_err(),
"cancelled holds query must Err, got {holds:?}"
);
engine.clear_cancel_flag();
let (result, _, _) = engine
.query_text_with_proof("animal(Adam).")
.expect("query should succeed after clearing cancel flag");
assert_true(
&result,
"syllogism should hold once cancellation is cleared",
);
}
#[test]
fn universal_rule_chain_syllogism() {
let engine = engine_with_facts(&["animal(every dog).", "eats(every animal).", "dog(Adam)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("dog(Adam).").unwrap();
assert_true(&holds, "Direct fact should hold");
let (holds, trace, _json) = engine.query_text_with_proof("animal(Adam).").unwrap();
assert_true(&holds, "One-hop derived fact should hold");
assert!(trace.contains("Rule"), "Proof trace should show derivation");
let (holds, trace, _json) = engine.query_text_with_proof("eats(Adam).").unwrap();
assert_true(&holds, "Two-hop derived fact should hold");
assert!(
trace.contains("Rule"),
"Proof trace should show derivation chain"
);
let (holds, _trace, _json) = engine.query_text_with_proof("bird(Adam).").unwrap();
assert_false(&holds, "cipni (bird) is a real FALSE — not derivable");
}
#[test]
fn tensed_restrictor_rule_fires() {
let engine = engine_with_facts(&[
"be_hungry(every dog where past eats(it)).",
"dog(Rex).",
"past eats(Rex).",
]);
let (holds, _trace, _json) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_true(
&holds,
"tensed-antecedent rule should fire when the matching Past premise holds",
);
}
#[test]
fn tensed_restrictor_negative_control() {
let engine = engine_with_facts(&["be_hungry(every dog where past eats(it)).", "dog(Rex)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_false(
&holds,
"tensed-antecedent rule must not fire without the past premise",
);
}
#[test]
fn tensed_restrictor_wrong_tense_control() {
let engine = engine_with_facts(&[
"be_hungry(every dog where past eats(it)).",
"dog(Rex).",
"future eats(Rex).",
]);
let (holds, _trace, _json) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_false(
&holds,
"a Future premise must not satisfy a Past antecedent",
);
}
#[test]
fn tensed_restrictor_bare_premise_control() {
let engine = engine_with_facts(&[
"be_hungry(every dog where past eats(it)).",
"dog(Rex).",
"eats(Rex).",
]);
let (holds, _trace, _json) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_false(&holds, "a bare premise must not satisfy a Past antecedent");
}
#[test]
fn tensed_negated_restrictor_fires_without_witness() {
let engine = engine_with_facts(&["be_hungry(every dog where past ~eats(it)).", "dog(Rex)."]);
let (holds, _t, _j) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_true(
&holds,
"a tensed NAF restrictor fires when no matching-flavor witness exists",
);
}
#[test]
fn tensed_negated_restrictor_blocked_by_past_witness() {
let engine = engine_with_facts(&[
"be_hungry(every dog where past ~eats(it)).",
"dog(Rex).",
"past eats(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_false(
&holds,
"a Past witness blocks a `past ~P` NAF restrictor (flavor-exact)",
);
}
#[test]
fn tensed_negated_restrictor_bare_witness_does_not_block() {
let engine = engine_with_facts(&[
"be_hungry(every dog where past ~eats(it)).",
"dog(Rex).",
"eats(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_true(
&holds,
"a bare witness must not block a Past NAF restrictor",
);
}
#[test]
fn tensed_negated_restrictor_future_witness_does_not_block() {
let engine = engine_with_facts(&[
"be_hungry(every dog where past ~eats(it)).",
"dog(Rex).",
"future eats(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("be_hungry(Rex).").unwrap();
assert_true(
&holds,
"a Future witness must not block a Past NAF restrictor",
);
}
#[test]
fn bare_negated_restrictor_is_temporally_lifted_by_the_query() {
let engine = engine_with_facts(&[
"be_hungry(every dog where ~eats(it)).",
"past dog(Rex).",
"past eats(Rex).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("past be_hungry(Rex).")
.unwrap();
assert_false(
&holds,
"a bare NAF restrictor lifts to the query flavor: a Past witness blocks a Past query",
);
}
#[test]
fn bare_negated_restrictor_lifted_query_fires_without_matching_witness() {
let engine = engine_with_facts(&[
"be_hungry(every dog where ~eats(it)).",
"past dog(Rex).",
"eats(Rex).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("past be_hungry(Rex).")
.unwrap();
assert_true(
&holds,
"a bare eating must not block the Past-lifted NAF restrictor",
);
}
#[test]
fn disjunctive_restrictor_fires_via_left_branch() {
let engine = engine_with_facts(&[
"animal(every dog where loves(it) | friend(it)).",
"dog(Rex).",
"loves(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_true(
&holds,
"disjunctive antecedent fires via the left disjunct (prami)",
);
}
#[test]
fn disjunctive_restrictor_fires_via_right_branch() {
let engine = engine_with_facts(&[
"animal(every dog where loves(it) | friend(it)).",
"dog(Rex).",
"friend(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_true(
&holds,
"disjunctive antecedent fires via the right disjunct (pendo)",
);
}
#[test]
fn disjunctive_restrictor_negative_control() {
let engine = engine_with_facts(&[
"animal(every dog where loves(it) | friend(it)).",
"dog(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_false(
&holds,
"neither disjunct satisfied → disjunctive rule does not fire",
);
}
#[test]
fn conjunctive_where_clauses_require_both() {
let engine = engine_with_facts(&[
"animal(every dog where loves(it) & friend(it)).",
"dog(Rex).",
"loves(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_false(
&holds,
"conjunctive `je` restrictor requires both conjuncts — one is not enough",
);
}
#[test]
fn disjunctive_forethought_implication_fires() {
let engine = engine_with_facts(&[
"loves(Rex, Alis).",
"loves(Rex, Alis) | friend(Rex, Alis) -> animal(Rex).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_true(
&holds,
"forethought disjunctive antecedent (ganai ga…gi…gi) fires via a held disjunct",
);
}
#[test]
fn tensed_conclusion_implication_fires() {
let engine = engine_with_facts(&["dog(Rex) -> past animal(Rex).", "dog(Rex)."]);
let (past_holds, _t, _j) = engine.query_text_with_proof("past animal(Rex).").unwrap();
assert_true(
&past_holds,
"tensed conclusion derives the Past fact when the antecedent holds",
);
let (bare_holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_false(
&bare_holds,
"tensed conclusion must NOT derive a bare fact (tense-exact)",
);
}
#[test]
fn tensed_conclusion_prenex_fires() {
let engine = engine_with_facts(&["all $da: dog($da) -> past animal($da).", "dog(Rex)."]);
let (past_holds, _t, _j) = engine.query_text_with_proof("past animal(Rex).").unwrap();
assert_true(
&past_holds,
"prenex tensed conclusion derives the Past fact",
);
let (bare_holds, _t, _j) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_false(
&bare_holds,
"prenex tensed conclusion must NOT derive a bare fact",
);
}
#[test]
fn disjunctive_conclusion_contradiction_flagged() {
let engine = engine_with_facts(&[
"every dog $d: animal($d) | fish($d).",
"dog(Rex).",
"~animal(Rex).",
"~fish(Rex).",
]);
let v = engine.check_contradictions();
assert!(
v.iter()
.any(|m| m.contains("Disjunctive constraint violated")),
"gerku(rex) holds and both disjuncts explicitly denied → contradiction: {v:?}"
);
}
#[test]
fn disjunctive_conclusion_one_denied_no_contradiction() {
let engine = engine_with_facts(&[
"every dog $d: animal($d) | fish($d).",
"dog(Rex).",
"~animal(Rex).",
]);
assert!(
engine.check_contradictions().is_empty(),
"only one disjunct denied → the other could hold → no contradiction"
);
}
#[test]
fn disjunctive_query_still_works() {
let engine = engine_with_facts(&["animal(Rex)."]);
let (holds, _t, _j) = engine
.query_text_with_proof("animal(Rex) | fish(Rex).")
.unwrap();
assert_true(
&holds,
"a disjunctive query is TRUE when one disjunct holds (handled by the query evaluator)",
);
}
#[test]
fn disjunctive_conclusion_jo_ju_stay_fail_closed() {
let engine = fresh_engine();
assert!(
engine
.assert_text("every dog $d: animal($d) <-> fish($d).")
.is_err(),
"a `jo` (biconditional) conclusion head must fail closed (Not-bearing, not Horn)"
);
let engine2 = fresh_engine();
assert!(
engine2
.assert_text("every dog $d: animal($d) ^ fish($d).")
.is_err(),
"a `ju` (xor) conclusion head must fail closed (Not(And(..)), not Horn)"
);
assert!(
engine2.check_contradictions().is_empty(),
"a failed `ju` assertion leaves no constraint (rollback)"
);
}
#[test]
fn conjoined_tails_assert_both_conjuncts() {
let engine = engine_with_facts(&["goes(me) & eats(me)."]);
let (klama, _, _) = engine.query_text_with_proof("goes(me).").unwrap();
assert_true(&klama, "first gi'e tail is independently queryable");
let (citka, _, _) = engine.query_text_with_proof("eats(me).").unwrap();
assert_true(&citka, "second gi'e tail is independently queryable");
let (sipna, _, _) = engine.query_text_with_proof("sleep(me).").unwrap();
assert_false(&sipna, "unasserted predication stays FALSE (CWA control)");
}
#[test]
fn conjoined_tails_per_tail_trailing_argument() {
let engine = engine_with_facts(&["goes(me, the market) & eats(me, some apple)."]);
let (klama, _, _) = engine
.query_text_with_proof("goes(me, the market).")
.unwrap();
assert_true(&klama, "first tail with its own x2");
let (citka, _, _) = engine
.query_text_with_proof("eats(me, some apple).")
.unwrap();
assert_true(&citka, "second tail with its own x2");
let (cross, _, _) = engine
.query_text_with_proof("eats(me, the market).")
.unwrap();
assert_false(&cross, "tail sumti must not leak across tails");
}
#[test]
fn conjoined_tails_genesis_negated_tail_verse() {
let engine = engine_with_facts(&["~shape(object: Terdi) & empty(Terdi)."]);
let (kunti, _, _) = engine.query_text_with_proof("empty(Terdi).").unwrap();
assert_true(&kunti, "positive tail asserted");
let (tarmi, _, _) = engine
.query_text_with_proof("shape(object: Terdi).")
.unwrap();
assert_false(&tarmi, "negated tail stores no positive fact");
}
#[test]
fn conjoined_tails_fused_negation_negates_right_tail() {
let engine = engine_with_facts(&["goes(me) & ~eats(me)."]);
let (klama, _, _) = engine.query_text_with_proof("goes(me).").unwrap();
assert_true(&klama, "positive tail asserted");
let (citka, _, _) = engine.query_text_with_proof("eats(me).").unwrap();
assert_false(&citka, "nai-negated tail stores no positive fact");
engine.assert_text("eats(me).").unwrap();
assert!(
!engine.check_contradictions().is_empty(),
"contrary positive after a gi'enai tail must flag a contradiction"
);
}
#[test]
fn conjoined_tails_xor_negated_tail_fabricates_no_contradiction() {
let engine = engine_with_facts(&["goes(me) ^ ~eats(me).", "goes(me).", "eats(me)."]);
assert!(
engine.check_contradictions().is_empty(),
"consistent KB must not report a fabricated contradiction: {:?}",
engine.check_contradictions()
);
}
#[test]
fn iju_negated_operand_fabricates_no_contradiction() {
let engine = engine_with_facts(&["~goes(me) ^ eats(me).", "goes(me).", "eats(me)."]);
assert!(
engine.check_contradictions().is_empty(),
"consistent KB must not report a fabricated contradiction: {:?}",
engine.check_contradictions()
);
}
#[test]
fn negation_inside_abstraction_fabricates_no_contradiction() {
let engine = engine_with_facts(&["knows(me, fact { ~goes(Rex) }).", "goes(Rex)."]);
assert!(
engine.check_contradictions().is_empty(),
"a quoted negation must not feed contradiction detection: {:?}",
engine.check_contradictions()
);
}
#[test]
fn conjoined_tails_retraction_removes_both_conjuncts_and_negative_entry() {
let engine = fresh_engine();
let ids = engine.assert_text("dog(Rex) & ~animal(Rex).").unwrap();
assert_eq!(ids.len(), 1, "a GIhA chain is one fact");
let (gerku, _, _) = engine.query_text_with_proof("dog(Rex).").unwrap();
assert_true(&gerku, "conjunct stored");
engine.retract_fact(ids[0]).unwrap();
let (gerku2, _, _) = engine.query_text_with_proof("dog(Rex).").unwrap();
assert_false(&gerku2, "retracted conjunct gone");
engine.assert_text("animal(Rex).").unwrap();
assert!(
engine.check_contradictions().is_empty(),
"retracted na-tail must leave no negative-registry entry"
);
}
#[test]
fn and_statement_negated_conjunct_preserves_tense_context() {
let engine = engine_with_facts(&["dog(Rex) & past ~goes(Rex).", "past goes(Rex)."]);
assert!(
!engine.check_contradictions().is_empty(),
"tensed contrary positive must flag the tensed negative conjunct"
);
}
#[test]
fn conjoined_tails_iff_and_whether_assert_behavior_pinned() {
let engine = fresh_engine();
engine.assert_text("goes(me) <-> eats(me).").unwrap();
let (klama, _, _) = engine.query_text_with_proof("goes(me).").unwrap();
assert!(
!klama.is_true(),
"a bare biconditional must not derive either side TRUE: got {klama:?}"
);
let engine2 = fresh_engine();
assert!(
engine2.assert_text("goes(me) ^ eats(me).").is_err(),
"a positive-tails xor assertion must fail closed like `.i ju`"
);
}
#[test]
fn conjoined_tails_negated_tail_records_negative_fact_for_contradiction() {
let engine = engine_with_facts(&["dog(Rex) & ~animal(Rex).", "animal(Rex)."]);
assert!(
!engine.check_contradictions().is_empty(),
"contrary positive after a na-tail must flag a contradiction"
);
}
#[test]
fn and_statement_negated_conjunct_records_negative_fact() {
let engine = engine_with_facts(&["dog(Rex) & ~animal(Rex).", "animal(Rex)."]);
assert!(
!engine.check_contradictions().is_empty(),
"contrary positive after a negated .i je conjunct must flag a contradiction"
);
}
#[test]
fn conjoined_tails_all_negative_conjunction_accepted() {
let engine = engine_with_facts(&["~goes(me) & ~eats(me)."]);
let (klama, _, _) = engine.query_text_with_proof("goes(me).").unwrap();
assert_false(&klama, "negated conjuncts store no positive facts");
engine.assert_text("goes(me).").unwrap();
assert!(
!engine.check_contradictions().is_empty(),
"contrary positive after an all-negative conjunction must flag a contradiction"
);
}
#[test]
fn conjoined_tails_or_assert_stays_fail_closed_like_or_statement() {
let engine = fresh_engine();
assert!(
engine.assert_text("goes(me) | eats(me).").is_err(),
"asserting a bare gi'a disjunction must fail closed"
);
let q = engine_with_facts(&["goes(me)."]);
let (holds, _, _) = q.query_text_with_proof("goes(me) | eats(me).").unwrap();
assert_true(&holds, "gi'a as a query is TRUE when one disjunct holds");
}
#[test]
fn query_leading_existential_over_universal() {
let one_eater = engine_with_facts(&[
"eats(Adam, Rex).",
"eats(Adam, Spot).",
"dog(Rex).",
"dog(Spot).",
]);
let (holds, _t, _j) = one_eater
.query_text_with_proof("eats($da, every dog).")
.unwrap();
assert_true(&holds, "adam eats every dog → ∃ one eater of all dogs");
let split_eaters = engine_with_facts(&[
"eats(Adam, Rex).",
"eats(Ben, Spot).",
"dog(Rex).",
"dog(Spot).",
]);
let (holds, _t, _j) = split_eaters
.query_text_with_proof("eats($da, every dog).")
.unwrap();
assert_false(
&holds,
"different eaters per dog → NO single eater of all (∃∀, not ∀∃)",
);
}
#[test]
fn assert_leading_existential_over_universal_compiles_and_round_trips() {
let engine = fresh_engine();
engine
.assert_text("eats($da, every dog).")
.expect("∃∀ assertion must compile via the leading-∃ skolemization path");
engine.assert_text("dog(Rex).").unwrap();
engine.assert_text("dog(Spot).").unwrap();
let (holds, _t, _j) = engine
.query_text_with_proof("eats($da, every dog).")
.unwrap();
assert_true(
&holds,
"the asserted single witness eats every dog (∃∀ round-trips)",
);
}
#[test]
fn tensed_leading_existential_over_universal_rejected() {
let engine = fresh_engine();
let err = engine
.assert_text("past eats($da, every dog).")
.expect_err("a tense wrapping a whole ∃∀ rule must be rejected");
assert!(
err.to_string().contains("whole universal/conditional"),
"expected the whole-rule rejection, got: {err}"
);
}
#[test]
fn trailing_existential_after_universal_is_per_witness() {
let engine = engine_with_facts(&[
"eats(food: every dog, eater: $da).",
"dog(Rex).",
"dog(Spot).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("eats($da, every dog).")
.unwrap();
assert_false(
&holds,
"∀∃ gives per-dog eaters → NO single eater of all dogs",
);
}
#[test]
fn implication_tensed_antecedent_fires_with_premise() {
let engine = engine_with_facts(&["past runs(Adam) -> animal(Adam).", "past runs(Adam)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("animal(Adam).").unwrap();
assert_true(
&holds,
"tensed-antecedent ground conditional should fire on its Past premise",
);
}
#[test]
fn whole_rule_tense_universal_rejected() {
let engine = fresh_engine();
let err = engine
.assert_text("past animal(every dog).")
.expect_err("a tense wrapping a whole universal must be rejected");
assert!(
err.to_string().contains("whole universal/conditional"),
"expected the whole-rule rejection, got: {err}"
);
}
#[test]
fn whole_rule_deontic_universal_rejected() {
let engine = fresh_engine();
let err = engine
.assert_text("must good(every person).")
.expect_err("a deontic wrapping a whole universal must be rejected");
assert!(
err.to_string().contains("whole universal/conditional"),
"expected the whole-rule rejection, got: {err}"
);
}
#[test]
fn ground_obligation_does_not_imply_actuality() {
let engine = fresh_engine();
engine
.assert_text("must removes(Adam).")
.expect("a ground deontic fact should assert");
assert_false(
&engine.query_holds("removes(Adam).").unwrap(),
"ought must not imply is (ground obligation is not actuality)",
);
assert_true(
&engine.query_holds("must removes(Adam).").unwrap(),
"the obligation itself is preserved and queryable",
);
}
#[test]
fn prenex_tensed_body_universal_rejected() {
let engine = fresh_engine();
let err = engine
.assert_text("all $da: past loves($da).")
.expect_err("a prenex with a tensed body must be rejected");
assert!(
err.to_string().contains("whole universal/conditional"),
"expected the whole-rule rejection, got: {err}"
);
}
#[test]
fn via_modal_arity_one_rejected() {
let engine = fresh_engine();
let err = engine
.assert_text("big(me) via person(you).")
.expect_err("a 1-place fi'o modal must be rejected");
assert!(
err.to_string().contains("modal tag predicate"),
"expected the modal-arity rejection, got: {err}"
);
}
#[test]
fn untensed_universal_still_compiles_and_fires() {
let engine = engine_with_facts(&["animal(every dog).", "dog(Rex)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_true(
&holds,
"an untensed universal must still compile and fire (only whole-rule tense is rejected)",
);
}
#[test]
fn binary_restrictor_rule_fires() {
let engine = engine_with_facts(&[
"animal(every dog where loves(Alis, it)).",
"dog(Rex).",
"loves(loved: Rex, lover: Alis).",
]);
let (holds, _trace, _json) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_true(
&holds,
"binary-restrictor rule should fire when both the gadri and the 2-place relation hold",
);
}
#[test]
fn binary_restrictor_negative_control() {
let engine = engine_with_facts(&[
"animal(every dog where loves(Alis, it)).",
"loves(loved: Rex, lover: Alis).",
]);
let (holds, _trace, _json) = engine.query_text_with_proof("animal(Rex).").unwrap();
assert_false(
&holds,
"rule must not fire when the gadri predicate is unsatisfied",
);
}
#[test]
fn incidental_clause_predicate_is_asserted() {
let engine = engine_with_facts(&["dog(Rex).", "goes(every dog also big)."]);
let (big, _trace, _json) = engine.query_text_with_proof("big(Rex).").unwrap();
assert_true(
&big,
"noi asserts the incidental predicate about every dog (derived from gerku alone)",
);
let (goes, _trace, _json) = engine.query_text_with_proof("goes(Rex).").unwrap();
assert_true(
&goes,
"noi rule fires on the unrestricted domain regardless of the incidental property",
);
}
#[test]
fn restrictive_where_does_not_assert_incidental() {
let engine = engine_with_facts(&["dog(Rex).", "goes(every dog where big)."]);
let (big, _trace, _json) = engine.query_text_with_proof("big(Rex).").unwrap();
assert_false(
&big,
"poi keeps the restrictor as a premise, not a derived conclusion",
);
}
#[test]
fn binary_restrictor_constant_second_place_fires() {
let engine = engine_with_facts(&[
"warns(every chemical where metabolized_by(it, Siptucin)).",
"chemical(Uarfarin).",
"metabolized_by(Uarfarin, Siptucin).",
]);
let (holds, _trace, _json) = engine.query_text_with_proof("warns(Uarfarin).").unwrap();
assert_true(
&holds,
"2-place restrictor with a constant second place should fire",
);
}
#[test]
fn object_position_universal_fires() {
let engine = engine_with_facts(&["friend(every dog, every cat).", "dog(Rex).", "cat(Tom)."]);
let (holds, _t, _j) = engine.query_text_with_proof("friend(Rex, Tom).").unwrap();
assert_true(
&holds,
"object-position universal: every dog befriends every cat",
);
}
#[test]
fn object_position_universal_negative_control() {
let engine = engine_with_facts(&["friend(every dog, every cat).", "dog(Rex)."]);
let (holds, _t, _j) = engine.query_text_with_proof("friend(Rex, Tom).").unwrap();
assert_false(&holds, "tom is not a cat → no friendship derived");
}
#[test]
fn object_position_existential_import_no_phantom_entity() {
let engine = engine_with_facts(&["friend(every dog, every cat)."]);
let (holds, _t, _j) = engine.query_text_with_proof("cat(some dog).").unwrap();
assert_false(&holds, "no single xorlo witness is both a dog and a cat");
}
#[test]
fn object_position_count_object_fails_closed() {
let engine = fresh_engine();
let err = engine
.assert_text("friend(every dog, exactly 3 cat).")
.expect_err("an exact-count object position must be rejected");
assert!(
err.to_string().contains("not a flat predicate") || err.to_string().contains("Rejecting"),
"expected a fail-closed rejection, got: {err}"
);
}
#[test]
fn prenex_symmetric_rule_fires() {
let engine = engine_with_facts(&[
"all $da, $de: friend($da, $de) -> friend($de, $da).",
"friend(Rex, Felix).",
]);
let (holds, _t, _j) = engine.query_text_with_proof("friend(Felix, Rex).").unwrap();
assert_true(
&holds,
"prenex symmetric rule should derive the reverse friendship",
);
}
#[test]
fn prenex_cross_entity_join_fires() {
let engine = engine_with_facts(&[
"all $da, $de, $di: prevents($da, $di) & metabolized_by($de, $di) -> increases($de).",
"prevents(Flukonazol, Siptucin).",
"metabolized_by(Uarfarin, Siptucin).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("increases(Uarfarin).")
.unwrap();
assert_true(
&holds,
"prenex CYP cross-entity join should raise warfarin concentration",
);
}
#[test]
fn prenex_cross_entity_join_negative_control() {
let engine = engine_with_facts(&[
"all $da, $de, $di: prevents($da, $di) & metabolized_by($de, $di) -> increases($de).",
"prevents(Flukonazol, Siptucin).",
"metabolized_by(Apiksaban, Sipibeman).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("increases(Apiksaban).")
.unwrap();
assert_false(
&holds,
"no drug inhibits apixaban's enzyme → no concentration rise",
);
}
#[test]
fn prenex_join_terminates_without_blowup() {
use std::sync::mpsc;
use std::time::Duration;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut lines = vec![
"all $da, $de, $di: prevents($da, $di) & metabolized_by($de, $di) -> increases($de)."
.to_string(),
"prevents(Flukonazol, Siptucin).".to_string(),
];
for v in [
"a", "e", "i", "o", "u", "ai", "au", "ei", "oi", "ia", "ie", "io",
] {
lines.push(format!("metabolized_by(Druk{v}n, Enk{v}n)."));
}
lines.push("metabolized_by(Uarfarin, Siptucin).".to_string());
let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
let engine = engine_with_facts(&refs);
let r = engine.query_text_with_proof("increases(Uarfarin).");
let _ = tx.send(r.map(|(h, _, _)| h.is_true()));
});
match rx.recv_timeout(Duration::from_secs(15)) {
Ok(Ok(true)) => {}
Ok(other) => panic!("prenex join gave unexpected result: {other:?}"),
Err(_) => panic!("prenex join did not terminate within 15s (candidates^k blowup?)"),
}
}
#[test]
fn temporal_past_assertion_and_query() {
let engine = engine_with_facts(&["past big(some dog)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("past big(some dog).").unwrap();
assert_true(&holds, "Past-tensed query should hold");
let (holds, _trace, _json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_false(&holds, "Bare query should not match past-tensed fact");
}
#[test]
fn temporal_tense_discrimination() {
let engine = engine_with_facts(&["past big(some dog)."]);
let (holds, _trace, _json) = engine
.query_text_with_proof("future big(some dog).")
.unwrap();
assert_false(&holds, "Future query should not match past-tensed fact");
}
#[test]
fn temporal_future_and_present_matrix() {
let engine = engine_with_facts(&["future eats(Rex).", "now eats(Bel)."]);
assert_true(
&engine.query_holds("future eats(Rex).").unwrap(),
"Future fact matches a Future query",
);
assert_false(
&engine.query_holds("past eats(Rex).").unwrap(),
"Future fact must not match a Past query",
);
assert_false(
&engine.query_holds("eats(Rex).").unwrap(),
"Future fact must not leak into a bare query",
);
assert_true(
&engine.query_holds("now eats(Bel).").unwrap(),
"Present fact matches a Present query",
);
assert_false(
&engine.query_holds("future eats(Bel).").unwrap(),
"Present fact must not match a Future query",
);
}
#[test]
fn future_rule_consequent_derives_future_fact() {
let engine = engine_with_facts(&["dog(Rex) -> future dead(Rex).", "dog(Rex)."]);
assert_true(
&engine.query_holds("future dead(Rex).").unwrap(),
"Future conclusion derives the Future fact",
);
assert_false(
&engine.query_holds("dead(Rex).").unwrap(),
"Future conclusion must not derive a bare fact",
);
assert_false(
&engine.query_holds("past dead(Rex).").unwrap(),
"Future conclusion must not derive a Past fact",
);
}
#[test]
fn deontic_permitted_and_obligatory_matrix() {
let engine = engine_with_facts(&["may eats(Rex).", "must goes(Bel)."]);
assert_true(
&engine.query_holds("may eats(Rex).").unwrap(),
"Permitted fact matches a Permitted query",
);
assert_false(
&engine.query_holds("eats(Rex).").unwrap(),
"Permitted fact must not leak into a bare query",
);
assert_false(
&engine.query_holds("must eats(Rex).").unwrap(),
"Permitted fact must not match an Obligatory query",
);
assert_true(
&engine.query_holds("must goes(Bel).").unwrap(),
"Obligatory fact matches an Obligatory query",
);
assert_false(
&engine.query_holds("may goes(Bel).").unwrap(),
"Obligatory fact must not match a Permitted query",
);
}
#[test]
fn deontic_rule_consequent_derives_flavored_fact() {
let engine = engine_with_facts(&["dog(Rex) -> may eats(Rex).", "dog(Rex)."]);
assert_true(
&engine.query_holds("may eats(Rex).").unwrap(),
"a Permitted conclusion derives the Permitted fact",
);
assert_false(
&engine.query_holds("eats(Rex).").unwrap(),
"a Permitted conclusion must NOT derive a bare fact",
);
assert_false(
&engine.query_holds("must eats(Rex).").unwrap(),
"a Permitted conclusion must NOT derive an Obligatory fact",
);
}
#[test]
fn deontic_rule_condition_is_flavor_exact() {
let engine = engine_with_facts(&["may dog(Rex) -> eats(Rex).", "dog(Rex)."]);
assert_false(
&engine.query_holds("eats(Rex).").unwrap(),
"a bare fact must not fire a Permitted-flavored condition",
);
let engine2 = engine_with_facts(&["may dog(Rex) -> eats(Rex).", "may dog(Rex)."]);
assert_true(
&engine2.query_holds("eats(Rex).").unwrap(),
"a Permitted fact fires the Permitted-flavored condition",
);
}
#[test]
fn future_existential_witness_query() {
let engine = engine_with_facts(&["future eats(Rex)."]);
assert_true(
&engine.query_holds("future eats($da).").unwrap(),
"existential finds the Future fact under a Future query",
);
assert_false(
&engine.query_holds("past eats($da).").unwrap(),
"existential must not find the Future fact under a Past query",
);
}
#[test]
fn exact_count_query_over_ground_facts() {
let engine = engine_with_facts(&["dog(Adam).", "dog(Bel).", "animal(Adam).", "animal(Bel)."]);
assert_true(
&engine.query_holds("animal(exactly 2 dog).").unwrap(),
"exactly-2 holds when exactly two members satisfy the body",
);
assert_false(
&engine.query_holds("animal(exactly 3 dog).").unwrap(),
"exactly-3 fails when only two members satisfy the body",
);
assert_false(
&engine.query_holds("animal(exactly 1 dog).").unwrap(),
"exactly-1 fails when two members satisfy the body",
);
}
#[test]
fn present_rule_consequent_derives_present_fact() {
let engine = engine_with_facts(&["dog(Rex) -> now dead(Rex).", "dog(Rex)."]);
assert_true(
&engine.query_holds("now dead(Rex).").unwrap(),
"Present conclusion derives the Present fact",
);
assert_false(
&engine.query_holds("dead(Rex).").unwrap(),
"Present conclusion must not derive a bare fact",
);
}
#[test]
fn obligatory_rule_consequent_derives_obligatory_fact() {
let engine = engine_with_facts(&["dog(Rex) -> must eats(Rex).", "dog(Rex)."]);
assert_true(
&engine.query_holds("must eats(Rex).").unwrap(),
"Obligatory conclusion derives the Obligatory fact",
);
assert_false(
&engine.query_holds("eats(Rex).").unwrap(),
"Obligatory conclusion must not derive a bare fact",
);
assert_false(
&engine.query_holds("may eats(Rex).").unwrap(),
"Obligatory conclusion must not derive a Permitted fact",
);
}
#[test]
fn flavor_polymorphic_rule_firing_is_flavor_exact() {
let engine = engine_with_facts(&["animal(every dog).", "future dog(Rex)."]);
assert_true(
&engine.query_holds("future animal(Rex).").unwrap(),
"unmarked rule fires for a Future goal from a Future condition fact",
);
let engine2 = engine_with_facts(&["animal(every dog).", "dog(Rex)."]);
assert_false(
&engine2.query_holds("future animal(Rex).").unwrap(),
"a Future goal must NOT fire the rule from a bare condition fact",
);
}
#[test]
fn disjunctive_existential_witness() {
let engine = engine_with_facts(&["cat(Adam)."]);
assert_true(
&engine.query_holds("dog($da) | cat($da).").unwrap(),
"a witness satisfying the right disjunct suffices",
);
assert_false(
&engine.query_holds("dog($da) & cat($da).").unwrap(),
"the conjunctive form still needs both",
);
}
#[test]
fn tensed_negation_is_flavor_exact() {
for tense in ["past", "now", "future"] {
let engine = engine_with_facts(&[&format!("{tense} ~eats(Adam).")]);
assert_false(
&engine.query_holds(&format!("{tense} eats(Adam).")).unwrap(),
"the flavored positive must be FALSE after the flavored denial",
);
}
}
#[test]
fn count_block_lowering_matches_term_position_behavior() {
let engine = engine_with_facts(&["exactly 2 dog $d: big($d)."]);
for q in ["big(some dog).", "dog($da).", "big(exactly 2 dog)."] {
assert_true(
&engine.query_holds(q).unwrap(),
"count-block assertion must materialize satisfying witnesses",
);
}
assert_false(
&engine.query_holds("big(exactly 3 dog).").unwrap(),
"exactly 3 must be FALSE after a 2-witness count block",
);
}
#[test]
fn count_assertion_materializes_witnesses() {
let engine = engine_with_facts(&["big(exactly 1 dog)."]);
for q in [
"big(exactly 1 dog).",
"dog($da).",
"big($da).",
"big(some dog).",
] {
assert_true(
&engine.query_holds(q).unwrap(),
"a count assertion materializes its witness",
);
}
assert_false(
&engine.query_holds("big(exactly 2 dog).").unwrap(),
"exactly-one stays exactly one",
);
let engine2 = engine_with_facts(&["small(exactly 2 cat)."]);
assert_true(
&engine2.query_holds("small(exactly 2 cat).").unwrap(),
"exactly-two materializes two distinct witnesses",
);
assert_false(
&engine2.query_holds("small(exactly 1 cat).").unwrap(),
"two witnesses are not one",
);
assert_true(
&engine2.query_holds("cat($da).").unwrap(),
"the witnesses satisfy the restrictor",
);
}
#[test]
fn exact_count_excludes_existential_import_witness() {
let engine = engine_with_facts(&["dog(Adam).", "dog(Karl).", "animal(every dog)."]);
assert_true(
&engine.query_holds("animal(exactly 2 dog).").unwrap(),
"two real dogs count as two — the presupposition phantom is not counted",
);
assert_false(
&engine.query_holds("animal(exactly 3 dog).").unwrap(),
"the phantom must not push the count to three",
);
}
#[test]
fn find_witnesses_collapse_equals_and_events() {
let engine = engine_with_facts(&["dog(Adam).", "dog(Karl).", "Adam = Karl."]);
let tuples = engine.query_find_text("dog($da).").unwrap();
assert_eq!(
tuples.len(),
1,
"one entity, one witness tuple (was 4 pre-decision): {tuples:?}"
);
assert_eq!(
engine.count_witnesses_text("dog($da).").unwrap(),
1,
"count_witnesses agrees with the entity-level enumeration",
);
}
#[test]
fn zero_count_assertion_mints_no_witness() {
let engine = engine_with_facts(&["big(no dog)."]);
assert_false(
&engine.query_holds("dog($da).").unwrap(),
"a zero-count assertion must not mint a witness",
);
}
#[test]
fn over_arity_untagged_argument_is_rejected() {
let engine = fresh_engine();
assert!(
engine.assert_text("dog(Adam, Bob, Kim).").is_err(),
"untagged over-arity sumti must fail closed, not drop silently"
);
}
#[test]
fn builtin_arithmetic_verdicts() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("sum(5, 2, 3).").unwrap(),
"5 = 2 + 3 is TRUE by built-in arithmetic",
);
assert_false(
&engine.query_holds("sum(4, 2, 3).").unwrap(),
"4 = 2 + 3 is FALSE by built-in arithmetic",
);
}
#[test]
fn ground_conditional_with_existential_conclusion() {
let engine = engine_with_facts(&["dog(Adam) -> big(some cat).", "dog(Adam)."]);
assert_true(
&engine.query_holds("cat($da).").unwrap(),
"the fired conclusion's skolem witness satisfies the restrictor",
);
assert_true(
&engine.query_holds("big(some cat).").unwrap(),
"the fired conclusion itself holds",
);
}
#[test]
fn be_clause_with_tagged_tail_term_compiles_both() {
let engine = fresh_engine();
let buf = engine
.compile_debug("goes(Adam, Paris, origin: Rom).")
.expect("be-clause with fi-tagged tail should compile");
assert!(
role_has_const(&buf, "goes_x2", "paris"),
"be must bind x2; buffer: {buf:?}"
);
assert!(
role_has_const(&buf, "goes_x3", "rom"),
"fi-tagged tail must land in x3; buffer: {buf:?}"
);
}
#[test]
fn equals_equivalence_transfers_across_tense_flavor() {
let engine = engine_with_facts(&["past dog(Adam).", "Adam = Bob."]);
assert_true(
&engine.query_holds("past dog(Bob).").unwrap(),
"du equivalence transfers the Past fact to the equivalent name",
);
assert_false(
&engine.query_holds("dog(Bob).").unwrap(),
"the transfer must stay flavor-exact (no bare leak)",
);
}
#[test]
fn explicitly_tensed_rule_condition_is_flavor_exact() {
for tense in ["past", "now", "future"] {
let engine = engine_with_facts(&[
&format!("{tense} dog(Rex) -> dead(Rex)."),
&format!("{tense} dog(Rex)."),
]);
assert_true(
&engine.query_holds("dead(Rex).").unwrap(),
"same-flavor condition fact fires the rule",
);
let engine2 = engine_with_facts(&[&format!("{tense} dog(Rex) -> dead(Rex)."), "dog(Rex)."]);
assert_false(
&engine2.query_holds("dead(Rex).").unwrap(),
"a bare fact must NOT fire an explicitly tensed condition",
);
}
}
#[test]
fn x3_conversion_swaps_x1_and_x3() {
let engine = fresh_engine();
let buf = engine
.compile_debug("goes(origin: Rom, destination: _, goer: Adam).")
.expect("te klama should compile");
assert!(
role_has_const(&buf, "goes_x3", "rom"),
"te must move the head term to x3 (origin); buffer: {buf:?}"
);
assert!(
role_has_const(&buf, "goes_x1", "adam"),
"te must move the third term to x1 (goer); buffer: {buf:?}"
);
}
#[test]
fn numeric_terms_are_universal_domain_members() {
let engine = engine_with_facts(&["big(5)."]);
assert_true(
&engine.query_holds("sum(every big, 2, 3).").unwrap(),
"5 = 2 + 3 holds of the one member",
);
assert_false(
&engine.query_holds("sum(every big, 2, 2).").unwrap(),
"5 ≠ 2 + 2 — the number is enumerated and fails the body",
);
}
#[test]
fn exact_count_ranges_over_asserted_numbers() {
let engine = engine_with_facts(&["big(5).", "dog(5).", "dog(Rex)."]);
assert_false(
&engine.query_holds("dog(no big).").unwrap(),
"5 is big and a dog — 'no big thing is a dog' must be FALSE",
);
assert_true(
&engine.query_holds("dog(exactly 1 big).").unwrap(),
"exactly one big thing (5) is a dog",
);
assert_true(
&engine.query_holds("dog(some big).").unwrap(),
"the existential agrees with the count",
);
}
#[test]
fn presupposition_witnesses_stay_out_of_numeric_counts() {
let engine = engine_with_facts(&["big(5).", "animal(every big)."]);
assert_true(
&engine.query_holds("animal(exactly 1 big).").unwrap(),
"the witness is skipped: only the number 5 counts as big",
);
assert_true(
&engine.query_holds("dog(exactly 0 big).").unwrap(),
"5 is not a dog — the member is enumerated and fails the body",
);
}
#[test]
fn compute_role_predicates_do_not_anchor_existential_narrowing() {
let engine = engine_with_facts(&["big(5)."]);
assert_true(
&engine.query_holds("sum(some big, 2, 3).").unwrap(),
"the existential must reach the number 5 via the big_x1 index",
);
assert_false(
&engine.query_holds("sum(some big, 2, 2).").unwrap(),
"an arithmetically false body still fails — the fix widens candidates, not truth",
);
}
#[test]
fn naf_over_a_numeric_existential_inverts_the_corrected_verdict() {
let engine = engine_with_facts(&["big(5)."]);
assert_false(
&engine.query_holds("~sum(some big, 2, 3).").unwrap(),
"NAF over the now-TRUE existential must be FALSE",
);
assert_true(
&engine.query_holds("~sum(some big, 2, 2).").unwrap(),
"NAF over the arithmetically false body stays TRUE",
);
}
#[test]
fn entity_existentials_are_untouched_by_the_anchor_fix() {
let engine = engine_with_facts(&["big(5).", "dog(Rex)."]);
assert_true(
&engine.query_holds("dog(some dog).").unwrap(),
"entity narrowing control",
);
assert_false(
&engine.query_holds("dog(some big).").unwrap(),
"nothing big is a dog — the 5 candidate fails the dog body",
);
assert_false(
&engine.query_holds("sum(some dog, 2, 3).").unwrap(),
"a non-numeric witness fails the compute body — closed-world FALSE",
);
}
#[test]
fn lo_under_connective_is_per_occurrence_existential() {
let engine = engine_with_facts(&[
"dog(Rex).",
"dog(Dan).",
"bite(Rex, Adam).",
"bite(Dan, Bel).",
]);
assert_true(
&engine
.query_holds("bite(some dog, Adam) & bite(some dog, Bel).")
.unwrap(),
"per-occurrence reading: a different witness per conjunct suffices",
);
let engine2 = engine_with_facts(&["dog(Rex).", "bite(Rex, Adam)."]);
assert_false(
&engine2
.query_holds("bite(some dog, Adam) & bite(some dog, Bel).")
.unwrap(),
"an unwitnessed conjunct still fails",
);
}
#[test]
fn exact_count_collapses_equals_classes() {
let engine = engine_with_facts(&[
"dog(Adam).",
"dog(Karl).",
"animal(Adam).",
"animal(Karl).",
"Adam = Karl.",
]);
assert_true(
&engine.query_holds("animal(exactly 1 dog).").unwrap(),
"collapsed: the merged entity counts as ONE",
);
assert_false(
&engine.query_holds("animal(exactly 2 dog).").unwrap(),
"collapsed: two names for one entity do NOT count as two",
);
}
#[test]
fn naf_antecedent_rule_fires_and_blocks() {
let engine = engine_with_facts(&[
"all $da: dog($da) & ~cat($da) -> be_hungry($da).",
"dog(Rex).",
]);
assert_true(
&engine.query_holds("be_hungry(Rex).").unwrap(),
"NAF condition with no witness lets the rule fire",
);
let engine2 = engine_with_facts(&[
"all $da: dog($da) & ~cat($da) -> be_hungry($da).",
"dog(Rex).",
"cat(Rex).",
]);
assert_false(
&engine2.query_holds("be_hungry(Rex).").unwrap(),
"an asserted witness blocks the NAF condition",
);
}
#[test]
fn description_opacity_definite_vs_indefinite() {
let engine = engine_with_facts(&["big(the dog)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("big(the dog).").unwrap();
assert_true(&holds, "le (opaque) query should hold");
}
#[test]
fn la_name_assertion() {
let engine = engine_with_facts(&["dog(Adam)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("dog(Adam).").unwrap();
assert_true(&holds, "la name assertion should hold");
}
#[test]
fn parse_error_returns_syntax_error() {
let engine = fresh_engine();
let err = engine
.assert_text("not valid lojban at all !!!")
.expect_err("Invalid Lojban should produce an error");
assert!(
matches!(err, EngineError::Syntax(_)),
"a parse failure must be EngineError::Syntax, got: {err}"
);
}
#[test]
fn assert_stage_failure_is_reasoning_class() {
let engine = fresh_engine();
let err = engine
.assert_text("past animal(every dog).")
.expect_err("a whole-rule tense must be rejected");
assert!(
matches!(err, EngineError::Reasoning(_)),
"an assertion-stage rejection is a Reasoning class, got: {err}"
);
}
#[test]
fn query_parse_error() {
let engine = fresh_engine();
let result = engine.query_text_with_proof("blorp bleep !!!");
assert!(result.is_err(), "Invalid query should produce an error");
}
#[test]
fn partial_parse_fails_closed_for_query() {
let engine = engine_with_facts(&["dog(Adam)."]);
let err = engine
.query_holds("la .adam. cu gerku .i \u{ff}\u{ff}\u{ff}")
.expect_err("a partial-parse query must fail closed");
assert!(
matches!(err, EngineError::Syntax(_)),
"a parse error must be the Syntax class, got: {err:?}"
);
}
#[test]
fn proof_trace_contains_asserted_for_ground_fact() {
let engine = engine_with_facts(&["big(some dog)."]);
let (holds, trace, json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_true(&holds, "Ground fact proof query should be true");
assert!(
trace.contains("Fact:"),
"Ground fact proof should contain 'Fact:'"
);
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Proof JSON should parse");
assert!(
parsed.get("steps").is_some(),
"JSON should have 'steps' field"
);
assert!(
parsed.get("root").is_some(),
"JSON should have 'root' field"
);
}
#[test]
fn proof_trace_json_valid_for_derived_fact() {
let engine = engine_with_facts(&["animal(every dog).", "dog(Adam)."]);
let (_holds, _trace, json) = engine.query_text_with_proof("animal(Adam).").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Proof JSON should parse");
let steps = parsed["steps"].as_array().expect("steps should be array");
assert!(steps.len() > 1, "Derived proof should have multiple steps");
}
#[test]
fn reset_clears_knowledge_base() {
let engine = engine_with_facts(&["big(some dog)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_true(&holds, "Fact should hold before reset");
engine.reset();
let (holds, _trace, _json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_false(&holds, "Fact should not hold after reset");
}
#[test]
fn multiple_independent_facts() {
let engine = engine_with_facts(&["big(some dog).", "small(some cat)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_true(&holds, "First fact should hold");
let (holds, _trace, _json) = engine.query_text_with_proof("small(some cat).").unwrap();
assert_true(&holds, "Second fact should hold");
}
#[test]
fn multi_sentence_assertion() {
let engine = fresh_engine();
engine
.assert_text("big(some dog). small(some cat).")
.unwrap();
let (holds, _trace, _json) = engine.query_text_with_proof("big(some dog).").unwrap();
assert_true(&holds, "First sentence should hold");
let (holds, _trace, _json) = engine.query_text_with_proof("small(some cat).").unwrap();
assert_true(&holds, "Second sentence should hold");
}
#[test]
fn universal_rule_with_named_entity() {
let engine = engine_with_facts(&["animal(every dog).", "dog(Adam)."]);
let (holds, _trace, _json) = engine.query_text_with_proof("animal(Adam).").unwrap();
assert_true(&holds, "Named entity should derive through universal rule");
}
#[test]
fn forethought_implication_reasons() {
let engine = engine_with_facts(&["dog(Adam) -> animal(Adam).", "dog(Adam)."]);
let (holds, _t, _j) = engine.query_text_with_proof("animal(Adam).").unwrap();
assert_true(
&holds,
"ganai: danlu should derive from gerku (modus ponens)",
);
let only_rule = engine_with_facts(&["dog(Adam) -> animal(Adam)."]);
let (holds, _t, _j) = only_rule.query_text_with_proof("animal(Adam).").unwrap();
assert_false(&holds, "ganai: danlu must NOT hold without gerku");
}
#[test]
fn reversed_disjunction_reasons_modus_ponens() {
let engine = engine_with_facts(&["goes(Adam) | ~eats(Adam).", "eats(Adam)."]);
let (holds, _t, _j) = engine.query_text_with_proof("goes(Adam).").unwrap();
assert_true(
&holds,
"A | ~B: goes should derive from eats (modus ponens through the reversed arm)",
);
let only_rule = engine_with_facts(&["goes(Adam) | ~eats(Adam)."]);
let (holds, _t, _j) = only_rule.query_text_with_proof("goes(Adam).").unwrap();
assert_false(&holds, "A | ~B: goes must NOT hold without eats");
let wrong = engine_with_facts(&["goes(Adam) | ~eats(Adam).", "eats(Bel)."]);
let (holds, _t, _j) = wrong.query_text_with_proof("goes(Adam).").unwrap();
assert_false(&holds, "A | ~B: eats(Bel) must not derive goes(Adam)");
}
#[test]
fn reversed_disjunction_assertion_order_invariant() {
let engine = engine_with_facts(&["eats(Adam).", "goes(Adam) | ~eats(Adam)."]);
let (holds, _t, _j) = engine.query_text_with_proof("goes(Adam).").unwrap();
assert_true(
&holds,
"A | ~B: premise asserted BEFORE the disjunction must still derive",
);
}
#[test]
fn trailing_negation_multi_disjunct_registers_constraint() {
let engine = engine_with_facts(&[
"goes(Adam) | walks(Adam) | ~eats(Adam).",
"eats(Adam).",
"~goes(Adam).",
"~walks(Adam).",
]);
let v = engine.check_contradictions();
assert!(
v.iter()
.any(|m| m.contains("Disjunctive constraint violated")),
"eats holds and both positive disjuncts denied → constraint violation: {v:?}"
);
}
#[test]
fn tensed_find_enumerates_witnesses_per_flavor() {
let engine = engine_with_facts(&["past dog(Dan).", "now dog(Adam).", "future dog(Bel)."]);
for (q, who) in [
("past dog($da).", "dan"),
("now dog($da).", "adam"),
("future dog($da).", "bel"),
] {
let tuples = engine.query_find_text(q).unwrap();
assert_eq!(tuples.len(), 1, "{q}: exactly one witness expected");
let bound = format!("{:?}", tuples[0]).to_lowercase();
assert!(
bound.contains(who),
"{q}: binding must name {who}, got {bound}"
);
}
}
#[test]
fn now_and_future_naf_restrictors_blocked_by_matching_witness() {
for flavor in ["now", "future"] {
let engine = engine_with_facts(&[
&format!("beautiful(every person where {flavor} ~dog(it))."),
"person(Adam).",
&format!("{flavor} dog(Adam)."),
]);
let (holds, _t, _j) = engine.query_text_with_proof("beautiful(Adam).").unwrap();
assert_false(
&holds,
&format!("a `{flavor}` witness must block the `{flavor} ~dog` restrictor"),
);
let fires = engine_with_facts(&[
&format!("beautiful(every person where {flavor} ~dog(it))."),
"person(Bel).",
]);
let (holds, _t, _j) = fires.query_text_with_proof("beautiful(Bel).").unwrap();
assert_true(&holds, "no witness → the tensed NAF restrictor fires");
}
}
#[test]
fn deontic_negated_fact_asserts_ok() {
let engine = fresh_engine();
let ids = engine
.assert_text("must ~eats(Adam).")
.expect("`must ~eats(Adam).` is a legal, representable assertion");
assert!(!ids.is_empty(), "the deontic negation must ingest a record");
}
#[test]
fn negated_tail_xor_forward_half_reasons() {
let engine = engine_with_facts(&["goes(me) ^ ~eats(me).", "eats(me)."]);
let (holds, _t, _j) = engine.query_text_with_proof("goes(me).").unwrap();
assert_true(
&holds,
"negated-tail xor: eats should derive goes (the K↔C forward half)",
);
}
#[test]
fn forethought_biconditional_go_gi_reasons_both_directions() {
let fwd = engine_with_facts(&["dog(Adam) <-> animal(Adam).", "dog(Adam)."]);
let (holds, _t, _j) = fwd.query_text_with_proof("animal(Adam).").unwrap();
assert_true(
&holds,
"go biconditional: gerku should derive danlu (forward)",
);
let rev = engine_with_facts(&["dog(Adam) <-> animal(Adam).", "animal(Adam)."]);
let (holds, _t, _j) = rev.query_text_with_proof("dog(Adam).").unwrap();
assert_true(
&holds,
"go biconditional: danlu should derive gerku (reverse)",
);
}
#[test]
fn afterthought_biconditional_jo_reasons_both_directions() {
let fwd = engine_with_facts(&["dog(Adam) <-> animal(Adam).", "dog(Adam)."]);
let (holds, _t, _j) = fwd.query_text_with_proof("animal(Adam).").unwrap();
assert_true(
&holds,
".i jo biconditional: gerku should derive danlu (forward)",
);
let rev = engine_with_facts(&["dog(Adam) <-> animal(Adam).", "animal(Adam)."]);
let (holds, _t, _j) = rev.query_text_with_proof("dog(Adam).").unwrap();
assert_true(
&holds,
".i jo biconditional: danlu should derive gerku (reverse)",
);
}
#[test]
fn second_witness_family_survives_skolem_registry_dedup() {
let engine = engine_with_facts(&[
"loves(every dog, some cat).",
"gives(every person, recipient: every dog, gift: some book).",
"dog(Rex).",
"person(Adam).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("loves(Rex, some cat).")
.unwrap();
assert_true(&holds, "first family: the dog's loved-cat witness derives");
let (holds, _t, _j) = engine
.query_text_with_proof("gives(Adam, some book, Rex).")
.unwrap();
assert_true(
&holds,
"second family: the (person, dog)-dependent book witness derives",
);
let tuples = engine.query_find_text("gives(Adam, $b, Rex).").unwrap();
assert_eq!(
tuples.len(),
1,
"find must enumerate the dep-2 book witness: {tuples:?}"
);
let tuples = engine.query_find_text("loves(Rex, $c).").unwrap();
assert_eq!(
tuples.len(),
1,
"find must enumerate the dep-1 cat witness: {tuples:?}"
);
}
#[test]
fn existentially_scoped_ground_conditional_registers_and_chains() {
let engine = engine_with_facts(&[
"some person $p: goes($p) -> eats($p).",
"goes(every person).",
"person(Kim).",
]);
let (holds, _t, _j) = engine
.query_text_with_proof("some person $p: eats($p).")
.unwrap();
assert_true(
&holds,
"the ∃-witness person goes (universal) hence eats (the ∃-scoped conditional)",
);
}
#[test]
fn impure_negation_body_stays_fail_closed() {
let engine = fresh_engine();
let err = engine
.assert_text("~eats(some dog where walks(it) | goes(it)).")
.expect_err("a disjunctive negation body must be rejected fail-closed");
assert!(
err.to_string().contains("no representable content"),
"expected the zero-ingest rejection, got: {err}"
);
assert!(
engine
.assert_text("~eats(some dog where walks(it) & goes(it)).")
.is_ok(),
"a pure-conjunction negation body must stay assertable"
);
let xor = engine_with_facts(&["goes(me) ^ ~eats(me).", "goes(me)."]);
assert!(
xor.check_contradictions().is_empty(),
"no negative group may be recorded from the impure Xor half: {:?}",
xor.check_contradictions()
);
}
#[test]
fn find_expands_du_aliases_from_the_index() {
let engine = engine_with_facts(&["dog(Kim).", "dog(Bel).", "Kim = Adam."]);
let tuples = engine.query_find_text("dog($da).").unwrap();
assert_eq!(tuples.len(), 2, "two distinct dogs expected: {tuples:?}");
let bound = format!("{tuples:?}").to_lowercase();
assert!(
bound.contains("adam") && bound.contains("bel"),
"du-expanded canonical witness (adam) + the plain dog (bel) expected, got {bound}"
);
}
#[test]
fn negated_conjunct_inside_existential_records_for_contradictions() {
let engine = engine_with_facts(&["some person $p: goes($p) & ~dog(Kim).", "dog(Kim)."]);
let v = engine.check_contradictions();
assert!(
v.iter()
.any(|m| m.contains("Negation contradiction") && m.contains("dog")),
"the ∃-scoped ~dog(Kim) must be recorded and contradicted by dog(Kim): {v:?}"
);
}
#[test]
fn x2_conversion_assertion_and_query() {
let engine = engine_with_facts(&["owned(Adam, some dog)."]);
let (holds, _trace, _json) = engine
.query_text_with_proof("owned(Adam, some dog).")
.unwrap();
assert_true(&holds, "se-converted assertion should be queryable");
}
#[test]
fn connected_arguments_under_x1_tag_hold_for_both() {
let engine = engine_with_facts(&["goes(me) & goes(you)."]);
let (mi_holds, _, _) = engine.query_text_with_proof("goes(me).").unwrap();
assert_true(&mi_holds, "me must be a goer");
let (do_holds, _, _) = engine.query_text_with_proof("goes(you).").unwrap();
assert_true(
&do_holds,
"do must be a goer (right operand was dropped before the fix)",
);
}
#[test]
fn connected_under_x1_tag_negative_control() {
let engine = engine_with_facts(&["goes(me)."]);
let (do_holds, _, _) = engine.query_text_with_proof("goes(you).").unwrap();
assert_false(&do_holds, "do was never asserted as a goer");
}
#[test]
fn cll_place_counter_x3_tag_then_untagged() {
let engine = fresh_engine();
let buf = engine
.compile_debug("goes(origin: the market, route: you).")
.expect("`klama fi le zarci do` should compile");
assert!(
role_has_const(&buf, "goes_x4", "you"),
"untagged `you` must fill x4 after the route tag; buffer: {buf:?}"
);
assert!(
!role_has_const(&buf, "goes_x1", "you"),
"you must NOT land in x1 (pre-fix `first free slot` bug); buffer: {buf:?}"
);
}
#[test]
fn x5_conversion_swaps_x1_and_x5() {
let engine = fresh_engine();
let buf = engine
.compile_debug("goes(means: Ford, destination: _, origin: _, route: _, goer: Adam).")
.expect("xe klama with five places should compile");
assert!(
role_has_const(&buf, "goes_x5", "ford"),
"xe must move the head term to x5 (vehicle); buffer: {buf:?}"
);
assert!(
role_has_const(&buf, "goes_x1", "adam"),
"xe must move the fifth term to x1 (goer); buffer: {buf:?}"
);
assert!(
!role_has_const(&buf, "goes_x1", "ford"),
"xe must not leave the head term in x1; buffer: {buf:?}"
);
}
#[test]
fn query_holds_matches_proof_query_boolean() {
let engine = engine_with_facts(&["animal(every dog).", "dog(Adam)."]);
let via_bool = engine
.query_holds("animal(Adam).")
.expect("Boolean query should succeed");
let (via_proof, _trace, _json) = engine
.query_text_with_proof("animal(Adam).")
.expect("Proof query should succeed");
assert_eq!(
via_bool, via_proof,
"Boolean query API and proof query API must agree on whether a fact holds"
);
}
#[test]
fn reset_then_reassert_replaces_previous_kb_contents() {
let engine = engine_with_facts(&["dog(Adam)."]);
assert!(
engine
.query_holds("dog(Adam).")
.expect("Initial fact should be queryable")
.is_true()
);
engine.reset();
engine
.assert_text("cat(Elis).")
.expect("New fact should assert after reset");
assert!(
engine
.query_holds("dog(Adam).")
.expect("Old fact query should still run")
.is_false(),
"Reset should remove prior KB contents before new facts are asserted"
);
assert!(
engine
.query_holds("cat(Elis).")
.expect("New fact should be queryable")
.is_true(),
"Facts asserted after reset should become the whole active KB"
);
}
#[test]
fn persistent_engine_replays_asserted_facts_after_reopen() {
let path = temp_db_path("replay_after_reopen");
cleanup(&path);
{
let engine = fresh_open(&path, "Persistent engine should open");
engine
.assert_text("animal(every dog).")
.expect("Rule should persist");
engine
.assert_text("dog(Adam).")
.expect("Fact should persist");
assert!(
engine
.query_holds("animal(Adam).")
.expect("Derived query should run before reopen")
.is_true()
);
}
{
let reopened = fresh_open(&path, "Persistent engine should reopen");
assert!(
reopened
.query_holds("animal(Adam).")
.expect("Derived query should run after reopen")
.is_true(),
"Reopened engine should replay persisted rule and fact"
);
}
cleanup(&path);
}
#[test]
fn persistent_engine_honors_store_retractions_after_reopen() {
let path = temp_db_path("retract_then_reopen");
cleanup(&path);
let fact_id = {
let engine = fresh_open(&path, "Persistent engine should open");
engine
.assert_text("dog(Adam).")
.expect("Fact should persist")[0]
};
{
let mut store = NibliStore::open(&path, "local".into()).expect("Store should open");
store
.retract_fact(fact_id)
.expect("Retracting persisted fact should succeed");
}
{
let reopened = fresh_open(&path, "Persistent engine should reopen");
assert!(
reopened
.query_holds("dog(Adam).")
.expect("Query should run after reopen")
.is_false(),
"Retracted facts must not replay into the reopened engine"
);
}
cleanup(&path);
}
#[test]
fn persistent_engine_retraction_via_engine_api_survives_reopen() {
let path = temp_db_path("engine_api_retract_then_reopen");
cleanup(&path);
let fact_id = {
let engine = fresh_open(&path, "Persistent engine should open");
let id = engine
.assert_text("dog(Adam).")
.expect("Fact should persist")[0];
assert!(
engine
.query_holds("dog(Adam).")
.expect("Query should run before retraction")
.is_true(),
"Fact should hold immediately after assertion"
);
engine
.retract_fact(id)
.expect("Engine-level retraction should succeed");
assert!(
engine
.query_holds("dog(Adam).")
.expect("Query should run after retraction")
.is_false(),
"Retracted fact must not hold in the live engine"
);
id
};
{
let store = NibliStore::open(&path, "local".into()).expect("Store should reopen");
let record = store
.get_fact(fact_id)
.expect("Store read should succeed")
.expect("Retracted fact record should still exist as a tombstone");
assert!(
record.retracted,
"Engine-level retraction must durably tombstone the persisted fact"
);
}
{
let reopened = fresh_open(&path, "Persistent engine should reopen");
assert!(
reopened
.query_holds("dog(Adam).")
.expect("Query should run after reopen")
.is_false(),
"Facts retracted via the engine API must stay retracted after reopen"
);
}
cleanup(&path);
}
fn v2_meta_downgrade(path: &Path) {
use redb::{Database, TableDefinition};
const META: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata");
let db = Database::create(path).unwrap();
let txn = db.begin_write().unwrap();
{
let mut meta = txn.open_table(META).unwrap();
let bytes = postcard::to_allocvec(&2u32).unwrap();
meta.insert("schema_version", bytes.as_slice()).unwrap();
}
txn.commit().unwrap();
}
fn seed_v2_host_text_row(path: &Path, id: u64, text: &str) {
use nibli_store::{StoredAssertion, StoredFactRecord};
use redb::{Database, TableDefinition};
const FACTS: TableDefinition<u64, &[u8]> = TableDefinition::new("facts");
const META: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata");
let record = StoredFactRecord {
id,
payload: postcard::to_allocvec(&StoredAssertion::Text(text.to_string())).unwrap(),
label: text.to_string(),
retracted: false,
node_id: "seed".to_string(),
hlc_timestamp: id,
predicates: Vec::new(),
};
let db = Database::create(path).unwrap();
let txn = db.begin_write().unwrap();
{
let mut facts = txn.open_table(FACTS).unwrap();
let bytes = postcard::to_allocvec(&record).unwrap();
facts.insert(id, bytes.as_slice()).unwrap();
let mut meta = txn.open_table(META).unwrap();
let vb = postcard::to_allocvec(&2u32).unwrap();
meta.insert("schema_version", vb.as_slice()).unwrap();
}
txn.commit().unwrap();
}
#[test]
fn v2_engine_db_restamps_to_v3_and_replays() {
let path = temp_db_path("v3_engine_restamp");
cleanup(&path);
{
let engine = fresh_open(&path, "engine should open");
engine.assert_text("dog(Adam).").expect("fact persists");
}
v2_meta_downgrade(&path); {
let store = NibliStore::open(&path, "local".into()).expect("store opens v2");
assert!(
store.needs_migration(),
"downgraded DB should read as migratable"
);
}
{
let engine = fresh_open(&path, "engine reopens v2 → v3");
assert!(
engine.query_holds("dog(Adam).").unwrap().is_true(),
"a v2 engine DB must replay after the v3 restamp, not be rejected",
);
}
{
let store = NibliStore::open(&path, "local".into()).expect("store reopens");
assert!(
!store.needs_migration(),
"engine open must have finalized v3"
);
}
cleanup(&path);
}
#[test]
fn v2_text_row_migrates_via_real_compiler_and_replays() {
use nibli_store::StoredAssertion;
use nibli_types::logic::LogicBuffer;
let path = temp_db_path("v3_text_fidelity");
cleanup(&path);
let text = "dog(Adam). dog(Bel)."; seed_v2_host_text_row(&path, 7, text);
let preds = nibli_reason::default_compute_predicates();
let mut store = NibliStore::open(&path, "local".into()).expect("store opens v2");
assert!(store.needs_migration());
let migrated = store
.migrate_v2_text_rows(|t| {
nibli_session::compile_text(t, &preds)
.map(|buf| postcard::to_allocvec(&buf).expect("serialize buffer"))
.map_err(|e| e.to_string())
})
.expect("KR text migrates");
assert_eq!(migrated, 1);
assert!(!store.needs_migration());
let rec = store.get_fact(7).unwrap().unwrap();
let inner = match postcard::from_bytes::<StoredAssertion>(&rec.payload).unwrap() {
StoredAssertion::Buffer(inner) => inner,
other => panic!("migrated row must be Buffer, got {other:?}"),
};
let buf: LogicBuffer = postcard::from_bytes(&inner).expect("inner decodes as LogicBuffer");
let fresh = nibli_session::compile_text(text, &preds).unwrap();
assert_eq!(
buf.roots.len(),
fresh.roots.len(),
"whole composite buffer preserved (not split into per-root rows)",
);
assert!(
buf.roots.len() >= 2,
"the two-sentence composite has multiple roots"
);
assert_eq!(
rec.label, text,
"label sourced from the recovered payload text"
);
let core = nibli_session::CoreSession::new();
core.kb()
.assert_fact_with_id(buf, text.to_string(), rec.id)
.expect("replay asserts the migrated buffer");
assert!(core.query_text("dog(Adam).").unwrap().is_true());
assert!(core.query_text("dog(Bel).").unwrap().is_true());
cleanup(&path);
}
#[test]
fn gdpr_file_loads_clean() {
let corpus = include_str!("../../gdpr.nibli");
let engine = fresh_engine();
for (line_num, line) in corpus.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
engine.assert_text(trimmed).unwrap_or_else(|e| {
panic!(
"gdpr.lojban line {} failed to assert: {:?}\n{}",
line_num + 1,
trimmed,
e
)
});
}
}
#[test]
fn utopia_file_loads_and_pins() {
let corpus = include_str!("../../utopia.nibli");
let engine = fresh_engine();
let mut n = 0u32;
for (line_num, line) in corpus.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
engine.assert_text(trimmed).unwrap_or_else(|e| {
panic!(
"utopia.nibli line {} failed to assert: {:?}\n{}",
line_num + 1,
trimmed,
e
)
});
n += 1;
}
assert!(n >= 60, "expected a full utopia corpus, got {n} statements");
let pins: &[(&str, bool)] = &[
("person(Adam).", true),
("expresses(Adam).", true),
("travel(Adam).", false),
("travel(Bela).", true),
("false(Bela).", true),
("reward(Bela).", false),
("lose(Points, Bela).", true),
("lose(Points, Cira).", true),
("false(Dev).", true),
("reward(Gia).", true),
("false(Lupo).", true),
("false(Mira).", false),
("reward(Mira).", true),
("false(Esa).", false),
("reward(Esa).", true),
("reward(Quin).", true), ("reward(Koa).", false),
("prisoner(Hano).", true),
("dwell(Hano).", true),
("prisoner(Jala).", false),
("prisoner(Nia).", false),
("prisoner(Lalo).", true),
("building(HighSec, Lalo).", true),
("dwell(Lalo).", true),
("prisoner(Nando).", true),
("building(LowSec, Nando).", true),
("dwell(Nando).", true),
("obligated_by(Adam, event { eats() }).", true),
];
for (q, want_true) in pins {
let r = engine
.query_holds(q)
.unwrap_or_else(|e| panic!("query {q}: {e}"));
if *want_true {
assert_true(&r, q);
} else {
assert_false(&r, q);
}
}
assert!(
engine.check_contradictions().is_empty(),
"shipped utopia scenario must be store+derived-negation clean: {:?}",
engine.check_contradictions()
);
}
#[test]
fn utopia_style_derived_negation_contradiction_flagged() {
let engine = engine_with_facts(&[
"travel(every person where ~prisoner).",
"person(Kilo).",
"~travel(Kilo).",
]);
assert_true(
&engine.query_holds("travel(Kilo).").unwrap(),
"travel(Kilo) derived",
);
let v = engine.check_contradictions();
assert!(
v.iter().any(|m| m.contains("Negation contradiction")),
"derived positive must flag against asserted negation: {v:?}"
);
}
#[test]
fn gdpr_why_lawful_basis_is_domain_termed() {
let engine = engine_with_facts(&[
"permitted(every person where approves).",
"person(Adam).",
"approves(Adam).",
]);
let (_r, trace) = engine.query_text_raw_proof("permitted(Adam).").unwrap();
let overlay = summarize_proof_with(&trace, Register::Spec, Some(&GDPR_OVERLAY))
.expect("lawful-basis proof has a why summary");
assert!(overlay.contains("Adam consents"), "why: {overlay}");
assert!(
overlay.contains("Adam has a lawful basis for processing"),
"why: {overlay}"
);
assert!(!overlay.contains('X'), "bare variable leaked: {overlay}");
let fallback = summarize_proof_with(&trace, Register::Spec, None).unwrap();
assert!(
!fallback.contains("lawful basis"),
"fallback must stay literal: {fallback}"
);
assert!(!fallback.contains('X'), "bare variable leaked: {fallback}");
}
#[test]
fn gdpr_belief_revision_consent_withdrawal() {
let engine = fresh_engine();
engine.assert_text("person(Adam).").unwrap();
engine
.assert_text("permitted(every person where approves).")
.unwrap(); let consent_id = engine.assert_text("approves(Adam).").unwrap()[0];
assert_true(
&engine.query_holds("permitted(Adam).").unwrap(),
"With consent, Adam's processing has a lawful basis",
);
assert_false(
&engine.query_holds("~permitted(Adam).").unwrap(),
"With consent, there is no right to erasure",
);
engine.retract_fact(consent_id).unwrap();
assert_false(
&engine.query_holds("permitted(Adam).").unwrap(),
"After withdrawal, no lawful basis remains",
);
let (erasure, trace, json) = engine.query_text_with_proof("~permitted(Adam).").unwrap();
assert_true(
&erasure,
"After withdrawal, the right to erasure (Art 17) is triggered",
);
assert!(!trace.is_empty(), "Erasure proof trace should be non-empty");
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("Erasure proof JSON should parse");
assert_eq!(
parsed["naf_dependent"],
serde_json::Value::Bool(true),
"Erasure verdict must be flagged as negation-as-failure dependent"
);
}
#[test]
fn gdpr_lawful_basis_via_contract() {
let engine = engine_with_facts(&[
"permitted(every person where promise).",
"person(Adam).",
"promise(Adam).",
"person(Bet).", ]);
assert_true(
&engine.query_holds("permitted(Adam).").unwrap(),
"Contract is a lawful basis (Art 6(1)(b))",
);
assert_false(
&engine.query_holds("permitted(Bet).").unwrap(),
"A subject with no lawful basis has no lawful processing",
);
}
#[test]
fn gdpr_special_category_requires_stricter_basis() {
let engine = engine_with_facts(&[
"obligated_by(every healthy data, event { exact() }).",
"healthy data(Kanrek).",
"data(Ordrek).",
]);
assert_true(
&engine
.query_holds("obligated_by(Kanrek, event { exact() }).")
.unwrap(),
"Health data requires a stricter basis (Art 9)",
);
assert_false(
&engine
.query_holds("obligated_by(Ordrek, event { exact() }).")
.unwrap(),
"Ordinary data does not require the special-category basis",
);
}
#[test]
fn gdpr_art5_accuracy_applies_to_health_data() {
let engine = engine_with_facts(&[
"data(every healthy data).",
"obligated_by(every data, event { correct() }).",
"healthy data(Kanrek).",
]);
let (holds, trace, _json) = engine
.query_text_with_proof("obligated_by(Kanrek, event { correct() }).")
.unwrap();
assert_true(
&holds,
"Accuracy obligation reaches health data via kanro datni -> datni -> drani",
);
assert!(
trace.contains("Rule"),
"Accuracy proof should show a derivation chain"
);
}
#[test]
fn gdpr_right_of_access_dsar() {
let engine = engine_with_facts(&[
"permitted(every person, event { data discovers() }).",
"person(Adam).",
"data governs(Akmes).", ]);
assert_true(
&engine
.query_holds("permitted(Adam, event { data discovers() }).")
.unwrap(),
"A data subject has the right of access (Art 15)",
);
assert_false(
&engine
.query_holds("permitted(Akmes, event { data discovers() }).")
.unwrap(),
"A controller (non-subject) does not acquire the access right",
);
}
#[test]
fn gdpr_breach_notification() {
let engine = engine_with_facts(&[
"obligated_by(every data governs where flaw, event { message() }).",
"data governs(Akmes).",
"data governs(Gugli).",
"flaw(Akmes).", ]);
assert_true(
&engine
.query_holds("obligated_by(Akmes, event { message() }).")
.unwrap(),
"A breached controller must notify (Art 33)",
);
assert_false(
&engine
.query_holds("obligated_by(Gugli, event { message() }).")
.unwrap(),
"A controller with no breach has no notification obligation",
);
}
#[test]
fn gdpr_erasure_rule_via_negated_consent_restrictor() {
let engine = fresh_engine();
engine.assert_text("person(Adam).").unwrap();
engine
.assert_text("obligated_by(every person where ~approves, event { removes() }).")
.expect("the negated-restrictor erasure rule must now compile");
assert_true(
&engine
.query_holds("obligated_by(Adam, event { removes() }).")
.unwrap(),
"No consent → erasure obligation holds (Art 17 as a stored rule)",
);
let consent_id = engine.assert_text("approves(Adam).").unwrap()[0];
assert_false(
&engine
.query_holds("obligated_by(Adam, event { removes() }).")
.unwrap(),
"Consent present → no erasure obligation",
);
engine.retract_fact(consent_id).unwrap();
let (holds, trace, json) = engine
.query_text_with_proof("obligated_by(Adam, event { removes() }).")
.unwrap();
assert_true(&holds, "After withdrawal, the erasure obligation re-arises");
assert!(!trace.is_empty(), "Erasure proof trace should be non-empty");
let parsed: serde_json::Value =
serde_json::from_str(&json).expect("Erasure proof JSON should parse");
assert_eq!(
parsed["naf_dependent"],
serde_json::Value::Bool(true),
"Erasure-rule verdict rests on a negation-as-failure dependency",
);
}
#[test]
fn gdpr_erasure_rule_is_per_subject() {
let engine = fresh_engine();
engine.assert_text("person(Adam).").unwrap();
engine.assert_text("person(Bet).").unwrap();
engine
.assert_text("obligated_by(every person where ~approves, event { removes() }).")
.unwrap();
engine.assert_text("approves(Bet).").unwrap();
assert_true(
&engine
.query_holds("obligated_by(Adam, event { removes() }).")
.unwrap(),
"adam (no consent) is obligated to be erased",
);
assert_false(
&engine
.query_holds("obligated_by(Bet, event { removes() }).")
.unwrap(),
"bet (consented) is NOT obligated — the rule is per-subject, not global",
);
}
#[test]
fn gdpr_full_corpus_lawful_basis_query_completes() {
let start = std::time::Instant::now();
let corpus = include_str!("../../gdpr.nibli");
let engine = fresh_engine();
let mut consent_id = None;
for (line_num, line) in corpus.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let id = engine.assert_text(trimmed).unwrap_or_else(|e| {
panic!(
"gdpr.nibli line {} failed to assert: {:?}\n{}",
line_num + 1,
trimmed,
e
)
});
if trimmed == "approves(Adam)." {
consent_id = id.first().copied();
}
}
assert_true(
&engine.query_holds("permitted(Adam).").unwrap(),
"Against the full corpus, Adam's processing has a lawful basis (Art 6)",
);
engine
.retract_fact(consent_id.expect("consent line present in gdpr.nibli"))
.unwrap();
assert_false(
&engine.query_holds("permitted(Adam).").unwrap(),
"After withdrawal, no lawful basis remains (full-corpus exhaustive search)",
);
assert_true(
&engine.query_holds("~permitted(Adam).").unwrap(),
"After withdrawal, the right to erasure (Art 17) is triggered",
);
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(120),
"full-corpus Ch 19 sequence took {elapsed:?} (budget 120s) — the \
backward-chaining candidate search has regressed"
);
}
const CREDENTIAL_KB: &[&str] = &[
"derived_only(\"permits\").",
"all $a: choose(Electorate, $a) & ~rotten($a) & ~broken($a) -> permits(Review, $a).",
"choose(Electorate, Gia).",
];
#[test]
fn derived_only_still_derives() {
let engine = engine_with_facts(CREDENTIAL_KB);
assert_true(
&engine.query_holds("permits(Review, Gia).").unwrap(),
"a seated auditor still derives the credential",
);
assert_false(
&engine.query_holds("permits(Review, Sock).").unwrap(),
"an unseated one does not",
);
}
#[test]
fn derived_only_refuses_direct_assertion() {
let engine = engine_with_facts(CREDENTIAL_KB);
let err = engine
.assert_text("permits(Review, Sock).")
.expect_err("a closed relation must not be directly assertable");
assert!(
matches!(err, EngineError::Reasoning(_)),
"must be a Reasoning error, not a syntax one: {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("permits") && msg.contains("derived-only"),
"the error must name the relation and the reason: {msg}"
);
assert_false(
&engine.query_holds("permits(Review, Sock).").unwrap(),
"the refused fact must leave no trace",
);
}
#[test]
fn derived_only_refusal_is_atomic() {
let engine = engine_with_facts(CREDENTIAL_KB);
assert!(
engine
.assert_text("person(Sock) & permits(Review, Sock).")
.is_err()
);
assert_false(
&engine.query_holds("person(Sock).").unwrap(),
"the legal conjunct must be rolled back with the illegal one",
);
}
#[test]
fn derived_only_survives_retraction_and_replay() {
let engine = fresh_engine();
for line in CREDENTIAL_KB {
engine.assert_text(line).unwrap();
}
let extra = engine.assert_text("choose(Electorate, Bet).").unwrap();
engine.retract_fact(extra[0]).unwrap();
assert!(
engine.assert_text("permits(Review, Sock).").is_err(),
"the closure must survive the rebuild — this is the whole retraction constraint"
);
assert_true(
&engine.query_holds("permits(Review, Gia).").unwrap(),
"and derivation must still work after the replay",
);
}
#[test]
fn derived_only_is_cleared_by_reset() {
let engine = engine_with_facts(CREDENTIAL_KB);
assert!(engine.assert_text("permits(Review, Sock).").is_err());
engine.reset();
engine
.assert_text("permits(Review, Sock).")
.expect("a reset KB has no closures");
}
#[test]
fn derived_only_declaration_order_does_not_matter() {
let engine = engine_with_facts(&[
"all $a: choose(Electorate, $a) & ~rotten($a) & ~broken($a) -> permits(Review, $a).",
"choose(Electorate, Gia).",
"derived_only(\"permits\").",
"derived_only(\"permits\").",
]);
assert!(engine.assert_text("permits(Review, Sock).").is_err());
assert_true(&engine.query_holds("permits(Review, Gia).").unwrap(), "");
}
#[test]
fn derived_only_is_scoped_to_the_named_relation() {
let engine = engine_with_facts(CREDENTIAL_KB);
engine
.assert_text("person(Adam).")
.expect("an unrelated relation stays open");
engine
.assert_text("choose(Electorate, Bet).")
.expect("the rule's own antecedent relation stays assertable");
}
#[test]
fn derived_only_closes_converted_alias_spellings_too() {
let engine = engine_with_facts(CREDENTIAL_KB);
let err = engine
.assert_text("permitted(Adam).")
.expect_err("the converted alias must not be a bypass");
assert!(
err.to_string().contains("permits"),
"the error names the CANONICAL relation, which is the one actually closed: {err}"
);
}
#[test]
fn derived_only_refuses_an_inert_late_declaration() {
let engine = engine_with_facts(&["permits(Review, Sock)."]);
let err = engine
.assert_text("derived_only(\"permits\").")
.expect_err("a declaration that protects nothing must not look like one that works");
let msg = err.to_string();
assert!(
msg.contains("comes too late") && msg.contains("permits"),
"the error must say WHY and name the relation: {msg}"
);
}
#[test]
fn derived_only_late_declaration_is_fine_after_rules_only() {
let engine = engine_with_facts(&[
"all $a: choose(Electorate, $a) & ~rotten($a) & ~broken($a) -> permits(Review, $a).",
"choose(Electorate, Gia).",
"derived_only(\"permits\").",
]);
assert_true(
&engine.query_holds("permits(Review, Gia).").unwrap(),
"the derived credential survives a late declaration",
);
assert!(
engine.assert_text("permits(Review, Sock).").is_err(),
"and the closure is live"
);
}
#[test]
fn derived_only_redeclaration_is_idempotent() {
let engine = engine_with_facts(&["derived_only(\"permits\").", "person(Adam)."]);
engine
.assert_text("derived_only(\"permits\").")
.expect("re-declaring a closed relation is a no-op, not an error");
}
#[test]
fn derived_only_declaration_is_queryable() {
let engine = engine_with_facts(CREDENTIAL_KB);
assert_true(
&engine.query_holds("derived_only(\"permits\").").unwrap(),
"the declaration stores like any assertion",
);
}
#[test]
fn rights_floor_entitled_asserts_and_routes() {
let engine = engine_with_facts(&["entitled(every person, event { eats() }).", "person(Adam)."]);
assert_true(
&engine
.query_holds("entitled(Adam, event { eats() }).")
.unwrap(),
"a person is entitled to the floor right",
);
let engine = engine_with_facts(&["entitled(holder: Adam, entitlement: Bread)."]);
assert_true(
&engine.query_holds("entitled(Adam, Bread).").unwrap(),
"named-arg routing must equal positional routing",
);
assert!(
fresh_engine()
.assert_text("entitled(bound: Adam).")
.is_err(),
"`entitled` must not accept `obliged`'s place labels"
);
}
#[test]
fn rights_floor_does_not_fabricate_the_actuality() {
let engine = engine_with_facts(&["entitled(every person, event { eats() }).", "person(Adam)."]);
assert_true(
&engine
.query_holds("entitled(Adam, event { eats() }).")
.unwrap(),
"the entitlement itself holds",
);
assert_false(
&engine.query_holds("eats(Adam).").unwrap(),
"being entitled to eat must NOT derive that Adam eats",
);
assert_false(
&engine.query_holds("eats(some person).").unwrap(),
"nor that some person eats — the abstraction body stays opaque",
);
}
#[test]
fn rights_floor_blocks_punishment_for_lacking_it() {
let engine = engine_with_facts(&[
"entitled(every person, event { eats() }).",
"all $anyone: prisoner($anyone) -> person($anyone).",
]);
let err = engine
.assert_text("all $x: person($x) & ~eats($x) -> prisoner($x).")
.expect_err("a rule punishing the absence of a floor right must be rejected");
let msg = format!("{err:?}");
assert!(
msg.contains("Unstratifiable") && msg.contains("prisoner") && msg.contains("eats"),
"the rejection must name the prisoner->eats negative cycle, got: {msg}"
);
}
#[test]
fn punishment_rule_alone_is_stratifiable() {
let engine = engine_with_facts(&[
"all $anyone: prisoner($anyone) -> person($anyone).",
"all $x: person($x) & ~eats($x) -> prisoner($x).",
"person(Adam).",
]);
assert_true(
&engine.query_holds("prisoner(Adam).").unwrap(),
"with no floor asserted the punishing rule registers and fires",
);
}
#[test]
fn rights_floor_protection_is_placement_dependent() {
let engine = engine_with_facts(&[
"entitled(event { eats() }, every person).",
"all $anyone: prisoner($anyone) -> person($anyone).",
]);
assert!(
engine
.assert_text("all $x: person($x) & ~eats($x) -> prisoner($x).")
.is_ok(),
"universal-in-x2 must NOT build the firewall — if this starts failing, the \
event-abstraction head walk changed and the x1 requirement may have been \
relaxed (good news, but the floor docs and NIBLI_KR need updating)"
);
engine.assert_text("person(Adam).").unwrap();
assert_true(
&engine.query_holds("prisoner(Adam).").unwrap(),
"with the floor mis-spelled, lacking the right is punishable",
);
}
fn load_corpus_like_host(engine: &NibliEngine, corpus: &str) -> (u32, u32, Vec<(String, u64)>) {
let mut asserted = 0u32;
let mut skipped = 0u32;
let mut ids = Vec::new();
for (line_num, line) in corpus.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
skipped += 1;
continue;
}
let id = engine.assert_text(trimmed).unwrap_or_else(|e| {
panic!(
"corpus line {} failed to assert (book pins 0 errors): {:?}\n{}",
line_num + 1,
trimmed,
e
)
});
asserted += 1;
ids.push((trimmed.to_string(), id[0]));
}
(asserted, skipped, ids)
}
fn pinned_id(ids: &[(String, u64)], line: &str) -> u64 {
let hits: Vec<u64> = ids
.iter()
.filter(|(l, _)| l == line)
.map(|&(_, id)| id)
.collect();
assert!(
hits.len() == 1,
"expected exactly one corpus occurrence of {line:?}, found {}",
hits.len()
);
hits[0]
}
#[test]
fn gdpr_corpus_transcript_pins() {
let engine = fresh_engine();
let (asserted, skipped, ids) = load_corpus_like_host(&engine, include_str!("../../gdpr.nibli"));
assert_eq!(
(asserted, skipped),
(24, 77),
"Ch 19 pins `[Load] Done: 24 asserted, 77 skipped, 0 errors`"
);
assert_eq!(
pinned_id(&ids, "approves(Adam)."),
21,
"Ch 19 retracts the consent fact as id #21"
);
let contract_id = engine.assert_text("promise(Adam).").unwrap()[0];
assert_eq!(
contract_id, 24,
"Ch 19 retracts the post-load contract fact as id #24"
);
}
#[test]
fn ddi_corpus_transcript_pins() {
let engine = fresh_engine();
let (asserted, skipped, ids) =
load_corpus_like_host(&engine, include_str!("../../drug-interactions.nibli"));
assert_eq!(
(asserted, skipped),
(16, 78),
"Ch 20 pins `[Load] Done: 16 asserted, 78 skipped, 0 errors`"
);
assert_eq!(
pinned_id(&ids, "prevents(Flukonazol, Siptucin)."),
4,
"Ch 20 retracts the inhibition fact as id #4"
);
assert_eq!(
pinned_id(&ids, "uses(Adam, Varfarin)."),
10,
"Ch 20 retracts the warfarin regimen fact as id #10"
);
}
#[test]
fn stacked_where_clauses_conjoin_both() {
let engine = engine_with_facts(&[
"dangerous(every chemical where increases where thin).",
"chemical(Alfan).",
"increases(Alfan).",
"thin(Alfan).", "chemical(Betan).",
"increases(Betan).", "chemical(Gaman).",
"thin(Gaman).", ]);
assert_true(
&engine.query_holds("dangerous(Alfan).").unwrap(),
"both zenba and cinla -> ckape",
);
assert_false(
&engine.query_holds("dangerous(Betan).").unwrap(),
"zenba only (cinla missing) -> NOT ckape",
);
assert_false(
&engine.query_holds("dangerous(Gaman).").unwrap(),
"cinla only (zenba missing) -> NOT ckape (the pre-fix bug)",
);
}
fn engine_with_ddi_corpus() -> NibliEngine {
let corpus = include_str!("../../drug-interactions.nibli");
let engine = fresh_engine();
for (line_num, line) in corpus.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
engine.assert_text(trimmed).unwrap_or_else(|e| {
panic!(
"drug-interactions.lojban line {} failed to assert: {:?}\n{}",
line_num + 1,
trimmed,
e
)
});
}
engine
}
#[test]
fn ddi_file_loads_clean() {
let _ = engine_with_ddi_corpus();
}
#[test]
fn ddi_headline_warfarin_fluconazole_alert() {
let engine = engine_with_ddi_corpus();
assert_true(
&engine.query_holds("increases(Varfarin).").unwrap(),
"Warfarin concentration rises (fluconazole inhibits CYP2C9, warfarin is a substrate)",
);
assert_true(
&engine.query_holds("dangerous(Varfarin).").unwrap(),
"Warfarin is at toxicity risk (increased concentration + narrow therapeutic index)",
);
let (alert, trace, _json) = engine.query_text_with_proof("warns(Varfarin).").unwrap();
assert_true(&alert, "Warfarin co-prescription warrants a safety alert");
assert!(
trace.contains("Rule"),
"Alert proof should show a derivation chain, got:\n{trace}"
);
assert_false(
&engine.query_holds("increases(Apiksaban).").unwrap(),
"Apixaban concentration does not rise (CYP3A4 not inhibited by fluconazole)",
);
assert_false(
&engine.query_holds("warns(Apiksaban).").unwrap(),
"Apixaban co-administration produces NO alert (deduced False, not unknown)",
);
}
#[test]
fn ddi_why_toxicity_is_concrete_and_domain_termed() {
let engine = engine_with_ddi_corpus();
let (_r, trace) = engine.query_text_raw_proof("dangerous(Varfarin).").unwrap();
let overlay = summarize_proof_with(&trace, Register::Spec, Some(&DRUG_INTERACTIONS_OVERLAY))
.expect("toxicity proof has a why summary");
assert!(
overlay.contains("fluconazole inhibits CYP2C9"),
"why: {overlay}"
);
assert!(
overlay.contains("warfarin is metabolized by CYP2C9"),
"why: {overlay}"
);
assert!(
overlay.contains("warfarin is at toxicity risk"),
"why: {overlay}"
);
assert!(
overlay.contains("narrow therapeutic index"),
"why: {overlay}"
);
assert!(!overlay.contains('X'), "bare variable leaked: {overlay}");
assert!(
!overlay.contains("varfarin"),
"raw cmevla leaked: {overlay}"
);
assert!(
!overlay.contains("siptucin"),
"raw cmevla leaked: {overlay}"
);
let fallback = summarize_proof_with(&trace, Register::Spec, None).unwrap();
assert!(
fallback.contains("varfarin is in danger"),
"why: {fallback}"
);
assert!(!fallback.contains('X'), "bare variable leaked: {fallback}");
assert!(
!fallback.contains("toxicity risk"),
"fallback must stay literal: {fallback}"
);
let tree = render_collapsed_text_with(
&trace,
Register::Spec,
0,
false,
Some(&DRUG_INTERACTIONS_OVERLAY),
);
assert!(
tree.contains("every drug that has a raised concentration and has a narrow therapeutic index is at toxicity risk"),
"tree:\n{tree}"
);
assert!(!tree.contains('X'), "bare variable leaked in tree:\n{tree}");
}
#[test]
fn ddi_why_alert_chains_to_the_regimen() {
let engine = engine_with_ddi_corpus();
let (_r, trace) = engine.query_text_raw_proof("warns(Varfarin).").unwrap();
let why = summarize_proof_with(&trace, Register::Spec, Some(&DRUG_INTERACTIONS_OVERLAY))
.expect("alert proof has a why summary");
assert!(why.contains("warfarin is at toxicity risk"), "why: {why}");
assert!(why.contains("Adam takes warfarin"), "why: {why}");
assert!(
why.contains("warfarin warrants a safety alert"),
"why: {why}"
);
assert!(!why.contains('X'), "bare variable leaked: {why}");
}
#[test]
fn ddi_general_rules_fire_for_second_drug() {
let engine = engine_with_ddi_corpus();
assert_true(
&engine.query_holds("dangerous(Fenitoin).").unwrap(),
"Phenytoin reaches toxicity risk via the same general toxicity rule as warfarin",
);
assert_false(
&engine.query_holds("warns(Fenitoin).").unwrap(),
"But phenytoin warrants NO alert: Adam does not take it (the alert is regimen-gated)",
);
}
#[test]
fn ddi_toxicity_requires_both_conditions() {
let wide = engine_with_facts(&[
"chemical(Raxitidin).",
"increases(Raxitidin).", "dangerous(every chemical where increases where thin).",
"warns(every chemical where dangerous).",
]);
assert_false(
&wide.query_holds("dangerous(Raxitidin).").unwrap(),
"A wide-margin drug with raised concentration is not at toxicity risk",
);
assert_false(
&wide.query_holds("warns(Raxitidin).").unwrap(),
"A wide-margin drug with raised concentration warrants no alert",
);
let narrow = engine_with_facts(&[
"chemical(Narotil).",
"thin(Narotil).", "dangerous(every chemical where increases where thin).",
"warns(every chemical where dangerous).",
]);
assert_false(
&narrow.query_holds("dangerous(Narotil).").unwrap(),
"A narrow-index drug with no interaction is not at toxicity risk",
);
assert_false(
&narrow.query_holds("warns(Narotil).").unwrap(),
"A narrow-index drug with no interaction warrants no alert",
);
}
#[test]
fn ddi_belief_revision_discontinue_inhibitor() {
let engine = fresh_engine();
for line in [
"chemical(Varfarin).",
"chemical(Fenitoin).",
"chemical(Flukonazol).",
"metabolized_by(Varfarin, Siptucin).",
"metabolized_by(Fenitoin, Siptucin).",
"thin(Varfarin).",
"thin(Fenitoin).",
"uses(Adam, Varfarin).",
] {
engine.assert_text(line).unwrap();
}
let inhibits_id = engine
.assert_text("prevents(Flukonazol, Siptucin).")
.unwrap()[0];
for line in [
"prevents(Flukonazol, Siptucin) & metabolized_by(Varfarin, Siptucin) -> increases(Varfarin).",
"prevents(Flukonazol, Siptucin) & metabolized_by(Fenitoin, Siptucin) -> increases(Fenitoin).",
"dangerous(every chemical where increases where thin).",
"all $da: dangerous($da) & uses(Adam, $da) -> warns($da).",
] {
engine.assert_text(line).unwrap();
}
assert_true(
&engine.query_holds("warns(Varfarin).").unwrap(),
"Warfarin alerts: at risk via the inhibitor AND on Adam's chart",
);
assert_true(
&engine.query_holds("dangerous(Fenitoin).").unwrap(),
"Phenytoin is at toxicity risk via the same shared inhibitor",
);
assert_false(
&engine.query_holds("warns(Fenitoin).").unwrap(),
"But phenytoin raises no alert: Adam does not take it (regimen-gated)",
);
engine.retract_fact(inhibits_id).unwrap();
assert_false(
&engine.query_holds("increases(Varfarin).").unwrap(),
"After discontinuation, warfarin's concentration no longer rises",
);
assert_false(
&engine.query_holds("dangerous(Varfarin).").unwrap(),
"After discontinuation, warfarin's toxicity basis is gone",
);
assert_false(
&engine.query_holds("warns(Varfarin).").unwrap(),
"After discontinuation, the warfarin alert is automatically withdrawn",
);
assert_false(
&engine.query_holds("dangerous(Fenitoin).").unwrap(),
"Discontinuing the shared inhibitor also clears phenytoin's toxicity risk",
);
}
#[test]
fn ddi_belief_revision_discontinue_drug() {
let engine = fresh_engine();
for line in [
"chemical(Varfarin).",
"chemical(Flukonazol).",
"metabolized_by(Varfarin, Siptucin).",
"thin(Varfarin).",
"prevents(Flukonazol, Siptucin).",
] {
engine.assert_text(line).unwrap();
}
let takes_id = engine.assert_text("uses(Adam, Varfarin).").unwrap()[0];
for line in [
"prevents(Flukonazol, Siptucin) & metabolized_by(Varfarin, Siptucin) -> increases(Varfarin).",
"dangerous(every chemical where increases where thin).",
"all $da: dangerous($da) & uses(Adam, $da) -> warns($da).",
] {
engine.assert_text(line).unwrap();
}
assert_true(
&engine.query_holds("dangerous(Varfarin).").unwrap(),
"Warfarin is at toxicity risk",
);
assert_true(
&engine.query_holds("warns(Varfarin).").unwrap(),
"Adam takes warfarin, so its alert fires",
);
engine.retract_fact(takes_id).unwrap();
assert_true(
&engine.query_holds("dangerous(Varfarin).").unwrap(),
"Warfarin is STILL pharmacologically at toxicity risk (drug-level, not regimen-gated)",
);
assert_false(
&engine.query_holds("warns(Varfarin).").unwrap(),
"But the alert is withdrawn: Adam no longer takes warfarin",
);
}
#[test]
fn ddi_witness_cyp2c9_substrates() {
let engine = engine_with_ddi_corpus();
let witnesses = engine
.query_find_text("metabolized_by($da, Siptucin).")
.unwrap();
let mut substrates: Vec<String> = witnesses
.iter()
.filter_map(|set| {
set.iter()
.find(|b| b.variable == "$da")
.map(|b| nibli_engine::display_term(&b.term))
})
.collect();
substrates.sort();
substrates.dedup();
assert!(
substrates.iter().any(|s| s.contains("varfarin")),
"warfarin should be a CYP2C9 substrate witness, got {substrates:?}"
);
assert!(
substrates.iter().any(|s| s.contains("fenitoin")),
"phenytoin should be a CYP2C9 substrate witness, got {substrates:?}"
);
assert!(
!substrates.iter().any(|s| s.contains("apiksaban")),
"apixaban (CYP3A4) must NOT appear as a CYP2C9 substrate, got {substrates:?}"
);
}
#[test]
fn ddi_regimen_count_aggregation() {
let engine = engine_with_ddi_corpus();
let n = engine.count_witnesses_text("uses(Adam, $da).").unwrap();
assert_eq!(
n, 2,
"Adam's regimen contains exactly two drugs (warfarin + fluconazole)"
);
}
#[test]
fn ddi_dose_sum_aggregation() {
let engine = engine_with_facts(&[
"quantity(Varfarin, 5).", "quantity(Fenitoin, 7).", ]);
let total = engine
.aggregate_text("quantity($da, $de).", "$de", EngineAggregateOp::Sum)
.unwrap();
assert_eq!(total, Some(12.0), "Summed dose across drugs should be 12");
}
#[test]
fn cyclic_rules_do_not_hang_count() {
use std::sync::mpsc;
use std::time::Duration;
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let engine = engine_with_facts(&["animal(every dog).", "dog(every animal).", "cat(Rex)."]);
let _ = tx.send(engine.count_witnesses_text("dog($da).").is_err());
});
match rx.recv_timeout(Duration::from_secs(20)) {
Ok(true) => {}
Ok(false) => panic!("cyclic count_witnesses_text must refuse (Err), not undercount"),
Err(_) => panic!(
"cyclic count_witnesses_text did NOT terminate within 20s \
— the backward-chain cycle guard regressed"
),
}
}
#[test]
fn ddi_temporal_alert_discrimination() {
let engine = engine_with_facts(&["now warns(Varfarin)."]);
assert_true(
&engine.query_holds("now warns(Varfarin).").unwrap(),
"A present-tense alert holds",
);
assert_false(
&engine.query_holds("past warns(Varfarin).").unwrap(),
"There was no alert in the past (tense discrimination)",
);
}
#[test]
fn find_witness_output_order_is_deterministic() {
let lines = ["dog(Zod).", "dog(Alis).", "dog(Mik).", "dog(Bob)."];
let e1 = engine_with_facts(&lines);
let e2 = engine_with_facts(&lines);
let render = |engine: &NibliEngine| -> Vec<String> {
engine
.query_find_text("dog(?).")
.unwrap()
.iter()
.map(|bindings| {
bindings
.iter()
.map(|b| format!("{} = {:?}", b.variable, b.term))
.collect::<Vec<_>>()
.join(", ")
})
.collect()
};
let r1a = render(&e1);
let r1b = render(&e1);
let r2 = render(&e2);
assert!(!r1a.is_empty(), "ma gerku should find witnesses");
assert_eq!(r1a, r1b, "repeated find on one engine must be order-stable");
assert_eq!(
r1a, r2,
"a fresh engine on the same corpus must produce identical find order"
);
}
#[test]
fn find_dependent_skolem_witness_event_decomposed_is_bound() {
let engine = engine_with_facts(&["dog(Adam).", "likes(every dog, some cat)."]);
let witnesses = engine.query_find_text("likes(Adam, ?).").unwrap();
assert!(!witnesses.is_empty(), "the rule provides a witness cat");
let terms: Vec<String> = witnesses
.iter()
.flat_map(|set| set.iter())
.map(|b| nibli_engine::display_term(&b.term))
.collect();
assert!(
terms.iter().all(|t| !t.contains("(_)") && !t.contains('?')),
"no witness term may be an unbound dependent Skolem, got {terms:?}"
);
assert!(
terms.iter().any(|t| t.contains("(adam)")),
"the dependent witness must be bound to its dependency adam, got {terms:?}"
);
let mut seen = std::collections::HashSet::new();
for set in &witnesses {
let key: Vec<(String, String)> = set
.iter()
.map(|b| (b.variable.clone(), nibli_engine::display_term(&b.term)))
.collect();
assert!(
seen.insert(key),
"duplicate binding set in find output: {witnesses:?}"
);
}
}
#[test]
fn surface_numeric_pilji_true_and_false() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("product(10, 2, 5).").unwrap(),
"10 = 2 × 5 must be derivable through surface Lojban",
);
assert_false(
&engine.query_holds("product(11, 2, 5).").unwrap(),
"11 = 2 × 5 must be FALSE through surface Lojban",
);
}
fn stub_tenfa_eval(rel: &str, _args: &[EngineLogicalTerm]) -> Result<bool, String> {
Ok(rel == "exponential")
}
fn stub_tenfa_batch(reqs: &[EngineComputeRequest]) -> Vec<Result<bool, String>> {
reqs.iter()
.map(|r| Ok(r.relation == "exponential"))
.collect()
}
#[test]
fn per_instance_compute_dispatch_is_isolated() {
let mut engine_a = fresh_engine();
engine_a.register_compute_predicate("exponential".to_string());
engine_a.set_compute_dispatch(stub_tenfa_eval, stub_tenfa_batch);
assert_true(
&engine_a.query_holds("exponential(8, 2, 3).").unwrap(),
"an engine with per-instance dispatch must resolve external `tenfa`",
);
let mut engine_b = fresh_engine();
engine_b.register_compute_predicate("exponential".to_string());
let r = engine_b.query_holds("exponential(8, 2, 3).").unwrap();
assert!(
!r.is_true(),
"an engine WITHOUT dispatch must not resolve external `tenfa`: got {r:?}"
);
assert_eq!(
r.detail_label(),
Some("backend-unavailable"),
"unresolved compute dispatch must surface backend-unavailable, not FALSE: got {r:?}"
);
}
#[test]
fn overflowing_numeric_literal_fails_closed_at_parse() {
let nines = "so ".repeat(320); let engine = fresh_engine();
let err = engine
.query_holds(&format!("li {nines}cu dunli li {nines}"))
.expect_err("an overflowing numeric literal must be a parse error, not a verdict");
assert!(
matches!(err, EngineError::Syntax(_)),
"the overflow rejection must be the typed syntax error, got: {err}"
);
}
#[test]
fn surface_numeric_sumji_dilcu() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("sum(5, 2, 3).").unwrap(),
"5 = 2 + 3 must be TRUE through surface Lojban",
);
assert_false(
&engine.query_holds("sum(6, 2, 3).").unwrap(),
"6 = 2 + 3 must be FALSE through surface Lojban",
);
assert_true(
&engine.query_holds("quotient(3, 6, 2).").unwrap(),
"3 = 6 / 2 must be TRUE through surface Lojban",
);
assert_false(
&engine.query_holds("quotient(3, 6, 0).").unwrap(),
"division by zero must be FALSE, not an error",
);
}
#[test]
fn surface_numeric_float_tolerance() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("sum(0.3, 0.1, 0.2).").unwrap(),
"0.3 = 0.1 + 0.2 must be TRUE (tolerant float equality)",
);
}
fn mock_compute_server(response: &str) -> String {
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let resp = response.to_string();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let mut reader = BufReader::new(stream);
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) => {
let mut r = resp.clone();
r.push('\n');
if reader.get_mut().write_all(r.as_bytes()).is_err() {
break;
}
let _ = reader.get_mut().flush();
}
}
}
}
});
addr
}
#[test]
fn native_compute_backend_dispatches_external_predicate() {
let addr = mock_compute_server(r#"{"result": true}"#);
let mut engine = fresh_engine();
engine.enable_compute_backend(&addr);
engine.register_compute_predicate("exponential".to_string());
assert_true(
&engine.query_holds("exponential(8, 2, 3).").unwrap(),
"tenfa dispatches through the native TCP client to the backend",
);
}
#[test]
fn native_compute_backend_is_opt_in() {
let mut engine = fresh_engine();
engine.register_compute_predicate("exponential".to_string());
let r = engine.query_holds("exponential(8, 2, 3).").unwrap();
assert!(
!r.is_true(),
"tenfa with no backend wired must not be TRUE: {r:?}"
);
}
#[test]
#[ignore = "starts the Python compute backend; run with --ignored from the repo root"]
fn native_compute_backend_real_python_tenfa() {
let port = "15556";
let addr = format!("127.0.0.1:{port}");
let script = concat!(env!("CARGO_MANIFEST_DIR"), "/../python/nibli_backend.py");
let mut child = std::process::Command::new("python3")
.args([script, "--port", port])
.spawn()
.expect("failed to start python3 (needs python3 on PATH)");
let mut ready = false;
for _ in 0..50 {
if std::net::TcpStream::connect(&addr).is_ok() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
let run = || {
let mut engine = fresh_engine();
engine.enable_compute_backend(&addr);
engine.register_compute_predicate("exponential".to_string());
let t = engine.query_holds("exponential(8, 2, 3).").unwrap();
let f = engine.query_holds("exponential(9, 2, 3).").unwrap();
(t, f)
};
let result = std::panic::catch_unwind(run);
let _ = child.kill();
let _ = child.wait(); assert!(ready, "Python backend did not start on {addr}");
let (t, f) = result.expect("query panicked");
assert_true(&t, "8 = 2^3 must be TRUE through the real Python backend");
assert_false(&f, "9 = 2^3 must be FALSE through the real Python backend");
}
#[test]
fn surface_numeric_comparison_greater_less_num_equal() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("greater(5, 3).").unwrap(),
"5 > 3 must be TRUE through surface Lojban",
);
assert_false(
&engine.query_holds("greater(3, 5).").unwrap(),
"3 > 5 must be FALSE through surface Lojban",
);
assert_true(
&engine.query_holds("less(2, 3).").unwrap(),
"2 < 3 must be TRUE through surface Lojban",
);
assert_true(
&engine.query_holds("num_equal(3, 3).").unwrap(),
"3 == 3 must be TRUE through surface Lojban",
);
assert_false(
&engine.query_holds("num_equal(3, 2).").unwrap(),
"3 == 2 must be FALSE through surface Lojban",
);
}
#[test]
fn assert_numeric_comparison_rejected() {
let engine = fresh_engine();
for line in ["greater(3, 5).", "less(5, 3).", "num_equal(5, 3)."] {
let err = engine
.assert_text(line)
.expect_err("asserting a numeric comparison must be rejected");
assert!(
err.to_string().contains("computed comparison"),
"expected the computed-comparison rejection for `{line}`, got: {err}"
);
}
engine
.assert_text("greater(Alis, Bob).")
.expect("a non-numeric relational comparison must still assert");
assert_true(
&engine.query_holds("greater(5, 3).").unwrap(),
"5 > 3 must still compute TRUE at query time",
);
}
#[test]
fn surface_numeric_negation() {
let engine = fresh_engine();
assert_true(
&engine.query_holds("~greater(3, 5).").unwrap(),
"NOT(3 > 5) must be TRUE through surface Lojban",
);
assert_false(
&engine.query_holds("~greater(5, 3).").unwrap(),
"NOT(5 > 3) must be FALSE through surface Lojban",
);
}
#[test]
fn surface_numeric_traced_verdicts_agree() {
let engine = fresh_engine();
let (verdict, trace, _json) = engine.query_text_with_proof("product(10, 2, 5).").unwrap();
assert_true(&verdict, "traced 10 = 2 × 5 must be TRUE");
assert!(
trace.contains("product"),
"trace should mention the computed relation: {trace}"
);
}
#[test]
fn closed_world_false_carries_cwa_note_but_numeric_false_does_not() {
let engine = fresh_engine();
let (v1, proof1, _) = engine.query_text_with_proof("dog(Adam).").unwrap();
assert!(v1.is_false(), "a missing fact must be FALSE: got {v1:?}");
assert!(
proof1.contains("FALSE is closed-world"),
"an absence-driven FALSE must carry the closed-world caveat: {proof1}"
);
let (v2, proof2, _) = engine.query_text_with_proof("num_equal(5, 3).").unwrap();
assert!(v2.is_false(), "`5 = 3` must be FALSE: got {v2:?}");
assert!(
!proof2.contains("FALSE is closed-world"),
"a numeric-decided FALSE must NOT carry the closed-world caveat: {proof2}"
);
}
#[test]
fn injected_fact_matches_surface_text_query() {
let engine = fresh_engine();
engine
.assert_fact_direct(
"dog".to_string(),
vec![nibli_engine::EngineLogicalTerm::Constant(
"adam".to_string(),
)],
)
.unwrap();
assert_true(
&engine.query_holds("dog(Adam).").unwrap(),
"directly-injected gerku(adam) must satisfy the surface text query",
);
}
#[test]
fn injected_fact_multiplace_arity_padding_matches_text_query() {
let engine = fresh_engine();
engine
.assert_fact_direct(
"goes".to_string(),
vec![
nibli_engine::EngineLogicalTerm::Constant("adam".to_string()),
nibli_engine::EngineLogicalTerm::Constant("paris".to_string()),
],
)
.unwrap();
assert_true(
&engine.query_holds("goes(Adam, Paris).").unwrap(),
"injecting a 5-place predicate with 2 args must pad and still match the text query",
);
}
#[test]
fn injected_known_over_arity_fails_closed() {
let engine = fresh_engine();
let e = engine
.assert_fact_direct(
"product".to_string(),
(0..4)
.map(|n| nibli_engine::EngineLogicalTerm::Number(n as f64))
.collect(),
)
.unwrap_err();
let msg = format!("{e}");
assert!(
msg.contains("arity 3") && msg.contains("4 arguments"),
"{msg}"
);
}
#[test]
fn injected_unknown_arity_is_callers_count() {
let engine = fresh_engine();
let args: Vec<_> = ["a", "b", "c"]
.iter()
.map(|n| nibli_engine::EngineLogicalTerm::Constant(n.to_string()))
.collect();
engine
.assert_fact_direct("zzz_unknown_rel".to_string(), args)
.expect("a 3-arg unknown injected fact must be accepted at arity 3");
}
#[test]
fn injected_fact_is_findable_as_witness() {
let engine = fresh_engine();
engine
.assert_fact_direct(
"dog".to_string(),
vec![nibli_engine::EngineLogicalTerm::Constant(
"adam".to_string(),
)],
)
.unwrap();
let witnesses = engine.query_find_text("dog(?).").unwrap();
assert!(
!witnesses.is_empty(),
"injected gerku(adam) should yield a witness binding"
);
let mentions_adam = witnesses
.iter()
.flat_map(|set| set.iter())
.any(|b| nibli_engine::display_term(&b.term).contains("adam"));
assert!(
mentions_adam,
"the discovered witness should be adam: {witnesses:?}"
);
}
#[test]
fn belief_does_not_leak_as_actuality() {
let engine = fresh_engine();
engine
.assert_text("believe(me, fact { goes(me) }).")
.unwrap();
assert_false(
&engine.query_holds("goes(me).").unwrap(),
"believing P must not entail P (no abstraction-content leak)",
);
assert_true(
&engine
.query_holds("believe(me, fact { goes(me) }).")
.unwrap(),
"the belief itself is preserved and queryable",
);
assert_false(
&engine
.query_holds("believe(me, fact { eats(me) }).")
.unwrap(),
"believing P must not satisfy a query about believing a different proposition",
);
}
#[test]
fn abstraction_subject_does_not_leak_inner_predicate() {
let engine = fresh_engine();
engine.assert_text("big(fact { goes(me) }).").unwrap();
assert_false(
&engine.query_holds("goes(me).").unwrap(),
"an abstraction used as a subject must not leak its inner predicate",
);
}
#[test]
fn unresolvable_query_after_reset_errors() {
let engine = engine_with_facts(&["dog(Adam)."]);
engine.reset();
assert!(
engine.query_holds("go'i").is_err(),
"an unresolvable spelling must error, not answer"
);
}
#[test]
fn predicate_less_clause_rejected() {
let engine = fresh_engine();
let err = engine.assert_text("every dog").unwrap_err();
assert!(
matches!(err, EngineError::Syntax(_)),
"a bare sumti must be a Syntax error, got {err:?}"
);
assert!(
err.to_string().contains("expected a predicate word"),
"expected the bare-term parse rejection, got: {err}"
);
assert!(engine.assert_text("dog(Adam).").is_ok());
}
#[test]
fn deep_chain_query_completes_within_watchdog() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let engine = nibli_engine::NibliEngine::new();
engine.assert_text("dog(Adam).").unwrap();
let chain = [
"dog", "animal", "alive", "big", "fast", "healthy", "thin", "eats", "goes",
];
for w in chain.windows(2) {
engine
.assert_text(&format!("{}(every {}).", w[1], w[0]))
.unwrap();
}
let result = engine.query_holds("goes(Adam).").unwrap();
tx.send(result.is_true()).unwrap();
});
let is_true = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("deep-chain query exceeded the 10 s watchdog (cliff regression)");
assert!(is_true, "the 8-hop chain must derive TRUE");
}
#[test]
fn an_undeclared_kb_admits_everything() {
let engine = engine_with_facts(&["person(Adam).", "rich(Adam).", "banana(Adam)."]);
assert!(!engine.kb().vocabulary_is_closed());
assert!(engine.kb().admitted_relations().is_empty());
assert_true(
&engine.query_holds("rich(Adam).").unwrap(),
"open KB asserts",
);
}
#[test]
fn admits_closes_the_vocabulary_fail_closed() {
let engine = engine_with_facts(&["admits(\"person\").", "person(Adam)."]);
assert!(engine.kb().vocabulary_is_closed());
assert_eq!(engine.kb().admitted_relations(), vec!["person".to_string()]);
let err = engine
.assert_text("rich(Adam).")
.expect_err("an unadmitted relation must be refused");
let msg = format!("{err}");
assert!(
msg.contains("not admitted vocabulary") && msg.contains("rich"),
"message must name the relation and the reason: {msg}"
);
assert_false(
&engine.query_holds("rich(Adam).").unwrap(),
"a refused assertion must not half-land",
);
}
#[test]
fn a_closed_vocabulary_still_derives_outside_itself() {
let engine = engine_with_facts(&[
"admits(\"person\").",
"person(Adam).",
"all $x: person($x) -> prisoner($x).",
]);
assert_true(
&engine.query_holds("prisoner(Adam).").unwrap(),
"closing the base vocabulary must not close the derived one",
);
assert!(engine.assert_text("prisoner(Bela).").is_err());
}
#[test]
fn an_admits_block_below_the_facts_is_refused() {
let engine = engine_with_facts(&["person(Adam)."]);
let err = engine
.assert_text("admits(\"person\").")
.expect_err("a late declaration must be refused, not silently honoured");
assert!(
format!("{err}").contains("comes too late"),
"must say why: {err}"
);
assert!(
!engine.kb().vocabulary_is_closed(),
"the refusal is atomic — the vocabulary must NOT have closed"
);
}
#[test]
fn retraction_replay_does_not_reopen_the_vocabulary() {
let engine = engine_with_facts(&["admits(\"person\").", "person(Adam).", "person(Bela)."]);
let ids = engine.assert_text("person(Cira).").unwrap();
engine.retract_fact(ids[0]).expect("retract");
assert!(
engine.kb().vocabulary_is_closed(),
"a retraction must not re-open a closed vocabulary"
);
assert!(
engine.assert_text("rich(Adam).").is_err(),
"and the closure must still refuse after replay"
);
}
#[test]
fn reset_reopens_the_vocabulary() {
let engine = engine_with_facts(&["admits(\"person\").", "person(Adam)."]);
assert!(engine.kb().vocabulary_is_closed());
engine.reset();
assert!(!engine.kb().vocabulary_is_closed());
assert!(engine.assert_text("rich(Adam).").is_ok());
}