From 50f2f82b95f48a0c66d8be6b365493cd4a288335 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sat, 22 Aug 2026 23:58:00 +0800 Subject: [PATCH 1/4] feat(ontology): expose public SHACL validation API --- docs/guides/shacl-validation.md | 43 ++++++++++++------------ semantica/ontology/__init__.py | 2 ++ semantica/ontology/ontology_validator.py | 19 +++++++++-- tests/ontology/test_ontology_advanced.py | 24 +++++++++++++ 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/docs/guides/shacl-validation.md b/docs/guides/shacl-validation.md index ad797821..eafea841 100644 --- a/docs/guides/shacl-validation.md +++ b/docs/guides/shacl-validation.md @@ -8,7 +8,7 @@ icon: "shield-check" SHACL (Shapes Constraint Language) is a standard for validating graph-based data. While an ontology defines the conceptual *schema* (the "what" exists in your domain), SHACL defines the structural *rules and constraints* (the "how" it should be structured). -In Semantica, `SHACLGenerator` produces constraint rules (shapes) based on your ontology, and `_run_pyshacl` evaluates your actual data against these rules. If a node violates a rule (e.g., missing a required property or using the wrong datatype), a detailed violation report is generated. +In Semantica, `SHACLGenerator` produces constraint rules (shapes) based on your ontology, and the public `run_shacl_validation` function evaluates your actual data against these rules. If a node violates a rule (e.g., missing a required property or using the wrong datatype), a detailed violation report is generated. The historical `run_shacl_validation` name remains available as a compatibility alias. ## Why Use SHACL Validation? @@ -55,7 +55,7 @@ Let's look at a simple, universally understood example: ensuring every `Employee ```python from semantica.context import ContextGraph from semantica.ontology import OntologyGenerator, SHACLGenerator, PropertyShape -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation # 1. Prepare your data graph graph = ContextGraph() @@ -95,7 +95,7 @@ data_ttl = """ """ # 5. Run Validation -report = _run_pyshacl(data_ttl, shacl_ttl) +report = run_shacl_validation(data_ttl, shacl_ttl) # 6. Analyze the Report print(f"Graph conforms: {report.conforms}") @@ -265,10 +265,10 @@ cve_id_shape = NodeShape( ## Step 4 — Run validation and read the report -Serialize the graph to RDF, then run `_run_pyshacl` against the shapes. +Serialize the graph to RDF, then run `run_shacl_validation` against the shapes. ```python -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation # Prepare your RDF data string (since export_rdf primarily exports structural metadata, # you typically serialize your custom data graph to Turtle using rdflib or similar). @@ -281,7 +281,7 @@ data_ttl = """ """ # Run SHACL validation -report = _run_pyshacl( +report = run_shacl_validation( data_ttl, shacl_ttl, data_graph_format="turtle", @@ -366,8 +366,8 @@ print(f"Malware nodes missing 'family': {len(missing_family)}") # e.g. graph.update_node(node_id, {"family": "UNKNOWN — requires triage"}) # After remediation, re-run validation to confirm the fix -# (re-export the patched graph to Turtle first, then call _run_pyshacl again) -report2 = _run_pyshacl(patched_data_ttl, shacl_ttl) +# (re-export the patched graph to Turtle first, then call run_shacl_validation again) +report2 = run_shacl_validation(patched_data_ttl, shacl_ttl) print(f"Violations after remediation: {report2.violation_count}") # Violations after remediation: 0 ``` @@ -377,7 +377,7 @@ print(f"Violations after remediation: {report2.violation_count}") ## Common Pitfalls - **Assuming the ontology automatically enforces data quality**: `SHACLGenerator` generates shapes based on what it observes in the data. If your data is missing a field, the generator won't know it was mandatory unless you explicitly inject the constraint (as shown in Step 3). -- **Passing `ContextGraph` directly to SHACL validators**: The `_run_pyshacl` function expects an RDF string (like Turtle format), not a raw Python dictionary or `ContextGraph` object. +- **Passing `ContextGraph` directly to SHACL validators**: The `run_shacl_validation` function expects an RDF string (like Turtle format), not a raw Python dictionary or `ContextGraph` object. - **Forgetting RDF serialization**: You must serialize your graph (often via a temporary file using `export_rdf`) before validating it. - **Treating validation as a one-time step**: Validation should be integrated as an automated step in your CI/CD pipeline or data ingestion flow, acting as a recurring gatekeeper rather than a one-off script. - **Ignoring validation reports**: A graph that does not conform must be remediated. Failing to review the `violation_count` and address the issues negates the purpose of SHACL validation. @@ -418,7 +418,7 @@ print(f"Violations after remediation: {report2.violation_count}") # rdfs True <- the entailment manufactured the type ``` - Mitigations: prefer not to declare `rdfs:range` on properties you intend to constrain with `sh:class`; when class membership is the thing under test, run validation without RDFS entailment (`inference="none"`); or express the check as a constraint the entailment cannot satisfy (for example a literal property constraint). Note the trade-off: with entailment off, `sh:targetClass` no longer reaches subclasses, so subclass hierarchies need explicit typing or inference-aware target selection. Semantica's own `_run_pyshacl` wrapper already calls pyshacl with `inference="none"`, so this pitfall only bites when calling `pyshacl.validate` directly with entailment enabled. + Mitigations: prefer not to declare `rdfs:range` on properties you intend to constrain with `sh:class`; when class membership is the thing under test, run validation without RDFS entailment (`inference="none"`); or express the check as a constraint the entailment cannot satisfy (for example a literal property constraint). Note the trade-off: with entailment off, `sh:targetClass` no longer reaches subclasses, so subclass hierarchies need explicit typing or inference-aware target selection. Semantica's own `run_shacl_validation` wrapper already calls pyshacl with `inference="none"`, so this pitfall only bites when calling `pyshacl.validate` directly with entailment enabled. - **Trusting `conforms: True` without checking the inference mode**: an inference-enabled run can hide the exact violations the shapes were written to catch (see above). Record which inference mode validation ran under alongside the result, and re-run shape sets that contain `sh:class`/`sh:node` with entailment off before treating a pass as authoritative. --- @@ -435,7 +435,7 @@ A DoD CTI team enforces STIX-compatible constraints on a threat graph before sha from semantica.context import AgentContext, ContextGraph from semantica.vector_store import VectorStore from semantica.ontology import OntologyGenerator, SHACLGenerator, PropertyShape -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation graph = ContextGraph() ctx = AgentContext( @@ -487,7 +487,7 @@ data_ttl = """ a ex:Malware . """ -report = _run_pyshacl(data_ttl, shacl_ttl) +report = run_shacl_validation(data_ttl, shacl_ttl) print(f"CTI graph conforms : {report.conforms}") print(f"Violations : {report.violation_count}") print(f"Warnings : {report.warning_count}") @@ -508,7 +508,7 @@ A SOC team validates zero-trust policy nodes before publishing them to the polic ```python from semantica.context import ContextGraph from semantica.ontology import OntologyGenerator, SHACLGenerator, PropertyShape -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation graph = ContextGraph() graph.add_node("policy-001", "Policy", "MFA Required for Tier-1 Resources", @@ -555,7 +555,7 @@ data_ttl = """ a ex:Policy . """ -report = _run_pyshacl(data_ttl, shacl_ttl) +report = run_shacl_validation(data_ttl, shacl_ttl) print(f"Policy graph conforms: {report.conforms}") # Policy graph conforms: False @@ -573,7 +573,7 @@ A clinical informatics team validates trial ontology nodes before loading them i ```python from semantica.ontology import LLMOntologyGenerator, SHACLGenerator, PropertyShape -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation from semantica.export import export_rdf import tempfile, os @@ -625,7 +625,7 @@ with open(tmp.name) as f: data_ttl = f.read() os.unlink(tmp.name) -report = _run_pyshacl(data_ttl, shacl_ttl) +report = run_shacl_validation(data_ttl, shacl_ttl) print(f"Trial data conforms: {report.conforms}") print(f"Warnings : {report.warning_count}") ``` @@ -639,7 +639,7 @@ A credit risk team validates every `LoanApplication` node against Basel III CRE2 ```python from semantica.context import ContextGraph from semantica.ontology import OntologyGenerator, SHACLGenerator, PropertyShape -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation graph = ContextGraph() graph.add_node("loan-001", "LoanApplication", "Prime mortgage APP-2025-88421", @@ -684,7 +684,7 @@ data_ttl = """ ex:ltv "0.65" . """ -report = _run_pyshacl(data_ttl, shacl_ttl) +report = run_shacl_validation(data_ttl, shacl_ttl) print(f"Loan portfolio conforms: {report.conforms}") # Loan portfolio conforms: False @@ -714,14 +714,14 @@ Call this function as a pre-publish gate; exit code 1 blocks the pipeline. ```python import sys from semantica.ontology import OntologyGenerator, SHACLGenerator -from semantica.ontology.ontology_validator import _run_pyshacl +from semantica.ontology import run_shacl_validation def validate_before_publish(data_graph_str: str, ontology: dict) -> None: shacl_gen = SHACLGenerator(base_uri="https://example.org/shapes/") shacl_graph = shacl_gen.generate(ontology) shacl_ttl = shacl_gen.serialize(shacl_graph, format="turtle") - report = _run_pyshacl(data_graph_str, shacl_ttl) + report = run_shacl_validation(data_graph_str, shacl_ttl) if not report.conforms: print(f"Graph validation FAILED — {report.violation_count} violation(s)") @@ -739,7 +739,6 @@ def validate_before_publish(data_graph_str: str, ontology: dict) -> None: - [Ontology Management](ontology) — generate the OWL ontology that SHACL shapes are derived from - [Reasoning & Rules](reasoning) — complement SHACL structural constraints with logical inference rules -- [Export & Serialization](export) — serialize graph data to Turtle/RDF/XML for `_run_pyshacl` input +- [Export & Serialization](export) — serialize graph data to Turtle/RDF/XML for `run_shacl_validation` input - [Conflict Resolution](conflict-resolution) — detect and resolve data conflicts before SHACL validation - [Change Management](change-management) — version-gate SHACL shapes alongside ontology versions - diff --git a/semantica/ontology/__init__.py b/semantica/ontology/__init__.py index 98edff7a..27b1318a 100644 --- a/semantica/ontology/__init__.py +++ b/semantica/ontology/__init__.py @@ -159,6 +159,7 @@ from .ontology_validator import ( SHACLValidationReport, SHACLViolation, ValidationResult, + run_shacl_validation, validate_ontology, ) from .owl_generator import OWLGenerator @@ -192,6 +193,7 @@ __all__ = [ "PropertyShape", "SHACLValidationReport", "SHACLViolation", + "run_shacl_validation", # OWL/RDF generation "OWLGenerator", # Requirements and competency questions diff --git a/semantica/ontology/ontology_validator.py b/semantica/ontology/ontology_validator.py index 5d9d10df..85adb0a8 100644 --- a/semantica/ontology/ontology_validator.py +++ b/semantica/ontology/ontology_validator.py @@ -145,14 +145,14 @@ class SHACLValidationReport: } -def _run_pyshacl( +def run_shacl_validation( data_graph_str: str, shacl_str: str, data_graph_format: str = "turtle", shacl_format: str = "turtle", ) -> SHACLValidationReport: """ - Run pyshacl validation and return a structured SHACLValidationReport. + Run pySHACL validation and return a structured SHACLValidationReport. Args: data_graph_str: Serialized data graph string. @@ -272,6 +272,21 @@ def _run_pyshacl( raw_report=results_text, ) + +def _run_pyshacl( + data_graph_str: str, + shacl_str: str, + data_graph_format: str = "turtle", + shacl_format: str = "turtle", +) -> SHACLValidationReport: + """Backward-compatible alias for :func:`run_shacl_validation`.""" + return run_shacl_validation( + data_graph_str, + shacl_str, + data_graph_format=data_graph_format, + shacl_format=shacl_format, + ) + @dataclass class ValidationResult: """Result of an ontology validation operation.""" diff --git a/tests/ontology/test_ontology_advanced.py b/tests/ontology/test_ontology_advanced.py index 98d5dc6e..149a8789 100644 --- a/tests/ontology/test_ontology_advanced.py +++ b/tests/ontology/test_ontology_advanced.py @@ -525,6 +525,30 @@ class TestSHACLHierarchicalAndValidation(unittest.TestCase): self.assertEqual(mc[0].max_count, 2) # 33 + def test_public_run_shacl_validation_api(self): + """The public API validates data and retains the legacy alias.""" + try: + import pyshacl # noqa: F401 + import rdflib # noqa: F401 + except ImportError: + self.skipTest("pyshacl/rdflib not installed") + from semantica.ontology import run_shacl_validation + from semantica.ontology.ontology_validator import _run_pyshacl + + data = "@prefix ex: . ex:alice a ex:Person ." + shacl = """ + @prefix ex: . + @prefix sh: . + ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; + sh:property [ sh:path ex:name ; sh:minCount 1 ] . + """ + public_report = run_shacl_validation(data, shacl) + legacy_report = _run_pyshacl(data, shacl) + self.assertFalse(public_report.conforms) + self.assertEqual(public_report.violation_count, 1) + self.assertEqual(legacy_report.to_dict(), public_report.to_dict()) + + # 34 def test_shacl_violation_to_dict(self): from semantica.ontology.ontology_validator import SHACLViolation v = SHACLViolation( From fe3baad67c25106d0a84e122d109fe2e3fab6be7 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 23 Aug 2026 00:02:57 +0800 Subject: [PATCH 2/4] docs(shacl): correct legacy alias name --- docs/guides/shacl-validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/shacl-validation.md b/docs/guides/shacl-validation.md index eafea841..76c07b89 100644 --- a/docs/guides/shacl-validation.md +++ b/docs/guides/shacl-validation.md @@ -8,7 +8,7 @@ icon: "shield-check" SHACL (Shapes Constraint Language) is a standard for validating graph-based data. While an ontology defines the conceptual *schema* (the "what" exists in your domain), SHACL defines the structural *rules and constraints* (the "how" it should be structured). -In Semantica, `SHACLGenerator` produces constraint rules (shapes) based on your ontology, and the public `run_shacl_validation` function evaluates your actual data against these rules. If a node violates a rule (e.g., missing a required property or using the wrong datatype), a detailed violation report is generated. The historical `run_shacl_validation` name remains available as a compatibility alias. +In Semantica, `SHACLGenerator` produces constraint rules (shapes) based on your ontology, and the public `run_shacl_validation` function evaluates your actual data against these rules. If a node violates a rule (e.g., missing a required property or using the wrong datatype), a detailed violation report is generated. The historical `_run_pyshacl` name remains available as a compatibility alias. ## Why Use SHACL Validation? From 6cbe0ae43846021d056d8d0e4151d817b37d80b7 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 23 Aug 2026 00:13:12 +0800 Subject: [PATCH 3/4] test(shacl): cover conforming validation result --- tests/ontology/test_ontology_advanced.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/ontology/test_ontology_advanced.py b/tests/ontology/test_ontology_advanced.py index 149a8789..7b47f3a3 100644 --- a/tests/ontology/test_ontology_advanced.py +++ b/tests/ontology/test_ontology_advanced.py @@ -549,6 +549,30 @@ class TestSHACLHierarchicalAndValidation(unittest.TestCase): self.assertEqual(legacy_report.to_dict(), public_report.to_dict()) # 34 + def test_public_run_shacl_validation_conforming_graph(self): + """The public API reports a valid graph without violations.""" + try: + import pyshacl # noqa: F401 + import rdflib # noqa: F401 + except ImportError: + self.skipTest("pyshacl/rdflib not installed") + from semantica.ontology import run_shacl_validation + + data = """ + @prefix ex: . + ex:alice a ex:Person ; ex:name "Alice" . + """ + shacl = """ + @prefix ex: . + @prefix sh: . + ex:PersonShape a sh:NodeShape ; sh:targetClass ex:Person ; + sh:property [ sh:path ex:name ; sh:minCount 1 ] . + """ + + report = run_shacl_validation(data, shacl) + + self.assertTrue(report.conforms) + self.assertEqual(report.violation_count, 0) def test_shacl_violation_to_dict(self): from semantica.ontology.ontology_validator import SHACLViolation v = SHACLViolation( From b891902d6d501df683449b41f4cff99fe59ba7eb Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Sun, 23 Aug 2026 00:15:41 +0800 Subject: [PATCH 4/4] test(shacl): compare stable report fields --- tests/ontology/test_ontology_advanced.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/ontology/test_ontology_advanced.py b/tests/ontology/test_ontology_advanced.py index 7b47f3a3..dfdde003 100644 --- a/tests/ontology/test_ontology_advanced.py +++ b/tests/ontology/test_ontology_advanced.py @@ -546,7 +546,18 @@ class TestSHACLHierarchicalAndValidation(unittest.TestCase): legacy_report = _run_pyshacl(data, shacl) self.assertFalse(public_report.conforms) self.assertEqual(public_report.violation_count, 1) - self.assertEqual(legacy_report.to_dict(), public_report.to_dict()) + self.assertEqual(legacy_report.conforms, public_report.conforms) + self.assertEqual(legacy_report.violation_count, public_report.violation_count) + self.assertEqual( + [ + (v.focus_node, v.result_path, v.constraint, v.severity, v.message) + for v in legacy_report.violations + ], + [ + (v.focus_node, v.result_path, v.constraint, v.severity, v.message) + for v in public_report.violations + ], + ) # 34 def test_public_run_shacl_validation_conforming_graph(self):