Skip to content

Commit a818fde

Browse files
fabio-rovaiclaude
andcommitted
A node-level sh:or result carries the node shape's sh:severity instead of always Violation
Cause. sh:or asserted on a node shape over member shapes built its result with `"severity": "Violation"` and `RdfTerms::new(None, ...)`, so the JSON said "Violation" and the RDF report wrote sh:resultSeverity sh:Violation whatever sh:severity the shape declared. SHACL 3.6.2 makes the result's severity the shape's sh:severity, and every other path already reads it. Fix. The query that already fetched the node shape's sh:message now also fetches its sh:severity (both OPTIONAL). The JSON field goes through `severity_name` (#334: local name for sh: severities, full IRI otherwise) and the declared term goes to `RdfTerms` for the RDF writer; with none declared both stay sh:Violation. Verification. - tests/shacl_validation_report_test.rs: a_node_level_or_result_carries_the_shapes_declared_severity checks sh:Warning, a custom hash-namespace IRI and no declaration, in the JSON and in the Turtle report. Run against origin/main's src/shacl.rs it fails at the JSON assertion for sh:Warning (left "Violation", right "Warning"); it passes now. - W3C suite 94d8bc2: gated 61/15/44/0 and full tuple 53/23/44/0, unchanged; regenerated baseline byte-identical. Report match 49 of 56. - cargo test (full, this tree): 2032 passed, 0 failed, 2 ignored. cargo clippy --all-features --all-targets -D warnings: clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ba3fde0 commit a818fde

3 files changed

Lines changed: 57 additions & 8 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ All notable changes to Open Ontologies are documented here.
6161
datatype on a property inside the referenced shape and not on the shape
6262
itself, so every value the store had widened was reported. The `sh:node` is
6363
now recorded as not evaluated. W3C columns unchanged.
64+
- **A node-level `sh:or` result carries the node shape's `sh:severity`.** It
65+
was written `"Violation"` whatever the shape declared, and the RDF report
66+
followed it with `sh:resultSeverity sh:Violation`; it now uses the declared
67+
term, through the same `severity_name` as every other path (#334). W3C
68+
columns unchanged.
6469
- **A custom `sh:severity` keeps its namespace in the JSON report** (#334).
6570
Every path set `severity` to the text after the last `#` of the declared
6671
IRI, and every reader takes a bare name as `sh:`, so W3C

‎src/shacl.rs‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,20 +1119,30 @@ impl ShaclValidator {
11191119
.collect();
11201120
members.sort();
11211121
let disjunction = members.join(" || ");
1122-
let node_message_term = query_solutions(
1122+
let node_terms = query_solutions(
11231123
&shapes_store,
11241124
&format!(
11251125
r#"PREFIX sh: <http://www.w3.org/ns/shacl#>
1126-
SELECT ?m WHERE {{ {} sh:message ?m }}"#,
1127-
shape_term
1126+
SELECT ?m ?sev WHERE {{
1127+
OPTIONAL {{ {shape_term} sh:message ?m }}
1128+
OPTIONAL {{ {shape_term} sh:severity ?sev }}
1129+
}}"#
11281130
),
1129-
)?
1130-
.first()
1131-
.and_then(|r| r.get("m").cloned());
1131+
)?;
1132+
let node_term = |k: &str| node_terms.first().and_then(|r| r.get(k).cloned());
1133+
let node_message_term = node_term("m");
11321134
let node_message =
11331135
node_message_term.as_deref().map(strip_quotes).unwrap_or_default();
1136+
// The node shape's sh:severity, as on every other path.
1137+
// This one wrote "Violation" whatever was declared, and
1138+
// the RDF report followed it with sh:Violation.
1139+
let node_severity_term = node_term("sev");
1140+
let node_severity = node_severity_term
1141+
.as_deref()
1142+
.map(severity_name)
1143+
.unwrap_or_else(|| "Violation".to_string());
11341144
let node_or_rdf = RdfTerms::new(
1135-
None,
1145+
node_severity_term,
11361146
node_message_term.filter(|_| !node_message.is_empty()),
11371147
);
11381148
let query = format!(
@@ -1149,7 +1159,7 @@ impl ShaclValidator {
11491159
node_message.clone()
11501160
};
11511161
violations.push(attribute(&shape_iri, &shape_iri, &node_or_rdf, serde_json::json!({
1152-
"severity": "Violation",
1162+
"severity": node_severity,
11531163
"focus_node": strip_angle_brackets(focus),
11541164
"value": strip_angle_brackets(focus),
11551165
"constraint": "or",

‎tests/shacl_validation_report_test.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,40 @@ ex:SparqlLevel a sh:NodeShape ; sh:targetClass ex:Person ;
285285
assert_eq!(got, want, "{j}");
286286
}
287287

288+
/// `sh:or` asserted on a node shape over member shapes wrote every result as
289+
/// `"Violation"` whatever the shape declared, and the RDF report followed it
290+
/// with `sh:resultSeverity sh:Violation`. The node shape's `sh:severity` is
291+
/// the result's severity (SHACL 3.6.2) as on every other path.
292+
#[test]
293+
fn a_node_level_or_result_carries_the_shapes_declared_severity() {
294+
for (declared, json, rdf) in [
295+
("sh:Warning", "Warning", "sh:Warning"),
296+
("sev:MySeverity", "http://ex.org/sev#MySeverity", "<http://ex.org/sev#MySeverity>"),
297+
("", "Violation", "sh:Violation"),
298+
] {
299+
let severity = if declared.is_empty() { String::new() } else { format!("sh:severity {declared} ;") };
300+
let shapes = format!(
301+
r#"
302+
@prefix sh: <http://www.w3.org/ns/shacl#> .
303+
@prefix ex: <http://ex.org/> .
304+
@prefix sev: <http://ex.org/sev#> .
305+
ex:S a sh:NodeShape ; sh:targetClass ex:Person ; {severity}
306+
sh:or ( [ sh:property [ sh:path ex:email ; sh:minCount 1 ] ]
307+
[ sh:property [ sh:path ex:phone ; sh:minCount 1 ] ] ) .
308+
"#
309+
);
310+
let j = json_report(DATA, &shapes);
311+
assert_eq!(j["violation_count"], 1, "{declared}: {j}");
312+
assert_eq!(j["violations"][0]["constraint"], "or", "{declared}: {j}");
313+
assert_eq!(j["violations"][0]["severity"], json, "{declared}: {j}");
314+
let s = parsed(&rdf_report(DATA, &shapes, ReportFormat::Turtle), RdfFormat::Turtle);
315+
assert!(
316+
ask(&s, &format!("?x sh:sourceConstraintComponent sh:OrConstraintComponent ; sh:resultSeverity {rdf}")),
317+
"{declared}: the RDF report must carry {rdf}"
318+
);
319+
}
320+
}
321+
288322
/// #335, MEASURED AND NOT FIXED IN THIS REPOSITORY. This test pins the loss
289323
/// so that it cannot be mistaken for a report-writer bug, and so that it fails,
290324
/// by name, on the day the store starts keeping lexical forms.

0 commit comments

Comments
 (0)