Why evidence¶
A SHACL validation report is a list of what went wrong. That is the right output for a gate — a build step that should fail on bad data needs a boolean and a diagnostic — and it is a lossy summary of what the validator computed.
The validator decided conformance by structural recursion over the constraint. At every step it knew which sub-constraint held, on which values, supported by which triples. The evidence interface retains that derivation after computing the boolean.
Information absent from validation reports¶
Why a node passed. A report has no row for a conforming node, so there is no way to ask what satisfied the constraint. If your shape says a VAV has a supply-air temperature sensor, and it does, the report will not tell you which sensor — you have to write a second query that re-implements the shape’s property paths and qualified-value filtering. That query is a duplicate of logic the validator already executed, and it can drift out of sync with the shape. Shape map reference exists to eliminate it.
Whether a node was checked at all. In a report, a node that passed and a node no target selected look identical: absent. For a coverage question — “which of my assets did this profile actually apply to?” — that is exactly the distinction you need.
Why the failure is a failure. A report gives a message and a constraint component. It does not give the shape of the derivation: which branch of a disjunction was tried, which values were counted, which triples supported the path that reached the offending value. Anything downstream that wants to act on a failure has to reconstruct it.
The interface is statement-oriented¶
Evidence is organised around authored statements rather than findings. Every
statement that was included in the run appears, and each selected
(statement, focus) pair gets exactly one row of one polarity:
EvidenceRun
└── StatementEvaluation one per included authored statement
├── selected_foci = [] target selected nothing
└── FocusEvaluation one per selected focus node
├── status = "pass" → Satisfaction
└── status = "fail" → Failure
No row means unselected, a pass row means checked and held, and a fail
row means checked and did not.
Retaining statements whose target selected nothing is the part that requires
deliberate effort — the easy implementation drops them, and with them the
answer to “did this shape apply to anything?”
Satisfaction and failure evidence¶
Satisfaction and Failure are not two report formats that happen to
resemble each other. They are logical complements, computed by mutually
recursive folds over the same arena with the same conformance oracle, and they
share their traversal and projection code.
The mutual recursion is forced by negation. To explain why ¬φ failed, you
have to explain why φ held — so the failure fold calls the satisfaction
fold, and vice versa. Every ¬ flips the direction. Counting is the other
flip point, and it is self-dual: a lower bound is broken by removing matches, an
upper bound by adding them.
This is also why satisfaction evidence is not a nice-to-have. Repair needs it: when a repair crosses a negation, the only way to fix the failure is to falsify something that currently holds, and the satisfaction trace is the record of what to falsify. The two polarities are one machine because the problem is one problem.
Canonical evidence¶
A failed conjunction retains the children that establish the failure and drops the ones that passed.
Canonical evidence answers why did this result hold? The answer to that question, for a conjunction, is the failing conjunct. A passing sibling is not part of the explanation — including it would inflate every failure tree with irrelevant material, and the trees are already large enough to be a performance concern (see Evidence performance). It also happens to be exactly the shape repair needs, since a repair must address the failing conjunct and has no business touching the passing one.
But a UI often does want the siblings: “three of these four obligations are
met” is useful to a person, and it is not what a proof contains. So the
authored children are available separately, through
FocusEvaluation.progress, which reports the immediate authored children and
their statuses without materializing why each held.
Canonical evidence, progress, and on-demand evidence provide different views:
canonical evidence answers why did this result hold?
progress answers what happened to the immediate authored children while evaluating it?
session.evidence_for(focus, constraint_id) materializes the full evidence for one of those elided children, on demand.
Source and normalized identities¶
Evidence carries both a source and a normalized identity for every statement and constraint. This is a direct consequence of compiling shapes (How shapes are compiled): the normalizer deduplicates structurally identical nodes, folds contradictions, and rewrites boolean structure, so the executed algebra does not correspond one-to-one with what you wrote.
The source identity is what you correlate with author intent — this constraint came from that line of that shapes file. The normalized identity is what actually ran, and several source statements may share one after common-subexpression elimination. Discarding either one loses something: with only source ids you cannot explain the execution, and with only normalized ids you cannot point at the SHACL the user wrote.
Limits of evidence¶
The validation status is exact everywhere. The explanation is not always available, and the cases where it is missing are marked in the tree rather than silently degraded.
A sh:sparql constraint is opaque. An arbitrary SPARQL query is not
something the algebra can fold over, so a failing one carries its query
diagnostic and nothing structural, and a passing one is blocked for repair
purposes — a query cannot generally be falsified by a sound deletion. SHACL-AF
expression failures are opaque for the same reason. Passing closed and
relational constraints are blocked only in the deletive direction, which does
not affect their validation result.
Under greatest-fixed-point semantics (Recursion and stratification), a node can conform
because no counterexample is
reachable, and there is then no finite set of supporting triples to point at.
Evidence records a coinductive leaf. That is a real limit, not a placeholder
for missing work.
PathSupport records one concrete successful route rather than enumerating
all of them. For an alternative path,
Shifty keeps the first successful syntactic alternative. So a path support is a
positive reachability certificate and is not a deletion cut — anything
derived from it is a candidate that still has to pass the repair gate.
The costs¶
Materializing evidence for every selected pair costs 2.5–5.4x deciding conformance, rising with model size, and a mid-size model’s serialized run can reach tens of megabytes before compaction.
That is why the interface is a set of graded entry points rather than one function: decide conformance, find which pairs failed, explain one pair, or explain everything. Most callers want to know why something failed, and failures are a small minority of pairs — so finding the failures and explaining each one cost 3–34% over plain conformance on measured Brick models, compared with 2.5–5.4 times for full evidence. Evidence performance gives the choice of entry point; Evidence performance study has the measurements and attribution.