git:20260718.9c29a02 to git:20260718.999cb78

7 added, 0 removed. Audit A to A.

---
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`:
```toml
[dependencies]
sparq-core = { path = "../sparq-core" } # or version = "0.1"
sparq-shacl = { path = "../sparq-shacl" } # or version = "0.1"
```
```rust
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); // sh:conforms = false
assert_eq!(report.results.len(), 1); // one DatatypeConstraintComponent violation
eprintln!("{}", report.to_text()); // human-readable
println!("{}", report.to_turtle()); // W3C sh:ValidationReport graph
println!("{}", report.to_ntriples()); // same graph, one N-Triples statement per line
println!("{}", report.to_json()); // deterministic machine-readable object
```
CLI-style end-to-end run via the bundled example (exits 0 iff the data conforms):
```sh
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::…`):
```rust
// Parse shapes + validate in one call.
pub fn validate(data: &Graph, shapes: &Graph) -> ValidationReport;
// Validate against an ALREADY-parsed shapes model (amortise parsing across many graphs).
pub fn validate_with_model(data: &Graph, model: &ShapesModel) -> ValidationReport;
// STRICT validation (sq-0mjfd): returns Err(ShaclFailure) for what a conformant
// processor REJECTS (the W3C `sht:Failure` outcome) — an unsound SHACL-SPARQL
// pre-binding (MINUS / VALUES / SERVICE / a sub-SELECT dropping $this / a BIND
// re-binding it), and (sq-11a) an ILL-FORMED shapes-graph construct: an unparsable
// sh:path (or >1 sh:path values), a non-integer count/length (a NEGATIVE integer is
// well-formed and stays a silent skip), a literal sh:datatype/sh:class/sh:nodeKind/
// sh:pattern or a non-IRI list member, a malformed SHACL list (sh:in/and/or/xone/
// languageIn/ignoredProperties), a literal shape ref (sh:node/not/property/…), a
// non-boolean sh:closed/uniqueLang/…, a non-literal range comparand, an ill-formed
// comparand path (sh:equals/lessThan/…), a non-IRI sh:target{Class,SubjectsOf,
// ObjectsOf}, an sh:sparql node with no sh:select literal. (sq-ehq4g) adds: a
// PROPERTY shape with NO sh:path (an sh:property value, or a node typed
// sh:PropertyShape), sh:qualifiedValueShape with NEITHER sh:qualifiedMinCount nor
// sh:qualifiedMaxCount (both qualified components then miss a mandatory parameter),
// an sh:nodeKind value outside the SIX sh:* kinds, and a PRESENT sh:select /
// sh:sparqlExpr (on sh:sparql constraints AND on the SPARQL-based node expressions
// of sh:targetNode/sh:values) whose text does not parse as the required query form
// — FAIL-CLOSED relative to this engine's vendored SPARQL parser: a valid query
// beyond the parser's coverage is also rejected strictly. (sq-c1v3e) adds: a non-IRI
// sh:severity (shape-level AND on an sh:SPARQLConstraint node), the count/length
// DATATYPE check (integer-lexical but non-xsd:integer-typed, e.g. "3"^^xsd:string —
// bare Turtle 3 types as xsd:integer, so ordinary graphs are unaffected), a
// qualified COUNT without sh:qualifiedValueShape (the symmetric partial-parameter
// case), ANY sh:entailment declaration (no entailment regime is supported — the
// SHACL §3.4 unsupported-regime failure), and (feature `shacl-af`) an
// sh:expression / sh:nodeByExpression structural node expression that does not
// build. Construct-local checks, NOT a full SHACL-of-SHACL pass;
// ShaclFailure.ill_formed / ShapesModel::ill_formed() carry (node, predicate,
// message). `validate` instead SKIPS all of the above unchanged (its never-fails
// contract) and (sq-c1v3e) surfaces each record as a ShapeDiagnostic in
// report.diagnostics (source_component = the offending SHACL predicate IRI).
pub fn validate_strict(data: &Graph, shapes: &Graph) -> Result<ValidationReport, ShaclFailure>;
pub fn validate_strict_with_model(data: &Graph, model: &ShapesModel)
-> Result<ValidationReport, ShaclFailure>;
// Load Turtle resolving relative IRIs against a base (Graph::load_str has no base param).
pub fn load_turtle_with_base(text: &str, base: &str) -> Result<Graph, String>;
// Build a Graph from already-parsed oxrdf::Triples.
pub fn graph_from_triples<I: IntoIterator<Item = oxrdf::Triple>>(triples: I) -> Graph;
// Per-thread monotonic count of sh:sparql query executions (sq-7d3dj.33.1). Snapshot
// the delta across a `validate` call to assert focus-node batching fired (a small
// delta for a large focus set) — see "Focus-node batching" under SHACL-SPARQL below.
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):
```rust
// Parse SCS text -> SHACL shapes triples (relative IRIs + the owl:Ontology subject
// resolve against `base`; pass DEFAULT_BASE for the no-`BASE` convention).
pub fn parse_scs(text: &str, base: &str) -> Result<Vec<oxrdf::Triple>, ScsError>;
// Same, then build a queryable Graph ready to feed `validate`.
pub fn parse_scs_to_graph(text: &str, base: &str) -> Result<Graph, ScsError>;
pub const DEFAULT_BASE: &str; // "urn:x-base:default"
pub struct ScsError { pub line: usize, pub message: String } // typed; never a silent mis-parse
```
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.
```rust
// The union of every graph the data graph references via sh:shapesGraph, plus
// the transitive owl:imports closure of each loaded document (data triples are
// NOT included). Loader contract: Ok(Some(g)) = fetched, Ok(None) = cannot
// resolve (recorded in `unresolved`, not fatal — SHACL keeps imports support
// optional), Err = hard failure (aborts, naming the IRI).
pub fn resolve_shapes_graph(data: &Graph, loader: impl FnMut(&str) -> Result<Option<Graph>, String>)
-> Result<ShapesGraphResolution, String>;
// The caller already HAS a shapes graph: its own triples (blank labels intact)
// unioned with its transitive, cycle-guarded owl:imports closure.
pub fn resolve_imports(seed: &Graph, loader: impl FnMut(&str) -> Result<Option<Graph>, String>)
-> Result<ShapesGraphResolution, String>;
pub struct ShapesGraphResolution {
pub shapes: Graph, // feed this to `validate` / `ShapesModel::parse`
pub resolved: Vec<String>, // IRIs loaded, breadth-first traversal order
pub unresolved: Vec<String>, // IRIs the loader declined (honest record)
}
```
**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:
```rust
use sparq_shaclc::{parse, parse_strict, parse_extended, Profile, DEFAULT_BASE};
// -> Result<(Vec<oxrdf::Triple>, Outcome), ShaclcError>; Outcome carries
// prefixes (5 predeclared first) + final base. Profile::Strict = W3C CG
// surface + RDF 1.2 layer (triple terms, dir-lang tags, TripleTerm
// nodeKind, reifierShape/reificationRequired) and PROVABLY rejects the
// four shaclc-js extensions; Profile::Extended accepts them.
// sparq_shaclc::raw::{shaclc12, shaclc12ext} — streaming + chunked push
// parsing on the generated zero-dependency term model.
use sparq_shaclc::write; // (triples, base, prefixes, Profile) -> Result<String, ShaclcWriteError>
// The derived residual-consumption printer (first Rust-side SHACL-CS
// writer): all-or-nothing — a non-expressible graph returns the typed
// residual verdict (exact unconsumed triples), never a lossy document.
```
`ShapesModel` (`sparq_shacl::ShapesModel`):
```rust
pub fn ShapesModel::parse(shapes_graph: &Graph) -> ShapesModel; // parse once, reuse
```
`ValidationReport` (`sparq_shacl::ValidationReport`):
```rust
pub conforms: bool; // SHACL-1.2 sh:conforms: false iff any result is in the
// default disallowed set {Violation,Warning,Info}; Debug/Trace conform
pub results: Vec<ValidationResult>;
pub diagnostics: Vec<ShapeDiagnostic>; // skipped-constraint diagnostics (e.g. uncompilable sh:pattern); never affect `conforms`
pub fn conforms_violations_only(&self) -> bool; // stricter-threshold toggle: ignore sh:Warning / sh:Info
pub fn conforms_with_disallowed(&self, disallowed: &[&str]) -> bool; // custom sh:conformanceDisallows set (full severity IRIs)
pub fn results_with_severity<'a>(&'a self, severity: &'a str) // full IRI, e.g. ".../shacl#Warning"
-> impl Iterator<Item = &'a ValidationResult>;
pub fn to_turtle(&self) -> String; // W3C report vocabulary (valid, round-trippable Turtle)
pub fn to_ntriples(&self) -> String; // same report graph, one N-Triples statement per line
pub fn to_text(&self) -> String; // human-readable summary
pub fn to_json(&self) -> String; // deterministic JSON; fixed keys and result order
```
`ValidationResult` (`sparq_shacl::ValidationResult`) — all fields public:
```rust
pub focus_node: oxrdf::Term;
pub path: Option<sparq_shacl::Path>; // sh:resultPath (property shapes / sh:closed)
pub value: Option<oxrdf::Term>; // offending value node
pub source_shape: oxrdf::Term;
pub source_constraint: Option<oxrdf::Term>; // sh:sourceConstraint — the sh:SPARQLConstraint node (sh:sparql results only; None for Core + §6 components)
pub source_component: String; // constraint-component IRI, e.g. ".../MinCountConstraintComponent"
pub severity: String; // severity IRI (default ".../shacl#Violation")
pub messages: Vec<oxrdf::Term>; // sh:message literals
pub default_message: String;
pub details: Vec<ValidationResult>; // nested sh:detail sub-results (see below); empty for most components
pub fn effective_messages(&self) -> Vec<oxrdf::Term>; // messages, or a generated default
```
`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):
```rust
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:
```rust
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:
```rust
for r in &report.results {
let comp = r.source_component.rsplit(['#', '/']).next().unwrap(); // "MinCountConstraintComponent"
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`:
```rust
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); // source_component ends with "SPARQLConstraintComponent"
```
*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:type`s (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`:
```rust
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();
// source_component is the component IRI; ASK=false → violation. sh:nodeValidator /
// sh:propertyValidator are preferred over the generic sh:validator by shape kind (§6.2.2).
```
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:
```rust
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"] }`
```rust
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();
// The INFERRED triples only (data is not mutated):
let inf = sparq_shacl::apply_rules(&data, &shapes); // -> sparq_shacl::Inference
// inf.triples : Vec<oxrdf::Triple> inf.iterations : usize inf.capped : bool
// Or get a fresh graph of data ∪ inferred, ready to query/validate:
let expanded: Graph = sparq_shacl::expand(&data, &shapes);
// Or select the validation fact domain directly. These APIs are also gated by
// `shacl-af`; the existing `validate` function remains asserted-only.
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`.