Repair a graph¶
Warning
Experimental. The repair layer is the newest and least settled part of
Shifty. Expect the API to change, and expect gaps: several constraint kinds
have no invertible form yet, sh:equals reconciliation is coarse, and
repairs edit data graphs only. Validation and inference are stable; this is
not. Call gate() to check a candidate against the whole graph before
accepting it.
Shifty synthesizes candidate edit templates for a failing node. You choose among them: which term fills a hole, how many values to add, which alternative to take, whether to accept the result — all of those are yours. The library computes; you decide. Why repair computes but does not choose argues why that line is where it is.
The input is the failure evidence from Explain a validation result; the recipes below assume you have read that.
Look at the repair space without writing code¶
shifty repair --shapes shapes.ttl --data data.ttl
<http://example.org/bob> [target: class(ex:Person)]
All — do all:
Edits:
del <http://example.org/bob> <http://example.org/name> "123"^^<http://www.w3.org/2001/XMLSchema#integer>
add <http://example.org/bob> <http://example.org/name> ?0
?0 : typed value
Repeat [1..∞]:
Edits:
add <http://example.org/bob> <http://example.org/email> ?1
?1 : any node
--stage picks which structure to print:
witness— why each focus node violates (the failure evidence).tree(default) — the synthesized repair template.solve— a concrete delta found by the built-in enumeration driver.
--format json on any of them, and --no-infer to skip SHACL-AF rules
before witnessing.
Apply a repair from the command line¶
shifty repair --shapes shapes.ttl --data data.ttl --apply
This runs the fixpoint driver and writes the repaired data graph as N-Triples
to stdout, overriding --stage. The driver fills holes by enumerating terms
already present in the graph, which is fine for inspection and rarely the
policy you want in production — it will happily satisfy ex:email with any
node that fits. Use the Python API when the choices matter.
Enumerate the failures¶
session = shifty.RepairSession(shapes, data)
for failure in session.witnesses():
print(failure.focus, failure.shape_name, failure.selector)
print(failure.explain())
witnesses() is the violation horizon: one Failure per failing
(focus, statement). An empty list means the graph conforms. To scope it to a
single shape, witnesses_for(shape_iri); for the passing side,
satisfactions_for(shape_iri) gives one Satisfaction per conforming focus
node, each recording the values matched along every checked path.
Fill in a repair template¶
tree = failure.repair_tree()
print(tree.explain())
plan = shifty.RepairPlan()
choices = {choice.node_id: choice for choice in tree.choices()}
instance = tree.instantiate(plan)
while not instance.is_complete:
for node_id in instance.open_choices:
choice = choices[node_id]
if choice.kind == shifty.ChoiceKind.Repeat:
plan.count(node_id, choice.min or 0)
else:
plan.choose(node_id, choose_branch(choice))
instance = tree.instantiate(plan)
for hole in instance.open_holes:
plan.bind(hole.id, choose_a_term(hole))
instance = tree.instantiate(plan)
choose_branch and choose_a_term stand for application policies: select
an Any branch and supply a term for a hole. Setting a Repeat count stamps
out that many copies of its body, and each copy gets fresh holes with new
ids. Resolve choices, re-instantiate, then bind the holes in that instance.
A RepairPlan is just data — choose(node_id, branch) at an alternative,
count(node_id, n) at a repeat, bind(hole_id, term) at a hole, and
clear(id) to undo one. Partial plans are legal; instantiate reports
what is still open and never validates or decides anything.
Terms passed to bind are N-Triples spellings: '"Bob"' for a string
literal, '<http://example.org/bob>' for an IRI.
Choose what goes in a hole¶
Each hole states what a legal value looks like:
for hole in tree.holes():
print(hole.id, hole.constraint)
print(hole.candidates(limit=8))
candidates() enumerates existing terms in the graph that satisfy the
constraint. It is a convenience for drivers that prefer reusing a node over
minting one; nothing requires a binding to come from it, and for a hole that
wants a fresh node it is the wrong tool.
When a hole demands conformance to a sub-shape rather than a simple type, it says so:
if hole.conforms_to is not None:
print(session.describe_shape(hole.conforms_to))
subtree = session.repair_node_against(candidate, hole.conforms_to)
describe_shape prints the sub-shape fully expanded, with every child inlined
and no @id indirection, which is what you need to understand what the hole
is actually asking for. repair_node_against synthesizes a tree that would
make a chosen node conform to it, returning None if it already does — that
is how you recurse into a nested obligation.
Propose an edit you wrote yourself¶
You are not restricted to filling templates. A driver often knows the right answer as a subgraph — a new node with its type and properties — rather than as a single term:
delta = shifty.delta_from_graph(add=my_turtle_patch)
outcome = session.gate(delta)
It gates and applies exactly like a synthesized delta. Application order is deletes first, then adds, so a triple appearing on both sides is a net add.
Check before you commit¶
outcome = session.gate(instance.delta)
outcome.fixed # violations this delta removes
outcome.introduced # NEW violations it would cause
outcome.remaining # pre-existing, still unfixed
outcome.is_progress # sound, and fixed something
The gate is whole-graph, not focus-local, because a repair that fixes one node
by breaking another is not a repair. It re-validates G ⊕ ΔG and returns the
difference. It applies nothing and decides nothing — is_progress is a
summary offered for convenience, not a verdict acted on.
Accept and iterate¶
session = session.advance(instance.delta)
advance returns a new session over the patched graph, reusing the compiled
schema and skipping re-inference. The fixpoint driver is this loop:
while True:
failures = session.witnesses()
if not failures:
break # conforms
tree = failures[0].repair_tree() # your focus-ordering policy
... # your hole policy
outcome = session.gate(instance.delta)
if not outcome.is_progress:
break # give up, or choose differently
session = session.advance(instance.delta)
Get the result out with session.to_graph() for an rdflib.Graph of the
current state, or session.apply(delta) to materialize G ⊕ ΔG without
advancing.
When there is nothing to offer¶
Some branches admit no data repair, and the tree says so rather than staying silent:
if tree.is_blocked:
print(tree.explain())
A sh:sparql constraint is opaque — an arbitrary query is not algebraically
invertible. An identity test on the focus node itself cannot be fixed by editing
data. A support reached only through a recursive back-edge has no finite set of
facts to delete. A conjunction with any blocked child is blocked; an alternative
drops its blocked branches, and is blocked only when all of them are. So a
subtree you are handed never contains a dead branch inside a live one, and a
blocked root means Shifty has no supported repair template for that focus.
The scope limit is deliberate: repairs edit the data graph, never the schema.
Widening a closed list or lowering a minCount would often be the right
fix in practice, and Shifty will not propose it.
See also¶
Repair reference — the complete object model.
Why repair computes but does not choose — repair as abduction, and the driver boundary.
python/examples/repair.pyandrepair_interactive.py— worked drivers.