"""Tests for explanation records: 'why does the system believe this?' Every active claim must carry an explanation that names its deriver, cites its supporting evidence, exposes the confidence factors that were combined, matches the claim's confidence, serializes to JSON for audit export, and remains retrievable after refutation (audit trail).""" from __future__ import annotations import json import math from mnema.derive.model import ClaimStatus def test_explanation_identifies_claim_and_deriver(ran): engine, result, _evidence = ran for claim in result.new_claims: expl = engine.explain(claim.claim_id) assert expl.claim_id == claim.claim_id assert isinstance(expl.deriver, str) and expl.deriver.strip(), ( f"explanation for {claim.claim_id} does not name its deriver" ) def test_explanation_summary_is_human_readable(ran): engine, result, _evidence = ran for claim in result.new_claims: expl = engine.explain(claim.claim_id) assert isinstance(expl.summary, str) assert len(expl.summary.strip()) >= 15, ( f"summary for {claim.claim_id} is too short to explain anything: " f"{expl.summary!r}" ) assert "{" not in expl.summary and "}" not in expl.summary, ( f"summary for {claim.claim_id} looks like an unrendered template: " f"{expl.summary!r}" ) def test_explanation_cites_known_supporting_inputs(ran): engine, result, evidence = ran known_ids = {e.evidence_id for e in evidence} | { c.claim_id for c in engine.claims() } for claim in result.new_claims: expl = engine.explain(claim.claim_id) assert expl.evidence_ids, ( f"explanation for {claim.claim_id} cites no supporting inputs" ) stray = set(expl.evidence_ids) - known_ids assert not stray, ( f"explanation for {claim.claim_id} cites unknown ids: {sorted(stray)}" ) def test_explanation_factors_are_named_and_finite(ran): engine, result, _evidence = ran for claim in result.new_claims: expl = engine.explain(claim.claim_id) assert expl.factors, f"explanation for {claim.claim_id} has no factors" for factor in expl.factors: assert isinstance(factor.name, str) and factor.name.strip() w = float(factor.weight) assert math.isfinite(w), ( f"factor {factor.name!r} on {claim.claim_id} has non-finite weight" ) def test_explanation_confidence_matches_claim(ran): engine, result, _evidence = ran for claim in result.new_claims: expl = engine.explain(claim.claim_id) assert abs(float(expl.confidence) - float(claim.confidence)) < 1e-9, ( f"explanation confidence for {claim.claim_id} disagrees with the claim" ) def test_explanations_serialize_to_json(ran): engine, result, _evidence = ran for claim in result.new_claims: expl = engine.explain(claim.claim_id) payload = expl.to_dict() encoded = json.dumps(payload, sort_keys=True, default=str) decoded = json.loads(encoded) assert claim.claim_id in encoded, ( "serialized explanation must reference its claim id" ) assert isinstance(decoded, dict) def test_explanation_survives_refutation_as_audit_record(ran): engine, result, _evidence = ran claim = result.new_claims[0] engine.refute(claim.claim_id, reason="testing audit trail") assert engine.graph.status(claim.claim_id) == ClaimStatus.REFUTED expl = engine.explain(claim.claim_id) assert expl.claim_id == claim.claim_id, ( "explanations must remain retrievable for refuted claims (audit trail)" ) assert isinstance(expl.summary, str) and expl.summary.strip()