Break a graph and repair it

Experimental: this can change at any time

Violation generation is experimental. The shifty.experimental.vio Python module, its command line, its output files, and shifty_experimental::vio in Rust can change in any release, without a deprecation period. Pin the Shifty version if you depend on them.

In this tutorial you break a small, valid graph on purpose, watch the validator catch it, and then fix it with the edits that broke it. You need pip install "pyshifty[rdflib]".

The graph and the shapes

Two sensors, each in a room. The shapes say every sensor needs one string label, a unit of "degC" or "degF", and a room that is an ex:Room:


SHAPES = """
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://ex/> .

ex:SensorShape a sh:NodeShape ;
    sh:targetClass ex:Sensor ;
    sh:property ex:label, ex:unit, ex:room .

ex:label sh:path ex:label ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 .
ex:unit sh:path ex:unit ; sh:in ( "degC" "degF" ) .
ex:room sh:path ex:inRoom ; sh:class ex:Room ; sh:minCount 1 .
"""

DATA = """
@prefix ex: <http://ex/> .

ex:s1 a ex:Sensor ; ex:label "Temp 1" ; ex:unit "degC" ; ex:inRoom ex:r1 .
ex:s2 a ex:Sensor ; ex:label "Temp 2" ; ex:unit "degF" ; ex:inRoom ex:r2 .
ex:r1 a ex:Room .
ex:r2 a ex:Room .
"""

Each property shape has an IRI (ex:label, ex:unit, ex:room), so the validation reports below can name them.

1. Check that the graph conforms

vio only breaks graphs that conform. If a node already fails a shape, vio leaves it alone. So check first:

import shifty

conforms, _, text = shifty.validate(DATA, SHAPES)
print(conforms)
True

2. Draw a case

A Universe holds every way vio found to break the graph. Each way is an injection: a few edits that make one node fail one shape.

draw(2, seed=0) picks up to two injections that can be applied together:

from shifty.experimental import vio

universe = vio.Universe(DATA, SHAPES)
print(len(universe), "ways to break the graph")

case = universe.draw(2, seed=0)
for injection in case.injections:
    print(injection.focus, injection.operations[0].detail)
38 ways to break the graph
<http://ex/s1> remove type Room
<http://ex/s2> add r1 via unit

ex:r1 loses its type, so ex:s1 now points at something that is not a room. ex:s2 gets a unit that is not allowed.

The same seed gives the same case every time. Try another seed to see a different case.

3. Read the edits

case.delta holds the edits, as triples to delete and to add:

for s, p, o in sorted(case.delta.delete):
    print("-", s, p, o)
for s, p, o in sorted(case.delta.add):
    print("+", s, p, o)
- <http://ex/r1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Room>
+ <http://ex/s2> <http://ex/unit> <http://ex/r1>

4. Validate the broken graph

case.graph() is the original graph with those edits applied. Validate it:

broken = case.graph()
conforms, _, text = shifty.validate(broken, SHAPES)
print(text)
Validation Report
Conforms: False
Results (2):
Constraint Violation in ClassConstraintComponent
  Severity: sh:Violation
  Source Shape: <http://ex/room>
  Focus Node: <http://ex/s1>
  Result Path: <http://ex/inRoom>
  Value: <http://ex/r1>
  Message: Value <http://ex/r1> is not an instance of class <http://ex/Room>

Constraint Violation in InConstraintComponent
  Severity: sh:Violation
  Source Shape: <http://ex/unit>
  Focus Node: <http://ex/s2>
  Result Path: <http://ex/unit>
  Value: <http://ex/r1>
  Message: Value <http://ex/r1> is not in the list of allowed values

The case already knows which nodes should fail. Compare case.failures:

for failure in case.failures:
    print(failure.focus, failure.shape)
<http://ex/s1> http://ex/SensorShape
<http://ex/s2> http://ex/SensorShape

The two agree. Every failure a case lists has been confirmed this way.

5. Repair it

case.repair undoes case.delta. case.apply applies a set of edits to the broken graph:

repaired = case.apply(case.repair)
conforms, _, text = shifty.validate(repaired, SHAPES)
print(conforms)
True

The graph conforms again.

To score a repair tool, apply the tool’s edits instead of case.repair, and compare the result with the original:

candidate = case.apply(tool_delta)
intended = case.apply(case.repair)

Next steps