| name | shacl-validation |
| description | Validate RDF data against SHACL shapes with the sparq engine: SHACL Core constraints (class, datatype incl. the SHACL-1.2 disjunctive list form, cardinality, ranges, paths, logical, node/property, qualified, closed incl. sh:ByTypes, in/hasValue, the SHACL-1.2 list constraints sh:memberShape / sh:uniqueMembers / sh:min+maxListLength / sh:uniqueValuesFor, and the SHACL-1.2 value constraints sh:subsetOf / sh:someValue / sh:singleLine / sh:rootClass with path-valued sh:equals/disjoint/lessThan comparands and severity-threshold sh:conforms), SHACL-SPARQL sh:sparql constraints (§5.2), and custom SPARQL-based constraint components (sh:ConstraintComponent, §6) — then read the conformance/violations validation report as N-Triples, deterministic JSON, W3C report-vocabulary Turtle, or human text. Also runs opt-in SHACL Advanced Features (SHACL-AF) rules — sh:rule (sh:TripleRule + sh:SPARQLRule) — to INFER triples (feature `shacl-af`), and assembles the shapes graph itself — sh:shapesGraph discovery + transitive owl:imports closure via a caller-supplied loader (feature `imports`). Use when an agent needs to check whether a sparq_core::Graph conforms to shapes, run shape validation, produce a SHACL validation report, assemble a shapes graph from sh:shapesGraph/owl:imports references, or apply SHACL rules to infer/expand a graph in Rust. |
sparq-shacl-validation
Validate a data Graph against a shapes Graph and get back a ValidationReport
(conformance flag + per-violation results, renderable as N-Triples, deterministic
JSON, W3C-vocabulary Turtle, or plain text). Covers the full SHACL Core component set,
SHACL-SPARQL (sh:sparql, §5.2), and custom SPARQL-based constraint components
(sh:ConstraintComponent, §6).
sparq-shacl is an opt-in crate: depending on it is what turns on SHACL. It is
NOT a dependency of any other sparq crate by default, so the core engine and the
default wasm bundle carry zero SHACL code/cost unless you pull it in. The browser/JS
consumer opts in through sparq-wasm's non-default shacl feature, which exposes
validate as a stateless Store.validate(data, shapes, format) wasm binding
returning a JSON report — a drop-in for rdf-validate-shacl (sq-yqi1, #162). On that
wasm32 build the sparq-engine dep drops its defaults so rayon never enters the
bundle; see the javascript-wasm skill for the JS API + report shape.
For the showcase site there is also a standalone, lazy-loaded wasm bundle,
sparq-shacl-wasm (the tier-b "W-shacl" artifact, sq-lfmf), kept separate from the lean
default bundle so SHACL never ships on the landing page. It exposes a stateless
Validator with the FULL report surface — Validator.validate(data, shapes, format)
(JSON report), validateTurtle (report-RDF in the sh:ValidationReport vocabulary),
validateText (human-readable), and conforms(..., violationsOnly) (the W3C
sh:conforms flag, or a violations-only gate). SHACL-AF sh:rule validation is behind
its opt-in shacl-af feature. See crates/sparq-shacl-wasm/README.md.
Quickstart
Cargo.toml:
[dependencies]
sparq-core = { path = "../sparq-core" }
sparq-shacl = { path = "../sparq-shacl" }
use sparq_core::Graph;
let data = Graph::load_str(r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person ; ex:age "thirty" . // age is a string, not an integer
"#, "turtle").unwrap();
let shapes = Graph::load_str(r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:minCount 1 ] .
"#, "turtle").unwrap();
let report = sparq_shacl::validate(&data, &shapes);
assert!(!report.conforms);
assert_eq!(report.results.len(), 1);
eprintln!("{}", report.to_text());
println!("{}", report.to_turtle());
println!("{}", report.to_ntriples());
println!("{}", report.to_json());
CLI-style end-to-end run via the bundled example (exits 0 iff the data conforms):
cargo run -p sparq-shacl --example validate -- data.ttl shapes.ttl
cargo run -p sparq-shacl --example validate -- data.ttl shapes.ttl --turtle
Key APIs
Top-level functions (sparq_shacl::…):
pub fn validate(data: &Graph, shapes: &Graph) -> ValidationReport;
pub fn validate_with_model(data: &Graph, model: &ShapesModel) -> ValidationReport;
pub fn validate_strict(data: &Graph, shapes: &Graph) -> Result<ValidationReport, ShaclFailure>;
pub fn validate_strict_with_model(data: &Graph, model: &ShapesModel)
-> Result<ValidationReport, ShaclFailure>;
pub fn load_turtle_with_base(text: &str, base: &str) -> Result<Graph, String>;
pub fn graph_from_triples<I: IntoIterator<Item = oxrdf::Triple>>(triples: I) -> Graph;
pub fn sparql_constraint_executions() -> u64;
ValidationReport::conforms honours a shapes-graph sh:conformanceDisallows
declaration (SHACL 1.2 Core §3.9, sq-5q76d) — e.g. a graph that disallows only
sh:Violation conforms despite a sh:Warning result — falling back to the default
{Violation, Warning, Info} set. sh:reifierShape / sh:reificationRequired validate
the RDF-1.2 reifiers of a value's asserted triple, and sh:uniqueLang keys on the
rdf:dirLangString base direction (@ar, @ar--ltr, @ar--rtl are distinct keys).
SHACL Compact Syntax (SCS) parser (opt-in feature scs) — the parse
direction of the W3C SCS (sparq_shacl::scs::…, re-exported at the crate root):
pub fn parse_scs(text: &str, base: &str) -> Result<Vec<oxrdf::Triple>, ScsError>;
pub fn parse_scs_to_graph(text: &str, base: &str) -> Result<Graph, ScsError>;
pub const DEFAULT_BASE: &str;
pub struct ScsError { pub line: usize, pub message: String }
It emits the SAME shapes triples validate consumes, so an SCS document validates
data identically to the equivalent Turtle. Covers the grammar the W3C shacl12-cs
corpus exercises (32/32 fixtures round-trip graph-isomorphically): directives,
shape/shapeClass, full path expressions, [min..max], nodeKind, bare-IRI
sh:datatype-vs-sh:class, @shape-refs (sh:node), param=value, ! (sh:not),
| (sh:or), nested {...} shapes (sh:node), and [ ... ] arrays (sh:in /
sh:ignoredProperties). The browser/JS surface exposes this as the opt-in
Store.parseShaclCompact(text, base?) wasm binding (sq-quly) — SCS text → the shapes
graph as a Turtle string, behind sparq-wasm's non-default scs feature; see the
javascript-wasm skill for the JS API.
Shapes-graph assembly: sh:shapesGraph + owl:imports (opt-in feature
imports, sq-uz0) — W3C SHACL §§3.1/3.3 (shapes-graph) shapes-graph discovery and union, so
callers no longer hand-assemble the shapes graph. Dereferencing an IRI to a
document stays a caller concern (the engine never touches the network): you
supply a loader callback, the library owns the traversal, the per-IRI dedupe /
cycle guard, the RDF-merge discipline (each loaded document's blank nodes are
standardised apart so labels reused across documents never collapse), and the
final deduplicated union: Note the graph IRIs handed to your loader originate from (possibly untrusted) input data — a network-dereferencing loader should allowlist hosts/schemes.
pub fn resolve_shapes_graph(data: &Graph, loader: impl FnMut(&str) -> Result<Option<Graph>, String>)
-> Result<ShapesGraphResolution, String>;
pub fn resolve_imports(seed: &Graph, loader: impl FnMut(&str) -> Result<Option<Graph>, String>)
-> Result<ShapesGraphResolution, String>;
pub struct ShapesGraphResolution {
pub shapes: Graph,
pub resolved: Vec<String>,
pub unresolved: Vec<String>,
}
Generated SCS 1.2 + extended parser — the sparq-shaclc crate (opt-in by
being a separate crate; epic sq-tonhr) — rdf-shuttle-generated strict/extended
parsers from one Shuttle grammar, COEXISTING with (not replacing) the scs
feature above and differential-tested against it:
use sparq_shaclc::{parse, parse_strict, parse_extended, Profile, DEFAULT_BASE};
use sparq_shaclc::write;
ShapesModel (sparq_shacl::ShapesModel):
pub fn ShapesModel::parse(shapes_graph: &Graph) -> ShapesModel;
ValidationReport (sparq_shacl::ValidationReport):
pub conforms: bool;
pub results: Vec<ValidationResult>;
pub diagnostics: Vec<ShapeDiagnostic>;
pub fn conforms_violations_only(&self) -> bool;
pub fn conforms_with_disallowed(&self, disallowed: &[&str]) -> bool;
pub fn results_with_severity<'a>(&'a self, severity: &'a str)
-> impl Iterator<Item = &'a ValidationResult>;
pub fn to_turtle(&self) -> String;
pub fn to_ntriples(&self) -> String;
pub fn to_text(&self) -> String;
pub fn to_json(&self) -> String;
ValidationResult (sparq_shacl::ValidationResult) — all fields public:
pub focus_node: oxrdf::Term;
pub path: Option<sparq_shacl::Path>;
pub value: Option<oxrdf::Term>;
pub source_shape: oxrdf::Term;
pub source_constraint: Option<oxrdf::Term>;
pub source_component: String;
pub severity: String;
pub messages: Vec<oxrdf::Term>;
pub default_message: String;
pub details: Vec<ValidationResult>;
pub fn effective_messages(&self) -> Vec<oxrdf::Term>;
details carries non-normative sh:detail sub-results that explain WHY a result
fired: a sh:memberShape violation lists one sub-result per non-conforming list
member (the actual results of validating that member against the member shape),
and a sh:uniqueMembers violation lists one sub-result per duplicated member
(sh:value = the duplicated term). sh:detail is non-normative — it never
affects sh:conforms and the W3C suite compares only top-level result fields —
so it is empty for every other component. to_turtle nests each detail as a
sh:ValidationResult blank node under sh:detail; to_text indents them.
Path (sparq_shacl::Path) — Predicate | Inverse | Sequence | Alternative | ZeroOrMore | OneOrMore | ZeroOrOne; path.to_turtle() gives the Turtle path
expression used in sh:resultPath.
Common recipes
CI gating — fail on violations, allow warnings. report.conforms follows the
SHACL-1.2 default (also disallows sh:Warning/sh:Info); use the stricter-threshold
toggle so only sh:Violation fails the build (or conforms_with_disallowed for a
custom sh:conformanceDisallows set):
let report = sparq_shacl::validate(&data, &shapes);
if !report.conforms_violations_only() {
eprintln!("{}", report.to_text());
std::process::exit(1);
}
Validate many data graphs against one shapes graph — parse the shapes once:
let model = sparq_shacl::ShapesModel::parse(&shapes);
for data in data_graphs {
let report = sparq_shacl::validate_with_model(&data, &model);
}
Inspect failures programmatically instead of rendering:
for r in &report.results {
let comp = r.source_component.rsplit(['#', '/']).next().unwrap();
println!("focus={} comp={comp} value={:?}", r.focus_node, r.value);
}
SHACL-SPARQL (sh:sparql, §5.2) — a constraint node carries an sh:select; it
runs per focus node with $this pre-bound (and $PATH on property shapes), and EACH
returned solution is one violation. ?value→sh:value (defaults to the focus node
when unprojected), ?path→sh:resultPath, ?message→sh:resultMessage; {?var} /
{$var} templating in sh:message:
let shapes = Graph::load_str(r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:prefixes ex:p ;
sh:message "Age must not be negative" ;
sh:select """SELECT $this ?value WHERE {
$this <http://example.org/age> ?value . FILTER (?value < 0) }""" ;
] .
ex:p sh:declare [ sh:prefix "ex" ; sh:namespace "http://example.org/"^^xsd:anyURI ] .
"#, "turtle").unwrap();
let report = sparq_shacl::validate(&data, &shapes);
Focus-node batching (perf, sq-7d3dj.33.1). Semantically each sh:sparql constraint
is "run per focus node", but the engine evaluates it for all of a shape's focus
nodes in ONE query: a single multi-row VALUES ?this { … } is injected (chunked at
10 000 foci), executed once, and the solution rows are grouped by ?this to build the
per-focus results. This replaces the old O(N_focus × full-query) per-focus loop (which
re-materialised the whole BGP for every focus node — quadratic) with O(1) queries per
shape; the report is byte-for-byte identical. A constraint whose TOP-level form is
NOT per-focus-equivalent — a LIMIT/OFFSET, a GROUP BY/aggregate not keyed on
$this (an implicit single group or GROUP BY ?other), or REDUCED — falls back to
the per-focus path automatically (a nested aggregate sub-select is always batched: the
pre-binding rules force it to group by $this). sparq_shacl::sparql_constraint_executions()
exposes a per-thread sh:sparql query-execution counter (snapshot the delta across a
validate call) so a perf guard can assert the batched path fired.
Id-level core-constraint fast path (perf, sq-7d3dj.33.4). Core constraints are
evaluated at the dictionary-id level: each shape's sh:path is compiled once per
validate (predicate IRIs → ids), the per-focus path walk and dedup run over u32
ids, and the hot value checks (sh:datatype / sh:pattern / sh:nodeKind /
sh:minCount·maxCount / sh:minLength·maxLength / sh:node, which also gets an
id-keyed conformance memo) read the dictionary's zero-copy literal records — a term is
materialised only for a VIOLATING value, at the report boundary. Compiled sh:pattern
regexes are Rc-shared across focus nodes (a per-focus Regex clone would rebuild the
lazy-DFA cache on every match). All of it is internal — no API or feature flag — and the
report is byte-identical to the Term-level route: a focus node absent from the data
dictionary (e.g. a sh:targetNode naming a ghost IRI) falls back to the Term-level walk,
and the in-crate idfast_* differential tests diff full reports fast-vs-forced-slow.
SHACL-1.2 core constraints (always on, no feature flag). The disjunctive
set spellings of sh:datatype / sh:nodeKind — sh:datatype ( xsd:string rdf:langString ), sh:nodeKind ( sh:BlankNode sh:IRI ) — conform a value node
when it matches ANY listed datatype / kind (the single-IRI form is the singleton
case). sh:closed sh:ByTypes is the "close by types" mode: the allowed-predicate
set is recomputed per value node from its rdf:types (transitively through
rdfs:subClassOf / inbound sh:targetClass / sh:node, SHACL §4.8.1), unlike
sh:closed true which fixes it to the shape's own sh:property paths. The four
SHACL list constraints validate that each value node is a well-formed SHACL list:
sh:memberShape (every member conforms to a shape), sh:uniqueMembers true
(members pairwise distinct), sh:min/sh:maxListLength (member-count bounds),
and sh:uniqueValuesFor (the listed properties' values are unique across the
shape's target nodes — one IRI, or a SHACL list for a composite key). A value that
is not a well-formed SHACL list violates the list constraints; a node with no
values for any sh:uniqueValuesFor property is never reported.
SHACL-1.2 value constraints (always on, sq-sx15d). sh:class also takes a
disjunctive SHACL-list object (sh:class ( ex:A ex:B ) — a value conforms iff it is a
SHACL instance of ANY listed class, subclass-aware). The comparand of sh:equals /
sh:disjoint / sh:lessThan / sh:lessThanOrEquals — and the new sh:subsetOf
(path value set ⊆ comparand value set) — is a full SHACL property PATH (often an
RDF-list sequence ( ex:p ex:q )), not just a predicate IRI; a bare IRI parses to a
trivial predicate path, so the SHACL-1.0 forms stay unchanged. sh:someValue [ shape ]
is EXISTENTIAL (at least one value node must conform to the nested shape; one result on
the focus/path when none do). sh:singleLine true flags string values containing a
line break (LF/CR/FF/VT). sh:rootClass C requires each value node to be C or a
transitive rdfs:subClassOf-descendant of it.
SHACL-1.2 per-constraint-statement reified-annotation overrides (always on, sq-pb0wm).
An RDF-1.2 reified annotation on a single constraint statement —
ex:S sh:datatype xsd:integer {| sh:deactivated true |} (likewise {| sh:message … |} /
{| sh:severity … |}) — overrides JUST that constraint occurrence, distinct from the
shape-level sh:deactivated/sh:message/sh:severity (which apply to the whole shape).
{| sh:deactivated true |} suppresses ONLY that constraint (the shape's other constraints
still validate); {| sh:message "…"@en |} sets sh:resultMessage for ONLY that
constraint's results; {| sh:severity sh:Warning |} sets sh:resultSeverity for ONLY that
constraint's violations. The {| … |} is parsed by oxttl's rdf-12 Turtle support and stored
as _:r rdf:reifies <<( ex:S sh:datatype xsd:integer )>> . _:r sh:deactivated|message|severity V;
the override resolves per occurrence from that reifier (misc/{deactivated-003,message-002, severity-003}). Supported on single-statement Core constraints (sh:datatype, sh:nodeKind,
sh:class, sh:hasValue, sh:rootClass, sh:node, sh:property, sh:not, sh:someValue,
sh:memberShape); list-/path-valued operands are not single statements and carry no override.
On a RECURSING composite (sh:node / sh:not / sh:someValue / sh:memberShape) the
message/severity override governs the composite component's OWN result and survives the
nested shape evaluation (sq-1jemy); it does NOT govern the nested shape's results — those
carry the nested shape's own metas (the 1.2 severity precedence keys on the reifier of the
constraint statement that caused each result). On sh:property — which reports the nested
property shape's results directly, with no composite result — only {| sh:deactivated |}
is observable.
SHACL-1.2 targets & SPARQL node expressions (always on, no feature flag, sq-rnkdh).
Beyond sh:targetNode/Class/SubjectsOf/ObjectsOf + implicit class targets:
sh:targetWhere [ <inline shape> ] — focus nodes are every data-graph node that
CONFORMS to the inline (object) shape (conformance is checked through the validator).
sh:shape — a DATA-graph triple ?n sh:shape ?S makes ?n a focus node of
shape ?S (the data-driven dual of sh:targetNode).
sh:ShapeClass — a class that is ALSO a node shape; its instances (via the
subclass closure) are implicit-class-targeted, no rdfs:Class+sh:NodeShape pair.
- SPARQL-valued targets / value nodes —
sh:targetNode [ sh:select "…" ] computes
focus nodes from the first result variable; on a property shape sh:values [ sh:select "…" ] / [ sh:sparqlExpr "EXPR" ] COMPUTES the value nodes (with $this = focus
node) instead of traversing sh:path (the reported sh:resultPath is still the path).
- A constraint-level
sh:severity on a sh:SPARQLConstraint overrides the shape's
default severity for the results it produces.
Custom SPARQL-based constraint component (sh:ConstraintComponent, §6). Declare
the component (parameters + an sh:ask/sh:select validator) IN THE SHAPES GRAPH; it
activates on any shape that uses all its mandatory parameter predicates. Each parameter
value is pre-bound as $paramName alongside $this/$value:
let shapes = Graph::load_str(r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ex: <http://example.org/> .
# Component typed via a subclass of sh:ConstraintComponent (discovery follows rdfs:subClassOf*).
ex:MyCC rdfs:subClassOf sh:ConstraintComponent .
ex:MaxLenComponent a ex:MyCC ;
sh:parameter [ sh:path ex:maxLen ] ;
sh:validator [ a sh:SPARQLAskValidator ;
sh:message "Value is longer than {$maxLen} characters" ;
sh:ask "ASK { FILTER (STRLEN(STR($value)) <= $maxLen) }" ] .
ex:S a sh:NodeShape ;
sh:targetNode "abcdef", "ab" ;
ex:maxLen 3 . # using ex:maxLen activates the component on ex:S
"#, "turtle").unwrap();
On a PROPERTY shape, a validator that references the $PATH variable gets it
pre-bound to the shape's property path (SHACL §6.3). Because $PATH is a SPARQL
property PATH (not a term), it is bound — like the §5.2 sh:sparql path — by
re-parsing the validator per property shape with the path's property-path form
textually substituted, rather than via the VALUES table the term bindings
($this / $value / $paramName) use. The re-parsed per-shape validator is
held off the public Component enum in a crate-private store; the public
Component::CustomSparql { component, args, path_validator } variant carries
only an Option<usize> index into it (path_validator), present when the
shape is a property shape, the chosen validator references $PATH, and the
substituted query re-parses — otherwise None and the component's shared
(path-free) validator is used as-is. Each $paramName variable is the LOCAL
NAME of the parameter's sh:path IRI (not its sh:name display label, §6.2.1).
Relative-IRI test files / a base IRI — Graph::load_str exposes no base, so use:
let g = sparq_shacl::load_turtle_with_base(&text, &format!("file://{path}")).unwrap();
SHACL Advanced Features rules (sh:rule + sh:values, SHACL-AF) — INFER
triples (opt-in feature shacl-af). A shape's rules infer new triples for that
shape's focus nodes (its targets). Three rule types: sh:TripleRule (sh:subject
/ sh:predicate / sh:object node expressions — the inferred triples are the
cartesian product of the three evaluated sets), sh:SPARQLRule (an sh:construct
CONSTRUCT run per focus node with $this pre-bound), and the sh:values value
rule (a property shape with a single-predicate sh:path and an sh:values node
expression infers (focus, predicate, v) per evaluated v). Rules honour
sh:condition (fire only for focus nodes conforming to every condition shape),
sh:order (ascending, a rule sees earlier groups' inferences), and
sh:deactivated. The engine iterates to a fixpoint (bounded by
rules::MAX_ITERATIONS = 100); the input graph is never mutated.
Cargo.toml: sparq-shacl = { path = "...", features = ["shacl-af"] }
use sparq_core::Graph;
let data = Graph::load_str(r#"
@prefix ex: <http://example.org/> .
ex:alice a ex:Person ; ex:firstName "Alice" .
"#, "turtle").unwrap();
let shapes = Graph::load_str(r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
ex:PersonShape a sh:NodeShape ;
sh:targetClass ex:Person ;
sh:rule [ a sh:TripleRule ; # infer (this, rdf:type, ex:Agent)
sh:subject sh:this ; sh:predicate rdf:type ; sh:object ex:Agent ] ;
sh:rule [ a sh:SPARQLRule ; # infer a label from the first name
sh:construct "CONSTRUCT { $this <http://example.org/label> ?n } WHERE { $this <http://example.org/firstName> ?n }" ] .
"#, "turtle").unwrap();
let inf = sparq_shacl::apply_rules(&data, &shapes);
let expanded: Graph = sparq_shacl::expand(&data, &shapes);
use sparq_shacl::{validate_with_domain, FactDomain};
let asserted = validate_with_domain(&data, &shapes, FactDomain::Asserted);
let closure = validate_with_domain(&data, &shapes, FactDomain::AssertedPlusInferred);
Node-expression algebra (operand of sh:subject/sh:predicate/sh:object,
the sh:values value rule, and sh:expression): sh:this (focus node); a
constant IRI/literal; a path expression [ sh:path P ; sh:nodes N? ] (any SHACL
property path; the optional sh:nodes is itself a node expression giving the start
nodes, default sh:this); a filter-shape expression [ sh:filterShape S ; sh:nodes N ] (the nodes of N conforming to shape S); [ sh:intersection ( … ) ]; [ sh:union ( … ) ]; a bare rdf:list (a SHACL 1.2 list expression — its members in
order, preserving duplicates); and the function-expression form (sq-mk9n). These
nest.
Function registry (sq-mk9n): the SHACL 1.2 built-in node-expression operators
(shnex:/sh:) — concat, count, sum, min, max, distinct,
if/then/else, exists, limit, offset, instancesOf, nodesMatching,
flatMap, findFirst, matchAll, remove, orderBy, var ("focusNode" ⇒
the focus; any other name resolves against a caller-supplied Scope, sq-u5rxj) —
plus a custom sh:SPARQLFunction IRI applied to a sh:list of arguments
(dispatched through the SPARQL engine with the ordered sh:parameter variables
pre-bound). An unregistered function IRI is dropped (lenient), inferring nothing.
Caller-supplied variable scope (sq-u5rxj): eval_node_expression_with_scope(data, shapes, expr, focus, &Scope) threads a Scope (FxHashMap<String, Vec<Term>>) of
variable name → bound node set that a shnex:var "<name>" resolves against (the W3C
suite's sht:scope-<name> injection); eval_node_expression is the empty-scope
wrapper.
sh:values value rule: a property shape with a single-predicate sh:path and
an sh:values node expression infers (focus, predicate, v) for each evaluated
v. A value rule on a sh:property child of a targeted node shape ranges over the
parent's focus nodes.
sh:expression constraint (sh:ExpressionConstraintComponent): a value node
violates when its sh:expression node expression does NOT evaluate to { true }
(value = focus on a node shape; each path value on a property shape).
sh:nodeByExpression constraint (sh:NodeByExpressionConstraintComponent):
like sh:node, but the node shape is computed by a node expression. For each
value node v, the expression is evaluated against v as focus to a set of
node-shape terms; v violates when it does NOT conform to one of them. A constant
IRI expression is the sh:node special case; an expression result naming no parsed
shape is skipped (lenient).
API: apply_rules(data, shapes), apply_rules_with_model(data, shapes, &model)
(amortise shape parsing), expand(data, shapes) -> Graph,
validate_with_domain(data, shapes, FactDomain) and
validate_with_domain_and_model(data, shapes, &model, FactDomain) (choose asserted
facts or the data-plus-inferred closure for validation), the node-expression
seam eval_node_expression(data, shapes, expr, focus) -> Option<Vec<Term>>, and
the conformance primitive conforms(data, shapes, shape_node) -> ConformanceCheck
(call .holds(node) per focus). A gated W3C harness (tests/w3c_node_expr.rs)
drives the sht:EvalNodeExpr suite — all evaluation entries pass; a companion
harness (tests/w3c_node_expr_constraints.rs) drives the suite's two sht:Validate
entries (sh:expression / sh:nodeByExpression) end-to-end (both self-skip when
the suite is not fetched). Because those W3C harnesses self-skip on a fresh
checkout, the node-expression function operators are also pinned by a
fixture-independent unit suite (tests/node_expr_operators.rs, sq-qcnn) that drives
every built-in (concat/count/sum/min/max/distinct/if/exists/limit/
offset/flatMap/orderBy/findFirst/matchAll/remove/instancesOf/
nodesMatching/var + custom sh:SPARQLFunction) through the public
eval_node_expression seam and asserts hand-derived result sets — so the operator
semantics are gated even when the suite is absent. The SCS parser's fail-closed
error paths and the SHACL-SPARQL §5.2/§6 edge cases get the same treatment
(tests/scs_error_paths.rs under scs, tests/sparql_edge_cases.rs). The
pre-binding's deep-algebra arms (push_values_down over Group / Slice / Distinct /
Reduced / OrderBy / Minus-left / LeftJoin-left, plus the multi-scope arms — both
UNION branches, sibling joins, and a projecting sub-SELECT, sq-mue75) and the
fail-closed runtime-error paths (an inexpressible blank-node focus, a SERVICE-clause
runtime query error) are pinned directly by the in-src/sparql.rs unit module
(sparql::tests, sq-qcnn.1 / sq-mue75): each arm is asserted both structurally (the
VALUES table lands BELOW the modifier / inside every branch so $this/$value/
$param stays in scope) and semantically (a real validator over real data yields the
SHACL-spec-correct conforms/violations).
Gotchas / feature flags / prerequisites
- Base SHACL is engaged purely by depending on
sparq-shacl (no feature
needed). It transitively pulls in sparq-engine (to run sh:sparql/§6 queries).
Neither is in the default wasm dependency graph, so the default browser bundle
stays SHACL-free; they enter the wasm graph ONLY when a consumer opts in via
sparq-wasm's non-default shacl feature, on which build sparq-engine's defaults
(rayon/regex/digest) are dropped so the bundle stays lean. The native build is
unaffected (full engine defaults).
- SHACL-AF rules (
sh:rule) are OPT-IN behind the shacl-af cargo feature.
With the feature off, the base validation path carries zero rule code/parse cost
and the apply_rules / apply_rules_with_model / expand / Inference /
FactDomain / validate_with_domain* symbols are absent. SHACL-AF rules are an
INFERENCE step (they produce triples), not part of the existing validate(..)
path. Use FactDomain::AssertedPlusInferred when constraints should see the rule
closure without expanding manually.
- The SHACL Compact Syntax parser is OPT-IN behind the
scs cargo feature.
With it off the scs module and the parse_scs / parse_scs_to_graph / ScsError
/ DEFAULT_BASE symbols are absent (zero parser code compiled in). It adds no new
dependencies. Coverage is honest: any construct outside the supported grammar
returns a typed ScsError rather than mis-parsing. Both the SCS parse and the
reference Turtle must resolve relative IRIs against the same base to agree, so
the round-trip test passes the fixture's BASE (or DEFAULT_BASE) to both sides.
- Shapes-graph assembly is OPT-IN behind the
imports cargo feature (sq-uz0).
With it off the imports module and the resolve_shapes_graph / resolve_imports
/ ShapesGraphResolution symbols are absent (zero assembly code compiled in; no
new dependencies). The library never dereferences an IRI itself — the loader
callback does — so there is no network / no SSRF surface here by construction.
- Rule fixpoint is bounded.
apply_rules iterates the rule schedule until a
pass infers nothing, capped at rules::MAX_ITERATIONS (100); Inference::capped
flags a non-terminating rule set (e.g. a CONSTRUCT minting a fresh blank node each
pass) whose inferred set may be incomplete.
sh:conforms uses the SHACL-1.2 default disallowed set {Violation,Warning,Info}
(sq-sx15d): a Debug/Trace-only report conforms; a Warning/Info result does NOT. For a
stricter "only Violation fails" gate use conforms_violations_only(); for a custom
sh:conformanceDisallows set use conforms_with_disallowed(&[..]).
- Ill-formed shapes are skipped by
validate, reported as a failure by
validate_strict (sq-11a, sq-ehq4g). A shape never declared, an unparsable path,
or an sh:select that fails to parse (e.g. undeclared prefix) contributes no
results; the rest of validation still runs. validate never returns a
Result/panics on bad shapes — so a silently-empty report can mean "no targets"
rather than "conforms". When the distinction matters (CI shape linting, the suite's
sht:Failure entries), validate_strict rejects ill-formed constructs with
ShaclFailure.ill_formed (see the strict-validation list above). A PRESENT
sh:select/sh:sparqlExpr whose text does not parse is rejected strictly too
(sq-ehq4g) — FAIL-CLOSED relative to this engine's vendored SPARQL parser, so a
valid query using syntax the parser lacks is also rejected; prefer fixing the query
(or filing the parser gap) over weakening the strict gate.
- An uncompilable
sh:pattern is SKIPPED, not fail-closed (sq-lz99x). The Rust
regex crate has no lookahead/lookbehind — neither does the XML Schema regex flavour
the SHACL spec ties sh:pattern to — so e.g. ^(?!(TODO|TBD)).* does not compile.
That constraint is skipped (it reports no violations) and surfaced once in
report.diagnostics (a ShapeDiagnostic carrying the shape, component, and the
regex crate's error), so the skip is not silent. Earlier this wrongly flagged
EVERY value. To express a "must NOT start with X" check, use a POSITIVE-match
sh:sparql REGEX(?str, "^\\s*(TODO|...)") constraint (flag when it matches) instead.
- XPath-regex divergences are translated, not passed through (sq-8ro).
sh:pattern
is matched by the Rust regex crate, but the XPath/XSD constructs it lacks are
translated first (eval.rs::compose_pattern): the q flag in sh:flags gives XPath
F&O literal-pattern mode (only i combines with it, matching the engine's SPARQL
REGEX), and \i / \I / \c / \C (XML NameStartChar / NameChar classes and
complements) expand to explicit character classes — including inside [...], via
nested classes. Look-around remains genuinely unsupported → the sq-lz99x skip path.
- Results are NOT deduplicated across traversal routes / component occurrences — a
nested shape reached via two parents reports twice (intentional, matches the suite).
- Recursion is treated as conforming. Re-entering the same (focus, shape) pair
counts as conforming (SHACL leaves recursion undefined); cyclic
sh:node/sh:property
terminate without stack overflow.
sh:sparql pre-binding: $this (and $PATH on property shapes) is injected via
an algebra-level VALUES on the parsed query — it lands below solution modifiers (so
LIMIT/ORDER BY/DISTINCT behave correctly) AND propagates into every scope the
variable can reach: both UNION branches, sibling joins, and a sub-SELECT that
explicitly projects the variable (sq-mue75). A SELECT * sub-select re-scopes the
variable, so the VALUES is joined above it (the spec-rejection case). Each sh:sparql
result carries sh:sourceConstraint (the sh:SPARQLConstraint node). sh:prefixes
chases sh:declare(sh:prefix/sh:namespace) transitively through owl:imports.
- §6 limits: the W3C
sparql/component/* suite owl:imports the external
http://datashapes.org/dash vocabulary; it is run offline (tests/w3c_sparql_component.rs)
by resolving that import against a vendored, minimal pinned excerpt at
crates/sparq-shacl/tests/vendor/dash.ttl. Still out of scope: the sparql/pre-binding
rejection channel (signalling a failure for a re-binding / SELECT * sub-select) and
$shapesGraph — see the crate's open beads (bd list -l area:sparq-shacl).
- W3C conformance: 98/98 of the 1.0/1.1 core
sht:Validate suite passes
(--test w3c_core). The full vendored SHACL 1.2 tree is gated by a ratchet
(sq-6glcr) in BOTH feature states: full core 136 (default) / 137 (shacl-af)
— every in-scope core entry passes, 0 honest FAILs (sq-pb0wm closed the final
per-statement reified-annotation gap) — (--test w3c_core_full_shacl12), 1.2 SPARQL
24 of 24 incl. 7 expected-rejection
sht:Failure entries (--test w3c_sparql_shacl12), node-expr 62 + 1 xfail
(driven through the REAL eval_node_expression, --test w3c_node_expr, shacl-af;
the xfail is the harness sht:scope-* var entry the crate's eval has no counterpart
for). Pass must not drop, the gap must
not grow — the not-yet-passing entries are the honest per-category gap map in
research/shacl12-conformance-gap.md (clustered into beads sq-sx15d / sq-rnkdh /
sq-mue75 / sq-0mjfd under epic sq-waf9o). Reproduce with
crates/sparq-shacl/fetch-shacl-tests.sh then
cargo test -p sparq-shacl --test w3c_core (self-skips if the gitignored suite is absent).
- §6 SPARQL-based constraint components are implemented and tested
(
tests/sparql_components.rs plus the W3C sparql/component sub-suite in
tests/w3c_sparql_component.rs); the crate README documents them under
"Supported constraint components".
See also
sparql-query — running standalone SPARQL through sparq-engine (what sh:sparql
routes through).
graph-loading / compressed-ingest — building the sparq_core::Graph you validate.
fused-decompress-parse, hdt-format — alternative ingest paths feeding a Graph.