Explicit negatives and validation reports

gestaltdb.reasoning.validation checks a finite typed closure against an explicit ValidationSuite and returns bounded Violation records with exact supporting facts and constraint identities. Validation never infers, never merges, and never explodes: the input closure stands unchanged, and a report is evidence — not a classical inconsistency and not a shape-validation certificate beyond the implemented checks.

from gestaltdb.reasoning.terms import Literal
from gestaltdb.reasoning.typed import TypedFact
from gestaltdb.reasoning.validation import Denial, ValidationSuite, validate

facts = [TypedFact("type", ["mallory", "Cat"]), TypedFact("type", ["mallory", "Dog"])]
suite = ValidationSuite("type", disjoint=[("Cat", "Dog")])
report = validate(facts, suite)
assert not report.clean
assert report.violations[0].kind == "disjoint-members"
assert report.violations[0].sources == ("disjoint:Cat:Dog",)

What is checked

  • Denied positives: an explicit Denial(predicate, values) contradicting an identical ground fact. Absence of a fact is not a denial; only stated denials participate, so open-world silence never becomes a conflict.

  • Disjoint membership: two membership facts sharing an entity under disjoint classes. Membership reads the suite’s explicit membership predicate (callers validating ontology closures pass the same predicate the compiler emits); no vocabulary is assumed.

  • Reflexive loops: P(x, x) facts on properties declared irreflexive.

  • Functional conflicts: two facts sharing subject and predicate with different remaining values. The pair is reported with a note that equality is not inferred — equality-generating functionality belongs to RE-11.

max_violations bounds the report; hitting it sets truncated instead of failing, and deterministic canonical order makes repeat runs identical. cancel cooperates during indexed input processing and raises without a report; input is fully buffered and sorted before those periodic checks. It does not interrupt a blocking input iterator. max_violations limits report size, not input memory. Violations reference supporting TypedFact values and stable constraint identities (disjoint:A:B, functional:age), so every conflict traces to exact logical input facts and a structural scope entry. These source strings are generated constraint labels, not caller-supplied axiom IDs or exact historical claim-version references.

What is not claimed

Domain and range axioms keep their entailment meaning in the compiler; they do not validate required fields, and validation never emits facts. Unsatisfiable classes (find_unsatisfiable) are a static schema check over subclass edges and disjointness pairs — no instance data is read, and anonymous constructions are out of scope. Full SHACL-style shapes, satisfiability beyond disjointness closure, probabilistic combination and classical explosion are explicitly deferred; the report vocabulary stays within the four implemented checks and fails closed on anything else. Ground Denial values are validation inputs, not polarity-aware rule premises: missing positive evidence cannot satisfy a denied premise. Generic rules support positive atoms and explicitly scoped absence, not signed-denial premises. Datatype shape/constraint checking is not provided by this suite; strict Literal construction is a separate contract.

Measured cost

The original small-workload measurement was about 70ms warm for 20,000 typed facts with 100 planted disjointness conflicts (single process). It is not a general complexity or release-performance guarantee: validation buffers the closure, sorts/deduplicates it, and some checks rescan it per constraint or conflict. See #200 (D6) for the indexing/cancellation follow-up; callers must bound their input independently.