Unified inference audit

gestaltdb.inference.explain_inference joins explicitly tagged model-output claims with the existing historical logical explanation graph. This read-only adapter returns a detached InferenceExplanation with deterministic to_json(), copy-returning to_dict() and script-free as_html(). It does not run a model, request network resources, or change rule semantics.

Model outputs are assertions, not proofs

Tag model outputs with provenance['model_output'] containing explicit model_id and model_version strings. Optional training-data identifiers, feature snapshot identifiers or calibration notes can be stored alongside them. These fields are caller assertions, not authenticated model lineage. No association is guessed from confidence, agent names, or predicates.

Confidence remains a score on an assertion. A deterministic rule consuming a prediction proves only what follows assuming that assertion. It does not prove the prediction correct, propagate probabilities, or explain the model’s internal decisions. This export is an audit record, not independent proof verification or a regulatory-compliance certificate.

Runnable model-to-proof example

This example requires the lmdb extra and closes the database explicitly. The prediction could come from a GNN; the example deliberately uses a fixed score rather than requiring a training framework.

from pathlib import Path
from tempfile import TemporaryDirectory
from gestaltdb.graphdb import GraphDB
from gestaltdb.inference import explain_inference

with TemporaryDirectory() as directory:
    graph = GraphDB.create(Path(directory) / "db", backend="lmdb", serializer="json")
    try:
        prediction = graph.assert_claim(
            subject="supplier-1", predicate="PREDICTED_RISK", object="high",
            polarity="positive", agent="gnn-risk", confidence=0.8, valid=(0, 100),
            provenance={"model_output": {
                "model_id": "supplier-risk-gnn", "model_version": "weights-7"}},
        )
        graph.create_rule(
            "review-predictions", when=[("?s", "PREDICTED_RISK", "high")],
            then=("?s", "REQUIRES_REVIEW", "yes"),
        )
        graph.run_rules(as_of=10)
        conclusion = next(graph.iter_claims_as_of(
            valid_time=10, subject="supplier-1", predicate="REQUIRES_REVIEW"))
        report = explain_inference(graph, conclusion.logical_id, valid_time=10)
        assert any(node["kind"] == "model_output" for node in report.to_dict()["nodes"])
        Path(directory, "audit.json").write_text(report.to_json(), encoding="utf-8")
        Path(directory, "audit.html").write_text(report.as_html(), encoding="utf-8")
    finally:
        graph.close()

Identity, alternatives and bounds

Format-v1 exports carry the root logical claim/version IDs, requested valid time, resolved system time, exact claim/rule versions, validity, commit ID and ordinal, payload hashes, scores and provenance. Alternative justifications retain their ordered premise-version groups per conclusion. They are not collapsed through shared rule nodes; recursive evidence may be a finite graph, not a tree. Rules using closed-world negation also export absence nodes and per-support absences lists. These record absence checks at the evaluation horizon; they are not claim versions, negative facts or independent proof that the closed-world declaration was justified. Evidence is untrusted data and can contain sensitive metadata; review it before sharing. JSON serialization does not authenticate the report.

max_depth and max_nodes use the existing explain_claim traversal bounds. Missing/depth-limited evidence sets truncated=true; incomplete justifications are omitted, never presented as proofs. Additional max_edges and max_bytes bound the exported justification links and serialized JSON; overflow raises RuleEvaluationLimitError without an artifact. These are export bounds, not a guarantee of bounded backend reads or serializer memory. HTML escaping prevents metadata from becoming executable markup.

Absent or retracted roots return None. Pass system_time to inspect an older visible root. Exact premise versions remain historical even if corrected later. export_inference also accepts an existing ClaimExplanation; it does not authenticate manually constructed values.