glam/schemas/20251121/shacl/custodian_validation_shapes.ttl
kempersc 6eb18700f0 Add SHACL validation shapes and validation script for Heritage Custodian Ontology
- Created SHACL shapes for validating temporal consistency and bidirectional relationships in custodial collections and staff observations.
- Implemented a Python script to validate RDF data against the defined SHACL shapes using the pyshacl library.
- Added command-line interface for validation with options for specifying data formats and output reports.
- Included detailed error handling and reporting for validation results.
2025-11-22 23:22:10 +01:00

407 lines
16 KiB
Turtle

@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix custodian: <https://nde.nl/ontology/hc/custodian/> .
@prefix org: <http://www.w3.org/ns/org#> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
# ============================================================================
# Heritage Custodian SHACL Validation Shapes (v1.0.0)
# ============================================================================
#
# Schema Version: v0.7.0
# Created: 2025-11-22
# Purpose: Enforce temporal consistency and bidirectional relationship constraints
#
# Validation Rules:
# 1. Collection-Unit Temporal Consistency
# 2. Collection-Unit Bidirectional Relationships
# 3. Custody Transfer Continuity
# 4. Staff-Unit Temporal Consistency
# 5. Staff-Unit Bidirectional Relationships
#
# Usage:
# pyshacl -s custodian_validation_shapes.ttl -df turtle data.ttl
#
# ============================================================================
# ============================================================================
# Rule 1: Collection-Unit Temporal Consistency
# ============================================================================
#
# Constraint: Collection custody dates must fit within managing unit's validity period
# - Collection.valid_from >= OrganizationalStructure.valid_from
# - Collection.valid_to <= OrganizationalStructure.valid_to (if unit dissolved)
custodian:CollectionUnitTemporalConsistencyShape
a sh:NodeShape ;
sh:targetClass custodian:CustodianCollection ;
sh:name "Collection-Unit Temporal Consistency" ;
sh:description "Collection custody dates must fall within managing unit's validity period" ;
# Constraint 1.1: Collection starts on or after unit founding
sh:sparql [
sh:message "Collection valid_from ({?collectionStart}) must be >= managing unit valid_from ({?unitStart})" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?collectionStart ?unitStart ?managingUnit
WHERE {
$this a custodian:CustodianCollection ;
custodian:managing_unit ?managingUnit ;
custodian:valid_from ?collectionStart .
?managingUnit a custodian:OrganizationalStructure ;
custodian:valid_from ?unitStart .
# VIOLATION: Collection starts before unit exists
FILTER(?collectionStart < ?unitStart)
}
""" ;
] ;
# Constraint 1.2: Collection ends on or before unit dissolution (if unit dissolved)
sh:sparql [
sh:message "Collection valid_to ({?collectionEnd}) must be <= managing unit valid_to ({?unitEnd}) when unit is dissolved" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?collectionEnd ?unitEnd ?managingUnit
WHERE {
$this a custodian:CustodianCollection ;
custodian:managing_unit ?managingUnit ;
custodian:valid_to ?collectionEnd .
?managingUnit a custodian:OrganizationalStructure ;
custodian:valid_to ?unitEnd .
# Unit is dissolved (valid_to is set)
FILTER(BOUND(?unitEnd))
# VIOLATION: Collection custody ends after unit dissolution
FILTER(?collectionEnd > ?unitEnd)
}
""" ;
] ;
# Warning: Collection custody ongoing but unit dissolved
sh:sparql [
sh:severity sh:Warning ;
sh:message "Collection has ongoing custody (no valid_to) but managing unit was dissolved on {?unitEnd} - missing custody transfer?" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?managingUnit ?unitEnd
WHERE {
$this a custodian:CustodianCollection ;
custodian:managing_unit ?managingUnit .
# Collection has no end date (ongoing custody)
FILTER NOT EXISTS { $this custodian:valid_to ?collectionEnd }
# But unit is dissolved
?managingUnit a custodian:OrganizationalStructure ;
custodian:valid_to ?unitEnd .
}
""" ;
] .
# ============================================================================
# Rule 2: Collection-Unit Bidirectional Relationships
# ============================================================================
#
# Constraint: If collection.managing_unit = unit, then unit.managed_collections must include collection
custodian:CollectionUnitBidirectionalShape
a sh:NodeShape ;
sh:targetClass custodian:CustodianCollection ;
sh:name "Collection-Unit Bidirectional Relationship" ;
sh:description "Collection → unit relationship must have inverse unit → collection relationship" ;
sh:sparql [
sh:message "Collection references managing_unit {?unit} but unit does not list collection in managed_collections" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?unit
WHERE {
$this a custodian:CustodianCollection ;
custodian:managing_unit ?unit .
?unit a custodian:OrganizationalStructure .
# VIOLATION: Unit does not reference collection back
FILTER NOT EXISTS {
?unit custodian:managed_collections $this
}
}
""" ;
] .
# ============================================================================
# Rule 3: Custody Transfer Continuity
# ============================================================================
#
# Constraint: Custody transfers must be continuous (no gaps or overlaps)
# - If collection has multiple custody events, end date of previous custody = start date of next custody
custodian:CustodyTransferContinuityShape
a sh:NodeShape ;
sh:targetClass custodian:CustodianCollection ;
sh:name "Custody Transfer Continuity" ;
sh:description "Custody transfers must be continuous with no gaps or overlaps" ;
# Check for gaps in custody chain
sh:sparql [
sh:severity sh:Warning ;
sh:message "Custody gap detected: previous custody ended on {?prevEnd} but next custody started on {?nextStart} (gap: {?gapDays} days)" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?prevEnd ?nextStart ?gapDays
WHERE {
$this a custodian:CustodianCollection ;
custodian:custody_history ?event1 ;
custodian:custody_history ?event2 .
# First custody period
?event1 custodian:new_custodian ?prevCustodian ;
custodian:transfer_date ?prevEnd .
# Second custody period (chronologically after)
?event2 custodian:new_custodian ?nextCustodian ;
custodian:transfer_date ?nextStart .
# Ensure events are different and chronologically ordered
FILTER(?event1 != ?event2)
FILTER(?nextStart > ?prevEnd)
# Calculate gap in days
BIND((xsd:date(?nextStart) - xsd:date(?prevEnd)) AS ?gapDays)
# WARNING: Gap > 1 day
FILTER(?gapDays > 1)
}
""" ;
] ;
# Check for overlaps in custody chain
sh:sparql [
sh:message "Custody overlap detected: collection managed by {?custodian1} until {?end1} and simultaneously by {?custodian2} from {?start2}" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?custodian1 ?end1 ?custodian2 ?start2
WHERE {
$this a custodian:CustodianCollection ;
custodian:custody_history ?event1 ;
custodian:custody_history ?event2 .
# First custody period
?event1 custodian:new_custodian ?custodian1 ;
custodian:transfer_date ?start1 .
# Assume custody continues until next transfer (or infer end date)
OPTIONAL { ?event1 custodian:custody_end_date ?end1 }
# Second custody period
?event2 custodian:new_custodian ?custodian2 ;
custodian:transfer_date ?start2 .
# Ensure different events and different custodians
FILTER(?event1 != ?event2)
FILTER(?custodian1 != ?custodian2)
# VIOLATION: Second custody starts before first custody ends
FILTER(BOUND(?end1) && ?start2 < ?end1)
}
""" ;
] .
# ============================================================================
# Rule 4: Staff-Unit Temporal Consistency
# ============================================================================
#
# Constraint: Staff employment dates must fit within organizational unit's validity period
# - PersonObservation.employment_start_date >= OrganizationalStructure.valid_from
# - PersonObservation.employment_end_date <= OrganizationalStructure.valid_to (if unit dissolved)
custodian:StaffUnitTemporalConsistencyShape
a sh:NodeShape ;
sh:targetClass custodian:PersonObservation ;
sh:name "Staff-Unit Temporal Consistency" ;
sh:description "Staff employment dates must fall within organizational unit's validity period" ;
# Constraint 4.1: Staff employment starts on or after unit founding
sh:sparql [
sh:message "Staff employment_start_date ({?employmentStart}) must be >= unit valid_from ({?unitStart})" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?employmentStart ?unitStart ?unit
WHERE {
$this a custodian:PersonObservation ;
custodian:unit_affiliation ?unit ;
custodian:employment_start_date ?employmentStart .
?unit a custodian:OrganizationalStructure ;
custodian:valid_from ?unitStart .
# VIOLATION: Employment starts before unit exists
FILTER(?employmentStart < ?unitStart)
}
""" ;
] ;
# Constraint 4.2: Staff employment ends on or before unit dissolution (if unit dissolved)
sh:sparql [
sh:message "Staff employment_end_date ({?employmentEnd}) must be <= unit valid_to ({?unitEnd}) when unit is dissolved" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?employmentEnd ?unitEnd ?unit
WHERE {
$this a custodian:PersonObservation ;
custodian:unit_affiliation ?unit ;
custodian:employment_end_date ?employmentEnd .
?unit a custodian:OrganizationalStructure ;
custodian:valid_to ?unitEnd .
# Unit is dissolved (valid_to is set)
FILTER(BOUND(?unitEnd))
# VIOLATION: Employment ends after unit dissolution
FILTER(?employmentEnd > ?unitEnd)
}
""" ;
] ;
# Warning: Staff employment ongoing but unit dissolved
sh:sparql [
sh:severity sh:Warning ;
sh:message "Staff has ongoing employment (no employment_end_date) but unit was dissolved on {?unitEnd} - missing employment termination?" ;
sh:prefixes custodian: ;
sh:select """
SELECT $this ?unit ?unitEnd
WHERE {
$this a custodian:PersonObservation ;
custodian:unit_affiliation ?unit .
# Staff has no end date (ongoing employment)
FILTER NOT EXISTS { $this custodian:employment_end_date ?employmentEnd }
# But unit is dissolved
?unit a custodian:OrganizationalStructure ;
custodian:valid_to ?unitEnd .
}
""" ;
] .
# ============================================================================
# Rule 5: Staff-Unit Bidirectional Relationships
# ============================================================================
#
# Constraint: If person.unit_affiliation = unit, then unit.staff_members must include person
custodian:StaffUnitBidirectionalShape
a sh:NodeShape ;
sh:targetClass custodian:PersonObservation ;
sh:name "Staff-Unit Bidirectional Relationship" ;
sh:description "Person → unit relationship must have inverse unit → person relationship" ;
sh:sparql [
sh:message "Person references unit_affiliation {?unit} but unit does not list person in staff_members" ;
sh:prefixes custodian:, org: ;
sh:select """
SELECT $this ?unit
WHERE {
$this a custodian:PersonObservation ;
custodian:unit_affiliation ?unit .
?unit a custodian:OrganizationalStructure .
# VIOLATION: Unit does not reference person back
# Check both custodian:staff_members and org:hasMember (they are equivalent)
FILTER NOT EXISTS {
{ ?unit custodian:staff_members $this }
UNION
{ ?unit org:hasMember $this }
}
}
""" ;
] .
# ============================================================================
# Additional Shapes: Cardinality and Type Constraints
# ============================================================================
# Ensure managing_unit is always an OrganizationalStructure
custodian:CollectionManagingUnitTypeShape
a sh:NodeShape ;
sh:targetClass custodian:CustodianCollection ;
sh:name "Collection managing_unit Type Constraint" ;
sh:property [
sh:path custodian:managing_unit ;
sh:class custodian:OrganizationalStructure ;
sh:message "managing_unit must be an instance of OrganizationalStructure" ;
] .
# Ensure unit_affiliation is always an OrganizationalStructure
custodian:PersonUnitAffiliationTypeShape
a sh:NodeShape ;
sh:targetClass custodian:PersonObservation ;
sh:name "Person unit_affiliation Type Constraint" ;
sh:property [
sh:path custodian:unit_affiliation ;
sh:class custodian:OrganizationalStructure ;
sh:message "unit_affiliation must be an instance of OrganizationalStructure" ;
] .
# Ensure valid_from is a date or datetime
custodian:DatetimeFormatShape
a sh:NodeShape ;
sh:targetSubjectsOf custodian:valid_from, custodian:valid_to,
custodian:employment_start_date, custodian:employment_end_date ;
sh:name "Datetime Format Constraint" ;
sh:property [
sh:path custodian:valid_from ;
sh:or (
[ sh:datatype xsd:date ]
[ sh:datatype xsd:dateTime ]
) ;
sh:message "valid_from must be xsd:date or xsd:dateTime" ;
] ;
sh:property [
sh:path custodian:valid_to ;
sh:or (
[ sh:datatype xsd:date ]
[ sh:datatype xsd:dateTime ]
) ;
sh:message "valid_to must be xsd:date or xsd:dateTime" ;
] ;
sh:property [
sh:path custodian:employment_start_date ;
sh:or (
[ sh:datatype xsd:date ]
[ sh:datatype xsd:dateTime ]
) ;
sh:message "employment_start_date must be xsd:date or xsd:dateTime" ;
] ;
sh:property [
sh:path custodian:employment_end_date ;
sh:or (
[ sh:datatype xsd:date ]
[ sh:datatype xsd:dateTime ]
) ;
sh:message "employment_end_date must be xsd:date or xsd:dateTime" ;
] .
# ============================================================================
# End of SHACL Shapes
# ============================================================================