SHACL Validation & Severity
Context
The PDTF v3 schemas are a constraint language: required, enum, type/format, minimum/maximum, and oneOf across 8,458 property-path entries — 935 base-schema leaves carry semantic annotation. Re-expressing the ontology as OWL alone discards this: OWL is open-world and cannot say which properties a conforming document must carry or what datatype a value must have. The closed-world contract is SHACL’s job, and Council Session 001 (Q3) was emphatic that the shapes graph is kept separate from the OWL class graph — owl:minCardinality and sh:minCount are not the same statement.
Three further problems sit on top of the mapping. Not every violation is equal: a Property with no resolvable identity key is catastrophic; an absent optional attribute is routine. Flat pass/fail buries the loudest error under noise. The schemas also drive forms — the shapes that validate should generate the BASPI/TA6 form that collects (Q5). And Cagle’s preference for inline opda:aiHint annotations clashed with Knublauch/Gandon’s refusal of any invented term a strict SHACL processor might misread as a constraint — Q5 resolved this by exiling advisory annotations to a third annotation graph (class ⊥ shapes ⊥ annotation).
Decision
Adopt severity-tiered SHACL in a separate shapes graph, with DASH rendering and a separate annotation graph: the only option that actually validates the closed-world contract (OWL cannot), surfaces the rarest and most damaging error loudest (flat severity cannot), and keeps form-hints and advisory annotations in their proper graphs without letting any invented term masquerade as a constraint.
Validation flow
Data flows from a JSON transaction document through profile-selected SHACL shapes to a structured validation report whose entries carry severity levels.
(935 annotated leaves)"]:::user SG["SHACL shapes graph
(generated, separate named graph)"]:::user DD -->|"drives sh:datatype
sh:minCount sh:in"| SG JSON["JSON transaction document"]:::process PROF["Overlay profile
(ODR-0010)"]:::user PROF -->|"selects active shapes"| SG JSON -->|"focus nodes"| PROC{"pySHACL /
TopBraid processor"}:::warning SG -->|"shapes"| PROC PROC -->|"conforms"| OK["Validation report:
no results"]:::success PROC -->|"violations"| RPT["Validation report:
sh:result entries"]:::success RPT --> V["sh:Violation
(identity / provenance breach)"]:::success RPT --> W["sh:Warning
(profile / sensitivity gap)"]:::success RPT --> I["sh:Info
(absent optional attribute)"]:::success
Rules
Constraint mapping — JSON-Schema constructs map to SHACL with datatypes and cardinalities grounded in the data dictionary:
| JSON Schema | SHACL | Source |
|---|---|---|
required | sh:minCount 1 | data dictionary requiredness on the leaf |
enum (closed) | sh:in over the SKOS scheme members (ODR-0011) | the enum’s concept scheme |
type / format | sh:datatype / sh:pattern / sh:nodeKind | data dictionary recorded type (string, integer, number, boolean, string (date), string (date-time), string (email)) |
minimum / maximum | sh:minInclusive / sh:maxInclusive | data dictionary bounds |
oneOf (discriminated) | sh:xone + sh:qualifiedValueShape on the discriminator | the oneOf branch + its discriminator leaf |
| array cardinality | sh:minCount / sh:maxCount | data dictionary array bounds |
| canonical key | dash:uniqueValueForClass (primary), owl:hasKey (secondary) | ODR-0005 key decision |
Recorded leaf types ground sh:datatype directly — dateOfBirth → xsd:date, accountNumber (recorded integer) → xsd:integer, annualGroundRent (recorded number) → xsd:decimal, emailAddress (recorded string (email)) → xsd:string with sh:pattern/sh:nodeKind. Each property shape carries dct:source to its canonical leaf path (e.g. …/pdtf-transaction.json#…/dateOfBirth) per ODR-0004.
Severity tiering (Guizzardi) — severity tracks regulatory weight, not schema nesting:
sh:Violation— breach of a Kind’s identity contract: aPropertywith no resolvable UPRN-or-equivalent key, aRegisteredTitlewith no title number, a Role instance with no founding Relator (aProprietorwith noProprietorship; aSellerwith noTransaction); an unprovenancedopda:Claimwith noprov:wasDerivedFromand no explicit “unverified” marker (Moreau, ODR-0009).sh:Warning— missing profile/disclosure constraints that overlays add; sensitivity-marker gaps on special-category or personal data (Pandit, ODR-0012 sensitivity gate).sh:Info— absent optional attributes.
The principle: the rarest, most damaging error (identity loss) must be the loudest; the routine omission must be the quietest. Enforcement: every sh:Violation shape must guard a Kind’s identity contract or an unprovenanced claim; no optional-attribute gap may carry sh:Violation.
Severity tier model
Each result entry in the validation report carries exactly one severity level; the assignment rule is determined by the nature of the breach, not by schema nesting depth.
identity contract or
unprovenanced Claim?"}:::warning D1 -->|"yes"| VIO["sh:Violation
— Property: no UPRN-or-equiv key
— RegisteredTitle: no title number
— Role: no founding Relator
— opda:Claim: no prov:wasDerivedFrom"]:::error D1 -->|"no"| D2{"Missing profile /
disclosure constraint or
sensitivity-marker gap?"}:::warning D2 -->|"yes"| WARN["sh:Warning
— overlay profile gap
— special-category /
personal-data marker absent"]:::warning D2 -->|"no"| INF["sh:Info
— absent optional attribute"]:::success
Open-world/closed-world guard (Gandon) — an owl:minCardinality in the class graph and a sh:minCount in the shapes graph are not the same statement and must not be authored on one property in the belief that they coincide. A drift check is required to catch the two falling out of step; it confirms no property carries both an owl: cardinality and a SHACL count authored as equivalent.
Class ⊥ shapes ⊥ annotation graph separation — the shapes graph is a distinct named graph from the class graph (ODR-0004). Advisory opda:aiHint-style annotations for LLM consumers do not go inline in the shapes graph (a strict processor would carry uninterpretable triples or misread them as constraints). They live in a separate annotation graph keyed to shape IRIs, consistent with the overlay annotation-graph convention in ODR-0010. Enforcement: the absence of any opda:aiHint (or other undocumented advisory term) inside the shapes graph — advisory triples resolve only in the separate annotation graph.
Three-graph separation
The class graph, shapes graph, and annotation graph are three distinct named graphs; advisory annotations never appear inside the shapes graph.
(OWL axioms,
owl:minCardinality etc.)"]:::user SHG["Shapes graph
(SHACL shapes,
sh:minCount etc.)"]:::user AG["Annotation graph
(opda:aiHint and
advisory triples,
keyed to shape IRIs)"]:::user PROC["SHACL processor"]:::process CG -..->|"NOT equivalent —
open-world vs closed-world
(drift check required)"| SHG SHG -->|"validates"| PROC AG -->|"LLM consumers
resolve here"| PROC BANNED["opda:aiHint
inline in shapes graph"]:::error BANNED -->|"PROHIBITED —
processor misreads
as constraint"| SHG
DASH rendering — dash:propertyRole/viewer/editor, sh:order/sh:group, and dash:EnumSelectEditor (fed by ODR-0011 schemes) reproduce the form on the same shapes that validate, so a loaded profile both validates a transaction and generates the form that collects it, with full dct:source traceability (Q5; see ODR-0010). DASH is acceptable because it is a documented vocabulary; undocumented terms are not.
Validation confirmation — shapes are validated with a SHACL processor (pySHACL or TopBraid) against the diagnostic exemplars (ODR-0005): a registered freehold house, an unregistered house pre-first-registration, and a flat whose UPRN was split must each produce the expected report — including a sh:Violation for the identity-key breach on the unregistered/split cases. The BASPI5 vertical slice (Q7 MVP) must round-trip: its profile shapes validate a transaction and generate the BASPI form with dct:source traceability.
Alternatives
- OWL cardinality only, no SHACL — rejected: OWL is open-world, so a missing required property is not a contradiction and nothing is actually validated; also collapses the class/shapes separation Q3 mandated.
- Flat SHACL with default severity — rejected: validates correctly but buries the catastrophic identity-loss error among trivial optional-gap reports, defeating the regulator’s need to see the catastrophic error first.
Consequences
- Generate shapes from the data dictionary’s recorded types, requiredness, and bounds — do not hand-author what is derivable (Allemang’s generator-first policy, ODR-0004).
- Maintain a standing drift check between class graph and shapes graph; the open-world/closed-world guard imposes this as ongoing cost.
- Author every
sh:Violationshape against the identity-contract or unprovenanced-claim rubric; reviewers rejectsh:Violationon optional-attribute gaps. - Keep advisory annotations out of the shapes graph; route
opda:aiHint-style triples to the annotation graph keyed to shape IRIs. Cagle’s inline-hint preference is recorded as dissent (≈7-2), not adopted. - Round-trip the BASPI5 vertical slice (Q7 MVP) end-to-end as the canonical proof: JSON → profile → rendered form + validated document with full provenance.
ODR dependency graph
This record depends on a chain of foundation decisions and in turn is implemented by downstream records.
Programme"]:::process O4["ODR-0004
Foundation"]:::process O5["ODR-0005
Identity key"]:::process O6["ODR-0006"]:::process O7["ODR-0007"]:::process O9["ODR-0009
Claims & provenance"]:::process O10["ODR-0010
Overlay profiles"]:::process O11["ODR-0011
Enumerations"]:::process O12["ODR-0012
Data governance"]:::process O15["ODR-0015"]:::process O17["ODR-0017
(implements + dep)"]:::process O18["ODR-0018"]:::process THIS["ODR-0013
SHACL Validation
& Severity"]:::info O3 --> THIS O4 --> THIS O5 --> THIS O6 --> THIS O7 --> THIS O9 --> THIS O10 --> THIS O11 --> THIS O12 --> THIS O15 --> THIS O17 --> THIS O18 --> THIS
References
- Target versions: RDF 1.2 and SHACL 1.2, per the Core-tier pin in ODR-0002.
- Vocabularies: SHACL (
sh:minCount/sh:maxCount/sh:datatype/sh:in/sh:xone/sh:qualifiedValueShape/severity); DASH (dash:propertyRole/viewer/editor/uniqueValueForClass/EnumSelectEditor); SKOS (forsh:inover the ODR-0011 schemes). - Data dictionary as input: the 935 annotated base-schema leaves in
data-dictionary.md/data-dictionary-canonical.json— recorded leaf types drivesh:datatype/sh:nodeKind; requiredness drivessh:minCount; array bounds drivesh:minCount/sh:maxCount;enumcolumns drivesh:inover the corresponding ODR-0011 scheme. - Related: anchor ODR-0003; foundation ODR-0004; identity-key source ODR-0005;
sh:inscheme source ODR-0011; sensitivity-gate source ODR-0012; overlay profiles + annotation-graph + ValidationContext ODR-0010; unprovenanced-claim violation ODR-0009. - Council deliberation: session-001 Q4 (identity-key mechanism), Q5 (overlays → SHACL, severity, aiHint resolution; Knublauch/Gandon/Guizzardi).
Comments